@chllming/wave-orchestration 0.6.2 → 0.6.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.3 - 2026-03-22
6
+
7
+ - Added a best-effort npmjs update notice on `wave launch`, `wave autonomous`, and `wave adhoc run`, with cached lookup state under `.wave/package-update-check.json` and opt-out via `WAVE_SKIP_UPDATE_CHECK=1`.
8
+ - Added `wave self-update`, which detects the workspace package manager, updates `@chllming/wave-orchestration`, prints the changelog delta since the recorded install, and then runs `wave upgrade`.
9
+ - Suppressed duplicate notices for nested launcher calls so autonomous and ad-hoc runs announce at most once, while keeping JSON-oriented stdout surfaces clean by emitting notices on stderr.
10
+ - Documented the new update flow and added regression coverage for notice caching, package-manager-aware self-update, and nested-launch suppression.
11
+
5
12
  ## 0.6.2 - 2026-03-22
6
13
 
7
14
  - Added first-class `claude.effort` support across config profiles, lane overrides, and per-agent `### Executor` blocks, and now emit `--effort` in Claude launch previews and live runs.
package/README.md CHANGED
@@ -75,17 +75,16 @@ Wave is built to mitigate those failures with canonical shared state, generated
75
75
 
76
76
  Current release:
77
77
 
78
- - `@chllming/wave-orchestration@0.6.2`
79
- - Release tag: [`v0.6.2`](https://github.com/chllming/wave-orchestration/releases/tag/v0.6.2)
78
+ - `@chllming/wave-orchestration@0.6.3`
79
+ - Release tag: [`v0.6.3`](https://github.com/chllming/wave-orchestration/releases/tag/v0.6.3)
80
80
  - Public install path: npmjs
81
81
  - Authenticated fallback: GitHub Packages
82
82
 
83
- Highlights in `0.6.2`:
83
+ Highlights in `0.6.3`:
84
84
 
85
- - Runtime previews and docs now expose first-class Claude effort plus structured limit metadata, making known Claude/OpenCode ceilings explicit and Codex opacity explicit.
86
- - The global dashboard and VS Code terminal surfaces are easier to read: active vs pending counts are distinct, the current-wave dashboard keeps a stable terminal name, and TTY dashboards now use simple color cues.
87
- - Dry-run executor preview directories now prune stale agent folders when a wave shrinks.
88
- - Shared promoted-component retries now preserve already-landed owner slices and relaunch only the sibling owners still needed for closure.
85
+ - Runtime launch entrypoints now check npmjs for a newer published package in the background, cache the result under `.wave/package-update-check.json`, and warn on stderr when the workspace is behind.
86
+ - `wave self-update` now gives downstream repos a one-command update path that detects the workspace package manager, updates the dependency, shows the changelog delta, and records the workspace upgrade report.
87
+ - Autonomous and ad-hoc flows suppress nested notices so operators see at most one update banner per top-level run, and structured stdout remains clean for JSON consumers.
89
88
 
90
89
  Requirements:
91
90
 
@@ -113,6 +112,8 @@ pnpm exec wave init --adopt-existing
113
112
 
114
113
  Fresh init also seeds a starter `skills/` library plus `docs/evals/benchmark-catalog.json`. The launcher projects those skill bundles into Codex, Claude, OpenCode, and local executor overlays after the final runtime for each agent is resolved, and waves that include `cont-EVAL` can declare `## Eval targets` against that catalog.
115
114
 
115
+ When runtime launch commands detect a newer npmjs release, Wave prints a non-blocking update notice on stderr. The fast path is `pnpm exec wave self-update`, which updates the dependency, prints the changelog delta, and then records the workspace upgrade report.
116
+
116
117
  ## Common Commands
117
118
 
118
119
  ```bash
@@ -129,6 +130,9 @@ pnpm exec wave dep show --lane main --wave 0 --json
129
130
 
130
131
  # Run autonomous mode after the wave set is stable
131
132
  pnpm exec wave autonomous --lane main --executor codex --codex-sandbox danger-full-access
133
+
134
+ # Pull the latest published package and record the workspace upgrade
135
+ pnpm exec wave self-update
132
136
  ```
133
137
 
134
138
  ## Develop This Package
@@ -1,8 +1,9 @@
1
1
  # Current State
2
2
 
3
- - The starter workspace in this source repo reflects the `0.6.1` package release surface.
3
+ - The starter workspace in this source repo reflects the `0.6.3` package release surface.
4
4
  - The repository contains the published `@chllming/wave-orchestration` package plus the starter scaffold used by `wave init`.
5
5
  - The runtime is package-first and non-destructive for adopting repos: `wave init --adopt-existing` records existing repo-owned plans, waves, prompts, and config without overwriting them, and `wave upgrade` writes only `.wave/install-state.json` plus `.wave/upgrade-history/`.
6
+ - Runtime launch entrypoints now perform a best-effort npmjs version check, cache the result under `.wave/package-update-check.json`, and point operators at `pnpm exec wave self-update` when a newer published package exists.
6
7
  - This source repo is itself kept as an adopted Wave workspace, so `node scripts/wave.mjs doctor --json` should pass from the repo root.
7
8
  - The default lane is `main`.
8
9
  - Planner foundation is now shipped:
@@ -54,6 +54,7 @@ This runbook is the operational view of the architecture:
54
54
  - `pnpm exec wave dep show --lane main --wave 0 --json`
55
55
  - `pnpm exec wave dep post --owner-lane main --requester-lane release --owner-wave 0 --requester-wave 2 --agent launcher --summary "Need shared-plan reconciliation" --target capability:docs-shared-plan --required`
56
56
  - `pnpm exec wave upgrade`
57
+ - `pnpm exec wave self-update`
57
58
 
58
59
  ## Configuration
59
60
 
@@ -151,6 +152,16 @@ Required inbound dependencies block autonomous next-wave start and lane finaliza
151
152
 
152
153
  ## Upgrade Flow
153
154
 
155
+ Fast path:
156
+
157
+ ```bash
158
+ pnpm exec wave self-update
159
+ ```
160
+
161
+ That command updates the dependency through the workspace package manager, prints the changelog delta since the recorded install, and then runs `wave upgrade` to record the new install-state and upgrade report.
162
+
163
+ Manual path:
164
+
154
165
  1. Upgrade the package version:
155
166
 
156
167
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chllming/wave-orchestration",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "license": "MIT",
5
5
  "description": "Generic wave-based multi-agent orchestration for repository work.",
6
6
  "repository": {
@@ -2,6 +2,21 @@
2
2
  "schemaVersion": 1,
3
3
  "packageName": "@chllming/wave-orchestration",
4
4
  "releases": [
5
+ {
6
+ "version": "0.6.3",
7
+ "date": "2026-03-22",
8
+ "summary": "Runtime npmjs update notices plus a one-command self-update flow for downstream repos.",
9
+ "features": [
10
+ "Top-level runtime entrypoints now perform a best-effort npmjs version check, cache the result under `.wave/package-update-check.json`, and emit a non-blocking stderr notice when a newer `@chllming/wave-orchestration` release is available.",
11
+ "New `wave self-update` detects the workspace package manager, updates the package dependency to the latest published release, prints the changelog delta since the recorded install, and then runs `wave upgrade`.",
12
+ "Autonomous and ad-hoc flows now suppress nested update notices so operators see at most one banner per top-level run, while structured stdout such as `wave adhoc run --json` remains parseable."
13
+ ],
14
+ "manualSteps": [
15
+ "No migration is required. If you prefer not to check npmjs at runtime on a workstation, set `WAVE_SKIP_UPDATE_CHECK=1` in that shell environment.",
16
+ "After upgrading, try `pnpm exec wave self-update` once in an adopted repo to confirm the workspace package manager and install-state workflow behave the way you expect."
17
+ ],
18
+ "breaking": false
19
+ },
5
20
  {
6
21
  "version": "0.6.2",
7
22
  "date": "2026-03-22",
@@ -5,9 +5,7 @@ import { bootstrapWaveArgs } from "./wave-cli-bootstrap.mjs";
5
5
  const argv = bootstrapWaveArgs(process.argv.slice(2));
6
6
  const { runAutonomousCli } = await import("./wave-orchestrator/autonomous.mjs");
7
7
 
8
- try {
9
- runAutonomousCli(argv);
10
- } catch (error) {
8
+ runAutonomousCli(argv).catch((error) => {
11
9
  console.error(`[wave-autonomous] ${error instanceof Error ? error.message : String(error)}`);
12
10
  process.exit(1);
13
- }
11
+ });
@@ -8,6 +8,10 @@ import {
8
8
  buildDefaultProjectProfile,
9
9
  readProjectProfile,
10
10
  } from "./project-profile.mjs";
11
+ import {
12
+ maybeAnnouncePackageUpdate,
13
+ WAVE_SUPPRESS_UPDATE_NOTICE_ENV,
14
+ } from "./package-update-notice.mjs";
11
15
  import { runLauncherCli } from "./launcher.mjs";
12
16
  import { renderWaveMarkdown } from "./planner.mjs";
13
17
  import {
@@ -187,6 +191,20 @@ function buildAdhocRunId() {
187
191
  return sanitizeAdhocRunId(`adhoc-${stamp}-${random}`);
188
192
  }
189
193
 
194
+ async function withSuppressedNestedUpdateNotice(fn) {
195
+ const previousValue = process.env[WAVE_SUPPRESS_UPDATE_NOTICE_ENV];
196
+ process.env[WAVE_SUPPRESS_UPDATE_NOTICE_ENV] = "1";
197
+ try {
198
+ return await fn();
199
+ } finally {
200
+ if (previousValue === undefined) {
201
+ delete process.env[WAVE_SUPPRESS_UPDATE_NOTICE_ENV];
202
+ } else {
203
+ process.env[WAVE_SUPPRESS_UPDATE_NOTICE_ENV] = previousValue;
204
+ }
205
+ }
206
+ }
207
+
190
208
  function readEffectiveProjectProfile(config) {
191
209
  return readProjectProfile({ config }) || buildDefaultProjectProfile(config);
192
210
  }
@@ -1135,6 +1153,7 @@ export async function runAdhocCli(argv) {
1135
1153
  if (options.tasks.length === 0) {
1136
1154
  throw new Error("At least one --task is required for `wave adhoc run`.");
1137
1155
  }
1156
+ await maybeAnnouncePackageUpdate();
1138
1157
  const stored = createStoredRun({ config, options });
1139
1158
  const summary = summarizePlan(stored.spec, stored.lanePaths);
1140
1159
  if (options.json) {
@@ -1157,17 +1176,19 @@ export async function runAdhocCli(argv) {
1157
1176
  writeJsonAtomic(stored.lanePaths.adhocResultPath, runningResult);
1158
1177
  upsertAdhocIndexEntry(stored.lanePaths.adhocIndexPath, runningResult);
1159
1178
  try {
1160
- await runLauncherCli([
1161
- "--lane",
1162
- stored.lanePaths.lane,
1163
- "--adhoc-run",
1164
- stored.lanePaths.runId,
1165
- "--start-wave",
1166
- String(ADHOC_WAVE_NUMBER),
1167
- "--end-wave",
1168
- String(ADHOC_WAVE_NUMBER),
1169
- ...options.launcherArgs,
1170
- ]);
1179
+ await withSuppressedNestedUpdateNotice(() =>
1180
+ runLauncherCli([
1181
+ "--lane",
1182
+ stored.lanePaths.lane,
1183
+ "--adhoc-run",
1184
+ stored.lanePaths.runId,
1185
+ "--start-wave",
1186
+ String(ADHOC_WAVE_NUMBER),
1187
+ "--end-wave",
1188
+ String(ADHOC_WAVE_NUMBER),
1189
+ ...options.launcherArgs,
1190
+ ]),
1191
+ );
1171
1192
  const completedResult = buildResultRecord(
1172
1193
  stored.lanePaths,
1173
1194
  stored.request,
@@ -21,6 +21,10 @@ import {
21
21
  DEFAULT_CODEX_SANDBOX_MODE,
22
22
  normalizeCodexSandboxMode,
23
23
  } from "./launcher.mjs";
24
+ import {
25
+ maybeAnnouncePackageUpdate,
26
+ WAVE_SUPPRESS_UPDATE_NOTICE_ENV,
27
+ } from "./package-update-notice.mjs";
24
28
  import { readRunState } from "./wave-files.mjs";
25
29
  import { readDependencyTickets } from "./coordination-store.mjs";
26
30
  import { readWaveLedger } from "./ledger.mjs";
@@ -155,21 +159,30 @@ export function nextIncompleteWave(allWaves, completed) {
155
159
  return null;
156
160
  }
157
161
 
158
- function runCommand(args) {
162
+ function runCommand(args, envOverrides = {}) {
159
163
  const result = spawnSync("node", args, {
160
164
  cwd: REPO_ROOT,
161
165
  stdio: "inherit",
162
- env: process.env,
166
+ env: {
167
+ ...process.env,
168
+ ...envOverrides,
169
+ },
163
170
  });
164
171
  return Number.isInteger(result.status) ? result.status : 1;
165
172
  }
166
173
 
167
174
  function reconcile(lane) {
168
- return runCommand([path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"), "--lane", lane, "--reconcile-status"]);
175
+ return runCommand(
176
+ [path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"), "--lane", lane, "--reconcile-status"],
177
+ { [WAVE_SUPPRESS_UPDATE_NOTICE_ENV]: "1" },
178
+ );
169
179
  }
170
180
 
171
181
  function dryRun(lane) {
172
- return runCommand([path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"), "--lane", lane, "--dry-run", "--no-dashboard"]);
182
+ return runCommand(
183
+ [path.join(PACKAGE_ROOT, "scripts", "wave-launcher.mjs"), "--lane", lane, "--dry-run", "--no-dashboard"],
184
+ { [WAVE_SUPPRESS_UPDATE_NOTICE_ENV]: "1" },
185
+ );
173
186
  }
174
187
 
175
188
  function listPendingFeedback(lane) {
@@ -215,7 +228,7 @@ function launchSingleWave(params) {
215
228
  if (params.keepTerminals) {
216
229
  args.push("--keep-terminals");
217
230
  }
218
- return runCommand(args);
231
+ return runCommand(args, { [WAVE_SUPPRESS_UPDATE_NOTICE_ENV]: "1" });
219
232
  }
220
233
 
221
234
  function requiredInboundDependenciesOpen(lanePaths, lane) {
@@ -291,12 +304,13 @@ export function readAutonomousBarrier(lanePaths, lane, wave = null) {
291
304
  return null;
292
305
  }
293
306
 
294
- export function runAutonomousCli(argv) {
307
+ export async function runAutonomousCli(argv) {
295
308
  const parsed = parseArgs(argv);
296
309
  if (parsed.help) {
297
310
  printUsage();
298
311
  return;
299
312
  }
313
+ await maybeAnnouncePackageUpdate();
300
314
  const options = parsed.options;
301
315
  const allWaves = getWaveNumbers(options.lane);
302
316
  console.log(`[autonomous] lane=${options.lane} orchestrator=${options.orchestratorId}`);
@@ -1,10 +1,17 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
3
4
  import {
4
5
  applyContext7SelectionsToWave,
5
6
  loadContext7BundleIndex,
6
7
  } from "./context7.mjs";
7
8
  import { buildLanePaths, ensureDirectory, PACKAGE_ROOT, readJsonOrNull, REPO_ROOT, writeJsonAtomic } from "./shared.mjs";
9
+ import { fetchLatestPackageVersion } from "./package-update-notice.mjs";
10
+ import {
11
+ compareVersions,
12
+ readInstalledPackageMetadata,
13
+ WAVE_PACKAGE_NAME,
14
+ } from "./package-version.mjs";
8
15
  import { loadWaveConfig } from "./config.mjs";
9
16
  import { applyExecutorSelectionsToWave, parseWaveFiles, validateWaveDefinition } from "./wave-files.mjs";
10
17
  import { validateLaneSkillConfiguration } from "./skills.mjs";
@@ -14,7 +21,7 @@ export const INSTALL_STATE_DIR = ".wave";
14
21
  export const INSTALL_STATE_PATH = path.join(REPO_ROOT, INSTALL_STATE_DIR, "install-state.json");
15
22
  export const UPGRADE_HISTORY_DIR = path.join(REPO_ROOT, INSTALL_STATE_DIR, "upgrade-history");
16
23
  export const CHANGELOG_MANIFEST_PATH = path.join(PACKAGE_ROOT, "releases", "manifest.json");
17
- export const PACKAGE_METADATA_PATH = path.join(PACKAGE_ROOT, "package.json");
24
+ export const WORKSPACE_PACKAGE_JSON_PATH = path.join(REPO_ROOT, "package.json");
18
25
  export const STARTER_TEMPLATE_PATHS = [
19
26
  "wave.config.json",
20
27
  "docs/README.md",
@@ -69,11 +76,7 @@ function collectDeclaredDeployKinds(waves = []) {
69
76
  }
70
77
 
71
78
  function packageMetadata() {
72
- const payload = readJsonOrNull(PACKAGE_METADATA_PATH);
73
- if (!payload?.name || !payload?.version) {
74
- throw new Error(`Invalid package metadata: ${PACKAGE_METADATA_PATH}`);
75
- }
76
- return payload;
79
+ return readInstalledPackageMetadata();
77
80
  }
78
81
 
79
82
  function readInstallState() {
@@ -149,25 +152,6 @@ function nextHistoryRecord(existingState, entry) {
149
152
  return history;
150
153
  }
151
154
 
152
- function normalizeVersionParts(version) {
153
- return String(version || "")
154
- .split(".")
155
- .map((part) => Number.parseInt(part.replace(/[^0-9].*$/, ""), 10) || 0);
156
- }
157
-
158
- function compareVersions(a, b) {
159
- const left = normalizeVersionParts(a);
160
- const right = normalizeVersionParts(b);
161
- const length = Math.max(left.length, right.length);
162
- for (let index = 0; index < length; index += 1) {
163
- const diff = (left[index] || 0) - (right[index] || 0);
164
- if (diff !== 0) {
165
- return diff;
166
- }
167
- }
168
- return 0;
169
- }
170
-
171
155
  function readChangelogManifest() {
172
156
  const payload = readJsonOrNull(CHANGELOG_MANIFEST_PATH);
173
157
  if (!payload?.releases || !Array.isArray(payload.releases)) {
@@ -478,6 +462,186 @@ export function upgradeWorkspace() {
478
462
  };
479
463
  }
480
464
 
465
+ function readWorkspacePackageManifest(workspaceRoot = REPO_ROOT) {
466
+ const payload = readJsonOrNull(path.join(workspaceRoot, "package.json"));
467
+ if (!payload || typeof payload !== "object") {
468
+ throw new Error(`Missing package.json at ${path.join(workspaceRoot, "package.json")}`);
469
+ }
470
+ return payload;
471
+ }
472
+
473
+ function readInstallStateForWorkspace(workspaceRoot = REPO_ROOT) {
474
+ const payload = readJsonOrNull(path.join(workspaceRoot, INSTALL_STATE_DIR, "install-state.json"));
475
+ return payload && typeof payload === "object" ? payload : null;
476
+ }
477
+
478
+ function parsePackageManagerId(value) {
479
+ const normalized = String(value || "")
480
+ .trim()
481
+ .toLowerCase();
482
+ if (!normalized) {
483
+ return null;
484
+ }
485
+ if (normalized.startsWith("pnpm@")) {
486
+ return "pnpm";
487
+ }
488
+ if (normalized.startsWith("npm@")) {
489
+ return "npm";
490
+ }
491
+ if (normalized.startsWith("yarn@")) {
492
+ return "yarn";
493
+ }
494
+ if (normalized.startsWith("bun@")) {
495
+ return "bun";
496
+ }
497
+ return null;
498
+ }
499
+
500
+ export function detectWorkspacePackageManager(workspaceRoot = REPO_ROOT) {
501
+ const manifest = readWorkspacePackageManifest(workspaceRoot);
502
+ const packageManagerFromManifest = parsePackageManagerId(manifest.packageManager);
503
+ if (packageManagerFromManifest) {
504
+ return {
505
+ id: packageManagerFromManifest,
506
+ source: "packageManager",
507
+ raw: manifest.packageManager,
508
+ };
509
+ }
510
+ for (const [fileName, id] of [
511
+ ["pnpm-lock.yaml", "pnpm"],
512
+ ["package-lock.json", "npm"],
513
+ ["npm-shrinkwrap.json", "npm"],
514
+ ["yarn.lock", "yarn"],
515
+ ["bun.lockb", "bun"],
516
+ ["bun.lock", "bun"],
517
+ ]) {
518
+ if (fs.existsSync(path.join(workspaceRoot, fileName))) {
519
+ return {
520
+ id,
521
+ source: "lockfile",
522
+ raw: fileName,
523
+ };
524
+ }
525
+ }
526
+ return {
527
+ id: "npm",
528
+ source: "default",
529
+ raw: null,
530
+ };
531
+ }
532
+
533
+ function packageManagerCommands(managerId, packageName = WAVE_PACKAGE_NAME) {
534
+ if (managerId === "pnpm") {
535
+ return {
536
+ install: ["pnpm", ["add", "-D", `${packageName}@latest`]],
537
+ execWave: (args) => ["pnpm", ["exec", "wave", ...args]],
538
+ };
539
+ }
540
+ if (managerId === "npm") {
541
+ return {
542
+ install: ["npm", ["install", "--save-dev", `${packageName}@latest`]],
543
+ execWave: (args) => ["npm", ["exec", "--", "wave", ...args]],
544
+ };
545
+ }
546
+ if (managerId === "yarn") {
547
+ return {
548
+ install: ["yarn", ["add", "-D", `${packageName}@latest`]],
549
+ execWave: (args) => ["yarn", ["exec", "wave", ...args]],
550
+ };
551
+ }
552
+ if (managerId === "bun") {
553
+ return {
554
+ install: ["bun", ["add", "-d", `${packageName}@latest`]],
555
+ execWave: (args) => ["bun", ["x", "wave", ...args]],
556
+ };
557
+ }
558
+ throw new Error(`Unsupported package manager: ${managerId}`);
559
+ }
560
+
561
+ function runCommandOrThrow(command, args, options = {}) {
562
+ const spawnImpl = options.spawnImpl || spawnSync;
563
+ const result = spawnImpl(command, args, {
564
+ cwd: options.workspaceRoot || REPO_ROOT,
565
+ stdio: options.stdio || "inherit",
566
+ env: options.env || process.env,
567
+ encoding: "utf8",
568
+ });
569
+ const status = Number.isInteger(result?.status) ? result.status : 1;
570
+ if (status !== 0) {
571
+ throw new Error(`${command} ${args.join(" ")} failed with status ${status}`);
572
+ }
573
+ return result;
574
+ }
575
+
576
+ export async function selfUpdateWorkspace(options = {}) {
577
+ const workspaceRoot = options.workspaceRoot || REPO_ROOT;
578
+ const metadata = options.packageMetadata || packageMetadata();
579
+ const installState = readInstallStateForWorkspace(workspaceRoot);
580
+ const packageManager = detectWorkspacePackageManager(workspaceRoot);
581
+ const commands = packageManagerCommands(packageManager.id, metadata.name || WAVE_PACKAGE_NAME);
582
+ const emit = options.emit || console.log;
583
+ let latestVersion = null;
584
+
585
+ try {
586
+ latestVersion = await fetchLatestPackageVersion(metadata.name || WAVE_PACKAGE_NAME, {
587
+ fetchImpl: options.fetchImpl,
588
+ timeoutMs: options.timeoutMs,
589
+ });
590
+ } catch {
591
+ latestVersion = null;
592
+ }
593
+
594
+ const currentVersion = String(metadata.version || "").trim();
595
+ const recordedVersion = String(installState?.installedVersion || "").trim() || null;
596
+ const needsUpgradeOnly = recordedVersion && compareVersions(currentVersion, recordedVersion) !== 0;
597
+
598
+ emit(`[wave:self-update] package_manager=${packageManager.id}`);
599
+
600
+ if (latestVersion && compareVersions(latestVersion, currentVersion) <= 0) {
601
+ if (!needsUpgradeOnly) {
602
+ emit(`[wave:self-update] ${metadata.name} is already current at ${currentVersion}.`);
603
+ return {
604
+ mode: "already-current",
605
+ packageManager: packageManager.id,
606
+ currentVersion,
607
+ latestVersion,
608
+ };
609
+ }
610
+ emit(
611
+ `[wave:self-update] dependency is already at ${currentVersion}; recording workspace upgrade state.`,
612
+ );
613
+ const [upgradeCommand, upgradeArgs] = commands.execWave(["upgrade"]);
614
+ runCommandOrThrow(upgradeCommand, upgradeArgs, options);
615
+ return {
616
+ mode: "upgrade-only",
617
+ packageManager: packageManager.id,
618
+ currentVersion,
619
+ latestVersion,
620
+ };
621
+ }
622
+
623
+ emit(
624
+ `[wave:self-update] updating ${metadata.name} from ${currentVersion}${latestVersion ? ` to ${latestVersion}` : " to the latest published version"}.`,
625
+ );
626
+ const [installCommand, installArgs] = commands.install;
627
+ runCommandOrThrow(installCommand, installArgs, options);
628
+
629
+ emit("[wave:self-update] release notes since the recorded install:");
630
+ const [changelogCommand, changelogArgs] = commands.execWave(["changelog", "--since-installed"]);
631
+ runCommandOrThrow(changelogCommand, changelogArgs, options);
632
+
633
+ emit("[wave:self-update] recording install-state and upgrade report:");
634
+ const [upgradeCommand, upgradeArgs] = commands.execWave(["upgrade"]);
635
+ runCommandOrThrow(upgradeCommand, upgradeArgs, options);
636
+
637
+ return {
638
+ mode: "updated",
639
+ packageManager: packageManager.id,
640
+ currentVersion,
641
+ latestVersion,
642
+ };
643
+ }
644
+
481
645
  function printJson(payload) {
482
646
  console.log(JSON.stringify(payload, null, 2));
483
647
  }
@@ -486,6 +650,7 @@ function printHelp() {
486
650
  console.log(`Usage:
487
651
  wave init [--adopt-existing] [--json]
488
652
  wave upgrade [--json]
653
+ wave self-update
489
654
  wave changelog [--since-installed] [--json]
490
655
  wave doctor [--json]
491
656
  `);
@@ -562,6 +727,14 @@ export async function runInstallCli(argv) {
562
727
  return;
563
728
  }
564
729
 
730
+ if (subcommand === "self-update") {
731
+ if (options.json) {
732
+ throw new Error("`wave self-update` does not support --json.");
733
+ }
734
+ await selfUpdateWorkspace();
735
+ return;
736
+ }
737
+
565
738
  if (subcommand === "changelog") {
566
739
  const result = readChangelog({ sinceInstalled: options.sinceInstalled });
567
740
  if (options.json) {
@@ -98,6 +98,7 @@ import {
98
98
  commandForExecutor,
99
99
  isExecutorCommandAvailable,
100
100
  } from "./executors.mjs";
101
+ import { maybeAnnouncePackageUpdate } from "./package-update-notice.mjs";
101
102
  import {
102
103
  agentRequiresProofCentricValidation,
103
104
  buildRunStateEvidence,
@@ -2971,6 +2972,9 @@ export async function runLauncherCli(argv) {
2971
2972
  return;
2972
2973
  }
2973
2974
  const { lanePaths, options } = parsed;
2975
+ if (!options.reconcileStatus) {
2976
+ await maybeAnnouncePackageUpdate();
2977
+ }
2974
2978
  let lockHeld = false;
2975
2979
  let globalDashboard = null;
2976
2980
  let globalDashboardTerminalEntry = null;
@@ -0,0 +1,230 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ensureDirectory, readJsonOrNull, REPO_ROOT, writeJsonAtomic } from "./shared.mjs";
4
+ import {
5
+ compareVersions,
6
+ readInstalledPackageMetadata,
7
+ WAVE_PACKAGE_NAME,
8
+ } from "./package-version.mjs";
9
+
10
+ export const PACKAGE_UPDATE_CHECK_SCHEMA_VERSION = 1;
11
+ export const PACKAGE_UPDATE_CHECK_PATH = path.join(REPO_ROOT, ".wave", "package-update-check.json");
12
+ export const PACKAGE_UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
13
+ export const PACKAGE_UPDATE_CHECK_TIMEOUT_MS = 2000;
14
+ export const WAVE_SKIP_UPDATE_CHECK_ENV = "WAVE_SKIP_UPDATE_CHECK";
15
+ export const WAVE_SUPPRESS_UPDATE_NOTICE_ENV = "WAVE_SUPPRESS_UPDATE_NOTICE";
16
+ export const NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org";
17
+
18
+ function isTruthyEnvValue(value) {
19
+ const normalized = String(value ?? "")
20
+ .trim()
21
+ .toLowerCase();
22
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
23
+ }
24
+
25
+ function parsePackageManagerId(value) {
26
+ const normalized = String(value || "")
27
+ .trim()
28
+ .toLowerCase();
29
+ if (normalized.startsWith("pnpm@")) {
30
+ return "pnpm";
31
+ }
32
+ if (normalized.startsWith("npm@")) {
33
+ return "npm";
34
+ }
35
+ if (normalized.startsWith("yarn@")) {
36
+ return "yarn";
37
+ }
38
+ if (normalized.startsWith("bun@")) {
39
+ return "bun";
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function runtimeSelfUpdateCommand(workspaceRoot = REPO_ROOT) {
45
+ const workspacePackage = readJsonOrNull(path.join(workspaceRoot, "package.json"));
46
+ const packageManagerId = parsePackageManagerId(workspacePackage?.packageManager);
47
+ if (packageManagerId === "pnpm") {
48
+ return "pnpm exec wave self-update";
49
+ }
50
+ if (packageManagerId === "npm") {
51
+ return "npm exec -- wave self-update";
52
+ }
53
+ if (packageManagerId === "yarn") {
54
+ return "yarn exec wave self-update";
55
+ }
56
+ if (packageManagerId === "bun") {
57
+ return "bun x wave self-update";
58
+ }
59
+ if (fs.existsSync(path.join(workspaceRoot, "pnpm-lock.yaml"))) {
60
+ return "pnpm exec wave self-update";
61
+ }
62
+ if (fs.existsSync(path.join(workspaceRoot, "yarn.lock"))) {
63
+ return "yarn exec wave self-update";
64
+ }
65
+ if (fs.existsSync(path.join(workspaceRoot, "bun.lock")) || fs.existsSync(path.join(workspaceRoot, "bun.lockb"))) {
66
+ return "bun x wave self-update";
67
+ }
68
+ return "npm exec -- wave self-update";
69
+ }
70
+
71
+ function buildPackageLatestUrl(packageName) {
72
+ return `${NPM_REGISTRY_LATEST_URL}/${encodeURIComponent(String(packageName || WAVE_PACKAGE_NAME)).replace("%40", "@")}/latest`;
73
+ }
74
+
75
+ function readUpdateCheckCache(cachePath = PACKAGE_UPDATE_CHECK_PATH) {
76
+ const payload = readJsonOrNull(cachePath);
77
+ return payload && typeof payload === "object" ? payload : null;
78
+ }
79
+
80
+ function writeUpdateCheckCache(cachePath, payload) {
81
+ ensureDirectory(path.dirname(cachePath));
82
+ writeJsonAtomic(cachePath, payload);
83
+ }
84
+
85
+ function buildNoticeLines(packageName, currentVersion, latestVersion, workspaceRoot = REPO_ROOT) {
86
+ return [
87
+ `[wave:update] newer ${packageName} available: installed ${currentVersion}, latest ${latestVersion}`,
88
+ `[wave:update] update now with: ${runtimeSelfUpdateCommand(workspaceRoot)}`,
89
+ ];
90
+ }
91
+
92
+ function emitNotice(packageName, currentVersion, latestVersion, emit = console.error, workspaceRoot = REPO_ROOT) {
93
+ for (const line of buildNoticeLines(packageName, currentVersion, latestVersion, workspaceRoot)) {
94
+ emit(line);
95
+ }
96
+ }
97
+
98
+ export async function fetchLatestPackageVersion(
99
+ packageName = WAVE_PACKAGE_NAME,
100
+ {
101
+ fetchImpl = globalThis.fetch,
102
+ timeoutMs = PACKAGE_UPDATE_CHECK_TIMEOUT_MS,
103
+ } = {},
104
+ ) {
105
+ if (typeof fetchImpl !== "function") {
106
+ throw new Error("Package update check is unavailable in this Node runtime.");
107
+ }
108
+ const abortController = new AbortController();
109
+ const timer = setTimeout(() => abortController.abort(), timeoutMs);
110
+ try {
111
+ const response = await fetchImpl(buildPackageLatestUrl(packageName), {
112
+ signal: abortController.signal,
113
+ headers: {
114
+ Accept: "application/json",
115
+ },
116
+ });
117
+ if (!response?.ok) {
118
+ throw new Error(`Upstream package check failed with status ${response?.status || "unknown"}.`);
119
+ }
120
+ const payload = await response.json();
121
+ const latestVersion = String(payload?.version || "").trim();
122
+ if (!latestVersion) {
123
+ throw new Error("Upstream package check returned no version.");
124
+ }
125
+ return latestVersion;
126
+ } finally {
127
+ clearTimeout(timer);
128
+ }
129
+ }
130
+
131
+ export async function maybeAnnouncePackageUpdate(options = {}) {
132
+ const env = options.env || process.env;
133
+ if (
134
+ isTruthyEnvValue(env[WAVE_SKIP_UPDATE_CHECK_ENV]) ||
135
+ isTruthyEnvValue(env[WAVE_SUPPRESS_UPDATE_NOTICE_ENV])
136
+ ) {
137
+ return {
138
+ skipped: true,
139
+ reason: "disabled",
140
+ updateAvailable: false,
141
+ latestVersion: null,
142
+ currentVersion: null,
143
+ };
144
+ }
145
+
146
+ const metadata = options.packageMetadata || readInstalledPackageMetadata();
147
+ const packageName = String(metadata.name || WAVE_PACKAGE_NAME);
148
+ const currentVersion = String(metadata.version || "").trim();
149
+ const cachePath = options.cachePath || PACKAGE_UPDATE_CHECK_PATH;
150
+ const workspaceRoot = options.workspaceRoot || REPO_ROOT;
151
+ const cacheTtlMs = options.cacheTtlMs ?? PACKAGE_UPDATE_CHECK_TTL_MS;
152
+ const nowMs = options.nowMs ?? Date.now();
153
+ const emit = options.emit || console.error;
154
+ const cache = readUpdateCheckCache(cachePath);
155
+ const cachedCheckedAtMs = Date.parse(String(cache?.checkedAt || ""));
156
+ const cacheMatchesCurrentVersion = cache?.currentVersion === currentVersion;
157
+ const cachedUpdateAvailable =
158
+ cacheMatchesCurrentVersion &&
159
+ typeof cache?.latestVersion === "string" &&
160
+ compareVersions(cache.latestVersion, currentVersion) > 0;
161
+ const cacheFresh =
162
+ cacheMatchesCurrentVersion &&
163
+ Number.isFinite(cachedCheckedAtMs) &&
164
+ nowMs - cachedCheckedAtMs <= cacheTtlMs;
165
+ let emitted = false;
166
+
167
+ if (cachedUpdateAvailable) {
168
+ emitNotice(packageName, currentVersion, cache.latestVersion, emit, workspaceRoot);
169
+ emitted = true;
170
+ }
171
+
172
+ if (cacheFresh) {
173
+ return {
174
+ skipped: false,
175
+ source: "cache",
176
+ updateAvailable: cachedUpdateAvailable,
177
+ latestVersion: cache?.latestVersion || currentVersion,
178
+ currentVersion,
179
+ };
180
+ }
181
+
182
+ try {
183
+ const latestVersion = await fetchLatestPackageVersion(packageName, options);
184
+ const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
185
+ writeUpdateCheckCache(cachePath, {
186
+ schemaVersion: PACKAGE_UPDATE_CHECK_SCHEMA_VERSION,
187
+ packageName,
188
+ checkedAt: new Date(nowMs).toISOString(),
189
+ currentVersion,
190
+ latestVersion,
191
+ updateAvailable,
192
+ lastErrorAt: null,
193
+ lastErrorMessage: null,
194
+ });
195
+ if (updateAvailable && !emitted) {
196
+ emitNotice(packageName, currentVersion, latestVersion, emit, workspaceRoot);
197
+ }
198
+ return {
199
+ skipped: false,
200
+ source: "network",
201
+ updateAvailable,
202
+ latestVersion,
203
+ currentVersion,
204
+ };
205
+ } catch (error) {
206
+ writeUpdateCheckCache(cachePath, {
207
+ schemaVersion: PACKAGE_UPDATE_CHECK_SCHEMA_VERSION,
208
+ packageName,
209
+ checkedAt: new Date(nowMs).toISOString(),
210
+ currentVersion,
211
+ latestVersion:
212
+ cacheMatchesCurrentVersion && typeof cache?.latestVersion === "string"
213
+ ? cache.latestVersion
214
+ : currentVersion,
215
+ updateAvailable: cachedUpdateAvailable,
216
+ lastErrorAt: new Date(nowMs).toISOString(),
217
+ lastErrorMessage: error instanceof Error ? error.message : String(error),
218
+ });
219
+ return {
220
+ skipped: false,
221
+ source: "error",
222
+ updateAvailable: cachedUpdateAvailable,
223
+ latestVersion:
224
+ cacheMatchesCurrentVersion && typeof cache?.latestVersion === "string"
225
+ ? cache.latestVersion
226
+ : currentVersion,
227
+ currentVersion,
228
+ };
229
+ }
230
+ }
@@ -0,0 +1,32 @@
1
+ import path from "node:path";
2
+ import { PACKAGE_ROOT, readJsonOrNull } from "./shared.mjs";
3
+
4
+ export const WAVE_PACKAGE_NAME = "@chllming/wave-orchestration";
5
+ export const PACKAGE_METADATA_PATH = path.join(PACKAGE_ROOT, "package.json");
6
+
7
+ export function readInstalledPackageMetadata(metadataPath = PACKAGE_METADATA_PATH) {
8
+ const payload = readJsonOrNull(metadataPath);
9
+ if (!payload?.name || !payload?.version) {
10
+ throw new Error(`Invalid package metadata: ${metadataPath}`);
11
+ }
12
+ return payload;
13
+ }
14
+
15
+ function normalizeVersionParts(version) {
16
+ return String(version || "")
17
+ .split(".")
18
+ .map((part) => Number.parseInt(part.replace(/[^0-9].*$/, ""), 10) || 0);
19
+ }
20
+
21
+ export function compareVersions(a, b) {
22
+ const left = normalizeVersionParts(a);
23
+ const right = normalizeVersionParts(b);
24
+ const length = Math.max(left.length, right.length);
25
+ for (let index = 0; index < length; index += 1) {
26
+ const diff = (left[index] || 0) - (right[index] || 0);
27
+ if (diff !== 0) {
28
+ return diff;
29
+ }
30
+ }
31
+ return 0;
32
+ }
package/scripts/wave.mjs CHANGED
@@ -12,6 +12,7 @@ function printHelp() {
12
12
  console.log(`Usage:
13
13
  wave init [options]
14
14
  wave upgrade [options]
15
+ wave self-update
15
16
  wave changelog [options]
16
17
  wave doctor [options]
17
18
  wave project setup [options]
@@ -25,6 +26,7 @@ function printHelp() {
25
26
  wave local [local executor options]
26
27
  wave coord [coordination options]
27
28
  wave dep [dependency options]
29
+ wave benchmark [benchmark options]
28
30
 
29
31
  Global options:
30
32
  --repo-root <path> Run the command against a target workspace root
@@ -36,7 +38,7 @@ if (!subcommand || subcommand === "--help" || subcommand === "-h" || subcommand
36
38
  process.exit(0);
37
39
  }
38
40
 
39
- if (["init", "upgrade", "changelog", "doctor"].includes(subcommand)) {
41
+ if (["init", "upgrade", "self-update", "changelog", "doctor"].includes(subcommand)) {
40
42
  try {
41
43
  const { runInstallCli } = await import("./wave-orchestrator/install.mjs");
42
44
  await runInstallCli([subcommand, ...rest]);
@@ -71,7 +73,7 @@ if (["init", "upgrade", "changelog", "doctor"].includes(subcommand)) {
71
73
  } else if (subcommand === "autonomous") {
72
74
  const { runAutonomousCli } = await import("./wave-orchestrator/autonomous.mjs");
73
75
  try {
74
- runAutonomousCli(rest);
76
+ await runAutonomousCli(rest);
75
77
  } catch (error) {
76
78
  console.error(`[wave] ${error instanceof Error ? error.message : String(error)}`);
77
79
  process.exit(1);
@@ -116,6 +118,14 @@ if (["init", "upgrade", "changelog", "doctor"].includes(subcommand)) {
116
118
  console.error(`[wave] ${error instanceof Error ? error.message : String(error)}`);
117
119
  process.exit(1);
118
120
  }
121
+ } else if (subcommand === "benchmark") {
122
+ try {
123
+ const { runBenchmarkCli } = await import("./wave-orchestrator/benchmark.mjs");
124
+ await runBenchmarkCli(rest);
125
+ } catch (error) {
126
+ console.error(`[wave] ${error instanceof Error ? error.message : String(error)}`);
127
+ process.exit(1);
128
+ }
119
129
  } else {
120
130
  console.error(`[wave] Unknown subcommand: ${subcommand}`);
121
131
  printHelp();