agentxchain 0.8.5 → 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,16 +16,25 @@ 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
 
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.
37
+ 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
38
 
30
39
  Agents are now required to maintain `TALK.md` as the human-readable handoff log each turn.
31
40
 
@@ -46,8 +55,29 @@ 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
 
61
+ ### Full command list
62
+
63
+ ```bash
64
+ agentxchain init
65
+ agentxchain status
66
+ agentxchain start
67
+ agentxchain kickoff
68
+ agentxchain stop
69
+ agentxchain config
70
+ agentxchain rebind
71
+ agentxchain generate
72
+ agentxchain watch
73
+ agentxchain supervise
74
+ agentxchain claim
75
+ agentxchain release
76
+ agentxchain update
77
+ agentxchain doctor
78
+ agentxchain validate
79
+ ```
80
+
51
81
  ### IDE options
52
82
 
53
83
  ```bash
@@ -62,15 +92,24 @@ agentxchain start --ide claude-code # Claude Code — spawns CLI processes
62
92
  agentxchain kickoff # guided first-run PM-first workflow
63
93
  agentxchain kickoff --ide vscode # guided flow for VS Code mode
64
94
  agentxchain kickoff --send # with Cursor auto-nudge auto-send enabled
95
+ agentxchain kickoff --interval 2 # nudge poll interval override
96
+ agentxchain kickoff --no-autonudge # skip auto-nudge prompt
65
97
 
66
98
  agentxchain start --agent pm # launch only one specific agent
67
99
  agentxchain start --remaining # launch all agents except PM (PM-first flow)
68
100
  agentxchain start --dry-run # preview agents without launching
69
101
  agentxchain validate --mode kickoff # required before --remaining
70
102
  agentxchain validate --mode turn --agent pm
103
+ agentxchain validate --json # machine-readable validation output
71
104
  agentxchain watch --daemon # run watch in background
72
105
  agentxchain supervise --autonudge # run watch + AppleScript nudge loop
73
106
  agentxchain supervise --autonudge --send # auto-press Enter after paste
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
111
+ agentxchain claim --agent pm # guarded claim as agent turn owner
112
+ agentxchain release --agent pm # guarded release as agent turn owner
74
113
  agentxchain release --force # force-release non-human holder lock
75
114
  ```
76
115
 
@@ -120,8 +159,8 @@ Notes:
120
159
  1. `agentxchain kickoff` launches PM first for human-product alignment
121
160
  2. Each window gets a unique prompt copied to clipboard
122
161
  3. Kickoff validates PM signoff and launches remaining agents
123
- 4. Agent prompts include a self-polling loop: read `lock.json` → check if it's my turn → claim → work → releasesleep 60s repeat
124
- 5. Agents know their rotation order from `agentxchain.json` and only claim when the previous agent released
162
+ 4. Agent prompts are single-turn: claim → work → validatereleasestop
163
+ 5. Agents use the latest `Next owner:` in `TALK.md` to pick who goes next (fallback: config order)
125
164
  6. Human can `claim` to pause and `release` to resume anytime
126
165
 
127
166
  ### VS Code mode
@@ -130,18 +169,17 @@ Notes:
130
169
  2. VS Code auto-discovers agents in the Chat dropdown
131
170
  3. The `Stop` hook acts as referee — hands off to next agent automatically
132
171
 
133
- ### Turn rotation
172
+ ### Turn ownership
134
173
 
135
- Agents follow a round-robin order defined in `agentxchain.json`:
136
- - PM waits for: lock free + last released by `human`, `null`, or last agent in rotation
137
- - Dev waits for: lock free + last released by `pm`
138
- - QA waits for: lock free + last released by `dev`
139
- - 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)
140
178
 
141
179
  ## Key features
142
180
 
143
181
  - **One window per agent** — each agent has its own Cursor window and chat session
144
- - **Self-polling coordination** — agents check `lock.json` every 60s, no external process needed
182
+ - **Referee-driven coordination** — `watch`/`supervise` wakes the next correct agent each turn
145
183
  - **Works in Cursor, VS Code, Claude Code** — adapters for each IDE
146
184
  - **User-defined teams** — any number of agents, any roles
147
185
  - **No API keys or cloud required** — everything runs locally
@@ -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,15 +92,24 @@ 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)')
105
+ .option('--agent <id>', 'Claim lock as a specific agent (guarded by turn order)')
97
106
  .option('--force', 'Force-claim even if an agent holds the lock')
98
107
  .action(claimCommand);
99
108
 
100
109
  program
101
110
  .command('release')
102
111
  .description('Release the lock (hand back to agents)')
112
+ .option('--agent <id>', 'Release lock as a specific agent')
103
113
  .option('--force', 'Force release even if a non-human holder has the lock')
104
114
  .action(releaseCommand);
105
115
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.8.5",
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('');
@@ -107,7 +103,7 @@ export async function launchCursorLocal(config, root, opts) {
107
103
  console.log(` ${chalk.bold('agentxchain watch')} ${chalk.dim('# watcher / trigger writer')}`);
108
104
  console.log(` ${chalk.bold('agentxchain doctor')} ${chalk.dim('# check local setup + trigger health')}`);
109
105
  console.log('');
110
- console.log(chalk.dim(' Agents self-coordinate via lock.json polling (sleep 60s between checks).'));
106
+ console.log(chalk.dim(' Agents run single turns. Watch/supervise wakes the correct next agent.'));
111
107
  console.log(chalk.dim(' Re-paste a prompt: cat .agentxchain-prompts/<agent>.prompt.md | pbcopy'));
112
108
  console.log('');
113
109
  }
@@ -162,31 +158,34 @@ Project root (strict boundary): "${projectRoot}"
162
158
  Work only inside this project folder. Do NOT scan unrelated local directories.
163
159
 
164
160
  Actions:
165
- 1) Read:
161
+ 1) FIRST QUESTION TO HUMAN (mandatory before anything else):
162
+ "Please describe your product idea in one paragraph, in as much detail as possible."
163
+
164
+ 2) Read:
166
165
  - .planning/PROJECT.md
167
166
  - .planning/REQUIREMENTS.md
168
167
  - .planning/ROADMAP.md
169
168
  - TALK.md
170
169
  - state.md
171
170
  - lock.json
172
- 2) Ask the human focused product questions until scope is clear:
171
+ 3) After receiving that paragraph, summarize it in TALK.md, then ask focused follow-up questions:
173
172
  - target user
174
173
  - top pain point
175
174
  - core workflow
176
175
  - MVP boundary
177
176
  - success metric
178
- 3) Update planning docs with concrete acceptance criteria and Get Shit Done structure:
177
+ 4) Update planning docs with concrete acceptance criteria and Get Shit Done structure:
179
178
  - .planning/ROADMAP.md must define Waves and Phases.
180
179
  - Create .planning/phases/phase-1/PLAN.md and TESTS.md.
181
- 4) Update .planning/PM_SIGNOFF.md:
180
+ 5) Update .planning/PM_SIGNOFF.md:
182
181
  - Set "Approved: YES" only when human agrees kickoff is complete.
183
- 5) Append kickoff summary to TALK.md with:
182
+ 6) Append kickoff summary to TALK.md with:
184
183
  - Status
185
184
  - Decision
186
185
  - Action
187
186
  - Risks/Questions
188
187
  - Next owner
189
- 6) Do NOT start round-robin agent handoffs yet.
188
+ 7) Do NOT start round-robin agent handoffs yet.
190
189
 
191
190
  Context:
192
191
  - Project: ${config.project}
@@ -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();
@@ -11,6 +12,10 @@ export async function claimCommand(opts) {
11
12
  const lock = loadLock(root);
12
13
  if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
13
14
 
15
+ if (opts.agent) {
16
+ return claimAsAgent({ opts, root, config, lock });
17
+ }
18
+
14
19
  if (lock.holder === 'human') {
15
20
  console.log('');
16
21
  console.log(chalk.yellow(' You already hold the lock.'));
@@ -52,6 +57,10 @@ export async function releaseCommand(opts) {
52
57
  const lock = loadLock(root);
53
58
  if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
54
59
 
60
+ if (opts.agent) {
61
+ return releaseAsAgent({ opts, root, config, lock });
62
+ }
63
+
55
64
  if (!lock.holder) {
56
65
  console.log(chalk.yellow(' Lock is already free.'));
57
66
  return;
@@ -85,6 +94,62 @@ export async function releaseCommand(opts) {
85
94
  console.log('');
86
95
  }
87
96
 
97
+ function claimAsAgent({ opts, root, config, lock }) {
98
+ const agentId = opts.agent;
99
+ if (!config.agents?.[agentId]) {
100
+ console.log(chalk.red(` Agent "${agentId}" is not defined in agentxchain.json.`));
101
+ process.exit(1);
102
+ }
103
+
104
+ if (lock.holder && !opts.force) {
105
+ console.log(chalk.red(` Lock is currently held by "${lock.holder}".`));
106
+ console.log(chalk.dim(' Use --force only for recovery scenarios.'));
107
+ process.exit(1);
108
+ }
109
+
110
+ const expected = pickNextAgent(root, lock, config);
111
+ if (!opts.force && expected && expected !== agentId) {
112
+ console.log(chalk.red(` Out-of-turn claim blocked. Expected: ${expected}, got: ${agentId}.`));
113
+ process.exit(1);
114
+ }
115
+
116
+ const lockPath = join(root, LOCK_FILE);
117
+ const next = {
118
+ holder: agentId,
119
+ last_released_by: lock.last_released_by,
120
+ turn_number: lock.turn_number,
121
+ claimed_at: new Date().toISOString()
122
+ };
123
+ writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
124
+ console.log(chalk.green(` ✓ Lock claimed by ${agentId} (turn ${next.turn_number})`));
125
+ }
126
+
127
+ function releaseAsAgent({ opts, root, config, lock }) {
128
+ const agentId = opts.agent;
129
+ if (!config.agents?.[agentId]) {
130
+ console.log(chalk.red(` Agent "${agentId}" is not defined in agentxchain.json.`));
131
+ process.exit(1);
132
+ }
133
+ if (lock.holder !== agentId && !opts.force) {
134
+ console.log(chalk.red(` Lock is held by "${lock.holder}", not "${agentId}".`));
135
+ process.exit(1);
136
+ }
137
+
138
+ const lockPath = join(root, LOCK_FILE);
139
+ const next = {
140
+ holder: null,
141
+ last_released_by: agentId,
142
+ turn_number: lock.turn_number + 1,
143
+ claimed_at: null
144
+ };
145
+ writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
146
+ console.log(chalk.green(` ✓ Lock released by ${agentId} (turn ${next.turn_number})`));
147
+ }
148
+
149
+ function pickNextAgent(root, lock, config) {
150
+ return resolveNextAgent(root, config, lock).next;
151
+ }
152
+
88
153
  function clearBlockedState(root) {
89
154
  const statePath = join(root, 'state.json');
90
155
  if (!existsSync(statePath)) return;
@@ -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') {
@@ -50,6 +51,17 @@ export async function watchCommand(opts) {
50
51
 
51
52
  const stateKey = `${lock.holder}:${lock.turn_number}`;
52
53
 
54
+ if (lock.holder && lock.holder !== 'human') {
55
+ const expected = pickNextAgent(root, lock, config);
56
+ if (!isValidClaimer(root, lock, config)) {
57
+ log('warn', `Illegal claim detected: holder=${lock.holder}, expected=${expected}. Handing lock to HUMAN.`);
58
+ blockOnIllegalClaim(root, lock, config, expected);
59
+ sendNotification(`Illegal claim detected (${lock.holder}). Human intervention required.`);
60
+ lastState = null;
61
+ return;
62
+ }
63
+ }
64
+
53
65
  if (lock.holder && lock.holder !== 'human' && lock.claimed_at) {
54
66
  const elapsed = Date.now() - new Date(lock.claimed_at).getTime();
55
67
  const ttlMs = ttlMinutes * 60 * 1000;
@@ -97,7 +109,7 @@ export async function watchCommand(opts) {
97
109
  }
98
110
  }
99
111
 
100
- const next = pickNextAgent(lock, config);
112
+ const next = pickNextAgent(root, lock, config);
101
113
  log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Next: ${chalk.bold(next)}.`);
102
114
  writeTrigger(root, next, lock, config);
103
115
  lastState = stateKey;
@@ -118,15 +130,15 @@ export async function watchCommand(opts) {
118
130
  });
119
131
  }
120
132
 
121
- function pickNextAgent(lock, config) {
122
- const agentIds = Object.keys(config.agents);
123
- if (agentIds.length === 0) return null;
124
- const lastAgent = lock.last_released_by;
125
-
126
- if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
133
+ function pickNextAgent(root, lock, config) {
134
+ return resolveNextAgent(root, config, lock).next;
135
+ }
127
136
 
128
- const lastIndex = agentIds.indexOf(lastAgent);
129
- return agentIds[(lastIndex + 1) % agentIds.length];
137
+ function isValidClaimer(root, lock, config) {
138
+ if (!lock.holder || lock.holder === 'human') return true;
139
+ if (!config.agents?.[lock.holder]) return false;
140
+ const expected = pickNextAgent(root, lock, config);
141
+ return lock.holder === expected;
130
142
  }
131
143
 
132
144
  function forceRelease(root, lock, staleAgent, config) {
@@ -192,6 +204,39 @@ function blockOnValidation(root, lock, config, validation) {
192
204
  }
193
205
  }
194
206
 
207
+ function blockOnIllegalClaim(root, lock, config, expected) {
208
+ const lockPath = join(root, LOCK_FILE);
209
+ const newLock = {
210
+ holder: 'human',
211
+ last_released_by: lock.last_released_by,
212
+ turn_number: lock.turn_number,
213
+ claimed_at: new Date().toISOString()
214
+ };
215
+ writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
216
+
217
+ const statePath = join(root, 'state.json');
218
+ if (existsSync(statePath)) {
219
+ try {
220
+ const current = JSON.parse(readFileSync(statePath, 'utf8'));
221
+ const nextState = {
222
+ ...current,
223
+ blocked: true,
224
+ blocked_on: `illegal-claim: expected ${expected}, got ${lock.holder}`
225
+ };
226
+ writeFileSync(statePath, JSON.stringify(nextState, null, 2) + '\n');
227
+ } catch {}
228
+ }
229
+
230
+ const logFile = config.log || 'log.md';
231
+ const logPath = join(root, logFile);
232
+ if (existsSync(logPath)) {
233
+ appendFileSync(
234
+ logPath,
235
+ `\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`
236
+ );
237
+ }
238
+ }
239
+
195
240
  function log(type, msg) {
196
241
  const time = new Date().toLocaleTimeString();
197
242
  const tags = {
@@ -142,12 +142,13 @@ Create or update these files when your role requires it.
142
142
 
143
143
  The AgentXchain system coordinates turns. When prompted, do this:
144
144
 
145
- 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.
146
146
  2. **READ**: ${readInstructions}
147
147
  3. **THINK**: What did the previous agent do? What is most important for YOUR role? What is one risk?
148
148
  4. **WORK**: ${writeInstructions}${verifyInstructions}
149
- 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}\`.
150
150
  This MUST be the last thing you write.
151
+ 6. **STOP**: End your turn. The referee will wake the next agent.
151
152
 
152
153
  ---
153
154
 
@@ -231,12 +232,29 @@ if [ -z "$HOLDER" ] || [ "$HOLDER" = "null" ]; then
231
232
  fi
232
233
 
233
234
  NEXT=$(node -e "
234
- const cfg = JSON.parse(require('fs').readFileSync('agentxchain.json','utf8'));
235
- const ids = Object.keys(cfg.agents);
236
- 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 || '';
237
255
  const idx = ids.indexOf(last);
238
- const next = ids[(idx + 1) % ids.length];
239
- process.stdout.write(next);
256
+ const next = idx >= 0 ? ids[(idx + 1) % ids.length] : ids[0];
257
+ process.stdout.write(next || '');
240
258
  " -- "$LAST" 2>/dev/null)
241
259
 
242
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,13 +97,13 @@ 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
 
110
- YOUR LOOP (run forever, never exit, never say "I'm done"):
106
+ TURN MODE (single turn only, referee wakes you again later):
111
107
 
112
108
  0. CHECK WORKING DIRECTORY:
113
109
  - Run: pwd
@@ -115,20 +111,16 @@ YOUR LOOP (run forever, never exit, never say "I'm done"):
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
- - If holder is NOT null someone else is working. Run the shell command: sleep 60
121
- Then go back to step 1.
122
- - If holder IS null check last_released_by:
123
- ${isFirstAgent
124
- ? `- If last_released_by is null, "human", starts with "system", or "${agentIds[agentIds.length - 1]}" → IT IS YOUR TURN. Go to step 3.`
125
- : `- If last_released_by is "${prevAgent}" → IT IS YOUR TURN. Go to step 3.`}
126
- - Otherwise → it is another agent's turn. Run the shell command: sleep 60
127
- Then go back to step 1.
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.
119
+ - If NOT your turn: STOP. Do not claim lock and do not write files.
128
120
 
129
121
  3. CLAIM the lock:
130
- Write lock.json: {"holder":"${agentId}","last_released_by":<keep previous>,"turn_number":<keep previous>,"claimed_at":"<current ISO timestamp>"}
131
- Then RE-READ lock.json immediately. If holder is not "${agentId}", someone else won. Go to step 1.
122
+ Run: agentxchain claim --agent ${agentId}
123
+ If claim is blocked, STOP.
132
124
 
133
125
  4. DO YOUR WORK:
134
126
  ${readSection}
@@ -142,20 +134,16 @@ YOUR LOOP (run forever, never exit, never say "I'm done"):
142
134
  If validation fails, fix docs/artifacts first. Do NOT release.
143
135
 
144
136
  5. RELEASE the lock:
145
- Write lock.json: {"holder":null,"last_released_by":"${agentId}","turn_number":<previous + 1>,"claimed_at":null}
137
+ Run: agentxchain release --agent ${agentId}
146
138
  THIS MUST BE THE LAST FILE YOU WRITE.
147
139
 
148
- 6. Run the shell command: sleep 60
149
- Then go back to step 1.
150
-
151
140
  ---
152
141
 
153
142
  CRITICAL RULES:
154
- - ACTUALLY RUN "sleep 60" in the terminal between checks. Do NOT skip this. Do NOT just say "waiting."
155
143
  - Never write files or code without holding the lock. Reading is always allowed.
156
144
  - One git commit per turn: "Turn N - ${agentId} - description"
157
145
  - Max ${maxClaims} consecutive turns. If you have held the lock ${maxClaims} times in a row, do a short turn and release.
158
146
  - ALWAYS release the lock. A stuck lock blocks the entire team.
159
147
  - ALWAYS find at least one problem, risk, or question about the previous work. Blind agreement is forbidden.
160
- - NEVER exit or stop. After releasing, always sleep and poll again. You are a persistent agent.`;
148
+ - This session is SINGLE-TURN. After release, STOP and wait for the referee to wake you again.`;
161
149
  }