@aiwg/cli 2026.9.7 → 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/artifacts/index-builder.js +43 -1
- package/dist/src/artifacts/query-engine.js +7 -0
- package/dist/src/cli/handlers/help.js +6 -5
- package/dist/src/cli/handlers/init.js +7 -2
- package/dist/src/cli/handlers/installation.js +102 -2
- package/dist/src/cli/handlers/mc.js +87 -17
- package/dist/src/cli/handlers/ralph.js +8 -8
- package/dist/src/cli/handlers/refresh.js +67 -7
- package/dist/src/cli/handlers/repo-access.js +155 -4
- package/dist/src/cli/handlers/setup.js +5 -5
- package/dist/src/cli/handlers/steward.js +39 -2
- package/dist/src/cli/handlers/use.js +13 -4
- package/dist/src/cli/handlers/version.js +49 -14
- package/dist/src/cli/handlers/workspace-context.js +8 -0
- package/dist/src/cli/services/deployment-verification.js +71 -10
- package/dist/src/cli/watch-service.js +43 -3
- package/dist/src/config/aiwg-config.js +95 -3
- package/dist/src/config/cli.js +16 -1
- package/dist/src/config/gitignore.js +5 -0
- 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/claude-hooks-installer.js +22 -6
- 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 +297 -4
- package/dist/src/policy/authorization.js +1 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +138 -49
- 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 +8 -0
- package/tools/agents/providers/base.mjs +261 -16
- 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']],
|
|
@@ -712,6 +712,33 @@ export function parseFlowDoc(content, filePath) {
|
|
|
712
712
|
searchTerms: [...searchTerms],
|
|
713
713
|
};
|
|
714
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* Best-effort absolute path to the AIWG install root, for error text that must
|
|
717
|
+
* name where the framework graph can actually be built (#2530).
|
|
718
|
+
*/
|
|
719
|
+
function resolveInstallRootHint() {
|
|
720
|
+
try {
|
|
721
|
+
// The running module lives under the install root; walk up to the package.
|
|
722
|
+
let dir = path.dirname(new URL(import.meta.url).pathname);
|
|
723
|
+
for (let i = 0; i < 10; i += 1) {
|
|
724
|
+
const pkg = path.join(dir, 'package.json');
|
|
725
|
+
if (fs.existsSync(pkg)) {
|
|
726
|
+
try {
|
|
727
|
+
const content = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
728
|
+
if (content.name === 'aiwg' || content.name === '@aiwg/cli')
|
|
729
|
+
return dir;
|
|
730
|
+
}
|
|
731
|
+
catch { /* keep walking */ }
|
|
732
|
+
}
|
|
733
|
+
const parent = path.dirname(dir);
|
|
734
|
+
if (parent === dir)
|
|
735
|
+
break;
|
|
736
|
+
dir = parent;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
catch { /* fall through */ }
|
|
740
|
+
return '<aiwg install root>';
|
|
741
|
+
}
|
|
715
742
|
export async function buildIndex(cwd, options = {}) {
|
|
716
743
|
const { force = false, verbose = false, scope, outputDir, graph, explicit = true } = options;
|
|
717
744
|
const startTime = Date.now();
|
|
@@ -748,7 +775,22 @@ export async function buildIndex(cwd, options = {}) {
|
|
|
748
775
|
return;
|
|
749
776
|
}
|
|
750
777
|
console.error(`Error: No scan directories found: ${scanDirs.join(', ')}`);
|
|
751
|
-
|
|
778
|
+
// The framework graph scans the AIWG corpus, which only exists at the install
|
|
779
|
+
// root — never in a consumer project. Saying "run from a project with the
|
|
780
|
+
// required directories" sends the operator looking in the wrong place (#2530).
|
|
781
|
+
if (graph === 'framework') {
|
|
782
|
+
const installRoot = resolveInstallRootHint();
|
|
783
|
+
console.log('The framework graph indexes the AIWG corpus and can only be built at the');
|
|
784
|
+
console.log('install root, not in a consumer project. Build it there, then sync here:');
|
|
785
|
+
console.log('');
|
|
786
|
+
console.log(` cd ${installRoot} && aiwg index build --graph framework --force`);
|
|
787
|
+
console.log(` cd ${cwd} && aiwg index sync --backend fortemi-core --graph framework`);
|
|
788
|
+
console.log('');
|
|
789
|
+
console.log("As an immediate workaround, discovery also works with '--backend local'.");
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
console.log('Run this command from a project with the required directories.');
|
|
793
|
+
}
|
|
752
794
|
process.exit(1);
|
|
753
795
|
}
|
|
754
796
|
// Determine output index directory
|
|
@@ -59,6 +59,13 @@ function canonicalLocalityRank(entryPath) {
|
|
|
59
59
|
const normalized = entryPath.replace(/\\/g, '/');
|
|
60
60
|
if (normalized.startsWith('.aiwg/') || normalized.includes('/.aiwg/'))
|
|
61
61
|
return 0;
|
|
62
|
+
// Top-level persona mirrors are the least canonical source for a name a bundle
|
|
63
|
+
// also owns. Without this they fell into the catch-all below and scored 1,
|
|
64
|
+
// beating the bundle copy at 2 — the opposite of #1643. It only ever looked
|
|
65
|
+
// correct because a populated user index supplied provenance and `scopeRank`
|
|
66
|
+
// never reached this fallback (#2544).
|
|
67
|
+
if (normalized.startsWith('agentic/code/agents/') || normalized.includes('/agentic/code/agents/'))
|
|
68
|
+
return 4;
|
|
62
69
|
if (normalized.includes('/plugins/') || normalized.startsWith('agentic/code/plugins/'))
|
|
63
70
|
return 3;
|
|
64
71
|
if (normalized.includes('/frameworks/') ||
|
|
@@ -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`);
|
|
@@ -29,16 +29,84 @@ function display(status, json) {
|
|
|
29
29
|
for (const item of status.drift)
|
|
30
30
|
console.log(` - ${item}`);
|
|
31
31
|
}
|
|
32
|
+
const remedy = remediation(status);
|
|
33
|
+
if (remedy) {
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log('Resolve:');
|
|
36
|
+
for (const line of remedy)
|
|
37
|
+
console.log(` ${line}`);
|
|
38
|
+
}
|
|
32
39
|
console.log('');
|
|
33
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* `switch` and `adopt` only write installation.json — they cannot change which
|
|
43
|
+
* binary is on PATH. So when the declaration and reality disagree, neither command
|
|
44
|
+
* resolves it and the operator needs a shell step this output never mentioned.
|
|
45
|
+
* Print it, tailored to the direction of the drift (#2534).
|
|
46
|
+
*/
|
|
47
|
+
export function remediation(status) {
|
|
48
|
+
if (status.state !== 'mismatch')
|
|
49
|
+
return null;
|
|
50
|
+
const canonicalMethod = status.identity?.method;
|
|
51
|
+
const canonicalRoot = status.identity?.root;
|
|
52
|
+
const actualMethod = status.actualMethod;
|
|
53
|
+
if (canonicalMethod === 'source' && canonicalRoot && actualMethod !== 'source') {
|
|
54
|
+
return [
|
|
55
|
+
`The declared source install is not what runs. Put it on PATH:`,
|
|
56
|
+
` cd ${canonicalRoot} && npm link`,
|
|
57
|
+
`Then re-run 'aiwg installation show' to confirm State: aligned.`,
|
|
58
|
+
`('aiwg installation switch' would only rewrite the declaration, which already says source.)`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
if (canonicalMethod === 'npm' && actualMethod === 'source') {
|
|
62
|
+
return [
|
|
63
|
+
`The declared npm package is not what runs. Restore it:`,
|
|
64
|
+
` npm install -g aiwg`,
|
|
65
|
+
`If a source checkout was linked, unlink it first: npm unlink -g aiwg`,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
return [
|
|
69
|
+
`Declaration and reality disagree (${canonicalMethod ?? 'unrecorded'} vs ${actualMethod}).`,
|
|
70
|
+
`'switch' and 'adopt' are declaration-only and cannot change PATH.`,
|
|
71
|
+
`Install or link the intended root, then re-run 'aiwg installation show'.`,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function usage() {
|
|
75
|
+
return `
|
|
76
|
+
aiwg installation — inspect, adopt, or switch the canonical global installation
|
|
77
|
+
|
|
78
|
+
Usage:
|
|
79
|
+
aiwg installation show [--json]
|
|
80
|
+
aiwg installation adopt --method <npm|web|source> [--run-mode <normal|development>] [--yes]
|
|
81
|
+
aiwg installation switch --root <path> --method <npm|web|source> [--manager <absolute-path>]
|
|
82
|
+
|
|
83
|
+
Options:
|
|
84
|
+
--json Machine-readable output
|
|
85
|
+
--config-dir Override the installation config directory
|
|
86
|
+
--manager Absolute path to the package manager executable
|
|
87
|
+
--channel Release channel (stable|edge)
|
|
88
|
+
--run-mode normal|development (derived from --method when omitted)
|
|
89
|
+
--yes Confirm an adopt that abandons the declared install
|
|
90
|
+
|
|
91
|
+
Notes:
|
|
92
|
+
These commands are declaration-only: they record which installation is
|
|
93
|
+
canonical, they do not change which binary is on PATH. When \`show\` reports
|
|
94
|
+
State: mismatch, it prints the concrete command that resolves it.
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
34
97
|
export const installationHandler = {
|
|
35
98
|
id: 'installation',
|
|
36
99
|
name: 'Installation',
|
|
37
100
|
description: 'Inspect, adopt, or deliberately switch the canonical global installation',
|
|
38
101
|
category: 'maintenance',
|
|
39
102
|
aliases: [],
|
|
103
|
+
async help() {
|
|
104
|
+
return { exitCode: 0, message: usage(), rawOutput: true };
|
|
105
|
+
},
|
|
40
106
|
async execute(ctx) {
|
|
41
107
|
const [action = 'show'] = ctx.args;
|
|
108
|
+
if (action === 'help')
|
|
109
|
+
return { exitCode: 0, message: usage(), rawOutput: true };
|
|
42
110
|
const json = ctx.args.includes('--json');
|
|
43
111
|
const actualRoot = getPackageRoot();
|
|
44
112
|
const common = {
|
|
@@ -54,11 +122,43 @@ export const installationHandler = {
|
|
|
54
122
|
}
|
|
55
123
|
if (action === 'adopt') {
|
|
56
124
|
const method = valueAfter(ctx.args, '--method');
|
|
125
|
+
// adopt resolves a mismatch by rewriting canonical to match whatever is
|
|
126
|
+
// running — i.e. by abandoning the declared install. That is the opposite
|
|
127
|
+
// of what an operator standardizing on a source checkout wants, so make it
|
|
128
|
+
// a deliberate choice rather than a silent capitulation (#2534).
|
|
129
|
+
const before = inspectInstallation({
|
|
130
|
+
...common,
|
|
131
|
+
identity: loadInstallationIdentity({ ...common, createIfMissing: true }),
|
|
132
|
+
});
|
|
133
|
+
const declaredMethod = before.identity?.method;
|
|
134
|
+
const abandoning = before.state === 'mismatch'
|
|
135
|
+
&& declaredMethod
|
|
136
|
+
&& declaredMethod !== before.actualMethod;
|
|
137
|
+
if (abandoning && !ctx.args.includes('--yes')) {
|
|
138
|
+
return {
|
|
139
|
+
exitCode: 2,
|
|
140
|
+
rawOutput: true,
|
|
141
|
+
message: [
|
|
142
|
+
`Refusing to adopt: this would abandon the declared ${declaredMethod} install.`,
|
|
143
|
+
``,
|
|
144
|
+
` declared: ${declaredMethod} at ${before.identity?.root ?? '(unrecorded)'}`,
|
|
145
|
+
` running: ${before.actualMethod} at ${before.actualRoot}`,
|
|
146
|
+
``,
|
|
147
|
+
`adopt rewrites the declaration to match what is running; it does not change`,
|
|
148
|
+
`which binary is on PATH. If you meant to keep the declared install, run`,
|
|
149
|
+
`'aiwg installation show' for the command that puts it back on PATH.`,
|
|
150
|
+
`If you really mean to abandon it, re-run with --yes.`,
|
|
151
|
+
].join('\n'),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
57
154
|
const status = adoptInstallation({
|
|
58
155
|
...common,
|
|
59
156
|
method,
|
|
60
157
|
runMode: valueAfter(ctx.args, '--run-mode'),
|
|
61
158
|
});
|
|
159
|
+
if (abandoning) {
|
|
160
|
+
console.log(`Warning: adopted the running ${status.actualMethod} install; the previously declared ${declaredMethod} install is no longer canonical.`);
|
|
161
|
+
}
|
|
62
162
|
display(status, json);
|
|
63
163
|
return { exitCode: 0 };
|
|
64
164
|
}
|
|
@@ -66,7 +166,7 @@ export const installationHandler = {
|
|
|
66
166
|
const root = valueAfter(ctx.args, '--root');
|
|
67
167
|
const method = valueAfter(ctx.args, '--method');
|
|
68
168
|
if (!root || !method) {
|
|
69
|
-
return { exitCode: 2, message:
|
|
169
|
+
return { exitCode: 2, message: `switch requires --root and --method\n${usage()}`, rawOutput: true };
|
|
70
170
|
}
|
|
71
171
|
const status = switchInstallation({
|
|
72
172
|
...common,
|
|
@@ -77,7 +177,7 @@ export const installationHandler = {
|
|
|
77
177
|
display(status, json);
|
|
78
178
|
return { exitCode: 0 };
|
|
79
179
|
}
|
|
80
|
-
return { exitCode: 2, message:
|
|
180
|
+
return { exitCode: 2, message: `Unknown installation action: ${action}\n${usage()}`, rawOutput: true };
|
|
81
181
|
},
|
|
82
182
|
};
|
|
83
183
|
//# sourceMappingURL=installation.js.map
|
|
@@ -228,7 +228,13 @@ Drain queued missions in a session by launching each as a ralph loop. Missions
|
|
|
228
228
|
without --completion criteria are skipped with a warning.
|
|
229
229
|
|
|
230
230
|
--accept-cost Skip the cost-warning gate (required for non-TTY contexts
|
|
231
|
-
when estimated cumulative cost exceeds $5). See #1450
|
|
231
|
+
when estimated cumulative cost exceeds $5). See #1450.
|
|
232
|
+
|
|
233
|
+
The estimate is the cumulative iteration floor (missions x max-iterations x
|
|
234
|
+
~$1.60 cache cost per headless iteration). A mission's --max-total-cost caps its
|
|
235
|
+
share of that estimate only on a provider that reports spend; on a provider that
|
|
236
|
+
reports none the ceiling is inert (#1766), so it is reported but not subtracted.
|
|
237
|
+
See #2522.`,
|
|
232
238
|
status: `Usage: aiwg mc status [<session-id>] [--json]
|
|
233
239
|
|
|
234
240
|
Show mission status for a session. Auto-syncs from ralph loop state files.
|
|
@@ -479,6 +485,71 @@ async function mcDispatch(ctx) {
|
|
|
479
485
|
* detached process, so they DO run in parallel after launch — the loop here
|
|
480
486
|
* is just for orderly dispatch, not for parallel scheduling)
|
|
481
487
|
*/
|
|
488
|
+
/**
|
|
489
|
+
* Per-iteration cache-creation floor for a headless claude session (sonnet
|
|
490
|
+
* baseline; opus is ~$3.90).
|
|
491
|
+
*/
|
|
492
|
+
export const SONNET_CACHE_USD = 1.60;
|
|
493
|
+
/** Estimate at or above which `mc run` warns and refuses in non-TTY contexts. */
|
|
494
|
+
export const COST_WARNING_THRESHOLD_USD = 5.0;
|
|
495
|
+
/**
|
|
496
|
+
* Providers whose headless stream reports spend, so a declared `--max-total-cost`
|
|
497
|
+
* ceiling can actually fire mid-loop.
|
|
498
|
+
*
|
|
499
|
+
* Only `claude` emits cost on its stream-json events, which is what
|
|
500
|
+
* `SessionLauncher._extractUsageStats` reads. Adapters without stream-json
|
|
501
|
+
* (codex, factory, opencode, deepseek) emit no usage events at all — #1766
|
|
502
|
+
* recorded that a spend ceiling on those is inert and never fires. Adapters that
|
|
503
|
+
* do stream JSON but have not been observed carrying cost fields (omp, pi) stay
|
|
504
|
+
* out of this set deliberately: assuming an observability we have not seen would
|
|
505
|
+
* weaken the gate in exactly the case where the operator has least protection.
|
|
506
|
+
*/
|
|
507
|
+
export const COST_REPORTING_PROVIDERS = new Set(['claude']);
|
|
508
|
+
/**
|
|
509
|
+
* Estimate cumulative spend for a set of queued missions (#2522).
|
|
510
|
+
*
|
|
511
|
+
* The floor for each mission is `maxIterations × SONNET_CACHE_USD`. A declared
|
|
512
|
+
* `maxTotalCost` bounds that floor **only** when the target provider reports
|
|
513
|
+
* spend — on a provider that reports none, the ceiling is inert (#1766), so
|
|
514
|
+
* subtracting it would make the warning weaker precisely where the operator has
|
|
515
|
+
* no enforced protection. Those ceilings are counted and surfaced instead.
|
|
516
|
+
*/
|
|
517
|
+
export function estimateMissionRunCost(missions, provider) {
|
|
518
|
+
const spendObservable = COST_REPORTING_PROVIDERS.has(provider);
|
|
519
|
+
let estimateUsd = 0;
|
|
520
|
+
let iterationFloorUsd = 0;
|
|
521
|
+
let declaredCeilingUsd = 0;
|
|
522
|
+
let cappedMissions = 0;
|
|
523
|
+
let inertCeilingMissions = 0;
|
|
524
|
+
for (const mission of missions) {
|
|
525
|
+
const floor = mission.maxIterations * SONNET_CACHE_USD;
|
|
526
|
+
iterationFloorUsd += floor;
|
|
527
|
+
const ceiling = typeof mission.maxTotalCost === 'number' && Number.isFinite(mission.maxTotalCost)
|
|
528
|
+
? mission.maxTotalCost
|
|
529
|
+
: undefined;
|
|
530
|
+
if (ceiling === undefined) {
|
|
531
|
+
estimateUsd += floor;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
declaredCeilingUsd += ceiling;
|
|
535
|
+
if (spendObservable) {
|
|
536
|
+
cappedMissions += 1;
|
|
537
|
+
estimateUsd += Math.min(floor, ceiling);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
inertCeilingMissions += 1;
|
|
541
|
+
estimateUsd += floor;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
estimateUsd,
|
|
546
|
+
iterationFloorUsd,
|
|
547
|
+
declaredCeilingUsd,
|
|
548
|
+
cappedMissions,
|
|
549
|
+
inertCeilingMissions,
|
|
550
|
+
spendObservable,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
482
553
|
async function mcRun(ctx) {
|
|
483
554
|
const positional = getPositionalArgs(ctx.args);
|
|
484
555
|
const sessionId = positional[0];
|
|
@@ -493,24 +564,28 @@ async function mcRun(ctx) {
|
|
|
493
564
|
ui.info(`No queued missions in session ${session.id}. Run \`aiwg mc dispatch ${session.id} "<objective>"\` to add one.`);
|
|
494
565
|
return { exitCode: 0 };
|
|
495
566
|
}
|
|
496
|
-
// #1450 P0: cost warning gate.
|
|
567
|
+
// #1450 P0: cost warning gate, with declared ceilings honored (#2522).
|
|
497
568
|
//
|
|
498
569
|
// Each headless claude session pays a ~$1.60 cache-creation cost on iteration
|
|
499
570
|
// 1 before any user-meaningful work (sonnet baseline; opus is ~$3.90). Across
|
|
500
571
|
// N missions × M iterations the floor compounds quickly. Warn before launch
|
|
501
572
|
// and refuse in non-TTY contexts unless --accept-cost is set.
|
|
502
|
-
//
|
|
503
|
-
// Estimate is intentionally conservative: cumulative iteration floor =
|
|
504
|
-
// missions × max_iterations × sonnet_cache_cost. Real spend may be lower if
|
|
505
|
-
// missions complete in fewer iterations.
|
|
506
573
|
const eligible = queued.filter(m => m.mode !== 'pty-orchestrator' && !!m.completion);
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
const
|
|
510
|
-
const
|
|
511
|
-
|
|
574
|
+
const projectRoot = ctx.cwd || process.cwd();
|
|
575
|
+
const frameworkRoot = ctx.frameworkRoot;
|
|
576
|
+
const { readAiwgConfig, resolveParallelism } = await import('../../config/aiwg-config.js');
|
|
577
|
+
const cfg = await readAiwgConfig(projectRoot).catch(() => null);
|
|
578
|
+
const provider = cfg?.providers[0] ?? 'unknown';
|
|
579
|
+
const estimate = estimateMissionRunCost(eligible, provider);
|
|
580
|
+
if (estimate.estimateUsd >= COST_WARNING_THRESHOLD_USD && !acceptCost) {
|
|
512
581
|
ui.blank();
|
|
513
|
-
ui.warn(`Cost estimate: ~$${estimateUsd.toFixed(2)} (${eligible.length} missions × iteration floors × ~$${SONNET_CACHE_USD.toFixed(2)} cache cost per claude headless iter).`);
|
|
582
|
+
ui.warn(`Cost estimate: ~$${estimate.estimateUsd.toFixed(2)} (${eligible.length} missions × iteration floors × ~$${SONNET_CACHE_USD.toFixed(2)} cache cost per claude headless iter).`);
|
|
583
|
+
if (estimate.cappedMissions > 0) {
|
|
584
|
+
ui.warn(`Declared ceilings: $${estimate.declaredCeilingUsd.toFixed(2)} across ${estimate.cappedMissions} mission(s) — counted toward the estimate because ${provider} reports spend.`);
|
|
585
|
+
}
|
|
586
|
+
if (estimate.inertCeilingMissions > 0) {
|
|
587
|
+
ui.warn(`${estimate.inertCeilingMissions} mission(s) declare --max-total-cost totalling $${estimate.declaredCeilingUsd.toFixed(2)}, but ${provider} does not report spend — those ceilings cannot fire and are NOT subtracted from this estimate (#1766).`);
|
|
588
|
+
}
|
|
514
589
|
ui.warn('Actual spend may be lower if missions complete early, higher if model is opus or context grows.');
|
|
515
590
|
if (!process.stdout.isTTY) {
|
|
516
591
|
ui.error('Refusing to launch in non-interactive context. Re-run with `--accept-cost` to proceed.');
|
|
@@ -531,12 +606,7 @@ async function mcRun(ctx) {
|
|
|
531
606
|
let launched = 0;
|
|
532
607
|
let skipped = 0;
|
|
533
608
|
let failed = 0;
|
|
534
|
-
const projectRoot = ctx.cwd || process.cwd();
|
|
535
|
-
const frameworkRoot = ctx.frameworkRoot;
|
|
536
|
-
const { readAiwgConfig, resolveParallelism } = await import('../../config/aiwg-config.js');
|
|
537
609
|
const { FileAdmissionStore, SharedHostScheduler } = await import('../../serve/shared-host-scheduler.js');
|
|
538
|
-
const cfg = await readAiwgConfig(projectRoot).catch(() => null);
|
|
539
|
-
const provider = cfg?.providers[0] ?? 'unknown';
|
|
540
610
|
const maxConcurrent = resolveParallelism(cfg?.parallelism, provider).max_parallel_mc_missions;
|
|
541
611
|
const scheduler = new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), {
|
|
542
612
|
maxConcurrent,
|
|
@@ -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');
|