mandrel 1.59.0 → 1.60.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 (240) hide show
  1. package/.agents/README.md +14 -14
  2. package/.agents/docs/SDLC.md +129 -134
  3. package/.agents/docs/configuration.md +16 -16
  4. package/.agents/docs/workflows.md +6 -8
  5. package/.agents/instructions.md +12 -11
  6. package/.agents/personas/architect.md +1 -1
  7. package/.agents/personas/product.md +1 -1
  8. package/.agents/personas/project-manager.md +14 -14
  9. package/.agents/personas/technical-writer.md +1 -1
  10. package/.agents/rules/changelog-style.md +5 -5
  11. package/.agents/rules/git-conventions.md +3 -3
  12. package/.agents/schemas/agentrc.schema.json +3 -3
  13. package/.agents/schemas/dispatch-manifest.json +4 -4
  14. package/.agents/schemas/epic-spec.schema.json +15 -45
  15. package/.agents/schemas/lifecycle/README.md +1 -1
  16. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
  17. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
  18. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
  19. package/.agents/schemas/validation-evidence.schema.json +1 -1
  20. package/.agents/scripts/README.md +1 -1
  21. package/.agents/scripts/acceptance-eval.js +1 -1
  22. package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
  23. package/.agents/scripts/analyze-execution.js +2 -2
  24. package/.agents/scripts/audit-to-stories.js +1 -1
  25. package/.agents/scripts/check-doc-links.js +2 -3
  26. package/.agents/scripts/diagnose-friction.js +1 -1
  27. package/.agents/scripts/dispatcher.js +2 -2
  28. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  29. package/.agents/scripts/epic-audit-prepare.js +3 -3
  30. package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
  31. package/.agents/scripts/epic-deliver-preflight.js +6 -6
  32. package/.agents/scripts/epic-deliver-prepare.js +1 -1
  33. package/.agents/scripts/epic-execute-record-wave.js +4 -4
  34. package/.agents/scripts/epic-plan-healthcheck.js +6 -10
  35. package/.agents/scripts/epic-plan-spec-validate.js +1 -1
  36. package/.agents/scripts/epic-reconcile.js +11 -29
  37. package/.agents/scripts/evidence-gate.js +1 -1
  38. package/.agents/scripts/generate-workflows-doc.js +1 -1
  39. package/.agents/scripts/hierarchy-gate.js +7 -11
  40. package/.agents/scripts/lib/ITicketingProvider.js +1 -1
  41. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  42. package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
  43. package/.agents/scripts/lib/baseline-snapshot.js +7 -7
  44. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  45. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  46. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
  47. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  48. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
  49. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  50. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  51. package/.agents/scripts/lib/config/explain.js +1 -1
  52. package/.agents/scripts/lib/config/runners.js +2 -2
  53. package/.agents/scripts/lib/config/runtime.js +1 -1
  54. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  55. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  56. package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
  57. package/.agents/scripts/lib/config-settings-schema.js +3 -3
  58. package/.agents/scripts/lib/duplicate-search.js +1 -1
  59. package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
  60. package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
  61. package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
  62. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
  63. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  64. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  65. package/.agents/scripts/lib/findings/promote-finding.js +10 -10
  66. package/.agents/scripts/lib/label-constants.js +3 -4
  67. package/.agents/scripts/lib/label-taxonomy.js +3 -8
  68. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  69. package/.agents/scripts/lib/orchestration/code-review.js +5 -5
  70. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
  71. package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
  72. package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
  73. package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
  74. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
  75. package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
  76. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
  77. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
  78. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
  79. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
  80. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
  81. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
  82. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
  83. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
  84. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
  85. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
  86. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
  87. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
  88. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
  89. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
  90. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
  91. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
  92. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
  93. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
  94. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
  95. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
  96. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
  97. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
  98. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
  99. package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
  100. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
  101. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
  102. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
  105. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
  106. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
  107. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
  108. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
  109. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
  110. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
  111. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  112. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
  113. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
  114. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
  115. package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
  116. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
  117. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
  118. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
  119. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  120. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
  121. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
  122. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
  123. package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
  124. package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
  126. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  127. package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
  128. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  129. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
  130. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
  131. package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
  132. package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
  133. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
  134. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
  135. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
  136. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
  137. package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
  138. package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
  139. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
  140. package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
  141. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  142. package/.agents/scripts/lib/preflight-runner.js +1 -1
  143. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
  144. package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
  145. package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
  146. package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
  147. package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
  148. package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
  149. package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
  150. package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
  151. package/.agents/scripts/lib/signals/schema.js +1 -1
  152. package/.agents/scripts/lib/spec/index.js +1 -1
  153. package/.agents/scripts/lib/spec/loader.js +2 -2
  154. package/.agents/scripts/lib/spec/state.js +7 -16
  155. package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
  156. package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
  157. package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
  158. package/.agents/scripts/lib/story-lifecycle.js +8 -8
  159. package/.agents/scripts/lib/story-plan.js +1 -1
  160. package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
  161. package/.agents/scripts/lib/wave-runner/tick.js +1 -1
  162. package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
  163. package/.agents/scripts/lifecycle-emit.js +1 -1
  164. package/.agents/scripts/providers/github/board-add.js +1 -1
  165. package/.agents/scripts/providers/github/errors.js +1 -1
  166. package/.agents/scripts/providers/github/mappers.js +2 -2
  167. package/.agents/scripts/providers/github/tickets.js +4 -4
  168. package/.agents/scripts/resync-status-column.js +1 -1
  169. package/.agents/scripts/retro-run.js +2 -2
  170. package/.agents/scripts/run-lint.js +1 -1
  171. package/.agents/scripts/single-story-init.js +1 -1
  172. package/.agents/scripts/stories-wave-tick.js +5 -5
  173. package/.agents/scripts/story-close.js +1 -1
  174. package/.agents/scripts/story-init.js +13 -16
  175. package/.agents/scripts/story-phase.js +5 -5
  176. package/.agents/scripts/story-plan.js +3 -3
  177. package/.agents/scripts/sync-branch-from-base.js +1 -1
  178. package/.agents/scripts/validate-docs-freshness.js +1 -1
  179. package/.agents/scripts/wave-tick.js +1 -1
  180. package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
  181. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
  182. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
  183. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
  184. package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
  185. package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
  186. package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
  187. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
  188. package/.agents/skills/core/scope-triage/SKILL.md +9 -10
  189. package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
  190. package/.agents/skills/skills.index.json +7 -7
  191. package/.agents/templates/agent-protocol.md +2 -2
  192. package/.agents/workflows/agents-update.md +2 -2
  193. package/.agents/workflows/audit-architecture.md +2 -2
  194. package/.agents/workflows/audit-clean-code.md +2 -2
  195. package/.agents/workflows/audit-dependencies.md +1 -1
  196. package/.agents/workflows/audit-devops.md +1 -1
  197. package/.agents/workflows/audit-documentation.md +2 -2
  198. package/.agents/workflows/audit-lighthouse.md +1 -1
  199. package/.agents/workflows/audit-performance.md +2 -2
  200. package/.agents/workflows/audit-privacy.md +1 -1
  201. package/.agents/workflows/audit-quality.md +2 -2
  202. package/.agents/workflows/audit-security.md +2 -2
  203. package/.agents/workflows/audit-seo.md +1 -1
  204. package/.agents/workflows/audit-sre.md +1 -1
  205. package/.agents/workflows/audit-to-stories.md +10 -10
  206. package/.agents/workflows/audit-ux-ui.md +1 -1
  207. package/.agents/workflows/deliver.md +85 -0
  208. package/.agents/workflows/explain.md +3 -3
  209. package/.agents/workflows/git-merge-pr.md +1 -1
  210. package/.agents/workflows/git-pr-all.md +13 -10
  211. package/.agents/workflows/git-push.md +6 -3
  212. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  213. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  214. package/.agents/workflows/helpers/code-review.md +5 -5
  215. package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
  216. package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
  217. package/.agents/workflows/helpers/diagnose.md +1 -1
  218. package/.agents/workflows/helpers/epic-audit.md +6 -6
  219. package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
  220. package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
  221. package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
  222. package/.agents/workflows/helpers/epic-testing.md +3 -3
  223. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  224. package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
  225. package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
  226. package/.agents/workflows/helpers/signals.md +1 -1
  227. package/.agents/workflows/helpers/single-story-deliver.md +11 -11
  228. package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
  229. package/.agents/workflows/onboard.md +17 -17
  230. package/.agents/workflows/plan.md +89 -0
  231. package/.agents/workflows/qa-explore.md +1 -1
  232. package/.agents/workflows/qa-run-harness.md +1 -1
  233. package/README.md +4 -12
  234. package/docs/CHANGELOG.md +1149 -0
  235. package/lib/cli/__tests__/update-changelog-surface.test.js +357 -0
  236. package/lib/cli/__tests__/update-reexec.test.js +513 -0
  237. package/lib/cli/init.js +31 -29
  238. package/lib/cli/update.js +413 -52
  239. package/package.json +2 -1
  240. package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
@@ -0,0 +1,357 @@
1
+ // lib/cli/__tests__/update-changelog-surface.test.js
2
+ /**
3
+ * Unit tests for the `defaultSurfaceChangelog` fallback chain and
4
+ * `fetchChangelogFromGitHub` introduced by Story #4035 — mandrel update
5
+ * changelog surface (ship or fetch).
6
+ *
7
+ * The three paths under test (via the `run` entrypoint):
8
+ * 1. Packaged file present → reads and prints the matching section(s).
9
+ * 2. Packaged file absent, GitHub fetch succeeds → prints the section(s)
10
+ * from the fetched content.
11
+ * 3. Both sources unavailable → emits an actionable warning with a link
12
+ * to the GitHub Releases page; never throws.
13
+ *
14
+ * Plus unit coverage for `fetchChangelogFromGitHub` itself:
15
+ * - Resolves the content from the first tag that returns 2xx.
16
+ * - Tries the namespaced tag (`mandrel-v<ver>`) first, then bare (`v<ver>`).
17
+ * - Throws when both tag forms return non-2xx.
18
+ * - Throws when the HTTP request errors out.
19
+ *
20
+ * `defaultSurfaceChangelog` is exercised through the `run` default export
21
+ * so the full wiring from `deps` through to output is verified. The
22
+ * `fetchChangelog` seam (and `https` in `fetchChangelogFromGitHub`) are
23
+ * injected so no real HTTP call occurs (testing-standards § Unit: mock all I/O).
24
+ *
25
+ * Tier: unit (testing-standards § Unit). All I/O — filesystem and network
26
+ * — is mocked via injectable seams.
27
+ *
28
+ * Security (security-baseline § 5 — Data Leakage & Logging): fixtures carry
29
+ * only version strings and file paths; no tokens or credentials are used.
30
+ */
31
+
32
+ import assert from 'node:assert/strict';
33
+ import { EventEmitter } from 'node:events';
34
+ import { describe, it } from 'node:test';
35
+
36
+ import run, { fetchChangelogFromGitHub } from '../update.js';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Shared fixtures
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const CURRENT_VERSION = '1.58.0';
43
+ const TARGET_VERSION = '1.59.0';
44
+ const CACHE_PATH = '/virtual/temp/version-check.json';
45
+ const CHANGELOG_PATH = '/virtual/docs/CHANGELOG.md';
46
+
47
+ const CHANGELOG_CONTENT = `# Changelog
48
+
49
+ ## [1.59.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.58.0...mandrel-v1.59.0) (2026-06-11)
50
+
51
+ ### Added
52
+
53
+ * **cli:** add mandrel init one-command cold start
54
+
55
+ ## [1.58.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.57.0...mandrel-v1.58.0) (2026-05-30)
56
+
57
+ ### Fixed
58
+
59
+ * an older fix from 1.58
60
+ `;
61
+
62
+ /**
63
+ * Minimal in-memory fs fake. When `changelogContent` is provided, the
64
+ * changelog path resolves; otherwise it throws ENOENT (simulating the
65
+ * absent-from-tarball scenario pre-Story #4035).
66
+ *
67
+ * @param {{ changelogContent?: string }} [opts]
68
+ */
69
+ function makeFs({ changelogContent } = {}) {
70
+ const files = new Map([
71
+ [
72
+ CACHE_PATH,
73
+ JSON.stringify({
74
+ latestVersion: TARGET_VERSION,
75
+ checkedAt: '2026-06-11T00:00:00.000Z',
76
+ }),
77
+ ],
78
+ ...(changelogContent ? [[CHANGELOG_PATH, changelogContent]] : []),
79
+ ]);
80
+ return {
81
+ readFileSync(p, _enc) {
82
+ if (!files.has(p)) {
83
+ throw Object.assign(new Error(`ENOENT: ${p}`), { code: 'ENOENT' });
84
+ }
85
+ return files.get(p);
86
+ },
87
+ writeFileSync() {},
88
+ mkdirSync() {},
89
+ existsSync(p) {
90
+ return files.has(p);
91
+ },
92
+ };
93
+ }
94
+
95
+ /** Capture stdout/stderr writes. */
96
+ function makeCapture() {
97
+ const out = [];
98
+ const err = [];
99
+ return {
100
+ out,
101
+ err,
102
+ write: (s) => out.push(s),
103
+ writeErr: (s) => err.push(s),
104
+ exit: () => {},
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Build the stubbed `deps` for `run`. Downstream seams (sync, migrate,
110
+ * doctor) are no-ops — the boundary under test is the changelog surface.
111
+ *
112
+ * @param {{
113
+ * fs: ReturnType<typeof makeFs>,
114
+ * cap: ReturnType<typeof makeCapture>,
115
+ * fetchChangelog?: (v: string) => Promise<string>,
116
+ * }} opts
117
+ */
118
+ function makeDeps(fs, cap, fetchChangelog) {
119
+ return {
120
+ currentVersion: CURRENT_VERSION,
121
+ cachePath: CACHE_PATH,
122
+ changelogPath: CHANGELOG_PATH,
123
+ fs,
124
+ now: new Date('2026-06-11T00:30:00.000Z'),
125
+ versionRunner: () => TARGET_VERSION,
126
+ runInstall: () => ({ status: 0, stderr: '' }),
127
+ runSync: () => ({ copied: 0, planned: 0, dryRun: false }),
128
+ runMigrations: () => ({ applied: [], skipped: [] }),
129
+ runDoctor: async () => ({ ok: true, results: [] }),
130
+ write: cap.write,
131
+ writeErr: cap.writeErr,
132
+ exit: cap.exit,
133
+ ...(fetchChangelog !== undefined ? { fetchChangelog } : {}),
134
+ };
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Path 1 — packaged file present
139
+ // ---------------------------------------------------------------------------
140
+
141
+ describe('defaultSurfaceChangelog — packaged file present (Story #4035)', () => {
142
+ it('prints the matching in-range section from the packaged changelog', async () => {
143
+ const fs = makeFs({ changelogContent: CHANGELOG_CONTENT });
144
+ const cap = makeCapture();
145
+
146
+ await run([], makeDeps(fs, cap));
147
+
148
+ const joined = cap.out.join('');
149
+ // In-range (1.59.0) section surfaced.
150
+ assert.match(joined, /Changelog for v1\.59\.0/);
151
+ assert.match(joined, /mandrel init one-command cold start/);
152
+ // Out-of-range section (1.58.0) must NOT appear.
153
+ assert.doesNotMatch(joined, /an older fix from 1\.58/);
154
+ });
155
+
156
+ it('does not invoke the GitHub fetch seam when the packaged file is readable', async () => {
157
+ const fs = makeFs({ changelogContent: CHANGELOG_CONTENT });
158
+ const cap = makeCapture();
159
+ let fetchCalled = false;
160
+ const fetchChangelog = async () => {
161
+ fetchCalled = true;
162
+ return CHANGELOG_CONTENT;
163
+ };
164
+
165
+ await run([], makeDeps(fs, cap, fetchChangelog));
166
+
167
+ assert.equal(
168
+ fetchCalled,
169
+ false,
170
+ 'GitHub fetch must not run when packaged file is present',
171
+ );
172
+ });
173
+ });
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Path 2 — packaged file absent, GitHub fetch succeeds
177
+ // ---------------------------------------------------------------------------
178
+
179
+ describe('defaultSurfaceChangelog — GitHub fetch fallback (Story #4035)', () => {
180
+ it('fetches from GitHub and prints the matching section when the packaged file is absent', async () => {
181
+ const fs = makeFs(); // no changelog seeded → ENOENT
182
+ const cap = makeCapture();
183
+ const fetchChangelog = async (_version) => CHANGELOG_CONTENT;
184
+
185
+ await run([], makeDeps(fs, cap, fetchChangelog));
186
+
187
+ const joined = cap.out.join('');
188
+ assert.match(joined, /Changelog for v1\.59\.0/);
189
+ assert.match(joined, /mandrel init one-command cold start/);
190
+ assert.doesNotMatch(joined, /an older fix from 1\.58/);
191
+ // No error written — successful fallback.
192
+ assert.deepEqual(cap.err, []);
193
+ });
194
+
195
+ it('passes the target version to the fetchChangelog seam', async () => {
196
+ const fs = makeFs();
197
+ const cap = makeCapture();
198
+ const fetched = [];
199
+ const fetchChangelog = async (version) => {
200
+ fetched.push(version);
201
+ return CHANGELOG_CONTENT;
202
+ };
203
+
204
+ await run([], makeDeps(fs, cap, fetchChangelog));
205
+
206
+ assert.deepEqual(fetched, [TARGET_VERSION]);
207
+ });
208
+ });
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Path 3 — both sources unavailable → actionable degradation message
212
+ // ---------------------------------------------------------------------------
213
+
214
+ describe('defaultSurfaceChangelog — actionable degradation (Story #4035)', () => {
215
+ it('emits a message with the GitHub Releases URL when both sources fail', async () => {
216
+ const fs = makeFs(); // no changelog → ENOENT
217
+ const cap = makeCapture();
218
+ const fetchChangelog = async () => {
219
+ throw new Error('HTTP 404');
220
+ };
221
+
222
+ await run([], makeDeps(fs, cap, fetchChangelog));
223
+
224
+ const errJoined = cap.err.join('');
225
+ // Must mention v1.59.0 and include the releases link.
226
+ assert.match(errJoined, /v1\.59\.0/);
227
+ assert.match(errJoined, /github\.com\/dsj1984\/mandrel\/releases/);
228
+ // Must NOT be the old bare "not found … skipping" message.
229
+ assert.doesNotMatch(errJoined, /skipping changelog surface/);
230
+ });
231
+
232
+ it('never throws even when both the packaged file and GitHub fetch fail', async () => {
233
+ const fs = makeFs();
234
+ const cap = makeCapture();
235
+ const fetchChangelog = async () => {
236
+ throw new Error('network error');
237
+ };
238
+
239
+ // Must not reject.
240
+ await assert.doesNotReject(() =>
241
+ run([], makeDeps(fs, cap, fetchChangelog)),
242
+ );
243
+ });
244
+
245
+ it('emits an actionable message when the changelog has no matching section', async () => {
246
+ // Changelog only has a 1.57.0 entry — no 1.59.0 section.
247
+ const sparseChangelog = `# Changelog\n\n## [1.57.0](https://example.test) (2026-04-01)\n\n### Fixed\n\n* old fix\n`;
248
+ const fs = makeFs({ changelogContent: sparseChangelog });
249
+ const cap = makeCapture();
250
+
251
+ await run([], makeDeps(fs, cap));
252
+
253
+ const errJoined = cap.err.join('');
254
+ assert.match(errJoined, /v1\.59\.0/);
255
+ assert.match(errJoined, /github\.com\/dsj1984\/mandrel\/releases/);
256
+ assert.doesNotMatch(errJoined, /skipping changelog surface/);
257
+ });
258
+ });
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // fetchChangelogFromGitHub unit tests
262
+ // ---------------------------------------------------------------------------
263
+
264
+ /**
265
+ * Build a minimal fake `node:https` module. Each `response` entry defines
266
+ * `{ status, body }` for successive calls to `https.get`. When an entry has
267
+ * `error: true` the fake emits an 'error' event on the request object instead
268
+ * of responding.
269
+ *
270
+ * @param {Array<{ status?: number, body?: string, error?: boolean }>} responses
271
+ */
272
+ function makeHttpsFake(responses) {
273
+ let callIdx = 0;
274
+ const capturedUrls = [];
275
+ return {
276
+ capturedUrls,
277
+ https: {
278
+ get(url, callback) {
279
+ capturedUrls.push(url);
280
+ const entry = responses[callIdx] ?? { status: 404, body: '' };
281
+ callIdx += 1;
282
+
283
+ const req = new EventEmitter();
284
+
285
+ if (entry.error) {
286
+ // Emit 'error' asynchronously so the Promise constructor has time to
287
+ // attach the `.on('error')` listener.
288
+ setImmediate(() =>
289
+ req.emit('error', new Error('connection refused')),
290
+ );
291
+ } else {
292
+ const res = new EventEmitter();
293
+ res.statusCode = entry.status ?? 200;
294
+ setImmediate(() => {
295
+ callback(res);
296
+ res.emit('data', Buffer.from(entry.body ?? ''));
297
+ res.emit('end');
298
+ });
299
+ }
300
+
301
+ return req;
302
+ },
303
+ },
304
+ };
305
+ }
306
+
307
+ describe('fetchChangelogFromGitHub — HTTP seam (Story #4035)', () => {
308
+ it('returns the body from a 200 response on the namespaced tag', async () => {
309
+ const { https, capturedUrls } = makeHttpsFake([
310
+ { status: 200, body: '# Changelog\n\n## [1.59.0] content' },
311
+ ]);
312
+
313
+ const result = await fetchChangelogFromGitHub('1.59.0', { https });
314
+
315
+ assert.match(result, /\[1\.59\.0\] content/);
316
+ // Must have tried the namespaced tag first.
317
+ assert.ok(
318
+ capturedUrls[0].includes('mandrel-v1.59.0'),
319
+ 'namespaced tag tried first',
320
+ );
321
+ });
322
+
323
+ it('falls back to bare vX.Y.Z tag when the namespaced tag returns 404', async () => {
324
+ const { https, capturedUrls } = makeHttpsFake([
325
+ { status: 404, body: 'Not Found' }, // mandrel-v1.59.0 → 404
326
+ { status: 200, body: '# Changelog\n## [1.59.0] bare' }, // v1.59.0 → 200
327
+ ]);
328
+
329
+ const result = await fetchChangelogFromGitHub('1.59.0', { https });
330
+
331
+ assert.match(result, /\[1\.59\.0\] bare/);
332
+ assert.equal(capturedUrls.length, 2);
333
+ assert.ok(capturedUrls[0].includes('mandrel-v1.59.0'));
334
+ assert.ok(capturedUrls[1].includes('/v1.59.0/'));
335
+ });
336
+
337
+ it('throws when both tag forms return non-2xx', async () => {
338
+ const { https } = makeHttpsFake([
339
+ { status: 404, body: 'Not Found' },
340
+ { status: 404, body: 'Not Found' },
341
+ ]);
342
+
343
+ await assert.rejects(
344
+ () => fetchChangelogFromGitHub('1.59.0', { https }),
345
+ /non-2xx for all tag forms/,
346
+ );
347
+ });
348
+
349
+ it('throws when the HTTP request emits an error', async () => {
350
+ const { https } = makeHttpsFake([{ error: true }]);
351
+
352
+ await assert.rejects(
353
+ () => fetchChangelogFromGitHub('1.59.0', { https }),
354
+ /connection refused/,
355
+ );
356
+ });
357
+ });