@attalabs/vinaya 0.24.0 → 0.24.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/enforcement.md +1 -1
- package/aeg-root/roles/developer.md +1 -1
- package/dist/checks/bin/check-changeset-coverage.js +12 -0
- package/dist/checks/bin/check-registry-gates.js +1 -1
- package/dist/checks/bin/check-workspace-escape.js +10 -2
- package/dist/index.js +45 -13
- package/package.json +2 -2
package/aeg-root/enforcement.md
CHANGED
|
@@ -144,7 +144,7 @@ Every pull request, on open and on every push, re-runs the same checks in CI:
|
|
|
144
144
|
| doctrine-portability | Ever shipped doctrine that named a file only you could see? | ci | Re-checks that shipped doctrine doesn't cite a path that exists only in the authoring repository. | A cited path's top segment judged against an allow-list: the caller-supplied ships prefix itself (`<doctrineRoot>/`, `aeg-root/` by default — doctrine citing its own tree, wherever that tree actually lives for the caller, checked dynamically so a non-default `doctrineRoot` never falsely flags an adopter's own self-citations), plus a fixed set of static portable prefixes (`roles/`, `contracts/`, `skills/`, and adopter-owned `.github/`, `.vinaya/`, `.claude/`, `.git/`, `.husky/`) — an unclassified prefix is a finding, never a silent pass, so a never-seen author-repo directory (e.g. a build artifact under `apps/*/dist/`) cannot slip through the way it would under a deny-list. Zero I/O in `@attalabs/aeg-core`'s `checkDoctrinePortability`; the bin supplies the only I/O, reading `<doctrineRoot>/**` twice — once from the working tree, once from `BASE_SHA` (`origin/main` by default) via `git show`/`git ls-tree` — and reports only citations present in the former and absent from the latter, so day-one install of an unfixed backlog never turns every adopter's CI red. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-doctrine-portability.ts` |
|
|
145
145
|
| doctrine-no-procedures | Ever pasted a copy-paste runbook into doctrine, and it rotted the moment the real command changed? | ci | Re-checks that doctrine (`roles/*.md`, `contracts/*.md`, …) describes commands rather than scripting them (task 9/task 10's rule). | A fenced code block anywhere under `<doctrineRoot>/**/*.md`, tagged with no language or an explicit `sh`/`bash`/`shell`/`console`, containing two or more lines that each start with a shell command word (`export`/`bun`/`gh`/`git`/`grep`/`sed`/`cat`/`diff`/`vinaya`) is a finding — a runbook, not an illustration. A block tagged with any other language (` ```ts `, ` ```json `, …) is never a candidate at all, however many command-word-shaped lines it contains — a two-line ` ```ts ` excerpt whose lines both start with `export` is TypeScript, not shell. Two further exemptions: a block sitting inside the existing `AEG:VENDOR-EXAMPLE` anchor pair (`tranche-model.md` §12's one sanctioned fenced home for a real command sequence), and any file under a `templates/` directory. Zero I/O in `@attalabs/aeg-core`'s `checkDoctrineNoProcedures`; the bin supplies the only I/O, walking `<doctrineRoot>` once. Unlike its sibling `doctrine-portability` above, this is **blocking, not baseline** (`severity: error`, exit 1 on any finding) — a fresh rule with an expected-zero corpus after this task's own sweep-and-fix, not a 200+-finding backlog needing a report-only rollout. Registered in `coreCheckRegistry()`. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. | product | `apps/cli/src/checks/bin/check-doctrine-no-procedures.ts` |
|
|
146
146
|
| 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. | product | `packages/aeg-core/bin/check-no-disk-state.ts` |
|
|
147
|
-
| workspace-escape | Ever deleted a package only to break a sibling's tests through a path nobody's dependency graph saw? | ci | Re-checks that no source file's constructed filesystem reference reaches outside its own workspace package, or points at a path that does not exist. | Every `readFileSync`/`readFile` call with a literal relative-path argument, and every `new URL('…', import.meta.url)`, across every `.ts`/`.tsx` file under `apps/*`/`packages/*` — resolved against the citing file's own directory and judged against its own two-segment workspace-package prefix (`apps/<name>`, `packages/<name>`), never against `import`/`require` module specifiers (the dependency graph already owns those). Zero I/O in `@attalabs/aeg-core`'s `findWorkspaceEscapes`; the bin supplies the only I/O, walking the working tree once for source content and once for the existence-check universe (files AND directories, so a reference to a directory itself resolves as existing). Registered in `coreCheckRegistry()` (task 17), report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability` — this repo's own corpus
|
|
147
|
+
| workspace-escape | Ever deleted a package only to break a sibling's tests through a path nobody's dependency graph saw? | ci | Re-checks that no source file's constructed filesystem reference reaches outside its own workspace package, or points at a path that does not exist. | Every `readFileSync`/`readFile` call with a literal relative-path argument, and every `new URL('…', import.meta.url)`, across every `.ts`/`.tsx` file under `apps/*`/`packages/*` — resolved against the citing file's own directory and judged against its own two-segment workspace-package prefix (`apps/<name>`, `packages/<name>`), never against `import`/`require` module specifiers (the dependency graph already owns those). Zero I/O in `@attalabs/aeg-core`'s `findWorkspaceEscapes`; the bin supplies the only I/O, walking the working tree once for source content and once for the existence-check universe (files AND directories, so a reference to a directory itself resolves as existing). Registered in `coreCheckRegistry()` (task 17), report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`. `*.test.ts`/`*.test.tsx` files are excluded from the swept surface entirely (O2, found live 2026-09-04) — this repo's own corpus carried a genuine escape at `packages/sources/src/commands-router-coverage.test.ts` reaching into `apps/cli/src/index.ts`, structurally the same incident this check exists to catch, until that exclusion silenced it alongside `workspace-escape.test.ts`'s own fixture-string false positive; report-only severity stays regardless, since an ordinary (non-test) source file's constructed reference can still exist with nobody having triaged it yet. **Line-scoped under `--diff-only`** (this task): a finding prints only when its own line falls inside a changed hunk of that file's diff against the base. A full sweep is unchanged and still reports every finding in the tree. The hunk parser lives in exactly one place — the shared diff-evidence helper both this and every sibling sweep call — never a second regex per check. Without this, a diff that touched one line of a long doctrine page reported that page's whole standing backlog as if this PR had caused it, and a real new finding was indistinguishable from inherited noise. Registered with `include: ['aeg-root/**/*.md']` (task 12) — pinning/documentation only today, since `runner.ts`'s `shouldSkip` returns before consulting `include` for a `scope: 'full'` entry (out of that task's surface); a future `runner.ts` change extending `--diff-only` skipping to `scope: 'full'` finds this already declared. (this sweep's real domain is `apps`/`packages`, not `aeg-root` — named per the Issue's own list; harmless either way while `include` has no runtime effect on a `scope: 'full'` entry.) | product | `apps/cli/src/checks/bin/check-workspace-escape.ts` |
|
|
148
148
|
| changeset-coverage | Ever shipped a change to a published package and the release train never picked it up? | ci | Re-checks that a diff touching a published package's own shipped files also carries a changeset in the same diff. | For each member of `.changeset/config.json`'s `fixed` group, a changed path counts as SHIPPED iff it falls under that member's own `package.json` `files` allowlist, read live from every workspace member's own manifest — never a hardcoded path list, never a directory heuristic. A diff that hits at least one shipped path with no `.changeset/*.md` entry (excluding `README.md`) in the same diff is a finding, naming the shipped paths hit. The Changesets-release branch itself is exempt by construction (its diff IS the changesets being consumed) — the exemption reuses `@attalabs/aeg-core`'s own release-branch constant, the same one `body-bare-digits`'s Changesets-release exemption is keyed on, rather than inventing a second branch-name special-case; unlike that check it never live-fetches a PR author, since a report-only, always-exit-`0` check has no gate for a spoofed branch name to bypass. Written after four separate published-package pull requests, over the course of one day, each merged with no changeset and caught only by a human after the fact — the obligation existed in no enforceable place before this check: not in `roles/developer.md`'s deliverable conventions (now amended alongside this row), not in any registered check. Zero I/O in the pure predicate (`changeset-coverage-logic.ts`); the bin supplies the only I/O — reading the config, every fixed-group member's own manifest, and the diff. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape` above — graduating to a blocking check, and any waiver-label escape that would need, is a later, separately-dispatched decision once the false-positive rate is observed; v1 carries no such escape. | product | `apps/cli/src/checks/bin/check-changeset-coverage.ts` |
|
|
149
149
|
| quoted-command | Ever read a doc that quoted a command as "what runs today", and the command changed underneath it? | ci | Re-checks that a doc's explicitly marked quote of a command or config line still matches, verbatim, the file it names as its source. | Marker-based only, by explicit Principal decision, not inferred from command-looking spans: a doc opts a span in with `<!-- AEG:QUOTES-FILE:START:<path> -->…<!-- AEG:QUOTES-FILE:END -->` (the same invisible-on-render, code-blind HTML-comment idiom `anchored-region.ts` uses for its own seven PR/Issue-body fields, reusing that file's masking primitive rather than a second parser — but a distinct marker kind, since the cited path is free text, not one of that file's seven closed field names). An adopter-facing command in a README with no marker is instruction, not a claim, and is never flagged — inference over command-looking spans was explicitly rejected as the false-positive shape that gets a gate disabled. `findCitedQuotes` sweeps the identical `ships`/`reader-facing` corpus `reader-resolvable-prose` sweeps (never a second notion of "governed doc"); `evaluateCitedQuotes` then checks each marked span against its cited file's live content, wherever that file lives in the repo, naming both sides in a finding — what the doc claims, and which file no longer contains it verbatim. Zero I/O in `@attalabs/aeg-core`'s pure predicate (`quoted-command.ts`), corpus-tested against the real `aeg-root/**` tree; the bin supplies the only I/O. Ships with one real, live annotation (this very paragraph's own sibling callout above, quoting the CI invocation this page names) rather than only synthetic fixtures. Written after a measured incident: a CI invocation was pinned to a version in one pull request; this page quoted the pre-pin form verbatim as current fact; every registered check passed, `doctor` reported healthy, security passed, and a code-review agent found the drift only after independent re-derivation. Registered in `coreCheckRegistry()`, report-only like `reader-resolvable-prose`/`retired-vocabulary`/`doctrine-portability`/`workspace-escape`/`changeset-coverage` above — graduating to a blocking check is a later, separately-dispatched decision once the false-positive rate is observed against real adopter corpora; v1 carries no waiver escape. | product | `apps/cli/src/checks/bin/check-quoted-command.ts` |
|
|
150
150
|
| token-report | Ever seen a cost figure nobody could trace to a source? | ci | Re-checks that a PR's "Token report" section actually carries real numeric Tokens in/out figures, on any host this repo can prove is metering-capable. | The presence and shape of the `## Token report` block `roles/developer.md` requires every self-metering role to paste at turn-end — never whether the reported figures are TRUE, which CI structurally cannot recompute (the same bounded honesty this page's `evidence-fresh` row states for its own Group B). Runs `resolveMeteringCapability` (task 1's probe, `@attalabs/aeg-core`) fresh in THIS process; an incapable verdict (any reason) passes silently — the sanctioned operator-metered case, same discipline `token-collection-wired` above already applies. **Ring derived mechanically as `1`, not chosen** (a 2026-08-29 amendment correcting the original brief rationale's now-retracted ring-0/1 instruction): `requiresOpenPr: true` makes this check CI-only by `CoreCheckRing`'s own derivation rule, since the PR body it reads does not exist before a PR does. A probe that itself fails to run (an unexpected `exists`/`readFile` throw the probe does not catch) is never read as a clean incapable verdict: the bin lets it propagate uncaught, which the runner surfaces as `status: 'error'`, distinct from the `status: 'pass'` a real incapable verdict produces. | product | `apps/cli/src/checks/bin/check-token-report.ts` |
|
|
@@ -103,7 +103,7 @@ Items 3, 5, and 7 read live forge state. Item 6 checks the brief's own Step 0 te
|
|
|
103
103
|
|
|
104
104
|
**Worktree discipline.** Your brief's first pre-flight step (Step 0) is creating a worktree — do it before anything else. If dispatched by an automation layer, you work in the worktree it created at `.worktrees/task/<tranche>/<n>/`. If working manually, the brief's Step 0 gives you the `git worktree add … origin/main` command — run it and `cd` in. Never branch from a local checkout that may be behind.
|
|
105
105
|
|
|
106
|
-
**Commit per Part, push once.** Commit after each numbered Part in the brief — small, one logical change per commit, so the history reads as a narrative of how you approached the problem. Push exactly once, immediately before `pr create` — not after every commit.
|
|
106
|
+
**Commit per Part, push once.** Commit after each numbered Part in the brief — small, one logical change per commit, so the history reads as a narrative of how you approached the problem. Push exactly once, immediately before `pr create` — not after every commit. The pre-push hook itself runs the affected test suite (`bunx turbo test --affected`) on that one push and refuses it on failure; you do not additionally run it yourself per Part. A regeneration or evidence run (`vinaya pr report --write`/`--push`) passes `--force` to its own test invocation. A verdict binds to the head it judged; a push landing after the newest verdict's judged head voids it and is named as such: `vinaya review status` prints `push after verdict — re-review required`, and merge waits on a fresh review round.
|
|
107
107
|
|
|
108
108
|
**Opening the PR with a complete description.** The PR description must (1) **carry the report only** — `pr create` posts the brief as its own comment marked `aeg:brief`, once; that comment is the brief's permanent, durable home, and the Reviewer and Archivist read it there; (2) follow the canonical form in [§ PR body — canonical form](#pr-body--canonical-form) below — that section holds the verbatim copy-pasteable template, including the **exact `Tier:` field syntax** the `verify-docs` gate requires; (3) reference the task's Issue (`Closes #N`) so the merge auto-closes it. The description is not optional — the reviews depend on it. Opening the PR is itself the `in-flight → in-review` transition; you write no status field. **The body is authored once, at open.** After the PR is open, you never hand-edit it again — not to append a response to a review round, not to record a decision, not for any reason. Two writes are sanctioned after open, both machine-regenerated, never typed: the Evidence block, and one appended row in the Token report for a re-entry turn (see [§ Evidence is emitted, never typed](#evidence-is-emitted-never-typed)). Everything else a review round produces — your response to findings, re-run `[agent]` evidence, any disclosure the brief didn't anticipate — is a PR comment.
|
|
109
109
|
|
|
@@ -4948,6 +4948,13 @@ function git(args) {
|
|
|
4948
4948
|
return "";
|
|
4949
4949
|
}
|
|
4950
4950
|
}
|
|
4951
|
+
function currentBranch() {
|
|
4952
|
+
return git(["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
4953
|
+
}
|
|
4954
|
+
function defaultBranch() {
|
|
4955
|
+
const ref = git(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
|
|
4956
|
+
return ref.startsWith("origin/") ? ref.slice("origin/".length) : "";
|
|
4957
|
+
}
|
|
4951
4958
|
function toPosix(p) {
|
|
4952
4959
|
return p.split("\\").join("/");
|
|
4953
4960
|
}
|
|
@@ -5004,6 +5011,11 @@ function fixedGroupNames(root) {
|
|
|
5004
5011
|
}
|
|
5005
5012
|
function main() {
|
|
5006
5013
|
const root = repoRoot() ?? process.cwd();
|
|
5014
|
+
const current = currentBranch();
|
|
5015
|
+
const base = defaultBranch();
|
|
5016
|
+
if (current !== "" && current === base) {
|
|
5017
|
+
process.exit(0);
|
|
5018
|
+
}
|
|
5007
5019
|
const changedAbs = resolveChangedFiles();
|
|
5008
5020
|
if (changedAbs === null) {
|
|
5009
5021
|
emitCheckError({
|
|
@@ -4877,6 +4877,9 @@ var CHECK_NAME = "workspace-escape";
|
|
|
4877
4877
|
var WORKSPACE_DIRS = ["apps", "packages"];
|
|
4878
4878
|
var SOURCE_EXTENSIONS = [".ts", ".tsx"];
|
|
4879
4879
|
var EXCLUDED_DIRS = new Set(["node_modules", "dist", ".turbo", ".next", ".git"]);
|
|
4880
|
+
function isTestFile(path) {
|
|
4881
|
+
return /\.test\.tsx?$/.test(path);
|
|
4882
|
+
}
|
|
4880
4883
|
function collectAllPaths(dir, out = []) {
|
|
4881
4884
|
let entries;
|
|
4882
4885
|
try {
|
|
@@ -4903,7 +4906,7 @@ function collectAllPaths(dir, out = []) {
|
|
|
4903
4906
|
function main() {
|
|
4904
4907
|
const allPaths = WORKSPACE_DIRS.flatMap((d) => collectAllPaths(d)).map((p) => p.split("\\").join("/"));
|
|
4905
4908
|
const knownPaths = new Set(allPaths);
|
|
4906
|
-
const sourceFiles = allPaths.filter((p) => SOURCE_EXTENSIONS.some((ext) => p.endsWith(ext))).map((p) => ({ path: p, content: readFileSync3(p, "utf8") }));
|
|
4909
|
+
const sourceFiles = allPaths.filter((p) => SOURCE_EXTENSIONS.some((ext) => p.endsWith(ext))).filter((p) => !isTestFile(p)).map((p) => ({ path: p, content: readFileSync3(p, "utf8") }));
|
|
4907
4910
|
const findings = findWorkspaceEscapes(sourceFiles, knownPaths, WORKSPACE_DIRS);
|
|
4908
4911
|
const reportable = findingsInThisDiff(findings);
|
|
4909
4912
|
console.log(`${CHECK_NAME}: ${sourceFiles.length} source file(s) scanned under ${WORKSPACE_DIRS.join("/")}; ${findings.length} finding(s), ${reportable.length} in this diff`);
|
|
@@ -4921,4 +4924,9 @@ function main() {
|
|
|
4921
4924
|
}
|
|
4922
4925
|
process.exit(0);
|
|
4923
4926
|
}
|
|
4924
|
-
main
|
|
4927
|
+
if (__require.main == __require.module) {
|
|
4928
|
+
main();
|
|
4929
|
+
}
|
|
4930
|
+
export {
|
|
4931
|
+
isTestFile
|
|
4932
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -3309,7 +3309,7 @@ function renderSection6(facts) {
|
|
|
3309
3309
|
" Files:",
|
|
3310
3310
|
...files.map((f) => ` - ${f.path}`),
|
|
3311
3311
|
"",
|
|
3312
|
-
`
|
|
3312
|
+
` Touches ${pkg}. The pre-push hook runs the affected suite on your one push and refuses it on failure — do not run it yourself per Part.`
|
|
3313
3313
|
].join(`
|
|
3314
3314
|
`));
|
|
3315
3315
|
n++;
|
|
@@ -3335,7 +3335,7 @@ function renderSection8() {
|
|
|
3335
3335
|
return [
|
|
3336
3336
|
"## 8. Verification before claiming done",
|
|
3337
3337
|
"",
|
|
3338
|
-
"- `
|
|
3338
|
+
"- The pre-push hook already ran the affected suite on your one push and refused it on failure — do not additionally run it yourself; `vinaya pr report --write`/`--push` separately re-runs it with `--force` to attest the command and its output in the Evidence block.",
|
|
3339
3339
|
"- The full `bun run test` suite is CI's to run, on the one push — never run it locally.",
|
|
3340
3340
|
"- Every blast-radius consumer named in §4, re-verified by name.",
|
|
3341
3341
|
'- `roles/developer.md`\'s tier checklist genuinely satisfied, and `PR_BODY="$(cat <body-file>)" bun packages/aeg-core/bin/verify-docs.ts --pr` green.'
|
|
@@ -3911,7 +3911,7 @@ ${vinayaSetupSteps(selfHost, "pull-request")}${adopterSetupStep(ciSetup)} #
|
|
|
3911
3911
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
3912
3912
|
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
3913
3913
|
run: |
|
|
3914
|
-
DELIM="PR_BODY_$(
|
|
3914
|
+
DELIM="PR_BODY_$(openssl rand -hex 16)"
|
|
3915
3915
|
echo "PR_BODY<<$DELIM" >> "$GITHUB_ENV"
|
|
3916
3916
|
gh pr view "$PR_NUMBER" --json body --jq .body >> "$GITHUB_ENV"
|
|
3917
3917
|
echo "$DELIM" >> "$GITHUB_ENV"
|
|
@@ -4081,13 +4081,26 @@ jobs:
|
|
|
4081
4081
|
# lookup vinaya-review-verdict.yml's own retrigger step runs.
|
|
4082
4082
|
set -o pipefail
|
|
4083
4083
|
RUN_TITLE="Vinaya Review Gate PR #$PR_NUMBER @ $HEAD_SHA"
|
|
4084
|
+
# O7 (found live 2026-09-04): dropped \`select(.status == "completed")\`
|
|
4085
|
+
# — two retriggers for the same head race the SAME required run
|
|
4086
|
+
# (PR #401, PR #409: runs 33841069791/33841065139), and the loser's
|
|
4087
|
+
# query landed while the winner's \`gh run rerun\` had already
|
|
4088
|
+
# flipped the run to \`in_progress\`. Requiring \`completed\` made
|
|
4089
|
+
# that run invisible to the loser entirely — not "already handled,
|
|
4090
|
+
# skip", but "nothing matched, give up" — so when that in-flight
|
|
4091
|
+
# attempt itself later failed, nothing retried it again and the
|
|
4092
|
+
# gate stayed red until a hand rerun. Matching on title+event alone
|
|
4093
|
+
# (any status) lets the loser find the SAME run mid-run and try
|
|
4094
|
+
# \`gh run rerun\` on it too; the existing "declined (already
|
|
4095
|
+
# queued)" fallback below already handles that outcome harmlessly
|
|
4096
|
+
# — it is not a new failure mode, only a query that used to see
|
|
4097
|
+
# nothing at all now sees the run and takes the same safe no-op.
|
|
4084
4098
|
RUN_ID=$(gh api --paginate \\
|
|
4085
4099
|
"repos/\${{ github.repository }}/actions/workflows/vinaya-review.yml/runs?event=pull_request_target&per_page=100" \\
|
|
4086
4100
|
| jq -sr --arg title "$RUN_TITLE" '
|
|
4087
4101
|
[.[].workflow_runs[]
|
|
4088
4102
|
| select(.display_title == $title)
|
|
4089
4103
|
| select(.event == "pull_request_target")
|
|
4090
|
-
| select(.status == "completed")
|
|
4091
4104
|
| select(.conclusion != "cancelled")
|
|
4092
4105
|
| .id][0] // empty')
|
|
4093
4106
|
if [ -z "$RUN_ID" ]; then
|
|
@@ -4164,7 +4177,7 @@ ${vinayaSetupSteps(selfHost, "trusted")} # PR_BODY is what makes body-bare-
|
|
|
4164
4177
|
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
4165
4178
|
PR_NUMBER: \${{ github.event.pull_request.number }}
|
|
4166
4179
|
run: |
|
|
4167
|
-
DELIM="PR_BODY_$(
|
|
4180
|
+
DELIM="PR_BODY_$(openssl rand -hex 16)"
|
|
4168
4181
|
echo "PR_BODY<<$DELIM" >> "$GITHUB_ENV"
|
|
4169
4182
|
gh pr view "$PR_NUMBER" --json body --jq .body >> "$GITHUB_ENV"
|
|
4170
4183
|
echo "$DELIM" >> "$GITHUB_ENV"
|
|
@@ -4285,13 +4298,26 @@ ${vinayaSetupSteps(selfHost, "trusted")} - name: Review gate (verdict evalu
|
|
|
4285
4298
|
# query matches that immutable display_title exactly.
|
|
4286
4299
|
set -o pipefail
|
|
4287
4300
|
RUN_TITLE="Vinaya Review Gate PR #$PR_NUMBER @ $HEAD_SHA"
|
|
4301
|
+
# O7 (found live 2026-09-04): dropped \`select(.status == "completed")\`
|
|
4302
|
+
# — two retriggers for the same head race the SAME required run
|
|
4303
|
+
# (PR #401, PR #409: runs 33841069791/33841065139), and the loser's
|
|
4304
|
+
# query landed while the winner's \`gh run rerun\` had already
|
|
4305
|
+
# flipped the run to \`in_progress\`. Requiring \`completed\` made
|
|
4306
|
+
# that run invisible to the loser entirely — not "already handled,
|
|
4307
|
+
# skip", but "nothing matched, give up" — so when that in-flight
|
|
4308
|
+
# attempt itself later failed, nothing retried it again and the
|
|
4309
|
+
# gate stayed red until a hand rerun. Matching on title+event alone
|
|
4310
|
+
# (any status) lets the loser find the SAME run mid-run and try
|
|
4311
|
+
# \`gh run rerun\` on it too; the existing "declined (already
|
|
4312
|
+
# queued)" fallback below already handles that outcome harmlessly
|
|
4313
|
+
# — it is not a new failure mode, only a query that used to see
|
|
4314
|
+
# nothing at all now sees the run and takes the same safe no-op.
|
|
4288
4315
|
RUN_ID=$(gh api --paginate \\
|
|
4289
4316
|
"repos/\${{ github.repository }}/actions/workflows/vinaya-review.yml/runs?event=pull_request_target&per_page=100" \\
|
|
4290
4317
|
| jq -sr --arg title "$RUN_TITLE" '
|
|
4291
4318
|
[.[].workflow_runs[]
|
|
4292
4319
|
| select(.display_title == $title)
|
|
4293
4320
|
| select(.event == "pull_request_target")
|
|
4294
|
-
| select(.status == "completed")
|
|
4295
4321
|
| select(.conclusion != "cancelled")
|
|
4296
4322
|
| .id][0] // empty')
|
|
4297
4323
|
if [ -z "$RUN_ID" ]; then
|
|
@@ -4429,8 +4455,13 @@ ${hookRun(selfHost, "check --all --local")}`;
|
|
|
4429
4455
|
return base;
|
|
4430
4456
|
return `${base}
|
|
4431
4457
|
# Ring 0: the affected test suite. A failing test refuses the push with
|
|
4432
|
-
# its own output (#407, O4).
|
|
4433
|
-
|
|
4458
|
+
# its own output (#407, O4). --concurrency=1 (O9, found live 2026-09-04):
|
|
4459
|
+
# a local machine already running other work (another worktree's own
|
|
4460
|
+
# build/test, an IDE indexer) alongside this hook's parallel package
|
|
4461
|
+
# suites was measured pushing a git-clone-heavy fixture test past its
|
|
4462
|
+
# timeout under real contention — CI's own runner is dedicated and keeps
|
|
4463
|
+
# turbo.json's default concurrency; only this hook invocation is serialized.
|
|
4464
|
+
bunx turbo test --affected --concurrency=1 || exit 1`;
|
|
4434
4465
|
}
|
|
4435
4466
|
function commitMsgBody(selfHost) {
|
|
4436
4467
|
return `# Vinaya commit-message gate. Validates the message's first line against
|
|
@@ -7931,7 +7962,7 @@ var REGISTRY = [
|
|
|
7931
7962
|
run: bin("check-main-branch-refusal"),
|
|
7932
7963
|
scope: "full",
|
|
7933
7964
|
timeoutMs: 15000,
|
|
7934
|
-
env: {}
|
|
7965
|
+
env: { VINAYA_PUSH_REFS: { optional: true } }
|
|
7935
7966
|
},
|
|
7936
7967
|
0
|
|
7937
7968
|
],
|
|
@@ -9719,7 +9750,7 @@ async function runDoctor(args, deps) {
|
|
|
9719
9750
|
findings.push(...diagnoseTestCi(repo.repoRoot));
|
|
9720
9751
|
const healthy = findings.every((f) => f.severity === "ok" || f.severity === "info");
|
|
9721
9752
|
if (jsonOutput) {
|
|
9722
|
-
printJson({ healthy, findings });
|
|
9753
|
+
printJson({ healthy, findings, doctrineInfo });
|
|
9723
9754
|
} else {
|
|
9724
9755
|
printReport(findings, healthy, doctrineInfo);
|
|
9725
9756
|
}
|
|
@@ -11929,16 +11960,17 @@ async function buildReport(opts = {}) {
|
|
|
11929
11960
|
const groupA = opts.groupA ?? computeGroupA();
|
|
11930
11961
|
const gateRunner = opts.gateRunner ?? runRealGates;
|
|
11931
11962
|
const gateResult = await gateRunner();
|
|
11963
|
+
const outcomes = gateResult.outcomes.filter((o) => o.name !== "evidence-fresh");
|
|
11932
11964
|
const groupC = opts.groupC ?? computeGroupC(opts.body ?? process.env.PR_BODY ?? "");
|
|
11933
|
-
const blockInner = buildBlockInner(groupA,
|
|
11965
|
+
const blockInner = buildBlockInner(groupA, outcomes, groupC);
|
|
11934
11966
|
const block = `${EVIDENCE_START}
|
|
11935
11967
|
${blockInner}
|
|
11936
11968
|
${EVIDENCE_END}`;
|
|
11937
11969
|
return {
|
|
11938
11970
|
block,
|
|
11939
11971
|
blockInner,
|
|
11940
|
-
gatesFailed:
|
|
11941
|
-
gateOutcomes:
|
|
11972
|
+
gatesFailed: anyGateFailed(outcomes) || groupCFailed(groupC),
|
|
11973
|
+
gateOutcomes: outcomes,
|
|
11942
11974
|
groupC
|
|
11943
11975
|
};
|
|
11944
11976
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@attalabs/vinaya",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "Vinaya — Agentic Engineering Harness. Deterministic checks every AI coding agent must satisfy before merge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"bundle-doctrine": "bun scripts/bundle-doctrine.ts",
|
|
42
42
|
"bundle-studio": "bun scripts/bundle-studio.ts",
|
|
43
43
|
"prepack": "bun scripts/build.ts && bun scripts/bundle-doctrine.ts && bun scripts/bundle-studio.ts",
|
|
44
|
-
"test": "bun test",
|
|
44
|
+
"test": "bun test --timeout=30000",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"lint": "biome check src tests scripts",
|
|
47
47
|
"verify-published-lifecycle": "bun scripts/verify-published-lifecycle.ts"
|