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.
- package/README.md +123 -154
- package/bin/agentxchain.js +240 -8
- package/dashboard/app.js +305 -0
- package/dashboard/components/blocked.js +145 -0
- package/dashboard/components/cross-repo.js +126 -0
- package/dashboard/components/gate.js +311 -0
- package/dashboard/components/hooks.js +177 -0
- package/dashboard/components/initiative.js +147 -0
- package/dashboard/components/ledger.js +165 -0
- package/dashboard/components/timeline.js +222 -0
- package/dashboard/index.html +352 -0
- package/package.json +16 -7
- package/scripts/agentxchain-autonudge.applescript +32 -5
- package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
- package/scripts/publish-from-tag.sh +88 -0
- package/scripts/release-postflight.sh +231 -0
- package/scripts/release-preflight.sh +167 -0
- package/scripts/run-autonudge.sh +1 -1
- package/src/adapters/claude-code.js +7 -14
- package/src/adapters/cursor-local.js +17 -16
- package/src/commands/accept-turn.js +160 -0
- package/src/commands/approve-completion.js +80 -0
- package/src/commands/approve-transition.js +85 -0
- package/src/commands/branch.js +2 -2
- package/src/commands/claim.js +84 -9
- package/src/commands/config.js +16 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +540 -5
- package/src/commands/migrate.js +348 -0
- package/src/commands/multi.js +549 -0
- package/src/commands/plugin.js +157 -0
- package/src/commands/reject-turn.js +204 -0
- package/src/commands/resume.js +389 -0
- package/src/commands/status.js +196 -3
- package/src/commands/step.js +947 -0
- package/src/commands/stop.js +65 -33
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/update.js +24 -3
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/commands/watch.js +112 -25
- package/src/lib/adapters/api-proxy-adapter.js +1076 -0
- package/src/lib/adapters/local-cli-adapter.js +337 -0
- package/src/lib/adapters/manual-adapter.js +169 -0
- package/src/lib/blocked-state.js +94 -0
- package/src/lib/config.js +143 -12
- package/src/lib/context-compressor.js +121 -0
- package/src/lib/context-section-parser.js +220 -0
- package/src/lib/coordinator-acceptance.js +428 -0
- package/src/lib/coordinator-config.js +461 -0
- package/src/lib/coordinator-dispatch.js +276 -0
- package/src/lib/coordinator-gates.js +487 -0
- package/src/lib/coordinator-hooks.js +239 -0
- package/src/lib/coordinator-recovery.js +523 -0
- package/src/lib/coordinator-state.js +365 -0
- package/src/lib/cross-repo-context.js +247 -0
- package/src/lib/dashboard/bridge-server.js +284 -0
- package/src/lib/dashboard/file-watcher.js +93 -0
- package/src/lib/dashboard/state-reader.js +96 -0
- package/src/lib/dispatch-bundle.js +568 -0
- package/src/lib/dispatch-manifest.js +252 -0
- package/src/lib/filter-agents.js +12 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/generate-vscode.js +158 -68
- package/src/lib/governed-state.js +2139 -0
- package/src/lib/governed-templates.js +145 -0
- package/src/lib/hook-runner.js +788 -0
- package/src/lib/next-owner.js +61 -6
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/notify.js +14 -12
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/prompt-core.js +108 -0
- package/src/lib/protocol-conformance.js +291 -0
- package/src/lib/reference-conformance-adapter.js +717 -0
- package/src/lib/repo-observer.js +597 -0
- package/src/lib/repo.js +0 -31
- package/src/lib/safe-write.js +44 -0
- package/src/lib/schema.js +189 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/seed-prompt-polling.js +15 -73
- package/src/lib/seed-prompt.js +17 -63
- package/src/lib/token-budget.js +206 -0
- package/src/lib/token-counter.js +27 -0
- package/src/lib/turn-paths.js +67 -0
- package/src/lib/turn-result-validator.js +496 -0
- package/src/lib/validation.js +167 -19
- package/src/lib/verify-command.js +72 -0
- package/src/templates/governed/api-service.json +31 -0
- package/src/templates/governed/cli-tool.json +30 -0
- package/src/templates/governed/generic.json +10 -0
- 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
|
+
}
|
package/src/commands/branch.js
CHANGED
|
@@ -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
|
-
|
|
97
|
+
safeWriteJson(configPath, config);
|
|
98
98
|
}
|
package/src/commands/claim.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/config.js
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
}
|