@deftai/directive-core 0.79.2 → 0.79.4

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.
Files changed (64) hide show
  1. package/dist/check/orchestrator.d.ts +4 -0
  2. package/dist/check/orchestrator.js +9 -1
  3. package/dist/coverage-hotspots/evaluate.d.ts +49 -0
  4. package/dist/coverage-hotspots/evaluate.js +262 -0
  5. package/dist/coverage-hotspots/index.d.ts +3 -0
  6. package/dist/coverage-hotspots/index.js +3 -0
  7. package/dist/coverage-hotspots/thresholds.d.ts +5 -0
  8. package/dist/coverage-hotspots/thresholds.js +48 -0
  9. package/dist/hooks/dispatcher.d.ts +7 -0
  10. package/dist/hooks/dispatcher.js +44 -4
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/init-deposit/xbrief-projections.d.ts +7 -0
  14. package/dist/init-deposit/xbrief-projections.js +29 -1
  15. package/dist/integration-e2e/bootstrap-cache-module.d.ts +2 -4
  16. package/dist/integration-e2e/bootstrap-cache-module.js +2 -20
  17. package/dist/platform/resolve-version.d.ts +2 -6
  18. package/dist/platform/resolve-version.js +3 -129
  19. package/dist/pr-merge-readiness/gh.js +13 -5
  20. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.d.ts +18 -0
  21. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +76 -0
  22. package/dist/pr-monitor/monitor.js +10 -1
  23. package/dist/pr-wait-mergeable/cascade.js +8 -1
  24. package/dist/pr-wait-mergeable/classify.js +5 -1
  25. package/dist/pr-wait-mergeable/wrappers.js +15 -4
  26. package/dist/pr-watch/constants.d.ts +15 -0
  27. package/dist/pr-watch/constants.js +37 -0
  28. package/dist/pr-watch/index.d.ts +1 -1
  29. package/dist/pr-watch/index.js +1 -1
  30. package/dist/pr-watch/main.d.ts +3 -0
  31. package/dist/pr-watch/main.js +33 -3
  32. package/dist/pr-watch/probe.js +5 -1
  33. package/dist/pr-watch/types.d.ts +2 -0
  34. package/dist/pr-watch/watch.js +17 -1
  35. package/dist/release/constants.d.ts +5 -0
  36. package/dist/release/constants.js +14 -2
  37. package/dist/release/flags.js +16 -0
  38. package/dist/release/main.js +7 -0
  39. package/dist/release/pipeline.js +7 -3
  40. package/dist/release/preflight.js +8 -1
  41. package/dist/release/skip-ci-incident.d.ts +22 -0
  42. package/dist/release/skip-ci-incident.js +68 -0
  43. package/dist/release/types.d.ts +2 -0
  44. package/dist/release/version.js +68 -32
  45. package/dist/release-e2e/entrypoint-worker.js +1 -0
  46. package/dist/release-e2e/entrypoint.js +13 -0
  47. package/dist/review-monitor/constants.d.ts +14 -0
  48. package/dist/review-monitor/constants.js +55 -0
  49. package/dist/review-monitor/index.d.ts +5 -0
  50. package/dist/review-monitor/index.js +5 -0
  51. package/dist/review-monitor/record.d.ts +50 -0
  52. package/dist/review-monitor/record.js +178 -0
  53. package/dist/review-monitor/tier-detection.d.ts +15 -0
  54. package/dist/review-monitor/tier-detection.js +61 -0
  55. package/dist/review-monitor/verify.d.ts +32 -0
  56. package/dist/review-monitor/verify.js +171 -0
  57. package/dist/scope/undo.js +12 -2
  58. package/dist/triage/bootstrap/cache-module.d.ts +25 -0
  59. package/dist/triage/bootstrap/cache-module.js +39 -0
  60. package/dist/triage/bootstrap/index.d.ts +1 -0
  61. package/dist/triage/bootstrap/index.js +7 -82
  62. package/dist/xbrief-migrate/agents-header.d.ts +0 -11
  63. package/dist/xbrief-migrate/agents-header.js +10 -1
  64. package/package.json +7 -3
@@ -20,12 +20,16 @@ export interface CheckOrchestratorSeams {
20
20
  cwd: string;
21
21
  stdio: string;
22
22
  env?: NodeJS.ProcessEnv;
23
+ timeoutMs?: number;
23
24
  }) => {
24
25
  status: number | null;
26
+ signal?: NodeJS.Signals | null;
25
27
  error?: Error;
26
28
  };
27
29
  /** Child-process environment (default: process.env). */
28
30
  readonly env?: NodeJS.ProcessEnv;
31
+ /** Wall-clock spawn timeout in milliseconds (default: none). */
32
+ readonly timeoutMs?: number;
29
33
  }
30
34
  /**
31
35
  * True when `path` is the directive framework source checkout root (not a
@@ -78,11 +78,16 @@ export function dispatchTaskCheck(frameworkRoot, projectRoot, seams = {}) {
78
78
  cwd,
79
79
  stdio: "inherit",
80
80
  env: seams.env,
81
+ timeoutMs: seams.timeoutMs,
81
82
  });
82
83
  if (result.error !== undefined) {
83
84
  process.stderr.write(`check: failed to invoke task: ${result.error.message}\n`);
84
85
  return 2;
85
86
  }
87
+ if (result.signal === "SIGTERM" && result.status === null && seams.timeoutMs !== undefined) {
88
+ process.stderr.write(`check: timed out after ${seams.timeoutMs / 60_000}m (Step 5 vitest coverage budget; #2652)\n`);
89
+ return 124;
90
+ }
86
91
  return result.status ?? 1;
87
92
  }
88
93
  function defaultSpawn(cmd, args, opts) {
@@ -90,7 +95,10 @@ function defaultSpawn(cmd, args, opts) {
90
95
  cwd: opts.cwd,
91
96
  stdio: opts.stdio,
92
97
  env: opts.env ?? process.env,
98
+ ...(opts.timeoutMs !== undefined
99
+ ? { timeout: opts.timeoutMs, killSignal: "SIGTERM" }
100
+ : {}),
93
101
  });
94
- return { status: result.status, error: result.error };
102
+ return { status: result.status, signal: result.signal, error: result.error };
95
103
  }
96
104
  //# sourceMappingURL=orchestrator.js.map
@@ -0,0 +1,49 @@
1
+ import { type CoverageMetric, type CoverageTotals, summarizeCoverageFinal } from "../vitest-runner/coverage-debt.js";
2
+ import { type CoverageThresholds } from "./thresholds.js";
3
+ export declare const DEFAULT_MIN_HEADROOM_PP = 0.3;
4
+ export declare const DEFAULT_LOWEST_MODULE_LIMIT = 10;
5
+ export interface ModuleCoverage {
6
+ readonly path: string;
7
+ readonly lines: number;
8
+ readonly functions: number;
9
+ readonly branches: number;
10
+ readonly statements: number;
11
+ }
12
+ export interface UncoveredBranchSample {
13
+ readonly path: string;
14
+ readonly line: number;
15
+ readonly branchId: string;
16
+ readonly type: string;
17
+ }
18
+ export interface CoverageHotspotsReport {
19
+ readonly ok: boolean;
20
+ readonly global: CoverageTotals;
21
+ readonly thresholds: CoverageThresholds;
22
+ readonly headroomPp: Record<CoverageMetric, number>;
23
+ readonly minHeadroomPp: number;
24
+ readonly failReasons: readonly string[];
25
+ readonly lowestModules: readonly ModuleCoverage[];
26
+ readonly uncoveredBranches: readonly UncoveredBranchSample[];
27
+ readonly pathFilter: readonly string[];
28
+ readonly coverageReportPath: string;
29
+ }
30
+ export interface CoverageHotspotsResult {
31
+ readonly exitCode: 0 | 1 | 2;
32
+ readonly report: CoverageHotspotsReport | null;
33
+ readonly message: string;
34
+ }
35
+ export interface CoverageHotspotsOptions {
36
+ readonly projectRoot: string;
37
+ readonly coverageDir?: string;
38
+ readonly minHeadroomPp?: number;
39
+ readonly baseRef?: string | null;
40
+ readonly pathFilter?: readonly string[] | null;
41
+ readonly useDiffPaths?: boolean;
42
+ readonly lowestModuleLimit?: number;
43
+ }
44
+ export declare function evaluateCoverageHotspots(options: CoverageHotspotsOptions): CoverageHotspotsResult;
45
+ export declare function formatJsonReport(report: CoverageHotspotsReport): string;
46
+ export declare function formatTextReport(report: CoverageHotspotsReport, failed: boolean): string;
47
+ /** Re-export for tests that build synthetic reports. */
48
+ export { summarizeCoverageFinal };
49
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1,262 @@
1
+ import * as childProcess from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { summarizeCoverageFinal, } from "../vitest-runner/coverage-debt.js";
5
+ import { readProjectCoverageThresholds } from "./thresholds.js";
6
+ export const DEFAULT_MIN_HEADROOM_PP = 0.3;
7
+ export const DEFAULT_LOWEST_MODULE_LIMIT = 10;
8
+ class GitCommandError extends Error {
9
+ }
10
+ function runGit(args, projectRoot) {
11
+ try {
12
+ return childProcess.execFileSync("git", args, {
13
+ cwd: projectRoot,
14
+ encoding: "utf8",
15
+ stdio: ["ignore", "pipe", "pipe"],
16
+ });
17
+ }
18
+ catch (err) {
19
+ const e = err;
20
+ const stderr = String(e.stderr ?? e.message ?? err).trim();
21
+ throw new GitCommandError(`coverage-hotspots: git ${args.join(" ")} failed: ${stderr}`);
22
+ }
23
+ }
24
+ function splitLines(stdout) {
25
+ return stdout
26
+ .split("\n")
27
+ .map((line) => line.trim())
28
+ .filter((line) => line.length > 0);
29
+ }
30
+ function resolveBaseRef(projectRoot, override) {
31
+ if (override && override.trim().length > 0)
32
+ return override.trim();
33
+ for (const candidate of ["origin/master", "origin/main", "master", "main", "HEAD~1"]) {
34
+ try {
35
+ runGit(["rev-parse", "--verify", "-q", candidate], projectRoot);
36
+ return candidate;
37
+ }
38
+ catch {
39
+ // try next
40
+ }
41
+ }
42
+ return "HEAD~1";
43
+ }
44
+ function diffPaths(projectRoot, baseRef) {
45
+ return splitLines(runGit(["diff", "--name-only", baseRef, "HEAD"], projectRoot));
46
+ }
47
+ function pct(covered, total) {
48
+ return total === 0 ? 100 : (covered / total) * 100;
49
+ }
50
+ function summarizeFileCoverage(relPath, file) {
51
+ let stmtTotal = 0;
52
+ let stmtCovered = 0;
53
+ let fnTotal = 0;
54
+ let fnCovered = 0;
55
+ let branchTotal = 0;
56
+ let branchCovered = 0;
57
+ if (file.s) {
58
+ for (const hits of Object.values(file.s)) {
59
+ stmtTotal += 1;
60
+ if (hits > 0)
61
+ stmtCovered += 1;
62
+ }
63
+ }
64
+ if (file.f) {
65
+ for (const hits of Object.values(file.f)) {
66
+ fnTotal += 1;
67
+ if (hits > 0)
68
+ fnCovered += 1;
69
+ }
70
+ }
71
+ if (file.b) {
72
+ for (const paths of Object.values(file.b)) {
73
+ for (const hits of paths) {
74
+ branchTotal += 1;
75
+ if (hits > 0)
76
+ branchCovered += 1;
77
+ }
78
+ }
79
+ }
80
+ const statements = pct(stmtCovered, stmtTotal);
81
+ const functions = pct(fnCovered, fnTotal);
82
+ const branches = pct(branchCovered, branchTotal);
83
+ return {
84
+ path: relPath,
85
+ statements,
86
+ functions,
87
+ branches,
88
+ lines: statements,
89
+ };
90
+ }
91
+ function normalizeReportPath(projectRoot, rawKey, file) {
92
+ if (file.path && file.path.length > 0) {
93
+ const abs = file.path;
94
+ if (abs.startsWith(projectRoot)) {
95
+ return relative(projectRoot, abs).replace(/\\/g, "/");
96
+ }
97
+ return abs.replace(/\\/g, "/");
98
+ }
99
+ return rawKey.replace(/\\/g, "/");
100
+ }
101
+ function pathMatchesFilter(relPath, filters) {
102
+ if (filters.length === 0)
103
+ return true;
104
+ // Substring only — do not feed CLI/argv filters into RegExp (CodeQL js/regex-injection).
105
+ return filters.some((pattern) => relPath.includes(pattern));
106
+ }
107
+ function collectUncoveredBranches(relPath, file) {
108
+ if (!file.b)
109
+ return [];
110
+ const samples = [];
111
+ for (const [branchId, hits] of Object.entries(file.b)) {
112
+ if (!hits.some((count) => count === 0))
113
+ continue;
114
+ const meta = file.branchMap?.[branchId];
115
+ const line = meta?.line ?? meta?.loc?.start?.line ?? 0;
116
+ samples.push({
117
+ path: relPath,
118
+ line,
119
+ branchId,
120
+ type: meta?.type ?? "branch",
121
+ });
122
+ }
123
+ return samples;
124
+ }
125
+ function computeHeadroom(totals, thresholds) {
126
+ return {
127
+ lines: totals.lines - thresholds.lines,
128
+ functions: totals.functions - thresholds.functions,
129
+ branches: totals.branches - thresholds.branches,
130
+ statements: totals.statements - thresholds.statements,
131
+ };
132
+ }
133
+ function evaluatePassFail(totals, thresholds, headroomPp, minHeadroomPp) {
134
+ const reasons = [];
135
+ if (totals.branches + 1e-9 < thresholds.branches) {
136
+ reasons.push(`branches ${totals.branches.toFixed(2)}% below project floor ${thresholds.branches}%`);
137
+ }
138
+ if (headroomPp.branches + 1e-9 < minHeadroomPp) {
139
+ reasons.push(`branch headroom ${headroomPp.branches.toFixed(2)}pp below minimum ${minHeadroomPp}pp`);
140
+ }
141
+ return reasons;
142
+ }
143
+ function configError(message) {
144
+ return { exitCode: 2, report: null, message };
145
+ }
146
+ export function evaluateCoverageHotspots(options) {
147
+ const projectRoot = options.projectRoot;
148
+ const coverageDir = options.coverageDir ?? join(projectRoot, "coverage");
149
+ const minHeadroomPp = options.minHeadroomPp ?? DEFAULT_MIN_HEADROOM_PP;
150
+ const lowestModuleLimit = options.lowestModuleLimit ?? DEFAULT_LOWEST_MODULE_LIMIT;
151
+ const useDiffPaths = options.useDiffPaths ?? true;
152
+ const reportPath = join(coverageDir, "coverage-final.json");
153
+ if (!existsSync(reportPath)) {
154
+ return configError(`coverage-hotspots: coverage report missing at ${reportPath}\n` +
155
+ " Run tests with coverage first (e.g. vitest run --coverage or task test:coverage).");
156
+ }
157
+ let rawReport;
158
+ try {
159
+ rawReport = JSON.parse(readFileSync(reportPath, "utf8"));
160
+ }
161
+ catch (err) {
162
+ return configError(`coverage-hotspots: unreadable coverage report at ${reportPath}: ${String(err.message)}`);
163
+ }
164
+ const totals = summarizeCoverageFinal(rawReport);
165
+ const thresholds = readProjectCoverageThresholds(projectRoot);
166
+ const headroomPp = computeHeadroom(totals, thresholds);
167
+ let pathFilter = options.pathFilter ?? [];
168
+ if (pathFilter.length === 0 && useDiffPaths) {
169
+ try {
170
+ const baseRef = resolveBaseRef(projectRoot, options.baseRef);
171
+ pathFilter = diffPaths(projectRoot, baseRef);
172
+ }
173
+ catch (err) {
174
+ const message = err instanceof GitCommandError
175
+ ? `${err.message}\n Recovery: pass explicit --path filters or run inside a git repo.`
176
+ : `coverage-hotspots: ${String(err.message)}`;
177
+ return configError(message);
178
+ }
179
+ }
180
+ const modules = [];
181
+ const uncoveredBranches = [];
182
+ for (const [rawKey, file] of Object.entries(rawReport)) {
183
+ const relPath = normalizeReportPath(projectRoot, rawKey, file);
184
+ modules.push(summarizeFileCoverage(relPath, file));
185
+ if (pathMatchesFilter(relPath, pathFilter)) {
186
+ uncoveredBranches.push(...collectUncoveredBranches(relPath, file));
187
+ }
188
+ }
189
+ modules.sort((a, b) => a.branches - b.branches || a.path.localeCompare(b.path));
190
+ const lowestModules = modules.slice(0, lowestModuleLimit);
191
+ uncoveredBranches.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.branchId.localeCompare(b.branchId));
192
+ const failReasons = evaluatePassFail(totals, thresholds, headroomPp, minHeadroomPp);
193
+ const report = {
194
+ ok: failReasons.length === 0,
195
+ global: totals,
196
+ thresholds,
197
+ headroomPp,
198
+ minHeadroomPp,
199
+ failReasons,
200
+ lowestModules,
201
+ uncoveredBranches,
202
+ pathFilter,
203
+ coverageReportPath: reportPath,
204
+ };
205
+ if (failReasons.length > 0) {
206
+ return {
207
+ exitCode: 1,
208
+ report,
209
+ message: formatTextReport(report, true),
210
+ };
211
+ }
212
+ return {
213
+ exitCode: 0,
214
+ report,
215
+ message: formatTextReport(report, false),
216
+ };
217
+ }
218
+ export function formatJsonReport(report) {
219
+ return `${JSON.stringify(report, null, 2)}\n`;
220
+ }
221
+ function metricLine(label, value, threshold, headroom) {
222
+ return ` ${label.padEnd(12)} ${value.toFixed(2)}% (floor ${threshold}% | headroom ${headroom.toFixed(2)}pp)`;
223
+ }
224
+ export function formatTextReport(report, failed) {
225
+ const lines = [];
226
+ lines.push(failed ? "coverage-hotspots: FAIL" : "coverage-hotspots: OK");
227
+ lines.push(`report: ${report.coverageReportPath}`);
228
+ lines.push("global:");
229
+ lines.push(metricLine("branches", report.global.branches, report.thresholds.branches, report.headroomPp.branches));
230
+ lines.push(metricLine("lines", report.global.lines, report.thresholds.lines, report.headroomPp.lines));
231
+ lines.push(metricLine("functions", report.global.functions, report.thresholds.functions, report.headroomPp.functions));
232
+ lines.push(metricLine("statements", report.global.statements, report.thresholds.statements, report.headroomPp.statements));
233
+ lines.push(`min branch headroom: ${report.minHeadroomPp}pp`);
234
+ if (report.pathFilter.length > 0) {
235
+ lines.push(`path filter (${report.pathFilter.length}): ${report.pathFilter.slice(0, 8).join(", ")}${report.pathFilter.length > 8 ? ", ..." : ""}`);
236
+ }
237
+ if (report.failReasons.length > 0) {
238
+ lines.push("fail reasons:");
239
+ for (const reason of report.failReasons) {
240
+ lines.push(` - ${reason}`);
241
+ }
242
+ }
243
+ if (report.lowestModules.length > 0) {
244
+ lines.push("lowest modules (branches):");
245
+ for (const mod of report.lowestModules.slice(0, 5)) {
246
+ lines.push(` - ${mod.path}: branches ${mod.branches.toFixed(2)}%`);
247
+ }
248
+ }
249
+ if (report.uncoveredBranches.length > 0) {
250
+ lines.push("uncovered branch samples:");
251
+ for (const sample of report.uncoveredBranches.slice(0, 12)) {
252
+ lines.push(` - ${sample.path}:${sample.line} (${sample.type} #${sample.branchId})`);
253
+ }
254
+ if (report.uncoveredBranches.length > 12) {
255
+ lines.push(` ... ${report.uncoveredBranches.length - 12} more`);
256
+ }
257
+ }
258
+ return `${lines.join("\n")}\n`;
259
+ }
260
+ /** Re-export for tests that build synthetic reports. */
261
+ export { summarizeCoverageFinal };
262
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,3 @@
1
+ export * from "./evaluate.js";
2
+ export * from "./thresholds.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from "./evaluate.js";
2
+ export * from "./thresholds.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,5 @@
1
+ import { type CoverageMetric } from "../vitest-runner/coverage-debt.js";
2
+ export type CoverageThresholds = Record<CoverageMetric, number>;
3
+ /** Best-effort read of vitest coverage thresholds; falls back to framework 85% defaults. */
4
+ export declare function readProjectCoverageThresholds(projectRoot: string): CoverageThresholds;
5
+ //# sourceMappingURL=thresholds.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { COVERAGE_GOAL } from "../vitest-runner/coverage-debt.js";
4
+ const VITEST_CONFIG_CANDIDATES = [
5
+ "vitest.config.ts",
6
+ "vitest.config.mts",
7
+ "vitest.config.js",
8
+ "vitest.config.cjs",
9
+ "vitest.config.mjs",
10
+ ];
11
+ const METRIC_PATTERN = /(?:^|[\n\r{,])\s*(branches|lines|functions|statements)\s*:\s*(\d+(?:\.\d+)?)/g;
12
+ function parseMetricBlock(text, anchor) {
13
+ const match = anchor.exec(text);
14
+ if (match?.index === undefined)
15
+ return {};
16
+ const slice = text.slice(match.index, match.index + 800);
17
+ const out = {};
18
+ for (const metricMatch of slice.matchAll(METRIC_PATTERN)) {
19
+ const metric = metricMatch[1];
20
+ const value = metricMatch[2];
21
+ if (metric === undefined || value === undefined)
22
+ continue;
23
+ out[metric] = Number.parseFloat(value);
24
+ }
25
+ return out;
26
+ }
27
+ /** Best-effort read of vitest coverage thresholds; falls back to framework 85% defaults. */
28
+ export function readProjectCoverageThresholds(projectRoot) {
29
+ for (const name of VITEST_CONFIG_CANDIDATES) {
30
+ const path = join(projectRoot, name);
31
+ if (!existsSync(path))
32
+ continue;
33
+ const text = readFileSync(path, "utf8");
34
+ const fromCoverageThresholds = parseMetricBlock(text, /coverageThresholds\s*=\s*\{/);
35
+ const fromNested = parseMetricBlock(text, /thresholds\s*:\s*\{/);
36
+ const merged = { ...fromNested, ...fromCoverageThresholds };
37
+ if (Object.keys(merged).length > 0) {
38
+ return {
39
+ lines: merged.lines ?? COVERAGE_GOAL.lines,
40
+ functions: merged.functions ?? COVERAGE_GOAL.functions,
41
+ branches: merged.branches ?? COVERAGE_GOAL.branches,
42
+ statements: merged.statements ?? COVERAGE_GOAL.statements,
43
+ };
44
+ }
45
+ }
46
+ return { ...COVERAGE_GOAL };
47
+ }
48
+ //# sourceMappingURL=thresholds.js.map
@@ -17,11 +17,17 @@ export interface HookDecision {
17
17
  readonly message: string;
18
18
  readonly scopePath: string | null;
19
19
  }
20
+ /** Stdin parse metadata from hook-dispatch; absent when callers supply payload directly. */
21
+ export interface HookPayloadContext {
22
+ readonly stdinEmpty?: boolean;
23
+ readonly parseFailed?: boolean;
24
+ }
20
25
  export interface HookDispatchInput {
21
26
  readonly host: HookHost;
22
27
  readonly event: HookEvent;
23
28
  readonly projectRoot: string;
24
29
  readonly payload: unknown;
30
+ readonly payloadContext?: HookPayloadContext;
25
31
  }
26
32
  export interface HookPolicySeams {
27
33
  readonly inspectRitual?: (projectRoot: string) => VerifyResult;
@@ -32,6 +38,7 @@ export interface HookPolicySeams {
32
38
  stderr: string;
33
39
  };
34
40
  }
41
+ export declare function hookPayloadTopLevelKeys(payload: unknown): string[];
35
42
  export declare function hookToolName(payload: unknown, host?: HookHost): string | null;
36
43
  /**
37
44
  * Best-effort write-target path from host PreToolUse payloads (#2625).
@@ -29,7 +29,12 @@ function fieldString(input, key) {
29
29
  return null;
30
30
  }
31
31
  function toolInputRecord(payload) {
32
- return record(payload.tool_input) ?? record(payload.toolInput) ?? record(payload.input);
32
+ const toolCall = record(payload.tool_call) ?? record(payload.toolCall);
33
+ return (record(payload.tool_input) ??
34
+ record(payload.toolInput) ??
35
+ record(payload.input) ??
36
+ record(payload.arguments) ??
37
+ (toolCall !== null ? record(toolCall.arguments) : null));
33
38
  }
34
39
  /**
35
40
  * Cursor preToolUse payloads sometimes omit `tool_name` even when the hook matcher
@@ -65,19 +70,50 @@ function inferCursorDirectWriteToolName(payload) {
65
70
  return "Write";
66
71
  return null;
67
72
  }
73
+ export function hookPayloadTopLevelKeys(payload) {
74
+ const input = record(payload);
75
+ if (input === null)
76
+ return [];
77
+ return Object.keys(input).sort();
78
+ }
68
79
  export function hookToolName(payload, host) {
69
80
  const input = record(payload);
70
81
  if (input === null)
71
82
  return null;
72
- const direct = fieldString(input, "tool_name") ?? fieldString(input, "toolName") ?? fieldString(input, "tool");
83
+ const toolObject = record(input.tool);
84
+ const toolCall = record(input.tool_call) ?? record(input.toolCall);
85
+ // OpenAI-style nestings are host-agnostic; checked before Cursor-only inference.
86
+ const direct = fieldString(input, "tool_name") ??
87
+ fieldString(input, "toolName") ??
88
+ fieldString(input, "tool") ??
89
+ (toolObject !== null ? fieldString(toolObject, "name") : null) ??
90
+ (toolCall !== null ? fieldString(toolCall, "name") : null);
73
91
  if (direct !== null)
74
92
  return direct;
75
93
  if (host === "cursor")
76
94
  return inferCursorDirectWriteToolName(input);
77
95
  return null;
78
96
  }
79
- function missingToolNameMessage(host) {
97
+ function missingToolNameMessage(input) {
98
+ const { host, payload, context } = input;
80
99
  if (host === "cursor") {
100
+ if (context?.stdinEmpty) {
101
+ return ("Directive denied this Cursor preToolUse event because the host sent an empty payload " +
102
+ "(stdin was empty — not a session ritual or scope failure). " +
103
+ "If write tools should pass, update Directive or report the payload shape from Cursor.");
104
+ }
105
+ if (context?.parseFailed) {
106
+ return ("Directive denied this Cursor preToolUse event because the host payload was not valid JSON " +
107
+ "(host-integration mismatch — not a session ritual or scope failure). " +
108
+ "If write tools should pass, update Directive or report the payload shape from Cursor.");
109
+ }
110
+ const keys = hookPayloadTopLevelKeys(payload);
111
+ if (keys.length > 0) {
112
+ return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
113
+ "recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
114
+ `Top-level payload keys: ${keys.join(", ")}. ` +
115
+ "If write tools should pass, update Directive or report the payload shape from Cursor.");
116
+ }
81
117
  return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
82
118
  "recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
83
119
  "If write tools should pass, update Directive or report the payload shape from Cursor.");
@@ -189,7 +225,11 @@ export function decideHook(input, seams = {}) {
189
225
  }
190
226
  const toolName = hookToolName(input.payload, input.host);
191
227
  if (toolName === null) {
192
- return deny(input, "invalid-input", null, missingToolNameMessage(input.host));
228
+ return deny(input, "invalid-input", null, missingToolNameMessage({
229
+ host: input.host,
230
+ payload: input.payload,
231
+ context: input.payloadContext,
232
+ }));
193
233
  }
194
234
  if (!isDirectWriteTool(toolName)) {
195
235
  return {
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * as branch from "./branch/index.js";
13
13
  export * as cache from "./cache/index.js";
14
14
  export * as capacity from "./capacity/index.js";
15
15
  export * as codebase from "./codebase/index.js";
16
+ export * from "./coverage-hotspots/index.js";
16
17
  export * as doctor from "./doctor/index.js";
17
18
  export * from "./encoding/index.js";
18
19
  export * as evalCrud from "./eval/crud-telemetry.js";
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export * as branch from "./branch/index.js";
13
13
  export * as cache from "./cache/index.js";
14
14
  export * as capacity from "./capacity/index.js";
15
15
  export * as codebase from "./codebase/index.js";
16
+ export * from "./coverage-hotspots/index.js";
16
17
  export * as doctor from "./doctor/index.js";
17
18
  export * from "./encoding/index.js";
18
19
  export * as evalCrud from "./eval/crud-telemetry.js";
@@ -5,6 +5,13 @@
5
5
  * freshness. A current `.deft/core/VERSION` therefore cannot short-circuit
6
6
  * these repairs.
7
7
  */
8
+ /** Rewrite legacy vbrief/.eval description paths for xbrief/schemas projection (#2670). */
9
+ export declare function rewriteProjectedSchemaContent(content: string): string;
10
+ /**
11
+ * Fail closed when a projected xbrief/schemas file still cites vbrief/.eval/.
12
+ * Upstream vbrief/schemas/ may keep legacy paths; consumer copies must not (#2670).
13
+ */
14
+ export declare function assertProjectedSchemaDescriptionsRooted(projectDir: string, destinationDir: string): void;
8
15
  /**
9
16
  * Synchronize framework-owned xBRIEF schemas while preserving unknown consumer
10
17
  * files. The obsolete v0.6 root schema is the only destination-only file this
@@ -13,6 +13,10 @@ import { DEV_FALLBACK } from "../platform/constants.js";
13
13
  import { MIGRATED_ARTIFACT_DIR } from "../xbrief-migrate/constants.js";
14
14
  const OBSOLETE_CORE_SCHEMA = "vbrief-core.schema.json";
15
15
  const CURRENT_CORE_SCHEMA = "xbrief-core-0.8.schema.json";
16
+ /** Legacy lifecycle eval prefix in framework vbrief/schemas source copies. */
17
+ const LEGACY_EVAL_PATH_PREFIX = "vbrief/.eval/";
18
+ /** Consumer xbrief/schemas projection must root description paths here (#2670). */
19
+ const XBRIEF_EVAL_PATH_PREFIX = "xbrief/.eval/";
16
20
  function normalizeVersion(version) {
17
21
  return version.trim().replace(/^v/, "");
18
22
  }
@@ -39,6 +43,28 @@ function collectSchemaFiles(root, dir = root, files = []) {
39
43
  }
40
44
  return files;
41
45
  }
46
+ /** Rewrite legacy vbrief/.eval description paths for xbrief/schemas projection (#2670). */
47
+ export function rewriteProjectedSchemaContent(content) {
48
+ return content.includes(LEGACY_EVAL_PATH_PREFIX)
49
+ ? content.replaceAll(LEGACY_EVAL_PATH_PREFIX, XBRIEF_EVAL_PATH_PREFIX)
50
+ : content;
51
+ }
52
+ /**
53
+ * Fail closed when a projected xbrief/schemas file still cites vbrief/.eval/.
54
+ * Upstream vbrief/schemas/ may keep legacy paths; consumer copies must not (#2670).
55
+ */
56
+ export function assertProjectedSchemaDescriptionsRooted(projectDir, destinationDir) {
57
+ assertProjectionContained(projectDir, destinationDir);
58
+ if (!isDirectory(destinationDir))
59
+ return;
60
+ for (const rel of collectSchemaFiles(destinationDir)) {
61
+ const full = join(destinationDir, rel);
62
+ const text = readFileSync(full, "utf8");
63
+ if (text.includes(LEGACY_EVAL_PATH_PREFIX)) {
64
+ throw new Error(`projected xbrief schema still cites ${LEGACY_EVAL_PATH_PREFIX}: ${join(MIGRATED_ARTIFACT_DIR, "schemas", rel)}`);
65
+ }
66
+ }
67
+ }
42
68
  function writeFileIfChanged(projectDir, target, content) {
43
69
  assertProjectionContained(projectDir, target);
44
70
  const desired = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8");
@@ -73,7 +99,8 @@ export function syncConsumerXbriefSchemas(projectDir, deftDir) {
73
99
  continue;
74
100
  const source = join(sourceDir, rel);
75
101
  const destination = join(destinationDir, rel);
76
- changed = writeFileIfChanged(projectDir, destination, readFileSync(source)) || changed;
102
+ const projected = rewriteProjectedSchemaContent(readFileSync(source, "utf8"));
103
+ changed = writeFileIfChanged(projectDir, destination, projected) || changed;
77
104
  }
78
105
  const obsoleteDestination = join(destinationDir, OBSOLETE_CORE_SCHEMA);
79
106
  assertProjectionContained(projectDir, obsoleteDestination);
@@ -81,6 +108,7 @@ export function syncConsumerXbriefSchemas(projectDir, deftDir) {
81
108
  rmSync(obsoleteDestination, { force: true });
82
109
  changed = true;
83
110
  }
111
+ assertProjectedSchemaDescriptionsRooted(projectDir, destinationDir);
84
112
  return changed;
85
113
  }
86
114
  function syncBareVersionMarkerWithPolicy(projectDir, version, allowRootFallback) {
@@ -1,5 +1,3 @@
1
- import type { FetchAllReportImpl } from "../cache/fetch.js";
2
- import type { CacheModule } from "../triage/bootstrap/types.js";
3
- /** Adapt TS cache reports to the bootstrap `FetchAllReport` shape. */
4
- export declare function bootstrapCacheModule(cacheFetchAll: (options: Parameters<typeof import("../cache/fetch.js").cacheFetchAll>[0]) => FetchAllReportImpl): CacheModule;
1
+ /** Re-export bootstrap TS cache adapter (canonical home: triage/bootstrap). */
2
+ export { bootstrapCacheModule } from "../triage/bootstrap/cache-module.js";
5
3
  //# sourceMappingURL=bootstrap-cache-module.d.ts.map
@@ -1,21 +1,3 @@
1
- /** Adapt TS cache reports to the bootstrap `FetchAllReport` shape. */
2
- export function bootstrapCacheModule(cacheFetchAll) {
3
- return {
4
- cacheFetchAll(kwargs) {
5
- const report = cacheFetchAll({
6
- source: kwargs.source,
7
- repo: kwargs.repo,
8
- batchSize: kwargs.batchSize,
9
- delayMs: kwargs.delayMs,
10
- cacheRoot: kwargs.cacheRoot,
11
- });
12
- return Promise.resolve({
13
- succeeded: report.issuesWritten,
14
- failed: report.issuesFailed,
15
- skipped: report.alreadyFresh,
16
- summaryLine: (source, repo) => report.summaryLine(source, repo),
17
- });
18
- },
19
- };
20
- }
1
+ /** Re-export bootstrap TS cache adapter (canonical home: triage/bootstrap). */
2
+ export { bootstrapCacheModule } from "../triage/bootstrap/cache-module.js";
21
3
  //# sourceMappingURL=bootstrap-cache-module.js.map
@@ -1,10 +1,6 @@
1
+ import { isPublishable, NonPublishableVersionError, toPep440 } from "../release/version.js";
1
2
  import { DEV_FALLBACK, ENV_VAR } from "./constants.js";
2
- export { DEV_FALLBACK, ENV_VAR };
3
- export declare class NonPublishableVersionError extends Error {
4
- constructor(message: string);
5
- }
6
- export declare function toPep440(version: string): string;
7
- export declare function isPublishable(version: string): boolean;
3
+ export { DEV_FALLBACK, ENV_VAR, isPublishable, NonPublishableVersionError, toPep440 };
8
4
  export declare function tagNameFromRef(ref: string): string;
9
5
  export declare function latestPublishableTag(tags: Iterable<string>): string | null;
10
6
  export declare function payloadIsOwnGitRoot(payloadDir: string): boolean;