@deftai/directive-core 0.85.0 → 0.87.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 (50) hide show
  1. package/dist/check/gate-lists.js +1 -0
  2. package/dist/doctor/constants.d.ts +2 -2
  3. package/dist/doctor/constants.js +2 -2
  4. package/dist/doctor/main.d.ts +8 -6
  5. package/dist/doctor/main.js +102 -36
  6. package/dist/doctor/taskfile.d.ts +8 -0
  7. package/dist/doctor/taskfile.js +19 -0
  8. package/dist/doctor/types.d.ts +3 -0
  9. package/dist/hooks/dispatcher.d.ts +21 -1
  10. package/dist/hooks/dispatcher.js +85 -15
  11. package/dist/init-deposit/agent-hooks.js +14 -6
  12. package/dist/init-deposit/gitignore.js +5 -0
  13. package/dist/intake/issue-emit.d.ts +45 -2
  14. package/dist/intake/issue-emit.js +420 -17
  15. package/dist/intake/issue-ingest.js +54 -4
  16. package/dist/platform/platform-capabilities.js +3 -0
  17. package/dist/policy/org-force-on-migration.js +2 -0
  18. package/dist/render/framework-commands.d.ts +1 -1
  19. package/dist/render/framework-commands.js +6 -6
  20. package/dist/render/roadmap-render.d.ts +5 -1
  21. package/dist/render/roadmap-render.js +20 -2
  22. package/dist/render/rule-map.js +5 -0
  23. package/dist/review-monitor/constants.js +3 -2
  24. package/dist/review-monitor/tier-detection.d.ts +6 -2
  25. package/dist/review-monitor/tier-detection.js +27 -2
  26. package/dist/scope/transition.js +43 -0
  27. package/dist/session/release-availability.d.ts +2 -0
  28. package/dist/session/release-availability.js +23 -8
  29. package/dist/swarm/routing-set-cli.js +5 -10
  30. package/dist/swarm/routing.d.ts +3 -2
  31. package/dist/swarm/routing.js +16 -4
  32. package/dist/triage/help/registry-data.d.ts +7 -7
  33. package/dist/triage/help/registry-data.js +15 -6
  34. package/dist/triage/queue/index.d.ts +1 -0
  35. package/dist/triage/queue/index.js +1 -0
  36. package/dist/triage/queue/show.d.ts +69 -0
  37. package/dist/triage/queue/show.js +293 -0
  38. package/dist/triage/scope/cli.js +3 -0
  39. package/dist/triage/scope/coverage.d.ts +2 -0
  40. package/dist/triage/scope/coverage.js +18 -3
  41. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  42. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  43. package/dist/verify-env/index.d.ts +1 -0
  44. package/dist/verify-env/index.js +1 -0
  45. package/dist/verify-source/index.d.ts +1 -0
  46. package/dist/verify-source/index.js +1 -0
  47. package/dist/verify-source/openclaw-tier1.d.ts +37 -0
  48. package/dist/verify-source/openclaw-tier1.js +100 -0
  49. package/dist/xbrief-migrate/migrate-project.js +35 -22
  50. package/package.json +3 -3
@@ -22,6 +22,7 @@ export const FRAMEWORK_CHECK_GATES = [
22
22
  "verify:skill-external-fetch-gate",
23
23
  "verify:contract-drift",
24
24
  "verify:cursor-tier1",
25
+ "verify:openclaw-tier1",
25
26
  "verify:go-freeze",
26
27
  "verify:bridge-drift",
27
28
  "verify:branch",
@@ -14,9 +14,9 @@ export declare const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
14
14
  export declare const NPM_REGISTRY_MIRROR_DOC_URL = "https://github.com/deftai/directive/blob/master/content/UPGRADING.md#corporate-or-mirrored-npm-registry";
15
15
  export declare const NETWORK_DISCLOSURE_LINE: string;
16
16
  export declare const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE: string;
17
- export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "vbrief"];
17
+ export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "xbrief"];
18
18
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
19
- export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "vbrief"];
19
+ export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "xbrief"];
20
20
  export declare const DEFT_REPO_POSITIVE_MARKERS: readonly ["content/templates/agents-entry.md", "content/skills/deft-directive-build/SKILL.md"];
21
21
  export declare const EXPECTED_CONTENT_DIRS: readonly ["languages", "strategies", "skills", "templates"];
22
22
  /** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
@@ -46,9 +46,9 @@ export const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE = "skip -- offline tier (def
46
46
  "(discloses tool + registry before contacting either).";
47
47
  // Engine / lifecycle dirs that stay at the framework root (NOT relocated by
48
48
  // #1875). Shippable-content dirs moved under content/ -- see EXPECTED_CONTENT_DIRS.
49
- export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "vbrief"];
49
+ export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "xbrief"];
50
50
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
51
- export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "vbrief"];
51
+ export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "xbrief"];
52
52
  // Post-#1875 content/ move: these framework-internal markers now live under
53
53
  // content/ in the SOURCE repo. They identify a deft source checkout (a consumer
54
54
  // would never reproduce them); the C1 flatten means a consumer deposit has no
@@ -2,14 +2,16 @@ import { type ResolutionFacts } from "../resolution/index.js";
2
2
  import { createPlainSink } from "./output.js";
3
3
  import type { DoctorSeams, Finding, ResolutionSummary } from "./types.js";
4
4
  export declare function cmdDoctor(args: readonly string[], seams?: DoctorSeams): number;
5
- export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
5
+ export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): boolean;
6
+ export declare function runAgentHooksLiveProbeCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
6
7
  /**
7
8
  * Never emit a bare `task ...` remediation in a project without Taskfile wiring
8
- * (#2267). The `directive` surface always works; `task deft:X` is optional and
9
- * only present when the consumer wired the include. `plan()` already emits the
10
- * `directive` / `npx` / `npm` surface, so this guard is a defensive invariant:
11
- * any `task`-prefixed command is rewritten to the `directive` surface unless the
12
- * project actually has the include.
9
+ * (#2267 / #2893). The `directive`/`deft` surface always works; `task deft:X` is
10
+ * the go-task namespaced form only when the consumer wired the include (bare
11
+ * `task pr:watch` is not the consumer form under include key `deft:`). `plan()`
12
+ * already emits the `directive` / `npx` / `npm` surface, so this guard is a
13
+ * defensive invariant: any `task`-prefixed command is rewritten to the
14
+ * `directive` surface unless the project actually has the include.
13
15
  */
14
16
  /**
15
17
  * Detect Taskfile wiring through the injected seam (#2267). Mirrors
@@ -7,6 +7,7 @@ import { loadProjectDefinition } from "../policy/resolve.js";
7
7
  import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
8
8
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
9
9
  import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
10
+ import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
10
11
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
11
12
  import { runChecks } from "./checks.js";
12
13
  import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
@@ -16,10 +17,10 @@ import { pythonJsonDump } from "./json.js";
16
17
  import { parseInstallRootFromAgentsMd } from "./manifest.js";
17
18
  import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
18
19
  import { createPlainSink } from "./output.js";
19
- import { readTextSafe, resolveDefaultFrameworkRoot, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
20
+ import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
20
21
  import { runPayloadStalenessCheck } from "./payload-staleness.js";
21
22
  import { runLocalSignpostChecks } from "./signpost-checks.js";
22
- import { classifyTaskfileInclude, formatMissingIncludeSnippet, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
23
+ import { classifyTaskfileInclude, formatGatesSurfaceDualRemediation, formatMissingIncludeSnippet, GATES_SURFACE_DEFT_REMEDIATION, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
23
24
  import { defaultWhich } from "./which.js";
24
25
  const DEFAULT_RESOLUTION_PLATFORMS = ["linux", "darwin", "win32"];
25
26
  /**
@@ -57,7 +58,7 @@ export function cmdDoctor(args, seams = {}) {
57
58
  const quietMode = flags.quiet;
58
59
  const fullMode = flags.full;
59
60
  const projectRoot = resolvePath(flags.projectRoot ?? process.cwd());
60
- const frameworkRoot = seams.frameworkRoot ?? resolveDefaultFrameworkRoot();
61
+ const frameworkRoot = seams.frameworkRoot ?? resolveFrameworkRootForProject(projectRoot);
61
62
  const consumerContext = resolve(projectRoot) !== resolve(frameworkRoot);
62
63
  const whichFn = seams.whichFn ?? defaultWhich;
63
64
  const nowFn = seams.now ?? (() => new Date());
@@ -165,7 +166,17 @@ export function cmdDoctor(args, seams = {}) {
165
166
  sink.blank();
166
167
  }
167
168
  sink.info("Checking agent-host hook registration...");
168
- runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
169
+ const findingCountBeforeHooks = findings.length;
170
+ const hooksHealthy = runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
171
+ if (fullMode && hooksHealthy) {
172
+ const lastFinding = findings[findings.length - 1];
173
+ if (findings.length > findingCountBeforeHooks &&
174
+ lastFinding?.check === "agent-hooks-registration" &&
175
+ lastFinding?.status === "registered") {
176
+ findings.pop();
177
+ }
178
+ runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams);
179
+ }
169
180
  if (consumerContext) {
170
181
  if (!jsonMode) {
171
182
  sink.blank();
@@ -280,7 +291,7 @@ export function cmdDoctor(args, seams = {}) {
280
291
  if (!jsonMode) {
281
292
  sink.blank();
282
293
  }
283
- sink.info("Checking optional root Taskfile.yml include...");
294
+ sink.info("Checking gates-surface readiness (Taskfile include for deep-think agent gates)...");
284
295
  runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFinding, seams);
285
296
  let resolution = null;
286
297
  if (!runningInsideDeftRepo(projectRoot, seams)) {
@@ -353,40 +364,81 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
353
364
  const reason = "maintainer source checkout; project hook deposit is consumer-only";
354
365
  sink.info(`${checkName}: skip -- ${reason}`);
355
366
  addFinding({ severity: "skip", message: reason, check: checkName, status: "skip" });
356
- return;
367
+ return false;
357
368
  }
358
369
  try {
359
370
  const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
360
- if (result.code === 0) {
361
- const message = `${checkName}: registered and structurally valid; ` +
362
- "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
363
- sink.success(message);
371
+ if (result.code !== 0) {
372
+ const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
373
+ sink.warn(message);
364
374
  addFinding({
365
- severity: "skip",
375
+ severity: "warning",
366
376
  message,
367
377
  check: checkName,
368
- status: "registered",
378
+ status: result.code === 2 ? "unavailable" : "incomplete",
369
379
  registrations: result.registrations,
370
- trust_status: "not-verifiable",
371
- trust_review: "Open `/hooks` in Codex and review the project hook commands.",
380
+ suggestion: "deft update",
372
381
  });
373
- return;
382
+ return false;
374
383
  }
375
- const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
376
- sink.warn(message);
384
+ const message = `${checkName}: registered and structurally valid; ` +
385
+ "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
386
+ sink.success(message);
377
387
  addFinding({
378
- severity: "warning",
388
+ severity: "skip",
379
389
  message,
380
390
  check: checkName,
381
- status: result.code === 2 ? "unavailable" : "incomplete",
391
+ status: "registered",
382
392
  registrations: result.registrations,
383
- suggestion: "deft update",
393
+ trust_status: "not-verifiable",
394
+ trust_review: "Open `/hooks` in Codex and review the project hook commands.",
384
395
  });
396
+ return true;
385
397
  }
386
398
  catch (cause) {
387
399
  const message = `${checkName}: probe failed -- ${String(cause)}`;
388
400
  sink.warn(message);
389
401
  addFinding({ severity: "warning", message, check: checkName, suggestion: "deft update" });
402
+ return false;
403
+ }
404
+ }
405
+ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams) {
406
+ const checkName = "agent-hooks-registration";
407
+ const liveCheckName = "agent-hooks-live-probe";
408
+ try {
409
+ const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
410
+ const liveResult = (seams.probeAgentHooksLive ?? probeAgentHooksLive)(projectRoot);
411
+ if (liveResult.code !== 0) {
412
+ const message = `${liveCheckName}: ${liveResult.message.replace(/\s+/g, " ").trim()}`;
413
+ sink.warn(message);
414
+ addFinding({
415
+ severity: "warning",
416
+ message,
417
+ check: liveCheckName,
418
+ status: liveResult.code === 2 ? "unavailable" : "non-functional",
419
+ cases: liveResult.cases,
420
+ suggestion: "npm i -g @deftai/directive@latest && deft update",
421
+ });
422
+ return;
423
+ }
424
+ const message = `${checkName}: registered, structurally valid, and live probe passed; ` +
425
+ "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
426
+ sink.success(message);
427
+ addFinding({
428
+ severity: "skip",
429
+ message,
430
+ check: liveCheckName,
431
+ status: "registered-and-functional",
432
+ registrations: result.registrations,
433
+ trust_status: "not-verifiable",
434
+ trust_review: "Open `/hooks` in Codex and review the project hook commands.",
435
+ live_probe: "passed",
436
+ });
437
+ }
438
+ catch (cause) {
439
+ const message = `${liveCheckName}: probe failed -- ${String(cause)}`;
440
+ sink.warn(message);
441
+ addFinding({ severity: "warning", message, check: liveCheckName, suggestion: "deft update" });
390
442
  }
391
443
  }
392
444
  function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
@@ -588,15 +640,21 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
588
640
  }
589
641
  const includeStatus = classifyTaskfileInclude(projectRoot);
590
642
  if (includeStatus === "ok") {
591
- sink.success("Root Taskfile.yml includes the deft framework");
643
+ sink.success("Gates-surface ready: root Taskfile.yml includes the deft framework (`task deft:<verb>`)");
592
644
  return;
593
645
  }
594
646
  if (includeStatus === "missing-file") {
595
647
  let includeMissing = true;
596
648
  const target = join(projectRoot, "Taskfile.yml");
597
- const message = "Root Taskfile.yml missing. This is OK for package-manager installs that use the `deft X` surface directly. To also enable the optional `task deft:X` surface, paste this into " +
598
- `${target}:`;
599
- sink.info(message);
649
+ // #2893: elevate to warning deep-think agent gates need a working invoke path.
650
+ // Dual remediations: (1) deft CLI primary (2) Taskfile include for task deft: verbs.
651
+ const message = "Gates-surface readiness: root Taskfile.yml missing. Deep-think agent gates " +
652
+ "(`pr:watch`, `review-monitor:*`) need a working invoke path — not optional convenience. " +
653
+ "1. " +
654
+ GATES_SURFACE_DEFT_REMEDIATION +
655
+ ` 2. Create ${target} with the canonical include so go-task exposes \`task deft:<verb>\` ` +
656
+ "(include key `deft:` → namespaced tasks; bare `task pr:watch` is not the consumer form):";
657
+ sink.warn(message);
600
658
  if (!jsonMode) {
601
659
  sink.blank();
602
660
  sink.raw(TASKFILE_INCLUDE_SNIPPET);
@@ -616,22 +674,27 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
616
674
  }
617
675
  }
618
676
  else {
619
- sink.info("Skipped Taskfile.yml creation -- paste the snippet above when you are ready.");
677
+ sink.info("Skipped Taskfile.yml creation -- use `deft <verb>` now, or paste the include snippet when ready.");
620
678
  }
621
679
  }
622
680
  if (includeMissing) {
623
681
  addFinding({
624
682
  severity: "warning",
625
- message: "Root Taskfile.yml missing; optional Taskfile include unavailable",
683
+ message: "Gates-surface: root Taskfile.yml missing deep-think gates need `deft` CLI or `task deft:` include",
626
684
  check: "taskfile-include",
627
685
  file: target,
628
- suggestion: TASKFILE_INCLUDE_SNIPPET,
686
+ suggestion: formatGatesSurfaceDualRemediation("missing-file"),
629
687
  });
630
688
  }
631
689
  return;
632
690
  }
633
691
  if (includeStatus === "missing-include") {
634
- const message = "Root Taskfile.yml exists but does not include the deft framework. The `deft X` surface still works; add this to the Taskfile `includes:` block only if you want the optional `task deft:X` surface (doctor NEVER mutates an existing user-owned Taskfile):";
692
+ const message = "Gates-surface readiness: root Taskfile.yml exists but does not include the deft framework. " +
693
+ "Deep-think agent gates (`pr:watch`, `review-monitor:*`) need a working invoke path. " +
694
+ "1. " +
695
+ GATES_SURFACE_DEFT_REMEDIATION +
696
+ " 2. Add this to the Taskfile `includes:` block so go-task exposes `task deft:<verb>` " +
697
+ "(doctor NEVER mutates an existing user-owned Taskfile; bare `task pr:watch` is not the consumer form when the include key is `deft:`):";
635
698
  sink.warn(message);
636
699
  if (!jsonMode) {
637
700
  sink.blank();
@@ -640,21 +703,23 @@ function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFindin
640
703
  const tf = resolveConsumerTaskfile(projectRoot);
641
704
  addFinding({
642
705
  severity: "warning",
643
- message: "Root Taskfile.yml does not include the deft framework",
706
+ message: "Gates-surface: root Taskfile.yml does not include the deft framework — deep-think gates need `deft` CLI or `task deft:` include",
644
707
  check: "taskfile-include",
645
708
  file: tf,
646
- suggestion: formatMissingIncludeSnippet(),
709
+ suggestion: formatGatesSurfaceDualRemediation("missing-include"),
647
710
  });
648
711
  return;
649
712
  }
650
713
  const taskfilePath = resolveConsumerTaskfile(projectRoot) ?? join(projectRoot, "Taskfile.yml");
651
- const message = `Root Taskfile.yml at ${taskfilePath} exists but could not be read -- check file permissions.`;
714
+ const message = `Gates-surface readiness: root Taskfile.yml at ${taskfilePath} exists but could not be read ` +
715
+ "check file permissions. Deep-think gates still work via `deft <verb>` until the include is readable.";
652
716
  sink.warn(message);
653
717
  addFinding({
654
718
  severity: "warning",
655
719
  message,
656
720
  check: "taskfile-include",
657
721
  file: taskfilePath,
722
+ suggestion: GATES_SURFACE_DEFT_REMEDIATION,
658
723
  });
659
724
  }
660
725
  /**
@@ -734,11 +799,12 @@ function runPlanExtensionShadowCheck(projectRoot, sink, addFinding, seams) {
734
799
  }
735
800
  /**
736
801
  * Never emit a bare `task ...` remediation in a project without Taskfile wiring
737
- * (#2267). The `directive` surface always works; `task deft:X` is optional and
738
- * only present when the consumer wired the include. `plan()` already emits the
739
- * `directive` / `npx` / `npm` surface, so this guard is a defensive invariant:
740
- * any `task`-prefixed command is rewritten to the `directive` surface unless the
741
- * project actually has the include.
802
+ * (#2267 / #2893). The `directive`/`deft` surface always works; `task deft:X` is
803
+ * the go-task namespaced form only when the consumer wired the include (bare
804
+ * `task pr:watch` is not the consumer form under include key `deft:`). `plan()`
805
+ * already emits the `directive` / `npx` / `npm` surface, so this guard is a
806
+ * defensive invariant: any `task`-prefixed command is rewritten to the
807
+ * `directive` surface unless the project actually has the include.
742
808
  */
743
809
  /**
744
810
  * Detect Taskfile wiring through the injected seam (#2267). Mirrors
@@ -3,4 +3,12 @@ export declare function resolveConsumerTaskfile(projectRoot: string): string | n
3
3
  export type TaskfileIncludeStatus = "ok" | "missing-file" | "missing-include" | "unreadable";
4
4
  export declare function classifyTaskfileInclude(projectRoot: string): TaskfileIncludeStatus;
5
5
  export declare function formatMissingIncludeSnippet(): string;
6
+ /** Primary remediation for deep-think gates when Taskfile include is absent (#2893). */
7
+ export declare const GATES_SURFACE_DEFT_REMEDIATION: string;
8
+ /**
9
+ * Dual remediations for gates-surface readiness (#2893):
10
+ * 1. `deft <verb>` CLI (works without Taskfile)
11
+ * 2. Taskfile include so go-task exposes `task deft:<verb>` (not bare `task pr:watch`)
12
+ */
13
+ export declare function formatGatesSurfaceDualRemediation(kind: "missing-file" | "missing-include"): string;
6
14
  //# sourceMappingURL=taskfile.d.ts.map
@@ -62,4 +62,23 @@ export function classifyTaskfileInclude(projectRoot) {
62
62
  export function formatMissingIncludeSnippet() {
63
63
  return " deft:\n taskfile: ./.deft/core/Taskfile.yml\n optional: true\n";
64
64
  }
65
+ /** Primary remediation for deep-think gates when Taskfile include is absent (#2893). */
66
+ export const GATES_SURFACE_DEFT_REMEDIATION = "Prefer `deft pr:watch` / `deft review-monitor:register` / `deft verify:review-monitor` " +
67
+ "(primary npm/CLI surface; no Taskfile required).";
68
+ /**
69
+ * Dual remediations for gates-surface readiness (#2893):
70
+ * 1. `deft <verb>` CLI (works without Taskfile)
71
+ * 2. Taskfile include so go-task exposes `task deft:<verb>` (not bare `task pr:watch`)
72
+ */
73
+ export function formatGatesSurfaceDualRemediation(kind) {
74
+ const includePart = kind === "missing-file"
75
+ ? "Create root Taskfile.yml with the canonical deft include so go-task exposes `task deft:<verb>` " +
76
+ "(not bare `task pr:watch`):\n" +
77
+ "version: '3'\n\nincludes:\n" +
78
+ formatMissingIncludeSnippet()
79
+ : "Add the deft include to the existing Taskfile `includes:` block so go-task exposes " +
80
+ "`task deft:<verb>` (doctor NEVER mutates an existing user-owned Taskfile):\n" +
81
+ formatMissingIncludeSnippet();
82
+ return `${GATES_SURFACE_DEFT_REMEDIATION}\n2. ${includePart}`;
83
+ }
65
84
  //# sourceMappingURL=taskfile.js.map
@@ -4,6 +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
8
  export declare const EXIT_CLEAN = 0;
8
9
  export declare const EXIT_DRIFT = 1;
9
10
  export declare const EXIT_CONFIG_ERROR = 2;
@@ -147,5 +148,7 @@ export interface DoctorSeams {
147
148
  readonly detectPlanExtensionShadows?: (projectRoot: string) => readonly ShadowedPlanExtension[];
148
149
  /** Read-only agent-host hook registration probe (#2438). */
149
150
  readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
151
+ /** Live hook spawn probe for doctor --full (#2852). */
152
+ readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
150
153
  }
151
154
  //# sourceMappingURL=types.d.ts.map
@@ -13,7 +13,9 @@ export type CompactHookHost = (typeof COMPACT_HOOK_HOSTS)[number];
13
13
  /** Hosts without a native compact hook surface — deposit skips cleanly (#2113). */
14
14
  export declare const COMPACT_HOOK_SKIP_HOSTS: readonly ["codex"];
15
15
  export type HookVerdict = "allow" | "deny";
16
- export type HookDecisionCode = "session-start" | "session-start-degraded" | "session-compact-rearm" | "session-compact-rearm-degraded" | "session-compact-noop" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready" | "write-ready" | "read-only-deny" | "spawn-explore-ready" | "spawn-ready" | "spawn-not-ready" | "runtime-policy-deny-path" | "runtime-policy-deny-scope";
16
+ export type HookDecisionCode = "session-start" | "session-start-degraded" | "session-compact-rearm" | "session-compact-rearm-degraded" | "session-compact-noop" | "not-direct-write" | "invalid-input"
17
+ /** Host closed stdin with zero bytes — integration failure, not a policy gate (#2864). */
18
+ | "stdin-empty" | "ritual-not-ready" | "scope-not-ready" | "write-propose-ready" | "write-ready" | "read-only-deny" | "spawn-explore-ready" | "spawn-ready" | "spawn-not-ready" | "runtime-policy-deny-path" | "runtime-policy-deny-scope";
17
19
  export interface HookDecision {
18
20
  readonly verdict: HookVerdict;
19
21
  readonly code: HookDecisionCode;
@@ -62,6 +64,19 @@ export declare function hookToolName(payload: unknown, host?: HookHost): string
62
64
  export declare function hookWriteTargetPath(payload: unknown): string | null;
63
65
  /** POSIX-ish project-relative path for lifecycle matching. */
64
66
  export declare function toProjectRelativePosix(projectRoot: string, targetPath: string): string;
67
+ /**
68
+ * Lexical "outside project root" predicate used by #2885.
69
+ * - `".."` / `"../…"` are outside (not bare `startsWith("..")` — that matches `..secret`).
70
+ * - Absolute relatives and win32 cross-drive paths (`D:/…`) are outside.
71
+ * - Drive-letter form is win32-only so POSIX children like `D:/tmp/x` stay in-repo.
72
+ */
73
+ export declare function isLexicalOutsideProjectRoot(relPosix: string): boolean;
74
+ /**
75
+ * True when a write target is outside `projectRoot` for the active-scope skip (#2885).
76
+ * Lexically outside paths still fail the skip when a symlink/junction re-enters the project.
77
+ * When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
78
+ */
79
+ export declare function isOutsideProjectRootWrite(projectRoot: string, targetPath: string): boolean;
65
80
  /**
66
81
  * Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
67
82
  * planning, not implementation dispatch — exempt from the active-scope gate (#2625).
@@ -81,6 +96,11 @@ export declare function decideHook(input: HookDispatchInput, seams?: HookPolicyS
81
96
  * hook failure and blocks the tool — so Cursor allows must emit explicit
82
97
  * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
83
98
  * flow is unchanged.
99
+ *
100
+ * Cursor stdout always includes `code` (stable machine-readable decision code)
101
+ * so agents can distinguish policy denials from host-integration failures
102
+ * without parsing English (#2864). Exit status still does not encode the
103
+ * verdict — see hook-dispatch `run()` exit-code contract.
84
104
  */
85
105
  export declare function renderHostDecision(host: HookHost, decision: HookDecision): string;
86
106
  //# sourceMappingURL=dispatcher.d.ts.map
@@ -1,4 +1,5 @@
1
- import { relative, resolve, sep } from "node:path";
1
+ import { realpathSync } from "node:fs";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
2
3
  import { hasArtifactSuffix } from "../layout/resolve.js";
3
4
  import { evaluateRuntimeAuthorityDirectWrite, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
4
5
  import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
@@ -145,6 +146,57 @@ export function toProjectRelativePosix(projectRoot, targetPath) {
145
146
  const rel = relative(resolve(projectRoot), abs);
146
147
  return rel.split(sep).join("/").replace(/\\/g, "/");
147
148
  }
149
+ function posixRelative(fromAbs, toAbs) {
150
+ return relative(fromAbs, toAbs).split(sep).join("/").replace(/\\/g, "/");
151
+ }
152
+ /**
153
+ * Lexical "outside project root" predicate used by #2885.
154
+ * - `".."` / `"../…"` are outside (not bare `startsWith("..")` — that matches `..secret`).
155
+ * - Absolute relatives and win32 cross-drive paths (`D:/…`) are outside.
156
+ * - Drive-letter form is win32-only so POSIX children like `D:/tmp/x` stay in-repo.
157
+ */
158
+ export function isLexicalOutsideProjectRoot(relPosix) {
159
+ if (relPosix === ".." || relPosix.startsWith("../") || isAbsolute(relPosix)) {
160
+ return true;
161
+ }
162
+ // path.relative returns absolute drive paths across volumes on Windows only.
163
+ return process.platform === "win32" && /^[A-Za-z]:\//.test(relPosix);
164
+ }
165
+ /**
166
+ * True when a write target is outside `projectRoot` for the active-scope skip (#2885).
167
+ * Lexically outside paths still fail the skip when a symlink/junction re-enters the project.
168
+ * When the project root cannot be realpath'd (unit fixtures), lexical classification wins.
169
+ */
170
+ export function isOutsideProjectRootWrite(projectRoot, targetPath) {
171
+ const projectAbs = resolve(projectRoot);
172
+ const targetAbs = resolve(projectRoot, targetPath.replace(/\\/g, "/"));
173
+ const rel = posixRelative(projectAbs, targetAbs);
174
+ if (!isLexicalOutsideProjectRoot(rel))
175
+ return false;
176
+ try {
177
+ const projectReal = realpathSync(projectAbs);
178
+ let probe = targetAbs;
179
+ for (;;) {
180
+ try {
181
+ const probeReal = realpathSync(probe);
182
+ const reenter = posixRelative(projectReal, probeReal);
183
+ // Empty reenter ⇒ probeReal === projectReal (inside). Lexical-outside reenter ⇒ truly out.
184
+ if (reenter === "" || !isLexicalOutsideProjectRoot(reenter))
185
+ return false;
186
+ return true;
187
+ }
188
+ catch {
189
+ const parent = dirname(probe);
190
+ if (parent === probe)
191
+ return true;
192
+ probe = parent;
193
+ }
194
+ }
195
+ }
196
+ catch {
197
+ return true;
198
+ }
199
+ }
148
200
  /**
149
201
  * Proposing a scope under xbrief/proposed/ (or legacy vbrief/proposed/) is
150
202
  * planning, not implementation dispatch — exempt from the active-scope gate (#2625).
@@ -298,18 +350,25 @@ function inspectMutationGates(input, toolName, seams, options) {
298
350
  if (!scope.ready) {
299
351
  const writeTarget = hookWriteTargetPath(input.payload);
300
352
  const relTarget = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
301
- const proposedPathHint = options.proposedLifecycleExempt &&
302
- relTarget !== null &&
303
- (relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
304
- ? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
305
- "filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
306
- "planning writes (#2625)."
307
- : " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
308
- (options.proposedLifecycleExempt
309
- ? "or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption)."
310
- : "then re-run the pre-start_agent gate stack.");
311
- const denyCode = isSpawnTool(toolName) ? "spawn-not-ready" : "scope-not-ready";
312
- return deny(input, denyCode, toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
353
+ // Active-scope governs in-repo lifecycle work only. Outside-root Write/Edit
354
+ // (agent memory, $TMPDIR, user config) skips the deny; null/unparseable
355
+ // targets stay fail-closed. Spawn has no write target → still requires scope (#2885).
356
+ // Lexical ../ + realpath re-entry guard (not bare startsWith(".."); not symlink aliases).
357
+ const outsideRoot = writeTarget !== null && isOutsideProjectRootWrite(projectRoot, writeTarget);
358
+ if (!outsideRoot || isSpawnTool(toolName)) {
359
+ const proposedPathHint = options.proposedLifecycleExempt &&
360
+ relTarget !== null &&
361
+ (relTarget.startsWith("xbrief/proposed/") || relTarget.startsWith("vbrief/proposed/"))
362
+ ? " For a new proposal under xbrief/proposed/, include a lifecycle artifact " +
363
+ "filename (*.xbrief.json) in the Write/Edit payload so the gate can exempt " +
364
+ "planning writes (#2625)."
365
+ : " Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF, " +
366
+ (options.proposedLifecycleExempt
367
+ ? "or Write a new proposal to xbrief/proposed/*.xbrief.json (planning exemption)."
368
+ : "then re-run the pre-start_agent gate stack.");
369
+ const denyCode = isSpawnTool(toolName) ? "spawn-not-ready" : "scope-not-ready";
370
+ return deny(input, denyCode, toolName, `Directive denied ${toolName}: ${scope.message}${proposedPathHint}`);
371
+ }
313
372
  }
314
373
  const allowCode = isSpawnTool(toolName) ? "spawn-ready" : "write-ready";
315
374
  if (!isSpawnTool(toolName)) {
@@ -414,7 +473,12 @@ export function decideHook(input, seams = {}) {
414
473
  }
415
474
  const toolName = hookToolName(input.payload, input.host);
416
475
  if (toolName === null) {
417
- return deny(input, "invalid-input", null, missingToolNameMessage({
476
+ // stdin-empty is a host-integration failure; keep it distinct from invalid-input
477
+ // so agents can retry without treating it as a policy refusal (#2864).
478
+ const missingCode = input.payloadContext?.stdinEmpty
479
+ ? "stdin-empty"
480
+ : "invalid-input";
481
+ return deny(input, missingCode, null, missingToolNameMessage({
418
482
  host: input.host,
419
483
  payload: input.payload,
420
484
  context: input.payloadContext,
@@ -467,11 +531,16 @@ export function decideHook(input, seams = {}) {
467
531
  * hook failure and blocks the tool — so Cursor allows must emit explicit
468
532
  * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
469
533
  * flow is unchanged.
534
+ *
535
+ * Cursor stdout always includes `code` (stable machine-readable decision code)
536
+ * so agents can distinguish policy denials from host-integration failures
537
+ * without parsing English (#2864). Exit status still does not encode the
538
+ * verdict — see hook-dispatch `run()` exit-code contract.
470
539
  */
471
540
  export function renderHostDecision(host, decision) {
472
541
  if (decision.verdict === "allow") {
473
542
  if (host === "cursor") {
474
- return JSON.stringify({ permission: "allow" });
543
+ return JSON.stringify({ permission: "allow", code: decision.code });
475
544
  }
476
545
  return "";
477
546
  }
@@ -492,6 +561,7 @@ export function renderHostDecision(host, decision) {
492
561
  permission: "deny",
493
562
  user_message: decision.message,
494
563
  agent_message: decision.message,
564
+ code: decision.code,
495
565
  });
496
566
  }
497
567
  }
@@ -238,14 +238,22 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
238
238
  changedPaths.push(item.path);
239
239
  }
240
240
  }
241
- const adapterAbsolute = join(projectRoot, ".cursor/hooks/deft-cursor-hook-adapter.mjs");
242
- assertDepositContained(projectRoot, adapterAbsolute);
243
- if (existsSync(adapterAbsolute)) {
244
- rmSync(adapterAbsolute, { force: true });
245
- if (!changedPaths.includes(AGENT_HOOK_PATHS[2])) {
246
- changedPaths.push(AGENT_HOOK_PATHS[2]);
241
+ const legacyAdapterPaths = [
242
+ ".cursor/hooks/deft-cursor-hook-adapter.mjs",
243
+ ".cursor/hooks/deft-cursor-hook-adapter.test.mjs",
244
+ ];
245
+ let removedLegacyAdapter = false;
246
+ for (const relative of legacyAdapterPaths) {
247
+ const absolute = join(projectRoot, relative);
248
+ assertDepositContained(projectRoot, absolute);
249
+ if (existsSync(absolute)) {
250
+ rmSync(absolute, { force: true });
251
+ removedLegacyAdapter = true;
247
252
  }
248
253
  }
254
+ if (removedLegacyAdapter && !changedPaths.includes(AGENT_HOOK_PATHS[2])) {
255
+ changedPaths.push(AGENT_HOOK_PATHS[2]);
256
+ }
249
257
  if (changedPaths.length > 0) {
250
258
  io.printf(`Installed Directive agent hooks: ${changedPaths.join(", ")}\n`);
251
259
  }
@@ -11,6 +11,7 @@
11
11
  import { execFileSync } from "node:child_process";
12
12
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
14
15
  import { FORBIDDEN_BLANKET_EVAL_LINES, stripGitignoreInlineComment, } from "../triage/bootstrap/gitignore.js";
15
16
  /** Directory ignore entry for the hybrid deposit (greenfield only). */
16
17
  export const GITIGNORE_DEFT_CORE_LINE = ".deft/core/";
@@ -195,9 +196,13 @@ function reconcileGitignoreFile(projectDir, targetLines, includeDeftCoreRational
195
196
  }
196
197
  }
197
198
  try {
199
+ assertWriteTargetSafe(projectDir, path);
198
200
  writeFileSync(path, body, { encoding: "utf8", mode: 0o644 });
199
201
  }
200
202
  catch (cause) {
203
+ if (cause instanceof ProjectionContainmentError) {
204
+ throw cause;
205
+ }
201
206
  throw new Error(`could not write .gitignore: ${String(cause)}`);
202
207
  }
203
208
  const finalPresent = collectPresentGitignoreLines(body);