Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
- **Local-model Claude agents now launch in a lean, isolated mode.** CoS agents on the Ollama-backed Claude providers (`claude-ollama`, `claude-ollama-tui`) previously inherited your full personal Claude Code environment — hooks, plugins, MCP servers, global CLAUDE.md — which reliably derailed small local models (a live eval showed qwen3.6:35b answering a session-start hook instead of its task). These sessions now spawn with `--bare --strict-mcp-config` (no hooks, no plugins, no MCP tool schemas, no CLAUDE.md auto-discovery), authenticating through the provider's existing `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` (pointed at your local Ollama), and get the same plain-`git`/`gh` completion workflow OpenCode agents use instead of slashdo commands bare mode can't load. In the same eval, once isolated this way qwen3.6:35b wrote a correct, test-passing helper (verified 46/46) end to end — the failures were the environment, not the model.
- **Every Claude Code agent now separates its task from the PortOS operating contract.** For all Claude providers (TUI and headless CLI, local and hosted), the PortOS coordination boilerplate — worktree branch, completion-sentinel protocol, git/PR workflow — now rides in a real system prompt via `--append-system-prompt-file` (saved beside the agent's `prompt.txt` as `system-prompt.md`), while the pasted/stdin prompt carries only the task. The model weights the contract as standing instructions rather than conversation, and hosted providers get better prompt-cache reuse on the stable system block. The prompt content is byte-identical to before — only its placement changed. Codex, Antigravity, and OpenCode agents (no equivalent flag) keep the combined prompt unchanged.

- **Partially-completed issues no longer become un-claimable "zombies," and a new scheduled task heals any that already exist.** When a claim run shipped only PART of an issue's scope and merged the PR, the issue was left OPEN with its `in-progress` label (and assignee) still on — and since the claim queue treats `in-progress` as "claimed and being worked," the remaining scope was never re-picked and never finished (real cases: #2220, #2179, #2175). The completion flow was binary (fully-done → close + unlabel) with no "partial ship" path, and the branch reconciler is branch-centric so it never touched the linked issue. Two fixes: **(1)** the `claim-issue` / `-gitlab` / `-jira` prompts now choose the PR trailer deliberately (`Closes` for a full ship, `Refs` + a `## Remaining` section for a partial one) and Phase 7 reconciles the issue with an agent-judged hybrid — close + file a scoped follow-up when the remainder is a clean separable chunk, else post a `Done ✓ / Remaining ▢` comment and release the `in-progress` claim so the queue re-picks it (JIRA records remaining scope + files a follow-up ticket). **(2)** a new opt-in per-app `issue-reconcile` CoS task (sibling to `branch-reconcile`, disabled by default) runs a deterministic `gh`/`git` scan for zombie issues — open + `in-progress` yet with their PR merged and no live claim anywhere (no open PR, no local/remote/CoS claim branch, no active agent) — and dispatches a coordinator agent that applies the same hybrid per issue, with an `autoClose` toggle (OFF ⇒ comment + release only) and follow-up dedup so two federated peers can't file duplicates. Perpetual drain with a convergence signature, exactly like branch-reconcile. (`server/services/issueReconcile.js`, `server/services/cosTaskGenerator.js`, `server/services/taskSchedule.js`, `server/lib/cosValidation.js`, `server/services/taskPromptDefaults/`)

## Creative Writing Quality Engine

- **[issue-2165] Editorial review now catches recognizable "AI slop" writing tells automatically.** Four new deterministic editorial checks join the existing 68: a tiered banned-word scan (hard-bans like "delve"/"myriad"/"utilize"; suspicious words like "robust"/"seamless" only flagged when 3+ cluster in one paragraph), fiction-specific AI-tell idioms ("a sense of dread", "couldn't help but", "eyes widened", "a wave of relief washed over him"), rhetorical structural tics ("not just X, but Y", runs of "did not [verb]", punchy three-sentence-in-a-row bursts like "Fast. Precise. Deadly."), and structural burstiness (em-dash overuse, essay-style transition-word openers, mechanically uniform paragraph lengths). Each surfaces as an editorial finding with the exact offending quote, same as the existing prose checks — configurable thresholds and house-style allowlists included.
Expand Down
84 changes: 84 additions & 0 deletions docs/plans/2026-07-06-issue-reconcile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Issue Reconcile — kill zombie issues, self-heal partial ships

**Date:** 2026-07-06
**Status:** approved → implementing
**Related:** #2220 (closed, follow-up #2241), #2179, #2175 (released); epic tie-in with the branch-reconcile CoS task (#2237/#2240)

## Problem

An issue that is only PARTIALLY completed by a merged PR becomes a **zombie**:
`OPEN` + `in-progress` label + assignee, with its PR already merged. The claim
queue treats `in-progress` as "claimed and being worked" and skips it, so the
remaining scope is never re-claimed and never completed.

Root cause — the completion flow has no "partial completion" state:

1. **`/do:next` Phase 7** (`taskPromptDefaults/prompts.js`) is *binary*: it verifies
the issue auto-closed via a `Closes #NUM` trailer, and if still open closes it +
removes `in-progress`. There is no branch for "I shipped part of the scope." The
only pre-ship escape (`needs-input`, `:558`) releases *without shipping*.
2. **Branch reconciler** (`branchReconcile.js`) is entirely branch-centric — after it
merges an `IN_REVIEW` PR it deletes the branch/worktree but **never touches the
linked issue's label or state**.

So a sensible partial ship with `Refs #NUM` (not `Closes`) merged via the
reconciler/swarm path → zombie. Live examples: #2220, #2179, #2175.

## Design

Agent-judged **hybrid** partial-ship policy, applied in two places.

### Part A — teach the completion flow "partial ship" (future direct claims)

`claim-issue`, `claim-issue-gitlab`, `claim-issue-jira` Phase 5/7:

- Phase 5: the PR body uses `Closes #NUM` only when the PR fully satisfies the
issue; a partial ship uses `Refs #NUM` + a "Remaining" section.
- Phase 7: after merge, decide *did this PR fully satisfy the issue?*
- **Yes** → existing close + remove `in-progress`.
- **No, remainder is a clean separable chunk** → close original (comment:
shipped ✓ / moved to #NEW), file a scoped follow-up (`plan` + inherited area
labels, `Refs #orig`).
- **No, remainder is a continuation of the same scope** → keep open, post
`Done ✓ / Remaining ▢` comment, remove `in-progress` + assignee → re-claimable.

Prompt-version discipline (CLAUDE.md "Distribution model"): bump `PROMPT_VERSIONS`
for all three keys, append the outgoing bodies verbatim to
`PREVIOUS_DEFAULT_PROMPTS`, regenerate `integrity.snapshot.json`.

### Part B — new `issue-reconcile` per-app CoS task (self-heal + reconciler path)

Mirrors the `branch-reconcile` shape:

- **`server/services/issueReconcile.js`** (pure classifier + `gh` gatherers, unit-tested):
scan open + `in-progress` issues; classify each `ZOMBIE` iff a linked **merged**
PR references it (head ref encodes `issue-<num>`, or body mentions `#<num>`) **and**
there is no **live** claim (no open PR / local / remote / `cos/*/issue-<num>/*`
branch for it) **and** no active CoS agent. Emit a `zombieSignature` convergence
guard so the perpetual drain parks instead of looping.
- **`cosTaskGenerator.js`**: deterministic pre-step + dispatch block parallel to the
branch-reconcile block — zombies → dispatch a coordinator agent that reads each
issue+PR and applies the SAME hybrid decision; none → park on `recheckCron`.
- **Registration**: `SELF_IMPROVEMENT_TASK_TYPES`, `DEFAULT_TASK_INTERVALS`
(PERPETUAL, disabled, `recheckCron: '0 4 * * *'`, `taskMetadata.autoClose: true`),
`MANAGED_AGENT_OPTIONS` (lock `useWorktree`/`openPR` off — the coordinator works
over `gh`, no worktree), the task-description map, `PROMPT_VERSIONS`
(`issue-reconcile: 1`), `ALLOWED_TASK_METADATA_KEYS` (`autoClose`), and the new
coordinator prompt body.
- **Disabled by default** → no migration needed (new opt-in task).
- **`autoClose` toggle** (default ON): when OFF the coordinator never closes an
issue, only comments + unlabels — a safety valve for users who prefer one thread
per feature.

**Peer safety** (differs from branch-reconcile, which is local-ref-only): issue
state is shared GitHub state across federated peers (void/null/NaN/undefined).
Close/unlabel are idempotent; the one real race is *filing a duplicate follow-up
from two machines*, so the coordinator dedupes by searching for an existing
follow-up (`Refs #orig` marker) before filing. v1 pre-step is `gh`-only and parks
on non-GitHub apps; GitLab/JIRA siblings are deferred (see PLAN.md).

## Part C — heal the three current zombies (done)

- **#2220** — separable remainder → **closed**, follow-up **#2241** filed.
- **#2179**, **#2175** — continuation → kept open, `Done/Remaining` comment,
`in-progress` + assignee removed.
13 changes: 8 additions & 5 deletions server/lib/cosValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,16 @@ export const PIPELINE_BEHAVIOR_FLAGS = ['useWorktree', 'openPR', 'simplify', 're
export const MAX_TOTAL_SPAWNS = 5;

// `cleanupMerged` / `openPr` / `resolveConflicts` / `autoMerge` are the
// per-app action toggles for the `branch-reconcile` task type (each ON unless
// explicitly false). They live in the shared task-metadata allowlist — like
// `prAuthorFilter` / `issueAuthorFilter` — so a per-app override can disable an
// individual rectification behavior and survive sanitizeTaskMetadata.
// per-app action toggles for the `branch-reconcile` task type; `autoClose` is
// the `issue-reconcile` toggle (ON unless explicitly false — OFF forbids the
// coordinator from closing an issue or filing a follow-up, leaving it to only
// comment + release the claim). Each lives in the shared task-metadata
// allowlist — like `prAuthorFilter` / `issueAuthorFilter` — so a per-app
// override can disable an individual rectification behavior and survive
// sanitizeTaskMetadata.
const ALLOWED_TASK_METADATA_KEYS = [
...PIPELINE_BEHAVIOR_FLAGS, 'readOnly',
'cleanupMerged', 'openPr', 'resolveConflicts', 'autoMerge'
'cleanupMerged', 'openPr', 'resolveConflicts', 'autoMerge', 'autoClose'
];

// pr-watcher author-gate values. 'self' = PRs opened by the gh-authenticated
Expand Down
66 changes: 61 additions & 5 deletions server/services/cosTaskGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1582,11 +1582,13 @@ export async function generateManagedAppImprovementTaskForType(taskType, app, st
// tick retries instead of waiting out a full recheck cadence.
// The detector keys on the RESOLVED promptTaskType so a claim-work router run
// probes the concrete tracker (claim-issue → GitHub issues, plan-task → PLAN.md).
// branch-reconcile is PERPETUAL but is NOT gated by the generic work-detector
// registry — its "detector" and its actual work are the SAME scan (the
// deterministic reconcile below), so splitting them would double the git/gh
// I/O. Its dedicated block further down does the park/clear itself.
if (interval.type === taskSchedule.INTERVAL_TYPES.PERPETUAL && taskType !== 'branch-reconcile') {
// branch-reconcile and issue-reconcile are PERPETUAL but are NOT gated by the
// generic work-detector registry — for each, the "detector" and the actual
// work are the SAME scan (the deterministic reconcile below), so splitting them
// would double the git/gh I/O. Their dedicated blocks further down do the
// park/clear themselves.
if (interval.type === taskSchedule.INTERVAL_TYPES.PERPETUAL
&& taskType !== 'branch-reconcile' && taskType !== 'issue-reconcile') {
const { detectActionableWork } = await import('./perpetualWork.js');
const detection = await detectActionableWork(promptTaskType, app, {
issueAuthorFilter: metadata.issueAuthorFilter || 'self'
Expand Down Expand Up @@ -1682,6 +1684,59 @@ export async function generateManagedAppImprovementTaskForType(taskType, app, st
emitLog('info', `🔀 branch-reconcile dispatching for ${app.name}: ${actionable.length} in-flight branch(es)`, { appId: app.id, analysisType: taskType });
}

// issue-reconcile: deterministic pre-step — scan the app's GitHub repo for
// ZOMBIE issues (open + `in-progress` yet with their PR merged and no live
// claim anywhere) and hand the set to the coordinator agent. Same perpetual
// drain shape as branch-reconcile: the scan IS the work-detector (gh/git only,
// no LLM), and the coordinator dispatches only while zombies remain — else it
// PARKS on the daily recheckCron. `{zombieIssues}` carries the set into the
// prompt. No worktree: the coordinator mutates issue state over `gh` only.
let zombieIssuesBlock = '';
if (taskType === 'issue-reconcile') {
const { reconcile, zombieSignature, formatZombiesForPrompt } = await import('./issueReconcile.js');
// The live-claim guard (open PR + local/remote/CoS claim branch, scanned via
// for-each-ref inside reconcile) already covers every agent past Phase 2 of
// the claim flow — the point at which it creates its claim/worktree branch.
// The sub-minute pre-branch window is left to the coordinator's per-zombie
// re-verification, so no separate active-agent set is threaded here.
const autoClose = metadata.autoClose !== false;
const result = await reconcile(app.repoPath).catch((err) => {
emitLog('warn', `issue-reconcile pre-step failed for ${app.name}: ${err.message}`, { appId: app.id });
return null;
});
// null = non-GitHub remote OR transient gh failure → skip WITHOUT parking so
// the next tick retries (gh/git-only cost), mirroring branch-reconcile.
if (!result) return null;
if (result.stalled.length) {
// In-progress issues with NO merged PR and NO live claim — a different stuck
// state (claimed, nothing shipped) that issue-reconcile deliberately does
// NOT auto-heal. Surface them so they're not silently ignored.
emitLog('info', `🧟 issue-reconcile ${app.name}: ${result.stalled.length} stalled in-progress issue(s) with no merged PR (left for human/branch-reconcile)`, { appId: app.id, analysisType: taskType });
}
if (result.zombies.length === 0) {
await taskSchedule.parkPerpetual(taskType, app.id, { reason: 'no-zombie-issues', actionableCount: 0, signature: null });
emitLog('info', `🧟 issue-reconcile parked for ${app.name}: no zombie issues`, { appId: app.id });
return null;
}
// Convergence guard — identical to branch-reconcile's: a productive run closes
// or releases zombies (changing the signature) → keep draining; an unchanged
// signature (coordinator errored, or a case it punted to a human) → park on
// the recheck cadence and CLEAR the signature so the next daily recheck
// re-drives unconditionally rather than re-parking forever.
const signature = zombieSignature(result.zombies);
const lastSignature = await taskSchedule.getPerpetualSignature(taskType, app.id);
if (signature === lastSignature) {
await taskSchedule.parkPerpetual(taskType, app.id, { reason: 'no-progress', actionableCount: result.zombies.length, signature: null });
emitLog('info', `🧟 issue-reconcile parked for ${app.name}: ${result.zombies.length} zombie issue(s) unchanged since last run (no progress — will re-drive on next recheck)`, { appId: app.id });
return null;
}
await taskSchedule.clearPerpetualPark(taskType, app.id);
await taskSchedule.setPerpetualSignature(taskType, app.id, signature);
metadata.perpetual = true;
zombieIssuesBlock = formatZombiesForPrompt(result.zombies, { fullName: result.fullName, autoClose });
emitLog('info', `🧟 issue-reconcile dispatching for ${app.name}: ${result.zombies.length} zombie issue(s)`, { appId: app.id, analysisType: taskType });
}

// Honor a direct claim-work prompt customization if the user set one;
// otherwise delegate to the resolved tracker's prompt body via
// getTaskPrompt(promptTaskType), which reads THAT type's interval.prompt
Expand Down Expand Up @@ -1846,6 +1901,7 @@ export async function generateManagedAppImprovementTaskForType(taskType, app, st
.replace(/\{referenceData\}/g, () => referenceDataBlock)
.replace(/\{prData\}/g, () => prDataBlock)
.replace(/\{inFlightBranches\}/g, () => inFlightBranchesBlock)
.replace(/\{zombieIssues\}/g, () => zombieIssuesBlock)
.replace(/\{repoFullName\}/g, () => prRepoFullName)
.replace(/\{defaultBranch\}/g, () => prDefaultBranch)
.replace(/\{planConstraint\}/g, () => planConstraintBlock);
Expand Down
Loading