@deftai/directive-core 0.105.0 → 0.106.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 (85) hide show
  1. package/dist/authz/classify.js +591 -34
  2. package/dist/check/named-cause.js +1 -1
  3. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  4. package/dist/forward-coverage/diff-coverage.js +112 -0
  5. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  6. package/dist/forward-coverage/diff-lines.js +173 -0
  7. package/dist/forward-coverage/evaluate.d.ts +24 -9
  8. package/dist/forward-coverage/evaluate.js +125 -19
  9. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  10. package/dist/init-deposit/agent-hooks.js +1 -1
  11. package/dist/intake/clause-derivation.d.ts +9 -0
  12. package/dist/intake/clause-derivation.js +30 -3
  13. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  14. package/dist/lifecycle-visible/evaluate.js +710 -0
  15. package/dist/lifecycle-visible/index.d.ts +2 -0
  16. package/dist/lifecycle-visible/index.js +2 -0
  17. package/dist/literal-acceptance/capture.js +3 -3
  18. package/dist/orchestration/judgment-policy.d.ts +13 -0
  19. package/dist/orchestration/judgment-policy.js +15 -0
  20. package/dist/orchestration/probe-session.js +13 -8
  21. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  22. package/dist/orchestration/verify-judgment-gates.js +43 -11
  23. package/dist/policy/ceremony-dial.js +7 -7
  24. package/dist/policy/host-hooks.d.ts +20 -0
  25. package/dist/policy/host-hooks.js +121 -2
  26. package/dist/policy/org-force-on-migration.js +2 -1
  27. package/dist/policy/plan-extensions.d.ts +1 -1
  28. package/dist/policy/plan-extensions.js +2 -0
  29. package/dist/policy/product-signal.js +6 -7
  30. package/dist/policy/require-human-merge.js +8 -6
  31. package/dist/policy/resolve.d.ts +7 -2
  32. package/dist/policy/resolve.js +25 -8
  33. package/dist/policy/value-feedback.js +7 -8
  34. package/dist/preflight-cache/evaluate.d.ts +12 -0
  35. package/dist/preflight-cache/evaluate.js +15 -1
  36. package/dist/preflight-cache/index.d.ts +1 -1
  37. package/dist/preflight-cache/index.js +1 -1
  38. package/dist/product-first-done-gate/evaluate.d.ts +4 -0
  39. package/dist/product-first-done-gate/evaluate.js +46 -5
  40. package/dist/release/closed-verb-gate.d.ts +12 -0
  41. package/dist/release/closed-verb-gate.js +51 -0
  42. package/dist/release/index.d.ts +1 -0
  43. package/dist/release/index.js +1 -0
  44. package/dist/release/pipeline.js +7 -0
  45. package/dist/release/types.d.ts +12 -0
  46. package/dist/render/constants.d.ts +2 -1
  47. package/dist/render/constants.js +2 -1
  48. package/dist/render/framework-commands.js +4 -0
  49. package/dist/render/spec-validate.js +23 -12
  50. package/dist/run-summary/types.d.ts +4 -0
  51. package/dist/scope/main.js +15 -5
  52. package/dist/scope/promote-from-issue.js +5 -5
  53. package/dist/scope-provenance/evaluate.js +3 -3
  54. package/dist/session/ac-pass-banking.d.ts +5 -0
  55. package/dist/session/ac-pass-banking.js +3 -2
  56. package/dist/session/process-cost.d.ts +32 -10
  57. package/dist/session/process-cost.js +189 -1
  58. package/dist/session/product-state-hash.js +41 -24
  59. package/dist/session/ritual-entrypoint.js +16 -1
  60. package/dist/session/session-start.d.ts +6 -0
  61. package/dist/session/session-start.js +32 -2
  62. package/dist/session/verify-session-ritual.d.ts +24 -1
  63. package/dist/session/verify-session-ritual.js +89 -6
  64. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  65. package/dist/swarm/finalize-cohort-cli.js +165 -35
  66. package/dist/swarm/finalize-cohort.d.ts +1 -0
  67. package/dist/swarm/finalize-cohort.js +14 -2
  68. package/dist/swarm/routing-set-cli.js +2 -0
  69. package/dist/swarm/routing-verify.d.ts +1 -1
  70. package/dist/swarm/routing-verify.js +2 -2
  71. package/dist/triage/bootstrap/index.js +6 -6
  72. package/dist/triage/help/registry-data.d.ts +1 -1
  73. package/dist/triage/help/registry-data.js +1 -1
  74. package/dist/triage/welcome/writers.d.ts +1 -1
  75. package/dist/triage/welcome/writers.js +10 -5
  76. package/dist/value/readback.d.ts +4 -1
  77. package/dist/value/readback.js +51 -12
  78. package/dist/vbrief-validate/schema.d.ts +4 -0
  79. package/dist/vbrief-validate/schema.js +1 -1
  80. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  81. package/dist/verify-env/agent-hook-readiness.js +12 -8
  82. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  83. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  84. package/dist/verify-env/agent-hooks.js +3 -4
  85. package/package.json +7 -3
@@ -178,6 +178,37 @@ const POLICY_AUTHORITY_MUTATORS = new Set([
178
178
  ]);
179
179
  /** Kill-switch / permanent opt-out basenames agents must not plant under UAT (#3186 / #3039). */
180
180
  const KILL_SWITCH_BASENAMES = [".deft-directive-disable", ".no-deft-directive"];
181
+ /**
182
+ * Dest-form writers #3529 harvest missed (#3545). Shared across downloader,
183
+ * archive-alt, positional, and indirect-write membership so a named-peer
184
+ * allowlist cannot be the only fail-closed path.
185
+ */
186
+ const UAT_RESIDUAL_DEST_WRITE_BINS_3545 = [
187
+ "fromdos",
188
+ "todos",
189
+ "emacsclient",
190
+ "pico",
191
+ "pdftk",
192
+ "gs",
193
+ "dpkg",
194
+ "degit",
195
+ "composer",
196
+ "ddrescue",
197
+ "dc3dd",
198
+ "sg_dd",
199
+ ];
200
+ /** In-place residual writers: any protected operand is a dest. Extract/output bins stay dest-flag gated. */
201
+ const UAT_RESIDUAL_INPLACE_WRITE_BINS_3545 = [
202
+ "fromdos",
203
+ "todos",
204
+ "emacsclient",
205
+ "pico",
206
+ "degit",
207
+ "composer",
208
+ "ddrescue",
209
+ "dc3dd",
210
+ "sg_dd",
211
+ ];
181
212
  /**
182
213
  * Downloaders / decoders / remote-copy tools that can plant files without shell
183
214
  * redirects (#3206 / #3213 / #3245). Not in INDIRECT_WRITE_BINS: those feed hasWriteShape
@@ -302,6 +333,23 @@ const DOWNLOADER_DECODER_BINS = new Set([
302
333
  "gh",
303
334
  "glab",
304
335
  "hub",
336
+ // #3529 residual after #3459: dest-form writers harvest missed.
337
+ "xcopy",
338
+ "robocopy",
339
+ "bitsadmin",
340
+ "start-bitstransfer",
341
+ "expand-archive",
342
+ "tee-object",
343
+ "export-csv",
344
+ "pscp",
345
+ "jj",
346
+ "sponge",
347
+ "expand",
348
+ "extrac32",
349
+ "makecab",
350
+ "replace",
351
+ // #3545 residual after #3529: dest-form writers harvest missed.
352
+ ...UAT_RESIDUAL_DEST_WRITE_BINS_3545,
305
353
  ]);
306
354
  /**
307
355
  * Archive extractors / alt writers that can plant via pathish operands without
@@ -399,6 +447,21 @@ const ARCHIVE_ALT_WRITE_BINS = new Set([
399
447
  "iwr",
400
448
  "invoke-webrequest",
401
449
  "fsutil",
450
+ // #3529 residual: dest writers that are not general-purpose cmd/git.
451
+ "xcopy",
452
+ "robocopy",
453
+ "bitsadmin",
454
+ "start-bitstransfer",
455
+ "expand-archive",
456
+ "tee-object",
457
+ "export-csv",
458
+ "sponge",
459
+ "expand",
460
+ "extrac32",
461
+ "makecab",
462
+ "replace",
463
+ // #3545 residual: dest writers that are not general-purpose cmd/git.
464
+ ...UAT_RESIDUAL_INPLACE_WRITE_BINS_3545,
402
465
  ]);
403
466
  /**
404
467
  * Bins whose pathish operands are scanned for authz/kill destinations (#3213 / #3245 / #3288 / #3311 / #3336 / #3354 / #3382).
@@ -502,6 +565,23 @@ const PROTECTED_POSITIONAL_BINS = new Set([
502
565
  "iwr",
503
566
  "invoke-webrequest",
504
567
  "fsutil",
568
+ // #3529 residual: positional dest writers (xcopy/robocopy/BitsTransfer/pscp).
569
+ // jj clone dests are harvested only on the clone subcommand.
570
+ "xcopy",
571
+ "robocopy",
572
+ "bitsadmin",
573
+ "start-bitstransfer",
574
+ "expand-archive",
575
+ "tee-object",
576
+ "export-csv",
577
+ "pscp",
578
+ "sponge",
579
+ "expand",
580
+ "extrac32",
581
+ "makecab",
582
+ "replace",
583
+ // #3545 residual: positional dest writers (fromdos/dpkg/degit/ddrescue).
584
+ ...UAT_RESIDUAL_DEST_WRITE_BINS_3545,
505
585
  ]);
506
586
  /**
507
587
  * Bins whose dest is the last pathish operand (src … dest). A protected
@@ -626,6 +706,19 @@ const GENERIC_PROTECTED_EXTRA_DEST_FLAGS = new Set([
626
706
  "--file",
627
707
  "--separate-git-dir",
628
708
  "--pack-destination",
709
+ // #3529 residual dest flags (PowerShell single-dash + extra dir dests).
710
+ "--output-dir",
711
+ "--outdir",
712
+ "--target-directory",
713
+ "--destdir",
714
+ "-destination",
715
+ "-destinationpath",
716
+ "-filepath",
717
+ "--prefix",
718
+ // #3545 residual dest flags (VCS repo dest + ghostscript output).
719
+ "--repodir",
720
+ "--repo-dir",
721
+ "-soutputfile",
629
722
  ]);
630
723
  /** Bins whose `--file` / `-f` operand is an input, not a dest plant. */
631
724
  const READ_SHAPED_FILE_FLAG_BINS = new Set([
@@ -649,6 +742,18 @@ const READ_INPUT_FILE_FLAGS = new Set(["--file", "-f"]);
649
742
  const NEW_ITEM_PATH_DEST_FLAGS = new Set(["-path", "-literalpath", "--literalpath"]);
650
743
  /** git subcommands whose positionals can plant a dest (#3421). */
651
744
  const GIT_WRITE_SUBCOMMANDS = new Set(["clone", "worktree", "submodule"]);
745
+ /** git bundle subcommands that write a bundle file (#3529). Reads: verify / list-heads / unbundle. */
746
+ const GIT_BUNDLE_WRITE_SUBCOMMANDS = new Set(["create"]);
747
+ /** jj subcommands whose positionals can plant a dest (#3529). */
748
+ const JJ_WRITE_SUBCOMMANDS = new Set(["clone"]);
749
+ const JJ_PRE_SUBCOMMAND_BOOLEAN_FLAGS = new Set([
750
+ "--help",
751
+ "-h",
752
+ "--quiet",
753
+ "-q",
754
+ "--ignore-immutable",
755
+ "--no-pager",
756
+ ]);
652
757
  /** Forge CLIs whose `clone` dest is harvested like git clone (#3459). */
653
758
  const FORGE_CLONE_BINS = new Set(["gh", "glab", "hub"]);
654
759
  /** fossil dest-dir flags (#3382 PATH form + #3421 --workdir=). */
@@ -756,14 +861,17 @@ function isDownloaderDecoderBin(token) {
756
861
  return false;
757
862
  return DOWNLOADER_DECODER_BINS.has(writeBinName(token));
758
863
  }
864
+ function isScpFamilyBin(bin) {
865
+ return bin === "scp" || bin === "pscp";
866
+ }
759
867
  /**
760
868
  * True when `flag` (normalized lower) or `rawFlag` (original token) is a dest flag for `bin`.
761
869
  * tar `-C` must use raw case: lowercased `-c` is create-archive, not chdir (#3245).
762
870
  * 7z uses attached `-oDIR` only — separate `-o PATH` is not a 7z dest form (handled elsewhere).
763
871
  */
764
872
  function isDownloaderDestFlag(flag, bin, rawFlag) {
765
- // scp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
766
- if (bin === "scp")
873
+ // scp/pscp: `-o` is OpenSSH option (ProxyCommand, …), not a file dest flag.
874
+ if (isScpFamilyBin(bin))
767
875
  return false;
768
876
  // 7z family: only attached `-oDIR` (parsed in attached-short branch), not separate `-o PATH`.
769
877
  if (SEVEN_Z_FAMILY_BINS.has(bin)) {
@@ -864,6 +972,28 @@ function firstUnquotedShellOpIndex(raw) {
864
972
  }
865
973
  return -1;
866
974
  }
975
+ /**
976
+ * dpkg/pdftk/gs only harvest protected positionals on write forms
977
+ * (`dpkg -x`, `pdftk … output`, gs dest flags). Inspect/read forms stay unclassifiable.
978
+ */
979
+ function residualPositionalBinIsWriteShaped(bin, tokens, start) {
980
+ if (bin !== "dpkg" && bin !== "pdftk" && bin !== "gs")
981
+ return true;
982
+ for (let k = start; k < tokens.length; k++) {
983
+ const raw = tokens[k];
984
+ if (tokenEndsShellSegment(raw))
985
+ break;
986
+ const n = normalizeToken(raw);
987
+ if (bin === "dpkg" && (n === "-x" || n === "--extract" || n === "--vextract")) {
988
+ return true;
989
+ }
990
+ if (bin === "pdftk" && n === "output")
991
+ return true;
992
+ if (bin === "gs" && (n.startsWith("-soutputfile") || n === "-soutputfile"))
993
+ return true;
994
+ }
995
+ return false;
996
+ }
867
997
  /**
868
998
  * Destinations from curl/wget/xxd/openssl/scp/aria2c/certutil via -o/--output/-O/-out/
869
999
  * --output-dir/-P/-d/--dir (separate, =value, or attached short form), xxd -r path-like
@@ -892,8 +1022,10 @@ function downloaderDecoderDestinations(tokens) {
892
1022
  // Segment breaks (`;`/`\n`/glued ops) prevent following-command overwrite.
893
1023
  let lastPositionalPath = null;
894
1024
  const protectedPathish = [];
895
- const collectsProtectedPositionals = PROTECTED_POSITIONAL_BINS.has(bin) ||
1025
+ const collectsProtectedPositionals = (PROTECTED_POSITIONAL_BINS.has(bin) && residualPositionalBinIsWriteShaped(bin, tokens, i)) ||
896
1026
  (bin === "git" && gitHasWriteSubcommand(tokens, i)) ||
1027
+ (bin === "git" && gitHasBundleCreate(tokens, i)) ||
1028
+ (bin === "jj" && jjHasWriteSubcommand(tokens, i)) ||
897
1029
  (FORGE_CLONE_BINS.has(bin) && forgeHasCloneSubcommand(tokens, i));
898
1030
  while (i < tokens.length) {
899
1031
  const raw = tokens[i];
@@ -926,8 +1058,8 @@ function downloaderDecoderDestinations(tokens) {
926
1058
  i++;
927
1059
  continue;
928
1060
  }
929
- // scp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
930
- if (bin === "scp" &&
1061
+ // scp/pscp: skip OpenSSH value-taking flags + their values (-o Option=Value, -i key, -P port).
1062
+ if (isScpFamilyBin(bin) &&
931
1063
  (SCP_VALUE_FLAGS_LOWER.has(n) ||
932
1064
  SCP_VALUE_FLAGS_EXACT.has(raw) ||
933
1065
  SCP_VALUE_FLAGS_EXACT.has(n))) {
@@ -953,9 +1085,9 @@ function downloaderDecoderDestinations(tokens) {
953
1085
  continue;
954
1086
  }
955
1087
  // Attached short: -oPATH / -OPATH (after lowercasing both are -opath…)
956
- // Skip for scp (OpenSSH -oOption=Value attached forms are not file dests).
1088
+ // Skip for scp/pscp (OpenSSH -oOption=Value attached forms are not file dests).
957
1089
  // 7z requires attached -oDIR (#3245).
958
- if (bin !== "scp" && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
1090
+ if (!isScpFamilyBin(bin) && n.startsWith("-") && !n.startsWith("--") && n.length > 2) {
959
1091
  // `-outfile` is PowerShell -OutFile (next token is dest), not attached -outFILE (#3459).
960
1092
  if (n.startsWith("-out") && n.length > 4 && !n.startsWith("-output") && n !== "-outfile") {
961
1093
  dests.push(pathishToken(raw.slice(4)));
@@ -1180,6 +1312,179 @@ function pathishIsProtectedDest(pathish) {
1180
1312
  function isGenericProtectedDestFlag(flag) {
1181
1313
  return DOWNLOADER_FILE_DEST_FLAGS.has(flag) || GENERIC_PROTECTED_EXTRA_DEST_FLAGS.has(flag);
1182
1314
  }
1315
+ /** Env-style dest keys harvested even without a leading `--` (`make DESTDIR=`). */
1316
+ const DEST_ASSIGNMENT_KEYS = new Set(["destdir", "prefix", "install_root", "dest_dir"]);
1317
+ /** Bins whose DESTDIR=/PREFIX= assignment is a dest plant (not `echo DESTDIR=…`). */
1318
+ const DEST_ASSIGNMENT_OWNER_BINS = new Set(["make", "gmake"]);
1319
+ /**
1320
+ * First-command bins that print/read: a later `make` token is data, not the writer
1321
+ * (`echo DESTDIR=… make`, `git log make DESTDIR=…`). Also the first command of a
1322
+ * search-launcher `-exec` / `-c` payload (`find -exec echo make DESTDIR=`).
1323
+ * Search/launchers themselves are not in this set — `find -exec make DESTDIR=`
1324
+ * still harvests the payload writer.
1325
+ */
1326
+ const DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS = new Set([
1327
+ "echo",
1328
+ "printf",
1329
+ "true",
1330
+ "false",
1331
+ ":",
1332
+ "git",
1333
+ "gh",
1334
+ "hg",
1335
+ "svn",
1336
+ "cat",
1337
+ "ls",
1338
+ "grep",
1339
+ "egrep",
1340
+ "fgrep",
1341
+ "rg",
1342
+ "head",
1343
+ "tail",
1344
+ "less",
1345
+ "more",
1346
+ "wc",
1347
+ "diff",
1348
+ "stat",
1349
+ "file",
1350
+ ]);
1351
+ /**
1352
+ * Search/launchers whose first token is not the writer. Harvest make/DESTDIR
1353
+ * only from `-exec` / `--exec` / `-c` payloads (`find -exec make DESTDIR=`).
1354
+ * The payload's first command still uses NON_WRITER (`find -exec echo make`).
1355
+ * `xargs make DESTDIR=` stays a wrapper (not in the non-writer set).
1356
+ */
1357
+ const DEST_ASSIGNMENT_SEARCH_LAUNCHER_BINS = new Set(["find"]);
1358
+ const DEST_ASSIGNMENT_EXEC_PAYLOAD_FLAGS = new Set([
1359
+ "-exec",
1360
+ "--exec",
1361
+ "-execdir",
1362
+ "--execdir",
1363
+ "-c",
1364
+ ]);
1365
+ /** find `-exec` payload terminator (`\;` or `+`). Resets so a later `-exec` is still operative. */
1366
+ function isFindExecPayloadTerminator(n) {
1367
+ return n === ";" || n === "+";
1368
+ }
1369
+ /** make targets that do not apply DESTDIR (avoid denying `make DESTDIR=… clean`). */
1370
+ const DEST_ASSIGNMENT_NON_WRITE_TARGETS = new Set([
1371
+ "clean",
1372
+ "distclean",
1373
+ "mostlyclean",
1374
+ "maintainer-clean",
1375
+ "check",
1376
+ "test",
1377
+ "tests",
1378
+ "help",
1379
+ ]);
1380
+ /** make flags that consume the next token (`-C dir`, `-f Makefile`). */
1381
+ const MAKE_VALUE_FLAGS = new Set([
1382
+ "-c",
1383
+ "--directory",
1384
+ "-f",
1385
+ "--file",
1386
+ "--makefile",
1387
+ "--include-dir",
1388
+ "-j",
1389
+ "--jobs",
1390
+ "-l",
1391
+ "--load-average",
1392
+ "--eval",
1393
+ ]);
1394
+ /**
1395
+ * True when a DESTDIR/PREFIX assignment is an operative make write.
1396
+ * Looks for make/gmake anywhere after wrappers (including unknown wrappers
1397
+ * such as `xargs`), unless the first command is a print/read bin.
1398
+ * Search/launchers (`find`) still harvest from `-exec` / `--exec` / `-c`
1399
+ * payloads unless that payload's first command is a print/read bin.
1400
+ * Payload state resets at `\;` / `+` so a later `-exec make DESTDIR=` is
1401
+ * still operative (`find -exec echo hi \; -exec make DESTDIR= install`).
1402
+ * Non-writing targets (`clean`) are not operative.
1403
+ */
1404
+ function segmentDestAssignmentIsOperative(tokens, tokenIndex) {
1405
+ let start = tokenIndex;
1406
+ while (start > 0 && !tokenEndsShellSegment(tokens[start - 1]))
1407
+ start--;
1408
+ let skipNext = false;
1409
+ let firstCommand = "";
1410
+ let inExecPayload = false;
1411
+ let execPayloadFirstCommand = "";
1412
+ let sawMake = false;
1413
+ const targets = [];
1414
+ for (let k = start; k < tokens.length; k++) {
1415
+ const raw = tokens[k];
1416
+ if (tokenEndsShellSegment(raw))
1417
+ break;
1418
+ if (skipNext) {
1419
+ skipNext = false;
1420
+ continue;
1421
+ }
1422
+ const n = normalizeToken(raw);
1423
+ if (inExecPayload && isFindExecPayloadTerminator(n)) {
1424
+ inExecPayload = false;
1425
+ execPayloadFirstCommand = "";
1426
+ continue;
1427
+ }
1428
+ // Print/read `-exec` payload: skip until terminator so a later `-exec` is seen.
1429
+ if (inExecPayload &&
1430
+ execPayloadFirstCommand.length > 0 &&
1431
+ DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS.has(execPayloadFirstCommand)) {
1432
+ continue;
1433
+ }
1434
+ if (n.startsWith("-")) {
1435
+ // find -exec / -c opens the payload; do not consume the next token as a
1436
+ // wrapper value. After make is seen, -C still skips its directory.
1437
+ if (DEST_ASSIGNMENT_EXEC_PAYLOAD_FLAGS.has(n) &&
1438
+ DEST_ASSIGNMENT_SEARCH_LAUNCHER_BINS.has(firstCommand) &&
1439
+ !sawMake) {
1440
+ inExecPayload = true;
1441
+ execPayloadFirstCommand = "";
1442
+ continue;
1443
+ }
1444
+ if (!n.includes("=") && (WRAPPER_VALUE_FLAGS.has(n) || MAKE_VALUE_FLAGS.has(n))) {
1445
+ skipNext = true;
1446
+ }
1447
+ continue;
1448
+ }
1449
+ if (isEnvAssign(raw))
1450
+ continue;
1451
+ const bare = writeBinName(raw);
1452
+ if (COMMAND_WRAPPER_BINS.has(bare))
1453
+ continue;
1454
+ if (bare.length > 0 && bare[0] !== undefined && bare[0] >= "0" && bare[0] <= "9") {
1455
+ continue;
1456
+ }
1457
+ if (firstCommand.length === 0) {
1458
+ firstCommand = bare;
1459
+ if (DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS.has(bare))
1460
+ return false;
1461
+ }
1462
+ if (inExecPayload && execPayloadFirstCommand.length === 0) {
1463
+ execPayloadFirstCommand = bare;
1464
+ }
1465
+ if (inExecPayload && DEST_ASSIGNMENT_NON_WRITER_FIRST_BINS.has(execPayloadFirstCommand)) {
1466
+ continue;
1467
+ }
1468
+ if (DEST_ASSIGNMENT_OWNER_BINS.has(bare)) {
1469
+ if (DEST_ASSIGNMENT_SEARCH_LAUNCHER_BINS.has(firstCommand) && !inExecPayload) {
1470
+ continue;
1471
+ }
1472
+ sawMake = true;
1473
+ continue;
1474
+ }
1475
+ if (sawMake)
1476
+ targets.push(n);
1477
+ }
1478
+ if (!sawMake)
1479
+ return false;
1480
+ if (targets.length === 0)
1481
+ return true;
1482
+ for (const t of targets) {
1483
+ if (!DEST_ASSIGNMENT_NON_WRITE_TARGETS.has(t))
1484
+ return true;
1485
+ }
1486
+ return false;
1487
+ }
1183
1488
  function isReadShapedInputFileFlag(bin, flag) {
1184
1489
  return READ_SHAPED_FILE_FLAG_BINS.has(bin) && READ_INPUT_FILE_FLAGS.has(flag);
1185
1490
  }
@@ -1208,13 +1513,13 @@ const GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS = new Set([
1208
1513
  "-q",
1209
1514
  ]);
1210
1515
  /**
1211
- * True only when the git *subcommand* (first non-flag token) is a dest writer.
1516
+ * True only when the VCS *subcommand* (first non-flag token) is a dest writer.
1212
1517
  * Later operands named clone/worktree/submodule (e.g. `git log worktree -- …`)
1213
1518
  * are not write subcommands (#3423 residual).
1214
1519
  * Boolean globals (`--no-pager`) do not consume the next token. Other globals
1215
1520
  * consume it even when that token spells a subcommand (#3421 residual).
1216
1521
  */
1217
- function gitHasWriteSubcommand(tokens, start) {
1522
+ function hasWriteSubcommand(tokens, start, writeSubs, booleanFlags) {
1218
1523
  let i = start;
1219
1524
  while (i < tokens.length) {
1220
1525
  const raw = tokens[i];
@@ -1222,7 +1527,7 @@ function gitHasWriteSubcommand(tokens, start) {
1222
1527
  return false;
1223
1528
  const n = normalizeToken(raw);
1224
1529
  if (n.startsWith("-")) {
1225
- if (!n.includes("=") && !GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS.has(n)) {
1530
+ if (!n.includes("=") && !booleanFlags.has(n) && !n.startsWith("--no-")) {
1226
1531
  const next = tokens[i + 1];
1227
1532
  if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
1228
1533
  i += 2;
@@ -1232,10 +1537,46 @@ function gitHasWriteSubcommand(tokens, start) {
1232
1537
  i++;
1233
1538
  continue;
1234
1539
  }
1235
- return GIT_WRITE_SUBCOMMANDS.has(n);
1540
+ return writeSubs.has(n);
1236
1541
  }
1237
1542
  return false;
1238
1543
  }
1544
+ function gitHasWriteSubcommand(tokens, start) {
1545
+ return hasWriteSubcommand(tokens, start, GIT_WRITE_SUBCOMMANDS, GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS);
1546
+ }
1547
+ /**
1548
+ * True when this git segment is `bundle create` (write), not `bundle verify` / `list-heads`
1549
+ * / `unbundle` (read of an existing bundle) (#3529 Greptile P1).
1550
+ */
1551
+ function gitHasBundleCreate(tokens, start) {
1552
+ const nonFlags = [];
1553
+ let i = start;
1554
+ while (i < tokens.length) {
1555
+ const raw = tokens[i];
1556
+ if (isShellSegmentBreak(raw))
1557
+ break;
1558
+ const n = normalizeToken(raw);
1559
+ if (n.startsWith("-")) {
1560
+ if (!n.includes("=") && !GIT_PRE_SUBCOMMAND_BOOLEAN_FLAGS.has(n) && !n.startsWith("--no-")) {
1561
+ const next = tokens[i + 1];
1562
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
1563
+ i += 2;
1564
+ continue;
1565
+ }
1566
+ }
1567
+ i++;
1568
+ continue;
1569
+ }
1570
+ nonFlags.push(n);
1571
+ if (nonFlags.length >= 2)
1572
+ break;
1573
+ i++;
1574
+ }
1575
+ return nonFlags[0] === "bundle" && GIT_BUNDLE_WRITE_SUBCOMMANDS.has(nonFlags[1] ?? "");
1576
+ }
1577
+ function jjHasWriteSubcommand(tokens, start) {
1578
+ return hasWriteSubcommand(tokens, start, JJ_WRITE_SUBCOMMANDS, JJ_PRE_SUBCOMMAND_BOOLEAN_FLAGS);
1579
+ }
1239
1580
  /**
1240
1581
  * True when this forge-CLI segment is a clone dest writer
1241
1582
  * (`gh repo clone`, `glab repo clone`, `hub clone`) (#3459).
@@ -1288,7 +1629,7 @@ function genericProtectedDests(tokens) {
1288
1629
  currentBin = "";
1289
1630
  continue;
1290
1631
  }
1291
- if (!n.startsWith("-")) {
1632
+ if (!n.startsWith("-") && !isEnvAssign(raw)) {
1292
1633
  if (isDownloaderDecoderBin(raw)) {
1293
1634
  currentBin = writeBinName(raw);
1294
1635
  }
@@ -1299,8 +1640,22 @@ function genericProtectedDests(tokens) {
1299
1640
  currentBin = writeBinName(raw);
1300
1641
  }
1301
1642
  }
1302
- if (currentBin === "scp" || currentBin === "cpio")
1643
+ if (isScpFamilyBin(currentBin) || currentBin === "cpio")
1303
1644
  continue;
1645
+ // Env-style dest assignments (`DESTDIR=`, `PREFIX=`) without a leading dash (#3545).
1646
+ if (n.includes("=") && !n.startsWith("-")) {
1647
+ const eq = raw.indexOf("=");
1648
+ const keyRaw = raw.slice(0, eq);
1649
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(keyRaw)) {
1650
+ const key = normalizeToken(keyRaw);
1651
+ if (DEST_ASSIGNMENT_KEYS.has(key) && segmentDestAssignmentIsOperative(tokens, i)) {
1652
+ const dest = pathishToken(raw.slice(eq + 1));
1653
+ if (pathishIsProtectedDest(dest))
1654
+ dests.push(dest);
1655
+ }
1656
+ }
1657
+ continue;
1658
+ }
1304
1659
  if (n.includes("=") && (n.startsWith("-") || n.startsWith("--"))) {
1305
1660
  const eq = raw.indexOf("=");
1306
1661
  const flag = normalizeToken(raw.slice(0, eq));
@@ -1311,6 +1666,17 @@ function genericProtectedDests(tokens) {
1311
1666
  }
1312
1667
  continue;
1313
1668
  }
1669
+ // Named dest flags (`-Destination`, `--output-dir`) take the next token.
1670
+ // Check before attached `-oDIR` / `-dDIR` so `-destination` is not `-d` + `estination`.
1671
+ if (isGenericProtectedDestFlag(n) && !isReadShapedInputFileFlag(currentBin, n)) {
1672
+ const next = tokens[i + 1];
1673
+ if (next !== undefined && !String(next).startsWith("-") && !isShellSegmentBreak(next)) {
1674
+ const dest = pathishToken(next);
1675
+ if (pathishIsProtectedDest(dest))
1676
+ dests.push(dest);
1677
+ }
1678
+ continue;
1679
+ }
1314
1680
  if (n.startsWith("-") &&
1315
1681
  !n.startsWith("--") &&
1316
1682
  n.startsWith("-o") &&
@@ -1325,15 +1691,6 @@ function genericProtectedDests(tokens) {
1325
1691
  const dest = pathishToken(raw.slice(2));
1326
1692
  if (pathishIsProtectedDest(dest))
1327
1693
  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
1694
  }
1338
1695
  }
1339
1696
  return dests;
@@ -1497,20 +1854,39 @@ function isProgrammaticWriteBinToken(token) {
1497
1854
  bare === "nodejs" ||
1498
1855
  bare === "perl" ||
1499
1856
  bare === "ruby" ||
1857
+ bare === "jruby" ||
1858
+ bare === "pypy" ||
1859
+ bare === "pypy3" ||
1500
1860
  bare === "pwsh" ||
1501
1861
  bare === "powershell" ||
1502
1862
  bare === "tsx" ||
1503
1863
  bare === "ts-node" ||
1504
- bare === "tsnode") {
1864
+ bare === "tsnode" ||
1865
+ bare === "bun" ||
1866
+ bare === "deno" ||
1867
+ bare === "php" ||
1868
+ bare === "lua" ||
1869
+ bare === "luajit") {
1505
1870
  return true;
1506
1871
  }
1507
- // Versioned: python3.11, python3.12, node18, …
1872
+ // Versioned: python3.11, python3.12, node18, php8.3, lua5.4,
1508
1873
  if (bare.startsWith("python3.") || bare.startsWith("python2."))
1509
1874
  return true;
1510
1875
  if (/^python\d+(\.\d+)*$/.test(bare))
1511
1876
  return true;
1512
1877
  if (/^node\d+$/.test(bare))
1513
1878
  return true;
1879
+ if (/^php\d+(\.\d+)*$/.test(bare))
1880
+ return true;
1881
+ if (/^lua\d+(\.\d+)*$/.test(bare))
1882
+ return true;
1883
+ // #3545 residual: versioned ruby / jruby / pypy (bare `ruby` already matched).
1884
+ if (/^ruby\d+(\.\d+)*$/.test(bare))
1885
+ return true;
1886
+ if (/^jruby\d+(\.\d+)*$/.test(bare))
1887
+ return true;
1888
+ if (bare.startsWith("pypy3.") || /^pypy\d+(\.\d+)*$/.test(bare))
1889
+ return true;
1514
1890
  return false;
1515
1891
  }
1516
1892
  /**
@@ -1518,6 +1894,74 @@ function isProgrammaticWriteBinToken(token) {
1518
1894
  * Escaped quotes (`\'` / `\"`) stay inside the string. Used so `print('.write(')` is not
1519
1895
  * a write API while `open(p,'w').write('x')` still matches `.write(` (Greptile conf residual).
1520
1896
  */
1897
+ /**
1898
+ * True when `ident` is followed by optional whitespace then `(` (PHP `file_put_contents (`).
1899
+ * Skips matches inside quotes so `echo "file_put_contents ($x)"` is not a write.
1900
+ * O(n) — no nested-quantifier regex.
1901
+ */
1902
+ function includesIdentCall(haystack, ident) {
1903
+ if (ident.length === 0 || haystack.length < ident.length)
1904
+ return false;
1905
+ let inSingle = false;
1906
+ let inDouble = false;
1907
+ for (let i = 0; i < haystack.length; i++) {
1908
+ const c = haystack[i];
1909
+ if (c === "\\" && i + 1 < haystack.length && (inSingle || inDouble)) {
1910
+ i++;
1911
+ continue;
1912
+ }
1913
+ if (c === "'" && !inDouble) {
1914
+ inSingle = !inSingle;
1915
+ continue;
1916
+ }
1917
+ if (c === '"' && !inSingle) {
1918
+ inDouble = !inDouble;
1919
+ continue;
1920
+ }
1921
+ if (inSingle || inDouble)
1922
+ continue;
1923
+ if (!haystack.startsWith(ident, i))
1924
+ continue;
1925
+ let j = i + ident.length;
1926
+ while (j < haystack.length) {
1927
+ const w = haystack[j];
1928
+ if (w === " " || w === "\t" || w === "\n" || w === "\r") {
1929
+ j++;
1930
+ continue;
1931
+ }
1932
+ break;
1933
+ }
1934
+ if (j < haystack.length && haystack[j] === "(")
1935
+ return true;
1936
+ }
1937
+ return false;
1938
+ }
1939
+ /** Strip matching wrapping quotes from a `-r`/`-e` payload. */
1940
+ function unwrapQuotedPayload(token) {
1941
+ if (token.length >= 2) {
1942
+ const a = token[0];
1943
+ const b = token[token.length - 1];
1944
+ if ((a === '"' && b === '"') || (a === "'" && b === "'")) {
1945
+ return token.slice(1, -1);
1946
+ }
1947
+ }
1948
+ return token;
1949
+ }
1950
+ /** Script bodies after interpreter `-r`/`-e`/`-c` (PHP/node one-liners). */
1951
+ function interpreterScriptPayloads(tokens) {
1952
+ const out = [];
1953
+ const flags = new Set(["-r", "-e", "-c", "--run", "--eval", "--execute"]);
1954
+ for (let i = 0; i < tokens.length; i++) {
1955
+ const n = normalizeToken(tokens[i]);
1956
+ if (!flags.has(n))
1957
+ continue;
1958
+ const next = tokens[i + 1];
1959
+ if (next === undefined || isShellSegmentBreak(next) || String(next).startsWith("-"))
1960
+ continue;
1961
+ out.push(unwrapQuotedPayload(next));
1962
+ }
1963
+ return out;
1964
+ }
1521
1965
  function includesOutsideQuotes(haystack, needle) {
1522
1966
  if (needle.length === 0 || haystack.length < needle.length)
1523
1967
  return false;
@@ -1544,6 +1988,85 @@ function includesOutsideQuotes(haystack, needle) {
1544
1988
  }
1545
1989
  return false;
1546
1990
  }
1991
+ /**
1992
+ * Perl `open F,">dest"` / `open FH, '>'` without parens (#3545).
1993
+ * Quote-aware so `print 'open F,>'` is not a write. O(n).
1994
+ */
1995
+ function hasPerlBareOpenWrite(haystack) {
1996
+ let inSingle = false;
1997
+ let inDouble = false;
1998
+ for (let i = 0; i < haystack.length; i++) {
1999
+ const c = haystack[i];
2000
+ if (c === "\\" && i + 1 < haystack.length && (inSingle || inDouble)) {
2001
+ i++;
2002
+ continue;
2003
+ }
2004
+ if (c === "'" && !inDouble) {
2005
+ inSingle = !inSingle;
2006
+ continue;
2007
+ }
2008
+ if (c === '"' && !inSingle) {
2009
+ inDouble = !inDouble;
2010
+ continue;
2011
+ }
2012
+ if (inSingle || inDouble)
2013
+ continue;
2014
+ if (!haystack.startsWith("open", i))
2015
+ continue;
2016
+ if (i > 0) {
2017
+ const prev = haystack[i - 1];
2018
+ if ((prev >= "a" && prev <= "z") || (prev >= "0" && prev <= "9") || prev === "_")
2019
+ continue;
2020
+ }
2021
+ let j = i + 4;
2022
+ while (j < haystack.length) {
2023
+ const w = haystack[j];
2024
+ if (w === " " || w === "\t" || w === "\n" || w === "\r") {
2025
+ j++;
2026
+ continue;
2027
+ }
2028
+ break;
2029
+ }
2030
+ const identStart = j;
2031
+ while (j < haystack.length) {
2032
+ const ch = haystack[j];
2033
+ if ((ch >= "a" && ch <= "z") ||
2034
+ (ch >= "A" && ch <= "Z") ||
2035
+ (ch >= "0" && ch <= "9") ||
2036
+ ch === "_") {
2037
+ j++;
2038
+ continue;
2039
+ }
2040
+ break;
2041
+ }
2042
+ if (j === identStart)
2043
+ continue;
2044
+ while (j < haystack.length) {
2045
+ const w = haystack[j];
2046
+ if (w === " " || w === "\t") {
2047
+ j++;
2048
+ continue;
2049
+ }
2050
+ break;
2051
+ }
2052
+ if (j >= haystack.length || haystack[j] !== ",")
2053
+ continue;
2054
+ j++;
2055
+ while (j < haystack.length) {
2056
+ const w = haystack[j];
2057
+ if (w === " " || w === "\t") {
2058
+ j++;
2059
+ continue;
2060
+ }
2061
+ break;
2062
+ }
2063
+ const q = haystack[j];
2064
+ if ((q === "'" || q === '"') && j + 1 < haystack.length && haystack[j + 1] === ">") {
2065
+ return true;
2066
+ }
2067
+ }
2068
+ return false;
2069
+ }
1547
2070
  /**
1548
2071
  * True when command uses a programmatic bin with a **write** API, optionally with
1549
2072
  * path-obfuscation markers (base64/bytes/chr). Pure reads / print-only stay unclassifiable.
@@ -1574,6 +2097,8 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
1574
2097
  const writeApi = lower.includes("writefilesync") ||
1575
2098
  lower.includes("writefile") ||
1576
2099
  lower.includes("writetext") ||
2100
+ includesIdentCall(lower, "file_put_contents") ||
2101
+ interpreterScriptPayloads(tokens).some((p) => includesIdentCall(p.toLowerCase(), "file_put_contents")) ||
1577
2102
  lower.includes("set-content") ||
1578
2103
  lower.includes("out-file") ||
1579
2104
  lower.includes("fs.write") ||
@@ -1581,6 +2106,17 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
1581
2106
  lower.includes("path.write(") ||
1582
2107
  lower.includes("file.write(") ||
1583
2108
  lower.includes("spurt") ||
2109
+ interpreterScriptPayloads(tokens).some((p) => {
2110
+ const pl = p.toLowerCase();
2111
+ return (includesIdentCall(pl, "write_file") ||
2112
+ includesIdentCall(pl, "spew") ||
2113
+ includesOutsideQuotes(pl, "->spew") ||
2114
+ hasPerlBareOpenWrite(pl));
2115
+ }) ||
2116
+ includesIdentCall(lower, "write_file") ||
2117
+ includesIdentCall(lower, "spew") ||
2118
+ includesOutsideQuotes(lower, "->spew") ||
2119
+ hasPerlBareOpenWrite(lower) ||
1584
2120
  lower.includes(">>") ||
1585
2121
  lower.includes("unlink(") ||
1586
2122
  lower.includes("rmsync") ||
@@ -1618,7 +2154,13 @@ function hasWriteCapableProgrammaticShell(command, tokens) {
1618
2154
  lower.includes("mode='a") ||
1619
2155
  lower.includes('mode="a') ||
1620
2156
  lower.includes("mode=w"));
1621
- const writeish = writeApi || openWriteMode;
2157
+ // Perl `open F,">dest"` `open(` is absent; `-e` payloads with spaces split tokens (#3545).
2158
+ const perlBareOpenWrite = (lower.includes("open ") || lower.includes("open\t")) &&
2159
+ (lower.includes(",'>") ||
2160
+ lower.includes(',">') ||
2161
+ lower.includes(", '>") ||
2162
+ lower.includes(', ">'));
2163
+ const writeish = writeApi || openWriteMode || perlBareOpenWrite;
1622
2164
  // Path construction that hides the destination from literal classifiers.
1623
2165
  const obfuscatedPath = lower.includes("base64") ||
1624
2166
  lower.includes("fromcharcode") ||
@@ -1833,6 +2375,29 @@ const INDIRECT_WRITE_BINS = new Set([
1833
2375
  "touch",
1834
2376
  "tsx",
1835
2377
  "ts-node",
2378
+ // #3529 residual: Windows dest writers / rename / moreutils sponge / runtimes.
2379
+ "xcopy",
2380
+ "robocopy",
2381
+ "move",
2382
+ "ren",
2383
+ "rename",
2384
+ "rename-item",
2385
+ "sponge",
2386
+ "bun",
2387
+ "deno",
2388
+ "php",
2389
+ "lua",
2390
+ "start-bitstransfer",
2391
+ "bitsadmin",
2392
+ "expand-archive",
2393
+ "tee-object",
2394
+ "export-csv",
2395
+ "expand",
2396
+ "extrac32",
2397
+ "makecab",
2398
+ "replace",
2399
+ // #3545 residual: dest writers (fromdos/degit/ddrescue). dpkg/pdftk/gs stay dest-flag gated.
2400
+ ...UAT_RESIDUAL_INPLACE_WRITE_BINS_3545,
1836
2401
  ]);
1837
2402
  /**
1838
2403
  * O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
@@ -2035,15 +2600,7 @@ function hasProgrammaticAuthzEnvWrite(command, tokens) {
2035
2600
  const lower = command.toLowerCase();
2036
2601
  let hasProg = false;
2037
2602
  for (const t of tokens) {
2038
- const n = normalizeToken(t);
2039
- if (n === "python" ||
2040
- n === "python3" ||
2041
- n === "node" ||
2042
- n === "nodejs" ||
2043
- n === "perl" ||
2044
- n === "ruby" ||
2045
- n === "pwsh" ||
2046
- n === "powershell") {
2603
+ if (isProgrammaticWriteBinToken(t)) {
2047
2604
  hasProg = true;
2048
2605
  break;
2049
2606
  }