@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 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.14";
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.14';
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-3.1-pro',
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 startBuiltInPullPulse(server: Server | null, client: AgileMonsterClient, intervalMs?: number): NodeJS.Timeout;
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. Pre-warm latest @agilemonster/mcp-server in npx cache
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-4 most relevant files
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, 4);
1041
+ const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
998
1042
  if (selectedFiles.length === 0) {
999
- selectedFiles.push(...fileIndex.slice(0, 3));
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
- const editsRes = await client.autonomousStep({
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
- const edits = editsRes.edits || [];
1024
- const modifiedFiles = [];
1025
- const originalContents = new Map();
1026
- for (const edit of edits) {
1027
- const relPath = edit.file || edit.path;
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
- if (currentContent.includes(edit.search)) {
1040
- const updatedContent = currentContent.replace(edit.search, edit.replace);
1041
- if (updatedContent !== currentContent) {
1042
- // Guard against unbalanced braces/parens/brackets introduced by partial block replacement
1043
- const countChar = (s, ch) => s.split(ch).length - 1;
1044
- const searchBraceDelta = countChar(edit.search, '{') - countChar(edit.search, '}');
1045
- const replaceBraceDelta = countChar(edit.replace, '{') - countChar(edit.replace, '}');
1046
- const searchParenDelta = countChar(edit.search, '(') - countChar(edit.search, ')');
1047
- const replaceParenDelta = countChar(edit.replace, '(') - countChar(edit.replace, ')');
1048
- if (searchBraceDelta !== replaceBraceDelta || searchParenDelta !== replaceParenDelta) {
1049
- continue;
1050
- }
1051
- fs.writeFileSync(fullPath, updatedContent, 'utf8');
1052
- // Pre-submit static analysis gate for Dart/Flutter and PHP files
1053
- if (relPath.endsWith('.dart')) {
1054
- try {
1055
- execSync(`dart analyze "${fullPath}"`, {
1056
- cwd: projectPath,
1057
- stdio: ['ignore', 'pipe', 'pipe'],
1058
- timeout: 25000,
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
- catch (analyzeErr) {
1062
- const out = String(analyzeErr?.stdout || '') + String(analyzeErr?.stderr || '');
1063
- if (/\berror\s+[•-]/i.test(out)) {
1064
- // Revert broken Dart edit so it never breaks the build
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
- else if (relPath.endsWith('.php')) {
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
- if (!modifiedFiles.includes(relPath)) {
1084
- modifiedFiles.push(relPath);
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
- // Pre-submit frontend build verification gate for JS/TS/React/Vite/Next.js projects
1090
- const hasFrontendEdits = modifiedFiles.some((f) => /\.(tsx?|jsx?|vue)$/i.test(f));
1091
- const pkgJsonPath = path.join(projectPath, 'package.json');
1092
- if (hasFrontendEdits && fs.existsSync(pkgJsonPath)) {
1093
- try {
1094
- const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
1095
- if (pkg?.scripts?.build && fs.existsSync(path.join(projectPath, 'node_modules'))) {
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
- catch (buildErr) {
1104
- // Revert broken frontend edits so we never leave the project with a broken build
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
- // Keep card comment concise plain English (no raw code diffs on the card!)
1128
- const cleanPlainSummary = baseSummary
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 updated behavior for "${card.title}".\n2. Click **Approve & Move to Done** to deploy and confirm.`;
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
- await client.askQuestion(card.id, provider, `Inspected ${Object.keys(filesPayload)
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(', ')}, but needed more specific details to apply a verified code change automatically. Reply in the comments with additional details (or open your IDE agent) to continue!`);
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
- const cardUpdatedAt = card.updated_at || '';
1551
+ let existing = null;
1201
1552
  if (fs.existsSync(deployLock)) {
1202
1553
  try {
1203
- const existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
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 cleanTitle = (card.title || 'update').replace(/["`\r\n]+/g, ' ').trim();
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
- fs.writeFileSync(deployLock, JSON.stringify({
1216
- card_id: card.id,
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
- else if (isContainerRestartCard) {
1264
- execSync(`git commit --allow-empty -m "chore: trigger container redeploy [AM-${shortId}]"`, {
1265
- cwd: projectPath,
1266
- stdio: 'ignore',
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
- else {
1291
- const headHash = execSync('git rev-parse --short HEAD', {
1292
- cwd: projectPath,
1293
- encoding: 'utf8',
1294
- stdio: ['ignore', 'pipe', 'ignore'],
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
- try {
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
- export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
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 for any queued cards bound to local folders on this machine
1406
- const boundQueued = res.all_bound_queued_cards || [];
1407
- for (const qCard of boundQueued) {
1408
- const targetPath = (qCard.bound_project_path || '').trim() ||
1409
- (qCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
1410
- if (targetPath && fs.existsSync(targetPath)) {
1411
- await executeQueuedCardAutonomously(client, qCard, targetPath);
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
- // Zero-touch auto-commit & push (deploy to production) for cards approved into Production / Done!
1415
- const boundDone = res.all_bound_done_cards || [];
1416
- for (const dCard of boundDone) {
1417
- const targetPath = (dCard.bound_project_path || '').trim() ||
1418
- (dCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
1419
- if (targetPath && fs.existsSync(targetPath)) {
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
- // Keep process alive
1450
- setInterval(() => { }, 60000);
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.14",
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",