@deftai/directive-core 0.74.0 → 0.75.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 +61 -0
- package/dist/agents-md-budget/evaluate.js +321 -19
- package/dist/agents-md-budget/index.d.ts +1 -0
- package/dist/agents-md-budget/index.js +1 -0
- package/dist/agents-md-budget/skill-frontmatter.d.ts +33 -0
- package/dist/agents-md-budget/skill-frontmatter.js +115 -0
- package/dist/content-contracts/skills/helpers.d.ts +0 -1
- package/dist/content-contracts/skills/helpers.js +1 -11
- package/dist/content-contracts/skills/skill-frontmatter.js +8 -1
- package/dist/content-contracts/standards/_helpers.js +4 -2
- package/dist/content-contracts/standards/_taskfile-helpers.d.ts +2 -0
- package/dist/content-contracts/standards/_taskfile-helpers.js +11 -5
- package/dist/doctor/doctor-state.js +2 -1
- package/dist/doctor/paths.js +2 -1
- package/dist/eval-health-relocation/evaluate.d.ts +71 -0
- package/dist/eval-health-relocation/evaluate.js +252 -0
- package/dist/eval-health-relocation/index.d.ts +2 -0
- package/dist/eval-health-relocation/index.js +2 -0
- package/dist/init-deposit/scaffold.js +7 -3
- package/dist/intake/issue-emit.js +2 -2
- package/dist/policy/agents-md-budget.d.ts +23 -0
- package/dist/policy/agents-md-budget.js +75 -4
- package/dist/release/spawn.js +5 -3
- package/dist/scope/decompose.js +4 -3
- package/dist/scope/vbrief-ref.js +3 -3
- package/dist/swarm/complete-cohort.js +6 -6
- package/dist/swarm/launch.js +2 -2
- package/dist/triage/queue/cache.d.ts +14 -0
- package/dist/triage/queue/cache.js +61 -1
- package/dist/validate-content/validate-links.js +2 -2
- package/dist/value/readback.d.ts +1 -0
- package/dist/value/readback.js +5 -1
- package/dist/verify-env/toolchain-check.js +20 -5
- package/dist/verify-env/verify-hooks-installed.d.ts +2 -0
- package/dist/verify-env/verify-hooks-installed.js +17 -15
- package/package.json +7 -3
|
@@ -27,10 +27,12 @@ export function resolveContentPath(relPath) {
|
|
|
27
27
|
return join(root, relPath);
|
|
28
28
|
}
|
|
29
29
|
export function readText(relPath) {
|
|
30
|
-
return readFileSync(resolveContentPath(relPath), { encoding: "utf8" })
|
|
30
|
+
return readFileSync(resolveContentPath(relPath), { encoding: "utf8" })
|
|
31
|
+
.replace(/\r\n/g, "\n")
|
|
32
|
+
.replace(/\r/g, "\n");
|
|
31
33
|
}
|
|
32
34
|
export function readAbs(absPath) {
|
|
33
|
-
return readFileSync(absPath, { encoding: "utf8" });
|
|
35
|
+
return readFileSync(absPath, { encoding: "utf8" }).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
34
36
|
}
|
|
35
37
|
export function pathExists(relPath) {
|
|
36
38
|
return existsSync(resolveContentPath(relPath));
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare function taskYamlFiles(): string[];
|
|
2
|
+
/** Normalize CRLF so Taskfile parsers do not treat lone `\r` lines as section exits (#2467). */
|
|
3
|
+
export declare function normalizeYamlNewlines(text: string): string;
|
|
2
4
|
export declare function iterTaskBlocks(text: string): Array<{
|
|
3
5
|
name: string;
|
|
4
6
|
start: number;
|
|
@@ -10,8 +10,12 @@ export function taskYamlFiles() {
|
|
|
10
10
|
.map((f) => join(dir, f))
|
|
11
11
|
.sort();
|
|
12
12
|
}
|
|
13
|
+
/** Normalize CRLF so Taskfile parsers do not treat lone `\r` lines as section exits (#2467). */
|
|
14
|
+
export function normalizeYamlNewlines(text) {
|
|
15
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
16
|
+
}
|
|
13
17
|
export function iterTaskBlocks(text) {
|
|
14
|
-
const lines = text.split("\n");
|
|
18
|
+
const lines = normalizeYamlNewlines(text).split("\n");
|
|
15
19
|
const taskPositions = [];
|
|
16
20
|
let inTasksSection = false;
|
|
17
21
|
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
@@ -43,19 +47,21 @@ export function iterTaskBlocks(text) {
|
|
|
43
47
|
return blocks;
|
|
44
48
|
}
|
|
45
49
|
export function blockBody(text, start, end) {
|
|
46
|
-
return text.split("\n").slice(start, end).join("\n");
|
|
50
|
+
return normalizeYamlNewlines(text).split("\n").slice(start, end).join("\n");
|
|
47
51
|
}
|
|
48
52
|
export function nonCommentLines(block) {
|
|
49
|
-
return block
|
|
53
|
+
return normalizeYamlNewlines(block)
|
|
54
|
+
.split("\n")
|
|
55
|
+
.filter((ln) => !ln.trimStart().startsWith("#"));
|
|
50
56
|
}
|
|
51
57
|
export function cachingKeyOnLine(line) {
|
|
52
58
|
const m = line.match(CACHING_KEY);
|
|
53
59
|
return m?.[1] ?? null;
|
|
54
60
|
}
|
|
55
61
|
export function readTaskfile(name) {
|
|
56
|
-
return readFileSync(join(repoRoot(), "tasks", name), { encoding: "utf8" });
|
|
62
|
+
return normalizeYamlNewlines(readFileSync(join(repoRoot(), "tasks", name), { encoding: "utf8" }));
|
|
57
63
|
}
|
|
58
64
|
export function readRoot(name) {
|
|
59
|
-
return readFileSync(join(repoRoot(), "tasks", "..", name), { encoding: "utf8" });
|
|
65
|
+
return normalizeYamlNewlines(readFileSync(join(repoRoot(), "tasks", "..", name), { encoding: "utf8" }));
|
|
60
66
|
}
|
|
61
67
|
//# sourceMappingURL=_taskfile-helpers.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { resolveTriageCachePath } from "../triage/cache-path.js";
|
|
4
5
|
import { CLEAN_WINDOW_HOURS, DIRTY_WINDOW_HOURS, ENV_STATE_PATH } from "./constants.js";
|
|
@@ -7,7 +8,7 @@ export function statePath(projectRoot) {
|
|
|
7
8
|
const override = process.env[ENV_STATE_PATH]?.trim();
|
|
8
9
|
if (override) {
|
|
9
10
|
return override.startsWith("~")
|
|
10
|
-
? join(
|
|
11
|
+
? join(homedir(), override.slice(1).replace(/^[\\/]/, ""))
|
|
11
12
|
: override;
|
|
12
13
|
}
|
|
13
14
|
return resolveTriageCachePath(projectRoot, STATE_FILENAME);
|
package/dist/doctor/paths.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { DEFT_REPO_POSITIVE_MARKERS } from "./constants.js";
|
|
@@ -8,7 +9,7 @@ export function resolvePath(pathStr, cwd = process.cwd()) {
|
|
|
8
9
|
return cwd;
|
|
9
10
|
}
|
|
10
11
|
const expanded = pathStr.startsWith("~")
|
|
11
|
-
? join(
|
|
12
|
+
? join(homedir(), pathStr.slice(1).replace(/^[\\/]/, ""))
|
|
12
13
|
: pathStr;
|
|
13
14
|
return resolve(cwd, expanded);
|
|
14
15
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type HealthReport } from "../eval/health.js";
|
|
2
|
+
export type OutputStream = "stdout" | "stderr" | "none";
|
|
3
|
+
/** Glob patterns that classify a change as epic #2369 rule-relocation (#2373). */
|
|
4
|
+
export declare const RULE_RELOCATION_PATH_PATTERNS: readonly ["AGENTS.md", "content/templates/agents-entry.md", "content/skills/**/SKILL.md", "content/packs/**"];
|
|
5
|
+
/** Committed no-regression baseline relative to `xbrief/.eval/`. */
|
|
6
|
+
export declare const HEALTH_BASELINE_REL = "results/eval-health-baseline.json";
|
|
7
|
+
/** Result of verify:eval-health-relocation; three-state exit contract. */
|
|
8
|
+
export interface EvaluateResult {
|
|
9
|
+
readonly code: 0 | 1 | 2;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly stream: OutputStream;
|
|
12
|
+
/** True when the diff did not touch rule-relocation paths (gate skipped). */
|
|
13
|
+
readonly skipped?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface EvaluateOptions {
|
|
16
|
+
readonly projectRoot?: string;
|
|
17
|
+
/** Git ref for `git diff --name-only` (CI / branch comparison). */
|
|
18
|
+
readonly baseRef?: string;
|
|
19
|
+
/** Use `git diff --cached --name-only` (pre-commit / staged). */
|
|
20
|
+
readonly staged?: boolean;
|
|
21
|
+
/** Explicit path list (tests / overrides). */
|
|
22
|
+
readonly paths?: readonly string[];
|
|
23
|
+
readonly quiet?: boolean;
|
|
24
|
+
/** Write the current eval:health report as the committed baseline. */
|
|
25
|
+
readonly seedBaseline?: boolean;
|
|
26
|
+
/** Test hook: override eval:health collection. */
|
|
27
|
+
readonly healthEvaluator?: (projectRoot: string) => HealthReport | null;
|
|
28
|
+
}
|
|
29
|
+
/** Validate a parsed baseline JSON object before use. */
|
|
30
|
+
export declare function parseHealthBaseline(parsed: unknown): HealthReport | null;
|
|
31
|
+
/** True when *path* matches a rule-relocation home. */
|
|
32
|
+
export declare function isRuleRelocationPath(path: string): boolean;
|
|
33
|
+
/** Classify a path list for rule-relocation coverage. */
|
|
34
|
+
export declare function classifyRuleRelocationPaths(paths: readonly string[]): {
|
|
35
|
+
readonly isRelocation: boolean;
|
|
36
|
+
readonly matchedPaths: readonly string[];
|
|
37
|
+
};
|
|
38
|
+
/** Absolute path to the committed eval-health baseline snapshot. */
|
|
39
|
+
export declare function healthBaselinePath(projectRoot: string): string;
|
|
40
|
+
/** Read the committed baseline snapshot, if present. */
|
|
41
|
+
export declare function readHealthBaseline(projectRoot: string): HealthReport | null;
|
|
42
|
+
/** Persist a health report as the committed baseline (#2373 Wave 2 seed). */
|
|
43
|
+
export declare function writeHealthBaseline(projectRoot: string, report: HealthReport): void;
|
|
44
|
+
/** Collect changed paths from git (base ref or staged index). */
|
|
45
|
+
export declare function collectChangedPaths(projectRoot: string, options: {
|
|
46
|
+
baseRef?: string;
|
|
47
|
+
staged?: boolean;
|
|
48
|
+
}): string[] | {
|
|
49
|
+
error: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Detect eval:health regression against a committed baseline.
|
|
53
|
+
*
|
|
54
|
+
* Fail-closed rules (#2373):
|
|
55
|
+
* - score must not drop below baseline
|
|
56
|
+
* - gates that passed in the baseline must still pass
|
|
57
|
+
* - no new contradictory-gate ids beyond the baseline set
|
|
58
|
+
*/
|
|
59
|
+
export declare function detectHealthRegression(current: HealthReport, baseline: HealthReport): {
|
|
60
|
+
pass: boolean;
|
|
61
|
+
reasons: readonly string[];
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Conditional fail-closed gate for epic #2369 rule-relocation PRs (#2373).
|
|
65
|
+
*
|
|
66
|
+
* Skips (exit 0) when the diff does not touch relocation homes. When relocation
|
|
67
|
+
* paths are present, requires eval:health no-regression against the committed
|
|
68
|
+
* baseline at `xbrief/.eval/results/eval-health-baseline.json`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function evaluate(options?: EvaluateOptions): EvaluateResult;
|
|
71
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { evaluateHealth } from "../eval/health.js";
|
|
5
|
+
import { resolveEvalPath } from "../layout/resolve.js";
|
|
6
|
+
import { matchAny } from "../orchestration/pathspec.js";
|
|
7
|
+
/** Glob patterns that classify a change as epic #2369 rule-relocation (#2373). */
|
|
8
|
+
export const RULE_RELOCATION_PATH_PATTERNS = [
|
|
9
|
+
"AGENTS.md",
|
|
10
|
+
"content/templates/agents-entry.md",
|
|
11
|
+
"content/skills/**/SKILL.md",
|
|
12
|
+
"content/packs/**",
|
|
13
|
+
];
|
|
14
|
+
/** Committed no-regression baseline relative to `xbrief/.eval/`. */
|
|
15
|
+
export const HEALTH_BASELINE_REL = "results/eval-health-baseline.json";
|
|
16
|
+
/** Validate a parsed baseline JSON object before use. */
|
|
17
|
+
export function parseHealthBaseline(parsed) {
|
|
18
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const record = parsed;
|
|
22
|
+
if (typeof record.score !== "number" || !Array.isArray(record.gates)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(record.contradictions)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
/** True when *path* matches a rule-relocation home. */
|
|
31
|
+
export function isRuleRelocationPath(path) {
|
|
32
|
+
return matchAny(RULE_RELOCATION_PATH_PATTERNS, path);
|
|
33
|
+
}
|
|
34
|
+
/** Classify a path list for rule-relocation coverage. */
|
|
35
|
+
export function classifyRuleRelocationPaths(paths) {
|
|
36
|
+
const matchedPaths = paths.filter(isRuleRelocationPath);
|
|
37
|
+
return { isRelocation: matchedPaths.length > 0, matchedPaths };
|
|
38
|
+
}
|
|
39
|
+
/** Absolute path to the committed eval-health baseline snapshot. */
|
|
40
|
+
export function healthBaselinePath(projectRoot) {
|
|
41
|
+
return resolveEvalPath(projectRoot, HEALTH_BASELINE_REL);
|
|
42
|
+
}
|
|
43
|
+
/** Read the committed baseline snapshot, if present. */
|
|
44
|
+
export function readHealthBaseline(projectRoot) {
|
|
45
|
+
const path = healthBaselinePath(projectRoot);
|
|
46
|
+
if (!existsSync(path)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
+
return parseHealthBaseline(parsed);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Persist a health report as the committed baseline (#2373 Wave 2 seed). */
|
|
58
|
+
export function writeHealthBaseline(projectRoot, report) {
|
|
59
|
+
const path = healthBaselinePath(projectRoot);
|
|
60
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
61
|
+
writeFileSync(path, `${JSON.stringify(report)}\n`, "utf8");
|
|
62
|
+
}
|
|
63
|
+
function gitNameOnlyDiff(projectRoot, args) {
|
|
64
|
+
try {
|
|
65
|
+
const stdout = execFileSync("git", ["-C", projectRoot, "diff", "--name-only", ...args], {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
68
|
+
});
|
|
69
|
+
return stdout
|
|
70
|
+
.split("\n")
|
|
71
|
+
.map((line) => line.trim())
|
|
72
|
+
.filter((line) => line.length > 0);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return { error: String(err) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Collect changed paths from git (base ref or staged index). */
|
|
79
|
+
export function collectChangedPaths(projectRoot, options) {
|
|
80
|
+
if (options.staged) {
|
|
81
|
+
return gitNameOnlyDiff(projectRoot, ["--cached"]);
|
|
82
|
+
}
|
|
83
|
+
if (options.baseRef !== undefined && options.baseRef.length > 0) {
|
|
84
|
+
return gitNameOnlyDiff(projectRoot, [options.baseRef, "HEAD"]);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Detect eval:health regression against a committed baseline.
|
|
90
|
+
*
|
|
91
|
+
* Fail-closed rules (#2373):
|
|
92
|
+
* - score must not drop below baseline
|
|
93
|
+
* - gates that passed in the baseline must still pass
|
|
94
|
+
* - no new contradictory-gate ids beyond the baseline set
|
|
95
|
+
*/
|
|
96
|
+
export function detectHealthRegression(current, baseline) {
|
|
97
|
+
const reasons = [];
|
|
98
|
+
if (current.score < baseline.score) {
|
|
99
|
+
reasons.push(`framework health score dropped ${baseline.score}->${current.score}`);
|
|
100
|
+
}
|
|
101
|
+
const baselineGates = new Map(baseline.gates.filter((gate) => !gate.skipped).map((gate) => [gate.id, gate]));
|
|
102
|
+
for (const [id, baseGate] of baselineGates) {
|
|
103
|
+
if (!baseGate.pass) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const currentGate = current.gates.find((gate) => gate.id === id && !gate.skipped);
|
|
107
|
+
if (currentGate === undefined) {
|
|
108
|
+
reasons.push(`gate '${id}' missing from current eval:health report`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!currentGate.pass) {
|
|
112
|
+
reasons.push(`gate '${id}' regressed (baseline pass -> current fail)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const baselineContradictionIds = new Set(baseline.contradictions.map((c) => c.id));
|
|
116
|
+
for (const contradiction of current.contradictions) {
|
|
117
|
+
if (!baselineContradictionIds.has(contradiction.id)) {
|
|
118
|
+
reasons.push(`new contradictory gate '${contradiction.id}'`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { pass: reasons.length === 0, reasons };
|
|
122
|
+
}
|
|
123
|
+
function formatSkipMessage(matchedPaths) {
|
|
124
|
+
return ("✓ verify:eval-health-relocation: no rule-relocation paths in diff " +
|
|
125
|
+
`(checked ${RULE_RELOCATION_PATH_PATTERNS.length} patterns; ` +
|
|
126
|
+
`diff had ${matchedPaths.length} relocation match${matchedPaths.length === 1 ? "" : "es"}).`);
|
|
127
|
+
}
|
|
128
|
+
function formatPassMessage(score, matchedPaths) {
|
|
129
|
+
return (`✓ verify:eval-health-relocation: eval:health no-regression OK ` +
|
|
130
|
+
`(score=${score}/100; relocation paths: ${matchedPaths.join(", ")}).`);
|
|
131
|
+
}
|
|
132
|
+
function formatRegressionMessage(reasons, matchedPaths, baseline, current) {
|
|
133
|
+
const detail = reasons.map((reason) => ` - ${reason}`).join("\n");
|
|
134
|
+
return ("❌ verify:eval-health-relocation: eval:health regression on rule-relocation PR " +
|
|
135
|
+
`(#2373 / epic #2369).\n` +
|
|
136
|
+
` Baseline score: ${baseline.score}/100; current: ${current.score}/100.\n` +
|
|
137
|
+
` Matched relocation paths: ${matchedPaths.join(", ")}\n` +
|
|
138
|
+
`${detail}\n` +
|
|
139
|
+
" Remediation: restore gate pass states / score, or bump the committed baseline\n" +
|
|
140
|
+
` at ${HEALTH_BASELINE_REL} only after a deliberate, reviewed health change.\n` +
|
|
141
|
+
" Run `task eval:health` for the full gate breakdown.");
|
|
142
|
+
}
|
|
143
|
+
function runHealthEvaluator(projectRoot, healthEvaluator) {
|
|
144
|
+
if (healthEvaluator !== undefined) {
|
|
145
|
+
return healthEvaluator(projectRoot);
|
|
146
|
+
}
|
|
147
|
+
const healthResult = evaluateHealth({
|
|
148
|
+
projectRoot,
|
|
149
|
+
persist: false,
|
|
150
|
+
frameworkSource: true,
|
|
151
|
+
});
|
|
152
|
+
return healthResult.report;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Conditional fail-closed gate for epic #2369 rule-relocation PRs (#2373).
|
|
156
|
+
*
|
|
157
|
+
* Skips (exit 0) when the diff does not touch relocation homes. When relocation
|
|
158
|
+
* paths are present, requires eval:health no-regression against the committed
|
|
159
|
+
* baseline at `xbrief/.eval/results/eval-health-baseline.json`.
|
|
160
|
+
*/
|
|
161
|
+
export function evaluate(options = {}) {
|
|
162
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
163
|
+
const quiet = options.quiet ?? false;
|
|
164
|
+
if (options.seedBaseline) {
|
|
165
|
+
const report = runHealthEvaluator(projectRoot, options.healthEvaluator);
|
|
166
|
+
if (report === null) {
|
|
167
|
+
return {
|
|
168
|
+
code: 2,
|
|
169
|
+
message: "❌ verify:eval-health-relocation: eval:health returned no report.",
|
|
170
|
+
stream: "stderr",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
writeHealthBaseline(projectRoot, report);
|
|
174
|
+
return {
|
|
175
|
+
code: 0,
|
|
176
|
+
message: `✓ verify:eval-health-relocation: seeded baseline at ${HEALTH_BASELINE_REL} ` +
|
|
177
|
+
`(score=${report.score}/100).`,
|
|
178
|
+
stream: "stdout",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
let paths;
|
|
182
|
+
if (options.paths !== undefined) {
|
|
183
|
+
paths = [...options.paths];
|
|
184
|
+
}
|
|
185
|
+
else if (options.baseRef !== undefined || options.staged) {
|
|
186
|
+
const collected = collectChangedPaths(projectRoot, {
|
|
187
|
+
baseRef: options.baseRef,
|
|
188
|
+
staged: options.staged,
|
|
189
|
+
});
|
|
190
|
+
if ("error" in collected) {
|
|
191
|
+
return {
|
|
192
|
+
code: 2,
|
|
193
|
+
message: "❌ verify:eval-health-relocation: could not read git diff " +
|
|
194
|
+
`(project_root=${projectRoot}): ${collected.error}`,
|
|
195
|
+
stream: "stderr",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
paths = collected;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
return { code: 0, message: "", stream: "none", skipped: true };
|
|
202
|
+
}
|
|
203
|
+
const { isRelocation, matchedPaths } = classifyRuleRelocationPaths(paths);
|
|
204
|
+
if (!isRelocation) {
|
|
205
|
+
if (quiet) {
|
|
206
|
+
return { code: 0, message: "", stream: "none", skipped: true };
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
code: 0,
|
|
210
|
+
message: formatSkipMessage(matchedPaths),
|
|
211
|
+
stream: "stdout",
|
|
212
|
+
skipped: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const current = runHealthEvaluator(projectRoot, options.healthEvaluator);
|
|
216
|
+
if (current === null) {
|
|
217
|
+
return {
|
|
218
|
+
code: 2,
|
|
219
|
+
message: "❌ verify:eval-health-relocation: eval:health returned no report.",
|
|
220
|
+
stream: "stderr",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const baseline = readHealthBaseline(projectRoot);
|
|
224
|
+
if (baseline === null) {
|
|
225
|
+
return {
|
|
226
|
+
code: 2,
|
|
227
|
+
message: "❌ verify:eval-health-relocation: missing committed baseline " +
|
|
228
|
+
`at ${HEALTH_BASELINE_REL}.\n` +
|
|
229
|
+
" Seed once with: task verify:eval-health-relocation -- --seed-baseline\n" +
|
|
230
|
+
" (after `task eval:health` is green on master). Baseline seeding is intentional\n" +
|
|
231
|
+
" and does not weaken verify:agents-md-budget.",
|
|
232
|
+
stream: "stderr",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const regression = detectHealthRegression(current, baseline);
|
|
236
|
+
if (!regression.pass) {
|
|
237
|
+
return {
|
|
238
|
+
code: 1,
|
|
239
|
+
message: formatRegressionMessage(regression.reasons, matchedPaths, baseline, current),
|
|
240
|
+
stream: "stderr",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (quiet) {
|
|
244
|
+
return { code: 0, message: "", stream: "none" };
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
code: 0,
|
|
248
|
+
message: formatPassMessage(current.score, matchedPaths),
|
|
249
|
+
stream: "stdout",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=evaluate.js.map
|
|
@@ -457,7 +457,9 @@ export function ensureTaskfile(projectDir, io) {
|
|
|
457
457
|
return true;
|
|
458
458
|
}
|
|
459
459
|
const HOOK_FILENAMES = ["pre-commit", "pre-push"];
|
|
460
|
+
const HOOK_SUPPORT_FILENAMES = ["_deft-run.sh"];
|
|
460
461
|
const HOOK_FILE_MODE = 0o755;
|
|
462
|
+
const HOOK_SUPPORT_FILE_MODE = 0o644;
|
|
461
463
|
export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
462
464
|
const srcDir = join(deftDir, ".githooks");
|
|
463
465
|
if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) {
|
|
@@ -467,18 +469,20 @@ export function writeConsumerGitHooks(projectDir, deftDir, io, seams = {}) {
|
|
|
467
469
|
const dstDir = join(projectDir, ".githooks");
|
|
468
470
|
mkdirSync(dstDir, { recursive: true });
|
|
469
471
|
let filesDeposited = false;
|
|
470
|
-
for (const name of HOOK_FILENAMES) {
|
|
472
|
+
for (const name of [...HOOK_FILENAMES, ...HOOK_SUPPORT_FILENAMES]) {
|
|
471
473
|
const src = join(srcDir, name);
|
|
472
474
|
if (!existsSync(src))
|
|
473
475
|
continue;
|
|
474
476
|
const data = readFileSync(src);
|
|
475
477
|
const dst = join(dstDir, name);
|
|
476
478
|
const existing = existsSync(dst) ? readFileSync(dst) : null;
|
|
479
|
+
const isHookScript = HOOK_FILENAMES.includes(name);
|
|
477
480
|
if (!existing?.equals(data)) {
|
|
478
|
-
|
|
481
|
+
const mode = isHookScript ? HOOK_FILE_MODE : HOOK_SUPPORT_FILE_MODE;
|
|
482
|
+
writeFileSync(dst, data, { mode });
|
|
479
483
|
filesDeposited = true;
|
|
480
484
|
}
|
|
481
|
-
if (platform() !== "win32") {
|
|
485
|
+
if (platform() !== "win32" && isHookScript) {
|
|
482
486
|
try {
|
|
483
487
|
const mode = statSync(dst).mode & 0o777;
|
|
484
488
|
if ((mode & 0o111) === 0) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
5
5
|
import { call } from "../scm/call.js";
|
|
6
6
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
@@ -217,7 +217,7 @@ export function expandPatterns(patterns, root = null) {
|
|
|
217
217
|
const seen = new Set();
|
|
218
218
|
const out = [];
|
|
219
219
|
for (const pattern of patterns) {
|
|
220
|
-
const candidate = root !== null && !pattern
|
|
220
|
+
const candidate = root !== null && !isAbsolute(pattern) ? join(root, pattern) : pattern;
|
|
221
221
|
let matches = globSync(candidate).sort();
|
|
222
222
|
if (matches.length === 0) {
|
|
223
223
|
try {
|
|
@@ -9,9 +9,32 @@
|
|
|
9
9
|
* it is an explicit, reviewed diff to this typed field -- that diff IS the
|
|
10
10
|
* "was this growth deliberate?" checkpoint.
|
|
11
11
|
*/
|
|
12
|
+
export type HarnessProfile = "cursor" | "none";
|
|
13
|
+
export type SkillFrontmatterTier = "daily-core" | "all" | "none";
|
|
12
14
|
export interface AgentsMdBudget {
|
|
13
15
|
readonly managedMaxLines: number;
|
|
14
16
|
readonly unmanagedMaxLines: number;
|
|
17
|
+
/**
|
|
18
|
+
* Fail-closed UTF-8 byte ratchet for the managed section (#2452).
|
|
19
|
+
* Seeded at the current measured size so the gate ships green; growth past
|
|
20
|
+
* this ceiling fails. The <=8192 B north-star is reported separately.
|
|
21
|
+
*/
|
|
22
|
+
readonly absoluteMaxBytes?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Harness profile for DD-3 skill-frontmatter measurement (#2463).
|
|
25
|
+
* When unset, the gate auto-detects `cursor` when `content/skills/` exists.
|
|
26
|
+
*/
|
|
27
|
+
readonly harnessProfile?: HarnessProfile;
|
|
28
|
+
/**
|
|
29
|
+
* Skill tier for DD-3 frontmatter caps (`daily-core` vs `all` vs `none`).
|
|
30
|
+
* Defaults to `all` for reporting when unset.
|
|
31
|
+
*/
|
|
32
|
+
readonly skillFrontmatterTier?: SkillFrontmatterTier;
|
|
33
|
+
/**
|
|
34
|
+
* Optional fail-closed ratchet for DD-3 skill-frontmatter bytes (#2463).
|
|
35
|
+
* Advisory when unset; growth past this ceiling fails when set.
|
|
36
|
+
*/
|
|
37
|
+
readonly skillFrontmatterMaxBytes?: number;
|
|
15
38
|
}
|
|
16
39
|
export type AgentsMdBudgetSource = "typed" | "unset" | "default-on-error";
|
|
17
40
|
export interface AgentsMdBudgetResult {
|
|
@@ -39,6 +39,19 @@ function readRegion(block, key) {
|
|
|
39
39
|
}
|
|
40
40
|
return { value: raw, error: null };
|
|
41
41
|
}
|
|
42
|
+
function readOptionalRegion(block, key) {
|
|
43
|
+
if (!(key in block)) {
|
|
44
|
+
return { value: undefined, error: null };
|
|
45
|
+
}
|
|
46
|
+
const raw = block[key];
|
|
47
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
48
|
+
return {
|
|
49
|
+
value: undefined,
|
|
50
|
+
error: `plan.policy.agentsMdBudget.${key} must be a non-negative integer; got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { value: raw, error: null };
|
|
54
|
+
}
|
|
42
55
|
/** Resolve plan.policy.agentsMdBudget from PROJECT-DEFINITION (#645). */
|
|
43
56
|
export function resolveAgentsMdBudget(projectRoot) {
|
|
44
57
|
const [data, err] = loadProjectDefinition(projectRoot);
|
|
@@ -77,13 +90,71 @@ export function resolveAgentsMdBudget(projectRoot) {
|
|
|
77
90
|
if (unmanaged.error !== null) {
|
|
78
91
|
return { budget: null, source: "default-on-error", error: unmanaged.error };
|
|
79
92
|
}
|
|
93
|
+
const absolute = readOptionalRegion(block, "absoluteMaxBytes");
|
|
94
|
+
if (absolute.error !== null) {
|
|
95
|
+
return { budget: null, source: "default-on-error", error: absolute.error };
|
|
96
|
+
}
|
|
97
|
+
const skillFrontmatterMax = readOptionalRegion(block, "skillFrontmatterMaxBytes");
|
|
98
|
+
if (skillFrontmatterMax.error !== null) {
|
|
99
|
+
return { budget: null, source: "default-on-error", error: skillFrontmatterMax.error };
|
|
100
|
+
}
|
|
101
|
+
const harnessProfile = readOptionalHarnessProfile(block, "harnessProfile");
|
|
102
|
+
if (harnessProfile.error !== null) {
|
|
103
|
+
return { budget: null, source: "default-on-error", error: harnessProfile.error };
|
|
104
|
+
}
|
|
105
|
+
const skillFrontmatterTier = readOptionalSkillTier(block, "skillFrontmatterTier");
|
|
106
|
+
if (skillFrontmatterTier.error !== null) {
|
|
107
|
+
return { budget: null, source: "default-on-error", error: skillFrontmatterTier.error };
|
|
108
|
+
}
|
|
109
|
+
const budget = {
|
|
110
|
+
managedMaxLines: managed.value,
|
|
111
|
+
unmanagedMaxLines: unmanaged.value,
|
|
112
|
+
};
|
|
113
|
+
const withOptional = {
|
|
114
|
+
...budget,
|
|
115
|
+
...(absolute.value !== undefined ? { absoluteMaxBytes: absolute.value } : {}),
|
|
116
|
+
...(skillFrontmatterMax.value !== undefined
|
|
117
|
+
? { skillFrontmatterMaxBytes: skillFrontmatterMax.value }
|
|
118
|
+
: {}),
|
|
119
|
+
...(harnessProfile.value !== undefined ? { harnessProfile: harnessProfile.value } : {}),
|
|
120
|
+
...(skillFrontmatterTier.value !== undefined
|
|
121
|
+
? { skillFrontmatterTier: skillFrontmatterTier.value }
|
|
122
|
+
: {}),
|
|
123
|
+
};
|
|
80
124
|
return {
|
|
81
|
-
budget:
|
|
82
|
-
managedMaxLines: managed.value,
|
|
83
|
-
unmanagedMaxLines: unmanaged.value,
|
|
84
|
-
},
|
|
125
|
+
budget: withOptional,
|
|
85
126
|
source: "typed",
|
|
86
127
|
error: null,
|
|
87
128
|
};
|
|
88
129
|
}
|
|
130
|
+
const HARNESS_PROFILES = new Set(["cursor", "none"]);
|
|
131
|
+
const SKILL_FRONTMATTER_TIERS = new Set(["daily-core", "all", "none"]);
|
|
132
|
+
function readOptionalHarnessProfile(block, key) {
|
|
133
|
+
if (!(key in block)) {
|
|
134
|
+
return { value: undefined, error: null };
|
|
135
|
+
}
|
|
136
|
+
const raw = block[key];
|
|
137
|
+
if (typeof raw !== "string" || !HARNESS_PROFILES.has(raw)) {
|
|
138
|
+
return {
|
|
139
|
+
value: undefined,
|
|
140
|
+
error: `plan.policy.agentsMdBudget.${key} must be one of ${[...HARNESS_PROFILES].join(", ")}; ` +
|
|
141
|
+
`got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { value: raw, error: null };
|
|
145
|
+
}
|
|
146
|
+
function readOptionalSkillTier(block, key) {
|
|
147
|
+
if (!(key in block)) {
|
|
148
|
+
return { value: undefined, error: null };
|
|
149
|
+
}
|
|
150
|
+
const raw = block[key];
|
|
151
|
+
if (typeof raw !== "string" || !SKILL_FRONTMATTER_TIERS.has(raw)) {
|
|
152
|
+
return {
|
|
153
|
+
value: undefined,
|
|
154
|
+
error: `plan.policy.agentsMdBudget.${key} must be one of ${[...SKILL_FRONTMATTER_TIERS].join(", ")}; ` +
|
|
155
|
+
`got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { value: raw, error: null };
|
|
159
|
+
}
|
|
89
160
|
//# sourceMappingURL=agents-md-budget.js.map
|
package/dist/release/spawn.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
|
|
3
3
|
export function defaultWhich(name) {
|
|
4
|
-
const
|
|
4
|
+
const locator = process.platform === "win32" ? "where" : "which";
|
|
5
|
+
const result = spawnSync(locator, [name], { encoding: "utf8" });
|
|
5
6
|
if (result.status !== 0) {
|
|
6
7
|
return null;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// `where` on Windows may return multiple lines; take the first non-empty match.
|
|
10
|
+
const first = (result.stdout ?? "").split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
11
|
+
return first ? first.trim() : null;
|
|
10
12
|
}
|
|
11
13
|
export function spawnText(cmd, args, options = {}) {
|
|
12
14
|
const result = spawnSync(cmd, [...args], {
|
package/dist/scope/decompose.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* that draft, writes the child story vBRIEFs, and updates the parent scope references.
|
|
8
8
|
*/
|
|
9
9
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
11
11
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
12
12
|
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleRoot, } from "../layout/resolve.js";
|
|
13
13
|
import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
|
|
@@ -415,7 +415,7 @@ function vbriefDir(projectRoot) {
|
|
|
415
415
|
function relToVbrief(vbriefDirPath, path) {
|
|
416
416
|
const resolvedPath = resolve(path);
|
|
417
417
|
const resolvedVbrief = resolve(vbriefDirPath);
|
|
418
|
-
if (resolvedPath.startsWith(
|
|
418
|
+
if (resolvedPath.startsWith(resolvedVbrief + sep) || resolvedPath === resolvedVbrief) {
|
|
419
419
|
return resolvedPath.slice(resolvedVbrief.length + 1).replace(/\\/g, "/");
|
|
420
420
|
}
|
|
421
421
|
throw new DecompositionError(`${path}: path must be inside ${vbriefDirPath}`);
|
|
@@ -823,7 +823,8 @@ export function applyDecomposition(opts) {
|
|
|
823
823
|
if (!LIFECYCLE_FOLDERS.has(outputFolderName)) {
|
|
824
824
|
throw new DecompositionError("output_dir must be a vbrief lifecycle folder");
|
|
825
825
|
}
|
|
826
|
-
|
|
826
|
+
const resolvedVbriefDir = resolve(vbriefDirPath);
|
|
827
|
+
if (!outputDir.startsWith(resolvedVbriefDir + sep) && outputDir !== resolvedVbriefDir) {
|
|
827
828
|
throw new DecompositionError("output_dir must be inside vbrief/");
|
|
828
829
|
}
|
|
829
830
|
if (outputFolderName === "active") {
|