@deftai/directive-core 0.84.0 → 0.86.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 (79) hide show
  1. package/dist/cache/fetch.d.ts +17 -0
  2. package/dist/cache/fetch.js +50 -0
  3. package/dist/doctor/checks.js +16 -1
  4. package/dist/doctor/constants.d.ts +4 -2
  5. package/dist/doctor/constants.js +10 -6
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.d.ts +2 -1
  9. package/dist/doctor/main.js +83 -22
  10. package/dist/doctor/npm-registry.d.ts +23 -0
  11. package/dist/doctor/npm-registry.js +128 -0
  12. package/dist/doctor/npm-view.d.ts +10 -0
  13. package/dist/doctor/npm-view.js +31 -0
  14. package/dist/doctor/payload-staleness.js +2 -9
  15. package/dist/doctor/signpost-checks.d.ts +2 -1
  16. package/dist/doctor/signpost-checks.js +2 -0
  17. package/dist/doctor/types.d.ts +12 -0
  18. package/dist/eval-health-relocation/evaluate.js +15 -1
  19. package/dist/fs/projection-containment.js +1 -1
  20. package/dist/hooks/cursor-hooks.d.ts +6 -20
  21. package/dist/hooks/cursor-hooks.js +7 -92
  22. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  23. package/dist/init-deposit/agent-hooks.js +24 -38
  24. package/dist/init-deposit/gitignore.js +5 -0
  25. package/dist/init-deposit/hygiene.d.ts +22 -0
  26. package/dist/init-deposit/hygiene.js +136 -4
  27. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  28. package/dist/init-deposit/legacy-detect.js +143 -11
  29. package/dist/init-deposit/refresh.d.ts +2 -0
  30. package/dist/init-deposit/refresh.js +18 -5
  31. package/dist/policy/index.d.ts +2 -0
  32. package/dist/policy/index.js +2 -0
  33. package/dist/policy/org-force-on-migration.d.ts +44 -0
  34. package/dist/policy/org-force-on-migration.js +242 -0
  35. package/dist/policy/product-signal.d.ts +3 -1
  36. package/dist/policy/product-signal.js +15 -4
  37. package/dist/policy/value-feedback.d.ts +18 -1
  38. package/dist/policy/value-feedback.js +78 -5
  39. package/dist/preflight-cache/evaluate.js +12 -1
  40. package/dist/product-signal/consent-prompt.d.ts +13 -0
  41. package/dist/product-signal/consent-prompt.js +30 -0
  42. package/dist/product-signal/submit.js +22 -1
  43. package/dist/render/framework-commands.d.ts +1 -1
  44. package/dist/render/framework-commands.js +6 -6
  45. package/dist/render/index.d.ts +1 -0
  46. package/dist/render/index.js +1 -0
  47. package/dist/render/roadmap-render.d.ts +5 -1
  48. package/dist/render/roadmap-render.js +20 -2
  49. package/dist/render/rule-map-template.d.ts +2 -0
  50. package/dist/render/rule-map-template.js +407 -0
  51. package/dist/render/rule-map.d.ts +2 -0
  52. package/dist/render/rule-map.js +708 -0
  53. package/dist/review-monitor/constants.d.ts +7 -1
  54. package/dist/review-monitor/constants.js +40 -14
  55. package/dist/review-monitor/github-lease.d.ts +45 -0
  56. package/dist/review-monitor/github-lease.js +106 -0
  57. package/dist/review-monitor/index.d.ts +2 -0
  58. package/dist/review-monitor/index.js +2 -0
  59. package/dist/review-monitor/lease-comment.d.ts +36 -0
  60. package/dist/review-monitor/lease-comment.js +152 -0
  61. package/dist/review-monitor/record.d.ts +61 -13
  62. package/dist/review-monitor/record.js +379 -132
  63. package/dist/review-monitor/verify.d.ts +2 -0
  64. package/dist/review-monitor/verify.js +32 -13
  65. package/dist/scm/gh-rest.d.ts +7 -1
  66. package/dist/scm/gh-rest.js +35 -0
  67. package/dist/session/session-start.js +5 -0
  68. package/dist/task-surface/index.js +53 -13
  69. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  70. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  71. package/dist/verify-env/index.d.ts +1 -0
  72. package/dist/verify-env/index.js +1 -0
  73. package/dist/xbrief-migrate/constants.d.ts +6 -0
  74. package/dist/xbrief-migrate/constants.js +6 -0
  75. package/dist/xbrief-migrate/index.d.ts +2 -2
  76. package/dist/xbrief-migrate/index.js +2 -2
  77. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  78. package/dist/xbrief-migrate/migrate-project.js +44 -18
  79. package/package.json +3 -3
@@ -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;
@@ -30,6 +31,13 @@ export interface Finding {
30
31
  readonly status?: string;
31
32
  readonly [key: string]: unknown;
32
33
  }
34
+ /** Result of an offline `npm config get` query. */
35
+ export interface NpmConfigGetResult {
36
+ readonly ok: boolean;
37
+ readonly value: string;
38
+ }
39
+ /** Injectable npm-config reader used by doctor and its throttle-safe signposts. */
40
+ export type NpmConfigGet = (key: string, cwd: string) => NpmConfigGetResult;
33
41
  /**
34
42
  * The single read-only decision surface derived from the shared keystone
35
43
  * `plan()` (#2267 / epic #2203). `plan()` is the ONE classifier: doctor never
@@ -99,6 +107,8 @@ export interface DoctorSeams {
99
107
  ok: boolean;
100
108
  version: string;
101
109
  };
110
+ /** Offline npm configuration reader for effective-registry diagnostics (#2808). */
111
+ readonly runNpmConfigGet?: NpmConfigGet;
102
112
  readonly agentsRefreshPlan?: (projectRoot: string) => Record<string, unknown>;
103
113
  readonly agentsMdAdvisoryEvaluate?: (projectRoot: string) => AdvisoryEvaluateResult;
104
114
  readonly readState?: (projectRoot: string) => DoctorState | null;
@@ -138,5 +148,7 @@ export interface DoctorSeams {
138
148
  readonly detectPlanExtensionShadows?: (projectRoot: string) => readonly ShadowedPlanExtension[];
139
149
  /** Read-only agent-host hook registration probe (#2438). */
140
150
  readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
151
+ /** Live hook spawn probe for doctor --full (#2852). */
152
+ readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
141
153
  }
142
154
  //# sourceMappingURL=types.d.ts.map
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { evaluateHealth } from "../eval/health.js";
5
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
5
6
  import { resolveEvalPath } from "../layout/resolve.js";
6
7
  import { matchAny } from "../orchestration/pathspec.js";
7
8
  /** Glob patterns that classify a change as epic #2369 rule-relocation (#2373). */
@@ -57,6 +58,7 @@ export function readHealthBaseline(projectRoot) {
57
58
  /** Persist a health report as the committed baseline (#2373 Wave 2 seed). */
58
59
  export function writeHealthBaseline(projectRoot, report) {
59
60
  const path = healthBaselinePath(projectRoot);
61
+ assertWriteTargetSafe(projectRoot, path);
60
62
  mkdirSync(dirname(path), { recursive: true });
61
63
  writeFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
62
64
  }
@@ -170,7 +172,19 @@ export function evaluate(options = {}) {
170
172
  stream: "stderr",
171
173
  };
172
174
  }
173
- writeHealthBaseline(projectRoot, report);
175
+ try {
176
+ writeHealthBaseline(projectRoot, report);
177
+ }
178
+ catch (err) {
179
+ if (err instanceof ProjectionContainmentError) {
180
+ return {
181
+ code: 2,
182
+ message: `❌ verify:eval-health-relocation: ${err.message}`,
183
+ stream: "stderr",
184
+ };
185
+ }
186
+ throw err;
187
+ }
174
188
  return {
175
189
  code: 0,
176
190
  message: `✓ verify:eval-health-relocation: seeded baseline at ${HEALTH_BASELINE_REL} ` +
@@ -91,7 +91,7 @@ export function assertProjectionContained(projectDir, targetPath) {
91
91
  try {
92
92
  deepestExistingReal = realpathSync(current);
93
93
  }
94
- catch (err) {
94
+ catch {
95
95
  throw new ProjectionContainmentError(`projection write refused: could not resolve ${current} on the projection path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: current });
96
96
  }
97
97
  }
@@ -1,23 +1,9 @@
1
- /** Cursor ApplyPatch spellings handled by the project adapter, not generic write dispatch. */
1
+ /** Cursor's ApplyPatch payload is parsed directly by the shared hook executable. */
2
2
  export declare const APPLY_PATCH_TOOL_NAMES: readonly ["ApplyPatch", "apply_patch"];
3
3
  export declare const APPLY_PATCH_HOOK_MATCHER: string;
4
- /** Generic Cursor write matcher excludes ApplyPatch — adapter owns it (#2764). */
5
- export declare const CURSOR_GENERIC_WRITE_TOOL_NAMES: ("Edit" | "Write" | "WriteFile" | "CreateFile" | "MultiEdit" | "NotebookEdit" | "StrReplace" | "SearchReplace" | "Delete" | "DeleteFile" | "ApplyPatch" | "apply_patch")[];
6
- export declare const CURSOR_GENERIC_WRITE_HOOK_MATCHER: string;
7
- export declare const CURSOR_APPLY_PATCH_ADAPTER_RELATIVE = ".cursor/hooks/deft-cursor-hook-adapter.mjs";
8
- export declare const DEFT_CURSOR_ADAPTER_COMMAND_MARKER = "deft-cursor-hook-adapter.mjs";
9
- export declare const CURSOR_APPLY_PATCH_ADAPTER_COMMAND = "node .cursor/hooks/deft-cursor-hook-adapter.mjs ApplyPatch";
10
- /** Deposited adapter forwards free-form ApplyPatch stdin to hook:dispatch with explicit project root. */
11
- export declare const CURSOR_APPLY_PATCH_ADAPTER_SOURCE = "#!/usr/bin/env node\nimport { spawnSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nconst projectRoot = resolve(process.cwd());\nconst stdin = readFileSync(0, \"utf8\");\n\nfunction deftCommand() {\n for (const candidate of [\"deft\", \"directive\"]) {\n const probe = spawnSync(candidate, [\"--version\"], {\n encoding: \"utf8\",\n stdio: \"ignore\",\n shell: process.platform === \"win32\",\n windowsHide: true,\n });\n if (probe.error === undefined && probe.status === 0) return candidate;\n }\n process.stderr.write(\n \"Directive ApplyPatch adapter: neither deft nor directive is on PATH.\\n\",\n );\n process.exit(2);\n}\n\nconst cli = deftCommand();\nconst result = spawnSync(\n cli,\n [\n \"hook:dispatch\",\n \"--host\",\n \"cursor\",\n \"--event\",\n \"tool.before\",\n \"--project-root\",\n projectRoot,\n ],\n {\n input: stdin,\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n shell: process.platform === \"win32\",\n windowsHide: true,\n },\n);\n\nif (result.stderr) process.stderr.write(result.stderr);\nif (result.error) {\n process.stderr.write(String(result.error));\n process.exit(2);\n}\nif (result.status !== 0 && result.status !== null) {\n if (result.stdout) process.stdout.write(result.stdout);\n process.exit(result.status);\n}\n// Cursor failClosed treats empty stdout as failure \u2014 normalize allow.\nconst out = (result.stdout ?? \"\").trim();\nprocess.stdout.write((out.length > 0 ? out : '{\"permission\":\"allow\"}') + \"\\n\");\nprocess.exit(0);\n";
12
- export interface CursorPreToolUseEntry {
13
- readonly command: string;
14
- readonly matcher?: string;
15
- readonly failClosed?: boolean;
16
- readonly timeout?: number;
17
- }
18
- export declare function cursorApplyPatchAdapterEntry(): CursorPreToolUseEntry;
19
- /** True when generic and adapter matchers share no tool tokens. */
20
- export declare function cursorApplyPatchMatchersDisjoint(): boolean;
21
- /** Fail closed when Cursor hook projection would double-dispatch ApplyPatch (#2764). */
22
- export declare function assertCursorApplyPatchMatchersDisjoint(): void;
4
+ /**
5
+ * Cursor uses one disjoint direct-write registration: hook-dispatch recognizes
6
+ * JSON and free-form ApplyPatch payloads, avoiding an adapter process (#2790).
7
+ */
8
+ export declare const CURSOR_DIRECT_WRITE_HOOK_MATCHER: string;
23
9
  //# sourceMappingURL=cursor-hooks.d.ts.map
@@ -1,95 +1,10 @@
1
- import { DIRECT_WRITE_TOOL_NAMES } from "./tools.js";
2
- /** Cursor ApplyPatch spellings handled by the project adapter, not generic write dispatch. */
1
+ import { DIRECT_WRITE_HOOK_MATCHER } from "./tools.js";
2
+ /** Cursor's ApplyPatch payload is parsed directly by the shared hook executable. */
3
3
  export const APPLY_PATCH_TOOL_NAMES = ["ApplyPatch", "apply_patch"];
4
4
  export const APPLY_PATCH_HOOK_MATCHER = APPLY_PATCH_TOOL_NAMES.join("|");
5
- const APPLY_PATCH_TOOLS = new Set(APPLY_PATCH_TOOL_NAMES);
6
- /** Generic Cursor write matcher excludes ApplyPatch adapter owns it (#2764). */
7
- export const CURSOR_GENERIC_WRITE_TOOL_NAMES = DIRECT_WRITE_TOOL_NAMES.filter((name) => !APPLY_PATCH_TOOLS.has(name));
8
- export const CURSOR_GENERIC_WRITE_HOOK_MATCHER = CURSOR_GENERIC_WRITE_TOOL_NAMES.join("|");
9
- export const CURSOR_APPLY_PATCH_ADAPTER_RELATIVE = ".cursor/hooks/deft-cursor-hook-adapter.mjs";
10
- export const DEFT_CURSOR_ADAPTER_COMMAND_MARKER = "deft-cursor-hook-adapter.mjs";
11
- export const CURSOR_APPLY_PATCH_ADAPTER_COMMAND = `node ${CURSOR_APPLY_PATCH_ADAPTER_RELATIVE} ApplyPatch`;
12
- /** Deposited adapter forwards free-form ApplyPatch stdin to hook:dispatch with explicit project root. */
13
- export const CURSOR_APPLY_PATCH_ADAPTER_SOURCE = `#!/usr/bin/env node
14
- import { spawnSync } from "node:child_process";
15
- import { readFileSync } from "node:fs";
16
- import { resolve } from "node:path";
17
-
18
- const projectRoot = resolve(process.cwd());
19
- const stdin = readFileSync(0, "utf8");
20
-
21
- function deftCommand() {
22
- for (const candidate of ["deft", "directive"]) {
23
- const probe = spawnSync(candidate, ["--version"], {
24
- encoding: "utf8",
25
- stdio: "ignore",
26
- shell: process.platform === "win32",
27
- windowsHide: true,
28
- });
29
- if (probe.error === undefined && probe.status === 0) return candidate;
30
- }
31
- process.stderr.write(
32
- "Directive ApplyPatch adapter: neither deft nor directive is on PATH.\\n",
33
- );
34
- process.exit(2);
35
- }
36
-
37
- const cli = deftCommand();
38
- const result = spawnSync(
39
- cli,
40
- [
41
- "hook:dispatch",
42
- "--host",
43
- "cursor",
44
- "--event",
45
- "tool.before",
46
- "--project-root",
47
- projectRoot,
48
- ],
49
- {
50
- input: stdin,
51
- encoding: "utf8",
52
- stdio: ["pipe", "pipe", "pipe"],
53
- shell: process.platform === "win32",
54
- windowsHide: true,
55
- },
56
- );
57
-
58
- if (result.stderr) process.stderr.write(result.stderr);
59
- if (result.error) {
60
- process.stderr.write(String(result.error));
61
- process.exit(2);
62
- }
63
- if (result.status !== 0 && result.status !== null) {
64
- if (result.stdout) process.stdout.write(result.stdout);
65
- process.exit(result.status);
66
- }
67
- // Cursor failClosed treats empty stdout as failure — normalize allow.
68
- const out = (result.stdout ?? "").trim();
69
- process.stdout.write((out.length > 0 ? out : '{"permission":"allow"}') + "\\n");
70
- process.exit(0);
71
- `;
72
- export function cursorApplyPatchAdapterEntry() {
73
- return {
74
- command: CURSOR_APPLY_PATCH_ADAPTER_COMMAND,
75
- matcher: APPLY_PATCH_HOOK_MATCHER,
76
- failClosed: true,
77
- timeout: 5,
78
- };
79
- }
80
- /** True when generic and adapter matchers share no tool tokens. */
81
- export function cursorApplyPatchMatchersDisjoint() {
82
- const generic = new Set(CURSOR_GENERIC_WRITE_HOOK_MATCHER.split("|"));
83
- for (const token of APPLY_PATCH_HOOK_MATCHER.split("|")) {
84
- if (generic.has(token))
85
- return false;
86
- }
87
- return true;
88
- }
89
- /** Fail closed when Cursor hook projection would double-dispatch ApplyPatch (#2764). */
90
- export function assertCursorApplyPatchMatchersDisjoint() {
91
- if (!cursorApplyPatchMatchersDisjoint()) {
92
- throw new Error("Cursor ApplyPatch and generic direct-write matchers overlap — refusing hook deposit (#2764).");
93
- }
94
- }
5
+ /**
6
+ * Cursor uses one disjoint direct-write registration: hook-dispatch recognizes
7
+ * JSON and free-form ApplyPatch payloads, avoiding an adapter process (#2790).
8
+ */
9
+ export const CURSOR_DIRECT_WRITE_HOOK_MATCHER = DIRECT_WRITE_HOOK_MATCHER;
95
10
  //# sourceMappingURL=cursor-hooks.js.map
@@ -1,9 +1,9 @@
1
1
  import type { HookHost } from "../hooks/dispatcher.js";
2
2
  import { type HostHooksPolicy } from "../policy/host-hooks.js";
3
3
  import type { InitDepositIo } from "./constants.js";
4
- export { CURSOR_GENERIC_WRITE_HOOK_MATCHER, DEFT_CURSOR_ADAPTER_COMMAND_MARKER, } from "../hooks/cursor-hooks.js";
5
4
  export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
6
- export declare const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
5
+ export declare const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
6
+ export declare const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
7
7
  export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
8
8
  export type AgentHookPath = (typeof AGENT_HOOK_PATHS)[number];
9
9
  export type AgentHookRegistrationStatus = "healthy" | "missing" | "drifted";
@@ -1,12 +1,11 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { assertDepositContained } from "../deposit/contain.js";
4
- import { assertCursorApplyPatchMatchersDisjoint, CURSOR_APPLY_PATCH_ADAPTER_RELATIVE, CURSOR_APPLY_PATCH_ADAPTER_SOURCE, CURSOR_GENERIC_WRITE_HOOK_MATCHER, cursorApplyPatchAdapterEntry, DEFT_CURSOR_ADAPTER_COMMAND_MARKER, } from "../hooks/cursor-hooks.js";
5
4
  import { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
6
5
  import { isHostHookDepositEnabled, loadHostHooksPolicyFromProject, } from "../policy/host-hooks.js";
7
- export { CURSOR_GENERIC_WRITE_HOOK_MATCHER, DEFT_CURSOR_ADAPTER_COMMAND_MARKER, } from "../hooks/cursor-hooks.js";
8
6
  export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
9
- export const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
7
+ export const DEFT_HOOK_COMMAND_MARKER = "deft-hook";
8
+ export const LEGACY_DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
10
9
  export const AGENT_HOOK_PATHS = [
11
10
  ".claude/settings.json",
12
11
  ".grok/hooks/deft.json",
@@ -64,17 +63,21 @@ function nestedCommands(value) {
64
63
  });
65
64
  }
66
65
  function isManagedNestedGroup(value) {
67
- return nestedCommands(value).some((value) => value.includes(DEFT_HOOK_COMMAND_MARKER));
66
+ return nestedCommands(value).some((command) => command.includes(DEFT_HOOK_COMMAND_MARKER) ||
67
+ command.includes(LEGACY_DEFT_HOOK_COMMAND_MARKER));
68
68
  }
69
69
  function isManagedNestedGroupForHost(value, host) {
70
- return nestedCommands(value).some((command) => command.includes(DEFT_HOOK_COMMAND_MARKER) && command.includes(`--host ${host}`));
70
+ return nestedCommands(value).some((command) => (command.includes(DEFT_HOOK_COMMAND_MARKER) ||
71
+ command.includes(LEGACY_DEFT_HOOK_COMMAND_MARKER)) &&
72
+ command.includes(`--host ${host}`));
71
73
  }
72
74
  function isManagedCursorEntry(value) {
73
75
  const entry = object(value);
74
76
  if (typeof entry?.command !== "string")
75
77
  return false;
76
78
  return (entry.command.includes(DEFT_HOOK_COMMAND_MARKER) ||
77
- entry.command.includes(DEFT_CURSOR_ADAPTER_COMMAND_MARKER));
79
+ entry.command.includes(LEGACY_DEFT_HOOK_COMMAND_MARKER) ||
80
+ entry.command.includes("deft-cursor-hook-adapter.mjs"));
78
81
  }
79
82
  function isManagedCursorEntryForHost(value) {
80
83
  return isManagedCursorEntry(value);
@@ -142,7 +145,6 @@ function stripManagedCursorConfig(config, path) {
142
145
  return { ...config, version: 1, hooks: nextHooks };
143
146
  }
144
147
  function mergeCursorConfig(config, path) {
145
- assertCursorApplyPatchMatchersDisjoint();
146
148
  const hooks = hooksObject(config, path);
147
149
  const session = eventArray(hooks, "sessionStart", path).filter((entry) => !isManagedCursorEntry(entry));
148
150
  const preTool = eventArray(hooks, "preToolUse", path).filter((entry) => !isManagedCursorEntry(entry));
@@ -150,10 +152,9 @@ function mergeCursorConfig(config, path) {
150
152
  hooks.sessionStart = [...session, { command: command("cursor", "session.start"), timeout: 5 }];
151
153
  hooks.preToolUse = [
152
154
  ...preTool,
153
- cursorApplyPatchAdapterEntry(),
154
155
  {
155
156
  command: command("cursor", "tool.before"),
156
- matcher: CURSOR_GENERIC_WRITE_HOOK_MATCHER,
157
+ matcher: DIRECT_WRITE_HOOK_MATCHER,
157
158
  failClosed: true,
158
159
  timeout: 5,
159
160
  },
@@ -167,15 +168,6 @@ function mergeCursorConfig(config, path) {
167
168
  hooks.preCompact = [...preCompact, { command: command("cursor", "session.compact"), timeout: 5 }];
168
169
  return { ...config, version: 1, hooks };
169
170
  }
170
- function writeTextIfChanged(path, contents) {
171
- if (existsSync(path) && readFileSync(path, "utf8") === contents)
172
- return false;
173
- mkdirSync(dirname(path), { recursive: true });
174
- const temporary = `${path}.deft-${process.pid}.tmp`;
175
- writeFileSync(temporary, contents, "utf8");
176
- renameSync(temporary, path);
177
- return true;
178
- }
179
171
  function writeJsonIfChanged(path, payload) {
180
172
  const next = `${JSON.stringify(payload, null, 2)}\n`;
181
173
  if (existsSync(path) && readFileSync(path, "utf8") === next)
@@ -246,20 +238,21 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
246
238
  changedPaths.push(item.path);
247
239
  }
248
240
  }
249
- const adapterAbsolute = join(projectRoot, CURSOR_APPLY_PATCH_ADAPTER_RELATIVE);
250
- assertDepositContained(projectRoot, adapterAbsolute);
251
- if (isHostHookDepositEnabled("cursor", hostHooksPolicy)) {
252
- if (writeTextIfChanged(adapterAbsolute, CURSOR_APPLY_PATCH_ADAPTER_SOURCE)) {
253
- if (!changedPaths.includes(AGENT_HOOK_PATHS[2])) {
254
- changedPaths.push(AGENT_HOOK_PATHS[2]);
255
- }
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;
256
252
  }
257
253
  }
258
- else if (existsSync(adapterAbsolute)) {
259
- rmSync(adapterAbsolute, { force: true });
260
- if (!strippedPaths.includes(AGENT_HOOK_PATHS[2])) {
261
- strippedPaths.push(AGENT_HOOK_PATHS[2]);
262
- }
254
+ if (removedLegacyAdapter && !changedPaths.includes(AGENT_HOOK_PATHS[2])) {
255
+ changedPaths.push(AGENT_HOOK_PATHS[2]);
263
256
  }
264
257
  if (changedPaths.length > 0) {
265
258
  io.printf(`Installed Directive agent hooks: ${changedPaths.join(", ")}\n`);
@@ -309,18 +302,11 @@ function hasCursorRegistration(config) {
309
302
  const session = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
310
303
  const preTool = Array.isArray(hooks.preToolUse) ? hooks.preToolUse : [];
311
304
  const preCompact = Array.isArray(hooks.preCompact) ? hooks.preCompact : [];
312
- const adapter = cursorApplyPatchAdapterEntry();
313
305
  return (session.some((entry) => object(entry)?.command === command("cursor", "session.start")) &&
314
306
  preTool.some((entry) => {
315
307
  const hook = object(entry);
316
308
  return (hook?.command === command("cursor", "tool.before") &&
317
- hook.matcher === CURSOR_GENERIC_WRITE_HOOK_MATCHER &&
318
- hook.failClosed === true);
319
- }) &&
320
- preTool.some((entry) => {
321
- const hook = object(entry);
322
- return (hook?.command === adapter.command &&
323
- hook.matcher === adapter.matcher &&
309
+ hook.matcher === DIRECT_WRITE_HOOK_MATCHER &&
324
310
  hook.failClosed === true);
325
311
  }) &&
326
312
  preTool.some((entry) => {
@@ -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);
@@ -57,6 +57,28 @@ export declare function stageFrameworkPaths(projectDir: string, paths: readonly
57
57
  * Refs #2347.
58
58
  */
59
59
  export declare const STRAY_DEPOSIT_FRAMEWORK_PATHS: readonly ["packages"];
60
+ /**
61
+ * Deposit-local files generated by init/update that are intentionally absent
62
+ * from `@deftai/directive-content` (#2804).
63
+ */
64
+ export declare const DEPOSIT_GENERATED_METADATA_PATHS: readonly ["VERSION"];
65
+ export declare function isDepositGeneratedMetadata(relPath: string): boolean;
66
+ /**
67
+ * List deposit-relative paths that are absent from the installed content package,
68
+ * excluding generated deposit metadata such as `VERSION` (#2804).
69
+ */
70
+ export declare function findPackageAbsentDepositPathsSync(deftDir: string, contentRoot: string): string[];
71
+ export declare function findPackageAbsentDepositPaths(deftDir: string, contentRoot: string): Promise<string[]>;
72
+ export interface PrunePackageAbsentDepositPathsResult {
73
+ readonly pruned: string[];
74
+ readonly prunedDirs: string[];
75
+ }
76
+ /**
77
+ * Remove deposit files not shipped by `@deftai/directive-content`, preserving
78
+ * generated deposit metadata such as `VERSION` (#2804). Subsumes the legacy
79
+ * hard-coded `packages/` prune (#2347).
80
+ */
81
+ export declare function prunePackageAbsentDepositPaths(deftDir: string, contentRoot: string, io: InitDepositIo): Promise<PrunePackageAbsentDepositPathsResult>;
60
82
  export interface PruneStrayDepositPathsResult {
61
83
  readonly pruned: string[];
62
84
  }
@@ -5,9 +5,9 @@
5
5
  * Refs #1576, #1453, #1430.
6
6
  */
7
7
  import { execFileSync } from "node:child_process";
8
- import { existsSync } from "node:fs";
9
- import { rm, stat } from "node:fs/promises";
10
- import { join, relative } from "node:path";
8
+ import { existsSync, readdirSync, rmSync } from "node:fs";
9
+ import { readdir, rm, stat } from "node:fs/promises";
10
+ import { dirname, join, relative } from "node:path";
11
11
  import { gitPorcelain } from "../story-ready/git.js";
12
12
  import { CANONICAL_INSTALL_ROOT } from "./constants.js";
13
13
  export const CODEQL_CONFIG_REL = ".github/codeql/codeql-config.yml";
@@ -24,7 +24,6 @@ export function installerManagedMatchers() {
24
24
  { exact: ".claude/settings.json" },
25
25
  { exact: ".grok/hooks/deft.json" },
26
26
  { exact: ".cursor/hooks.json" },
27
- { exact: ".cursor/hooks/deft-cursor-hook-adapter.mjs" },
28
27
  { exact: ".codex/hooks.json" },
29
28
  { exact: ".gitattributes" },
30
29
  { exact: ".gitignore" },
@@ -146,6 +145,139 @@ export function stageFrameworkPaths(projectDir, paths, seams = {}) {
146
145
  * Refs #2347.
147
146
  */
148
147
  export const STRAY_DEPOSIT_FRAMEWORK_PATHS = ["packages"];
148
+ /**
149
+ * Deposit-local files generated by init/update that are intentionally absent
150
+ * from `@deftai/directive-content` (#2804).
151
+ */
152
+ export const DEPOSIT_GENERATED_METADATA_PATHS = ["VERSION"];
153
+ export function isDepositGeneratedMetadata(relPath) {
154
+ const normalized = relPath.replace(/\\/g, "/");
155
+ return DEPOSIT_GENERATED_METADATA_PATHS.includes(normalized);
156
+ }
157
+ function normalizeRelativePath(relPath) {
158
+ return relPath.replace(/\\/g, "/");
159
+ }
160
+ function listRelativeFilePathsSync(root) {
161
+ if (!existsSync(root))
162
+ return [];
163
+ const results = [];
164
+ const walk = (dir, prefix) => {
165
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
166
+ const rel = normalizeRelativePath(prefix ? `${prefix}/${entry.name}` : entry.name);
167
+ const abs = join(dir, entry.name);
168
+ if (entry.isSymbolicLink())
169
+ continue;
170
+ if (entry.isDirectory()) {
171
+ walk(abs, rel);
172
+ }
173
+ else if (entry.isFile()) {
174
+ results.push(rel);
175
+ }
176
+ }
177
+ };
178
+ walk(root, "");
179
+ return results;
180
+ }
181
+ async function listRelativeFilePaths(root) {
182
+ if (!existsSync(root))
183
+ return [];
184
+ const results = [];
185
+ const walk = async (dir, prefix) => {
186
+ const entries = await readdir(dir, { withFileTypes: true });
187
+ for (const entry of entries) {
188
+ const rel = normalizeRelativePath(prefix ? `${prefix}/${entry.name}` : entry.name);
189
+ const abs = join(dir, entry.name);
190
+ if (entry.isSymbolicLink())
191
+ continue;
192
+ if (entry.isDirectory()) {
193
+ await walk(abs, rel);
194
+ }
195
+ else if (entry.isFile()) {
196
+ results.push(rel);
197
+ }
198
+ }
199
+ };
200
+ await walk(root, "");
201
+ return results;
202
+ }
203
+ function contentDirectoryPaths(contentFiles) {
204
+ const dirs = new Set();
205
+ for (const file of contentFiles) {
206
+ let dir = dirname(file);
207
+ while (dir && dir !== ".") {
208
+ dirs.add(normalizeRelativePath(dir));
209
+ const parent = dirname(dir);
210
+ if (parent === dir)
211
+ break;
212
+ dir = parent;
213
+ }
214
+ }
215
+ return dirs;
216
+ }
217
+ /**
218
+ * List deposit-relative paths that are absent from the installed content package,
219
+ * excluding generated deposit metadata such as `VERSION` (#2804).
220
+ */
221
+ export function findPackageAbsentDepositPathsSync(deftDir, contentRoot) {
222
+ const depositFiles = listRelativeFilePathsSync(deftDir);
223
+ const contentFiles = new Set(listRelativeFilePathsSync(contentRoot));
224
+ return depositFiles
225
+ .filter((rel) => !contentFiles.has(rel) && !isDepositGeneratedMetadata(rel))
226
+ .sort();
227
+ }
228
+ export async function findPackageAbsentDepositPaths(deftDir, contentRoot) {
229
+ const depositFiles = await listRelativeFilePaths(deftDir);
230
+ const contentFiles = new Set(await listRelativeFilePaths(contentRoot));
231
+ return depositFiles
232
+ .filter((rel) => !contentFiles.has(rel) && !isDepositGeneratedMetadata(rel))
233
+ .sort();
234
+ }
235
+ async function pruneEmptyParentsForFile(deftDir, contentDirs, relFile) {
236
+ const prunedDirs = [];
237
+ let rel = dirname(relFile);
238
+ while (rel && rel !== ".") {
239
+ const normalized = normalizeRelativePath(rel);
240
+ if (contentDirs.has(normalized))
241
+ break;
242
+ const abs = join(deftDir, rel);
243
+ try {
244
+ if (!existsSync(abs) || readdirSync(abs).length !== 0)
245
+ break;
246
+ rmSync(abs, { recursive: false, force: true });
247
+ prunedDirs.push(normalized);
248
+ }
249
+ catch {
250
+ break;
251
+ }
252
+ rel = dirname(rel);
253
+ }
254
+ return prunedDirs;
255
+ }
256
+ /**
257
+ * Remove deposit files not shipped by `@deftai/directive-content`, preserving
258
+ * generated deposit metadata such as `VERSION` (#2804). Subsumes the legacy
259
+ * hard-coded `packages/` prune (#2347).
260
+ */
261
+ export async function prunePackageAbsentDepositPaths(deftDir, contentRoot, io) {
262
+ const absent = await findPackageAbsentDepositPaths(deftDir, contentRoot);
263
+ const contentDirs = contentDirectoryPaths(listRelativeFilePathsSync(contentRoot));
264
+ const pruned = [];
265
+ const prunedDirs = [];
266
+ for (const rel of absent) {
267
+ try {
268
+ rmSync(join(deftDir, rel), { force: true });
269
+ pruned.push(rel);
270
+ prunedDirs.push(...(await pruneEmptyParentsForFile(deftDir, contentDirs, rel)));
271
+ }
272
+ catch (cause) {
273
+ io.printf(`Warning: could not prune .deft/core/${rel}: ${String(cause)}\n`);
274
+ }
275
+ }
276
+ if (pruned.length > 0) {
277
+ io.printf(`Pruned ${pruned.length} package-absent deposit file(s) not shipped by @deftai/directive-content (#2804).\n`);
278
+ }
279
+ return { pruned, prunedDirs };
280
+ }
149
281
  /**
150
282
  * Remove framework-source subdirectories from `.deft/core/` that are not
151
283
  * present in the deposited content package (#2347). Silently skips any path
@@ -25,7 +25,7 @@
25
25
  */
26
26
  /** Non-zero exit code for a use-time legacy-layout refusal (needs-action). */
27
27
  export declare const LEGACY_LAYOUT_REFUSED_EXIT_CODE = 2;
28
- export type LegacyLayoutKind = "orphan-deft-version" | "legacy-deft-prefixed" | "git-clone-or-submodule" | "pre-v0.27-sentinel-agents-md";
28
+ export type LegacyLayoutKind = "orphan-deft-version" | "legacy-deft-prefixed" | "git-clone-or-submodule" | "pre-v0.27-sentinel-agents-md" | "dual-layout";
29
29
  export interface LegacyLayoutDetection {
30
30
  readonly legacy: boolean;
31
31
  readonly kind: LegacyLayoutKind | null;
@@ -37,6 +37,12 @@ export interface LegacyDetectSeams {
37
37
  readonly isDir?: (p: string) => boolean;
38
38
  readonly readText?: (p: string) => string | null;
39
39
  }
40
+ /**
41
+ * Detect a post-bridge dual-layout: canonical `.deft/core/` plus a leftover
42
+ * framework-marked `deft/` tree. Doctor-only -- the npm deposit path treats
43
+ * `.deft/core/` as canonical and must not refuse here (#2805 / #2804 wiring).
44
+ */
45
+ export declare function detectDualLayout(projectDir: string, seams?: LegacyDetectSeams): LegacyLayoutDetection | null;
40
46
  /**
41
47
  * Classify the on-disk layout at `projectDir`. Returns `legacy: false` for the
42
48
  * canonical `.deft/core/` vendored layout and for a greenfield project (so init
@@ -58,4 +64,22 @@ export declare function buildLegacyRefusalMessage(command: "init" | "update", de
58
64
  export declare function buildLegacyRefusalJson(command: "init" | "update", projectDir: string, detection: LegacyLayoutDetection): Record<string, unknown>;
59
65
  /** One-line doctor signpost carrying the stable URL (no baked version/command). */
60
66
  export declare function legacyLayoutSignpostLine(detection: LegacyLayoutDetection): string;
67
+ /** Doctor / npm signpost for the post-bridge dual-layout state (#2805). */
68
+ export declare function dualLayoutSignpostLine(detection: LegacyLayoutDetection): string;
69
+ export interface LegacyDeftCleanupSeams {
70
+ readonly gitPorcelain?: (projectRoot: string) => string | null;
71
+ readonly runGitRm?: (projectDir: string, relPath: string) => void;
72
+ readonly removeDir?: (absPath: string) => void;
73
+ }
74
+ export type LegacyDeftCleanupAction = "removed" | "staged" | "refused" | "skipped";
75
+ export interface LegacyDeftCleanupResult {
76
+ readonly ok: boolean;
77
+ readonly action: LegacyDeftCleanupAction;
78
+ readonly detail: string;
79
+ }
80
+ /**
81
+ * Remove a clean tracked legacy `deft/` tree after bridge migration (#2805).
82
+ * Refuses when the tree has local modifications or untracked files.
83
+ */
84
+ export declare function tryCleanupLegacyDeftTree(projectDir: string, seams?: LegacyDeftCleanupSeams): LegacyDeftCleanupResult;
61
85
  //# sourceMappingURL=legacy-detect.d.ts.map