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.
- package/bin/agentxchain.js +2 -1
- package/package.json +1 -1
- package/scripts/release-bump.sh +73 -5
- package/src/commands/doctor.js +265 -17
package/bin/agentxchain.js
CHANGED
|
@@ -249,7 +249,8 @@ program
|
|
|
249
249
|
|
|
250
250
|
program
|
|
251
251
|
.command('doctor')
|
|
252
|
-
.description('Check local environment
|
|
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
package/scripts/release-bump.sh
CHANGED
|
@@ -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/
|
|
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:
|
|
281
|
-
echo "Then: git push origin main --follow-tags"
|
|
349
|
+
echo "Next: git push origin main --follow-tags"
|
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
91
|
-
if (
|
|
92
|
-
|
|
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
|
-
|
|
131
|
-
'osascript
|
|
132
|
-
|
|
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',
|