@bradheitmann/odin-sentinel 0.4.7 → 0.4.8
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 +7 -3
- package/dist/src/mcp/server.js +23 -4
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/protocol/index.d.ts +1 -1
- package/dist/src/protocol/index.js +1 -1
- package/dist/src/protocol/index.js.map +1 -1
- package/dist/src/protocol/schemas.d.ts +117 -0
- package/dist/src/protocol/schemas.js +41 -10
- package/dist/src/protocol/schemas.js.map +1 -1
- package/dist/src/protocol/service.d.ts +53 -0
- package/dist/src/protocol/service.js +404 -6
- package/dist/src/protocol/service.js.map +1 -1
- package/dist/src/protocol/version.d.ts +2 -2
- package/dist/src/protocol/version.js +2 -2
- package/docs/guides/quick-start.md +38 -2
- package/docs/guides/quickstart-prompts.md +2 -2
- package/docs/reference/client-compatibility.md +27 -0
- package/docs/reference/distribution.md +1 -1
- package/docs/reference/public-surface-audit.md +2 -2
- package/package.json +5 -5
- package/protocol/SCP.md +38 -2
- package/protocol/bootstrap-skill.md +17 -1
- package/protocol/closeout.yaml +1 -1
- package/protocol/delegation.yaml +15 -1
- package/protocol/model-profiles.yaml +20 -1
- package/protocol/receipts/boot-receipt.yaml +18 -0
- package/protocol/roles.yaml +1 -1
- package/protocol/topology.yaml +9 -1
- package/scripts/audit/verify-pack.mjs +14 -4
- package/scripts/protocol/install-activation-hooks.mjs +167 -0
- package/scripts/protocol/verify-instruction-read.mjs +205 -0
- package/templates/dev-slice-template.md +8 -0
- package/templates/pm-role-template.md +13 -0
- package/templates/qa-slice-template.md +3 -0
|
@@ -201,7 +201,13 @@ export function validatePublicProtocolSync({ scpText, bootstrapText, currentVers
|
|
|
201
201
|
return errors;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
export function
|
|
204
|
+
export function extractToolCount(text) {
|
|
205
|
+
if (typeof text !== "string") return null;
|
|
206
|
+
const match = text.match(/(\d+)\s+(?:`?odin\.\*`?\s+)?tools\b/i);
|
|
207
|
+
return match ? Number(match[1]) : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function validatePluginSync({ pluginManifestText, pluginSkillText, pluginReadmeText, currentVersion, minimumCompatibleVersion = MINIMUM_COMPATIBLE_CHILD_MCP_VERSION, expectedToolCount }) {
|
|
205
211
|
const errors = [];
|
|
206
212
|
let manifest;
|
|
207
213
|
try {
|
|
@@ -228,8 +234,11 @@ export function validatePluginSync({ pluginManifestText, pluginSkillText, plugin
|
|
|
228
234
|
for (const marker of [`SCP_PUBLIC_VERSION: ${currentVersion}`, `MIN_COMPATIBLE_CHILD_MCP: ${minimumCompatibleVersion}`]) {
|
|
229
235
|
if (!pluginSkillText.includes(marker)) errors.push(`Claude plugin skill missing ${marker}`);
|
|
230
236
|
}
|
|
231
|
-
|
|
232
|
-
|
|
237
|
+
const pluginToolCount = extractToolCount(pluginReadmeText);
|
|
238
|
+
if (pluginToolCount === null) {
|
|
239
|
+
errors.push("Claude plugin README must advertise its odin.* tool count");
|
|
240
|
+
} else if (typeof expectedToolCount === "number" && pluginToolCount !== expectedToolCount) {
|
|
241
|
+
errors.push(`Claude plugin README advertises ${pluginToolCount} odin.* tools but package.json describes ${expectedToolCount}`);
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
return errors;
|
|
@@ -304,7 +313,8 @@ export function runVerifyPack({ pack, packageJson, publicVersionFiles, costPriva
|
|
|
304
313
|
pluginManifestText: publicVersionFiles["plugins/sentinel-coordination-protocol/.claude-plugin/plugin.json"],
|
|
305
314
|
pluginSkillText: publicVersionFiles["plugins/sentinel-coordination-protocol/skills/sentinel-coordination-protocol/SKILL.md"],
|
|
306
315
|
pluginReadmeText: publicVersionFiles["plugins/sentinel-coordination-protocol/README.md"],
|
|
307
|
-
currentVersion: packageJson.version
|
|
316
|
+
currentVersion: packageJson.version,
|
|
317
|
+
expectedToolCount: extractToolCount(packageJson.description)
|
|
308
318
|
}),
|
|
309
319
|
...validateBootstrapReadiness(publicVersionFiles["protocol/bootstrap-skill.md"]),
|
|
310
320
|
...validateTelemetryWording(costPrivacyText)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/protocol/install-activation-hooks.mjs
|
|
3
|
+
//
|
|
4
|
+
// Install (or preview) the ODIN/SCP activation-gate hook that forces full-instruction-read
|
|
5
|
+
// proof before an activated role begins implementation, QA acceptance, or ACTIVE_WATCH work.
|
|
6
|
+
//
|
|
7
|
+
// The hook is intentionally a small, dependency-free shell wrapper that runs the
|
|
8
|
+
// instruction-read verifier against a declared proof file and refuses to proceed unless the
|
|
9
|
+
// verifier exits 0. This makes "did the agent actually read its instructions?" a
|
|
10
|
+
// machine-checkable gate instead of an honor-system claim.
|
|
11
|
+
//
|
|
12
|
+
// Safety: default mode is a dry-run that prints the plan and the hook body. Files are only
|
|
13
|
+
// written when --install --target <dir> is given explicitly. Zero-secret: no environment
|
|
14
|
+
// values are read or printed.
|
|
15
|
+
|
|
16
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
const HOOK_FILENAME = "odin-activation-precheck.sh";
|
|
20
|
+
|
|
21
|
+
const USAGE = `odin install-activation-hooks
|
|
22
|
+
|
|
23
|
+
Install or preview the SCP activation-gate precheck hook. The hook runs the
|
|
24
|
+
instruction-read verifier before an activated role acts, and blocks activation unless a
|
|
25
|
+
full-instruction-read proof verifies against local files.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
node scripts/protocol/install-activation-hooks.mjs # dry-run: print plan + hook
|
|
29
|
+
node scripts/protocol/install-activation-hooks.mjs --print-hook # print hook body only
|
|
30
|
+
node scripts/protocol/install-activation-hooks.mjs --install --target <dir>
|
|
31
|
+
node scripts/protocol/install-activation-hooks.mjs --help
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--install Write the hook file. Requires --target.
|
|
35
|
+
--target <dir> Directory to write ${HOOK_FILENAME} into (created if absent).
|
|
36
|
+
--print-hook Print the hook script body to stdout and exit 0.
|
|
37
|
+
--force Overwrite an existing hook file when installing.
|
|
38
|
+
--help, -h Show this help and exit 0.
|
|
39
|
+
|
|
40
|
+
Exit codes: 0 = help / dry-run / print / successful install; 2 = usage error;
|
|
41
|
+
3 = refused to overwrite without --force.
|
|
42
|
+
Zero-secret: this installer reads no environment variables and prints no secrets.`;
|
|
43
|
+
|
|
44
|
+
// The activation precheck hook body. Pure POSIX sh; calls the sibling verifier.
|
|
45
|
+
export function renderHookScript() {
|
|
46
|
+
return `#!/bin/sh
|
|
47
|
+
# odin-activation-precheck.sh — installed by scripts/protocol/install-activation-hooks.mjs
|
|
48
|
+
#
|
|
49
|
+
# Block an activated SCP role from acting until its full-instruction-read proof verifies.
|
|
50
|
+
# Usage: odin-activation-precheck.sh <proof.json> [--base <dir>]
|
|
51
|
+
#
|
|
52
|
+
# Wire this into a harness pre-activation step, a git pre-commit hook, or a launch runbook
|
|
53
|
+
# so implementation / QA / ACTIVE_WATCH work cannot start on a skimmed instruction set.
|
|
54
|
+
set -eu
|
|
55
|
+
|
|
56
|
+
PROOF="\${1:-}"
|
|
57
|
+
if [ -z "\$PROOF" ]; then
|
|
58
|
+
echo "odin-activation-precheck: missing <proof.json> argument" >&2
|
|
59
|
+
echo "an activated role must declare and verify a full-instruction-read proof first" >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
shift || true
|
|
63
|
+
|
|
64
|
+
SCRIPT_DIR=\$(CDPATH= cd -- "\$(dirname -- "\$0")" && pwd)
|
|
65
|
+
VERIFIER="\$SCRIPT_DIR/verify-instruction-read.mjs"
|
|
66
|
+
if [ ! -f "\$VERIFIER" ]; then
|
|
67
|
+
# Fall back to the repo-relative protocol script location.
|
|
68
|
+
VERIFIER="scripts/protocol/verify-instruction-read.mjs"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo "odin-activation-precheck: verifying full-instruction-read proof \$PROOF"
|
|
72
|
+
if node "\$VERIFIER" "\$PROOF" "\$@"; then
|
|
73
|
+
echo "odin-activation-precheck: PASS — instruction-read proof verified; activation allowed"
|
|
74
|
+
exit 0
|
|
75
|
+
else
|
|
76
|
+
echo "odin-activation-precheck: FAIL — instruction-read proof did not verify; activation blocked" >&2
|
|
77
|
+
echo "read the declared instruction files in full and regenerate the proof before acting" >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function planInstall(target) {
|
|
84
|
+
return {
|
|
85
|
+
action: "install-activation-hooks",
|
|
86
|
+
hookFile: target ? join(target, HOOK_FILENAME) : `<target>/${HOOK_FILENAME}`,
|
|
87
|
+
verifier: "scripts/protocol/verify-instruction-read.mjs",
|
|
88
|
+
gates: [
|
|
89
|
+
"An activated role must produce a full-instruction-read proof before implementation, QA acceptance, or ACTIVE_WATCH work.",
|
|
90
|
+
"The precheck hook runs verify-instruction-read.mjs and blocks activation unless it exits 0.",
|
|
91
|
+
"CMUX dispatch must additionally satisfy delivery proof: submitted=true plus verified processing on the target surface."
|
|
92
|
+
],
|
|
93
|
+
mcp: ["odin.get_activation_gates", "odin.validate_instruction_read_proof", "odin.validate_cmux_delivery_proof"],
|
|
94
|
+
wiring: [
|
|
95
|
+
"harness pre-activation step (preferred)",
|
|
96
|
+
"git pre-commit / pre-push hook",
|
|
97
|
+
"launch runbook checklist step"
|
|
98
|
+
]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseArgs(argv) {
|
|
103
|
+
const args = { install: false, printHook: false, force: false, help: false, target: undefined };
|
|
104
|
+
for (let i = 0; i < argv.length; i++) {
|
|
105
|
+
const a = argv[i];
|
|
106
|
+
if (a === "--help" || a === "-h") args.help = true;
|
|
107
|
+
else if (a === "--install") args.install = true;
|
|
108
|
+
else if (a === "--print-hook") args.printHook = true;
|
|
109
|
+
else if (a === "--force") args.force = true;
|
|
110
|
+
else if (a === "--target") args.target = argv[++i];
|
|
111
|
+
}
|
|
112
|
+
return args;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main() {
|
|
116
|
+
const args = parseArgs(process.argv.slice(2));
|
|
117
|
+
|
|
118
|
+
if (args.help) {
|
|
119
|
+
console.log(USAGE);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (args.printHook) {
|
|
124
|
+
process.stdout.write(renderHookScript());
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!args.install) {
|
|
129
|
+
// Dry-run: show the plan and the hook body without writing anything.
|
|
130
|
+
console.log("odin install-activation-hooks (dry-run; no files written)");
|
|
131
|
+
console.log(JSON.stringify(planInstall(args.target), null, 2));
|
|
132
|
+
console.log("");
|
|
133
|
+
console.log(`Run with --install --target <dir> to write ${HOOK_FILENAME}.`);
|
|
134
|
+
console.log("--- hook body ---");
|
|
135
|
+
process.stdout.write(renderHookScript());
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!args.target) {
|
|
140
|
+
console.error("error: --install requires --target <dir>");
|
|
141
|
+
console.error("run with --help for usage");
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const targetDir = resolve(process.cwd(), args.target);
|
|
146
|
+
const hookPath = join(targetDir, HOOK_FILENAME);
|
|
147
|
+
if (existsSync(hookPath) && !args.force) {
|
|
148
|
+
console.error(`refused: ${hookPath} already exists (use --force to overwrite)`);
|
|
149
|
+
process.exit(3);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
mkdirSync(targetDir, { recursive: true });
|
|
153
|
+
writeFileSync(hookPath, renderHookScript());
|
|
154
|
+
try {
|
|
155
|
+
chmodSync(hookPath, 0o755);
|
|
156
|
+
} catch {
|
|
157
|
+
// chmod is best-effort on platforms that do not support it.
|
|
158
|
+
}
|
|
159
|
+
console.log(`installed activation precheck hook: ${hookPath}`);
|
|
160
|
+
console.log("wire it into a harness pre-activation step, git hook, or launch runbook.");
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
165
|
+
if (invokedDirectly) {
|
|
166
|
+
main();
|
|
167
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/protocol/verify-instruction-read.mjs
|
|
3
|
+
//
|
|
4
|
+
// Verify an ODIN/SCP full-instruction-read proof against local files.
|
|
5
|
+
//
|
|
6
|
+
// An activated role (DEV, QA, ODIN ACTIVE_WATCH) must read its assigned instruction
|
|
7
|
+
// sources in full before acting. This verifier confirms that every file declared in a
|
|
8
|
+
// read proof still exists and matches its recorded byte count and SHA-256 digest, so a
|
|
9
|
+
// role cannot claim it read instructions it only skimmed (first-screen / `head` / partial)
|
|
10
|
+
// or never opened. A missing, truncated, or checksum-mismatched file fails the gate.
|
|
11
|
+
//
|
|
12
|
+
// Zero-secret: this script reads only the files named in the proof. It never reads
|
|
13
|
+
// environment variables and never prints file contents — only paths, byte counts, line
|
|
14
|
+
// counts, SHA-256 digests, and PASS/FAIL reasons.
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
const USAGE = `odin verify-instruction-read
|
|
21
|
+
|
|
22
|
+
Verify a full-instruction-read proof against local files. Confirms each declared
|
|
23
|
+
instruction file exists and matches its recorded byte count and SHA-256 digest, so an
|
|
24
|
+
activated role cannot claim a full read it did not perform.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
node scripts/protocol/verify-instruction-read.mjs <proof.json> [--base <dir>] [--json]
|
|
28
|
+
node scripts/protocol/verify-instruction-read.mjs --record <file...> [--base <dir>] [--role <role>]
|
|
29
|
+
node scripts/protocol/verify-instruction-read.mjs --help
|
|
30
|
+
|
|
31
|
+
Modes:
|
|
32
|
+
verify (default) Read <proof.json> and verify every files[] entry against disk.
|
|
33
|
+
Exit 0 only if all entries match. Exit 1 if any file is missing,
|
|
34
|
+
truncated, or checksum-mismatched.
|
|
35
|
+
--record Compute a fresh proof for the listed files and print it as JSON on
|
|
36
|
+
stdout (does not write any file). Useful for an agent generating its
|
|
37
|
+
own pre-edit read proof: redirect stdout to a proof file.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--base <dir> Resolve proof file entry paths against <dir> (default: current dir).
|
|
41
|
+
--role <role> Role label embedded when using --record (default: UNDECLARED).
|
|
42
|
+
--json Emit a machine-readable JSON result in verify mode.
|
|
43
|
+
--help, -h Show this help and exit 0.
|
|
44
|
+
|
|
45
|
+
Exit codes: 0 = all files verified / help / record; 1 = one or more files failed; 2 = usage error.
|
|
46
|
+
Output is zero-secret: only paths, sizes, and digests are printed (never file contents).`;
|
|
47
|
+
|
|
48
|
+
function sha256(buf) {
|
|
49
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function countLines(buf) {
|
|
53
|
+
return buf.toString("utf8").split("\n").length - 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveEntryPath(base, p) {
|
|
57
|
+
return isAbsolute(p) ? p : join(base, p);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseArgs(argv) {
|
|
61
|
+
const args = { positional: [], base: ".", role: "UNDECLARED", json: false, help: false, record: false };
|
|
62
|
+
for (let i = 0; i < argv.length; i++) {
|
|
63
|
+
const a = argv[i];
|
|
64
|
+
if (a === "--help" || a === "-h") args.help = true;
|
|
65
|
+
else if (a === "--json") args.json = true;
|
|
66
|
+
else if (a === "--record") args.record = true;
|
|
67
|
+
else if (a === "--base") args.base = argv[++i] ?? ".";
|
|
68
|
+
else if (a === "--role") args.role = argv[++i] ?? "UNDECLARED";
|
|
69
|
+
else args.positional.push(a);
|
|
70
|
+
}
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function recordProof(files, base = ".", role = "UNDECLARED") {
|
|
75
|
+
const entries = files.map((p) => {
|
|
76
|
+
const buf = readFileSync(resolveEntryPath(base, p));
|
|
77
|
+
return { path: p, bytes: buf.length, lines: countLines(buf), sha256: sha256(buf), read: "full" };
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
schema: "odin.instruction_read_proof.v1",
|
|
81
|
+
role,
|
|
82
|
+
generated_at: new Date().toISOString(),
|
|
83
|
+
base: ".",
|
|
84
|
+
file_count: entries.length,
|
|
85
|
+
files: entries
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pure verification core: takes a parsed proof object and a base dir, returns a result.
|
|
90
|
+
// Exported so tests can exercise it without spawning a process.
|
|
91
|
+
export function verifyProof(proof, base = ".") {
|
|
92
|
+
const files = Array.isArray(proof?.files) ? proof.files : null;
|
|
93
|
+
if (!files || files.length === 0) {
|
|
94
|
+
return { ok: false, base, passed: 0, total: 0, results: [], error: "proof has no files[] entries" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const results = files.map((entry) => {
|
|
98
|
+
const path = entry && typeof entry.path === "string" ? entry.path : "";
|
|
99
|
+
const reasons = [];
|
|
100
|
+
if (path.trim() === "") {
|
|
101
|
+
return { path: path || "<unknown>", status: "FAIL", reasons: ["proof entry is missing a path"] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const abs = resolveEntryPath(base, path);
|
|
105
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) {
|
|
106
|
+
return { path, status: "FAIL", reasons: ["file missing"] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const buf = readFileSync(abs);
|
|
110
|
+
const actualBytes = buf.length;
|
|
111
|
+
const actualSha = sha256(buf);
|
|
112
|
+
|
|
113
|
+
if (typeof entry.sha256 === "string" && entry.sha256.trim() !== "") {
|
|
114
|
+
if (entry.sha256 !== actualSha) {
|
|
115
|
+
reasons.push("sha256 mismatch (file changed, truncated, or only partially read)");
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
reasons.push("proof entry is missing a sha256 digest");
|
|
119
|
+
}
|
|
120
|
+
if (typeof entry.bytes === "number" && entry.bytes !== actualBytes) {
|
|
121
|
+
reasons.push(`byte count mismatch: recorded ${entry.bytes}, actual ${actualBytes}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
path,
|
|
126
|
+
status: reasons.length === 0 ? "PASS" : "FAIL",
|
|
127
|
+
reasons,
|
|
128
|
+
recordedBytes: typeof entry.bytes === "number" ? entry.bytes : null,
|
|
129
|
+
actualBytes,
|
|
130
|
+
recordedSha256: typeof entry.sha256 === "string" ? entry.sha256 : null,
|
|
131
|
+
actualSha256: actualSha,
|
|
132
|
+
recordedLines: typeof entry.lines === "number" ? entry.lines : null,
|
|
133
|
+
actualLines: countLines(buf)
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const passed = results.filter((r) => r.status === "PASS").length;
|
|
138
|
+
return { ok: passed === results.length, base, passed, total: results.length, results };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function main() {
|
|
142
|
+
const args = parseArgs(process.argv.slice(2));
|
|
143
|
+
|
|
144
|
+
if (args.help) {
|
|
145
|
+
console.log(USAGE);
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (args.record) {
|
|
150
|
+
if (args.positional.length === 0) {
|
|
151
|
+
console.error("error: --record requires at least one file path");
|
|
152
|
+
console.error("run with --help for usage");
|
|
153
|
+
process.exit(2);
|
|
154
|
+
}
|
|
155
|
+
let proof;
|
|
156
|
+
try {
|
|
157
|
+
proof = recordProof(args.positional, args.base, args.role);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
160
|
+
process.exit(2);
|
|
161
|
+
}
|
|
162
|
+
console.log(JSON.stringify(proof, null, 2));
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const proofPath = args.positional[0];
|
|
167
|
+
if (!proofPath) {
|
|
168
|
+
console.error("error: missing <proof.json> argument");
|
|
169
|
+
console.error("run with --help for usage");
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let proof;
|
|
174
|
+
try {
|
|
175
|
+
proof = JSON.parse(readFileSync(resolve(process.cwd(), proofPath), "utf8"));
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(`error: cannot read or parse proof file: ${err instanceof Error ? err.message : String(err)}`);
|
|
178
|
+
process.exit(2);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = verifyProof(proof, args.base);
|
|
182
|
+
|
|
183
|
+
if (args.json) {
|
|
184
|
+
console.log(JSON.stringify(result, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
console.log(`instruction-read verify (base: ${result.base})`);
|
|
187
|
+
if (result.error) console.log(` ! ${result.error}`);
|
|
188
|
+
for (const r of result.results) {
|
|
189
|
+
if (r.status === "PASS") {
|
|
190
|
+
console.log(` [PASS] ${r.path} (${r.actualBytes} bytes, sha256 ${r.actualSha256.slice(0, 12)}…)`);
|
|
191
|
+
} else {
|
|
192
|
+
console.log(` [FAIL] ${r.path} — ${r.reasons.join("; ")}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
console.log(`${result.passed}/${result.total} files verified`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
process.exit(result.ok ? 0 : 1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Only run the CLI when invoked directly, not when imported by tests.
|
|
202
|
+
const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
|
|
203
|
+
if (invokedDirectly) {
|
|
204
|
+
main();
|
|
205
|
+
}
|
|
@@ -32,6 +32,14 @@ Use this as a public starter template. Replace every placeholder before launch.
|
|
|
32
32
|
- Manual checks:
|
|
33
33
|
- `<check>`
|
|
34
34
|
|
|
35
|
+
## Instruction-Read Proof (before editing)
|
|
36
|
+
|
|
37
|
+
Before changing any file, read the full reading-list and context sources, then record a
|
|
38
|
+
full-instruction-read proof (each file with a byte or line count and a SHA-256 digest).
|
|
39
|
+
Generate it with `node scripts/protocol/verify-instruction-read.mjs --record <file...>` and
|
|
40
|
+
verify it with `node scripts/protocol/verify-instruction-read.mjs <proof.json>`. First-screen
|
|
41
|
+
or partial reads are insufficient.
|
|
42
|
+
|
|
35
43
|
## DEV Report
|
|
36
44
|
|
|
37
45
|
Return:
|
|
@@ -37,6 +37,19 @@ Use this as a public starter template for an ODIN Sentinel PM role.
|
|
|
37
37
|
- Safe next choice for the human operator: `<approve | sign in | choose fallback harness | keep slot vacant | ask for help>`
|
|
38
38
|
- Secret-handling reminder: `Do not paste API keys or tokens into chat.`
|
|
39
39
|
|
|
40
|
+
## Dispatch Delivery Proof
|
|
41
|
+
|
|
42
|
+
When dispatching to a CMUX role, delivery is not complete until you:
|
|
43
|
+
|
|
44
|
+
1. Send the text to the target surface.
|
|
45
|
+
2. Submit with Enter (input-bar text is not delivery).
|
|
46
|
+
3. Read the target surface.
|
|
47
|
+
4. Confirm the agent processed or acknowledged the message.
|
|
48
|
+
|
|
49
|
+
Record `target_surface_locator`, `submitted`, `verification_method`,
|
|
50
|
+
`observed_processing_state`, `timestamp`, and `sender_role`, then validate with
|
|
51
|
+
`odin.validate_cmux_delivery_proof`.
|
|
52
|
+
|
|
40
53
|
## Assignments
|
|
41
54
|
|
|
42
55
|
- `<role slot>` -> `<agent/harness>` -> `<scope>`
|
|
@@ -23,6 +23,9 @@ QA starts from the task contract and changed files, not from DEV's confidence.
|
|
|
23
23
|
- No unsafe permission or auth behavior: PASS/FAIL
|
|
24
24
|
- Regression risk:
|
|
25
25
|
- Relevant tests or manual checks reproduced: PASS/FAIL
|
|
26
|
+
- Activation gates:
|
|
27
|
+
- DEV provided a full-instruction-read proof and it verifies against local files: PASS/FAIL
|
|
28
|
+
- Any CMUX delivery proof is submitted and confirmed (not input-bar-only): PASS/FAIL
|
|
26
29
|
- User-defined criteria:
|
|
27
30
|
- `<project-specific criterion>` -> PASS/FAIL
|
|
28
31
|
|