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,513 @@
1
+ // lib/cli/__tests__/update-reexec.test.js
2
+ /**
3
+ * Unit tests for the Story #4034 re-exec fix in lib/cli/update.js.
4
+ *
5
+ * After `npm-update` lands the new version, the post-install phases
6
+ * (sync, migrate, doctor) MUST run from the newly-installed binary rather
7
+ * than in the still-running old process. The `spawnPhase` seam is the
8
+ * injectable boundary that makes this fully testable without a real npm
9
+ * install.
10
+ *
11
+ * Coverage contract (Story #4034):
12
+ * - When `spawnPhase` is injected, post-install phases run through it
13
+ * (re-exec path) instead of the in-process runSync/runMigrations/runDoctor.
14
+ * - Phases are spawned in the correct order: sync → migrate → doctor.
15
+ * - `mandrel migrate` receives `--from <current>` and `--to <target>`.
16
+ * - The new bin path passed to `spawnPhase` is resolved from the cwd's
17
+ * `node_modules/.bin/mandrel` (the production path).
18
+ * - A failing sync phase throws and exits non-zero (never silently continues).
19
+ * - A failing migrate phase throws and exits non-zero.
20
+ * - A non-zero doctor phase maps to `action: 'doctor-failed'` + exit 1.
21
+ * - `resolveNewBinPath` returns the correct platform path.
22
+ * - `defaultSpawnPhase` routes stdout/stderr through the write sinks and
23
+ * throws on spawn error.
24
+ *
25
+ * Tier: unit (testing-standards § Unit). All seams — including `spawnPhase`
26
+ * — are injected; no real child process, filesystem, or network call occurs.
27
+ *
28
+ * Security (security-baseline § 5 — Data Leakage & Logging): fixtures carry
29
+ * only version strings and paths; no tokens, credentials, or env values.
30
+ */
31
+
32
+ import assert from 'node:assert/strict';
33
+ import path from 'node:path';
34
+ import { describe, it } from 'node:test';
35
+
36
+ import { defaultSpawnPhase, resolveNewBinPath, runUpdate } from '../update.js';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /** Capture stdout/stderr writes and the exit code. */
43
+ function makeCapture() {
44
+ const out = [];
45
+ const err = [];
46
+ let exitCode = null;
47
+ return {
48
+ out,
49
+ err,
50
+ get exitCode() {
51
+ return exitCode;
52
+ },
53
+ write: (s) => out.push(s),
54
+ writeErr: (s) => err.push(s),
55
+ exit: (code) => {
56
+ exitCode = code;
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Build a minimal seam set for a minor-ahead non-dry-run run.
63
+ * `spawnPhase` replaces the in-process runSync/runMigrations/runDoctor.
64
+ */
65
+ function makeReExecSeams({
66
+ target = '1.44.0',
67
+ current = '1.43.0',
68
+ spawnResults = {},
69
+ } = {}) {
70
+ const calls = [];
71
+ return {
72
+ calls,
73
+ currentVersion: current,
74
+ resolveTargetVersion: async () => {
75
+ calls.push('resolve');
76
+ return target;
77
+ },
78
+ npmUpdate: async (version) => {
79
+ calls.push(`npm-update:${version}`);
80
+ },
81
+ spawnPhase: async (phase, args, opts) => {
82
+ calls.push({ phase, args, binPath: opts.binPath, cwd: opts.cwd });
83
+ const result = spawnResults[phase] ?? {
84
+ ok: true,
85
+ stdout: '',
86
+ stderr: '',
87
+ };
88
+ return result;
89
+ },
90
+ surfaceChangelog: async (version) => {
91
+ calls.push(`changelog:${version}`);
92
+ },
93
+ cwd: () => '/fake/consumer',
94
+ };
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // AC: spawnPhase is invoked for post-install phases when injected
99
+ // ---------------------------------------------------------------------------
100
+
101
+ describe('runUpdate — re-exec path via spawnPhase', () => {
102
+ it('routes post-install phases through spawnPhase, not in-process seams', async () => {
103
+ const cap = makeCapture();
104
+ const seams = makeReExecSeams({ target: '1.44.0', current: '1.43.0' });
105
+
106
+ const result = await runUpdate({
107
+ argv: [],
108
+ ...seams,
109
+ write: cap.write,
110
+ writeErr: cap.writeErr,
111
+ exit: cap.exit,
112
+ });
113
+
114
+ // npm-update ran, then all three phases were spawned via spawnPhase.
115
+ const phaseNames = seams.calls
116
+ .filter((c) => typeof c === 'object' && c.phase)
117
+ .map((c) => c.phase);
118
+ assert.deepEqual(phaseNames, ['sync', 'migrate', 'doctor']);
119
+
120
+ // The in-process runSync/runMigrations/runDoctor were NOT invoked directly
121
+ // (there are no 'runSync' / 'runMigrations' / 'runDoctor' string entries).
122
+ assert.ok(
123
+ !seams.calls.some(
124
+ (c) => c === 'runSync' || c === 'runMigrations' || c === 'runDoctor',
125
+ ),
126
+ 'in-process seams must NOT be called when spawnPhase is injected',
127
+ );
128
+
129
+ assert.equal(result.ok, true);
130
+ assert.equal(result.action, 'updated');
131
+ assert.deepEqual(result.stepsRun, [
132
+ 'npm-update',
133
+ 'runSync',
134
+ 'runMigrations',
135
+ 'doctor',
136
+ ]);
137
+ assert.equal(cap.exitCode, null);
138
+ });
139
+
140
+ it('passes correct ordered argv to each phase: sync→migrate(--from,--to)→doctor', async () => {
141
+ const cap = makeCapture();
142
+ const seams = makeReExecSeams({ target: '1.50.0', current: '1.43.0' });
143
+
144
+ await runUpdate({
145
+ argv: [],
146
+ ...seams,
147
+ write: cap.write,
148
+ writeErr: cap.writeErr,
149
+ exit: cap.exit,
150
+ });
151
+
152
+ const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
153
+ assert.equal(phases.length, 3);
154
+
155
+ assert.equal(phases[0].phase, 'sync');
156
+ assert.deepEqual(phases[0].args, []);
157
+
158
+ assert.equal(phases[1].phase, 'migrate');
159
+ assert.deepEqual(phases[1].args, ['--from', '1.43.0', '--to', '1.50.0']);
160
+
161
+ assert.equal(phases[2].phase, 'doctor');
162
+ assert.deepEqual(phases[2].args, []);
163
+ });
164
+
165
+ it('resolves new bin path from cwd/node_modules/.bin/mandrel', async () => {
166
+ const cap = makeCapture();
167
+ const seams = makeReExecSeams({ target: '1.44.0', current: '1.43.0' });
168
+
169
+ await runUpdate({
170
+ argv: [],
171
+ ...seams,
172
+ write: cap.write,
173
+ writeErr: cap.writeErr,
174
+ exit: cap.exit,
175
+ });
176
+
177
+ const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
178
+ const expectedBin = resolveNewBinPath('/fake/consumer');
179
+ for (const p of phases) {
180
+ assert.equal(
181
+ p.binPath,
182
+ expectedBin,
183
+ `phase '${p.phase}' binPath should resolve to new bin`,
184
+ );
185
+ assert.equal(p.cwd, '/fake/consumer');
186
+ }
187
+ });
188
+
189
+ it('throws when sync phase exits non-zero (never silently materialises stale payload)', async () => {
190
+ const cap = makeCapture();
191
+ const seams = makeReExecSeams({
192
+ target: '1.44.0',
193
+ current: '1.43.0',
194
+ spawnResults: { sync: { ok: false, stdout: '', stderr: 'sync failed' } },
195
+ });
196
+
197
+ await assert.rejects(
198
+ () =>
199
+ runUpdate({
200
+ argv: [],
201
+ ...seams,
202
+ write: cap.write,
203
+ writeErr: cap.writeErr,
204
+ exit: cap.exit,
205
+ }),
206
+ /mandrel sync.*new binary exited non-zero/,
207
+ );
208
+
209
+ // migrate and doctor must NOT have been called after sync failure.
210
+ const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
211
+ assert.deepEqual(
212
+ phases.map((p) => p.phase),
213
+ ['sync'],
214
+ );
215
+ });
216
+
217
+ it('throws when migrate phase exits non-zero', async () => {
218
+ const cap = makeCapture();
219
+ const seams = makeReExecSeams({
220
+ target: '1.44.0',
221
+ current: '1.43.0',
222
+ spawnResults: {
223
+ migrate: { ok: false, stdout: '', stderr: 'migrate failed' },
224
+ },
225
+ });
226
+
227
+ await assert.rejects(
228
+ () =>
229
+ runUpdate({
230
+ argv: [],
231
+ ...seams,
232
+ write: cap.write,
233
+ writeErr: cap.writeErr,
234
+ exit: cap.exit,
235
+ }),
236
+ /mandrel migrate.*new binary exited non-zero/,
237
+ );
238
+
239
+ // doctor must NOT have been called after migrate failure.
240
+ const phases = seams.calls.filter((c) => typeof c === 'object' && c.phase);
241
+ assert.deepEqual(
242
+ phases.map((p) => p.phase),
243
+ ['sync', 'migrate'],
244
+ );
245
+ });
246
+
247
+ it('maps non-zero doctor to doctor-failed + exit 1', async () => {
248
+ const cap = makeCapture();
249
+ const seams = makeReExecSeams({
250
+ target: '1.44.0',
251
+ current: '1.43.0',
252
+ spawnResults: {
253
+ doctor: {
254
+ ok: false,
255
+ stdout: '',
256
+ stderr: 'agents-drift: drift detected',
257
+ },
258
+ },
259
+ });
260
+
261
+ const result = await runUpdate({
262
+ argv: [],
263
+ ...seams,
264
+ write: cap.write,
265
+ writeErr: cap.writeErr,
266
+ exit: cap.exit,
267
+ });
268
+
269
+ assert.equal(result.ok, false);
270
+ assert.equal(result.action, 'doctor-failed');
271
+ assert.equal(cap.exitCode, 1);
272
+ assert.match(cap.err.join(''), /doctor reported failures/);
273
+ });
274
+
275
+ it('still runs the changelog seam after a successful re-exec cycle', async () => {
276
+ const cap = makeCapture();
277
+ const seams = makeReExecSeams({ target: '1.44.0', current: '1.43.0' });
278
+
279
+ await runUpdate({
280
+ argv: [],
281
+ ...seams,
282
+ write: cap.write,
283
+ writeErr: cap.writeErr,
284
+ exit: cap.exit,
285
+ });
286
+
287
+ assert.ok(
288
+ seams.calls.includes('changelog:1.44.0'),
289
+ 'changelog seam must fire after successful re-exec phases',
290
+ );
291
+ });
292
+ });
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // AC: in-process seams still work when spawnPhase is NOT injected
296
+ // (backward compatibility — pre-Story-#4034 tests must stay green)
297
+ // ---------------------------------------------------------------------------
298
+
299
+ describe('runUpdate — in-process backward-compat path (no spawnPhase)', () => {
300
+ it('uses injected runSync/runMigrations/runDoctor when spawnPhase is absent', async () => {
301
+ const calls = [];
302
+ const cap = makeCapture();
303
+
304
+ const result = await runUpdate({
305
+ argv: [],
306
+ currentVersion: '1.43.0',
307
+ resolveTargetVersion: async () => '1.44.0',
308
+ npmUpdate: async (v) => {
309
+ calls.push(`npm-update:${v}`);
310
+ },
311
+ runSync: (_opts) => {
312
+ calls.push('runSync');
313
+ return {};
314
+ },
315
+ runMigrations: ({ fromVersion, toVersion }) => {
316
+ calls.push(`runMigrations:${fromVersion}->${toVersion}`);
317
+ return { applied: [], skipped: [] };
318
+ },
319
+ runDoctor: async () => {
320
+ calls.push('runDoctor');
321
+ return { ok: true, results: [] };
322
+ },
323
+ surfaceChangelog: async (v) => {
324
+ calls.push(`changelog:${v}`);
325
+ },
326
+ write: cap.write,
327
+ writeErr: cap.writeErr,
328
+ exit: cap.exit,
329
+ });
330
+
331
+ assert.deepEqual(calls, [
332
+ 'npm-update:1.44.0',
333
+ 'runSync',
334
+ 'runMigrations:1.43.0->1.44.0',
335
+ 'runDoctor',
336
+ 'changelog:1.44.0',
337
+ ]);
338
+ assert.equal(result.ok, true);
339
+ assert.equal(result.action, 'updated');
340
+ assert.equal(cap.exitCode, null);
341
+ });
342
+ });
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // AC: resolveNewBinPath returns the correct platform path
346
+ // ---------------------------------------------------------------------------
347
+
348
+ describe('resolveNewBinPath', () => {
349
+ it('returns node_modules/.bin/mandrel on POSIX', () => {
350
+ // We cannot force-test win32 on non-win32 but we can verify the POSIX shape.
351
+ if (process.platform !== 'win32') {
352
+ const p = resolveNewBinPath('/some/project');
353
+ assert.equal(
354
+ p,
355
+ path.join('/some', 'project', 'node_modules', '.bin', 'mandrel'),
356
+ );
357
+ }
358
+ });
359
+
360
+ it('returns a path that ends with mandrel or mandrel.cmd', () => {
361
+ const p = resolveNewBinPath('/project');
362
+ assert.ok(
363
+ p.endsWith('mandrel') || p.endsWith('mandrel.cmd'),
364
+ `expected path to end with mandrel[.cmd], got: ${p}`,
365
+ );
366
+ });
367
+
368
+ it('contains node_modules/.bin in the path', () => {
369
+ const p = resolveNewBinPath('/project');
370
+ assert.ok(
371
+ p.includes(path.join('node_modules', '.bin')),
372
+ `expected node_modules/.bin in path, got: ${p}`,
373
+ );
374
+ });
375
+ });
376
+
377
+ // ---------------------------------------------------------------------------
378
+ // AC: defaultSpawnPhase routes output through write sinks and throws on error
379
+ // ---------------------------------------------------------------------------
380
+
381
+ describe('defaultSpawnPhase — output routing and error handling', () => {
382
+ it('routes child stdout through the write sink', () => {
383
+ const out = [];
384
+ const err = [];
385
+ const fakeSpawn = (_bin, _args, _opts) => ({
386
+ status: 0,
387
+ stdout: 'Materialized 832 files\n',
388
+ stderr: '',
389
+ error: undefined,
390
+ });
391
+
392
+ defaultSpawnPhase('sync', [], {
393
+ binPath: '/fake/bin/mandrel',
394
+ cwd: '/project',
395
+ write: (s) => out.push(s),
396
+ writeErr: (s) => err.push(s),
397
+ spawnFn: fakeSpawn,
398
+ });
399
+
400
+ assert.deepEqual(out, ['Materialized 832 files\n']);
401
+ assert.deepEqual(err, []);
402
+ });
403
+
404
+ it('routes child stderr through the writeErr sink', () => {
405
+ const out = [];
406
+ const err = [];
407
+ const fakeSpawn = () => ({
408
+ status: 0,
409
+ stdout: '',
410
+ stderr: 'warning: something\n',
411
+ error: undefined,
412
+ });
413
+
414
+ defaultSpawnPhase('doctor', [], {
415
+ binPath: '/fake/bin/mandrel',
416
+ cwd: '/project',
417
+ write: (s) => out.push(s),
418
+ writeErr: (s) => err.push(s),
419
+ spawnFn: fakeSpawn,
420
+ });
421
+
422
+ assert.deepEqual(out, []);
423
+ assert.deepEqual(err, ['warning: something\n']);
424
+ });
425
+
426
+ it('returns ok:false when child exits non-zero', () => {
427
+ const fakeSpawn = () => ({
428
+ status: 1,
429
+ stdout: '',
430
+ stderr: 'drift detected',
431
+ error: undefined,
432
+ });
433
+
434
+ const result = defaultSpawnPhase('doctor', [], {
435
+ binPath: '/fake/bin/mandrel',
436
+ cwd: '/project',
437
+ write: () => {},
438
+ writeErr: () => {},
439
+ spawnFn: fakeSpawn,
440
+ });
441
+
442
+ assert.equal(result.ok, false);
443
+ });
444
+
445
+ it('returns ok:true when child exits 0', () => {
446
+ const fakeSpawn = () => ({
447
+ status: 0,
448
+ stdout: 'all good',
449
+ stderr: '',
450
+ error: undefined,
451
+ });
452
+
453
+ const result = defaultSpawnPhase('sync', [], {
454
+ binPath: '/fake/bin/mandrel',
455
+ cwd: '/project',
456
+ write: () => {},
457
+ writeErr: () => {},
458
+ spawnFn: fakeSpawn,
459
+ });
460
+
461
+ assert.equal(result.ok, true);
462
+ });
463
+
464
+ it('throws a descriptive error when the spawn itself fails (ENOENT)', () => {
465
+ const fakeSpawn = () => ({
466
+ status: null,
467
+ stdout: '',
468
+ stderr: '',
469
+ error: new Error('spawnSync mandrel ENOENT'),
470
+ });
471
+
472
+ assert.throws(
473
+ () =>
474
+ defaultSpawnPhase('sync', [], {
475
+ binPath: '/fake/bin/mandrel',
476
+ cwd: '/project',
477
+ write: () => {},
478
+ writeErr: () => {},
479
+ spawnFn: fakeSpawn,
480
+ }),
481
+ /failed to spawn.*mandrel sync.*new binary.*ENOENT/,
482
+ );
483
+ });
484
+
485
+ it('passes the correct argv vector to the spawn function', () => {
486
+ const calls = [];
487
+ const fakeSpawn = (bin, args, opts) => {
488
+ calls.push({ bin, args, opts });
489
+ return { status: 0, stdout: '', stderr: '', error: undefined };
490
+ };
491
+
492
+ defaultSpawnPhase('migrate', ['--from', '1.43.0', '--to', '1.44.0'], {
493
+ binPath: '/nm/.bin/mandrel',
494
+ cwd: '/proj',
495
+ write: () => {},
496
+ writeErr: () => {},
497
+ spawnFn: fakeSpawn,
498
+ });
499
+
500
+ assert.equal(calls.length, 1);
501
+ assert.equal(calls[0].bin, '/nm/.bin/mandrel');
502
+ assert.deepEqual(calls[0].args, [
503
+ 'migrate',
504
+ '--from',
505
+ '1.43.0',
506
+ '--to',
507
+ '1.44.0',
508
+ ]);
509
+ assert.equal(calls[0].opts.cwd, '/proj');
510
+ // Shell flag is win32-gated: true only on Windows.
511
+ assert.equal(calls[0].opts.shell, process.platform === 'win32');
512
+ });
513
+ });
package/lib/cli/init.js CHANGED
@@ -20,13 +20,15 @@
20
20
  * `init` goes straight to the prompt — the one subcommand is idempotent
21
21
  * across both the cold-start and post-install entry points.
22
22
  *
23
- * 2. **Two-option prompt.** Ask whether to configure now (option 1 → run
24
- * `node .agents/scripts/bootstrap.js`, forwarding every passthrough flag
25
- * unchanged) or stop at "just the files" (option 2 → print a re-run hint
26
- * and exit 0). `--assume-yes` skips the prompt and configures (the flag is
27
- * also forwarded to bootstrap for its own non-interactive run). A non-TTY
28
- * stdin without `--assume-yes` defaults to option 2 (files-only) so the
29
- * side-effecting GitHub provisioning never runs unattended.
23
+ * 2. **Yes/no prompt.** Ask whether to begin the interactive setup now
24
+ * (yes → run `node .agents/scripts/bootstrap.js`, forwarding every
25
+ * passthrough flag unchanged) or stop at "just the files" (no → print a
26
+ * re-run hint and exit 0). Yes is the default, so a bare Enter configures
27
+ * (mirrors the `[Y/n]` convention in bootstrap.js). `--assume-yes` skips
28
+ * the prompt and configures (the flag is also forwarded to bootstrap for
29
+ * its own non-interactive run). A non-TTY stdin without `--assume-yes`
30
+ * defaults to no (files-only) so the side-effecting GitHub provisioning
31
+ * never runs unattended.
30
32
  *
31
33
  * ## Cold-start provenance
32
34
  *
@@ -48,8 +50,9 @@
48
50
  * for `./.agents/` in the cwd
49
51
  * - `runStep` — `(cmd, args) => { status }`; runs one install/sync/bootstrap
50
52
  * step. Defaults to a `spawnSync` runner with `stdio: inherit`.
51
- * - `confirm` — `() => '1' | '2'`; reads the operator's numbered choice.
52
- * Defaults to a synchronous stdin readline prompt.
53
+ * - `confirm` — `() => boolean`; reads the operator's yes/no answer (true =
54
+ * configure now). Defaults to a synchronous stdin readline
55
+ * prompt with yes as the default.
53
56
  * - `stdout` — `(s) => void`; defaults to `process.stdout.write`.
54
57
  * - `isTTY` — boolean; defaults to `process.stdin.isTTY`.
55
58
  * - `exit` — `(code) => void`; defaults to `process.exit`.
@@ -92,12 +95,11 @@ const BOOTSTRAP_SCRIPT = path.join('.agents', 'scripts', 'bootstrap.js');
92
95
  const SYNC_BIN = path.join('node_modules', PACKAGE_NAME, 'bin', 'mandrel.js');
93
96
 
94
97
  const PROMPT_TEXT =
95
- 'Mandrel is installed. What next?\n' +
96
- ' 1) Configure my environment now (creates the GitHub repo + Projects board)\n' +
97
- " 2) Just the files — I'll configure later\n" +
98
- 'Choice [1/2]: ';
98
+ 'The Mandrel .agents package has been copied to your directory.\n' +
99
+ 'Would you like to begin the interactive process to setup your local and ' +
100
+ 'github environments now? [Y/n]: ';
99
101
 
100
- const FILES_ONLY_HINT = 'Configure any time with: mandrel init\n';
102
+ const FILES_ONLY_HINT = 'Configure any time with: npx mandrel init\n';
101
103
 
102
104
  // On win32, `npm` resolves to a `.cmd` shim that Node refuses to spawn without
103
105
  // a shell after the CVE-2024-27980 hardening; mirror update.js and set
@@ -149,7 +151,7 @@ function buildBootstrapArgs(argv, assumeYes) {
149
151
  * argv?: string[],
150
152
  * exists?: (relPath: string) => boolean,
151
153
  * runStep?: (cmd: string, args: string[]) => { status: number | null },
152
- * confirm?: () => '1' | '2',
154
+ * confirm?: () => boolean,
153
155
  * stdout?: (s: string) => void,
154
156
  * isTTY?: boolean,
155
157
  * }} [opts]
@@ -224,18 +226,18 @@ export function planInit({
224
226
  // Decide the outcome: configure (run bootstrap) vs. files-only.
225
227
  // - `--assume-yes` → configure, prompt skipped entirely.
226
228
  // - non-TTY without `--assume-yes` → files-only (never provision unattended).
227
- // - TTY → consult the confirm seam for the numbered choice.
228
- let choice;
229
+ // - TTY → consult the confirm seam for the yes/no answer (yes = configure).
230
+ let proceed;
229
231
  if (assumeYes) {
230
- choice = '1';
232
+ proceed = true;
231
233
  } else if (!isTTY) {
232
- choice = '2';
234
+ proceed = false;
233
235
  } else {
234
236
  stdout(PROMPT_TEXT);
235
- choice = confirm();
237
+ proceed = confirm();
236
238
  }
237
239
 
238
- if (choice === '1') {
240
+ if (proceed) {
239
241
  const bootstrapArgs = buildBootstrapArgs(argv, assumeYes);
240
242
  const bootstrapStatus = step(process.execPath, [
241
243
  BOOTSTRAP_SCRIPT,
@@ -249,7 +251,7 @@ export function planInit({
249
251
  };
250
252
  }
251
253
 
252
- // Option 2 (files-only): print the re-run hint and exit cleanly.
254
+ // Declined (files-only): print the re-run hint and exit cleanly.
253
255
  stdout(FILES_ONLY_HINT);
254
256
  return { installed, ranBootstrap: false, steps, exitCode: 0 };
255
257
  }
@@ -284,23 +286,23 @@ function defaultRunStep(cmd, args) {
284
286
  }
285
287
 
286
288
  /**
287
- * Default `confirm` seam — synchronous numbered-choice prompt. Reads one line
288
- * from stdin and normalizes it to `'1'` or `'2'`; any input other than `'2'`
289
- * (including bare Enter) defaults to `'1'` (configure), matching the
290
- * `Choice [1/2]:` convention where the first option is the default.
289
+ * Default `confirm` seam — synchronous yes/no prompt. Reads one line from stdin
290
+ * and normalizes it to a boolean; any input other than an explicit "no"
291
+ * (`n`/`no`, case-insensitive) — including bare Enter defaults to `true`
292
+ * (configure), matching the `[Y/n]` convention where yes is the default.
291
293
  *
292
- * @returns {'1' | '2'}
294
+ * @returns {boolean}
293
295
  */
294
296
  function defaultConfirm() {
295
297
  let answer = '';
296
298
  try {
297
299
  const buf = fs.readFileSync(0, 'utf8');
298
- answer = buf.split('\n', 1)[0].trim();
300
+ answer = buf.split('\n', 1)[0].trim().toLowerCase();
299
301
  } catch {
300
302
  // No readable line (e.g. stdin closed) → fall through to the default.
301
303
  answer = '';
302
304
  }
303
- return answer === '2' ? '2' : '1';
305
+ return answer !== 'n' && answer !== 'no';
304
306
  }
305
307
 
306
308
  /**