@deftai/directive-core 0.103.0 → 0.105.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 (255) hide show
  1. package/dist/authz/classify.js +682 -35
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/gate-lists.d.ts +3 -2
  5. package/dist/check/gate-lists.js +5 -2
  6. package/dist/check/index.d.ts +1 -0
  7. package/dist/check/index.js +1 -0
  8. package/dist/check/named-cause.d.ts +1 -1
  9. package/dist/check/named-cause.js +33 -3
  10. package/dist/check/session-completed-ac.d.ts +39 -0
  11. package/dist/check/session-completed-ac.js +198 -0
  12. package/dist/codebase/default-extractor.d.ts +5 -0
  13. package/dist/codebase/default-extractor.js +7 -15
  14. package/dist/deposit/copy-tree.js +60 -0
  15. package/dist/deposit/python-free.js +26 -18
  16. package/dist/doctor/checks.d.ts +8 -0
  17. package/dist/doctor/checks.js +37 -25
  18. package/dist/doctor/doctor-state.d.ts +4 -0
  19. package/dist/doctor/doctor-state.js +7 -1
  20. package/dist/doctor/main.js +9 -14
  21. package/dist/doctor/types.d.ts +9 -0
  22. package/dist/fs/contained-write.d.ts +115 -1
  23. package/dist/fs/contained-write.js +242 -5
  24. package/dist/fs/mutation-ledger.d.ts +54 -0
  25. package/dist/fs/mutation-ledger.js +133 -0
  26. package/dist/fs/non-product-dirs.d.ts +79 -0
  27. package/dist/fs/non-product-dirs.js +96 -0
  28. package/dist/hooks/dispatcher.d.ts +3 -1
  29. package/dist/hooks/dispatcher.js +14 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +3 -0
  32. package/dist/init-deposit/agent-hooks.js +17 -15
  33. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  34. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  35. package/dist/init-deposit/gitignore.js +9 -0
  36. package/dist/init-deposit/hygiene.d.ts +47 -11
  37. package/dist/init-deposit/hygiene.js +172 -37
  38. package/dist/init-deposit/legacy-detect.js +6 -2
  39. package/dist/init-deposit/refresh.d.ts +21 -1
  40. package/dist/init-deposit/refresh.js +106 -9
  41. package/dist/init-deposit/scaffold.js +28 -279
  42. package/dist/init-deposit/slash-deposit.js +13 -7
  43. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  44. package/dist/init-deposit/xbrief-projections.js +24 -11
  45. package/dist/intake/clause-derivation.d.ts +74 -0
  46. package/dist/intake/clause-derivation.js +409 -0
  47. package/dist/intake/index.d.ts +1 -0
  48. package/dist/intake/index.js +1 -0
  49. package/dist/intake/issue-ingest.d.ts +3 -1
  50. package/dist/intake/issue-ingest.js +36 -43
  51. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  52. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  53. package/dist/literal-acceptance/capture.d.ts +10 -0
  54. package/dist/literal-acceptance/capture.js +224 -75
  55. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  56. package/dist/literal-acceptance/evaluate.js +70 -2
  57. package/dist/literal-acceptance/index.d.ts +3 -3
  58. package/dist/literal-acceptance/index.js +3 -3
  59. package/dist/literal-acceptance/safety.d.ts +41 -0
  60. package/dist/literal-acceptance/safety.js +126 -2
  61. package/dist/literal-acceptance/types.d.ts +8 -0
  62. package/dist/orphan-active/evaluate.d.ts +7 -1
  63. package/dist/orphan-active/evaluate.js +102 -23
  64. package/dist/platform/platform-capabilities.js +2 -3
  65. package/dist/policy/base-branch.d.ts +37 -0
  66. package/dist/policy/base-branch.js +114 -0
  67. package/dist/policy/branch-sync.d.ts +80 -0
  68. package/dist/policy/branch-sync.js +211 -0
  69. package/dist/policy/ceremony-dial-escalation.js +1 -0
  70. package/dist/policy/ceremony-dial.js +4 -14
  71. package/dist/policy/delivery-branch.d.ts +5 -3
  72. package/dist/policy/delivery-branch.js +7 -3
  73. package/dist/policy/file-size-thresholds.d.ts +24 -0
  74. package/dist/policy/file-size-thresholds.js +23 -0
  75. package/dist/policy/forge-outage-retry.d.ts +47 -0
  76. package/dist/policy/forge-outage-retry.js +157 -0
  77. package/dist/policy/index.d.ts +7 -0
  78. package/dist/policy/index.js +66 -7
  79. package/dist/policy/project-invariants.d.ts +50 -0
  80. package/dist/policy/project-invariants.js +205 -0
  81. package/dist/policy/sync-default.d.ts +129 -0
  82. package/dist/policy/sync-default.js +452 -0
  83. package/dist/policy/sync-max-files.d.ts +93 -0
  84. package/dist/policy/sync-max-files.js +192 -0
  85. package/dist/pr-monitor/absent-required.d.ts +11 -0
  86. package/dist/pr-monitor/absent-required.js +61 -0
  87. package/dist/pr-monitor/constants.d.ts +7 -0
  88. package/dist/pr-monitor/constants.js +7 -0
  89. package/dist/pr-monitor/index.d.ts +2 -1
  90. package/dist/pr-monitor/index.js +2 -1
  91. package/dist/pr-monitor/main.js +4 -0
  92. package/dist/pr-monitor/monitor.js +24 -1
  93. package/dist/pr-wait-mergeable/classify.js +6 -0
  94. package/dist/preflight/evaluate.d.ts +9 -0
  95. package/dist/preflight/evaluate.js +49 -1
  96. package/dist/preflight/index.d.ts +5 -1
  97. package/dist/preflight/index.js +2 -0
  98. package/dist/preflight/intended-placement.d.ts +62 -0
  99. package/dist/preflight/intended-placement.js +275 -0
  100. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  101. package/dist/preflight/project-invariants-gate.js +82 -0
  102. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  103. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  104. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  105. package/dist/product-first-done-gate/acceptance.js +79 -8
  106. package/dist/product-first-done-gate/empty-resolution.d.ts +5 -2
  107. package/dist/product-first-done-gate/empty-resolution.js +13 -2
  108. package/dist/product-first-done-gate/evaluate.d.ts +29 -0
  109. package/dist/product-first-done-gate/evaluate.js +307 -33
  110. package/dist/product-first-done-gate/index.d.ts +3 -1
  111. package/dist/product-first-done-gate/index.js +2 -1
  112. package/dist/release/build-dist.d.ts +101 -0
  113. package/dist/release/build-dist.js +428 -46
  114. package/dist/run-summary/emit.d.ts +50 -4
  115. package/dist/run-summary/emit.js +262 -26
  116. package/dist/run-summary/index.d.ts +3 -2
  117. package/dist/run-summary/index.js +2 -1
  118. package/dist/run-summary/session-id.d.ts +21 -0
  119. package/dist/run-summary/session-id.js +92 -0
  120. package/dist/run-summary/share.d.ts +1 -1
  121. package/dist/run-summary/share.js +2 -2
  122. package/dist/run-summary/types.d.ts +38 -6
  123. package/dist/run-summary/types.js +1 -0
  124. package/dist/scope/acceptance-activate-gate.d.ts +1 -1
  125. package/dist/scope/acceptance-activate-gate.js +4 -4
  126. package/dist/scope/acceptance-evidence.d.ts +27 -0
  127. package/dist/scope/acceptance-evidence.js +66 -0
  128. package/dist/scope/capacity-stamp.d.ts +2 -0
  129. package/dist/scope/capacity-stamp.js +4 -0
  130. package/dist/scope/delivery-evidence.d.ts +9 -3
  131. package/dist/scope/delivery-evidence.js +39 -27
  132. package/dist/scope/index.d.ts +1 -0
  133. package/dist/scope/index.js +1 -0
  134. package/dist/scope/parent-lineage.js +7 -6
  135. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  136. package/dist/scope/project-invariant-coverage.js +322 -0
  137. package/dist/scope/transition.js +66 -4
  138. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  139. package/dist/scope-provenance/compare-intent.js +233 -0
  140. package/dist/scope-provenance/digest.d.ts +18 -1
  141. package/dist/scope-provenance/digest.js +16 -11
  142. package/dist/scope-provenance/evaluate.d.ts +5 -1
  143. package/dist/scope-provenance/evaluate.js +64 -4
  144. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  145. package/dist/scope-provenance/extract-intent.js +262 -0
  146. package/dist/scope-provenance/index.d.ts +7 -1
  147. package/dist/scope-provenance/index.js +7 -1
  148. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  149. package/dist/scope-provenance/intent-digest.js +33 -0
  150. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  151. package/dist/scope-provenance/intent-evaluate.js +149 -0
  152. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  153. package/dist/scope-provenance/json-tokenizer.js +225 -0
  154. package/dist/scope-provenance/known-machine.d.ts +38 -0
  155. package/dist/scope-provenance/known-machine.js +157 -0
  156. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  157. package/dist/scope-provenance/mint-artifacts.js +425 -0
  158. package/dist/session/ac-pass-banking.d.ts +4 -36
  159. package/dist/session/ac-pass-banking.js +30 -77
  160. package/dist/session/ac-pass-reuse.d.ts +53 -0
  161. package/dist/session/ac-pass-reuse.js +95 -0
  162. package/dist/session/ceremony-dial-evidence.d.ts +42 -0
  163. package/dist/session/ceremony-dial-evidence.js +194 -0
  164. package/dist/session/git.js +19 -4
  165. package/dist/session/index.d.ts +5 -0
  166. package/dist/session/index.js +5 -0
  167. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  168. package/dist/session/occupancy-stress-worker.js +36 -0
  169. package/dist/session/occupancy.d.ts +97 -0
  170. package/dist/session/occupancy.js +422 -0
  171. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  172. package/dist/session/product-state-hash.d.ts +26 -0
  173. package/dist/session/product-state-hash.js +683 -0
  174. package/dist/session/session-ready.d.ts +2 -0
  175. package/dist/session/session-ready.js +52 -30
  176. package/dist/session/session-start.d.ts +7 -0
  177. package/dist/session/session-start.js +88 -5
  178. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  179. package/dist/session/verify-ac-session-cache.js +126 -0
  180. package/dist/session/verify-session-ritual.d.ts +5 -0
  181. package/dist/session/verify-session-ritual.js +9 -0
  182. package/dist/slash/openclaw-deposit.js +13 -9
  183. package/dist/slice/constants.d.ts +2 -2
  184. package/dist/slice/constants.js +2 -2
  185. package/dist/slice/existing.js +21 -4
  186. package/dist/slice/lock.d.ts +16 -1
  187. package/dist/slice/lock.js +137 -11
  188. package/dist/slice/record.d.ts +8 -5
  189. package/dist/slice/record.js +21 -13
  190. package/dist/story-ready/evaluate.d.ts +2 -0
  191. package/dist/story-ready/evaluate.js +13 -0
  192. package/dist/swarm/complete-cohort.d.ts +3 -0
  193. package/dist/swarm/complete-cohort.js +59 -11
  194. package/dist/swarm/finalize-cohort.js +33 -2
  195. package/dist/swarm/launch.d.ts +22 -0
  196. package/dist/swarm/launch.js +155 -0
  197. package/dist/swarm/routing-set-cli.d.ts +11 -1
  198. package/dist/swarm/routing-set-cli.js +13 -3
  199. package/dist/swarm/routing-verify.d.ts +12 -0
  200. package/dist/swarm/routing-verify.js +59 -20
  201. package/dist/swarm/routing.d.ts +14 -0
  202. package/dist/swarm/routing.js +53 -3
  203. package/dist/telemetry-coverage/evaluate.d.ts +42 -0
  204. package/dist/telemetry-coverage/evaluate.js +148 -0
  205. package/dist/telemetry-coverage/fake-trial.d.ts +45 -0
  206. package/dist/telemetry-coverage/fake-trial.js +204 -0
  207. package/dist/telemetry-coverage/index.d.ts +5 -0
  208. package/dist/telemetry-coverage/index.js +5 -0
  209. package/dist/telemetry-coverage/kinds.d.ts +19 -0
  210. package/dist/telemetry-coverage/kinds.js +54 -0
  211. package/dist/telemetry-coverage/scan-callers.d.ts +25 -0
  212. package/dist/telemetry-coverage/scan-callers.js +168 -0
  213. package/dist/triage/actions/index.js +4 -1
  214. package/dist/ts-check-lane/index.d.ts +3 -0
  215. package/dist/ts-check-lane/index.js +2 -0
  216. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  217. package/dist/ts-check-lane/progress-reporter.js +38 -0
  218. package/dist/ts-check-lane/progress.d.ts +32 -0
  219. package/dist/ts-check-lane/progress.js +57 -0
  220. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  221. package/dist/ts-check-lane/run-lane.js +8 -6
  222. package/dist/validate-content/validate-links.d.ts +8 -0
  223. package/dist/validate-content/validate-links.js +9 -15
  224. package/dist/value/feedback-file.js +12 -2
  225. package/dist/vbrief-build/index.d.ts +1 -0
  226. package/dist/vbrief-build/index.js +1 -0
  227. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  228. package/dist/vbrief-build/project-invariants-io.js +41 -0
  229. package/dist/vbrief-reconcile/graph.js +18 -1
  230. package/dist/vbrief-reconcile/index.d.ts +3 -1
  231. package/dist/vbrief-reconcile/index.js +2 -1
  232. package/dist/vbrief-reconcile/origin-freshness.d.ts +44 -0
  233. package/dist/vbrief-reconcile/origin-freshness.js +221 -0
  234. package/dist/vbrief-reconcile/types.d.ts +13 -0
  235. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  236. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  237. package/dist/vbrief-validate/index.d.ts +1 -1
  238. package/dist/vbrief-validate/index.js +1 -1
  239. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  240. package/dist/vbrief-validate/plan-hooks.js +68 -0
  241. package/dist/verify-ac/evaluate.d.ts +7 -1
  242. package/dist/verify-ac/evaluate.js +34 -26
  243. package/dist/verify-ac/flag.d.ts +4 -0
  244. package/dist/verify-ac/flag.js +16 -0
  245. package/dist/verify-ac/index.d.ts +2 -2
  246. package/dist/verify-ac/index.js +2 -2
  247. package/dist/verify-source/contained-writes.js +2 -0
  248. package/dist/verify-source/index.d.ts +1 -0
  249. package/dist/verify-source/index.js +1 -0
  250. package/dist/verify-source/verify-stubs.d.ts +15 -1
  251. package/dist/verify-source/verify-stubs.js +20 -16
  252. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  253. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  254. package/dist/xbrief-migrate/migrate-project.js +2 -5
  255. package/package.json +3 -3
@@ -245,10 +245,67 @@ const DOWNLOADER_DECODER_BINS = new Set([
245
245
  "dpkg-deb",
246
246
  "hg",
247
247
  "msguniq",
248
+ // #3354 residual after #3336: nc/7zz/msgfmt peers + plant writers.
249
+ "nc",
250
+ "netcat",
251
+ "7zz",
252
+ "msgfmt",
253
+ "msgcat",
254
+ "lz4",
255
+ "lzop",
256
+ "unrar",
257
+ "rar",
258
+ "aunpack",
259
+ "atool",
260
+ "ftpget",
261
+ "tftp",
262
+ "sqlite3",
263
+ "crane",
264
+ "objcopy",
265
+ // #3382 residual after #3354: dest forms that are not generic -o/--output/--outfile.
266
+ "cmake",
267
+ "script",
268
+ "gallery-dl",
269
+ "megadl",
270
+ "ncftpget",
271
+ "svn",
272
+ "fossil",
273
+ "bzr",
274
+ "cvs",
275
+ "darcs",
276
+ "ed",
277
+ "nvim",
278
+ "nano",
279
+ "vim",
280
+ "vi",
281
+ // #3421 residual after #3382: dest forms the harvest missed.
282
+ "git",
283
+ "ex",
284
+ "dos2unix",
285
+ "unix2dos",
286
+ "mac2unix",
287
+ "unix2mac",
288
+ "aws",
289
+ "pijul",
290
+ "pg_dump",
291
+ "pg_restore",
292
+ "pg_dumpall",
293
+ "convert",
294
+ "magick",
295
+ "mogrify",
296
+ "fallocate",
297
+ "new-item",
298
+ // #3459 residual after #3421: GNU g* peers, forge clone, iwr, fsutil.
299
+ "iwr",
300
+ "invoke-webrequest",
301
+ "fsutil",
302
+ "gh",
303
+ "glab",
304
+ "hub",
248
305
  ]);
249
306
  /**
250
307
  * Archive extractors / alt writers that can plant via pathish operands without
251
- * shell redirects (#3245 / #3288 / #3311 / #3336). Used for pathish authz/kill scans (prefer deny)
308
+ * shell redirects (#3245 / #3288 / #3311 / #3336 / #3354 / #3382). Used for pathish authz/kill scans (prefer deny)
252
309
  * and write-shape residual under UAT — not bare curl-class URL fetches.
253
310
  */
254
311
  const ARCHIVE_ALT_WRITE_BINS = new Set([
@@ -304,9 +361,47 @@ const ARCHIVE_ALT_WRITE_BINS = new Set([
304
361
  "dpkg-deb",
305
362
  "hg",
306
363
  "msguniq",
364
+ // #3354 residual after #3336.
365
+ "nc",
366
+ "netcat",
367
+ "7zz",
368
+ "msgfmt",
369
+ "msgcat",
370
+ "lz4",
371
+ "lzop",
372
+ "unrar",
373
+ "rar",
374
+ "aunpack",
375
+ "atool",
376
+ "ftpget",
377
+ "tftp",
378
+ "sqlite3",
379
+ "crane",
380
+ "objcopy",
381
+ // #3382 residual: dedicated dest-form writers (not general-purpose cmake/git/editors).
382
+ "gallery-dl",
383
+ "megadl",
384
+ "ncftpget",
385
+ // #3421 residual: dedicated dest-form writers (not general-purpose git/aws/editors).
386
+ "dos2unix",
387
+ "unix2dos",
388
+ "mac2unix",
389
+ "unix2mac",
390
+ "pijul",
391
+ "pg_dump",
392
+ "pg_restore",
393
+ "pg_dumpall",
394
+ "convert",
395
+ "magick",
396
+ "mogrify",
397
+ "fallocate",
398
+ // #3459 residual: dest writers that are not general-purpose gh/cmd.
399
+ "iwr",
400
+ "invoke-webrequest",
401
+ "fsutil",
307
402
  ]);
308
403
  /**
309
- * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336).
404
+ * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354 / #3382).
310
405
  * Prefer a Set over a long `||` chain so coverage counts one membership check, not N branches.
311
406
  */
312
407
  const PROTECTED_POSITIONAL_BINS = new Set([
@@ -352,17 +447,215 @@ const PROTECTED_POSITIONAL_BINS = new Set([
352
447
  "dpkg-deb",
353
448
  "hg",
354
449
  "msguniq",
450
+ // #3354 residual: positional dest writers (nc/7zz/unrar/ftpget/crane/objcopy/sqlite3).
451
+ "nc",
452
+ "netcat",
453
+ "7zz",
454
+ "msgfmt",
455
+ "msgcat",
456
+ "lz4",
457
+ "lzop",
458
+ "unrar",
459
+ "rar",
460
+ "aunpack",
461
+ "atool",
462
+ "ftpget",
463
+ "tftp",
464
+ "sqlite3",
465
+ "crane",
466
+ "objcopy",
467
+ // #3382 residual: positional dest writers (cmake copy / script / VCS / editors).
468
+ "cmake",
469
+ "script",
470
+ "gallery-dl",
471
+ "megadl",
472
+ "ncftpget",
473
+ "svn",
474
+ "fossil",
475
+ "bzr",
476
+ "cvs",
477
+ "darcs",
478
+ "ed",
479
+ "nvim",
480
+ "nano",
481
+ "vim",
482
+ "vi",
483
+ // #3421 residual: positional dest writers (aws, convert, New-Item).
484
+ // git clone/worktree/submodule dests are harvested only on those write subcommands.
485
+ "ex",
486
+ "dos2unix",
487
+ "unix2dos",
488
+ "mac2unix",
489
+ "unix2mac",
490
+ "aws",
491
+ "pijul",
492
+ "pg_dump",
493
+ "pg_restore",
494
+ "pg_dumpall",
495
+ "convert",
496
+ "magick",
497
+ "mogrify",
498
+ "fallocate",
499
+ "new-item",
500
+ // #3459 residual: positional dest writers (fsutil createnew, iwr dest flags).
501
+ // gh/glab/hub clone dests are harvested only on the clone subcommand.
502
+ "iwr",
503
+ "invoke-webrequest",
504
+ "fsutil",
355
505
  ]);
506
+ /**
507
+ * Bins whose dest is the last pathish operand (src … dest). A protected
508
+ * source that writes elsewhere is not a dest plant (#3421 residual).
509
+ */
510
+ const LAST_POSITIONAL_DEST_BINS = new Set(["aws", "convert", "magick"]);
511
+ /** Wrappers / assignments before the dest-owning bin (`env aws`, `FOO=1 convert`). */
512
+ const COMMAND_WRAPPER_BINS = new Set([
513
+ "env",
514
+ "command",
515
+ "nice",
516
+ "nohup",
517
+ "sudo",
518
+ "doas",
519
+ "time",
520
+ "stdbuf",
521
+ "timeout",
522
+ "ionice",
523
+ ]);
524
+ function isWrapperOrAssignmentToken(raw, n) {
525
+ if (COMMAND_WRAPPER_BINS.has(writeBinName(raw)))
526
+ return true;
527
+ return n.includes("=") && !n.startsWith("-") && !raw.includes("/") && !raw.includes("\\");
528
+ }
529
+ /** Wrapper flags that take a separate value (`env -C DIR`, `nice -n N`). */
530
+ const WRAPPER_VALUE_FLAGS = new Set([
531
+ "-c",
532
+ "--chdir",
533
+ "-u",
534
+ "--unset",
535
+ "-n",
536
+ "--adjustment",
537
+ "-o",
538
+ "--output",
539
+ "-i",
540
+ "--input",
541
+ "-e",
542
+ "--error",
543
+ "-s",
544
+ "--signal",
545
+ "-k",
546
+ "--kill-after",
547
+ ]);
548
+ /** In-place ImageMagick writer: every protected operand is a dest, not last-only. */
549
+ const MAGICK_FAMILY_BINS = new Set(["convert", "magick", "mogrify"]);
550
+ const MAGICK_WRITE_DEST_FLAGS = new Set(["-write"]);
551
+ /** True when this token is a subcommand of a last-positional dest bin (`aws s3 cp`). */
552
+ function tokenEndsShellSegment(token) {
553
+ return isShellSegmentBreak(token) || firstUnquotedShellOpIndex(token) >= 0;
554
+ }
555
+ function segmentStartedByLastPositionalDest(tokens, tokenIndex) {
556
+ let start = tokenIndex;
557
+ while (start > 0 && !tokenEndsShellSegment(tokens[start - 1]))
558
+ start--;
559
+ let skipNext = false;
560
+ let afterWrapper = false;
561
+ for (let k = start; k < tokenIndex; k++) {
562
+ const raw = tokens[k];
563
+ if (isShellSegmentBreak(raw))
564
+ continue;
565
+ const n = normalizeToken(raw);
566
+ const bare = binBareName(raw);
567
+ if (skipNext) {
568
+ skipNext = false;
569
+ if (LAST_POSITIONAL_DEST_BINS.has(bare))
570
+ return true;
571
+ continue;
572
+ }
573
+ if (n.startsWith("-")) {
574
+ if (!n.includes("=") && WRAPPER_VALUE_FLAGS.has(n))
575
+ skipNext = true;
576
+ continue;
577
+ }
578
+ if (isWrapperOrAssignmentToken(raw, n)) {
579
+ afterWrapper = COMMAND_WRAPPER_BINS.has(bare);
580
+ continue;
581
+ }
582
+ if (LAST_POSITIONAL_DEST_BINS.has(bare))
583
+ return true;
584
+ // timeout 5 aws / similar: skip one non-bin operand after a wrapper.
585
+ if (afterWrapper) {
586
+ afterWrapper = false;
587
+ continue;
588
+ }
589
+ return false;
590
+ }
591
+ return false;
592
+ }
356
593
  /** wget family (directory-prefix dest flags). */
357
594
  const WGET_FAMILY_BINS = new Set(["wget", "wget2"]);
358
595
  /** aria2 family (dir dest flags). */
359
596
  const ARIA2_FAMILY_BINS = new Set(["aria2c", "aria2"]);
360
- /** 7z family (attached -oDIR only). */
361
- const SEVEN_Z_FAMILY_BINS = new Set(["7z", "7za", "7zr"]);
597
+ /** 7z family (attached -oDIR only). Includes 7zz (#3354). */
598
+ const SEVEN_Z_FAMILY_BINS = new Set(["7z", "7za", "7zr", "7zz"]);
599
+ /** aunpack / atool extract-to dest flags (#3354). */
600
+ const ATOOL_FAMILY_BINS = new Set(["aunpack", "atool"]);
601
+ const ATOOL_DIR_DEST_FLAGS = new Set(["-x", "--extract-to"]);
602
+ /** sqlite3 meta-commands that plant a file dest (#3354). */
603
+ const SQLITE3_OUTPUT_META = [".output", ".once"];
362
604
  /** tar family (chdir -C / --directory). Includes GNU/Schily aliases (#3311). */
363
605
  const TAR_FAMILY_BINS = new Set(["tar", "bsdtar", "gtar", "star", "gnutar"]);
364
606
  /** xh / httpie family (download-dir dest flags) (#3311). */
365
607
  const XH_FAMILY_BINS = new Set(["xh", "httpie"]);
608
+ /** gallery-dl dest-dir flags (#3382). */
609
+ const GALLERY_DL_FAMILY_BINS = new Set(["gallery-dl", "gallery_dl"]);
610
+ const GALLERY_DL_DIR_DEST_FLAGS = new Set(["-d", "--destination", "--dest"]);
611
+ /** megadl dest-path flags (#3382). */
612
+ const MEGADL_FAMILY_BINS = new Set(["megadl"]);
613
+ const MEGADL_PATH_DEST_FLAGS = new Set(["--path"]);
614
+ /**
615
+ * Extra dest flags harvested on unknown write-shaped bins when dest is protected (#3382 / #3421).
616
+ * Not merged into DOWNLOADER_FILE_DEST_FLAGS: curl `-d` is POST data, not a file dest.
617
+ */
618
+ const GENERIC_PROTECTED_EXTRA_DEST_FLAGS = new Set([
619
+ "-d",
620
+ "--dir",
621
+ "--destination",
622
+ "--dest",
623
+ "--path",
624
+ "--directory",
625
+ "--workdir",
626
+ "--file",
627
+ "--separate-git-dir",
628
+ "--pack-destination",
629
+ ]);
630
+ /** Bins whose `--file` / `-f` operand is an input, not a dest plant. */
631
+ const READ_SHAPED_FILE_FLAG_BINS = new Set([
632
+ "grep",
633
+ "egrep",
634
+ "fgrep",
635
+ "rg",
636
+ "ag",
637
+ "ack",
638
+ "get-content",
639
+ "gc",
640
+ "cat",
641
+ "type",
642
+ "less",
643
+ "more",
644
+ "head",
645
+ "tail",
646
+ ]);
647
+ const READ_INPUT_FILE_FLAGS = new Set(["--file", "-f"]);
648
+ /** PowerShell New-Item dest flags (not generic: Get-Content -Path is a read). */
649
+ const NEW_ITEM_PATH_DEST_FLAGS = new Set(["-path", "-literalpath", "--literalpath"]);
650
+ /** git subcommands whose positionals can plant a dest (#3421). */
651
+ const GIT_WRITE_SUBCOMMANDS = new Set(["clone", "worktree", "submodule"]);
652
+ /** Forge CLIs whose `clone` dest is harvested like git clone (#3459). */
653
+ const FORGE_CLONE_BINS = new Set(["gh", "glab", "hub"]);
654
+ /** fossil dest-dir flags (#3382 PATH form + #3421 --workdir=). */
655
+ const FOSSIL_WORKDIR_DEST_FLAGS = new Set(["--workdir"]);
656
+ /** pg_dump family dest-file flags (#3421). */
657
+ const PG_DUMP_FAMILY_BINS = new Set(["pg_dump", "pg_restore", "pg_dumpall"]);
658
+ const PG_DUMP_FILE_DEST_FLAGS = new Set(["-f", "--file"]);
366
659
  /**
367
660
  * File destination flags for downloaders/decoders (#3206).
368
661
  * normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
@@ -377,6 +670,8 @@ const DOWNLOADER_FILE_DEST_FLAGS = new Set([
377
670
  "-out",
378
671
  "--out",
379
672
  "--outfile",
673
+ // PowerShell iwr/Invoke-WebRequest -OutFile (single-dash long form) (#3459).
674
+ "-outfile",
380
675
  // unar single-dash long form; GNU/others use --output-directory.
381
676
  "-output-directory",
382
677
  "--output-directory",
@@ -423,12 +718,43 @@ function binBareName(token) {
423
718
  const base = pathish.includes("/") ? pathish.slice(pathish.lastIndexOf("/") + 1) : pathish;
424
719
  return base.endsWith(".exe") ? base.slice(0, -4) : base;
425
720
  }
721
+ /**
722
+ * GNU coreutils g* prefixes (Homebrew `ginstall`, `gcp`).
723
+ * Stems only — `git` / `gh` / `gpg` do not map to `it` / `h` / `pg` (#3459).
724
+ */
725
+ const GNU_COREUTILS_WRITE_STEMS = new Set([
726
+ "cp",
727
+ "mv",
728
+ "rm",
729
+ "rmdir",
730
+ "unlink",
731
+ "shred",
732
+ "truncate",
733
+ "chmod",
734
+ "chown",
735
+ "dd",
736
+ "tee",
737
+ "install",
738
+ "ln",
739
+ "link",
740
+ "mkdir",
741
+ "touch",
742
+ ]);
743
+ function writeBinName(token) {
744
+ const bare = binBareName(token);
745
+ if (bare.length > 1 && bare.startsWith("g")) {
746
+ const stem = bare.slice(1);
747
+ if (GNU_COREUTILS_WRITE_STEMS.has(stem))
748
+ return stem;
749
+ }
750
+ return bare;
751
+ }
426
752
  /** True when token names a downloader/decoder bin (bare or path-qualified). */
427
753
  function isDownloaderDecoderBin(token) {
428
754
  const n = normalizeToken(token);
429
755
  if (n.startsWith("-"))
430
756
  return false;
431
- return DOWNLOADER_DECODER_BINS.has(binBareName(token));
757
+ return DOWNLOADER_DECODER_BINS.has(writeBinName(token));
432
758
  }
433
759
  /**
434
760
  * True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
@@ -474,6 +800,20 @@ function isDownloaderDestFlag(flag, bin, rawFlag) {
474
800
  return true;
475
801
  if (XH_FAMILY_BINS.has(bin) && XH_DIR_DEST_FLAGS.has(flag))
476
802
  return true;
803
+ if (ATOOL_FAMILY_BINS.has(bin) && ATOOL_DIR_DEST_FLAGS.has(flag))
804
+ return true;
805
+ if (GALLERY_DL_FAMILY_BINS.has(bin) && GALLERY_DL_DIR_DEST_FLAGS.has(flag))
806
+ return true;
807
+ if (MEGADL_FAMILY_BINS.has(bin) && MEGADL_PATH_DEST_FLAGS.has(flag))
808
+ return true;
809
+ if (bin === "fossil" && FOSSIL_WORKDIR_DEST_FLAGS.has(flag))
810
+ return true;
811
+ if (PG_DUMP_FAMILY_BINS.has(bin) && PG_DUMP_FILE_DEST_FLAGS.has(flag))
812
+ return true;
813
+ if (bin === "new-item" && NEW_ITEM_PATH_DEST_FLAGS.has(flag))
814
+ return true;
815
+ if (MAGICK_FAMILY_BINS.has(bin) && MAGICK_WRITE_DEST_FLAGS.has(flag))
816
+ return true;
477
817
  return false;
478
818
  }
479
819
  /**
@@ -541,7 +881,7 @@ function downloaderDecoderDestinations(tokens) {
541
881
  i++;
542
882
  continue;
543
883
  }
544
- const bin = binBareName(tokens[i]);
884
+ const bin = writeBinName(tokens[i]);
545
885
  i++;
546
886
  // xxd reverse mode writes; without -r, path positionals are dump inputs (read).
547
887
  let xxdReverse = false;
@@ -552,7 +892,9 @@ function downloaderDecoderDestinations(tokens) {
552
892
  // Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
553
893
  let lastPositionalPath = null;
554
894
  const protectedPathish = [];
555
- const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin);
895
+ const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin) ||
896
+ (bin === "git" && gitHasWriteSubcommand(tokens, i)) ||
897
+ (FORGE_CLONE_BINS.has(bin) && forgeHasCloneSubcommand(tokens, i));
556
898
  while (i < tokens.length) {
557
899
  const raw = tokens[i];
558
900
  const n = normalizeToken(raw);
@@ -614,7 +956,8 @@ function downloaderDecoderDestinations(tokens) {
614
956
  // Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
615
957
  // 7z requires attached -oDIR (#3245).
616
958
  if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
617
- if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output")) {
959
+ // `-outfile` is PowerShell -OutFile (next token is dest), not attached -outFILE (#3459).
960
+ if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output") && n !== "-outfile") {
618
961
  dests.push(pathishToken(raw.slice(4)));
619
962
  i++;
620
963
  continue;
@@ -640,12 +983,24 @@ function downloaderDecoderDestinations(tokens) {
640
983
  i++;
641
984
  continue;
642
985
  }
986
+ // gallery-dl attached -dDIR (#3382)
987
+ if (GALLERY_DL_FAMILY_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
988
+ dests.push(pathishToken(raw.slice(2)));
989
+ i++;
990
+ continue;
991
+ }
643
992
  // unzip / cabextract attached -dDIR (#3245 / #3336)
644
993
  if (UNZIP_DIR_DEST_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
645
994
  dests.push(pathishToken(raw.slice(2)));
646
995
  i++;
647
996
  continue;
648
997
  }
998
+ // aunpack / atool attached -XDIR (#3354)
999
+ if (ATOOL_FAMILY_BINS.has(bin) && n.startsWith("-x") && n.length > 2) {
1000
+ dests.push(pathishToken(raw.slice(2)));
1001
+ i++;
1002
+ continue;
1003
+ }
649
1004
  // tar attached -CDIR (rare; capital C required)
650
1005
  if (TAR_FAMILY_BINS.has(bin) && raw.startsWith("-C") && raw.length > 2) {
651
1006
  dests.push(pathishToken(raw.slice(2)));
@@ -687,28 +1042,36 @@ function downloaderDecoderDestinations(tokens) {
687
1042
  if (p.startsWith(prefix) && p.length > prefix.length) {
688
1043
  const dest = p.slice(prefix.length);
689
1044
  dests.push(dest);
690
- if (dest.includes(".deft/authz") ||
691
- dest.includes(".deft-directive-disable") ||
692
- dest.includes(".no-deft-directive")) {
1045
+ if (pathishIsProtectedDest(dest)) {
693
1046
  protectedPathish.push(dest);
694
1047
  }
695
1048
  break;
696
1049
  }
697
1050
  }
698
1051
  }
1052
+ // sqlite3 `.output` / `.once` dest (separate token or `.output PATH` in one token) (#3354).
1053
+ if (bin === "sqlite3") {
1054
+ const meta = sqlite3MetaDest(raw, tokens[i + 1]);
1055
+ if (meta !== null) {
1056
+ dests.push(meta.dest);
1057
+ if (pathishIsProtectedDest(meta.dest)) {
1058
+ protectedPathish.push(meta.dest);
1059
+ }
1060
+ i += meta.consumedNext ? 2 : 1;
1061
+ continue;
1062
+ }
1063
+ }
699
1064
  // scp / certutil / rclone / archive extractors: pathish operands (quote-aware glued-op cut).
700
- // Under UAT: any `.deft/authz` / kill-switch pathish is fail-closed settings
701
- // (read vs write thrash deferred — prefer deny over dest-parser perfection; #3213 / #3245).
1065
+ // Under UAT: any `.deft/authz` / approved-scope / kill-switch pathish is
1066
+ // fail-closed settings (prefer deny over dest-parser perfection; #3213 / #3421).
1067
+ // Last-positional dest bins (aws/convert/magick): only the last operand is dest.
702
1068
  if (collectsProtectedPositionals && !n.startsWith("-")) {
703
1069
  const cut = firstUnquotedShellOpIndex(raw);
704
1070
  const cleaned = cut >= 0 ? raw.slice(0, cut) : raw;
705
1071
  const p = pathishToken(cleaned);
706
1072
  if (p.length > 0) {
707
1073
  lastPositionalPath = p;
708
- // Fail-closed: protected store/kill basenames anywhere in pathish.
709
- if (p.includes(".deft/authz") ||
710
- p.includes(".deft-directive-disable") ||
711
- p.includes(".no-deft-directive")) {
1074
+ if (pathishIsProtectedDest(p) && !LAST_POSITIONAL_DEST_BINS.has(bin)) {
712
1075
  protectedPathish.push(p);
713
1076
  }
714
1077
  }
@@ -727,6 +1090,10 @@ function downloaderDecoderDestinations(tokens) {
727
1090
  dests.push(lastPositionalPath);
728
1091
  }
729
1092
  }
1093
+ // #3354: fail-closed dest flags on unknown write-shaped bins (not named-peer only).
1094
+ for (const dest of genericProtectedDests(tokens)) {
1095
+ dests.push(dest);
1096
+ }
730
1097
  return dests;
731
1098
  }
732
1099
  function authzSubcommandFromToken(token) {
@@ -805,9 +1172,193 @@ function hasPolicyAuthorityMutator(tokens) {
805
1172
  return false;
806
1173
  }
807
1174
  /** True when pathish token names a kill-switch basename (quote-strip resistant). */
1175
+ function pathishIsProtectedDest(pathish) {
1176
+ return (pathishIsAuthzDir(pathish) ||
1177
+ pathishIsApprovedScopeDir(pathish) ||
1178
+ pathishMentionsKillSwitch(pathish));
1179
+ }
1180
+ function isGenericProtectedDestFlag(flag) {
1181
+ return DOWNLOADER_FILE_DEST_FLAGS.has(flag) || GENERIC_PROTECTED_EXTRA_DEST_FLAGS.has(flag);
1182
+ }
1183
+ function isReadShapedInputFileFlag(bin, flag) {
1184
+ return READ_SHAPED_FILE_FLAG_BINS.has(bin) && READ_INPUT_FILE_FLAGS.has(flag);
1185
+ }
1186
+ /**
1187
+ * Git globals that do not take a separate value. Unlisted globals consume the
1188
+ * next token so a later dest writer is still seen (#3421 residual).
1189
+ */
1190
+ const GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS = new Set([
1191
+ "--no-pager",
1192
+ "-p",
1193
+ "--paginate",
1194
+ "-P",
1195
+ "--bare",
1196
+ "--help",
1197
+ "-h",
1198
+ "--version",
1199
+ "--literal-pathspecs",
1200
+ "--glob-pathspecs",
1201
+ "--noglob-pathspecs",
1202
+ "--icase-pathspecs",
1203
+ "--no-replace-objects",
1204
+ "--no-optional-locks",
1205
+ "--no-lazy-fetch",
1206
+ "--no-advice",
1207
+ "--quiet",
1208
+ "-q",
1209
+ ]);
1210
+ /**
1211
+ * True only when the git *subcommand* (first non-flag token) is a dest writer.
1212
+ * Later operands named clone/worktree/submodule (e.g. `git log worktree -- …`)
1213
+ * are not write subcommands (#3423 residual).
1214
+ * Boolean globals (`--no-pager`) do not consume the next token. Other globals
1215
+ * consume it even when that token spells a subcommand (#3421 residual).
1216
+ */
1217
+ function gitHasWriteSubcommand(tokens, start) {
1218
+ let i = start;
1219
+ while (i < tokens.length) {
1220
+ const raw = tokens[i];
1221
+ if (isShellSegmentBreak(raw))
1222
+ return false;
1223
+ const n = normalizeToken(raw);
1224
+ if (n.startsWith("-")) {
1225
+ if (!n.includes("=") && !GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS.has(n)) {
1226
+ const next = tokens[i + 1];
1227
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
1228
+ i += 2;
1229
+ continue;
1230
+ }
1231
+ }
1232
+ i++;
1233
+ continue;
1234
+ }
1235
+ return GIT_WRITE_SUBCOMMANDS.has(n);
1236
+ }
1237
+ return false;
1238
+ }
1239
+ /**
1240
+ * True when this forge-CLI segment is a clone dest writer
1241
+ * (`gh repo clone`, `glab repo clone`, `hub clone`) (#3459).
1242
+ * `clone` as a later flag value (`gh pr create --title clone`) is not a dest writer.
1243
+ */
1244
+ function forgeHasCloneSubcommand(tokens, start) {
1245
+ const nonFlags = [];
1246
+ let i = start;
1247
+ while (i < tokens.length) {
1248
+ const raw = tokens[i];
1249
+ if (isShellSegmentBreak(raw))
1250
+ break;
1251
+ const n = normalizeToken(raw);
1252
+ if (n.startsWith("-")) {
1253
+ if (!n.includes("=") && (GH_VALUE_FLAGS.has(raw) || GH_VALUE_FLAGS.has(n))) {
1254
+ const next = tokens[i + 1];
1255
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
1256
+ i += 2;
1257
+ continue;
1258
+ }
1259
+ }
1260
+ i++;
1261
+ continue;
1262
+ }
1263
+ nonFlags.push(n);
1264
+ i++;
1265
+ }
1266
+ if (nonFlags[0] === "clone")
1267
+ return true;
1268
+ return nonFlags[0] === "repo" && nonFlags[1] === "clone";
1269
+ }
1270
+ /**
1271
+ * Fail-closed dest harvest for write-shaped Shell under UAT (#3354 / #3382 / #3421).
1272
+ * Named-bin parsers above are not the only path: any token that looks like
1273
+ * `-o` / `--output` / `--outfile` / `--output-file` / `-d` / `--dir` /
1274
+ * `--destination` / `--path` / `--directory` / `--workdir` / `--file` / `-f`
1275
+ * (or attached `-oDIR` / `-dDIR`) whose dest is authz/kill-switch/approved-scope
1276
+ * is collected even when the bin is unknown.
1277
+ * scp `-o` (OpenSSH option) and cpio `-o` (copy-out) stay excluded.
1278
+ */
1279
+ function genericProtectedDests(tokens) {
1280
+ const dests = [];
1281
+ // Track the current command bin — scp `-o` is OpenSSH option, cpio `-o` is copy-out.
1282
+ // Skipping only when the current token itself is `scp`/`cpio` missed later `-o` values (#3354).
1283
+ let currentBin = "";
1284
+ for (let i = 0; i < tokens.length; i++) {
1285
+ const raw = tokens[i];
1286
+ const n = normalizeToken(raw);
1287
+ if (isShellSegmentBreak(raw)) {
1288
+ currentBin = "";
1289
+ continue;
1290
+ }
1291
+ if (!n.startsWith("-")) {
1292
+ if (isDownloaderDecoderBin(raw)) {
1293
+ currentBin = writeBinName(raw);
1294
+ }
1295
+ else if (currentBin.length === 0 &&
1296
+ !raw.includes("/") &&
1297
+ !raw.includes("\\") &&
1298
+ n.length > 0) {
1299
+ currentBin = writeBinName(raw);
1300
+ }
1301
+ }
1302
+ if (currentBin === "scp" || currentBin === "cpio")
1303
+ continue;
1304
+ if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
1305
+ const eq = raw.indexOf("=");
1306
+ const flag = normalizeToken(raw.slice(0, eq));
1307
+ if (isGenericProtectedDestFlag(flag) && !isReadShapedInputFileFlag(currentBin, flag)) {
1308
+ const dest = pathishToken(raw.slice(eq + 1));
1309
+ if (pathishIsProtectedDest(dest))
1310
+ dests.push(dest);
1311
+ }
1312
+ continue;
1313
+ }
1314
+ if (n.startsWith("-") &&
1315
+ !n.startsWith("--") &&
1316
+ n.startsWith("-o") &&
1317
+ !n.startsWith("-out") &&
1318
+ n.length > 2) {
1319
+ const dest = pathishToken(raw.slice(2));
1320
+ if (pathishIsProtectedDest(dest))
1321
+ dests.push(dest);
1322
+ continue;
1323
+ }
1324
+ if (n.startsWith("-") && !n.startsWith("--") && n.startsWith("-d") && n.length > 2) {
1325
+ const dest = pathishToken(raw.slice(2));
1326
+ if (pathishIsProtectedDest(dest))
1327
+ dests.push(dest);
1328
+ continue;
1329
+ }
1330
+ if (isGenericProtectedDestFlag(n) && !isReadShapedInputFileFlag(currentBin, n)) {
1331
+ const next = tokens[i + 1];
1332
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
1333
+ const dest = pathishToken(next);
1334
+ if (pathishIsProtectedDest(dest))
1335
+ dests.push(dest);
1336
+ }
1337
+ }
1338
+ }
1339
+ return dests;
1340
+ }
1341
+ function sqlite3MetaDest(raw, next) {
1342
+ const p = pathishToken(raw);
1343
+ for (const meta of SQLITE3_OUTPUT_META) {
1344
+ if (p === meta) {
1345
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
1346
+ return { dest: pathishToken(next), consumedNext: true };
1347
+ }
1348
+ return null;
1349
+ }
1350
+ if (p.startsWith(`${meta} `) || p.startsWith(`${meta}\t`)) {
1351
+ const dest = p.slice(meta.length).trim();
1352
+ if (dest.length > 0 && dest !== "|")
1353
+ return { dest, consumedNext: false };
1354
+ }
1355
+ }
1356
+ return null;
1357
+ }
808
1358
  function pathishMentionsKillSwitch(pathish) {
1359
+ const p = canonicalizePathish(pathish);
809
1360
  for (const name of KILL_SWITCH_BASENAMES) {
810
- if (pathish === name || pathish.endsWith(`/${name}`) || pathish.includes(name)) {
1361
+ if (p === name || p.endsWith(`/${name}`) || p.includes(name)) {
811
1362
  return true;
812
1363
  }
813
1364
  }
@@ -882,9 +1433,13 @@ function hasKillSwitchShellWrite(command, tokens) {
882
1433
  ]);
883
1434
  for (let ti = 0; ti < tokens.length; ti++) {
884
1435
  const binTok = normalizeToken(tokens[ti]);
885
- const bare = binBareName(tokens[ti]);
1436
+ const bare = writeBinName(tokens[ti]);
886
1437
  if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
887
1438
  continue;
1439
+ // Last-positional dest bins (and their subcommands): dest harvest owns dest vs source.
1440
+ if (LAST_POSITIONAL_DEST_BINS.has(bare) || segmentStartedByLastPositionalDest(tokens, ti)) {
1441
+ continue;
1442
+ }
888
1443
  for (let tj = ti + 1; tj < tokens.length; tj++) {
889
1444
  const p = pathishToken(tokens[tj]);
890
1445
  if (pathishMentionsKillSwitch(p))
@@ -898,8 +1453,14 @@ function hasKillSwitchShellWrite(command, tokens) {
898
1453
  }
899
1454
  }
900
1455
  // Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` — path later.
901
- for (const t of tokens) {
902
- const p = pathishToken(t);
1456
+ // Skip tokens in a last-positional dest segment (dest harvest owns dest vs source).
1457
+ // Other segments still scan so `aws … ; touch .deft-directive-disable` is settings.
1458
+ for (let ti = 0; ti < tokens.length; ti++) {
1459
+ if (LAST_POSITIONAL_DEST_BINS.has(writeBinName(tokens[ti])) ||
1460
+ segmentStartedByLastPositionalDest(tokens, ti)) {
1461
+ continue;
1462
+ }
1463
+ const p = pathishToken(tokens[ti]);
903
1464
  for (const name of KILL_SWITCH_BASENAMES) {
904
1465
  // Exact basename or ends with /basename
905
1466
  if (p === name || p.endsWith(`/${name}`)) {
@@ -907,8 +1468,8 @@ function hasKillSwitchShellWrite(command, tokens) {
907
1468
  if (hasWriteShape(command, tokens) ||
908
1469
  lower.includes("touch") ||
909
1470
  lower.includes("new-item") ||
910
- SYMLINK_PLANT_BINS.has(binBareName(tokens[0])) ||
911
- tokens.some((tok) => SYMLINK_PLANT_BINS.has(binBareName(tok)))) {
1471
+ SYMLINK_PLANT_BINS.has(writeBinName(tokens[0])) ||
1472
+ tokens.some((tok) => SYMLINK_PLANT_BINS.has(writeBinName(tok)))) {
912
1473
  return true;
913
1474
  }
914
1475
  }
@@ -937,7 +1498,10 @@ function isProgrammaticWriteBinToken(token) {
937
1498
  bare === "perl" ||
938
1499
  bare === "ruby" ||
939
1500
  bare === "pwsh" ||
940
- bare === "powershell") {
1501
+ bare === "powershell" ||
1502
+ bare === "tsx" ||
1503
+ bare === "ts-node" ||
1504
+ bare === "tsnode") {
941
1505
  return true;
942
1506
  }
943
1507
  // Versioned: python3.11, python3.12, node18, …
@@ -1080,17 +1644,89 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
1080
1644
  function pathishToken(token) {
1081
1645
  return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
1082
1646
  }
1647
+ /**
1648
+ * Collapse `//`, `/./`, and `/../` lexically so equivalent dests still match
1649
+ * (`.deft/foo/../approved-scope/x`) without following symlinks or matching
1650
+ * prefix siblings. O(n) — no nested-quantifier regex.
1651
+ */
1652
+ function canonicalizePathish(pathish) {
1653
+ const parts = [];
1654
+ let cur = "";
1655
+ const abs = pathish.startsWith("/");
1656
+ for (let i = 0; i <= pathish.length; i++) {
1657
+ const c = i < pathish.length ? pathish[i] : "/";
1658
+ if (c === "/" || i === pathish.length) {
1659
+ if (cur.length === 0 || cur === ".") {
1660
+ // drop empty and `.`
1661
+ }
1662
+ else if (cur === "..") {
1663
+ if (parts.length > 0 && parts[parts.length - 1] !== "..") {
1664
+ parts.pop();
1665
+ }
1666
+ else if (!abs) {
1667
+ parts.push("..");
1668
+ }
1669
+ }
1670
+ else {
1671
+ parts.push(cur);
1672
+ }
1673
+ cur = "";
1674
+ }
1675
+ else {
1676
+ cur += c;
1677
+ }
1678
+ }
1679
+ const joined = parts.join("/");
1680
+ return abs ? `/${joined}` : joined;
1681
+ }
1083
1682
  /**
1084
1683
  * True when a pathish string targets `.deft/authz` (after quote strip / slash normalize).
1085
1684
  * Quote-split forms like `'.deft/'authz'/grants/x'` become `.deft/authz/grants/x` via pathishToken.
1086
1685
  */
1686
+ /** True when `dir` is a path segment (left-bounded; `foo.deft/authz` is not `.deft/authz`). */
1687
+ function pathishHasDirSegment(pathish, dir) {
1688
+ const p = canonicalizePathish(pathish.trim());
1689
+ if (p === dir || p.startsWith(`${dir}/`) || p.startsWith(`${dir}\\`))
1690
+ return true;
1691
+ for (let i = 0; i < p.length; i++) {
1692
+ const c = p[i];
1693
+ if (c !== "/" && c !== "\\" && c !== ":" && c !== "=")
1694
+ continue;
1695
+ const rest = p.slice(i + 1);
1696
+ if (rest === dir || rest.startsWith(`${dir}/`) || rest.startsWith(`${dir}\\`))
1697
+ return true;
1698
+ }
1699
+ return false;
1700
+ }
1087
1701
  function pathishIsAuthzDir(pathish) {
1088
- return pathish.includes(".deft/authz");
1702
+ return pathishHasDirSegment(pathish, ".deft/authz");
1703
+ }
1704
+ /** True when a pathish string targets `.deft/approved-scope` (#3421 / #3410 mint). */
1705
+ function pathishIsApprovedScopeDir(pathish) {
1706
+ return pathishHasDirSegment(pathish, ".deft/approved-scope");
1707
+ }
1708
+ function pathishIsSettingsStoreDir(pathish) {
1709
+ return pathishIsAuthzDir(pathish) || pathishIsApprovedScopeDir(pathish);
1710
+ }
1711
+ /** True when dest is a settings-store path or a split-path `authz` segment after `.deft`. */
1712
+ function destMentionsAuthzSegment(dest) {
1713
+ const p = canonicalizePathish(pathishToken(dest).trim());
1714
+ if (pathishIsSettingsStoreDir(p))
1715
+ return true;
1716
+ const segs = p.split("/").filter((s) => s.length > 0);
1717
+ for (let i = 0; i < segs.length; i++) {
1718
+ if (segs[i] !== "authz")
1719
+ continue;
1720
+ const prev = i > 0 ? segs[i - 1] : "";
1721
+ if (prev === ".deft" || prev.length === 0)
1722
+ return true;
1723
+ }
1724
+ return false;
1089
1725
  }
1090
1726
  /**
1091
- * Shell **write** targeting `.deft/authz/` (#3110 AC-3 / #3206 / #3213).
1727
+ * Shell **write** targeting `.deft/authz/` or `.deft/approved-scope/` (#3110 / #3421).
1092
1728
  * Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
1093
- * Redirects only count when the destination region contains `.deft/authz`.
1729
+ * Redirects only count when the destination region contains a settings-store dir.
1094
1730
  * Pathish/token checks run even when the raw command lacks contiguous `.deft/authz`
1095
1731
  * text (quote-split residual: `cp x '.deft/'authz'/grants/y'`).
1096
1732
  */
@@ -1116,7 +1752,7 @@ function hasAuthzDirShellWrite(command, tokens) {
1116
1752
  hay[end] !== "\n") {
1117
1753
  end++;
1118
1754
  }
1119
- if (hay.slice(j, end).includes(".deft/authz"))
1755
+ if (pathishIsSettingsStoreDir(hay.slice(j, end)))
1120
1756
  return true;
1121
1757
  }
1122
1758
  }
@@ -1126,7 +1762,7 @@ function hasAuthzDirShellWrite(command, tokens) {
1126
1762
  // must not fail-open as unclassifiable (SLizard residual). ARCHIVE_ALT_WRITE_BINS: tar/rclone
1127
1763
  // residual pathish without dest-flag perfection thrash.
1128
1764
  for (let ti = 0; ti < tokens.length; ti++) {
1129
- const bare = binBareName(tokens[ti]);
1765
+ const bare = writeBinName(tokens[ti]);
1130
1766
  const n = normalizeToken(tokens[ti]);
1131
1767
  if (!INDIRECT_WRITE_BINS.has(n) &&
1132
1768
  !INDIRECT_WRITE_BINS.has(bare) &&
@@ -1134,20 +1770,24 @@ function hasAuthzDirShellWrite(command, tokens) {
1134
1770
  !ARCHIVE_ALT_WRITE_BINS.has(bare)) {
1135
1771
  continue;
1136
1772
  }
1773
+ // Last-positional dest bins (and their subcommands): dest harvest owns dest vs source.
1774
+ if (LAST_POSITIONAL_DEST_BINS.has(bare) || segmentStartedByLastPositionalDest(tokens, ti)) {
1775
+ continue;
1776
+ }
1137
1777
  for (let tj = ti + 1; tj < tokens.length; tj++) {
1138
1778
  const p = pathishToken(tokens[tj]);
1139
- if (pathishIsAuthzDir(p))
1779
+ if (pathishIsSettingsStoreDir(p))
1140
1780
  return true;
1141
1781
  for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
1142
- if (p.startsWith(prefix) && pathishIsAuthzDir(p.slice(prefix.length))) {
1782
+ if (p.startsWith(prefix) && pathishIsSettingsStoreDir(p.slice(prefix.length))) {
1143
1783
  return true;
1144
1784
  }
1145
1785
  }
1146
1786
  }
1147
1787
  }
1148
- // Downloader/decoder destinations under .deft/authz (#3206 / #3213 / #3245 archive+alt).
1788
+ // Downloader/decoder destinations under settings-store dirs (#3206 / #3421).
1149
1789
  for (const dest of downloaderDecoderDestinations(tokens)) {
1150
- if (pathishIsAuthzDir(dest))
1790
+ if (pathishIsSettingsStoreDir(dest))
1151
1791
  return true;
1152
1792
  }
1153
1793
  // Contiguous mention without write shape stays false (reads like cat .deft/authz/…).
@@ -1184,8 +1824,15 @@ const INDIRECT_WRITE_BINS = new Set([
1184
1824
  "remove-item",
1185
1825
  "ri",
1186
1826
  "ni",
1827
+ "new-item",
1187
1828
  "sc",
1188
1829
  "mi",
1830
+ // #3459 residual: Windows copy / fsutil / touch / TS runners.
1831
+ "copy",
1832
+ "fsutil",
1833
+ "touch",
1834
+ "tsx",
1835
+ "ts-node",
1189
1836
  ]);
1190
1837
  /**
1191
1838
  * O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
@@ -1227,7 +1874,7 @@ function hasWriteShape(command, tokens) {
1227
1874
  if (command.includes(">"))
1228
1875
  return true;
1229
1876
  for (const t of tokens) {
1230
- const bare = binBareName(t);
1877
+ const bare = writeBinName(t);
1231
1878
  if (INDIRECT_WRITE_BINS.has(normalizeToken(t)) || INDIRECT_WRITE_BINS.has(bare))
1232
1879
  return true;
1233
1880
  // #3245: archive extractors / alt downloaders are write-shaped (defense-in-depth for
@@ -1585,7 +2232,7 @@ export function classifyShellAuthzOps(command) {
1585
2232
  lower[end] !== "\n") {
1586
2233
  end++;
1587
2234
  }
1588
- if (lower.slice(j, end).includes("authz")) {
2235
+ if (destMentionsAuthzSegment(lower.slice(j, end))) {
1589
2236
  found.add("settings");
1590
2237
  break;
1591
2238
  }