chore(release): v0.12.0 — verdict emission and consumption - #113
Conversation
An unknown flag placed AFTER the positional target was dropped and the command exited 0. One typo -- `loop doctor <ws> --expect-chain-ancester <hex>` -- was a green tamper gate for a check that never ran, and `scaffold <target> --bogus x` wrote a contract past a flag it ignored. The exit 2 an unknown LEADING flag produced was not a guard: the flag name became argv[0] and failed the target-exists check, so the protection vanished the moment the flag moved after the path. Both orderings are legitimate for known flags, and both now behave alike. Generalizes a pattern the repo already had in two places -- _run_metrics and the approve/pause/resume/cancel family both reject residual dash-leading tokens -- and keeps the three per-flag wrong-command guards ahead of it, so "only valid for doctor/validate/verify" survives as the more useful message. test_mode_is_not_consumed_by_other_commands flips: it pinned scaffold creating a directory literally named --mode, which is the wart the per-flag guards' own comments called out.
Collects slice 4a (#106) and slice 4b (#110, #111, #112): loop verdict, opt-in keyless attestation, --compare, replay-based chain ancestry, anchor@1, signer trust, and the head-bearing subject. Neither slice shipped on its own; both sat in Unreleased and go out under one number. Beyond the version surfaces, this cut carries corrections a mechanical bump would not have made. The changelog said the predicate is handed to actions/attest as a subject-path. It is handed as predicate-path, alongside a SEPARATE subject file, and §23 says outright that the predicate bytes are deliberately not the subject — doctor validation_mode and tool.version live inside the predicate, so the same run projects different bytes in different environments. A reader following that sentence would hash the wrong file and fail every verification. "the three attestations minted before this release": the count was right, the scope word was wrong. attest.yml shipped inside this same window, so nothing predates the release; the three carrying the retired subject form are the pushes through c493804. Re-scoped to match what attest.yml and reference §24 already said. ADR 0002 decision 6 is withdrawn, in a new dated amendment. It described code-owner review as in force once the ruleset required it. The ruleset cannot require it: one collaborator, no self-approval, bypass_actors empty, current_user_can_bypass never, and no classic protection underneath. Flipping it would invert the intent rather than merely fail — maintainer-authored pull requests become unmergeable while bot-authored ones become gated, and agent work here lands under the maintainer's account. CODEOWNERS is demoted in place to what it honestly is, a record of the gate-defining surface, across the ADR, the changelog, reference §23 and §24, and the file's own header. test_docs_version.py's changelog check now ADDS 0.12.0 and keeps 0.11.0, and restores 0.10.0 — the previous cut replaced its predecessor's assertion instead of adding to it, leaving that heading with no cover. Baselines, live checkout: 1577 passed / 16 skipped with pyyaml+jsonschema+pytest, 1478 / 115 with pyyaml+pytest. Both legs +7 against pre-cut, which is the CLI guard's new tests plus the retired version-bump pin offset by the new code-owner pin. CI additionally installs hypothesis, which moves the number.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49decee464
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| stray = [arg for arg in argv if arg.startswith("-")] | ||
| if stray: | ||
| print(f"{command}: unknown option: {stray[0]}", file=sys.stderr) | ||
| print(_USAGE, file=sys.stderr) | ||
| return 2 |
There was a problem hiding this comment.
Reject option-looking values before consuming flag arguments
When a value-taking option is missing its value, _extract_value_flag consumes the following unknown option before this residual-token check can see it. For example, loop run <workspace> --executor --dry-run treats --dry-run as the executor identity and proceeds to dispatch rather than returning the promised unknown-option error; the same bypass affects mutating pause/resume/cancel flags. Reject a dash-leading next token as a missing value, or parse unknown options before extracting option values.
Useful? React with 👍 / 👎.
Cuts v0.12.0, collecting slice 4a (#106) and slice 4b (#110, #111, #112). Neither shipped on its own — both sat in
## Unreleased, so every adopter pinning a release today gets 0.11.0, which has no--compare, no--anchor, and no ancestry. This is also the standing gate on the Show HN launch.Two commits, deliberately separable:
fix(cli)chore(release)The CLI fix is not cosmetic
Measured on
mainbefore this branch:An unknown flag after the positional target was silently dropped, so one typo —
--expect-chain-ancester— was a green tamper gate for a check that never ran. The exit 2 you got with the flag before the path was never a guard:target = Path(argv[0])picked up the flag name and failed the target-exists check, so the protection vanished the moment the flag moved. Both orderings are legitimate for known flags and both now behave alike.This generalizes a pattern the repo already had in two places —
_run_metricsand theapprove/pause/resume/cancelblock — and reuses_run_metrics' exact message wording. The three per-flag wrong-command guards stay ahead of it, so--expect-chain-head is only valid for doctor/validate/verifysurvives as the more useful message.Behavior change.
test_mode_is_not_consumed_by_other_commandsflips: it pinnedscaffold --mode <target>creating a directory literally named--mode, which is verbatim the wart the per-flag guards' own comments called out. A script passing a flag this CLI never supported now fails where it previously passed quietly.Corrections the release notes needed
Three claims in the shipping prose were wrong. All three were caught by an adversarial verifier overturning the agent that first examined them.
subject-path→predicate-path. The changelog said the predicate is handed toactions/attestas asubject-path. It is handed aspredicate-path, alongside a separate subject file — and §23 says outright that the predicate bytes are deliberately not the subject, becausedoctor.validation_modeandtool.versionlive inside the predicate. A reader following that sentence would hash the wrong file and fail every verification.attest.ymlshipped inside this same window, so no attestation predates the release at all. The three carrying the retired subject form are the pushes throughc493804— which is whatattest.yml:181and reference §24 already said.ghcall site." True only of the fetch/verify path;action.ymlcallsgh apithree times in the PR-comment step. Narrowed, and the sentence now records that the denial shape is a captured fixture rather than a paraphrase (fix(action): classify gh's real denial stderr as contradicted, not unavailable #111).ADR 0002 decision 6 is withdrawn
It described code-owner review as "in force only once the repository ruleset requires it" — a switch awaiting a flip. The ruleset cannot flip it. Verified live:
SollanSystemsis the sole collaborator;main protectioncarriesrequired_approving_review_count: 0,require_code_owner_review: false,bypass_actors: [],current_user_can_bypass: "never";branches/main/protectionreturns 404, so there is no classic-protection admin escape either. GitHub forbids approving your own pull request.The sharper point, which the earlier framing missed: flipping it would not block "every PR" — it would leave every maintainer-authored PR touching an owned path permanently unmergeable while bot-authored ones became gated and approvable. That is close to the inverse of the intent, because agent work in this harness lands under the maintainer's account.
Escapes are closed, not merely unattractive: a GitHub App cannot be a code owner (CODEOWNERS admits only users and teams with write access); per-path
required_reviewersneeds teams, which needs an org, and this is a user-owned repo; a machine user is a signature from the same hand; a bypass actor restores mergeability by making the control inert..github/CODEOWNERSstays, demoted in place to a record of the gate-defining surface — corrected across the ADR, the changelog, reference §23 and §24, and the file's own header. The residual risk is now named rather than deferred, and the amendment declines to overclaim--expect-chain-ancestoras a substitute: it binds an actor who can rewrite the event store but not the repository, and is defeated in the same commit by anyone with merge rights, since the anchor file is editable in that diff.Form note: appended as a new dated section naming what it supersedes, matching the Slice 4b amendment. That amendment overrode decisions 2/4/5 with no in-body markers, so decision 6's original text at
:106and the## Standing limitsline at:153are left intact and governed by the amendment. Say the word if you'd rather have inline pointers — it is a one-line change and I deliberately followed precedent over improvement.Coverage drift, fixed
test_docs_version.py's changelog check now adds0.12.0and keeps0.11.0— and restores0.10.0. Every cut through v0.10.0 added its heading and kept the ones below; the v0.11.0 cut replaced its predecessor's line instead, leaving that heading with no cover. Also renamedtest_plugin_version_is_0_10_0, which had been asserting0.11.0.scripts/test_docs_slice4b.pywas absent from the release-surface list entirely — it postdates both prior cuts, so it was not in institutional memory. A naive bump leaves three of its tests red: two slice the changelog ontext.index("## Unreleased")and one asserts the version has not moved.Verification
pyyaml + jsonschema + pytestpyyaml + pytest(structural fallback)Live-checkout numbers; +7 in both legs — the CLI guard's 7 new tests, plus the retired version-bump pin offset by the new code-owner pin. 1593 collected either way.
validate_frontmatter9/9,self_eval13/13 (structural_pass_rate: 1.000). CI additionally installshypothesis, which moves the count.Deliberately not in this PR
The
action.ymlcapability precheck for a staleversion:pin. The obvious probe was refuted before it could ship:python - <<PYputs the workspace atsys.path[0], so any consumer repo containing aloop/directory shadows the pinned install and the probe passes while measuring the wrong package. It also probes_COMMANDSmembership, which says nothing about the flags actually invoked, and it would ship with zero CI coverage because none of this repo's four self-uses setversion:. That needs a console-script probe plus a negative-control CI leg pinning an old version — its own PR, not a release-cut line item. Filing it separately.Note the version-skew hazard is narrower than it looked:
action.yml:252uses flag-before-path ordering, which fails loudly on 0.11.0. It is a diagnostics problem, not a silent pass — and the durable half is fixed here for every future version.After merge
Tag push is not automatic and stays gated on your explicit go:
git tag v0.12.0 <squash-sha>thengit push origin v0.12.0triggers the PyPI publish, which is irreversible. Then verify the funnel withuvx loop-engineer@0.12.0 inspect examples/coverage-repairfrom a scratch clone.