Repository navigation
feat(backends): completion contracts can declare pushed-changes alternatives (spec 025, plan 1/3) - #1555
Conversation
A profile may list outcomes that satisfy requiresPushedChanges instead of a push (only pr-response exists). The flag is rejected at load time unless requiresPushedChanges is true, so no agent inherits an alternative implicitly. Nothing consumes the flag yet (plan 025/1, task 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ate pushed-changes outcomes readCompletionEvidence now reads the PR-response sidecar (url + kind, malformed files are evidence of nothing) and, given repoDir + initialHeadSha, the repository state (clean tree, HEAD unchanged). evaluatePushedChanges checks the push branch first and then each declared alternative, fail-closed, returning one rejection reason per branch. Nothing calls the evaluator yet (plan 025/1, tasks 2-3). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lternatives getCompletionFailure now routes the pushed-changes requirement through evaluatePushedChanges. Profiles without alternatives keep the exact legacy error and prompt. Profiles with alternatives get COMPLETION_ERROR_NO_PUSH_OR_RESPONSE, the per-outcome rejections, and a continuation prompt that names an already recorded response and forbids re-posting it (plan 025/1, task 4). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ome rejections decideContinuation logs INFO with pushedChangesOutcome when the pushed-changes requirement is met, and carries the per-outcome rejections on the continuation WARN and on a new exhaustion WARN, so a run log alone explains the decision. Adds tests/helpers/tempGitRepo.ts and moves the repo-state tests onto it (plan 025/1, task 5). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uation postProcessResult takes the evaluatePushedChanges verdict (plus the requirement-specific error) instead of a boolean, logs the satisfied outcome or every rejection, and the adapter derives both from the same evidence read. secretOrchestrator forwards pushedChangesAlternatives into completionRequirements so the worker-side evaluator sees declared alternatives. A guard test keeps the shared completion modules free of agent-type names (plan 025/1, task 6). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pawn budget The three cascade-tools tests spawn the built oclif CLI, whose cold boot is 2-5 s and slower under full-suite CPU pressure. They already allow the spawn 30 s but ran under vitest's 5 s default, so a loaded machine turned them red (observed twice during npm run verify). One named constant now feeds both budgets. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rnatives Plan 025/1 changelog entry and progress record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- readRepoState git calls carry a 10 s timeout so a wedged git fails the comment-only outcome closed instead of blocking the turn - RepoStateEvidence records initialHeadSha; outcome checks are pure functions of evidence and the 'run start' fallback is gone - applyCompletionEvidence reads only the PR sidecar (no second git shell-out per turn once repoDir is wired) - getCompletionFailure passes the alternatives straight to pushedChangesFailure - evaluator tests pin the exported rejection constants instead of regexes - CLI-spawn test budgets live once in tests/helpers/cliSpawnBudget.ts, with the per-test budget strictly above the spawn budget; both new helpers are listed in tests/README.md Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Shared, fail-closed pushed-changes alternatives: schema flag, evidence model, evaluator, outcome-aware prompt, loop + post-run logging. Dormant until plan 3 opts the PR-comment agent in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
APPROVE — a clean, well-decomposed, fully-dormant infrastructure change. It teaches the shared completion contract to express pushed-changes alternatives without altering any agent's observable behaviour, and it's backed by exhaustive tests. I verified the affected unit suites (198 tests), typecheck, and lint all pass locally, and confirmed the flag is dormant (no built-in YAML declares pushedChangesAlternatives).
What I verified
Correctness / logic
evaluatePushedChangescheckspushed-changesfirst, then declared alternatives in order; first satisfied wins; otherwise one rejection per branch.OUTCOME_CHECKSis aRecord<PushedChangesOutcome, …>, so adding an alternative without a check is a compile error — good extension guard.rejectPRResponseordering (malformed → no response → no repo-state → dirty → HEAD moved) is sensible and fail-closed.readRepoStatetreats any git failure/timeout asundefined(unavailable ⇒ rejected), and untracked files count as dirty via--porcelain.- Backward-compat is genuinely preserved:
COMPLETION_ERROR_NO_PUSHandCONTINUATION_PROMPT_NO_PUSHare byte-identical to the previous inline literal, andresolvePushedChangesErrorreturns the legacy error whenever no alternative is declared (pinned by the "keeps the legacy no-push error and prompt" test). - The post-run gate guard (
pushedChangesSidecarPath !== undefinedinadapter.ts) preserves the exact skip semantics of the oldhasAuthoritativePushedChanges === falseguard, so non-native-tool runs behave as before.
Blast radius
postProcessResulthas a single caller (adapter.ts), correctly migrated fromhasAuthoritativePushedChangestopushedChangesEvaluation+pushedChangesError. No dangling references to the removed option.applyCompletionEvidencenow reads onlyprSidecarPath— safe, since it only consumesprUrl/prCommand, and it avoids a per-turn git shell-out (the full evidence read, incl. repo state, happens once per turn indecideContinuation).- The
superRefineonFinishHooksSchema(now aZodEffects) has no external consumers —ScmFinishSchemastays a plain object, soFinishHookFlags'z.inferis unaffected.DefinitionPatchSchema.partial()still typechecks.
Security: readRepoState uses execFileSync('git', [...], { cwd }) with fixed args and a 10 s timeout — no shell, no interpolation of initialHeadSha.
Non-blocking notes
- Forward risk for plan 2 (not this PR): the comment-only outcome hinges on
headSha === initialHeadSha, whereinitialHeadShawill beinput.headSha/CASCADE_INITIAL_HEAD_SHAandheadShacomes fromgit rev-parse HEAD(full 40-char). Plan 2/3's e2e tests should confirminput.headShais a full SHA, or thepr-responsebranch would silently always reject as "HEAD moved." Design decision 5 already anticipates this; flagging only so the plan-2 reviewer keeps it in view. - Legacy push-requiring agents now also get a
rejectionsfield on their WARN logs plus a new turns-exhausted WARN — both are additive observability wins, not behaviour changes.
LGTM.
🕵️ claude-code · claude-opus-4-8 · run details
…alternatives # Conflicts: # CHANGELOG.md
Resolves GHSA-2883-xcg3-v3hh (js-yaml maxTotalMergeKeys CPU DoS) that failed the `npm audit --omit=dev --audit-level=high` CI step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI Failures ResolvedFixes Applied
Verification (local, matching CI)
Only 🕵️ claude-code · claude-opus-4-8 · run details |
…om/mongrel-intelligence/cascade into feat/025-1-completion-alternatives
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
APPROVE — a well-scoped, fail-closed, and provably dormant refactor that centralizes the pushed-changes verdict into one shared evaluator. Verified locally: typecheck and lint clean (no warnings in changed files), and 198 affected unit tests pass (completion 34, postProcess 18, continuationLoop 17, schema 44, profiles 23, adapter 62).
What I verified
- Dormancy holds. No built-in
*.yamldeclarespushedChangesAlternatives, andsecretOrchestrator.tspopulates onlypushedChangesAlternatives(undefined everywhere) — notrepoDir/initialHeadSha/prResponseSidecarPath. SoreadRepoStatenever shells out to git,readPRResponseSidecarreads nothing, andevaluatePushedChangesonly ever evaluates thepushed-changesbranch at runtime. Existing agents are observably unchanged. - Legacy path preserved. For profiles without alternatives,
getCompletionFailurereturns the exactCOMPLETION_ERROR_NO_PUSH+CONTINUATION_PROMPT_NO_PUSH(pinned by test); the newrejectionsfield is purely additive to log payloads. - Schema refinement is safe. The
superRefineis attached toFinishHooksSchema(now aZodEffects, only ever consumed via.optional()), soScmFinishSchemastays a plainZodObjectandz.infer→FinishHookFlagsstill resolves. Type-only import ofPushedChangesAlternativeintocompletion.tsintroduces no runtime cycle. applyCompletionEvidencenarrowing is correct. It only ever consumesprUrl/prCommand, so reading justprSidecarPathis output-identical while avoiding a second git shell-out per turn.- Both gates share one evaluator.
decideContinuationandpostProcessResultboth route throughevaluatePushedChanges, and the adapter's guard (pushedChangesSidecarPath !== undefined) mirrors the previoushasAuthoritativePushedChangesguard, so the post-run verdict can't drift from the loop. Record<PushedChangesOutcome, OutcomeCheck>forces a compile error if a future alternative is added without a check — good type-safety.
Optional (non-blocking, already tracked)
- On the success path,
decideContinuationcallsevaluatePushedChangesa second time (aftergetCompletionFailurealready computed it) purely to logpushedChangesOutcome. It's a pure function over already-read evidence, so the cost is nil — the plan's Progress section already lists returning the evaluation fromgetCompletionFailureas a deferred cosmetic cleanup. A satisfied push run also logs the outcome twice (loop INFO + postProcess INFO); harmless. Neither needs action here.
The bundled js-yaml ^4.3.1 → ^4.3.2 bump (audit fix) has a consistent lock-file entry and is low risk.
🕵️ claude-code · claude-opus-4-8 · run details
Spec 025 plan 1 of 3 —
docs/plans/025-pr-comment-completion-and-codex-resume/1-completion-alternatives.md.done(parent specdocs/specs/025-…; the spec and all three plans are committed here). Independent of #1553 / #1554.What changed
hooks.finish.scm.pushedChangesAlternatives: [pr-response], valid only next torequiresPushedChanges: true(load-time refinement) — no agent can inherit the alternative implicitly.src/backends/completion.ts): PR-response sidecar (url+kind: top-level | inline-reply; anything else existing is malformed) and repository state (git status --porcelain+rev-parse HEADinrepoDirvsinitialHeadSha, 10 s timeout, any failure ⇒ unavailable).evaluatePushedChanges: push branch first, then each declared alternative; first satisfied wins; otherwise one rejection reason per branch. Fail-closed on missing/malformed/contradictory evidence.COMPLETION_ERROR_NO_PUSH_OR_RESPONSE, rejections, and a continuation prompt that names an already-posted response and forbids re-posting it.decideContinuationlogspushedChangesOutcomeon success, rejections on continuation and on a new exhaustion WARN;postProcessResultconsumes the same evaluation (duplicate push check removed) and logs the same.Also:
tests/helpers/tempGitRepo.ts(real-git helper), and an out-of-plantest(cli)commit giving the threecascade-toolsspawn tests a per-test budget above their 30 s spawn budget (they timed out at vitest's 5 s default under load; house rule: flaky = failing).Verification
npm run lint,npm run typecheck, and the four unit projects: green — 624 files / 11275 tests (run with--maxWorkers=4; an unrelated test run pushed the host past load 90 during this session).z:local-reviewgate, round 1: 0 must-fix; 3 should-fix applied (git timeout, DRY'd CLI budget, helper index), suggestions applied where cheap (single evidence read inapplyCompletionEvidence, pure repo-state checks, constant-based test pins). Deferred items are listed in the plan's Progress section..donefile.Commits
docs(spec)→chore(plan): lock→ 6×feat(...)(one per TDD task) →test(cli)→docs(changelog)→refactor(backends)(review findings) →chore(plan): done.Next:
/z:implement docs/plans/025-pr-comment-completion-and-codex-resume/2-pr-response-evidence.md(after this merges — plans are serial).🤖 Generated with Claude Code