@attalabs/vinaya 0.6.0 → 0.7.1
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/aeg-root/contracts/security-archivist.md +103 -0
- package/aeg-root/enforcement.md +9 -3
- package/aeg-root/roles/brief-author.md +2 -2
- package/aeg-root/roles/planner.md +14 -3
- package/aeg-root/roles/reviewer.md +5 -2
- package/aeg-root/roles/security.md +3 -1
- package/dist/checks/bin/check-doc-coverage-push.js +2 -1
- package/dist/checks/bin/check-doc-coverage.js +2 -1
- package/dist/checks/bin/check-review-gate.js +2 -1
- package/dist/index.js +49 -14
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
sidebar_title: Security → Archivist
|
|
3
|
+
title: Security → Archivist
|
|
4
|
+
order: 7
|
|
5
|
+
contract_id: security-archivist
|
|
6
|
+
description: Carries the security review's verdict and findings into the permanent record, so a recorded pass is a copied fact, not a claim.
|
|
7
|
+
status: active
|
|
8
|
+
producer: security
|
|
9
|
+
consumer: archivist
|
|
10
|
+
carrier: pr-verdict-comment
|
|
11
|
+
summary: Ever seen a record claim a security pass with nothing behind it?
|
|
12
|
+
---
|
|
13
|
+
# Contract: Security Reviewer → per-task Archivist
|
|
14
|
+
|
|
15
|
+
## The short version
|
|
16
|
+
|
|
17
|
+
This seam sits between the security pass and the permanent record of the work it examined. It exists because the record's `Security:` field can only be honest if it is copied from a verdict that says what was actually checked.
|
|
18
|
+
|
|
19
|
+
**What crosses** — the Security Reviewer's verdict, and the merged pull request that carries it. The verdict itself: pass, or fail. The findings, each with a severity, so the record can tell a hardening note from an exploitable surface. The explicit result of each scan — the configuration scan's outcome, and whether any secret was found. And the reviewer's one-line token report, the ledger's only source for this role's turn.
|
|
20
|
+
|
|
21
|
+
**The hand-off is malformed when** — the verdict line is absent, decorated, or unclear; when a finding carries no severity; when a scan result is simply not stated; or when a re-pass edits the earlier verdict instead of posting a new one. A verdict comment in any of these states is not posted; the reviewer revises it first. "Looks fine security-wise" is the failure this seam was written against: it leaves close-out the choice of inventing a field or leaving a hole.
|
|
22
|
+
|
|
23
|
+
**What it does not carry** — permission to close out an unmerged change: the verdict is not the authorisation, the merge is. Nor a second security opinion at close-out: the record copies the verdict, it never re-judges it. Nor the content of any secret found: the verdict comment freezes into the permanent record at merge, so a finding names where a secret lives and enough characters to identify it — never the value.
|
|
24
|
+
|
|
25
|
+
**How it physically runs** — the carrier is the security verdict comment on the pull request, which becomes a frozen fact once that pull request merges. The verdict line is written bare, on its own line, because it is machine-read twice by the same parser: before the merge, by the blocking review gate that refuses to merge without a clean pass, and after it, by the close-out that copies the verdict into the provenance record. The most recent clear verdict wins, which is how a fail that was fixed and re-passed reads as resolved rather than as a contradiction. A critical or high finding that merged anyway means a deviation was consciously accepted — it is recorded as such, not quietly dropped.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Reference
|
|
31
|
+
|
|
32
|
+
**Status:** active
|
|
33
|
+
**Seam:** the hand-off from the Security Reviewer (producer) to the per-task Archivist (consumer).
|
|
34
|
+
**Single source of truth for this seam.** The two role docs do **not** redefine what crosses this boundary — they point here. `aeg-root/roles/security.md` (producer side) and `aeg-root/roles/archivist.md` (consumer side) each reference this file; this file is where the field-by-field hand-off lives, once.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Why this file exists
|
|
39
|
+
|
|
40
|
+
The provenance block's template (`roles/archivist.md`) carries a `Security: PASS | FAIL→resolved` field — and before this file, nothing defined the producer obligations behind it. The Reviewer → Archivist seam had a contract; the security half of the same close-out did not, even though both verdicts are read from the same pull request by the same assembly. An unwritten seam fails the same way a vague one does: the Archivist cannot tell what the security pass established from what it merely never mentioned.
|
|
41
|
+
|
|
42
|
+
The failure mode this prevents: a `Security:` field written from memory or inference rather than copied from a frozen verdict; a `CRITICAL` or `HIGH` finding that merged under a waiver and then vanished from the record; a token ledger row fabricated for a reviewer who never reported one.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## The hand-off carrier
|
|
47
|
+
|
|
48
|
+
The **Security Reviewer's verdict comment** on the open (then merged) PR, plus the **merged PR itself**. The verdict comment is the producer's output; the merge is the trigger that authorizes the Archivist to begin close-out. Two mechanical readers consume the comment through one shared parser *(in this repo: `packages/aeg-core/src/verdict-extraction.ts`)*: the pre-merge review gate, which blocks a task PR from merging without a clean `PASS` from a principal-allowlisted author or a principal's actor-verified `vinaya/waiver:review` label (`roles/security.md` § merge gate), and the post-merge provenance assembly, which copies the most recent clear verdict into the record. The parser is line-anchored: a blockquoted, bulleted, or backticked `VERDICT:` line reads as *missing*, not as a verdict.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## The contract — field-by-field mapping
|
|
53
|
+
|
|
54
|
+
Every item the Security Reviewer produces in the verdict (left) has exactly one obligation for the per-task Archivist (right). A verdict missing any left-column item is malformed — the Security Reviewer refuses to post it in that state.
|
|
55
|
+
|
|
56
|
+
| Security Reviewer produces | per-task Archivist consumes at | What the consumption means |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| **Verdict** (`PASS` or `FAIL`, bare `VERDICT:` line) | Entry gate + the provenance block's `Security:` field | Close-out runs only on merged PRs, and a task PR only merges with a clean `PASS` (or a principal's actor-verified waiver). The field records the grammar `PASS \| FAIL→resolved` (`roles/archivist.md`): the most recent clear verdict, an earlier fixed-and-re-passed `FAIL` reading as resolved. An absent or unclear verdict records as DANGLING — never as an inferred pass. |
|
|
59
|
+
| **Finding list** with severity tags (`CRITICAL` / `HIGH` / `MEDIUM` / `LOW`) | Provenance block assembly + pinned lessons Issue | Any `CRITICAL` or `HIGH` forces `VERDICT: FAIL` (`roles/security.md`), so one present on a merged PR means the fail was resolved and re-passed — or the merge rode a waiver. A waived `CRITICAL`/`HIGH` is a consciously accepted deviation: the Archivist logs it under DANGLING and posts to the pinned lessons Issue, mirroring the `BLOCKER`/`MAJOR` row of `contracts/reviewer-archivist.md`. |
|
|
60
|
+
| **Token report** (the closing `Tokens: <task-id>: security — …` line) | Token ledger collection at close-out | The Archivist records one ledger row from this line, copying its figures exactly (`—` cells stay `—`). A verdict comment with no `Tokens:` line means no row — the gap is flagged under DANGLING, never filled in. |
|
|
61
|
+
|
|
62
|
+
**Reading the table:** left is the producer obligation (`roles/security.md` and this contract enforce it), right is the consumer obligation (`roles/archivist.md` and this contract enforce it). The two role docs must not contradict this table.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Producer obligations (the Security Reviewer)
|
|
67
|
+
|
|
68
|
+
- The verdict line is written bare, on its own line, in the exact format specified by `roles/security.md`: `VERDICT: PASS | FAIL`. No heading, no blockquote, no code span — the machine readers are line-anchored, and a decorated verdict reads as missing.
|
|
69
|
+
- Every finding must carry a severity tag (`CRITICAL`, `HIGH`, `MEDIUM`, or `LOW`). Any `CRITICAL` or `HIGH` means the verdict is `FAIL` — this contract records that grammar, it does not define it; `roles/security.md` does.
|
|
70
|
+
- The `CONFIG SCAN:` and `SECRETS:` lines must be stated explicitly, even when the answer is `not applicable` or `none found`. An unstated scan is indistinguishable from an unrun one — and the `SECRETS:` line is evidence-backed, not asserted: the secret scanner's pasted output must appear in the verdict comment above it (`roles/security.md`, check 1).
|
|
71
|
+
- No discovered secret is quoted in full — file, line, and enough characters to identify it. The verdict comment becomes a permanent record at merge; a full value in it is the second leak.
|
|
72
|
+
- The comment closes with the one-line token report (`roles/security.md` § turn-end).
|
|
73
|
+
- A re-pass after the Developer's fixes posts a **new** verdict comment, never edits the prior one — most-recent-wins is how the record tells a resolved fail from a contradiction.
|
|
74
|
+
- A verdict comment missing any of these elements is malformed. The Security Reviewer does not post it.
|
|
75
|
+
|
|
76
|
+
## Consumer obligations (the per-task Archivist)
|
|
77
|
+
|
|
78
|
+
- Do not run close-out on unmerged PRs. The merge is the authorization signal — confirmed by querying the forge, not by reading a status field.
|
|
79
|
+
- **Read the verdict before recording it.** Before writing or re-confirming the `Security:` field, read the frozen verdict comment from the PR itself and paste what was read into the close-out report *(in this repo)*:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
gh pr view <N> --json comments \
|
|
83
|
+
--jq '[.comments[].body | select(test("(?m)^\\s*\\**VERDICT:\\s*(PASS|FAIL)"))] | last'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
A `Security:` field written without that read is authored, not assembled — the exact failure this contract exists to prevent.
|
|
87
|
+
- Copy, never judge: `PASS` records as `PASS`; an earlier `FAIL` fixed and re-passed records as resolved; an absent or unclear verdict records as DANGLING. Close-out is bookkeeping, not a second security review.
|
|
88
|
+
- A `CRITICAL` or `HIGH` finding present in the verdict history of a merged PR means a deviation was approved. Log it in the provenance block under DANGLING and post a new comment on the pinned lessons Issue.
|
|
89
|
+
- Record the token ledger row from the verdict comment's `Tokens:` line, exactly as reported. A missing line is flagged under DANGLING; a row is never fabricated for it.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Changing this contract
|
|
94
|
+
|
|
95
|
+
A contract changes **as a unit**. You may not change what the Security Reviewer produces without, in the same change, updating what the per-task Archivist consumes — because the property that makes the seam sound is that the producer's output side is *identical* to the consumer's input side. Concretely:
|
|
96
|
+
|
|
97
|
+
- A change to this file is a **Tier 3** change: it alters a cross-role contract, so the reasoning belongs in the pull request that makes it, where the reviewer and the close-out both read it.
|
|
98
|
+
- The same PR that edits this contract must verify both `aeg-root/roles/security.md` and `aeg-root/roles/archivist.md` still point here and still match the table.
|
|
99
|
+
- Never edit one side's role doc to add/drop a hand-off field directly. Add/drop it **here**; the role docs inherit it by reference.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
*This contract is the seam. The Security Reviewer fills the left column; the per-task Archivist drains the right. One source of truth, changed as a unit.*
|
package/aeg-root/enforcement.md
CHANGED
|
@@ -16,11 +16,11 @@ provenance: >
|
|
|
16
16
|
they are not in the surface map of a task that only meant to add a gate:
|
|
17
17
|
|
|
18
18
|
- packages/aeg-core/src/registry-parse.test.ts asserts the TOTAL row count
|
|
19
|
-
across all three rings (currently
|
|
19
|
+
across all three rings (currently 40).
|
|
20
20
|
- packages/aeg-core/src/markdown-table.test.ts asserts per-ring counts
|
|
21
|
-
(currently ring 0 =
|
|
21
|
+
(currently ring 0 = 16, ring 1 = 17, ring 2 = 7).
|
|
22
22
|
- packages/aeg-core/src/docs/node-route.test.ts asserts the ring-0 count
|
|
23
|
-
(currently
|
|
23
|
+
(currently 16) alongside the actions/roles/contracts counts.
|
|
24
24
|
|
|
25
25
|
Bumping those numbers is a mechanical consequence of a correct edit, not a
|
|
26
26
|
design change — fix them in the same commit and say so in the PR body.
|
|
@@ -79,6 +79,10 @@ The same check implementations run at ring 0 and ring 1 — one codebase, two en
|
|
|
79
79
|
| Opening a task PR (final self-check before creation) | Ever opened a PR and only then discovered the tests were failing? | event | Runs the whole exit check before a pull request is created, so failures surface first. | **`verify-task` CLI** (`packages/aeg-core/bin/verify-task.ts` — mandated by `roles/developer.md`; **now also hook-automated** — task 25) | Typecheck, lint, tests, build, `verify-docs --pr`, and the premise coverage/recheck pair all pass, as one summary, against the PR's actual diff, before `open-pr.ts` is invoked. **`open-pr.ts` now runs this composite itself** for task branches (via `gatePlanForBranch`), invoked wholesale rather than partially re-implemented — non-task branches are unaffected (byte-identical gate set). `verify-docs --pr` runs twice on a task-branch PR-open as an accepted, measured overlap (~4s with a warm turbo cache). | `packages/aeg-core/bin/verify-task.ts` |
|
|
80
80
|
| Spawning a check (`vinaya check`, ring-0 pre-push AND ring-1 CI) | Ever had a check quietly read a secret it had no business seeing, because nothing scoped what it could reach? | hook | Governs which environment variables a spawned check's child process can see, instead of every check inheriting the full parent environment unconditionally. | **Env allowlist** (`CheckSpec['env']`, `apps/cli/src/checks/contract.ts`; construction: `buildCheckEnv`, `apps/cli/src/checks/runner.ts`) | **⚠️ BREAKING, live (task 3):** a spawned check's child process sees only a fixed baseline (`PATH`, `LANG`, `HOME`, `HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, `TMPDIR`) plus whatever its own `env` declaration (`true` / `{ optional: true }` / `{ anyOf: [...] }` / a literal string) explicitly forwards — **no longer the full parent environment.** An undeclared variable a check's own code reads is now genuinely invisible to it, not merely warned about; a `true` or unsatisfied `anyOf` declaration missing from the caller's environment synthesizes a `CheckError` and the check never spawns at all. All 15 core checks in `registry.ts` carry an audited declaration, and every core check that spawns `gh` or reaches the forge through the token-resolution chain additionally forwards `GITHUB_TOKEN`/`GH_TOKEN` as `{ optional: true }` — on a CI runner those env vars are `gh`'s only authentication path, so a check that shells out without forwarding them runs unauthenticated there (hard-failing or silently degrading, depending on the bin's failure mode) while passing locally on `gh`'s keyring; the pairing is coupling-tested (`apps/cli/tests/checks/registry-env.test.ts` detects `gh` invocations in any call shape — array-form, string-form, and template-literal — and demands both declarations); that detection is source-text pattern matching, so it does not yet catch a check that reaches the forge indirectly through `createForgeSource` (`@atta/vinaya-sources`) with no literal `gh` call of its own — found live: `closes-n` reaches the forge this way and had shipped without either token declared, passing locally under a developer's own `gh` keyring and failing unauthenticated on every CI runner, a known gap in the coupling test's own coverage rather than in the rule it's checking. This repo's own core checks are therefore unaffected — an adopter's **custom** check that reads `process.env` directly with no `env` declared will lose that access at this minor; declare `env` on it (`vinaya.config.json`'s `checks.<name>.env`) before upgrading. `vinaya check`'s prior warn-phase print (task 2) is retired now that the behavior it warned about is live; `vinaya doctor` still carries the same missing-declaration diagnostic permanently, at `info` severity, so a custom check's gap remains visible after upgrade even though `vinaya check` no longer prints it inline. A second field, `requiresOpenPr?: boolean` (same no-privileged-field discipline as `env` — a custom check can declare it exactly like a core one), marks a check that can only evaluate meaningfully once a pull request exists; the generated `pre-commit`/`pre-push` hooks pass a new `--local` flag that skips every check declaring it, while CI (`vinaya-checks.yml`, `pull_request`-triggered) omits the flag and always runs them for real. Found live: `closes-n`/`test-plan` ran unconditionally in both hooks with no `requiresOpenPr` field to opt out on, so the first commit on a fresh task branch could never satisfy either — no PR exists yet at commit or push time, only after. Found live again, same missing-declaration class: `brief-shape` read `BRANCH` from `process.env` in its own bin without declaring it in `registry.ts`'s allowlist, so the runner stripped the variable before the child ever spawned — the non-task-branch bypass its logic depended on (skipping the `Closes #N` requirement for a standalone brief with no Issue to close) silently never fired; `registry.ts` now declares `BRANCH: { optional: true }` on that entry alongside `PR_BODY`, mirroring `test-plan`'s existing declaration for the same pair. `vinaya.config.json`'s `principals` field (the review-gate/waiver trust anchor, `apps/cli/src/lib/config.ts`'s `resolvePrincipalAllowlist`) is resolved via `loadTrustAnchorConfig()` — a `gh api` read of the repository's DEFAULT BRANCH, never local git, the PR's working tree, or any env var. Three successive attempts got this wrong before landing (all caught pre-merge): reading the PR's working tree; reading `git show ${BASE_SHA}:…` where `BASE_SHA` was an env var; and reading `git show origin/main:…`, where `origin/main` is a LOCAL remote-tracking ref the PR's own workflow can `git update-ref`. The rule they establish: **inside a `pull_request`-triggered workflow nothing on the job's own disk or environment is a trust boundary against the PR author**, because the workflow definition itself comes from the PR. `check-doc-coverage.ts`/`check-doc-coverage-push.ts` keep their own, separate, legitimately-overridable `BASE_SHA` for diff-scoping only, never reused for a trust decision. The API read raises the bar but is not itself the boundary — and the boundary is not where the earlier version of this sentence put it. **Branch protection with the check marked required is still worth enabling** — `init` prints that command and `doctor` reports its absence — but state exactly what it buys: a required status check is satisfied by a conclusion reported under its name, and it does not certify that the conclusion came from running the real check. Deleting the gate's *step* does not stop the *job* reporting green; only removing the job or the workflow outright produces the never-reports case that leaves a PR unmergeable, and a step edited to `exit 0` reports success under the required name having run nothing at all. So the rule stated in bold above extends one step further, to the verdict itself: **the trust boundary is who controls the workflow definition that produces the required check, and under a `pull_request` trigger that is the PR author — whatever the check is packaged as.** Packaging decides the blast radius, not the boundary. Where CI invokes an immutable published artifact, the invocation is the only thing the PR can rewrite. Where a repo vendors the CLI — its own workspaces declaring the published package name, which needs a build-and-run CI shape because `npx` matches on package **name** before any version spec is read and would otherwise exec an unbuilt local bin — the PR additionally controls the check sources, the build script, and the **dependency lifecycle scripts** the install step executes in a base-branch-scoped cache: the surface becomes all code the PR controls. The same widening arrives with no packaging question at all whenever a required workflow checks out the PR's head and runs a gate from that tree — this harness's own review gate does exactly that, so it sits in the wider class today by a hand-written workflow, not by any generator. Two things are therefore NOT claimed here. Not that verdict tampering is closed for an ordinary adopter: it is not, and an adopter who enables branch protection and stops there has closed merge-without-a-report and nothing else. And not that the vendored CI shape is something generated today — as of 2026-08-14 `init` writes only the published `npx` invocation, which in a vendoring repo misresolves to the unbuilt local bin and kills the job, so such a repo's required check currently yields no verdict at all rather than an untrustworthy one; that is its own governance problem, and the build-and-run generator that fixes it is not yet merged. What closes the residual gap is not a mechanism: a repo whose required checks are built from code its own pull requests can edit is governing itself, and there the last line is the reviewer — changes to check sources, the build script, or the dependency manifest are reviewed as governance changes, not as ordinary code. | `apps/cli/src/checks/runner.ts` |
|
|
81
81
|
| `vinaya check` / `vinaya check --plan` (a CLI mechanism, not a git hook — this row names no hook path) | Ever had a config entry silently double-run alongside the core check it was meant to replace, with no way to see that from the outside? | event | Resolves core-registered and config-registered checks into one deterministic table before anything runs, instead of letting a config entry run alongside the core check it collides with, unannounced. | **Checks-side resolver** (`resolveChecks`/`isValidNamespacedKey`, `apps/cli/src/checks/resolver.ts`) | An exact-key match against a core check ID is an **override** — a complete replacement of the spec, never a merge. A key containing exactly one `/`, both segments matching `[a-z0-9][a-z0-9-]*` and the `vinaya` prefix rejected as an exact segment match, is **additive**. Anything else — a bare key with no `/` matching no core ID — is a loud `FAIL_CLOSED` refusal: never silently dropped, never defaulted, never last-wins. `--plan` never resolves or prints an env *value*, only how each one resolves. **This release feeds `vinaya check --plan` / `--plan --json` only** — `vinaya check`'s real execution (`check.ts`'s flat `[...coreCheckRegistry(), ...customSpecs]` concat) is unaffected until a later task wires it in. | `apps/cli/src/checks/resolver.ts` |
|
|
82
|
+
| Pushing a task branch that matches no planned task | Ever pushed a task branch that belonged to no plan? | hook | Refuses to push a task branch whose name matches no task row derived from the forge. | **`check-branch-topology` standalone shim** (in this repo the managed pre-push hook runs the same gate as the registered `branch-topology` check — a thin adapter in `apps/cli/src/checks/bin/` over the same pure evaluator; this row names the seeded standalone CLI form) | The branch's `task/<tranche>/<n>` fields resolve against the forge-derived tranche (a Milestone plus labeled task Issues): the tranche derives, and some task row's id literal-equals `<n>`. Fail-closed — forge/network unreachability refuses the push, matching `verify-dispatch`'s precedent for hard gates, unlike the fail-open dead-branch guard below. Known prose staleness, accepted: its refusal text still names a per-tranche topology file under `aeg-root/tranches/` that post-cutover repos no longer carry — the gate's logic is forge-native and correct, but the message text is a byte-identical-compatibility contract shared with the registered adapter and asserted verbatim by tests, so rewording it is a real change to that contract, not a message tweak. | `packages/aeg-core/bin/check-branch-topology.ts` |
|
|
83
|
+
| Pushing to a branch whose pull request already resolved | Ever pushed more commits to an already-merged branch? | hook | Refuses a push to a task branch whose most recent pull request is already merged or closed. | **`check-push-target` standalone shim** (the registered `dead-branch-push` check is a thin adapter over the same `checkDeadBranchPush` evaluator; this row names the seeded standalone CLI form) | The branch's most recent pull request — one batched `gh pr list --head <branch>` call — is still `OPEN`, or none exists yet. Deliberately fail-open: any forge-reachability failure (auth, network, rate limit, malformed JSON) maps to `UNKNOWN`, treated as allow — a transient outage must never block every push. The ring-2 dead-branch-push audit below is this gate's detection backstop for writers the hook cannot reach. | `packages/aeg-core/bin/check-push-target.ts` |
|
|
84
|
+
| A task branch's first push (dispatch re-check) | Ever found out mid-push that your task was never dispatchable? | hook | Re-runs the dispatch-readiness gate once, on a task branch's first push, before its pull request exists. | **`check-first-push-dispatch` standalone shim** (wraps the unchanged `verify-dispatch` gate mode — see the dedicated `verify-dispatch` row above; the registered `first-push-dispatch` check classifies the same readiness through the shared evaluator) | On a `task/*/*` branch with no pull request yet, `verify-dispatch`'s `dispatch-readiness:` line reads `READY`. Only that line is read — never the combined exit code, which also folds in leftover-detection and would false-block every push after the first on a task legitimately mid-flight. Fail-open on infrastructure, loudly: a failed `gh auth status` probe (or `verify-dispatch`'s own infra marker) classifies as `UNKNOWN` and the push is allowed; unparseable output refuses — fail loud, never silently allow. Once a PR exists, later pushes skip the gate: dispatch was already validated once. | `packages/aeg-core/bin/check-first-push-dispatch.ts` |
|
|
85
|
+
| A task branch's first push (Issue self-assignment) | Ever had no idea who was actually working a task? | hook | Assigns the task's Issue to the authenticated pusher on the branch's genuinely first push — visibility automation, deliberately not a gate. | **`assign-task-issue` standalone shim** (the registered `issue-assignment` check mirrors it; the Issue-self-assignment note below this table is the same mechanism's prose record) | Nothing — this row names a mechanism, not a refusal. Fail-open by contract: it runs after every blocking gate has passed, degrades every failed forge call to a skip, warns and exits 0 on any API failure, and nothing gates on its exit code — a failed assignment can never block a legitimate push. Idempotent: an already-assigned Issue is a no-op, and a push to an already-existing branch never re-triggers it. | `packages/aeg-core/bin/assign-task-issue.ts` |
|
|
82
86
|
|
|
83
87
|
**Documentation coverage, specifically** (a historical pain point): the code→document ownership rule is enforced at *two* prevention chokepoints — at every push (over the branch's cumulative change set) and again at pull-request creation and editing. A change to owned code cannot be published, let alone turned into a pull request, without its owning document.
|
|
84
88
|
|
|
@@ -114,6 +118,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
114
118
|
| G4 — cited forge numbers resolve | Ever read a doc that cited a ticket number that didn't exist? | ci | Re-checks that every Issue and PR number cited in the docs resolves to a real one. | Every `#NNN` cited anywhere in this page's body is a real Issue or PR in the forge — a fabricated citation fails the build. **Currently scans nothing by design** (task 14): this page's body carries zero forge citations, since task 3 banned them from `aeg-root/**` as doctrine and the `reader-resolvable-prose` check (task 15) now enforces that ban directly. G4 is a standing guard against reintroduction, not a live proof of ongoing citation correctness — it fires the moment a `#NNN` reappears here and doesn't resolve, but has nothing to scan while the ban holds. Verified live: `packages/aeg-core/src/registry-checks.test.ts` appends a fabricated citation to this page's real content and asserts `checkG4` catches it. | `packages/aeg-core/bin/verify-registry.ts` |
|
|
115
119
|
| G5 — role/contract integrity | Ever had a process doc reference a role that was never actually defined? | ci | Re-checks that every role and contract the doctrine references is really defined. | Every `aeg-root/contracts/*.md`'s `producer`/`consumer` names a real `role_id` from `aeg-root/roles/*.md`; every role's `performs`/`refuses_when` frontmatter is present and non-empty. | `packages/aeg-core/bin/verify-registry.ts` |
|
|
116
120
|
| reader-resolvable-prose | Ever read a doc that assumed you already worked here? | ci | Re-checks that reader-facing doctrine and site pages don't cite a forge number/tranche slug the reader can't resolve, or use coined vocabulary without defining it. | Two mechanizable classes over every `aeg-root/**` doc and every public-site `(site)/**/page.tsx`, never `apps/*/specs/**` or a `CLAUDE.md` (this reader has this forge): unresolvable references (a bare forge number, or a `-vN`/legacy tranche slug — the same pattern shapes `retired-vocabulary.test.ts` proved, scoped wider here); and a coined term (`tranche`, `brief`, `forge`, `provenance`, `dispatch`, …) used with neither an inline definition nor a link to the glossary — the term list is derived live from `aeg-root/glossary.md`'s own entry headings, never hard-coded. NOT registered in `coreCheckRegistry()` — its scan hardcodes the attalabs monorepo's own doctrine layout and site paths, a scope-registration decision recorded in `apps/cli/src/checks/registry.ts` — so in this repo it runs only on direct invocation (`vinaya check reader-resolvable-prose`); it is part of neither the managed hooks nor `vinaya-checks.yml`. | `apps/cli/src/checks/bin/check-reader-resolvable-prose.ts` |
|
|
121
|
+
| No new on-disk state | Ever had a file and the forge disagree about the same fact? | ci | Blocks a diff that creates a new on-disk state file duplicating what the forge already derives. | No live per-tranche topology file appears under `aeg-root/tranches/` (a top-level file there fails on add or edit; under `completed/**` only genuinely NEW files are refused, so the legacy archives stay editable), and no new `*.tokens.md` appears anywhere — token figures live in the PR body, and the ledger row is appended post-merge. Status-aware (`git diff --name-status`), never fired by a deletion; a rename counts as an addition at its new path. The registered `no-disk-state` check runs the same `isNewDiskStateFile` predicate at ring 0 (the managed hooks) and here; this row names the standalone CLI shim of that gate. | `packages/aeg-core/bin/check-no-disk-state.ts` |
|
|
117
122
|
|
|
118
123
|
**G-checks rollout (task 3):** G1/G2 ship report-only this tranche — they can only ever print an `info` finding, never fail CI, so pre-existing gaps (four orphan hooks, several orphan `bin/*.ts` CLIs found on first run) surface as visible debt without retroactively failing in-flight work. G3/G4/G5 are blocking from this tranche onward. G1 flips to blocking in a later, separately-dispatched task, once the report-only period has let the orphan backlog get cleaned up.
|
|
119
124
|
|
|
@@ -133,6 +138,7 @@ Red CI is now unmergeable by any actor — the repository ruleset's `required_st
|
|
|
133
138
|
| **Staleness audits** | Ever had documentation flatly contradict a decision everyone agreed to? | event | Flags documentation that has fallen behind the decisions it is meant to follow. | Dispatched periodically | Documentation whose claims contradict recorded decisions; each contradiction becomes a tracked fix Issue | |
|
|
134
139
|
| Direct-main-push detection | Ever had someone push straight to main and nobody noticed for a while? | event | Catches pushes that reached main anyway, including from writers the hooks cannot reach. | Every push to `main`, or manual `workflow_dispatch` (task 31) | Whether the pushed commit is introduced by a merged pull request, via the commits→pulls association API. No associated merged PR → opens an incident Issue (`aeg:direct-main-push` label, idempotent per SHA) and **fails the run loudly** — the one ring-2 check in this table that IS allowed to go red, because nothing it does blocks a pull request or a merge; it only makes an already-irreversible direct push visible. **Detection only, never mutation** — no revert, no force-push; permissions are read-only plus `issues: write`, including the `pull-requests: read` the association API requires — as shipped by task 24 that scope was missing, so the job 403'd (`Resource not accessible by integration`) on every real `main` push instead of detecting anything; fixed by task 31 (item 2). | `packages/aeg-core/bin/check-direct-main-push.ts` |
|
|
135
140
|
| Dead-branch-push audit | Ever kept committing to a branch after its PR had already merged? | event | Catches commits still landing on a branch whose pull request already resolved. | Scheduled (`0 2 * * *`) or manual `workflow_dispatch` | Every remote `task/*` branch whose most recent PR already resolved (`MERGED`/`CLOSED`): flags it when the branch's current tip commit is dated strictly after that resolution — commits kept landing on a branch after the forge considered its work done (the exact "six topology rows landed on a merged plan PR's branch" incident class, 2026-07-03, now caught within a day instead of never). `aeg:dead-branch-push` label plus one idempotent tracking comment on the branch's own PR. **Notification only, never a gate** — `continue-on-error: true`. (2026-07-13: this job previously also ran a stuck row-adjacent-blocker check, `stale-blocker.ts`, retired along with the dispatch-gate predicate it watched for.) | `packages/aeg-core/bin/dead-branch-audit.ts` |
|
|
141
|
+
| Token self-report | Ever seen a cost figure nobody could trace to a source? | event | Reads a role's exact token usage from its own session transcript, feeding the PR-body token report the post-merge ledger row is built from. | By each terminal role at turn-end, before its pull request opens or re-pushes — mandated by the role docs, not hook-fired | Reporting, not enforcement — it refuses no repo action; its refusals guard only its own output. A stale transcript pointer (written by a previous session in a reused worktree) or a transcript yielding zero usage data throws loudly instead of emitting a plausible-looking wrong or zero figure — the misattribution and fabrication classes that would otherwise flow into the PR body and, from there, the post-merge token ledger. | `packages/aeg-core/bin/report-tokens.ts` |
|
|
136
142
|
|
|
137
143
|
---
|
|
138
144
|
|
|
@@ -58,13 +58,13 @@ You are the Brief Author when a task already exists on the forge with a planner'
|
|
|
58
58
|
1. **The task has a real issue.** A task with no forge issue is not briefable. Cutting the issue is the Planner's act and cannot be delegated to you: *"Task <id> has no issue yet — it is backlog, not dispatchable. The Planner cuts it first."*
|
|
59
59
|
2. **Its dispatch gates are clear.** Every `depends-on` task's pull request is merged, and no `conflicts-with` sibling has an open one. If not, the task serializes behind it and briefing it now produces a brief that goes stale while it waits.
|
|
60
60
|
3. **The rationale is complete.** All eight planner fields present. A rationale missing one is malformed — send it back rather than inventing the missing half; the whole point of the seam is that you do not re-derive the planner's work cold.
|
|
61
|
-
4. **You have read the surfaces.** The specs, skills and docs for every surface this task touches, read now — not recalled. The documentation-update list is derived from that reading plus the mechanical derivation against the current bindings
|
|
61
|
+
4. **You have read the surfaces.** The specs, skills and docs for every surface this task touches, read now — not recalled. The documentation-update list is derived from that reading plus the mechanical derivation against the current bindings, and the derivation is a command, not a judgment call: run `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n> --surfaces <glob1,glob2,...>` against the brief's real file surface and paste its output into the brief before writing the list (the same command the Planner runs during Dig; `roles/planner.md` explains the derivation). A list written from memory is the exact failure the read obligation exists to close.
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
65
|
## What you own
|
|
66
66
|
|
|
67
|
-
**The brief.** Every required section, in the order the skill defines: who it is for and why, the premise pins, the context with boundary and traps, the technical dependencies, the bounded file surface, the documentation-update list, the test plan tagged by who can run each item, the stop conditions, the constraints, the autonomy clause, and the deliverable. A brief missing any of them is malformed and you do not dispatch it.
|
|
67
|
+
**The brief.** Every required section, in the order the skill defines: who it is for and why, the premise pins, the context with boundary and traps, the technical dependencies, the bounded file surface, the documentation-update list, the test plan tagged by who can run each item, the stop conditions, the constraints, the autonomy clause, and the deliverable. A brief missing any of them is malformed and you do not dispatch it. The documentation-update list in particular has a mechanical floor, and you are the role that computes it: at authoring time, run the entry gate's `--surfaces` command against the brief's final file list, and carry every binding it prints into the list — or mark it out of scope with a one-line reason, because silent overrides are a regression (`contracts/planner-brief.md`).
|
|
68
68
|
|
|
69
69
|
**The perishable half of the hand-off.** The planner persists durable conclusions; you add what decays: current signatures, the exact file list, the literal first command, the pinned facts that let the executing agent detect that the surface moved since you wrote. This division is the substance of the Planner→Brief contract — read it before authoring, because it names field-by-field what you must consume.
|
|
70
70
|
|
|
@@ -87,7 +87,7 @@ Before planning, you MUST verify every one of these and explicitly confirm them
|
|
|
87
87
|
3. **The code you must dig into is readable.** Sizing requires reading the actual code (call sites, shared packages, schemas — see the deep-dig section). Confirm you can actually access every relevant path. If a task would touch a repo/package/service you cannot read, STOP: *"I can't size the X task — I can't read `<path/repo>`. Sizing blind is forbidden; give me access or the relevant code."*
|
|
88
88
|
4. **The shared substrate is inspectable.** If the work plausibly touches a shared package, you can read that package AND enumerate its consumers (to compute the blast radius). If you can't enumerate consumers, STOP — you cannot correctly set `Project(s)` without it.
|
|
89
89
|
5. **The relevant prior decisions are known.** You've read the specs and skills that bear on this work, so you don't plan a task that re-litigates a settled call. A past choice's reasoning lives in the spec it governs and in the pull request that made it. If you cannot reach them, STOP.
|
|
90
|
-
6. **The registry resolves every project you'll assign** (`.vinaya/projects.md`) — see the entry gate.
|
|
90
|
+
6. **The registry resolves every project you'll assign** (`.vinaya/projects.md`) — see the entry gate. "Resolves" is a claim about a file, so back it with the file: for each `Project:` name you will declare, quote that name's exact registry row text in your readiness check. A bare "all projects resolve" with no quoted rows is a self-attestation, not a check — the unregistered name it hides surfaces later, at Issue creation (`checkProjectsRegistered`), after the whole plan is already built on it.
|
|
91
91
|
7. **Open ambiguities are surfaced, not assumed.** If, after the above, real decisions remain unmade (which DB owns this? is structured output required on all vendors?), collect them and put them to the Principal BEFORE planning — do not pick an answer and plan on top of a guess. A plan built on an unstated assumption is a plan that ships the wrong thing.
|
|
92
92
|
8. **Previous tranches on each in-scope product are archived.** For every product in scope, confirm the previous tranche on that product is in `aeg-root/tranches/completed/`. If any prior tranche on an in-scope product exists in `aeg-root/tranches/` but NOT in `completed/`, the Tranche Archivist has not run — STOP: *"The previous tranche `<name>` on `<product>` has not been archived. Dispatch the Tranche Archivist for it before planning proceeds."* The contract governing this gate is `aeg-root/contracts/tranche-archivist-planner.md`. **Supersession carve-out:** this gate does NOT apply to a prior tranche that *this* plan is superseding — i.e. absorbing `todo`/backlog tasks from. For that one source tranche you refactor it in-place during this plan (see "Tranche refactor & cross-tranche task-movement" below) and the Tranche Archivist archives it *after* the plan lands; the order is refactor-and-plan → then archive, never archive-then-plan. The gate still fully applies to every *unrelated* prior tranche.
|
|
93
93
|
|
|
@@ -149,6 +149,8 @@ Decide by **verification-coupling** (not by project boundaries):
|
|
|
149
149
|
- The reason is the Reviewer: the `Project(s)` list is what tells the Reviewer whose behavior to verify. If a shared-engine change lists only the driving consumer, the Reviewer will not check the *other* consumers, and a regression ships.
|
|
150
150
|
- In the rationale, state explicitly: which shared package changes, which consumers are therefore in the blast radius, and whether each consumer is expected to need **re-verification only** (the change is additive — new code paths that existing consumers don't hit) or **actual edits** (the change alters a shared contract the consumer depends on). Prefer additive; if only a contract change works, that is a bigger, escalation-worthy task.
|
|
151
151
|
|
|
152
|
+
**Enumerate consumers by command, not by memory.** Before finalizing a task's **Project(s) + blast radius**, run the consumer enumeration for every shared package the task's Boundary or Project(s) names, and paste its output into your planning pass — in this repo: `git grep -l '@atta/<pkg>' -- '*/package.json'` (substitute your repo's package scope and manifest layout). The pasted list is the floor for the consumer set. A consumer list written from memory is a self-attestation, and the forgotten consumer is exactly the shipped regression this rule exists to stop. (One hit is the package's own manifest — its `name` field matches; every other hit is a consumer.)
|
|
153
|
+
|
|
152
154
|
**Mechanized.** This rule is no longer prose you have to remember. `checkBlastRadiusScope` reads the collision-domain list in `.aeg/packages` and refuses a task Issue whose **Boundary** or **Project(s) + blast radius** names a path under a domain none of its declared projects owns (ownership resolves against `.vinaya/projects.md` — a task on `Project: aeg-core` editing `packages/aeg-core` owns its surface and passes). Satisfy it by listing the consumers, which is what this rule asks for anyway; or, when one review lens genuinely suffices, by an explicit `blast-radius-ack: <why>` line, which makes the judgment reviewable instead of silent.
|
|
153
155
|
|
|
154
156
|
**Only registered names count, and only the line-anchored project field is read.** The declared set comes from `projectsFromBody` — the same parser that derives a task's project for the board and dispatch — so the gate cannot disagree with the derivation about what a task declares, and a file path written in the surrounding prose can no longer be mistaken for a project. A listed consumer with no row in `.vinaya/projects.md` buys nothing: it resolves to no specs, no state and no reviewer, so it adds no review lens and does not satisfy the rule above (it is separately refused by `checkProjectsRegistered`). Listing a name you have not registered is not a way past this gate — register the project first.
|
|
@@ -179,11 +181,11 @@ This rationale is the **producer side of the `aeg-root/contracts/planner-brief.m
|
|
|
179
181
|
- **Boundary** — what this task is and, crucially, what it is *not* (what was deliberately split out).
|
|
180
182
|
- **Sizing** — that it passed the four "too big?" tests (or how a larger candidate was split).
|
|
181
183
|
- **Project(s) + blast radius** — every project touched, and for shared-package changes, which consumers are in the blast radius and whether each needs re-verification or edits.
|
|
182
|
-
- **Dependency rationale** — *why* each `depends-on` / `conflicts-with` edge exists (not just that it does). **Write it bold-inline only, exactly `**Dependency rationale** — …`** — no colon inside the bold, no heading form. This one field has a downstream consumer beyond the creation gate (`amendRationaleDeps`, the only sanctioned way to edit these edges after creation) that locates it by that exact anchor; see `aeg-root/contracts/planner-brief.md`'s "Rationale grammar" section for why.
|
|
184
|
+
- **Dependency rationale** — *why* each `depends-on` / `conflicts-with` edge exists (not just that it does). For a `conflicts-with` edge, "why" means the overlap itself: name the specific file(s) or path both tasks touch — on both sides of the edge, so either task's reader can see what it collides with. An edge with no nameable overlap is not a valid edge: drop it, or, if you suspect a coupling you cannot yet name, raise it as a calibrated warning ("Possible undeclared cross-package coupling" below) instead of hard-serializing on a hunch. **Write it bold-inline only, exactly `**Dependency rationale** — …`** — no colon inside the bold, no heading form. This one field has a downstream consumer beyond the creation gate (`amendRationaleDeps`, the only sanctioned way to edit these edges after creation) that locates it by that exact anchor; see `aeg-root/contracts/planner-brief.md`'s "Rationale grammar" section for why.
|
|
183
185
|
- **Traps to avoid** — concrete pitfalls the dig surfaced that would otherwise bite the executing agent (e.g. "do NOT use `loadYamlFromCatalog` — it hardcodes another project's directory; use `loadFlow(readFileSync(...))`"). This single field is often the highest-value thing the planner produces.
|
|
184
186
|
- **Suggested agent-class** — high / mid / fast capability, with a one-line reason (this is plan-time; the Brief Author confirms the final model pick at dispatch — see below).
|
|
185
187
|
- **Stop-and-escalate** — the conditions under which the executing agent must stop and escalate rather than improvise (e.g. "if making it work requires changing the shared contract, escalate `severity:strategy`").
|
|
186
|
-
- **Docs to keep coherent** — which specs/skills/docs this task will make incoherent and therefore must update. Derived from the read you did at the readiness gate. Conditional: if this task touches no documented surface, state that explicitly — "No docs touched." This field is the Planner's input to the Brief Author's documentation-update list; leaving it out forces the Brief Author to re-derive it cold or populate that list from memory. **When possible, derive this field mechanically:** take the task's intended surface globs, match them against `.vinaya/doc-owners` bindings (segment-wise glob overlap, not exact-string match — a task surface of `packages/ui/topbar/**` must match a binding on the same or an overlapping glob), and the union of matched pointers is the floor for this field. You may still add docs the derivation misses (cross-cutting judgment) or mark a derived pointer as "not in scope" — but every such override carries a one-line reason. Silent overrides are a regression. The actual derivation runs at brief-authoring time against the live manifest (`packages/aeg-core`'s `deriveSection7`); here you name the intended surfaces, not resolved doc pointers. **Run it, don't just cite it
|
|
188
|
+
- **Docs to keep coherent** — which specs/skills/docs this task will make incoherent and therefore must update. Derived from the read you did at the readiness gate. Conditional: if this task touches no documented surface, state that explicitly — "No docs touched." This field is the Planner's input to the Brief Author's documentation-update list; leaving it out forces the Brief Author to re-derive it cold or populate that list from memory. **When possible, derive this field mechanically:** take the task's intended surface globs, match them against `.vinaya/doc-owners` bindings (segment-wise glob overlap, not exact-string match — a task surface of `packages/ui/topbar/**` must match a binding on the same or an overlapping glob), and the union of matched pointers is the floor for this field. You may still add docs the derivation misses (cross-cutting judgment) or mark a derived pointer as "not in scope" — but every such override carries a one-line reason. Silent overrides are a regression. The actual derivation runs at brief-authoring time against the live manifest (`packages/aeg-core`'s `deriveSection7`); here you name the intended surfaces, not resolved doc pointers. **Run it, don't just cite it** — the derivation is a hard gate, not prose: the exact command, when to run it, and the refuse condition live under "Plan-integrity gates" below ("A `Docs to keep coherent` field written without the derivation run"). Fold every pointer it prints into this field.
|
|
187
189
|
|
|
188
190
|
The Brief Author **starts from** this rationale and adds only the just-in-time perishable detail. The rationale is the planner's thinking, carried forward — not re-thought.
|
|
189
191
|
|
|
@@ -193,6 +195,14 @@ You suggest the **agent-class** (high/mid/fast) as part of sizing — "is this t
|
|
|
193
195
|
|
|
194
196
|
---
|
|
195
197
|
|
|
198
|
+
## Boundary self-lint — run it before presenting the topology
|
|
199
|
+
|
|
200
|
+
Before presenting the topology to the Principal, re-read every task's **Boundary** field hunting hedge language — "or", "if present", "maybe", "possibly", "as needed". Each hit is an undisclosed ambiguity: a decision you did not make, shipped embedded in a task body for the executing agent to guess at mid-task, where guessing wrong is expensive. Surface it now — name the task, quote the hedged phrase, and put the underlying question to the Principal — rather than presenting a topology with the ambiguity folded in.
|
|
201
|
+
|
|
202
|
+
Two calibrations keep this lint honest. A deliberate alternative is not a hedge when the decider and the criterion are named — "A or B, chosen by the Brief Author at dispatch against X" is a decision; a bare "A or B" is not. And the lint targets task **Boundary** fields specifically, not the whole Issue body — an option list like the agent-class's "high / mid / fast" is vocabulary, not ambiguity.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
196
206
|
## Plan-integrity gates
|
|
197
207
|
|
|
198
208
|
These encode failure modes an external review panel flagged. They are split into **hard gates** (refuse — there is a checkable signal) and **calibrated warnings** (flag and ask — judgment, not certainty). Calibration matters: warn only when you can point to a *specific* reason. Flagging every parallel pair trains the human to ignore you, which is its own failure.
|
|
@@ -213,6 +223,7 @@ These encode failure modes an external review panel flagged. They are split into
|
|
|
213
223
|
- **A "conflict scanner."** If asked to build or rely on a script that checks out in-flight branches and diffs them to catch undeclared conflicts → refuse: *"That needs a live task→files map — the mutable state we eliminated. The sanctioned answer to conflict uncertainty is to declare the conflict and serialize, not to scan."*
|
|
214
224
|
- **Unregistered project** or a `Project:` that doesn't resolve against `projects.md` → refuse (see entry gate). Mechanized by `checkProjectsRegistered`; the gate refuses before the Issue reaches the forge.
|
|
215
225
|
- **Dispatch against an unmet gate** — if asked to mark a task ready while its `depends-on` isn't merged, or while a `conflicts-with` sibling's PR is open → refuse: *"Gate not satisfied — this serializes behind <task>."*
|
|
226
|
+
- **A "Docs to keep coherent" field written without the derivation run.** Before emitting any task's **Docs to keep coherent** field, run `bun packages/aeg-core/bin/verify-dispatch.ts <tranche> <n> --surfaces <glob1,glob2,...>` against the task's real intended surface globs, during Dig, and paste what it printed — the fired bindings, or the explicit zero-match line. If asked to emit the field from a content grep or from memory → refuse: *"Doc derivation not run — a content grep says nothing about whether a blanket path-glob binding (e.g. an entire app's `apps/<app>/**` bound to its `CLAUDE.md`) will fire at PR-open (C5). Running `--surfaces` against the task's intended surfaces now."* Before this command existed, `deriveSection7` had no CLI entry point, so this obligation was routinely satisfied by content grep alone — the two are not the same check. The field definition above ("Docs to keep coherent") holds the why; this gate holds the command and the paste.
|
|
216
227
|
- **A rationale field proposing a new committed file as a task deliverable for a one-off finding/report/audit.** If a task's "Docs to keep coherent" field (or any other rationale field) names a *new* file under `aeg-root/` or a product's `aeg-project/` as the destination for a one-off finding, report, coverage summary, or working brief → refuse and correct: *"That's a one-off deliverable — its home is the PR body (task-scoped) or an Issue/PR comment (not task-scoped), never a new repo file. I'm naming the PR body/Issue comment as the destination instead."* This is not optional or a style preference — it is the same weight as the other hard gates in this list. If you are genuinely unsure whether a proposed deliverable is durable reference content (legitimately a new file — the read-obligation test: something future tasks will need to *read*, not just a record of what happened once) or a one-off report (forbidden as a file), that ambiguity is itself a refuse-and-ask condition — put it to the Principal rather than guessing. This gate exists because one task's own Planner rationale ("Docs to keep coherent") is what first named the violating file path that later broke AEG Studio's tranche loader — "there is no prior convention, you set it" is not license to invent a committed-scratch-file convention.
|
|
217
228
|
|
|
218
229
|
### Calibrated warnings — flag and ask (only with a concrete signal)
|
|
@@ -71,11 +71,11 @@ This is why the review is a separate pass and not something the Developer does t
|
|
|
71
71
|
|
|
72
72
|
1. **Does the code match the brief?** Read the brief **in the PR body**. Does the diff implement what was asked — no more, no less?
|
|
73
73
|
2. **Does the code match the project's spec?** When the brief names a `Project:` (resolved via `projects.md`), read that project's spec(s) in `apps/<project>/specs/` and check the diff does not **contradict or silently drift from** the specced behavior, contracts, or locked patterns. The brief says what *this task* intended; the spec says what the *project* is. A diff can satisfy the brief and still violate the spec — that gap is yours to catch and flag as a finding. (This is brief-conformance *and* spec-conformance.) Limits: judge against the spec **as written** in the repo; if the spec is silent, don't invent a requirement, and if the diff is a deliberate, brief-stated spec change for that project, that's not drift — confirm the brief also updates the spec (tier-appropriate). Multi-valued `Project:` → check each named project's spec.
|
|
74
|
-
3. **Scope violations.** Did the PR touch files outside the brief's stated scope? Flag every out-of-scope change. "While I was here" cleanups are scope creep — flag them.
|
|
74
|
+
3. **Scope violations.** Did the PR touch files outside the brief's stated scope? Flag every out-of-scope change. "While I was here" cleanups are scope creep — flag them. **Run the check before you write the verdict line:** `git diff origin/main...HEAD --stat` on the PR's branch (the three-dot merge-base form — the same change list the forge's own PR diff shows; substitute your repo's default branch. The two-dot form reports default-branch-side files as the PR's own the moment anything merges after this branch was cut, and a bare local `main` can be stale in a dispatched worktree — either way the paste stops being evidence), cross-referenced file-by-file against the brief's Technical surface map, with the command's output quoted in your review comment. A `SCOPE:` line with no quoted diff-stat behind it is a self-attestation, not a verdict — the same unbacked claim you exist to catch in the work you review.
|
|
75
75
|
4. **Honest tests.** Do the tests prove real behavior, or do they mock the thing under test? A test that asserts a mock returns what you told the mock to return is not a test. Flag it.
|
|
76
76
|
5. **Spot-check code quality** on 2-3 of the most substantive files: clarity, obvious bugs, error handling, dead code, accidental debug/log leftovers, traces of skipped verification hooks.
|
|
77
77
|
6. **Doc coupling.** Tier 1+ work should carry spec/skill updates. If code changed contracts but no docs moved, flag it. (`verify-docs` also gates this in CI — your job is the judgment CI cannot make: are the docs *correct*, not just *present*.) For every doc named in the brief's documentation-update list: if it is absent from the diff, that is a **BLOCKER** (the list is a definition-of-done obligation, not guidance); if it is present but incorrect, that is also a BLOCKER. Check that compliance before reviewing logic. **Coverage of the `.vinaya/doc-owners` bindings is mechanical (`verify-docs` C5).** You no longer carry the "did the right doc move?" cognitive load — CI does. Your job shrinks to **judging correctness of the covered doc**: did the update actually reflect the code change, or is it a no-op edit / a misleading rewrite that silences C5 without reflecting reality? A passing C5 plus an incorrect doc update is a **BLOCKER**. A doc-coverage waiver is no longer a mechanism you weigh: the waiver body-grammar was removed, so a `doc-owners` obligation is deferred only when a principal applies the actor-verified `vinaya/waiver:docs` label — a Developer cannot self-serve it, and there is no body field for you to judge. What is still yours, because no CI gate can check it: whether a published doc reads complete to a stranger who lands on it cold. Hold every doc the brief surfaces to the reader-readability rule — a reader must resolve every symbol on the page from the page itself. A doc update that satisfies C5 mechanically but leaves a sentence leaning on a decision id or bare section number a stranger can't resolve is a MAJOR finding.
|
|
78
|
-
7. **Multi-project reach.** If the PR's brief lists more than one `Project:`, review through each project's lens — the change's blast radius spans all of them. Confirm a shared-package change (e.g. a shared `core`/`engine` package) doesn't silently break a consumer the brief didn't mention.
|
|
78
|
+
7. **Multi-project reach.** If the PR's brief lists more than one `Project:`, review through each project's lens — the change's blast radius spans all of them. Confirm a shared-package change (e.g. a shared `core`/`engine` package) doesn't silently break a consumer the brief didn't mention. **Before asserting blast-radius coverage is complete** — required whenever the brief lists more than one `Project:`, or the diff touches a path under a shared collision domain (`.aeg/packages`) even on a single-project brief; the collision-domain half is dormant when `.aeg/packages` is absent, as in `contracts/planner-brief.md` — run the consumer check for each touched shared package and quote its output in your review comment: `git grep -l '@atta/<pkg>' -- 'package.json' '*/package.json'` (this repo's form — substitute the touched package's published name; the two pathspecs are both needed, since `'*/package.json'` alone skips a repo-root manifest; use `git grep`, not `rg`, which silently skips gitignored doc trees). A consumer list you never generated is a consumer list you guessed. This applies to these two verdict fields only — the other checks keep their existing shape; evidence-on-everything is the "flag everything, get ignored" failure in another costume.
|
|
79
79
|
8. **Register and slop, in any reader-facing prose the diff adds or edits.** Two other defects in this same family — an unresolvable citation, and a coined term used without a definition — are checked mechanically now, not by you. This one still isn't, and it is a checkable property, not a stylistic preference: does a sentence narrate the work episode instead of stating the durable fact ("this fixes the bug from the last review," "I checked every case," a first-person aside) where a stranger reading the page later has no session to place it in? Does a padding adjective ("robust," "seamless," "comprehensive") carry no concrete referent a reader could verify? Flag the sentence and say what's wrong with it, the same way you'd flag a bug — this is not the taste-based-rewrite exclusion below; it fires only on a nameable defect (narration, unearned padding), never on a phrasing you'd merely have chosen differently.
|
|
80
80
|
|
|
81
81
|
## What you do NOT do
|
|
@@ -85,6 +85,7 @@ This is why the review is a separate pass and not something the Developer does t
|
|
|
85
85
|
- **You do not write status.** Your verdict is the signal; the PR's review decision (which your verdict sets) is what the forge reflects as `changes-requested` or clears. You don't touch any status field or the tranche file.
|
|
86
86
|
- **You do not expand scope** or request improvements unrelated to correctness/safety/brief-conformance/spec-conformance. Taste-based rewrites are not review feedback.
|
|
87
87
|
- **You do not approve to be agreeable.** A clean "REQUEST CHANGES" with three specific items is more valuable than a vague approval.
|
|
88
|
+
- **You do not write `SCOPE: clean` or assert blast-radius coverage without having run the check.** A verdict field written from reading the diff and finding it plausible is indistinguishable from one written after running the command — until it is wrong. The diff-stat (check 3) and the consumer grep (check 7) exist to be run and quoted, not believed.
|
|
88
89
|
- **You write nothing to disk — your verdict is PR comments only.** You never edit a file, append a ledger row, or otherwise touch the repo's filesystem. Everything you produce lands as a PR comment or review verdict.
|
|
89
90
|
- **If dispatched as an agent, you run in an isolated worktree, never the main checkout.** A dispatched Reviewer session never operates against the shared local checkout — a review that has no code to change has no reason to touch `main`'s working tree at all.
|
|
90
91
|
|
|
@@ -113,6 +114,8 @@ DOCS: [tier-appropriate | missing items listed in findings]
|
|
|
113
114
|
- **MAJOR** — should fix before merge (likely bug, weak error handling, **spec drift that isn't an outright contradiction**).
|
|
114
115
|
- **MINOR** — note it; Developer's discretion.
|
|
115
116
|
|
|
117
|
+
The `SCOPE:` line, and any blast-radius assertion under check 7, are evidence-backed claims: each may be written only after its named check has run — `git diff origin/main...HEAD --stat` for scope, the consumer grep for multi-project reach — with the output quoted in the same review comment the verdict lands in: a fenced block directly below the verdict block, so evidence sits in one predictable place. The other verdict lines carry no such requirement.
|
|
118
|
+
|
|
116
119
|
If you have only MINOR findings, VERDICT is APPROVE. Any BLOCKER → REQUEST CHANGES. (A REQUEST CHANGES sets the PR's review decision to `CHANGES_REQUESTED`, which is the derived `changes-requested` status — no one writes it down.)
|
|
117
120
|
|
|
118
121
|
## Escalation
|
|
@@ -62,7 +62,7 @@ Read the brief from the PR body first — it tells you what the change is *suppo
|
|
|
62
62
|
|
|
63
63
|
## What you check
|
|
64
64
|
|
|
65
|
-
1. **Secret / credential leakage.** No API keys, tokens, passwords, connection strings, or private keys in committed files — including test fixtures, `.env` examples with real values, and inline comments. Run
|
|
65
|
+
1. **Secret / credential leakage.** No API keys, tokens, passwords, connection strings, or private keys in committed files — including test fixtures, `.env` examples with real values, and inline comments. Run a real secret scanner over the branch's full commit range — in addition to your own read of the diff, never instead of it — and paste its actual output (the scan summary and any findings, secrets redacted) into your verdict comment: without that paste, the `SECRETS:` line may not be written. As with the config scanner below, the scanner's output is input to your judgment, never the verdict — a ruleset is shape-aware, so a plaintext password or an off-shape credential can ride through a clean scan that your read of the diff must still catch. Flag anything that looks like a live credential. If the scanner is unavailable in your environment, write exactly that in the verdict in place of the `SECRETS:` claim and route the gap to the Principal — never write `SECRETS: none found` unscanned. *(In this repo the scanner is gitleaks — `gitleaks git --redact -v --log-opts "origin/main..HEAD"`, run from the PR branch's worktree; install via `brew install gitleaks` or a release binary from github.com/gitleaks/gitleaks. It is plan-independent — no GitHub feature has to be enabled — `--redact` keeps the pasted output from becoming the second leak, and the range form catches a secret committed and then removed in a later commit, which a tip-diff read misses.)*
|
|
66
66
|
2. **BYOK / crypto handling.** Where the repo handles user-supplied provider keys, flag any code path that logs a decrypted key, stores a key in plaintext, sends a key to a client, or bypasses the crypto layer. *(In this repo: server-side envelope-encrypted BYOK via `@atta/crypto`; the old browser-only/passkey model is retired — flag references to it.)*
|
|
67
67
|
3. **Auth / permissions.** Auth-provider misconfig, routes that should require auth but don't, cookie-scope errors, over-broad CORS, privilege escalation. *(Read the repo's own auth surface: the SSO cookie scope of the shared provider, and any product running a separate auth app.)*
|
|
68
68
|
4. **MCP / agent tooling exposure.** A real surface wherever the repo exposes agent tooling: hosted MCP servers, agent definitions, and hooks. Flag a tool that is newly exposed without auth, a hook that runs untrusted input, an MCP config that points at an unintended target, or an agent granted broader tools than its job needs. *(In this repo: the hosted Vāda MCP and the `.claude/` agent/skill/hook configs.)*
|
|
@@ -106,6 +106,8 @@ SECRETS: [none found | listed above, redacted]
|
|
|
106
106
|
|
|
107
107
|
Any CRITICAL or HIGH → VERDICT FAIL. Only MEDIUM/LOW → PASS with notes.
|
|
108
108
|
|
|
109
|
+
The `SECRETS:` line is evidence-backed, not asserted: the secret scanner's pasted output (check 1) must appear in the verdict comment above it — necessary evidence that the scan ran, never sufficient on its own, since the judgment half of check 1 still stands behind the claim. `SECRETS: none found` with no scan output pasted is an unbacked self-attestation — the exact claim this check exists to catch in others' work, not to commit in your own.
|
|
110
|
+
|
|
109
111
|
## Escalation
|
|
110
112
|
|
|
111
113
|
A security finding that implies a product/architecture decision (e.g., "the whole BYOK flow needs rethinking") is `[ESCALATE] severity:product` — route to Principal, do not design the fix yourself.
|
|
@@ -3028,7 +3028,8 @@ var VinayaConfigSchema = z.object({
|
|
|
3028
3028
|
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
3029
3029
|
briefSchema: BriefSchemaSchema.optional(),
|
|
3030
3030
|
managed: ManagedManifestSchema.optional(),
|
|
3031
|
-
principals: z.array(z.string()).min(1).optional()
|
|
3031
|
+
principals: z.array(z.string()).min(1).optional(),
|
|
3032
|
+
ci: z.object({ setup: z.string().min(1) }).optional()
|
|
3032
3033
|
});
|
|
3033
3034
|
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
3034
3035
|
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -3029,7 +3029,8 @@ var VinayaConfigSchema = z.object({
|
|
|
3029
3029
|
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
3030
3030
|
briefSchema: BriefSchemaSchema.optional(),
|
|
3031
3031
|
managed: ManagedManifestSchema.optional(),
|
|
3032
|
-
principals: z.array(z.string()).min(1).optional()
|
|
3032
|
+
principals: z.array(z.string()).min(1).optional(),
|
|
3033
|
+
ci: z.object({ setup: z.string().min(1) }).optional()
|
|
3033
3034
|
});
|
|
3034
3035
|
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
3035
3036
|
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -3027,7 +3027,8 @@ var VinayaConfigSchema = z.object({
|
|
|
3027
3027
|
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
3028
3028
|
briefSchema: BriefSchemaSchema.optional(),
|
|
3029
3029
|
managed: ManagedManifestSchema.optional(),
|
|
3030
|
-
principals: z.array(z.string()).min(1).optional()
|
|
3030
|
+
principals: z.array(z.string()).min(1).optional(),
|
|
3031
|
+
ci: z.object({ setup: z.string().min(1) }).optional()
|
|
3031
3032
|
});
|
|
3032
3033
|
var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
|
|
3033
3034
|
var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
|
package/dist/index.js
CHANGED
|
@@ -950,7 +950,25 @@ function vinayaSetupSteps(selfHost) {
|
|
|
950
950
|
function vinayaRun(selfHost, args) {
|
|
951
951
|
return selfHost ? `node ${selfHost.bin} ${args}` : `npx --yes @attalabs/vinaya ${args}`;
|
|
952
952
|
}
|
|
953
|
-
function
|
|
953
|
+
function adopterSetupStep(ciSetup) {
|
|
954
|
+
if (!ciSetup)
|
|
955
|
+
return "";
|
|
956
|
+
const indented = ciSetup.trimStart().split(`
|
|
957
|
+
`).map((line) => line.length > 0 ? ` ${line}` : line).join(`
|
|
958
|
+
`);
|
|
959
|
+
return ` # Adopter-declared CI setup (vinaya.config.json \`ci.setup\`), emitted
|
|
960
|
+
# verbatim. It prepares this repository's OWN custom checks — scripts
|
|
961
|
+
# committed here that may import this repository's code — which the
|
|
962
|
+
# \`npx\` invocation below cannot do: npx prepares only vinaya itself.
|
|
963
|
+
# The command is repo-committed, reviewed config; under \`pull_request\`
|
|
964
|
+
# the workflow definition is already controlled by the pull request, so
|
|
965
|
+
# this step adds no trust surface that did not already exist.
|
|
966
|
+
- name: Adopter CI setup
|
|
967
|
+
run: |
|
|
968
|
+
${indented}
|
|
969
|
+
`;
|
|
970
|
+
}
|
|
971
|
+
function checksWorkflow(selfHost, ciSetup) {
|
|
954
972
|
return `# ${MANAGED_NOTE}
|
|
955
973
|
#
|
|
956
974
|
# The deterministic gate suite. Runs every registered vinaya check over the
|
|
@@ -1006,7 +1024,7 @@ jobs:
|
|
|
1006
1024
|
- uses: actions/setup-node@v4
|
|
1007
1025
|
with:
|
|
1008
1026
|
node-version: 20
|
|
1009
|
-
${vinayaSetupSteps(selfHost)} - name: Run checks
|
|
1027
|
+
${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Run checks
|
|
1010
1028
|
env:
|
|
1011
1029
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1012
1030
|
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
@@ -1040,7 +1058,7 @@ ${vinayaSetupSteps(selfHost)} - name: Run checks
|
|
|
1040
1058
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
1041
1059
|
`;
|
|
1042
1060
|
}
|
|
1043
|
-
function reviewWorkflow(selfHost) {
|
|
1061
|
+
function reviewWorkflow(selfHost, ciSetup) {
|
|
1044
1062
|
return `# ${MANAGED_NOTE}
|
|
1045
1063
|
#
|
|
1046
1064
|
# The required review gate — pull_request events only. The verdict-comment
|
|
@@ -1101,7 +1119,7 @@ jobs:
|
|
|
1101
1119
|
- uses: actions/setup-node@v4
|
|
1102
1120
|
with:
|
|
1103
1121
|
node-version: 20
|
|
1104
|
-
${vinayaSetupSteps(selfHost)} - name: Review gate
|
|
1122
|
+
${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review gate
|
|
1105
1123
|
env:
|
|
1106
1124
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1107
1125
|
# PR_NUMBER is what makes the review-gate check EVALUATE: without
|
|
@@ -1112,7 +1130,7 @@ ${vinayaSetupSteps(selfHost)} - name: Review gate
|
|
|
1112
1130
|
run: ${vinayaRun(selfHost, "check review-gate")}
|
|
1113
1131
|
`;
|
|
1114
1132
|
}
|
|
1115
|
-
function reviewVerdictWorkflow(selfHost) {
|
|
1133
|
+
function reviewVerdictWorkflow(selfHost, ciSetup) {
|
|
1116
1134
|
return `# ${MANAGED_NOTE}
|
|
1117
1135
|
#
|
|
1118
1136
|
# The verdict-comment half of the review gate. A reviewer's verdict arrives
|
|
@@ -1171,7 +1189,7 @@ jobs:
|
|
|
1171
1189
|
- uses: actions/setup-node@v4
|
|
1172
1190
|
with:
|
|
1173
1191
|
node-version: 20
|
|
1174
|
-
${vinayaSetupSteps(selfHost)} - name: Review gate (verdict evaluation)
|
|
1192
|
+
${vinayaSetupSteps(selfHost)}${adopterSetupStep(ciSetup)} - name: Review gate (verdict evaluation)
|
|
1175
1193
|
env:
|
|
1176
1194
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
1177
1195
|
# Same wiring as the required workflow: PR_NUMBER is what makes
|
|
@@ -1496,19 +1514,19 @@ function buildInitOps(ctx) {
|
|
|
1496
1514
|
ops.push({
|
|
1497
1515
|
kind: "create-file",
|
|
1498
1516
|
path: CHECKS_WORKFLOW_PATH,
|
|
1499
|
-
content: checksWorkflow(ctx.selfHost),
|
|
1517
|
+
content: checksWorkflow(ctx.selfHost, ctx.ciSetup),
|
|
1500
1518
|
group: "CI workflows"
|
|
1501
1519
|
});
|
|
1502
1520
|
ops.push({
|
|
1503
1521
|
kind: "create-file",
|
|
1504
1522
|
path: REVIEW_WORKFLOW_PATH,
|
|
1505
|
-
content: reviewWorkflow(ctx.selfHost),
|
|
1523
|
+
content: reviewWorkflow(ctx.selfHost, ctx.ciSetup),
|
|
1506
1524
|
group: "CI workflows"
|
|
1507
1525
|
});
|
|
1508
1526
|
ops.push({
|
|
1509
1527
|
kind: "create-file",
|
|
1510
1528
|
path: REVIEW_VERDICT_WORKFLOW_PATH,
|
|
1511
|
-
content: reviewVerdictWorkflow(ctx.selfHost),
|
|
1529
|
+
content: reviewVerdictWorkflow(ctx.selfHost, ctx.ciSetup),
|
|
1512
1530
|
group: "CI workflows"
|
|
1513
1531
|
});
|
|
1514
1532
|
ops.push({
|
|
@@ -1714,7 +1732,8 @@ var VinayaConfigSchema = z.object({
|
|
|
1714
1732
|
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
1715
1733
|
briefSchema: BriefSchemaSchema.optional(),
|
|
1716
1734
|
managed: ManagedManifestSchema.optional(),
|
|
1717
|
-
principals: z.array(z.string()).min(1).optional()
|
|
1735
|
+
principals: z.array(z.string()).min(1).optional(),
|
|
1736
|
+
ci: z.object({ setup: z.string().min(1) }).optional()
|
|
1718
1737
|
});
|
|
1719
1738
|
var GLOBAL_VINAYA_HOME = join3(homedir(), ".vinaya");
|
|
1720
1739
|
var GLOBAL_CONFIG_PATH = join3(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -1759,6 +1778,16 @@ function stripGlobalOnlyKeys(config, path) {
|
|
|
1759
1778
|
}
|
|
1760
1779
|
return result;
|
|
1761
1780
|
}
|
|
1781
|
+
function readRepoCiSetup(repoRoot) {
|
|
1782
|
+
const p = join3(repoRoot, "vinaya.config.json");
|
|
1783
|
+
if (!existsSync2(p))
|
|
1784
|
+
return null;
|
|
1785
|
+
try {
|
|
1786
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync2(p, "utf-8"))).ci?.setup ?? null;
|
|
1787
|
+
} catch {
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1762
1791
|
function loadConfigChecked() {
|
|
1763
1792
|
const path = configPath();
|
|
1764
1793
|
if (!path)
|
|
@@ -2862,6 +2891,9 @@ var BIN_EXT = BIN_DIR === DIST_BIN_DIR ? ".js" : ".ts";
|
|
|
2862
2891
|
function bin(name) {
|
|
2863
2892
|
return join6(BIN_DIR, `${name}${BIN_EXT}`);
|
|
2864
2893
|
}
|
|
2894
|
+
function runsUnderAll(spec) {
|
|
2895
|
+
return !spec.ownWorkflow;
|
|
2896
|
+
}
|
|
2865
2897
|
function coreCheckRegistry() {
|
|
2866
2898
|
return [
|
|
2867
2899
|
{
|
|
@@ -3501,7 +3533,7 @@ async function checkCommand(args) {
|
|
|
3501
3533
|
}
|
|
3502
3534
|
const { specs: customSpecs, errorOutcome, checks: configChecks } = customSpecsFromConfig();
|
|
3503
3535
|
const allSpecs = [...coreCheckRegistry(), ...customSpecs];
|
|
3504
|
-
const specsToRun = allRequested ? allSpecs.filter(
|
|
3536
|
+
const specsToRun = allRequested ? allSpecs.filter(runsUnderAll) : allSpecs.filter((s) => s.name === requestedName);
|
|
3505
3537
|
if (!allRequested && specsToRun.length === 0) {
|
|
3506
3538
|
console.error(`Unknown check: ${requestedName}`);
|
|
3507
3539
|
process.exit(2);
|
|
@@ -4146,7 +4178,8 @@ async function runDoctor(args, deps) {
|
|
|
4146
4178
|
owner: repo.owner,
|
|
4147
4179
|
repo: repo.repo,
|
|
4148
4180
|
hookDir,
|
|
4149
|
-
selfHost: detectVendoredVinaya(repo.repoRoot)
|
|
4181
|
+
selfHost: detectVendoredVinaya(repo.repoRoot),
|
|
4182
|
+
ciSetup: readRepoCiSetup(repo.repoRoot)
|
|
4150
4183
|
};
|
|
4151
4184
|
const install = diagnoseInstall(repo.repoRoot, ctx, manifest);
|
|
4152
4185
|
findings.push(...install.findings);
|
|
@@ -4478,7 +4511,8 @@ async function runInit(args, deps) {
|
|
|
4478
4511
|
owner: repo.owner,
|
|
4479
4512
|
repo: repo.repo,
|
|
4480
4513
|
hookDir: deps.hookDirFor(repo.repoRoot),
|
|
4481
|
-
selfHost: detectVendoredVinaya(repo.repoRoot)
|
|
4514
|
+
selfHost: detectVendoredVinaya(repo.repoRoot),
|
|
4515
|
+
ciSetup: readRepoCiSetup(repo.repoRoot)
|
|
4482
4516
|
};
|
|
4483
4517
|
const allOps = buildInitOps(ctx);
|
|
4484
4518
|
const ops = noRemote ? allOps.filter((op) => op.kind !== "create-label") : allOps;
|
|
@@ -5742,7 +5776,8 @@ async function runUpgrade(args, deps) {
|
|
|
5742
5776
|
owner: repo.owner,
|
|
5743
5777
|
repo: repo.repo,
|
|
5744
5778
|
hookDir: routing.target,
|
|
5745
|
-
selfHost: detectVendoredVinaya(repo.repoRoot)
|
|
5779
|
+
selfHost: detectVendoredVinaya(repo.repoRoot),
|
|
5780
|
+
ciSetup: readRepoCiSetup(repo.repoRoot)
|
|
5746
5781
|
};
|
|
5747
5782
|
const ops = buildInitOps(ctx);
|
|
5748
5783
|
const plan = planUpgrade(ops, repo.repoRoot, planManifest, routing);
|
package/package.json
CHANGED