@agilemonster/mcp-server 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -10
- package/bin/mcp.js +33 -1
- package/bin/orchestrator.js +1 -0
- package/dist/agent-engine.d.ts +17 -0
- package/dist/agent-engine.js +119 -4
- package/dist/client.d.ts +17 -3
- package/dist/client.js +24 -5
- package/dist/machine.d.ts +25 -0
- package/dist/machine.js +32 -0
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +33 -7
- package/dist/server.d.ts +10 -1
- package/dist/server.js +83 -29
- package/dist/service.d.ts +82 -0
- package/dist/service.js +327 -0
- package/package.json +4 -1
package/dist/server.js
CHANGED
|
@@ -8,13 +8,30 @@ import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
|
8
8
|
import { isUtf8 } from 'node:buffer';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { AgileMonsterClient, MCP_SERVER_VERSION, isClaimConflict } from './client.js';
|
|
11
|
+
import { machineReport } from './machine.js';
|
|
12
|
+
import { managedRunnerDir, updateArgs } from './service.js';
|
|
11
13
|
import { AGENT_GUIDELINES_MARKDOWN } from './guidelines.js';
|
|
12
14
|
import { knownSecrets, redactResult } from './redact.js';
|
|
13
15
|
import { describeProfile, methodLabel, normalizeMethod, resolveDeployProfile, trustBoundRoute, } from './deploy-profile.js';
|
|
14
16
|
import { addPendingVerification, getCardState, loadMemory, recordAttempt, removePendingVerification, setCardState, setExplicitProfile, } from './deploy-memory.js';
|
|
15
17
|
import { checkCiOnce, describeFailureForHuman, ensureLocalGitExcludes, parseDeployDirective, runDeployment, } from './deploy-runner.js';
|
|
16
|
-
import { baselineFor, buildAgentPrompt, changesSince, clearBaseline, engineLabelFor, markNewFilesForDiff, parseAgentReport, proseOf, resolveEngines, restoreWorkingTree, runEngine, } from './agent-engine.js';
|
|
18
|
+
import { baselineFor, buildAgentPrompt, changesSince, clearBaseline, engineLabelFor, engineStatuses, markNewFilesForDiff, parseAgentReport, proseOf, resolveEngines, restoreWorkingTree, runEngine, } from './agent-engine.js';
|
|
17
19
|
import { addWellKnownToolDirs, agileMonsterHome, configureLogger, ensureUserPath, errorMessage, exec, log, readJson, which, writeJsonAtomic, } from './runtime.js';
|
|
20
|
+
/**
|
|
21
|
+
* The answers a deploy question accepts, as reply buttons: nobody should have to type `deploy: ./deploy.sh`.
|
|
22
|
+
* A first-time route gets "yes, this way" and "I deploy it myself"; a failure gets "try again".
|
|
23
|
+
*/
|
|
24
|
+
export function deployReplyOptions(res) {
|
|
25
|
+
if (res.errorKind === 'needs_confirmation') {
|
|
26
|
+
const route = (res.command || res.method || '').trim();
|
|
27
|
+
const options = [];
|
|
28
|
+
if (route)
|
|
29
|
+
options.push({ label: 'Yes, deploy this way', reply: `deploy: ${route}` });
|
|
30
|
+
options.push({ label: 'I deploy it myself', reply: 'deploy: manual' });
|
|
31
|
+
return options;
|
|
32
|
+
}
|
|
33
|
+
return [{ label: 'Try again', reply: 'retry' }];
|
|
34
|
+
}
|
|
18
35
|
export function createServer(client = new AgileMonsterClient()) {
|
|
19
36
|
const server = new Server({
|
|
20
37
|
name: 'agilemonster-mcp-server',
|
|
@@ -263,6 +280,11 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
263
280
|
type: 'string',
|
|
264
281
|
description: 'The question for the user.',
|
|
265
282
|
},
|
|
283
|
+
options: {
|
|
284
|
+
type: 'array',
|
|
285
|
+
items: { type: 'string' },
|
|
286
|
+
description: 'Optional: up to four short answers the user can pick with one tap. The card shows them as buttons, and the user can still type a different answer.',
|
|
287
|
+
},
|
|
266
288
|
},
|
|
267
289
|
required: ['card_id', 'provider', 'question'],
|
|
268
290
|
},
|
|
@@ -640,7 +662,10 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
640
662
|
const cardId = args.card_id;
|
|
641
663
|
const provider = args.provider || projectConfig?.provider || 'antigravity';
|
|
642
664
|
const question = args.question;
|
|
643
|
-
const
|
|
665
|
+
const answers = Array.isArray(args.options)
|
|
666
|
+
? args.options.map((o) => String(o).trim()).filter((o) => o.length > 0 && o.length <= 80).slice(0, 4)
|
|
667
|
+
: [];
|
|
668
|
+
const card = await client.askQuestion(cardId, provider, question, answers);
|
|
644
669
|
releaseClaim(cardId);
|
|
645
670
|
return {
|
|
646
671
|
content: [
|
|
@@ -1015,11 +1040,16 @@ function triggerBackgroundSelfUpdate(targetVersion) {
|
|
|
1015
1040
|
catch { }
|
|
1016
1041
|
}
|
|
1017
1042
|
}
|
|
1018
|
-
// 2.
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1043
|
+
// 2. Fetch the new version, then exit once idle so launchd/systemd starts it. The background service
|
|
1044
|
+
// updates its own copy in place; a runner started any other way refreshes the global install / npx cache.
|
|
1045
|
+
const ownDir = managedRunnerDir(fileURLToPath(import.meta.url));
|
|
1046
|
+
const npm = which('npm');
|
|
1047
|
+
const child = ownDir && npm
|
|
1048
|
+
? spawn(npm, updateArgs(ownDir), { stdio: 'ignore', detached: true, cwd: ownDir })
|
|
1049
|
+
: 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'], {
|
|
1050
|
+
stdio: 'ignore',
|
|
1051
|
+
detached: true,
|
|
1052
|
+
});
|
|
1023
1053
|
child.on('exit', () => {
|
|
1024
1054
|
appendIdeActivityLog(cwd, {
|
|
1025
1055
|
cardId: 'system-update',
|
|
@@ -1579,7 +1609,7 @@ async function runRedeployOnlyCard(client, card, projectPath, provider, workerLa
|
|
|
1579
1609
|
pending_confirmation: res.errorKind === 'needs_confirmation' ? { method: res.method, command: res.command } : null,
|
|
1580
1610
|
}));
|
|
1581
1611
|
removeTaskLock(lockFile);
|
|
1582
|
-
await client.askQuestion(card.id, provider, res.errorKind === 'needs_confirmation' ? describeFailureForHuman(res) : `The redeploy did not go through.\n\n${describeFailureForHuman(res)}
|
|
1612
|
+
await client.askQuestion(card.id, provider, res.errorKind === 'needs_confirmation' ? describeFailureForHuman(res) : `The redeploy did not go through.\n\n${describeFailureForHuman(res)}`, deployReplyOptions(res));
|
|
1583
1613
|
return false;
|
|
1584
1614
|
}
|
|
1585
1615
|
if (res.pending && res.pendingVerification)
|
|
@@ -1683,7 +1713,7 @@ async function resumeFailedDeployment(client, card, projectPath, provider, worke
|
|
|
1683
1713
|
// Nothing is ever confirmed on the human's behalf.
|
|
1684
1714
|
removeTaskLock(lockFile);
|
|
1685
1715
|
setCardState(projectPath, card.id, { ...state, at: new Date().toISOString() });
|
|
1686
|
-
await client.askQuestion(card.id, provider, `I still need an answer before I deploy this project for the first time. Reply \`deploy: ${pendingRoute.command || pendingRoute.method}\` to confirm, \`deploy: manual\` if you deploy it yourself, or tell me the right route
|
|
1716
|
+
await client.askQuestion(card.id, provider, `I still need an answer before I deploy this project for the first time. Reply \`deploy: ${pendingRoute.command || pendingRoute.method}\` to confirm, \`deploy: manual\` if you deploy it yourself, or tell me the right route.`, deployReplyOptions({ errorKind: 'needs_confirmation', method: pendingRoute.method, command: pendingRoute.command }));
|
|
1687
1717
|
return 'failed';
|
|
1688
1718
|
}
|
|
1689
1719
|
// No fresh reply at all: a re-queue of a deploy-failed card means "try the same deployment again".
|
|
@@ -1736,7 +1766,7 @@ async function resumeFailedDeployment(client, card, projectPath, provider, worke
|
|
|
1736
1766
|
});
|
|
1737
1767
|
await client.askQuestion(card.id, provider, res.errorKind === 'needs_confirmation'
|
|
1738
1768
|
? describeFailureForHuman(res)
|
|
1739
|
-
: `The deployment still did not go through, so this card stays in progress.\n\n${describeFailureForHuman(res)}
|
|
1769
|
+
: `The deployment still did not go through, so this card stays in progress.\n\n${describeFailureForHuman(res)}`, deployReplyOptions(res));
|
|
1740
1770
|
return 'failed';
|
|
1741
1771
|
}
|
|
1742
1772
|
if (res.pending && res.pendingVerification) {
|
|
@@ -1878,8 +1908,10 @@ async function produceChangeWithAgent(run) {
|
|
|
1878
1908
|
const { client, card, projectPath, provider, workerLabel, lockFile, carryOverFiles } = run;
|
|
1879
1909
|
const { engines, notes } = await resolveEngines(provider);
|
|
1880
1910
|
run.engineNotes.push(...notes);
|
|
1881
|
-
if (engines.length === 0)
|
|
1911
|
+
if (engines.length === 0) {
|
|
1912
|
+
log.info('agent.unavailable', { card: card.id, provider, notes });
|
|
1882
1913
|
return 'unavailable';
|
|
1914
|
+
}
|
|
1883
1915
|
// The agent gets a shell. Like a deploy command, what runs on this machine is for its owner to decide:
|
|
1884
1916
|
// only cards its owner wrote are handed over (and only the owner's replies were kept in the conversation).
|
|
1885
1917
|
if (!run.ownerId || !card.created_by || card.created_by !== run.ownerId) {
|
|
@@ -1902,7 +1934,7 @@ async function produceChangeWithAgent(run) {
|
|
|
1902
1934
|
return files.length > 0;
|
|
1903
1935
|
};
|
|
1904
1936
|
run.rollback.fn = async () => revert((await changesSince(projectPath, baseline)).changed);
|
|
1905
|
-
const backToHuman = async (question, summary, files) => {
|
|
1937
|
+
const backToHuman = async (question, summary, files, options = []) => {
|
|
1906
1938
|
run.rollback.fn = null;
|
|
1907
1939
|
removeTaskLock(lockFile);
|
|
1908
1940
|
appendIdeActivityLog(projectPath, {
|
|
@@ -1914,9 +1946,10 @@ async function produceChangeWithAgent(run) {
|
|
|
1914
1946
|
summary,
|
|
1915
1947
|
files,
|
|
1916
1948
|
});
|
|
1917
|
-
await client.askQuestion(card.id, provider, question);
|
|
1949
|
+
await client.askQuestion(card.id, provider, question, options);
|
|
1918
1950
|
return 'handled';
|
|
1919
1951
|
};
|
|
1952
|
+
const TRY_AGAIN = [{ label: 'Try again', reply: 'Please try again.' }];
|
|
1920
1953
|
const prompt = buildAgentPrompt({
|
|
1921
1954
|
title: card.title || '',
|
|
1922
1955
|
description: card.description || '',
|
|
@@ -1931,8 +1964,14 @@ async function produceChangeWithAgent(run) {
|
|
|
1931
1964
|
// 1. Run the first agent that actually starts. One that is logged out or crashes on launch touches nothing.
|
|
1932
1965
|
let engine = engines[0];
|
|
1933
1966
|
let result = null;
|
|
1967
|
+
const nativeLabel = engineLabelFor(provider);
|
|
1934
1968
|
for (const candidate of engines) {
|
|
1935
|
-
|
|
1969
|
+
// A card assigned to Cursor that Codex ends up working must say so, with the one step that fixes it
|
|
1970
|
+
const passedOver = nativeLabel && candidate.label !== nativeLabel ? run.engineNotes.filter((n) => n.startsWith(nativeLabel)) : [];
|
|
1971
|
+
await client.addProgress(card.id, provider, `Autonomous background runner claimed task in \`${projectPath}\` and moved card to **In Progress**. **${candidate.label}** is working on it in the project folder — reading, searching, editing and building, as it would in the IDE.` +
|
|
1972
|
+
(passedOver.length > 0
|
|
1973
|
+
? `\n\n${candidate.label} is standing in for ${nativeLabel}, which cannot run on this machine yet:\n${passedOver.map((n) => `- ${n}`).join('\n')}`
|
|
1974
|
+
: ''));
|
|
1936
1975
|
log.info('agent.start', { card: card.id, engine: candidate.id, project: projectPath });
|
|
1937
1976
|
const attempt = await runEngine(candidate, { prompt, projectPath, model });
|
|
1938
1977
|
log.info('agent.end', { card: card.id, engine: candidate.id, ok: attempt.ok, seconds: Math.round(attempt.durationMs / 1000), failure: attempt.failure });
|
|
@@ -1966,11 +2005,11 @@ async function produceChangeWithAgent(run) {
|
|
|
1966
2005
|
await revert(change.changed);
|
|
1967
2006
|
return backToHuman(`I need to change ${foreign.map((f) => `\`${f}\``).join(', ')}, but ${foreign.length === 1 ? 'that file has' : 'those files have'} uncommitted changes in \`${projectPath}\` that are not mine. Committing my edit would also commit that unfinished work, so I stopped and put everything back.\n\n${started.interrupted
|
|
1968
2007
|
? 'An earlier run of this card was interrupted before it finished, so these may be its leftovers rather than your work. If you did not make them, discard them (`git checkout -- <file>`); otherwise commit or stash them. Then reply here and I will continue.'
|
|
1969
|
-
: 'Commit or stash those changes, then reply here and I will continue.'}`, `Stopped: ${foreign.join(', ')} had uncommitted changes`, foreign);
|
|
2008
|
+
: 'Commit or stash those changes, then reply here and I will continue.'}`, `Stopped: ${foreign.join(', ')} had uncommitted changes`, foreign, [{ label: 'Done, continue', reply: 'I committed or stashed those changes. Please continue.' }]);
|
|
1970
2009
|
}
|
|
1971
2010
|
if (!result.ok) {
|
|
1972
2011
|
await revert(change.changed);
|
|
1973
|
-
return backToHuman(`${engine.label} worked on this for ${Math.max(1, Math.round(result.durationMs / 60000))} minute(s) but did not finish: ${result.failure || 'it stopped without a result'}.\n\nI put the files back the way they were. Reply here to have it try again — if the card is large, splitting it into smaller cards helps.`, `${engine.label} did not finish: ${result.failure || 'no result'}`, []);
|
|
2012
|
+
return backToHuman(`${engine.label} worked on this for ${Math.max(1, Math.round(result.durationMs / 60000))} minute(s) but did not finish: ${result.failure || 'it stopped without a result'}.\n\nI put the files back the way they were. Reply here to have it try again — if the card is large, splitting it into smaller cards helps.`, `${engine.label} did not finish: ${result.failure || 'no result'}`, [], TRY_AGAIN);
|
|
1974
2013
|
}
|
|
1975
2014
|
let report = parseAgentReport(result.text);
|
|
1976
2015
|
if (report.status === 'blocked' || change.changed.length === 0) {
|
|
@@ -1978,7 +2017,7 @@ async function produceChangeWithAgent(run) {
|
|
|
1978
2017
|
const said = report.question || report.summary || proseOf(result.text);
|
|
1979
2018
|
return backToHuman(report.status === 'blocked' && report.question
|
|
1980
2019
|
? report.question
|
|
1981
|
-
: `${engine.label} finished without changing any files.${said ? `\n\n${said}` : ''}\n\nIf something is still missing, reply here with what you expected to change.`, `Needs input: ${said.slice(0, 400)}`, []);
|
|
2020
|
+
: `${engine.label} finished without changing any files.${said ? `\n\n${said}` : ''}\n\nIf something is still missing, reply here with what you expected to change.`, `Needs input: ${said.slice(0, 400)}`, [], report.status === 'blocked' && report.question ? report.options : []);
|
|
1982
2021
|
}
|
|
1983
2022
|
// 2. The runner's own build gate. The agent built it too, so a failure here is usually this machine's
|
|
1984
2023
|
// environment — check that before sending the agent after an error that was never its own.
|
|
@@ -2024,7 +2063,7 @@ async function produceChangeWithAgent(run) {
|
|
|
2024
2063
|
const stillForeign = change.overlapping.filter((f) => !carryOverFiles.includes(f));
|
|
2025
2064
|
if (!gate.ok || change.changed.length === 0 || stillForeign.length > 0) {
|
|
2026
2065
|
await revert(change.changed);
|
|
2027
|
-
return backToHuman(`${engine.label} made the change, but the project did not build with it, and its attempt to fix that did not get the build green. I reverted everything rather than ship a broken build.\n\n\`\`\`\n${buildErrorExcerpt(gate.errorOutput || '')}\n\`\`\`\n\nYour description was clear — nothing more is needed from you on that. Reply here to have it try again.`, `Reverted: the build failed after ${engine.label}'s change`, []);
|
|
2066
|
+
return backToHuman(`${engine.label} made the change, but the project did not build with it, and its attempt to fix that did not get the build green. I reverted everything rather than ship a broken build.\n\n\`\`\`\n${buildErrorExcerpt(gate.errorOutput || '')}\n\`\`\`\n\nYour description was clear — nothing more is needed from you on that. Reply here to have it try again.`, `Reverted: the build failed after ${engine.label}'s change`, [], TRY_AGAIN);
|
|
2028
2067
|
}
|
|
2029
2068
|
}
|
|
2030
2069
|
}
|
|
@@ -2497,7 +2536,7 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
2497
2536
|
const changed = modifiedFiles.map((f) => `\`${f}\``).join(', ');
|
|
2498
2537
|
await client.askQuestion(card.id, provider, deployInfo.errorKind === 'needs_confirmation'
|
|
2499
2538
|
? `I made the code change (${changed}). One question before I deploy this project for the first time:\n\n${describeFailureForHuman(deployInfo)}`
|
|
2500
|
-
: `I made the code change (${changed}), but the deployment could not be verified, so I kept this card in progress instead of moving it to Testing.\n\n${describeFailureForHuman(deployInfo)}
|
|
2539
|
+
: `I made the code change (${changed}), but the deployment could not be verified, so I kept this card in progress instead of moving it to Testing.\n\n${describeFailureForHuman(deployInfo)}`, deployReplyOptions(deployInfo));
|
|
2501
2540
|
return false;
|
|
2502
2541
|
}
|
|
2503
2542
|
if (deployInfo.pending && deployInfo.pendingVerification) {
|
|
@@ -2812,7 +2851,7 @@ async function processPendingVerifications(client, projectPaths) {
|
|
|
2812
2851
|
attempts: 1,
|
|
2813
2852
|
last_error: detail,
|
|
2814
2853
|
});
|
|
2815
|
-
await client.askQuestion(p.card_id, p.provider, `The deployment I started for this card did not succeed, so the change is NOT live yet.\n\nWhat failed: ${detail}\n\nFix the failing step and reply **retry**, or tell me the right route with \`deploy: <method or command
|
|
2854
|
+
await client.askQuestion(p.card_id, p.provider, `The deployment I started for this card did not succeed, so the change is NOT live yet.\n\nWhat failed: ${detail}\n\nFix the failing step and reply **retry**, or tell me the right route with \`deploy: <method or command>\`.`, deployReplyOptions({ errorKind: 'ci_failed' }));
|
|
2816
2855
|
await client.reportDeployment({ card_id: p.card_id, project_path: projectPath, method: 'github_actions', ok: false, error: detail.slice(0, 480), error_kind: 'ci_failed', commit: short });
|
|
2817
2856
|
}
|
|
2818
2857
|
else if (ageMs > 90 * 60 * 1000) {
|
|
@@ -2879,6 +2918,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
|
|
|
2879
2918
|
let holdsWorkerLock = false;
|
|
2880
2919
|
let journalsRecovered = false;
|
|
2881
2920
|
let consecutiveFailures = 0;
|
|
2921
|
+
let warnedNoKey = false;
|
|
2882
2922
|
const autoProvisionSavedBindings = (apiKey, bindings) => {
|
|
2883
2923
|
for (const b of bindings) {
|
|
2884
2924
|
const projPath = (b.project_path || '').trim();
|
|
@@ -2957,19 +2997,28 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
|
|
|
2957
2997
|
try {
|
|
2958
2998
|
const config = client.loadProjectConfig();
|
|
2959
2999
|
const apiKey = client.getApiKey();
|
|
2960
|
-
|
|
3000
|
+
// The background service is not started inside a project folder: it has no board of its own and
|
|
3001
|
+
// serves every folder linked on this machine. A coding tool's MCP server only pulses for its project.
|
|
3002
|
+
const homeBoardId = config?.board_id || '';
|
|
3003
|
+
if (!apiKey || (!homeBoardId && role !== 'daemon')) {
|
|
3004
|
+
if (role === 'daemon' && !apiKey && !warnedNoKey) {
|
|
3005
|
+
warnedNoKey = true;
|
|
3006
|
+
log.warn('daemon.no_api_key', { hint: 'run `npx -y @agilemonster/mcp-server bootstrap` in a project folder' });
|
|
3007
|
+
}
|
|
2961
3008
|
return;
|
|
2962
3009
|
}
|
|
3010
|
+
const homePath = homeBoardId ? config?.project_path || process.cwd() : '';
|
|
2963
3011
|
const includeDiscovery = pulseCount === 0 || pulseCount % 3 === 0;
|
|
2964
3012
|
pulseCount++;
|
|
2965
3013
|
const res = await client.pulse({
|
|
2966
|
-
boardId:
|
|
3014
|
+
boardId: homeBoardId || undefined,
|
|
2967
3015
|
provider: 'any',
|
|
2968
|
-
projectName: config
|
|
2969
|
-
projectPath:
|
|
3016
|
+
projectName: homeBoardId ? config?.project_name || path.basename(process.cwd()) : undefined,
|
|
3017
|
+
projectPath: homePath || undefined,
|
|
2970
3018
|
discoveredProjects: includeDiscovery ? client.discoverLocalProjects() : undefined,
|
|
2971
3019
|
discoveredModels: includeDiscovery ? client.discoverLocalAgentModels() : undefined,
|
|
2972
3020
|
activeClaims: activeClaims(),
|
|
3021
|
+
machine: await machineReport(role).catch(() => undefined),
|
|
2973
3022
|
});
|
|
2974
3023
|
if (consecutiveFailures > 0) {
|
|
2975
3024
|
log.info('pulse.recovered', { after_failures: consecutiveFailures });
|
|
@@ -2979,13 +3028,14 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
|
|
|
2979
3028
|
autoProvisionSavedBindings(apiKey, res.saved_bindings);
|
|
2980
3029
|
}
|
|
2981
3030
|
const queued = res.queued_cards || [];
|
|
2982
|
-
|
|
3031
|
+
if (homeBoardId)
|
|
3032
|
+
syncIdeTaskRules(config?.board_name || 'Board', queued);
|
|
2983
3033
|
for (const card of queued) {
|
|
2984
3034
|
if (!notifiedCardIds.has(card.id)) {
|
|
2985
3035
|
notifiedCardIds.add(card.id);
|
|
2986
3036
|
const worker = card.agent_display_name || card.agent_provider || 'AI Agent';
|
|
2987
3037
|
const modelTag = card.agent_model && card.agent_model !== 'auto' ? ` [${card.agent_model}]` : '';
|
|
2988
|
-
sendDesktopNotification(`Agile Monster -> ${worker}${modelTag}`, `New task queued on ${config
|
|
3038
|
+
sendDesktopNotification(`Agile Monster -> ${worker}${modelTag}`, `New task queued on ${config?.board_name || 'Board'}: "${card.title}"`);
|
|
2989
3039
|
try {
|
|
2990
3040
|
if (server) {
|
|
2991
3041
|
await server.sendResourceUpdated({ uri: 'agilemonster://project/context' });
|
|
@@ -3005,7 +3055,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
|
|
|
3005
3055
|
const boundQueued = res.all_bound_queued_cards || [];
|
|
3006
3056
|
for (const qCard of boundQueued) {
|
|
3007
3057
|
const targetPath = (qCard.bound_project_path || '').trim() ||
|
|
3008
|
-
(qCard.board_id ===
|
|
3058
|
+
(homeBoardId && qCard.board_id === homeBoardId ? homePath : '');
|
|
3009
3059
|
if (targetPath && fs.existsSync(targetPath)) {
|
|
3010
3060
|
await executeQueuedCardAutonomously(client, qCard, targetPath);
|
|
3011
3061
|
}
|
|
@@ -3016,12 +3066,12 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
|
|
|
3016
3066
|
const boundDone = res.all_bound_done_cards || [];
|
|
3017
3067
|
for (const dCard of boundDone) {
|
|
3018
3068
|
const targetPath = (dCard.bound_project_path || '').trim() ||
|
|
3019
|
-
(dCard.board_id ===
|
|
3069
|
+
(homeBoardId && dCard.board_id === homeBoardId ? homePath : '');
|
|
3020
3070
|
if (targetPath && fs.existsSync(targetPath)) {
|
|
3021
3071
|
await deployApprovedCardIfNeeded(client, dCard, targetPath);
|
|
3022
3072
|
}
|
|
3023
3073
|
}
|
|
3024
|
-
const knownPaths = new Set([
|
|
3074
|
+
const knownPaths = new Set(homePath ? [homePath] : []);
|
|
3025
3075
|
for (const b of res.saved_bindings || []) {
|
|
3026
3076
|
if (b.project_path)
|
|
3027
3077
|
knownPaths.add(b.project_path.trim());
|
|
@@ -3079,6 +3129,10 @@ export async function runBackgroundPulseDaemon(intervalMs = 20000) {
|
|
|
3079
3129
|
log.error('daemon.uncaught_exception', { error: errorMessage(err), stack: err instanceof Error ? err.stack : undefined });
|
|
3080
3130
|
process.exit(1);
|
|
3081
3131
|
});
|
|
3132
|
+
// Which agent will work a queued card here, on record before the first card arrives
|
|
3133
|
+
engineStatuses()
|
|
3134
|
+
.then((agents) => log.info('daemon.agents', { agents: agents.map((a) => ({ id: a.id, ready: !a.problem, bin: a.bin, problem: a.problem })) }))
|
|
3135
|
+
.catch(() => { });
|
|
3082
3136
|
startBuiltInPullPulse(null, client, intervalMs, 'daemon');
|
|
3083
3137
|
// Watch dist/server.js for updates so launchd reloads new code after a rebuild — but only between jobs,
|
|
3084
3138
|
// never in the middle of an edit or a deployment.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ExecOptions, ExecResult } from './runtime.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs the card runner as a background service that starts with the computer.
|
|
4
|
+
*
|
|
5
|
+
* Without this, cards queued from the board are only worked while an AI coding tool happens to be open in
|
|
6
|
+
* the project folder (its MCP server pulses as a side effect). People do not know that, so their first card
|
|
7
|
+
* sits in "queued" for ever. `bootstrap` installs the service; `service install|status|uninstall` manage it.
|
|
8
|
+
*
|
|
9
|
+
* The service runs its own copy of the package from ~/.agilemonster/runner, so it does not depend on an npx
|
|
10
|
+
* cache that can be cleared or on a project folder that can move, and it updates that copy itself.
|
|
11
|
+
*
|
|
12
|
+
* macOS: a launchd agent. Linux: a systemd user unit. Windows: not yet.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SERVICE_LABEL = "com.agilemonster.pulse";
|
|
15
|
+
export declare const SYSTEMD_UNIT = "agilemonster-runner.service";
|
|
16
|
+
export type ServiceKind = 'launchd' | 'systemd';
|
|
17
|
+
export interface ServiceDeps {
|
|
18
|
+
platform: NodeJS.Platform;
|
|
19
|
+
home: string;
|
|
20
|
+
/** Machine-local Agile Monster folder (~/.agilemonster). */
|
|
21
|
+
amHome: string;
|
|
22
|
+
uid: number | null;
|
|
23
|
+
node: string;
|
|
24
|
+
npm: string | null;
|
|
25
|
+
pathEnv: string;
|
|
26
|
+
exec: (command: string, args?: string[], options?: ExecOptions) => Promise<ExecResult>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The node binary the service should start. `process.execPath` is often a versioned folder (Homebrew's
|
|
30
|
+
* Cellar/node@24/24.21.0_1/…) that disappears with the next upgrade and would leave the service unable to
|
|
31
|
+
* start; the `node` on PATH is a stable link to the same binary, so prefer it when it is the same one.
|
|
32
|
+
*/
|
|
33
|
+
export declare function stableNodePath(): string;
|
|
34
|
+
export declare function defaultServiceDeps(): ServiceDeps;
|
|
35
|
+
export declare function runnerDir(amHome: string): string;
|
|
36
|
+
/** The service's own copy of this package's entry point. */
|
|
37
|
+
export declare function runnerEntry(amHome: string): string;
|
|
38
|
+
export declare function serviceKind(platform: NodeJS.Platform): ServiceKind | null;
|
|
39
|
+
export declare function unitPath(kind: ServiceKind, home: string): string;
|
|
40
|
+
export interface UnitOptions {
|
|
41
|
+
node: string;
|
|
42
|
+
entry: string;
|
|
43
|
+
home: string;
|
|
44
|
+
logDir: string;
|
|
45
|
+
pathEnv: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function launchdPlist(o: UnitOptions): string;
|
|
48
|
+
export declare function systemdUnit(o: UnitOptions): string;
|
|
49
|
+
/** The program an existing unit file starts (the script after the node binary), or null when unreadable. */
|
|
50
|
+
export declare function unitTarget(kind: ServiceKind, content: string): string | null;
|
|
51
|
+
export interface ServiceStatus {
|
|
52
|
+
supported: boolean;
|
|
53
|
+
kind: ServiceKind | null;
|
|
54
|
+
unitPath: string | null;
|
|
55
|
+
installed: boolean;
|
|
56
|
+
/** True when the unit starts the copy this module installs (not a hand-made or development setup). */
|
|
57
|
+
managed: boolean;
|
|
58
|
+
target: string | null;
|
|
59
|
+
running: boolean;
|
|
60
|
+
}
|
|
61
|
+
export declare function serviceStatus(deps?: ServiceDeps): Promise<ServiceStatus>;
|
|
62
|
+
export interface ServiceResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
/** 'installed' | 'kept' (someone else's setup left alone) | 'unsupported' | 'failed' | 'removed' | 'absent' */
|
|
65
|
+
outcome: 'installed' | 'kept' | 'unsupported' | 'failed' | 'removed' | 'absent';
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function installService(options: {
|
|
69
|
+
force?: boolean;
|
|
70
|
+
hasApiKey: boolean;
|
|
71
|
+
}, deps?: ServiceDeps): Promise<ServiceResult>;
|
|
72
|
+
export declare function uninstallService(options?: {
|
|
73
|
+
force?: boolean;
|
|
74
|
+
}, deps?: ServiceDeps): Promise<ServiceResult>;
|
|
75
|
+
/** npm arguments that install or update the service's own copy of the package. */
|
|
76
|
+
export declare function updateArgs(dir: string, version?: string): string[];
|
|
77
|
+
/**
|
|
78
|
+
* When this process is the service's own copy, the folder to update in place. Null for a copy started by
|
|
79
|
+
* npx, a global install or a source checkout: those update the way they were installed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function managedRunnerDir(runningFile: string, amHome?: string): string | null;
|
|
82
|
+
export declare function describeService(deps?: ServiceDeps): Promise<string>;
|