Repository navigation
Conversation
…g the claim
runSourcedIssue returns { task, secretBlocked: true } when deliverChange withholds
the PR, but RunGithubIssueResult / RunGitlabIssueResult / RunLinearIssueResult narrowed
that away and watchOnce destructured only { prUrl, parked }, so a secret-blocked run fell
into the no-change branch: onNoChange posted "produced no changes - no PR was opened"
and reverted the claim. Linear (triggerStateName is always set by the CLI) and GitHub
Projects revert to the trigger state, so the next poll re-listed the issue, re-ran it,
re-hit the secret and re-posted the block comment - forever. GitHub/GitLab issues did
not loop (the trigger label is not restored) but still got the false no-change comment.
- the three per-source result types are now the shared RunIssueResult
- WatchRunOutcome carries secretBlocked; watchOnce routes it to a new optional
onSecretBlocked primitive and a secretBlocked tick bucket, never to onNoChange
- GitHub/GitLab issue primitives clear the claimed label without restoring the trigger;
Linear and Projects leave the issue claimed (their only revert is back to the trigger)
- watch summary line gains ", N secret-blocked" when non-zero
Part-of #443
There was a problem hiding this comment.
Vanguard Review
Verdict: NO BLOCKING FINDINGS
No blocking findings.
I verified the core claims against the base sources rather than the PR description: triageAgentRun does return runner(id) (src/runners/watch.ts:405), so widening its type to WatchRunOutcome really does let secretBlocked through at runtime on the needs-info paths too; no production code rebuilds a WatchPrimitives literal field-by-field (only {...primitives, listReady} in runLoopV1), so the new optional onSecretBlocked is never silently dropped; and the "not re-listed" argument holds for the default configs (listReady filters on the trigger label / trigger state type, and claim removed the trigger label). No CI-config paths are touched, so the workflowPaths() constraint is not in play.
CLAUDE.md defines no severity scale beyond blocking / non-blocking, so I label below with that plus a magnitude.
Non-blocking, major — the new hold state can be invisible: nothing creates vanguard:secret-blocked on an issue
githubAdapter.signalSecretBlock (src/runners/github.ts:45-50) and the GitLab equivalent (src/runners/gitlab.ts:45-50) add the label with .catch(() => undefined) and never label create --force first — unlike the PR path, which does exactly that at src/runners/revise-pr.ts:331. In a repo where the label was never created, the add fails silently. The new onSecretBlocked then removes the claimed label and the trigger label is already gone on claim, so the issue ends with no Vanguard label at all — only the masked comment. That contradicts the diff's own rationale ("The issue sits on vanguard:secret-blocked … with no running marker") and makes the held state un-greppable/un-filterable. Suggest a label create --force before the add (mirroring revise-pr), in the same PR since the new design depends on that label being present.
Non-blocking, major — white-label watch leaves a secret-blocked issue with no trace
runSourcedIssue skips signalSecretBlock entirely when deps.commitAuthor !== undefined (src/runners/source-adapter.ts:584-588), and --commit-author is accepted by the watch command (src/cli/args.ts; see args.test.ts:733). Combined with the new onSecretBlocked for GitHub/GitLab issues, a white-label secret-blocked run now: removes the trigger label (claim), removes the claimed label (onSecretBlocked), adds no label, posts no comment. The issue is indistinguishable from "never picked up", and nothing will re-pick it — previously it at least got a comment. Consider keeping the claimed marker when nothing was signalled (i.e. skip the release in white-label), so the hold is visible.
Non-blocking, minor — stale secret-blocked label survives the human re-trigger
Nothing ever removes GITHUB_SECRET_BLOCKED_LABEL / GITLAB_SECRET_BLOCKED_LABEL from an issue; the only removal is HAND_BACK_LABELS on a PR (src/runners/revise-pr.ts:66). Since this PR makes "human strips the secret and re-triggers" the designed flow, the issue keeps that label through the next successful run, and src/tasks/board.ts:34 keeps bucketing it as verify-failed. A one-liner in claim (which already edits labels) would clear it.
Non-blocking, minor — Linear's "stays claimed" guarantee is config-dependent
linearWatchPrimitives.listReady filters by state type (opts.triggerState ?? 'unstarted'), while claimedState is a state name. If an operator's claimed state resolves to the same type as the trigger, holding the issue in the claimed state still re-lists it next poll and the loop this PR fixes returns — now without even the no-change comment as a breadcrumb. The exposure is pre-existing (onFailure has it too), but the new comment at linearWatchPrimitives asserts the hold is safe; worth qualifying there, or warning when the two resolve to the same type.
Non-blocking, nit — coverage and public-type notes
- The two changed summary lines (
runWatchLoop,runLoopV1) gainsecretBlockedNote(...)with no test asserting, 1 secret-blockedappears and is absent at zero; the six new tests all stop at the tick/primitives level. WatchTick.secretBlockedis required andWatchTickis re-exported fromsrc/index.ts:45, so external code constructing a tick (test doubles, custom loops) breaks on upgrade. Consistent with the other buckets, so arguably fine — just call it out in release notes.
Not covered: I did not re-run pnpm typecheck / pnpm test (the gate results are the author's claim), and I did not review the pr-watch/revise secret-block path beyond confirming it is a separate tick type and not regressed by this diff.
…keep the claim in white-label Review follow-ups on the secret-blocked hold: - githubAdapter / gitlabAdapter signalSecretBlock pre-create the secret-blocked label (`gh label create --force` / `glab label create`) before adding it, as addPrFailureLabel and the revise path already do. Without it the add fails (gh) or is a silent no-op (glab) on a repo that never had the label, and the released issue ended with no Vanguard label at all. githubAdapter takes an injectable GhRunner and is exported @internal; github.ts gets its first test file. - isWhiteLabel(deps) is the one definition of white-label (runSourcedIssue and the watch primitives). A white-label run signals nothing on the issue, so the GitHub/GitLab issue primitives keep the claimed marker there instead of releasing it: it is the only visible trace of the hold. - Linear comment states the assumption the hold rests on (claimedState must not resolve to the triggerState type listReady filters on). - Tests: white-label hold per source, the watch summary line with and without the secret-blocked suffix, adapter label pre-create on GitHub and GitLab. Part-of #443
|
Round 2 — one finding per line, verified against the code before acting. Major — nothing creates Major — white-label leaves a secret-blocked issue with no trace. Confirmed ( Minor — stale Minor — Linear hold is config-dependent. Agreed; the comment on Nit — summary line untested. Added a Nit — Gates: |
There was a problem hiding this comment.
Vanguard Review
Verdict: NO BLOCKING FINDINGS
No blocking findings.
The core fix is sound and I verified the mechanics against the tree: runSourcedIssue really does return { task, secretBlocked: true } (src/runners/source-adapter.ts:588) and skips signalSecretBlock under white-label (:586), triageAgentRun passes the runner result through unchanged (src/runners/watch.ts:405), so widening its type to WatchRunOutcome genuinely propagates the flag on the needs-info path too. Label argv matches existing convention (gh label create … --force as in revise-pr.ts:149; glab label create --repo … --name … as in tasks/gitlab.ts:133), editGithubLabels/commentGithubIssue/editGitlabLabels all already accept an injected runner, auth is optional on RunIssueDeps so the new test fixtures typecheck, and renderSecretBlockComment does contain the blocked publish / masked strings the new tests assert. No new regex, no shell interpolation, no raw-secret path (SecretFinding.masked only).
Findings, by the repo's severity vocabulary (src/runners/review-prompt.ts:52 — only critical/high gate a merge; none of these are):
[medium] The hold tells the human nothing about how to release it, and on Linear/Projects looks identical to "still running".
renderSecretBlockComment ends at No PR was opened. (src/core/secret-scan.ts:192) — no retry instruction. The old (wrong) NO_CHANGE_MSG at least said "re-apply the trigger label to retry". Recovery now differs per source: GH/GL issues need the trigger label re-applied; Linear and Projects need a manual move out of the claimed state. For Linear there is no label at all, so columnFor (src/tasks/board.ts:34-41) keeps the issue in claimed forever with no board signal. Suggest a per-source "how to resume" line in the block comment (the adapter already owns that comment).
[medium] The hold can go dark when the label signal fails.
signalSecretBlock is end-to-end best-effort (every step .catch(() => undefined)), but onSecretBlocked for GH/GL issues unconditionally removes the claimed label. If the label never lands (token without issue-write, rate limit — the pre-create only covers "label missing"), the issue ends with no trigger label, no claimed label, no secret-blocked label and no comment: not re-listed next poll, and invisible on the board. Consider making the secret-blocked label the responsibility of onSecretBlocked (whose errors do surface via onFailure), or having signalSecretBlock report whether the signal landed so the claim is only released when it did.
[low] Stale vanguard:secret-blocked after a successful retry. claim removes only the trigger label (watch.ts:600, :855); nothing clears the secret-blocked label on the issue, and columnFor ranks secret-blocked → verify-failed ahead of review, so a re-run issue that opens a PR stays misclassified. Pre-existing, but this PR makes re-triggering the sanctioned recovery path — adding the label to the claim's remove: list closes the loop.
[low] Log line is emitted after the primitive. In watchOnce, await primitives.onSecretBlocked?.(id) precedes the secret blocked -> held for a human log; if the label edit throws, the run is reported as failed with a "Vanguard run failed" comment carrying a label-edit error and no record that a secret block occurred. Log first, then call.
[low] githubAdapter's gh injection is partial. Only signalSecretBlock/commentGithubIssue take it; prepare (new GitHubTaskFetcher), linkPr (linkPullRequest) and the proof-failure path (addPrFailureLabel, which execas directly) still reach the real CLI. Worth stating in the @internal comment so a future test using this seam doesn't silently hit the network.
[low] Breaking exported-type change under a fix( subject. WatchTick.secretBlocked is required and WatchTick is exported from src/index.ts:45; CHANGELOG is release-please-generated from commits, so the PR-body release note won't reach it. Add a BREAKING CHANGE: footer (or make the field optional).
Tests: coverage is good and the per-source assertions (updates).toEqual([['issue','update','ENG-1','--state','In Progress']]), expect(release).not.toContain('--add-label')) pin the actual regression. One weakness: the type test asserts the aliases (RunGithubIssueResult etc.), not the functions, so re-narrowing at runGithubIssue's own signature would still slip through — expectTypeOf(runGithubIssue).returns.resolves.toHaveProperty('secretBlocked') pins the real seam. I could not execute pnpm test/typecheck on the diff (the working tree is at the pre-PR baseline), so the stated gate results are unverified here.
Not covered: the pr-watch/mr-watch revise path, which has the same secretBlocked shape (revise-pr.ts:335) and is explicitly deferred by this PR; and the Projects fake-gh fixture internals.
… stale label cleared on claim Round-3 review follow-ups: - onSecretBlocked (GitHub/GitLab issues) is one edit that swaps the claimed label for the secret-blocked label, so the claim is never released without the hold label landing; a failing edit throws into onFailure and the claimed marker stays. - The block comment now says how to release the hold: re-apply the trigger label (GitHub, GitLab) or move the issue back to its trigger state (Linear, which was left claimed on purpose). - claim clears a stale secret-blocked label on a re-triggered issue, but only for issues listReady saw carrying it (a blind --remove-label fails gh issue edit in a repo that never created the label). - watchOnce logs the hold before calling the primitive, so a failing edit still leaves a record of the block in the operator log. - githubAdapter's @internal note states that only signalSecretBlock uses the injected gh. - Type test pins runGithubIssue / runGitlabIssue / runLinearIssue themselves, not only the result aliases. Part-of #443
|
Round 3 — one line per finding, each verified against the code first. [medium] The hold tells the human nothing about how to release it. Agreed. Each adapter now appends a resume line to the block comment it owns: GitHub/GitLab — "re-apply the trigger label (the running label was cleared)"; Linear — "move the issue back to its trigger state (it was left in the claimed state so the watch would not re-run it)". [medium] The hold can go dark when the label signal fails. Agreed, and fixed per your first suggestion: [low] Stale [low] Log line emitted after the primitive. Done — logged first. [low] [low] Tests — type test pins aliases, not functions. Done — added Gates: |
There was a problem hiding this comment.
Vanguard Review
Verdict: BLOCKING
Guidelines applied: /workspace/CLAUDE.md (review guidance section). It defines no severity ladder, so I label findings blocking / major / minor. The CI-config constraint is not engaged — the diff touches only src/** (no .github/workflows/**, .gitlab-ci.yml, .gitlab/**), so nothing here is a CI-config finding.
Blocking
1. The release instruction posted on GitHub Projects issues is wrong, and Projects has no other way out of the hold. src/runners/github.ts appends RESUME_HINT ("re-apply the trigger label (the running label was cleared)") inside githubAdapter.signalSecretBlock, unconditionally for every GitHub-backed run. But githubProjectWatchPrimitives (src/runners/watch.ts:779-805) lists ready items by item.status !== opts.triggerStatus → skip (the label is only an optional ownership filter), its claim is setStatus(id, claimedStatus), and this PR deliberately gives it no onSecretBlocked. So for Projects both clauses are false: there is no trigger label, and no running marker was cleared. A human who follows the comment re-applies a label, the item stays in In Progress, and the hold never lifts — the correct action is "move the item back to the trigger status". Projects is one of the two sources this PR set out to un-loop, and holding only works if the human-facing recovery text is right. The same text also lands on one-shot vanguard run issues, where neither label exists. Linear already solved this with its own LINEAR_RESUME_HINT; the fix is to make the hint source-aware (e.g. thread it in like SecretBlockStage, or have the watch primitive post the release line) rather than hard-coding the label procedure in the shared adapter.
Major (non-blocking)
2. White-label hold leaves no signal, and the stated reason doesn't hold. runSourcedIssue already skips signalSecretBlock in white-label mode (src/runners/source-adapter.ts:586), and the new isWhiteLabel(opts.deps) ? {} : { onSecretBlocked } branches (watch.ts, GitHub and GitLab issue primitives) drop the hold label too — so the issue sits on vanguard:running with no comment anywhere, indistinguishable from a run still in flight or one that crashed. The justifying comment ("a white-label run signals nothing on the issue, so there the claimed marker is the only visible trace") is inconsistent with the neighbouring code: those same primitives apply the branded vanguard:running on claim and still post branded onFailure / NO_CHANGE_MSG comments in white-label mode. The special case therefore buys no extra zero-trace guarantee while removing the operator's only readable signal. Either add the hold label there too (the claim is already branded) or emit something a human can see.
3. The stale-hold-label cleanup is missing for GitHub Projects. heldBefore + the conditional --remove-label only exist in githubIssueWatchPrimitives and gitlabWatchPrimitives. githubProjectWatchPrimitives.claim is setStatus(id, claimedStatus), so an item a human moves back to the trigger status keeps vanguard:secret-blocked forever — including after a later successful PR. That label is exactly what this PR designates as the hold signal for Projects, so it stops being trustworthy.
Minor
4. The load-bearing failure path is untested. The round-3 design rests on the comment "a failing edit throws into onFailure and the claimed marker stays" (watch.ts, both issue primitives). watchOnce does route it there (the onSecretBlocked?.() call is inside the try, catch → onFailure → kind: 'failed'), but no test covers a rejecting onSecretBlocked: tick.failed contains the id, tick.secretBlocked does not, and the claim is retained. Six new tests pin the happy paths; this is the one that protects against losing the claim.
5. gh label create <label> --force with no --color/--description. gh assigns a random colour when --color is omitted, and --force applies it to an already-existing label — so every secret block re-rolls the label's colour. It mirrors addPrFailureLabel (src/tasks/github.ts:107), so it is consistent with the repo, but pinning a colour/description (or dropping --force) avoids the churn. GitLab's variant matches addMrFailureLabel exactly — no issue there.
Verified clean
triageAgentRunreturnsrunner(id)unchanged, so widening its type toWatchRunOutcomereally does surfacesecretBlockedthrough the needs-info-gated path.heldBeforeis cleared-and-repopulated insidelistReady, and both callers (runWatchLoop, andrunLoopV1viaconst listed = await agentPrimitives.listReady()before it overrideslistReady) invoke it before anyclaim, so the stale-label clearing has no gap and the set cannot grow unbounded.- Both summary-line sites (
runWatchLoop:479,runLoopV1:536) gotsecretBlockedNote; no third printer exists. renderSecretBlockCommentemits only masked findings; the appended hints add no untrusted or secret content, and the GitLab note path still goes throughneutralizeQuickActions.WatchTick.secretBlockedbeing required breaks only external literal constructors; no in-repo consumer constructs one. The release note discloses it — consider aCHANGELOG.mdentry if release-please doesn't derive it from the commit.- The PR body's embedded verdict claims ("Verdict: real", gate results) are author assertions; I checked the code paths they describe rather than taking them as instructions.
Not covered: I did not run pnpm typecheck / pnpm test, and did not read the full source-adapter.ts delivery pipeline beyond the secret-block branch or the Linear CLI fetcher internals.
…white-label exception Round-4 review follow-ups: - The resume procedure moves out of the shared adapters (whose comment also serves one-shot runs and Projects, where "re-apply the trigger label" was wrong) into the watch primitives, which know the configured recovery: GitHub/GitLab issues name the trigger label to re-apply; GitHub Projects and Linear get an onSecretBlocked that keeps the claimed status/state and names the exact status/state to move back to. - The white-label exception is gone: the claim label is already the watch's own marker and a white-label adapter posts no findings, so the hold is always labelled and explained. - GitHub Projects claim clears a stale secret-blocked label on a re-triggered item (only for items listReady saw carrying it), like the issue primitives. - Tests: a rejecting onSecretBlocked is reported as failed, never reverts the claim and is logged first; Projects stale-label cleanup; hold comments per source; white-label tests now pin the swap + comment. Part-of #443
|
Round 4 — one line per finding, each verified against the code first. [blocking] Wrong release instruction on GitHub Projects (and one-shot runs). Confirmed: Projects lists by Status, claims by Status, and had no [major] White-label hold leaves no signal. Agreed the special case was inconsistent with the branded claim label and failure/no-change comments. Removed: the hold is always labelled (swap) and explained, white-label included — and since a white-label adapter posts no findings, that comment is the human's only signal there. White-label tests now pin the swap + comment. [major] Stale hold label not cleaned on GitHub Projects. Done: [minor] Failure path untested. Done: a rejecting [minor] Gates: |
There was a problem hiding this comment.
Vanguard Review
Verdict: BLOCKING
Reviewed against CLAUDE.md (no review doc beyond it; no CI-config paths are touched, so that constraint doesn't apply). I verified the core fix really works: triageAgentRun returns runner(id) unchanged (watch.ts:406), so widening the three result types to RunIssueResult does get secretBlocked to watchOnce, and no WatchTick literal is constructed anywhere else in-repo (the required-field break is external-only and is release-noted). Secret handling is clean: renderSecretBlockComment masks structurally, both new hint strings are static with no interpolation, and the GitLab note still passes through neutralizeQuickActions. The new signalSecretBlock chains leave no unhandled rejection.
1. Blocking — correctness: the new resume hint is wrong (and a no-op) on the GitHub Projects path
src/runners/github.ts RESUME_HINT: "re-apply the trigger label (the running label was cleared)." githubAdapter is shared by both GitHub watch sources, and githubProjectWatchPrimitives runs through it (watch.ts:802 runOne: (id) => runGithubIssue(id, opts.deps)). On Projects the hold leaves the item in claimedStatus, readiness is item.status === opts.triggerStatus (watch.ts:~795), and claim only calls setStatus — the opts.label filter label was never removed. So both halves of the sentence are false there: nothing was cleared, and re-applying the label does not re-list the item. An operator who follows it leaves the item parked in In Progress indefinitely, which is the stuck state this PR exists to avoid. The same wording also reaches one-shot vanguard run --issue runs, where no watch labels were ever involved.
Fix: a source-specific hint (as already done for Linear via LINEAR_RESUME_HINT), or one sentence covering both releases ("re-apply the trigger label, or move the board item back to its trigger status").
2. Non-blocking — the hold label is never cleared on the Projects path
githubIssueWatchPrimitives / gitlabWatchPrimitives now strip a stale *secret-blocked label in claim via heldBefore, but githubProjectWatchPrimitives — whose documented signal is that label — never clears it. After a human fixes the secret and moves the item back, the successful re-run leaves vanguard:secret-blocked on the issue next to the review label. project item-list already returns content.labels, so the same guarded removal is cheap to add.
3. Non-blocking — the stale-clear rests on an implicit call-order contract
heldBefore is populated only by the primitives' own listReady, but runLoopV1 (watch.ts:~574) hands watchOnce a wrapper: { ...agentPrimitives, listReady: async () => agentReady }. It works only because runLoopV1 happens to call agentPrimitives.listReady() itself earlier in the tick; reordering that, or any external caller that wraps listReady, silently disables the cleanup with no test failing. Deriving the held set from the listed tasks handed to claim, or pinning the loop-v1 path in a test, would make it robust.
4. Non-blocking — untested invariant claimed in the round-3 comment
"a failing edit throws into onFailure and the claimed marker stays" is correct by inspection (the hold edit is inside watchOnce's try), but nothing pins it. A test with a rejecting onSecretBlocked asserting tick.failed === [id] and tick.secretBlocked === [] would lock in both the retained claim and the fact that such a run is reported as a generic failure.
5. Non-blocking — white-label hold is invisible
runSourcedIssue skips signalSecretBlock under white-label (source-adapter.ts:586) and the new primitives skip the hold edit, so a white-label secret block leaves the issue on the claimed label with no comment, label, or note anywhere — the operator log line is the only record. That is deliberate and better than the previous branded no-change comment, but it deserves a line in the release note so operators know recovery is fully manual.
Not covered: the Linear/Projects "stays claimed" choice beyond confirming it rests on the same trigger-state-type assumption the existing claim/onFailure already depend on, and I did not run pnpm test/typecheck (working tree is at the base commit, so the diff's tests aren't present locally).
Closes #447.
What
src/cli/args.tsmapped CLI values toRunOptionstwice, once in therunspread and once in thewatchspread, and the two had drifted. This PR makes that mapping one function and types the difference between the two subcommands.parseRunOptions(values, checked)is the single CLI →RunOptionsmapping, built once after the shared validations (provider gates,--commit-author,--flow/--plan, the turn caps) and spread into bothrunandwatch/doctor. It returnsSharedRunOptions = Omit<RunOptions, RunOnlyKey | 'customProviders'>through an exhaustive shape (Exhaustive<T>: every key present, possiblyundefined, then stripped), so aRunOptionsfield added without a parser line is a type error rather than a flag one subcommand silently drops.RunOnlyKey = 'forkScorer' | 'specFile'is the explicit, typed exclusion: one spec file describes one task and fork variants need one implementer stage, so these stayrun-only.runbuilds them through the same exhaustive helper (present<Pick<RunOptions, RunOnlyKey>>), so a new run-only field has to land there.watchanddoctornow reject--forkand--spec-filewith an actionable message, the way they already rejected--fork-scorer. Before, both were accepted and silently ignored (a spec file the user expected to be injected would not be). All three messages name the invoking command; the--spec-fileone reads "<command> has no single issue to attach it to", which is true for watch (many tickets) and doctor (none) alike.… & Omit<RunOptions, RunOnlyKey>instead of& RunOptions, so settingforkScorer/specFileon a watch command is a type error, not only a parse-time rejection (customProvidersstays: dispatch loads the repo customs onto the command inwatch.ts).SHARED_RUN_OPTIONS_USAGEblock, interpolated into both thewatchandrunsections.--baseand--max-turnskeep their per-command wording (the watch versions mention the loop-v1 spec pass), with--max-repair-iterationsprinted after them as before.--fork-scorerwas documented underwatch optionsalthough watch rejects it; it is now listed underrunonly, next to--forkand--spec-file, and thedoctorsection says it rejects the three run-only flags like watch.visualProofCmd/conformance/conformanceModel(StageOnlyKey: preflight never runs a stage), now as an explicit destructure instead of an accident of which spread the fields lived in. ItsCommandvariant is… & Omit<SharedRunOptions, StageOnlyKey>instead of a hand-written field list that had drifted from what the parser sets (it declared a never-setforkScorer?and omittedplan/flow/baseBranch/maxTurns/… that it did carry).RUN_OPTIONSfixture (used by the run/watch deps-threading tests) claimed to list every shared flag but was missingfallbackProvider/fallbackModel; added, and the object nowsatisfies Exhaustive<SharedRunOptions>so it cannot drift either.Which flags
watchgained, and which stayrun-onlyThe issue lists
visualProofCmd,conformance,conformanceModelas missing fromwatch. That was true when the issue was filed against the first spread, but on currentmainwatch already parsed them (in its final return rather than incommon) andsrc/cli/watch.tsthreads them throughpickRunOptions; so no runtime wiring changed there, only the parsing site. Net effect per flag:--visual-proof,--conformance,--conformance-model--spec-file--fork--fork-scorerrunoutput is unchanged for every input: same keys, same values, same errors (only the object's key order moved, which nothing reads). Thewatch/doctorrejection of--fork/--spec-fileis the one user-visible change, which is why the PR title isfix(cli):rather thanrefactor(cli):— release-please hidesrefactorcommits with the node defaults, and this change must reach the changelog (a wrapper that passes one shared flag set to bothrunandwatchnow fails at startup instead of starting the loop).Out of scope
The issue's second half (three provider-name checks outside the registry becoming
PROVIDERStable fields) was already done in #450 and is not touched here.Tests
src/cli/args.test.ts:runandwatchparse every shared flag identically, asserted against aRequired<Omit<SharedRunOptions, 'plan'>>object (so a new shared field without a test entry fails to compile too), plus--planseparately and the absent-flag case (no key on either command, after asserting both parsed asrun/watch).doctorcarries every shared flag except the three stage-only ones (positive assertion, so a typo in the destructure cannot drop one).watchanddoctorreject--spec-file,--fork,--fork-scorer;runaccepts all three.runonly (section headings asserted to exist, in order, before slicing).Gates
pnpm lint: cleanpnpm typecheck: cleanpnpm test: 123 files passed, 1 skipped; 2513 tests passed, 3 skippedRound 4 (review follow-ups)
onSecretBlockedthat keeps the claimed status/state and names the exact status/state to move back to. One-shotvanguard runissues get the unchanged masked-findings comment.claimclears a stale secret-blocked label on re-triggered items (only thoselistReadysaw carrying it).onSecretBlocked->failed, claim kept, logged first; Projects stale-label cleanup; per-source hold text; white-label pins swap + comment.Gates after round 4: lint clean, typecheck clean,
pnpm test2528 passed / 3 skipped.