@deftai/directive-core 0.94.0 → 0.96.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 (99) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/cache/archive.d.ts +134 -0
  3. package/dist/cache/archive.js +630 -0
  4. package/dist/cache/index.d.ts +1 -0
  5. package/dist/cache/index.js +1 -0
  6. package/dist/cache/main.js +298 -1
  7. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  8. package/dist/content-contracts/skills/helpers.js +1 -0
  9. package/dist/doctor/main.js +41 -9
  10. package/dist/doctor/types.d.ts +2 -2
  11. package/dist/freshness/bind.d.ts +67 -0
  12. package/dist/freshness/bind.js +201 -0
  13. package/dist/freshness/cli.d.ts +30 -0
  14. package/dist/freshness/cli.js +180 -0
  15. package/dist/freshness/compare.d.ts +14 -0
  16. package/dist/freshness/compare.js +134 -0
  17. package/dist/freshness/generation.d.ts +44 -0
  18. package/dist/freshness/generation.js +172 -0
  19. package/dist/freshness/index.d.ts +13 -0
  20. package/dist/freshness/index.js +13 -0
  21. package/dist/freshness/report.d.ts +43 -0
  22. package/dist/freshness/report.js +139 -0
  23. package/dist/freshness/types.d.ts +70 -0
  24. package/dist/freshness/types.js +30 -0
  25. package/dist/handoff-evidence/index.d.ts +8 -0
  26. package/dist/handoff-evidence/index.js +7 -0
  27. package/dist/handoff-evidence/validate.d.ts +105 -0
  28. package/dist/handoff-evidence/validate.js +423 -0
  29. package/dist/hooks/dispatcher.d.ts +3 -0
  30. package/dist/hooks/dispatcher.js +12 -3
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +3 -0
  33. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  34. package/dist/init-deposit/agent-hooks.js +5 -2
  35. package/dist/init-deposit/hygiene.d.ts +1 -1
  36. package/dist/init-deposit/hygiene.js +16 -4
  37. package/dist/init-deposit/init-deposit.d.ts +7 -1
  38. package/dist/init-deposit/init-deposit.js +24 -4
  39. package/dist/init-deposit/refresh.d.ts +9 -1
  40. package/dist/init-deposit/refresh.js +48 -5
  41. package/dist/init-deposit/scaffold.js +6 -5
  42. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  43. package/dist/parent-turn-shape/evaluate.js +353 -0
  44. package/dist/parent-turn-shape/index.d.ts +8 -0
  45. package/dist/parent-turn-shape/index.js +8 -0
  46. package/dist/policy/index.d.ts +3 -0
  47. package/dist/policy/index.js +35 -0
  48. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  49. package/dist/review-monitor/constants.js +3 -2
  50. package/dist/review-monitor/tier-detection.d.ts +7 -3
  51. package/dist/review-monitor/tier-detection.js +18 -1
  52. package/dist/review-monitor/verify.js +18 -0
  53. package/dist/scope/index.d.ts +2 -0
  54. package/dist/scope/index.js +2 -0
  55. package/dist/scope/main.d.ts +10 -0
  56. package/dist/scope/main.js +109 -24
  57. package/dist/scope/promote-from-issue.d.ts +49 -0
  58. package/dist/scope/promote-from-issue.js +367 -0
  59. package/dist/scope/promote-path.d.ts +39 -0
  60. package/dist/scope/promote-path.js +105 -0
  61. package/dist/session/ritual-entrypoint.d.ts +6 -2
  62. package/dist/session/ritual-entrypoint.js +15 -2
  63. package/dist/session/session-ready.js +31 -14
  64. package/dist/session/session-start.d.ts +2 -1
  65. package/dist/session/session-start.js +66 -3
  66. package/dist/session/verify-session-ritual.d.ts +4 -2
  67. package/dist/session/verify-session-ritual.js +11 -11
  68. package/dist/slash/product-set.js +4 -1
  69. package/dist/swarm/routing.d.ts +4 -2
  70. package/dist/swarm/routing.js +26 -4
  71. package/dist/swarm/verify-review-clean.d.ts +4 -3
  72. package/dist/swarm/verify-review-clean.js +28 -73
  73. package/dist/triage/actions/index.js +62 -2
  74. package/dist/triage/actions/types.d.ts +8 -1
  75. package/dist/triage/author-filter.d.ts +51 -0
  76. package/dist/triage/author-filter.js +152 -0
  77. package/dist/triage/classify/index.d.ts +9 -0
  78. package/dist/triage/classify/index.js +34 -2
  79. package/dist/triage/classify/label-mirror.d.ts +206 -0
  80. package/dist/triage/classify/label-mirror.js +914 -0
  81. package/dist/triage/help/registry-data.d.ts +49 -38
  82. package/dist/triage/help/registry-data.js +134 -39
  83. package/dist/triage/index.d.ts +1 -0
  84. package/dist/triage/index.js +1 -0
  85. package/dist/triage/queue/index.d.ts +1 -0
  86. package/dist/triage/queue/index.js +1 -0
  87. package/dist/triage/queue/render.d.ts +2 -0
  88. package/dist/triage/queue/render.js +6 -0
  89. package/dist/triage/queue/show.d.ts +1 -1
  90. package/dist/triage/queue/show.js +4 -2
  91. package/dist/vbrief-validate/project-definition.js +1 -1
  92. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  93. package/dist/verify-env/agent-hook-readiness.js +150 -0
  94. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  95. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  96. package/dist/verify-env/agent-hooks.js +11 -3
  97. package/dist/verify-env/index.d.ts +1 -0
  98. package/dist/verify-env/index.js +1 -0
  99. package/package.json +11 -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. */
@@ -0,0 +1,134 @@
1
+ import { ProjectionContainmentError } from "../fs/projection-containment.js";
2
+ import { type Clock } from "./time.js";
3
+ export { ProjectionContainmentError as CacheArchiveContainmentError };
4
+ /** Archive age default matches TTL prune window but is a separate concept (#1137). */
5
+ export declare const DEFAULT_ARCHIVE_OLDER_THAN_DAYS = 30;
6
+ export declare const ARCHIVE_SOURCE_GITHUB_ISSUE = "github-issue";
7
+ export declare const ARCHIVED_ROOT_SEGMENT = "archived";
8
+ export declare const ARCHIVE_META_FILENAME = "archive-meta.json";
9
+ export interface ArchiveMeta {
10
+ archived_at: string;
11
+ reason: string;
12
+ source_path: string;
13
+ key: string;
14
+ source: string;
15
+ pre_archive_decision?: string | null;
16
+ closed_at?: string | null;
17
+ age_basis?: string;
18
+ }
19
+ export type ArchiveSkipReason = "not-closed" | "too-recent" | "open-lifecycle-scope" | "non-terminal-decision" | "missing-raw" | "invalid-key" | "already-archived";
20
+ export interface ArchiveCandidate {
21
+ source: string;
22
+ key: string;
23
+ liveDir: string;
24
+ archiveDir: string;
25
+ closedAt: string | null;
26
+ ageBasis: string;
27
+ ageMs: number;
28
+ preArchiveDecision: string | null;
29
+ }
30
+ export interface ArchiveSkip {
31
+ source: string;
32
+ key: string;
33
+ reason: ArchiveSkipReason;
34
+ detail?: string;
35
+ }
36
+ export interface ArchiveClosedResult {
37
+ dryRun: boolean;
38
+ olderThanDays: number;
39
+ source: string;
40
+ repo: string | null;
41
+ archived: ArchiveCandidate[];
42
+ skipped: ArchiveSkip[];
43
+ archivedCount: number;
44
+ skippedCount: number;
45
+ }
46
+ export interface ArchivedEntry {
47
+ source: string;
48
+ key: string;
49
+ archiveDir: string;
50
+ archivedAt: string;
51
+ meta: ArchiveMeta;
52
+ }
53
+ export interface ArchiveListResult {
54
+ entries: ArchivedEntry[];
55
+ count: number;
56
+ source: string;
57
+ repo: string | null;
58
+ }
59
+ export interface RestoreResult {
60
+ source: string;
61
+ key: string;
62
+ status: "restored" | "already-live" | "missing" | "conflict";
63
+ liveDir: string;
64
+ archiveDir: string;
65
+ detail?: string;
66
+ }
67
+ /** Live entry dir for a github-issue key. */
68
+ export declare function liveEntryDir(source: string, key: string, cacheRoot: string): string;
69
+ /** Archived entry dir: `.deft-cache/archived/<source>/<owner>/<repo>/<N>/`. */
70
+ export declare function archivedEntryDir(source: string, key: string, cacheRoot: string): string;
71
+ /**
72
+ * Open-lifecycle protection set: preferred keys are `owner/repo#N`.
73
+ * Bare `#N` keys are also recorded when the plan URI has no repo (conservative).
74
+ */
75
+ export declare function openLifecycleReferencedKeys(projectRoot: string): ReadonlySet<string>;
76
+ /** True when cache key `owner/repo/N` is protected by open lifecycle refs. */
77
+ export declare function isLifecycleProtected(cacheKey: string, protectedKeys: ReadonlySet<string>): boolean;
78
+ /**
79
+ * Resolve closed age basis: `closed_at` from raw, else `meta.fetched_at`, else
80
+ * meta.json mtime (#1137).
81
+ */
82
+ export declare function resolveClosedAge(raw: Record<string, unknown>, meta: Record<string, unknown> | null, metaPath: string, now: Date): {
83
+ closedAt: string | null;
84
+ ageBasis: string;
85
+ ageMs: number;
86
+ };
87
+ export interface ArchiveClosedOptions {
88
+ olderThanDays?: number;
89
+ source?: string;
90
+ repo?: string | null;
91
+ dryRun?: boolean;
92
+ cacheRoot?: string;
93
+ projectRoot?: string;
94
+ /** Optional map of `owner/repo#N` or `N` → latest decision string. */
95
+ latestDecisions?: ReadonlyMap<string, string> | null;
96
+ /** When true, only archive reject / mark-duplicate latest decisions. */
97
+ terminalDecisionOnly?: boolean;
98
+ /** Override protected issue set (tests); default scans open lifecycle. */
99
+ protectedIssueNumbers?: ReadonlySet<number> | null;
100
+ /** Override protected keys `owner/repo#N` / `#N` (preferred multi-repo form). */
101
+ protectedKeys?: ReadonlySet<string> | null;
102
+ reason?: string;
103
+ clock?: Clock;
104
+ }
105
+ /**
106
+ * Archive closed github-issue cache entries older than the threshold.
107
+ * Skips (does not half-move) entries still referenced by open lifecycle scopes.
108
+ */
109
+ export declare function archiveClosedEntries(options?: ArchiveClosedOptions): ArchiveClosedResult;
110
+ export interface ArchiveListOptions {
111
+ source?: string;
112
+ repo?: string | null;
113
+ since?: string | null;
114
+ limit?: number | null;
115
+ cacheRoot?: string;
116
+ }
117
+ /** List archived entries newest `archived_at` first. */
118
+ export declare function listArchivedEntries(options?: ArchiveListOptions): ArchiveListResult;
119
+ export interface RestoreFromArchiveOptions {
120
+ source?: string;
121
+ key?: string;
122
+ issue?: number;
123
+ repo?: string | null;
124
+ force?: boolean;
125
+ cacheRoot?: string;
126
+ clock?: Clock;
127
+ }
128
+ /**
129
+ * Move an archived entry back to the live cache path.
130
+ * Idempotent no-op when already live with identical raw.json.
131
+ * Refuse when live exists with different content unless force.
132
+ */
133
+ export declare function restoreFromArchive(options?: RestoreFromArchiveOptions): RestoreResult;
134
+ //# sourceMappingURL=archive.d.ts.map