@deftai/directive-core 0.85.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.
@@ -14,9 +14,9 @@ export declare const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
14
14
  export declare const NPM_REGISTRY_MIRROR_DOC_URL = "https://github.com/deftai/directive/blob/master/content/UPGRADING.md#corporate-or-mirrored-npm-registry";
15
15
  export declare const NETWORK_DISCLOSURE_LINE: string;
16
16
  export declare const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE: string;
17
- export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "vbrief"];
17
+ export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "xbrief"];
18
18
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
19
- export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "vbrief"];
19
+ export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "xbrief"];
20
20
  export declare const DEFT_REPO_POSITIVE_MARKERS: readonly ["content/templates/agents-entry.md", "content/skills/deft-directive-build/SKILL.md"];
21
21
  export declare const EXPECTED_CONTENT_DIRS: readonly ["languages", "strategies", "skills", "templates"];
22
22
  /** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
@@ -46,9 +46,9 @@ export const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE = "skip -- offline tier (def
46
46
  "(discloses tool + registry before contacting either).";
47
47
  // Engine / lifecycle dirs that stay at the framework root (NOT relocated by
48
48
  // #1875). Shippable-content dirs moved under content/ -- see EXPECTED_CONTENT_DIRS.
49
- export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "vbrief"];
49
+ export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "xbrief"];
50
50
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
51
- export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "vbrief"];
51
+ export const CONSUMER_FRAMEWORK_DIRS = ["tasks", "xbrief"];
52
52
  // Post-#1875 content/ move: these framework-internal markers now live under
53
53
  // content/ in the SOURCE repo. They identify a deft source checkout (a consumer
54
54
  // would never reproduce them); the C1 flatten means a consumer deposit has no
@@ -2,7 +2,8 @@ import { type ResolutionFacts } from "../resolution/index.js";
2
2
  import { createPlainSink } from "./output.js";
3
3
  import type { DoctorSeams, Finding, ResolutionSummary } from "./types.js";
4
4
  export declare function cmdDoctor(args: readonly string[], seams?: DoctorSeams): number;
5
- export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
5
+ export declare function runAgentHooksHealthCheck(projectRoot: string, consumerContext: boolean, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): boolean;
6
+ export declare function runAgentHooksLiveProbeCheck(projectRoot: string, sink: ReturnType<typeof createPlainSink>, addFinding: (finding: Finding) => void, seams: DoctorSeams): void;
6
7
  /**
7
8
  * Never emit a bare `task ...` remediation in a project without Taskfile wiring
8
9
  * (#2267). The `directive` surface always works; `task deft:X` is optional and
@@ -7,6 +7,7 @@ import { loadProjectDefinition } from "../policy/resolve.js";
7
7
  import { checkLocalEngineIntegrity, classify, detectPackageManager, evaluateSkew, reconcileVersions, plan as resolvePlan, } from "../resolution/index.js";
8
8
  import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
9
9
  import { evaluateAgentHooks } from "../verify-env/agent-hooks.js";
10
+ import { probeAgentHooksLive } from "../verify-env/agent-hooks-live-probe.js";
10
11
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
11
12
  import { runChecks } from "./checks.js";
12
13
  import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
@@ -16,7 +17,7 @@ import { pythonJsonDump } from "./json.js";
16
17
  import { parseInstallRootFromAgentsMd } from "./manifest.js";
17
18
  import { runNpmRegistryMirrorCheck } from "./npm-registry.js";
18
19
  import { createPlainSink } from "./output.js";
19
- import { readTextSafe, resolveDefaultFrameworkRoot, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
20
+ import { readTextSafe, resolveFrameworkRootForProject, resolvePath, resolveVersion, runningInsideDeftRepo, } from "./paths.js";
20
21
  import { runPayloadStalenessCheck } from "./payload-staleness.js";
21
22
  import { runLocalSignpostChecks } from "./signpost-checks.js";
22
23
  import { classifyTaskfileInclude, formatMissingIncludeSnippet, includesBlockHasDeftTaskfile, resolveConsumerTaskfile, } from "./taskfile.js";
@@ -57,7 +58,7 @@ export function cmdDoctor(args, seams = {}) {
57
58
  const quietMode = flags.quiet;
58
59
  const fullMode = flags.full;
59
60
  const projectRoot = resolvePath(flags.projectRoot ?? process.cwd());
60
- const frameworkRoot = seams.frameworkRoot ?? resolveDefaultFrameworkRoot();
61
+ const frameworkRoot = seams.frameworkRoot ?? resolveFrameworkRootForProject(projectRoot);
61
62
  const consumerContext = resolve(projectRoot) !== resolve(frameworkRoot);
62
63
  const whichFn = seams.whichFn ?? defaultWhich;
63
64
  const nowFn = seams.now ?? (() => new Date());
@@ -165,7 +166,17 @@ export function cmdDoctor(args, seams = {}) {
165
166
  sink.blank();
166
167
  }
167
168
  sink.info("Checking agent-host hook registration...");
168
- runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
169
+ const findingCountBeforeHooks = findings.length;
170
+ const hooksHealthy = runAgentHooksHealthCheck(projectRoot, consumerContext, sink, addFinding, seams);
171
+ if (fullMode && hooksHealthy) {
172
+ const lastFinding = findings[findings.length - 1];
173
+ if (findings.length > findingCountBeforeHooks &&
174
+ lastFinding?.check === "agent-hooks-registration" &&
175
+ lastFinding?.status === "registered") {
176
+ findings.pop();
177
+ }
178
+ runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams);
179
+ }
169
180
  if (consumerContext) {
170
181
  if (!jsonMode) {
171
182
  sink.blank();
@@ -353,40 +364,81 @@ export function runAgentHooksHealthCheck(projectRoot, consumerContext, sink, add
353
364
  const reason = "maintainer source checkout; project hook deposit is consumer-only";
354
365
  sink.info(`${checkName}: skip -- ${reason}`);
355
366
  addFinding({ severity: "skip", message: reason, check: checkName, status: "skip" });
356
- return;
367
+ return false;
357
368
  }
358
369
  try {
359
370
  const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
360
- if (result.code === 0) {
361
- const message = `${checkName}: registered and structurally valid; ` +
362
- "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
363
- sink.success(message);
371
+ if (result.code !== 0) {
372
+ const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
373
+ sink.warn(message);
364
374
  addFinding({
365
- severity: "skip",
375
+ severity: "warning",
366
376
  message,
367
377
  check: checkName,
368
- status: "registered",
378
+ status: result.code === 2 ? "unavailable" : "incomplete",
369
379
  registrations: result.registrations,
370
- trust_status: "not-verifiable",
371
- trust_review: "Open `/hooks` in Codex and review the project hook commands.",
380
+ suggestion: "deft update",
372
381
  });
373
- return;
382
+ return false;
374
383
  }
375
- const message = `${checkName}: ${result.message.replace(/\s+/g, " ").trim()}`;
376
- sink.warn(message);
384
+ const message = `${checkName}: registered and structurally valid; ` +
385
+ "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
386
+ sink.success(message);
377
387
  addFinding({
378
- severity: "warning",
388
+ severity: "skip",
379
389
  message,
380
390
  check: checkName,
381
- status: result.code === 2 ? "unavailable" : "incomplete",
391
+ status: "registered",
382
392
  registrations: result.registrations,
383
- suggestion: "deft update",
393
+ trust_status: "not-verifiable",
394
+ trust_review: "Open `/hooks` in Codex and review the project hook commands.",
384
395
  });
396
+ return true;
385
397
  }
386
398
  catch (cause) {
387
399
  const message = `${checkName}: probe failed -- ${String(cause)}`;
388
400
  sink.warn(message);
389
401
  addFinding({ severity: "warning", message, check: checkName, suggestion: "deft update" });
402
+ return false;
403
+ }
404
+ }
405
+ export function runAgentHooksLiveProbeCheck(projectRoot, sink, addFinding, seams) {
406
+ const checkName = "agent-hooks-registration";
407
+ const liveCheckName = "agent-hooks-live-probe";
408
+ try {
409
+ const result = (seams.evaluateAgentHooks ?? evaluateAgentHooks)(projectRoot);
410
+ const liveResult = (seams.probeAgentHooksLive ?? probeAgentHooksLive)(projectRoot);
411
+ if (liveResult.code !== 0) {
412
+ const message = `${liveCheckName}: ${liveResult.message.replace(/\s+/g, " ").trim()}`;
413
+ sink.warn(message);
414
+ addFinding({
415
+ severity: "warning",
416
+ message,
417
+ check: liveCheckName,
418
+ status: liveResult.code === 2 ? "unavailable" : "non-functional",
419
+ cases: liveResult.cases,
420
+ suggestion: "npm i -g @deftai/directive@latest && deft update",
421
+ });
422
+ return;
423
+ }
424
+ const message = `${checkName}: registered, structurally valid, and live probe passed; ` +
425
+ "Codex runtime trust is user-controlled and must be reviewed with `/hooks`";
426
+ sink.success(message);
427
+ addFinding({
428
+ severity: "skip",
429
+ message,
430
+ check: liveCheckName,
431
+ status: "registered-and-functional",
432
+ registrations: result.registrations,
433
+ trust_status: "not-verifiable",
434
+ trust_review: "Open `/hooks` in Codex and review the project hook commands.",
435
+ live_probe: "passed",
436
+ });
437
+ }
438
+ catch (cause) {
439
+ const message = `${liveCheckName}: probe failed -- ${String(cause)}`;
440
+ sink.warn(message);
441
+ addFinding({ severity: "warning", message, check: liveCheckName, suggestion: "deft update" });
390
442
  }
391
443
  }
392
444
  function runInstallIntegrityChecks(projectRoot, sink, addFinding, seams) {
@@ -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;
@@ -147,5 +148,7 @@ export interface DoctorSeams {
147
148
  readonly detectPlanExtensionShadows?: (projectRoot: string) => readonly ShadowedPlanExtension[];
148
149
  /** Read-only agent-host hook registration probe (#2438). */
149
150
  readonly evaluateAgentHooks?: (projectRoot: string) => AgentHookHealthResult;
151
+ /** Live hook spawn probe for doctor --full (#2852). */
152
+ readonly probeAgentHooksLive?: (projectRoot: string) => AgentHookLiveProbeResult;
150
153
  }
151
154
  //# sourceMappingURL=types.d.ts.map
@@ -238,14 +238,22 @@ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefine
238
238
  changedPaths.push(item.path);
239
239
  }
240
240
  }
241
- const adapterAbsolute = join(projectRoot, ".cursor/hooks/deft-cursor-hook-adapter.mjs");
242
- assertDepositContained(projectRoot, adapterAbsolute);
243
- if (existsSync(adapterAbsolute)) {
244
- rmSync(adapterAbsolute, { force: true });
245
- if (!changedPaths.includes(AGENT_HOOK_PATHS[2])) {
246
- changedPaths.push(AGENT_HOOK_PATHS[2]);
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;
247
252
  }
248
253
  }
254
+ if (removedLegacyAdapter && !changedPaths.includes(AGENT_HOOK_PATHS[2])) {
255
+ changedPaths.push(AGENT_HOOK_PATHS[2]);
256
+ }
249
257
  if (changedPaths.length > 0) {
250
258
  io.printf(`Installed Directive agent hooks: ${changedPaths.join(", ")}\n`);
251
259
  }
@@ -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);
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { readCorePackageVersion } from "../engine-version.js";
4
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
4
5
  import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
5
6
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
6
7
  import { appendAuditLog, loadProjectDefinition, projectDefinitionPath } from "./resolve.js";
@@ -56,6 +57,7 @@ export function readOrgForceOnMarker(projectRoot) {
56
57
  }
57
58
  function writeOrgForceOnMarker(projectRoot, marker) {
58
59
  const path = markerPath(projectRoot);
60
+ assertWriteTargetSafe(projectRoot, path);
59
61
  mkdirSync(join(path, ".."), { recursive: true });
60
62
  writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, "utf8");
61
63
  }
@@ -25,7 +25,7 @@ export declare function formatFrameworkCommand(args: readonly string[], options?
25
25
  surface?: string;
26
26
  taskPrefix?: string | null;
27
27
  }): string;
28
- export declare function cmdCoreValidate(argv: readonly string[]): number;
28
+ export declare function cmdCoreValidate(argv: readonly string[], root?: string): number;
29
29
  export declare function cmdCoreLint(_argv: readonly string[]): number;
30
30
  export declare function cmdCoreTest(_argv: readonly string[]): number;
31
31
  export declare function resolveFrameworkRoot(): string;
@@ -154,7 +154,7 @@ export function formatFrameworkCommand(args, options = {}) {
154
154
  }
155
155
  return [surface, ...parts].join(" ");
156
156
  }
157
- const EXCLUDE_PARTS = new Set([".git", "backup"]);
157
+ const EXCLUDE_PARTS = new Set([".git", "backup", "node_modules", ".deft-scratch"]);
158
158
  function collectMarkdownFiles(root) {
159
159
  const out = [];
160
160
  const walk = (dir, parts) => {
@@ -189,12 +189,12 @@ function collectMarkdownFiles(root) {
189
189
  walk(root, []);
190
190
  return out.sort();
191
191
  }
192
- export function cmdCoreValidate(argv) {
192
+ export function cmdCoreValidate(argv, root = ".") {
193
193
  if (argv.length > 0) {
194
194
  process.stderr.write(`error: core:validate does not accept arguments: ${argv.join(" ")}\n`);
195
195
  return 2;
196
196
  }
197
- const files = collectMarkdownFiles(".");
197
+ const files = collectMarkdownFiles(root);
198
198
  for (const path of files)
199
199
  process.stdout.write(`✓ ${path}\n`);
200
200
  process.stdout.write(`✓ All ${files.length} markdown files validated\n`);
@@ -271,7 +271,7 @@ function runBuildDistArgv(argv) {
271
271
  return result.status ?? 1;
272
272
  }
273
273
  const TS_INLINE = {
274
- "framework_commands:_cmd_core_validate": (argv) => cmdCoreValidate(argv),
274
+ "framework_commands:_cmd_core_validate": (argv, cwd) => cmdCoreValidate(argv, cwd),
275
275
  "doctor:cmd_doctor": (argv) => cmdDoctor(argv),
276
276
  "build_dist:main": (argv) => runBuildDistArgv(argv),
277
277
  };
@@ -349,7 +349,7 @@ function invokeEntrypoint(entrypoint, argv, cwd, frameworkRoot, noArgv, capture)
349
349
  };
350
350
  let code;
351
351
  try {
352
- code = inline(argv);
352
+ code = inline(argv, cwd);
353
353
  }
354
354
  finally {
355
355
  process.stdout.write = prevOut;
@@ -357,7 +357,7 @@ function invokeEntrypoint(entrypoint, argv, cwd, frameworkRoot, noArgv, capture)
357
357
  }
358
358
  return { code, stdout: chunks.out, stderr: chunks.err };
359
359
  }
360
- return { code: inline(argv), stdout: "", stderr: "" };
360
+ return { code: inline(argv, cwd), stdout: "", stderr: "" };
361
361
  }
362
362
  const verb = ENTRYPOINT_VERB[entrypoint];
363
363
  if (verb) {
@@ -3,7 +3,11 @@ export declare function renderRoadmapToBuffer(pendingDir: string, completedDir?:
3
3
  /** @deprecated Prefer ``renderRoadmapToBuffer`` — kept for existing imports and parity harnesses. */
4
4
  export declare function generateRoadmapContent(pendingDir: string, completedDir?: string): string;
5
5
  export type RenderRoadmapResult = readonly [boolean, string];
6
- export declare function renderRoadmap(pendingDir: string, outPath: string, completedDir?: string): RenderRoadmapResult;
6
+ export type RenderRoadmapOptions = {
7
+ completedDir?: string;
8
+ projectRoot?: string;
9
+ };
10
+ export declare function renderRoadmap(pendingDir: string, outPath: string, completedDirOrOptions?: string | RenderRoadmapOptions): RenderRoadmapResult;
7
11
  export declare function checkDrift(pendingDir: string, roadmapPath: string, completedDir?: string): RenderRoadmapResult;
8
12
  /** CLI entry (mirrors ``scripts/roadmap_render.main``). */
9
13
  export declare function main(argv: readonly string[]): number;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
3
4
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
4
5
  import { MIGRATOR_METADATA_KEY, ROADMAP_BANNER } from "./constants.js";
5
6
  import { phaseSortKey } from "./text-utils.js";
@@ -351,9 +352,22 @@ export function renderRoadmapToBuffer(pendingDir, completedDir) {
351
352
  export function generateRoadmapContent(pendingDir, completedDir) {
352
353
  return renderRoadmapToBuffer(pendingDir, completedDir);
353
354
  }
354
- export function renderRoadmap(pendingDir, outPath, completedDir) {
355
+ export function renderRoadmap(pendingDir, outPath, completedDirOrOptions) {
356
+ let completedDir;
357
+ let projectRoot;
358
+ if (typeof completedDirOrOptions === "string") {
359
+ completedDir = completedDirOrOptions;
360
+ }
361
+ else if (completedDirOrOptions !== undefined) {
362
+ completedDir = completedDirOrOptions.completedDir;
363
+ projectRoot = completedDirOrOptions.projectRoot;
364
+ }
355
365
  try {
356
366
  const content = renderRoadmapToBuffer(pendingDir, completedDir);
367
+ // Trust boundary is the project root — never dirname(outPath), which follows a
368
+ // diverted parent symlink and would make containment pass outside the checkout.
369
+ const projectDir = projectRoot !== undefined ? resolve(projectRoot) : resolve(pendingDir, "..", "..");
370
+ assertWriteTargetSafe(projectDir, resolve(outPath));
357
371
  writeFileSync(outPath, content, "utf8");
358
372
  return [true, `✓ Rendered ROADMAP.md to ${outPath}`];
359
373
  }
@@ -415,7 +429,11 @@ export function main(argv) {
415
429
  process.stdout.write(`${msg}\n`);
416
430
  return ok ? 0 : 1;
417
431
  }
418
- const [ok, msg] = renderRoadmap(pendingDir, outPath);
432
+ // When --project-root is set, use it; otherwise renderRoadmap derives from pendingDir
433
+ // (…/xbrief|vbrief/pending → project root). Never use dirname(outPath).
434
+ const [ok, msg] = renderRoadmap(pendingDir, outPath, {
435
+ projectRoot: projectRoot !== undefined ? resolve(projectRoot) : undefined,
436
+ });
419
437
  process.stdout.write(`${msg}\n`);
420
438
  return ok ? 0 : 1;
421
439
  }
@@ -13,6 +13,7 @@
13
13
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
14
14
  import { basename, dirname, join, relative, resolve } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { assertWriteTargetSafe } from "../fs/projection-containment.js";
16
17
  import { TEMPLATE } from "./rule-map-template.js";
17
18
  const MARKER_RE = /^\s*[-*]\s*([!~⊗≉?])\s/;
18
19
  const MARKER_LABEL = {
@@ -684,6 +685,10 @@ export function main(argv) {
684
685
  process.stdout.write("ok: docs/RULE-MAP.md is up to date\n");
685
686
  return 0;
686
687
  }
688
+ // Preflight both destinations before either write so a later HTML refusal
689
+ // cannot leave RULE-MAP.md updated while docs/rule-map/index.html stays stale.
690
+ assertWriteTargetSafe(repo, mdPath);
691
+ assertWriteTargetSafe(repo, htmlPath);
687
692
  mkdirSync(dirname(mdPath), { recursive: true });
688
693
  writeFileSync(mdPath, md, "utf8");
689
694
  mkdirSync(htmlDir, { recursive: true });
@@ -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";
@@ -1,7 +1,8 @@
1
1
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
3
4
  import { checkGitClean } from "../migrate-preflight/index.js";
4
- import { agentsRefreshPlan } from "../platform/agents-md.js";
5
+ import { applyAgentsRefresh } from "../platform/agents-md.js";
5
6
  import { patchAgentsMdHeader, renderHeaderPatchSummary } from "./agents-header.js";
6
7
  import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, OBSOLETE_FRAMEWORK_NARRATIVE_FILENAME, VBRIEF_DEPRECATION_MARKER_BODY, VBRIEF_DEPRECATION_MARKER_FILENAME, } from "./constants.js";
7
8
  import { detectLegacyVbriefLayout, detectXbriefConvergence } from "./detect.js";
@@ -157,25 +158,33 @@ function renameOrReplace(src, dest) {
157
158
  rmSync(src, { recursive: true, force: true });
158
159
  }
159
160
  function runAgentsRefresh(projectRoot, frameworkRoot, io) {
160
- const plan = agentsRefreshPlan(projectRoot, { frameworkRoot });
161
- const state = String(plan.state ?? "unknown");
162
- if (state === "current") {
163
- io.writeOut("AGENTS.md managed section is current — no changes.\n");
161
+ try {
162
+ assertWriteTargetSafe(projectRoot, join(projectRoot, "AGENTS.md"));
163
+ const { state, wrote, writable } = applyAgentsRefresh(projectRoot, {}, { frameworkRoot });
164
+ if (state === "current") {
165
+ io.writeOut("AGENTS.md managed section is current — no changes.\n");
166
+ return 0;
167
+ }
168
+ if (state === "template-missing" || state === "template-malformed" || state === "unreadable") {
169
+ io.writeErr(`agents:refresh failed: ${state}\n`);
170
+ return 2;
171
+ }
172
+ if (!writable) {
173
+ io.writeErr("agents:refresh failed: plan produced no new_content\n");
174
+ return 2;
175
+ }
176
+ if (wrote) {
177
+ io.writeOut(`AGENTS.md updated (state=${state}).\n`);
178
+ }
164
179
  return 0;
165
180
  }
166
- if (state === "template-missing" || state === "template-malformed" || state === "unreadable") {
167
- io.writeErr(`agents:refresh failed: ${state}\n`);
168
- return 2;
169
- }
170
- const newContent = plan.new_content;
171
- if (typeof newContent !== "string") {
172
- io.writeErr("agents:refresh failed: plan produced no new_content\n");
173
- return 2;
181
+ catch (err) {
182
+ if (err instanceof ProjectionContainmentError) {
183
+ io.writeErr(`agents:refresh failed: ${err.message}\n`);
184
+ return 2;
185
+ }
186
+ throw err;
174
187
  }
175
- const path = String(plan.path ?? join(projectRoot, "AGENTS.md"));
176
- writeFileSync(path, newContent, "utf8");
177
- io.writeOut(`AGENTS.md updated (state=${state}).\n`);
178
- return 0;
179
188
  }
180
189
  /** Core orchestrator for the consumer xbrief rename (#2110) + convergence (#2270). */
181
190
  export function runXbriefMigration(args, _io) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -317,8 +317,8 @@
317
317
  "provenance": true
318
318
  },
319
319
  "dependencies": {
320
- "@deftai/directive-content": "^0.85.0",
321
- "@deftai/directive-types": "^0.85.0",
320
+ "@deftai/directive-content": "^0.86.0",
321
+ "@deftai/directive-types": "^0.86.0",
322
322
  "archiver": "^8.0.0"
323
323
  },
324
324
  "scripts": {