@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.
- package/dist/cache/fetch.d.ts +17 -0
- package/dist/cache/fetch.js +50 -0
- package/dist/doctor/checks.js +16 -1
- package/dist/doctor/constants.d.ts +4 -2
- package/dist/doctor/constants.js +10 -6
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.d.ts +2 -1
- package/dist/doctor/main.js +83 -22
- package/dist/doctor/npm-registry.d.ts +23 -0
- package/dist/doctor/npm-registry.js +128 -0
- package/dist/doctor/npm-view.d.ts +10 -0
- package/dist/doctor/npm-view.js +31 -0
- package/dist/doctor/payload-staleness.js +2 -9
- package/dist/doctor/signpost-checks.d.ts +2 -1
- package/dist/doctor/signpost-checks.js +2 -0
- package/dist/doctor/types.d.ts +12 -0
- package/dist/eval-health-relocation/evaluate.js +15 -1
- package/dist/fs/projection-containment.js +1 -1
- package/dist/hooks/cursor-hooks.d.ts +6 -20
- package/dist/hooks/cursor-hooks.js +7 -92
- package/dist/init-deposit/agent-hooks.d.ts +2 -2
- package/dist/init-deposit/agent-hooks.js +24 -38
- package/dist/init-deposit/gitignore.js +5 -0
- package/dist/init-deposit/hygiene.d.ts +22 -0
- package/dist/init-deposit/hygiene.js +136 -4
- package/dist/init-deposit/legacy-detect.d.ts +25 -1
- package/dist/init-deposit/legacy-detect.js +143 -11
- package/dist/init-deposit/refresh.d.ts +2 -0
- package/dist/init-deposit/refresh.js +18 -5
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/org-force-on-migration.d.ts +44 -0
- package/dist/policy/org-force-on-migration.js +242 -0
- package/dist/policy/product-signal.d.ts +3 -1
- package/dist/policy/product-signal.js +15 -4
- package/dist/policy/value-feedback.d.ts +18 -1
- package/dist/policy/value-feedback.js +78 -5
- package/dist/preflight-cache/evaluate.js +12 -1
- package/dist/product-signal/consent-prompt.d.ts +13 -0
- package/dist/product-signal/consent-prompt.js +30 -0
- package/dist/product-signal/submit.js +22 -1
- package/dist/render/framework-commands.d.ts +1 -1
- package/dist/render/framework-commands.js +6 -6
- package/dist/render/index.d.ts +1 -0
- package/dist/render/index.js +1 -0
- package/dist/render/roadmap-render.d.ts +5 -1
- package/dist/render/roadmap-render.js +20 -2
- package/dist/render/rule-map-template.d.ts +2 -0
- package/dist/render/rule-map-template.js +407 -0
- package/dist/render/rule-map.d.ts +2 -0
- package/dist/render/rule-map.js +708 -0
- package/dist/review-monitor/constants.d.ts +7 -1
- package/dist/review-monitor/constants.js +40 -14
- package/dist/review-monitor/github-lease.d.ts +45 -0
- package/dist/review-monitor/github-lease.js +106 -0
- package/dist/review-monitor/index.d.ts +2 -0
- package/dist/review-monitor/index.js +2 -0
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +152 -0
- package/dist/review-monitor/record.d.ts +61 -13
- package/dist/review-monitor/record.js +379 -132
- package/dist/review-monitor/verify.d.ts +2 -0
- package/dist/review-monitor/verify.js +32 -13
- package/dist/scm/gh-rest.d.ts +7 -1
- package/dist/scm/gh-rest.js +35 -0
- package/dist/session/session-start.js +5 -0
- package/dist/task-surface/index.js +53 -13
- package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
- package/dist/verify-env/agent-hooks-live-probe.js +216 -0
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/dist/xbrief-migrate/constants.d.ts +6 -0
- package/dist/xbrief-migrate/constants.js +6 -0
- package/dist/xbrief-migrate/index.d.ts +2 -2
- package/dist/xbrief-migrate/index.js +2 -2
- package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
- package/dist/xbrief-migrate/migrate-project.js +44 -18
- package/package.json +3 -3
package/dist/doctor/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export declare const
|
|
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 {
|
|
2
|
-
/** Cursor ApplyPatch
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const
|
|
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
|
|
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
|
|
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((
|
|
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)
|
|
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(
|
|
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:
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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 ===
|
|
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
|