diff --git a/.changelog/next/fixed-issue-4185.md b/.changelog/next/fixed-issue-4185.md new file mode 100644 index 0000000000..96f986bec4 --- /dev/null +++ b/.changelog/next/fixed-issue-4185.md @@ -0,0 +1 @@ +- Claim and planning tasks no longer risk pushing work directly to their default branch. diff --git a/server/services/taskPromptDefaults.test.js b/server/services/taskPromptDefaults.test.js index 9d77d4b5a2..ac4f924476 100644 --- a/server/services/taskPromptDefaults.test.js +++ b/server/services/taskPromptDefaults.test.js @@ -218,10 +218,10 @@ describe('taskPromptDefaults integrity snapshot', () => { // declared the reviewer unavailable, and merged its PR on a self-review. Every // claim/plan prompt that enumerates the CLI reviewers must name the binary. it.each([ - ['plan-task', 13], - ['claim-issue', 11], - ['claim-issue-gitlab', 10], - ['claim-issue-jira', 8], + ['plan-task', 14], + ['claim-issue', 12], + ['claim-issue-gitlab', 11], + ['claim-issue-jira', 9], ])('%s v%d names the antigravity reviewer\'s `agy` binary, preserving the pre-`agy` default', (key, version) => { const current = DEFAULT_TASK_PROMPTS[key]; expect(PROMPT_VERSIONS[key]).toBe(version); @@ -245,6 +245,38 @@ describe('taskPromptDefaults integrity snapshot', () => { expect(preAgy).not.toBe(current); }); + // A branch created from a remote default-branch ref normally inherits that + // ref as its upstream. The claim flows later derive their push destination + // from the branch config, so that inherited upstream could send claim work + // directly to the default branch instead of its PR branch. Keep these four + // commands untracked until their explicit `git push -u` phase establishes + // the correct upstream. dependency-updates intentionally differs: it starts + // from the bot PR head, where tracking the existing PR branch is correct. + it.each([ + 'plan-task', + 'claim-issue', + 'claim-issue-gitlab', + 'claim-issue-jira', + ])('%s creates a no-track claim worktree and preserves the outgoing default', (key) => { + const current = DEFAULT_TASK_PROMPTS[key]; + const worktreeCommands = current.match(/^git(?: -C \{repoPath\})? worktree add\b.*$/gm) || []; + + expect(worktreeCommands).toHaveLength(1); + expect(worktreeCommands.every((command) => command.includes('--no-track'))).toBe(true); + + const outgoing = PREVIOUS_DEFAULT_PROMPTS[key].at(-1); + expect(outgoing).toMatch(/\bworktree add -b\b/); + expect(outgoing).not.toContain('--no-track'); + expect(outgoing).not.toBe(current); + }); + + it('keeps dependency-update worktrees tracking their PR head', () => { + const current = DEFAULT_TASK_PROMPTS['dependency-updates']; + + expect(current).toContain('worktree add -b dep-{appName}-pr-'); + expect(current).not.toContain('--no-track'); + }); + // Changelog instructions defer to the convention the repo documents rather // than prescribing an append to `.changelog/NEXT.md`. PortOS (and any repo // that adopts the same shape) collects per-branch fragments so parallel @@ -304,7 +336,7 @@ describe('taskPromptDefaults integrity snapshot', () => { it.each([ ['claim-issue', 'gh issue close', 'gh issue edit "${NUM}" --add-label needs-input'], ['claim-issue-gitlab', 'glab issue close', 'glab issue update "${NUM}" --label needs-input'], - ])('%s converges every Phase-3 release, preserving the outgoing default', (key, closeCommand, parkCommand) => { + ])('%s converges every Phase-3 release, preserving the pre-convergence default', (key, closeCommand, parkCommand) => { const current = DEFAULT_TASK_PROMPTS[key]; const phase3 = phaseSection(current, 3); // The already-fixed/superseded branch CLOSES rather than releasing open… @@ -321,13 +353,15 @@ describe('taskPromptDefaults integrity snapshot', () => { // the issue open and unlabeled — it must be gone, not merely qualified. expect(phase3).not.toContain('If ANY of these are true, release the claim and re-pick'); - // The outgoing default is preserved verbatim so installs holding it are - // recognized and auto-upgraded rather than read as user customizations. - const outgoing = PREVIOUS_DEFAULT_PROMPTS[key][PREVIOUS_DEFAULT_PROMPTS[key].length - 1]; - const outgoingPhase3 = phaseSection(outgoing, 3); - expect(outgoingPhase3).toContain('If ANY of these are true, release the claim and re-pick'); - expect(outgoingPhase3).not.toContain(closeCommand); - expect(outgoing).not.toBe(current); + // Later prompt revisions append their own outgoing defaults, so identify + // the pre-convergence body by its Phase-3 behavior rather than array slot. + const preConvergence = PREVIOUS_DEFAULT_PROMPTS[key].findLast( + (body) => phaseSection(body, 3).includes('If ANY of these are true, release the claim and re-pick'), + ); + expect(preConvergence).toBeDefined(); + const preConvergencePhase3 = phaseSection(preConvergence, 3); + expect(preConvergencePhase3).not.toContain(closeCommand); + expect(preConvergence).not.toBe(current); }); // JIRA has no labels, so its converging vocabulary is status: an already-fixed @@ -335,7 +369,7 @@ describe('taskPromptDefaults integrity snapshot', () => { // status behind a Review Hub todo. Transitioning back to a not-started status // is the JIRA shape of the same bug — Phase 1's not-started-only filter // re-picks it immediately. - it('claim-issue-jira converges every Phase-3 release, preserving the outgoing default', () => { + it('claim-issue-jira converges every Phase-3 release, preserving the pre-convergence default', () => { const current = DEFAULT_TASK_PROMPTS['claim-issue-jira']; const phase3 = phaseSection(current, 3); expect(phase3).toContain('CONVERGING status'); @@ -348,10 +382,13 @@ describe('taskPromptDefaults integrity snapshot', () => { expect(phase3).toContain('NOT back to a not-started status'); expect(phase3).not.toContain('If ANY of these are true, release the claim and re-pick'); - const previous = PREVIOUS_DEFAULT_PROMPTS['claim-issue-jira']; - const outgoing = previous[previous.length - 1]; - expect(phaseSection(outgoing, 3)).toContain('transition the ticket back to its not-started status'); - expect(outgoing).not.toBe(current); + // Newer prompt revisions append another outgoing default, so retain this + // historical assertion by its Phase-3 behavior rather than its array slot. + const preConvergence = PREVIOUS_DEFAULT_PROMPTS['claim-issue-jira'].findLast( + (body) => phaseSection(body, 3).includes('transition the ticket back to its not-started status'), + ); + expect(preConvergence).toBeDefined(); + expect(preConvergence).not.toBe(current); }); // release-check READS the changelog rather than writing it, so its fix is the diff --git a/server/services/taskPromptDefaults/integrity.snapshot.json b/server/services/taskPromptDefaults/integrity.snapshot.json index 87c957b487..6cf559c2e6 100644 --- a/server/services/taskPromptDefaults/integrity.snapshot.json +++ b/server/services/taskPromptDefaults/integrity.snapshot.json @@ -12,10 +12,10 @@ "mobile-responsive": "16d8e7f63a673a2de48994c0ac08ef3d", "ux": "e03d03fc7c16faa5b5db93be2896b9ad", "feature-ideas": "d3282f16da29efe2d53595b3b34778bc", - "plan-task": "bbc5495a02102fd470d2a429eedfbe76", - "claim-issue": "d5465bd5d6fa1cd979746b99a881dba9", - "claim-issue-gitlab": "8e8f795bb75e996b80992883983a3c08", - "claim-issue-jira": "927766326e492e4c9c2566534f465512", + "plan-task": "4df36bed4974ff41f369c86828e7f94f", + "claim-issue": "0ffd94e6c7966e5ce457effea63f71d3", + "claim-issue-gitlab": "da0a8fbb33ac274a3b2efd968e302a1a", + "claim-issue-jira": "364ecf5feb38ec1f7ada50a7bcbf3199", "code-reviewer-review": "1cd13ee5142d130f95cc390098d0f278", "code-reviewer-implement": "ab2c3e16d54e195bdf856574b7702042", "error-handling": "9afba2e99b90a8d5771c75e7f0615d96", @@ -36,10 +36,10 @@ }, "PROMPT_VERSIONS": { "feature-ideas": 10, - "plan-task": 13, - "claim-issue": 11, - "claim-issue-gitlab": 10, - "claim-issue-jira": 8, + "plan-task": 14, + "claim-issue": 12, + "claim-issue-gitlab": 11, + "claim-issue-jira": 9, "pr-reviewer": 3, "code-reviewer-a": 1, "code-reviewer-b": 1, @@ -82,7 +82,8 @@ "2189019880ee12a8ba4d7975e7afc646", "dced853944c2cad8fc0c151dea71301a", "8f5b69fd9336b17184e7f7989f97def5", - "808140457aff9ff344ddac426e0022ed" + "808140457aff9ff344ddac426e0022ed", + "bbc5495a02102fd470d2a429eedfbe76" ], "pr-reviewer": [ "9ceeed08f238b3787fc1201a0ce8e023", @@ -100,7 +101,8 @@ "3f5cc58943bc08cbbc00f836660ecdfc", "97a3994ce4da2d2743bdffec110a7482", "e3d1a2e884a2a49c306a586089f4f376", - "367c22d2587212c9c391f50d146948e8" + "367c22d2587212c9c391f50d146948e8", + "927766326e492e4c9c2566534f465512" ], "claim-issue-gitlab": [ "271ad9c6efa9dce424103711292fd055", @@ -111,7 +113,8 @@ "c9888390df157b72741aafb9f2f9d8e4", "d7b44882f4d47ae44d39989e29e5fe2b", "4f323dddadb5c7f0047e35914ba80529", - "12d78a91126213f7c10e9a6725cdb464" + "12d78a91126213f7c10e9a6725cdb464", + "8e8f795bb75e996b80992883983a3c08" ], "claim-issue": [ "89308f712f99ad8134224639cd99f135", @@ -123,7 +126,8 @@ "e9039e0ee911636db2927fe239280f35", "dc985be3422c9fdee07d92a059666f0a", "ed113d11f477bf1b50cd5a48364b9da9", - "9b90d29048e85c9db640b1db643a08c8" + "9b90d29048e85c9db640b1db643a08c8", + "d5465bd5d6fa1cd979746b99a881dba9" ], "security": [ "d0f54c1fbe62d067b1a23ee0a9f3528c" diff --git a/server/services/taskPromptDefaults/previousDefaults.js b/server/services/taskPromptDefaults/previousDefaults.js index 01349a3917..d92e9d18e4 100644 --- a/server/services/taskPromptDefaults/previousDefaults.js +++ b/server/services/taskPromptDefaults/previousDefaults.js @@ -2028,6 +2028,197 @@ If \`git branch -d\` refuses (the PR squash-merged on GitHub but local doesn't k **Do NOT \`git pull\` from inside this phase** (no \`--rebase\`, no \`--autostash\`, no plain \`pull\`). The agent's work is already integrated on GitHub via \`gh pr merge\`; pulling locally provides no functional benefit and risks rebasing the user's in-progress branch / shuffling their uncommitted changes through stash if the source repo HEAD happens to be on a tracking feature branch when the agent runs. Leave the user's working tree alone. +_(Phase 3b is defined above, right after Phase 3 — see the "alternative exit from Phase 3" section.)_`, + // v13 default (pre-`--no-track`, bumped to v14) + `[Plan Task: {appName}] Claim and ship next PLAN.md item + +Pick the next available unclaimed PLAN.md item by its \`[]\` ID, **create your own worktree at \`claim/\`**, implement, ship a PR, and clean up. Mirrors the \`/claim\` slash command — same in-flight scan, same branch naming, same no-local-merge cleanup. **YOU pick the item in Phase 1 — the scheduler does not reserve one for you.** Picking at execution time and immediately creating the \`claim/\` branch **narrows** the window for two concurrent runs to collide on the same slug — it does NOT eliminate it: two runs can still complete Phase 1 before either creates a branch, then race at \`git worktree add\`. That race is handled in Phase 2 — the loser re-picks the next item. (A dispatch-time pre-pick is strictly worse: it commits both runs to the same slug long before any branch exists.) Do NOT modify files in the source repo directly; ALL editing happens inside the worktree you create. + +**How claiming works.** Every PLAN.md checkbox carries a \`[]\` ID. A slug is "in flight" when it appears as the slug-position segment in either a \`claim/\` ref (the human/TUI pattern) or a \`cos///\` ref (the CoS sub-agent pattern) — across local branches, remote branches, or open PR head refs. The \`claim/\` branch you create IS the claim, visible to every other agent and to the human running \`/claim\` in a TUI. + +## Phase 1 — Pick the target slug + +Run steps 1–5 in order. + +1. Read PLAN.md from the repo root. +2. **If any \`- [ ]\` line lacks an \`[]\` ID, stop and exit cleanly** — \`do-replan\` populates IDs in one pass; without IDs, this task has nothing to claim. +3. Build the in-flight set. Collect every ref from these sources: + \`\`\`bash + git fetch --prune 2>/dev/null + git branch -a --no-color --format='%(refname:short)' + gh pr list --state open --json headRefName -q '.[].headRefName' 2>/dev/null + \`\`\` + For each ref, extract the slug **only when the ref matches one of these documented patterns** (after stripping any leading remote prefix like \`origin/\` or \`upstream/\`): + - \`claim/\` — the slug is everything after \`claim/\`. + - \`cos///\` — the slug is the third \`/\`-separated segment. + + A slug is "in flight" iff it appears in a ref matching one of those patterns AND is present in PLAN.md. **Do NOT** flag a slug just because the bare word appears as some other segment of a ref — that would falsely flag any slug literally named \`main\`, \`fix\`, \`feature\`, \`release\`, \`dev\`, etc. against virtually every branch in the repo. +4. **Pick the target slug:** walk PLAN.md top-to-bottom and pick the FIRST \`- [ ]\` line where ALL of the following are true: + - The slug is NOT in the in-flight set. + - The immediately-preceding line does NOT start with \`> ⚠️ DRIFT:\`. + - The line does NOT carry the \`\` annotation. +5. **If no eligible item exists**, exit cleanly — that's a healthy plan state, not a failure. Brainstorming is handled by the \`feature-ideas\` task; do NOT add new items here. + +Capture the exact text of the selected item (without the leading \`- [ ]\`) verbatim, **including its \`[]\` ID** — the changelog entry will reuse both. + +## Phase 2 — Claim (worktree) + +Create the worktree on a branch named \`claim/\`. This branch name is the claim — once created and pushed, no other agent or \`/claim\` session will pick the same slug. Do all editing inside the worktree, NEVER in the source repo's working tree (which may have the user's in-flight work). + +\`\`\`bash +SLUG= +WORKTREE="{worktreesRoot}/claim-\${SLUG}" +mkdir -p {worktreesRoot} +git fetch origin main +git worktree add -b "claim/\${SLUG}" "\${WORKTREE}" origin/main +cd "\${WORKTREE}" +\`\`\` + +**If the worktree-creation command fails because the claim/ branch already exists** (a concurrent run won the branch-creation race, or a remote claim/ is now visible), do NOT force or reuse it — that branch IS another run's claim. Treat the slug as in-flight, return to Phase 1, and pick the next eligible item; if nothing else is eligible, exit cleanly. + +Stash the worktree path; you'll need it for Phase 7 cleanup. + +## Phase 3 — Verify still valid + +Before writing any code, sanity-check that executing the item won't regress newer work. **If ANY of these are true, jump to Phase 3b** (clarification path, not implementation): + +- The picked line is preceded by a \`> ⚠️ DRIFT:\` blockquote (you should already have filtered it; double-check). +- The item description references a function, file, or component that no longer exists. Run \`grep -rn\` for the named identifiers — if they're gone, the item is stale. +- The item depends on a predecessor that hasn't shipped (e.g. "Phase B work" when Phase B isn't done). +- The work would require touching files outside the inferred scope (>5 unrelated files), suggesting the item is bigger than originally estimated. + +Otherwise: **ambiguity is not a reason to jump to Phase 3b — decide.** If the item merely leaves a design choice unstated or is open to more than one reasonable reading, pick the most reasonable interpretation, note the approach you chose in the commit/PR, and proceed to Phase 4. Jump to Phase 3b ONLY when proceeding would be destructive/irreversible, or genuinely requires the human — specific hardware/credentials you don't have, or a judgment only they can make (the same narrow bar as a \`blocked\` item). The user would rather iterate on top of a shipped best-guess than have the item parked waiting on a decision they didn't ask to make. + +## Phase 3b — Request Clarification (alternative exit from Phase 3) + +Done from INSIDE the worktree (you've already created \`claim/\` in Phase 2): + +1. Create \`.plan-questions.md\` in the worktree: + \`\`\` + # Plan Question: + + ## PLAN.md Item + ]> + + ## Questions + - + - + \`\`\` +2. **Move the unchecked item to the bottom of PLAN.md and annotate it with \` \`** — remove from its current position and append at the end with the annotation, **preserving the \`[]\` ID**. This keeps the queue moving so the next \`plan-task\` run picks a different actionable item. +3. Commit, push the branch (\`git push -u origin claim/\`), and open a PR with \`gh pr create\` so the user can see the questions. **Do NOT merge** — the user resolves \`.plan-questions.md\` first. +4. Then run the **Phase 3b cleanup** (which differs from Phase 7 — the PR is intentionally unmerged here, so the local branch must NOT be deleted): + \`\`\`bash + cd {repoPath} + git worktree remove "\${WORKTREE}" + \`\`\` + Leave the local \`claim/\` branch alone — \`git branch -d\` will refuse (PR not merged) and \`-D\` would discard work that's still in flight. The branch lives on locally and remotely until the user resolves the questions and the PR merges; \`git branch -d "claim/"\` becomes safe only after that point. + +After Phase 3b runs, **exit** — do NOT proceed to Phase 4. The implementing path resumes only when the user reopens the slug post-clarification. + +## Phase 4 — Implement + +Write the code, tests, and any docs the item requires. Follow the repo conventions in CLAUDE.md (no try/catch in route handlers, functional programming, Zod validation, Tailwind tokens, reactive UI updates). + +Run the relevant test suite as you go. + +**Commit messages reference the slug** so the work is grep-able across the changelog, branches, and PR titles: + +\`\`\` +([]): + + +\`\`\` + +Use \`feat:\` / \`fix:\` / \`refactor:\` / \`chore:\` / etc. (The bracketed-scope form \`([])\` is intentional and matches the project's existing convention — grep \`git log --oneline\` for prior examples. The brackets carry the PLAN.md \`[]\` ID syntax through to commits, branches, and PRs so a single slug grep finds the whole trail.) + +## Phase 5 — Update PLAN.md and the changelog + +**Remove the item from PLAN.md outright.** The audit trail for shipped work lives in \`git log\` and the project's changelog (however that repo keeps it) — do NOT archive to a \`DONE.md\`, that file has been retired. Do NOT leave a checked \`- [x]\` behind in PLAN.md. + +1. Remove the picked \`- [ ]\` line from PLAN.md entirely. If removing it leaves a heading empty, leave the heading alone — section curation is \`do-replan\`'s job. +2. Record the shipped item in the repo's changelog, **following the convention that repo documents** — read its \`CLAUDE.md\` and changelog README (e.g. \`.changelog/README.md\`) BEFORE writing anything. Some repos collect per-branch fragments in a directory (e.g. \`.changelog/next/\`) via a helper script rather than appending to one shared file, precisely so parallel agents don't conflict on every merge. When such a convention is documented, use it — run the documented command and remember the fragment file it created as \`CHANGELOG_FILE\`. + + Only when no convention is documented, detect a changelog file (in this order — pick the first match) and append an entry there: + - \`.changelog/NEXT.md\` (staged-release file) + - \`CHANGELOG.md\` at repo root with an \`## Unreleased\` or \`## [Unreleased]\` heading + - any other \`changelog\`-shaped file the repo already maintains (look at recent \`git log\` for examples of where prior entries landed) + + Either way, mirror the prose style of recent entries; lead with the slug in brackets so \`git log --grep=''\` and changelog greps line up: + + \`\`\`markdown + - **[] ** — <1–3 sentences on what shipped, key files touched, any caveats> + \`\`\` + + Remember the exact path you wrote to as \`CHANGELOG_FILE\` — you'll stage it in step 3. If the repo has no changelog at all, skip this step and leave \`CHANGELOG_FILE\` unset; the commit message + \`git log\` becomes the audit trail. + +3. Stage PLAN.md plus the changelog file you actually edited (if any) and commit. **Do NOT use a glob or a swallow-on-failure fallback** — staging the exact file you edited is what keeps the audit trail honest: + + \`\`\`bash + git add PLAN.md + [ -n "$CHANGELOG_FILE" ] && git add "$CHANGELOG_FILE" + git commit -m "docs([<slug>]): remove from PLAN.md and log to changelog" + \`\`\` + +## Phase 6 — Review and ship + +The configured reviewers for this task, in order, are \`{reviewers}\`. \`copilot\` waits for GitHub's auto-review; \`claude\` / \`codex\` / \`antigravity\` (CLI binary: \`agy\`) / \`grok\` invoke a local-CLI critique. When more than one is configured, run each in the listed order before merging — this mirrors slashdo's \`/do:pr --review-with <list>\` (the lone default \`copilot\` needs no flag; multi-reviewer runs may also carry \`--review-stop-on-*\` / \`--reviewer-applies\`). + +1. **Self-review your diff for reuse, quality, and efficiency** (DRY, dead code, naming, simpler equivalents, missed edge cases) and fix findings in the same diff — BEFORE opening the PR, not retroactively. Claude Code runs this as the three-agent \`/simplify\` pass; on other CLIs, do the equivalent review by hand. +2. Push the branch: \`git push -u origin claim/<slug>\` +3. Open the PR with \`gh pr create\` — title MUST encode the slug: \`<type>([<slug>]): <description>\`. Body should summarize what shipped + test plan. +4. **Wait for each configured reviewer's findings BEFORE merging.** \`gh pr merge --auto\` only waits for required status checks; it does NOT wait for code-review feedback. Run the reviewers in the listed order (\`{reviewers}\`); for each one, apply the matching handling below before advancing to the next: + + - **\`copilot\`** — This repo has GitHub Copilot Code Review configured to auto-run on every new PR. Poll until Copilot's review lands or a 10-minute timeout fires: + \`\`\`bash + PR=<num> + # Match both forms: GraphQL/\`gh pr view\` returns the login without \`[bot]\`; + # the REST request-a-reviewer endpoint requires the \`[bot]\` suffix. Future + # GitHub API changes could flip which form callers see — accept either. + for i in $(seq 1 20); do + REVIEW=$(gh pr view "$PR" --json reviews \\ + -q '.reviews[] | select(.author.login | test("^copilot-pull-request-reviewer(\\\\[bot\\\\])?$")) | {state, submittedAt}') + [ -n "$REVIEW" ] && break + sleep 30 + done + \`\`\` + - **No review within 10 min**: proceed to merge (Copilot was slow or skipped). + - **\`APPROVED\` with no inline comments**: proceed to merge. + - **\`COMMENTED\` or \`CHANGES_REQUESTED\`**: fetch findings with \`gh api "repos/{owner}/{repo}/pulls/$PR/comments"\` (\`gh\` substitutes \`{owner}\`/\`{repo}\` from the current git checkout — those are gh path-placeholders, not prompt template vars) and \`gh pr view "$PR" --json reviews\`. Address each finding inside the worktree, commit, \`git push\`. Re-poll — Copilot re-reviews the new head SHA. Cap re-iterations at **3 rounds**; if findings keep arriving past that, exit to the **review-stuck cleanup** below — do NOT route to Phase 3b. Phase 3b is reserved for items that are blocked on *requirements clarification* and would inappropriately mutate PLAN.md and write \`.plan-questions.md\` for a review-feedback stall. + + **Review-stuck cleanup** (exit after 3 rounds of unresolved review feedback): add one final PR comment via \`gh pr comment $PR\` summarizing what was addressed across the rounds and what's still outstanding so the human picks up cold, then run the worktree-only cleanup (same shape as Phase 3b's, since the PR remains open and unmerged): + \`\`\`bash + cd {repoPath} + git worktree remove "\${WORKTREE}" + \`\`\` + Leave the local \`claim/<slug>\` branch and the open PR alone. Do NOT run Phase 7 — that phase assumes a merged PR. PLAN.md and the changelog were already updated in Phase 5, and that's fine even though the merge didn't happen: the next \`plan-task\` run will see the slug as in-flight via the open PR and pick a different item. + + - **\`claude\` / \`codex\` / \`antigravity\` (CLI binary: \`agy\`) / \`grok\`** — Invoke that CLI in headless mode against the PR diff to critique it, apply the fixes, run tests, and re-push. Iterate until the CLI reports no further blocking findings, then advance to the next configured reviewer (or merge if it's the last). Cap at **3 rounds** here — this inline claim flow is intentionally more conservative than the dedicated CoS-spawned review-loop follow-up (which allows up to 10 iterations per reviewer); after 3 rounds, leave the PR for human follow-up. A configured CLI reviewer whose binary is not on PATH is UNSATISFIED, not clean — that is a broken setup, not a passing review. Do NOT substitute your own self-review and do NOT merge; comment on the PR naming the missing command and stop. + +5. **Merge via \`gh pr merge\`** — NEVER a local \`git merge\` into main or any other branch. The repo may allow only one of \`--merge\` / \`--squash\` / \`--rebase\`, so don't hardcode a method. Try in this order and use the first one that succeeds: + \`\`\`bash + gh pr merge <num> --auto --delete-branch \\ + || gh pr merge <num> --merge --delete-branch \\ + || gh pr merge <num> --squash --delete-branch \\ + || gh pr merge <num> --rebase --delete-branch + \`\`\` + \`--auto\` lets GitHub apply the repo's configured default once required checks pass; the explicit-method fallbacks cover repos that disallow auto-merge or restrict to a single method. \`--delete-branch\` removes the remote branch atomically on merge. + +## Phase 7 — Clean up (post-merge ONLY) + +This phase runs only after the PR was merged via Phase 6. If you exited via Phase 3b instead, you already did the 3b-specific cleanup — do NOT also run Phase 7. + +From the **source repo** (cd back to {repoPath} first; you are currently inside the worktree): + +\`\`\`bash +cd {repoPath} +git worktree remove "\${WORKTREE}" +git branch -d "claim/\${SLUG}" +\`\`\` + +If \`git branch -d\` refuses (the PR squash-merged on GitHub but local doesn't know yet), use \`-D\` — the PR is confirmed merged via Phase 6, so the local branch is genuinely redundant. + +**Do NOT \`git pull\` from inside this phase** (no \`--rebase\`, no \`--autostash\`, no plain \`pull\`). The agent's work is already integrated on GitHub via \`gh pr merge\`; pulling locally provides no functional benefit and risks rebasing the user's in-progress branch / shuffling their uncommitted changes through stash if the source repo HEAD happens to be on a tracking feature branch when the agent runs. Leave the user's working tree alone. + _(Phase 3b is defined above, right after Phase 3 — see the "alternative exit from Phase 3" section.)_`, ], 'pr-reviewer': [ @@ -2930,10 +3121,122 @@ The audit trail is the merged MR/PR + \`git log\`. Detect the forge from the git ## Phase 6 — Review and clean up -The configured reviewers for this task, in order, are \`{reviewers}\`. \`claude\` / \`codex\` / \`antigravity\` / \`grok\` invoke a local-CLI critique. (\`copilot\` is GitHub-only — skip it on GitLab.) When more than one is configured, run each in the listed order. +The configured reviewers for this task, in order, are \`{reviewers}\`. \`claude\` / \`codex\` / \`antigravity\` / \`grok\` invoke a local-CLI critique. (\`copilot\` is GitHub-only — skip it on GitLab.) When more than one is configured, run each in the listed order. + +1. **Self-review your diff for reuse, quality, and efficiency** (DRY, dead code, naming, simpler equivalents, missed edge cases) and fix findings in the same diff. Claude Code runs this as the three-agent \`/simplify\` pass; on other CLIs, do the equivalent by hand. +2. **Run each configured CLI reviewer** headless against the MR/PR diff, apply fixes, run tests, re-push — capped at 3 rounds — then advance to the next reviewer. +3. **Leave the MR/PR open for a human to land.** Unlike the GitHub/GitLab issue flows, this flow does NOT auto-merge — the ticket is "In Review" and a human reviews + merges. After the reviewers finish, run the worktree-only cleanup so the source checkout is left clean: + \`\`\`bash + cd {repoPath} + git worktree remove "\${WORKTREE}" + \`\`\` + Leave the local \`claim/<KEY>\` branch and the open MR/PR in place. **Do NOT \`git merge\`, \`gh pr merge\`, \`glab mr merge\`, or \`git pull\`** — landing the change and moving the ticket to Done is the human's call. + +## Phase 7 — Report + +Generate a short summary: the ticket KEY + summary worked, the MR/PR URL, the ticket's FINAL JIRA status (confirm it is "In Review"), reviewers run, and any status transition that failed and why.`, + + // v6 default prompt — pre-changelog-fragment-deference (issue #3998) + `[Claim Issue: {appName}] Claim and ship the next ready JIRA ticket + +Pick the next ready JIRA ticket assigned to me in the current sprint, move it to **In Progress**, **create your own worktree at \`claim/<KEY>\`**, implement it, open a merge/pull request that references the ticket, move the ticket to **In Review**, and clean up. This is the \`/claim --issues\` flow for JIRA: same self-managed worktree and no-local-merge cleanup, but the work source is the app's **JIRA** project (via the PortOS JIRA API) and the ticket *status* — not an assignee/label — is the claim. **YOU pick the ticket in Phase 1.** Do NOT modify files in the source repo directly; ALL editing happens inside the worktree you create. + +All PortOS API calls below are relative to this base URL: ${PORTOS_API_URL} + +**How claiming works.** A ticket is "in flight" when (a) a \`claim/<KEY>\` or \`cos/<task>/<KEY>/<agent>\` ref exists across local branches, remote branches, or open MR/PR source refs, OR (b) its JIRA status is anything other than a not-started status (so "In Progress", "In Review", "Done", etc. are already taken). Moving the ticket to **In Progress** + the \`claim/<KEY>\` branch ARE the claim, visible to every other agent and to the human looking at the sprint board. + +## Phase 1 — Pick the target ticket + +Run steps 1–5 in order. + +1. Resolve the app's JIRA config: GET ${PORTOS_API_URL}/api/apps and find the app whose \`id\` is \`{appId}\`. Read \`jira.enabled\`, \`jira.instanceId\`, and \`jira.projectKey\`. If \`jira.enabled\` is not true or either id is missing, exit cleanly — this task only works against JIRA-configured apps. +2. Fetch my current-sprint tickets: GET ${PORTOS_API_URL}/api/jira/instances/<instanceId>/my-sprint-tickets/<projectKey>. This returns the tickets assigned to me in the active sprint. +3. Build the in-flight set from git refs: + \`\`\`bash + cd {repoPath} + git fetch --prune 2>/dev/null + git branch -a --no-color --format='%(refname:short)' + \`\`\` + For each ref (after stripping any leading \`origin/\` / \`upstream/\` prefix), extract the ticket KEY **only when the ref matches** \`claim/<KEY>\` (the segment after \`claim/\`) or \`cos/<task>/<KEY>/<agent>\` (the \`<KEY>\` third segment). A KEY looks like \`PROJ-1234\`. +4. **Pick the target ticket:** walk the sprint tickets and pick the FIRST where ALL of the following are true (prefer higher priority — Blocker/Highest/High — then oldest): + - Its status is a not-started status (e.g. "To Do", "Open", "Backlog", "Selected for Development", "Ready"). Skip tickets already "In Progress", "In Review", "Done", or any closed/resolved status — those are claimed or finished. + - Its KEY is NOT in the in-flight set from step 3. + - It has enough of a summary/description to act on. A ticket that merely leaves a design choice unstated is still eligible — you'll decide the reading in Phase 3, not skip it here. Skip only a ticket with essentially no actionable content (bare title, no description or acceptance criteria). + - It is NOT an Epic (issue type "Epic", or a title ending in "(epic)"). Leave epics for a human to split. +5. **If no eligible ticket exists**, exit cleanly — an empty actionable queue is a healthy state, not a failure. If a ticket is in the sprint but too vague or blocked to start, create a Review Hub todo (POST ${PORTOS_API_URL}/api/review/todo with title "[<KEY>] Needs clarification" or "[<KEY>] Blocked" and a description of what's missing) instead of claiming it. + +Capture the ticket KEY as \`KEY\`, its summary, and its full description — you'll reuse them in the branch, the MR/PR, and the commit trailer. + +## Phase 2 — Claim (In Progress + worktree) + +First move the ticket to **In Progress** (this is the claim, and it must happen BEFORE you start coding), then create the worktree. Transition names vary per project workflow, so resolve the id dynamically: + +1. GET ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>/transitions to list available transitions. +2. Pick the transition whose target status best matches "In Progress" (e.g. "In Progress", "Start Progress", "Start Work"); match case-insensitively. If none clearly matches, pick the transition that moves the ticket out of the To Do / Backlog column. +3. POST ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>/transition with body {"transitionId": "<id>"}. If this fails (the status is unreachable), the ticket may have been claimed concurrently — return to Phase 1 and pick the next eligible ticket. + +Then create the worktree on a branch named \`claim/<KEY>\`. Do all editing inside the worktree, NEVER in the source repo's working tree: + +\`\`\`bash +KEY=<picked-key> +DEFAULT_BRANCH="$(git -C {repoPath} symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@')" +DEFAULT_BRANCH="\${DEFAULT_BRANCH:-main}" +WORKTREE="{worktreesRoot}/claim-\${KEY}" +mkdir -p "{worktreesRoot}" +git -C {repoPath} fetch origin "\${DEFAULT_BRANCH}" +git -C {repoPath} worktree add -b "claim/\${KEY}" "\${WORKTREE}" "origin/\${DEFAULT_BRANCH}" +cd "\${WORKTREE}" +\`\`\` + +**If \`git worktree add\` fails because the \`claim/<KEY>\` branch already exists** (a concurrent run won the race), do NOT force or reuse it — that branch IS another run's claim. Move the ticket back to its prior status if you transitioned it, treat the ticket as in-flight, return to Phase 1, and pick the next eligible ticket. Stash \`WORKTREE\` — you'll need it for Phase 6 cleanup. + +## Phase 3 — Verify still valid + +Re-read the ticket (GET ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>) before writing any code. **If ANY of these are true, release the claim and re-pick** (transition the ticket back to its not-started status, remove the worktree, return to Phase 1): + +- The ticket references a function, file, or component that no longer exists (\`grep -rn\` the named identifiers — if they're gone, it's stale). +(A too-large scope is NOT in this release-and-re-pick list — releasing it just lets the next pass re-pick the same oversized ticket. It has its own park path below.) + +**A genuinely too-large ticket needs parking so the queue converges.** If the work is bigger than one coherent claim — it would touch files far outside the ticket's scope (>5 unrelated files) — and you can't carve a valuable standalone slice to ship first, splitting the omnibus is a human call: create a Review Hub todo (POST ${PORTOS_API_URL}/api/review/todo with title "[<KEY>] Needs clarification" and the split you'd suggest), transition the ticket to a **Blocked/On Hold status if the workflow has one — NOT back to a not-started status**, which would re-queue it under Phase 1 (ticket selection never consults Review Hub todos, so a not-started ticket is immediately re-eligible and the run would re-park it every pass); if the workflow has no held status, leave it **In Progress**. Phase 1's not-started-only filter keeps a non-not-started ticket out of the ready queue, and the Review Hub todo is the durable human signal — together they let the claim run converge. Then remove the worktree and exit. + +**Ambiguity is NOT a release trigger — decide, don't defer.** If the ticket is merely open to more than one reasonable reading, or leaves a design choice unstated, do NOT bail to a "Needs clarification" todo. Pick the most reasonable interpretation, record the approach you chose in a ticket comment (POST ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>/comments) so the decision is on the record, and implement it. The user would rather iterate on top of a shipped best-guess than have the ticket parked waiting on a decision they didn't ask to make. Reserve the "Needs clarification" todo for cases where proceeding would be **destructive or irreversible**, or genuinely requires the human: specific hardware/credentials you don't have, or a judgment only they can make. In those cases only, create the todo (POST ${PORTOS_API_URL}/api/review/todo with title "[<KEY>] Needs clarification" and what a human must resolve), transition the ticket to a **Blocked/On Hold status if the workflow has one — NOT back to a not-started status** (which would re-queue it under Phase 1, since ticket selection never consults Review Hub todos); if the workflow has no held status, leave it **In Progress** so Phase 1's not-started-only filter excludes it. Then remove the worktree and **exit — do NOT proceed to Phase 4**. + +Do NOT leave an *unexplained* ticket in "In Progress" with no branch — that is the zombie to avoid. A deliberate park above is different: it always carries a Review Hub todo saying why (and a Blocked/On Hold status where the workflow supports one), so it reads as intentionally held, not abandoned. + +## Phase 4 — Implement + +Write the code, tests, and any docs the ticket requires. Follow the repo conventions in CLAUDE.md (no try/catch in route handlers, functional programming, Zod validation, Tailwind tokens, reactive UI updates). Run the relevant test suite as you go. + +**Roll discovered backbone work INTO this MR/PR** — small supporting helpers, refactors, and tests the fix depends on belong here, not a follow-up. Only defer genuinely-large adjacent work. + +Commit with a conventional message referencing the ticket so the trail is grep-able: + +\`\`\` +<type>: <one-line description> (<KEY>) +\`\`\` + +## Phase 5 — Open the MR/PR and move to In Review + +The audit trail is the merged MR/PR + \`git log\`. Detect the forge from the git origin and use the matching CLI (\`gh\` for GitHub, \`glab\` for GitLab). + +1. If the repo maintains a changelog (\`.changelog/NEXT.md\`, or a \`## Unreleased\` section in \`CHANGELOG.md\`), append a one-line entry mirroring the repo's existing prose style. Otherwise skip it. +2. Push the branch: \`git push -u origin "claim/\${KEY}"\` +3. Open the MR/PR. Reference the JIRA \`KEY\` in the title and description (there is NO \`Closes\` auto-close for JIRA). Summarize what shipped + a short test plan. + - GitHub: \`gh pr create --fill --head "claim/\${KEY}"\` (then edit the body to mention \`KEY\` if \`--fill\` didn't). + - GitLab: \`glab mr create --fill --source-branch "claim/\${KEY}" --target-branch "\${DEFAULT_BRANCH}" --yes\`. + Capture the MR/PR URL as \`PR_URL\`. +4. **Move the ticket to "In Review" — REQUIRED, not optional. Do not finish while it is still "In Progress":** + - GET ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>/transitions again (transitions change once In Progress). + - Pick the transition whose target status best matches "In Review" (e.g. "In Review", "Code Review", "Review", "Ready for Review"); match case-insensitively. + - POST ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>/transition with body {"transitionId": "<id>"}. +5. Add the MR/PR link to the ticket: POST ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>/comments with body {"comment": "Implementation complete. MR/PR: \${PR_URL}\\n\\nReady for code review."}. **If you shipped only PART of the ticket's scope** (a valuable slice with real work remaining), make the comment a \`Done ✓ / Remaining ▢\` summary so the remaining scope is not lost when a human lands the MR/PR; when that remainder is a clean, separable chunk, ALSO file a new follow-up ticket for it (POST ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets with a summary + a description referencing \`KEY\`) so it re-enters the sprint queue. If a transition in step 4 failed (status unreachable), say so in this comment AND in the Phase 6 summary — do not silently drop it. + +## Phase 6 — Review and clean up + +The configured reviewers for this task, in order, are \`{reviewers}\`. \`claude\` / \`codex\` / \`antigravity\` (CLI binary: \`agy\`) / \`grok\` invoke a local-CLI critique. (\`copilot\` is GitHub-only — skip it on GitLab.) When more than one is configured, run each in the listed order. 1. **Self-review your diff for reuse, quality, and efficiency** (DRY, dead code, naming, simpler equivalents, missed edge cases) and fix findings in the same diff. Claude Code runs this as the three-agent \`/simplify\` pass; on other CLIs, do the equivalent by hand. -2. **Run each configured CLI reviewer** headless against the MR/PR diff, apply fixes, run tests, re-push — capped at 3 rounds — then advance to the next reviewer. +2. **Run each configured CLI reviewer** headless against the MR/PR diff, apply fixes, run tests, re-push — capped at 3 rounds — then advance to the next reviewer. A configured CLI reviewer whose binary is not on PATH is UNSATISFIED, not clean — that is a broken setup, not a passing review. Do NOT substitute your own self-review; comment on the MR/PR naming the missing command and stop. 3. **Leave the MR/PR open for a human to land.** Unlike the GitHub/GitLab issue flows, this flow does NOT auto-merge — the ticket is "In Review" and a human reviews + merges. After the reviewers finish, run the worktree-only cleanup so the source checkout is left clean: \`\`\`bash cd {repoPath} @@ -2945,7 +3248,7 @@ The configured reviewers for this task, in order, are \`{reviewers}\`. \`claude\ Generate a short summary: the ticket KEY + summary worked, the MR/PR URL, the ticket's FINAL JIRA status (confirm it is "In Review"), reviewers run, and any status transition that failed and why.`, - // v6 default prompt — pre-changelog-fragment-deference (issue #3998) + // v7 default prompt — pre-converging-Phase-3-release (issue #4106) `[Claim Issue: {appName}] Claim and ship the next ready JIRA ticket Pick the next ready JIRA ticket assigned to me in the current sprint, move it to **In Progress**, **create your own worktree at \`claim/<KEY>\`**, implement it, open a merge/pull request that references the ticket, move the ticket to **In Review**, and clean up. This is the \`/claim --issues\` flow for JIRA: same self-managed worktree and no-local-merge cleanup, but the work source is the app's **JIRA** project (via the PortOS JIRA API) and the ticket *status* — not an assignee/label — is the claim. **YOU pick the ticket in Phase 1.** Do NOT modify files in the source repo directly; ALL editing happens inside the worktree you create. @@ -3028,7 +3331,7 @@ Commit with a conventional message referencing the ticket so the trail is grep-a The audit trail is the merged MR/PR + \`git log\`. Detect the forge from the git origin and use the matching CLI (\`gh\` for GitHub, \`glab\` for GitLab). -1. If the repo maintains a changelog (\`.changelog/NEXT.md\`, or a \`## Unreleased\` section in \`CHANGELOG.md\`), append a one-line entry mirroring the repo's existing prose style. Otherwise skip it. +1. If the repo maintains a changelog, record a one-line entry **following the convention that repo documents** — read its \`CLAUDE.md\` and changelog README (e.g. \`.changelog/README.md\`) first. Some repos collect per-branch fragments in a directory (e.g. \`.changelog/next/\`) via a helper script rather than appending to one shared file, precisely so parallel agents don't conflict on every merge; use that flow when it's documented. Fall back to appending to the unreleased section (\`.changelog/NEXT.md\`, or \`## Unreleased\` in \`CHANGELOG.md\`) in the repo's existing prose style only when no convention is documented. Otherwise skip it. 2. Push the branch: \`git push -u origin "claim/\${KEY}"\` 3. Open the MR/PR. Reference the JIRA \`KEY\` in the title and description (there is NO \`Closes\` auto-close for JIRA). Summarize what shipped + a short test plan. - GitHub: \`gh pr create --fill --head "claim/\${KEY}"\` (then edit the body to mention \`KEY\` if \`--fill\` didn't). @@ -3056,8 +3359,7 @@ The configured reviewers for this task, in order, are \`{reviewers}\`. \`claude\ ## Phase 7 — Report Generate a short summary: the ticket KEY + summary worked, the MR/PR URL, the ticket's FINAL JIRA status (confirm it is "In Review"), reviewers run, and any status transition that failed and why.`, - - // v7 default prompt — pre-converging-Phase-3-release (issue #4106) + // v8 default (pre-`--no-track`, bumped to v9) `[Claim Issue: {appName}] Claim and ship the next ready JIRA ticket Pick the next ready JIRA ticket assigned to me in the current sprint, move it to **In Progress**, **create your own worktree at \`claim/<KEY>\`**, implement it, open a merge/pull request that references the ticket, move the ticket to **In Review**, and clean up. This is the \`/claim --issues\` flow for JIRA: same self-managed worktree and no-local-merge cleanup, but the work source is the app's **JIRA** project (via the PortOS JIRA API) and the ticket *status* — not an assignee/label — is the claim. **YOU pick the ticket in Phase 1.** Do NOT modify files in the source repo directly; ALL editing happens inside the worktree you create. @@ -3113,10 +3415,12 @@ cd "\${WORKTREE}" ## Phase 3 — Verify still valid -Re-read the ticket (GET ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>) before writing any code. **If ANY of these are true, release the claim and re-pick** (transition the ticket back to its not-started status, remove the worktree, return to Phase 1): +Re-read the ticket (GET ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>) before writing any code. **Every exit from this phase must leave a CONVERGING status on the ticket — Done/Closed, or a held status backed by a Review Hub todo.** Transitioning back to a not-started status is NOT an exit: Phase 1's not-started-only filter makes the ticket immediately re-eligible again, so the next pass re-picks it and burns another no-op run — every pass, forever. -- The ticket references a function, file, or component that no longer exists (\`grep -rn\` the named identifiers — if they're gone, it's stale). -(A too-large scope is NOT in this release-and-re-pick list — releasing it just lets the next pass re-pick the same oversized ticket. It has its own park path below.) +- **Already fixed, superseded, or duplicated by another ticket** — a comment says so, or the change it asks for is already on the default branch. Post a ticket comment naming the PR/MR, commit, or ticket that already delivered it (POST ${PORTOS_API_URL}/api/jira/instances/<instanceId>/tickets/<KEY>/comments), transition the ticket to **Done/Closed**, remove the worktree, and return to Phase 1. **Evidence gate: if you cannot name the PR/MR, commit, or ticket that delivered it, this branch does NOT apply** — closing on a hunch destroys live work, which is far worse than one wasted pass. Treat the ticket as real work and continue to Phase 4. +- **Stale reference** — the ticket names a function, file, or component that no longer exists (\`grep -rn\` the named identifiers; if they're gone, it's stale). Post a ticket comment naming what you searched for and what you found instead, create a Review Hub todo (POST ${PORTOS_API_URL}/api/review/todo with title "[<KEY>] Stale reference" and what a human must re-scope), and transition the ticket to a **Blocked/On Hold status if the workflow has one — NOT back to a not-started status**; if the workflow has no held status, leave it **In Progress** so Phase 1's filter excludes it. Then remove the worktree and return to Phase 1. + +(A too-large scope is NOT in this list — it has its own park path below.) **A genuinely too-large ticket needs parking so the queue converges.** If the work is bigger than one coherent claim — it would touch files far outside the ticket's scope (>5 unrelated files) — and you can't carve a valuable standalone slice to ship first, splitting the omnibus is a human call: create a Review Hub todo (POST ${PORTOS_API_URL}/api/review/todo with title "[<KEY>] Needs clarification" and the split you'd suggest), transition the ticket to a **Blocked/On Hold status if the workflow has one — NOT back to a not-started status**, which would re-queue it under Phase 1 (ticket selection never consults Review Hub todos, so a not-started ticket is immediately re-eligible and the run would re-park it every pass); if the workflow has no held status, leave it **In Progress**. Phase 1's not-started-only filter keeps a non-not-started ticket out of the ready queue, and the Review Hub todo is the durable human signal — together they let the claim run converge. Then remove the worktree and exit. @@ -4273,6 +4577,131 @@ If \`git branch -d\` refuses, use \`-D\` — the MR is confirmed merged, so the - **No — the remainder is a clean, separable chunk** — close THIS issue with a summarizing note (shipped ✓ / moved to #NEW), remove \`in-progress\`, and file ONE tightly-scoped follow-up for the remainder: \`glab issue create --title "…" --label plan --description "…\\n\\nRefs #\${NUM}"\` (carry over any \`area:*\` labels the issue had). - **No — the remainder is a continuation of the same scope** — keep the issue OPEN, post a \`Done ✓ / Remaining ▢\` note, and release the claim so the queue re-picks it: \`glab issue update "\${NUM}" --unassign --unlabel in-progress\`. +NEVER leave the issue OPEN with \`in-progress\` still on it — that strands it as a zombie (the claim queue skips \`in-progress\`, so the remaining scope is never re-picked). **Do NOT \`git pull\`** from inside this phase — the work is already integrated on GitLab via \`glab mr merge\`; leave the user's working tree alone.`, + // v10 default (pre-`--no-track`, bumped to v11) + `[Claim Issue: {appName}] Claim and ship the next open GitLab issue + +Pick the next available unclaimed open GitLab issue, **create your own worktree at \`claim/issue-<num>\`**, implement the fix, ship a merge request (MR) that closes the issue, and clean up. This is the \`/claim --issues\` flow for GitLab — same in-flight scan, same branch naming, same no-local-merge cleanup, but the work source is the repo's **GitLab** issue tracker and the forge CLI is \`glab\` (not \`gh\`). **YOU pick the issue in Phase 1 — the scheduler does not reserve one for you.** Picking at execution time and immediately claiming (worktree + assignee + label) **narrows** the window for two concurrent runs to collide on the same issue — it does NOT eliminate it. Do NOT modify files in the source repo directly; ALL editing happens inside the worktree you create. + +{issueAuthorFilter} + +**How claiming works.** An issue is "in flight" when its number appears as the issue-position segment in either a \`claim/issue-<num>\` ref (the human/TUI pattern) or a \`cos/<task>/issue-<num>/<agent>\` ref (the CoS sub-agent pattern) across local branches, remote branches, or open MR source-branch refs — OR the issue is already assigned to someone OR carries an \`in-progress\` label. The \`claim/issue-<num>\` branch + the assignee/\`in-progress\` markers you set ARE the claim, visible to every other agent (including parallel machines). + +## Phase 1 — Pick the target issue + +Run steps 1–5 in order. + +1. cd into the repo root ({repoPath}) and confirm GitLab is the forge and \`glab\` is authenticated: \`glab auth status\` and \`glab repo view\`. If \`glab\` is not authenticated or the remote is not GitLab, exit cleanly — this task only works against GitLab issue trackers. +2. List candidate open issues, honoring the author filter described above. Fetch a JSON page and order **oldest-first** (GitLab returns newest-first by default; sort client-side by \`created_at\` since the page is bounded): + \`\`\`bash + git fetch --prune 2>/dev/null + # Owner-only mode (default): add --author <owner> (resolve <owner> from the project namespace). + glab issue list --per-page 100 -F json + # Any-author mode: run the SAME command WITHOUT --author. + \`\`\` +3. Build the in-flight set. Collect every branch/MR source ref: + \`\`\`bash + git branch -a --no-color --format='%(refname:short)' + glab mr list --per-page 100 -F json # read each MR's source_branch + \`\`\` + For each ref (after stripping any leading \`origin/\` prefix), extract the issue number **only when the ref matches** \`claim/issue-<num>\` (number after \`claim/issue-\`) or \`cos/<task>/issue-<num>/<agent>\` (the \`issue-<num>\` third segment). Do NOT flag an issue just because its bare number appears elsewhere in a ref. +4. **Pick the target issue:** walk the candidate list oldest-first and pick the FIRST issue where ALL of the following are true: + - Its number (\`iid\`) is NOT in the in-flight set. + - It has NO assignees (an assignee means another machine/human already claimed it). + - It does NOT carry any of these blocking labels: \`in-progress\`, \`blocked\`, \`needs-input\`, \`future\`, \`wontfix\`, \`question\`, \`discussion\`. + - It is NOT a tracking/umbrella **epic** — recognized by an \`epic\` label, a title ending in "(epic)", OR a title beginning with an \`[epic]\` bracket or \`Epic:\` tag (e.g. "[Epic] …" / "Epic: …", case-insensitive). Leave epics for a human to split. **The bare \`plan\` label is NOT a skip signal** — it marks the claimable queue, not a blocker. +5. **If no eligible issue exists**, exit cleanly — an empty actionable queue is a healthy state, not a failure. + +Capture the issue number (GitLab \`iid\`) as \`NUM\`, its title, and its full description — you'll reuse them in the MR and the \`Closes #<num>\` line. + +## Phase 2 — Claim (worktree + markers) + +Detect the default branch first (forge-agnostic), then create the worktree on \`claim/issue-<num>\` and set the cross-machine claim markers. Do all editing inside the worktree, NEVER in the source repo's working tree. + +\`\`\`bash +NUM=<picked-number> +DEFAULT_BRANCH="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's@^origin/@@')" +DEFAULT_BRANCH="\${DEFAULT_BRANCH:-main}" +WORKTREE="{worktreesRoot}/claim-issue-\${NUM}" +mkdir -p {worktreesRoot} +git fetch origin "\${DEFAULT_BRANCH}" +git worktree add -b "claim/issue-\${NUM}" "\${WORKTREE}" "origin/\${DEFAULT_BRANCH}" +# Cross-machine claim markers (best-effort — do not abort the run if these fail). +# Resolve your own username first — glab's --assignee wants a username (the +# \`@me\` gh-ism isn't universally supported), falling back to @me if the lookup fails: +ME="$(glab api user 2>/dev/null | sed -n 's/.*"username":"\\([^"]*\\)".*/\\1/p')" +glab issue update "\${NUM}" --assignee "\${ME:-@me}" 2>/dev/null +glab issue update "\${NUM}" --label in-progress 2>/dev/null +cd "\${WORKTREE}" +\`\`\` + +**If \`git worktree add\` fails because the \`claim/issue-<num>\` branch already exists** (a concurrent run won the race), do NOT force or reuse it — that branch IS another run's claim. Treat the issue as in-flight, return to Phase 1, and pick the next eligible issue; if nothing else is eligible, exit cleanly. Stash \`WORKTREE\` — you'll need it for Phase 7 cleanup. + +## Phase 3 — Verify still valid + +Read the full issue (\`glab issue view "\${NUM}"\`) before writing any code. **Every exit from this phase must leave a CONVERGING outcome on the issue — closed, or labeled \`needs-input\`.** Phase 1 step 4 skips both, so an autonomous drain stops re-picking the item. Releasing an issue OPEN and unlabeled is NOT an exit: the work detector still reports it actionable, so the next pass re-picks it and burns another no-op agent — every pass, forever. + +- **Already fixed, superseded, or closed-then-reopened-for-tracking** — a note says so, or the change it asks for is already on the default branch. **Close it:** post a note naming the MR/commit (or issue) that already delivered it (\`glab issue note "\${NUM}" -m "..."\`), then \`glab issue close "\${NUM}"\` and clear the markers (\`glab issue update "\${NUM}" --unassign --unlabel in-progress\`). Remove the worktree and return to Phase 1. **Evidence gate: if you cannot name the MR, commit, or issue that delivered it, this branch does NOT apply** — closing on a hunch destroys live work, which is far worse than one wasted pass. Treat the issue as real work and continue to Phase 4. +- **Stale reference** — the request names a function, file, or component that no longer exists (\`grep -rn\` the named identifiers; if they're gone, the issue is stale). Post a note naming what you searched for and what you found instead, **tag it \`needs-input\`** (\`glab issue update "\${NUM}" --label needs-input\`), release the claim markers (\`glab issue update "\${NUM}" --unassign --unlabel in-progress\`), remove the worktree, and return to Phase 1. Re-scoping a stale issue against today's code is a human call — and the label is what keeps the drain off it in the meantime. + +(A too-large scope is NOT in this list — it has its own park path below.) + +**A genuinely too-large issue needs parking so the drain converges.** If the work is bigger than one coherent claim — it would touch files far outside the issue's scope (>5 unrelated files) — and you can't carve a valuable standalone slice to partial-ship via Phase 5's \`Refs\` path, post a brief note naming the split you'd suggest, **tag it \`needs-input\`** (\`glab issue update "\${NUM}" --label needs-input\`), release the claim markers (\`glab issue update "\${NUM}" --unassign --unlabel in-progress\`), remove the worktree, and exit — splitting an omnibus issue is a human call, and parking it stops a perpetual drain from re-picking the same un-shippable issue every pass (Phase 1 step 4 skips \`needs-input\`). + +**Ambiguity is NOT a release trigger — decide, don't defer.** If the issue is merely open to more than one reasonable reading, or leaves a design choice unstated, do NOT bail to \`needs-input\`. Pick the most reasonable interpretation, record the approach you chose in a brief issue note (\`glab issue note "\${NUM}" -m "..."\`) so the decision is on the record, and implement it. The user would rather iterate on top of a shipped best-guess than have the issue parked waiting on a decision they didn't ask to make. Reserve \`needs-input\` — which pulls the issue out of the autonomous queue — for the narrow cases where proceeding would be **destructive or irreversible**, or genuinely requires the human: specific hardware/credentials you don't have, or a judgment only they can make. In those cases only, post the explaining note, **tag it \`needs-input\`** (\`glab issue update "\${NUM}" --label needs-input\`), release the claim markers (\`glab issue update "\${NUM}" --unassign --unlabel in-progress\`), remove the worktree, and exit cleanly. **That label is what lets an autonomous drain converge** — Phase 1 step 4 skips \`needs-input\` issues. Never leave a half-claimed issue. + +## Phase 4 — Implement + +Write the code, tests, and any docs the issue requires. Follow the repo conventions in CLAUDE.md. Run the relevant test suite as you go. + +**Roll discovered backbone work INTO this MR** — small supporting helpers, refactors, and tests that the fix depends on belong here, not a follow-up. Only defer genuinely-large adjacent work; when you do, file a NEW issue (\`glab issue create\`) tagged \`plan\` that references this one (\`Related to #<num>\`). + +Commit with a conventional message referencing the issue: + +\`\`\` +<type>: <one-line description> (#<num>) +\`\`\` + +## Phase 5 — Open the merge request + +This flow ships GitLab issues — it does NOT touch PLAN.md. The audit trail is the merged MR + \`git log\`. + +1. If the repo maintains a changelog, record a one-line entry **following the convention that repo documents** — read its \`CLAUDE.md\` and changelog README (e.g. \`.changelog/README.md\`) first. Some repos collect per-branch fragments in a directory (e.g. \`.changelog/next/\`) via a helper script rather than appending to one shared file, precisely so parallel agents don't conflict on every merge; use that flow when it's documented. Fall back to appending to the unreleased section (\`.changelog/NEXT.md\`, or \`## Unreleased\` in \`CHANGELOG.md\`) in the repo's existing prose style only when no convention is documented. If the repo has no changelog, skip this. +2. Push the branch: \`git push -u origin "claim/issue-\${NUM}"\` +3. Open the MR with \`glab mr create --fill --source-branch "claim/issue-\${NUM}" --target-branch "\${DEFAULT_BRANCH}" --yes\`. **Choose the issue trailer deliberately:** if this MR FULLY satisfies the issue's scope, the description MUST contain \`Closes #\${NUM}\` so the merge auto-closes it; if you deliberately shipped only PART of the issue (a valuable slice with real scope remaining), use \`Refs #\${NUM}\` instead (NOT \`Closes\`) and add a \`## Remaining\` section listing what's left — Phase 7 reconciles the issue so it is never stranded. Summarize what shipped + a short test plan (pass \`--description\` if \`--fill\` didn't capture it). + +## Phase 6 — Review and ship + +The configured reviewers for this task, in order, are \`{reviewers}\`. \`claude\` / \`codex\` / \`antigravity\` (CLI binary: \`agy\`) / \`grok\` invoke a local-CLI critique. (\`copilot\` is GitHub-only — skip it on GitLab.) When more than one is configured, run each in the listed order before merging. + +1. **Self-review your diff for reuse, quality, and efficiency** (DRY, dead code, naming, simpler equivalents, missed edge cases) and fix findings in the same diff BEFORE the reviewers run. Claude Code runs this as the three-agent \`/simplify\` pass; on other CLIs, do the equivalent review by hand. +2. **Wait for each configured CLI reviewer's findings BEFORE merging.** For \`claude\` / \`codex\` / \`antigravity\` (CLI binary: \`agy\`) / \`grok\`, invoke that CLI headless against the MR diff, apply fixes, run tests, re-push — capped at 3 rounds — then advance to the next reviewer. A configured CLI reviewer whose binary is not on PATH is UNSATISFIED, not clean — that is a broken setup, not a passing review. Do NOT substitute your own self-review and do NOT merge; comment on the MR naming the missing command and stop. + + **Review-stuck cleanup** (after 3 unresolved rounds): post one summarizing MR note (\`glab mr note\`), then run the worktree-only cleanup (\`cd {repoPath} && git worktree remove "\${WORKTREE}"\`). Leave the local branch, the open MR, the assignee, and the \`in-progress\` label in place so the human picks up cold. Do NOT run Phase 7. +3. **Merge via \`glab mr merge\`** — NEVER a local \`git merge\`. \`glab mr merge\` takes the **MR IID**, which is NOT the issue number — resolve it from the source branch first, then merge by that IID: + \`\`\`bash + MR_IID="$(glab mr list --source-branch "claim/issue-\${NUM}" -F json | sed -n 's/.*"iid":\\([0-9]\\{1,\\}\\).*/\\1/p' | head -1)" + glab mr merge "\${MR_IID}" --yes --remove-source-branch \\ + || glab mr merge "\${MR_IID}" --yes --squash --remove-source-branch + \`\`\` + +## Phase 7 — Clean up (post-merge ONLY) + +This phase runs only after the MR merged via Phase 6. From the **source repo** (cd back to {repoPath} first): + +\`\`\`bash +cd {repoPath} +git worktree remove "\${WORKTREE}" +git branch -d "claim/issue-\${NUM}" +\`\`\` + +If \`git branch -d\` refuses, use \`-D\` — the MR is confirmed merged, so the local branch is redundant. + +**Reconcile the issue — did this MR FULLY satisfy its scope?** +- **Yes (full)** — the \`Closes #\${NUM}\` line already auto-closed it on merge to the default branch; if it's somehow still open, close it (\`glab issue close "\${NUM}"\`) and remove the label (\`glab issue update "\${NUM}" --unlabel in-progress\`). +- **No — the remainder is a clean, separable chunk** — close THIS issue with a summarizing note (shipped ✓ / moved to #NEW), remove \`in-progress\`, and file ONE tightly-scoped follow-up for the remainder: \`glab issue create --title "…" --label plan --description "…\\n\\nRefs #\${NUM}"\` (carry over any \`area:*\` labels the issue had). +- **No — the remainder is a continuation of the same scope** — keep the issue OPEN, post a \`Done ✓ / Remaining ▢\` note, and release the claim so the queue re-picks it: \`glab issue update "\${NUM}" --unassign --unlabel in-progress\`. + NEVER leave the issue OPEN with \`in-progress\` still on it — that strands it as a zombie (the claim queue skips \`in-progress\`, so the remaining scope is never re-picked). **Do NOT \`git pull\`** from inside this phase — the work is already integrated on GitLab via \`glab mr merge\`; leave the user's working tree alone.`, ], // claim-issue v1 default — excluded every `plan`-labelled issue (the entire @@ -5520,6 +5949,133 @@ If \`git branch -d\` refuses (the PR squash-merged on GitHub but local doesn't k - **No — the remainder is a clean, separable chunk** — close THIS issue with a summarizing comment (shipped ✓ / moved to #NEW), remove \`in-progress\`, and file ONE tightly-scoped follow-up for the remainder: \`gh issue create --title "…" --label plan --body "…\\n\\nRefs #\${NUM}"\` (carry over any \`area:*\` labels the issue had). - **No — the remainder is a continuation of the same scope** — keep the issue OPEN, post a \`Done ✓ / Remaining ▢\` comment, and release the claim so the queue re-picks it: \`gh issue edit "\${NUM}" --remove-label in-progress --remove-assignee @me\`. +NEVER leave the issue OPEN with \`in-progress\` still on it — that strands it as a zombie (the claim queue skips \`in-progress\`, so the remaining scope is never re-picked). **Do NOT \`git pull\`** from inside this phase — the work is already integrated on GitHub via \`gh pr merge\`; leave the user's working tree alone.`, + // v11 default (pre-`--no-track`, bumped to v12) + `[Claim Issue: {appName}] Claim and ship the next open GitHub issue + +Pick the next available unclaimed open GitHub issue, **create your own worktree at \`claim/issue-<num>\`**, implement the fix, ship a PR that closes the issue, and clean up. This is the \`/claim --issues\` flow — same in-flight scan, same branch naming, same no-local-merge cleanup, but the work source is the repo's GitHub issue tracker instead of PLAN.md. **YOU pick the issue in Phase 1 — the scheduler does not reserve one for you.** Picking at execution time and immediately claiming (worktree + assignee + label) **narrows** the window for two concurrent runs to collide on the same issue — it does NOT eliminate it. Do NOT modify files in the source repo directly; ALL editing happens inside the worktree you create. + +{issueAuthorFilter} + +**How claiming works.** An issue is "in flight" when its number appears as the issue-position segment in either a \`claim/issue-<num>\` ref (the human/TUI pattern) or a \`cos/<task>/issue-<num>/<agent>\` ref (the CoS sub-agent pattern) across local branches, remote branches, or open PR head refs — OR the issue is already assigned to someone OR carries an \`in-progress\` label. The \`claim/issue-<num>\` branch + the assignee/\`in-progress\` markers you set ARE the claim, visible to every other agent (including parallel machines) and to the human running \`/claim --issues\` in a TUI. + +## Phase 1 — Pick the target issue + +Run steps 1–5 in order. + +1. cd into the repo root ({repoPath}) and confirm GitHub is the forge: \`gh repo view --json nameWithOwner -q .nameWithOwner\`. If \`gh\` is not authenticated or the remote is not GitHub, exit cleanly — this task only works against GitHub issue trackers. +2. List candidate open issues **oldest-first**, honoring the author filter described above. \`gh issue list\` defaults to newest-first, so order on the SERVER with \`--search "sort:created-asc"\` — a client-side \`jq\` sort would only reorder the already-truncated newest page, dropping the true oldest issues on repos with more than \`--limit\` open issues: + \`\`\`bash + git fetch --prune 2>/dev/null + # Author filter (see the block above). Pass --author as a QUOTED single token — + # do NOT pack flag+value into one variable: a bare \`$VAR\` holding "--author x" + # is a single argv token in zsh (no word-splitting) and gh rejects it. + # Owner-only mode (default): resolve the owner, then add --author "$OWNER" + OWNER="$(gh repo view --json owner -q .owner.login)" + gh issue list --state open --author "$OWNER" --search "sort:created-asc" --json number,title,author,assignees,labels,createdAt --limit 100 + # Any-author mode: run the SAME command WITHOUT the --author "$OWNER" flag. + \`\`\` +3. Build the in-flight set. Collect every branch/PR ref: + \`\`\`bash + git branch -a --no-color --format='%(refname:short)' + gh pr list --state open --json headRefName -q '.[].headRefName' 2>/dev/null + \`\`\` + For each ref (after stripping any leading \`origin/\` / \`upstream/\` prefix), extract the issue number **only when the ref matches** \`claim/issue-<num>\` (number after \`claim/issue-\`) or \`cos/<task>/issue-<num>/<agent>\` (the \`issue-<num>\` third segment). Do NOT flag an issue just because its bare number appears elsewhere in a ref. +4. **Pick the target issue:** walk the candidate list oldest-first and pick the FIRST issue where ALL of the following are true: + - Its number is NOT in the in-flight set. + - It has NO assignees (an assignee means another machine/human already claimed it). + - It does NOT carry any of these blocking labels: \`in-progress\`, \`blocked\`, \`needs-input\`, \`future\`, \`wontfix\`, \`question\`, \`discussion\`. + - It is NOT a tracking/umbrella **epic** — recognized by an \`epic\` label, a title ending in "(epic)", OR a title beginning with an \`[epic]\` bracket or \`Epic:\` tag (e.g. "[Epic] …" / "Epic: …", case-insensitive). An epic needs per-slice partial-ship (each slice its own PR, \`Refs\` not \`Closes\`), so leave it for a human or \`/claim --issues\` to split — don't claim it wholesale here. **The bare \`plan\` label is NOT a skip signal.** \`do-replan --issues\` (and \`/do:replan --issues\`) labels EVERY migrated backlog item \`plan\` — atomic bug-fixes included — so \`plan\` marks the *claimable* queue exactly as \`/do:next --issues\` treats it (it is that flow's required candidate label). Skipping all \`plan\` issues would discard the entire actionable backlog and falsely report an empty queue. +5. **If no eligible issue exists**, exit cleanly — an empty actionable queue is a healthy state, not a failure. + +Capture the issue number as \`NUM\`, its title, and its full body — you'll reuse them in the PR and the \`Closes #<num>\` trailer. + +## Phase 2 — Claim (worktree + markers) + +Create the worktree on a branch named \`claim/issue-<num>\`, then set the cross-machine claim markers. Do all editing inside the worktree, NEVER in the source repo's working tree. + +\`\`\`bash +NUM=<picked-number> +WORKTREE="{worktreesRoot}/claim-issue-\${NUM}" +mkdir -p {worktreesRoot} +git fetch origin main +git worktree add -b "claim/issue-\${NUM}" "\${WORKTREE}" origin/main +# Cross-machine claim markers (best-effort — do not abort the run if these fail): +gh issue edit "\${NUM}" --add-assignee @me 2>/dev/null +gh issue edit "\${NUM}" --add-label in-progress 2>/dev/null +cd "\${WORKTREE}" +\`\`\` + +(If the repo's default branch is not \`main\`, detect it with \`gh repo view --json defaultBranchRef -q .defaultBranchRef.name\` and substitute it for \`main\` above.) + +**If \`git worktree add\` fails because the \`claim/issue-<num>\` branch already exists** (a concurrent run won the race, or a remote claim branch is now visible), do NOT force or reuse it — that branch IS another run's claim. Treat the issue as in-flight, return to Phase 1, and pick the next eligible issue; if nothing else is eligible, exit cleanly. Stash \`WORKTREE\` — you'll need it for Phase 7 cleanup. + +## Phase 3 — Verify still valid + +Read the full issue (\`gh issue view "\${NUM}" --comments\`) before writing any code. **Every exit from this phase must leave a CONVERGING outcome on the issue — closed, or labeled \`needs-input\`.** Phase 1 step 4 skips both, so an autonomous drain stops re-picking the item. Releasing an issue OPEN and unlabeled is NOT an exit: the work detector still reports it actionable, so the next pass re-picks it and burns another no-op agent — every pass, forever. + +- **Already fixed, superseded, or closed-then-reopened-for-tracking** — a comment says so, or the change it asks for is already on the default branch. **Close it:** post a comment naming the PR/commit (or issue) that already delivered it (\`gh issue comment "\${NUM}" --body "..."\`), then \`gh issue close "\${NUM}" --reason completed\` (use \`--reason "not planned"\` when it was superseded rather than delivered) and clear the markers (\`gh issue edit "\${NUM}" --remove-assignee @me --remove-label in-progress\`). Remove the worktree and return to Phase 1. **Evidence gate: if you cannot name the PR, commit, or issue that delivered it, this branch does NOT apply** — closing on a hunch destroys live work, which is far worse than one wasted pass. Treat the issue as real work and continue to Phase 4. +- **Stale reference** — the request names a function, file, or component that no longer exists (\`grep -rn\` the named identifiers; if they're gone, the issue is stale). Post a comment naming what you searched for and what you found instead, **tag it \`needs-input\`** (\`gh issue edit "\${NUM}" --add-label needs-input\`), release the claim markers (\`gh issue edit "\${NUM}" --remove-assignee @me --remove-label in-progress\`), remove the worktree, and return to Phase 1. Re-scoping a stale issue against today's code is a human call — and the label is what keeps the drain off it in the meantime. + +(A too-large scope is NOT in this list — it has its own park path below.) + +**A genuinely too-large issue needs parking so the drain converges.** If the work is bigger than one coherent claim — it would touch files far outside the issue's scope (>5 unrelated files) — and you can't carve a valuable standalone slice to partial-ship via Phase 5's \`Refs\` path, post a brief comment naming the split you'd suggest, **tag it \`needs-input\`** (\`gh issue edit "\${NUM}" --add-label needs-input\`), release the claim markers (\`gh issue edit "\${NUM}" --remove-assignee @me --remove-label in-progress\`), remove the worktree, and exit — splitting an omnibus issue is a human call, and parking it is what stops a perpetual drain from re-picking the same un-shippable issue every pass (Phase 1 step 4 skips \`needs-input\`). + +**Ambiguity is NOT a release trigger — decide, don't defer.** If the issue is merely open to more than one reasonable reading, or leaves a design choice unstated, do NOT bail to \`needs-input\`. Pick the most reasonable interpretation, record the approach you chose in a brief issue comment (\`gh issue comment "\${NUM}" --body "..."\`) so the decision is on the record, and implement it. The user would rather iterate on top of a shipped best-guess than have the issue parked waiting on a decision they didn't ask to make. Reserve \`needs-input\` — which pulls the issue out of the autonomous queue — for the narrow cases where proceeding would be **destructive or irreversible**, or genuinely requires the human: specific hardware/credentials you don't have, or a judgment only they can make. In those cases only, post the explaining comment, **tag it \`needs-input\`** (\`gh issue edit "\${NUM}" --add-label needs-input\`), release the claim markers (\`gh issue edit "\${NUM}" --remove-assignee @me --remove-label in-progress\`), remove the worktree, and exit cleanly. **That label is what lets an autonomous drain converge** — Phase 1 step 4 skips \`needs-input\` issues. Never leave a half-claimed issue. + +## Phase 4 — Implement + +Write the code, tests, and any docs the issue requires. Follow the repo conventions in CLAUDE.md (no try/catch in route handlers, functional programming, Zod validation, Tailwind tokens, reactive UI updates). Run the relevant test suite as you go. + +**Roll discovered backbone work INTO this PR** — small supporting helpers, refactors, and tests that the fix depends on belong here, not a follow-up. Only defer genuinely-large adjacent work; when you do, file a NEW issue (\`gh issue create\`) tagged \`plan\` that references this one (\`Related to #<num>\`) rather than appending to PLAN.md. + +Commit with a conventional message referencing the issue so the trail is grep-able: + +\`\`\` +<type>: <one-line description> (#<num>) +\`\`\` + +## Phase 5 — Open the PR + +This flow ships GitHub issues — it does NOT touch PLAN.md. The audit trail is the merged PR + \`git log\`. + +1. If the repo maintains a changelog, record a one-line entry **following the convention that repo documents** — read its \`CLAUDE.md\` and changelog README (e.g. \`.changelog/README.md\`) first. Some repos collect per-branch fragments in a directory (e.g. \`.changelog/next/\`) via a helper script rather than appending to one shared file, precisely so parallel agents don't conflict on every merge; use that flow when it's documented. Fall back to appending to the unreleased section (\`.changelog/NEXT.md\`, or \`## Unreleased\` in \`CHANGELOG.md\`) in the repo's existing prose style only when no convention is documented. If the repo has no changelog, skip this — the PR + commit history is the record. +2. Push the branch: \`git push -u origin "claim/issue-\${NUM}"\` +3. Open the PR with \`gh pr create\`. Summarize what shipped + a short test plan. **Choose the issue trailer deliberately:** if this PR FULLY satisfies the issue's scope, the body MUST contain \`Closes #\${NUM}\` so the merge auto-closes it. If you deliberately shipped only PART of the issue (a valuable slice, with real scope still remaining), use \`Refs #\${NUM}\` instead (NOT \`Closes\`) and add a \`## Remaining\` section listing what's left — Phase 7 reconciles the issue so it is never stranded. + +## Phase 6 — Review and ship + +The configured reviewers for this task, in order, are \`{reviewers}\`. \`copilot\` waits for GitHub's auto-review; \`claude\` / \`codex\` / \`antigravity\` (CLI binary: \`agy\`) / \`grok\` invoke a local-CLI critique. When more than one is configured, run each in the listed order before merging — this mirrors slashdo's \`/do:pr --review-with <list>\`. + +1. **Self-review your diff for reuse, quality, and efficiency** (DRY, dead code, naming, simpler equivalents, missed edge cases) and fix findings in the same diff BEFORE the reviewers run. Claude Code runs this as the three-agent \`/simplify\` pass; on other CLIs, do the equivalent review by hand. +2. **Wait for each configured reviewer's findings BEFORE merging.** \`gh pr merge --auto\` only waits for required status checks; it does NOT wait for code-review feedback. Run the reviewers in the listed order (\`{reviewers}\`); for \`copilot\`, poll up to 10 minutes for its review and address \`COMMENTED\` / \`CHANGES_REQUESTED\` findings (commit + push inside the worktree, re-poll), capped at 3 rounds. For \`claude\` / \`codex\` / \`antigravity\` (CLI binary: \`agy\`) / \`grok\`, invoke that CLI headless against the PR diff, apply fixes, run tests, re-push — capped at 3 rounds — then advance to the next reviewer. A configured CLI reviewer whose binary is not on PATH is UNSATISFIED, not clean — that is a broken setup, not a passing review. Do NOT substitute your own self-review and do NOT merge; comment on the PR naming the missing command and stop. + + **Review-stuck cleanup** (after 3 unresolved rounds): post one summarizing PR comment (\`gh pr comment\`), then run the worktree-only cleanup (\`cd {repoPath} && git worktree remove "\${WORKTREE}"\`). Leave the local branch, the open PR, the assignee, and the \`in-progress\` label in place so the human picks up cold. Do NOT run Phase 7. +3. **Merge via \`gh pr merge\`** — NEVER a local \`git merge\`. The repo may allow only one method, so try in order and use the first that succeeds: + \`\`\`bash + gh pr merge <pr-num> --auto --delete-branch \\ + || gh pr merge <pr-num> --merge --delete-branch \\ + || gh pr merge <pr-num> --squash --delete-branch \\ + || gh pr merge <pr-num> --rebase --delete-branch + \`\`\` + +## Phase 7 — Clean up (post-merge ONLY) + +This phase runs only after the PR merged via Phase 6. From the **source repo** (cd back to {repoPath} first): + +\`\`\`bash +cd {repoPath} +git worktree remove "\${WORKTREE}" +git branch -d "claim/issue-\${NUM}" +\`\`\` + +If \`git branch -d\` refuses (the PR squash-merged on GitHub but local doesn't know yet), use \`-D\` — the PR is confirmed merged, so the local branch is redundant. + +**Reconcile the issue — did this PR FULLY satisfy its scope?** +- **Yes (full)** — the \`Closes #\${NUM}\` trailer already auto-closed it; if it's somehow still open, close it (\`gh issue close "\${NUM}"\`) and remove the label (\`gh issue edit "\${NUM}" --remove-label in-progress\`). +- **No — the remainder is a clean, separable chunk** — close THIS issue with a summarizing comment (shipped ✓ / moved to #NEW), remove \`in-progress\`, and file ONE tightly-scoped follow-up for the remainder: \`gh issue create --title "…" --label plan --body "…\\n\\nRefs #\${NUM}"\` (carry over any \`area:*\` labels the issue had). +- **No — the remainder is a continuation of the same scope** — keep the issue OPEN, post a \`Done ✓ / Remaining ▢\` comment, and release the claim so the queue re-picks it: \`gh issue edit "\${NUM}" --remove-label in-progress --remove-assignee @me\`. + NEVER leave the issue OPEN with \`in-progress\` still on it — that strands it as a zombie (the claim queue skips \`in-progress\`, so the remaining scope is never re-picked). **Do NOT \`git pull\`** from inside this phase — the work is already integrated on GitHub via \`gh pr merge\`; leave the user's working tree alone.`, ], // Basic self-improvement tasks: prior defaults that hardcoded the target app as diff --git a/server/services/taskPromptDefaults/prompts.js b/server/services/taskPromptDefaults/prompts.js index b7776c63a2..e901851ea2 100644 --- a/server/services/taskPromptDefaults/prompts.js +++ b/server/services/taskPromptDefaults/prompts.js @@ -533,7 +533,7 @@ SLUG=<picked-slug> WORKTREE="{worktreesRoot}/claim-\${SLUG}" mkdir -p {worktreesRoot} git fetch origin main -git worktree add -b "claim/\${SLUG}" "\${WORKTREE}" origin/main +git worktree add --no-track -b "claim/\${SLUG}" "\${WORKTREE}" origin/main cd "\${WORKTREE}" \`\`\` @@ -732,7 +732,7 @@ NUM=<picked-number> WORKTREE="{worktreesRoot}/claim-issue-\${NUM}" mkdir -p {worktreesRoot} git fetch origin main -git worktree add -b "claim/issue-\${NUM}" "\${WORKTREE}" origin/main +git worktree add --no-track -b "claim/issue-\${NUM}" "\${WORKTREE}" origin/main # Cross-machine claim markers (best-effort — do not abort the run if these fail): gh issue edit "\${NUM}" --add-assignee @me 2>/dev/null gh issue edit "\${NUM}" --add-label in-progress 2>/dev/null @@ -864,7 +864,7 @@ DEFAULT_BRANCH="\${DEFAULT_BRANCH:-main}" WORKTREE="{worktreesRoot}/claim-issue-\${NUM}" mkdir -p {worktreesRoot} git fetch origin "\${DEFAULT_BRANCH}" -git worktree add -b "claim/issue-\${NUM}" "\${WORKTREE}" "origin/\${DEFAULT_BRANCH}" +git worktree add --no-track -b "claim/issue-\${NUM}" "\${WORKTREE}" "origin/\${DEFAULT_BRANCH}" # Cross-machine claim markers (best-effort — do not abort the run if these fail). # Resolve your own username first — glab's --assignee wants a username (the # \`@me\` gh-ism isn't universally supported), falling back to @me if the lookup fails: @@ -1001,7 +1001,7 @@ DEFAULT_BRANCH="\${DEFAULT_BRANCH:-main}" WORKTREE="{worktreesRoot}/claim-\${KEY}" mkdir -p "{worktreesRoot}" git -C {repoPath} fetch origin "\${DEFAULT_BRANCH}" -git -C {repoPath} worktree add -b "claim/\${KEY}" "\${WORKTREE}" "origin/\${DEFAULT_BRANCH}" +git -C {repoPath} worktree add --no-track -b "claim/\${KEY}" "\${WORKTREE}" "origin/\${DEFAULT_BRANCH}" cd "\${WORKTREE}" \`\`\` diff --git a/server/services/taskPromptDefaults/versions.js b/server/services/taskPromptDefaults/versions.js index 2b4b4db8dc..dc2b595c28 100644 --- a/server/services/taskPromptDefaults/versions.js +++ b/server/services/taskPromptDefaults/versions.js @@ -8,10 +8,10 @@ // Only non-customized prompts (promptCustomized !== true) are upgraded. export const PROMPT_VERSIONS = { 'feature-ideas': 10, // v10: Phase 4 (Brainstorm) consults the repo's REJECTED.md and recently closed-unmerged PRs before proposing, so ideas the user already declined aren't re-proposed (mirrors idle-review's REJECTED.md contract). v9: drop DONE.md reads — use `.changelog/` + `git log` (last 50) as the completed-work signal - 'plan-task': 13, // v13: Phase 5's changelog step reads the repo's documented convention FIRST (per-branch fragment directory + helper script, as PortOS now uses) and only falls back to detect-and-append when none is documented; the Phase 6 review-stuck note no longer names `.changelog/NEXT.md` as the file it wrote. v12: the reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — the bare `antigravity` slug is a command that exists nowhere, and a review-loop agent that probed for it concluded no reviewer was available and merged on its own self-review. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v11: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of a repo-relative `data/cos/worktrees/` path, so the agent's checkout no longer lands inside the managed app's working tree. v10: Phase 3 "decide, don't defer" — an *ambiguous* PLAN.md item no longer jumps to the Phase 3b clarification PR; the agent picks the most reasonable reading, notes it, and implements. Phase 3b is reserved for destructive/irreversible or genuinely-human-gated items. Mirrors claim-issue v7 + CLAUDE.md. v9: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v8: Phase-6 merge fallback prefers --merge over --squash (after --auto), matching the /do:pr + review-loop default and PortOS's merge-only policy - 'claim-issue': 11, // v11: EVERY Phase-3 release now converges — the "already fixed / superseded" case CLOSES the issue (with a comment naming what delivered it) and the "stale reference" case tags `needs-input`, instead of both releasing the issue open and unlabeled. `isActionableIssue` (perpetualWork.js) can only see labels/assignees/epic/in-flight, so a body-or-comment-driven release left the issue looking actionable and the perpetual drain re-spawned a no-op agent on it every tick. v10: Phase 5's changelog step defers to the convention the repo documents (per-branch fragment directory + helper script when present) instead of prescribing an append to `.changelog/NEXT.md`. v9: reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — mirrors plan-task v12. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v8: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of a repo-relative `data/cos/worktrees/` path, so the agent's checkout no longer lands inside the managed app's working tree. v7: Phase 3 no longer releases/parks an *ambiguous* issue to `needs-input` — the agent decides (picks the most reasonable reading, records it in an issue comment, ships) rather than punting the choice back to a human; `needs-input` is reserved for destructive/irreversible or genuinely-human-gated (hardware/credentials) cases. Mirrors the "Decide, don't defer" policy in CLAUDE.md. v6: Phase 1 epic skip also recognizes a leading `[epic]` bracket or `Epic:` colon title tag (e.g. "[Epic] …" / "Epic: …"), not just an `epic` label or a "(epic)" suffix — mirrors the perpetualWork detector so a `[Epic]`-titled issue with no `epic` label stops re-spawning a claim agent that always skips it (perpetual drain now converges/parks). v5: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v4: Phase 5 chooses the issue trailer deliberately (Closes for a full ship, Refs + a `## Remaining` section for a partial one) and Phase 7 reconciles the issue with the partial-ship hybrid — close + file a scoped follow-up when the remainder is separable, else comment "done/remaining" + release the `in-progress` claim — so a partial ship is never left OPEN + `in-progress` (a zombie the claim queue skips forever). v3: Phase 3 tags an ambiguous/too-large issue `needs-input` (not just a comment) so it's excluded from future autonomous claims — required for `perpetual` (drain-until-done) mode to converge instead of re-picking the same un-actionable issue. v2: stop treating the bare `plan` label as a skip — `plan` is the claimable-queue label (do-replan --issues labels every migrated backlog item `plan`), so v1's exclusion emptied the whole actionable queue; now skip only true epics (`epic` label or "(epic)" title) - 'claim-issue-gitlab': 10, // v10: mirrors claim-issue v11 — every Phase-3 release converges (close the already-fixed/superseded issue, tag the stale-reference one `needs-input`) so the perpetual drain stops re-picking it. v9: mirrors claim-issue v10 — Phase 5's changelog step defers to the repo's documented convention (per-branch fragments) rather than prescribing a `.changelog/NEXT.md` append. v8: reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — mirrors plan-task v12. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v7: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of a repo-relative `data/cos/worktrees/` path, so the agent's checkout no longer lands inside the managed app's working tree (mirrors claim-issue v8). v6: mirrors claim-issue v7 — Phase 3 decides an *ambiguous* issue (record the chosen reading in an issue note, ship) instead of parking it to `needs-input`, which is reserved for destructive/irreversible or genuinely-human-gated (hardware/credentials) cases. v5: mirrors claim-issue v6 — Phase 1 epic skip also recognizes a leading `[epic]` bracket or `Epic:` colon title tag (e.g. "[Epic] …" / "Epic: …"), not just an `epic` label or a "(epic)" suffix, so the GitLab detector/agent converge on epic-titled issues too. v4: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v3: mirrors claim-issue v4 — Phase 5 chooses Closes-vs-Refs deliberately and Phase 7 reconciles the issue with the partial-ship hybrid (close + scoped follow-up when separable, else "done/remaining" note + release the `in-progress` claim) so a partial ship is never stranded. v2: Phase 3 tags an ambiguous/too-large issue `needs-input` (mirrors claim-issue v3) so it's excluded from future autonomous claims — required for `perpetual` (drain-until-done) mode to converge. v1: GitLab sibling of claim-issue — same 7-phase /claim --issues flow over `glab` issues + merge requests. Reached via the claim-work router when an app's resolved workTracker is 'gitlab'. - 'claim-issue-jira': 8, // v8: mirrors claim-issue v11 in JIRA's status vocabulary — an already-fixed/superseded ticket transitions to Done/Closed and a stale-reference ticket parks on a Blocked/On Hold status behind a Review Hub todo, instead of transitioning back to a not-started status that Phase 1 immediately re-picks. v7: mirrors claim-issue v10 — Phase 5's changelog step defers to the repo's documented convention (per-branch fragments) rather than prescribing a `.changelog/NEXT.md` append. v6: reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — mirrors plan-task v12. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v5: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of `{repoPath}/data/cos/worktrees/`, so the agent's checkout no longer lands inside the managed app's working tree (mirrors claim-issue v8). v4: mirrors claim-issue v7 — Phase 3 decides an *ambiguous* ticket (record the chosen reading in a ticket comment, ship) instead of parking it to a "Needs clarification" Review Hub todo, and Phase 1 no longer skips a merely-underspecified ticket; the todo is reserved for destructive/irreversible or genuinely-human-gated cases. v3: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v2: Phase 5 records remaining scope in the ticket (Done/Remaining comment) and files a follow-up ticket when a partial ship's remainder is separable, so remaining work isn't lost when a human lands the MR/PR. v1: JIRA sibling of claim-issue — claim ONE ready sprint ticket, move it To Do→In Progress→In Review around a self-managed worktree + MR/PR. Reached via the claim-work router when an app's resolved workTracker is 'jira' (replaces the prior jira→jira-sprint-manager route). + 'plan-task': 14, // v14: claim worktree creation passes `--no-track`, so a branch based on `origin/main` cannot inherit `main` as its upstream and make a config-derived push write directly to the default branch; the later `git push -u` sets the intended branch upstream. v13: Phase 5's changelog step reads the repo's documented convention FIRST (per-branch fragment directory + helper script, as PortOS now uses) and only falls back to detect-and-append when none is documented; the Phase 6 review-stuck note no longer names `.changelog/NEXT.md` as the file it wrote. v12: the reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — the bare `antigravity` slug is a command that exists nowhere, and a review-loop agent that probed for it concluded no reviewer was available and merged on its own self-review. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v11: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of a repo-relative `data/cos/worktrees/` path, so the agent's checkout no longer lands inside the managed app's working tree. v10: Phase 3 "decide, don't defer" — an *ambiguous* PLAN.md item no longer jumps to the Phase 3b clarification PR; the agent picks the most reasonable reading, notes it, and implements. Phase 3b is reserved for destructive/irreversible or genuinely-human-gated items. Mirrors claim-issue v7 + CLAUDE.md. v9: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v8: Phase-6 merge fallback prefers --merge over --squash (after --auto), matching the /do:pr + review-loop default and PortOS's merge-only policy + 'claim-issue': 12, // v12: claim worktree creation passes `--no-track`, so a branch based on `origin/main` cannot inherit `main` as its upstream and make a config-derived push write directly to the default branch; the later `git push -u` sets the intended branch upstream. v11: EVERY Phase-3 release now converges — the "already fixed / superseded" case CLOSES the issue (with a comment naming what delivered it) and the "stale reference" case tags `needs-input`, instead of both releasing the issue open and unlabeled. `isActionableIssue` (perpetualWork.js) can only see labels/assignees/epic/in-flight, so a body-or-comment-driven release left the issue looking actionable and the perpetual drain re-spawned a no-op agent on it every tick. v10: Phase 5's changelog step defers to the convention the repo documents (per-branch fragment directory + helper script when present) instead of prescribing an append to `.changelog/NEXT.md`. v9: reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — mirrors plan-task v12. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v8: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of a repo-relative `data/cos/worktrees/` path, so the agent's checkout no longer lands inside the managed app's working tree. v7: Phase 3 no longer releases/parks an *ambiguous* issue to `needs-input` — the agent decides (picks the most reasonable reading, records it in an issue comment, ships) rather than punting the choice back to a human; `needs-input` is reserved for destructive/irreversible or genuinely-human-gated (hardware/credentials) cases. Mirrors the "Decide, don't defer" policy in CLAUDE.md. v6: Phase 1 epic skip also recognizes a leading `[epic]` bracket or `Epic:` colon title tag (e.g. "[Epic] …" / "Epic: …"), not just an `epic` label or a "(epic)" suffix — mirrors the perpetualWork detector so a `[Epic]`-titled issue with no `epic` label stops re-spawning a claim agent that always skips it (perpetual drain now converges/parks). v5: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v4: Phase 5 chooses the issue trailer deliberately (Closes for a full ship, Refs + a `## Remaining` section for a partial one) and Phase 7 reconciles the issue with the partial-ship hybrid — close + file a scoped follow-up when the remainder is separable, else comment "done/remaining" + release the `in-progress` claim — so a partial ship is never left OPEN + `in-progress` (a zombie the claim queue skips forever). v3: Phase 3 tags an ambiguous/too-large issue `needs-input` (not just a comment) so it's excluded from future autonomous claims — required for `perpetual` (drain-until-done) mode to converge instead of re-picking the same un-actionable issue. v2: stop treating the bare `plan` label as a skip — `plan` is the claimable-queue label (do-replan --issues labels every migrated backlog item `plan`), so v1's exclusion emptied the whole actionable queue; now skip only true epics (`epic` label or "(epic)" title) + 'claim-issue-gitlab': 11, // v11: claim worktree creation passes `--no-track`, so a branch based on the default-branch remote ref does not inherit it as an upstream; the later `git push -u` sets the intended branch upstream. v10: mirrors claim-issue v11 — every Phase-3 release converges (close the already-fixed/superseded issue, tag the stale-reference one `needs-input`) so the perpetual drain stops re-picking it. v9: mirrors claim-issue v10 — Phase 5's changelog step defers to the repo's documented convention (per-branch fragments) rather than prescribing a `.changelog/NEXT.md` append. v8: reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — mirrors plan-task v12. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v7: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of a repo-relative `data/cos/worktrees/` path, so the agent's checkout no longer lands inside the managed app's working tree (mirrors claim-issue v8). v6: mirrors claim-issue v7 — Phase 3 decides an *ambiguous* issue (record the chosen reading in an issue note, ship) instead of parking it to `needs-input`, which is reserved for destructive/irreversible or genuinely-human-gated (hardware/credentials) cases. v5: mirrors claim-issue v6 — Phase 1 epic skip also recognizes a leading `[epic]` bracket or `Epic:` colon title tag (e.g. "[Epic] …" / "Epic: …"), not just an `epic` label or a "(epic)" suffix, so the GitLab detector/agent converge on epic-titled issues too. v4: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v3: mirrors claim-issue v4 — Phase 5 chooses Closes-vs-Refs deliberately and Phase 7 reconciles the issue with the partial-ship hybrid (close + scoped follow-up when separable, else "done/remaining" note + release the `in-progress` claim) so a partial ship is never stranded. v2: Phase 3 tags an ambiguous/too-large issue `needs-input` (mirrors claim-issue v3) so it's excluded from future autonomous claims — required for `perpetual` (drain-until-done) mode to converge. v1: GitLab sibling of claim-issue — same 7-phase /claim --issues flow over `glab` issues + merge requests. Reached via the claim-work router when an app's resolved workTracker is 'gitlab'. + 'claim-issue-jira': 9, // v9: claim worktree creation passes `--no-track`, so a branch based on the default-branch remote ref does not inherit it as an upstream; the later `git push -u` sets the intended branch upstream. v8: mirrors claim-issue v11 in JIRA's status vocabulary — an already-fixed/superseded ticket transitions to Done/Closed and a stale-reference ticket parks on a Blocked/On Hold status behind a Review Hub todo, instead of transitioning back to a not-started status that Phase 1 immediately re-picks. v7: mirrors claim-issue v10 — Phase 5's changelog step defers to the repo's documented convention (per-branch fragments) rather than prescribing a `.changelog/NEXT.md` append. v6: reviewer bullets name the antigravity reviewer's actual PATH binary (`agy`) — mirrors plan-task v12. The same bump adds the missing-binary guard: a reviewer whose CLI is not on PATH is UNSATISFIED, never a clean review the agent substitutes its own self-review for. v5: worktree is created under PortOS's shared worktrees dir (`{worktreesRoot}` → data/cos/worktrees) instead of `{repoPath}/data/cos/worktrees/`, so the agent's checkout no longer lands inside the managed app's working tree (mirrors claim-issue v8). v4: mirrors claim-issue v7 — Phase 3 decides an *ambiguous* ticket (record the chosen reading in a ticket comment, ship) instead of parking it to a "Needs clarification" Review Hub todo, and Phase 1 no longer skips a merely-underspecified ticket; the todo is reserved for destructive/irreversible or genuinely-human-gated cases. v3: per-kind reviewer bullets name `grok` alongside `claude`/`codex`/`antigravity` as a local-CLI reviewer (grok is now a selectable Review Loop reviewer). v2: Phase 5 records remaining scope in the ticket (Done/Remaining comment) and files a follow-up ticket when a partial ship's remainder is separable, so remaining work isn't lost when a human lands the MR/PR. v1: JIRA sibling of claim-issue — claim ONE ready sprint ticket, move it To Do→In Progress→In Review around a self-managed worktree + MR/PR. Reached via the claim-work router when an app's resolved workTracker is 'jira' (replaces the prior jira→jira-sprint-manager route). 'pr-reviewer': 3, // v3: multi-stage pipeline (security scan → code review + merge) 'code-reviewer-a': 1, // v1: 2-stage pipeline (codebase review → triage & implement) 'code-reviewer-b': 1, // v1: 2-stage pipeline (codebase review → triage & implement)