@deftai/directive-core 0.76.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/agents-md-budget/evaluate.d.ts +8 -1
- package/dist/agents-md-budget/evaluate.js +54 -19
- package/dist/content-contracts/skills/skill-frontmatter.js +4 -0
- package/dist/doctor/constants.js +3 -3
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.d.ts +1 -0
- package/dist/doctor/main.js +39 -1
- package/dist/doctor/payload-staleness.js +62 -63
- package/dist/doctor/release-availability.d.ts +28 -0
- package/dist/doctor/release-availability.js +35 -0
- package/dist/doctor/types.d.ts +3 -0
- package/dist/eval/crud-telemetry.d.ts +5 -4
- package/dist/eval/crud-telemetry.js +9 -5
- package/dist/eval/health.d.ts +5 -4
- package/dist/eval/health.js +24 -16
- package/dist/eval/readback.js +2 -8
- 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/index.d.ts +1 -0
- package/dist/index.js +1 -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/metrics/index.d.ts +2 -0
- package/dist/metrics/index.js +2 -0
- package/dist/metrics/resolve-metrics-home.d.ts +50 -0
- package/dist/metrics/resolve-metrics-home.js +125 -0
- package/dist/plan-sequence/index.d.ts +3 -0
- package/dist/plan-sequence/index.js +3 -0
- package/dist/plan-sequence/store.d.ts +6 -0
- package/dist/plan-sequence/store.js +29 -0
- package/dist/plan-sequence/types.d.ts +79 -0
- package/dist/plan-sequence/types.js +220 -0
- package/dist/release/build-dist.js +1 -0
- package/dist/release/version.d.ts +2 -0
- package/dist/release/version.js +42 -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/release-e2e/npm-ops.js +20 -6
- 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 +29 -0
- package/dist/verify-env/command-spawn.js +97 -0
- package/dist/verify-env/index.d.ts +2 -0
- package/dist/verify-env/index.js +2 -0
- package/package.json +19 -3
package/dist/release/version.js
CHANGED
|
@@ -5,6 +5,12 @@ const PRE_KIND_MAP = {
|
|
|
5
5
|
rc: "rc",
|
|
6
6
|
};
|
|
7
7
|
const NON_PUBLISHABLE_KINDS = new Set(["test"]);
|
|
8
|
+
const PRERELEASE_RANK = {
|
|
9
|
+
alpha: 0,
|
|
10
|
+
beta: 1,
|
|
11
|
+
rc: 2,
|
|
12
|
+
"": 3,
|
|
13
|
+
};
|
|
8
14
|
export class NonPublishableVersionError extends Error {
|
|
9
15
|
constructor(message) {
|
|
10
16
|
super(message);
|
|
@@ -71,4 +77,40 @@ export function isPublishable(version) {
|
|
|
71
77
|
return false;
|
|
72
78
|
}
|
|
73
79
|
}
|
|
80
|
+
function publishableVersionSortKey(version) {
|
|
81
|
+
const candidate = version.trim();
|
|
82
|
+
const match = PEP440_TAG_RE.exec(candidate);
|
|
83
|
+
if (match?.groups === undefined) {
|
|
84
|
+
throw new Error(`Cannot compare '${candidate}': expected ` + "[v]X.Y.Z or [v]X.Y.Z-(rc|alpha|beta).N");
|
|
85
|
+
}
|
|
86
|
+
const kind = match.groups.kind ?? "";
|
|
87
|
+
if (NON_PUBLISHABLE_KINDS.has(kind)) {
|
|
88
|
+
throw new NonPublishableVersionError(`Version '${candidate}' carries non-publishable pre-release tag '${kind}'.${match.groups.num}.`);
|
|
89
|
+
}
|
|
90
|
+
const rank = PRERELEASE_RANK[kind];
|
|
91
|
+
if (rank === undefined) {
|
|
92
|
+
throw new Error(`Cannot compare '${candidate}': unsupported pre-release kind '${kind}'.`);
|
|
93
|
+
}
|
|
94
|
+
return [
|
|
95
|
+
Number(match.groups.major),
|
|
96
|
+
Number(match.groups.minor),
|
|
97
|
+
Number(match.groups.patch),
|
|
98
|
+
rank,
|
|
99
|
+
Number(match.groups.num ?? 0),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
/** Compare two publishable Deft release versions using stable/prerelease ordering. */
|
|
103
|
+
export function comparePublishableVersions(left, right) {
|
|
104
|
+
const leftKey = publishableVersionSortKey(left);
|
|
105
|
+
const rightKey = publishableVersionSortKey(right);
|
|
106
|
+
for (let index = 0; index < leftKey.length; index += 1) {
|
|
107
|
+
const leftPart = leftKey[index] ?? 0;
|
|
108
|
+
const rightPart = rightKey[index] ?? 0;
|
|
109
|
+
if (leftPart < rightPart)
|
|
110
|
+
return -1;
|
|
111
|
+
if (leftPart > rightPart)
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
74
116
|
//# sourceMappingURL=version.js.map
|
|
@@ -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",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { defaultWhich
|
|
3
|
+
import { defaultWhich } from "../release/spawn.js";
|
|
4
|
+
import { resolveCommandOnPath, spawnCommandText } from "../verify-env/command-spawn.js";
|
|
4
5
|
import { MODULE_NOT_FOUND_MARKERS, NPM_BUILD_TIMEOUT_SECONDS, NPM_E2E_REHEARSAL_TAG, NPM_INSTALL_RUN_TIMEOUT_SECONDS, NPM_INSTALL_TIMEOUT_SECONDS, NPM_PACK_TIMEOUT_SECONDS, NPM_PUBLISH_DRYRUN_TIMEOUT_SECONDS, NPM_PUBLISH_PACKAGES, } from "./constants.js";
|
|
5
6
|
/**
|
|
6
7
|
* npm publish dry-run rehearsal (#1910) -- the TS port of the
|
|
@@ -18,19 +19,32 @@ function resolveWhich(seams) {
|
|
|
18
19
|
* available.
|
|
19
20
|
*/
|
|
20
21
|
export function resolvePnpm(seams = {}) {
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// Injected which seam wins for unit tests; production uses PATHEXT-aware PATH scan
|
|
23
|
+
// so `where pnpm` extensionless shims do not ENOENT under spawnSync (#2548 / #2467).
|
|
24
|
+
if (seams.which ?? seams.whichGh) {
|
|
25
|
+
const which = resolveWhich(seams);
|
|
26
|
+
const pnpm = which("pnpm");
|
|
27
|
+
if (pnpm) {
|
|
28
|
+
return [pnpm];
|
|
29
|
+
}
|
|
30
|
+
const corepack = which("corepack");
|
|
31
|
+
if (corepack) {
|
|
32
|
+
return [corepack, "pnpm"];
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const pnpm = resolveCommandOnPath("pnpm");
|
|
23
37
|
if (pnpm) {
|
|
24
38
|
return [pnpm];
|
|
25
39
|
}
|
|
26
|
-
const corepack =
|
|
40
|
+
const corepack = resolveCommandOnPath("corepack");
|
|
27
41
|
if (corepack) {
|
|
28
42
|
return [corepack, "pnpm"];
|
|
29
43
|
}
|
|
30
44
|
return null;
|
|
31
45
|
}
|
|
32
46
|
function runNpmStep(cmd, cwd, env, label, timeoutSeconds, seams) {
|
|
33
|
-
const spawn = seams.spawnText ??
|
|
47
|
+
const spawn = seams.spawnText ?? spawnCommandText;
|
|
34
48
|
const head = cmd[0] ?? "";
|
|
35
49
|
const result = spawn(head, cmd.slice(1), {
|
|
36
50
|
cwd,
|
|
@@ -217,7 +231,7 @@ export function rehearseNpmInstallAndRun(cloneDir, version, seams = {}, options
|
|
|
217
231
|
[ok, reason] = runNpmStep(installArgs, consumerDir, env, "npm install packed tarballs", NPM_INSTALL_RUN_TIMEOUT_SECONDS, seams);
|
|
218
232
|
if (!ok)
|
|
219
233
|
return [false, reason];
|
|
220
|
-
const spawn = seams.spawnText ??
|
|
234
|
+
const spawn = seams.spawnText ?? spawnCommandText;
|
|
221
235
|
const cliBin = join(consumerDir, "node_modules", "@deftai", "directive", "dist", "bin.js");
|
|
222
236
|
// Liveness probe (#2010): `--version` loads the cli + core engine via
|
|
223
237
|
// engineInfo(), exercising the cross-package import path that #1993 broke,
|
|
@@ -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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SpawnResult } from "../release/types.js";
|
|
2
|
+
export interface ResolveCommandOnPathOptions {
|
|
3
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
4
|
+
readonly platform?: NodeJS.Platform;
|
|
5
|
+
readonly exists?: (path: string) => boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
|
|
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;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
|
|
16
|
+
* Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveCommandOnPath(command: string, options?: ResolveCommandOnPathOptions): string | null;
|
|
19
|
+
export interface SpawnCommandTextOptions {
|
|
20
|
+
readonly cwd?: string;
|
|
21
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
22
|
+
readonly timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* spawnSync wrapper that applies win32 PATHEXT / shell rules (#2467 / #2548).
|
|
26
|
+
* Retries with `shell: true` on win32 ENOENT (npm global `.cmd` shims).
|
|
27
|
+
*/
|
|
28
|
+
export declare function spawnCommandText(cmd: string, args: readonly string[], options?: SpawnCommandTextOptions): SpawnResult;
|
|
29
|
+
//# sourceMappingURL=command-spawn.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
5
|
+
/** Windows command shims (.cmd/.bat) need a shell; native executables do not. */
|
|
6
|
+
export function shouldUseShellForCommand(command, platform = process.platform) {
|
|
7
|
+
return platform === "win32" && /\.(?:cmd|bat)$/i.test(command);
|
|
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
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve an executable on PATH with PATHEXT / Path awareness (#2467 / #2548).
|
|
25
|
+
* Mirrors ts-check-lane `resolvePnpm` and verify-tools `defaultProbe`.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveCommandOnPath(command, options = {}) {
|
|
28
|
+
const env = options.env ?? process.env;
|
|
29
|
+
const platform = options.platform ?? process.platform;
|
|
30
|
+
const exists = options.exists ?? existsSync;
|
|
31
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
32
|
+
if (pathValue === "") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const isWindows = platform === "win32";
|
|
36
|
+
const exts = isWindows ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";") : [""];
|
|
37
|
+
const sep = isWindows ? ";" : ":";
|
|
38
|
+
const joinPath = isWindows ? win32.join : posix.join;
|
|
39
|
+
for (const dir of pathValue.split(sep)) {
|
|
40
|
+
if (dir === "")
|
|
41
|
+
continue;
|
|
42
|
+
for (const ext of exts) {
|
|
43
|
+
const candidate = joinPath(dir, `${command}${ext}`);
|
|
44
|
+
if (exists(candidate)) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* spawnSync wrapper that applies win32 PATHEXT / shell rules (#2467 / #2548).
|
|
53
|
+
* Retries with `shell: true` on win32 ENOENT (npm global `.cmd` shims).
|
|
54
|
+
*/
|
|
55
|
+
export function spawnCommandText(cmd, args, options = {}) {
|
|
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
|
+
};
|
|
70
|
+
let result = trySpawn(shouldUseShellForCommand(cmd));
|
|
71
|
+
const spawnErr = result.error;
|
|
72
|
+
if (spawnErr?.code === "ENOENT" && process.platform === "win32") {
|
|
73
|
+
result = trySpawn(true);
|
|
74
|
+
}
|
|
75
|
+
let status = result.status;
|
|
76
|
+
let stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
77
|
+
if (status === null) {
|
|
78
|
+
if (result.signal !== null && result.signal !== undefined) {
|
|
79
|
+
status = 128;
|
|
80
|
+
}
|
|
81
|
+
else if (result.error) {
|
|
82
|
+
status = 2;
|
|
83
|
+
if (stderr.trim().length === 0) {
|
|
84
|
+
stderr = result.error.message;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
status = 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status,
|
|
93
|
+
stdout: typeof result.stdout === "string" ? result.stdout : "",
|
|
94
|
+
stderr,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=command-spawn.js.map
|