@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.
- package/bin/aiwg.mjs +55 -10
- package/dist/src/artifacts/cli.js +20 -8
- package/dist/src/artifacts/corpus-views/taxonomies.js +1 -1
- package/dist/src/cli/handlers/help.js +6 -5
- package/dist/src/cli/handlers/init.js +7 -2
- package/dist/src/cli/handlers/ralph.js +8 -8
- package/dist/src/cli/handlers/steward.js +9 -1
- package/dist/src/cli/handlers/use.js +4 -0
- package/dist/src/cli/handlers/version.js +13 -4
- package/dist/src/cli/services/deployment-verification.js +32 -4
- package/dist/src/cli/watch-service.js +43 -3
- package/dist/src/dataset/fortemi-capability.d.ts +70 -0
- package/dist/src/dataset/fortemi-capability.js +211 -0
- package/dist/src/dataset/fortemi-dataset-execution.d.ts +9 -0
- package/dist/src/dataset/fortemi-dataset-execution.js +49 -8
- package/dist/src/dataset/fortemi-run-receipt.d.ts +1 -0
- package/dist/src/dataset/fortemi-run-receipt.js +2 -19
- package/dist/src/dataset/fortemi-schema-path.d.ts +7 -0
- package/dist/src/dataset/fortemi-schema-path.js +24 -0
- package/dist/src/dataset/index.d.ts +1 -0
- package/dist/src/dataset/index.js +1 -0
- package/dist/src/extensions/project-local-doctor.js +15 -1
- package/dist/src/lint/cli.js +5 -1
- package/dist/src/lint/reporters.js +17 -0
- package/dist/src/lint/runner.js +160 -5
- package/dist/src/policy/authorization.js +1 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +102 -63
- package/package.json +1 -1
- package/schemas/dataset/fortemi-capability-validation/1.0.1/authority.json +61 -0
- package/schemas/dataset/fortemi-capability-validation/1.0.1/capability.schema.json +469 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +2 -1
- package/tools/agents/deploy-agents.mjs +4 -0
- package/tools/agents/providers/base.mjs +160 -12
- package/tools/agents/providers/claude.mjs +61 -4
- package/tools/agents/providers/codex.mjs +9 -0
- 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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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
|
|
1398
|
-
|
|
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-
|
|
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('
|
|
138
|
-
['
|
|
139
|
-
['
|
|
140
|
-
['
|
|
141
|
-
['
|
|
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
|
-
|
|
66
|
-
|
|
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: ['
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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.', `
|
|
387
|
-
+ `
|
|
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', () =>
|
|
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
|
|
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
|
|
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
|