instar 1.3.1192 → 1.3.1194

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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
5
- "packageVersion": "1.3.1192",
5
+ "packageVersion": "1.3.1194",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "1d4a49b0ff0277fab4b6a429d4018f35f6d504f0c762fbc628479590882b64f4",
2
+ "sha256": "682c4fe7ea234c958ab4c7c34ca753fc73b5758a613693ca0b86f2d608bf6a32",
3
3
  "registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
4
- "packageVersion": "1.3.1192"
4
+ "packageVersion": "1.3.1194"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
3
3
  "articleCount": 90,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1192"
5
+ "packageVersion": "1.3.1194"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1192",
3
+ "version": "1.3.1194",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -565,22 +565,49 @@ function main() {
565
565
  const files = argv.filter((a) => !a.startsWith('--'));
566
566
  const targets = files.length ? files : listSpecFiles();
567
567
 
568
+ // ── The DENOMINATOR, reported beside the verdict (2026-08-23) ──────────
569
+ //
570
+ // WHY. On 2026-08-21 this gate was found to have been matching its section
571
+ // heading EXACTLY while spec authors had begun numbering theirs. It saw 91 of
572
+ // 149 posture-carrying specs and silently skipped 58 — printing "clean" about
573
+ // a corpus it had never read. Nobody made a mistake; the population drifted
574
+ // under the instrument, and there is no diff showing the moment it broke.
575
+ //
576
+ // The matcher was widened, which fixes THAT drift. This fixes the READING,
577
+ // which is the part that generalises: "clean" is a sentence nobody questions,
578
+ // and "clean — 0 findings across 149 spec(s), 91 carrying a posture section"
579
+ // is one somebody does. A shrinking denominator beside a reassuring word is
580
+ // the cheapest thing that would have surfaced the original bug without anyone
581
+ // auditing the matcher, and it costs a line.
582
+ //
583
+ // `unreadable` is counted rather than swallowed for the same reason: a file
584
+ // that could not be read is not a file with no findings.
568
585
  const allFindings = [];
586
+ let scanned = 0;
587
+ let withPosture = 0;
588
+ let unreadable = 0;
569
589
  for (const file of targets) {
570
590
  let text;
571
591
  try {
572
592
  text = fs.readFileSync(file, 'utf8');
573
593
  } catch {
594
+ unreadable += 1;
574
595
  continue;
575
596
  }
597
+ scanned += 1;
598
+ if (findPostureSection(text)) withPosture += 1;
576
599
  const { findings } = gradeMachineLocalMarkers(text);
577
600
  for (const f of findings) allFindings.push({ file: path.relative(ROOT, path.resolve(file)), ...f });
578
601
  }
602
+ const population = { scanned, withPosture, unreadable, findings: allFindings.length };
603
+ const denominator =
604
+ `${scanned} spec(s) scanned, ${withPosture} carrying a posture section` +
605
+ (unreadable > 0 ? `, ${unreadable} UNREADABLE` : '');
579
606
 
580
607
  if (json) {
581
- process.stdout.write(JSON.stringify({ findings: allFindings, strict }, null, 2) + '\n');
608
+ process.stdout.write(JSON.stringify({ findings: allFindings, population, strict }, null, 2) + '\n');
582
609
  } else if (allFindings.length === 0) {
583
- console.log('lint-machine-local-justification: clean — no undefended or malformed markers.');
610
+ console.log(`lint-machine-local-justification: clean — no undefended or malformed markers (${denominator}).`);
584
611
  } else {
585
612
  const header = strict
586
613
  ? 'lint-machine-local-justification: FINDINGS (strict — blocking):'
@@ -591,7 +618,7 @@ function main() {
591
618
  console.error(` ${f.message}`);
592
619
  }
593
620
  console.error(
594
- `\n ${allFindings.length} finding(s). Standard A: docs/STANDARDS-REGISTRY.md ` +
621
+ `\n ${allFindings.length} finding(s) — ${denominator}. Standard A: docs/STANDARDS-REGISTRY.md ` +
595
622
  `("An Instar Agent Is Always a Multi-Machine Entity").`,
596
623
  );
597
624
  }
@@ -625,7 +625,13 @@ function validateRootSelfWiring() {
625
625
  // silent one. The guard caught exactly that when this key was introduced.
626
626
  STANDARDS_DIRECTION_REVIEW_FILE: '${{ runner.temp }}/standards-direction-review.json',
627
627
  };
628
- if (!exactKeys(job, ['name', 'runs-on', 'steps']) ||
628
+ if (!exactKeys(job, ['name', 'runs-on', 'permissions', 'steps']) ||
629
+ // Second-pass finding 2: the token scope path B needs is now EXPLICIT and
630
+ // pinned, not inherited from the repository's default workflow permission —
631
+ // a tightened org default would otherwise 403 the association call and
632
+ // silently restore the always-refuse-on-push state this fix ends.
633
+ !exactKeys(job.permissions, ['contents', 'pull-requests']) ||
634
+ job.permissions.contents !== 'read' || job.permissions['pull-requests'] !== 'read' ||
629
635
  job.name !== 'Standards Enforcement Coverage' || job['runs-on'] !== 'ubuntu-latest' ||
630
636
  !exactKeys(checkStep, ['run', 'env']) || !exactKeys(checkStep?.env, Object.keys(checkEnv)) ||
631
637
  Object.entries(checkEnv).some(([key, value]) => checkStep.env[key] !== value) ||
@@ -653,6 +659,40 @@ function validateRootSelfWiring() {
653
659
  'git show "$BASE_SHA:.github/keyrings/telegram-principal-pub.pem" > "$RUNNER_TEMP/standards-direction-approver-base.pem"',
654
660
  '',
655
661
  ].join('\n');
662
+ // Second-pass finding 1 (2026-08-23): the pin used to constrain this step's KEYS
663
+ // and env map but not its `run`, so the entire push-resolution branch could be
664
+ // deleted — returning the guard to refusing on every push — with every ratchet
665
+ // assertion still green. The body is pinned literally now, exactly as the
666
+ // protected-base step's is, so a behaviour edit here is a DECLARED edit.
667
+ const expectedReviewRun = [
668
+ 'set -u',
669
+ 'if [ "$EVENT_NAME" != "pull_request" ]; then',
670
+ " associated=$(gh api \"repos/${GITHUB_REPOSITORY}/commits/${PUSH_SHA}/pulls\" 2>/dev/null || echo '[]')",
671
+ " match=$(SHA=\"$PUSH_SHA\" jq -c '[.[] | select(.merge_commit_sha == env.SHA)] | if length == 1 then .[0] else empty end' <<< \"${associated:-[]}\" 2>/dev/null || echo '')",
672
+ " PR=''",
673
+ " HEAD_SHA=''",
674
+ " PR_AUTHOR=''",
675
+ ' if [ -n "$match" ]; then',
676
+ " PR=$(jq -r '.number // empty' <<< \"$match\" 2>/dev/null || echo '')",
677
+ " HEAD_SHA=$(jq -r '.head.sha // empty' <<< \"$match\" 2>/dev/null || echo '')",
678
+ " PR_AUTHOR=$(jq -r '.user.login // empty' <<< \"$match\" 2>/dev/null || echo '')",
679
+ ' fi',
680
+ " echo \"path-b: event=${EVENT_NAME} associated=$(jq -r 'length' <<< \"${associated:-[]}\" 2>/dev/null || echo unreadable) matched-pr=${PR:-none}\"",
681
+ 'fi',
682
+ "reviews='[]'",
683
+ 'if [ -n "$PR" ]; then',
684
+ " reviews=$(gh api \"repos/${GITHUB_REPOSITORY}/pulls/${PR}/reviews?per_page=100\" 2>/dev/null || echo '[]')",
685
+ 'fi',
686
+ "echo \"path-b: reviews=$(jq -r 'length' <<< \"${reviews:-[]}\" 2>/dev/null || echo unreadable) head-sha=${HEAD_SHA:-none} owner=${OWNER_LOGIN:-none}/${OWNER_TYPE:-none}\"",
687
+ 'jq -n --argjson reviews "${reviews:-[]}" \\',
688
+ ' --arg headSha "$HEAD_SHA" \\',
689
+ ' --arg ownerLogin "$OWNER_LOGIN" \\',
690
+ ' --arg ownerType "$OWNER_TYPE" \\',
691
+ ' --arg prAuthorLogin "$PR_AUTHOR" \\',
692
+ " '{reviews:$reviews, headSha:$headSha, ownerLogin:$ownerLogin, ownerType:$ownerType, prAuthorLogin:$prAuthorLogin}' \\",
693
+ " > \"$OUT\" || echo '{}' > \"$OUT\"",
694
+ '',
695
+ ].join('\n');
656
696
  const expectedBaseSha = "${{ github.event.pull_request.base.sha || github.event.before || format('{0}^', github.sha) }}";
657
697
  const exactPrefix = [checkoutStep, setupStep, installStep, baseStep, reviewStep, checkStep];
658
698
  const ordered = exactPrefix.every((step, index) => step && steps[index] === step);
@@ -664,9 +704,16 @@ function validateRootSelfWiring() {
664
704
  baseStep.name === 'Resolve protected-base area ledger' &&
665
705
  exactKeys(baseStep.env, ['BASE_SHA']) && baseStep.env.BASE_SHA === expectedBaseSha &&
666
706
  baseStep.run === expectedBaseRun &&
667
- exactKeys(reviewStep, ['name', 'if', 'env', 'run']) &&
668
- reviewStep.if === "github.event_name == 'pull_request'" &&
669
- exactKeys(reviewStep.env, ['GH_TOKEN', 'PR', 'HEAD_SHA', 'OWNER_LOGIN', 'OWNER_TYPE', 'PR_AUTHOR', 'OUT']) &&
707
+ // 2026-08-23: the `if:` key is GONE from this pin because it is gone from the
708
+ // step. Gating the evidence-gathering on `pull_request` made the guard refuse
709
+ // on every push to main — the operator's approval existed, on the pull request
710
+ // that produced the commit, and the step simply was not run to go and read it.
711
+ // The pin follows the step: EVENT_NAME and PUSH_SHA are the inputs the push
712
+ // branch needs, and their presence here is what makes adding them a declared
713
+ // edit rather than a silent one.
714
+ exactKeys(reviewStep, ['name', 'env', 'run']) &&
715
+ reviewStep.run === expectedReviewRun &&
716
+ exactKeys(reviewStep.env, ['GH_TOKEN', 'EVENT_NAME', 'PR', 'HEAD_SHA', 'PR_AUTHOR', 'PUSH_SHA', 'OWNER_LOGIN', 'OWNER_TYPE', 'OUT']) &&
670
717
  reviewStep.env.OUT === '${{ runner.temp }}/standards-direction-review.json';
671
718
  if (!protectedBaseWired) {
672
719
  errors.push('The Root self-wiring requires dependency install plus full-history protected-base extraction and required base env on the standards check');
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-08-23T18:38:29.859Z",
5
- "instarVersion": "1.3.1192",
4
+ "generatedAt": "2026-08-24T00:25:20.165Z",
5
+ "instarVersion": "1.3.1194",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
5
- "packageVersion": "1.3.1192",
5
+ "packageVersion": "1.3.1194",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "1d4a49b0ff0277fab4b6a429d4018f35f6d504f0c762fbc628479590882b64f4",
2
+ "sha256": "682c4fe7ea234c958ab4c7c34ca753fc73b5758a613693ca0b86f2d608bf6a32",
3
3
  "registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
4
- "packageVersion": "1.3.1192"
4
+ "packageVersion": "1.3.1194"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
3
3
  "articleCount": 90,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1192"
5
+ "packageVersion": "1.3.1194"
6
6
  }
@@ -0,0 +1,31 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The check that verifies a constitutional change was ratified now looks for the owner's approval on a push to `main`, not only while the pull request is open.
9
+
10
+ The evidence-gathering step was gated on the `pull_request` event. On a push — the merge itself — it never ran, so the file holding the review context was never written, and the check read the resulting "file not found" as "no approval" and refused. It was demanding a ratification the operator had already given, minutes earlier, on the pull request that produced that exact commit.
11
+
12
+ That is the safe direction taken at the wrong moment, and it was not theoretical: canonical `main` went red at 05:05Z on 2026-08-23 and stayed red across three consecutive merges, every one of them a registry change the operator had approved.
13
+
14
+ The evidence is still reachable on a push; it just has to be reached through the commit. The step now runs on both events and, on a push, resolves the pull request whose merge produced this commit — matching on `merge_commit_sha` rather than taking the first associated pull request, because a commit can be associated with several and only the one GitHub actually merged carries the approval that ratified it. Its head commit and reviews then feed the same unchanged evaluator.
15
+
16
+ ## What to Tell Your User
17
+
18
+ If you approve a constitutional change on GitHub, that approval now counts both while the pull request is open and after it merges. Previously it counted only until the moment you merged, at which point the check asked for it again on the main branch — where there is no pull request to approve, so nothing could ever satisfy it and the build stayed red until someone noticed.
19
+
20
+ Nothing about what you do changes. You still approve once, in the same place.
21
+
22
+ ## Summary of New Capabilities
23
+
24
+ - A constitutional change ratified by the owner's review no longer turns the main branch red the moment it merges.
25
+ - Re-introducing the event gate that caused it now fails a check instead of passing quietly.
26
+
27
+ ## Evidence
28
+
29
+ - The binding is unchanged in strictness: an `APPROVED` review, from the repository owner, on the exact head commit, submitted by someone other than the author. Only *where the inputs come from* on a push is new.
30
+ - **The bypass stays closed.** A commit with no such pull request — a direct push to `main` — resolves nothing, writes an empty context, and the check refuses exactly as before. Every failure path (API error, rate limit, no matching pull request, or more than one match) yields an empty context rather than an error, so "cannot verify" can never become "verified". One honest qualifier, unchanged by this fix: the check is per-push rather than per-article, so a push whose tip is an approved merge commit covers everything in that push. That only matters to someone who can already push to `main` directly, which the ruleset — not this check — is what prevents.
31
+ - The workflow self-wiring contract now REFUSES an `if:` on that step, and pins the two push-branch inputs. Re-gating it, or dropping either input, fails the check rather than silently returning the guard to refusing on every push. Three regression assertions pin all three routes.
@@ -0,0 +1,34 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ `scripts/lint-machine-local-justification.js` reports its population beside its verdict — specs scanned, specs carrying a posture section, and unreadable files — on the clean path, the findings path, and as `population` in `--json`. Unreadable files are counted rather than skipped silently.
9
+
10
+ Zero behaviour change: no verdict, threshold or exit code moves.
11
+
12
+ ## What to Tell Your User
13
+
14
+ None — internal change (no user-facing surface).
15
+
16
+ ## Summary of New Capabilities
17
+
18
+ None — internal change (no user-facing surface).
19
+
20
+ ## Evidence
21
+
22
+ On 2026-08-21 this gate was found matching its section heading by exact text while authors had begun numbering theirs. It saw 91 of 149 posture-carrying specs, silently skipped 58 — including the replicated-store foundation, the mesh self-heal spec, the secure-pairing spec and the standards-registry spec — and printed `clean` about a corpus it had never read. Widening the matcher fixed that drift; this fixes the reading, which is the part that generalises to the next drift.
23
+
24
+ 40 tests, including the property the incident turned on: a one-file run and a two-file run must not print the same sentence. Also pinned: the denominator on the FINDINGS path (where a shrunken population is most dangerous — a reader sees findings and assumes the sweep was whole), and an unreadable file counted rather than swallowed.
25
+
26
+ First live output: `135 finding(s) — 1818 spec(s) scanned, 133 carrying a posture section`.
27
+
28
+ ## Known Limits
29
+
30
+ A denominator does not DETECT drift; it makes drift legible to a human reading the output. If nobody reads it, it buys nothing. An automated ratchet on the population would be the stronger control and is not built here.
31
+
32
+ The historical comment says 149 posture-carrying specs; this measures 133. **Drift is ruled out** — three independent methods (this lint, a peer's `git grep`, and a third regex pass) all return 133, including at `c465a94ef`, the widening commit itself, with the count unmoved across the two commits since. The population is not shrinking under the gate. What produced 149 remains unmeasured and no claim is made between the candidates.
33
+
34
+ No other gate in this repository was audited for the same defect, and nothing enumerates the gates that print a reassuring word with no scale.
@@ -0,0 +1,44 @@
1
+ # Saying how much was checked, not just that it passed — Plain-English Overview
2
+
3
+ ## The problem in one breath
4
+
5
+ A safety check in this project spent an unknown period reporting "clean" about documents it had never opened. It found the section it cared about by matching a heading word-for-word, and authors had started numbering their headings. It quietly saw 91 documents out of 149 and skipped 58 — including some of the most important ones — and said the same reassuring word it says when everything really is fine.
6
+
7
+ ## What already exists
8
+
9
+ That matching bug was fixed on the 21st. The check now finds numbered headings properly.
10
+
11
+ ## What this adds
12
+
13
+ The check now says **how much it looked at**, not only what it found.
14
+
15
+ Before: `clean`
16
+ After: `clean — 0 findings across 149 documents, 91 carrying the section we grade`
17
+
18
+ ## Why that matters more than it sounds
19
+
20
+ Fixing the matcher fixes that one drift. It doesn't fix the next one. The set of documents can shrink again for reasons nobody records, because no author does anything wrong — and the verdict stays the single most comforting word available.
21
+
22
+ "Clean" is a sentence nobody questions. "Clean, 91 of 149" is a sentence somebody does. A shrinking number next to a reassuring word is the cheapest thing that would have caught the original problem without anyone going and auditing the checker, and it costs one line.
23
+
24
+ ## The safeguards
25
+
26
+ **Nothing can newly fail.** Every verdict and exit code is identical; only the wording changes.
27
+
28
+ **A file that couldn't be opened is counted, not skipped.** A document that failed to load is not a document with no problems — silently dropping it is the same defect one level down.
29
+
30
+ **The tests pin the point, not the wording:** a run over one document and a run over two must not print the same sentence. That's the property the whole incident turned on.
31
+
32
+ ## What ships when
33
+
34
+ Immediately. Nothing to decide, no setting, no risk.
35
+
36
+ ## What you actually need to decide
37
+
38
+ Nothing — and the one loose number has since been chased down, which is worth reporting because it is the change doing its job.
39
+
40
+ On the first run the new count (133) disagreed with a number written in an old comment (149). The worrying reading was that sixteen documents had quietly lost the section nobody was checking. **That is now ruled out.** Two other people measured it different ways and both got 133 — including at the exact moment the old comment was written. Nothing has been slipping away.
41
+
42
+ What nobody has worked out is where 149 came from. A different branch, a different way of counting, or a number that was never right. Three possibilities, none of them picked, because guessing between them would be the same habit this change exists to break.
43
+
44
+ One small thing worth keeping: my own quick check first said 131, not 133. The difference was capital letters — two documents write "Posture" rather than "posture". Two measurements disagreeing is information; averaging them or taking the nicer one would have been exactly the mistake.
@@ -0,0 +1,155 @@
1
+ # Side-Effects Review — the direction guard reads the operator's approval on push, not only on the PR
2
+
3
+ **Version / slug:** `direction-guard-push-event`
4
+ **Date:** `2026-08-23`
5
+ **Author:** `Echo (claude-opus-5)`
6
+ **Second-pass reviewer:** `independent subagent (general-purpose) — CONCERN, six findings, five fixed in this change; see Second-pass review below`
7
+
8
+ ## Summary of the change
9
+
10
+ The direction guard's path-B evidence-gathering step in `.github/workflows/ci.yml` shipped gated `if: github.event_name == 'pull_request'`. On a push to `main` it therefore never ran, `STANDARDS_DIRECTION_REVIEW_FILE` was never written, and `scripts/standards-coverage.mjs` read the resulting ENOENT as "review unavailable" and refused — demanding a ratification the operator had already given on the pull request that produced that exact commit. The step now runs on both events; on a push it resolves the pull request whose merge produced the commit (matching `merge_commit_sha`, not list position) and feeds that PR's head sha and reviews to the same unchanged `evaluateOperatorReviewApproval`. `scripts/standards-coverage.mjs`'s Root self-wiring pin follows the step: `if` is now refused on it, and `EVENT_NAME`/`PUSH_SHA` are pinned. Three regression assertions in `tests/unit/standards-coverage-ratchet.test.ts` pin all three routes.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `scripts/standards-direction-guard.mjs#evaluateOperatorReviewApproval` — **pass-through** — the approval evaluator is untouched; only where its inputs come from on a push event changes.
15
+ - `scripts/standards-coverage.mjs#validateRootSelfWiring` — **modify** — the exact-keys contract for the review step now refuses `if` and requires the two push-branch env inputs.
16
+ - `.github/workflows/ci.yml` → "Fetch operator review context (direction guard path B)" — **modify** — runs on both events; adds a push branch that resolves the originating PR.
17
+
18
+ ---
19
+
20
+ ## 1. Over-block
21
+
22
+ **What legitimate inputs does this change reject that it shouldn't?**
23
+
24
+ The change is strictly un-blocking on the push path: previously *every* review-ratified registry change was rejected there; now the ones with a resolvable originating PR and a bound approval pass. It rejects nothing it previously accepted.
25
+
26
+ Residual over-block, stated rather than implied away: a merge commit whose originating pull request GitHub cannot associate — a rewritten/force-pushed history, a merge performed outside GitHub, or an association API outage — still refuses on the push build even though a human did approve. That is the pre-existing behaviour for that shape, unchanged, and it fails toward refusal by design.
27
+
28
+ ---
29
+
30
+ ## 2. Under-block
31
+
32
+ **What failure modes does this still miss?**
33
+
34
+ - A repository admin who pushes directly to `main`, bypassing branch protection, is refused by this check — but this check is not what stops them; the ruleset is, and an admin can disable that. Unchanged, and stated in the article itself: this is legibility, not the boundary.
35
+ - If a merged PR's branch is force-pushed *after* the merge so `head.sha` no longer matches the reviewed commit, the push build refuses. Fails closed, correct direction, no new hole.
36
+ - The check still cannot tell a *considered* approval from a reflexive one. Out of scope for any mechanical check.
37
+
38
+ ---
39
+
40
+ ## 3. Level-of-abstraction fit
41
+
42
+ Correct layer, and deliberately the low one. The step is an evidence *gatherer* — it fetches an API fact GitHub holds and writes it to a file. All judgment stays in `evaluateOperatorReviewApproval`, which is untouched. The alternative (teaching the node script to fetch its own reviews) was built earlier in the same evening as `c4b3a0be3` and **withdrawn** precisely because it put network I/O and identity comparison inside the evaluator and duplicated an implementation that already existed. This change keeps the split: shell gathers, evaluator judges.
43
+
44
+ ---
45
+
46
+ ## 4. Signal vs authority compliance
47
+
48
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
49
+
50
+ - [x] No — this change produces a signal consumed by an existing smart gate.
51
+
52
+ The step produces evidence (a JSON file); the authority to accept or refuse remains entirely with `evaluateOperatorReviewApproval` and, above it, the repository ruleset. The step is designed not to fail the job: every command carries an explicit fallback, so a broken gatherer degrades to "no approval found" (refuse) and never to "approved".
53
+
54
+ **Corrected by second-pass finding 3.** The first draft of this section asserted the step was "deliberately incapable of failing the job", and that was not true as written: GitHub's default shell is `bash -e`, and three `jq` invocations on the push branch carried no `|| …`. Practically unreachable — the input was jq-produced JSON — but an invariant that holds by accident is the kind that breaks later, and stating it as a guarantee was the defect. All three now carry `2>/dev/null || echo ''`, and the claim is true because it is enforced rather than because it happened to hold.
55
+
56
+ ---
57
+
58
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
59
+
60
+ No new static heuristic at a competing-signals decision point. The one added predicate — "which pull request produced this commit?" — is not a judgment: GitHub records the answer as `merge_commit_sha`, so the domain is enumerable and the match is an equality test on an authoritative field. The deliberate rejection of the softer heuristic ("take the first associated pull request") is the point: that one *would* have been a guess at a decision point where a wrong guess reads someone else's approval.
61
+
62
+ **Second-pass finding 5 closed a gap between that reasoning and the code.** The filter took `.[0]` of the matches, which is the singleton assumption *asserted in prose and not in the code* — precisely the shape this section exists to catch. It is now `if length == 1 then .[0] else empty end`: more than one match resolves nothing and the guard refuses, which is what the argument above actually claims.
63
+
64
+ ---
65
+
66
+ ## 5. Interactions
67
+
68
+ - **Shadowing:** the step runs immediately before the `--check` invocation and writes only its own file. It shadows nothing; the base-resolution step (`area-audit-base`) and the check step are unchanged and still ordered exactly as the self-wiring pin requires.
69
+ - **Double-fire:** on a `pull_request` event the behaviour is byte-identical to before (the push branch is skipped by the `EVENT_NAME` test), so no event produces two fetches.
70
+ - **Races:** none. The step writes one file in `$RUNNER_TEMP`, consumed once, in-process, by the next step of the same job.
71
+ - **Feedback loops:** none. The check reads GitHub state and never writes it.
72
+ - **Self-wiring pin coupling (the real interaction):** loosening the pin in the same change that changes the step is exactly the shape the pin exists to catch, so the loosening is narrow and directional — `if` moves from *asserted-equal* to *asserted-absent*, which is strictly stricter, and two required env keys are added rather than removed.
73
+
74
+ ---
75
+
76
+ ## 6. External surfaces
77
+
78
+ - **GitHub API:** adds one call on push builds only — `GET /repos/{repo}/commits/{sha}/pulls`. Read-only, uses the run's own `github.token`.
79
+ - **Token scope — corrected by second-pass finding 2, and it was a real error.** This section originally claimed the call ran "under the job's existing `contents:read` + `pull-requests:read`. No new permission." There was no `permissions:` block anywhere in the workflow; the job ran on the repository's *default* workflow permission, which I had asserted rather than read. That matters beyond bookkeeping: if that default is ever tightened by policy to contents-only, the association call 403s, `associated` becomes `[]`, and the fix silently reverts to always-refusing on push — the exact state it exists to end, reported as "head sha unavailable". The job now declares `contents: read` + `pull-requests: read` explicitly, and the Root self-wiring pin REQUIRES that block, so the scope is ratcheted rather than inherited.
80
+ - **Other agents / install base:** none. This is repository CI for the instar source tree; nothing ships to an installed agent.
81
+ - **Persistent state:** none. The context file lives in the runner's temp dir for the length of one job.
82
+ - **Timing we don't control:** the association API can lag immediately after a merge. Failure mode is refusal, not a false pass, and a re-run repairs it.
83
+ - **Observability — second-pass finding 4.** The reviewer's sharpest point: as first written, this change reproduced its own named defect class one layer down. A 403, a rate limit, association lag, and a genuine direct-push-with-no-PR all produced an empty `headSha` and the identical downstream message, and the step printed nothing at all — so the first real failure would have been indistinguishable from a legitimate refusal, on the branch where you cannot iterate. The step now logs the resolved state (event name, association count, matched PR number or `none`, review count, head sha, owner) to the job log. Nothing in that line is secret.
84
+ - **Operator surface (Mobile-Complete Operator Actions):** no operator-facing action added or changed. The operator's one action — approving on GitHub — is unchanged and already phone-completable; this change is precisely what makes that phone action count on the push build too.
85
+
86
+ ---
87
+
88
+ ## 6b. Operator-surface quality
89
+
90
+ No operator surface — not applicable. No dashboard renderer, approval page, or grant/revoke/secret-drop form is touched.
91
+
92
+ ---
93
+
94
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
95
+
96
+ **machine-local BY DESIGN — and specifically not-on-any-machine.** This code executes only inside GitHub Actions runners, never on an agent machine. There is no per-agent state, no replication path to name, and no pool-wide read: two agent machines observing the same repository see the same CI verdict because the verdict is computed in GitHub, not locally.
97
+
98
+ Explicitly: it emits **no** user-facing notices (so no one-voice gating question), holds **no** durable state (so nothing strands on topic transfer), and generates **no** URLs (so no machine-boundary link problem).
99
+
100
+ ---
101
+
102
+ ## 8. Rollback cost
103
+
104
+ - **Hot-fix release:** pure revert of two files plus the tests. No release needed — CI configuration takes effect on the next run.
105
+ - **Data migration:** none. No persistent state.
106
+ - **Agent state repair:** none. No installed agent runs this code.
107
+ - **User visibility:** reverting restores the red main branch this change fixes; it does not create a new user-visible regression.
108
+
109
+ ---
110
+
111
+ ## Conclusion
112
+
113
+ The review changed nothing about the design, because the design's only real question — how the push path identifies the originating pull request — was settled by verification rather than argument before the review began: `5a4efecc1` resolves to PR #1960 as its sole association with a matching `merge_commit_sha` and one `APPROVED` review bound to that PR's head, and `6bd584de0` resolves to #1965 the same way. The one thing the review sharpened is the framing of the defect: the guard's "review path unavailable" verdict collapsed two genuinely different world states — *the operator did not approve* and *nobody ever asked* — into one refusal, which is why the red state was unfixable by any action the operator could take. That is the class, and the pin is what closes it. Clear to ship.
114
+
115
+ ---
116
+
117
+ ## Second-pass review (if required)
118
+
119
+ **Reviewer:** independent subagent (general-purpose), briefed to attack rather than concur
120
+ **Independent read of the artifact: CONCERN — six findings**
121
+
122
+ What the reviewer verified before objecting, which is why the findings are worth their weight: the evaluator really is untouched; the real `ci.yml` passes its own pin; `exactKeys` sorts, so ordering is a non-issue; and it simulated the new shell across eleven paths (gh failure, `[]`, non-JSON, JSON object, empty stdout, no match, match with missing `head.sha`, reviews-fetch failure, reviews non-JSON, and the PR event) — every one exits 0 and writes a well-formed file, with the PR-event path byte-identical to before. On **security it found no hole**: the push path still requires an APPROVED owner review whose `commit_id` equals the resolved PR's `head.sha`; an empty `headSha` can never be accepted because the 40-hex check runs first; org owner, fork PR, bot author, self-approval and post-merge force-push all still refuse.
123
+
124
+ **Fixed in this change (findings 1-5):**
125
+
126
+ 1. **The `run:` body was not pinned at all** — the ratchet guarded the step's key set and env map but never its behaviour, so the entire push-resolution block could be deleted and all three new assertions would still pass, because the fixtures' run body was `echo "{}" > "$OUT"` and no test ever executed the real shell. This directly falsified the `guardEvidence` claim below, which said the exact edit that skips the push path fails the ratchet — true for the `if:` route only, one of at least three. The body is now pinned literally, exactly as the protected-base step's is, and the three fixtures share one definition so they cannot drift apart.
127
+ 2. **The permissions claim was factually wrong** — see §6. Fixed in code and in the text.
128
+ 3. **"Incapable of failing the job" was not true as written** — see §4. Fixed in code and in the text.
129
+ 4. **The change reproduced its own defect class one layer down** — see §6. Fixed with explicit step logging.
130
+ 5. **`.[0]` was an undeclared silent pick on multiple matches** — see §4b. Now fails closed.
131
+
132
+ **Accepted and NOT fixed here (finding 6), stated rather than closed:** the `push` webhook payload's `repository.owner` carries a different schema from the pull-request payload's, and the evaluator refuses first on `ownerType !== 'User'`. The evidence pointers below verify the REST repo object and the association API from a local token; neither proves the *runner's* push payload populates `owner.login`/`owner.type`. If it does not, the fix no-ops — safely, but silently. The reviewer is right that the artifact read as end-to-end verified when the push path has never actually executed. Finding 4's logging is what makes the answer readable; the commitment attached to this change is to read the first post-merge push build's `path-b:` and `direction-guard=` lines before calling the class closed, and to say so plainly if it did not work.
133
+
134
+ **Minor, acknowledged, not changed:** `!Object.hasOwn(reviewStep,'if')` is strictly redundant given `exactKeys` — removed, since presenting a redundant assertion as the load-bearing one is its own small dishonesty. And the release note's flat "the bypass stays closed" is one notch stronger than §2 supports: path B is non-per-article, so a push whose tip is an approved merge commit gives blanket coverage to any earlier unapproved registry commit in the same push. Admin-only and consistent with the stated boundary, but the note now carries the qualifier the artifact already did.
135
+
136
+ ---
137
+
138
+ ## Evidence pointers
139
+
140
+ - Failing CI job: run `32619488561`, "Standards Enforcement Coverage" — `review context unreadable (ENOENT ... standards-direction-review.json)` on the push build for `5a4efecc1`.
141
+ - Working PR-event path for contrast: run `32614325158` on PR #1960 — `direction-guard=passed` on an unsigned registry change.
142
+ - Association verification: `GET /repos/JKHeadley/instar/commits/{5a4efecc1,6bd584de0}/pulls` → exactly one PR each, `merge_commit_sha` matching, `APPROVED` review bound to that PR's head.
143
+ - Regression assertions: `tests/unit/standards-coverage-ratchet.test.ts` — re-gated step refused; `EVENT_NAME` dropped refused; `PUSH_SHA` dropped refused.
144
+
145
+ ---
146
+
147
+ ## Class-Closure Declaration (display-only mirror)
148
+
149
+ - **`defectClass`** — `instrument-semantic-darkness`
150
+ - **`closure`** — `guard`
151
+ - **`guardEvidence`** — `{ enforcementType: ratchet, citation: tests/unit/standards-coverage-ratchet.test.ts (three assertions on the review-step wiring, over a fixture that carries the step's real run body), howCaught: the Root self-wiring pin refuses an "if:" on the evidence-gathering step, requires EVENT_NAME + PUSH_SHA, and pins the run body LITERALLY — so every route that makes the producer skip the push path while the consumer still evaluates there (re-gating the step, dropping an input, or gutting the shell) fails the ratchet instead of shipping a verdict that cannot distinguish "no approval" from "never fetched" }`
152
+
153
+ *The literal run-body pin is in this declaration because of second-pass finding 1: without it the claim above was false for two of the three routes, and a guardEvidence claim that only covers the route the author happened to think of is the closure equivalent of a green test that never ran.*
154
+
155
+ The class fits on its own terms: the guard emitted one verdict for two distinct world states, and a reader of that verdict could not tell which one they were in. The registry's exclusion for "an honestly unassessable run with a future observation path that can repair it" does not apply — on the push build no future observation could repair it, because the step that produces the observation was structurally excluded from that path.
@@ -0,0 +1,89 @@
1
+ # Side-Effects Review — the marker lint reports its population beside its verdict
2
+
3
+ **Version / slug:** `lint-reports-its-denominator`
4
+ **Date:** `2026-08-23`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `peer session (echo-fb, W24) — the structural remedy is theirs; the worked example is mine`
7
+
8
+ ## Summary of the change
9
+
10
+ `scripts/lint-machine-local-justification.js` now prints how many specs it scanned and how many carried a posture section, on both the clean and the findings path, and exposes the same counts as `population` in `--json`. Files it could not read are COUNTED, not skipped silently.
11
+
12
+ ## Why — the incident this is the reading-side fix for
13
+
14
+ On 2026-08-21 this gate was found matching its section heading by exact text while spec authors had begun numbering theirs (`## 8. Multi-machine posture`). It saw **91 of 149** posture-carrying specs and silently skipped **58** — including the replicated-store foundation, the mesh self-heal spec, the secure-pairing spec, and the standards-registry spec itself — while printing `clean` about a corpus it had never read.
15
+
16
+ From the fix's own comment: *"Nobody had to make a mistake; you just had to number your heading."*
17
+
18
+ Widening the matcher fixed **that** drift. It does not fix the next one. The population can shrink again for reasons no diff records, because no author does anything wrong — and the verdict stays the single most reassuring word available. `clean` is a sentence nobody questions. `clean — 0 findings across 149 spec(s), 91 carrying a posture section` is one somebody does.
19
+
20
+ ## Decision-point inventory
21
+
22
+ None. This changes only what the gate SAYS. Every verdict, threshold and exit code is byte-identical.
23
+
24
+ ## 1. Over-block
25
+
26
+ Impossible: no code path can newly fail. `--strict` still exits non-zero on exactly the same condition (`allFindings.length > 0`).
27
+
28
+ ## 2. Under-block
29
+
30
+ Unchanged, and worth being precise: a denominator does not detect drift. It makes drift **legible to a human who reads the output**. If nobody reads it, it buys nothing. It is deliberately not sold as a detector — an automated ratchet on the population would be the stronger control and is not built here.
31
+
32
+ ## 3. Level-of-abstraction fit
33
+
34
+ Three counters in the CLI's existing loop. No new file, no new invocation, no new dependency. It is one line of output.
35
+
36
+ ## 4. Signal vs authority compliance
37
+
38
+ Compliant and unchanged. The lint is a report-first signal; this makes the signal more honest about its own reach.
39
+
40
+ ## 4b. Judgment-point check
41
+
42
+ None. Three integers.
43
+
44
+ ## 5. Interactions
45
+
46
+ - `findPostureSection` is now called once more per file to count coverage. Pure, no I/O, negligible.
47
+ - The `--json` shape GAINS a `population` key. Additive; nothing reads it yet.
48
+ - The self-wiring contract does not pin this script, so nothing else moves.
49
+
50
+ ## 6. External surfaces
51
+
52
+ None.
53
+
54
+ ## 6b. Operator-surface quality
55
+
56
+ The operator never runs this. The audience is whoever reads a CI log — and the incident proves that reader was previously given a reassuring word with no scale attached.
57
+
58
+ ## 7. Multi-machine posture
59
+
60
+ `unified` — trivially. A pure counter in a stateless CLI.
61
+
62
+ ## 8. Rollback cost
63
+
64
+ Three counters and a string. Reverting restores a verdict with no scale attached, which is the condition the incident occurred under.
65
+
66
+ ## Conclusion
67
+
68
+ Ship. Zero behaviour change, one line of output, and it addresses the reading-side half of a defect that cost this repository 39% of its posture coverage for an unknown period.
69
+
70
+ ## Evidence pointers
71
+
72
+ - 40 tests, including: the denominator appears on the clean path AND the findings path (the failing path is where a shrunken population is most dangerous — a reader sees findings, assumes the sweep was whole, and never asks how far it reached); the JSON `population` shape; an unreadable file COUNTED rather than swallowed; and the property the incident turned on — **a one-file run and a two-file run must not print the same sentence**.
73
+ - First real output on the live corpus: `135 finding(s) — 1818 spec(s) scanned, 133 carrying a posture section`.
74
+
75
+ ## The discrepancy this surfaced on its first run — and what three instruments then established
76
+
77
+ The historical comment records **149** posture-carrying specs; this measures **133**. That gap was reported as fully unresolved when this branch was first pushed. It has since been narrowed by two further measurements, and the narrowing is recorded here rather than folded silently into the original text.
78
+
79
+ **DRIFT IS RULED OUT — the alarming reading is disconfirmed.** A peer session (echo-fb, W24) measured by an independent method (`git grep`, not this lint's regex) and got 133 at HEAD *and* 133 at `c465a94ef`, the widening commit itself, with only two commits touching `docs/specs` in between and the count unmoved. I then reproduced that with a third method: 133 at `c465a94ef`, 133 at `upstream/main`, control on an invented token 0, control on a common token 1811 of 1818.
80
+
81
+ So the count was **already 133 when the comment claiming 149 was written**. Sixteen specs did not silently lose their posture section.
82
+
83
+ **A 2-file disagreement between my third method and the other two, resolved rather than averaged.** My first pass returned 131. The difference is case: my throwaway regex was case-sensitive on the second word, while the lint's matcher carries `/i`. The two specs are `matrix-cell-operator-cancel.md` and `playwright-profile-registry.md`, whose headings capitalise "Posture". 133 is correct and matches the lint. Recorded because a two-instrument disagreement is information, and resolving it by preference would have been the error this whole change is about.
84
+
85
+ **Still unmeasured: what produced 149.** A different branch, a method nobody has guessed, or a figure that was never right. Three candidates, none distinguished, and no claim made between them. What is now measured is the part that mattered — the population is not shrinking under the gate.
86
+
87
+ ## Class-Closure Declaration (display-only mirror)
88
+
89
+ The class is "a verdict reported without its scale." Closed for this gate. NOT closed generally — no other gate in this repository was audited for it, and nothing enumerates the gates that print a reassuring word with no denominator. That sweep is the real work and is not done here.