agentxchain 0.8.3 → 0.8.4
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 +13 -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 +6 -2
- package/src/commands/claim.js +17 -1
- package/src/commands/doctor.js +14 -0
- package/src/commands/init.js +2 -1
- 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/seed-prompt-polling.js +12 -0
- package/src/lib/validation.js +179 -0
package/README.md
CHANGED
|
@@ -20,18 +20,9 @@ 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.
|
|
@@ -41,9 +32,11 @@ Each agent runs in its own Cursor window with a self-polling loop. Agents check
|
|
|
41
32
|
| Command | What it does |
|
|
42
33
|
|---------|-------------|
|
|
43
34
|
| `init` | Create project folder with agents, protocol files, and templates |
|
|
35
|
+
| `kickoff` | Guided PM-first flow: PM kickoff, validate, launch remaining, release |
|
|
44
36
|
| `start` | Open a Cursor window per agent + copy prompts to clipboard |
|
|
45
37
|
| `supervise` | Run watcher and optional AppleScript auto-nudge together |
|
|
46
38
|
| `generate` | Regenerate agent files from `agentxchain.json` |
|
|
39
|
+
| `validate` | Enforce PM signoff + waves/phases + turn artifact schema |
|
|
47
40
|
| `status` | Show lock holder, phase, turn number, agents |
|
|
48
41
|
| `doctor` | Validate local setup (tools, trigger flow, accessibility checks) |
|
|
49
42
|
| `claim` | Human takes control (agents stop claiming) |
|
|
@@ -64,9 +57,15 @@ agentxchain start --ide claude-code # Claude Code — spawns CLI processes
|
|
|
64
57
|
### Additional flags
|
|
65
58
|
|
|
66
59
|
```bash
|
|
60
|
+
agentxchain kickoff # guided first-run PM-first workflow
|
|
61
|
+
agentxchain kickoff --ide vscode # guided flow for VS Code mode
|
|
62
|
+
agentxchain kickoff --send # with Cursor auto-nudge auto-send enabled
|
|
63
|
+
|
|
67
64
|
agentxchain start --agent pm # launch only one specific agent
|
|
68
65
|
agentxchain start --remaining # launch all agents except PM (PM-first flow)
|
|
69
66
|
agentxchain start --dry-run # preview agents without launching
|
|
67
|
+
agentxchain validate --mode kickoff # required before --remaining
|
|
68
|
+
agentxchain validate --mode turn --agent pm
|
|
70
69
|
agentxchain watch --daemon # run watch in background
|
|
71
70
|
agentxchain supervise --autonudge # run watch + AppleScript nudge loop
|
|
72
71
|
agentxchain supervise --autonudge --send # auto-press Enter after paste
|
|
@@ -110,14 +109,15 @@ Notes:
|
|
|
110
109
|
- Grant Accessibility permissions to Terminal and Cursor
|
|
111
110
|
- The script watches `.agentxchain-trigger.json`, which is written by `agentxchain watch`
|
|
112
111
|
- `run-autonudge.sh` now requires watch to be running first
|
|
112
|
+
- The script only nudges when it finds a unique matching agent window (no random fallback)
|
|
113
113
|
|
|
114
114
|
## How it works
|
|
115
115
|
|
|
116
116
|
### Cursor mode (default)
|
|
117
117
|
|
|
118
|
-
1. `agentxchain
|
|
118
|
+
1. `agentxchain kickoff` launches PM first for human-product alignment
|
|
119
119
|
2. Each window gets a unique prompt copied to clipboard
|
|
120
|
-
3.
|
|
120
|
+
3. Kickoff validates PM signoff and launches remaining agents
|
|
121
121
|
4. Agent prompts include a self-polling loop: read `lock.json` → check if it's my turn → claim → work → release → sleep 60s → repeat
|
|
122
122
|
5. Agents know their rotation order from `agentxchain.json` and only claim when the previous agent released
|
|
123
123
|
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
|
|
@@ -171,8 +171,12 @@ Actions:
|
|
|
171
171
|
- core workflow
|
|
172
172
|
- MVP boundary
|
|
173
173
|
- success metric
|
|
174
|
-
3) Update planning docs with concrete acceptance criteria
|
|
175
|
-
|
|
174
|
+
3) Update planning docs with concrete acceptance criteria and Get Shit Done structure:
|
|
175
|
+
- .planning/ROADMAP.md must define Waves and Phases.
|
|
176
|
+
- Create .planning/phases/phase-1/PLAN.md and TESTS.md.
|
|
177
|
+
4) Update .planning/PM_SIGNOFF.md:
|
|
178
|
+
- Set "Approved: YES" only when human agrees kickoff is complete.
|
|
179
|
+
5) Do NOT start round-robin agent handoffs yet.
|
|
176
180
|
|
|
177
181
|
Context:
|
|
178
182
|
- 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
|
@@ -229,6 +229,7 @@ export async function initCommand(opts) {
|
|
|
229
229
|
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
230
|
|
|
231
231
|
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`);
|
|
232
|
+
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
233
|
|
|
233
234
|
// QA structure
|
|
234
235
|
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`);
|
|
@@ -254,7 +255,7 @@ export async function initCommand(opts) {
|
|
|
254
255
|
console.log(` ${chalk.dim('├──')} state.json / state.md / history.jsonl`);
|
|
255
256
|
console.log(` ${chalk.dim('├──')} log.md / HUMAN_TASKS.md`);
|
|
256
257
|
console.log(` ${chalk.dim('├──')} .planning/`);
|
|
257
|
-
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} PROJECT.md / REQUIREMENTS.md / ROADMAP.md`);
|
|
258
|
+
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} PROJECT.md / REQUIREMENTS.md / ROADMAP.md / PM_SIGNOFF.md`);
|
|
258
259
|
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} research/ / phases/`);
|
|
259
260
|
console.log(` ${chalk.dim('│')} ${chalk.dim('└──')} qa/ ${chalk.dim('TEST-COVERAGE / BUGS / UX-AUDIT / ACCEPTANCE-MATRIX')}`);
|
|
260
261
|
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 = {
|
|
@@ -75,6 +75,14 @@ These files give you project context. Read the ones relevant to your role.
|
|
|
75
75
|
|
|
76
76
|
When your role requires it, CREATE or UPDATE these files.
|
|
77
77
|
|
|
78
|
+
GET SHIT DONE FRAMEWORK (mandatory):
|
|
79
|
+
- Plan in waves and phases (not ad hoc tasks).
|
|
80
|
+
- Keep .planning/ROADMAP.md updated with explicit Wave and Phase sections.
|
|
81
|
+
- For every active phase, maintain:
|
|
82
|
+
- .planning/phases/phase-N/PLAN.md
|
|
83
|
+
- .planning/phases/phase-N/TESTS.md
|
|
84
|
+
- QA must keep acceptance matrix and UX audit current with evidence, not placeholders.
|
|
85
|
+
|
|
78
86
|
---
|
|
79
87
|
|
|
80
88
|
TEAM ROTATION: ${agentIds.join(' → ')} → (repeat)
|
|
@@ -108,6 +116,10 @@ YOUR LOOP (run forever, never exit, never say "I'm done"):
|
|
|
108
116
|
|
|
109
117
|
${writeSection}${verifySection}
|
|
110
118
|
|
|
119
|
+
VALIDATE (mandatory before release):
|
|
120
|
+
Run: agentxchain validate --mode turn --agent ${agentId}
|
|
121
|
+
If validation fails, fix docs/artifacts first. Do NOT release.
|
|
122
|
+
|
|
111
123
|
5. RELEASE the lock:
|
|
112
124
|
Write lock.json: {"holder":null,"last_released_by":"${agentId}","turn_number":<previous + 1>,"claimed_at":null}
|
|
113
125
|
THIS MUST BE THE LAST FILE YOU WRITE.
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
|
|
8
|
+
const errors = [];
|
|
9
|
+
const warnings = [];
|
|
10
|
+
|
|
11
|
+
const mustExist = [
|
|
12
|
+
'.planning/PROJECT.md',
|
|
13
|
+
'.planning/REQUIREMENTS.md',
|
|
14
|
+
'.planning/ROADMAP.md',
|
|
15
|
+
'.planning/PM_SIGNOFF.md',
|
|
16
|
+
'.planning/qa/TEST-COVERAGE.md',
|
|
17
|
+
'.planning/qa/BUGS.md',
|
|
18
|
+
'.planning/qa/UX-AUDIT.md',
|
|
19
|
+
'.planning/qa/ACCEPTANCE-MATRIX.md',
|
|
20
|
+
'.planning/qa/REGRESSION-LOG.md',
|
|
21
|
+
'state.md',
|
|
22
|
+
'history.jsonl',
|
|
23
|
+
'lock.json',
|
|
24
|
+
'state.json'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const rel of mustExist) {
|
|
28
|
+
if (!existsSync(join(root, rel))) {
|
|
29
|
+
errors.push(`Missing required file: ${rel}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const signoff = readText(root, '.planning/PM_SIGNOFF.md');
|
|
34
|
+
const signoffApproved = /approved\s*:\s*yes/i.test(signoff || '');
|
|
35
|
+
if (!signoffApproved) {
|
|
36
|
+
if (mode === 'kickoff' || mode === 'full') {
|
|
37
|
+
errors.push('PM signoff is not approved. Set "Approved: YES" in .planning/PM_SIGNOFF.md.');
|
|
38
|
+
} else {
|
|
39
|
+
warnings.push('PM signoff is not approved.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const roadmap = readText(root, '.planning/ROADMAP.md') || '';
|
|
44
|
+
if (!/\bwave\b/i.test(roadmap)) {
|
|
45
|
+
errors.push('ROADMAP.md must define at least one Wave.');
|
|
46
|
+
}
|
|
47
|
+
if (!/\bphase\b/i.test(roadmap)) {
|
|
48
|
+
errors.push('ROADMAP.md must define at least one Phase.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const phaseStatus = validatePhaseArtifacts(root);
|
|
52
|
+
errors.push(...phaseStatus.errors);
|
|
53
|
+
warnings.push(...phaseStatus.warnings);
|
|
54
|
+
|
|
55
|
+
const history = validateHistory(root, config, { expectedAgent, requireEntry: mode !== 'kickoff' });
|
|
56
|
+
errors.push(...history.errors);
|
|
57
|
+
warnings.push(...history.warnings);
|
|
58
|
+
|
|
59
|
+
const qaSignals = validateQaArtifacts(root);
|
|
60
|
+
warnings.push(...qaSignals.warnings);
|
|
61
|
+
|
|
62
|
+
return { ok: errors.length === 0, mode, errors, warnings };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validatePhaseArtifacts(root) {
|
|
66
|
+
const result = { errors: [], warnings: [] };
|
|
67
|
+
const phasesDir = join(root, '.planning', 'phases');
|
|
68
|
+
if (!existsSync(phasesDir)) {
|
|
69
|
+
result.errors.push('Missing .planning/phases directory.');
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entries = safeReadDir(phasesDir).filter(name => !name.startsWith('.'));
|
|
74
|
+
if (entries.length === 0) {
|
|
75
|
+
result.errors.push('No phase artifacts found. Add .planning/phases/phase-*/PLAN.md and TESTS.md.');
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let hasAnyPlan = false;
|
|
80
|
+
let hasAnyTests = false;
|
|
81
|
+
for (const name of entries) {
|
|
82
|
+
const plan = join(phasesDir, name, 'PLAN.md');
|
|
83
|
+
const tests = join(phasesDir, name, 'TESTS.md');
|
|
84
|
+
if (existsSync(plan)) hasAnyPlan = true;
|
|
85
|
+
if (existsSync(tests)) hasAnyTests = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!hasAnyPlan) result.errors.push('No phase PLAN.md found under .planning/phases/.');
|
|
89
|
+
if (!hasAnyTests) result.errors.push('No phase TESTS.md found under .planning/phases/.');
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateHistory(root, config, opts) {
|
|
94
|
+
const result = { errors: [], warnings: [] };
|
|
95
|
+
const historyPath = join(root, 'history.jsonl');
|
|
96
|
+
if (!existsSync(historyPath)) {
|
|
97
|
+
result.errors.push('history.jsonl is missing.');
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lines = (readFileSync(historyPath, 'utf8') || '')
|
|
102
|
+
.split(/\r?\n/)
|
|
103
|
+
.map(l => l.trim())
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
|
|
106
|
+
if (lines.length === 0) {
|
|
107
|
+
if (opts.requireEntry) {
|
|
108
|
+
result.errors.push('history.jsonl has no entries.');
|
|
109
|
+
} else {
|
|
110
|
+
result.warnings.push('history.jsonl has no entries yet.');
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const lastRaw = lines[lines.length - 1];
|
|
116
|
+
let last;
|
|
117
|
+
try {
|
|
118
|
+
last = JSON.parse(lastRaw);
|
|
119
|
+
} catch {
|
|
120
|
+
result.errors.push('Last history.jsonl entry is not valid JSON.');
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requiredFields = ['turn', 'agent', 'summary', 'files_changed', 'verify_result', 'timestamp'];
|
|
125
|
+
for (const f of requiredFields) {
|
|
126
|
+
if (!(f in last)) result.errors.push(`Last history entry missing field: ${f}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (last.agent && !config.agents?.[last.agent]) {
|
|
130
|
+
result.errors.push(`Last history agent "${last.agent}" is not in agentxchain.json.`);
|
|
131
|
+
}
|
|
132
|
+
if (!Array.isArray(last.files_changed)) {
|
|
133
|
+
result.errors.push('Last history entry: files_changed must be an array.');
|
|
134
|
+
}
|
|
135
|
+
if (last.verify_result && !['pass', 'fail', 'skipped'].includes(last.verify_result)) {
|
|
136
|
+
result.errors.push('Last history entry: verify_result must be pass|fail|skipped.');
|
|
137
|
+
}
|
|
138
|
+
if (opts.expectedAgent && last.agent !== opts.expectedAgent) {
|
|
139
|
+
result.errors.push(`Last history entry agent "${last.agent}" does not match expected "${opts.expectedAgent}".`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateQaArtifacts(root) {
|
|
146
|
+
const result = { warnings: [] };
|
|
147
|
+
const checks = [
|
|
148
|
+
['.planning/qa/TEST-COVERAGE.md', /\(QA fills this as testing progresses\)/i, 'TEST-COVERAGE.md still looks uninitialized'],
|
|
149
|
+
['.planning/qa/ACCEPTANCE-MATRIX.md', /\(QA fills this from REQUIREMENTS\.md\)/i, 'ACCEPTANCE-MATRIX.md still looks uninitialized'],
|
|
150
|
+
['.planning/qa/UX-AUDIT.md', /\(QA adds UX issues here\)/i, 'UX-AUDIT.md issues table appears unmodified']
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const [rel, placeholderRegex, message] of checks) {
|
|
154
|
+
const text = readText(root, rel);
|
|
155
|
+
if (text && placeholderRegex.test(text)) {
|
|
156
|
+
result.warnings.push(message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readText(root, rel) {
|
|
164
|
+
const path = join(root, rel);
|
|
165
|
+
if (!existsSync(path)) return null;
|
|
166
|
+
try {
|
|
167
|
+
return readFileSync(path, 'utf8');
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function safeReadDir(path) {
|
|
174
|
+
try {
|
|
175
|
+
return readdirSync(path);
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|