@deftai/directive-core 0.99.0 → 0.101.0

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 (166) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +241 -30
  4. package/dist/authz/decompose-apply.d.ts +81 -0
  5. package/dist/authz/decompose-apply.js +377 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/check/cached-orchestrator.d.ts +26 -5
  13. package/dist/check/cached-orchestrator.js +228 -6
  14. package/dist/check/gate-lists.d.ts +10 -4
  15. package/dist/check/gate-lists.js +19 -5
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +2 -1
  18. package/dist/check/named-cause.d.ts +45 -0
  19. package/dist/check/named-cause.js +121 -0
  20. package/dist/check/orchestrator.js +6 -1
  21. package/dist/doctor/checks.d.ts +44 -0
  22. package/dist/doctor/checks.js +428 -2
  23. package/dist/doctor/help.d.ts +5 -0
  24. package/dist/doctor/help.js +31 -0
  25. package/dist/doctor/index.d.ts +2 -0
  26. package/dist/doctor/index.js +2 -0
  27. package/dist/doctor/main.d.ts +6 -7
  28. package/dist/doctor/main.js +130 -79
  29. package/dist/doctor/session-coda.d.ts +82 -0
  30. package/dist/doctor/session-coda.js +151 -0
  31. package/dist/doctor/types.d.ts +16 -0
  32. package/dist/doctor/which.d.ts +20 -1
  33. package/dist/doctor/which.js +67 -1
  34. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  35. package/dist/finish-loop/pr-finish-loop.js +116 -5
  36. package/dist/finish-loop/types.d.ts +1 -1
  37. package/dist/hooks/dispatcher.d.ts +9 -5
  38. package/dist/hooks/dispatcher.js +23 -15
  39. package/dist/hooks/readonly.d.ts +6 -3
  40. package/dist/hooks/readonly.js +44 -25
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +3 -0
  43. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  44. package/dist/init-deposit/agent-hooks.js +51 -31
  45. package/dist/init-deposit/gitignore.js +2 -0
  46. package/dist/intake/issue-ingest.js +36 -0
  47. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  48. package/dist/lifecycle/completed-consistency.js +443 -0
  49. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  50. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  51. package/dist/lifecycle/index.d.ts +2 -0
  52. package/dist/lifecycle/index.js +2 -0
  53. package/dist/literal-acceptance/capture.d.ts +52 -0
  54. package/dist/literal-acceptance/capture.js +658 -0
  55. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  56. package/dist/literal-acceptance/evaluate.js +193 -0
  57. package/dist/literal-acceptance/index.d.ts +12 -0
  58. package/dist/literal-acceptance/index.js +12 -0
  59. package/dist/literal-acceptance/run.d.ts +38 -0
  60. package/dist/literal-acceptance/run.js +216 -0
  61. package/dist/literal-acceptance/safety.d.ts +24 -0
  62. package/dist/literal-acceptance/safety.js +251 -0
  63. package/dist/literal-acceptance/types.d.ts +82 -0
  64. package/dist/literal-acceptance/types.js +26 -0
  65. package/dist/policy/ac-pass-banking.d.ts +58 -0
  66. package/dist/policy/ac-pass-banking.js +150 -0
  67. package/dist/policy/ceremony-dial.d.ts +96 -11
  68. package/dist/policy/ceremony-dial.js +250 -23
  69. package/dist/policy/index.d.ts +2 -0
  70. package/dist/policy/index.js +16 -1
  71. package/dist/policy/merge-approval-head.d.ts +129 -0
  72. package/dist/policy/merge-approval-head.js +461 -0
  73. package/dist/policy/require-human-merge.d.ts +5 -0
  74. package/dist/policy/require-human-merge.js +5 -0
  75. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  76. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  77. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  78. package/dist/pr-merge-readiness/compute.js +99 -14
  79. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  80. package/dist/pr-merge-readiness/gh.js +268 -0
  81. package/dist/pr-merge-readiness/index.d.ts +2 -2
  82. package/dist/pr-merge-readiness/index.js +2 -2
  83. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  84. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  85. package/dist/pr-wait-mergeable/cascade.js +115 -1
  86. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  87. package/dist/pr-wait-mergeable/main.js +4 -0
  88. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  89. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  90. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  91. package/dist/preflight/evaluate.d.ts +15 -1
  92. package/dist/preflight/evaluate.js +33 -2
  93. package/dist/preflight-cache/evaluate.d.ts +3 -0
  94. package/dist/preflight-cache/evaluate.js +3 -0
  95. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  96. package/dist/product-first-done-gate/acceptance.js +247 -0
  97. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  98. package/dist/product-first-done-gate/check-mode.js +148 -0
  99. package/dist/product-first-done-gate/evaluate.d.ts +50 -0
  100. package/dist/product-first-done-gate/evaluate.js +255 -0
  101. package/dist/product-first-done-gate/index.d.ts +11 -0
  102. package/dist/product-first-done-gate/index.js +11 -0
  103. package/dist/product-first-done-gate/types.d.ts +64 -0
  104. package/dist/product-first-done-gate/types.js +61 -0
  105. package/dist/render/framework-commands.js +4 -0
  106. package/dist/run-summary/emit.d.ts +56 -0
  107. package/dist/run-summary/emit.js +171 -0
  108. package/dist/run-summary/index.d.ts +4 -0
  109. package/dist/run-summary/index.js +4 -0
  110. package/dist/run-summary/path.d.ts +25 -0
  111. package/dist/run-summary/path.js +78 -0
  112. package/dist/run-summary/types.d.ts +76 -0
  113. package/dist/run-summary/types.js +21 -0
  114. package/dist/scope/acceptance-evidence.d.ts +110 -0
  115. package/dist/scope/acceptance-evidence.js +422 -0
  116. package/dist/scope/coverage-map.d.ts +98 -0
  117. package/dist/scope/coverage-map.js +558 -0
  118. package/dist/scope/decompose.d.ts +5 -0
  119. package/dist/scope/decompose.js +134 -11
  120. package/dist/scope/index.d.ts +3 -0
  121. package/dist/scope/index.js +3 -0
  122. package/dist/scope/parent-lineage.d.ts +125 -0
  123. package/dist/scope/parent-lineage.js +626 -0
  124. package/dist/scope/transition.d.ts +8 -0
  125. package/dist/scope/transition.js +61 -2
  126. package/dist/session/ac-pass-banking.d.ts +221 -0
  127. package/dist/session/ac-pass-banking.js +761 -0
  128. package/dist/session/active-cli.d.ts +79 -0
  129. package/dist/session/active-cli.js +382 -0
  130. package/dist/session/deposit-sha.d.ts +49 -0
  131. package/dist/session/deposit-sha.js +121 -0
  132. package/dist/session/effort-budget.d.ts +136 -0
  133. package/dist/session/effort-budget.js +352 -0
  134. package/dist/session/index.d.ts +7 -0
  135. package/dist/session/index.js +7 -0
  136. package/dist/session/orientation-compression.d.ts +127 -0
  137. package/dist/session/orientation-compression.js +425 -0
  138. package/dist/session/orientation-state.d.ts +29 -0
  139. package/dist/session/orientation-state.js +85 -0
  140. package/dist/session/session-start.d.ts +31 -0
  141. package/dist/session/session-start.js +228 -2
  142. package/dist/session/toolchain-preflight.d.ts +64 -0
  143. package/dist/session/toolchain-preflight.js +151 -0
  144. package/dist/session/verify-session-ritual.d.ts +14 -0
  145. package/dist/session/verify-session-ritual.js +33 -0
  146. package/dist/story-ready/evaluate.d.ts +11 -0
  147. package/dist/story-ready/evaluate.js +41 -3
  148. package/dist/triage/actions/candidates-log.d.ts +1 -2
  149. package/dist/triage/actions/candidates-log.js +37 -6
  150. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  151. package/dist/triage/bootstrap/gitignore.js +8 -2
  152. package/dist/triage/bootstrap/index.js +15 -3
  153. package/dist/triage/classify/index.d.ts +1 -0
  154. package/dist/triage/classify/index.js +2 -0
  155. package/dist/triage/classify/label-mirror.js +16 -0
  156. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  157. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  158. package/dist/triage/reconcile/reconcile.js +28 -6
  159. package/dist/triage/summary/index.js +12 -4
  160. package/dist/triage/welcome/default-mode.js +16 -0
  161. package/dist/ts-check-lane/run-lane.js +21 -4
  162. package/dist/vbrief-validate/conformance.js +7 -0
  163. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  164. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  165. package/dist/xbrief-migrate/migrate-project.js +92 -3
  166. package/package.json +15 -3
@@ -0,0 +1,658 @@
1
+ /**
2
+ * Capture exact stated acceptance commands from a task statement (#3267).
3
+ *
4
+ * Only extracts commands that appear literally in the text — never invents
5
+ * or paraphrases. Prefer fenced blocks and labeled lines (verify:/command:/run:).
6
+ *
7
+ * Safety-rejected shell-shaped lines are recorded on a rejected ledger so they
8
+ * do not vanish silently (#3267 residual). Dedup keys include cwd/expectedExitCode
9
+ * so distinct execution contexts are not collapsed.
10
+ */
11
+ import { evaluateCommandSafety } from "./safety.js";
12
+ import { EXECUTABLE_LITERAL_SOURCES, LITERAL_ACCEPTANCE_REJECTED_METADATA_KEY } from "./types.js";
13
+ /** Labeled keywords (single-token, linear match). */
14
+ const LABELED_KEYWORDS = new Set(["verify", "command", "run", "check", "exec", "shell"]);
15
+ /** Words that look like a CLI invocation start (not prose). */
16
+ const CLI_START_RE = /^(?:task|deft|directive|pnpm|npm|npx|yarn|bun|node|python|py|pytest|vitest|cargo|go|dotnet|make|curl|gh|git|uv|pip|poetry|rg|echo|true|false)\b/i;
17
+ function isNonEmptyString(value) {
18
+ return typeof value === "string" && value.trim().length > 0;
19
+ }
20
+ /**
21
+ * Normalize a captured command string.
22
+ * Trailing period is stripped only when it is sentence punctuation — not when the
23
+ * last token is `.` / `..` (cwd / parent path, e.g. `docker build .`).
24
+ */
25
+ function normalizeCommand(raw) {
26
+ let s = raw.trim();
27
+ // Strip surrounding backticks once.
28
+ if (s.startsWith("`") && s.endsWith("`") && s.length >= 2) {
29
+ s = s.slice(1, -1).trim();
30
+ }
31
+ // Strip trailing period that is clearly sentence punctuation (not a path token).
32
+ // Preserve: `file.ts`, `..`, and commands ending in whitespace+`.` (cwd path).
33
+ if (s.endsWith(".") && !s.endsWith("..") && !/\.\w+$/.test(s) && !/\s\.$/.test(s) && s !== ".") {
34
+ s = s.slice(0, -1).trim();
35
+ }
36
+ if (s.length === 0)
37
+ return null;
38
+ // Reject pure prose / multi-sentence blobs without CLI shape.
39
+ if (s.length > 500)
40
+ return null;
41
+ if (s.includes("\n")) {
42
+ // Multi-line only accepted from fenced blocks (caller splits).
43
+ return null;
44
+ }
45
+ return s;
46
+ }
47
+ function looksLikeShellCommand(command) {
48
+ if (CLI_START_RE.test(command))
49
+ return true;
50
+ // Flags or paths suggest a real invocation.
51
+ if (/\s--?[a-zA-Z]/.test(command))
52
+ return true;
53
+ // Subcommand shape: word word
54
+ if (/^\S+\s+\S+/.test(command) && !/^[A-Z][a-z]+\s/.test(command))
55
+ return true;
56
+ return false;
57
+ }
58
+ /** Dedupe key includes execution context so distinct cwd/exit targets stay distinct. */
59
+ function commandDedupeKey(cmd) {
60
+ const cwd = cmd.cwd !== null && cmd.cwd !== undefined && cmd.cwd.trim().length > 0 ? cmd.cwd.trim() : "";
61
+ const exit = typeof cmd.expectedExitCode === "number" && Number.isFinite(cmd.expectedExitCode)
62
+ ? cmd.expectedExitCode
63
+ : 0;
64
+ return `${cmd.command}\0${cwd}\0${exit}`;
65
+ }
66
+ function recordRejected(buckets, command, reason, sourceSpan) {
67
+ const key = `${command}\0${reason}`;
68
+ if (buckets.rejectedSeen.has(key))
69
+ return;
70
+ buckets.rejectedSeen.add(key);
71
+ buckets.rejected.push({
72
+ command,
73
+ reason,
74
+ sourceSpan,
75
+ });
76
+ }
77
+ /**
78
+ * Push a full command record (preserves cwd / expectedStdout / expectedExitCode).
79
+ * Safety failures go to the rejected ledger instead of silent drop.
80
+ */
81
+ function pushCommand(buckets, cmd) {
82
+ const safety = evaluateCommandSafety(cmd.command);
83
+ if (!safety.ok) {
84
+ recordRejected(buckets, cmd.command, safety.reason ?? "unsafe command", cmd.sourceSpan ?? null);
85
+ return;
86
+ }
87
+ const key = commandDedupeKey(cmd);
88
+ if (buckets.seen.has(key))
89
+ return;
90
+ buckets.seen.add(key);
91
+ buckets.out.push({
92
+ command: cmd.command,
93
+ cwd: cmd.cwd ?? null,
94
+ expectedStdout: cmd.expectedStdout ?? null,
95
+ expectedExitCode: cmd.expectedExitCode ?? 0,
96
+ source: cmd.source,
97
+ sourceSpan: cmd.sourceSpan ?? null,
98
+ });
99
+ }
100
+ /** Capture-path helper: command string only (defaults for context fields). */
101
+ function pushUnique(buckets, command, source, sourceSpan) {
102
+ pushCommand(buckets, {
103
+ command,
104
+ cwd: null,
105
+ expectedStdout: null,
106
+ expectedExitCode: 0,
107
+ source,
108
+ sourceSpan,
109
+ });
110
+ }
111
+ /** Linear: does heading text mention acceptance/verify regions? */
112
+ function isRegionHeading(text) {
113
+ const low = text.toLowerCase();
114
+ if (low.includes("acceptance"))
115
+ return true;
116
+ if (low.includes("verify") || low.includes("verification"))
117
+ return true;
118
+ if (low.includes("done-gate") || low.includes("done gate"))
119
+ return true;
120
+ if (low.includes("run verbatim"))
121
+ return true;
122
+ if (low === "check" || low.startsWith("check ") || low.endsWith(" check"))
123
+ return true;
124
+ return false;
125
+ }
126
+ /** Skip leading spaces/tabs only (bounded, linear). */
127
+ function skipWs(line, start) {
128
+ let i = start;
129
+ while (i < line.length && (line[i] === " " || line[i] === "\t"))
130
+ i += 1;
131
+ return i;
132
+ }
133
+ /** Parse `verify: cmd` / bullet-prefixed labeled lines without polynomial regex. */
134
+ function matchLabeledCommand(line) {
135
+ let i = skipWs(line, 0);
136
+ // Optional bullet or number prefix.
137
+ if (i < line.length && "-*+".includes(line[i])) {
138
+ i = skipWs(line, i + 1);
139
+ }
140
+ else if (i < line.length) {
141
+ const ch = line[i];
142
+ if (ch >= "0" && ch <= "9") {
143
+ while (i < line.length) {
144
+ const d = line[i];
145
+ if (d < "0" || d > "9")
146
+ break;
147
+ i += 1;
148
+ }
149
+ if (i < line.length && (line[i] === "." || line[i] === ")"))
150
+ i += 1;
151
+ i = skipWs(line, i);
152
+ }
153
+ }
154
+ const keywordStart = i;
155
+ while (i < line.length && /[a-zA-Z]/.test(line[i]))
156
+ i += 1;
157
+ if (i === keywordStart)
158
+ return null;
159
+ const keyword = line.slice(keywordStart, i).toLowerCase();
160
+ if (!LABELED_KEYWORDS.has(keyword))
161
+ return null;
162
+ i = skipWs(line, i);
163
+ if (i >= line.length || line[i] !== ":")
164
+ return null;
165
+ i = skipWs(line, i + 1);
166
+ if (i >= line.length)
167
+ return null;
168
+ return line.slice(i).trimEnd();
169
+ }
170
+ /** Mid-line `verify: cmd` (e.g. "Also run: verify: task check"). */
171
+ function matchMidlineLabeled(line) {
172
+ const low = line.toLowerCase();
173
+ for (const kw of LABELED_KEYWORDS) {
174
+ if (kw === "run" || kw === "check")
175
+ continue; // too ambiguous mid-line
176
+ const needle = `${kw}:`;
177
+ const idx = low.indexOf(needle);
178
+ if (idx < 0)
179
+ continue;
180
+ const rest = line.slice(idx + needle.length).trim();
181
+ if (rest.length > 0)
182
+ return rest;
183
+ }
184
+ return null;
185
+ }
186
+ /** `$ cmd` or `> cmd` prompt lines. */
187
+ function matchPromptCommand(line) {
188
+ let i = skipWs(line, 0);
189
+ if (i >= line.length)
190
+ return null;
191
+ if (line[i] !== "$" && line[i] !== ">")
192
+ return null;
193
+ i = skipWs(line, i + 1);
194
+ if (i >= line.length)
195
+ return null;
196
+ return line.slice(i).trimEnd();
197
+ }
198
+ /**
199
+ * Walk fenced ``` / ~~~ blocks; keep fence body lines that look like shell.
200
+ * When `requireRegion` is true, only fences under an acceptance/verify heading.
201
+ */
202
+ function extractFromFences(text, requireRegion, buckets) {
203
+ const lines = text.split(/\r?\n/);
204
+ let inFence = false;
205
+ let fenceLang = "";
206
+ let regionActive = !requireRegion;
207
+ let fenceStartLine = 0;
208
+ for (let i = 0; i < lines.length; i += 1) {
209
+ const line = lines[i] ?? "";
210
+ const heading = matchMarkdownHeading(line);
211
+ if (heading !== null && !inFence) {
212
+ regionActive = requireRegion ? isRegionHeading(heading.text) : true;
213
+ continue;
214
+ }
215
+ const fenceOpen = matchFenceOpen(line);
216
+ if (fenceOpen !== null && !inFence) {
217
+ inFence = true;
218
+ fenceLang = fenceOpen.lang;
219
+ fenceStartLine = i + 1;
220
+ continue;
221
+ }
222
+ if (inFence && matchFenceClose(line)) {
223
+ inFence = false;
224
+ fenceLang = "";
225
+ continue;
226
+ }
227
+ if (!inFence)
228
+ continue;
229
+ if (requireRegion && !regionActive)
230
+ continue;
231
+ // Skip comment-only fence lines.
232
+ const trimmed = line.trim();
233
+ if (trimmed.length === 0 || trimmed.startsWith("#") || trimmed.startsWith("//")) {
234
+ continue;
235
+ }
236
+ // Shell-ish langs always accepted; unknown langs need CLI shape.
237
+ const langOk = isShellFenceLang(fenceLang);
238
+ if (!langOk && fenceLang !== "")
239
+ continue;
240
+ // Strip leading prompt markers inside fences.
241
+ let body = trimmed;
242
+ if (body.startsWith("$ ") || body.startsWith("> ")) {
243
+ body = body.slice(2).trim();
244
+ }
245
+ const normalized = normalizeCommand(body);
246
+ if (normalized === null)
247
+ continue;
248
+ if (!looksLikeShellCommand(normalized) && (fenceLang === "" || !langOk))
249
+ continue;
250
+ pushUnique(buckets, normalized, "task_statement", `fence@L${fenceStartLine}${fenceLang ? `:${fenceLang}` : ""}`);
251
+ }
252
+ }
253
+ function isShellFenceLang(lang) {
254
+ return (lang === "" ||
255
+ lang === "bash" ||
256
+ lang === "sh" ||
257
+ lang === "shell" ||
258
+ lang === "zsh" ||
259
+ lang === "console" ||
260
+ lang === "powershell" ||
261
+ lang === "pwsh" ||
262
+ lang === "cmd" ||
263
+ lang === "text");
264
+ }
265
+ /** Linear fence open: ```lang or ~~~lang */
266
+ function matchFenceOpen(line) {
267
+ const t = line.trimEnd();
268
+ if (t.startsWith("```")) {
269
+ const rest = t.slice(3).trim().toLowerCase();
270
+ if (rest.includes(" ") || rest.includes("\t"))
271
+ return null;
272
+ return { lang: rest };
273
+ }
274
+ if (t.startsWith("~~~")) {
275
+ const rest = t.slice(3).trim().toLowerCase();
276
+ if (rest.includes(" ") || rest.includes("\t"))
277
+ return null;
278
+ return { lang: rest };
279
+ }
280
+ return null;
281
+ }
282
+ function matchFenceClose(line) {
283
+ const t = line.trim();
284
+ return t === "```" || t === "~~~";
285
+ }
286
+ function matchMarkdownHeading(line) {
287
+ if (!line.startsWith("#"))
288
+ return null;
289
+ let level = 0;
290
+ while (level < line.length && line[level] === "#")
291
+ level += 1;
292
+ if (level < 1 || level > 6)
293
+ return null;
294
+ if (level >= line.length || line[level] !== " ")
295
+ return null;
296
+ return { level, text: line.slice(level + 1).trim() };
297
+ }
298
+ /** Extract from labeled lines and `$` prompts anywhere in the statement. */
299
+ function extractFromLabeledLines(text, buckets) {
300
+ const lines = text.split(/\r?\n/);
301
+ for (let i = 0; i < lines.length; i += 1) {
302
+ const line = lines[i] ?? "";
303
+ let capturedLabeled = false;
304
+ let labeledBody = matchLabeledCommand(line);
305
+ if (labeledBody === null && matchPromptCommand(line) === null) {
306
+ // Avoid treating `$ task verify:branch` as labeled "branch".
307
+ labeledBody = matchMidlineLabeled(line);
308
+ }
309
+ if (labeledBody !== null) {
310
+ const normalized = normalizeCommand(labeledBody);
311
+ if (normalized !== null && looksLikeShellCommand(normalized)) {
312
+ pushUnique(buckets, normalized, "task_statement", `labeled@L${i + 1}`);
313
+ capturedLabeled = true;
314
+ }
315
+ }
316
+ if (capturedLabeled)
317
+ continue;
318
+ const promptBody = matchPromptCommand(line);
319
+ if (promptBody !== null) {
320
+ const normalized = normalizeCommand(promptBody);
321
+ if (normalized !== null && looksLikeShellCommand(normalized)) {
322
+ pushUnique(buckets, normalized, "task_statement", `prompt@L${i + 1}`);
323
+ }
324
+ }
325
+ }
326
+ }
327
+ /**
328
+ * Extract inline `` `command` `` spans that follow verify/run language on the same line.
329
+ * Example: `run \`task check\` before done`
330
+ */
331
+ function extractInlineVerifySpans(text, buckets) {
332
+ const lines = text.split(/\r?\n/);
333
+ for (let i = 0; i < lines.length; i += 1) {
334
+ const line = lines[i] ?? "";
335
+ if (!/\b(verify|run|execute|acceptance)\b/i.test(line))
336
+ continue;
337
+ let pos = 0;
338
+ while (pos < line.length) {
339
+ const open = line.indexOf("`", pos);
340
+ if (open < 0)
341
+ break;
342
+ const close = line.indexOf("`", open + 1);
343
+ if (close < 0)
344
+ break;
345
+ const inner = line.slice(open + 1, close);
346
+ pos = close + 1;
347
+ if (inner.includes("\n"))
348
+ continue;
349
+ const normalized = normalizeCommand(inner);
350
+ if (normalized === null || !looksLikeShellCommand(normalized))
351
+ continue;
352
+ pushUnique(buckets, normalized, "task_statement", `inline@L${i + 1}`);
353
+ }
354
+ }
355
+ }
356
+ function emptyBuckets() {
357
+ return {
358
+ out: [],
359
+ seen: new Set(),
360
+ rejected: [],
361
+ rejectedSeen: new Set(),
362
+ };
363
+ }
364
+ /**
365
+ * Capture literal acceptance commands from free-form task statement text.
366
+ * Returns commands + rejected ledger — never invents commands.
367
+ */
368
+ export function captureLiteralAcceptanceCommandsDetailed(taskStatement) {
369
+ if (!isNonEmptyString(taskStatement)) {
370
+ return { commands: [], rejected: [] };
371
+ }
372
+ const buckets = emptyBuckets();
373
+ // Prefer fences under acceptance/verify headings first.
374
+ extractFromFences(taskStatement, true, buckets);
375
+ // Then any shell fences in the whole statement (still literal, not invented).
376
+ extractFromFences(taskStatement, false, buckets);
377
+ extractFromLabeledLines(taskStatement, buckets);
378
+ extractInlineVerifySpans(taskStatement, buckets);
379
+ return { commands: buckets.out, rejected: buckets.rejected };
380
+ }
381
+ /**
382
+ * Capture literal acceptance commands from free-form task statement text.
383
+ * Returns [] when none are stated — never invents commands.
384
+ */
385
+ export function captureLiteralAcceptanceCommands(taskStatement) {
386
+ return [...captureLiteralAcceptanceCommandsDetailed(taskStatement).commands];
387
+ }
388
+ /**
389
+ * Read already-stored commands from plan.metadata / swarm.verify_commands / item.command.
390
+ * Preserves exact strings and execution context (cwd / expectedStdout / expectedExitCode).
391
+ */
392
+ export function readStoredLiteralAcceptanceCommands(plan) {
393
+ return readStoredLiteralAcceptanceDetailed(plan).commands;
394
+ }
395
+ /** Read stored commands plus any persisted rejected ledger. */
396
+ export function readStoredLiteralAcceptanceDetailed(plan) {
397
+ if (plan === null || plan === undefined || typeof plan !== "object") {
398
+ return { commands: [], rejected: [] };
399
+ }
400
+ const buckets = emptyBuckets();
401
+ const metadata = asRecord(plan.metadata);
402
+ if (metadata !== null) {
403
+ const explicit = metadata.literal_acceptance_commands ?? metadata.literalAcceptanceCommands ?? null;
404
+ for (const cmd of coerceCommandList(explicit, "explicit", "metadata.literal_acceptance")) {
405
+ pushCommand(buckets, cmd);
406
+ }
407
+ // Persisted rejected ledger (operator visibility across reloads).
408
+ const persistedRejected = metadata[LITERAL_ACCEPTANCE_REJECTED_METADATA_KEY] ??
409
+ metadata.literalAcceptanceRejected ??
410
+ null;
411
+ if (Array.isArray(persistedRejected)) {
412
+ for (const entry of persistedRejected) {
413
+ const rec = asRecord(entry);
414
+ if (rec === null)
415
+ continue;
416
+ if (!isNonEmptyString(rec.command) || !isNonEmptyString(rec.reason))
417
+ continue;
418
+ recordRejected(buckets, rec.command.trim(), rec.reason.trim(), isNonEmptyString(rec.sourceSpan) ? rec.sourceSpan : null);
419
+ }
420
+ }
421
+ const swarm = asRecord(metadata.swarm);
422
+ if (swarm !== null) {
423
+ // swarm.verify_commands is a string list (legacy). Prefer richer
424
+ // literal_acceptance_commands rows already loaded — do not invent a
425
+ // null-cwd duplicate for the same command text.
426
+ const alreadyHasCommand = (command) => buckets.out.some((c) => c.command === command);
427
+ for (const cmd of coerceCommandList(swarm.verify_commands, "verify_commands", "swarm.verify_commands")) {
428
+ if (alreadyHasCommand(cmd.command))
429
+ continue;
430
+ pushCommand(buckets, cmd);
431
+ }
432
+ for (const cmd of coerceCommandList(swarm.literal_acceptance_commands ?? swarm.literalAcceptanceCommands, "metadata", "swarm.literal_acceptance")) {
433
+ pushCommand(buckets, cmd);
434
+ }
435
+ }
436
+ }
437
+ walkPlanItems(plan.items, "items", buckets);
438
+ return { commands: buckets.out, rejected: buckets.rejected };
439
+ }
440
+ function walkPlanItems(items, pathPrefix, buckets) {
441
+ if (!Array.isArray(items))
442
+ return;
443
+ items.forEach((item, index) => {
444
+ const rec = asRecord(item);
445
+ if (rec === null)
446
+ return;
447
+ const path = `${pathPrefix}[${index}]`;
448
+ const commandField = rec.command ?? rec.verify ?? rec.verify_command;
449
+ if (isNonEmptyString(commandField)) {
450
+ const normalized = normalizeCommand(commandField);
451
+ if (normalized !== null) {
452
+ pushUnique(buckets, normalized, "plan_item", `${path}.command`);
453
+ }
454
+ }
455
+ walkPlanItems(rec.subItems, `${path}.subItems`, buckets);
456
+ walkPlanItems(rec.items, `${path}.items`, buckets);
457
+ });
458
+ }
459
+ function coerceCommandList(raw, source, span) {
460
+ if (raw === null || raw === undefined)
461
+ return [];
462
+ if (typeof raw === "string") {
463
+ const normalized = normalizeCommand(raw);
464
+ if (normalized === null)
465
+ return [];
466
+ return [
467
+ {
468
+ command: normalized,
469
+ cwd: null,
470
+ expectedStdout: null,
471
+ expectedExitCode: 0,
472
+ source,
473
+ sourceSpan: span,
474
+ },
475
+ ];
476
+ }
477
+ if (!Array.isArray(raw))
478
+ return [];
479
+ const out = [];
480
+ for (const entry of raw) {
481
+ if (typeof entry === "string") {
482
+ const normalized = normalizeCommand(entry);
483
+ if (normalized !== null) {
484
+ out.push({
485
+ command: normalized,
486
+ cwd: null,
487
+ expectedStdout: null,
488
+ expectedExitCode: 0,
489
+ source,
490
+ sourceSpan: span,
491
+ });
492
+ }
493
+ continue;
494
+ }
495
+ const rec = asRecord(entry);
496
+ if (rec === null)
497
+ continue;
498
+ const command = rec.command ?? rec.cmd ?? rec.shell;
499
+ if (!isNonEmptyString(command))
500
+ continue;
501
+ const normalized = normalizeCommand(command);
502
+ if (normalized === null)
503
+ continue;
504
+ // Preserve persisted source when present so task_statement stays capture-only (#3267).
505
+ const persistedSource = typeof rec.source === "string" && rec.source.trim().length > 0
506
+ ? rec.source.trim()
507
+ : source;
508
+ out.push({
509
+ command: normalized,
510
+ cwd: isNonEmptyString(rec.cwd) ? rec.cwd.trim() : null,
511
+ expectedStdout: isNonEmptyString(rec.expectedStdout)
512
+ ? rec.expectedStdout
513
+ : isNonEmptyString(rec.expected_stdout)
514
+ ? rec.expected_stdout
515
+ : null,
516
+ expectedExitCode: typeof rec.expectedExitCode === "number"
517
+ ? rec.expectedExitCode
518
+ : typeof rec.expected_exit_code === "number"
519
+ ? rec.expected_exit_code
520
+ : 0,
521
+ source: persistedSource,
522
+ sourceSpan: isNonEmptyString(rec.sourceSpan)
523
+ ? rec.sourceSpan
524
+ : isNonEmptyString(rec.source_span)
525
+ ? rec.source_span
526
+ : span,
527
+ });
528
+ }
529
+ return out;
530
+ }
531
+ function asRecord(value) {
532
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
533
+ return value;
534
+ }
535
+ return null;
536
+ }
537
+ function isExecutableSource(source) {
538
+ return EXECUTABLE_LITERAL_SOURCES.includes(source);
539
+ }
540
+ /**
541
+ * Attach captured commands onto plan.metadata without paraphrasing.
542
+ * Merges with existing literal_acceptance_commands.
543
+ * Only agent-executable sources (not task_statement) are mirrored into
544
+ * swarm.verify_commands — issue text must be promoted explicitly (#3267).
545
+ * Returns a shallow-cloned plan with updated metadata.
546
+ */
547
+ export function attachLiteralAcceptanceCommands(plan, commands, rejected = []) {
548
+ if (commands.length === 0 && rejected.length === 0) {
549
+ return plan;
550
+ }
551
+ const metadata = {
552
+ ...(asRecord(plan.metadata) ?? {}),
553
+ };
554
+ const existing = readStoredLiteralAcceptanceDetailed(plan);
555
+ const merged = mergeCommands(existing.commands, commands);
556
+ metadata.literal_acceptance_commands = merged.map(toSerializable);
557
+ // Rejected ledger: merge and persist for operator visibility.
558
+ const rejectedMerged = mergeRejected(existing.rejected, rejected);
559
+ if (rejectedMerged.length > 0) {
560
+ metadata[LITERAL_ACCEPTANCE_REJECTED_METADATA_KEY] = rejectedMerged.map((r) => {
561
+ const row = { command: r.command, reason: r.reason };
562
+ if (r.sourceSpan !== null && r.sourceSpan !== undefined && r.sourceSpan.length > 0) {
563
+ row.sourceSpan = r.sourceSpan;
564
+ }
565
+ return row;
566
+ });
567
+ }
568
+ // Keep swarm.verify_commands in sync only for executable (agent-authored) sources.
569
+ // task_statement stays capture-only until an agent promotes exact strings (#3267).
570
+ const swarm = { ...(asRecord(metadata.swarm) ?? {}) };
571
+ const executableCmds = merged.filter((c) => isExecutableSource(c.source)).map((c) => c.command);
572
+ const prevVerify = Array.isArray(swarm.verify_commands)
573
+ ? swarm.verify_commands.filter((x) => typeof x === "string")
574
+ : [];
575
+ const verifyMerged = [...prevVerify];
576
+ for (const cmd of executableCmds) {
577
+ if (!verifyMerged.includes(cmd))
578
+ verifyMerged.push(cmd);
579
+ }
580
+ if (verifyMerged.length > 0) {
581
+ swarm.verify_commands = verifyMerged;
582
+ }
583
+ metadata.swarm = swarm;
584
+ return {
585
+ ...plan,
586
+ metadata,
587
+ };
588
+ }
589
+ /**
590
+ * Capture from task statement text and attach onto a plan (intake helper).
591
+ */
592
+ export function captureAndAttachLiteralAcceptance(plan, taskStatement) {
593
+ const captured = captureLiteralAcceptanceCommandsDetailed(taskStatement);
594
+ if (captured.commands.length === 0 && captured.rejected.length === 0) {
595
+ const stored = readStoredLiteralAcceptanceDetailed(plan);
596
+ return { plan, commands: stored.commands, rejected: stored.rejected };
597
+ }
598
+ const next = attachLiteralAcceptanceCommands(plan, captured.commands, captured.rejected);
599
+ const stored = readStoredLiteralAcceptanceDetailed(next);
600
+ return { plan: next, commands: stored.commands, rejected: stored.rejected };
601
+ }
602
+ function mergeCommands(a, b) {
603
+ const seen = new Set();
604
+ const out = [];
605
+ for (const cmd of [...a, ...b]) {
606
+ const key = commandDedupeKey(cmd);
607
+ if (seen.has(key))
608
+ continue;
609
+ seen.add(key);
610
+ out.push(cmd);
611
+ }
612
+ return out;
613
+ }
614
+ function mergeRejected(a, b) {
615
+ const seen = new Set();
616
+ const out = [];
617
+ for (const r of [...a, ...b]) {
618
+ const key = `${r.command}\0${r.reason}`;
619
+ if (seen.has(key))
620
+ continue;
621
+ seen.add(key);
622
+ out.push(r);
623
+ }
624
+ return out;
625
+ }
626
+ function toSerializable(cmd) {
627
+ const row = {
628
+ command: cmd.command,
629
+ source: cmd.source,
630
+ };
631
+ if (cmd.cwd !== null && cmd.cwd !== undefined && cmd.cwd.length > 0) {
632
+ row.cwd = cmd.cwd;
633
+ }
634
+ if (cmd.expectedStdout !== null &&
635
+ cmd.expectedStdout !== undefined &&
636
+ cmd.expectedStdout.length > 0) {
637
+ row.expectedStdout = cmd.expectedStdout;
638
+ }
639
+ if (cmd.expectedExitCode !== undefined && cmd.expectedExitCode !== 0) {
640
+ row.expectedExitCode = cmd.expectedExitCode;
641
+ }
642
+ if (cmd.sourceSpan !== null && cmd.sourceSpan !== undefined && cmd.sourceSpan.length > 0) {
643
+ row.sourceSpan = cmd.sourceSpan;
644
+ }
645
+ return row;
646
+ }
647
+ /** Format rejected ledger for CLI / complete-gate messages. */
648
+ export function formatRejectedLedger(rejected) {
649
+ if (rejected.length === 0)
650
+ return "";
651
+ const lines = rejected.map((r) => {
652
+ const span = r.sourceSpan !== null && r.sourceSpan !== undefined ? ` @${r.sourceSpan}` : "";
653
+ return ` ✗ rejected: ${r.command}${span} — ${r.reason}`;
654
+ });
655
+ return (`Literal acceptance rejected ${rejected.length} shell-shaped command(s) (#3267 safety ledger):\n` +
656
+ lines.join("\n"));
657
+ }
658
+ //# sourceMappingURL=capture.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Evaluate literal acceptance-command gate from an xBRIEF plan or path (#3267).
3
+ */
4
+ import type { LiteralAcceptanceCommand, LiteralAcceptanceGateResult, LiteralAcceptanceRunner, RejectedLiteralCommand } from "./types.js";
5
+ export interface EvaluateLiteralAcceptanceOptions {
6
+ readonly projectRoot?: string;
7
+ readonly runner?: LiteralAcceptanceRunner;
8
+ /**
9
+ * When true and the plan has no stored commands, re-scan Overview/Description
10
+ * narratives for stated commands (intake recovery). Default true.
11
+ */
12
+ readonly captureFromNarratives?: boolean;
13
+ /** Quiet success (empty message on pass). */
14
+ readonly quiet?: boolean;
15
+ }
16
+ /**
17
+ * Resolve commands + rejected ledger.
18
+ * Stored first; when captureFromNarratives is true (default), also re-scan narratives
19
+ * and merge any newly stated commands so stored entries cannot hide AC updates
20
+ * (Greptile conf residual).
21
+ */
22
+ export declare function resolveLiteralAcceptanceDetailed(plan: Record<string, unknown>, options?: {
23
+ readonly captureFromNarratives?: boolean;
24
+ }): {
25
+ readonly commands: readonly LiteralAcceptanceCommand[];
26
+ readonly rejected: readonly RejectedLiteralCommand[];
27
+ };
28
+ /**
29
+ * Resolve commands: stored first; optional narrative re-capture when empty.
30
+ */
31
+ export declare function resolveLiteralAcceptanceCommands(plan: Record<string, unknown>, options?: {
32
+ readonly captureFromNarratives?: boolean;
33
+ }): LiteralAcceptanceCommand[];
34
+ /**
35
+ * Evaluate from an in-memory plan object.
36
+ */
37
+ export declare function evaluateLiteralAcceptanceFromPlan(plan: Record<string, unknown>, options?: EvaluateLiteralAcceptanceOptions): LiteralAcceptanceGateResult;
38
+ /**
39
+ * Evaluate from an xBRIEF JSON file path.
40
+ */
41
+ export declare function evaluateLiteralAcceptanceFromPath(xbriefPath: string, options?: EvaluateLiteralAcceptanceOptions): LiteralAcceptanceGateResult;
42
+ /**
43
+ * Pure ceremony-dial contract helper: literal AC run is required at every depth
44
+ * including rapid/minimal (#3267 / #3214 / #3156).
45
+ */
46
+ export declare function isLiteralAcceptanceRequiredAtCeremonyDepth(_depth: string | null | undefined): boolean;
47
+ //# sourceMappingURL=evaluate.d.ts.map