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 +23 -10
- package/bin/agentxchain.js +8 -0
- package/package.json +1 -1
- package/scripts/agentxchain-autonudge.applescript +17 -5
- package/src/adapters/cursor-local.js +9 -13
- package/src/commands/claim.js +4 -8
- package/src/commands/rebind.js +77 -0
- package/src/commands/watch.js +8 -14
- package/src/lib/generate-vscode.js +22 -5
- package/src/lib/next-owner.js +61 -0
- package/src/lib/seed-prompt-polling.js +7 -11
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
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
|
172
|
+
### Turn ownership
|
|
159
173
|
|
|
160
|
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
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
|
|
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,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,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('');
|
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();
|
|
@@ -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
|
-
|
|
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
|
+
|
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') {
|
|
@@ -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
|
-
|
|
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
|
|
236
|
-
const
|
|
237
|
-
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 || '';
|
|
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
|
|
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
|
|
|
@@ -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
|
-
${
|
|
121
|
-
|
|
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:
|