agentxchain 2.83.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.83.0",
3
+ "version": "2.84.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ });
@@ -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 links the release page
152
- if ! grep -q "'releases/${RELEASE_DOC_ID}'" "${REPO_ROOT}/website-v2/sidebars.ts" 2>/dev/null; then
153
- SURFACE_ERRORS+=("sidebars.ts does not link 'releases/${RELEASE_DOC_ID}'")
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 must list the current release notes route
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 9 governed version surfaces reference ${TARGET_VERSION}"
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.
@@ -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
 
@@ -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 });
@@ -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
- return { ok: errors.length === 0, errors };
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
 
@@ -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',