mandrel 2.30.0 → 2.32.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 (253) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/ci-remediation.md +44 -1
  10. package/.agents/rules/git-conventions-reference.md +27 -27
  11. package/.agents/rules/git-conventions.md +4 -2
  12. package/.agents/rules/known-tooling-behavior.md +66 -30
  13. package/.agents/rules/testing-standards.md +35 -71
  14. package/.agents/runtime-deps.json +0 -1
  15. package/.agents/schemas/agentrc.schema.json +1939 -1400
  16. package/.agents/schemas/lifecycle/README.md +21 -14
  17. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  18. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  19. package/.agents/scripts/README.md +7 -29
  20. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +136 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  38. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  39. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  40. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  41. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  42. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  43. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  44. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  45. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  46. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  47. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  48. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  49. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  50. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  51. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  52. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  53. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  54. package/.agents/scripts/lib/child-exec.js +193 -0
  55. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  56. package/.agents/scripts/lib/cli-args.js +64 -0
  57. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  58. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  59. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  60. package/.agents/scripts/lib/config/commands.js +3 -5
  61. package/.agents/scripts/lib/config/explain.js +5 -7
  62. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  63. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  64. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  65. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  66. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  67. package/.agents/scripts/lib/config/gates/index.js +5 -2
  68. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  69. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  70. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  71. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  72. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  73. package/.agents/scripts/lib/config/github.js +0 -1
  74. package/.agents/scripts/lib/config/limits.js +3 -48
  75. package/.agents/scripts/lib/config/qa.js +105 -0
  76. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  77. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  78. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  79. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  80. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  81. package/.agents/scripts/lib/crap-utils.js +84 -520
  82. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  83. package/.agents/scripts/lib/degraded-mode.js +2 -2
  84. package/.agents/scripts/lib/doc-tiers.js +3 -3
  85. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  86. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  87. package/.agents/scripts/lib/fs-walk.js +52 -0
  88. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  89. package/.agents/scripts/lib/git-utils.js +16 -36
  90. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  91. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  92. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  93. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  94. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  95. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  96. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  97. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  98. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  99. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  100. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  101. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  102. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  103. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  104. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  106. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  107. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  108. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  109. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  110. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  111. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  112. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  113. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  124. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
  125. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  126. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  127. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  128. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  129. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  130. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  131. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  132. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  133. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  134. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  135. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  136. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  137. package/.agents/scripts/lib/signals/index.js +8 -6
  138. package/.agents/scripts/lib/signals/schema.js +20 -25
  139. package/.agents/scripts/lib/signals/write.js +8 -8
  140. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  141. package/.agents/scripts/lib/temp-retention.js +1 -1
  142. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  143. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  144. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  145. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  146. package/.agents/scripts/lib/worktree-manager.js +2 -3
  147. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  148. package/.agents/scripts/pr-watch-with-update.js +7 -5
  149. package/.agents/scripts/providers/github/cache.js +2 -2
  150. package/.agents/scripts/providers/github/comments.js +6 -28
  151. package/.agents/scripts/providers/github/compose.js +0 -15
  152. package/.agents/scripts/providers/github/errors.js +10 -27
  153. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  154. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  155. package/.agents/scripts/providers/github.js +4 -7
  156. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  157. package/.agents/scripts/resolve-stories.js +0 -2
  158. package/.agents/scripts/run-lint.js +61 -61
  159. package/.agents/scripts/run-test-profile.js +6 -6
  160. package/.agents/scripts/run-verify.js +48 -30
  161. package/.agents/scripts/single-story-close.js +20 -0
  162. package/.agents/scripts/single-story-init.js +12 -35
  163. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  164. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  165. package/.agents/skills/skills.index.json +2 -12
  166. package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
  167. package/.agents/workflows/audit-documentation.md +5 -6
  168. package/.agents/workflows/audit-to-stories.md +2 -2
  169. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  170. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  171. package/.agents/workflows/helpers/code-review.md +3 -8
  172. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  173. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  174. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  175. package/.agents/workflows/mandrel-update.md +10 -10
  176. package/.agents/workflows/qa-assist.md +15 -20
  177. package/.agents/workflows/qa-explore.md +9 -8
  178. package/README.md +1 -1
  179. package/docs/CHANGELOG.md +49 -0
  180. package/lib/migrations/index.js +2 -0
  181. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  182. package/package.json +12 -3
  183. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  184. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  185. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  186. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  187. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  188. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  189. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  190. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  191. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  192. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  193. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  194. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  195. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  196. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  197. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  198. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  199. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  200. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  201. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  202. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  203. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  204. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  205. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  206. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  207. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  208. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  209. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  210. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  211. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  212. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  213. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  214. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  215. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  216. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  217. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  218. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  219. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  220. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  221. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  222. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  223. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  224. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  225. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  226. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  227. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  228. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  229. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  230. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  231. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  232. package/.agents/scripts/lib/planning-corpus.js +0 -37
  233. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  234. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  235. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  236. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  237. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  238. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  239. package/.agents/scripts/lib/signals/read.js +0 -268
  240. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  241. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  242. package/.agents/scripts/lib/story-plan.js +0 -379
  243. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  244. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  245. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  246. package/.agents/scripts/lint-baseline.js +0 -507
  247. package/.agents/scripts/providers/github/prs.js +0 -103
  248. package/.agents/scripts/signals-view.js +0 -309
  249. package/.agents/scripts/story-plan.js +0 -370
  250. package/.agents/scripts/sync-branch-from-base.js +0 -149
  251. package/.agents/scripts/validate-docs-freshness.js +0 -314
  252. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  253. package/.agents/workflows/helpers/signals.md +0 -112
@@ -1,31 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * .agents/scripts/generate-config-docs.js — Schema-backed `.agentrc` reference
3
+ * .agents/scripts/generate-config-docs.js — the `.agentrc` config surface
4
+ * generator (Story #5007).
4
5
  *
5
- * Renders a bounded region inside `.agents/docs/configuration.md` from
6
- * `.agents/schemas/agentrc.schema.json`. The region is delimited by:
6
+ * **One annotated source, three generated artifacts.** The runtime AJV schema
7
+ * (`AGENTRC_SCHEMA`, composed from `lib/config-settings-schema*.js` and
8
+ * `lib/config/gates/*`) is the single hand-authored enumeration of the
9
+ * `.agentrc.json` surface. `description` and `default` annotations live on
10
+ * those literals — AJV ignores both — and this generator emits everything
11
+ * downstream of them:
7
12
  *
8
- * <!-- BEGIN GENERATED:agentrc -->
9
- * ...generated tables...
10
- * <!-- END GENERATED:agentrc -->
13
+ * 1. `.agents/schemas/agentrc.schema.json` — the JSON-Schema mirror every
14
+ * consumer `.agentrc.json` points `$schema` at. Serialized 2020-12,
15
+ * fully inlined (no `$defs`): it IS the runtime schema, so the class of
16
+ * bug where a key exists in the mirror but not in the runtime AJV — and
17
+ * a consumer's whole config is therefore dead on arrival (Epic #4131) —
18
+ * cannot be expressed any more.
19
+ * 2. `.agents/docs/agentrc-reference.json` — the defaults inventory
20
+ * `lib/config/defaults.js` reads for `mandrel explain` and the
21
+ * sync-agentrc redundancy advisory. Built from the `default`
22
+ * annotations: a node carrying one contributes its value verbatim and is
23
+ * not descended into; a node carrying none contributes nothing.
24
+ * 3. The bounded key-table region inside `.agents/docs/configuration.md`,
25
+ * delimited by:
11
26
  *
12
- * One Markdown section is emitted per top-level schema key (`project`,
13
- * `github`, `planning`, `delivery`). Each section's table has columns:
27
+ * <!-- BEGIN GENERATED:agentrc -->
28
+ * ...generated tables...
29
+ * <!-- END GENERATED:agentrc -->
14
30
  *
15
- * | Key | Required | Type | Default | Description |
31
+ * One Markdown section per top-level schema key, each table columned
32
+ * `| Key | Required | Type | Default | Description |`, with nested
33
+ * properties flattened into dot-paths (`paths.agentRoot`,
34
+ * `branchProtection.requiredChecks[]`).
16
35
  *
17
- * Nested properties are flattened into dot-paths (e.g. `paths.agentRoot`,
18
- * `branchProtection.requiredChecks[]`). Defaults, descriptions, and types
19
- * are sourced from the schema directly; rows without a `default` declaration
20
- * render `—`.
36
+ * Until this Story the first two were hand-maintained alongside the runtime
37
+ * schema and reconciled by two parity suites; those suites are now
38
+ * generator-fidelity checks.
21
39
  *
22
40
  * Modes:
23
- * (default) — rewrites the bounded region in place. If the region markers
24
- * are absent from `.agents/docs/configuration.md`, they are inserted
25
- * just after the "Top-level shape" section header before the
26
- * hand-authored per-section docs.
27
- * --check — exits 0 when the on-disk region matches the freshly
28
- * generated content, exits 1 with a diff hint otherwise.
41
+ * (default) — rewrite any artifact whose content is stale.
42
+ * --check — exit 0 when all three are current, exit 1 naming the stale
43
+ * ones. Wired into `npm run docs:check`, hence `npm run lint`.
44
+ *
45
+ * **Whitespace is not content.** The two JSON artifacts are re-formatted by
46
+ * Biome via lint-staged after they are written, so comparing raw bytes would
47
+ * make the check fail on formatting alone. Both comparison and write go
48
+ * through {@link canonicalJson}: parse, re-serialize, compare. That is
49
+ * insensitive to whitespace and *sensitive* to key order, which is what
50
+ * actually matters — key order drives the doc-table order and the ordering a
51
+ * consumer's editor offers completions in.
29
52
  *
30
53
  * Per `.agents/rules/orchestration-error-handling.md`, unrecoverable
31
54
  * failures surface via `throw new Error(...)` so `runAsCli` can map the
@@ -37,6 +60,7 @@ import path from 'node:path';
37
60
  import { fileURLToPath } from 'node:url';
38
61
  import { parseArgs } from 'node:util';
39
62
  import { runAsCli } from './lib/cli-utils.js';
63
+ import { AGENTRC_SCHEMA } from './lib/config-settings-schema.js';
40
64
  import { Logger } from './lib/Logger.js';
41
65
 
42
66
  const __filename = fileURLToPath(import.meta.url);
@@ -48,103 +72,139 @@ const SCHEMA_PATH = path.join(
48
72
  'schemas',
49
73
  'agentrc.schema.json',
50
74
  );
75
+ const REFERENCE_PATH = path.join(
76
+ PROJECT_ROOT,
77
+ '.agents',
78
+ 'docs',
79
+ 'agentrc-reference.json',
80
+ );
51
81
  const DOC_PATH = path.join(PROJECT_ROOT, '.agents', 'docs', 'configuration.md');
52
82
  const REGION_BEGIN = '<!-- BEGIN GENERATED:agentrc -->';
53
83
  const REGION_END = '<!-- END GENERATED:agentrc -->';
54
84
 
85
+ /** Envelope keys prepended to the serialized runtime schema. */
86
+ const MIRROR_ENVELOPE = {
87
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
88
+ $id: 'https://github.com/dsj1984/mandrel/blob/main/.agents/schemas/agentrc.schema.json',
89
+ title: 'Mandrel .agentrc',
90
+ description:
91
+ 'GENERATED — do not edit. Emitted by `node .agents/scripts/generate-config-docs.js` from the runtime AJV schema in `.agents/scripts/lib/config-settings-schema.js` (plus its `-delivery` / `-quality` / `config/gates/*` modules), which is the single source of truth for the `.agentrc.json` surface. This file exists for editor tooling and human readers; because it is a serialization of the runtime schema rather than a hand-kept mirror, the two cannot disagree. Edit the annotated schema literals and re-run `npm run docs:gen`.',
92
+ };
93
+
94
+ /** `$schema` pointer written into the generated defaults inventory. */
95
+ const REFERENCE_SCHEMA_POINTER = '../schemas/agentrc.schema.json';
96
+
55
97
  // Order matters — drives the per-section emission sequence.
56
- const TOP_LEVEL_KEYS = ['project', 'github', 'planning', 'delivery'];
98
+ const TOP_LEVEL_KEYS = ['project', 'github', 'planning', 'delivery', 'qa'];
57
99
 
58
100
  /**
59
- * Read and parse the agentrc JSON Schema.
101
+ * Canonical JSON text for comparison and for writing. Key order is preserved
102
+ * (and therefore compared); whitespace is normalized away.
60
103
  *
61
- * @param {string} file Absolute path to the schema file.
62
- * @returns {object}
104
+ * @param {unknown} value
105
+ * @returns {string}
63
106
  */
64
- function readSchema(file) {
65
- if (!fs.existsSync(file)) {
66
- throw new Error(`Schema file not found: ${file}`);
67
- }
68
- let raw;
69
- try {
70
- raw = fs.readFileSync(file, 'utf8');
71
- } catch (err) {
72
- throw new Error(`Failed to read schema ${file}: ${err.message}`);
73
- }
107
+ function canonicalJson(value) {
108
+ return `${JSON.stringify(value, null, 2)}\n`;
109
+ }
110
+
111
+ /**
112
+ * Read a JSON artifact and return its canonical text, or `null` when the file
113
+ * is absent (first generation) or unparseable (a hand-mangled artifact is
114
+ * treated as stale, not as a crash).
115
+ *
116
+ * @param {string} file
117
+ * @returns {string | null}
118
+ */
119
+ function readCanonicalJson(file) {
120
+ if (!fs.existsSync(file)) return null;
74
121
  try {
75
- return JSON.parse(raw);
76
- } catch (err) {
77
- throw new Error(`Failed to parse schema ${file}: ${err.message}`);
122
+ return canonicalJson(JSON.parse(fs.readFileSync(file, 'utf8')));
123
+ } catch {
124
+ return null;
78
125
  }
79
126
  }
80
127
 
81
128
  /**
82
- * Resolve a `$ref` pointing into `#/$defs/<name>`. Throws on unknown refs
83
- * because every ref in the agentrc schema is a local one.
129
+ * Build the shipped JSON-Schema mirror: the 2020-12 envelope followed by the
130
+ * runtime schema verbatim.
131
+ *
132
+ * The runtime schema is a plain, cycle-free object literal (no RegExp or
133
+ * function values), so `structuredClone` is a faithful serialization. Shared
134
+ * sub-schema objects — `TOLERANCE_SCHEMA` and friends, referenced by several
135
+ * gates — are inlined at each use rather than hoisted into `$defs`. The
136
+ * inlined document is smaller than the hand-maintained `$defs` mirror it
137
+ * replaces, and it removes the naming heuristic a hoisting pass would need.
84
138
  *
85
- * @param {object} schema The root schema object.
86
- * @param {string} ref The `$ref` value.
139
+ * @param {object} schema
87
140
  * @returns {object}
88
141
  */
89
- function resolveRef(schema, ref) {
90
- if (typeof ref !== 'string' || !ref.startsWith('#/$defs/')) {
91
- throw new Error(`Unsupported $ref shape: ${ref}`);
92
- }
93
- const name = ref.slice('#/$defs/'.length);
94
- const target = schema.$defs?.[name];
95
- if (!target) {
96
- throw new Error(`Unresolved $ref: ${ref}`);
142
+ function buildMirrorSchema(schema) {
143
+ return { ...MIRROR_ENVELOPE, ...structuredClone(schema) };
144
+ }
145
+
146
+ /**
147
+ * Collect the `default` annotations under `node` into a nested plain object.
148
+ *
149
+ * A node carrying `default` contributes that value verbatim and is NOT
150
+ * descended into — that is what lets an object-shaped or array-shaped default
151
+ * (`github.branchProtection.requiredChecks`, `qa.environments`) be declared in
152
+ * one place. A properties-bearing node with no `default` contributes an object
153
+ * built from whichever children contributed something, or nothing at all when
154
+ * none did.
155
+ *
156
+ * @param {object} node
157
+ * @returns {unknown} The built value, or `undefined` for "contributes nothing".
158
+ */
159
+ function collectDefaults(node) {
160
+ if (!node || typeof node !== 'object') return undefined;
161
+ if (Object.hasOwn(node, 'default')) return structuredClone(node.default);
162
+ if (!node.properties) return undefined;
163
+ const out = {};
164
+ for (const [key, child] of Object.entries(node.properties)) {
165
+ const built = collectDefaults(child);
166
+ if (built !== undefined) out[key] = built;
97
167
  }
98
- return target;
168
+ return Object.keys(out).length > 0 ? out : undefined;
169
+ }
170
+
171
+ /**
172
+ * Build the defaults inventory shipped as `.agents/docs/agentrc-reference.json`.
173
+ *
174
+ * @param {object} schema
175
+ * @returns {object}
176
+ */
177
+ function buildReferenceInventory(schema) {
178
+ return {
179
+ $schema: REFERENCE_SCHEMA_POINTER,
180
+ ...(collectDefaults(schema) ?? {}),
181
+ };
99
182
  }
100
183
 
101
184
  /**
102
- * Collapse an `allOf` envelope down to its first concrete sub-schema. The
103
- * agentrc schema uses `allOf` exclusively to apply guard constraints
104
- * (`safeString`, `minLength`) on top of a base type the first member
105
- * carries the type for documentation purposes.
185
+ * Collapse an `allOf` envelope down onto its base node. The schema uses
186
+ * `allOf` only to hang a conditional (`if`/`then`) constraint off a block, so
187
+ * the merged view carries the base type for documentation purposes.
106
188
  *
107
- * @param {object} schema The root schema (needed for ref resolution).
108
- * @param {object} node The schema node to flatten.
189
+ * @param {object} node The schema node to flatten.
109
190
  * @returns {object}
110
191
  */
111
- function flattenAllOf(schema, node) {
192
+ function flattenAllOf(node) {
112
193
  if (!node || typeof node !== 'object') return node;
113
194
  if (!Array.isArray(node.allOf)) return node;
114
195
  const merged = { ...node };
115
196
  delete merged.allOf;
116
197
  for (const member of node.allOf) {
117
- const resolved = member.$ref ? resolveRef(schema, member.$ref) : member;
118
- for (const [key, value] of Object.entries(resolved)) {
198
+ for (const [key, value] of Object.entries(member)) {
119
199
  if (merged[key] === undefined) merged[key] = value;
120
200
  }
121
201
  }
122
202
  return merged;
123
203
  }
124
204
 
125
- /**
126
- * Resolve a property node down to its first non-ref form. Returns the
127
- * resolved node alongside the original ref name (if any) so callers can
128
- * detect when to recurse into a referenced object def.
129
- *
130
- * @param {object} schema
131
- * @param {object} node
132
- * @returns {{ node: object, refName: string | null }}
133
- */
134
- function resolveNode(schema, node) {
135
- if (node && typeof node === 'object' && typeof node.$ref === 'string') {
136
- const refName = node.$ref.startsWith('#/$defs/')
137
- ? node.$ref.slice('#/$defs/'.length)
138
- : null;
139
- return { node: resolveRef(schema, node.$ref), refName };
140
- }
141
- return { node, refName: null };
142
- }
143
-
144
205
  /**
145
206
  * Render the "Type" cell for an `array`-typed node by inspecting its `items`
146
- * schema. Mirrors the original inline ladder exactly: a `$ref` item renders
147
- * `array<RefName>`, an enum item renders `array<enum>`, a typed item renders
207
+ * schema: an enum item renders `array<enum>`, a typed item renders
148
208
  * `array<type>`, and anything else collapses to a bare `array`.
149
209
  *
150
210
  * @param {object} flat Flattened array node.
@@ -153,12 +213,6 @@ function resolveNode(schema, node) {
153
213
  function renderArrayType(flat) {
154
214
  const items = flat.items;
155
215
  if (items && typeof items === 'object') {
156
- if (items.$ref) {
157
- const refName = items.$ref.startsWith('#/$defs/')
158
- ? items.$ref.slice('#/$defs/'.length)
159
- : items.$ref;
160
- return `\`array<${refName}>\``;
161
- }
162
216
  if (Array.isArray(items.enum)) {
163
217
  return `\`array<enum>\``;
164
218
  }
@@ -194,16 +248,24 @@ function renderObjectType(flat) {
194
248
  * row here rather than another nested branch.
195
249
  *
196
250
  * Order is load-bearing — `oneOf` and `enum` are matched before the plain
197
- * `type` rules, exactly as the original ladder short-circuited.
251
+ * `type` rules.
198
252
  *
199
253
  * @type {Array<{ when: (flat: object) => boolean, render: (flat: object) => string }>}
200
254
  */
201
255
  const TYPE_RULES = [
202
- // The only oneOf in the schema is `listOrExtenderOfStrings`.
203
256
  {
204
- when: (flat) => Array.isArray(flat.oneOf),
257
+ // The list-or-extender union: a plain `string[]` (replace) or an
258
+ // `{ append?, prepend? }` object that deep-merges with the framework list.
259
+ when: (flat) =>
260
+ Array.isArray(flat.oneOf) &&
261
+ flat.oneOf.some((m) => m?.properties?.append || m?.properties?.prepend),
205
262
  render: () => '`string[]` or `{ append?, prepend? }`',
206
263
  },
264
+ {
265
+ when: (flat) => Array.isArray(flat.oneOf),
266
+ render: (flat) =>
267
+ `one of: ${flat.oneOf.map((m) => `\`${m?.type ?? '?'}\``).join(', ')}`,
268
+ },
207
269
  {
208
270
  when: (flat) => Array.isArray(flat.enum),
209
271
  render: (flat) =>
@@ -228,32 +290,31 @@ const TYPE_RULES = [
228
290
  ];
229
291
 
230
292
  /**
231
- * Render the "Type" cell for a schema node. The agentrc schema uses a few
232
- * recurring shapes string, integer, number, boolean, array, object,
233
- * `oneOf` (the `listOrExtenderOfStrings` extender form), enum, and nullable
234
- * variants. Anything else falls through to a `?` so missing coverage is
235
- * visible rather than silently wrong.
293
+ * Render the "Type" cell for a schema node. Anything the table does not match
294
+ * falls through to a `?` so missing coverage is visible rather than silently
295
+ * wrong.
236
296
  *
237
- * @param {object} schema
238
297
  * @param {object} node
239
298
  * @returns {string}
240
299
  */
241
- function renderType(schema, node) {
300
+ function renderType(node) {
242
301
  if (!node || typeof node !== 'object') return '?';
243
- const flat = flattenAllOf(schema, node);
302
+ const flat = flattenAllOf(node);
244
303
  const rule = TYPE_RULES.find((r) => r.when(flat));
245
304
  return rule ? rule.render(flat) : '?';
246
305
  }
247
306
 
248
307
  /**
249
- * Render the "Default" cell. Schemas without an explicit `default` get an
250
- * em dash — the value is documented prose-side in `.agents/docs/configuration.md`.
308
+ * Render the "Default" cell. Nodes without an explicit `default` annotation
309
+ * get an em dash — see the annotation contract in `config-settings-schema.js`
310
+ * for why a key can have a runtime default and no annotation.
251
311
  *
252
- * @param {unknown} value
312
+ * @param {object} node
253
313
  * @returns {string}
254
314
  */
255
- function renderDefault(value) {
256
- if (value === undefined) return '—';
315
+ function renderDefault(node) {
316
+ if (!node || !Object.hasOwn(node, 'default')) return '—';
317
+ const value = node.default;
257
318
  if (value === null) return '`null`';
258
319
  if (typeof value === 'string') return `\`"${value}"\``;
259
320
  if (typeof value === 'boolean' || typeof value === 'number') {
@@ -282,11 +343,10 @@ function escapeCell(text) {
282
343
  * Returns `null` when `flat` is not a properties-bearing object, so the
283
344
  * caller can fall through to the next row shape.
284
345
  *
285
- * @param {object} schema
286
346
  * @param {{flat: object, keyPath: string, pathParts: string[], propName: string, isRequired: boolean, description: string}} ctx
287
347
  * @returns {Array<object> | null}
288
348
  */
289
- function nestedObjectRows(schema, ctx) {
349
+ function nestedObjectRows(ctx) {
290
350
  const { flat, keyPath, pathParts, propName, isRequired, description } = ctx;
291
351
  if (flat.type !== 'object' || !flat.properties) return null;
292
352
  const childRequired = new Set(
@@ -297,10 +357,10 @@ function nestedObjectRows(schema, ctx) {
297
357
  key: keyPath,
298
358
  required: isRequired ? 'Yes' : 'No',
299
359
  type: '`object`',
300
- def: renderDefault(flat.default),
360
+ def: renderDefault(flat),
301
361
  description: description || 'Nested configuration block.',
302
362
  },
303
- ...flattenObject(schema, flat, [...pathParts, propName], childRequired),
363
+ ...flattenObject(flat, [...pathParts, propName], childRequired),
304
364
  ];
305
365
  }
306
366
 
@@ -309,28 +369,24 @@ function nestedObjectRows(schema, ctx) {
309
369
  * describing the item shape in the Description cell. Returns `null` when the
310
370
  * property is not an array whose items are a properties-bearing object.
311
371
  *
312
- * @param {object} schema
313
372
  * @param {{flat: object, keyPath: string, isRequired: boolean, description: string}} ctx
314
373
  * @returns {Array<object> | null}
315
374
  */
316
- function arrayOfObjectsRows(schema, ctx) {
375
+ function arrayOfObjectsRows(ctx) {
317
376
  const { flat, keyPath, isRequired, description } = ctx;
318
377
  if (flat.type !== 'array' || !flat.items) return null;
319
- const { node: itemNode, refName } = resolveNode(schema, flat.items);
378
+ const itemNode = flat.items;
320
379
  if (!itemNode || itemNode.type !== 'object' || !itemNode.properties) {
321
380
  return null;
322
381
  }
323
382
  const itemKeys = Object.keys(itemNode.properties).join(', ');
324
- const suffix = refName ? ` (\`${refName}\`)` : '';
325
- const desc =
326
- (description ? `${description} ` : '') +
327
- `Each item${suffix} has: ${itemKeys}.`;
383
+ const desc = `${description ? `${description} ` : ''}Each item has: ${itemKeys}.`;
328
384
  return [
329
385
  {
330
386
  key: `${keyPath}[]`,
331
387
  required: isRequired ? 'Yes' : 'No',
332
- type: renderType(schema, flat),
333
- def: renderDefault(flat.default),
388
+ type: renderType(flat),
389
+ def: renderDefault(flat),
334
390
  description: desc,
335
391
  },
336
392
  ];
@@ -341,66 +397,61 @@ function arrayOfObjectsRows(schema, ctx) {
341
397
  * it is the fallthrough shape when neither the nested-object nor the
342
398
  * array-of-objects builder applied.
343
399
  *
344
- * @param {object} schema
345
400
  * @param {{flat: object, keyPath: string, isRequired: boolean, description: string}} ctx
346
401
  * @returns {Array<object>}
347
402
  */
348
- function leafRow(schema, ctx) {
403
+ function leafRow(ctx) {
349
404
  const { flat, keyPath, isRequired, description } = ctx;
350
405
  return [
351
406
  {
352
407
  key: keyPath,
353
408
  required: isRequired ? 'Yes' : 'No',
354
- type: renderType(schema, flat),
355
- def: renderDefault(flat.default),
409
+ type: renderType(flat),
410
+ def: renderDefault(flat),
356
411
  description: description || '—',
357
412
  },
358
413
  ];
359
414
  }
360
415
 
361
416
  // Ordered row-shape builders for one property. The loop in flattenObject
362
- // returns the first builder that yields rows (non-null), matching the
363
- // original if/continue ladder: nested-object first, array-of-objects next,
364
- // scalar leaf as the always-matching fallthrough.
417
+ // returns the first builder that yields rows (non-null): nested-object first,
418
+ // array-of-objects next, scalar leaf as the always-matching fallthrough.
365
419
  const ROW_BUILDERS = [nestedObjectRows, arrayOfObjectsRows, leafRow];
366
420
 
367
421
  /**
368
- * Flatten one object-typed schema node into table rows. Recurses into
369
- * nested `object` properties (resolving `$ref`s along the way) so dot-paths
370
- * like `paths.agentRoot` and `branchProtection.requiredChecks` show up as
371
- * individual rows.
422
+ * Flatten one object-typed schema node into table rows. Recurses into nested
423
+ * `object` properties so dot-paths like `paths.agentRoot` and
424
+ * `branchProtection.requiredChecks` show up as individual rows.
372
425
  *
373
- * Arrays of objects (`requiredChecks[]`, `routes[]`, `bundles[]`) are
374
- * emitted as a single row whose Key column carries a `[]` suffix; the row's
375
- * item shape is captured in the Description cell. This keeps the output
376
- * legible without exploding into per-item-property rows.
426
+ * Arrays of objects (`requiredChecks[]`, `routes[]`, `bundles[]`) are emitted
427
+ * as a single row whose Key column carries a `[]` suffix; the item shape is
428
+ * captured in the Description cell. This keeps the output legible without
429
+ * exploding into per-item-property rows.
377
430
  *
378
- * @param {object} schema Root schema (for ref resolution).
379
431
  * @param {object} node Schema node to flatten.
380
432
  * @param {string[]} pathParts Dot-path accumulator.
381
433
  * @param {Set<string>} required Required-property names on the parent.
382
434
  * @returns {Array<{key:string, required:string, type:string, def:string, description:string}>}
383
435
  */
384
- function flattenObject(schema, node, pathParts, required) {
436
+ function flattenObject(node, pathParts, required) {
385
437
  const rows = [];
386
438
  const properties = node.properties || {};
387
439
  const localRequired = new Set(
388
440
  Array.isArray(node.required) ? node.required : [],
389
441
  );
390
442
 
391
- for (const [propName, rawChild] of Object.entries(properties)) {
392
- const { node: child } = resolveNode(schema, rawChild);
393
- const flat = flattenAllOf(schema, child);
443
+ for (const [propName, child] of Object.entries(properties)) {
444
+ const flat = flattenAllOf(child);
394
445
  const ctx = {
395
446
  flat,
396
447
  keyPath: [...pathParts, propName].join('.'),
397
448
  pathParts,
398
449
  propName,
399
450
  isRequired: required.has(propName) || localRequired.has(propName),
400
- description: flat.description || rawChild.description || '',
451
+ description: flat.description || '',
401
452
  };
402
453
  for (const build of ROW_BUILDERS) {
403
- const built = build(schema, ctx);
454
+ const built = build(ctx);
404
455
  if (built !== null) {
405
456
  rows.push(...built);
406
457
  break;
@@ -419,13 +470,11 @@ function flattenObject(schema, node, pathParts, required) {
419
470
  * @returns {string}
420
471
  */
421
472
  function renderSection(schema, topKey) {
422
- const rootProps = schema.properties || {};
423
- const rawNode = rootProps[topKey];
424
- if (!rawNode) {
473
+ const node = (schema.properties || {})[topKey];
474
+ if (!node) {
425
475
  throw new Error(`Top-level key "${topKey}" missing from schema.properties`);
426
476
  }
427
- const { node } = resolveNode(schema, rawNode);
428
- const flat = flattenAllOf(schema, node);
477
+ const flat = flattenAllOf(node);
429
478
 
430
479
  if (flat.type !== 'object' || !flat.properties) {
431
480
  throw new Error(
@@ -441,7 +490,7 @@ function renderSection(schema, topKey) {
441
490
  Array.isArray(flat.required) ? flat.required : [],
442
491
  );
443
492
 
444
- const rows = flattenObject(schema, flat, [], childRequired);
493
+ const rows = flattenObject(flat, [], childRequired);
445
494
  const header = `### \`${topKey}\` ${sectionRequired ? '(required)' : '(optional)'}`;
446
495
  const tableHeader = '| Key | Required | Type | Default | Description |';
447
496
  const tableSep = '| --- | --- | --- | --- | --- |';
@@ -467,10 +516,13 @@ function renderSection(schema, topKey) {
467
516
  function renderRegion(schema) {
468
517
  const blocks = [
469
518
  '',
470
- '> Generated by `node .agents/scripts/generate-config-docs.js` from',
471
- `> [\`.agents/schemas/agentrc.schema.json\`](../schemas/agentrc.schema.json).`,
472
- '> Edit the schema (and its AJV mirror under `.agents/scripts/lib/`),',
473
- '> then re-run the generator do not hand-edit this region.',
519
+ '> Generated by `node .agents/scripts/generate-config-docs.js` from the',
520
+ '> runtime AJV schema in',
521
+ '> [`.agents/scripts/lib/config-settings-schema.js`](../scripts/lib/config-settings-schema.js).',
522
+ '> Edit the `description` / `default` annotations on those schema literals',
523
+ '> and re-run `npm run docs:gen` — do not hand-edit this region, and do not',
524
+ '> hand-edit `agentrc.schema.json` or `agentrc-reference.json` either: both',
525
+ '> are emitted by the same generator.',
474
526
  '',
475
527
  ];
476
528
  for (const key of TOP_LEVEL_KEYS) {
@@ -541,21 +593,53 @@ function spliceRegion(original, body) {
541
593
  }
542
594
 
543
595
  /**
544
- * Build the canonical post-generation file content.
596
+ * Build every generated artifact from `schema` and pair each with what is on
597
+ * disk today.
598
+ *
599
+ * The JSON artifacts compare canonically (see the module header); the
600
+ * Markdown artifact compares raw, because its region is spliced into
601
+ * hand-authored prose that must survive byte-for-byte.
545
602
  *
546
- * @param {string} schemaPath
547
- * @param {string} docPath
548
- * @returns {{ generated: string, original: string }}
603
+ * @param {{ schema?: object, schemaPath?: string, referencePath?: string,
604
+ * docPath?: string }} [opts]
605
+ * @returns {Array<{ name: string, file: string, generated: string,
606
+ * current: string | null, stale: boolean }>}
549
607
  */
550
- function buildExpected(schemaPath, docPath) {
608
+ function buildArtifacts(opts = {}) {
609
+ const {
610
+ schema = AGENTRC_SCHEMA,
611
+ schemaPath = SCHEMA_PATH,
612
+ referencePath = REFERENCE_PATH,
613
+ docPath = DOC_PATH,
614
+ } = opts;
615
+
551
616
  if (!fs.existsSync(docPath)) {
552
617
  throw new Error(`Target doc not found: ${docPath}`);
553
618
  }
554
- const original = fs.readFileSync(docPath, 'utf8');
555
- const schema = readSchema(schemaPath);
556
- const body = renderRegion(schema);
557
- const generated = spliceRegion(original, body);
558
- return { generated, original };
619
+ const docOriginal = fs.readFileSync(docPath, 'utf8');
620
+
621
+ const artifacts = [
622
+ {
623
+ name: 'mirror schema',
624
+ file: schemaPath,
625
+ generated: canonicalJson(buildMirrorSchema(schema)),
626
+ current: readCanonicalJson(schemaPath),
627
+ },
628
+ {
629
+ name: 'defaults inventory',
630
+ file: referencePath,
631
+ generated: canonicalJson(buildReferenceInventory(schema)),
632
+ current: readCanonicalJson(referencePath),
633
+ },
634
+ {
635
+ name: 'configuration.md key table',
636
+ file: docPath,
637
+ generated: spliceRegion(docOriginal, renderRegion(schema)),
638
+ current: docOriginal,
639
+ },
640
+ ];
641
+ for (const a of artifacts) a.stale = a.generated !== a.current;
642
+ return artifacts;
559
643
  }
560
644
 
561
645
  /**
@@ -570,39 +654,47 @@ async function main(argv = process.argv.slice(2)) {
570
654
  allowPositionals: false,
571
655
  });
572
656
 
573
- const { generated, original } = buildExpected(SCHEMA_PATH, DOC_PATH);
657
+ const artifacts = buildArtifacts();
658
+ const stale = artifacts.filter((a) => a.stale);
659
+ const rel = (f) => path.relative(PROJECT_ROOT, f);
574
660
 
575
661
  if (values.check) {
576
- if (generated === original) {
662
+ if (stale.length === 0) {
577
663
  Logger.info(
578
- `generate-config-docs: ${path.relative(PROJECT_ROOT, DOC_PATH)} is up to date.`,
664
+ `generate-config-docs: all ${artifacts.length} generated config artifacts are up to date.`,
579
665
  );
580
666
  return;
581
667
  }
582
- const hint =
583
- `${path.relative(PROJECT_ROOT, DOC_PATH)} is out of date. ` +
584
- 'Run `node .agents/scripts/generate-config-docs.js` to regenerate the bounded region.';
585
- throw new Error(hint);
668
+ throw new Error(
669
+ `${stale.length} generated config artifact(s) drifted from the runtime schema: ` +
670
+ `${stale.map((a) => `${a.name} (${rel(a.file)})`).join(', ')}. ` +
671
+ 'Run `node .agents/scripts/generate-config-docs.js` to regenerate.',
672
+ );
586
673
  }
587
674
 
588
- if (generated === original) {
675
+ if (stale.length === 0) {
589
676
  Logger.info(
590
- `generate-config-docs: ${path.relative(PROJECT_ROOT, DOC_PATH)} already current — no write.`,
677
+ 'generate-config-docs: every generated config artifact already current — no write.',
591
678
  );
592
679
  return;
593
680
  }
594
- fs.writeFileSync(DOC_PATH, generated, 'utf8');
681
+ for (const a of stale) {
682
+ fs.writeFileSync(a.file, a.generated, 'utf8');
683
+ }
595
684
  Logger.info(
596
- `generate-config-docs: wrote bounded region into ${path.relative(PROJECT_ROOT, DOC_PATH)}.`,
685
+ `generate-config-docs: rewrote ${stale.map((a) => rel(a.file)).join(', ')}.`,
597
686
  );
598
687
  }
599
688
 
600
689
  export {
601
- buildExpected,
690
+ buildArtifacts,
691
+ buildMirrorSchema,
692
+ buildReferenceInventory,
693
+ canonicalJson,
694
+ collectDefaults,
602
695
  flattenObject,
603
696
  REGION_BEGIN,
604
697
  REGION_END,
605
- readSchema,
606
698
  renderRegion,
607
699
  renderSection,
608
700
  spliceRegion,
@@ -613,11 +705,11 @@ runAsCli(import.meta.url, main, {
613
705
  usage: {
614
706
  invocation: 'node .agents/scripts/generate-config-docs.js [--check]',
615
707
  summary:
616
- 'Regenerate the configuration reference from the .agentrc schema. Writes only when the generated content differs.',
708
+ 'Regenerate the three .agentrc config artifacts (JSON-Schema mirror, defaults inventory, configuration.md key table) from the runtime AJV schema. Writes only what drifted.',
617
709
  flags: [
618
710
  [
619
711
  '--check',
620
- 'Verify the doc is current and fail if stale; write nothing.',
712
+ 'Verify every artifact is current and fail naming the stale ones; write nothing.',
621
713
  ],
622
714
  ],
623
715
  },