agentxchain 0.8.4 → 0.8.6
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 +30 -3
- package/bin/agentxchain.js +2 -0
- package/package.json +1 -1
- package/src/adapters/cursor-local.js +24 -11
- package/src/commands/claim.js +69 -0
- package/src/commands/init.js +3 -1
- package/src/commands/watch.js +51 -0
- package/src/lib/generate-vscode.js +21 -6
- package/src/lib/seed-prompt-polling.js +33 -20
- package/src/lib/validation.js +26 -0
package/README.md
CHANGED
|
@@ -25,7 +25,9 @@ cd my-project/
|
|
|
25
25
|
agentxchain kickoff
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Each agent runs in its own Cursor window
|
|
28
|
+
Each agent runs in its own Cursor window for a single turn at a time. The referee loop (`watch` / `supervise --autonudge`) determines the next agent and wakes that specific session.
|
|
29
|
+
|
|
30
|
+
Agents are now required to maintain `TALK.md` as the human-readable handoff log each turn.
|
|
29
31
|
|
|
30
32
|
## Commands
|
|
31
33
|
|
|
@@ -46,6 +48,25 @@ Each agent runs in its own Cursor window with a self-polling loop. Agents check
|
|
|
46
48
|
| `config` | View/edit config, add/remove agents, change rules |
|
|
47
49
|
| `update` | Self-update CLI from npm |
|
|
48
50
|
|
|
51
|
+
### Full command list
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
agentxchain init
|
|
55
|
+
agentxchain status
|
|
56
|
+
agentxchain start
|
|
57
|
+
agentxchain kickoff
|
|
58
|
+
agentxchain stop
|
|
59
|
+
agentxchain config
|
|
60
|
+
agentxchain generate
|
|
61
|
+
agentxchain watch
|
|
62
|
+
agentxchain supervise
|
|
63
|
+
agentxchain claim
|
|
64
|
+
agentxchain release
|
|
65
|
+
agentxchain update
|
|
66
|
+
agentxchain doctor
|
|
67
|
+
agentxchain validate
|
|
68
|
+
```
|
|
69
|
+
|
|
49
70
|
### IDE options
|
|
50
71
|
|
|
51
72
|
```bash
|
|
@@ -60,15 +81,21 @@ agentxchain start --ide claude-code # Claude Code — spawns CLI processes
|
|
|
60
81
|
agentxchain kickoff # guided first-run PM-first workflow
|
|
61
82
|
agentxchain kickoff --ide vscode # guided flow for VS Code mode
|
|
62
83
|
agentxchain kickoff --send # with Cursor auto-nudge auto-send enabled
|
|
84
|
+
agentxchain kickoff --interval 2 # nudge poll interval override
|
|
85
|
+
agentxchain kickoff --no-autonudge # skip auto-nudge prompt
|
|
63
86
|
|
|
64
87
|
agentxchain start --agent pm # launch only one specific agent
|
|
65
88
|
agentxchain start --remaining # launch all agents except PM (PM-first flow)
|
|
66
89
|
agentxchain start --dry-run # preview agents without launching
|
|
67
90
|
agentxchain validate --mode kickoff # required before --remaining
|
|
68
91
|
agentxchain validate --mode turn --agent pm
|
|
92
|
+
agentxchain validate --json # machine-readable validation output
|
|
69
93
|
agentxchain watch --daemon # run watch in background
|
|
70
94
|
agentxchain supervise --autonudge # run watch + AppleScript nudge loop
|
|
71
95
|
agentxchain supervise --autonudge --send # auto-press Enter after paste
|
|
96
|
+
agentxchain supervise --interval 2 # set auto-nudge poll interval
|
|
97
|
+
agentxchain claim --agent pm # guarded claim as agent turn owner
|
|
98
|
+
agentxchain release --agent pm # guarded release as agent turn owner
|
|
72
99
|
agentxchain release --force # force-release non-human holder lock
|
|
73
100
|
```
|
|
74
101
|
|
|
@@ -118,7 +145,7 @@ Notes:
|
|
|
118
145
|
1. `agentxchain kickoff` launches PM first for human-product alignment
|
|
119
146
|
2. Each window gets a unique prompt copied to clipboard
|
|
120
147
|
3. Kickoff validates PM signoff and launches remaining agents
|
|
121
|
-
4. Agent prompts
|
|
148
|
+
4. Agent prompts are single-turn: claim → work → validate → release → stop
|
|
122
149
|
5. Agents know their rotation order from `agentxchain.json` and only claim when the previous agent released
|
|
123
150
|
6. Human can `claim` to pause and `release` to resume anytime
|
|
124
151
|
|
|
@@ -139,7 +166,7 @@ Agents follow a round-robin order defined in `agentxchain.json`:
|
|
|
139
166
|
## Key features
|
|
140
167
|
|
|
141
168
|
- **One window per agent** — each agent has its own Cursor window and chat session
|
|
142
|
-
- **
|
|
169
|
+
- **Referee-driven coordination** — `watch`/`supervise` wakes the next correct agent each turn
|
|
143
170
|
- **Works in Cursor, VS Code, Claude Code** — adapters for each IDE
|
|
144
171
|
- **User-defined teams** — any number of agents, any roles
|
|
145
172
|
- **No API keys or cloud required** — everything runs locally
|
package/bin/agentxchain.js
CHANGED
|
@@ -94,12 +94,14 @@ program
|
|
|
94
94
|
program
|
|
95
95
|
.command('claim')
|
|
96
96
|
.description('Claim the lock as a human (take control)')
|
|
97
|
+
.option('--agent <id>', 'Claim lock as a specific agent (guarded by turn order)')
|
|
97
98
|
.option('--force', 'Force-claim even if an agent holds the lock')
|
|
98
99
|
.action(claimCommand);
|
|
99
100
|
|
|
100
101
|
program
|
|
101
102
|
.command('release')
|
|
102
103
|
.description('Release the lock (hand back to agents)')
|
|
104
|
+
.option('--agent <id>', 'Release lock as a specific agent')
|
|
103
105
|
.option('--force', 'Force release even if a non-human holder has the lock')
|
|
104
106
|
.action(releaseCommand);
|
|
105
107
|
|
package/package.json
CHANGED
|
@@ -24,8 +24,8 @@ export async function launchCursorLocal(config, root, opts) {
|
|
|
24
24
|
// Save all prompts first
|
|
25
25
|
for (const [id, agent] of agentEntries) {
|
|
26
26
|
const prompt = isPmKickoff
|
|
27
|
-
? generateKickoffPrompt(id, agent, config)
|
|
28
|
-
: generatePollingPrompt(id, agent, config);
|
|
27
|
+
? generateKickoffPrompt(id, agent, config, root)
|
|
28
|
+
: generatePollingPrompt(id, agent, config, root);
|
|
29
29
|
writeFileSync(join(promptDir, `${id}.prompt.md`), prompt);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -36,8 +36,8 @@ export async function launchCursorLocal(config, root, opts) {
|
|
|
36
36
|
for (let i = 0; i < agentEntries.length; i++) {
|
|
37
37
|
const [id, agent] = agentEntries[i];
|
|
38
38
|
const prompt = isPmKickoff
|
|
39
|
-
? generateKickoffPrompt(id, agent, config)
|
|
40
|
-
: generatePollingPrompt(id, agent, config);
|
|
39
|
+
? generateKickoffPrompt(id, agent, config, root)
|
|
40
|
+
: generatePollingPrompt(id, agent, config, root);
|
|
41
41
|
|
|
42
42
|
// Create symlink: .agentxchain-workspaces/<id> -> project root
|
|
43
43
|
const agentWorkspace = join(workspacesDir, id);
|
|
@@ -107,7 +107,7 @@ export async function launchCursorLocal(config, root, opts) {
|
|
|
107
107
|
console.log(` ${chalk.bold('agentxchain watch')} ${chalk.dim('# watcher / trigger writer')}`);
|
|
108
108
|
console.log(` ${chalk.bold('agentxchain doctor')} ${chalk.dim('# check local setup + trigger health')}`);
|
|
109
109
|
console.log('');
|
|
110
|
-
console.log(chalk.dim(' Agents
|
|
110
|
+
console.log(chalk.dim(' Agents run single turns. Watch/supervise wakes the correct next agent.'));
|
|
111
111
|
console.log(chalk.dim(' Re-paste a prompt: cat .agentxchain-prompts/<agent>.prompt.md | pbcopy'));
|
|
112
112
|
console.log('');
|
|
113
113
|
}
|
|
@@ -153,30 +153,43 @@ function isPmLike(agentId, agentDef) {
|
|
|
153
153
|
return name.includes('product manager');
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
function generateKickoffPrompt(agentId, agentDef, config) {
|
|
156
|
+
function generateKickoffPrompt(agentId, agentDef, config, projectRoot) {
|
|
157
157
|
return `You are "${agentId}" — ${agentDef.name}.
|
|
158
158
|
|
|
159
159
|
This is PM kickoff mode. Your job now is to collaborate with the human and finalize scope before autonomous turns begin.
|
|
160
160
|
|
|
161
|
+
Project root (strict boundary): "${projectRoot}"
|
|
162
|
+
Work only inside this project folder. Do NOT scan unrelated local directories.
|
|
163
|
+
|
|
161
164
|
Actions:
|
|
162
|
-
1)
|
|
165
|
+
1) FIRST QUESTION TO HUMAN (mandatory before anything else):
|
|
166
|
+
"Please describe your product idea in one paragraph, in as much detail as possible."
|
|
167
|
+
|
|
168
|
+
2) Read:
|
|
163
169
|
- .planning/PROJECT.md
|
|
164
170
|
- .planning/REQUIREMENTS.md
|
|
165
171
|
- .planning/ROADMAP.md
|
|
172
|
+
- TALK.md
|
|
166
173
|
- state.md
|
|
167
174
|
- lock.json
|
|
168
|
-
|
|
175
|
+
3) After receiving that paragraph, summarize it in TALK.md, then ask focused follow-up questions:
|
|
169
176
|
- target user
|
|
170
177
|
- top pain point
|
|
171
178
|
- core workflow
|
|
172
179
|
- MVP boundary
|
|
173
180
|
- success metric
|
|
174
|
-
|
|
181
|
+
4) Update planning docs with concrete acceptance criteria and Get Shit Done structure:
|
|
175
182
|
- .planning/ROADMAP.md must define Waves and Phases.
|
|
176
183
|
- Create .planning/phases/phase-1/PLAN.md and TESTS.md.
|
|
177
|
-
|
|
184
|
+
5) Update .planning/PM_SIGNOFF.md:
|
|
178
185
|
- Set "Approved: YES" only when human agrees kickoff is complete.
|
|
179
|
-
|
|
186
|
+
6) Append kickoff summary to TALK.md with:
|
|
187
|
+
- Status
|
|
188
|
+
- Decision
|
|
189
|
+
- Action
|
|
190
|
+
- Risks/Questions
|
|
191
|
+
- Next owner
|
|
192
|
+
7) Do NOT start round-robin agent handoffs yet.
|
|
180
193
|
|
|
181
194
|
Context:
|
|
182
195
|
- Project: ${config.project}
|
package/src/commands/claim.js
CHANGED
|
@@ -11,6 +11,10 @@ export async function claimCommand(opts) {
|
|
|
11
11
|
const lock = loadLock(root);
|
|
12
12
|
if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
|
|
13
13
|
|
|
14
|
+
if (opts.agent) {
|
|
15
|
+
return claimAsAgent({ opts, root, config, lock });
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
if (lock.holder === 'human') {
|
|
15
19
|
console.log('');
|
|
16
20
|
console.log(chalk.yellow(' You already hold the lock.'));
|
|
@@ -52,6 +56,10 @@ export async function releaseCommand(opts) {
|
|
|
52
56
|
const lock = loadLock(root);
|
|
53
57
|
if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
|
|
54
58
|
|
|
59
|
+
if (opts.agent) {
|
|
60
|
+
return releaseAsAgent({ opts, root, config, lock });
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
if (!lock.holder) {
|
|
56
64
|
console.log(chalk.yellow(' Lock is already free.'));
|
|
57
65
|
return;
|
|
@@ -85,6 +93,67 @@ export async function releaseCommand(opts) {
|
|
|
85
93
|
console.log('');
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
function claimAsAgent({ opts, root, config, lock }) {
|
|
97
|
+
const agentId = opts.agent;
|
|
98
|
+
if (!config.agents?.[agentId]) {
|
|
99
|
+
console.log(chalk.red(` Agent "${agentId}" is not defined in agentxchain.json.`));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (lock.holder && !opts.force) {
|
|
104
|
+
console.log(chalk.red(` Lock is currently held by "${lock.holder}".`));
|
|
105
|
+
console.log(chalk.dim(' Use --force only for recovery scenarios.'));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const expected = pickNextAgent(lock, config);
|
|
110
|
+
if (!opts.force && expected && expected !== agentId) {
|
|
111
|
+
console.log(chalk.red(` Out-of-turn claim blocked. Expected: ${expected}, got: ${agentId}.`));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lockPath = join(root, LOCK_FILE);
|
|
116
|
+
const next = {
|
|
117
|
+
holder: agentId,
|
|
118
|
+
last_released_by: lock.last_released_by,
|
|
119
|
+
turn_number: lock.turn_number,
|
|
120
|
+
claimed_at: new Date().toISOString()
|
|
121
|
+
};
|
|
122
|
+
writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
|
|
123
|
+
console.log(chalk.green(` ✓ Lock claimed by ${agentId} (turn ${next.turn_number})`));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function releaseAsAgent({ opts, root, config, lock }) {
|
|
127
|
+
const agentId = opts.agent;
|
|
128
|
+
if (!config.agents?.[agentId]) {
|
|
129
|
+
console.log(chalk.red(` Agent "${agentId}" is not defined in agentxchain.json.`));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (lock.holder !== agentId && !opts.force) {
|
|
133
|
+
console.log(chalk.red(` Lock is held by "${lock.holder}", not "${agentId}".`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lockPath = join(root, LOCK_FILE);
|
|
138
|
+
const next = {
|
|
139
|
+
holder: null,
|
|
140
|
+
last_released_by: agentId,
|
|
141
|
+
turn_number: lock.turn_number + 1,
|
|
142
|
+
claimed_at: null
|
|
143
|
+
};
|
|
144
|
+
writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
|
|
145
|
+
console.log(chalk.green(` ✓ Lock released by ${agentId} (turn ${next.turn_number})`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pickNextAgent(lock, config) {
|
|
149
|
+
const agentIds = Object.keys(config.agents || {});
|
|
150
|
+
if (agentIds.length === 0) return null;
|
|
151
|
+
const lastAgent = lock.last_released_by;
|
|
152
|
+
if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
|
|
153
|
+
const lastIndex = agentIds.indexOf(lastAgent);
|
|
154
|
+
return agentIds[(lastIndex + 1) % agentIds.length];
|
|
155
|
+
}
|
|
156
|
+
|
|
88
157
|
function clearBlockedState(root) {
|
|
89
158
|
const statePath = join(root, 'state.json');
|
|
90
159
|
if (!existsSync(statePath)) return;
|
package/src/commands/init.js
CHANGED
|
@@ -183,6 +183,7 @@ export async function initCommand(opts) {
|
|
|
183
183
|
project,
|
|
184
184
|
agents,
|
|
185
185
|
log: 'log.md',
|
|
186
|
+
talk_file: 'TALK.md',
|
|
186
187
|
state_file: 'state.md',
|
|
187
188
|
history_file: 'history.jsonl',
|
|
188
189
|
rules: {
|
|
@@ -205,6 +206,7 @@ export async function initCommand(opts) {
|
|
|
205
206
|
writeFileSync(join(dir, 'state.md'), `# ${project} — Current State\n\n## Architecture\n\n(Agents update this each turn with current decisions.)\n\n## Active Work\n\n(What's in progress right now.)\n\n## Open Issues\n\n(Bugs, blockers, risks.)\n\n## Next Steps\n\n(What should happen next.)\n`);
|
|
206
207
|
writeFileSync(join(dir, 'history.jsonl'), '');
|
|
207
208
|
writeFileSync(join(dir, 'log.md'), `# ${project} — Agent Log\n\n## COMPRESSED CONTEXT\n\n(No compressed context yet.)\n\n## MESSAGE LOG\n\n(Agents append messages below this line.)\n`);
|
|
209
|
+
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`);
|
|
208
210
|
writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
|
|
209
211
|
const gitignorePath = join(dir, '.gitignore');
|
|
210
212
|
const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/'];
|
|
@@ -253,7 +255,7 @@ export async function initCommand(opts) {
|
|
|
253
255
|
console.log(` ${chalk.dim('├──')} agentxchain.json ${chalk.dim(`(${agentCount} agents)`)}`);
|
|
254
256
|
console.log(` ${chalk.dim('├──')} lock.json`);
|
|
255
257
|
console.log(` ${chalk.dim('├──')} state.json / state.md / history.jsonl`);
|
|
256
|
-
console.log(` ${chalk.dim('├──')} log.md / HUMAN_TASKS.md`);
|
|
258
|
+
console.log(` ${chalk.dim('├──')} TALK.md / log.md / HUMAN_TASKS.md`);
|
|
257
259
|
console.log(` ${chalk.dim('├──')} .planning/`);
|
|
258
260
|
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} PROJECT.md / REQUIREMENTS.md / ROADMAP.md / PM_SIGNOFF.md`);
|
|
259
261
|
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} research/ / phases/`);
|
package/src/commands/watch.js
CHANGED
|
@@ -50,6 +50,17 @@ export async function watchCommand(opts) {
|
|
|
50
50
|
|
|
51
51
|
const stateKey = `${lock.holder}:${lock.turn_number}`;
|
|
52
52
|
|
|
53
|
+
if (lock.holder && lock.holder !== 'human') {
|
|
54
|
+
const expected = pickNextAgent(lock, config);
|
|
55
|
+
if (!isValidClaimer(lock, config)) {
|
|
56
|
+
log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected}. Handing lock to HUMAN.`);
|
|
57
|
+
blockOnIllegalClaim(root, lock, config, expected);
|
|
58
|
+
sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
|
|
59
|
+
lastState = null;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
if (lock.holder && lock.holder !== 'human' && lock.claimed_at) {
|
|
54
65
|
const elapsed = Date.now() - new Date(lock.claimed_at).getTime();
|
|
55
66
|
const ttlMs = ttlMinutes * 60 * 1000;
|
|
@@ -129,6 +140,13 @@ function pickNextAgent(lock, config) {
|
|
|
129
140
|
return agentIds[(lastIndex + 1) % agentIds.length];
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
function isValidClaimer(lock, config) {
|
|
144
|
+
if (!lock.holder || lock.holder === 'human') return true;
|
|
145
|
+
if (!config.agents?.[lock.holder]) return false;
|
|
146
|
+
const expected = pickNextAgent(lock, config);
|
|
147
|
+
return lock.holder === expected;
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
function forceRelease(root, lock, staleAgent, config) {
|
|
133
151
|
const lockPath = join(root, LOCK_FILE);
|
|
134
152
|
const newLock = {
|
|
@@ -192,6 +210,39 @@ function blockOnValidation(root, lock, config, validation) {
|
|
|
192
210
|
}
|
|
193
211
|
}
|
|
194
212
|
|
|
213
|
+
function blockOnIllegalClaim(root, lock, config, expected) {
|
|
214
|
+
const lockPath = join(root, LOCK_FILE);
|
|
215
|
+
const newLock = {
|
|
216
|
+
holder: 'human',
|
|
217
|
+
last_released_by: lock.last_released_by,
|
|
218
|
+
turn_number: lock.turn_number,
|
|
219
|
+
claimed_at: new Date().toISOString()
|
|
220
|
+
};
|
|
221
|
+
writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
|
|
222
|
+
|
|
223
|
+
const statePath = join(root, 'state.json');
|
|
224
|
+
if (existsSync(statePath)) {
|
|
225
|
+
try {
|
|
226
|
+
const current = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
227
|
+
const nextState = {
|
|
228
|
+
...current,
|
|
229
|
+
blocked: true,
|
|
230
|
+
blocked_on: `illegal-claim: expected ${expected}, got ${lock.holder}`
|
|
231
|
+
};
|
|
232
|
+
writeFileSync(statePath, JSON.stringify(nextState, null, 2) + '\n');
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const logFile = config.log || 'log.md';
|
|
237
|
+
const logPath = join(root, logFile);
|
|
238
|
+
if (existsSync(logPath)) {
|
|
239
|
+
appendFileSync(
|
|
240
|
+
logPath,
|
|
241
|
+
`\n---\n\n### [system] (Watch Guard) | Turn ${lock.turn_number}\n\n**Status:** Illegal out-of-turn lock claim detected.\n\n**Action:** Lock assigned to human for intervention.\n\n**Details:** expected \`${expected}\`, got \`${lock.holder}\`.\n\n`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
195
246
|
function log(type, msg) {
|
|
196
247
|
const time = new Date().toLocaleTimeString();
|
|
197
248
|
const tags = {
|
|
@@ -15,7 +15,7 @@ export function generateVSCodeFiles(dir, config) {
|
|
|
15
15
|
|
|
16
16
|
for (const id of agentIds) {
|
|
17
17
|
const agent = config.agents[id];
|
|
18
|
-
const md = buildAgentMd(id, agent, config, agentIds);
|
|
18
|
+
const md = buildAgentMd(id, agent, config, agentIds, dir);
|
|
19
19
|
writeFileSync(join(agentsDir, `${id}.agent.md`), md);
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -33,13 +33,14 @@ export function generateVSCodeFiles(dir, config) {
|
|
|
33
33
|
return { agentsDir, hooksDir, scriptsDir, agentCount: agentIds.length };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function buildAgentMd(agentId, agentDef, config, allAgentIds) {
|
|
36
|
+
function buildAgentMd(agentId, agentDef, config, allAgentIds, projectRoot) {
|
|
37
37
|
const otherAgents = allAgentIds.filter(id => id !== agentId);
|
|
38
38
|
const verifyCmd = config.rules?.verify_command || null;
|
|
39
39
|
const maxClaims = config.rules?.max_consecutive_claims || 2;
|
|
40
40
|
const stateFile = config.state_file || 'state.md';
|
|
41
41
|
const historyFile = config.history_file || 'history.jsonl';
|
|
42
42
|
const logFile = config.log || 'log.md';
|
|
43
|
+
const talkFile = config.talk_file || 'TALK.md';
|
|
43
44
|
const useSplit = config.state_file || config.history_file;
|
|
44
45
|
|
|
45
46
|
const handoffs = otherAgents.map(otherId => {
|
|
@@ -77,10 +78,12 @@ hooks:
|
|
|
77
78
|
? `Read these files at the start of your turn:
|
|
78
79
|
- \`${stateFile}\` — living project state (primary context)
|
|
79
80
|
- \`${historyFile}\` — last 3 lines for recent turns
|
|
81
|
+
- \`${talkFile}\` — team handoff updates (read latest entries)
|
|
80
82
|
- \`lock.json\` — current lock holder
|
|
81
83
|
- \`state.json\` — phase and blocked status`
|
|
82
84
|
: `Read these files at the start of your turn:
|
|
83
85
|
- \`${logFile}\` — message log (read last few messages)
|
|
86
|
+
- \`${talkFile}\` — team handoff updates (read latest entries)
|
|
84
87
|
- \`lock.json\` — current lock holder
|
|
85
88
|
- \`state.json\` — phase and blocked status`;
|
|
86
89
|
|
|
@@ -90,13 +93,15 @@ hooks:
|
|
|
90
93
|
2. Overwrite \`${stateFile}\` with current project state.
|
|
91
94
|
3. Append one line to \`${historyFile}\`:
|
|
92
95
|
\`{"turn": N, "agent": "${agentId}", "summary": "...", "files_changed": [...], "verify_result": "pass|fail|skipped", "timestamp": "ISO8601"}\`
|
|
93
|
-
4.
|
|
96
|
+
4. Append one handoff entry to \`${talkFile}\` with: Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
97
|
+
5. Update \`state.json\` if phase or blocked status changed.`
|
|
94
98
|
: `When you finish your work, write in this order:
|
|
95
99
|
1. Your actual work: code, files, commands, decisions.
|
|
96
100
|
2. Append one message to \`${logFile}\`:
|
|
97
101
|
\`### [${agentId}] (${agentDef.name}) | Turn N\`
|
|
98
102
|
with Status, Decision, Action, Next sections.
|
|
99
|
-
3.
|
|
103
|
+
3. Append one handoff entry to \`${talkFile}\` with: Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
104
|
+
4. Update \`state.json\` if phase or blocked status changed.`;
|
|
100
105
|
|
|
101
106
|
const verifyInstructions = verifyCmd
|
|
102
107
|
? `\n## Verify before release\nBefore releasing the lock, run: \`${verifyCmd}\`\nIf it fails, fix the problem and run again. Do NOT release with a failing verification.`
|
|
@@ -110,6 +115,15 @@ ${agentDef.mandate}
|
|
|
110
115
|
|
|
111
116
|
---
|
|
112
117
|
|
|
118
|
+
## Project boundary
|
|
119
|
+
|
|
120
|
+
- Project root: \`${projectRoot}\`
|
|
121
|
+
- Work only inside this project folder.
|
|
122
|
+
- Never scan unrelated local directories.
|
|
123
|
+
- Start your turn by checking \`pwd\`.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
113
127
|
## Project documentation
|
|
114
128
|
|
|
115
129
|
Read the files relevant to your role in the \`.planning/\` folder:
|
|
@@ -128,12 +142,13 @@ Create or update these files when your role requires it.
|
|
|
128
142
|
|
|
129
143
|
The AgentXchain system coordinates turns. When prompted, do this:
|
|
130
144
|
|
|
131
|
-
1. **CLAIM**:
|
|
145
|
+
1. **CLAIM**: Run \`agentxchain claim --agent ${agentId}\`. If blocked, stop.
|
|
132
146
|
2. **READ**: ${readInstructions}
|
|
133
147
|
3. **THINK**: What did the previous agent do? What is most important for YOUR role? What is one risk?
|
|
134
148
|
4. **WORK**: ${writeInstructions}${verifyInstructions}
|
|
135
|
-
5. **RELEASE**:
|
|
149
|
+
5. **RELEASE**: Run \`agentxchain release --agent ${agentId}\`.
|
|
136
150
|
This MUST be the last thing you write.
|
|
151
|
+
6. **STOP**: End your turn. The referee will wake the next agent.
|
|
137
152
|
|
|
138
153
|
---
|
|
139
154
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export function generatePollingPrompt(agentId, agentDef, config) {
|
|
1
|
+
export function generatePollingPrompt(agentId, agentDef, config, projectRoot = '.') {
|
|
2
2
|
const logFile = config.log || 'log.md';
|
|
3
|
+
const talkFile = config.talk_file || 'TALK.md';
|
|
3
4
|
const maxClaims = config.rules?.max_consecutive_claims || 2;
|
|
4
5
|
const verifyCmd = config.rules?.verify_command || null;
|
|
5
6
|
const stateFile = config.state_file || 'state.md';
|
|
@@ -19,10 +20,12 @@ export function generatePollingPrompt(agentId, agentDef, config) {
|
|
|
19
20
|
? `READ THESE FILES:
|
|
20
21
|
- "${stateFile}" — the living project state. Read fully. Primary context.
|
|
21
22
|
- "${historyFile}" — turn history. Read last 3 lines for recent context.
|
|
23
|
+
- "${talkFile}" — team handoff updates. Read the latest 5 entries.
|
|
22
24
|
- lock.json — who holds the lock.
|
|
23
25
|
- state.json — phase and blocked status.`
|
|
24
26
|
: `READ THESE FILES:
|
|
25
27
|
- "${logFile}" — the message log. Read last few messages.
|
|
28
|
+
- "${talkFile}" — team handoff updates. Read the latest 5 entries.
|
|
26
29
|
- lock.json — who holds the lock.
|
|
27
30
|
- state.json — phase and blocked status.`;
|
|
28
31
|
|
|
@@ -32,7 +35,9 @@ a. Do your actual work: write code, create files, run commands, make decisions.
|
|
|
32
35
|
b. Update "${stateFile}" — OVERWRITE with current project state.
|
|
33
36
|
c. Append ONE line to "${historyFile}":
|
|
34
37
|
{"turn": N, "agent": "${agentId}", "summary": "what you did", "files_changed": [...], "verify_result": "pass|fail|skipped", "timestamp": "ISO8601"}
|
|
35
|
-
d.
|
|
38
|
+
d. Append ONE handoff entry to "${talkFile}" with:
|
|
39
|
+
Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
40
|
+
e. Update state.json if phase or blocked status changed.`
|
|
36
41
|
: `WRITE (in this order):
|
|
37
42
|
a. Do your actual work: write code, create files, run commands, make decisions.
|
|
38
43
|
b. Append ONE message to ${logFile}:
|
|
@@ -42,7 +47,9 @@ b. Append ONE message to ${logFile}:
|
|
|
42
47
|
**Decision:** What you decided and why.
|
|
43
48
|
**Action:** What you did. Commands, files, results.
|
|
44
49
|
**Next:** What the next agent should focus on.
|
|
45
|
-
c.
|
|
50
|
+
c. Append ONE handoff entry to "${talkFile}" with:
|
|
51
|
+
Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
52
|
+
d. Update state.json if phase or blocked status changed.`;
|
|
46
53
|
|
|
47
54
|
const verifySection = verifyCmd
|
|
48
55
|
? `
|
|
@@ -58,6 +65,15 @@ ${agentDef.mandate}
|
|
|
58
65
|
|
|
59
66
|
---
|
|
60
67
|
|
|
68
|
+
PROJECT ROOT (strict boundary):
|
|
69
|
+
- Absolute project root: "${projectRoot}"
|
|
70
|
+
- You MUST work only inside this project root.
|
|
71
|
+
- Do NOT scan your home directory or unrelated folders.
|
|
72
|
+
- If unsure, run: pwd
|
|
73
|
+
- If not in project root, run: cd "${projectRoot}"
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
61
77
|
PROJECT DOCUMENTATION (.planning/ folder):
|
|
62
78
|
|
|
63
79
|
These files give you project context. Read the ones relevant to your role.
|
|
@@ -91,23 +107,24 @@ ${turnCondition}
|
|
|
91
107
|
|
|
92
108
|
---
|
|
93
109
|
|
|
94
|
-
|
|
110
|
+
TURN MODE (single turn only, referee wakes you again later):
|
|
111
|
+
|
|
112
|
+
0. CHECK WORKING DIRECTORY:
|
|
113
|
+
- Run: pwd
|
|
114
|
+
- If not inside "${projectRoot}", run: cd "${projectRoot}"
|
|
115
|
+
- Never run broad searches outside this project root.
|
|
95
116
|
|
|
96
117
|
1. READ lock.json.
|
|
97
118
|
|
|
98
119
|
2. CHECK — is it my turn?
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
? `- If last_released_by is null, "human", starts with "system", or "${agentIds[agentIds.length - 1]}" → IT IS YOUR TURN. Go to step 3.`
|
|
104
|
-
: `- If last_released_by is "${prevAgent}" → IT IS YOUR TURN. Go to step 3.`}
|
|
105
|
-
- Otherwise → it is another agent's turn. Run the shell command: sleep 60
|
|
106
|
-
Then go back to step 1.
|
|
120
|
+
${isFirstAgent
|
|
121
|
+
? `- It is your turn only when lock holder is null and last_released_by is null/human/system/${agentIds[agentIds.length - 1]}.`
|
|
122
|
+
: `- It is your turn only when lock holder is null and last_released_by is "${prevAgent}".`}
|
|
123
|
+
- If NOT your turn: STOP. Do not claim lock and do not write files.
|
|
107
124
|
|
|
108
125
|
3. CLAIM the lock:
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
Run: agentxchain claim --agent ${agentId}
|
|
127
|
+
If claim is blocked, STOP.
|
|
111
128
|
|
|
112
129
|
4. DO YOUR WORK:
|
|
113
130
|
${readSection}
|
|
@@ -121,20 +138,16 @@ YOUR LOOP (run forever, never exit, never say "I'm done"):
|
|
|
121
138
|
If validation fails, fix docs/artifacts first. Do NOT release.
|
|
122
139
|
|
|
123
140
|
5. RELEASE the lock:
|
|
124
|
-
|
|
141
|
+
Run: agentxchain release --agent ${agentId}
|
|
125
142
|
THIS MUST BE THE LAST FILE YOU WRITE.
|
|
126
143
|
|
|
127
|
-
6. Run the shell command: sleep 60
|
|
128
|
-
Then go back to step 1.
|
|
129
|
-
|
|
130
144
|
---
|
|
131
145
|
|
|
132
146
|
CRITICAL RULES:
|
|
133
|
-
- ACTUALLY RUN "sleep 60" in the terminal between checks. Do NOT skip this. Do NOT just say "waiting."
|
|
134
147
|
- Never write files or code without holding the lock. Reading is always allowed.
|
|
135
148
|
- One git commit per turn: "Turn N - ${agentId} - description"
|
|
136
149
|
- Max ${maxClaims} consecutive turns. If you have held the lock ${maxClaims} times in a row, do a short turn and release.
|
|
137
150
|
- ALWAYS release the lock. A stuck lock blocks the entire team.
|
|
138
151
|
- ALWAYS find at least one problem, risk, or question about the previous work. Blind agreement is forbidden.
|
|
139
|
-
-
|
|
152
|
+
- This session is SINGLE-TURN. After release, STOP and wait for the referee to wake you again.`;
|
|
140
153
|
}
|
package/src/lib/validation.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
export function validateProject(root, config, opts = {}) {
|
|
5
5
|
const mode = opts.mode || 'full';
|
|
6
6
|
const expectedAgent = opts.expectedAgent || null;
|
|
7
|
+
const talkFile = config.talk_file || 'TALK.md';
|
|
7
8
|
|
|
8
9
|
const errors = [];
|
|
9
10
|
const warnings = [];
|
|
@@ -18,6 +19,7 @@ export function validateProject(root, config, opts = {}) {
|
|
|
18
19
|
'.planning/qa/UX-AUDIT.md',
|
|
19
20
|
'.planning/qa/ACCEPTANCE-MATRIX.md',
|
|
20
21
|
'.planning/qa/REGRESSION-LOG.md',
|
|
22
|
+
talkFile,
|
|
21
23
|
'state.md',
|
|
22
24
|
'history.jsonl',
|
|
23
25
|
'lock.json',
|
|
@@ -56,6 +58,10 @@ export function validateProject(root, config, opts = {}) {
|
|
|
56
58
|
errors.push(...history.errors);
|
|
57
59
|
warnings.push(...history.warnings);
|
|
58
60
|
|
|
61
|
+
const talk = validateTalkFile(root, talkFile, { requireEntry: mode !== 'kickoff' });
|
|
62
|
+
errors.push(...talk.errors);
|
|
63
|
+
warnings.push(...talk.warnings);
|
|
64
|
+
|
|
59
65
|
const qaSignals = validateQaArtifacts(root);
|
|
60
66
|
warnings.push(...qaSignals.warnings);
|
|
61
67
|
|
|
@@ -160,6 +166,26 @@ function validateQaArtifacts(root) {
|
|
|
160
166
|
return result;
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
function validateTalkFile(root, talkFile, opts) {
|
|
170
|
+
const result = { errors: [], warnings: [] };
|
|
171
|
+
const text = readText(root, talkFile);
|
|
172
|
+
if (!text) {
|
|
173
|
+
result.errors.push(`${talkFile} is missing or unreadable.`);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const hasTurnEntry = /##\s*Turn\s+\d+/i.test(text);
|
|
178
|
+
if (!hasTurnEntry) {
|
|
179
|
+
if (opts.requireEntry) {
|
|
180
|
+
result.errors.push(`${talkFile} has no turn entries.`);
|
|
181
|
+
} else {
|
|
182
|
+
result.warnings.push(`${talkFile} has no turn entries yet.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
163
189
|
function readText(root, rel) {
|
|
164
190
|
const path = join(root, rel);
|
|
165
191
|
if (!existsSync(path)) return null;
|