agentxchain 2.24.3 → 2.25.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/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -681,6 +681,7 @@ async function initGoverned(opts) {
|
|
|
681
681
|
console.log(` ${chalk.dim('└──')} TALK.md`);
|
|
682
682
|
console.log('');
|
|
683
683
|
console.log(` ${chalk.dim('Roles:')} pm, dev, qa, eng_director`);
|
|
684
|
+
console.log(` ${chalk.dim('Phases:')} planning → implementation → qa ${chalk.dim('(default; extend via routing in agentxchain.json)')}`);
|
|
684
685
|
console.log(` ${chalk.dim('Template:')} ${templateId}`);
|
|
685
686
|
console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
|
|
686
687
|
console.log(` ${chalk.dim('Protocol:')} governed convergence`);
|
|
@@ -6,7 +6,8 @@ import { safeParseJson } from './schema.js';
|
|
|
6
6
|
export const COORDINATOR_CONFIG_FILE = 'agentxchain-multi.json';
|
|
7
7
|
|
|
8
8
|
const VALID_ID = /^[a-z0-9_-]+$/;
|
|
9
|
-
const
|
|
9
|
+
const DEFAULT_PHASES = new Set(['planning', 'implementation', 'qa']);
|
|
10
|
+
const VALID_PHASE_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
10
11
|
const VALID_BARRIER_TYPES = new Set([
|
|
11
12
|
'all_repos_accepted',
|
|
12
13
|
'interface_alignment',
|
|
@@ -88,11 +89,14 @@ function validateWorkstreams(raw, repoIds, errors) {
|
|
|
88
89
|
continue;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
|
|
92
|
+
// Derive valid phases from routing keys when present; fall back to defaults
|
|
93
|
+
const validPhases = raw.routing ? new Set(Object.keys(raw.routing)) : DEFAULT_PHASES;
|
|
94
|
+
if (!validPhases.has(workstream.phase)) {
|
|
95
|
+
const phaseList = [...validPhases].join(', ');
|
|
92
96
|
pushError(
|
|
93
97
|
errors,
|
|
94
98
|
'workstream_phase_invalid',
|
|
95
|
-
`workstream "${workstreamId}" phase must be one of:
|
|
99
|
+
`workstream "${workstreamId}" phase must be one of: ${phaseList}`,
|
|
96
100
|
);
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -279,8 +283,8 @@ function validateRouting(raw, workstreamIds, errors) {
|
|
|
279
283
|
|
|
280
284
|
const workstreamIdSet = new Set(workstreamIds);
|
|
281
285
|
for (const [phase, route] of Object.entries(raw.routing)) {
|
|
282
|
-
if (!
|
|
283
|
-
pushError(errors, 'routing_phase_invalid', `routing phase "${phase}" must be
|
|
286
|
+
if (!VALID_PHASE_NAME.test(phase)) {
|
|
287
|
+
pushError(errors, 'routing_phase_invalid', `routing phase "${phase}" must be lowercase alphanumeric starting with a letter (hyphens and underscores allowed)`);
|
|
284
288
|
}
|
|
285
289
|
|
|
286
290
|
if (!route || typeof route !== 'object' || Array.isArray(route)) {
|
|
@@ -73,6 +73,15 @@ export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
const invalidOrderReason = getInvalidPhaseTransitionReason(currentPhase, transitionRequest, routing);
|
|
77
|
+
if (invalidOrderReason) {
|
|
78
|
+
return {
|
|
79
|
+
...baseResult,
|
|
80
|
+
action: 'gate_failed',
|
|
81
|
+
reasons: [invalidOrderReason],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
// Find the exit gate for the current phase
|
|
77
86
|
const currentRouting = routing[currentPhase];
|
|
78
87
|
if (!currentRouting || !currentRouting.exit_gate) {
|
|
@@ -285,6 +294,43 @@ export function getPhaseOrder(routing) {
|
|
|
285
294
|
return Object.keys(routing || {});
|
|
286
295
|
}
|
|
287
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Return the next declared phase after the current phase, or null when the
|
|
299
|
+
* current phase is final or not part of the routing config.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} currentPhase
|
|
302
|
+
* @param {object} routing
|
|
303
|
+
* @returns {string|null}
|
|
304
|
+
*/
|
|
305
|
+
export function getNextPhase(currentPhase, routing) {
|
|
306
|
+
const phases = getPhaseOrder(routing || {});
|
|
307
|
+
const currentIndex = phases.indexOf(currentPhase);
|
|
308
|
+
if (currentIndex === -1 || currentIndex >= phases.length - 1) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
return phases[currentIndex + 1];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Validate that a requested phase transition follows the declared routing
|
|
316
|
+
* order. Returns null when the request is valid.
|
|
317
|
+
*
|
|
318
|
+
* @param {string} currentPhase
|
|
319
|
+
* @param {string} requestedPhase
|
|
320
|
+
* @param {object} routing
|
|
321
|
+
* @returns {string|null}
|
|
322
|
+
*/
|
|
323
|
+
export function getInvalidPhaseTransitionReason(currentPhase, requestedPhase, routing) {
|
|
324
|
+
const nextPhase = getNextPhase(currentPhase, routing);
|
|
325
|
+
if (!nextPhase) {
|
|
326
|
+
return `phase_transition_request "${requestedPhase}" is invalid in final phase "${currentPhase}"; use run_completion_request instead.`;
|
|
327
|
+
}
|
|
328
|
+
if (requestedPhase !== nextPhase) {
|
|
329
|
+
return `phase_transition_request "${requestedPhase}" is invalid from phase "${currentPhase}"; next phase is "${nextPhase}".`;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
288
334
|
/**
|
|
289
335
|
* Check if a phase is the final phase in the routing config.
|
|
290
336
|
*
|
|
@@ -21,7 +21,8 @@ const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp'];
|
|
|
21
21
|
const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai'];
|
|
22
22
|
export const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
23
23
|
const VALID_MCP_TRANSPORTS = ['stdio', 'streamable_http'];
|
|
24
|
-
const
|
|
24
|
+
const DEFAULT_PHASES = ['planning', 'implementation', 'qa'];
|
|
25
|
+
const VALID_PHASE_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
25
26
|
const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
|
|
26
27
|
const VALID_API_PROXY_RETRY_CLASSES = [
|
|
27
28
|
'rate_limited',
|
|
@@ -400,10 +401,11 @@ export function validateV4Config(data, projectRoot) {
|
|
|
400
401
|
}
|
|
401
402
|
|
|
402
403
|
// Routing (optional but validated if present)
|
|
404
|
+
// Phase names are derived from routing keys when present; fall back to defaults
|
|
403
405
|
if (data.routing) {
|
|
404
406
|
for (const [phase, route] of Object.entries(data.routing)) {
|
|
405
|
-
if (!
|
|
406
|
-
errors.push(`Routing
|
|
407
|
+
if (!VALID_PHASE_NAME.test(phase)) {
|
|
408
|
+
errors.push(`Routing phase name "${phase}" must be lowercase alphanumeric starting with a letter (hyphens and underscores allowed)`);
|
|
407
409
|
}
|
|
408
410
|
if (route.entry_role && data.roles && !data.roles[route.entry_role]) {
|
|
409
411
|
errors.push(`Routing "${phase}": entry_role "${route.entry_role}" is not a defined role`);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { existsSync, readFileSync } from 'fs';
|
|
16
16
|
import { join } from 'path';
|
|
17
17
|
import { getActiveTurn } from './governed-state.js';
|
|
18
|
+
import { getInvalidPhaseTransitionReason } from './gate-evaluator.js';
|
|
18
19
|
|
|
19
20
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -519,6 +520,15 @@ function validateProtocol(tr, state, config) {
|
|
|
519
520
|
errors.push(
|
|
520
521
|
`phase_transition_request "${tr.phase_transition_request}" is not a defined phase in routing.`
|
|
521
522
|
);
|
|
523
|
+
} else if (config.routing && state?.phase) {
|
|
524
|
+
const invalidOrderReason = getInvalidPhaseTransitionReason(
|
|
525
|
+
state.phase,
|
|
526
|
+
tr.phase_transition_request,
|
|
527
|
+
config.routing
|
|
528
|
+
);
|
|
529
|
+
if (invalidOrderReason) {
|
|
530
|
+
errors.push(invalidOrderReason);
|
|
531
|
+
}
|
|
522
532
|
}
|
|
523
533
|
}
|
|
524
534
|
|