@deftai/directive-core 0.105.0 → 0.107.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 (173) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/gate-lists.js +2 -0
  3. package/dist/check/named-cause.js +1 -1
  4. package/dist/design-critique/exclusive-chip.d.ts +33 -0
  5. package/dist/design-critique/exclusive-chip.js +68 -0
  6. package/dist/design-critique/parent-audit.d.ts +59 -0
  7. package/dist/design-critique/parent-audit.js +121 -0
  8. package/dist/doctor/checks.d.ts +6 -0
  9. package/dist/doctor/checks.js +35 -0
  10. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  11. package/dist/forward-coverage/diff-coverage.js +112 -0
  12. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  13. package/dist/forward-coverage/diff-lines.js +173 -0
  14. package/dist/forward-coverage/evaluate.d.ts +24 -9
  15. package/dist/forward-coverage/evaluate.js +125 -19
  16. package/dist/hooks/dest-form.d.ts +46 -0
  17. package/dist/hooks/dest-form.js +575 -0
  18. package/dist/hooks/dispatcher.d.ts +17 -1
  19. package/dist/hooks/dispatcher.js +82 -11
  20. package/dist/hooks/index.d.ts +1 -0
  21. package/dist/hooks/index.js +1 -0
  22. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  23. package/dist/init-deposit/agent-hooks.js +1 -1
  24. package/dist/intake/clause-derivation.d.ts +9 -0
  25. package/dist/intake/clause-derivation.js +30 -3
  26. package/dist/intake/github-auth-modes-cli.js +14 -2
  27. package/dist/intake/github-auth-modes.d.ts +54 -9
  28. package/dist/intake/github-auth-modes.js +326 -123
  29. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +26 -0
  30. package/dist/lifecycle/completed-tracked-on-delivery.js +37 -14
  31. package/dist/lifecycle/completed-write-guard.d.ts +48 -0
  32. package/dist/lifecycle/completed-write-guard.js +373 -0
  33. package/dist/lifecycle/index.d.ts +1 -0
  34. package/dist/lifecycle/index.js +1 -0
  35. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  36. package/dist/lifecycle-visible/evaluate.js +710 -0
  37. package/dist/lifecycle-visible/index.d.ts +2 -0
  38. package/dist/lifecycle-visible/index.js +2 -0
  39. package/dist/literal-acceptance/capture.d.ts +16 -0
  40. package/dist/literal-acceptance/capture.js +78 -10
  41. package/dist/literal-acceptance/evaluate.js +25 -3
  42. package/dist/literal-acceptance/index.d.ts +2 -2
  43. package/dist/literal-acceptance/index.js +2 -2
  44. package/dist/literal-acceptance/run.js +9 -3
  45. package/dist/literal-acceptance/types.d.ts +6 -0
  46. package/dist/literal-acceptance/types.js +6 -0
  47. package/dist/orchestration/judgment-policy.d.ts +13 -0
  48. package/dist/orchestration/judgment-policy.js +15 -0
  49. package/dist/orchestration/probe-session.js +13 -8
  50. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  51. package/dist/orchestration/verify-judgment-gates.js +43 -11
  52. package/dist/policy/ceremony-dial.js +7 -7
  53. package/dist/policy/host-hooks.d.ts +20 -0
  54. package/dist/policy/host-hooks.js +121 -2
  55. package/dist/policy/org-force-on-migration.js +2 -1
  56. package/dist/policy/plan-extensions.d.ts +1 -1
  57. package/dist/policy/plan-extensions.js +2 -0
  58. package/dist/policy/product-signal.js +6 -7
  59. package/dist/policy/require-human-merge.js +8 -6
  60. package/dist/policy/resolve.d.ts +7 -2
  61. package/dist/policy/resolve.js +25 -8
  62. package/dist/policy/runtime-authority.d.ts +18 -0
  63. package/dist/policy/runtime-authority.js +11 -0
  64. package/dist/policy/value-feedback.js +7 -8
  65. package/dist/policy/write-fence.js +3 -0
  66. package/dist/preflight-cache/evaluate.d.ts +12 -0
  67. package/dist/preflight-cache/evaluate.js +24 -4
  68. package/dist/preflight-cache/index.d.ts +1 -1
  69. package/dist/preflight-cache/index.js +1 -1
  70. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  71. package/dist/product-first-done-gate/evaluate.js +82 -10
  72. package/dist/release/closed-verb-gate.d.ts +12 -0
  73. package/dist/release/closed-verb-gate.js +51 -0
  74. package/dist/release/index.d.ts +1 -0
  75. package/dist/release/index.js +1 -0
  76. package/dist/release/pipeline.js +7 -0
  77. package/dist/release/types.d.ts +12 -0
  78. package/dist/render/constants.d.ts +2 -1
  79. package/dist/render/constants.js +2 -1
  80. package/dist/render/framework-commands.js +4 -0
  81. package/dist/render/spec-validate.js +23 -12
  82. package/dist/run-summary/types.d.ts +4 -0
  83. package/dist/scm/design-critique-chip.d.ts +37 -0
  84. package/dist/scm/design-critique-chip.js +171 -0
  85. package/dist/scm/index.d.ts +1 -0
  86. package/dist/scm/index.js +1 -0
  87. package/dist/scm/main.d.ts +3 -0
  88. package/dist/scm/main.js +15 -1
  89. package/dist/scm/readiness-cli.d.ts +2 -0
  90. package/dist/scm/readiness-cli.js +42 -1
  91. package/dist/scm/readiness.d.ts +2 -1
  92. package/dist/scm/readiness.js +7 -1
  93. package/dist/scope/decompose.js +2 -1
  94. package/dist/scope/delivery-evidence.d.ts +27 -1
  95. package/dist/scope/index.d.ts +1 -0
  96. package/dist/scope/index.js +1 -0
  97. package/dist/scope/lifecycle-write.d.ts +24 -0
  98. package/dist/scope/lifecycle-write.js +64 -0
  99. package/dist/scope/main.js +18 -6
  100. package/dist/scope/promote-from-issue.js +5 -5
  101. package/dist/scope/transition.js +90 -11
  102. package/dist/scope-provenance/evaluate.js +8 -4
  103. package/dist/session/ac-pass-banking.d.ts +5 -0
  104. package/dist/session/ac-pass-banking.js +3 -2
  105. package/dist/session/git.d.ts +18 -0
  106. package/dist/session/git.js +81 -1
  107. package/dist/session/occupancy.d.ts +2 -1
  108. package/dist/session/occupancy.js +11 -4
  109. package/dist/session/process-cost.d.ts +32 -10
  110. package/dist/session/process-cost.js +189 -1
  111. package/dist/session/product-state-hash.js +41 -24
  112. package/dist/session/ritual-entrypoint.js +16 -1
  113. package/dist/session/session-start.d.ts +18 -0
  114. package/dist/session/session-start.js +49 -2
  115. package/dist/session/verify-session-ritual.d.ts +46 -4
  116. package/dist/session/verify-session-ritual.js +118 -10
  117. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  118. package/dist/swarm/finalize-cohort-cli.js +165 -35
  119. package/dist/swarm/finalize-cohort.d.ts +1 -0
  120. package/dist/swarm/finalize-cohort.js +14 -2
  121. package/dist/swarm/launch.d.ts +69 -0
  122. package/dist/swarm/launch.js +191 -1
  123. package/dist/swarm/routing-set-cli.js +2 -0
  124. package/dist/swarm/routing-verify.d.ts +1 -1
  125. package/dist/swarm/routing-verify.js +2 -2
  126. package/dist/triage/actions/index.d.ts +4 -0
  127. package/dist/triage/actions/index.js +6 -0
  128. package/dist/triage/bootstrap/index.js +6 -6
  129. package/dist/triage/evaluate/evaluate.d.ts +11 -0
  130. package/dist/triage/evaluate/evaluate.js +169 -0
  131. package/dist/triage/evaluate/github.d.ts +16 -0
  132. package/dist/triage/evaluate/github.js +153 -0
  133. package/dist/triage/evaluate/index.d.ts +9 -0
  134. package/dist/triage/evaluate/index.js +8 -0
  135. package/dist/triage/evaluate/paths.d.ts +7 -0
  136. package/dist/triage/evaluate/paths.js +25 -0
  137. package/dist/triage/evaluate/sink.d.ts +5 -0
  138. package/dist/triage/evaluate/sink.js +52 -0
  139. package/dist/triage/evaluate/types.d.ts +93 -0
  140. package/dist/triage/evaluate/types.js +13 -0
  141. package/dist/triage/evaluate/validity.d.ts +8 -0
  142. package/dist/triage/evaluate/validity.js +87 -0
  143. package/dist/triage/evaluate/value.d.ts +12 -0
  144. package/dist/triage/evaluate/value.js +33 -0
  145. package/dist/triage/evaluate/wip-census.d.ts +5 -0
  146. package/dist/triage/evaluate/wip-census.js +39 -0
  147. package/dist/triage/evaluate/worktrees.d.ts +8 -0
  148. package/dist/triage/evaluate/worktrees.js +68 -0
  149. package/dist/triage/evaluate/xbrief-refs.d.ts +8 -0
  150. package/dist/triage/evaluate/xbrief-refs.js +60 -0
  151. package/dist/triage/help/registry-data.d.ts +22 -8
  152. package/dist/triage/help/registry-data.js +54 -6
  153. package/dist/triage/index.d.ts +1 -0
  154. package/dist/triage/index.js +1 -0
  155. package/dist/triage/welcome/writers.d.ts +1 -1
  156. package/dist/triage/welcome/writers.js +10 -5
  157. package/dist/value/feedback-file.d.ts +17 -2
  158. package/dist/value/feedback-file.js +80 -6
  159. package/dist/value/readback.d.ts +4 -1
  160. package/dist/value/readback.js +51 -12
  161. package/dist/vbrief-reconcile/labels.d.ts +1 -0
  162. package/dist/vbrief-reconcile/labels.js +30 -0
  163. package/dist/vbrief-validate/schema.d.ts +4 -0
  164. package/dist/vbrief-validate/schema.js +1 -1
  165. package/dist/vbrief-validation/story-quality.d.ts +7 -0
  166. package/dist/vbrief-validation/story-quality.js +8 -1
  167. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  168. package/dist/verify-env/agent-hook-readiness.js +12 -8
  169. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  170. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  171. package/dist/verify-env/agent-hooks.js +3 -4
  172. package/dist/xbrief-migrate/agents-header.js +69 -7
  173. package/package.json +7 -3
@@ -0,0 +1,575 @@
1
+ /**
2
+ * Product dest-form classifier for in-repo Shell mutations (#3438).
3
+ *
4
+ * NEW harvest — do not reuse `classifyShellAuthzOps` / `listShellOps` as-is.
5
+ * Those return [] for `git checkout --` / `rm` of ordinary product paths.
6
+ *
7
+ * ! Resolves a target for ONE shape only: a single simple command. Everything
8
+ * else is recognized and fail-closed. Reconstructing a target from shell state
9
+ * was implemented and withdrawn — cwd depends on operator precedence, on exit
10
+ * status, on subshell boundaries, and on git config, and every resolution rule
11
+ * added produced its own fence bypass. Do not add it back; widen the recognized
12
+ * *forms* instead, and see `content/contracts/path-write-fence.md`.
13
+ *
14
+ * Residual known-open (recognition, not resolution): `python -c`,
15
+ * `cmd /c copy`, obfuscated `bash -c`.
16
+ *
17
+ * Commit-time active-scope is out of this slice (#3438), and ⊗ do not weaken
18
+ * Edit to match Shell. Keep this sentence on ONE line: acceptance clause 4 is
19
+ * verified by a literal token search, so wrapping it silently fails the gate.
20
+ */
21
+ import { record, toolInputRecord } from "./classify/payload.js";
22
+ /** Injected write target for expansion dests so the fence cannot match a literal glob. */
23
+ export const SHELL_DEST_EXPANSION_SENTINEL = "__shell_dest_expansion__";
24
+ /** Globs / braces on a token. `$` and backtick never reach here unescaped — `hasUnsupportedSyntax` already fail-closed those. */
25
+ const EXPANSION_DEST = /[*?[\]{}]/;
26
+ const WRAP_BINS = new Set(["sudo", "env", "command"]);
27
+ /** Wrapper flags that consume the next token. Unlisted `-` flags are boolean. */
28
+ const WRAP_VALUE_OPTS = {
29
+ sudo: new Set(["-u", "-g", "-C", "-p", "-r", "-t", "-T", "-D"]),
30
+ env: new Set(["-u", "-C", "-S", "--chdir", "--split-string"]),
31
+ command: new Set(),
32
+ };
33
+ /** Verbs this classifier recognizes, for scanning past a wrapper's options. */
34
+ const RECOGNIZED_BINS = new Set(["rm", "rm.exe", "rmdir", "rmdir.exe", "git", "git.exe"]);
35
+ function isRecognizedBin(token) {
36
+ if (token === undefined)
37
+ return false;
38
+ return RECOGNIZED_BINS.has(token.replace(/^[({!]+/, "").toLowerCase());
39
+ }
40
+ const GIT_GLOBAL_VALUE_OPTS = new Set([
41
+ "-C",
42
+ "-c",
43
+ "--git-dir",
44
+ "--work-tree",
45
+ "--namespace",
46
+ "--config-env",
47
+ "--super-prefix",
48
+ "--list-cmds",
49
+ ]);
50
+ const RESTORE_VALUE_OPTS = new Set(["--source", "-s", "--conflict"]);
51
+ /**
52
+ * Characters a preceding backslash escapes in an unquoted word. A backslash
53
+ * before anything else is retained, so Windows dests keep their separators.
54
+ */
55
+ const SHELL_ESCAPABLE = new Set([
56
+ " ",
57
+ "\t",
58
+ "\n",
59
+ "\r",
60
+ '"',
61
+ "'",
62
+ "\\",
63
+ "$",
64
+ "`",
65
+ "&",
66
+ "|",
67
+ ";",
68
+ "(",
69
+ ")",
70
+ "<",
71
+ ">",
72
+ "*",
73
+ "?",
74
+ "[",
75
+ "]",
76
+ "{",
77
+ "}",
78
+ "~",
79
+ "#",
80
+ ]);
81
+ /**
82
+ * Inject a dest path so `inspectMutationGates` sees the same write target as Edit/Write.
83
+ * Preserves original payload fields (command, posture markers).
84
+ */
85
+ export function payloadWithInjectedWriteTarget(payload, destPath) {
86
+ const rec = record(payload) ?? {};
87
+ const toolInput = toolInputRecord(rec);
88
+ return {
89
+ ...rec,
90
+ tool_input: {
91
+ ...(toolInput ?? {}),
92
+ file_path: destPath,
93
+ },
94
+ };
95
+ }
96
+ /**
97
+ * Classify recognized product dest-forms in a shell command (#3438 v1 list).
98
+ * Empty when the command is not `git checkout -- <paths>`, `git restore`, or `rm`/`rmdir`.
99
+ */
100
+ export function destFormHasExpansion(path) {
101
+ return EXPANSION_DEST.test(path);
102
+ }
103
+ export function classifyProductDestForms(command) {
104
+ const cmd = command.trim();
105
+ if (cmd.length === 0)
106
+ return [];
107
+ const segments = splitCommandSegments(cmd);
108
+ // A single simple command is the only shape whose target this classifier can
109
+ // PROVE. Anything compound gets recognized but never resolved: reconstructing
110
+ // a target from shell state was tried and abandoned (#3438) — cwd depends on
111
+ // operator precedence, on exit status (`cd x || …`), on subshell boundaries,
112
+ // and on git context options, so each reconstruction rule grew its own
113
+ // bypass.
114
+ //
115
+ // ⊗ Recognition is NOT total either, and this is not a security boundary.
116
+ // Only four verbs are recognized, and a non-literal verb (`\rm`, `rm${IFS}x`)
117
+ // defeats the tokenizer, so the fail-closed branch below only fires when the
118
+ // verb is still legible. Unrecognized mutators (`git reset --hard`,
119
+ // `git clean`, `git checkout` without `--`, `mv`, `sed -i`, `>` redirection)
120
+ // and interpreters (`bash -c`, `python -c`) are fail-OPEN. See the threat
121
+ // model in content/contracts/path-write-fence.md: this is a guardrail for
122
+ // careless agents, not a boundary against adversarial ones.
123
+ if (segments.length === 1 && !hasUnsupportedSyntax(cmd)) {
124
+ return classifySimpleDestForms(segments[0] ?? "");
125
+ }
126
+ // Compound: report one fail-closed dest per recognized kind. No path is
127
+ // claimed, because any path claimed here would be a guess.
128
+ const kinds = new Set();
129
+ for (const seg of segments) {
130
+ for (const dest of classifyDestFormSegment(seg))
131
+ kinds.add(dest.kind);
132
+ }
133
+ return [...kinds].map((kind) => ({
134
+ kind,
135
+ path: SHELL_DEST_EXPANSION_SENTINEL,
136
+ expansion: true,
137
+ }));
138
+ }
139
+ /** Classify one command with no operators, no grouping, and no substitution. */
140
+ function classifySimpleDestForms(segment) {
141
+ const found = [];
142
+ const seen = new Set();
143
+ for (const dest of classifyDestFormSegment(segment)) {
144
+ const path = dest.path;
145
+ const expansion =
146
+ // `unsafe` from the classifier (git context options relocate the tree).
147
+ dest.expansion === true ||
148
+ // A RETAINED backslash (one the tokenizer did not consume as an escape)
149
+ // is dialect-ambiguous. On win32 it is a path separator, so a Windows
150
+ // absolute dest means what it looks like; under a POSIX shell -- including
151
+ // Git Bash ON win32 -- the same word drops its separators and targets
152
+ // something else entirely. The payload does not say which shell will run
153
+ // the string, so the target cannot be proved either way. Fail closed (#3624).
154
+ path.includes("\\") ||
155
+ // `~` expands to $HOME only at the START of a word, so a trailing `~`
156
+ // (`foo.ts~`) is an ordinary path and must not be swept up here.
157
+ path.startsWith("~") ||
158
+ destFormHasExpansion(path);
159
+ const key = `${dest.kind}\0${path}`;
160
+ if (seen.has(key))
161
+ continue;
162
+ seen.add(key);
163
+ found.push(expansion ? { kind: dest.kind, path, expansion: true } : { kind: dest.kind, path });
164
+ }
165
+ return found;
166
+ }
167
+ /**
168
+ * Unquoted syntax whose effect on the target this classifier does not model:
169
+ * subshell grouping, brace groups, and command substitution. Presence of any of
170
+ * it makes the command non-simple, so it is recognized and fail-closed rather
171
+ * than resolved (#3438).
172
+ */
173
+ function hasUnsupportedSyntax(command) {
174
+ let quote = null;
175
+ for (let i = 0; i < command.length; i++) {
176
+ const c = command[i];
177
+ if (c === undefined)
178
+ break;
179
+ if (quote !== null) {
180
+ // Double-quoted `\$` / `\`` are literals; skip the escape so we do not
181
+ // treat them as substitution (false-deny under `shellDestForms: enforce`).
182
+ if (quote === '"' && c === "\\" && i + 1 < command.length) {
183
+ i++;
184
+ continue;
185
+ }
186
+ // A single-quoted string is fully literal; unescaped `"$` / backtick still expand.
187
+ if (c === quote)
188
+ quote = null;
189
+ else if (quote === '"' && (c === "$" || c === "`"))
190
+ return true;
191
+ continue;
192
+ }
193
+ if (c === "\\" && i + 1 < command.length) {
194
+ i++;
195
+ continue;
196
+ }
197
+ if (c === "'" || c === '"') {
198
+ quote = c;
199
+ continue;
200
+ }
201
+ if (c === "(" || c === ")" || c === "{" || c === "}" || c === "`" || c === "$")
202
+ return true;
203
+ }
204
+ return false;
205
+ }
206
+ /**
207
+ * Split on unquoted `&&`, `||`, `|`, `&`, `;`, and newline. Segment TEXT only —
208
+ * deliberately no cwd or precedence metadata. A command that splits into more
209
+ * than one segment is not simple, and the only thing done with the pieces is to
210
+ * recognize which dest-form kinds appear in them.
211
+ */
212
+ function splitCommandSegments(command) {
213
+ const segments = [];
214
+ let cur = "";
215
+ let quote = null;
216
+ const flush = () => {
217
+ segments.push(cur);
218
+ cur = "";
219
+ };
220
+ for (let i = 0; i < command.length; i++) {
221
+ const c = command[i];
222
+ if (c === undefined)
223
+ break;
224
+ if (quote !== null) {
225
+ if (c === quote)
226
+ quote = null;
227
+ cur += c;
228
+ continue;
229
+ }
230
+ if (c === "\\" && i + 1 < command.length) {
231
+ cur += c;
232
+ cur += command[i + 1] ?? "";
233
+ i++;
234
+ continue;
235
+ }
236
+ if (c === "'" || c === '"') {
237
+ quote = c;
238
+ cur += c;
239
+ continue;
240
+ }
241
+ if ((c === "&" && command[i + 1] === "&") || (c === "|" && command[i + 1] === "|")) {
242
+ flush();
243
+ i++;
244
+ continue;
245
+ }
246
+ if (c === "|" || c === "&" || c === ";" || c === "\n" || c === "\r") {
247
+ flush();
248
+ continue;
249
+ }
250
+ cur += c;
251
+ }
252
+ flush();
253
+ return segments.filter((seg) => seg.trim().length > 0);
254
+ }
255
+ function tokenizeSegment(segment) {
256
+ const tokens = [];
257
+ let cur = "";
258
+ let quote = null;
259
+ const push = () => {
260
+ if (cur.length > 0) {
261
+ tokens.push(cur);
262
+ cur = "";
263
+ }
264
+ };
265
+ for (let i = 0; i < segment.length; i++) {
266
+ const c = segment[i];
267
+ if (c === undefined)
268
+ break;
269
+ if (quote !== null) {
270
+ if (c === quote) {
271
+ quote = null;
272
+ continue;
273
+ }
274
+ if (quote === '"' && c === "\\" && i + 1 < segment.length) {
275
+ cur += segment[i + 1] ?? "";
276
+ i++;
277
+ continue;
278
+ }
279
+ cur += c;
280
+ continue;
281
+ }
282
+ if (c === "\\" && i + 1 < segment.length) {
283
+ const next = segment[i + 1] ?? "";
284
+ // `rm protected\ file` is ONE path. But this module also accepts Windows
285
+ // dests (`C:\Repos\file.ts`), where a backslash is a separator, not an
286
+ // escape — so only consume it before a character that needs escaping and
287
+ // otherwise keep it verbatim (#3438).
288
+ if (SHELL_ESCAPABLE.has(next)) {
289
+ cur += next;
290
+ i++;
291
+ continue;
292
+ }
293
+ cur += c;
294
+ continue;
295
+ }
296
+ if (c === "'" || c === '"') {
297
+ quote = c;
298
+ continue;
299
+ }
300
+ if (c === " " || c === "\t") {
301
+ push();
302
+ continue;
303
+ }
304
+ cur += c;
305
+ }
306
+ push();
307
+ return tokens;
308
+ }
309
+ function isEnvAssign(token) {
310
+ const eq = token.indexOf("=");
311
+ if (eq <= 0)
312
+ return false;
313
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(token.slice(0, eq));
314
+ }
315
+ /**
316
+ * Environment assignments that can relocate git's work tree.
317
+ *
318
+ * `GIT_WORK_TREE=` / `GIT_DIR=` do it directly. `GIT_CONFIG_KEY_n=core.worktree`
319
+ * does it through the env-config protocol, and `GIT_CONFIG*` file overrides can
320
+ * carry `core.worktree` in a file this classifier never reads — all fail closed
321
+ * rather than resolving a prefix (#3438).
322
+ */
323
+ function isGitWorkTreeAssign(token) {
324
+ const eq = token.indexOf("=");
325
+ if (eq <= 0)
326
+ return false;
327
+ const key = token.slice(0, eq);
328
+ const value = token.slice(eq + 1);
329
+ if (key === "GIT_WORK_TREE" || key === "GIT_DIR")
330
+ return value.length > 0;
331
+ // `GIT_CONFIG_KEY_0=core.worktree` with its paired VALUE relocates the tree.
332
+ if (/^GIT_CONFIG_KEY_\d+$/.test(key))
333
+ return value.toLowerCase() === "core.worktree";
334
+ // A config FILE override can set core.worktree out of sight.
335
+ return key === "GIT_CONFIG" || key === "GIT_CONFIG_GLOBAL" || key === "GIT_CONFIG_SYSTEM";
336
+ }
337
+ function skipPrefix(tokens) {
338
+ let i = 0;
339
+ let gitContext = false;
340
+ const consumeAssigns = () => {
341
+ while (i < tokens.length) {
342
+ const tok = tokens[i];
343
+ if (tok === undefined)
344
+ break;
345
+ // Standalone grouping / negation punctuation: skip so the binary behind it
346
+ // is still RECOGNIZED. Only compound commands can reach here, and every
347
+ // one of them is fail-closed, so liberal recognition is the safe side.
348
+ if (tok === "{" || tok === "}" || tok === "(" || tok === ")" || tok === "!") {
349
+ i++;
350
+ continue;
351
+ }
352
+ if (!isEnvAssign(tok))
353
+ break;
354
+ if (isGitWorkTreeAssign(tok))
355
+ gitContext = true;
356
+ i++;
357
+ }
358
+ };
359
+ consumeAssigns();
360
+ const wrap = tokens[i]?.toLowerCase();
361
+ if (wrap !== undefined && WRAP_BINS.has(wrap)) {
362
+ i++;
363
+ consumeAssigns();
364
+ // Skip this wrapper's own options, including value-taking ones (`sudo -u
365
+ // root`, `env -u VAR`). Do NOT scan past a later non-option token: that is
366
+ // the real executable (`sudo grep rm file` must stay unclassifiable).
367
+ const valueOpts = WRAP_VALUE_OPTS[wrap] ?? new Set();
368
+ while (i < tokens.length) {
369
+ const tok = tokens[i];
370
+ if (tok === undefined)
371
+ break;
372
+ if (isRecognizedBin(tok))
373
+ break;
374
+ if (tok === "--") {
375
+ i++;
376
+ break;
377
+ }
378
+ if (tok.startsWith("-")) {
379
+ i += valueOpts.has(tok) ? 2 : 1;
380
+ continue;
381
+ }
382
+ if (isEnvAssign(tok)) {
383
+ if (isGitWorkTreeAssign(tok))
384
+ gitContext = true;
385
+ i++;
386
+ continue;
387
+ }
388
+ break;
389
+ }
390
+ }
391
+ return { i, gitContext };
392
+ }
393
+ /**
394
+ * Skip git's global options, reporting whether any of them RELOCATES the work
395
+ * tree. Relocation is not resolved: `-C` composes, `--work-tree` resolves
396
+ * against the `-C` chain, `--git-dir` interacts with both, and `-c
397
+ * core.workTree` depends on the git dir. Each of those resolution rules grew
398
+ * its own fence bypass (#3438), so a relocating command fails closed instead.
399
+ */
400
+ function skipGitGlobals(tokens, start) {
401
+ let i = start;
402
+ let gitContext = false;
403
+ while (i < tokens.length) {
404
+ const t = tokens[i];
405
+ if (t === undefined || !t.startsWith("-"))
406
+ return { i, gitContext };
407
+ if (t === "-c" || t === "--config-env") {
408
+ if (isCoreWorkTreeConfig(tokens[i + 1]))
409
+ gitContext = true;
410
+ i += 2;
411
+ continue;
412
+ }
413
+ if (t.startsWith("--config-env=")) {
414
+ if (isCoreWorkTreeConfig(t.slice("--config-env=".length)))
415
+ gitContext = true;
416
+ i++;
417
+ continue;
418
+ }
419
+ if (t === "-C" || t === "--work-tree" || t === "--git-dir") {
420
+ gitContext = true;
421
+ i += 2;
422
+ continue;
423
+ }
424
+ if (t.startsWith("--work-tree=") ||
425
+ t.startsWith("--git-dir=") ||
426
+ (t.startsWith("-C") && t.length > 2)) {
427
+ gitContext = true;
428
+ i++;
429
+ continue;
430
+ }
431
+ if (t.startsWith("--") && t.includes("=")) {
432
+ i++;
433
+ continue;
434
+ }
435
+ if (GIT_GLOBAL_VALUE_OPTS.has(t)) {
436
+ i += 2;
437
+ continue;
438
+ }
439
+ if (t.startsWith("-c")) {
440
+ if (isCoreWorkTreeConfig(t.slice(2)))
441
+ gitContext = true;
442
+ i++;
443
+ continue;
444
+ }
445
+ i++;
446
+ }
447
+ return { i, gitContext };
448
+ }
449
+ function pathsAfterDashDash(tokens, start) {
450
+ const dash = tokens.indexOf("--", start);
451
+ if (dash < 0)
452
+ return [];
453
+ return tokens.slice(dash + 1).filter((t) => t.length > 0);
454
+ }
455
+ function skipRestoreFlag(tokens, i) {
456
+ const t = tokens[i];
457
+ if (t === undefined)
458
+ return i;
459
+ if (t.startsWith("--") && t.includes("="))
460
+ return i + 1;
461
+ if (RESTORE_VALUE_OPTS.has(t))
462
+ return i + 2;
463
+ if (t.startsWith("-s") && t.length > 2)
464
+ return i + 1;
465
+ return i + 1;
466
+ }
467
+ function dests(kind, paths, unsafe = false) {
468
+ return paths
469
+ .filter((path) => path.length > 0)
470
+ .map((path) => (unsafe ? { kind, path, expansion: true } : { kind, path }));
471
+ }
472
+ /**
473
+ * `-c core.workTree=...` / `--config-env` relocates the work tree through git
474
+ * config. Resolution depends on the git dir, which this classifier does not
475
+ * model, so the target is not reconstructable and stays fail-closed (#3438).
476
+ */
477
+ function isCoreWorkTreeConfig(token) {
478
+ if (token === undefined)
479
+ return false;
480
+ const eq = token.indexOf("=");
481
+ const key = eq < 0 ? token : token.slice(0, eq);
482
+ return key.toLowerCase() === "core.worktree";
483
+ }
484
+ function classifyDestFormSegment(segment) {
485
+ const tokens = tokenizeSegment(segment.trim());
486
+ const prefixSkip = skipPrefix(tokens);
487
+ let i = prefixSkip.i;
488
+ const binRaw = tokens[i];
489
+ if (binRaw === undefined)
490
+ return [];
491
+ // Strip grouping punctuation glued to the binary (`{rm`, `(rm`) for the same
492
+ // recognition reason as in skipPrefix. Unquoted grouping can only appear in a
493
+ // command that is already fail-closed.
494
+ const bin = binRaw.replace(/^[({!]+/, "").toLowerCase();
495
+ if (bin === "git" || bin === "git.exe") {
496
+ const skipped = skipGitGlobals(tokens, i + 1);
497
+ i = skipped.i;
498
+ // `--pathspec-from-file=<f>` (and its NUL variant) put the targets INSIDE a
499
+ // file. Reading it would mean file I/O at hook time plus resolving those
500
+ // paths against a cwd this classifier does not know — the resolution trap
501
+ // #3438 abandoned. Recognized, never resolved (#3624).
502
+ const pathspecFromFile = tokens.some((t) => t.startsWith("--pathspec-from-file") || t === "--pathspec-file-nul");
503
+ // Any relocating context option makes the target unprovable — fail closed.
504
+ const unsafe = skipped.gitContext || prefixSkip.gitContext || pathspecFromFile;
505
+ const sub = tokens[i]?.toLowerCase();
506
+ // `--pathspec-from-file` supplies the pathspec instead of argv, so there is
507
+ // no dest token to mark unsafe and no `--` to require. Emit the sentinel
508
+ // directly, the way a compound command does — the kind is known, the target
509
+ // is not (#3624).
510
+ if (pathspecFromFile && (sub === "checkout" || sub === "restore")) {
511
+ return [
512
+ {
513
+ kind: sub === "checkout" ? "git-checkout" : "git-restore",
514
+ path: SHELL_DEST_EXPANSION_SENTINEL,
515
+ expansion: true,
516
+ },
517
+ ];
518
+ }
519
+ if (sub === "checkout") {
520
+ return dests("git-checkout", pathsAfterDashDash(tokens, i + 1), unsafe);
521
+ }
522
+ if (sub === "restore") {
523
+ i++;
524
+ const afterDash = pathsAfterDashDash(tokens, i);
525
+ if (afterDash.length > 0)
526
+ return dests("git-restore", afterDash, unsafe);
527
+ const paths = [];
528
+ while (i < tokens.length) {
529
+ const t = tokens[i];
530
+ if (t === undefined)
531
+ break;
532
+ if (t === "--") {
533
+ paths.push(...tokens.slice(i + 1));
534
+ break;
535
+ }
536
+ if (t.startsWith("-")) {
537
+ i = skipRestoreFlag(tokens, i);
538
+ continue;
539
+ }
540
+ paths.push(t);
541
+ i++;
542
+ }
543
+ return dests("git-restore", paths, unsafe);
544
+ }
545
+ return [];
546
+ }
547
+ if (bin === "rm" || bin === "rm.exe") {
548
+ return dests("rm", collectRmPaths(tokens, i + 1));
549
+ }
550
+ if (bin === "rmdir" || bin === "rmdir.exe") {
551
+ return dests("rmdir", collectRmPaths(tokens, i + 1));
552
+ }
553
+ return [];
554
+ }
555
+ function collectRmPaths(tokens, start) {
556
+ const paths = [];
557
+ let i = start;
558
+ while (i < tokens.length) {
559
+ const t = tokens[i];
560
+ if (t === undefined)
561
+ break;
562
+ if (t === "--") {
563
+ paths.push(...tokens.slice(i + 1));
564
+ break;
565
+ }
566
+ if (t.startsWith("-")) {
567
+ i++;
568
+ continue;
569
+ }
570
+ paths.push(t);
571
+ i++;
572
+ }
573
+ return paths;
574
+ }
575
+ //# sourceMappingURL=dest-form.js.map
@@ -2,7 +2,8 @@ import { type AuthzState, type HumanOriginGrant } from "../authz/index.js";
2
2
  import { detectDeftDirectiveDisable } from "../policy/deft-directive-disable.js";
3
3
  import { detectNoDeftDirective } from "../policy/no-deft-directive.js";
4
4
  import { type RuntimeAuthorityPolicy } from "../policy/runtime-authority.js";
5
- import { type VerifyResult } from "../session/verify-session-ritual.js";
5
+ import { type GitRunner } from "../session/git.js";
6
+ import { type DetectWorkSelection, type RitualRunner, type VerifyResult } from "../session/verify-session-ritual.js";
6
7
  import { type HookPayloadContext } from "./classify/index.js";
7
8
  import { type ActiveScopeInspection } from "./scope.js";
8
9
  export { ASSIST_SESSION_POSTURE_ENV, hookReadOnlyFromPayload, isAssistPosture, isEphemeralSpawn, isExploreSpawn, isReadOnlyHookContext, } from "./readonly.js";
@@ -60,6 +61,21 @@ export interface HookPolicySeams {
60
61
  readonly verifyRitual?: (projectRoot: string) => VerifyResult;
61
62
  /** @deprecated Compatibility seam for existing tests and callers. */
62
63
  readonly inspectRitual?: (projectRoot: string) => VerifyResult;
64
+ /**
65
+ * Test seam: ritual runner used by the default write-gate verifier (#3738).
66
+ * Ignored when `verifyRitual` / `inspectRitual` is supplied.
67
+ */
68
+ readonly ritualRunner?: RitualRunner;
69
+ /**
70
+ * Test seam: work-selection detector for the default write-gate verifier (#3738).
71
+ * Ignored when `verifyRitual` / `inspectRitual` is supplied.
72
+ */
73
+ readonly detectWorkSelection?: DetectWorkSelection;
74
+ /**
75
+ * Test seam: git runner for the default write-gate verifier (#3738).
76
+ * Ignored when `verifyRitual` / `inspectRitual` is supplied.
77
+ */
78
+ readonly ritualRunGit?: GitRunner;
63
79
  readonly inspectScope?: (projectRoot: string) => ActiveScopeInspection;
64
80
  readonly sessionStart?: (projectRoot: string) => {
65
81
  code: number;