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 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 (human + PM align scope first)
23
+ # 2. Run guided PM-first kickoff wizard
24
24
  cd my-project/
25
- agentxchain start --agent pm
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 start` opens a **separate Cursor window** for each agent
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. Recommended first-run flow: `start --agent pm` (kickoff), then `start --remaining`
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "CLI for AgentXchain — multi-agent coordination in your IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 didRaise to false
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 contains agentId then
93
- perform action "AXRaise" of w
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 didRaise is false then
104
+ if (count of matchedIndexes) = 1 then
101
105
  try
102
- perform action "AXRaise" of window 1
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
- 4) Do NOT start round-robin agent handoffs yet.
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}
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }