mandrel 1.69.0 → 1.71.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 +7 -7
- package/.agents/docs/SDLC.md +4 -5
- package/.agents/docs/configuration.md +9 -9
- package/.agents/docs/workflows.md +4 -6
- package/.agents/schemas/qa-finding.schema.json +1 -1
- package/.agents/scripts/apply-quality-bootstrap.js +79 -0
- package/.agents/scripts/audit-labels-bootstrap.js +52 -30
- package/.agents/scripts/audit-to-stories.js +54 -0
- package/.agents/scripts/bootstrap.js +13 -3
- package/.agents/scripts/generate-config-docs.js +189 -94
- package/.agents/scripts/lib/audit-suite/findings.js +0 -4
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
- package/.agents/scripts/lib/baseline-snapshot.js +163 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +0 -4
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +1 -1
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/defaults.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/config-resolver.js +1 -1
- package/.agents/scripts/lib/crap-utils.js +281 -0
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -84
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
- package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
- package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +1 -1
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +1 -1
- package/.agents/scripts/lib/story-body/story-body.js +110 -65
- package/.agents/scripts/lib/test-tiers.js +13 -7
- package/.agents/scripts/lib/wave-runner/tick.js +177 -53
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
- package/.agents/scripts/mandrel-update-preflight.js +235 -0
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/scripts/sync-agentrc.js +2 -2
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +3 -3
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +4 -4
- package/.agents/workflows/git-deliver.md +298 -0
- package/.agents/workflows/helpers/epic-testing.md +6 -6
- package/.agents/workflows/helpers/{agents-sync-config.md → mandrel-sync-config.md} +5 -4
- package/.agents/workflows/{agents-update.md → mandrel-update.md} +210 -33
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/{qa-run-harness.md → qa-run.md} +5 -5
- package/README.md +40 -0
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
- package/.agents/workflows/git-commit-all.md +0 -15
- package/.agents/workflows/git-pr-all.md +0 -281
- package/.agents/workflows/git-push.md +0 -63
package/lib/cli/update.js
CHANGED
|
@@ -48,13 +48,9 @@
|
|
|
48
48
|
* code) stays in the parent process; only the version-sensitive phases run from
|
|
49
49
|
* the new bin. The `spawnPhase` seam makes the child-process boundary fully
|
|
50
50
|
* injectable so tests can verify the re-exec path without a real npm install.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
51
|
+
* It is the **only** post-install execution path — tests stub the spawn
|
|
52
|
+
* boundary rather than swapping in an in-process implementation (No-Shim:
|
|
53
|
+
* `.agents/rules/git-conventions.md` § Contract Cutovers).
|
|
58
54
|
*
|
|
59
55
|
* ## No git mutation
|
|
60
56
|
*
|
|
@@ -97,22 +93,14 @@
|
|
|
97
93
|
* receives `(target, { installCmd })`
|
|
98
94
|
* - `spawnPhase` — async, spawns a post-install phase from the new
|
|
99
95
|
* binary; receives `(phase, args, { binPath, cwd })`
|
|
100
|
-
* and returns `{ ok, stdout, stderr }`.
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* phases. See § Re-exec of post-install phases.
|
|
104
|
-
* - `runSync` — re-materializes ./.agents/ (lib/cli/sync.js).
|
|
105
|
-
* Used when `spawnPhase` is NOT injected (backward
|
|
106
|
-
* compat for tests that pre-date Story #4034).
|
|
107
|
-
* - `runMigrations` — version-keyed migration runner (lib/migrations).
|
|
108
|
-
* Used when `spawnPhase` is NOT injected.
|
|
109
|
-
* - `runDoctor` — async, returns { ok, results } from the registry.
|
|
110
|
-
* Used when `spawnPhase` is NOT injected.
|
|
96
|
+
* and returns `{ ok, stdout, stderr }`. This is the
|
|
97
|
+
* sole post-install execution path. See § Re-exec of
|
|
98
|
+
* post-install phases.
|
|
111
99
|
* - `surfaceChangelog` — emits the target changelog section
|
|
112
100
|
* - `write` / `writeErr` — stdout / stderr sinks
|
|
113
101
|
* - `exit` — process.exit replacement
|
|
114
102
|
* - `cwd` — process.cwd() replacement (used to resolve the
|
|
115
|
-
* new binary path
|
|
103
|
+
* new binary path the post-install phases spawn from)
|
|
116
104
|
*
|
|
117
105
|
* Security (security-baseline § 5 — Data Leakage & Logging): logs only version
|
|
118
106
|
* strings and step names. No tokens, credentials, or env
|
|
@@ -147,9 +135,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
147
135
|
|
|
148
136
|
import { detectPackageManagerWithWorkspace } from '../../.agents/scripts/lib/detect-package-manager.js';
|
|
149
137
|
import { runInstallCommand } from '../../.agents/scripts/lib/install-cmd-parser.js';
|
|
150
|
-
import {
|
|
151
|
-
import { registry, runAgentsDrift } from './registry.js';
|
|
152
|
-
import { runSync as defaultRunSync } from './sync.js';
|
|
138
|
+
import { runAgentsDrift } from './registry.js';
|
|
153
139
|
import { isStale } from './version-check.js';
|
|
154
140
|
import { compareVersions } from './version-helpers.js';
|
|
155
141
|
|
|
@@ -632,25 +618,6 @@ function parseChangelogSections(raw) {
|
|
|
632
618
|
return sections;
|
|
633
619
|
}
|
|
634
620
|
|
|
635
|
-
/**
|
|
636
|
-
* Default doctor seam: run every check in the registry sequentially and
|
|
637
|
-
* report whether all passed. Mirrors lib/cli/doctor.js's pass accounting
|
|
638
|
-
* without the formatted report (the orchestrator owns its own output).
|
|
639
|
-
*
|
|
640
|
-
* This is used only when `spawnPhase` is NOT injected (backward-compat path).
|
|
641
|
-
*
|
|
642
|
-
* @param {{ checks?: typeof registry }} [opts]
|
|
643
|
-
* @returns {Promise<{ ok: boolean, results: Array<{ name: string, ok: boolean }> }>}
|
|
644
|
-
*/
|
|
645
|
-
async function defaultRunDoctor({ checks = registry } = {}) {
|
|
646
|
-
const results = [];
|
|
647
|
-
for (const check of checks) {
|
|
648
|
-
const r = await check.run();
|
|
649
|
-
results.push({ name: check.name, ok: Boolean(r.ok) });
|
|
650
|
-
}
|
|
651
|
-
return { ok: results.every((r) => r.ok), results };
|
|
652
|
-
}
|
|
653
|
-
|
|
654
621
|
/**
|
|
655
622
|
* Resolve the path to the `mandrel` binary inside `node_modules/.bin/` for the
|
|
656
623
|
* given project root. On Windows the binary is a `.cmd` shim; on POSIX it is a
|
|
@@ -735,6 +702,144 @@ const STEP_PLAN = [
|
|
|
735
702
|
'surface changelog',
|
|
736
703
|
];
|
|
737
704
|
|
|
705
|
+
/**
|
|
706
|
+
* The ordered post-install phase descriptors for a full upgrade. Each entry is
|
|
707
|
+
* a plain value (no I/O) describing one step the executor drives:
|
|
708
|
+
*
|
|
709
|
+
* - `kind: 'npm-update'` — bump the dependency via the `npmUpdate` seam.
|
|
710
|
+
* - `kind: 'spawn'` — spawn `phase`/`args` from the new binary; a
|
|
711
|
+
* non-zero exit is fatal and throws `failMessage`.
|
|
712
|
+
* - `kind: 'doctor'` — spawn `doctor` from the new binary; a non-zero
|
|
713
|
+
* exit is *soft* (maps to `action: 'doctor-failed'`
|
|
714
|
+
* + exit 1), so it carries no `failMessage`.
|
|
715
|
+
*
|
|
716
|
+
* `label` is the name pushed into the run's `stepsRun[]` (the external return
|
|
717
|
+
* contract). `migrate` is the only phase whose argv depends on the version
|
|
718
|
+
* range, so its descriptor is built per-plan in `planUpdate`.
|
|
719
|
+
*
|
|
720
|
+
* @param {string} current
|
|
721
|
+
* @param {string} target
|
|
722
|
+
* @returns {Array<{ kind: 'npm-update' | 'spawn' | 'doctor', label: string, phase?: string, args?: string[], failMessage?: string }>}
|
|
723
|
+
*/
|
|
724
|
+
function fullUpgradeSteps(current, target) {
|
|
725
|
+
return [
|
|
726
|
+
{ kind: 'npm-update', label: 'npm-update' },
|
|
727
|
+
{
|
|
728
|
+
kind: 'spawn',
|
|
729
|
+
phase: 'sync',
|
|
730
|
+
args: [],
|
|
731
|
+
label: 'runSync',
|
|
732
|
+
failMessage:
|
|
733
|
+
'mandrel update: `mandrel sync` from new binary exited non-zero — ' +
|
|
734
|
+
'the .agents/ materialization may be incomplete. ' +
|
|
735
|
+
'Run `mandrel sync` manually to restore.',
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
kind: 'spawn',
|
|
739
|
+
phase: 'sync-commands',
|
|
740
|
+
args: [],
|
|
741
|
+
label: 'sync-commands',
|
|
742
|
+
failMessage:
|
|
743
|
+
'mandrel update: `mandrel sync-commands` from new binary exited non-zero — ' +
|
|
744
|
+
'the .claude/commands/ tree may be out of sync. ' +
|
|
745
|
+
'Run `npm run sync:commands` manually to restore.',
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
kind: 'spawn',
|
|
749
|
+
phase: 'migrate',
|
|
750
|
+
args: ['--from', current, '--to', target],
|
|
751
|
+
label: 'runMigrations',
|
|
752
|
+
failMessage:
|
|
753
|
+
'mandrel update: `mandrel migrate` from new binary exited non-zero — ' +
|
|
754
|
+
`some migrations for v${current} → v${target} may not have applied. ` +
|
|
755
|
+
`Run \`mandrel migrate --from ${current} --to ${target}\` manually to retry.`,
|
|
756
|
+
},
|
|
757
|
+
{ kind: 'doctor', phase: 'doctor', args: [], label: 'doctor' },
|
|
758
|
+
];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* The ordered phase descriptors for a drift-heal (version already current, but
|
|
763
|
+
* `.agents/` is stale). No npm-update, no migrations, no doctor — only the two
|
|
764
|
+
* sync phases re-materialize the payload from the already-installed binary.
|
|
765
|
+
*
|
|
766
|
+
* @returns {Array<{ kind: 'spawn', phase: string, args: string[], label: string, failMessage: string }>}
|
|
767
|
+
*/
|
|
768
|
+
function driftHealSteps() {
|
|
769
|
+
return [
|
|
770
|
+
{
|
|
771
|
+
kind: 'spawn',
|
|
772
|
+
phase: 'sync',
|
|
773
|
+
args: [],
|
|
774
|
+
label: 'runSync',
|
|
775
|
+
failMessage:
|
|
776
|
+
'mandrel update: `mandrel sync` from installed binary exited non-zero — ' +
|
|
777
|
+
'the .agents/ materialization may be incomplete. ' +
|
|
778
|
+
'Run `mandrel sync` manually to restore.',
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
kind: 'spawn',
|
|
782
|
+
phase: 'sync-commands',
|
|
783
|
+
args: [],
|
|
784
|
+
label: 'sync-commands',
|
|
785
|
+
failMessage:
|
|
786
|
+
'mandrel update: `mandrel sync-commands` from installed binary exited non-zero — ' +
|
|
787
|
+
'the .claude/commands/ tree may be out of sync. ' +
|
|
788
|
+
'Run `npm run sync:commands` manually to restore.',
|
|
789
|
+
},
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Pure decision function for `mandrel update`: given the resolved version
|
|
795
|
+
* inputs and the two flags, decide which of the four actions to take and the
|
|
796
|
+
* ordered phase plan for that action. **No I/O** — no filesystem, child
|
|
797
|
+
* process, network, `write`, or `exit`. This isolates the scheduler-style
|
|
798
|
+
* branch-selection and step-sequencing logic (the surface under review in
|
|
799
|
+
* Story #4182 / audit::architecture) so it can be exercised as a table over
|
|
800
|
+
* plain inputs rather than by running the whole async orchestration with every
|
|
801
|
+
* seam stubbed.
|
|
802
|
+
*
|
|
803
|
+
* The four actions:
|
|
804
|
+
*
|
|
805
|
+
* - `up-to-date` — version is current and no drift. True no-op; `steps: []`.
|
|
806
|
+
* - `dry-run` — `dryRun` is set. `steps: []` (nothing is executed); the
|
|
807
|
+
* `variant` distinguishes the drift-heal preview from the
|
|
808
|
+
* full-upgrade preview so the executor prints the right plan.
|
|
809
|
+
* - `resynced` — version is current but drift detected. Heal via the two
|
|
810
|
+
* sync phases (`driftHealSteps()`).
|
|
811
|
+
* - `updated` — a newer version is available. Full upgrade
|
|
812
|
+
* (`fullUpgradeSteps(current, target)`).
|
|
813
|
+
*
|
|
814
|
+
* @param {{ current: string, target: string, dryRun: boolean, hasDrift: boolean }} input
|
|
815
|
+
* @returns {{
|
|
816
|
+
* action: 'up-to-date' | 'dry-run' | 'resynced' | 'updated',
|
|
817
|
+
* steps: Array<{ kind: 'npm-update' | 'spawn' | 'doctor', label: string, phase?: string, args?: string[], failMessage?: string }>,
|
|
818
|
+
* variant?: 'drift-heal' | 'full-upgrade',
|
|
819
|
+
* }}
|
|
820
|
+
*/
|
|
821
|
+
export function planUpdate({ current, target, dryRun, hasDrift }) {
|
|
822
|
+
const versionCurrent = compareVersions(target, current) <= 0;
|
|
823
|
+
|
|
824
|
+
if (versionCurrent) {
|
|
825
|
+
// Version is already newest. The only remaining question is drift.
|
|
826
|
+
if (!hasDrift) {
|
|
827
|
+
return { action: 'up-to-date', steps: [] };
|
|
828
|
+
}
|
|
829
|
+
if (dryRun) {
|
|
830
|
+
return { action: 'dry-run', steps: [], variant: 'drift-heal' };
|
|
831
|
+
}
|
|
832
|
+
return { action: 'resynced', steps: driftHealSteps() };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// A newer version is available — full upgrade (drift is irrelevant here; the
|
|
836
|
+
// post-upgrade doctor phase re-checks materialization).
|
|
837
|
+
if (dryRun) {
|
|
838
|
+
return { action: 'dry-run', steps: [], variant: 'full-upgrade' };
|
|
839
|
+
}
|
|
840
|
+
return { action: 'updated', steps: fullUpgradeSteps(current, target) };
|
|
841
|
+
}
|
|
842
|
+
|
|
738
843
|
/**
|
|
739
844
|
* Extract the `--install-cmd "<cmd>"` value from the subcommand argv. Accepts
|
|
740
845
|
* both the space form (`--install-cmd npm install …`, captured as the single
|
|
@@ -761,9 +866,125 @@ function parseInstallCmdFlag(argv) {
|
|
|
761
866
|
return undefined;
|
|
762
867
|
}
|
|
763
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Resolve the drift signal for the no-op short-circuit. Prefers the injected
|
|
871
|
+
* `checkDrift` seam (unit-test friendly); falls back to the production
|
|
872
|
+
* `runAgentsDrift` helper. Only consulted when the installed version is already
|
|
873
|
+
* the newest (Story #4065) — a real version bump skips drift entirely (the
|
|
874
|
+
* post-upgrade doctor phase re-checks materialization).
|
|
875
|
+
*
|
|
876
|
+
* @param {(() => boolean | Promise<boolean>) | undefined} checkDrift
|
|
877
|
+
* @returns {Promise<boolean>}
|
|
878
|
+
*/
|
|
879
|
+
async function resolveDrift(checkDrift) {
|
|
880
|
+
const driftProbe =
|
|
881
|
+
typeof checkDrift === 'function' ? checkDrift : () => !runAgentsDrift().ok;
|
|
882
|
+
return Boolean(await driftProbe());
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Execute the ordered phase plan returned by `planUpdate` for the `updated` /
|
|
887
|
+
* `resynced` actions. This is the thin side-effecting shell: it owns the
|
|
888
|
+
* `npmUpdate` seam call, the `spawnPhase` re-exec boundary, the per-step
|
|
889
|
+
* `stepsRun` accounting, and the soft doctor-fail (`exit(1)` +
|
|
890
|
+
* `action: 'doctor-failed'`). The branch-selection logic that produced `steps`
|
|
891
|
+
* lives in the pure `planUpdate`.
|
|
892
|
+
*
|
|
893
|
+
* @param {{
|
|
894
|
+
* steps: Array<{ kind: 'npm-update' | 'spawn' | 'doctor', label: string, phase?: string, args?: string[], failMessage?: string }>,
|
|
895
|
+
* target: string,
|
|
896
|
+
* installCmd: string | undefined,
|
|
897
|
+
* npmUpdate: ((version: string, opts: { installCmd?: string }) => unknown | Promise<unknown>) | undefined,
|
|
898
|
+
* spawnPhase: ((phase: string, args: string[], opts: object) => { ok: boolean } | Promise<{ ok: boolean }>) | undefined,
|
|
899
|
+
* surfaceChangelog: ((version: string) => unknown | Promise<unknown>) | undefined,
|
|
900
|
+
* binPath: string,
|
|
901
|
+
* projectRoot: string,
|
|
902
|
+
* write: (s: string) => void,
|
|
903
|
+
* writeErr: (s: string) => void,
|
|
904
|
+
* exit: (code: number) => void,
|
|
905
|
+
* }} ctx
|
|
906
|
+
* @returns {Promise<{ stepsRun: string[], doctorOk: boolean }>}
|
|
907
|
+
*/
|
|
908
|
+
async function executePlan({
|
|
909
|
+
steps,
|
|
910
|
+
target,
|
|
911
|
+
installCmd,
|
|
912
|
+
npmUpdate,
|
|
913
|
+
spawnPhase,
|
|
914
|
+
surfaceChangelog,
|
|
915
|
+
binPath,
|
|
916
|
+
projectRoot,
|
|
917
|
+
write,
|
|
918
|
+
writeErr,
|
|
919
|
+
exit,
|
|
920
|
+
}) {
|
|
921
|
+
const stepsRun = [];
|
|
922
|
+
let doctorOk = true;
|
|
923
|
+
|
|
924
|
+
for (const step of steps) {
|
|
925
|
+
if (step.kind === 'npm-update') {
|
|
926
|
+
// Bump the dependency. The lockfile change is left STAGED on disk; this
|
|
927
|
+
// module never commits.
|
|
928
|
+
if (typeof npmUpdate !== 'function') {
|
|
929
|
+
throw new Error(
|
|
930
|
+
'mandrel update: npmUpdate seam is required to bump the dependency',
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
// eslint-disable-next-line no-await-in-loop
|
|
934
|
+
await npmUpdate(target, { installCmd });
|
|
935
|
+
stepsRun.push(step.label);
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Both 'spawn' and 'doctor' kinds run a post-install phase from the
|
|
940
|
+
// newly-installed binary (the Story #4034 re-exec boundary), so the new
|
|
941
|
+
// package's module code — not the old loaded module — executes.
|
|
942
|
+
// eslint-disable-next-line no-await-in-loop
|
|
943
|
+
const result = await spawnPhase(step.phase, step.args, {
|
|
944
|
+
binPath,
|
|
945
|
+
cwd: projectRoot,
|
|
946
|
+
write,
|
|
947
|
+
writeErr,
|
|
948
|
+
});
|
|
949
|
+
stepsRun.push(step.label);
|
|
950
|
+
|
|
951
|
+
if (step.kind === 'doctor') {
|
|
952
|
+
// Doctor failure is SOFT: record it, keep going to surface the changelog,
|
|
953
|
+
// then map to exit(1) + doctor-failed by the caller.
|
|
954
|
+
doctorOk = result.ok;
|
|
955
|
+
} else if (!result.ok) {
|
|
956
|
+
// sync / sync-commands / migrate failures are FATAL.
|
|
957
|
+
throw new Error(step.failMessage);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Surface the target changelog (best-effort; optional seam). Runs even when
|
|
962
|
+
// doctor failed, so the operator still sees the changelog for the version
|
|
963
|
+
// that landed on disk.
|
|
964
|
+
if (typeof surfaceChangelog === 'function') {
|
|
965
|
+
await surfaceChangelog(target);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (!doctorOk) {
|
|
969
|
+
writeErr(
|
|
970
|
+
`mandrel update: upgraded to v${target} but doctor reported failures.\n` +
|
|
971
|
+
' → Run `mandrel doctor` for remedies.\n',
|
|
972
|
+
);
|
|
973
|
+
exit(1);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return { stepsRun, doctorOk };
|
|
977
|
+
}
|
|
978
|
+
|
|
764
979
|
/**
|
|
765
980
|
* Run the `mandrel update` orchestration cycle.
|
|
766
981
|
*
|
|
982
|
+
* The cycle is split into a pure decision (`planUpdate`) and a side-effecting
|
|
983
|
+
* shell (this function + `executePlan`). `runUpdate` resolves the inputs
|
|
984
|
+
* (current / target / drift) through the injectable seams, calls `planUpdate`
|
|
985
|
+
* to select the action and its ordered phase plan, then drives the plan through
|
|
986
|
+
* the `spawnPhase` / `write` / `exit` shell.
|
|
987
|
+
*
|
|
767
988
|
* @param {{
|
|
768
989
|
* argv?: string[],
|
|
769
990
|
* currentVersion?: string | (() => string),
|
|
@@ -771,9 +992,6 @@ function parseInstallCmdFlag(argv) {
|
|
|
771
992
|
* npmUpdate?: (version: string, opts: { installCmd?: string }) => unknown | Promise<unknown>,
|
|
772
993
|
* checkDrift?: () => (boolean | Promise<boolean>),
|
|
773
994
|
* 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 },
|
|
774
|
-
* runSync?: typeof defaultRunSync,
|
|
775
|
-
* runMigrations?: typeof defaultRunMigrations,
|
|
776
|
-
* runDoctor?: typeof defaultRunDoctor,
|
|
777
995
|
* surfaceChangelog?: (version: string) => unknown | Promise<unknown>,
|
|
778
996
|
* write?: (s: string) => void,
|
|
779
997
|
* writeErr?: (s: string) => void,
|
|
@@ -796,9 +1014,6 @@ export async function runUpdate({
|
|
|
796
1014
|
npmUpdate,
|
|
797
1015
|
checkDrift,
|
|
798
1016
|
spawnPhase,
|
|
799
|
-
runSync = defaultRunSync,
|
|
800
|
-
runMigrations = defaultRunMigrations,
|
|
801
|
-
runDoctor = defaultRunDoctor,
|
|
802
1017
|
surfaceChangelog,
|
|
803
1018
|
write = (s) => process.stdout.write(s),
|
|
804
1019
|
writeErr = (s) => process.stderr.write(s),
|
|
@@ -820,37 +1035,32 @@ export async function runUpdate({
|
|
|
820
1035
|
}
|
|
821
1036
|
const target = String(await resolveTargetVersion());
|
|
822
1037
|
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
// Resolve the drift probe: prefer the injected checkDrift seam (unit-test
|
|
831
|
-
// friendly); fall back to the production runAgentsDrift helper.
|
|
832
|
-
const driftProbe =
|
|
833
|
-
typeof checkDrift === 'function'
|
|
834
|
-
? checkDrift
|
|
835
|
-
: () => !runAgentsDrift().ok;
|
|
836
|
-
const hasDrift = await driftProbe();
|
|
1038
|
+
// Drift only matters when the installed version is already the newest. Probe
|
|
1039
|
+
// it solely on that branch so a real version bump never calls the drift seam
|
|
1040
|
+
// (Story #4065).
|
|
1041
|
+
const hasDrift =
|
|
1042
|
+
compareVersions(target, current) <= 0
|
|
1043
|
+
? await resolveDrift(checkDrift)
|
|
1044
|
+
: false;
|
|
837
1045
|
|
|
838
|
-
|
|
839
|
-
write(`✅ Already up to date (v${current} is the newest version).\n`);
|
|
840
|
-
return {
|
|
841
|
-
ok: true,
|
|
842
|
-
action: 'up-to-date',
|
|
843
|
-
currentVersion: current,
|
|
844
|
-
targetVersion: target,
|
|
845
|
-
stepsRun: [],
|
|
846
|
-
dryRun,
|
|
847
|
-
};
|
|
848
|
-
}
|
|
1046
|
+
const plan = planUpdate({ current, target, dryRun, hasDrift });
|
|
849
1047
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1048
|
+
// --- up-to-date: true no-op ----------------------------------------------
|
|
1049
|
+
if (plan.action === 'up-to-date') {
|
|
1050
|
+
write(`✅ Already up to date (v${current} is the newest version).\n`);
|
|
1051
|
+
return {
|
|
1052
|
+
ok: true,
|
|
1053
|
+
action: 'up-to-date',
|
|
1054
|
+
currentVersion: current,
|
|
1055
|
+
targetVersion: target,
|
|
1056
|
+
stepsRun: [],
|
|
1057
|
+
dryRun,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// --- dry-run: print the plan, execute nothing -----------------------------
|
|
1062
|
+
if (plan.action === 'dry-run') {
|
|
1063
|
+
if (plan.variant === 'drift-heal') {
|
|
854
1064
|
write(
|
|
855
1065
|
`mandrel update — drift detected, sync heal planned (v${current} is already current)\n`,
|
|
856
1066
|
);
|
|
@@ -861,62 +1071,50 @@ export async function runUpdate({
|
|
|
861
1071
|
' 2. sync-commands — regenerate .claude/commands/ from .agents/workflows/\n',
|
|
862
1072
|
);
|
|
863
1073
|
write('Dry run: no files written.\n');
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
dryRun: true,
|
|
871
|
-
};
|
|
1074
|
+
} else {
|
|
1075
|
+
write(`mandrel update — planned upgrade v${current} → v${target}\n`);
|
|
1076
|
+
STEP_PLAN.forEach((step, i) => {
|
|
1077
|
+
write(` ${i + 1}. ${step}\n`);
|
|
1078
|
+
});
|
|
1079
|
+
write('Dry run: no files written, no dependency bumped.\n');
|
|
872
1080
|
}
|
|
1081
|
+
return {
|
|
1082
|
+
ok: true,
|
|
1083
|
+
action: 'dry-run',
|
|
1084
|
+
currentVersion: current,
|
|
1085
|
+
targetVersion: target,
|
|
1086
|
+
stepsRun: [],
|
|
1087
|
+
dryRun: true,
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// --- resynced / updated: execute the phase plan ---------------------------
|
|
1092
|
+
const projectRoot = cwd();
|
|
1093
|
+
const binPath = resolveNewBinPath(projectRoot);
|
|
873
1094
|
|
|
1095
|
+
if (plan.action === 'resynced') {
|
|
874
1096
|
write(
|
|
875
1097
|
`Healing .agents/ drift (v${current} is already current, but .agents/ is stale)…\n`,
|
|
876
1098
|
);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if (useReExec) {
|
|
882
|
-
const projectRoot = cwd();
|
|
883
|
-
const binPath = resolveNewBinPath(projectRoot);
|
|
884
|
-
|
|
885
|
-
const syncResult = await spawnPhase('sync', [], {
|
|
886
|
-
binPath,
|
|
887
|
-
cwd: projectRoot,
|
|
888
|
-
write,
|
|
889
|
-
writeErr,
|
|
890
|
-
});
|
|
891
|
-
if (!syncResult.ok) {
|
|
892
|
-
throw new Error(
|
|
893
|
-
`mandrel update: \`mandrel sync\` from installed binary exited non-zero — ` +
|
|
894
|
-
'the .agents/ materialization may be incomplete. ' +
|
|
895
|
-
'Run `mandrel sync` manually to restore.',
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
stepsRun.push('runSync');
|
|
1099
|
+
} else {
|
|
1100
|
+
write(`Updating v${current} → v${target}…\n`);
|
|
1101
|
+
}
|
|
899
1102
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
stepsRun.push('sync-commands');
|
|
914
|
-
} else {
|
|
915
|
-
// In-process backward-compat path.
|
|
916
|
-
runSync({ argv: [] });
|
|
917
|
-
stepsRun.push('runSync');
|
|
918
|
-
}
|
|
1103
|
+
const { stepsRun, doctorOk } = await executePlan({
|
|
1104
|
+
steps: plan.steps,
|
|
1105
|
+
target,
|
|
1106
|
+
installCmd,
|
|
1107
|
+
npmUpdate,
|
|
1108
|
+
spawnPhase,
|
|
1109
|
+
surfaceChangelog,
|
|
1110
|
+
binPath,
|
|
1111
|
+
projectRoot,
|
|
1112
|
+
write,
|
|
1113
|
+
writeErr,
|
|
1114
|
+
exit,
|
|
1115
|
+
});
|
|
919
1116
|
|
|
1117
|
+
if (plan.action === 'resynced') {
|
|
920
1118
|
write(
|
|
921
1119
|
`✅ Healed .agents/ drift (v${current}). The materialized payload is now current.\n`,
|
|
922
1120
|
);
|
|
@@ -930,194 +1128,18 @@ export async function runUpdate({
|
|
|
930
1128
|
};
|
|
931
1129
|
}
|
|
932
1130
|
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
// write nothing to disk.
|
|
936
|
-
if (dryRun) {
|
|
937
|
-
write(`mandrel update — planned upgrade v${current} → v${target}\n`);
|
|
938
|
-
STEP_PLAN.forEach((step, i) => {
|
|
939
|
-
write(` ${i + 1}. ${step}\n`);
|
|
940
|
-
});
|
|
941
|
-
write('Dry run: no files written, no dependency bumped.\n');
|
|
1131
|
+
// plan.action === 'updated'
|
|
1132
|
+
if (!doctorOk) {
|
|
942
1133
|
return {
|
|
943
|
-
ok:
|
|
944
|
-
action: '
|
|
1134
|
+
ok: false,
|
|
1135
|
+
action: 'doctor-failed',
|
|
945
1136
|
currentVersion: current,
|
|
946
1137
|
targetVersion: target,
|
|
947
|
-
stepsRun
|
|
948
|
-
dryRun:
|
|
1138
|
+
stepsRun,
|
|
1139
|
+
dryRun: false,
|
|
949
1140
|
};
|
|
950
1141
|
}
|
|
951
1142
|
|
|
952
|
-
write(`Updating v${current} → v${target}…\n`);
|
|
953
|
-
|
|
954
|
-
const stepsRun = [];
|
|
955
|
-
|
|
956
|
-
// 1. npm update — bump the dependency. The lockfile change is left STAGED
|
|
957
|
-
// on disk; this module never commits.
|
|
958
|
-
if (typeof npmUpdate !== 'function') {
|
|
959
|
-
throw new Error(
|
|
960
|
-
'mandrel update: npmUpdate seam is required to bump the dependency',
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
await npmUpdate(target, { installCmd });
|
|
964
|
-
stepsRun.push('npm-update');
|
|
965
|
-
|
|
966
|
-
// Decide whether to use the re-exec path (spawnPhase) or the in-process
|
|
967
|
-
// backward-compat seams (runSync / runMigrations / runDoctor).
|
|
968
|
-
//
|
|
969
|
-
// spawnPhase injected → re-exec path: all post-install phases run from the
|
|
970
|
-
// newly-installed binary. This is the production path and is what fixes
|
|
971
|
-
// the stale-materialization bug (Story #4034).
|
|
972
|
-
//
|
|
973
|
-
// spawnPhase NOT injected → in-process path: the original pre-Story-#4034
|
|
974
|
-
// behaviour. Tests that pre-date this change inject runSync/runMigrations/
|
|
975
|
-
// runDoctor and rely on the in-process path; they stay green without any
|
|
976
|
-
// modification.
|
|
977
|
-
const useReExec = typeof spawnPhase === 'function';
|
|
978
|
-
|
|
979
|
-
if (useReExec) {
|
|
980
|
-
// Re-exec path: post-install phases run from the new binary.
|
|
981
|
-
const projectRoot = cwd();
|
|
982
|
-
const binPath = resolveNewBinPath(projectRoot);
|
|
983
|
-
|
|
984
|
-
// 2. runSync from new bin — re-materialize ./.agents/ from the freshly
|
|
985
|
-
// installed payload. Running from the new bin ensures the copied files
|
|
986
|
-
// come from the new package's .agents/ tree, not the old loaded module.
|
|
987
|
-
const syncResult = await spawnPhase('sync', [], {
|
|
988
|
-
binPath,
|
|
989
|
-
cwd: projectRoot,
|
|
990
|
-
write,
|
|
991
|
-
writeErr,
|
|
992
|
-
});
|
|
993
|
-
if (!syncResult.ok) {
|
|
994
|
-
throw new Error(
|
|
995
|
-
`mandrel update: \`mandrel sync\` from new binary exited non-zero — ` +
|
|
996
|
-
'the .agents/ materialization may be incomplete. ' +
|
|
997
|
-
'Run `mandrel sync` manually to restore.',
|
|
998
|
-
);
|
|
999
|
-
}
|
|
1000
|
-
stepsRun.push('runSync');
|
|
1001
|
-
|
|
1002
|
-
// 3. sync-commands from new bin — regenerate .claude/commands/ from the
|
|
1003
|
-
// freshly-materialized .agents/workflows/. Running from the new bin
|
|
1004
|
-
// ensures the command tree is consistent with the new payload; an
|
|
1005
|
-
// upstream-renamed workflow will be projected correctly and the old
|
|
1006
|
-
// command file will be reaped. This step must follow runSync so the
|
|
1007
|
-
// workflow sources are up to date before the command tree is rebuilt
|
|
1008
|
-
// (Story #4046 A1c — `commands-in-sync` validates the post-sync state).
|
|
1009
|
-
const syncCommandsResult = await spawnPhase('sync-commands', [], {
|
|
1010
|
-
binPath,
|
|
1011
|
-
cwd: projectRoot,
|
|
1012
|
-
write,
|
|
1013
|
-
writeErr,
|
|
1014
|
-
});
|
|
1015
|
-
if (!syncCommandsResult.ok) {
|
|
1016
|
-
throw new Error(
|
|
1017
|
-
`mandrel update: \`mandrel sync-commands\` from new binary exited non-zero — ` +
|
|
1018
|
-
'the .claude/commands/ tree may be out of sync. ' +
|
|
1019
|
-
'Run `npm run sync:commands` manually to restore.',
|
|
1020
|
-
);
|
|
1021
|
-
}
|
|
1022
|
-
stepsRun.push('sync-commands');
|
|
1023
|
-
|
|
1024
|
-
// 4. runMigrations from new bin — apply version-keyed steps for the
|
|
1025
|
-
// crossed range. The new binary's migration registry contains any steps
|
|
1026
|
-
// added in the target version; the old process's registry does not.
|
|
1027
|
-
const migrateResult = await spawnPhase(
|
|
1028
|
-
'migrate',
|
|
1029
|
-
['--from', current, '--to', target],
|
|
1030
|
-
{ binPath, cwd: projectRoot, write, writeErr },
|
|
1031
|
-
);
|
|
1032
|
-
if (!migrateResult.ok) {
|
|
1033
|
-
throw new Error(
|
|
1034
|
-
`mandrel update: \`mandrel migrate\` from new binary exited non-zero — ` +
|
|
1035
|
-
`some migrations for v${current} → v${target} may not have applied. ` +
|
|
1036
|
-
`Run \`mandrel migrate --from ${current} --to ${target}\` manually to retry.`,
|
|
1037
|
-
);
|
|
1038
|
-
}
|
|
1039
|
-
stepsRun.push('runMigrations');
|
|
1040
|
-
|
|
1041
|
-
// 5. doctor from new bin — verify the resulting install. Running from the
|
|
1042
|
-
// new bin is critical: the agents-drift check compares the materialized
|
|
1043
|
-
// .agents/ against the installed package payload. When the old process
|
|
1044
|
-
// runs this check, it resolves the package root to its own (old) install
|
|
1045
|
-
// dir, so drift against the new payload is invisible. The new binary
|
|
1046
|
-
// resolves the package root to the now-installed new version, producing
|
|
1047
|
-
// an accurate result.
|
|
1048
|
-
const doctorResult = await spawnPhase('doctor', [], {
|
|
1049
|
-
binPath,
|
|
1050
|
-
cwd: projectRoot,
|
|
1051
|
-
write,
|
|
1052
|
-
writeErr,
|
|
1053
|
-
});
|
|
1054
|
-
stepsRun.push('doctor');
|
|
1055
|
-
|
|
1056
|
-
// 6. surface the target changelog (best-effort; optional seam).
|
|
1057
|
-
if (typeof surfaceChangelog === 'function') {
|
|
1058
|
-
await surfaceChangelog(target);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
if (!doctorResult.ok) {
|
|
1062
|
-
writeErr(
|
|
1063
|
-
`mandrel update: upgraded to v${target} but doctor reported failures.\n` +
|
|
1064
|
-
' → Run `mandrel doctor` for remedies.\n',
|
|
1065
|
-
);
|
|
1066
|
-
exit(1);
|
|
1067
|
-
return {
|
|
1068
|
-
ok: false,
|
|
1069
|
-
action: 'doctor-failed',
|
|
1070
|
-
currentVersion: current,
|
|
1071
|
-
targetVersion: target,
|
|
1072
|
-
stepsRun,
|
|
1073
|
-
dryRun: false,
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
} else {
|
|
1077
|
-
// In-process backward-compat path (pre-Story-#4034 behaviour).
|
|
1078
|
-
// Used when no `spawnPhase` seam is injected — preserves full backward
|
|
1079
|
-
// compatibility with existing tests that inject runSync/runMigrations/
|
|
1080
|
-
// runDoctor directly.
|
|
1081
|
-
|
|
1082
|
-
// 2. runSync — re-materialize ./.agents/ from the new payload.
|
|
1083
|
-
runSync({ argv: [] });
|
|
1084
|
-
stepsRun.push('runSync');
|
|
1085
|
-
|
|
1086
|
-
// 3. runMigrations — apply version-keyed steps for the crossed range.
|
|
1087
|
-
// Note: the in-process path (pre-Story-#4034) does not run sync-commands
|
|
1088
|
-
// here because sync-commands runs as a child process and there is no
|
|
1089
|
-
// in-process seam for it. The re-exec path (spawnPhase) handles it.
|
|
1090
|
-
runMigrations({ fromVersion: current, toVersion: target, ctx: {} });
|
|
1091
|
-
stepsRun.push('runMigrations');
|
|
1092
|
-
|
|
1093
|
-
// 4. doctor — verify the resulting install.
|
|
1094
|
-
const doctor = await runDoctor();
|
|
1095
|
-
stepsRun.push('doctor');
|
|
1096
|
-
|
|
1097
|
-
// 5. surface the target changelog (best-effort; optional seam).
|
|
1098
|
-
if (typeof surfaceChangelog === 'function') {
|
|
1099
|
-
await surfaceChangelog(target);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (!doctor.ok) {
|
|
1103
|
-
const failed = doctor.results.filter((r) => !r.ok).map((r) => r.name);
|
|
1104
|
-
writeErr(
|
|
1105
|
-
`mandrel update: upgraded to v${target} but doctor reported failures: ` +
|
|
1106
|
-
`${failed.join(', ')}\n` +
|
|
1107
|
-
' → Run `mandrel doctor` for remedies.\n',
|
|
1108
|
-
);
|
|
1109
|
-
exit(1);
|
|
1110
|
-
return {
|
|
1111
|
-
ok: false,
|
|
1112
|
-
action: 'doctor-failed',
|
|
1113
|
-
currentVersion: current,
|
|
1114
|
-
targetVersion: target,
|
|
1115
|
-
stepsRun,
|
|
1116
|
-
dryRun: false,
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
1143
|
write(`✅ Updated to v${target}. The lockfile bump is staged for review.\n`);
|
|
1122
1144
|
return {
|
|
1123
1145
|
ok: true,
|
|
@@ -1180,9 +1202,6 @@ export async function runUpdate({
|
|
|
1180
1202
|
* changelogPath?: string,
|
|
1181
1203
|
* fetchChangelog?: (version: string) => Promise<string>,
|
|
1182
1204
|
* runUpdate?: typeof runUpdate,
|
|
1183
|
-
* runSync?: typeof defaultRunSync,
|
|
1184
|
-
* runMigrations?: typeof defaultRunMigrations,
|
|
1185
|
-
* runDoctor?: typeof defaultRunDoctor,
|
|
1186
1205
|
* write?: (s: string) => void,
|
|
1187
1206
|
* writeErr?: (s: string) => void,
|
|
1188
1207
|
* exit?: (code: number) => void,
|
|
@@ -1201,9 +1220,6 @@ export default async function run(argv = [], deps = {}) {
|
|
|
1201
1220
|
changelogPath,
|
|
1202
1221
|
fetchChangelog,
|
|
1203
1222
|
runUpdate: runUpdateImpl = runUpdate,
|
|
1204
|
-
runSync,
|
|
1205
|
-
runMigrations,
|
|
1206
|
-
runDoctor,
|
|
1207
1223
|
write = (s) => process.stdout.write(s),
|
|
1208
1224
|
writeErr = (s) => process.stderr.write(s),
|
|
1209
1225
|
exit = (code) => process.exit(code),
|
|
@@ -1212,30 +1228,17 @@ export default async function run(argv = [], deps = {}) {
|
|
|
1212
1228
|
|
|
1213
1229
|
const current = deps.currentVersion ?? defaultCurrentVersion(fs);
|
|
1214
1230
|
|
|
1215
|
-
// The production spawnPhase
|
|
1216
|
-
// node_modules/.bin/mandrel (the newly-installed binary).
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1231
|
+
// The production spawnPhase: spawn each post-install phase from
|
|
1232
|
+
// node_modules/.bin/mandrel (the newly-installed binary). This is the sole
|
|
1233
|
+
// post-install execution path (No-Shim — Story #4182 retired the in-process
|
|
1234
|
+
// runSync/runMigrations/runDoctor seam set). spawnFn is injectable so tests
|
|
1235
|
+
// can stub the spawn boundary without running a real child process.
|
|
1219
1236
|
const productionSpawnPhase = (phase, args, opts) =>
|
|
1220
1237
|
defaultSpawnPhase(phase, args, {
|
|
1221
1238
|
...opts,
|
|
1222
1239
|
...(spawnFn ? { spawnFn } : {}),
|
|
1223
1240
|
});
|
|
1224
1241
|
|
|
1225
|
-
// Resolve which seam set to use for post-install phases. If any old-style
|
|
1226
|
-
// in-process seam (runSync/runMigrations/runDoctor) is injected, fall back
|
|
1227
|
-
// to the pre-Story-#4034 in-process path so the entrypoint test stays green.
|
|
1228
|
-
// Otherwise use the re-exec path (spawnPhase). This is a single ternary
|
|
1229
|
-
// rather than stacked optional spreads (tidy, Story #4046).
|
|
1230
|
-
const phaseSeams =
|
|
1231
|
-
runSync || runMigrations || runDoctor
|
|
1232
|
-
? {
|
|
1233
|
-
...(runSync ? { runSync } : {}),
|
|
1234
|
-
...(runMigrations ? { runMigrations } : {}),
|
|
1235
|
-
...(runDoctor ? { runDoctor } : {}),
|
|
1236
|
-
}
|
|
1237
|
-
: { spawnPhase: productionSpawnPhase };
|
|
1238
|
-
|
|
1239
1242
|
await runUpdateImpl({
|
|
1240
1243
|
argv,
|
|
1241
1244
|
currentVersion: current,
|
|
@@ -1257,7 +1260,7 @@ export default async function run(argv = [], deps = {}) {
|
|
|
1257
1260
|
runInstall: runInstall ?? runInstallCommand,
|
|
1258
1261
|
fs,
|
|
1259
1262
|
}),
|
|
1260
|
-
|
|
1263
|
+
spawnPhase: productionSpawnPhase,
|
|
1261
1264
|
surfaceChangelog: (target) =>
|
|
1262
1265
|
defaultSurfaceChangelog(target, {
|
|
1263
1266
|
current,
|