agentxchain 0.1.2 → 0.2.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 +19 -0
- package/package.json +1 -1
- package/src/adapters/cursor.js +134 -44
- package/src/commands/claim.js +117 -0
- package/src/commands/config.js +1 -0
- package/src/commands/init.js +123 -71
- package/src/commands/status.js +54 -16
- package/src/commands/stop.js +43 -59
- package/src/commands/watch.js +210 -0
- package/src/lib/notify.js +24 -0
- package/src/lib/repo.js +37 -0
- package/src/lib/seed-prompt.js +49 -24
- package/src/templates/api-builder.json +29 -0
- package/src/templates/bug-squad.json +25 -0
- package/src/templates/landing-page.json +36 -0
- package/src/templates/refactor-team.json +25 -0
- package/src/templates/saas-mvp.json +29 -0
package/src/commands/status.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig, loadLock, loadState } from '../lib/config.js';
|
|
3
|
+
import { getAgentStatus, loadSession } from '../adapters/cursor.js';
|
|
3
4
|
|
|
4
5
|
export async function statusCommand(opts) {
|
|
5
6
|
const result = loadConfig();
|
|
@@ -19,10 +20,9 @@ export async function statusCommand(opts) {
|
|
|
19
20
|
|
|
20
21
|
console.log('');
|
|
21
22
|
console.log(chalk.bold(' AgentXchain Status'));
|
|
22
|
-
console.log(chalk.dim(' ' + '─'.repeat(
|
|
23
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
23
24
|
console.log('');
|
|
24
25
|
|
|
25
|
-
// Project
|
|
26
26
|
console.log(` ${chalk.dim('Project:')} ${config.project}`);
|
|
27
27
|
console.log(` ${chalk.dim('Phase:')} ${state ? formatPhase(state.phase) : chalk.dim('unknown')}`);
|
|
28
28
|
if (state?.blocked) {
|
|
@@ -32,45 +32,83 @@ export async function statusCommand(opts) {
|
|
|
32
32
|
|
|
33
33
|
// Lock
|
|
34
34
|
if (lock) {
|
|
35
|
-
if (lock.holder) {
|
|
35
|
+
if (lock.holder === 'human') {
|
|
36
|
+
console.log(` ${chalk.dim('Lock:')} ${chalk.magenta('HUMAN')} — you hold the lock`);
|
|
37
|
+
} else if (lock.holder) {
|
|
36
38
|
const agentName = config.agents[lock.holder]?.name || lock.holder;
|
|
37
39
|
console.log(` ${chalk.dim('Lock:')} ${chalk.yellow('CLAIMED')} by ${chalk.bold(lock.holder)} (${agentName})`);
|
|
38
40
|
if (lock.claimed_at) {
|
|
39
|
-
|
|
40
|
-
console.log(` ${chalk.dim('Claimed:')} ${ago} ago`);
|
|
41
|
+
console.log(` ${chalk.dim('Claimed:')} ${timeSince(lock.claimed_at)} ago`);
|
|
41
42
|
}
|
|
42
43
|
} else {
|
|
43
44
|
console.log(` ${chalk.dim('Lock:')} ${chalk.green('FREE')} — any agent can claim`);
|
|
44
45
|
}
|
|
45
46
|
console.log(` ${chalk.dim('Turn:')} ${lock.turn_number}`);
|
|
46
47
|
if (lock.last_released_by) {
|
|
47
|
-
|
|
48
|
-
console.log(` ${chalk.dim('Last:')} ${lock.last_released_by} (${lastName})`);
|
|
48
|
+
console.log(` ${chalk.dim('Last:')} ${lock.last_released_by}`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
console.log('');
|
|
52
52
|
|
|
53
|
+
// Cursor session info
|
|
54
|
+
const session = loadSession(root);
|
|
55
|
+
const apiKey = process.env.CURSOR_API_KEY;
|
|
56
|
+
const hasCursor = session?.ide === 'cursor' && session?.launched?.length > 0;
|
|
57
|
+
|
|
58
|
+
if (hasCursor) {
|
|
59
|
+
console.log(` ${chalk.dim('Cursor:')} ${chalk.cyan('Active session')} (${session.launched.length} agents)`);
|
|
60
|
+
console.log(` ${chalk.dim('Started:')} ${session.started_at}`);
|
|
61
|
+
if (session.repo) console.log(` ${chalk.dim('Repo:')} ${session.repo}`);
|
|
62
|
+
console.log('');
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
// Agents
|
|
54
66
|
console.log(` ${chalk.dim('Agents:')} ${Object.keys(config.agents).length}`);
|
|
67
|
+
|
|
55
68
|
for (const [id, agent] of Object.entries(config.agents)) {
|
|
56
69
|
const isHolder = lock?.holder === id;
|
|
57
70
|
const marker = isHolder ? chalk.yellow('●') : chalk.dim('○');
|
|
58
71
|
const label = isHolder ? chalk.bold(id) : id;
|
|
59
|
-
|
|
72
|
+
|
|
73
|
+
let cursorStatus = '';
|
|
74
|
+
if (hasCursor && apiKey) {
|
|
75
|
+
const cloudAgent = session.launched.find(a => a.id === id);
|
|
76
|
+
if (cloudAgent) {
|
|
77
|
+
try {
|
|
78
|
+
const statusData = await getAgentStatus(apiKey, cloudAgent.cloudId);
|
|
79
|
+
if (statusData?.status) {
|
|
80
|
+
cursorStatus = ` ${formatCursorStatus(statusData.status)}`;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
cursorStatus = chalk.dim(' [API error]');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(` ${marker} ${label} — ${agent.name}${cursorStatus}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (lock?.holder === 'human') {
|
|
92
|
+
console.log(` ${chalk.magenta('●')} ${chalk.bold('human')} — You`);
|
|
60
93
|
}
|
|
94
|
+
|
|
61
95
|
console.log('');
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
function formatPhase(phase) {
|
|
65
|
-
const colors = {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
const colors = { discovery: chalk.blue, build: chalk.green, qa: chalk.yellow, deploy: chalk.magenta, blocked: chalk.red };
|
|
100
|
+
return (colors[phase] || chalk.white)(phase);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatCursorStatus(status) {
|
|
104
|
+
const map = {
|
|
105
|
+
CREATING: chalk.dim('[creating]'),
|
|
106
|
+
RUNNING: chalk.cyan('[running]'),
|
|
107
|
+
FINISHED: chalk.green('[finished]'),
|
|
108
|
+
STOPPED: chalk.yellow('[stopped]'),
|
|
109
|
+
ERRORED: chalk.red('[errored]'),
|
|
71
110
|
};
|
|
72
|
-
|
|
73
|
-
return fn(phase);
|
|
111
|
+
return map[status] || chalk.dim(`[${status}]`);
|
|
74
112
|
}
|
|
75
113
|
|
|
76
114
|
function timeSince(iso) {
|
package/src/commands/stop.js
CHANGED
|
@@ -2,85 +2,69 @@ 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 { deleteAgent, stopAgent, loadSession } from '../adapters/cursor.js';
|
|
5
6
|
|
|
6
7
|
const SESSION_FILE = '.agentxchain-session.json';
|
|
7
8
|
|
|
8
9
|
export async function stopCommand() {
|
|
9
10
|
const result = loadConfig();
|
|
10
|
-
if (!result) {
|
|
11
|
-
console.log(chalk.red('No agentxchain.json found.'));
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
11
|
+
if (!result) { console.log(chalk.red(' No agentxchain.json found.')); process.exit(1); }
|
|
14
12
|
|
|
15
13
|
const { root } = result;
|
|
16
|
-
const
|
|
14
|
+
const session = loadSession(root);
|
|
17
15
|
|
|
18
|
-
if (!
|
|
19
|
-
console.log(chalk.yellow(' No active session found
|
|
20
|
-
console.log(chalk.dim(' If agents are running, stop them manually
|
|
16
|
+
if (!session) {
|
|
17
|
+
console.log(chalk.yellow(' No active session found.'));
|
|
18
|
+
console.log(chalk.dim(' If agents are running, stop them manually.'));
|
|
21
19
|
return;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
const session = JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
25
22
|
console.log('');
|
|
26
23
|
console.log(chalk.bold(` Stopping ${session.launched.length} agents (${session.ide})`));
|
|
27
24
|
console.log('');
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function stopCursorAgents(session) {
|
|
48
|
-
const apiKey = process.env.CURSOR_API_KEY;
|
|
49
|
-
|
|
50
|
-
for (const agent of session.launched) {
|
|
51
|
-
if (agent.cloudId && apiKey) {
|
|
52
|
-
try {
|
|
53
|
-
const res = await fetch(`https://api.cursor.com/v0/agents/${agent.cloudId}`, {
|
|
54
|
-
method: 'DELETE',
|
|
55
|
-
headers: { 'Authorization': `Basic ${btoa(apiKey + ':')}` }
|
|
56
|
-
});
|
|
57
|
-
if (res.ok) {
|
|
58
|
-
console.log(chalk.green(` ✓ Stopped ${agent.id} (cloud ID: ${agent.cloudId})`));
|
|
59
|
-
} else {
|
|
60
|
-
console.log(chalk.yellow(` ⚠ Could not stop ${agent.id}: ${res.status}`));
|
|
26
|
+
if (session.ide === 'cursor') {
|
|
27
|
+
const apiKey = process.env.CURSOR_API_KEY;
|
|
28
|
+
if (!apiKey) {
|
|
29
|
+
console.log(chalk.yellow(' CURSOR_API_KEY not set. Cannot stop agents via API.'));
|
|
30
|
+
console.log(chalk.dim(' Stop them manually at cursor.com/agents'));
|
|
31
|
+
} else {
|
|
32
|
+
for (const agent of session.launched) {
|
|
33
|
+
try {
|
|
34
|
+
const deleted = await deleteAgent(apiKey, agent.cloudId);
|
|
35
|
+
if (deleted) {
|
|
36
|
+
console.log(chalk.green(` ✓ Deleted ${chalk.bold(agent.id)} (${agent.cloudId})`));
|
|
37
|
+
} else {
|
|
38
|
+
console.log(chalk.yellow(` ⚠ Could not delete ${agent.id} — may already be gone`));
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
61
42
|
}
|
|
62
|
-
} catch (err) {
|
|
63
|
-
console.log(chalk.red(` ✗ Error stopping ${agent.id}: ${err.message}`));
|
|
64
43
|
}
|
|
65
|
-
} else {
|
|
66
|
-
console.log(chalk.dim(` ${agent.id} — no cloud ID or API key; stop manually in Cursor.`));
|
|
67
44
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} else {
|
|
81
|
-
console.log(chalk.red(` ✗ Error stopping ${agent.id}: ${err.message}`));
|
|
45
|
+
} else if (session.ide === 'claude-code') {
|
|
46
|
+
for (const agent of session.launched) {
|
|
47
|
+
if (agent.pid) {
|
|
48
|
+
try {
|
|
49
|
+
process.kill(agent.pid, 'SIGTERM');
|
|
50
|
+
console.log(chalk.green(` ✓ Sent SIGTERM to ${agent.id} (PID: ${agent.pid})`));
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err.code === 'ESRCH') {
|
|
53
|
+
console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
|
|
54
|
+
} else {
|
|
55
|
+
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
56
|
+
}
|
|
82
57
|
}
|
|
83
58
|
}
|
|
84
59
|
}
|
|
85
60
|
}
|
|
61
|
+
|
|
62
|
+
// Remove session file
|
|
63
|
+
const sessionPath = join(root, SESSION_FILE);
|
|
64
|
+
if (existsSync(sessionPath)) unlinkSync(sessionPath);
|
|
65
|
+
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.dim(' Session file removed.'));
|
|
68
|
+
console.log(chalk.green(' All agents stopped.'));
|
|
69
|
+
console.log('');
|
|
86
70
|
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
|
|
5
|
+
import { notifyHuman as sendNotification } from '../lib/notify.js';
|
|
6
|
+
import { sendFollowup, getAgentStatus, stopAgent, loadSession } from '../adapters/cursor.js';
|
|
7
|
+
|
|
8
|
+
export async function watchCommand(opts) {
|
|
9
|
+
const result = loadConfig();
|
|
10
|
+
if (!result) {
|
|
11
|
+
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { root, config } = result;
|
|
16
|
+
const interval = config.rules?.watch_interval_ms || 5000;
|
|
17
|
+
const ttlMinutes = config.rules?.ttl_minutes || 10;
|
|
18
|
+
const agentIds = Object.keys(config.agents);
|
|
19
|
+
const apiKey = process.env.CURSOR_API_KEY;
|
|
20
|
+
const session = loadSession(root);
|
|
21
|
+
const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
|
|
22
|
+
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(chalk.bold(' AgentXchain Watch'));
|
|
25
|
+
console.log(chalk.dim(` Project: ${config.project}`));
|
|
26
|
+
console.log(chalk.dim(` Agents: ${agentIds.join(', ')}`));
|
|
27
|
+
console.log(chalk.dim(` Poll: ${interval}ms | TTL: ${ttlMinutes}min`));
|
|
28
|
+
if (hasCursorSession) {
|
|
29
|
+
console.log(chalk.cyan(` Mode: Cursor Cloud Agents (${session.launched.length} agents)`));
|
|
30
|
+
} else {
|
|
31
|
+
console.log(chalk.dim(` Mode: Local trigger file (no Cursor session found)`));
|
|
32
|
+
}
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.cyan(' Watching lock.json... (Ctrl+C to stop)'));
|
|
35
|
+
console.log('');
|
|
36
|
+
|
|
37
|
+
let lastState = null;
|
|
38
|
+
|
|
39
|
+
const tick = async () => {
|
|
40
|
+
try {
|
|
41
|
+
const lock = loadLock(root);
|
|
42
|
+
if (!lock) { log('warn', 'lock.json not found'); return; }
|
|
43
|
+
|
|
44
|
+
const stateKey = `${lock.holder}:${lock.turn_number}`;
|
|
45
|
+
|
|
46
|
+
// TTL check — stale lock
|
|
47
|
+
if (lock.holder && lock.holder !== 'human' && lock.claimed_at) {
|
|
48
|
+
const elapsed = Date.now() - new Date(lock.claimed_at).getTime();
|
|
49
|
+
const ttlMs = ttlMinutes * 60 * 1000;
|
|
50
|
+
|
|
51
|
+
if (elapsed > ttlMs) {
|
|
52
|
+
const staleAgent = lock.holder;
|
|
53
|
+
const minutes = Math.round(elapsed / 60000);
|
|
54
|
+
log('ttl', `Lock held by ${staleAgent} for ${minutes}min. Force-releasing.`);
|
|
55
|
+
|
|
56
|
+
if (hasCursorSession && apiKey) {
|
|
57
|
+
const cloudAgent = session.launched.find(a => a.id === staleAgent);
|
|
58
|
+
if (cloudAgent) {
|
|
59
|
+
await stopAgent(apiKey, cloudAgent.cloudId);
|
|
60
|
+
log('ttl', `Stopped Cursor agent ${staleAgent}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
forceRelease(root, lock, staleAgent, config);
|
|
65
|
+
lastState = null;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Human holder
|
|
71
|
+
if (lock.holder === 'human') {
|
|
72
|
+
if (stateKey !== lastState) {
|
|
73
|
+
log('human', 'Lock held by HUMAN. Run `agentxchain release` when done.');
|
|
74
|
+
sendNotification('An agent needs your help. Run: agentxchain release');
|
|
75
|
+
lastState = stateKey;
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Agent is working
|
|
81
|
+
if (lock.holder) {
|
|
82
|
+
if (stateKey !== lastState) {
|
|
83
|
+
const name = config.agents[lock.holder]?.name || lock.holder;
|
|
84
|
+
log('claimed', `${lock.holder} (${name}) working... (turn ${lock.turn_number})`);
|
|
85
|
+
lastState = stateKey;
|
|
86
|
+
|
|
87
|
+
// Check Cursor agent status if available
|
|
88
|
+
if (hasCursorSession && apiKey) {
|
|
89
|
+
const cloudAgent = session.launched.find(a => a.id === lock.holder);
|
|
90
|
+
if (cloudAgent) {
|
|
91
|
+
const status = await getAgentStatus(apiKey, cloudAgent.cloudId);
|
|
92
|
+
if (status?.status === 'FINISHED') {
|
|
93
|
+
log('warn', `${lock.holder} Cursor agent is FINISHED but lock not released. May need TTL.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Lock is FREE — wake the next agent
|
|
102
|
+
if (stateKey !== lastState) {
|
|
103
|
+
const next = pickNextAgent(lock, config);
|
|
104
|
+
log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Waking ${chalk.bold(next)}.`);
|
|
105
|
+
lastState = stateKey;
|
|
106
|
+
|
|
107
|
+
if (hasCursorSession && apiKey) {
|
|
108
|
+
await wakeCursorAgent(apiKey, session, next, lock, config);
|
|
109
|
+
} else {
|
|
110
|
+
writeTrigger(root, next, lock, config);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log('error', err.message);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
await tick();
|
|
119
|
+
const timer = setInterval(tick, interval);
|
|
120
|
+
|
|
121
|
+
process.on('SIGINT', () => {
|
|
122
|
+
clearInterval(timer);
|
|
123
|
+
console.log('');
|
|
124
|
+
log('stop', 'Watch stopped.');
|
|
125
|
+
process.exit(0);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function wakeCursorAgent(apiKey, session, agentId, lock, config) {
|
|
130
|
+
const cloudAgent = session.launched.find(a => a.id === agentId);
|
|
131
|
+
if (!cloudAgent) {
|
|
132
|
+
log('warn', `No Cursor cloud agent found for "${agentId}". Using trigger file.`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const name = config.agents[agentId]?.name || agentId;
|
|
137
|
+
const wakeMessage = `The lock is free. It's your turn.
|
|
138
|
+
|
|
139
|
+
READ lock.json — if holder is null, CLAIM it by writing holder="${agentId}" and claimed_at=now.
|
|
140
|
+
Then do your work per your mandate:
|
|
141
|
+
- Name: ${name}
|
|
142
|
+
- Mandate: ${config.agents[agentId]?.mandate || '(see agentxchain.json)'}
|
|
143
|
+
|
|
144
|
+
When done:
|
|
145
|
+
1. Update state.md with current project state
|
|
146
|
+
2. Append one line to history.jsonl
|
|
147
|
+
${config.rules?.verify_command ? `3. Run verify: ${config.rules.verify_command} — fix if it fails` : ''}
|
|
148
|
+
${config.rules?.verify_command ? '4' : '3'}. RELEASE lock.json: holder=null, last_released_by="${agentId}", turn_number=${lock.turn_number + 1}, claimed_at=null
|
|
149
|
+
|
|
150
|
+
This must be the last thing you write. The watch process will wake the next agent.`;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await sendFollowup(apiKey, cloudAgent.cloudId, wakeMessage);
|
|
154
|
+
log('wake', `Sent followup to ${chalk.bold(agentId)} (${cloudAgent.cloudId})`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
log('error', `Failed to wake ${agentId}: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function pickNextAgent(lock, config) {
|
|
161
|
+
const agentIds = Object.keys(config.agents);
|
|
162
|
+
const lastAgent = lock.last_released_by;
|
|
163
|
+
|
|
164
|
+
if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
|
|
165
|
+
|
|
166
|
+
const lastIndex = agentIds.indexOf(lastAgent);
|
|
167
|
+
return agentIds[(lastIndex + 1) % agentIds.length];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function forceRelease(root, lock, staleAgent, config) {
|
|
171
|
+
const lockPath = join(root, LOCK_FILE);
|
|
172
|
+
const newLock = {
|
|
173
|
+
holder: null,
|
|
174
|
+
last_released_by: `system:ttl:${staleAgent}`,
|
|
175
|
+
turn_number: lock.turn_number,
|
|
176
|
+
claimed_at: null
|
|
177
|
+
};
|
|
178
|
+
writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
|
|
179
|
+
|
|
180
|
+
const logFile = config.log || 'log.md';
|
|
181
|
+
const logPath = join(root, logFile);
|
|
182
|
+
if (existsSync(logPath)) {
|
|
183
|
+
appendFileSync(logPath, `\n---\n\n### [system] (Watch) | Turn ${lock.turn_number}\n\n**Warning:** Lock force-released from \`${staleAgent}\` (TTL expired).\n\n`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function writeTrigger(root, agentId, lock, config) {
|
|
188
|
+
const triggerPath = join(root, '.agentxchain-trigger.json');
|
|
189
|
+
writeFileSync(triggerPath, JSON.stringify({
|
|
190
|
+
agent: agentId,
|
|
191
|
+
turn_number: lock.turn_number,
|
|
192
|
+
triggered_at: new Date().toISOString(),
|
|
193
|
+
project: config.project
|
|
194
|
+
}, null, 2) + '\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function log(type, msg) {
|
|
198
|
+
const time = new Date().toLocaleTimeString();
|
|
199
|
+
const tags = {
|
|
200
|
+
free: chalk.green('FREE '),
|
|
201
|
+
claimed: chalk.yellow('WORK '),
|
|
202
|
+
wake: chalk.cyan('WAKE '),
|
|
203
|
+
ttl: chalk.red(' TTL '),
|
|
204
|
+
human: chalk.magenta('HUMAN'),
|
|
205
|
+
warn: chalk.yellow('WARN '),
|
|
206
|
+
error: chalk.red('ERR '),
|
|
207
|
+
stop: chalk.dim('STOP '),
|
|
208
|
+
};
|
|
209
|
+
console.log(` ${chalk.dim(time)} ${tags[type] || chalk.dim(type)} ${msg}`);
|
|
210
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export function notifyHuman(message, title = 'AgentXchain') {
|
|
4
|
+
// Terminal bell
|
|
5
|
+
process.stdout.write('\x07');
|
|
6
|
+
|
|
7
|
+
// macOS notification
|
|
8
|
+
if (process.platform === 'darwin') {
|
|
9
|
+
try {
|
|
10
|
+
execSync(`osascript -e 'display notification "${message}" with title "${title}"'`);
|
|
11
|
+
} catch {
|
|
12
|
+
// osascript not available or permission denied
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Linux notification
|
|
17
|
+
if (process.platform === 'linux') {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`notify-send "${title}" "${message}"`);
|
|
20
|
+
} catch {
|
|
21
|
+
// notify-send not available
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/lib/repo.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export async function getRepoUrl(root) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = execSync('git remote get-url origin', { cwd: root, encoding: 'utf8' }).trim();
|
|
6
|
+
|
|
7
|
+
// Convert SSH to HTTPS if needed
|
|
8
|
+
// git@github.com:user/repo.git -> https://github.com/user/repo
|
|
9
|
+
if (raw.startsWith('git@github.com:')) {
|
|
10
|
+
const path = raw.replace('git@github.com:', '').replace(/\.git$/, '');
|
|
11
|
+
return `https://github.com/${path}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Already HTTPS — strip .git suffix
|
|
15
|
+
if (raw.includes('github.com')) {
|
|
16
|
+
return raw.replace(/\.git$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Strip tokens from URL (x-access-token:TOKEN@github.com)
|
|
20
|
+
if (raw.includes('x-access-token')) {
|
|
21
|
+
const cleaned = raw.replace(/https:\/\/x-access-token:[^@]+@/, 'https://');
|
|
22
|
+
return cleaned.replace(/\.git$/, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return raw;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getCurrentBranch(root) {
|
|
32
|
+
try {
|
|
33
|
+
return execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, encoding: 'utf8' }).trim();
|
|
34
|
+
} catch {
|
|
35
|
+
return 'main';
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/lib/seed-prompt.js
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
export function generateSeedPrompt(agentId, agentDef, config) {
|
|
2
2
|
const logFile = config.log || 'log.md';
|
|
3
3
|
const maxClaims = config.rules?.max_consecutive_claims || 2;
|
|
4
|
+
const verifyCmd = config.rules?.verify_command || null;
|
|
5
|
+
const stateFile = config.state_file || 'state.md';
|
|
6
|
+
const historyFile = config.history_file || 'history.jsonl';
|
|
7
|
+
const useSplit = config.state_file || config.history_file;
|
|
8
|
+
|
|
9
|
+
const stateInstructions = useSplit
|
|
10
|
+
? `- Current project state is in "${stateFile}" (read this fully each turn).
|
|
11
|
+
- Turn history is in "${historyFile}" (read only the last 3 lines for recent context).`
|
|
12
|
+
: `- The message log is "${logFile}". The lock is lock.json. Project phase is in state.json.`;
|
|
13
|
+
|
|
14
|
+
const logInstructions = useSplit
|
|
15
|
+
? `4. Update "${stateFile}" — overwrite it with the current state of the project: architecture, active bugs, next steps, open decisions. This is a living document, not append-only.
|
|
16
|
+
5. Append ONE line to "${historyFile}" as JSON: {"turn": N, "agent": "${agentId}", "summary": "...", "files_changed": [...], "verify_result": "pass|fail|skipped", "timestamp": "..."}`
|
|
17
|
+
: `4. Append ONE message to ${logFile}:
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
### [${agentId}] (${agentDef.name}) | Turn N
|
|
21
|
+
**Status:** ...
|
|
22
|
+
**Decision:** ...
|
|
23
|
+
**Action:** ...
|
|
24
|
+
**Next:** ...`;
|
|
25
|
+
|
|
26
|
+
const verifyInstructions = verifyCmd
|
|
27
|
+
? `
|
|
28
|
+
VERIFY BEFORE RELEASING
|
|
29
|
+
- Before releasing the lock, you MUST run: ${verifyCmd}
|
|
30
|
+
- If it fails, fix the issue and run it again. Do NOT release until it passes.
|
|
31
|
+
- Report the verify result in your turn summary.`
|
|
32
|
+
: '';
|
|
4
33
|
|
|
5
34
|
return `You are agent "${agentId}" on an AgentXchain team.
|
|
6
35
|
|
|
@@ -10,35 +39,31 @@ YOUR IDENTITY
|
|
|
10
39
|
|
|
11
40
|
SETUP
|
|
12
41
|
- The project config is in agentxchain.json. Your entry is under agents."${agentId}".
|
|
13
|
-
|
|
14
|
-
- The message log is "${logFile}". The lock is lock.json. Project state is state.json.
|
|
15
|
-
|
|
16
|
-
YOUR LOOP (run forever, do not exit)
|
|
17
|
-
1. Read lock.json. If holder is not null, wait 30 seconds and try again.
|
|
18
|
-
2. When holder is null, CLAIM: write lock.json with holder="${agentId}", claimed_at=now.
|
|
19
|
-
Re-read lock.json to confirm you won. If someone else claimed, go back to step 1.
|
|
20
|
-
3. You have the lock. Read state.json (phase, blocked), then read the latest messages in ${logFile}.
|
|
21
|
-
- If blocked and you can't unblock: short "Still blocked" message, release lock, go to step 1.
|
|
22
|
-
- Otherwise: do your work per your mandate.
|
|
23
|
-
4. Append ONE message to ${logFile}:
|
|
42
|
+
${stateInstructions}
|
|
24
43
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
**Decision:** ...
|
|
29
|
-
**Action:** ...
|
|
30
|
-
**Next:** ...
|
|
44
|
+
HOW YOU WORK
|
|
45
|
+
The AgentXchain Watch process manages coordination. You do NOT need to poll or wait.
|
|
46
|
+
When it's your turn, a trigger file (.agentxchain-trigger.json) appears with your agent ID.
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
YOUR TURN (when triggered):
|
|
49
|
+
1. Read lock.json. Confirm holder is null or is being assigned to you.
|
|
50
|
+
2. CLAIM the lock: write lock.json with holder="${agentId}", claimed_at=current timestamp.
|
|
51
|
+
Re-read to confirm you won. If someone else claimed, stop and wait for next trigger.
|
|
52
|
+
3. You have the lock. Read state and recent context per the files above.
|
|
53
|
+
- If blocked and you can't unblock: short "Still blocked" message, release, done.
|
|
54
|
+
- Otherwise: do your work per your mandate. Write code, run tests, make decisions.
|
|
55
|
+
${logInstructions}
|
|
56
|
+
6. Update state.json if phase or blocked status changed.${verifyInstructions}
|
|
57
|
+
7. RELEASE lock.json: holder=null, last_released_by="${agentId}", turn_number=previous+1, claimed_at=null.
|
|
34
58
|
This MUST be the last thing you write.
|
|
35
|
-
|
|
59
|
+
|
|
60
|
+
After releasing, your turn is done. The watch process will trigger the next agent.
|
|
36
61
|
|
|
37
62
|
RULES
|
|
38
|
-
- Never write without holding the lock.
|
|
39
|
-
- One message per turn. One git commit per turn: "Turn N - ${agentId} - description".
|
|
63
|
+
- Never write files or code without holding the lock.
|
|
64
|
+
- One message/entry per turn. One git commit per turn: "Turn N - ${agentId} - description".
|
|
40
65
|
- Challenge previous work. Find at least one risk or issue. No blind agreement.
|
|
41
66
|
- Stay in your lane. Do what your mandate says.
|
|
42
|
-
- Max ${maxClaims} consecutive claims. If you've hit the limit,
|
|
43
|
-
- Always release the lock.`;
|
|
67
|
+
- Max ${maxClaims} consecutive claims. If you've hit the limit, release without major work.
|
|
68
|
+
- Always release the lock. A stuck lock blocks the entire team.`;
|
|
44
69
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label": "API Builder",
|
|
3
|
+
"description": "Design and build a REST/GraphQL API with tests and docs",
|
|
4
|
+
"project": "API service",
|
|
5
|
+
"agents": {
|
|
6
|
+
"architect": {
|
|
7
|
+
"name": "API Architect",
|
|
8
|
+
"mandate": "Define endpoints, data models, auth strategy, error handling patterns. Review for consistency and RESTful conventions."
|
|
9
|
+
},
|
|
10
|
+
"dev": {
|
|
11
|
+
"name": "Backend Developer",
|
|
12
|
+
"mandate": "Implement endpoints, write database queries, add validation. Run tests. One endpoint or feature per turn."
|
|
13
|
+
},
|
|
14
|
+
"qa": {
|
|
15
|
+
"name": "API Tester",
|
|
16
|
+
"mandate": "Write and run API tests (integration + edge cases). Test auth, validation, error responses. Report pass/fail with curl examples."
|
|
17
|
+
},
|
|
18
|
+
"docs": {
|
|
19
|
+
"name": "Documentation Writer",
|
|
20
|
+
"mandate": "Write API docs: endpoint reference, request/response examples, auth guide. Keep docs in sync with implementation."
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"rules": {
|
|
24
|
+
"max_consecutive_claims": 2,
|
|
25
|
+
"verify_command": "npm test",
|
|
26
|
+
"ttl_minutes": 10,
|
|
27
|
+
"compress_after_words": 5000
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"label": "Bug Squad",
|
|
3
|
+
"description": "Triage, reproduce, fix, and verify a backlog of bugs",
|
|
4
|
+
"project": "Bug fix sprint",
|
|
5
|
+
"agents": {
|
|
6
|
+
"triage": {
|
|
7
|
+
"name": "Triage Lead",
|
|
8
|
+
"mandate": "Read bug reports, prioritize by severity and user impact. Assign clear reproduction steps and acceptance criteria for each bug."
|
|
9
|
+
},
|
|
10
|
+
"dev": {
|
|
11
|
+
"name": "Developer",
|
|
12
|
+
"mandate": "Fix bugs. Write the smallest correct change. Run tests. One bug per turn. Report what was changed and why."
|
|
13
|
+
},
|
|
14
|
+
"qa": {
|
|
15
|
+
"name": "QA Verifier",
|
|
16
|
+
"mandate": "Verify each fix: reproduce the original bug, confirm it's fixed, check for regressions. Pass or fail with evidence."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"rules": {
|
|
20
|
+
"max_consecutive_claims": 3,
|
|
21
|
+
"verify_command": "npm test",
|
|
22
|
+
"ttl_minutes": 8,
|
|
23
|
+
"compress_after_words": 3000
|
|
24
|
+
}
|
|
25
|
+
}
|