agentxchain 0.8.6 → 0.8.7

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
@@ -16,12 +16,21 @@ npx agentxchain init
16
16
 
17
17
  ## Quick start
18
18
 
19
+ ### Happy path: net-new project
20
+
19
21
  ```bash
20
- # 1. Create a project (interactive template selection)
21
22
  agentxchain init
23
+ cd my-project
24
+ agentxchain kickoff
25
+ ```
26
+
27
+ ### Happy path: existing project
22
28
 
23
- # 2. Run guided PM-first kickoff wizard
24
- cd my-project/
29
+ Run these commands from inside your existing project folder:
30
+
31
+ ```bash
32
+ agentxchain doctor
33
+ agentxchain generate
25
34
  agentxchain kickoff
26
35
  ```
27
36
 
@@ -46,6 +55,7 @@ Agents are now required to maintain `TALK.md` as the human-readable handoff log
46
55
  | `stop` | Terminate running Claude Code agent sessions |
47
56
  | `watch` | Optional: TTL safety net + status logging |
48
57
  | `config` | View/edit config, add/remove agents, change rules |
58
+ | `rebind` | Rebuild Cursor workspace/prompt bindings for agents |
49
59
  | `update` | Self-update CLI from npm |
50
60
 
51
61
  ### Full command list
@@ -57,6 +67,7 @@ agentxchain start
57
67
  agentxchain kickoff
58
68
  agentxchain stop
59
69
  agentxchain config
70
+ agentxchain rebind
60
71
  agentxchain generate
61
72
  agentxchain watch
62
73
  agentxchain supervise
@@ -94,6 +105,9 @@ agentxchain watch --daemon # run watch in background
94
105
  agentxchain supervise --autonudge # run watch + AppleScript nudge loop
95
106
  agentxchain supervise --autonudge --send # auto-press Enter after paste
96
107
  agentxchain supervise --interval 2 # set auto-nudge poll interval
108
+ agentxchain rebind # regenerate agent prompt/workspace bindings
109
+ agentxchain rebind --open # regenerate and reopen all Cursor agent windows
110
+ agentxchain rebind --agent pm # regenerate one agent binding only
97
111
  agentxchain claim --agent pm # guarded claim as agent turn owner
98
112
  agentxchain release --agent pm # guarded release as agent turn owner
99
113
  agentxchain release --force # force-release non-human holder lock
@@ -146,7 +160,7 @@ Notes:
146
160
  2. Each window gets a unique prompt copied to clipboard
147
161
  3. Kickoff validates PM signoff and launches remaining agents
148
162
  4. Agent prompts are single-turn: claim → work → validate → release → stop
149
- 5. Agents know their rotation order from `agentxchain.json` and only claim when the previous agent released
163
+ 5. Agents use the latest `Next owner:` in `TALK.md` to pick who goes next (fallback: config order)
150
164
  6. Human can `claim` to pause and `release` to resume anytime
151
165
 
152
166
  ### VS Code mode
@@ -155,13 +169,12 @@ Notes:
155
169
  2. VS Code auto-discovers agents in the Chat dropdown
156
170
  3. The `Stop` hook acts as referee — hands off to next agent automatically
157
171
 
158
- ### Turn rotation
172
+ ### Turn ownership
159
173
 
160
- Agents follow a round-robin order defined in `agentxchain.json`:
161
- - PM waits for: lock free + last released by `human`, `null`, or last agent in rotation
162
- - Dev waits for: lock free + last released by `pm`
163
- - QA waits for: lock free + last released by `dev`
164
- - And so on...
174
+ Agent turns are handoff-driven:
175
+ - Each turn appends a `Next owner:` in `TALK.md` with a valid agent id
176
+ - `watch`/`supervise` dispatches the next trigger from that handoff
177
+ - `claim --agent <id>` enforces that expected owner (with guarded fallback)
165
178
 
166
179
  ## Key features
167
180
 
@@ -17,6 +17,7 @@ import { doctorCommand } from '../src/commands/doctor.js';
17
17
  import { superviseCommand } from '../src/commands/supervise.js';
18
18
  import { validateCommand } from '../src/commands/validate.js';
19
19
  import { kickoffCommand } from '../src/commands/kickoff.js';
20
+ import { rebindCommand } from '../src/commands/rebind.js';
20
21
 
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
23
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -91,6 +92,13 @@ program
91
92
  .option('--interval <seconds>', 'Auto-nudge poll interval in seconds', '3')
92
93
  .action(superviseCommand);
93
94
 
95
+ program
96
+ .command('rebind')
97
+ .description('Rebuild Cursor prompt/workspace bindings for agents')
98
+ .option('--agent <id>', 'Rebind a single agent only')
99
+ .option('--open', 'Reopen Cursor windows after rebinding')
100
+ .action(rebindCommand);
101
+
94
102
  program
95
103
  .command('claim')
96
104
  .description('Claim the lock as a human (take control)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "CLI for AgentXchain — multi-agent coordination in your IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  property projectRoot : ""
2
2
  property pollSeconds : 3
3
3
  property autoSend : false
4
+ property lastFailedDispatch : ""
4
5
 
5
6
  on run argv
6
7
  if (count of argv) < 1 then
@@ -41,8 +42,10 @@ on run argv
41
42
  set lastKey to do shell script "test -f " & quoted form of statePath & " && cat " & quoted form of statePath & " || echo ''"
42
43
 
43
44
  if dispatchKey is not lastKey then
44
- my nudgeAgent(agentId, turnNum)
45
- do shell script "printf %s " & quoted form of dispatchKey & " > " & quoted form of statePath
45
+ set nudgedOk to my nudgeAgent(agentId, turnNum, dispatchKey)
46
+ if nudgedOk then
47
+ do shell script "printf %s " & quoted form of dispatchKey & " > " & quoted form of statePath
48
+ end if
46
49
  end if
47
50
  end if
48
51
  end if
@@ -52,7 +55,7 @@ on run argv
52
55
  end repeat
53
56
  end run
54
57
 
55
- on nudgeAgent(agentId, turnNum)
58
+ on nudgeAgent(agentId, turnNum, dispatchKey)
56
59
  set nudgeText to "Hey " & agentId & ", it is your turn now (turn " & turnNum & "). Read lock.json, claim the lock, check state.md + history.jsonl + planning docs, do your work, and release lock."
57
60
  set the clipboard to nudgeText
58
61
 
@@ -60,8 +63,11 @@ on nudgeAgent(agentId, turnNum)
60
63
  delay 0.5
61
64
  set focusedOk to my focusAgentWindow(agentId)
62
65
  if focusedOk is false then
63
- do shell script "osascript -e " & quoted form of ("display notification \"Could not identify a unique window for " & agentId & ".\" with title \"AgentXchain\"")
64
- return
66
+ if lastFailedDispatch is not dispatchKey then
67
+ do shell script "osascript -e " & quoted form of ("display notification \"Could not identify a unique window for " & agentId & ".\" with title \"AgentXchain\"")
68
+ set lastFailedDispatch to dispatchKey
69
+ end if
70
+ return false
65
71
  end if
66
72
  delay 0.2
67
73
 
@@ -80,7 +86,9 @@ on nudgeAgent(agentId, turnNum)
80
86
  end tell
81
87
  end tell
82
88
 
89
+ set lastFailedDispatch to ""
83
90
  do shell script "osascript -e " & quoted form of ("display notification \"Nudged " & agentId & " for turn " & turnNum & "\" with title \"AgentXchain\"")
91
+ return true
84
92
  end nudgeAgent
85
93
 
86
94
  on focusAgentWindow(agentId)
@@ -117,6 +125,10 @@ end focusAgentWindow
117
125
 
118
126
  on isStrongWindowMatch(windowName, agentId)
119
127
  set tokenA to ".agentxchain-workspaces/" & agentId
128
+ set tokenB to ".agentxchain-workspaces\\" & agentId
129
+ set tokenC to agentId & ".code-workspace"
120
130
  if windowName contains tokenA then return true
131
+ if windowName contains tokenB then return true
132
+ if windowName contains tokenC then return true
121
133
  return false
122
134
  end isStrongWindowMatch
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'child_process';
2
- import { writeFileSync, mkdirSync, existsSync, symlinkSync, lstatSync, unlinkSync } from 'fs';
2
+ import { writeFileSync, mkdirSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import inquirer from 'inquirer';
@@ -29,7 +29,7 @@ export async function launchCursorLocal(config, root, opts) {
29
29
  writeFileSync(join(promptDir, `${id}.prompt.md`), prompt);
30
30
  }
31
31
 
32
- // Create per-agent symlinked workspace folders so Cursor opens separate windows
32
+ // Create per-agent workspace files so each Cursor window has a unique identity
33
33
  const workspacesDir = join(root, '.agentxchain-workspaces');
34
34
  mkdirSync(workspacesDir, { recursive: true });
35
35
 
@@ -39,17 +39,13 @@ export async function launchCursorLocal(config, root, opts) {
39
39
  ? generateKickoffPrompt(id, agent, config, root)
40
40
  : generatePollingPrompt(id, agent, config, root);
41
41
 
42
- // Create symlink: .agentxchain-workspaces/<id> -> project root
43
- const agentWorkspace = join(workspacesDir, id);
44
- try {
45
- if (existsSync(agentWorkspace)) {
46
- const stat = lstatSync(agentWorkspace);
47
- if (stat.isSymbolicLink()) unlinkSync(agentWorkspace);
48
- }
49
- if (!existsSync(agentWorkspace)) {
50
- symlinkSync(root, agentWorkspace, 'dir');
51
- }
52
- } catch {}
42
+ // Create workspace file: .agentxchain-workspaces/<id>.code-workspace
43
+ const agentWorkspace = join(workspacesDir, `${id}.code-workspace`);
44
+ const workspaceJson = {
45
+ folders: [{ path: root }],
46
+ settings: { 'agentxchain.agentId': id }
47
+ };
48
+ writeFileSync(agentWorkspace, JSON.stringify(workspaceJson, null, 2) + '\n');
53
49
 
54
50
  console.log(chalk.cyan(` ─── Agent ${i + 1}/${total}: ${chalk.bold(id)} — ${agent.name} ───`));
55
51
  console.log('');
@@ -2,6 +2,7 @@ import { writeFileSync, existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
5
+ import { resolveNextAgent } from '../lib/next-owner.js';
5
6
 
6
7
  export async function claimCommand(opts) {
7
8
  const result = loadConfig();
@@ -106,7 +107,7 @@ function claimAsAgent({ opts, root, config, lock }) {
106
107
  process.exit(1);
107
108
  }
108
109
 
109
- const expected = pickNextAgent(lock, config);
110
+ const expected = pickNextAgent(root, lock, config);
110
111
  if (!opts.force && expected && expected !== agentId) {
111
112
  console.log(chalk.red(` Out-of-turn claim blocked. Expected: ${expected}, got: ${agentId}.`));
112
113
  process.exit(1);
@@ -145,13 +146,8 @@ function releaseAsAgent({ opts, root, config, lock }) {
145
146
  console.log(chalk.green(` ✓ Lock released by ${agentId} (turn ${next.turn_number})`));
146
147
  }
147
148
 
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];
149
+ function pickNextAgent(root, lock, config) {
150
+ return resolveNextAgent(root, config, lock).next;
155
151
  }
156
152
 
157
153
  function clearBlockedState(root) {
@@ -0,0 +1,77 @@
1
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { generatePollingPrompt } from '../lib/seed-prompt-polling.js';
7
+
8
+ export async function rebindCommand(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 agentEntries = Object.entries(config.agents || {});
17
+ if (agentEntries.length === 0) {
18
+ console.log(chalk.red('No agents configured in agentxchain.json.'));
19
+ process.exit(1);
20
+ }
21
+
22
+ const selected = opts.agent
23
+ ? agentEntries.filter(([id]) => id === opts.agent)
24
+ : agentEntries;
25
+
26
+ if (opts.agent && selected.length === 0) {
27
+ console.log(chalk.red(`Agent "${opts.agent}" not found in agentxchain.json.`));
28
+ process.exit(1);
29
+ }
30
+
31
+ const promptDir = join(root, '.agentxchain-prompts');
32
+ const workspacesDir = join(root, '.agentxchain-workspaces');
33
+ mkdirSync(promptDir, { recursive: true });
34
+ mkdirSync(workspacesDir, { recursive: true });
35
+
36
+ for (const [id, def] of selected) {
37
+ const prompt = generatePollingPrompt(id, def, config, root);
38
+ writeFileSync(join(promptDir, `${id}.prompt.md`), prompt);
39
+
40
+ const wsPath = join(workspacesDir, `${id}.code-workspace`);
41
+ const workspaceJson = {
42
+ folders: [{ path: root }],
43
+ settings: { 'agentxchain.agentId': id }
44
+ };
45
+ writeFileSync(wsPath, JSON.stringify(workspaceJson, null, 2) + '\n');
46
+
47
+ if (opts.open) {
48
+ openCursorWindow(wsPath);
49
+ }
50
+ }
51
+
52
+ const statePath = join(root, '.agentxchain-autonudge.state');
53
+ if (existsSync(statePath)) {
54
+ rmSync(statePath, { force: true });
55
+ }
56
+
57
+ console.log('');
58
+ console.log(chalk.green(` ✓ Rebound ${selected.length} agent session(s).`));
59
+ console.log(chalk.dim(` Prompts: ${join('.agentxchain-prompts', opts.agent ? `${opts.agent}.prompt.md` : '')}`));
60
+ console.log(chalk.dim(` Workspaces: ${join('.agentxchain-workspaces', opts.agent ? `${opts.agent}.code-workspace` : '')}`));
61
+ console.log(chalk.dim(' Auto-nudge dispatch state reset.'));
62
+ if (!opts.open) {
63
+ console.log(chalk.dim(' Use `agentxchain rebind --open` to reopen agent windows now.'));
64
+ }
65
+ console.log('');
66
+ }
67
+
68
+ function openCursorWindow(targetPath) {
69
+ try {
70
+ if (process.platform === 'darwin') {
71
+ execSync(`open -na "Cursor" --args "${targetPath}"`, { stdio: 'ignore' });
72
+ return;
73
+ }
74
+ execSync(`cursor --new-window "${targetPath}"`, { stdio: 'ignore' });
75
+ } catch {}
76
+ }
77
+
@@ -6,6 +6,7 @@ import chalk from 'chalk';
6
6
  import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
7
7
  import { notifyHuman as sendNotification } from '../lib/notify.js';
8
8
  import { validateProject } from '../lib/validation.js';
9
+ import { resolveNextAgent } from '../lib/next-owner.js';
9
10
 
10
11
  export async function watchCommand(opts) {
11
12
  if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
@@ -51,8 +52,8 @@ export async function watchCommand(opts) {
51
52
  const stateKey = `${lock.holder}:${lock.turn_number}`;
52
53
 
53
54
  if (lock.holder && lock.holder !== 'human') {
54
- const expected = pickNextAgent(lock, config);
55
- if (!isValidClaimer(lock, config)) {
55
+ const expected = pickNextAgent(root, lock, config);
56
+ if (!isValidClaimer(root, lock, config)) {
56
57
  log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected}. Handing lock to HUMAN.`);
57
58
  blockOnIllegalClaim(root, lock, config, expected);
58
59
  sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
@@ -108,7 +109,7 @@ export async function watchCommand(opts) {
108
109
  }
109
110
  }
110
111
 
111
- const next = pickNextAgent(lock, config);
112
+ const next = pickNextAgent(root, lock, config);
112
113
  log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Next: ${chalk.bold(next)}.`);
113
114
  writeTrigger(root, next, lock, config);
114
115
  lastState = stateKey;
@@ -129,21 +130,14 @@ export async function watchCommand(opts) {
129
130
  });
130
131
  }
131
132
 
132
- function pickNextAgent(lock, config) {
133
- const agentIds = Object.keys(config.agents);
134
- if (agentIds.length === 0) return null;
135
- const lastAgent = lock.last_released_by;
136
-
137
- if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
138
-
139
- const lastIndex = agentIds.indexOf(lastAgent);
140
- return agentIds[(lastIndex + 1) % agentIds.length];
133
+ function pickNextAgent(root, lock, config) {
134
+ return resolveNextAgent(root, config, lock).next;
141
135
  }
142
136
 
143
- function isValidClaimer(lock, config) {
137
+ function isValidClaimer(root, lock, config) {
144
138
  if (!lock.holder || lock.holder === 'human') return true;
145
139
  if (!config.agents?.[lock.holder]) return false;
146
- const expected = pickNextAgent(lock, config);
140
+ const expected = pickNextAgent(root, lock, config);
147
141
  return lock.holder === expected;
148
142
  }
149
143
 
@@ -232,12 +232,29 @@ if [ -z "$HOLDER" ] || [ "$HOLDER" = "null" ]; then
232
232
  fi
233
233
 
234
234
  NEXT=$(node -e "
235
- const cfg = JSON.parse(require('fs').readFileSync('agentxchain.json','utf8'));
236
- const ids = Object.keys(cfg.agents);
237
- const last = process.argv[1] || '';
235
+ const fs = require('fs');
236
+ const cfg = JSON.parse(fs.readFileSync('agentxchain.json','utf8'));
237
+ const ids = Object.keys(cfg.agents || {});
238
+ const lock = JSON.parse(fs.readFileSync('lock.json','utf8'));
239
+ const talkFile = cfg.talk_file || 'TALK.md';
240
+ let fromTalk = '';
241
+ try {
242
+ const talk = fs.readFileSync(talkFile, 'utf8').split(/\\r?\\n/);
243
+ for (let i = talk.length - 1; i >= 0; i -= 1) {
244
+ const m = talk[i].trim().match(/^(?:-|\\*)?\\s*\\**next\\s*owner\\**\\s*:\\s*(.+)$/i);
245
+ if (!m) continue;
246
+ let candidate = String(m[1] || '').replace(/[\\*_]/g, '').replace(/\\(.*?\\)/g, '').trim().split(/[\\s,]+/)[0].toLowerCase();
247
+ if (ids.includes(candidate)) { fromTalk = candidate; break; }
248
+ }
249
+ } catch {}
250
+ if (fromTalk) {
251
+ process.stdout.write(fromTalk);
252
+ process.exit(0);
253
+ }
254
+ const last = lock.last_released_by || '';
238
255
  const idx = ids.indexOf(last);
239
- const next = ids[(idx + 1) % ids.length];
240
- process.stdout.write(next);
256
+ const next = idx >= 0 ? ids[(idx + 1) % ids.length] : ids[0];
257
+ process.stdout.write(next || '');
241
258
  " -- "$LAST" 2>/dev/null)
242
259
 
243
260
  if [ -z "$NEXT" ]; then
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export function resolveNextAgent(root, config, lock = {}) {
5
+ const agents = Object.keys(config.agents || {});
6
+ if (agents.length === 0) return { next: null, source: 'none', raw: null };
7
+
8
+ const talkFile = config.talk_file || 'TALK.md';
9
+ const talkPath = join(root, talkFile);
10
+ const fromTalk = parseNextOwnerFromTalk(talkPath, agents);
11
+ if (fromTalk) {
12
+ return { next: fromTalk, source: 'talk', raw: fromTalk };
13
+ }
14
+
15
+ const last = lock.last_released_by;
16
+ if (last && agents.includes(last)) {
17
+ const idx = agents.indexOf(last);
18
+ return { next: agents[(idx + 1) % agents.length], source: 'fallback-cyclic', raw: null };
19
+ }
20
+
21
+ return { next: agents[0], source: 'fallback-first', raw: null };
22
+ }
23
+
24
+ function parseNextOwnerFromTalk(talkPath, validAgentIds) {
25
+ if (!existsSync(talkPath)) return null;
26
+
27
+ let text = '';
28
+ try {
29
+ text = readFileSync(talkPath, 'utf8');
30
+ } catch {
31
+ return null;
32
+ }
33
+
34
+ if (!text.trim()) return null;
35
+
36
+ const lines = text.split(/\r?\n/);
37
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
38
+ const line = lines[i].trim();
39
+ if (!line) continue;
40
+ const match = line.match(/^(?:-|\*)?\s*\**next\s*owner\**\s*:\s*(.+)$/i);
41
+ if (!match) continue;
42
+
43
+ const candidate = normalizeAgentId(match[1]);
44
+ if (candidate && validAgentIds.includes(candidate)) {
45
+ return candidate;
46
+ }
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ function normalizeAgentId(raw) {
53
+ if (!raw) return null;
54
+ let value = String(raw).trim();
55
+ value = value.replace(/[`*_]/g, '').trim();
56
+ value = value.replace(/\(.*?\)/g, '').trim();
57
+ value = value.split(/[,\s]+/)[0];
58
+ value = value.toLowerCase();
59
+ return /^[a-z0-9_-]+$/.test(value) ? value : null;
60
+ }
61
+
@@ -9,12 +9,6 @@ export function generatePollingPrompt(agentId, agentDef, config, projectRoot = '
9
9
 
10
10
  const agentIds = Object.keys(config.agents);
11
11
  const myIndex = agentIds.indexOf(agentId);
12
- const prevAgent = myIndex === 0 ? null : agentIds[myIndex - 1];
13
- const isFirstAgent = myIndex === 0;
14
-
15
- const turnCondition = isFirstAgent
16
- ? `It is YOUR turn when lock.json shows holder=null AND (last_released_by is null, "human", "system", OR the LAST agent in the rotation: "${agentIds[agentIds.length - 1]}")`
17
- : `It is YOUR turn when lock.json shows holder=null AND last_released_by="${prevAgent}"`;
18
12
 
19
13
  const readSection = useSplit
20
14
  ? `READ THESE FILES:
@@ -37,6 +31,7 @@ c. Append ONE line to "${historyFile}":
37
31
  {"turn": N, "agent": "${agentId}", "summary": "what you did", "files_changed": [...], "verify_result": "pass|fail|skipped", "timestamp": "ISO8601"}
38
32
  d. Append ONE handoff entry to "${talkFile}" with:
39
33
  Turn, Status, Decision, Action, Risks/Questions, Next owner.
34
+ IMPORTANT: "Next owner" must be a valid agent id from [${agentIds.join(', ')}].
40
35
  e. Update state.json if phase or blocked status changed.`
41
36
  : `WRITE (in this order):
42
37
  a. Do your actual work: write code, create files, run commands, make decisions.
@@ -49,6 +44,7 @@ b. Append ONE message to ${logFile}:
49
44
  **Next:** What the next agent should focus on.
50
45
  c. Append ONE handoff entry to "${talkFile}" with:
51
46
  Turn, Status, Decision, Action, Risks/Questions, Next owner.
47
+ IMPORTANT: "Next owner" must be a valid agent id from [${agentIds.join(', ')}].
52
48
  d. Update state.json if phase or blocked status changed.`;
53
49
 
54
50
  const verifySection = verifyCmd
@@ -101,9 +97,9 @@ GET SHIT DONE FRAMEWORK (mandatory):
101
97
 
102
98
  ---
103
99
 
104
- TEAM ROTATION: ${agentIds.join(' ')} → (repeat)
100
+ TEAM IDS: ${agentIds.join(', ')}
105
101
  YOUR POSITION: ${agentId} (index ${myIndex} of ${agentIds.length})
106
- ${turnCondition}
102
+ Turn assignment is handoff-driven: previous owner writes "Next owner" in TALK.md.
107
103
 
108
104
  ---
109
105
 
@@ -115,11 +111,11 @@ TURN MODE (single turn only, referee wakes you again later):
115
111
  - Never run broad searches outside this project root.
116
112
 
117
113
  1. READ lock.json.
114
+ Also read .agentxchain-trigger.json when present.
118
115
 
119
116
  2. CHECK — is it my turn?
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}".`}
117
+ - It is your turn when lock holder is null AND trigger.agent is "${agentId}".
118
+ - If trigger file is missing, you may still attempt claim; claim guardrails enforce expected next owner.
123
119
  - If NOT your turn: STOP. Do not claim lock and do not write files.
124
120
 
125
121
  3. CLAIM the lock: