agentxchain 2.59.0 → 2.61.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/README.md +1 -1
- package/bin/agentxchain.js +1 -1
- package/package.json +1 -1
- package/src/commands/config.js +87 -14
- package/src/commands/init.js +2 -1
- package/src/lib/governed-state.js +3 -3
- package/src/lib/normalized-config.js +81 -0
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ agentxchain status
|
|
|
74
74
|
agentxchain step --role pm
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
If you skipped `--goal` during scaffold, set
|
|
77
|
+
If you skipped `--goal` during scaffold, run `agentxchain config --set project.goal "Build an API change planner for release teams"` before the first governed turn instead of re-running init in place.
|
|
78
78
|
|
|
79
79
|
The default governed dev runtime is `claude --print --dangerously-skip-permissions` with stdin prompt delivery. The non-interactive governed path needs write access, so do not pretend bare `claude --print` is sufficient for unattended implementation turns. If your local coding agent uses a different launch contract, set it during scaffold creation:
|
|
80
80
|
|
package/bin/agentxchain.js
CHANGED
|
@@ -192,7 +192,7 @@ program
|
|
|
192
192
|
.description('View or edit project configuration')
|
|
193
193
|
.option('--add-agent', 'Add a new agent interactively')
|
|
194
194
|
.option('--remove-agent <id>', 'Remove an agent by ID')
|
|
195
|
-
.option('--set <
|
|
195
|
+
.option('--set <path_and_value...>', 'Set a config value (e.g. --set project.goal "Build a governed CLI")')
|
|
196
196
|
.option('-j, --json', 'Output config as JSON')
|
|
197
197
|
.action(configCommand);
|
|
198
198
|
|
package/package.json
CHANGED
package/src/commands/config.js
CHANGED
|
@@ -2,31 +2,43 @@ import { readFileSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
-
import {
|
|
5
|
+
import { loadProjectContext, CONFIG_FILE } from '../lib/config.js';
|
|
6
6
|
import { validateConfigSchema } from '../lib/schema.js';
|
|
7
|
+
import { validateV4Config } from '../lib/normalized-config.js';
|
|
7
8
|
|
|
8
9
|
export async function configCommand(opts) {
|
|
9
|
-
const
|
|
10
|
-
if (!
|
|
10
|
+
const context = loadProjectContext();
|
|
11
|
+
if (!context) {
|
|
11
12
|
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
const { root,
|
|
16
|
+
const { root, rawConfig, version } = context;
|
|
17
|
+
const config = rawConfig;
|
|
16
18
|
const configPath = join(root, CONFIG_FILE);
|
|
17
19
|
|
|
20
|
+
if (version === 4 && opts.addAgent) {
|
|
21
|
+
printLegacyOnlyMutationError('--add-agent');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
if (opts.addAgent) {
|
|
19
26
|
await addAgent(config, configPath);
|
|
20
27
|
return;
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
if (version === 4 && opts.removeAgent) {
|
|
31
|
+
printLegacyOnlyMutationError('--remove-agent');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
if (opts.removeAgent) {
|
|
24
36
|
removeAgent(config, configPath, opts.removeAgent);
|
|
25
37
|
return;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
if (opts.set) {
|
|
29
|
-
setSetting(config, configPath, opts.set);
|
|
41
|
+
setSetting(config, configPath, opts.set, { version, root });
|
|
30
42
|
return;
|
|
31
43
|
}
|
|
32
44
|
|
|
@@ -35,10 +47,15 @@ export async function configCommand(opts) {
|
|
|
35
47
|
return;
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
if (version === 4) {
|
|
51
|
+
printGovernedConfig(config);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
printLegacyConfig(config);
|
|
39
56
|
}
|
|
40
57
|
|
|
41
|
-
function
|
|
58
|
+
function printLegacyConfig(config) {
|
|
42
59
|
console.log('');
|
|
43
60
|
console.log(chalk.bold(' AgentXchain Config'));
|
|
44
61
|
console.log(chalk.dim(' ' + '─'.repeat(40)));
|
|
@@ -69,6 +86,32 @@ function printConfig(config) {
|
|
|
69
86
|
console.log('');
|
|
70
87
|
}
|
|
71
88
|
|
|
89
|
+
function printGovernedConfig(config) {
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(chalk.bold(' AgentXchain Governed Config'));
|
|
92
|
+
console.log(chalk.dim(' ' + '─'.repeat(40)));
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(` ${chalk.dim('Project:')} ${config.project?.name || '(unknown)'}`);
|
|
95
|
+
console.log(` ${chalk.dim('Project ID:')} ${config.project?.id || '(unknown)'}`);
|
|
96
|
+
console.log(` ${chalk.dim('Goal:')} ${config.project?.goal || '(not set)'}`);
|
|
97
|
+
console.log(` ${chalk.dim('Template:')} ${config.template || 'generic'}`);
|
|
98
|
+
console.log(` ${chalk.dim('Roles:')} ${Object.keys(config.roles || {}).length}`);
|
|
99
|
+
console.log(` ${chalk.dim('Runtimes:')} ${Object.keys(config.runtimes || {}).length}`);
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(chalk.dim(' Commands:'));
|
|
102
|
+
console.log(` ${chalk.bold('agentxchain config --set project.goal "Build a ..."')} Set mission context without hand-editing JSON`);
|
|
103
|
+
console.log(` ${chalk.bold('agentxchain config --set roles.qa.runtime manual-qa')} Switch a governed role runtime`);
|
|
104
|
+
console.log(` ${chalk.bold('agentxchain config --json')} Output raw config`);
|
|
105
|
+
console.log('');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function printLegacyOnlyMutationError(flag) {
|
|
109
|
+
console.log(chalk.red(` ${flag} is legacy-only.`));
|
|
110
|
+
console.log(chalk.dim(' Governed repos use roles and runtimes instead of legacy v3 agents.'));
|
|
111
|
+
console.log(chalk.dim(' Use `agentxchain config --set <path> <value>` for governed config changes.'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
72
115
|
async function addAgent(config, configPath) {
|
|
73
116
|
const answers = await inquirer.prompt([
|
|
74
117
|
{
|
|
@@ -117,16 +160,15 @@ function removeAgent(config, configPath, id) {
|
|
|
117
160
|
console.log('');
|
|
118
161
|
}
|
|
119
162
|
|
|
120
|
-
function setSetting(config, configPath, keyValPair) {
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
163
|
+
function setSetting(config, configPath, keyValPair, context) {
|
|
164
|
+
const parsed = parseSetInput(keyValPair);
|
|
165
|
+
if (!parsed) {
|
|
123
166
|
console.log(chalk.red(' Usage: agentxchain config --set <key> <value>'));
|
|
124
|
-
console.log(chalk.dim(' Example: agentxchain config --set
|
|
167
|
+
console.log(chalk.dim(' Example: agentxchain config --set project.goal "Build a governed CLI"'));
|
|
125
168
|
process.exit(1);
|
|
126
169
|
}
|
|
127
170
|
|
|
128
|
-
const key =
|
|
129
|
-
const rawVal = parts.slice(1).join(' ');
|
|
171
|
+
const { key, rawVal } = parsed;
|
|
130
172
|
const segments = key.split('.');
|
|
131
173
|
const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
132
174
|
|
|
@@ -152,7 +194,7 @@ function setSetting(config, configPath, keyValPair) {
|
|
|
152
194
|
else if (!isNaN(rawVal) && rawVal !== '') val = Number(rawVal);
|
|
153
195
|
|
|
154
196
|
target[lastKey] = val;
|
|
155
|
-
const validation =
|
|
197
|
+
const validation = validateEditedConfig(config, context);
|
|
156
198
|
if (!validation.ok) {
|
|
157
199
|
target[lastKey] = oldVal;
|
|
158
200
|
if (oldVal === undefined) {
|
|
@@ -168,3 +210,34 @@ function setSetting(config, configPath, keyValPair) {
|
|
|
168
210
|
if (oldVal !== undefined) console.log(chalk.dim(` (was: ${oldVal})`));
|
|
169
211
|
console.log('');
|
|
170
212
|
}
|
|
213
|
+
|
|
214
|
+
function parseSetInput(input) {
|
|
215
|
+
if (Array.isArray(input)) {
|
|
216
|
+
if (input.length >= 2) {
|
|
217
|
+
return { key: input[0], rawVal: input.slice(1).join(' ') };
|
|
218
|
+
}
|
|
219
|
+
if (input.length === 1 && typeof input[0] === 'string') {
|
|
220
|
+
const parts = input[0].trim().split(/\s+/);
|
|
221
|
+
if (parts.length >= 2) {
|
|
222
|
+
return { key: parts[0], rawVal: parts.slice(1).join(' ') };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof input === 'string') {
|
|
229
|
+
const parts = input.trim().split(/\s+/);
|
|
230
|
+
if (parts.length >= 2) {
|
|
231
|
+
return { key: parts[0], rawVal: parts.slice(1).join(' ') };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function validateEditedConfig(config, context) {
|
|
239
|
+
if (context.version === 4) {
|
|
240
|
+
return validateV4Config(config, context.root);
|
|
241
|
+
}
|
|
242
|
+
return validateConfigSchema(config);
|
|
243
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -978,7 +978,8 @@ async function initGoverned(opts) {
|
|
|
978
978
|
console.log('');
|
|
979
979
|
if (!config?.project?.goal) {
|
|
980
980
|
console.log(` ${chalk.dim('Tip:')} Add a project goal to guide agent context:`);
|
|
981
|
-
console.log(` ${chalk.bold('agentxchain init --governed --goal "Build a ..."')} ${chalk.dim('#
|
|
981
|
+
console.log(` ${chalk.bold('agentxchain init --governed --goal "Build a ..."')} ${chalk.dim('# preferred during scaffold')}`);
|
|
982
|
+
console.log(` ${chalk.bold('agentxchain config --set project.goal "Build a ..."')} ${chalk.dim('# add it later without hand-editing JSON')}`);
|
|
982
983
|
console.log('');
|
|
983
984
|
}
|
|
984
985
|
console.log(` ${chalk.dim('Guide:')} https://agentxchain.dev/docs/getting-started`);
|
|
@@ -575,7 +575,7 @@ function attachLegacyCurrentTurnAlias(state) {
|
|
|
575
575
|
function formatBudgetRecoveryAction(isReadyToResume) {
|
|
576
576
|
return isReadyToResume
|
|
577
577
|
? 'Run agentxchain resume to assign the next turn'
|
|
578
|
-
: 'Increase
|
|
578
|
+
: 'Increase budget with agentxchain config --set budget.per_run_max_usd <usd>, then run agentxchain resume';
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
function formatBudgetRecoveryDetail(spentUsd, limitUsd, remainingUsd, isReadyToResume) {
|
|
@@ -1938,7 +1938,7 @@ export function assignGovernedTurn(root, config, roleId) {
|
|
|
1938
1938
|
|
|
1939
1939
|
// DEC-BUDGET-ENFORCE-001: Pre-assignment budget exhaustion guard
|
|
1940
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
|
|
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
1942
|
}
|
|
1943
1943
|
|
|
1944
1944
|
// DEC-PARALLEL-011: Budget reservation
|
|
@@ -2675,7 +2675,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
2675
2675
|
recovery: {
|
|
2676
2676
|
typed_reason: 'budget_exhausted',
|
|
2677
2677
|
owner: 'human',
|
|
2678
|
-
recovery_action: 'Increase
|
|
2678
|
+
recovery_action: 'Increase budget with agentxchain config --set budget.per_run_max_usd <usd>, then run agentxchain resume',
|
|
2679
2679
|
turn_retained: false,
|
|
2680
2680
|
detail: `Run budget exhausted: spent $${updatedState.budget_status.spent_usd.toFixed(2)} of $${limit.toFixed(2)} limit ($${overBy.toFixed(2)} over)`,
|
|
2681
2681
|
},
|
|
@@ -61,6 +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
65
|
|
|
65
66
|
function validateMcpRuntime(runtimeId, runtime, errors) {
|
|
66
67
|
const transport = typeof runtime?.transport === 'string' && runtime.transport.trim()
|
|
@@ -537,6 +538,12 @@ export function validateV4Config(data, projectRoot) {
|
|
|
537
538
|
errors.push(...policyValidation.errors);
|
|
538
539
|
}
|
|
539
540
|
|
|
541
|
+
// Budget (optional but validated if present)
|
|
542
|
+
if (data.budget !== undefined) {
|
|
543
|
+
const budgetValidation = validateBudgetConfig(data.budget);
|
|
544
|
+
errors.push(...budgetValidation.errors);
|
|
545
|
+
}
|
|
546
|
+
|
|
540
547
|
// Approval Policy (optional but validated if present)
|
|
541
548
|
if (data.approval_policy !== undefined) {
|
|
542
549
|
errors.push(...validateApprovalPolicy(data.approval_policy, data.routing));
|
|
@@ -551,6 +558,80 @@ export function validateV4Config(data, projectRoot) {
|
|
|
551
558
|
return { ok: errors.length === 0, errors };
|
|
552
559
|
}
|
|
553
560
|
|
|
561
|
+
export function validateBudgetConfig(budget) {
|
|
562
|
+
const errors = [];
|
|
563
|
+
|
|
564
|
+
if (budget === null) {
|
|
565
|
+
return { ok: true, errors };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (!budget || typeof budget !== 'object' || Array.isArray(budget)) {
|
|
569
|
+
errors.push('budget must be an object');
|
|
570
|
+
return { ok: false, errors };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
validateBudgetUsdLimit('budget.per_turn_max_usd', budget.per_turn_max_usd, errors);
|
|
574
|
+
validateBudgetUsdLimit('budget.per_run_max_usd', budget.per_run_max_usd, errors);
|
|
575
|
+
|
|
576
|
+
if (
|
|
577
|
+
Number.isFinite(budget.per_turn_max_usd) &&
|
|
578
|
+
Number.isFinite(budget.per_run_max_usd) &&
|
|
579
|
+
budget.per_turn_max_usd > budget.per_run_max_usd
|
|
580
|
+
) {
|
|
581
|
+
errors.push('budget.per_turn_max_usd must be less than or equal to budget.per_run_max_usd when both are set');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (budget.on_exceed !== undefined) {
|
|
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(', ')} (warn is not implemented)`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (budget.cost_rates !== undefined) {
|
|
591
|
+
if (!budget.cost_rates || typeof budget.cost_rates !== 'object' || Array.isArray(budget.cost_rates)) {
|
|
592
|
+
errors.push('budget.cost_rates must be an object');
|
|
593
|
+
} else {
|
|
594
|
+
for (const [model, rates] of Object.entries(budget.cost_rates)) {
|
|
595
|
+
if (typeof model !== 'string' || !model.trim()) {
|
|
596
|
+
errors.push('budget.cost_rates model keys must be non-empty strings');
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (!rates || typeof rates !== 'object' || Array.isArray(rates)) {
|
|
600
|
+
errors.push(`budget.cost_rates.${model} must be an object`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
validateBudgetCostRate(`budget.cost_rates.${model}.input_per_1m`, rates.input_per_1m, errors);
|
|
604
|
+
validateBudgetCostRate(`budget.cost_rates.${model}.output_per_1m`, rates.output_per_1m, errors);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return { ok: errors.length === 0, errors };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function validateBudgetUsdLimit(path, value, errors) {
|
|
613
|
+
if (value === undefined || value === null) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
617
|
+
errors.push(`${path} must be a finite number when provided`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (value <= 0) {
|
|
621
|
+
errors.push(`${path} must be greater than 0 when provided`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function validateBudgetCostRate(path, value, errors) {
|
|
626
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
627
|
+
errors.push(`${path} must be a finite number`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (value < 0) {
|
|
631
|
+
errors.push(`${path} must be greater than or equal to 0`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
554
635
|
export function validateSchedulesConfig(schedules, roles) {
|
|
555
636
|
const errors = [];
|
|
556
637
|
|