@bradheitmann/odin-sentinel 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +39 -20
  2. package/dist/src/mcp/server.js +23 -4
  3. package/dist/src/mcp/server.js.map +1 -1
  4. package/dist/src/protocol/index.d.ts +1 -1
  5. package/dist/src/protocol/index.js +1 -1
  6. package/dist/src/protocol/index.js.map +1 -1
  7. package/dist/src/protocol/schemas.d.ts +117 -0
  8. package/dist/src/protocol/schemas.js +41 -10
  9. package/dist/src/protocol/schemas.js.map +1 -1
  10. package/dist/src/protocol/service.d.ts +53 -0
  11. package/dist/src/protocol/service.js +404 -6
  12. package/dist/src/protocol/service.js.map +1 -1
  13. package/dist/src/protocol/version.d.ts +2 -2
  14. package/dist/src/protocol/version.js +2 -2
  15. package/docs/guides/quick-start.md +55 -11
  16. package/docs/guides/quickstart-prompts.md +3 -3
  17. package/docs/reference/client-compatibility.md +27 -0
  18. package/docs/reference/distribution.md +18 -6
  19. package/docs/reference/public-surface-audit.md +2 -2
  20. package/package.json +5 -5
  21. package/protocol/SCP.md +38 -2
  22. package/protocol/bootstrap-skill.md +17 -1
  23. package/protocol/closeout.yaml +1 -1
  24. package/protocol/delegation.yaml +15 -1
  25. package/protocol/model-profiles.yaml +20 -1
  26. package/protocol/receipts/boot-receipt.yaml +18 -0
  27. package/protocol/roles.yaml +1 -1
  28. package/protocol/topology.yaml +9 -1
  29. package/scripts/audit/verify-pack.mjs +16 -6
  30. package/scripts/protocol/install-activation-hooks.mjs +167 -0
  31. package/scripts/protocol/verify-instruction-read.mjs +205 -0
  32. package/templates/dev-slice-template.md +8 -0
  33. package/templates/pm-role-template.md +13 -0
  34. package/templates/qa-slice-template.md +3 -0
@@ -1,4 +1,4 @@
1
- version: 0.4.7
1
+ version: 0.4.9
2
2
  policy:
3
3
  semantics: Recommended starter profiles, not bundled dependencies or availability guarantees.
4
4
  runtime_requirement: Users must install and configure their own harnesses. Launchers must verify local harness/model availability before dispatch and apply fallbacks when unavailable.
@@ -22,6 +22,14 @@ policy:
22
22
  - MODEL_REASONING_ONLY
23
23
  - STREAMING_PROTOCOL_MISMATCH
24
24
  - MODEL_UNREACHABLE
25
+ readiness_dimensions:
26
+ - installed_binary
27
+ - authenticated
28
+ - mcp_configured
29
+ - mcp_tool_hydration
30
+ - governed_role_ready
31
+ non_governed_one_shot_only: Harnesses without MCP access, native SCP skill, or full injected protocol text must be classified NON_GOVERNED_ONE_SHOT_ONLY and must not hold persistent governed roles.
32
+ scp_skill_install_recommendation: Skill-capable harnesses should install the sentinel-coordination-protocol skill before governed launch; native skill discoverability improved compliance in observed runs.
25
33
  harness_capabilities:
26
34
  Codex:
27
35
  can_hydrate_deferred_mcp_tools_at_boot: true
@@ -35,6 +43,12 @@ harness_capabilities:
35
43
  can_hydrate_deferred_mcp_tools_at_boot: true
36
44
  native_skill_invocation: false
37
45
  scp_skill_recommended: false
46
+ mcp_management_command: droid mcp
47
+ governed_readiness_requires_mcp: true
48
+ read_only_exec_allowed_without_write_authority: true
49
+ mission_or_high_autonomy_requires: --auto high
50
+ unsafe_skip_flag_is_a_blocker: true
51
+ auto_levels: [low, medium, high]
38
52
  Goose:
39
53
  local_inference_smoke_test_required: true
40
54
  visible_content_required_within_seconds: 60
@@ -42,6 +56,11 @@ harness_capabilities:
42
56
  streaming_mismatch_class: STREAMING_PROTOCOL_MISMATCH
43
57
  Crush:
44
58
  permission_prompt_class: BLOCKED_BY_PERMISSION
59
+ auth_header_failure_class: BLOCKED_BY_AUTH
60
+ auth_provider_blocked_class: AUTH_PROVIDER_BLOCKED
61
+ auth_header_failure_example: "unauthorized: Authentication parameter not received in Header"
62
+ mcp_management_command: none
63
+ mcp_unavailable_class: MCP_UNAVAILABLE
45
64
  OpenHands:
46
65
  missing_inference_credentials_class: BLOCKED_BY_API_KEY
47
66
  provider_config_blocker_class: AUTH_PROVIDER_BLOCKED
@@ -64,6 +64,7 @@ recommended_fields:
64
64
  - lifecycle_state
65
65
  - mcp_version
66
66
  - scp_context_source
67
+ - instruction_read_proof
67
68
  receipt_type_policy:
68
69
  SCP_BOOT_RECEIPT: full governed occupant receipt after role/context/readiness are known
69
70
  SCP_MIN_BOOT_RECEIPT: minimal bootstrap-only receipt for orientation or pre-dispatch identity proof
@@ -80,3 +81,20 @@ staffing_audit:
80
81
  - column_index
81
82
  - team_letter
82
83
  staffed_by_canonical_value: A/EXEC-PM
84
+ instruction_read_proof_policy:
85
+ required_before:
86
+ - implementation
87
+ - QA acceptance
88
+ - ACTIVE_WATCH
89
+ per_file_fields:
90
+ - path
91
+ - bytes_or_lines
92
+ - sha256
93
+ description: >-
94
+ Activated roles must produce a full-instruction-read proof before acting. Each required
95
+ instruction file is recorded with a byte or line count and a SHA-256 digest; first-screen
96
+ or partial reads are insufficient. Verify with scripts/protocol/verify-instruction-read.mjs
97
+ and validate shape with odin.validate_instruction_read_proof.
98
+ non_breaking: >-
99
+ This is a recommended, non-breaking addition. Existing receipts without an
100
+ instruction_read_proof field remain valid.
@@ -1,4 +1,4 @@
1
- version: 0.4.7
1
+ version: 0.4.9
2
2
  roles:
3
3
  EXEC_PM:
4
4
  title: EXEC PM
@@ -1,4 +1,4 @@
1
- version: 0.4.7
1
+ version: 0.4.9
2
2
  default_topology:
3
3
  executive_office:
4
4
  team: A
@@ -44,6 +44,14 @@ default_topology:
44
44
  - receive setup guidance without pasting secrets
45
45
  - mark slot VACANT_ROLE_SLOT
46
46
  - request EXEC PM-approved substitution
47
+ role_compatibility_smoke_test:
48
+ before_assignment: true
49
+ questions:
50
+ - accepts the role contract, authority limits, and reports-to chain
51
+ - can emit a valid SCP boot receipt
52
+ - can remain in the assigned lifecycle state until directed
53
+ - treats the protocol as real governance, not fictional roleplay
54
+ fail_classification: ROLE_COMPATIBILITY_FAILED
47
55
  surface_layout:
48
56
  description: >-
49
57
  Canonical CMUX surface layout rule for EXEC PM. Governed team bootstrap
@@ -201,7 +201,13 @@ export function validatePublicProtocolSync({ scpText, bootstrapText, currentVers
201
201
  return errors;
202
202
  }
203
203
 
204
- export function validatePluginSync({ pluginManifestText, pluginSkillText, pluginReadmeText, currentVersion, minimumCompatibleVersion = MINIMUM_COMPATIBLE_CHILD_MCP_VERSION }) {
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 {
@@ -218,9 +224,9 @@ export function validatePluginSync({ pluginManifestText, pluginSkillText, plugin
218
224
  if (!server) {
219
225
  errors.push("Claude plugin manifest missing odin-sentinel MCP server");
220
226
  } else {
221
- if (server.command !== "npx") errors.push("Claude plugin odin-sentinel server must use npx");
227
+ if (server.command !== "pnpm") errors.push("Claude plugin odin-sentinel server must use pnpm");
222
228
  const args = Array.isArray(server.args) ? server.args : [];
223
- for (const requiredArg of ["-y", "-p", "@bradheitmann/odin-sentinel", "odin-sentinel-mcp"]) {
229
+ for (const requiredArg of ["dlx", "--package", `@bradheitmann/odin-sentinel@${currentVersion}`, "odin-sentinel-mcp"]) {
224
230
  if (!args.includes(requiredArg)) errors.push(`Claude plugin odin-sentinel args missing ${requiredArg}`);
225
231
  }
226
232
  }
@@ -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
- if (!/23\s+`?odin\.\*`?\s+tools/i.test(pluginReadmeText)) {
232
- errors.push("Claude plugin README must advertise 23 odin.* tools");
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