Skip to content

v2.17.0 - Merge gate on main, downstream canary, driver conformance, hook golden corpus

Choose a tag to compare

@admarble admarble released this 21 Sep 00:45
· 166 commits to main since this release
ad499b5

Fixed

  • sequant init on an initialized project no longer replaces a hand-written AGENTS.md (#1098). init wrote the generated file unconditionally, so init --agent codex --yes erased a user-owned AGENTS.md that sync would have preserved (#990). It now applies the same rule as sync — only an unmodified sequant-marked file is regenerated, anything else is reported preserved and replaced only by --force. Found by the new downstream canary; the writer-state matrix (#1097) had pinned the four cells as tracked defect #1123 and now expects them preserved.

  • Codex usage-limit turn.failed is now a typed error (#1087). CodexDriver classified every turn.failed as a generic SequantError, so a quota-exhausted phase looked like any other failure: the -Q quality loop re-ran into the same limit and --auto-wait could not engage. classifyCodexTurnFailure maps the usage-limit text to BillingError (reset beyond a fixed 7-day horizon, past, or unparseable; resetsAtText and resetsAt in metadata), to a RateLimitError with resetsAt when the reset is within the horizon, and throttle wording (rate limit exceeded, 429, too many requests) to a retryable RateLimitError. Anything else keeps SequantError with code turn-failed.

  • The pre-PR sync merges origin/main into a branch that is already pushed instead of rebasing it (#1069). Rebasing rewrote pushed SHAs and could only be landed by force-push. rebaseBeforePR now merges when the branch has an upstream other than the base ref (a fresh worktree add -b … origin/main branch tracks origin/main and still rebases), aborts the merge on conflict, and prints merged origin/main or rebased onto origin/main.

  • A failing QA verdict (AC_NOT_MET) is now posted to the issue (#1070). postQaVerdictComment was gated on result.success, but AC_NOT_MET is reported as a phase failure, so the one review that blocks a merge left no comment — its findings lived only in the rotating, gitignored .sequant/logs. The gate is now result.verdict alone: a verdict is only set once one parsed, so turn-capped and unparseable-verdict phases still post nothing (#964 AC-4). Each AC_NOT_MET round in a quality loop now posts its own comment.

  • parseQaSummary counts checklist- and bold-prefixed AC lines, and the QA verdict comment no longer prints AC coverage: 0/N met under a passing verdict (#1073, #1095). A checklist-shaped report (- [x] **AC-1**: …) matched no table row and parsed as 0/1 met under an AC_MET verdict. Checklist lines now count (checkbox = MET; a table row wins on a duplicate id), and buildQaVerdictComment omits the count line when the parser found no counts or acMet === 0 under READY_FOR_MERGE/AC_MET*.

Added

  • Golden corpus for pre-tool.sh (#1094). __tests__/fixtures/hook-corpus.jsonl records the exit code and block reason the hook gives every command form it has been asked to judge (the existing suite, the hook's own block log, and Claude Code transcripts, secrets and home paths redacted). __tests__/hook-corpus.integration.test.ts replays it through all three hook copies, so a changed verdict fails until the snapshot is updated in the same PR. npx tsx scripts/hook-corpus.ts --diff <ref> prints only the verdict changes against a ref; --harvest adds new commands. The exec skill now requires listing changed verdicts under Hook verdicts changed: when pre-tool.sh is edited.

  • CI canary job: a downstream fixture project on every PR (#1098). __tests__/canary/downstream.integration.test.ts does a real npm install of the previous minor release, runs its init, customizes the project the way real ones are (constitution, .claude/settings.local.json matcher group, hand-written AGENTS.md, tuned .sequant/settings.json, stale ^1.20.0 devDependency), then drives the PR's build through sync --dry-run, sync, init --agent codex --yes and update. It asserts dry-run decisions equal the files that changed, every customization survives byte-for-byte, no {{PLACEHOLDER}} lands under .claude/, doctor exits 0, and a real MCP initialize over stdio from the fixture's cwd — with the stale local node_modules/sequant present — reports the PR's version (the #1084 regression test). It runs in a new canary vitest project (npm run test:canary) that npm test excludes, so the required test check never depends on the npm registry.

  • /release publishes to the next dist-tag first and refuses latest without --soaked (#1098). Step 9 now runs npm publish --tag next; new Step 9b prints the soak checklist and only runs npm dist-tag add … latest when --soaked was passed. Pinned in all three skill copies by release-skill-soak.test.ts.

  • Driver conformance suite (#1096). src/lib/workflow/drivers/__tests__/driver-conformance.test.ts enumerates every registered driver from the registry (listDriverNames()) and checks six contract items per driver: commit inside the sandbox, network, typed error mapping, env injection, structured outcome, and skill loading. The Codex probes run the real codex sandbox with the driver's own writable roots. docs/guides/writing-an-agent-driver.md states the contract for new drivers. Gaps in the aider and opencode drivers are tracked in #1115.

  • Writer state-matrix tests (#1097). src/lib/__tests__/writer-state-matrix.test.ts runs init, sync and update (each with and without --force, plus --dry-run for the latter two) against nine destinations in seven starting states (absent, sequant-owned, user-modified, marker + changed body, local symlink, foreign symlink, directory) on a real temp project, and asserts every dry-run prints the decision its apply run acts on. It pins three writer defects instead of fixing them: symlink write-through (#1122), init replacing a user-modified AGENTS.md (#1123), and a raw EISDIR crash on a directory at a template path (#1124).

  • The ownership gate's side (b) now registers every writeFile( site individually (#1106). #1090's scan keyed sites by enclosing function and matched await writeFile( only, so a second write to a new destination inside an already-registered function, a void writeFile(...), or a writer declared as const foo = async () => {…} (which inherited the previous function's key) all passed silently. src/lib/ownership-gate.ts scans every writeFile( call, attributes it to its innermost named function by brace scope, and keys the registry by file#function#destination-expression; a test cross-checks the site count against a naive count so a scanner slip fails loudly. Test-only: the three real writers are unchanged.

  • Copy-mode sync now replaces an existing scripts/dev symlink instead of writing through it (#1053). With no local node_modules/sequant, sync left foreign links (an npx cache, a sibling checkout) in place and overwrote their targets. Symlinks are now unlinked and replaced by a copy; regular files are left alone without --force. sync --dry-run lists each replacement as old → (copy), and doctor names the fix that will actually change a machine-specific link.

  • The test suite is hermetic against the orchestrator's environment (#1086). vitest.global-setup.ts scrubs every SEQUANT_* variable by prefix before any worker starts, so a phase agent running a test file under sequant run sees the same result as a clean shell. Before this, SEQUANT_ORCHESTRATOR and SEQUANT_WORKTREE turned three unrelated files red inside every exec phase.

Changed

  • /release lands the release commit through a PR (#1131). The main ruleset (#1109) rejects direct pushes, so Step 6 now branches, opens the release PR, waits for test and canary, squash-merges, and Step 7 tags the merge on main.

  • A merge gate on main, and a mechanical proof for any "pre-existing failure" claim (#1093). The main ruleset enforced only deletion and non_fast_forward, and targeted no ref at all (conditions.ref_name.include: []), so a PR could merge with stale or red CI — which is how #1036, #1042 and #1048 each shipped with "3 pre-existing failures, unrelated to this diff" in the body a week before those failures (#1086) stopped a gate run. scripts/ruleset-main.sh --print now emits the corrected payload (~DEFAULT_BRANCH, the existing two rules, and a required_status_checks rule naming the test check with strict_required_status_checks_policy: true), documented field by field in docs/internal/graph-2026-09-guard/ruleset.md; the owner applies it by hand in #1109, and the script has --print as its only mode so no agent can. ci.yml's build job is renamed test and loses its single-entry matrix, because a matrixed job reports the context build (22.x) and a required context that never reports blocks every merge to main permanently. scripts/settle-against-base.sh <test-file> runs a named test at HEAD and again at the base commit in the same worktree with every SEQUANT_* var unset, prints both results as a ### Settled against base block for the PR body, and restores the branch and any uncommitted work on every exit path — it also distinguishes "the test file does not exist at base" from "the test failed at base", which is the difference between a new test and a pre-existing failure. /exec now calls it for every red test outside the diff; /qa §2a sets settle_evidence_status and §7 step 4 floors an unbacked claim at AC_MET_BUT_NOT_A_PLUS. CLAUDE.md gains a ## Red main rule — a failing push run on main is reverted first and debugged second, and the revert PR references the run — repeated in the release skill's pre-flight checks.

  • The exec skill and CLAUDE.md now state the SEQUANT_MUTATION marker contract (#1104). failedTest must start with the test file path (<file> > <describe> > <case>) and the payload must stay flat; /qa §6i already enforced both without telling authors, which tripped two of the four gate PRs before 2.16.0. A gate test in scripts/exec-skill-marker.test.ts pins the section.

Full Changelog: v2.16.0...v2.17.0