agentxchain 0.8.7 → 2.1.1

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.
Files changed (94) hide show
  1. package/README.md +123 -154
  2. package/bin/agentxchain.js +240 -8
  3. package/dashboard/app.js +305 -0
  4. package/dashboard/components/blocked.js +145 -0
  5. package/dashboard/components/cross-repo.js +126 -0
  6. package/dashboard/components/gate.js +311 -0
  7. package/dashboard/components/hooks.js +177 -0
  8. package/dashboard/components/initiative.js +147 -0
  9. package/dashboard/components/ledger.js +165 -0
  10. package/dashboard/components/timeline.js +222 -0
  11. package/dashboard/index.html +352 -0
  12. package/package.json +16 -7
  13. package/scripts/agentxchain-autonudge.applescript +32 -5
  14. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  15. package/scripts/publish-from-tag.sh +88 -0
  16. package/scripts/release-postflight.sh +231 -0
  17. package/scripts/release-preflight.sh +167 -0
  18. package/scripts/run-autonudge.sh +1 -1
  19. package/src/adapters/claude-code.js +7 -14
  20. package/src/adapters/cursor-local.js +17 -16
  21. package/src/commands/accept-turn.js +160 -0
  22. package/src/commands/approve-completion.js +80 -0
  23. package/src/commands/approve-transition.js +85 -0
  24. package/src/commands/branch.js +2 -2
  25. package/src/commands/claim.js +84 -9
  26. package/src/commands/config.js +16 -0
  27. package/src/commands/dashboard.js +70 -0
  28. package/src/commands/doctor.js +9 -1
  29. package/src/commands/init.js +540 -5
  30. package/src/commands/migrate.js +348 -0
  31. package/src/commands/multi.js +549 -0
  32. package/src/commands/plugin.js +157 -0
  33. package/src/commands/reject-turn.js +204 -0
  34. package/src/commands/resume.js +389 -0
  35. package/src/commands/status.js +196 -3
  36. package/src/commands/step.js +947 -0
  37. package/src/commands/stop.js +65 -33
  38. package/src/commands/template-list.js +33 -0
  39. package/src/commands/template-set.js +279 -0
  40. package/src/commands/update.js +24 -3
  41. package/src/commands/validate.js +20 -11
  42. package/src/commands/verify.js +71 -0
  43. package/src/commands/watch.js +112 -25
  44. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  45. package/src/lib/adapters/local-cli-adapter.js +337 -0
  46. package/src/lib/adapters/manual-adapter.js +169 -0
  47. package/src/lib/blocked-state.js +94 -0
  48. package/src/lib/config.js +143 -12
  49. package/src/lib/context-compressor.js +121 -0
  50. package/src/lib/context-section-parser.js +220 -0
  51. package/src/lib/coordinator-acceptance.js +428 -0
  52. package/src/lib/coordinator-config.js +461 -0
  53. package/src/lib/coordinator-dispatch.js +276 -0
  54. package/src/lib/coordinator-gates.js +487 -0
  55. package/src/lib/coordinator-hooks.js +239 -0
  56. package/src/lib/coordinator-recovery.js +523 -0
  57. package/src/lib/coordinator-state.js +365 -0
  58. package/src/lib/cross-repo-context.js +247 -0
  59. package/src/lib/dashboard/bridge-server.js +284 -0
  60. package/src/lib/dashboard/file-watcher.js +93 -0
  61. package/src/lib/dashboard/state-reader.js +96 -0
  62. package/src/lib/dispatch-bundle.js +568 -0
  63. package/src/lib/dispatch-manifest.js +252 -0
  64. package/src/lib/filter-agents.js +12 -0
  65. package/src/lib/gate-evaluator.js +285 -0
  66. package/src/lib/generate-vscode.js +158 -68
  67. package/src/lib/governed-state.js +2139 -0
  68. package/src/lib/governed-templates.js +145 -0
  69. package/src/lib/hook-runner.js +788 -0
  70. package/src/lib/next-owner.js +61 -6
  71. package/src/lib/normalized-config.js +539 -0
  72. package/src/lib/notify.js +14 -12
  73. package/src/lib/plugin-config-schema.js +192 -0
  74. package/src/lib/plugins.js +692 -0
  75. package/src/lib/prompt-core.js +108 -0
  76. package/src/lib/protocol-conformance.js +291 -0
  77. package/src/lib/reference-conformance-adapter.js +717 -0
  78. package/src/lib/repo-observer.js +597 -0
  79. package/src/lib/repo.js +0 -31
  80. package/src/lib/safe-write.js +44 -0
  81. package/src/lib/schema.js +189 -0
  82. package/src/lib/schemas/turn-result.schema.json +205 -0
  83. package/src/lib/seed-prompt-polling.js +15 -73
  84. package/src/lib/seed-prompt.js +17 -63
  85. package/src/lib/token-budget.js +206 -0
  86. package/src/lib/token-counter.js +27 -0
  87. package/src/lib/turn-paths.js +67 -0
  88. package/src/lib/turn-result-validator.js +496 -0
  89. package/src/lib/validation.js +167 -19
  90. package/src/lib/verify-command.js +72 -0
  91. package/src/templates/governed/api-service.json +31 -0
  92. package/src/templates/governed/cli-tool.json +30 -0
  93. package/src/templates/governed/generic.json +10 -0
  94. package/src/templates/governed/web-app.json +30 -0
@@ -0,0 +1,80 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
3
+ import { approveRunCompletion } from '../lib/governed-state.js';
4
+ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
5
+
6
+ export async function approveCompletionCommand(opts) {
7
+ const context = loadProjectContext();
8
+ if (!context) {
9
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ if (context.config.protocol_mode !== 'governed') {
14
+ console.log(chalk.red('approve-completion is only available in governed mode.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const { root, config } = context;
19
+ const state = loadProjectState(root, config);
20
+
21
+ if (!state?.pending_run_completion) {
22
+ console.log(chalk.yellow('No pending run completion to approve.'));
23
+ if (state?.status === 'completed') {
24
+ console.log(chalk.dim(' This run is already completed.'));
25
+ } else if (state?.phase) {
26
+ console.log(chalk.dim(` Current phase: ${state.phase}, status: ${state.status}`));
27
+ }
28
+ process.exit(1);
29
+ }
30
+
31
+ const pc = state.pending_run_completion;
32
+ console.log('');
33
+ console.log(chalk.bold(' Approving Run Completion'));
34
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
35
+ console.log(` ${chalk.dim('Phase:')} ${state.phase}`);
36
+ console.log(` ${chalk.dim('Gate:')} ${pc.gate}`);
37
+ console.log(` ${chalk.dim('Turn:')} ${pc.requested_by_turn}`);
38
+ console.log('');
39
+
40
+ const result = approveRunCompletion(root, config);
41
+
42
+ if (!result.ok) {
43
+ if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
44
+ printGateHookFailure(result, 'run_completion', pc);
45
+ } else {
46
+ console.log(chalk.red(` Failed: ${result.error}`));
47
+ }
48
+ process.exit(1);
49
+ }
50
+
51
+ console.log(chalk.green(' \u2713 Run completed'));
52
+ console.log(chalk.dim(` Completed at: ${result.state.completed_at}`));
53
+ console.log('');
54
+ }
55
+
56
+ function printGateHookFailure(result, gateType, gateInfo) {
57
+ const recovery = deriveRecoveryDescriptor(result.state);
58
+ const hookName = result.hookResults?.blocker?.hook_name
59
+ || result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
60
+ || '(unknown)';
61
+
62
+ console.log('');
63
+ console.log(chalk.yellow(` ${gateType === 'phase_transition' ? 'Phase Transition' : 'Run Completion'} Blocked By Hook`));
64
+ console.log(chalk.dim(' ' + '-'.repeat(44)));
65
+ console.log('');
66
+ console.log(` ${chalk.dim('Gate:')} ${gateInfo.gate}`);
67
+ console.log(` ${chalk.dim('Hook:')} ${hookName}`);
68
+ console.log(` ${chalk.dim('Error:')} ${result.error}`);
69
+ if (recovery) {
70
+ console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
71
+ console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
72
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
73
+ if (recovery.detail) {
74
+ console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
75
+ }
76
+ } else {
77
+ console.log(` ${chalk.dim('Action:')} Fix or reconfigure hook "${hookName}", then rerun agentxchain approve-completion`);
78
+ }
79
+ console.log('');
80
+ }
@@ -0,0 +1,85 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
3
+ import { approvePhaseTransition } from '../lib/governed-state.js';
4
+ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
5
+
6
+ export async function approveTransitionCommand(opts) {
7
+ const context = loadProjectContext();
8
+ if (!context) {
9
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ if (context.config.protocol_mode !== 'governed') {
14
+ console.log(chalk.red('approve-transition is only available in governed mode.'));
15
+ process.exit(1);
16
+ }
17
+
18
+ const { root, config } = context;
19
+ const state = loadProjectState(root, config);
20
+
21
+ if (!state?.pending_phase_transition) {
22
+ console.log(chalk.yellow('No pending phase transition to approve.'));
23
+ if (state?.phase) {
24
+ console.log(chalk.dim(` Current phase: ${state.phase}`));
25
+ }
26
+ process.exit(1);
27
+ }
28
+
29
+ const pt = state.pending_phase_transition;
30
+ console.log('');
31
+ console.log(chalk.bold(' Approving Phase Transition'));
32
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
33
+ console.log(` ${chalk.dim('From:')} ${pt.from}`);
34
+ console.log(` ${chalk.dim('To:')} ${pt.to}`);
35
+ console.log(` ${chalk.dim('Gate:')} ${pt.gate}`);
36
+ console.log(` ${chalk.dim('Turn:')} ${pt.requested_by_turn}`);
37
+ console.log('');
38
+
39
+ const result = approvePhaseTransition(root, config);
40
+
41
+ if (!result.ok) {
42
+ if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
43
+ printGateHookFailure(result, 'phase_transition', pt);
44
+ } else {
45
+ console.log(chalk.red(` Failed: ${result.error}`));
46
+ }
47
+ process.exit(1);
48
+ }
49
+
50
+ console.log(chalk.green(` ✓ Phase advanced: ${pt.from} → ${pt.to}`));
51
+ console.log(chalk.dim(` Run status: ${result.state.status}`));
52
+ console.log('');
53
+ console.log(chalk.dim(` Next: agentxchain step (to run the first turn in ${pt.to} phase)`));
54
+ console.log('');
55
+ }
56
+
57
+ function printGateHookFailure(result, gateType, gateInfo) {
58
+ const recovery = deriveRecoveryDescriptor(result.state);
59
+ const hookName = result.hookResults?.blocker?.hook_name
60
+ || result.hookResults?.results?.find((entry) => entry.hook_name)?.hook_name
61
+ || '(unknown)';
62
+
63
+ console.log('');
64
+ console.log(chalk.yellow(` ${gateType === 'phase_transition' ? 'Phase Transition' : 'Run Completion'} Blocked By Hook`));
65
+ console.log(chalk.dim(' ' + '-'.repeat(44)));
66
+ console.log('');
67
+ if (gateType === 'phase_transition') {
68
+ console.log(` ${chalk.dim('From:')} ${gateInfo.from}`);
69
+ console.log(` ${chalk.dim('To:')} ${gateInfo.to}`);
70
+ }
71
+ console.log(` ${chalk.dim('Gate:')} ${gateInfo.gate}`);
72
+ console.log(` ${chalk.dim('Hook:')} ${hookName}`);
73
+ console.log(` ${chalk.dim('Error:')} ${result.error}`);
74
+ if (recovery) {
75
+ console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
76
+ console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
77
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
78
+ if (recovery.detail) {
79
+ console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
80
+ }
81
+ } else {
82
+ console.log(` ${chalk.dim('Action:')} Fix or reconfigure hook "${hookName}", then rerun agentxchain approve-transition`);
83
+ }
84
+ console.log('');
85
+ }
@@ -1,7 +1,7 @@
1
- import { writeFileSync } from 'fs';
2
1
  import { join } from 'path';
3
2
  import chalk from 'chalk';
4
3
  import { loadConfig, CONFIG_FILE } from '../lib/config.js';
4
+ import { safeWriteJson } from '../lib/safe-write.js';
5
5
  import { getCurrentBranch } from '../lib/repo.js';
6
6
 
7
7
  export async function branchCommand(name, opts) {
@@ -94,5 +94,5 @@ function setBranchOverride(config, configPath, branch) {
94
94
  }
95
95
 
96
96
  function saveConfig(configPath, config) {
97
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
97
+ safeWriteJson(configPath, config);
98
98
  }
@@ -1,8 +1,10 @@
1
- import { writeFileSync, existsSync, readFileSync } from 'fs';
1
+ import { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
5
- import { resolveNextAgent } from '../lib/next-owner.js';
5
+ import { safeWriteJson } from '../lib/safe-write.js';
6
+ import { resolveExpectedClaimer } from '../lib/next-owner.js';
7
+ import { runConfiguredVerify } from '../lib/verify-command.js';
6
8
 
7
9
  export async function claimCommand(opts) {
8
10
  const result = loadConfig();
@@ -40,7 +42,12 @@ export async function claimCommand(opts) {
40
42
  turn_number: lock.turn_number,
41
43
  claimed_at: new Date().toISOString()
42
44
  };
43
- writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
45
+ safeWriteJson(lockPath, newLock);
46
+ const verify = loadLock(root);
47
+ if (verify?.holder !== 'human') {
48
+ console.log(chalk.red(` Claim race: expected holder=human, got ${verify?.holder}. Another process won.`));
49
+ process.exit(1);
50
+ }
44
51
  clearBlockedState(root);
45
52
 
46
53
  console.log('');
@@ -76,6 +83,11 @@ export async function releaseCommand(opts) {
76
83
  }
77
84
 
78
85
  const who = lock.holder;
86
+ const verifyResult = runConfiguredVerify(config, root);
87
+ if (!verifyResult.ok) {
88
+ console.log(chalk.red(` Verification failed: ${verifyResult.command}`));
89
+ process.exit(1);
90
+ }
79
91
  const lockPath = join(root, LOCK_FILE);
80
92
  const newLock = {
81
93
  holder: null,
@@ -83,14 +95,19 @@ export async function releaseCommand(opts) {
83
95
  turn_number: who === 'human' ? lock.turn_number : lock.turn_number + 1,
84
96
  claimed_at: null
85
97
  };
86
- writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
98
+ safeWriteJson(lockPath, newLock);
99
+ const verify = loadLock(root);
100
+ if (verify?.holder !== null || verify?.last_released_by !== who || verify?.turn_number !== newLock.turn_number) {
101
+ console.log(chalk.red(' Release race: lock.json changed unexpectedly after release attempt.'));
102
+ process.exit(1);
103
+ }
87
104
  if (who === 'human') {
88
105
  clearBlockedState(root);
89
106
  }
90
107
 
91
108
  console.log('');
92
109
  console.log(chalk.green(` ✓ Lock released by ${chalk.bold(who)} (turn ${newLock.turn_number})`));
93
- console.log(chalk.dim(' The Stop hook will coordinate the next agent turn in VS Code.'));
110
+ console.log(chalk.dim(' Next turn will be coordinated by the VS Code Stop hook or watch/supervise.'));
94
111
  console.log('');
95
112
  }
96
113
 
@@ -108,11 +125,25 @@ function claimAsAgent({ opts, root, config, lock }) {
108
125
  }
109
126
 
110
127
  const expected = pickNextAgent(root, lock, config);
128
+ if (!opts.force && config.rules?.strict_next_owner && (expected === null || expected === undefined)) {
129
+ console.log(chalk.red(' No next owner resolved. Add a valid `Next owner: <agent_id>` line to TALK.md, or set rules.strict_next_owner to false.'));
130
+ process.exit(1);
131
+ }
111
132
  if (!opts.force && expected && expected !== agentId) {
112
133
  console.log(chalk.red(` Out-of-turn claim blocked. Expected: ${expected}, got: ${agentId}.`));
113
134
  process.exit(1);
114
135
  }
115
136
 
137
+ const maxClaims = Number(config.rules?.max_consecutive_claims || 0);
138
+ if (!opts.force && maxClaims > 0 && lock.last_released_by === agentId) {
139
+ const consecutiveTurns = countRecentTurnsByAgent(root, config, agentId);
140
+ if (consecutiveTurns >= maxClaims) {
141
+ console.log(chalk.red(` Consecutive-claim limit reached for "${agentId}" (${consecutiveTurns}/${maxClaims}).`));
142
+ console.log(chalk.dim(' Hand off to another agent or use --force for recovery only.'));
143
+ process.exit(1);
144
+ }
145
+ }
146
+
116
147
  const lockPath = join(root, LOCK_FILE);
117
148
  const next = {
118
149
  holder: agentId,
@@ -120,7 +151,14 @@ function claimAsAgent({ opts, root, config, lock }) {
120
151
  turn_number: lock.turn_number,
121
152
  claimed_at: new Date().toISOString()
122
153
  };
123
- writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
154
+ safeWriteJson(lockPath, next);
155
+
156
+ const verify = loadLock(root);
157
+ if (verify?.holder !== agentId) {
158
+ console.log(chalk.red(` Claim race: expected holder=${agentId}, got ${verify?.holder}. Another process won.`));
159
+ process.exit(1);
160
+ }
161
+
124
162
  console.log(chalk.green(` ✓ Lock claimed by ${agentId} (turn ${next.turn_number})`));
125
163
  }
126
164
 
@@ -135,6 +173,12 @@ function releaseAsAgent({ opts, root, config, lock }) {
135
173
  process.exit(1);
136
174
  }
137
175
 
176
+ const verifyResult = runConfiguredVerify(config, root);
177
+ if (!verifyResult.ok) {
178
+ console.log(chalk.red(` Verification failed: ${verifyResult.command}`));
179
+ process.exit(1);
180
+ }
181
+
138
182
  const lockPath = join(root, LOCK_FILE);
139
183
  const next = {
140
184
  holder: null,
@@ -142,12 +186,21 @@ function releaseAsAgent({ opts, root, config, lock }) {
142
186
  turn_number: lock.turn_number + 1,
143
187
  claimed_at: null
144
188
  };
145
- writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
189
+ safeWriteJson(lockPath, next);
190
+ const verifyRelease = loadLock(root);
191
+ if (
192
+ verifyRelease?.holder !== null ||
193
+ verifyRelease?.last_released_by !== agentId ||
194
+ verifyRelease?.turn_number !== next.turn_number
195
+ ) {
196
+ console.log(chalk.red(' Release race: lock.json changed unexpectedly after release attempt.'));
197
+ process.exit(1);
198
+ }
146
199
  console.log(chalk.green(` ✓ Lock released by ${agentId} (turn ${next.turn_number})`));
147
200
  }
148
201
 
149
202
  function pickNextAgent(root, lock, config) {
150
- return resolveNextAgent(root, config, lock).next;
203
+ return resolveExpectedClaimer(root, config, lock).next;
151
204
  }
152
205
 
153
206
  function clearBlockedState(root) {
@@ -157,7 +210,29 @@ function clearBlockedState(root) {
157
210
  const state = JSON.parse(readFileSync(statePath, 'utf8'));
158
211
  if (state.blocked || state.blocked_on) {
159
212
  const next = { ...state, blocked: false, blocked_on: null };
160
- writeFileSync(statePath, JSON.stringify(next, null, 2) + '\n');
213
+ safeWriteJson(statePath, next);
161
214
  }
162
215
  } catch {}
163
216
  }
217
+
218
+ function countRecentTurnsByAgent(root, config, agentId) {
219
+ const historyPath = join(root, config.history_file || 'history.jsonl');
220
+ if (!existsSync(historyPath)) return 0;
221
+
222
+ try {
223
+ const lines = readFileSync(historyPath, 'utf8')
224
+ .split(/\r?\n/)
225
+ .map(line => line.trim())
226
+ .filter(Boolean);
227
+
228
+ let count = 0;
229
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
230
+ const entry = JSON.parse(lines[i]);
231
+ if (entry?.agent !== agentId) break;
232
+ count += 1;
233
+ }
234
+ return count;
235
+ } catch {
236
+ return 0;
237
+ }
238
+ }
@@ -3,6 +3,7 @@ import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import inquirer from 'inquirer';
5
5
  import { loadConfig, CONFIG_FILE } from '../lib/config.js';
6
+ import { validateConfigSchema } from '../lib/schema.js';
6
7
 
7
8
  export async function configCommand(opts) {
8
9
  const result = loadConfig();
@@ -127,6 +128,12 @@ function setSetting(config, configPath, keyValPair) {
127
128
  const key = parts[0];
128
129
  const rawVal = parts.slice(1).join(' ');
129
130
  const segments = key.split('.');
131
+ const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
132
+
133
+ if (segments.some(segment => forbiddenKeys.has(segment))) {
134
+ console.log(chalk.red(' Refusing to write reserved object path.'));
135
+ process.exit(1);
136
+ }
130
137
 
131
138
  let target = config;
132
139
  for (let i = 0; i < segments.length - 1; i++) {
@@ -145,6 +152,15 @@ function setSetting(config, configPath, keyValPair) {
145
152
  else if (!isNaN(rawVal) && rawVal !== '') val = Number(rawVal);
146
153
 
147
154
  target[lastKey] = val;
155
+ const validation = validateConfigSchema(config);
156
+ if (!validation.ok) {
157
+ target[lastKey] = oldVal;
158
+ if (oldVal === undefined) {
159
+ delete target[lastKey];
160
+ }
161
+ console.log(chalk.red(` Refusing to save invalid config: ${validation.errors.join(', ')}`));
162
+ process.exit(1);
163
+ }
148
164
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
149
165
 
150
166
  console.log('');
@@ -0,0 +1,70 @@
1
+ /**
2
+ * CLI command: agentxchain dashboard
3
+ *
4
+ * Starts the read-only dashboard bridge server and opens a browser.
5
+ * See: V2_DASHBOARD_SPEC.md, DEC-DASH-002 (read-only in v2.0).
6
+ */
7
+
8
+ import { existsSync } from 'fs';
9
+ import { join, dirname } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import { createBridgeServer } from '../lib/dashboard/bridge-server.js';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const DEFAULT_PORT = 3847;
15
+
16
+ export async function dashboardCommand(options) {
17
+ const cwd = process.cwd();
18
+ const agentxchainDir = join(cwd, '.agentxchain');
19
+ const dashboardDir = join(__dirname, '..', '..', 'dashboard');
20
+
21
+ if (!existsSync(agentxchainDir)) {
22
+ console.error('Error: No .agentxchain/ directory found in the current directory.');
23
+ console.error('Run "agentxchain init --governed" first to create a governed project.');
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!existsSync(dashboardDir)) {
28
+ console.error('Error: Dashboard assets not found at', dashboardDir);
29
+ process.exit(1);
30
+ }
31
+
32
+ const port = parseInt(options.port, 10) || DEFAULT_PORT;
33
+ const bridge = createBridgeServer({ agentxchainDir, dashboardDir, port });
34
+
35
+ try {
36
+ const { port: actualPort } = await bridge.start();
37
+ const url = `http://localhost:${actualPort}`;
38
+
39
+ console.log(`Dashboard running at ${url}`);
40
+ console.log('Press Ctrl+C to stop.\n');
41
+
42
+ if (options.open !== false) {
43
+ try {
44
+ const { exec } = await import('child_process');
45
+ const openCmd = process.platform === 'darwin' ? 'open'
46
+ : process.platform === 'win32' ? 'start'
47
+ : 'xdg-open';
48
+ exec(`${openCmd} ${url}`);
49
+ } catch {
50
+ // Browser open is best-effort
51
+ }
52
+ }
53
+
54
+ // Keep running until interrupted
55
+ const shutdown = async () => {
56
+ console.log('\nShutting down dashboard...');
57
+ await bridge.stop();
58
+ process.exit(0);
59
+ };
60
+ process.on('SIGINT', shutdown);
61
+ process.on('SIGTERM', shutdown);
62
+
63
+ } catch (err) {
64
+ if (err.code === 'EADDRINUSE') {
65
+ console.error(`Error: Port ${port} is already in use. Try --port <number>.`);
66
+ process.exit(1);
67
+ }
68
+ throw err;
69
+ }
70
+ }
@@ -4,6 +4,7 @@ import { join } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import { loadConfig, loadLock } from '../lib/config.js';
6
6
  import { validateProject } from '../lib/validation.js';
7
+ import { getWatchPid } from './watch.js';
7
8
 
8
9
  export async function doctorCommand() {
9
10
  const result = loadConfig();
@@ -86,9 +87,16 @@ function checkPm(config) {
86
87
  }
87
88
 
88
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
+ }
96
+ }
89
97
  try {
90
98
  execSync('pgrep -f "agentxchain.*watch" >/dev/null', { stdio: 'ignore' });
91
- return { name: 'watch process', level: 'pass', detail: 'watch appears to be running' };
99
+ return { name: 'watch process', level: 'pass', detail: 'watch appears to be running (no PID file)' };
92
100
  } catch {
93
101
  return { name: 'watch process', level: 'warn', detail: 'watch not running (start with `agentxchain watch` or `agentxchain supervise --autonudge`)' };
94
102
  }