@deftai/directive-core 0.77.0 → 0.78.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/doctor/main.d.ts +1 -0
- package/dist/doctor/main.js +37 -0
- package/dist/doctor/types.d.ts +3 -0
- package/dist/eval/triggers.d.ts +76 -0
- package/dist/eval/triggers.js +259 -0
- package/dist/eval-triggers-relocation/evaluate.d.ts +36 -0
- package/dist/eval-triggers-relocation/evaluate.js +115 -0
- package/dist/eval-triggers-relocation/index.d.ts +2 -0
- package/dist/eval-triggers-relocation/index.js +2 -0
- package/dist/hooks/dispatcher.d.ts +43 -0
- package/dist/hooks/dispatcher.js +171 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/scope.d.ts +12 -0
- package/dist/hooks/scope.js +46 -0
- package/dist/hooks/tools.d.ts +4 -0
- package/dist/hooks/tools.js +23 -0
- package/dist/init-deposit/agent-hooks.d.ts +22 -0
- package/dist/init-deposit/agent-hooks.js +228 -0
- package/dist/init-deposit/hygiene.js +3 -0
- package/dist/init-deposit/index.d.ts +1 -0
- package/dist/init-deposit/index.js +1 -0
- package/dist/init-deposit/init-deposit.js +2 -0
- package/dist/init-deposit/refresh.js +2 -0
- package/dist/intake/reconcile-issues.d.ts +1 -0
- package/dist/intake/reconcile-issues.js +51 -49
- package/dist/release/build-dist.js +1 -0
- package/dist/release-e2e/greenfield-python-free-smoke-cli.js +40 -1
- package/dist/release-e2e/greenfield-python-free-smoke.d.ts +2 -0
- package/dist/release-e2e/greenfield-python-free-smoke.js +28 -8
- package/dist/session/verify-session-ritual.d.ts +16 -0
- package/dist/session/verify-session-ritual.js +63 -0
- package/dist/triage/help/registry-data.d.ts +12 -1
- package/dist/triage/help/registry-data.js +22 -1
- package/dist/verify-env/agent-hooks.d.ts +11 -0
- package/dist/verify-env/agent-hooks.js +45 -0
- package/dist/verify-env/command-spawn.d.ts +5 -0
- package/dist/verify-env/command-spawn.js +28 -9
- package/dist/verify-env/index.d.ts +1 -0
- package/dist/verify-env/index.js +1 -0
- package/package.json +15 -3
|
@@ -188,6 +188,52 @@ function splitRepoSlug(repo) {
|
|
|
188
188
|
}
|
|
189
189
|
return [parts[0], parts[1]];
|
|
190
190
|
}
|
|
191
|
+
function normalizeRestIssueState(raw) {
|
|
192
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
193
|
+
return "NOT_FOUND";
|
|
194
|
+
}
|
|
195
|
+
return raw.toUpperCase();
|
|
196
|
+
}
|
|
197
|
+
function normalizeRestStateReason(raw) {
|
|
198
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return raw.toUpperCase();
|
|
202
|
+
}
|
|
203
|
+
function isRestNotFoundResult(result) {
|
|
204
|
+
const stderr = result.stderr.trim();
|
|
205
|
+
return (stderr.includes("404") ||
|
|
206
|
+
stderr.includes("Not Found") ||
|
|
207
|
+
stderr.includes("Could not resolve to an Issue"));
|
|
208
|
+
}
|
|
209
|
+
function issueStateFromRestPayload(payload) {
|
|
210
|
+
return new IssueState(normalizeRestIssueState(payload.state), normalizeRestStateReason(payload.state_reason));
|
|
211
|
+
}
|
|
212
|
+
function fetchOneIssueStateRest(owner, name, issueNumber, scmCall, cwd) {
|
|
213
|
+
let result;
|
|
214
|
+
try {
|
|
215
|
+
result = scmCall("github-issue", "api", [`repos/${owner}/${name}/issues/${issueNumber}`], {
|
|
216
|
+
timeout: 30,
|
|
217
|
+
cwd,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return "CLI_MISSING";
|
|
222
|
+
}
|
|
223
|
+
if (result.returncode !== 0) {
|
|
224
|
+
if (isRestNotFoundResult(result)) {
|
|
225
|
+
return new IssueState("NOT_FOUND", null);
|
|
226
|
+
}
|
|
227
|
+
process.stderr.write(`Error: gh REST failed fetching issue #${issueNumber}: ${result.stderr.trim()}\n`);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
return issueStateFromRestPayload(JSON.parse(result.stdout));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
191
237
|
export function fetchIssueStates(repo, issueNumbers, options = {}) {
|
|
192
238
|
if (issueNumbers.size === 0) {
|
|
193
239
|
return new Map();
|
|
@@ -198,63 +244,19 @@ export function fetchIssueStates(repo, issueNumbers, options = {}) {
|
|
|
198
244
|
return null;
|
|
199
245
|
}
|
|
200
246
|
const [owner, name] = parsed;
|
|
201
|
-
const batchSize = options.batchSize ?? GRAPHQL_BATCH_SIZE;
|
|
202
247
|
const sortedNumbers = [...issueNumbers].sort((a, b) => a - b);
|
|
203
248
|
const states = new Map();
|
|
204
249
|
const scmCall = options.scmCall ?? call;
|
|
205
|
-
for (
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
.map((n) => `i${n}: issue(number: ${n}) { state stateReason }`)
|
|
209
|
-
.join("\n ");
|
|
210
|
-
const query = `query {\n repository(owner: "${owner}", name: "${name}") {\n ${aliases}\n }\n}\n`;
|
|
211
|
-
let result;
|
|
212
|
-
try {
|
|
213
|
-
result = scmCall("github-issue", "api", ["graphql", "-f", `query=${query}`], {
|
|
214
|
-
timeout: 60,
|
|
215
|
-
cwd: options.cwd ?? undefined,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
250
|
+
for (const n of sortedNumbers) {
|
|
251
|
+
const fetched = fetchOneIssueStateRest(owner, name, n, scmCall, options.cwd ?? undefined);
|
|
252
|
+
if (fetched === "CLI_MISSING") {
|
|
219
253
|
process.stderr.write("Error: gh CLI not found. Install GitHub CLI.\n");
|
|
220
254
|
return null;
|
|
221
255
|
}
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
payload = result.stdout ? JSON.parse(result.stdout) : null;
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
payload = null;
|
|
228
|
-
}
|
|
229
|
-
if (result.returncode !== 0) {
|
|
230
|
-
if (payload === null || typeof payload.data !== "object" || payload.data === null) {
|
|
231
|
-
process.stderr.write(`Error: gh CLI failed: ${result.stderr.trim()}\n`);
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
const firstLine = result.stderr.trim().split("\n")[0] ?? "";
|
|
235
|
-
process.stderr.write(`Warning: gh GraphQL returned partial errors (likely PR numbers referenced as issues): ${firstLine}\n`);
|
|
236
|
-
}
|
|
237
|
-
if (payload === null) {
|
|
238
|
-
process.stderr.write("Error: failed to parse gh CLI graphql output.\n");
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
const repoData = payload.data?.repository;
|
|
242
|
-
if (repoData === null || typeof repoData !== "object" || Array.isArray(repoData)) {
|
|
243
|
-
process.stderr.write("Error: gh CLI graphql response missing repository payload.\n");
|
|
256
|
+
if (fetched === null) {
|
|
244
257
|
return null;
|
|
245
258
|
}
|
|
246
|
-
|
|
247
|
-
const node = repoData[`i${n}`];
|
|
248
|
-
if (node !== null && typeof node === "object" && !Array.isArray(node)) {
|
|
249
|
-
const nodeObj = node;
|
|
250
|
-
if (typeof nodeObj.state === "string") {
|
|
251
|
-
const reason = typeof nodeObj.stateReason === "string" ? nodeObj.stateReason : null;
|
|
252
|
-
states.set(n, new IssueState(nodeObj.state, reason));
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
states.set(n, new IssueState("NOT_FOUND", null));
|
|
257
|
-
}
|
|
259
|
+
states.set(n, fetched);
|
|
258
260
|
}
|
|
259
261
|
return states;
|
|
260
262
|
}
|
|
@@ -2,9 +2,48 @@
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { rehearseGreenfieldPythonFreeSmoke } from "./greenfield-python-free-smoke.js";
|
|
5
|
+
const DEFAULT_OVERALL_TIMEOUT_MS = 900_000;
|
|
6
|
+
function parseOverallTimeoutMs() {
|
|
7
|
+
const raw = process.env.DEFT_GREENFIELD_OVERALL_TIMEOUT_MS?.trim();
|
|
8
|
+
if (!raw) {
|
|
9
|
+
return DEFAULT_OVERALL_TIMEOUT_MS;
|
|
10
|
+
}
|
|
11
|
+
const parsed = Number.parseInt(raw, 10);
|
|
12
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_OVERALL_TIMEOUT_MS;
|
|
13
|
+
}
|
|
14
|
+
function logProgress(message) {
|
|
15
|
+
process.stderr.write(`${message}\n`);
|
|
16
|
+
}
|
|
5
17
|
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
|
6
18
|
const skipWorkspacePrep = process.env.DEFT_GREENFIELD_SKIP_PREP === "1";
|
|
7
|
-
const
|
|
19
|
+
const overallTimeoutMs = parseOverallTimeoutMs();
|
|
20
|
+
let lastProgress = "starting";
|
|
21
|
+
let finished = false;
|
|
22
|
+
const onProgress = (message) => {
|
|
23
|
+
lastProgress = message;
|
|
24
|
+
logProgress(message);
|
|
25
|
+
};
|
|
26
|
+
const failClosed = (reason, exitCode = 1) => {
|
|
27
|
+
if (finished) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
finished = true;
|
|
31
|
+
process.stdout.write(`${reason}\n`);
|
|
32
|
+
process.exit(exitCode);
|
|
33
|
+
};
|
|
34
|
+
const overallTimer = setTimeout(() => {
|
|
35
|
+
failClosed(`greenfield-python-free-smoke FAIL: overall budget exceeded (${overallTimeoutMs}ms); last step: ${lastProgress}`, 1);
|
|
36
|
+
}, overallTimeoutMs);
|
|
37
|
+
process.on("SIGTERM", () => {
|
|
38
|
+
failClosed(`greenfield-python-free-smoke FAIL: received SIGTERM during "${lastProgress}" — likely runner kill after hang (root cause: spawn pipe deadlock fixed in engine-invoke #2554)`, 143);
|
|
39
|
+
});
|
|
40
|
+
process.on("SIGINT", () => {
|
|
41
|
+
failClosed(`greenfield-python-free-smoke FAIL: interrupted during "${lastProgress}"`, 130);
|
|
42
|
+
});
|
|
43
|
+
logProgress(`greenfield-python-free-smoke: starting (overall budget ${overallTimeoutMs}ms, skipPrep=${skipWorkspacePrep})`);
|
|
44
|
+
const [ok, reason] = rehearseGreenfieldPythonFreeSmoke(repoRoot, {}, { skipWorkspacePrep, onProgress });
|
|
45
|
+
clearTimeout(overallTimer);
|
|
46
|
+
finished = true;
|
|
8
47
|
process.stdout.write(`${reason}\n`);
|
|
9
48
|
process.exit(ok ? 0 : 1);
|
|
10
49
|
//# sourceMappingURL=greenfield-python-free-smoke-cli.js.map
|
|
@@ -3,6 +3,8 @@ export interface GreenfieldSmokeSeams extends E2ESeams {
|
|
|
3
3
|
}
|
|
4
4
|
export interface GreenfieldSmokeOptions {
|
|
5
5
|
skipWorkspacePrep?: boolean;
|
|
6
|
+
/** Emits step progress immediately (stderr in CLI) so CI logs are never empty on hang (#2554). */
|
|
7
|
+
onProgress?: (message: string) => void;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Greenfield npm smoke (#2022 Phase 3): pack/install directive, run init +
|
|
@@ -46,12 +46,23 @@ function packTarballPath(packDir, pkgDir) {
|
|
|
46
46
|
const scoped = manifest.name.replaceAll("@", "").replaceAll("/", "-");
|
|
47
47
|
return join(packDir, `${scoped}-${manifest.version}.tgz`);
|
|
48
48
|
}
|
|
49
|
-
function runStep(spawn, label, cmd, args, options = {}) {
|
|
49
|
+
function runStep(spawn, label, cmd, args, options = {}, onProgress) {
|
|
50
|
+
onProgress?.(`greenfield smoke: ${label} — starting`);
|
|
51
|
+
const startedMs = Date.now();
|
|
50
52
|
const result = spawn(cmd, args, options);
|
|
53
|
+
const elapsedMs = Date.now() - startedMs;
|
|
51
54
|
if (result.status !== 0) {
|
|
52
55
|
const detail = (result.stderr || result.stdout || "").trim();
|
|
53
|
-
|
|
56
|
+
const timeoutHint = options.timeoutMs !== undefined && result.status === 128
|
|
57
|
+
? `; subprocess killed after ${elapsedMs}ms (spawn budget ${options.timeoutMs}ms — likely hang or timeout)`
|
|
58
|
+
: "";
|
|
59
|
+
onProgress?.(`greenfield smoke: ${label} — failed (exit ${result.status}) after ${elapsedMs}ms`);
|
|
60
|
+
return [
|
|
61
|
+
false,
|
|
62
|
+
`${label} failed (exit ${result.status})${timeoutHint}: ${detail.slice(-800) || "(no captured output)"}`,
|
|
63
|
+
];
|
|
54
64
|
}
|
|
65
|
+
onProgress?.(`greenfield smoke: ${label} — OK (${elapsedMs}ms)`);
|
|
55
66
|
return [true, `${label} OK`];
|
|
56
67
|
}
|
|
57
68
|
/**
|
|
@@ -77,6 +88,8 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
|
|
|
77
88
|
return [false, "greenfield-python-free-smoke FAIL: pnpm command prefix is empty"];
|
|
78
89
|
}
|
|
79
90
|
const spawn = seams.spawnText ?? spawnText;
|
|
91
|
+
const onProgress = options.onProgress;
|
|
92
|
+
onProgress?.("greenfield smoke: workspace prep starting");
|
|
80
93
|
const work = mkdtempSync(join(tmpdir(), "deft-greenfield-smoke-"));
|
|
81
94
|
const packDir = join(work, "packs");
|
|
82
95
|
mkdirSync(packDir, { recursive: true });
|
|
@@ -98,24 +111,28 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
|
|
|
98
111
|
cwd: repoRoot,
|
|
99
112
|
env: envBase,
|
|
100
113
|
timeoutMs: 120_000,
|
|
101
|
-
});
|
|
114
|
+
}, onProgress);
|
|
102
115
|
if (!ok)
|
|
103
116
|
return [false, `greenfield smoke: ${reason}`];
|
|
104
117
|
[ok, reason] = runStep(spawn, "pnpm build", pnpmCmd, [...pnpmArgs, "run", "build"], {
|
|
105
118
|
cwd: repoRoot,
|
|
106
119
|
env: envBase,
|
|
107
120
|
timeoutMs: 120_000,
|
|
108
|
-
});
|
|
121
|
+
}, onProgress);
|
|
109
122
|
if (!ok)
|
|
110
123
|
return [false, `greenfield smoke: ${reason}`];
|
|
111
124
|
}
|
|
125
|
+
else {
|
|
126
|
+
onProgress?.("greenfield smoke: skipping workspace prep (DEFT_GREENFIELD_SKIP_PREP=1)");
|
|
127
|
+
}
|
|
128
|
+
onProgress?.("greenfield smoke: aligning npm package versions");
|
|
112
129
|
[ok, reason] = alignNpmPackageVersions(repoRoot, SMOKE_VERSION);
|
|
113
130
|
if (!ok)
|
|
114
131
|
return [false, `greenfield smoke: ${reason}`];
|
|
115
132
|
const packed = [];
|
|
116
133
|
for (const pkg of NPM_PUBLISH_PACKAGES) {
|
|
117
134
|
const pkgDir = join(repoRoot, "packages", pkg);
|
|
118
|
-
[ok, reason] = runStep(spawn, `npm pack packages/${pkg}`, npm, ["pack", "--pack-destination", packDir], { cwd: pkgDir, env: envBase, timeoutMs: 120_000 });
|
|
135
|
+
[ok, reason] = runStep(spawn, `npm pack packages/${pkg}`, npm, ["pack", "--pack-destination", packDir], { cwd: pkgDir, env: envBase, timeoutMs: 120_000 }, onProgress);
|
|
119
136
|
if (!ok)
|
|
120
137
|
return [false, `greenfield smoke: ${reason}`];
|
|
121
138
|
const tgz = packTarballPath(packDir, pkgDir);
|
|
@@ -127,7 +144,7 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
|
|
|
127
144
|
[ok, reason] = runStep(spawn, "npm install -g", npm, ["install", "-g", ...packed], {
|
|
128
145
|
env: envBase,
|
|
129
146
|
timeoutMs: 120_000,
|
|
130
|
-
});
|
|
147
|
+
}, onProgress);
|
|
131
148
|
if (!ok)
|
|
132
149
|
return [false, `greenfield smoke: ${reason}`];
|
|
133
150
|
const deft = join(npmPrefix, "bin", "deft");
|
|
@@ -139,9 +156,10 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
|
|
|
139
156
|
cwd: work,
|
|
140
157
|
env: pyFree,
|
|
141
158
|
timeoutMs: 120_000,
|
|
142
|
-
});
|
|
159
|
+
}, onProgress);
|
|
143
160
|
if (!ok)
|
|
144
161
|
return [false, `greenfield smoke: ${reason}`];
|
|
162
|
+
onProgress?.("greenfield smoke: seeding fixture PROJECT-DEFINITION");
|
|
145
163
|
seedMinimalProjectDefinition(projectDir);
|
|
146
164
|
const depositDir = join(projectDir, ".deft", "core");
|
|
147
165
|
const artifacts = collectPythonArtifacts(depositDir);
|
|
@@ -159,13 +177,15 @@ export function rehearseGreenfieldPythonFreeSmoke(repoRoot, seams = {}, options
|
|
|
159
177
|
PATH: `${join(npmPrefix, "bin")}:${pyFree.PATH ?? ""}`,
|
|
160
178
|
DEFT_SESSION_RITUAL_SKIP: "1",
|
|
161
179
|
};
|
|
180
|
+
onProgress?.("greenfield smoke: running consumer task deft:check (engine-invoke path)");
|
|
162
181
|
[ok, reason] = runStep(spawn, "task deft:check", task, ["deft:check"], {
|
|
163
182
|
cwd: projectDir,
|
|
164
183
|
env: checkEnv,
|
|
165
184
|
timeoutMs: 180_000,
|
|
166
|
-
});
|
|
185
|
+
}, onProgress);
|
|
167
186
|
if (!ok)
|
|
168
187
|
return [false, `greenfield smoke: ${reason}`];
|
|
188
|
+
onProgress?.("greenfield smoke: all steps passed");
|
|
169
189
|
return [
|
|
170
190
|
true,
|
|
171
191
|
"greenfield-python-free-smoke: directive init + task deft:check passed with Python absent from PATH",
|
|
@@ -29,6 +29,22 @@ export interface VerifySessionRitualOptions {
|
|
|
29
29
|
readonly envPosture?: string | undefined;
|
|
30
30
|
readonly handoffText?: string | null;
|
|
31
31
|
}
|
|
32
|
+
export interface InspectSessionRitualOptions {
|
|
33
|
+
readonly tier?: "quick" | "gated";
|
|
34
|
+
readonly now?: Date;
|
|
35
|
+
readonly runGit?: GitRunner;
|
|
36
|
+
readonly posture?: DirectivePosture;
|
|
37
|
+
readonly envPosture?: string | undefined;
|
|
38
|
+
readonly handoffText?: string | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read-only ritual-state inspection for host hooks.
|
|
42
|
+
*
|
|
43
|
+
* Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
|
|
44
|
+
* and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
|
|
45
|
+
* probe, not a hidden `doctor` / cache-refresh mutation boundary.
|
|
46
|
+
*/
|
|
47
|
+
export declare function inspectSessionRitual(projectRoot: string, options?: InspectSessionRitualOptions): VerifyResult;
|
|
32
48
|
export declare function verifySessionRitual(projectRoot: string, options?: VerifySessionRitualOptions): VerifyResult;
|
|
33
49
|
export declare function emitVerifyJson(result: VerifyResult): string;
|
|
34
50
|
export declare function emitBypassWarning(result: VerifyResult): string;
|
|
@@ -105,6 +105,69 @@ function evaluateLoadedState(projectRoot, state, input) {
|
|
|
105
105
|
}
|
|
106
106
|
return [0, `OK session ritual ${input.tier} tier is fresh.`];
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Read-only ritual-state inspection for host hooks.
|
|
110
|
+
*
|
|
111
|
+
* Unlike {@link verifySessionRitual}, this never runs missing gated entrypoints
|
|
112
|
+
* and never rewrites `.deft/ritual-state.json`. A PreToolUse decision must be a
|
|
113
|
+
* probe, not a hidden `doctor` / cache-refresh mutation boundary.
|
|
114
|
+
*/
|
|
115
|
+
export function inspectSessionRitual(projectRoot, options = {}) {
|
|
116
|
+
const tier = options.tier ?? "quick";
|
|
117
|
+
const posture = resolveSessionPosture({
|
|
118
|
+
explicitPosture: options.posture ?? null,
|
|
119
|
+
envPosture: options.envPosture ?? process.env.DEFT_SESSION_POSTURE,
|
|
120
|
+
handoffText: options.handoffText,
|
|
121
|
+
tier,
|
|
122
|
+
});
|
|
123
|
+
const ritualStateRequired = posture === "mutation" && !ritualStateIsPostureAuthority();
|
|
124
|
+
const statePath = ritualStatePath(projectRoot);
|
|
125
|
+
if (posture === "read-only") {
|
|
126
|
+
return {
|
|
127
|
+
code: 0,
|
|
128
|
+
message: readOnlyPostureMessage(tier),
|
|
129
|
+
tier,
|
|
130
|
+
statePath,
|
|
131
|
+
bypassed: false,
|
|
132
|
+
wouldFailCode: null,
|
|
133
|
+
posture,
|
|
134
|
+
ritualStateRequired: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const missingStateFile = !existsSync(statePath);
|
|
138
|
+
const [state, err] = readRitualState(projectRoot);
|
|
139
|
+
if (state === null) {
|
|
140
|
+
const code = missingStateFile ? 1 : 2;
|
|
141
|
+
const startCommand = formatFrameworkCommand(["session:start"]);
|
|
142
|
+
return {
|
|
143
|
+
code,
|
|
144
|
+
message: code === 1
|
|
145
|
+
? `${err}. Run \`${startCommand}\` before implementation dispatch.`
|
|
146
|
+
: (err ?? "ritual state invalid"),
|
|
147
|
+
tier,
|
|
148
|
+
statePath,
|
|
149
|
+
bypassed: false,
|
|
150
|
+
wouldFailCode: null,
|
|
151
|
+
posture,
|
|
152
|
+
ritualStateRequired,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const [code, message] = evaluateLoadedState(projectRoot, state, {
|
|
156
|
+
tier,
|
|
157
|
+
now: options.now ?? new Date(),
|
|
158
|
+
runGit: options.runGit,
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
code,
|
|
162
|
+
message,
|
|
163
|
+
tier,
|
|
164
|
+
statePath,
|
|
165
|
+
bypassed: false,
|
|
166
|
+
wouldFailCode: null,
|
|
167
|
+
posture,
|
|
168
|
+
ritualStateRequired,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
108
171
|
export function verifySessionRitual(projectRoot, options = {}) {
|
|
109
172
|
const tier = options.tier ?? "quick";
|
|
110
173
|
const posture = resolveSessionPosture({
|
|
@@ -364,6 +364,17 @@ export declare const registryData: {
|
|
|
364
364
|
readonly see_also: readonly ["task eval:run", "task eval:report", "#1703"];
|
|
365
365
|
readonly placeholder: false;
|
|
366
366
|
};
|
|
367
|
+
readonly "task eval:triggers": {
|
|
368
|
+
readonly name: "task eval:triggers";
|
|
369
|
+
readonly summary: "Trigger routing coverage for Skills Index rules";
|
|
370
|
+
readonly refs: "(#1586)";
|
|
371
|
+
readonly description: "Offline skill-pi-trigger-eval compatible routing check: grades evals/trigger-cases.jsonl against REFERENCES.md Skills Index triggers (should-fire / should-not-fire per skill).";
|
|
372
|
+
readonly usage: "task eval:triggers [-- --json] [--project-root PATH]";
|
|
373
|
+
readonly flags: readonly [readonly ["--json", "(off)", "Emit the TriggerEvalReport JSON."], readonly ["--project-root PATH", "(cwd)", "Project root override (Taskfile threads USER_WORKING_DIR)."]];
|
|
374
|
+
readonly examples: readonly ["task eval:triggers", "task eval:triggers -- --json"];
|
|
375
|
+
readonly see_also: readonly ["task eval:health", "task verify:eval-triggers-relocation", "#1862"];
|
|
376
|
+
readonly placeholder: false;
|
|
377
|
+
};
|
|
367
378
|
readonly "task eval:run": {
|
|
368
379
|
readonly name: "task eval:run";
|
|
369
380
|
readonly summary: "Tier 2 golden corpus eval for a model";
|
|
@@ -508,7 +519,7 @@ export declare const registryData: {
|
|
|
508
519
|
readonly placeholder: false;
|
|
509
520
|
};
|
|
510
521
|
};
|
|
511
|
-
readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:run", "task eval:report"]]];
|
|
522
|
+
readonly categoriesTriage: readonly [readonly ["Session-start", readonly ["task triage:summary", "task verify:cache-fresh"]], readonly ["State verbs (mutate audit log)", readonly ["task triage:accept", "task triage:defer", "task triage:reject", "task triage:needs-ac", "task triage:mark-duplicate", "task triage:reset", "task triage:status", "task triage:history"]], readonly ["Read verbs", readonly ["task triage:queue", "task triage:audit", "task triage:show", "task triage:scope", "task triage:scope-drift", "task triage:classify"]], readonly ["Lifecycle", readonly ["task triage:bootstrap", "task triage:welcome", "task triage:reconcile"]], readonly ["Bulk variants", readonly ["task triage:bulk-accept", "task triage:bulk-reject", "task triage:bulk-defer", "task triage:bulk-needs-ac", "task triage:refresh-active", "task triage:smoketest"]], readonly ["Subscription mutation", readonly ["task triage:subscribe", "task triage:unsubscribe"]], readonly ["Archive / rotation", readonly ["task triage:audit:prune", "task triage:archive-list", "task triage:restore-from-archive", "task triage:audit-log:rotate", "task triage:metrics"]], readonly ["Framework eval (#1703)", readonly ["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"]]];
|
|
512
523
|
readonly categoriesScope: readonly [readonly ["Promote / demote", readonly ["task scope:promote", "task scope:demote"]], readonly ["Activate / complete", readonly ["task scope:activate", "task scope:complete", "task scope:fail", "task scope:cancel", "task scope:block", "task scope:unblock"]], readonly ["Reversibility", readonly ["task scope:undo", "task scope:restore"]], readonly ["Decomposition", readonly ["task scope:decompose"]]];
|
|
513
524
|
readonly scriptSubcommandMap: {
|
|
514
525
|
readonly triage_actions: {
|
|
@@ -548,6 +548,24 @@ export const registryData = {
|
|
|
548
548
|
see_also: ["task eval:run", "task eval:report", "#1703"],
|
|
549
549
|
placeholder: false,
|
|
550
550
|
},
|
|
551
|
+
"task eval:triggers": {
|
|
552
|
+
name: "task eval:triggers",
|
|
553
|
+
summary: "Trigger routing coverage for Skills Index rules",
|
|
554
|
+
refs: "(#1586)",
|
|
555
|
+
description: "Offline skill-pi-trigger-eval compatible routing check: grades evals/trigger-cases.jsonl against REFERENCES.md Skills Index triggers (should-fire / should-not-fire per skill).",
|
|
556
|
+
usage: "task eval:triggers [-- --json] [--project-root PATH]",
|
|
557
|
+
flags: [
|
|
558
|
+
["--json", "(off)", "Emit the TriggerEvalReport JSON."],
|
|
559
|
+
[
|
|
560
|
+
"--project-root PATH",
|
|
561
|
+
"(cwd)",
|
|
562
|
+
"Project root override (Taskfile threads USER_WORKING_DIR).",
|
|
563
|
+
],
|
|
564
|
+
],
|
|
565
|
+
examples: ["task eval:triggers", "task eval:triggers -- --json"],
|
|
566
|
+
see_also: ["task eval:health", "task verify:eval-triggers-relocation", "#1862"],
|
|
567
|
+
placeholder: false,
|
|
568
|
+
},
|
|
551
569
|
"task eval:run": {
|
|
552
570
|
name: "task eval:run",
|
|
553
571
|
summary: "Tier 2 golden corpus eval for a model",
|
|
@@ -829,7 +847,10 @@ export const registryData = {
|
|
|
829
847
|
"task triage:metrics",
|
|
830
848
|
],
|
|
831
849
|
],
|
|
832
|
-
[
|
|
850
|
+
[
|
|
851
|
+
"Framework eval (#1703)",
|
|
852
|
+
["task eval:health", "task eval:triggers", "task eval:run", "task eval:report"],
|
|
853
|
+
],
|
|
833
854
|
],
|
|
834
855
|
categoriesScope: [
|
|
835
856
|
["Promote / demote", ["task scope:promote", "task scope:demote"]],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentHookInspection } from "../init-deposit/agent-hooks.js";
|
|
2
|
+
import type { OutputStream } from "./verify-hooks-installed.js";
|
|
3
|
+
export interface AgentHookHealthResult {
|
|
4
|
+
readonly code: 0 | 1 | 2;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly stream: OutputStream;
|
|
7
|
+
readonly registrations: readonly AgentHookInspection[];
|
|
8
|
+
}
|
|
9
|
+
/** Read-only P0 agent-host registration health, independent of git hooks. */
|
|
10
|
+
export declare function evaluateAgentHooks(projectRoot: string): AgentHookHealthResult;
|
|
11
|
+
//# sourceMappingURL=agent-hooks.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { inspectAgentHookDeposit } from "../init-deposit/agent-hooks.js";
|
|
4
|
+
function isDirectory(path) {
|
|
5
|
+
try {
|
|
6
|
+
return statSync(path).isDirectory();
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Read-only P0 agent-host registration health, independent of git hooks. */
|
|
13
|
+
export function evaluateAgentHooks(projectRoot) {
|
|
14
|
+
const root = resolve(projectRoot);
|
|
15
|
+
if (!isDirectory(root)) {
|
|
16
|
+
return {
|
|
17
|
+
code: 2,
|
|
18
|
+
message: `❌ deft agent hooks: project root ${root} does not exist (config error).`,
|
|
19
|
+
stream: "stderr",
|
|
20
|
+
registrations: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const registrations = inspectAgentHookDeposit(root);
|
|
24
|
+
const unhealthy = registrations.filter((entry) => entry.status !== "healthy");
|
|
25
|
+
if (unhealthy.length > 0) {
|
|
26
|
+
return {
|
|
27
|
+
code: 1,
|
|
28
|
+
message: "❌ deft agent hooks NON-FUNCTIONAL:\n" +
|
|
29
|
+
unhealthy
|
|
30
|
+
.map((entry) => ` - ${entry.host}: ${entry.status} at ${entry.path} — ${entry.detail}`)
|
|
31
|
+
.join("\n") +
|
|
32
|
+
"\n Recovery: run `deft update` (or `directive init`) to refresh project hooks.",
|
|
33
|
+
stream: "stderr",
|
|
34
|
+
registrations,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
code: 0,
|
|
39
|
+
message: "✓ deft agent hooks installed and functional for Claude, Grok, Cursor " +
|
|
40
|
+
"(SessionStart + PreToolUse direct-write tools only; shell/MCP policy is deferred).",
|
|
41
|
+
stream: "stdout",
|
|
42
|
+
registrations,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=agent-hooks.js.map
|
|
@@ -6,6 +6,11 @@ export interface ResolveCommandOnPathOptions {
|
|
|
6
6
|
}
|
|
7
7
|
/** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
|
|
8
8
|
export declare function shouldUseShellForCommand(command: string, platform?: NodeJS.Platform): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Quote a win32 executable path for `shell: true` spawns when it contains spaces.
|
|
11
|
+
* Without quoting, cmd.exe treats `C:\Program` as the command (#2555).
|
|
12
|
+
*/
|
|
13
|
+
export declare function quoteWin32CommandForShell(command: string, platform?: NodeJS.Platform): string;
|
|
9
14
|
/**
|
|
10
15
|
* Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
|
|
11
16
|
* Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
|
|
@@ -6,6 +6,20 @@ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
|
6
6
|
export function shouldUseShellForCommand(command, platform = process.platform) {
|
|
7
7
|
return platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Quote a win32 executable path for `shell: true` spawns when it contains spaces.
|
|
11
|
+
* Without quoting, cmd.exe treats `C:\Program` as the command (#2555).
|
|
12
|
+
*/
|
|
13
|
+
export function quoteWin32CommandForShell(command, platform = process.platform) {
|
|
14
|
+
if (platform !== "win32" || !command.includes(" ")) {
|
|
15
|
+
return command;
|
|
16
|
+
}
|
|
17
|
+
if ((command.startsWith('"') && command.endsWith('"')) ||
|
|
18
|
+
(command.startsWith("'") && command.endsWith("'"))) {
|
|
19
|
+
return command;
|
|
20
|
+
}
|
|
21
|
+
return `"${command}"`;
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
|
|
11
25
|
* Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
|
|
@@ -39,15 +53,20 @@ export function resolveCommandOnPath(command, options = {}) {
|
|
|
39
53
|
* Retries with `shell: true` on win32 ENOENT (npm global `.cmd` shims).
|
|
40
54
|
*/
|
|
41
55
|
export function spawnCommandText(cmd, args, options = {}) {
|
|
42
|
-
const trySpawn = (shell) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
const trySpawn = (shell) => {
|
|
57
|
+
const spawnCmd = shell && process.platform === "win32" ? quoteWin32CommandForShell(cmd) : cmd;
|
|
58
|
+
return spawnSync(spawnCmd, [...args], {
|
|
59
|
+
cwd: options.cwd,
|
|
60
|
+
env: options.env ?? process.env,
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
timeout: options.timeoutMs,
|
|
63
|
+
maxBuffer: SUBPROCESS_MAX_BUFFER,
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
+
shell,
|
|
66
|
+
// CREATE_NO_WINDOW on win32; harmless elsewhere (#2563).
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
});
|
|
69
|
+
};
|
|
51
70
|
let result = trySpawn(shouldUseShellForCommand(cmd));
|
|
52
71
|
const spawnErr = result.error;
|
|
53
72
|
if (spawnErr?.code === "ENOENT" && process.platform === "win32") {
|
package/dist/verify-env/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
4
4
|
"description": "TypeScript engine core for the Directive framework.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"types": "./dist/eval-health-relocation/index.d.ts",
|
|
43
43
|
"default": "./dist/eval-health-relocation/index.js"
|
|
44
44
|
},
|
|
45
|
+
"./eval-triggers-relocation": {
|
|
46
|
+
"types": "./dist/eval-triggers-relocation/index.d.ts",
|
|
47
|
+
"default": "./dist/eval-triggers-relocation/index.js"
|
|
48
|
+
},
|
|
45
49
|
"./xbrief-migrate": {
|
|
46
50
|
"types": "./dist/xbrief-migrate/index.d.ts",
|
|
47
51
|
"default": "./dist/xbrief-migrate/index.js"
|
|
@@ -62,6 +66,10 @@
|
|
|
62
66
|
"types": "./dist/session/index.d.ts",
|
|
63
67
|
"default": "./dist/session/index.js"
|
|
64
68
|
},
|
|
69
|
+
"./hooks": {
|
|
70
|
+
"types": "./dist/hooks/index.d.ts",
|
|
71
|
+
"default": "./dist/hooks/index.js"
|
|
72
|
+
},
|
|
65
73
|
"./plan-sequence": {
|
|
66
74
|
"types": "./dist/plan-sequence/index.d.ts",
|
|
67
75
|
"default": "./dist/plan-sequence/index.js"
|
|
@@ -106,6 +114,10 @@
|
|
|
106
114
|
"types": "./dist/eval/run.d.ts",
|
|
107
115
|
"default": "./dist/eval/run.js"
|
|
108
116
|
},
|
|
117
|
+
"./eval/triggers": {
|
|
118
|
+
"types": "./dist/eval/triggers.d.ts",
|
|
119
|
+
"default": "./dist/eval/triggers.js"
|
|
120
|
+
},
|
|
109
121
|
"./eval/report": {
|
|
110
122
|
"types": "./dist/eval/report.d.ts",
|
|
111
123
|
"default": "./dist/eval/report.js"
|
|
@@ -289,8 +301,8 @@
|
|
|
289
301
|
"provenance": true
|
|
290
302
|
},
|
|
291
303
|
"dependencies": {
|
|
292
|
-
"@deftai/directive-content": "^0.
|
|
293
|
-
"@deftai/directive-types": "^0.
|
|
304
|
+
"@deftai/directive-content": "^0.78.0",
|
|
305
|
+
"@deftai/directive-types": "^0.78.0",
|
|
294
306
|
"archiver": "^8.0.0"
|
|
295
307
|
},
|
|
296
308
|
"scripts": {
|