@deftai/directive-core 0.94.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 (61) hide show
  1. package/dist/authz/classify.js +485 -0
  2. package/dist/doctor/main.js +41 -9
  3. package/dist/doctor/types.d.ts +2 -2
  4. package/dist/freshness/bind.d.ts +67 -0
  5. package/dist/freshness/bind.js +201 -0
  6. package/dist/freshness/cli.d.ts +30 -0
  7. package/dist/freshness/cli.js +180 -0
  8. package/dist/freshness/compare.d.ts +14 -0
  9. package/dist/freshness/compare.js +134 -0
  10. package/dist/freshness/generation.d.ts +44 -0
  11. package/dist/freshness/generation.js +172 -0
  12. package/dist/freshness/index.d.ts +13 -0
  13. package/dist/freshness/index.js +13 -0
  14. package/dist/freshness/report.d.ts +43 -0
  15. package/dist/freshness/report.js +139 -0
  16. package/dist/freshness/types.d.ts +70 -0
  17. package/dist/freshness/types.js +30 -0
  18. package/dist/handoff-evidence/index.d.ts +8 -0
  19. package/dist/handoff-evidence/index.js +7 -0
  20. package/dist/handoff-evidence/validate.d.ts +105 -0
  21. package/dist/handoff-evidence/validate.js +423 -0
  22. package/dist/hooks/dispatcher.d.ts +3 -0
  23. package/dist/hooks/dispatcher.js +12 -3
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +2 -0
  26. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  27. package/dist/init-deposit/agent-hooks.js +5 -2
  28. package/dist/init-deposit/init-deposit.d.ts +7 -1
  29. package/dist/init-deposit/init-deposit.js +24 -4
  30. package/dist/init-deposit/refresh.d.ts +9 -1
  31. package/dist/init-deposit/refresh.js +48 -5
  32. package/dist/policy/index.d.ts +3 -0
  33. package/dist/policy/index.js +35 -0
  34. package/dist/release-e2e/greenfield-python-free-smoke.js +7 -3
  35. package/dist/session/ritual-entrypoint.d.ts +6 -2
  36. package/dist/session/ritual-entrypoint.js +15 -2
  37. package/dist/session/session-ready.js +31 -14
  38. package/dist/session/session-start.d.ts +2 -1
  39. package/dist/session/session-start.js +66 -3
  40. package/dist/session/verify-session-ritual.d.ts +4 -2
  41. package/dist/session/verify-session-ritual.js +11 -11
  42. package/dist/slash/product-set.js +4 -1
  43. package/dist/swarm/verify-review-clean.d.ts +4 -3
  44. package/dist/swarm/verify-review-clean.js +28 -73
  45. package/dist/triage/classify/index.d.ts +9 -0
  46. package/dist/triage/classify/index.js +34 -2
  47. package/dist/triage/classify/label-mirror.d.ts +143 -0
  48. package/dist/triage/classify/label-mirror.js +684 -0
  49. package/dist/triage/help/registry-data.d.ts +7 -7
  50. package/dist/triage/help/registry-data.js +27 -7
  51. package/dist/triage/queue/show.d.ts +1 -1
  52. package/dist/triage/queue/show.js +4 -2
  53. package/dist/vbrief-validate/project-definition.js +1 -1
  54. package/dist/verify-env/agent-hook-readiness.d.ts +42 -0
  55. package/dist/verify-env/agent-hook-readiness.js +150 -0
  56. package/dist/verify-env/agent-hooks-live-probe.d.ts +16 -4
  57. package/dist/verify-env/agent-hooks-live-probe.js +120 -107
  58. package/dist/verify-env/agent-hooks.js +11 -3
  59. package/dist/verify-env/index.d.ts +1 -0
  60. package/dist/verify-env/index.js +1 -0
  61. 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. */
@@ -517,8 +517,11 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
517
517
  });
518
518
  return false;
519
519
  }
520
- const message = `${checkName}: registered and structurally valid; ` +
521
- "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
520
+ const codexEnabled = result.registrations.some((entry) => entry.host === "codex" && entry.status !== "disabled");
521
+ const message = `${checkName}: registered and structurally valid` +
522
+ (codexEnabled
523
+ ? "; Codex trust is manual-review-required — open `/hooks` to review the project commands"
524
+ : "");
522
525
  sink.success(message);
523
526
  addFinding({
524
527
  severity: "skip",
@@ -526,8 +529,11 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
526
529
  check: checkName,
527
530
  status: "registered",
528
531
  registrations: result.registrations,
529
- trust_status: "not-verifiable",
530
- trust_review: "Open `/hooks` in Codex and review the project hook commands.",
532
+ trust_status: codexEnabled ? "manual-review-required" : "not-applicable",
533
+ trust_review: codexEnabled
534
+ ? "Open `/hooks` in Codex and review the exact project hook commands."
535
+ : null,
536
+ interception_status: "not-directly-verified",
531
537
  });
532
538
  return true;
533
539
  }
@@ -543,7 +549,25 @@ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams
543
549
  const liveCheckName = "agent-hooks-live-probe";
544
550
  try {
545
551
  const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
546
- const liveResult = (seams.probeAgentHooksLive ?? probeAgentHooksLive)(projectRoot);
552
+ if (result.code !== 0) {
553
+ const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
554
+ sink.warn(message);
555
+ addFinding({
556
+ severity: "warning",
557
+ message,
558
+ check: checkName,
559
+ status: result.code === 2 ? "unavailable" : "incomplete",
560
+ registrations: result.registrations,
561
+ suggestion: "deft update",
562
+ });
563
+ return;
564
+ }
565
+ const enabledHosts = result.registrations
566
+ .filter((entry) => entry.status !== "disabled")
567
+ .map((entry) => entry.host);
568
+ const liveResult = (seams.probeAgentHooksLive ?? probeAgentHooksLive)(projectRoot, {
569
+ hosts: enabledHosts,
570
+ });
547
571
  if (liveResult.code !== 0) {
548
572
  const message = `${liveCheckName}: ${liveResult.message.replace(/\s+/g, " ").trim()}`;
549
573
  sink.warn(message);
@@ -557,8 +581,12 @@ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams
557
581
  });
558
582
  return;
559
583
  }
560
- const message = `${checkName}: registered, structurally valid, and live probe passed; ` +
561
- "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
584
+ const codexEnabled = result.registrations.some((entry) => entry.host === "codex" && entry.status !== "disabled");
585
+ const message = `${checkName}: registered, structurally valid, and live probe passed` +
586
+ (codexEnabled
587
+ ? "; Codex trust is manual-review-required — open `/hooks` to review the project commands"
588
+ : "") +
589
+ "; direct shim invocation does not verify host interception";
562
590
  sink.success(message);
563
591
  addFinding({
564
592
  severity: "skip",
@@ -566,9 +594,13 @@ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams
566
594
  check: liveCheckName,
567
595
  status: "registered-and-functional",
568
596
  registrations: result.registrations,
569
- trust_status: "not-verifiable",
570
- trust_review: "Open `/hooks` in Codex and review the project hook commands.",
597
+ trust_status: codexEnabled ? "manual-review-required" : "not-applicable",
598
+ trust_review: codexEnabled
599
+ ? "Open `/hooks` in Codex and review the exact project hook commands."
600
+ : null,
601
+ interception_status: "not-directly-verified",
571
602
  live_probe: "passed",
603
+ live_probe_duration_ms: liveResult.durationMs,
572
604
  });
573
605
  }
574
606
  catch (cause) {
@@ -4,7 +4,7 @@ import type { EngineProbeResult } from "../resolution/classify.js";
4
4
  import type { ResolutionMode } from "../resolution/index.js";
5
5
  import type { ResolveUserMdResult } from "../user-config/resolve-user-md.js";
6
6
  import type { AgentHookHealthResult } from "../verify-env/agent-hooks.js";
7
- import type { AgentHookLiveProbeResult } from "../verify-env/agent-hooks-live-probe.js";
7
+ import type { AgentHookLiveProbeResult, AgentHookLiveProbeSeams } from "../verify-env/agent-hooks-live-probe.js";
8
8
  export declare const EXIT_CLEAN = 0;
9
9
  export declare const EXIT_DRIFT = 1;
10
10
  export declare const EXIT_CONFIG_ERROR = 2;
@@ -153,7 +153,7 @@ export interface DoctorSeams {
153
153
  /** Read-only agent-host hook registration probe (#2438). */
154
154
  readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
155
155
  /** Live hook spawn probe for doctor --full (#2852). */
156
- readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
156
+ readonly probeAgentHooksLive?: (projectRoot: string, seams?: AgentHookLiveProbeSeams) => AgentHookLiveProbeResult;
157
157
  /**
158
158
  * xBRIEF project-envelope staleness probe (#2971). Injected so doctor can
159
159
  * fail closed on 0.6 project JSON under an xbrief/ layout without re-deriving
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Session bind of live deposit generation (#3117).
3
+ *
4
+ * Host-agnostic storage:
5
+ * - Default (no sessionId): `.deft/session-bind.json` — single-operator convenience.
6
+ * - With sessionId: `.deft/session-binds/<safeId>.json` — isolated multi-session binds.
7
+ *
8
+ * Multi-agent hosts MUST pass a stable host session identity when binding and
9
+ * reporting so one session cannot certify another as current.
10
+ */
11
+ import { type BoundGeneration, type LiveGeneration } from "./types.js";
12
+ /** Relative path for the default (no-sessionId) bind record. */
13
+ export declare const SESSION_BIND_REL: string;
14
+ /** Directory for per-session bind records. */
15
+ export declare const SESSION_BINDS_DIR_REL: string;
16
+ export declare function sessionBindPath(projectRoot: string, sessionId?: string | null): string;
17
+ /**
18
+ * Stable filesystem-safe file name for a host session id.
19
+ * Keeps a short prefix for debug, hashes the rest to avoid path injection.
20
+ * Character filter is O(n) (no regex) — CodeQL poly-redos on uncontrolled ids.
21
+ */
22
+ export declare function safeSessionFileName(sessionId: string): string;
23
+ export interface BindSessionOptions {
24
+ readonly sessionId?: string | null;
25
+ readonly nowIso?: string;
26
+ /**
27
+ * When live generation is missing (legacy deposit), stamp generation 1 from
28
+ * contentVersion before binding. Default true.
29
+ */
30
+ readonly ensureLive?: boolean;
31
+ /** Used only when ensuring a missing live token. */
32
+ readonly contentVersion?: string;
33
+ readonly stampedBy?: string;
34
+ /**
35
+ * When binding with a sessionId, also write the default bind path.
36
+ * Default **false** — multi-agent isolation requires hosts to report with
37
+ * `--session-id`. Enabling this reopens cross-session false-current (Greptile).
38
+ */
39
+ readonly alsoWriteDefault?: boolean;
40
+ /**
41
+ * Host attests that payload surfaces for the live generation were reloaded
42
+ * into the session. Required for trusted readiness. Default false.
43
+ */
44
+ readonly payloadLoaded?: boolean;
45
+ }
46
+ export interface ReadBoundOptions {
47
+ /** When set, read only that session's bind (never the default). */
48
+ readonly sessionId?: string | null;
49
+ }
50
+ /** Parse a bound generation record (null if invalid). */
51
+ export declare function parseBoundGeneration(raw: unknown): BoundGeneration | null;
52
+ /** Read the session bind record (null when absent/unreadable). */
53
+ export declare function readBoundGeneration(projectRoot: string, options?: ReadBoundOptions): BoundGeneration | null;
54
+ /**
55
+ * Bind the current live generation into session context.
56
+ *
57
+ * Does not require restarting a shared host runtime — callers re-load payload
58
+ * surfaces into the session and call this (or `freshness:bind`) to rebind.
59
+ *
60
+ * Multi-agent hosts MUST supply `sessionId` so binds do not overwrite each other.
61
+ */
62
+ export declare function bindSessionGeneration(projectRoot: string, options?: BindSessionOptions): {
63
+ bound: BoundGeneration;
64
+ live: LiveGeneration;
65
+ path: string;
66
+ };
67
+ //# sourceMappingURL=bind.d.ts.map