agentxchain 0.8.6 → 0.8.8
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 +36 -38
- package/bin/agentxchain.js +62 -3
- package/package.json +3 -2
- package/scripts/agentxchain-autonudge.applescript +49 -10
- package/scripts/run-autonudge.sh +1 -1
- package/src/adapters/claude-code.js +7 -14
- package/src/adapters/cursor-local.js +26 -29
- package/src/commands/branch.js +2 -2
- package/src/commands/claim.js +86 -15
- package/src/commands/config.js +16 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +24 -5
- package/src/commands/rebind.js +77 -0
- package/src/commands/stop.js +65 -33
- package/src/commands/update.js +24 -3
- package/src/commands/watch.js +115 -34
- package/src/lib/config.js +47 -12
- package/src/lib/filter-agents.js +12 -0
- package/src/lib/generate-vscode.js +158 -51
- package/src/lib/next-owner.js +116 -0
- package/src/lib/notify.js +14 -12
- package/src/lib/prompt-core.js +108 -0
- package/src/lib/safe-write.js +44 -0
- package/src/lib/schema.js +68 -0
- package/src/lib/seed-prompt-polling.js +21 -83
- package/src/lib/seed-prompt.js +17 -63
- package/src/lib/validation.js +30 -19
- package/src/lib/verify-command.js +72 -0
package/src/commands/claim.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
|
|
5
|
+
import { safeWriteJson } from '../lib/safe-write.js';
|
|
6
|
+
import { resolveExpectedClaimer } from '../lib/next-owner.js';
|
|
7
|
+
import { runConfiguredVerify } from '../lib/verify-command.js';
|
|
5
8
|
|
|
6
9
|
export async function claimCommand(opts) {
|
|
7
10
|
const result = loadConfig();
|
|
@@ -39,7 +42,12 @@ export async function claimCommand(opts) {
|
|
|
39
42
|
turn_number: lock.turn_number,
|
|
40
43
|
claimed_at: new Date().toISOString()
|
|
41
44
|
};
|
|
42
|
-
|
|
45
|
+
safeWriteJson(lockPath, newLock);
|
|
46
|
+
const verify = loadLock(root);
|
|
47
|
+
if (verify?.holder !== 'human') {
|
|
48
|
+
console.log(chalk.red(` Claim race: expected holder=human, got ${verify?.holder}. Another process won.`));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
43
51
|
clearBlockedState(root);
|
|
44
52
|
|
|
45
53
|
console.log('');
|
|
@@ -75,6 +83,11 @@ export async function releaseCommand(opts) {
|
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
const who = lock.holder;
|
|
86
|
+
const verifyResult = runConfiguredVerify(config, root);
|
|
87
|
+
if (!verifyResult.ok) {
|
|
88
|
+
console.log(chalk.red(` Verification failed: ${verifyResult.command}`));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
78
91
|
const lockPath = join(root, LOCK_FILE);
|
|
79
92
|
const newLock = {
|
|
80
93
|
holder: null,
|
|
@@ -82,14 +95,19 @@ export async function releaseCommand(opts) {
|
|
|
82
95
|
turn_number: who === 'human' ? lock.turn_number : lock.turn_number + 1,
|
|
83
96
|
claimed_at: null
|
|
84
97
|
};
|
|
85
|
-
|
|
98
|
+
safeWriteJson(lockPath, newLock);
|
|
99
|
+
const verify = loadLock(root);
|
|
100
|
+
if (verify?.holder !== null || verify?.last_released_by !== who || verify?.turn_number !== newLock.turn_number) {
|
|
101
|
+
console.log(chalk.red(' Release race: lock.json changed unexpectedly after release attempt.'));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
86
104
|
if (who === 'human') {
|
|
87
105
|
clearBlockedState(root);
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
console.log('');
|
|
91
109
|
console.log(chalk.green(` ✓ Lock released by ${chalk.bold(who)} (turn ${newLock.turn_number})`));
|
|
92
|
-
console.log(chalk.dim('
|
|
110
|
+
console.log(chalk.dim(' Next turn will be coordinated by the VS Code Stop hook or watch/supervise.'));
|
|
93
111
|
console.log('');
|
|
94
112
|
}
|
|
95
113
|
|
|
@@ -106,12 +124,26 @@ function claimAsAgent({ opts, root, config, lock }) {
|
|
|
106
124
|
process.exit(1);
|
|
107
125
|
}
|
|
108
126
|
|
|
109
|
-
const expected = pickNextAgent(lock, config);
|
|
127
|
+
const expected = pickNextAgent(root, lock, config);
|
|
128
|
+
if (!opts.force && config.rules?.strict_next_owner && (expected === null || expected === undefined)) {
|
|
129
|
+
console.log(chalk.red(' No next owner resolved. Add a valid `Next owner: <agent_id>` line to TALK.md, or set rules.strict_next_owner to false.'));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
110
132
|
if (!opts.force && expected && expected !== agentId) {
|
|
111
133
|
console.log(chalk.red(` Out-of-turn claim blocked. Expected: ${expected}, got: ${agentId}.`));
|
|
112
134
|
process.exit(1);
|
|
113
135
|
}
|
|
114
136
|
|
|
137
|
+
const maxClaims = Number(config.rules?.max_consecutive_claims || 0);
|
|
138
|
+
if (!opts.force && maxClaims > 0 && lock.last_released_by === agentId) {
|
|
139
|
+
const consecutiveTurns = countRecentTurnsByAgent(root, config, agentId);
|
|
140
|
+
if (consecutiveTurns >= maxClaims) {
|
|
141
|
+
console.log(chalk.red(` Consecutive-claim limit reached for "${agentId}" (${consecutiveTurns}/${maxClaims}).`));
|
|
142
|
+
console.log(chalk.dim(' Hand off to another agent or use --force for recovery only.'));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
const lockPath = join(root, LOCK_FILE);
|
|
116
148
|
const next = {
|
|
117
149
|
holder: agentId,
|
|
@@ -119,7 +151,14 @@ function claimAsAgent({ opts, root, config, lock }) {
|
|
|
119
151
|
turn_number: lock.turn_number,
|
|
120
152
|
claimed_at: new Date().toISOString()
|
|
121
153
|
};
|
|
122
|
-
|
|
154
|
+
safeWriteJson(lockPath, next);
|
|
155
|
+
|
|
156
|
+
const verify = loadLock(root);
|
|
157
|
+
if (verify?.holder !== agentId) {
|
|
158
|
+
console.log(chalk.red(` Claim race: expected holder=${agentId}, got ${verify?.holder}. Another process won.`));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
123
162
|
console.log(chalk.green(` ✓ Lock claimed by ${agentId} (turn ${next.turn_number})`));
|
|
124
163
|
}
|
|
125
164
|
|
|
@@ -134,6 +173,12 @@ function releaseAsAgent({ opts, root, config, lock }) {
|
|
|
134
173
|
process.exit(1);
|
|
135
174
|
}
|
|
136
175
|
|
|
176
|
+
const verifyResult = runConfiguredVerify(config, root);
|
|
177
|
+
if (!verifyResult.ok) {
|
|
178
|
+
console.log(chalk.red(` Verification failed: ${verifyResult.command}`));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
137
182
|
const lockPath = join(root, LOCK_FILE);
|
|
138
183
|
const next = {
|
|
139
184
|
holder: null,
|
|
@@ -141,17 +186,21 @@ function releaseAsAgent({ opts, root, config, lock }) {
|
|
|
141
186
|
turn_number: lock.turn_number + 1,
|
|
142
187
|
claimed_at: null
|
|
143
188
|
};
|
|
144
|
-
|
|
189
|
+
safeWriteJson(lockPath, next);
|
|
190
|
+
const verifyRelease = loadLock(root);
|
|
191
|
+
if (
|
|
192
|
+
verifyRelease?.holder !== null ||
|
|
193
|
+
verifyRelease?.last_released_by !== agentId ||
|
|
194
|
+
verifyRelease?.turn_number !== next.turn_number
|
|
195
|
+
) {
|
|
196
|
+
console.log(chalk.red(' Release race: lock.json changed unexpectedly after release attempt.'));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
145
199
|
console.log(chalk.green(` ✓ Lock released by ${agentId} (turn ${next.turn_number})`));
|
|
146
200
|
}
|
|
147
201
|
|
|
148
|
-
function pickNextAgent(lock, config) {
|
|
149
|
-
|
|
150
|
-
if (agentIds.length === 0) return null;
|
|
151
|
-
const lastAgent = lock.last_released_by;
|
|
152
|
-
if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
|
|
153
|
-
const lastIndex = agentIds.indexOf(lastAgent);
|
|
154
|
-
return agentIds[(lastIndex + 1) % agentIds.length];
|
|
202
|
+
function pickNextAgent(root, lock, config) {
|
|
203
|
+
return resolveExpectedClaimer(root, config, lock).next;
|
|
155
204
|
}
|
|
156
205
|
|
|
157
206
|
function clearBlockedState(root) {
|
|
@@ -161,7 +210,29 @@ function clearBlockedState(root) {
|
|
|
161
210
|
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
162
211
|
if (state.blocked || state.blocked_on) {
|
|
163
212
|
const next = { ...state, blocked: false, blocked_on: null };
|
|
164
|
-
|
|
213
|
+
safeWriteJson(statePath, next);
|
|
165
214
|
}
|
|
166
215
|
} catch {}
|
|
167
216
|
}
|
|
217
|
+
|
|
218
|
+
function countRecentTurnsByAgent(root, config, agentId) {
|
|
219
|
+
const historyPath = join(root, config.history_file || 'history.jsonl');
|
|
220
|
+
if (!existsSync(historyPath)) return 0;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const lines = readFileSync(historyPath, 'utf8')
|
|
224
|
+
.split(/\r?\n/)
|
|
225
|
+
.map(line => line.trim())
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
|
|
228
|
+
let count = 0;
|
|
229
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
230
|
+
const entry = JSON.parse(lines[i]);
|
|
231
|
+
if (entry?.agent !== agentId) break;
|
|
232
|
+
count += 1;
|
|
233
|
+
}
|
|
234
|
+
return count;
|
|
235
|
+
} catch {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
package/src/commands/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
import { loadConfig, CONFIG_FILE } from '../lib/config.js';
|
|
6
|
+
import { validateConfigSchema } from '../lib/schema.js';
|
|
6
7
|
|
|
7
8
|
export async function configCommand(opts) {
|
|
8
9
|
const result = loadConfig();
|
|
@@ -127,6 +128,12 @@ function setSetting(config, configPath, keyValPair) {
|
|
|
127
128
|
const key = parts[0];
|
|
128
129
|
const rawVal = parts.slice(1).join(' ');
|
|
129
130
|
const segments = key.split('.');
|
|
131
|
+
const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
132
|
+
|
|
133
|
+
if (segments.some(segment => forbiddenKeys.has(segment))) {
|
|
134
|
+
console.log(chalk.red(' Refusing to write reserved object path.'));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
130
137
|
|
|
131
138
|
let target = config;
|
|
132
139
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
@@ -145,6 +152,15 @@ function setSetting(config, configPath, keyValPair) {
|
|
|
145
152
|
else if (!isNaN(rawVal) && rawVal !== '') val = Number(rawVal);
|
|
146
153
|
|
|
147
154
|
target[lastKey] = val;
|
|
155
|
+
const validation = validateConfigSchema(config);
|
|
156
|
+
if (!validation.ok) {
|
|
157
|
+
target[lastKey] = oldVal;
|
|
158
|
+
if (oldVal === undefined) {
|
|
159
|
+
delete target[lastKey];
|
|
160
|
+
}
|
|
161
|
+
console.log(chalk.red(` Refusing to save invalid config: ${validation.errors.join(', ')}`));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
148
164
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
149
165
|
|
|
150
166
|
console.log('');
|
package/src/commands/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadConfig, loadLock } from '../lib/config.js';
|
|
6
6
|
import { validateProject } from '../lib/validation.js';
|
|
7
|
+
import { getWatchPid } from './watch.js';
|
|
7
8
|
|
|
8
9
|
export async function doctorCommand() {
|
|
9
10
|
const result = loadConfig();
|
|
@@ -86,9 +87,16 @@ function checkPm(config) {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
function checkWatchProcess() {
|
|
90
|
+
const result = loadConfig();
|
|
91
|
+
if (result) {
|
|
92
|
+
const pid = getWatchPid(result.root);
|
|
93
|
+
if (pid) {
|
|
94
|
+
return { name: 'watch process', level: 'pass', detail: `watch running (PID: ${pid})` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
89
97
|
try {
|
|
90
98
|
execSync('pgrep -f "agentxchain.*watch" >/dev/null', { stdio: 'ignore' });
|
|
91
|
-
return { name: 'watch process', level: 'pass', detail: 'watch appears to be running' };
|
|
99
|
+
return { name: 'watch process', level: 'pass', detail: 'watch appears to be running (no PID file)' };
|
|
92
100
|
} catch {
|
|
93
101
|
return { name: 'watch process', level: 'warn', detail: 'watch not running (start with `agentxchain watch` or `agentxchain supervise --autonudge`)' };
|
|
94
102
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -52,7 +52,12 @@ export async function initCommand(opts) {
|
|
|
52
52
|
project = 'My AgentXchain project';
|
|
53
53
|
agents = DEFAULT_AGENTS;
|
|
54
54
|
folderName = slugify(project);
|
|
55
|
-
rules = {
|
|
55
|
+
rules = {
|
|
56
|
+
max_consecutive_claims: 2,
|
|
57
|
+
require_message: true,
|
|
58
|
+
compress_after_words: 5000,
|
|
59
|
+
strict_next_owner: false
|
|
60
|
+
};
|
|
56
61
|
} else {
|
|
57
62
|
const templates = loadTemplates();
|
|
58
63
|
|
|
@@ -86,7 +91,12 @@ export async function initCommand(opts) {
|
|
|
86
91
|
project = projectName;
|
|
87
92
|
} else if (template === 'default') {
|
|
88
93
|
agents = DEFAULT_AGENTS;
|
|
89
|
-
rules = {
|
|
94
|
+
rules = {
|
|
95
|
+
max_consecutive_claims: 2,
|
|
96
|
+
require_message: true,
|
|
97
|
+
compress_after_words: 5000,
|
|
98
|
+
strict_next_owner: false
|
|
99
|
+
};
|
|
90
100
|
const { projectName } = await inquirer.prompt([{
|
|
91
101
|
type: 'input',
|
|
92
102
|
name: 'projectName',
|
|
@@ -103,7 +113,12 @@ export async function initCommand(opts) {
|
|
|
103
113
|
}]);
|
|
104
114
|
project = projectName;
|
|
105
115
|
agents = {};
|
|
106
|
-
rules = {
|
|
116
|
+
rules = {
|
|
117
|
+
max_consecutive_claims: 2,
|
|
118
|
+
require_message: true,
|
|
119
|
+
compress_after_words: 5000,
|
|
120
|
+
strict_next_owner: false
|
|
121
|
+
};
|
|
107
122
|
|
|
108
123
|
const { count } = await inquirer.prompt([{
|
|
109
124
|
type: 'number',
|
|
@@ -192,6 +207,7 @@ export async function initCommand(opts) {
|
|
|
192
207
|
compress_after_words: rules.compress_after_words || 5000,
|
|
193
208
|
ttl_minutes: rules.ttl_minutes || 10,
|
|
194
209
|
watch_interval_ms: rules.watch_interval_ms || 5000,
|
|
210
|
+
strict_next_owner: rules.strict_next_owner === true,
|
|
195
211
|
...(rules.verify_command ? { verify_command: rules.verify_command } : {})
|
|
196
212
|
}
|
|
197
213
|
};
|
|
@@ -209,7 +225,7 @@ export async function initCommand(opts) {
|
|
|
209
225
|
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`);
|
|
210
226
|
writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
|
|
211
227
|
const gitignorePath = join(dir, '.gitignore');
|
|
212
|
-
const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/'];
|
|
228
|
+
const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/', '.agentxchain-watch.pid', '.agentxchain-autonudge.state'];
|
|
213
229
|
if (!existsSync(gitignorePath)) {
|
|
214
230
|
writeFileSync(gitignorePath, requiredIgnores.join('\n') + '\n');
|
|
215
231
|
} else {
|
|
@@ -230,10 +246,13 @@ export async function initCommand(opts) {
|
|
|
230
246
|
|
|
231
247
|
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`);
|
|
232
248
|
|
|
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`);
|
|
249
|
+
writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${project}\n\n## Waves\n\n| Wave | Goal | Status |\n|------|------|--------|\n| Wave 1 | Discovery, planning, and phase setup | In progress |\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
250
|
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`);
|
|
235
251
|
|
|
236
252
|
// QA structure
|
|
253
|
+
mkdirSync(join(dir, '.planning', 'phases', 'phase-1'), { recursive: true });
|
|
254
|
+
writeFileSync(join(dir, '.planning', 'phases', 'phase-1', 'PLAN.md'), `# Phase 1 Plan — ${project}\n\n## Goal\n\nAlign scope, requirements, and initial implementation plan.\n\n## Deliverables\n\n- PM signoff\n- Initial requirements and roadmap\n- First implementation slice\n`);
|
|
255
|
+
writeFileSync(join(dir, '.planning', 'phases', 'phase-1', 'TESTS.md'), `# Phase 1 Tests — ${project}\n\n## Planned checks\n\n- Kickoff validation passes\n- Requirements have acceptance criteria\n- First implementation slice has an executable verification path\n`);
|
|
237
256
|
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`);
|
|
238
257
|
|
|
239
258
|
writeFileSync(join(dir, '.planning', 'qa', 'REGRESSION-LOG.md'), `# Regression Log — ${project}\n\nBugs that were found and fixed. Each entry has a regression test to prevent recurrence.\n\n| Bug ID | Description | Found turn | Fixed turn | Regression test | Status |\n|--------|-------------|-----------|-----------|----------------|--------|\n| (QA adds entries as bugs are found and fixed) | | | | | |\n`);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { generatePollingPrompt } from '../lib/seed-prompt-polling.js';
|
|
7
|
+
|
|
8
|
+
export async function rebindCommand(opts) {
|
|
9
|
+
const result = loadConfig();
|
|
10
|
+
if (!result) {
|
|
11
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { root, config } = result;
|
|
16
|
+
const agentEntries = Object.entries(config.agents || {});
|
|
17
|
+
if (agentEntries.length === 0) {
|
|
18
|
+
console.log(chalk.red('No agents configured in agentxchain.json.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const selected = opts.agent
|
|
23
|
+
? agentEntries.filter(([id]) => id === opts.agent)
|
|
24
|
+
: agentEntries;
|
|
25
|
+
|
|
26
|
+
if (opts.agent && selected.length === 0) {
|
|
27
|
+
console.log(chalk.red(`Agent "${opts.agent}" not found in agentxchain.json.`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const promptDir = join(root, '.agentxchain-prompts');
|
|
32
|
+
const workspacesDir = join(root, '.agentxchain-workspaces');
|
|
33
|
+
mkdirSync(promptDir, { recursive: true });
|
|
34
|
+
mkdirSync(workspacesDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
for (const [id, def] of selected) {
|
|
37
|
+
const prompt = generatePollingPrompt(id, def, config, root);
|
|
38
|
+
writeFileSync(join(promptDir, `${id}.prompt.md`), prompt);
|
|
39
|
+
|
|
40
|
+
const wsPath = join(workspacesDir, `${id}.code-workspace`);
|
|
41
|
+
const workspaceJson = {
|
|
42
|
+
folders: [{ path: root }],
|
|
43
|
+
settings: { 'agentxchain.agentId': id }
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(wsPath, JSON.stringify(workspaceJson, null, 2) + '\n');
|
|
46
|
+
|
|
47
|
+
if (opts.open) {
|
|
48
|
+
openCursorWindow(wsPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const statePath = join(root, '.agentxchain-autonudge.state');
|
|
53
|
+
if (existsSync(statePath)) {
|
|
54
|
+
rmSync(statePath, { force: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(chalk.green(` ✓ Rebound ${selected.length} agent session(s).`));
|
|
59
|
+
console.log(chalk.dim(` Prompts: ${join('.agentxchain-prompts', opts.agent ? `${opts.agent}.prompt.md` : '')}`));
|
|
60
|
+
console.log(chalk.dim(` Workspaces: ${join('.agentxchain-workspaces', opts.agent ? `${opts.agent}.code-workspace` : '')}`));
|
|
61
|
+
console.log(chalk.dim(' Auto-nudge dispatch state reset.'));
|
|
62
|
+
if (!opts.open) {
|
|
63
|
+
console.log(chalk.dim(' Use `agentxchain rebind --open` to reopen agent windows now.'));
|
|
64
|
+
}
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function openCursorWindow(targetPath) {
|
|
69
|
+
try {
|
|
70
|
+
if (process.platform === 'darwin') {
|
|
71
|
+
execSync(`open -na "Cursor" --args "${targetPath}"`, { stdio: 'ignore' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
execSync(`cursor --new-window "${targetPath}"`, { stdio: 'ignore' });
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
package/src/commands/stop.js
CHANGED
|
@@ -2,8 +2,10 @@ import { readFileSync, existsSync, unlinkSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadConfig } from '../lib/config.js';
|
|
5
|
+
import { getWatchPid } from './watch.js';
|
|
5
6
|
|
|
6
7
|
const SESSION_FILE = '.agentxchain-session.json';
|
|
8
|
+
const WATCH_PID_FILE = '.agentxchain-watch.pid';
|
|
7
9
|
|
|
8
10
|
export async function stopCommand() {
|
|
9
11
|
const result = loadConfig();
|
|
@@ -11,47 +13,77 @@ export async function stopCommand() {
|
|
|
11
13
|
|
|
12
14
|
const { root } = result;
|
|
13
15
|
const sessionPath = join(root, SESSION_FILE);
|
|
16
|
+
const watchPidPath = join(root, WATCH_PID_FILE);
|
|
17
|
+
const watchPid = getWatchPid(root);
|
|
18
|
+
let didStopAnything = false;
|
|
14
19
|
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
if (watchPid) {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(watchPid, 'SIGTERM');
|
|
23
|
+
didStopAnything = true;
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(chalk.green(` ✓ Stopped watch process (PID: ${watchPid})`));
|
|
26
|
+
console.log('');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code === 'ESRCH') {
|
|
29
|
+
if (existsSync(watchPidPath)) {
|
|
30
|
+
try { unlinkSync(watchPidPath); } catch {}
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
console.log(chalk.red(` ✗ Could not stop watch process (PID: ${watchPid}): ${err.message}`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else if (existsSync(watchPidPath)) {
|
|
37
|
+
// Stale PID file from an unexpected shutdown.
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(watchPidPath);
|
|
40
|
+
console.log(chalk.dim(' Removed stale watch PID file.'));
|
|
41
|
+
} catch {}
|
|
19
42
|
}
|
|
20
43
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
if (existsSync(sessionPath)) {
|
|
45
|
+
let session;
|
|
46
|
+
try {
|
|
47
|
+
session = JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
48
|
+
} catch {
|
|
49
|
+
console.log(chalk.yellow(' Could not read session file.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(chalk.bold(` Stopping ${session.launched?.length || 0} agents (${session.ide || 'unknown'})`));
|
|
55
|
+
console.log('');
|
|
28
56
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} else {
|
|
43
|
-
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
57
|
+
if (session.ide === 'claude-code') {
|
|
58
|
+
for (const agent of (session.launched || [])) {
|
|
59
|
+
if (agent.pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(agent.pid, 'SIGTERM');
|
|
62
|
+
didStopAnything = true;
|
|
63
|
+
console.log(chalk.green(` ✓ Sent SIGTERM to ${agent.id} (PID: ${agent.pid})`));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err.code === 'ESRCH') {
|
|
66
|
+
console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
|
|
67
|
+
} else {
|
|
68
|
+
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
69
|
+
}
|
|
44
70
|
}
|
|
45
71
|
}
|
|
46
72
|
}
|
|
73
|
+
} else {
|
|
74
|
+
console.log(chalk.dim(' For VS Code / Cursor agents, close the chat sessions manually.'));
|
|
47
75
|
}
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
|
|
77
|
+
unlinkSync(sessionPath);
|
|
78
|
+
console.log('');
|
|
79
|
+
console.log(chalk.dim(' Session file removed.'));
|
|
80
|
+
console.log(chalk.green(' Done.'));
|
|
81
|
+
console.log('');
|
|
82
|
+
return;
|
|
50
83
|
}
|
|
51
84
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('');
|
|
85
|
+
if (!didStopAnything) {
|
|
86
|
+
console.log(chalk.yellow(' No active session found.'));
|
|
87
|
+
console.log(chalk.dim(' If agents are running in VS Code / Cursor, close their chat sessions manually.'));
|
|
88
|
+
}
|
|
57
89
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -4,6 +4,19 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
|
|
7
|
+
function printGlobalInstallFallbacks() {
|
|
8
|
+
console.log(chalk.dim(' If global install failed with permission errors (EACCES):'));
|
|
9
|
+
console.log(` ${chalk.bold('sudo npm install -g agentxchain@latest')} ${chalk.dim('(macOS/Linux)')}`);
|
|
10
|
+
console.log(` ${chalk.bold('npm config get prefix')} ${chalk.dim('— fix ownership of that directory, or use a user prefix:')}`);
|
|
11
|
+
console.log(` ${chalk.bold('mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global')}`);
|
|
12
|
+
console.log(` ${chalk.dim('(add ~/.npm-global/bin to PATH)')}`);
|
|
13
|
+
console.log(chalk.dim(' Or run without global install:'));
|
|
14
|
+
console.log(` ${chalk.bold('npx agentxchain@latest <command>')}`);
|
|
15
|
+
console.log('');
|
|
16
|
+
console.log(chalk.dim(' Node: use 18.17+ or 20.5+ to avoid engine warnings from dependencies.'));
|
|
17
|
+
console.log('');
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export async function updateCommand() {
|
|
8
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
22
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
|
|
@@ -24,18 +37,26 @@ export async function updateCommand() {
|
|
|
24
37
|
|
|
25
38
|
console.log(` ${chalk.dim('Latest version:')} ${chalk.cyan(latest)}`);
|
|
26
39
|
console.log('');
|
|
27
|
-
console.log(
|
|
40
|
+
console.log(' Updating...');
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
try {
|
|
43
|
+
execSync('npm install -g agentxchain@latest', { stdio: 'inherit' });
|
|
44
|
+
} catch {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(chalk.yellow(' Global install failed. Common fixes:'));
|
|
47
|
+
printGlobalInstallFallbacks();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
30
50
|
|
|
31
51
|
console.log('');
|
|
32
52
|
console.log(chalk.green(` ✓ Updated to ${latest}`));
|
|
33
53
|
console.log('');
|
|
34
54
|
} catch (err) {
|
|
35
55
|
console.log('');
|
|
36
|
-
console.log(chalk.yellow(' Could not
|
|
56
|
+
console.log(chalk.yellow(' Could not check or install the latest version.'));
|
|
37
57
|
console.log(` ${chalk.bold('npm install -g agentxchain@latest')}`);
|
|
38
58
|
console.log('');
|
|
59
|
+
printGlobalInstallFallbacks();
|
|
39
60
|
console.log(chalk.dim(` Error: ${err.message}`));
|
|
40
61
|
console.log('');
|
|
41
62
|
}
|