ruvnet-brain 4.3.26 → 4.3.27

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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +19 -4
  3. package/data/model-catalog.json +1 -1
  4. package/docs/RELEASE-NOTES-4.0.md +4 -3
  5. package/kb/capability-only.mjs +27 -0
  6. package/kb/capability-summaries/cognitum-ruos/CAPABILITIES.md +26 -0
  7. package/kb/verify-citation.mjs +16 -4
  8. package/package.json +5 -1
  9. package/plugin/.claude-plugin/plugin.json +1 -1
  10. package/plugin/.codex-plugin/plugin.json +1 -1
  11. package/plugin/docs/RELEASE-NOTES-4.0.md +4 -3
  12. package/plugin/hooks/codex-hooks.json +6 -1
  13. package/plugin/hooks/hook-contracts.json +24 -2
  14. package/plugin/hooks/hooks.json +6 -1
  15. package/plugin/scripts/capacity-aware-parallel-work.mjs +200 -0
  16. package/plugin/scripts/codex-hook-adapter.mjs +37 -0
  17. package/plugin/scripts/continuity-hook-policy.mjs +4 -0
  18. package/plugin/scripts/coverage-integrity.mjs +17 -0
  19. package/plugin/scripts/hook-shim.mjs +1 -0
  20. package/plugin/scripts/lesson-gate.mjs +4 -1
  21. package/plugin/scripts/project-progression-reader.mjs +12 -1
  22. package/plugin/scripts/project-progression-session-start.mjs +24 -6
  23. package/plugin/scripts/project-progression-store.mjs +76 -4
  24. package/plugin/skills/release-proof/SKILL.md +28 -4
  25. package/plugin/skills/release-proof/scripts/release-proof.mjs +48 -24
  26. package/scripts/brain-novice-50.mjs +14 -16
  27. package/scripts/build-bundle.mjs +2 -0
  28. package/scripts/corpus-dispatch-receipt.mjs +22 -0
  29. package/scripts/corpus-reconcile.mjs +26 -4
  30. package/scripts/doc-currency.mjs +12 -4
  31. package/scripts/eval-brain.mjs +7 -5
  32. package/scripts/gist-git-transport.mjs +218 -0
  33. package/scripts/gist-receipts.mjs +168 -38
  34. package/scripts/ingest-gists.mjs +5 -2
  35. package/scripts/installed-brain-health.mjs +99 -0
  36. package/scripts/public-inputs.mjs +2 -1
  37. package/scripts/public-verification-inputs.mjs +14 -6
  38. package/scripts/refresh-capability-only-store.mjs +143 -0
  39. package/scripts/release-vector.mjs +44 -20
  40. package/scripts/run-operational-benchmark.mjs +151 -0
  41. package/scripts/run-operational-benchmark.v3.mjs +194 -0
  42. package/scripts/self-update.mjs +2 -0
  43. package/scripts/source-coverage.mjs +6 -1
  44. package/scripts/sync-version.mjs +10 -2
  45. package/scripts/wired-check.mjs +73 -45
@@ -212,6 +212,33 @@ const stdout = merge(stdouts);
212
212
  let parsed = null;
213
213
  try { parsed = JSON.parse(stdout); } catch { /* a shared body may legitimately print prose */ }
214
214
 
215
+ function validPostToolUseOutput(value) {
216
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
217
+ const topLevelKeys = new Set([
218
+ 'continue', 'stopReason', 'suppressOutput', 'systemMessage',
219
+ 'terminalSequence', 'decision', 'reason', 'hookSpecificOutput',
220
+ ]);
221
+ if (Object.keys(value).some((key) => !topLevelKeys.has(key))) return false;
222
+ if (value.continue !== undefined && typeof value.continue !== 'boolean') return false;
223
+ if (value.stopReason !== undefined && typeof value.stopReason !== 'string') return false;
224
+ if (value.suppressOutput !== undefined && typeof value.suppressOutput !== 'boolean') return false;
225
+ if (value.systemMessage !== undefined && typeof value.systemMessage !== 'string') return false;
226
+ if (value.terminalSequence !== undefined && typeof value.terminalSequence !== 'string') return false;
227
+ if (value.decision !== undefined && value.decision !== 'block') return false;
228
+ if (value.decision === 'block' && (typeof value.reason !== 'string' || !value.reason.trim())) return false;
229
+ if (value.hookSpecificOutput !== undefined) {
230
+ const specific = value.hookSpecificOutput;
231
+ if (!specific || typeof specific !== 'object' || Array.isArray(specific)) return false;
232
+ const specificKeys = new Set([
233
+ 'hookEventName', 'additionalContext', 'updatedToolOutput', 'updatedMCPToolOutput',
234
+ ]);
235
+ if (Object.keys(specific).some((key) => !specificKeys.has(key))) return false;
236
+ if (specific.hookEventName !== 'PostToolUse') return false;
237
+ if (specific.additionalContext !== undefined && typeof specific.additionalContext !== 'string') return false;
238
+ }
239
+ return true;
240
+ }
241
+
215
242
  if (event === 'Stop') {
216
243
  const reason = parsed?.hookSpecificOutput?.additionalContext
217
244
  || parsed?.reason
@@ -241,6 +268,16 @@ if (!parsed) {
241
268
  process.exit(0);
242
269
  }
243
270
 
271
+ // A body can emit syntactically valid JSON that is still invalid for Codex's event-specific wire
272
+ // schema (wrong event name, unsupported fields, or bad field types). Preserve the advisory as text
273
+ // inside the known-good PostToolUse envelope instead of forwarding a payload the host rejects.
274
+ if (event === 'PostToolUse' && !validPostToolUseOutput(parsed)) {
275
+ process.stdout.write(JSON.stringify({
276
+ hookSpecificOutput: { hookEventName: event, additionalContext: stdout.trim() },
277
+ }));
278
+ process.exit(0);
279
+ }
280
+
244
281
  // `deny` is the only permissionDecision Codex accepts; `allow`, `ask` and the shared bodies' own
245
282
  // `defer` are all rejected by name. Strip, then drop an envelope that has nothing left to say.
246
283
  const decision = parsed?.hookSpecificOutput?.permissionDecision;
@@ -24,6 +24,7 @@
24
24
  * session-snapshot continuity capture at PreCompact claude
25
25
  * session-snapshot continuity capture at SessionEnd claude, codex
26
26
  * ground-ruvnet grounding injection at UserPromptSubmit claude, codex
27
+ * capacity-aware-parallel-work coordination guidance at UserPromptSubmit claude, codex
27
28
  * decision-gate write authorization at PreToolUse (write) claude, codex
28
29
  * grounding-stamp grounding receipt at PostToolUse claude, codex
29
30
  * grounding-turn-mark grounding turn marker at UserPromptSubmit claude, codex
@@ -125,6 +126,9 @@ export const CONTINUITY_EVENTS = Object.freeze({
125
126
  UserPromptSubmit: Object.freeze([
126
127
  registration('unprompted-speech', '*', ['claude', 'codex']),
127
128
  registration('ground-ruvnet', '*', ['claude', 'codex']),
129
+ // Context-only recommendation for clearly substantial, independently splittable work. The
130
+ // body samples bounded local pressure signals; it never starts workers or reports that it did.
131
+ registration('capacity-aware-parallel-work', '*', ['claude', 'codex']),
128
132
  // The "answered without searching" gate, half 1 of 2 (2026-09-12) — see grounding-turn-gate.mjs's
129
133
  // header for the full rationale. Records that ground-ruvnet's Gate 1 fired for this turn, since
130
134
  // Stop's own payload carries no prompt text for grounding-turn-gate to test.
@@ -89,6 +89,23 @@ export function validateGistAggregateReceipt({ receipt, passagesFile, expectedId
89
89
  || row.contentDigest !== digest(files)) {
90
90
  throw new Error(`gist ${id} receipt is incomplete or internally inconsistent`);
91
91
  }
92
+ const proofs = files.map((file) => file?.sourceGit).filter(Boolean);
93
+ if (proofs.length && (proofs.length !== files.length || proofs.some((proof) =>
94
+ !HEX40.test(String(proof.headSha || '')) || !HEX40.test(String(proof.treeSha || ''))
95
+ || !HEX40.test(String(proof.blobSha || '')) || proof.captureMethod !== 'public-bare-git-v1'
96
+ || proof.revisionKind !== 'git-commit' || proof.headSha !== row.versionSha
97
+ || proof.sourceObservationSha256 !== receipt.sourceObservationSha256
98
+ || !HEX64.test(String(proof.observedRowsSha256 || ''))
99
+ || !Number.isSafeInteger(proof.treeFileCount) || proof.treeFileCount !== files.length
100
+ || proof.observedTruncated !== false || proof.observedFileCount !== proof.treeFileCount
101
+ || proof.observed !== true || !HEX40.test(String(proof.observedRawRevisionSha || ''))
102
+ || !['blob', 'commit'].includes(proof.observedRawRevisionKind)
103
+ || proof.observedRawBlobSha !== proof.blobSha
104
+ || proofs.some((other) => other.headSha !== proof.headSha || other.treeSha !== proof.treeSha
105
+ || other.sourceObservationSha256 !== proof.sourceObservationSha256
106
+ || other.observedRowsSha256 !== proof.observedRowsSha256)))) {
107
+ throw new Error(`gist ${id} Git tree proof is incomplete or inconsistent`);
108
+ }
92
109
  const { receiptSha256, ...payload } = row;
93
110
  if (!HEX64.test(String(receiptSha256 || '')) || receiptSha256 !== digest(payload)) {
94
111
  throw new Error(`gist ${id} receipt digest differs`);
@@ -86,6 +86,7 @@ catch (e) { BRAIN_OFF = !(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')); }
86
86
  const TABLE = {
87
87
  'session-start': { file: 'session-start-core.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'partial' },
88
88
  'ground-ruvnet': { file: 'ground-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence', stdinBytes: 32768 },
89
+ 'capacity-aware-parallel-work': { file: 'capacity-aware-parallel-work.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'run', stdinBytes: 32768 },
89
90
  // ADR-063 / issue #103: `blocking` so an opt-in refusal can actually reach the host. The hook
90
91
  // still exits 0 for every user at the shipped default (managedMemoryBoundary=advise), so this
91
92
  // changes the CEILING of what it may do, not what it does.
@@ -203,7 +203,10 @@ const GATE_STATE_PATH = process.env.RUVNET_LESSON_GATE_STATE
203
203
  || path.join(CONFIG_ROOT, 'lesson-gate-state.json');
204
204
  const MAX_SHOWS = (() => {
205
205
  const n = Number(process.env.RUVNET_LESSON_MAX_SHOWS);
206
- return Number.isInteger(n) && n > 0 ? n : 3;
206
+ // A user-correction advisory is useful once in a session; repeating identical prose on each
207
+ // matching edit trains the recipient to ignore it. Keep an explicit override for installations
208
+ // that want a different cadence. Actual opted-in refusals remain exempt below.
209
+ return Number.isInteger(n) && n > 0 ? n : 1;
207
210
  })();
208
211
  const KEEP_SESSIONS = 20; // bound the state file to the most-recent sessions, same as anticipate.sh
209
212
  const SID = (typeof session === 'string' && session.trim())
@@ -91,6 +91,13 @@ const SCHEMA_FINGERPRINT = Object.freeze({
91
91
  ]),
92
92
  });
93
93
 
94
+ // The current managed store on the host also carries `session_id` on memory_entries, although the
95
+ // installed Ruflo memory_entries DDL does not declare it. This reader has no reason to attribute
96
+ // that column to Ruflo: it is an observed local compatibility shape only. It is safe for this
97
+ // projection because the reader never selects or interprets it; every required column must still
98
+ // match, and every other addition remains a hard fallback to the managed CLI.
99
+ const OBSERVED_COMPATIBILITY_COLUMNS = Object.freeze(['session_id']);
100
+
94
101
  /** The fingerprint this module requires, so the doctor and tests can name it exactly. */
95
102
  export function expectedSchemaFingerprint() {
96
103
  return { userVersion: SCHEMA_FINGERPRINT.userVersion, columns: [...SCHEMA_FINGERPRINT.columns] };
@@ -110,7 +117,11 @@ function assertSchemaFingerprint(database) {
110
117
  throw new ProgressionReaderUnavailable(
111
118
  `schema fingerprint mismatch: user_version ${userVersion} is not ${SCHEMA_FINGERPRINT.userVersion}`);
112
119
  }
113
- if (columns.join(',') !== SCHEMA_FINGERPRINT.columns.join(',')) {
120
+ const required = SCHEMA_FINGERPRINT.columns;
121
+ const compatibleObservedShape = columns.length === required.length + 1
122
+ && OBSERVED_COMPATIBILITY_COLUMNS.every((name) => columns.includes(name))
123
+ && required.every((name) => columns.includes(name));
124
+ if (columns.join(',') !== required.join(',') && !compatibleObservedShape) {
114
125
  const missing = SCHEMA_FINGERPRINT.columns.filter((name) => !columns.includes(name));
115
126
  const added = columns.filter((name) => !SCHEMA_FINGERPRINT.columns.includes(name));
116
127
  throw new ProgressionReaderUnavailable('schema fingerprint mismatch: memory_entries columns differ'
@@ -4,11 +4,15 @@ import { spawnSync } from 'node:child_process';
4
4
  import { ProjectProgressionStore } from './project-progression-store.mjs';
5
5
  import { resolveProjectStore } from './project-store-resolver.mjs';
6
6
  import { withProgressionReader } from './project-progression-reader.mjs';
7
+ import { STAGE_BUDGETS_MS } from './session-start-budget.mjs';
7
8
 
8
9
  const PROGRESSION_NAMESPACE = 'project-progression';
9
10
 
10
11
  export const SESSION_CONTINUITY_LIMIT_BYTES = 8 * 1024;
11
- export const SESSION_CONTINUITY_DEADLINE_MS = 2_500;
12
+ // Keep the restore's enforced wall-clock ceiling identical to the SessionStart latency contract.
13
+ // The fast path normally completes in milliseconds; when the managed CLI fallback cannot fit this
14
+ // stage, the caller reports UNKNOWN with its reason instead of consuming the rest of the hook budget.
15
+ export const SESSION_CONTINUITY_DEADLINE_MS = STAGE_BUDGETS_MS.restore;
12
16
 
13
17
  const RESTORED_HEADER = '[RuvNet Brain — PROJECT CONTINUITY RESTORED]';
14
18
  const UNKNOWN_HEADER = '[RuvNet Brain — PROJECT CONTINUITY UNKNOWN]';
@@ -21,7 +25,7 @@ const UNKNOWN_EXPLANATIONS = Object.freeze({
21
25
  'malformed-store': 'Structural AgentDB output is malformed or internally inconsistent.',
22
26
  'exact-readback': 'An exact-listed AgentDB row could not be read back by its exact key.',
23
27
  'outbox-replay': 'The durable progression outbox could not be replayed safely.',
24
- 'output-bound': 'The verified resume payload exceeds the host context bound.',
28
+ 'output-bound': 'The full checkpoint and a goal/action-preserving bounded summary do not fit the host context; no checkpoint state was injected.',
25
29
  'no-coherent-state': 'No coherent progression head survived validation.',
26
30
  'restore-failed': 'The exact structural restore did not complete.',
27
31
  // MEASURED, and named rather than hidden. The in-process read path costs ~15ms for six snapshots;
@@ -198,7 +202,9 @@ export function restoreProgressionForSession({
198
202
  const miss = (reason) => unknown(reason, { rowCount });
199
203
 
200
204
  const prefix = `${RESTORED_HEADER}\n`;
201
- const payloadLimit = maxOutputBytes - Buffer.byteLength(prefix, 'utf8');
205
+ // Reserve room for the status prefix, the bounded-summary explanation, and any pending-outbox
206
+ // notice appended after the payload. The final complete context is still checked below.
207
+ const payloadLimit = maxOutputBytes - Buffer.byteLength(prefix, 'utf8') - 400;
202
208
  if (!Number.isSafeInteger(payloadLimit) || payloadLimit < 1) return miss('output-bound');
203
209
 
204
210
  let store;
@@ -226,11 +232,23 @@ export function restoreProgressionForSession({
226
232
  // COMMITTED ROWS ONLY (ADR-073 §5). Replay is a write, a write is a `ruflo memory store`
227
233
  // process, and one of those costs more than this entire boundary's budget. Pending durable
228
234
  // snapshots are REPORTED below and replayed at the next capture boundary or by /checkpoint.
229
- const restored = store.restoreLatest({ maxOutputBytes: payloadLimit, replayPending: false });
235
+ const restored = store.restoreLatest({ maxOutputBytes: payloadLimit, replayPending: false, projectToBound: true });
230
236
  if (!validResume(restored)) return miss('malformed-store');
231
- const context = `${prefix}${restored.rendered}${pendingNotice(restored.pendingReplay)}`;
237
+ const summaryNotice = restored.projected
238
+ ? '\n[BOUNDED CONTINUITY SUMMARY] The merged current goal and next action are preserved exactly; '
239
+ + 'a null value means the journal heads conflict. '
240
+ + 'Other omitted details remain in the canonical AgentDB records; consult the listed head keys '
241
+ + 'and omission digests. Omitted fields are marked and are not empty.'
242
+ : '';
243
+ const context = `${prefix}${summaryNotice}\n${restored.rendered}${pendingNotice(restored.pendingReplay)}`;
232
244
  if (Buffer.byteLength(context, 'utf8') > maxOutputBytes) return miss('output-bound');
233
- return { status: 'restored', severity: 'info', pendingReplay: restored.pendingReplay, context };
245
+ return {
246
+ status: restored.projected ? 'restored-summary' : 'restored',
247
+ severity: restored.projected ? 'warning' : 'info',
248
+ degraded: restored.projected === true,
249
+ pendingReplay: restored.pendingReplay,
250
+ context,
251
+ };
234
252
  } catch (error) {
235
253
  // A structurally enumerated, genuinely empty namespace is normal for a newly adopted project.
236
254
  if (/no coherent progression state/i.test(String(error?.message ?? ''))
@@ -40,6 +40,70 @@ function requirePositiveInteger(value, label) {
40
40
  if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${label} must be a positive safe integer`);
41
41
  }
42
42
 
43
+ function omissionSummary(value) {
44
+ return {
45
+ count: Array.isArray(value) ? value.length : 1,
46
+ sha256: digestCanonical(value),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Deterministically reduce a verified resume payload to fit the host context without pretending
52
+ * omitted evidence is empty. The canonical snapshots remain in AgentDB; the projection retains the
53
+ * merged goal/action values, head keys and per-omission digests so the omitted values stay addressable.
54
+ * Returns null when the required resume identity and goal/action alone cannot fit.
55
+ */
56
+ export function projectResumePayloadToBound(payload, maxOutputBytes) {
57
+ requirePositiveInteger(maxOutputBytes, 'maxOutputBytes');
58
+ const summary = structuredClone(payload);
59
+ const omissions = [];
60
+ summary.projection = { mode: 'bounded-summary', omitted: omissions };
61
+ const recordOmission = (target, key, pathName) => {
62
+ const value = target[key];
63
+ if (value === undefined) return;
64
+ const digest = omissionSummary(value);
65
+ omissions.push({ path: pathName, ...digest });
66
+ target[key] = { omitted: true, ...digest };
67
+ };
68
+ const size = () => Buffer.byteLength(JSON.stringify(summary), 'utf8');
69
+
70
+ // Conflicts are important facts, but their full competing values can dominate the resume context.
71
+ // Keep each conflicting field, its count, and a digest of the exact competing values; top-level
72
+ // head keys remain, and canonical snapshots retain the source values. No winner is chosen.
73
+ if (size() > maxOutputBytes && Array.isArray(summary.state?.resumeConflicts)) {
74
+ const originals = summary.state.resumeConflicts;
75
+ summary.state.resumeConflicts = originals.map((conflict) => ({
76
+ field: conflict.field,
77
+ valueCount: (conflict.values ?? []).length,
78
+ valuesDigest: digestCanonical(conflict.values ?? []),
79
+ }));
80
+ omissions.push({
81
+ path: 'state.resumeConflicts[].values',
82
+ ...omissionSummary(originals.flatMap((conflict) => conflict.values ?? []).map((row) => row.value)),
83
+ });
84
+ }
85
+
86
+ if (size() > maxOutputBytes) recordOmission(summary.state, 'journalHeads', 'state.journalHeads');
87
+ // Least central detail first. currentGoal and nextAction are deliberately absent from this list.
88
+ const stateFields = [
89
+ 'commands', 'proofArtifacts', 'changedFiles', 'completed', 'untested', 'decisions',
90
+ 'plan', 'inProgress', 'blockers', 'failures', 'acceptanceContract', 'provenance',
91
+ 'evidence', 'activeStep', 'activeProcess', 'sourceIdentity',
92
+ ];
93
+ for (const field of stateFields) {
94
+ if (size() <= maxOutputBytes) break;
95
+ recordOmission(summary.state, field, `state.${field}`);
96
+ }
97
+ if (size() > maxOutputBytes && summary.evidence) {
98
+ recordOmission(summary, 'evidence', 'evidence');
99
+ }
100
+
101
+ // Never clip or replace the user goal or next action. If those plus the identity/omission ledger
102
+ // do not fit, refuse to call this a restore and let SessionStart emit explicit UNKNOWN.
103
+ if (size() > maxOutputBytes) return null;
104
+ return { payload: summary, rendered: JSON.stringify(summary), projected: true };
105
+ }
106
+
43
107
  function validatePage(page, { offset, pageSize, total }) {
44
108
  if (!plainRecord(page) || !Array.isArray(page.entries)
45
109
  || !Number.isSafeInteger(page.total) || page.total < 0
@@ -326,7 +390,8 @@ export class ProjectProgressionStore {
326
390
  * boundaries that already own a write budget. The outbox's fsync-then-commit ordering and its
327
391
  * replay-required semantics are untouched: nothing is dropped, only deferred.
328
392
  */
329
- restoreLatest({ pageSize = 100, maxEntries = 10_000, maxOutputBytes = 64 * 1024, replayPending = true } = {}) {
393
+ restoreLatest({ pageSize = 100, maxEntries = 10_000, maxOutputBytes = 64 * 1024,
394
+ replayPending = true, projectToBound = false } = {}) {
330
395
  requirePositiveInteger(maxOutputBytes, 'maxOutputBytes');
331
396
  if (replayPending) this.replay();
332
397
  const pendingReplay = replayPending ? 0 : this.pendingReplayCount();
@@ -361,10 +426,17 @@ export class ProjectProgressionStore {
361
426
  pendingReplay,
362
427
  },
363
428
  };
364
- const rendered = JSON.stringify(payload);
429
+ let rendered = JSON.stringify(payload);
430
+ let finalPayload = payload;
431
+ let projected = false;
365
432
  if (Buffer.byteLength(rendered, 'utf8') > maxOutputBytes) {
366
- throw new Error(`resume payload exceeds the ${maxOutputBytes}-byte output bound`);
433
+ const bounded = projectToBound ? projectResumePayloadToBound(payload, maxOutputBytes) : null;
434
+ if (!bounded) {
435
+ throw new Error(`resume payload${projectToBound ? ' and mandatory bounded summary' : ''}`
436
+ + ` exceeds the ${maxOutputBytes}-byte output bound`);
437
+ }
438
+ ({ payload: finalPayload, rendered, projected } = bounded);
367
439
  }
368
- return { payload, rendered, pendingReplay };
440
+ return { payload: finalPayload, rendered, pendingReplay, projected };
369
441
  }
370
442
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: release-proof
3
3
  description: Fail-closed exact-artifact release and deployment authority. Use before saying a release is ready, pushing a release commit, publishing npm packages, creating GitHub releases, deploying production, closing release-blocking issues, or claiming all gates are green. Requires clean immutable lineage, zero labeled release blockers, exact-SHA GitHub success, nonzero no-skip QE, packed-artifact host tests, installed Brain/RVF proof, and post-publication byte verification.
4
- updated: 2026-09-07
4
+ updated: 2026-09-20
5
5
  ---
6
6
 
7
7
  # Release Proof
@@ -37,9 +37,28 @@ green.
37
37
  ## Candidate seal
38
38
 
39
39
  Generate the candidate receipt in `.github/workflows/release-candidate-preflight.yml`; do not
40
- hand-author it. The preflight runs the long CI, integration, UX, and stranger lanes once on
41
- `release/**`, then emits the source-bound package and aggregate as
42
- `release-candidate-<exact SHA>`. Fast-forward that unchanged SHA to `main`.
40
+ hand-author it. Push the exact candidate commit to `release/<version>` to run the long CI,
41
+ integration, UX, and stranger lanes once. Require the successful exact-SHA artifact
42
+ `release-candidate-<SHA>` before promotion.
43
+
44
+ Then open a PR from that `release/**` branch to `main`. The release-branch `canonical-qa` and
45
+ `integration` consumers verify the producer receipt and report the required checks on the exact
46
+ candidate SHA. Wait for both required checks to pass on that SHA. Do **not** use the GitHub PR merge
47
+ button: merge, squash, and rebase all create a different commit identity and invalidate the sealed
48
+ artifact. Promote only with an ordinary non-force fast-forward push after confirming current `main`
49
+ is an ancestor of the candidate:
50
+
51
+ ```bash
52
+ git fetch origin main
53
+ git merge-base --is-ancestor origin/main "$CANDIDATE_SHA"
54
+ git push origin "$CANDIDATE_SHA:refs/heads/main"
55
+ test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$CANDIDATE_SHA"
56
+ ```
57
+
58
+ If the normal push is rejected, stop and repair branch-policy or required-check configuration; never
59
+ force-push, use an admin bypass, or substitute a merge-created SHA. Any source change requires a new
60
+ candidate preflight and artifact. The PR is review context; its merge button is not the promotion
61
+ mechanism.
43
62
 
44
63
  Dispatch `.github/workflows/protected-release.yml` only after the fast-forward. It is the sole
45
64
  publication controller: it proves current `origin/main` is the preflight SHA, selects the artifact
@@ -78,6 +97,11 @@ Only exit 0 permits “shipped,” “deployed,” “green,” or “ready.”
78
97
  seal fails, say `PUBLICATION DEGRADED`, preserve the previous known-good release, and repair or
79
98
  roll back through the release workflow.
80
99
 
100
+ `npm run release:proof -- --status --quick` is a live local/main diagnostic only. It deliberately
101
+ does not evaluate the release vector and therefore reports `INCOMPLETE` when that is its only
102
+ missing evidence. It is never a candidate receipt or publication authority. Use the exact candidate
103
+ receipt and workflow artifacts for release decisions.
104
+
81
105
  `scripts/release.mjs --publish` is intentionally unusable from a local shell or another workflow.
82
106
  Its invocation guard requires GitHub Actions workflow `protected-release`, the candidate receipt,
83
107
  and matching SHA/digest/version bindings before any push, tag, release, or npm action. The workflow
@@ -195,24 +195,42 @@ export function evaluatePublicationReceipt(candidate, publication) {
195
195
 
196
196
  export function evaluateLivePreflight(observed) {
197
197
  const failures = [];
198
+ const incomplete = [];
198
199
  if (observed?.dirty !== false) failures.push(fail('DIRTY_WORKTREE', 'local worktree is dirty'));
199
200
  if (observed?.localSha !== observed?.remoteSha) failures.push(fail('REMOTE_SHA_MISMATCH', 'local HEAD is not origin/main'));
200
- if (!Array.isArray(observed?.openIssues) || observed.openIssues.length > 0) failures.push(fail('OPEN_ISSUES', `${observed?.openIssues?.length ?? 'unknown'} issue(s) open`));
201
- if (!Array.isArray(observed?.failedRuns) || observed.failedRuns.length > 0) failures.push(fail('GITHUB_FAILURES', `${observed?.failedRuns?.length ?? 'unknown'} recent failed run(s)`));
201
+ const blockers = observed?.releaseBlockers;
202
+ if (!Array.isArray(blockers)) failures.push(fail('RELEASE_BLOCKERS_UNKNOWN', 'could not verify open issues labeled release-blocker'));
203
+ else if (blockers.length > 0) failures.push(fail('RELEASE_BLOCKERS_OPEN', `${blockers.length} release-blocker issue(s) remain open`));
204
+ const requiredChecks = observed?.requiredChecks;
205
+ if (!Array.isArray(requiredChecks) || requiredChecks.length === 0) {
206
+ failures.push(fail('REQUIRED_CHECKS_UNKNOWN', 'could not verify the required main-branch checks on the exact remote SHA'));
207
+ } else {
208
+ for (const check of requiredChecks) {
209
+ if (check?.status !== 'PASS') failures.push(fail('REQUIRED_CHECK_NOT_GREEN', `${check?.context || 'unknown'} is ${check?.status || 'UNKNOWN'} on the exact remote SHA`));
210
+ }
211
+ }
202
212
  if (observed?.activeSelfStore !== true) failures.push(fail('BRAIN_SELF_STORE_MISSING', 'installed active registry lacks ruvnet-brain'));
203
213
  if (observed?.branchEnforceAdmins !== true) failures.push(fail('ADMIN_BYPASS_ENABLED', 'main protection does not enforce required checks for admins'));
204
- if (observed?.productionProtected !== true) failures.push(fail('PRODUCTION_ENV_UNPROTECTED', 'production environment has no required reviewer protection'));
205
- if (observed?.releaseVector !== 'PASS') failures.push(fail('RELEASE_VECTOR_NOT_PASS', `release vector is ${observed?.releaseVector ?? 'UNKNOWN'}`));
206
- return { verdict: failures.length === 0 ? 'PASS' : 'FAIL', observed, failures };
214
+ if (observed?.productionProtected !== true) failures.push(fail('PRODUCTION_ENV_UNPROTECTED', 'production environment lacks an enforced protection rule'));
215
+ if (observed?.releaseVector === 'NOT_EVALUATED') incomplete.push(fail('RELEASE_VECTOR_NOT_EVALUATED', 'quick status skipped the release vector; run full status for that evidence'));
216
+ else if (observed?.releaseVector !== 'PASS') failures.push(fail('RELEASE_VECTOR_NOT_PASS', `release vector is ${observed?.releaseVector ?? 'UNKNOWN'}`));
217
+ return {
218
+ verdict: failures.length > 0 ? 'FAIL' : incomplete.length > 0 ? 'INCOMPLETE' : 'PASS',
219
+ observed,
220
+ failures,
221
+ incomplete,
222
+ };
207
223
  }
208
224
 
209
- export function latestRunsByWorkflow(runs) {
210
- const latest = new Map();
211
- for (const run of Array.isArray(runs) ? runs : []) {
212
- const prior = latest.get(run.workflowName);
213
- if (!prior || Number(run.databaseId || 0) > Number(prior.databaseId || 0)) latest.set(run.workflowName, run);
214
- }
215
- return [...latest.values()];
225
+ export function resolveRequiredChecks(requiredContexts, checkRuns, statuses) {
226
+ if (!Array.isArray(requiredContexts) || !Array.isArray(checkRuns) || !Array.isArray(statuses)) return null;
227
+ return requiredContexts.map(({ context, app_id: appId }) => {
228
+ const checkRun = checkRuns.find((row) => row.name === context && (appId == null || row.app?.id === appId));
229
+ const status = appId == null ? statuses.find((row) => row.context === context) : null;
230
+ const passed = (checkRun?.status === 'completed' && checkRun.conclusion === 'success') || status?.state === 'success';
231
+ const pending = checkRun?.status === 'queued' || checkRun?.status === 'in_progress' || status?.state === 'pending';
232
+ return { context, appId, status: passed ? 'PASS' : pending ? 'PENDING' : checkRun || status ? 'FAIL' : 'MISSING' };
233
+ });
216
234
  }
217
235
 
218
236
  function command(cmd, args, options = {}) {
@@ -235,15 +253,21 @@ export function collectLivePreflight({ root = process.cwd(), repo = 'stuinfla/ru
235
253
  const git = (args) => command('git', args, { cwd: root });
236
254
  const localSha = git(['rev-parse', 'HEAD']).stdout.trim();
237
255
  const dirty = Boolean(git(['status', '--porcelain']).stdout.trim());
238
- git(['fetch', '--quiet', 'origin', 'main']);
239
- const remoteSha = git(['rev-parse', 'origin/main']).stdout.trim();
240
- const openIssues = jsonCommand('gh', ['issue', 'list', '--repo', repo, '--state', 'open', '--limit', '100', '--json', 'number,title,url']) ?? null;
241
- const runs = jsonCommand('gh', ['run', 'list', '--repo', repo, '--limit', '50', '--json', 'databaseId,workflowName,headSha,status,conclusion,url']) ?? null;
242
- const failedRuns = Array.isArray(runs)
243
- ? latestRunsByWorkflow(runs.filter((run) => run.headSha === remoteSha))
244
- .filter((run) => run.status !== 'completed' || run.conclusion !== 'success')
245
- : null;
256
+ const fetched = git(['fetch', '--quiet', 'origin', 'main']);
257
+ const remoteSha = fetched.status === 0 ? git(['rev-parse', 'origin/main']).stdout.trim() : null;
258
+ const releaseBlockers = jsonCommand('gh', ['issue', 'list', '--repo', repo, '--state', 'open', '--label', 'release-blocker', '--limit', '100', '--json', 'number,title,url']) ?? null;
246
259
  const protection = jsonCommand('gh', ['api', `repos/${repo}/branches/main/protection`]);
260
+ const requiredChecksConfig = protection?.required_status_checks?.checks;
261
+ const requiredContexts = Array.isArray(requiredChecksConfig)
262
+ ? requiredChecksConfig
263
+ : protection?.required_status_checks?.contexts?.map((context) => ({ context, app_id: null }));
264
+ const checkRuns = remoteSha
265
+ ? jsonCommand('gh', ['api', `repos/${repo}/commits/${remoteSha}/check-runs?per_page=100`])?.check_runs
266
+ : null;
267
+ const statuses = remoteSha
268
+ ? jsonCommand('gh', ['api', `repos/${repo}/commits/${remoteSha}/status`])?.statuses
269
+ : null;
270
+ const requiredChecks = resolveRequiredChecks(requiredContexts, checkRuns, statuses);
247
271
  const environments = jsonCommand('gh', ['api', `repos/${repo}/environments`]);
248
272
  const production = environments?.environments?.find((environment) => environment.name === 'Production – ruvnet-brain');
249
273
  let activeSelfStore = false;
@@ -256,13 +280,13 @@ export function collectLivePreflight({ root = process.cwd(), repo = 'stuinfla/ru
256
280
  if (includeVector) {
257
281
  const vector = jsonCommand(process.execPath, [path.join(root, 'scripts/release-vector.mjs'), '--json'], { cwd: root });
258
282
  releaseVector = vector?.verdict || 'UNKNOWN';
259
- }
283
+ } else releaseVector = 'NOT_EVALUATED';
260
284
  return {
261
285
  localSha,
262
286
  remoteSha,
263
287
  dirty,
264
- openIssues,
265
- failedRuns,
288
+ releaseBlockers,
289
+ requiredChecks,
266
290
  activeSelfStore,
267
291
  branchEnforceAdmins: protection?.enforce_admins?.enabled === true,
268
292
  productionProtected: Array.isArray(production?.protection_rules) && production.protection_rules.length > 0 && production.can_admins_bypass === false,
@@ -280,7 +304,7 @@ export function main(args = process.argv.slice(2)) {
280
304
  if (args.includes('--status')) {
281
305
  const result = evaluateLivePreflight(collectLivePreflight({ includeVector: !args.includes('--quick') }));
282
306
  console.log(JSON.stringify(result, null, 2));
283
- return result.verdict === 'PASS' ? 0 : 1;
307
+ return result.verdict === 'PASS' ? 0 : result.verdict === 'INCOMPLETE' ? 2 : 1;
284
308
  }
285
309
  const candidatePath = argument(args, '--candidate');
286
310
  const publicationPath = argument(args, '--publication');
@@ -69,23 +69,21 @@ function percentile(values, fraction) {
69
69
  return sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)];
70
70
  }
71
71
 
72
- function grade(spec, text, elapsedMs, transportOk) {
73
- const repo = spec.repo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
74
- const expectedRepoCited = new RegExp(
75
- `(?:repo\\s*=\\s*["']?${repo}\\b|path\\s*:\\s*${repo}/|capability-cards\\.md#${repo}\\b)`,
76
- 'i',
77
- ).test(text);
78
- const cited = /(?:#\d+\s+repo=[a-z0-9._-]+|path\s*:\s*[a-z0-9._-]+\/|capability-cards\.md#[a-z0-9._-]+)/i
79
- .test(text);
80
- const useful = spec.required.test(text);
72
+ export function grade(spec, text, elapsedMs, transportOk) {
73
+ const top = /^#1\s+repo=(\S+)(?:\s+ce=(-?[\d.]+))?/m.exec(text);
74
+ const expectedRepoCited = [...String(text).matchAll(/^#\d+\s+repo=(\S+)/gm)]
75
+ .some((match) => match[1].toLowerCase() === spec.repo.toLowerCase());
76
+ const cited = /^#\d+\s+repo=[a-z0-9._-]+/im.test(text);
77
+ const abstained = !top || (top[2] !== undefined && Number(top[2]) < 0);
78
+ // This regex is retained as a visible keyword signal only. It is not semantic accuracy and it
79
+ // cannot make a row effective without a citation from the independently expected owner.
80
+ const keywordSignal = spec.required.test(text);
81
81
  const unsupportedAbsence = /\b(?:does not exist|must be built|you need to build it)\b/i.test(text);
82
82
  const evidenceQualified = /EVIDENCE:\s*THIN|NOT PROVEN|PROPOSED|curated-capability-card|THIS QUERY found nothing/i.test(text);
83
83
  const honest = !unsupportedAbsence || evidenceQualified;
84
84
  const latencyPoints = elapsedMs <= 4000 ? 10 : elapsedMs <= 8000 ? 5 : 0;
85
- const score = (transportOk ? 20 : 0) + (text.length > 200 ? 20 : 0)
86
- + (cited ? 20 : 0) + (useful ? 20 : 0) + (honest ? 10 : 0) + latencyPoints;
87
- const effective = transportOk && cited && useful && honest && elapsedMs <= 4000;
88
- return { score, cited, expectedRepoCited, useful, honest, effective, latencyPoints };
85
+ const effective = transportOk && cited && expectedRepoCited && honest && !abstained && elapsedMs <= 4000;
86
+ return { cited, expectedRepoCited, keywordSignal, honest, abstained, effective, latencyPoints };
89
87
  }
90
88
 
91
89
  async function main() {
@@ -153,7 +151,7 @@ async function main() {
153
151
  answer: text,
154
152
  ...grade(spec, text, elapsedMs, transportOk),
155
153
  };
156
- process.stdout.write(`${String(spec.id).padStart(2, '0')} ${String(results[index].score).padStart(3)} ${String(elapsedMs).padStart(6)}ms ${spec.category.padEnd(14)} ${spec.query}\n`);
154
+ process.stdout.write(`${String(spec.id).padStart(2, '0')} ${results[index].effective ? 'ELIGIBLE' : 'MISS '} ${String(elapsedMs).padStart(6)}ms ${spec.category.padEnd(14)} ${spec.query}\n`);
157
155
  }
158
156
  }));
159
157
 
@@ -170,17 +168,17 @@ async function main() {
170
168
  readinessMs,
171
169
  passed: results.filter((result) => result.effective).length,
172
170
  under4s: results.filter((result) => result.elapsedMs <= 4000).length,
173
- averageScore: Math.round(results.reduce((sum, result) => sum + result.score, 0) / results.length),
174
171
  averageMs: Math.round(times.reduce((sum, value) => sum + value, 0) / times.length),
175
172
  medianMs: percentile(times, 0.5),
176
173
  p95Ms: percentile(times, 0.95),
177
174
  longestMs: Math.max(...times),
178
175
  stderr: stderr.trim(),
176
+ gradingScope: 'Operational eligibility only: transport, citation presence, expected-owner citation, honest uncertainty, and latency. keywordSignal is diagnostic only; this report does not measure semantic answer accuracy.',
179
177
  results,
180
178
  };
181
179
  const output = path.join(ROOT, 'data/novice-50-report.json');
182
180
  fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
183
- console.log(`SUMMARY ${report.passed}/50 effective · ${report.under4s}/50 under 4s · score ${report.averageScore}/100 · avg ${report.averageMs}ms · p95 ${report.p95Ms}ms · max ${report.longestMs}ms`);
181
+ console.log(`SUMMARY ${report.passed}/50 eligible · ${report.under4s}/50 under 4s · avg ${report.averageMs}ms · p95 ${report.p95Ms}ms · max ${report.longestMs}ms`);
184
182
  console.log(`REPORT ${output}`);
185
183
  }
186
184
 
@@ -66,6 +66,7 @@ import { validateCoverageLedger } from './coverage-integrity.mjs';
66
66
  // The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
67
67
  // sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
68
68
  import { orgRepoCount } from './org-repo-count.mjs';
69
+ import { assertCapabilityOnlyStore } from '../kb/capability-only.mjs';
69
70
 
70
71
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
71
72
 
@@ -742,6 +743,7 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
742
743
  }
743
744
 
744
745
  const selectedResults = discovered.map((name) => {
746
+ assertCapabilityOnlyStore(corpus, name);
745
747
  const folded = name.toLowerCase();
746
748
  const generation = ledgerIn.stores?.[name]
747
749
  || Object.entries(ledgerIn.stores || {}).find(([key]) => key.toLowerCase() === folded)?.[1];
@@ -0,0 +1,22 @@
1
+ // Correlation is not authorization: protected-release still enforces its own release authority.
2
+ import fs from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ export function selectCorpusDispatch(runs, { sha, dispatchId, notBefore }) {
6
+ if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^corpus-\d+-\d+$/.test(dispatchId || '')
7
+ || !Number.isFinite(Date.parse(notBefore))) throw new Error('invalid corpus dispatch identity');
8
+ const matches = runs.filter(run => run.event === 'workflow_dispatch' && run.head_sha === sha
9
+ && run.head_branch === 'main' && run.display_title === `protected-release corpus ${dispatchId}`
10
+ && Date.parse(run.created_at) >= Date.parse(notBefore));
11
+ if (matches.length > 1) throw new Error('multiple runs claim the corpus dispatch identity');
12
+ return matches[0] || null;
13
+ }
14
+
15
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
16
+ const response = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
17
+ const run = selectCorpusDispatch(response.workflow_runs || [], {
18
+ sha: process.env.CANDIDATE_SHA, dispatchId: process.env.CORPUS_DISPATCH_ID,
19
+ notBefore: process.env.DISPATCHED_AT,
20
+ });
21
+ if (run) console.log(JSON.stringify(run));
22
+ }