agentxchain 0.8.7 → 2.1.1
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 +123 -154
- package/bin/agentxchain.js +240 -8
- package/dashboard/app.js +305 -0
- package/dashboard/components/blocked.js +145 -0
- package/dashboard/components/cross-repo.js +126 -0
- package/dashboard/components/gate.js +311 -0
- package/dashboard/components/hooks.js +177 -0
- package/dashboard/components/initiative.js +147 -0
- package/dashboard/components/ledger.js +165 -0
- package/dashboard/components/timeline.js +222 -0
- package/dashboard/index.html +352 -0
- package/package.json +16 -7
- package/scripts/agentxchain-autonudge.applescript +32 -5
- package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
- package/scripts/publish-from-tag.sh +88 -0
- package/scripts/release-postflight.sh +231 -0
- package/scripts/release-preflight.sh +167 -0
- package/scripts/run-autonudge.sh +1 -1
- package/src/adapters/claude-code.js +7 -14
- package/src/adapters/cursor-local.js +17 -16
- package/src/commands/accept-turn.js +160 -0
- package/src/commands/approve-completion.js +80 -0
- package/src/commands/approve-transition.js +85 -0
- package/src/commands/branch.js +2 -2
- package/src/commands/claim.js +84 -9
- package/src/commands/config.js +16 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +540 -5
- package/src/commands/migrate.js +348 -0
- package/src/commands/multi.js +549 -0
- package/src/commands/plugin.js +157 -0
- package/src/commands/reject-turn.js +204 -0
- package/src/commands/resume.js +389 -0
- package/src/commands/status.js +196 -3
- package/src/commands/step.js +947 -0
- package/src/commands/stop.js +65 -33
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/update.js +24 -3
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/commands/watch.js +112 -25
- package/src/lib/adapters/api-proxy-adapter.js +1076 -0
- package/src/lib/adapters/local-cli-adapter.js +337 -0
- package/src/lib/adapters/manual-adapter.js +169 -0
- package/src/lib/blocked-state.js +94 -0
- package/src/lib/config.js +143 -12
- package/src/lib/context-compressor.js +121 -0
- package/src/lib/context-section-parser.js +220 -0
- package/src/lib/coordinator-acceptance.js +428 -0
- package/src/lib/coordinator-config.js +461 -0
- package/src/lib/coordinator-dispatch.js +276 -0
- package/src/lib/coordinator-gates.js +487 -0
- package/src/lib/coordinator-hooks.js +239 -0
- package/src/lib/coordinator-recovery.js +523 -0
- package/src/lib/coordinator-state.js +365 -0
- package/src/lib/cross-repo-context.js +247 -0
- package/src/lib/dashboard/bridge-server.js +284 -0
- package/src/lib/dashboard/file-watcher.js +93 -0
- package/src/lib/dashboard/state-reader.js +96 -0
- package/src/lib/dispatch-bundle.js +568 -0
- package/src/lib/dispatch-manifest.js +252 -0
- package/src/lib/filter-agents.js +12 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/generate-vscode.js +158 -68
- package/src/lib/governed-state.js +2139 -0
- package/src/lib/governed-templates.js +145 -0
- package/src/lib/hook-runner.js +788 -0
- package/src/lib/next-owner.js +61 -6
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/notify.js +14 -12
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/prompt-core.js +108 -0
- package/src/lib/protocol-conformance.js +291 -0
- package/src/lib/reference-conformance-adapter.js +717 -0
- package/src/lib/repo-observer.js +597 -0
- package/src/lib/repo.js +0 -31
- package/src/lib/safe-write.js +44 -0
- package/src/lib/schema.js +189 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/seed-prompt-polling.js +15 -73
- package/src/lib/seed-prompt.js +17 -63
- package/src/lib/token-budget.js +206 -0
- package/src/lib/token-counter.js +27 -0
- package/src/lib/turn-paths.js +67 -0
- package/src/lib/turn-result-validator.js +496 -0
- package/src/lib/validation.js +167 -19
- package/src/lib/verify-command.js +72 -0
- package/src/templates/governed/api-service.json +31 -0
- package/src/templates/governed/cli-tool.json +30 -0
- package/src/templates/governed/generic.json +10 -0
- package/src/templates/governed/web-app.json +30 -0
package/src/lib/next-owner.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
|
+
export function resolveExpectedClaimer(root, config, lock = {}) {
|
|
5
|
+
const agents = Object.keys(config.agents || {});
|
|
6
|
+
if (agents.length === 0) return { next: null, source: 'none', raw: null };
|
|
7
|
+
|
|
8
|
+
const trigger = readTrigger(root);
|
|
9
|
+
if (
|
|
10
|
+
trigger &&
|
|
11
|
+
typeof trigger.turn_number === 'number' &&
|
|
12
|
+
trigger.turn_number === lock.turn_number &&
|
|
13
|
+
typeof trigger.agent === 'string' &&
|
|
14
|
+
agents.includes(trigger.agent)
|
|
15
|
+
) {
|
|
16
|
+
return { next: trigger.agent, source: 'trigger', raw: trigger.agent };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return resolveNextAgent(root, config, lock);
|
|
20
|
+
}
|
|
21
|
+
|
|
4
22
|
export function resolveNextAgent(root, config, lock = {}) {
|
|
5
23
|
const agents = Object.keys(config.agents || {});
|
|
6
24
|
if (agents.length === 0) return { next: null, source: 'none', raw: null };
|
|
@@ -12,6 +30,10 @@ export function resolveNextAgent(root, config, lock = {}) {
|
|
|
12
30
|
return { next: fromTalk, source: 'talk', raw: fromTalk };
|
|
13
31
|
}
|
|
14
32
|
|
|
33
|
+
if (config.rules?.strict_next_owner) {
|
|
34
|
+
return { next: null, source: 'strict-missing', raw: null };
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
const last = lock.last_released_by;
|
|
16
38
|
if (last && agents.includes(last)) {
|
|
17
39
|
const idx = agents.indexOf(last);
|
|
@@ -21,6 +43,13 @@ export function resolveNextAgent(root, config, lock = {}) {
|
|
|
21
43
|
return { next: agents[0], source: 'fallback-first', raw: null };
|
|
22
44
|
}
|
|
23
45
|
|
|
46
|
+
const NEXT_OWNER_PATTERNS = [
|
|
47
|
+
/^(?:-|\*)?\s*\**next\s*owner\**\s*:\s*(.+)$/i,
|
|
48
|
+
/^(?:-|\*)?\s*\**handoff\s*(?:to)?\**\s*:\s*(.+)$/i,
|
|
49
|
+
/^(?:-|\*)?\s*\**next\**\s*:\s*(.+)$/i,
|
|
50
|
+
/^(?:-|\*)?\s*\**hand\s*off\s*to\**\s*:\s*(.+)$/i,
|
|
51
|
+
];
|
|
52
|
+
|
|
24
53
|
function parseNextOwnerFromTalk(talkPath, validAgentIds) {
|
|
25
54
|
if (!existsSync(talkPath)) return null;
|
|
26
55
|
|
|
@@ -37,12 +66,18 @@ function parseNextOwnerFromTalk(talkPath, validAgentIds) {
|
|
|
37
66
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
38
67
|
const line = lines[i].trim();
|
|
39
68
|
if (!line) continue;
|
|
40
|
-
const match = line.match(/^(?:-|\*)?\s*\**next\s*owner\**\s*:\s*(.+)$/i);
|
|
41
|
-
if (!match) continue;
|
|
42
69
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
for (const pattern of NEXT_OWNER_PATTERNS) {
|
|
71
|
+
const match = line.match(pattern);
|
|
72
|
+
if (!match) continue;
|
|
73
|
+
|
|
74
|
+
const candidate = normalizeAgentId(match[1]);
|
|
75
|
+
if (candidate && validAgentIds.includes(candidate)) {
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const fuzzy = fuzzyMatchAgentId(match[1], validAgentIds);
|
|
80
|
+
if (fuzzy) return fuzzy;
|
|
46
81
|
}
|
|
47
82
|
}
|
|
48
83
|
|
|
@@ -52,10 +87,30 @@ function parseNextOwnerFromTalk(talkPath, validAgentIds) {
|
|
|
52
87
|
function normalizeAgentId(raw) {
|
|
53
88
|
if (!raw) return null;
|
|
54
89
|
let value = String(raw).trim();
|
|
55
|
-
value = value.replace(/[`*_]/g, '').trim();
|
|
90
|
+
value = value.replace(/[`*_\[\]]/g, '').trim();
|
|
56
91
|
value = value.replace(/\(.*?\)/g, '').trim();
|
|
57
92
|
value = value.split(/[,\s]+/)[0];
|
|
58
93
|
value = value.toLowerCase();
|
|
59
94
|
return /^[a-z0-9_-]+$/.test(value) ? value : null;
|
|
60
95
|
}
|
|
61
96
|
|
|
97
|
+
function fuzzyMatchAgentId(raw, validAgentIds) {
|
|
98
|
+
if (!raw) return null;
|
|
99
|
+
const cleaned = String(raw).replace(/[`*_\[\]]/g, '').replace(/\(.*?\)/g, '').trim().toLowerCase();
|
|
100
|
+
for (const id of validAgentIds) {
|
|
101
|
+
if (cleaned.startsWith(id)) return id;
|
|
102
|
+
if (cleaned.includes(id)) return id;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readTrigger(root) {
|
|
108
|
+
const triggerPath = join(root, '.agentxchain-trigger.json');
|
|
109
|
+
if (!existsSync(triggerPath)) return null;
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(readFileSync(triggerPath, 'utf8'));
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized config loader for AgentXchain.
|
|
3
|
+
*
|
|
4
|
+
* Supports two config generations:
|
|
5
|
+
* - Legacy v3: the current CLI format (lock.json-centered, TALK.md routing)
|
|
6
|
+
* - Governed: the current spec format (orchestrator-owned state, structured turn results)
|
|
7
|
+
*
|
|
8
|
+
* Both are normalized into a single internal shape so that all downstream code
|
|
9
|
+
* can operate without branching on config version.
|
|
10
|
+
*
|
|
11
|
+
* Design rule: Legacy projects are supported, not upgraded silently.
|
|
12
|
+
* No automatic rewrite on read.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { validateHooksConfig } from './hook-runner.js';
|
|
16
|
+
|
|
17
|
+
const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
|
|
18
|
+
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy'];
|
|
19
|
+
const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
20
|
+
const VALID_PHASES = ['planning', 'implementation', 'qa'];
|
|
21
|
+
const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
|
|
22
|
+
const VALID_API_PROXY_RETRY_CLASSES = [
|
|
23
|
+
'rate_limited',
|
|
24
|
+
'network_failure',
|
|
25
|
+
'timeout',
|
|
26
|
+
'response_parse_failure',
|
|
27
|
+
'turn_result_extraction_failure',
|
|
28
|
+
'unknown_api_error',
|
|
29
|
+
'provider_overloaded',
|
|
30
|
+
];
|
|
31
|
+
const VALID_API_PROXY_RETRY_POLICY_FIELDS = [
|
|
32
|
+
'enabled',
|
|
33
|
+
'max_attempts',
|
|
34
|
+
'base_delay_ms',
|
|
35
|
+
'max_delay_ms',
|
|
36
|
+
'backoff_multiplier',
|
|
37
|
+
'jitter',
|
|
38
|
+
'retry_on',
|
|
39
|
+
];
|
|
40
|
+
const VALID_API_PROXY_PREFLIGHT_TOKENIZERS = ['provider_local'];
|
|
41
|
+
const VALID_API_PROXY_PREFLIGHT_FIELDS = [
|
|
42
|
+
'enabled',
|
|
43
|
+
'tokenizer',
|
|
44
|
+
'safety_margin_tokens',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function validateApiProxyRetryPolicy(runtimeId, retryPolicy, errors) {
|
|
48
|
+
if (!retryPolicy || typeof retryPolicy !== 'object' || Array.isArray(retryPolicy)) {
|
|
49
|
+
errors.push(`Runtime "${runtimeId}": retry_policy must be an object`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const key of Object.keys(retryPolicy)) {
|
|
54
|
+
if (!VALID_API_PROXY_RETRY_POLICY_FIELDS.includes(key)) {
|
|
55
|
+
errors.push(`Runtime "${runtimeId}": retry_policy contains unknown field "${key}"`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ('enabled' in retryPolicy && typeof retryPolicy.enabled !== 'boolean') {
|
|
60
|
+
errors.push(`Runtime "${runtimeId}": retry_policy.enabled must be a boolean`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if ('max_attempts' in retryPolicy && (!Number.isInteger(retryPolicy.max_attempts) || retryPolicy.max_attempts < 1)) {
|
|
64
|
+
errors.push(`Runtime "${runtimeId}": retry_policy.max_attempts must be an integer >= 1`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if ('base_delay_ms' in retryPolicy && (!Number.isFinite(retryPolicy.base_delay_ms) || retryPolicy.base_delay_ms < 0)) {
|
|
68
|
+
errors.push(`Runtime "${runtimeId}": retry_policy.base_delay_ms must be a finite number >= 0`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if ('max_delay_ms' in retryPolicy && (!Number.isFinite(retryPolicy.max_delay_ms) || retryPolicy.max_delay_ms < 0)) {
|
|
72
|
+
errors.push(`Runtime "${runtimeId}": retry_policy.max_delay_ms must be a finite number >= 0`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
Number.isFinite(retryPolicy.base_delay_ms)
|
|
77
|
+
&& Number.isFinite(retryPolicy.max_delay_ms)
|
|
78
|
+
&& retryPolicy.max_delay_ms < retryPolicy.base_delay_ms
|
|
79
|
+
) {
|
|
80
|
+
errors.push(`Runtime "${runtimeId}": retry_policy.max_delay_ms must be >= retry_policy.base_delay_ms`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
'backoff_multiplier' in retryPolicy
|
|
85
|
+
&& (!Number.isFinite(retryPolicy.backoff_multiplier) || retryPolicy.backoff_multiplier <= 0)
|
|
86
|
+
) {
|
|
87
|
+
errors.push(`Runtime "${runtimeId}": retry_policy.backoff_multiplier must be a finite number > 0`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ('jitter' in retryPolicy && !VALID_API_PROXY_RETRY_JITTER.includes(retryPolicy.jitter)) {
|
|
91
|
+
errors.push(
|
|
92
|
+
`Runtime "${runtimeId}": retry_policy.jitter must be one of: ${VALID_API_PROXY_RETRY_JITTER.join(', ')}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if ('retry_on' in retryPolicy) {
|
|
97
|
+
if (!Array.isArray(retryPolicy.retry_on)) {
|
|
98
|
+
errors.push(`Runtime "${runtimeId}": retry_policy.retry_on must be an array`);
|
|
99
|
+
} else {
|
|
100
|
+
for (const errorClass of retryPolicy.retry_on) {
|
|
101
|
+
if (!VALID_API_PROXY_RETRY_CLASSES.includes(errorClass)) {
|
|
102
|
+
errors.push(
|
|
103
|
+
`Runtime "${runtimeId}": retry_policy.retry_on contains unknown class "${errorClass}"`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateApiProxyPreflightTokenization(runtimeId, runtime, errors) {
|
|
112
|
+
const preflight = runtime?.preflight_tokenization;
|
|
113
|
+
|
|
114
|
+
if ('context_window_tokens' in runtime) {
|
|
115
|
+
if (!Number.isInteger(runtime.context_window_tokens) || runtime.context_window_tokens <= 0) {
|
|
116
|
+
errors.push(`Runtime "${runtimeId}": context_window_tokens must be a positive integer`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!preflight || typeof preflight !== 'object' || Array.isArray(preflight)) {
|
|
121
|
+
errors.push(`Runtime "${runtimeId}": preflight_tokenization must be an object`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const key of Object.keys(preflight)) {
|
|
126
|
+
if (!VALID_API_PROXY_PREFLIGHT_FIELDS.includes(key)) {
|
|
127
|
+
errors.push(`Runtime "${runtimeId}": preflight_tokenization contains unknown field "${key}"`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if ('enabled' in preflight && typeof preflight.enabled !== 'boolean') {
|
|
132
|
+
errors.push(`Runtime "${runtimeId}": preflight_tokenization.enabled must be a boolean`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if ('tokenizer' in preflight && !VALID_API_PROXY_PREFLIGHT_TOKENIZERS.includes(preflight.tokenizer)) {
|
|
136
|
+
errors.push(
|
|
137
|
+
`Runtime "${runtimeId}": preflight_tokenization.tokenizer must be one of: ${VALID_API_PROXY_PREFLIGHT_TOKENIZERS.join(', ')}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
'safety_margin_tokens' in preflight
|
|
143
|
+
&& (!Number.isInteger(preflight.safety_margin_tokens) || preflight.safety_margin_tokens < 0)
|
|
144
|
+
) {
|
|
145
|
+
errors.push(`Runtime "${runtimeId}": preflight_tokenization.safety_margin_tokens must be an integer >= 0`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (preflight.enabled === true) {
|
|
149
|
+
if (!Number.isInteger(runtime.context_window_tokens) || runtime.context_window_tokens <= 0) {
|
|
150
|
+
errors.push(`Runtime "${runtimeId}": context_window_tokens is required when preflight_tokenization.enabled is true`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const maxOutputTokens = Number.isInteger(runtime.max_output_tokens) && runtime.max_output_tokens > 0
|
|
155
|
+
? runtime.max_output_tokens
|
|
156
|
+
: 4096;
|
|
157
|
+
const safetyMarginTokens = Number.isInteger(preflight.safety_margin_tokens)
|
|
158
|
+
? preflight.safety_margin_tokens
|
|
159
|
+
: 2048;
|
|
160
|
+
|
|
161
|
+
if (runtime.context_window_tokens <= maxOutputTokens + safetyMarginTokens) {
|
|
162
|
+
errors.push(
|
|
163
|
+
`Runtime "${runtimeId}": context_window_tokens must be greater than max_output_tokens + preflight_tokenization.safety_margin_tokens`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Detect config generation from raw parsed JSON.
|
|
171
|
+
* Returns 3, 4, or null if unrecognizable.
|
|
172
|
+
*/
|
|
173
|
+
export function detectConfigVersion(raw) {
|
|
174
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
175
|
+
if (raw.schema_version === '1.0' || raw.schema_version === 4) return 4;
|
|
176
|
+
if (raw.version === 3) return 3;
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Validate a governed config.
|
|
182
|
+
* Returns { ok, errors }.
|
|
183
|
+
*/
|
|
184
|
+
export function validateV4Config(data, projectRoot) {
|
|
185
|
+
const errors = [];
|
|
186
|
+
|
|
187
|
+
if (!data || typeof data !== 'object') {
|
|
188
|
+
return { ok: false, errors: ['Config must be a JSON object'] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Top-level required sections
|
|
192
|
+
if (!data.project || typeof data.project !== 'object') {
|
|
193
|
+
errors.push('project must be an object with id and name');
|
|
194
|
+
} else {
|
|
195
|
+
if (typeof data.project.id !== 'string' || !data.project.id.trim()) errors.push('project.id must be a non-empty string');
|
|
196
|
+
if (typeof data.project.name !== 'string' || !data.project.name.trim()) errors.push('project.name must be a non-empty string');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Roles
|
|
200
|
+
if (!data.roles || typeof data.roles !== 'object') {
|
|
201
|
+
errors.push('roles must be an object');
|
|
202
|
+
} else {
|
|
203
|
+
for (const [id, role] of Object.entries(data.roles)) {
|
|
204
|
+
if (!/^[a-z0-9_-]+$/.test(id)) errors.push(`Invalid role id: "${id}"`);
|
|
205
|
+
if (!role || typeof role !== 'object') { errors.push(`Role "${id}" must be an object`); continue; }
|
|
206
|
+
if (typeof role.title !== 'string' || !role.title.trim()) errors.push(`Role "${id}": title required`);
|
|
207
|
+
if (typeof role.mandate !== 'string' || !role.mandate.trim()) errors.push(`Role "${id}": mandate required`);
|
|
208
|
+
if (!VALID_WRITE_AUTHORITIES.includes(role.write_authority)) {
|
|
209
|
+
errors.push(`Role "${id}": write_authority must be one of: ${VALID_WRITE_AUTHORITIES.join(', ')}`);
|
|
210
|
+
}
|
|
211
|
+
if (typeof role.runtime !== 'string' || !role.runtime.trim()) errors.push(`Role "${id}": runtime required`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Runtimes
|
|
216
|
+
if (!data.runtimes || typeof data.runtimes !== 'object') {
|
|
217
|
+
errors.push('runtimes must be an object');
|
|
218
|
+
} else {
|
|
219
|
+
for (const [id, rt] of Object.entries(data.runtimes)) {
|
|
220
|
+
if (!rt || typeof rt !== 'object') { errors.push(`Runtime "${id}" must be an object`); continue; }
|
|
221
|
+
if (!VALID_RUNTIME_TYPES.includes(rt.type)) {
|
|
222
|
+
errors.push(`Runtime "${id}": type must be one of: ${VALID_RUNTIME_TYPES.join(', ')}`);
|
|
223
|
+
}
|
|
224
|
+
// Validate prompt_transport for local_cli runtimes
|
|
225
|
+
if (rt.type === 'local_cli' && rt.prompt_transport) {
|
|
226
|
+
if (!VALID_PROMPT_TRANSPORTS.includes(rt.prompt_transport)) {
|
|
227
|
+
errors.push(`Runtime "${id}": prompt_transport must be one of: ${VALID_PROMPT_TRANSPORTS.join(', ')}`);
|
|
228
|
+
}
|
|
229
|
+
if (rt.prompt_transport === 'argv') {
|
|
230
|
+
// Verify {prompt} placeholder exists in command/args
|
|
231
|
+
const parts = Array.isArray(rt.command) ? rt.command : [rt.command, ...(rt.args || [])];
|
|
232
|
+
const hasPlaceholder = parts.some(p => typeof p === 'string' && p.includes('{prompt}'));
|
|
233
|
+
if (!hasPlaceholder) {
|
|
234
|
+
errors.push(`Runtime "${id}": prompt_transport is "argv" but command/args do not contain {prompt} placeholder`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Validate api_proxy required fields (Session #19 freeze)
|
|
239
|
+
if (rt.type === 'api_proxy') {
|
|
240
|
+
if (typeof rt.provider !== 'string' || !rt.provider.trim()) {
|
|
241
|
+
errors.push(`Runtime "${id}": api_proxy requires "provider" (e.g. "anthropic", "openai")`);
|
|
242
|
+
}
|
|
243
|
+
if (typeof rt.model !== 'string' || !rt.model.trim()) {
|
|
244
|
+
errors.push(`Runtime "${id}": api_proxy requires "model" (e.g. "claude-sonnet-4-6")`);
|
|
245
|
+
}
|
|
246
|
+
if (typeof rt.auth_env !== 'string' || !rt.auth_env.trim()) {
|
|
247
|
+
errors.push(`Runtime "${id}": api_proxy requires "auth_env" (environment variable name for API key)`);
|
|
248
|
+
}
|
|
249
|
+
if ('retry_policy' in rt) {
|
|
250
|
+
validateApiProxyRetryPolicy(id, rt.retry_policy, errors);
|
|
251
|
+
}
|
|
252
|
+
if ('preflight_tokenization' in rt || 'context_window_tokens' in rt) {
|
|
253
|
+
validateApiProxyPreflightTokenization(id, rt, errors);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Cross-references: every role.runtime must reference an existing runtime
|
|
260
|
+
if (data.roles && data.runtimes) {
|
|
261
|
+
for (const [id, role] of Object.entries(data.roles)) {
|
|
262
|
+
if (role.runtime && !data.runtimes[role.runtime]) {
|
|
263
|
+
errors.push(`Role "${id}" references unknown runtime "${role.runtime}"`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Cross-reference: review_only roles should not use authoritative runtimes
|
|
269
|
+
if (data.roles && data.runtimes) {
|
|
270
|
+
for (const [id, role] of Object.entries(data.roles)) {
|
|
271
|
+
if (role.write_authority === 'review_only' && role.runtime && data.runtimes[role.runtime]) {
|
|
272
|
+
const rt = data.runtimes[role.runtime];
|
|
273
|
+
if (rt.type === 'local_cli') {
|
|
274
|
+
errors.push(`Role "${id}" is review_only but uses local_cli runtime "${role.runtime}" — review_only roles should not have authoritative write access`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// v1 api_proxy restriction: only review_only roles may bind to api_proxy runtimes (Session #19 freeze)
|
|
278
|
+
if (role.runtime && data.runtimes[role.runtime]) {
|
|
279
|
+
const rt = data.runtimes[role.runtime];
|
|
280
|
+
if (rt.type === 'api_proxy' && role.write_authority !== 'review_only') {
|
|
281
|
+
errors.push(`Role "${id}" has write_authority "${role.write_authority}" but uses api_proxy runtime "${role.runtime}" — v1 api_proxy only supports review_only roles`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Routing (optional but validated if present)
|
|
288
|
+
if (data.routing) {
|
|
289
|
+
for (const [phase, route] of Object.entries(data.routing)) {
|
|
290
|
+
if (!VALID_PHASES.includes(phase)) {
|
|
291
|
+
errors.push(`Routing references unknown phase: "${phase}"`);
|
|
292
|
+
}
|
|
293
|
+
if (route.entry_role && data.roles && !data.roles[route.entry_role]) {
|
|
294
|
+
errors.push(`Routing "${phase}": entry_role "${route.entry_role}" is not a defined role`);
|
|
295
|
+
}
|
|
296
|
+
if (route.allowed_next_roles && Array.isArray(route.allowed_next_roles)) {
|
|
297
|
+
for (const r of route.allowed_next_roles) {
|
|
298
|
+
if (r !== 'human' && data.roles && !data.roles[r]) {
|
|
299
|
+
errors.push(`Routing "${phase}": allowed_next_roles references unknown role "${r}"`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if ('max_concurrent_turns' in route) {
|
|
304
|
+
if (!Number.isInteger(route.max_concurrent_turns) || route.max_concurrent_turns < 1 || route.max_concurrent_turns > 4) {
|
|
305
|
+
errors.push(`Routing "${phase}": max_concurrent_turns must be an integer between 1 and 4`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Gates (optional but validated if present)
|
|
312
|
+
if (data.gates) {
|
|
313
|
+
if (data.routing) {
|
|
314
|
+
for (const [, route] of Object.entries(data.routing)) {
|
|
315
|
+
if (route.exit_gate && !data.gates[route.exit_gate]) {
|
|
316
|
+
errors.push(`Routing references unknown gate: "${route.exit_gate}"`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Hooks (optional but validated if present)
|
|
323
|
+
if (data.hooks) {
|
|
324
|
+
const hookValidation = validateHooksConfig(data.hooks, projectRoot || null);
|
|
325
|
+
errors.push(...hookValidation.errors);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { ok: errors.length === 0, errors };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Normalize a legacy v3 config into the internal shape.
|
|
333
|
+
* Does NOT modify the original file — this is a read-time transformation.
|
|
334
|
+
*/
|
|
335
|
+
export function normalizeV3(raw) {
|
|
336
|
+
const agents = {};
|
|
337
|
+
if (raw.agents && typeof raw.agents === 'object') {
|
|
338
|
+
for (const [id, agent] of Object.entries(raw.agents)) {
|
|
339
|
+
agents[id] = {
|
|
340
|
+
title: agent.name || id,
|
|
341
|
+
mandate: agent.mandate || '',
|
|
342
|
+
write_authority: inferWriteAuthority(id),
|
|
343
|
+
runtime_class: inferRuntimeClass(id),
|
|
344
|
+
runtime_id: `legacy-${id}`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const runtimes = {};
|
|
350
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
351
|
+
runtimes[agent.runtime_id] = {
|
|
352
|
+
type: agent.runtime_class,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
schema_version: 3,
|
|
358
|
+
protocol_mode: 'legacy',
|
|
359
|
+
template: null,
|
|
360
|
+
project: {
|
|
361
|
+
id: slugify(raw.project || 'unknown'),
|
|
362
|
+
name: raw.project || 'Unknown Project',
|
|
363
|
+
default_branch: 'main',
|
|
364
|
+
},
|
|
365
|
+
roles: agents,
|
|
366
|
+
runtimes,
|
|
367
|
+
routing: buildLegacyRouting(Object.keys(agents)),
|
|
368
|
+
gates: {},
|
|
369
|
+
hooks: {},
|
|
370
|
+
budget: null,
|
|
371
|
+
retention: {
|
|
372
|
+
talk_strategy: 'append_only',
|
|
373
|
+
history_strategy: 'jsonl_append_only',
|
|
374
|
+
},
|
|
375
|
+
rules: {
|
|
376
|
+
challenge_required: raw.rules?.require_message ?? true,
|
|
377
|
+
max_turn_retries: 2,
|
|
378
|
+
max_deadlock_cycles: 2,
|
|
379
|
+
max_consecutive_claims: raw.rules?.max_consecutive_claims ?? 2,
|
|
380
|
+
verify_command: raw.rules?.verify_command ?? null,
|
|
381
|
+
compress_after_words: raw.rules?.compress_after_words ?? null,
|
|
382
|
+
ttl_minutes: raw.rules?.ttl_minutes ?? 20,
|
|
383
|
+
},
|
|
384
|
+
files: {
|
|
385
|
+
talk: raw.talk_file || 'TALK.md',
|
|
386
|
+
history: raw.history_file || 'history.jsonl',
|
|
387
|
+
state: raw.state_file || 'state.json',
|
|
388
|
+
log: raw.log || 'log.md',
|
|
389
|
+
},
|
|
390
|
+
compat: {
|
|
391
|
+
next_owner_source: 'talk-md',
|
|
392
|
+
lock_based_coordination: true,
|
|
393
|
+
original_version: 3,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Normalize a governed config into the internal shape.
|
|
400
|
+
*/
|
|
401
|
+
export function normalizeV4(raw) {
|
|
402
|
+
const roles = {};
|
|
403
|
+
if (raw.roles) {
|
|
404
|
+
for (const [id, role] of Object.entries(raw.roles)) {
|
|
405
|
+
roles[id] = {
|
|
406
|
+
title: role.title,
|
|
407
|
+
mandate: role.mandate,
|
|
408
|
+
write_authority: role.write_authority,
|
|
409
|
+
runtime_class: raw.runtimes?.[role.runtime]?.type || 'manual',
|
|
410
|
+
runtime_id: role.runtime,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
schema_version: 4,
|
|
417
|
+
protocol_mode: 'governed',
|
|
418
|
+
template: raw.template || 'generic',
|
|
419
|
+
project: {
|
|
420
|
+
id: raw.project?.id || 'unknown',
|
|
421
|
+
name: raw.project?.name || 'Unknown',
|
|
422
|
+
default_branch: raw.project?.default_branch || 'main',
|
|
423
|
+
},
|
|
424
|
+
roles,
|
|
425
|
+
runtimes: raw.runtimes || {},
|
|
426
|
+
routing: raw.routing || {},
|
|
427
|
+
gates: raw.gates || {},
|
|
428
|
+
hooks: raw.hooks || {},
|
|
429
|
+
budget: raw.budget || null,
|
|
430
|
+
retention: raw.retention || {
|
|
431
|
+
talk_strategy: 'append_only',
|
|
432
|
+
history_strategy: 'jsonl_append_only',
|
|
433
|
+
},
|
|
434
|
+
rules: {
|
|
435
|
+
challenge_required: raw.rules?.challenge_required ?? true,
|
|
436
|
+
max_turn_retries: raw.rules?.max_turn_retries ?? 2,
|
|
437
|
+
max_deadlock_cycles: raw.rules?.max_deadlock_cycles ?? 2,
|
|
438
|
+
max_consecutive_claims: null,
|
|
439
|
+
verify_command: null,
|
|
440
|
+
compress_after_words: null,
|
|
441
|
+
ttl_minutes: null,
|
|
442
|
+
},
|
|
443
|
+
files: {
|
|
444
|
+
talk: 'TALK.md',
|
|
445
|
+
history: '.agentxchain/history.jsonl',
|
|
446
|
+
state: '.agentxchain/state.json',
|
|
447
|
+
log: null,
|
|
448
|
+
},
|
|
449
|
+
compat: {
|
|
450
|
+
next_owner_source: 'state-json',
|
|
451
|
+
lock_based_coordination: false,
|
|
452
|
+
original_version: 4,
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Load and normalize a config from raw JSON.
|
|
459
|
+
* Returns { ok, normalized, errors, version }.
|
|
460
|
+
*/
|
|
461
|
+
export function loadNormalizedConfig(raw, projectRoot) {
|
|
462
|
+
const version = detectConfigVersion(raw);
|
|
463
|
+
|
|
464
|
+
if (version === null) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
normalized: null,
|
|
468
|
+
errors: ['Unrecognized config format. Expected version: 3 or schema_version: "1.0" / 4'],
|
|
469
|
+
version: null,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (version === 3) {
|
|
474
|
+
// Use the existing v3 validator for basic shape checks
|
|
475
|
+
const errors = [];
|
|
476
|
+
if (typeof raw.project !== 'string' || !raw.project.trim()) errors.push('project must be a non-empty string');
|
|
477
|
+
if (!raw.agents || typeof raw.agents !== 'object') {
|
|
478
|
+
errors.push('agents must be an object');
|
|
479
|
+
} else {
|
|
480
|
+
for (const [id, agent] of Object.entries(raw.agents)) {
|
|
481
|
+
if (!/^[a-z0-9_-]+$/.test(id)) errors.push(`Invalid agent id: "${id}"`);
|
|
482
|
+
if (!agent || typeof agent !== 'object') { errors.push(`Agent "${id}" must be an object`); continue; }
|
|
483
|
+
if (typeof agent.name !== 'string' || !agent.name.trim()) errors.push(`Agent "${id}": name required`);
|
|
484
|
+
if (typeof agent.mandate !== 'string' || !agent.mandate.trim()) errors.push(`Agent "${id}": mandate required`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (errors.length > 0) {
|
|
488
|
+
return { ok: false, normalized: null, errors, version: 3 };
|
|
489
|
+
}
|
|
490
|
+
return { ok: true, normalized: normalizeV3(raw), errors: [], version: 3 };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (version === 4) {
|
|
494
|
+
const validation = validateV4Config(raw, projectRoot || null);
|
|
495
|
+
if (!validation.ok) {
|
|
496
|
+
return { ok: false, normalized: null, errors: validation.errors, version: 4 };
|
|
497
|
+
}
|
|
498
|
+
return { ok: true, normalized: normalizeV4(raw), errors: [], version: 4 };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function getMaxConcurrentTurns(config, phase) {
|
|
503
|
+
const configured = config?.routing?.[phase]?.max_concurrent_turns;
|
|
504
|
+
if (!Number.isInteger(configured) || configured < 1) {
|
|
505
|
+
return 1;
|
|
506
|
+
}
|
|
507
|
+
return Math.min(configured, 4);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
// --- Internal helpers ---
|
|
512
|
+
|
|
513
|
+
function inferWriteAuthority(agentId) {
|
|
514
|
+
const id = agentId.toLowerCase();
|
|
515
|
+
if (id.includes('pm') || id.includes('product') || id.includes('manager')) return 'review_only';
|
|
516
|
+
if (id.includes('qa') || id.includes('test') || id.includes('quality')) return 'review_only';
|
|
517
|
+
if (id.includes('ux') || id.includes('design') || id.includes('reviewer')) return 'review_only';
|
|
518
|
+
if (id.includes('director') || id.includes('lead') || id.includes('architect')) return 'review_only';
|
|
519
|
+
return 'authoritative';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function inferRuntimeClass(agentId) {
|
|
523
|
+
// In legacy mode, all agents are effectively manual (user pastes prompts)
|
|
524
|
+
return 'manual';
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function buildLegacyRouting(agentIds) {
|
|
528
|
+
// Legacy doesn't have formal routing — build a simple pass-through
|
|
529
|
+
return {
|
|
530
|
+
default: {
|
|
531
|
+
sequence: agentIds,
|
|
532
|
+
exit_gate: null,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function slugify(str) {
|
|
538
|
+
return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
539
|
+
}
|
package/src/lib/notify.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
function sanitize(str) {
|
|
4
|
+
return String(str).replace(/[\\"]/g, ' ').replace(/'/g, ' ').slice(0, 200);
|
|
5
|
+
}
|
|
2
6
|
|
|
3
7
|
export function notifyHuman(message, title = 'AgentXchain') {
|
|
4
|
-
// Terminal bell
|
|
5
8
|
process.stdout.write('\x07');
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
const safeMsg = sanitize(message);
|
|
11
|
+
const safeTitle = sanitize(title);
|
|
12
|
+
|
|
8
13
|
if (process.platform === 'darwin') {
|
|
9
14
|
try {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
15
|
+
execFileSync('osascript', [
|
|
16
|
+
'-e', `display notification "${safeMsg}" with title "${safeTitle}"`
|
|
17
|
+
], { stdio: 'ignore' });
|
|
18
|
+
} catch {}
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
// Linux notification
|
|
17
21
|
if (process.platform === 'linux') {
|
|
18
22
|
try {
|
|
19
|
-
|
|
20
|
-
} catch {
|
|
21
|
-
// notify-send not available
|
|
22
|
-
}
|
|
23
|
+
execFileSync('notify-send', [safeTitle, safeMsg], { stdio: 'ignore' });
|
|
24
|
+
} catch {}
|
|
23
25
|
}
|
|
24
26
|
}
|