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 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 `project.goal` in `agentxchain.json` before the first governed turn instead of re-running init in place.
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
 
@@ -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 <key_value>', 'Set a config value (e.g. --set "rules.max_consecutive_claims 3")')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.59.0",
3
+ "version": "2.61.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 { loadConfig, CONFIG_FILE } from '../lib/config.js';
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 result = loadConfig();
10
- if (!result) {
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, config } = result;
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
- printConfig(config);
50
+ if (version === 4) {
51
+ printGovernedConfig(config);
52
+ return;
53
+ }
54
+
55
+ printLegacyConfig(config);
39
56
  }
40
57
 
41
- function printConfig(config) {
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 parts = keyValPair.split(/\s+/);
122
- if (parts.length < 2) {
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 rules.max_consecutive_claims 3'));
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 = parts[0];
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 = validateConfigSchema(config);
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
+ }
@@ -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('# or set project.goal in agentxchain.json')}`);
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 per_run_max_usd in agentxchain.json, then run agentxchain resume';
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 per_run_max_usd in agentxchain.json, then run agentxchain resume` };
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 per_run_max_usd in agentxchain.json, then run agentxchain resume',
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