@yemi33/minions 0.1.2447 → 0.1.2449
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/bin/cli-api-client.js +1 -1
- package/bin/install-internal-minions.js +1382 -44
- package/bin/install-layout.js +150 -0
- package/bin/minions.js +460 -167
- package/dashboard/docs/typography.md +65 -12
- package/dashboard/js/command-center.js +66 -7
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +186 -45
- package/dashboard/js/refresh.js +148 -12
- package/dashboard/js/render-dispatch.js +3 -4
- package/dashboard/js/render-inbox.js +2 -2
- package/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-pipelines.js +14 -0
- package/dashboard/js/render-plans.js +57 -9
- package/dashboard/js/render-prd.js +132 -23
- package/dashboard/js/render-prs.js +195 -166
- package/dashboard/js/render-schedules.js +63 -3
- package/dashboard/js/render-utils.js +3 -3
- package/dashboard/js/render-watches.js +19 -3
- package/dashboard/js/render-work-items.js +238 -30
- package/dashboard/js/settings.js +205 -54
- package/dashboard/js/utils.js +51 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/qa.html +1 -16
- package/dashboard/pages/work.html +40 -0
- package/dashboard/shared/cc-limits.js +79 -0
- package/dashboard/shared/pr-filters.js +21 -38
- package/dashboard/shared/project-git-summary.js +1 -1
- package/dashboard/shared/record-filters.js +169 -0
- package/dashboard/shared/watches-source.js +1 -1
- package/dashboard/shared/welcome-popup.js +1 -1
- package/dashboard/shared/wi-filters.js +302 -0
- package/dashboard/slim/body.html +1 -0
- package/dashboard/slim/js/command-send.js +26 -0
- package/dashboard/slim/js/modals-tiles.js +380 -39
- package/dashboard/slim/js/status.js +13 -21
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/panel-bootstrap.js +6 -2
- package/dashboard/slim/styles.css +38 -0
- package/dashboard/styles.css +159 -55
- package/dashboard-build.js +52 -6
- package/dashboard.js +956 -423
- package/docs/README.md +11 -6
- package/docs/api-errors.md +2 -2
- package/docs/architecture-review-2026-07-09.md +1 -1
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +18 -9
- package/docs/branch-derivation.md +4 -4
- package/docs/capture-demos.js +39 -2
- package/docs/ci-runner-canary.md +123 -0
- package/docs/claude-md-propagation.md +2 -2
- package/docs/cloud-agent-dispatch.md +204 -0
- package/docs/command-center.md +7 -7
- package/docs/completion-reports.md +43 -20
- package/docs/constants.md +10 -3
- package/docs/constellation-bridge.md +134 -6
- package/docs/constellation-style-telemetry.md +4 -4
- package/docs/contracts/capability-protocol.v1.json +165 -0
- package/docs/cooldown-merge-semantics.md +12 -12
- package/docs/copilot-cli-schema.md +7 -7
- package/docs/cross-repo-plans.md +10 -10
- package/docs/dead-code-audit-retractions.md +5 -5
- package/docs/default-branch-ci.md +173 -0
- package/docs/deprecated.json +31 -31
- package/docs/design-inbox-entries-schema.md +3 -3
- package/docs/design-language.md +1051 -0
- package/docs/design-state-storage.md +11 -11
- package/docs/diagnostics-crash-reports.md +9 -9
- package/docs/diagnostics-memory.md +5 -5
- package/docs/documentation-audit-2026-07-09.md +7 -7
- package/docs/engine-restart.md +90 -5
- package/docs/harness-mode.md +1 -1
- package/docs/internal-install.md +338 -39
- package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
- package/docs/kb-pr3223-cascade-archiving.md +1 -1
- package/docs/kb-pr696-merge-conflict-docs.md +6 -6
- package/docs/kb-sweep.md +35 -35
- package/docs/keep-processes.md +1 -1
- package/docs/live-checkout-mode.md +30 -30
- package/docs/managed-spawn.md +18 -14
- package/docs/named-agents.md +7 -7
- package/docs/plan-lifecycle.md +69 -2
- package/docs/pr-author-identity.md +114 -0
- package/docs/pr-auto-fix-dispatch.md +19 -4
- package/docs/pr-comment-followup.md +6 -6
- package/docs/pr-review-fix-loop.md +59 -10
- package/docs/process-termination.md +40 -0
- package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
- package/docs/qa-runbook-lifecycle.md +367 -17
- package/docs/qa-runbooks.md +3 -3
- package/docs/rfc-completion-json.md +18 -18
- package/docs/runtime-adapters.md +26 -21
- package/docs/security.md +6 -6
- package/docs/self-improvement.md +4 -4
- package/docs/shared-lifecycle-module-map.md +473 -472
- package/docs/skills.md +52 -3
- package/docs/slim-ux/concepts.md +121 -116
- package/docs/specs/agent-configurability.md +18 -18
- package/docs/specs/agent-rename.md +18 -18
- package/docs/team-memory.md +38 -21
- package/docs/timeouts-and-liveness.md +118 -10
- package/docs/tutorials/01-install-and-connect.md +1 -1
- package/docs/watches.md +40 -39
- package/docs/workspace-manifests.md +4 -4
- package/docs/worktree-lifecycle.md +293 -14
- package/engine/README.md +46 -0
- package/engine/{ado-comment.js → ado/comment.js} +8 -8
- package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
- package/engine/{ado.js → ado/index.js} +417 -63
- package/engine/{ado-status.js → ado/status.js} +6 -8
- package/engine/{ado-token.js → ado/token.js} +1 -1
- package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
- package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
- package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
- package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
- package/engine/{harness-context.js → agents/harness-context.js} +5 -5
- package/engine/{harness.js → agents/harness.js} +3 -3
- package/engine/{llm.js → agents/llm.js} +18 -14
- package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
- package/engine/{playbook.js → agents/playbook.js} +155 -22
- package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
- package/engine/{preflight.js → agents/preflight.js} +29 -10
- package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
- package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
- package/engine/{steering.js → agents/steering.js} +5 -5
- package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
- package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
- package/engine/{api-validation.js → api/api-validation.js} +1 -1
- package/engine/api/bridge.js +787 -0
- package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
- package/engine/api/companion.js +560 -0
- package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
- package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
- package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
- package/engine/api-contracts/agent-content.js +4 -4
- package/engine/api-contracts/capability-manifest.js +236 -0
- package/engine/api-contracts/capability-protocol.js +333 -0
- package/engine/api-contracts/cc-ops.js +1 -1
- package/engine/api-contracts/config-runtime.js +5 -0
- package/engine/api-contracts/core.js +28 -1
- package/engine/api-contracts/index.js +100 -0
- package/engine/api-contracts/orchestration.js +18 -5
- package/engine/api-contracts/pull-requests.js +37 -6
- package/engine/api-contracts/qa-process.js +29 -6
- package/engine/api-contracts/work-plan-prd.js +21 -1
- package/engine/cloud/contract.js +212 -0
- package/engine/cloud/index.js +159 -0
- package/engine/{execution-model.js → core/execution-model.js} +1 -1
- package/engine/{features.js → core/features.js} +4 -4
- package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
- package/engine/{queries.js → core/queries.js} +201 -36
- package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
- package/engine/{shared.js → core/shared.js} +1637 -175
- package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
- package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
- package/engine/db/index.js +11 -2
- package/engine/db/migrations/002-dispatches.js +3 -3
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/db/migrations/006-metrics.js +1 -1
- package/engine/db/migrations/007-watches.js +2 -2
- package/engine/db/migrations/008-small-state.js +1 -1
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/010-pr-links.js +1 -1
- package/engine/db/migrations/011-remaining-state.js +1 -1
- package/engine/db/migrations/012-steering-deliveries.js +2 -2
- package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
- package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
- package/engine/db/migrations/015-plans-prds.js +0 -0
- package/engine/db/migrations/018-sql-only-cutover.js +2 -2
- package/engine/db/migrations/021-archived-work-items.js +1 -1
- package/engine/db/migrations/022-global-cc-session.js +1 -1
- package/engine/db/migrations/023-engine-state.js +1 -1
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
- package/engine/db/migrations/029-repair-reused-versions.js +20 -0
- package/engine/db/migrations/031-pr-author-identity.js +137 -0
- package/engine/{consolidation.js → memory/consolidation.js} +6 -6
- package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
- package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
- package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
- package/engine/{memory-store.js → memory/memory-store.js} +3 -3
- package/engine/{promotion.js → memory/promotion.js} +3 -3
- package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
- package/engine/{review-learning.js → memory/review-learning.js} +10 -5
- package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
- package/engine/{logs-store.js → observability/logs-store.js} +5 -5
- package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
- package/engine/{check-status.js → operations/check-status.js} +3 -3
- package/engine/{cli.js → operations/cli.js} +271 -113
- package/engine/{distribution.js → operations/distribution.js} +5 -6
- package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
- package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
- package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
- package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
- package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
- package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
- package/engine/{meeting.js → orchestration/meeting.js} +6 -16
- package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
- package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
- package/engine/{routing.js → orchestration/routing.js} +3 -3
- package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
- package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
- package/engine/{timeout.js → orchestration/timeout.js} +158 -109
- package/engine/{db-events.js → persistence/db-events.js} +2 -2
- package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
- package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
- package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
- package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
- package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
- package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
- package/engine/persistence/state-operations.js +350 -0
- package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
- package/engine/{issues.js → planning/issues.js} +2 -2
- package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
- package/engine/planning/prd-result-sidecar.js +190 -0
- package/engine/{prd-store.js → planning/prd-store.js} +17 -17
- package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
- package/engine/{projects.js → planning/projects.js} +10 -10
- package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
- package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
- package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
- package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
- package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
- package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
- package/engine/{process-utils.js → processes/process-utils.js} +599 -55
- package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
- package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
- package/engine/{comment-format.js → providers/comment-format.js} +5 -5
- package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
- package/engine/{gh-token.js → providers/gh-token.js} +4 -4
- package/engine/{github.js → providers/github.js} +131 -54
- package/engine/{pr-action.js → providers/pr-action.js} +13 -12
- package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
- package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
- package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
- package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
- package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
- package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
- package/engine/{pr-track.js → providers/pr-track.js} +11 -13
- package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
- package/engine/qa/auto-prd-qa.js +313 -0
- package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
- package/engine/qa/prd-session.js +240 -0
- package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
- package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
- package/engine/{qa-runs.js → qa/runs.js} +286 -15
- package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
- package/engine/qa/visual-journey.js +654 -0
- package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
- package/engine/qa-runners/maestro.js +3 -3
- package/engine/qa-runners/playwright.js +2 -2
- package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
- package/engine/recovery/stop-stack.js +607 -0
- package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
- package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
- package/engine/runtimes/claude.js +14 -12
- package/engine/runtimes/codex.js +8 -6
- package/engine/runtimes/copilot.js +17 -16
- package/engine/{watch-actions.js → watches/actions.js} +13 -13
- package/engine/{watches.js → watches/index.js} +43 -32
- package/engine/{watches-store.js → watches/store.js} +4 -4
- package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
- package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
- package/engine/worktrees/inventory.js +671 -0
- package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
- package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
- package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
- package/engine/worktrees/quarantine-refs.js +173 -0
- package/engine.js +1137 -208
- package/minions.js +147 -77
- package/package.json +10 -6
- package/playbooks/_pr-description-audit.md +110 -78
- package/playbooks/build-fix-complex.md +2 -0
- package/playbooks/fix.md +16 -12
- package/playbooks/implement-shared.md +2 -0
- package/playbooks/implement.md +19 -20
- package/playbooks/plan-to-prd.md +18 -3
- package/playbooks/qa-session-draft.md +136 -1
- package/playbooks/qa-session-execute.md +80 -2
- package/playbooks/qa-session-setup.md +17 -1
- package/playbooks/qa-validate.md +1 -1
- package/playbooks/setup.md +2 -0
- package/playbooks/shared-rules.md +25 -32
- package/playbooks/templates/followup-dispatch.md +4 -3
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +19 -27
- package/watch-plugins/README.md +92 -0
- package/watch-plugins/ado-author-prs.js +336 -0
- package/watch-plugins/gh-author-prs.js +375 -0
- package/watch-plugins/http.js +474 -0
- package/watch-plugins/teams-channel.js +869 -0
- package/docs/dev-composite-workflow.md +0 -101
- package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
- package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
- package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/visual-evidence-ci.md +0 -103
- package/engine/bridge.js +0 -379
- package/engine/quarantine-refs.js +0 -103
- package/engine/state-operations.js +0 -178
- /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
package/bin/minions.js
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* minions remove <project-dir> Unlink a project
|
|
10
10
|
* minions list List linked projects
|
|
11
11
|
* minions update [--no-wait] Update to latest version (--no-wait backgrounds the post-update restart)
|
|
12
|
+
* Channel-gated: refuses a public install over an internal runtime.
|
|
13
|
+
* --allow-downgrade overrides the semver downgrade guard.
|
|
12
14
|
* minions version Show installed and package versions
|
|
13
15
|
* minions doctor [--harness] Check prerequisites and runtime health (--harness: print runtime harness inventory)
|
|
14
16
|
* minions state <subcmd> Check, backup, or export SQLite state
|
|
15
17
|
* minions restart [--open] Stop + start engine + dashboard (--open forces a new browser tab)
|
|
16
18
|
* minions start [--open] Start engine + dashboard if not already running (no-op when up)
|
|
17
|
-
* minions stop
|
|
19
|
+
* minions stop Request engine shutdown and return (engine only)
|
|
20
|
+
* minions stop --all [--wait] Stop the whole stack (supervisor + dashboard + engine)
|
|
18
21
|
* minions status Show engine status
|
|
19
22
|
* minions pause / resume Pause/resume dispatching
|
|
20
23
|
* minions queue Show dispatch queue
|
|
@@ -62,9 +65,28 @@ const net = require('net');
|
|
|
62
65
|
const { spawn, spawnSync, execSync } = require('child_process');
|
|
63
66
|
|
|
64
67
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
65
|
-
const shared = require(path.join(PKG_ROOT, 'engine', 'shared'));
|
|
68
|
+
const shared = require(path.join(PKG_ROOT, 'engine', 'core', 'shared'));
|
|
66
69
|
const { openUrlInBrowser } = shared;
|
|
67
|
-
const {
|
|
70
|
+
const {
|
|
71
|
+
confirmRestartHealthBeforeCleanup,
|
|
72
|
+
waitForRestartHealth,
|
|
73
|
+
formatRestartHealthError,
|
|
74
|
+
} = require(path.join(PKG_ROOT, 'engine', 'recovery', 'restart-health'));
|
|
75
|
+
const { removeSupersededModuleFiles } = require('./install-layout');
|
|
76
|
+
const {
|
|
77
|
+
terminateProcess,
|
|
78
|
+
listNodePidsMatchingScripts,
|
|
79
|
+
} = require(path.join(PKG_ROOT, 'engine', 'processes', 'process-utils'));
|
|
80
|
+
// P-b28e4f60 — the ordered whole-stack teardown lives in exactly one place.
|
|
81
|
+
// Every CLI path that stops the runtime (stop --all, restart, nuke, uninstall,
|
|
82
|
+
// the init upgrade) calls stopWholeRuntimeStack() below, which wraps this.
|
|
83
|
+
const {
|
|
84
|
+
stopRuntimeStack,
|
|
85
|
+
stopStackExitCode,
|
|
86
|
+
holderIsLiveDaemon,
|
|
87
|
+
STOP_STACK_EXIT,
|
|
88
|
+
DEFAULT_STOP_TIMEOUT_MS: STOP_STACK_DEFAULT_TIMEOUT_MS,
|
|
89
|
+
} = require(path.join(PKG_ROOT, 'engine', 'recovery', 'stop-stack'));
|
|
68
90
|
// Dev-mode dashboard port. `--dev` mode (see argv parser below) retargets
|
|
69
91
|
// MINIONS_HOME at PKG_ROOT and routes the dashboard onto this port so a dev
|
|
70
92
|
// checkout coexists with a global npm install on the normal port (7331).
|
|
@@ -126,16 +148,6 @@ function getListeningPids(port) {
|
|
|
126
148
|
} catch { return []; }
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
/** Kill process(es) listening on a given port. Works cross-platform. */
|
|
130
|
-
function killByPort(port) {
|
|
131
|
-
const pids = getListeningPids(port);
|
|
132
|
-
if (process.platform === 'win32') {
|
|
133
|
-
for (const pid of pids) try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', timeout: 5000, windowsHide: true }); } catch {}
|
|
134
|
-
} else {
|
|
135
|
-
for (const pid of pids) try { process.kill(Number(pid), 'SIGKILL'); } catch {}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
151
|
const isPortListening = (port) => getListeningPids(port).length > 0;
|
|
140
152
|
|
|
141
153
|
/** Authoritative "is something accepting TCP connections on this port?" probe.
|
|
@@ -157,15 +169,13 @@ function tcpPortAccepts(port, timeoutMs = 1500) {
|
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
/**
|
|
160
|
-
* Wait until no process is listening on `port
|
|
161
|
-
*
|
|
162
|
-
* missed, or a process that respawned itself). Returns true when the port is
|
|
163
|
-
* free, false on timeout.
|
|
172
|
+
* Wait until no process is listening on `port`. Daemons are terminated only
|
|
173
|
+
* after exact runtime-root ownership checks before this helper is entered.
|
|
164
174
|
*
|
|
165
175
|
* Rationale: `taskkill /F` and `SIGKILL` return immediately while the OS does
|
|
166
176
|
* the actual termination asynchronously, and the port doesn't transition to
|
|
167
177
|
* available until the kernel finalises the socket close. Without this wait,
|
|
168
|
-
* the new dashboard race-spawned just after
|
|
178
|
+
* the new dashboard race-spawned just after termination can hit EADDRINUSE
|
|
169
179
|
* and exit silently — the bug pattern that surfaced as "Dashboard failed
|
|
170
180
|
* health check, port=7331 listening=no" while the OLD dashboard PID was
|
|
171
181
|
* still bound to the port.
|
|
@@ -175,9 +185,6 @@ function waitForPortRelease(port, timeoutMs = 10000, pollMs = 200) {
|
|
|
175
185
|
while (Date.now() - start < timeoutMs) {
|
|
176
186
|
const pids = getListeningPids(port);
|
|
177
187
|
if (pids.length === 0) return { ok: true, waitedMs: Date.now() - start };
|
|
178
|
-
// Retry the kill — covers the case where the original killByPort missed a
|
|
179
|
-
// sibling listener or a new orphan appeared mid-wait.
|
|
180
|
-
killByPort(port);
|
|
181
188
|
const sleepUntil = Date.now() + pollMs;
|
|
182
189
|
while (Date.now() < sleepUntil) { /* spin */ }
|
|
183
190
|
}
|
|
@@ -264,42 +271,50 @@ function readEnginePid(minionsHome) {
|
|
|
264
271
|
* they can be re-attached on next start (CLAUDE.md timeouts/liveness section).
|
|
265
272
|
* Tree-kill (`taskkill /T`, `pgrep -P` walk) would orphan in-flight work.
|
|
266
273
|
*/
|
|
267
|
-
function killPidOnly(pid) {
|
|
268
|
-
if (!pid || pid === process.pid) return;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
+
function killPidOnly(pid, scriptPaths = daemonScriptPaths()) {
|
|
275
|
+
if (!pid || pid === process.pid) return false;
|
|
276
|
+
return terminateProcess('bin.daemon-stop-process', { pid }, { scriptPaths });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function daemonScriptPaths({ includeAgents = false } = {}) {
|
|
280
|
+
const scripts = [
|
|
281
|
+
path.join(MINIONS_HOME, 'engine.js'),
|
|
282
|
+
path.join(MINIONS_HOME, 'dashboard.js'),
|
|
283
|
+
path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js'),
|
|
284
|
+
];
|
|
285
|
+
if (includeAgents) scripts.push(path.join(MINIONS_HOME, 'engine', 'agents', 'spawn-agent.js'));
|
|
286
|
+
return scripts;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Kill only exact daemon scripts owned by this runtime root. */
|
|
290
|
+
function killMinionsProcesses(scriptPaths, { includeDescendants = false } = {}) {
|
|
291
|
+
const pids = listNodePidsMatchingScripts(scriptPaths);
|
|
292
|
+
for (const pid of pids) {
|
|
293
|
+
if (pid === process.pid) continue;
|
|
294
|
+
if (includeDescendants) {
|
|
295
|
+
shared.terminateProcess('bin.daemon-stop-tree', { pid }, { scriptPaths });
|
|
274
296
|
}
|
|
275
|
-
|
|
297
|
+
else killPidOnly(pid, scriptPaths);
|
|
298
|
+
}
|
|
299
|
+
return pids.filter(pid => pid !== process.pid);
|
|
276
300
|
}
|
|
277
301
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
try { out = execSync('wmic process where "name=\'node.exe\'" get processid,commandline /format:csv', { encoding: 'utf8', timeout: 10000, windowsHide: true }); } catch { return; }
|
|
289
|
-
}
|
|
290
|
-
for (const line of out.split('\n')) {
|
|
291
|
-
if (patterns.some(p => line.includes(p))) {
|
|
292
|
-
const pidMatch = line.match(/(\d{2,})/);
|
|
293
|
-
const pid = pidMatch ? pidMatch[1] : null;
|
|
294
|
-
if (pid && pid !== String(process.pid)) killPidOnly(pid);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
for (const p of patterns) {
|
|
299
|
-
try { execSync(`pkill -f "${p}" 2>/dev/null`, { timeout: 5000 }); } catch {}
|
|
300
|
-
}
|
|
302
|
+
function retryKillVerifiedPids(pids, scriptPaths, { attempts = 3, pollMs = 200 } = {}) {
|
|
303
|
+
const remaining = new Set(
|
|
304
|
+
(pids || []).map(Number).filter(pid => Number.isInteger(pid) && pid > 0 && pid !== process.pid),
|
|
305
|
+
);
|
|
306
|
+
const killed = [];
|
|
307
|
+
for (let attempt = 0; attempt < attempts && remaining.size > 0; attempt++) {
|
|
308
|
+
for (const pid of [...remaining]) {
|
|
309
|
+
if (!killPidOnly(pid, scriptPaths)) continue;
|
|
310
|
+
remaining.delete(pid);
|
|
311
|
+
killed.push(pid);
|
|
301
312
|
}
|
|
302
|
-
|
|
313
|
+
if (remaining.size > 0 && attempt + 1 < attempts) {
|
|
314
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pollMs);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return killed;
|
|
303
318
|
}
|
|
304
319
|
|
|
305
320
|
/** Open append-mode FDs under engine/ for detached-process stdio. If the file
|
|
@@ -363,7 +378,7 @@ function spawnDashboard(requestedPort) {
|
|
|
363
378
|
return proc;
|
|
364
379
|
}
|
|
365
380
|
|
|
366
|
-
/** Spawn the external supervisor (engine/supervisor.js). The supervisor
|
|
381
|
+
/** Spawn the external supervisor (engine/recovery/supervisor.js). The supervisor
|
|
367
382
|
* watches BOTH the engine and the dashboard and respawns whichever dies,
|
|
368
383
|
* unless `engine/stop-intent.json` is present (set by `minions stop`).
|
|
369
384
|
* Single-instance guard inside supervisor.js means double-spawn during
|
|
@@ -372,18 +387,13 @@ function spawnDashboard(requestedPort) {
|
|
|
372
387
|
function spawnSupervisor() {
|
|
373
388
|
const out = _openStdioLog('supervisor-stdio.log');
|
|
374
389
|
const err = _openStdioLog('supervisor-stdio.log');
|
|
375
|
-
const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine', 'supervisor.js')], {
|
|
390
|
+
const proc = spawn(process.execPath, [..._sqliteSpawnFlags(), path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js')], {
|
|
376
391
|
cwd: MINIONS_HOME, stdio: ['ignore', out, err], detached: true, windowsHide: true,
|
|
377
392
|
});
|
|
378
393
|
proc.unref();
|
|
379
394
|
return proc;
|
|
380
395
|
}
|
|
381
396
|
|
|
382
|
-
// Path getter — evaluated lazily inside the helper so MINIONS_HOME, which
|
|
383
|
-
// is initialized later in module load, is bound at call time rather than
|
|
384
|
-
// at function-definition time.
|
|
385
|
-
const _supervisorPidPath = () => path.join(MINIONS_HOME, 'engine', 'supervisor.pid');
|
|
386
|
-
|
|
387
397
|
/**
|
|
388
398
|
* Resolve the cold-start restart-health budget (ms) from config, falling back
|
|
389
399
|
* to ENGINE_DEFAULTS.restartHealthTimeoutMs. Mirrors the Settings clamp range
|
|
@@ -449,7 +459,7 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
449
459
|
void (async () => {
|
|
450
460
|
const healthTimeoutMs = _resolveRestartHealthTimeoutMs();
|
|
451
461
|
const verificationStartedAt = Date.now();
|
|
452
|
-
|
|
462
|
+
let startupResult = await waitForRestartHealth({
|
|
453
463
|
minionsHome: MINIONS_HOME,
|
|
454
464
|
expectedEnginePid: engineProc.pid,
|
|
455
465
|
dashboardPid: dashProc.pid,
|
|
@@ -460,13 +470,14 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
460
470
|
});
|
|
461
471
|
if (!startupResult.ok) {
|
|
462
472
|
console.error(formatRestartHealthError(startupResult));
|
|
463
|
-
_cleanupFailedStackSpawn({
|
|
473
|
+
const cleanup = await _cleanupFailedStackSpawn({
|
|
464
474
|
enginePid: engineProc.pid,
|
|
465
475
|
dashboardPid: dashProc.pid,
|
|
466
476
|
supervisorPid: null,
|
|
467
477
|
dashboardPort: requested.port,
|
|
468
478
|
});
|
|
469
|
-
process.exit(1);
|
|
479
|
+
if (!cleanup.skipped) process.exit(1);
|
|
480
|
+
startupResult = cleanup.health;
|
|
470
481
|
}
|
|
471
482
|
|
|
472
483
|
// The CLI owns startup recovery until the exact dashboard PID has bound the
|
|
@@ -476,10 +487,10 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
476
487
|
const supProc = spawnSupervisor();
|
|
477
488
|
console.log(` Supervisor started (PID: ${supProc.pid})`);
|
|
478
489
|
console.log(' Verifying restart health...');
|
|
479
|
-
const supervisorInternals = require(path.join(PKG_ROOT, 'engine', 'supervisor'))._internals;
|
|
490
|
+
const supervisorInternals = require(path.join(PKG_ROOT, 'engine', 'recovery', 'supervisor'))._internals;
|
|
480
491
|
const startupElapsedMs = Date.now() - verificationStartedAt;
|
|
481
492
|
const topologyTimeoutMs = Math.max(MIN_TOPOLOGY_VERIFY_TIMEOUT_MS, healthTimeoutMs - startupElapsedMs);
|
|
482
|
-
|
|
493
|
+
let result = await waitForRestartHealth({
|
|
483
494
|
minionsHome: MINIONS_HOME,
|
|
484
495
|
expectedEnginePid: engineProc.pid,
|
|
485
496
|
dashboardPid: dashProc.pid,
|
|
@@ -495,18 +506,19 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
495
506
|
topologyScripts: {
|
|
496
507
|
engine: path.join(MINIONS_HOME, 'engine.js'),
|
|
497
508
|
dashboard: path.join(MINIONS_HOME, 'dashboard.js'),
|
|
498
|
-
supervisor: path.join(MINIONS_HOME, 'engine', 'supervisor.js'),
|
|
509
|
+
supervisor: path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js'),
|
|
499
510
|
},
|
|
500
511
|
});
|
|
501
512
|
if (!result.ok) {
|
|
502
513
|
console.error(formatRestartHealthError(result));
|
|
503
|
-
_cleanupFailedStackSpawn({
|
|
514
|
+
const cleanup = await _cleanupFailedStackSpawn({
|
|
504
515
|
enginePid: engineProc.pid,
|
|
505
516
|
dashboardPid: dashProc.pid,
|
|
506
517
|
supervisorPid: supProc.pid,
|
|
507
518
|
dashboardPort: requested.port,
|
|
508
519
|
});
|
|
509
|
-
process.exit(1);
|
|
520
|
+
if (!cleanup.skipped) process.exit(1);
|
|
521
|
+
result = cleanup.health;
|
|
510
522
|
}
|
|
511
523
|
console.log(` Restart verified: engine PID ${result.engine.pid}; dashboard healthy.`);
|
|
512
524
|
|
|
@@ -528,15 +540,39 @@ function spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs })
|
|
|
528
540
|
});
|
|
529
541
|
}
|
|
530
542
|
|
|
531
|
-
function _cleanupFailedStackSpawn({ enginePid, dashboardPid, supervisorPid, dashboardPort }) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
543
|
+
async function _cleanupFailedStackSpawn({ enginePid, dashboardPid, supervisorPid, dashboardPort }) {
|
|
544
|
+
const finalHealth = await confirmRestartHealthBeforeCleanup({
|
|
545
|
+
minionsHome: MINIONS_HOME,
|
|
546
|
+
expectedEnginePid: enginePid,
|
|
547
|
+
dashboardPid,
|
|
548
|
+
dashboardPort,
|
|
549
|
+
supervisorPid,
|
|
550
|
+
requireBeaconOwner: true,
|
|
551
|
+
});
|
|
552
|
+
if (finalHealth.ok) {
|
|
553
|
+
console.error(
|
|
554
|
+
' Cleanup skipped: the expected engine, dashboard, and supervisor are currently healthy and owned.'
|
|
555
|
+
);
|
|
556
|
+
return { skipped: true, health: finalHealth };
|
|
538
557
|
}
|
|
539
|
-
|
|
558
|
+
|
|
559
|
+
const cleanupSource = `restart verification failed (${process.pid})`;
|
|
560
|
+
writeStopIntent(cleanupSource);
|
|
561
|
+
try {
|
|
562
|
+
for (const [pid, scriptPath] of [
|
|
563
|
+
[supervisorPid, path.join(MINIONS_HOME, 'engine', 'recovery', 'supervisor.js')],
|
|
564
|
+
[dashboardPid, path.join(MINIONS_HOME, 'dashboard.js')],
|
|
565
|
+
[enginePid, path.join(MINIONS_HOME, 'engine.js')],
|
|
566
|
+
]) {
|
|
567
|
+
killPidOnly(pid, [scriptPath]);
|
|
568
|
+
}
|
|
569
|
+
for (const pid of [supervisorPid, dashboardPid, enginePid]) waitForPidDeath(pid, 5000);
|
|
570
|
+
if (dashboardPort) waitForPortRelease(dashboardPort, 5000);
|
|
571
|
+
shared.clearDashboardPortFile(MINIONS_HOME);
|
|
572
|
+
} finally {
|
|
573
|
+
_clearStopIntentIfSource(cleanupSource);
|
|
574
|
+
}
|
|
575
|
+
return { skipped: false, health: finalHealth };
|
|
540
576
|
}
|
|
541
577
|
|
|
542
578
|
/** Poll engine/dashboard-port.json until it appears (dashboard wrote it on
|
|
@@ -578,7 +614,7 @@ async function _waitForDashboardPortFile(home, timeoutMs = 12000, pollMs = 150,
|
|
|
578
614
|
|
|
579
615
|
/** Clear the stop-intent flag so the supervisor resumes guarding the engine
|
|
580
616
|
* and dashboard. Called at the top of every start/restart path.
|
|
581
|
-
* Delegates to engine/shared.js when available so engine.js, dashboard.js,
|
|
617
|
+
* Delegates to engine/core/shared.js when available so engine.js, dashboard.js,
|
|
582
618
|
* and the CLI all share one source of truth. Falls back to a direct unlink
|
|
583
619
|
* during early init when shared.js may not have loaded yet. */
|
|
584
620
|
function clearStopIntent() {
|
|
@@ -603,19 +639,62 @@ function writeStopIntent(source) {
|
|
|
603
639
|
}
|
|
604
640
|
}
|
|
605
641
|
|
|
606
|
-
|
|
607
|
-
* Used by `minions stop` and `minions restart` after writing stop-intent. */
|
|
608
|
-
function killSupervisor() {
|
|
642
|
+
function _clearStopIntentIfSource(source) {
|
|
609
643
|
try {
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
644
|
+
const intent = JSON.parse(fs.readFileSync(shared.STOP_INTENT_PATH, 'utf8'));
|
|
645
|
+
if (intent && intent.source === source) shared.clearStopIntent();
|
|
646
|
+
} catch { /* missing, replaced, or unreadable intent is preserved */ }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The CLI's ONE whole-stack teardown call site (P-b28e4f60).
|
|
651
|
+
*
|
|
652
|
+
* The ordered sequence — stop-intent → supervisor → dashboard → engine drain →
|
|
653
|
+
* verified reap → WAL-index check → late-respawn sweep → verification — is
|
|
654
|
+
* owned by engine/recovery/stop-stack.js. `minions stop --all`, `restart`,
|
|
655
|
+
* `nuke`, `uninstall` and the init upgrade path all route through here, so
|
|
656
|
+
* exactly one implementation of that sequence exists in the tree.
|
|
657
|
+
*
|
|
658
|
+
* `waitForRelease` is opt-in and defaults OFF: only a caller that must PROVE
|
|
659
|
+
* the database handles are gone (the pre-migration gate behind
|
|
660
|
+
* `minions stop --all --wait`) blocks on `engine/state.db-shm`. Callers that
|
|
661
|
+
* immediately replace the stack, or delete the runtime root outright, run the
|
|
662
|
+
* identical teardown and get the same evidence reported without the wait.
|
|
663
|
+
*
|
|
664
|
+
* Never throws for an expected outcome — the caller decides what a refusal or
|
|
665
|
+
* an expired budget means for ITS command.
|
|
666
|
+
*/
|
|
667
|
+
function stopWholeRuntimeStack({ source, timeoutMs, waitForRelease = false }) {
|
|
668
|
+
const verdict = stopRuntimeStack({
|
|
669
|
+
minionsHome: MINIONS_HOME,
|
|
670
|
+
source,
|
|
671
|
+
timeoutMs,
|
|
672
|
+
waitForRelease,
|
|
673
|
+
});
|
|
674
|
+
for (const svc of verdict.stopped) {
|
|
675
|
+
console.log(` Stopped ${svc.name} (PID ${svc.pid}, ${svc.via})`);
|
|
676
|
+
}
|
|
677
|
+
for (const holder of verdict.holders) {
|
|
678
|
+
console.error(` Still running: ${holder.name} (PID ${holder.pid}) — ${holder.reason}`);
|
|
679
|
+
}
|
|
680
|
+
console.log(` ${verdict.reason} (${verdict.waitedMs}ms)`);
|
|
681
|
+
return verdict;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** `--timeout <ms>` / `--timeout=<ms>` for `minions stop --all`. Defaults to
|
|
685
|
+
* the shared module's budget, which matches the internal installer's
|
|
686
|
+
* quiescence window so a stop can never be shorter than the wait after it. */
|
|
687
|
+
function _resolveStopAllTimeoutMs(argv) {
|
|
688
|
+
const flagIdx = argv.indexOf('--timeout');
|
|
689
|
+
const inline = argv.find(a => typeof a === 'string' && a.startsWith('--timeout='));
|
|
690
|
+
if (flagIdx < 0 && !inline) return STOP_STACK_DEFAULT_TIMEOUT_MS;
|
|
691
|
+
const raw = flagIdx >= 0 ? argv[flagIdx + 1] : inline.slice('--timeout='.length);
|
|
692
|
+
const n = Number(raw);
|
|
693
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
694
|
+
console.error(`\n --timeout requires a positive integer in milliseconds, got: ${raw || '(missing)'}\n`);
|
|
695
|
+
process.exit(2);
|
|
696
|
+
}
|
|
697
|
+
return n;
|
|
619
698
|
}
|
|
620
699
|
|
|
621
700
|
const LEGACY_DEFAULT_SQUAD_HOME = path.join(os.homedir(), '.squad');
|
|
@@ -823,14 +902,19 @@ if (devMode) process.env.MINIONS_DASHBOARD_PORT = String(devPort);
|
|
|
823
902
|
// dynamic chain re-introduces the 7331 bug W-mq5nwl9l was opened to fix.
|
|
824
903
|
const DASHBOARD_PORT = resolveDashboardPort(rest).port;
|
|
825
904
|
// Propagate the port to every child (engine, dashboard, delegated subcommands).
|
|
826
|
-
// engine/cli.js status reads MINIONS_PORT; dashboard.js reads PORT. We export
|
|
905
|
+
// engine/operations/cli.js status reads MINIONS_PORT; dashboard.js reads PORT. We export
|
|
827
906
|
// MINIONS_PORT as the *requested* port for legacy consumers, but loopback callers
|
|
828
|
-
// (watch
|
|
907
|
+
// (watch action handlers / pipeline / managed-spawn) must read the *actual* bound port
|
|
829
908
|
// from the dashboard-port.json beacon via shared.readDashboardPortFile() — the
|
|
830
909
|
// dashboard may bind to a fallback port (7332+) on EADDRINUSE, and MINIONS_PORT
|
|
831
910
|
// stays frozen at the originally-requested value in that case.
|
|
832
911
|
process.env.MINIONS_PORT = String(DASHBOARD_PORT);
|
|
833
912
|
const POST_UPDATE_INIT_TIMEOUT_MS = 120000;
|
|
913
|
+
// Teardown budget for the paths that immediately REPLACE the stack (upgrade)
|
|
914
|
+
// or DELETE the runtime root (nuke, uninstall). Mirrors the 10s timeout the
|
|
915
|
+
// inline `engine.js stop` call used to carry, so a hung engine cannot stall
|
|
916
|
+
// them — whatever survives the budget is force-reaped or deleted next.
|
|
917
|
+
const FORCED_STOP_BUDGET_MS = 10000;
|
|
834
918
|
// W-mqpx0tpi — the post-update `minions restart` runs as a child of `minions
|
|
835
919
|
// update` under this wall-clock cap. It must outlive the restart-health budget
|
|
836
920
|
// (now up to ~60s for a cold post-`npm install` dashboard boot) plus spawn +
|
|
@@ -859,7 +943,7 @@ function getPkgVersionFresh() {
|
|
|
859
943
|
|
|
860
944
|
// Which npm distribution is this CLI? Derived from the installed package
|
|
861
945
|
// manifest (public `@yemi33/minions` vs an internal rename produced by
|
|
862
|
-
//
|
|
946
|
+
// scripts/prepare-package.js) so `minions version` compares against the right
|
|
863
947
|
// registry channel and `minions update` reinstalls ITSELF instead of pulling
|
|
864
948
|
// in a second package that fights over the shared `minions` bin (npm EEXIST).
|
|
865
949
|
function getPkgName() {
|
|
@@ -875,6 +959,18 @@ function getInstalledVersion() {
|
|
|
875
959
|
|
|
876
960
|
function saveInstalledVersion(version) {
|
|
877
961
|
fs.writeFileSync(path.join(MINIONS_HOME, '.minions-version'), version);
|
|
962
|
+
// Record WHICH npm distribution produced this runtime root (W-ms6b7prf006tef7f-b).
|
|
963
|
+
// `.minions-version` alone cannot tell an internal `@opg-microsoft/minions`
|
|
964
|
+
// root apart from a public `@yemi33/minions` one, so a channel gate has
|
|
965
|
+
// nothing to compare against. Read back via shared.readRuntimeChannel();
|
|
966
|
+
// best-effort, because failing to stamp the channel must never fail an init.
|
|
967
|
+
try {
|
|
968
|
+
fs.writeFileSync(path.join(MINIONS_HOME, shared.RUNTIME_CHANNEL_MARKER), `${JSON.stringify({
|
|
969
|
+
name: getPkgName(),
|
|
970
|
+
version,
|
|
971
|
+
installedAt: new Date().toISOString(),
|
|
972
|
+
}, null, 2)}\n`);
|
|
973
|
+
} catch {}
|
|
878
974
|
// Persist source commit so dashboard can detect repo-based installs
|
|
879
975
|
try {
|
|
880
976
|
const commit = execSync('git rev-parse --short HEAD', { cwd: PKG_ROOT, encoding: 'utf8', timeout: 5000, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
@@ -942,6 +1038,9 @@ function init() {
|
|
|
942
1038
|
|
|
943
1039
|
// Copy with smart merge logic
|
|
944
1040
|
copyDir(PKG_ROOT, MINIONS_HOME, excludeTop, alwaysUpdate, neverOverwrite, isUpgrade, actions);
|
|
1041
|
+
for (const removed of removeSupersededModuleFiles(MINIONS_HOME)) {
|
|
1042
|
+
actions.updated.push(`removed ${removed}`);
|
|
1043
|
+
}
|
|
945
1044
|
shared.syncBundledPersonalSkills(path.join(MINIONS_HOME, 'skills'), { homeDir: os.homedir() });
|
|
946
1045
|
|
|
947
1046
|
// Create config from template if it doesn't exist
|
|
@@ -989,7 +1088,7 @@ function init() {
|
|
|
989
1088
|
// current config only references one. Missing optional runtimes surface as
|
|
990
1089
|
// warns, not failures.
|
|
991
1090
|
try {
|
|
992
|
-
const { runPreflight, printPreflight } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
|
|
1091
|
+
const { runPreflight, printPreflight } = require(path.join(MINIONS_HOME, 'engine', 'agents', 'preflight'));
|
|
993
1092
|
let preflightConfig = null;
|
|
994
1093
|
try { preflightConfig = JSON.parse(fs.readFileSync(path.join(MINIONS_HOME, 'config.json'), 'utf8')); }
|
|
995
1094
|
catch { /* config may not exist on first init — fine, preflight handles null */ }
|
|
@@ -997,22 +1096,43 @@ function init() {
|
|
|
997
1096
|
printPreflight(results, { label: 'Preflight checks' });
|
|
998
1097
|
} catch {}
|
|
999
1098
|
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1099
|
+
// Constellation companion report (P-e93c85a1). Purely informational and
|
|
1100
|
+
// strictly non-fatal: init/update must succeed identically whether or not a
|
|
1101
|
+
// Constellation companion exists. The SYNC probe is used deliberately —
|
|
1102
|
+
// init() is synchronous, and the marker-only path is bounded by a single file
|
|
1103
|
+
// read, so a hung or absent companion can never stall an install or upgrade.
|
|
1104
|
+
// Minions never downloads, executes, or installs Constellation; when the
|
|
1105
|
+
// companion is missing or incompatible this only PRINTS the documented
|
|
1106
|
+
// install command for the operator to run. The whole block is wrapped so a
|
|
1107
|
+
// probe defect can never break `minions init` / `minions update`.
|
|
1108
|
+
try {
|
|
1109
|
+
const companion = require(path.join(MINIONS_HOME, 'engine', 'api', 'companion'));
|
|
1110
|
+
const companionLines = companion.formatCompanionLines(
|
|
1111
|
+
companion.probeCompanionSync({ config: _readConfigJsonSafe() })
|
|
1112
|
+
);
|
|
1113
|
+
if (companionLines.length > 0) {
|
|
1114
|
+
console.log('');
|
|
1115
|
+
for (const line of companionLines) console.log(` ${line}`);
|
|
1116
|
+
console.log('');
|
|
1117
|
+
}
|
|
1118
|
+
} catch { /* companion reporting is optional — never block init/update */ }
|
|
1119
|
+
|
|
1120
|
+
// The caller owns process lifecycle whenever it requests copy/sync-only mode.
|
|
1121
|
+
// This must also hold for a fresh runtime root; otherwise isolated installs
|
|
1122
|
+
// unexpectedly launch a second engine/dashboard/supervisor stack.
|
|
1123
|
+
if (skipStart) return;
|
|
1002
1124
|
|
|
1003
1125
|
// Auto-start on fresh install; direct force-upgrade restarts automatically.
|
|
1004
1126
|
const upgradeRequested = resolveDashboardPort([]);
|
|
1005
1127
|
const dashWasUp = isPortListening(upgradeRequested.port);
|
|
1006
1128
|
const restartStartMs = Date.now();
|
|
1007
1129
|
if (isUpgrade) {
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
// silently and the user keeps running stale code from the old dashboard process.
|
|
1015
|
-
killByPort(upgradeRequested.port);
|
|
1130
|
+
// Ordered teardown (stop-intent → supervisor → dashboard → engine drain →
|
|
1131
|
+
// verified reap → late-respawn sweep) is owned by
|
|
1132
|
+
// engine/recovery/stop-stack.js. Best-effort: an upgrade must not be
|
|
1133
|
+
// blocked by a refusal, and the spawn below replaces whatever survives.
|
|
1134
|
+
// Budget mirrors the 10s the inline `engine.js stop` call used to get.
|
|
1135
|
+
stopWholeRuntimeStack({ source: 'minions upgrade', timeoutMs: FORCED_STOP_BUDGET_MS });
|
|
1016
1136
|
// Wipe the runtime port file so the new dashboard's freshly-written
|
|
1017
1137
|
// beacon is the source of truth (avoids polling-back a stale entry).
|
|
1018
1138
|
shared.clearDashboardPortFile(MINIONS_HOME);
|
|
@@ -1278,10 +1398,17 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
1278
1398
|
Setup:
|
|
1279
1399
|
minions init Bootstrap ~/.minions/ (first time)
|
|
1280
1400
|
minions update [--no-wait] Update to latest version (--no-wait backgrounds the restart)
|
|
1401
|
+
Refuses a public install over an internal runtime.
|
|
1402
|
+
An internal install updates from its own feed (Azure CLI auth).
|
|
1403
|
+
--allow-downgrade Permit a target version older than the installed one
|
|
1404
|
+
--registry <url> Override the internal feed (internal channel only)
|
|
1281
1405
|
minions version Show installed vs package version
|
|
1282
1406
|
minions doctor [--harness] Check prerequisites and runtime health (--harness: print runtime harness inventory)
|
|
1283
1407
|
minions state check Run SQLite integrity and invariant checks
|
|
1284
1408
|
minions state backup <file> Checkpoint and create a consistent SQLite backup
|
|
1409
|
+
minions state summary [--json] Row-count census + fingerprint of the SQL state
|
|
1410
|
+
minions state verify-backup <file>
|
|
1411
|
+
Verify a backup file's integrity and contents
|
|
1285
1412
|
minions state export <file> Write a human-readable diagnostic JSON export
|
|
1286
1413
|
minions state import-legacy-json <dir> --confirm
|
|
1287
1414
|
Offline recovery import from a legacy home
|
|
@@ -1295,7 +1422,13 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
|
1295
1422
|
Engine:
|
|
1296
1423
|
minions restart [--open] Stop + start engine + dashboard (--open forces a new browser tab)
|
|
1297
1424
|
minions start [--open] Start engine + dashboard if not already running (idempotent; --open forces a new tab)
|
|
1298
|
-
minions stop Stop the engine
|
|
1425
|
+
minions stop Stop the engine (requests shutdown and returns immediately)
|
|
1426
|
+
minions stop --all [--wait] Stop the WHOLE stack: supervisor + dashboard + engine
|
|
1427
|
+
--wait Block until the engine/state.db handles are
|
|
1428
|
+
released (quiescence gate before a migration)
|
|
1429
|
+
--timeout <ms> Budget for the teardown (default 60000)
|
|
1430
|
+
Exit codes: 0 stopped · 3 not quiesced within the budget
|
|
1431
|
+
· 4 refused (a live PID could not be verified)
|
|
1299
1432
|
minions status Show agents, projects, queue
|
|
1300
1433
|
minions pause / resume Pause/resume dispatching
|
|
1301
1434
|
minions dispatch Force a dispatch cycle
|
|
@@ -1351,6 +1484,13 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1351
1484
|
init();
|
|
1352
1485
|
} else if (cmd === 'update') {
|
|
1353
1486
|
const noWait = rest.includes('--no-wait') || rest.includes('--detach');
|
|
1487
|
+
const allowDowngrade = rest.includes('--allow-downgrade');
|
|
1488
|
+
const registryIdx = rest.indexOf('--registry');
|
|
1489
|
+
const registryOverride = registryIdx !== -1 ? rest[registryIdx + 1] : null;
|
|
1490
|
+
if (registryIdx !== -1 && (!registryOverride || registryOverride.startsWith('--'))) {
|
|
1491
|
+
console.error('\n ERROR: --registry requires a feed URL.\n');
|
|
1492
|
+
process.exit(1);
|
|
1493
|
+
}
|
|
1354
1494
|
console.log('\n Updating Minions...\n');
|
|
1355
1495
|
// Dev/symlink installs: PKG_ROOT === MINIONS_HOME — npm update is a no-op (symlink already
|
|
1356
1496
|
// points to the repo), and `minions init --force` would fail (cwd/.minions is inside PKG_ROOT).
|
|
@@ -1361,6 +1501,26 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1361
1501
|
console.log(` Version synced to ${getPkgVersion()} (dev/symlink install — pull from git to update code)`);
|
|
1362
1502
|
} else {
|
|
1363
1503
|
const preVersion = getPkgVersionFresh();
|
|
1504
|
+
// CHANNEL GATE (W-ms6b7prf006tef7f-d). The runtime root — not PKG_ROOT — is
|
|
1505
|
+
// the channel authority. PKG_ROOT only says which package an install WOULD
|
|
1506
|
+
// pull; it resolves PUBLIC both from an opg repo checkout (whose own
|
|
1507
|
+
// package.json is named `@yemi33/minions`) and from a stale public global
|
|
1508
|
+
// shim standing in front of an internal runtime. Gating on the runtime's
|
|
1509
|
+
// `.minions-package` marker is what stops a stale public build from landing
|
|
1510
|
+
// on top of a newer internal one. Runs BEFORE any npm call so a refusal
|
|
1511
|
+
// never touches the registry or the install.
|
|
1512
|
+
const channel = shared.resolveUpdateChannel({ pkgRoot: PKG_ROOT, runtimeRoot: MINIONS_HOME });
|
|
1513
|
+
if (!channel.allowed) {
|
|
1514
|
+
console.error(`\n REFUSED: this would install the public package over an internal runtime.`);
|
|
1515
|
+
console.error(` Runtime channel: ${channel.runtimeName}${channel.runtimeVersion ? `@${channel.runtimeVersion}` : ''} (${MINIONS_HOME})`);
|
|
1516
|
+
console.error(` Would install: ${channel.pkgName} (resolved from ${PKG_ROOT})`);
|
|
1517
|
+
console.error(` A public install is never a valid upgrade for an internal runtime.`);
|
|
1518
|
+
console.error(`\n Use the internal installer instead:`);
|
|
1519
|
+
for (const line of channel.remediation) console.error(` ${line}`);
|
|
1520
|
+
console.error('');
|
|
1521
|
+
process.exit(1);
|
|
1522
|
+
}
|
|
1523
|
+
if (channel.warning) console.log(` ${channel.warning}`);
|
|
1364
1524
|
// Resolve the ACTIVE distribution first: an internal build must update
|
|
1365
1525
|
// itself, not the public package. Installing `@yemi33/minions` on top of an
|
|
1366
1526
|
// `@opg-microsoft/minions` install adds a second package claiming the same
|
|
@@ -1368,6 +1528,52 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1368
1528
|
// Reinstalling the same package name is an in-place upgrade, so no bin
|
|
1369
1529
|
// collision is possible.
|
|
1370
1530
|
const pkgName = getPkgName();
|
|
1531
|
+
|
|
1532
|
+
// INTERNAL FEED ROUTING (W-ms9bhf9k001j29cb). `npm view` / `npm install -g`
|
|
1533
|
+
// with no registry of their own resolve a SCOPED package through whatever
|
|
1534
|
+
// `@scope:registry` the caller's npm config carries — which routed an
|
|
1535
|
+
// `@opg-microsoft/minions` update to a public proxy and answered E404 for a
|
|
1536
|
+
// package that exists in the ISS ProjectFeed-ISS feed. A package served by a
|
|
1537
|
+
// private feed is therefore updated through that feed, using the SAME
|
|
1538
|
+
// auth/config seams as the bootstrap installer (short-lived `az` token →
|
|
1539
|
+
// 0600 temporary userconfig → npm, with the scoped registry pinned on the
|
|
1540
|
+
// command line so ambient config cannot redirect it). Anything not on a
|
|
1541
|
+
// private channel — the public package above all — keeps the plain npm path
|
|
1542
|
+
// below unchanged.
|
|
1543
|
+
const internalInstaller = require('./install-internal-minions');
|
|
1544
|
+
let feed = null;
|
|
1545
|
+
try {
|
|
1546
|
+
feed = internalInstaller.resolveInternalFeed(pkgName, { registry: registryOverride || undefined });
|
|
1547
|
+
} catch (e) {
|
|
1548
|
+
console.error(`\n ERROR: invalid --registry value: ${e.message}\n`);
|
|
1549
|
+
process.exit(1);
|
|
1550
|
+
}
|
|
1551
|
+
if (registryOverride && !feed) {
|
|
1552
|
+
console.error(`\n ERROR: --registry applies only to an internal channel install; ${pkgName} is served by your own npm registry.\n`);
|
|
1553
|
+
process.exit(1);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
let feedSession = null;
|
|
1557
|
+
// Every failure below exits the process, and process.exit() skips `finally`,
|
|
1558
|
+
// so the staged credential is removed explicitly here. The session also
|
|
1559
|
+
// registers its own process-exit hook as a backstop.
|
|
1560
|
+
const exitUpdate = (code) => {
|
|
1561
|
+
if (feedSession) { feedSession.close(); feedSession = null; }
|
|
1562
|
+
process.exit(code);
|
|
1563
|
+
};
|
|
1564
|
+
// Feed failures arrive pre-formatted as operator-actionable lines (which
|
|
1565
|
+
// feed, which auth flow, what a 404 there actually means); anything else is
|
|
1566
|
+
// a plain npm/exec failure.
|
|
1567
|
+
const reportUpdateFailure = (e, fallback) => {
|
|
1568
|
+
if (e && Array.isArray(e.lines)) {
|
|
1569
|
+
console.error('');
|
|
1570
|
+
for (const line of e.lines) console.error(` ${line}`);
|
|
1571
|
+
console.error('');
|
|
1572
|
+
} else {
|
|
1573
|
+
console.error(fallback, e && e.message ? e.message : e);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1371
1577
|
// Resolve the target version explicitly BEFORE installing, and pin the
|
|
1372
1578
|
// install to that exact version instead of a floating `@latest` tag. This
|
|
1373
1579
|
// avoids a race where `npm view` (used here and by `minions version`) and
|
|
@@ -1379,21 +1585,51 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1379
1585
|
// though `npm view pkg version` reports a newer one (W-mr9k4a5x00090184).
|
|
1380
1586
|
let targetVersion;
|
|
1381
1587
|
try {
|
|
1382
|
-
|
|
1588
|
+
if (feed) {
|
|
1589
|
+
console.log(` Internal channel — resolving ${pkgName} from ${feed.registry}`);
|
|
1590
|
+
feedSession = internalInstaller.openInternalFeedSession(feed);
|
|
1591
|
+
targetVersion = feedSession.resolveVersion();
|
|
1592
|
+
} else {
|
|
1593
|
+
targetVersion = execSync(shared.buildNpmViewVersionCommand(pkgName), { encoding: 'utf8', timeout: 15000, windowsHide: true }).trim();
|
|
1594
|
+
}
|
|
1383
1595
|
} catch (e) {
|
|
1384
|
-
|
|
1385
|
-
|
|
1596
|
+
reportUpdateFailure(e, ` Failed to resolve the latest version of ${pkgName} from the npm registry:`);
|
|
1597
|
+
exitUpdate(1);
|
|
1386
1598
|
}
|
|
1387
1599
|
if (!targetVersion) {
|
|
1388
|
-
console.error(` Failed to resolve the latest version of ${pkgName} from the npm registry (empty response).`);
|
|
1389
|
-
|
|
1600
|
+
console.error(` Failed to resolve the latest version of ${pkgName} from the ${feed ? 'internal feed' : 'npm registry'} (empty response).`);
|
|
1601
|
+
exitUpdate(1);
|
|
1602
|
+
}
|
|
1603
|
+
// The public path validates the version inside its install-command builder;
|
|
1604
|
+
// a feed-resolved version reaches npm as an argument the same way, so gate
|
|
1605
|
+
// it identically rather than trusting registry output.
|
|
1606
|
+
if (feed && !shared.isValidNpmVersion(targetVersion)) {
|
|
1607
|
+
console.error(` Refusing to install an invalid version resolved from the internal feed: ${JSON.stringify(targetVersion)}`);
|
|
1608
|
+
exitUpdate(1);
|
|
1390
1609
|
}
|
|
1391
1610
|
|
|
1611
|
+
// DOWNGRADE GUARD (W-ms6b7prf006tef7f-d). The same stale-packument failure
|
|
1612
|
+
// described above can also resolve BACKWARDS, in which case the install
|
|
1613
|
+
// succeeds and the post-install check below passes (on-disk == target) while
|
|
1614
|
+
// the runtime silently moves to an older build. Compare numerically against
|
|
1615
|
+
// what is on disk and refuse by default.
|
|
1616
|
+
const downgrade = shared.evaluateUpdateDowngrade({ installedVersion: preVersion, targetVersion, allowDowngrade });
|
|
1617
|
+
if (!downgrade.allowed) {
|
|
1618
|
+
console.error(`\n REFUSED: ${pkgName}@${targetVersion} would downgrade the installed ${pkgName}@${preVersion}.`);
|
|
1619
|
+
for (const line of downgrade.message.split('\n')) console.error(` ${line}`);
|
|
1620
|
+
console.error('');
|
|
1621
|
+
exitUpdate(1);
|
|
1622
|
+
}
|
|
1623
|
+
if (downgrade.message) console.log(` ${downgrade.message}`);
|
|
1624
|
+
|
|
1392
1625
|
try {
|
|
1393
|
-
|
|
1626
|
+
if (feedSession) feedSession.install(targetVersion);
|
|
1627
|
+
else execSync(shared.buildNpmGlobalInstallCommand(pkgName, targetVersion), { stdio: 'inherit', timeout: 120000 });
|
|
1394
1628
|
} catch (e) {
|
|
1395
|
-
|
|
1396
|
-
|
|
1629
|
+
reportUpdateFailure(e, ' npm update failed:');
|
|
1630
|
+
exitUpdate(1);
|
|
1631
|
+
} finally {
|
|
1632
|
+
if (feedSession) { feedSession.close(); feedSession = null; }
|
|
1397
1633
|
}
|
|
1398
1634
|
// Equivalent to `minions init --force --skip-start`, but avoids recursing through
|
|
1399
1635
|
// the global shim while npm is still settling the updated install.
|
|
@@ -1470,6 +1706,36 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1470
1706
|
}
|
|
1471
1707
|
console.log('\n Starting Minions (engine + dashboard + supervisor)...');
|
|
1472
1708
|
spawnFullStackAndVerify({ rest, forceOpen, dashWasUp: false, restartStartMs: Date.now() });
|
|
1709
|
+
} else if (cmd === 'stop' && rest.includes('--all')) {
|
|
1710
|
+
// Opt-in WHOLE-STACK stop. Bare `minions stop` deliberately keeps its
|
|
1711
|
+
// engine-only, non-blocking delegation (engineCmds → `engine.js stop`, which
|
|
1712
|
+
// requests shutdown and returns) because recovery paths, operator scripts
|
|
1713
|
+
// and the engine's own callers depend on that semantic. This branch is the
|
|
1714
|
+
// additive contract on top of it.
|
|
1715
|
+
//
|
|
1716
|
+
// minions stop --all full ordered teardown; does not block on
|
|
1717
|
+
// the engine/state.db handle release
|
|
1718
|
+
// minions stop --all --wait additionally waits for the WAL index to be
|
|
1719
|
+
// released, i.e. proves quiescence
|
|
1720
|
+
//
|
|
1721
|
+
// The exit code is the machine-readable result (STOP_STACK_EXIT): 0 stopped,
|
|
1722
|
+
// 3 not quiesced within the budget, 4 refused on an unverified holder. No
|
|
1723
|
+
// caller should ever have to scrape this output.
|
|
1724
|
+
ensureInstalled();
|
|
1725
|
+
const stopAllWait = rest.includes('--wait');
|
|
1726
|
+
const stopAllVerdict = stopWholeRuntimeStack({
|
|
1727
|
+
source: stopAllWait ? 'minions stop --all --wait' : 'minions stop --all',
|
|
1728
|
+
timeoutMs: _resolveStopAllTimeoutMs(rest),
|
|
1729
|
+
waitForRelease: stopAllWait,
|
|
1730
|
+
});
|
|
1731
|
+
process.exit(stopStackExitCode(stopAllVerdict));
|
|
1732
|
+
} else if (cmd === 'stop' && rest.includes('--wait')) {
|
|
1733
|
+
// `--wait` alone would silently wait for nothing: the engine-only delegation
|
|
1734
|
+
// returns as soon as shutdown is REQUESTED. Fail loudly instead.
|
|
1735
|
+
console.error('\n ERROR: `--wait` only applies to the whole-stack stop.');
|
|
1736
|
+
console.error(' Run `minions stop --all --wait` to wait for the runtime handles to be released,');
|
|
1737
|
+
console.error(' or `minions stop` to request engine shutdown and return immediately.\n');
|
|
1738
|
+
process.exit(1);
|
|
1473
1739
|
} else if (cmd === 'restart') {
|
|
1474
1740
|
// `--cli` / `--model` flags forward to `engine.js start` so the runtime
|
|
1475
1741
|
// fleet flips before the daemon spawns (P-6b3f9c2e AC: works on restart).
|
|
@@ -1480,16 +1746,6 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1480
1746
|
const restartResolved = resolveDashboardPort(rest);
|
|
1481
1747
|
const dashWasUp = isPortListening(restartResolved.port);
|
|
1482
1748
|
const restartStartMs = Date.now();
|
|
1483
|
-
// Pre-write stop-intent so the supervisor doesn't race-respawn the
|
|
1484
|
-
// engine/dashboard we're about to kill. Killed before the engine/dashboard
|
|
1485
|
-
// so its 30s tick doesn't fire mid-restart against a half-dead engine.
|
|
1486
|
-
writeStopIntent('minions restart');
|
|
1487
|
-
killSupervisor();
|
|
1488
|
-
// Layered kill — each step is best-effort, so the next still runs if one
|
|
1489
|
-
// fails. Goal: the old engine is gone before we spawn a new one, even if
|
|
1490
|
-
// PowerShell is unavailable, the engine is hung, or its cmdline doesn't match.
|
|
1491
|
-
const oldEnginePid = readEnginePid(MINIONS_HOME);
|
|
1492
|
-
try { execSync(`node "${path.join(MINIONS_HOME, 'engine.js')}" stop`, { stdio: 'ignore', cwd: MINIONS_HOME, timeout: 10000, windowsHide: true }); } catch {}
|
|
1493
1749
|
// Give the engine's graceful handler time to drain non-reattachable pooled
|
|
1494
1750
|
// leases. Detached cold agents are not part of that drain and remain alive
|
|
1495
1751
|
// for normal PID-file reattachment by the replacement engine.
|
|
@@ -1499,34 +1755,40 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1499
1755
|
return Number(cfg?.engine?.shutdownTimeout) || shared.ENGINE_DEFAULTS.shutdownTimeout;
|
|
1500
1756
|
} catch { return shared.ENGINE_DEFAULTS.shutdownTimeout; }
|
|
1501
1757
|
})();
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1758
|
+
// Ordered teardown — stop-intent first so the supervisor stands down and
|
|
1759
|
+
// cannot race-respawn what we are about to stop, then supervisor, dashboard,
|
|
1760
|
+
// graceful engine drain, verified reap, and the late-respawn sweep that
|
|
1761
|
+
// closes the watchdog race window. All of it lives in
|
|
1762
|
+
// engine/recovery/stop-stack.js; restart owns only what follows.
|
|
1763
|
+
// The WAL-index release is deliberately NOT awaited: restart replaces the
|
|
1764
|
+
// stack, and the port gate below is its real readiness condition.
|
|
1765
|
+
const teardown = stopWholeRuntimeStack({
|
|
1766
|
+
source: 'minions restart',
|
|
1767
|
+
timeoutMs: shutdownTimeout + 5000,
|
|
1768
|
+
waitForRelease: false,
|
|
1769
|
+
});
|
|
1770
|
+
// A holder that is provably NOT one of our daemons (a stale PID file, a
|
|
1771
|
+
// recycled PID) must not wedge the command restart exists to heal — the port
|
|
1772
|
+
// gate below still refuses to spawn over a live dashboard. Anything else
|
|
1773
|
+
// means a daemon survived, and spawning a second stack over it is exactly
|
|
1774
|
+
// the multi-engine state we must never create.
|
|
1775
|
+
const blockingHolders = teardown.holders.filter(h => holderIsLiveDaemon(h) || h.name === 'engine');
|
|
1776
|
+
if (blockingHolders.length > 0) {
|
|
1777
|
+
console.error(`\n ERROR: Refusing restart — ${teardown.reason}`);
|
|
1778
|
+
console.error(' The new stack cannot safely take over control.json while those processes are alive.');
|
|
1779
|
+
process.exit(1);
|
|
1507
1780
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
// ~10ms later hits EADDRINUSE and exits, producing the "Restart verification
|
|
1514
|
-
// failed, port=7331 listening=no" symptom against an orphan PID that's
|
|
1515
|
-
// STILL bound to the port a heartbeat later.
|
|
1516
|
-
if (oldEnginePid) {
|
|
1517
|
-
const engineDead = waitForPidDeath(oldEnginePid, 5000);
|
|
1518
|
-
if (!engineDead.ok) {
|
|
1519
|
-
console.error(`\n ERROR: Old engine (PID ${oldEnginePid}) did not exit within 5s after kill.`);
|
|
1520
|
-
console.error(` The new engine cannot safely take over control.json. Aborting restart.`);
|
|
1521
|
-
process.exit(1);
|
|
1781
|
+
let portFree = waitForPortRelease(restartResolved.port, 10000);
|
|
1782
|
+
if (!portFree.ok) {
|
|
1783
|
+
const retriedPids = retryKillVerifiedPids(portFree.stillBound, daemonScriptPaths());
|
|
1784
|
+
if (retriedPids.length > 0) {
|
|
1785
|
+
portFree = waitForPortRelease(restartResolved.port, 5000);
|
|
1522
1786
|
}
|
|
1523
1787
|
}
|
|
1524
|
-
const portFree = waitForPortRelease(restartResolved.port, 10000);
|
|
1525
1788
|
if (!portFree.ok) {
|
|
1526
|
-
console.error(`\n ERROR: Port ${restartResolved.port} still in use after
|
|
1789
|
+
console.error(`\n ERROR: Port ${restartResolved.port} still in use after verified stop retries.`);
|
|
1527
1790
|
console.error(` Bound by PID(s): ${portFree.stillBound.join(', ')}`);
|
|
1528
|
-
console.error(
|
|
1529
|
-
console.error(` Then retry: minions restart`);
|
|
1791
|
+
console.error(' Refusing to terminate an unverified port owner. Stop that application explicitly, then retry.');
|
|
1530
1792
|
process.exit(1);
|
|
1531
1793
|
}
|
|
1532
1794
|
// Wipe the runtime port file so a stale entry from the old dashboard
|
|
@@ -1539,11 +1801,17 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1539
1801
|
// fresh engine/dashboard during the kill→spawn window — those become ORPHANS
|
|
1540
1802
|
// the new stack never reaps, and orphan engines keep dispatching agents +
|
|
1541
1803
|
// spawning copilot (the root of the recurring multi-engine / MCP-auth storms).
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
//
|
|
1545
|
-
//
|
|
1546
|
-
|
|
1804
|
+
// The sweep itself is step 8 of the shared teardown above (it runs with
|
|
1805
|
+
// stop-intent still set, so any respawn has already stood down, and a
|
|
1806
|
+
// survivor comes back as a holder and aborts this command). What remains
|
|
1807
|
+
// restart-specific is proving the port was not reclaimed in the window
|
|
1808
|
+
// between that sweep and the spawn below.
|
|
1809
|
+
const finalPortFree = waitForPortRelease(restartResolved.port, 5000);
|
|
1810
|
+
if (!finalPortFree.ok) {
|
|
1811
|
+
console.error(`\n ERROR: Port ${restartResolved.port} was reclaimed during the restart race window.`);
|
|
1812
|
+
console.error(` Bound by PID(s): ${finalPortFree.stillBound.join(', ')}`);
|
|
1813
|
+
process.exit(1);
|
|
1814
|
+
}
|
|
1547
1815
|
// Clear stop-intent so the freshly-spawned supervisor resumes guarding.
|
|
1548
1816
|
clearStopIntent();
|
|
1549
1817
|
spawnFullStackAndVerify({ rest, forceOpen, dashWasUp, restartStartMs });
|
|
@@ -1576,9 +1844,16 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1576
1844
|
console.log('\n Minions Factory Reset\n');
|
|
1577
1845
|
|
|
1578
1846
|
// 1. Kill all processes
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1847
|
+
// The ordered teardown (stop-intent → supervisor → dashboard → engine drain
|
|
1848
|
+
// → verified reap) stands the supervisor down FIRST, so it cannot
|
|
1849
|
+
// race-respawn the engine while we delete the state database underneath it.
|
|
1850
|
+
stopWholeRuntimeStack({ source: 'minions nuke', timeoutMs: FORCED_STOP_BUDGET_MS });
|
|
1851
|
+
// Agents are deliberately spared by the shared teardown so restarts can
|
|
1852
|
+
// reattach them; a factory reset deletes their state, so they go too.
|
|
1853
|
+
killMinionsProcesses(daemonScriptPaths({ includeAgents: true }), { includeDescendants: true });
|
|
1854
|
+
// The nested `init` below re-spawns the stack, so this root must not be left
|
|
1855
|
+
// carrying a stop-intent that would stand the new supervisor down.
|
|
1856
|
+
clearStopIntent();
|
|
1582
1857
|
shared.clearDashboardPortFile(MINIONS_HOME);
|
|
1583
1858
|
console.log(' Killed all processes');
|
|
1584
1859
|
|
|
@@ -1664,12 +1939,12 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1664
1939
|
// 1. Kill all processes
|
|
1665
1940
|
// Stop-intent FIRST so the supervisor stands down before we kill it. Without
|
|
1666
1941
|
// this the supervisor could race-respawn the engine/dashboard between our
|
|
1667
|
-
// engine-stop and the supervisor kill.
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
killMinionsProcesses(
|
|
1942
|
+
// engine-stop and the supervisor kill. That ordering — and the graceful
|
|
1943
|
+
// engine drain that follows it — is owned by engine/recovery/stop-stack.js.
|
|
1944
|
+
stopWholeRuntimeStack({ source: 'minions uninstall', timeoutMs: FORCED_STOP_BUDGET_MS });
|
|
1945
|
+
// Agents are deliberately spared by the shared teardown so restarts can
|
|
1946
|
+
// reattach them; an uninstall deletes the runtime root, so they go too.
|
|
1947
|
+
killMinionsProcesses(daemonScriptPaths({ includeAgents: true }), { includeDescendants: true });
|
|
1673
1948
|
console.log(' Killed all processes');
|
|
1674
1949
|
|
|
1675
1950
|
// 2. Remove minions-authored skills from ~/.claude/skills/
|
|
@@ -1680,7 +1955,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1680
1955
|
const skillFile = path.join(claudeSkills, dir, 'SKILL.md');
|
|
1681
1956
|
try {
|
|
1682
1957
|
const content = fs.readFileSync(skillFile, 'utf8');
|
|
1683
|
-
if (
|
|
1958
|
+
if (/<!-- Auto-extracted by [^>]+ on \d{4}-\d{2}-\d{2} -->/.test(content)) {
|
|
1684
1959
|
fs.rmSync(path.join(claudeSkills, dir), { recursive: true, force: true });
|
|
1685
1960
|
}
|
|
1686
1961
|
} catch {}
|
|
@@ -1704,13 +1979,27 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1704
1979
|
console.log(` npm install -g ${uninstallPkgName} && minions init\n`);
|
|
1705
1980
|
} else if (cmd === 'state') {
|
|
1706
1981
|
ensureInstalled();
|
|
1707
|
-
const operations = require(path.join(MINIONS_HOME, 'engine', 'state-operations'));
|
|
1982
|
+
const operations = require(path.join(MINIONS_HOME, 'engine', 'persistence', 'state-operations'));
|
|
1708
1983
|
const sub = rest[0];
|
|
1709
1984
|
try {
|
|
1710
1985
|
let result;
|
|
1711
1986
|
if (sub === 'check') result = operations.checkState();
|
|
1712
1987
|
else if (sub === 'backup') result = operations.backupState(rest[1]);
|
|
1713
1988
|
else if (sub === 'export') result = operations.exportState(rest[1]);
|
|
1989
|
+
else if (sub === 'summary') {
|
|
1990
|
+
result = operations.summarizeState();
|
|
1991
|
+
if (!rest.includes('--json')) {
|
|
1992
|
+
console.log(`\n Schema version: ${result.schemaVersion}`);
|
|
1993
|
+
console.log(` Fingerprint: ${result.fingerprint}`);
|
|
1994
|
+
console.log(' Row counts:');
|
|
1995
|
+
for (const [table, count] of Object.entries(result.tables)) {
|
|
1996
|
+
console.log(` ${String(table).padEnd(30)} ${count === null ? 'n/a' : count}`);
|
|
1997
|
+
}
|
|
1998
|
+
console.log('');
|
|
1999
|
+
process.exit(0);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
else if (sub === 'verify-backup') result = operations.verifyBackup(rest[1]);
|
|
1714
2003
|
else if (sub === 'import-legacy-json') {
|
|
1715
2004
|
if (!rest.includes('--confirm')) throw new Error('legacy import requires --confirm');
|
|
1716
2005
|
const enginePid = readEnginePid(MINIONS_HOME);
|
|
@@ -1725,7 +2014,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1725
2014
|
result = operations.importLegacyJson(rest[1]);
|
|
1726
2015
|
}
|
|
1727
2016
|
else {
|
|
1728
|
-
console.error('\n Usage: minions state (check|backup <file>|export <file>|import-legacy-json <dir> --confirm)\n');
|
|
2017
|
+
console.error('\n Usage: minions state (check|backup <file>|summary [--json]|verify-backup <file>|export <file>|import-legacy-json <dir> --confirm)\n');
|
|
1729
2018
|
process.exit(2);
|
|
1730
2019
|
}
|
|
1731
2020
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -1765,7 +2054,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1765
2054
|
}
|
|
1766
2055
|
}
|
|
1767
2056
|
try {
|
|
1768
|
-
const { backfillReviewLearnings } = require(path.join(MINIONS_HOME, 'engine', 'review-learning-backfill'));
|
|
2057
|
+
const { backfillReviewLearnings } = require(path.join(MINIONS_HOME, 'engine', 'memory', 'review-learning-backfill'));
|
|
1769
2058
|
const result = backfillReviewLearnings({ dryRun, limit, project });
|
|
1770
2059
|
console.log(JSON.stringify(result, null, 2));
|
|
1771
2060
|
process.exit(0);
|
|
@@ -1775,7 +2064,7 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1775
2064
|
}
|
|
1776
2065
|
} else if (cmd === 'doctor') {
|
|
1777
2066
|
ensureInstalled();
|
|
1778
|
-
const { doctor, runHarnessDoctor } = require(path.join(MINIONS_HOME, 'engine', 'preflight'));
|
|
2067
|
+
const { doctor, runHarnessDoctor } = require(path.join(MINIONS_HOME, 'engine', 'agents', 'preflight'));
|
|
1779
2068
|
if (rest.includes('--harness')) {
|
|
1780
2069
|
// Print the read-only runtime harness inventory and exit. The
|
|
1781
2070
|
// legacy `minions doctor` still runs the full preflight + runtime fleet
|
|
@@ -1791,11 +2080,15 @@ ${fs.existsSync(path.join(PKG_ROOT, '.git')) ? `
|
|
|
1791
2080
|
// the in-process supervisor can't (Windows job-object teardown when the
|
|
1792
2081
|
// parent terminal closes, Linux OOM-killer, cgroup limits, pkill, reboot).
|
|
1793
2082
|
ensureInstalled();
|
|
1794
|
-
const watchdog = require(path.join(MINIONS_HOME, 'engine', 'watchdog'));
|
|
2083
|
+
const watchdog = require(path.join(MINIONS_HOME, 'engine', 'recovery', 'watchdog'));
|
|
1795
2084
|
const sub = rest[0];
|
|
1796
|
-
|
|
2085
|
+
// Source checkouts should recover with the same code they run, not an older
|
|
2086
|
+
// globally installed package. npm-state installs have no local bin and keep
|
|
2087
|
+
// using this executable as the fallback.
|
|
2088
|
+
const localMinionsBin = path.join(MINIONS_HOME, 'bin', 'minions.js');
|
|
2089
|
+
const minionsBin = fs.existsSync(localMinionsBin) ? localMinionsBin : __filename;
|
|
1797
2090
|
if (sub === 'tick') {
|
|
1798
|
-
const shared = require(path.join(MINIONS_HOME, 'engine', 'shared'));
|
|
2091
|
+
const shared = require(path.join(MINIONS_HOME, 'engine', 'core', 'shared'));
|
|
1799
2092
|
// Resolve the live dashboard port via the in-file resolver, which honors
|
|
1800
2093
|
// the runtime beacon (written by dashboard.js after bind), env, and
|
|
1801
2094
|
// config — so the watchdog probes whatever port the dashboard actually
|