@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
@@ -39,6 +39,12 @@ export interface MintGrantInput {
39
39
  readonly now?: Date;
40
40
  /** When true, pin grant id into state.activeGrantIds. */
41
41
  readonly pinActive?: boolean;
42
+ /** SHA-256 hex of exact draft bytes for scope.decompose.apply.structural (#3239). */
43
+ readonly contentDigest?: string | null;
44
+ /** Project-relative parent artifact path (#3239). */
45
+ readonly parentPath?: string | null;
46
+ /** Project-relative draft/target path (#3239). */
47
+ readonly targetPath?: string | null;
42
48
  }
43
49
  export declare function mintHumanOriginGrant(input: MintGrantInput): HumanOriginGrant;
44
50
  export interface RevokeGrantInput {
@@ -78,6 +78,9 @@ export function mintHumanOriginGrant(input) {
78
78
  storyIds: input.storyIds ?? [],
79
79
  issueIds: input.issueIds ?? [],
80
80
  cohortId: input.cohortId ?? null,
81
+ contentDigest: input.contentDigest ?? null,
82
+ parentPath: input.parentPath ?? null,
83
+ targetPath: input.targetPath ?? null,
81
84
  },
82
85
  semantics: {
83
86
  expiresAt: input.expiresAt ?? null,
@@ -180,7 +180,7 @@ const POLICY_AUTHORITY_MUTATORS = new Set([
180
180
  const KILL_SWITCH_BASENAMES = [".deft-directive-disable", ".no-deft-directive"];
181
181
  /**
182
182
  * Downloaders / decoders / remote-copy tools that can plant files without shell
183
- * redirects (#3206 / #3213). Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
183
+ * redirects (#3206 / #3213 / #3245). Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
184
184
  * and would classify bare `curl $URL` as a store write via opaque-expansion heuristics.
185
185
  */
186
186
  const DOWNLOADER_DECODER_BINS = new Set([
@@ -192,11 +192,103 @@ const DOWNLOADER_DECODER_BINS = new Set([
192
192
  "scp",
193
193
  "aria2c",
194
194
  "certutil",
195
+ // #3245 residual after #3213: archive extractors + further alt downloaders.
196
+ "tar",
197
+ "bsdtar",
198
+ "unzip",
199
+ "7z",
200
+ "7za",
201
+ "7zr",
202
+ "rclone",
203
+ "axel",
204
+ "fetch",
205
+ "socat",
206
+ "lftp",
207
+ // #3288 residual after #3245: crypto / alt-download / pipe / archive bins.
208
+ "gpg",
209
+ "age",
210
+ "zstd",
211
+ "unzstd",
212
+ "sftp",
213
+ "wget2",
214
+ "http",
215
+ "https",
216
+ "yt-dlp",
217
+ "ytdlp",
218
+ "aria2",
219
+ "mbuffer",
220
+ "cpio",
221
+ ]);
222
+ /**
223
+ * Archive extractors / alt writers that can plant via pathish operands without
224
+ * shell redirects (#3245 / #3288). Used for pathish authz/kill scans (prefer deny)
225
+ * and write-shape residual under UAT — not bare curl-class URL fetches.
226
+ */
227
+ const ARCHIVE_ALT_WRITE_BINS = new Set([
228
+ "tar",
229
+ "bsdtar",
230
+ "unzip",
231
+ "7z",
232
+ "7za",
233
+ "7zr",
234
+ "rclone",
235
+ "axel",
236
+ "fetch",
237
+ "socat",
238
+ "lftp",
239
+ // #3288 residual after #3245.
240
+ "gpg",
241
+ "age",
242
+ "zstd",
243
+ "unzstd",
244
+ "sftp",
245
+ "wget2",
246
+ "http",
247
+ "https",
248
+ "yt-dlp",
249
+ "ytdlp",
250
+ "aria2",
251
+ "mbuffer",
252
+ "cpio",
253
+ ]);
254
+ /**
255
+ * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288).
256
+ * Prefer a Set over a long `||` chain so coverage counts one membership check, not N branches.
257
+ */
258
+ const PROTECTED_POSITIONAL_BINS = new Set([
259
+ "scp",
260
+ "certutil",
261
+ "rclone",
262
+ "tar",
263
+ "bsdtar",
264
+ "unzip",
265
+ "7z",
266
+ "7za",
267
+ "7zr",
268
+ "socat",
269
+ "lftp",
270
+ // #3288 residual.
271
+ "sftp",
272
+ "cpio",
273
+ "gpg",
274
+ "age",
275
+ "zstd",
276
+ "unzstd",
277
+ "mbuffer",
195
278
  ]);
279
+ /** wget family (directory-prefix dest flags). */
280
+ const WGET_FAMILY_BINS = new Set(["wget", "wget2"]);
281
+ /** aria2 family (dir dest flags). */
282
+ const ARIA2_FAMILY_BINS = new Set(["aria2c", "aria2"]);
283
+ /** 7z family (attached -oDIR only). */
284
+ const SEVEN_Z_FAMILY_BINS = new Set(["7z", "7za", "7zr"]);
285
+ /** tar family (chdir -C / --directory). */
286
+ const TAR_FAMILY_BINS = new Set(["tar", "bsdtar"]);
196
287
  /**
197
288
  * File destination flags for downloaders/decoders (#3206).
198
289
  * normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
199
290
  * scp: `-o` is an SSH option — excluded in isDownloaderDestFlag.
291
+ * 7z: `-oDIR` is attached-only (no separate value token).
200
292
  */
201
293
  const DOWNLOADER_FILE_DEST_FLAGS = new Set([
202
294
  "-o",
@@ -209,11 +301,35 @@ const DOWNLOADER_FILE_DEST_FLAGS = new Set([
209
301
  const CURL_DIR_DEST_FLAGS = new Set(["--output-dir"]);
210
302
  const WGET_DIR_DEST_FLAGS = new Set(["-p", "--directory-prefix"]);
211
303
  const ARIA2C_DIR_DEST_FLAGS = new Set(["-d", "--dir"]);
304
+ /**
305
+ * tar/bsdtar extract directory (#3245).
306
+ * Case-sensitive: POSIX tar uses capital `-C` for chdir; lower `-c` is create and is NOT dest.
307
+ */
308
+ const TAR_DIR_DEST_FLAGS_EXACT = new Set(["-C"]);
309
+ const TAR_DIR_DEST_FLAGS_LOWER = new Set(["--directory"]);
310
+ /** unzip extract directory (#3245). */
311
+ const UNZIP_DIR_DEST_FLAGS = new Set(["-d"]);
312
+ /**
313
+ * cpio chdir before extract/create (#3288).
314
+ * Case-sensitive short form: POSIX cpio uses capital `-D`; lower `-d` is a create option bit.
315
+ * Long form `--directory` is accepted lowercased.
316
+ */
317
+ const CPIO_DIR_DEST_FLAGS_EXACT = new Set(["-D"]);
318
+ const CPIO_DIR_DEST_FLAGS_LOWER = new Set(["--directory"]);
212
319
  /**
213
320
  * Symlink / hard-link plant bins (#3213). Absent from prior killWriteBins →
214
321
  * `ln -sf … .deft-directive-disable` classified empty → UAT fail-open.
215
322
  */
216
323
  const SYMLINK_PLANT_BINS = new Set(["ln", "link", "mklink"]);
324
+ /** socat address prefixes that open a write target (#3245). */
325
+ const SOCAT_WRITE_ADDR_PREFIXES = [
326
+ "open:",
327
+ "create:",
328
+ "creat:",
329
+ "append:",
330
+ "owronly:",
331
+ "oappend:",
332
+ ];
217
333
  /** Final path segment of a token (path-qualified bins / .exe). */
218
334
  function binBareName(token) {
219
335
  const pathish = token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
@@ -227,17 +343,47 @@ function isDownloaderDecoderBin(token) {
227
343
  return false;
228
344
  return DOWNLOADER_DECODER_BINS.has(binBareName(token));
229
345
  }
230
- function isDownloaderDestFlag(flag, bin) {
346
+ /**
347
+ * True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
348
+ * tar `-C` must use raw case: lowercased `-c` is create-archive, not chdir (#3245).
349
+ * 7z uses attached `-oDIR` only — separate `-o PATH` is not a 7z dest form (handled elsewhere).
350
+ */
351
+ function isDownloaderDestFlag(flag, bin, rawFlag) {
231
352
  // scp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
232
353
  if (bin === "scp")
233
354
  return false;
355
+ // 7z family: only attached `-oDIR` (parsed in attached-short branch), not separate `-o PATH`.
356
+ if (SEVEN_Z_FAMILY_BINS.has(bin)) {
357
+ return false;
358
+ }
359
+ // #3288: cpio `-o` is copy-out (create archive), not a file dest; dest is `-D` / `--directory`.
360
+ if (bin === "cpio") {
361
+ if (rawFlag !== undefined && CPIO_DIR_DEST_FLAGS_EXACT.has(rawFlag))
362
+ return true;
363
+ // Long form is case-insensitive via normalizeToken (`flag` already lowercased).
364
+ if (CPIO_DIR_DEST_FLAGS_LOWER.has(flag))
365
+ return true;
366
+ return false;
367
+ }
234
368
  if (DOWNLOADER_FILE_DEST_FLAGS.has(flag))
235
369
  return true;
236
370
  if (bin === "curl" && CURL_DIR_DEST_FLAGS.has(flag))
237
371
  return true;
238
- if (bin === "wget" && WGET_DIR_DEST_FLAGS.has(flag))
372
+ if (WGET_FAMILY_BINS.has(bin) && WGET_DIR_DEST_FLAGS.has(flag))
239
373
  return true;
240
- if (bin === "aria2c" && ARIA2C_DIR_DEST_FLAGS.has(flag))
374
+ if (ARIA2_FAMILY_BINS.has(bin) && ARIA2C_DIR_DEST_FLAGS.has(flag))
375
+ return true;
376
+ if (TAR_FAMILY_BINS.has(bin)) {
377
+ if (rawFlag !== undefined && TAR_DIR_DEST_FLAGS_EXACT.has(rawFlag))
378
+ return true;
379
+ if (TAR_DIR_DEST_FLAGS_LOWER.has(flag))
380
+ return true;
381
+ // Exact match on raw when normalize collapsed case for long flags only.
382
+ if (rawFlag !== undefined && TAR_DIR_DEST_FLAGS_LOWER.has(normalizeToken(rawFlag)))
383
+ return true;
384
+ return false;
385
+ }
386
+ if (bin === "unzip" && UNZIP_DIR_DEST_FLAGS.has(flag))
241
387
  return true;
242
388
  return false;
243
389
  }
@@ -292,7 +438,8 @@ function firstUnquotedShellOpIndex(raw) {
292
438
  /**
293
439
  * Destinations from curl/wget/xxd/openssl/scp/aria2c/certutil via -o/--output/-O/-out/
294
440
  * --output-dir/-P/-d/--dir (separate, =value, or attached short form), xxd -r path-like
295
- * write positionals (#3206), and positional dests for scp/certutil (#3213).
441
+ * write positionals (#3206), positional dests for scp/certutil (#3213), and #3245 residual
442
+ * archive/alt bins (tar -C, unzip -d, 7z -oDIR, rclone positionals, socat OPEN:, axel/fetch -o).
296
443
  * openssl uses flags only (no positional dests — avoids treating -in paths as writes).
297
444
  * Segment stops at shell operators so compound `scp …authz…; echo` cannot overwrite dests.
298
445
  * O(n) token walk — no nested-quantifier regex on untrusted input.
@@ -309,13 +456,14 @@ function downloaderDecoderDestinations(tokens) {
309
456
  i++;
310
457
  // xxd reverse mode writes; without -r, path positionals are dump inputs (read).
311
458
  let xxdReverse = false;
312
- // scp / certutil: collect pathish operands in this segment (#3213).
459
+ // scp / certutil / rclone / archive extractors: collect pathish operands in this segment.
313
460
  // Fail-closed under UAT: any pathish mentioning protected store/kill paths is a dest
314
461
  // candidate (scp source-or-dest of `.deft/authz` is treated as settings — prefer deny
315
462
  // over source/dest thrash). Last pathish remains the ordinary write dest.
316
463
  // Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
317
464
  let lastPositionalPath = null;
318
465
  const protectedPathish = [];
466
+ const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin);
319
467
  while (i < tokens.length) {
320
468
  const raw = tokens[i];
321
469
  const n = normalizeToken(raw);
@@ -361,10 +509,12 @@ function downloaderDecoderDestinations(tokens) {
361
509
  continue;
362
510
  }
363
511
  // --flag=value (and rare -out=value); scp -oOption=Value attached form also lands here.
512
+ // tar --directory=PATH also lands here.
364
513
  if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
365
514
  const eq = raw.indexOf("=");
366
- const flag = normalizeToken(raw.slice(0, eq));
367
- if (isDownloaderDestFlag(flag, bin)) {
515
+ const flagRaw = raw.slice(0, eq);
516
+ const flag = normalizeToken(flagRaw);
517
+ if (isDownloaderDestFlag(flag, bin, flagRaw)) {
368
518
  dests.push(pathishToken(raw.slice(eq + 1)));
369
519
  }
370
520
  // scp attached -oProxyCommand=… is not a file dest — skip without recording.
@@ -373,32 +523,55 @@ function downloaderDecoderDestinations(tokens) {
373
523
  }
374
524
  // Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
375
525
  // Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
526
+ // 7z requires attached -oDIR (#3245).
376
527
  if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
377
528
  if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output")) {
378
529
  dests.push(pathishToken(raw.slice(4)));
379
530
  i++;
380
531
  continue;
381
532
  }
382
- if (n.startsWith("-o") && !n.startsWith("-out") && n.length > 2) {
533
+ // cpio `-o` is copy-out create mode, not a file dest (#3288).
534
+ if (bin !== "cpio" && n.startsWith("-o") && !n.startsWith("-out") && n.length > 2) {
535
+ dests.push(pathishToken(raw.slice(2)));
536
+ i++;
537
+ continue;
538
+ }
539
+ // wget / wget2 attached -Pdir
540
+ if (WGET_FAMILY_BINS.has(bin) &&
541
+ n.startsWith("-p") &&
542
+ n.length > 2 &&
543
+ !n.startsWith("-proxy")) {
544
+ dests.push(pathishToken(raw.slice(2)));
545
+ i++;
546
+ continue;
547
+ }
548
+ // aria2c / aria2 attached -dDIR (#3213 / #3288)
549
+ if (ARIA2_FAMILY_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
550
+ dests.push(pathishToken(raw.slice(2)));
551
+ i++;
552
+ continue;
553
+ }
554
+ // unzip attached -dDIR (#3245)
555
+ if (bin === "unzip" && n.startsWith("-d") && n.length > 2) {
383
556
  dests.push(pathishToken(raw.slice(2)));
384
557
  i++;
385
558
  continue;
386
559
  }
387
- // wget attached -Pdir
388
- if (bin === "wget" && n.startsWith("-p") && n.length > 2 && !n.startsWith("-proxy")) {
560
+ // tar attached -CDIR (rare; capital C required)
561
+ if (TAR_FAMILY_BINS.has(bin) && raw.startsWith("-C") && raw.length > 2) {
389
562
  dests.push(pathishToken(raw.slice(2)));
390
563
  i++;
391
564
  continue;
392
565
  }
393
- // aria2c attached -dDIR (#3213)
394
- if (bin === "aria2c" && n.startsWith("-d") && n.length > 2) {
566
+ // cpio attached -DDIR (#3288; capital D required)
567
+ if (bin === "cpio" && raw.startsWith("-D") && raw.length > 2) {
395
568
  dests.push(pathishToken(raw.slice(2)));
396
569
  i++;
397
570
  continue;
398
571
  }
399
572
  }
400
- // Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P / -d
401
- if (isDownloaderDestFlag(n, bin)) {
573
+ // Separate value: -o PATH / --output PATH / -out PATH / --output-dir / -P / -d / -C DIR
574
+ if (isDownloaderDestFlag(n, bin, raw)) {
402
575
  const next = tokens[i + 1];
403
576
  if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
404
577
  dests.push(pathishToken(next));
@@ -418,16 +591,32 @@ function downloaderDecoderDestinations(tokens) {
418
591
  dests.push(p);
419
592
  }
420
593
  }
421
- // scp / certutil: pathish operands (quote-aware glued-op cut).
422
- // certutil under UAT: any `.deft/authz` / kill-switch pathish is fail-closed settings
423
- // (read vs write thrash deferred — prefer deny over dest-parser perfection; #3213).
424
- if ((bin === "scp" || bin === "certutil") && !n.startsWith("-")) {
594
+ // socat write address forms: OPEN:path, CREATE:path, APPEND:path (#3245).
595
+ if (bin === "socat" && !n.startsWith("-")) {
596
+ const p = pathishToken(raw);
597
+ for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
598
+ if (p.startsWith(prefix) && p.length > prefix.length) {
599
+ const dest = p.slice(prefix.length);
600
+ dests.push(dest);
601
+ if (dest.includes(".deft/authz") ||
602
+ dest.includes(".deft-directive-disable") ||
603
+ dest.includes(".no-deft-directive")) {
604
+ protectedPathish.push(dest);
605
+ }
606
+ break;
607
+ }
608
+ }
609
+ }
610
+ // scp / certutil / rclone / archive extractors: pathish operands (quote-aware glued-op cut).
611
+ // Under UAT: any `.deft/authz` / kill-switch pathish is fail-closed settings
612
+ // (read vs write thrash deferred — prefer deny over dest-parser perfection; #3213 / #3245).
613
+ if (collectsProtectedPositionals && !n.startsWith("-")) {
425
614
  const cut = firstUnquotedShellOpIndex(raw);
426
615
  const cleaned = cut >= 0 ? raw.slice(0, cut) : raw;
427
616
  const p = pathishToken(cleaned);
428
617
  if (p.length > 0) {
429
618
  lastPositionalPath = p;
430
- // Fail-closed: protected store/kill basenames anywhere in scp/certutil pathish.
619
+ // Fail-closed: protected store/kill basenames anywhere in pathish.
431
620
  if (p.includes(".deft/authz") ||
432
621
  p.includes(".deft-directive-disable") ||
433
622
  p.includes(".no-deft-directive")) {
@@ -589,10 +778,12 @@ function hasKillSwitchShellWrite(command, tokens) {
589
778
  if (pathishMentionsKillSwitch(dest))
590
779
  return true;
591
780
  }
592
- // Write/destructive + symlink plant bins with a kill-switch path argument (#3213: ln/link/mklink).
781
+ // Write/destructive + symlink plant + archive/alt-write bins with a kill-switch path
782
+ // argument (#3213 ln/link/mklink; #3245 tar/axel/rclone/socat residual).
593
783
  const killWriteBins = new Set([
594
784
  ...INDIRECT_WRITE_BINS,
595
785
  ...SYMLINK_PLANT_BINS,
786
+ ...ARCHIVE_ALT_WRITE_BINS,
596
787
  "touch",
597
788
  "new-item",
598
789
  "ni",
@@ -606,8 +797,15 @@ function hasKillSwitchShellWrite(command, tokens) {
606
797
  if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
607
798
  continue;
608
799
  for (let tj = ti + 1; tj < tokens.length; tj++) {
609
- if (pathishMentionsKillSwitch(pathishToken(tokens[tj])))
800
+ const p = pathishToken(tokens[tj]);
801
+ if (pathishMentionsKillSwitch(p))
610
802
  return true;
803
+ // socat OPEN:.deft-directive-disable — path after address prefix (#3245).
804
+ for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
805
+ if (p.startsWith(prefix) && pathishMentionsKillSwitch(p.slice(prefix.length))) {
806
+ return true;
807
+ }
808
+ }
611
809
  }
612
810
  }
613
811
  // Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` — path later.
@@ -833,24 +1031,32 @@ function hasAuthzDirShellWrite(command, tokens) {
833
1031
  return true;
834
1032
  }
835
1033
  }
836
- // Write/destructive + symlink plant bins with an authz path argument (pathish =
837
- // quote-strip resistant). Always run — do not gate on contiguous `.deft/authz`
838
- // in the raw command (#3213). SYMLINK_PLANT_BINS: `ln -s forged .deft/authz/grants/x`
839
- // must not fail-open as unclassifiable (SLizard residual).
1034
+ // Write/destructive + symlink plant + archive/alt-write bins with an authz path argument
1035
+ // (pathish = quote-strip resistant). Always run — do not gate on contiguous `.deft/authz`
1036
+ // in the raw command (#3213 / #3245). SYMLINK_PLANT_BINS: `ln -s forged .deft/authz/grants/x`
1037
+ // must not fail-open as unclassifiable (SLizard residual). ARCHIVE_ALT_WRITE_BINS: tar/rclone
1038
+ // residual pathish without dest-flag perfection thrash.
840
1039
  for (let ti = 0; ti < tokens.length; ti++) {
841
1040
  const bare = binBareName(tokens[ti]);
842
1041
  const n = normalizeToken(tokens[ti]);
843
1042
  if (!INDIRECT_WRITE_BINS.has(n) &&
844
1043
  !INDIRECT_WRITE_BINS.has(bare) &&
845
- !SYMLINK_PLANT_BINS.has(bare)) {
1044
+ !SYMLINK_PLANT_BINS.has(bare) &&
1045
+ !ARCHIVE_ALT_WRITE_BINS.has(bare)) {
846
1046
  continue;
847
1047
  }
848
1048
  for (let tj = ti + 1; tj < tokens.length; tj++) {
849
- if (pathishIsAuthzDir(pathishToken(tokens[tj])))
1049
+ const p = pathishToken(tokens[tj]);
1050
+ if (pathishIsAuthzDir(p))
850
1051
  return true;
1052
+ for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
1053
+ if (p.startsWith(prefix) && pathishIsAuthzDir(p.slice(prefix.length))) {
1054
+ return true;
1055
+ }
1056
+ }
851
1057
  }
852
1058
  }
853
- // Downloader/decoder destinations under .deft/authz (#3206 / #3213 scp/aria2c/certutil).
1059
+ // Downloader/decoder destinations under .deft/authz (#3206 / #3213 / #3245 archive+alt).
854
1060
  for (const dest of downloaderDecoderDestinations(tokens)) {
855
1061
  if (pathishIsAuthzDir(dest))
856
1062
  return true;
@@ -932,7 +1138,12 @@ function hasWriteShape(command, tokens) {
932
1138
  if (command.includes(">"))
933
1139
  return true;
934
1140
  for (const t of tokens) {
935
- if (INDIRECT_WRITE_BINS.has(normalizeToken(t)))
1141
+ const bare = binBareName(t);
1142
+ if (INDIRECT_WRITE_BINS.has(normalizeToken(t)) || INDIRECT_WRITE_BINS.has(bare))
1143
+ return true;
1144
+ // #3245: archive extractors / alt downloaders are write-shaped (defense-in-depth for
1145
+ // unclassifiable plant paths under UAT — not bare curl $URL which stays non-write-shaped).
1146
+ if (ARCHIVE_ALT_WRITE_BINS.has(bare))
936
1147
  return true;
937
1148
  }
938
1149
  return false;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Human-origin structural apply gate for scope:decompose (#3239 / epic #3237 Q2).
3
+ *
4
+ * Apply requires a human-origin grant bound to:
5
+ * project root identity (worktree and/or repo), parent path, draft/target path,
6
+ * SHA-256 of exact draft bytes, and op scope.decompose.apply.structural.
7
+ *
8
+ * --check stays ungated (caller responsibility). Missing / expired / mismatched /
9
+ * agent-origin grants fail closed before any child story writes.
10
+ */
11
+ import { type AuthzDecisionCode, type HumanOriginGrant } from "./types.js";
12
+ export type DecomposeApplyDecisionCode = AuthzDecisionCode | "authz-grant-digest-mismatch" | "authz-grant-parent-mismatch" | "authz-grant-target-mismatch" | "authz-grant-project-mismatch" | "authz-grant-binding-incomplete";
13
+ export interface DecomposeApplyDecision {
14
+ readonly allowed: boolean;
15
+ readonly code: DecomposeApplyDecisionCode;
16
+ readonly reason: string;
17
+ readonly humanApprovalRef: string | null;
18
+ readonly draftDigest: string;
19
+ }
20
+ export interface EvaluateDecomposeStructuralApplyInput {
21
+ readonly projectRoot: string;
22
+ /** Absolute or project-relative parent artifact path. */
23
+ readonly parentPath: string;
24
+ /** Absolute or project-relative draft path whose bytes were digested. */
25
+ readonly draftPath: string;
26
+ /** SHA-256 hex of the exact draft file bytes about to be applied. */
27
+ readonly draftDigest: string;
28
+ readonly grants?: readonly HumanOriginGrant[];
29
+ /** Optional GitHub-style repo identity (owner/name); compared when grant pins repo. */
30
+ readonly repo?: string | null;
31
+ readonly now?: Date;
32
+ }
33
+ /** SHA-256 hex of a buffer or utf8 string. */
34
+ export declare function sha256Hex(data: Buffer | string): string;
35
+ /** SHA-256 hex of file bytes at path (exact on-disk digest). */
36
+ export declare function sha256FileHex(path: string): string;
37
+ /**
38
+ * Exact operator CLI mint command for a structural decompose apply grant (#3291).
39
+ * Paths are project-relative POSIX when inside projectRoot; otherwise as provided.
40
+ * Parent / draft / repo tokens are shell-quoted when they contain whitespace or
41
+ * shell metacharacters. When POSIX and PowerShell apostrophe escapes differ,
42
+ * both forms are emitted on separate labeled lines (`bash:` / `pwsh:`) so each
43
+ * is independently copy-pasteable. Paths containing newline or NUL are refused
44
+ * (no rewritten fake path) so the operator renames before minting. Does not
45
+ * include agent/CI/TTY gates — those remain on the CLI multi-factor path.
46
+ */
47
+ export declare function formatDecomposeStructuralMintCommand(parentPath: string, draftPath: string, options?: {
48
+ readonly projectRoot?: string;
49
+ readonly repo?: string | null;
50
+ }): string;
51
+ /**
52
+ * Normalize a path to project-relative POSIX form for grant binding compare.
53
+ * Returns null when the path escapes the project root.
54
+ */
55
+ export declare function toProjectRelativePosix(projectRoot: string, path: string): string | null;
56
+ /**
57
+ * Evaluate whether a human-origin grant covers structural decompose apply for
58
+ * the exact draft digest + parent + target + project identity.
59
+ */
60
+ export declare function evaluateDecomposeStructuralApply(input: EvaluateDecomposeStructuralApplyInput): DecomposeApplyDecision;
61
+ export interface MintDecomposeStructuralApplyGrantInput {
62
+ readonly projectRoot: string;
63
+ readonly parentPath: string;
64
+ readonly draftPath: string;
65
+ /** When omitted, digest is computed from draftPath on-disk bytes. */
66
+ readonly contentDigest?: string;
67
+ readonly actor?: string;
68
+ readonly repo?: string | null;
69
+ readonly expiresAt?: string | null;
70
+ readonly singleUse?: boolean;
71
+ readonly grantId?: string;
72
+ readonly now?: Date;
73
+ readonly pinActive?: boolean;
74
+ readonly eventRef?: string | null;
75
+ }
76
+ /**
77
+ * Operator mint helper: human-origin grant bound to exact draft bytes + paths.
78
+ * Sole mint path remains mintHumanOriginGrant (operator-cli origin).
79
+ */
80
+ export declare function mintDecomposeStructuralApplyGrant(input: MintDecomposeStructuralApplyGrantInput): HumanOriginGrant;
81
+ //# sourceMappingURL=decompose-apply.d.ts.map