@deftai/directive-core 0.77.0 → 0.79.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/cache/empty-populate.d.ts +44 -0
- package/dist/cache/empty-populate.js +117 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/check-updates/index.d.ts +10 -0
- package/dist/check-updates/index.js +81 -1
- package/dist/doctor/main.d.ts +1 -0
- package/dist/doctor/main.js +48 -0
- package/dist/doctor/payload-staleness.js +7 -1
- 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 +173 -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 +238 -0
- package/dist/init-deposit/hygiene.js +4 -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 +4 -0
- package/dist/init-deposit/prettierignore.d.ts +21 -0
- package/dist/init-deposit/prettierignore.js +83 -0
- package/dist/init-deposit/refresh.d.ts +3 -1
- package/dist/init-deposit/refresh.js +31 -40
- package/dist/init-deposit/scaffold.js +6 -12
- package/dist/init-deposit/xbrief-projections.d.ts +18 -0
- package/dist/init-deposit/xbrief-projections.js +117 -0
- package/dist/intake/reconcile-issues.d.ts +11 -0
- package/dist/intake/reconcile-issues.js +166 -51
- package/dist/issue-sync/index.d.ts +2 -0
- package/dist/issue-sync/index.js +2 -0
- package/dist/issue-sync/sync-from-xbrief-cli.d.ts +11 -0
- package/dist/issue-sync/sync-from-xbrief-cli.js +44 -0
- package/dist/issue-sync/sync-from-xbrief.d.ts +46 -0
- package/dist/issue-sync/sync-from-xbrief.js +208 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +5 -0
- package/dist/platform/platform-capabilities.js +9 -0
- package/dist/platform/shell-context.d.ts +30 -0
- package/dist/platform/shell-context.js +111 -0
- package/dist/pr-monitor/cadence.d.ts +6 -0
- package/dist/pr-monitor/cadence.js +13 -0
- package/dist/pr-monitor/index.d.ts +1 -1
- package/dist/pr-monitor/index.js +1 -1
- package/dist/pr-monitor/monitor.d.ts +7 -0
- package/dist/pr-monitor/monitor.js +59 -13
- package/dist/preflight-cache/evaluate.d.ts +3 -8
- package/dist/preflight-cache/evaluate.js +19 -19
- package/dist/release/build-dist.js +1 -0
- package/dist/release/constants.d.ts +2 -0
- package/dist/release/constants.js +8 -1
- package/dist/release/flags.js +26 -0
- package/dist/release/issue-state-fetch.d.ts +27 -0
- package/dist/release/issue-state-fetch.js +146 -0
- package/dist/release/main.js +1 -0
- package/dist/release/native-steps.js +6 -4
- package/dist/release/pipeline.js +15 -5
- package/dist/release/preflight.d.ts +6 -2
- package/dist/release/preflight.js +19 -5
- package/dist/release/types.d.ts +3 -1
- package/dist/release-e2e/constants.js +6 -6
- package/dist/release-e2e/flags.d.ts +2 -0
- package/dist/release-e2e/flags.js +8 -4
- 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/main.js +15 -9
- package/dist/release-e2e/types.d.ts +4 -2
- package/dist/resolution/classify.d.ts +1 -1
- package/dist/resolution/classify.js +36 -11
- package/dist/scope/project-definition-sync.js +2 -150
- package/dist/scope/registry-artifact-sync.d.ts +3 -0
- package/dist/scope/registry-artifact-sync.js +159 -0
- package/dist/scope/specification-sync.d.ts +3 -0
- package/dist/scope/specification-sync.js +16 -0
- package/dist/scope/transition.js +40 -12
- package/dist/session/cache-recovery.d.ts +19 -0
- package/dist/session/cache-recovery.js +26 -0
- package/dist/session/session-start.d.ts +2 -0
- package/dist/session/session-start.js +14 -3
- package/dist/session/verify-session-ritual.d.ts +17 -0
- package/dist/session/verify-session-ritual.js +64 -0
- package/dist/triage/help/registry-data.d.ts +12 -1
- package/dist/triage/help/registry-data.js +22 -1
- package/dist/triage/welcome/default-mode.js +2 -0
- package/dist/triage/welcome/onboard.js +2 -0
- package/dist/ts-check-lane/run-lane.d.ts +5 -0
- package/dist/ts-check-lane/run-lane.js +9 -0
- package/dist/vbrief-reconcile/labels.d.ts +3 -0
- package/dist/vbrief-reconcile/labels.js +14 -0
- package/dist/vbrief-reconcile/main.d.ts +1 -0
- package/dist/vbrief-reconcile/main.js +13 -3
- package/dist/vbrief-reconcile/parity-scenarios.js +18 -4
- package/dist/vbrief-reconcile/repo-guard.d.ts +17 -0
- package/dist/vbrief-reconcile/repo-guard.js +33 -0
- package/dist/vbrief-reconcile/umbrellas.d.ts +3 -0
- package/dist/vbrief-reconcile/umbrellas.js +17 -1
- package/dist/verify-env/agent-hooks.d.ts +11 -0
- package/dist/verify-env/agent-hooks.js +46 -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/dist/verify-env/verify-hooks-installed.js +5 -5
- package/dist/vitest-runner/coverage-debt-teardown.d.ts +3 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +33 -0
- package/dist/vitest-runner/coverage-debt.d.ts +50 -0
- package/dist/vitest-runner/coverage-debt.js +155 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +12 -0
- package/dist/vitest-runner/win32-coverage-tmp-setup.js +24 -0
- package/dist/xbrief-migrate/detect.js +46 -1
- package/dist/xbrief-migrate/migrate-project.js +19 -4
- package/dist/xbrief-migrate/migration-containment.d.ts +6 -0
- package/dist/xbrief-migrate/migration-containment.js +37 -0
- package/package.json +19 -3
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
/** Relative path to skill-pi-trigger-eval compatible case rows (#1586). */
|
|
4
|
+
export const TRIGGER_CASES_REL = "evals/trigger-cases.jsonl";
|
|
5
|
+
/** Default Skills Index source for AGENTS.md skill routing (#1586). */
|
|
6
|
+
export const SKILLS_INDEX_REL = "REFERENCES.md";
|
|
7
|
+
/** Normalize user text for deterministic substring trigger matching. */
|
|
8
|
+
export function normalizeTriggerText(text) {
|
|
9
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
10
|
+
}
|
|
11
|
+
/** Collapse embedded newlines before interpolating user/data into log lines. */
|
|
12
|
+
export function sanitizeTriggerLogLine(text) {
|
|
13
|
+
return text.replace(/\r?\n/g, " ").trim();
|
|
14
|
+
}
|
|
15
|
+
/** Extract backtick trigger phrases from a Skills Index triggers cell. */
|
|
16
|
+
export function parseTriggerCell(cell) {
|
|
17
|
+
const triggers = [];
|
|
18
|
+
for (const match of cell.matchAll(/`([^`]+)`/g)) {
|
|
19
|
+
const phrase = normalizeTriggerText(match[1] ?? "");
|
|
20
|
+
if (phrase.length > 0) {
|
|
21
|
+
triggers.push(phrase);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return triggers;
|
|
25
|
+
}
|
|
26
|
+
/** Parse the Skills Index markdown table in REFERENCES.md. */
|
|
27
|
+
export function parseSkillsIndex(markdown) {
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
30
|
+
if (!line.startsWith("| [deft-directive-")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const columns = line.split("|").map((col) => col.trim());
|
|
34
|
+
if (columns.length < 5) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const linkCell = columns[1] ?? "";
|
|
38
|
+
const triggersCell = columns[3] ?? "";
|
|
39
|
+
// Avoid nested character-class regex (CodeQL js/polynomial-redos on \[([^\]]+)\]).
|
|
40
|
+
const open = linkCell.indexOf("[");
|
|
41
|
+
const close = open === -1 ? -1 : linkCell.indexOf("]", open + 1);
|
|
42
|
+
if (open === -1 || close === -1) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const skillId = linkCell.slice(open + 1, close).trim();
|
|
46
|
+
if (skillId.length === 0) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const triggers = parseTriggerCell(triggersCell);
|
|
50
|
+
if (triggers.length === 0) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
entries.push({ skillId, triggers });
|
|
54
|
+
}
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
/** Find the winning skill for a query using longest-trigger-wins disambiguation. */
|
|
58
|
+
export function resolveTriggerWinner(query, index) {
|
|
59
|
+
const normalized = normalizeTriggerText(query);
|
|
60
|
+
let best = null;
|
|
61
|
+
for (const entry of index) {
|
|
62
|
+
for (const trigger of entry.triggers) {
|
|
63
|
+
if (!normalized.includes(trigger)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (best === null ||
|
|
67
|
+
trigger.length > best.trigger.length ||
|
|
68
|
+
(trigger.length === best.trigger.length && entry.skillId.localeCompare(best.skillId) < 0)) {
|
|
69
|
+
best = { skillId: entry.skillId, trigger };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return best;
|
|
74
|
+
}
|
|
75
|
+
/** True when the Skills Index routing would send *query* to *skillId*. */
|
|
76
|
+
export function wouldRouteToSkill(query, skillId, index) {
|
|
77
|
+
const winner = resolveTriggerWinner(query, index);
|
|
78
|
+
return winner !== null && winner.skillId === skillId;
|
|
79
|
+
}
|
|
80
|
+
/** Validate one JSONL row against the skill-pi-trigger-eval row contract. */
|
|
81
|
+
export function parseTriggerCaseRow(raw, lineNumber) {
|
|
82
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
83
|
+
return `line ${lineNumber}: expected a JSON object`;
|
|
84
|
+
}
|
|
85
|
+
const record = raw;
|
|
86
|
+
const id = record.id;
|
|
87
|
+
const query = record.query;
|
|
88
|
+
const skill = record.skill;
|
|
89
|
+
const shouldTrigger = record.should_trigger;
|
|
90
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
91
|
+
return `line ${lineNumber}: id must be a non-empty string`;
|
|
92
|
+
}
|
|
93
|
+
if (typeof query !== "string" || query.trim().length === 0) {
|
|
94
|
+
return `line ${lineNumber}: query must be a non-empty string`;
|
|
95
|
+
}
|
|
96
|
+
if (typeof skill !== "string" || !skill.startsWith("deft-directive-")) {
|
|
97
|
+
return `line ${lineNumber}: skill must be a deft-directive-* id`;
|
|
98
|
+
}
|
|
99
|
+
if (typeof shouldTrigger !== "boolean") {
|
|
100
|
+
return `line ${lineNumber}: should_trigger must be true or false`;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
id: id.trim(),
|
|
104
|
+
query: query.trim(),
|
|
105
|
+
skill: skill.trim(),
|
|
106
|
+
should_trigger: shouldTrigger,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Load evals/trigger-cases.jsonl. */
|
|
110
|
+
export function loadTriggerCases(path) {
|
|
111
|
+
if (!existsSync(path)) {
|
|
112
|
+
return { error: `missing trigger cases at ${path}` };
|
|
113
|
+
}
|
|
114
|
+
const text = readFileSync(path, "utf8");
|
|
115
|
+
const rows = [];
|
|
116
|
+
const lines = text.split(/\r?\n/);
|
|
117
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
118
|
+
const line = lines[i]?.trim() ?? "";
|
|
119
|
+
if (line.length === 0 || line.startsWith("#")) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(line);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { error: `line ${i + 1}: invalid JSON` };
|
|
128
|
+
}
|
|
129
|
+
const row = parseTriggerCaseRow(parsed, i + 1);
|
|
130
|
+
if (typeof row === "string") {
|
|
131
|
+
return { error: row };
|
|
132
|
+
}
|
|
133
|
+
rows.push(row);
|
|
134
|
+
}
|
|
135
|
+
if (rows.length === 0) {
|
|
136
|
+
return { error: "trigger-cases.jsonl has no cases" };
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
/** Verify at least two positive and one negative case per indexed skill. */
|
|
141
|
+
export function validateTriggerCoverage(cases, index) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
const bySkill = new Map();
|
|
144
|
+
for (const entry of index) {
|
|
145
|
+
bySkill.set(entry.skillId, { positive: 0, negative: 0 });
|
|
146
|
+
}
|
|
147
|
+
for (const row of cases) {
|
|
148
|
+
const bucket = bySkill.get(row.skill);
|
|
149
|
+
if (bucket === undefined) {
|
|
150
|
+
errors.push(`case '${row.id}' references unknown skill '${row.skill}'`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (row.should_trigger) {
|
|
154
|
+
bucket.positive += 1;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
bucket.negative += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const [skillId, counts] of bySkill) {
|
|
161
|
+
if (counts.positive < 2) {
|
|
162
|
+
errors.push(`${skillId}: need at least 2 positive trigger cases (have ${counts.positive})`);
|
|
163
|
+
}
|
|
164
|
+
if (counts.negative < 1) {
|
|
165
|
+
errors.push(`${skillId}: need at least 1 negative trigger case (have ${counts.negative})`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return errors;
|
|
169
|
+
}
|
|
170
|
+
/** Run deterministic trigger routing eval (offline skill-pi-trigger-eval counterpart). */
|
|
171
|
+
export function runTriggerEval(options = {}) {
|
|
172
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
173
|
+
const casesPath = options.casesPath ?? resolve(projectRoot, TRIGGER_CASES_REL);
|
|
174
|
+
const indexPath = options.indexPath ?? resolve(projectRoot, SKILLS_INDEX_REL);
|
|
175
|
+
let cases;
|
|
176
|
+
if (options.cases !== undefined) {
|
|
177
|
+
cases = [...options.cases];
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const loaded = loadTriggerCases(casesPath);
|
|
181
|
+
if ("error" in loaded) {
|
|
182
|
+
return { code: 2, report: null, message: `eval:triggers: ${loaded.error}` };
|
|
183
|
+
}
|
|
184
|
+
cases = loaded;
|
|
185
|
+
}
|
|
186
|
+
let indexText;
|
|
187
|
+
if (options.indexText !== undefined) {
|
|
188
|
+
indexText = options.indexText;
|
|
189
|
+
}
|
|
190
|
+
else if (!existsSync(indexPath)) {
|
|
191
|
+
return { code: 2, report: null, message: `eval:triggers: missing ${SKILLS_INDEX_REL}` };
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
indexText = readFileSync(indexPath, "utf8");
|
|
195
|
+
}
|
|
196
|
+
const index = parseSkillsIndex(indexText);
|
|
197
|
+
if (index.length === 0) {
|
|
198
|
+
return {
|
|
199
|
+
code: 2,
|
|
200
|
+
report: null,
|
|
201
|
+
message: `eval:triggers: no Skills Index rows parsed from ${SKILLS_INDEX_REL}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const coverageErrors = validateTriggerCoverage(cases, index);
|
|
205
|
+
const results = [];
|
|
206
|
+
for (const row of cases) {
|
|
207
|
+
const winner = resolveTriggerWinner(row.query, index);
|
|
208
|
+
const actual = wouldRouteToSkill(row.query, row.skill, index);
|
|
209
|
+
results.push({
|
|
210
|
+
id: row.id,
|
|
211
|
+
query: row.query,
|
|
212
|
+
skill: row.skill,
|
|
213
|
+
should_trigger: row.should_trigger,
|
|
214
|
+
actual,
|
|
215
|
+
pass: actual === row.should_trigger,
|
|
216
|
+
matchedTrigger: winner?.trigger,
|
|
217
|
+
winnerSkill: winner?.skillId,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const passed = results.filter((result) => result.pass).length;
|
|
221
|
+
const failed = results.length - passed;
|
|
222
|
+
const report = {
|
|
223
|
+
total: results.length,
|
|
224
|
+
passed,
|
|
225
|
+
failed,
|
|
226
|
+
pass_rate: results.length > 0 ? passed / results.length : null,
|
|
227
|
+
results,
|
|
228
|
+
coverageErrors,
|
|
229
|
+
};
|
|
230
|
+
if (coverageErrors.length > 0) {
|
|
231
|
+
const detail = coverageErrors.map((err) => ` - ${sanitizeTriggerLogLine(err)}`).join("\n");
|
|
232
|
+
return {
|
|
233
|
+
code: 1,
|
|
234
|
+
report,
|
|
235
|
+
message: `eval:triggers: trigger coverage incomplete\n${detail}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (failed > 0) {
|
|
239
|
+
const detail = results
|
|
240
|
+
.filter((result) => !result.pass)
|
|
241
|
+
.slice(0, 8)
|
|
242
|
+
.map((result) => ` - ${sanitizeTriggerLogLine(result.id)}: expected ${result.should_trigger ? "trigger" : "no-trigger"} ` +
|
|
243
|
+
`for ${sanitizeTriggerLogLine(result.skill)}; winner=${sanitizeTriggerLogLine(result.winnerSkill ?? "none")}`)
|
|
244
|
+
.join("\n");
|
|
245
|
+
return {
|
|
246
|
+
code: 1,
|
|
247
|
+
report,
|
|
248
|
+
message: `eval:triggers: ${failed}/${results.length} trigger cases failed ` +
|
|
249
|
+
`(offline skill-pi-trigger-eval routing against ${SKILLS_INDEX_REL})\n${detail}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
code: 0,
|
|
254
|
+
report,
|
|
255
|
+
message: `eval:triggers: ${passed}/${results.length} trigger cases passed ` +
|
|
256
|
+
`(offline skill-pi-trigger-eval routing against ${SKILLS_INDEX_REL})`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=triggers.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type OutputStream = "stdout" | "stderr" | "none";
|
|
2
|
+
/** Glob patterns that classify a change as skill-routing / trigger eval relevant (#1586). */
|
|
3
|
+
export declare const TRIGGER_ROUTING_PATH_PATTERNS: readonly ["AGENTS.md", "REFERENCES.md", "evals/trigger-cases.jsonl", "content/templates/agents-entry.md", "packages/core/src/eval/triggers.ts"];
|
|
4
|
+
export interface EvaluateResult {
|
|
5
|
+
readonly code: 0 | 1 | 2;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly stream: OutputStream;
|
|
8
|
+
readonly skipped?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface EvaluateOptions {
|
|
11
|
+
readonly projectRoot?: string;
|
|
12
|
+
readonly baseRef?: string;
|
|
13
|
+
readonly staged?: boolean;
|
|
14
|
+
readonly paths?: readonly string[];
|
|
15
|
+
readonly quiet?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** True when *path* matches a trigger-routing home. */
|
|
18
|
+
export declare function isTriggerRoutingPath(path: string): boolean;
|
|
19
|
+
/** Classify a path list for trigger-routing coverage. */
|
|
20
|
+
export declare function classifyTriggerRoutingPaths(paths: readonly string[]): {
|
|
21
|
+
readonly isTriggerRouting: boolean;
|
|
22
|
+
readonly matchedPaths: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
/** Collect changed paths from git (base ref or staged index). */
|
|
25
|
+
export declare function collectChangedPaths(projectRoot: string, options: {
|
|
26
|
+
baseRef?: string;
|
|
27
|
+
staged?: boolean;
|
|
28
|
+
}): string[] | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Conditional gate: run eval:triggers when AGENTS.md / REFERENCES.md / trigger
|
|
33
|
+
* cases change (#1586). Skips (exit 0) when the diff does not touch routing homes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function evaluate(options?: EvaluateOptions): EvaluateResult;
|
|
36
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { runTriggerEval } from "../eval/triggers.js";
|
|
4
|
+
import { matchAny } from "../orchestration/pathspec.js";
|
|
5
|
+
/** Glob patterns that classify a change as skill-routing / trigger eval relevant (#1586). */
|
|
6
|
+
export const TRIGGER_ROUTING_PATH_PATTERNS = [
|
|
7
|
+
"AGENTS.md",
|
|
8
|
+
"REFERENCES.md",
|
|
9
|
+
"evals/trigger-cases.jsonl",
|
|
10
|
+
"content/templates/agents-entry.md",
|
|
11
|
+
"packages/core/src/eval/triggers.ts",
|
|
12
|
+
];
|
|
13
|
+
/** True when *path* matches a trigger-routing home. */
|
|
14
|
+
export function isTriggerRoutingPath(path) {
|
|
15
|
+
return matchAny(TRIGGER_ROUTING_PATH_PATTERNS, path);
|
|
16
|
+
}
|
|
17
|
+
/** Classify a path list for trigger-routing coverage. */
|
|
18
|
+
export function classifyTriggerRoutingPaths(paths) {
|
|
19
|
+
const matchedPaths = paths.filter(isTriggerRoutingPath);
|
|
20
|
+
return { isTriggerRouting: matchedPaths.length > 0, matchedPaths };
|
|
21
|
+
}
|
|
22
|
+
function gitNameOnlyDiff(projectRoot, args) {
|
|
23
|
+
try {
|
|
24
|
+
const stdout = execFileSync("git", ["-C", projectRoot, "diff", "--name-only", ...args], {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
27
|
+
});
|
|
28
|
+
return stdout
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter((line) => line.length > 0);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return { error: String(err) };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Collect changed paths from git (base ref or staged index). */
|
|
38
|
+
export function collectChangedPaths(projectRoot, options) {
|
|
39
|
+
if (options.staged) {
|
|
40
|
+
return gitNameOnlyDiff(projectRoot, ["--cached"]);
|
|
41
|
+
}
|
|
42
|
+
if (options.baseRef !== undefined && options.baseRef.length > 0) {
|
|
43
|
+
return gitNameOnlyDiff(projectRoot, [options.baseRef, "HEAD"]);
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
function formatSkipMessage() {
|
|
48
|
+
return ("✓ verify:eval-triggers-relocation: no trigger-routing paths in diff " +
|
|
49
|
+
`(checked ${TRIGGER_ROUTING_PATH_PATTERNS.length} patterns).`);
|
|
50
|
+
}
|
|
51
|
+
function formatPassMessage(matchedPaths, summary) {
|
|
52
|
+
return (`✓ verify:eval-triggers-relocation: eval:triggers OK ` +
|
|
53
|
+
`(routing paths: ${matchedPaths.join(", ")}; ${summary}).`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Conditional gate: run eval:triggers when AGENTS.md / REFERENCES.md / trigger
|
|
57
|
+
* cases change (#1586). Skips (exit 0) when the diff does not touch routing homes.
|
|
58
|
+
*/
|
|
59
|
+
export function evaluate(options = {}) {
|
|
60
|
+
const projectRoot = resolve(options.projectRoot ?? process.cwd());
|
|
61
|
+
const quiet = options.quiet ?? false;
|
|
62
|
+
let paths;
|
|
63
|
+
if (options.paths !== undefined) {
|
|
64
|
+
paths = [...options.paths];
|
|
65
|
+
}
|
|
66
|
+
else if (options.baseRef !== undefined || options.staged) {
|
|
67
|
+
const collected = collectChangedPaths(projectRoot, {
|
|
68
|
+
baseRef: options.baseRef,
|
|
69
|
+
staged: options.staged,
|
|
70
|
+
});
|
|
71
|
+
if ("error" in collected) {
|
|
72
|
+
return {
|
|
73
|
+
code: 2,
|
|
74
|
+
message: "❌ verify:eval-triggers-relocation: could not read git diff " +
|
|
75
|
+
`(project_root=${projectRoot}): ${collected.error}`,
|
|
76
|
+
stream: "stderr",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
paths = collected;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return { code: 0, message: "", stream: "none", skipped: true };
|
|
83
|
+
}
|
|
84
|
+
const { isTriggerRouting, matchedPaths } = classifyTriggerRoutingPaths(paths);
|
|
85
|
+
if (!isTriggerRouting) {
|
|
86
|
+
if (quiet) {
|
|
87
|
+
return { code: 0, message: "", stream: "none", skipped: true };
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
code: 0,
|
|
91
|
+
message: formatSkipMessage(),
|
|
92
|
+
stream: "stdout",
|
|
93
|
+
skipped: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const evalResult = runTriggerEval({ projectRoot });
|
|
97
|
+
if (evalResult.code !== 0) {
|
|
98
|
+
return {
|
|
99
|
+
code: evalResult.code,
|
|
100
|
+
message: `❌ verify:eval-triggers-relocation: eval:triggers failed on trigger-routing PR (#1586).\n` +
|
|
101
|
+
` Matched routing paths: ${matchedPaths.join(", ")}\n` +
|
|
102
|
+
` ${evalResult.message.replace(/\n/g, "\n ")}`,
|
|
103
|
+
stream: "stderr",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (quiet) {
|
|
107
|
+
return { code: 0, message: "", stream: "none" };
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
code: 0,
|
|
111
|
+
message: formatPassMessage(matchedPaths, evalResult.message),
|
|
112
|
+
stream: "stdout",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=evaluate.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type VerifyResult } from "../session/verify-session-ritual.js";
|
|
2
|
+
import { type ActiveScopeInspection } from "./scope.js";
|
|
3
|
+
export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
|
|
4
|
+
export declare const HOOK_HOSTS: readonly ["claude", "grok", "cursor", "codex"];
|
|
5
|
+
export type HookHost = (typeof HOOK_HOSTS)[number];
|
|
6
|
+
export declare const HOOK_EVENTS: readonly ["session.start", "tool.before"];
|
|
7
|
+
export type HookEvent = (typeof HOOK_EVENTS)[number];
|
|
8
|
+
export type HookVerdict = "allow" | "deny";
|
|
9
|
+
export type HookDecisionCode = "session-start" | "session-start-degraded" | "not-direct-write" | "invalid-input" | "ritual-not-ready" | "scope-not-ready" | "write-ready";
|
|
10
|
+
export interface HookDecision {
|
|
11
|
+
readonly verdict: HookVerdict;
|
|
12
|
+
readonly code: HookDecisionCode;
|
|
13
|
+
readonly event: HookEvent;
|
|
14
|
+
readonly host: HookHost;
|
|
15
|
+
readonly toolName: string | null;
|
|
16
|
+
readonly projectRoot: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly scopePath: string | null;
|
|
19
|
+
}
|
|
20
|
+
export interface HookDispatchInput {
|
|
21
|
+
readonly host: HookHost;
|
|
22
|
+
readonly event: HookEvent;
|
|
23
|
+
readonly projectRoot: string;
|
|
24
|
+
readonly payload: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface HookPolicySeams {
|
|
27
|
+
readonly inspectRitual?: (projectRoot: string) => VerifyResult;
|
|
28
|
+
readonly inspectScope?: (projectRoot: string) => ActiveScopeInspection;
|
|
29
|
+
readonly sessionStart?: (projectRoot: string) => {
|
|
30
|
+
code: number;
|
|
31
|
+
stdout: string;
|
|
32
|
+
stderr: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export declare function hookToolName(payload: unknown): string | null;
|
|
36
|
+
export declare function projectRootFromHookPayload(payload: unknown, fallback: string): string;
|
|
37
|
+
export declare function isHookHost(value: string): value is HookHost;
|
|
38
|
+
export declare function isHookEvent(value: string): value is HookEvent;
|
|
39
|
+
/** Decide a normalized event using only the P0 direct-write policy. */
|
|
40
|
+
export declare function decideHook(input: HookDispatchInput, seams?: HookPolicySeams): HookDecision;
|
|
41
|
+
/** Render only authoritative denials; allow preserves the host's own permission flow. */
|
|
42
|
+
export declare function renderHostDecision(host: HookHost, decision: HookDecision): string;
|
|
43
|
+
//# sourceMappingURL=dispatcher.d.ts.map
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { runSessionStartHookWrite } from "../session/session-start-hook.js";
|
|
3
|
+
import { inspectSessionRitual } from "../session/verify-session-ritual.js";
|
|
4
|
+
import { inspectActiveScope } from "./scope.js";
|
|
5
|
+
import { isDirectWriteTool } from "./tools.js";
|
|
6
|
+
export { DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool } from "./tools.js";
|
|
7
|
+
export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
|
|
8
|
+
export const HOOK_EVENTS = ["session.start", "tool.before"];
|
|
9
|
+
function record(value) {
|
|
10
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
14
|
+
function firstString(...values) {
|
|
15
|
+
for (const value of values) {
|
|
16
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export function hookToolName(payload) {
|
|
22
|
+
const input = record(payload);
|
|
23
|
+
if (input === null)
|
|
24
|
+
return null;
|
|
25
|
+
return firstString(input.tool_name, input.toolName, input.tool);
|
|
26
|
+
}
|
|
27
|
+
export function projectRootFromHookPayload(payload, fallback) {
|
|
28
|
+
const input = record(payload);
|
|
29
|
+
if (input === null)
|
|
30
|
+
return resolve(fallback);
|
|
31
|
+
const workspaceRoots = input.workspace_roots;
|
|
32
|
+
const root = firstString(input.workspaceRoot, input.workspace_root, Array.isArray(workspaceRoots) ? workspaceRoots[0] : null, input.cwd, fallback);
|
|
33
|
+
return resolve(root ?? fallback);
|
|
34
|
+
}
|
|
35
|
+
export function isHookHost(value) {
|
|
36
|
+
return HOOK_HOSTS.includes(value);
|
|
37
|
+
}
|
|
38
|
+
export function isHookEvent(value) {
|
|
39
|
+
return HOOK_EVENTS.includes(value);
|
|
40
|
+
}
|
|
41
|
+
function deny(input, code, toolName, message) {
|
|
42
|
+
return {
|
|
43
|
+
verdict: "deny",
|
|
44
|
+
code,
|
|
45
|
+
event: input.event,
|
|
46
|
+
host: input.host,
|
|
47
|
+
toolName,
|
|
48
|
+
projectRoot: resolve(input.projectRoot),
|
|
49
|
+
message,
|
|
50
|
+
scopePath: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Decide a normalized event using only the P0 direct-write policy. */
|
|
54
|
+
export function decideHook(input, seams = {}) {
|
|
55
|
+
const projectRoot = resolve(input.projectRoot);
|
|
56
|
+
if (input.event === "session.start") {
|
|
57
|
+
try {
|
|
58
|
+
const result = (seams.sessionStart ?? runSessionStartHookWrite)(projectRoot);
|
|
59
|
+
if (result.code !== 0) {
|
|
60
|
+
const detail = (result.stderr || result.stdout).trim().replace(/\s+/g, " ").slice(0, 400);
|
|
61
|
+
return {
|
|
62
|
+
verdict: "allow",
|
|
63
|
+
code: "session-start-degraded",
|
|
64
|
+
event: input.event,
|
|
65
|
+
host: input.host,
|
|
66
|
+
toolName: null,
|
|
67
|
+
projectRoot,
|
|
68
|
+
message: `Directive SessionStart bookkeeping reported exit ${result.code} on its non-blocking path` +
|
|
69
|
+
`${detail.length > 0 ? `: ${detail}` : "."}`,
|
|
70
|
+
scopePath: null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
return {
|
|
76
|
+
verdict: "allow",
|
|
77
|
+
code: "session-start-degraded",
|
|
78
|
+
event: input.event,
|
|
79
|
+
host: input.host,
|
|
80
|
+
toolName: null,
|
|
81
|
+
projectRoot,
|
|
82
|
+
message: `Directive SessionStart bookkeeping failed on its non-blocking path: ${String(cause)}`,
|
|
83
|
+
scopePath: null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
verdict: "allow",
|
|
88
|
+
code: "session-start",
|
|
89
|
+
event: input.event,
|
|
90
|
+
host: input.host,
|
|
91
|
+
toolName: null,
|
|
92
|
+
projectRoot,
|
|
93
|
+
message: "SessionStart bookkeeping completed on a non-blocking path.",
|
|
94
|
+
scopePath: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const toolName = hookToolName(input.payload);
|
|
98
|
+
if (toolName === null) {
|
|
99
|
+
return deny(input, "invalid-input", null, "Directive denied this matched write event because the host payload omitted its tool name.");
|
|
100
|
+
}
|
|
101
|
+
if (!isDirectWriteTool(toolName)) {
|
|
102
|
+
return {
|
|
103
|
+
verdict: "allow",
|
|
104
|
+
code: "not-direct-write",
|
|
105
|
+
event: input.event,
|
|
106
|
+
host: input.host,
|
|
107
|
+
toolName,
|
|
108
|
+
projectRoot,
|
|
109
|
+
message: `${toolName} is outside the P0 direct-write enforcement slice.`,
|
|
110
|
+
scopePath: null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
let ritual;
|
|
114
|
+
try {
|
|
115
|
+
ritual = (seams.inspectRitual ??
|
|
116
|
+
((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
|
|
120
|
+
"Run `deft session:start`, then `deft verify:session-ritual -- --tier=gated`.");
|
|
121
|
+
}
|
|
122
|
+
if (ritual.code !== 0) {
|
|
123
|
+
return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ` +
|
|
124
|
+
"Recovery: run `deft session:start`, then " +
|
|
125
|
+
"`deft verify:session-ritual -- --tier=gated`.");
|
|
126
|
+
}
|
|
127
|
+
let scope;
|
|
128
|
+
try {
|
|
129
|
+
scope = (seams.inspectScope ?? inspectActiveScope)(projectRoot);
|
|
130
|
+
}
|
|
131
|
+
catch (cause) {
|
|
132
|
+
scope = { ready: false, path: null, message: String(cause) };
|
|
133
|
+
}
|
|
134
|
+
if (!scope.ready) {
|
|
135
|
+
return deny(input, "scope-not-ready", toolName, `Directive denied ${toolName}: ${scope.message} ` +
|
|
136
|
+
"Recovery: run `deft scope:activate -- <path>` for the approved xBRIEF.");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
verdict: "allow",
|
|
140
|
+
code: "write-ready",
|
|
141
|
+
event: input.event,
|
|
142
|
+
host: input.host,
|
|
143
|
+
toolName,
|
|
144
|
+
projectRoot,
|
|
145
|
+
message: `Directive write gate passed for ${toolName}.`,
|
|
146
|
+
scopePath: scope.path,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Render only authoritative denials; allow preserves the host's own permission flow. */
|
|
150
|
+
export function renderHostDecision(host, decision) {
|
|
151
|
+
if (decision.verdict === "allow")
|
|
152
|
+
return "";
|
|
153
|
+
switch (host) {
|
|
154
|
+
case "claude":
|
|
155
|
+
case "codex":
|
|
156
|
+
return JSON.stringify({
|
|
157
|
+
hookSpecificOutput: {
|
|
158
|
+
hookEventName: "PreToolUse",
|
|
159
|
+
permissionDecision: "deny",
|
|
160
|
+
permissionDecisionReason: decision.message,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
case "grok":
|
|
164
|
+
return JSON.stringify({ decision: "deny", reason: decision.message });
|
|
165
|
+
case "cursor":
|
|
166
|
+
return JSON.stringify({
|
|
167
|
+
permission: "deny",
|
|
168
|
+
user_message: decision.message,
|
|
169
|
+
agent_message: decision.message,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ActiveScopeInspection {
|
|
2
|
+
readonly ready: boolean;
|
|
3
|
+
readonly path: string | null;
|
|
4
|
+
readonly message: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Find an implementation-eligible scope by delegating every candidate to the
|
|
8
|
+
* existing xBRIEF preflight evaluator. This intentionally creates no second
|
|
9
|
+
* lifecycle/status policy stack.
|
|
10
|
+
*/
|
|
11
|
+
export declare function inspectActiveScope(projectRoot: string): ActiveScopeInspection;
|
|
12
|
+
//# sourceMappingURL=scope.d.ts.map
|