@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.
- package/bin/aiwg.mjs +24 -2
- package/dist/src/a2a/agent-card.js +4 -1
- package/dist/src/a2a/client.js +148 -68
- package/dist/src/a2a/codecs.js +480 -0
- package/dist/src/a2a/events.js +226 -0
- package/dist/src/a2a/hitl-driver.js +8 -6
- package/dist/src/a2a/hitl.js +2 -1
- package/dist/src/a2a/http.js +85 -5
- package/dist/src/a2a/protocol.js +136 -0
- package/dist/src/a2a/types.js +4 -14
- package/dist/src/a2a/webhook.js +101 -4
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/cli.js +1 -1
- package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -0
- package/dist/src/artifacts/types.js +73 -1
- package/dist/src/audit/operator-decision.js +15 -1
- package/dist/src/channel/manager.mjs +89 -17
- package/dist/src/cli/agent-spawn.js +4 -2
- package/dist/src/cli/handlers/cockpit.js +41 -0
- package/dist/src/cli/handlers/help.js +1 -1
- package/dist/src/cli/handlers/index.js +3 -1
- package/dist/src/cli/handlers/installation.js +79 -0
- package/dist/src/cli/handlers/ralph.js +2 -1
- package/dist/src/cli/handlers/refresh.js +4 -2
- package/dist/src/cli/handlers/runtime-info.js +9 -1
- package/dist/src/cli/handlers/sdlc-accelerate.js +2 -1
- package/dist/src/cli/handlers/serve.js +107 -4
- package/dist/src/cli/handlers/session.js +12 -26
- package/dist/src/cli/handlers/use.js +117 -12
- package/dist/src/cli/handlers/utilities.js +4 -0
- package/dist/src/cli/handlers/version.js +4 -0
- package/dist/src/cli/handlers/workspace.js +24 -2
- package/dist/src/cli/services/deployment-verification.js +9 -2
- package/dist/src/cockpit/doctor.js +257 -0
- package/dist/src/config/aiwg-config.js +36 -3
- package/dist/src/config/user-config-dir.mjs +29 -0
- package/dist/src/config/user-config.js +4 -22
- package/dist/src/extensions/commands/definitions.js +20 -1
- package/dist/src/features/catalog.js +2 -1
- package/dist/src/flow/graph-metadata.js +56 -0
- package/dist/src/installation/manager.mjs +243 -0
- package/dist/src/providers/transformation-receipt-integration.js +130 -3
- package/dist/src/security/artifact-verifier.js +7 -1
- package/dist/src/serve/a2a-terminal-observer.js +28 -5
- package/dist/src/serve/dispatch-router.js +32 -4
- package/dist/src/serve/executor-registry.js +29 -0
- package/dist/src/serve/mission-conductor.js +15 -1
- package/dist/src/serve/pty-bridge.js +6 -11
- package/dist/src/serve/stack-adapters.js +2 -1
- package/dist/src/serve/telemetry.js +5 -1
- package/dist/src/skills/run.js +15 -6
- package/dist/src/update/checker.mjs +16 -15
- package/dist/src/update/notifier.mjs +8 -3
- package/dist/src/update/service.mjs +49 -5
- package/package.json +2 -1
package/dist/src/a2a/webhook.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// @issue #1256
|
|
17
17
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
18
|
+
import { decodeStreamResponse, A2AEventReconciler } from './events.js';
|
|
18
19
|
/** Header name (case-insensitive). */
|
|
19
20
|
export const SIGNATURE_HEADER = 'x-aiwg-signature';
|
|
20
21
|
/** Five minutes — RFC 8941 timestamp tolerance. */
|
|
@@ -111,6 +112,7 @@ function constantTimeHexEqual(a, b) {
|
|
|
111
112
|
export class IdempotencyCache {
|
|
112
113
|
capacity;
|
|
113
114
|
seen = new Set();
|
|
115
|
+
pending = new Set();
|
|
114
116
|
order = [];
|
|
115
117
|
constructor(capacity = DEFAULT_IDEMPOTENCY_CAPACITY) {
|
|
116
118
|
this.capacity = Math.max(16, capacity);
|
|
@@ -119,6 +121,23 @@ export class IdempotencyCache {
|
|
|
119
121
|
markFresh(id) {
|
|
120
122
|
if (this.seen.has(id))
|
|
121
123
|
return false;
|
|
124
|
+
this.commit(id);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
/** Reserve an event before parsing/routing so concurrent deliveries cannot race. */
|
|
128
|
+
begin(id) {
|
|
129
|
+
if (this.seen.has(id))
|
|
130
|
+
return 'duplicate';
|
|
131
|
+
if (this.pending.has(id))
|
|
132
|
+
return 'pending';
|
|
133
|
+
this.pending.add(id);
|
|
134
|
+
return 'fresh';
|
|
135
|
+
}
|
|
136
|
+
/** Mark a successfully routed reservation as completed. */
|
|
137
|
+
commit(id) {
|
|
138
|
+
this.pending.delete(id);
|
|
139
|
+
if (this.seen.has(id))
|
|
140
|
+
return;
|
|
122
141
|
this.seen.add(id);
|
|
123
142
|
this.order.push(id);
|
|
124
143
|
while (this.order.length > this.capacity) {
|
|
@@ -126,7 +145,10 @@ export class IdempotencyCache {
|
|
|
126
145
|
if (evicted !== undefined)
|
|
127
146
|
this.seen.delete(evicted);
|
|
128
147
|
}
|
|
129
|
-
|
|
148
|
+
}
|
|
149
|
+
/** Release a failed reservation so a later retry can be processed. */
|
|
150
|
+
release(id) {
|
|
151
|
+
this.pending.delete(id);
|
|
130
152
|
}
|
|
131
153
|
size() {
|
|
132
154
|
return this.seen.size;
|
|
@@ -134,15 +156,35 @@ export class IdempotencyCache {
|
|
|
134
156
|
}
|
|
135
157
|
export class PushSecretRegistry {
|
|
136
158
|
entries = new Map();
|
|
159
|
+
reconcilers = new Map();
|
|
137
160
|
register(entry) {
|
|
161
|
+
if (entry.protocolVersion === '1.0' && !entry.taskId) {
|
|
162
|
+
throw new Error('A2A 1.0 push config registration requires taskId ownership scope');
|
|
163
|
+
}
|
|
138
164
|
this.entries.set(entry.configId, entry);
|
|
165
|
+
if (entry.taskId) {
|
|
166
|
+
this.reconcilers.set(entry.configId, new A2AEventReconciler({
|
|
167
|
+
taskId: entry.taskId,
|
|
168
|
+
...(entry.contextId ? { contextId: entry.contextId } : {}),
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
139
171
|
}
|
|
140
172
|
lookup(configId) {
|
|
141
173
|
return this.entries.get(configId) ?? null;
|
|
142
174
|
}
|
|
143
175
|
unregister(configId) {
|
|
176
|
+
this.reconcilers.delete(configId);
|
|
144
177
|
return this.entries.delete(configId);
|
|
145
178
|
}
|
|
179
|
+
reconcile(configId, event) {
|
|
180
|
+
const entry = this.entries.get(configId);
|
|
181
|
+
const eventOwner = ownerOf(event);
|
|
182
|
+
if (entry?.taskOwner && eventOwner && entry.taskOwner !== eventOwner) {
|
|
183
|
+
throw new Error(`A2A event belongs to owner ${eventOwner}, expected ${entry.taskOwner}`);
|
|
184
|
+
}
|
|
185
|
+
const reconciler = this.reconcilers.get(configId);
|
|
186
|
+
return reconciler ? reconciler.accept(event) : event;
|
|
187
|
+
}
|
|
146
188
|
/** Test/debug helper. */
|
|
147
189
|
size() {
|
|
148
190
|
return this.entries.size;
|
|
@@ -193,40 +235,83 @@ export async function handleWebhook(configId, body, signature, eventId, opts) {
|
|
|
193
235
|
// Idempotency check — duplicate event-ids are accepted with 200 but
|
|
194
236
|
// not re-routed. The executor's retry logic depends on a 2xx response
|
|
195
237
|
// to mark delivery complete; failing here would cause infinite retry.
|
|
196
|
-
const
|
|
197
|
-
|
|
238
|
+
const entryForScope = opts.registry.lookup(configId);
|
|
239
|
+
const protocolVersion = entryForScope?.protocolVersion ?? '0.3';
|
|
240
|
+
const scopedEventId = [
|
|
241
|
+
configId,
|
|
242
|
+
protocolVersion,
|
|
243
|
+
entryForScope?.taskOwner ?? '',
|
|
244
|
+
entryForScope?.taskId ?? '',
|
|
245
|
+
eventId,
|
|
246
|
+
].join('|');
|
|
247
|
+
const reservation = opts.idempotency.begin(scopedEventId);
|
|
248
|
+
if (reservation === 'duplicate') {
|
|
198
249
|
return { status: 200, body: { ok: true, deduped: true } };
|
|
199
250
|
}
|
|
251
|
+
if (reservation === 'pending') {
|
|
252
|
+
return {
|
|
253
|
+
status: 409,
|
|
254
|
+
body: errorBody('aiwg.webhook_event_in_progress', 'a concurrent delivery is still being processed'),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
200
257
|
// Route the verified payload. Errors thrown here become 500 so the
|
|
201
258
|
// executor will retry — pick the abstraction carefully on the
|
|
202
259
|
// mission-state side.
|
|
203
260
|
const entry = opts.registry.lookup(configId);
|
|
204
261
|
if (!entry) {
|
|
205
262
|
// Edge case: secret was unregistered between verify and route.
|
|
263
|
+
opts.idempotency.release(scopedEventId);
|
|
206
264
|
return {
|
|
207
265
|
status: 404,
|
|
208
266
|
body: errorBody('aiwg.webhook_secret_unknown', `configId='${configId}' no longer registered`),
|
|
209
267
|
};
|
|
210
268
|
}
|
|
269
|
+
if (protocolVersion === '1.0' && opts.contentType?.split(';')[0]?.trim().toLowerCase() !== 'application/a2a+json') {
|
|
270
|
+
opts.idempotency.release(scopedEventId);
|
|
271
|
+
return {
|
|
272
|
+
status: 415,
|
|
273
|
+
body: errorBody('aiwg.webhook_content_type_invalid', 'A2A 1.0 push requires application/a2a+json'),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
211
276
|
let parsed;
|
|
212
277
|
try {
|
|
213
278
|
parsed = JSON.parse(body.toString('utf8'));
|
|
214
279
|
}
|
|
215
280
|
catch (e) {
|
|
281
|
+
opts.idempotency.release(scopedEventId);
|
|
216
282
|
return {
|
|
217
283
|
status: 400,
|
|
218
284
|
body: errorBody('aiwg.webhook_body_not_json', e.message),
|
|
219
285
|
};
|
|
220
286
|
}
|
|
287
|
+
let event;
|
|
221
288
|
try {
|
|
222
|
-
|
|
289
|
+
event = decodeStreamResponse(protocolVersion, parsed, { eventId });
|
|
290
|
+
const accepted = opts.registry.reconcile(configId, event);
|
|
291
|
+
if (!accepted) {
|
|
292
|
+
opts.idempotency.commit(scopedEventId);
|
|
293
|
+
return { status: 200, body: { ok: true, deduped: true } };
|
|
294
|
+
}
|
|
295
|
+
event = accepted;
|
|
223
296
|
}
|
|
224
297
|
catch (e) {
|
|
298
|
+
opts.idempotency.release(scopedEventId);
|
|
299
|
+
return {
|
|
300
|
+
status: 400,
|
|
301
|
+
body: errorBody('aiwg.webhook_event_invalid', e.message),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await opts.route(entry, event);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
opts.idempotency.release(scopedEventId);
|
|
225
309
|
return {
|
|
226
310
|
status: 500,
|
|
227
311
|
body: errorBody('aiwg.webhook_route_failed', e.message),
|
|
228
312
|
};
|
|
229
313
|
}
|
|
314
|
+
opts.idempotency.commit(scopedEventId);
|
|
230
315
|
return { status: 200, body: { ok: true } };
|
|
231
316
|
}
|
|
232
317
|
function errorBody(code, detail) {
|
|
@@ -237,4 +322,16 @@ function errorBody(code, detail) {
|
|
|
237
322
|
detail,
|
|
238
323
|
};
|
|
239
324
|
}
|
|
325
|
+
function ownerOf(event) {
|
|
326
|
+
const metadata = event.type === 'task'
|
|
327
|
+
? event.task.metadata
|
|
328
|
+
: event.type === 'message'
|
|
329
|
+
? event.message.metadata
|
|
330
|
+
: event.metadata;
|
|
331
|
+
const owner = metadata?.['task_owner']
|
|
332
|
+
?? metadata?.['taskOwner']
|
|
333
|
+
?? metadata?.['tenant_id']
|
|
334
|
+
?? metadata?.['tenantId'];
|
|
335
|
+
return typeof owner === 'string' && owner ? owner : undefined;
|
|
336
|
+
}
|
|
240
337
|
//# sourceMappingURL=webhook.js.map
|
|
@@ -454,6 +454,7 @@ function recordForEntry(cwd, entry, graphName, dependencyGraph, privacy, schemaV
|
|
|
454
454
|
...(entry.searchTerms?.length
|
|
455
455
|
? { aiwg_search_terms: uniqueSorted(entry.searchTerms) }
|
|
456
456
|
: {}),
|
|
457
|
+
...(entry.script ? { aiwg_script: entry.script } : {}),
|
|
457
458
|
},
|
|
458
459
|
},
|
|
459
460
|
}
|
|
@@ -494,7 +494,7 @@ async function handleBuild(args) {
|
|
|
494
494
|
console.log('');
|
|
495
495
|
console.log('Default behavior (no --graph): builds all graphs with defaultBuild: true');
|
|
496
496
|
console.log('Multi-graph builds run by buildOrder/buildTier (refs → citations → bibliography before heavy graphs)');
|
|
497
|
-
console.log(' Built-in defaults: project (always), codebase (
|
|
497
|
+
console.log(' Built-in defaults: project (always), codebase (auto-detects JavaScript/TypeScript and Python layouts)');
|
|
498
498
|
console.log('');
|
|
499
499
|
console.log('Examples:');
|
|
500
500
|
console.log(' aiwg index build');
|
|
@@ -210,6 +210,7 @@ function artifactTypeFromRecord(record) {
|
|
|
210
210
|
function entryFromRecord(record) {
|
|
211
211
|
const indexedFrontmatter = record.search?.frontmatter ?? {};
|
|
212
212
|
const indexedSearchTerms = indexedFrontmatter.aiwg_search_terms;
|
|
213
|
+
const indexedScript = indexedFrontmatter.aiwg_script;
|
|
213
214
|
return {
|
|
214
215
|
path: record.source.path,
|
|
215
216
|
type: artifactTypeFromRecord(record),
|
|
@@ -245,6 +246,11 @@ function entryFromRecord(record) {
|
|
|
245
246
|
kernel: typeof record.search?.frontmatter?.kernel === "boolean"
|
|
246
247
|
? record.search.frontmatter.kernel
|
|
247
248
|
: undefined,
|
|
249
|
+
script: indexedScript && typeof indexedScript === "object"
|
|
250
|
+
&& typeof indexedScript.entrypoint === "string"
|
|
251
|
+
&& typeof indexedScript.runtime === "string"
|
|
252
|
+
? indexedScript
|
|
253
|
+
: undefined,
|
|
248
254
|
operationalState: record.operational_state,
|
|
249
255
|
};
|
|
250
256
|
}
|
|
@@ -195,6 +195,60 @@ export const BUILTIN_GRAPH_CONFIGS = {
|
|
|
195
195
|
* @implements #426
|
|
196
196
|
*/
|
|
197
197
|
export const GRAPH_CONFIGS = { ...BUILTIN_GRAPH_CONFIGS };
|
|
198
|
+
function freshBuiltinGraphConfig(name) {
|
|
199
|
+
const config = BUILTIN_GRAPH_CONFIGS[name];
|
|
200
|
+
return {
|
|
201
|
+
...config,
|
|
202
|
+
scanDirs: [...config.scanDirs],
|
|
203
|
+
extensions: [...config.extensions],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Detect conventional Python layouts without treating every top-level folder
|
|
208
|
+
* as source. A Python project manifest activates `.py`/`.pyi` support; package
|
|
209
|
+
* roots are immediate directories containing `__init__.py`, plus the common
|
|
210
|
+
* `tests/` and `scripts/` roots when present.
|
|
211
|
+
*/
|
|
212
|
+
function detectPythonCodebaseConfig(cwd, base) {
|
|
213
|
+
const hasPythonManifest = ['pyproject.toml', 'setup.py', 'setup.cfg']
|
|
214
|
+
.some((manifest) => fs.existsSync(path.join(cwd, manifest)));
|
|
215
|
+
if (!hasPythonManifest)
|
|
216
|
+
return base;
|
|
217
|
+
const detectedRoots = [];
|
|
218
|
+
for (const root of ['tests', 'scripts']) {
|
|
219
|
+
if (fs.existsSync(path.join(cwd, root)))
|
|
220
|
+
detectedRoots.push(root);
|
|
221
|
+
}
|
|
222
|
+
const excluded = new Set([
|
|
223
|
+
'.aiwg', '.git', '.github', '.venv', 'venv', 'node_modules',
|
|
224
|
+
'src', 'test', 'tests', 'tools', 'scripts', 'docs', 'documentation',
|
|
225
|
+
]);
|
|
226
|
+
try {
|
|
227
|
+
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
228
|
+
if (!entry.isDirectory() || excluded.has(entry.name) || entry.name.startsWith('.'))
|
|
229
|
+
continue;
|
|
230
|
+
if (fs.existsSync(path.join(cwd, entry.name, '__init__.py')))
|
|
231
|
+
detectedRoots.push(entry.name);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Layout detection is best-effort; the immutable defaults still apply.
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
...base,
|
|
239
|
+
scanDirs: [...new Set([...base.scanDirs, ...detectedRoots])],
|
|
240
|
+
extensions: [...new Set([...base.extensions, '.py', '.pyi'])],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function applyBuiltinGraphOverride(base, override) {
|
|
244
|
+
if (!override)
|
|
245
|
+
return base;
|
|
246
|
+
return {
|
|
247
|
+
...base,
|
|
248
|
+
scanDirs: override.scanDirs ? [...override.scanDirs] : base.scanDirs,
|
|
249
|
+
extensions: override.extensions ? [...override.extensions] : base.extensions,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
198
252
|
/**
|
|
199
253
|
* Normalize metadataSupplements entries.
|
|
200
254
|
*
|
|
@@ -400,6 +454,11 @@ export function loadModuleGraphConfigs(cwd, diagnostics) {
|
|
|
400
454
|
* @implements #426 #726
|
|
401
455
|
*/
|
|
402
456
|
export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
457
|
+
// Built-ins are immutable in index.graphs, but codebase roots/extensions may
|
|
458
|
+
// be adapted through the explicitly bounded graphOverrides contract (#2123).
|
|
459
|
+
// Reset on every project load so a prior cwd cannot leak its override or
|
|
460
|
+
// detected Python package roots into a later build in the same process.
|
|
461
|
+
GRAPH_CONFIGS.codebase = detectPythonCodebaseConfig(cwd, freshBuiltinGraphConfig('codebase'));
|
|
403
462
|
// Load module-declared graphs first (frameworks/addons)
|
|
404
463
|
const moduleLoaded = loadModuleGraphConfigs(cwd, diagnostics);
|
|
405
464
|
const loaded = [...moduleLoaded];
|
|
@@ -408,6 +467,7 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
408
467
|
// config.yaml is a
|
|
409
468
|
// deprecated fallback so un-migrated corpora keep working.
|
|
410
469
|
let graphs;
|
|
470
|
+
let graphOverrides;
|
|
411
471
|
let fromDeprecatedYaml = false;
|
|
412
472
|
// (a) Canonical: .aiwg/aiwg.config (JSON).
|
|
413
473
|
try {
|
|
@@ -418,6 +478,9 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
418
478
|
const g = idx?.graphs;
|
|
419
479
|
if (g && typeof g === 'object')
|
|
420
480
|
graphs = g;
|
|
481
|
+
const overrides = idx?.graphOverrides;
|
|
482
|
+
if (overrides && typeof overrides === 'object')
|
|
483
|
+
graphOverrides = overrides;
|
|
421
484
|
}
|
|
422
485
|
}
|
|
423
486
|
catch {
|
|
@@ -430,7 +493,7 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
430
493
|
});
|
|
431
494
|
}
|
|
432
495
|
// (b) Fallback: legacy .aiwg/config.yaml.
|
|
433
|
-
if (!graphs) {
|
|
496
|
+
if (!graphs && !graphOverrides) {
|
|
434
497
|
try {
|
|
435
498
|
const configPath = projectAiwgPath(cwd, 'config.yaml');
|
|
436
499
|
if (fs.existsSync(configPath)) {
|
|
@@ -441,12 +504,21 @@ export function loadUserGraphConfigs(cwd, diagnostics) {
|
|
|
441
504
|
graphs = g;
|
|
442
505
|
fromDeprecatedYaml = true;
|
|
443
506
|
}
|
|
507
|
+
const overrides = idx?.graphOverrides;
|
|
508
|
+
if (overrides && typeof overrides === 'object') {
|
|
509
|
+
graphOverrides = overrides;
|
|
510
|
+
fromDeprecatedYaml = true;
|
|
511
|
+
}
|
|
444
512
|
}
|
|
445
513
|
}
|
|
446
514
|
catch {
|
|
447
515
|
// best-effort
|
|
448
516
|
}
|
|
449
517
|
}
|
|
518
|
+
const codebaseOverride = graphOverrides?.codebase;
|
|
519
|
+
if (codebaseOverride && typeof codebaseOverride === 'object' && !Array.isArray(codebaseOverride)) {
|
|
520
|
+
GRAPH_CONFIGS.codebase = applyBuiltinGraphOverride(GRAPH_CONFIGS.codebase, codebaseOverride);
|
|
521
|
+
}
|
|
450
522
|
if (!graphs)
|
|
451
523
|
return loaded;
|
|
452
524
|
if (fromDeprecatedYaml && !yamlIndexDeprecationWarned) {
|
|
@@ -22,8 +22,9 @@ export function createDecisionRecord(input, previousHash) {
|
|
|
22
22
|
const actor = redact(input.actor);
|
|
23
23
|
const correlation = redact(input.correlation);
|
|
24
24
|
const runtime = input.runtime ? redact(input.runtime) : undefined;
|
|
25
|
+
const graph = input.graph ? redact(input.graph) : undefined;
|
|
25
26
|
const reason = redact(input.reason);
|
|
26
|
-
const detected = [...actor.paths, ...correlation.paths, ...(runtime?.paths ?? []), ...reason.paths];
|
|
27
|
+
const detected = [...actor.paths, ...correlation.paths, ...(runtime?.paths ?? []), ...(graph?.paths ?? []), ...reason.paths];
|
|
27
28
|
const unsigned = {
|
|
28
29
|
schema_version: OPERATOR_DECISION_SCHEMA,
|
|
29
30
|
event_id: input.event_id ?? randomUUID(),
|
|
@@ -36,6 +37,7 @@ export function createDecisionRecord(input, previousHash) {
|
|
|
36
37
|
classification: input.classification,
|
|
37
38
|
correlation: correlation.value,
|
|
38
39
|
...(runtime ? { runtime: runtime.value } : {}),
|
|
40
|
+
...(graph ? { graph: graph.value } : {}),
|
|
39
41
|
...(input.policy_ref ? { policy_ref: input.policy_ref } : {}),
|
|
40
42
|
redacted_fields: [...new Set([...(input.redacted_fields ?? []), ...detected])].sort(),
|
|
41
43
|
previous_hash: previousHash,
|
|
@@ -74,6 +76,15 @@ export function toOpenTelemetryLog(record) {
|
|
|
74
76
|
'aiwg.provider.id': record.correlation.provider_id,
|
|
75
77
|
'aiwg.sandbox.task_id': record.correlation.sandbox_task_id,
|
|
76
78
|
'aiwg.prompt.id': record.correlation.prompt_id,
|
|
79
|
+
'aiwg.graph.id': record.graph?.graph_id,
|
|
80
|
+
'aiwg.graph.version': record.graph?.graph_version,
|
|
81
|
+
'aiwg.graph.run_id': record.graph?.run_id,
|
|
82
|
+
'aiwg.graph.node_id': record.graph?.node_id,
|
|
83
|
+
'aiwg.graph.node_run_id': record.graph?.node_run_id,
|
|
84
|
+
'aiwg.graph.edge_id': record.graph?.edge_id,
|
|
85
|
+
'aiwg.graph.route_name': record.graph?.route_name,
|
|
86
|
+
'aiwg.graph.checkpoint_id': record.graph?.checkpoint_id,
|
|
87
|
+
'aiwg.graph.replay_parent_run_id': record.graph?.replay_parent_run_id,
|
|
77
88
|
}).filter(([, value]) => value !== undefined).map(([key, value]) => ({
|
|
78
89
|
key,
|
|
79
90
|
value: { stringValue: String(value) },
|
|
@@ -135,6 +146,9 @@ function validateInput(input) {
|
|
|
135
146
|
throw new Error('a non-empty operator reason is required');
|
|
136
147
|
if (!Object.values(input.correlation).some(Boolean))
|
|
137
148
|
throw new Error('at least one correlation identifier is required');
|
|
149
|
+
if (input.graph && ![input.graph.graph_id, input.graph.graph_version, input.graph.run_id, input.graph.node_id, input.graph.node_run_id].every(value => typeof value === 'string' && value.length > 0)) {
|
|
150
|
+
throw new Error('graph decision context requires graph, run, node, and node-run identity');
|
|
151
|
+
}
|
|
138
152
|
if (input.timestamp && !Number.isFinite(Date.parse(input.timestamp)))
|
|
139
153
|
throw new Error('timestamp must be valid ISO time');
|
|
140
154
|
}
|
|
@@ -13,8 +13,17 @@ import fs from 'fs/promises';
|
|
|
13
13
|
import { readFileSync, existsSync } from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
-
import { execSync, spawn } from 'child_process';
|
|
16
|
+
import { execFileSync, execSync, spawn } from 'child_process';
|
|
17
17
|
import os from 'os';
|
|
18
|
+
import { resolveUserConfigDir } from '../config/user-config-dir.mjs';
|
|
19
|
+
import {
|
|
20
|
+
assertCanonicalInstallation,
|
|
21
|
+
createInstallationIdentity,
|
|
22
|
+
inferInstallationMethod,
|
|
23
|
+
inspectInstallation,
|
|
24
|
+
loadInstallationIdentity,
|
|
25
|
+
saveInstallationIdentity,
|
|
26
|
+
} from '../installation/manager.mjs';
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
29
|
* Run a command with inherited stdio, applying a wall-clock timeout so a
|
|
@@ -80,8 +89,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
80
89
|
const __dirname = path.dirname(__filename);
|
|
81
90
|
|
|
82
91
|
// Configuration paths
|
|
83
|
-
const CONFIG_DIR = path.join(os.homedir(), '.aiwg');
|
|
84
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'channel.json');
|
|
85
92
|
const EDGE_INSTALL_PATH = path.join(os.homedir(), '.local', 'share', 'ai-writing-guide');
|
|
86
93
|
const REPO_URL = 'https://github.com/jmagly/aiwg.git';
|
|
87
94
|
|
|
@@ -132,22 +139,65 @@ export function getPackageRoot() {
|
|
|
132
139
|
* Load channel configuration
|
|
133
140
|
* @returns {Promise<object>} Channel configuration
|
|
134
141
|
*/
|
|
135
|
-
export async function loadConfig() {
|
|
142
|
+
export async function loadConfig(options = {}) {
|
|
143
|
+
const configDir = resolveUserConfigDir(options);
|
|
144
|
+
const configFile = path.join(configDir, 'channel.json');
|
|
145
|
+
let legacy = {};
|
|
136
146
|
try {
|
|
137
|
-
const data = await fs.readFile(
|
|
138
|
-
|
|
147
|
+
const data = await fs.readFile(configFile, 'utf8');
|
|
148
|
+
legacy = JSON.parse(data);
|
|
139
149
|
} catch {
|
|
140
|
-
|
|
150
|
+
// Legacy state is optional. The canonical identity below is authoritative.
|
|
141
151
|
}
|
|
152
|
+
const actualRoot = options.actualRoot ?? getPackageRoot();
|
|
153
|
+
const identity = loadInstallationIdentity({ ...options, actualRoot, legacyConfig: legacy });
|
|
154
|
+
if (!identity) return { ...DEFAULT_CONFIG, ...legacy };
|
|
155
|
+
return {
|
|
156
|
+
...DEFAULT_CONFIG,
|
|
157
|
+
...legacy,
|
|
158
|
+
channel: identity.channel,
|
|
159
|
+
edgePath: identity.edgePath ?? legacy.edgePath ?? EDGE_INSTALL_PATH,
|
|
160
|
+
devMode: identity.runMode === 'development',
|
|
161
|
+
lastUpdateCheck: identity.lastUpdateCheck,
|
|
162
|
+
updateCheckInterval: identity.updateCheckInterval,
|
|
163
|
+
checkOnStartup: identity.checkOnStartup,
|
|
164
|
+
installation: identity,
|
|
165
|
+
};
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
/**
|
|
145
169
|
* Save channel configuration
|
|
146
170
|
* @param {object} config - Configuration to save
|
|
147
171
|
*/
|
|
148
|
-
export async function saveConfig(config) {
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
export async function saveConfig(config, options = {}) {
|
|
173
|
+
const configDir = resolveUserConfigDir(options);
|
|
174
|
+
const configFile = path.join(configDir, 'channel.json');
|
|
175
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
176
|
+
const { installation: _installation, ...legacyConfig } = config;
|
|
177
|
+
await fs.writeFile(configFile, `${JSON.stringify(legacyConfig, null, 2)}\n`);
|
|
178
|
+
|
|
179
|
+
const actualRoot = options.actualRoot ?? getPackageRoot();
|
|
180
|
+
const previous = loadInstallationIdentity({ ...options, actualRoot, legacyConfig: config });
|
|
181
|
+
if (previous) {
|
|
182
|
+
const development = config.devMode === true;
|
|
183
|
+
const root = development && config.edgePath ? config.edgePath : previous.root;
|
|
184
|
+
const method = development ? 'source' : previous.method;
|
|
185
|
+
saveInstallationIdentity(createInstallationIdentity({
|
|
186
|
+
...options,
|
|
187
|
+
actualRoot,
|
|
188
|
+
root,
|
|
189
|
+
method,
|
|
190
|
+
runMode: development ? 'development' : 'normal',
|
|
191
|
+
channel: config.channel ?? previous.channel,
|
|
192
|
+
edgePath: config.edgePath ?? previous.edgePath,
|
|
193
|
+
managerExecutable: development ? undefined : previous.managerExecutable,
|
|
194
|
+
updateStrategy: development ? 'source-git' : previous.updateStrategy,
|
|
195
|
+
lastUpdateCheck: config.lastUpdateCheck ?? previous.lastUpdateCheck,
|
|
196
|
+
updateCheckInterval: config.updateCheckInterval ?? previous.updateCheckInterval,
|
|
197
|
+
checkOnStartup: config.checkOnStartup ?? previous.checkOnStartup,
|
|
198
|
+
recordedAt: previous.recordedAt,
|
|
199
|
+
}), options);
|
|
200
|
+
}
|
|
151
201
|
}
|
|
152
202
|
|
|
153
203
|
/**
|
|
@@ -178,7 +228,7 @@ export async function getFrameworkRoot() {
|
|
|
178
228
|
}
|
|
179
229
|
}
|
|
180
230
|
|
|
181
|
-
return getPackageRoot();
|
|
231
|
+
return config.installation?.root ?? getPackageRoot();
|
|
182
232
|
}
|
|
183
233
|
|
|
184
234
|
/**
|
|
@@ -316,12 +366,16 @@ export async function switchToDev(devPath) {
|
|
|
316
366
|
*/
|
|
317
367
|
export async function switchToNext() {
|
|
318
368
|
const config = await loadConfig();
|
|
369
|
+
const status = assertCanonicalInstallation({ actualRoot: getPackageRoot() });
|
|
370
|
+
if (status.identity.method !== 'npm' || !status.identity.managerExecutable) {
|
|
371
|
+
throw new Error('The next channel requires a canonical npm installation with a recorded package-manager executable.');
|
|
372
|
+
}
|
|
319
373
|
|
|
320
374
|
console.log('Switching to next channel (alpha/beta/RC — latest pre-release)...');
|
|
321
375
|
console.log('');
|
|
322
376
|
|
|
323
377
|
try {
|
|
324
|
-
|
|
378
|
+
execFileSync(status.identity.managerExecutable, ['install', '--global', 'aiwg@next'], { stdio: 'inherit' });
|
|
325
379
|
} catch (error) {
|
|
326
380
|
console.error('Failed to install aiwg@next:', error.message);
|
|
327
381
|
console.error('Check that npm is available and you have write access to the global prefix.');
|
|
@@ -346,12 +400,16 @@ export async function switchToNext() {
|
|
|
346
400
|
*/
|
|
347
401
|
export async function switchToNightly() {
|
|
348
402
|
const config = await loadConfig();
|
|
403
|
+
const status = assertCanonicalInstallation({ actualRoot: getPackageRoot() });
|
|
404
|
+
if (status.identity.method !== 'npm' || !status.identity.managerExecutable) {
|
|
405
|
+
throw new Error('The nightly channel requires a canonical npm installation with a recorded package-manager executable.');
|
|
406
|
+
}
|
|
349
407
|
|
|
350
408
|
console.log('Switching to nightly channel (latest automated snapshot)...');
|
|
351
409
|
console.log('');
|
|
352
410
|
|
|
353
411
|
try {
|
|
354
|
-
|
|
412
|
+
execFileSync(status.identity.managerExecutable, ['install', '--global', 'aiwg@nightly'], { stdio: 'inherit' });
|
|
355
413
|
} catch (error) {
|
|
356
414
|
console.error('Failed to install aiwg@nightly:', error.message);
|
|
357
415
|
console.error('Check that npm is available and you have write access to the global prefix.');
|
|
@@ -376,17 +434,29 @@ export async function switchToNightly() {
|
|
|
376
434
|
export async function switchToStable() {
|
|
377
435
|
const config = await loadConfig();
|
|
378
436
|
|
|
379
|
-
console.log('Switching to stable channel
|
|
437
|
+
console.log('Switching to the stable channel...');
|
|
380
438
|
console.log('');
|
|
381
439
|
|
|
382
440
|
config.channel = 'stable';
|
|
383
441
|
config.devMode = false;
|
|
384
442
|
await saveConfig(config);
|
|
443
|
+
const actualRoot = getPackageRoot();
|
|
444
|
+
saveInstallationIdentity(createInstallationIdentity({
|
|
445
|
+
actualRoot,
|
|
446
|
+
root: actualRoot,
|
|
447
|
+
method: inferInstallationMethod(actualRoot),
|
|
448
|
+
runMode: 'normal',
|
|
449
|
+
channel: 'stable',
|
|
450
|
+
edgePath: config.edgePath,
|
|
451
|
+
lastUpdateCheck: config.lastUpdateCheck,
|
|
452
|
+
updateCheckInterval: config.updateCheckInterval,
|
|
453
|
+
checkOnStartup: config.checkOnStartup,
|
|
454
|
+
}));
|
|
385
455
|
|
|
386
456
|
console.log('Switched to stable channel.');
|
|
387
|
-
console.log('You are now using the
|
|
457
|
+
console.log('You are now using the canonical installed package.');
|
|
388
458
|
console.log('');
|
|
389
|
-
console.log('To update:
|
|
459
|
+
console.log('To update: aiwg refresh --channel latest');
|
|
390
460
|
console.log('To switch to edge: aiwg --use-main');
|
|
391
461
|
}
|
|
392
462
|
|
|
@@ -415,6 +485,7 @@ function normalizeRepoUrl(repository) {
|
|
|
415
485
|
export async function getVersionInfo() {
|
|
416
486
|
const config = await loadConfig();
|
|
417
487
|
const packageRoot = getPackageRoot();
|
|
488
|
+
const installation = inspectInstallation({ actualRoot: packageRoot, identity: config.installation });
|
|
418
489
|
|
|
419
490
|
// Read package.json version
|
|
420
491
|
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
@@ -441,6 +512,7 @@ export async function getVersionInfo() {
|
|
|
441
512
|
version,
|
|
442
513
|
channel,
|
|
443
514
|
packageRoot,
|
|
515
|
+
installation,
|
|
444
516
|
devMode: config.devMode || false,
|
|
445
517
|
// Public-facing URLs — single source of truth is package.json, so user-visible
|
|
446
518
|
// stamps/links never hardcode the internal build origin. The published package
|
|
@@ -480,7 +552,7 @@ export async function updateEdge() {
|
|
|
480
552
|
const config = await loadConfig();
|
|
481
553
|
|
|
482
554
|
if (config.channel !== 'edge') {
|
|
483
|
-
console.log('Not in edge channel. Use
|
|
555
|
+
console.log('Not in edge channel. Use `aiwg update` for the canonical installed channel.');
|
|
484
556
|
return;
|
|
485
557
|
}
|
|
486
558
|
|
|
@@ -27,8 +27,10 @@ export const PROVIDER_CONFIGS = {
|
|
|
27
27
|
},
|
|
28
28
|
codex: {
|
|
29
29
|
binary: 'codex',
|
|
30
|
-
// Codex
|
|
31
|
-
|
|
30
|
+
// Current Codex releases use the explicit bypass flag for unrestricted,
|
|
31
|
+
// non-interactive execution. Keep this centralized so every launcher maps
|
|
32
|
+
// AIWG's --dangerous option consistently.
|
|
33
|
+
dangerousFlag: '--dangerously-bypass-approvals-and-sandbox',
|
|
32
34
|
name: 'OpenAI Codex',
|
|
33
35
|
},
|
|
34
36
|
hermes: {
|
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { formatCockpitDoctor, runCockpitDoctor, } from '../../cockpit/doctor.js';
|
|
6
7
|
export const COCKPIT_PACKAGE_NAME = '@aiwg/cockpit';
|
|
7
8
|
export function cockpitHome() {
|
|
8
9
|
return process.env.AIWG_COCKPIT_HOME || path.join(homedir(), '.aiwg', 'cockpit', 'package');
|
|
@@ -22,6 +23,16 @@ async function coreVersion(frameworkRoot) {
|
|
|
22
23
|
function packageRoot(home = cockpitHome()) {
|
|
23
24
|
return path.join(home, 'node_modules', '@aiwg', 'cockpit');
|
|
24
25
|
}
|
|
26
|
+
function valueAfter(args, flag) {
|
|
27
|
+
const index = args.indexOf(flag);
|
|
28
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
29
|
+
}
|
|
30
|
+
function doctorFormat(args) {
|
|
31
|
+
if (args.includes('--json'))
|
|
32
|
+
return 'json';
|
|
33
|
+
const value = valueAfter(args, '--format');
|
|
34
|
+
return value === 'json' || value === 'markdown' ? value : 'text';
|
|
35
|
+
}
|
|
25
36
|
export async function resolveCockpitInstall(home = cockpitHome()) {
|
|
26
37
|
const root = packageRoot(home);
|
|
27
38
|
const pkg = await readJson(path.join(root, 'package.json'));
|
|
@@ -95,6 +106,36 @@ export const cockpitHandler = {
|
|
|
95
106
|
const install = await resolveCockpitInstall();
|
|
96
107
|
const version = await coreVersion(ctx.frameworkRoot);
|
|
97
108
|
const autoInstall = ctx.args.includes('--install') || ctx.args.includes('--yes') || ctx.args.includes('-y');
|
|
109
|
+
if (ctx.args[0] === 'doctor' || ctx.args.includes('--doctor')) {
|
|
110
|
+
const sourcePackageRoot = path.join(ctx.frameworkRoot, 'apps', 'cockpit');
|
|
111
|
+
const sourcePackage = !install.installed ? await readJson(path.join(sourcePackageRoot, 'package.json')) : null;
|
|
112
|
+
const doctorInstall = install.installed ? install : {
|
|
113
|
+
installed: Boolean(sourcePackage),
|
|
114
|
+
version: typeof sourcePackage?.version === 'string' ? sourcePackage.version : undefined,
|
|
115
|
+
packageRoot: sourcePackageRoot,
|
|
116
|
+
};
|
|
117
|
+
const topologyValue = valueAfter(ctx.args, '--topology');
|
|
118
|
+
const topology = topologyValue === 'ssh-local' || topologyValue === 'ssh-reverse'
|
|
119
|
+
? topologyValue
|
|
120
|
+
: 'same-host';
|
|
121
|
+
const report = await runCockpitDoctor({
|
|
122
|
+
coreVersion: version,
|
|
123
|
+
cockpitInstalled: doctorInstall.installed,
|
|
124
|
+
cockpitVersion: doctorInstall.version,
|
|
125
|
+
cockpitPackageRoot: doctorInstall.packageRoot,
|
|
126
|
+
topology,
|
|
127
|
+
cockpitHost: valueAfter(ctx.args, '--cockpit-host'),
|
|
128
|
+
executorHost: valueAfter(ctx.args, '--executor-host'),
|
|
129
|
+
expectedExecutorVersion: valueAfter(ctx.args, '--executor-version'),
|
|
130
|
+
forwardEndpoint: valueAfter(ctx.args, '--forward-endpoint'),
|
|
131
|
+
runtimeFile: valueAfter(ctx.args, '--runtime-file'),
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
exitCode: report.status === 'blocked' ? 1 : 0,
|
|
135
|
+
message: formatCockpitDoctor(report, doctorFormat(ctx.args)),
|
|
136
|
+
rawOutput: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
98
139
|
if (ctx.args.includes('--status')) {
|
|
99
140
|
return {
|
|
100
141
|
exitCode: 0,
|