agentxchain 2.50.0 → 2.52.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.
@@ -249,7 +249,8 @@ program
249
249
 
250
250
  program
251
251
  .command('doctor')
252
- .description('Check local environment and first-run readiness')
252
+ .description('Check governed project readiness (v4) or local environment (v3)')
253
+ .option('-j, --json', 'Output as JSON')
253
254
  .action(doctorCommand);
254
255
 
255
256
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.50.0",
3
+ "version": "2.52.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # Release identity creation — replaces raw `npm version <semver>`.
3
3
  # Creates version bump commit + annotated tag with fail-closed verification.
4
- # Usage: bash scripts/release-bump.sh --target-version <semver>
4
+ # Usage: bash scripts/release-bump.sh --target-version <semver> [--skip-preflight]
5
5
  set -euo pipefail
6
6
 
7
7
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -10,9 +10,10 @@ REPO_ROOT="$(cd "${CLI_DIR}/.." && pwd)"
10
10
  cd "$CLI_DIR"
11
11
 
12
12
  TARGET_VERSION=""
13
+ SKIP_PREFLIGHT=0
13
14
 
14
15
  usage() {
15
- echo "Usage: bash scripts/release-bump.sh --target-version <semver>" >&2
16
+ echo "Usage: bash scripts/release-bump.sh --target-version <semver> [--skip-preflight]" >&2
16
17
  }
17
18
 
18
19
  while [[ $# -gt 0 ]]; do
@@ -31,6 +32,10 @@ while [[ $# -gt 0 ]]; do
31
32
  TARGET_VERSION="$2"
32
33
  shift 2
33
34
  ;;
35
+ --skip-preflight)
36
+ SKIP_PREFLIGHT=1
37
+ shift
38
+ ;;
34
39
  *)
35
40
  usage
36
41
  exit 1
@@ -250,8 +255,67 @@ if [[ "$COMMIT_MSG" != "$TARGET_VERSION" ]]; then
250
255
  fi
251
256
  echo " OK: commit ${RELEASE_SHA:0:7} with message '${TARGET_VERSION}'"
252
257
 
258
+ # 8.5. Inline preflight gate — tests, pack, and docs build must pass before tag
259
+ if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
260
+ echo ""
261
+ echo "[8.5/10] Inline preflight gate SKIPPED (--skip-preflight)"
262
+ else
263
+ echo ""
264
+ echo "[8.5/10] Running inline preflight gate..."
265
+ echo " Running test suite..."
266
+
267
+ # Install MCP example deps if needed (same as release-preflight.sh)
268
+ for example_dir in "${CLI_DIR}/../examples/mcp-echo-agent" "${CLI_DIR}/../examples/mcp-http-echo-agent"; do
269
+ if [[ -f "${example_dir}/package.json" && ! -d "${example_dir}/node_modules" ]]; then
270
+ echo " Installing deps for $(basename "$example_dir")..."
271
+ (cd "$example_dir" && env -u NODE_AUTH_TOKEN -u NPM_CONFIG_USERCONFIG npm install --ignore-scripts --userconfig /dev/null 2>&1) || true
272
+ fi
273
+ done
274
+
275
+ PREFLIGHT_FAILED=0
276
+
277
+ # 8.5a. Full test suite with release env vars
278
+ if env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test >/dev/null 2>&1; then
279
+ echo " OK: test suite passed"
280
+ else
281
+ echo " FAIL: test suite failed" >&2
282
+ echo " Re-running with output for diagnostics..." >&2
283
+ env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test 2>&1 | tail -30 >&2
284
+ PREFLIGHT_FAILED=1
285
+ fi
286
+
287
+ # 8.5b. npm pack dry-run
288
+ if npm pack --dry-run >/dev/null 2>&1; then
289
+ echo " OK: npm pack --dry-run passed"
290
+ else
291
+ echo " FAIL: npm pack --dry-run failed" >&2
292
+ PREFLIGHT_FAILED=1
293
+ fi
294
+
295
+ # 8.5c. Docs build
296
+ if (cd "${REPO_ROOT}/website-v2" && npm run build >/dev/null 2>&1); then
297
+ echo " OK: docs build passed"
298
+ else
299
+ echo " FAIL: docs build failed" >&2
300
+ PREFLIGHT_FAILED=1
301
+ fi
302
+
303
+ if [[ "$PREFLIGHT_FAILED" -eq 1 ]]; then
304
+ echo "" >&2
305
+ echo "PREFLIGHT FAILED — release commit created but NOT tagged." >&2
306
+ echo " Commit: ${RELEASE_SHA:0:7}" >&2
307
+ echo " Fix the failures, amend the commit, and re-run:" >&2
308
+ echo " bash scripts/release-bump.sh --target-version ${TARGET_VERSION}" >&2
309
+ echo " Or skip preflight if already verified:" >&2
310
+ echo " bash scripts/release-bump.sh --target-version ${TARGET_VERSION} --skip-preflight" >&2
311
+ exit 1
312
+ fi
313
+
314
+ echo " Inline preflight gate passed — proceeding to tag"
315
+ fi
316
+
253
317
  # 9. Create annotated tag
254
- echo "[9/9] Creating annotated tag..."
318
+ echo "[9/10] Creating annotated tag..."
255
319
  git tag -a "v${TARGET_VERSION}" -m "v${TARGET_VERSION}"
256
320
  TAG_SHA=$(git rev-parse "v${TARGET_VERSION}")
257
321
  if [[ -z "$TAG_SHA" ]]; then
@@ -276,6 +340,10 @@ echo "Release identity created successfully."
276
340
  echo " Version: ${TARGET_VERSION}"
277
341
  echo " Commit: ${RELEASE_SHA:0:7}"
278
342
  echo " Tag: v${TARGET_VERSION}"
343
+ if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
344
+ echo ""
345
+ echo "WARNING: Inline preflight was skipped. Verify before pushing:"
346
+ echo " npm run preflight:release:strict -- --target-version ${TARGET_VERSION}"
347
+ fi
279
348
  echo ""
280
- echo "Next: npm run preflight:release:strict -- --target-version ${TARGET_VERSION}"
281
- echo "Then: git push origin main --follow-tags"
349
+ echo "Next: git push origin main --follow-tags"
@@ -1,19 +1,258 @@
1
1
  import { existsSync, readFileSync } from 'fs';
2
- import { execSync } from 'child_process';
2
+ import { execFileSync, execSync } from 'child_process';
3
3
  import { join } from 'path';
4
4
  import chalk from 'chalk';
5
- import { loadConfig, loadLock } from '../lib/config.js';
5
+ import { loadConfig, loadLock, findProjectRoot } from '../lib/config.js';
6
6
  import { validateProject } from '../lib/validation.js';
7
7
  import { getWatchPid } from './watch.js';
8
+ import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
9
+ import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
8
10
 
9
- export async function doctorCommand() {
10
- const result = loadConfig();
11
+ export async function doctorCommand(opts = {}) {
12
+ const root = findProjectRoot(process.cwd());
13
+ if (!root) {
14
+ if (opts.json) {
15
+ console.log(JSON.stringify({ error: 'No agentxchain.json found' }));
16
+ } else {
17
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
18
+ }
19
+ process.exit(1);
20
+ }
21
+
22
+ // Detect config version to dispatch
23
+ let rawConfig;
24
+ try {
25
+ rawConfig = JSON.parse(readFileSync(join(root, 'agentxchain.json'), 'utf8'));
26
+ } catch (err) {
27
+ if (opts.json) {
28
+ console.log(JSON.stringify({ error: `agentxchain.json is invalid JSON: ${err.message}` }));
29
+ } else {
30
+ console.log(chalk.red(`agentxchain.json is invalid JSON: ${err.message}`));
31
+ }
32
+ process.exit(1);
33
+ }
34
+
35
+ const version = detectConfigVersion(rawConfig);
36
+
37
+ if (version === 4) {
38
+ return governedDoctor(root, rawConfig, opts);
39
+ }
40
+
41
+ // Legacy v3 path — existing behavior
42
+ return legacyDoctor(root, opts);
43
+ }
44
+
45
+ // ── Governed (v4) Doctor ────────────────────────────────────────────────────
46
+
47
+ function governedDoctor(root, rawConfig, opts) {
48
+ const checks = [];
49
+
50
+ // 1. Config validation
51
+ const configResult = loadNormalizedConfig(rawConfig, root);
52
+ if (configResult.ok) {
53
+ checks.push({ id: 'config_valid', name: 'Config validation', level: 'pass', detail: 'Config loads and validates' });
54
+ } else {
55
+ const errorSummary = configResult.errors.slice(0, 3).join('; ');
56
+ checks.push({ id: 'config_valid', name: 'Config validation', level: 'fail', detail: errorSummary });
57
+ }
58
+
59
+ const normalized = configResult.normalized;
60
+
61
+ // 2. Roles defined
62
+ const roles = normalized ? Object.keys(normalized.roles || {}) : [];
63
+ if (roles.length > 0) {
64
+ checks.push({ id: 'roles_defined', name: 'Roles defined', level: 'pass', detail: `${roles.length} role${roles.length > 1 ? 's' : ''}: ${roles.join(', ')}` });
65
+ } else {
66
+ checks.push({ id: 'roles_defined', name: 'Roles defined', level: 'fail', detail: 'No roles defined' });
67
+ }
68
+
69
+ // 3. Runtime reachable — one sub-check per runtime
70
+ // Use normalized runtimes if available, otherwise fall back to raw config
71
+ const runtimes = (normalized && normalized.runtimes) || rawConfig.runtimes || {};
72
+ for (const [rtId, rt] of Object.entries(runtimes)) {
73
+ const check = checkRuntimeReachable(rtId, rt);
74
+ checks.push(check);
75
+ }
76
+
77
+ // 4. State directory
78
+ const stateDir = join(root, '.agentxchain');
79
+ if (existsSync(stateDir)) {
80
+ checks.push({ id: 'state_dir', name: 'State directory', level: 'pass', detail: '.agentxchain/ exists' });
81
+ } else {
82
+ checks.push({ id: 'state_dir', name: 'State directory', level: 'warn', detail: '.agentxchain/ missing (created on first run)' });
83
+ }
84
+
85
+ // 5. State health
86
+ const statePath = join(root, '.agentxchain', 'state.json');
87
+ if (existsSync(statePath)) {
88
+ try {
89
+ const stateData = JSON.parse(readFileSync(statePath, 'utf8'));
90
+ if (stateData.schema_version) {
91
+ checks.push({ id: 'state_health', name: 'State health', level: 'pass', detail: `schema_version: ${stateData.schema_version}, status: ${stateData.status || 'unknown'}` });
92
+ } else {
93
+ checks.push({ id: 'state_health', name: 'State health', level: 'fail', detail: 'State file missing schema_version' });
94
+ }
95
+ } catch {
96
+ checks.push({ id: 'state_health', name: 'State health', level: 'fail', detail: 'State file is malformed JSON' });
97
+ }
98
+ } else {
99
+ checks.push({ id: 'state_health', name: 'State health', level: 'warn', detail: 'No state file yet (first run pending)' });
100
+ }
101
+
102
+ // 6. Schedule health (only when schedules configured)
103
+ const schedules = normalized?.schedules;
104
+ const hasSchedules = schedules && typeof schedules === 'object' && Object.keys(schedules).length > 0;
105
+ if (hasSchedules) {
106
+ const daemonState = readDaemonState(root);
107
+ const daemonEval = evaluateDaemonStatus(daemonState);
108
+ if (daemonEval.status === 'running') {
109
+ const detail = `Daemon running (last heartbeat ${daemonEval.heartbeat_age_seconds}s ago)`;
110
+ checks.push({ id: 'schedule_health', name: 'Schedule health', level: 'pass', detail });
111
+ } else {
112
+ const detail = `Daemon ${daemonEval.status}${daemonEval.warning ? `: ${daemonEval.warning}` : ''}`;
113
+ checks.push({ id: 'schedule_health', name: 'Schedule health', level: 'warn', detail });
114
+ }
115
+ }
116
+
117
+ // 7. Workflow-kit artifacts (current phase)
118
+ if (normalized?.workflow_kit?.phases) {
119
+ const currentPhase = getCurrentPhase(root) || Object.keys(normalized.routing || {})[0] || 'planning';
120
+ const phaseKit = normalized.workflow_kit.phases[currentPhase];
121
+ if (phaseKit?.artifacts?.length > 0) {
122
+ const required = phaseKit.artifacts.filter(a => a.required !== false);
123
+ const missing = required.filter(a => !existsSync(join(root, a.path)));
124
+ if (missing.length === 0) {
125
+ checks.push({ id: 'workflow_kit', name: 'Workflow-kit artifacts', level: 'pass', detail: `All ${required.length} required artifacts present for ${currentPhase}` });
126
+ } else {
127
+ checks.push({ id: 'workflow_kit', name: 'Workflow-kit artifacts', level: 'warn', detail: `${missing.length}/${required.length} required artifacts missing for ${currentPhase}` });
128
+ }
129
+ }
130
+ }
131
+
132
+ // Compute summary
133
+ const failCount = checks.filter(c => c.level === 'fail').length;
134
+ const warnCount = checks.filter(c => c.level === 'warn').length;
135
+ const overall = failCount > 0 ? 'fail' : warnCount > 0 ? 'warn' : 'pass';
136
+
137
+ if (opts.json) {
138
+ const projectId = rawConfig?.project?.id || rawConfig?.project?.name || 'unknown';
139
+ console.log(JSON.stringify({
140
+ project: projectId,
141
+ config_version: 4,
142
+ overall,
143
+ checks,
144
+ fail_count: failCount,
145
+ warn_count: warnCount,
146
+ }, null, 2));
147
+ } else {
148
+ const projectId = rawConfig?.project?.id || rawConfig?.project?.name || 'unknown';
149
+ console.log('');
150
+ console.log(chalk.bold(' AgentXchain Governed Doctor'));
151
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
152
+ console.log(chalk.dim(` Project: ${projectId} (v4)`));
153
+ console.log('');
154
+
155
+ for (const c of checks) {
156
+ const badge = c.level === 'pass'
157
+ ? chalk.green('PASS')
158
+ : c.level === 'warn'
159
+ ? chalk.yellow('WARN')
160
+ : chalk.red('FAIL');
161
+ console.log(` ${badge} ${c.name.padEnd(24)} ${chalk.dim(c.detail)}`);
162
+ }
163
+
164
+ console.log('');
165
+ if (failCount === 0 && warnCount === 0) {
166
+ console.log(chalk.green(' ✓ Governed project is ready.'));
167
+ } else if (failCount === 0) {
168
+ console.log(chalk.yellow(` Ready with ${warnCount} warning${warnCount > 1 ? 's' : ''}.`));
169
+ } else {
170
+ console.log(chalk.red(` Not ready: ${failCount} failure${failCount > 1 ? 's' : ''}, ${warnCount} warning${warnCount > 1 ? 's' : ''}.`));
171
+ }
172
+ console.log('');
173
+ }
174
+
175
+ process.exit(failCount > 0 ? 1 : 0);
176
+ }
177
+
178
+ function checkRuntimeReachable(rtId, rt) {
179
+ const base = { id: `runtime_${rtId}`, name: `Runtime: ${rtId}` };
180
+
181
+ if (!rt || !rt.type) {
182
+ return { ...base, level: 'warn', detail: 'No runtime type specified' };
183
+ }
184
+
185
+ switch (rt.type) {
186
+ case 'manual':
187
+ return { ...base, level: 'pass', detail: 'Manual runtime (no binary needed)' };
188
+
189
+ case 'local_cli': {
190
+ const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
191
+ if (!cmd) return { ...base, level: 'warn', detail: 'No command configured' };
192
+ try {
193
+ execSync(`command -v ${cmd}`, { stdio: 'ignore' });
194
+ return { ...base, level: 'pass', detail: `${cmd} binary found` };
195
+ } catch {
196
+ return { ...base, level: 'fail', detail: `${cmd} not found in PATH` };
197
+ }
198
+ }
199
+
200
+ case 'api_proxy': {
201
+ const envVar = rt.auth_env;
202
+ if (!envVar) {
203
+ // ollama and similar providers may not require auth
204
+ return { ...base, level: 'pass', detail: `${rt.provider || 'unknown'} provider (no auth required)` };
205
+ }
206
+ if (process.env[envVar]) {
207
+ return { ...base, level: 'pass', detail: `${envVar} is set` };
208
+ }
209
+ return { ...base, level: 'fail', detail: `${envVar} not set` };
210
+ }
211
+
212
+ case 'mcp': {
213
+ const transport = rt.transport || 'stdio';
214
+ if (transport === 'streamable_http') {
215
+ return { ...base, level: 'warn', detail: 'Remote MCP endpoint (cannot verify at doctor time)' };
216
+ }
217
+ const cmd = Array.isArray(rt.command) ? rt.command[0] : (typeof rt.command === 'string' ? rt.command.split(/\s+/)[0] : null);
218
+ if (!cmd) return { ...base, level: 'warn', detail: 'No MCP command configured' };
219
+ try {
220
+ execSync(`command -v ${cmd}`, { stdio: 'ignore' });
221
+ return { ...base, level: 'pass', detail: `${cmd} binary found` };
222
+ } catch {
223
+ return { ...base, level: 'fail', detail: `${cmd} not found in PATH` };
224
+ }
225
+ }
226
+
227
+ case 'remote_agent':
228
+ return { ...base, level: 'warn', detail: 'Remote agent endpoint (cannot verify at doctor time)' };
229
+
230
+ default:
231
+ return { ...base, level: 'warn', detail: `Unknown runtime type: ${rt.type}` };
232
+ }
233
+ }
234
+
235
+ function getCurrentPhase(root) {
236
+ const statePath = join(root, '.agentxchain', 'state.json');
237
+ if (!existsSync(statePath)) return null;
238
+ try {
239
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
240
+ return state.current_phase || null;
241
+ } catch {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ // ── Legacy (v3) Doctor ──────────────────────────────────────────────────────
247
+
248
+ function legacyDoctor(root, opts) {
249
+ const result = loadConfig(root);
11
250
  if (!result) {
12
251
  console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
13
252
  process.exit(1);
14
253
  }
15
254
 
16
- const { root, config } = result;
255
+ const { config } = result;
17
256
  const lock = loadLock(root);
18
257
  const checks = [];
19
258
 
@@ -24,7 +263,7 @@ export async function doctorCommand() {
24
263
  checks.push(checkBinary('osascript', 'osascript available (required for auto-nudge, macOS)'));
25
264
  checks.push(checkPm(config));
26
265
  checks.push(checkValidation(root, config));
27
- checks.push(checkWatchProcess());
266
+ checks.push(checkWatchProcess(root));
28
267
  checks.push(checkTrigger(root));
29
268
  checks.push(checkAccessibility());
30
269
 
@@ -86,13 +325,10 @@ function checkPm(config) {
86
325
  return { name: 'PM agent', level: 'warn', detail: 'No explicit PM agent. PM-first onboarding will be less clear.' };
87
326
  }
88
327
 
89
- function checkWatchProcess() {
90
- const result = loadConfig();
91
- if (result) {
92
- const pid = getWatchPid(result.root);
93
- if (pid) {
94
- return { name: 'watch process', level: 'pass', detail: `watch running (PID: ${pid})` };
95
- }
328
+ function checkWatchProcess(root) {
329
+ const pid = getWatchPid(root);
330
+ if (pid) {
331
+ return { name: 'watch process', level: 'pass', detail: `watch running (PID: ${pid})` };
96
332
  }
97
333
  try {
98
334
  execSync('pgrep -f "agentxchain.*watch" >/dev/null', { stdio: 'ignore' });
@@ -127,12 +363,24 @@ function checkAccessibility() {
127
363
  }
128
364
 
129
365
  try {
130
- execSync(
131
- 'osascript -e \'tell application "System Events" to get name of first process\'',
132
- { stdio: 'pipe' }
366
+ execFileSync(
367
+ 'osascript',
368
+ ['-e', 'tell application "System Events" to get name of first process'],
369
+ {
370
+ stdio: 'pipe',
371
+ timeout: 1500,
372
+ killSignal: 'SIGKILL',
373
+ },
133
374
  );
134
375
  return { name: 'macOS Accessibility', level: 'pass', detail: 'System Events access available' };
135
- } catch {
376
+ } catch (err) {
377
+ if (err?.signal === 'SIGKILL' || err?.message?.includes('ETIMEDOUT')) {
378
+ return {
379
+ name: 'macOS Accessibility',
380
+ level: 'warn',
381
+ detail: 'Accessibility probe timed out. Grant Accessibility to Terminal and Cursor in System Settings.',
382
+ };
383
+ }
136
384
  return {
137
385
  name: 'macOS Accessibility',
138
386
  level: 'warn',