@agilemonster/mcp-server 1.0.14 → 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 +578 -201
- 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) {
|
|
@@ -988,15 +1030,31 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
988
1030
|
await client.addProgress(card.id, provider, `Card is now **In Progress**, but no matching source files were found in \`${projectPath}\`.`);
|
|
989
1031
|
return false;
|
|
990
1032
|
}
|
|
991
|
-
// 3. Ask Agile Monster Autonomous Step to pick the 1-
|
|
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
|
-
const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0,
|
|
1041
|
+
const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
|
|
998
1042
|
if (selectedFiles.length === 0) {
|
|
999
|
-
selectedFiles.push(...fileIndex.slice(0,
|
|
1043
|
+
selectedFiles.push(...fileIndex.slice(0, 4));
|
|
1044
|
+
}
|
|
1045
|
+
// Automatically include project type definition files (e.g. types.ts,frontend/src/lib/types.ts) so the model knows exact schemas
|
|
1046
|
+
for (const candidateTypeFile of [
|
|
1047
|
+
'frontend/src/lib/types.ts',
|
|
1048
|
+
'src/lib/types.ts',
|
|
1049
|
+
'src/types.ts',
|
|
1050
|
+
'src/types/index.ts',
|
|
1051
|
+
'frontend/src/types.ts',
|
|
1052
|
+
]) {
|
|
1053
|
+
if (fileIndex.includes(candidateTypeFile) &&
|
|
1054
|
+
!selectedFiles.includes(candidateTypeFile) &&
|
|
1055
|
+
selectedFiles.length < 7) {
|
|
1056
|
+
selectedFiles.push(candidateTypeFile);
|
|
1057
|
+
}
|
|
1000
1058
|
}
|
|
1001
1059
|
const filesPayload = {};
|
|
1002
1060
|
for (const rel of selectedFiles) {
|
|
@@ -1015,93 +1073,156 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1015
1073
|
.map((f) => `\`${f}\``)
|
|
1016
1074
|
.join(', ')}. Generating code changes...`);
|
|
1017
1075
|
// 4. Generate surgical code edits
|
|
1018
|
-
|
|
1076
|
+
let editsRes = await client.autonomousStep({
|
|
1019
1077
|
card_id: card.id,
|
|
1020
1078
|
action: 'generate_edits',
|
|
1021
1079
|
files: filesPayload,
|
|
1080
|
+
project_context: projectContext,
|
|
1022
1081
|
});
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
if (!relPath || !edit.search || typeof edit.replace !== 'string')
|
|
1029
|
-
continue;
|
|
1030
|
-
const fullPath = path.resolve(projectPath, relPath);
|
|
1031
|
-
if (!fullPath.startsWith(path.resolve(projectPath)))
|
|
1032
|
-
continue;
|
|
1033
|
-
if (!fs.existsSync(fullPath))
|
|
1034
|
-
continue;
|
|
1035
|
-
const currentContent = fs.readFileSync(fullPath, 'utf8');
|
|
1036
|
-
if (!originalContents.has(fullPath)) {
|
|
1037
|
-
originalContents.set(fullPath, currentContent);
|
|
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);
|
|
1038
1087
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
+
}
|
|
1101
|
+
const applyEditsPass = (editsList) => {
|
|
1102
|
+
const modFiles = [];
|
|
1103
|
+
for (const edit of editsList) {
|
|
1104
|
+
const relPath = edit.file || edit.path;
|
|
1105
|
+
if (!relPath || !edit.search || typeof edit.replace !== 'string')
|
|
1106
|
+
continue;
|
|
1107
|
+
const fullPath = path.resolve(projectPath, relPath);
|
|
1108
|
+
if (!fullPath.startsWith(path.resolve(projectPath)))
|
|
1109
|
+
continue;
|
|
1110
|
+
if (!fs.existsSync(fullPath))
|
|
1111
|
+
continue;
|
|
1112
|
+
const currentContent = fs.readFileSync(fullPath, 'utf8');
|
|
1113
|
+
if (!originalContents.has(fullPath)) {
|
|
1114
|
+
originalContents.set(fullPath, currentContent);
|
|
1115
|
+
}
|
|
1116
|
+
if (currentContent.includes(edit.search)) {
|
|
1117
|
+
const updatedContent = currentContent.replace(edit.search, edit.replace);
|
|
1118
|
+
if (updatedContent !== currentContent) {
|
|
1119
|
+
const countChar = (s, ch) => s.split(ch).length - 1;
|
|
1120
|
+
const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
|
|
1121
|
+
const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
|
|
1122
|
+
const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
|
|
1123
|
+
const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
|
|
1124
|
+
if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
fs.writeFileSync(fullPath, updatedContent, 'utf8');
|
|
1128
|
+
if (relPath.endsWith('.dart')) {
|
|
1129
|
+
try {
|
|
1130
|
+
execSync(`dart analyze "${fullPath}"`, {
|
|
1131
|
+
cwd: projectPath,
|
|
1132
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1133
|
+
timeout: 25000,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
catch (analyzeErr) {
|
|
1137
|
+
const out = String(analyzeErr?.stdout || '') + String(analyzeErr?.stderr || '');
|
|
1138
|
+
if (/\berror\s+[•-]/i.test(out)) {
|
|
1139
|
+
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1060
1143
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1144
|
+
else if (relPath.endsWith('.php')) {
|
|
1145
|
+
try {
|
|
1146
|
+
execSync(`php -l "${fullPath}"`, {
|
|
1147
|
+
cwd: projectPath,
|
|
1148
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1149
|
+
timeout: 10000,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1065
1153
|
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1066
1154
|
continue;
|
|
1067
1155
|
}
|
|
1068
1156
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
try {
|
|
1072
|
-
execSync(`php -l "${fullPath}"`, {
|
|
1073
|
-
cwd: projectPath,
|
|
1074
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1075
|
-
timeout: 10000,
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
catch {
|
|
1079
|
-
fs.writeFileSync(fullPath, currentContent, 'utf8');
|
|
1080
|
-
continue;
|
|
1157
|
+
if (!modFiles.includes(relPath)) {
|
|
1158
|
+
modFiles.push(relPath);
|
|
1081
1159
|
}
|
|
1082
1160
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return modFiles;
|
|
1164
|
+
};
|
|
1165
|
+
const originalContents = new Map();
|
|
1166
|
+
const modifiedFiles = applyEditsPass(editsRes.edits || []);
|
|
1167
|
+
// Pre-submit frontend build verification gate for root AND monorepo subdirectories (frontend/, web/, client/)
|
|
1168
|
+
const verifyFrontendBuilds = () => {
|
|
1169
|
+
const hasFrontendEdits = modifiedFiles.some((f) => /\.(tsx?|jsx?|vue|css)$/i.test(f));
|
|
1170
|
+
if (!hasFrontendEdits)
|
|
1171
|
+
return { ok: true };
|
|
1172
|
+
const candidateDirs = [
|
|
1173
|
+
projectPath,
|
|
1174
|
+
path.join(projectPath, 'frontend'),
|
|
1175
|
+
path.join(projectPath, 'web'),
|
|
1176
|
+
path.join(projectPath, 'client'),
|
|
1177
|
+
];
|
|
1178
|
+
for (const dir of candidateDirs) {
|
|
1179
|
+
const pkgJsonPath = path.join(dir, 'package.json');
|
|
1180
|
+
if (!fs.existsSync(pkgJsonPath) || !fs.existsSync(path.join(dir, 'node_modules')))
|
|
1181
|
+
continue;
|
|
1182
|
+
try {
|
|
1183
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
1184
|
+
if (pkg?.scripts?.build) {
|
|
1185
|
+
const buildCmd = fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))
|
|
1186
|
+
? 'pnpm run build'
|
|
1187
|
+
: 'npm run build';
|
|
1188
|
+
execSync(buildCmd, {
|
|
1189
|
+
cwd: dir,
|
|
1190
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1191
|
+
timeout: 60000,
|
|
1192
|
+
});
|
|
1085
1193
|
}
|
|
1086
1194
|
}
|
|
1195
|
+
catch (buildErr) {
|
|
1196
|
+
const errOut = (String(buildErr?.stdout || '') + '\n' + String(buildErr?.stderr || '')).trim();
|
|
1197
|
+
return { ok: false, errorOutput: errOut.slice(-1500) };
|
|
1198
|
+
}
|
|
1087
1199
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
execSync('npm run build', {
|
|
1097
|
-
cwd: projectPath,
|
|
1098
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1099
|
-
timeout: 45000,
|
|
1100
|
-
});
|
|
1200
|
+
return { ok: true };
|
|
1201
|
+
};
|
|
1202
|
+
let buildCheck = verifyFrontendBuilds();
|
|
1203
|
+
if (!buildCheck.ok) {
|
|
1204
|
+
// Revert first pass, attach compiler error context, and do 1 self-correction pass
|
|
1205
|
+
for (const [fPath, orig] of originalContents.entries()) {
|
|
1206
|
+
try {
|
|
1207
|
+
fs.writeFileSync(fPath, orig, 'utf8');
|
|
1101
1208
|
}
|
|
1209
|
+
catch { }
|
|
1102
1210
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1211
|
+
modifiedFiles.length = 0;
|
|
1212
|
+
const retryFiles = {
|
|
1213
|
+
...filesPayload,
|
|
1214
|
+
'__COMPILER_ERROR_TO_FIX__.txt': `The previous edit failed to compile with:\n${buildCheck.errorOutput}\nPlease fix the TypeScript/build error using the exact property names from types.ts.`,
|
|
1215
|
+
};
|
|
1216
|
+
editsRes = await client.autonomousStep({
|
|
1217
|
+
card_id: card.id,
|
|
1218
|
+
action: 'generate_edits',
|
|
1219
|
+
files: retryFiles,
|
|
1220
|
+
project_context: projectContext,
|
|
1221
|
+
});
|
|
1222
|
+
const retryMods = applyEditsPass(editsRes.edits || []);
|
|
1223
|
+
modifiedFiles.push(...retryMods);
|
|
1224
|
+
buildCheck = verifyFrontendBuilds();
|
|
1225
|
+
if (!buildCheck.ok) {
|
|
1105
1226
|
for (const [fPath, orig] of originalContents.entries()) {
|
|
1106
1227
|
try {
|
|
1107
1228
|
fs.writeFileSync(fPath, orig, 'utf8');
|
|
@@ -1122,19 +1243,45 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1122
1243
|
}).trim();
|
|
1123
1244
|
}
|
|
1124
1245
|
catch { }
|
|
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
|
+
}
|
|
1125
1273
|
const baseSummary = editsRes.summary ||
|
|
1126
1274
|
`Updated ${modifiedFiles.join(', ')} to complete "${card.title}".`;
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
1130
|
-
.trim();
|
|
1275
|
+
const cleanPlainSummary = baseSummary.replace(/```[\s\S]*?```/g, '').trim() +
|
|
1276
|
+
(deployInfo.deployedNote ? `\n\n${deployInfo.deployedNote}` : '');
|
|
1131
1277
|
const verificationSteps = editsRes.verification_steps ||
|
|
1132
|
-
`1. Check the
|
|
1278
|
+
`1. Check the live update for "${card.title}".\n2. Click **Approve & Deploy to Done** when verified.`;
|
|
1133
1279
|
const logLines = [
|
|
1134
1280
|
`Claimed card "${card.title}" (${card.id}) in ${projectPath}`,
|
|
1135
1281
|
`Agent: ${workerLabel} | Model: ${card.agent_model || 'auto'}`,
|
|
1136
1282
|
`Selected files: ${Object.keys(filesPayload).join(', ')}`,
|
|
1137
1283
|
`Modified & verified files: ${modifiedFiles.join(', ')}`,
|
|
1284
|
+
...(deployInfo.deployedNote ? [`Deployment: ${deployInfo.deployedNote}`] : []),
|
|
1138
1285
|
`Summary: ${cleanPlainSummary}`,
|
|
1139
1286
|
];
|
|
1140
1287
|
appendIdeActivityLog(projectPath, {
|
|
@@ -1170,9 +1317,12 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1170
1317
|
fs.unlinkSync(lockFile);
|
|
1171
1318
|
}
|
|
1172
1319
|
catch { }
|
|
1173
|
-
|
|
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)
|
|
1174
1324
|
.map((f) => `\`${f}\``)
|
|
1175
|
-
.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?`);
|
|
1176
1326
|
return false;
|
|
1177
1327
|
}
|
|
1178
1328
|
}
|
|
@@ -1192,121 +1342,333 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1192
1342
|
activeAutonomousCards.delete(card.id);
|
|
1193
1343
|
}
|
|
1194
1344
|
}
|
|
1345
|
+
/**
|
|
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.
|
|
1354
|
+
*/
|
|
1355
|
+
async function runProjectDeploymentPipeline(projectPath, rawTitle, shortId, forceEmptyCommit = false, profile) {
|
|
1356
|
+
const cleanTitle = (rawTitle || 'update').replace(/["`$\\\r\n]+/g, ' ').trim();
|
|
1357
|
+
let branch = 'main';
|
|
1358
|
+
let commitHash = '';
|
|
1359
|
+
let fullSha = '';
|
|
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
|
|
1374
|
+
try {
|
|
1375
|
+
branch = run('git rev-parse --abbrev-ref HEAD') || 'main';
|
|
1376
|
+
const statusOut = run('git status --porcelain -uno');
|
|
1377
|
+
if (statusOut.length > 0) {
|
|
1378
|
+
run('git add -u');
|
|
1379
|
+
run(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`);
|
|
1380
|
+
}
|
|
1381
|
+
else if (forceEmptyCommit) {
|
|
1382
|
+
run(`git commit --allow-empty -m "chore: trigger redeploy [AM-${shortId}]"`);
|
|
1383
|
+
}
|
|
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
|
+
}
|
|
1400
|
+
}
|
|
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)`);
|
|
1426
|
+
}
|
|
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');
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
catch (anyErr) {
|
|
1506
|
+
return { ok: false, commitHash, branch, deployedNote: '', errorDetail: errText(anyErr) };
|
|
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: '' };
|
|
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
|
+
*/
|
|
1195
1546
|
export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
1196
1547
|
if (!projectPath || !fs.existsSync(projectPath))
|
|
1197
1548
|
return false;
|
|
1198
1549
|
const shortId = card.id.slice(0, 8);
|
|
1199
1550
|
const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
|
|
1200
|
-
|
|
1551
|
+
let existing = null;
|
|
1201
1552
|
if (fs.existsSync(deployLock)) {
|
|
1202
1553
|
try {
|
|
1203
|
-
|
|
1204
|
-
if (!cardUpdatedAt || existing?.card_updated_at === cardUpdatedAt) {
|
|
1205
|
-
return false;
|
|
1206
|
-
}
|
|
1554
|
+
existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
|
|
1207
1555
|
}
|
|
1208
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)
|
|
1209
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;
|
|
1210
1570
|
}
|
|
1211
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();
|
|
1212
1574
|
const provider = card.agent_provider || 'cursor';
|
|
1213
|
-
const
|
|
1575
|
+
const rawText = `${card.title || ''} ${card.description || ''}`;
|
|
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');
|
|
1214
1591
|
try {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
card_updated_at: cardUpdatedAt,
|
|
1218
|
-
title: card.title,
|
|
1219
|
-
started_at: new Date().toISOString(),
|
|
1220
|
-
}), 'utf8');
|
|
1221
|
-
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
1222
|
-
cwd: projectPath,
|
|
1223
|
-
encoding: 'utf8',
|
|
1224
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1225
|
-
}).trim();
|
|
1226
|
-
if (!branch || branch === 'HEAD')
|
|
1592
|
+
if (deployStage === 'manual') {
|
|
1593
|
+
writeLock('deployed', { note: 'manual deploy stage' });
|
|
1227
1594
|
return false;
|
|
1228
|
-
const statusOut = execSync('git status --porcelain -uno', {
|
|
1229
|
-
cwd: projectPath,
|
|
1230
|
-
encoding: 'utf8',
|
|
1231
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1232
|
-
}).trim();
|
|
1233
|
-
const rawText = `${card.title || ''} ${card.description || ''}`;
|
|
1234
|
-
const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
|
|
1235
|
-
if (statusOut.length > 0) {
|
|
1236
|
-
execSync('git add -u', { cwd: projectPath, stdio: 'ignore' });
|
|
1237
|
-
execSync(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`, {
|
|
1238
|
-
cwd: projectPath,
|
|
1239
|
-
stdio: 'ignore',
|
|
1240
|
-
});
|
|
1241
|
-
execSync(`git push origin ${branch}`, {
|
|
1242
|
-
cwd: projectPath,
|
|
1243
|
-
stdio: 'ignore',
|
|
1244
|
-
});
|
|
1245
|
-
const commitHash = execSync('git rev-parse --short HEAD', {
|
|
1246
|
-
cwd: projectPath,
|
|
1247
|
-
encoding: 'utf8',
|
|
1248
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1249
|
-
}).trim();
|
|
1250
|
-
const msg = `🚀 **[Auto-Deployed to Production]** Deployment confirmed! Committed and pushed \`${commitHash}\` to \`origin/${branch}\`, and moved card to **Done**.`;
|
|
1251
|
-
await client.completeTask(card.id, provider, msg);
|
|
1252
|
-
appendIdeActivityLog(projectPath, {
|
|
1253
|
-
cardId: card.id,
|
|
1254
|
-
cardTitle: card.title,
|
|
1255
|
-
agentName: card.agent_display_name || provider,
|
|
1256
|
-
model: card.agent_model || 'auto',
|
|
1257
|
-
status: 'done',
|
|
1258
|
-
summary: `Deployment confirmed (${commitHash} -> origin/${branch}). Moved card to Done.`,
|
|
1259
|
-
});
|
|
1260
|
-
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${commitHash}) and moved to Done!`);
|
|
1261
|
-
return true;
|
|
1262
1595
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
});
|
|
1268
|
-
execSync(`git push origin ${branch}`, {
|
|
1269
|
-
cwd: projectPath,
|
|
1270
|
-
stdio: 'ignore',
|
|
1271
|
-
});
|
|
1272
|
-
const commitHash = execSync('git rev-parse --short HEAD', {
|
|
1273
|
-
cwd: projectPath,
|
|
1274
|
-
encoding: 'utf8',
|
|
1275
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1276
|
-
}).trim();
|
|
1277
|
-
const msg = `🚀 **[Auto-Deployed to Production]** Triggered production container rebuild & restart (\`${commitHash}\` on \`origin/${branch}\`) and confirmed move to **Done**.`;
|
|
1278
|
-
await client.completeTask(card.id, provider, msg);
|
|
1279
|
-
appendIdeActivityLog(projectPath, {
|
|
1280
|
-
cardId: card.id,
|
|
1281
|
-
cardTitle: card.title,
|
|
1282
|
-
agentName: card.agent_display_name || provider,
|
|
1283
|
-
model: card.agent_model || 'auto',
|
|
1284
|
-
status: 'done',
|
|
1285
|
-
summary: `Triggered container restart (${commitHash}) and moved card to Done.`,
|
|
1286
|
-
});
|
|
1287
|
-
sendDesktopNotification(`Agile Monster -> Container Redeploy`, `Triggered container restart (${commitHash}) on origin/${branch}!`);
|
|
1288
|
-
return true;
|
|
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;
|
|
1289
1600
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}).trim();
|
|
1296
|
-
const msg = `🚀 **[Auto-Deployed to Production]** Confirmed latest commit \`${headHash}\` is pushed to \`origin/${branch}\` and live in production. Moved card to **Done**.`;
|
|
1297
|
-
await client.completeTask(card.id, provider, msg);
|
|
1298
|
-
return true;
|
|
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;
|
|
1299
1606
|
}
|
|
1607
|
+
const msg = `${res.deployedNote} Confirmed and moved card to **Done**.`;
|
|
1608
|
+
await client.completeTask(card.id, provider, msg);
|
|
1609
|
+
writeLock('deployed', { commit: res.commitHash, completed_at: new Date().toISOString() });
|
|
1610
|
+
appendIdeActivityLog(projectPath, {
|
|
1611
|
+
cardId: card.id,
|
|
1612
|
+
cardTitle: card.title,
|
|
1613
|
+
agentName: card.agent_display_name || provider,
|
|
1614
|
+
model: card.agent_model || 'auto',
|
|
1615
|
+
status: 'done',
|
|
1616
|
+
summary: msg,
|
|
1617
|
+
});
|
|
1618
|
+
sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${res.commitHash})`);
|
|
1619
|
+
return true;
|
|
1300
1620
|
}
|
|
1301
1621
|
catch (err) {
|
|
1302
|
-
|
|
1303
|
-
await client.addProgress(card.id, provider, `⚠️ **[Auto-Deploy Note]** Attempted to auto-push approved changes in \`${projectPath}\`: ${err?.message || String(err)}`);
|
|
1304
|
-
}
|
|
1305
|
-
catch { }
|
|
1622
|
+
writeLock('failed', { error: String(err?.message || err).slice(0, 500) });
|
|
1306
1623
|
return false;
|
|
1307
1624
|
}
|
|
1308
1625
|
}
|
|
1309
|
-
|
|
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') {
|
|
1310
1672
|
const notifiedCardIds = new Set();
|
|
1311
1673
|
let updateTriggered = false;
|
|
1312
1674
|
let pulseCount = 0;
|
|
@@ -1402,28 +1764,31 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
|
|
|
1402
1764
|
catch { }
|
|
1403
1765
|
}
|
|
1404
1766
|
}
|
|
1405
|
-
// Zero-touch autonomous execution
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
+
}
|
|
1412
1777
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
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
|
+
}
|
|
1421
1785
|
}
|
|
1422
1786
|
}
|
|
1423
1787
|
if ((res.version?.force_update_requested || res.version?.update_available) &&
|
|
1424
|
-
!updateTriggered
|
|
1788
|
+
!updateTriggered &&
|
|
1789
|
+
role === 'daemon') {
|
|
1425
1790
|
updateTriggered = true;
|
|
1426
|
-
triggerBackgroundSelfUpdate();
|
|
1791
|
+
triggerBackgroundSelfUpdate(res.version?.latest_version);
|
|
1427
1792
|
}
|
|
1428
1793
|
}
|
|
1429
1794
|
catch {
|
|
@@ -1440,12 +1805,24 @@ export async function runStdioServer() {
|
|
|
1440
1805
|
const server = createServer(client);
|
|
1441
1806
|
const transport = new StdioServerTransport();
|
|
1442
1807
|
await server.connect(transport);
|
|
1443
|
-
startBuiltInPullPulse(server, client, 20000);
|
|
1808
|
+
startBuiltInPullPulse(server, client, 20000, 'stdio');
|
|
1444
1809
|
}
|
|
1445
1810
|
export async function runBackgroundPulseDaemon(intervalMs = 20000) {
|
|
1446
1811
|
const client = new AgileMonsterClient();
|
|
1447
1812
|
console.log(`[Agile Monster Pulse Daemon v${MCP_SERVER_VERSION}] Listening for queued agent tasks across all bound repositories (interval: ${intervalMs / 1000}s)...`);
|
|
1448
|
-
startBuiltInPullPulse(null, client, intervalMs);
|
|
1449
|
-
|
|
1450
|
-
|
|
1813
|
+
startBuiltInPullPulse(null, client, intervalMs, 'daemon');
|
|
1814
|
+
const selfScript = process.argv[1] || '';
|
|
1815
|
+
const initialMtime = selfScript && fs.existsSync(selfScript) ? fs.statSync(selfScript).mtimeMs : 0;
|
|
1816
|
+
// Auto-restart daemon if mcp-server binary/dist on disk is updated (launchd KeepAlive respawns it cleanly)
|
|
1817
|
+
setInterval(() => {
|
|
1818
|
+
try {
|
|
1819
|
+
if (selfScript && initialMtime > 0 && fs.existsSync(selfScript)) {
|
|
1820
|
+
const curMtime = fs.statSync(selfScript).mtimeMs;
|
|
1821
|
+
if (curMtime > initialMtime + 1000) {
|
|
1822
|
+
process.exit(0);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
catch { }
|
|
1827
|
+
}, 30000);
|
|
1451
1828
|
}
|
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",
|