@attalabs/vinaya 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/aeg-root/enforcement.md +2 -2
- package/aeg-root/milestone-model.md +2 -0
- package/aeg-root/process.md +1 -1
- package/aeg-root/roles/planner.md +2 -0
- package/aeg-root/roles/principal.md +4 -2
- package/aeg-root/roles/reviewer.md +6 -2
- package/aeg-root/roles/security.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +128 -22
- package/dist/checks/bin/check-branch-topology.js +148 -24
- package/dist/checks/bin/check-brief-shape.js +430 -32
- package/dist/checks/bin/check-changeset-coverage.js +148 -24
- package/dist/checks/bin/check-closes-n.js +148 -24
- package/dist/checks/bin/check-coherence.js +148 -24
- package/dist/checks/bin/check-dead-branch-push.js +110 -22
- package/dist/checks/bin/check-dispatch-readiness.js +148 -24
- package/dist/checks/bin/check-doc-coverage-push.js +148 -24
- package/dist/checks/bin/check-doc-coverage.js +148 -24
- package/dist/checks/bin/check-doctrine-no-procedures.js +110 -22
- package/dist/checks/bin/check-doctrine-portability.js +148 -24
- package/dist/checks/bin/check-evidence-fresh.js +650 -63
- package/dist/checks/bin/check-exec-bits.js +148 -24
- package/dist/checks/bin/check-first-push-dispatch.js +148 -24
- package/dist/checks/bin/check-issue-assignment.js +148 -24
- package/dist/checks/bin/check-main-branch-refusal.js +110 -22
- package/dist/checks/bin/check-no-disk-state.js +110 -22
- package/dist/checks/bin/check-pr-premise-reassert.js +110 -22
- package/dist/checks/bin/check-pr-report-density.js +110 -22
- package/dist/checks/bin/check-quoted-command.js +148 -24
- package/dist/checks/bin/check-reader-resolvable-prose.js +148 -24
- package/dist/checks/bin/check-registry-gates.js +110 -22
- package/dist/checks/bin/check-retired-vocabulary.js +148 -24
- package/dist/checks/bin/check-review-gate.js +169 -77
- package/dist/checks/bin/check-single-plan-pr.js +110 -22
- package/dist/checks/bin/check-surface-scope.js +148 -24
- package/dist/checks/bin/check-test-plan.js +110 -22
- package/dist/checks/bin/check-token-collection-wired.js +110 -22
- package/dist/checks/bin/check-token-report.js +110 -22
- package/dist/checks/bin/check-workspace-escape.js +148 -24
- package/dist/index.js +1584 -572
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,9 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
26
26
|
| `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
|
|
27
27
|
| `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
|
|
28
28
|
| `vinaya brief render <tranche> <n> --surfaces <glob,...> [--out <path>]` | Emit the twelve-section brief skeleton from the task Issue and the tree, with every mechanically-derivable section filled: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file, and Out of surface from the Issue's own `## Surface` `out:` list), §6 from the Issue's `## Parts`, §7 from the `.vinaya/doc-owners` derivation, §9 from the Issue's `## Test plan`, §10 from the Issue's `## Stop conditions` plus the rationale's Stop-and-escalate field, and every remaining section from the Issue's eight-field Planner rationale. Refuses, naming the missing section, when a section cannot be derived: no Issue, the dispatch gate not clear, a `--surfaces` glob matching no tracked file, or the Issue missing/malformed `## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` — never a bracketed placeholder. Never writes under `aeg-root/` or to the Issue — stdout, or `--out <path>`, only; a brief is pasted to the Developer, never committed. |
|
|
29
|
-
| `vinaya task
|
|
29
|
+
| `vinaya task brief <tranche> <n>` | Preparation only: render the same brief `brief render` renders, then post it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. Starts no agent under any circumstances — there is no `--agent` flag. Principal-only: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. The successor to `task dispatch` for the preparation step; `task run` is the full unattended run. |
|
|
30
|
+
| `vinaya task dispatch <tranche> <n> [--agent claude \| codex \| gemini] [--model <name>]` | **Deprecated** — prefer `task brief` (preparation only) or `task run` (the full unattended loop). Renders the same brief `brief render` renders, then posts it once as a frozen Issue comment (first line `<!-- aeg:brief:v1 -->`, second line `Brief hash: <sha256>` over everything below it) — refusing outright, naming the existing comment's URL, if the Issue already carries one. With `--agent`, starts the Developer through `dispatchRole` (`apps/cli/src/lib/dispatch.ts`) when that function is available; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block. `--model <name>` names the model to run, passed through to `dispatchRole` and always taking precedence over any class-derived resolution; without it, the task's own Issue rationale is read for its "Suggested agent-class" (`high`/`mid`/`fast`) and resolved through the chosen vendor's own class-to-model table, falling through to that vendor's default when neither resolves. Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist. |
|
|
31
|
+
| `vinaya task run <tranche> <n> --agent <claude \| codex \| gemini>` | One command from a planned Issue to a reviewed pull request, exactly one developer started. Composes `task brief`'s own preparation (starts no agent) with `dev-review-loop` (whose own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task) — nothing else. A brief already frozen on the Issue is reused, not re-posted; a task whose Issue refuses preparation is refused before any agent starts, with nothing posted; a frozen brief whose developer branch already has an open pull request refuses a second start. Exit `0` with the PR URL on a published, reviewed pull request; exit `1` with the exact `vinaya dev-review-loop --resume <pr>` command on a pause; exit `2` on a usage/argv error (an unrecognized flag, a missing `--agent`); exit `3` on any other failure (a refused preparation, an open-PR refusal) — never sharing `1` with a pause, so an unattended host tells the two apart from the exit code alone. The loop's own existing `--resume` flag is how a paused run continues, never a flag on this command. |
|
|
30
32
|
| `vinaya pr create --body-file <path> --title <title> [--label ...] [--validate-only] [--json]` | Open a pull request after full brief-schema validation of the body against `vinaya.config.json`'s configured sections — never a body containing the retired `<!-- aeg:brief:start -->`/`<!-- aeg:brief:end -->` markers, refused outright: the brief lives on the task Issue's `aeg:brief:v1` comment now (`vinaya task dispatch`), never split out of or pasted into the PR body. `--validate-only` runs every gate and reports PASS without opening anything; `--json` for the enveloped machine form. |
|
|
31
33
|
| `vinaya issue objectives edit <n> --add "<sentence>" \| --drop O<k> \| --replace O<k> "<sentence>" --reason "<text>"` | Rewrite a task Issue's `## Objectives` section by command — the Principal's way to change a task's scope mid-flight, findable and versioned rather than a silent hand-edit. Exactly one of `--add`/`--drop`/`--replace` plus a non-empty `--reason` is required; the edit runs through the same validated `issue edit` write path (`writeValidatedIssueEdit`) as `vinaya issue edit` itself. `--drop` never renumbers the survivors — a drop that leaves the list non-contiguous from `O1` is refused with `objectivesOf`'s own parser message, since task 1's contiguous-from-O1 grammar and this command's never-renumber rule can genuinely contradict on a real drop, and that contradiction is reported rather than papered over. Splices the rendered section back in place, leaving every other byte of the body untouched, then posts one comment marked `<!-- aeg:objectives:v<k> -->` carrying the previous list, the new list, the reason, and the new version hash — `k` counted on the forge at post time, never from a local file. |
|
|
32
34
|
| `vinaya log flush --issue <n> \| --pr <n>` | Post a target Issue or PR's outbox (`~/.vinaya/outbox/<owner>-<repo>/<issue-or-none>.ndjson`, written by `log()`) as one or more comments, each opening with `<!-- aeg:log:<run_id>:<seq_from>-<seq_to> -->` followed by a fenced `ndjson` block. Splits first at `run_id` boundaries — a maximal run of consecutive lines sharing one `run_id`, so an interleaved outbox never produces a range spanning a gap — then at `FORGE_COMMENT_MAX_CHARS` (65536); a single line too large to fit alone is refused by its seq, never split. `--pr <n>` resolves the Issue from that PR's body `Closes #N` line and posts on the PR instead of the Issue; a body with none is refused. Logs its own `forge_write` line (`validated` before posting, `written` with every comment id after the last post succeeds, or `refused` with gh's error) through `log()` into the same outbox before truncating, so that line rides to the next flush; the outbox is truncated only to the lines confirmed posted, and a gh failure leaves the rest untouched. `--json` for the enveloped machine form. |
|
|
@@ -43,7 +45,7 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
|
|
|
43
45
|
| `vinaya milestone close --slug <slug>` | Close a tranche's Milestone — the gated replacement for the raw `gh api .../milestones/<n> -X PATCH -f state=closed` recipe the Tranche Archivist used to run on faith. Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge. `--validate-only` verifies attachment without closing; `--json` for the enveloped machine form. |
|
|
44
46
|
| `vinaya quickstart [--yes] [--dry-run]` | Guided wizard that runs `init` → optional doc-owners bind → optional project registration → commit → `demo break` → `doctor` → `push` in sequence, prompting between steps. `--yes` forwards straight through to `init` and answers every one of quickstart's own prompts with its documented default (skip the two optional steps, run the refusal-then-fix proof, push) — no prompt is opened at all, so the command completes with no human at the keyboard rather than merely with stdin closed. `--dry-run` also forwards to `init` and stops immediately after its preview — nothing is installed, so no later step runs against an uninstalled repo. |
|
|
45
47
|
| `vinaya release [--dry-run] [--allow-any-commit]` | Run this repo's own publish sequence in one command (`apps/cli/specs/self-hosting.md`, "How the published version is produced"). Refuses unless HEAD is the default branch, the tree is clean, HEAD equals `origin/<default>` (after `git fetch origin`), HEAD's commit subject starts with `Chore(release): Version packages` (unless `--allow-any-commit`), and `npm whoami` exits `0` — each its own refusal naming the fix. Then streams `bun install --frozen-lockfile`, `bun run build`, `bun run changeset:publish`, and a real `git push origin --tags`, so the repo's own generated pre-push hook sees the push exactly as any other push would. Afterward prints `npm view <pkg> version` for every tag now on HEAD, noting registry lag on `@attalabs/vinaya` (observed ~20 minutes) when it still shows the previous version. `--dry-run` stops after the preconditions and prints the plan; publishes nothing. |
|
|
46
|
-
| `vinaya dispatch <role> --agent claude \| codex \| gemini --prompt-file <path> [--task <n> \| --pr <n>] [--round <n>] [--resume <id>] [--model <name>]` | Start a role's headless agent session, attributed: `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` are set on the CHILD's environment only, never on this process's own — a role started this way is what makes its later `vinaya` calls (the Stop hook, a nested dispatch) attributable instead of `unattributed`. The prompt is read from `--prompt-file` and sent on the child's stdin, never argv. `--model <name>` is passed to the chosen vendor through that vendor's own model flag, and the dispatch log's `model` field never records the vendor name; omitted, no model flag reaches the vendor and its own default decides, exactly as before this flag existed. A model shaped as another vendor's own (e.g. a Claude alias passed to `codex`) is refused by name before any spawn, naming the vendor and what it accepts. Before the vendor produces any output, and for any dispatch that never completes, the log records the requested value as a marked `requested:<name>` label (or `default` when none was named) — never presented as a confirmed observation. Once the vendor's own headless output actually reports which model ran (Claude's `modelUsage` object key, Gemini's `stats.models` object key(s) — Codex emits no such receipt in any event), that bare, unprefixed model name is recorded instead, even when it differs from what was requested or when no model was requested at all. Refuses by name, before any spawn, when the named vendor binary is absent from `PATH` or present but not executable. While the child runs, its raw stdout/stderr bytes are teed (never taken from the same stream the outcome parser reads) to a machine-local file under `~/.vinaya/dispatch-output/<effect-id>.log` — never inside the repo tree — whose path is printed once to this process's own stderr so an operator can `tail -f` it; a heartbeat line reporting elapsed time prints there every minute so a working agent is distinguishable from a hung one without inspecting processes by hand. A wall-time ceiling (`dispatch.timeoutMs` in config, default four hours) sends `SIGTERM` then, after a grace window, `SIGKILL`; a warning prints before the ceiling fires (at most 5 minutes ahead, sooner for a shorter configured ceiling) so a timeout is an expected, visible event rather than a silent disappearance. Records `dispatched`/`outcome_received`/`dispatch_failed` through the Vinaya Log's `dispatch` family (`apps/cli/specs/log.md`); when `--task`/`--pr` is given, flushes that outbox via `vinaya log flush` immediately after. A successful dispatch's own vendor session/thread identifier is returned as `resumeId` (printed alongside the other fields); passing that value back as `--resume <id>` on a later call swaps in that vendor's own resume invocation instead of starting a fresh session. That id is also durably recorded — never only printed — at `~/.vinaya/dispatch-resume/<owner>-<repo>|unresolved/<role>-<agent>-<issue<n>\|pr<n>\|unscoped>.json` (the repo segment keeps two different repos' own same-numbered task from overwriting each other's record on this one machine-wide home), overwritten by each dispatch's own outcome; a run that stops with a question is answered by reading the id from that file (or from the run's own printed output, if that terminal is still open) and running `vinaya dispatch <role> --agent <vendor> --resume <id> --prompt-file <answer-file>` — the same session continues from where it stopped, rather than being killed and re-dispatched fresh. There is no live channel: an unattended dispatch never blocks on a read waiting for an answer, so a stop condition always degrades to record-and-exit, and the resume path above is how it is answered afterward. |
|
|
48
|
+
| `vinaya dispatch <role> --agent claude \| codex \| gemini --prompt-file <path> [--task <n> \| --pr <n>] [--round <n>] [--resume <id>] [--model <name>]` | Start a role's headless agent session, attributed: `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` are set on the CHILD's environment only, never on this process's own — a role started this way is what makes its later `vinaya` calls (the Stop hook, a nested dispatch) attributable instead of `unattributed`. The prompt is read from `--prompt-file` and sent on the child's stdin, never argv. `--model <name>` is passed to the chosen vendor through that vendor's own model flag, and the dispatch log's `model` field never records the vendor name; omitted, no model flag reaches the vendor and its own default decides, exactly as before this flag existed. A model shaped as another vendor's own (e.g. a Claude alias passed to `codex`) is refused by name before any spawn, naming the vendor and what it accepts. Before the vendor produces any output, and for any dispatch that never completes, the log records the requested value as a marked `requested:<name>` label (or `default` when none was named) — never presented as a confirmed observation. Once the vendor's own headless output actually reports which model ran (Claude's `modelUsage` object key, Gemini's `stats.models` object key(s) — Codex emits no such receipt in any event), that bare, unprefixed model name is recorded instead, even when it differs from what was requested or when no model was requested at all. Refuses by name, before any spawn, when the named vendor binary is absent from `PATH` or present but not executable. Every line this command and the loop it can drive print to the terminal is prefixed with its role (e.g. `[developer]`) and colour-coded per role, with the coordinator/loop's own lines in a distinct colour from every role — colour is applied only on a live TTY and is disabled entirely, prefix kept, when `NO_COLOR` (https://no-color.org) is set to any value. While the child runs, its raw stdout/stderr bytes are teed (never taken from the same stream the outcome parser reads) to a machine-local file under `~/.vinaya/dispatch-output/<effect-id>.log` — never inside the repo tree — whose path is printed once to this process's own stderr so an operator can `tail -f` it; a heartbeat line reporting elapsed time prints there every minute so a working agent is distinguishable from a hung one without inspecting processes by hand. A wall-time ceiling (`dispatch.timeoutMs` in config, default four hours) sends `SIGTERM` then, after a grace window, `SIGKILL`; a warning prints before the ceiling fires (at most 5 minutes ahead, sooner for a shorter configured ceiling) so a timeout is an expected, visible event rather than a silent disappearance. Records `dispatched`/`outcome_received`/`dispatch_failed` through the Vinaya Log's `dispatch` family (`apps/cli/specs/log.md`); when `--task`/`--pr` is given, flushes that outbox via `vinaya log flush` immediately after. A successful dispatch's own vendor session/thread identifier is returned as `resumeId` (printed alongside the other fields); passing that value back as `--resume <id>` on a later call swaps in that vendor's own resume invocation instead of starting a fresh session. That id is also durably recorded — never only printed — at `~/.vinaya/dispatch-resume/<owner>-<repo>|unresolved/<role>-<agent>-<issue<n>\|pr<n>\|unscoped>.json` (the repo segment keeps two different repos' own same-numbered task from overwriting each other's record on this one machine-wide home), overwritten by each dispatch's own outcome; a run that stops with a question is answered by reading the id from that file (or from the run's own printed output, if that terminal is still open) and running `vinaya dispatch <role> --agent <vendor> --resume <id> --prompt-file <answer-file>` — the same session continues from where it stopped, rather than being killed and re-dispatched fresh. There is no live channel: an unattended dispatch never blocks on a read waiting for an answer, so a stop condition always degrades to record-and-exit, and the resume path above is how it is answered afterward. |
|
|
47
49
|
| `vinaya dev-review-loop --task <n> --agent claude \| codex \| gemini [--json]` / `vinaya dev-review-loop --resume <pr> --agent … [--json]` | Dispatch the developer through `dispatchRole` with the brief read from the task Issue's frozen `aeg:brief:v1` comment, wait for the PR it opens, then run rounds by calling `assessRound` (`@attalabs/aeg-core`) — the entire policy — with observations read from the forge (`git ls-remote` for the head, the check-runs API for CI, `<!-- aeg:principal:ruling:<pr>-<k> -->`-marked comments for rulings) until it returns `publish` or `pause`. Each round's reviewer and security verdicts are dispatched fresh (never resumed), rendered through `review post`'s own render functions, and held as local files under the outbox — nothing is posted to the PR (`gh pr comment`) before `publish`. At `publish`, the two held verdicts and a `renderSummary` comment are posted, in order, each re-read back through the same extractors the merge gate uses, each idempotent across a rerun. At `pause`, one comment marked `<!-- aeg:loop:paused:<reason> -->` is posted (carrying the exact `--resume <pr>` command) and the process exits non-zero; `--resume <pr>` reads that held state and a since-posted Principal ruling off the same PR and continues. Full account: `apps/cli/specs/loop.md`. The developer's own session IS resumed every round via `dispatchRole`'s `resumeId`; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than falling back to a fresh session. |
|
|
48
50
|
|
|
49
51
|
Which one lib function backs each command, and today's dated exemptions where it calls more than that one: `apps/cli/specs/surface.md`, enforced by `apps/cli/tests/surface-index.test.ts`.
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -137,7 +137,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
137
137
|
| Typecheck + unit tests | Ever had a change silently break something it wasn't even touching? | ci | Re-runs the type checker and the unit tests for every package this change can reach. | Packages affected by this PR's diff, plus their full transitive-dependent set (`turbo --affected`; application builds are verified by the deployment pipeline). A diff touching no package (docs/workflow-only) runs 0 packages — sound, since repo-file gates like `verify-docs`/coherence run in their own jobs regardless (task 27) | repo-own | `.github/workflows/ci.yml` |
|
|
138
138
|
| Conventions | Ever opened a PR full of inconsistent formatting and naming? | ci | States where formatting/naming conventions stand in this repo: currently unenforced. | **Nothing re-checks lint/format or commit-message grammar in this repo today** — the managed hooks run only the `vinaya check` suite, and the one CI workflow that builds the code (`ci.yml`, the implementation named here; its typecheck + unit-test substance belongs to the `Typecheck + unit tests` row) carries no lint or commitlint step. Those conventions are discipline, not a gate, until a step is added. | repo-own | `.github/workflows/ci.yml` |
|
|
139
139
|
| AI review | Ever wished every PR got a second pair of eyes, even at 2am? | ci | Requires independent review verdicts to exist on every pull request before merge. | No automated reviewer workflow is installed in this repo — review passes are separate, fresh-context dispatched agent sessions that post verdict comments on the PR. `vinaya-review-verdict.yml` evaluates a landed verdict comment (only principal-allowlisted authors count — the same verdict-author verification rule the Review gate applies) and re-runs the required review-gate check so it can go green natively. Until clean verdicts exist, the required check stays red by design. | repo-own | `.github/workflows/vinaya-review-verdict.yml` |
|
|
140
|
-
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. **Mechanical checks are a third precondition:** a task-branch PR must additionally carry at least one reported, green (non-review) check-run for the PR's current head — `checkReviewGate` reads a caller-supplied `mechanicalChecks` list (populated via `gh pr checks --json name,bucket`, with this repo's own `vinaya review gate` check-run name excluded so the gate cannot judge its own status) and fails the same way an unclean or unbound verdict does, naming which check is not green, or that none have reported yet. The exclusion of the gate's own check-run name is repo-specific and lives in the thin CLI shims that resolve check-run status via `gh`, never inside the pure evaluator itself, since that evaluator ships to every adopter under a different workflow name. **A verdict is also bound by PATCH IDENTITY** (this task): the gate compares each side's `git diff <base>...<sha>` output run through `git patch-id --stable`, for the judged head and the current head, and a verdict whose sha binding has lapsed still counts when the two identities are equal and neither is `null`. A merge from the main branch, or a rebase that leaves the PR's own patch untouched, therefore no longer voids a review that already read exactly those changes — the round it used to cost proved nothing. Fails closed on every uncertainty: `null` on either side (an unreachable judged head after a force-push, a shallow clone, no git) is "cannot answer", never "they match". The known limit is stated rather than papered over — a base that moved under an identical patch can carry a semantic conflict the earlier review could not have seen, and this binding still holds; that is the same limit GitHub's own stale-review rule has, and the green-mechanical-checks requirement this gate already imposes at the new head is the guard for it. **A verdict is also bound to the OBJECTIVES LIST it was judged against**: the winning comment's `Objectives version:` line (a hash `objectivesVersion` computes over the closed Issue's, or the PR body's own, `## Objectives` list) must equal the current version of that same list — a verdict cast before an edit to the objectives no longer counts as clean, even when its head binding still holds. `input.objectivesVersion === null` (an Issue below the objectives cutover, or no resolvable list at all) skips this half of the binding entirely, so the pre-cutover PR stock keeps passing unchanged. **Resolving that `objectivesVersion` value diverges between the two bins that call `checkReviewGate`**: this repo's live pre-merge path, `apps/cli/src/checks/bin/check-review-gate.ts`, resolves it for real (Issue-then-body, fail-closed on every unresolvable case, waiver-checked first so an actor-verified `vinaya/waiver:review` label is never blocked by that resolution); the older reference script named in this row's own implementation column, `packages/aeg-core/bin/verify-review-gate.ts`, has no equivalent Issue-fetch machinery and always passes `objectivesVersion: null`, unconditionally skipping this half of the binding. Read the objectives-version paragraph above as describing `checkReviewGate`'s pure evaluation logic (shared by both callers); for which file actually resolves the value it is handed, see `check-review-gate.ts`, not this row's pointer. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
140
|
+
| Review gate | Ever had a PR merge with nobody actually approving it? | ci | Holds the merge until the required review verdicts actually exist. | Required, blocking: a task-branch PR must carry a clean code-reviewer `APPROVE` verdict comment AND a clean security-review `PASS` verdict comment before merge — reusing `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` (`packages/aeg-core/src/verdict-extraction.ts`), extracted out of the post-merge Archivist's `archive-task.ts` so both the pre-merge gate and the post-merge provenance assembly read the identical detection logic (one implementation, two call sites). Picks the MOST RECENT comment carrying a clear value, not the first comment merely matching the loose marker pattern — fixed live during this task, since the original single-verdict `.find()` broke on real multi-comment PRs (a `REQUEST_CHANGES` verdict followed by fixes and a later clean `APPROVE`, or the post-merge provenance comment's own DANGLING-note prose, which contains the word "verdict" and would otherwise poison a naive first-match search). **A verdict is bound to the commit it judged** (reviewed-commit binding): the winning comment must also carry a same-comment `Judged head: <sha>` line (parsed by `verdict-extraction.ts`, same anchor discipline as the `VERDICT:` marker itself, abbreviated or full sha both accepted), and `checkReviewGate` requires it to cover the PR's *current* `headRefOid` — a verdict that predates a later push, or carries no binding at all, no longer counts as clean; the gate names both the verdict's sha and the current head in its failure message. **Verdict authors are verified** (2026-08-09): only comments whose author is on the principal allowlist (`PRINCIPAL_ALLOWLIST` — the same trust anchor the waiver actor-check uses) participate in verdict extraction; forged, bot, and unresolvable-author comments are ignored, never fatal — a later forged `APPROVE` can no longer override a real `REQUEST_CHANGES`. **What this precondition does and does not establish:** it defeats a *forged* or *bot*-authored comment; it does not defeat a comment genuinely posted by a dispatched reviewer agent sharing the Principal's own local `gh` credential — that comment IS on the allowlist, correctly, because it really is the Principal's forge identity, so author verification alone cannot tell the Principal's own verdict apart from an agent's verdict cast under that same shared credential (`vinaya review post`'s `Cast by:` line records which role/session actually cast it, making the inheritance auditable without closing it — the close requires running the reviewer somewhere the Principal's credential is not, tracked separately). A principal-actor-verified `vinaya/waiver:review` label (the same `isWaiverLabelActorVerified` pattern, now parameterized by label name and reused — not duplicated — for this second label) skips the requirement for one PR; label presence alone is never sufficient. **Tool-layer protection extended** (`check-forge-gates.sh`): mutating `vinaya/waiver:review` from an agent session is denied, exactly like `vinaya/waiver:docs` — without this, an agent sharing the Principal's own `gh` credential could self-apply the waiver and inherit the Principal's identity on the labeling event, defeating actor verification entirely (confirmed live during this task before the hook was extended). Non-task branches (plan PRs) bypass — a plan PR touching only topology files has no code to review. Going-forward only: never re-evaluates already-merged history. **Mechanical checks are a third precondition:** a task-branch PR must additionally carry at least one reported, green (non-review) check-run for the PR's current head — `checkReviewGate` reads a caller-supplied `mechanicalChecks` list (populated via `gh pr checks --json name,bucket`, with this repo's own `vinaya review gate` check-run name excluded so the gate cannot judge its own status) and fails the same way an unclean or unbound verdict does, naming which check is not green, or that none have reported yet. The exclusion of the gate's own check-run name is repo-specific and lives in the thin CLI shims that resolve check-run status via `gh`, never inside the pure evaluator itself, since that evaluator ships to every adopter under a different workflow name. **A verdict is also bound by PATCH IDENTITY** (this task): the gate compares each side's `git diff <base>...<sha>` output run through `git patch-id --stable`, for the judged head and the current head, and a verdict whose sha binding has lapsed still counts when the two identities are equal and neither is `null`. A merge from the main branch, or a rebase that leaves the PR's own patch untouched, therefore no longer voids a review that already read exactly those changes — the round it used to cost proved nothing. Fails closed on every uncertainty: `null` on either side (an unreachable judged head after a force-push, a shallow clone, no git) is "cannot answer", never "they match". The known limit is stated rather than papered over — a base that moved under an identical patch can carry a semantic conflict the earlier review could not have seen, and this binding still holds; that is the same limit GitHub's own stale-review rule has, and the green-mechanical-checks requirement this gate already imposes at the new head is the guard for it. **A verdict is also bound to the OBJECTIVES LIST it was judged against**: the winning comment's `Objectives version:` line (a hash `objectivesVersion` computes over the closed Issue's, or the PR body's own, `## Objectives` list) must equal the current version of that same list — a verdict cast before an edit to the objectives no longer counts as clean, even when its head binding still holds. `input.objectivesVersion === null` (an Issue below the objectives cutover, or no resolvable list at all) skips this half of the binding entirely, so the pre-cutover PR stock keeps passing unchanged. **A verdict is also bound to the newest PRINCIPAL RULING on the PR** (`review-validity-v1` task 3): the winning comment's `Ruling ordinal: <k>` line (`0` when the PR carried no ruling at cast time — this line renders UNCONDITIONALLY, unlike the objectives-version line) must equal `input.rulingOrdinal`, the newest ruling ordinal `check-review-gate.ts` counts off the PR's own principal-authored `<!-- aeg:principal:ruling:<pr>-<k> -->` comments — a ruling posted after a verdict was cast no longer counts as clean, even when its head and objectives bindings still hold, and the gate names the newer ruling in its failure message. Unlike `objectivesVersion`, there is no `null`-means-skip input value here — a PR's ruling count is never ambiguous — but a verdict predating this feature (no `Ruling ordinal:` line at all) still binds when the PR's newest ruling ordinal is `0`, the same "nothing to bind against" case expressed as an equality rather than a skip. **Resolving that `objectivesVersion` value diverges between the two bins that call `checkReviewGate`**: this repo's live pre-merge path, `apps/cli/src/checks/bin/check-review-gate.ts`, resolves it for real (Issue-then-body, fail-closed on every unresolvable case, waiver-checked first so an actor-verified `vinaya/waiver:review` label is never blocked by that resolution); the older reference script named in this row's own implementation column, `packages/aeg-core/bin/verify-review-gate.ts`, has no equivalent Issue-fetch machinery and always passes `objectivesVersion: null`, unconditionally skipping this half of the binding. Read the objectives-version paragraph above as describing `checkReviewGate`'s pure evaluation logic (shared by both callers); for which file actually resolves the value it is handed, see `check-review-gate.ts`, not this row's pointer. | product | `apps/cli/src/checks/bin/check-review-gate.ts` |
|
|
141
141
|
| G1 — implementation exists | Ever read about a safeguard that turned out not to actually exist? | ci | Re-checks that every gate the doctrine describes has real code behind it. | Every ring's non-empty `implementation` cell on this very page resolves to a real path on disk — makes this page's registry columns load-bearing instead of decorative. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
142
142
|
| G2 — no orphan hook/CLI | Ever found a script nobody remembers the purpose of? | ci | Re-checks that every hook and CLI in the repo is one the doctrine claims, and that a row scaffolded to fix that stays visibly incomplete until a human finishes it. | Every file under `.husky/*`, `.claude/hooks/*.sh`, `packages/aeg-core/bin/*.ts`, `apps/cli/src/checks/bin/*.ts` is named by some row's `implementation`, OR — **twin-form recognition (O14)** — resolves, through the same `claimedCheckNames` derivation G6 uses, to a `coreCheckRegistry()` name some OTHER row's `implementation` already claims: the same enforcement mechanism routinely ships as both a `packages/aeg-core/bin/*.ts` standalone form and an `apps/cli/src/checks/bin/check-*.ts` CLI-registered form, and a row's single `implementation` cell can only name one — the other is not a second undocumented fact, it is the first fact read through its other physical shape. A `NON_GATE_BINS`-listed aeg-core bin (a forge writer, a one-shot reporter, an eval harness — `report-tokens.ts`, `eval-agent-compliance.ts`, …) and a listed non-gate hook script (`.claude/hooks/track-transcript.sh`, real Stop-hook plumbing feeding two already-documented mechanisms' own inputs, with no pass/fail decision of its own) implement no enforcement mechanism to be named for — the honest fix for those is that this gate stops asking, never an invented row (`.husky/*` matches nothing in this repo today — that directory does not exist here). **Blocking as of this task** (re-graded from report-only: the twin-form gap above was G2's entire non-scaffold-placeholder backlog, and a 0-finding G2 run means something again now that it can fail). **Scaffold-writer** (task 22, later widened to cover that second location): `verify-registry.ts --scaffold` auto-inserts a stub row for an orphan candidate whose ring is mechanically derivable — a `packages/aeg-core/bin/*.ts` file resolving through `GATE_AUDIENCE` to a registered check (its ring comes from that check's own `apps/cli/src/checks/registry.ts` entry), an `apps/cli/src/checks/bin/*.ts` file whose `check-`-stripped name resolves through the analogous `CLI_CHECK_RING` mirror table, or a `.husky`/`.claude/hooks` file (ring 0 by source). An orphan with no derivable ring (an unregistered bin under either directory) gets no stub and stays a plain finding — the writer never guesses. A second, independent half of G2 scans every row's `summary`/`description`/`spec` cells for the literal placeholder marker `[undocumented — fill in why]` the scaffold writes into every non-mechanical cell: a stub row that filled only `implementation` would otherwise read as "documented" to the orphan half above and silence G2 outright, which is the exact trap this second half exists to close. The writer only ever appends a new row after a ring table's last existing row — it never edits, reorders, or removes one, so a hand-authored row (including `workspace-escape`'s and `main-branch-refusal`'s own) is untouched. Repo-side only: the writer lives on `verify-registry.ts` (aeg-core), never on the shipped `check-registry-gates.ts` (apps/cli) — an adopter's checks stay read-only over doctrine. | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
143
143
|
| G3 — no seventh way into GitHub | Ever discovered a backdoor that skipped all your checks? | ci | Re-checks that no route into GitHub exists beyond the ones the doctrine gates. | Every file making a GitHub-mutating call of the gated class (PR/Issue create, PR/Issue body/title edit, `gh api` create/edit, raw curl/wget writes) is named by some Ring-0 row's `implementation` — a mutating call outside that set would be an unguarded hole in the prevention model. (The class was originally defined by attalabs's `check-forge-gates.sh` tool-interception hook; this repo has no such hook — see the raw-API ring-1 row above — but the scan's definition of "mutating call" is unchanged.) | product | `packages/aeg-core/bin/verify-registry.ts` |
|
|
@@ -213,7 +213,7 @@ A full audit of this page against the installed hooks and CI workflows (2026-07-
|
|
|
213
213
|
| `verify-task` (pre-PR exit composite) | ✅ **new** — `open-pr.ts` runs it wholesale for task branches (`gatePlanForBranch`); on `edit <n>` the gate set now resolves from the target PR's **actual forge head branch** (`gh pr view --json headRefName`, fetch failure = hard refusal), not the local checkout/`BRANCH` env, which silently skipped `closes-n`/`verify-task` on task-PR body edits from a `main` checkout (task 36 bundled finding) | — | — | **Forced, ring-0 only (this task, item 2)**; **edit-path gate-selection bypass fixed** (task 36) | was (b) — closed; ring-1/2 backstop not yet built (same accepted shape as the branch-ID check below) |
|
|
214
214
|
| Runtime Test Plan checkbox state (`verify-test-plan`) | n/a (CI-only check) | ✅ now a step of the AEG gate suite job (`verify-test-plan.yml` deleted — task 31; still runs on PR-body `edited` events) — **reads `BRANCH`; parses both the inline `**Test Plan:**` marker and the `## N. Test Plan` heading form** (task 25, item 3) | — | Paired (pre-existing); **parser bug fixed** | n/a — not a coverage gap, a correctness fix (a live-fire gap: heading-form sections were never matched, so a task PR with an unticked `[principal]` box advisory-PASSED) |
|
|
215
215
|
| Waiver-label-actor verification (task 29; **extended to `vinaya/waiver:review`, review-gate task 1**) | ✅ **new** — pre-push is warn-only (no `vinaya/waiver:docs`/`vinaya/waiver:review` label can exist before a PR does) **+ tool-layer (attalabs only)**: the attalabs reference implementation adds a session hook denying any agent-session command that mutates either label; this repo has no tool-interception layer (see the raw-API ring-0 row), so the shared-credential hole is OPEN here — a local agent using the Principal's own `gh` credential can apply a waiver label whose timeline actor then reads as the Principal, and the actor-verification step cannot distinguish it. Until an interception layer exists, waiver labels in this repo rest on credential discipline, not a gate | ✅ **new** — a GraphQL `LABELED_EVENT`/REST timeline step resolves the label's actor, verified by `isWaiverLabelActorVerified` (now parameterized by label name) and fed into `verify-docs --pr`'s C5 waiver check (`vinaya/waiver:docs`) and `verify-review-gate.ts`'s review check (`vinaya/waiver:review`) | — | **New (this task)**; **extended (review-gate task 1)** | n/a — new mechanism |
|
|
216
|
-
| Review gate — code-reviewer + security-review verdicts (review-gate task 1) | n/a (CI-only check; the underlying verdict comments come from a human, a dispatched reviewer subagent, or the AI-review job) | ✅ **new** — `verify-review-gate.ts`, a step of the AEG gate suite job. Blocks merge unless a clean code-reviewer `APPROVE` AND a clean security-review `PASS` verdict comment exist, both bound to the PR's current head commit (`headRefOid` — a stale or unbound verdict fails the same as a missing one), or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)**; **reviewed-commit binding added (review-gate-verdict-sha-binding)**; **objectives-version binding added by a later task — implemented in `apps/cli/src/checks/bin/check-review-gate.ts`, this repo's actual live gate path, not in this row's own `verify-review-gate.ts`, which never resolves it (see the Ring 1 Review gate row above)** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only; the reviewed-commit binding closed a second gap where a clean verdict could still merge stale code because nothing checked which commit it covered) |
|
|
216
|
+
| Review gate — code-reviewer + security-review verdicts (review-gate task 1) | n/a (CI-only check; the underlying verdict comments come from a human, a dispatched reviewer subagent, or the AI-review job) | ✅ **new** — `verify-review-gate.ts`, a step of the AEG gate suite job. Blocks merge unless a clean code-reviewer `APPROVE` AND a clean security-review `PASS` verdict comment exist, both bound to the PR's current head commit (`headRefOid` — a stale or unbound verdict fails the same as a missing one), or the PR carries an actor-verified `vinaya/waiver:review` label | — | **New (this task)**; **reviewed-commit binding added (review-gate-verdict-sha-binding)**; **objectives-version binding added by a later task — implemented in `apps/cli/src/checks/bin/check-review-gate.ts`, this repo's actual live gate path, not in this row's own `verify-review-gate.ts`, which never resolves it (see the Ring 1 Review gate row above)**; **ruling-freshness binding added by `review-validity-v1` task 3 — no divergence here: both `check-review-gate.ts` and `verify-review-gate.ts` count real ruling comments off `pr.comments`, already fetched by each before `checkReviewGate` is called** | n/a — new mechanism (closes the gap where task PRs could previously merge with no review pass at all — the post-merge Archivist's `extractVerdict`/DANGLING flag was advisory-only; the reviewed-commit binding closed a second gap where a clean verdict could still merge stale code because nothing checked which commit it covered) |
|
|
217
217
|
|
|
218
218
|
This table describes **installed mechanisms only** (the page's own change-discipline rule, below). The `verify-dispatch`/`verify-task` rows and the `verify-test-plan` fix landed in task 25's own change set, per that rule.
|
|
219
219
|
|
|
@@ -58,6 +58,8 @@ The same three states a tranche derives (`tranche-model.md` §11, one altitude d
|
|
|
58
58
|
|
|
59
59
|
Nobody sets this by hand and nothing writes it back to the Milestone object — it's asked of the forge the same way every other altitude is.
|
|
60
60
|
|
|
61
|
+
`vinaya milestone status <n>` is that ask, made concrete: for every `- <slug>: …` line in a Milestone's `### Tranche intents` section, it prints the slug, its derived lifecycle, and its labeled Issues' counts (`merged`/`open`/`not planned`) — read-only, nothing written. A slug with an intent line and zero labeled Issues yet prints `planned` with `0 issues`, per §1: a real, not-yet-started tranche, never an error.
|
|
62
|
+
|
|
61
63
|
### The trap this level actually has: a closed legacy Milestone with real work still under it
|
|
62
64
|
|
|
63
65
|
`vinaya milestone adopt` closes the old one-tranche Milestone it retires — closed, never deleted, so the provenance survives. The closed Milestone still legacy-title-matches its slug forever (§1's exception). Found live, `0.19.1`: a reader that trusts a closed legacy Milestone's own `state` unconditionally reports the tranche `complete` even when its real, still-open Issues have already moved to a new Milestone via `adopt` — the tranche's true lifecycle lives in the label population, not in the Milestone object `adopt` walked away from. The fix: when a legacy-matched Milestone is closed AND its slug's labeled Issues are non-empty, derive from those Issues, not from the closed Milestone's `state`. An empty label population under a closed legacy Milestone still means what it always meant — a genuinely historical tranche, or one nobody has adopted away from yet.
|
package/aeg-root/process.md
CHANGED
|
@@ -89,7 +89,7 @@ Before dispatching a task, the Planner checks four gates (`roles/planner.md` §
|
|
|
89
89
|
|
|
90
90
|
`vinaya check dispatch-readiness`, run against the task, re-derives gates 1–3 mechanically in one command.
|
|
91
91
|
|
|
92
|
-
**Running the dispatch:** `vinaya task dispatch <tranche> <n> [--agent claude | codex | gemini]` performs the whole phase once the gates pass: it renders the brief (the same render `brief render` uses, from `aeg-root/templates/brief-template.md` and every derivable fact), posts it once, frozen, as the task Issue's own `aeg:brief:v1` comment, then — with `--agent` — starts the Developer through `dispatchRole`. Without `--agent` it prints the rendered brief and the manual dispatch instruction and exits `0`, so a human can paste it instead. The brief MUST include (all mechanically derived or refused on):
|
|
92
|
+
**Running the dispatch:** `vinaya task dispatch <tranche> <n> [--agent claude | codex | gemini]` performs the whole phase once the gates pass: it renders the brief (the same render `brief render` uses, from `aeg-root/templates/brief-template.md` and every derivable fact), posts it once, frozen, as the task Issue's own `aeg:brief:v1` comment, then — with `--agent` — starts the Developer through `dispatchRole`. Without `--agent` it prints the rendered brief and the manual dispatch instruction and exits `0`, so a human can paste it instead. `vinaya task dispatch` is deprecated: `vinaya task run <tranche> <n> --agent <claude | codex | gemini>` is the one command that now runs this whole phase AND the phases after it — preparation through this same render/freeze/post, then the developer/review loop — unattended, from one planned Issue to a reviewed pull request, exactly one developer started; a paused run resumes with the loop's own `vinaya dev-review-loop --resume <pr>`. The brief itself MUST include (all mechanically derived or refused on):
|
|
93
93
|
- Impact tier (0 / 1 / 3), rendered from the Issue's tier declaration
|
|
94
94
|
- Type 1 / Type 2 declaration if architectural decisions are expected, gated at dispatch (a PENDING Type 1 decision blocks dispatch)
|
|
95
95
|
- `principal_delegate:` if the work runs while the Principal is offline
|
|
@@ -337,6 +337,8 @@ These four are the same shape as `roles/developer.md`'s entry gate items 2, 3, a
|
|
|
337
337
|
|
|
338
338
|
`vinaya task dispatch <tranche> <n> [--agent claude | codex | gemini]` is the one command that performs the dispatch act once the gates above pass: it renders the brief, posts it as the frozen `aeg:brief:v1` Issue comment, and — with `--agent` — starts the Developer through `dispatchRole`. Without `--agent` it prints the rendered brief and the manual dispatch instruction and exits `0`, so a human can paste it instead. Either way, posting the frozen comment **is** the `todo → in-flight` transition once the Developer's own worktree opens — nobody writes a status field.
|
|
339
339
|
|
|
340
|
+
`vinaya task dispatch` is deprecated in favor of two narrower commands: `vinaya task brief <tranche> <n>` performs preparation only — the render, the refusals, and the frozen post — and starts no agent under any circumstances; `vinaya task run <tranche> <n> --agent <claude|codex|gemini>` composes that same preparation with starting the Developer and running the full review loop unattended — this is the operator's normal way to run the dispatch act end to end, one command from a planned Issue to a reviewed pull request. Use `task brief` when the act is freeze-only — confirming a brief renders cleanly, or handing it to a human to paste — rather than reaching for `task dispatch` with no `--agent`.
|
|
341
|
+
|
|
340
342
|
**Announce the act on entry, the same way the plan act does (conversational protocol, above).** *"I'm dispatching task <n>. Checking the four gates first, then running `vinaya task dispatch`."* Confirm the render's output looks right before handing it (or its `--agent` session) over — a rendered brief that inherited a stale or malformed rationale field is still your responsibility to catch, the same way a code review catches a bug the linter didn't.
|
|
341
343
|
|
|
342
344
|
**`[NEEDS CLARIFICATION]` markers block dispatch.** If the rendered brief carries any `[NEEDS CLARIFICATION: …]` marker (surfaced from an ambiguous or incomplete rationale field), do not dispatch it — resolve the ambiguity by editing the Issue's rationale (or, if the ambiguity is a real open decision, put it to the Principal) and re-render. A dispatched brief has no open markers; the Developer's own standing autonomy clause governs ambiguity met *during* execution, not ambiguity left unresolved at dispatch.
|
|
@@ -54,8 +54,8 @@ The Principal holds final authority over:
|
|
|
54
54
|
- **Right to reject.** The Principal can reject a direction at any phase — idea, brief, PR, or post-merge — and the system accepts that without pushback. Agents can surface concerns but not override.
|
|
55
55
|
- **Ratification.** Type 1 decisions are not ratified until the Principal explicitly says so. PENDING decisions wait; they do not auto-promote.
|
|
56
56
|
- **Editing an already-declared Milestone.** `vinaya milestone edit <n> --body-file <path>` corrects a Milestone's goal or `Release:` field after the Architect's one-time write — the same product call declaring it was (`milestone-model.md` §5), never the Architect's or Planner's (`roles/architect.md` "What you do NOT do").
|
|
57
|
-
- **Changing a task's scope mid-flight.** `vinaya issue objectives edit <n> --add "<sentence>" | --drop O<k> | --replace O<k> "<sentence>" --reason "<text>"` rewrites the task Issue's `## Objectives` section through the validated forge-write path and posts one versioned, findable comment recording the previous list, the new list, and the reason. This voids the standing review verdict on that task's open PR — `vinaya review status` names it `objectives moved — re-review required` (once the sibling task wiring that reason lands).
|
|
58
|
-
- **Ruling on a contested finding.** `vinaya pr rule <pr> --file <ruling.md>` posts the Principal's decision as its own marked, versioned PR comment — never mistaken for a code-review or security verdict, since it refuses a file carrying verdict grammar.
|
|
57
|
+
- **Changing a task's scope mid-flight.** `vinaya issue objectives edit <n> --add "<sentence>" | --drop O<k> | --replace O<k> "<sentence>" --reason "<text>"` rewrites the task Issue's `## Objectives` section through the validated forge-write path and posts one versioned, findable comment recording the previous list, the new list, and the reason. This voids the standing review verdict on that task's open PR — `vinaya review status` names it `objectives moved — re-review required` (once the sibling task wiring that reason lands). If a `dev-review-loop` is running that task, it reads objectives from this same versioned edit comment (never the live Issue body), so its next round picks up the change automatically; if the edit lands between a round's reviewer dispatch and its verdicts coming back, the loop discards that round's verdicts unheld and unpublished and pauses with reason `objectives_changed`, naming the superseded version, the new version, and this exact edit invocation — resume it the same way as any other pause, with `vinaya dev-review-loop --resume <pr>`.
|
|
58
|
+
- **Ruling on a contested finding.** `vinaya pr rule <pr> --file <ruling.md>` posts the Principal's decision as its own marked, versioned PR comment — never mistaken for a code-review or security verdict, since it refuses a file carrying verdict grammar. A ruling posted after a clean verdict already exists voids it (`review-validity-v1` task 3, O2): every rendered verdict carries the newest ruling ordinal it was judged against on a `Ruling ordinal:` line, and the merge gate compares that ordinal to the PR's actual newest ruling — a mismatch reads as unbound, the same fail-closed shape `objectives moved — re-review required` already uses, until reviewers re-cast against the new ruling. If a `dev-review-loop` is running that task and a ruling lands between a round's reviewer dispatch and its verdicts coming back, the loop discards that round's verdicts unheld and unpublished and pauses with reason `ruling_posted`, naming the superseded ordinal, the new ordinal, and the ruling's own marker identifier — resume it the same way as any other pause, with `vinaya dev-review-loop --resume <pr>`.
|
|
59
59
|
|
|
60
60
|
---
|
|
61
61
|
|
|
@@ -99,6 +99,8 @@ Until a coordinator program exists, the Principal is the loop's coordinator, per
|
|
|
99
99
|
|
|
100
100
|
A coordinator program replaces this by-hand duty when one exists.
|
|
101
101
|
|
|
102
|
+
**Recovering the automated loop.** `vinaya dev-review-loop --task <n>` is that coordinator. If it crashes, or a poll (waiting for the PR to appear, or for the branch head to change after a gate-red dispatch) times out, the recovery is simply re-running the same command against the same task. Round `1`'s own entry checks the forge first: an already-open pull request on the developer's branch means it attaches — no developer is started — and a remote branch with no open pull request yet resumes the recorded developer session once, instructed to open it. Either way, a re-run never starts a second developer.
|
|
103
|
+
|
|
102
104
|
---
|
|
103
105
|
|
|
104
106
|
## Communication style with the Principal
|
|
@@ -57,6 +57,8 @@ A review turn started via `vinaya dispatch code-reviewer --agent <vendor>` carri
|
|
|
57
57
|
- The task Issue carries the brief, frozen on its `aeg:brief:v1` comment, posted by the Planner's dispatch act before the Developer started.
|
|
58
58
|
- Your job is to judge whether the PR does what the brief said, safely and honestly — not to improve it yourself.
|
|
59
59
|
|
|
60
|
+
**Dispatched by `vinaya dev-review-loop` (unattended)?** You do not run `vinaya review post` yourself — write `findings.txt` and `report.txt` to the work directory the dispatch names, plus `objectives.txt` (one `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` line per objective) whenever the task carries objectives; leave `findings.txt` empty if there are none. A work directory still missing a required file after the dispatch is an infrastructure failure, not a clean approval.
|
|
61
|
+
|
|
60
62
|
## Entry gate (self-locating) — refuse if it isn't your turn
|
|
61
63
|
|
|
62
64
|
- **No open PR** for the task → *"Nothing to review — there's no open PR. Come back when one is open."*
|
|
@@ -97,7 +99,7 @@ This is why the review is a separate pass and not something the Developer does t
|
|
|
97
99
|
|
|
98
100
|
## Output format
|
|
99
101
|
|
|
100
|
-
**Run `vinaya review post --role code-reviewer` with this data; do not hand-type a verdict comment.** The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate. So is the `Judged head:` line immediately below it: the gate binds your verdict to the exact commit you reviewed, and a verdict that does not cover the PR's current head does not count as clean, however clean its `VERDICT:` value is (`review-gate.ts`). A third head line, `Objectives version:`, binds your verdict the same way to the objectives list you judged it against — a hash the command computes from the Issue's (or the PR body's) `## Objectives` list; if the Issue's objectives change after you cast a verdict, the gate treats it exactly like a stale head. A verdict also holds for a later head whose patch identity equals the judged head's: the gate compares `git diff <base>...<sha> | git patch-id --stable` on both sides, so a merge from the main branch or a rebase that leaves the PR's own patch untouched keeps your verdict alive rather than costing a round to re-cast it over changes you already read. That comparison ignores whitespace, so a whitespace-only push also keeps your verdict; any change to non-whitespace content does not, and comes back to you. Free-typing this shape into `gh pr comment` is no longer the sanctioned path — a decorated heading or a bolded/blockquoted line the gate's line-anchored parser cannot see reaches the forge looking correct to a human reader and is invisible to `verify-review-gate.ts`, with no pointer back to what was wrong until CI goes red. `vinaya review post` resolves the PR's real head itself (`gh pr view --json headRefOid` — never a self-reported sha), renders every structural line from your validated inputs, posts the comment, and refuses to exit 0 unless its own post re-parses clean through the exact same `extractCodeReviewVerdict` function the gate calls:
|
|
102
|
+
**Run `vinaya review post --role code-reviewer` with this data; do not hand-type a verdict comment.** The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate. So is the `Judged head:` line immediately below it: the gate binds your verdict to the exact commit you reviewed, and a verdict that does not cover the PR's current head does not count as clean, however clean its `VERDICT:` value is (`review-gate.ts`). A third head line, `Objectives version:`, binds your verdict the same way to the objectives list you judged it against — a hash the command computes from the Issue's (or the PR body's) `## Objectives` list; if the Issue's objectives change after you cast a verdict, the gate treats it exactly like a stale head. A fourth line, `Ruling ordinal:`, renders unconditionally — `0` when the PR carried no principal ruling at cast time — and binds the same way to the newest principal ruling on the PR (`review-validity-v1` task 3): a ruling posted after you cast your verdict turns the gate red exactly like a stale head, until you re-cast against it. A verdict also holds for a later head whose patch identity equals the judged head's: the gate compares `git diff <base>...<sha> | git patch-id --stable` on both sides, so a merge from the main branch or a rebase that leaves the PR's own patch untouched keeps your verdict alive rather than costing a round to re-cast it over changes you already read. That comparison ignores whitespace, so a whitespace-only push also keeps your verdict; any change to non-whitespace content does not, and comes back to you. Free-typing this shape into `gh pr comment` is no longer the sanctioned path — a decorated heading or a bolded/blockquoted line the gate's line-anchored parser cannot see reaches the forge looking correct to a human reader and is invisible to `verify-review-gate.ts`, with no pointer back to what was wrong until CI goes red. `vinaya review post` resolves the PR's real head itself (`gh pr view --json headRefOid` — never a self-reported sha), renders every structural line from your validated inputs, posts the comment, and refuses to exit 0 unless its own post re-parses clean through the exact same `extractCodeReviewVerdict` function the gate calls:
|
|
101
103
|
|
|
102
104
|
```
|
|
103
105
|
vinaya review post --role code-reviewer --pr <n> --verdict APPROVE|REQUEST_CHANGES \
|
|
@@ -118,6 +120,8 @@ Judged head: <sha>
|
|
|
118
120
|
|
|
119
121
|
Objectives version: <hash>
|
|
120
122
|
|
|
123
|
+
Ruling ordinal: <k>
|
|
124
|
+
|
|
121
125
|
BRIEF CONFORMANCE: [does it do what the brief asked? 1-2 sentences]
|
|
122
126
|
SPEC CONFORMANCE: [does it agree with the Product spec? "n/a — no Product named" | "clean" | drift listed in findings]
|
|
123
127
|
|
|
@@ -137,7 +141,7 @@ DOCS: [tier-appropriate | missing items listed in findings]
|
|
|
137
141
|
<!-- AEG:CLAIM: packages/aeg-core/src/verdict-extraction.ts contains:function firstFiveLines(comment: string): string { -->
|
|
138
142
|
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function renderEscalationComment(input: EscalationInput): string { -->
|
|
139
143
|
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function checkRenderedComment(body: string, expectation: RenderExpectation): RenderCheckResult { -->
|
|
140
|
-
`vinaya review post` also refuses before posting anything if you pass a BLOCKER finding together with `--verdict APPROVE`, or any `NOT MET` objective together with `--verdict APPROVE` — both contradictions are caught mechanically, not left to review. Before its own post reaches the forge, it refuses to post anything the gate would misread: it runs the exact same `VERDICT:`/`Judged head:`/`Objectives version:` extraction the merge gate uses over the rendered comment, and requires exactly the intended verdict to come back. Free text in a finding, a conformance field, an objective's evidence, or `--scope-evidence-file` can say `VERDICT` or span multiple lines without risk — the extraction reads only a comment's first five lines, and in a code-reviewer or security comment a caller field never opens one of those lines (it only trails a fixed, renderer-owned label); an escalation's `--summary`
|
|
144
|
+
`vinaya review post` also refuses before posting anything if you pass a BLOCKER finding together with `--verdict APPROVE`, or any `NOT MET` objective together with `--verdict APPROVE` — both contradictions are caught mechanically, not left to review. Before its own post reaches the forge, it refuses to post anything the gate would misread: it runs the exact same `VERDICT:`/`Judged head:`/`Objectives version:`/`Ruling ordinal:` extraction the merge gate uses over the rendered comment, and requires exactly the intended verdict to come back. Free text in a finding, a conformance field, an objective's evidence, or `--scope-evidence-file` can say `VERDICT` or span multiple lines without risk — the extraction reads only a comment's first five lines (the ruling ordinal's own first-seven-line window is wider still), and in a code-reviewer or security comment a caller field never opens one of those lines (it only trails a fixed, renderer-owned label); an escalation's `--summary` occupies line 7 at minimum (`Ruling ordinal:` renders unconditionally ahead of it), which is exactly why this pre-post re-parse exists rather than construction alone.
|
|
141
145
|
|
|
142
146
|
- **BLOCKER** — blocks merge. Wrong behavior; a dishonest test; a document the brief's documentation-update list names that is absent from the diff or states the changed behavior backwards; a scope violation; a **spec contradiction**.
|
|
143
147
|
- **MAJOR** — surfaced, never blocks. A likely bug, weak error handling, spec drift short of contradiction, a wrong sentence in a document the brief did not name. Listed in the verdict, shown to the Principal at the go (the Principal's decision, before merge, on whether any surfaced finding blocks this change), published in the record — the Principal decides whether a surfaced finding blocks this change.
|
|
@@ -56,6 +56,8 @@ A pass started via `vinaya dispatch security --agent <vendor>` carries its role
|
|
|
56
56
|
- The task Issue carries the brief, frozen on its `aeg:brief:v1` comment.
|
|
57
57
|
- Your single question: **could this change leak a secret, widen an attack surface, or misconfigure auth/permissions/agent tooling?**
|
|
58
58
|
|
|
59
|
+
**Dispatched by `vinaya dev-review-loop` (unattended)?** You do not run `vinaya review post` yourself — write `findings.txt` and `report.txt` to the work directory the dispatch names, plus `objectives.txt` (one `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` line per objective) whenever the task carries objectives; leave `findings.txt` empty if there are none. A work directory still missing a required file after the dispatch is an infrastructure failure, not a clean pass.
|
|
60
|
+
|
|
59
61
|
## Entry gate (self-locating) — refuse if it isn't your turn
|
|
60
62
|
|
|
61
63
|
- **No open PR** → *"Nothing to security-review — no open PR."*
|
|
@@ -91,7 +93,7 @@ When the PR touches agent/skill/hook definitions, MCP configs, or anything under
|
|
|
91
93
|
|
|
92
94
|
## Output format
|
|
93
95
|
|
|
94
|
-
**Run `vinaya review post --role security` with this data; do not hand-type a verdict comment.** The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate. So is the `Judged head:` line immediately below it: the gate binds your verdict to the exact commit you reviewed, and a verdict that does not cover the PR's current head does not count as clean, however clean its `VERDICT:` value is (`review-gate.ts`). A third head line, `Objectives version:`, binds your verdict the same way to the objectives list you judged it against — a hash the command computes from the Issue's (or the PR body's) `## Objectives` list; if the Issue's objectives change after you cast a verdict, the gate treats it exactly like a stale head. A verdict also holds for a later head whose patch identity equals the judged head's: the gate compares `git diff <base>...<sha> | git patch-id --stable` on both sides, so a merge from the main branch or a rebase that leaves the PR's own patch untouched keeps your verdict alive rather than costing a round to re-cast it over changes you already read. That comparison ignores whitespace, so a whitespace-only push also keeps your verdict; any change to non-whitespace content does not, and comes back to you. Free-typing this shape into `gh pr comment` is no longer the sanctioned path — a decorated heading or a bolded/blockquoted line the gate's line-anchored parser cannot see reaches the forge looking correct to a human reader and is invisible to `verify-review-gate.ts`, with no pointer back to what was wrong until CI goes red. `vinaya review post` resolves the PR's real head itself (`gh pr view --json headRefOid` — never a self-reported sha), renders every structural line from your validated inputs, posts the comment, and refuses to exit 0 unless its own post re-parses clean through the exact same `extractSecurityReviewVerdict` function the gate calls:
|
|
96
|
+
**Run `vinaya review post --role security` with this data; do not hand-type a verdict comment.** The `VERDICT:` line is bare — no bold, no heading, no blockquote — it is machine-read by the pre-merge review gate. So is the `Judged head:` line immediately below it: the gate binds your verdict to the exact commit you reviewed, and a verdict that does not cover the PR's current head does not count as clean, however clean its `VERDICT:` value is (`review-gate.ts`). A third head line, `Objectives version:`, binds your verdict the same way to the objectives list you judged it against — a hash the command computes from the Issue's (or the PR body's) `## Objectives` list; if the Issue's objectives change after you cast a verdict, the gate treats it exactly like a stale head. A fourth line, `Ruling ordinal:`, renders unconditionally — `0` when the PR carried no principal ruling at cast time — and binds the same way to the newest principal ruling on the PR (`review-validity-v1` task 3): a ruling posted after you cast your verdict turns the gate red exactly like a stale head, until you re-cast against it. A verdict also holds for a later head whose patch identity equals the judged head's: the gate compares `git diff <base>...<sha> | git patch-id --stable` on both sides, so a merge from the main branch or a rebase that leaves the PR's own patch untouched keeps your verdict alive rather than costing a round to re-cast it over changes you already read. That comparison ignores whitespace, so a whitespace-only push also keeps your verdict; any change to non-whitespace content does not, and comes back to you. Free-typing this shape into `gh pr comment` is no longer the sanctioned path — a decorated heading or a bolded/blockquoted line the gate's line-anchored parser cannot see reaches the forge looking correct to a human reader and is invisible to `verify-review-gate.ts`, with no pointer back to what was wrong until CI goes red. `vinaya review post` resolves the PR's real head itself (`gh pr view --json headRefOid` — never a self-reported sha), renders every structural line from your validated inputs, posts the comment, and refuses to exit 0 unless its own post re-parses clean through the exact same `extractSecurityReviewVerdict` function the gate calls:
|
|
95
97
|
|
|
96
98
|
```
|
|
97
99
|
vinaya review post --role security --pr <n> --verdict PASS|FAIL \
|
|
@@ -111,6 +113,8 @@ Judged head: <sha>
|
|
|
111
113
|
|
|
112
114
|
Objectives version: <hash>
|
|
113
115
|
|
|
116
|
+
Ruling ordinal: <k>
|
|
117
|
+
|
|
114
118
|
FINDINGS (ordered by severity):
|
|
115
119
|
1. [CRITICAL|HIGH|MEDIUM|LOW] <file:line> — <what and why>
|
|
116
120
|
2. ...
|
|
@@ -126,7 +130,7 @@ SECRETS: [none found | listed above, redacted]
|
|
|
126
130
|
<!-- AEG:CLAIM: packages/aeg-core/src/verdict-extraction.ts contains:function firstFiveLines(comment: string): string { -->
|
|
127
131
|
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function renderEscalationComment(input: EscalationInput): string { -->
|
|
128
132
|
<!-- AEG:CLAIM: apps/cli/src/commands/review-post.ts contains:export function checkRenderedComment(body: string, expectation: RenderExpectation): RenderCheckResult { -->
|
|
129
|
-
Before its own post reaches the forge, `vinaya review post` refuses to post anything the gate would misread: it runs the exact same `VERDICT:`/`Judged head:`/`Objectives version:` extraction the merge gate uses over the rendered comment, and requires exactly the intended verdict to come back — and refuses outright if you pass a `--verdict PASS` together with any `NOT MET` objective, the same contradiction check `--verdict APPROVE` gets. Free text in a finding, `--config-scan`, `--secrets`, or an objective's evidence can say `VERDICT` or span multiple lines without risk — the extraction reads only a comment's first five lines, and in a code-reviewer or security comment a caller field never opens one of those lines (it only trails a fixed, renderer-owned label); an escalation's `--summary`
|
|
133
|
+
Before its own post reaches the forge, `vinaya review post` refuses to post anything the gate would misread: it runs the exact same `VERDICT:`/`Judged head:`/`Objectives version:`/`Ruling ordinal:` extraction the merge gate uses over the rendered comment, and requires exactly the intended verdict to come back — and refuses outright if you pass a `--verdict PASS` together with any `NOT MET` objective, the same contradiction check `--verdict APPROVE` gets. Free text in a finding, `--config-scan`, `--secrets`, or an objective's evidence can say `VERDICT` or span multiple lines without risk — the extraction reads only a comment's first five lines (the ruling ordinal's own first-seven-line window is wider still), and in a code-reviewer or security comment a caller field never opens one of those lines (it only trails a fixed, renderer-owned label); an escalation's `--summary` occupies line 7 at minimum (`Ruling ordinal:` renders unconditionally ahead of it), which is exactly why this pre-post re-parse exists rather than construction alone.
|
|
130
134
|
|
|
131
135
|
- **CRITICAL** — leaked live credential, auth bypass, key sent to client. Any CRITICAL → FAIL.
|
|
132
136
|
- **HIGH** — likely exploitable misconfig or injection surface.
|
|
@@ -303,7 +303,7 @@ function issueListByLabelArgs(owner, repo, label) {
|
|
|
303
303
|
"--state",
|
|
304
304
|
"all",
|
|
305
305
|
"--json",
|
|
306
|
-
"number,title,body,state,labels,milestone",
|
|
306
|
+
"number,title,body,state,labels,milestone,stateReason",
|
|
307
307
|
"--limit",
|
|
308
308
|
"200"
|
|
309
309
|
];
|
|
@@ -649,14 +649,19 @@ function resolveTaskIssueRef(title, labels) {
|
|
|
649
649
|
var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
|
|
650
650
|
var NEXT_HEADING = /^#{1,6}\s+\S/m;
|
|
651
651
|
var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
652
|
-
function
|
|
653
|
-
const text = stripCode(description, { inlineSpans: "keep" });
|
|
652
|
+
function intentsSection(text) {
|
|
654
653
|
const start = text.match(INTENTS_HEADING);
|
|
655
654
|
if (!start || start.index === undefined)
|
|
656
|
-
return
|
|
655
|
+
return null;
|
|
657
656
|
const rest = text.slice(start.index + start[0].length);
|
|
658
657
|
const next = rest.match(NEXT_HEADING);
|
|
659
|
-
|
|
658
|
+
return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
|
|
659
|
+
}
|
|
660
|
+
function intentGoalForSlug(description, slug) {
|
|
661
|
+
const text = stripCode(description, { inlineSpans: "keep" });
|
|
662
|
+
const section = intentsSection(text);
|
|
663
|
+
if (section === null)
|
|
664
|
+
return "";
|
|
660
665
|
for (const line of section.split(`
|
|
661
666
|
`)) {
|
|
662
667
|
const trimmed = line.trim();
|
|
@@ -1948,6 +1953,7 @@ function parseInlineFieldList(section) {
|
|
|
1948
1953
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1949
1954
|
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1950
1955
|
var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
|
|
1956
|
+
var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
|
|
1951
1957
|
function firstFiveLines(comment) {
|
|
1952
1958
|
return comment.split(`
|
|
1953
1959
|
`).slice(0, 5).join(`
|
|
@@ -1961,6 +1967,15 @@ function extractObjectivesVersion(comment) {
|
|
|
1961
1967
|
const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
|
|
1962
1968
|
return m ? m[1].toLowerCase() : null;
|
|
1963
1969
|
}
|
|
1970
|
+
function firstSevenLines(comment) {
|
|
1971
|
+
return comment.split(`
|
|
1972
|
+
`).slice(0, 7).join(`
|
|
1973
|
+
`);
|
|
1974
|
+
}
|
|
1975
|
+
function extractRulingOrdinal(comment) {
|
|
1976
|
+
const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
|
|
1977
|
+
return m ? Number.parseInt(m[1], 10) : null;
|
|
1978
|
+
}
|
|
1964
1979
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
1965
1980
|
const candidates = comments.filter((c) => valuePattern.test(c));
|
|
1966
1981
|
if (candidates.length === 0) {
|
|
@@ -1968,6 +1983,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1968
1983
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1969
1984
|
headSha: null,
|
|
1970
1985
|
objectivesVersion: null,
|
|
1986
|
+
rulingOrdinal: null,
|
|
1971
1987
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
1972
1988
|
};
|
|
1973
1989
|
}
|
|
@@ -1978,6 +1994,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1978
1994
|
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
|
|
1979
1995
|
headSha: null,
|
|
1980
1996
|
objectivesVersion: null,
|
|
1997
|
+
rulingOrdinal: null,
|
|
1981
1998
|
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
|
|
1982
1999
|
};
|
|
1983
2000
|
}
|
|
@@ -1985,6 +2002,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1985
2002
|
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1986
2003
|
headSha: extractHeadSha(latest),
|
|
1987
2004
|
objectivesVersion: extractObjectivesVersion(latest),
|
|
2005
|
+
rulingOrdinal: extractRulingOrdinal(latest),
|
|
1988
2006
|
danglingNote: null
|
|
1989
2007
|
};
|
|
1990
2008
|
}
|
|
@@ -2346,6 +2364,9 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
2346
2364
|
var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
|
|
2347
2365
|
var NEXT_HEADING_RE = /^##[ \t]/m;
|
|
2348
2366
|
var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
|
|
2367
|
+
function maskedForHeadingSearch(body) {
|
|
2368
|
+
return maskDetailsBlocks(maskCode(body));
|
|
2369
|
+
}
|
|
2349
2370
|
function hasBacktickedPath(text) {
|
|
2350
2371
|
let i = 0;
|
|
2351
2372
|
while (i < text.length) {
|
|
@@ -2368,16 +2389,22 @@ function stripObjectiveBackticks(text) {
|
|
|
2368
2389
|
function wordCount(text) {
|
|
2369
2390
|
return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
|
|
2370
2391
|
}
|
|
2371
|
-
function
|
|
2372
|
-
const
|
|
2392
|
+
function objectivesSectionBounds(body) {
|
|
2393
|
+
const masked = maskedForHeadingSearch(body);
|
|
2394
|
+
const heading = HEADING_RE.exec(masked);
|
|
2373
2395
|
if (!heading)
|
|
2374
2396
|
return null;
|
|
2375
|
-
const
|
|
2397
|
+
const start = heading.index + heading[0].length;
|
|
2398
|
+
const afterHeading = masked.slice(start);
|
|
2376
2399
|
const next = NEXT_HEADING_RE.exec(afterHeading);
|
|
2377
|
-
return next ?
|
|
2400
|
+
return { start, end: next ? start + next.index : body.length };
|
|
2401
|
+
}
|
|
2402
|
+
function objectivesSectionText(body) {
|
|
2403
|
+
const bounds = objectivesSectionBounds(body);
|
|
2404
|
+
return bounds === null ? null : body.slice(bounds.start, bounds.end);
|
|
2378
2405
|
}
|
|
2379
2406
|
function hasObjectivesHeading(body) {
|
|
2380
|
-
return HEADING_RE.test(body);
|
|
2407
|
+
return HEADING_RE.test(maskedForHeadingSearch(body));
|
|
2381
2408
|
}
|
|
2382
2409
|
function objectivesOf(body) {
|
|
2383
2410
|
const section = objectivesSectionText(body);
|
|
@@ -2437,6 +2464,15 @@ function isIssueNotFoundError(err) {
|
|
|
2437
2464
|
`);
|
|
2438
2465
|
return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
|
|
2439
2466
|
}
|
|
2467
|
+
function resolveObjectivesSource(prBody, issue, cutoverIssue) {
|
|
2468
|
+
if (issue !== null && issue < cutoverIssue)
|
|
2469
|
+
return { kind: "none" };
|
|
2470
|
+
if (issue !== null)
|
|
2471
|
+
return { kind: "issue", issue };
|
|
2472
|
+
if (hasObjectivesHeading(prBody))
|
|
2473
|
+
return { kind: "body" };
|
|
2474
|
+
return { kind: "none" };
|
|
2475
|
+
}
|
|
2440
2476
|
|
|
2441
2477
|
// ../../packages/aeg-core/src/premise-check.ts
|
|
2442
2478
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -2938,17 +2974,40 @@ function checkBriefSections(prBody, readTier, options = {}) {
|
|
|
2938
2974
|
];
|
|
2939
2975
|
return { errors: results.flatMap((r) => r.errors) };
|
|
2940
2976
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
`
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2977
|
+
function contentAfterNLines(body, n) {
|
|
2978
|
+
let idx = -1;
|
|
2979
|
+
for (let i = 0;i < n; i++) {
|
|
2980
|
+
idx = body.indexOf(`
|
|
2981
|
+
`, idx + 1);
|
|
2982
|
+
if (idx === -1)
|
|
2983
|
+
return "";
|
|
2984
|
+
}
|
|
2985
|
+
return body.slice(idx + 1);
|
|
2986
|
+
}
|
|
2987
|
+
var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
|
|
2988
|
+
function parseBriefMarkerVersion(firstLine) {
|
|
2989
|
+
const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
|
|
2990
|
+
if (!m)
|
|
2991
|
+
return null;
|
|
2992
|
+
const version = Number.parseInt(m[1], 10);
|
|
2993
|
+
return Number.isInteger(version) && version >= 1 ? version : null;
|
|
2994
|
+
}
|
|
2995
|
+
function frozenBriefContent(body, version) {
|
|
2996
|
+
return contentAfterNLines(body, version === 1 ? 2 : 3);
|
|
2997
|
+
}
|
|
2998
|
+
function resolveNewestFrozenBrief(comments, allowlist) {
|
|
2999
|
+
let best = null;
|
|
3000
|
+
for (const c of comments) {
|
|
3001
|
+
if (!isPrincipal(c.author, allowlist))
|
|
3002
|
+
continue;
|
|
3003
|
+
const version = parseBriefMarkerVersion(c.body.split(`
|
|
3004
|
+
`)[0] ?? "");
|
|
3005
|
+
if (version === null)
|
|
3006
|
+
continue;
|
|
3007
|
+
if (best === null || version > best.version)
|
|
3008
|
+
best = { ...c, version };
|
|
3009
|
+
}
|
|
3010
|
+
return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
|
|
2952
3011
|
}
|
|
2953
3012
|
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
2954
3013
|
var DEFAULT_SHIPS_PREFIX = "aeg-root/";
|
|
@@ -3675,6 +3734,11 @@ function isBoundToObjectives(extraction, currentVersion) {
|
|
|
3675
3734
|
return true;
|
|
3676
3735
|
return extraction.objectivesVersion === currentVersion;
|
|
3677
3736
|
}
|
|
3737
|
+
function isBoundToRulings(extraction, currentOrdinal) {
|
|
3738
|
+
if (extraction.rulingOrdinal === null)
|
|
3739
|
+
return currentOrdinal === 0;
|
|
3740
|
+
return extraction.rulingOrdinal === currentOrdinal;
|
|
3741
|
+
}
|
|
3678
3742
|
function checkReviewGate(input) {
|
|
3679
3743
|
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
3680
3744
|
const waived = isWaiverLabelActorVerified({
|
|
@@ -3703,7 +3767,9 @@ function checkReviewGate(input) {
|
|
|
3703
3767
|
const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
|
|
3704
3768
|
const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
|
|
3705
3769
|
const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
|
|
3706
|
-
|
|
3770
|
+
const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
|
|
3771
|
+
const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
|
|
3772
|
+
if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
|
|
3707
3773
|
return {
|
|
3708
3774
|
verdict: "pass",
|
|
3709
3775
|
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
|
|
@@ -3717,6 +3783,8 @@ function checkReviewGate(input) {
|
|
|
3717
3783
|
problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3718
3784
|
} else if (!codeReviewObjectivesBound) {
|
|
3719
3785
|
problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3786
|
+
} else if (!codeReviewRulingsBound) {
|
|
3787
|
+
problems.push(`the newest code-review verdict was cast against ruling ordinal ${codeReview.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
|
|
3720
3788
|
}
|
|
3721
3789
|
if (!securityClean) {
|
|
3722
3790
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
@@ -3724,6 +3792,8 @@ function checkReviewGate(input) {
|
|
|
3724
3792
|
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3725
3793
|
} else if (!securityObjectivesBound) {
|
|
3726
3794
|
problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3795
|
+
} else if (!securityRulingsBound) {
|
|
3796
|
+
problems.push(`the newest security-review verdict was cast against ruling ordinal ${security.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
|
|
3727
3797
|
}
|
|
3728
3798
|
if (!mechanicalChecksClean) {
|
|
3729
3799
|
problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
|
|
@@ -3740,6 +3810,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
|
|
|
3740
3810
|
function isChangesetsReleasePr(branch, author, expectedAuthor) {
|
|
3741
3811
|
return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
|
|
3742
3812
|
}
|
|
3813
|
+
// ../../packages/aeg-core/src/ruling-ordinal.ts
|
|
3814
|
+
var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
|
|
3815
|
+
function newestPrincipalRulingOrdinal(comments, allowlist) {
|
|
3816
|
+
let best = 0;
|
|
3817
|
+
for (const c of comments) {
|
|
3818
|
+
if (!isPrincipal(c.author, allowlist))
|
|
3819
|
+
continue;
|
|
3820
|
+
const firstLine = (c.body.split(`
|
|
3821
|
+
`)[0] ?? "").trim();
|
|
3822
|
+
const m = RULING_MARKER_ORDINAL.exec(firstLine);
|
|
3823
|
+
if (!m)
|
|
3824
|
+
continue;
|
|
3825
|
+
const k = Number.parseInt(m[1], 10);
|
|
3826
|
+
if (k > best)
|
|
3827
|
+
best = k;
|
|
3828
|
+
}
|
|
3829
|
+
return best;
|
|
3830
|
+
}
|
|
3743
3831
|
// ../../packages/aeg-core/src/markdown-table.ts
|
|
3744
3832
|
function splitRow(line) {
|
|
3745
3833
|
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
@@ -5519,6 +5607,23 @@ function blankUnanchoredStructuralFields(body) {
|
|
|
5519
5607
|
`).map((l) => blankProjectField(blankTierField(l))).join(`
|
|
5520
5608
|
`);
|
|
5521
5609
|
}
|
|
5610
|
+
var OBJECTIVE_MARKER_LINE = /^ {0,3}O\d{1,9}\./;
|
|
5611
|
+
function blankObjectiveMarkerLine(line) {
|
|
5612
|
+
const m = OBJECTIVE_MARKER_LINE.exec(line);
|
|
5613
|
+
if (!m)
|
|
5614
|
+
return line;
|
|
5615
|
+
return " ".repeat(m[0].length) + line.slice(m[0].length);
|
|
5616
|
+
}
|
|
5617
|
+
function blankObjectiveMarkers(body) {
|
|
5618
|
+
const bounds = objectivesSectionBounds(body);
|
|
5619
|
+
if (!bounds)
|
|
5620
|
+
return body;
|
|
5621
|
+
const section = body.slice(bounds.start, bounds.end);
|
|
5622
|
+
const blanked = section.split(`
|
|
5623
|
+
`).map(blankObjectiveMarkerLine).join(`
|
|
5624
|
+
`);
|
|
5625
|
+
return body.slice(0, bounds.start) + blanked + body.slice(bounds.end);
|
|
5626
|
+
}
|
|
5522
5627
|
var CLOSES_REF = /Closes\s*#\d+/i;
|
|
5523
5628
|
var EVIDENCE_HEADING = /^#{1,6}\s/;
|
|
5524
5629
|
var EVIDENCE_HEAD_LINE = /^Head:\s*\S+$/i;
|
|
@@ -5546,6 +5651,7 @@ function buildScanMask(ctx) {
|
|
|
5546
5651
|
masked = blankPremiseValues(masked);
|
|
5547
5652
|
masked = blankTokenReportSection(masked);
|
|
5548
5653
|
masked = blankUnanchoredStructuralFields(masked);
|
|
5654
|
+
masked = blankObjectiveMarkers(masked);
|
|
5549
5655
|
return masked;
|
|
5550
5656
|
}
|
|
5551
5657
|
var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
|