@lifeaitools/rdc-skills 0.24.38 → 0.24.39
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/.claude/settings.json +15 -15
- package/.claude-plugin/marketplace.json +21 -21
- package/.claude-plugin/plugin.json +1371 -1371
- package/.github/workflows/publish.yml +34 -34
- package/.github/workflows/self-test.yml +58 -58
- package/CHANGELOG.md +310 -310
- package/LICENSE +21 -21
- package/MANIFEST.md +221 -221
- package/README.md +377 -377
- package/README.sandbox.md +3 -3
- package/assets/watcher/viewer.html +164 -164
- package/bin/rdc-skills-mcp.mjs +316 -316
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -152
- package/commands/design.md +31 -31
- package/commands/edit.md +28 -28
- package/commands/fixit.md +124 -124
- package/commands/handoff.md +173 -173
- package/commands/help.md +95 -95
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/release.md +49 -49
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/self-test.md +113 -113
- package/commands/status.md +86 -86
- package/commands/watch.md +98 -98
- package/commands/workitems.md +137 -137
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +295 -295
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +281 -281
- package/guides/agents/verify.md +151 -151
- package/guides/agents/viz.md +106 -106
- package/guides/backend.md +146 -146
- package/guides/content.md +147 -147
- package/guides/cs2.md +190 -190
- package/guides/data.md +123 -123
- package/guides/design.md +116 -116
- package/guides/engineering-behavior.md +43 -43
- package/guides/escalation-protocol.md +125 -125
- package/guides/frontend.md +151 -151
- package/guides/history-md-spec.md +297 -297
- package/guides/infrastructure.md +179 -179
- package/guides/lessons-learned-spec.md +153 -153
- package/guides/output-contract.md +108 -108
- package/guides/publish-md-spec.md +289 -289
- package/guides/rdc-skills-startup.md +30 -30
- package/guides/verify.md +11 -11
- package/hooks/check-cwd.js +31 -31
- package/hooks/check-rdc-environment.js +164 -164
- package/hooks/check-services.js +6 -6
- package/hooks/check-stale-work-items.js +19 -19
- package/hooks/foreground-process-gate.js +128 -128
- package/hooks/gate-watchdog-selfcheck.js +257 -257
- package/hooks/hook-logger.js +25 -25
- package/hooks/lib/run-evidence-gate.mjs +241 -241
- package/hooks/no-stop-open-epics.js +127 -127
- package/hooks/post-tool-batch-gate.js +203 -203
- package/hooks/post-work-check.js +21 -21
- package/hooks/postcompact-log.js +13 -13
- package/hooks/precompact-log.js +13 -13
- package/hooks/rate-limit-retry.js +46 -46
- package/hooks/rdc-invocation-marker.js +157 -157
- package/hooks/rdc-output-contract-gate.js +94 -94
- package/hooks/require-work-item-on-commit.js +294 -294
- package/hooks/restart-brief.js +19 -19
- package/hooks/run-hidden-hook.ps1 +47 -47
- package/hooks/task-completed-gate.js +274 -274
- package/hooks/work-item-exit-gate.js +944 -944
- package/lib/catalog.mjs +236 -236
- package/lib/cloud-rewrite.mjs +155 -155
- package/package.json +56 -56
- package/rules/work-items-rpc.md +520 -520
- package/scaffold/templates/HISTORY.md.template +39 -39
- package/scaffold/templates/PUBLISH.md.template +21 -21
- package/scaffold/templates/brochure-studio-default.html +70 -70
- package/scripts/acceptance.mjs +502 -502
- package/scripts/fixtures/guides/bad-guide.md +15 -15
- package/scripts/fixtures/guides-clean/good-guide.md +16 -16
- package/scripts/install-rdc-skills.js +1289 -1289
- package/scripts/install.ps1 +202 -202
- package/scripts/install.sh +132 -132
- package/scripts/lib/assertions.mjs +287 -287
- package/scripts/lib/manifest-schema.mjs +754 -754
- package/scripts/lib/runner.mjs +465 -465
- package/scripts/lib/sandbox.mjs +435 -435
- package/scripts/prepack.mjs +32 -32
- package/scripts/rdc-brochure.mjs +464 -464
- package/scripts/rdc-design-cli.mjs +134 -134
- package/scripts/rebuild-mcp.mjs +107 -107
- package/scripts/self-test.mjs +1460 -1460
- package/scripts/stamp-git-sha.mjs +29 -29
- package/scripts/test-guide-validator.mjs +196 -196
- package/scripts/test-rdc-hooks.mjs +145 -145
- package/scripts/uninstall.ps1 +77 -77
- package/scripts/uninstall.sh +69 -69
- package/scripts/update.ps1 +43 -43
- package/scripts/update.sh +43 -43
- package/scripts/validate-place-histories.js +461 -461
- package/scripts/validate-publish-manifests.js +424 -424
- package/scripts/watch-init.mjs +100 -100
- package/skills/brochure/SKILL.md +107 -107
- package/skills/build/SKILL.md +563 -563
- package/skills/channel-formatter/SKILL.md +533 -533
- package/skills/co-develop/SKILL.md +196 -196
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +140 -140
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +211 -211
- package/skills/design/reference/ownership.md +16 -16
- package/skills/design/reference/rampa.md +92 -92
- package/skills/design/reference/studio-model.md +153 -153
- package/skills/edit/SKILL.md +98 -98
- package/skills/fixit/SKILL.md +165 -165
- package/skills/fs-mcp/SKILL.md +148 -148
- package/skills/handoff/SKILL.md +236 -200
- package/skills/help/SKILL.md +143 -143
- package/skills/housekeeping/SKILL.md +189 -189
- package/skills/lifeai-brochure-author/SKILL.md +340 -340
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -345
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/rdc-brochurify/SKILL.md +245 -245
- package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +152 -152
- package/skills/rpms-filemap/SKILL.cloud.md +111 -111
- package/skills/rpms-filemap/SKILL.md +111 -111
- package/skills/self-test/SKILL.md +132 -132
- package/skills/status/SKILL.md +99 -99
- package/skills/terminal-config/SKILL.md +62 -62
- package/skills/tests/MATRIX.md +54 -54
- package/skills/tests/README.md +47 -47
- package/skills/tests/rdc-brochure.test.json +34 -34
- package/skills/tests/rdc-build.test.json +36 -36
- package/skills/tests/rdc-channel-formatter.test.json +45 -45
- package/skills/tests/rdc-co-develop.test.json +29 -29
- package/skills/tests/rdc-collab.test.json +29 -29
- package/skills/tests/rdc-convert.test.json +35 -35
- package/skills/tests/rdc-deploy.test.json +30 -30
- package/skills/tests/rdc-design.test.json +27 -27
- package/skills/tests/rdc-edit.test.json +29 -29
- package/skills/tests/rdc-fixit.test.json +36 -36
- package/skills/tests/rdc-fs-mcp.test.json +36 -36
- package/skills/tests/rdc-handoff.test.json +28 -28
- package/skills/tests/rdc-help.test.json +29 -29
- package/skills/tests/rdc-housekeeping.test.json +31 -31
- package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
- package/skills/tests/rdc-overnight.test.json +37 -37
- package/skills/tests/rdc-plan.test.json +27 -27
- package/skills/tests/rdc-preplan.test.json +31 -31
- package/skills/tests/rdc-prototype.test.json +28 -28
- package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
- package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
- package/skills/tests/rdc-release.test.json +29 -29
- package/skills/tests/rdc-report.test.json +28 -28
- package/skills/tests/rdc-review.test.json +29 -29
- package/skills/tests/rdc-rpms-filemap.test.json +28 -28
- package/skills/tests/rdc-self-test.test.json +24 -24
- package/skills/tests/rdc-status.test.json +29 -29
- package/skills/tests/rdc-terminal-config.test.json +29 -29
- package/skills/tests/rdc-watch.test.json +24 -24
- package/skills/tests/rdc-workitems.test.json +27 -27
- package/skills/watch/SKILL.md +97 -97
- package/skills/workitems/SKILL.md +151 -151
- package/tests/acceptance.test.mjs +59 -59
- package/tests/channel-formatter.contract.test.mjs +251 -251
- package/tests/curl-surface.test.mjs +289 -289
- package/tests/harness-gates.test.mjs +325 -325
- package/tests/help-surface.test.mjs +61 -61
- package/tests/install-rdc-skills.test.mjs +49 -49
- package/tests/manifest-contract-fields.test.mjs +78 -78
- package/tests/mcp.test.mjs +271 -271
- package/tests/require-work-item-on-commit.test.mjs +162 -162
- package/tests/run-evidence-gate.test.mjs +82 -82
- package/tests/skill-test-matrix.test.mjs +66 -66
- package/tests/validate-skills.js +27 -27
- package/tests/work-item-exit-gate-l2.test.mjs +368 -368
- package/tests/work-item-exit-gate-l3.test.mjs +197 -197
|
@@ -1,241 +1,241 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* run-evidence-gate.mjs — Truth Gate 3.0 Layer 2, the FUSED evidence primitive.
|
|
4
|
-
*
|
|
5
|
-
* Nidus / D4 principle: "you cannot submit evidence without running the test."
|
|
6
|
-
* This is ONE atomic call that (1) RUNS a verification command, (2) HASHES its
|
|
7
|
-
* captured output server-side, and (3) records the verdict TOGETHER with the
|
|
8
|
-
* hash and a timestamp. Evidence and execution are produced in the same call —
|
|
9
|
-
* an agent can never hand the gate a hash for a run that never happened.
|
|
10
|
-
*
|
|
11
|
-
* The output of runEvidenceGate() is the only legitimate shape of a
|
|
12
|
-
* machine-parseable `verification` artifact: it carries the exact command, the
|
|
13
|
-
* exit code, an SHA-256 of stdout+stderr, and a pass/fail verdict the CALLER
|
|
14
|
-
* did not author. The exit-gate's L2 verifier recognises this shape (and a few
|
|
15
|
-
* other captured-artifact shapes) and rejects anything that is bare prose.
|
|
16
|
-
*
|
|
17
|
-
* Pure-ish: it shells out to run the command but has no DB or network side
|
|
18
|
-
* effects, so it is unit-testable offline.
|
|
19
|
-
*/
|
|
20
|
-
'use strict';
|
|
21
|
-
|
|
22
|
-
import { spawnSync } from 'node:child_process';
|
|
23
|
-
import { createHash } from 'node:crypto';
|
|
24
|
-
|
|
25
|
-
/** Stable SHA-256 of a string (server-side hash — the caller cannot forge it). */
|
|
26
|
-
export function hashOutput(text) {
|
|
27
|
-
return createHash('sha256').update(String(text == null ? '' : text), 'utf8').digest('hex');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* The discriminant marker stamped on every fused artifact. The exit-gate keys
|
|
32
|
-
* on this to recognise a real run-and-attest result vs. an agent-typed string.
|
|
33
|
-
*/
|
|
34
|
-
export const EVIDENCE_KIND = 'run-evidence-gate/v1';
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Run a verification command and atomically produce a hashed, verdicted
|
|
38
|
-
* evidence artifact. There is NO path to a verdict that skips the run: the
|
|
39
|
-
* verdict is derived from the actual exit code of the actual spawn.
|
|
40
|
-
*
|
|
41
|
-
* @param {object} spec
|
|
42
|
-
* @param {string} spec.command executable to run (e.g. "node", "npx")
|
|
43
|
-
* @param {string[]} [spec.args] argv for the command
|
|
44
|
-
* @param {string} [spec.cwd] working directory
|
|
45
|
-
* @param {object} [spec.env] extra env
|
|
46
|
-
* @param {number} [spec.timeoutMs] hard timeout (default 120s)
|
|
47
|
-
* @param {string} [spec.label] human label for the check
|
|
48
|
-
* @param {(spec)=>{status:number,stdout:string,stderr:string}} [runner]
|
|
49
|
-
* injectable runner — defaults to spawnSync. Lets tests drive a fake
|
|
50
|
-
* process WITHOUT removing the "must run" property (the runner is still
|
|
51
|
-
* invoked exactly once and its result is what the verdict is built from).
|
|
52
|
-
* @returns {object} a fused evidence artifact (see EVIDENCE_KIND).
|
|
53
|
-
*/
|
|
54
|
-
export function runEvidenceGate(spec, runner) {
|
|
55
|
-
if (!spec || typeof spec !== 'object' || typeof spec.command !== 'string' || !spec.command) {
|
|
56
|
-
// Fail-closed: a malformed request can never produce a "pass".
|
|
57
|
-
return {
|
|
58
|
-
kind: EVIDENCE_KIND,
|
|
59
|
-
ran: false,
|
|
60
|
-
verdict: 'error',
|
|
61
|
-
reason: 'invalid-spec: command is required',
|
|
62
|
-
ts: new Date().toISOString(),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const exec = typeof runner === 'function' ? runner : defaultRunner;
|
|
67
|
-
|
|
68
|
-
let result;
|
|
69
|
-
try {
|
|
70
|
-
result = exec(spec);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
// Spawn itself threw — fail-closed.
|
|
73
|
-
return {
|
|
74
|
-
kind: EVIDENCE_KIND,
|
|
75
|
-
ran: false,
|
|
76
|
-
verdict: 'error',
|
|
77
|
-
reason: `runner-threw: ${e && e.message ? e.message : String(e)}`,
|
|
78
|
-
command: renderCommand(spec),
|
|
79
|
-
ts: new Date().toISOString(),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// The runner MUST return a numeric status for the verdict to exist. No status
|
|
84
|
-
// (e.g. the process could not be spawned) => no run => fail-closed.
|
|
85
|
-
const status = result && typeof result.status === 'number' ? result.status : null;
|
|
86
|
-
const stdout = result && result.stdout != null ? String(result.stdout) : '';
|
|
87
|
-
const stderr = result && result.stderr != null ? String(result.stderr) : '';
|
|
88
|
-
|
|
89
|
-
if (status === null) {
|
|
90
|
-
return {
|
|
91
|
-
kind: EVIDENCE_KIND,
|
|
92
|
-
ran: false,
|
|
93
|
-
verdict: 'error',
|
|
94
|
-
reason: 'runner-produced-no-exit-status (process did not run)',
|
|
95
|
-
command: renderCommand(spec),
|
|
96
|
-
ts: new Date().toISOString(),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const combined = `EXIT:${status}\n--STDOUT--\n${stdout}\n--STDERR--\n${stderr}`;
|
|
101
|
-
return {
|
|
102
|
-
kind: EVIDENCE_KIND,
|
|
103
|
-
ran: true,
|
|
104
|
-
label: spec.label || null,
|
|
105
|
-
command: renderCommand(spec),
|
|
106
|
-
exit_code: status,
|
|
107
|
-
verdict: status === 0 ? 'pass' : 'fail',
|
|
108
|
-
output_sha256: hashOutput(combined),
|
|
109
|
-
output_bytes: Buffer.byteLength(combined, 'utf8'),
|
|
110
|
-
ts: new Date().toISOString(),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function renderCommand(spec) {
|
|
115
|
-
return [spec.command, ...(Array.isArray(spec.args) ? spec.args : [])].join(' ');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function defaultRunner(spec) {
|
|
119
|
-
const res = spawnSync(spec.command, Array.isArray(spec.args) ? spec.args : [], {
|
|
120
|
-
cwd: spec.cwd || process.cwd(),
|
|
121
|
-
env: { ...process.env, ...(spec.env || {}) },
|
|
122
|
-
encoding: 'utf8',
|
|
123
|
-
timeout: typeof spec.timeoutMs === 'number' ? spec.timeoutMs : 120000,
|
|
124
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
125
|
-
});
|
|
126
|
-
return { status: res.status, stdout: res.stdout, stderr: res.stderr };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Is `v` a legitimate machine-parseable verification artifact (NOT prose)?
|
|
131
|
-
*
|
|
132
|
-
* Accepts, in order of strength:
|
|
133
|
-
* 1. A fused run-evidence-gate artifact (object or its JSON string) — strongest.
|
|
134
|
-
* 2. A captured-artifact OBJECT with a recognised machine shape:
|
|
135
|
-
* - { exit_code: <number> } (tsc / test-runner exit)
|
|
136
|
-
* - { http_status: <number> } (captured HTTP status)
|
|
137
|
-
* - { rowcount: <number> } / row_count (SQL rowcount)
|
|
138
|
-
* - { passed: <number>, ... } (test-runner JSON, e.g. vitest)
|
|
139
|
-
* (a JSON string of any of these is also accepted)
|
|
140
|
-
* REJECTS:
|
|
141
|
-
* - bare strings ("HTTP 200", "107 nodes", "works", "done") — proxy/prose.
|
|
142
|
-
* - objects with no machine field.
|
|
143
|
-
*/
|
|
144
|
-
export function isMachineArtifact(v) {
|
|
145
|
-
if (v == null) return false;
|
|
146
|
-
|
|
147
|
-
// String input: only accepted if it parses to a recognised JSON artifact.
|
|
148
|
-
if (typeof v === 'string') {
|
|
149
|
-
const s = v.trim();
|
|
150
|
-
if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
|
|
151
|
-
let parsed;
|
|
152
|
-
try { parsed = JSON.parse(s); } catch { return false; }
|
|
153
|
-
return isMachineArtifact(parsed);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (typeof v !== 'object') return false;
|
|
157
|
-
|
|
158
|
-
// 1. Fused artifact.
|
|
159
|
-
if (v.kind === EVIDENCE_KIND && v.ran === true && typeof v.output_sha256 === 'string') {
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 2. Recognised captured-artifact object shapes.
|
|
164
|
-
if (typeof v.exit_code === 'number') return true;
|
|
165
|
-
if (typeof v.http_status === 'number' || typeof v.httpStatus === 'number') return true;
|
|
166
|
-
if (typeof v.status_code === 'number' || typeof v.statusCode === 'number') return true;
|
|
167
|
-
if (typeof v.rowcount === 'number' || typeof v.row_count === 'number' || typeof v.rowCount === 'number') return true;
|
|
168
|
-
if (typeof v.passed === 'number' && (typeof v.failed === 'number' || typeof v.total === 'number')) return true;
|
|
169
|
-
if (typeof v.tsc_errors === 'number' || typeof v.tscErrors === 'number') return true;
|
|
170
|
-
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Does `v` represent a verification whose OUTCOME is a PASS — not merely that it
|
|
176
|
-
* ran? This is the outcome gate that complements isMachineArtifact (the shape
|
|
177
|
-
* gate). A failing run (exit_code:1), an error HTTP status (500), a fused
|
|
178
|
-
* artifact with verdict:'fail', or a test artifact with failures must NOT be
|
|
179
|
-
* accepted as evidence of a passing verification.
|
|
180
|
-
*
|
|
181
|
-
* Returns true ONLY when the artifact is a recognised machine shape AND its
|
|
182
|
-
* outcome reads as a pass. Anything ambiguous or non-passing returns false.
|
|
183
|
-
*
|
|
184
|
-
* Pass rules (mirrors isMachineArtifact's accepted shapes):
|
|
185
|
-
* - fused run-evidence-gate/v1 → ran===true && verdict==='pass'
|
|
186
|
-
* - { exit_code } → exit_code === 0
|
|
187
|
-
* - { http_status|status_code }→ 200 <= s <= 399
|
|
188
|
-
* - { passed, failed } → failed === 0
|
|
189
|
-
* - { passed, total } → passed === total
|
|
190
|
-
* - { tsc_errors } → tsc_errors === 0
|
|
191
|
-
* - { rowcount } → a captured rowcount is presence-only evidence;
|
|
192
|
-
* any numeric rowcount counts as a pass.
|
|
193
|
-
*/
|
|
194
|
-
export function isPassingArtifact(v) {
|
|
195
|
-
if (v == null) return false;
|
|
196
|
-
|
|
197
|
-
// String input: only accepted if it parses to a recognised JSON artifact.
|
|
198
|
-
if (typeof v === 'string') {
|
|
199
|
-
const s = v.trim();
|
|
200
|
-
if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
|
|
201
|
-
let parsed;
|
|
202
|
-
try { parsed = JSON.parse(s); } catch { return false; }
|
|
203
|
-
return isPassingArtifact(parsed);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (typeof v !== 'object') return false;
|
|
207
|
-
|
|
208
|
-
// Must be a recognised machine shape first.
|
|
209
|
-
if (!isMachineArtifact(v)) return false;
|
|
210
|
-
|
|
211
|
-
// 1. Fused artifact — the verdict is authoritative.
|
|
212
|
-
if (v.kind === EVIDENCE_KIND) {
|
|
213
|
-
return v.ran === true && v.verdict === 'pass';
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// 2. Captured-artifact shapes — read the outcome, not just the presence.
|
|
217
|
-
// A tsc/test error count is checked even alongside another field.
|
|
218
|
-
if (typeof v.tsc_errors === 'number') return v.tsc_errors === 0;
|
|
219
|
-
if (typeof v.tscErrors === 'number') return v.tscErrors === 0;
|
|
220
|
-
|
|
221
|
-
if (typeof v.exit_code === 'number') return v.exit_code === 0;
|
|
222
|
-
|
|
223
|
-
if (typeof v.http_status === 'number') return v.http_status >= 200 && v.http_status <= 399;
|
|
224
|
-
if (typeof v.httpStatus === 'number') return v.httpStatus >= 200 && v.httpStatus <= 399;
|
|
225
|
-
if (typeof v.status_code === 'number') return v.status_code >= 200 && v.status_code <= 399;
|
|
226
|
-
if (typeof v.statusCode === 'number') return v.statusCode >= 200 && v.statusCode <= 399;
|
|
227
|
-
|
|
228
|
-
if (typeof v.passed === 'number') {
|
|
229
|
-
if (typeof v.failed === 'number') return v.failed === 0;
|
|
230
|
-
if (typeof v.total === 'number') return v.passed === v.total;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (typeof v.rowcount === 'number') return true;
|
|
234
|
-
if (typeof v.row_count === 'number') return true;
|
|
235
|
-
if (typeof v.rowCount === 'number') return true;
|
|
236
|
-
|
|
237
|
-
// Recognised shape but no readable outcome → not a pass (fail-closed).
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export default { runEvidenceGate, hashOutput, isMachineArtifact, isPassingArtifact, EVIDENCE_KIND };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* run-evidence-gate.mjs — Truth Gate 3.0 Layer 2, the FUSED evidence primitive.
|
|
4
|
+
*
|
|
5
|
+
* Nidus / D4 principle: "you cannot submit evidence without running the test."
|
|
6
|
+
* This is ONE atomic call that (1) RUNS a verification command, (2) HASHES its
|
|
7
|
+
* captured output server-side, and (3) records the verdict TOGETHER with the
|
|
8
|
+
* hash and a timestamp. Evidence and execution are produced in the same call —
|
|
9
|
+
* an agent can never hand the gate a hash for a run that never happened.
|
|
10
|
+
*
|
|
11
|
+
* The output of runEvidenceGate() is the only legitimate shape of a
|
|
12
|
+
* machine-parseable `verification` artifact: it carries the exact command, the
|
|
13
|
+
* exit code, an SHA-256 of stdout+stderr, and a pass/fail verdict the CALLER
|
|
14
|
+
* did not author. The exit-gate's L2 verifier recognises this shape (and a few
|
|
15
|
+
* other captured-artifact shapes) and rejects anything that is bare prose.
|
|
16
|
+
*
|
|
17
|
+
* Pure-ish: it shells out to run the command but has no DB or network side
|
|
18
|
+
* effects, so it is unit-testable offline.
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
/** Stable SHA-256 of a string (server-side hash — the caller cannot forge it). */
|
|
26
|
+
export function hashOutput(text) {
|
|
27
|
+
return createHash('sha256').update(String(text == null ? '' : text), 'utf8').digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The discriminant marker stamped on every fused artifact. The exit-gate keys
|
|
32
|
+
* on this to recognise a real run-and-attest result vs. an agent-typed string.
|
|
33
|
+
*/
|
|
34
|
+
export const EVIDENCE_KIND = 'run-evidence-gate/v1';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run a verification command and atomically produce a hashed, verdicted
|
|
38
|
+
* evidence artifact. There is NO path to a verdict that skips the run: the
|
|
39
|
+
* verdict is derived from the actual exit code of the actual spawn.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} spec
|
|
42
|
+
* @param {string} spec.command executable to run (e.g. "node", "npx")
|
|
43
|
+
* @param {string[]} [spec.args] argv for the command
|
|
44
|
+
* @param {string} [spec.cwd] working directory
|
|
45
|
+
* @param {object} [spec.env] extra env
|
|
46
|
+
* @param {number} [spec.timeoutMs] hard timeout (default 120s)
|
|
47
|
+
* @param {string} [spec.label] human label for the check
|
|
48
|
+
* @param {(spec)=>{status:number,stdout:string,stderr:string}} [runner]
|
|
49
|
+
* injectable runner — defaults to spawnSync. Lets tests drive a fake
|
|
50
|
+
* process WITHOUT removing the "must run" property (the runner is still
|
|
51
|
+
* invoked exactly once and its result is what the verdict is built from).
|
|
52
|
+
* @returns {object} a fused evidence artifact (see EVIDENCE_KIND).
|
|
53
|
+
*/
|
|
54
|
+
export function runEvidenceGate(spec, runner) {
|
|
55
|
+
if (!spec || typeof spec !== 'object' || typeof spec.command !== 'string' || !spec.command) {
|
|
56
|
+
// Fail-closed: a malformed request can never produce a "pass".
|
|
57
|
+
return {
|
|
58
|
+
kind: EVIDENCE_KIND,
|
|
59
|
+
ran: false,
|
|
60
|
+
verdict: 'error',
|
|
61
|
+
reason: 'invalid-spec: command is required',
|
|
62
|
+
ts: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const exec = typeof runner === 'function' ? runner : defaultRunner;
|
|
67
|
+
|
|
68
|
+
let result;
|
|
69
|
+
try {
|
|
70
|
+
result = exec(spec);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Spawn itself threw — fail-closed.
|
|
73
|
+
return {
|
|
74
|
+
kind: EVIDENCE_KIND,
|
|
75
|
+
ran: false,
|
|
76
|
+
verdict: 'error',
|
|
77
|
+
reason: `runner-threw: ${e && e.message ? e.message : String(e)}`,
|
|
78
|
+
command: renderCommand(spec),
|
|
79
|
+
ts: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The runner MUST return a numeric status for the verdict to exist. No status
|
|
84
|
+
// (e.g. the process could not be spawned) => no run => fail-closed.
|
|
85
|
+
const status = result && typeof result.status === 'number' ? result.status : null;
|
|
86
|
+
const stdout = result && result.stdout != null ? String(result.stdout) : '';
|
|
87
|
+
const stderr = result && result.stderr != null ? String(result.stderr) : '';
|
|
88
|
+
|
|
89
|
+
if (status === null) {
|
|
90
|
+
return {
|
|
91
|
+
kind: EVIDENCE_KIND,
|
|
92
|
+
ran: false,
|
|
93
|
+
verdict: 'error',
|
|
94
|
+
reason: 'runner-produced-no-exit-status (process did not run)',
|
|
95
|
+
command: renderCommand(spec),
|
|
96
|
+
ts: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const combined = `EXIT:${status}\n--STDOUT--\n${stdout}\n--STDERR--\n${stderr}`;
|
|
101
|
+
return {
|
|
102
|
+
kind: EVIDENCE_KIND,
|
|
103
|
+
ran: true,
|
|
104
|
+
label: spec.label || null,
|
|
105
|
+
command: renderCommand(spec),
|
|
106
|
+
exit_code: status,
|
|
107
|
+
verdict: status === 0 ? 'pass' : 'fail',
|
|
108
|
+
output_sha256: hashOutput(combined),
|
|
109
|
+
output_bytes: Buffer.byteLength(combined, 'utf8'),
|
|
110
|
+
ts: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderCommand(spec) {
|
|
115
|
+
return [spec.command, ...(Array.isArray(spec.args) ? spec.args : [])].join(' ');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function defaultRunner(spec) {
|
|
119
|
+
const res = spawnSync(spec.command, Array.isArray(spec.args) ? spec.args : [], {
|
|
120
|
+
cwd: spec.cwd || process.cwd(),
|
|
121
|
+
env: { ...process.env, ...(spec.env || {}) },
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
timeout: typeof spec.timeoutMs === 'number' ? spec.timeoutMs : 120000,
|
|
124
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
125
|
+
});
|
|
126
|
+
return { status: res.status, stdout: res.stdout, stderr: res.stderr };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Is `v` a legitimate machine-parseable verification artifact (NOT prose)?
|
|
131
|
+
*
|
|
132
|
+
* Accepts, in order of strength:
|
|
133
|
+
* 1. A fused run-evidence-gate artifact (object or its JSON string) — strongest.
|
|
134
|
+
* 2. A captured-artifact OBJECT with a recognised machine shape:
|
|
135
|
+
* - { exit_code: <number> } (tsc / test-runner exit)
|
|
136
|
+
* - { http_status: <number> } (captured HTTP status)
|
|
137
|
+
* - { rowcount: <number> } / row_count (SQL rowcount)
|
|
138
|
+
* - { passed: <number>, ... } (test-runner JSON, e.g. vitest)
|
|
139
|
+
* (a JSON string of any of these is also accepted)
|
|
140
|
+
* REJECTS:
|
|
141
|
+
* - bare strings ("HTTP 200", "107 nodes", "works", "done") — proxy/prose.
|
|
142
|
+
* - objects with no machine field.
|
|
143
|
+
*/
|
|
144
|
+
export function isMachineArtifact(v) {
|
|
145
|
+
if (v == null) return false;
|
|
146
|
+
|
|
147
|
+
// String input: only accepted if it parses to a recognised JSON artifact.
|
|
148
|
+
if (typeof v === 'string') {
|
|
149
|
+
const s = v.trim();
|
|
150
|
+
if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
|
|
151
|
+
let parsed;
|
|
152
|
+
try { parsed = JSON.parse(s); } catch { return false; }
|
|
153
|
+
return isMachineArtifact(parsed);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof v !== 'object') return false;
|
|
157
|
+
|
|
158
|
+
// 1. Fused artifact.
|
|
159
|
+
if (v.kind === EVIDENCE_KIND && v.ran === true && typeof v.output_sha256 === 'string') {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 2. Recognised captured-artifact object shapes.
|
|
164
|
+
if (typeof v.exit_code === 'number') return true;
|
|
165
|
+
if (typeof v.http_status === 'number' || typeof v.httpStatus === 'number') return true;
|
|
166
|
+
if (typeof v.status_code === 'number' || typeof v.statusCode === 'number') return true;
|
|
167
|
+
if (typeof v.rowcount === 'number' || typeof v.row_count === 'number' || typeof v.rowCount === 'number') return true;
|
|
168
|
+
if (typeof v.passed === 'number' && (typeof v.failed === 'number' || typeof v.total === 'number')) return true;
|
|
169
|
+
if (typeof v.tsc_errors === 'number' || typeof v.tscErrors === 'number') return true;
|
|
170
|
+
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Does `v` represent a verification whose OUTCOME is a PASS — not merely that it
|
|
176
|
+
* ran? This is the outcome gate that complements isMachineArtifact (the shape
|
|
177
|
+
* gate). A failing run (exit_code:1), an error HTTP status (500), a fused
|
|
178
|
+
* artifact with verdict:'fail', or a test artifact with failures must NOT be
|
|
179
|
+
* accepted as evidence of a passing verification.
|
|
180
|
+
*
|
|
181
|
+
* Returns true ONLY when the artifact is a recognised machine shape AND its
|
|
182
|
+
* outcome reads as a pass. Anything ambiguous or non-passing returns false.
|
|
183
|
+
*
|
|
184
|
+
* Pass rules (mirrors isMachineArtifact's accepted shapes):
|
|
185
|
+
* - fused run-evidence-gate/v1 → ran===true && verdict==='pass'
|
|
186
|
+
* - { exit_code } → exit_code === 0
|
|
187
|
+
* - { http_status|status_code }→ 200 <= s <= 399
|
|
188
|
+
* - { passed, failed } → failed === 0
|
|
189
|
+
* - { passed, total } → passed === total
|
|
190
|
+
* - { tsc_errors } → tsc_errors === 0
|
|
191
|
+
* - { rowcount } → a captured rowcount is presence-only evidence;
|
|
192
|
+
* any numeric rowcount counts as a pass.
|
|
193
|
+
*/
|
|
194
|
+
export function isPassingArtifact(v) {
|
|
195
|
+
if (v == null) return false;
|
|
196
|
+
|
|
197
|
+
// String input: only accepted if it parses to a recognised JSON artifact.
|
|
198
|
+
if (typeof v === 'string') {
|
|
199
|
+
const s = v.trim();
|
|
200
|
+
if (!(s.startsWith('{') || s.startsWith('['))) return false; // bare prose
|
|
201
|
+
let parsed;
|
|
202
|
+
try { parsed = JSON.parse(s); } catch { return false; }
|
|
203
|
+
return isPassingArtifact(parsed);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (typeof v !== 'object') return false;
|
|
207
|
+
|
|
208
|
+
// Must be a recognised machine shape first.
|
|
209
|
+
if (!isMachineArtifact(v)) return false;
|
|
210
|
+
|
|
211
|
+
// 1. Fused artifact — the verdict is authoritative.
|
|
212
|
+
if (v.kind === EVIDENCE_KIND) {
|
|
213
|
+
return v.ran === true && v.verdict === 'pass';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 2. Captured-artifact shapes — read the outcome, not just the presence.
|
|
217
|
+
// A tsc/test error count is checked even alongside another field.
|
|
218
|
+
if (typeof v.tsc_errors === 'number') return v.tsc_errors === 0;
|
|
219
|
+
if (typeof v.tscErrors === 'number') return v.tscErrors === 0;
|
|
220
|
+
|
|
221
|
+
if (typeof v.exit_code === 'number') return v.exit_code === 0;
|
|
222
|
+
|
|
223
|
+
if (typeof v.http_status === 'number') return v.http_status >= 200 && v.http_status <= 399;
|
|
224
|
+
if (typeof v.httpStatus === 'number') return v.httpStatus >= 200 && v.httpStatus <= 399;
|
|
225
|
+
if (typeof v.status_code === 'number') return v.status_code >= 200 && v.status_code <= 399;
|
|
226
|
+
if (typeof v.statusCode === 'number') return v.statusCode >= 200 && v.statusCode <= 399;
|
|
227
|
+
|
|
228
|
+
if (typeof v.passed === 'number') {
|
|
229
|
+
if (typeof v.failed === 'number') return v.failed === 0;
|
|
230
|
+
if (typeof v.total === 'number') return v.passed === v.total;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (typeof v.rowcount === 'number') return true;
|
|
234
|
+
if (typeof v.row_count === 'number') return true;
|
|
235
|
+
if (typeof v.rowCount === 'number') return true;
|
|
236
|
+
|
|
237
|
+
// Recognised shape but no readable outcome → not a pass (fail-closed).
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default { runEvidenceGate, hashOutput, isMachineArtifact, isPassingArtifact, EVIDENCE_KIND };
|