@aiwg/cli 2026.8.17 → 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 (37) 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/audit/operator-decision.js +15 -1
  13. package/dist/src/channel/manager.mjs +89 -17
  14. package/dist/src/cli/handlers/index.js +3 -1
  15. package/dist/src/cli/handlers/installation.js +79 -0
  16. package/dist/src/cli/handlers/refresh.js +4 -2
  17. package/dist/src/cli/handlers/runtime-info.js +9 -1
  18. package/dist/src/cli/handlers/serve.js +107 -4
  19. package/dist/src/cli/handlers/session.js +12 -26
  20. package/dist/src/cli/handlers/utilities.js +4 -0
  21. package/dist/src/cli/handlers/version.js +4 -0
  22. package/dist/src/config/user-config-dir.mjs +29 -0
  23. package/dist/src/config/user-config.js +4 -22
  24. package/dist/src/extensions/commands/definitions.js +20 -1
  25. package/dist/src/features/catalog.js +2 -1
  26. package/dist/src/flow/graph-metadata.js +56 -0
  27. package/dist/src/installation/manager.mjs +243 -0
  28. package/dist/src/serve/a2a-terminal-observer.js +28 -5
  29. package/dist/src/serve/dispatch-router.js +32 -4
  30. package/dist/src/serve/executor-registry.js +29 -0
  31. package/dist/src/serve/mission-conductor.js +15 -1
  32. package/dist/src/serve/stack-adapters.js +2 -1
  33. package/dist/src/serve/telemetry.js +5 -1
  34. package/dist/src/update/checker.mjs +16 -15
  35. package/dist/src/update/notifier.mjs +8 -3
  36. package/dist/src/update/service.mjs +49 -5
  37. package/package.json +1 -1
@@ -0,0 +1,243 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import {
3
+ accessSync,
4
+ constants,
5
+ existsSync,
6
+ mkdirSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ renameSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
15
+
16
+ export const INSTALLATION_IDENTITY_VERSION = 1;
17
+ export const INSTALLATION_FILE = 'installation.json';
18
+ const METHODS = new Set(['npm', 'web', 'source']);
19
+ const RUN_MODES = new Set(['normal', 'development']);
20
+ const CHANNELS = new Set(['stable', 'next', 'nightly', 'edge']);
21
+ const STRATEGIES = new Set(['npm-global', 'signed-web', 'source-git']);
22
+
23
+ function canonicalPath(value) {
24
+ const resolved = path.resolve(value);
25
+ try { return realpathSync.native(resolved); } catch { return resolved; }
26
+ }
27
+
28
+ function packageName(root) {
29
+ try {
30
+ return JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')).name ?? null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export function inferInstallationMethod(root) {
37
+ if (packageName(root) === '@aiwg/cli') return 'web';
38
+ if (existsSync(path.join(root, '.git'))) return 'source';
39
+ return 'npm';
40
+ }
41
+
42
+ function resolveExecutable(name, options = {}) {
43
+ const env = options.env ?? process.env;
44
+ const explicit = options.managerExecutable ?? env.AIWG_PACKAGE_MANAGER_EXECUTABLE;
45
+ if (explicit) return canonicalPath(explicit);
46
+ if (name === 'npm' && env.npm_execpath) return canonicalPath(env.npm_execpath);
47
+
48
+ const besideNode = path.join(path.dirname(process.execPath), process.platform === 'win32' ? `${name}.cmd` : name);
49
+ if (existsSync(besideNode)) return canonicalPath(besideNode);
50
+ try {
51
+ const finder = process.platform === 'win32' ? 'where.exe' : 'which';
52
+ const found = execFileSync(finder, [name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
53
+ .split(/\r?\n/, 1)[0]?.trim();
54
+ return found ? canonicalPath(found) : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function strategyFor(method) {
61
+ if (method === 'web') return 'signed-web';
62
+ if (method === 'source') return 'source-git';
63
+ return 'npm-global';
64
+ }
65
+
66
+ function executableIsUsable(file) {
67
+ try {
68
+ if (!statSync(file).isFile()) return false;
69
+ if (process.platform !== 'win32') accessSync(file, constants.X_OK);
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function validateIdentity(value, file) {
77
+ const invalid = !value || typeof value !== 'object'
78
+ || value.schemaVersion !== INSTALLATION_IDENTITY_VERSION
79
+ || !METHODS.has(value.method)
80
+ || !RUN_MODES.has(value.runMode)
81
+ || !CHANNELS.has(value.channel)
82
+ || typeof value.root !== 'string'
83
+ || !path.isAbsolute(value.root)
84
+ || !STRATEGIES.has(value.updateStrategy)
85
+ || value.updateStrategy !== strategyFor(value.method)
86
+ || (value.managerExecutable !== null
87
+ && (typeof value.managerExecutable !== 'string' || !path.isAbsolute(value.managerExecutable)));
88
+ if (invalid) {
89
+ const error = new Error(`Invalid AIWG installation identity at ${file}. Run \`aiwg installation adopt\` to replace it.`);
90
+ error.code = 'AIWG_INSTALLATION_INVALID';
91
+ throw error;
92
+ }
93
+ return value;
94
+ }
95
+
96
+ export function installationFile(options = {}) {
97
+ return path.join(resolveUserConfigDir(options), INSTALLATION_FILE);
98
+ }
99
+
100
+ export function createInstallationIdentity(options) {
101
+ if (!options?.actualRoot) throw new Error('actualRoot is required to create an installation identity');
102
+ const root = canonicalPath(options.root ?? options.actualRoot);
103
+ const method = options.method ?? inferInstallationMethod(root);
104
+ const runMode = options.runMode ?? (method === 'source' ? 'development' : 'normal');
105
+ const requestedChannel = options.channel ?? (runMode === 'development' ? 'edge' : 'stable');
106
+ const channel = requestedChannel === 'latest'
107
+ ? 'stable'
108
+ : ['alpha', 'beta', 'rc'].includes(requestedChannel) ? 'next' : requestedChannel;
109
+ const executableName = method === 'npm' ? 'npm' : method === 'source' ? 'git' : null;
110
+ return {
111
+ schemaVersion: INSTALLATION_IDENTITY_VERSION,
112
+ runMode,
113
+ method,
114
+ root,
115
+ updateStrategy: options.updateStrategy ?? strategyFor(method),
116
+ managerExecutable: executableName ? resolveExecutable(executableName, options) : null,
117
+ channel,
118
+ edgePath: options.edgePath ? canonicalPath(options.edgePath) : (runMode === 'development' ? root : null),
119
+ checkOnStartup: options.checkOnStartup ?? true,
120
+ lastUpdateCheck: options.lastUpdateCheck ?? null,
121
+ updateCheckInterval: options.updateCheckInterval ?? 86_400_000,
122
+ recordedAt: options.recordedAt ?? new Date().toISOString(),
123
+ };
124
+ }
125
+
126
+ export function saveInstallationIdentity(identity, options = {}) {
127
+ const file = installationFile(options);
128
+ const validated = validateIdentity(identity, file);
129
+ mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
130
+ const temporary = `${file}.${process.pid}.tmp`;
131
+ writeFileSync(temporary, `${JSON.stringify(validated, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
132
+ renameSync(temporary, file);
133
+ return validated;
134
+ }
135
+
136
+ function readLegacy(options = {}) {
137
+ const file = path.join(resolveUserConfigDir(options), 'channel.json');
138
+ try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
139
+ }
140
+
141
+ /** Load the canonical record, migrating legacy channel.json on first access. */
142
+ export function loadInstallationIdentity(options = {}) {
143
+ const file = installationFile(options);
144
+ if (existsSync(file)) {
145
+ try { return validateIdentity(JSON.parse(readFileSync(file, 'utf8')), file); }
146
+ catch (error) {
147
+ if (error?.code === 'AIWG_INSTALLATION_INVALID') throw error;
148
+ const wrapped = new Error(`Cannot read AIWG installation identity at ${file}: ${error.message}`);
149
+ wrapped.code = 'AIWG_INSTALLATION_INVALID';
150
+ throw wrapped;
151
+ }
152
+ }
153
+ if (options.createIfMissing === false) return null;
154
+ if (!options.actualRoot) return null;
155
+
156
+ const legacy = options.legacyConfig ?? readLegacy(options) ?? {};
157
+ const development = legacy.devMode === true;
158
+ const root = development && legacy.edgePath ? legacy.edgePath : options.actualRoot;
159
+ const method = options.method ?? (development ? 'source' : inferInstallationMethod(root));
160
+ const identity = createInstallationIdentity({
161
+ ...options,
162
+ root,
163
+ method,
164
+ runMode: development ? 'development' : undefined,
165
+ channel: legacy.channel ?? options.channel,
166
+ edgePath: legacy.edgePath,
167
+ lastUpdateCheck: legacy.lastUpdateCheck,
168
+ updateCheckInterval: legacy.updateCheckInterval,
169
+ checkOnStartup: legacy.checkOnStartup,
170
+ });
171
+ return saveInstallationIdentity(identity, options);
172
+ }
173
+
174
+ export function inspectInstallation(options = {}) {
175
+ const actualRoot = canonicalPath(options.actualRoot);
176
+ const actualMethod = options.actualMethod ?? inferInstallationMethod(actualRoot);
177
+ const identity = options.identity ?? loadInstallationIdentity({ ...options, actualRoot });
178
+ if (!identity) return { state: 'unrecorded', identity: null, actualRoot, actualMethod, drift: ['installation identity is not recorded'] };
179
+
180
+ const drift = [];
181
+ const canonicalRoot = canonicalPath(identity.root);
182
+ if (!existsSync(canonicalRoot)) drift.push(`canonical root does not exist: ${canonicalRoot}`);
183
+ if (canonicalRoot !== actualRoot) drift.push(`actual root ${actualRoot} differs from canonical root ${canonicalRoot}`);
184
+ if (identity.method !== actualMethod) drift.push(`actual method ${actualMethod} differs from canonical method ${identity.method}`);
185
+ if (identity.method !== 'web' && !identity.managerExecutable) {
186
+ drift.push(`canonical ${identity.method} installation has no recorded manager executable`);
187
+ }
188
+ if (identity.managerExecutable && !executableIsUsable(identity.managerExecutable)) {
189
+ drift.push(`recorded manager executable is missing or not executable: ${identity.managerExecutable}`);
190
+ }
191
+ return {
192
+ state: drift.length === 0 ? 'aligned' : (existsSync(canonicalRoot) ? 'mismatch' : 'stale'),
193
+ identity,
194
+ canonicalRoot,
195
+ actualRoot,
196
+ actualMethod,
197
+ drift,
198
+ };
199
+ }
200
+
201
+ export function formatInstallationDiagnostic(status) {
202
+ if (status.state === 'aligned') return 'Canonical installation is aligned.';
203
+ return [
204
+ 'AIWG installation identity drift detected; update and refresh are blocked.',
205
+ ...status.drift.map((item) => `- ${item}`),
206
+ 'Inspect: aiwg installation show',
207
+ 'Adopt this installation: aiwg installation adopt',
208
+ 'Switch deliberately: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]',
209
+ ].join('\n');
210
+ }
211
+
212
+ export function assertCanonicalInstallation(options = {}) {
213
+ const status = inspectInstallation(options);
214
+ if (status.state !== 'aligned') {
215
+ const error = new Error(formatInstallationDiagnostic(status));
216
+ error.code = 'AIWG_INSTALLATION_DRIFT';
217
+ error.status = status;
218
+ throw error;
219
+ }
220
+ return status;
221
+ }
222
+
223
+ export function adoptInstallation(options) {
224
+ const inferred = inferInstallationMethod(options.actualRoot);
225
+ if (options.method && options.method !== inferred) {
226
+ throw new Error(`Cannot adopt ${options.actualRoot} as ${options.method}; package contents identify it as ${inferred}.`);
227
+ }
228
+ const identity = createInstallationIdentity({ ...options, root: options.actualRoot });
229
+ saveInstallationIdentity(identity, options);
230
+ return inspectInstallation({ ...options, actualRoot: identity.root, identity });
231
+ }
232
+
233
+ export function switchInstallation(options) {
234
+ if (!options?.root || !options?.method) throw new Error('switch requires root and method');
235
+ if (!existsSync(path.resolve(options.root))) throw new Error(`Installation root does not exist: ${path.resolve(options.root)}`);
236
+ const inferred = inferInstallationMethod(options.root);
237
+ if (options.method !== inferred) {
238
+ throw new Error(`Cannot switch ${options.root} as ${options.method}; package contents identify it as ${inferred}.`);
239
+ }
240
+ const identity = createInstallationIdentity({ ...options, actualRoot: options.root });
241
+ saveInstallationIdentity(identity, options);
242
+ return inspectInstallation({ ...options, actualRoot: identity.root, identity });
243
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { A2AClient } from '../a2a/client.js';
12
12
  import { isTerminalTaskState, } from '../a2a/types.js';
13
+ import { extractGraphMetadata } from '../flow/graph-metadata.js';
13
14
  const DEFAULT_POLL_INTERVAL_MS = 1000;
14
15
  const DEFAULT_MAX_POLLS = 300;
15
16
  export async function observeA2ATerminalState(registry, executor, missionId, a2aInstanceId, initialTask, opts = {}) {
@@ -18,7 +19,11 @@ export async function observeA2ATerminalState(registry, executor, missionId, a2a
18
19
  baseUrl: executor.transportEndpoints.rest,
19
20
  bearer: executor.token,
20
21
  instanceId: a2aInstanceId,
22
+ protocolVersion: opts.protocolVersion ?? '0.3',
23
+ protocolPolicy: opts.protocolVersion ?? '0.3',
21
24
  };
25
+ if (opts.selectedInterface)
26
+ clientOpts.selectedInterface = opts.selectedInterface;
22
27
  if (opts.fetch)
23
28
  clientOpts.fetch = opts.fetch;
24
29
  const client = new A2AClient(clientOpts);
@@ -90,21 +95,39 @@ function emitTerminalTask(registry, executorId, missionId, task) {
90
95
  }));
91
96
  }
92
97
  function makeEnvelope(event, executorId, missionId, task, data) {
98
+ const graph = extractGraphMetadata(task.metadata);
93
99
  return {
94
100
  event,
95
101
  executor_id: executorId,
96
102
  mission_id: missionId,
97
103
  ts: task.status.timestamp ?? new Date().toISOString(),
98
- data,
104
+ data: {
105
+ ...data,
106
+ ...(graph ? {
107
+ graph_metadata: { ...graph, nodeState: taskStateToGraphState(task.status.state) },
108
+ graph_node_state: taskStateToGraphState(task.status.state),
109
+ } : {}),
110
+ },
99
111
  };
100
112
  }
113
+ function taskStateToGraphState(state) {
114
+ switch (state) {
115
+ case 'submitted': return 'pending';
116
+ case 'working': return 'running';
117
+ case 'input-required':
118
+ case 'auth-required': return 'blocked-hitl';
119
+ case 'completed': return 'succeeded';
120
+ case 'failed':
121
+ case 'rejected': return 'failed';
122
+ case 'canceled': return 'canceled';
123
+ default: return 'unknown';
124
+ }
125
+ }
101
126
  function taskStatusSummary(task) {
102
- const status = task.status;
103
- return typeof status.summary === 'string' ? status.summary : undefined;
127
+ return task.status.summary;
104
128
  }
105
129
  function exitCodeData(task) {
106
- const status = task.status;
107
- return typeof status.exit_code === 'number' ? { exit_code: status.exit_code } : {};
130
+ return task.status.exitCode !== undefined ? { exit_code: task.status.exitCode } : {};
108
131
  }
109
132
  function sleep(ms) {
110
133
  return new Promise((resolve) => {
@@ -12,7 +12,7 @@
12
12
  * v1 payload → A2A Message
13
13
  * -------------------------------- --------------------------------------
14
14
  * mission_id message.messageId (idempotency key)
15
- * objective parts[0] = { kind: 'text', text: ... }
15
+ * objective parts[0] = normalized text content
16
16
  * completion metadata.completion
17
17
  * executor_filter metadata.executor_filter
18
18
  * long_running metadata.long_running
@@ -39,7 +39,10 @@ export async function routeDispatch(executor, payload, opts = {}) {
39
39
  }
40
40
  catch (err) {
41
41
  // Only fall back on a 404 from the v2 path. Everything else propagates.
42
- if (err instanceof A2AError && err.status === 404) {
42
+ const policy = opts.a2aProtocolPolicy ?? '0.3';
43
+ const allowLegacyFallback = policy !== '1.0'
44
+ && (opts.allowLegacyExecutorFallback ?? true);
45
+ if (allowLegacyFallback && err instanceof A2AError && err.status === 404) {
43
46
  // Capture sunset for the telemetry event if any was attached.
44
47
  const sunset = err.problem.code === 'aiwg.deprecation_strict' ? undefined : undefined;
45
48
  if (opts.onV1Fallback) {
@@ -62,6 +65,8 @@ async function dispatchV2(executor, payload, opts) {
62
65
  bearer: executor.token,
63
66
  instanceId: a2aInstanceId,
64
67
  requiredExtensions: opts.requiredExtensions ?? [A2A_RUNTIME_V1, A2A_IDEMPOTENCY_V1],
68
+ protocolPolicy: opts.a2aProtocolPolicy ?? '0.3',
69
+ allowProtocolFallback: opts.allowA2AProtocolFallback ?? false,
65
70
  };
66
71
  if (opts.fetch)
67
72
  clientOpts.fetch = opts.fetch;
@@ -69,7 +74,27 @@ async function dispatchV2(executor, payload, opts) {
69
74
  clientOpts.optionalExtensions = opts.optionalExtensions;
70
75
  if (opts.onDeprecation)
71
76
  clientOpts.onDeprecation = opts.onDeprecation;
72
- const client = new A2AClient(clientOpts);
77
+ if (opts.onA2AProtocolFallback)
78
+ clientOpts.onProtocolFallback = opts.onA2AProtocolFallback;
79
+ if (opts.onA2AProtocolSelection) {
80
+ clientOpts.onProtocolSelection = info => opts.onA2AProtocolSelection?.(info);
81
+ }
82
+ // The deployed 0.3 compatibility route predates AgentCard negotiation. Model
83
+ // it as an explicit interface so headerless legacy selection is observable
84
+ // in registry, telemetry, audit, and dispatch results without adding a new
85
+ // discovery dependency to the compatibility path.
86
+ if (clientOpts.protocolPolicy === '0.3') {
87
+ clientOpts.selectedInterface = {
88
+ url: `${executor.transportEndpoints.rest.replace(/\/+$/, '')}/agents/${encodeURIComponent(a2aInstanceId)}`,
89
+ protocolBinding: 'REST',
90
+ protocolVersion: '0.3',
91
+ preference: 0,
92
+ legacy: true,
93
+ };
94
+ }
95
+ const client = clientOpts.protocolPolicy === '0.3'
96
+ ? new A2AClient(clientOpts)
97
+ : await A2AClient.negotiate(clientOpts);
73
98
  const message = payloadToMessage(payload);
74
99
  const result = await client.sendMessage(message);
75
100
  return {
@@ -79,6 +104,9 @@ async function dispatchV2(executor, payload, opts) {
79
104
  dispatchPath: 'v2',
80
105
  task: result.task,
81
106
  idempotentReplayed: result.idempotentReplayed,
107
+ a2aProtocolVersion: result.protocolVersion,
108
+ ...(result.selectedInterface ? { a2aInterface: result.selectedInterface } : {}),
109
+ ...(result.fallbackReason ? { a2aFallbackReason: result.fallbackReason } : {}),
82
110
  };
83
111
  }
84
112
  function resolveA2AInstanceId(executor, payload, opts) {
@@ -145,7 +173,7 @@ function payloadToMessage(payload) {
145
173
  return {
146
174
  messageId: payload.mission_id,
147
175
  role: 'user',
148
- parts: [{ kind: 'text', text: payload.objective }],
176
+ parts: [{ type: 'text', text: payload.objective }],
149
177
  metadata,
150
178
  };
151
179
  }
@@ -232,6 +232,7 @@ export class ExecutorRegistry extends EventEmitter {
232
232
  // Upsert — preserve token and registeredAt
233
233
  existing.name = req.name;
234
234
  existing.a2aInstanceId = req.a2a_instance_id;
235
+ delete existing.a2aProtocol;
235
236
  existing.version = req.version;
236
237
  existing.specVersion = req.spec_version;
237
238
  existing.transportEndpoints = req.transport_endpoints;
@@ -614,6 +615,24 @@ export class ExecutorRegistry extends EventEmitter {
614
615
  getRegistration(executorId) {
615
616
  return this.executors.get(executorId);
616
617
  }
618
+ /** Publish the negotiated interface used for the most recent A2A dispatch. */
619
+ recordA2AProtocolSelection(executorId, selection) {
620
+ const executor = this.executors.get(executorId);
621
+ if (!executor)
622
+ return false;
623
+ executor.a2aProtocol = {
624
+ ...selection,
625
+ selectedAt: selection.selectedAt ?? new Date().toISOString(),
626
+ };
627
+ this.emit('executor:a2a_protocol_selected', {
628
+ executorId,
629
+ selectedVersion: selection.selectedVersion,
630
+ protocolBinding: selection.interface.protocolBinding,
631
+ interfaceUrl: selection.interface.url,
632
+ ...(selection.fallbackReason ? { fallbackReason: selection.fallbackReason } : {}),
633
+ });
634
+ return true;
635
+ }
617
636
  /**
618
637
  * Pick the best executor matching the given filter.
619
638
  *
@@ -708,6 +727,16 @@ function toSummary(e) {
708
727
  };
709
728
  if (e.a2aInstanceId)
710
729
  summary.a2a_instance_id = e.a2aInstanceId;
730
+ if (e.a2aProtocol) {
731
+ summary.a2a_protocol = {
732
+ policy: e.a2aProtocol.policy,
733
+ selected_version: e.a2aProtocol.selectedVersion,
734
+ protocol_binding: e.a2aProtocol.interface.protocolBinding,
735
+ interface_url: e.a2aProtocol.interface.url,
736
+ selected_at: e.a2aProtocol.selectedAt,
737
+ ...(e.a2aProtocol.fallbackReason ? { fallback_reason: e.a2aProtocol.fallbackReason } : {}),
738
+ };
739
+ }
711
740
  return summary;
712
741
  }
713
742
  // Singleton instance
@@ -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);
@@ -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
@@ -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
  }