agentxchain 2.51.0 → 2.53.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.51.0",
3
+ "version": "2.53.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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',
@@ -968,6 +968,7 @@ async function initGoverned(opts) {
968
968
  console.log(` ${chalk.bold('git init')} ${chalk.dim('# initialize the governed repo')}`);
969
969
  }
970
970
  console.log(` ${chalk.bold('agentxchain template validate')} ${chalk.dim('# prove the scaffold contract before the first turn')}`);
971
+ console.log(` ${chalk.bold('agentxchain doctor')} ${chalk.dim('# verify runtimes, config, and readiness')}`);
971
972
  console.log(` ${chalk.bold('git add -A')} ${chalk.dim('# stage the governed scaffold')}`);
972
973
  console.log(` ${chalk.bold('git commit -m "initial governed scaffold"')} ${chalk.dim('# checkpoint the starting state')}`);
973
974
  console.log(` ${chalk.bold('agentxchain step')} ${chalk.dim('# run the first governed turn')}`);