@deftai/directive-core 0.66.2 → 0.68.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 (147) 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/check/index.d.ts +1 -1
  10. package/dist/check/index.js +1 -1
  11. package/dist/check/orchestrator.d.ts +14 -4
  12. package/dist/check/orchestrator.js +30 -6
  13. package/dist/codebase/map.d.ts +1 -0
  14. package/dist/codebase/map.js +5 -2
  15. package/dist/doctor/checks.js +11 -11
  16. package/dist/doctor/constants.d.ts +7 -2
  17. package/dist/doctor/constants.js +17 -1
  18. package/dist/doctor/flags.js +5 -1
  19. package/dist/doctor/main.js +99 -8
  20. package/dist/doctor/payload-staleness.js +15 -6
  21. package/dist/doctor/types.d.ts +7 -0
  22. package/dist/forward-coverage/evaluate.d.ts +55 -0
  23. package/dist/forward-coverage/evaluate.js +254 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/init-deposit/constants.d.ts +14 -0
  27. package/dist/init-deposit/constants.js +10 -0
  28. package/dist/init-deposit/hygiene.d.ts +50 -0
  29. package/dist/init-deposit/hygiene.js +176 -0
  30. package/dist/init-deposit/init-deposit.d.ts +1 -0
  31. package/dist/init-deposit/init-deposit.js +7 -1
  32. package/dist/init-deposit/refresh.d.ts +2 -0
  33. package/dist/init-deposit/refresh.js +48 -30
  34. package/dist/init-deposit/scaffold.d.ts +3 -4
  35. package/dist/init-deposit/scaffold.js +3 -24
  36. package/dist/intake/github-auth-modes.d.ts +1 -0
  37. package/dist/intake/github-auth-modes.js +1 -0
  38. package/dist/intake/issue-ingest.d.ts +6 -2
  39. package/dist/intake/issue-ingest.js +65 -9
  40. package/dist/integration-e2e/helpers.d.ts +0 -1
  41. package/dist/integration-e2e/helpers.js +3 -7
  42. package/dist/layout/lifecycle-ref.d.ts +15 -0
  43. package/dist/layout/lifecycle-ref.js +55 -0
  44. package/dist/platform/agents-consumer-header.d.ts +13 -0
  45. package/dist/platform/agents-consumer-header.js +57 -0
  46. package/dist/platform/agents-md.d.ts +28 -0
  47. package/dist/platform/agents-md.js +38 -1
  48. package/dist/platform/index.d.ts +1 -0
  49. package/dist/platform/index.js +1 -0
  50. package/dist/policy/agents-md-advisory.d.ts +52 -0
  51. package/dist/policy/agents-md-advisory.js +63 -0
  52. package/dist/policy/agents-md-budget.d.ts +24 -0
  53. package/dist/policy/agents-md-budget.js +89 -0
  54. package/dist/policy/index.d.ts +1 -0
  55. package/dist/policy/index.js +1 -0
  56. package/dist/policy/resolve.js +56 -46
  57. package/dist/pr-merge-readiness/ci-gate.d.ts +28 -0
  58. package/dist/pr-merge-readiness/ci-gate.js +79 -0
  59. package/dist/pr-merge-readiness/compute.d.ts +15 -1
  60. package/dist/pr-merge-readiness/compute.js +129 -9
  61. package/dist/pr-merge-readiness/gh.d.ts +8 -0
  62. package/dist/pr-merge-readiness/gh.js +30 -5
  63. package/dist/pr-merge-readiness/index.d.ts +5 -1
  64. package/dist/pr-merge-readiness/index.js +3 -0
  65. package/dist/pr-merge-readiness/main.d.ts +3 -0
  66. package/dist/pr-merge-readiness/main.js +73 -6
  67. package/dist/pr-merge-readiness/mergeability.d.ts +43 -0
  68. package/dist/pr-merge-readiness/mergeability.js +105 -0
  69. package/dist/pr-merge-readiness/output.js +20 -0
  70. package/dist/pr-merge-readiness/slizard-gate.d.ts +47 -0
  71. package/dist/pr-merge-readiness/slizard-gate.js +119 -0
  72. package/dist/pr-monitor/index.d.ts +1 -1
  73. package/dist/pr-monitor/index.js +1 -1
  74. package/dist/pr-monitor/monitor.d.ts +8 -2
  75. package/dist/pr-monitor/monitor.js +35 -6
  76. package/dist/pr-wait-mergeable/wrappers.d.ts +12 -1
  77. package/dist/pr-wait-mergeable/wrappers.js +13 -5
  78. package/dist/pr-watch/constants.d.ts +33 -0
  79. package/dist/pr-watch/constants.js +33 -0
  80. package/dist/pr-watch/index.d.ts +6 -0
  81. package/dist/pr-watch/index.js +6 -0
  82. package/dist/pr-watch/main.d.ts +21 -0
  83. package/dist/pr-watch/main.js +206 -0
  84. package/dist/pr-watch/probe.d.ts +13 -0
  85. package/dist/pr-watch/probe.js +100 -0
  86. package/dist/pr-watch/types.d.ts +47 -0
  87. package/dist/pr-watch/types.js +2 -0
  88. package/dist/pr-watch/watch.d.ts +12 -0
  89. package/dist/pr-watch/watch.js +122 -0
  90. package/dist/preflight-cache/evaluate.d.ts +1 -0
  91. package/dist/preflight-cache/evaluate.js +17 -10
  92. package/dist/render/project-render.d.ts +16 -2
  93. package/dist/render/project-render.js +155 -97
  94. package/dist/scope/capacity-stamp.d.ts +24 -2
  95. package/dist/scope/capacity-stamp.js +80 -28
  96. package/dist/scope/decomposed-refs.js +2 -2
  97. package/dist/scope/project-definition-sync.js +3 -3
  98. package/dist/scope/vbrief-ref.d.ts +2 -1
  99. package/dist/scope/vbrief-ref.js +3 -2
  100. package/dist/spec-authority/constants.d.ts +6 -0
  101. package/dist/spec-authority/constants.js +11 -0
  102. package/dist/spec-authority/resolver.js +3 -3
  103. package/dist/swarm/finalize-cohort-cli.d.ts +5 -0
  104. package/dist/swarm/finalize-cohort-cli.js +100 -0
  105. package/dist/swarm/finalize-cohort.d.ts +39 -0
  106. package/dist/swarm/finalize-cohort.js +589 -0
  107. package/dist/swarm/index.d.ts +2 -0
  108. package/dist/swarm/index.js +2 -0
  109. package/dist/swarm/launch.d.ts +13 -0
  110. package/dist/swarm/launch.js +19 -7
  111. package/dist/swarm/routing-verify.d.ts +1 -1
  112. package/dist/swarm/routing-verify.js +11 -10
  113. package/dist/swarm/routing.d.ts +9 -0
  114. package/dist/swarm/routing.js +46 -0
  115. package/dist/swarm/verify-review-clean-cli.js +25 -1
  116. package/dist/swarm/verify-review-clean.d.ts +9 -1
  117. package/dist/swarm/verify-review-clean.js +76 -3
  118. package/dist/triage/queue/index.d.ts +1 -0
  119. package/dist/triage/queue/index.js +1 -0
  120. package/dist/triage/queue/project.d.ts +6 -1
  121. package/dist/triage/queue/project.js +9 -4
  122. package/dist/triage/queue/reconcile-live-state.d.ts +42 -0
  123. package/dist/triage/queue/reconcile-live-state.js +54 -0
  124. package/dist/triage/scope/mutations-core.js +91 -81
  125. package/dist/triage/scope/resolve.js +4 -3
  126. package/dist/triage/scope-drift/add-ignore.js +33 -32
  127. package/dist/triage/scope-drift/scope-rules.js +3 -3
  128. package/dist/triage/subscribe/index.js +51 -45
  129. package/dist/triage/welcome/writers.js +79 -64
  130. package/dist/vbrief-build/project-definition-io.js +8 -3
  131. package/dist/vbrief-validate/paths.js +8 -4
  132. package/dist/vbrief-validate/precutover.d.ts +1 -1
  133. package/dist/vbrief-validate/precutover.js +12 -10
  134. package/dist/verify-source/biome-config.d.ts +41 -0
  135. package/dist/verify-source/biome-config.js +97 -0
  136. package/dist/verify-source/index.d.ts +1 -0
  137. package/dist/verify-source/index.js +1 -0
  138. package/dist/xbrief-migrate/agents-header.d.ts +69 -0
  139. package/dist/xbrief-migrate/agents-header.js +179 -0
  140. package/dist/xbrief-migrate/fs-helpers.d.ts +3 -0
  141. package/dist/xbrief-migrate/fs-helpers.js +11 -0
  142. package/dist/xbrief-migrate/index.d.ts +1 -0
  143. package/dist/xbrief-migrate/index.js +1 -0
  144. package/dist/xbrief-migrate/migrate-project.js +29 -10
  145. package/package.json +15 -7
  146. package/dist/install-upgrade/index.d.ts +0 -13
  147. package/dist/install-upgrade/index.js +0 -251
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import { basename, resolve } from "node:path";
3
+ import { hasArtifactSuffix } from "./resolve.js";
4
+ /** Lifecycle folders searched when a folder-qualified ref dangles (#1926). */
5
+ export const LIFECYCLE_SCOPE_FOLDERS = [
6
+ "proposed",
7
+ "pending",
8
+ "active",
9
+ "completed",
10
+ "cancelled",
11
+ ];
12
+ function isLifecycleFolder(name) {
13
+ return LIFECYCLE_SCOPE_FOLDERS.includes(name);
14
+ }
15
+ function searchLifecycleFolders(lifecycleRoot, artifactBasename) {
16
+ for (const folder of LIFECYCLE_SCOPE_FOLDERS) {
17
+ const candidate = resolve(lifecycleRoot, folder, artifactBasename);
18
+ if (existsSync(candidate)) {
19
+ return candidate;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * Resolve a lifecycle-relative artifact reference to an absolute path.
26
+ *
27
+ * When the direct path is missing but the ref names a lifecycle artifact
28
+ * (folder-qualified or bare basename), search all lifecycle folders for the
29
+ * same filename (#1926).
30
+ */
31
+ export function resolveLifecycleArtifactRef(rel, lifecycleRoot, options = {}) {
32
+ const allowCrossFolderSearch = options.allowCrossFolderSearch ?? true;
33
+ const direct = resolve(lifecycleRoot, rel);
34
+ if (existsSync(direct)) {
35
+ return direct;
36
+ }
37
+ if (!allowCrossFolderSearch) {
38
+ return direct;
39
+ }
40
+ const parts = rel.split("/").filter(Boolean);
41
+ if (parts.length >= 2 && isLifecycleFolder(parts[0] ?? "")) {
42
+ const found = searchLifecycleFolders(lifecycleRoot, basename(rel));
43
+ if (found !== null) {
44
+ return found;
45
+ }
46
+ }
47
+ if (!rel.includes("/") && hasArtifactSuffix(rel)) {
48
+ const found = searchLifecycleFolders(lifecycleRoot, rel);
49
+ if (found !== null) {
50
+ return found;
51
+ }
52
+ }
53
+ return direct;
54
+ }
55
+ //# sourceMappingURL=lifecycle-ref.js.map
@@ -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
@@ -1,3 +1,4 @@
1
+ import { type LockDeps } from "../slice/lock.js";
1
2
  export interface ManagedSectionAttrs {
2
3
  readonly version: number;
3
4
  readonly sha: string | null;
@@ -27,5 +28,32 @@ export declare function attributeRenderManagedSection(rendered: string, attrs: {
27
28
  }): string;
28
29
  /** Compute AGENTS.md managed-section freshness plan (mirrors scripts/_agents_md.py). */
29
30
  export declare function agentsRefreshPlan(projectRoot: string, seams?: AgentsMdSeams): Record<string, unknown>;
31
+ export interface ApplyAgentsRefreshOptions {
32
+ /** Compute state only, never write — mirrors `agents:refresh --check`. */
33
+ readonly check?: boolean;
34
+ /** Compute the plan but skip the write — mirrors `agents:refresh --dry-run`. */
35
+ readonly dryRun?: boolean;
36
+ }
37
+ export interface AgentsRefreshApplyResult {
38
+ readonly state: string;
39
+ readonly path: string;
40
+ /** True only when the managed section was written to disk on this call. */
41
+ readonly wrote: boolean;
42
+ /** True when the plan produced writable `new_content` for a writable state. */
43
+ readonly writable: boolean;
44
+ }
45
+ /**
46
+ * Serialize the AGENTS.md managed-section read->compute->write behind an advisory
47
+ * file lock and write atomically (temp file + rename), so concurrent
48
+ * install/upgrade/migrate/refresh writers cannot clobber one another's `session=`
49
+ * write or leave a partially-written file (#1329).
50
+ *
51
+ * Reuses the existing cross-process append lock (`withAppendLock`, keyed here on
52
+ * the AGENTS.md path -> `AGENTS.md.lock`) and atomic writer (`atomicWriteText`)
53
+ * rather than introducing a competing lock utility. The parallel
54
+ * `projectDefinitionMutationLock` remains owned by #1260; a general lock helper is
55
+ * deliberately not added here to avoid two competing mutation-lock utilities.
56
+ */
57
+ export declare function applyAgentsRefresh(projectRoot: string, options?: ApplyAgentsRefreshOptions, seams?: AgentsMdSeams, lockDeps?: LockDeps): AgentsRefreshApplyResult;
30
58
  export declare function hasV3ManagedMarker(projectRoot: string, readText?: (path: string) => string | null): boolean;
31
59
  //# sourceMappingURL=agents-md.d.ts.map
@@ -3,7 +3,10 @@ import { randomUUID } from "node:crypto";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { atomicWriteText } from "../cache/io.js";
6
7
  import { contentRoot } from "../content-root.js";
8
+ import { withAppendLock } from "../slice/lock.js";
9
+ import { composeGreenfieldAgentsMd } from "./agents-consumer-header.js";
7
10
  import { AGENTS_MANAGED_CLOSE, AGENTS_MANAGED_OPEN_V3_LITERAL } from "./constants.js";
8
11
  import { findManagedOpenMarker } from "./linear-scan.js";
9
12
  /** Return the framework root (directory owning `templates/`). */
@@ -228,7 +231,9 @@ export function agentsRefreshPlan(projectRoot, seams = {}) {
228
231
  refreshed,
229
232
  session: sessionId,
230
233
  existing: null,
231
- new_content: `${attributedRendered}\n`,
234
+ new_content: composeGreenfieldAgentsMd(attributedRendered, {
235
+ frameworkRoot: frameworkRoot(seams),
236
+ }),
232
237
  };
233
238
  }
234
239
  const normalised = existing.replace(/\r\n/g, "\n");
@@ -291,6 +296,38 @@ export function agentsRefreshPlan(projectRoot, seams = {}) {
291
296
  new_content: newContent,
292
297
  };
293
298
  }
299
+ /** Managed-section states whose plan yields a new AGENTS.md body to write. */
300
+ const AGENTS_REFRESH_WRITABLE_STATES = new Set(["absent", "missing", "stale"]);
301
+ /**
302
+ * Serialize the AGENTS.md managed-section read->compute->write behind an advisory
303
+ * file lock and write atomically (temp file + rename), so concurrent
304
+ * install/upgrade/migrate/refresh writers cannot clobber one another's `session=`
305
+ * write or leave a partially-written file (#1329).
306
+ *
307
+ * Reuses the existing cross-process append lock (`withAppendLock`, keyed here on
308
+ * the AGENTS.md path -> `AGENTS.md.lock`) and atomic writer (`atomicWriteText`)
309
+ * rather than introducing a competing lock utility. The parallel
310
+ * `projectDefinitionMutationLock` remains owned by #1260; a general lock helper is
311
+ * deliberately not added here to avoid two competing mutation-lock utilities.
312
+ */
313
+ export function applyAgentsRefresh(projectRoot, options = {}, seams = {}, lockDeps = {}) {
314
+ const agentsMd = join(projectRoot, "AGENTS.md");
315
+ return withAppendLock(agentsMd, () => {
316
+ const plan = agentsRefreshPlan(projectRoot, seams);
317
+ const state = String(plan.state ?? "unknown");
318
+ const path = String(plan.path ?? agentsMd);
319
+ const newContent = plan.new_content;
320
+ if (options.check === true ||
321
+ options.dryRun === true ||
322
+ !AGENTS_REFRESH_WRITABLE_STATES.has(state) ||
323
+ typeof newContent !== "string") {
324
+ const writable = AGENTS_REFRESH_WRITABLE_STATES.has(state) && typeof newContent === "string";
325
+ return { state, path, wrote: false, writable };
326
+ }
327
+ atomicWriteText(path, newContent);
328
+ return { state, path, wrote: true, writable: true };
329
+ }, lockDeps);
330
+ }
294
331
  export function hasV3ManagedMarker(projectRoot, readText = defaultReadAgents) {
295
332
  const agentsMd = join(projectRoot, "AGENTS.md");
296
333
  const text = readText(agentsMd);
@@ -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";
@@ -1,6 +1,7 @@
1
1
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve as pathResolve } from "node:path";
3
3
  import { resolveProjectDefinitionPath } from "../layout/resolve.js";
4
+ import { atomicWriteProjectDefinition, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
4
5
  import { migrateLegacyPolicyKey, PLAN_POLICY_KEY, readPlanPolicy } from "./plan-extensions.js";
5
6
  /** Filesystem-relative location of the project-definition vBRIEF (display/back-compat). */
6
7
  export const PROJECT_DEFINITION_REL_PATH = "vbrief/PROJECT-DEFINITION.vbrief.json";
@@ -184,55 +185,64 @@ export function setPolicy(projectRoot, options) {
184
185
  if (!existsSync(path)) {
185
186
  throw new Error(`PROJECT-DEFINITION not found at ${path}`);
186
187
  }
187
- const data = JSON.parse(readFileSync(path, { encoding: "utf8" }));
188
- if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
189
- if (data.plan === undefined) {
190
- data.plan = {};
188
+ // Serialise the read-modify-write + audit-log append behind the shared
189
+ // PROJECT-DEFINITION mutation lock so a concurrent policy/ritual mutator
190
+ // cannot lose this update or desync the typed flag from the audit row (#1260).
191
+ return projectDefinitionMutationLock(projectRoot, () => {
192
+ const parsed = JSON.parse(readFileSync(path, { encoding: "utf8" }));
193
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
194
+ throw new Error(`PROJECT-DEFINITION at ${path} top-level value is not a JSON object`);
191
195
  }
192
- else {
193
- throw new Error("PROJECT-DEFINITION 'plan' is not an object");
196
+ const data = parsed;
197
+ if (typeof data.plan !== "object" || data.plan === null || Array.isArray(data.plan)) {
198
+ if (data.plan === undefined) {
199
+ data.plan = {};
200
+ }
201
+ else {
202
+ throw new Error("PROJECT-DEFINITION 'plan' is not an object");
203
+ }
194
204
  }
195
- }
196
- const plan = data.plan;
197
- migrateLegacyPolicyKey(plan);
198
- const existingPolicy = plan[PLAN_POLICY_KEY];
199
- if (typeof existingPolicy !== "object" ||
200
- existingPolicy === null ||
201
- Array.isArray(existingPolicy)) {
202
- if (existingPolicy === undefined) {
203
- plan[PLAN_POLICY_KEY] = {};
205
+ const plan = data.plan;
206
+ migrateLegacyPolicyKey(plan);
207
+ const existingPolicy = plan[PLAN_POLICY_KEY];
208
+ if (typeof existingPolicy !== "object" ||
209
+ existingPolicy === null ||
210
+ Array.isArray(existingPolicy)) {
211
+ if (existingPolicy === undefined) {
212
+ plan[PLAN_POLICY_KEY] = {};
213
+ }
214
+ else {
215
+ throw new Error("plan.policy is not an object");
216
+ }
204
217
  }
205
- else {
206
- throw new Error("plan.policy is not an object");
218
+ const policyBlock = plan[PLAN_POLICY_KEY];
219
+ const previous = policyBlock.allowDirectCommitsToMaster;
220
+ policyBlock.allowDirectCommitsToMaster = Boolean(allowDirectCommits);
221
+ let legacyDropped = false;
222
+ const narratives = plan.narratives;
223
+ if (typeof narratives === "object" &&
224
+ narratives !== null &&
225
+ !Array.isArray(narratives) &&
226
+ LEGACY_NARRATIVE_KEY in narratives) {
227
+ delete narratives[LEGACY_NARRATIVE_KEY];
228
+ legacyDropped = true;
207
229
  }
208
- }
209
- const policyBlock = plan[PLAN_POLICY_KEY];
210
- const previous = policyBlock.allowDirectCommitsToMaster;
211
- policyBlock.allowDirectCommitsToMaster = Boolean(allowDirectCommits);
212
- let legacyDropped = false;
213
- const narratives = plan.narratives;
214
- if (typeof narratives === "object" &&
215
- narratives !== null &&
216
- !Array.isArray(narratives) &&
217
- LEGACY_NARRATIVE_KEY in narratives) {
218
- delete narratives[LEGACY_NARRATIVE_KEY];
219
- legacyDropped = true;
220
- }
221
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, { encoding: "utf8" });
222
- const changed = previous !== Boolean(allowDirectCommits) || legacyDropped;
223
- const parts = [
224
- `actor=${actor}`,
225
- `allowDirectCommitsToMaster=${allowDirectCommits ? "true" : "false"}`,
226
- `previous=${pythonRepr(previous)}`,
227
- ];
228
- if (legacyDropped) {
229
- parts.push("legacy-narrative-migrated=true");
230
- }
231
- if (note) {
232
- parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
233
- }
234
- const auditEntry = parts.join(" ");
235
- appendAuditLog(projectRoot, auditEntry);
236
- return { changed, auditEntry };
230
+ atomicWriteProjectDefinition(path, data);
231
+ const changed = previous !== Boolean(allowDirectCommits) || legacyDropped;
232
+ const parts = [
233
+ `actor=${actor}`,
234
+ `allowDirectCommitsToMaster=${allowDirectCommits ? "true" : "false"}`,
235
+ `previous=${pythonRepr(previous)}`,
236
+ ];
237
+ if (legacyDropped) {
238
+ parts.push("legacy-narrative-migrated=true");
239
+ }
240
+ if (note) {
241
+ parts.push(`note=${note.replace(/\n/g, " ").replace(/\r/g, " ")}`);
242
+ }
243
+ const auditEntry = parts.join(" ");
244
+ appendAuditLog(projectRoot, auditEntry);
245
+ return { changed, auditEntry };
246
+ });
237
247
  }
238
248
  //# sourceMappingURL=resolve.js.map
@@ -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