@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
@@ -32,6 +32,61 @@ export function resolveVersion(frameworkRoot) {
32
32
  }
33
33
  return "dev";
34
34
  }
35
+ /**
36
+ * Resolve the framework root for a CLI command running against `projectRoot`.
37
+ *
38
+ * Priority: explicit `--deft-root` / `--framework-root` → `DEFT_ROOT` env →
39
+ * maintainer source checkout at `projectRoot` (when `main.md` + framework markers
40
+ * are present, even if a dogfood `.deft/core` deposit also exists) → consumer
41
+ * deposit (`.deft/core`, legacy `deft/`) → maintainer checkout walk
42
+ * (`resolveDefaultFrameworkRoot`).
43
+ *
44
+ * Refs #2146 (npm global `deft` must not default to `node_modules/vbrief/schemas`).
45
+ */
46
+ function isFrameworkSourceCheckoutAt(projectRoot) {
47
+ try {
48
+ if (!statSync(join(projectRoot, "main.md")).isFile()) {
49
+ return false;
50
+ }
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ return DEFT_REPO_POSITIVE_MARKERS.every((marker) => {
56
+ try {
57
+ return statSync(join(projectRoot, marker)).isFile();
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ });
63
+ }
64
+ export function resolveFrameworkRootForProject(projectRoot, explicitRoot) {
65
+ const explicit = explicitRoot?.trim();
66
+ if (explicit) {
67
+ return resolve(explicit);
68
+ }
69
+ const envRoot = process.env.DEFT_ROOT?.trim();
70
+ if (envRoot) {
71
+ return resolve(envRoot);
72
+ }
73
+ const root = resolve(projectRoot);
74
+ if (isFrameworkSourceCheckoutAt(root)) {
75
+ return root;
76
+ }
77
+ for (const rel of [join(".deft", "core"), "deft"]) {
78
+ const candidate = join(root, rel);
79
+ try {
80
+ if (statSync(candidate).isDirectory()) {
81
+ return candidate;
82
+ }
83
+ }
84
+ catch {
85
+ // try next candidate
86
+ }
87
+ }
88
+ return resolveDefaultFrameworkRoot();
89
+ }
35
90
  /** Return the deft framework repo root (the directory containing `main.md`). */
36
91
  export function resolveDefaultFrameworkRoot() {
37
92
  const envRoot = process.env.DEFT_ROOT?.trim();
@@ -1,3 +1,4 @@
1
+ import type { AdvisoryEvaluateResult } from "../agents-md-advisory/evaluate.js";
1
2
  export declare const EXIT_CLEAN = 0;
2
3
  export declare const EXIT_DRIFT = 1;
3
4
  export declare const EXIT_CONFIG_ERROR = 2;
@@ -30,6 +31,7 @@ export interface DoctorFlags {
30
31
  readonly json: boolean;
31
32
  readonly quiet: boolean;
32
33
  readonly full: boolean;
34
+ readonly network: boolean;
33
35
  readonly help: boolean;
34
36
  readonly projectRoot: string | null;
35
37
  readonly unknown: readonly string[];
@@ -60,7 +62,12 @@ export interface DoctorSeams {
60
62
  ok: boolean;
61
63
  stdout: string;
62
64
  };
65
+ readonly runNpmViewVersion?: () => {
66
+ ok: boolean;
67
+ version: string;
68
+ };
63
69
  readonly agentsRefreshPlan?: (projectRoot: string) => Record<string, unknown>;
70
+ readonly agentsMdAdvisoryEvaluate?: (projectRoot: string) => AdvisoryEvaluateResult;
64
71
  readonly readState?: (projectRoot: string) => DoctorState | null;
65
72
  readonly writeState?: (projectRoot: string, payload: {
66
73
  exitCode: number;
@@ -48,6 +48,7 @@ export interface GitHubAuthModesCliArgs {
48
48
  githubAuthMode?: string | null;
49
49
  repo?: string;
50
50
  json?: boolean;
51
+ runGh?: GhRunner;
51
52
  }
52
53
  export declare function githubAuthModesMain(args: GitHubAuthModesCliArgs): number;
53
54
  export { probeRuntimeCapabilities };
@@ -257,6 +257,7 @@ export function resultToDict(result) {
257
257
  export function githubAuthModesMain(args) {
258
258
  const result = validateGithubAuthForWorker(args.githubAuthMode ?? null, {
259
259
  repo: args.repo ?? DEFAULT_VALIDATION_REPO,
260
+ runGh: args.runGh,
260
261
  });
261
262
  if (args.json) {
262
263
  process.stdout.write(`${JSON.stringify(resultToDict(result), null, 2)}\n`);
@@ -1,3 +1,4 @@
1
+ import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY } from "../xbrief-migrate/constants.js";
1
2
  import { type ScmCallFn } from "./reconcile-issues.js";
2
3
  export declare const INGEST_STATUSES: readonly ["proposed", "pending", "active"];
3
4
  export type IngestStatus = (typeof INGEST_STATUSES)[number];
@@ -22,8 +23,11 @@ export declare function scanProvenanceRefs(vbriefDir: string): Map<number, strin
22
23
  export declare function composeOverviewWithComments(body: string, comments: readonly IssueComment[]): string;
23
24
  export declare function issueCommentThread(issue: Record<string, unknown>): IssueComment[];
24
25
  export declare function issueCommentsAlreadyFetched(issue: Record<string, unknown>): boolean;
25
- export declare function buildIssueVbrief(issue: Record<string, unknown>, status: IngestStatus, repoUrl: string): [Record<string, unknown>, string];
26
- export declare function targetFilename(number: number, title: string): string;
26
+ export declare function buildIssueVbrief(issue: Record<string, unknown>, status: IngestStatus, repoUrl: string, options?: {
27
+ infoRootKey?: typeof LEGACY_INFO_ROOT_KEY | typeof MIGRATED_INFO_ROOT_KEY;
28
+ infoVersion?: string;
29
+ }): [Record<string, unknown>, string];
30
+ export declare function targetFilename(number: number, title: string, artifactSuffix?: typeof LEGACY_ARTIFACT_SUFFIX | typeof MIGRATED_ARTIFACT_SUFFIX): string;
27
31
  export interface FetchIssueOptions {
28
32
  readonly cwd?: string | null;
29
33
  readonly cacheRoot?: string | null;
@@ -1,5 +1,5 @@
1
1
  import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { basename, join, resolve } from "node:path";
3
3
  import { cacheGet } from "../cache/operations.js";
4
4
  import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
5
5
  import { call } from "../scm/call.js";
@@ -7,6 +7,7 @@ import { resolveProjectRoot } from "../scope/project-context.js";
7
7
  import { resolveProjectRepo } from "../slice/project-context.js";
8
8
  import { slugify, TODAY } from "../vbrief-build/build.js";
9
9
  import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
10
+ import { LEGACY_ARTIFACT_SUFFIX, LEGACY_INFO_ROOT_KEY, LEGACY_VBRIEF_VERSION, MIGRATED_ARTIFACT_DIR, MIGRATED_ARTIFACT_SUFFIX, MIGRATED_INFO_ROOT_KEY, VBRIEF_VERSION, } from "../xbrief-migrate/constants.js";
10
11
  import { findAcHeading, parseCheckboxItems, parseListItems, sliceAcSection, stripCodeBlocks, stripFencedCodeBlocks, } from "./markdown-scanners.js";
11
12
  import { detectRepo, extractReferencesFromVbrief, fetchOpenIssues, GITHUB_ISSUE_REF_TYPES, LIFECYCLE_FOLDERS, parseIssueNumber, } from "./reconcile-issues.js";
12
13
  export const INGEST_STATUSES = ["proposed", "pending", "active"];
@@ -17,6 +18,53 @@ const STATUS_MAP = {
17
18
  pending: ["pending", "pending"],
18
19
  active: ["active", "running"],
19
20
  };
21
+ function readDeclaredInfoVersion(vbriefDir, infoRootKey, artifactSuffix, fallbackVersion) {
22
+ const projectDefinitionPath = join(vbriefDir, `PROJECT-DEFINITION${artifactSuffix}`);
23
+ try {
24
+ const parsed = JSON.parse(readFileSync(projectDefinitionPath, "utf8"));
25
+ const info = parsed[infoRootKey];
26
+ if (info !== null && typeof info === "object" && !Array.isArray(info)) {
27
+ const declared = info.version;
28
+ if (typeof declared === "string" && declared.length > 0) {
29
+ return declared;
30
+ }
31
+ }
32
+ }
33
+ catch {
34
+ // Fall back to the schema-default version when no declaration is available.
35
+ }
36
+ return fallbackVersion;
37
+ }
38
+ /**
39
+ * Decide the emission format (`.xbrief.json` + `xBRIEFInfo` vs legacy `.vbrief.json`
40
+ * + `vBRIEFInfo`) for an ingested scope artifact.
41
+ *
42
+ * The decision is STRUCTURAL and keyed on the resolved lifecycle root directory
43
+ * (`vbriefDir`), which is itself produced by `resolveLifecycleLayout` / `resolveLifecycleRoot`
44
+ * -- the exact same layout-decision logic `project:render` uses. This guarantees the two
45
+ * surfaces cannot diverge (#2149 finding #1): whichever tree `resolveLifecycleLayout` selected
46
+ * (`xbrief/` when migrated, else `vbrief/`) is the tree ingest writes into, and the emission
47
+ * format always matches that directory.
48
+ *
49
+ * It deliberately does NOT content-scan the tree for legacy markers (`detectLegacyVbriefLayout`).
50
+ * A historical vBRIEF-serialized artifact sitting in `xbrief/completed/` is migrated content,
51
+ * not a legacy layout, and must NOT force legacy emission on a migrated project (#2149 finding #3).
52
+ */
53
+ function resolveIngestEmissionLayout(vbriefDir) {
54
+ const migrated = basename(vbriefDir) === MIGRATED_ARTIFACT_DIR;
55
+ if (!migrated) {
56
+ return {
57
+ artifactSuffix: LEGACY_ARTIFACT_SUFFIX,
58
+ infoRootKey: LEGACY_INFO_ROOT_KEY,
59
+ infoVersion: LEGACY_VBRIEF_VERSION,
60
+ };
61
+ }
62
+ return {
63
+ artifactSuffix: MIGRATED_ARTIFACT_SUFFIX,
64
+ infoRootKey: MIGRATED_INFO_ROOT_KEY,
65
+ infoVersion: readDeclaredInfoVersion(vbriefDir, MIGRATED_INFO_ROOT_KEY, MIGRATED_ARTIFACT_SUFFIX, VBRIEF_VERSION),
66
+ };
67
+ }
20
68
  const CONTROL_CHAR_LABELS = {
21
69
  "\b": "U+0008 backspace",
22
70
  "\t": "U+0009 tab",
@@ -230,7 +278,7 @@ export function issueCommentThread(issue) {
230
278
  export function issueCommentsAlreadyFetched(issue) {
231
279
  return Object.hasOwn(issue, ISSUE_COMMENT_THREAD_KEY);
232
280
  }
233
- export function buildIssueVbrief(issue, status, repoUrl) {
281
+ export function buildIssueVbrief(issue, status, repoUrl, options = {}) {
234
282
  const number = Number(issue.number);
235
283
  const title = (typeof issue.title === "string" && issue.title.length > 0
236
284
  ? issue.title
@@ -291,20 +339,23 @@ export function buildIssueVbrief(issue, status, repoUrl) {
291
339
  }
292
340
  plan.references = references;
293
341
  }
342
+ const infoRootKey = options.infoRootKey ?? LEGACY_INFO_ROOT_KEY;
343
+ const infoVersion = options.infoVersion ?? EMITTED_VBRIEF_VERSION;
344
+ const briefLabel = infoRootKey === MIGRATED_INFO_ROOT_KEY ? "xBRIEF" : "vBRIEF";
294
345
  return [
295
346
  {
296
- vBRIEFInfo: {
297
- version: EMITTED_VBRIEF_VERSION,
298
- description: `Scope vBRIEF ingested from GitHub issue #${number}`,
347
+ [infoRootKey]: {
348
+ version: infoVersion,
349
+ description: `Scope ${briefLabel} ingested from GitHub issue #${number}`,
299
350
  },
300
351
  plan,
301
352
  },
302
353
  folder,
303
354
  ];
304
355
  }
305
- export function targetFilename(number, title) {
356
+ export function targetFilename(number, title, artifactSuffix = LEGACY_ARTIFACT_SUFFIX) {
306
357
  const slug = slugify(title) || `issue-${number}`;
307
- return `${TODAY}-${number}-${slug}.vbrief.json`;
358
+ return `${TODAY}-${number}-${slug}${artifactSuffix}`;
308
359
  }
309
360
  export function fetchFromCache(repo, number, options = {}) {
310
361
  const key = `${repo}/${number}`;
@@ -423,8 +474,12 @@ export function ingestOne(issue, options) {
423
474
  cwd: options.cwd,
424
475
  cacheRoot: options.cacheRoot,
425
476
  });
426
- const [vbrief, folder] = buildIssueVbrief(enriched, options.status, options.repoUrl);
427
- const filename = targetFilename(number, String(issue.title ?? ""));
477
+ const emissionLayout = resolveIngestEmissionLayout(options.vbriefDir);
478
+ const [vbrief, folder] = buildIssueVbrief(enriched, options.status, options.repoUrl, {
479
+ infoRootKey: emissionLayout.infoRootKey,
480
+ infoVersion: emissionLayout.infoVersion,
481
+ });
482
+ const filename = targetFilename(number, String(issue.title ?? ""), emissionLayout.artifactSuffix);
428
483
  const target = join(options.vbriefDir, folder, filename);
429
484
  if (options.dryRun) {
430
485
  return ["dryrun", target, `DRY-RUN would write ${folder}/${filename}`];
@@ -568,6 +623,7 @@ export function ingestSingleForAccept(n, repo, options = {}) {
568
623
  vbriefDir,
569
624
  status: options.status ?? "proposed",
570
625
  repoUrl,
626
+ cwd: root,
571
627
  });
572
628
  return [result, path];
573
629
  }
@@ -0,0 +1,13 @@
1
+ /** Rot-prone unmanaged-header sections retired by Option A (#2065). */
2
+ export declare const RETIRED_UNMANAGED_HEADER_SECTIONS: readonly ["## Status", "## Known Issues"];
3
+ export interface ConsumerHeaderSeams {
4
+ readonly frameworkRoot?: string;
5
+ readonly readTemplate?: () => string | null;
6
+ }
7
+ /** Bounded unmanaged header scaffold for fresh consumer installs (#2065 Option A). */
8
+ export declare function renderConsumerHeader(seams?: ConsumerHeaderSeams): string;
9
+ /** Compose a greenfield AGENTS.md: bounded unmanaged header + attributed managed section. */
10
+ export declare function composeGreenfieldAgentsMd(attributedManagedSection: string, seams?: ConsumerHeaderSeams): string;
11
+ /** True when text contains rot-prone retired header patterns (#2065). */
12
+ export declare function containsRetiredUnmanagedHeaderPatterns(text: string): boolean;
13
+ //# sourceMappingURL=agents-consumer-header.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { contentRoot } from "../content-root.js";
4
+ import { frameworkRoot } from "./agents-md.js";
5
+ /** Rot-prone unmanaged-header sections retired by Option A (#2065). */
6
+ export const RETIRED_UNMANAGED_HEADER_SECTIONS = ["## Status", "## Known Issues"];
7
+ // "Next:" is a bare label, not a markdown heading like the two patterns above --
8
+ // anchor it to the start of a line so prose that happens to contain "Next:"
9
+ // mid-sentence (e.g. "See UPGRADING.md ... Next: run `deft triage:queue`") does
10
+ // not false-positive (#2170 review).
11
+ const RETIRED_NEXT_LABEL_PATTERN = /(^|\n)\s*Next:/;
12
+ const CONSUMER_HEADER_TEMPLATE = "templates/agents-consumer-header.md";
13
+ function readConsumerHeaderTemplate(seams = {}) {
14
+ if (seams.readTemplate)
15
+ return seams.readTemplate();
16
+ const root = frameworkRoot(seams);
17
+ const candidate = join(contentRoot(root), CONSUMER_HEADER_TEMPLATE);
18
+ try {
19
+ if (!existsSync(candidate))
20
+ return null;
21
+ return readFileSync(candidate, "utf8");
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ /** Bounded unmanaged header scaffold for fresh consumer installs (#2065 Option A). */
28
+ export function renderConsumerHeader(seams = {}) {
29
+ const template = readConsumerHeaderTemplate(seams);
30
+ if (template === null) {
31
+ return [
32
+ "# Project",
33
+ "",
34
+ "One-line project description (edit me).",
35
+ "",
36
+ "## Session orientation",
37
+ "",
38
+ "Scoped work → `xbrief/` lifecycle; ranked queue → `deft triage:queue`; tracked bugs → GitHub issues; identity → `xbrief/PROJECT-DEFINITION.xbrief.json`.",
39
+ ].join("\n");
40
+ }
41
+ return template.replace(/\r\n/g, "\n").replace(/\n$/, "");
42
+ }
43
+ /** Compose a greenfield AGENTS.md: bounded unmanaged header + attributed managed section. */
44
+ export function composeGreenfieldAgentsMd(attributedManagedSection, seams = {}) {
45
+ const header = renderConsumerHeader(seams);
46
+ const managed = attributedManagedSection.replace(/\r\n/g, "\n").replace(/\n$/, "");
47
+ return `${header}\n\n${managed}\n`;
48
+ }
49
+ /** True when text contains rot-prone retired header patterns (#2065). */
50
+ export function containsRetiredUnmanagedHeaderPatterns(text) {
51
+ const normalized = text.replace(/\r\n/g, "\n");
52
+ if (RETIRED_UNMANAGED_HEADER_SECTIONS.some((pattern) => normalized.includes(pattern))) {
53
+ return true;
54
+ }
55
+ return RETIRED_NEXT_LABEL_PATTERN.test(normalized);
56
+ }
57
+ //# sourceMappingURL=agents-consumer-header.js.map
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { contentRoot } from "../content-root.js";
7
+ import { composeGreenfieldAgentsMd } from "./agents-consumer-header.js";
7
8
  import { AGENTS_MANAGED_CLOSE, AGENTS_MANAGED_OPEN_V3_LITERAL } from "./constants.js";
8
9
  import { findManagedOpenMarker } from "./linear-scan.js";
9
10
  /** Return the framework root (directory owning `templates/`). */
@@ -228,7 +229,9 @@ export function agentsRefreshPlan(projectRoot, seams = {}) {
228
229
  refreshed,
229
230
  session: sessionId,
230
231
  existing: null,
231
- new_content: `${attributedRendered}\n`,
232
+ new_content: composeGreenfieldAgentsMd(attributedRendered, {
233
+ frameworkRoot: frameworkRoot(seams),
234
+ }),
232
235
  };
233
236
  }
234
237
  const normalised = existing.replace(/\r\n/g, "\n");
@@ -1,3 +1,4 @@
1
+ export * from "./agents-consumer-header.js";
1
2
  export * from "./agents-md.js";
2
3
  export * from "./constants.js";
3
4
  export * from "./ip-risk.js";
@@ -1,3 +1,4 @@
1
+ export * from "./agents-consumer-header.js";
1
2
  export * from "./agents-md.js";
2
3
  export * from "./constants.js";
3
4
  export * from "./ip-risk.js";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Advisory (consumer-side) AGENTS.md legibility budget (#2155).
3
+ *
4
+ * This is the consumer companion to the maintainer-only #645 ratchet
5
+ * (`plan.policy.agentsMdBudget`, fail-closed via `check:framework-source`). A
6
+ * consumer's AGENTS.md has a framework-owned managed section (rendered from the
7
+ * template; the consumer cannot act on its size) and an UNMANAGED region
8
+ * (project header + project-specific rules) that is theirs. Because we cannot
9
+ * know what a given project legitimately needs in that region, the framework
10
+ * has no business failing THEIR build over it. So this budget is:
11
+ *
12
+ * - unmanaged-focused (the managed section is excluded from the count),
13
+ * - a SOFT, operator-adjustable knob, generous by DEFAULT when unset, and
14
+ * - never fail-closing in the default advisory posture (advise -> observe ->
15
+ * enforce per #1419).
16
+ *
17
+ * Raising `unmanagedSoftMaxLines` is the no-friction, documented way a consumer
18
+ * accepts legitimate growth and silences the advisory nudge.
19
+ */
20
+ export interface AgentsMdAdvisoryConfig {
21
+ readonly unmanagedSoftMaxLines: number;
22
+ }
23
+ export type AgentsMdAdvisorySource = "typed" | "default" | "default-on-error";
24
+ export interface AgentsMdAdvisoryResult {
25
+ readonly config: AgentsMdAdvisoryConfig;
26
+ readonly source: AgentsMdAdvisorySource;
27
+ /** Populated only when a malformed field forced a fallback to the default. */
28
+ readonly error: string | null;
29
+ }
30
+ /**
31
+ * Generous default soft budget for the consumer-authored (unmanaged) region.
32
+ *
33
+ * Deliberately generous: the empirical "map, not a manual" guidance
34
+ * (`content/docs/good-agents-md.md`) is about the whole file's front-door cost,
35
+ * but a compliance-heavy repo or a monorepo with real per-package rules may
36
+ * legitimately need a larger project section. This default only nudges once the
37
+ * unmanaged region gets genuinely large; the consumer raises the field to
38
+ * accept their own trade-off.
39
+ */
40
+ export declare const DEFAULT_UNMANAGED_SOFT_MAX_LINES = 300;
41
+ /** Canonical dotted-path name of the typed advisory soft-budget field. */
42
+ export declare const FIELD_AGENTS_MD_ADVISORY_UNMANAGED_SOFT_MAX_LINES = "plan.policy.agentsMdAdvisory.unmanagedSoftMaxLines";
43
+ /**
44
+ * Resolve the advisory soft budget from PROJECT-DEFINITION (#2155).
45
+ *
46
+ * NEVER throws and NEVER surfaces a hard config error: because the advisory is
47
+ * non-blocking by contract, any malformed input degrades gracefully to the
48
+ * generous default (`source: "default-on-error"`, `error` populated for
49
+ * optional display) rather than failing the caller.
50
+ */
51
+ export declare function resolveAgentsMdAdvisory(projectRoot: string): AgentsMdAdvisoryResult;
52
+ //# sourceMappingURL=agents-md-advisory.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { readPlanPolicy } from "./plan-extensions.js";
2
+ import { loadProjectDefinition } from "./resolve.js";
3
+ /**
4
+ * Generous default soft budget for the consumer-authored (unmanaged) region.
5
+ *
6
+ * Deliberately generous: the empirical "map, not a manual" guidance
7
+ * (`content/docs/good-agents-md.md`) is about the whole file's front-door cost,
8
+ * but a compliance-heavy repo or a monorepo with real per-package rules may
9
+ * legitimately need a larger project section. This default only nudges once the
10
+ * unmanaged region gets genuinely large; the consumer raises the field to
11
+ * accept their own trade-off.
12
+ */
13
+ export const DEFAULT_UNMANAGED_SOFT_MAX_LINES = 300;
14
+ /** Canonical dotted-path name of the typed advisory soft-budget field. */
15
+ export const FIELD_AGENTS_MD_ADVISORY_UNMANAGED_SOFT_MAX_LINES = "plan.policy.agentsMdAdvisory.unmanagedSoftMaxLines";
16
+ function defaultConfig() {
17
+ return { unmanagedSoftMaxLines: DEFAULT_UNMANAGED_SOFT_MAX_LINES };
18
+ }
19
+ /**
20
+ * Resolve the advisory soft budget from PROJECT-DEFINITION (#2155).
21
+ *
22
+ * NEVER throws and NEVER surfaces a hard config error: because the advisory is
23
+ * non-blocking by contract, any malformed input degrades gracefully to the
24
+ * generous default (`source: "default-on-error"`, `error` populated for
25
+ * optional display) rather than failing the caller.
26
+ */
27
+ export function resolveAgentsMdAdvisory(projectRoot) {
28
+ const [data, err] = loadProjectDefinition(projectRoot);
29
+ if (data === null) {
30
+ return { config: defaultConfig(), source: "default-on-error", error: err };
31
+ }
32
+ const policyBlock = readPlanPolicy(data.plan);
33
+ if (typeof policyBlock !== "object" ||
34
+ policyBlock === null ||
35
+ Array.isArray(policyBlock) ||
36
+ !("agentsMdAdvisory" in policyBlock)) {
37
+ return { config: defaultConfig(), source: "default", error: null };
38
+ }
39
+ const rawAdvisory = policyBlock.agentsMdAdvisory;
40
+ if (typeof rawAdvisory !== "object" || rawAdvisory === null || Array.isArray(rawAdvisory)) {
41
+ return {
42
+ config: defaultConfig(),
43
+ source: "default-on-error",
44
+ error: "plan.policy.agentsMdAdvisory must be an object with unmanagedSoftMaxLines",
45
+ };
46
+ }
47
+ const block = rawAdvisory;
48
+ if (!("unmanagedSoftMaxLines" in block)) {
49
+ // Object present but the field unset: treat as unset -> generous default.
50
+ return { config: defaultConfig(), source: "default", error: null };
51
+ }
52
+ const raw = block.unmanagedSoftMaxLines;
53
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
54
+ return {
55
+ config: defaultConfig(),
56
+ source: "default-on-error",
57
+ error: `${FIELD_AGENTS_MD_ADVISORY_UNMANAGED_SOFT_MAX_LINES} must be a non-negative integer; ` +
58
+ "falling back to the generous default",
59
+ };
60
+ }
61
+ return { config: { unmanagedSoftMaxLines: raw }, source: "typed", error: null };
62
+ }
63
+ //# sourceMappingURL=agents-md-advisory.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-region line-budget ratchet for AGENTS.md (#645).
3
+ *
4
+ * The budget is seeded at the CURRENT per-region line counts and forbids
5
+ * INCREASE, rather than encoding a static absolute ceiling. This decouples the
6
+ * gate from the reduction work (#1882): the file is by definition within its
7
+ * own seeded baseline, so the gate ships green, while any growth past the
8
+ * ratchet fails. Lowering a budget (a reduction PR) is always allowed; raising
9
+ * it is an explicit, reviewed diff to this typed field -- that diff IS the
10
+ * "was this growth deliberate?" checkpoint.
11
+ */
12
+ export interface AgentsMdBudget {
13
+ readonly managedMaxLines: number;
14
+ readonly unmanagedMaxLines: number;
15
+ }
16
+ export type AgentsMdBudgetSource = "typed" | "unset" | "default-on-error";
17
+ export interface AgentsMdBudgetResult {
18
+ readonly budget: AgentsMdBudget | null;
19
+ readonly source: AgentsMdBudgetSource;
20
+ readonly error: string | null;
21
+ }
22
+ /** Resolve plan.policy.agentsMdBudget from PROJECT-DEFINITION (#645). */
23
+ export declare function resolveAgentsMdBudget(projectRoot: string): AgentsMdBudgetResult;
24
+ //# sourceMappingURL=agents-md-budget.d.ts.map
@@ -0,0 +1,89 @@
1
+ import { readPlanPolicy } from "./plan-extensions.js";
2
+ import { loadProjectDefinition } from "./resolve.js";
3
+ function pythonTypeName(value) {
4
+ if (value === null)
5
+ return "None";
6
+ if (Array.isArray(value))
7
+ return "list";
8
+ if (typeof value === "boolean")
9
+ return "bool";
10
+ if (typeof value === "number")
11
+ return Number.isInteger(value) ? "int" : "float";
12
+ if (typeof value === "string")
13
+ return "str";
14
+ if (typeof value === "object")
15
+ return "dict";
16
+ return typeof value;
17
+ }
18
+ function pythonRepr(value) {
19
+ if (value === undefined)
20
+ return "None";
21
+ if (typeof value === "string")
22
+ return `'${value}'`;
23
+ if (value === null)
24
+ return "None";
25
+ if (typeof value === "boolean")
26
+ return value ? "True" : "False";
27
+ return String(value);
28
+ }
29
+ function readRegion(block, key) {
30
+ if (!(key in block)) {
31
+ return { value: null, error: `plan.policy.agentsMdBudget.${key} is required` };
32
+ }
33
+ const raw = block[key];
34
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
35
+ return {
36
+ value: null,
37
+ error: `plan.policy.agentsMdBudget.${key} must be a non-negative integer; got ${pythonTypeName(raw)} (${pythonRepr(raw)})`,
38
+ };
39
+ }
40
+ return { value: raw, error: null };
41
+ }
42
+ /** Resolve plan.policy.agentsMdBudget from PROJECT-DEFINITION (#645). */
43
+ export function resolveAgentsMdBudget(projectRoot) {
44
+ const [data, err] = loadProjectDefinition(projectRoot);
45
+ if (data === null) {
46
+ return { budget: null, source: "default-on-error", error: err };
47
+ }
48
+ const plan = data.plan;
49
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
50
+ return {
51
+ budget: null,
52
+ source: "default-on-error",
53
+ error: "PROJECT-DEFINITION 'plan' is not an object",
54
+ };
55
+ }
56
+ const policyBlock = readPlanPolicy(plan);
57
+ if (typeof policyBlock !== "object" ||
58
+ policyBlock === null ||
59
+ Array.isArray(policyBlock) ||
60
+ !("agentsMdBudget" in policyBlock)) {
61
+ return { budget: null, source: "unset", error: null };
62
+ }
63
+ const rawBudget = policyBlock.agentsMdBudget;
64
+ if (typeof rawBudget !== "object" || rawBudget === null || Array.isArray(rawBudget)) {
65
+ return {
66
+ budget: null,
67
+ source: "default-on-error",
68
+ error: `plan.policy.agentsMdBudget must be an object with managedMaxLines and unmanagedMaxLines; got ${pythonTypeName(rawBudget)}`,
69
+ };
70
+ }
71
+ const block = rawBudget;
72
+ const managed = readRegion(block, "managedMaxLines");
73
+ if (managed.error !== null) {
74
+ return { budget: null, source: "default-on-error", error: managed.error };
75
+ }
76
+ const unmanaged = readRegion(block, "unmanagedMaxLines");
77
+ if (unmanaged.error !== null) {
78
+ return { budget: null, source: "default-on-error", error: unmanaged.error };
79
+ }
80
+ return {
81
+ budget: {
82
+ managedMaxLines: managed.value,
83
+ unmanagedMaxLines: unmanaged.value,
84
+ },
85
+ source: "typed",
86
+ error: null,
87
+ };
88
+ }
89
+ //# sourceMappingURL=agents-md-budget.js.map
@@ -1,3 +1,4 @@
1
+ export * from "./agents-md-advisory.js";
1
2
  export * from "./autonomy.js";
2
3
  export * from "./capacity.js";
3
4
  export * from "./decisions.js";
@@ -1,6 +1,7 @@
1
1
  import { readPlanPolicy } from "./plan-extensions.js";
2
2
  import { coerceLegacyNarrative, LEGACY_NARRATIVE_KEY, loadProjectDefinition } from "./resolve.js";
3
3
  import { DEFAULT_WIP_CAP } from "./wip.js";
4
+ export * from "./agents-md-advisory.js";
4
5
  export * from "./autonomy.js";
5
6
  export * from "./capacity.js";
6
7
  export * from "./decisions.js";
@@ -0,0 +1,28 @@
1
+ import type { CheckRunRecord } from "./gh.js";
2
+ export type CiReadyState = "ready" | "blocked" | "not_ready_yet" | "skipped";
3
+ export interface CiGateOptions {
4
+ readonly skipCi?: boolean;
5
+ readonly ignoreCheckNames?: readonly string[];
6
+ }
7
+ export interface CiCheckConclusion {
8
+ readonly name: string;
9
+ readonly status: string;
10
+ readonly conclusion: string;
11
+ readonly required: boolean;
12
+ readonly ignored: boolean;
13
+ }
14
+ export interface CiGateSummary {
15
+ readonly ready_state: CiReadyState;
16
+ readonly checked_count: number;
17
+ readonly ignored_checks: readonly string[];
18
+ readonly failed_required: readonly string[];
19
+ readonly pending_required: readonly string[];
20
+ readonly conclusions: readonly CiCheckConclusion[];
21
+ }
22
+ export interface CiGateResult {
23
+ readonly failures: readonly string[];
24
+ readonly summary: CiGateSummary;
25
+ }
26
+ export declare function buildCiSummaryLine(summary: CiGateSummary): string;
27
+ export declare function evaluateCiGate(checkRuns: readonly CheckRunRecord[], options?: CiGateOptions): CiGateResult;
28
+ //# sourceMappingURL=ci-gate.d.ts.map