mandrel 1.59.0 → 1.61.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 (267) hide show
  1. package/.agents/README.md +86 -44
  2. package/.agents/docs/SDLC.md +135 -141
  3. package/.agents/docs/configuration.md +77 -20
  4. package/.agents/docs/quality-gates.md +796 -0
  5. package/.agents/docs/workflows.md +6 -9
  6. package/.agents/instructions.md +12 -11
  7. package/.agents/personas/architect.md +1 -1
  8. package/.agents/personas/product.md +1 -1
  9. package/.agents/personas/project-manager.md +14 -14
  10. package/.agents/personas/technical-writer.md +1 -1
  11. package/.agents/rules/changelog-style.md +5 -5
  12. package/.agents/rules/git-conventions.md +3 -3
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +3 -3
  15. package/.agents/schemas/dispatch-manifest.json +4 -4
  16. package/.agents/schemas/epic-spec.schema.json +15 -45
  17. package/.agents/schemas/lifecycle/README.md +1 -1
  18. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
  20. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
  21. package/.agents/schemas/validation-evidence.schema.json +1 -1
  22. package/.agents/scripts/README.md +2 -2
  23. package/.agents/scripts/acceptance-eval.js +1 -1
  24. package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
  25. package/.agents/scripts/agents-bootstrap-github.js +23 -119
  26. package/.agents/scripts/analyze-execution.js +2 -2
  27. package/.agents/scripts/audit-to-stories.js +1 -1
  28. package/.agents/scripts/check-doc-links.js +2 -3
  29. package/.agents/scripts/diagnose-friction.js +1 -1
  30. package/.agents/scripts/dispatcher.js +2 -2
  31. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  32. package/.agents/scripts/epic-audit-prepare.js +3 -3
  33. package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
  34. package/.agents/scripts/epic-deliver-preflight.js +6 -6
  35. package/.agents/scripts/epic-deliver-prepare.js +1 -1
  36. package/.agents/scripts/epic-execute-record-wave.js +4 -4
  37. package/.agents/scripts/epic-plan-healthcheck.js +6 -10
  38. package/.agents/scripts/epic-plan-spec-validate.js +1 -1
  39. package/.agents/scripts/epic-reconcile.js +11 -29
  40. package/.agents/scripts/evidence-gate.js +1 -1
  41. package/.agents/scripts/generate-workflows-doc.js +1 -1
  42. package/.agents/scripts/hierarchy-gate.js +7 -11
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -1
  44. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  45. package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
  46. package/.agents/scripts/lib/baseline-snapshot.js +7 -7
  47. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  48. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  49. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
  50. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  51. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +47 -1
  52. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  53. package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
  54. package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
  55. package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
  56. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
  57. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
  65. package/.agents/scripts/lib/config-settings-schema.js +3 -3
  66. package/.agents/scripts/lib/detect-package-manager.js +72 -0
  67. package/.agents/scripts/lib/duplicate-search.js +1 -1
  68. package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
  69. package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
  70. package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
  71. package/.agents/scripts/lib/errors/index.js +4 -4
  72. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
  73. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  74. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  75. package/.agents/scripts/lib/findings/promote-finding.js +10 -10
  76. package/.agents/scripts/lib/label-constants.js +3 -4
  77. package/.agents/scripts/lib/label-taxonomy.js +5 -10
  78. package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
  79. package/.agents/scripts/lib/onboard/init-tail.js +218 -0
  80. package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
  81. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  82. package/.agents/scripts/lib/orchestration/code-review.js +5 -5
  83. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
  84. package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
  85. package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
  86. package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
  87. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
  88. package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
  89. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
  90. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
  91. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
  92. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
  93. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
  94. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
  95. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
  96. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
  97. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
  98. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
  99. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
  100. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
  101. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
  102. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
  103. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
  104. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
  105. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
  106. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
  107. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
  108. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
  109. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
  110. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
  111. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
  112. package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
  113. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
  114. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
  115. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
  116. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
  117. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
  118. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
  119. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
  120. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
  121. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
  122. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
  123. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
  124. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  125. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
  126. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
  127. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
  128. package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
  129. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
  130. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
  131. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
  132. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  133. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
  134. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
  135. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
  136. package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
  137. package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
  139. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  140. package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
  141. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  142. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
  143. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
  144. package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
  145. package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
  146. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
  147. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
  148. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
  149. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
  150. package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
  151. package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
  152. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
  153. package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
  154. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  155. package/.agents/scripts/lib/preflight-runner.js +1 -1
  156. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
  157. package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
  158. package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
  159. package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
  160. package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
  161. package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
  162. package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
  163. package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
  164. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
  165. package/.agents/scripts/lib/signals/schema.js +1 -1
  166. package/.agents/scripts/lib/spec/index.js +1 -1
  167. package/.agents/scripts/lib/spec/loader.js +2 -2
  168. package/.agents/scripts/lib/spec/state.js +7 -16
  169. package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
  170. package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
  171. package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
  172. package/.agents/scripts/lib/story-lifecycle.js +8 -8
  173. package/.agents/scripts/lib/story-plan.js +1 -1
  174. package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
  175. package/.agents/scripts/lib/wave-runner/tick.js +1 -1
  176. package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
  177. package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
  178. package/.agents/scripts/lifecycle-emit.js +1 -1
  179. package/.agents/scripts/providers/github/board-add.js +1 -1
  180. package/.agents/scripts/providers/github/errors.js +1 -1
  181. package/.agents/scripts/providers/github/mappers.js +2 -2
  182. package/.agents/scripts/providers/github/tickets.js +4 -4
  183. package/.agents/scripts/resync-status-column.js +1 -1
  184. package/.agents/scripts/retro-run.js +2 -2
  185. package/.agents/scripts/run-lint.js +1 -1
  186. package/.agents/scripts/single-story-init.js +1 -1
  187. package/.agents/scripts/stories-wave-tick.js +5 -5
  188. package/.agents/scripts/story-close.js +1 -1
  189. package/.agents/scripts/story-init.js +13 -16
  190. package/.agents/scripts/story-phase.js +5 -5
  191. package/.agents/scripts/story-plan.js +3 -3
  192. package/.agents/scripts/sync-branch-from-base.js +1 -1
  193. package/.agents/scripts/validate-docs-freshness.js +1 -1
  194. package/.agents/scripts/wave-tick.js +1 -1
  195. package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
  196. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
  197. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
  198. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
  199. package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
  200. package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
  201. package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
  202. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
  203. package/.agents/skills/core/scope-triage/SKILL.md +9 -10
  204. package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
  205. package/.agents/skills/skills.index.json +7 -7
  206. package/.agents/templates/agent-protocol.md +2 -2
  207. package/.agents/workflows/agents-update.md +16 -31
  208. package/.agents/workflows/audit-architecture.md +2 -2
  209. package/.agents/workflows/audit-clean-code.md +2 -2
  210. package/.agents/workflows/audit-dependencies.md +1 -1
  211. package/.agents/workflows/audit-devops.md +1 -1
  212. package/.agents/workflows/audit-documentation.md +2 -2
  213. package/.agents/workflows/audit-lighthouse.md +1 -1
  214. package/.agents/workflows/audit-performance.md +2 -2
  215. package/.agents/workflows/audit-privacy.md +1 -1
  216. package/.agents/workflows/audit-quality.md +2 -2
  217. package/.agents/workflows/audit-security.md +2 -2
  218. package/.agents/workflows/audit-seo.md +1 -1
  219. package/.agents/workflows/audit-sre.md +1 -1
  220. package/.agents/workflows/audit-to-stories.md +10 -10
  221. package/.agents/workflows/audit-ux-ui.md +1 -1
  222. package/.agents/workflows/deliver.md +85 -0
  223. package/.agents/workflows/explain.md +3 -3
  224. package/.agents/workflows/git-merge-pr.md +1 -1
  225. package/.agents/workflows/git-pr-all.md +13 -10
  226. package/.agents/workflows/git-push.md +6 -3
  227. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  228. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  229. package/.agents/workflows/helpers/agents-sync-config.md +3 -2
  230. package/.agents/workflows/helpers/code-review.md +5 -5
  231. package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
  232. package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
  233. package/.agents/workflows/helpers/diagnose.md +1 -1
  234. package/.agents/workflows/helpers/epic-audit.md +6 -6
  235. package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
  236. package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
  237. package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
  238. package/.agents/workflows/helpers/epic-testing.md +3 -3
  239. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  240. package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
  241. package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
  242. package/.agents/workflows/helpers/signals.md +1 -1
  243. package/.agents/workflows/helpers/single-story-deliver.md +11 -11
  244. package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
  245. package/.agents/workflows/plan.md +131 -0
  246. package/.agents/workflows/qa-explore.md +1 -1
  247. package/.agents/workflows/qa-run-harness.md +1 -1
  248. package/README.md +19 -39
  249. package/bin/mandrel.js +235 -16
  250. package/docs/CHANGELOG.md +1173 -0
  251. package/lib/cli/doctor.js +45 -3
  252. package/lib/cli/init.js +97 -36
  253. package/lib/cli/registry.js +41 -145
  254. package/lib/cli/sync.js +122 -23
  255. package/lib/cli/uninstall.js +42 -7
  256. package/lib/cli/update.js +524 -210
  257. package/lib/cli/version-helpers.js +59 -0
  258. package/package.json +7 -6
  259. package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
  260. package/.agents/workflows/onboard.md +0 -208
  261. package/lib/cli/__tests__/migrate.test.js +0 -268
  262. package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
  263. package/lib/cli/__tests__/sync.test.js +0 -372
  264. package/lib/cli/__tests__/update-major.test.js +0 -217
  265. package/lib/cli/__tests__/update.test.js +0 -696
  266. package/lib/cli/__tests__/version-check.test.js +0 -398
  267. package/lib/migrations/__tests__/index.test.js +0 -216
package/lib/cli/doctor.js CHANGED
@@ -17,11 +17,15 @@
17
17
  * Exit code 0 = all pass, 1 = any fail.
18
18
  *
19
19
  * Injectable seams (used by tests):
20
- * - `checks` — replaces the default registry array
21
- * - `write` — replaces process.stdout.write
22
- * - `exit` — replaces process.exit
20
+ * - `checks` — replaces the default registry array
21
+ * - `write` — replaces process.stdout.write
22
+ * - `exit` — replaces process.exit
23
+ * - `writeResultCache` — replaces the temp/doctor-result.json writer
23
24
  */
24
25
 
26
+ import nodeFs from 'node:fs';
27
+ import path from 'node:path';
28
+
25
29
  import { registry } from './registry.js';
26
30
 
27
31
  // ---------------------------------------------------------------------------
@@ -74,6 +78,40 @@ function formatSummary(passed, total) {
74
78
  return `❌ Not ready (${failed}/${total} checks failed)\n`;
75
79
  }
76
80
 
81
+ // ---------------------------------------------------------------------------
82
+ // Result cache
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /**
86
+ * Best-effort write of the doctor verdict to `temp/doctor-result.json` under
87
+ * the consumer root so downstream workflows (e.g. the `/plan` first-run
88
+ * preflight) can read the last recorded verdict without re-running doctor.
89
+ * `temp/` is the gitignored scratch root, so neither git nor the sync prune
90
+ * pass ever sees the cache. Any write failure is swallowed — the cache is
91
+ * advisory, never a gate.
92
+ *
93
+ * Exported for testing.
94
+ *
95
+ * @param {'ready'|'unready'} verdict
96
+ * @param {{ fs?: typeof nodeFs, cwd?: () => string }} [opts]
97
+ * @returns {void}
98
+ */
99
+ export function writeDoctorResultCache(
100
+ verdict,
101
+ { fs = nodeFs, cwd = process.cwd } = {},
102
+ ) {
103
+ try {
104
+ const dir = path.join(cwd(), 'temp');
105
+ fs.mkdirSync(dir, { recursive: true });
106
+ fs.writeFileSync(
107
+ path.join(dir, 'doctor-result.json'),
108
+ `${JSON.stringify({ verdict, checkedAt: new Date().toISOString() }, null, 2)}\n`,
109
+ );
110
+ } catch {
111
+ // Best-effort only — an unwritable temp/ must never fail the doctor run.
112
+ }
113
+ }
114
+
77
115
  // ---------------------------------------------------------------------------
78
116
  // Doctor runner (exported for testing)
79
117
  // ---------------------------------------------------------------------------
@@ -85,6 +123,7 @@ function formatSummary(passed, total) {
85
123
  * checks?: Array<{ name: string, run(opts?: unknown): { ok: boolean, detail: string, remedy?: string } }>,
86
124
  * write?: (s: string) => void,
87
125
  * exit?: (code: number) => void,
126
+ * writeResultCache?: (verdict: 'ready'|'unready') => void,
88
127
  * }} [opts]
89
128
  * @returns {void}
90
129
  */
@@ -92,6 +131,7 @@ export async function runDoctor({
92
131
  checks = registry,
93
132
  write = (s) => process.stdout.write(s),
94
133
  exit = (code) => process.exit(code),
134
+ writeResultCache = writeDoctorResultCache,
95
135
  } = {}) {
96
136
  let passed = 0;
97
137
 
@@ -104,6 +144,8 @@ export async function runDoctor({
104
144
  const total = checks.length;
105
145
  write(formatSummary(passed, total));
106
146
 
147
+ writeResultCache(passed === total ? 'ready' : 'unready');
148
+
107
149
  if (passed < total) {
108
150
  exit(1);
109
151
  }
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`.
@@ -63,6 +66,30 @@
63
66
  import { spawnSync } from 'node:child_process';
64
67
  import fs from 'node:fs';
65
68
  import path from 'node:path';
69
+ import { pathToFileURL } from 'node:url';
70
+
71
+ // Lazily resolved at runtime so cold-start `npx mandrel init` (where
72
+ // `.agents/` is absent) does not try to import from a directory that does not
73
+ // exist yet. The import is performed after bootstrap succeeds and `.agents/`
74
+ // is guaranteed to be materialised.
75
+ let _runInitTail = null;
76
+ async function getRunInitTail(projectRoot) {
77
+ if (_runInitTail) return _runInitTail;
78
+ const tailPath = path.join(
79
+ projectRoot,
80
+ '.agents',
81
+ 'scripts',
82
+ 'lib',
83
+ 'onboard',
84
+ 'init-tail.js',
85
+ );
86
+ // pathToFileURL handles Windows drive letters correctly (a raw
87
+ // `file://C:\…` template treats the drive letter as a URL host — same
88
+ // fix as commit 2e3d210b in lib/transpile.js).
89
+ const mod = await import(pathToFileURL(tailPath).href);
90
+ _runInitTail = mod.runInitTail;
91
+ return _runInitTail;
92
+ }
66
93
 
67
94
  /**
68
95
  * Hardcoded build-time package name. NEVER read from argv or env — cold-start
@@ -92,12 +119,11 @@ const BOOTSTRAP_SCRIPT = path.join('.agents', 'scripts', 'bootstrap.js');
92
119
  const SYNC_BIN = path.join('node_modules', PACKAGE_NAME, 'bin', 'mandrel.js');
93
120
 
94
121
  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]: ';
122
+ 'The Mandrel .agents package has been copied to your directory.\n' +
123
+ 'Would you like to begin the interactive process to setup your local and ' +
124
+ 'github environments now? [Y/n]: ';
99
125
 
100
- const FILES_ONLY_HINT = 'Configure any time with: mandrel init\n';
126
+ const FILES_ONLY_HINT = 'Configure any time with: npx mandrel init\n';
101
127
 
102
128
  // On win32, `npm` resolves to a `.cmd` shim that Node refuses to spawn without
103
129
  // a shell after the CVE-2024-27980 hardening; mirror update.js and set
@@ -149,24 +175,26 @@ function buildBootstrapArgs(argv, assumeYes) {
149
175
  * argv?: string[],
150
176
  * exists?: (relPath: string) => boolean,
151
177
  * runStep?: (cmd: string, args: string[]) => { status: number | null },
152
- * confirm?: () => '1' | '2',
178
+ * confirm?: () => boolean,
153
179
  * stdout?: (s: string) => void,
154
180
  * isTTY?: boolean,
181
+ * afterBootstrap?: (root: string) => Promise<{ ok?: boolean } | void> | { ok?: boolean } | void,
155
182
  * }} [opts]
156
- * @returns {{
183
+ * @returns {Promise<{
157
184
  * installed: boolean,
158
185
  * ranBootstrap: boolean,
159
186
  * steps: Array<{ cmd: string, args: string[] }>,
160
187
  * exitCode: number,
161
- * }}
188
+ * }>}
162
189
  */
163
- export function planInit({
190
+ export async function planInit({
164
191
  argv = [],
165
192
  exists,
166
193
  runStep,
167
194
  confirm,
168
195
  stdout = (s) => process.stdout.write(s),
169
196
  isTTY,
197
+ afterBootstrap,
170
198
  } = {}) {
171
199
  const steps = [];
172
200
 
@@ -224,32 +252,58 @@ export function planInit({
224
252
  // Decide the outcome: configure (run bootstrap) vs. files-only.
225
253
  // - `--assume-yes` → configure, prompt skipped entirely.
226
254
  // - non-TTY without `--assume-yes` → files-only (never provision unattended).
227
- // - TTY → consult the confirm seam for the numbered choice.
228
- let choice;
255
+ // - TTY → consult the confirm seam for the yes/no answer (yes = configure).
256
+ let proceed;
229
257
  if (assumeYes) {
230
- choice = '1';
258
+ proceed = true;
231
259
  } else if (!isTTY) {
232
- choice = '2';
260
+ proceed = false;
233
261
  } else {
234
262
  stdout(PROMPT_TEXT);
235
- choice = confirm();
263
+ proceed = confirm();
236
264
  }
237
265
 
238
- if (choice === '1') {
266
+ if (proceed) {
239
267
  const bootstrapArgs = buildBootstrapArgs(argv, assumeYes);
240
268
  const bootstrapStatus = step(process.execPath, [
241
269
  BOOTSTRAP_SCRIPT,
242
270
  ...bootstrapArgs,
243
271
  ]);
272
+ if (bootstrapStatus !== 0) {
273
+ return {
274
+ installed,
275
+ ranBootstrap: true,
276
+ steps,
277
+ exitCode: bootstrapStatus,
278
+ };
279
+ }
280
+
281
+ // Bootstrap succeeded — run the onboarding tail: stack detection, docs
282
+ // scaffolding offer, doctor gate, and /plan handoff. A tail that reports
283
+ // `ok: false` (the doctor gate failed) makes the whole init exit
284
+ // non-zero; the tail already printed its own remediation message, and the
285
+ // earlier install/sync/bootstrap phases' results stand as completed.
286
+ if (afterBootstrap) {
287
+ const tail = await afterBootstrap(process.cwd());
288
+ if (tail && tail.ok === false) {
289
+ return {
290
+ installed,
291
+ ranBootstrap: true,
292
+ steps,
293
+ exitCode: 1,
294
+ };
295
+ }
296
+ }
297
+
244
298
  return {
245
299
  installed,
246
300
  ranBootstrap: true,
247
301
  steps,
248
- exitCode: bootstrapStatus,
302
+ exitCode: 0,
249
303
  };
250
304
  }
251
305
 
252
- // Option 2 (files-only): print the re-run hint and exit cleanly.
306
+ // Declined (files-only): print the re-run hint and exit cleanly.
253
307
  stdout(FILES_ONLY_HINT);
254
308
  return { installed, ranBootstrap: false, steps, exitCode: 0 };
255
309
  }
@@ -284,32 +338,32 @@ function defaultRunStep(cmd, args) {
284
338
  }
285
339
 
286
340
  /**
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.
341
+ * Default `confirm` seam — synchronous yes/no prompt. Reads one line from stdin
342
+ * and normalizes it to a boolean; any input other than an explicit "no"
343
+ * (`n`/`no`, case-insensitive) — including bare Enter defaults to `true`
344
+ * (configure), matching the `[Y/n]` convention where yes is the default.
291
345
  *
292
- * @returns {'1' | '2'}
346
+ * @returns {boolean}
293
347
  */
294
348
  function defaultConfirm() {
295
349
  let answer = '';
296
350
  try {
297
351
  const buf = fs.readFileSync(0, 'utf8');
298
- answer = buf.split('\n', 1)[0].trim();
352
+ answer = buf.split('\n', 1)[0].trim().toLowerCase();
299
353
  } catch {
300
354
  // No readable line (e.g. stdin closed) → fall through to the default.
301
355
  answer = '';
302
356
  }
303
- return answer === '2' ? '2' : '1';
357
+ return answer !== 'n' && answer !== 'no';
304
358
  }
305
359
 
306
360
  /**
307
361
  * Default export consumed by `bin/mandrel.js`.
308
362
  *
309
363
  * @param {string[]} [argv] - Subcommand arguments (after `mandrel init`).
310
- * @returns {void}
364
+ * @returns {Promise<void>}
311
365
  */
312
- export default function run(argv = []) {
366
+ export default async function run(argv = []) {
313
367
  if (argv.includes('--help') || argv.includes('-h')) {
314
368
  process.stdout.write(
315
369
  'Usage: mandrel init [bootstrap flags]\n\n' +
@@ -322,12 +376,19 @@ export default function run(argv = []) {
322
376
  return;
323
377
  }
324
378
 
325
- const result = planInit({
379
+ const result = await planInit({
326
380
  argv,
327
381
  exists: defaultExists,
328
382
  runStep: defaultRunStep,
329
383
  confirm: defaultConfirm,
330
384
  isTTY: Boolean(process.stdin.isTTY),
385
+ afterBootstrap: async (projectRoot) => {
386
+ const runInitTail = await getRunInitTail(projectRoot);
387
+ return runInitTail({
388
+ root: projectRoot,
389
+ isTTY: Boolean(process.stdin.isTTY),
390
+ });
391
+ },
331
392
  });
332
393
 
333
394
  if (result.exitCode !== 0) {
@@ -26,7 +26,17 @@ import { createRequire } from 'node:module';
26
26
  import path from 'node:path';
27
27
  import { fileURLToPath } from 'node:url';
28
28
 
29
+ import {
30
+ REQUIRED_NODE_CEILING_MAJOR,
31
+ REQUIRED_NODE_FLOOR,
32
+ satisfiesNodeEngine,
33
+ } from '../../.agents/scripts/lib/bootstrap/project-bootstrap.js';
34
+ import {
35
+ defaultResolvePackageRoot,
36
+ listFiles as listPayloadFiles,
37
+ } from './sync.js';
29
38
  import { readCache } from './version-check.js';
39
+ import { compareVersions as compareSemver } from './version-helpers.js';
30
40
 
31
41
  // ---------------------------------------------------------------------------
32
42
  // Internal helpers
@@ -53,32 +63,6 @@ function spawn(cmd, args) {
53
63
  // check: node-version
54
64
  // ---------------------------------------------------------------------------
55
65
 
56
- /**
57
- * The minimum Node version the framework requires (`engines.node`).
58
- * Mirrors `lib/bootstrap/project-bootstrap.js#REQUIRED_NODE_FLOOR`.
59
- */
60
- const REQUIRED_NODE_FLOOR = '22.22.1';
61
- const REQUIRED_NODE_CEILING_MAJOR = 25;
62
-
63
- /**
64
- * Return true when `version` satisfies `>=22.22.1 <25`.
65
- *
66
- * @param {string} version
67
- * @returns {boolean}
68
- */
69
- function satisfiesNodeEngine(version) {
70
- const [majorRaw, minorRaw, patchRaw] = String(version).split('.');
71
- const major = Number.parseInt(majorRaw, 10) || 0;
72
- const minor = Number.parseInt(minorRaw, 10) || 0;
73
- const patch = Number.parseInt(patchRaw, 10) || 0;
74
- if (major >= REQUIRED_NODE_CEILING_MAJOR) return false;
75
- if (major > 22) return true;
76
- if (major < 22) return false;
77
- if (minor > 22) return true;
78
- if (minor < 22) return false;
79
- return patch >= 1;
80
- }
81
-
82
66
  /**
83
67
  * @param {{ nodeVersion?: string }} [opts]
84
68
  * @returns {{ ok: boolean, detail: string, remedy?: string }}
@@ -235,19 +219,6 @@ function runGhAuth({ runner = spawn, env = process.env } = {}) {
235
219
  // check: commands-in-sync
236
220
  // ---------------------------------------------------------------------------
237
221
 
238
- /**
239
- * Resolve the project root — the directory that contains `.agents/` and
240
- * `.claude/`. Walks up from this file's own location.
241
- *
242
- * @returns {string}
243
- */
244
- function resolveProjectRoot() {
245
- // lib/cli/registry.js lives at <root>/lib/cli/registry.js, so walk up two
246
- // levels from __dirname to reach the project root.
247
- const here = path.dirname(fileURLToPath(import.meta.url));
248
- return path.resolve(here, '..', '..');
249
- }
250
-
251
222
  /**
252
223
  * Dry-run the sync-claude-commands logic: compare `.agents/workflows/*.md`
253
224
  * sources to the generated flat command tree `.claude/commands/*.md`
@@ -324,6 +295,14 @@ function runCommandsInSync({ projectRoot, cwd, readDir } = {}) {
324
295
  * Verify that the framework's required runtime dependencies are resolvable
325
296
  * from the project's node_modules.
326
297
  *
298
+ * `projectRoot` defaults to `process.cwd()` (the consumer project root) so
299
+ * the manifest path and the require-resolution context mirror the context in
300
+ * which the framework scripts actually run (Story #4046 A2). Using
301
+ * `resolveProjectRoot()` (the package root inside `node_modules/mandrel/`)
302
+ * breaks the check under pnpm isolated-mode layouts where the consumer's
303
+ * node_modules are hoisted above `node_modules/mandrel/` and are invisible
304
+ * from the package root's resolver.
305
+ *
327
306
  * Injectable seams:
328
307
  * - `resolve(dep)` — replaces the real `require.resolve`; throws when a dep
329
308
  * is missing.
@@ -338,10 +317,15 @@ function runRuntimeDeps({
338
317
  resolve: resolveSeam,
339
318
  manifestRequired,
340
319
  } = {}) {
320
+ // Anchor at process.cwd() (the consumer root), not resolveProjectRoot() (the
321
+ // package root). Under pnpm isolated-mode the consumer's node_modules are not
322
+ // visible from inside node_modules/mandrel/, so the old anchor produced false
323
+ // "missing" reports on a clean install (Story #4046 A2).
324
+ const root = projectRoot ?? process.cwd();
325
+
341
326
  let required = manifestRequired;
342
327
  if (!required) {
343
328
  try {
344
- const root = projectRoot ?? resolveProjectRoot();
345
329
  const manifestPath = path.join(root, '.agents', 'runtime-deps.json');
346
330
  const raw = fs.readFileSync(manifestPath, 'utf8');
347
331
  const parsed = JSON.parse(raw);
@@ -366,9 +350,11 @@ function runRuntimeDeps({
366
350
  }
367
351
  }
368
352
  } else {
369
- // Anchor resolution to the project root so it mirrors the context in which
370
- // the framework scripts run (they free-ride on the consumer's node_modules).
371
- const root = projectRoot ?? resolveProjectRoot();
353
+ // Anchor resolution to the consumer project root so it mirrors the context
354
+ // in which the framework scripts run (they free-ride on the consumer's
355
+ // node_modules). Under pnpm isolated-mode the consumer's node_modules are
356
+ // not reachable from inside node_modules/mandrel/; anchoring at process.cwd()
357
+ // finds them correctly.
372
358
  const req = createRequire(path.join(root, 'package.json'));
373
359
  for (const dep of required) {
374
360
  try {
@@ -467,68 +453,9 @@ function runAgentsMaterialized({ cwd, existsSync, resolvePackage } = {}) {
467
453
  // check: agents-drift
468
454
  // ---------------------------------------------------------------------------
469
455
 
470
- /**
471
- * Package name whose `.agents/` payload is the drift baseline. Mirrors
472
- * `lib/cli/sync.js#PACKAGE_NAME`.
473
- */
474
- const PACKAGE_NAME = 'mandrel';
475
-
476
- /**
477
- * Top-level directory name (relative to `.agents/`) reserved as the
478
- * sync-exempt local-additions zone (Story #3498, f-drift-local-zone).
479
- *
480
- * `.agents/local/` is consumer-owned space that `mandrel sync` never
481
- * materializes nor prunes, so it is excluded from the drift comparison —
482
- * a hand-authored file under `.agents/local/` is sanctioned, not drift.
483
- * Mirrors `lib/cli/sync.js#LOCAL_ZONE_DIR`.
484
- */
485
- const LOCAL_ZONE_DIR = 'local';
486
-
487
- /**
488
- * Default resolver: locate the installed `mandrel` package root by
489
- * resolving its `package.json` and returning the directory that contains it.
490
- * Mirrors `lib/cli/sync.js#defaultResolvePackageRoot` so the drift baseline
491
- * is exactly the payload that `mandrel sync` would copy.
492
- *
493
- * @param {string} fromDir - Directory to resolve from (the consumer project).
494
- * @returns {string} Absolute path to the package root.
495
- */
496
- function defaultResolvePackageRoot(fromDir) {
497
- const requireFrom = createRequire(path.join(fromDir, 'noop.js'));
498
- const pkgJsonPath = requireFrom.resolve(`${PACKAGE_NAME}/package.json`);
499
- return path.dirname(pkgJsonPath);
500
- }
501
-
502
- /**
503
- * Recursively enumerate every regular file under `dir`, returning paths
504
- * relative to `dir` using OS separators. The top-level `local/` subtree is
505
- * skipped entirely — it is the consumer-owned local-additions zone and is
506
- * never part of the package payload (Story #3498). Mirrors
507
- * `lib/cli/sync.js#listFiles` so source enumeration matches the materializer.
508
- *
509
- * @param {string} dir - Absolute directory to walk.
510
- * @param {typeof fs} fsImpl
511
- * @param {string} [prefix] - Accumulated relative prefix (internal).
512
- * @returns {string[]} Relative file paths.
513
- */
514
- function listPayloadFiles(dir, fsImpl, prefix = '') {
515
- const out = [];
516
- for (const ent of fsImpl.readdirSync(dir, { withFileTypes: true })) {
517
- // Scope the local-zone skip to the top-level `.agents/local/` only; a
518
- // deeper directory that happens to be named `local` stays in scope.
519
- if (prefix === '' && ent.name === LOCAL_ZONE_DIR && ent.isDirectory()) {
520
- continue;
521
- }
522
- const rel = prefix ? path.join(prefix, ent.name) : ent.name;
523
- const abs = path.join(dir, ent.name);
524
- if (ent.isDirectory()) {
525
- out.push(...listPayloadFiles(abs, fsImpl, rel));
526
- } else {
527
- out.push(rel);
528
- }
529
- }
530
- return out;
531
- }
456
+ // defaultResolvePackageRoot and listPayloadFiles (re-exported as `listFiles`
457
+ // from sync.js) are imported from `lib/cli/sync.js` at the top of this file
458
+ // (Story #4048 B3 — no mirror copies).
532
459
 
533
460
  /**
534
461
  * Compare the consumer's materialized `./.agents/<f>` bytes against the
@@ -630,39 +557,9 @@ function runAgentsDrift({ cwd, fsImpl = fs, resolvePackageRoot } = {}) {
630
557
  // check: version-current
631
558
  // ---------------------------------------------------------------------------
632
559
 
633
- /**
634
- * Parse a dotted semver-ish string into a numeric tuple. Non-numeric or
635
- * missing segments coerce to 0 so a partial version still compares sanely.
636
- * Mirrors `lib/cli/update.js#parseVersion`.
637
- *
638
- * @param {string} version
639
- * @returns {[number, number, number]}
640
- */
641
- function parseVersionTuple(version) {
642
- const [major, minor, patch] = String(version).split('.');
643
- return [
644
- Number.parseInt(major, 10) || 0,
645
- Number.parseInt(minor, 10) || 0,
646
- Number.parseInt(patch, 10) || 0,
647
- ];
648
- }
649
-
650
- /**
651
- * Compare two version strings. Negative when `a < b`, zero when equal,
652
- * positive when `a > b`. Mirrors `lib/cli/update.js#compareVersions`.
653
- *
654
- * @param {string} a
655
- * @param {string} b
656
- * @returns {number}
657
- */
658
- function compareSemver(a, b) {
659
- const pa = parseVersionTuple(a);
660
- const pb = parseVersionTuple(b);
661
- for (let i = 0; i < 3; i += 1) {
662
- if (pa[i] !== pb[i]) return pa[i] - pb[i];
663
- }
664
- return 0;
665
- }
560
+ // parseVersion and compareVersions are imported from `lib/cli/version-helpers.js`
561
+ // at the top of this file (Story #4048 B3 — no mirror copies). `compareSemver`
562
+ // is the registry-local alias for the imported `compareVersions`.
666
563
 
667
564
  /**
668
565
  * Resolve the installed `mandrel` version from this package's own
@@ -685,17 +582,16 @@ function defaultInstalledVersion(fsImpl) {
685
582
  const DEFAULT_VERSION_CACHE_FILENAME = 'version-check.json';
686
583
 
687
584
  /**
688
- * Default cache path: `<projectRoot>/temp/version-check.json`, the same daily
689
- * freshness cache that `lib/cli/version-check.js` reads and refreshes.
585
+ * Default cache path: `<consumerRoot>/temp/version-check.json`, anchored at
586
+ * `process.cwd()` (the consumer project root) so the cache file survives
587
+ * npm/pnpm reinstalls that may replace `node_modules/` entirely (Story #4046
588
+ * A2). Mirrors `commands-in-sync` / `agents-materialized` / `agents-drift`
589
+ * which all anchor at `process.cwd()`.
690
590
  *
691
591
  * @returns {string}
692
592
  */
693
593
  function defaultVersionCachePath() {
694
- return path.join(
695
- resolveProjectRoot(),
696
- 'temp',
697
- DEFAULT_VERSION_CACHE_FILENAME,
698
- );
594
+ return path.join(process.cwd(), 'temp', DEFAULT_VERSION_CACHE_FILENAME);
699
595
  }
700
596
 
701
597
  /**