@deftai/directive-core 0.83.0 → 0.85.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 (110) hide show
  1. package/dist/cache/fetch.d.ts +17 -0
  2. package/dist/cache/fetch.js +50 -0
  3. package/dist/cache/main.js +36 -2
  4. package/dist/cache/task-cache/constants.d.ts +4 -0
  5. package/dist/cache/task-cache/constants.js +4 -0
  6. package/dist/cache/task-cache/executor.d.ts +9 -0
  7. package/dist/cache/task-cache/executor.js +51 -0
  8. package/dist/cache/task-cache/hash.d.ts +17 -0
  9. package/dist/cache/task-cache/hash.js +92 -0
  10. package/dist/cache/task-cache/index.d.ts +14 -0
  11. package/dist/cache/task-cache/index.js +15 -0
  12. package/dist/cache/task-cache/lint.d.ts +4 -0
  13. package/dist/cache/task-cache/lint.js +55 -0
  14. package/dist/cache/task-cache/registry.d.ts +7 -0
  15. package/dist/cache/task-cache/registry.js +67 -0
  16. package/dist/cache/task-cache/store.d.ts +10 -0
  17. package/dist/cache/task-cache/store.js +48 -0
  18. package/dist/cache/task-cache/types.d.ts +48 -0
  19. package/dist/cache/task-cache/types.js +3 -0
  20. package/dist/check/cached-orchestrator.d.ts +16 -0
  21. package/dist/check/cached-orchestrator.js +76 -0
  22. package/dist/check/context.d.ts +30 -0
  23. package/dist/check/context.js +28 -0
  24. package/dist/check/gate-lists.d.ts +18 -0
  25. package/dist/check/gate-lists.js +68 -0
  26. package/dist/check/index.d.ts +4 -1
  27. package/dist/check/index.js +3 -0
  28. package/dist/check/orchestrator.d.ts +3 -45
  29. package/dist/check/orchestrator.js +8 -46
  30. package/dist/check/runner-detect.d.ts +20 -0
  31. package/dist/check/runner-detect.js +131 -0
  32. package/dist/doctor/checks.js +16 -1
  33. package/dist/doctor/constants.d.ts +2 -0
  34. package/dist/doctor/constants.js +8 -4
  35. package/dist/doctor/index.d.ts +1 -0
  36. package/dist/doctor/index.js +1 -0
  37. package/dist/doctor/main.js +13 -4
  38. package/dist/doctor/npm-registry.d.ts +23 -0
  39. package/dist/doctor/npm-registry.js +128 -0
  40. package/dist/doctor/npm-view.d.ts +10 -0
  41. package/dist/doctor/npm-view.js +31 -0
  42. package/dist/doctor/payload-staleness.js +2 -9
  43. package/dist/doctor/signpost-checks.d.ts +2 -1
  44. package/dist/doctor/signpost-checks.js +2 -0
  45. package/dist/doctor/types.d.ts +9 -0
  46. package/dist/eval/readback.js +6 -1
  47. package/dist/eval-health-relocation/evaluate.js +15 -1
  48. package/dist/fs/projection-containment.js +1 -1
  49. package/dist/hooks/cursor-hooks.d.ts +6 -20
  50. package/dist/hooks/cursor-hooks.js +7 -92
  51. package/dist/hooks/dispatcher.d.ts +2 -0
  52. package/dist/hooks/dispatcher.js +48 -6
  53. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  54. package/dist/init-deposit/agent-hooks.js +15 -37
  55. package/dist/init-deposit/gitignore.js +1 -0
  56. package/dist/init-deposit/hygiene.d.ts +22 -0
  57. package/dist/init-deposit/hygiene.js +136 -4
  58. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  59. package/dist/init-deposit/legacy-detect.js +143 -11
  60. package/dist/init-deposit/refresh.d.ts +2 -0
  61. package/dist/init-deposit/refresh.js +18 -5
  62. package/dist/policy/index.d.ts +2 -0
  63. package/dist/policy/index.js +2 -0
  64. package/dist/policy/org-force-on-migration.d.ts +44 -0
  65. package/dist/policy/org-force-on-migration.js +240 -0
  66. package/dist/policy/product-signal.d.ts +3 -1
  67. package/dist/policy/product-signal.js +15 -4
  68. package/dist/policy/value-feedback.d.ts +18 -1
  69. package/dist/policy/value-feedback.js +78 -5
  70. package/dist/preflight-cache/evaluate.js +12 -1
  71. package/dist/product-signal/consent-prompt.d.ts +13 -0
  72. package/dist/product-signal/consent-prompt.js +30 -0
  73. package/dist/product-signal/submit.js +22 -1
  74. package/dist/render/index.d.ts +1 -0
  75. package/dist/render/index.js +1 -0
  76. package/dist/render/rule-map-template.d.ts +2 -0
  77. package/dist/render/rule-map-template.js +407 -0
  78. package/dist/render/rule-map.d.ts +2 -0
  79. package/dist/render/rule-map.js +703 -0
  80. package/dist/review-monitor/constants.d.ts +7 -1
  81. package/dist/review-monitor/constants.js +40 -14
  82. package/dist/review-monitor/github-lease.d.ts +45 -0
  83. package/dist/review-monitor/github-lease.js +106 -0
  84. package/dist/review-monitor/index.d.ts +2 -0
  85. package/dist/review-monitor/index.js +2 -0
  86. package/dist/review-monitor/lease-comment.d.ts +36 -0
  87. package/dist/review-monitor/lease-comment.js +152 -0
  88. package/dist/review-monitor/record.d.ts +61 -13
  89. package/dist/review-monitor/record.js +379 -132
  90. package/dist/review-monitor/verify.d.ts +2 -0
  91. package/dist/review-monitor/verify.js +32 -13
  92. package/dist/scm/gh-rest.d.ts +7 -1
  93. package/dist/scm/gh-rest.js +35 -0
  94. package/dist/scope/decompose.js +9 -3
  95. package/dist/session/git.d.ts +2 -0
  96. package/dist/session/git.js +14 -0
  97. package/dist/session/session-start.js +5 -0
  98. package/dist/session/verify-session-ritual.js +39 -5
  99. package/dist/swarm/routing-set-cli.js +16 -5
  100. package/dist/swarm/routing.d.ts +1 -1
  101. package/dist/swarm/routing.js +3 -1
  102. package/dist/task-surface/index.js +53 -13
  103. package/dist/value/readback.js +6 -1
  104. package/dist/xbrief-migrate/constants.d.ts +6 -0
  105. package/dist/xbrief-migrate/constants.js +6 -0
  106. package/dist/xbrief-migrate/index.d.ts +2 -2
  107. package/dist/xbrief-migrate/index.js +2 -2
  108. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  109. package/dist/xbrief-migrate/migrate-project.js +18 -1
  110. package/package.json +7 -3
@@ -0,0 +1,23 @@
1
+ import type { OutputSink } from "./output.js";
2
+ import type { Finding, NpmConfigGet, NpmConfigGetResult } from "./types.js";
3
+ /** Seams for the read-only npm registry routing diagnostic. */
4
+ export interface NpmRegistryMirrorSeams {
5
+ readonly runNpmConfigGet?: NpmConfigGet;
6
+ }
7
+ /**
8
+ * Read one npm configuration key without a shell.
9
+ *
10
+ * `npm config get` reads local/user configuration only; it does not resolve a
11
+ * package or contact a registry. Failures are returned explicitly so doctor can
12
+ * skip this advisory without turning a missing npm binary into a hard error.
13
+ */
14
+ export declare function defaultNpmConfigGet(key: string, cwd: string): NpmConfigGetResult;
15
+ /**
16
+ * Warn when `@deftai/*` resolves through a non-public npm registry.
17
+ *
18
+ * The finding intentionally records only whether the scoped or default key won
19
+ * precedence. It never includes the configured URL, which may contain internal
20
+ * hostnames or credentials.
21
+ */
22
+ export declare function runNpmRegistryMirrorCheck(projectRoot: string, sink: OutputSink, addFinding: (finding: Finding) => void, seams?: NpmRegistryMirrorSeams): void;
23
+ //# sourceMappingURL=npm-registry.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { NPM_REGISTRY_MIRROR_DOC_URL, PUBLIC_NPM_REGISTRY } from "./constants.js";
3
+ const CHECK_NAME = "npm-registry-mirror";
4
+ const DEFT_SCOPE_REGISTRY_KEY = "@deftai:registry";
5
+ const DEFAULT_REGISTRY_KEY = "registry";
6
+ const UNSET_NPM_VALUES = new Set(["", "null", "undefined"]);
7
+ /**
8
+ * Read one npm configuration key without a shell.
9
+ *
10
+ * `npm config get` reads local/user configuration only; it does not resolve a
11
+ * package or contact a registry. Failures are returned explicitly so doctor can
12
+ * skip this advisory without turning a missing npm binary into a hard error.
13
+ */
14
+ export function defaultNpmConfigGet(key, cwd) {
15
+ try {
16
+ const proc = spawnSync("npm", ["config", "get", key], {
17
+ cwd,
18
+ encoding: "utf8",
19
+ shell: false,
20
+ timeout: 5_000,
21
+ windowsHide: true,
22
+ });
23
+ return {
24
+ ok: proc.status === 0,
25
+ value: typeof proc.stdout === "string" ? proc.stdout.trim() : "",
26
+ };
27
+ }
28
+ catch {
29
+ return { ok: false, value: "" };
30
+ }
31
+ }
32
+ function configuredValue(value) {
33
+ const trimmed = value.trim();
34
+ return UNSET_NPM_VALUES.has(trimmed.toLowerCase()) ? null : trimmed;
35
+ }
36
+ function effectiveRegistry(runConfigGet, projectRoot) {
37
+ let scoped;
38
+ try {
39
+ scoped = runConfigGet(DEFT_SCOPE_REGISTRY_KEY, projectRoot);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ if (!scoped.ok) {
45
+ return null;
46
+ }
47
+ const scopedValue = configuredValue(scoped.value);
48
+ if (scopedValue !== null) {
49
+ return { source: "scoped", value: scopedValue };
50
+ }
51
+ let fallback;
52
+ try {
53
+ fallback = runConfigGet(DEFAULT_REGISTRY_KEY, projectRoot);
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ if (!fallback.ok) {
59
+ return null;
60
+ }
61
+ const fallbackValue = configuredValue(fallback.value);
62
+ return fallbackValue === null ? null : { source: "default", value: fallbackValue };
63
+ }
64
+ function isValidRegistryUrl(value) {
65
+ try {
66
+ const parsed = new URL(value);
67
+ return (parsed.protocol === "https:" || parsed.protocol === "http:") && parsed.hostname !== "";
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ function isCanonicalPublicRegistry(value) {
74
+ try {
75
+ const parsed = new URL(value);
76
+ return (parsed.protocol === "https:" &&
77
+ parsed.hostname === "registry.npmjs.org" &&
78
+ parsed.port === "" &&
79
+ (parsed.pathname === "" || parsed.pathname === "/") &&
80
+ parsed.username === "" &&
81
+ parsed.password === "" &&
82
+ parsed.search === "" &&
83
+ parsed.hash === "");
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function skipFinding(addFinding) {
90
+ addFinding({
91
+ severity: "skip",
92
+ message: `${CHECK_NAME}: skip -- npm registry configuration is unavailable or invalid`,
93
+ check: CHECK_NAME,
94
+ status: "skip",
95
+ });
96
+ }
97
+ /**
98
+ * Warn when `@deftai/*` resolves through a non-public npm registry.
99
+ *
100
+ * The finding intentionally records only whether the scoped or default key won
101
+ * precedence. It never includes the configured URL, which may contain internal
102
+ * hostnames or credentials.
103
+ */
104
+ export function runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams = {}) {
105
+ const selected = effectiveRegistry(seams.runNpmConfigGet ?? defaultNpmConfigGet, projectRoot);
106
+ if (selected === null || !isValidRegistryUrl(selected.value)) {
107
+ skipFinding(addFinding);
108
+ return;
109
+ }
110
+ if (isCanonicalPublicRegistry(selected.value)) {
111
+ return;
112
+ }
113
+ const message = `${CHECK_NAME}: @deftai packages resolve through a non-public npm registry. ` +
114
+ "Corporate mirrors can return E404/ETARGET or silently serve stale @latest metadata. " +
115
+ `Where organization policy permits, retry with \`npm i -g @deftai/directive@latest --registry=${PUBLIC_NPM_REGISTRY}\` ` +
116
+ `or add \`@deftai:registry=${PUBLIC_NPM_REGISTRY}\` to \`.npmrc\`. ` +
117
+ `Otherwise ask your registry administrator to sync the @deftai packages. See ${NPM_REGISTRY_MIRROR_DOC_URL}.`;
118
+ sink.warn(message);
119
+ addFinding({
120
+ severity: "warning",
121
+ message,
122
+ check: CHECK_NAME,
123
+ status: "non-public",
124
+ registry_source: selected.source,
125
+ suggestion: NPM_REGISTRY_MIRROR_DOC_URL,
126
+ });
127
+ }
128
+ //# sourceMappingURL=npm-registry.js.map
@@ -0,0 +1,10 @@
1
+ export interface NpmViewVersionResult {
2
+ readonly ok: boolean;
3
+ readonly version: string;
4
+ }
5
+ /**
6
+ * Query the canonical public registry for the latest published Directive
7
+ * version, independent of the consumer's configured corporate mirror.
8
+ */
9
+ export declare function defaultNpmViewVersion(): NpmViewVersionResult;
10
+ //# sourceMappingURL=npm-view.d.ts.map
@@ -0,0 +1,31 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { NPM_PACKAGE_NAME, PUBLIC_NPM_REGISTRY } from "./constants.js";
3
+ /**
4
+ * Query the canonical public registry for the latest published Directive
5
+ * version, independent of the consumer's configured corporate mirror.
6
+ */
7
+ export function defaultNpmViewVersion() {
8
+ try {
9
+ const proc = spawnSync("npm", [
10
+ "view",
11
+ NPM_PACKAGE_NAME,
12
+ "version",
13
+ `--registry=${PUBLIC_NPM_REGISTRY}`,
14
+ "--ignore-scripts",
15
+ ], {
16
+ encoding: "utf8",
17
+ shell: false,
18
+ timeout: 15_000,
19
+ windowsHide: true,
20
+ });
21
+ if (proc.error !== undefined || proc.status !== 0) {
22
+ return { ok: false, version: "" };
23
+ }
24
+ const version = (typeof proc.stdout === "string" ? proc.stdout : "").trim().split("\n")[0]?.trim() ?? "";
25
+ return { ok: version.length > 0, version };
26
+ }
27
+ catch {
28
+ return { ok: false, version: "" };
29
+ }
30
+ }
31
+ //# sourceMappingURL=npm-view.js.map
@@ -2,8 +2,9 @@ import { spawnSync } from "node:child_process";
2
2
  import { dirname, join } from "node:path";
3
3
  import { detectCanonicalVendoredManifest, isNpmManaged } from "../init-deposit/migrate.js";
4
4
  import { isPublishable } from "../release/version.js";
5
- import { CANONICAL_UPGRADE_COMMAND, NPM_PACKAGE_NAME, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
5
+ import { CANONICAL_UPGRADE_COMMAND, upgradeCommandFor, VENDORED_NPM_DEPOSIT_UPGRADE_COMMAND, } from "./constants.js";
6
6
  import { locateManifest, parseInstallManifest } from "./manifest.js";
7
+ import { defaultNpmViewVersion } from "./npm-view.js";
7
8
  import { readTextSafe, resolveDefaultFrameworkRoot } from "./paths.js";
8
9
  import { evaluateReleaseAvailability } from "./release-availability.js";
9
10
  function isDeftFrameworkRepo(projectRoot, readText = readTextSafe) {
@@ -40,14 +41,6 @@ function parseRemoteSha(stdout) {
40
41
  const firstLine = stdout.split("\n").find((ln) => ln.trim()) ?? "";
41
42
  return firstLine.trim().split(/\s+/)[0] ?? "";
42
43
  }
43
- function defaultNpmViewVersion() {
44
- const proc = spawnSync("npm", ["view", NPM_PACKAGE_NAME, "version"], {
45
- encoding: "utf8",
46
- timeout: 15_000,
47
- });
48
- const version = (proc.stdout ?? "").trim().split("\n")[0]?.trim() ?? "";
49
- return { ok: proc.status === 0 && version.length > 0, version };
50
- }
51
44
  function emitReleaseAvailable(checkName, installedVersion, latestVersion, ref, upgradeCommand, sink, addFinding) {
52
45
  const msg = `Newer framework release available (installed v${installedVersion}; ` +
53
46
  `latest v${latestVersion} from npm registry). ` +
@@ -1,7 +1,8 @@
1
1
  import { type CheckSeams } from "./checks.js";
2
+ import { type NpmRegistryMirrorSeams } from "./npm-registry.js";
2
3
  import type { OutputSink } from "./output.js";
3
4
  import type { Finding } from "./types.js";
4
- export interface LocalSignpostSeams extends CheckSeams {
5
+ export interface LocalSignpostSeams extends CheckSeams, NpmRegistryMirrorSeams {
5
6
  readonly runningInsideDeftRepo?: (root: string) => boolean;
6
7
  }
7
8
  /** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
@@ -1,4 +1,5 @@
1
1
  import { checkCanonicalVendoredNpmSignpost, checkLegacyLayout } from "./checks.js";
2
+ import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
2
3
  import { runningInsideDeftRepo } from "./paths.js";
3
4
  /** Lightweight, local-only signpost probes for the throttle-skip path (#1997). */
4
5
  export function runLocalSignpostChecks(projectRoot, sink, addFinding, seams = {}) {
@@ -6,6 +7,7 @@ export function runLocalSignpostChecks(projectRoot, sink, addFinding, seams = {}
6
7
  if (insideDeft) {
7
8
  return;
8
9
  }
10
+ runNpmRegistryMirrorCheck(projectRoot, sink, addFinding, seams);
9
11
  for (const result of [
10
12
  checkLegacyLayout(projectRoot, seams),
11
13
  checkCanonicalVendoredNpmSignpost(projectRoot, seams),
@@ -30,6 +30,13 @@ export interface Finding {
30
30
  readonly status?: string;
31
31
  readonly [key: string]: unknown;
32
32
  }
33
+ /** Result of an offline `npm config get` query. */
34
+ export interface NpmConfigGetResult {
35
+ readonly ok: boolean;
36
+ readonly value: string;
37
+ }
38
+ /** Injectable npm-config reader used by doctor and its throttle-safe signposts. */
39
+ export type NpmConfigGet = (key: string, cwd: string) => NpmConfigGetResult;
33
40
  /**
34
41
  * The single read-only decision surface derived from the shared keystone
35
42
  * `plan()` (#2267 / epic #2203). `plan()` is the ONE classifier: doctor never
@@ -99,6 +106,8 @@ export interface DoctorSeams {
99
106
  ok: boolean;
100
107
  version: string;
101
108
  };
109
+ /** Offline npm configuration reader for effective-registry diagnostics (#2808). */
110
+ readonly runNpmConfigGet?: NpmConfigGet;
102
111
  readonly agentsRefreshPlan?: (projectRoot: string) => Record<string, unknown>;
103
112
  readonly agentsMdAdvisoryEvaluate?: (projectRoot: string) => AdvisoryEvaluateResult;
104
113
  readonly readState?: (projectRoot: string) => DoctorState | null;
@@ -1,5 +1,6 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
3
4
  import { MAX_LINE_CHARS } from "../triage/welcome/constants.js";
4
5
  import { evaluateHealth, healthHistoryPath } from "./health.js";
5
6
  /** Repeat-suppression window for the budgeted eval session nudge (#1703 / #1279 parity). */
@@ -124,10 +125,14 @@ function appendEvalReadbackHistory(projectRoot, nudgeKey, line, options = {}) {
124
125
  line,
125
126
  };
126
127
  try {
128
+ assertWriteTargetSafe(projectRoot, path);
127
129
  mkdirSync(join(path, ".."), { recursive: true });
128
130
  appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
129
131
  }
130
- catch {
132
+ catch (err) {
133
+ if (err instanceof ProjectionContainmentError) {
134
+ throw err;
135
+ }
131
136
  // observability only
132
137
  }
133
138
  }
@@ -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
@@ -67,6 +67,8 @@ export declare function toProjectRelativePosix(projectRoot: string, targetPath:
67
67
  * planning, not implementation dispatch — exempt from the active-scope gate (#2625).
68
68
  */
69
69
  export declare function isProposedLifecycleWrite(projectRoot: string, targetPath: string | null): boolean;
70
+ /** Normalize hook project-root resolution on Windows (doubled drive + MSYS `/c/...`). */
71
+ export declare function normalizeHookProjectRoot(path: string): string;
70
72
  export declare function projectRootFromHookPayload(payload: unknown, fallback: string): string;
71
73
  export declare function isHookHost(value: string): value is HookHost;
72
74
  export declare function isHookEvent(value: string): value is HookEvent;
@@ -164,17 +164,59 @@ export function isProposedLifecycleWrite(projectRoot, targetPath) {
164
164
  function isWindowsDriveOnlyRoot(value) {
165
165
  return /^[A-Za-z]:[/\\]?$/.test(value.trim());
166
166
  }
167
+ function hookPayloadRootCandidates(input) {
168
+ const candidates = [];
169
+ const push = (value) => {
170
+ if (typeof value === "string" && value.trim().length > 0) {
171
+ candidates.push(value.trim());
172
+ }
173
+ };
174
+ push(input.workspaceRoot);
175
+ push(input.workspace_root);
176
+ const workspaceRoots = input.workspace_roots;
177
+ if (Array.isArray(workspaceRoots)) {
178
+ for (const entry of workspaceRoots)
179
+ push(entry);
180
+ }
181
+ push(input.cwd);
182
+ return candidates;
183
+ }
184
+ /** Collapse join('C:', 'c:\\...') doubled drive prefix on Windows (#2787). */
185
+ function collapseDoubledWindowsDrivePrefix(path) {
186
+ return path.replace(/^([A-Za-z]:\\)(?=[A-Za-z]:\\)/i, "");
187
+ }
188
+ function msysPathToWin32(path) {
189
+ const match = /^\/([a-zA-Z])\/(.*)$/.exec(path.trim());
190
+ if (match === null || match[1] === undefined || match[2] === undefined)
191
+ return null;
192
+ return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
193
+ }
194
+ /** Normalize hook project-root resolution on Windows (doubled drive + MSYS `/c/...`). */
195
+ export function normalizeHookProjectRoot(path) {
196
+ if (process.platform !== "win32")
197
+ return resolve(path);
198
+ const trimmed = path.trim();
199
+ const msys = msysPathToWin32(trimmed);
200
+ let resolved = resolve(msys ?? trimmed);
201
+ const collapsed = collapseDoubledWindowsDrivePrefix(resolved);
202
+ if (collapsed !== resolved) {
203
+ resolved = resolve(collapsed);
204
+ }
205
+ if (/^[a-z]:\\/.test(resolved)) {
206
+ resolved = `${resolved.charAt(0).toUpperCase()}${resolved.slice(1)}`;
207
+ }
208
+ return resolved;
209
+ }
167
210
  export function projectRootFromHookPayload(payload, fallback) {
211
+ const fallbackResolved = normalizeHookProjectRoot(fallback);
168
212
  const input = record(payload);
169
- const fallbackResolved = resolve(fallback);
170
213
  if (input === null)
171
214
  return fallbackResolved;
172
- const workspaceRoots = input.workspace_roots;
173
- const candidate = firstString(input.workspaceRoot, input.workspace_root, Array.isArray(workspaceRoots) ? workspaceRoots[0] : null, input.cwd);
174
- if (candidate === null || isWindowsDriveOnlyRoot(candidate)) {
175
- return fallbackResolved;
215
+ const usable = hookPayloadRootCandidates(input).find((candidate) => !isWindowsDriveOnlyRoot(candidate));
216
+ if (usable !== undefined) {
217
+ return normalizeHookProjectRoot(usable);
176
218
  }
177
- return resolve(candidate);
219
+ return fallbackResolved;
178
220
  }
179
221
  export function isHookHost(value) {
180
222
  return HOOK_HOSTS.includes(value);
@@ -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";