@deftai/directive-core 0.104.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 (225) hide show
  1. package/dist/authz/classify.js +526 -42
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/named-cause.d.ts +1 -1
  5. package/dist/check/named-cause.js +33 -3
  6. package/dist/codebase/default-extractor.d.ts +5 -0
  7. package/dist/codebase/default-extractor.js +7 -15
  8. package/dist/deposit/copy-tree.js +60 -0
  9. package/dist/deposit/python-free.js +26 -18
  10. package/dist/doctor/checks.d.ts +8 -0
  11. package/dist/doctor/checks.js +37 -25
  12. package/dist/doctor/doctor-state.d.ts +4 -0
  13. package/dist/doctor/doctor-state.js +7 -1
  14. package/dist/doctor/main.js +9 -19
  15. package/dist/doctor/types.d.ts +9 -0
  16. package/dist/fs/contained-write.d.ts +115 -1
  17. package/dist/fs/contained-write.js +241 -5
  18. package/dist/fs/mutation-ledger.d.ts +54 -0
  19. package/dist/fs/mutation-ledger.js +133 -0
  20. package/dist/fs/non-product-dirs.d.ts +79 -0
  21. package/dist/fs/non-product-dirs.js +96 -0
  22. package/dist/hooks/dispatcher.d.ts +3 -1
  23. package/dist/hooks/dispatcher.js +14 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/init-deposit/agent-hooks.js +17 -15
  27. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  28. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  29. package/dist/init-deposit/gitignore.js +9 -0
  30. package/dist/init-deposit/hygiene.d.ts +47 -11
  31. package/dist/init-deposit/hygiene.js +172 -37
  32. package/dist/init-deposit/legacy-detect.js +6 -2
  33. package/dist/init-deposit/refresh.d.ts +21 -1
  34. package/dist/init-deposit/refresh.js +106 -9
  35. package/dist/init-deposit/scaffold.js +28 -279
  36. package/dist/init-deposit/slash-deposit.js +13 -7
  37. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  38. package/dist/init-deposit/xbrief-projections.js +24 -11
  39. package/dist/intake/clause-derivation.d.ts +38 -1
  40. package/dist/intake/clause-derivation.js +211 -9
  41. package/dist/intake/issue-ingest.d.ts +3 -1
  42. package/dist/intake/issue-ingest.js +35 -6
  43. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  44. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  45. package/dist/literal-acceptance/capture.d.ts +10 -0
  46. package/dist/literal-acceptance/capture.js +224 -75
  47. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  48. package/dist/literal-acceptance/evaluate.js +70 -2
  49. package/dist/literal-acceptance/index.d.ts +3 -3
  50. package/dist/literal-acceptance/index.js +3 -3
  51. package/dist/literal-acceptance/safety.d.ts +41 -0
  52. package/dist/literal-acceptance/safety.js +126 -2
  53. package/dist/literal-acceptance/types.d.ts +8 -0
  54. package/dist/orphan-active/evaluate.d.ts +7 -1
  55. package/dist/orphan-active/evaluate.js +102 -23
  56. package/dist/platform/platform-capabilities.js +2 -3
  57. package/dist/policy/base-branch.d.ts +37 -0
  58. package/dist/policy/base-branch.js +114 -0
  59. package/dist/policy/branch-sync.d.ts +80 -0
  60. package/dist/policy/branch-sync.js +211 -0
  61. package/dist/policy/ceremony-dial-escalation.js +1 -0
  62. package/dist/policy/ceremony-dial.js +4 -14
  63. package/dist/policy/delivery-branch.d.ts +5 -3
  64. package/dist/policy/delivery-branch.js +7 -3
  65. package/dist/policy/file-size-thresholds.d.ts +24 -0
  66. package/dist/policy/file-size-thresholds.js +23 -0
  67. package/dist/policy/forge-outage-retry.d.ts +47 -0
  68. package/dist/policy/forge-outage-retry.js +157 -0
  69. package/dist/policy/index.d.ts +7 -0
  70. package/dist/policy/index.js +66 -7
  71. package/dist/policy/project-invariants.d.ts +50 -0
  72. package/dist/policy/project-invariants.js +205 -0
  73. package/dist/policy/sync-default.d.ts +129 -0
  74. package/dist/policy/sync-default.js +452 -0
  75. package/dist/policy/sync-max-files.d.ts +93 -0
  76. package/dist/policy/sync-max-files.js +192 -0
  77. package/dist/pr-monitor/absent-required.d.ts +11 -0
  78. package/dist/pr-monitor/absent-required.js +61 -0
  79. package/dist/pr-monitor/constants.d.ts +7 -0
  80. package/dist/pr-monitor/constants.js +7 -0
  81. package/dist/pr-monitor/index.d.ts +2 -1
  82. package/dist/pr-monitor/index.js +2 -1
  83. package/dist/pr-monitor/main.js +4 -0
  84. package/dist/pr-monitor/monitor.js +24 -1
  85. package/dist/pr-wait-mergeable/classify.js +6 -0
  86. package/dist/preflight/evaluate.d.ts +4 -0
  87. package/dist/preflight/evaluate.js +35 -1
  88. package/dist/preflight/index.d.ts +4 -0
  89. package/dist/preflight/index.js +2 -0
  90. package/dist/preflight/intended-placement.d.ts +62 -0
  91. package/dist/preflight/intended-placement.js +275 -0
  92. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  93. package/dist/preflight/project-invariants-gate.js +82 -0
  94. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  95. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  96. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  97. package/dist/product-first-done-gate/acceptance.js +79 -8
  98. package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
  99. package/dist/product-first-done-gate/empty-resolution.js +12 -1
  100. package/dist/product-first-done-gate/evaluate.d.ts +12 -0
  101. package/dist/product-first-done-gate/evaluate.js +254 -31
  102. package/dist/product-first-done-gate/index.d.ts +2 -0
  103. package/dist/product-first-done-gate/index.js +1 -0
  104. package/dist/release/build-dist.d.ts +101 -0
  105. package/dist/release/build-dist.js +428 -46
  106. package/dist/run-summary/emit.d.ts +16 -10
  107. package/dist/run-summary/emit.js +26 -15
  108. package/dist/run-summary/index.d.ts +3 -2
  109. package/dist/run-summary/index.js +2 -1
  110. package/dist/run-summary/session-id.d.ts +21 -0
  111. package/dist/run-summary/session-id.js +92 -0
  112. package/dist/run-summary/types.d.ts +29 -4
  113. package/dist/run-summary/types.js +1 -0
  114. package/dist/scope/acceptance-evidence.d.ts +19 -7
  115. package/dist/scope/acceptance-evidence.js +38 -11
  116. package/dist/scope/delivery-evidence.d.ts +3 -3
  117. package/dist/scope/delivery-evidence.js +22 -27
  118. package/dist/scope/index.d.ts +1 -0
  119. package/dist/scope/index.js +1 -0
  120. package/dist/scope/parent-lineage.js +7 -6
  121. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  122. package/dist/scope/project-invariant-coverage.js +322 -0
  123. package/dist/scope/transition.js +1 -1
  124. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  125. package/dist/scope-provenance/compare-intent.js +233 -0
  126. package/dist/scope-provenance/digest.d.ts +18 -1
  127. package/dist/scope-provenance/digest.js +16 -11
  128. package/dist/scope-provenance/evaluate.d.ts +5 -1
  129. package/dist/scope-provenance/evaluate.js +64 -4
  130. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  131. package/dist/scope-provenance/extract-intent.js +262 -0
  132. package/dist/scope-provenance/index.d.ts +7 -1
  133. package/dist/scope-provenance/index.js +7 -1
  134. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  135. package/dist/scope-provenance/intent-digest.js +33 -0
  136. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  137. package/dist/scope-provenance/intent-evaluate.js +149 -0
  138. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  139. package/dist/scope-provenance/json-tokenizer.js +225 -0
  140. package/dist/scope-provenance/known-machine.d.ts +38 -0
  141. package/dist/scope-provenance/known-machine.js +157 -0
  142. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  143. package/dist/scope-provenance/mint-artifacts.js +425 -0
  144. package/dist/session/ac-pass-banking.d.ts +4 -36
  145. package/dist/session/ac-pass-banking.js +30 -77
  146. package/dist/session/ac-pass-reuse.d.ts +53 -0
  147. package/dist/session/ac-pass-reuse.js +95 -0
  148. package/dist/session/git.js +19 -4
  149. package/dist/session/index.d.ts +4 -0
  150. package/dist/session/index.js +4 -0
  151. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  152. package/dist/session/occupancy-stress-worker.js +36 -0
  153. package/dist/session/occupancy.d.ts +97 -0
  154. package/dist/session/occupancy.js +422 -0
  155. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  156. package/dist/session/product-state-hash.d.ts +26 -0
  157. package/dist/session/product-state-hash.js +683 -0
  158. package/dist/session/session-ready.d.ts +2 -0
  159. package/dist/session/session-ready.js +52 -30
  160. package/dist/session/session-start.d.ts +7 -0
  161. package/dist/session/session-start.js +71 -7
  162. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  163. package/dist/session/verify-ac-session-cache.js +126 -0
  164. package/dist/session/verify-session-ritual.d.ts +5 -0
  165. package/dist/session/verify-session-ritual.js +9 -0
  166. package/dist/slash/openclaw-deposit.js +13 -9
  167. package/dist/slice/constants.d.ts +2 -2
  168. package/dist/slice/constants.js +2 -2
  169. package/dist/slice/existing.js +9 -3
  170. package/dist/slice/lock.d.ts +16 -1
  171. package/dist/slice/lock.js +137 -11
  172. package/dist/story-ready/evaluate.d.ts +2 -0
  173. package/dist/story-ready/evaluate.js +13 -0
  174. package/dist/swarm/complete-cohort.d.ts +3 -0
  175. package/dist/swarm/complete-cohort.js +59 -11
  176. package/dist/swarm/finalize-cohort.js +33 -2
  177. package/dist/swarm/launch.d.ts +22 -0
  178. package/dist/swarm/launch.js +155 -0
  179. package/dist/swarm/routing-set-cli.d.ts +11 -1
  180. package/dist/swarm/routing-set-cli.js +13 -3
  181. package/dist/swarm/routing-verify.d.ts +12 -0
  182. package/dist/swarm/routing-verify.js +59 -20
  183. package/dist/swarm/routing.d.ts +14 -0
  184. package/dist/swarm/routing.js +53 -3
  185. package/dist/telemetry-coverage/fake-trial.js +10 -0
  186. package/dist/telemetry-coverage/kinds.js +2 -0
  187. package/dist/triage/actions/index.js +4 -1
  188. package/dist/ts-check-lane/index.d.ts +3 -0
  189. package/dist/ts-check-lane/index.js +2 -0
  190. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  191. package/dist/ts-check-lane/progress-reporter.js +38 -0
  192. package/dist/ts-check-lane/progress.d.ts +32 -0
  193. package/dist/ts-check-lane/progress.js +57 -0
  194. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  195. package/dist/ts-check-lane/run-lane.js +8 -6
  196. package/dist/validate-content/validate-links.d.ts +8 -0
  197. package/dist/validate-content/validate-links.js +9 -15
  198. package/dist/value/feedback-file.js +12 -2
  199. package/dist/vbrief-build/index.d.ts +1 -0
  200. package/dist/vbrief-build/index.js +1 -0
  201. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  202. package/dist/vbrief-build/project-invariants-io.js +41 -0
  203. package/dist/vbrief-reconcile/graph.js +18 -1
  204. package/dist/vbrief-reconcile/index.d.ts +1 -1
  205. package/dist/vbrief-reconcile/index.js +1 -1
  206. package/dist/vbrief-reconcile/types.d.ts +13 -0
  207. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  208. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  209. package/dist/vbrief-validate/index.d.ts +1 -1
  210. package/dist/vbrief-validate/index.js +1 -1
  211. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  212. package/dist/vbrief-validate/plan-hooks.js +68 -0
  213. package/dist/verify-ac/evaluate.d.ts +7 -1
  214. package/dist/verify-ac/evaluate.js +34 -26
  215. package/dist/verify-ac/flag.d.ts +4 -0
  216. package/dist/verify-ac/flag.js +16 -0
  217. package/dist/verify-ac/index.d.ts +2 -2
  218. package/dist/verify-ac/index.js +2 -2
  219. package/dist/verify-source/contained-writes.js +2 -0
  220. package/dist/verify-source/verify-stubs.d.ts +15 -1
  221. package/dist/verify-source/verify-stubs.js +20 -16
  222. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  223. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  224. package/dist/xbrief-migrate/migrate-project.js +2 -5
  225. package/package.json +3 -3
@@ -262,10 +262,50 @@ const DOWNLOADER_DECODER_BINS = new Set([
262
262
  "sqlite3",
263
263
  "crane",
264
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",
265
305
  ]);
266
306
  /**
267
307
  * Archive extractors / alt writers that can plant via pathish operands without
268
- * shell redirects (#3245 / #3288 / #3311 / #3336 / #3354). 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)
269
309
  * and write-shape residual under UAT — not bare curl-class URL fetches.
270
310
  */
271
311
  const ARCHIVE_ALT_WRITE_BINS = new Set([
@@ -338,9 +378,30 @@ const ARCHIVE_ALT_WRITE_BINS = new Set([
338
378
  "sqlite3",
339
379
  "crane",
340
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",
341
402
  ]);
342
403
  /**
343
- * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354).
404
+ * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354 / #3382).
344
405
  * Prefer a Set over a long `||` chain so coverage counts one membership check, not N branches.
345
406
  */
346
407
  const PROTECTED_POSITIONAL_BINS = new Set([
@@ -403,7 +464,132 @@ const PROTECTED_POSITIONAL_BINS = new Set([
403
464
  "sqlite3",
404
465
  "crane",
405
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",
406
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
+ }
407
593
  /** wget family (directory-prefix dest flags). */
408
594
  const WGET_FAMILY_BINS = new Set(["wget", "wget2"]);
409
595
  /** aria2 family (dir dest flags). */
@@ -419,6 +605,57 @@ const SQLITE3_OUTPUT_META = [".output", ".once"];
419
605
  const TAR_FAMILY_BINS = new Set(["tar", "bsdtar", "gtar", "star", "gnutar"]);
420
606
  /** xh / httpie family (download-dir dest flags) (#3311). */
421
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"]);
422
659
  /**
423
660
  * File destination flags for downloaders/decoders (#3206).
424
661
  * normalizeToken lowercases, so wget `-O` and curl `-o` share `-o`.
@@ -433,6 +670,8 @@ const DOWNLOADER_FILE_DEST_FLAGS = new Set([
433
670
  "-out",
434
671
  "--out",
435
672
  "--outfile",
673
+ // PowerShell iwr/Invoke-WebRequest -OutFile (single-dash long form) (#3459).
674
+ "-outfile",
436
675
  // unar single-dash long form; GNU/others use --output-directory.
437
676
  "-output-directory",
438
677
  "--output-directory",
@@ -479,12 +718,43 @@ function binBareName(token) {
479
718
  const base = pathish.includes("/") ? pathish.slice(pathish.lastIndexOf("/") + 1) : pathish;
480
719
  return base.endsWith(".exe") ? base.slice(0, -4) : base;
481
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
+ }
482
752
  /** True when token names a downloader/decoder bin (bare or path-qualified). */
483
753
  function isDownloaderDecoderBin(token) {
484
754
  const n = normalizeToken(token);
485
755
  if (n.startsWith("-"))
486
756
  return false;
487
- return DOWNLOADER_DECODER_BINS.has(binBareName(token));
757
+ return DOWNLOADER_DECODER_BINS.has(writeBinName(token));
488
758
  }
489
759
  /**
490
760
  * True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
@@ -532,6 +802,18 @@ function isDownloaderDestFlag(flag, bin, rawFlag) {
532
802
  return true;
533
803
  if (ATOOL_FAMILY_BINS.has(bin) && ATOOL_DIR_DEST_FLAGS.has(flag))
534
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;
535
817
  return false;
536
818
  }
537
819
  /**
@@ -599,7 +881,7 @@ function downloaderDecoderDestinations(tokens) {
599
881
  i++;
600
882
  continue;
601
883
  }
602
- const bin = binBareName(tokens[i]);
884
+ const bin = writeBinName(tokens[i]);
603
885
  i++;
604
886
  // xxd reverse mode writes; without -r, path positionals are dump inputs (read).
605
887
  let xxdReverse = false;
@@ -610,7 +892,9 @@ function downloaderDecoderDestinations(tokens) {
610
892
  // Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
611
893
  let lastPositionalPath = null;
612
894
  const protectedPathish = [];
613
- 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));
614
898
  while (i < tokens.length) {
615
899
  const raw = tokens[i];
616
900
  const n = normalizeToken(raw);
@@ -672,7 +956,8 @@ function downloaderDecoderDestinations(tokens) {
672
956
  // Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
673
957
  // 7z requires attached -oDIR (#3245).
674
958
  if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
675
- 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") {
676
961
  dests.push(pathishToken(raw.slice(4)));
677
962
  i++;
678
963
  continue;
@@ -698,6 +983,12 @@ function downloaderDecoderDestinations(tokens) {
698
983
  i++;
699
984
  continue;
700
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
+ }
701
992
  // unzip / cabextract attached -dDIR (#3245 / #3336)
702
993
  if (UNZIP_DIR_DEST_BINS.has(bin) && n.startsWith("-d") && n.length > 2) {
703
994
  dests.push(pathishToken(raw.slice(2)));
@@ -751,9 +1042,7 @@ function downloaderDecoderDestinations(tokens) {
751
1042
  if (p.startsWith(prefix) && p.length > prefix.length) {
752
1043
  const dest = p.slice(prefix.length);
753
1044
  dests.push(dest);
754
- if (dest.includes(".deft/authz") ||
755
- dest.includes(".deft-directive-disable") ||
756
- dest.includes(".no-deft-directive")) {
1045
+ if (pathishIsProtectedDest(dest)) {
757
1046
  protectedPathish.push(dest);
758
1047
  }
759
1048
  break;
@@ -765,7 +1054,7 @@ function downloaderDecoderDestinations(tokens) {
765
1054
  const meta = sqlite3MetaDest(raw, tokens[i + 1]);
766
1055
  if (meta !== null) {
767
1056
  dests.push(meta.dest);
768
- if (pathishIsAuthzDir(meta.dest) || pathishMentionsKillSwitch(meta.dest)) {
1057
+ if (pathishIsProtectedDest(meta.dest)) {
769
1058
  protectedPathish.push(meta.dest);
770
1059
  }
771
1060
  i += meta.consumedNext ? 2 : 1;
@@ -773,18 +1062,16 @@ function downloaderDecoderDestinations(tokens) {
773
1062
  }
774
1063
  }
775
1064
  // scp / certutil / rclone / archive extractors: pathish operands (quote-aware glued-op cut).
776
- // Under UAT: any `.deft/authz` / kill-switch pathish is fail-closed settings
777
- // (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.
778
1068
  if (collectsProtectedPositionals && !n.startsWith("-")) {
779
1069
  const cut = firstUnquotedShellOpIndex(raw);
780
1070
  const cleaned = cut >= 0 ? raw.slice(0, cut) : raw;
781
1071
  const p = pathishToken(cleaned);
782
1072
  if (p.length > 0) {
783
1073
  lastPositionalPath = p;
784
- // Fail-closed: protected store/kill basenames anywhere in pathish.
785
- if (p.includes(".deft/authz") ||
786
- p.includes(".deft-directive-disable") ||
787
- p.includes(".no-deft-directive")) {
1074
+ if (pathishIsProtectedDest(p) && !LAST_POSITIONAL_DEST_BINS.has(bin)) {
788
1075
  protectedPathish.push(p);
789
1076
  }
790
1077
  }
@@ -886,13 +1173,107 @@ function hasPolicyAuthorityMutator(tokens) {
886
1173
  }
887
1174
  /** True when pathish token names a kill-switch basename (quote-strip resistant). */
888
1175
  function pathishIsProtectedDest(pathish) {
889
- return pathishIsAuthzDir(pathish) || pathishMentionsKillSwitch(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";
890
1269
  }
891
1270
  /**
892
- * Fail-closed dest harvest for write-shaped Shell under UAT (#3354).
1271
+ * Fail-closed dest harvest for write-shaped Shell under UAT (#3354 / #3382 / #3421).
893
1272
  * Named-bin parsers above are not the only path: any token that looks like
894
- * `-o` / `--output` / `--outfile` / `--output-file` (or attached `-oDIR`)
895
- * whose dest is authz/kill-switch is collected even when the bin is unknown.
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.
896
1277
  * scp `-o` (OpenSSH option) and cpio `-o` (copy-out) stay excluded.
897
1278
  */
898
1279
  function genericProtectedDests(tokens) {
@@ -909,13 +1290,13 @@ function genericProtectedDests(tokens) {
909
1290
  }
910
1291
  if (!n.startsWith("-")) {
911
1292
  if (isDownloaderDecoderBin(raw)) {
912
- currentBin = binBareName(raw);
1293
+ currentBin = writeBinName(raw);
913
1294
  }
914
1295
  else if (currentBin.length === 0 &&
915
1296
  !raw.includes("/") &&
916
1297
  !raw.includes("\\") &&
917
1298
  n.length > 0) {
918
- currentBin = binBareName(raw);
1299
+ currentBin = writeBinName(raw);
919
1300
  }
920
1301
  }
921
1302
  if (currentBin === "scp" || currentBin === "cpio")
@@ -923,7 +1304,7 @@ function genericProtectedDests(tokens) {
923
1304
  if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
924
1305
  const eq = raw.indexOf("=");
925
1306
  const flag = normalizeToken(raw.slice(0, eq));
926
- if (DOWNLOADER_FILE_DEST_FLAGS.has(flag)) {
1307
+ if (isGenericProtectedDestFlag(flag) && !isReadShapedInputFileFlag(currentBin, flag)) {
927
1308
  const dest = pathishToken(raw.slice(eq + 1));
928
1309
  if (pathishIsProtectedDest(dest))
929
1310
  dests.push(dest);
@@ -940,7 +1321,13 @@ function genericProtectedDests(tokens) {
940
1321
  dests.push(dest);
941
1322
  continue;
942
1323
  }
943
- if (DOWNLOADER_FILE_DEST_FLAGS.has(n)) {
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)) {
944
1331
  const next = tokens[i + 1];
945
1332
  if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
946
1333
  const dest = pathishToken(next);
@@ -969,8 +1356,9 @@ function sqlite3MetaDest(raw, next) {
969
1356
  return null;
970
1357
  }
971
1358
  function pathishMentionsKillSwitch(pathish) {
1359
+ const p = canonicalizePathish(pathish);
972
1360
  for (const name of KILL_SWITCH_BASENAMES) {
973
- if (pathish === name || pathish.endsWith(`/${name}`) || pathish.includes(name)) {
1361
+ if (p === name || p.endsWith(`/${name}`) || p.includes(name)) {
974
1362
  return true;
975
1363
  }
976
1364
  }
@@ -1045,9 +1433,13 @@ function hasKillSwitchShellWrite(command, tokens) {
1045
1433
  ]);
1046
1434
  for (let ti = 0; ti < tokens.length; ti++) {
1047
1435
  const binTok = normalizeToken(tokens[ti]);
1048
- const bare = binBareName(tokens[ti]);
1436
+ const bare = writeBinName(tokens[ti]);
1049
1437
  if (!killWriteBins.has(binTok) && !killWriteBins.has(bare))
1050
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
+ }
1051
1443
  for (let tj = ti + 1; tj < tokens.length; tj++) {
1052
1444
  const p = pathishToken(tokens[tj]);
1053
1445
  if (pathishMentionsKillSwitch(p))
@@ -1061,8 +1453,14 @@ function hasKillSwitchShellWrite(command, tokens) {
1061
1453
  }
1062
1454
  }
1063
1455
  // Bare `touch .deft-directive-disable` / `ln -sf x .deft-directive-disable` — path later.
1064
- for (const t of tokens) {
1065
- 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]);
1066
1464
  for (const name of KILL_SWITCH_BASENAMES) {
1067
1465
  // Exact basename or ends with /basename
1068
1466
  if (p === name || p.endsWith(`/${name}`)) {
@@ -1070,8 +1468,8 @@ function hasKillSwitchShellWrite(command, tokens) {
1070
1468
  if (hasWriteShape(command, tokens) ||
1071
1469
  lower.includes("touch") ||
1072
1470
  lower.includes("new-item") ||
1073
- SYMLINK_PLANT_BINS.has(binBareName(tokens[0])) ||
1074
- 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)))) {
1075
1473
  return true;
1076
1474
  }
1077
1475
  }
@@ -1100,7 +1498,10 @@ function isProgrammaticWriteBinToken(token) {
1100
1498
  bare === "perl" ||
1101
1499
  bare === "ruby" ||
1102
1500
  bare === "pwsh" ||
1103
- bare === "powershell") {
1501
+ bare === "powershell" ||
1502
+ bare === "tsx" ||
1503
+ bare === "ts-node" ||
1504
+ bare === "tsnode") {
1104
1505
  return true;
1105
1506
  }
1106
1507
  // Versioned: python3.11, python3.12, node18, …
@@ -1243,17 +1644,89 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
1243
1644
  function pathishToken(token) {
1244
1645
  return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
1245
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
+ }
1246
1682
  /**
1247
1683
  * True when a pathish string targets `.deft/authz` (after quote strip / slash normalize).
1248
1684
  * Quote-split forms like `'.deft/'authz'/grants/x'` become `.deft/authz/grants/x` via pathishToken.
1249
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
+ }
1250
1701
  function pathishIsAuthzDir(pathish) {
1251
- 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;
1252
1725
  }
1253
1726
  /**
1254
- * Shell **write** targeting `.deft/authz/` (#3110 AC-3 / #3206 / #3213).
1727
+ * Shell **write** targeting `.deft/authz/` or `.deft/approved-scope/` (#3110 / #3421).
1255
1728
  * Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
1256
- * Redirects only count when the destination region contains `.deft/authz`.
1729
+ * Redirects only count when the destination region contains a settings-store dir.
1257
1730
  * Pathish/token checks run even when the raw command lacks contiguous `.deft/authz`
1258
1731
  * text (quote-split residual: `cp x '.deft/'authz'/grants/y'`).
1259
1732
  */
@@ -1279,7 +1752,7 @@ function hasAuthzDirShellWrite(command, tokens) {
1279
1752
  hay[end] !== "\n") {
1280
1753
  end++;
1281
1754
  }
1282
- if (hay.slice(j, end).includes(".deft/authz"))
1755
+ if (pathishIsSettingsStoreDir(hay.slice(j, end)))
1283
1756
  return true;
1284
1757
  }
1285
1758
  }
@@ -1289,7 +1762,7 @@ function hasAuthzDirShellWrite(command, tokens) {
1289
1762
  // must not fail-open as unclassifiable (SLizard residual). ARCHIVE_ALT_WRITE_BINS: tar/rclone
1290
1763
  // residual pathish without dest-flag perfection thrash.
1291
1764
  for (let ti = 0; ti < tokens.length; ti++) {
1292
- const bare = binBareName(tokens[ti]);
1765
+ const bare = writeBinName(tokens[ti]);
1293
1766
  const n = normalizeToken(tokens[ti]);
1294
1767
  if (!INDIRECT_WRITE_BINS.has(n) &&
1295
1768
  !INDIRECT_WRITE_BINS.has(bare) &&
@@ -1297,20 +1770,24 @@ function hasAuthzDirShellWrite(command, tokens) {
1297
1770
  !ARCHIVE_ALT_WRITE_BINS.has(bare)) {
1298
1771
  continue;
1299
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
+ }
1300
1777
  for (let tj = ti + 1; tj < tokens.length; tj++) {
1301
1778
  const p = pathishToken(tokens[tj]);
1302
- if (pathishIsAuthzDir(p))
1779
+ if (pathishIsSettingsStoreDir(p))
1303
1780
  return true;
1304
1781
  for (const prefix of SOCAT_WRITE_ADDR_PREFIXES) {
1305
- if (p.startsWith(prefix) && pathishIsAuthzDir(p.slice(prefix.length))) {
1782
+ if (p.startsWith(prefix) && pathishIsSettingsStoreDir(p.slice(prefix.length))) {
1306
1783
  return true;
1307
1784
  }
1308
1785
  }
1309
1786
  }
1310
1787
  }
1311
- // Downloader/decoder destinations under .deft/authz (#3206 / #3213 / #3245 archive+alt).
1788
+ // Downloader/decoder destinations under settings-store dirs (#3206 / #3421).
1312
1789
  for (const dest of downloaderDecoderDestinations(tokens)) {
1313
- if (pathishIsAuthzDir(dest))
1790
+ if (pathishIsSettingsStoreDir(dest))
1314
1791
  return true;
1315
1792
  }
1316
1793
  // Contiguous mention without write shape stays false (reads like cat .deft/authz/…).
@@ -1347,8 +1824,15 @@ const INDIRECT_WRITE_BINS = new Set([
1347
1824
  "remove-item",
1348
1825
  "ri",
1349
1826
  "ni",
1827
+ "new-item",
1350
1828
  "sc",
1351
1829
  "mi",
1830
+ // #3459 residual: Windows copy / fsutil / touch / TS runners.
1831
+ "copy",
1832
+ "fsutil",
1833
+ "touch",
1834
+ "tsx",
1835
+ "ts-node",
1352
1836
  ]);
1353
1837
  /**
1354
1838
  * O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
@@ -1390,7 +1874,7 @@ function hasWriteShape(command, tokens) {
1390
1874
  if (command.includes(">"))
1391
1875
  return true;
1392
1876
  for (const t of tokens) {
1393
- const bare = binBareName(t);
1877
+ const bare = writeBinName(t);
1394
1878
  if (INDIRECT_WRITE_BINS.has(normalizeToken(t)) || INDIRECT_WRITE_BINS.has(bare))
1395
1879
  return true;
1396
1880
  // #3245: archive extractors / alt downloaders are write-shaped (defense-in-depth for
@@ -1748,7 +2232,7 @@ export function classifyShellAuthzOps(command) {
1748
2232
  lower[end] !== "\n") {
1749
2233
  end++;
1750
2234
  }
1751
- if (lower.slice(j, end).includes("authz")) {
2235
+ if (destMentionsAuthzSegment(lower.slice(j, end))) {
1752
2236
  found.add("settings");
1753
2237
  break;
1754
2238
  }