@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.
- package/dist/content-contracts/skills/helpers.d.ts +6 -0
- package/dist/content-contracts/skills/helpers.js +47 -3
- package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
- package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
- package/dist/eval/crud-telemetry.d.ts +2 -0
- package/dist/eval/crud-telemetry.js +30 -0
- package/dist/hooks/cursor-hooks.d.ts +23 -0
- package/dist/hooks/cursor-hooks.js +95 -0
- package/dist/hooks/dispatcher.d.ts +8 -1
- package/dist/hooks/dispatcher.js +23 -5
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -2
- package/dist/init-deposit/agent-hooks.js +137 -14
- package/dist/init-deposit/hygiene.js +1 -0
- package/dist/intake/github-body-cli.js +2 -0
- package/dist/intake/github-body.d.ts +11 -0
- package/dist/intake/github-body.js +95 -24
- package/dist/integration-e2e/helpers.js +1 -1
- package/dist/issue-sync/sync-from-xbrief.js +3 -0
- package/dist/lifecycle/events.d.ts +1 -0
- package/dist/lifecycle/events.js +59 -9
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +275 -0
- package/dist/orphan-active/index.d.ts +3 -0
- package/dist/orphan-active/index.js +3 -0
- package/dist/orphan-active/refs.d.ts +16 -0
- package/dist/orphan-active/refs.js +105 -0
- package/dist/policy/host-hooks.d.ts +21 -0
- package/dist/policy/host-hooks.js +96 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +18 -0
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +44 -73
- package/dist/pr-wait-mergeable/result.d.ts +2 -1
- package/dist/pr-wait-mergeable/result.js +4 -0
- package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
- package/dist/pr-wait-mergeable/semantic-green.js +202 -0
- package/dist/pr-wait-mergeable/types.d.ts +1 -0
- package/dist/pr-watch/constants.d.ts +4 -4
- package/dist/pr-watch/constants.js +4 -4
- package/dist/pr-watch/watch.js +5 -3
- package/dist/product-signal/consent.d.ts +20 -3
- package/dist/product-signal/consent.js +63 -6
- package/dist/product-signal/gates.d.ts +1 -1
- package/dist/product-signal/submit.d.ts +5 -1
- package/dist/product-signal/submit.js +42 -18
- package/dist/release/native-steps.js +11 -4
- package/dist/render/framework-commands.js +6 -0
- package/dist/scope/brief-io.d.ts +26 -0
- package/dist/scope/brief-io.js +77 -0
- package/dist/scope/decompose.js +2 -2
- package/dist/scope/decomposed-refs.js +3 -3
- package/dist/scope/demote.js +2 -2
- package/dist/scope/project-definition-sync.d.ts +2 -2
- package/dist/scope/project-definition-sync.js +15 -3
- package/dist/scope/registry-artifact-sync.d.ts +6 -1
- package/dist/scope/registry-artifact-sync.js +31 -13
- package/dist/scope/transition.js +23 -15
- package/dist/scope/undo.js +2 -2
- package/dist/scope/vbrief-json.d.ts +1 -2
- package/dist/scope/vbrief-json.js +1 -4
- package/dist/staleness-tickler/state.js +19 -2
- package/dist/triage/queue/cache.js +5 -0
- package/dist/vbrief-validate/plan-hooks.d.ts +2 -0
- package/dist/vbrief-validate/plan-hooks.js +25 -0
- package/dist/verify-env/agent-hooks.d.ts +2 -1
- package/dist/verify-env/agent-hooks.js +3 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
- package/dist/verify-source/skill-external-fetch-gate.js +71 -0
- 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
|
|
17
|
-
const underContent = join(
|
|
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
|
|
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
|
-
/**
|
|
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
|
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -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
|
|
171
|
+
return fallbackResolved;
|
|
168
172
|
const workspaceRoots = input.workspace_roots;
|
|
169
|
-
const
|
|
170
|
-
|
|
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
|
-
/**
|
|
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":
|
package/dist/hooks/index.d.ts
CHANGED
package/dist/hooks/index.js
CHANGED
|
@@ -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
|