@deftai/directive-core 0.81.0 → 0.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content-contracts/skills/helpers.d.ts +6 -0
- package/dist/content-contracts/skills/helpers.js +47 -3
- package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
- package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
- package/dist/eval/crud-telemetry.d.ts +2 -0
- package/dist/eval/crud-telemetry.js +30 -0
- package/dist/hooks/cursor-hooks.d.ts +23 -0
- package/dist/hooks/cursor-hooks.js +95 -0
- package/dist/hooks/dispatcher.d.ts +8 -1
- package/dist/hooks/dispatcher.js +23 -5
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -2
- package/dist/init-deposit/agent-hooks.js +137 -14
- package/dist/init-deposit/hygiene.js +1 -0
- package/dist/intake/github-body-cli.js +2 -0
- package/dist/intake/github-body.d.ts +11 -0
- package/dist/intake/github-body.js +95 -24
- package/dist/integration-e2e/helpers.js +1 -1
- package/dist/issue-sync/sync-from-xbrief.js +3 -0
- package/dist/lifecycle/events.d.ts +1 -0
- package/dist/lifecycle/events.js +59 -9
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +275 -0
- package/dist/orphan-active/index.d.ts +3 -0
- package/dist/orphan-active/index.js +3 -0
- package/dist/orphan-active/refs.d.ts +16 -0
- package/dist/orphan-active/refs.js +105 -0
- package/dist/policy/host-hooks.d.ts +21 -0
- package/dist/policy/host-hooks.js +96 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +18 -0
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +44 -73
- package/dist/pr-wait-mergeable/result.d.ts +2 -1
- package/dist/pr-wait-mergeable/result.js +4 -0
- package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
- package/dist/pr-wait-mergeable/semantic-green.js +202 -0
- package/dist/pr-wait-mergeable/types.d.ts +1 -0
- package/dist/pr-watch/constants.d.ts +4 -4
- package/dist/pr-watch/constants.js +4 -4
- package/dist/pr-watch/watch.js +5 -3
- package/dist/product-signal/consent.d.ts +20 -3
- package/dist/product-signal/consent.js +63 -6
- package/dist/product-signal/gates.d.ts +1 -1
- package/dist/product-signal/submit.d.ts +5 -1
- package/dist/product-signal/submit.js +42 -18
- package/dist/release/native-steps.js +11 -4
- package/dist/render/framework-commands.js +6 -0
- package/dist/scope/brief-io.d.ts +26 -0
- package/dist/scope/brief-io.js +77 -0
- package/dist/scope/decompose.js +2 -2
- package/dist/scope/decomposed-refs.js +3 -3
- package/dist/scope/demote.js +2 -2
- package/dist/scope/project-definition-sync.d.ts +2 -2
- package/dist/scope/project-definition-sync.js +15 -3
- package/dist/scope/registry-artifact-sync.d.ts +6 -1
- package/dist/scope/registry-artifact-sync.js +31 -13
- package/dist/scope/transition.js +23 -15
- package/dist/scope/undo.js +2 -2
- package/dist/scope/vbrief-json.d.ts +1 -2
- package/dist/scope/vbrief-json.js +1 -4
- package/dist/staleness-tickler/state.js +19 -2
- package/dist/triage/queue/cache.js +5 -0
- package/dist/vbrief-validate/plan-hooks.d.ts +2 -0
- package/dist/vbrief-validate/plan-hooks.js +25 -0
- package/dist/verify-env/agent-hooks.d.ts +2 -1
- package/dist/verify-env/agent-hooks.js +3 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
- package/dist/verify-source/skill-external-fetch-gate.js +71 -0
- package/package.json +7 -3
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
156
|
-
|
|
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
|
|
159
|
-
if (
|
|
160
|
-
|
|
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
|
-
|
|
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 {
|
|
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 ===
|
|
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,
|
|
@@ -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" },
|
|
@@ -9,6 +9,8 @@ export type RunGhApiFn = (args: readonly string[], options?: {
|
|
|
9
9
|
/** Resolve the live GitHub CLI used for writes and mutation read-back. */
|
|
10
10
|
export declare function resolveLiveGh(): string;
|
|
11
11
|
export declare function readBody(bodyFile: string, stdinText?: string | null): string;
|
|
12
|
+
/** Fail closed when live read-back body is flattened or mojibaked vs intended payload (#2607). */
|
|
13
|
+
export declare function verifyBodyPostcondition(intended: string, live: string): void;
|
|
12
14
|
export declare function createIssue(repo: string, options: {
|
|
13
15
|
title: string;
|
|
14
16
|
body: string;
|
|
@@ -35,6 +37,14 @@ export declare function editPrBody(repo: string, pr: number, options: {
|
|
|
35
37
|
binary?: string | null;
|
|
36
38
|
runFn?: RunGhApiFn;
|
|
37
39
|
}): Record<string, unknown>;
|
|
40
|
+
export declare function fetchIssueBody(repo: string, issue: number, options?: {
|
|
41
|
+
binary?: string | null;
|
|
42
|
+
runFn?: RunGhApiFn;
|
|
43
|
+
}): string;
|
|
44
|
+
export declare function writeIssueBodyToFile(repo: string, issue: number, outFile: string, options?: {
|
|
45
|
+
binary?: string | null;
|
|
46
|
+
runFn?: RunGhApiFn;
|
|
47
|
+
}): string;
|
|
38
48
|
export interface GitHubBodyCliArgs {
|
|
39
49
|
command: string;
|
|
40
50
|
repo?: string;
|
|
@@ -43,6 +53,7 @@ export interface GitHubBodyCliArgs {
|
|
|
43
53
|
comment?: number;
|
|
44
54
|
pr?: number;
|
|
45
55
|
bodyFile?: string;
|
|
56
|
+
outFile?: string;
|
|
46
57
|
}
|
|
47
58
|
export declare function githubBodyMain(args: GitHubBodyCliArgs): number;
|
|
48
59
|
//# sourceMappingURL=github-body.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { defaultWhich } from "../scm/binary.js";
|
|
3
3
|
import { call } from "../scm/call.js";
|
|
4
4
|
export const DEFAULT_TIMEOUT_SECONDS = 60;
|
|
@@ -73,14 +73,62 @@ function requireIntField(obj, field) {
|
|
|
73
73
|
}
|
|
74
74
|
return value;
|
|
75
75
|
}
|
|
76
|
+
function requireStringField(obj, field) {
|
|
77
|
+
const value = obj[field];
|
|
78
|
+
if (typeof value !== "string") {
|
|
79
|
+
throw new GitHubBodyError(`response did not include string field ${JSON.stringify(field)}`);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function countNewlines(text) {
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (const ch of text) {
|
|
86
|
+
if (ch === "\n")
|
|
87
|
+
count += 1;
|
|
88
|
+
}
|
|
89
|
+
return count;
|
|
90
|
+
}
|
|
91
|
+
function normalizeNewlines(text) {
|
|
92
|
+
return text.replace(/\r\n/g, "\n");
|
|
93
|
+
}
|
|
94
|
+
/** Fail closed when live read-back body is flattened or mojibaked vs intended payload (#2607). */
|
|
95
|
+
export function verifyBodyPostcondition(intended, live) {
|
|
96
|
+
if (intended === live) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (normalizeNewlines(intended) === normalizeNewlines(live)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const diagnoses = [];
|
|
103
|
+
if (live.includes("\uFFFD") && !intended.includes("\uFFFD")) {
|
|
104
|
+
diagnoses.push("live body contains U+FFFD replacement character not present in intended payload");
|
|
105
|
+
}
|
|
106
|
+
const intendedNl = countNewlines(intended);
|
|
107
|
+
const liveNl = countNewlines(live);
|
|
108
|
+
if (intendedNl > 0 && liveNl < intendedNl) {
|
|
109
|
+
diagnoses.push(`newline count mismatch (intended ${intendedNl}, live ${liveNl})`);
|
|
110
|
+
}
|
|
111
|
+
if (intended.includes("\n") && intended.replace(/\n/g, " ") === live) {
|
|
112
|
+
diagnoses.push("live body looks like intended newlines were collapsed to spaces (PowerShell string[] join)");
|
|
113
|
+
}
|
|
114
|
+
if (diagnoses.length > 0) {
|
|
115
|
+
throw new GitHubBodyError(`body postcondition failed: ${diagnoses.join("; ")}`);
|
|
116
|
+
}
|
|
117
|
+
throw new GitHubBodyError(`body postcondition failed: re-fetched body does not match intended payload (length intended=${intended.length}, live=${live.length})`);
|
|
118
|
+
}
|
|
76
119
|
function mutateWithReadback(mutationEndpoint, method, payload, readbackEndpoint, options = {}) {
|
|
120
|
+
const intendedBody = typeof payload.body === "string" ? payload.body : undefined;
|
|
77
121
|
const mutation = runGhApiJson([mutationEndpoint, "--method", method, "--input", "-"], {
|
|
78
122
|
inputText: jsonInput(payload),
|
|
79
123
|
binary: options.binary,
|
|
80
124
|
runFn: options.runFn,
|
|
81
125
|
});
|
|
82
126
|
const endpoint = typeof readbackEndpoint === "function" ? readbackEndpoint(mutation) : readbackEndpoint;
|
|
83
|
-
|
|
127
|
+
const readback = runGhApiJson([endpoint], { binary: options.binary, runFn: options.runFn });
|
|
128
|
+
if (intendedBody !== undefined) {
|
|
129
|
+
verifyBodyPostcondition(intendedBody, requireStringField(readback, "body"));
|
|
130
|
+
}
|
|
131
|
+
return readback;
|
|
84
132
|
}
|
|
85
133
|
export function createIssue(repo, options) {
|
|
86
134
|
const [owner, name] = splitRepo(repo);
|
|
@@ -116,32 +164,55 @@ export function editPrBody(repo, pr, options) {
|
|
|
116
164
|
runFn: options.runFn,
|
|
117
165
|
});
|
|
118
166
|
}
|
|
167
|
+
export function fetchIssueBody(repo, issue, options = {}) {
|
|
168
|
+
const [owner, name] = splitRepo(repo);
|
|
169
|
+
const endpoint = `repos/${owner}/${name}/issues/${issue}`;
|
|
170
|
+
const response = runGhApiJson([endpoint], { binary: options.binary, runFn: options.runFn });
|
|
171
|
+
return requireStringField(response, "body");
|
|
172
|
+
}
|
|
173
|
+
export function writeIssueBodyToFile(repo, issue, outFile, options = {}) {
|
|
174
|
+
const body = fetchIssueBody(repo, issue, options);
|
|
175
|
+
writeFileSync(outFile, body, { encoding: "utf8" });
|
|
176
|
+
return body;
|
|
177
|
+
}
|
|
119
178
|
export function githubBodyMain(args) {
|
|
120
179
|
try {
|
|
121
|
-
const body = readBody(args.bodyFile ?? "-");
|
|
122
|
-
let result;
|
|
123
180
|
switch (args.command) {
|
|
124
|
-
case "issue-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
result
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
181
|
+
case "issue-fetch": {
|
|
182
|
+
if (args.repo === undefined || args.issue === undefined || args.outFile === undefined) {
|
|
183
|
+
process.stderr.write("error: issue-fetch requires --repo, --issue, and --out-file\n");
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
writeIssueBodyToFile(args.repo, args.issue, args.outFile);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
default: {
|
|
190
|
+
const body = readBody(args.bodyFile ?? "-");
|
|
191
|
+
let result;
|
|
192
|
+
switch (args.command) {
|
|
193
|
+
case "issue-create":
|
|
194
|
+
result = createIssue(args.repo, { title: args.title, body });
|
|
195
|
+
break;
|
|
196
|
+
case "issue-edit":
|
|
197
|
+
result = editIssueBody(args.repo, args.issue, { body });
|
|
198
|
+
break;
|
|
199
|
+
case "comment-create":
|
|
200
|
+
result = createIssueComment(args.repo, args.issue, { body });
|
|
201
|
+
break;
|
|
202
|
+
case "comment-edit":
|
|
203
|
+
result = editIssueCommentBody(args.repo, args.comment, { body });
|
|
204
|
+
break;
|
|
205
|
+
case "pr-edit":
|
|
206
|
+
result = editPrBody(args.repo, args.pr, { body });
|
|
207
|
+
break;
|
|
208
|
+
default:
|
|
209
|
+
process.stderr.write(`error: unknown command ${JSON.stringify(args.command)}\n`);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
142
215
|
}
|
|
143
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
144
|
-
return 0;
|
|
145
216
|
}
|
|
146
217
|
catch (exc) {
|
|
147
218
|
process.stderr.write(`error: ${String(exc)}\n`);
|
|
@@ -98,7 +98,7 @@ export function createConsumerProject(parent) {
|
|
|
98
98
|
export function writeScopeVbrief(projectRoot, folder, filename, status = "proposed") {
|
|
99
99
|
const target = join(projectRoot, "xbrief", folder, filename);
|
|
100
100
|
writeFileSync(target, `${JSON.stringify({
|
|
101
|
-
|
|
101
|
+
xBRIEFInfo: { version: "0.8" },
|
|
102
102
|
plan: {
|
|
103
103
|
title: "Consumer fixture scope",
|
|
104
104
|
status,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
4
5
|
import { createIssueComment } from "../intake/github-body.js";
|
|
5
6
|
import { provenanceIssueNumber, repoSlugFromUrl } from "../intake/issue-ingest.js";
|
|
6
7
|
import { extractReferencesFromVbrief, GITHUB_ISSUE_REF_TYPES, parseIssueNumber, } from "../intake/reconcile-issues.js";
|
|
@@ -192,9 +193,11 @@ export function syncFromXbrief(options) {
|
|
|
192
193
|
writeOut(`issue:sync-from-xbrief: posted comment to ${origin.repo}#${origin.number} (id: ${commentResult.id}).`);
|
|
193
194
|
const writeFingerprint = options.writeFingerprint ??
|
|
194
195
|
((targetPath, stamped) => {
|
|
196
|
+
assertWriteTargetSafe(projectRoot, targetPath);
|
|
195
197
|
writeFileSync(targetPath, `${JSON.stringify(stamped, null, 2)}\n`, "utf8");
|
|
196
198
|
});
|
|
197
199
|
try {
|
|
200
|
+
assertWriteTargetSafe(projectRoot, absPath);
|
|
198
201
|
writeFingerprint(absPath, stampIssueSyncFingerprint(data, origin));
|
|
199
202
|
}
|
|
200
203
|
catch (exc) {
|
|
@@ -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[];
|
package/dist/lifecycle/events.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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,25 @@
|
|
|
1
|
+
import type { RunGhFn } from "../pr-protected-issues/types.js";
|
|
2
|
+
export type OutputStream = "stdout" | "stderr" | "none";
|
|
3
|
+
export interface OrphanActiveBrief {
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly reason: string;
|
|
6
|
+
}
|
|
7
|
+
export interface EvaluateResult {
|
|
8
|
+
readonly code: 0 | 1 | 2;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly stream: OutputStream;
|
|
11
|
+
readonly orphans: readonly OrphanActiveBrief[];
|
|
12
|
+
}
|
|
13
|
+
export interface EvaluateOptions {
|
|
14
|
+
readonly quiet?: boolean;
|
|
15
|
+
readonly repo?: string | null;
|
|
16
|
+
readonly runGh?: RunGhFn;
|
|
17
|
+
readonly skipGh?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pure evaluator for orphaned active/running xBRIEF detection (#2321).
|
|
21
|
+
* Fails when active/ briefs with plan.status==running reference only closed
|
|
22
|
+
* issues and/or a merged PR — the stop-at:pr-open orphan signature.
|
|
23
|
+
*/
|
|
24
|
+
export declare function evaluate(projectRoot: string, options?: EvaluateOptions): EvaluateResult;
|
|
25
|
+
//# sourceMappingURL=evaluate.d.ts.map
|