@aiwg/cli 2026.8.16 → 2026.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/bin/aiwg.mjs +24 -2
  2. package/dist/src/a2a/agent-card.js +4 -1
  3. package/dist/src/a2a/client.js +148 -68
  4. package/dist/src/a2a/codecs.js +480 -0
  5. package/dist/src/a2a/events.js +226 -0
  6. package/dist/src/a2a/hitl-driver.js +8 -6
  7. package/dist/src/a2a/hitl.js +2 -1
  8. package/dist/src/a2a/http.js +85 -5
  9. package/dist/src/a2a/protocol.js +136 -0
  10. package/dist/src/a2a/types.js +4 -14
  11. package/dist/src/a2a/webhook.js +101 -4
  12. package/dist/src/artifacts/browser-export.js +1 -0
  13. package/dist/src/artifacts/cli.js +1 -1
  14. package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -0
  15. package/dist/src/artifacts/types.js +73 -1
  16. package/dist/src/audit/operator-decision.js +15 -1
  17. package/dist/src/channel/manager.mjs +89 -17
  18. package/dist/src/cli/agent-spawn.js +4 -2
  19. package/dist/src/cli/handlers/cockpit.js +41 -0
  20. package/dist/src/cli/handlers/help.js +1 -1
  21. package/dist/src/cli/handlers/index.js +3 -1
  22. package/dist/src/cli/handlers/installation.js +79 -0
  23. package/dist/src/cli/handlers/ralph.js +2 -1
  24. package/dist/src/cli/handlers/refresh.js +4 -2
  25. package/dist/src/cli/handlers/runtime-info.js +9 -1
  26. package/dist/src/cli/handlers/sdlc-accelerate.js +2 -1
  27. package/dist/src/cli/handlers/serve.js +107 -4
  28. package/dist/src/cli/handlers/session.js +12 -26
  29. package/dist/src/cli/handlers/use.js +117 -12
  30. package/dist/src/cli/handlers/utilities.js +4 -0
  31. package/dist/src/cli/handlers/version.js +4 -0
  32. package/dist/src/cli/handlers/workspace.js +24 -2
  33. package/dist/src/cli/services/deployment-verification.js +9 -2
  34. package/dist/src/cockpit/doctor.js +257 -0
  35. package/dist/src/config/aiwg-config.js +36 -3
  36. package/dist/src/config/user-config-dir.mjs +29 -0
  37. package/dist/src/config/user-config.js +4 -22
  38. package/dist/src/extensions/commands/definitions.js +20 -1
  39. package/dist/src/features/catalog.js +2 -1
  40. package/dist/src/flow/graph-metadata.js +56 -0
  41. package/dist/src/installation/manager.mjs +243 -0
  42. package/dist/src/providers/transformation-receipt-integration.js +130 -3
  43. package/dist/src/security/artifact-verifier.js +7 -1
  44. package/dist/src/serve/a2a-terminal-observer.js +28 -5
  45. package/dist/src/serve/dispatch-router.js +32 -4
  46. package/dist/src/serve/executor-registry.js +29 -0
  47. package/dist/src/serve/mission-conductor.js +15 -1
  48. package/dist/src/serve/pty-bridge.js +6 -11
  49. package/dist/src/serve/stack-adapters.js +2 -1
  50. package/dist/src/serve/telemetry.js +5 -1
  51. package/dist/src/skills/run.js +15 -6
  52. package/dist/src/update/checker.mjs +16 -15
  53. package/dist/src/update/notifier.mjs +8 -3
  54. package/dist/src/update/service.mjs +49 -5
  55. package/package.json +2 -1
@@ -46,6 +46,9 @@ export class MissionConductor {
46
46
  * crash-resilient resume path; their prior results are carried forward.
47
47
  */
48
48
  async conduct(plan, pool, resumeFrom) {
49
+ if (plan.cycles.some((cycle) => cycle.graph) && !plan.graph) {
50
+ throw new Error('Graph-projected worker cycles require MissionPlan.graph identity.');
51
+ }
49
52
  const carried = new Map();
50
53
  if (resumeFrom) {
51
54
  for (const c of resumeFrom.cycles) {
@@ -62,10 +65,17 @@ export class MissionConductor {
62
65
  totalCost: 0,
63
66
  checkpoint: { completed: [], pending: [], failed: [] },
64
67
  runtimesUsed: [],
68
+ ...(plan.graph ? { graph: structuredClone(plan.graph) } : {}),
65
69
  };
66
70
  ledger.activityLog.push(`mission ${plan.missionId} start — goal: ${plan.goal} — ${plan.cycles.length} cycle(s)` +
67
71
  (resumeFrom ? ` (resume: ${carried.size} carried)` : ''));
68
72
  for (const cycle of plan.cycles) {
73
+ const graph = plan.graph && cycle.graph ? {
74
+ ...structuredClone(plan.graph),
75
+ ...structuredClone(cycle.graph),
76
+ schemaVersion: 'graph.flow.aiwg.io/v1',
77
+ nodeRunId: cycle.graph.nodeRunId ?? `${plan.graph.runId}:${cycle.graph.nodeId}`,
78
+ } : undefined;
69
79
  // Resume: carry a previously-completed cycle forward, identical bookkeeping.
70
80
  const prior = carried.get(cycle.id);
71
81
  if (prior) {
@@ -86,6 +96,7 @@ export class MissionConductor {
86
96
  routed: false,
87
97
  reason: `no stack adapter registered for runtime '${cycle.runtime}'`,
88
98
  cost: 0,
99
+ ...(graph ? { graph } : {}),
89
100
  };
90
101
  ledger.cycles.push(result);
91
102
  ledger.checkpoint.failed.push(cycle.id);
@@ -104,6 +115,7 @@ export class MissionConductor {
104
115
  routed: false,
105
116
  reason: `no connected executor advertises ${filter.capabilities.join(', ')}`,
106
117
  cost: 0,
118
+ ...(graph ? { graph } : {}),
107
119
  };
108
120
  ledger.cycles.push(result);
109
121
  ledger.checkpoint.failed.push(cycle.id);
@@ -111,7 +123,7 @@ export class MissionConductor {
111
123
  continue;
112
124
  }
113
125
  const executor = routing.selected.executor;
114
- const invocation = adapter.invoke(cycle.prompt);
126
+ const invocation = adapter.invoke(cycle.prompt, graph);
115
127
  ledger.activityLog.push(`cycle ${cycle.id} → executor ${executor.name} (${executor.executorId}) on ${cycle.runtime}: ${invocation.describe}`);
116
128
  let output;
117
129
  let cost = 0;
@@ -129,6 +141,7 @@ export class MissionConductor {
129
141
  routed: true,
130
142
  reason: `worker error: ${err instanceof Error ? err.message : String(err)}`,
131
143
  cost: 0,
144
+ ...(graph ? { graph } : {}),
132
145
  };
133
146
  ledger.cycles.push(result);
134
147
  ledger.checkpoint.failed.push(cycle.id);
@@ -144,6 +157,7 @@ export class MissionConductor {
144
157
  reason: routing.selected.matchReason,
145
158
  output,
146
159
  cost,
160
+ ...(graph ? { graph } : {}),
147
161
  };
148
162
  ledger.cycles.push(result);
149
163
  ledger.checkpoint.completed.push(cycle.id);
@@ -695,19 +695,14 @@ export async function handlePtyConnection(sessionId, ws, command = 'aiwg', cmdAr
695
695
  }
696
696
  }
697
697
  else if (!session.exited) {
698
- // Reconnect to existing session replay buffer
698
+ // Reconnect to an existing session by replaying the complete retained
699
+ // buffer. Do not trim at the last full-screen erase: terminal erase
700
+ // sequences repaint the current viewport but xterm still needs the bytes
701
+ // that preceded them to reconstruct scrollback when a user switches away
702
+ // from a session and later returns (#2146).
699
703
  registry.addClient(sessionId, clientId, ws);
700
704
  if (session.outputBuffer) {
701
- // Trim replay to start from the last full-screen erase so that tmux's
702
- // screen-init sequences (cursor moves, status-bar paint) from before the
703
- // erase don't render as literal garbage in a fresh xterm.js context.
704
- // Everything before \x1b[2J would be cleared by the erase anyway;
705
- // everything after is the session content tmux redrew (MOTD, history, etc).
706
- // If no erase is found, replay the whole buffer unchanged.
707
- const ERASE = '\x1b[2J';
708
- const lastErase = session.outputBuffer.lastIndexOf(ERASE);
709
- const replay = lastErase !== -1 ? session.outputBuffer.slice(lastErase) : session.outputBuffer;
710
- ws.send(JSON.stringify({ type: 'data', payload: replay }));
705
+ ws.send(JSON.stringify({ type: 'data', payload: session.outputBuffer }));
711
706
  }
712
707
  }
713
708
  else {
@@ -23,7 +23,7 @@ function makeAdapter(runtime, primitive) {
23
23
  runtime,
24
24
  runtimeCapability,
25
25
  primitive,
26
- invoke(prompt) {
26
+ invoke(prompt, graph) {
27
27
  // prompt is carried by the dispatch payload; the descriptor records the
28
28
  // mechanism so the conductor's ledger is identical-shape across stacks.
29
29
  const trimmed = prompt.length > 60 ? `${prompt.slice(0, 57)}...` : prompt;
@@ -31,6 +31,7 @@ function makeAdapter(runtime, primitive) {
31
31
  runtimeCapability,
32
32
  primitive,
33
33
  describe: `dispatch worker to ${runtime} executor via ${primitive} (${trimmed})`,
34
+ ...(graph ? { graph: structuredClone(graph) } : {}),
34
35
  };
35
36
  },
36
37
  };
@@ -130,7 +130,10 @@ export const telemetryStore = new TelemetryStore();
130
130
  // Event Factory
131
131
  // ============================================================
132
132
  let eventCounter = 0;
133
- export function createEvent(type, sessionId, payload, missionId) {
133
+ export function createEvent(type, sessionId, payload, missionId, graph) {
134
+ if (type.startsWith('graph.') && !graph) {
135
+ throw new Error(`Telemetry event '${type}' requires graph execution metadata.`);
136
+ }
134
137
  return {
135
138
  id: `evt-${Date.now()}-${++eventCounter}`,
136
139
  sessionId,
@@ -138,6 +141,7 @@ export function createEvent(type, sessionId, payload, missionId) {
138
141
  timestamp: new Date().toISOString(),
139
142
  type,
140
143
  payload,
144
+ ...(graph ? { graph: structuredClone(graph) } : {}),
141
145
  };
142
146
  }
143
147
  //# sourceMappingURL=telemetry.js.map
@@ -25,6 +25,8 @@ import { spawn } from 'node:child_process';
25
25
  import { promises as fs } from 'node:fs';
26
26
  import * as path from 'node:path';
27
27
  import { resolveRuntime, supportedRuntimes } from './runtime.js';
28
+ import { recordTypeForEntry, stableRecordId } from '../artifacts/browser-export.js';
29
+ import { loadFortemiCoreMetadataEntries } from '../artifacts/fortemi-core-query-adapter.js';
28
30
  /**
29
31
  * Resolve the AIWG installation root. Prefers `$AIWG_ROOT` env, falls
30
32
  * back to the channel manager's framework-root resolver.
@@ -53,22 +55,29 @@ async function findSkillEntry(cwd, name) {
53
55
  const reader = await import('../artifacts/index-reader.js');
54
56
  const entries = [];
55
57
  for (const g of ['framework', 'project', 'codebase']) {
56
- const idx = reader.loadGraphIndexFile(cwd, 'metadata.json', g);
57
- if (idx)
58
- entries.push(...Object.values(idx.entries));
58
+ const canonical = loadFortemiCoreMetadataEntries(cwd, g);
59
+ if (canonical.entries.length > 0) {
60
+ entries.push(...canonical.entries);
61
+ }
62
+ else {
63
+ const idx = reader.loadGraphIndexFile(cwd, 'metadata.json', g);
64
+ if (idx)
65
+ entries.push(...Object.values(idx.entries));
66
+ }
59
67
  }
60
68
  if (entries.length === 0) {
61
69
  const legacy = reader.loadMetadataIndex(cwd);
62
70
  if (legacy)
63
71
  entries.push(...Object.values(legacy.entries));
64
72
  }
65
- const skills = entries.filter(e => e.type === 'skill');
73
+ const skills = entries.filter(e => e.type === 'skill' || e.type === 'aiwg.skill');
66
74
  const needle = name.trim();
67
75
  // Basename match — skills are conventionally `<dir>/SKILL.md`
68
76
  const matches = skills.filter(e => {
69
77
  const dir = path.basename(path.dirname(e.path));
70
78
  const stem = path.basename(e.path).replace(/\.[^.]+$/, '');
71
- return dir === needle || stem === needle || e.path === needle;
79
+ const id = stableRecordId(recordTypeForEntry({ ...e, type: 'skill' }, 'v2'), e.path);
80
+ return id === needle || e.name === needle || dir === needle || stem === needle || e.path === needle;
72
81
  });
73
82
  if (matches.length === 0)
74
83
  return null;
@@ -248,7 +257,7 @@ export async function main(args, env) {
248
257
  });
249
258
  }
250
259
  function printUsage() {
251
- console.log('Usage: aiwg run skill <name> [--cwd <path>] [-- <args...>]');
260
+ console.log('Usage: aiwg run skill <stable-id-or-name> [--cwd <path>] [-- <args...>]');
252
261
  console.log('');
253
262
  console.log('Examples:');
254
263
  console.log(' aiwg run skill voice-apply -- --voice technical-authority --input draft.md');
@@ -15,6 +15,7 @@ import https from 'https';
15
15
  import { execSync } from 'child_process';
16
16
  import { createInterface } from 'readline';
17
17
  import { loadConfig, saveConfig, getChannel, getPackageRoot } from '../channel/manager.mjs';
18
+ import { updateInstallation } from './service.mjs';
18
19
 
19
20
  const NPM_REGISTRY = 'https://registry.npmjs.org/aiwg';
20
21
 
@@ -212,13 +213,13 @@ async function checkStableUpdates(config) {
212
213
  console.log('');
213
214
  console.log('Updating aiwg...');
214
215
  try {
215
- execSync('npm install -g aiwg@latest', { stdio: 'inherit' });
216
+ await updateInstallation({ config, channel: 'stable' });
216
217
  console.log('Update complete! Please restart your terminal.');
217
218
  } catch (error) {
218
- console.error('Update failed. Run manually: npm install -g aiwg@latest');
219
+ console.error('Update failed. Run manually: aiwg update');
219
220
  }
220
221
  } else {
221
- console.log('Update skipped. Run `npm install -g aiwg@latest` when ready.');
222
+ console.log('Update skipped. Run `aiwg update` when ready.');
222
223
  }
223
224
  console.log('');
224
225
  }
@@ -251,13 +252,13 @@ async function checkNextUpdates(config) {
251
252
  console.log('');
252
253
  console.log('Updating aiwg@next...');
253
254
  try {
254
- execSync('npm install -g aiwg@next', { stdio: 'inherit' });
255
+ await updateInstallation({ config, channel: 'next' });
255
256
  console.log('Update complete! Please restart your terminal.');
256
257
  } catch {
257
- console.error('Update failed. Run manually: npm install -g aiwg@next');
258
+ console.error('Update failed. Run manually: aiwg update');
258
259
  }
259
260
  } else {
260
- console.log('Update skipped. Run `npm install -g aiwg@next` when ready.');
261
+ console.log('Update skipped. Run `aiwg update` when ready.');
261
262
  }
262
263
  console.log('');
263
264
  }
@@ -290,13 +291,13 @@ async function checkNightlyUpdates(config) {
290
291
  console.log('');
291
292
  console.log('Updating aiwg@nightly...');
292
293
  try {
293
- execSync('npm install -g aiwg@nightly', { stdio: 'inherit' });
294
+ await updateInstallation({ config, channel: 'nightly' });
294
295
  console.log('Update complete! Please restart your terminal.');
295
296
  } catch {
296
- console.error('Update failed. Run manually: npm install -g aiwg@nightly');
297
+ console.error('Update failed. Run manually: aiwg update');
297
298
  }
298
299
  } else {
299
- console.log('Update skipped. Run `npm install -g aiwg@nightly` when ready.');
300
+ console.log('Update skipped. Run `aiwg update` when ready.');
300
301
  }
301
302
  console.log('');
302
303
  }
@@ -348,13 +349,13 @@ export async function forceUpdateCheck() {
348
349
  console.log('Checking for updates on next channel...');
349
350
  const latestVersion = await fetchNpmDistTag('next');
350
351
  if (!latestVersion) {
351
- console.log('Could not check npm registry. Try: npm install -g aiwg@next');
352
+ console.log('Could not check npm registry. Try: aiwg update');
352
353
  return;
353
354
  }
354
355
  if (currentVersion !== latestVersion) {
355
356
  console.log(`Update available: ${currentVersion} → ${latestVersion}`);
356
357
  console.log('');
357
- console.log('Run: npm install -g aiwg@next');
358
+ console.log('Run: aiwg update');
358
359
  } else {
359
360
  console.log(`You are on the latest next release: ${currentVersion}`);
360
361
  }
@@ -362,13 +363,13 @@ export async function forceUpdateCheck() {
362
363
  console.log('Checking for updates on nightly channel...');
363
364
  const latestVersion = await fetchNpmDistTag('nightly');
364
365
  if (!latestVersion) {
365
- console.log('Could not check npm registry. Try: npm install -g aiwg@nightly');
366
+ console.log('Could not check npm registry. Try: aiwg update');
366
367
  return;
367
368
  }
368
369
  if (currentVersion !== latestVersion) {
369
370
  console.log(`Update available: ${currentVersion} → ${latestVersion}`);
370
371
  console.log('');
371
- console.log('Run: npm install -g aiwg@nightly');
372
+ console.log('Run: aiwg update');
372
373
  } else {
373
374
  console.log(`You are on the latest nightly snapshot: ${currentVersion}`);
374
375
  }
@@ -381,14 +382,14 @@ export async function forceUpdateCheck() {
381
382
  const latestVersion = await fetchLatestNpmVersion();
382
383
 
383
384
  if (!latestVersion) {
384
- console.log('Could not check npm registry. Try: npm install -g aiwg@latest');
385
+ console.log('Could not check npm registry. Try: aiwg update');
385
386
  return;
386
387
  }
387
388
 
388
389
  if (isNewerVersion(currentVersion, latestVersion)) {
389
390
  console.log(`Update available: ${currentVersion} → ${latestVersion}`);
390
391
  console.log('');
391
- console.log('Run: npm install -g aiwg@latest');
392
+ console.log('Run: aiwg update');
392
393
  } else {
393
394
  console.log(`You are on the latest version: ${currentVersion}`);
394
395
  }
@@ -27,12 +27,13 @@
27
27
 
28
28
  import https from 'https';
29
29
  import path from 'path';
30
- import os from 'os';
31
30
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
32
31
  import { spawn } from 'child_process';
33
32
  import { fileURLToPath } from 'url';
33
+ import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
34
+ import { loadInstallationIdentity } from '../installation/manager.mjs';
34
35
 
35
- const CACHE_DIR = path.join(os.homedir(), '.aiwg');
36
+ const CACHE_DIR = resolveUserConfigDir();
36
37
  const CACHE_FILE = path.join(CACHE_DIR, 'update-notifier.json');
37
38
 
38
39
  /**
@@ -191,11 +192,13 @@ async function runCheck(packageRoot) {
191
192
  */
192
193
  export function scheduleBackgroundCheck(packageRoot) {
193
194
  if (isDisabled()) return;
195
+ const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
196
+ if (installation?.checkOnStartup === false) return;
194
197
 
195
198
  const cache = readCache();
196
199
  if (cacheMatchesPackage(cache, packageRoot) && cache?.lastCheckAt) {
197
200
  const age = Date.now() - new Date(cache.lastCheckAt).getTime();
198
- if (age < CHECK_INTERVAL_MS) return; // recent enough — skip
201
+ if (age < (installation?.updateCheckInterval ?? CHECK_INTERVAL_MS)) return; // recent enough — skip
199
202
  }
200
203
 
201
204
  try {
@@ -225,6 +228,8 @@ export function scheduleBackgroundCheck(packageRoot) {
225
228
  */
226
229
  export function maybePrintNotice(packageRoot) {
227
230
  if (isDisabled()) return;
231
+ const installation = loadInstallationIdentity({ actualRoot: packageRoot, createIfMissing: false });
232
+ if (installation?.checkOnStartup === false) return;
228
233
  const cache = readCache();
229
234
  if (!cacheMatchesPackage(cache, packageRoot)) return;
230
235
  if (!cache?.hasUpdate || !cache.current || !cache.latest) return;
@@ -10,6 +10,12 @@ import { execFileSync } from 'node:child_process';
10
10
  import { existsSync, readFileSync } from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { getPackageRoot, loadConfig } from '../channel/manager.mjs';
13
+ import {
14
+ assertCanonicalInstallation,
15
+ createInstallationIdentity,
16
+ loadInstallationIdentity,
17
+ saveInstallationIdentity,
18
+ } from '../installation/manager.mjs';
13
19
 
14
20
  const VALID_MODES = new Set(['npm', 'web', 'source']);
15
21
 
@@ -24,14 +30,42 @@ function readPackageName(packageRoot) {
24
30
  export async function detectInstallMode(options = {}) {
25
31
  const packageRoot = options.packageRoot ?? getPackageRoot();
26
32
  const override = options.env?.AIWG_INSTALL_MODE ?? process.env.AIWG_INSTALL_MODE;
33
+ const config = options.config ?? await loadConfig(options);
34
+ let identity = options.identity ?? config.installation ?? null;
35
+ let identityPersistent = Boolean(config.installation) || options.identityPersistent === true;
36
+ if (!identity && options.config) {
37
+ const method = override ?? (readPackageName(packageRoot) === '@aiwg/cli'
38
+ ? 'web'
39
+ : (config.devMode || config.channel === 'edge' || existsSync(path.join(packageRoot, '.git'))) ? 'source' : 'npm');
40
+ identity = createInstallationIdentity({
41
+ ...options,
42
+ actualRoot: packageRoot,
43
+ method,
44
+ channel: config.channel,
45
+ runMode: config.devMode ? 'development' : undefined,
46
+ });
47
+ } else if (!identity) {
48
+ identity = loadInstallationIdentity({ ...options, actualRoot: packageRoot });
49
+ identityPersistent = Boolean(identity);
50
+ }
51
+
52
+ if (override && !VALID_MODES.has(override)) {
53
+ throw new Error(`AIWG_INSTALL_MODE must be one of: ${[...VALID_MODES].join(', ')}`);
54
+ }
55
+ if (override && identity && override !== identity.method) {
56
+ throw new Error(`AIWG_INSTALL_MODE=${override} conflicts with the canonical installation method ${identity.method}. Use \`aiwg installation switch\` instead.`);
57
+ }
58
+ if (identity) {
59
+ const status = assertCanonicalInstallation({ ...options, actualRoot: packageRoot, identity });
60
+ return { mode: identity.method, packageRoot, packageName: readPackageName(packageRoot), identity, identityPersistent, installation: status };
61
+ }
62
+
27
63
  if (override) {
28
64
  if (!VALID_MODES.has(override)) {
29
65
  throw new Error(`AIWG_INSTALL_MODE must be one of: ${[...VALID_MODES].join(', ')}`);
30
66
  }
31
67
  return { mode: override, packageRoot, packageName: readPackageName(packageRoot) };
32
68
  }
33
-
34
- const config = options.config ?? await loadConfig();
35
69
  const packageName = readPackageName(packageRoot);
36
70
  if (packageName === '@aiwg/cli') return { mode: 'web', packageRoot, packageName };
37
71
  if (config.devMode || config.channel === 'edge' || existsSync(path.join(packageRoot, '.git'))) {
@@ -56,7 +90,7 @@ function npmTag(channel) {
56
90
  export async function updateInstallation(options = {}) {
57
91
  const config = options.config ?? await loadConfig();
58
92
  const detected = await detectInstallMode({ ...options, config });
59
- const channel = options.channel ?? config.channel ?? 'stable';
93
+ const channel = options.channel ?? detected.identity?.channel ?? config.channel ?? 'stable';
60
94
  const dryRun = options.dryRun === true;
61
95
  const offline = options.offline === true;
62
96
 
@@ -84,6 +118,9 @@ export async function updateInstallation(options = {}) {
84
118
  return resolveWebRelease({ selector });
85
119
  });
86
120
  const release = await refreshWebResources(channel);
121
+ if (detected.identity && detected.identityPersistent && options.persistIdentity !== false) {
122
+ saveInstallationIdentity({ ...detected.identity, channel }, options);
123
+ }
87
124
  return {
88
125
  ...detected,
89
126
  channel,
@@ -106,16 +143,23 @@ export async function updateInstallation(options = {}) {
106
143
 
107
144
  const tag = npmTag(channel);
108
145
  const command = ['install', '--global', `aiwg@${tag}`];
146
+ const managerExecutable = detected.identity?.managerExecutable;
147
+ if (!managerExecutable) {
148
+ throw new Error('Canonical npm installation has no package-manager executable. Run `aiwg installation adopt --manager <absolute-path-to-npm>`.');
149
+ }
109
150
  if (!dryRun) {
110
151
  const execute = options.execute ?? ((file, args) => execFileSync(file, args, { stdio: 'inherit' }));
111
- execute('npm', command);
152
+ execute(managerExecutable, command);
153
+ if (detected.identity && detected.identityPersistent && options.persistIdentity !== false) {
154
+ saveInstallationIdentity({ ...detected.identity, channel }, options);
155
+ }
112
156
  }
113
157
  return {
114
158
  ...detected,
115
159
  channel,
116
160
  status: dryRun ? 'dry-run' : 'updated',
117
161
  changed: !dryRun,
118
- command: `npm ${command.join(' ')}`,
162
+ command: `${managerExecutable} ${command.join(' ')}`,
119
163
  message: dryRun
120
164
  ? `Would update the full AIWG npm distribution on the '${tag}' channel.`
121
165
  : `Updated the full AIWG npm distribution on the '${tag}' channel.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cli",
3
- "version": "2026.8.16",
3
+ "version": "2026.8.18",
4
4
  "description": "Lightweight AIWG CLI for signed, versioned web-backed resources.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,6 +67,7 @@
67
67
  "dependencies": {
68
68
  "@fortemi/core": "2026.7.15",
69
69
  "@modelcontextprotocol/sdk": "^1.30.0",
70
+ "ajv": "^8.20.0",
70
71
  "chalk": "^4.1.2",
71
72
  "chokidar": "^4.0.3",
72
73
  "commander": "^12.1.0",