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.
- package/.agents/README.md +14 -14
- package/.agents/docs/SDLC.md +129 -134
- package/.agents/docs/configuration.md +16 -16
- package/.agents/docs/workflows.md +6 -8
- package/.agents/instructions.md +12 -11
- package/.agents/personas/architect.md +1 -1
- package/.agents/personas/product.md +1 -1
- package/.agents/personas/project-manager.md +14 -14
- package/.agents/personas/technical-writer.md +1 -1
- package/.agents/rules/changelog-style.md +5 -5
- package/.agents/rules/git-conventions.md +3 -3
- package/.agents/schemas/agentrc.schema.json +3 -3
- package/.agents/schemas/dispatch-manifest.json +4 -4
- package/.agents/schemas/epic-spec.schema.json +15 -45
- package/.agents/schemas/lifecycle/README.md +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/README.md +1 -1
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
- package/.agents/scripts/analyze-execution.js +2 -2
- package/.agents/scripts/audit-to-stories.js +1 -1
- package/.agents/scripts/check-doc-links.js +2 -3
- package/.agents/scripts/diagnose-friction.js +1 -1
- package/.agents/scripts/dispatcher.js +2 -2
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/epic-audit-prepare.js +3 -3
- package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
- package/.agents/scripts/epic-deliver-preflight.js +6 -6
- package/.agents/scripts/epic-deliver-prepare.js +1 -1
- package/.agents/scripts/epic-execute-record-wave.js +4 -4
- package/.agents/scripts/epic-plan-healthcheck.js +6 -10
- package/.agents/scripts/epic-plan-spec-validate.js +1 -1
- package/.agents/scripts/epic-reconcile.js +11 -29
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +7 -11
- package/.agents/scripts/lib/ITicketingProvider.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
- package/.agents/scripts/lib/baseline-snapshot.js +7 -7
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- package/.agents/scripts/lib/codebase-snapshot.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +3 -3
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
- package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
- package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +10 -10
- package/.agents/scripts/lib/label-constants.js +3 -4
- package/.agents/scripts/lib/label-taxonomy.js +3 -8
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +5 -5
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
- package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
- package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
- package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
- package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
- package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
- package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
- package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
- package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
- package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
- package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +1 -1
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
- package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
- package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
- package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
- package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/spec/index.js +1 -1
- package/.agents/scripts/lib/spec/loader.js +2 -2
- package/.agents/scripts/lib/spec/state.js +7 -16
- package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
- package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
- package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
- package/.agents/scripts/lib/story-lifecycle.js +8 -8
- package/.agents/scripts/lib/story-plan.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
- package/.agents/scripts/lib/wave-runner/tick.js +1 -1
- package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
- package/.agents/scripts/lifecycle-emit.js +1 -1
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/mappers.js +2 -2
- package/.agents/scripts/providers/github/tickets.js +4 -4
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/retro-run.js +2 -2
- package/.agents/scripts/run-lint.js +1 -1
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/stories-wave-tick.js +5 -5
- package/.agents/scripts/story-close.js +1 -1
- package/.agents/scripts/story-init.js +13 -16
- package/.agents/scripts/story-phase.js +5 -5
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/scripts/validate-docs-freshness.js +1 -1
- package/.agents/scripts/wave-tick.js +1 -1
- package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
- package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
- package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
- package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
- package/.agents/skills/core/scope-triage/SKILL.md +9 -10
- package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
- package/.agents/skills/skills.index.json +7 -7
- package/.agents/templates/agent-protocol.md +2 -2
- package/.agents/workflows/agents-update.md +2 -2
- package/.agents/workflows/audit-architecture.md +2 -2
- package/.agents/workflows/audit-clean-code.md +2 -2
- package/.agents/workflows/audit-dependencies.md +1 -1
- package/.agents/workflows/audit-devops.md +1 -1
- package/.agents/workflows/audit-documentation.md +2 -2
- package/.agents/workflows/audit-lighthouse.md +1 -1
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/audit-privacy.md +1 -1
- package/.agents/workflows/audit-quality.md +2 -2
- package/.agents/workflows/audit-security.md +2 -2
- package/.agents/workflows/audit-seo.md +1 -1
- package/.agents/workflows/audit-sre.md +1 -1
- package/.agents/workflows/audit-to-stories.md +10 -10
- package/.agents/workflows/audit-ux-ui.md +1 -1
- package/.agents/workflows/deliver.md +85 -0
- package/.agents/workflows/explain.md +3 -3
- package/.agents/workflows/git-merge-pr.md +1 -1
- package/.agents/workflows/git-pr-all.md +13 -10
- package/.agents/workflows/git-push.md +6 -3
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
- package/.agents/workflows/helpers/code-review.md +5 -5
- package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
- package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/epic-audit.md +6 -6
- package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
- package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
- package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
- package/.agents/workflows/helpers/epic-testing.md +3 -3
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
- package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
- package/.agents/workflows/helpers/signals.md +1 -1
- package/.agents/workflows/helpers/single-story-deliver.md +11 -11
- package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
- package/.agents/workflows/onboard.md +17 -17
- package/.agents/workflows/plan.md +89 -0
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/qa-run-harness.md +1 -1
- package/README.md +4 -12
- package/docs/CHANGELOG.md +1149 -0
- package/lib/cli/__tests__/update-changelog-surface.test.js +357 -0
- package/lib/cli/__tests__/update-reexec.test.js +513 -0
- package/lib/cli/init.js +31 -29
- package/lib/cli/update.js +413 -52
- package/package.json +2 -1
- package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
package/lib/cli/update.js
CHANGED
|
@@ -25,11 +25,37 @@
|
|
|
25
25
|
* the resolved version so an override can still consume the auto-probed
|
|
26
26
|
* newest. The registry probe in step 1 always stays on `npm view` (a
|
|
27
27
|
* PM-agnostic registry query).
|
|
28
|
-
* 5. runSync — re-materialize ./.agents/ from the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* 5. runSync — re-materialize ./.agents/ **from the newly-installed
|
|
29
|
+
* binary** so the materialized payload is always the target version's.
|
|
30
|
+
* 6. runMigrations — apply version-keyed steps for the crossed range,
|
|
31
|
+
* **from the newly-installed binary**.
|
|
32
|
+
* 7. doctor — run the check registry **from the newly-installed
|
|
33
|
+
* binary** so `agents-drift` is never a false-green against stale payload.
|
|
31
34
|
* 8. surface the changelog for the target version
|
|
32
35
|
*
|
|
36
|
+
* ## Re-exec of post-install phases (Story #4034)
|
|
37
|
+
*
|
|
38
|
+
* Steps 5–7 execute as **child processes spawned from the newly-installed
|
|
39
|
+
* binary** (`<cwd>/node_modules/.bin/mandrel`) rather than in the running
|
|
40
|
+
* process. Node cannot hot-swap a `require`d module mid-process, so without
|
|
41
|
+
* re-exec, the still-running old binary's `runSync`/`runMigrations`/`runDoctor`
|
|
42
|
+
* code would materialise the old payload even though the package on disk has
|
|
43
|
+
* already been updated. This produced the silent stale-`.agents/`
|
|
44
|
+
* materialization and `doctor` false-green observed in the v1.58.0 → v1.59.0
|
|
45
|
+
* consumer upgrade.
|
|
46
|
+
*
|
|
47
|
+
* The orchestration (progress messages, step tracking, changelog surface, exit
|
|
48
|
+
* code) stays in the parent process; only the version-sensitive phases run from
|
|
49
|
+
* the new bin. The `spawnPhase` seam makes the child-process boundary fully
|
|
50
|
+
* injectable so tests can verify the re-exec path without a real npm install.
|
|
51
|
+
*
|
|
52
|
+
* Backward compatibility: when `runSync`, `runMigrations`, or `runDoctor`
|
|
53
|
+
* are explicitly injected (the historical test-seam pattern) and `spawnPhase`
|
|
54
|
+
* is NOT injected, the in-process seams are used unchanged (old tests stay
|
|
55
|
+
* green). When `spawnPhase` IS injected, it takes priority over the in-process
|
|
56
|
+
* seams for the live phases — so new tests targeting the re-exec boundary can
|
|
57
|
+
* inject `spawnPhase` without touching the old seam interface.
|
|
58
|
+
*
|
|
33
59
|
* ## No git mutation
|
|
34
60
|
*
|
|
35
61
|
* The npm dependency bump rewrites `package.json` / `package-lock.json` in the
|
|
@@ -55,6 +81,20 @@
|
|
|
55
81
|
* - **with `--major`**: apply the major target and print the runbook inline.
|
|
56
82
|
* Routine minor/patch bumps within the 1.x line are never gated.
|
|
57
83
|
*
|
|
84
|
+
* ## Changelog surface
|
|
85
|
+
*
|
|
86
|
+
* `defaultSurfaceChangelog` prints the `docs/CHANGELOG.md` section(s) for the
|
|
87
|
+
* applied range `(current, target]`. It resolves the file against the target
|
|
88
|
+
* version's install directory (the freshly bumped `node_modules/mandrel/`),
|
|
89
|
+
* where the changelog is now included in the published tarball
|
|
90
|
+
* (`docs/CHANGELOG.md` in the `files` allowlist — Story #4035).
|
|
91
|
+
*
|
|
92
|
+
* When the packaged file is absent (e.g. an older installed version predating
|
|
93
|
+
* Story #4035), the seam attempts a one-shot HTTP GET of the raw file from
|
|
94
|
+
* GitHub via the injectable `fetchChangelog` seam. If that fetch also fails,
|
|
95
|
+
* the seam degrades gracefully — never throwing — and emits an actionable
|
|
96
|
+
* message directing the operator to the GitHub Releases page.
|
|
97
|
+
*
|
|
58
98
|
* ## Injectable seams (used by lib/cli/__tests__/update*.test.js)
|
|
59
99
|
*
|
|
60
100
|
* - `argv` — subcommand args (after `mandrel update`)
|
|
@@ -62,12 +102,24 @@
|
|
|
62
102
|
* - `resolveTargetVersion`— async, returns the newest published version
|
|
63
103
|
* - `npmUpdate` — async, performs the dependency bump (no git);
|
|
64
104
|
* receives `(target, { installCmd })`
|
|
65
|
-
* - `
|
|
66
|
-
*
|
|
67
|
-
*
|
|
105
|
+
* - `spawnPhase` — async, spawns a post-install phase from the new
|
|
106
|
+
* binary; receives `(phase, args, { binPath, cwd })`
|
|
107
|
+
* and returns `{ ok, stdout, stderr }`. When
|
|
108
|
+
* injected, it takes priority over `runSync`,
|
|
109
|
+
* `runMigrations`, and `runDoctor` for the live
|
|
110
|
+
* phases. See § Re-exec of post-install phases.
|
|
111
|
+
* - `runSync` — re-materializes ./.agents/ (lib/cli/sync.js).
|
|
112
|
+
* Used when `spawnPhase` is NOT injected (backward
|
|
113
|
+
* compat for tests that pre-date Story #4034).
|
|
114
|
+
* - `runMigrations` — version-keyed migration runner (lib/migrations).
|
|
115
|
+
* Used when `spawnPhase` is NOT injected.
|
|
116
|
+
* - `runDoctor` — async, returns { ok, results } from the registry.
|
|
117
|
+
* Used when `spawnPhase` is NOT injected.
|
|
68
118
|
* - `surfaceChangelog` — emits the target changelog section
|
|
69
119
|
* - `write` / `writeErr` — stdout / stderr sinks
|
|
70
120
|
* - `exit` — process.exit replacement
|
|
121
|
+
* - `cwd` — process.cwd() replacement (used to resolve the
|
|
122
|
+
* new binary path when `spawnPhase` is absent)
|
|
71
123
|
*
|
|
72
124
|
* Security (security-baseline § 5 — Data Leakage & Logging): logs only version
|
|
73
125
|
* strings, step names, and the runbook path. No tokens, credentials, or env
|
|
@@ -86,10 +138,17 @@
|
|
|
86
138
|
* the install argv is a tokenized list whose only variable segment is a
|
|
87
139
|
* resolved semver string — see `lib/install-cmd-parser.js` for the shared
|
|
88
140
|
* tokenize-and-spawn rationale this module reuses (no duplicated workaround).
|
|
141
|
+
*
|
|
142
|
+
* The `spawnPhase` default (Story #4034) similarly uses `shell: true` only on
|
|
143
|
+
* Windows: the new binary resolves from `node_modules/.bin/mandrel` (a fixed,
|
|
144
|
+
* non-operator-supplied path) and the per-phase argv vector is a constant
|
|
145
|
+
* fixed list (e.g. `['sync']`, `['migrate', '--from', v, '--to', v]`,
|
|
146
|
+
* `['doctor']`) with no injection risk regardless of the shell flag.
|
|
89
147
|
*/
|
|
90
148
|
|
|
91
149
|
import { spawnSync } from 'node:child_process';
|
|
92
150
|
import nodeFs from 'node:fs';
|
|
151
|
+
import nodeHttps from 'node:https';
|
|
93
152
|
import path from 'node:path';
|
|
94
153
|
import { fileURLToPath } from 'node:url';
|
|
95
154
|
|
|
@@ -105,6 +164,19 @@ const RUNBOOK_PATH = 'docs/upgrade-major.md';
|
|
|
105
164
|
/** The published package whose newest version `mandrel update` advances to. */
|
|
106
165
|
const PACKAGE_NAME = 'mandrel';
|
|
107
166
|
|
|
167
|
+
/**
|
|
168
|
+
* GitHub raw-file base URL for fetching `docs/CHANGELOG.md` when the packaged
|
|
169
|
+
* file is absent (Story #4035 — GitHub fallback). Resolves to the tagged
|
|
170
|
+
* release, e.g. `.../mandrel-v1.59.0/docs/CHANGELOG.md`.
|
|
171
|
+
*/
|
|
172
|
+
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/dsj1984/mandrel/';
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Human-readable GitHub Releases page — surfaced in the actionable fallback
|
|
176
|
+
* message when neither the packaged file nor the GitHub fetch succeeds.
|
|
177
|
+
*/
|
|
178
|
+
const GITHUB_RELEASES_URL = 'https://github.com/dsj1984/mandrel/releases';
|
|
179
|
+
|
|
108
180
|
/** Default freshness-cache filename — mirrors version-check.js. */
|
|
109
181
|
const DEFAULT_CACHE_FILENAME = 'version-check.json';
|
|
110
182
|
|
|
@@ -408,6 +480,72 @@ export function defaultNpmUpdate(
|
|
|
408
480
|
}
|
|
409
481
|
}
|
|
410
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Fetch `docs/CHANGELOG.md` for a specific mandrel tag from GitHub's raw
|
|
485
|
+
* content endpoint. This is the fallback when the packaged file is absent
|
|
486
|
+
* (e.g. an older install predating Story #4035 which added the file to the
|
|
487
|
+
* npm `files` allowlist).
|
|
488
|
+
*
|
|
489
|
+
* Injectable via the `fetchChangelog` seam so tests can verify the fallback
|
|
490
|
+
* path without issuing real network calls.
|
|
491
|
+
*
|
|
492
|
+
* The tag shape follows the `mandrel-vX.Y.Z` namespace (namespaced at
|
|
493
|
+
* `mandrel-v1.44.0`; bare `vX.Y.Z` for earlier releases). This function
|
|
494
|
+
* tries the namespaced tag first, then the bare-tag form, so it covers both
|
|
495
|
+
* tag series without forcing callers to know the boundary.
|
|
496
|
+
*
|
|
497
|
+
* Security (security-baseline § Transport & Headers): the URL is constructed
|
|
498
|
+
* from a constant base and a semver string — no user input, no shell
|
|
499
|
+
* interpolation. The GET is a read-only fetch with no credentials.
|
|
500
|
+
*
|
|
501
|
+
* @param {string} version - The target semver string (e.g. `"1.59.0"`).
|
|
502
|
+
* @param {{
|
|
503
|
+
* https?: typeof nodeHttps,
|
|
504
|
+
* }} [deps]
|
|
505
|
+
* @returns {Promise<string>} The raw changelog text.
|
|
506
|
+
* @throws {Error} When both tag forms return a non-2xx response or the request
|
|
507
|
+
* errors out — the caller handles this gracefully.
|
|
508
|
+
*/
|
|
509
|
+
export async function fetchChangelogFromGitHub(
|
|
510
|
+
version,
|
|
511
|
+
{ https: httpsImpl = nodeHttps } = {},
|
|
512
|
+
) {
|
|
513
|
+
const tags = [`mandrel-v${version}`, `v${version}`];
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @param {string} url
|
|
517
|
+
* @returns {Promise<{ status: number, body: string }>}
|
|
518
|
+
*/
|
|
519
|
+
const httpGet = (url) =>
|
|
520
|
+
new Promise((resolve, reject) => {
|
|
521
|
+
httpsImpl
|
|
522
|
+
.get(url, (res) => {
|
|
523
|
+
const chunks = [];
|
|
524
|
+
res.on('data', (d) => chunks.push(d));
|
|
525
|
+
res.on('end', () =>
|
|
526
|
+
resolve({
|
|
527
|
+
status: res.statusCode ?? 0,
|
|
528
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
})
|
|
532
|
+
.on('error', reject);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
for (const tag of tags) {
|
|
536
|
+
const url = `${GITHUB_RAW_BASE}${tag}/docs/CHANGELOG.md`;
|
|
537
|
+
// eslint-disable-next-line no-await-in-loop
|
|
538
|
+
const { status, body } = await httpGet(url);
|
|
539
|
+
if (status >= 200 && status < 300) {
|
|
540
|
+
return body;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
throw new Error(
|
|
545
|
+
`mandrel update: GitHub fetch for mandrel v${version} docs/CHANGELOG.md returned non-2xx for all tag forms (tried: ${tags.join(', ')})`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
411
549
|
/**
|
|
412
550
|
* Default `surfaceChangelog` seam: print the `docs/CHANGELOG.md` section(s)
|
|
413
551
|
* covering the applied version range `(current, target]`. The changelog is
|
|
@@ -415,38 +553,61 @@ export function defaultNpmUpdate(
|
|
|
415
553
|
* prints every section whose version is newer than `current` and no newer than
|
|
416
554
|
* `target`.
|
|
417
555
|
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
556
|
+
* Resolution order (Story #4035):
|
|
557
|
+
* 1. Read `docs/CHANGELOG.md` from the target version's install directory
|
|
558
|
+
* (`node_modules/mandrel/docs/CHANGELOG.md` — now in the published
|
|
559
|
+
* tarball since `package.json` lists `docs/CHANGELOG.md` in `files`).
|
|
560
|
+
* 2. When the packaged file is absent (older install), fetch it from GitHub
|
|
561
|
+
* via the injectable `fetchChangelog` seam.
|
|
562
|
+
* 3. When both sources fail, emit an actionable warning with a link to the
|
|
563
|
+
* GitHub Releases page — never a bare "not found … skipping".
|
|
564
|
+
*
|
|
565
|
+
* Degrades gracefully (warns, never throws) — surfacing the changelog is
|
|
566
|
+
* best-effort and must never fail an otherwise-successful upgrade.
|
|
421
567
|
*
|
|
422
568
|
* @param {string} target - The applied target version.
|
|
423
569
|
* @param {{
|
|
424
570
|
* current?: string,
|
|
425
571
|
* changelogPath?: string,
|
|
426
572
|
* fs?: typeof nodeFs,
|
|
573
|
+
* fetchChangelog?: (version: string) => Promise<string>,
|
|
427
574
|
* write?: (s: string) => void,
|
|
428
575
|
* writeErr?: (s: string) => void,
|
|
429
576
|
* }} [opts]
|
|
430
|
-
* @returns {void}
|
|
577
|
+
* @returns {Promise<void>}
|
|
431
578
|
*/
|
|
432
|
-
function defaultSurfaceChangelog(
|
|
579
|
+
async function defaultSurfaceChangelog(
|
|
433
580
|
target,
|
|
434
581
|
{
|
|
435
582
|
current,
|
|
436
583
|
changelogPath = path.join(resolveProjectRoot(), 'docs', 'CHANGELOG.md'),
|
|
437
584
|
fs = nodeFs,
|
|
585
|
+
fetchChangelog = fetchChangelogFromGitHub,
|
|
438
586
|
write = (s) => process.stdout.write(s),
|
|
439
587
|
writeErr = (s) => process.stderr.write(s),
|
|
440
588
|
} = {},
|
|
441
589
|
) {
|
|
442
590
|
let raw;
|
|
591
|
+
|
|
592
|
+
// 1. Try the packaged file (present in installs since Story #4035).
|
|
443
593
|
try {
|
|
444
594
|
raw = fs.readFileSync(changelogPath, 'utf8');
|
|
445
595
|
} catch {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
596
|
+
// File absent — fall through to GitHub fetch.
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// 2. Packaged file absent: attempt a GitHub fetch for the target tag.
|
|
600
|
+
if (raw === undefined) {
|
|
601
|
+
try {
|
|
602
|
+
raw = await fetchChangelog(target);
|
|
603
|
+
} catch {
|
|
604
|
+
// Both sources unavailable — emit an actionable message and return.
|
|
605
|
+
writeErr(
|
|
606
|
+
`mandrel update: changelog not available for v${target} — ` +
|
|
607
|
+
`view the release notes at ${GITHUB_RELEASES_URL}\n`,
|
|
608
|
+
);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
450
611
|
}
|
|
451
612
|
|
|
452
613
|
const sections = parseChangelogSections(raw);
|
|
@@ -458,7 +619,8 @@ function defaultSurfaceChangelog(
|
|
|
458
619
|
|
|
459
620
|
if (relevant.length === 0) {
|
|
460
621
|
writeErr(
|
|
461
|
-
`mandrel update: no CHANGELOG section found for v${target} —
|
|
622
|
+
`mandrel update: no CHANGELOG section found for v${target} — ` +
|
|
623
|
+
`view the release notes at ${GITHUB_RELEASES_URL}\n`,
|
|
462
624
|
);
|
|
463
625
|
return;
|
|
464
626
|
}
|
|
@@ -509,6 +671,8 @@ function parseChangelogSections(raw) {
|
|
|
509
671
|
* report whether all passed. Mirrors lib/cli/doctor.js's pass accounting
|
|
510
672
|
* without the formatted report (the orchestrator owns its own output).
|
|
511
673
|
*
|
|
674
|
+
* This is used only when `spawnPhase` is NOT injected (backward-compat path).
|
|
675
|
+
*
|
|
512
676
|
* @param {{ checks?: typeof registry }} [opts]
|
|
513
677
|
* @returns {Promise<{ ok: boolean, results: Array<{ name: string, ok: boolean }> }>}
|
|
514
678
|
*/
|
|
@@ -521,6 +685,69 @@ async function defaultRunDoctor({ checks = registry } = {}) {
|
|
|
521
685
|
return { ok: results.every((r) => r.ok), results };
|
|
522
686
|
}
|
|
523
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Resolve the path to the `mandrel` binary inside `node_modules/.bin/` for the
|
|
690
|
+
* given project root. On Windows the binary is a `.cmd` shim; on POSIX it is a
|
|
691
|
+
* plain executable. The resolved path is used as the target for the post-install
|
|
692
|
+
* phase re-exec (Story #4034).
|
|
693
|
+
*
|
|
694
|
+
* @param {string} projectRoot - Absolute path to the consumer project.
|
|
695
|
+
* @returns {string} Absolute path to the new binary.
|
|
696
|
+
*/
|
|
697
|
+
export function resolveNewBinPath(projectRoot) {
|
|
698
|
+
const binName = process.platform === 'win32' ? 'mandrel.cmd' : 'mandrel';
|
|
699
|
+
return path.join(projectRoot, 'node_modules', '.bin', binName);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Default `spawnPhase` seam (Story #4034): spawn a post-install phase from the
|
|
704
|
+
* newly-installed `mandrel` binary and stream its stdout/stderr through the
|
|
705
|
+
* parent's write sinks. Each phase runs as an isolated child process so the
|
|
706
|
+
* newly-installed module code (not the currently-loaded old module) executes.
|
|
707
|
+
*
|
|
708
|
+
* The spawn uses `shell: true` only on Windows where the binary is a `.cmd`
|
|
709
|
+
* shim (CVE-2024-27980 parity). The argv vector is a fixed constant list
|
|
710
|
+
* per phase — no operator-supplied data enters the vector, so the shell flag
|
|
711
|
+
* carries no injection risk (security-baseline § Output & Rendering).
|
|
712
|
+
*
|
|
713
|
+
* Throws when the child exits non-zero so the orchestrator can surface the
|
|
714
|
+
* failure to the operator.
|
|
715
|
+
*
|
|
716
|
+
* @param {string} phase - The mandrel sub-command to run (e.g. `'sync'`).
|
|
717
|
+
* @param {string[]} args - Additional arguments for the sub-command.
|
|
718
|
+
* @param {{
|
|
719
|
+
* binPath: string,
|
|
720
|
+
* cwd: string,
|
|
721
|
+
* write: (s: string) => void,
|
|
722
|
+
* writeErr: (s: string) => void,
|
|
723
|
+
* spawnFn?: typeof spawnSync,
|
|
724
|
+
* }} opts
|
|
725
|
+
* @returns {{ ok: boolean, stdout: string, stderr: string }}
|
|
726
|
+
*/
|
|
727
|
+
export function defaultSpawnPhase(
|
|
728
|
+
phase,
|
|
729
|
+
args,
|
|
730
|
+
{ binPath, cwd, write, writeErr, spawnFn = spawnSync },
|
|
731
|
+
) {
|
|
732
|
+
const argv = [phase, ...args];
|
|
733
|
+
const r = spawnFn(binPath, argv, {
|
|
734
|
+
cwd,
|
|
735
|
+
encoding: 'utf8',
|
|
736
|
+
shell: process.platform === 'win32',
|
|
737
|
+
});
|
|
738
|
+
const stdout = typeof r.stdout === 'string' ? r.stdout : '';
|
|
739
|
+
const stderr = typeof r.stderr === 'string' ? r.stderr : '';
|
|
740
|
+
if (stdout) write(stdout);
|
|
741
|
+
if (stderr) writeErr(stderr);
|
|
742
|
+
if (r.error) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
`mandrel update: failed to spawn \`mandrel ${phase}\` from new binary: ${r.error.message}`,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
const ok = r.status === 0;
|
|
748
|
+
return { ok, stdout, stderr };
|
|
749
|
+
}
|
|
750
|
+
|
|
524
751
|
/**
|
|
525
752
|
* The ordered step names the orchestrator drives on a non-major bump. Shared
|
|
526
753
|
* by the live path and the `--dry-run` plan printout so the two never drift.
|
|
@@ -576,6 +803,7 @@ function emitMajorRefusal(target, writeErr) {
|
|
|
576
803
|
* currentVersion?: string | (() => string),
|
|
577
804
|
* resolveTargetVersion?: () => (string | Promise<string>),
|
|
578
805
|
* npmUpdate?: (version: string, opts: { installCmd?: string }) => unknown | Promise<unknown>,
|
|
806
|
+
* spawnPhase?: (phase: string, args: string[], opts: { binPath: string, cwd: string, write: (s: string) => void, writeErr: (s: string) => void }) => Promise<{ ok: boolean, stdout: string, stderr: string }> | { ok: boolean, stdout: string, stderr: string },
|
|
579
807
|
* runSync?: typeof defaultRunSync,
|
|
580
808
|
* runMigrations?: typeof defaultRunMigrations,
|
|
581
809
|
* runDoctor?: typeof defaultRunDoctor,
|
|
@@ -583,6 +811,7 @@ function emitMajorRefusal(target, writeErr) {
|
|
|
583
811
|
* write?: (s: string) => void,
|
|
584
812
|
* writeErr?: (s: string) => void,
|
|
585
813
|
* exit?: (code: number) => void,
|
|
814
|
+
* cwd?: () => string,
|
|
586
815
|
* }} [opts]
|
|
587
816
|
* @returns {Promise<{
|
|
588
817
|
* ok: boolean,
|
|
@@ -599,6 +828,7 @@ export async function runUpdate({
|
|
|
599
828
|
currentVersion,
|
|
600
829
|
resolveTargetVersion,
|
|
601
830
|
npmUpdate,
|
|
831
|
+
spawnPhase,
|
|
602
832
|
runSync = defaultRunSync,
|
|
603
833
|
runMigrations = defaultRunMigrations,
|
|
604
834
|
runDoctor = defaultRunDoctor,
|
|
@@ -606,6 +836,7 @@ export async function runUpdate({
|
|
|
606
836
|
write = (s) => process.stdout.write(s),
|
|
607
837
|
writeErr = (s) => process.stderr.write(s),
|
|
608
838
|
exit = (code) => process.exit(code),
|
|
839
|
+
cwd = () => process.cwd(),
|
|
609
840
|
} = {}) {
|
|
610
841
|
const dryRun = argv.includes('--dry-run');
|
|
611
842
|
const allowMajor = argv.includes('--major');
|
|
@@ -703,40 +934,136 @@ export async function runUpdate({
|
|
|
703
934
|
await npmUpdate(target, { installCmd });
|
|
704
935
|
stepsRun.push('npm-update');
|
|
705
936
|
|
|
706
|
-
//
|
|
707
|
-
runSync
|
|
708
|
-
|
|
937
|
+
// Decide whether to use the re-exec path (spawnPhase) or the in-process
|
|
938
|
+
// backward-compat seams (runSync / runMigrations / runDoctor).
|
|
939
|
+
//
|
|
940
|
+
// spawnPhase injected → re-exec path: all post-install phases run from the
|
|
941
|
+
// newly-installed binary. This is the production path and is what fixes
|
|
942
|
+
// the stale-materialization bug (Story #4034).
|
|
943
|
+
//
|
|
944
|
+
// spawnPhase NOT injected → in-process path: the original pre-Story-#4034
|
|
945
|
+
// behaviour. Tests that pre-date this change inject runSync/runMigrations/
|
|
946
|
+
// runDoctor and rely on the in-process path; they stay green without any
|
|
947
|
+
// modification.
|
|
948
|
+
const useReExec = typeof spawnPhase === 'function';
|
|
709
949
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
950
|
+
if (useReExec) {
|
|
951
|
+
// Re-exec path: post-install phases run from the new binary.
|
|
952
|
+
const projectRoot = cwd();
|
|
953
|
+
const binPath = resolveNewBinPath(projectRoot);
|
|
713
954
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
955
|
+
// 2. runSync from new bin — re-materialize ./.agents/ from the freshly
|
|
956
|
+
// installed payload. Running from the new bin ensures the copied files
|
|
957
|
+
// come from the new package's .agents/ tree, not the old loaded module.
|
|
958
|
+
const syncResult = await spawnPhase('sync', [], {
|
|
959
|
+
binPath,
|
|
960
|
+
cwd: projectRoot,
|
|
961
|
+
write,
|
|
962
|
+
writeErr,
|
|
963
|
+
});
|
|
964
|
+
if (!syncResult.ok) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
`mandrel update: \`mandrel sync\` from new binary exited non-zero — ` +
|
|
967
|
+
'the .agents/ materialization may be incomplete. ' +
|
|
968
|
+
'Run `mandrel sync` manually to restore.',
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
stepsRun.push('runSync');
|
|
722
972
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
973
|
+
// 3. runMigrations from new bin — apply version-keyed steps for the
|
|
974
|
+
// crossed range. The new binary's migration registry contains any steps
|
|
975
|
+
// added in the target version; the old process's registry does not.
|
|
976
|
+
const migrateResult = await spawnPhase(
|
|
977
|
+
'migrate',
|
|
978
|
+
['--from', current, '--to', target],
|
|
979
|
+
{ binPath, cwd: projectRoot, write, writeErr },
|
|
729
980
|
);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
981
|
+
if (!migrateResult.ok) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
`mandrel update: \`mandrel migrate\` from new binary exited non-zero — ` +
|
|
984
|
+
`some migrations for v${current} → v${target} may not have applied. ` +
|
|
985
|
+
`Run \`mandrel migrate --from ${current} --to ${target}\` manually to retry.`,
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
stepsRun.push('runMigrations');
|
|
989
|
+
|
|
990
|
+
// 4. doctor from new bin — verify the resulting install. Running from the
|
|
991
|
+
// new bin is critical: the agents-drift check compares the materialized
|
|
992
|
+
// .agents/ against the installed package payload. When the old process
|
|
993
|
+
// runs this check, it resolves the package root to its own (old) install
|
|
994
|
+
// dir, so drift against the new payload is invisible. The new binary
|
|
995
|
+
// resolves the package root to the now-installed new version, producing
|
|
996
|
+
// an accurate result.
|
|
997
|
+
const doctorResult = await spawnPhase('doctor', [], {
|
|
998
|
+
binPath,
|
|
999
|
+
cwd: projectRoot,
|
|
1000
|
+
write,
|
|
1001
|
+
writeErr,
|
|
1002
|
+
});
|
|
1003
|
+
stepsRun.push('doctor');
|
|
1004
|
+
|
|
1005
|
+
// 5. surface the target changelog (best-effort; optional seam).
|
|
1006
|
+
if (typeof surfaceChangelog === 'function') {
|
|
1007
|
+
await surfaceChangelog(target);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (!doctorResult.ok) {
|
|
1011
|
+
writeErr(
|
|
1012
|
+
`mandrel update: upgraded to v${target} but doctor reported failures.\n` +
|
|
1013
|
+
' → Run `mandrel doctor` for remedies.\n',
|
|
1014
|
+
);
|
|
1015
|
+
exit(1);
|
|
1016
|
+
return {
|
|
1017
|
+
ok: false,
|
|
1018
|
+
action: 'doctor-failed',
|
|
1019
|
+
currentVersion: current,
|
|
1020
|
+
targetVersion: target,
|
|
1021
|
+
major,
|
|
1022
|
+
stepsRun,
|
|
1023
|
+
dryRun: false,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
} else {
|
|
1027
|
+
// In-process backward-compat path (pre-Story-#4034 behaviour).
|
|
1028
|
+
// Used when no `spawnPhase` seam is injected — preserves full backward
|
|
1029
|
+
// compatibility with existing tests that inject runSync/runMigrations/
|
|
1030
|
+
// runDoctor directly.
|
|
1031
|
+
|
|
1032
|
+
// 2. runSync — re-materialize ./.agents/ from the new payload.
|
|
1033
|
+
runSync({ argv: [] });
|
|
1034
|
+
stepsRun.push('runSync');
|
|
1035
|
+
|
|
1036
|
+
// 3. runMigrations — apply version-keyed steps for the crossed range.
|
|
1037
|
+
runMigrations({ fromVersion: current, toVersion: target, ctx: {} });
|
|
1038
|
+
stepsRun.push('runMigrations');
|
|
1039
|
+
|
|
1040
|
+
// 4. doctor — verify the resulting install.
|
|
1041
|
+
const doctor = await runDoctor();
|
|
1042
|
+
stepsRun.push('doctor');
|
|
1043
|
+
|
|
1044
|
+
// 5. surface the target changelog (best-effort; optional seam).
|
|
1045
|
+
if (typeof surfaceChangelog === 'function') {
|
|
1046
|
+
await surfaceChangelog(target);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (!doctor.ok) {
|
|
1050
|
+
const failed = doctor.results.filter((r) => !r.ok).map((r) => r.name);
|
|
1051
|
+
writeErr(
|
|
1052
|
+
`mandrel update: upgraded to v${target} but doctor reported failures: ` +
|
|
1053
|
+
`${failed.join(', ')}\n` +
|
|
1054
|
+
' → Run `mandrel doctor` for remedies.\n',
|
|
1055
|
+
);
|
|
1056
|
+
exit(1);
|
|
1057
|
+
return {
|
|
1058
|
+
ok: false,
|
|
1059
|
+
action: 'doctor-failed',
|
|
1060
|
+
currentVersion: current,
|
|
1061
|
+
targetVersion: target,
|
|
1062
|
+
major,
|
|
1063
|
+
stepsRun,
|
|
1064
|
+
dryRun: false,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
740
1067
|
}
|
|
741
1068
|
|
|
742
1069
|
write(`✅ Updated to v${target}. The lockfile bump is staged for review.\n`);
|
|
@@ -763,8 +1090,17 @@ export async function runUpdate({
|
|
|
763
1090
|
* lockfile (`pnpm`/`yarn`/`npm`), or the `--install-cmd` override —
|
|
764
1091
|
* through the shared `runInstallCommand` helper — no git mutation;
|
|
765
1092
|
* lockfile left staged.
|
|
1093
|
+
* - `spawnPhase` is wired to `defaultSpawnPhase`, which spawns each
|
|
1094
|
+
* post-install phase (sync, migrate, doctor) from the newly-installed
|
|
1095
|
+
* binary (`node_modules/.bin/mandrel`). This is the Story #4034 fix:
|
|
1096
|
+
* the new bin loads the new package's module code and resolves paths
|
|
1097
|
+
* against the new install dir, so sync/migrate/doctor can never observe
|
|
1098
|
+
* the old payload.
|
|
766
1099
|
* - `surfaceChangelog` prints the relevant `docs/CHANGELOG.md` section(s)
|
|
767
|
-
* for the applied range
|
|
1100
|
+
* for the applied range. Reads from the packaged file first; falls back to
|
|
1101
|
+
* a GitHub raw-content fetch via the injectable `fetchChangelog` seam when
|
|
1102
|
+
* the packaged file is absent; emits an actionable link to the GitHub
|
|
1103
|
+
* Releases page when both sources fail (Story #4035).
|
|
768
1104
|
*
|
|
769
1105
|
* Every seam stays injectable on `runUpdate`; these are merely the
|
|
770
1106
|
* no-seam-provided fallbacks, so the existing seam-driven tests stay green.
|
|
@@ -773,8 +1109,9 @@ export async function runUpdate({
|
|
|
773
1109
|
*
|
|
774
1110
|
* The second `deps` argument exposes the **process boundaries** the production
|
|
775
1111
|
* defaults shell out across (`versionRunner` = `npm view`, `runInstall` =
|
|
776
|
-
* the install spawn
|
|
777
|
-
* driven end-to-end with the
|
|
1112
|
+
* the install spawn, `spawnFn` = the phase-spawn boundary) plus `fs` /
|
|
1113
|
+
* `cachePath` / `now`, so the entrypoint can be driven end-to-end with the
|
|
1114
|
+
* network/npm boundary stubbed and no real I/O.
|
|
778
1115
|
* `bin/mandrel.js` calls `run(argv)` with no `deps`, getting the production
|
|
779
1116
|
* wiring; tests pass fakes. The `deps` surface is NOT part of the public
|
|
780
1117
|
* subcommand contract — `bin/mandrel.js` only ever supplies `argv`.
|
|
@@ -787,7 +1124,9 @@ export async function runUpdate({
|
|
|
787
1124
|
* now?: Date,
|
|
788
1125
|
* versionRunner?: () => string,
|
|
789
1126
|
* runInstall?: (installCmd: string, cwd: string) => { status: number, stderr: string },
|
|
1127
|
+
* spawnFn?: typeof spawnSync,
|
|
790
1128
|
* changelogPath?: string,
|
|
1129
|
+
* fetchChangelog?: (version: string) => Promise<string>,
|
|
791
1130
|
* runUpdate?: typeof runUpdate,
|
|
792
1131
|
* runSync?: typeof defaultRunSync,
|
|
793
1132
|
* runMigrations?: typeof defaultRunMigrations,
|
|
@@ -806,7 +1145,9 @@ export default async function run(argv = [], deps = {}) {
|
|
|
806
1145
|
now,
|
|
807
1146
|
versionRunner,
|
|
808
1147
|
runInstall,
|
|
1148
|
+
spawnFn,
|
|
809
1149
|
changelogPath,
|
|
1150
|
+
fetchChangelog,
|
|
810
1151
|
runUpdate: runUpdateImpl = runUpdate,
|
|
811
1152
|
runSync,
|
|
812
1153
|
runMigrations,
|
|
@@ -819,6 +1160,16 @@ export default async function run(argv = [], deps = {}) {
|
|
|
819
1160
|
|
|
820
1161
|
const current = deps.currentVersion ?? defaultCurrentVersion(fs);
|
|
821
1162
|
|
|
1163
|
+
// The production spawnPhase default: spawn each post-install phase from
|
|
1164
|
+
// node_modules/.bin/mandrel (the newly-installed binary). spawnFn is
|
|
1165
|
+
// injectable so tests can stub the spawn boundary without running a real
|
|
1166
|
+
// child process.
|
|
1167
|
+
const productionSpawnPhase = (phase, args, opts) =>
|
|
1168
|
+
defaultSpawnPhase(phase, args, {
|
|
1169
|
+
...opts,
|
|
1170
|
+
...(spawnFn ? { spawnFn } : {}),
|
|
1171
|
+
});
|
|
1172
|
+
|
|
822
1173
|
await runUpdateImpl({
|
|
823
1174
|
argv,
|
|
824
1175
|
currentVersion: current,
|
|
@@ -836,17 +1187,27 @@ export default async function run(argv = [], deps = {}) {
|
|
|
836
1187
|
...(runInstall ? { runInstall } : {}),
|
|
837
1188
|
fs,
|
|
838
1189
|
}),
|
|
1190
|
+
// In the production default export, always wire spawnPhase unless the
|
|
1191
|
+
// caller injects the old-style in-process seams (runSync/runMigrations/
|
|
1192
|
+
// runDoctor). When any old-style seam is present, fall back to in-process
|
|
1193
|
+
// behavior to preserve the full backward-compat surface that the
|
|
1194
|
+
// entrypoint test (update-entrypoint.test.js) relies on.
|
|
1195
|
+
...(runSync || runMigrations || runDoctor
|
|
1196
|
+
? {
|
|
1197
|
+
...(runSync ? { runSync } : {}),
|
|
1198
|
+
...(runMigrations ? { runMigrations } : {}),
|
|
1199
|
+
...(runDoctor ? { runDoctor } : {}),
|
|
1200
|
+
}
|
|
1201
|
+
: { spawnPhase: productionSpawnPhase }),
|
|
839
1202
|
surfaceChangelog: (target) =>
|
|
840
1203
|
defaultSurfaceChangelog(target, {
|
|
841
1204
|
current,
|
|
842
1205
|
fs,
|
|
843
1206
|
...(changelogPath ? { changelogPath } : {}),
|
|
1207
|
+
...(fetchChangelog ? { fetchChangelog } : {}),
|
|
844
1208
|
...(write ? { write } : {}),
|
|
845
1209
|
...(writeErr ? { writeErr } : {}),
|
|
846
1210
|
}),
|
|
847
|
-
...(runSync ? { runSync } : {}),
|
|
848
|
-
...(runMigrations ? { runMigrations } : {}),
|
|
849
|
-
...(runDoctor ? { runDoctor } : {}),
|
|
850
1211
|
...(write ? { write } : {}),
|
|
851
1212
|
...(writeErr ? { writeErr } : {}),
|
|
852
1213
|
...(exit ? { exit } : {}),
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mandrel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.60.0",
|
|
4
4
|
"description": "Claude Code-first opinionated workflow framework: instructions, personas, skills, and SDLC workflows that govern AI coding assistants.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
".agents/",
|
|
8
8
|
"bin/",
|
|
9
|
+
"docs/CHANGELOG.md",
|
|
9
10
|
"lib/"
|
|
10
11
|
],
|
|
11
12
|
"publishConfig": {
|