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 +51 -13
- package/bin/agentxchain.js +10 -0
- package/package.json +1 -1
- package/scripts/agentxchain-autonudge.applescript +17 -5
- package/src/adapters/cursor-local.js +19 -20
- package/src/commands/claim.js +65 -0
- package/src/commands/rebind.js +77 -0
- package/src/commands/watch.js +54 -9
- package/src/lib/generate-vscode.js +25 -7
- package/src/lib/next-owner.js +61 -0
- package/src/lib/seed-prompt-polling.js +13 -25
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
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
124
|
-
5. Agents
|
|
162
|
+
4. Agent prompts are single-turn: claim → work → validate → release → stop
|
|
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
|
|
172
|
+
### Turn ownership
|
|
134
173
|
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
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
|
-
- **
|
|
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
|
package/bin/agentxchain.js
CHANGED
|
@@ -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,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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
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
|
|
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
|
|
43
|
-
const agentWorkspace = join(workspacesDir, id);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
5) Update .planning/PM_SIGNOFF.md:
|
|
182
181
|
- Set "Approved: YES" only when human agrees kickoff is complete.
|
|
183
|
-
|
|
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
|
-
|
|
188
|
+
7) Do NOT start round-robin agent handoffs yet.
|
|
190
189
|
|
|
191
190
|
Context:
|
|
192
191
|
- Project: ${config.project}
|
package/src/commands/claim.js
CHANGED
|
@@ -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
|
+
|
package/src/commands/watch.js
CHANGED
|
@@ -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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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**:
|
|
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**:
|
|
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
|
|
235
|
-
const
|
|
236
|
-
const
|
|
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
|
|
100
|
+
TEAM IDS: ${agentIds.join(', ')}
|
|
105
101
|
YOUR POSITION: ${agentId} (index ${myIndex} of ${agentIds.length})
|
|
106
|
-
|
|
102
|
+
Turn assignment is handoff-driven: previous owner writes "Next owner" in TALK.md.
|
|
107
103
|
|
|
108
104
|
---
|
|
109
105
|
|
|
110
|
-
|
|
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
|
-
-
|
|
121
|
-
|
|
122
|
-
- If
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
148
|
+
- This session is SINGLE-TURN. After release, STOP and wait for the referee to wake you again.`;
|
|
161
149
|
}
|