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 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 with a self-polling loop. Agents check `lock.json` every 60 seconds, claim when it's their turn, do their work, release, and go back to waiting. `supervise --autonudge` handles watch + nudging automatically.
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 include a self-polling loop: read `lock.json` → check if it's my turn → claim → work → releasesleep 60s repeat
148
+ 4. Agent prompts are single-turn: claim → work → validatereleasestop
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
- - **Self-polling coordination** — agents check `lock.json` every 60s, no external process needed
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "CLI for AgentXchain — multi-agent coordination in your IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 self-coordinate via lock.json polling (sleep 60s between checks).'));
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) Read:
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
- 2) Ask the human focused product questions until scope is clear:
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
- 3) Update planning docs with concrete acceptance criteria and Get Shit Done structure:
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
- 4) Update .planning/PM_SIGNOFF.md:
184
+ 5) Update .planning/PM_SIGNOFF.md:
178
185
  - Set "Approved: YES" only when human agrees kickoff is complete.
179
- 5) Do NOT start round-robin agent handoffs yet.
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}
@@ -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;
@@ -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/`);
@@ -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. Update \`state.json\` if phase or blocked status changed.`
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. Update \`state.json\` if phase or blocked status changed.`;
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**: Write \`lock.json\` with \`holder="${agentId}"\` and \`claimed_at\` = current time. Re-read to confirm.
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**: Write \`lock.json\`: \`holder=null\`, \`last_released_by="${agentId}"\`, \`turn_number\` = previous + 1, \`claimed_at=null\`.
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. Update state.json if phase or blocked status changed.`
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. Update state.json if phase or blocked status changed.`;
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
- YOUR LOOP (run forever, never exit, never say "I'm done"):
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
- - If holder is NOT null → someone else is working. Run the shell command: sleep 60
100
- Then go back to step 1.
101
- - If holder IS null check last_released_by:
102
- ${isFirstAgent
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
- Write lock.json: {"holder":"${agentId}","last_released_by":<keep previous>,"turn_number":<keep previous>,"claimed_at":"<current ISO timestamp>"}
110
- Then RE-READ lock.json immediately. If holder is not "${agentId}", someone else won. Go to step 1.
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
- Write lock.json: {"holder":null,"last_released_by":"${agentId}","turn_number":<previous + 1>,"claimed_at":null}
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
- - NEVER exit or stop. After releasing, always sleep and poll again. You are a persistent agent.`;
152
+ - This session is SINGLE-TURN. After release, STOP and wait for the referee to wake you again.`;
140
153
  }
@@ -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;