@deftai/directive-core 0.81.0 → 0.83.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 (75) hide show
  1. package/dist/content-contracts/skills/helpers.d.ts +6 -0
  2. package/dist/content-contracts/skills/helpers.js +47 -3
  3. package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
  4. package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
  5. package/dist/eval/crud-telemetry.d.ts +2 -0
  6. package/dist/eval/crud-telemetry.js +30 -0
  7. package/dist/hooks/cursor-hooks.d.ts +23 -0
  8. package/dist/hooks/cursor-hooks.js +95 -0
  9. package/dist/hooks/dispatcher.d.ts +8 -1
  10. package/dist/hooks/dispatcher.js +23 -5
  11. package/dist/hooks/index.d.ts +1 -0
  12. package/dist/hooks/index.js +1 -0
  13. package/dist/init-deposit/agent-hooks.d.ts +4 -2
  14. package/dist/init-deposit/agent-hooks.js +137 -14
  15. package/dist/init-deposit/hygiene.js +1 -0
  16. package/dist/intake/github-body-cli.js +2 -0
  17. package/dist/intake/github-body.d.ts +11 -0
  18. package/dist/intake/github-body.js +95 -24
  19. package/dist/integration-e2e/helpers.js +1 -1
  20. package/dist/issue-sync/sync-from-xbrief.js +3 -0
  21. package/dist/lifecycle/events.d.ts +1 -0
  22. package/dist/lifecycle/events.js +59 -9
  23. package/dist/orphan-active/evaluate.d.ts +25 -0
  24. package/dist/orphan-active/evaluate.js +275 -0
  25. package/dist/orphan-active/index.d.ts +3 -0
  26. package/dist/orphan-active/index.js +3 -0
  27. package/dist/orphan-active/refs.d.ts +16 -0
  28. package/dist/orphan-active/refs.js +105 -0
  29. package/dist/policy/host-hooks.d.ts +21 -0
  30. package/dist/policy/host-hooks.js +96 -0
  31. package/dist/policy/index.d.ts +1 -0
  32. package/dist/policy/index.js +15 -1
  33. package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
  34. package/dist/pr-wait-mergeable/cascade.js +18 -0
  35. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  36. package/dist/pr-wait-mergeable/main.js +44 -73
  37. package/dist/pr-wait-mergeable/result.d.ts +2 -1
  38. package/dist/pr-wait-mergeable/result.js +4 -0
  39. package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
  40. package/dist/pr-wait-mergeable/semantic-green.js +202 -0
  41. package/dist/pr-wait-mergeable/types.d.ts +1 -0
  42. package/dist/pr-watch/constants.d.ts +4 -4
  43. package/dist/pr-watch/constants.js +4 -4
  44. package/dist/pr-watch/watch.js +5 -3
  45. package/dist/product-signal/consent.d.ts +20 -3
  46. package/dist/product-signal/consent.js +63 -6
  47. package/dist/product-signal/gates.d.ts +1 -1
  48. package/dist/product-signal/submit.d.ts +5 -1
  49. package/dist/product-signal/submit.js +42 -18
  50. package/dist/release/native-steps.js +11 -4
  51. package/dist/render/framework-commands.js +6 -0
  52. package/dist/scope/brief-io.d.ts +26 -0
  53. package/dist/scope/brief-io.js +77 -0
  54. package/dist/scope/decompose.js +2 -2
  55. package/dist/scope/decomposed-refs.js +3 -3
  56. package/dist/scope/demote.js +2 -2
  57. package/dist/scope/project-definition-sync.d.ts +2 -2
  58. package/dist/scope/project-definition-sync.js +15 -3
  59. package/dist/scope/registry-artifact-sync.d.ts +6 -1
  60. package/dist/scope/registry-artifact-sync.js +31 -13
  61. package/dist/scope/transition.js +23 -15
  62. package/dist/scope/undo.js +2 -2
  63. package/dist/scope/vbrief-json.d.ts +1 -2
  64. package/dist/scope/vbrief-json.js +1 -4
  65. package/dist/staleness-tickler/state.js +19 -2
  66. package/dist/triage/queue/cache.js +5 -0
  67. package/dist/vbrief-validate/plan-hooks.d.ts +2 -0
  68. package/dist/vbrief-validate/plan-hooks.js +25 -0
  69. package/dist/verify-env/agent-hooks.d.ts +2 -1
  70. package/dist/verify-env/agent-hooks.js +3 -2
  71. package/dist/verify-source/index.d.ts +1 -0
  72. package/dist/verify-source/index.js +1 -0
  73. package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
  74. package/dist/verify-source/skill-external-fetch-gate.js +71 -0
  75. package/package.json +7 -3
@@ -8,6 +8,7 @@ export declare function repoPath(...segments: string[]): string;
8
8
  * first (SOURCE layout), then fall back to the repo root so root-resident
9
9
  * harness entries (AGENTS.md) and the flattened consumer layout still resolve.
10
10
  */
11
+ export declare function resolveContentPathFromRoot(projectRoot: string, relPath: string): string;
11
12
  export declare function resolveRepoPath(relPath: string): string;
12
13
  export declare function readRepoFile(relPath: string): string;
13
14
  export declare function repoFileExists(relPath: string): boolean;
@@ -15,7 +16,12 @@ export declare function readSkill(relPath: string): string;
15
16
  export declare function readAgentsMd(): string;
16
17
  /** Slice the first `## Returning Sessions` section body out of AGENTS.md. */
17
18
  export declare function returningSessionsSection(): string;
19
+ export declare function listSkillMdFilesFromRoot(projectRoot: string): string[];
18
20
  export declare function listSkillMdFiles(): string[];
21
+ export declare function listSkillMdEntriesFromRoot(projectRoot: string): ReadonlyArray<{
22
+ path: string;
23
+ text: string;
24
+ }>;
19
25
  export declare const RFC2119_LEGEND = "!=MUST, ~=SHOULD";
20
26
  export declare const PLATFORM_DETECTION_HEADING = "## Platform Detection";
21
27
  export declare const USER_MD_GATE_HEADING = "## USER.md Gate";
@@ -13,12 +13,15 @@ export function repoPath(...segments) {
13
13
  * first (SOURCE layout), then fall back to the repo root so root-resident
14
14
  * harness entries (AGENTS.md) and the flattened consumer layout still resolve.
15
15
  */
16
- export function resolveRepoPath(relPath) {
17
- const underContent = join(REPO_ROOT, "content", relPath);
16
+ export function resolveContentPathFromRoot(projectRoot, relPath) {
17
+ const underContent = join(projectRoot, "content", relPath);
18
18
  if (existsSync(underContent)) {
19
19
  return underContent;
20
20
  }
21
- return repoPath(relPath);
21
+ return join(projectRoot, relPath);
22
+ }
23
+ export function resolveRepoPath(relPath) {
24
+ return resolveContentPathFromRoot(REPO_ROOT, relPath);
22
25
  }
23
26
  export function readRepoFile(relPath) {
24
27
  return readFileSync(resolveRepoPath(relPath), "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
@@ -43,6 +46,26 @@ export function returningSessionsSection() {
43
46
  const nextHeading = rest.indexOf("\n## ");
44
47
  return nextHeading === -1 ? rest : rest.slice(0, nextHeading);
45
48
  }
49
+ function skillsDirFromRoot(projectRoot) {
50
+ const resolved = resolveContentPathFromRoot(projectRoot, "skills");
51
+ return existsSync(resolved) ? resolved : null;
52
+ }
53
+ export function listSkillMdFilesFromRoot(projectRoot) {
54
+ const skillsDir = skillsDirFromRoot(projectRoot);
55
+ if (skillsDir === null) {
56
+ return [];
57
+ }
58
+ const results = [];
59
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
60
+ if (entry.isDirectory()) {
61
+ const skillMd = join(skillsDir, entry.name, "SKILL.md");
62
+ if (existsSync(skillMd)) {
63
+ results.push(join("skills", entry.name, "SKILL.md"));
64
+ }
65
+ }
66
+ }
67
+ return results.sort();
68
+ }
46
69
  export function listSkillMdFiles() {
47
70
  const skillsDir = resolveRepoPath("skills");
48
71
  const results = [];
@@ -56,6 +79,27 @@ export function listSkillMdFiles() {
56
79
  }
57
80
  return results.sort();
58
81
  }
82
+ export function listSkillMdEntriesFromRoot(projectRoot) {
83
+ const skillsDir = skillsDirFromRoot(projectRoot);
84
+ if (skillsDir === null) {
85
+ return [];
86
+ }
87
+ const entries = [];
88
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
89
+ if (!entry.isDirectory()) {
90
+ continue;
91
+ }
92
+ const skillMd = join(skillsDir, entry.name, "SKILL.md");
93
+ if (!existsSync(skillMd)) {
94
+ continue;
95
+ }
96
+ entries.push({
97
+ path: join("skills", entry.name, "SKILL.md"),
98
+ text: readFileSync(skillMd, "utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n"),
99
+ });
100
+ }
101
+ return entries.sort((a, b) => a.path.localeCompare(b.path));
102
+ }
59
103
  export const RFC2119_LEGEND = "!=MUST, ~=SHOULD";
60
104
  export const PLATFORM_DETECTION_HEADING = "## Platform Detection";
61
105
  export const USER_MD_GATE_HEADING = "## USER.md Gate";
@@ -0,0 +1,18 @@
1
+ /** Skill-validation gate: external fetch must not pair with execute/install without mitigation (#1532 / #1936). */
2
+ export type ExternalFetchViolation = {
3
+ skillPath: string;
4
+ detail: string;
5
+ };
6
+ /** Strip YAML frontmatter and HTML comment banners before scanning skill prose. */
7
+ export declare function skillProse(raw: string): string;
8
+ export declare function hasExternalFetchSignal(prose: string): boolean;
9
+ export declare function hasRiskyExternalFetchPattern(prose: string): boolean;
10
+ export declare function hasExecuteFromExternalSignal(prose: string): boolean;
11
+ export declare function hasUntrustedFetchMitigation(prose: string): boolean;
12
+ export declare function analyzeSkillExternalFetch(skillPath: string, rawText: string): ExternalFetchViolation | null;
13
+ /** Collect violations across skill entries (used by verify-source gate + tests). */
14
+ export declare function collectExternalFetchViolations(entries: ReadonlyArray<{
15
+ path: string;
16
+ text: string;
17
+ }>): ExternalFetchViolation[];
18
+ //# sourceMappingURL=skill-external-fetch-gate.d.ts.map
@@ -0,0 +1,81 @@
1
+ /** Skill-validation gate: external fetch must not pair with execute/install without mitigation (#1532 / #1936). */
2
+ const EXTERNAL_FETCH_RE = /\b(fetch(?:ing)?(?:\s+\w+){0,6}\s+(?:the\s+)?(?:url|content|article|related\s+urls?|http|https|web\s+page)|fetch\s+and\s+read|webfetch|mcp.*fetch)/i;
3
+ const EXECUTE_FROM_EXTERNAL_RE = /\b(download|install|run|execute)\b.{0,40}\b(?:found|inside|within|in)\b.{0,40}\b(?:fetched|external|url|content|page|article)\b|\bfollow\b.{0,40}\b(?:instructions?|commands?)\b.{0,40}\b(?:download|install|run|execute)\b|\bexecute\b.{0,40}\b(?:commands?|scripts?|instructions?)\b.{0,40}\b(?:found|inside|within)\b/i;
4
+ const FETCH_THEN_EXECUTE_RE = /\bfetch\b.{0,120}\b(?:then|and)\b.{0,60}\b(?:run|execute|install|download)\b/i;
5
+ const RUN_FETCHED_ARTIFACT_RE = /\b(?:run|execute|install)\b.{0,40}\b(?:downloaded|fetched)\b/i;
6
+ const FETCHED_ARTIFACT_RE = /\b(?:downloaded|fetched)\b.{0,40}\b(?:script|binary|executable|tool|installer|package|file)\b/i;
7
+ const FOLLOW_RELATED_URLS_RE = /\bfetch(?:ing)?\s+related\s+urls?\b/i;
8
+ const SECURITY_CONTEXT_HEADING = /## Security context/i;
9
+ const UNTRUSTED_DOCTRINE_RE = /untrusted\s+(?:external\s+)?(?:content|data)/i;
10
+ const FORBID_EXTERNAL_EXECUTE_RE = /⊗[\s\S]{0,200}\b(?:download|install|execute|run)\b[\s\S]{0,200}\b(?:external|fetched|externally|found inside)\b/i;
11
+ /** Strip HTML comment blocks; repeat until stable for CodeQL multi-char sanitization. */
12
+ function stripHtmlComments(text) {
13
+ let body = text;
14
+ let prev = "";
15
+ while (prev !== body) {
16
+ prev = body;
17
+ body = body.replace(/<!--[\s\S]*?-->/g, "");
18
+ }
19
+ return body;
20
+ }
21
+ /** Strip YAML frontmatter and HTML comment banners before scanning skill prose. */
22
+ export function skillProse(raw) {
23
+ let body = raw.replace(/\r\n/g, "\n");
24
+ if (body.startsWith("---")) {
25
+ const end = body.indexOf("\n---", 3);
26
+ if (end !== -1) {
27
+ body = body.slice(end + 4);
28
+ }
29
+ }
30
+ return stripHtmlComments(body);
31
+ }
32
+ export function hasExternalFetchSignal(prose) {
33
+ return EXTERNAL_FETCH_RE.test(prose);
34
+ }
35
+ export function hasRiskyExternalFetchPattern(prose) {
36
+ if (!hasExternalFetchSignal(prose)) {
37
+ return false;
38
+ }
39
+ return (hasExecuteFromExternalSignal(prose) ||
40
+ FOLLOW_RELATED_URLS_RE.test(prose) ||
41
+ /\bfetch\b.{0,40}\bfollow\b/i.test(prose));
42
+ }
43
+ export function hasExecuteFromExternalSignal(prose) {
44
+ return (EXECUTE_FROM_EXTERNAL_RE.test(prose) ||
45
+ FETCH_THEN_EXECUTE_RE.test(prose) ||
46
+ RUN_FETCHED_ARTIFACT_RE.test(prose) ||
47
+ FETCHED_ARTIFACT_RE.test(prose));
48
+ }
49
+ export function hasUntrustedFetchMitigation(prose) {
50
+ return (SECURITY_CONTEXT_HEADING.test(prose) &&
51
+ UNTRUSTED_DOCTRINE_RE.test(prose) &&
52
+ FORBID_EXTERNAL_EXECUTE_RE.test(prose));
53
+ }
54
+ export function analyzeSkillExternalFetch(skillPath, rawText) {
55
+ const prose = skillProse(rawText);
56
+ if (!hasExternalFetchSignal(prose)) {
57
+ return null;
58
+ }
59
+ if (!hasRiskyExternalFetchPattern(prose)) {
60
+ return null;
61
+ }
62
+ if (hasUntrustedFetchMitigation(prose)) {
63
+ return null;
64
+ }
65
+ return {
66
+ skillPath,
67
+ detail: "external fetch or follow-through language without Security context untrusted-data / forbid-execute mitigation (#1936)",
68
+ };
69
+ }
70
+ /** Collect violations across skill entries (used by verify-source gate + tests). */
71
+ export function collectExternalFetchViolations(entries) {
72
+ const violations = [];
73
+ for (const { path, text } of entries) {
74
+ const finding = analyzeSkillExternalFetch(path, text);
75
+ if (finding !== null) {
76
+ violations.push(finding);
77
+ }
78
+ }
79
+ return violations;
80
+ }
81
+ //# sourceMappingURL=skill-external-fetch-gate.js.map
@@ -54,6 +54,8 @@ export declare class InstrumentedVbriefCrud {
54
54
  update(path: string, content: string, options?: {
55
55
  trustedWrite?: boolean;
56
56
  }): CrudResult;
57
+ /** Record update metrics without persisting (caller owns validated atomic write). */
58
+ recordTrustedUpdate(path: string, content: string): void;
57
59
  delete(path: string): CrudResult;
58
60
  private recordMetric;
59
61
  }
@@ -248,6 +248,36 @@ export class InstrumentedVbriefCrud {
248
248
  writeFileSync(path, content, "utf8");
249
249
  return { ok: true };
250
250
  }
251
+ /** Record update metrics without persisting (caller owns validated atomic write). */
252
+ recordTrustedUpdate(path, content) {
253
+ let parsed;
254
+ try {
255
+ parsed = JSON.parse(content);
256
+ }
257
+ catch (err) {
258
+ const message = sanitizeInlineMessage(err instanceof Error ? err.message : String(err));
259
+ this.recordMetric({
260
+ operation: "update",
261
+ path,
262
+ schemaValid: false,
263
+ schemaErrors: [`${path}: invalid JSON -- ${message}`],
264
+ inventedKeys: [],
265
+ byteDiffMinimality: null,
266
+ byteDiffChangedRatio: null,
267
+ });
268
+ return;
269
+ }
270
+ const assessment = assessDocument(path, parsed);
271
+ this.recordMetric({
272
+ operation: "update",
273
+ path,
274
+ schemaValid: assessment.schemaValid,
275
+ schemaErrors: assessment.schemaErrors,
276
+ inventedKeys: assessment.inventedKeys,
277
+ byteDiffMinimality: null,
278
+ byteDiffChangedRatio: null,
279
+ });
280
+ }
251
281
  delete(path) {
252
282
  if (!existsSync(path)) {
253
283
  this.recordMetric({
@@ -0,0 +1,23 @@
1
+ /** Cursor ApplyPatch spellings handled by the project adapter, not generic write dispatch. */
2
+ export declare const APPLY_PATCH_TOOL_NAMES: readonly ["ApplyPatch", "apply_patch"];
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;
23
+ //# sourceMappingURL=cursor-hooks.d.ts.map
@@ -0,0 +1,95 @@
1
+ import { DIRECT_WRITE_TOOL_NAMES } from "./tools.js";
2
+ /** Cursor ApplyPatch spellings handled by the project adapter, not generic write dispatch. */
3
+ export const APPLY_PATCH_TOOL_NAMES = ["ApplyPatch", "apply_patch"];
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
+ }
95
+ //# sourceMappingURL=cursor-hooks.js.map
@@ -72,6 +72,13 @@ export declare function isHookHost(value: string): value is HookHost;
72
72
  export declare function isHookEvent(value: string): value is HookEvent;
73
73
  /** Decide a normalized event using only the P0 direct-write policy. */
74
74
  export declare function decideHook(input: HookDispatchInput, seams?: HookPolicySeams): HookDecision;
75
- /** Render only authoritative denials; allow preserves the host's own permission flow. */
75
+ /**
76
+ * Render host-facing hook output.
77
+ *
78
+ * Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
79
+ * hook failure and blocks the tool — so Cursor allows must emit explicit
80
+ * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
81
+ * flow is unchanged.
82
+ */
76
83
  export declare function renderHostDecision(host: HookHost, decision: HookDecision): string;
77
84
  //# sourceMappingURL=dispatcher.d.ts.map
@@ -161,13 +161,20 @@ export function isProposedLifecycleWrite(projectRoot, targetPath) {
161
161
  return false;
162
162
  return posix.startsWith("xbrief/proposed/") || posix.startsWith("vbrief/proposed/");
163
163
  }
164
+ function isWindowsDriveOnlyRoot(value) {
165
+ return /^[A-Za-z]:[/\\]?$/.test(value.trim());
166
+ }
164
167
  export function projectRootFromHookPayload(payload, fallback) {
165
168
  const input = record(payload);
169
+ const fallbackResolved = resolve(fallback);
166
170
  if (input === null)
167
- return resolve(fallback);
171
+ return fallbackResolved;
168
172
  const workspaceRoots = input.workspace_roots;
169
- const root = firstString(input.workspaceRoot, input.workspace_root, Array.isArray(workspaceRoots) ? workspaceRoots[0] : null, input.cwd, fallback);
170
- return resolve(root ?? fallback);
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;
176
+ }
177
+ return resolve(candidate);
171
178
  }
172
179
  export function isHookHost(value) {
173
180
  return HOOK_HOSTS.includes(value);
@@ -411,10 +418,21 @@ export function decideHook(input, seams = {}) {
411
418
  }
412
419
  return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: true });
413
420
  }
414
- /** Render only authoritative denials; allow preserves the host's own permission flow. */
421
+ /**
422
+ * Render host-facing hook output.
423
+ *
424
+ * Cursor deposits use `failClosed: true`. Cursor treats empty/null stdout as a
425
+ * hook failure and blocks the tool — so Cursor allows must emit explicit
426
+ * `{"permission":"allow"}`. Other hosts keep empty allow so the host permission
427
+ * flow is unchanged.
428
+ */
415
429
  export function renderHostDecision(host, decision) {
416
- if (decision.verdict === "allow")
430
+ if (decision.verdict === "allow") {
431
+ if (host === "cursor") {
432
+ return JSON.stringify({ permission: "allow" });
433
+ }
417
434
  return "";
435
+ }
418
436
  switch (host) {
419
437
  case "claude":
420
438
  case "codex":
@@ -1,3 +1,4 @@
1
+ export * from "./cursor-hooks.js";
1
2
  export * from "./dispatcher.js";
2
3
  export * from "./scope.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
+ export * from "./cursor-hooks.js";
1
2
  export * from "./dispatcher.js";
2
3
  export * from "./scope.js";
3
4
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,7 @@
1
1
  import type { HookHost } from "../hooks/dispatcher.js";
2
+ import { type HostHooksPolicy } from "../policy/host-hooks.js";
2
3
  import type { InitDepositIo } from "./constants.js";
4
+ export { CURSOR_GENERIC_WRITE_HOOK_MATCHER, DEFT_CURSOR_ADAPTER_COMMAND_MARKER, } from "../hooks/cursor-hooks.js";
3
5
  export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
4
6
  export declare const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
5
7
  export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
@@ -19,7 +21,7 @@ export interface AgentHookDepositResult {
19
21
  readonly changedPaths: AgentHookPath[];
20
22
  }
21
23
  /** Merge Directive-owned project hook entries without replacing user configuration. */
22
- export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo): AgentHookDepositResult;
24
+ export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo, hostHooksPolicy?: HostHooksPolicy): AgentHookDepositResult;
23
25
  /** Read-only registration probe shared by verify and doctor. */
24
- export declare function inspectAgentHookDeposit(projectRoot: string): AgentHookInspection[];
26
+ export declare function inspectAgentHookDeposit(projectRoot: string, hostHooksPolicy?: HostHooksPolicy): AgentHookInspection[];
25
27
  //# sourceMappingURL=agent-hooks.d.ts.map