@deftai/directive-core 0.93.0 → 0.95.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 (118) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/cache/fetch.d.ts +29 -0
  3. package/dist/cache/fetch.js +131 -4
  4. package/dist/cache/operations.d.ts +8 -0
  5. package/dist/cache/operations.js +67 -4
  6. package/dist/check/cached-orchestrator.js +11 -0
  7. package/dist/check/consumer-gate-integrity.d.ts +68 -0
  8. package/dist/check/consumer-gate-integrity.js +265 -0
  9. package/dist/check/index.d.ts +1 -0
  10. package/dist/check/index.js +1 -0
  11. package/dist/check/orchestrator.js +10 -0
  12. package/dist/content-contracts/skills/greptile-detector.d.ts +9 -0
  13. package/dist/content-contracts/skills/greptile-detector.js +9 -2
  14. package/dist/doctor/main.js +98 -9
  15. package/dist/doctor/types.d.ts +2 -2
  16. package/dist/finish-loop/pr-finish-loop.js +1 -0
  17. package/dist/freshness/bind.d.ts +67 -0
  18. package/dist/freshness/bind.js +201 -0
  19. package/dist/freshness/cli.d.ts +30 -0
  20. package/dist/freshness/cli.js +180 -0
  21. package/dist/freshness/compare.d.ts +14 -0
  22. package/dist/freshness/compare.js +134 -0
  23. package/dist/freshness/generation.d.ts +44 -0
  24. package/dist/freshness/generation.js +172 -0
  25. package/dist/freshness/index.d.ts +13 -0
  26. package/dist/freshness/index.js +13 -0
  27. package/dist/freshness/report.d.ts +43 -0
  28. package/dist/freshness/report.js +139 -0
  29. package/dist/freshness/types.d.ts +70 -0
  30. package/dist/freshness/types.js +30 -0
  31. package/dist/handoff-evidence/index.d.ts +8 -0
  32. package/dist/handoff-evidence/index.js +7 -0
  33. package/dist/handoff-evidence/validate.d.ts +105 -0
  34. package/dist/handoff-evidence/validate.js +423 -0
  35. package/dist/hooks/dispatcher.d.ts +27 -2
  36. package/dist/hooks/dispatcher.js +134 -14
  37. package/dist/hooks/readonly.d.ts +14 -0
  38. package/dist/hooks/readonly.js +126 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +2 -0
  41. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  42. package/dist/init-deposit/agent-hooks.js +5 -2
  43. package/dist/init-deposit/init-deposit.d.ts +7 -1
  44. package/dist/init-deposit/init-deposit.js +24 -4
  45. package/dist/init-deposit/refresh.d.ts +9 -1
  46. package/dist/init-deposit/refresh.js +48 -5
  47. package/dist/intake/issue-ingest.d.ts +20 -0
  48. package/dist/intake/issue-ingest.js +58 -0
  49. package/dist/policy/index.d.ts +4 -0
  50. package/dist/policy/index.js +50 -1
  51. package/dist/policy/min-greptile-confidence.d.ts +57 -0
  52. package/dist/policy/min-greptile-confidence.js +123 -0
  53. package/dist/pr-merge-readiness/compute.d.ts +11 -1
  54. package/dist/pr-merge-readiness/compute.js +17 -4
  55. package/dist/pr-merge-readiness/evaluate.d.ts +8 -1
  56. package/dist/pr-merge-readiness/evaluate.js +7 -4
  57. package/dist/pr-merge-readiness/mergeability.d.ts +7 -1
  58. package/dist/pr-merge-readiness/mergeability.js +14 -2
  59. package/dist/pr-monitor/main.d.ts +2 -0
  60. package/dist/pr-monitor/main.js +31 -1
  61. package/dist/pr-monitor/monitor.js +5 -1
  62. package/dist/pr-monitor/readiness.js +3 -1
  63. package/dist/pr-monitor/types.d.ts +8 -0
  64. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  65. package/dist/pr-wait-mergeable/cascade.js +28 -1
  66. package/dist/pr-wait-mergeable/main.d.ts +2 -0
  67. package/dist/pr-wait-mergeable/main.js +16 -0
  68. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  69. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
  70. package/dist/pr-wait-mergeable/wrappers.js +3 -0
  71. package/dist/pr-watch/constants.d.ts +4 -1
  72. package/dist/pr-watch/constants.js +5 -2
  73. package/dist/pr-watch/main.js +1 -0
  74. package/dist/pr-watch/probe.d.ts +1 -1
  75. package/dist/pr-watch/probe.js +4 -1
  76. package/dist/pr-watch/types.d.ts +7 -1
  77. package/dist/pr-watch/watch.js +3 -2
  78. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  79. package/dist/review-monitor/index.d.ts +1 -0
  80. package/dist/review-monitor/index.js +1 -0
  81. package/dist/review-monitor/l4-owner.d.ts +46 -0
  82. package/dist/review-monitor/l4-owner.js +262 -0
  83. package/dist/session/ritual-entrypoint.d.ts +6 -2
  84. package/dist/session/ritual-entrypoint.js +15 -2
  85. package/dist/session/session-ready.js +31 -14
  86. package/dist/session/session-start.d.ts +2 -1
  87. package/dist/session/session-start.js +66 -3
  88. package/dist/session/verify-session-ritual.d.ts +4 -2
  89. package/dist/session/verify-session-ritual.js +11 -11
  90. package/dist/slash/product-set.js +4 -1
  91. package/dist/swarm/verify-review-clean.d.ts +4 -3
  92. package/dist/swarm/verify-review-clean.js +28 -73
  93. package/dist/tool-events/classify.js +1 -0
  94. package/dist/triage/classify/index.d.ts +9 -0
  95. package/dist/triage/classify/index.js +34 -2
  96. package/dist/triage/classify/label-mirror.d.ts +143 -0
  97. package/dist/triage/classify/label-mirror.js +684 -0
  98. package/dist/triage/help/registry-data.d.ts +7 -7
  99. package/dist/triage/help/registry-data.js +27 -7
  100. package/dist/triage/queue/show.d.ts +1 -1
  101. package/dist/triage/queue/show.js +4 -2
  102. package/dist/triage/welcome/writers.js +16 -7
  103. package/dist/umbrella-current-shape/index.d.ts +45 -0
  104. package/dist/umbrella-current-shape/index.js +162 -8
  105. package/dist/vbrief-reconcile/index.d.ts +2 -2
  106. package/dist/vbrief-reconcile/index.js +1 -1
  107. package/dist/vbrief-reconcile/types.d.ts +14 -0
  108. package/dist/vbrief-reconcile/umbrellas.d.ts +31 -2
  109. package/dist/vbrief-reconcile/umbrellas.js +231 -19
  110. package/dist/vbrief-validate/project-definition.js +1 -1
  111. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  112. package/dist/verify-env/agent-hook-readiness.js +150 -0
  113. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  114. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  115. package/dist/verify-env/agent-hooks.js +11 -3
  116. package/dist/verify-env/index.d.ts +1 -0
  117. package/dist/verify-env/index.js +1 -0
  118. package/package.json +7 -3
@@ -145,6 +145,442 @@ function hasGhApiPath(tokens, needle) {
145
145
  }
146
146
  return false;
147
147
  }
148
+ /**
149
+ * Authz authority-mutating CLI verbs (#3110). Classified as **settings** so under
150
+ * active UAT they deny without a prior human grant — never empty → shell-op-unclassifiable fail-open.
151
+ */
152
+ const AUTHZ_MUTATING_SUBCOMMANDS = new Set(["grant", "uat-start", "uat-suspend", "revoke"]);
153
+ function authzSubcommandFromToken(token) {
154
+ const t = normalizeToken(token);
155
+ if (t.startsWith("authz:")) {
156
+ const sub = t.slice("authz:".length);
157
+ return AUTHZ_MUTATING_SUBCOMMANDS.has(sub) ? sub : null;
158
+ }
159
+ return AUTHZ_MUTATING_SUBCOMMANDS.has(t) ? t : null;
160
+ }
161
+ /**
162
+ * Detect `deft|task|directive authz:grant` / `authz grant` (and wrappers) in shell tokens.
163
+ * O(n) token walk — no nested-quantifier regex on untrusted input.
164
+ */
165
+ function hasAuthzMutatingCli(tokens) {
166
+ for (let i = 0; i < tokens.length; i++) {
167
+ const raw = tokens[i];
168
+ if (raw === undefined)
169
+ break;
170
+ const t = normalizeToken(raw);
171
+ // Combined form anywhere: authz:grant / authz:uat-suspend / …
172
+ if (authzSubcommandFromToken(t) !== null && t.startsWith("authz:")) {
173
+ return true;
174
+ }
175
+ // Separated form: … authz grant|uat-start|uat-suspend|revoke
176
+ // Also path-ish bins ending in /authz or \authz (node …/authz.js grant).
177
+ const isAuthzBin = t === "authz" ||
178
+ t.endsWith("/authz") ||
179
+ t.endsWith("\\authz") ||
180
+ t.endsWith("/authz.js") ||
181
+ t.endsWith("\\authz.js") ||
182
+ t.endsWith("/authz.ts") ||
183
+ t.endsWith("\\authz.ts");
184
+ if (!isAuthzBin)
185
+ continue;
186
+ const next = tokens[i + 1] !== undefined ? normalizeToken(tokens[i + 1]) : "";
187
+ if (authzSubcommandFromToken(next) !== null)
188
+ return true;
189
+ }
190
+ return false;
191
+ }
192
+ /**
193
+ * Path-ish normalize: keep separators (do not strip `\` like normalizeToken).
194
+ */
195
+ function pathishToken(token) {
196
+ return token.replace(/['"]/g, "").toLowerCase().replace(/\\/g, "/");
197
+ }
198
+ /**
199
+ * Shell **write** targeting `.deft/authz/` (#3110 AC-3).
200
+ * Pure reads (`cat .deft/authz/state.json`) stay unclassifiable — use `authz:show`.
201
+ * Redirects only count when the destination region contains `.deft/authz`.
202
+ */
203
+ function hasAuthzDirShellWrite(command, tokens) {
204
+ const lower = command.toLowerCase().replace(/\\/g, "/");
205
+ if (!lower.includes(".deft/authz"))
206
+ return false;
207
+ // Redirect dest region after each `>` / `>>` (O(n); no nested-quantifier regex).
208
+ for (let i = 0; i < lower.length; i++) {
209
+ if (lower[i] !== ">")
210
+ continue;
211
+ let j = i + 1;
212
+ if (j < lower.length && lower[j] === ">")
213
+ j++;
214
+ // Dest until pipe/semicolon/ampersand/newline.
215
+ let end = j;
216
+ while (end < lower.length &&
217
+ lower[end] !== "|" &&
218
+ lower[end] !== ";" &&
219
+ lower[end] !== "&" &&
220
+ lower[end] !== "\n") {
221
+ end++;
222
+ }
223
+ if (lower.slice(j, end).includes(".deft/authz"))
224
+ return true;
225
+ }
226
+ // Write/destructive bins with an authz path argument.
227
+ for (let ti = 0; ti < tokens.length; ti++) {
228
+ if (!INDIRECT_WRITE_BINS.has(normalizeToken(tokens[ti])))
229
+ continue;
230
+ for (let tj = ti + 1; tj < tokens.length; tj++) {
231
+ if (pathishToken(tokens[tj]).includes(".deft/authz"))
232
+ return true;
233
+ }
234
+ }
235
+ return false;
236
+ }
237
+ /** Write/destructive shell bins (token match after normalizeToken). */
238
+ const INDIRECT_WRITE_BINS = new Set([
239
+ "dd",
240
+ "sed",
241
+ "tee",
242
+ "cp",
243
+ "mv",
244
+ "rsync",
245
+ "rm",
246
+ "rmdir",
247
+ "unlink",
248
+ "shred",
249
+ "truncate",
250
+ "chmod",
251
+ "chown",
252
+ "install",
253
+ "python",
254
+ "python3",
255
+ "node",
256
+ "perl",
257
+ "ruby",
258
+ "pwsh",
259
+ "powershell",
260
+ "set-content",
261
+ "out-file",
262
+ "add-content",
263
+ "copy-item",
264
+ "move-item",
265
+ "remove-item",
266
+ "ri",
267
+ "ni",
268
+ "sc",
269
+ "mi",
270
+ ]);
271
+ /**
272
+ * O(n): true when command expands `$…` / `` `…` `` / `%VAR%`
273
+ * (no nested-quantifier regex). Includes command substitution and positional `$1`.
274
+ */
275
+ function hasEnvExpansion(command) {
276
+ for (let i = 0; i < command.length; i++) {
277
+ const c = command[i];
278
+ if (c === "`")
279
+ return true;
280
+ if (c === "$" && i + 1 < command.length) {
281
+ const n = command[i + 1];
282
+ // $VAR / ${VAR} / $(cmd) / $1 / $@ / $* / $? / $'…' (ANSI-C)
283
+ if (n === "{" ||
284
+ n === "(" ||
285
+ n === "_" ||
286
+ n === "'" ||
287
+ n === "@" ||
288
+ n === "*" ||
289
+ n === "?" ||
290
+ n === "#" ||
291
+ n === "!" ||
292
+ (n >= "0" && n <= "9") ||
293
+ (n >= "A" && n <= "Z") ||
294
+ (n >= "a" && n <= "z")) {
295
+ return true;
296
+ }
297
+ }
298
+ if (c === "%" && i + 1 < command.length) {
299
+ const n = command[i + 1];
300
+ if (n === "_" || (n >= "A" && n <= "Z") || (n >= "a" && n <= "z")) {
301
+ return true;
302
+ }
303
+ }
304
+ }
305
+ return false;
306
+ }
307
+ function hasWriteShape(command, tokens) {
308
+ if (command.includes(">"))
309
+ return true;
310
+ for (const t of tokens) {
311
+ if (INDIRECT_WRITE_BINS.has(normalizeToken(t)))
312
+ return true;
313
+ }
314
+ return false;
315
+ }
316
+ /**
317
+ * Split-path containment: `.deft` and `authz` both appear (e.g. `cd .deft && … authz/…`).
318
+ * O(n) substring checks — no nested-quantifier regex.
319
+ */
320
+ function hasSplitAuthzPath(command) {
321
+ const lower = command.toLowerCase().replace(/\\/g, "/");
322
+ if (!lower.includes("authz"))
323
+ return false;
324
+ return lower.includes(".deft") || lower.includes("/deft/") || lower.includes("deft/");
325
+ }
326
+ /**
327
+ * Last non-flag token is a pure expansion dest (`$STORE`, `${STORE}`, `%TEMP%`)
328
+ * with no trailing path segment (`$HOME/out` is NOT pure — ordinary user write).
329
+ */
330
+ function lastTokenIsOpaqueExpansion(tokens) {
331
+ let last = "";
332
+ for (const t of tokens) {
333
+ if (t.startsWith("-"))
334
+ continue;
335
+ last = t;
336
+ }
337
+ if (last.length === 0)
338
+ return false;
339
+ const n = last.replace(/['"]/g, "");
340
+ // Path after expansion → ordinary dest, not opaque store alias.
341
+ if (n.includes("/") || n.includes("\\"))
342
+ return false;
343
+ if (n.startsWith("$") && n.length > 1)
344
+ return true;
345
+ if (n.startsWith("%") && n.endsWith("%") && n.length > 2)
346
+ return true;
347
+ return false;
348
+ }
349
+ /** Env / path tokens that are ordinary non-store destinations (not authz containment). */
350
+ const ORDINARY_EXPANSION_PREFIXES = [
351
+ "home",
352
+ "tmpdir",
353
+ "temp",
354
+ "tmp",
355
+ "pwd",
356
+ "user",
357
+ "username",
358
+ "userprofile",
359
+ "xdg_",
360
+ "path",
361
+ "psmodulepath",
362
+ "appdata",
363
+ "localappdata",
364
+ "programfiles",
365
+ "systemroot",
366
+ "windir",
367
+ "shell",
368
+ "term",
369
+ "color",
370
+ "lang",
371
+ "lc_",
372
+ "editor",
373
+ "visual",
374
+ "pager",
375
+ "browser",
376
+ "http",
377
+ "https",
378
+ "proxy",
379
+ "npm_",
380
+ "pnpm_",
381
+ "yarn_",
382
+ "node_",
383
+ "python",
384
+ "virtual_env",
385
+ "conda",
386
+ "cargo",
387
+ "go",
388
+ "java",
389
+ "ssh",
390
+ "gpg",
391
+ "git_",
392
+ "gh_",
393
+ "github_",
394
+ "ci",
395
+ "tf_",
396
+ "aws_",
397
+ "azure",
398
+ "gcloud",
399
+ ];
400
+ /**
401
+ * True when the expansion name itself suggests authz / grant store
402
+ * (e.g. $AUTHZ_DIR, $DEFT_AUTHZ_ROOT, %GRANT_STORE%) — residual path without keywords.
403
+ */
404
+ function hasAuthzPlausibleExpansionName(command) {
405
+ const lower = command.toLowerCase();
406
+ // O(n) scan for $NAME / ${NAME} / %NAME% containing authz/grant/store store-ish tokens.
407
+ for (let i = 0; i < lower.length; i++) {
408
+ const c = lower[i];
409
+ if (c === "$" && i + 1 < lower.length) {
410
+ let j = i + 1;
411
+ if (lower[j] === "{" || lower[j] === "(")
412
+ j++;
413
+ let name = "";
414
+ while (j < lower.length) {
415
+ const ch = lower[j];
416
+ if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_") {
417
+ name += ch;
418
+ j++;
419
+ continue;
420
+ }
421
+ break;
422
+ }
423
+ if (nameLooksAuthzStore(name))
424
+ return true;
425
+ }
426
+ if (c === "%" && i + 1 < lower.length) {
427
+ let j = i + 1;
428
+ let name = "";
429
+ while (j < lower.length && lower[j] !== "%") {
430
+ const ch = lower[j];
431
+ if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9") || ch === "_") {
432
+ name += ch;
433
+ j++;
434
+ continue;
435
+ }
436
+ break;
437
+ }
438
+ if (nameLooksAuthzStore(name))
439
+ return true;
440
+ }
441
+ }
442
+ return false;
443
+ }
444
+ function nameLooksAuthzStore(name) {
445
+ if (name.length === 0)
446
+ return false;
447
+ if (name.includes("authz") ||
448
+ name.includes("grant") ||
449
+ name === "store" ||
450
+ name.endsWith("_store") ||
451
+ name.startsWith("store_") ||
452
+ name.includes("deft_auth") ||
453
+ name.includes("auth_store")) {
454
+ // Exclude ordinary false friends if any appear later.
455
+ return true;
456
+ }
457
+ return false;
458
+ }
459
+ /**
460
+ * Programmatic env write to store: python/node open(os.environ[...]) / process.env patterns
461
+ * that lack shell `$` expansion but still hit authz paths (#3110 residual).
462
+ */
463
+ function hasProgrammaticAuthzEnvWrite(command, tokens) {
464
+ const lower = command.toLowerCase();
465
+ let hasProg = false;
466
+ for (const t of tokens) {
467
+ const n = normalizeToken(t);
468
+ if (n === "python" ||
469
+ n === "python3" ||
470
+ n === "node" ||
471
+ n === "nodejs" ||
472
+ n === "perl" ||
473
+ n === "ruby" ||
474
+ n === "pwsh" ||
475
+ n === "powershell") {
476
+ hasProg = true;
477
+ break;
478
+ }
479
+ }
480
+ if (!hasProg)
481
+ return false;
482
+ // Must look like a write (open/write/writefile/set-content) not a pure read.
483
+ const writeish = lower.includes("open(") ||
484
+ lower.includes(".write") ||
485
+ lower.includes("writefile") ||
486
+ lower.includes("writetext") ||
487
+ lower.includes("set-content") ||
488
+ lower.includes("out-file") ||
489
+ lower.includes("fs.write") ||
490
+ lower.includes("createwritestream") ||
491
+ lower.includes(">>") ||
492
+ lower.includes("mode='w'") ||
493
+ lower.includes('mode="w"') ||
494
+ lower.includes(",'w'") ||
495
+ lower.includes(',"w"');
496
+ if (!writeish)
497
+ return false;
498
+ // Authz-store target only — bare `state.json` alone is ordinary app state (#3110 residual).
499
+ if (lower.includes("authz") ||
500
+ lower.includes("/grants/") ||
501
+ lower.includes("grant-") ||
502
+ lower.includes("deft_auth") ||
503
+ lower.includes("auth_store") ||
504
+ lower.includes(".deft/authz") ||
505
+ lower.includes(".deft\\authz")) {
506
+ return true;
507
+ }
508
+ // os.environ / process.env + authz-store-ish key (not generic "auth"/"store" alone).
509
+ if (lower.includes("os.environ") ||
510
+ lower.includes("process.env") ||
511
+ lower.includes("$env:") ||
512
+ lower.includes("getenv")) {
513
+ if (lower.includes("authz") ||
514
+ lower.includes("grant") ||
515
+ lower.includes("deft_auth") ||
516
+ lower.includes("auth_store")) {
517
+ return true;
518
+ }
519
+ }
520
+ return false;
521
+ }
522
+ /**
523
+ * Indirect shell FS mutation that can plausibly hit the authz store (#3110).
524
+ * Narrower than "any write + any expansion" (avoids denying `echo > $HOME/out` under UAT)
525
+ * but still catches opaque `$STORE` dest, `rm -rf $STORE`, authz-named expansions,
526
+ * and programmatic os.environ writes. Does **not** flag ordinary cleanup `rm $TMP/x`.
527
+ * O(n) walks — no polynomial regex on input.
528
+ */
529
+ function hasIndirectAuthzStoreWrite(command, tokens) {
530
+ if (hasProgrammaticAuthzEnvWrite(command, tokens))
531
+ return true;
532
+ if (!hasWriteShape(command, tokens))
533
+ return false;
534
+ const lower = command.toLowerCase().replace(/\\/g, "/");
535
+ // Authz-plausible destination text (literal or expanded path segments).
536
+ // Bare `state.json` alone is ordinary app state — require authz/grants/.deft context
537
+ // (Greptile residual: do not deny unrelated expanded state-file writes under UAT).
538
+ if (lower.includes("authz") ||
539
+ lower.includes("/grants/") ||
540
+ lower.includes("grant-") ||
541
+ lower.includes(".deft/authz") ||
542
+ (lower.includes("state.json") &&
543
+ (lower.includes("authz") ||
544
+ lower.includes(".deft") ||
545
+ lower.includes("/grants/") ||
546
+ hasAuthzPlausibleExpansionName(command)))) {
547
+ // Require write shape already true; still need expansion OR already handled by literal path.
548
+ // When expansion is absent, hasAuthzDirShellWrite covers literals; here catch expanded.
549
+ if (hasEnvExpansion(command) || hasAuthzPlausibleExpansionName(command))
550
+ return true;
551
+ }
552
+ if (!hasEnvExpansion(command))
553
+ return false;
554
+ // Expansion var **name** suggests store (e.g. $AUTHZ_DIR without "authz" path text after).
555
+ if (hasAuthzPlausibleExpansionName(command))
556
+ return true;
557
+ // Destructive bins: only pure-opaque dest or authz-named expansion — not `rm $TMP/build`.
558
+ let destructive = false;
559
+ for (const t of tokens) {
560
+ const n = normalizeToken(t);
561
+ if (n === "rm" ||
562
+ n === "rmdir" ||
563
+ n === "unlink" ||
564
+ n === "shred" ||
565
+ n === "remove-item" ||
566
+ n === "ri") {
567
+ destructive = true;
568
+ break;
569
+ }
570
+ }
571
+ if (destructive && lastTokenIsOpaqueExpansion(tokens))
572
+ return true;
573
+ // cp/mv/tee/redirect dest is only `$VAR` / `%VAR%` (opaque absolute store path).
574
+ // Skip ordinary well-known env prefixes ($HOME, $TMPDIR, …).
575
+ if (lastTokenIsOpaqueExpansion(tokens)) {
576
+ const last = [...tokens].reverse().find((t) => !t.startsWith("-")) ?? "";
577
+ const bare = last.replace(/['"%${}]/g, "").toLowerCase();
578
+ if (!ORDINARY_EXPANSION_PREFIXES.some((p) => bare === p || bare.startsWith(p))) {
579
+ return true;
580
+ }
581
+ }
582
+ return false;
583
+ }
148
584
  /** Best-effort shell classification for UAT-sensitive ops beyond push/merge. */
149
585
  export function classifyShellAuthzOps(command) {
150
586
  const cmd = command.trim();
@@ -180,6 +616,55 @@ export function classifyShellAuthzOps(command) {
180
616
  found.add("test");
181
617
  if (hasDeploy(tokens))
182
618
  found.add("deployment");
619
+ // #3110: authz authority CLI + store **writes** (literal / split / $VAR / rm) → settings.
620
+ if (hasAuthzMutatingCli(tokens))
621
+ found.add("settings");
622
+ if (hasAuthzDirShellWrite(cmd, tokens))
623
+ found.add("settings");
624
+ // Split path write: `cd .deft && echo x > authz/state.json` OR `cd .deft/authz && echo x > state.json`
625
+ // OR `cd .deft/authz && cp … grants/x` (write bin without redirect).
626
+ // When the command cds into an authz path, any write shape is settings (relative dest has no "authz" text).
627
+ {
628
+ let cdsIntoAuthz = false;
629
+ for (let ti = 0; ti < tokens.length - 1; ti++) {
630
+ const bin = normalizeToken(tokens[ti]);
631
+ if (bin !== "cd" && bin !== "pushd" && bin !== "set-location" && bin !== "sl")
632
+ continue;
633
+ const dest = pathishToken(tokens[ti + 1]);
634
+ if (dest.includes("authz")) {
635
+ cdsIntoAuthz = true;
636
+ break;
637
+ }
638
+ }
639
+ if (cdsIntoAuthz && hasWriteShape(cmd, tokens)) {
640
+ found.add("settings");
641
+ }
642
+ else if (hasSplitAuthzPath(cmd) && cmd.includes(">")) {
643
+ // Scan every `>` region — not only the last — so a later `> /tmp/x` cannot hide an earlier store write.
644
+ const lower = cmd.toLowerCase().replace(/\\/g, "/");
645
+ for (let i = 0; i < lower.length; i++) {
646
+ if (lower[i] !== ">")
647
+ continue;
648
+ let j = i + 1;
649
+ if (j < lower.length && lower[j] === ">")
650
+ j++;
651
+ let end = j;
652
+ while (end < lower.length &&
653
+ lower[end] !== "|" &&
654
+ lower[end] !== ";" &&
655
+ lower[end] !== "&" &&
656
+ lower[end] !== "\n") {
657
+ end++;
658
+ }
659
+ if (lower.slice(j, end).includes("authz")) {
660
+ found.add("settings");
661
+ break;
662
+ }
663
+ }
664
+ }
665
+ }
666
+ if (hasIndirectAuthzStoreWrite(cmd, tokens))
667
+ found.add("settings");
183
668
  return [...found];
184
669
  }
185
670
  /** Map a PreToolUse tool name + optional shell command to authz ops. */
@@ -32,9 +32,36 @@ export type PaginatedLister = (repo: string, options?: {
32
32
  }) => Array<Record<string, unknown>>;
33
33
  export type SleepFn = (seconds: number) => void;
34
34
  export type ProgressWriter = (line: string) => void;
35
+ export type IssueCommentsFetcher = (repo: string, issueNumber: number) => readonly Record<string, unknown>[];
36
+ /**
37
+ * Default REST fetch for an issue's comment thread (#1870).
38
+ * Returns raw REST comment objects (with id/body/html_url/author_association).
39
+ */
40
+ export declare function restIssueComments(repo: string, issueNumber: number, options?: {
41
+ runGhApiFn?: RunGhApiFn;
42
+ }): Record<string, unknown>[];
35
43
  export declare function setSingleIssueFetcher(fn: (repo: string, n: number) => Record<string, unknown>): void;
44
+ export declare function setIssueCommentsFetcher(fn: IssueCommentsFetcher): void;
36
45
  export declare function setPaginatedLister(fn: PaginatedLister): void;
37
46
  export declare function setSleepFn(fn: SleepFn): void;
47
+ /**
48
+ * Attach the issue comment thread onto a raw issue payload when the issue is
49
+ * umbrella/tracker-like (#1870). Non-umbrellas skip the extra REST call.
50
+ * Failures leave the payload unchanged so body-only cache still lands.
51
+ *
52
+ * When `force` is true, re-fetches even if a comments array is already present
53
+ * (closed-state refresh and shape backfill paths).
54
+ */
55
+ export declare function enrichRawWithCommentsIfUmbrella(repo: string, raw: Record<string, unknown>, options?: {
56
+ fetchComments?: IssueCommentsFetcher;
57
+ force?: boolean;
58
+ }): Record<string, unknown>;
59
+ /**
60
+ * True when an on-disk cache entry is umbrella-like but was written before
61
+ * #1870 comment enrichment (no `comments` key on raw.json). Used to override
62
+ * the TTL-fresh skip so body-only entries get one backfill pass.
63
+ */
64
+ export declare function entryNeedsCurrentShapeBackfill(edir: string): boolean;
38
65
  export declare function setProgressWriter(fn: ProgressWriter, flusher?: () => void): void;
39
66
  export declare function detectRateLimit(stderr: string): [boolean, number];
40
67
  export declare function restIssueListPaginated(repo: string, options?: {
@@ -73,6 +100,7 @@ export declare function runFetchAll(options: {
73
100
  probeDriftFn?: typeof probeCacheDrift;
74
101
  isFresh?: (metaPath: string) => boolean;
75
102
  doPut?: (key: string, raw: Record<string, unknown>) => void;
103
+ fetchComments?: IssueCommentsFetcher;
76
104
  }): FetchAllReportImpl;
77
105
  export declare function cacheFetchAll(options: {
78
106
  source: string;
@@ -88,6 +116,7 @@ export declare function cacheFetchAll(options: {
88
116
  force?: boolean;
89
117
  contentDriftNumbers?: readonly number[];
90
118
  probeDriftFn?: typeof probeCacheDrift;
119
+ fetchComments?: IssueCommentsFetcher;
91
120
  }): FetchAllReportImpl;
92
121
  export declare class StateRefreshReportImpl implements StateRefreshReport {
93
122
  revisited: number;