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 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 (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.
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 start` opens a **separate Cursor window** for each agent
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. Recommended first-run flow: `start --agent pm` (kickoff), then `start --remaining`
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
@@ -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.5",
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
@@ -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
- 4) Do NOT start round-robin agent handoffs yet.
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}
@@ -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
+ }
@@ -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
+ }
@@ -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 = {
@@ -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. Update \`state.json\` if phase or blocked status changed.`
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. Update \`state.json\` if phase or blocked status changed.`;
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. Update state.json if phase or blocked status changed.`
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. Update state.json if phase or blocked status changed.`;
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
+ }