@deftai/directive-core 0.66.1 → 0.67.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.
Files changed (71) hide show
  1. package/dist/agents-md-advisory/evaluate.d.ts +44 -0
  2. package/dist/agents-md-advisory/evaluate.js +115 -0
  3. package/dist/agents-md-advisory/index.d.ts +2 -0
  4. package/dist/agents-md-advisory/index.js +2 -0
  5. package/dist/agents-md-budget/evaluate.d.ts +38 -0
  6. package/dist/agents-md-budget/evaluate.js +152 -0
  7. package/dist/agents-md-budget/index.d.ts +2 -0
  8. package/dist/agents-md-budget/index.js +2 -0
  9. package/dist/codebase/map.d.ts +1 -0
  10. package/dist/codebase/map.js +5 -2
  11. package/dist/doctor/constants.d.ts +5 -2
  12. package/dist/doctor/constants.js +15 -1
  13. package/dist/doctor/flags.js +5 -1
  14. package/dist/doctor/main.js +99 -8
  15. package/dist/doctor/paths.d.ts +1 -0
  16. package/dist/doctor/paths.js +55 -0
  17. package/dist/doctor/types.d.ts +7 -0
  18. package/dist/intake/github-auth-modes.d.ts +1 -0
  19. package/dist/intake/github-auth-modes.js +1 -0
  20. package/dist/intake/issue-ingest.d.ts +6 -2
  21. package/dist/intake/issue-ingest.js +65 -9
  22. package/dist/platform/agents-consumer-header.d.ts +13 -0
  23. package/dist/platform/agents-consumer-header.js +57 -0
  24. package/dist/platform/agents-md.js +4 -1
  25. package/dist/platform/index.d.ts +1 -0
  26. package/dist/platform/index.js +1 -0
  27. package/dist/policy/agents-md-advisory.d.ts +52 -0
  28. package/dist/policy/agents-md-advisory.js +63 -0
  29. package/dist/policy/agents-md-budget.d.ts +24 -0
  30. package/dist/policy/agents-md-budget.js +89 -0
  31. package/dist/policy/index.d.ts +1 -0
  32. package/dist/policy/index.js +1 -0
  33. package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
  34. package/dist/pr-merge-readiness/ci-gate.js +79 -0
  35. package/dist/pr-merge-readiness/compute.d.ts +5 -1
  36. package/dist/pr-merge-readiness/compute.js +90 -6
  37. package/dist/pr-merge-readiness/gh.d.ts +8 -0
  38. package/dist/pr-merge-readiness/gh.js +30 -5
  39. package/dist/pr-merge-readiness/index.d.ts +3 -0
  40. package/dist/pr-merge-readiness/index.js +2 -0
  41. package/dist/pr-merge-readiness/main.d.ts +3 -0
  42. package/dist/pr-merge-readiness/main.js +73 -6
  43. package/dist/pr-merge-readiness/output.js +20 -0
  44. package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
  45. package/dist/pr-merge-readiness/slizard-gate.js +119 -0
  46. package/dist/preflight-cache/evaluate.d.ts +1 -0
  47. package/dist/preflight-cache/evaluate.js +17 -10
  48. package/dist/render/project-render.d.ts +16 -2
  49. package/dist/render/project-render.js +55 -22
  50. package/dist/swarm/launch.d.ts +6 -0
  51. package/dist/swarm/launch.js +2 -2
  52. package/dist/swarm/routing-verify.d.ts +1 -1
  53. package/dist/swarm/routing-verify.js +11 -10
  54. package/dist/swarm/routing.d.ts +9 -0
  55. package/dist/swarm/routing.js +46 -0
  56. package/dist/swarm/verify-review-clean-cli.js +25 -1
  57. package/dist/swarm/verify-review-clean.d.ts +9 -1
  58. package/dist/swarm/verify-review-clean.js +76 -3
  59. package/dist/verify-source/biome-config.d.ts +41 -0
  60. package/dist/verify-source/biome-config.js +97 -0
  61. package/dist/verify-source/index.d.ts +1 -0
  62. package/dist/verify-source/index.js +1 -0
  63. package/dist/xbrief-migrate/agents-header.d.ts +69 -0
  64. package/dist/xbrief-migrate/agents-header.js +179 -0
  65. package/dist/xbrief-migrate/drift-gate.js +2 -0
  66. package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
  67. package/dist/xbrief-migrate/fs-helpers.js +11 -0
  68. package/dist/xbrief-migrate/index.d.ts +1 -0
  69. package/dist/xbrief-migrate/index.js +1 -0
  70. package/dist/xbrief-migrate/migrate-project.js +29 -10
  71. package/package.json +11 -3
@@ -1,8 +1,10 @@
1
1
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { hasArtifactSuffix } from "../layout/resolve.js";
4
- import { defaultRunGh, fetchGreptileCommentBody, fetchPrHeadSha, } from "../pr-merge-readiness/gh.js";
4
+ import { buildCiSummaryLine, evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
5
+ import { defaultRunGh, fetchCheckRunsRest, fetchGreptileCommentBody, fetchPrHeadSha, } from "../pr-merge-readiness/gh.js";
5
6
  import { evaluateGates, parseGreptileBody } from "../pr-merge-readiness/index.js";
7
+ import { evaluateSlizardGate, isSlizardCheck } from "../pr-merge-readiness/slizard-gate.js";
6
8
  import { EXIT_EXTERNAL_ERROR, EXIT_OK, EXIT_UNCLEAN } from "./constants.js";
7
9
  function extractPrFromUri(uri) {
8
10
  const pullIdx = uri.indexOf("/pull/");
@@ -101,7 +103,7 @@ export function resolveCohortFromVbriefs(vbriefGlobs) {
101
103
  }
102
104
  return { prNumbers: seenPrs, failures };
103
105
  }
104
- export function evaluatePr(prNumber, repo, runGh = defaultRunGh) {
106
+ export function evaluatePr(prNumber, repo, runGh = defaultRunGh, options = {}) {
105
107
  const headSha = fetchPrHeadSha(prNumber, repo, runGh);
106
108
  if (headSha === null) {
107
109
  return null;
@@ -112,12 +114,71 @@ export function evaluatePr(prNumber, repo, runGh = defaultRunGh) {
112
114
  }
113
115
  const verdict = parseGreptileBody(body);
114
116
  const failures = evaluateGates(prNumber, headSha, verdict);
117
+ let ciSummary = null;
118
+ let slizardSummary = null;
119
+ if (failures.length === 0) {
120
+ if (repo === null) {
121
+ failures.push("Could not resolve repo for required CI check-run gate. " +
122
+ "Use --repo OWNER/REPO or run from a checked-out repository.");
123
+ ciSummary = {
124
+ ready_state: "blocked",
125
+ error: "repo unresolved for check-runs lookup",
126
+ };
127
+ slizardSummary = { ready_state: "skipped", present: false };
128
+ }
129
+ else if (options.skipCi === true) {
130
+ const ci = evaluateCiGate([], { skipCi: true });
131
+ ciSummary = {
132
+ ...ci.summary,
133
+ summary_line: "CI check-runs: skipped (--skip-ci)",
134
+ };
135
+ const slizard = evaluateSlizardGate([], options);
136
+ failures.push(...slizard.failures);
137
+ slizardSummary = { ...slizard.summary };
138
+ }
139
+ else {
140
+ const checks = fetchCheckRunsRest(headSha, repo, runGh);
141
+ if (checks.summary === null) {
142
+ failures.push("Required CI check-runs could not be fetched; fail closed by default (#2169). " +
143
+ `Root cause: ${checks.error}`);
144
+ ciSummary = {
145
+ ready_state: "blocked",
146
+ error: checks.error,
147
+ checked_count: 0,
148
+ ignored_checks: [...(options.ignoreCheckNames ?? [])],
149
+ failed_required: [],
150
+ pending_required: [],
151
+ conclusions: [],
152
+ };
153
+ slizardSummary = { ready_state: "skipped", present: false };
154
+ }
155
+ else {
156
+ const slizard = evaluateSlizardGate(checks.checkRuns, options);
157
+ // SLizard is scored by the structured gate; exclude it from the generic CI set.
158
+ const slizardNames = checks.checkRuns
159
+ .filter((r) => isSlizardCheck(r.name))
160
+ .map((r) => r.name);
161
+ const ci = evaluateCiGate(checks.checkRuns, {
162
+ skipCi: options.skipCi,
163
+ ignoreCheckNames: [...(options.ignoreCheckNames ?? []), ...slizardNames],
164
+ });
165
+ failures.push(...ci.failures, ...slizard.failures);
166
+ ciSummary = {
167
+ ...ci.summary,
168
+ summary_line: buildCiSummaryLine(ci.summary),
169
+ };
170
+ slizardSummary = { ...slizard.summary };
171
+ }
172
+ }
173
+ }
115
174
  return {
116
175
  pr_number: prNumber,
117
176
  head_sha: headSha,
118
177
  verdict: { ...verdict },
119
178
  failures: [...failures],
120
179
  clean: failures.length === 0,
180
+ ci_summary: ciSummary,
181
+ slizard_summary: slizardSummary,
121
182
  };
122
183
  }
123
184
  export function cohortResultToDict(cohort) {
@@ -131,6 +192,8 @@ export function cohortResultToDict(cohort) {
131
192
  clean: r.clean,
132
193
  verdict: r.verdict,
133
194
  failures: r.failures,
195
+ ci_summary: r.ci_summary,
196
+ slizard_summary: r.slizard_summary,
134
197
  })),
135
198
  resolution_errors: cohort.resolution_errors,
136
199
  };
@@ -159,6 +222,12 @@ export function renderReviewCleanText(cohort) {
159
222
  lines.push(` Findings: P0=${String(v.p0Count ?? 0)} ` +
160
223
  `P1=${String(v.p1Count ?? 0)} P2=${String(v.p2Count ?? 0)}`);
161
224
  lines.push(` Errored sentinel: ${v.errored === true ? "True" : "False"}`);
225
+ if (r.ci_summary && typeof r.ci_summary.summary_line === "string") {
226
+ lines.push(` ${r.ci_summary.summary_line}`);
227
+ }
228
+ if (r.slizard_summary && typeof r.slizard_summary.summary_line === "string") {
229
+ lines.push(` ${r.slizard_summary.summary_line}`);
230
+ }
162
231
  r.failures.forEach((fail, index) => {
163
232
  lines.push(` [${index + 1}] ${fail}`);
164
233
  });
@@ -212,7 +281,11 @@ export function verifyReviewClean(args) {
212
281
  const runGh = args.runGh ?? defaultRunGh;
213
282
  const prResults = [];
214
283
  for (const prNum of prNumbers) {
215
- const perPr = evaluatePr(prNum, args.repo ?? null, runGh);
284
+ const perPr = evaluatePr(prNum, args.repo ?? null, runGh, {
285
+ skipCi: args.skipCi,
286
+ skipSlizard: args.skipSlizard,
287
+ ignoreCheckNames: args.ciIgnoreChecks,
288
+ });
216
289
  if (perPr === null) {
217
290
  return { exitCode: EXIT_EXTERNAL_ERROR, stdout: "", stderr: "" };
218
291
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * biome-config.ts -- guard against #2190 (biome check . reports
3
+ * recommended-preset diagnostics as errors vs warnings non-deterministically
4
+ * on a pinned biome version).
5
+ *
6
+ * A preset-inherited rule severity can silently change tier across a biome
7
+ * version bump (or, per #2190, apparently even across runs on the identical
8
+ * pinned version). Pinning `noUnusedVariables` / `noNonNullAssertion` to an
9
+ * explicit non-"error" severity in biome.json makes the tier config-owned so
10
+ * it cannot flip a required CI check to failing on pre-existing diagnostics.
11
+ */
12
+ /** The two rules #2190 observed flipping severity in CI (see docs/analysis/2026-07-02-2190-biome-determinism.md). */
13
+ export declare const GUARDED_RULES: ReadonlyArray<{
14
+ readonly group: string;
15
+ readonly rule: string;
16
+ }>;
17
+ export interface RuleSeverityFinding {
18
+ readonly group: string;
19
+ readonly rule: string;
20
+ /** The literal severity value found, or null when the rule has no explicit entry. */
21
+ readonly severity: string | null;
22
+ }
23
+ export interface BiomeConfigGuardResult {
24
+ readonly code: 0 | 1 | 2;
25
+ readonly findings: readonly RuleSeverityFinding[];
26
+ readonly message: string;
27
+ }
28
+ /**
29
+ * Parse a biome.json document (already JSON.parse'd) and report the explicit
30
+ * severity declared for each guarded rule.
31
+ */
32
+ export declare function findRuleSeverities(biomeConfig: unknown, rules?: ReadonlyArray<{
33
+ readonly group: string;
34
+ readonly rule: string;
35
+ }>): RuleSeverityFinding[];
36
+ /**
37
+ * Evaluate biome.json at `projectRoot` and fail closed when a guarded rule
38
+ * has no explicit severity, or an explicit severity of "error".
39
+ */
40
+ export declare function evaluateBiomeConfigGuard(projectRoot?: string): BiomeConfigGuardResult;
41
+ //# sourceMappingURL=biome-config.d.ts.map
@@ -0,0 +1,97 @@
1
+ /**
2
+ * biome-config.ts -- guard against #2190 (biome check . reports
3
+ * recommended-preset diagnostics as errors vs warnings non-deterministically
4
+ * on a pinned biome version).
5
+ *
6
+ * A preset-inherited rule severity can silently change tier across a biome
7
+ * version bump (or, per #2190, apparently even across runs on the identical
8
+ * pinned version). Pinning `noUnusedVariables` / `noNonNullAssertion` to an
9
+ * explicit non-"error" severity in biome.json makes the tier config-owned so
10
+ * it cannot flip a required CI check to failing on pre-existing diagnostics.
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ /** The two rules #2190 observed flipping severity in CI (see docs/analysis/2026-07-02-2190-biome-determinism.md). */
15
+ export const GUARDED_RULES = [
16
+ { group: "correctness", rule: "noUnusedVariables" },
17
+ { group: "style", rule: "noNonNullAssertion" },
18
+ ];
19
+ /** Severity levels biome accepts that never fail the build. */
20
+ const NON_ERROR_LEVELS = new Set(["warn", "info", "off"]);
21
+ function extractSeverity(rulesNode, group, rule) {
22
+ if (typeof rulesNode !== "object" || rulesNode === null) {
23
+ return null;
24
+ }
25
+ const groupNode = rulesNode[group];
26
+ if (typeof groupNode !== "object" || groupNode === null) {
27
+ return null;
28
+ }
29
+ const ruleNode = groupNode[rule];
30
+ if (typeof ruleNode === "string") {
31
+ return ruleNode;
32
+ }
33
+ if (typeof ruleNode === "object" && ruleNode !== null) {
34
+ const level = ruleNode.level;
35
+ return typeof level === "string" ? level : null;
36
+ }
37
+ return null;
38
+ }
39
+ /**
40
+ * Parse a biome.json document (already JSON.parse'd) and report the explicit
41
+ * severity declared for each guarded rule.
42
+ */
43
+ export function findRuleSeverities(biomeConfig, rules = GUARDED_RULES) {
44
+ const linter = typeof biomeConfig === "object" && biomeConfig !== null
45
+ ? biomeConfig.linter
46
+ : null;
47
+ const rulesNode = typeof linter === "object" && linter !== null
48
+ ? linter.rules
49
+ : null;
50
+ return rules.map(({ group, rule }) => ({
51
+ group,
52
+ rule,
53
+ severity: extractSeverity(rulesNode, group, rule),
54
+ }));
55
+ }
56
+ /**
57
+ * Evaluate biome.json at `projectRoot` and fail closed when a guarded rule
58
+ * has no explicit severity, or an explicit severity of "error".
59
+ */
60
+ export function evaluateBiomeConfigGuard(projectRoot = ".") {
61
+ const path = join(projectRoot, "biome.json");
62
+ let raw;
63
+ try {
64
+ raw = readFileSync(path, { encoding: "utf8" });
65
+ }
66
+ catch (err) {
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ return { code: 2, findings: [], message: `biome-config: cannot read ${path}: ${msg}` };
69
+ }
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(raw);
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ return { code: 2, findings: [], message: `biome-config: ${path} is not valid JSON: ${msg}` };
77
+ }
78
+ const findings = findRuleSeverities(parsed);
79
+ const bad = findings.filter((f) => f.severity === null || !NON_ERROR_LEVELS.has(f.severity));
80
+ if (bad.length > 0) {
81
+ const lines = bad.map((f) => ` linter.rules.${f.group}.${f.rule}: ${f.severity === null ? "not explicitly set (inherits preset default)" : `"${f.severity}"`}`);
82
+ return {
83
+ code: 1,
84
+ findings,
85
+ message: "biome-config: guarded rule(s) lack an explicit non-error severity in biome.json (#2190 " +
86
+ "-- a preset-inherited severity can flip to error non-deterministically):\n" +
87
+ `${lines.join("\n")}\n` +
88
+ ' Fix: declare "warn" (or "info") explicitly under linter.rules.<group>.<rule> in biome.json.',
89
+ };
90
+ }
91
+ return {
92
+ code: 0,
93
+ findings,
94
+ message: `biome-config: ${findings.length} guarded rule(s) have explicit non-error severities (#2190).`,
95
+ };
96
+ }
97
+ //# sourceMappingURL=biome-config.js.map
@@ -1,3 +1,4 @@
1
+ export * from "./biome-config.js";
1
2
  export * from "./code-structure-validate.js";
2
3
  export * from "./content-manifest.js";
3
4
  export { CANONICAL_SCHEMA_REL, type ContractDriftOptions, type ContractDriftResult, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
@@ -1,3 +1,4 @@
1
+ export * from "./biome-config.js";
1
2
  export * from "./code-structure-validate.js";
2
3
  export * from "./content-manifest.js";
3
4
  export { CANONICAL_SCHEMA_REL, evaluateContractDrift, PUBLISHED_SCHEMA_REL, } from "./contract-drift.js";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Bounded, ordered set of legacy crossover tokens rewritten in the UNMANAGED
3
+ * region of a consumer AGENTS.md after `migrate:xbrief` (#2154 / Option A).
4
+ *
5
+ * Each entry is a mechanical path / verb literal — NOT freeform prose. The
6
+ * casing-only `vBRIEF format` product-description token from the issue table is
7
+ * intentionally excluded so freeform prose survives untouched. The tokens are
8
+ * disjoint substrings (`.vbrief.json` has no trailing slash, `vbrief:preflight`
9
+ * uses a colon, `vbrief/` requires a slash), so replacement order does not
10
+ * change the result and a second pass is a guaranteed no-op (idempotent).
11
+ */
12
+ export declare const LEGACY_HEADER_TOKENS: ReadonlyArray<{
13
+ readonly legacy: string;
14
+ readonly migrated: string;
15
+ }>;
16
+ export interface HeaderTokenReplacement {
17
+ readonly legacy: string;
18
+ readonly migrated: string;
19
+ readonly count: number;
20
+ }
21
+ export interface HeaderRewriteResult {
22
+ readonly content: string;
23
+ readonly changed: boolean;
24
+ readonly replacements: HeaderTokenReplacement[];
25
+ }
26
+ /**
27
+ * Rewrite known legacy `vbrief` crossover tokens in the UNMANAGED region(s) of
28
+ * an AGENTS.md document, leaving every `<!-- deft:managed-section ... -->` block
29
+ * byte-for-byte intact. Idempotent: running on already-migrated content is a
30
+ * no-op. Managed sections are located by literal markers on the raw text so no
31
+ * line-ending normalisation is performed (UTF-8 / CRLF safe).
32
+ */
33
+ export declare function rewriteUnmanagedHeaderTokens(content: string): HeaderRewriteResult;
34
+ export interface HeaderPatchOutcome {
35
+ readonly kind: "patched" | "clean" | "absent" | "failed";
36
+ readonly path: string;
37
+ readonly replacements: HeaderTokenReplacement[];
38
+ readonly error?: string;
39
+ }
40
+ /**
41
+ * Read AGENTS.md at `projectRoot`, rewrite legacy tokens in the unmanaged
42
+ * header, and write the result back only when something changed. Returns a
43
+ * structured outcome so the caller can log a LEGACY-REPORT-style summary.
44
+ *
45
+ * Non-fatal: a write failure (read-only file, full disk) is captured as a
46
+ * `failed` outcome rather than thrown, so a post-migration header patch can
47
+ * never crash a migration that already succeeded.
48
+ */
49
+ export declare function patchAgentsMdHeader(projectRoot: string, seams?: {
50
+ readText?: (path: string) => string | null;
51
+ writeText?: (path: string, text: string) => void;
52
+ }): HeaderPatchOutcome;
53
+ /** Human-readable one-line summary of a header patch outcome (LEGACY-REPORT style). */
54
+ export declare function renderHeaderPatchSummary(outcome: HeaderPatchOutcome): string;
55
+ export interface StaleHeaderDetection {
56
+ readonly stale: boolean;
57
+ readonly matches: string[];
58
+ }
59
+ /**
60
+ * Detect the #2154 half-migrated state: the `xbrief/` tree exists (lifecycle
61
+ * migration + managed-section refresh already happened) yet the UNMANAGED
62
+ * AGENTS.md header still references legacy `vbrief` path / verb literals — a
63
+ * regression `deft doctor` cannot see because the managed-section byte compare
64
+ * passes (#1308). Returns the matched legacy tokens for the signpost.
65
+ */
66
+ export declare function detectStaleUnmanagedHeader(projectRoot: string, readText?: (path: string) => string | null): StaleHeaderDetection;
67
+ /** One-line doctor / ritual signpost for the #2154 stale-header regression. */
68
+ export declare function renderStaleHeaderLine(projectRoot: string, readText?: (path: string) => string | null): string;
69
+ //# sourceMappingURL=agents-header.d.ts.map
@@ -0,0 +1,179 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { iterManagedSections } from "../platform/agents-md.js";
4
+ import { MIGRATED_ARTIFACT_DIR } from "./constants.js";
5
+ import { isDirectory } from "./fs-helpers.js";
6
+ /**
7
+ * Bounded, ordered set of legacy crossover tokens rewritten in the UNMANAGED
8
+ * region of a consumer AGENTS.md after `migrate:xbrief` (#2154 / Option A).
9
+ *
10
+ * Each entry is a mechanical path / verb literal — NOT freeform prose. The
11
+ * casing-only `vBRIEF format` product-description token from the issue table is
12
+ * intentionally excluded so freeform prose survives untouched. The tokens are
13
+ * disjoint substrings (`.vbrief.json` has no trailing slash, `vbrief:preflight`
14
+ * uses a colon, `vbrief/` requires a slash), so replacement order does not
15
+ * change the result and a second pass is a guaranteed no-op (idempotent).
16
+ */
17
+ export const LEGACY_HEADER_TOKENS = [
18
+ { legacy: ".vbrief.json", migrated: ".xbrief.json" },
19
+ { legacy: "vbrief:preflight", migrated: "xbrief:preflight" },
20
+ { legacy: "vbrief/", migrated: "xbrief/" },
21
+ ];
22
+ function countOccurrences(haystack, needle) {
23
+ if (needle.length === 0)
24
+ return 0;
25
+ let count = 0;
26
+ let index = haystack.indexOf(needle);
27
+ while (index !== -1) {
28
+ count += 1;
29
+ index = haystack.indexOf(needle, index + needle.length);
30
+ }
31
+ return count;
32
+ }
33
+ /** Rewrite the bounded legacy tokens inside a single unmanaged text slice. */
34
+ function rewriteSlice(slice, tally) {
35
+ let next = slice;
36
+ for (const { legacy, migrated } of LEGACY_HEADER_TOKENS) {
37
+ const occurrences = countOccurrences(next, legacy);
38
+ if (occurrences === 0)
39
+ continue;
40
+ tally.set(legacy, (tally.get(legacy) ?? 0) + occurrences);
41
+ next = next.replaceAll(legacy, migrated);
42
+ }
43
+ return next;
44
+ }
45
+ /**
46
+ * Rewrite known legacy `vbrief` crossover tokens in the UNMANAGED region(s) of
47
+ * an AGENTS.md document, leaving every `<!-- deft:managed-section ... -->` block
48
+ * byte-for-byte intact. Idempotent: running on already-migrated content is a
49
+ * no-op. Managed sections are located by literal markers on the raw text so no
50
+ * line-ending normalisation is performed (UTF-8 / CRLF safe).
51
+ */
52
+ export function rewriteUnmanagedHeaderTokens(content) {
53
+ const managed = iterManagedSections(content);
54
+ const tally = new Map();
55
+ let out = "";
56
+ let cursor = 0;
57
+ for (const [start, end] of managed) {
58
+ // Unmanaged slice before this managed block: eligible for rewrite.
59
+ out += rewriteSlice(content.slice(cursor, start), tally);
60
+ // Managed block: preserved verbatim.
61
+ out += content.slice(start, end);
62
+ cursor = end;
63
+ }
64
+ // Trailing unmanaged slice after the last managed block (or the whole file
65
+ // when there is no managed section at all).
66
+ out += rewriteSlice(content.slice(cursor), tally);
67
+ const replacements = LEGACY_HEADER_TOKENS.filter((t) => tally.has(t.legacy)).map((t) => ({
68
+ legacy: t.legacy,
69
+ migrated: t.migrated,
70
+ count: tally.get(t.legacy) ?? 0,
71
+ }));
72
+ return { content: out, changed: out !== content, replacements };
73
+ }
74
+ /**
75
+ * Read AGENTS.md at `projectRoot`, rewrite legacy tokens in the unmanaged
76
+ * header, and write the result back only when something changed. Returns a
77
+ * structured outcome so the caller can log a LEGACY-REPORT-style summary.
78
+ *
79
+ * Non-fatal: a write failure (read-only file, full disk) is captured as a
80
+ * `failed` outcome rather than thrown, so a post-migration header patch can
81
+ * never crash a migration that already succeeded.
82
+ */
83
+ export function patchAgentsMdHeader(projectRoot, seams = {}) {
84
+ const agentsPath = join(projectRoot, "AGENTS.md");
85
+ const readText = seams.readText ??
86
+ ((path) => {
87
+ try {
88
+ if (!existsSync(path))
89
+ return null;
90
+ return readFileSync(path, "utf8");
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ });
96
+ const writeText = seams.writeText ?? ((path, text) => writeFileSync(path, text, "utf8"));
97
+ const existing = readText(agentsPath);
98
+ if (existing === null) {
99
+ return { kind: "absent", path: agentsPath, replacements: [] };
100
+ }
101
+ const result = rewriteUnmanagedHeaderTokens(existing);
102
+ if (!result.changed) {
103
+ return { kind: "clean", path: agentsPath, replacements: [] };
104
+ }
105
+ try {
106
+ writeText(agentsPath, result.content);
107
+ }
108
+ catch (err) {
109
+ return {
110
+ kind: "failed",
111
+ path: agentsPath,
112
+ replacements: [],
113
+ error: err instanceof Error ? err.message : String(err),
114
+ };
115
+ }
116
+ return { kind: "patched", path: agentsPath, replacements: result.replacements };
117
+ }
118
+ /** Human-readable one-line summary of a header patch outcome (LEGACY-REPORT style). */
119
+ export function renderHeaderPatchSummary(outcome) {
120
+ if (outcome.kind === "absent") {
121
+ return "AGENTS.md unmanaged header: no AGENTS.md present — nothing to patch.";
122
+ }
123
+ if (outcome.kind === "clean") {
124
+ return "AGENTS.md unmanaged header: no legacy vbrief tokens found — nothing to patch.";
125
+ }
126
+ if (outcome.kind === "failed") {
127
+ return (`AGENTS.md unmanaged header: patch failed (${outcome.error ?? "unknown error"}) — ` +
128
+ "re-run `deft migrate:xbrief` (idempotent) or hand-edit the header.");
129
+ }
130
+ const total = outcome.replacements.reduce((sum, r) => sum + r.count, 0);
131
+ const detail = outcome.replacements.map((r) => `${r.legacy} ×${r.count}`).join(", ");
132
+ return `AGENTS.md unmanaged header: rewrote ${total} legacy vbrief token(s) -> xbrief (${detail}).`;
133
+ }
134
+ /**
135
+ * Detect the #2154 half-migrated state: the `xbrief/` tree exists (lifecycle
136
+ * migration + managed-section refresh already happened) yet the UNMANAGED
137
+ * AGENTS.md header still references legacy `vbrief` path / verb literals — a
138
+ * regression `deft doctor` cannot see because the managed-section byte compare
139
+ * passes (#1308). Returns the matched legacy tokens for the signpost.
140
+ */
141
+ export function detectStaleUnmanagedHeader(projectRoot, readText = (path) => {
142
+ try {
143
+ if (!existsSync(path))
144
+ return null;
145
+ return readFileSync(path, "utf8");
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }) {
151
+ if (!isDirectory(join(projectRoot, MIGRATED_ARTIFACT_DIR))) {
152
+ return { stale: false, matches: [] };
153
+ }
154
+ const content = readText(join(projectRoot, "AGENTS.md"));
155
+ if (content === null) {
156
+ return { stale: false, matches: [] };
157
+ }
158
+ const managed = iterManagedSections(content);
159
+ let unmanaged = "";
160
+ let cursor = 0;
161
+ for (const [start, end] of managed) {
162
+ unmanaged += content.slice(cursor, start);
163
+ cursor = end;
164
+ }
165
+ unmanaged += content.slice(cursor);
166
+ const matches = LEGACY_HEADER_TOKENS.filter((t) => unmanaged.includes(t.legacy)).map((t) => t.legacy);
167
+ return { stale: matches.length > 0, matches };
168
+ }
169
+ /** One-line doctor / ritual signpost for the #2154 stale-header regression. */
170
+ export function renderStaleHeaderLine(projectRoot, readText) {
171
+ const { stale, matches } = detectStaleUnmanagedHeader(projectRoot, readText);
172
+ if (!stale) {
173
+ return "AGENTS.md header drift: none -- unmanaged header has no legacy vbrief path literals.";
174
+ }
175
+ return (`AGENTS.md header drift: xbrief/ tree present but the unmanaged AGENTS.md header still ` +
176
+ `references legacy token(s) ${matches.join(", ")}. ` +
177
+ "Run `deft migrate:xbrief` (idempotent) to rewrite them, or hand-edit the header.");
178
+ }
179
+ //# sourceMappingURL=agents-header.js.map
@@ -38,6 +38,8 @@ import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIG
38
38
  export const LEGACY_REFERENCE_PREFIX = "x-vbrief/";
39
39
  /** Default allowlist: sanctioned data-plane trees that legitimately retain legacy tokens for back-compat. */
40
40
  export const BUILTIN_ALLOW_LIST = [
41
+ // Vendored npm deposit (#2146): C1 flatten maps content/vbrief/* -> .deft/core/vbrief/*.
42
+ ".deft/core/vbrief/**",
41
43
  // Shipped vBRIEF surface (#1875 C3) + conformance round-trip fixtures (#715).
42
44
  "content/vbrief/**",
43
45
  // Test fixtures exercising the legacy read path + pre-cutover migration (#2108 / #2110)
@@ -0,0 +1,3 @@
1
+ /** True when `path` exists and is a directory; false on any stat error. */
2
+ export declare function isDirectory(path: string): boolean;
3
+ //# sourceMappingURL=fs-helpers.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { statSync } from "node:fs";
2
+ /** True when `path` exists and is a directory; false on any stat error. */
3
+ export function isDirectory(path) {
4
+ try {
5
+ return statSync(path).isDirectory();
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ //# sourceMappingURL=fs-helpers.js.map
@@ -1,3 +1,4 @@
1
+ export { detectStaleUnmanagedHeader, type HeaderPatchOutcome, type HeaderRewriteResult, type HeaderTokenReplacement, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, type StaleHeaderDetection, } from "./agents-header.js";
1
2
  export { detectLegacyVbriefLayout, type LegacyVbriefLayoutDetection, } from "./detect.js";
2
3
  export { BUILTIN_ALLOW_LIST, type DriftEvaluateOptions, type DriftEvaluateResult, type DriftFinding, type DriftScanMode, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
3
4
  export { emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, type XbriefMigrationArgs, type XbriefMigrationIo, type XbriefMigrationOutcome, } from "./migrate-project.js";
@@ -1,3 +1,4 @@
1
+ export { detectStaleUnmanagedHeader, LEGACY_HEADER_TOKENS, patchAgentsMdHeader, renderHeaderPatchSummary, renderStaleHeaderLine, rewriteUnmanagedHeaderTokens, } from "./agents-header.js";
1
2
  export { detectLegacyVbriefLayout, } from "./detect.js";
2
3
  export { BUILTIN_ALLOW_LIST, evaluateXbriefDrift, LEGACY_REFERENCE_PREFIX, scanCorpusToken, } from "./drift-gate.js";
3
4
  export { emitXbriefMigration, runXbriefMigration, runXbriefMigrationCli, } from "./migrate-project.js";
@@ -1,19 +1,13 @@
1
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
1
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
3
  import { checkGitClean } from "../migrate-preflight/index.js";
4
4
  import { agentsRefreshPlan } from "../platform/agents-md.js";
5
+ import { patchAgentsMdHeader, renderHeaderPatchSummary } from "./agents-header.js";
5
6
  import { LEGACY_ARTIFACT_DIR, LEGACY_ARTIFACT_SUFFIX, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, } from "./constants.js";
6
7
  import { detectLegacyVbriefLayout } from "./detect.js";
8
+ import { isDirectory } from "./fs-helpers.js";
7
9
  import { renderXbriefMigrationLine, xbriefMigrationGuidance } from "./signpost.js";
8
10
  import { rewriteEmbeddedTokens, transformArtifactV06ToV08Transactional } from "./transforms.js";
9
- function isDirectory(path) {
10
- try {
11
- return statSync(path).isDirectory();
12
- }
13
- catch {
14
- return false;
15
- }
16
- }
17
11
  function collectFiles(root, acc = []) {
18
12
  if (!isDirectory(root)) {
19
13
  return acc;
@@ -177,6 +171,26 @@ export function emitXbriefMigration(outcome, io, options = {}) {
177
171
  }
178
172
  }
179
173
  }
174
+ /**
175
+ * Rewrite legacy `vbrief` crossover tokens in the UNMANAGED AGENTS.md header
176
+ * after lifecycle migration + agents:refresh (#2154). The managed section is
177
+ * left byte-for-byte intact; only the freeform header/tail path literals are
178
+ * patched. Idempotent and non-fatal — a header with no legacy tokens is a
179
+ * clean no-op, and a write failure surfaces as a `failed` outcome on stderr
180
+ * rather than an exception. Always returns 0; the migration itself already
181
+ * succeeded, so a header-patch hiccup must not fail the whole command.
182
+ */
183
+ function runHeaderPatch(projectRoot, io) {
184
+ const outcome = patchAgentsMdHeader(projectRoot);
185
+ const summary = `${renderHeaderPatchSummary(outcome)}\n`;
186
+ if (outcome.kind === "failed") {
187
+ io.writeErr(summary);
188
+ }
189
+ else {
190
+ io.writeOut(summary);
191
+ }
192
+ return 0;
193
+ }
180
194
  /** End-to-end migrate:xbrief handler including optional agents:refresh (#2110). */
181
195
  export function runXbriefMigrationCli(args, io) {
182
196
  const outcome = runXbriefMigration(args, io);
@@ -184,6 +198,11 @@ export function runXbriefMigrationCli(args, io) {
184
198
  if (code !== 0 || outcome.kind !== "migrated") {
185
199
  return code;
186
200
  }
187
- return runAgentsRefresh(resolve(args.projectRoot), args.frameworkRoot, io);
201
+ const projectRoot = resolve(args.projectRoot);
202
+ const refreshCode = runAgentsRefresh(projectRoot, args.frameworkRoot, io);
203
+ if (refreshCode !== 0) {
204
+ return refreshCode;
205
+ }
206
+ return runHeaderPatch(projectRoot, io);
188
207
  }
189
208
  //# sourceMappingURL=migrate-project.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.66.1",
3
+ "version": "0.67.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,6 +30,14 @@
30
30
  "types": "./dist/wip-cap/index.d.ts",
31
31
  "default": "./dist/wip-cap/index.js"
32
32
  },
33
+ "./agents-md-budget": {
34
+ "types": "./dist/agents-md-budget/index.d.ts",
35
+ "default": "./dist/agents-md-budget/index.js"
36
+ },
37
+ "./agents-md-advisory": {
38
+ "types": "./dist/agents-md-advisory/index.d.ts",
39
+ "default": "./dist/agents-md-advisory/index.js"
40
+ },
33
41
  "./xbrief-migrate": {
34
42
  "types": "./dist/xbrief-migrate/index.d.ts",
35
43
  "default": "./dist/xbrief-migrate/index.js"
@@ -237,8 +245,8 @@
237
245
  "provenance": true
238
246
  },
239
247
  "dependencies": {
240
- "@deftai/directive-content": "^0.66.1",
241
- "@deftai/directive-types": "^0.66.1",
248
+ "@deftai/directive-content": "^0.67.0",
249
+ "@deftai/directive-types": "^0.67.0",
242
250
  "archiver": "^8.0.0"
243
251
  },
244
252
  "scripts": {