agentxchain 2.82.0 → 2.84.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 +1 -1
- package/scripts/model-compatibility-probe.mjs +312 -0
- package/scripts/release-bump.sh +5 -9
- package/src/commands/config.js +6 -0
- package/src/commands/doctor.js +4 -1
- package/src/commands/intake-status.js +21 -3
- package/src/commands/restart.js +1 -1
- package/src/commands/resume.js +53 -0
- package/src/commands/run.js +19 -0
- package/src/commands/status.js +23 -1
- package/src/commands/step.js +50 -0
- package/src/lib/governed-state.js +1 -1
- package/src/lib/intake.js +124 -1
- package/src/lib/normalized-config.js +81 -6
- package/src/lib/validation.js +4 -0
package/package.json
CHANGED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Model Compatibility Probe — api_proxy + proposed write authority
|
|
4
|
+
*
|
|
5
|
+
* Dispatches a lightweight governed turn to each target model and records
|
|
6
|
+
* whether the model can produce a well-formed turn result with proposed_changes.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node cli/scripts/model-compatibility-probe.mjs
|
|
10
|
+
* node cli/scripts/model-compatibility-probe.mjs --json
|
|
11
|
+
*
|
|
12
|
+
* Requires ANTHROPIC_API_KEY in the environment.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ANTHROPIC_ENDPOINT = 'https://api.anthropic.com/v1/messages';
|
|
16
|
+
|
|
17
|
+
const SYSTEM_PROMPT = [
|
|
18
|
+
'You are acting as a governed agent in an AgentXchain protocol run.',
|
|
19
|
+
'Your task and rules are described in the user message.',
|
|
20
|
+
'You MUST respond with a valid JSON object matching the turn result schema provided in the prompt.',
|
|
21
|
+
'Do NOT wrap the JSON in markdown code fences. Respond with raw JSON only.',
|
|
22
|
+
].join('\n');
|
|
23
|
+
|
|
24
|
+
const PROBE_PROMPT = `You are a governed agent with write_authority: "proposed".
|
|
25
|
+
|
|
26
|
+
Your task: create a single file called \`probe-result.txt\` containing the text "model compatibility probe passed".
|
|
27
|
+
|
|
28
|
+
Respond with a single raw JSON object (no markdown fences) matching this exact schema:
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
"schema_version": "1.0",
|
|
32
|
+
"run_id": "probe_run",
|
|
33
|
+
"turn_id": "probe_turn_001",
|
|
34
|
+
"role": "dev",
|
|
35
|
+
"runtime_id": "probe_runtime",
|
|
36
|
+
"status": "completed",
|
|
37
|
+
"summary": "Created probe-result.txt for model compatibility verification.",
|
|
38
|
+
"decisions": [],
|
|
39
|
+
"objections": [],
|
|
40
|
+
"files_changed": ["probe-result.txt"],
|
|
41
|
+
"verification": { "status": "pass", "evidence": "File created successfully." },
|
|
42
|
+
"artifact": { "type": "patch", "ref": null },
|
|
43
|
+
"proposed_next_role": "human",
|
|
44
|
+
"proposed_changes": [
|
|
45
|
+
{
|
|
46
|
+
"path": "probe-result.txt",
|
|
47
|
+
"action": "create",
|
|
48
|
+
"content": "model compatibility probe passed"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Return ONLY the JSON object. No explanation, no markdown fences, no extra text.`;
|
|
54
|
+
|
|
55
|
+
const MODELS = [
|
|
56
|
+
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', maxTokens: 2048, costInput: 1.00, costOutput: 5.00 },
|
|
57
|
+
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', maxTokens: 2048, costInput: 3.00, costOutput: 15.00 },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
function extractTurnResult(text) {
|
|
61
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
62
|
+
return { ok: false, error: 'Empty response text' };
|
|
63
|
+
}
|
|
64
|
+
const trimmed = text.trim();
|
|
65
|
+
|
|
66
|
+
// Direct JSON parse
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(trimmed);
|
|
69
|
+
if (parsed && typeof parsed === 'object' && parsed.schema_version) {
|
|
70
|
+
return { ok: true, turnResult: parsed, method: 'direct' };
|
|
71
|
+
}
|
|
72
|
+
} catch { /* not pure JSON */ }
|
|
73
|
+
|
|
74
|
+
// Markdown fence extraction
|
|
75
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
|
|
76
|
+
if (fenceMatch) {
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(fenceMatch[1].trim());
|
|
79
|
+
if (parsed && typeof parsed === 'object' && parsed.schema_version) {
|
|
80
|
+
return { ok: true, turnResult: parsed, method: 'fence' };
|
|
81
|
+
}
|
|
82
|
+
} catch { /* invalid JSON inside fence */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Substring extraction
|
|
86
|
+
const jsonStart = trimmed.indexOf('{');
|
|
87
|
+
const jsonEnd = trimmed.lastIndexOf('}');
|
|
88
|
+
if (jsonStart >= 0 && jsonEnd > jsonStart) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(trimmed.slice(jsonStart, jsonEnd + 1));
|
|
91
|
+
if (parsed && typeof parsed === 'object' && parsed.schema_version) {
|
|
92
|
+
return { ok: true, turnResult: parsed, method: 'substring' };
|
|
93
|
+
}
|
|
94
|
+
} catch { /* not valid JSON */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { ok: false, error: 'Could not extract structured turn result JSON' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateProposedChanges(turnResult) {
|
|
101
|
+
const changes = turnResult?.proposed_changes;
|
|
102
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
103
|
+
return { present: false, wellFormed: false, reason: 'proposed_changes missing or empty' };
|
|
104
|
+
}
|
|
105
|
+
for (const c of changes) {
|
|
106
|
+
if (!c.path || typeof c.path !== 'string') {
|
|
107
|
+
return { present: true, wellFormed: false, reason: `entry missing path` };
|
|
108
|
+
}
|
|
109
|
+
if (!['create', 'modify', 'delete'].includes(c.action)) {
|
|
110
|
+
return { present: true, wellFormed: false, reason: `invalid action: ${c.action}` };
|
|
111
|
+
}
|
|
112
|
+
if ((c.action === 'create' || c.action === 'modify') && (!c.content || typeof c.content !== 'string')) {
|
|
113
|
+
return { present: true, wellFormed: false, reason: `${c.action} entry missing content for ${c.path}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { present: true, wellFormed: true, count: changes.length };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function probeModel(model, apiKey) {
|
|
120
|
+
const startMs = Date.now();
|
|
121
|
+
const body = {
|
|
122
|
+
model: model.id,
|
|
123
|
+
max_tokens: model.maxTokens,
|
|
124
|
+
system: SYSTEM_PROMPT,
|
|
125
|
+
messages: [{ role: 'user', content: PROBE_PROMPT }],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
let responseData;
|
|
129
|
+
let rawError = null;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(ANTHROPIC_ENDPOINT, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'x-api-key': apiKey,
|
|
137
|
+
'anthropic-version': '2023-06-01',
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify(body),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
const errorBody = await res.text().catch(() => '');
|
|
144
|
+
rawError = `HTTP ${res.status}: ${errorBody.slice(0, 500)}`;
|
|
145
|
+
return {
|
|
146
|
+
model: model.id,
|
|
147
|
+
label: model.label,
|
|
148
|
+
extraction_success: false,
|
|
149
|
+
schema_valid: false,
|
|
150
|
+
proposed_changes_present: false,
|
|
151
|
+
proposed_changes_well_formed: false,
|
|
152
|
+
latency_ms: Date.now() - startMs,
|
|
153
|
+
cost_usd: 0,
|
|
154
|
+
classification: 'unsupported',
|
|
155
|
+
raw_error: rawError,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
responseData = await res.json();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
model: model.id,
|
|
163
|
+
label: model.label,
|
|
164
|
+
extraction_success: false,
|
|
165
|
+
schema_valid: false,
|
|
166
|
+
proposed_changes_present: false,
|
|
167
|
+
proposed_changes_well_formed: false,
|
|
168
|
+
latency_ms: Date.now() - startMs,
|
|
169
|
+
cost_usd: 0,
|
|
170
|
+
classification: 'unsupported',
|
|
171
|
+
raw_error: err.message,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const latencyMs = Date.now() - startMs;
|
|
176
|
+
|
|
177
|
+
// Extract text from Anthropic response
|
|
178
|
+
const textBlock = responseData?.content?.find(b => b.type === 'text');
|
|
179
|
+
const responseText = textBlock?.text || '';
|
|
180
|
+
|
|
181
|
+
// Extract turn result
|
|
182
|
+
const extraction = extractTurnResult(responseText);
|
|
183
|
+
|
|
184
|
+
// Calculate cost
|
|
185
|
+
const usage = responseData?.usage || {};
|
|
186
|
+
const inputTokens = usage.input_tokens || 0;
|
|
187
|
+
const outputTokens = usage.output_tokens || 0;
|
|
188
|
+
const costUsd = (inputTokens / 1_000_000) * model.costInput + (outputTokens / 1_000_000) * model.costOutput;
|
|
189
|
+
|
|
190
|
+
if (!extraction.ok) {
|
|
191
|
+
return {
|
|
192
|
+
model: model.id,
|
|
193
|
+
label: model.label,
|
|
194
|
+
extraction_success: false,
|
|
195
|
+
extraction_method: null,
|
|
196
|
+
schema_valid: false,
|
|
197
|
+
proposed_changes_present: false,
|
|
198
|
+
proposed_changes_well_formed: false,
|
|
199
|
+
latency_ms: latencyMs,
|
|
200
|
+
input_tokens: inputTokens,
|
|
201
|
+
output_tokens: outputTokens,
|
|
202
|
+
cost_usd: Math.round(costUsd * 1_000_000) / 1_000_000,
|
|
203
|
+
classification: 'unsupported',
|
|
204
|
+
raw_error: extraction.error,
|
|
205
|
+
response_preview: responseText.slice(0, 300),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const tr = extraction.turnResult;
|
|
210
|
+
const schemaValid = typeof tr.schema_version === 'string' && tr.schema_version === '1.0';
|
|
211
|
+
const pcValidation = validateProposedChanges(tr);
|
|
212
|
+
|
|
213
|
+
let classification;
|
|
214
|
+
if (schemaValid && pcValidation.present && pcValidation.wellFormed) {
|
|
215
|
+
classification = 'reliable';
|
|
216
|
+
} else if (extraction.ok && (!pcValidation.present || !pcValidation.wellFormed)) {
|
|
217
|
+
classification = 'inconsistent';
|
|
218
|
+
} else {
|
|
219
|
+
classification = 'unsupported';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
model: model.id,
|
|
224
|
+
label: model.label,
|
|
225
|
+
extraction_success: true,
|
|
226
|
+
extraction_method: extraction.method,
|
|
227
|
+
schema_valid: schemaValid,
|
|
228
|
+
proposed_changes_present: pcValidation.present,
|
|
229
|
+
proposed_changes_well_formed: pcValidation.wellFormed,
|
|
230
|
+
proposed_changes_count: pcValidation.count || 0,
|
|
231
|
+
latency_ms: latencyMs,
|
|
232
|
+
input_tokens: inputTokens,
|
|
233
|
+
output_tokens: outputTokens,
|
|
234
|
+
cost_usd: Math.round(costUsd * 1_000_000) / 1_000_000,
|
|
235
|
+
classification,
|
|
236
|
+
raw_error: pcValidation.reason || null,
|
|
237
|
+
status_returned: tr.status,
|
|
238
|
+
summary_returned: typeof tr.summary === 'string' ? tr.summary.slice(0, 100) : null,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function main() {
|
|
243
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
244
|
+
if (!apiKey) {
|
|
245
|
+
console.error('ANTHROPIC_API_KEY not set');
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const jsonMode = process.argv.includes('--json');
|
|
250
|
+
|
|
251
|
+
if (!jsonMode) {
|
|
252
|
+
console.log('AgentXchain Model Compatibility Probe');
|
|
253
|
+
console.log('Provider: Anthropic | Write Authority: proposed');
|
|
254
|
+
console.log('─'.repeat(60));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const results = [];
|
|
258
|
+
|
|
259
|
+
for (const model of MODELS) {
|
|
260
|
+
if (!jsonMode) {
|
|
261
|
+
process.stdout.write(`Probing ${model.label} (${model.id})... `);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result = await probeModel(model, apiKey);
|
|
265
|
+
results.push(result);
|
|
266
|
+
|
|
267
|
+
if (!jsonMode) {
|
|
268
|
+
const icon = result.classification === 'reliable' ? '✓' : result.classification === 'inconsistent' ? '~' : '✗';
|
|
269
|
+
console.log(`${icon} ${result.classification} (${result.latency_ms}ms, $${result.cost_usd})`);
|
|
270
|
+
if (result.raw_error) {
|
|
271
|
+
console.log(` └─ ${result.raw_error}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const output = {
|
|
277
|
+
probe_version: '1.0',
|
|
278
|
+
timestamp: new Date().toISOString(),
|
|
279
|
+
provider: 'anthropic',
|
|
280
|
+
write_authority: 'proposed',
|
|
281
|
+
models: results,
|
|
282
|
+
total_cost_usd: results.reduce((s, r) => s + r.cost_usd, 0),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (jsonMode) {
|
|
286
|
+
console.log(JSON.stringify(output, null, 2));
|
|
287
|
+
} else {
|
|
288
|
+
console.log('─'.repeat(60));
|
|
289
|
+
console.log(`Total cost: $${output.total_cost_usd.toFixed(6)}`);
|
|
290
|
+
console.log();
|
|
291
|
+
console.log('Matrix:');
|
|
292
|
+
for (const r of results) {
|
|
293
|
+
console.log(` ${r.label.padEnd(12)} ${r.classification.padEnd(14)} extraction=${r.extraction_success} schema=${r.schema_valid} proposed=${r.proposed_changes_well_formed}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Write results to .planning/ for durable reference
|
|
298
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
299
|
+
const { dirname, join } = await import('node:path');
|
|
300
|
+
const { fileURLToPath } = await import('node:url');
|
|
301
|
+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
302
|
+
const outPath = join(repoRoot, '.planning', 'MODEL_COMPATIBILITY_RESULTS.json');
|
|
303
|
+
writeFileSync(outPath, JSON.stringify(output, null, 2) + '\n');
|
|
304
|
+
if (!jsonMode) {
|
|
305
|
+
console.log(`\nResults written to: .planning/MODEL_COMPATIBILITY_RESULTS.json`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
main().catch(err => {
|
|
310
|
+
console.error(err);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
});
|
package/scripts/release-bump.sh
CHANGED
|
@@ -62,7 +62,6 @@ ALLOWED_RELEASE_PATHS=(
|
|
|
62
62
|
"website-v2/docs/protocol-implementor-guide.mdx"
|
|
63
63
|
".planning/LAUNCH_EVIDENCE_REPORT.md"
|
|
64
64
|
"website-v2/static/llms.txt"
|
|
65
|
-
"website-v2/static/sitemap.xml"
|
|
66
65
|
"cli/homebrew/agentxchain.rb"
|
|
67
66
|
"cli/homebrew/README.md"
|
|
68
67
|
)
|
|
@@ -148,9 +147,9 @@ if [[ ! -f "${REPO_ROOT}/${RELEASE_DOC_PATH}" ]]; then
|
|
|
148
147
|
SURFACE_ERRORS+=("release notes page missing: ${RELEASE_DOC_PATH}")
|
|
149
148
|
fi
|
|
150
149
|
|
|
151
|
-
# 4c. Docs sidebar
|
|
152
|
-
if ! grep -q "
|
|
153
|
-
SURFACE_ERRORS+=("sidebars.ts does not
|
|
150
|
+
# 4c. Docs sidebar auto-generates releases from dirName (release doc existence is sufficient)
|
|
151
|
+
if ! grep -q "dirName.*releases" "${REPO_ROOT}/website-v2/sidebars.ts" 2>/dev/null; then
|
|
152
|
+
SURFACE_ERRORS+=("sidebars.ts does not auto-generate releases (missing dirName: 'releases')")
|
|
154
153
|
fi
|
|
155
154
|
|
|
156
155
|
# 4d. Homepage hero badge shows target version
|
|
@@ -181,10 +180,7 @@ if ! grep -q "${CURRENT_RELEASE_ROUTE}" "${REPO_ROOT}/website-v2/static/llms.txt
|
|
|
181
180
|
SURFACE_ERRORS+=("website-v2/static/llms.txt does not list '${CURRENT_RELEASE_ROUTE}'")
|
|
182
181
|
fi
|
|
183
182
|
|
|
184
|
-
# 4i. sitemap.xml
|
|
185
|
-
if ! grep -q "${CURRENT_RELEASE_ROUTE}" "${REPO_ROOT}/website-v2/static/sitemap.xml" 2>/dev/null; then
|
|
186
|
-
SURFACE_ERRORS+=("website-v2/static/sitemap.xml does not list '${CURRENT_RELEASE_ROUTE}'")
|
|
187
|
-
fi
|
|
183
|
+
# 4i. sitemap.xml is now auto-generated by Docusaurus at build time — no static file check needed
|
|
188
184
|
|
|
189
185
|
if [[ "${#SURFACE_ERRORS[@]}" -gt 0 ]]; then
|
|
190
186
|
echo "FAIL: ${#SURFACE_ERRORS[@]} version-surface(s) not aligned to ${TARGET_VERSION}:" >&2
|
|
@@ -194,7 +190,7 @@ if [[ "${#SURFACE_ERRORS[@]}" -gt 0 ]]; then
|
|
|
194
190
|
echo "create release identity when governed surfaces are stale." >&2
|
|
195
191
|
exit 1
|
|
196
192
|
fi
|
|
197
|
-
echo " OK: all
|
|
193
|
+
echo " OK: all 8 governed version surfaces reference ${TARGET_VERSION}"
|
|
198
194
|
|
|
199
195
|
# 5. Auto-align Homebrew mirror to target version
|
|
200
196
|
# The formula URL and README version/tarball are updated automatically.
|
package/src/commands/config.js
CHANGED
|
@@ -219,6 +219,12 @@ function setSetting(config, configPath, keyValPair, context) {
|
|
|
219
219
|
console.log('');
|
|
220
220
|
console.log(chalk.green(` ✓ Set ${chalk.bold(key)} = ${val}`));
|
|
221
221
|
if (oldVal !== undefined) console.log(chalk.dim(` (was: ${oldVal})`));
|
|
222
|
+
if ((validation.warnings || []).length > 0) {
|
|
223
|
+
console.log(chalk.yellow(' Warnings:'));
|
|
224
|
+
for (const warning of validation.warnings) {
|
|
225
|
+
console.log(chalk.dim(` - ${warning}`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
222
228
|
console.log('');
|
|
223
229
|
}
|
|
224
230
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -51,8 +51,11 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
51
51
|
|
|
52
52
|
// 1. Config validation
|
|
53
53
|
const configResult = loadNormalizedConfig(rawConfig, root);
|
|
54
|
-
if (configResult.ok) {
|
|
54
|
+
if (configResult.ok && (configResult.warnings || []).length === 0) {
|
|
55
55
|
checks.push({ id: 'config_valid', name: 'Config validation', level: 'pass', detail: 'Config loads and validates' });
|
|
56
|
+
} else if (configResult.ok) {
|
|
57
|
+
const warningSummary = configResult.warnings.slice(0, 2).join('; ');
|
|
58
|
+
checks.push({ id: 'config_valid', name: 'Config validation', level: 'warn', detail: warningSummary });
|
|
56
59
|
} else {
|
|
57
60
|
const errorSummary = configResult.errors.slice(0, 3).join('; ');
|
|
58
61
|
checks.push({ id: 'config_valid', name: 'Config validation', level: 'fail', detail: errorSummary });
|
|
@@ -19,7 +19,7 @@ export async function intakeStatusCommand(opts) {
|
|
|
19
19
|
|
|
20
20
|
// Detail mode: single intent
|
|
21
21
|
if (result.intent) {
|
|
22
|
-
printIntentDetail(result.intent, result.event);
|
|
22
|
+
printIntentDetail(result.intent, result.event, result.next_action);
|
|
23
23
|
process.exit(0);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -46,14 +46,17 @@ function printSummary(summary) {
|
|
|
46
46
|
const pri = i.priority ? i.priority.padEnd(3) : '---';
|
|
47
47
|
const tpl = (i.template || '---').padEnd(12);
|
|
48
48
|
const st = statusColor(i.status);
|
|
49
|
-
|
|
49
|
+
const actionHint = i.next_action?.action_required && i.next_action?.label !== 'none'
|
|
50
|
+
? ` ${chalk.dim(`→ ${i.next_action.label}`)}`
|
|
51
|
+
: '';
|
|
52
|
+
console.log(` ${chalk.dim(i.intent_id)} ${pri} ${tpl} ${st} ${chalk.dim(i.updated_at)}${actionHint}`);
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
console.log('');
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
function printIntentDetail(intent, event) {
|
|
59
|
+
function printIntentDetail(intent, event, nextAction) {
|
|
57
60
|
console.log('');
|
|
58
61
|
console.log(chalk.bold(` Intent: ${intent.intent_id}`));
|
|
59
62
|
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
@@ -93,6 +96,21 @@ function printIntentDetail(intent, event) {
|
|
|
93
96
|
console.log(` ${chalk.dim('Signal:')} ${JSON.stringify(event.signal)}`);
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
if (nextAction) {
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(chalk.dim(' Next Action:'));
|
|
102
|
+
console.log(` ${nextAction.summary}`);
|
|
103
|
+
if (nextAction.command) {
|
|
104
|
+
console.log(` ${chalk.dim('Command:')} ${nextAction.command}`);
|
|
105
|
+
}
|
|
106
|
+
for (const alternative of nextAction.alternatives || []) {
|
|
107
|
+
console.log(` ${chalk.dim('Alternative:')} ${alternative}`);
|
|
108
|
+
}
|
|
109
|
+
if (nextAction.recovery) {
|
|
110
|
+
console.log(` ${chalk.dim('Recovery:')} ${nextAction.recovery}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
console.log('');
|
|
97
115
|
}
|
|
98
116
|
|
package/src/commands/restart.js
CHANGED
|
@@ -167,7 +167,7 @@ export async function restartCommand(opts) {
|
|
|
167
167
|
// Load state
|
|
168
168
|
const statePath = join(root, STATE_PATH);
|
|
169
169
|
if (!existsSync(statePath)) {
|
|
170
|
-
console.log(chalk.red('No governed run found. Use `agentxchain
|
|
170
|
+
console.log(chalk.red('No governed run found. Use `agentxchain run` to start a governed run.'));
|
|
171
171
|
process.exit(1);
|
|
172
172
|
}
|
|
173
173
|
|
package/src/commands/resume.js
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from '../lib/turn-paths.js';
|
|
39
39
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
40
40
|
import { runHooks } from '../lib/hook-runner.js';
|
|
41
|
+
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
41
42
|
|
|
42
43
|
export async function resumeCommand(opts) {
|
|
43
44
|
const context = loadProjectContext();
|
|
@@ -128,6 +129,7 @@ export async function resumeCommand(opts) {
|
|
|
128
129
|
|
|
129
130
|
const turnStatus = retainedTurn.status;
|
|
130
131
|
if (turnStatus === 'failed' || turnStatus === 'retrying') {
|
|
132
|
+
printResumeRunContext({ root, state, config });
|
|
131
133
|
console.log(chalk.yellow(`Re-dispatching failed turn: ${retainedTurn.turn_id}`));
|
|
132
134
|
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
133
135
|
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
@@ -187,6 +189,7 @@ export async function resumeCommand(opts) {
|
|
|
187
189
|
process.exit(1);
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
printResumeRunContext({ root, state, config });
|
|
190
193
|
console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
|
|
191
194
|
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
192
195
|
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
@@ -251,6 +254,9 @@ export async function resumeCommand(opts) {
|
|
|
251
254
|
console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
|
|
252
255
|
}
|
|
253
256
|
|
|
257
|
+
// Print run-context header before dispatch
|
|
258
|
+
printResumeRunContext({ root, state, config });
|
|
259
|
+
|
|
254
260
|
// Resolve target role
|
|
255
261
|
const roleId = resolveTargetRole(opts, state, config);
|
|
256
262
|
if (!roleId) {
|
|
@@ -302,6 +308,53 @@ export async function resumeCommand(opts) {
|
|
|
302
308
|
|
|
303
309
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
304
310
|
|
|
311
|
+
function printResumeRunContext({ root, state, config }) {
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log(chalk.cyan.bold('agentxchain resume'));
|
|
314
|
+
console.log(` ${chalk.dim('Run:')} ${state?.run_id || '(uninitialized)'}`);
|
|
315
|
+
console.log(` ${chalk.dim('Phase:')} ${state?.phase || '(unknown)'}`);
|
|
316
|
+
|
|
317
|
+
const provenanceSummary = summarizeRunProvenance(state?.provenance);
|
|
318
|
+
if (provenanceSummary) {
|
|
319
|
+
console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (state?.inherited_context?.parent_run_id) {
|
|
323
|
+
console.log(
|
|
324
|
+
` ${chalk.dim('Inherits:')} ${chalk.magenta(
|
|
325
|
+
`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`
|
|
326
|
+
)}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const activeGate = config?.routing?.[state?.phase]?.exit_gate || null;
|
|
331
|
+
if (activeGate) {
|
|
332
|
+
const gateStatus = state?.phase_gate_status?.[activeGate] || 'pending';
|
|
333
|
+
console.log(` ${chalk.dim('Gate:')} ${activeGate} (${gateStatus})`);
|
|
334
|
+
|
|
335
|
+
if (gateStatus !== 'passed') {
|
|
336
|
+
const gateDef = config?.gates?.[activeGate];
|
|
337
|
+
if (Array.isArray(gateDef?.requires_files) && gateDef.requires_files.length > 0) {
|
|
338
|
+
const fileChecks = gateDef.requires_files.map((filePath) => {
|
|
339
|
+
const exists = existsSync(join(root, filePath));
|
|
340
|
+
const shortPath = filePath.replace(/^\.planning\//, '');
|
|
341
|
+
return exists ? chalk.green(shortPath) : chalk.red(shortPath);
|
|
342
|
+
});
|
|
343
|
+
console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const requirements = [];
|
|
347
|
+
if (gateDef?.requires_human_approval) requirements.push('human approval');
|
|
348
|
+
if (gateDef?.requires_verification_pass) requirements.push('verification pass');
|
|
349
|
+
if (requirements.length > 0) {
|
|
350
|
+
console.log(` ${chalk.dim('Needs:')} ${requirements.join(', ')}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log('');
|
|
356
|
+
}
|
|
357
|
+
|
|
305
358
|
function resolveTargetRole(opts, state, config) {
|
|
306
359
|
const phase = state.phase;
|
|
307
360
|
const routing = config.routing?.[phase];
|
package/src/commands/run.js
CHANGED
|
@@ -32,6 +32,7 @@ import { dispatchRemoteAgent, describeRemoteAgentTarget } from '../lib/adapters/
|
|
|
32
32
|
import { runHooks } from '../lib/hook-runner.js';
|
|
33
33
|
import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
|
|
34
34
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
35
|
+
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
35
36
|
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
36
37
|
import { buildInheritedContext } from '../lib/run-context-inheritance.js';
|
|
37
38
|
import {
|
|
@@ -179,6 +180,24 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
179
180
|
// ── Run header ──────────────────────────────────────────────────────────
|
|
180
181
|
log(chalk.cyan.bold('agentxchain run'));
|
|
181
182
|
log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
|
|
183
|
+
if (provenance) {
|
|
184
|
+
const provenanceSummary = summarizeRunProvenance(provenance);
|
|
185
|
+
if (provenanceSummary) {
|
|
186
|
+
log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (inheritedContext) {
|
|
190
|
+
const ic = inheritedContext;
|
|
191
|
+
const phasesCount = ic.parent_phases_completed?.length || 0;
|
|
192
|
+
const decisionsCount = ic.recent_decisions?.length || 0;
|
|
193
|
+
const turnsCount = ic.recent_accepted_turns?.length || 0;
|
|
194
|
+
const parts = [];
|
|
195
|
+
if (phasesCount) parts.push(`${phasesCount} phase${phasesCount !== 1 ? 's' : ''}`);
|
|
196
|
+
if (decisionsCount) parts.push(`${decisionsCount} decision${decisionsCount !== 1 ? 's' : ''}`);
|
|
197
|
+
if (turnsCount) parts.push(`${turnsCount} turn${turnsCount !== 1 ? 's' : ''}`);
|
|
198
|
+
const detail = parts.length ? ` — ${parts.join(', ')}` : '';
|
|
199
|
+
log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${ic.parent_run_id} (${ic.parent_status || 'unknown'})${detail}`)}`);
|
|
200
|
+
}
|
|
182
201
|
log('');
|
|
183
202
|
|
|
184
203
|
// ── Track first-call for --role override ────────────────────────────────
|
package/src/commands/status.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
2
4
|
import { loadConfig, loadLock, loadProjectContext, loadProjectState, loadState } from '../lib/config.js';
|
|
3
5
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
4
6
|
import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
|
|
@@ -267,9 +269,29 @@ function renderGovernedStatus(context, opts) {
|
|
|
267
269
|
if (state?.phase_gate_status) {
|
|
268
270
|
console.log('');
|
|
269
271
|
console.log(` ${chalk.dim('Gates:')}`);
|
|
272
|
+
const activePhase = state.phase;
|
|
273
|
+
const activeRouting = config.routing?.[activePhase];
|
|
274
|
+
const activeExitGate = activeRouting?.exit_gate || null;
|
|
270
275
|
for (const [gate, status] of Object.entries(state.phase_gate_status)) {
|
|
271
276
|
const icon = status === 'passed' ? chalk.green('✓') : chalk.dim('○');
|
|
272
277
|
console.log(` ${icon} ${gate}: ${status}`);
|
|
278
|
+
if (status !== 'passed' && gate === activeExitGate && config.gates?.[gate]) {
|
|
279
|
+
const gateDef = config.gates[gate];
|
|
280
|
+
if (Array.isArray(gateDef.requires_files) && gateDef.requires_files.length > 0) {
|
|
281
|
+
const fileChecks = gateDef.requires_files.map(f => {
|
|
282
|
+
const exists = existsSync(join(root, f));
|
|
283
|
+
const short = f.replace(/^\.planning\//, '');
|
|
284
|
+
return exists ? chalk.green(short) : chalk.red(short);
|
|
285
|
+
});
|
|
286
|
+
console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
|
|
287
|
+
}
|
|
288
|
+
const reqs = [];
|
|
289
|
+
if (gateDef.requires_human_approval) reqs.push('human approval');
|
|
290
|
+
if (gateDef.requires_verification_pass) reqs.push('verification pass');
|
|
291
|
+
if (reqs.length > 0) {
|
|
292
|
+
console.log(` ${chalk.dim('Needs:')} ${reqs.join(', ')}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
|
|
@@ -425,7 +447,7 @@ function renderWorkflowKitArtifactsSection(wkData) {
|
|
|
425
447
|
|
|
426
448
|
function renderLastGateFailure(failure, config) {
|
|
427
449
|
const entryRole = config?.routing?.[failure.phase]?.entry_role || null;
|
|
428
|
-
const suggestedCommand = entryRole ? `agentxchain
|
|
450
|
+
const suggestedCommand = entryRole ? `agentxchain step --role ${entryRole}` : 'agentxchain step --role <role>';
|
|
429
451
|
const requestLabel = failure.gate_type === 'run_completion'
|
|
430
452
|
? 'Run completion'
|
|
431
453
|
: `${failure.from_phase || failure.phase} -> ${failure.to_phase || 'unknown'}`;
|
package/src/commands/step.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from '../lib/adapters/local-cli-adapter.js';
|
|
52
52
|
import { describeMcpRuntimeTarget, dispatchMcp, resolveMcpTransport } from '../lib/adapters/mcp-adapter.js';
|
|
53
53
|
import { dispatchRemoteAgent, describeRemoteAgentTarget } from '../lib/adapters/remote-agent-adapter.js';
|
|
54
|
+
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
54
55
|
import {
|
|
55
56
|
getDispatchAssignmentPath,
|
|
56
57
|
getDispatchContextPath,
|
|
@@ -326,6 +327,8 @@ export async function stepCommand(opts) {
|
|
|
326
327
|
const runtimeType = runtime?.type || role?.runtime_class || 'manual';
|
|
327
328
|
const hooksConfig = config.hooks || {};
|
|
328
329
|
|
|
330
|
+
printStepRunContext({ root, state, config });
|
|
331
|
+
|
|
329
332
|
if (bundleWritten && hooksConfig.after_dispatch?.length > 0) {
|
|
330
333
|
const afterDispatchHooks = runHooks(root, hooksConfig, 'after_dispatch', {
|
|
331
334
|
turn_id: turn.turn_id,
|
|
@@ -945,6 +948,53 @@ function printRecoverySummary(state, heading) {
|
|
|
945
948
|
}
|
|
946
949
|
}
|
|
947
950
|
|
|
951
|
+
function printStepRunContext({ root, state, config }) {
|
|
952
|
+
console.log('');
|
|
953
|
+
console.log(chalk.cyan.bold('agentxchain step'));
|
|
954
|
+
console.log(` ${chalk.dim('Run:')} ${state?.run_id || '(uninitialized)'}`);
|
|
955
|
+
console.log(` ${chalk.dim('Phase:')} ${state?.phase || '(unknown)'}`);
|
|
956
|
+
|
|
957
|
+
const provenanceSummary = summarizeRunProvenance(state?.provenance);
|
|
958
|
+
if (provenanceSummary) {
|
|
959
|
+
console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (state?.inherited_context?.parent_run_id) {
|
|
963
|
+
console.log(
|
|
964
|
+
` ${chalk.dim('Inherits:')} ${chalk.magenta(
|
|
965
|
+
`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`
|
|
966
|
+
)}`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const activeGate = config?.routing?.[state?.phase]?.exit_gate || null;
|
|
971
|
+
if (activeGate) {
|
|
972
|
+
const gateStatus = state?.phase_gate_status?.[activeGate] || 'pending';
|
|
973
|
+
console.log(` ${chalk.dim('Gate:')} ${activeGate} (${gateStatus})`);
|
|
974
|
+
|
|
975
|
+
if (gateStatus !== 'passed') {
|
|
976
|
+
const gateDef = config?.gates?.[activeGate];
|
|
977
|
+
if (Array.isArray(gateDef?.requires_files) && gateDef.requires_files.length > 0) {
|
|
978
|
+
const fileChecks = gateDef.requires_files.map((filePath) => {
|
|
979
|
+
const exists = existsSync(join(root, filePath));
|
|
980
|
+
const shortPath = filePath.replace(/^\.planning\//, '');
|
|
981
|
+
return exists ? chalk.green(shortPath) : chalk.red(shortPath);
|
|
982
|
+
});
|
|
983
|
+
console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const requirements = [];
|
|
987
|
+
if (gateDef?.requires_human_approval) requirements.push('human approval');
|
|
988
|
+
if (gateDef?.requires_verification_pass) requirements.push('verification pass');
|
|
989
|
+
if (requirements.length > 0) {
|
|
990
|
+
console.log(` ${chalk.dim('Needs:')} ${requirements.join(', ')}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
console.log('');
|
|
996
|
+
}
|
|
997
|
+
|
|
948
998
|
function printDispatchBundleWarnings(bundleResult) {
|
|
949
999
|
for (const warning of bundleResult.warnings || []) {
|
|
950
1000
|
console.log(chalk.yellow(`Dispatch bundle warning: ${warning}`));
|
|
@@ -1835,7 +1835,7 @@ export function reactivateGovernedRun(root, state, details = {}) {
|
|
|
1835
1835
|
export function initializeGovernedRun(root, config, options = {}) {
|
|
1836
1836
|
let state = readState(root);
|
|
1837
1837
|
if (!state) {
|
|
1838
|
-
|
|
1838
|
+
state = buildFreshIdleStateForNewRun(null, config);
|
|
1839
1839
|
}
|
|
1840
1840
|
const allowTerminalRestart = options.allow_terminal_restart === true
|
|
1841
1841
|
&& (state.status === 'completed' || state.status === 'blocked');
|
package/src/lib/intake.js
CHANGED
|
@@ -120,6 +120,122 @@ function readJsonDir(dirPath) {
|
|
|
120
120
|
.filter(Boolean);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function buildIntakeNextAction(intent) {
|
|
124
|
+
const intentId = intent?.intent_id || '<intent_id>';
|
|
125
|
+
const status = intent?.status || 'unknown';
|
|
126
|
+
const resolveCommand = `agentxchain intake resolve --intent ${intentId}`;
|
|
127
|
+
|
|
128
|
+
switch (status) {
|
|
129
|
+
case 'detected':
|
|
130
|
+
return {
|
|
131
|
+
label: 'triage',
|
|
132
|
+
summary: 'Triage this detected intent so it can enter governed delivery.',
|
|
133
|
+
command: `agentxchain intake triage --intent ${intentId} --priority <p0-p3> --template <template_id> --charter "<charter>" --acceptance "<criterion>"`,
|
|
134
|
+
alternatives: [
|
|
135
|
+
`agentxchain intake triage --intent ${intentId} --suppress --reason "<reason>"`,
|
|
136
|
+
],
|
|
137
|
+
recovery: null,
|
|
138
|
+
action_required: true,
|
|
139
|
+
};
|
|
140
|
+
case 'triaged':
|
|
141
|
+
return {
|
|
142
|
+
label: 'approve',
|
|
143
|
+
summary: 'Approve this triaged intent for planning or reject it explicitly.',
|
|
144
|
+
command: `agentxchain intake approve --intent ${intentId}`,
|
|
145
|
+
alternatives: [
|
|
146
|
+
`agentxchain intake triage --intent ${intentId} --reject --reason "<reason>"`,
|
|
147
|
+
],
|
|
148
|
+
recovery: null,
|
|
149
|
+
action_required: true,
|
|
150
|
+
};
|
|
151
|
+
case 'approved':
|
|
152
|
+
return {
|
|
153
|
+
label: 'plan',
|
|
154
|
+
summary: 'Generate planning artifacts for this approved intent.',
|
|
155
|
+
command: `agentxchain intake plan --intent ${intentId}`,
|
|
156
|
+
alternatives: [],
|
|
157
|
+
recovery: null,
|
|
158
|
+
action_required: true,
|
|
159
|
+
};
|
|
160
|
+
case 'planned':
|
|
161
|
+
return {
|
|
162
|
+
label: 'start',
|
|
163
|
+
summary: 'Start repo-local execution or hand the intent off to a coordinator workstream.',
|
|
164
|
+
command: `agentxchain intake start --intent ${intentId}`,
|
|
165
|
+
alternatives: [
|
|
166
|
+
`agentxchain intake handoff --intent ${intentId} --coordinator-root <path> --workstream <id>`,
|
|
167
|
+
],
|
|
168
|
+
recovery: null,
|
|
169
|
+
action_required: true,
|
|
170
|
+
};
|
|
171
|
+
case 'executing':
|
|
172
|
+
return {
|
|
173
|
+
label: 'resolve',
|
|
174
|
+
summary: intent?.target_workstream
|
|
175
|
+
? 'Re-check the coordinator workstream outcome for this intent.'
|
|
176
|
+
: 'Re-check the governed run outcome for this intent.',
|
|
177
|
+
command: resolveCommand,
|
|
178
|
+
alternatives: [],
|
|
179
|
+
recovery: null,
|
|
180
|
+
action_required: true,
|
|
181
|
+
};
|
|
182
|
+
case 'blocked':
|
|
183
|
+
return {
|
|
184
|
+
label: 'recover',
|
|
185
|
+
summary: 'Resolve the linked run blockage, then re-check intake resolution.',
|
|
186
|
+
command: resolveCommand,
|
|
187
|
+
alternatives: [],
|
|
188
|
+
recovery: intent?.run_blocked_recovery || null,
|
|
189
|
+
action_required: true,
|
|
190
|
+
};
|
|
191
|
+
case 'completed':
|
|
192
|
+
return {
|
|
193
|
+
label: 'none',
|
|
194
|
+
summary: 'No action required. This intent completed successfully.',
|
|
195
|
+
command: null,
|
|
196
|
+
alternatives: [],
|
|
197
|
+
recovery: null,
|
|
198
|
+
action_required: false,
|
|
199
|
+
};
|
|
200
|
+
case 'suppressed':
|
|
201
|
+
return {
|
|
202
|
+
label: 'none',
|
|
203
|
+
summary: 'No action required. This intent was suppressed.',
|
|
204
|
+
command: null,
|
|
205
|
+
alternatives: [],
|
|
206
|
+
recovery: null,
|
|
207
|
+
action_required: false,
|
|
208
|
+
};
|
|
209
|
+
case 'rejected':
|
|
210
|
+
return {
|
|
211
|
+
label: 'none',
|
|
212
|
+
summary: 'No action required. This intent was rejected.',
|
|
213
|
+
command: null,
|
|
214
|
+
alternatives: [],
|
|
215
|
+
recovery: null,
|
|
216
|
+
action_required: false,
|
|
217
|
+
};
|
|
218
|
+
case 'failed':
|
|
219
|
+
return {
|
|
220
|
+
label: 'inspect',
|
|
221
|
+
summary: 'Manual inspection required. This intent is in a reserved failed state.',
|
|
222
|
+
command: null,
|
|
223
|
+
alternatives: [],
|
|
224
|
+
recovery: 'Inspect the linked intent and run artifacts manually before continuing.',
|
|
225
|
+
action_required: true,
|
|
226
|
+
};
|
|
227
|
+
default:
|
|
228
|
+
return {
|
|
229
|
+
label: 'inspect',
|
|
230
|
+
summary: 'Manual inspection required. The intent state is not recognized by the current intake surface.',
|
|
231
|
+
command: null,
|
|
232
|
+
alternatives: [],
|
|
233
|
+
recovery: null,
|
|
234
|
+
action_required: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
123
239
|
// ---------------------------------------------------------------------------
|
|
124
240
|
// Validation
|
|
125
241
|
// ---------------------------------------------------------------------------
|
|
@@ -329,7 +445,13 @@ export function intakeStatus(root, intentId) {
|
|
|
329
445
|
const intent = JSON.parse(readFileSync(intentPath, 'utf8'));
|
|
330
446
|
const eventPath = join(dirs.events, `${intent.event_id}.json`);
|
|
331
447
|
const event = existsSync(eventPath) ? JSON.parse(readFileSync(eventPath, 'utf8')) : null;
|
|
332
|
-
return {
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
intent,
|
|
451
|
+
event,
|
|
452
|
+
next_action: buildIntakeNextAction(intent),
|
|
453
|
+
exitCode: 0,
|
|
454
|
+
};
|
|
333
455
|
}
|
|
334
456
|
|
|
335
457
|
const events = readJsonDir(dirs.events);
|
|
@@ -354,6 +476,7 @@ export function intakeStatus(root, intentId) {
|
|
|
354
476
|
template: i.template,
|
|
355
477
|
status: i.status,
|
|
356
478
|
updated_at: i.updated_at,
|
|
479
|
+
next_action: buildIntakeNextAction(i),
|
|
357
480
|
})),
|
|
358
481
|
};
|
|
359
482
|
|
|
@@ -330,9 +330,10 @@ export function detectConfigVersion(raw) {
|
|
|
330
330
|
*/
|
|
331
331
|
export function validateV4Config(data, projectRoot) {
|
|
332
332
|
const errors = [];
|
|
333
|
+
const warnings = [];
|
|
333
334
|
|
|
334
335
|
if (!data || typeof data !== 'object') {
|
|
335
|
-
return { ok: false, errors: ['Config must be a JSON object'] };
|
|
336
|
+
return { ok: false, errors: ['Config must be a JSON object'], warnings };
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
// Top-level required sections
|
|
@@ -555,7 +556,80 @@ export function validateV4Config(data, projectRoot) {
|
|
|
555
556
|
errors.push(...timeoutValidation.errors);
|
|
556
557
|
}
|
|
557
558
|
|
|
558
|
-
|
|
559
|
+
warnings.push(...collectRemoteReviewOnlyGateWarnings(data));
|
|
560
|
+
|
|
561
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function collectRemoteReviewOnlyGateWarnings(data) {
|
|
565
|
+
const warnings = [];
|
|
566
|
+
const routing = data?.routing;
|
|
567
|
+
const gates = data?.gates;
|
|
568
|
+
const roles = data?.roles;
|
|
569
|
+
const runtimes = data?.runtimes;
|
|
570
|
+
|
|
571
|
+
if (!routing || !gates || !roles || !runtimes) {
|
|
572
|
+
return warnings;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
for (const [phase, route] of Object.entries(routing)) {
|
|
576
|
+
const exitGateId = route?.exit_gate;
|
|
577
|
+
if (!exitGateId || !gates[exitGateId]) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const requiredFiles = Array.isArray(gates[exitGateId]?.requires_files)
|
|
582
|
+
? gates[exitGateId].requires_files.filter(filePath => typeof filePath === 'string' && filePath.trim())
|
|
583
|
+
: [];
|
|
584
|
+
if (requiredFiles.length === 0) {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const candidateRoleIds = [
|
|
589
|
+
route?.entry_role,
|
|
590
|
+
...(Array.isArray(route?.allowed_next_roles) ? route.allowed_next_roles : []),
|
|
591
|
+
].filter((roleId) => roleId && roleId !== 'human');
|
|
592
|
+
|
|
593
|
+
if (candidateRoleIds.length === 0) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const candidateRoles = [...new Set(candidateRoleIds)]
|
|
598
|
+
.map((roleId) => {
|
|
599
|
+
const role = roles[roleId];
|
|
600
|
+
const runtime = role?.runtime ? runtimes[role.runtime] : null;
|
|
601
|
+
if (!role || !runtime) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
return { roleId, role, runtime };
|
|
605
|
+
})
|
|
606
|
+
.filter(Boolean);
|
|
607
|
+
|
|
608
|
+
if (candidateRoles.length === 0) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const hasFileProducingRole = candidateRoles.some(({ role }) =>
|
|
613
|
+
role.write_authority === 'authoritative' || role.write_authority === 'proposed');
|
|
614
|
+
if (hasFileProducingRole) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const allRemoteReviewOnly = candidateRoles.every(({ role, runtime }) =>
|
|
619
|
+
role.write_authority === 'review_only' && (runtime.type === 'api_proxy' || runtime.type === 'remote_agent'));
|
|
620
|
+
if (!allRemoteReviewOnly) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const roleSummary = candidateRoles
|
|
625
|
+
.map(({ roleId, runtime }) => `${roleId}:${runtime.type}`)
|
|
626
|
+
.join(', ');
|
|
627
|
+
warnings.push(
|
|
628
|
+
`Routing "${phase}" exits through gate "${exitGateId}" with requires_files (${requiredFiles.join(', ')}) but all participating roles are review_only remote runtimes (${roleSummary}). Those files cannot be produced through governed turns; add a proposed/authoritative writer, remove the gate files, or expect operator-managed out-of-band artifacts.`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return warnings;
|
|
559
633
|
}
|
|
560
634
|
|
|
561
635
|
export function validateBudgetConfig(budget) {
|
|
@@ -1133,6 +1207,7 @@ export function loadNormalizedConfig(raw, projectRoot) {
|
|
|
1133
1207
|
ok: false,
|
|
1134
1208
|
normalized: null,
|
|
1135
1209
|
errors: ['Unrecognized config format. Expected version: 3 or schema_version: "1.0" / 4'],
|
|
1210
|
+
warnings: [],
|
|
1136
1211
|
version: null,
|
|
1137
1212
|
};
|
|
1138
1213
|
}
|
|
@@ -1152,17 +1227,17 @@ export function loadNormalizedConfig(raw, projectRoot) {
|
|
|
1152
1227
|
}
|
|
1153
1228
|
}
|
|
1154
1229
|
if (errors.length > 0) {
|
|
1155
|
-
return { ok: false, normalized: null, errors, version: 3 };
|
|
1230
|
+
return { ok: false, normalized: null, errors, warnings: [], version: 3 };
|
|
1156
1231
|
}
|
|
1157
|
-
return { ok: true, normalized: normalizeV3(raw), errors: [], version: 3 };
|
|
1232
|
+
return { ok: true, normalized: normalizeV3(raw), errors: [], warnings: [], version: 3 };
|
|
1158
1233
|
}
|
|
1159
1234
|
|
|
1160
1235
|
if (version === 4) {
|
|
1161
1236
|
const validation = validateV4Config(raw, projectRoot || null);
|
|
1162
1237
|
if (!validation.ok) {
|
|
1163
|
-
return { ok: false, normalized: null, errors: validation.errors, version: 4 };
|
|
1238
|
+
return { ok: false, normalized: null, errors: validation.errors, warnings: validation.warnings || [], version: 4 };
|
|
1164
1239
|
}
|
|
1165
|
-
return { ok: true, normalized: normalizeV4(raw), errors: [], version: 4 };
|
|
1240
|
+
return { ok: true, normalized: normalizeV4(raw), errors: [], warnings: validation.warnings || [], version: 4 };
|
|
1166
1241
|
}
|
|
1167
1242
|
}
|
|
1168
1243
|
|
package/src/lib/validation.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
validateAcceptanceHintCompletion,
|
|
10
10
|
validateGovernedWorkflowKit,
|
|
11
11
|
} from './governed-templates.js';
|
|
12
|
+
import { collectRemoteReviewOnlyGateWarnings } from './normalized-config.js';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_REQUIRED_FILES = [
|
|
14
15
|
'.planning/PROJECT.md',
|
|
@@ -115,6 +116,9 @@ export function validateGovernedProject(root, rawConfig, config, opts = {}) {
|
|
|
115
116
|
errors.push(...workflowKit.errors);
|
|
116
117
|
warnings.push(...workflowKit.warnings);
|
|
117
118
|
|
|
119
|
+
// Config-shape warnings (dead-end gates, etc.) — mirrors doctor/config --set surfaces
|
|
120
|
+
warnings.push(...collectRemoteReviewOnlyGateWarnings(rawConfig));
|
|
121
|
+
|
|
118
122
|
const mustExist = [
|
|
119
123
|
config.files?.state || '.agentxchain/state.json',
|
|
120
124
|
config.files?.history || '.agentxchain/history.jsonl',
|