dsh-completion-guard 0.2.1

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.
@@ -0,0 +1,1997 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as path from "node:path";
3
+
4
+ //#region src/domain/types.ts
5
+ function createProjection() {
6
+ return {
7
+ enabled: false,
8
+ epoch: 0,
9
+ contractRevision: 0,
10
+ items: /* @__PURE__ */ new Map(),
11
+ evidence: /* @__PURE__ */ new Map(),
12
+ checkpoints: [],
13
+ lastObservedSourceSeq: -1,
14
+ lastGuardEventSeq: -1,
15
+ continuationAttempts: /* @__PURE__ */ new Map(),
16
+ integrity: "valid"
17
+ };
18
+ }
19
+
20
+ //#endregion
21
+ //#region src/domain/canonicalize.ts
22
+ function normalizeClause(text) {
23
+ return text.trim().replace(/\s+/g, " ");
24
+ }
25
+ function isWindowsStylePath(value) {
26
+ return /^[A-Za-z]:/.test(value) || value.startsWith("\\\\") || value.startsWith("//") || value.includes("\\");
27
+ }
28
+ /**
29
+ * Canonicalize a filesystem path for subject matching. Windows-style paths are
30
+ * normalized (drive letter, both separator kinds, `.`/`..`, duplicate
31
+ * separators) and case-folded, because Windows paths compare case-insensitively
32
+ * and treat `/` and `\` as equivalent. POSIX-style paths are normalized but
33
+ * keep their case, so a case-sensitive filesystem is never made insensitive.
34
+ * Exactly one canonicalizer is shared by contract capture and evidence
35
+ * extraction so a Windows contract subject and a Windows evidence subject match.
36
+ */
37
+ function canonicalizePath(value) {
38
+ if (!value) return value;
39
+ return isWindowsStylePath(value) ? path.win32.normalize(value).toLowerCase() : path.posix.normalize(value);
40
+ }
41
+ function sha256(text) {
42
+ return createHash("sha256").update(text, "utf8").digest("hex");
43
+ }
44
+ function digestStrings(values) {
45
+ return sha256(values.slice().sort().join("\n"));
46
+ }
47
+ const KEYS = "(?:authorization|proxy-authorization|api[-_]?key|token|cookie|set-cookie|password|secret|session[-_]?id)";
48
+ const SENSITIVE_KEYS = `("${KEYS}"|'${KEYS}'|${KEYS})`;
49
+ const HEADER_VALUE = /(authorization|proxy-authorization|cookie|set-cookie)\s*:\s*.+$/gi;
50
+ const DOUBLE_QUOTED = new RegExp(`${SENSITIVE_KEYS}\\s*[:=]\\s*"(?:\\\\.|[^"\\\\])*"`, "gi");
51
+ const SINGLE_QUOTED = new RegExp(`${SENSITIVE_KEYS}\\s*[:=]\\s*'(?:\\\\.|[^'\\\\])*'`, "gi");
52
+ const UNCLOSED_DOUBLE = new RegExp(`${SENSITIVE_KEYS}\\s*[:=]\\s*"(?:\\\\.|[^"\\\\])*\\\\?$`, "gi");
53
+ const UNCLOSED_SINGLE = new RegExp(`${SENSITIVE_KEYS}\\s*[:=]\\s*'(?:\\\\.|[^'\\\\])*\\\\?$`, "gi");
54
+ const BARE_VALUE = new RegExp(`${SENSITIVE_KEYS}\\s*[:=]\\s*[^\\s,;'"\`)\\]}]+`, "gi");
55
+ const BEARER_TOKEN = /\bbearer\s+[A-Za-z0-9._~+/=-]+/gi;
56
+ const PLAIN_KEY = /\b(?:sk|pk|ak)-[a-zA-Z0-9_-]{16,}\b/g;
57
+ function sanitizeClauseText(text) {
58
+ let value = text;
59
+ const label = (key) => `${key.replace(/^["']|["']$/g, "")}=<redacted>`;
60
+ value = value.replace(BEARER_TOKEN, "bearer <redacted>");
61
+ value = value.replace(HEADER_VALUE, (_match, key) => `${key}=<redacted>`);
62
+ value = value.replace(DOUBLE_QUOTED, (_match, key) => label(key));
63
+ value = value.replace(SINGLE_QUOTED, (_match, key) => label(key));
64
+ value = value.replace(UNCLOSED_DOUBLE, (_match, key) => label(key));
65
+ value = value.replace(UNCLOSED_SINGLE, (_match, key) => label(key));
66
+ value = value.replace(BARE_VALUE, (_match, key) => label(key));
67
+ value = value.replace(PLAIN_KEY, (_match) => `${_match.slice(0, 3)}-<redacted>`);
68
+ value = value.replace(/https?:\/\/[^\s'"`,。)]+[?#][^\s'"`,。)]*/g, (match) => {
69
+ const cut = Math.min(...["?", "#"].map((marker) => {
70
+ const index = match.indexOf(marker);
71
+ return index === -1 ? Infinity : index;
72
+ }));
73
+ return cut === Infinity ? match : `${match.slice(0, cut)}\u2026`;
74
+ });
75
+ return value;
76
+ }
77
+ function sanitizeUrl(value) {
78
+ const cut = Math.min(...["?", "#"].map((marker) => {
79
+ const index = value.indexOf(marker);
80
+ return index === -1 ? Infinity : index;
81
+ }));
82
+ return cut === Infinity ? value : value.slice(0, cut);
83
+ }
84
+
85
+ //#endregion
86
+ //#region src/domain/manifest.ts
87
+ const COMMAND_SURFACE_MANIFEST = {
88
+ fileTools: [
89
+ "printf",
90
+ "echo",
91
+ "touch",
92
+ "cat"
93
+ ],
94
+ readTools: [
95
+ "cat",
96
+ "grep",
97
+ "rg",
98
+ "head",
99
+ "tail",
100
+ "wc",
101
+ "sed"
102
+ ],
103
+ runExecutables: [
104
+ "node",
105
+ "python",
106
+ "python3",
107
+ "pnpm",
108
+ "npm",
109
+ "yarn",
110
+ "bun",
111
+ "pytest",
112
+ "vitest",
113
+ "jest",
114
+ "tsc",
115
+ "eslint",
116
+ "mypy",
117
+ "ruff",
118
+ "prettier",
119
+ "go",
120
+ "cargo",
121
+ "make",
122
+ "cmake",
123
+ "git",
124
+ "mvn",
125
+ "gradle",
126
+ "tox",
127
+ "nox",
128
+ "dsh"
129
+ ],
130
+ pwshExternalExecutables: [
131
+ "node",
132
+ "python",
133
+ "python3",
134
+ "pnpm",
135
+ "npm",
136
+ "yarn",
137
+ "bun",
138
+ "pytest",
139
+ "vitest",
140
+ "jest",
141
+ "tsc",
142
+ "eslint",
143
+ "mypy",
144
+ "ruff",
145
+ "prettier",
146
+ "go",
147
+ "cargo",
148
+ "make",
149
+ "cmake",
150
+ "git",
151
+ "mvn",
152
+ "gradle",
153
+ "tox",
154
+ "nox",
155
+ "dsh"
156
+ ],
157
+ operationVerbs: [
158
+ {
159
+ op: "create",
160
+ pattern: "创建|生成|新建|touch|\\bcreates?\\b|\\bcreated\\b|\\bcreating\\b|\\bwrite\\b|写入"
161
+ },
162
+ {
163
+ op: "modify",
164
+ pattern: "修改|编辑|更改|modif(?:y|ies|ied|ying)|\\bedit\\b|改"
165
+ },
166
+ {
167
+ op: "read",
168
+ pattern: "读取|阅读|打开|读(?![A-Za-z0-9])|\\bread\\b"
169
+ },
170
+ {
171
+ op: "verify",
172
+ pattern: "验证|确认|确保|检查|verif(?:y|ies|ied|ying)|\\bconfirm\\b|\\bconfirms\\b|\\bconfirmed\\b|\\bensure\\b"
173
+ },
174
+ {
175
+ op: "run",
176
+ pattern: "运行|执行|拉取|获取|同步|更新|下载|安装|部署|上传|提交|推送|发布|升级|重启|重新启动|重载|\\brun\\b|execute(?:d)?|\\bpull\\b|\\bfetch\\b|\\bclone\\b|\\bsync\\b|\\bupdate\\b|\\binstall\\b|\\bdeploy\\b|\\bcommit\\b|\\bpush\\b|\\brelease\\b|\\bdownload\\b|\\bupload\\b|\\brestart\\b|\\breload\\b|\\breboot\\b"
177
+ }
178
+ ]
179
+ };
180
+ const OPERATION_ORDER = [
181
+ "create",
182
+ "modify",
183
+ "read",
184
+ "verify",
185
+ "run"
186
+ ];
187
+ /**
188
+ * Validate the manifest invariants the parsers and capture depend on:
189
+ * - every collection is non-empty, sorted-case-insensitively, and duplicate-free
190
+ * - external executables mirror the POSIX run set exactly
191
+ * - verb groups exist once, in the documented priority order, and compile
192
+ * (they compile by construction when validated, so a typo cannot silently
193
+ * widen or break the surface).
194
+ */
195
+ function validateManifest(manifest = COMMAND_SURFACE_MANIFEST) {
196
+ const issues = [];
197
+ const sets = [
198
+ ["fileTools", manifest.fileTools],
199
+ ["readTools", manifest.readTools],
200
+ ["runExecutables", manifest.runExecutables],
201
+ ["pwshExternalExecutables", manifest.pwshExternalExecutables]
202
+ ];
203
+ for (const [name, values] of sets) {
204
+ if (!values.length) issues.push({
205
+ path: name,
206
+ message: "must not be empty"
207
+ });
208
+ const sorted = [...values].map((value) => value.toLowerCase()).sort();
209
+ if (sorted.some((value, index) => index > 0 && value === sorted[index - 1])) issues.push({
210
+ path: name,
211
+ message: "contains duplicates"
212
+ });
213
+ }
214
+ if (manifest.runExecutables.length !== manifest.pwshExternalExecutables.length || [...manifest.runExecutables].map((value) => value.toLowerCase()).sort().join(",") !== [...manifest.pwshExternalExecutables].map((value) => value.toLowerCase()).sort().join(",")) issues.push({
215
+ path: "pwshExternalExecutables",
216
+ message: "must mirror runExecutables exactly"
217
+ });
218
+ const seenOps = /* @__PURE__ */ new Set();
219
+ for (const entry of manifest.operationVerbs) {
220
+ if (seenOps.has(entry.op)) issues.push({
221
+ path: `operationVerbs.${entry.op}`,
222
+ message: "duplicate operation group"
223
+ });
224
+ seenOps.add(entry.op);
225
+ try {
226
+ new RegExp(entry.pattern, "i");
227
+ } catch {
228
+ issues.push({
229
+ path: `operationVerbs.${entry.op}`,
230
+ message: `uncompilable pattern: ${entry.pattern}`
231
+ });
232
+ }
233
+ }
234
+ const order = manifest.operationVerbs.map((entry) => entry.op);
235
+ const expected = OPERATION_ORDER.filter((operation) => seenOps.has(operation));
236
+ if (order.join(",") !== expected.join(",")) issues.push({
237
+ path: "operationVerbs",
238
+ message: `priority order must be ${expected.join(" → ")}`
239
+ });
240
+ return issues;
241
+ }
242
+
243
+ //#endregion
244
+ //#region src/domain/capture.ts
245
+ const CLAUSE_PATTERNS = [["prohibition", /^(?:(?:do not|don't|never)(?![A-Za-z0-9_./@\\-])|禁止|不要|不得)\s*(.+)$/i], ["acceptance", /^(?:verify|confirm|ensure|验收|确认|确保)\s*(.+)$/i]];
246
+ function classifyClause(text) {
247
+ const normalizedText = normalizeClause(text);
248
+ for (const [kind, pattern] of CLAUSE_PATTERNS) {
249
+ const match = normalizedText.match(pattern);
250
+ if (match) return {
251
+ kind,
252
+ body: normalizeClause(match[1].replace(/^[::,,\s]+/, ""))
253
+ };
254
+ }
255
+ return {
256
+ kind: "requirement",
257
+ body: normalizedText
258
+ };
259
+ }
260
+ const METHOD_TOOL = "(?:bash|shell|powershell|pwsh|git|read|write|edit|node|python|python3|npm|pnpm|tsc|vitest)";
261
+ const METHOD_ALIASES = {
262
+ powershell: "pwsh",
263
+ python3: "python"
264
+ };
265
+ const METHOD_PATTERNS = [
266
+ new RegExp(`(?:用|使用|通过|借助|利用|以)\\s*(${METHOD_TOOL})\\b`, "i"),
267
+ new RegExp(`\\b(?:via|using|use|with)\\s+(?:the\\s+)?(${METHOD_TOOL})\\b`, "i"),
268
+ new RegExp(`\\b(${METHOD_TOOL})\\s+(?:创建|写入|生成|修改|执行|运行|rename|create|write|modify)\\b`, "i")
269
+ ];
270
+ /**
271
+ * Detect an explicitly named tool/method in a clause ("使用 bash 创建",
272
+ * "via bash", "bash to create"). Returns the canonical tool id (e.g. 'bash')
273
+ * or undefined when no explicit method is named.
274
+ */
275
+ function extractMethod(text) {
276
+ for (const pattern of METHOD_PATTERNS) {
277
+ const match = text.match(pattern);
278
+ if (match) {
279
+ const raw = match[1].toLowerCase();
280
+ return METHOD_ALIASES[raw] ?? raw;
281
+ }
282
+ }
283
+ }
284
+ const OPERATION_PATTERNS = COMMAND_SURFACE_MANIFEST.operationVerbs.map((entry) => [entry.op, new RegExp(entry.pattern, "i")]);
285
+ /**
286
+ * Whether a whole user message reads as an informational report (acceptance
287
+ * receipt, progress summary, pasted log) rather than a task instruction.
288
+ * Evaluation is deliberately conservative: reports are detected only when the
289
+ * shape is clearly report-like (markdown headings, bold key/value lines, list
290
+ * or table rows, evidence terms) AND no sentence opens with an imperative, and
291
+ * any question mark keeps the message a task. False positives here would drop
292
+ * real instructions, so plain short sentences are never treated as reports.
293
+ */
294
+ function isInformationalMessage(text) {
295
+ if (!text.trim()) return false;
296
+ if (/[??]|是否|是不是/.test(text)) return false;
297
+ const lines = text.split(/\r?\n/);
298
+ const titledLines = lines.filter((line) => /^\s{0,3}#{1,6}\s+/.test(line)).length;
299
+ const evidenceLines = lines.filter((line) => /^\s*(?:[-*|]\s{0,2}|\*\*.+?\*\*)/.test(line)).length;
300
+ const evidenceTerms = (text.match(/\b(?:commit|passed|failed|exit\s+code|checkpoint|verify|回执|汇总|状态|通过|全绿|验收|读回|回读)\b|✓|\b[0-9a-f]{40}\b/g) ?? []).length;
301
+ if (!(titledLines >= 1 && evidenceTerms >= 2 || evidenceLines >= 2 && evidenceTerms >= 2 || evidenceTerms >= 4)) return false;
302
+ const imperativeLead = /^(?:请|请你|麻烦|帮我|需要你|你看看|看一下|检查一下|分析|列出|回顾|修复|推送|安装|确认|验证|能否|能不能)/i;
303
+ return !text.split(/(?<=[。!?;\n])|(?<=[.!?])(?=\s|$)/).some((sentence) => imperativeLead.test(sentence.trim()));
304
+ }
305
+ /**
306
+ * Detect an explicit operation/effect in a clause ("创建" → create,
307
+ * "读取" → read, "运行" → run). Returns the first operation named, or undefined
308
+ * when the clause requests no specific effect.
309
+ */
310
+ function extractOperation(text) {
311
+ for (const [operation, pattern] of OPERATION_PATTERNS) if (pattern.test(text)) return operation;
312
+ }
313
+ const EXTENSION_TAIL = new RegExp(`\\.(?:ts|tsx|js|jsx|mjs|cjs|py|rs|go|java|kt|c|cpp|h|hpp|cs|rb|php|vue|svelte|md|mdx|json|jsonc|yml|yaml|toml|ini|cfg|sh|bash|zsh|fish|ps1|html|css|scss|less|sql|txt|lock|mod|sum|env|patch|diff|pkl|tf|hcl|proto)(?:$|[^A-Za-z0-9])`, "i");
314
+ function isArtifactCandidate(value) {
315
+ return EXTENSION_TAIL.test(value);
316
+ }
317
+ /** Wrapped path spellings: backticks, double/single quotes, and parentheses. */
318
+ const WRAPPED_PATH = /`([^`]+)`|"([^"]+)"|'([^']+)'|\(([^()]+)\)/g;
319
+ function extractArtifactPaths(text) {
320
+ const found = /* @__PURE__ */ new Set();
321
+ const push = (candidate) => {
322
+ const trimmed = candidate.trim();
323
+ if (trimmed && isArtifactCandidate(trimmed)) found.add(trimmed);
324
+ };
325
+ for (const match of text.matchAll(WRAPPED_PATH)) push(match[1] ?? match[2] ?? match[3] ?? match[4] ?? "");
326
+ for (const token of text.split(/[\s,;,;]+/)) {
327
+ const bare = token.replace(/^[('"]+|['")]+$/g, "").replace(/[。!?;.!?,,;::]+$/, "");
328
+ if (bare && !bare.includes("`") && isArtifactCandidate(bare)) found.add(bare);
329
+ }
330
+ return [...found];
331
+ }
332
+ function segmentClauses(text) {
333
+ const normalized = normalizeClause(text);
334
+ if (!normalized) return [];
335
+ const parts = normalized.split(/(?<=[。!?;])|(?<=[.!?])(?=\s|$)|(?<=(?:^|[\s。!?;.!?,,;:]))(?=(?:(?:do not|don't|never)(?![A-Za-z0-9_./@\\-])|禁止|不要|不得))/i).map((part) => part.trim()).filter(Boolean);
336
+ const segments = [];
337
+ for (const part of parts) {
338
+ const { kind, body } = classifyClause(part);
339
+ segments.push({
340
+ kind,
341
+ body,
342
+ paths: extractArtifactPaths(body)
343
+ });
344
+ }
345
+ return segments;
346
+ }
347
+ /**
348
+ * Build a GuardItem from an already-classified clause body and a resolved
349
+ * verification subject/surface.
350
+ */
351
+ function captureItem(kind, body, sourceMessageId, id, revision, subject, surface, method, operation) {
352
+ const sanitized = sanitizeClauseText(body);
353
+ return {
354
+ id,
355
+ revision,
356
+ kind,
357
+ sourceMessageId,
358
+ normalizedText: sanitized,
359
+ textSha256: sha256(sanitized),
360
+ status: "pending",
361
+ verification: kind === "prohibition" ? {
362
+ enforced: false,
363
+ surface,
364
+ subject
365
+ } : {
366
+ enforced: true,
367
+ surface,
368
+ subject,
369
+ method,
370
+ operation
371
+ }
372
+ };
373
+ }
374
+ /**
375
+ * Capture one contract clause. Every captured item receives a concrete
376
+ * verification contract: a named artifact path (artifact surface) or the
377
+ * session scope (scope surface), so an unrelated file read can never close it.
378
+ */
379
+ function captureClause(text, sourceMessageId, id, revision, scope = {}) {
380
+ const { kind, body } = classifyClause(text);
381
+ const path$1 = extractArtifactPaths(sanitizeClauseText(body))[0] ?? "";
382
+ const surface = path$1 ? "artifact" : "scope";
383
+ return captureItem(kind, body, sourceMessageId, id, revision, path$1 || scope.cwd || "scope", surface, extractMethod(body), extractOperation(body));
384
+ }
385
+
386
+ //#endregion
387
+ //#region src/domain/matching.ts
388
+ const STATE_VERIFICATION_CAPABILITIES = new Set([
389
+ "filesystem-read",
390
+ "web-fetch",
391
+ "deterministic-check"
392
+ ]);
393
+ /** Capabilities that may close an explicit `verify` contract. */
394
+ const VERIFY_CAPABILITIES = new Set([
395
+ "filesystem-read",
396
+ "verify",
397
+ "web-fetch",
398
+ "deterministic-check"
399
+ ]);
400
+ /**
401
+ * Whether this evidence closes the artifact/scope facet of the item: a success
402
+ * outcome, a verifying capability, and (when the contract names them) a match
403
+ * on the canonical subject and the surface. Both sides of the subject
404
+ * comparison run through the shared {@link canonicalizePath}, so Windows
405
+ * drive-letter case, separator kind, `.`/`..`, and duplicate separators are
406
+ * treated as equal while POSIX stays case-sensitive.
407
+ */
408
+ function stateVerificationFacetCovered(item, evidence) {
409
+ if (evidence.outcome !== "success") return false;
410
+ if (!evidence.capabilities.some((capability) => STATE_VERIFICATION_CAPABILITIES.has(capability))) return false;
411
+ const { subject, surface, operation } = item.verification;
412
+ if (subject && !evidence.subjects.some((subjectValue) => canonicalizePath(subjectValue) === canonicalizePath(subject))) return false;
413
+ if (surface && !evidence.surfaces.includes(surface)) return false;
414
+ if (operation === "create" || operation === "write" || operation === "modify") {
415
+ if (!(evidence.operations ?? []).some((entry) => (entry.op === "read" || entry.op === "verify") && (!subject || entry.path !== void 0 && canonicalizePath(entry.path) === canonicalizePath(subject))) && !(evidence.capabilities.includes("deterministic-check") && (!subject || evidence.subjects.some((value) => canonicalizePath(value) === canonicalizePath(subject))))) return false;
416
+ }
417
+ return true;
418
+ }
419
+ function artifactFacetCovered(item, evidence) {
420
+ return stateVerificationFacetCovered(item, evidence);
421
+ }
422
+ function methodIdentityMatches(item, evidence) {
423
+ const method = item.verification.method;
424
+ if (!method || evidence.outcome !== "success") return false;
425
+ const toolMethod = DSH_TOOL_METHODS.has(method);
426
+ const toolMatch = toolMethod ? method === "bash" || method === "shell" ? evidence.toolName === "bash" || evidence.toolName === "shell" : evidence.toolName === method : false;
427
+ const executableMatch = !toolMethod && (evidence.executables?.some((value) => value.toLowerCase() === method) ?? false);
428
+ return toolMatch || executableMatch;
429
+ }
430
+ function isVerifyingCapability(evidence) {
431
+ return evidence.capabilities.some((capability) => STATE_VERIFICATION_CAPABILITIES.has(capability));
432
+ }
433
+ /**
434
+ * Combined verification facet: success, capability, subject, surface and any
435
+ * required method identity must all come from this one evidence.
436
+ */
437
+ function verifyFacetCovered(item, evidence) {
438
+ if (evidence.outcome !== "success") return false;
439
+ if (!evidence.capabilities.some((capability) => VERIFY_CAPABILITIES.has(capability))) return false;
440
+ const { subject, surface, method } = item.verification;
441
+ if (subject && !evidence.subjects.some((subjectValue) => canonicalizePath(subjectValue) === canonicalizePath(subject))) return false;
442
+ if (surface && !evidence.surfaces.includes(surface)) return false;
443
+ return !method || methodIdentityMatches(item, evidence);
444
+ }
445
+ /**
446
+ * DSH tool ids that can appear as `evidence.toolName`. An explicit method that
447
+ * names one of these is a tool constraint; anything else (pnpm, git, node, …) is
448
+ * a shell executable that runs inside a command tool.
449
+ */
450
+ const DSH_TOOL_METHODS = new Set([
451
+ "bash",
452
+ "shell",
453
+ "pwsh",
454
+ "read",
455
+ "write",
456
+ "edit",
457
+ "read_file",
458
+ "write_file",
459
+ "edit_file",
460
+ "web_search",
461
+ "web_fetch",
462
+ "web_fetch_url"
463
+ ]);
464
+ /**
465
+ * Operation compatibility: a contract operation is closed by the evidence
466
+ * operations that produce the same effect (create/write are the same artifact
467
+ * production family; verify is closed by a read, run, or verify check).
468
+ */
469
+ const OPERATION_COMPATIBLE = {
470
+ create: ["create", "write"],
471
+ write: ["create", "write"],
472
+ modify: [
473
+ "modify",
474
+ "write",
475
+ "create"
476
+ ],
477
+ read: ["read"],
478
+ run: ["run"],
479
+ verify: [
480
+ "read",
481
+ "verify",
482
+ "run"
483
+ ]
484
+ };
485
+ /**
486
+ * Whether this evidence satisfies an explicitly required tool/method facet:
487
+ * a success outcome, the right identity (DSH tool name for tool constraints,
488
+ * the invoked executable for executable constraints), and — when the contract
489
+ * names a subject and/or operation — an operation performed on the same
490
+ * canonical subject. Mentioning a file in a command (`echo guard-demo.txt`) is
491
+ * not an operation and cannot satisfy a create requirement.
492
+ */
493
+ /** The effect facet proves what the evidence actually did, not merely who ran it. */
494
+ function effectFacetCovered(item, evidence) {
495
+ if (evidence.outcome !== "success") return false;
496
+ const { subject, surface, operation, method } = item.verification;
497
+ if (!operation) return false;
498
+ if (method && !methodIdentityMatches(item, evidence)) return false;
499
+ const compatible = OPERATION_COMPATIBLE[operation] ?? [];
500
+ const effects = evidence.operations ?? [];
501
+ if (surface === "artifact" && subject) {
502
+ const target = canonicalizePath(subject);
503
+ return effects.some((entry) => compatible.includes(entry.op) && entry.path !== void 0 && canonicalizePath(entry.path) === target);
504
+ }
505
+ if (surface === "scope") return effects.some((entry) => compatible.includes(entry.op));
506
+ return false;
507
+ }
508
+ /** The method facet proves only the required tool or executable identity. */
509
+ function methodFacetCovered(item, evidence) {
510
+ if (item.verification.operation === void 0 && item.verification.method) return false;
511
+ return methodIdentityMatches(item, evidence);
512
+ }
513
+ /** Whether this evidence performed the run operation on the contract subject. */
514
+ function runFacetCovered(item, evidence) {
515
+ if (evidence.outcome !== "success") return false;
516
+ const operations = evidence.operations ?? [];
517
+ const { subject } = item.verification;
518
+ if (subject) {
519
+ const target = canonicalizePath(subject);
520
+ return operations.some((entry) => entry.op === "run" && entry.path !== void 0 && canonicalizePath(entry.path) === target);
521
+ }
522
+ return operations.some((entry) => entry.op === "run");
523
+ }
524
+ function evidenceCoverage(item, evidence) {
525
+ return {
526
+ artifact: artifactFacetCovered(item, evidence),
527
+ effect: effectFacetCovered(item, evidence),
528
+ method: methodFacetCovered(item, evidence),
529
+ verify: verifyFacetCovered(item, evidence),
530
+ run: runFacetCovered(item, evidence)
531
+ };
532
+ }
533
+ /**
534
+ * Whether a single evidence can close an enforced item on its own. This is the
535
+ * conservative per-evidence check; the certifier additionally verifies that the
536
+ * whole binding satisfies every required facet.
537
+ */
538
+ function evidenceMatchesItem(item, evidence) {
539
+ if (evidence.outcome !== "success") return false;
540
+ if (!item.verification.enforced) return true;
541
+ const coverage = evidenceCoverage(item, evidence);
542
+ return coverage.artifact || coverage.effect || coverage.method || coverage.verify || coverage.run;
543
+ }
544
+ /**
545
+ * Whether a whole binding (a set of evidence ids) satisfies the fixed v0.1
546
+ * binding invariants:
547
+ *
548
+ * - run: the method (or run) evidence alone closes the contract — no extra
549
+ * read or unrelated deterministic-check is required.
550
+ * - create/write/modify: BOTH a method evidence (method + operation + subject)
551
+ * and a state-verification evidence on the same subject are required.
552
+ * - read: a successful read evidence matching method, read operation and
553
+ * subject satisfies the method side and the object side at once.
554
+ * - verify: only explicit read/verify/deterministic-check evidence on the
555
+ * subject closes; unrelated scope calls cannot be spliced in.
556
+ * - explicit method without a parsable operation fails closed.
557
+ * - a non-enforced item (prohibition) is acknowledged by any valid success
558
+ * evidence.
559
+ */
560
+ function bindingSatisfies(projection, item, evidenceIds) {
561
+ if (!item.verification.enforced) return evidenceIds.every((id) => {
562
+ const value = projection.evidence.get(id);
563
+ return !!value && value.epoch === projection.epoch && value.outcome === "success";
564
+ });
565
+ const { method, operation } = item.verification;
566
+ if (method && operation === void 0) return false;
567
+ let artifact = false;
568
+ let effect = false;
569
+ let verify = false;
570
+ let run = false;
571
+ const stateEvidenceIds = /* @__PURE__ */ new Set();
572
+ const effectEvidenceIds = /* @__PURE__ */ new Set();
573
+ for (const id of evidenceIds) {
574
+ const value = projection.evidence.get(id);
575
+ if (!value || value.epoch !== projection.epoch) return false;
576
+ const coverage = evidenceCoverage(item, value);
577
+ if (!coverage.artifact && !coverage.effect && !coverage.method && !coverage.verify && !coverage.run) return false;
578
+ artifact = artifact || coverage.artifact;
579
+ effect = effect || coverage.effect;
580
+ verify = verify || coverage.verify;
581
+ run = run || coverage.run;
582
+ if (coverage.artifact) stateEvidenceIds.add(id);
583
+ if (coverage.effect) effectEvidenceIds.add(id);
584
+ }
585
+ switch (operation) {
586
+ case "run": return effect;
587
+ case "read": return effect;
588
+ case "create":
589
+ case "write":
590
+ case "modify": {
591
+ const independentState = [...stateEvidenceIds].some((id) => !effectEvidenceIds.has(id));
592
+ const independentEffect = [...effectEvidenceIds].some((id) => !stateEvidenceIds.has(id));
593
+ return effect && independentEffect && independentState;
594
+ }
595
+ case "verify": return verify;
596
+ default: return artifact;
597
+ }
598
+ }
599
+
600
+ //#endregion
601
+ //#region src/domain/recovery.ts
602
+ const DEFAULT_RECOVERY_CHAR_BUDGET = 4e3;
603
+ const MAX_RECOVERY_ITEMS = 8;
604
+ const MAX_RECOVERY_EVIDENCE = 20;
605
+ const MORE_ITEMS_RULE = (remaining) => `…(${remaining} more open items; the full list is in the checkpoint tool response)`;
606
+ const MORE_EVIDENCE_RULE = (remaining) => `…(${remaining} more evidence rows)`;
607
+ const COMPLETION_RULE = "Obtain a Context Guard checkpoint from matching durable evidence before claiming completion.";
608
+ /**
609
+ * An actionable one-line hint for how an open item's verification contract can
610
+ * be closed. It never weakens the contract; it only names the missing facet so
611
+ * the agent can produce the right evidence shape instead of reverse-engineering
612
+ * the guard. When `evidenceIds` is given, the hint accounts for what those
613
+ * evidence already cover.
614
+ */
615
+ function closingHint(projection, item, evidenceIds) {
616
+ const verification = item.verification;
617
+ const parts = [];
618
+ if (evidenceIds?.length) {
619
+ if (!evidenceIds.map((id) => projection.evidence.get(id)).filter((value) => value !== void 0).map((value) => evidenceCoverage(item, value)).some((facet) => facet.artifact || facet.effect || facet.method || facet.verify || facet.run)) parts.push("cited evidence matches no facet");
620
+ }
621
+ if (verification.method) parts.push(`method '${verification.method}'`);
622
+ if (verification.subject && verification.surface === "artifact") parts.push(`subject '${verification.subject}'`);
623
+ if (verification.subject && verification.surface === "scope") parts.push("in the scope directory");
624
+ const operation = verification.operation;
625
+ if (operation === "run") parts.push("needs a scope run effect: a whitelisted executable (git/pnpm/python/dsh/...) without pipes, `;` or `&&`, e.g. `python -m unittest`");
626
+ else if (operation === "create" || operation === "write" || operation === "modify") parts.push("needs an effect evidence AND an independent same-subject state verification (read tool or a deterministic check)");
627
+ else if (operation === "verify") parts.push("needs a read or deterministic-check evidence on the contract subject");
628
+ else if (operation === "read") parts.push("needs a read evidence on the contract subject");
629
+ else parts.push("needs a state-verification evidence (read tool, or a deterministic check run in scope) matching the subject");
630
+ return parts.join("; ");
631
+ }
632
+ function openItems$1(projection) {
633
+ return [...projection.items.values()].filter((item) => item.status === "pending").sort((a, b) => a.revision - b.revision || (a.id < b.id ? -1 : 1));
634
+ }
635
+ /**
636
+ * Content identity of a rendered recovery packet, bound to the contract
637
+ * revision and epoch it was rendered from. The runtime compares digests before
638
+ * re-injecting, so a repeatedly re-armed recovery with unchanged content is
639
+ * injected once instead of looping (v0.2.1).
640
+ */
641
+ function recoveryDigest(packet, projection) {
642
+ return sha256(JSON.stringify({
643
+ packet,
644
+ revision: projection.contractRevision,
645
+ epoch: projection.epoch
646
+ }));
647
+ }
648
+ function renderRecoveryPacket(projection, options = {}) {
649
+ const budget = options.charBudget ?? DEFAULT_RECOVERY_CHAR_BUDGET;
650
+ const lines = [];
651
+ let used = 0;
652
+ const push = (line) => {
653
+ if (used + line.length + 1 > budget) return false;
654
+ lines.push(line);
655
+ used += line.length + 1;
656
+ return true;
657
+ };
658
+ const items = openItems$1(projection);
659
+ const listedIds = /* @__PURE__ */ new Set();
660
+ const pushItems = (list, render) => {
661
+ let count = 0;
662
+ for (const item of list) {
663
+ if (count >= MAX_RECOVERY_ITEMS) {
664
+ push(MORE_ITEMS_RULE(list.length - count));
665
+ return true;
666
+ }
667
+ if (!push(render(item))) return false;
668
+ listedIds.add(item.id);
669
+ count += 1;
670
+ }
671
+ return true;
672
+ };
673
+ if (!pushItems(items.filter((item) => item.kind === "requirement"), (item) => `[${item.id}] ${item.normalizedText}`)) return finalize();
674
+ if (!pushItems(items.filter((item) => item.kind === "prohibition"), (item) => `[${item.id}] DO NOT ${item.normalizedText}`)) return finalize();
675
+ if (!pushItems(items.filter((item) => item.kind === "acceptance"), (item) => `[${item.id}] VERIFY ${item.normalizedText}`)) return finalize();
676
+ const citableEvidence = [...projection.evidence.values()].filter((evidence) => evidence.epoch === projection.epoch && evidence.outcome === "success").sort((a, b) => a.id < b.id ? -1 : 1);
677
+ let evidenceCount = 0;
678
+ for (const evidence of citableEvidence) {
679
+ if (evidenceCount >= MAX_RECOVERY_EVIDENCE) {
680
+ push(MORE_EVIDENCE_RULE(citableEvidence.length - evidenceCount));
681
+ break;
682
+ }
683
+ if (!push(`evidence ${evidence.id} ${evidence.toolName} ${evidence.subjects.join(",") || "-"} ${evidence.surfaces.join(",")}`)) return finalize();
684
+ evidenceCount += 1;
685
+ }
686
+ for (const item of [...projection.items.values()].filter((item$1) => item$1.status === "superseded")) if (item.supersededBy && !push(`[${item.id} -> ${item.supersededBy}]`)) return finalize();
687
+ for (const binding of options.rejectedBindings ?? []) if (!push(`rejected ${binding.itemId}: ${binding.reason}`)) return finalize();
688
+ for (const item of items) {
689
+ if (item.kind === "prohibition" || !listedIds.has(item.id)) continue;
690
+ push(`closing hint [${item.id}]: ${closingHint(projection, item)}`);
691
+ }
692
+ push(COMPLETION_RULE);
693
+ return finalize();
694
+ function finalize() {
695
+ return lines.join("\n");
696
+ }
697
+ }
698
+
699
+ //#endregion
700
+ //#region src/domain/checkpoint.ts
701
+ function certifyCheckpoint(projection, bindings, id) {
702
+ if (projection.integrity !== "valid") return {
703
+ status: "unknown",
704
+ contractRevision: projection.contractRevision,
705
+ openItems: openItems(projection),
706
+ rejectedBindings: []
707
+ };
708
+ const rejectedBindings = [];
709
+ for (const binding of bindings) {
710
+ const item = projection.items.get(binding.itemId);
711
+ if (!item || item.status === "superseded") {
712
+ rejectedBindings.push({
713
+ itemId: binding.itemId,
714
+ reason: "item is missing or superseded"
715
+ });
716
+ continue;
717
+ }
718
+ if (!binding.evidenceIds.length) {
719
+ rejectedBindings.push({
720
+ itemId: binding.itemId,
721
+ reason: "no evidence cited",
722
+ hint: closingHint(projection, item)
723
+ });
724
+ continue;
725
+ }
726
+ if (!bindingSatisfies(projection, item, binding.evidenceIds)) {
727
+ rejectedBindings.push({
728
+ itemId: binding.itemId,
729
+ reason: "evidence does not match the current verification contract",
730
+ hint: closingHint(projection, item, binding.evidenceIds)
731
+ });
732
+ continue;
733
+ }
734
+ }
735
+ const open = openItems(projection).filter((itemId) => !bindings.some((binding) => binding.itemId === itemId));
736
+ if (rejectedBindings.length || open.length) return {
737
+ status: "incomplete",
738
+ contractRevision: projection.contractRevision,
739
+ openItems: openItems(projection),
740
+ rejectedBindings
741
+ };
742
+ const openDigest = digestStrings(openItems(projection));
743
+ const bindingDigest = sha256(JSON.stringify(bindings));
744
+ const checkpoint = {
745
+ id,
746
+ epoch: projection.epoch,
747
+ contractRevision: projection.contractRevision,
748
+ openDigest,
749
+ bindingDigest,
750
+ bindings,
751
+ result: "certified"
752
+ };
753
+ projection.checkpoints.push(checkpoint);
754
+ for (const binding of bindings) projection.items.get(binding.itemId).status = "passed";
755
+ return {
756
+ status: "certified",
757
+ contractRevision: projection.contractRevision,
758
+ openItems: [],
759
+ rejectedBindings: [],
760
+ checkpoint
761
+ };
762
+ }
763
+ function openItems(projection) {
764
+ return [...projection.items.values()].filter((item) => item.status === "pending" && item.kind !== "prohibition").map((item) => item.id);
765
+ }
766
+
767
+ //#endregion
768
+ //#region src/domain/conversation.ts
769
+ /**
770
+ * Punctuation and whitespace that may surround a bare progression phrase
771
+ * without turning it into sentence content.
772
+ */
773
+ const PUNCT = String.raw`[\s。,、;:!?.,;:!?\-*"'“”‘’()().…~~]`;
774
+ /**
775
+ * Session-layer phrases that acknowledge or advance the conversation without
776
+ * stating a task. Longer forms come first so the alternation consumes them
777
+ * before their prefixes.
778
+ */
779
+ const PROGRESSION_SOURCE = String.raw`(?:继续执行|继续吧|请继续|继续|接着做|接着|下一步|没问题|知道了|明白了|了解|好的?|是的?|对的?|收到|可以|行|嗯+|continue|go on|go ahead|keep going|proceed|okay|ok|yes|sure|right|next)`;
780
+ const PROGRESSION_WHOLE = new RegExp(`^${PUNCT}*${PROGRESSION_SOURCE}${PUNCT}*$`, "i");
781
+ const PROGRESSION_LEAD = new RegExp(`^${PROGRESSION_SOURCE}${PUNCT}+`, "i");
782
+ const PROGRESSION_ANYWHERE = new RegExp(PROGRESSION_SOURCE, "gi");
783
+ /**
784
+ * Clause-leading prohibition keywords. A message that opens with one is a
785
+ * captured prohibition, never a meta comment.
786
+ */
787
+ const PROHIBITION_LEAD = /^(?:(?:do not|don't|never)(?![A-Za-z0-9_./@\\-])|禁止|不要|不得)/i;
788
+ /**
789
+ * Question markers: a question mark, an interrogative pronoun/particle, or an
790
+ * explicit request-for-answer phrase.
791
+ */
792
+ const QUESTION_TERMS = /[??]|什么|为什么|怎么|如何|是否|是不是|哪|谁|啥|吗|呢|对不对|正常吗|bug吗|有问题吗|有必要|合理吗|可否|能否|能不能|请问|问一下/;
793
+ /**
794
+ * Meta-comment/objection leads (no question mark required). `不是` requires
795
+ * trailing punctuation so negated statements ("不是都要推送") stay fail-closed.
796
+ */
797
+ const META_COMMENT_LEAD = /^(?:不是[,,。;;::\s]|你(?:这|光|啥|怎么|什么|到底|就)|我(?:只是|就是|想|问|建议|认为|觉得)|这(?:有|什么)意义|有什么用|有什么意义)/;
798
+ /** Diagnostic/inspection verbs: mentioning them alone is never a task feature. */
799
+ const META_VERBS = /确认下|看看|看一下|想问|确认|验证|检查|查看|分析|解释|说明|排查|定位|诊断|评估|考虑|建议|讨论|复查|核对|盘点|复盘|问|看/g;
800
+ /**
801
+ * Operation verbs that indicate a real task effect. English verbs are
802
+ * word-bounded so "latest" does not contain "test". The classifier vocabulary
803
+ * is intentionally independent from the command-surface manifest.
804
+ */
805
+ const OPERATION_VERBS = /创建|生成|新建|写入|修改|编辑|运行|执行|编写|撰写|起草|整理|总结|记录|更新|修复|改进|解决|处理|推送|发布|安装|升级|提交|下载|上传|拉取|同步|部署|重启|测试|写|\b(?:build|create|write|modify|run|fix|update|install|push|publish|test)\b/gi;
806
+ const NEGATIONS = /没有|并无|不存在|无需|不用|不需要|尚未|还未|没|未|不是/;
807
+ function excludedRanges(text) {
808
+ const ranges = [];
809
+ for (const pattern of [PROGRESSION_ANYWHERE, META_VERBS]) {
810
+ pattern.lastIndex = 0;
811
+ for (const match of text.matchAll(pattern)) {
812
+ const start = match.index;
813
+ ranges.push([start, start + match[0].length]);
814
+ }
815
+ }
816
+ return ranges;
817
+ }
818
+ /** The negation filter is scoped to the clause (sentence or comma segment). */
819
+ function isNegatedInClause(text, verbStart) {
820
+ const clause = text.slice(0, verbStart).split(/[。!?;.!?;,,\r\n]/).pop() ?? "";
821
+ return NEGATIONS.test(clause);
822
+ }
823
+ function hasOperationVerb(text) {
824
+ const excluded = excludedRanges(text);
825
+ for (const match of text.matchAll(OPERATION_VERBS)) {
826
+ const start = match.index;
827
+ if (excluded.some(([from, to]) => start >= from && start < to)) continue;
828
+ if (isNegatedInClause(text, start)) continue;
829
+ return true;
830
+ }
831
+ return false;
832
+ }
833
+ function hasStrongTaskFeature(text) {
834
+ if (extractArtifactPaths(text).length > 0) return true;
835
+ if (extractMethod(text) !== void 0) return true;
836
+ return hasOperationVerb(text);
837
+ }
838
+ /**
839
+ * Classify a direct user message (or one clause of it) as an actionable
840
+ * `instruction` or a session-layer `conversational` utterance. Only
841
+ * conversational results drop capture, so the classifier fails closed:
842
+ * everything it cannot confidently recognize as session-layer talk stays an
843
+ * instruction and is captured exactly as before.
844
+ *
845
+ * Order matters: progression and prohibition leads first, then strong task
846
+ * features (artifact path, explicit method, or a non-negated operation verb
847
+ * outside progression/meta spans), then the meta-question and meta-comment
848
+ * forms, and finally a progression lead over a featureless remainder.
849
+ */
850
+ function classifyUserInteraction(text) {
851
+ const normalized = normalizeClause(text);
852
+ if (!normalized) return "instruction";
853
+ if (PROGRESSION_WHOLE.test(normalized)) return "conversational";
854
+ if (PROHIBITION_LEAD.test(normalized)) return "instruction";
855
+ if (hasStrongTaskFeature(normalized)) return "instruction";
856
+ if (QUESTION_TERMS.test(normalized)) return "conversational";
857
+ if (META_COMMENT_LEAD.test(normalized)) return "conversational";
858
+ if (PROGRESSION_LEAD.test(normalized)) return "conversational";
859
+ return "instruction";
860
+ }
861
+
862
+ //#endregion
863
+ //#region src/domain/shell-parse.ts
864
+ const TWO_CHAR_OPS = new Set([
865
+ "&&",
866
+ "||",
867
+ ">>",
868
+ "<<",
869
+ "<&",
870
+ ">&",
871
+ "|&"
872
+ ]);
873
+ const STATEMENT_OPS = new Set([
874
+ "&&",
875
+ "||",
876
+ "|",
877
+ "|&",
878
+ "&",
879
+ ";",
880
+ "\n",
881
+ "(",
882
+ ")"
883
+ ]);
884
+ /**
885
+ * Quote-aware shell tokenizer. Single quotes are literal, double quotes allow
886
+ * `\` escapes, and backslash escapes are honored outside quotes. Unterminated
887
+ * quotes mark the input as malformed.
888
+ */
889
+ function tokenizeShell(command) {
890
+ const tokens = [];
891
+ let index = 0;
892
+ let malformed = false;
893
+ const length = command.length;
894
+ while (index < length) {
895
+ const char = command[index];
896
+ if (char === "\n" || char === "\r") {
897
+ tokens.push({
898
+ kind: "op",
899
+ value: "\n",
900
+ quoted: false
901
+ });
902
+ index += char === "\r" && command[index + 1] === "\n" ? 2 : 1;
903
+ continue;
904
+ }
905
+ if (char === " " || char === " ") {
906
+ index += 1;
907
+ continue;
908
+ }
909
+ const two = command.slice(index, index + 2);
910
+ if (TWO_CHAR_OPS.has(two)) {
911
+ tokens.push({
912
+ kind: "op",
913
+ value: two,
914
+ quoted: false
915
+ });
916
+ index += 2;
917
+ continue;
918
+ }
919
+ if (char === ";" || char === "|" || char === "&" || char === "(" || char === ")" || char === "<" || char === ">") {
920
+ tokens.push({
921
+ kind: "op",
922
+ value: char,
923
+ quoted: false
924
+ });
925
+ index += 1;
926
+ continue;
927
+ }
928
+ let word = "";
929
+ let quoted = false;
930
+ let quote = null;
931
+ while (index < length) {
932
+ const current = command[index];
933
+ if (quote === "'") {
934
+ if (current === "'") {
935
+ quote = null;
936
+ index += 1;
937
+ continue;
938
+ }
939
+ quoted = true;
940
+ word += current;
941
+ index += 1;
942
+ continue;
943
+ }
944
+ if (quote === "\"") {
945
+ if (current === "\"") {
946
+ quote = null;
947
+ index += 1;
948
+ continue;
949
+ }
950
+ quoted = true;
951
+ if (current === "\\" && index + 1 < length) {
952
+ word += command[index + 1];
953
+ index += 2;
954
+ continue;
955
+ }
956
+ word += current;
957
+ index += 1;
958
+ continue;
959
+ }
960
+ if (current === "'") {
961
+ quote = "'";
962
+ index += 1;
963
+ continue;
964
+ }
965
+ if (current === "\"") {
966
+ quote = "\"";
967
+ index += 1;
968
+ continue;
969
+ }
970
+ if (current === "\\" && index + 1 < length) {
971
+ word += command[index + 1];
972
+ index += 2;
973
+ continue;
974
+ }
975
+ if (current === " " || current === " " || current === "\n" || current === "\r") break;
976
+ if (current === ";" || current === "|" || current === "&" || current === "(" || current === ")" || current === "<" || current === ">") break;
977
+ if (TWO_CHAR_OPS.has(command.slice(index, index + 2))) break;
978
+ word += current;
979
+ index += 1;
980
+ }
981
+ if (quote !== null) {
982
+ malformed = true;
983
+ break;
984
+ }
985
+ if (word) tokens.push({
986
+ kind: "word",
987
+ value: word,
988
+ quoted
989
+ });
990
+ }
991
+ return {
992
+ tokens,
993
+ malformed
994
+ };
995
+ }
996
+ /** Characters that indicate non-literal paths (variables, expansion, globs). */
997
+ const DYNAMIC_PATH = /[$`~*?[\]{}]/u;
998
+ function isLiteralPath(value) {
999
+ return value.length > 0 && !DYNAMIC_PATH.test(value);
1000
+ }
1001
+ /** v0.1 whitelist: single foreground simple commands only (manifest-driven). */
1002
+ const SHELL_FILE_TOOLS = new Set(COMMAND_SURFACE_MANIFEST.fileTools);
1003
+ /** Read-only inspection tools: every pathish argument counts as a read effect. */
1004
+ const SHELL_READ_TOOLS = new Set(COMMAND_SURFACE_MANIFEST.readTools);
1005
+ const SHELL_RUN_EXECUTABLES = new Set(COMMAND_SURFACE_MANIFEST.runExecutables);
1006
+ /**
1007
+ * Whether an executable carries run semantics (as opposed to the tiny
1008
+ * file/read tool subset). Used for scope-subject attribution of a pathless
1009
+ * run operation; `echo` or `cat` never becomes a subject-carrying run.
1010
+ */
1011
+ function isRunExecutable(executable) {
1012
+ return SHELL_RUN_EXECUTABLES.has(executable.toLowerCase());
1013
+ }
1014
+ /** Looks like a filesystem path: contains a separator, or a file extension. */
1015
+ function isPathish(value) {
1016
+ return /[\\/]/.test(value) || /^\.\.?(\/|$)/.test(value) || /\.(?:[A-Za-z0-9][A-Za-z0-9_-]{0,15})$/.test(value);
1017
+ }
1018
+ function unsupported(reason) {
1019
+ return {
1020
+ status: "unsupported",
1021
+ reason,
1022
+ executables: [],
1023
+ operations: [],
1024
+ malformed: false
1025
+ };
1026
+ }
1027
+ /**
1028
+ * Parse one POSIX shell command against the v0.1 supported surface: a single
1029
+ * foreground simple command made of an env-assignment prefix, one whitelisted
1030
+ * executable and literal arguments, with at most one `>`/`>>` redirect to a
1031
+ * literal path. Compound syntax (`;`, `&&`, `||`, pipes, background, subshells,
1032
+ * command substitution, heredocs, unclosed quotes, dynamic eval/source,
1033
+ * variable/glob paths) makes the WHOLE command unsupported with no partial
1034
+ * results.
1035
+ */
1036
+ function parseShellCommand(command) {
1037
+ const { tokens, malformed } = tokenizeShell(command);
1038
+ if (malformed) return {
1039
+ status: "malformed",
1040
+ reason: "unterminated quote",
1041
+ executables: [],
1042
+ operations: [],
1043
+ malformed: true
1044
+ };
1045
+ if (tokens.length === 0) return {
1046
+ status: "supported",
1047
+ executables: [],
1048
+ operations: [],
1049
+ malformed: false
1050
+ };
1051
+ const writePaths = [];
1052
+ for (let index = 0; index < tokens.length; index += 1) {
1053
+ const token = tokens[index];
1054
+ if (token.kind === "word" && /^\d+$/.test(token.value) && tokens[index + 1]?.kind === "op" && tokens[index + 1]?.value === ">&" && tokens[index + 2]?.kind === "word" && /^\d+$/.test(tokens[index + 2].value)) {
1055
+ index += 2;
1056
+ continue;
1057
+ }
1058
+ if (token.kind === "word" && /^\d+$/.test(token.value) && tokens[index + 1]?.kind === "op" && (tokens[index + 1]?.value === ">" || tokens[index + 1]?.value === ">>")) return unsupported("file-descriptor-prefixed file redirect is not in the v0.1 subset");
1059
+ if (token.kind === "op") {
1060
+ if (token.value === ">") {
1061
+ const next = tokens[index + 1];
1062
+ if (!next || next.kind !== "word") return unsupported("redirect target is not a literal word");
1063
+ if (!isLiteralPath(next.value)) return unsupported("non-literal redirect path");
1064
+ writePaths.push(next.value);
1065
+ index += 1;
1066
+ continue;
1067
+ }
1068
+ if (token.value === ">>" || token.value === "<" || token.value === "<<" || token.value === "<&" || token.value === ">&") return unsupported(`redirect '${token.value}' is not in the v0.1 subset`);
1069
+ if (STATEMENT_OPS.has(token.value)) return unsupported(`statement operator '${token.value}' is not in the v0.1 subset`);
1070
+ return unsupported(`operator '${token.value}' is not in the v0.1 subset`);
1071
+ }
1072
+ }
1073
+ if (writePaths.length > 1) return unsupported("multiple write redirects are not in the v0.1 subset");
1074
+ const wordTokens = tokens.filter((token) => token.kind === "word");
1075
+ let executableIndex = 0;
1076
+ while (executableIndex < wordTokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(wordTokens[executableIndex].value)) executableIndex += 1;
1077
+ const executableToken = wordTokens[executableIndex];
1078
+ const executable = executableToken?.value ?? "";
1079
+ if (!executable) return unsupported("no executable");
1080
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(executable)) return unsupported("executable is not a plain literal name");
1081
+ if (executableToken.quoted) return unsupported("quoted executable is not in the v0.1 subset");
1082
+ if (wordTokens.slice(0, executableIndex).map((token) => token.value).some((word) => !isLiteralPath(word))) return unsupported("dynamic environment assignment");
1083
+ const exe = executable.toLowerCase();
1084
+ if (!SHELL_FILE_TOOLS.has(exe) && !SHELL_READ_TOOLS.has(exe) && !SHELL_RUN_EXECUTABLES.has(exe)) return unsupported(`executable '${executable}' is not in the v0.1 whitelist`);
1085
+ const args = wordTokens.slice(executableIndex + 1).map((token) => token.value);
1086
+ if (args.some((arg) => !isLiteralPath(arg))) return unsupported("non-literal argument");
1087
+ const pathishArgs = args.filter((arg) => isPathish(arg));
1088
+ const operations = [];
1089
+ for (const path$1 of writePaths) operations.push({
1090
+ op: "create",
1091
+ path: path$1
1092
+ });
1093
+ if (exe === "touch") for (const path$1 of pathishArgs) operations.push({
1094
+ op: "create",
1095
+ path: path$1
1096
+ });
1097
+ else if (SHELL_READ_TOOLS.has(exe)) {
1098
+ if (exe === "sed" && args.some((arg) => /^-i($|[A-Za-z0-9])|^--in-place/.test(arg))) return unsupported("in-place sed editing is not in the v0.1 subset");
1099
+ for (const path$1 of pathishArgs) operations.push({
1100
+ op: "read",
1101
+ path: path$1
1102
+ });
1103
+ }
1104
+ operations.push({
1105
+ op: "run",
1106
+ ...pathishArgs[0] !== void 0 ? { path: pathishArgs[0] } : {}
1107
+ });
1108
+ return {
1109
+ status: "supported",
1110
+ executables: [executable],
1111
+ operations,
1112
+ malformed: false
1113
+ };
1114
+ }
1115
+ const PWSH_CMDLETS = {
1116
+ "set-content": {
1117
+ op: "create",
1118
+ pathParams: ["-path", "-literalpath"],
1119
+ valueParams: ["-value", "-encoding"],
1120
+ switchParams: ["-nonewline"]
1121
+ },
1122
+ "add-content": {
1123
+ op: "create",
1124
+ pathParams: ["-path", "-literalpath"],
1125
+ valueParams: ["-value", "-encoding"],
1126
+ switchParams: ["-nonewline"]
1127
+ },
1128
+ "new-item": {
1129
+ op: "create",
1130
+ pathParams: ["-path"],
1131
+ valueParams: ["-value", "-itemtype"],
1132
+ switchParams: []
1133
+ },
1134
+ "out-file": {
1135
+ op: "create",
1136
+ pathParams: ["-filepath", "-literalpath"],
1137
+ valueParams: ["-encoding"],
1138
+ switchParams: ["-nonewline"]
1139
+ },
1140
+ "get-content": {
1141
+ op: "read",
1142
+ pathParams: ["-path", "-literalpath"],
1143
+ valueParams: ["-encoding"],
1144
+ switchParams: ["-raw"]
1145
+ }
1146
+ };
1147
+ /** PowerShell tokenizer: quoted strings (backtick-escaped) are one word. */
1148
+ function tokenizePwsh(command) {
1149
+ const words = [];
1150
+ let index = 0;
1151
+ let malformed = false;
1152
+ const length = command.length;
1153
+ while (index < length) {
1154
+ const char = command[index];
1155
+ if (char === " " || char === " " || char === "\n" || char === "\r") {
1156
+ index += 1;
1157
+ continue;
1158
+ }
1159
+ if (char === "'" || char === "\"") {
1160
+ const quote = char;
1161
+ let word$1 = "";
1162
+ let closed = false;
1163
+ index += 1;
1164
+ while (index < length) {
1165
+ const current = command[index];
1166
+ if (current === "`") {
1167
+ malformed = true;
1168
+ break;
1169
+ }
1170
+ if (current === quote) {
1171
+ closed = true;
1172
+ index += 1;
1173
+ break;
1174
+ }
1175
+ word$1 += current;
1176
+ index += 1;
1177
+ }
1178
+ if (!closed) malformed = true;
1179
+ words.push({
1180
+ value: word$1,
1181
+ quoted: true
1182
+ });
1183
+ continue;
1184
+ }
1185
+ let word = "";
1186
+ while (index < length) {
1187
+ const current = command[index];
1188
+ if (current === " " || current === " " || current === "\n" || current === "\r") break;
1189
+ if (current === "`") malformed = true;
1190
+ word += current;
1191
+ index += 1;
1192
+ if (malformed) break;
1193
+ }
1194
+ words.push({
1195
+ value: word,
1196
+ quoted: false
1197
+ });
1198
+ }
1199
+ return {
1200
+ words,
1201
+ malformed
1202
+ };
1203
+ }
1204
+ /** Unsupported PowerShell structure outside quoted strings. */
1205
+ function readPwshUnsupported(command) {
1206
+ let inSingle = false;
1207
+ let inDouble = false;
1208
+ let index = 0;
1209
+ while (index < command.length) {
1210
+ const char = command[index];
1211
+ if (inSingle) {
1212
+ if (char === "'") inSingle = false;
1213
+ index += 1;
1214
+ continue;
1215
+ }
1216
+ if (inDouble) {
1217
+ if (char === "`") return {
1218
+ unsupported: true,
1219
+ reason: "backtick escape"
1220
+ };
1221
+ if (char === "$") return {
1222
+ unsupported: true,
1223
+ reason: "variable or subexpression"
1224
+ };
1225
+ if (char === "\"") inDouble = false;
1226
+ index += 1;
1227
+ continue;
1228
+ }
1229
+ if (char === "'") {
1230
+ inSingle = true;
1231
+ index += 1;
1232
+ continue;
1233
+ }
1234
+ if (char === "\"") {
1235
+ inDouble = true;
1236
+ index += 1;
1237
+ continue;
1238
+ }
1239
+ if (char === "`") return {
1240
+ unsupported: true,
1241
+ reason: "backtick escape"
1242
+ };
1243
+ if (char === "$") return {
1244
+ unsupported: true,
1245
+ reason: "variable or subexpression"
1246
+ };
1247
+ if (char === "\n" || char === "\r") return {
1248
+ unsupported: true,
1249
+ reason: "unquoted newline"
1250
+ };
1251
+ if (char === "&") {
1252
+ const previous = command[index - 1] ?? "";
1253
+ const next = command[index + 1] ?? "";
1254
+ if (previous === ">" && /[0-9]/.test(next)) {
1255
+ index += 1;
1256
+ continue;
1257
+ }
1258
+ return {
1259
+ unsupported: true,
1260
+ reason: "structure character &"
1261
+ };
1262
+ }
1263
+ if (char === ";" || char === "|" || char === "{" || char === "}" || char === "(" || char === ")" || char === "[" || char === "]" || char === ",") return {
1264
+ unsupported: true,
1265
+ reason: `structure character '${char}'`
1266
+ };
1267
+ index += 1;
1268
+ }
1269
+ if (/^\s*\./.test(command)) return {
1270
+ unsupported: true,
1271
+ reason: "dot sourcing"
1272
+ };
1273
+ return { unsupported: false };
1274
+ }
1275
+ /** PowerShell v0.2 subset: external executables with literal arguments. */
1276
+ const PWSH_EXTERNAL_EXECUTABLES = new Set(COMMAND_SURFACE_MANIFEST.pwshExternalExecutables);
1277
+ /**
1278
+ * Parse one PowerShell command against the v0.2 subset: a single, directly
1279
+ * invoked whitelisted cmdlet (Set-Content / Add-Content / New-Item /
1280
+ * Out-File / Get-Content) whose path comes from an explicit named path
1281
+ * parameter, or a whitelisted external executable (git, pnpm, node, …) with
1282
+ * all-literal arguments. Unquoted `N>&M` diagnostic stream duplication is
1283
+ * stripped. Multi-statements (`;`), pipelines (`|`), the call operator (`&`),
1284
+ * script blocks, dot sourcing, .NET/dynamic invocation,
1285
+ * variable/expression/subexpression paths, positional paths, and unknown
1286
+ * parameters make the WHOLE command unsupported.
1287
+ */
1288
+ function parsePwshCommand(command) {
1289
+ const dynamic = readPwshUnsupported(command);
1290
+ if (dynamic.unsupported) return unsupported(`dynamic or compound PowerShell syntax (${dynamic.reason ?? "unknown"})`);
1291
+ const { words: rawWords, malformed } = tokenizePwsh(command);
1292
+ if (malformed) return {
1293
+ status: "malformed",
1294
+ reason: "unterminated quote or escape",
1295
+ executables: [],
1296
+ operations: [],
1297
+ malformed: true
1298
+ };
1299
+ const words = rawWords.filter((word) => !(word.quoted === false && /^[0-9]*>&[0-9]+$/.test(word.value)));
1300
+ if (words.length === 0) return {
1301
+ status: "supported",
1302
+ executables: [],
1303
+ operations: [],
1304
+ malformed: false
1305
+ };
1306
+ const cmdletRaw = words[0].value;
1307
+ const spec = PWSH_CMDLETS[cmdletRaw.toLowerCase()];
1308
+ const external = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(cmdletRaw) && PWSH_EXTERNAL_EXECUTABLES.has(cmdletRaw.toLowerCase());
1309
+ if (!spec && !external) return unsupported(`command '${cmdletRaw}' is not in the v0.1 whitelist`);
1310
+ if (words[0].quoted) return unsupported("quoted command name is not in the v0.1 subset");
1311
+ if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(cmdletRaw)) return unsupported("dynamic or .NET invocation is not in the v0.1 subset");
1312
+ if (external) {
1313
+ const args = words.slice(1).map((token) => token.value);
1314
+ if (args.some((arg) => !isLiteralPath(arg))) return unsupported("non-literal argument");
1315
+ const pathishArgs = args.filter((arg) => isPathish(arg));
1316
+ return {
1317
+ status: "supported",
1318
+ executables: [cmdletRaw],
1319
+ operations: [{
1320
+ op: "run",
1321
+ ...pathishArgs[0] !== void 0 ? { path: pathishArgs[0] } : {}
1322
+ }],
1323
+ malformed: false
1324
+ };
1325
+ }
1326
+ const paths = [];
1327
+ let expected = null;
1328
+ for (let index = 1; index < words.length; index += 1) {
1329
+ const token = words[index];
1330
+ const low = token.value.toLowerCase();
1331
+ if (token.value.startsWith("-")) {
1332
+ if (spec.pathParams.includes(low)) {
1333
+ expected = "path";
1334
+ continue;
1335
+ }
1336
+ if (spec.valueParams.includes(low)) {
1337
+ expected = "value";
1338
+ continue;
1339
+ }
1340
+ if (spec.switchParams.includes(low)) {
1341
+ expected = null;
1342
+ continue;
1343
+ }
1344
+ return unsupported(`parameter '${token.value}' is not in the v0.1 whitelist`);
1345
+ }
1346
+ if (expected === "path") {
1347
+ if (!isLiteralPath(token.value)) return unsupported("non-literal path");
1348
+ paths.push(token.value);
1349
+ expected = null;
1350
+ continue;
1351
+ }
1352
+ if (expected === "value") {
1353
+ expected = null;
1354
+ continue;
1355
+ }
1356
+ return unsupported("positional argument is not in the v0.1 subset");
1357
+ }
1358
+ if (expected !== null) return unsupported("missing parameter value");
1359
+ const operations = [];
1360
+ for (const path$1 of paths) operations.push({
1361
+ op: spec.op,
1362
+ path: path$1
1363
+ });
1364
+ return {
1365
+ status: "supported",
1366
+ executables: [cmdletRaw],
1367
+ operations,
1368
+ malformed: false
1369
+ };
1370
+ }
1371
+
1372
+ //#endregion
1373
+ //#region src/domain/evidence.ts
1374
+ function boundedSummary(value) {
1375
+ return value.length > 240 ? `${value.slice(0, 237)}...` : value;
1376
+ }
1377
+ function parseArguments$1(raw) {
1378
+ if (!raw) return {};
1379
+ try {
1380
+ const parsed = JSON.parse(raw);
1381
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1382
+ } catch {
1383
+ return {};
1384
+ }
1385
+ }
1386
+ function asRecord$1(value) {
1387
+ return typeof value === "object" && value !== null ? value : void 0;
1388
+ }
1389
+ function extractTextContent(content) {
1390
+ const parts = [];
1391
+ for (const block of content) {
1392
+ const record = asRecord$1(block);
1393
+ if (!record) continue;
1394
+ if (record.type === "text" && typeof record.text === "string") parts.push(record.text);
1395
+ if (record.type === "tool-result" && Array.isArray(record.content)) parts.push(extractTextContent(record.content));
1396
+ }
1397
+ return parts.join("\n");
1398
+ }
1399
+ function metaPaths(meta) {
1400
+ const record = asRecord$1(meta);
1401
+ if (!record) return [];
1402
+ if (typeof record.path === "string") return [record.path];
1403
+ if (Array.isArray(record.diffs)) return record.diffs.map((diff) => asRecord$1(diff)?.path).filter((path$1) => typeof path$1 === "string");
1404
+ return [];
1405
+ }
1406
+ function argsPaths(args) {
1407
+ const filePath = args.file_path;
1408
+ if (typeof filePath === "string") return [filePath];
1409
+ return [];
1410
+ }
1411
+ /** Resolve a relative command path reference against the command workdir. */
1412
+ function resolveCommandPath(reference, cwd) {
1413
+ if (!cwd) return reference;
1414
+ if (/^[A-Za-z]:[\\/]/.test(reference) || reference.startsWith("//") || reference.startsWith("\\\\") || reference.startsWith("/") || reference.startsWith("\\")) return reference;
1415
+ return `${cwd.replace(/[\\/]+$/, "")}/${reference}`;
1416
+ }
1417
+ /**
1418
+ * Analyze a shell/pwsh command against the v0.1 supported surface. Only a
1419
+ * fully supported command produces executables/operations; unsupported or
1420
+ * malformed syntax yields EMPTY executables and operations (fail-closed), so a
1421
+ * partially understood command can never certify an operation.
1422
+ */
1423
+ function analyzeCommand(command, workdir, toolName) {
1424
+ const cwd = typeof workdir === "string" ? workdir : void 0;
1425
+ const parsed = toolName === "pwsh" ? parsePwshCommand(command) : parseShellCommand(command);
1426
+ if (parsed.status !== "supported") return {
1427
+ status: parsed.status,
1428
+ reason: parsed.reason,
1429
+ executables: [],
1430
+ operations: [],
1431
+ subjects: cwd ? [cwd] : []
1432
+ };
1433
+ const operations = parsed.operations.map((entry) => {
1434
+ let path$1 = entry.path !== void 0 ? resolveCommandPath(entry.path, cwd) : void 0;
1435
+ if (path$1 === void 0 && entry.op === "run" && cwd !== void 0 && parsed.executables.some((executable) => isRunExecutable(executable))) path$1 = cwd;
1436
+ return {
1437
+ op: entry.op,
1438
+ ...path$1 !== void 0 ? { path: path$1 } : {}
1439
+ };
1440
+ });
1441
+ const subjects = unique([...cwd ? [cwd] : [], ...operations.map((entry) => entry.path).filter((path$1) => path$1 !== void 0)]);
1442
+ return {
1443
+ status: parsed.status,
1444
+ reason: parsed.reason,
1445
+ executables: parsed.executables,
1446
+ operations,
1447
+ subjects
1448
+ };
1449
+ }
1450
+ const PERSISTENT_RESET_LINE = /^The persistent (?:bash|pwsh) shell was reset;/;
1451
+ const PERSISTENT_TIMEOUT_INTRO = /^Your command timed out after \d+ seconds or experienced an OOM error\. Below is partial output:$/;
1452
+ /**
1453
+ * Structured terminal facts from the tool/result meta (defensive): the pinned
1454
+ * shell renderers currently emit text markers only, but the underlying run
1455
+ * result carries exitCode/signal, so a future harness that surfaces them in
1456
+ * `meta` is trusted directly. Absent structured facts, text scanning remains
1457
+ * the fallback.
1458
+ */
1459
+ function structuredTerminalFacts(meta) {
1460
+ const record = asRecord$1(meta);
1461
+ if (!record) return void 0;
1462
+ const rawExit = record.exitCode ?? record.exit_code;
1463
+ const rawSignal = record.signal;
1464
+ if (rawSignal !== void 0 && rawSignal !== null) return {
1465
+ exitCode: typeof rawExit === "number" ? rawExit : void 0,
1466
+ negative: true
1467
+ };
1468
+ if (typeof rawExit === "number") return {
1469
+ exitCode: rawExit,
1470
+ negative: false
1471
+ };
1472
+ }
1473
+ function extractTerminalFacts(textContent) {
1474
+ const lines = textContent.split(/\r?\n/);
1475
+ let index = lines.length - 1;
1476
+ while (index >= 0 && lines[index].trim() === "") index -= 1;
1477
+ const resetStripped = index >= 0 && PERSISTENT_RESET_LINE.test(lines[index].trim());
1478
+ if (resetStripped) {
1479
+ index -= 1;
1480
+ while (index >= 0 && lines[index].trim() === "") index -= 1;
1481
+ }
1482
+ const timeoutIntroAtHead = resetStripped && lines.length > 0 && PERSISTENT_TIMEOUT_INTRO.test(lines[0].trim());
1483
+ let exitCode;
1484
+ let negative = timeoutIntroAtHead;
1485
+ while (index >= 0) {
1486
+ const line = lines[index].trim();
1487
+ const exitMatch = line.match(/^\[(?:exit code|shell exited: code)\s*:?\s*(\d+)\]$/);
1488
+ const negativeLine = /^\[(?:timed out|sandbox[^\]]*|killed by signal[^\]]*|shell killed by signal[^\]]*|shell exited|interrupted[^\]]*)[^\]]*\]$/i.test(line);
1489
+ if (exitMatch) {
1490
+ if (exitCode === void 0) exitCode = Number(exitMatch[1]);
1491
+ } else if (negativeLine) negative = true;
1492
+ else break;
1493
+ index -= 1;
1494
+ }
1495
+ return {
1496
+ exitCode,
1497
+ negative
1498
+ };
1499
+ }
1500
+ function metaUrls(meta) {
1501
+ const record = asRecord$1(meta);
1502
+ if (!record) return [];
1503
+ if (typeof record.url === "string") return [sanitizeUrl(record.url)];
1504
+ if (Array.isArray(record.sources)) return record.sources.map((source) => asRecord$1(source)?.url).filter((url) => typeof url === "string").map((url) => sanitizeUrl(url));
1505
+ return [];
1506
+ }
1507
+ const DETERMINISTIC_CHECK_PATTERNS = [
1508
+ /\b(?:pnpm|npm|yarn|bun)\s+(?:test|tst|lint|check|typecheck|build)\b/,
1509
+ /\b(?:cargo|go|make|cmake|pytest|vitest|jest|eslint|tsc|mypy|ruff|prettier)\b/,
1510
+ /\b(?:mvn|gradle)\s+(?:test|check)\b/,
1511
+ /\bpython(?:3)?\s+-m\s+(?:unittest|doctest|pytest)\b/
1512
+ ];
1513
+ /** Prefixes that only quote or print a command without running a check. */
1514
+ const NON_RUNNING_PREFIXES = [/^\s*(?:echo|printf|echo\s+-e|cat|tee|true|false|:|#)\b/, /\b(?:echo|printf)\s+[^|;&]*["'][^"']*(?:test|lint|build|check)[^"']*["'][^]|;&]*$/i];
1515
+ /** Discovery/version/inspection commands, not verification runs. */
1516
+ const INSPECTION_COMMANDS = /\b(?:which|where|whereis|type|command\s+-v|grep|rg|cat|less|head|tail|find|ls|dir)\b|\s(?:--version|-V|-v|--help|-h)\s*$|\s(?:--version|--help)\b/i;
1517
+ /** Shell constructs that mask the real exit status or detach the check. */
1518
+ const MASKING_CONSTRUCTS = [
1519
+ /\|\|/,
1520
+ /;/,
1521
+ /\|/,
1522
+ /(?:^|\s)&(?!&)\s*$/,
1523
+ /(?:^|\s)&(?!&)\s*(?:disown)?/,
1524
+ /\((?:.*\s&(?!&)\s*)\)\s*$/,
1525
+ /\b(?:nohup|setsid)\b/,
1526
+ /\|\s*(?:true|:)\s*$/
1527
+ ];
1528
+ function isDeterministicCheck(command) {
1529
+ const normalized = command.trim().replace(/\s+/g, " ");
1530
+ if (!normalized || normalized.startsWith("#")) return false;
1531
+ if (/(?:^|[\s&|;(])\s*!(?=\s*[A-Za-z0-9/_.-])/.test(normalized)) return false;
1532
+ if (NON_RUNNING_PREFIXES.some((pattern) => pattern.test(normalized))) return false;
1533
+ if (INSPECTION_COMMANDS.test(normalized)) return false;
1534
+ if (MASKING_CONSTRUCTS.some((pattern) => pattern.test(normalized))) return false;
1535
+ const withoutCd = normalized.replace(/^cd\s+[^;&|]+\s*(?:&&|;)\s*/, "");
1536
+ return DETERMINISTIC_CHECK_PATTERNS.some((pattern) => pattern.test(withoutCd));
1537
+ }
1538
+ function unique(values) {
1539
+ return [...new Set(values)];
1540
+ }
1541
+ /** Resolve relative artifact subjects against the session scope cwd. */
1542
+ function resolveSubjectPaths(values, cwd) {
1543
+ return cwd ? values.map((value) => resolveCommandPath(value, cwd)) : values;
1544
+ }
1545
+ function extractToolSubject(call, result, defaultCwd) {
1546
+ const args = parseArguments$1(call.arguments);
1547
+ switch (call.name) {
1548
+ case "read":
1549
+ case "read_file": {
1550
+ const subjects = unique(resolveSubjectPaths([...metaPaths(result.meta), ...argsPaths(args)], defaultCwd));
1551
+ return {
1552
+ capabilities: ["filesystem-read"],
1553
+ subjects,
1554
+ surfaces: ["artifact"],
1555
+ operations: subjects.map((path$1) => ({
1556
+ op: "read",
1557
+ path: path$1
1558
+ }))
1559
+ };
1560
+ }
1561
+ case "write":
1562
+ case "write_file": {
1563
+ const subjects = unique(resolveSubjectPaths([...metaPaths(result.meta), ...argsPaths(args)], defaultCwd));
1564
+ return {
1565
+ capabilities: ["filesystem-write"],
1566
+ subjects,
1567
+ surfaces: ["artifact"],
1568
+ operations: subjects.map((path$1) => ({
1569
+ op: "create",
1570
+ path: path$1
1571
+ }))
1572
+ };
1573
+ }
1574
+ case "edit":
1575
+ case "edit_file": {
1576
+ const subjects = unique(resolveSubjectPaths([...metaPaths(result.meta), ...argsPaths(args)], defaultCwd));
1577
+ return {
1578
+ capabilities: ["filesystem-edit"],
1579
+ subjects,
1580
+ surfaces: ["artifact"],
1581
+ operations: subjects.map((path$1) => ({
1582
+ op: "modify",
1583
+ path: path$1
1584
+ }))
1585
+ };
1586
+ }
1587
+ case "bash":
1588
+ case "shell":
1589
+ case "pwsh": {
1590
+ const command = typeof args.command === "string" ? args.command : "";
1591
+ const terminal = structuredTerminalFacts(result.meta) ?? extractTerminalFacts(result.textContent);
1592
+ const backgrounded = args.run_in_background === true;
1593
+ const commandDetails = analyzeCommand(command, typeof args.workdir === "string" ? args.workdir : defaultCwd, call.name);
1594
+ const deterministic = commandDetails.status === "supported" && !backgrounded && isDeterministicCheck(command);
1595
+ const outcome = backgrounded ? "unknown" : result.error || terminal.negative ? "failure" : terminal.exitCode === void 0 ? call.name === "bash" || call.name === "pwsh" ? "success" : "unknown" : terminal.exitCode === 0 ? "success" : "failure";
1596
+ return {
1597
+ capabilities: ["shell", ...deterministic ? ["deterministic-check"] : []],
1598
+ subjects: unique(commandDetails.subjects),
1599
+ surfaces: ["scope"],
1600
+ outcome,
1601
+ executables: commandDetails.executables,
1602
+ operations: commandDetails.operations
1603
+ };
1604
+ }
1605
+ case "web_search":
1606
+ case "web_fetch":
1607
+ case "web_fetch_url": return {
1608
+ capabilities: ["web-fetch"],
1609
+ subjects: unique([...metaUrls(result.meta), ...typeof args.url === "string" ? [sanitizeUrl(args.url)] : []]),
1610
+ surfaces: ["ui"]
1611
+ };
1612
+ default: return {
1613
+ capabilities: ["generic"],
1614
+ subjects: [],
1615
+ surfaces: []
1616
+ };
1617
+ }
1618
+ }
1619
+ function evidenceFromPersistedToolResult(call, result, epoch, evidenceId, defaultCwd) {
1620
+ const subject = extractToolSubject(call, result, defaultCwd);
1621
+ const outcome = result.error ? "failure" : subject.outcome ?? "success";
1622
+ return {
1623
+ id: evidenceId,
1624
+ epoch,
1625
+ callId: call.callId,
1626
+ rootCallId: call.rootCallId ?? call.callId,
1627
+ toolName: call.name,
1628
+ toolResultSeq: result.seq,
1629
+ outcome,
1630
+ capabilities: subject.capabilities,
1631
+ subjects: subject.subjects,
1632
+ surfaces: subject.surfaces,
1633
+ boundedSummarySha256: sha256(boundedSummary(result.textContent)),
1634
+ ...subject.executables?.length ? { executables: subject.executables } : {},
1635
+ ...subject.operations?.length ? { operations: subject.operations } : {}
1636
+ };
1637
+ }
1638
+ function withDurability(evidence, confirmed) {
1639
+ if (confirmed) return evidence;
1640
+ return {
1641
+ ...evidence,
1642
+ outcome: "unknown"
1643
+ };
1644
+ }
1645
+
1646
+ //#endregion
1647
+ //#region src/domain/supersession.ts
1648
+ function supersedeItem(items, oldId, replacement) {
1649
+ const old = items.get(oldId);
1650
+ if (!old || old.status === "superseded") return false;
1651
+ old.status = "superseded";
1652
+ old.supersededBy = replacement.id;
1653
+ items.set(replacement.id, replacement);
1654
+ return true;
1655
+ }
1656
+
1657
+ //#endregion
1658
+ //#region src/domain/derive.ts
1659
+ function parseArguments(raw) {
1660
+ if (!raw) return {};
1661
+ try {
1662
+ const parsed = JSON.parse(raw);
1663
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1664
+ } catch {
1665
+ return {};
1666
+ }
1667
+ }
1668
+ function asRecord(value) {
1669
+ return typeof value === "object" && value !== null ? value : void 0;
1670
+ }
1671
+ function nextId(items, kind) {
1672
+ const prefix = kind === "requirement" ? "R" : kind === "acceptance" ? "A" : "P";
1673
+ let max = 0;
1674
+ for (const item of items.values()) {
1675
+ if (item.kind !== kind) continue;
1676
+ const num = Number(item.id.slice(prefix.length));
1677
+ if (Number.isInteger(num) && num > max) max = num;
1678
+ }
1679
+ return `${prefix}${String(max + 1).padStart(3, "0")}`;
1680
+ }
1681
+ /** Framing-only instruction clauses carry no task substance and never close. */
1682
+ const FRAMING_ZH = /^(?:请)?(?:完成|执行|按|按照|遵循|满足)?(?:以下|如下|下面|下列)?(?:完整|全部)?(?:任务|要求|事项|需求|指令|说明)$/;
1683
+ const FRAMING_EN = /^(?:please\s+)?(?:complete|do|perform|follow|satisfy)?\s*(?:the\s+)?(?:following|below)?\s*(?:full\s+|whole\s+)?(?:task|tasks|requirement|requirements|instruction|instructions)$/i;
1684
+ function isInstructionFraming(body) {
1685
+ return FRAMING_ZH.test(body) || FRAMING_EN.test(body);
1686
+ }
1687
+ /** Resolve a contract artifact path against the session working directory. */
1688
+ function resolveArtifact(path$1, scope) {
1689
+ if (!scope.cwd) return path$1;
1690
+ if (/^[A-Za-z]:[\\/]/.test(path$1) || path$1.startsWith("/") || path$1.startsWith("\\")) return path$1;
1691
+ return `${scope.cwd.replace(/[\\/]+$/, "")}/${path$1}`;
1692
+ }
1693
+ /**
1694
+ * Insert every independently tracked clause from one user message. Compound
1695
+ * instructions are segmented and each distinct artifact path becomes its own
1696
+ * item, so evidence for one file cannot close a message that also covers other
1697
+ * files or embeds prohibitions.
1698
+ */
1699
+ function insertItems(projection, text, sourceMessageId, scope) {
1700
+ for (const segment of segmentClauses(text)) {
1701
+ if (classifyUserInteraction(segment.body) === "conversational") continue;
1702
+ if (segment.kind === "requirement" && segment.paths.length === 0 && isInstructionFraming(segment.body)) continue;
1703
+ if (segment.kind === "prohibition" || segment.paths.length === 0) {
1704
+ insert(projection, segment.kind, segment.body, sourceMessageId, scope.cwd || "scope", "scope");
1705
+ continue;
1706
+ }
1707
+ for (const path$1 of segment.paths) insert(projection, segment.kind, segment.body, sourceMessageId, resolveArtifact(path$1, scope), "artifact");
1708
+ }
1709
+ }
1710
+ function insert(projection, kind, body, sourceMessageId, subject, surface) {
1711
+ const revision = projection.contractRevision + 1;
1712
+ const id = nextId(projection.items, kind);
1713
+ const item = captureItem(kind, body, sourceMessageId, id, revision, subject, surface, extractMethod(body), extractOperation(body));
1714
+ const duplicate = [...projection.items.values()].find((existing) => existing.kind === kind && existing.status === "pending" && existing.textSha256 === item.textSha256 && existing.verification.subject === subject);
1715
+ if (duplicate) supersedeItem(projection.items, duplicate.id, item);
1716
+ else projection.items.set(id, item);
1717
+ projection.contractRevision = item.revision;
1718
+ }
1719
+ /**
1720
+ * Pure, deterministic re-derivation of the guard projection from the DSH
1721
+ * native event log. Context Guard never writes custom session events, so every
1722
+ * piece of state is derived from `command/run`, `user/message`, `tool/call`,
1723
+ * `tool/result`, `tool/code-dispatch-start`, `tool/code-dispatch`, and
1724
+ * `compaction/summary`.
1725
+ */
1726
+ function deriveProjection(sourceEvents, config, scope, durableConfirmed) {
1727
+ const projection = createProjection();
1728
+ let enabled = config.activation === "always";
1729
+ let epoch = 0;
1730
+ let evidenceCounter = 0;
1731
+ let compacted = false;
1732
+ let enablementTransitioned = false;
1733
+ let lastCompactionSeq = -1;
1734
+ const pendingCalls = /* @__PURE__ */ new Map();
1735
+ for (const event of sourceEvents) {
1736
+ projection.lastObservedSourceSeq = Math.max(projection.lastObservedSourceSeq, event.seq);
1737
+ switch (event.type) {
1738
+ case "command/run": {
1739
+ const data = asRecord(event.data);
1740
+ if (data?.name !== "context-guard") break;
1741
+ const subcommand = typeof data.args === "string" ? data.args.trim().split(/\s+/, 1)[0] : "";
1742
+ if (subcommand === "on" && !enabled) {
1743
+ enabled = true;
1744
+ epoch += 1;
1745
+ enablementTransitioned = true;
1746
+ projection.epoch = epoch;
1747
+ } else if (subcommand === "off") enabled = false;
1748
+ else if (subcommand === "clear") {
1749
+ const revision = projection.contractRevision + 1;
1750
+ for (const item of projection.items.values()) {
1751
+ if (item.kind === "prohibition" || item.status !== "pending") continue;
1752
+ item.status = "superseded";
1753
+ item.supersededBy = `CLEAR:${revision}`;
1754
+ }
1755
+ projection.contractRevision = revision;
1756
+ }
1757
+ break;
1758
+ }
1759
+ case "compaction/summary":
1760
+ compacted = true;
1761
+ lastCompactionSeq = event.seq;
1762
+ break;
1763
+ case "user/message": {
1764
+ if (!enabled) break;
1765
+ const data = asRecord(event.data);
1766
+ if (asRecord(data?.source)?.kind !== "user") break;
1767
+ const text = extractTextContent(data?.content ?? []);
1768
+ if (!text.trim()) break;
1769
+ if (isInformationalMessage(text)) break;
1770
+ if (classifyUserInteraction(text) === "conversational") break;
1771
+ insertItems(projection, text, `m${event.seq}`, scope);
1772
+ break;
1773
+ }
1774
+ case "tool/call": {
1775
+ if (!enabled) break;
1776
+ const data = asRecord(event.data);
1777
+ const callId = String(data?.callId ?? "");
1778
+ const call = {
1779
+ name: String(data?.name ?? ""),
1780
+ arguments: String(data?.arguments ?? ""),
1781
+ rootCallId: typeof data?.rootCallId === "string" ? data.rootCallId : void 0
1782
+ };
1783
+ if (call.name === "context_guard_checkpoint") {
1784
+ const args = parseArguments(call.arguments);
1785
+ call.bindings = Array.isArray(args.bindings) ? args.bindings.map((binding) => {
1786
+ const record = asRecord(binding);
1787
+ return {
1788
+ itemId: String(record?.item_id ?? ""),
1789
+ evidenceIds: Array.isArray(record?.evidence_ids) ? record.evidence_ids.map(String) : []
1790
+ };
1791
+ }) : [];
1792
+ }
1793
+ pendingCalls.set(callId, call);
1794
+ break;
1795
+ }
1796
+ case "tool/code-dispatch-start": {
1797
+ if (!enabled) break;
1798
+ const data = asRecord(event.data);
1799
+ const subCallId = String(data?.subCallId ?? "");
1800
+ const rawArguments = data?.arguments;
1801
+ pendingCalls.set(subCallId, {
1802
+ name: String(data?.name ?? ""),
1803
+ arguments: typeof rawArguments === "string" ? rawArguments : JSON.stringify(rawArguments ?? ""),
1804
+ rootCallId: typeof data?.rootCallId === "string" ? data.rootCallId : void 0
1805
+ });
1806
+ break;
1807
+ }
1808
+ case "tool/result":
1809
+ case "tool/code-dispatch": {
1810
+ if (!enabled) break;
1811
+ const data = asRecord(event.data);
1812
+ const isDispatch = event.type === "tool/code-dispatch";
1813
+ const message = asRecord(data?.message);
1814
+ const source = asRecord(message?.source);
1815
+ const callId = String(source?.callId ?? (isDispatch ? data?.subCallId : "") ?? "");
1816
+ const call = pendingCalls.get(callId);
1817
+ if (!call) break;
1818
+ pendingCalls.delete(callId);
1819
+ const textContent = extractTextContent((isDispatch ? data?.content : void 0) ?? message?.content ?? []);
1820
+ if (call.name === "context_guard_checkpoint") {
1821
+ if (parseArguments(textContent).status !== "certified") break;
1822
+ if (certifyCheckpoint(projection, call.bindings ?? [], `C${projection.checkpoints.length + 1}`).status !== "certified") projection.integrity = "corrupt";
1823
+ break;
1824
+ }
1825
+ evidenceCounter += 1;
1826
+ const evidence = withDurability(evidenceFromPersistedToolResult({
1827
+ callId,
1828
+ name: call.name,
1829
+ arguments: call.arguments,
1830
+ rootCallId: call.rootCallId
1831
+ }, {
1832
+ seq: event.seq,
1833
+ error: data?.error ?? (isDispatch && data?.isError ? {
1834
+ name: "code",
1835
+ code: "DISPATCH_ERROR"
1836
+ } : void 0),
1837
+ meta: data?.meta,
1838
+ textContent
1839
+ }, epoch, `E${String(evidenceCounter).padStart(4, "0")}`, scope.cwd || void 0), durableConfirmed);
1840
+ projection.evidence.set(evidence.id, evidence);
1841
+ break;
1842
+ }
1843
+ default: break;
1844
+ }
1845
+ }
1846
+ projection.enabled = enabled;
1847
+ projection.epoch = epoch;
1848
+ return {
1849
+ projection,
1850
+ compacted,
1851
+ enablementTransitioned,
1852
+ lastCompactionSeq
1853
+ };
1854
+ }
1855
+
1856
+ //#endregion
1857
+ //#region src/domain/goal-gate.ts
1858
+ function hasCurrentCertificate(projection) {
1859
+ const checkpoint = projection.checkpoints.at(-1);
1860
+ return projection.integrity === "valid" && checkpoint?.result === "certified" && checkpoint.epoch === projection.epoch && checkpoint.contractRevision === projection.contractRevision;
1861
+ }
1862
+ /**
1863
+ * Denies `update_goal(action=complete)` while the guard is enabled and no
1864
+ * current completion certificate exists. The gate itself has no bypass; a
1865
+ * workflow that genuinely finished but cannot certify (for example a contract
1866
+ * polluted by session-layer talk, or evidence that lives in another session)
1867
+ * has three explicit remediation routes:
1868
+ *
1869
+ * 1. `/context-guard off` disables the guard, so completion is no longer
1870
+ * gated. Use only after the user confirms the work is actually done.
1871
+ * 2. `/context-guard clear` supersedes every pending requirement and
1872
+ * acceptance under a `CLEAR:<revision>` sentinel (prohibitions are
1873
+ * retained) and bumps the contract revision; an empty-binding checkpoint
1874
+ * can then certify while the guard stays enabled.
1875
+ * 3. `update_goal(action=blocked)` records the blocker truthfully, which is
1876
+ * never denied by this gate.
1877
+ */
1878
+ function goalCompletionDenial(projection, toolName, argumentsValue, configuredToolName = "update_goal") {
1879
+ if (toolName !== configuredToolName || typeof argumentsValue !== "object" || argumentsValue === null) return void 0;
1880
+ if (argumentsValue.action !== "complete") return void 0;
1881
+ if (!projection.enabled) return void 0;
1882
+ if (hasCurrentCertificate(projection)) return void 0;
1883
+ return projection.integrity === "valid" ? "Context Guard requires a current completion certificate before Goal completion." : "Context Guard integrity is unknown or corrupt; Goal completion is denied.";
1884
+ }
1885
+
1886
+ //#endregion
1887
+ //#region src/domain/stop-policy.ts
1888
+ const QUOTED = /["'“”‘’`].*?(?:complete|done|finished|完成|做完|搞定).*?["'“”‘’`]/i;
1889
+ const EXAMPLE = /\b(?:for example|e\.g\.|such as|like saying|例如|比如|举例|作为一个例子)\b/i;
1890
+ const QUESTION = /\?[ \t]*$|\b(?:should|could|would|can|will|what|how|whether)\b.*\?/i;
1891
+ const TRAILING_NEGATION = /\b(?:not (?:yet |quite |fully )?(?:complete|done|finished)|isn'?t (?:complete|done|finished)|hasn'?t (?:been )?(?:completed|finished)|尚未完成|还没完成|未完成|没有完成|还未完成)\b/i;
1892
+ const CONDITIONAL = /\b(?:if|unless|once|when|whenever|provided that|只要|如果|假如|一旦|除非)\b/i;
1893
+ const PARTIAL_ONLY = /\b(?:step|phase|stage|milestone)\s+\d+\b|第[一二三四五六七八九十\d]+\s*(?:步|阶段|环节)|(?:第一步|第二步|第三步)/i;
1894
+ const WHOLE_COMPLETION_EN = /\b(?:the )?(?:task|work|job|everything|all tasks?|all work) (?:is|are) (?:now )?(?:complete|done|finished|completed)\b|\b(?:task|work) (?:has been )?(?:completed|finished)\b|\ball (?:tasks|work|requirements) (?:have been )?(?:completed|done|met)\b/i;
1895
+ const WHOLE_COMPLETION_ZH = /(?:任务|工作|所有任务|全部工作|整体)(?:已经|已)?(?:全部)?(?:完成|搞定|做完)|(?:已|已经)(?:全部|所有)?(?:完成|搞定)(?:了)?(?:全部|所有)?(?:任务|工作)?/i;
1896
+ /** Bare completion confirmations, e.g. "Done." or "搞定了。" */
1897
+ const BARE_COMPLETION = /^(?:done|finished|completed|all\s+done)[.!]?$|^(?:已完成|完成了|搞定了|搞定|完成|done)[。..!!]?$/i;
1898
+ /** Continuation intent following a claim makes it partial, not whole-task. */
1899
+ const CONTINUATION = /接下来|下一步|然后|接着|继续|再去|最后再|还差|剩下|剩余|第二步|第三步|,\s*(?:next|then|after that|moving on)\b/i;
1900
+ function looksQuotedOrExemplary(text) {
1901
+ return QUOTED.test(text) || EXAMPLE.test(text);
1902
+ }
1903
+ function isWholeTaskCompletionClaim(text) {
1904
+ const normalized = normalizeClause(text);
1905
+ if (!normalized) return false;
1906
+ if (QUESTION.test(normalized)) return false;
1907
+ if (TRAILING_NEGATION.test(normalized)) return false;
1908
+ if (CONDITIONAL.test(normalized)) return false;
1909
+ if (CONTINUATION.test(normalized)) return false;
1910
+ if (looksQuotedOrExemplary(normalized)) return false;
1911
+ if (PARTIAL_ONLY.test(normalized) && !WHOLE_COMPLETION_EN.test(normalized) && !WHOLE_COMPLETION_ZH.test(normalized)) return false;
1912
+ const firstLine = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "";
1913
+ if (BARE_COMPLETION.test(normalizeTitleLine(firstLine))) return leadingBareCompletionClaim(text);
1914
+ return BARE_COMPLETION.test(normalized) || WHOLE_COMPLETION_EN.test(normalized) || WHOLE_COMPLETION_ZH.test(normalized);
1915
+ }
1916
+ const DECORATION_LEAD = /^\s*(?:[\p{Extended_Pictographic}\u2764\u2705\u2714\u2716\u2728\u274C\u26A0\u2611\u2612\u2713\u2717\u274E\u2B50\u2B55\u2022\u00B7\u25E6\u25AA\u25AB\u25CF\u25CB\u25A0\u25A1\u2013\u2014-]|\uFE0F|\uFE0E|\u200D)+/u;
1917
+ /** Strip a leading run of decorative glyphs from a title line. */
1918
+ function stripDecorationPrefix(text) {
1919
+ let value = text;
1920
+ let previous = "";
1921
+ while (value !== previous) {
1922
+ previous = value;
1923
+ value = value.replace(DECORATION_LEAD, "");
1924
+ }
1925
+ return value.replace(/^\s+/, "");
1926
+ }
1927
+ /**
1928
+ * Normalize a title line for the bare-completion test. Markdown heading markers,
1929
+ * fully-wrapping emphasis (`**…**`, `__…__`, `*…*`, `_…_`), and a leading run of
1930
+ * decorative glyphs are removed ITERATIVELY until stable, because stripping one
1931
+ * layer may expose another (`## ✅ **完成。**`). Blockquotes (`>`), quoted
1932
+ * titles, and examples are left untouched so they still fail closed.
1933
+ */
1934
+ function normalizeTitleLine(line) {
1935
+ let value = line.trim();
1936
+ if (value.startsWith(">")) return value;
1937
+ let previous = "";
1938
+ while (value !== previous) {
1939
+ previous = value;
1940
+ value = value.replace(/^#{1,6}\s+/, "").replace(/^\*\*(.+?)\*\*$/, "$1").replace(/^__(.+?)__$/, "$1").replace(/^\*(.+?)\*$/, "$1").replace(/^_(.+?)_$/, "$1");
1941
+ value = stripDecorationPrefix(value);
1942
+ }
1943
+ return value;
1944
+ }
1945
+ /**
1946
+ * A reply whose first non-empty line is a standalone bare completion ("完成。"
1947
+ * or "Done.") followed by a results summary. The whole text no longer matches
1948
+ * the single-line BARE_COMPLETION anchor, but the summary must still be treated
1949
+ * as a whole-task completion claim.
1950
+ */
1951
+ function leadingBareCompletionClaim(text) {
1952
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1953
+ const first = lines[0];
1954
+ if (!first || !BARE_COMPLETION.test(normalizeTitleLine(first))) return false;
1955
+ const rest = normalizeClause(lines.slice(1).join("\n"));
1956
+ if (!rest) return true;
1957
+ if (CONTINUATION.test(rest)) return false;
1958
+ if (TRAILING_NEGATION.test(rest)) return false;
1959
+ if (CONDITIONAL.test(rest)) return false;
1960
+ if (looksQuotedOrExemplary(rest)) return false;
1961
+ if (PARTIAL_ONLY.test(rest)) return false;
1962
+ return true;
1963
+ }
1964
+ function classifyCompletionClaim(text) {
1965
+ const normalized = normalizeClause(text);
1966
+ if (/waiting for (?:you|the user|input|your)|please (?:review|confirm|approve)|等待(?:您|你|用户)|请(?:确认|审阅|批准)/i.test(normalized)) return "user_wait";
1967
+ if (/waiting for (?:the )?(?:result|output|response|build|test|deployment)|等待(?:结果|输出|构建|测试|部署|响应)/i.test(normalized)) return "external_wait";
1968
+ if (isWholeTaskCompletionClaim(normalized)) return "complete";
1969
+ return "report";
1970
+ }
1971
+ function decideTurnStopping(projection, assistantText, turn, maxAttempts) {
1972
+ if (!projection.enabled) return { action: "stop" };
1973
+ if (!isWholeTaskCompletionClaim(assistantText)) return { action: "stop" };
1974
+ if (hasCurrentCertificate(projection)) return { action: "stop" };
1975
+ const attempts = projection.continuationAttempts.get(turn) ?? 0;
1976
+ if (attempts >= maxAttempts) return {
1977
+ action: "stop",
1978
+ reason: "continuation attempt limit reached"
1979
+ };
1980
+ projection.continuationAttempts.set(turn, attempts + 1);
1981
+ return {
1982
+ action: "continue",
1983
+ reason: "whole-task completion claimed without a current certificate"
1984
+ };
1985
+ }
1986
+ function latestAssistantText(events) {
1987
+ for (let index = events.length - 1; index >= 0; index--) {
1988
+ const event = events[index];
1989
+ if (event.type !== "assistant/message") continue;
1990
+ const text = event.data.message?.content?.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n") ?? "";
1991
+ if (text.trim()) return text;
1992
+ }
1993
+ return "";
1994
+ }
1995
+
1996
+ //#endregion
1997
+ export { classifyClause as A, normalizeClause as B, renderRecoveryPacket as C, isVerifyingCapability as D, evidenceMatchesItem as E, segmentClauses as F, sanitizeUrl as H, COMMAND_SURFACE_MANIFEST as I, validateManifest as L, extractMethod as M, extractOperation as N, captureClause as O, isInformationalMessage as P, canonicalizePath as R, recoveryDigest as S, evidenceCoverage as T, sha256 as U, sanitizeClauseText as V, createProjection as W, classifyUserInteraction as _, goalCompletionDenial as a, closingHint as b, supersedeItem as c, extractToolSubject as d, isDeterministicCheck as f, parseShellCommand as g, parsePwshCommand as h, latestAssistantText as i, extractArtifactPaths as j, captureItem as k, evidenceFromPersistedToolResult as l, isRunExecutable as m, decideTurnStopping as n, hasCurrentCertificate as o, withDurability as p, isWholeTaskCompletionClaim as r, deriveProjection as s, classifyCompletionClaim as t, extractTextContent as u, certifyCheckpoint as v, bindingSatisfies as w, openItems$1 as x, DEFAULT_RECOVERY_CHAR_BUDGET as y, digestStrings as z };