instar 1.3.1189 → 1.3.1190

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": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
5
- "packageVersion": "1.3.1189",
5
+ "packageVersion": "1.3.1190",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "5d762bbb3b25d0fb34ecaa317a638f1eab6af13dc6a22f7392c054800ccee190",
2
+ "sha256": "e2275b904b85c7db59a699f899b730f164b89ca204813d9766d5b1c5233a92ba",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1189"
4
+ "packageVersion": "1.3.1190"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
3
3
  "articleCount": 88,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1189"
5
+ "packageVersion": "1.3.1190"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1189",
3
+ "version": "1.3.1190",
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",
@@ -56,6 +56,7 @@ import yaml from 'js-yaml';
56
56
  import { articleIds, parseRegistryStructure } from './standards-registry-article-core.mjs';
57
57
  import {
58
58
  evaluateStandardsDirection,
59
+ evaluateOperatorReviewApproval,
59
60
  inventoryStandardsArticles,
60
61
  readCandidateApproverKey,
61
62
  readDirectionApprovalLedger,
@@ -599,6 +600,12 @@ function validateRootSelfWiring() {
599
600
  STANDARDS_DIRECTION_BASE_FILE: '${{ runner.temp }}/standards-registry-base.md',
600
601
  STANDARDS_DIRECTION_BASE_APPROVER_KEY_FILE: '${{ runner.temp }}/standards-direction-approver-base.pem',
601
602
  STANDARDS_DIRECTION_BASE_REVISION: "${{ github.event.pull_request.base.sha || github.event.before || format('{0}^', github.sha) }}",
603
+ // Path B (2026-08-22): the operator-review context the guard reads. Pinned
604
+ // here for the same reason as the others — this contract is deliberately
605
+ // EXACT so the CI wiring cannot be quietly rearranged, and adding an input
606
+ // to the check must therefore be a visible, declared edit rather than a
607
+ // silent one. The guard caught exactly that when this key was introduced.
608
+ STANDARDS_DIRECTION_REVIEW_FILE: '${{ runner.temp }}/standards-direction-review.json',
602
609
  };
603
610
  if (!exactKeys(job, ['name', 'runs-on', 'steps']) ||
604
611
  job.name !== 'Standards Enforcement Coverage' || job['runs-on'] !== 'ubuntu-latest' ||
@@ -611,6 +618,11 @@ function validateRootSelfWiring() {
611
618
  const setupStep = steps.find((step) => step?.uses === 'actions/setup-node@v4');
612
619
  const installStep = steps.find((step) => step?.run === 'npm ci --ignore-scripts');
613
620
  const baseStep = steps.find((step) => step?.id === 'area-audit-base');
621
+ // Path B's evidence-gathering step. Pinned into the ordered prefix so it
622
+ // cannot be dropped without this check failing — an absent step would make
623
+ // every operator approval read as UNAVAILABLE, which fails safe but would
624
+ // silently return the operator to needing a key.
625
+ const reviewStep = steps.find((step) => step?.name === 'Fetch operator review context (direction guard path B)');
614
626
  const expectedBaseRun = [
615
627
  'git cat-file -e "$BASE_SHA^{commit}"',
616
628
  'if git cat-file -e "$BASE_SHA:docs/standards-registry-area-audits.json"; then',
@@ -624,7 +636,7 @@ function validateRootSelfWiring() {
624
636
  '',
625
637
  ].join('\n');
626
638
  const expectedBaseSha = "${{ github.event.pull_request.base.sha || github.event.before || format('{0}^', github.sha) }}";
627
- const exactPrefix = [checkoutStep, setupStep, installStep, baseStep, checkStep];
639
+ const exactPrefix = [checkoutStep, setupStep, installStep, baseStep, reviewStep, checkStep];
628
640
  const ordered = exactPrefix.every((step, index) => step && steps[index] === step);
629
641
  const protectedBaseWired = ordered &&
630
642
  exactKeys(checkoutStep, ['uses', 'with']) && exactKeys(checkoutStep.with, ['fetch-depth']) && checkoutStep.with['fetch-depth'] === 0 &&
@@ -633,7 +645,11 @@ function validateRootSelfWiring() {
633
645
  exactKeys(baseStep, ['name', 'id', 'env', 'run']) &&
634
646
  baseStep.name === 'Resolve protected-base area ledger' &&
635
647
  exactKeys(baseStep.env, ['BASE_SHA']) && baseStep.env.BASE_SHA === expectedBaseSha &&
636
- baseStep.run === expectedBaseRun;
648
+ baseStep.run === expectedBaseRun &&
649
+ exactKeys(reviewStep, ['name', 'if', 'env', 'run']) &&
650
+ reviewStep.if === "github.event_name == 'pull_request'" &&
651
+ exactKeys(reviewStep.env, ['GH_TOKEN', 'PR', 'HEAD_SHA', 'OWNER_LOGIN', 'OWNER_TYPE', 'PR_AUTHOR', 'OUT']) &&
652
+ reviewStep.env.OUT === '${{ runner.temp }}/standards-direction-review.json';
637
653
  if (!protectedBaseWired) {
638
654
  errors.push('The Root self-wiring requires dependency install plus full-history protected-base extraction and required base env on the standards check');
639
655
  }
@@ -1317,6 +1333,31 @@ function compute() {
1317
1333
  if (key.revision !== base.revision) {
1318
1334
  key.errors.push('protected-base registry and approver trust root resolved from different revisions');
1319
1335
  }
1336
+ // PATH B context (2026-08-22). The WORKFLOW fetches the reviews and writes
1337
+ // them here, so this script performs no network I/O and stays testable.
1338
+ // Absent, unreadable or malformed => UNAVAILABLE, which falls back to
1339
+ // requiring a signed ratification. It never becomes an approval.
1340
+ const review = (() => {
1341
+ const file = process.env.STANDARDS_DIRECTION_REVIEW_FILE;
1342
+ if (!file) return null;
1343
+ let raw;
1344
+ try {
1345
+ const stat = fs.lstatSync(file);
1346
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('not a regular file');
1347
+ raw = JSON.parse(fs.readFileSync(file, 'utf8'));
1348
+ } catch (err) {
1349
+ return { approved: false, approvedBy: null, reason: `review context unreadable (${err.message})` };
1350
+ }
1351
+ if (!isPlainObject(raw)) return { approved: false, approvedBy: null, reason: 'review context is not an object' };
1352
+ return evaluateOperatorReviewApproval({
1353
+ reviews: raw.reviews,
1354
+ ownerLogin: raw.ownerLogin,
1355
+ ownerType: raw.ownerType,
1356
+ headSha: raw.headSha,
1357
+ prAuthorLogin: raw.prAuthorLogin,
1358
+ });
1359
+ })();
1360
+
1320
1361
  const assessed = evaluateStandardsDirection({
1321
1362
  baseMarkdown: base.markdown,
1322
1363
  candidateMarkdown: markdown,
@@ -1324,6 +1365,7 @@ function compute() {
1324
1365
  approverPublicKeyPem: key.pem,
1325
1366
  candidateApproverPublicKeyPem: candidateKey.pem,
1326
1367
  baseRevision: base.revision ?? 'unknown-protected-base',
1368
+ reviewApproval: review,
1327
1369
  });
1328
1370
  assessed.errors.unshift(...approvals.errors, ...candidateKey.errors, ...key.errors);
1329
1371
  if (assessed.errors.length > 0) assessed.status = 'not-proven';
@@ -218,6 +218,87 @@ function familyPopulation(articles) {
218
218
  return map;
219
219
  }
220
220
 
221
+ /**
222
+ * PATH B — an approving GitHub review from the repository owner.
223
+ *
224
+ * WHY THIS EXISTS (2026-08-22, topic 52222). Path A (an Ed25519 ratification)
225
+ * requires the operator to create, guard for years, and use a private key from
226
+ * a laptop. That key was never created, so NO registry change merged between
227
+ * this guard landing on 2026-08-17 and today. A guard nobody can satisfy stops
228
+ * the work it was meant to govern rather than the risk.
229
+ *
230
+ * WHAT MAKES THIS SOUND, AND WHAT DOES NOT. An adversarial review of the first
231
+ * draft established that no check the agent writes can constrain the agent: it
232
+ * authors this file and (before the ruleset changed) could push straight to
233
+ * main. So THIS FUNCTION IS NOT THE SECURITY BOUNDARY. The boundary is the
234
+ * repository ruleset — require-PR plus CODEOWNERS naming the operator on
235
+ * docs/STANDARDS-REGISTRY.md — which needs admin rights the agent does not
236
+ * hold. What this function adds is LEGIBILITY: it tells the pipeline, and the
237
+ * reader, which articles changed and whether the operator's approval is still
238
+ * attached to the bytes currently proposed. Removing it would not remove the
239
+ * protection; weakening it would not weaken the protection.
240
+ *
241
+ * Pure — the caller supplies the review list, so this stays testable and does
242
+ * no network I/O. Every uncertainty resolves to NOT approved.
243
+ */
244
+ export function evaluateOperatorReviewApproval({
245
+ reviews = null,
246
+ ownerLogin = null,
247
+ ownerType = null,
248
+ headSha = null,
249
+ prAuthorLogin = null,
250
+ } = {}) {
251
+ const no = (reason) => ({ approved: false, approvedBy: null, reason });
252
+
253
+ // An organization owner names no individual. Falling back to an in-repo
254
+ // allowlist here would reintroduce a trust root the agent can edit, which is
255
+ // the whole defect this design was rewritten around — so this is UNAVAILABLE,
256
+ // never a softer check.
257
+ if (ownerType !== 'User') return no(`owner is not an individual (type: ${ownerType ?? 'unknown'})`);
258
+ if (typeof ownerLogin !== 'string' || ownerLogin.length === 0) return no('owner login unavailable');
259
+ if (typeof headSha !== 'string' || !/^[0-9a-f]{40}$/i.test(headSha)) return no('head sha unavailable or malformed');
260
+ if (!Array.isArray(reviews)) return no('review list unavailable');
261
+
262
+ // The agent must not be able to approve its own change. GitHub already
263
+ // forbids this; checking it here too means the property does not depend on
264
+ // one platform behaviour staying the way it is today.
265
+ if (typeof prAuthorLogin === 'string' && prAuthorLogin.toLowerCase() === ownerLogin.toLowerCase()) {
266
+ return no('the pull request author is the owner — self-approval is not a different principal');
267
+ }
268
+
269
+ const ownerReviews = reviews
270
+ .filter((r) => r && typeof r === 'object' && typeof r.user?.login === 'string' &&
271
+ r.user.login.toLowerCase() === ownerLogin.toLowerCase())
272
+ .map((r) => ({
273
+ state: String(r.state || '').toUpperCase(),
274
+ commitId: typeof r.commit_id === 'string' ? r.commit_id : '',
275
+ at: typeof r.submitted_at === 'string' ? Date.parse(r.submitted_at) : NaN,
276
+ }))
277
+ // A review with no parseable timestamp cannot be ordered against a possible
278
+ // later withdrawal, so it is dropped rather than trusted.
279
+ .filter((r) => Number.isFinite(r.at))
280
+ .sort((a, b) => a.at - b.at);
281
+
282
+ if (ownerReviews.length === 0) return no(`no review from ${ownerLogin}`);
283
+
284
+ const onHead = ownerReviews.filter((r) => r.commitId.toLowerCase() === headSha.toLowerCase());
285
+ const approval = [...onHead].reverse().find((r) => r.state === 'APPROVED');
286
+ if (!approval) {
287
+ const stale = ownerReviews.some((r) => r.state === 'APPROVED');
288
+ return no(stale
289
+ ? `${ownerLogin} approved an earlier commit, not the current head — the proposed bytes changed after approval`
290
+ : `no APPROVED review from ${ownerLogin} on the current head`);
291
+ }
292
+
293
+ // A later withdrawal wins. DISMISSED is included because a dismissed approval
294
+ // is an approval the platform has already retracted.
295
+ const withdrawn = ownerReviews.find((r) => r.at >= approval.at &&
296
+ (r.state === 'CHANGES_REQUESTED' || r.state === 'DISMISSED'));
297
+ if (withdrawn) return no(`${ownerLogin} withdrew the approval (${withdrawn.state.toLowerCase()})`);
298
+
299
+ return { approved: true, approvedBy: ownerLogin, reason: `approved by ${ownerLogin} on the current head` };
300
+ }
301
+
221
302
  /**
222
303
  * Evaluate candidate constitutional direction against a protected base.
223
304
  * This exact export is imported by the pipeline and its negative-control test.
@@ -229,6 +310,12 @@ export function evaluateStandardsDirection({
229
310
  approverPublicKeyPem = '',
230
311
  candidateApproverPublicKeyPem = null,
231
312
  baseRevision = 'unknown-protected-base',
313
+ // PATH B (2026-08-22). The result of evaluateOperatorReviewApproval, or null
314
+ // when the caller could not obtain the review context (not a pull request, an
315
+ // API failure, a rate limit). NULL MEANS UNAVAILABLE, NOT APPROVED — an
316
+ // approval that cannot be verified is not an approval, so the article falls
317
+ // back to requiring path A.
318
+ reviewApproval = null,
232
319
  }) {
233
320
  const base = inventoryStandardsArticles(baseMarkdown);
234
321
  const candidate = inventoryStandardsArticles(candidateMarkdown);
@@ -280,8 +367,20 @@ export function evaluateStandardsDirection({
280
367
  payload.articleId === change.id);
281
368
  const displayName = change.after?.name ?? change.before?.name ?? change.id;
282
369
  if (matching.length === 0) {
370
+ // Path B before refusing: the operator's approving review on the exact
371
+ // head covers every article in the pull request, because a reviewer
372
+ // approves the diff in front of them. Per-article granularity was an
373
+ // artifact of the signing mechanism, not a requirement anyone stated.
374
+ if (reviewApproval?.approved === true) {
375
+ change.direction = change.kind === 'add' ? 'add' : change.kind === 'remove' ? 'remove' : 'unstated-review-approved';
376
+ change.approvedBy = reviewApproval.approvedBy;
377
+ change.approvedVia = 'github-review';
378
+ continue;
379
+ }
283
380
  errors.push(
284
- `${directionLabel(null, change.kind)} "${displayName}" (${change.id}) requires an independently signed direction ratification`,
381
+ `${directionLabel(null, change.kind)} "${displayName}" (${change.id}) requires the operator's approving review on this commit, ` +
382
+ `or an independently signed direction ratification` +
383
+ (reviewApproval?.reason ? ` — review path unavailable: ${reviewApproval.reason}` : ''),
285
384
  );
286
385
  continue;
287
386
  }
@@ -328,6 +427,9 @@ export function evaluateStandardsDirection({
328
427
  baseRevision,
329
428
  baseRegistrySha256,
330
429
  candidateRegistrySha256,
430
+ reviewApproval: reviewApproval
431
+ ? { approved: reviewApproval.approved === true, approvedBy: reviewApproval.approvedBy ?? null, reason: reviewApproval.reason ?? null }
432
+ : { approved: false, approvedBy: null, reason: 'review context unavailable' },
331
433
  changes: changes.map((change) => ({
332
434
  articleId: change.id,
333
435
  name: change.after?.name ?? change.before?.name ?? change.id,
@@ -336,6 +438,7 @@ export function evaluateStandardsDirection({
336
438
  change: change.kind,
337
439
  direction: change.direction ?? null,
338
440
  approvedBy: change.approvedBy ?? null,
441
+ approvedVia: change.approvedVia ?? (change.approvedBy ? 'signed-ratification' : null),
339
442
  })),
340
443
  population: {
341
444
  protectedBase: base.articles.length,
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-08-23T02:26:13.695Z",
5
- "instarVersion": "1.3.1189",
4
+ "generatedAt": "2026-08-23T02:53:10.208Z",
5
+ "instarVersion": "1.3.1190",
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": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
5
- "packageVersion": "1.3.1189",
5
+ "packageVersion": "1.3.1190",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "5d762bbb3b25d0fb34ecaa317a638f1eab6af13dc6a22f7392c054800ccee190",
2
+ "sha256": "e2275b904b85c7db59a699f899b730f164b89ca204813d9766d5b1c5233a92ba",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1189"
4
+ "packageVersion": "1.3.1190"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
3
3
  "articleCount": 88,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1189"
5
+ "packageVersion": "1.3.1190"
6
6
  }
@@ -0,0 +1,28 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The constitutional direction guard now accepts a second proof that a change to `docs/STANDARDS-REGISTRY.md` was approved: an approving GitHub review from the repository owner, on the exact head commit. The existing Ed25519 ratification path is unchanged and still takes precedence when both are present.
9
+
10
+ `evaluateOperatorReviewApproval` (pure, in `scripts/standards-direction-guard.mjs`) decides whether an owner approval is present, bound to the current head, and unwithdrawn. `standards-coverage.mjs` reads the review context from a file written by a new `pull_request`-only CI step, so the script performs no network I/O. The report gains `directionGuard.reviewApproval` and a per-change `approvedVia`.
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
+ 17 tests, both directions per arm: accepted on the current head; refused on an earlier commit; refused after a later `CHANGES_REQUESTED`; refused on `DISMISSED`; accepted on a re-approval that follows a resolved objection; refused for a non-owner; refused for self-approval; refused for `COMMENTED`; UNAVAILABLE rather than permissive on an organization-owned repository; and fails closed on a missing review list, a malformed head sha, an absent owner login, and an unparseable timestamp. Composition tests assert an unavailable context is not-approved *and names why*, and that an unchanged registry produces no change to approve.
23
+
24
+ ## Known Limits
25
+
26
+ **This code is not the security boundary and does not claim to be.** The agent authors it and the workflow that runs it, so it cannot constrain the agent. The boundary is the repository ruleset (require-PR on `main`, `require_code_owner_review`, empty bypass list) plus `.github/CODEOWNERS` naming the operator on the registry — all of which need admin rights the agent does not hold. This change adds legibility on top of that: which articles changed, and whether the approval still matches the proposed bytes.
27
+
28
+ The approval binds to the reviewed head, not to the bytes that finally land — a moved base or merge commit can differ. Mitigation is a platform setting (require branches up to date), recommended in `docs/specs/standards-approval-via-github-review.md` and not assumed here. `docs/STANDARDS-DIRECTION-GUARD.md` still describes path A as the only proof and is now incomplete; updating it is tracked separately.
@@ -0,0 +1,43 @@
1
+ # Letting the operator's approval count — Plain-English Overview
2
+
3
+ ## The problem in one breath
4
+
5
+ A check is supposed to stop the agent changing its own rulebook without the operator agreeing. The only proof it accepted was a cryptographic signature from a key the operator would have to create and guard for years. Nobody made that key, so the check refused everything — including the operator's own five approved changes. A check nobody can satisfy blocks the work rather than the risk.
6
+
7
+ ## What already exists
8
+
9
+ Since 17 August, any change to the rulebook has been refused for want of that signature. Nothing has slipped past it; nothing has got through it either.
10
+
11
+ As of this afternoon there is also a real lock, and it is not code: the repository now requires changes to the rulebook to be approved by the operator, enforced by GitHub against the filename. That setting needs owner rights the agent doesn't have.
12
+
13
+ ## What this adds
14
+
15
+ A second accepted proof: **the operator approving the pull request**. Three taps on a phone, on an account that already has two-factor. The signature route stays exactly as it was for anyone who prefers it.
16
+
17
+ ## The new pieces
18
+
19
+ When a change touches the rulebook, the pipeline asks GitHub a single question: did the owner approve *this exact version*? If yes, that counts. If no — or if the answer can't be obtained — it falls back to demanding the signature.
20
+
21
+ ## The safeguards
22
+
23
+ **The approval sticks to one exact version.** Push anything afterwards and it stops counting, so nobody can get a yes on one text and land another.
24
+
25
+ **A withdrawn approval counts as withdrawn** — and a re-approval after a resolved objection counts again, so an old objection can't block forever.
26
+
27
+ **Nobody can approve their own change**, checked here as well as by GitHub, so the property doesn't depend on one platform behaviour staying as it is.
28
+
29
+ **Anything unverifiable is refused.** An API failure means "we couldn't check", never "nobody objected".
30
+
31
+ ## The honest part
32
+
33
+ **This code is not the lock, and it says so.** The agent writes this check and the job that runs it, so a version of the agent that wanted around it would edit it rather than defeat it. The thing that actually stops that is the repository setting, which needs admin rights the agent doesn't have. What the code adds is legibility — it tells you which parts of the rulebook changed and whether your approval still matches them, so your three taps are informed rather than ceremonial.
34
+
35
+ **One gap left open on purpose.** The approval is tied to the version you read. If the project moves underneath while you're reading, what finally lands can differ slightly. A further setting closes that; it's recommended, not assumed.
36
+
37
+ ## What ships when
38
+
39
+ This can merge on its own — it touches no rulebook text, so it doesn't need an approval to install the approval path. Once it's in, the five amendments go green and merge on the operator's tap.
40
+
41
+ ## What you actually need to decide
42
+
43
+ Nothing here. The decision was made this afternoon; this is the implementation. The one open question is unrelated: whether to also require branches be up to date before merging, which would close the last gap above.
@@ -0,0 +1,80 @@
1
+ # Side-Effects Review — the direction guard accepts the operator's GitHub review
2
+
3
+ **Version / slug:** `direction-guard-review-approval`
4
+ **Date:** `2026-08-22`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `codex-cli — 4 adversarial rounds on the design (spec: docs/specs/standards-approval-via-github-review.md)`
7
+
8
+ ## Summary of the change
9
+
10
+ The constitutional direction guard accepted exactly one proof that a change to `docs/STANDARDS-REGISTRY.md` was approved: an Ed25519 signature over the exact article bytes, verified against a key pinned in the repository. That key shipped as an empty placeholder, so **no registry change merged between the guard landing on 2026-08-17 and 2026-08-22** — including the operator's own five ratified amendments (#1960). This adds a second accepted proof: an approving GitHub review from the repository owner, on the exact head commit. Path A is untouched. Files: `scripts/standards-direction-guard.mjs` (new `evaluateOperatorReviewApproval`, plus a `reviewApproval` input to `evaluateStandardsDirection`), `scripts/standards-coverage.mjs` (reads the review context from a file the workflow writes), `.github/workflows/ci.yml` (a step that fetches the reviews from the API).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `evaluateOperatorReviewApproval` (new) — **add** — decides whether an owner approval is present, current and unwithdrawn. Pure; no I/O.
15
+ - `evaluateStandardsDirection` per-article decision — **modify** — a changed article may now be satisfied by path B as well as path A.
16
+ - CI `Fetch operator review context` step — **add** — supplies the evidence. Cannot fail the job.
17
+
18
+ ## 1. Over-block
19
+
20
+ The added path only ever turns a refusal into an acceptance; it cannot refuse anything path A previously accepted. A signed ratification is still checked first and still wins. The one way this could over-block is if the new CI step failed the job — it cannot, because it tolerates API failure and writes an empty context, which the guard reads as UNAVAILABLE rather than as an error.
21
+
22
+ ## 2. Under-block
23
+
24
+ **This is the section that matters, and the honest answer is that the code does not carry the security.** An adversarial review of the first draft established it: the agent authors this file and the workflow that runs it, and (before 2026-08-22) could push directly to `main`. So a version of the agent that wanted to bypass this would edit it, not defeat it.
25
+
26
+ What actually blocks that is the repository ruleset the operator applied on 2026-08-22 — require-PR on `main`, `require_code_owner_review`, `bypass_actors: []` — plus `.github/CODEOWNERS` naming the operator on `docs/STANDARDS-REGISTRY.md`. Those need admin rights the agent does not hold (verified: a ruleset write returns 404 for its identity). **This change is legibility on top of that, not a lock.** Removing it would not remove the protection.
27
+
28
+ Within its own scope it under-blocks in one named way: it verifies that an approval exists on the reviewed head, not that the bytes reaching `main` are those bytes. A moved base or a merge commit can differ. Mitigation is a platform setting (require branches up to date), recommended in the spec and not assumed here.
29
+
30
+ ## 3. Level-of-abstraction fit
31
+
32
+ The decision lives beside the existing signature check, in the function that already owns "is this article's change approved?". The evidence-gathering lives in the workflow, so the script stays pure and testable and performs no network I/O. No new file, no new invocation point.
33
+
34
+ ## 4. Signal vs authority compliance
35
+
36
+ Compliant, and the change makes the split more honest than before. The script is a SIGNAL that reports which articles changed and whether an approval is attached. The AUTHORITY is the ruleset, which the agent cannot alter. The previous arrangement blurred this by describing an agent-authored check as the thing preventing self-amendment; the spec and the code comments now say plainly that it is not.
37
+
38
+ ## 4b. Judgment-point check
39
+
40
+ No judgment point is added. Every predicate is a deterministic comparison — login equality, sha equality, state string, timestamp ordering. No model is consulted, and nothing infers intent from prose.
41
+
42
+ ## 5. Interactions
43
+
44
+ - **`.github/CODEOWNERS`** (merged in #1962) is what makes the review exist at all: it causes GitHub to request the operator automatically. Verified live — pushing to #1960 auto-added `JKHeadley` as a reviewer and the PR went to `blocked`.
45
+ - **`docs/STANDARDS-DIRECTION-GUARD.md`** describes path A as the only proof. It is now incomplete; updating it is tracked, not done here, and named rather than left silent.
46
+ - **The `standards-coverage` report** gains `directionGuard.reviewApproval` and a per-change `approvedVia`, so a reader can tell which proof was used.
47
+
48
+ ## 6. External surfaces
49
+
50
+ One outbound call, from CI only: `GET /repos/{owner}/{repo}/pulls/{n}/reviews` using the workflow's own `github.token`. Read-only, no secrets beyond the ephemeral job token, and it never runs outside a `pull_request` event. No agent-side runtime surface, no route, no message.
51
+
52
+ ## 6b. Operator-surface quality
53
+
54
+ The operator's experience is three taps on a PR GitHub already asked them to review. The failure text names which proof is missing and, when the review path is unavailable, why — so a red check says "waiting for Justin" rather than something cryptic.
55
+
56
+ ## 7. Multi-machine posture
57
+
58
+ `unified` — trivially. Pure functions plus one CI-side read; no durable state, no per-machine state, no generated URL, no notification. Identical on any machine over the same inputs.
59
+
60
+ ## 8. Rollback cost
61
+
62
+ Near zero and additive to revert: delete the new function, the `reviewApproval` parameter, the CI step and the test, and the guard is byte-equivalent to today's behaviour. Because the ruleset carries the security, a revert does not reopen a hole — it only makes the pipeline less informative and returns the operator to needing a key.
63
+
64
+ ## Conclusion
65
+
66
+ Ship. It converts a guard nobody could satisfy into one the operator can satisfy in three taps, adds no authority, has no runtime or agent-facing surface, and is covered in both directions by 17 tests. The honest caveat, repeated because it is the point: the security is the ruleset, not this code.
67
+
68
+ ## Second-pass review
69
+
70
+ The DESIGN was adversarially reviewed over four rounds (5 → 3 → 1 → 0 security findings) before any of this was written; round 1 was invalidated wholesale and the design rewritten around its central finding. Record: `docs/specs/standards-approval-via-github-review.md`. The implementation follows that converged design.
71
+
72
+ ## Evidence pointers
73
+
74
+ - `tests/unit/standards-direction-review-approval.test.ts` — 17 tests. Both directions per arm: accepted on head; refused on an earlier commit; refused after withdrawal; refused on dismissal; **accepted on re-approval after an objection** (so a resolved objection cannot block forever); refused for a non-owner; refused for self-approval; refused for `COMMENTED`; unavailable (not permissive) for an organization owner; fails closed on a missing list, a malformed sha, an absent login, and an unparseable timestamp.
75
+ - Composition tests assert that an unavailable context is not-approved *and names why*, and that an unchanged registry produces no fabricated change to approve.
76
+ - Pre-existing local failures in `standards-coverage-ratchet.test.ts` (2) were verified to fail identically on clean `upstream/main` with this work stashed — a local git-ref artifact, not a regression from this change.
77
+
78
+ ## Class-Closure Declaration (display-only mirror)
79
+
80
+ The class is "a guard that cannot be satisfied, and is therefore not a guard." Closed here for the constitution: the operator now has a reachable proof. It is NOT closed generally — the same shape exists wherever a check depends on a credential nobody has created, and the dashboard-PIN finding (a dozen routes documented as operator-only, gated on a secret the agent reads) is the same class from the other direction. Tracked separately, open.