@deftai/directive-core 0.84.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/cache/fetch.d.ts +17 -0
  2. package/dist/cache/fetch.js +50 -0
  3. package/dist/doctor/checks.js +16 -1
  4. package/dist/doctor/constants.d.ts +4 -2
  5. package/dist/doctor/constants.js +10 -6
  6. package/dist/doctor/index.d.ts +1 -0
  7. package/dist/doctor/index.js +1 -0
  8. package/dist/doctor/main.d.ts +2 -1
  9. package/dist/doctor/main.js +83 -22
  10. package/dist/doctor/npm-registry.d.ts +23 -0
  11. package/dist/doctor/npm-registry.js +128 -0
  12. package/dist/doctor/npm-view.d.ts +10 -0
  13. package/dist/doctor/npm-view.js +31 -0
  14. package/dist/doctor/payload-staleness.js +2 -9
  15. package/dist/doctor/signpost-checks.d.ts +2 -1
  16. package/dist/doctor/signpost-checks.js +2 -0
  17. package/dist/doctor/types.d.ts +12 -0
  18. package/dist/eval-health-relocation/evaluate.js +15 -1
  19. package/dist/fs/projection-containment.js +1 -1
  20. package/dist/hooks/cursor-hooks.d.ts +6 -20
  21. package/dist/hooks/cursor-hooks.js +7 -92
  22. package/dist/init-deposit/agent-hooks.d.ts +2 -2
  23. package/dist/init-deposit/agent-hooks.js +24 -38
  24. package/dist/init-deposit/gitignore.js +5 -0
  25. package/dist/init-deposit/hygiene.d.ts +22 -0
  26. package/dist/init-deposit/hygiene.js +136 -4
  27. package/dist/init-deposit/legacy-detect.d.ts +25 -1
  28. package/dist/init-deposit/legacy-detect.js +143 -11
  29. package/dist/init-deposit/refresh.d.ts +2 -0
  30. package/dist/init-deposit/refresh.js +18 -5
  31. package/dist/policy/index.d.ts +2 -0
  32. package/dist/policy/index.js +2 -0
  33. package/dist/policy/org-force-on-migration.d.ts +44 -0
  34. package/dist/policy/org-force-on-migration.js +242 -0
  35. package/dist/policy/product-signal.d.ts +3 -1
  36. package/dist/policy/product-signal.js +15 -4
  37. package/dist/policy/value-feedback.d.ts +18 -1
  38. package/dist/policy/value-feedback.js +78 -5
  39. package/dist/preflight-cache/evaluate.js +12 -1
  40. package/dist/product-signal/consent-prompt.d.ts +13 -0
  41. package/dist/product-signal/consent-prompt.js +30 -0
  42. package/dist/product-signal/submit.js +22 -1
  43. package/dist/render/framework-commands.d.ts +1 -1
  44. package/dist/render/framework-commands.js +6 -6
  45. package/dist/render/index.d.ts +1 -0
  46. package/dist/render/index.js +1 -0
  47. package/dist/render/roadmap-render.d.ts +5 -1
  48. package/dist/render/roadmap-render.js +20 -2
  49. package/dist/render/rule-map-template.d.ts +2 -0
  50. package/dist/render/rule-map-template.js +407 -0
  51. package/dist/render/rule-map.d.ts +2 -0
  52. package/dist/render/rule-map.js +708 -0
  53. package/dist/review-monitor/constants.d.ts +7 -1
  54. package/dist/review-monitor/constants.js +40 -14
  55. package/dist/review-monitor/github-lease.d.ts +45 -0
  56. package/dist/review-monitor/github-lease.js +106 -0
  57. package/dist/review-monitor/index.d.ts +2 -0
  58. package/dist/review-monitor/index.js +2 -0
  59. package/dist/review-monitor/lease-comment.d.ts +36 -0
  60. package/dist/review-monitor/lease-comment.js +152 -0
  61. package/dist/review-monitor/record.d.ts +61 -13
  62. package/dist/review-monitor/record.js +379 -132
  63. package/dist/review-monitor/verify.d.ts +2 -0
  64. package/dist/review-monitor/verify.js +32 -13
  65. package/dist/scm/gh-rest.d.ts +7 -1
  66. package/dist/scm/gh-rest.js +35 -0
  67. package/dist/session/session-start.js +5 -0
  68. package/dist/task-surface/index.js +53 -13
  69. package/dist/verify-env/agent-hooks-live-probe.d.ts +32 -0
  70. package/dist/verify-env/agent-hooks-live-probe.js +216 -0
  71. package/dist/verify-env/index.d.ts +1 -0
  72. package/dist/verify-env/index.js +1 -0
  73. package/dist/xbrief-migrate/constants.d.ts +6 -0
  74. package/dist/xbrief-migrate/constants.js +6 -0
  75. package/dist/xbrief-migrate/index.d.ts +2 -2
  76. package/dist/xbrief-migrate/index.js +2 -2
  77. package/dist/xbrief-migrate/migrate-project.d.ts +7 -0
  78. package/dist/xbrief-migrate/migrate-project.js +44 -18
  79. package/package.json +3 -3
@@ -1,8 +1,9 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { sweepScratchDirs } from "../orchestration/subagent-monitor.js";
4
+ import { resolveRepo } from "../triage/queue/repo.js";
4
5
  import { EXIT_CONFIG_ERROR, EXIT_NOT_READY, EXIT_READY, MONITORING_TIER_1, MONITORING_TIER_3, } from "./constants.js";
5
- import { defaultSubagentStatusDir, findActiveMonitorForPr, readReviewMonitorFile, reviewMonitorPath, } from "./record.js";
6
+ import { defaultSubagentStatusDir, fetchActiveMonitorFromGithub, readReviewMonitorFile, reviewMonitorPath, } from "./record.js";
6
7
  import { isTier1, probeMonitoringTier } from "./tier-detection.js";
7
8
  function spawnRedirect(probe) {
8
9
  const primitive = probe.primitive ?? "sub-agent";
@@ -59,6 +60,8 @@ export function evaluateReviewMonitorGate(args) {
59
60
  const callSite = args.callSite ?? "unspecified";
60
61
  const staleMinutes = args.staleMinutes ?? 30;
61
62
  const now = args.now ?? new Date();
63
+ // Legacy `.deft/review-monitor.json` is obsolete (#2814); explicit no-op read documents migration.
64
+ readReviewMonitorFile(reviewMonitorPath(projectRoot));
62
65
  if (args.approach3 === true) {
63
66
  if (isTier1(tier)) {
64
67
  return {
@@ -103,33 +106,43 @@ export function evaluateReviewMonitorGate(args) {
103
106
  callSite,
104
107
  };
105
108
  }
106
- const path = reviewMonitorPath(projectRoot);
107
- const { data, error } = readReviewMonitorFile(path);
108
- if (data === null) {
109
+ const repo = resolveRepo(args.repo ?? null, projectRoot);
110
+ if (repo === null) {
109
111
  return {
110
112
  exitCode: EXIT_CONFIG_ERROR,
111
- message: `verify_review_monitor: ${error ?? "could not read review-monitor state"}`,
113
+ message: "verify_review_monitor: could not resolve owner/repo — pass --repo OWNER/REPO or run inside a git repo with origin",
112
114
  tier,
113
115
  monitorRecord: null,
114
116
  heartbeatActive: false,
115
117
  callSite,
116
118
  };
117
119
  }
118
- const monitorRecord = findActiveMonitorForPr(data, args.pr, {
120
+ const githubMonitor = fetchActiveMonitorFromGithub(repo, args.pr, {
119
121
  now,
120
- staleMinutes,
121
122
  headSha: args.headSha ?? null,
123
+ seams: args.seams,
122
124
  });
125
+ if (githubMonitor !== null && typeof githubMonitor === "object" && "error" in githubMonitor) {
126
+ return {
127
+ exitCode: EXIT_CONFIG_ERROR,
128
+ message: `verify_review_monitor: ${githubMonitor.error}`,
129
+ tier,
130
+ monitorRecord: null,
131
+ heartbeatActive: false,
132
+ callSite,
133
+ };
134
+ }
135
+ const monitorRecord = githubMonitor;
123
136
  const heartbeatActive = hasActivePollingHeartbeat(projectRoot, args.pr, {
124
137
  now,
125
138
  staleMinutes,
126
139
  });
127
- if (monitorRecord !== null || heartbeatActive) {
128
- const via = monitorRecord !== null ? "review-monitor record" : "subagent heartbeat";
140
+ if (monitorRecord !== null) {
129
141
  return {
130
142
  exitCode: EXIT_READY,
131
- message: `verify_review_monitor: active review-monitor for PR #${args.pr} via ${via} ` +
132
- `(call-site=${callSite}, tier=1, descriptor=${tier.descriptor ?? "unknown"}).`,
143
+ message: `verify_review_monitor: active GitHub review-owner lease for PR #${args.pr} ` +
144
+ `(monitor_agent_id=${monitorRecord.monitor_agent_id}, owner=${monitorRecord.owner}, ` +
145
+ `call-site=${callSite}, tier=1, descriptor=${tier.descriptor ?? "unknown"}).`,
133
146
  tier,
134
147
  monitorRecord,
135
148
  heartbeatActive,
@@ -141,15 +154,20 @@ export function evaluateReviewMonitorGate(args) {
141
154
  : callSite === "swarm-phase6-cascade"
142
155
  ? "Swarm Phase 6 post force-push (#380)"
143
156
  : "Solo drive-to merge-ready / review-cycle ownership";
157
+ const heartbeatHint = heartbeatActive
158
+ ? " Note: local subagent heartbeat is present but is not a GitHub review-owner lease (#2814).\n"
159
+ : "";
144
160
  return {
145
161
  exitCode: EXIT_NOT_READY,
146
- message: `verify_review_monitor: Tier 1 available but no active review-monitor for PR #${args.pr} (#2655).\n` +
162
+ message: `verify_review_monitor: Tier 1 available but no active GitHub review-owner lease for PR #${args.pr} (#2814).\n` +
163
+ heartbeatHint +
147
164
  ` Call site: ${siteHint}.\n` +
148
165
  ` Detected descriptor=${tier.descriptor ?? "unknown"} primitive=${tier.primitive ?? "none"}.\n` +
166
+ ` Legacy .deft/review-monitor.json is ignored.\n` +
149
167
  ` ${spawnRedirect(tier)}`,
150
168
  tier,
151
169
  monitorRecord: null,
152
- heartbeatActive: false,
170
+ heartbeatActive,
153
171
  callSite,
154
172
  };
155
173
  }
@@ -160,6 +178,7 @@ export function verifyResultToJson(result) {
160
178
  heartbeat_active: result.heartbeatActive,
161
179
  message: result.message,
162
180
  monitor_agent_id: result.monitorRecord?.monitor_agent_id ?? null,
181
+ monitor_owner: result.monitorRecord?.owner ?? null,
163
182
  monitor_record: result.monitorRecord,
164
183
  ready: result.exitCode === EXIT_READY,
165
184
  tier: result.tier.tier,
@@ -53,9 +53,15 @@ export interface RestIssueListOptions {
53
53
  export declare function restIssueList(repo: string, options?: RestIssueListOptions, seams?: GhRestSeams): Record<string, unknown>[];
54
54
  export declare const REST_MAX_PER_PAGE = 100;
55
55
  export declare const REST_PAGINATION_MAX_PAGES = 100;
56
- export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restUpdateIssue", "restCreateLabel", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
56
+ export declare const PUBLIC_HELPERS: readonly ["restCreateIssue", "restPostComment", "restUpdateComment", "restDeleteComment", "restGetUser", "restUpdateIssue", "restCreateLabel", "restCloseIssue", "restOpenPr", "restMergePr", "restIssueView", "restPrView", "restIssueList", "restIssueListPaginated"];
57
57
  export declare function restCreateIssue(repo: string, title: string, body: string, labels?: readonly string[], seams?: GhRestSeams): Record<string, unknown>;
58
58
  export declare function restPostComment(repo: string, n: number, body: string, seams?: GhRestSeams): Record<string, unknown>;
59
+ /** `PATCH /repos/{owner}/{repo}/issues/comments/{id}` -- edit a PR/issue comment in place. */
60
+ export declare function restUpdateComment(repo: string, commentId: number, body: string, seams?: GhRestSeams): Record<string, unknown>;
61
+ /** `DELETE /repos/{owner}/{repo}/issues/comments/{id}` -- remove a duplicate claim comment. */
62
+ export declare function restDeleteComment(repo: string, commentId: number, seams?: GhRestSeams): Record<string, unknown>;
63
+ /** `GET /user` -- authenticated GitHub login for review-owner claims. */
64
+ export declare function restGetUser(seams?: GhRestSeams): Record<string, unknown>;
59
65
  export declare function restUpdateIssue(repo: string, n: number, patch: Record<string, unknown>, seams?: GhRestSeams): Record<string, unknown>;
60
66
  export declare function restCreateLabel(repo: string, name: string, color: string, description: string, seams?: GhRestSeams): Record<string, unknown>;
61
67
  export declare function restCloseIssue(repo: string, n: number, reason?: string | null, seams?: GhRestSeams): Record<string, unknown>;
@@ -171,6 +171,9 @@ export const REST_PAGINATION_MAX_PAGES = 100;
171
171
  export const PUBLIC_HELPERS = [
172
172
  "restCreateIssue",
173
173
  "restPostComment",
174
+ "restUpdateComment",
175
+ "restDeleteComment",
176
+ "restGetUser",
174
177
  "restUpdateIssue",
175
178
  "restCreateLabel",
176
179
  "restCloseIssue",
@@ -226,6 +229,38 @@ export function restPostComment(repo, n, body, seams = {}) {
226
229
  ...seams,
227
230
  });
228
231
  }
232
+ /** `PATCH /repos/{owner}/{repo}/issues/comments/{id}` -- edit a PR/issue comment in place. */
233
+ export function restUpdateComment(repo, commentId, body, seams = {}) {
234
+ const [owner, name] = splitRepo(repo);
235
+ const endpoint = `repos/${owner}/${name}/issues/comments/${commentId}`;
236
+ return execMutation([endpoint, "--method", "PATCH"], {
237
+ endpoint,
238
+ payload: { body },
239
+ hint: "verify repo permissions and comment id; check gh auth status",
240
+ ...seams,
241
+ });
242
+ }
243
+ /** `DELETE /repos/{owner}/{repo}/issues/comments/{id}` -- remove a duplicate claim comment. */
244
+ export function restDeleteComment(repo, commentId, seams = {}) {
245
+ const [owner, name] = splitRepo(repo);
246
+ const endpoint = `repos/${owner}/${name}/issues/comments/${commentId}`;
247
+ return execMutation([endpoint, "--method", "DELETE"], {
248
+ endpoint,
249
+ payload: {},
250
+ hint: "verify repo permissions and comment id; check gh auth status",
251
+ ...seams,
252
+ });
253
+ }
254
+ /** `GET /user` -- authenticated GitHub login for review-owner claims. */
255
+ export function restGetUser(seams = {}) {
256
+ return execApi(["user"], {
257
+ endpoint: "user",
258
+ payload: null,
259
+ hint: "verify gh auth status (`gh auth login`)",
260
+ runGhApiFn: seams.runGhApiFn,
261
+ whichFn: seams.whichFn,
262
+ });
263
+ }
229
264
  export function restUpdateIssue(repo, n, patch, seams = {}) {
230
265
  const [owner, name] = splitRepo(repo);
231
266
  const endpoint = `repos/${owner}/${name}/issues/${n}`;
@@ -5,6 +5,7 @@ import { MIGRATE_COMPLETION_NUDGE, shouldEmitMigrateNudge } from "../init-deposi
5
5
  import { detectEnvironmentContext, environmentContextToDict, formatEnvironmentContext, } from "../platform/shell-context.js";
6
6
  import { disclosureLine } from "../policy/disclosure.js";
7
7
  import { resolvePolicy } from "../policy/resolve.js";
8
+ import { maybeFormatProductSignalConsentPrompt } from "../product-signal/consent-prompt.js";
8
9
  import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
9
10
  import { runDefaultMode } from "../triage/welcome/default-mode.js";
10
11
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
@@ -380,6 +381,10 @@ export function runSessionStart(projectRoot, options = {}) {
380
381
  catch {
381
382
  // observability only — session start must not abort on transient eval readback I/O
382
383
  }
384
+ const consentPrompt = maybeFormatProductSignalConsentPrompt({ projectRoot });
385
+ if (consentPrompt.length > 0) {
386
+ lines.push(consentPrompt.trimEnd());
387
+ }
383
388
  const payload = newRitualStatePayload({
384
389
  sessionId: (options.newSessionId ?? randomUUID)(),
385
390
  gitHead: gitHeadValue,
@@ -2,10 +2,24 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { LEGACY_VBRIEF_VERSION } from "@deftai/directive-types";
5
+ import { assertProjectionContained, assertWriteTargetSafe, ProjectionContainmentError, } from "../fs/projection-containment.js";
5
6
  const UNRELEASED_RE = /## \[Unreleased\][ \t]*\n([\s\S]*?)(?=\n## \[|$)/;
6
7
  const CHANGE_NAME_RE = /^[\w][\w-]*$/;
7
8
  const COMMIT_TYPES = "feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert";
8
9
  const COMMIT_SUBJECT_RE = new RegExp(`^(${COMMIT_TYPES})(\\(.+\\))?!?: .+`);
10
+ /** Refuse task-surface writes that escape via repo-controlled symlinks (#2807). */
11
+ function projectionTarget(projectDir, ...relSegments) {
12
+ const target = join(resolve(projectDir), ...relSegments);
13
+ assertProjectionContained(projectDir, target);
14
+ return target;
15
+ }
16
+ function containmentExitCode(io, err) {
17
+ if (err instanceof ProjectionContainmentError) {
18
+ io.writeErr(`FAIL: ${err.message}\n`);
19
+ return 2;
20
+ }
21
+ return null;
22
+ }
9
23
  function proposalTemplate(name) {
10
24
  return {
11
25
  vBRIEFInfo: { version: LEGACY_VBRIEF_VERSION },
@@ -67,19 +81,35 @@ export function runChangeInit(projectRoot, name, io) {
67
81
  io.writeOut("FAIL: Name must contain only alphanumeric characters, underscores, and hyphens\n");
68
82
  return 1;
69
83
  }
70
- const base = join(resolve(projectRoot), "history", "changes", trimmed);
71
- if (existsSync(base)) {
72
- io.writeOut(`FAIL: ${base} already exists\n`);
73
- return 1;
84
+ const projectAbs = resolve(projectRoot);
85
+ try {
86
+ const base = projectionTarget(projectAbs, join("history", "changes", trimmed));
87
+ if (existsSync(base)) {
88
+ io.writeOut(`FAIL: ${base} already exists\n`);
89
+ return 1;
90
+ }
91
+ const specsDir = join(base, "specs");
92
+ assertProjectionContained(projectAbs, specsDir);
93
+ mkdirSync(specsDir, { recursive: true });
94
+ const proposalPath = join(base, "proposal.xbrief.json");
95
+ const tasksPath = join(base, "tasks.xbrief.json");
96
+ assertWriteTargetSafe(projectAbs, proposalPath);
97
+ assertWriteTargetSafe(projectAbs, tasksPath);
98
+ writeFileSync(proposalPath, `${JSON.stringify(proposalTemplate(trimmed), null, 2)}\n`, "utf8");
99
+ writeFileSync(tasksPath, `${JSON.stringify(tasksTemplate(trimmed), null, 2)}\n`, "utf8");
100
+ io.writeOut(`OK: Created change proposal at ${base}/\n`);
101
+ for (const file of ["proposal.xbrief.json", "tasks.xbrief.json", "specs/"]) {
102
+ io.writeOut(` - ${file}\n`);
103
+ }
104
+ return 0;
74
105
  }
75
- mkdirSync(join(base, "specs"), { recursive: true });
76
- writeFileSync(join(base, "proposal.xbrief.json"), `${JSON.stringify(proposalTemplate(trimmed), null, 2)}\n`, "utf8");
77
- writeFileSync(join(base, "tasks.xbrief.json"), `${JSON.stringify(tasksTemplate(trimmed), null, 2)}\n`, "utf8");
78
- io.writeOut(`OK: Created change proposal at ${base}/\n`);
79
- for (const file of ["proposal.xbrief.json", "tasks.xbrief.json", "specs/"]) {
80
- io.writeOut(` - ${file}\n`);
106
+ catch (err) {
107
+ const code = containmentExitCode(io, err);
108
+ if (code !== null) {
109
+ return code;
110
+ }
111
+ throw err;
81
112
  }
82
- return 0;
83
113
  }
84
114
  /** Port of ``task commit:lint`` inline Python (#2022 Phase 2). */
85
115
  export function runCommitLint(projectRoot, io) {
@@ -119,8 +149,18 @@ export function runInstallUninstall(projectRoot, io) {
119
149
  const filtered = lines.filter((line) => !line.startsWith("See deft/main.md") && !line.startsWith("Skills: deft/skills/"));
120
150
  const next = filtered.join("");
121
151
  if (next !== original) {
122
- writeFileSync(path, next, "utf8");
123
- io.writeOut("Removed deft entry from AGENTS.md\n");
152
+ try {
153
+ assertWriteTargetSafe(resolve(projectRoot), path);
154
+ writeFileSync(path, next, "utf8");
155
+ io.writeOut("Removed deft entry from AGENTS.md\n");
156
+ }
157
+ catch (err) {
158
+ const code = containmentExitCode(io, err);
159
+ if (code !== null) {
160
+ return code;
161
+ }
162
+ throw err;
163
+ }
124
164
  }
125
165
  else {
126
166
  io.writeOut("No deft entry found in AGENTS.md\n");
@@ -0,0 +1,32 @@
1
+ import type { HookEvent, HookHost } from "../hooks/dispatcher.js";
2
+ export type AgentHookLiveProbeIssue = "hook-command-missing" | "spawn-failed" | "empty-stdout" | "unparseable-json" | "missing-allow" | "missing-deny";
3
+ export interface AgentHookLiveProbeCase {
4
+ readonly host: HookHost;
5
+ readonly event: HookEvent;
6
+ readonly fixture: "allow" | "deny";
7
+ readonly issue: AgentHookLiveProbeIssue;
8
+ readonly detail: string;
9
+ }
10
+ export interface AgentHookLiveProbeResult {
11
+ readonly code: 0 | 1 | 2;
12
+ readonly message: string;
13
+ readonly cases: readonly AgentHookLiveProbeCase[];
14
+ }
15
+ export interface AgentHookLiveProbeSeams {
16
+ readonly resolveCommand?: (name: string) => string | null;
17
+ readonly spawnHook?: (input: {
18
+ readonly command: string;
19
+ readonly args: readonly string[];
20
+ readonly stdin: string;
21
+ readonly cwd: string;
22
+ readonly env?: NodeJS.ProcessEnv;
23
+ }) => {
24
+ readonly status: number;
25
+ readonly stdout: string;
26
+ readonly stderr: string;
27
+ };
28
+ }
29
+ export declare function quoteWindowsCmdArg(value: string): string;
30
+ /** Spawn the configured hook command and assert Cursor tool.before allow/deny behavior. */
31
+ export declare function probeAgentHooksLive(projectRoot: string, seams?: AgentHookLiveProbeSeams): AgentHookLiveProbeResult;
32
+ //# sourceMappingURL=agent-hooks-live-probe.d.ts.map
@@ -0,0 +1,216 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { READ_ONLY_HOOK_ENV } from "../hooks/tools.js";
4
+ import { DEFT_HOOK_COMMAND_MARKER } from "../init-deposit/agent-hooks.js";
5
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
6
+ import { quoteWin32CommandForShell, resolveCommandOnPath, shouldUseShellForCommand, } from "./command-spawn.js";
7
+ const LIVE_PROBE_HOST = "cursor";
8
+ const LIVE_PROBE_EVENT = "tool.before";
9
+ function resolveHookCommand(seams) {
10
+ const resolveCommand = seams.resolveCommand ?? resolveCommandOnPath;
11
+ const deftHook = resolveCommand(DEFT_HOOK_COMMAND_MARKER);
12
+ if (deftHook !== null) {
13
+ return { command: deftHook, argsPrefix: [] };
14
+ }
15
+ const deft = resolveCommand("deft");
16
+ if (deft !== null) {
17
+ return { command: deft, argsPrefix: ["hook:dispatch"] };
18
+ }
19
+ const directive = resolveCommand("directive");
20
+ if (directive !== null) {
21
+ return { command: directive, argsPrefix: ["hook:dispatch"] };
22
+ }
23
+ return null;
24
+ }
25
+ export function quoteWindowsCmdArg(value) {
26
+ const escaped = value.replace(/%/g, "%%");
27
+ if (!/[\s"&|<>^()]/.test(escaped)) {
28
+ return escaped;
29
+ }
30
+ return `"${escaped.replace(/"/g, '""')}"`;
31
+ }
32
+ function spawnHookWithStdin(command, args, stdin, cwd, env = process.env) {
33
+ const shell = shouldUseShellForCommand(command);
34
+ if (shell && process.platform === "win32") {
35
+ const cmdLine = [quoteWin32CommandForShell(command), ...args.map(quoteWindowsCmdArg)].join(" ");
36
+ const proc = spawnSync(cmdLine, [], {
37
+ input: stdin,
38
+ cwd,
39
+ env,
40
+ encoding: "utf8",
41
+ stdio: ["pipe", "pipe", "pipe"],
42
+ shell: true,
43
+ windowsHide: true,
44
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
45
+ });
46
+ const status = proc.status ?? (proc.error ? 2 : proc.signal ? 128 : 0);
47
+ return {
48
+ status,
49
+ stdout: typeof proc.stdout === "string" ? proc.stdout : "",
50
+ stderr: typeof proc.stderr === "string" ? proc.stderr : "",
51
+ };
52
+ }
53
+ const spawnCmd = shell && process.platform === "win32" ? quoteWin32CommandForShell(command) : command;
54
+ const proc = spawnSync(spawnCmd, [...args], {
55
+ input: stdin,
56
+ cwd,
57
+ env,
58
+ encoding: "utf8",
59
+ stdio: ["pipe", "pipe", "pipe"],
60
+ shell,
61
+ windowsHide: true,
62
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
63
+ });
64
+ const status = proc.status ?? (proc.error ? 2 : proc.signal ? 128 : 0);
65
+ return {
66
+ status,
67
+ stdout: typeof proc.stdout === "string" ? proc.stdout : "",
68
+ stderr: typeof proc.stderr === "string" ? proc.stderr : "",
69
+ };
70
+ }
71
+ function allowFixture(projectRoot) {
72
+ return JSON.stringify({
73
+ tool_name: "Read",
74
+ cwd: projectRoot,
75
+ workspace_roots: [projectRoot],
76
+ });
77
+ }
78
+ function denyFixture(projectRoot) {
79
+ return JSON.stringify({
80
+ tool_name: "Task",
81
+ cwd: projectRoot,
82
+ workspace_roots: [projectRoot],
83
+ tool_input: { subagent_type: "generalPurpose", prompt: "implement" },
84
+ });
85
+ }
86
+ function denyProbeEnv() {
87
+ return { ...process.env, [READ_ONLY_HOOK_ENV]: "1" };
88
+ }
89
+ function parseCursorDecision(stdout) {
90
+ const trimmed = stdout.trim();
91
+ if (trimmed.length === 0) {
92
+ return { ok: false, detail: "empty stdout" };
93
+ }
94
+ try {
95
+ const parsed = JSON.parse(trimmed);
96
+ if (parsed !== null &&
97
+ typeof parsed === "object" &&
98
+ !Array.isArray(parsed) &&
99
+ parsed.permission === "allow") {
100
+ return { ok: true, permission: "allow", detail: "permission allow" };
101
+ }
102
+ if (parsed !== null &&
103
+ typeof parsed === "object" &&
104
+ !Array.isArray(parsed) &&
105
+ parsed.permission === "deny") {
106
+ return { ok: true, permission: "deny", detail: "permission deny" };
107
+ }
108
+ return { ok: false, detail: "stdout JSON missing permission allow/deny" };
109
+ }
110
+ catch {
111
+ return { ok: false, detail: "stdout is not valid JSON" };
112
+ }
113
+ }
114
+ function runFixtureProbe(resolved, projectRoot, fixture, stdin, env, spawnHook) {
115
+ const args = [
116
+ ...resolved.argsPrefix,
117
+ "--host",
118
+ LIVE_PROBE_HOST,
119
+ "--event",
120
+ LIVE_PROBE_EVENT,
121
+ "--project-root",
122
+ projectRoot,
123
+ ];
124
+ const spawned = spawnHook({
125
+ command: resolved.command,
126
+ args,
127
+ stdin,
128
+ cwd: projectRoot,
129
+ env,
130
+ });
131
+ if (spawned.status !== 0) {
132
+ return {
133
+ host: LIVE_PROBE_HOST,
134
+ event: LIVE_PROBE_EVENT,
135
+ fixture,
136
+ issue: "spawn-failed",
137
+ detail: `hook command exited ${spawned.status}${spawned.stderr.trim() ? `: ${spawned.stderr.trim()}` : ""}`,
138
+ };
139
+ }
140
+ const decision = parseCursorDecision(spawned.stdout);
141
+ if (!decision.ok) {
142
+ return {
143
+ host: LIVE_PROBE_HOST,
144
+ event: LIVE_PROBE_EVENT,
145
+ fixture,
146
+ issue: decision.detail.includes("JSON") ? "unparseable-json" : "empty-stdout",
147
+ detail: decision.detail,
148
+ };
149
+ }
150
+ if (fixture === "allow" && decision.permission !== "allow") {
151
+ return {
152
+ host: LIVE_PROBE_HOST,
153
+ event: LIVE_PROBE_EVENT,
154
+ fixture,
155
+ issue: "missing-allow",
156
+ detail: `expected permission allow, got ${decision.permission ?? "none"}`,
157
+ };
158
+ }
159
+ if (fixture === "deny" && decision.permission !== "deny") {
160
+ return {
161
+ host: LIVE_PROBE_HOST,
162
+ event: LIVE_PROBE_EVENT,
163
+ fixture,
164
+ issue: "missing-deny",
165
+ detail: `expected permission deny, got ${decision.permission ?? "none"}`,
166
+ };
167
+ }
168
+ return null;
169
+ }
170
+ /** Spawn the configured hook command and assert Cursor tool.before allow/deny behavior. */
171
+ export function probeAgentHooksLive(projectRoot, seams = {}) {
172
+ const root = resolve(projectRoot);
173
+ const resolved = resolveHookCommand(seams);
174
+ if (resolved === null) {
175
+ return {
176
+ code: 2,
177
+ message: "deft agent hooks live probe unavailable: neither deft-hook nor deft/directive hook:dispatch is on PATH.",
178
+ cases: [
179
+ {
180
+ host: LIVE_PROBE_HOST,
181
+ event: LIVE_PROBE_EVENT,
182
+ fixture: "allow",
183
+ issue: "hook-command-missing",
184
+ detail: `${DEFT_HOOK_COMMAND_MARKER} not found on PATH`,
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ const spawnHook = seams.spawnHook ??
190
+ ((input) => spawnHookWithStdin(input.command, input.args, input.stdin, input.cwd, input.env ?? process.env));
191
+ const failures = [];
192
+ for (const [fixture, stdin, env] of [
193
+ ["allow", allowFixture(root), process.env],
194
+ ["deny", denyFixture(root), denyProbeEnv()],
195
+ ]) {
196
+ const failure = runFixtureProbe(resolved, root, fixture, stdin, env, spawnHook);
197
+ if (failure !== null)
198
+ failures.push(failure);
199
+ }
200
+ if (failures.length === 0) {
201
+ return {
202
+ code: 0,
203
+ message: "deft agent hooks live probe passed for Cursor tool.before allow and deny fixtures.",
204
+ cases: [],
205
+ };
206
+ }
207
+ const summary = failures
208
+ .map((entry) => `${entry.fixture}: ${entry.issue} (${entry.detail})`)
209
+ .join("; ");
210
+ return {
211
+ code: 1,
212
+ message: `deft agent hooks live probe FAILED: ${summary}. Recovery: reinstall @deftai/directive and run \`deft update\`.`,
213
+ cases: failures,
214
+ };
215
+ }
216
+ //# sourceMappingURL=agent-hooks-live-probe.js.map
@@ -1,4 +1,5 @@
1
1
  export * from "./agent-hooks.js";
2
+ export * from "./agent-hooks-live-probe.js";
2
3
  export * from "./command-spawn.js";
3
4
  export * from "./node-runtime.js";
4
5
  export * from "./toolchain-check.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./agent-hooks.js";
2
+ export * from "./agent-hooks-live-probe.js";
2
3
  export * from "./command-spawn.js";
3
4
  export * from "./node-runtime.js";
4
5
  export * from "./toolchain-check.js";
@@ -4,6 +4,12 @@ export { LEGACY_VBRIEF_VERSION, VBRIEF_REFERENCE_PREFIX, VBRIEF_VERSION, XBRIEF_
4
4
  export declare const V08_CONTAINER_ITEM_TYPES: Set<string>;
5
5
  export declare const LEGACY_ARTIFACT_DIR: "vbrief";
6
6
  export declare const MIGRATED_ARTIFACT_DIR: "xbrief";
7
+ /**
8
+ * Obsolete framework support narrative at the lifecycle root. Legacy
9
+ * `migrate:xbrief` copied this to `xbrief/vbrief.md` with broken framework
10
+ * links; consumer projections use `xbrief.md` instead (#2806).
11
+ */
12
+ export declare const OBSOLETE_FRAMEWORK_NARRATIVE_FILENAME: "vbrief.md";
7
13
  export declare const LEGACY_ARTIFACT_SUFFIX: ".vbrief.json";
8
14
  export declare const MIGRATED_ARTIFACT_SUFFIX: ".xbrief.json";
9
15
  export declare const LEGACY_INFO_ROOT_KEY: "vBRIEFInfo";
@@ -5,6 +5,12 @@ export { LEGACY_VBRIEF_VERSION, VBRIEF_REFERENCE_PREFIX, VBRIEF_VERSION, XBRIEF_
5
5
  export const V08_CONTAINER_ITEM_TYPES = new Set([...VALID_PLAN_ITEM_TYPES].filter((t) => t !== "task"));
6
6
  export const LEGACY_ARTIFACT_DIR = "vbrief";
7
7
  export const MIGRATED_ARTIFACT_DIR = "xbrief";
8
+ /**
9
+ * Obsolete framework support narrative at the lifecycle root. Legacy
10
+ * `migrate:xbrief` copied this to `xbrief/vbrief.md` with broken framework
11
+ * links; consumer projections use `xbrief.md` instead (#2806).
12
+ */
13
+ export const OBSOLETE_FRAMEWORK_NARRATIVE_FILENAME = "vbrief.md";
8
14
  export const LEGACY_ARTIFACT_SUFFIX = ".vbrief.json";
9
15
  export const MIGRATED_ARTIFACT_SUFFIX = ".xbrief.json";
10
16
  export const LEGACY_INFO_ROOT_KEY = "vBRIEFInfo";
@@ -1,9 +1,9 @@
1
1
  export { detectStaleUnmanagedHeader, type HeaderPatchOutcome, type HeaderRewriteResult, type HeaderTokenReplacement, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, type StaleHeaderDetection, } from "./agents-header.js";
2
- export { VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, VBRIEF_DEPRECATION_MARKER_SENTINEL, } from "./constants.js";
2
+ export { OBSOLETE_FRAMEWORK_NARRATIVE_FILENAME, VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, VBRIEF_DEPRECATION_MARKER_SENTINEL, } from "./constants.js";
3
3
  export { detectLegacyVbriefLayout, detectXbriefConvergence, type LegacyVbriefLayoutDetection, type XbriefConvergenceDetection, type XbriefConvergenceState, } from "./detect.js";
4
4
  export { BUILTIN_ALLOW_LIST, type DriftEvaluateOptions, type DriftEvaluateResult, type DriftFinding, type DriftScanMode, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
5
5
  export { hasVbriefDeprecationMarker, isDirectory, isEffectivelyEmptyDir, } from "./fs-helpers.js";
6
- export { convergeLegacyVbriefRoot, emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, type VbriefConvergeAction, type XbriefMigrationArgs, type XbriefMigrationIo, type XbriefMigrationOutcome, } from "./migrate-project.js";
6
+ export { convergeLegacyVbriefRoot, emitXbriefMigration, removeStaleMigratedFrameworkNarrative, runXbriefMigration, runXbriefMigrationCli, shouldOmitLegacyMigrationFile, type VbriefConvergeAction, type XbriefMigrationArgs, type XbriefMigrationIo, type XbriefMigrationOutcome, } from "./migrate-project.js";
7
7
  export { renderXbriefMigrationLine, xbriefMigrationGuidance } from "./signpost.js";
8
8
  export { assertFeatureEmissionAllowed, assertLayoutAwareWritePath, FeatureEmissionRejectedError, type JsonObject, type JsonValue, readDeclaredArtifactVersion, resolveLayoutAwareRelativePath, rewriteEmbeddedTokens, SplitLayoutRejectedError, TransformError, transformArtifactV06ToV08, transformArtifactV06ToV08Transactional, } from "./transforms.js";
9
9
  export { isPatchOnlyUpgrade, parseSemverPrefix } from "./version.js";
@@ -1,9 +1,9 @@
1
1
  export { detectStaleUnmanagedHeader, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, } from "./agents-header.js";
2
- export { VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, VBRIEF_DEPRECATION_MARKER_SENTINEL, } from "./constants.js";
2
+ export { OBSOLETE_FRAMEWORK_NARRATIVE_FILENAME, VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, VBRIEF_DEPRECATION_MARKER_SENTINEL, } from "./constants.js";
3
3
  export { detectLegacyVbriefLayout, detectXbriefConvergence, } from "./detect.js";
4
4
  export { BUILTIN_ALLOW_LIST, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
5
5
  export { hasVbriefDeprecationMarker, isDirectory, isEffectivelyEmptyDir, } from "./fs-helpers.js";
6
- export { convergeLegacyVbriefRoot, emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, } from "./migrate-project.js";
6
+ export { convergeLegacyVbriefRoot, emitXbriefMigration, removeStaleMigratedFrameworkNarrative, runXbriefMigration, runXbriefMigrationCli, shouldOmitLegacyMigrationFile, } from "./migrate-project.js";
7
7
  export { renderXbriefMigrationLine, xbriefMigrationGuidance } from "./signpost.js";
8
8
  export { assertFeatureEmissionAllowed, assertLayoutAwareWritePath, FeatureEmissionRejectedError, readDeclaredArtifactVersion, resolveLayoutAwareRelativePath, rewriteEmbeddedTokens, SplitLayoutRejectedError, TransformError, transformArtifactV06ToV08, transformArtifactV06ToV08Transactional, } from "./transforms.js";
9
9
  export { isPatchOnlyUpgrade, parseSemverPrefix } from "./version.js";
@@ -33,6 +33,13 @@ export type XbriefMigrationOutcome = {
33
33
  readonly kind: "config";
34
34
  readonly message: string;
35
35
  };
36
+ /** True when a legacy lifecycle file is an obsolete framework narrative (#2806). */
37
+ export declare function shouldOmitLegacyMigrationFile(relativePath: string): boolean;
38
+ /**
39
+ * Remove a stale `xbrief/vbrief.md` left by an earlier migrate:xbrief run.
40
+ * Project JSON records and `xbrief/schemas/` are untouched (#2806).
41
+ */
42
+ export declare function removeStaleMigratedFrameworkNarrative(projectRoot: string): boolean;
36
43
  /**
37
44
  * Converge a leftover legacy `vbrief/` root to an unambiguous state (#2270).
38
45
  * An effectively-empty tree is removed by default (single canonical root); a