@deftai/directive-core 0.82.0 → 0.84.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 (64) hide show
  1. package/dist/cache/main.js +36 -2
  2. package/dist/cache/task-cache/constants.d.ts +4 -0
  3. package/dist/cache/task-cache/constants.js +4 -0
  4. package/dist/cache/task-cache/executor.d.ts +9 -0
  5. package/dist/cache/task-cache/executor.js +51 -0
  6. package/dist/cache/task-cache/hash.d.ts +17 -0
  7. package/dist/cache/task-cache/hash.js +92 -0
  8. package/dist/cache/task-cache/index.d.ts +14 -0
  9. package/dist/cache/task-cache/index.js +15 -0
  10. package/dist/cache/task-cache/lint.d.ts +4 -0
  11. package/dist/cache/task-cache/lint.js +55 -0
  12. package/dist/cache/task-cache/registry.d.ts +7 -0
  13. package/dist/cache/task-cache/registry.js +67 -0
  14. package/dist/cache/task-cache/store.d.ts +10 -0
  15. package/dist/cache/task-cache/store.js +48 -0
  16. package/dist/cache/task-cache/types.d.ts +48 -0
  17. package/dist/cache/task-cache/types.js +3 -0
  18. package/dist/check/cached-orchestrator.d.ts +16 -0
  19. package/dist/check/cached-orchestrator.js +76 -0
  20. package/dist/check/context.d.ts +30 -0
  21. package/dist/check/context.js +28 -0
  22. package/dist/check/gate-lists.d.ts +18 -0
  23. package/dist/check/gate-lists.js +68 -0
  24. package/dist/check/index.d.ts +4 -1
  25. package/dist/check/index.js +3 -0
  26. package/dist/check/orchestrator.d.ts +3 -45
  27. package/dist/check/orchestrator.js +8 -46
  28. package/dist/check/runner-detect.d.ts +20 -0
  29. package/dist/check/runner-detect.js +131 -0
  30. package/dist/eval/readback.js +6 -1
  31. package/dist/hooks/cursor-hooks.d.ts +23 -0
  32. package/dist/hooks/cursor-hooks.js +95 -0
  33. package/dist/hooks/dispatcher.d.ts +10 -1
  34. package/dist/hooks/dispatcher.js +66 -6
  35. package/dist/hooks/index.d.ts +1 -0
  36. package/dist/hooks/index.js +1 -0
  37. package/dist/init-deposit/agent-hooks.d.ts +4 -2
  38. package/dist/init-deposit/agent-hooks.js +137 -14
  39. package/dist/init-deposit/gitignore.js +1 -0
  40. package/dist/init-deposit/hygiene.js +1 -0
  41. package/dist/lifecycle/events.d.ts +1 -0
  42. package/dist/lifecycle/events.js +59 -9
  43. package/dist/policy/host-hooks.d.ts +21 -0
  44. package/dist/policy/host-hooks.js +96 -0
  45. package/dist/policy/index.d.ts +1 -0
  46. package/dist/policy/index.js +15 -1
  47. package/dist/product-signal/consent.d.ts +20 -3
  48. package/dist/product-signal/consent.js +63 -6
  49. package/dist/product-signal/gates.d.ts +1 -1
  50. package/dist/product-signal/submit.d.ts +5 -1
  51. package/dist/product-signal/submit.js +42 -18
  52. package/dist/scope/decompose.js +9 -3
  53. package/dist/session/git.d.ts +2 -0
  54. package/dist/session/git.js +14 -0
  55. package/dist/session/verify-session-ritual.js +39 -5
  56. package/dist/swarm/routing-set-cli.js +16 -5
  57. package/dist/swarm/routing.d.ts +1 -1
  58. package/dist/swarm/routing.js +3 -1
  59. package/dist/value/readback.js +6 -1
  60. package/dist/vbrief-validate/plan-hooks.d.ts +2 -0
  61. package/dist/vbrief-validate/plan-hooks.js +25 -0
  62. package/dist/verify-env/agent-hooks.d.ts +2 -1
  63. package/dist/verify-env/agent-hooks.js +3 -2
  64. package/package.json +7 -3
@@ -1,3 +1,4 @@
1
+ export * from "./cursor-hooks.js";
1
2
  export * from "./dispatcher.js";
2
3
  export * from "./scope.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,4 @@
1
+ export * from "./cursor-hooks.js";
1
2
  export * from "./dispatcher.js";
2
3
  export * from "./scope.js";
3
4
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,7 @@
1
1
  import type { HookHost } from "../hooks/dispatcher.js";
2
+ import { type HostHooksPolicy } from "../policy/host-hooks.js";
2
3
  import type { InitDepositIo } from "./constants.js";
4
+ export { CURSOR_GENERIC_WRITE_HOOK_MATCHER, DEFT_CURSOR_ADAPTER_COMMAND_MARKER, } from "../hooks/cursor-hooks.js";
3
5
  export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
4
6
  export declare const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
5
7
  export declare const AGENT_HOOK_PATHS: readonly [".claude/settings.json", ".grok/hooks/deft.json", ".cursor/hooks.json", ".codex/hooks.json"];
@@ -19,7 +21,7 @@ export interface AgentHookDepositResult {
19
21
  readonly changedPaths: AgentHookPath[];
20
22
  }
21
23
  /** Merge Directive-owned project hook entries without replacing user configuration. */
22
- export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo): AgentHookDepositResult;
24
+ export declare function writeAgentHookDeposit(projectRoot: string, io?: InitDepositIo, hostHooksPolicy?: HostHooksPolicy): AgentHookDepositResult;
23
25
  /** Read-only registration probe shared by verify and doctor. */
24
- export declare function inspectAgentHookDeposit(projectRoot: string): AgentHookInspection[];
26
+ export declare function inspectAgentHookDeposit(projectRoot: string, hostHooksPolicy?: HostHooksPolicy): AgentHookInspection[];
25
27
  //# sourceMappingURL=agent-hooks.d.ts.map
@@ -1,7 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
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";
4
5
  import { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
6
+ 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";
5
8
  export { DIRECT_WRITE_HOOK_MATCHER, SPAWN_HOOK_MATCHER } from "../hooks/tools.js";
6
9
  export const DEFT_HOOK_COMMAND_MARKER = "deft hook:dispatch";
7
10
  export const AGENT_HOOK_PATHS = [
@@ -63,9 +66,18 @@ function nestedCommands(value) {
63
66
  function isManagedNestedGroup(value) {
64
67
  return nestedCommands(value).some((value) => value.includes(DEFT_HOOK_COMMAND_MARKER));
65
68
  }
69
+ function isManagedNestedGroupForHost(value, host) {
70
+ return nestedCommands(value).some((command) => command.includes(DEFT_HOOK_COMMAND_MARKER) && command.includes(`--host ${host}`));
71
+ }
66
72
  function isManagedCursorEntry(value) {
67
73
  const entry = object(value);
68
- return typeof entry?.command === "string" && entry.command.includes(DEFT_HOOK_COMMAND_MARKER);
74
+ if (typeof entry?.command !== "string")
75
+ return false;
76
+ return (entry.command.includes(DEFT_HOOK_COMMAND_MARKER) ||
77
+ entry.command.includes(DEFT_CURSOR_ADAPTER_COMMAND_MARKER));
78
+ }
79
+ function isManagedCursorEntryForHost(value) {
80
+ return isManagedCursorEntry(value);
69
81
  }
70
82
  function nestedGroup(host, event, matcher) {
71
83
  return {
@@ -97,7 +109,40 @@ function mergeNestedConfig(config, path, host, options = {}) {
97
109
  }
98
110
  return { ...config, hooks };
99
111
  }
112
+ function stripManagedNestedConfig(config, path, host) {
113
+ const hooks = hooksObject(config, path);
114
+ const nextHooks = {};
115
+ for (const key of ["SessionStart", "PreToolUse", "PreCompact", "PostCompact"]) {
116
+ if (!(key in hooks))
117
+ continue;
118
+ const filtered = eventArray(hooks, key, path).filter((entry) => !isManagedNestedGroupForHost(entry, host));
119
+ if (filtered.length > 0)
120
+ nextHooks[key] = filtered;
121
+ }
122
+ if (Object.keys(nextHooks).length === 0) {
123
+ const { hooks: _hooks, ...rest } = config;
124
+ return rest;
125
+ }
126
+ return { ...config, hooks: nextHooks };
127
+ }
128
+ function stripManagedCursorConfig(config, path) {
129
+ const hooks = hooksObject(config, path);
130
+ const nextHooks = {};
131
+ for (const key of ["sessionStart", "preToolUse", "preCompact"]) {
132
+ if (!(key in hooks))
133
+ continue;
134
+ const filtered = eventArray(hooks, key, path).filter((entry) => !isManagedCursorEntryForHost(entry));
135
+ if (filtered.length > 0)
136
+ nextHooks[key] = filtered;
137
+ }
138
+ if (Object.keys(nextHooks).length === 0) {
139
+ const { hooks: _hooks, version: _version, ...rest } = config;
140
+ return rest;
141
+ }
142
+ return { ...config, version: 1, hooks: nextHooks };
143
+ }
100
144
  function mergeCursorConfig(config, path) {
145
+ assertCursorApplyPatchMatchersDisjoint();
101
146
  const hooks = hooksObject(config, path);
102
147
  const session = eventArray(hooks, "sessionStart", path).filter((entry) => !isManagedCursorEntry(entry));
103
148
  const preTool = eventArray(hooks, "preToolUse", path).filter((entry) => !isManagedCursorEntry(entry));
@@ -105,9 +150,10 @@ function mergeCursorConfig(config, path) {
105
150
  hooks.sessionStart = [...session, { command: command("cursor", "session.start"), timeout: 5 }];
106
151
  hooks.preToolUse = [
107
152
  ...preTool,
153
+ cursorApplyPatchAdapterEntry(),
108
154
  {
109
155
  command: command("cursor", "tool.before"),
110
- matcher: DIRECT_WRITE_HOOK_MATCHER,
156
+ matcher: CURSOR_GENERIC_WRITE_HOOK_MATCHER,
111
157
  failClosed: true,
112
158
  timeout: 5,
113
159
  },
@@ -121,6 +167,15 @@ function mergeCursorConfig(config, path) {
121
167
  hooks.preCompact = [...preCompact, { command: command("cursor", "session.compact"), timeout: 5 }];
122
168
  return { ...config, version: 1, hooks };
123
169
  }
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
+ }
124
179
  function writeJsonIfChanged(path, payload) {
125
180
  const next = `${JSON.stringify(payload, null, 2)}\n`;
126
181
  if (existsSync(path) && readFileSync(path, "utf8") === next)
@@ -132,41 +187,93 @@ function writeJsonIfChanged(path, payload) {
132
187
  return true;
133
188
  }
134
189
  /** Merge Directive-owned project hook entries without replacing user configuration. */
135
- export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefined }) {
190
+ export function writeAgentHookDeposit(projectRoot, io = { printf: () => undefined }, hostHooksPolicy = loadHostHooksPolicyFromProject(projectRoot)) {
136
191
  const changedPaths = [];
192
+ const strippedPaths = [];
137
193
  const definitions = [
138
194
  {
195
+ host: "claude",
139
196
  path: AGENT_HOOK_PATHS[0],
140
197
  merge: (config, path) => mergeNestedConfig(config, path, "claude", { compact: true }),
198
+ strip: (config, path) => stripManagedNestedConfig(config, path, "claude"),
141
199
  },
142
200
  {
201
+ host: "grok",
143
202
  path: AGENT_HOOK_PATHS[1],
144
203
  merge: (config, path) => mergeNestedConfig(config, path, "grok", { compact: true }),
204
+ strip: (config, path) => stripManagedNestedConfig(config, path, "grok"),
205
+ },
206
+ {
207
+ host: "cursor",
208
+ path: AGENT_HOOK_PATHS[2],
209
+ merge: mergeCursorConfig,
210
+ strip: stripManagedCursorConfig,
145
211
  },
146
- { path: AGENT_HOOK_PATHS[2], merge: mergeCursorConfig },
147
212
  {
213
+ host: "codex",
148
214
  path: AGENT_HOOK_PATHS[3],
149
215
  merge: (config, path) => mergeNestedConfig(config, path, "codex", { compact: false }),
216
+ strip: (config, path) => stripManagedNestedConfig(config, path, "codex"),
150
217
  },
151
218
  ];
152
219
  const prepared = definitions.map((definition) => {
153
220
  const absolute = join(projectRoot, definition.path);
154
221
  assertDepositContained(projectRoot, absolute);
155
- const merged = definition.merge(readConfig(absolute), absolute);
156
- return { ...definition, absolute, merged };
222
+ if (!isHostHookDepositEnabled(definition.host, hostHooksPolicy)) {
223
+ if (!existsSync(absolute))
224
+ return { mode: "skip" };
225
+ return {
226
+ mode: "strip",
227
+ absolute,
228
+ path: definition.path,
229
+ payload: definition.strip(readConfig(absolute), absolute),
230
+ };
231
+ }
232
+ return {
233
+ mode: "merge",
234
+ absolute,
235
+ path: definition.path,
236
+ payload: definition.merge(readConfig(absolute), absolute),
237
+ };
157
238
  });
158
- for (const definition of prepared) {
159
- if (writeJsonIfChanged(definition.absolute, definition.merged)) {
160
- changedPaths.push(definition.path);
239
+ for (const item of prepared) {
240
+ if (item.mode === "skip")
241
+ continue;
242
+ if (writeJsonIfChanged(item.absolute, item.payload)) {
243
+ if (item.mode === "strip")
244
+ strippedPaths.push(item.path);
245
+ else
246
+ changedPaths.push(item.path);
247
+ }
248
+ }
249
+ const adapterAbsolute = join(projectRoot, CURSOR_APPLY_PATCH_ADAPTER_RELATIVE);
250
+ assertDepositContained(projectRoot, adapterAbsolute);
251
+ if (isHostHookDepositEnabled("cursor", hostHooksPolicy)) {
252
+ if (writeTextIfChanged(adapterAbsolute, CURSOR_APPLY_PATCH_ADAPTER_SOURCE)) {
253
+ if (!changedPaths.includes(AGENT_HOOK_PATHS[2])) {
254
+ changedPaths.push(AGENT_HOOK_PATHS[2]);
255
+ }
256
+ }
257
+ }
258
+ else if (existsSync(adapterAbsolute)) {
259
+ rmSync(adapterAbsolute, { force: true });
260
+ if (!strippedPaths.includes(AGENT_HOOK_PATHS[2])) {
261
+ strippedPaths.push(AGENT_HOOK_PATHS[2]);
161
262
  }
162
263
  }
163
264
  if (changedPaths.length > 0) {
164
265
  io.printf(`Installed Directive agent hooks: ${changedPaths.join(", ")}\n`);
165
266
  }
166
- else {
267
+ if (strippedPaths.length > 0) {
268
+ io.printf(`Removed Directive-managed agent hooks (plan.policy.hostHooks opt-out): ${strippedPaths.join(", ")}\n`);
269
+ }
270
+ if (changedPaths.length === 0 && strippedPaths.length === 0) {
167
271
  io.printf("Directive agent hooks already current.\n");
168
272
  }
169
- return { changed: changedPaths.length > 0, changedPaths };
273
+ return {
274
+ changed: changedPaths.length + strippedPaths.length > 0,
275
+ changedPaths: [...changedPaths, ...strippedPaths],
276
+ };
170
277
  }
171
278
  function hasNestedRegistration(config, host, options = {}) {
172
279
  const hooks = object(config.hooks);
@@ -202,11 +309,18 @@ function hasCursorRegistration(config) {
202
309
  const session = Array.isArray(hooks.sessionStart) ? hooks.sessionStart : [];
203
310
  const preTool = Array.isArray(hooks.preToolUse) ? hooks.preToolUse : [];
204
311
  const preCompact = Array.isArray(hooks.preCompact) ? hooks.preCompact : [];
312
+ const adapter = cursorApplyPatchAdapterEntry();
205
313
  return (session.some((entry) => object(entry)?.command === command("cursor", "session.start")) &&
206
314
  preTool.some((entry) => {
207
315
  const hook = object(entry);
208
316
  return (hook?.command === command("cursor", "tool.before") &&
209
- hook.matcher === DIRECT_WRITE_HOOK_MATCHER &&
317
+ hook.matcher === CURSOR_GENERIC_WRITE_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 &&
210
324
  hook.failClosed === true);
211
325
  }) &&
212
326
  preTool.some((entry) => {
@@ -218,7 +332,7 @@ function hasCursorRegistration(config) {
218
332
  preCompact.some((entry) => object(entry)?.command === command("cursor", "session.compact")));
219
333
  }
220
334
  /** Read-only registration probe shared by verify and doctor. */
221
- export function inspectAgentHookDeposit(projectRoot) {
335
+ export function inspectAgentHookDeposit(projectRoot, hostHooksPolicy = loadHostHooksPolicyFromProject(projectRoot)) {
222
336
  const definitions = [
223
337
  {
224
338
  host: "claude",
@@ -250,6 +364,15 @@ export function inspectAgentHookDeposit(projectRoot) {
250
364
  const compactNote = definition.compactSupport === "unsupported"
251
365
  ? " Compact re-arm is not deposited for Codex (no native compact hook surface)."
252
366
  : " PreCompact/PostCompact or preCompact compact re-arm is deposited.";
367
+ if (!isHostHookDepositEnabled(definition.host, hostHooksPolicy)) {
368
+ return {
369
+ host: definition.host,
370
+ path: definition.path,
371
+ status: "healthy",
372
+ compactSupport: definition.compactSupport,
373
+ detail: `plan.policy.hostHooks.${definition.host} is false — Directive hook deposit is skipped for this host.`,
374
+ };
375
+ }
253
376
  if (!existsSync(absolute)) {
254
377
  return {
255
378
  host: definition.host,
@@ -22,6 +22,7 @@ const DEFT_CORE_COVERING_LINES = new Set([".deft/core/", ".deft/core"]);
22
22
  */
23
23
  export const CANONICAL_GITIGNORE_BASELINE = [
24
24
  ".deft-cache/",
25
+ ".deft/cache/",
25
26
  ".deft/.cli/",
26
27
  ".deft/ritual-state.json",
27
28
  ".deft/last-session.json",
@@ -24,6 +24,7 @@ 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" },
27
28
  { exact: ".codex/hooks.json" },
28
29
  { exact: ".gitattributes" },
29
30
  { exact: ".gitignore" },
@@ -32,6 +32,7 @@ export interface BehavioralEventRecord {
32
32
  export declare function emit(name: string, payload: Record<string, unknown>, options?: {
33
33
  logPath?: string | null;
34
34
  detectedAt?: string | null;
35
+ projectRoot?: string | null;
35
36
  }): BehavioralEventRecord;
36
37
  /** Return all events from the log in emission order. */
37
38
  export declare function readEvents(logPath?: string | null): BehavioralEventRecord[];
@@ -1,9 +1,10 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, writeSync, } from "node:fs";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { contentRoot } from "../content-root.js";
6
6
  import { ATTRIBUTION_REQUIRED_PAYLOAD } from "../events/attribution-constants.js";
7
+ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
7
8
  /** Default event log location (project-local). */
8
9
  export const DEFAULT_EVENT_LOG = join(".deft-cache", "events.jsonl");
9
10
  const BEHAVIORAL_CATEGORY = "behavioral";
@@ -125,15 +126,62 @@ function sortKeysDeep(value) {
125
126
  function jsonStringifySorted(value) {
126
127
  return JSON.stringify(sortKeysDeep(value));
127
128
  }
128
- function resolveLogPath(logPath) {
129
+ function resolveLogPath(logPath, projectRoot) {
129
130
  if (logPath !== undefined && logPath !== null) {
130
- return resolve(logPath);
131
+ return isAbsolute(logPath) ? resolve(logPath) : resolve(projectRoot, logPath);
131
132
  }
132
133
  const envPath = process.env.DEFT_EVENT_LOG;
133
134
  if (envPath !== undefined && envPath.length > 0) {
134
- return resolve(envPath);
135
+ return isAbsolute(envPath) ? resolve(envPath) : resolve(projectRoot, envPath);
136
+ }
137
+ return resolve(projectRoot, DEFAULT_EVENT_LOG);
138
+ }
139
+ function isNestedUnder(parent, child) {
140
+ const rel = relative(resolve(parent), resolve(child));
141
+ return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
142
+ }
143
+ function assertPathComponentsNotSymlinks(projectDir, targetPath) {
144
+ const targetAbs = resolve(targetPath);
145
+ const projectAbs = resolve(projectDir);
146
+ let current = targetAbs;
147
+ const chain = [];
148
+ while (true) {
149
+ chain.unshift(current);
150
+ const parent = dirname(current);
151
+ if (parent === current) {
152
+ break;
153
+ }
154
+ current = parent;
155
+ }
156
+ for (const segment of chain) {
157
+ let info;
158
+ try {
159
+ info = lstatSync(segment);
160
+ }
161
+ catch {
162
+ continue;
163
+ }
164
+ if (info.isSymbolicLink()) {
165
+ throw new ProjectionContainmentError(`projection write refused: ${segment} is a symlink on the write path`, { projectDir: projectAbs, targetPath: targetAbs, offendingPath: segment });
166
+ }
167
+ }
168
+ }
169
+ /** Refuse symlink write targets for project-owned and explicit event logs (#2766). */
170
+ function assertEventLogTargetSafe(projectRoot, targetPath) {
171
+ if (isNestedUnder(projectRoot, targetPath)) {
172
+ assertWriteTargetSafe(projectRoot, targetPath);
173
+ return;
174
+ }
175
+ assertPathComponentsNotSymlinks(projectRoot, targetPath);
176
+ }
177
+ function appendLineNoFollow(target, line) {
178
+ const fd = openSync(target, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW, 0o644);
179
+ try {
180
+ writeSync(fd, Buffer.from(line, "utf8"));
181
+ }
182
+ finally {
183
+ closeSync(fd);
135
184
  }
136
- return resolve(DEFAULT_EVENT_LOG);
137
185
  }
138
186
  function newEventId() {
139
187
  const wallNs = BigInt(Date.now()) * 1000000n;
@@ -160,14 +208,16 @@ export function emit(name, payload, options = {}) {
160
208
  detected_at: options.detectedAt ?? `${iso.slice(0, 19)}Z`,
161
209
  payload: { ...payload },
162
210
  };
163
- const target = resolveLogPath(options.logPath);
211
+ const projectRoot = resolve(options.projectRoot ?? process.cwd());
212
+ const target = resolveLogPath(options.logPath, projectRoot);
213
+ assertEventLogTargetSafe(projectRoot, target);
164
214
  mkdirSync(dirname(target), { recursive: true });
165
- appendFileSync(target, `${jsonStringifySorted(record)}\n`, "utf8");
215
+ appendLineNoFollow(target, `${jsonStringifySorted(record)}\n`);
166
216
  return record;
167
217
  }
168
218
  /** Return all events from the log in emission order. */
169
219
  export function readEvents(logPath) {
170
- const target = resolveLogPath(logPath);
220
+ const target = resolveLogPath(logPath, process.cwd());
171
221
  if (!existsSync(target)) {
172
222
  return [];
173
223
  }
@@ -0,0 +1,21 @@
1
+ import type { HookHost } from "../hooks/dispatcher.js";
2
+ export declare const FIELD_HOST_HOOKS = "plan.policy.hostHooks";
3
+ export declare const FIELD_HOST_HOOKS_CLI_ALIAS = "hostHooks";
4
+ /** Per-host Directive hook deposit toggles (#2752). */
5
+ export type HostHooksPolicy = Record<HookHost, boolean>;
6
+ export declare const DEFAULT_HOST_HOOKS_POLICY: HostHooksPolicy;
7
+ export interface HostHooksPolicyField {
8
+ readonly name: string;
9
+ readonly current: HostHooksPolicy;
10
+ readonly default: HostHooksPolicy;
11
+ readonly source: string;
12
+ }
13
+ /** Resolve typed host hook deposit policy from raw PROJECT-DEFINITION value. */
14
+ export declare function resolveHostHooksPolicy(raw: unknown): HostHooksPolicy;
15
+ export declare function validateHostHooks(value: unknown): string[];
16
+ export declare function isHostHookDepositEnabled(host: HookHost, policy?: HostHooksPolicy): boolean;
17
+ /** Inspector row for `policy:show --field=hostHooks`. */
18
+ export declare function inspectHostHooks(data: Record<string, unknown> | null): HostHooksPolicyField;
19
+ /** Resolve host hook deposit policy from PROJECT-DEFINITION on disk. */
20
+ export declare function loadHostHooksPolicyFromProject(projectRoot: string): HostHooksPolicy;
21
+ //# sourceMappingURL=host-hooks.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { HOOK_HOSTS } from "../hooks/dispatcher.js";
2
+ import { readPlanPolicy } from "./plan-extensions.js";
3
+ import { loadProjectDefinition } from "./resolve.js";
4
+ export const FIELD_HOST_HOOKS = "plan.policy.hostHooks";
5
+ export const FIELD_HOST_HOOKS_CLI_ALIAS = "hostHooks";
6
+ export const DEFAULT_HOST_HOOKS_POLICY = {
7
+ claude: true,
8
+ cursor: true,
9
+ grok: true,
10
+ codex: true,
11
+ };
12
+ function readHostBoolean(rec, host, fallback) {
13
+ if (host in rec && typeof rec[host] === "boolean") {
14
+ return rec[host];
15
+ }
16
+ return fallback;
17
+ }
18
+ /** Resolve typed host hook deposit policy from raw PROJECT-DEFINITION value. */
19
+ export function resolveHostHooksPolicy(raw) {
20
+ if (raw === null || raw === undefined) {
21
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
22
+ }
23
+ if (typeof raw !== "object" || Array.isArray(raw)) {
24
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
25
+ }
26
+ const rec = raw;
27
+ return {
28
+ claude: readHostBoolean(rec, "claude", DEFAULT_HOST_HOOKS_POLICY.claude),
29
+ cursor: readHostBoolean(rec, "cursor", DEFAULT_HOST_HOOKS_POLICY.cursor),
30
+ grok: readHostBoolean(rec, "grok", DEFAULT_HOST_HOOKS_POLICY.grok),
31
+ codex: readHostBoolean(rec, "codex", DEFAULT_HOST_HOOKS_POLICY.codex),
32
+ };
33
+ }
34
+ export function validateHostHooks(value) {
35
+ if (value === null || value === undefined) {
36
+ return [];
37
+ }
38
+ if (typeof value !== "object" || Array.isArray(value)) {
39
+ return [`${FIELD_HOST_HOOKS} must be an object; got ${typeof value}`];
40
+ }
41
+ const rec = value;
42
+ const errors = [];
43
+ for (const host of HOOK_HOSTS) {
44
+ if (host in rec && typeof rec[host] !== "boolean") {
45
+ errors.push(`${FIELD_HOST_HOOKS}.${host} must be a boolean`);
46
+ }
47
+ }
48
+ for (const key of Object.keys(rec)) {
49
+ if (!HOOK_HOSTS.includes(key)) {
50
+ errors.push(`${FIELD_HOST_HOOKS}.${key} is not a deposited host (${HOOK_HOSTS.join(", ")})`);
51
+ }
52
+ }
53
+ return errors;
54
+ }
55
+ export function isHostHookDepositEnabled(host, policy = DEFAULT_HOST_HOOKS_POLICY) {
56
+ return policy[host];
57
+ }
58
+ function fieldFromResolved(resolved, source) {
59
+ return {
60
+ name: FIELD_HOST_HOOKS,
61
+ current: resolved,
62
+ default: DEFAULT_HOST_HOOKS_POLICY,
63
+ source,
64
+ };
65
+ }
66
+ /** Inspector row for `policy:show --field=hostHooks`. */
67
+ export function inspectHostHooks(data) {
68
+ if (data === null) {
69
+ return fieldFromResolved(DEFAULT_HOST_HOOKS_POLICY, "default");
70
+ }
71
+ const policyBlock = readPlanPolicy(data.plan);
72
+ if (typeof policyBlock !== "object" ||
73
+ policyBlock === null ||
74
+ Array.isArray(policyBlock) ||
75
+ !("hostHooks" in policyBlock)) {
76
+ return fieldFromResolved(DEFAULT_HOST_HOOKS_POLICY, "default");
77
+ }
78
+ const resolved = resolveHostHooksPolicy(policyBlock.hostHooks);
79
+ return fieldFromResolved(resolved, "typed");
80
+ }
81
+ /** Resolve host hook deposit policy from PROJECT-DEFINITION on disk. */
82
+ export function loadHostHooksPolicyFromProject(projectRoot) {
83
+ const [data] = loadProjectDefinition(projectRoot);
84
+ if (data === null) {
85
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
86
+ }
87
+ const policyBlock = readPlanPolicy(data.plan);
88
+ if (typeof policyBlock !== "object" ||
89
+ policyBlock === null ||
90
+ Array.isArray(policyBlock) ||
91
+ !("hostHooks" in policyBlock)) {
92
+ return { ...DEFAULT_HOST_HOOKS_POLICY };
93
+ }
94
+ return resolveHostHooksPolicy(policyBlock.hostHooks);
95
+ }
96
+ //# sourceMappingURL=host-hooks.js.map
@@ -3,6 +3,7 @@ export * from "./autonomy.js";
3
3
  export * from "./capacity.js";
4
4
  export * from "./decisions.js";
5
5
  export * from "./disclosure.js";
6
+ export * from "./host-hooks.js";
6
7
  export * from "./plan-extensions.js";
7
8
  export * from "./policy-invocation.js";
8
9
  export * from "./product-signal.js";
@@ -1,3 +1,4 @@
1
+ import { FIELD_HOST_HOOKS, FIELD_HOST_HOOKS_CLI_ALIAS, inspectHostHooks } from "./host-hooks.js";
1
2
  import { readPlanPolicy } from "./plan-extensions.js";
2
3
  import { FIELD_PRODUCT_SIGNAL, FIELD_PRODUCT_SIGNAL_CLI_ALIAS, inspectProductSignal, } from "./product-signal.js";
3
4
  import { coerceLegacyNarrative, LEGACY_NARRATIVE_KEY, loadProjectDefinition } from "./resolve.js";
@@ -10,6 +11,7 @@ export * from "./autonomy.js";
10
11
  export * from "./capacity.js";
11
12
  export * from "./decisions.js";
12
13
  export * from "./disclosure.js";
14
+ export * from "./host-hooks.js";
13
15
  export * from "./plan-extensions.js";
14
16
  export * from "./policy-invocation.js";
15
17
  export * from "./product-signal.js";
@@ -258,6 +260,15 @@ function inspectRuntimeAuthorityField(data) {
258
260
  source: field.source,
259
261
  };
260
262
  }
263
+ function inspectHostHooksField(data) {
264
+ const field = inspectHostHooks(data);
265
+ return {
266
+ name: field.name,
267
+ current: field.current,
268
+ default: field.default,
269
+ source: field.source,
270
+ };
271
+ }
261
272
  const REGISTERED_POLICIES = [
262
273
  inspectAllowDirectCommits,
263
274
  inspectWipCap,
@@ -270,6 +281,7 @@ const REGISTERED_POLICIES = [
270
281
  emptyIsTyped: true,
271
282
  }),
272
283
  inspectSwarmSubagentBackend,
284
+ inspectHostHooksField,
273
285
  inspectStalenessTicklerField,
274
286
  inspectRuntimeAuthorityField,
275
287
  inspectProductSignalField,
@@ -290,7 +302,9 @@ export function inspectOnePolicy(name, projectRoot) {
290
302
  ? FIELD_STALENESS_TICKLER
291
303
  : name === FIELD_RUNTIME_AUTHORITY_CLI_ALIAS
292
304
  ? FIELD_RUNTIME_AUTHORITY
293
- : name;
305
+ : name === FIELD_HOST_HOOKS_CLI_ALIAS
306
+ ? FIELD_HOST_HOOKS
307
+ : name;
294
308
  for (const field of inspectAllPolicies(projectRoot)) {
295
309
  if (field.name === normalized)
296
310
  return field;
@@ -1,12 +1,14 @@
1
1
  export declare const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
2
- /** Consent record schema version (#2693 D2). */
3
- export declare const PRODUCT_SIGNAL_CONSENT_VERSION = 1;
2
+ /** Consent record schema version (#2693 D2, #2767 v2 sink binding). */
3
+ export declare const PRODUCT_SIGNAL_CONSENT_VERSION = 2;
4
4
  /** Phase-1 consent tier permitting qualitative outbound (#2693 D2). */
5
5
  export declare const PRODUCT_SIGNAL_CONSENT_TIER = "product-signal";
6
6
  export interface ProductSignalConsentRecord {
7
7
  readonly consentVersion: number;
8
8
  readonly grantedAt: string;
9
9
  readonly tier: string;
10
+ /** Normalized owner/repo sink authorized by v2 consent (#2767). */
11
+ readonly sinkRepo?: string;
10
12
  readonly revokedAt?: string;
11
13
  }
12
14
  export interface ResolveConsentPathOptions {
@@ -14,16 +16,31 @@ export interface ResolveConsentPathOptions {
14
16
  readonly env?: NodeJS.ProcessEnv;
15
17
  readonly homeDir?: string;
16
18
  }
19
+ export interface SinkAuthorizationResult {
20
+ readonly authorized: boolean;
21
+ readonly configuredSink: string;
22
+ readonly consentedSink: string | null;
23
+ readonly sinksMatch: boolean;
24
+ readonly message: string;
25
+ }
17
26
  /** Platform-config consent path adjacent to USER.md (#2693 D2). */
18
27
  export declare function resolveProductSignalConsentPath(options?: ResolveConsentPathOptions): string;
28
+ /** Normalize sinkRepo to lowercase owner/repo (#2767). */
29
+ export declare function normalizeProductSignalSinkRepo(raw: string): string;
30
+ /** Resolve the sink authorized by a consent record (#2767). */
31
+ export declare function resolveConsentedProductSignalSink(consent: ProductSignalConsentRecord | null): string | null;
32
+ /** Authorize configured sink against install consent (#2767). */
33
+ export declare function authorizeProductSignalSink(configuredSink: string, consent: ProductSignalConsentRecord | null): SinkAuthorizationResult;
19
34
  /** Read consent file; returns null when absent, invalid, or revoked. */
20
35
  export declare function readProductSignalConsent(options?: ResolveConsentPathOptions): ProductSignalConsentRecord | null;
21
36
  /** True when a non-revoked consent grant exists. */
22
37
  export declare function isProductSignalConsented(options?: ResolveConsentPathOptions): boolean;
23
38
  export interface WriteConsentOptions extends ResolveConsentPathOptions {
24
39
  readonly now?: Date;
40
+ /** Normalized sink to bind into v2 consent (#2767). Defaults to baked-in sink. */
41
+ readonly sinkRepo?: string;
25
42
  }
26
- /** Write a fresh consent grant (#2693 D17 yes path). */
43
+ /** Write a fresh consent grant (#2693 D17 yes path, #2767 v2 sink binding). */
27
44
  export declare function grantProductSignalConsent(options?: WriteConsentOptions): ProductSignalConsentRecord;
28
45
  /** Revoke consent by setting revokedAt (#2693 D2). */
29
46
  export declare function revokeProductSignalConsent(options?: WriteConsentOptions): boolean;