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.
- package/README.md +2 -2
- package/bin/install.mjs +19 -4
- package/data/model-catalog.json +1 -1
- package/docs/RELEASE-NOTES-4.0.md +4 -3
- package/kb/capability-only.mjs +27 -0
- package/kb/capability-summaries/cognitum-ruos/CAPABILITIES.md +26 -0
- package/kb/verify-citation.mjs +16 -4
- package/package.json +5 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/docs/RELEASE-NOTES-4.0.md +4 -3
- package/plugin/hooks/codex-hooks.json +6 -1
- package/plugin/hooks/hook-contracts.json +24 -2
- package/plugin/hooks/hooks.json +6 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +200 -0
- package/plugin/scripts/codex-hook-adapter.mjs +37 -0
- package/plugin/scripts/continuity-hook-policy.mjs +4 -0
- package/plugin/scripts/coverage-integrity.mjs +17 -0
- package/plugin/scripts/hook-shim.mjs +1 -0
- package/plugin/scripts/lesson-gate.mjs +4 -1
- package/plugin/scripts/project-progression-reader.mjs +12 -1
- package/plugin/scripts/project-progression-session-start.mjs +24 -6
- package/plugin/scripts/project-progression-store.mjs +76 -4
- package/plugin/skills/release-proof/SKILL.md +28 -4
- package/plugin/skills/release-proof/scripts/release-proof.mjs +48 -24
- package/scripts/brain-novice-50.mjs +14 -16
- package/scripts/build-bundle.mjs +2 -0
- package/scripts/corpus-dispatch-receipt.mjs +22 -0
- package/scripts/corpus-reconcile.mjs +26 -4
- package/scripts/doc-currency.mjs +12 -4
- package/scripts/eval-brain.mjs +7 -5
- package/scripts/gist-git-transport.mjs +218 -0
- package/scripts/gist-receipts.mjs +168 -38
- package/scripts/ingest-gists.mjs +5 -2
- package/scripts/installed-brain-health.mjs +99 -0
- package/scripts/public-inputs.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +14 -6
- package/scripts/refresh-capability-only-store.mjs +143 -0
- package/scripts/release-vector.mjs +44 -20
- package/scripts/run-operational-benchmark.mjs +151 -0
- package/scripts/run-operational-benchmark.v3.mjs +194 -0
- package/scripts/self-update.mjs +2 -0
- package/scripts/source-coverage.mjs +6 -1
- package/scripts/sync-version.mjs +10 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
429
|
+
let rendered = JSON.stringify(payload);
|
|
430
|
+
let finalPayload = payload;
|
|
431
|
+
let projected = false;
|
|
365
432
|
if (Buffer.byteLength(rendered, 'utf8') > maxOutputBytes) {
|
|
366
|
-
|
|
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-
|
|
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.
|
|
41
|
-
|
|
42
|
-
`release-candidate-<
|
|
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
|
-
|
|
201
|
-
if (!Array.isArray(
|
|
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
|
|
205
|
-
if (observed?.releaseVector
|
|
206
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
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
|
|
74
|
-
const expectedRepoCited =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
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
|
|
86
|
-
|
|
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')} ${
|
|
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
|
|
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
|
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -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
|
+
}
|