agentxchain 2.122.0 → 2.124.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 +2 -0
- package/package.json +1 -1
- package/src/commands/approve-transition.js +16 -1
- package/src/commands/init.js +22 -6
- package/src/commands/intake-start.js +1 -0
- package/src/lib/adapters/manual-adapter.js +97 -7
- package/src/lib/continuous-run.js +8 -1
- package/src/lib/intake.js +2 -1
- package/src/lib/turn-result-validator.js +14 -0
package/bin/agentxchain.js
CHANGED
|
@@ -675,6 +675,7 @@ program
|
|
|
675
675
|
.option('--vision <path>', 'Path to VISION.md (project-relative or absolute, default: .planning/VISION.md)')
|
|
676
676
|
.option('--max-runs <n>', 'Maximum consecutive governed runs in continuous mode (default: 100)', parseInt)
|
|
677
677
|
.option('--poll-seconds <n>', 'Seconds between idle-detection cycles in continuous mode (default: 30)', parseInt)
|
|
678
|
+
.option('--triage-approval <mode>', 'Triage policy for vision-derived intents: auto or human (default: config or auto)')
|
|
678
679
|
.option('--max-idle-cycles <n>', 'Stop after N consecutive idle cycles with no derivable work (default: 3)', parseInt)
|
|
679
680
|
.option('--session-budget <usd>', 'Cumulative session-level budget cap in USD for continuous mode', parseFloat)
|
|
680
681
|
.action(runCommand);
|
|
@@ -917,6 +918,7 @@ intakeCmd
|
|
|
917
918
|
.description('Start governed execution for a planned intent')
|
|
918
919
|
.option('--intent <id>', 'Intent ID to start')
|
|
919
920
|
.option('--role <role>', 'Override the default entry role for the governed phase')
|
|
921
|
+
.option('--restart-completed', 'Initialize a fresh governed run when state is already completed')
|
|
920
922
|
.option('-j, --json', 'Output as JSON')
|
|
921
923
|
.action(intakeStartCommand);
|
|
922
924
|
|
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { loadProjectContext, loadProjectState } from '../lib/config.js';
|
|
3
3
|
import { approvePhaseTransition } from '../lib/governed-state.js';
|
|
4
4
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
5
|
+
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
6
|
+
import { checkCleanBaseline } from '../lib/repo-observer.js';
|
|
5
7
|
|
|
6
8
|
export async function approveTransitionCommand(opts) {
|
|
7
9
|
const context = loadProjectContext();
|
|
@@ -72,8 +74,21 @@ export async function approveTransitionCommand(opts) {
|
|
|
72
74
|
console.log(` ${chalk.dim('Gate actions:')} ${result.gateActionRun.actions.length} completed`);
|
|
73
75
|
}
|
|
74
76
|
console.log(chalk.dim(` Run status: ${result.state.status}`));
|
|
77
|
+
const nextRole = resolveGovernedRole({ state: result.state, config });
|
|
78
|
+
const nextRoleConfig = nextRole.roleId ? config.roles?.[nextRole.roleId] : null;
|
|
79
|
+
const cleanBaseline = nextRoleConfig
|
|
80
|
+
? checkCleanBaseline(root, nextRoleConfig.write_authority || 'review_only')
|
|
81
|
+
: { clean: true };
|
|
75
82
|
console.log('');
|
|
76
|
-
|
|
83
|
+
if (!cleanBaseline.clean && nextRole.roleId) {
|
|
84
|
+
console.log(chalk.yellow(' Next turn blocked until the workspace is checkpointed.'));
|
|
85
|
+
console.log(` ${chalk.dim('Role:')} ${nextRole.roleId} (${nextRoleConfig?.write_authority || 'review_only'})`);
|
|
86
|
+
console.log(` ${chalk.dim('Why:')} ${cleanBaseline.reason}`);
|
|
87
|
+
console.log(chalk.dim(' Fix: git add -A && git commit -m "checkpoint accepted artifacts"'));
|
|
88
|
+
console.log(chalk.dim(` Then: agentxchain step (to run the first turn in ${pt.to} phase)`));
|
|
89
|
+
} else {
|
|
90
|
+
console.log(chalk.dim(` Next: agentxchain step (to run the first turn in ${pt.to} phase)`));
|
|
91
|
+
}
|
|
77
92
|
console.log('');
|
|
78
93
|
}
|
|
79
94
|
|
package/src/commands/init.js
CHANGED
|
@@ -495,6 +495,18 @@ function formatGovernedRuntimeCommand(runtime) {
|
|
|
495
495
|
return Array.isArray(runtime?.command) ? runtime.command.join(' ') : String(runtime?.command || '');
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
+
function formatRuntimeSummary(runtimeId, runtime) {
|
|
499
|
+
if (!runtimeId || !runtime) return 'unconfigured';
|
|
500
|
+
if (runtime.type === 'manual') return `${runtimeId} (manual)`;
|
|
501
|
+
const command = formatGovernedRuntimeCommand(runtime);
|
|
502
|
+
if (!command) return `${runtimeId} (${runtime.type})`;
|
|
503
|
+
return `${command} (${runtime.prompt_transport || runtime.type})`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function hasTemplateDefinedRouting(template) {
|
|
507
|
+
return Boolean(template?.scaffold_blueprint?.routing && Object.keys(template.scaffold_blueprint.routing).length > 0);
|
|
508
|
+
}
|
|
509
|
+
|
|
498
510
|
function resolveInitDirOption(dirOption) {
|
|
499
511
|
if (dirOption == null) return null;
|
|
500
512
|
const value = String(dirOption).trim();
|
|
@@ -862,10 +874,8 @@ async function initGoverned(opts) {
|
|
|
862
874
|
const dir = resolve(process.cwd(), folderName);
|
|
863
875
|
const targetLabel = formatInitTarget(dir);
|
|
864
876
|
const projectId = slugify(projectName);
|
|
865
|
-
let localDevRuntime;
|
|
866
|
-
|
|
867
877
|
try {
|
|
868
|
-
|
|
878
|
+
resolveGovernedLocalDevRuntime(opts);
|
|
869
879
|
} catch (err) {
|
|
870
880
|
console.error(chalk.red(` Error: ${err.message}`));
|
|
871
881
|
process.exit(1);
|
|
@@ -906,6 +916,9 @@ async function initGoverned(opts) {
|
|
|
906
916
|
? { ...opts, goal: projectGoal }
|
|
907
917
|
: { ...opts };
|
|
908
918
|
const { config, scaffoldWorkflowKitConfig } = scaffoldGoverned(dir, projectName, projectId, templateId, scaffoldOptions, workflowKitConfig);
|
|
919
|
+
const devRuntimeId = config.roles?.dev?.runtime;
|
|
920
|
+
const devRuntime = devRuntimeId ? config.runtimes?.[devRuntimeId] : null;
|
|
921
|
+
const hasLiveConnectorProbe = Object.values(config.runtimes || {}).some((runtime) => runtime?.type && runtime.type !== 'manual');
|
|
909
922
|
|
|
910
923
|
console.log('');
|
|
911
924
|
console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
|
|
@@ -928,12 +941,12 @@ async function initGoverned(opts) {
|
|
|
928
941
|
console.log(` ${chalk.dim('└──')} TALK.md`);
|
|
929
942
|
console.log('');
|
|
930
943
|
console.log(` ${chalk.dim('Roles:')} ${promptRoleIds.join(', ')}`);
|
|
931
|
-
console.log(` ${chalk.dim('Phases:')} ${phaseNames.join(' → ')} ${chalk.dim(selectedTemplate
|
|
944
|
+
console.log(` ${chalk.dim('Phases:')} ${phaseNames.join(' → ')} ${chalk.dim(hasTemplateDefinedRouting(selectedTemplate) ? '(template-defined; edit routing in agentxchain.json to customize)' : '(default; extend via routing in agentxchain.json)')}`);
|
|
932
945
|
console.log(` ${chalk.dim('Template:')} ${templateId}`);
|
|
933
946
|
if (config.project?.goal) {
|
|
934
947
|
console.log(` ${chalk.dim('Goal:')} ${config.project.goal}`);
|
|
935
948
|
}
|
|
936
|
-
console.log(` ${chalk.dim('Dev runtime:')} ${
|
|
949
|
+
console.log(` ${chalk.dim('Dev runtime:')} ${formatRuntimeSummary(devRuntimeId, devRuntime)}`);
|
|
937
950
|
console.log(` ${chalk.dim('Protocol:')} governed convergence`);
|
|
938
951
|
console.log('');
|
|
939
952
|
|
|
@@ -962,6 +975,7 @@ async function initGoverned(opts) {
|
|
|
962
975
|
console.log('');
|
|
963
976
|
} else if (Object.entries(config.roles).every(([, role]) => allRuntimes[role.runtime]?.type === 'manual')) {
|
|
964
977
|
console.log(` ${chalk.green('Ready:')} manual-only scaffold (${chalk.bold('no API keys')} and ${chalk.bold('no local coding CLI')} required).`);
|
|
978
|
+
console.log(` ${chalk.green(' ')}Use ${chalk.bold('agentxchain step')} for the first governed turn; ${chalk.bold('run')} requires automatable runtimes.`);
|
|
965
979
|
console.log('');
|
|
966
980
|
}
|
|
967
981
|
|
|
@@ -974,7 +988,9 @@ async function initGoverned(opts) {
|
|
|
974
988
|
}
|
|
975
989
|
console.log(` ${chalk.bold('agentxchain template validate')} ${chalk.dim('# prove the scaffold contract before the first turn')}`);
|
|
976
990
|
console.log(` ${chalk.bold('agentxchain doctor')} ${chalk.dim('# verify runtimes, config, and readiness')}`);
|
|
977
|
-
|
|
991
|
+
if (hasLiveConnectorProbe) {
|
|
992
|
+
console.log(` ${chalk.bold('agentxchain connector check')} ${chalk.dim('# live-probe configured runtimes before the first turn')}`);
|
|
993
|
+
}
|
|
978
994
|
console.log(` ${chalk.bold('git add -A')} ${chalk.dim('# stage the governed scaffold')}`);
|
|
979
995
|
console.log(` ${chalk.bold('git commit -m "initial governed scaffold"')} ${chalk.dim('# checkpoint the starting state')}`);
|
|
980
996
|
console.log(` ${chalk.bold('agentxchain step')} ${chalk.dim('# run the first governed turn')}`);
|
|
@@ -32,6 +32,10 @@ export function printManualDispatchInstructions(state, config, options = {}) {
|
|
|
32
32
|
const stagingPath = getTurnStagingResultPath(turn.turn_id);
|
|
33
33
|
const phase = state.phase || 'planning';
|
|
34
34
|
const roleId = turn.assigned_role;
|
|
35
|
+
const artifactType = getDefaultArtifactType(role);
|
|
36
|
+
const verificationExample = getVerificationExample(phase, config, role);
|
|
37
|
+
const phaseTransitionExample = getDefaultPhaseTransitionRequest(phase, config);
|
|
38
|
+
const runCompletionExample = getDefaultRunCompletionRequest(phase, config);
|
|
35
39
|
|
|
36
40
|
const lines = [];
|
|
37
41
|
lines.push('');
|
|
@@ -64,6 +68,15 @@ export function printManualDispatchInstructions(state, config, options = {}) {
|
|
|
64
68
|
lines.push('');
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
const completionHints = getPhaseCompletionHints(phase, role, config);
|
|
72
|
+
if (completionHints.length > 0) {
|
|
73
|
+
lines.push(' To exit this phase cleanly:');
|
|
74
|
+
for (const hint of completionHints) {
|
|
75
|
+
lines.push(` - ${hint}`);
|
|
76
|
+
}
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
|
|
67
80
|
// Minimal turn-result example
|
|
68
81
|
lines.push(' Minimal turn-result.json:');
|
|
69
82
|
lines.push(' {');
|
|
@@ -71,17 +84,17 @@ export function printManualDispatchInstructions(state, config, options = {}) {
|
|
|
71
84
|
lines.push(` "run_id": "${state.run_id || 'run_...'}",`);
|
|
72
85
|
lines.push(` "turn_id": "${turn.turn_id}",`);
|
|
73
86
|
lines.push(` "role": "${roleId}",`);
|
|
74
|
-
lines.push(` "runtime_id": "${role?.runtime || 'manual'}",`);
|
|
87
|
+
lines.push(` "runtime_id": "${turn.runtime_id || role?.runtime_id || role?.runtime || 'manual'}",`);
|
|
75
88
|
lines.push(' "status": "completed",');
|
|
76
89
|
lines.push(' "summary": "...",');
|
|
77
90
|
lines.push(' "decisions": [{"id":"DEC-001","category":"scope","statement":"...","rationale":"..."}],');
|
|
78
91
|
lines.push(' "objections": [{"id":"OBJ-001","severity":"medium","statement":"...","status":"raised"}],');
|
|
79
92
|
lines.push(' "files_changed": [],');
|
|
80
|
-
lines.push(
|
|
81
|
-
lines.push(
|
|
82
|
-
lines.push(` "proposed_next_role": "${getDefaultNextRole(roleId, config)}",`);
|
|
83
|
-
lines.push(
|
|
84
|
-
lines.push(
|
|
93
|
+
lines.push(` "verification": ${verificationExample},`);
|
|
94
|
+
lines.push(` "artifact": {"type":"${artifactType}","ref":null},`);
|
|
95
|
+
lines.push(` "proposed_next_role": "${getDefaultNextRole(roleId, config, phase)}",`);
|
|
96
|
+
lines.push(` "phase_transition_request": ${phaseTransitionExample === null ? 'null' : `"${phaseTransitionExample}"`},`);
|
|
97
|
+
lines.push(` "run_completion_request": ${runCompletionExample === null ? 'null' : runCompletionExample}`);
|
|
85
98
|
lines.push(' }');
|
|
86
99
|
lines.push('');
|
|
87
100
|
lines.push(' Docs: https://agentxchain.dev/docs/getting-started');
|
|
@@ -112,11 +125,48 @@ function getPhaseGateHints(phase, roleId, config) {
|
|
|
112
125
|
return hints;
|
|
113
126
|
}
|
|
114
127
|
|
|
128
|
+
function getPhaseCompletionHints(phase, role, config) {
|
|
129
|
+
const hints = [];
|
|
130
|
+
const writeAuthority = role?.write_authority || 'review_only';
|
|
131
|
+
const nextPhase = getNextPhase(phase, config);
|
|
132
|
+
const exitGate = config?.routing?.[phase]?.exit_gate;
|
|
133
|
+
const gateConfig = exitGate ? config?.gates?.[exitGate] : null;
|
|
134
|
+
|
|
135
|
+
if (gateConfig?.requires_verification_pass && writeAuthority !== 'review_only') {
|
|
136
|
+
hints.push('set `verification.status` to `pass` only when your listed checks actually passed');
|
|
137
|
+
}
|
|
138
|
+
if (writeAuthority === 'authoritative') {
|
|
139
|
+
hints.push('use `artifact.type: "workspace"` unless you created a real git commit during the turn');
|
|
140
|
+
} else if (writeAuthority === 'proposed') {
|
|
141
|
+
hints.push('use `artifact.type: "patch"` for non-completion turns');
|
|
142
|
+
} else {
|
|
143
|
+
hints.push('keep `artifact.type: "review"` because review-only roles cannot claim code-writing artifacts');
|
|
144
|
+
}
|
|
145
|
+
if (nextPhase) {
|
|
146
|
+
hints.push(`set \`phase_transition_request\` to \`${nextPhase}\` when this turn is ready to leave \`${phase}\``);
|
|
147
|
+
} else if (phase === 'qa') {
|
|
148
|
+
hints.push('set `run_completion_request` to `true` when QA evidence is complete and you are asking to end the run');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return hints;
|
|
152
|
+
}
|
|
153
|
+
|
|
115
154
|
/**
|
|
116
155
|
* Suggest a reasonable next role based on current role.
|
|
117
156
|
*/
|
|
118
|
-
function getDefaultNextRole(roleId, config) {
|
|
157
|
+
function getDefaultNextRole(roleId, config, phase) {
|
|
119
158
|
const routing = config.routing || {};
|
|
159
|
+
// Check phase-specific allowed_next first
|
|
160
|
+
const phaseAllowed = routing[phase]?.allowed_next_roles || routing[phase]?.allowed_next;
|
|
161
|
+
if (phase && phaseAllowed?.length > 0) {
|
|
162
|
+
const allowed = phaseAllowed;
|
|
163
|
+
// If the current role is in the allowlist, suggest it (another turn in same phase)
|
|
164
|
+
if (allowed.includes(roleId)) return roleId;
|
|
165
|
+
// Otherwise suggest the first non-human allowed role
|
|
166
|
+
const nonHuman = allowed.find(r => r !== 'human');
|
|
167
|
+
if (nonHuman) return nonHuman;
|
|
168
|
+
return allowed[0];
|
|
169
|
+
}
|
|
120
170
|
if (routing[roleId]?.default_next) return routing[roleId].default_next;
|
|
121
171
|
if (roleId === 'pm') return 'dev';
|
|
122
172
|
if (roleId === 'dev') return 'qa';
|
|
@@ -124,6 +174,46 @@ function getDefaultNextRole(roleId, config) {
|
|
|
124
174
|
return 'human';
|
|
125
175
|
}
|
|
126
176
|
|
|
177
|
+
function getDefaultArtifactType(role) {
|
|
178
|
+
const writeAuthority = role?.write_authority || 'review_only';
|
|
179
|
+
if (writeAuthority === 'authoritative') return 'workspace';
|
|
180
|
+
if (writeAuthority === 'proposed') return 'patch';
|
|
181
|
+
return 'review';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getVerificationExample(phase, config, role) {
|
|
185
|
+
const writeAuthority = role?.write_authority || 'review_only';
|
|
186
|
+
const exitGate = config?.routing?.[phase]?.exit_gate;
|
|
187
|
+
const gateConfig = exitGate ? config?.gates?.[exitGate] : null;
|
|
188
|
+
if (gateConfig?.requires_verification_pass && writeAuthority !== 'review_only') {
|
|
189
|
+
return '{"status":"pass","commands":["..."],"evidence_summary":"..."}';
|
|
190
|
+
}
|
|
191
|
+
return '{"status":"skipped","commands":[],"evidence_summary":"..."}';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getDefaultPhaseTransitionRequest(phase, config) {
|
|
195
|
+
return getNextPhase(phase, config);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getDefaultRunCompletionRequest(phase, config) {
|
|
199
|
+
const nextPhase = getNextPhase(phase, config);
|
|
200
|
+
if (!nextPhase && phase === 'qa') {
|
|
201
|
+
return 'true';
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getNextPhase(phase, config) {
|
|
207
|
+
const orderedPhases = Array.isArray(config?.phases) && config.phases.length > 0
|
|
208
|
+
? config.phases.map((entry) => typeof entry === 'string' ? entry : entry?.id).filter(Boolean)
|
|
209
|
+
: Object.keys(config?.routing || {});
|
|
210
|
+
const index = orderedPhases.indexOf(phase);
|
|
211
|
+
if (index === -1 || index === orderedPhases.length - 1) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return orderedPhases[index + 1];
|
|
215
|
+
}
|
|
216
|
+
|
|
127
217
|
/**
|
|
128
218
|
* Wait for the staged turn result file to appear.
|
|
129
219
|
*
|
|
@@ -86,6 +86,13 @@ function describeContinuousTerminalStep(step, contOpts) {
|
|
|
86
86
|
if (step.status === 'idle_exit') {
|
|
87
87
|
return `All vision goals appear addressed (${contOpts.maxIdleCycles} consecutive idle cycles). Stopping.`;
|
|
88
88
|
}
|
|
89
|
+
if (step.status === 'failed') {
|
|
90
|
+
const reason = step.stop_reason || step.action || 'unknown';
|
|
91
|
+
return `Continuous loop failed: ${reason}. Check "agentxchain status" for details.`;
|
|
92
|
+
}
|
|
93
|
+
if (step.status === 'blocked') {
|
|
94
|
+
return 'Continuous loop paused on blocker. Use "agentxchain unblock <id>" to resume.';
|
|
95
|
+
}
|
|
89
96
|
return null;
|
|
90
97
|
}
|
|
91
98
|
|
|
@@ -304,7 +311,7 @@ export function resolveContinuousOptions(opts, config) {
|
|
|
304
311
|
maxRuns: opts.maxRuns ?? configCont.max_runs ?? 100,
|
|
305
312
|
pollSeconds: opts.pollSeconds ?? configCont.poll_seconds ?? 30,
|
|
306
313
|
maxIdleCycles: opts.maxIdleCycles ?? configCont.max_idle_cycles ?? 3,
|
|
307
|
-
triageApproval: configCont.triage_approval ?? 'auto',
|
|
314
|
+
triageApproval: opts.triageApproval ?? configCont.triage_approval ?? 'auto',
|
|
308
315
|
cooldownSeconds: opts.cooldownSeconds ?? configCont.cooldown_seconds ?? 5,
|
|
309
316
|
perSessionMaxUsd: opts.sessionBudget ?? configCont.per_session_max_usd ?? null,
|
|
310
317
|
};
|
package/src/lib/intake.js
CHANGED
|
@@ -711,9 +711,10 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
if (state.status === 'completed' && !allowCompletedRestart) {
|
|
714
|
+
const restartCmd = `agentxchain intake start --intent ${intent.intent_id} --restart-completed`;
|
|
714
715
|
return {
|
|
715
716
|
ok: false,
|
|
716
|
-
error:
|
|
717
|
+
error: `cannot start: governed run is already completed. Re-run with "${restartCmd}" to initialize a fresh governed run.`,
|
|
717
718
|
exitCode: 1,
|
|
718
719
|
};
|
|
719
720
|
}
|
|
@@ -513,6 +513,20 @@ function validateArtifact(tr, config) {
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
// Authoritative roles with product-file changes must not claim artifact.type "review".
|
|
517
|
+
// "review" signals observation-only, but non-empty product files_changed means the actor
|
|
518
|
+
// wrote to the repo. Allowing the mismatch makes accepted_integration_ref look clean
|
|
519
|
+
// when the workspace is actually dirty — hiding state from the next authoritative turn.
|
|
520
|
+
if (writeAuthority === 'authoritative' && tr.artifact?.type === 'review') {
|
|
521
|
+
const productFiles = (tr.files_changed || []).filter(f => !isAllowedReviewPath(f));
|
|
522
|
+
if (productFiles.length > 0) {
|
|
523
|
+
errors.push(
|
|
524
|
+
`Role "${tr.role}" has authoritative write authority and changed product files (${productFiles.join(', ')}), but artifact type is "review". ` +
|
|
525
|
+
'Use "workspace" or "commit" when product files are modified.'
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
516
530
|
// Warn if files_changed is empty for authoritative + completed turns
|
|
517
531
|
if (writeAuthority === 'authoritative' && tr.status === 'completed' && (tr.files_changed || []).length === 0) {
|
|
518
532
|
warnings.push('Authoritative role completed with no files_changed — is this intentional?');
|