@aiwg/cli 2026.9.9 → 2026.9.10

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 (36) hide show
  1. package/bin/aiwg.mjs +55 -10
  2. package/dist/src/artifacts/cli.js +20 -8
  3. package/dist/src/artifacts/corpus-views/taxonomies.js +1 -1
  4. package/dist/src/cli/handlers/help.js +6 -5
  5. package/dist/src/cli/handlers/init.js +7 -2
  6. package/dist/src/cli/handlers/ralph.js +8 -8
  7. package/dist/src/cli/handlers/steward.js +9 -1
  8. package/dist/src/cli/handlers/use.js +4 -0
  9. package/dist/src/cli/handlers/version.js +13 -4
  10. package/dist/src/cli/services/deployment-verification.js +32 -4
  11. package/dist/src/cli/watch-service.js +43 -3
  12. package/dist/src/dataset/fortemi-capability.d.ts +70 -0
  13. package/dist/src/dataset/fortemi-capability.js +211 -0
  14. package/dist/src/dataset/fortemi-dataset-execution.d.ts +9 -0
  15. package/dist/src/dataset/fortemi-dataset-execution.js +49 -8
  16. package/dist/src/dataset/fortemi-run-receipt.d.ts +1 -0
  17. package/dist/src/dataset/fortemi-run-receipt.js +2 -19
  18. package/dist/src/dataset/fortemi-schema-path.d.ts +7 -0
  19. package/dist/src/dataset/fortemi-schema-path.js +24 -0
  20. package/dist/src/dataset/index.d.ts +1 -0
  21. package/dist/src/dataset/index.js +1 -0
  22. package/dist/src/extensions/project-local-doctor.js +15 -1
  23. package/dist/src/lint/cli.js +5 -1
  24. package/dist/src/lint/reporters.js +17 -0
  25. package/dist/src/lint/runner.js +160 -5
  26. package/dist/src/policy/authorization.js +1 -1
  27. package/dist/src/smiths/context-pipeline/workspace-context.js +102 -63
  28. package/package.json +1 -1
  29. package/schemas/dataset/fortemi-capability-validation/1.0.1/authority.json +61 -0
  30. package/schemas/dataset/fortemi-capability-validation/1.0.1/capability.schema.json +469 -0
  31. package/schemas/security/threat-assessment-report.v1.schema.json +2 -1
  32. package/tools/agents/deploy-agents.mjs +4 -0
  33. package/tools/agents/providers/base.mjs +160 -12
  34. package/tools/agents/providers/claude.mjs +61 -4
  35. package/tools/agents/providers/codex.mjs +9 -0
  36. package/tools/skills/deploy-skills-codex.mjs +121 -7
package/bin/aiwg.mjs CHANGED
@@ -166,11 +166,12 @@ const FAST_HELP_TEXT = `
166
166
  PROMOTE
167
167
  promote <name> Graduate project-local bundle to upstream (--to upstream|corpus, --dry-run, --cleanup)
168
168
 
169
- RALPH LOOP
170
- ralph "<task>" Execute iterative task loop (--completion, --max-iterations)
171
- ralph-status Check current loop status
172
- ralph-abort Abort running loop
173
- ralph-resume Resume interrupted loop
169
+ AGENT LOOP
170
+ agent-loop "<task>" Execute iterative task loop (--completion, --max-iterations)
171
+ agent-loop-status Check current loop status
172
+ agent-loop-abort Abort running loop
173
+ agent-loop-resume Resume interrupted loop
174
+ (legacy ralph* names remain accepted as aliases)
174
175
 
175
176
  MAINTENANCE
176
177
  doctor Check installation health
@@ -382,6 +383,29 @@ async function applyVerbosityFromArgs(args, routerPath) {
382
383
  return level;
383
384
  }
384
385
 
386
+ /**
387
+ * Commands that only read state and therefore stay available while the
388
+ * recorded installation identity disagrees with the executing checkout
389
+ * (#2559). Everything else fails closed until the operator adopts or
390
+ * switches the identity. `index` is limited to its inspection subcommands;
391
+ * `index build` rewrites the capability index and is treated as a write.
392
+ */
393
+ const RECOVERY_SAFE_COMMANDS = new Set([
394
+ 'version', 'status', 'doctor', 'runtime-info', 'discover', 'show', 'help',
395
+ ]);
396
+ const RECOVERY_SAFE_INDEX_SUBCOMMANDS = new Set(['query', 'deps', 'stats']);
397
+
398
+ function isRecoverySafeCommand(args) {
399
+ const command = args.find((arg) => !arg.startsWith('-'));
400
+ if (!command) return false;
401
+ if (RECOVERY_SAFE_COMMANDS.has(command)) return true;
402
+ if (command === 'index') {
403
+ const sub = args.slice(args.indexOf(command) + 1).find((arg) => !arg.startsWith('-'));
404
+ return sub !== undefined && RECOVERY_SAFE_INDEX_SUBCOMMANDS.has(sub);
405
+ }
406
+ return false;
407
+ }
408
+
385
409
  async function main() {
386
410
  const args = process.argv.slice(2);
387
411
 
@@ -423,11 +447,32 @@ async function main() {
423
447
  }
424
448
  const { assertCanonicalInstallation } = await import(pathToFileURL(identityPath).href);
425
449
  const readOnlyPreview = args.includes('--dry-run');
426
- assertCanonicalInstallation({
427
- actualRoot: activePackageRoot,
428
- createIfMissing: !readOnlyPreview,
429
- allowUnrecorded: readOnlyPreview,
430
- });
450
+ try {
451
+ assertCanonicalInstallation({
452
+ actualRoot: activePackageRoot,
453
+ createIfMissing: !readOnlyPreview,
454
+ allowUnrecorded: readOnlyPreview,
455
+ });
456
+ } catch (error) {
457
+ if (error?.code !== 'AIWG_INSTALLATION_DRIFT' || !isRecoverySafeCommand(args)) throw error;
458
+ // Read-only diagnostics stay reachable under drift so the documented
459
+ // recovery ladder (status → doctor → runtime-info → version → local
460
+ // discover) can run before the identity is repaired. Every mutating
461
+ // command still fails closed above (#2559).
462
+ const drift = Array.isArray(error.status?.drift) ? error.status.drift : [];
463
+ process.env['AIWG_INSTALLATION_DRIFT'] = JSON.stringify({
464
+ state: error.status?.state ?? 'mismatch',
465
+ drift,
466
+ });
467
+ process.stderr.write(
468
+ `aiwg: warning: installation identity drift detected; \`${args[0]}\` is read-only and continues, ` +
469
+ 'but update, refresh, and deployment stay blocked until the identity is repaired.\n' +
470
+ drift.map((item) => `- ${item}\n`).join('') +
471
+ 'Inspect: aiwg installation show\n' +
472
+ 'Adopt this installation: aiwg installation adopt\n' +
473
+ 'Switch deliberately: aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]\n',
474
+ );
475
+ }
431
476
  }
432
477
 
433
478
  // Wire up the logger level from -v/-vv/--quiet/AIWG_LOG_LEVEL before any
@@ -1374,6 +1374,10 @@ async function handleSetQuery(args) {
1374
1374
  backend,
1375
1375
  });
1376
1376
  }
1377
+ /** `index discover` flags that consume the following argument as a value. */
1378
+ const DISCOVER_VALUE_FLAGS = new Set([
1379
+ '--type', '--limit', '--format', '--graph', '--backend', '--resource-source', '--aiwg-version',
1380
+ ]);
1377
1381
  /**
1378
1382
  * Handle 'index discover' command — capability-search for AIWG operational
1379
1383
  * assets.
@@ -1391,17 +1395,25 @@ async function handleSetQuery(args) {
1391
1395
  async function handleDiscover(args) {
1392
1396
  const { discoverCapability } = await import('./query-engine.js');
1393
1397
  const cwd = process.cwd();
1394
- // Parse positional phrase (everything before flags)
1398
+ // Parse the positional phrase. Flags may appear before or after it — the
1399
+ // documented local-backend fallback is written flag-first
1400
+ // (`aiwg discover --backend local "<phrase>"`, #2530), so a phrase after a
1401
+ // value flag must not be swallowed as that flag's argument (#2559).
1395
1402
  const textParts = [];
1396
1403
  const flags = [];
1397
- let inFlags = false;
1398
- for (const arg of args) {
1399
- if (arg.startsWith('--'))
1400
- inFlags = true;
1401
- if (inFlags)
1402
- flags.push(arg);
1403
- else
1404
+ for (let index = 0; index < args.length; index += 1) {
1405
+ const arg = args[index];
1406
+ if (!arg.startsWith('--')) {
1404
1407
  textParts.push(arg);
1408
+ continue;
1409
+ }
1410
+ flags.push(arg);
1411
+ const bare = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
1412
+ const next = args[index + 1];
1413
+ if (DISCOVER_VALUE_FLAGS.has(bare) && !arg.includes('=') && next !== undefined && !next.startsWith('--')) {
1414
+ flags.push(next);
1415
+ index += 1;
1416
+ }
1405
1417
  }
1406
1418
  const phrase = textParts.join(' ').trim();
1407
1419
  if (!phrase) {
@@ -110,7 +110,7 @@ export const PIPELINE_STAGES = [
110
110
  ['4. Mixture-of-Experts & Sparse Architectures', 'Sparsely activated models as a parallel pretraining strategy.', ['REF-007']],
111
111
  ['5. Instruction Tuning & SFT', 'Supervised fine-tuning on instructions to convert base models into assistants.', ['REF-055', 'REF-435', 'REF-470']],
112
112
  ['6. RLHF, DPO, and Preference Optimization', 'Aligning models with human and AI feedback.', ['REF-025', 'REF-055', 'REF-449']],
113
- ['7. RLVR & Reasoning RL', 'Verifiable-reward RL for reasoning, math, and code.', ['REF-819', 'REF-957', 'REF-457']],
113
+ ['7. RLVR & Reasoning RL', 'Verifiable-reward RL for reasoning, math, and code.', ['REF-394', 'REF-957', 'REF-457']],
114
114
  ['8. PEFT / LoRA / Adapter-Based Adaptation', 'Parameter-efficient fine-tuning when full SFT is impractical.', ['REF-049']],
115
115
  ['9. Quantization, Pruning & Compression', 'Shrinking trained models for inference deployment.', ['REF-051']],
116
116
  ['10. Inference Serving & Optimization', 'Productionizing trained models: batching, KV cache, throughput.', ['REF-101', 'REF-102', 'REF-312', 'REF-313']],
@@ -134,11 +134,12 @@ function displayHelp() {
134
134
  helpGroup('PROMOTE', [
135
135
  ['promote <name>', 'Graduate project-local bundle to upstream (--to upstream|corpus, --dry-run, --cleanup)'],
136
136
  ]);
137
- helpGroup('RALPH LOOP', [
138
- ['ralph "<task>"', 'Execute iterative task loop (--completion, --max-iterations)'],
139
- ['ralph-status', 'Check current loop status'],
140
- ['ralph-abort', 'Abort running loop'],
141
- ['ralph-resume', 'Resume interrupted loop'],
137
+ helpGroup('AGENT LOOP', [
138
+ ['agent-loop "<task>"', 'Execute iterative task loop (--completion, --max-iterations)'],
139
+ ['agent-loop-status', 'Check current loop status'],
140
+ ['agent-loop-abort', 'Abort running loop'],
141
+ ['agent-loop-resume', 'Resume interrupted loop'],
142
+ ['', 'Legacy ralph* names remain accepted as aliases'],
142
143
  ]);
143
144
  helpGroup('MAINTENANCE', [
144
145
  ['doctor', 'Check installation health'],
@@ -17,6 +17,7 @@ import readline from 'readline';
17
17
  import { readAiwgConfig, writeAiwgConfig, emptyConfig, populateDeployedTo, VALID_PROVIDERS, getConfigPath, } from '../../config/aiwg-config.js';
18
18
  import * as ui from '../ui.js';
19
19
  import { askString as sharedAskString, askYesNo as sharedAskYesNo } from '../prompt-utils.js';
20
+ import { normalizeProviderDefinitionId } from '../../providers/provider-definitions.js';
20
21
  import { writeNormalizedAiwgMd } from '../../smiths/context-pipeline/finalization.js';
21
22
  import { ensureWorkspaceContext } from '../../smiths/context-pipeline/workspace-context.js';
22
23
  const PROVIDER_LABELS = {
@@ -61,9 +62,13 @@ async function askProviders(rl, signal) {
61
62
  const num = parseInt(part, 10);
62
63
  if (!isNaN(num) && num >= 1 && num <= VALID_PROVIDERS.length) {
63
64
  selected.push(VALID_PROVIDERS[num - 1]);
65
+ continue;
64
66
  }
65
- else if (VALID_PROVIDERS.includes(part)) {
66
- selected.push(part);
67
+ // Accept canonical ids and registered aliases (e.g. `dsh` for
68
+ // `deepseek-harness`) so the picker honours the same names as `--provider`.
69
+ const canonical = normalizeProviderDefinitionId(part);
70
+ if (canonical && VALID_PROVIDERS.includes(canonical)) {
71
+ selected.push(canonical);
67
72
  }
68
73
  else {
69
74
  ui.warn(` Unknown provider '${part}' — skipped`);
@@ -156,7 +156,7 @@ export class RalphHandler {
156
156
  name = 'Agent Loop';
157
157
  description = 'Execute iterative task loop with automatic completion detection';
158
158
  category = 'ralph';
159
- aliases = ['ralph', '-ralph', '--ralph'];
159
+ aliases = ['agent-loop', '--agent-loop', 'ralph', '-ralph', '--ralph'];
160
160
  async execute(ctx) {
161
161
  const parsed = parseRalphArgs(ctx.args);
162
162
  // Show help
@@ -334,7 +334,7 @@ export class RalphStatusHandler {
334
334
  name = 'Ralph Status';
335
335
  description = 'Show Agent loop status and iteration history';
336
336
  category = 'ralph';
337
- aliases = ['ralph-status'];
337
+ aliases = ['agent-loop-status', 'ralph-status'];
338
338
  async execute(ctx) {
339
339
  const showAll = ctx.args.includes('--all') || ctx.args.includes('-a');
340
340
  const statuses = getLoopStatuses(process.cwd());
@@ -402,7 +402,7 @@ export class RalphAbortHandler {
402
402
  name = 'Ralph Abort';
403
403
  description = 'Abort currently running Agent loop';
404
404
  category = 'ralph';
405
- aliases = ['ralph-abort'];
405
+ aliases = ['agent-loop-abort', 'ralph-abort'];
406
406
  async execute(ctx) {
407
407
  // Parse --loop-id if provided
408
408
  let loopId;
@@ -428,7 +428,7 @@ export class RalphResumeHandler {
428
428
  name = 'Ralph Resume';
429
429
  description = 'Resume previously aborted Agent loop';
430
430
  category = 'ralph';
431
- aliases = ['ralph-resume'];
431
+ aliases = ['agent-loop-resume', 'ralph-resume'];
432
432
  async execute(ctx) {
433
433
  // Parse arguments
434
434
  let loopId;
@@ -493,7 +493,7 @@ export const ralphExternalHandler = {
493
493
  name: 'Agent Loop External',
494
494
  description: 'Crash-resilient external loop with state persistence and CI/CD integration',
495
495
  category: 'ralph',
496
- aliases: ['ralph-external', '--ralph-external', '--agent-loop-ext'],
496
+ aliases: ['agent-loop-ext', '--agent-loop-ext', 'ralph-external', '--ralph-external'],
497
497
  async execute(ctx) {
498
498
  const runner = createScriptRunner(ctx.frameworkRoot);
499
499
  // Delegate to the external Ralph supervisor script
@@ -510,7 +510,7 @@ export const ralphMemoryHandler = {
510
510
  name: 'Ralph Memory',
511
511
  description: 'Manage Ralph semantic memory entries (list, query, clear)',
512
512
  category: 'ralph',
513
- aliases: ['--ralph-memory'],
513
+ aliases: ['--agent-loop-memory', '--ralph-memory'],
514
514
  async execute(ctx) {
515
515
  const runner = createScriptRunner(ctx.frameworkRoot);
516
516
  return runner.run('tools/ralph-external/memory-manager.mjs', ['--cli', ...ctx.args]);
@@ -526,7 +526,7 @@ export const ralphConfigHandler = {
526
526
  name: 'Ralph Config',
527
527
  description: 'View and configure Agent loop settings (show, set, reset, preset)',
528
528
  category: 'ralph',
529
- aliases: ['--ralph-config'],
529
+ aliases: ['--agent-loop-config', '--ralph-config'],
530
530
  async execute(ctx) {
531
531
  const runner = createScriptRunner(ctx.frameworkRoot);
532
532
  return runner.run('tools/ralph-external/orchestrator.mjs', ['--config', ...ctx.args]);
@@ -543,7 +543,7 @@ export class RalphAttachHandler {
543
543
  name = 'Ralph Attach';
544
544
  description = 'Attach to a running Agent loop\'s live output stream';
545
545
  category = 'ralph';
546
- aliases = ['ralph-attach'];
546
+ aliases = ['agent-loop-attach', 'ralph-attach'];
547
547
  async execute(ctx) {
548
548
  let loopId;
549
549
  const loopIdIndex = ctx.args.indexOf('--loop-id');
@@ -276,7 +276,15 @@ async function handleSteward(args, ctx) {
276
276
  return;
277
277
  }
278
278
  const diagnostics = await auditLegacyPermissions(projectDir, config);
279
- console.log(` ${dryRun ? 'Would normalize' : 'Normalizing'} ${diagnostics.filter(d => d.code.startsWith('legacy-')).length} legacy permission source(s).`);
279
+ const legacyCount = diagnostics.filter(d => d.code.startsWith('legacy-')).length;
280
+ if (legacyCount === 0 && !config.authorization) {
281
+ // Nothing legacy to convert: the only work is writing the initial
282
+ // default-deny block, which is what clears doctor's warning (#2563).
283
+ console.log(` ${dryRun ? 'Would write' : 'Writing'} an initial default-deny authorization block (no legacy permission sources found).`);
284
+ }
285
+ else {
286
+ console.log(` ${dryRun ? 'Would normalize' : 'Normalizing'} ${legacyCount} legacy permission source(s).`);
287
+ }
280
288
  console.log(` Result: ${Object.keys(normalized.authorization?.permissions ?? {}).length} permissions, ${Object.keys(normalized.authorization?.roles ?? {}).length} roles, ${normalized.authorization?.assignments.length ?? 0} assignments; default deny.`);
281
289
  if (apply) {
282
290
  const backup = await backupConfig(projectDir);
@@ -1256,6 +1256,10 @@ async function deployOneProjectLocalBundle(opts) {
1256
1256
  // never reach <provider>/.aiwg/skills/, leaving them invisible to
1257
1257
  // both the platform and the index.
1258
1258
  '--copy-all',
1259
+ // That copy-all is the deployer's doing, not the operator's, so the
1260
+ // provider's startup listing cap still applies; overflow lands on the
1261
+ // standard tier instead of over the cap (#2561).
1262
+ '--listing-budget',
1259
1263
  // Provenance for the managed marker (#2502). Without this the deployer
1260
1264
  // stamps `bundled`/`unknown`, and `aiwg refresh`'s stale-artifact prune —
1261
1265
  // whose desired set is the packaged framework corpus — deletes every
@@ -36,11 +36,20 @@ export const versionHandler = {
36
36
  * describing, and names the command that explains the rest (#2529).
37
37
  */
38
38
  function printDrift(fp) {
39
- if (!fp.drift)
39
+ if (fp.drift) {
40
+ const method = fp.installation?.identity?.method ?? 'unrecorded';
41
+ const declared = fp.drift.canonicalVersion ? ` (${fp.drift.canonicalVersion})` : '';
42
+ ui.dim(` ! canonical install declares ${method} at ${fp.drift.canonicalRoot}${declared} — run \`aiwg installation show\``);
40
43
  return;
41
- const method = fp.installation?.identity?.method ?? 'unrecorded';
42
- const declared = fp.drift.canonicalVersion ? ` (${fp.drift.canonicalVersion})` : '';
43
- ui.dim(` ! canonical install declares ${method} at ${fp.drift.canonicalRoot}${declared} — run \`aiwg installation show\``);
44
+ }
45
+ // Same root, different method (a source checkout recorded as npm, or the
46
+ // reverse) is drift too; `version` is the first recovery command an operator
47
+ // reaches for, so it must say so instead of looking aligned (#2559).
48
+ const state = fp.installation?.state;
49
+ if (state && state !== 'aligned' && state !== 'unrecorded') {
50
+ const reasons = Array.isArray(fp.installation?.drift) ? fp.installation.drift : [];
51
+ ui.dim(` ! installation identity ${state}: ${reasons.join('; ') || 'see aiwg installation show'} — run \`aiwg installation show\``);
52
+ }
44
53
  }
45
54
  function collectFingerprint(versionInfo) {
46
55
  const loggerInfo = getLoggerInfo();
@@ -3,6 +3,8 @@ import path from 'node:path';
3
3
  import { loadGraphIndexFile } from '../../artifacts/index-reader.js';
4
4
  import { readAiwgConfig } from '../../config/aiwg-config.js';
5
5
  import { readUserRegistry } from '../../config/user-registry.js';
6
+ import { inspectInstallation } from '../../installation/manager.mjs';
7
+ import { getPackageRoot } from '../../channel/manager.mjs';
6
8
  import { getProviderDefinition, normalizeProviderDefinitionId, resolveProviderPathValue, } from '../../providers/provider-definitions.js';
7
9
  import { diagnoseIntegratedProviderTransformationReceipt } from '../../providers/transformation-receipt-integration.js';
8
10
  import { diagnoseWorkspaceContext, providerContextContract, } from '../../smiths/context-pipeline/workspace-context.js';
@@ -379,12 +381,17 @@ export async function verifyProviderDeployment(options) {
379
381
  counts[flatKind] = tally.deployed;
380
382
  if (tally.unmanaged.length === 0)
381
383
  continue;
382
- const shown = tally.unmanaged.slice(0, 3).join(', ');
383
- const remainder = tally.unmanaged.length - 3;
384
+ // Name enough of the set that the operator can judge it, and make the
385
+ // overwrite previewable: `--force` replaces every listed file, so it is
386
+ // never the answer to an unrelated budget or listing warning (#2561).
387
+ const shown = tally.unmanaged.slice(0, 10).join(', ');
388
+ const remainder = tally.unmanaged.length - 10;
389
+ const bundle = options.requestedBundles[0] ?? 'all';
384
390
  findings.push(finding(normalized, `unmanaged-artifacts:${flatKind}`, 'advisory', `${tally.unmanaged.length} unmanaged ${FLAT_ARTIFACT_NOUNS[flatKind]} file(s) left in place at ${artifactPaths[flatKind]}: `
385
391
  + `${shown}${remainder > 0 ? `, and ${remainder} more` : ''}. `
386
- + 'They are not managed by AIWG and were not counted as deployed.', `Re-run aiwg use ${options.requestedBundles[0] ?? 'all'} --provider ${normalized} --force to replace them, `
387
- + `or delete ${artifactPaths[flatKind]} so AIWG can reclaim the directory.`, { kind: flatKind, unmanaged: tally.unmanaged }));
392
+ + 'They are not managed by AIWG and were not counted as deployed.', `Preview the overwrite set with aiwg use ${bundle} --provider ${normalized} --force --dry-run; `
393
+ + `then re-run aiwg use ${bundle} --provider ${normalized} --force to replace exactly those ${tally.unmanaged.length} file(s), `
394
+ + `or delete ${artifactPaths[flatKind]} so AIWG can reclaim the directory. Leave them in place if they are yours.`, { kind: flatKind, unmanaged: tally.unmanaged }));
388
395
  }
389
396
  const resolvedSkillsPath = resolveProviderPathValue(artifactPaths.skills, deploymentRoot);
390
397
  const kernelPath = options.scope === 'user'
@@ -645,6 +652,26 @@ export async function verifyConfiguredDeployments(projectRoot, filters = {}, fra
645
652
  }
646
653
  return aggregateUseDeploymentResult({ projectRoot, frameworkRoot, scope: filters.scope ?? 'project', requestedBundles: bundles, providers: results });
647
654
  }
655
+ /**
656
+ * Installation identity as the probe sees it. Read-only commands keep running
657
+ * under recorded/actual drift (#2559); the probe names that drift so an agent
658
+ * reading only the JSON does not treat a blocked installation as healthy.
659
+ */
660
+ function probeInstallationIdentity() {
661
+ try {
662
+ const status = inspectInstallation({ actualRoot: getPackageRoot(), createIfMissing: false });
663
+ return {
664
+ state: status.state,
665
+ canonical: status.identity ? { method: status.identity.method, root: status.identity.root } : null,
666
+ actual: { method: status.actualMethod, root: status.actualRoot },
667
+ drift: status.drift,
668
+ mutations_blocked: status.state !== 'aligned' && status.state !== 'unrecorded',
669
+ };
670
+ }
671
+ catch (error) {
672
+ return { state: 'unknown', drift: [error instanceof Error ? error.message : String(error)], mutations_blocked: true };
673
+ }
674
+ }
648
675
  export async function buildDeploymentStatusProbe(projectRoot, frameworkRoot = process.env.AIWG_ROOT || projectRoot, filters = {}) {
649
676
  const result = await verifyConfiguredDeployments(projectRoot, filters, frameworkRoot);
650
677
  const notConfigured = result.requestedBundles.length === 0
@@ -656,6 +683,7 @@ export async function buildDeploymentStatusProbe(projectRoot, frameworkRoot = pr
656
683
  generated_at: result.generatedAt,
657
684
  project_root: result.projectRoot,
658
685
  engaged,
686
+ installation: probeInstallationIdentity(),
659
687
  status: notConfigured ? 'not-configured' : result.outcome === 'failed' ? 'needs-repair' : result.outcome,
660
688
  checks: {
661
689
  workspace_exists: await exists(path.join(projectRoot, '.aiwg')),
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import chokidar from 'chokidar';
8
8
  import * as path from 'path';
9
+ const MAX_RECORDED_ERRORS = 20;
9
10
  /**
10
11
  * File watching service with debouncing
11
12
  */
@@ -16,6 +17,9 @@ export class WatchService {
16
17
  debounceMs = 500;
17
18
  stats;
18
19
  isRunning = false;
20
+ readyAt;
21
+ missingTargets = [];
22
+ recordedErrors = [];
19
23
  constructor() {
20
24
  this.stats = {
21
25
  filesWatched: 0,
@@ -48,13 +52,20 @@ export class WatchService {
48
52
  this.watcher.on('unlink', (path) => this.handleEvent('unlink', path));
49
53
  this.watcher.on('error', (error) => {
50
54
  this.stats.errors++;
55
+ this.recordError(error);
51
56
  console.error('Watch error:', error);
52
57
  });
53
58
  this.isRunning = true;
59
+ this.readyAt = undefined;
60
+ this.missingTargets = [];
61
+ this.recordedErrors = [];
54
62
  // Wait for ready
55
63
  await new Promise((resolve) => {
56
64
  if (this.watcher) {
57
- this.watcher.on('ready', () => resolve());
65
+ this.watcher.on('ready', () => {
66
+ this.readyAt = new Date();
67
+ resolve();
68
+ });
58
69
  }
59
70
  else {
60
71
  resolve();
@@ -67,6 +78,7 @@ export class WatchService {
67
78
  // Resolving `start()` only once every target appears in `getWatched()`
68
79
  // makes readiness mean armed.
69
80
  await this.waitUntilArmed(patterns);
81
+ this.missingTargets = this.unarmedTargets(patterns);
70
82
  const watched = this.watcher?.getWatched() ?? {};
71
83
  this.stats.filesWatched = Object.values(watched).reduce((sum, files) => sum + files.length, 0);
72
84
  }
@@ -90,8 +102,12 @@ export class WatchService {
90
102
  }
91
103
  /** True when chokidar reports a watch covering every requested target. */
92
104
  allTargetsArmed(patterns) {
105
+ return this.unarmedTargets(patterns).length === 0;
106
+ }
107
+ /** Requested targets that `getWatched()` does not yet cover. */
108
+ unarmedTargets(patterns) {
93
109
  if (!this.watcher) {
94
- return true;
110
+ return [];
95
111
  }
96
112
  const watched = this.watcher.getWatched();
97
113
  // chokidar keys `getWatched()` with the spelling it was given, so resolve
@@ -106,7 +122,16 @@ export class WatchService {
106
122
  }
107
123
  // A directory target is keyed directly; a file target is listed under its
108
124
  // parent. Either spelling resolves into the same set.
109
- return patterns.every((pattern) => armed.has(path.resolve(pattern)));
125
+ return patterns
126
+ .map((pattern) => path.resolve(pattern))
127
+ .filter((target) => !armed.has(target));
128
+ }
129
+ recordError(error) {
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ this.recordedErrors.push(message);
132
+ if (this.recordedErrors.length > MAX_RECORDED_ERRORS) {
133
+ this.recordedErrors.splice(0, this.recordedErrors.length - MAX_RECORDED_ERRORS);
134
+ }
110
135
  }
111
136
  /**
112
137
  * Stop watching
@@ -157,6 +182,21 @@ export class WatchService {
157
182
  getStats() {
158
183
  return { ...this.stats };
159
184
  }
185
+ /**
186
+ * What the watcher has observed about its own lifecycle. Intended for the
187
+ * failure path: a caller that waited for an event that never came reports
188
+ * this instead of a bare "no events" (#2553).
189
+ */
190
+ getDiagnostics() {
191
+ return {
192
+ ready: this.readyAt !== undefined,
193
+ readyAt: this.readyAt,
194
+ armed: this.missingTargets.length === 0,
195
+ missingTargets: [...this.missingTargets],
196
+ errors: [...this.recordedErrors],
197
+ watched: this.watcher ? this.watcher.getWatched() : {},
198
+ };
199
+ }
160
200
  /**
161
201
  * Check if service is running
162
202
  */
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Independent AIWG implementation of the Fortemi dataset capability wire rules
3
+ * (Fortemi/fortemi-react#422 validation revision 1.0.1). AIWG pins the authority
4
+ * schema and vectors but never imports the producer's validator, so agreement is
5
+ * established by shared vectors rather than by shared code.
6
+ */
7
+ export declare const FORTEMI_CAPABILITY_CONTRACT = "fortemi.dataset-execution-capabilities/v1";
8
+ export declare const FORTEMI_CAPABILITY_SCHEMA_MAJOR = 1;
9
+ export interface FortemiCapabilityDeclaration {
10
+ id: string;
11
+ version: string;
12
+ status: "supported" | "experimental" | "unsupported";
13
+ limits?: Record<string, number>;
14
+ evidence: string[];
15
+ }
16
+ export interface FortemiCapabilityDescriptor {
17
+ contract: string;
18
+ schemaVersion: string;
19
+ runtime: {
20
+ id: string;
21
+ version: string;
22
+ plane: string;
23
+ dataClass: string;
24
+ maturity: string;
25
+ };
26
+ guarantees: Record<string, string>;
27
+ capabilities: FortemiCapabilityDeclaration[];
28
+ evidence: Array<{
29
+ id: string;
30
+ kind: string;
31
+ uri: string;
32
+ digest?: string;
33
+ }>;
34
+ }
35
+ export interface FortemiCapabilityRequirement {
36
+ id: string;
37
+ minimumVersion?: string;
38
+ minimumLimits?: Record<string, number>;
39
+ fallback?: string[];
40
+ }
41
+ export interface FortemiCapabilityRequest {
42
+ contract: string;
43
+ required: FortemiCapabilityRequirement[];
44
+ optional?: FortemiCapabilityRequirement[];
45
+ }
46
+ export interface FortemiCapabilityDegradation {
47
+ requested: string;
48
+ selected?: string;
49
+ reason: "unsupported" | "version-insufficient" | "limit-insufficient";
50
+ changedGuarantees: string[];
51
+ }
52
+ export interface FortemiCapabilityDecision {
53
+ accepted: boolean;
54
+ selected: string[];
55
+ degradations: FortemiCapabilityDegradation[];
56
+ diagnostics: string[];
57
+ }
58
+ /** SemVer 2.0.0 precedence; build metadata is ignored. `null` means unparseable. */
59
+ export declare function compareFortemiCapabilityVersions(left: unknown, right: unknown): number | null;
60
+ /** Structural and semantic validation of an advertised capability descriptor. */
61
+ export declare function validateFortemiCapabilityDescriptor(descriptor: unknown): string[];
62
+ /** Structural and semantic validation of a negotiation request AIWG is about to send. */
63
+ export declare function validateFortemiCapabilityRequest(request: unknown): string[];
64
+ /**
65
+ * Pure negotiation of a request against an advertised descriptor. Performs no
66
+ * I/O and mutates neither input, so a caller can compare the result with the
67
+ * decision a server reports for the same pair.
68
+ */
69
+ export declare function negotiateFortemiCapabilities(descriptor: unknown, request: unknown): FortemiCapabilityDecision;
70
+ //# sourceMappingURL=fortemi-capability.d.ts.map