Skip to content

chore(release): v0.12.0 — verdict emission and consumption - #113

Merged
SollanSystems merged 2 commits into
mainfrom
release/v0.12.0
Jul 30, 2026
Merged

chore(release): v0.12.0 — verdict emission and consumption#113
SollanSystems merged 2 commits into
mainfrom
release/v0.12.0

Conversation

@SollanSystems

Copy link
Copy Markdown
Owner

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:

Commit What
fix(cli) Generic unknown-flag guard — a real silent-pass in the hard gate
chore(release) Version surfaces, changelog retitle + four factual corrections, ADR 0002 amendment

The CLI fix is not cosmetic

Measured on main before this branch:

loop doctor <path> --totally-made-up-flag xyz    -> EXIT 0   {"ok": true}
loop doctor --totally-made-up-flag xyz <path>    -> EXIT 2
loop scaffold <target> --totally-made-up-flag x  -> EXIT 0   contract created

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_metrics and the approve/pause/resume/cancel block — 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/verify survives as the more useful message.

Behavior change. test_mode_is_not_consumed_by_other_commands flips: it pinned scaffold --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.

  1. subject-pathpredicate-path. 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, because doctor.validation_mode and tool.version live inside the predicate. A reader following that sentence would hash the wrong file and fail every verification.
  2. "three attestations minted before this release." The count was right; the scope word was wrong. attest.yml shipped inside this same window, so no attestation predates the release at all. The three carrying the retired subject form are the pushes through c493804 — which is what attest.yml:181 and reference §24 already said.
  3. "the single gh call site." True only of the fetch/verify path; action.yml calls gh api three 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: SollanSystems is the sole collaborator; main protection carries required_approving_review_count: 0, require_code_owner_review: false, bypass_actors: [], current_user_can_bypass: "never"; branches/main/protection returns 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_reviewers needs 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/CODEOWNERS stays, 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-ancestor as 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 :106 and the ## Standing limits line at :153 are 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 adds 0.12.0 and keeps 0.11.0 — and restores 0.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 renamed test_plugin_version_is_0_10_0, which had been asserting 0.11.0.

scripts/test_docs_slice4b.py was 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 on text.index("## Unreleased") and one asserts the version has not moved.

Verification

Leg Pre-cut This branch
pyyaml + jsonschema + pytest 1570 / 16 1577 / 16
pyyaml + pytest (structural fallback) 1471 / 115 1478 / 115

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_frontmatter 9/9, self_eval 13/13 (structural_pass_rate: 1.000). CI additionally installs hypothesis, which moves the count.

Deliberately not in this PR

The action.yml capability precheck for a stale version: pin. The obvious probe was refuted before it could ship: python - <<PY puts the workspace at sys.path[0], so any consumer repo containing a loop/ directory shadows the pinned install and the probe passes while measuring the wrong package. It also probes _COMMANDS membership, 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 set version:. 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:252 uses 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> then git push origin v0.12.0 triggers the PyPI publish, which is irreversible. Then verify the funnel with uvx loop-engineer@0.12.0 inspect examples/coverage-repair from a scratch clone.

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.
Copilot AI review requested due to automatic review settings July 30, 2026 13:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread loop/__main__.py
Comment on lines +464 to +468
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@SollanSystems
SollanSystems merged commit 6fddb3b into main Jul 30, 2026
14 checks passed
@SollanSystems
SollanSystems deleted the release/v0.12.0 branch July 30, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants