agentxchain 0.8.3 → 0.8.5
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 +15 -13
- package/bin/agentxchain.js +19 -0
- package/package.json +1 -1
- package/scripts/agentxchain-autonudge.applescript +23 -8
- package/src/adapters/cursor-local.js +21 -7
- package/src/commands/claim.js +17 -1
- package/src/commands/doctor.js +14 -0
- package/src/commands/init.js +5 -2
- package/src/commands/kickoff.js +119 -0
- package/src/commands/start.js +17 -0
- package/src/commands/validate.js +49 -0
- package/src/commands/watch.js +50 -0
- package/src/lib/generate-vscode.js +18 -4
- package/src/lib/seed-prompt-polling.js +36 -3
- package/src/lib/validation.js +205 -0
package/README.md
CHANGED
|
@@ -20,30 +20,25 @@ npx agentxchain init
|
|
|
20
20
|
# 1. Create a project (interactive template selection)
|
|
21
21
|
agentxchain init
|
|
22
22
|
|
|
23
|
-
# 2. PM-first kickoff
|
|
23
|
+
# 2. Run guided PM-first kickoff wizard
|
|
24
24
|
cd my-project/
|
|
25
|
-
agentxchain
|
|
26
|
-
|
|
27
|
-
# 3. Launch remaining agents after planning is clear
|
|
28
|
-
agentxchain start --remaining
|
|
29
|
-
|
|
30
|
-
# 4. Start supervisor (watch + auto-nudge)
|
|
31
|
-
agentxchain supervise --autonudge
|
|
32
|
-
|
|
33
|
-
# 5. Release the human lock — agents start claiming turns
|
|
34
|
-
agentxchain release
|
|
25
|
+
agentxchain kickoff
|
|
35
26
|
```
|
|
36
27
|
|
|
37
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.
|
|
38
29
|
|
|
30
|
+
Agents are now required to maintain `TALK.md` as the human-readable handoff log each turn.
|
|
31
|
+
|
|
39
32
|
## Commands
|
|
40
33
|
|
|
41
34
|
| Command | What it does |
|
|
42
35
|
|---------|-------------|
|
|
43
36
|
| `init` | Create project folder with agents, protocol files, and templates |
|
|
37
|
+
| `kickoff` | Guided PM-first flow: PM kickoff, validate, launch remaining, release |
|
|
44
38
|
| `start` | Open a Cursor window per agent + copy prompts to clipboard |
|
|
45
39
|
| `supervise` | Run watcher and optional AppleScript auto-nudge together |
|
|
46
40
|
| `generate` | Regenerate agent files from `agentxchain.json` |
|
|
41
|
+
| `validate` | Enforce PM signoff + waves/phases + turn artifact schema |
|
|
47
42
|
| `status` | Show lock holder, phase, turn number, agents |
|
|
48
43
|
| `doctor` | Validate local setup (tools, trigger flow, accessibility checks) |
|
|
49
44
|
| `claim` | Human takes control (agents stop claiming) |
|
|
@@ -64,9 +59,15 @@ agentxchain start --ide claude-code # Claude Code — spawns CLI processes
|
|
|
64
59
|
### Additional flags
|
|
65
60
|
|
|
66
61
|
```bash
|
|
62
|
+
agentxchain kickoff # guided first-run PM-first workflow
|
|
63
|
+
agentxchain kickoff --ide vscode # guided flow for VS Code mode
|
|
64
|
+
agentxchain kickoff --send # with Cursor auto-nudge auto-send enabled
|
|
65
|
+
|
|
67
66
|
agentxchain start --agent pm # launch only one specific agent
|
|
68
67
|
agentxchain start --remaining # launch all agents except PM (PM-first flow)
|
|
69
68
|
agentxchain start --dry-run # preview agents without launching
|
|
69
|
+
agentxchain validate --mode kickoff # required before --remaining
|
|
70
|
+
agentxchain validate --mode turn --agent pm
|
|
70
71
|
agentxchain watch --daemon # run watch in background
|
|
71
72
|
agentxchain supervise --autonudge # run watch + AppleScript nudge loop
|
|
72
73
|
agentxchain supervise --autonudge --send # auto-press Enter after paste
|
|
@@ -110,14 +111,15 @@ Notes:
|
|
|
110
111
|
- Grant Accessibility permissions to Terminal and Cursor
|
|
111
112
|
- The script watches `.agentxchain-trigger.json`, which is written by `agentxchain watch`
|
|
112
113
|
- `run-autonudge.sh` now requires watch to be running first
|
|
114
|
+
- The script only nudges when it finds a unique matching agent window (no random fallback)
|
|
113
115
|
|
|
114
116
|
## How it works
|
|
115
117
|
|
|
116
118
|
### Cursor mode (default)
|
|
117
119
|
|
|
118
|
-
1. `agentxchain
|
|
120
|
+
1. `agentxchain kickoff` launches PM first for human-product alignment
|
|
119
121
|
2. Each window gets a unique prompt copied to clipboard
|
|
120
|
-
3.
|
|
122
|
+
3. Kickoff validates PM signoff and launches remaining agents
|
|
121
123
|
4. Agent prompts include a self-polling loop: read `lock.json` → check if it's my turn → claim → work → release → sleep 60s → repeat
|
|
122
124
|
5. Agents know their rotation order from `agentxchain.json` and only claim when the previous agent released
|
|
123
125
|
6. Human can `claim` to pause and `release` to resume anytime
|
package/bin/agentxchain.js
CHANGED
|
@@ -15,6 +15,8 @@ import { claimCommand, releaseCommand } from '../src/commands/claim.js';
|
|
|
15
15
|
import { generateCommand } from '../src/commands/generate.js';
|
|
16
16
|
import { doctorCommand } from '../src/commands/doctor.js';
|
|
17
17
|
import { superviseCommand } from '../src/commands/supervise.js';
|
|
18
|
+
import { validateCommand } from '../src/commands/validate.js';
|
|
19
|
+
import { kickoffCommand } from '../src/commands/kickoff.js';
|
|
18
20
|
|
|
19
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
22
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -47,6 +49,15 @@ program
|
|
|
47
49
|
.option('--dry-run', 'Print what would be launched without doing it')
|
|
48
50
|
.action(startCommand);
|
|
49
51
|
|
|
52
|
+
program
|
|
53
|
+
.command('kickoff')
|
|
54
|
+
.description('Guided PM-first first-run workflow')
|
|
55
|
+
.option('--ide <ide>', 'Target IDE: cursor, vscode, claude-code', 'cursor')
|
|
56
|
+
.option('--send', 'When using Cursor auto-nudge, auto-send nudges')
|
|
57
|
+
.option('--interval <seconds>', 'Auto-nudge poll interval in seconds', '3')
|
|
58
|
+
.option('--no-autonudge', 'Skip auto-nudge supervisor prompt')
|
|
59
|
+
.action(kickoffCommand);
|
|
60
|
+
|
|
50
61
|
program
|
|
51
62
|
.command('stop')
|
|
52
63
|
.description('Stop all running agent sessions')
|
|
@@ -102,4 +113,12 @@ program
|
|
|
102
113
|
.description('Check local environment and first-run readiness')
|
|
103
114
|
.action(doctorCommand);
|
|
104
115
|
|
|
116
|
+
program
|
|
117
|
+
.command('validate')
|
|
118
|
+
.description('Validate Get Shit Done docs and QA protocol artifacts')
|
|
119
|
+
.option('--mode <mode>', 'Validation mode: kickoff, turn, full', 'full')
|
|
120
|
+
.option('--agent <id>', 'Expected agent for last history entry (turn mode)')
|
|
121
|
+
.option('-j, --json', 'Output as JSON')
|
|
122
|
+
.action(validateCommand);
|
|
123
|
+
|
|
105
124
|
program.parse();
|
package/package.json
CHANGED
|
@@ -58,7 +58,11 @@ on nudgeAgent(agentId, turnNum)
|
|
|
58
58
|
|
|
59
59
|
tell application "Cursor" to activate
|
|
60
60
|
delay 0.5
|
|
61
|
-
my focusAgentWindow(agentId)
|
|
61
|
+
set focusedOk to my focusAgentWindow(agentId)
|
|
62
|
+
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
|
|
65
|
+
end if
|
|
62
66
|
delay 0.2
|
|
63
67
|
|
|
64
68
|
tell application "System Events"
|
|
@@ -85,23 +89,34 @@ on focusAgentWindow(agentId)
|
|
|
85
89
|
tell process "Cursor"
|
|
86
90
|
set frontmost to true
|
|
87
91
|
|
|
88
|
-
set
|
|
92
|
+
set matchedIndexes to {}
|
|
93
|
+
set idx to 0
|
|
89
94
|
repeat with w in windows
|
|
95
|
+
set idx to idx + 1
|
|
90
96
|
try
|
|
91
97
|
set wn to name of w as text
|
|
92
|
-
if wn
|
|
93
|
-
|
|
94
|
-
set didRaise to true
|
|
95
|
-
exit repeat
|
|
98
|
+
if my isStrongWindowMatch(wn, agentId) then
|
|
99
|
+
set end of matchedIndexes to idx
|
|
96
100
|
end if
|
|
97
101
|
end try
|
|
98
102
|
end repeat
|
|
99
103
|
|
|
100
|
-
if
|
|
104
|
+
if (count of matchedIndexes) = 1 then
|
|
101
105
|
try
|
|
102
|
-
|
|
106
|
+
set targetIndex to item 1 of matchedIndexes
|
|
107
|
+
perform action "AXRaise" of window targetIndex
|
|
108
|
+
return true
|
|
103
109
|
end try
|
|
110
|
+
else if (count of matchedIndexes) > 1 then
|
|
111
|
+
return false
|
|
104
112
|
end if
|
|
105
113
|
end tell
|
|
106
114
|
end tell
|
|
115
|
+
return false
|
|
107
116
|
end focusAgentWindow
|
|
117
|
+
|
|
118
|
+
on isStrongWindowMatch(windowName, agentId)
|
|
119
|
+
set tokenA to ".agentxchain-workspaces/" & agentId
|
|
120
|
+
if windowName contains tokenA then return true
|
|
121
|
+
return false
|
|
122
|
+
end isStrongWindowMatch
|
|
@@ -24,8 +24,8 @@ export async function launchCursorLocal(config, root, opts) {
|
|
|
24
24
|
// Save all prompts first
|
|
25
25
|
for (const [id, agent] of agentEntries) {
|
|
26
26
|
const prompt = isPmKickoff
|
|
27
|
-
? generateKickoffPrompt(id, agent, config)
|
|
28
|
-
: generatePollingPrompt(id, agent, config);
|
|
27
|
+
? generateKickoffPrompt(id, agent, config, root)
|
|
28
|
+
: generatePollingPrompt(id, agent, config, root);
|
|
29
29
|
writeFileSync(join(promptDir, `${id}.prompt.md`), prompt);
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -36,8 +36,8 @@ export async function launchCursorLocal(config, root, opts) {
|
|
|
36
36
|
for (let i = 0; i < agentEntries.length; i++) {
|
|
37
37
|
const [id, agent] = agentEntries[i];
|
|
38
38
|
const prompt = isPmKickoff
|
|
39
|
-
? generateKickoffPrompt(id, agent, config)
|
|
40
|
-
: generatePollingPrompt(id, agent, config);
|
|
39
|
+
? generateKickoffPrompt(id, agent, config, root)
|
|
40
|
+
: generatePollingPrompt(id, agent, config, root);
|
|
41
41
|
|
|
42
42
|
// Create symlink: .agentxchain-workspaces/<id> -> project root
|
|
43
43
|
const agentWorkspace = join(workspacesDir, id);
|
|
@@ -153,16 +153,20 @@ function isPmLike(agentId, agentDef) {
|
|
|
153
153
|
return name.includes('product manager');
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
function generateKickoffPrompt(agentId, agentDef, config) {
|
|
156
|
+
function generateKickoffPrompt(agentId, agentDef, config, projectRoot) {
|
|
157
157
|
return `You are "${agentId}" — ${agentDef.name}.
|
|
158
158
|
|
|
159
159
|
This is PM kickoff mode. Your job now is to collaborate with the human and finalize scope before autonomous turns begin.
|
|
160
160
|
|
|
161
|
+
Project root (strict boundary): "${projectRoot}"
|
|
162
|
+
Work only inside this project folder. Do NOT scan unrelated local directories.
|
|
163
|
+
|
|
161
164
|
Actions:
|
|
162
165
|
1) Read:
|
|
163
166
|
- .planning/PROJECT.md
|
|
164
167
|
- .planning/REQUIREMENTS.md
|
|
165
168
|
- .planning/ROADMAP.md
|
|
169
|
+
- TALK.md
|
|
166
170
|
- state.md
|
|
167
171
|
- lock.json
|
|
168
172
|
2) Ask the human focused product questions until scope is clear:
|
|
@@ -171,8 +175,18 @@ Actions:
|
|
|
171
175
|
- core workflow
|
|
172
176
|
- MVP boundary
|
|
173
177
|
- success metric
|
|
174
|
-
3) Update planning docs with concrete acceptance criteria
|
|
175
|
-
|
|
178
|
+
3) Update planning docs with concrete acceptance criteria and Get Shit Done structure:
|
|
179
|
+
- .planning/ROADMAP.md must define Waves and Phases.
|
|
180
|
+
- Create .planning/phases/phase-1/PLAN.md and TESTS.md.
|
|
181
|
+
4) Update .planning/PM_SIGNOFF.md:
|
|
182
|
+
- Set "Approved: YES" only when human agrees kickoff is complete.
|
|
183
|
+
5) Append kickoff summary to TALK.md with:
|
|
184
|
+
- Status
|
|
185
|
+
- Decision
|
|
186
|
+
- Action
|
|
187
|
+
- Risks/Questions
|
|
188
|
+
- Next owner
|
|
189
|
+
6) Do NOT start round-robin agent handoffs yet.
|
|
176
190
|
|
|
177
191
|
Context:
|
|
178
192
|
- Project: ${config.project}
|
package/src/commands/claim.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeFileSync } from 'fs';
|
|
1
|
+
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';
|
|
@@ -36,6 +36,7 @@ export async function claimCommand(opts) {
|
|
|
36
36
|
claimed_at: new Date().toISOString()
|
|
37
37
|
};
|
|
38
38
|
writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
|
|
39
|
+
clearBlockedState(root);
|
|
39
40
|
|
|
40
41
|
console.log('');
|
|
41
42
|
console.log(chalk.green(` ✓ Lock claimed by ${chalk.bold('human')} (turn ${lock.turn_number})`));
|
|
@@ -74,9 +75,24 @@ export async function releaseCommand(opts) {
|
|
|
74
75
|
claimed_at: null
|
|
75
76
|
};
|
|
76
77
|
writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
|
|
78
|
+
if (who === 'human') {
|
|
79
|
+
clearBlockedState(root);
|
|
80
|
+
}
|
|
77
81
|
|
|
78
82
|
console.log('');
|
|
79
83
|
console.log(chalk.green(` ✓ Lock released by ${chalk.bold(who)} (turn ${newLock.turn_number})`));
|
|
80
84
|
console.log(chalk.dim(' The Stop hook will coordinate the next agent turn in VS Code.'));
|
|
81
85
|
console.log('');
|
|
82
86
|
}
|
|
87
|
+
|
|
88
|
+
function clearBlockedState(root) {
|
|
89
|
+
const statePath = join(root, 'state.json');
|
|
90
|
+
if (!existsSync(statePath)) return;
|
|
91
|
+
try {
|
|
92
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
93
|
+
if (state.blocked || state.blocked_on) {
|
|
94
|
+
const next = { ...state, blocked: false, blocked_on: null };
|
|
95
|
+
writeFileSync(statePath, JSON.stringify(next, null, 2) + '\n');
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execSync } from 'child_process';
|
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadConfig, loadLock } from '../lib/config.js';
|
|
6
|
+
import { validateProject } from '../lib/validation.js';
|
|
6
7
|
|
|
7
8
|
export async function doctorCommand() {
|
|
8
9
|
const result = loadConfig();
|
|
@@ -21,6 +22,7 @@ export async function doctorCommand() {
|
|
|
21
22
|
checks.push(checkBinary('jq', 'jq installed (required for auto-nudge)'));
|
|
22
23
|
checks.push(checkBinary('osascript', 'osascript available (required for auto-nudge, macOS)'));
|
|
23
24
|
checks.push(checkPm(config));
|
|
25
|
+
checks.push(checkValidation(root, config));
|
|
24
26
|
checks.push(checkWatchProcess());
|
|
25
27
|
checks.push(checkTrigger(root));
|
|
26
28
|
checks.push(checkAccessibility());
|
|
@@ -130,3 +132,15 @@ function checkAccessibility() {
|
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
}
|
|
135
|
+
|
|
136
|
+
function checkValidation(root, config) {
|
|
137
|
+
const validation = validateProject(root, config, { mode: 'kickoff' });
|
|
138
|
+
if (validation.ok) {
|
|
139
|
+
return { name: 'kickoff validation', level: 'pass', detail: 'PM signoff + waves/phases look ready' };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
name: 'kickoff validation',
|
|
143
|
+
level: 'warn',
|
|
144
|
+
detail: `Run \`agentxchain validate --mode kickoff\` (${validation.errors.length} issue(s))`
|
|
145
|
+
};
|
|
146
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -183,6 +183,7 @@ export async function initCommand(opts) {
|
|
|
183
183
|
project,
|
|
184
184
|
agents,
|
|
185
185
|
log: 'log.md',
|
|
186
|
+
talk_file: 'TALK.md',
|
|
186
187
|
state_file: 'state.md',
|
|
187
188
|
history_file: 'history.jsonl',
|
|
188
189
|
rules: {
|
|
@@ -205,6 +206,7 @@ export async function initCommand(opts) {
|
|
|
205
206
|
writeFileSync(join(dir, 'state.md'), `# ${project} — Current State\n\n## Architecture\n\n(Agents update this each turn with current decisions.)\n\n## Active Work\n\n(What's in progress right now.)\n\n## Open Issues\n\n(Bugs, blockers, risks.)\n\n## Next Steps\n\n(What should happen next.)\n`);
|
|
206
207
|
writeFileSync(join(dir, 'history.jsonl'), '');
|
|
207
208
|
writeFileSync(join(dir, 'log.md'), `# ${project} — Agent Log\n\n## COMPRESSED CONTEXT\n\n(No compressed context yet.)\n\n## MESSAGE LOG\n\n(Agents append messages below this line.)\n`);
|
|
209
|
+
writeFileSync(join(dir, 'TALK.md'), `# ${project} — Team Talk File\n\nCanonical human-readable handoff log for all agents.\n\n## How to write entries\n\nUse this exact structure:\n\n## Turn N — <agent_id> (<role>)\n- Status:\n- Decision:\n- Action:\n- Risks/Questions:\n- Next owner:\n\n---\n\n`);
|
|
208
210
|
writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
|
|
209
211
|
const gitignorePath = join(dir, '.gitignore');
|
|
210
212
|
const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/'];
|
|
@@ -229,6 +231,7 @@ export async function initCommand(opts) {
|
|
|
229
231
|
writeFileSync(join(dir, '.planning', 'REQUIREMENTS.md'), `# Requirements — ${project}\n\n## v1 (MVP)\n\n(PM fills this: numbered list of requirements. Each requirement has one-sentence acceptance criteria.)\n\n| # | Requirement | Acceptance criteria | Phase | Status |\n|---|-------------|-------------------|-------|--------|\n| 1 | | | | Pending |\n\n## v2 (Future)\n\n(Out of scope for MVP. Captured here so they don't creep in.)\n\n## Out of scope\n\n(Explicitly not building.)\n`);
|
|
230
232
|
|
|
231
233
|
writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${project}\n\n## Phases\n\n| Phase | Description | Status | Requirements |\n|-------|-------------|--------|-------------|\n| 1 | Discovery + setup | In progress | — |\n\n(PM updates this as phases are planned and completed.)\n`);
|
|
234
|
+
writeFileSync(join(dir, '.planning', 'PM_SIGNOFF.md'), `# PM Signoff — ${project}\n\nApproved: NO\n\n## Discovery Checklist\n- [ ] Target user defined\n- [ ] Core pain point defined\n- [ ] Core workflow defined\n- [ ] MVP scope defined\n- [ ] Out-of-scope list defined\n- [ ] Success metric defined\n\n## Notes for team\n(PM and human add final kickoff notes here.)\n`);
|
|
232
235
|
|
|
233
236
|
// QA structure
|
|
234
237
|
writeFileSync(join(dir, '.planning', 'qa', 'TEST-COVERAGE.md'), `# Test Coverage — ${project}\n\n## Coverage Map\n\n| Feature / Area | Unit tests | Integration tests | E2E tests | Manual QA | UX audit | Status |\n|---------------|-----------|------------------|----------|----------|---------|--------|\n| (QA fills this as testing progresses) | | | | | | |\n\n## Coverage gaps\n\n(Areas with no tests or insufficient coverage.)\n`);
|
|
@@ -252,9 +255,9 @@ export async function initCommand(opts) {
|
|
|
252
255
|
console.log(` ${chalk.dim('├──')} agentxchain.json ${chalk.dim(`(${agentCount} agents)`)}`);
|
|
253
256
|
console.log(` ${chalk.dim('├──')} lock.json`);
|
|
254
257
|
console.log(` ${chalk.dim('├──')} state.json / state.md / history.jsonl`);
|
|
255
|
-
console.log(` ${chalk.dim('├──')} log.md / HUMAN_TASKS.md`);
|
|
258
|
+
console.log(` ${chalk.dim('├──')} TALK.md / log.md / HUMAN_TASKS.md`);
|
|
256
259
|
console.log(` ${chalk.dim('├──')} .planning/`);
|
|
257
|
-
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} PROJECT.md / REQUIREMENTS.md / ROADMAP.md`);
|
|
260
|
+
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} PROJECT.md / REQUIREMENTS.md / ROADMAP.md / PM_SIGNOFF.md`);
|
|
258
261
|
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} research/ / phases/`);
|
|
259
262
|
console.log(` ${chalk.dim('│')} ${chalk.dim('└──')} qa/ ${chalk.dim('TEST-COVERAGE / BUGS / UX-AUDIT / ACCEPTANCE-MATRIX')}`);
|
|
260
263
|
console.log(` ${chalk.dim('├──')} .github/agents/ ${chalk.dim(`(${agentCount} .agent.md files)`)}`);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { loadConfig } from '../lib/config.js';
|
|
4
|
+
import { validateProject } from '../lib/validation.js';
|
|
5
|
+
import { startCommand } from './start.js';
|
|
6
|
+
import { releaseCommand } from './claim.js';
|
|
7
|
+
import { superviseCommand } from './supervise.js';
|
|
8
|
+
|
|
9
|
+
export async function kickoffCommand(opts) {
|
|
10
|
+
const result = loadConfig();
|
|
11
|
+
if (!result) {
|
|
12
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { root, config } = result;
|
|
17
|
+
const pmId = pickPmAgentId(config);
|
|
18
|
+
const ide = opts.ide || 'cursor';
|
|
19
|
+
|
|
20
|
+
if (!pmId) {
|
|
21
|
+
console.log(chalk.red('No agents configured.'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(chalk.bold(' AgentXchain Kickoff Wizard'));
|
|
27
|
+
console.log(chalk.dim(` Project: ${config.project}`));
|
|
28
|
+
console.log(chalk.dim(` PM agent: ${pmId}`));
|
|
29
|
+
console.log(chalk.dim(` IDE: ${ide}`));
|
|
30
|
+
console.log('');
|
|
31
|
+
|
|
32
|
+
await startCommand({ ide, agent: pmId, remaining: false, dryRun: false });
|
|
33
|
+
|
|
34
|
+
console.log(chalk.cyan(' PM kickoff launched.'));
|
|
35
|
+
console.log(chalk.dim(' Discuss product scope with PM, then mark .planning/PM_SIGNOFF.md as Approved: YES.'));
|
|
36
|
+
console.log('');
|
|
37
|
+
|
|
38
|
+
const { readyForValidation } = await inquirer.prompt([{
|
|
39
|
+
type: 'confirm',
|
|
40
|
+
name: 'readyForValidation',
|
|
41
|
+
message: 'Is PM kickoff complete and signoff updated?',
|
|
42
|
+
default: false
|
|
43
|
+
}]);
|
|
44
|
+
|
|
45
|
+
if (!readyForValidation) {
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(chalk.yellow(' Kickoff paused.'));
|
|
48
|
+
console.log(chalk.dim(' Resume later with: agentxchain validate --mode kickoff && agentxchain start --remaining'));
|
|
49
|
+
console.log('');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const validation = validateProject(root, config, { mode: 'kickoff' });
|
|
54
|
+
if (!validation.ok) {
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(chalk.red(' Kickoff validation failed.'));
|
|
57
|
+
for (const err of validation.errors) {
|
|
58
|
+
console.log(chalk.dim(` - ${err}`));
|
|
59
|
+
}
|
|
60
|
+
if (validation.warnings.length > 0) {
|
|
61
|
+
console.log(chalk.yellow(' Warnings:'));
|
|
62
|
+
for (const warn of validation.warnings) {
|
|
63
|
+
console.log(chalk.dim(` - ${warn}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(chalk.bold(' Run: agentxchain validate --mode kickoff'));
|
|
68
|
+
console.log('');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(chalk.green(' ✓ Kickoff validation passed.'));
|
|
73
|
+
console.log('');
|
|
74
|
+
|
|
75
|
+
await startCommand({ ide, remaining: true, agent: undefined, dryRun: false });
|
|
76
|
+
|
|
77
|
+
const { doRelease } = await inquirer.prompt([{
|
|
78
|
+
type: 'confirm',
|
|
79
|
+
name: 'doRelease',
|
|
80
|
+
message: 'Release human lock now so agents can start?',
|
|
81
|
+
default: true
|
|
82
|
+
}]);
|
|
83
|
+
|
|
84
|
+
if (doRelease) {
|
|
85
|
+
await releaseCommand({ force: false });
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.dim(' Lock remains with human. Run `agentxchain release` when ready.'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (ide === 'cursor' && opts.autonudge !== false) {
|
|
91
|
+
const { startSupervisor } = await inquirer.prompt([{
|
|
92
|
+
type: 'confirm',
|
|
93
|
+
name: 'startSupervisor',
|
|
94
|
+
message: 'Start supervisor with auto-nudge now?',
|
|
95
|
+
default: true
|
|
96
|
+
}]);
|
|
97
|
+
|
|
98
|
+
if (startSupervisor) {
|
|
99
|
+
await superviseCommand({
|
|
100
|
+
autonudge: true,
|
|
101
|
+
send: !!opts.send,
|
|
102
|
+
interval: opts.interval || '3'
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(chalk.dim(' Start later with: agentxchain supervise --autonudge'));
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function pickPmAgentId(config) {
|
|
113
|
+
if (config.agents?.pm) return 'pm';
|
|
114
|
+
for (const [id, def] of Object.entries(config.agents || {})) {
|
|
115
|
+
const name = String(def?.name || '').toLowerCase();
|
|
116
|
+
if (name.includes('product manager')) return id;
|
|
117
|
+
}
|
|
118
|
+
return Object.keys(config.agents || {})[0] || null;
|
|
119
|
+
}
|
package/src/commands/start.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig } from '../lib/config.js';
|
|
3
|
+
import { validateProject } from '../lib/validation.js';
|
|
3
4
|
|
|
4
5
|
export async function startCommand(opts) {
|
|
5
6
|
const result = loadConfig();
|
|
@@ -30,6 +31,22 @@ export async function startCommand(opts) {
|
|
|
30
31
|
process.exit(1);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
if (opts.remaining) {
|
|
35
|
+
const kickoffValidation = validateProject(root, config, { mode: 'kickoff' });
|
|
36
|
+
if (!kickoffValidation.ok) {
|
|
37
|
+
console.log(chalk.red(' PM kickoff is incomplete. Cannot run --remaining yet.'));
|
|
38
|
+
console.log(chalk.dim(' Fix these first:'));
|
|
39
|
+
for (const e of kickoffValidation.errors) {
|
|
40
|
+
console.log(chalk.dim(` - ${e}`));
|
|
41
|
+
}
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(chalk.dim(' Suggested next step: complete .planning/PM_SIGNOFF.md and roadmap waves/phases, then run:'));
|
|
44
|
+
console.log(chalk.bold(' agentxchain validate --mode kickoff'));
|
|
45
|
+
console.log('');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
33
50
|
const launchConfig = buildLaunchConfig(config, opts);
|
|
34
51
|
const launchAgentIds = Object.keys(launchConfig.agents || {});
|
|
35
52
|
const launchCount = launchAgentIds.length;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { loadConfig } from '../lib/config.js';
|
|
3
|
+
import { validateProject } from '../lib/validation.js';
|
|
4
|
+
|
|
5
|
+
export async function validateCommand(opts) {
|
|
6
|
+
const result = loadConfig();
|
|
7
|
+
if (!result) {
|
|
8
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { root, config } = result;
|
|
13
|
+
const mode = opts.mode || 'full';
|
|
14
|
+
const validation = validateProject(root, config, {
|
|
15
|
+
mode,
|
|
16
|
+
expectedAgent: opts.agent || null
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
console.log(JSON.stringify(validation, null, 2));
|
|
21
|
+
} else {
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(chalk.bold(` AgentXchain Validate (${mode})`));
|
|
24
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
25
|
+
console.log(chalk.dim(` Root: ${root}`));
|
|
26
|
+
console.log('');
|
|
27
|
+
|
|
28
|
+
if (validation.ok) {
|
|
29
|
+
console.log(chalk.green(' ✓ Validation passed.'));
|
|
30
|
+
} else {
|
|
31
|
+
console.log(chalk.red(` ✗ Validation failed (${validation.errors.length} errors).`));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (validation.errors.length > 0) {
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(chalk.red(' Errors:'));
|
|
37
|
+
for (const e of validation.errors) console.log(` - ${e}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (validation.warnings.length > 0) {
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log(chalk.yellow(' Warnings:'));
|
|
43
|
+
for (const w of validation.warnings) console.log(` - ${w}`);
|
|
44
|
+
}
|
|
45
|
+
console.log('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!validation.ok) process.exit(1);
|
|
49
|
+
}
|
package/src/commands/watch.js
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
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
|
+
import { validateProject } from '../lib/validation.js';
|
|
8
9
|
|
|
9
10
|
export async function watchCommand(opts) {
|
|
10
11
|
if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
|
|
@@ -82,6 +83,20 @@ export async function watchCommand(opts) {
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (stateKey !== lastState) {
|
|
86
|
+
if (lock.last_released_by && config.agents?.[lock.last_released_by]) {
|
|
87
|
+
const validation = validateProject(root, config, {
|
|
88
|
+
mode: 'turn',
|
|
89
|
+
expectedAgent: lock.last_released_by
|
|
90
|
+
});
|
|
91
|
+
if (!validation.ok) {
|
|
92
|
+
log('warn', `Validation failed after ${lock.last_released_by}. Handing lock to HUMAN.`);
|
|
93
|
+
blockOnValidation(root, lock, config, validation);
|
|
94
|
+
sendNotification('Validation failed. Human action required: run agentxchain validate.');
|
|
95
|
+
lastState = null;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
const next = pickNextAgent(lock, config);
|
|
86
101
|
log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Next: ${chalk.bold(next)}.`);
|
|
87
102
|
writeTrigger(root, next, lock, config);
|
|
@@ -142,6 +157,41 @@ function writeTrigger(root, agentId, lock, config) {
|
|
|
142
157
|
}, null, 2) + '\n');
|
|
143
158
|
}
|
|
144
159
|
|
|
160
|
+
function blockOnValidation(root, lock, config, validation) {
|
|
161
|
+
const lockPath = join(root, LOCK_FILE);
|
|
162
|
+
const newLock = {
|
|
163
|
+
holder: 'human',
|
|
164
|
+
last_released_by: lock.last_released_by,
|
|
165
|
+
turn_number: lock.turn_number,
|
|
166
|
+
claimed_at: new Date().toISOString()
|
|
167
|
+
};
|
|
168
|
+
writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
|
|
169
|
+
|
|
170
|
+
const statePath = join(root, 'state.json');
|
|
171
|
+
if (existsSync(statePath)) {
|
|
172
|
+
try {
|
|
173
|
+
const current = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
174
|
+
const message = validation.errors[0] || 'Validation failed';
|
|
175
|
+
const nextState = {
|
|
176
|
+
...current,
|
|
177
|
+
blocked: true,
|
|
178
|
+
blocked_on: `validation: ${message}`
|
|
179
|
+
};
|
|
180
|
+
writeFileSync(statePath, JSON.stringify(nextState, null, 2) + '\n');
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const logFile = config.log || 'log.md';
|
|
185
|
+
const logPath = join(root, logFile);
|
|
186
|
+
if (existsSync(logPath)) {
|
|
187
|
+
const summary = validation.errors.map(e => `- ${e}`).join('\n');
|
|
188
|
+
appendFileSync(
|
|
189
|
+
logPath,
|
|
190
|
+
`\n---\n\n### [system] (Watch Validation) | Turn ${lock.turn_number}\n\n**Status:** Validation failed after ${lock.last_released_by}.\n\n**Action:** Lock assigned to human for intervention.\n\n**Errors:**\n${summary}\n\n`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
145
195
|
function log(type, msg) {
|
|
146
196
|
const time = new Date().toLocaleTimeString();
|
|
147
197
|
const tags = {
|
|
@@ -15,7 +15,7 @@ export function generateVSCodeFiles(dir, config) {
|
|
|
15
15
|
|
|
16
16
|
for (const id of agentIds) {
|
|
17
17
|
const agent = config.agents[id];
|
|
18
|
-
const md = buildAgentMd(id, agent, config, agentIds);
|
|
18
|
+
const md = buildAgentMd(id, agent, config, agentIds, dir);
|
|
19
19
|
writeFileSync(join(agentsDir, `${id}.agent.md`), md);
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -33,13 +33,14 @@ export function generateVSCodeFiles(dir, config) {
|
|
|
33
33
|
return { agentsDir, hooksDir, scriptsDir, agentCount: agentIds.length };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function buildAgentMd(agentId, agentDef, config, allAgentIds) {
|
|
36
|
+
function buildAgentMd(agentId, agentDef, config, allAgentIds, projectRoot) {
|
|
37
37
|
const otherAgents = allAgentIds.filter(id => id !== agentId);
|
|
38
38
|
const verifyCmd = config.rules?.verify_command || null;
|
|
39
39
|
const maxClaims = config.rules?.max_consecutive_claims || 2;
|
|
40
40
|
const stateFile = config.state_file || 'state.md';
|
|
41
41
|
const historyFile = config.history_file || 'history.jsonl';
|
|
42
42
|
const logFile = config.log || 'log.md';
|
|
43
|
+
const talkFile = config.talk_file || 'TALK.md';
|
|
43
44
|
const useSplit = config.state_file || config.history_file;
|
|
44
45
|
|
|
45
46
|
const handoffs = otherAgents.map(otherId => {
|
|
@@ -77,10 +78,12 @@ hooks:
|
|
|
77
78
|
? `Read these files at the start of your turn:
|
|
78
79
|
- \`${stateFile}\` — living project state (primary context)
|
|
79
80
|
- \`${historyFile}\` — last 3 lines for recent turns
|
|
81
|
+
- \`${talkFile}\` — team handoff updates (read latest entries)
|
|
80
82
|
- \`lock.json\` — current lock holder
|
|
81
83
|
- \`state.json\` — phase and blocked status`
|
|
82
84
|
: `Read these files at the start of your turn:
|
|
83
85
|
- \`${logFile}\` — message log (read last few messages)
|
|
86
|
+
- \`${talkFile}\` — team handoff updates (read latest entries)
|
|
84
87
|
- \`lock.json\` — current lock holder
|
|
85
88
|
- \`state.json\` — phase and blocked status`;
|
|
86
89
|
|
|
@@ -90,13 +93,15 @@ hooks:
|
|
|
90
93
|
2. Overwrite \`${stateFile}\` with current project state.
|
|
91
94
|
3. Append one line to \`${historyFile}\`:
|
|
92
95
|
\`{"turn": N, "agent": "${agentId}", "summary": "...", "files_changed": [...], "verify_result": "pass|fail|skipped", "timestamp": "ISO8601"}\`
|
|
93
|
-
4.
|
|
96
|
+
4. Append one handoff entry to \`${talkFile}\` with: Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
97
|
+
5. Update \`state.json\` if phase or blocked status changed.`
|
|
94
98
|
: `When you finish your work, write in this order:
|
|
95
99
|
1. Your actual work: code, files, commands, decisions.
|
|
96
100
|
2. Append one message to \`${logFile}\`:
|
|
97
101
|
\`### [${agentId}] (${agentDef.name}) | Turn N\`
|
|
98
102
|
with Status, Decision, Action, Next sections.
|
|
99
|
-
3.
|
|
103
|
+
3. Append one handoff entry to \`${talkFile}\` with: Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
104
|
+
4. Update \`state.json\` if phase or blocked status changed.`;
|
|
100
105
|
|
|
101
106
|
const verifyInstructions = verifyCmd
|
|
102
107
|
? `\n## Verify before release\nBefore releasing the lock, run: \`${verifyCmd}\`\nIf it fails, fix the problem and run again. Do NOT release with a failing verification.`
|
|
@@ -110,6 +115,15 @@ ${agentDef.mandate}
|
|
|
110
115
|
|
|
111
116
|
---
|
|
112
117
|
|
|
118
|
+
## Project boundary
|
|
119
|
+
|
|
120
|
+
- Project root: \`${projectRoot}\`
|
|
121
|
+
- Work only inside this project folder.
|
|
122
|
+
- Never scan unrelated local directories.
|
|
123
|
+
- Start your turn by checking \`pwd\`.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
113
127
|
## Project documentation
|
|
114
128
|
|
|
115
129
|
Read the files relevant to your role in the \`.planning/\` folder:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export function generatePollingPrompt(agentId, agentDef, config) {
|
|
1
|
+
export function generatePollingPrompt(agentId, agentDef, config, projectRoot = '.') {
|
|
2
2
|
const logFile = config.log || 'log.md';
|
|
3
|
+
const talkFile = config.talk_file || 'TALK.md';
|
|
3
4
|
const maxClaims = config.rules?.max_consecutive_claims || 2;
|
|
4
5
|
const verifyCmd = config.rules?.verify_command || null;
|
|
5
6
|
const stateFile = config.state_file || 'state.md';
|
|
@@ -19,10 +20,12 @@ export function generatePollingPrompt(agentId, agentDef, config) {
|
|
|
19
20
|
? `READ THESE FILES:
|
|
20
21
|
- "${stateFile}" — the living project state. Read fully. Primary context.
|
|
21
22
|
- "${historyFile}" — turn history. Read last 3 lines for recent context.
|
|
23
|
+
- "${talkFile}" — team handoff updates. Read the latest 5 entries.
|
|
22
24
|
- lock.json — who holds the lock.
|
|
23
25
|
- state.json — phase and blocked status.`
|
|
24
26
|
: `READ THESE FILES:
|
|
25
27
|
- "${logFile}" — the message log. Read last few messages.
|
|
28
|
+
- "${talkFile}" — team handoff updates. Read the latest 5 entries.
|
|
26
29
|
- lock.json — who holds the lock.
|
|
27
30
|
- state.json — phase and blocked status.`;
|
|
28
31
|
|
|
@@ -32,7 +35,9 @@ a. Do your actual work: write code, create files, run commands, make decisions.
|
|
|
32
35
|
b. Update "${stateFile}" — OVERWRITE with current project state.
|
|
33
36
|
c. Append ONE line to "${historyFile}":
|
|
34
37
|
{"turn": N, "agent": "${agentId}", "summary": "what you did", "files_changed": [...], "verify_result": "pass|fail|skipped", "timestamp": "ISO8601"}
|
|
35
|
-
d.
|
|
38
|
+
d. Append ONE handoff entry to "${talkFile}" with:
|
|
39
|
+
Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
40
|
+
e. Update state.json if phase or blocked status changed.`
|
|
36
41
|
: `WRITE (in this order):
|
|
37
42
|
a. Do your actual work: write code, create files, run commands, make decisions.
|
|
38
43
|
b. Append ONE message to ${logFile}:
|
|
@@ -42,7 +47,9 @@ b. Append ONE message to ${logFile}:
|
|
|
42
47
|
**Decision:** What you decided and why.
|
|
43
48
|
**Action:** What you did. Commands, files, results.
|
|
44
49
|
**Next:** What the next agent should focus on.
|
|
45
|
-
c.
|
|
50
|
+
c. Append ONE handoff entry to "${talkFile}" with:
|
|
51
|
+
Turn, Status, Decision, Action, Risks/Questions, Next owner.
|
|
52
|
+
d. Update state.json if phase or blocked status changed.`;
|
|
46
53
|
|
|
47
54
|
const verifySection = verifyCmd
|
|
48
55
|
? `
|
|
@@ -58,6 +65,15 @@ ${agentDef.mandate}
|
|
|
58
65
|
|
|
59
66
|
---
|
|
60
67
|
|
|
68
|
+
PROJECT ROOT (strict boundary):
|
|
69
|
+
- Absolute project root: "${projectRoot}"
|
|
70
|
+
- You MUST work only inside this project root.
|
|
71
|
+
- Do NOT scan your home directory or unrelated folders.
|
|
72
|
+
- If unsure, run: pwd
|
|
73
|
+
- If not in project root, run: cd "${projectRoot}"
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
61
77
|
PROJECT DOCUMENTATION (.planning/ folder):
|
|
62
78
|
|
|
63
79
|
These files give you project context. Read the ones relevant to your role.
|
|
@@ -75,6 +91,14 @@ These files give you project context. Read the ones relevant to your role.
|
|
|
75
91
|
|
|
76
92
|
When your role requires it, CREATE or UPDATE these files.
|
|
77
93
|
|
|
94
|
+
GET SHIT DONE FRAMEWORK (mandatory):
|
|
95
|
+
- Plan in waves and phases (not ad hoc tasks).
|
|
96
|
+
- Keep .planning/ROADMAP.md updated with explicit Wave and Phase sections.
|
|
97
|
+
- For every active phase, maintain:
|
|
98
|
+
- .planning/phases/phase-N/PLAN.md
|
|
99
|
+
- .planning/phases/phase-N/TESTS.md
|
|
100
|
+
- QA must keep acceptance matrix and UX audit current with evidence, not placeholders.
|
|
101
|
+
|
|
78
102
|
---
|
|
79
103
|
|
|
80
104
|
TEAM ROTATION: ${agentIds.join(' → ')} → (repeat)
|
|
@@ -85,6 +109,11 @@ ${turnCondition}
|
|
|
85
109
|
|
|
86
110
|
YOUR LOOP (run forever, never exit, never say "I'm done"):
|
|
87
111
|
|
|
112
|
+
0. CHECK WORKING DIRECTORY:
|
|
113
|
+
- Run: pwd
|
|
114
|
+
- If not inside "${projectRoot}", run: cd "${projectRoot}"
|
|
115
|
+
- Never run broad searches outside this project root.
|
|
116
|
+
|
|
88
117
|
1. READ lock.json.
|
|
89
118
|
|
|
90
119
|
2. CHECK — is it my turn?
|
|
@@ -108,6 +137,10 @@ YOUR LOOP (run forever, never exit, never say "I'm done"):
|
|
|
108
137
|
|
|
109
138
|
${writeSection}${verifySection}
|
|
110
139
|
|
|
140
|
+
VALIDATE (mandatory before release):
|
|
141
|
+
Run: agentxchain validate --mode turn --agent ${agentId}
|
|
142
|
+
If validation fails, fix docs/artifacts first. Do NOT release.
|
|
143
|
+
|
|
111
144
|
5. RELEASE the lock:
|
|
112
145
|
Write lock.json: {"holder":null,"last_released_by":"${agentId}","turn_number":<previous + 1>,"claimed_at":null}
|
|
113
146
|
THIS MUST BE THE LAST FILE YOU WRITE.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export function validateProject(root, config, opts = {}) {
|
|
5
|
+
const mode = opts.mode || 'full';
|
|
6
|
+
const expectedAgent = opts.expectedAgent || null;
|
|
7
|
+
const talkFile = config.talk_file || 'TALK.md';
|
|
8
|
+
|
|
9
|
+
const errors = [];
|
|
10
|
+
const warnings = [];
|
|
11
|
+
|
|
12
|
+
const mustExist = [
|
|
13
|
+
'.planning/PROJECT.md',
|
|
14
|
+
'.planning/REQUIREMENTS.md',
|
|
15
|
+
'.planning/ROADMAP.md',
|
|
16
|
+
'.planning/PM_SIGNOFF.md',
|
|
17
|
+
'.planning/qa/TEST-COVERAGE.md',
|
|
18
|
+
'.planning/qa/BUGS.md',
|
|
19
|
+
'.planning/qa/UX-AUDIT.md',
|
|
20
|
+
'.planning/qa/ACCEPTANCE-MATRIX.md',
|
|
21
|
+
'.planning/qa/REGRESSION-LOG.md',
|
|
22
|
+
talkFile,
|
|
23
|
+
'state.md',
|
|
24
|
+
'history.jsonl',
|
|
25
|
+
'lock.json',
|
|
26
|
+
'state.json'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const rel of mustExist) {
|
|
30
|
+
if (!existsSync(join(root, rel))) {
|
|
31
|
+
errors.push(`Missing required file: ${rel}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const signoff = readText(root, '.planning/PM_SIGNOFF.md');
|
|
36
|
+
const signoffApproved = /approved\s*:\s*yes/i.test(signoff || '');
|
|
37
|
+
if (!signoffApproved) {
|
|
38
|
+
if (mode === 'kickoff' || mode === 'full') {
|
|
39
|
+
errors.push('PM signoff is not approved. Set "Approved: YES" in .planning/PM_SIGNOFF.md.');
|
|
40
|
+
} else {
|
|
41
|
+
warnings.push('PM signoff is not approved.');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const roadmap = readText(root, '.planning/ROADMAP.md') || '';
|
|
46
|
+
if (!/\bwave\b/i.test(roadmap)) {
|
|
47
|
+
errors.push('ROADMAP.md must define at least one Wave.');
|
|
48
|
+
}
|
|
49
|
+
if (!/\bphase\b/i.test(roadmap)) {
|
|
50
|
+
errors.push('ROADMAP.md must define at least one Phase.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const phaseStatus = validatePhaseArtifacts(root);
|
|
54
|
+
errors.push(...phaseStatus.errors);
|
|
55
|
+
warnings.push(...phaseStatus.warnings);
|
|
56
|
+
|
|
57
|
+
const history = validateHistory(root, config, { expectedAgent, requireEntry: mode !== 'kickoff' });
|
|
58
|
+
errors.push(...history.errors);
|
|
59
|
+
warnings.push(...history.warnings);
|
|
60
|
+
|
|
61
|
+
const talk = validateTalkFile(root, talkFile, { requireEntry: mode !== 'kickoff' });
|
|
62
|
+
errors.push(...talk.errors);
|
|
63
|
+
warnings.push(...talk.warnings);
|
|
64
|
+
|
|
65
|
+
const qaSignals = validateQaArtifacts(root);
|
|
66
|
+
warnings.push(...qaSignals.warnings);
|
|
67
|
+
|
|
68
|
+
return { ok: errors.length === 0, mode, errors, warnings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validatePhaseArtifacts(root) {
|
|
72
|
+
const result = { errors: [], warnings: [] };
|
|
73
|
+
const phasesDir = join(root, '.planning', 'phases');
|
|
74
|
+
if (!existsSync(phasesDir)) {
|
|
75
|
+
result.errors.push('Missing .planning/phases directory.');
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries = safeReadDir(phasesDir).filter(name => !name.startsWith('.'));
|
|
80
|
+
if (entries.length === 0) {
|
|
81
|
+
result.errors.push('No phase artifacts found. Add .planning/phases/phase-*/PLAN.md and TESTS.md.');
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let hasAnyPlan = false;
|
|
86
|
+
let hasAnyTests = false;
|
|
87
|
+
for (const name of entries) {
|
|
88
|
+
const plan = join(phasesDir, name, 'PLAN.md');
|
|
89
|
+
const tests = join(phasesDir, name, 'TESTS.md');
|
|
90
|
+
if (existsSync(plan)) hasAnyPlan = true;
|
|
91
|
+
if (existsSync(tests)) hasAnyTests = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!hasAnyPlan) result.errors.push('No phase PLAN.md found under .planning/phases/.');
|
|
95
|
+
if (!hasAnyTests) result.errors.push('No phase TESTS.md found under .planning/phases/.');
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateHistory(root, config, opts) {
|
|
100
|
+
const result = { errors: [], warnings: [] };
|
|
101
|
+
const historyPath = join(root, 'history.jsonl');
|
|
102
|
+
if (!existsSync(historyPath)) {
|
|
103
|
+
result.errors.push('history.jsonl is missing.');
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = (readFileSync(historyPath, 'utf8') || '')
|
|
108
|
+
.split(/\r?\n/)
|
|
109
|
+
.map(l => l.trim())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
|
|
112
|
+
if (lines.length === 0) {
|
|
113
|
+
if (opts.requireEntry) {
|
|
114
|
+
result.errors.push('history.jsonl has no entries.');
|
|
115
|
+
} else {
|
|
116
|
+
result.warnings.push('history.jsonl has no entries yet.');
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const lastRaw = lines[lines.length - 1];
|
|
122
|
+
let last;
|
|
123
|
+
try {
|
|
124
|
+
last = JSON.parse(lastRaw);
|
|
125
|
+
} catch {
|
|
126
|
+
result.errors.push('Last history.jsonl entry is not valid JSON.');
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const requiredFields = ['turn', 'agent', 'summary', 'files_changed', 'verify_result', 'timestamp'];
|
|
131
|
+
for (const f of requiredFields) {
|
|
132
|
+
if (!(f in last)) result.errors.push(`Last history entry missing field: ${f}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (last.agent && !config.agents?.[last.agent]) {
|
|
136
|
+
result.errors.push(`Last history agent "${last.agent}" is not in agentxchain.json.`);
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(last.files_changed)) {
|
|
139
|
+
result.errors.push('Last history entry: files_changed must be an array.');
|
|
140
|
+
}
|
|
141
|
+
if (last.verify_result && !['pass', 'fail', 'skipped'].includes(last.verify_result)) {
|
|
142
|
+
result.errors.push('Last history entry: verify_result must be pass|fail|skipped.');
|
|
143
|
+
}
|
|
144
|
+
if (opts.expectedAgent && last.agent !== opts.expectedAgent) {
|
|
145
|
+
result.errors.push(`Last history entry agent "${last.agent}" does not match expected "${opts.expectedAgent}".`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateQaArtifacts(root) {
|
|
152
|
+
const result = { warnings: [] };
|
|
153
|
+
const checks = [
|
|
154
|
+
['.planning/qa/TEST-COVERAGE.md', /\(QA fills this as testing progresses\)/i, 'TEST-COVERAGE.md still looks uninitialized'],
|
|
155
|
+
['.planning/qa/ACCEPTANCE-MATRIX.md', /\(QA fills this from REQUIREMENTS\.md\)/i, 'ACCEPTANCE-MATRIX.md still looks uninitialized'],
|
|
156
|
+
['.planning/qa/UX-AUDIT.md', /\(QA adds UX issues here\)/i, 'UX-AUDIT.md issues table appears unmodified']
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
for (const [rel, placeholderRegex, message] of checks) {
|
|
160
|
+
const text = readText(root, rel);
|
|
161
|
+
if (text && placeholderRegex.test(text)) {
|
|
162
|
+
result.warnings.push(message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateTalkFile(root, talkFile, opts) {
|
|
170
|
+
const result = { errors: [], warnings: [] };
|
|
171
|
+
const text = readText(root, talkFile);
|
|
172
|
+
if (!text) {
|
|
173
|
+
result.errors.push(`${talkFile} is missing or unreadable.`);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const hasTurnEntry = /##\s*Turn\s+\d+/i.test(text);
|
|
178
|
+
if (!hasTurnEntry) {
|
|
179
|
+
if (opts.requireEntry) {
|
|
180
|
+
result.errors.push(`${talkFile} has no turn entries.`);
|
|
181
|
+
} else {
|
|
182
|
+
result.warnings.push(`${talkFile} has no turn entries yet.`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readText(root, rel) {
|
|
190
|
+
const path = join(root, rel);
|
|
191
|
+
if (!existsSync(path)) return null;
|
|
192
|
+
try {
|
|
193
|
+
return readFileSync(path, 'utf8');
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function safeReadDir(path) {
|
|
200
|
+
try {
|
|
201
|
+
return readdirSync(path);
|
|
202
|
+
} catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|