agentxchain 0.8.7 → 0.8.8
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 +14 -29
- package/bin/agentxchain.js +54 -3
- package/package.json +3 -2
- package/scripts/agentxchain-autonudge.applescript +32 -5
- 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/branch.js +2 -2
- package/src/commands/claim.js +84 -9
- package/src/commands/config.js +16 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +24 -5
- package/src/commands/stop.js +65 -33
- package/src/commands/update.js +24 -3
- package/src/commands/watch.js +112 -25
- package/src/lib/config.js +47 -12
- package/src/lib/filter-agents.js +12 -0
- package/src/lib/generate-vscode.js +158 -68
- package/src/lib/next-owner.js +61 -6
- package/src/lib/notify.js +14 -12
- package/src/lib/prompt-core.js +108 -0
- package/src/lib/safe-write.js +44 -0
- package/src/lib/schema.js +68 -0
- package/src/lib/seed-prompt-polling.js +15 -73
- package/src/lib/seed-prompt.js +17 -63
- package/src/lib/validation.js +30 -19
- package/src/lib/verify-command.js +72 -0
package/src/commands/init.js
CHANGED
|
@@ -52,7 +52,12 @@ export async function initCommand(opts) {
|
|
|
52
52
|
project = 'My AgentXchain project';
|
|
53
53
|
agents = DEFAULT_AGENTS;
|
|
54
54
|
folderName = slugify(project);
|
|
55
|
-
rules = {
|
|
55
|
+
rules = {
|
|
56
|
+
max_consecutive_claims: 2,
|
|
57
|
+
require_message: true,
|
|
58
|
+
compress_after_words: 5000,
|
|
59
|
+
strict_next_owner: false
|
|
60
|
+
};
|
|
56
61
|
} else {
|
|
57
62
|
const templates = loadTemplates();
|
|
58
63
|
|
|
@@ -86,7 +91,12 @@ export async function initCommand(opts) {
|
|
|
86
91
|
project = projectName;
|
|
87
92
|
} else if (template === 'default') {
|
|
88
93
|
agents = DEFAULT_AGENTS;
|
|
89
|
-
rules = {
|
|
94
|
+
rules = {
|
|
95
|
+
max_consecutive_claims: 2,
|
|
96
|
+
require_message: true,
|
|
97
|
+
compress_after_words: 5000,
|
|
98
|
+
strict_next_owner: false
|
|
99
|
+
};
|
|
90
100
|
const { projectName } = await inquirer.prompt([{
|
|
91
101
|
type: 'input',
|
|
92
102
|
name: 'projectName',
|
|
@@ -103,7 +113,12 @@ export async function initCommand(opts) {
|
|
|
103
113
|
}]);
|
|
104
114
|
project = projectName;
|
|
105
115
|
agents = {};
|
|
106
|
-
rules = {
|
|
116
|
+
rules = {
|
|
117
|
+
max_consecutive_claims: 2,
|
|
118
|
+
require_message: true,
|
|
119
|
+
compress_after_words: 5000,
|
|
120
|
+
strict_next_owner: false
|
|
121
|
+
};
|
|
107
122
|
|
|
108
123
|
const { count } = await inquirer.prompt([{
|
|
109
124
|
type: 'number',
|
|
@@ -192,6 +207,7 @@ export async function initCommand(opts) {
|
|
|
192
207
|
compress_after_words: rules.compress_after_words || 5000,
|
|
193
208
|
ttl_minutes: rules.ttl_minutes || 10,
|
|
194
209
|
watch_interval_ms: rules.watch_interval_ms || 5000,
|
|
210
|
+
strict_next_owner: rules.strict_next_owner === true,
|
|
195
211
|
...(rules.verify_command ? { verify_command: rules.verify_command } : {})
|
|
196
212
|
}
|
|
197
213
|
};
|
|
@@ -209,7 +225,7 @@ export async function initCommand(opts) {
|
|
|
209
225
|
writeFileSync(join(dir, 'TALK.md'), `# ${project} — Team Talk File\n\nCanonical human-readable handoff log for all agents.\n\n## How to write entries\n\nUse this exact structure:\n\n## Turn N — <agent_id> (<role>)\n- Status:\n- Decision:\n- Action:\n- Risks/Questions:\n- Next owner:\n\n---\n\n`);
|
|
210
226
|
writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
|
|
211
227
|
const gitignorePath = join(dir, '.gitignore');
|
|
212
|
-
const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/'];
|
|
228
|
+
const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/', '.agentxchain-watch.pid', '.agentxchain-autonudge.state'];
|
|
213
229
|
if (!existsSync(gitignorePath)) {
|
|
214
230
|
writeFileSync(gitignorePath, requiredIgnores.join('\n') + '\n');
|
|
215
231
|
} else {
|
|
@@ -230,10 +246,13 @@ export async function initCommand(opts) {
|
|
|
230
246
|
|
|
231
247
|
writeFileSync(join(dir, '.planning', 'REQUIREMENTS.md'), `# Requirements — ${project}\n\n## v1 (MVP)\n\n(PM fills this: numbered list of requirements. Each requirement has one-sentence acceptance criteria.)\n\n| # | Requirement | Acceptance criteria | Phase | Status |\n|---|-------------|-------------------|-------|--------|\n| 1 | | | | Pending |\n\n## v2 (Future)\n\n(Out of scope for MVP. Captured here so they don't creep in.)\n\n## Out of scope\n\n(Explicitly not building.)\n`);
|
|
232
248
|
|
|
233
|
-
writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${project}\n\n## Phases\n\n| Phase | Description | Status | Requirements |\n|-------|-------------|--------|-------------|\n| 1 | Discovery + setup | In progress | — |\n\n(PM updates this as phases are planned and completed.)\n`);
|
|
249
|
+
writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${project}\n\n## Waves\n\n| Wave | Goal | Status |\n|------|------|--------|\n| Wave 1 | Discovery, planning, and phase setup | In progress |\n\n## Phases\n\n| Phase | Description | Status | Requirements |\n|-------|-------------|--------|-------------|\n| 1 | Discovery + setup | In progress | — |\n\n(PM updates this as phases are planned and completed.)\n`);
|
|
234
250
|
writeFileSync(join(dir, '.planning', 'PM_SIGNOFF.md'), `# PM Signoff — ${project}\n\nApproved: NO\n\n## Discovery Checklist\n- [ ] Target user defined\n- [ ] Core pain point defined\n- [ ] Core workflow defined\n- [ ] MVP scope defined\n- [ ] Out-of-scope list defined\n- [ ] Success metric defined\n\n## Notes for team\n(PM and human add final kickoff notes here.)\n`);
|
|
235
251
|
|
|
236
252
|
// QA structure
|
|
253
|
+
mkdirSync(join(dir, '.planning', 'phases', 'phase-1'), { recursive: true });
|
|
254
|
+
writeFileSync(join(dir, '.planning', 'phases', 'phase-1', 'PLAN.md'), `# Phase 1 Plan — ${project}\n\n## Goal\n\nAlign scope, requirements, and initial implementation plan.\n\n## Deliverables\n\n- PM signoff\n- Initial requirements and roadmap\n- First implementation slice\n`);
|
|
255
|
+
writeFileSync(join(dir, '.planning', 'phases', 'phase-1', 'TESTS.md'), `# Phase 1 Tests — ${project}\n\n## Planned checks\n\n- Kickoff validation passes\n- Requirements have acceptance criteria\n- First implementation slice has an executable verification path\n`);
|
|
237
256
|
writeFileSync(join(dir, '.planning', 'qa', 'TEST-COVERAGE.md'), `# Test Coverage — ${project}\n\n## Coverage Map\n\n| Feature / Area | Unit tests | Integration tests | E2E tests | Manual QA | UX audit | Status |\n|---------------|-----------|------------------|----------|----------|---------|--------|\n| (QA fills this as testing progresses) | | | | | | |\n\n## Coverage gaps\n\n(Areas with no tests or insufficient coverage.)\n`);
|
|
238
257
|
|
|
239
258
|
writeFileSync(join(dir, '.planning', 'qa', 'REGRESSION-LOG.md'), `# Regression Log — ${project}\n\nBugs that were found and fixed. Each entry has a regression test to prevent recurrence.\n\n| Bug ID | Description | Found turn | Fixed turn | Regression test | Status |\n|--------|-------------|-----------|-----------|----------------|--------|\n| (QA adds entries as bugs are found and fixed) | | | | | |\n`);
|
package/src/commands/stop.js
CHANGED
|
@@ -2,8 +2,10 @@ import { readFileSync, existsSync, unlinkSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadConfig } from '../lib/config.js';
|
|
5
|
+
import { getWatchPid } from './watch.js';
|
|
5
6
|
|
|
6
7
|
const SESSION_FILE = '.agentxchain-session.json';
|
|
8
|
+
const WATCH_PID_FILE = '.agentxchain-watch.pid';
|
|
7
9
|
|
|
8
10
|
export async function stopCommand() {
|
|
9
11
|
const result = loadConfig();
|
|
@@ -11,47 +13,77 @@ export async function stopCommand() {
|
|
|
11
13
|
|
|
12
14
|
const { root } = result;
|
|
13
15
|
const sessionPath = join(root, SESSION_FILE);
|
|
16
|
+
const watchPidPath = join(root, WATCH_PID_FILE);
|
|
17
|
+
const watchPid = getWatchPid(root);
|
|
18
|
+
let didStopAnything = false;
|
|
14
19
|
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
if (watchPid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(watchPid, 'SIGTERM');
|
|
23
|
+
didStopAnything = true;
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(chalk.green(` ✓ Stopped watch process (PID: ${watchPid})`));
|
|
26
|
+
console.log('');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code === 'ESRCH') {
|
|
29
|
+
if (existsSync(watchPidPath)) {
|
|
30
|
+
try { unlinkSync(watchPidPath); } catch {}
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
console.log(chalk.red(` ✗ Could not stop watch process (PID: ${watchPid}): ${err.message}`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else if (existsSync(watchPidPath)) {
|
|
37
|
+
// Stale PID file from an unexpected shutdown.
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(watchPidPath);
|
|
40
|
+
console.log(chalk.dim(' Removed stale watch PID file.'));
|
|
41
|
+
} catch {}
|
|
19
42
|
}
|
|
20
43
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
if (existsSync(sessionPath)) {
|
|
45
|
+
let session;
|
|
46
|
+
try {
|
|
47
|
+
session = JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
console.log(chalk.yellow(' Could not read session file.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(chalk.bold(` Stopping ${session.launched?.length || 0} agents (${session.ide || 'unknown'})`));
|
|
55
|
+
console.log('');
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} else {
|
|
43
|
-
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
57
|
+
if (session.ide === 'claude-code') {
|
|
58
|
+
for (const agent of (session.launched || [])) {
|
|
59
|
+
if (agent.pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(agent.pid, 'SIGTERM');
|
|
62
|
+
didStopAnything = true;
|
|
63
|
+
console.log(chalk.green(` ✓ Sent SIGTERM to ${agent.id} (PID: ${agent.pid})`));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === 'ESRCH') {
|
|
66
|
+
console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
|
|
67
|
+
} else {
|
|
68
|
+
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
69
|
+
}
|
|
44
70
|
}
|
|
45
71
|
}
|
|
46
72
|
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log(chalk.dim(' For VS Code / Cursor agents, close the chat sessions manually.'));
|
|
47
75
|
}
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
|
|
77
|
+
unlinkSync(sessionPath);
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(chalk.dim(' Session file removed.'));
|
|
80
|
+
console.log(chalk.green(' Done.'));
|
|
81
|
+
console.log('');
|
|
82
|
+
return;
|
|
50
83
|
}
|
|
51
84
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('');
|
|
85
|
+
if (!didStopAnything) {
|
|
86
|
+
console.log(chalk.yellow(' No active session found.'));
|
|
87
|
+
console.log(chalk.dim(' If agents are running in VS Code / Cursor, close their chat sessions manually.'));
|
|
88
|
+
}
|
|
57
89
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -4,6 +4,19 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
7
|
+
function printGlobalInstallFallbacks() {
|
|
8
|
+
console.log(chalk.dim(' If global install failed with permission errors (EACCES):'));
|
|
9
|
+
console.log(` ${chalk.bold('sudo npm install -g agentxchain@latest')} ${chalk.dim('(macOS/Linux)')}`);
|
|
10
|
+
console.log(` ${chalk.bold('npm config get prefix')} ${chalk.dim('— fix ownership of that directory, or use a user prefix:')}`);
|
|
11
|
+
console.log(` ${chalk.bold('mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global')}`);
|
|
12
|
+
console.log(` ${chalk.dim('(add ~/.npm-global/bin to PATH)')}`);
|
|
13
|
+
console.log(chalk.dim(' Or run without global install:'));
|
|
14
|
+
console.log(` ${chalk.bold('npx agentxchain@latest <command>')}`);
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log(chalk.dim(' Node: use 18.17+ or 20.5+ to avoid engine warnings from dependencies.'));
|
|
17
|
+
console.log('');
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export async function updateCommand() {
|
|
8
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
22
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
|
|
@@ -24,18 +37,26 @@ export async function updateCommand() {
|
|
|
24
37
|
|
|
25
38
|
console.log(` ${chalk.dim('Latest version:')} ${chalk.cyan(latest)}`);
|
|
26
39
|
console.log('');
|
|
27
|
-
console.log(
|
|
40
|
+
console.log(' Updating...');
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
try {
|
|
43
|
+
execSync('npm install -g agentxchain@latest', { stdio: 'inherit' });
|
|
44
|
+
} catch {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(chalk.yellow(' Global install failed. Common fixes:'));
|
|
47
|
+
printGlobalInstallFallbacks();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
30
50
|
|
|
31
51
|
console.log('');
|
|
32
52
|
console.log(chalk.green(` ✓ Updated to ${latest}`));
|
|
33
53
|
console.log('');
|
|
34
54
|
} catch (err) {
|
|
35
55
|
console.log('');
|
|
36
|
-
console.log(chalk.yellow(' Could not
|
|
56
|
+
console.log(chalk.yellow(' Could not check or install the latest version.'));
|
|
37
57
|
console.log(` ${chalk.bold('npm install -g agentxchain@latest')}`);
|
|
38
58
|
console.log('');
|
|
59
|
+
printGlobalInstallFallbacks();
|
|
39
60
|
console.log(chalk.dim(` Error: ${err.message}`));
|
|
40
61
|
console.log('');
|
|
41
62
|
}
|
package/src/commands/watch.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
|
|
7
|
+
import { safeWriteJson } from '../lib/safe-write.js';
|
|
7
8
|
import { notifyHuman as sendNotification } from '../lib/notify.js';
|
|
8
9
|
import { validateProject } from '../lib/validation.js';
|
|
9
|
-
import { resolveNextAgent } from '../lib/next-owner.js';
|
|
10
|
+
import { resolveNextAgent, resolveExpectedClaimer } from '../lib/next-owner.js';
|
|
11
|
+
|
|
12
|
+
const PID_FILE = '.agentxchain-watch.pid';
|
|
10
13
|
|
|
11
14
|
export async function watchCommand(opts) {
|
|
12
15
|
if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
|
|
@@ -37,12 +40,15 @@ export async function watchCommand(opts) {
|
|
|
37
40
|
console.log(chalk.dim(` Poll: ${interval}ms | TTL: ${ttlMinutes}min`));
|
|
38
41
|
console.log(chalk.dim(` Mode: Local file watcher + trigger file`));
|
|
39
42
|
console.log('');
|
|
43
|
+
writePidFile(root);
|
|
44
|
+
|
|
40
45
|
console.log(chalk.cyan(' Watching lock.json... (Ctrl+C to stop)'));
|
|
41
46
|
console.log(chalk.dim(' Note: In VS Code/Cursor, the Stop hook coordinates turns automatically.'));
|
|
42
47
|
console.log(chalk.dim(' This watch process is a fallback for non-IDE environments.'));
|
|
43
48
|
console.log('');
|
|
44
49
|
|
|
45
50
|
let lastState = null;
|
|
51
|
+
let lastClaimedState = null;
|
|
46
52
|
|
|
47
53
|
const tick = async () => {
|
|
48
54
|
try {
|
|
@@ -51,17 +57,6 @@ export async function watchCommand(opts) {
|
|
|
51
57
|
|
|
52
58
|
const stateKey = `${lock.holder}:${lock.turn_number}`;
|
|
53
59
|
|
|
54
|
-
if (lock.holder && lock.holder !== 'human') {
|
|
55
|
-
const expected = pickNextAgent(root, lock, config);
|
|
56
|
-
if (!isValidClaimer(root, lock, config)) {
|
|
57
|
-
log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected}. Handing lock to HUMAN.`);
|
|
58
|
-
blockOnIllegalClaim(root, lock, config, expected);
|
|
59
|
-
sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
|
|
60
|
-
lastState = null;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
60
|
if (lock.holder && lock.holder !== 'human' && lock.claimed_at) {
|
|
66
61
|
const elapsed = Date.now() - new Date(lock.claimed_at).getTime();
|
|
67
62
|
const ttlMs = ttlMinutes * 60 * 1000;
|
|
@@ -86,11 +81,27 @@ export async function watchCommand(opts) {
|
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
if (lock.holder) {
|
|
84
|
+
// Validate claim ownership only once per new claimed state.
|
|
85
|
+
// With handoff-driven routing, TALK.md may change during an active turn.
|
|
86
|
+
// Re-validating every tick can produce false "illegal claim" alerts.
|
|
87
|
+
if (stateKey !== lastClaimedState && lock.holder !== 'human') {
|
|
88
|
+
const expected = pickNextAgent(root, lock, config);
|
|
89
|
+
if (!isValidClaimer(root, lock, config)) {
|
|
90
|
+
log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected ?? 'none'}. Handing lock to HUMAN.`);
|
|
91
|
+
blockOnIllegalClaim(root, lock, config, expected);
|
|
92
|
+
sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
|
|
93
|
+
lastState = null;
|
|
94
|
+
lastClaimedState = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
if (stateKey !== lastState) {
|
|
90
100
|
const name = config.agents[lock.holder]?.name || lock.holder;
|
|
91
101
|
log('claimed', `${lock.holder} (${name}) working... (turn ${lock.turn_number})`);
|
|
92
102
|
lastState = stateKey;
|
|
93
103
|
}
|
|
104
|
+
lastClaimedState = stateKey;
|
|
94
105
|
return;
|
|
95
106
|
}
|
|
96
107
|
|
|
@@ -109,7 +120,15 @@ export async function watchCommand(opts) {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
const
|
|
123
|
+
const resolved = resolveNextAgent(root, config, lock);
|
|
124
|
+
const next = resolved.next;
|
|
125
|
+
if (!next) {
|
|
126
|
+
log('warn', `No next owner (${resolved.source}). strict_next_owner requires TALK.md handoff. Handing lock to HUMAN.`);
|
|
127
|
+
blockOnMissingNext(root, lock, config, resolved.source);
|
|
128
|
+
sendNotification('No next owner in TALK.md. Human action required.');
|
|
129
|
+
lastState = null;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
113
132
|
log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Next: ${chalk.bold(next)}.`);
|
|
114
133
|
writeTrigger(root, next, lock, config);
|
|
115
134
|
lastState = stateKey;
|
|
@@ -122,12 +141,16 @@ export async function watchCommand(opts) {
|
|
|
122
141
|
await tick();
|
|
123
142
|
const timer = setInterval(tick, interval);
|
|
124
143
|
|
|
125
|
-
|
|
144
|
+
const cleanup = () => {
|
|
126
145
|
clearInterval(timer);
|
|
146
|
+
removePidFile(root);
|
|
127
147
|
console.log('');
|
|
128
148
|
log('stop', 'Watch stopped.');
|
|
129
149
|
process.exit(0);
|
|
130
|
-
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
process.on('SIGINT', cleanup);
|
|
153
|
+
process.on('SIGTERM', cleanup);
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
function pickNextAgent(root, lock, config) {
|
|
@@ -137,7 +160,7 @@ function pickNextAgent(root, lock, config) {
|
|
|
137
160
|
function isValidClaimer(root, lock, config) {
|
|
138
161
|
if (!lock.holder || lock.holder === 'human') return true;
|
|
139
162
|
if (!config.agents?.[lock.holder]) return false;
|
|
140
|
-
const expected =
|
|
163
|
+
const expected = resolveExpectedClaimer(root, config, lock).next;
|
|
141
164
|
return lock.holder === expected;
|
|
142
165
|
}
|
|
143
166
|
|
|
@@ -146,10 +169,10 @@ function forceRelease(root, lock, staleAgent, config) {
|
|
|
146
169
|
const newLock = {
|
|
147
170
|
holder: null,
|
|
148
171
|
last_released_by: `system:ttl:${staleAgent}`,
|
|
149
|
-
turn_number: lock.turn_number,
|
|
172
|
+
turn_number: lock.turn_number + 1,
|
|
150
173
|
claimed_at: null
|
|
151
174
|
};
|
|
152
|
-
|
|
175
|
+
safeWriteJson(lockPath, newLock);
|
|
153
176
|
|
|
154
177
|
const logFile = config.log || 'log.md';
|
|
155
178
|
const logPath = join(root, logFile);
|
|
@@ -161,12 +184,12 @@ function forceRelease(root, lock, staleAgent, config) {
|
|
|
161
184
|
function writeTrigger(root, agentId, lock, config) {
|
|
162
185
|
if (!agentId) return;
|
|
163
186
|
const triggerPath = join(root, '.agentxchain-trigger.json');
|
|
164
|
-
|
|
187
|
+
safeWriteJson(triggerPath, {
|
|
165
188
|
agent: agentId,
|
|
166
189
|
turn_number: lock.turn_number,
|
|
167
190
|
triggered_at: new Date().toISOString(),
|
|
168
191
|
project: config.project
|
|
169
|
-
}
|
|
192
|
+
});
|
|
170
193
|
}
|
|
171
194
|
|
|
172
195
|
function blockOnValidation(root, lock, config, validation) {
|
|
@@ -177,7 +200,7 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
177
200
|
turn_number: lock.turn_number,
|
|
178
201
|
claimed_at: new Date().toISOString()
|
|
179
202
|
};
|
|
180
|
-
|
|
203
|
+
safeWriteJson(lockPath, newLock);
|
|
181
204
|
|
|
182
205
|
const statePath = join(root, 'state.json');
|
|
183
206
|
if (existsSync(statePath)) {
|
|
@@ -189,7 +212,7 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
189
212
|
blocked: true,
|
|
190
213
|
blocked_on: `validation: ${message}`
|
|
191
214
|
};
|
|
192
|
-
|
|
215
|
+
safeWriteJson(statePath, nextState);
|
|
193
216
|
} catch {}
|
|
194
217
|
}
|
|
195
218
|
|
|
@@ -204,6 +227,39 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
204
227
|
}
|
|
205
228
|
}
|
|
206
229
|
|
|
230
|
+
function blockOnMissingNext(root, lock, config, source) {
|
|
231
|
+
const lockPath = join(root, LOCK_FILE);
|
|
232
|
+
const newLock = {
|
|
233
|
+
holder: 'human',
|
|
234
|
+
last_released_by: lock.last_released_by,
|
|
235
|
+
turn_number: lock.turn_number,
|
|
236
|
+
claimed_at: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
safeWriteJson(lockPath, newLock);
|
|
239
|
+
|
|
240
|
+
const statePath = join(root, 'state.json');
|
|
241
|
+
if (existsSync(statePath)) {
|
|
242
|
+
try {
|
|
243
|
+
const current = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
244
|
+
const nextState = {
|
|
245
|
+
...current,
|
|
246
|
+
blocked: true,
|
|
247
|
+
blocked_on: `missing-next-owner: ${source}`
|
|
248
|
+
};
|
|
249
|
+
safeWriteJson(statePath, nextState);
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const logFile = config.log || 'log.md';
|
|
254
|
+
const logPath = join(root, logFile);
|
|
255
|
+
if (existsSync(logPath)) {
|
|
256
|
+
appendFileSync(
|
|
257
|
+
logPath,
|
|
258
|
+
`\n---\n\n### [system] (Watch) | Turn ${lock.turn_number}\n\n**Status:** Could not resolve next agent (${source}).\n\n**Action:** Add \`Next owner: <agent_id>\` to ${config.talk_file || 'TALK.md'}, or set \`rules.strict_next_owner\` to false for cyclic fallback.\n\n`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
207
263
|
function blockOnIllegalClaim(root, lock, config, expected) {
|
|
208
264
|
const lockPath = join(root, LOCK_FILE);
|
|
209
265
|
const newLock = {
|
|
@@ -212,7 +268,7 @@ function blockOnIllegalClaim(root, lock, config, expected) {
|
|
|
212
268
|
turn_number: lock.turn_number,
|
|
213
269
|
claimed_at: new Date().toISOString()
|
|
214
270
|
};
|
|
215
|
-
|
|
271
|
+
safeWriteJson(lockPath, newLock);
|
|
216
272
|
|
|
217
273
|
const statePath = join(root, 'state.json');
|
|
218
274
|
if (existsSync(statePath)) {
|
|
@@ -223,7 +279,7 @@ function blockOnIllegalClaim(root, lock, config, expected) {
|
|
|
223
279
|
blocked: true,
|
|
224
280
|
blocked_on: `illegal-claim: expected ${expected}, got ${lock.holder}`
|
|
225
281
|
};
|
|
226
|
-
|
|
282
|
+
safeWriteJson(statePath, nextState);
|
|
227
283
|
} catch {}
|
|
228
284
|
}
|
|
229
285
|
|
|
@@ -262,8 +318,39 @@ function startWatchDaemon() {
|
|
|
262
318
|
});
|
|
263
319
|
child.unref();
|
|
264
320
|
|
|
321
|
+
const result = loadConfig();
|
|
322
|
+
if (result) {
|
|
323
|
+
writeFileSync(join(result.root, PID_FILE), String(child.pid));
|
|
324
|
+
}
|
|
325
|
+
|
|
265
326
|
console.log('');
|
|
266
327
|
console.log(chalk.green(` ✓ Watch started in daemon mode (PID: ${child.pid})`));
|
|
267
328
|
console.log(chalk.dim(' Use `agentxchain stop` or kill the PID to stop it.'));
|
|
268
329
|
console.log('');
|
|
269
330
|
}
|
|
331
|
+
|
|
332
|
+
function writePidFile(root) {
|
|
333
|
+
try {
|
|
334
|
+
writeFileSync(join(root, PID_FILE), String(process.pid));
|
|
335
|
+
} catch {}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function removePidFile(root) {
|
|
339
|
+
try {
|
|
340
|
+
const pidPath = join(root, PID_FILE);
|
|
341
|
+
if (existsSync(pidPath)) unlinkSync(pidPath);
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function getWatchPid(root) {
|
|
346
|
+
try {
|
|
347
|
+
const pidPath = join(root, PID_FILE);
|
|
348
|
+
if (!existsSync(pidPath)) return null;
|
|
349
|
+
const pid = parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
|
|
350
|
+
if (!Number.isFinite(pid)) return null;
|
|
351
|
+
process.kill(pid, 0);
|
|
352
|
+
return pid;
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
package/src/lib/config.js
CHANGED
|
@@ -1,36 +1,71 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { join, parse as pathParse, resolve } from 'path';
|
|
3
|
+
import { safeParseJson, validateConfigSchema, validateLockSchema, validateStateSchema } from './schema.js';
|
|
3
4
|
|
|
4
5
|
const CONFIG_FILE = 'agentxchain.json';
|
|
5
6
|
const LOCK_FILE = 'lock.json';
|
|
6
7
|
const STATE_FILE = 'state.json';
|
|
7
8
|
|
|
8
9
|
export function findProjectRoot(startDir = process.cwd()) {
|
|
9
|
-
let dir = startDir;
|
|
10
|
-
|
|
10
|
+
let dir = resolve(startDir);
|
|
11
|
+
const { root: fsRoot } = pathParse(dir);
|
|
12
|
+
while (true) {
|
|
11
13
|
if (existsSync(join(dir, CONFIG_FILE))) return dir;
|
|
14
|
+
if (dir === fsRoot) return null;
|
|
12
15
|
dir = join(dir, '..');
|
|
13
16
|
}
|
|
14
|
-
return null;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function loadConfig(dir = process.cwd()) {
|
|
18
20
|
const root = findProjectRoot(dir);
|
|
19
21
|
if (!root) return null;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const filePath = join(root, CONFIG_FILE);
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = readFileSync(filePath, 'utf8');
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const result = safeParseJson(raw, validateConfigSchema);
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
console.error(` Warning: agentxchain.json has issues: ${result.errors.join(', ')}`);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return { root, config: result.data };
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
export function loadLock(root) {
|
|
25
|
-
const
|
|
26
|
-
if (!existsSync(
|
|
27
|
-
|
|
38
|
+
const filePath = join(root, LOCK_FILE);
|
|
39
|
+
if (!existsSync(filePath)) return null;
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = readFileSync(filePath, 'utf8');
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const result = safeParseJson(raw, validateLockSchema);
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
console.error(` Warning: lock.json has issues: ${result.errors.join(', ')}`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return result.data;
|
|
28
52
|
}
|
|
29
53
|
|
|
30
54
|
export function loadState(root) {
|
|
31
|
-
const
|
|
32
|
-
if (!existsSync(
|
|
33
|
-
|
|
55
|
+
const filePath = join(root, STATE_FILE);
|
|
56
|
+
if (!existsSync(filePath)) return null;
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
raw = readFileSync(filePath, 'utf8');
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const result = safeParseJson(raw, validateStateSchema);
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
console.error(` Warning: state.json has issues: ${result.errors.join(', ')}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return result.data;
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
export { CONFIG_FILE, LOCK_FILE, STATE_FILE };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export function filterAgents(config, specificId) {
|
|
4
|
+
if (specificId) {
|
|
5
|
+
if (!config.agents[specificId]) {
|
|
6
|
+
console.log(chalk.red(` Agent "${specificId}" not found in agentxchain.json`));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
return { [specificId]: config.agents[specificId] };
|
|
10
|
+
}
|
|
11
|
+
return config.agents;
|
|
12
|
+
}
|