@aiwg/cli 2026.7.25 → 2026.8.1

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 (79) hide show
  1. package/README.md +33 -0
  2. package/agentic/code/providers/capability-matrix.yaml +511 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +120 -0
  4. package/agentic/code/providers/model-catalog.v1.json +96 -0
  5. package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
  6. package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
  7. package/bin/aiwg.mjs +14 -10
  8. package/dist/src/api/index.d.ts +1 -0
  9. package/dist/src/api/index.js +1 -0
  10. package/dist/src/artifacts/cli.js +55 -10
  11. package/dist/src/artifacts/fortemi-shard-export.js +107 -18
  12. package/dist/src/artifacts/types.js +4 -0
  13. package/dist/src/auth/client.js +209 -0
  14. package/dist/src/auth/config.js +38 -0
  15. package/dist/src/auth/credential-store.js +141 -0
  16. package/dist/src/auth/resource-credentials.js +25 -0
  17. package/dist/src/auth/types.js +2 -0
  18. package/dist/src/channel/manager.mjs +5 -5
  19. package/dist/src/cli/handlers/auth.js +125 -0
  20. package/dist/src/cli/handlers/help.js +1 -0
  21. package/dist/src/cli/handlers/index.js +6 -2
  22. package/dist/src/cli/handlers/job.js +97 -0
  23. package/dist/src/cli/handlers/resource-versions.js +2 -0
  24. package/dist/src/cli/handlers/runtime-info.js +2 -2
  25. package/dist/src/cli/handlers/serve.js +2 -2
  26. package/dist/src/cli/handlers/sessions.js +211 -5
  27. package/dist/src/cli/handlers/steward.js +16 -3
  28. package/dist/src/cli/handlers/subcommands.js +10 -1
  29. package/dist/src/cli/handlers/use.js +342 -43
  30. package/dist/src/config/gitignore.js +1 -0
  31. package/dist/src/extensions/commands/definitions.js +49 -5
  32. package/dist/src/extensions/manifest.js +1 -0
  33. package/dist/src/features/catalog.js +3 -3
  34. package/dist/src/jobs/executor.js +83 -0
  35. package/dist/src/jobs/flow.js +106 -0
  36. package/dist/src/jobs/gitea.js +91 -0
  37. package/dist/src/jobs/render.js +53 -0
  38. package/dist/src/jobs/runner.js +315 -0
  39. package/dist/src/jobs/types.js +3 -0
  40. package/dist/src/memory/canonical-context.js +342 -0
  41. package/dist/src/memory/context-pack.js +282 -0
  42. package/dist/src/memory/index.js +4 -0
  43. package/dist/src/memory/intake.js +118 -0
  44. package/dist/src/providers/capability-matrix.js +11 -4
  45. package/dist/src/providers/capability-matrix.yaml +39 -42
  46. package/dist/src/resources/resolver.js +1 -0
  47. package/dist/src/resources/web-release.d.ts +3 -1
  48. package/dist/src/resources/web-release.js +14 -6
  49. package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
  50. package/dist/src/serve/fleet-mission-conductor.js +293 -0
  51. package/dist/src/sessions/analytics.js +303 -0
  52. package/dist/src/sessions/importer.js +7 -1
  53. package/dist/src/sessions/index.js +2 -0
  54. package/dist/src/sessions/output-registration.js +338 -0
  55. package/dist/src/sessions/policy.js +1 -1
  56. package/dist/src/sessions/promotion.js +73 -2
  57. package/dist/src/sessions/repository.js +215 -1
  58. package/dist/src/update/notifier.mjs +13 -2
  59. package/package.json +17 -10
  60. package/tools/_resolve-impl.mjs +74 -0
  61. package/tools/agents/deploy-agents.mjs +962 -0
  62. package/tools/agents/providers/base.mjs +2954 -0
  63. package/tools/agents/providers/claude.mjs +711 -0
  64. package/tools/agents/providers/codex.mjs +699 -0
  65. package/tools/agents/providers/copilot.mjs +659 -0
  66. package/tools/agents/providers/cursor.mjs +714 -0
  67. package/tools/agents/providers/factory.mjs +1130 -0
  68. package/tools/agents/providers/hermes.mjs +663 -0
  69. package/tools/agents/providers/hook-capabilities.mjs +85 -0
  70. package/tools/agents/providers/model-role.mjs +56 -0
  71. package/tools/agents/providers/openclaw-translator.mjs +348 -0
  72. package/tools/agents/providers/openclaw.mjs +680 -0
  73. package/tools/agents/providers/opencode.mjs +675 -0
  74. package/tools/agents/providers/openhuman.mjs +292 -0
  75. package/tools/agents/providers/warp.mjs +413 -0
  76. package/tools/agents/providers/windsurf.mjs +748 -0
  77. package/tools/commands/deploy-prompts-codex.mjs +336 -0
  78. package/tools/plugin/package-plugins.mjs +1013 -0
  79. package/tools/skills/deploy-skills-codex.mjs +571 -0
@@ -0,0 +1,97 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { CodexJobExecutor } from '../../jobs/executor.js';
4
+ import { loadJobFlow, resolveWorkspaceFile } from '../../jobs/flow.js';
5
+ import { GiteaWorkItemClient } from '../../jobs/gitea.js';
6
+ import { renderExternalTrigger } from '../../jobs/render.js';
7
+ import { runExternalJob } from '../../jobs/runner.js';
8
+ import { AiwgError, EXIT_CODES, handlerResultFromError } from '../errors.js';
9
+ function option(args, name) {
10
+ const index = args.indexOf(name);
11
+ return index >= 0 ? args[index + 1] : undefined;
12
+ }
13
+ function usage(message) {
14
+ throw new AiwgError({ code: 'ERR_USAGE_JOB', message, exitCode: EXIT_CODES.USAGE, hint: 'Run `aiwg job help` for usage.' });
15
+ }
16
+ function help() {
17
+ console.log(`AIWG external-trigger jobs
18
+
19
+ Usage:
20
+ aiwg job validate <flow>
21
+ aiwg job render-cron <flow> [--format cron|systemd|gitea-actions]
22
+ aiwg job run <flow> --once [--state-dir <absolute-path>] [--json]
23
+
24
+ The operating system or CI owns time. AIWG validates and executes one reviewed job.`);
25
+ }
26
+ async function validateFiles(flow) {
27
+ const workspace = await fs.realpath(flow.spec.executor.workspace);
28
+ if (workspace !== flow.spec.executor.workspace) {
29
+ throw new Error('executor.workspace must be canonical (no symlink or relative segments)');
30
+ }
31
+ const prompt = resolveWorkspaceFile(flow, flow.spec.executor.prompt);
32
+ const schema = resolveWorkspaceFile(flow, flow.spec.executor.resultSchema);
33
+ const [promptStat, schemaSource] = await Promise.all([fs.stat(prompt), fs.readFile(schema, 'utf8'), fs.access(flow.spec.executor.binary, constants.X_OK)]);
34
+ if (!promptStat.isFile())
35
+ throw new Error('executor.prompt must resolve to a regular file');
36
+ JSON.parse(schemaSource);
37
+ for (const root of flow.spec.security.approvedAttachmentRoots) {
38
+ const [canonical, stat] = await Promise.all([fs.realpath(root), fs.stat(root)]);
39
+ if (canonical !== root || !stat.isDirectory())
40
+ throw new Error('approvedAttachmentRoots must contain canonical directories');
41
+ }
42
+ }
43
+ async function execute(ctx) {
44
+ try {
45
+ const [subcommand = 'help', flowArg] = ctx.args;
46
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
47
+ help();
48
+ return { exitCode: 0 };
49
+ }
50
+ if (!['validate', 'render-cron', 'run'].includes(subcommand))
51
+ usage(`Unknown job subcommand: ${subcommand}`);
52
+ if (!flowArg || flowArg.startsWith('-'))
53
+ usage(`job ${subcommand} requires a flow file`);
54
+ const loaded = await loadJobFlow(flowArg, ctx.cwd);
55
+ await validateFiles(loaded.flow);
56
+ if (subcommand === 'validate') {
57
+ console.log(JSON.stringify({ valid: true, apiVersion: loaded.flow.apiVersion, kind: loaded.flow.kind, name: loaded.flow.metadata.name }));
58
+ return { exitCode: 0 };
59
+ }
60
+ if (subcommand === 'render-cron') {
61
+ const format = (option(ctx.args.slice(2), '--format') ?? 'cron');
62
+ if (!['cron', 'systemd', 'gitea-actions'].includes(format))
63
+ usage(`Unsupported scheduler format: ${format}`);
64
+ console.log(renderExternalTrigger(loaded.flow, loaded.file, format));
65
+ return { exitCode: 0 };
66
+ }
67
+ if (!ctx.args.includes('--once'))
68
+ usage('job run requires --once; AIWG does not own a resident scheduler');
69
+ const stateRoot = option(ctx.args.slice(2), '--state-dir');
70
+ if (stateRoot && !stateRoot.startsWith('/'))
71
+ usage('--state-dir must be absolute');
72
+ if (stateRoot && path.resolve(stateRoot) === path.parse(path.resolve(stateRoot)).root)
73
+ usage('--state-dir must not be a filesystem root');
74
+ const client = await GiteaWorkItemClient.create(loaded.flow);
75
+ const result = await runExternalJob({
76
+ flow: loaded.flow,
77
+ client,
78
+ executor: new CodexJobExecutor(),
79
+ ...(stateRoot ? { stateRoot } : {}),
80
+ signal: ctx.signal,
81
+ });
82
+ console.log(JSON.stringify(result, null, ctx.args.includes('--json') ? 2 : 0));
83
+ return { exitCode: result.status === 'failed-verification' ? EXIT_CODES.GENERAL : EXIT_CODES.OK };
84
+ }
85
+ catch (error) {
86
+ return handlerResultFromError(error);
87
+ }
88
+ }
89
+ export const jobHandler = {
90
+ id: 'job',
91
+ name: 'External Job',
92
+ description: 'Validate, render, or run one externally triggered provider job',
93
+ category: 'orchestration',
94
+ aliases: ['job'],
95
+ execute,
96
+ };
97
+ //# sourceMappingURL=job.js.map
@@ -3,6 +3,7 @@ import { loadResourceTrustRootFile, readVerifiedRegularFile, resolveWebRelease,
3
3
  import { getProjectDir } from "../../config/aiwg-config.js";
4
4
  import { cleanWebResourceCache } from "../../resources/cache-cleanup.js";
5
5
  import { writeWebResourceLock } from "../../resources/lockfile.js";
6
+ import { createResourceCredentialProvider } from "../../auth/resource-credentials.js";
6
7
  const MAX_RESOURCE_MANIFEST_BYTES = 4 * 1024 * 1024;
7
8
  const DEFAULT_CHANNELS = ["stable", "latest", "canary", "main"];
8
9
  function usage() {
@@ -82,6 +83,7 @@ function webReleaseOptionsFromEnvironment() {
82
83
  ? undefined
83
84
  : loadResourceTrustRootFile(path.resolve(trustRootFile));
84
85
  return {
86
+ credentialProvider: createResourceCredentialProvider(process.env),
85
87
  ...(baseUrl === undefined ? {} : { baseUrl }),
86
88
  ...(cacheRoot === undefined ? {} : { cacheRoot }),
87
89
  ...(publicKeyPem === undefined ? {} : { publicKeyPem }),
@@ -258,7 +258,7 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
258
258
  console.log(`Catalog: ${summary.catalogPath}`);
259
259
  // Scheduler backend detection
260
260
  const { execSync } = await import('child_process');
261
- let schedulerBackend = 'aiwg-cli (daemon)';
261
+ let schedulerBackend = 'external trigger required (system cron/systemd/CI)';
262
262
  let chronyInstalled = false;
263
263
  try {
264
264
  execSync('which chronyc 2>/dev/null || which chronyd 2>/dev/null', { stdio: 'pipe' });
@@ -273,7 +273,7 @@ async function handleRuntimeInfo(args, cwd = process.cwd()) {
273
273
  const isClaudeCodeContext = process.env.CLAUDE_CODE_VERSION !== undefined ||
274
274
  process.env.ANTHROPIC_API_KEY !== undefined;
275
275
  if (isClaudeCodeContext) {
276
- schedulerBackend = 'native-cron (CronCreate) / aiwg-cli fallback';
276
+ schedulerBackend = 'native-cron (CronCreate); external trigger outside agent sessions';
277
277
  }
278
278
  console.log(`\nScheduler:`);
279
279
  console.log(` Backend: ${schedulerBackend}`);
@@ -64,7 +64,7 @@ function parseServeArgs(args) {
64
64
  // ============================================================
65
65
  // WebSocket routing (#851)
66
66
  //
67
- // @hono/node-server v1.x does not export createNodeWebSocket.
67
+ // @hono/node-server does not export createNodeWebSocket.
68
68
  // We wire WebSocket routes directly via the Node.js HTTP server's
69
69
  // 'upgrade' event and the `ws` npm package instead.
70
70
  // ============================================================
@@ -550,7 +550,7 @@ export async function startServer(opts) {
550
550
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
551
551
  const app = new Hono();
552
552
  // WebSocket routes are handled via Node.js upgrade event below (see setupWebSockets).
553
- // @hono/node-server v1.x does not export createNodeWebSocket.
553
+ // @hono/node-server does not export createNodeWebSocket.
554
554
  // Health check
555
555
  app.get('/api/health', (c) => c.json({ status: 'ok', readOnly: opts.readOnly }));
556
556
  // Connection status — server health, PTY sessions, sandboxes, subsystem status (#887)
@@ -1,7 +1,22 @@
1
1
  import { existsSync, mkdirSync, realpathSync, statSync, } from 'node:fs';
2
2
  import { dirname, isAbsolute, resolve, } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
3
4
  import { CLAUDE_ADAPTER_VERSION, ClaudeSessionAdapter, CODEX_ADAPTER_VERSION, CodexSessionAdapter, COPILOT_ADAPTER_VERSION, CopilotSessionAdapter, CURSOR_ADAPTER_VERSION, CursorSessionAdapter, FACTORY_ADAPTER_VERSION, FactorySessionAdapter, HERMES_ADAPTER_VERSION, HermesSessionAdapter, OPENCODE_ADAPTER_VERSION, OpenCodeSessionAdapter, OPENCLAW_ADAPTER_VERSION, OpenClawSessionAdapter, OPENHUMAN_ADAPTER_VERSION, OpenHumanSessionAdapter, WARP_ADAPTER_VERSION, WarpSessionAdapter, DEVIN_DESKTOP_ADAPTER_VERSION, DevinDesktopSessionAdapter, CandidateExtractionService, GENERIC_ADAPTER_VERSION, GenericSessionInterchangeAdapter, IncrementalSessionImporter, ImportLeaseContentionError, FilesystemMemoryDestination, FilesystemPromotionDispositionCoordinator, MemoryPromotionGateway, SESSION_CONTRACT_VERSION, SESSION_PROVIDER_IDS, SessionContractError, SessionRepository, SessionSourceSchema, StructuralCandidateExtractor, resolveMemoryConsumerManifest, assertSessionProviderId, acquireImportLease, defaultDiscoveryManifestPath, discoverWorkspaceHistories, deriveSessionTimeline, importDiscoveryManifest, previewDiscoveryImport, publicDiscoveryManifest, readDiscoveryManifest, redactSourceLocator, sha256, parseTimelineGap, writeDiscoveryManifest, } from '../../sessions/index.js';
4
5
  const JSON_CONTRACT_VERSION = '1.0.0';
6
+ async function createLineMemoryPromotionDestination(projectRoot, manifestPath) {
7
+ const modulePath = resolve(dirname(manifestPath), 'commands', 'line-memory.mjs');
8
+ if (!existsSync(modulePath)) {
9
+ throw new SessionContractError('UNSUPPORTED_OPERATION', 'line-memory promotion adapter is missing from the installed addon');
10
+ }
11
+ const loaded = await import(pathToFileURL(modulePath).href);
12
+ if (!loaded.LineMemoryPromotionDestination) {
13
+ throw new SessionContractError('UNSUPPORTED_OPERATION', 'line-memory addon does not export its promotion adapter');
14
+ }
15
+ return new loaded.LineMemoryPromotionDestination({
16
+ projectRoot,
17
+ consumer: 'line-memory',
18
+ });
19
+ }
5
20
  const EXIT = {
6
21
  ok: 0, usage: 2, unsupported: 3, unavailable: 4, contract: 5, storage: 6,
7
22
  locked: 7, coverage: 8,
@@ -28,6 +43,8 @@ Commands:
28
43
  restore <session-id> Restore a reversible catalog tombstone
29
44
  purge <session-id> Preview terminal AIWG-copy purge
30
45
  audit --workspace <id> Read content-free mutation events
46
+ analytics <view> --workspace <id> Summary, tool-calls, escalations, or HITL
47
+ forensics <view> --workspace <id> Authorized timeline, indicators, or evidence
31
48
  doctor Check catalog availability and integrity
32
49
 
33
50
  Options:
@@ -219,6 +236,95 @@ async function executeCommand(ctx, args) {
219
236
  nextCursor: result.nextCursor },
220
237
  });
221
238
  }
239
+ case 'analytics': {
240
+ const view = requiredPositional(args, 0, 'analytics view');
241
+ const { workspaceId } = readAuthorizationContext(ctx, args, command, repository);
242
+ const query = analyticsQuery(args, workspaceId);
243
+ if (view === 'summary') {
244
+ return ok(command, {
245
+ view,
246
+ summary: repository.analyticsSummary(query),
247
+ });
248
+ }
249
+ const categories = analyticsCategories(view);
250
+ const items = repository.listAnalyticsFacts({ ...query, categories });
251
+ return ok(command, {
252
+ analyticsVersion: '1.0.0',
253
+ view,
254
+ items,
255
+ count: items.length,
256
+ groupBy: analyticsGrouping(items, args.values.get('--group-by')),
257
+ });
258
+ }
259
+ case 'forensics': {
260
+ if (!args.flags.has('--authorize-forensics')) {
261
+ throw new CliError('OPERATION_NOT_AUTHORIZED', 'forensic extraction requires --authorize-forensics for this invocation', EXIT.usage);
262
+ }
263
+ const view = requiredPositional(args, 0, 'forensics view');
264
+ const { workspaceId } = authorizationContext(ctx, args, command);
265
+ const query = analyticsQuery(args, workspaceId);
266
+ if (view === 'indicators') {
267
+ const items = repository.listAnalyticsFacts({
268
+ ...query,
269
+ categories: ['indicator'],
270
+ });
271
+ return ok(command, forensicOutput(view, items, args));
272
+ }
273
+ if (view === 'timeline') {
274
+ const target = requiredPositional(args, 1, 'session-id or query');
275
+ const direct = repository.getSession(target, workspaceId);
276
+ const sessionIds = direct
277
+ ? [target]
278
+ : [...new Set(repository.search({
279
+ query: target,
280
+ workspaceId,
281
+ limit: boundedInteger(args.values.get('--limit'), 50, 1, 500, '--limit'),
282
+ }).items.map((item) => item.sessionId))];
283
+ const items = sessionIds.flatMap((sessionId) => repository.listAnalyticsFacts({ ...query, sessionId }));
284
+ return ok(command, forensicOutput(view, items, args));
285
+ }
286
+ if (view === 'evidence') {
287
+ const id = requiredPositional(args, 1, 'event-id, fact-id, or candidate-id');
288
+ const evidence = repository.getAnalyticsEvidence(id, workspaceId);
289
+ if (evidence.fact && evidence.event) {
290
+ return ok(command, {
291
+ analyticsVersion: '1.0.0',
292
+ view,
293
+ fact: evidence.fact,
294
+ event: {
295
+ eventId: evidence.event.eventId,
296
+ sessionId: evidence.event.sessionId,
297
+ sourceId: evidence.event.sourceId,
298
+ importRunId: evidence.event.importRunId,
299
+ sequence: evidence.event.sequence,
300
+ kind: evidence.event.kind,
301
+ occurredAt: evidence.event.occurredAt,
302
+ sensitivity: evidence.event.sensitivity,
303
+ rawReference: evidence.event.rawReference,
304
+ digest: evidence.event.digest,
305
+ },
306
+ });
307
+ }
308
+ const candidate = repository.getCandidate(id, undefined, workspaceId);
309
+ if (!candidate) {
310
+ throw new CliError('EVIDENCE_NOT_FOUND', `authorized analytics evidence not found: ${id}`, EXIT.unavailable);
311
+ }
312
+ return ok(command, {
313
+ analyticsVersion: '1.0.0',
314
+ view,
315
+ candidate: {
316
+ candidateId: candidate.candidateId,
317
+ version: candidate.version,
318
+ type: candidate.type,
319
+ evidence: candidate.evidence.map(({ quote: _quote, ...citation }) => citation),
320
+ sensitivity: candidate.sensitivity,
321
+ security: candidate.security,
322
+ reviewState: candidate.reviewState,
323
+ },
324
+ });
325
+ }
326
+ throw new CliError('INVALID_ARGUMENT', `unknown forensics view: ${view}`, EXIT.usage);
327
+ }
222
328
  case 'extract': {
223
329
  const { workspaceId } = authorizationContext(ctx, args, command);
224
330
  const sessionId = args.positionals[0];
@@ -315,11 +421,14 @@ async function executeCommand(ctx, args) {
315
421
  const candidateId = requiredPositional(args, 0, 'candidate-id');
316
422
  const version = boundedInteger(requiredPositional(args, 1, 'version'), 1, 1, Number.MAX_SAFE_INTEGER, 'version');
317
423
  const consumer = requiredValue(args, '--consumer');
318
- const destination = new FilesystemMemoryDestination({
319
- projectRoot: ctx.cwd,
320
- consumer,
321
- manifestPath: resolveMemoryConsumerManifest(ctx.cwd, consumer),
322
- });
424
+ const manifestPath = resolveMemoryConsumerManifest(ctx.cwd, consumer);
425
+ const destination = consumer === 'line-memory'
426
+ ? await createLineMemoryPromotionDestination(ctx.cwd, manifestPath)
427
+ : new FilesystemMemoryDestination({
428
+ projectRoot: ctx.cwd,
429
+ consumer,
430
+ manifestPath,
431
+ });
323
432
  const scopedPromotionStore = {
324
433
  getCandidate: (id, candidateVersion) => repository.getCandidate(id, candidateVersion, workspaceId),
325
434
  getPromotionReceipt: (id, candidateVersion, namedConsumer) => repository.getCandidate(id, candidateVersion, workspaceId)
@@ -997,6 +1106,7 @@ function parseArgs(argv) {
997
1106
  '--manifest', '--provider-home', '--codex-root', '--lock-wait-ms', '--min-coverage', '--gap',
998
1107
  '--inactivity-threshold',
999
1108
  '--control-events',
1109
+ '--session', '--status', '--actor', '--group-by',
1000
1110
  ]);
1001
1111
  let command;
1002
1112
  for (let index = 0; index < argv.length; index += 1) {
@@ -1184,6 +1294,96 @@ function dependentAction(value) {
1184
1294
  }
1185
1295
  return value;
1186
1296
  }
1297
+ function analyticsQuery(args, workspaceId) {
1298
+ const providerInput = args.values.get('--provider');
1299
+ const statusInput = args.values.get('--status');
1300
+ return {
1301
+ workspaceId,
1302
+ provider: providerInput ? assertSessionProviderId(providerInput) : undefined,
1303
+ sessionId: args.values.get('--session'),
1304
+ dateFrom: args.values.get('--date-from'),
1305
+ dateTo: args.values.get('--date-to'),
1306
+ tool: args.values.get('--tool'),
1307
+ status: statusInput ? analyticsStatus(statusInput) : undefined,
1308
+ actor: args.values.get('--actor') ?? args.values.get('--participant'),
1309
+ tag: args.values.get('--tag'),
1310
+ sensitivity: args.values.get('--sensitivity'),
1311
+ extractionState: args.values.get('--extraction-state'),
1312
+ limit: boundedInteger(args.values.get('--limit'), 500, 1, 5_000, '--limit'),
1313
+ };
1314
+ }
1315
+ function analyticsStatus(value) {
1316
+ const allowed = new Set([
1317
+ 'requested', 'running', 'succeeded', 'failed', 'granted', 'denied',
1318
+ 'timed-out', 'unsupported', 'provider-unknown', 'observed',
1319
+ ]);
1320
+ if (!allowed.has(value)) {
1321
+ throw new CliError('INVALID_ARGUMENT', `invalid analytics status: ${value}`, EXIT.usage);
1322
+ }
1323
+ return value;
1324
+ }
1325
+ function analyticsCategories(view) {
1326
+ if (view === 'tool-calls')
1327
+ return ['tool-call', 'tool-result'];
1328
+ if (view === 'escalations')
1329
+ return ['escalation'];
1330
+ if (view === 'hitl')
1331
+ return ['hitl'];
1332
+ throw new CliError('INVALID_ARGUMENT', `unknown analytics view: ${view}`, EXIT.usage);
1333
+ }
1334
+ function analyticsGrouping(items, groupBy) {
1335
+ if (!groupBy)
1336
+ return null;
1337
+ if (!['tool', 'session', 'provider'].includes(groupBy)) {
1338
+ throw new CliError('INVALID_ARGUMENT', `invalid --group-by value: ${groupBy}`, EXIT.usage);
1339
+ }
1340
+ const counts = {};
1341
+ for (const item of items) {
1342
+ const value = groupBy === 'tool'
1343
+ ? item.toolName
1344
+ : groupBy === 'session'
1345
+ ? item.sessionId
1346
+ : item.provider;
1347
+ const key = typeof value === 'string' && value ? value : '<unknown>';
1348
+ counts[key] = (counts[key] ?? 0) + 1;
1349
+ }
1350
+ return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
1351
+ }
1352
+ function forensicOutput(view, items, args) {
1353
+ const output = {
1354
+ analyticsVersion: '1.0.0',
1355
+ view,
1356
+ items,
1357
+ count: items.length,
1358
+ authorization: {
1359
+ explicit: true,
1360
+ providerLogsModified: false,
1361
+ historicalContentExecuted: false,
1362
+ },
1363
+ };
1364
+ if (args.flags.has('--markdown')) {
1365
+ output.markdown = [
1366
+ `# Session Forensics ${view}`,
1367
+ '',
1368
+ `Facts: ${items.length}`,
1369
+ '',
1370
+ '| Time | Provider | Session | Category | Status | Evidence |',
1371
+ '|---|---|---|---|---|---|',
1372
+ ...items.map((item) => {
1373
+ const citation = item.sourceCitation;
1374
+ return [
1375
+ item.occurredAt ?? '<unknown>',
1376
+ item.provider ?? '<unknown>',
1377
+ item.sessionId ?? '<unknown>',
1378
+ item.category ?? '<unknown>',
1379
+ item.status ?? '<unknown>',
1380
+ citation?.eventId ?? item.eventId ?? '<unknown>',
1381
+ ].map((value) => String(value).replaceAll('|', '\\|')).join(' | ');
1382
+ }).map((row) => `| ${row} |`),
1383
+ ].join('\n');
1384
+ }
1385
+ return output;
1386
+ }
1187
1387
  function isDryRun(ctx, args) {
1188
1388
  return Boolean(ctx.dryRun || args.flags.has('--dry-run'));
1189
1389
  }
@@ -1202,6 +1402,12 @@ function emit(value) {
1202
1402
  function printHuman(value) {
1203
1403
  if (value.status === 'preview')
1204
1404
  console.log('Preview (no changes applied)');
1405
+ if (value.command === 'sessions.forensics' && value.data
1406
+ && typeof value.data === 'object' && 'markdown' in value.data
1407
+ && typeof value.data.markdown === 'string') {
1408
+ console.log(value.data.markdown);
1409
+ return;
1410
+ }
1205
1411
  if (value.command === 'sessions.timeline' && value.data
1206
1412
  && typeof value.data === 'object' && 'items' in value.data) {
1207
1413
  const data = value.data;
@@ -19,7 +19,7 @@
19
19
  import { access } from 'node:fs/promises';
20
20
  import { join } from 'node:path';
21
21
  import { AiwgError, EXIT_CODES, handlerResultFromError } from '../errors.js';
22
- import { loadCapabilityMatrix, } from '../../providers/capability-matrix.js';
22
+ import { loadCapabilityMatrix, isExternalStrategy, } from '../../providers/capability-matrix.js';
23
23
  import { getProjectDir, readAiwgConfig, writeAiwgConfig } from '../../config/aiwg-config.js';
24
24
  import { auditLegacyPermissions, archiveLegacyPermissionManifests, backupConfig, normalizeProjectPermissions, } from '../../policy/authorization.js';
25
25
  import { capabilityProviderId, normalizeProviderId, resolveActiveProvider } from '../provider-resolution.js';
@@ -48,6 +48,7 @@ async function detectProvider(ctx) {
48
48
  // ── Formatters ────────────────────────────────────────────────────────────────
49
49
  const NATIVE_MARK = '✓ native';
50
50
  const EMULATED_MARK = '~ emulated';
51
+ const EXTERNAL_MARK = '↗ external';
51
52
  const UNSUPPORTED_MARK = '- not supported';
52
53
  function emulationLabel(strategy) {
53
54
  if (!strategy)
@@ -70,7 +71,9 @@ function formatProvider(id, provider, matrix, meta) {
70
71
  for (const featureId of featureKeys) {
71
72
  const isNative = provider.native_features?.[featureId] === true;
72
73
  const emulation = provider.emulation?.[featureId] ?? null;
73
- const status = isNative ? NATIVE_MARK : (emulation ? EMULATED_MARK : UNSUPPORTED_MARK);
74
+ const status = isNative
75
+ ? NATIVE_MARK
76
+ : isExternalStrategy(emulation) ? EXTERNAL_MARK : (emulation ? EMULATED_MARK : UNSUPPORTED_MARK);
74
77
  const feat = matrix.features[featureId];
75
78
  lines.push(`\n ${featureId} — ${status}`);
76
79
  if (feat?.description)
@@ -79,6 +82,9 @@ function formatProvider(id, provider, matrix, meta) {
79
82
  if (feat?.native_example)
80
83
  lines.push(` example: ${feat.native_example}`);
81
84
  }
85
+ else if (isExternalStrategy(emulation)) {
86
+ lines.push(` trigger: system cron, systemd timer, or CI; AIWG does not own the clock`);
87
+ }
82
88
  else if (emulation) {
83
89
  lines.push(` fallback: ${emulationLabel(emulation)}`);
84
90
  }
@@ -319,7 +325,9 @@ async function handleSteward(args, ctx) {
319
325
  const emulation = provider.emulation?.[featureId] ?? null;
320
326
  const status = isNative
321
327
  ? `✓ native`
322
- : (emulation ? `~ emulated (${emulationLabel(emulation)})` : `- not supported`);
328
+ : isExternalStrategy(emulation)
329
+ ? `↗ external trigger (system cron/systemd/CI)`
330
+ : (emulation ? `~ emulated (${emulationLabel(emulation)})` : `- not supported`);
323
331
  console.log(` ${provider.display_name.padEnd(20)} (${providerId.padEnd(12)}) ${status}`);
324
332
  }
325
333
  console.log('');
@@ -386,6 +394,11 @@ async function handleSteward(args, ctx) {
386
394
  if (feat.native_example)
387
395
  console.log(` Example: ${feat.native_example}`);
388
396
  }
397
+ else if (isExternalStrategy(emulation)) {
398
+ console.log(` ↗ Use an external trigger`);
399
+ console.log(` Strategy: system cron, systemd timer, or CI launches a reviewed provider command`);
400
+ console.log(` Boundary: AIWG does not provide or own the clock/scheduler`);
401
+ }
389
402
  else if (emulation) {
390
403
  console.log(` ~ Use AIWG emulation`);
391
404
  console.log(` Strategy: ${emulationLabel(emulation)}`);
@@ -1147,9 +1147,18 @@ export const packagePluginHandler = {
1147
1147
  message: "Error: plugin name is required.\n\nRun `aiwg package-plugin --help` for usage.",
1148
1148
  };
1149
1149
  }
1150
+ const positionalSource = positional && (positional.includes('/') || positional.includes('\\'))
1151
+ ? positional
1152
+ : undefined;
1150
1153
  const normalizedArgs = hasExplicitPlugin
1151
1154
  ? ctx.args
1152
- : ["--plugin", positional, ...ctx.args.slice(1)];
1155
+ : positionalSource
1156
+ ? [
1157
+ "--plugin", path.basename(path.resolve(ctx.cwd, positionalSource)),
1158
+ "--source", positionalSource,
1159
+ ...ctx.args.slice(1),
1160
+ ]
1161
+ : ["--plugin", positional, ...ctx.args.slice(1)];
1153
1162
  const frameworkRoot = await getFrameworkRoot();
1154
1163
  const runner = createScriptRunner(frameworkRoot);
1155
1164
  return runner.run("tools/plugin/package-plugins.mjs", normalizedArgs, {