@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
@@ -0,0 +1,44 @@
1
+ import { type RegionCounts } from "../agents-md-budget/evaluate.js";
2
+ import { type AgentsMdAdvisorySource } from "../policy/agents-md-advisory.js";
3
+ export type OutputStream = "stdout" | "stderr" | "none";
4
+ /**
5
+ * Result of the consumer AGENTS.md advisory evaluation (#2155).
6
+ *
7
+ * `code` is 0 in the DEFAULT advisory posture no matter what -- the advisory
8
+ * MUST NEVER fail-close a consumer build. Non-zero codes are only ever produced
9
+ * in the explicit `--enforce` opt-in posture (1 = over the hard cap the
10
+ * consumer asked for; 2 = a config problem that blocks an enforced check).
11
+ */
12
+ export interface AdvisoryEvaluateResult {
13
+ readonly code: 0 | 1 | 2;
14
+ readonly message: string;
15
+ readonly stream: OutputStream;
16
+ /** True when the unmanaged region exceeds the soft budget. */
17
+ readonly over: boolean;
18
+ /** Region counts, or null when AGENTS.md was missing / unreadable / malformed. */
19
+ readonly counts: RegionCounts | null;
20
+ /** The resolved soft budget the unmanaged region was compared against. */
21
+ readonly softMaxLines: number;
22
+ /** Provenance of the soft budget (typed / default / default-on-error). */
23
+ readonly source: AgentsMdAdvisorySource;
24
+ }
25
+ export interface AdvisoryEvaluateOptions {
26
+ /**
27
+ * Opt-in hard-cap posture (#1419 enforce tier). When true, an over-budget
28
+ * unmanaged region exits 1 and a config problem exits 2. Default (false) is
29
+ * the advisory posture that always exits 0.
30
+ */
31
+ readonly enforce?: boolean;
32
+ /** Suppress the within-budget "OK" line (used by aggregate callers). */
33
+ readonly quiet?: boolean;
34
+ }
35
+ /**
36
+ * Evaluate the consumer AGENTS.md against its soft, unmanaged-focused budget.
37
+ *
38
+ * Reuses the #645 region counter (`countRegions`) so the framework-owned
39
+ * managed section is EXCLUDED from the comparison -- only the consumer-authored
40
+ * unmanaged region is measured. In the default advisory posture the result code
41
+ * is always 0; `--enforce` promotes over-budget / config problems to non-zero.
42
+ */
43
+ export declare function evaluate(projectRoot: string, options?: AdvisoryEvaluateOptions): AdvisoryEvaluateResult;
44
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1,115 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { countRegions } from "../agents-md-budget/evaluate.js";
4
+ import { resolveAgentsMdAdvisory, } from "../policy/agents-md-advisory.js";
5
+ const GUIDANCE_LINE = " AGENTS.md is a map, not a manual (#1882): push project detail into a\n" +
6
+ " reference doc and leave a pointer, rather than growing AGENTS.md. See\n" +
7
+ " content/docs/good-agents-md.md.";
8
+ function overMessage(counts, softMax, enforce, fieldName) {
9
+ const over = counts.unmanaged - softMax;
10
+ if (enforce) {
11
+ return (`❌ agents-md-advisory: AGENTS.md unmanaged (project-authored) region is ` +
12
+ `${counts.unmanaged} lines, over the enforced cap of ${softMax} (OVER by ${over}).\n` +
13
+ `${GUIDANCE_LINE}\n` +
14
+ ` Raise ${fieldName} in PROJECT-DEFINITION to accept the growth.`);
15
+ }
16
+ return (`⚠ agents-md-advisory: AGENTS.md unmanaged (project-authored) region is ` +
17
+ `${counts.unmanaged} lines, over the soft budget of ${softMax} (OVER by ${over}). ` +
18
+ "This is advisory only -- your build is NOT affected.\n" +
19
+ `${GUIDANCE_LINE}\n` +
20
+ ` This is your knob: raise ${fieldName} in PROJECT-DEFINITION to accept the\n` +
21
+ " growth and silence this note.");
22
+ }
23
+ const FIELD_NAME = "plan.policy.agentsMdAdvisory.unmanagedSoftMaxLines";
24
+ /**
25
+ * Evaluate the consumer AGENTS.md against its soft, unmanaged-focused budget.
26
+ *
27
+ * Reuses the #645 region counter (`countRegions`) so the framework-owned
28
+ * managed section is EXCLUDED from the comparison -- only the consumer-authored
29
+ * unmanaged region is measured. In the default advisory posture the result code
30
+ * is always 0; `--enforce` promotes over-budget / config problems to non-zero.
31
+ */
32
+ export function evaluate(projectRoot, options = {}) {
33
+ const root = resolve(projectRoot);
34
+ const enforce = options.enforce ?? false;
35
+ const quiet = options.quiet ?? false;
36
+ const advisory = resolveAgentsMdAdvisory(root);
37
+ const softMax = advisory.config.unmanagedSoftMaxLines;
38
+ const agentsPath = join(root, "AGENTS.md");
39
+ if (!existsSync(agentsPath)) {
40
+ const message = `agents-md-advisory: no AGENTS.md found at ${agentsPath}`;
41
+ return {
42
+ code: enforce ? 2 : 0,
43
+ message: enforce ? `❌ ${message}` : quiet ? "" : `${message} (advisory; skipped).`,
44
+ stream: enforce ? "stderr" : quiet ? "none" : "stdout",
45
+ over: false,
46
+ counts: null,
47
+ softMaxLines: softMax,
48
+ source: advisory.source,
49
+ };
50
+ }
51
+ let text;
52
+ try {
53
+ text = readFileSync(agentsPath, { encoding: "utf8" });
54
+ }
55
+ catch (err) {
56
+ const message = `agents-md-advisory: AGENTS.md at ${agentsPath} cannot be read: ${String(err)}`;
57
+ return {
58
+ code: enforce ? 2 : 0,
59
+ message: enforce ? `❌ ${message}` : quiet ? "" : `${message} (advisory; skipped).`,
60
+ stream: enforce ? "stderr" : quiet ? "none" : "stdout",
61
+ over: false,
62
+ counts: null,
63
+ softMaxLines: softMax,
64
+ source: advisory.source,
65
+ };
66
+ }
67
+ const regionResult = countRegions(text);
68
+ if ("error" in regionResult) {
69
+ const message = `agents-md-advisory: ${regionResult.error}`;
70
+ return {
71
+ code: enforce ? 2 : 0,
72
+ message: enforce ? `❌ ${message}` : quiet ? "" : `${message} (advisory; skipped).`,
73
+ stream: enforce ? "stderr" : quiet ? "none" : "stdout",
74
+ over: false,
75
+ counts: null,
76
+ softMaxLines: softMax,
77
+ source: advisory.source,
78
+ };
79
+ }
80
+ const counts = regionResult.counts;
81
+ const over = counts.unmanaged > softMax;
82
+ if (!over) {
83
+ if (quiet) {
84
+ return {
85
+ code: 0,
86
+ message: "",
87
+ stream: "none",
88
+ over: false,
89
+ counts,
90
+ softMaxLines: softMax,
91
+ source: advisory.source,
92
+ };
93
+ }
94
+ return {
95
+ code: 0,
96
+ message: `✓ agents-md-advisory: AGENTS.md unmanaged region ${counts.unmanaged}/${softMax} lines ` +
97
+ `within soft budget (managed ${counts.managed} excluded; advisory).`,
98
+ stream: "stdout",
99
+ over: false,
100
+ counts,
101
+ softMaxLines: softMax,
102
+ source: advisory.source,
103
+ };
104
+ }
105
+ return {
106
+ code: enforce ? 1 : 0,
107
+ message: overMessage(counts, softMax, enforce, FIELD_NAME),
108
+ stream: "stderr",
109
+ over: true,
110
+ counts,
111
+ softMaxLines: softMax,
112
+ source: advisory.source,
113
+ };
114
+ }
115
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,2 @@
1
+ export * from "./evaluate.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from "./evaluate.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,38 @@
1
+ export type OutputStream = "stdout" | "stderr" | "none";
2
+ /** Result of verify:agents-md-budget evaluation; three-state exit contract. */
3
+ export interface EvaluateResult {
4
+ readonly code: 0 | 1 | 2;
5
+ readonly message: string;
6
+ readonly stream: OutputStream;
7
+ }
8
+ export interface EvaluateOptions {
9
+ readonly quiet?: boolean;
10
+ }
11
+ /** Per-region line counts of an AGENTS.md file. */
12
+ export interface RegionCounts {
13
+ readonly total: number;
14
+ readonly managed: number;
15
+ readonly unmanaged: number;
16
+ }
17
+ /**
18
+ * Split AGENTS.md into managed / unmanaged line counts.
19
+ *
20
+ * Returns `{ counts }` on success, or `{ error }` when the managed markers are
21
+ * malformed (exactly one marker present, or close-before-open). A file with no
22
+ * markers at all is valid: the whole file counts as unmanaged.
23
+ */
24
+ export declare function countRegions(text: string): {
25
+ counts: RegionCounts;
26
+ } | {
27
+ error: string;
28
+ };
29
+ /**
30
+ * Pure evaluator for the AGENTS.md line-budget ratchet gate (#645).
31
+ *
32
+ * Counts the managed section and the unmanaged region separately (the #1309
33
+ * propagation duplicates content across the marker) and fails when either
34
+ * region exceeds its typed per-region budget. Ships green because the budget is
35
+ * seeded at current size; growth past the ratchet fails.
36
+ */
37
+ export declare function evaluate(projectRoot: string, options?: EvaluateOptions): EvaluateResult;
38
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1,152 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { AGENTS_MANAGED_CLOSE } from "../platform/constants.js";
4
+ import { resolveAgentsMdBudget } from "../policy/agents-md-budget.js";
5
+ const OPEN_MARKER_PREFIX = "<!-- deft:managed-section";
6
+ /**
7
+ * Split AGENTS.md into managed / unmanaged line counts.
8
+ *
9
+ * Returns `{ counts }` on success, or `{ error }` when the managed markers are
10
+ * malformed (exactly one marker present, or close-before-open). A file with no
11
+ * markers at all is valid: the whole file counts as unmanaged.
12
+ */
13
+ export function countRegions(text) {
14
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
15
+ // Ignore the trailing empty element produced by a final newline.
16
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
17
+ lines.pop();
18
+ }
19
+ const total = lines.length;
20
+ const openLine = lines.findIndex((l) => l.startsWith(OPEN_MARKER_PREFIX));
21
+ const closeLine = lines.findIndex((l) => l.trim().startsWith(AGENTS_MANAGED_CLOSE));
22
+ const openCount = lines.filter((l) => l.startsWith(OPEN_MARKER_PREFIX)).length;
23
+ const closeCount = lines.filter((l) => l.trim().startsWith(AGENTS_MANAGED_CLOSE)).length;
24
+ if (openLine === -1 && closeLine === -1) {
25
+ return { counts: { total, managed: 0, unmanaged: total } };
26
+ }
27
+ // Malformed if a marker is missing, close precedes open, or either marker is
28
+ // duplicated -- the contract promises exactly one open/close pair.
29
+ if (openLine === -1 ||
30
+ closeLine === -1 ||
31
+ closeLine < openLine ||
32
+ openCount > 1 ||
33
+ closeCount > 1) {
34
+ return {
35
+ error: "AGENTS.md managed-section markers are malformed " +
36
+ `(open@${openLine === -1 ? "none" : openLine + 1}×${openCount}, ` +
37
+ `close@${closeLine === -1 ? "none" : closeLine + 1}×${closeCount}); ` +
38
+ "expected a single <!-- deft:managed-section ... --> ... " +
39
+ "<!-- /deft:managed-section --> pair.",
40
+ };
41
+ }
42
+ const managed = closeLine - openLine + 1;
43
+ return { counts: { total, managed, unmanaged: total - managed } };
44
+ }
45
+ function formatRefusal(counts, managedMax, unmanagedMax, projectRoot) {
46
+ const over = [];
47
+ if (counts.managed > managedMax) {
48
+ over.push(` managed region: ${counts.managed}/${managedMax} lines (OVER by ${counts.managed - managedMax})`);
49
+ }
50
+ if (counts.unmanaged > unmanagedMax) {
51
+ over.push(` unmanaged region: ${counts.unmanaged}/${unmanagedMax} lines (OVER by ${counts.unmanaged - unmanagedMax})`);
52
+ }
53
+ return (`❌ verify:agents-md-budget: AGENTS.md grew past its ratchet ` +
54
+ `(project_root=${projectRoot}).\n` +
55
+ `${over.join("\n")}\n` +
56
+ " AGENTS.md is a map, not a manual (#1882): push detail into a\n" +
57
+ " reference doc (main.md / a pack / docs/) and leave a pointer,\n" +
58
+ " rather than expanding AGENTS.md. See REFERENCES.md.\n" +
59
+ " If the growth is deliberate, raise the matching line in\n" +
60
+ " plan.policy.agentsMdBudget in PROJECT-DEFINITION (a reviewed diff). (#645)");
61
+ }
62
+ /**
63
+ * Pure evaluator for the AGENTS.md line-budget ratchet gate (#645).
64
+ *
65
+ * Counts the managed section and the unmanaged region separately (the #1309
66
+ * propagation duplicates content across the marker) and fails when either
67
+ * region exceeds its typed per-region budget. Ships green because the budget is
68
+ * seeded at current size; growth past the ratchet fails.
69
+ */
70
+ export function evaluate(projectRoot, options = {}) {
71
+ const root = resolve(projectRoot);
72
+ const quiet = options.quiet ?? false;
73
+ const budgetResult = resolveAgentsMdBudget(root);
74
+ if (budgetResult.source === "default-on-error") {
75
+ return {
76
+ code: 2,
77
+ message: `❌ verify:agents-md-budget: PROJECT-DEFINITION malformed: ${budgetResult.error}`,
78
+ stream: "stderr",
79
+ };
80
+ }
81
+ const agentsPath = join(root, "AGENTS.md");
82
+ if (!existsSync(agentsPath)) {
83
+ return {
84
+ code: 2,
85
+ message: `❌ verify:agents-md-budget: AGENTS.md not found at ${agentsPath}`,
86
+ stream: "stderr",
87
+ };
88
+ }
89
+ let text;
90
+ try {
91
+ text = readFileSync(agentsPath, { encoding: "utf8" });
92
+ }
93
+ catch (err) {
94
+ return {
95
+ code: 2,
96
+ message: `❌ verify:agents-md-budget: AGENTS.md at ${agentsPath} cannot be read: ${String(err)}`,
97
+ stream: "stderr",
98
+ };
99
+ }
100
+ const regionResult = countRegions(text);
101
+ if ("error" in regionResult) {
102
+ return {
103
+ code: 2,
104
+ message: `❌ verify:agents-md-budget: ${regionResult.error}`,
105
+ stream: "stderr",
106
+ };
107
+ }
108
+ const counts = regionResult.counts;
109
+ if (budgetResult.source === "unset") {
110
+ if (quiet) {
111
+ return { code: 0, message: "", stream: "none" };
112
+ }
113
+ return {
114
+ code: 0,
115
+ message: "⚠ verify:agents-md-budget: no plan.policy.agentsMdBudget configured " +
116
+ `(managed=${counts.managed}, unmanaged=${counts.unmanaged} lines).\n` +
117
+ " Seed a ratchet at current size to freeze growth (#645): set\n" +
118
+ " plan.policy.agentsMdBudget.{managedMaxLines,unmanagedMaxLines} in " +
119
+ "PROJECT-DEFINITION.",
120
+ stream: "stderr",
121
+ };
122
+ }
123
+ /* v8 ignore start -- defensive: source "typed" always carries a non-null budget. */
124
+ if (budgetResult.budget === null) {
125
+ return {
126
+ code: 2,
127
+ message: "❌ verify:agents-md-budget: unexpected null budget for typed source",
128
+ stream: "stderr",
129
+ };
130
+ }
131
+ /* v8 ignore stop */
132
+ const budget = budgetResult.budget;
133
+ const overManaged = counts.managed > budget.managedMaxLines;
134
+ const overUnmanaged = counts.unmanaged > budget.unmanagedMaxLines;
135
+ if (!overManaged && !overUnmanaged) {
136
+ if (quiet) {
137
+ return { code: 0, message: "", stream: "none" };
138
+ }
139
+ return {
140
+ code: 0,
141
+ message: `✓ verify:agents-md-budget: managed ${counts.managed}/${budget.managedMaxLines}, ` +
142
+ `unmanaged ${counts.unmanaged}/${budget.unmanagedMaxLines} lines (within ratchet).`,
143
+ stream: "stdout",
144
+ };
145
+ }
146
+ return {
147
+ code: 1,
148
+ message: formatRefusal(counts, budget.managedMaxLines, budget.unmanagedMaxLines, root),
149
+ stream: "stderr",
150
+ };
151
+ }
152
+ //# sourceMappingURL=evaluate.js.map
@@ -0,0 +1,2 @@
1
+ export * from "./evaluate.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from "./evaluate.js";
2
+ //# sourceMappingURL=index.js.map
@@ -1,6 +1,7 @@
1
1
  export declare const GENERATED_SENTINEL = "AUTO-GENERATED by task codebase:map";
2
2
  export declare const DEFAULT_OUTPUT_PATH = ".planning/codebase/MAP.md";
3
3
  export declare const BANNER_TEMPLATE: string;
4
+ export declare function markdownText(value: unknown): string;
4
5
  export declare function projectionOutputPath(projectRoot: string, explicitOutput?: string | null): string;
5
6
  export declare function renderCodebaseMap(artifact: Record<string, unknown>): string;
6
7
  export declare function isDeftGenerated(path: string): boolean;
@@ -13,9 +13,12 @@ export const BANNER_TEMPLATE = "<!-- AUTO-GENERATED by task codebase:map -- DO N
13
13
  "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json " +
14
14
  "plan.architecture.codeStructure -->\n" +
15
15
  "<!-- Regenerate with: task codebase:map -->\n";
16
- function markdownText(value) {
16
+ export function markdownText(value) {
17
17
  const text = value === null || value === undefined ? "" : String(value);
18
- return text.replace(/\|/g, "\\|").replace(/\n/g, " ").trim();
18
+ // Escape the backslash escape character FIRST, then the `|` table delimiter.
19
+ // Escaping `|` alone lets a pre-existing backslash bypass the delimiter
20
+ // escape (CodeQL js/incomplete-sanitization).
21
+ return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n/g, " ").trim();
19
22
  }
20
23
  function code(value) {
21
24
  const text = markdownText(value);
@@ -6,7 +6,11 @@ export declare const DEPRECATED_REDIRECT_SENTINEL = "<!-- deft:deprecated-redire
6
6
  export declare const DEPRECATED_SKILL_REDIRECT_SENTINEL = "<!-- deft:deprecated-skill-redirect -->";
7
7
  export declare const REDIRECT_STUB_HEADER_LINES = 8;
8
8
  export declare const TASKFILE_INCLUDE_SNIPPET = "version: '3'\n\nincludes:\n deft:\n taskfile: ./.deft/core/Taskfile.yml\n optional: true\n";
9
- export declare const DOCTOR_ALLOWED_FLAGS: readonly ["--session", "--fix", "--repair", "--repair-taskfile", "--json", "--quiet", "--full", "--project-root", "-h", "--help"];
9
+ export declare const DOCTOR_ALLOWED_FLAGS: readonly ["--session", "--fix", "--repair", "--repair-taskfile", "--json", "--quiet", "--full", "--network", "--project-root", "-h", "--help"];
10
+ /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
11
+ export declare const NPM_PACKAGE_NAME = "@deftai/directive";
12
+ export declare const NETWORK_DISCLOSURE_LINE: string;
13
+ export declare const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE: string;
10
14
  export declare const EXPECTED_FRAMEWORK_DIRS: readonly ["tasks", "scripts", "vbrief"];
11
15
  /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
12
16
  export declare const CONSUMER_FRAMEWORK_DIRS: readonly ["tasks", "vbrief"];
@@ -14,7 +18,6 @@ export declare const DEFT_REPO_POSITIVE_MARKERS: readonly ["content/templates/ag
14
18
  export declare const EXPECTED_CONTENT_DIRS: readonly ["languages", "strategies", "skills", "templates"];
15
19
  /** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
16
20
  export declare const CANONICAL_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
17
- export declare const NPM_PACKAGE_NAME = "@deftai/directive";
18
21
  export declare const CLEAN_WINDOW_HOURS = 24;
19
22
  export declare const DIRTY_WINDOW_HOURS = 4;
20
23
  export declare const ENV_STATE_PATH = "DEFT_DOCTOR_STATE_PATH";
@@ -20,10 +20,25 @@ export const DOCTOR_ALLOWED_FLAGS = [
20
20
  "--json",
21
21
  "--quiet",
22
22
  "--full",
23
+ "--network",
23
24
  "--project-root",
24
25
  "-h",
25
26
  "--help",
26
27
  ];
28
+ /** npm consumer deposit after #2022 Phase 3 -- Python scripts/ tree is intentionally absent. */
29
+ export const NPM_PACKAGE_NAME = "@deftai/directive";
30
+ // #2182: payload-staleness is the only doctor check that can reach a network
31
+ // endpoint (git ls-remote against the framework's git remote, falling back to
32
+ // `npm view` against the npm registry). It is OFF by default (offline tier)
33
+ // and requires the explicit `--network` flag; this line discloses exactly
34
+ // which tool + registry class it may contact BEFORE the check runs, and the
35
+ // skip line tells an offline run how to opt in.
36
+ export const NETWORK_DISCLOSURE_LINE = "[deft doctor] --network: this run may contact your git remote (framework " +
37
+ "repo host) and, as a fallback, the npm registry (registry.npmjs.org) to " +
38
+ `look up the latest ${NPM_PACKAGE_NAME} version.`;
39
+ export const PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE = "skip -- offline tier (default). Run `deft doctor --network` to check " +
40
+ "framework currency against your git remote and the npm registry " +
41
+ "(discloses tool + registry before contacting either).";
27
42
  // Engine / lifecycle dirs that stay at the framework root (NOT relocated by
28
43
  // #1875). Shippable-content dirs moved under content/ -- see EXPECTED_CONTENT_DIRS.
29
44
  export const EXPECTED_FRAMEWORK_DIRS = ["tasks", "scripts", "vbrief"];
@@ -45,7 +60,6 @@ export const DEFT_REPO_POSITIVE_MARKERS = [
45
60
  export const EXPECTED_CONTENT_DIRS = ["languages", "strategies", "skills", "templates"];
46
61
  /** Post-freeze canonical upgrade path (#1997 / #2003 / #1912). */
47
62
  export const CANONICAL_UPGRADE_COMMAND = "npm i -g @deftai/directive@latest";
48
- export const NPM_PACKAGE_NAME = "@deftai/directive";
49
63
  export const CLEAN_WINDOW_HOURS = 24;
50
64
  export const DIRTY_WINDOW_HOURS = 4;
51
65
  export const ENV_STATE_PATH = "DEFT_DOCTOR_STATE_PATH";
@@ -5,6 +5,7 @@ export function parseDoctorFlags(args) {
5
5
  let json = false;
6
6
  let quiet = false;
7
7
  let full = false;
8
+ let network = false;
8
9
  let help = false;
9
10
  let projectRoot = null;
10
11
  const unknown = [];
@@ -26,6 +27,9 @@ export function parseDoctorFlags(args) {
26
27
  else if (token === "--full") {
27
28
  full = true;
28
29
  }
30
+ else if (token === "--network") {
31
+ network = true;
32
+ }
29
33
  else if (token === "-h" || token === "--help") {
30
34
  help = true;
31
35
  }
@@ -52,7 +56,7 @@ export function parseDoctorFlags(args) {
52
56
  }
53
57
  i += 1;
54
58
  }
55
- return { session, fix, json, quiet, full, help, projectRoot, unknown };
59
+ return { session, fix, json, quiet, full, network, help, projectRoot, unknown };
56
60
  }
57
61
  export function formatUnknownFlagsError(unknown) {
58
62
  return `Unknown flag(s): ${unknown.join(", ")}`;
@@ -1,9 +1,10 @@
1
1
  import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
+ import { evaluate as evaluateAgentsMdAdvisory } from "../agents-md-advisory/evaluate.js";
3
4
  import { contentRoot } from "../content-root.js";
4
5
  import { agentsRefreshPlan, hasV3ManagedMarker } from "./agents-md.js";
5
6
  import { runChecks } from "./checks.js";
6
- import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
7
+ import { CONSUMER_FRAMEWORK_DIRS, EXPECTED_CONTENT_DIRS, EXPECTED_FRAMEWORK_DIRS, NETWORK_DISCLOSURE_LINE, PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE, TASKFILE_INCLUDE_SNIPPET, UV_INSTALL_URL, } from "./constants.js";
7
8
  import { decideThrottle, formatIsoZ, readState, renderDoctorStatusLine, writeState, } from "./doctor-state.js";
8
9
  import { formatAllowedFlagsHint, formatUnknownFlagsError, parseDoctorFlags } from "./flags.js";
9
10
  import { pythonJsonDump } from "./json.js";
@@ -130,13 +131,38 @@ export function cmdDoctor(args, seams = {}) {
130
131
  if (!jsonMode) {
131
132
  sink.blank();
132
133
  }
133
- sink.info("Checking payload staleness from install manifest...");
134
- runPayloadStalenessCheck(projectRoot, sink, addFinding, {
135
- frameworkRoot,
136
- readText: seams.readText,
137
- isFile: seams.isFile,
138
- runGitLsRemote: seams.runGitLsRemote,
139
- });
134
+ sink.info("Checking AGENTS.md legibility (advisory)...");
135
+ runAgentsMdAdvisoryCheck(projectRoot, sink, addFinding, seams);
136
+ if (!jsonMode) {
137
+ sink.blank();
138
+ }
139
+ // #2182: payload-staleness is the only doctor check that can reach a
140
+ // registry (git remote, then npm as a fallback). It stays in the OFFLINE
141
+ // tier (skipped) unless the operator explicitly opts into the NETWORK tier
142
+ // via `--network`, and the tool + registry class is disclosed BEFORE the
143
+ // check runs -- never silently, never as a side effect of a read-only
144
+ // `deft doctor` invocation (e.g. from the gated session-ritual step).
145
+ if (flags.network) {
146
+ sink.info(NETWORK_DISCLOSURE_LINE);
147
+ sink.info("Checking payload staleness from install manifest...");
148
+ runPayloadStalenessCheck(projectRoot, sink, addFinding, {
149
+ frameworkRoot,
150
+ readText: seams.readText,
151
+ isFile: seams.isFile,
152
+ runGitLsRemote: seams.runGitLsRemote,
153
+ runNpmViewVersion: seams.runNpmViewVersion,
154
+ });
155
+ }
156
+ else {
157
+ sink.info(`payload-staleness: ${PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE}`);
158
+ addFinding({
159
+ severity: "skip",
160
+ message: PAYLOAD_STALENESS_OFFLINE_SKIP_MESSAGE,
161
+ check: "payload-staleness",
162
+ status: "skip",
163
+ reason: "offline-tier",
164
+ });
165
+ }
140
166
  if (!jsonMode) {
141
167
  sink.blank();
142
168
  }
@@ -346,6 +372,71 @@ function runAgentsMdFreshnessCheck(projectRoot, sink, addFinding, seams) {
346
372
  addFinding({ severity: "warning", message, check: checkName });
347
373
  }
348
374
  }
375
+ /**
376
+ * Advisory (never-failing) consumer AGENTS.md legibility signal (#2155).
377
+ *
378
+ * Reports the unmanaged (project-authored) region size against the soft budget
379
+ * (`plan.policy.agentsMdAdvisory.unmanagedSoftMaxLines`, generous by default).
380
+ * The managed section is EXCLUDED. This is the consumer companion to the
381
+ * maintainer-only #645 ratchet and follows the advise -> observe -> enforce
382
+ * posture (#1419): it emits at most a `warning` finding, NEVER an `error`, so
383
+ * `deft doctor` (and the `check:consumer` aggregate that depends on it) can
384
+ * never fail-close on a judgment call about the consumer's own file.
385
+ */
386
+ function runAgentsMdAdvisoryCheck(projectRoot, sink, addFinding, seams) {
387
+ const checkName = "agents-md-advisory";
388
+ // Only meaningful for consumer installs (managed markers present). In the
389
+ // maintainer repo the #645 ratchet owns this file, so skip -- mirrors the
390
+ // freshness check's guard above.
391
+ if (runningInsideDeftRepo(projectRoot, seams) ||
392
+ !hasV3ManagedMarker(projectRoot, seams.readText)) {
393
+ const skipReason = "no managed-section markers (likely maintainer repo)";
394
+ sink.info(`${checkName}: skip -- ${skipReason}`);
395
+ addFinding({ severity: "skip", message: skipReason, check: checkName, status: "skip" });
396
+ return;
397
+ }
398
+ try {
399
+ const evalFn = seams.agentsMdAdvisoryEvaluate ?? ((root) => evaluateAgentsMdAdvisory(root));
400
+ const result = evalFn(projectRoot);
401
+ if (result.over && result.counts !== null) {
402
+ const message = `AGENTS.md unmanaged (project-authored) region is ${result.counts.unmanaged} lines, ` +
403
+ `over the soft budget of ${result.softMaxLines} (advisory only -- your build is NOT ` +
404
+ "affected). AGENTS.md is a map, not a manual (#1882): push detail into a reference doc " +
405
+ "and leave a pointer (see content/docs/good-agents-md.md). Raise " +
406
+ "plan.policy.agentsMdAdvisory.unmanagedSoftMaxLines to accept the growth and silence this.";
407
+ sink.warn(message);
408
+ addFinding({
409
+ severity: "warning",
410
+ message,
411
+ check: checkName,
412
+ status: "over-soft-budget",
413
+ suggestion: "content/docs/good-agents-md.md",
414
+ });
415
+ return;
416
+ }
417
+ if (result.counts !== null) {
418
+ sink.success(`${checkName}: unmanaged region ${result.counts.unmanaged}/${result.softMaxLines} lines (within soft budget)`);
419
+ return;
420
+ }
421
+ // No counts (missing / unreadable / malformed AGENTS.md): advisory stays
422
+ // silent-but-informational; never a doctor error.
423
+ sink.info(`${checkName}: skip -- AGENTS.md not measurable`);
424
+ addFinding({
425
+ severity: "skip",
426
+ message: "AGENTS.md not measurable",
427
+ check: checkName,
428
+ status: "skip",
429
+ });
430
+ }
431
+ catch (exc) {
432
+ // Sanitize newlines so an error string can't break out of the markdown
433
+ // bullet when findings are rendered (CWE-116).
434
+ const detail = `${exc instanceof Error ? exc.name : "Error"}: ${exc}`.replace(/\r?\n/g, " ");
435
+ const message = `${checkName}: probe failed -- ${detail}`;
436
+ sink.warn(message);
437
+ addFinding({ severity: "warning", message, check: checkName });
438
+ }
439
+ }
349
440
  function runTaskfileIncludeCheck(projectRoot, fixMode, jsonMode, sink, addFinding, seams) {
350
441
  if (runningInsideDeftRepo(projectRoot, seams)) {
351
442
  sink.info("Skipping Taskfile include check -- running inside the deft framework repo (the repo's own Taskfile.yml is the surface).");
@@ -2,6 +2,7 @@
2
2
  export declare function resolvePath(pathStr: string, cwd?: string): string;
3
3
  /** Best-effort version string (mirrors Python `_resolve_version`). */
4
4
  export declare function resolveVersion(frameworkRoot?: string): string;
5
+ export declare function resolveFrameworkRootForProject(projectRoot: string, explicitRoot?: string | null): string;
5
6
  /** Return the deft framework repo root (the directory containing `main.md`). */
6
7
  export declare function resolveDefaultFrameworkRoot(): string;
7
8
  /** Directory containing doctor implementation (Python: scripts/). */