@aiwg/cli 2026.9.6 → 2026.9.9
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/dist/src/artifacts/index-builder.js +43 -1
- package/dist/src/artifacts/query-engine.js +7 -0
- package/dist/src/cli/handlers/help.js +7 -1
- package/dist/src/cli/handlers/installation.js +106 -2
- package/dist/src/cli/handlers/mc.js +100 -37
- package/dist/src/cli/handlers/ralph.js +14 -4
- package/dist/src/cli/handlers/refresh.js +359 -31
- package/dist/src/cli/handlers/repo-access.js +155 -4
- package/dist/src/cli/handlers/runtime-info.js +3 -0
- package/dist/src/cli/handlers/serve.js +21 -3
- package/dist/src/cli/handlers/setup.js +5 -5
- package/dist/src/cli/handlers/steward.js +30 -1
- package/dist/src/cli/handlers/use.js +123 -12
- package/dist/src/cli/handlers/utilities.js +26 -10
- package/dist/src/cli/handlers/version.js +40 -14
- package/dist/src/cli/handlers/workspace-context.js +8 -0
- package/dist/src/cli/services/deployment-verification.js +156 -7
- package/dist/src/cli/watch-service.js +47 -4
- 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/config/project-artifacts-health.mjs +15 -2
- package/dist/src/cost/fleet-report.js +19 -5
- package/dist/src/extensions/claude-hooks-installer.js +22 -6
- package/dist/src/extensions/project-local-doctor.js +40 -2
- package/dist/src/extensions/project-quickref.js +4 -0
- package/dist/src/installation/manager.mjs +38 -3
- package/dist/src/lint/runner.js +138 -0
- package/dist/src/mcp/helpers.mjs +56 -22
- package/dist/src/mcp/registry.js +32 -22
- package/dist/src/mcp/registry.mjs +31 -26
- package/dist/src/mcp/toml-editor.mjs +117 -0
- package/dist/src/mcp/tools/orchestration.mjs +7 -7
- package/dist/src/mcp/tools/subsystems.mjs +7 -7
- package/dist/src/memory/context-pack.js +5 -1
- package/dist/src/plugin/skill-command-translator.js +70 -1
- package/dist/src/serve/a2a-terminal-observer.js +19 -1
- package/dist/src/serve/mission-hitl.js +91 -0
- package/dist/src/sessions/import-lease.js +5 -1
- package/dist/src/smiths/context-pipeline/workspace-context.js +132 -6
- package/dist/src/testing/fixtures/test-data-factory.js +3 -3
- package/dist/src/writing/pattern-library.js +29 -6
- package/package.json +2 -1
- package/tools/agents/deploy-agents.mjs +91 -5
- package/tools/agents/providers/base.mjs +162 -6
|
@@ -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/') ||
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import * as ui from '../ui.js';
|
|
11
11
|
import { maybePrintCommunityFooter } from '../../community/footer.js';
|
|
12
12
|
import { listProviderDefinitions } from '../../providers/provider-definitions.js';
|
|
13
|
+
import { getCommandIds } from '../../extensions/commands/definitions.js';
|
|
13
14
|
/**
|
|
14
15
|
* Help command handler
|
|
15
16
|
*/
|
|
@@ -19,7 +20,12 @@ export const helpHandler = {
|
|
|
19
20
|
description: 'Show CLI help message',
|
|
20
21
|
category: 'maintenance',
|
|
21
22
|
aliases: ['-h', '-help', '--help'],
|
|
22
|
-
async execute(
|
|
23
|
+
async execute(ctx) {
|
|
24
|
+
if (ctx.args.includes('--json')) {
|
|
25
|
+
// Canonical IDs only: aliases and example prose are not registry entries.
|
|
26
|
+
console.log(JSON.stringify({ schema: 'aiwg.command-registry.v1', commandIds: getCommandIds() }));
|
|
27
|
+
return { exitCode: 0 };
|
|
28
|
+
}
|
|
23
29
|
displayHelp();
|
|
24
30
|
return { exitCode: 0 };
|
|
25
31
|
},
|
|
@@ -20,21 +20,93 @@ function display(status, json) {
|
|
|
20
20
|
console.log(`Release channel: ${status.identity?.channel ?? '(unrecorded)'}`);
|
|
21
21
|
console.log(`Actual method: ${status.actualMethod}`);
|
|
22
22
|
console.log(`Actual root: ${status.actualRoot}`);
|
|
23
|
+
console.log(`Framework root: ${status.frameworkRoot}`);
|
|
24
|
+
if (status.launcher) {
|
|
25
|
+
console.log(`Launcher: ${status.launcher.method} at ${status.launcher.root} (edge redirect — expected)`);
|
|
26
|
+
}
|
|
23
27
|
if (status.drift.length > 0) {
|
|
24
28
|
console.log('Drift:');
|
|
25
29
|
for (const item of status.drift)
|
|
26
30
|
console.log(` - ${item}`);
|
|
27
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
|
+
}
|
|
28
39
|
console.log('');
|
|
29
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
|
+
}
|
|
30
97
|
export const installationHandler = {
|
|
31
98
|
id: 'installation',
|
|
32
99
|
name: 'Installation',
|
|
33
100
|
description: 'Inspect, adopt, or deliberately switch the canonical global installation',
|
|
34
101
|
category: 'maintenance',
|
|
35
102
|
aliases: [],
|
|
103
|
+
async help() {
|
|
104
|
+
return { exitCode: 0, message: usage(), rawOutput: true };
|
|
105
|
+
},
|
|
36
106
|
async execute(ctx) {
|
|
37
107
|
const [action = 'show'] = ctx.args;
|
|
108
|
+
if (action === 'help')
|
|
109
|
+
return { exitCode: 0, message: usage(), rawOutput: true };
|
|
38
110
|
const json = ctx.args.includes('--json');
|
|
39
111
|
const actualRoot = getPackageRoot();
|
|
40
112
|
const common = {
|
|
@@ -50,11 +122,43 @@ export const installationHandler = {
|
|
|
50
122
|
}
|
|
51
123
|
if (action === 'adopt') {
|
|
52
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
|
+
}
|
|
53
154
|
const status = adoptInstallation({
|
|
54
155
|
...common,
|
|
55
156
|
method,
|
|
56
157
|
runMode: valueAfter(ctx.args, '--run-mode'),
|
|
57
158
|
});
|
|
159
|
+
if (abandoning) {
|
|
160
|
+
console.log(`Warning: adopted the running ${status.actualMethod} install; the previously declared ${declaredMethod} install is no longer canonical.`);
|
|
161
|
+
}
|
|
58
162
|
display(status, json);
|
|
59
163
|
return { exitCode: 0 };
|
|
60
164
|
}
|
|
@@ -62,7 +166,7 @@ export const installationHandler = {
|
|
|
62
166
|
const root = valueAfter(ctx.args, '--root');
|
|
63
167
|
const method = valueAfter(ctx.args, '--method');
|
|
64
168
|
if (!root || !method) {
|
|
65
|
-
return { exitCode: 2, message:
|
|
169
|
+
return { exitCode: 2, message: `switch requires --root and --method\n${usage()}`, rawOutput: true };
|
|
66
170
|
}
|
|
67
171
|
const status = switchInstallation({
|
|
68
172
|
...common,
|
|
@@ -73,7 +177,7 @@ export const installationHandler = {
|
|
|
73
177
|
display(status, json);
|
|
74
178
|
return { exitCode: 0 };
|
|
75
179
|
}
|
|
76
|
-
return { exitCode: 2, message:
|
|
180
|
+
return { exitCode: 2, message: `Unknown installation action: ${action}\n${usage()}`, rawOutput: true };
|
|
77
181
|
},
|
|
78
182
|
};
|
|
79
183
|
//# sourceMappingURL=installation.js.map
|
|
@@ -169,12 +169,15 @@ function hasFlag(args, flag) {
|
|
|
169
169
|
* dispatch missions with no ceiling while the operator believed one applied
|
|
170
170
|
* (#1770).
|
|
171
171
|
*/
|
|
172
|
-
function parseNumberFlag(args, flag, invalidSink) {
|
|
173
|
-
|
|
174
|
-
if (raw === undefined)
|
|
172
|
+
function parseNumberFlag(args, flag, invalidSink, integer = false) {
|
|
173
|
+
if (!args.some(arg => arg === flag || arg.startsWith(`${flag}=`)))
|
|
175
174
|
return undefined;
|
|
176
|
-
const
|
|
177
|
-
|
|
175
|
+
const raw = parseFlag(args, flag);
|
|
176
|
+
// Presence and validity are distinct: a trailing flag must not silently
|
|
177
|
+
// disappear, and counter limits must not accept fractional/unsafe values.
|
|
178
|
+
const decimal = typeof raw === 'string' && /^[+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw);
|
|
179
|
+
const value = decimal ? Number(raw) : NaN;
|
|
180
|
+
if (!Number.isFinite(value) || value <= 0 || (integer && !Number.isSafeInteger(value))) {
|
|
178
181
|
invalidSink?.push(`${flag} (got '${raw}')`);
|
|
179
182
|
return undefined;
|
|
180
183
|
}
|
|
@@ -225,7 +228,13 @@ Drain queued missions in a session by launching each as a ralph loop. Missions
|
|
|
225
228
|
without --completion criteria are skipped with a warning.
|
|
226
229
|
|
|
227
230
|
--accept-cost Skip the cost-warning gate (required for non-TTY contexts
|
|
228
|
-
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.`,
|
|
229
238
|
status: `Usage: aiwg mc status [<session-id>] [--json]
|
|
230
239
|
|
|
231
240
|
Show mission status for a session. Auto-syncs from ralph loop state files.
|
|
@@ -330,23 +339,13 @@ async function mcDispatch(ctx) {
|
|
|
330
339
|
const completion = parseFlag(ctx.args, '--completion');
|
|
331
340
|
const priority = parseFlag(ctx.args, '--priority') || 'normal';
|
|
332
341
|
const invalidFlags = [];
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!Number.isFinite(parsedIterations) || parsedIterations <= 0) {
|
|
338
|
-
invalidFlags.push(`--max-iterations (got '${maxIterationsRaw}')`);
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
maxIterations = parsedIterations;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
const maxTotalTokens = parseNumberFlag(ctx.args, '--max-total-tokens', invalidFlags);
|
|
345
|
-
const maxOutputTokens = parseNumberFlag(ctx.args, '--max-output-tokens', invalidFlags);
|
|
346
|
-
const maxToolCalls = parseNumberFlag(ctx.args, '--max-tool-calls', invalidFlags);
|
|
342
|
+
const maxIterations = parseNumberFlag(ctx.args, '--max-iterations', invalidFlags, true) ?? 10;
|
|
343
|
+
const maxTotalTokens = parseNumberFlag(ctx.args, '--max-total-tokens', invalidFlags, true);
|
|
344
|
+
const maxOutputTokens = parseNumberFlag(ctx.args, '--max-output-tokens', invalidFlags, true);
|
|
345
|
+
const maxToolCalls = parseNumberFlag(ctx.args, '--max-tool-calls', invalidFlags, true);
|
|
347
346
|
const maxTotalCost = parseNumberFlag(ctx.args, '--max-total-cost', invalidFlags);
|
|
348
347
|
const maxWallClockMinutes = parseNumberFlag(ctx.args, '--max-wall-clock-minutes', invalidFlags);
|
|
349
|
-
const explorationQuota = parseNumberFlag(ctx.args, '--exploration-quota', invalidFlags);
|
|
348
|
+
const explorationQuota = parseNumberFlag(ctx.args, '--exploration-quota', invalidFlags, true);
|
|
350
349
|
const budgetStopPolicyRaw = parseFlag(ctx.args, '--budget-stop-policy');
|
|
351
350
|
let budgetStopPolicy;
|
|
352
351
|
if (budgetStopPolicyRaw !== undefined) {
|
|
@@ -486,6 +485,71 @@ async function mcDispatch(ctx) {
|
|
|
486
485
|
* detached process, so they DO run in parallel after launch — the loop here
|
|
487
486
|
* is just for orderly dispatch, not for parallel scheduling)
|
|
488
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
|
+
}
|
|
489
553
|
async function mcRun(ctx) {
|
|
490
554
|
const positional = getPositionalArgs(ctx.args);
|
|
491
555
|
const sessionId = positional[0];
|
|
@@ -500,24 +564,28 @@ async function mcRun(ctx) {
|
|
|
500
564
|
ui.info(`No queued missions in session ${session.id}. Run \`aiwg mc dispatch ${session.id} "<objective>"\` to add one.`);
|
|
501
565
|
return { exitCode: 0 };
|
|
502
566
|
}
|
|
503
|
-
// #1450 P0: cost warning gate.
|
|
567
|
+
// #1450 P0: cost warning gate, with declared ceilings honored (#2522).
|
|
504
568
|
//
|
|
505
569
|
// Each headless claude session pays a ~$1.60 cache-creation cost on iteration
|
|
506
570
|
// 1 before any user-meaningful work (sonnet baseline; opus is ~$3.90). Across
|
|
507
571
|
// N missions × M iterations the floor compounds quickly. Warn before launch
|
|
508
572
|
// and refuse in non-TTY contexts unless --accept-cost is set.
|
|
509
|
-
//
|
|
510
|
-
// Estimate is intentionally conservative: cumulative iteration floor =
|
|
511
|
-
// missions × max_iterations × sonnet_cache_cost. Real spend may be lower if
|
|
512
|
-
// missions complete in fewer iterations.
|
|
513
573
|
const eligible = queued.filter(m => m.mode !== 'pty-orchestrator' && !!m.completion);
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
|
|
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) {
|
|
519
581
|
ui.blank();
|
|
520
|
-
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
|
+
}
|
|
521
589
|
ui.warn('Actual spend may be lower if missions complete early, higher if model is opus or context grows.');
|
|
522
590
|
if (!process.stdout.isTTY) {
|
|
523
591
|
ui.error('Refusing to launch in non-interactive context. Re-run with `--accept-cost` to proceed.');
|
|
@@ -538,12 +606,7 @@ async function mcRun(ctx) {
|
|
|
538
606
|
let launched = 0;
|
|
539
607
|
let skipped = 0;
|
|
540
608
|
let failed = 0;
|
|
541
|
-
const projectRoot = ctx.cwd || process.cwd();
|
|
542
|
-
const frameworkRoot = ctx.frameworkRoot;
|
|
543
|
-
const { readAiwgConfig, resolveParallelism } = await import('../../config/aiwg-config.js');
|
|
544
609
|
const { FileAdmissionStore, SharedHostScheduler } = await import('../../serve/shared-host-scheduler.js');
|
|
545
|
-
const cfg = await readAiwgConfig(projectRoot).catch(() => null);
|
|
546
|
-
const provider = cfg?.providers[0] ?? 'unknown';
|
|
547
610
|
const maxConcurrent = resolveParallelism(cfg?.parallelism, provider).max_parallel_mc_missions;
|
|
548
611
|
const scheduler = new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), {
|
|
549
612
|
maxConcurrent,
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
import { createScriptRunner } from './script-runner.js';
|
|
14
14
|
import { launchExternalRalph, getLoopStatuses, abortLoop, resumeLoop, attachToLoopOutput, } from './ralph-launcher.js';
|
|
15
15
|
import { handlerResultFromError } from '../errors.js';
|
|
16
|
+
/** Parse a complete positive decimal value without truncating counter limits. */
|
|
17
|
+
function parsePositiveLimit(raw, integer = false) {
|
|
18
|
+
if (typeof raw !== 'string' || !/^[+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(raw))
|
|
19
|
+
return undefined;
|
|
20
|
+
const value = Number(raw);
|
|
21
|
+
return Number.isFinite(value) && value > 0 && (!integer || Number.isSafeInteger(value)) ? value : undefined;
|
|
22
|
+
}
|
|
16
23
|
/**
|
|
17
24
|
* Parse Ralph command arguments
|
|
18
25
|
*/
|
|
@@ -22,8 +29,8 @@ function parseRalphArgs(args) {
|
|
|
22
29
|
// Present-but-invalid numeric values are a hard usage error: an operator who
|
|
23
30
|
// typed --max-total-cost expects a ceiling to exist (#1770).
|
|
24
31
|
const positiveNumber = (flag, raw, integer = false) => {
|
|
25
|
-
const value =
|
|
26
|
-
if (
|
|
32
|
+
const value = parsePositiveLimit(raw, integer);
|
|
33
|
+
if (value === undefined) {
|
|
27
34
|
result.invalidFlags.push(`${flag} (got '${raw ?? ''}')`);
|
|
28
35
|
return undefined;
|
|
29
36
|
}
|
|
@@ -431,8 +438,11 @@ export class RalphResumeHandler {
|
|
|
431
438
|
if (arg === '--loop-id' && ctx.args[i + 1]) {
|
|
432
439
|
loopId = ctx.args[++i];
|
|
433
440
|
}
|
|
434
|
-
else if (arg === '--max-iterations'
|
|
435
|
-
maxIterations =
|
|
441
|
+
else if (arg === '--max-iterations') {
|
|
442
|
+
maxIterations = parsePositiveLimit(ctx.args[++i], true);
|
|
443
|
+
if (maxIterations === undefined) {
|
|
444
|
+
return { exitCode: 1, message: 'Error: --max-iterations requires a positive safe integer. Loop not resumed.' };
|
|
445
|
+
}
|
|
436
446
|
}
|
|
437
447
|
}
|
|
438
448
|
try {
|