agentxchain 2.61.0 → 2.63.0
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/agentxchain.js +30 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +38 -3
- package/src/commands/accept-turn.js +3 -0
- package/src/commands/config.js +57 -4
- package/src/commands/restart.js +3 -0
- package/src/commands/resume.js +7 -0
- package/src/commands/role.js +106 -0
- package/src/commands/status.js +4 -1
- package/src/commands/step.js +7 -0
- package/src/commands/turn.js +210 -0
- package/src/lib/governed-state.js +42 -9
- package/src/lib/normalized-config.js +2 -2
- package/src/lib/report.js +17 -2
- package/src/lib/run-events.js +1 -0
package/bin/agentxchain.js
CHANGED
|
@@ -86,6 +86,8 @@ import {
|
|
|
86
86
|
} from '../src/commands/plugin.js';
|
|
87
87
|
import { templateSetCommand } from '../src/commands/template-set.js';
|
|
88
88
|
import { templateListCommand } from '../src/commands/template-list.js';
|
|
89
|
+
import { roleCommand } from '../src/commands/role.js';
|
|
90
|
+
import { turnShowCommand } from '../src/commands/turn.js';
|
|
89
91
|
import { templateValidateCommand } from '../src/commands/template-validate.js';
|
|
90
92
|
import {
|
|
91
93
|
multiInitCommand,
|
|
@@ -192,6 +194,7 @@ program
|
|
|
192
194
|
.description('View or edit project configuration')
|
|
193
195
|
.option('--add-agent', 'Add a new agent interactively')
|
|
194
196
|
.option('--remove-agent <id>', 'Remove an agent by ID')
|
|
197
|
+
.option('--get <path>', 'Read a config value (e.g. --get project.goal)')
|
|
195
198
|
.option('--set <path_and_value...>', 'Set a config value (e.g. --set project.goal "Build a governed CLI")')
|
|
196
199
|
.option('-j, --json', 'Output config as JSON')
|
|
197
200
|
.action(configCommand);
|
|
@@ -484,6 +487,33 @@ templateCmd
|
|
|
484
487
|
.option('-j, --json', 'Output as JSON')
|
|
485
488
|
.action(templateValidateCommand);
|
|
486
489
|
|
|
490
|
+
const roleCmd = program
|
|
491
|
+
.command('role')
|
|
492
|
+
.description('Inspect governed role definitions');
|
|
493
|
+
|
|
494
|
+
roleCmd
|
|
495
|
+
.command('list')
|
|
496
|
+
.description('List all defined roles with title, authority, and runtime')
|
|
497
|
+
.option('-j, --json', 'Output as JSON')
|
|
498
|
+
.action((opts) => roleCommand('list', null, opts));
|
|
499
|
+
|
|
500
|
+
roleCmd
|
|
501
|
+
.command('show <role_id>')
|
|
502
|
+
.description('Show detailed information for a single role')
|
|
503
|
+
.option('-j, --json', 'Output as JSON')
|
|
504
|
+
.action((roleId, opts) => roleCommand('show', roleId, opts));
|
|
505
|
+
|
|
506
|
+
const turnCmd = program
|
|
507
|
+
.command('turn')
|
|
508
|
+
.description('Inspect active governed turn dispatch bundles');
|
|
509
|
+
|
|
510
|
+
turnCmd
|
|
511
|
+
.command('show [turn_id]')
|
|
512
|
+
.description('Show a selected active turn and its dispatch artifacts')
|
|
513
|
+
.option('--artifact <name>', 'Print one artifact: assignment, prompt, context, or manifest')
|
|
514
|
+
.option('-j, --json', 'Output as JSON')
|
|
515
|
+
.action(turnShowCommand);
|
|
516
|
+
|
|
487
517
|
const multiCmd = program
|
|
488
518
|
.command('multi')
|
|
489
519
|
.description('Multi-repo coordinator orchestration');
|
package/package.json
CHANGED
package/scripts/release-bump.sh
CHANGED
|
@@ -198,21 +198,53 @@ echo " OK: all 9 governed version surfaces reference ${TARGET_VERSION}"
|
|
|
198
198
|
|
|
199
199
|
# 5. Auto-align Homebrew mirror to target version
|
|
200
200
|
# The formula URL and README version/tarball are updated automatically.
|
|
201
|
-
# The SHA256 is carried from the previous
|
|
201
|
+
# The SHA256 is carried from the previous committed formula — it is inherently a
|
|
202
202
|
# post-publish artifact (npm registry tarballs are not byte-identical to
|
|
203
|
-
# local npm-pack output).
|
|
203
|
+
# local npm-pack output). Any working-tree SHA edit is overwritten here.
|
|
204
|
+
# sync-homebrew.sh corrects the SHA after publish.
|
|
204
205
|
echo "[5/9] Auto-aligning Homebrew mirror to ${TARGET_VERSION}..."
|
|
205
206
|
HOMEBREW_MIRROR="${REPO_ROOT}/cli/homebrew/agentxchain.rb"
|
|
206
207
|
HOMEBREW_MIRROR_README="${REPO_ROOT}/cli/homebrew/README.md"
|
|
207
208
|
TARBALL_URL="https://registry.npmjs.org/agentxchain/-/agentxchain-${TARGET_VERSION}.tgz"
|
|
208
209
|
HOMEBREW_ALIGNED=false
|
|
210
|
+
COMMITTED_HOMEBREW_SHA=""
|
|
211
|
+
|
|
212
|
+
extract_formula_sha() {
|
|
213
|
+
sed -nE 's|^[[:space:]]*sha256 "([a-f0-9]{64})".*|\1|p' "$1" | head -n 1
|
|
214
|
+
}
|
|
209
215
|
|
|
210
216
|
if [[ -f "$HOMEBREW_MIRROR" ]]; then
|
|
217
|
+
COMMITTED_FORMULA_TMP="$(mktemp "${TMPDIR:-/tmp}/agentxchain-homebrew-head.XXXXXX")"
|
|
218
|
+
if ! git -C "$REPO_ROOT" show "HEAD:cli/homebrew/agentxchain.rb" >"$COMMITTED_FORMULA_TMP" 2>/dev/null; then
|
|
219
|
+
rm -f "$COMMITTED_FORMULA_TMP"
|
|
220
|
+
echo "FAIL: could not load HEAD:cli/homebrew/agentxchain.rb to carry the pre-publish SHA" >&2
|
|
221
|
+
exit 1
|
|
222
|
+
fi
|
|
223
|
+
COMMITTED_HOMEBREW_SHA="$(extract_formula_sha "$COMMITTED_FORMULA_TMP")"
|
|
224
|
+
rm -f "$COMMITTED_FORMULA_TMP"
|
|
225
|
+
if [[ -z "$COMMITTED_HOMEBREW_SHA" ]]; then
|
|
226
|
+
echo "FAIL: HEAD:cli/homebrew/agentxchain.rb does not contain a parseable sha256" >&2
|
|
227
|
+
exit 1
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
WORKTREE_HOMEBREW_SHA="$(extract_formula_sha "$HOMEBREW_MIRROR")"
|
|
231
|
+
if [[ -z "$WORKTREE_HOMEBREW_SHA" ]]; then
|
|
232
|
+
echo "FAIL: cli/homebrew/agentxchain.rb does not contain a parseable sha256" >&2
|
|
233
|
+
exit 1
|
|
234
|
+
fi
|
|
235
|
+
|
|
211
236
|
ESCAPED_URL="$(printf '%s' "$TARBALL_URL" | sed 's/[&/\]/\\&/g')"
|
|
237
|
+
ESCAPED_SHA="$(printf '%s' "$COMMITTED_HOMEBREW_SHA" | sed 's/[&/\]/\\&/g')"
|
|
212
238
|
sed -i.bak -E "s|^([[:space:]]*url \").*(\")|\1${ESCAPED_URL}\2|" "$HOMEBREW_MIRROR"
|
|
239
|
+
sed -i.bak -E "s|^([[:space:]]*sha256 \").*(\")|\1${ESCAPED_SHA}\2|" "$HOMEBREW_MIRROR"
|
|
213
240
|
rm -f "${HOMEBREW_MIRROR}.bak"
|
|
214
241
|
HOMEBREW_ALIGNED=true
|
|
215
242
|
echo " OK: formula URL -> ${TARBALL_URL}"
|
|
243
|
+
if [[ "$WORKTREE_HOMEBREW_SHA" != "$COMMITTED_HOMEBREW_SHA" ]]; then
|
|
244
|
+
echo " OK: formula SHA normalized back to committed pre-publish SHA ${COMMITTED_HOMEBREW_SHA}"
|
|
245
|
+
else
|
|
246
|
+
echo " OK: formula SHA carried from committed pre-publish SHA ${COMMITTED_HOMEBREW_SHA}"
|
|
247
|
+
fi
|
|
216
248
|
fi
|
|
217
249
|
|
|
218
250
|
if [[ -f "$HOMEBREW_MIRROR_README" ]]; then
|
|
@@ -223,7 +255,7 @@ if [[ -f "$HOMEBREW_MIRROR_README" ]]; then
|
|
|
223
255
|
fi
|
|
224
256
|
|
|
225
257
|
if $HOMEBREW_ALIGNED; then
|
|
226
|
-
echo " Note:
|
|
258
|
+
echo " Note: local npm pack output is not canonical release truth; sync-homebrew.sh will set the real registry SHA post-publish"
|
|
227
259
|
else
|
|
228
260
|
echo " Skipped: no Homebrew mirror files found"
|
|
229
261
|
fi
|
|
@@ -346,4 +378,7 @@ if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
|
|
|
346
378
|
echo " npm run preflight:release:strict -- --target-version ${TARGET_VERSION}"
|
|
347
379
|
fi
|
|
348
380
|
echo ""
|
|
381
|
+
echo "Homebrew mirror is in Phase 1 (stale SHA from previous version)."
|
|
382
|
+
echo "After npm publish completes, run sync-homebrew.sh to reach Phase 3."
|
|
383
|
+
echo ""
|
|
349
384
|
echo "Next: git push origin main --follow-tags"
|
|
@@ -166,6 +166,9 @@ export async function acceptTurnCommand(opts = {}) {
|
|
|
166
166
|
if (accepted?.cost?.usd != null) {
|
|
167
167
|
console.log(` ${chalk.dim('Cost:')} $${formatUsd(accepted.cost.usd)}`);
|
|
168
168
|
}
|
|
169
|
+
if (result.budget_warning) {
|
|
170
|
+
console.log(` ${chalk.yellow('Budget warning:')} ${result.budget_warning}`);
|
|
171
|
+
}
|
|
169
172
|
console.log('');
|
|
170
173
|
|
|
171
174
|
const recovery = deriveRecoveryDescriptor(result.state);
|
package/src/commands/config.js
CHANGED
|
@@ -17,6 +17,12 @@ export async function configCommand(opts) {
|
|
|
17
17
|
const config = rawConfig;
|
|
18
18
|
const configPath = join(root, CONFIG_FILE);
|
|
19
19
|
|
|
20
|
+
if (opts.get && opts.set) {
|
|
21
|
+
console.log(chalk.red(' --get and --set are mutually exclusive.'));
|
|
22
|
+
console.log(chalk.dim(' Inspect a value with `agentxchain config --get <path>` or change it with `agentxchain config --set <path> <value>`.'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
if (version === 4 && opts.addAgent) {
|
|
21
27
|
printLegacyOnlyMutationError('--add-agent');
|
|
22
28
|
return;
|
|
@@ -37,6 +43,11 @@ export async function configCommand(opts) {
|
|
|
37
43
|
return;
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
if (opts.get) {
|
|
47
|
+
getSetting(config, opts.get, { json: opts.json });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
if (opts.set) {
|
|
41
52
|
setSetting(config, configPath, opts.set, { version, root });
|
|
42
53
|
return;
|
|
@@ -81,6 +92,7 @@ function printLegacyConfig(config) {
|
|
|
81
92
|
console.log(chalk.dim(' Commands:'));
|
|
82
93
|
console.log(` ${chalk.bold('agentxchain config --add-agent')} Add a new agent`);
|
|
83
94
|
console.log(` ${chalk.bold('agentxchain config --remove-agent <id>')} Remove an agent`);
|
|
95
|
+
console.log(` ${chalk.bold('agentxchain config --get <key>')} Read one config value`);
|
|
84
96
|
console.log(` ${chalk.bold('agentxchain config --set <key> <val>')} Update a setting`);
|
|
85
97
|
console.log(` ${chalk.bold('agentxchain config --json')} Output as JSON`);
|
|
86
98
|
console.log('');
|
|
@@ -99,6 +111,7 @@ function printGovernedConfig(config) {
|
|
|
99
111
|
console.log(` ${chalk.dim('Runtimes:')} ${Object.keys(config.runtimes || {}).length}`);
|
|
100
112
|
console.log('');
|
|
101
113
|
console.log(chalk.dim(' Commands:'));
|
|
114
|
+
console.log(` ${chalk.bold('agentxchain config --get project.goal')} Read one config value without opening JSON`);
|
|
102
115
|
console.log(` ${chalk.bold('agentxchain config --set project.goal "Build a ..."')} Set mission context without hand-editing JSON`);
|
|
103
116
|
console.log(` ${chalk.bold('agentxchain config --set roles.qa.runtime manual-qa')} Switch a governed role runtime`);
|
|
104
117
|
console.log(` ${chalk.bold('agentxchain config --json')} Output raw config`);
|
|
@@ -169,10 +182,8 @@ function setSetting(config, configPath, keyValPair, context) {
|
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
const { key, rawVal } = parsed;
|
|
172
|
-
const segments = key
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (segments.some(segment => forbiddenKeys.has(segment))) {
|
|
185
|
+
const segments = parseKeyPath(key);
|
|
186
|
+
if (!segments) {
|
|
176
187
|
console.log(chalk.red(' Refusing to write reserved object path.'));
|
|
177
188
|
process.exit(1);
|
|
178
189
|
}
|
|
@@ -211,6 +222,35 @@ function setSetting(config, configPath, keyValPair, context) {
|
|
|
211
222
|
console.log('');
|
|
212
223
|
}
|
|
213
224
|
|
|
225
|
+
function getSetting(config, key, opts = {}) {
|
|
226
|
+
const segments = parseKeyPath(key);
|
|
227
|
+
if (!segments) {
|
|
228
|
+
console.log(chalk.red(' Refusing to read reserved object path.'));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let value = config;
|
|
233
|
+
for (const segment of segments) {
|
|
234
|
+
if (value === null || typeof value !== 'object' || !(segment in value)) {
|
|
235
|
+
console.log(chalk.red(` Config path not found: ${key}`));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
value = value[segment];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (opts.json) {
|
|
242
|
+
console.log(JSON.stringify(value, null, 2));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (value !== null && typeof value === 'object') {
|
|
247
|
+
console.log(JSON.stringify(value, null, 2));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(String(value));
|
|
252
|
+
}
|
|
253
|
+
|
|
214
254
|
function parseSetInput(input) {
|
|
215
255
|
if (Array.isArray(input)) {
|
|
216
256
|
if (input.length >= 2) {
|
|
@@ -235,6 +275,19 @@ function parseSetInput(input) {
|
|
|
235
275
|
return null;
|
|
236
276
|
}
|
|
237
277
|
|
|
278
|
+
function parseKeyPath(input) {
|
|
279
|
+
if (typeof input !== 'string' || input.trim() === '') {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const segments = input.split('.');
|
|
284
|
+
const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
285
|
+
if (segments.some(segment => segment === '' || forbiddenKeys.has(segment))) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return segments;
|
|
289
|
+
}
|
|
290
|
+
|
|
238
291
|
function validateEditedConfig(config, context) {
|
|
239
292
|
if (context.version === 4) {
|
|
240
293
|
return validateV4Config(config, context.root);
|
package/src/commands/restart.js
CHANGED
|
@@ -297,6 +297,9 @@ export async function restartCommand(opts) {
|
|
|
297
297
|
console.log(chalk.red(`Failed to assign turn: ${assignment.error}`));
|
|
298
298
|
process.exit(1);
|
|
299
299
|
}
|
|
300
|
+
for (const warning of assignment.warnings || []) {
|
|
301
|
+
console.log(chalk.yellow(`Warning: ${warning}`));
|
|
302
|
+
}
|
|
300
303
|
|
|
301
304
|
// assignGovernedTurn already writes a checkpoint at turn_assigned
|
|
302
305
|
|
package/src/commands/resume.js
CHANGED
|
@@ -259,6 +259,7 @@ export async function resumeCommand(opts) {
|
|
|
259
259
|
console.log(chalk.red(`Failed to assign turn: ${assignResult.error}`));
|
|
260
260
|
process.exit(1);
|
|
261
261
|
}
|
|
262
|
+
printAssignmentWarnings(assignResult);
|
|
262
263
|
state = assignResult.state;
|
|
263
264
|
|
|
264
265
|
// Write dispatch bundle
|
|
@@ -415,6 +416,12 @@ function printDispatchBundleWarnings(bundleResult) {
|
|
|
415
416
|
}
|
|
416
417
|
}
|
|
417
418
|
|
|
419
|
+
function printAssignmentWarnings(assignResult) {
|
|
420
|
+
for (const warning of assignResult.warnings || []) {
|
|
421
|
+
console.log(chalk.yellow(`Warning: ${warning}`));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
418
425
|
function printAssignmentHookFailure(result, roleId) {
|
|
419
426
|
const recovery = deriveRecoveryDescriptor(result.state);
|
|
420
427
|
const hookName = result.hookResults?.blocker?.hook_name
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadProjectContext } from '../lib/config.js';
|
|
3
|
+
|
|
4
|
+
export function roleCommand(subcommand, roleId, opts) {
|
|
5
|
+
const context = loadProjectContext();
|
|
6
|
+
if (!context) {
|
|
7
|
+
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { rawConfig, version } = context;
|
|
12
|
+
|
|
13
|
+
if (version !== 4) {
|
|
14
|
+
console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const roles = rawConfig.roles || {};
|
|
19
|
+
const roleIds = Object.keys(roles);
|
|
20
|
+
|
|
21
|
+
if (subcommand === 'show') {
|
|
22
|
+
return showRole(roleId, roles, roleIds, opts);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Default: list
|
|
26
|
+
return listRoles(roles, roleIds, opts);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function listRoles(roles, roleIds, opts) {
|
|
30
|
+
if (roleIds.length === 0) {
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
console.log('[]');
|
|
33
|
+
} else {
|
|
34
|
+
console.log(' No roles defined.');
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
const output = roleIds.map((id) => ({
|
|
41
|
+
id,
|
|
42
|
+
title: roles[id].title,
|
|
43
|
+
mandate: roles[id].mandate,
|
|
44
|
+
write_authority: roles[id].write_authority,
|
|
45
|
+
runtime: roles[id].runtime,
|
|
46
|
+
}));
|
|
47
|
+
console.log(JSON.stringify(output, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.bold(`\n Roles (${roleIds.length}):\n`));
|
|
52
|
+
for (const id of roleIds) {
|
|
53
|
+
const r = roles[id];
|
|
54
|
+
const authority = r.write_authority === 'authoritative'
|
|
55
|
+
? chalk.green(r.write_authority)
|
|
56
|
+
: r.write_authority === 'proposed'
|
|
57
|
+
? chalk.yellow(r.write_authority)
|
|
58
|
+
: chalk.dim(r.write_authority);
|
|
59
|
+
console.log(` ${chalk.cyan(id)} — ${r.title} [${authority}] → ${chalk.dim(r.runtime)}`);
|
|
60
|
+
}
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log(chalk.dim(' Usage: agentxchain role show <role_id>\n'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function showRole(roleId, roles, roleIds, opts) {
|
|
66
|
+
if (!roleId) {
|
|
67
|
+
console.log(chalk.red(' Missing role ID.'));
|
|
68
|
+
console.log(chalk.dim(` Usage: agentxchain role show <role_id>`));
|
|
69
|
+
if (roleIds.length > 0) {
|
|
70
|
+
console.log(chalk.dim(` Available: ${roleIds.join(', ')}`));
|
|
71
|
+
}
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!roles[roleId]) {
|
|
76
|
+
console.log(chalk.red(` Unknown role: ${roleId}`));
|
|
77
|
+
console.log(chalk.dim(` Available: ${roleIds.join(', ')}`));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const r = roles[roleId];
|
|
82
|
+
|
|
83
|
+
if (opts.json) {
|
|
84
|
+
console.log(JSON.stringify({
|
|
85
|
+
id: roleId,
|
|
86
|
+
title: r.title,
|
|
87
|
+
mandate: r.mandate,
|
|
88
|
+
write_authority: r.write_authority,
|
|
89
|
+
runtime: r.runtime,
|
|
90
|
+
}, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const authority = r.write_authority === 'authoritative'
|
|
95
|
+
? chalk.green(r.write_authority)
|
|
96
|
+
: r.write_authority === 'proposed'
|
|
97
|
+
? chalk.yellow(r.write_authority)
|
|
98
|
+
: chalk.dim(r.write_authority);
|
|
99
|
+
|
|
100
|
+
console.log(chalk.bold(`\n Role: ${chalk.cyan(roleId)}\n`));
|
|
101
|
+
console.log(` Title: ${r.title}`);
|
|
102
|
+
console.log(` Mandate: ${r.mandate}`);
|
|
103
|
+
console.log(` Authority: ${authority}`);
|
|
104
|
+
console.log(` Runtime: ${chalk.dim(r.runtime)}`);
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -277,7 +277,10 @@ function renderGovernedStatus(context, opts) {
|
|
|
277
277
|
|
|
278
278
|
if (state?.budget_status) {
|
|
279
279
|
console.log('');
|
|
280
|
-
|
|
280
|
+
const budgetLabel = state.budget_status.warn_mode
|
|
281
|
+
? `spent $${formatUsd(state.budget_status.spent_usd)} / remaining $${formatUsd(state.budget_status.remaining_usd)} ${chalk.yellow('[OVER BUDGET]')}`
|
|
282
|
+
: `spent $${formatUsd(state.budget_status.spent_usd)} / remaining $${formatUsd(state.budget_status.remaining_usd)}`;
|
|
283
|
+
console.log(` ${chalk.dim('Budget:')} ${budgetLabel}`);
|
|
281
284
|
}
|
|
282
285
|
|
|
283
286
|
console.log('');
|
package/src/commands/step.js
CHANGED
|
@@ -293,6 +293,7 @@ export async function stepCommand(opts) {
|
|
|
293
293
|
console.log(chalk.red(`Failed to assign turn: ${assignResult.error}`));
|
|
294
294
|
process.exit(1);
|
|
295
295
|
}
|
|
296
|
+
printAssignmentWarnings(assignResult);
|
|
296
297
|
state = assignResult.state;
|
|
297
298
|
|
|
298
299
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
@@ -948,6 +949,12 @@ function printDispatchBundleWarnings(bundleResult) {
|
|
|
948
949
|
}
|
|
949
950
|
}
|
|
950
951
|
|
|
952
|
+
function printAssignmentWarnings(assignResult) {
|
|
953
|
+
for (const warning of assignResult.warnings || []) {
|
|
954
|
+
console.log(chalk.yellow(`Warning: ${warning}`));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
951
958
|
function printAssignmentHookFailure(result, roleId) {
|
|
952
959
|
const recovery = deriveRecoveryDescriptor(result.state);
|
|
953
960
|
const hookName = result.hookResults?.blocker?.hook_name
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
5
|
+
import { getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
|
|
6
|
+
import {
|
|
7
|
+
getDispatchAssignmentPath,
|
|
8
|
+
getDispatchContextPath,
|
|
9
|
+
getDispatchManifestPath,
|
|
10
|
+
getDispatchPromptPath,
|
|
11
|
+
getDispatchTurnDir,
|
|
12
|
+
} from '../lib/turn-paths.js';
|
|
13
|
+
|
|
14
|
+
const ARTIFACTS = {
|
|
15
|
+
assignment: {
|
|
16
|
+
label: 'Assignment',
|
|
17
|
+
pathFor: getDispatchAssignmentPath,
|
|
18
|
+
parse: 'json',
|
|
19
|
+
},
|
|
20
|
+
prompt: {
|
|
21
|
+
label: 'Prompt',
|
|
22
|
+
pathFor: getDispatchPromptPath,
|
|
23
|
+
parse: 'text',
|
|
24
|
+
},
|
|
25
|
+
context: {
|
|
26
|
+
label: 'Context',
|
|
27
|
+
pathFor: getDispatchContextPath,
|
|
28
|
+
parse: 'text',
|
|
29
|
+
},
|
|
30
|
+
manifest: {
|
|
31
|
+
label: 'Manifest',
|
|
32
|
+
pathFor: getDispatchManifestPath,
|
|
33
|
+
parse: 'json',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function turnShowCommand(turnId, opts) {
|
|
38
|
+
const context = requireGovernedContext();
|
|
39
|
+
const state = loadProjectState(context.root, context.config);
|
|
40
|
+
|
|
41
|
+
if (!state) {
|
|
42
|
+
console.log(chalk.red(' Governed state is missing or invalid.'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const activeTurns = getActiveTurns(state);
|
|
47
|
+
const selectedTurnId = resolveTurnId(turnId, activeTurns);
|
|
48
|
+
const turn = activeTurns[selectedTurnId];
|
|
49
|
+
const artifacts = buildArtifactIndex(context.root, selectedTurnId);
|
|
50
|
+
const assignment = readJsonArtifact(artifacts.assignment.absPath);
|
|
51
|
+
|
|
52
|
+
if (opts.artifact) {
|
|
53
|
+
return printArtifact(selectedTurnId, turn, artifacts, opts.artifact, opts.json);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
console.log(JSON.stringify(buildTurnPayload(selectedTurnId, turn, state, artifacts, assignment), null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
printTurnSummary(selectedTurnId, turn, state, artifacts, assignment);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function requireGovernedContext() {
|
|
65
|
+
const context = loadProjectContext();
|
|
66
|
+
if (!context) {
|
|
67
|
+
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
if (context.config.protocol_mode !== 'governed') {
|
|
71
|
+
console.log(chalk.red(' The turn command is only available for governed projects.'));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
return context;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveTurnId(requestedTurnId, activeTurns) {
|
|
78
|
+
const turnIds = Object.keys(activeTurns);
|
|
79
|
+
if (requestedTurnId) {
|
|
80
|
+
if (!activeTurns[requestedTurnId]) {
|
|
81
|
+
console.log(chalk.red(` Unknown active turn: ${requestedTurnId}`));
|
|
82
|
+
if (turnIds.length > 0) {
|
|
83
|
+
console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
|
|
84
|
+
}
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return requestedTurnId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (turnIds.length === 0) {
|
|
91
|
+
console.log(chalk.red(' No active turn found.'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (turnIds.length > 1) {
|
|
96
|
+
console.log(chalk.red(' Multiple active turns are present. Re-run with `agentxchain turn show <turn_id>`.'));
|
|
97
|
+
console.log(chalk.dim(` Available: ${turnIds.join(', ')}`));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return turnIds[0];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildArtifactIndex(root, turnId) {
|
|
105
|
+
return Object.fromEntries(
|
|
106
|
+
Object.entries(ARTIFACTS).map(([id, artifact]) => {
|
|
107
|
+
const relPath = artifact.pathFor(turnId);
|
|
108
|
+
const absPath = join(root, relPath);
|
|
109
|
+
return [id, {
|
|
110
|
+
id,
|
|
111
|
+
label: artifact.label,
|
|
112
|
+
relPath,
|
|
113
|
+
absPath,
|
|
114
|
+
exists: existsSync(absPath),
|
|
115
|
+
parse: artifact.parse,
|
|
116
|
+
}];
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildTurnPayload(turnId, turn, state, artifacts, assignment) {
|
|
122
|
+
return {
|
|
123
|
+
turn_id: turnId,
|
|
124
|
+
run_id: state.run_id || assignment?.run_id || null,
|
|
125
|
+
phase: state.phase || assignment?.phase || null,
|
|
126
|
+
role: turn.assigned_role,
|
|
127
|
+
runtime: turn.runtime_id,
|
|
128
|
+
status: turn.status,
|
|
129
|
+
attempt: turn.attempt,
|
|
130
|
+
dispatch_dir: getDispatchTurnDir(turnId),
|
|
131
|
+
staging_result_path: assignment?.staging_result_path || null,
|
|
132
|
+
active_turn_count: getActiveTurnCount(state),
|
|
133
|
+
artifacts: Object.fromEntries(
|
|
134
|
+
Object.entries(artifacts).map(([id, artifact]) => [id, {
|
|
135
|
+
path: artifact.relPath,
|
|
136
|
+
exists: artifact.exists,
|
|
137
|
+
}]),
|
|
138
|
+
),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printTurnSummary(turnId, turn, state, artifacts, assignment) {
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(chalk.bold(` Turn: ${chalk.cyan(turnId)}`));
|
|
145
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
146
|
+
console.log(` ${chalk.dim('Run:')} ${state.run_id || assignment?.run_id || chalk.dim('unknown')}`);
|
|
147
|
+
console.log(` ${chalk.dim('Phase:')} ${state.phase || assignment?.phase || chalk.dim('unknown')}`);
|
|
148
|
+
console.log(` ${chalk.dim('Role:')} ${chalk.bold(turn.assigned_role)}`);
|
|
149
|
+
console.log(` ${chalk.dim('Runtime:')} ${turn.runtime_id}`);
|
|
150
|
+
console.log(` ${chalk.dim('Status:')} ${turn.status}`);
|
|
151
|
+
console.log(` ${chalk.dim('Attempt:')} ${turn.attempt}`);
|
|
152
|
+
console.log(` ${chalk.dim('Dispatch:')} ${getDispatchTurnDir(turnId)}`);
|
|
153
|
+
if (assignment?.staging_result_path) {
|
|
154
|
+
console.log(` ${chalk.dim('Staging:')} ${assignment.staging_result_path}`);
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(` ${chalk.dim('Artifacts:')}`);
|
|
158
|
+
for (const artifact of Object.values(artifacts)) {
|
|
159
|
+
const marker = artifact.exists ? chalk.green('exists') : chalk.red('missing');
|
|
160
|
+
console.log(` ${artifact.label.padEnd(10)} ${marker} ${artifact.relPath}`);
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(chalk.dim(` View one: agentxchain turn show ${turnId} --artifact prompt`));
|
|
164
|
+
console.log('');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function printArtifact(turnId, turn, artifacts, artifactId, jsonMode) {
|
|
168
|
+
const artifact = artifacts[artifactId];
|
|
169
|
+
if (!artifact) {
|
|
170
|
+
console.log(chalk.red(` Unknown artifact: ${artifactId}`));
|
|
171
|
+
console.log(chalk.dim(` Allowed: ${Object.keys(ARTIFACTS).join(', ')}`));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (!artifact.exists) {
|
|
175
|
+
console.log(chalk.red(` Missing artifact: ${artifact.relPath}`));
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const raw = readFileSync(artifact.absPath, 'utf8');
|
|
180
|
+
if (!jsonMode) {
|
|
181
|
+
process.stdout.write(raw.endsWith('\n') ? raw : `${raw}\n`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const content = artifact.parse === 'json'
|
|
186
|
+
? JSON.parse(raw)
|
|
187
|
+
: raw;
|
|
188
|
+
|
|
189
|
+
console.log(JSON.stringify({
|
|
190
|
+
turn_id: turnId,
|
|
191
|
+
role: turn.assigned_role,
|
|
192
|
+
runtime: turn.runtime_id,
|
|
193
|
+
artifact: {
|
|
194
|
+
id: artifact.id,
|
|
195
|
+
path: artifact.relPath,
|
|
196
|
+
content,
|
|
197
|
+
},
|
|
198
|
+
}, null, 2));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readJsonArtifact(absPath) {
|
|
202
|
+
if (!existsSync(absPath)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(readFileSync(absPath, 'utf8'));
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -1936,13 +1936,19 @@ export function assignGovernedTurn(root, config, roleId) {
|
|
|
1936
1936
|
return { ok: false, error: `Cannot assign turn: ${activeCount} active turn(s) already at capacity (max_concurrent_turns = ${maxConcurrent})` };
|
|
1937
1937
|
}
|
|
1938
1938
|
|
|
1939
|
-
// DEC-BUDGET-ENFORCE-001: Pre-assignment budget exhaustion guard
|
|
1940
|
-
if (state.budget_status?.remaining_usd != null && state.budget_status.remaining_usd <= 0) {
|
|
1941
|
-
return { ok: false, error: `Cannot assign turn: run budget exhausted (spent $${(state.budget_status.spent_usd || 0).toFixed(2)} of $${((state.budget_status.spent_usd || 0) + state.budget_status.remaining_usd).toFixed(2)} limit). Increase budget with agentxchain config --set budget.per_run_max_usd <usd>, then run agentxchain resume` };
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
1939
|
// DEC-PARALLEL-011: Budget reservation
|
|
1945
1940
|
const warnings = [];
|
|
1941
|
+
|
|
1942
|
+
// DEC-BUDGET-ENFORCE-001 + DEC-BUDGET-WARN-001: Pre-assignment budget exhaustion guard
|
|
1943
|
+
if (state.budget_status?.remaining_usd != null && state.budget_status.remaining_usd <= 0) {
|
|
1944
|
+
const onExceed = config.budget?.on_exceed || 'pause_and_escalate';
|
|
1945
|
+
if (onExceed === 'warn') {
|
|
1946
|
+
// Allow assignment but add a warning
|
|
1947
|
+
warnings.push(`Budget exhausted (spent $${(state.budget_status.spent_usd || 0).toFixed(2)} of $${((state.budget_status.spent_usd || 0) + state.budget_status.remaining_usd).toFixed(2)} limit). Run continues in warn mode per on_exceed policy.`);
|
|
1948
|
+
} else {
|
|
1949
|
+
return { ok: false, error: `Cannot assign turn: run budget exhausted (spent $${(state.budget_status.spent_usd || 0).toFixed(2)} of $${((state.budget_status.spent_usd || 0) + state.budget_status.remaining_usd).toFixed(2)} limit). Increase budget with agentxchain config --set budget.per_run_max_usd <usd>, then run agentxchain resume` };
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1946
1952
|
const reservations = { ...(state.budget_reservations || {}) };
|
|
1947
1953
|
const turnId = generateId('turn');
|
|
1948
1954
|
const estimatedCost = estimateTurnBudget(config, roleId);
|
|
@@ -1950,7 +1956,8 @@ export function assignGovernedTurn(root, config, roleId) {
|
|
|
1950
1956
|
if (estimatedCost > 0 && state.budget_status?.remaining_usd != null) {
|
|
1951
1957
|
const alreadyReserved = Object.values(reservations).reduce((sum, r) => sum + (r.reserved_usd || 0), 0);
|
|
1952
1958
|
const available = state.budget_status.remaining_usd - alreadyReserved;
|
|
1953
|
-
|
|
1959
|
+
const onExceedReserve = config.budget?.on_exceed || 'pause_and_escalate';
|
|
1960
|
+
if (estimatedCost > available && onExceedReserve !== 'warn') {
|
|
1954
1961
|
return { ok: false, error: `Cannot assign turn: estimated cost $${estimatedCost.toFixed(2)} exceeds available budget $${available.toFixed(2)} (after reservations)` };
|
|
1955
1962
|
}
|
|
1956
1963
|
reservations[turnId] = {
|
|
@@ -2617,6 +2624,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2617
2624
|
accepted_integration_ref: derivedRef,
|
|
2618
2625
|
next_recommended_role: deriveNextRecommendedRole(turnResult, state, config),
|
|
2619
2626
|
budget_status: {
|
|
2627
|
+
...(state.budget_status || {}),
|
|
2620
2628
|
spent_usd: (state.budget_status?.spent_usd || 0) + costUsd,
|
|
2621
2629
|
remaining_usd: state.budget_status?.remaining_usd != null
|
|
2622
2630
|
? state.budget_status.remaining_usd - costUsd
|
|
@@ -2657,7 +2665,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2657
2665
|
if (turnReservation && costUsd > turnReservation.reserved_usd) {
|
|
2658
2666
|
budgetWarning = `Actual cost $${costUsd.toFixed(2)} exceeded reservation $${turnReservation.reserved_usd.toFixed(2)} for this turn`;
|
|
2659
2667
|
}
|
|
2660
|
-
// Budget exhaustion enforcement
|
|
2668
|
+
// Budget exhaustion enforcement (DEC-BUDGET-ENFORCE-001 + DEC-BUDGET-WARN-001)
|
|
2661
2669
|
if (
|
|
2662
2670
|
updatedState.budget_status.remaining_usd != null &&
|
|
2663
2671
|
updatedState.budget_status.remaining_usd <= 0 &&
|
|
@@ -2665,9 +2673,9 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2665
2673
|
updatedState.status !== 'completed'
|
|
2666
2674
|
) {
|
|
2667
2675
|
const onExceed = config.budget?.on_exceed || 'pause_and_escalate';
|
|
2676
|
+
const limit = (updatedState.budget_status.spent_usd + updatedState.budget_status.remaining_usd);
|
|
2677
|
+
const overBy = Math.abs(updatedState.budget_status.remaining_usd);
|
|
2668
2678
|
if (onExceed === 'pause_and_escalate') {
|
|
2669
|
-
const limit = (updatedState.budget_status.spent_usd + updatedState.budget_status.remaining_usd);
|
|
2670
|
-
const overBy = Math.abs(updatedState.budget_status.remaining_usd);
|
|
2671
2679
|
updatedState.status = 'blocked';
|
|
2672
2680
|
updatedState.blocked_on = 'budget:exhausted';
|
|
2673
2681
|
updatedState.blocked_reason = buildBlockedReason({
|
|
@@ -2685,6 +2693,15 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2685
2693
|
updatedState.budget_status.exhausted = true;
|
|
2686
2694
|
updatedState.budget_status.exhausted_at = now;
|
|
2687
2695
|
updatedState.budget_status.exhausted_after_turn = currentTurn.turn_id;
|
|
2696
|
+
} else if (onExceed === 'warn') {
|
|
2697
|
+
// DEC-BUDGET-WARN-001: Do not block — mark exhaustion and emit warning
|
|
2698
|
+
if (!updatedState.budget_status.exhausted) {
|
|
2699
|
+
updatedState.budget_status.exhausted = true;
|
|
2700
|
+
updatedState.budget_status.exhausted_at = now;
|
|
2701
|
+
updatedState.budget_status.exhausted_after_turn = currentTurn.turn_id;
|
|
2702
|
+
}
|
|
2703
|
+
updatedState.budget_status.warn_mode = true;
|
|
2704
|
+
budgetWarning = `Budget exhausted: spent $${updatedState.budget_status.spent_usd.toFixed(2)} of $${limit.toFixed(2)} limit ($${overBy.toFixed(2)} over). Run continues in warn mode.`;
|
|
2688
2705
|
}
|
|
2689
2706
|
}
|
|
2690
2707
|
|
|
@@ -3081,6 +3098,22 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3081
3098
|
});
|
|
3082
3099
|
}
|
|
3083
3100
|
|
|
3101
|
+
// DEC-BUDGET-WARN-001: Emit budget_exceeded_warn event when warn mode triggers
|
|
3102
|
+
if (updatedState.budget_status?.warn_mode && budgetWarning) {
|
|
3103
|
+
emitRunEvent(root, 'budget_exceeded_warn', {
|
|
3104
|
+
run_id: updatedState.run_id,
|
|
3105
|
+
phase: updatedState.phase,
|
|
3106
|
+
status: updatedState.status,
|
|
3107
|
+
turn: { turn_id: currentTurn.turn_id, role_id: currentTurn.assigned_role },
|
|
3108
|
+
payload: {
|
|
3109
|
+
spent_usd: updatedState.budget_status.spent_usd,
|
|
3110
|
+
limit_usd: updatedState.budget_status.spent_usd + updatedState.budget_status.remaining_usd,
|
|
3111
|
+
remaining_usd: updatedState.budget_status.remaining_usd,
|
|
3112
|
+
warning: budgetWarning,
|
|
3113
|
+
},
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3084
3117
|
if (updatedState.pending_phase_transition) {
|
|
3085
3118
|
emitPendingLifecycleNotification(root, config, updatedState, 'phase_transition_pending', {
|
|
3086
3119
|
from: updatedState.pending_phase_transition.from,
|
|
@@ -61,7 +61,7 @@ const VALID_API_PROXY_PREFLIGHT_FIELDS = [
|
|
|
61
61
|
'tokenizer',
|
|
62
62
|
'safety_margin_tokens',
|
|
63
63
|
];
|
|
64
|
-
const VALID_BUDGET_ON_EXCEED = ['pause_and_escalate'];
|
|
64
|
+
const VALID_BUDGET_ON_EXCEED = ['pause_and_escalate', 'warn'];
|
|
65
65
|
|
|
66
66
|
function validateMcpRuntime(runtimeId, runtime, errors) {
|
|
67
67
|
const transport = typeof runtime?.transport === 'string' && runtime.transport.trim()
|
|
@@ -583,7 +583,7 @@ export function validateBudgetConfig(budget) {
|
|
|
583
583
|
|
|
584
584
|
if (budget.on_exceed !== undefined) {
|
|
585
585
|
if (typeof budget.on_exceed !== 'string' || !VALID_BUDGET_ON_EXCEED.includes(budget.on_exceed)) {
|
|
586
|
-
errors.push(`budget.on_exceed must be one of: ${VALID_BUDGET_ON_EXCEED.join(', ')}
|
|
586
|
+
errors.push(`budget.on_exceed must be one of: ${VALID_BUDGET_ON_EXCEED.join(', ')}`);
|
|
587
587
|
}
|
|
588
588
|
}
|
|
589
589
|
|
package/src/lib/report.js
CHANGED
|
@@ -45,6 +45,19 @@ function normalizeBudgetStatus(budgetStatus) {
|
|
|
45
45
|
if (Number.isFinite(budgetStatus.remaining_usd)) {
|
|
46
46
|
normalized.remaining_usd = budgetStatus.remaining_usd;
|
|
47
47
|
}
|
|
48
|
+
// DEC-BUDGET-WARN-004: preserve warn-mode and exhaustion fields
|
|
49
|
+
if (budgetStatus.warn_mode === true) {
|
|
50
|
+
normalized.warn_mode = true;
|
|
51
|
+
}
|
|
52
|
+
if (budgetStatus.exhausted === true) {
|
|
53
|
+
normalized.exhausted = true;
|
|
54
|
+
}
|
|
55
|
+
if (budgetStatus.exhausted_at) {
|
|
56
|
+
normalized.exhausted_at = budgetStatus.exhausted_at;
|
|
57
|
+
}
|
|
58
|
+
if (budgetStatus.exhausted_after_turn) {
|
|
59
|
+
normalized.exhausted_after_turn = budgetStatus.exhausted_after_turn;
|
|
60
|
+
}
|
|
48
61
|
|
|
49
62
|
return Object.keys(normalized).length > 0 ? normalized : null;
|
|
50
63
|
}
|
|
@@ -890,8 +903,9 @@ export function formatGovernanceReportText(report) {
|
|
|
890
903
|
];
|
|
891
904
|
|
|
892
905
|
if (run.budget_status) {
|
|
906
|
+
const warnTag = run.budget_status.warn_mode ? ' [OVER BUDGET]' : '';
|
|
893
907
|
lines.push(
|
|
894
|
-
`Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}`,
|
|
908
|
+
`Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}${warnTag}`,
|
|
895
909
|
);
|
|
896
910
|
}
|
|
897
911
|
|
|
@@ -1300,7 +1314,8 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1300
1314
|
];
|
|
1301
1315
|
|
|
1302
1316
|
if (run.budget_status) {
|
|
1303
|
-
|
|
1317
|
+
const warnTag = run.budget_status.warn_mode ? ' **[OVER BUDGET]**' : '';
|
|
1318
|
+
lines.push(`- Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}${warnTag}`);
|
|
1304
1319
|
}
|
|
1305
1320
|
|
|
1306
1321
|
if (run.created_at) {
|