@agilemonster/mcp-server 1.0.15 → 1.0.16
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/dist/client.d.ts +11 -1
- package/dist/client.js +105 -3
- package/dist/server.d.ts +7 -1
- package/dist/server.js +408 -136
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgileMonsterProjectConfig, AutonomousStepResponse, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
|
|
2
|
-
export declare const MCP_SERVER_VERSION = "1.0.
|
|
2
|
+
export declare const MCP_SERVER_VERSION = "1.0.16";
|
|
3
3
|
export declare class AgileMonsterClient {
|
|
4
4
|
private apiKey;
|
|
5
5
|
private baseUrl;
|
|
@@ -50,6 +50,15 @@ export declare class AgileMonsterClient {
|
|
|
50
50
|
default_model?: string | null;
|
|
51
51
|
models: Record<string, string>;
|
|
52
52
|
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Auto-detect build command, deployment method, deploy script, and live URL for a project repository.
|
|
55
|
+
*/
|
|
56
|
+
static inspectProjectProfile(fullPath: string, name: string): {
|
|
57
|
+
build_command: string | null;
|
|
58
|
+
deploy_method: string | null;
|
|
59
|
+
deploy_command: string | null;
|
|
60
|
+
live_url: string | null;
|
|
61
|
+
};
|
|
53
62
|
/**
|
|
54
63
|
* Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
|
|
55
64
|
* to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
|
|
@@ -84,5 +93,6 @@ export declare class AgileMonsterClient {
|
|
|
84
93
|
action: 'select_files' | 'generate_edits';
|
|
85
94
|
file_index?: string[];
|
|
86
95
|
files?: Record<string, string>;
|
|
96
|
+
project_context?: string;
|
|
87
97
|
}): Promise<AutonomousStepResponse>;
|
|
88
98
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
-
export const MCP_SERVER_VERSION = '1.0.
|
|
4
|
+
export const MCP_SERVER_VERSION = '1.0.16';
|
|
5
5
|
export class AgileMonsterClient {
|
|
6
6
|
apiKey = '';
|
|
7
7
|
baseUrl;
|
|
@@ -315,17 +315,117 @@ export class AgileMonsterClient {
|
|
|
315
315
|
catch { }
|
|
316
316
|
// 5. Antigravity
|
|
317
317
|
result.antigravity = {
|
|
318
|
-
default_model: 'gemini-
|
|
318
|
+
default_model: 'gemini-4-argon',
|
|
319
319
|
models: {
|
|
320
|
+
'gemini-4-argon': 'Gemini 4 Argon (Fallback -> Claude Opus 5.5)',
|
|
321
|
+
'claude-opus-5-5': 'Claude Opus 5.5',
|
|
320
322
|
'gemini-3.1-pro': 'Gemini 3.1 Pro',
|
|
321
323
|
'gemini-3.0-flash': 'Gemini 3.0 Flash',
|
|
322
|
-
'claude-opus-5-5': 'Claude Opus 5.5',
|
|
323
324
|
'claude-sonnet-5-5': 'Claude Sonnet 5.5',
|
|
324
325
|
'gpt-6.1-sol': 'GPT-6.1 Sol',
|
|
325
326
|
},
|
|
326
327
|
};
|
|
327
328
|
return result;
|
|
328
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Auto-detect build command, deployment method, deploy script, and live URL for a project repository.
|
|
332
|
+
*/
|
|
333
|
+
static inspectProjectProfile(fullPath, name) {
|
|
334
|
+
let buildCmd = null;
|
|
335
|
+
let deployMethod = null;
|
|
336
|
+
let deployCmd = null;
|
|
337
|
+
let liveUrl = null;
|
|
338
|
+
// 1. Detect Build Command
|
|
339
|
+
try {
|
|
340
|
+
const fePkg = path.join(fullPath, 'frontend', 'package.json');
|
|
341
|
+
const rootPkg = path.join(fullPath, 'package.json');
|
|
342
|
+
if (fs.existsSync(fePkg)) {
|
|
343
|
+
const pkg = JSON.parse(fs.readFileSync(fePkg, 'utf8'));
|
|
344
|
+
if (pkg?.scripts?.build) {
|
|
345
|
+
buildCmd = fs.existsSync(path.join(fullPath, 'frontend', 'pnpm-lock.yaml'))
|
|
346
|
+
? 'cd frontend && pnpm run build'
|
|
347
|
+
: 'cd frontend && npm run build';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (fs.existsSync(rootPkg)) {
|
|
351
|
+
const pkg = JSON.parse(fs.readFileSync(rootPkg, 'utf8'));
|
|
352
|
+
if (pkg?.scripts?.build) {
|
|
353
|
+
buildCmd = fs.existsSync(path.join(fullPath, 'pnpm-lock.yaml'))
|
|
354
|
+
? 'pnpm run build'
|
|
355
|
+
: 'npm run build';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (fs.existsSync(path.join(fullPath, 'frontend-flutter', 'pubspec.yaml'))) {
|
|
359
|
+
buildCmd = 'cd frontend-flutter && flutter analyze';
|
|
360
|
+
}
|
|
361
|
+
else if (fs.existsSync(path.join(fullPath, 'pubspec.yaml'))) {
|
|
362
|
+
buildCmd = 'flutter analyze';
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch { }
|
|
366
|
+
// 2. Detect Deploy Method & Command
|
|
367
|
+
try {
|
|
368
|
+
const deploySh = path.join(fullPath, 'deploy', 'deploy.sh');
|
|
369
|
+
const scriptsDeploySh = path.join(fullPath, 'scripts', 'deploy.sh');
|
|
370
|
+
if (fs.existsSync(deploySh)) {
|
|
371
|
+
deployMethod = 'deploy_script';
|
|
372
|
+
const content = fs.readFileSync(deploySh, 'utf8');
|
|
373
|
+
deployCmd = content.includes('--skip-preflight')
|
|
374
|
+
? './deploy/deploy.sh --skip-preflight'
|
|
375
|
+
: './deploy/deploy.sh';
|
|
376
|
+
}
|
|
377
|
+
else if (fs.existsSync(scriptsDeploySh)) {
|
|
378
|
+
deployMethod = 'deploy_script';
|
|
379
|
+
deployCmd = './scripts/deploy.sh';
|
|
380
|
+
}
|
|
381
|
+
else if (fs.existsSync(path.join(fullPath, '.github', 'workflows'))) {
|
|
382
|
+
deployMethod = 'github_actions';
|
|
383
|
+
deployCmd = 'git push origin HEAD (GitHub Actions CI/CD)';
|
|
384
|
+
}
|
|
385
|
+
else if (fs.existsSync(path.join(fullPath, 'docker-compose.yml'))) {
|
|
386
|
+
deployMethod = 'docker_compose';
|
|
387
|
+
deployCmd = 'docker compose up -d --build';
|
|
388
|
+
}
|
|
389
|
+
else if (fs.existsSync(path.join(fullPath, '.git'))) {
|
|
390
|
+
deployMethod = 'git_push';
|
|
391
|
+
deployCmd = 'git push origin HEAD';
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch { }
|
|
395
|
+
// 3. Detect Live URL
|
|
396
|
+
try {
|
|
397
|
+
for (const docFile of ['CLAUDE.md', 'AGENTS.md', 'README.md', '.env']) {
|
|
398
|
+
const docPath = path.join(fullPath, docFile);
|
|
399
|
+
if (fs.existsSync(docPath)) {
|
|
400
|
+
const text = fs.readFileSync(docPath, 'utf8').slice(0, 6000);
|
|
401
|
+
const urlMatch = text.match(/https:\/\/(?:www\.)?([a-z0-9-]+\.(?:com|ai|io|app|dev|org|net))\b/i);
|
|
402
|
+
if (urlMatch && !/github\.com|openai\.com|anthropic\.com|googleapis\.com|npmjs\.com|localhost/i.test(urlMatch[0])) {
|
|
403
|
+
liveUrl = urlMatch[0];
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (!liveUrl) {
|
|
409
|
+
const clean = name.toLowerCase().replace(/^nieuw\.games[-_]*/i, '');
|
|
410
|
+
if (/^[a-z0-9-]+\.(com|ai|io|app|dev|org|net)$/i.test(clean)) {
|
|
411
|
+
liveUrl = `https://${clean}`;
|
|
412
|
+
}
|
|
413
|
+
else if (clean === 'meerlink') {
|
|
414
|
+
liveUrl = 'https://meerlink.com';
|
|
415
|
+
}
|
|
416
|
+
else if (clean === 'werkinghard' || clean === 'workinghard') {
|
|
417
|
+
liveUrl = 'https://werkinghard.com';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch { }
|
|
422
|
+
return {
|
|
423
|
+
build_command: buildCmd,
|
|
424
|
+
deploy_method: deployMethod,
|
|
425
|
+
deploy_command: deployCmd,
|
|
426
|
+
live_url: liveUrl,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
329
429
|
/**
|
|
330
430
|
* Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
|
|
331
431
|
* to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
|
|
@@ -405,10 +505,12 @@ export class AgileMonsterClient {
|
|
|
405
505
|
}
|
|
406
506
|
catch { }
|
|
407
507
|
}
|
|
508
|
+
const profile = AgileMonsterClient.inspectProjectProfile(fullPath, name);
|
|
408
509
|
found.set(fullPath, {
|
|
409
510
|
name,
|
|
410
511
|
path: fullPath,
|
|
411
512
|
git_remote: gitRemote,
|
|
513
|
+
...profile,
|
|
412
514
|
});
|
|
413
515
|
}
|
|
414
516
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -36,7 +36,13 @@ export declare function createServer(client?: AgileMonsterClient): Server<{
|
|
|
36
36
|
} | undefined;
|
|
37
37
|
}>;
|
|
38
38
|
export declare function executeQueuedCardAutonomously(client: AgileMonsterClient, card: Card, projectPath: string): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Deploy a card the human approved into Done (or flagged as `deploying`).
|
|
41
|
+
* Idempotent: keyed on card ID only (never on `updated_at`, which changes whenever we post a comment),
|
|
42
|
+
* so a card is deployed at most once unless the human explicitly re-triggers it into `deploying`.
|
|
43
|
+
*/
|
|
39
44
|
export declare function deployApprovedCardIfNeeded(client: AgileMonsterClient, card: Card, projectPath: string): Promise<boolean>;
|
|
40
|
-
export declare function
|
|
45
|
+
export declare function acquireWorkerLock(role: 'daemon' | 'stdio'): boolean;
|
|
46
|
+
export declare function startBuiltInPullPulse(server: Server | null, client: AgileMonsterClient, intervalMs?: number, role?: 'daemon' | 'stdio'): NodeJS.Timeout;
|
|
41
47
|
export declare function runStdioServer(): Promise<void>;
|
|
42
48
|
export declare function runBackgroundPulseDaemon(intervalMs?: number): Promise<void>;
|
package/dist/server.js
CHANGED
|
@@ -761,15 +761,9 @@ function syncIdeTaskRules(boardName, queuedCards) {
|
|
|
761
761
|
}
|
|
762
762
|
catch { }
|
|
763
763
|
}
|
|
764
|
-
function triggerBackgroundSelfUpdate() {
|
|
764
|
+
function triggerBackgroundSelfUpdate(targetVersion) {
|
|
765
765
|
try {
|
|
766
|
-
// 1.
|
|
767
|
-
const child = spawn('npx', ['-y', '@agilemonster/mcp-server@latest', '--version'], {
|
|
768
|
-
stdio: 'ignore',
|
|
769
|
-
detached: true,
|
|
770
|
-
});
|
|
771
|
-
child.unref();
|
|
772
|
-
// 2. Ensure local .cursor/mcp.json and .mcp.json use @latest so next IDE reload uses latest
|
|
766
|
+
// 1. Ensure local .cursor/mcp.json and .mcp.json use @latest so next IDE reload uses latest
|
|
773
767
|
const cwd = process.cwd();
|
|
774
768
|
for (const rel of ['.cursor/mcp.json', '.mcp.json', '.vscode/mcp.json']) {
|
|
775
769
|
const p = path.join(cwd, rel);
|
|
@@ -784,8 +778,56 @@ function triggerBackgroundSelfUpdate() {
|
|
|
784
778
|
catch { }
|
|
785
779
|
}
|
|
786
780
|
}
|
|
781
|
+
// 2. Install @agilemonster/mcp-server@latest into global/npx cache and then exit(0) so launchd respawns the daemon cleanly
|
|
782
|
+
const child = spawn('sh', ['-c', 'npm install -g @agilemonster/mcp-server@latest >/dev/null 2>&1 || npx -y @agilemonster/mcp-server@latest --version >/dev/null 2>&1'], {
|
|
783
|
+
stdio: 'ignore',
|
|
784
|
+
detached: true,
|
|
785
|
+
});
|
|
786
|
+
child.on('exit', () => {
|
|
787
|
+
appendIdeActivityLog(cwd, {
|
|
788
|
+
cardId: 'system-update',
|
|
789
|
+
cardTitle: 'MCP Daemon Auto-Update',
|
|
790
|
+
agentName: 'Agile Monster Daemon',
|
|
791
|
+
model: 'system',
|
|
792
|
+
status: 'updated',
|
|
793
|
+
summary: `Automatically updated @agilemonster/mcp-server from v${MCP_SERVER_VERSION} to v${targetVersion || 'latest'} and restarted daemon via launchd.`,
|
|
794
|
+
});
|
|
795
|
+
setTimeout(() => {
|
|
796
|
+
process.exit(0);
|
|
797
|
+
}, 1500).unref();
|
|
798
|
+
});
|
|
799
|
+
child.unref();
|
|
800
|
+
}
|
|
801
|
+
catch { }
|
|
802
|
+
}
|
|
803
|
+
function readProjectInstructionsContext(projectPath) {
|
|
804
|
+
const sections = [];
|
|
805
|
+
for (const docName of ['CLAUDE.md', 'AGENTS.md', 'README.md']) {
|
|
806
|
+
const docPath = path.join(projectPath, docName);
|
|
807
|
+
if (fs.existsSync(docPath)) {
|
|
808
|
+
try {
|
|
809
|
+
const raw = fs.readFileSync(docPath, 'utf8').trim();
|
|
810
|
+
if (raw.length > 0) {
|
|
811
|
+
sections.push(`=== ${docName} ===\n${raw.slice(0, 1800)}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch { }
|
|
815
|
+
}
|
|
816
|
+
if (sections.join('\n\n').length >= 3000)
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const recentCommits = execSync('git log -n 4 --oneline', {
|
|
821
|
+
cwd: projectPath,
|
|
822
|
+
encoding: 'utf8',
|
|
823
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
824
|
+
}).trim();
|
|
825
|
+
if (recentCommits) {
|
|
826
|
+
sections.push(`=== Recent Git Commits ===\n${recentCommits}`);
|
|
827
|
+
}
|
|
787
828
|
}
|
|
788
829
|
catch { }
|
|
830
|
+
return sections.join('\n\n').slice(0, 3800);
|
|
789
831
|
}
|
|
790
832
|
const activeAutonomousCards = new Set();
|
|
791
833
|
function collectProjectSourceFiles(projectPath, keywords) {
|
|
@@ -989,10 +1031,12 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
989
1031
|
return false;
|
|
990
1032
|
}
|
|
991
1033
|
// 3. Ask Agile Monster Autonomous Step to pick the 1-6 most relevant files
|
|
1034
|
+
const projectContext = readProjectInstructionsContext(projectPath);
|
|
992
1035
|
const selectionRes = await client.autonomousStep({
|
|
993
1036
|
card_id: card.id,
|
|
994
1037
|
action: 'select_files',
|
|
995
1038
|
file_index: fileIndex,
|
|
1039
|
+
project_context: projectContext,
|
|
996
1040
|
});
|
|
997
1041
|
const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
|
|
998
1042
|
if (selectedFiles.length === 0) {
|
|
@@ -1033,7 +1077,27 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1033
1077
|
card_id: card.id,
|
|
1034
1078
|
action: 'generate_edits',
|
|
1035
1079
|
files: filesPayload,
|
|
1080
|
+
project_context: projectContext,
|
|
1036
1081
|
});
|
|
1082
|
+
// Gate A: Strict truth — if the model is not confident / cannot find the target, ask the human instead of guessing
|
|
1083
|
+
if (editsRes.needs_clarification && (editsRes.clarification_question || '').trim() !== '') {
|
|
1084
|
+
try {
|
|
1085
|
+
if (fs.existsSync(lockFile))
|
|
1086
|
+
fs.unlinkSync(lockFile);
|
|
1087
|
+
}
|
|
1088
|
+
catch { }
|
|
1089
|
+
appendIdeActivityLog(projectPath, {
|
|
1090
|
+
cardId: card.id,
|
|
1091
|
+
cardTitle: card.title,
|
|
1092
|
+
agentName: workerLabel,
|
|
1093
|
+
model: card.agent_model || 'auto',
|
|
1094
|
+
status: 'waiting_for_input',
|
|
1095
|
+
summary: `Needs clarification: ${editsRes.clarification_question}`,
|
|
1096
|
+
files: Object.keys(filesPayload),
|
|
1097
|
+
});
|
|
1098
|
+
await client.askQuestion(card.id, provider, (editsRes.clarification_question || '').trim());
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1037
1101
|
const applyEditsPass = (editsList) => {
|
|
1038
1102
|
const modFiles = [];
|
|
1039
1103
|
for (const edit of editsList) {
|
|
@@ -1153,6 +1217,7 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1153
1217
|
card_id: card.id,
|
|
1154
1218
|
action: 'generate_edits',
|
|
1155
1219
|
files: retryFiles,
|
|
1220
|
+
project_context: projectContext,
|
|
1156
1221
|
});
|
|
1157
1222
|
const retryMods = applyEditsPass(editsRes.edits || []);
|
|
1158
1223
|
modifiedFiles.push(...retryMods);
|
|
@@ -1178,9 +1243,33 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1178
1243
|
}).trim();
|
|
1179
1244
|
}
|
|
1180
1245
|
catch { }
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
const
|
|
1246
|
+
// Gate C: Commit, push, and run the project's defined deployment pipeline, then VERIFY it actually succeeded.
|
|
1247
|
+
// If deploy_stage is 'done' or 'manual', only commit locally-verified changes and defer deployment to approval.
|
|
1248
|
+
const deployStage = (card.deploy_stage || 'review').toLowerCase();
|
|
1249
|
+
const deployInfo = deployStage === 'review'
|
|
1250
|
+
? await runProjectDeploymentPipeline(projectPath, card.title || 'update', card.id.slice(0, 8), false, card)
|
|
1251
|
+
: { ok: true, commitHash: '', branch: '', deployedNote: '', errorDetail: '' };
|
|
1252
|
+
if (!deployInfo.ok) {
|
|
1253
|
+
// Never claim success: keep card In Progress (waiting_for_input) with a plain-English diagnostic
|
|
1254
|
+
try {
|
|
1255
|
+
if (fs.existsSync(lockFile))
|
|
1256
|
+
fs.unlinkSync(lockFile);
|
|
1257
|
+
}
|
|
1258
|
+
catch { }
|
|
1259
|
+
const reason = (deployInfo.errorDetail || 'Unknown deployment error').slice(0, 700);
|
|
1260
|
+
appendIdeActivityLog(projectPath, {
|
|
1261
|
+
cardId: card.id,
|
|
1262
|
+
cardTitle: card.title,
|
|
1263
|
+
agentName: workerLabel,
|
|
1264
|
+
model: card.agent_model || 'auto',
|
|
1265
|
+
status: 'deploy_failed',
|
|
1266
|
+
summary: `Code change was made but deployment could not be verified: ${reason}`,
|
|
1267
|
+
files: modifiedFiles,
|
|
1268
|
+
diff: rawDiff,
|
|
1269
|
+
});
|
|
1270
|
+
await client.askQuestion(card.id, provider, `I made the code change (${modifiedFiles.join(', ')}), but the deployment could not be verified, so I kept this card in progress instead of moving it to Testing.\n\nWhat failed: ${reason}\n\nReply "retry" once the issue is fixed, or tell me how this project should be deployed.`);
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1184
1273
|
const baseSummary = editsRes.summary ||
|
|
1185
1274
|
`Updated ${modifiedFiles.join(', ')} to complete "${card.title}".`;
|
|
1186
1275
|
const cleanPlainSummary = baseSummary.replace(/```[\s\S]*?```/g, '').trim() +
|
|
@@ -1228,9 +1317,12 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1228
1317
|
fs.unlinkSync(lockFile);
|
|
1229
1318
|
}
|
|
1230
1319
|
catch { }
|
|
1231
|
-
|
|
1320
|
+
const buildNote = buildCheck && !buildCheck.ok && buildCheck.errorOutput
|
|
1321
|
+
? `\n\nThe attempted change did not compile, so it was reverted. Last build error:\n${buildCheck.errorOutput.split('\n').filter((l) => /error/i.test(l)).slice(0, 4).join('\n').slice(0, 600)}`
|
|
1322
|
+
: '';
|
|
1323
|
+
await client.askQuestion(card.id, provider, `I inspected ${Object.keys(filesPayload)
|
|
1232
1324
|
.map((f) => `\`${f}\``)
|
|
1233
|
-
.join(', ')}
|
|
1325
|
+
.join(', ')} but could not make a verified change automatically.${buildNote}\n\nCould you describe exactly where in the app this should appear (page or screen name) and what you expect to see?`);
|
|
1234
1326
|
return false;
|
|
1235
1327
|
}
|
|
1236
1328
|
}
|
|
@@ -1251,130 +1343,270 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1251
1343
|
}
|
|
1252
1344
|
}
|
|
1253
1345
|
/**
|
|
1254
|
-
* Detect and execute the project's defined deployment pipeline:
|
|
1255
|
-
* 1. Commits
|
|
1256
|
-
* 2. Runs
|
|
1257
|
-
*
|
|
1346
|
+
* Detect and execute the project's defined deployment pipeline, then VERIFY the result:
|
|
1347
|
+
* 1. Commits modified tracked files and pushes to `origin/<branch>`.
|
|
1348
|
+
* 2. Runs the configured/auto-detected deploy method:
|
|
1349
|
+
* - `deploy_script` (e.g. `./deploy/deploy.sh --skip-preflight`): requires exit code 0.
|
|
1350
|
+
* - `github_actions`: polls `gh run list --commit <sha>` until the workflow concludes; a failed run = not deployed.
|
|
1351
|
+
* - `docker_compose`: runs the deploy command and checks `docker compose ps` for running containers.
|
|
1352
|
+
* 3. If a `live_url` is known, performs an HTTP smoke check (must return < 400).
|
|
1353
|
+
* Returns ok=false with a plain-English `errorDetail` whenever deployment cannot be proven.
|
|
1258
1354
|
*/
|
|
1259
|
-
function runProjectDeploymentPipeline(projectPath, rawTitle, shortId, forceEmptyCommit = false) {
|
|
1260
|
-
const cleanTitle = (rawTitle || 'update').replace(/["
|
|
1355
|
+
async function runProjectDeploymentPipeline(projectPath, rawTitle, shortId, forceEmptyCommit = false, profile) {
|
|
1356
|
+
const cleanTitle = (rawTitle || 'update').replace(/["`$\\\r\n]+/g, ' ').trim();
|
|
1261
1357
|
let branch = 'main';
|
|
1262
1358
|
let commitHash = '';
|
|
1359
|
+
let fullSha = '';
|
|
1263
1360
|
const actions = [];
|
|
1361
|
+
const run = (cmd, timeout = 45000) => execSync(cmd, { cwd: projectPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout }).trim();
|
|
1362
|
+
const errText = (e) => (String(e?.stderr || '') + '\n' + String(e?.stdout || '') + '\n' + String(e?.message || ''))
|
|
1363
|
+
.split('\n')
|
|
1364
|
+
.map((l) => l.trim())
|
|
1365
|
+
.filter((l) => l.length > 0)
|
|
1366
|
+
.slice(-6)
|
|
1367
|
+
.join(' | ')
|
|
1368
|
+
.slice(0, 700);
|
|
1369
|
+
const detected = AgileMonsterClient.inspectProjectProfile(projectPath, path.basename(projectPath));
|
|
1370
|
+
const deployMethod = (profile?.deploy_method || detected.deploy_method || 'git_push').toLowerCase();
|
|
1371
|
+
const deployCommand = profile?.deploy_command || detected.deploy_command || '';
|
|
1372
|
+
const liveUrl = profile?.live_url || detected.live_url || '';
|
|
1373
|
+
// Step 1: commit & push
|
|
1264
1374
|
try {
|
|
1265
|
-
branch =
|
|
1266
|
-
|
|
1267
|
-
encoding: 'utf8',
|
|
1268
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1269
|
-
}).trim() || 'main';
|
|
1270
|
-
const statusOut = execSync('git status --porcelain -uno', {
|
|
1271
|
-
cwd: projectPath,
|
|
1272
|
-
encoding: 'utf8',
|
|
1273
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1274
|
-
}).trim();
|
|
1375
|
+
branch = run('git rev-parse --abbrev-ref HEAD') || 'main';
|
|
1376
|
+
const statusOut = run('git status --porcelain -uno');
|
|
1275
1377
|
if (statusOut.length > 0) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
cwd: projectPath,
|
|
1279
|
-
stdio: 'ignore',
|
|
1280
|
-
});
|
|
1281
|
-
execSync(`git push origin ${branch}`, {
|
|
1282
|
-
cwd: projectPath,
|
|
1283
|
-
stdio: 'ignore',
|
|
1284
|
-
timeout: 45000,
|
|
1285
|
-
});
|
|
1286
|
-
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1287
|
-
cwd: projectPath,
|
|
1288
|
-
encoding: 'utf8',
|
|
1289
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1290
|
-
}).trim();
|
|
1291
|
-
actions.push(`pushed \`${commitHash}\` to \`origin/${branch}\``);
|
|
1378
|
+
run('git add -u');
|
|
1379
|
+
run(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`);
|
|
1292
1380
|
}
|
|
1293
1381
|
else if (forceEmptyCommit) {
|
|
1294
|
-
|
|
1295
|
-
cwd: projectPath,
|
|
1296
|
-
stdio: 'ignore',
|
|
1297
|
-
});
|
|
1298
|
-
execSync(`git push origin ${branch}`, {
|
|
1299
|
-
cwd: projectPath,
|
|
1300
|
-
stdio: 'ignore',
|
|
1301
|
-
timeout: 45000,
|
|
1302
|
-
});
|
|
1303
|
-
commitHash = execSync('git rev-parse --short HEAD', {
|
|
1304
|
-
cwd: projectPath,
|
|
1305
|
-
encoding: 'utf8',
|
|
1306
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1307
|
-
}).trim();
|
|
1308
|
-
actions.push(`triggered redeploy commit \`${commitHash}\` on \`origin/${branch}\``);
|
|
1382
|
+
run(`git commit --allow-empty -m "chore: trigger redeploy [AM-${shortId}]"`);
|
|
1309
1383
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1384
|
+
fullSha = run('git rev-parse HEAD');
|
|
1385
|
+
commitHash = fullSha.slice(0, 7);
|
|
1386
|
+
if (fs.existsSync(path.join(projectPath, '.git')) && run('git remote').length > 0) {
|
|
1387
|
+
try {
|
|
1388
|
+
run(`git push origin ${branch}`, 60000);
|
|
1389
|
+
actions.push(`pushed \`${commitHash}\` to \`origin/${branch}\``);
|
|
1390
|
+
}
|
|
1391
|
+
catch (pushErr) {
|
|
1392
|
+
return {
|
|
1393
|
+
ok: false,
|
|
1394
|
+
commitHash,
|
|
1395
|
+
branch,
|
|
1396
|
+
deployedNote: '',
|
|
1397
|
+
errorDetail: `git push to origin/${branch} failed (check credentials or branch protection): ${errText(pushErr)}`,
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1316
1400
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1401
|
+
}
|
|
1402
|
+
catch (gitErr) {
|
|
1403
|
+
return { ok: false, commitHash, branch, deployedNote: '', errorDetail: `Git commit failed: ${errText(gitErr)}` };
|
|
1404
|
+
}
|
|
1405
|
+
// Step 2: run & verify the deploy method
|
|
1406
|
+
try {
|
|
1407
|
+
if (deployMethod === 'deploy_script') {
|
|
1408
|
+
const cmd = deployCommand && !deployCommand.includes('(')
|
|
1409
|
+
? deployCommand
|
|
1410
|
+
: './deploy/deploy.sh';
|
|
1411
|
+
const scriptPath = cmd.split(' ')[0];
|
|
1412
|
+
const args = cmd.split(' ').slice(1).join(' ');
|
|
1413
|
+
try {
|
|
1414
|
+
run(`bash "${path.resolve(projectPath, scriptPath)}" ${args}`, 300000);
|
|
1415
|
+
}
|
|
1416
|
+
catch (dErr) {
|
|
1417
|
+
return {
|
|
1418
|
+
ok: false,
|
|
1419
|
+
commitHash,
|
|
1420
|
+
branch,
|
|
1421
|
+
deployedNote: '',
|
|
1422
|
+
errorDetail: `Deploy script \`${cmd}\` exited with an error: ${errText(dErr)}`,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
actions.push(`deployed via \`${cmd}\` (exit 0)`);
|
|
1328
1426
|
}
|
|
1329
|
-
else if (
|
|
1330
|
-
|
|
1427
|
+
else if (deployMethod === 'github_actions') {
|
|
1428
|
+
let hasGh = false;
|
|
1429
|
+
try {
|
|
1430
|
+
run('gh --version', 5000);
|
|
1431
|
+
hasGh = true;
|
|
1432
|
+
}
|
|
1433
|
+
catch { }
|
|
1434
|
+
if (!hasGh) {
|
|
1435
|
+
actions.push('triggered GitHub Actions (could not verify: `gh` CLI not installed)');
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
1438
|
+
const deadline = Date.now() + 8 * 60 * 1000;
|
|
1439
|
+
let verdict = 'none';
|
|
1440
|
+
let failedName = '';
|
|
1441
|
+
let runUrl = '';
|
|
1442
|
+
// Give GitHub a moment to register the workflow run
|
|
1443
|
+
await new Promise((r) => setTimeout(r, 8000));
|
|
1444
|
+
while (Date.now() < deadline) {
|
|
1445
|
+
let runs = [];
|
|
1446
|
+
try {
|
|
1447
|
+
runs = JSON.parse(run(`gh run list --commit ${fullSha} --json status,conclusion,name,url --limit 10`, 20000) || '[]');
|
|
1448
|
+
}
|
|
1449
|
+
catch { }
|
|
1450
|
+
if (runs.length === 0) {
|
|
1451
|
+
verdict = 'none';
|
|
1452
|
+
}
|
|
1453
|
+
else if (runs.some((r) => r.status !== 'completed')) {
|
|
1454
|
+
verdict = 'pending';
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
const failed = runs.find((r) => r.conclusion !== 'success' && r.conclusion !== 'skipped');
|
|
1458
|
+
verdict = failed ? 'failure' : 'success';
|
|
1459
|
+
failedName = failed?.name || '';
|
|
1460
|
+
runUrl = (failed || runs[0])?.url || '';
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
if (verdict === 'none' && Date.now() > deadline - 7 * 60 * 1000)
|
|
1464
|
+
break; // no workflow for this commit after ~1 min
|
|
1465
|
+
await new Promise((r) => setTimeout(r, 15000));
|
|
1466
|
+
}
|
|
1467
|
+
if (verdict === 'failure') {
|
|
1468
|
+
return {
|
|
1469
|
+
ok: false,
|
|
1470
|
+
commitHash,
|
|
1471
|
+
branch,
|
|
1472
|
+
deployedNote: '',
|
|
1473
|
+
errorDetail: `GitHub Actions workflow "${failedName}" failed for commit ${commitHash}. ${runUrl}`.trim(),
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
if (verdict === 'pending') {
|
|
1477
|
+
return {
|
|
1478
|
+
ok: false,
|
|
1479
|
+
commitHash,
|
|
1480
|
+
branch,
|
|
1481
|
+
deployedNote: '',
|
|
1482
|
+
errorDetail: `GitHub Actions for commit ${commitHash} was still running after 8 minutes. ${runUrl}`.trim(),
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
actions.push(verdict === 'success'
|
|
1486
|
+
? `GitHub Actions passed for \`${commitHash}\``
|
|
1487
|
+
: 'pushed (no GitHub Actions workflow ran for this commit)');
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
else if (deployMethod === 'docker_compose') {
|
|
1491
|
+
const cmd = deployCommand || 'docker compose up -d --build';
|
|
1492
|
+
try {
|
|
1493
|
+
run(cmd, 600000);
|
|
1494
|
+
const ps = run('docker compose ps --format json', 20000);
|
|
1495
|
+
if (/"State"\s*:\s*"(exited|dead|restarting)"/i.test(ps)) {
|
|
1496
|
+
return { ok: false, commitHash, branch, deployedNote: '', errorDetail: 'A Docker container is not running after deploy (exited/restarting).' };
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
catch (dErr) {
|
|
1500
|
+
return { ok: false, commitHash, branch, deployedNote: '', errorDetail: `Docker deploy failed: ${errText(dErr)}` };
|
|
1501
|
+
}
|
|
1502
|
+
actions.push('Docker containers rebuilt and running');
|
|
1331
1503
|
}
|
|
1332
|
-
const deployedNote = actions.length > 0
|
|
1333
|
-
? `🚀 **Deployed**: Automatically ${actions.join(' and ')}.`
|
|
1334
|
-
: `🚀 **Deployed**: Verified commit \`${commitHash}\` is live on \`origin/${branch}\`.`;
|
|
1335
|
-
return { ok: true, commitHash, branch, deployedNote };
|
|
1336
1504
|
}
|
|
1337
|
-
catch (
|
|
1338
|
-
return {
|
|
1339
|
-
ok: false,
|
|
1340
|
-
commitHash,
|
|
1341
|
-
branch,
|
|
1342
|
-
deployedNote: '',
|
|
1343
|
-
};
|
|
1505
|
+
catch (anyErr) {
|
|
1506
|
+
return { ok: false, commitHash, branch, deployedNote: '', errorDetail: errText(anyErr) };
|
|
1344
1507
|
}
|
|
1508
|
+
// Step 3: live URL smoke check
|
|
1509
|
+
if (liveUrl && /^https?:\/\//i.test(liveUrl)) {
|
|
1510
|
+
let status = 0;
|
|
1511
|
+
for (let attempt = 0; attempt < 3 && (status === 0 || status >= 400); attempt++) {
|
|
1512
|
+
try {
|
|
1513
|
+
const ctrl = new AbortController();
|
|
1514
|
+
const t = setTimeout(() => ctrl.abort(), 15000);
|
|
1515
|
+
const res = await fetch(liveUrl, { method: 'GET', redirect: 'follow', signal: ctrl.signal });
|
|
1516
|
+
clearTimeout(t);
|
|
1517
|
+
status = res.status;
|
|
1518
|
+
}
|
|
1519
|
+
catch {
|
|
1520
|
+
status = 0;
|
|
1521
|
+
}
|
|
1522
|
+
if (status === 0 || status >= 400)
|
|
1523
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
1524
|
+
}
|
|
1525
|
+
if (status === 0 || status >= 400) {
|
|
1526
|
+
return {
|
|
1527
|
+
ok: false,
|
|
1528
|
+
commitHash,
|
|
1529
|
+
branch,
|
|
1530
|
+
deployedNote: '',
|
|
1531
|
+
errorDetail: `Deployment finished, but ${liveUrl} returned ${status === 0 ? 'no response' : `HTTP ${status}`}.`,
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
actions.push(`verified live at ${liveUrl} (HTTP ${status})`);
|
|
1535
|
+
}
|
|
1536
|
+
const deployedNote = actions.length > 0
|
|
1537
|
+
? `🚀 **Deployed**: ${actions.join(', ')}.`
|
|
1538
|
+
: `🚀 **Deployed**: commit \`${commitHash}\` on \`${branch}\`.`;
|
|
1539
|
+
return { ok: true, commitHash, branch, deployedNote, errorDetail: '' };
|
|
1345
1540
|
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Deploy a card the human approved into Done (or flagged as `deploying`).
|
|
1543
|
+
* Idempotent: keyed on card ID only (never on `updated_at`, which changes whenever we post a comment),
|
|
1544
|
+
* so a card is deployed at most once unless the human explicitly re-triggers it into `deploying`.
|
|
1545
|
+
*/
|
|
1346
1546
|
export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
1347
1547
|
if (!projectPath || !fs.existsSync(projectPath))
|
|
1348
1548
|
return false;
|
|
1349
1549
|
const shortId = card.id.slice(0, 8);
|
|
1350
1550
|
const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
|
|
1351
|
-
|
|
1551
|
+
let existing = null;
|
|
1352
1552
|
if (fs.existsSync(deployLock)) {
|
|
1353
1553
|
try {
|
|
1354
|
-
|
|
1355
|
-
if (!cardUpdatedAt || existing?.card_updated_at === cardUpdatedAt) {
|
|
1356
|
-
return false;
|
|
1357
|
-
}
|
|
1554
|
+
existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
|
|
1358
1555
|
}
|
|
1359
1556
|
catch {
|
|
1557
|
+
existing = { status: 'deployed' }; // unreadable lock: fail safe (never loop)
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
const explicitRedeploy = card.agent_status === 'deploying';
|
|
1561
|
+
if (existing) {
|
|
1562
|
+
// Already handled and the human has not explicitly asked for a re-deploy
|
|
1563
|
+
if (!explicitRedeploy)
|
|
1360
1564
|
return false;
|
|
1565
|
+
// Re-deploy request already being processed / processed for this exact request
|
|
1566
|
+
if (existing.redeploy_of === card.updated_at || existing.status === 'running') {
|
|
1567
|
+
const age = Date.now() - (Date.parse(existing.started_at || '') || 0);
|
|
1568
|
+
if (existing.status !== 'running' || age < 15 * 60 * 1000)
|
|
1569
|
+
return false;
|
|
1361
1570
|
}
|
|
1362
1571
|
}
|
|
1572
|
+
// Cards that were already deployed during the Review stage only need the Done confirmation, not another deploy
|
|
1573
|
+
const deployStage = (card.deploy_stage || 'review').toLowerCase();
|
|
1363
1574
|
const provider = card.agent_provider || 'cursor';
|
|
1364
1575
|
const rawText = `${card.title || ''} ${card.description || ''}`;
|
|
1365
1576
|
const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
|
|
1577
|
+
const writeLock = (status, extra = {}) => {
|
|
1578
|
+
try {
|
|
1579
|
+
fs.writeFileSync(deployLock, JSON.stringify({
|
|
1580
|
+
card_id: card.id,
|
|
1581
|
+
status,
|
|
1582
|
+
title: card.title,
|
|
1583
|
+
redeploy_of: explicitRedeploy ? card.updated_at : null,
|
|
1584
|
+
started_at: new Date().toISOString(),
|
|
1585
|
+
...extra,
|
|
1586
|
+
}), 'utf8');
|
|
1587
|
+
}
|
|
1588
|
+
catch { }
|
|
1589
|
+
};
|
|
1590
|
+
writeLock('running');
|
|
1366
1591
|
try {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1592
|
+
if (deployStage === 'manual') {
|
|
1593
|
+
writeLock('deployed', { note: 'manual deploy stage' });
|
|
1594
|
+
return false;
|
|
1595
|
+
}
|
|
1596
|
+
if (deployStage === 'review' && !explicitRedeploy && !isContainerRestartCard) {
|
|
1597
|
+
// Already deployed + verified before moving to Testing — nothing more to run.
|
|
1598
|
+
writeLock('deployed', { note: 'deployed at review stage' });
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
const res = await runProjectDeploymentPipeline(projectPath, card.title || 'update', shortId, isContainerRestartCard, card);
|
|
1602
|
+
if (!res.ok) {
|
|
1603
|
+
writeLock('failed', { error: res.errorDetail });
|
|
1604
|
+
await client.blockTask(card.id, provider, `⚠️ Deployment could not be verified, so this card was NOT marked as deployed.\n\nWhat failed: ${res.errorDetail}\n\nFix the issue and move the card back to Done (or set it to Deploying) to retry.`);
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
const msg = `${res.deployedNote} Confirmed and moved card to **Done**.`;
|
|
1377
1608
|
await client.completeTask(card.id, provider, msg);
|
|
1609
|
+
writeLock('deployed', { commit: res.commitHash, completed_at: new Date().toISOString() });
|
|
1378
1610
|
appendIdeActivityLog(projectPath, {
|
|
1379
1611
|
cardId: card.id,
|
|
1380
1612
|
cardTitle: card.title,
|
|
@@ -1383,18 +1615,60 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
1383
1615
|
status: 'done',
|
|
1384
1616
|
summary: msg,
|
|
1385
1617
|
});
|
|
1386
|
-
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${res.commitHash})
|
|
1618
|
+
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${res.commitHash})`);
|
|
1387
1619
|
return true;
|
|
1388
1620
|
}
|
|
1389
1621
|
catch (err) {
|
|
1390
|
-
|
|
1391
|
-
await client.addProgress(card.id, provider, `⚠️ **[Auto-Deploy Note]** Attempted to deploy approved changes in \`${projectPath}\`: ${err?.message || String(err)}`);
|
|
1392
|
-
}
|
|
1393
|
-
catch { }
|
|
1622
|
+
writeLock('failed', { error: String(err?.message || err).slice(0, 500) });
|
|
1394
1623
|
return false;
|
|
1395
1624
|
}
|
|
1396
1625
|
}
|
|
1397
|
-
|
|
1626
|
+
/**
|
|
1627
|
+
* Machine-wide single-worker election. Only ONE process per machine may claim, edit, or deploy cards.
|
|
1628
|
+
* IDE-spawned MCP instances (Cursor, Claude Code, Antigravity, …) can be long-lived and run stale code,
|
|
1629
|
+
* so they defer to the background pulse daemon (role 'daemon' always wins) or to any live, fresh holder.
|
|
1630
|
+
*/
|
|
1631
|
+
const WORKER_LOCK_STALE_MS = 75 * 1000;
|
|
1632
|
+
function isPidAlive(pid) {
|
|
1633
|
+
try {
|
|
1634
|
+
process.kill(pid, 0);
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
catch {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
export function acquireWorkerLock(role) {
|
|
1642
|
+
const lockPath = path.join(os.homedir(), '.agilemonster', 'worker.lock.json');
|
|
1643
|
+
try {
|
|
1644
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
1645
|
+
let holder = null;
|
|
1646
|
+
if (fs.existsSync(lockPath)) {
|
|
1647
|
+
try {
|
|
1648
|
+
holder = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
1649
|
+
}
|
|
1650
|
+
catch {
|
|
1651
|
+
holder = null;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const holderLive = !!holder?.pid &&
|
|
1655
|
+
holder.pid !== process.pid &&
|
|
1656
|
+
isPidAlive(holder.pid) &&
|
|
1657
|
+
Date.now() - (holder.heartbeat || 0) < WORKER_LOCK_STALE_MS;
|
|
1658
|
+
if (holderLive) {
|
|
1659
|
+
const holderIsDaemon = holder?.role === 'daemon';
|
|
1660
|
+
// A daemon only yields to another live daemon; stdio instances always yield to a live holder.
|
|
1661
|
+
if (role === 'stdio' || holderIsDaemon)
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, role, version: MCP_SERVER_VERSION, heartbeat: Date.now() }), 'utf8');
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
catch {
|
|
1668
|
+
return role === 'daemon';
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
export function startBuiltInPullPulse(server, client, intervalMs = 20000, role = 'stdio') {
|
|
1398
1672
|
const notifiedCardIds = new Set();
|
|
1399
1673
|
let updateTriggered = false;
|
|
1400
1674
|
let pulseCount = 0;
|
|
@@ -1490,28 +1764,31 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
|
|
|
1490
1764
|
catch { }
|
|
1491
1765
|
}
|
|
1492
1766
|
}
|
|
1493
|
-
// Zero-touch autonomous execution
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1767
|
+
// Zero-touch autonomous execution & deployment — ONLY the elected machine-wide worker may do this
|
|
1768
|
+
const isWorker = acquireWorkerLock(role);
|
|
1769
|
+
if (isWorker) {
|
|
1770
|
+
const boundQueued = res.all_bound_queued_cards || [];
|
|
1771
|
+
for (const qCard of boundQueued) {
|
|
1772
|
+
const targetPath = (qCard.bound_project_path || '').trim() ||
|
|
1773
|
+
(qCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
|
|
1774
|
+
if (targetPath && fs.existsSync(targetPath)) {
|
|
1775
|
+
await executeQueuedCardAutonomously(client, qCard, targetPath);
|
|
1776
|
+
}
|
|
1500
1777
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
await deployApprovedCardIfNeeded(client, dCard, targetPath);
|
|
1778
|
+
const boundDone = res.all_bound_done_cards || [];
|
|
1779
|
+
for (const dCard of boundDone) {
|
|
1780
|
+
const targetPath = (dCard.bound_project_path || '').trim() ||
|
|
1781
|
+
(dCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
|
|
1782
|
+
if (targetPath && fs.existsSync(targetPath)) {
|
|
1783
|
+
await deployApprovedCardIfNeeded(client, dCard, targetPath);
|
|
1784
|
+
}
|
|
1509
1785
|
}
|
|
1510
1786
|
}
|
|
1511
1787
|
if ((res.version?.force_update_requested || res.version?.update_available) &&
|
|
1512
|
-
!updateTriggered
|
|
1788
|
+
!updateTriggered &&
|
|
1789
|
+
role === 'daemon') {
|
|
1513
1790
|
updateTriggered = true;
|
|
1514
|
-
triggerBackgroundSelfUpdate();
|
|
1791
|
+
triggerBackgroundSelfUpdate(res.version?.latest_version);
|
|
1515
1792
|
}
|
|
1516
1793
|
}
|
|
1517
1794
|
catch {
|
|
@@ -1528,25 +1805,20 @@ export async function runStdioServer() {
|
|
|
1528
1805
|
const server = createServer(client);
|
|
1529
1806
|
const transport = new StdioServerTransport();
|
|
1530
1807
|
await server.connect(transport);
|
|
1531
|
-
startBuiltInPullPulse(server, client, 20000);
|
|
1808
|
+
startBuiltInPullPulse(server, client, 20000, 'stdio');
|
|
1532
1809
|
}
|
|
1533
1810
|
export async function runBackgroundPulseDaemon(intervalMs = 20000) {
|
|
1534
1811
|
const client = new AgileMonsterClient();
|
|
1535
1812
|
console.log(`[Agile Monster Pulse Daemon v${MCP_SERVER_VERSION}] Listening for queued agent tasks across all bound repositories (interval: ${intervalMs / 1000}s)...`);
|
|
1536
|
-
startBuiltInPullPulse(null, client, intervalMs);
|
|
1813
|
+
startBuiltInPullPulse(null, client, intervalMs, 'daemon');
|
|
1537
1814
|
const selfScript = process.argv[1] || '';
|
|
1538
1815
|
const initialMtime = selfScript && fs.existsSync(selfScript) ? fs.statSync(selfScript).mtimeMs : 0;
|
|
1539
|
-
// Auto-restart daemon if mcp-server binary/dist on disk is updated
|
|
1816
|
+
// Auto-restart daemon if mcp-server binary/dist on disk is updated (launchd KeepAlive respawns it cleanly)
|
|
1540
1817
|
setInterval(() => {
|
|
1541
1818
|
try {
|
|
1542
1819
|
if (selfScript && initialMtime > 0 && fs.existsSync(selfScript)) {
|
|
1543
1820
|
const curMtime = fs.statSync(selfScript).mtimeMs;
|
|
1544
1821
|
if (curMtime > initialMtime + 1000) {
|
|
1545
|
-
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
1546
|
-
detached: true,
|
|
1547
|
-
stdio: 'ignore',
|
|
1548
|
-
});
|
|
1549
|
-
child.unref();
|
|
1550
1822
|
process.exit(0);
|
|
1551
1823
|
}
|
|
1552
1824
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface Card {
|
|
|
34
34
|
updated_at?: string;
|
|
35
35
|
bound_project_path?: string | null;
|
|
36
36
|
bound_project_name?: string | null;
|
|
37
|
+
fallback_model?: string | null;
|
|
38
|
+
build_command?: string | null;
|
|
39
|
+
deploy_method?: string | null;
|
|
40
|
+
deploy_command?: string | null;
|
|
41
|
+
live_url?: string | null;
|
|
42
|
+
deploy_stage?: string | null;
|
|
37
43
|
}
|
|
38
44
|
export interface Board {
|
|
39
45
|
id: string;
|
|
@@ -63,9 +69,17 @@ export interface BoardAgentConfig {
|
|
|
63
69
|
custom_name?: string;
|
|
64
70
|
short_name?: string | null;
|
|
65
71
|
display_name?: string;
|
|
72
|
+
preferred_model?: string;
|
|
73
|
+
fallback_model?: string | null;
|
|
74
|
+
is_favorite?: boolean;
|
|
66
75
|
project_name?: string | null;
|
|
67
76
|
project_path?: string | null;
|
|
68
77
|
git_remote?: string | null;
|
|
78
|
+
build_command?: string | null;
|
|
79
|
+
deploy_method?: string | null;
|
|
80
|
+
deploy_command?: string | null;
|
|
81
|
+
live_url?: string | null;
|
|
82
|
+
deploy_stage?: string | null;
|
|
69
83
|
mcp_version?: string | null;
|
|
70
84
|
connected?: boolean;
|
|
71
85
|
last_connected_at?: string | null;
|
|
@@ -83,6 +97,10 @@ export interface DiscoveredProject {
|
|
|
83
97
|
name: string;
|
|
84
98
|
path: string;
|
|
85
99
|
git_remote?: string | null;
|
|
100
|
+
build_command?: string | null;
|
|
101
|
+
deploy_method?: string | null;
|
|
102
|
+
deploy_command?: string | null;
|
|
103
|
+
live_url?: string | null;
|
|
86
104
|
}
|
|
87
105
|
export interface PulseResponse {
|
|
88
106
|
board_id: string;
|
|
@@ -99,6 +117,13 @@ export interface PulseResponse {
|
|
|
99
117
|
provider: string;
|
|
100
118
|
project_name: string;
|
|
101
119
|
project_path?: string | null;
|
|
120
|
+
preferred_model?: string | null;
|
|
121
|
+
fallback_model?: string | null;
|
|
122
|
+
build_command?: string | null;
|
|
123
|
+
deploy_method?: string | null;
|
|
124
|
+
deploy_command?: string | null;
|
|
125
|
+
live_url?: string | null;
|
|
126
|
+
deploy_stage?: string | null;
|
|
102
127
|
}>;
|
|
103
128
|
}
|
|
104
129
|
export interface AutonomousEdit {
|
|
@@ -111,6 +136,8 @@ export interface AutonomousEdit {
|
|
|
111
136
|
export interface AutonomousStepResponse {
|
|
112
137
|
card_id: string;
|
|
113
138
|
action?: 'select_files' | 'generate_edits';
|
|
139
|
+
needs_clarification?: boolean;
|
|
140
|
+
clarification_question?: string;
|
|
114
141
|
selected_files?: string[];
|
|
115
142
|
target_files?: string[];
|
|
116
143
|
edits?: AutonomousEdit[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilemonster/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Official Model Context Protocol (MCP) server & Orchestrator for Agile Monster (Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf)",
|
|
6
6
|
"main": "dist/server.js",
|