@agilemonster/mcp-server 1.0.21 → 1.0.23
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 +15 -2
- package/dist/client.js +23 -2
- package/dist/deploy-profile.d.ts +26 -1
- package/dist/deploy-profile.js +69 -1
- package/dist/deploy-runner.js +3 -1
- package/dist/server.d.ts +19 -1
- package/dist/server.js +91 -13
- package/dist/types.d.ts +19 -0
- package/package.json +6 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { AgileMonsterProjectConfig, AutonomousStepResponse, CardComment, DeployReport, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
|
|
2
|
-
export declare const MCP_SERVER_VERSION = "1.0.
|
|
1
|
+
import { ActiveClaim, AgileMonsterProjectConfig, AutonomousStepResponse, CardComment, DeployReport, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
|
|
2
|
+
export declare const MCP_SERVER_VERSION = "1.0.23";
|
|
3
|
+
/** An API call the server answered with an error status. `status` lets callers tell "refused" from "broken". */
|
|
4
|
+
export declare class ApiError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
constructor(message: string, status: number);
|
|
7
|
+
}
|
|
8
|
+
/** The server refused a claim because another agent session holds the card (HTTP 409). */
|
|
9
|
+
export declare function isClaimConflict(err: unknown): boolean;
|
|
3
10
|
export declare class AgileMonsterClient {
|
|
4
11
|
private apiKey;
|
|
5
12
|
private baseUrl;
|
|
@@ -41,7 +48,13 @@ export declare class AgileMonsterClient {
|
|
|
41
48
|
models: Record<string, string> | string[];
|
|
42
49
|
}>;
|
|
43
50
|
installedAgents?: string[];
|
|
51
|
+
/** Cards this process holds: the server extends their claim (heartbeat). */
|
|
52
|
+
activeClaims?: ActiveClaim[];
|
|
44
53
|
}): Promise<PulseResponse>;
|
|
54
|
+
/** Extend the claims this process holds. Used while a long job keeps the pulse loop busy. */
|
|
55
|
+
heartbeat(activeClaims: ActiveClaim[]): Promise<{
|
|
56
|
+
renewed: number;
|
|
57
|
+
}>;
|
|
45
58
|
/**
|
|
46
59
|
* Cross-platform detection of AI coding agents installed on this computer.
|
|
47
60
|
* Checks for Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf, Aider.
|
package/dist/client.js
CHANGED
|
@@ -4,7 +4,20 @@ import { execSync } from 'node:child_process';
|
|
|
4
4
|
import { resolveDeployProfile } from './deploy-profile.js';
|
|
5
5
|
import { loadMemory } from './deploy-memory.js';
|
|
6
6
|
import { stripRemoteCredentials } from './redact.js';
|
|
7
|
-
export const MCP_SERVER_VERSION = '1.0.
|
|
7
|
+
export const MCP_SERVER_VERSION = '1.0.23';
|
|
8
|
+
/** An API call the server answered with an error status. `status` lets callers tell "refused" from "broken". */
|
|
9
|
+
export class ApiError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
constructor(message, status) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'ApiError';
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** The server refused a claim because another agent session holds the card (HTTP 409). */
|
|
18
|
+
export function isClaimConflict(err) {
|
|
19
|
+
return err instanceof ApiError && err.status === 409;
|
|
20
|
+
}
|
|
8
21
|
export class AgileMonsterClient {
|
|
9
22
|
apiKey = '';
|
|
10
23
|
baseUrl;
|
|
@@ -144,7 +157,7 @@ export class AgileMonsterClient {
|
|
|
144
157
|
}
|
|
145
158
|
if (!res.ok) {
|
|
146
159
|
const msg = json?.message || json?.error || `HTTP ${res.status}: ${text.slice(0, 150)}`;
|
|
147
|
-
throw new
|
|
160
|
+
throw new ApiError(`Agile Monster API Error: ${msg}`, res.status);
|
|
148
161
|
}
|
|
149
162
|
return json;
|
|
150
163
|
}
|
|
@@ -202,9 +215,17 @@ export class AgileMonsterClient {
|
|
|
202
215
|
discovered_projects: options.discoveredProjects,
|
|
203
216
|
discovered_models: options.discoveredModels,
|
|
204
217
|
installed_agents: installed,
|
|
218
|
+
active_claims: options.activeClaims && options.activeClaims.length > 0 ? options.activeClaims : undefined,
|
|
205
219
|
}),
|
|
206
220
|
});
|
|
207
221
|
}
|
|
222
|
+
/** Extend the claims this process holds. Used while a long job keeps the pulse loop busy. */
|
|
223
|
+
async heartbeat(activeClaims) {
|
|
224
|
+
return this.request('/api/agent/heartbeat', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
body: JSON.stringify({ active_claims: activeClaims }),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
208
229
|
/**
|
|
209
230
|
* Cross-platform detection of AI coding agents installed on this computer.
|
|
210
231
|
* Checks for Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf, Aider.
|
package/dist/deploy-profile.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ import type { ProjectMemory } from './deploy-memory.js';
|
|
|
11
11
|
* Precedence: explicit project file > user-set binding > last verified success > ranked evidence.
|
|
12
12
|
*/
|
|
13
13
|
export type DeployMethod = 'github_actions' | 'docker_compose' | 'deploy_script' | 'cloud_cli' | 'git_push' | 'manual' | 'none';
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* 'proposed' = a route someone ELSE set for this project on the board (a teammate, or a deployment verified
|
|
16
|
+
* on another machine). It is never "confirmed" for this computer by that alone.
|
|
17
|
+
*/
|
|
18
|
+
export type ProfileSource = 'explicit' | 'user' | 'learned' | 'verified' | 'detected' | 'proposed';
|
|
15
19
|
export type Confidence = 'high' | 'medium' | 'low';
|
|
16
20
|
export interface PushTrigger {
|
|
17
21
|
branches: string[] | null;
|
|
@@ -61,6 +65,16 @@ export interface DeployCandidate {
|
|
|
61
65
|
/** The deploy workflow only starts when dispatched (pushing alone does not deploy). */
|
|
62
66
|
dispatchOnly?: boolean;
|
|
63
67
|
}
|
|
68
|
+
/** A deploy route another account set on the board binding. The server sends it apart from the confirmed fields. */
|
|
69
|
+
export interface RouteProposal {
|
|
70
|
+
method?: string | null;
|
|
71
|
+
command?: string | null;
|
|
72
|
+
live_url?: string | null;
|
|
73
|
+
/** How it is stored on the board: 'user' (stated by that person), 'verified' (worked on their machine), 'auto'. */
|
|
74
|
+
source?: string | null;
|
|
75
|
+
set_by?: string | null;
|
|
76
|
+
set_by_name?: string | null;
|
|
77
|
+
}
|
|
64
78
|
export interface BoundProfile {
|
|
65
79
|
deploy_method?: string | null;
|
|
66
80
|
deploy_command?: string | null;
|
|
@@ -68,7 +82,18 @@ export interface BoundProfile {
|
|
|
68
82
|
build_command?: string | null;
|
|
69
83
|
/** 'user' = set by a human, 'verified' = confirmed by a successful deploy, 'auto' = a guess. */
|
|
70
84
|
deploy_source?: string | null;
|
|
85
|
+
/** The account that set deploy_method / deploy_command on the board. */
|
|
86
|
+
deploy_set_by?: string | null;
|
|
87
|
+
/** A route someone else set: used only as described at `resolveDeployProfile`. */
|
|
88
|
+
proposal?: RouteProposal | null;
|
|
71
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Second check on this side of the wire: the server only sends a binding's route as confirmed to the account
|
|
92
|
+
* that set it, but what runs in a shell on this computer is decided HERE. If the confirmed fields name a
|
|
93
|
+
* setter and it is not this machine's account, the route is demoted to a proposal.
|
|
94
|
+
* `ownerId` null = this machine's account could not be determined; the fields are left as the server sent them.
|
|
95
|
+
*/
|
|
96
|
+
export declare function trustBoundRoute(bound: BoundProfile | null | undefined, ownerId: string | null | undefined): BoundProfile | null;
|
|
72
97
|
export interface GitInfo {
|
|
73
98
|
isRepo: boolean;
|
|
74
99
|
branch: string | null;
|
package/dist/deploy-profile.js
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
+
/**
|
|
5
|
+
* Second check on this side of the wire: the server only sends a binding's route as confirmed to the account
|
|
6
|
+
* that set it, but what runs in a shell on this computer is decided HERE. If the confirmed fields name a
|
|
7
|
+
* setter and it is not this machine's account, the route is demoted to a proposal.
|
|
8
|
+
* `ownerId` null = this machine's account could not be determined; the fields are left as the server sent them.
|
|
9
|
+
*/
|
|
10
|
+
export function trustBoundRoute(bound, ownerId) {
|
|
11
|
+
if (!bound)
|
|
12
|
+
return null;
|
|
13
|
+
const setBy = (bound.deploy_set_by || '').trim();
|
|
14
|
+
const hasRoute = Boolean((bound.deploy_method || '').trim() || (bound.deploy_command || '').trim());
|
|
15
|
+
if (!hasRoute || !setBy || !ownerId || setBy === ownerId)
|
|
16
|
+
return bound;
|
|
17
|
+
return {
|
|
18
|
+
...bound,
|
|
19
|
+
deploy_method: null,
|
|
20
|
+
deploy_command: null,
|
|
21
|
+
deploy_source: null,
|
|
22
|
+
proposal: bound.proposal || {
|
|
23
|
+
method: bound.deploy_method,
|
|
24
|
+
command: bound.deploy_command,
|
|
25
|
+
live_url: bound.live_url,
|
|
26
|
+
source: bound.deploy_source || 'auto',
|
|
27
|
+
set_by: setBy,
|
|
28
|
+
set_by_name: null,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
4
32
|
// ---------------------------------------------------------------------------
|
|
5
33
|
// Small helpers
|
|
6
34
|
// ---------------------------------------------------------------------------
|
|
@@ -898,8 +926,43 @@ export function resolveDeployProfile(projectPath, options = {}) {
|
|
|
898
926
|
}
|
|
899
927
|
if (bound && boundSource === 'user') {
|
|
900
928
|
const method = normalizeMethod(bound.deploy_method);
|
|
929
|
+
// The server only sends source "user" to the account that set the route (see trustBoundRoute)
|
|
901
930
|
if (method)
|
|
902
|
-
return pinned(method, bound.deploy_command || null, 'user', 'set by
|
|
931
|
+
return pinned(method, bound.deploy_command || null, 'user', 'set by you in Agile Monster');
|
|
932
|
+
}
|
|
933
|
+
// A route someone else set on the board. It may steer this machine towards something it could have run
|
|
934
|
+
// unasked anyway (push and let CI deploy, refresh the detected Compose stack). Anything that puts THEIR
|
|
935
|
+
// command line, a deploy script, a cloud CLI or a manual dispatch on this computer is asked about once;
|
|
936
|
+
// only the machine owner's answer on the card confirms it, and it is then remembered locally.
|
|
937
|
+
const proposal = bound?.proposal || null;
|
|
938
|
+
const proposalSource = (proposal?.source || '').toLowerCase();
|
|
939
|
+
const fromProposal = () => {
|
|
940
|
+
const method = normalizeMethod(proposal?.method);
|
|
941
|
+
if (!method)
|
|
942
|
+
return null;
|
|
943
|
+
const match = candidateFor(method);
|
|
944
|
+
const theirCommand = isRunnableCommand(proposal?.command) ? (proposal?.command).trim() : null;
|
|
945
|
+
const custom = theirCommand !== null && theirCommand !== (match?.command || null);
|
|
946
|
+
const sensitive = requiresConfirmation({ method, dispatchOnly: Boolean(match?.dispatchOnly) });
|
|
947
|
+
if (!custom && sensitive && !match?.command)
|
|
948
|
+
return null; // nothing concrete to run or to confirm
|
|
949
|
+
if (!custom && !match && method !== 'manual' && method !== 'none')
|
|
950
|
+
return null; // does not apply to this checkout
|
|
951
|
+
const last = memory?.last_success || null;
|
|
952
|
+
const ranHereBefore = Boolean(last) && normalizeMethod(last?.method) === method && ((last?.command || '').trim() || null) === (theirCommand || match?.command || null);
|
|
953
|
+
const needsConfirmation = !ranHereBefore && (custom || sensitive);
|
|
954
|
+
const who = (proposal?.set_by_name || '').trim() || 'a teammate';
|
|
955
|
+
const what = proposalSource === 'verified' ? `verified by a deployment on ${who}'s machine` : `set by ${who} in Agile Monster`;
|
|
956
|
+
return {
|
|
957
|
+
...pinned(method, theirCommand, 'proposed', needsConfirmation ? `${what}; not confirmed on this computer yet` : what),
|
|
958
|
+
confidence: needsConfirmation ? 'medium' : 'high',
|
|
959
|
+
needsConfirmation,
|
|
960
|
+
};
|
|
961
|
+
};
|
|
962
|
+
if (proposal && proposalSource === 'user') {
|
|
963
|
+
const proposed = fromProposal();
|
|
964
|
+
if (proposed)
|
|
965
|
+
return proposed;
|
|
903
966
|
}
|
|
904
967
|
// What worked last time, as long as it has not started failing since
|
|
905
968
|
const last = memory?.last_success || null;
|
|
@@ -917,6 +980,11 @@ export function resolveDeployProfile(projectPath, options = {}) {
|
|
|
917
980
|
return pinned(method, bound.deploy_command || null, 'verified', 'verified by an earlier deployment recorded in Agile Monster');
|
|
918
981
|
}
|
|
919
982
|
}
|
|
983
|
+
if (proposal && proposalSource === 'verified') {
|
|
984
|
+
const proposed = fromProposal();
|
|
985
|
+
if (proposed)
|
|
986
|
+
return proposed;
|
|
987
|
+
}
|
|
920
988
|
const top = candidates[0];
|
|
921
989
|
if (!top) {
|
|
922
990
|
return {
|
package/dist/deploy-runner.js
CHANGED
|
@@ -973,7 +973,9 @@ export async function runDeployment(req) {
|
|
|
973
973
|
log.info('deploy.needs_confirmation', { card: req.cardId, project: req.projectPath, method: profile.method, command: profile.command });
|
|
974
974
|
return result({
|
|
975
975
|
errorKind: 'needs_confirmation',
|
|
976
|
-
errorDetail:
|
|
976
|
+
errorDetail: profile.source === 'proposed'
|
|
977
|
+
? `On the board this project is set to deploy with ${methodLabel(profile.method)}${profile.command ? ` (\`${profile.command}\`)` : ''}, but that was not set from this computer's account and has never been confirmed here.`
|
|
978
|
+
: `This project looks like it deploys with ${methodLabel(profile.method)}${profile.command ? ` (\`${profile.command}\`)` : ''}, but I have never run that here and nobody has confirmed it.`,
|
|
977
979
|
});
|
|
978
980
|
}
|
|
979
981
|
const git = await commitAndPush(req, profile, pushPolicy);
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { AgileMonsterClient } from './client.js';
|
|
3
|
-
import { Card } from './types.js';
|
|
3
|
+
import { ActiveClaim, Card } from './types.js';
|
|
4
|
+
import { BoundProfile } from './deploy-profile.js';
|
|
4
5
|
export declare function createServer(client?: AgileMonsterClient): Server<{
|
|
5
6
|
method: string;
|
|
6
7
|
params?: {
|
|
@@ -36,6 +37,23 @@ export declare function createServer(client?: AgileMonsterClient): Server<{
|
|
|
36
37
|
} | undefined;
|
|
37
38
|
}>;
|
|
38
39
|
export declare function collectProjectSourceFiles(projectPath: string, keywords: string[]): string[];
|
|
40
|
+
/**
|
|
41
|
+
* The board binding's deploy profile for a card, as this machine may use it. A route that another account
|
|
42
|
+
* set arrives as a proposal (or is demoted to one here): see trustBoundRoute and resolveDeployProfile.
|
|
43
|
+
*/
|
|
44
|
+
export declare function boundProfileOf(card: Partial<Card>, ownerId: string | null): BoundProfile;
|
|
45
|
+
/**
|
|
46
|
+
* The id under which this process claims cards. The server keeps a claim for the session that made it, so
|
|
47
|
+
* the id is stable for the life of the process. Background runs start with `auto-pulse-`: only those are
|
|
48
|
+
* re-queued automatically when they stop responding.
|
|
49
|
+
*/
|
|
50
|
+
export declare function sessionRunRef(kind: 'background' | 'interactive', provider?: string): string;
|
|
51
|
+
export declare function holdClaim(cardId: string, runRef: string): void;
|
|
52
|
+
/** The agent did something with the card: its claim is still in use. */
|
|
53
|
+
export declare function touchClaim(cardId: string): void;
|
|
54
|
+
export declare function releaseClaim(cardId: string): void;
|
|
55
|
+
/** What the next pulse reports as held by this process. */
|
|
56
|
+
export declare function activeClaims(now?: number): ActiveClaim[];
|
|
39
57
|
/**
|
|
40
58
|
* Roll back edits left behind by a worker that died mid-card. A file is only restored if it still holds
|
|
41
59
|
* exactly what that worker wrote — anything the developer touched since is left alone.
|
package/dist/server.js
CHANGED
|
@@ -7,10 +7,10 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import { spawn, execSync, execFileSync } from 'node:child_process';
|
|
8
8
|
import { isUtf8 } from 'node:buffer';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { AgileMonsterClient, MCP_SERVER_VERSION } from './client.js';
|
|
10
|
+
import { AgileMonsterClient, MCP_SERVER_VERSION, isClaimConflict } from './client.js';
|
|
11
11
|
import { AGENT_GUIDELINES_MARKDOWN } from './guidelines.js';
|
|
12
12
|
import { knownSecrets, redactResult } from './redact.js';
|
|
13
|
-
import { describeProfile, methodLabel, normalizeMethod, resolveDeployProfile, } from './deploy-profile.js';
|
|
13
|
+
import { describeProfile, methodLabel, normalizeMethod, resolveDeployProfile, trustBoundRoute, } from './deploy-profile.js';
|
|
14
14
|
import { addPendingVerification, getCardState, loadMemory, recordAttempt, removePendingVerification, setCardState, setExplicitProfile, } from './deploy-memory.js';
|
|
15
15
|
import { checkCiOnce, describeFailureForHuman, ensureLocalGitExcludes, parseDeployDirective, runDeployment, } from './deploy-runner.js';
|
|
16
16
|
import { addWellKnownToolDirs, agileMonsterHome, configureLogger, ensureUserPath, errorMessage, exec, log, readJson, which, writeJsonAtomic, } from './runtime.js';
|
|
@@ -608,7 +608,8 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
608
608
|
case 'agilemonster_claim_task': {
|
|
609
609
|
const cardId = args.card_id;
|
|
610
610
|
const provider = args.provider || projectConfig?.provider || 'antigravity';
|
|
611
|
-
|
|
611
|
+
// One id per IDE session, so the server can tell this session from the background daemon
|
|
612
|
+
const runRef = args.run_ref?.trim() || sessionRunRef('interactive', provider);
|
|
612
613
|
const expectedBoardId = projectConfig?.board_id;
|
|
613
614
|
if (!expectedBoardId) {
|
|
614
615
|
return {
|
|
@@ -623,6 +624,8 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
623
624
|
};
|
|
624
625
|
}
|
|
625
626
|
const card = await client.claimTask(cardId, provider, runRef, expectedBoardId);
|
|
627
|
+
// Reported with every pulse from now on: the claim stays ours while this IDE's MCP process lives
|
|
628
|
+
holdClaim(cardId, runRef);
|
|
626
629
|
return {
|
|
627
630
|
content: [
|
|
628
631
|
{
|
|
@@ -637,6 +640,7 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
637
640
|
const provider = args.provider || projectConfig?.provider || 'antigravity';
|
|
638
641
|
const question = args.question;
|
|
639
642
|
const card = await client.askQuestion(cardId, provider, question);
|
|
643
|
+
releaseClaim(cardId);
|
|
640
644
|
return {
|
|
641
645
|
content: [
|
|
642
646
|
{
|
|
@@ -653,6 +657,7 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
653
657
|
const tokensUsed = args.tokens_used;
|
|
654
658
|
const costUsd = args.cost_usd;
|
|
655
659
|
const card = await client.addProgress(cardId, provider, update, tokensUsed, costUsd);
|
|
660
|
+
touchClaim(cardId);
|
|
656
661
|
return {
|
|
657
662
|
content: [
|
|
658
663
|
{
|
|
@@ -669,6 +674,7 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
669
674
|
const costUsd = args.cost_usd || 0;
|
|
670
675
|
const note = args.note;
|
|
671
676
|
const card = await client.recordSpend(cardId, provider, tokens, costUsd, note);
|
|
677
|
+
touchClaim(cardId);
|
|
672
678
|
return {
|
|
673
679
|
content: [
|
|
674
680
|
{
|
|
@@ -700,6 +706,7 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
700
706
|
const targetList = args.target_list || args.target_list_id || undefined;
|
|
701
707
|
const assignToUserId = args.assign_to_user_id;
|
|
702
708
|
const card = await client.submitReview(cardId, provider, summary, verificationSteps, targetList, assignToUserId);
|
|
709
|
+
releaseClaim(cardId);
|
|
703
710
|
return {
|
|
704
711
|
content: [
|
|
705
712
|
{
|
|
@@ -728,6 +735,7 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
728
735
|
const provider = args.provider || projectConfig?.provider || 'antigravity';
|
|
729
736
|
const comment = args.comment;
|
|
730
737
|
const card = await client.blockTask(cardId, provider, comment);
|
|
738
|
+
releaseClaim(cardId);
|
|
731
739
|
return {
|
|
732
740
|
content: [
|
|
733
741
|
{
|
|
@@ -742,6 +750,7 @@ Please strictly follow the Agile Monster Task Lifecycle Protocol:
|
|
|
742
750
|
const provider = args.provider || projectConfig?.provider || 'antigravity';
|
|
743
751
|
const comment = args.comment;
|
|
744
752
|
const card = await client.completeTask(cardId, provider, comment);
|
|
753
|
+
releaseClaim(cardId);
|
|
745
754
|
return {
|
|
746
755
|
content: [
|
|
747
756
|
{
|
|
@@ -1351,14 +1360,61 @@ function appendIdeActivityLog(projectPath, entry) {
|
|
|
1351
1360
|
}
|
|
1352
1361
|
catch { }
|
|
1353
1362
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1363
|
+
/**
|
|
1364
|
+
* The board binding's deploy profile for a card, as this machine may use it. A route that another account
|
|
1365
|
+
* set arrives as a proposal (or is demoted to one here): see trustBoundRoute and resolveDeployProfile.
|
|
1366
|
+
*/
|
|
1367
|
+
export function boundProfileOf(card, ownerId) {
|
|
1368
|
+
return (trustBoundRoute({
|
|
1356
1369
|
deploy_method: card.deploy_method,
|
|
1357
1370
|
deploy_command: card.deploy_command,
|
|
1358
1371
|
live_url: card.live_url,
|
|
1359
1372
|
build_command: card.build_command,
|
|
1360
1373
|
deploy_source: card.deploy_source,
|
|
1361
|
-
|
|
1374
|
+
deploy_set_by: card.deploy_set_by,
|
|
1375
|
+
proposal: card.deploy_proposal || null,
|
|
1376
|
+
}, ownerId) || {});
|
|
1377
|
+
}
|
|
1378
|
+
// ---------------------------------------------------------------------------
|
|
1379
|
+
// Claims held by this process
|
|
1380
|
+
// ---------------------------------------------------------------------------
|
|
1381
|
+
const PROCESS_STARTED_AT = Date.now();
|
|
1382
|
+
/** An interactive claim nobody touched for this long is no longer reported as alive. */
|
|
1383
|
+
const CLAIM_REPORT_MAX_IDLE_MS = 4 * 60 * 60 * 1000;
|
|
1384
|
+
const heldClaims = new Map();
|
|
1385
|
+
/**
|
|
1386
|
+
* The id under which this process claims cards. The server keeps a claim for the session that made it, so
|
|
1387
|
+
* the id is stable for the life of the process. Background runs start with `auto-pulse-`: only those are
|
|
1388
|
+
* re-queued automatically when they stop responding.
|
|
1389
|
+
*/
|
|
1390
|
+
export function sessionRunRef(kind, provider) {
|
|
1391
|
+
return kind === 'background'
|
|
1392
|
+
? `auto-pulse-${process.pid}-${PROCESS_STARTED_AT}`
|
|
1393
|
+
: `ide-${(provider || 'agent').replace(/[^a-z0-9_]+/gi, '-').slice(0, 24)}-${process.pid}-${PROCESS_STARTED_AT}`;
|
|
1394
|
+
}
|
|
1395
|
+
export function holdClaim(cardId, runRef) {
|
|
1396
|
+
heldClaims.set(cardId, { runRef, touchedAt: Date.now() });
|
|
1397
|
+
}
|
|
1398
|
+
/** The agent did something with the card: its claim is still in use. */
|
|
1399
|
+
export function touchClaim(cardId) {
|
|
1400
|
+
const held = heldClaims.get(cardId);
|
|
1401
|
+
if (held)
|
|
1402
|
+
held.touchedAt = Date.now();
|
|
1403
|
+
}
|
|
1404
|
+
export function releaseClaim(cardId) {
|
|
1405
|
+
heldClaims.delete(cardId);
|
|
1406
|
+
}
|
|
1407
|
+
/** What the next pulse reports as held by this process. */
|
|
1408
|
+
export function activeClaims(now = Date.now()) {
|
|
1409
|
+
const out = [];
|
|
1410
|
+
for (const [cardId, held] of heldClaims.entries()) {
|
|
1411
|
+
if (now - held.touchedAt > CLAIM_REPORT_MAX_IDLE_MS) {
|
|
1412
|
+
heldClaims.delete(cardId);
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
out.push({ card_id: cardId, run_ref: held.runRef });
|
|
1416
|
+
}
|
|
1417
|
+
return out;
|
|
1362
1418
|
}
|
|
1363
1419
|
/** Feed the outcome back to the server so the board's binding converges on the route that really works. */
|
|
1364
1420
|
async function reportOutcome(client, card, projectPath, res) {
|
|
@@ -1497,7 +1553,7 @@ async function runRedeployOnlyCard(client, card, projectPath, provider, workerLa
|
|
|
1497
1553
|
title: card.title || 'redeploy',
|
|
1498
1554
|
cardId: card.id,
|
|
1499
1555
|
redeploy: true,
|
|
1500
|
-
bound: boundProfileOf(card),
|
|
1556
|
+
bound: boundProfileOf(card, await machineOwnerId(client)),
|
|
1501
1557
|
});
|
|
1502
1558
|
await reportOutcome(client, card, projectPath, res);
|
|
1503
1559
|
const blankState = (extra) => ({
|
|
@@ -1650,7 +1706,7 @@ async function resumeFailedDeployment(client, card, projectPath, provider, worke
|
|
|
1650
1706
|
cardId: card.id,
|
|
1651
1707
|
files: state.files,
|
|
1652
1708
|
redeploy: true,
|
|
1653
|
-
bound: boundProfileOf(card),
|
|
1709
|
+
bound: boundProfileOf(card, await machineOwnerId(client)),
|
|
1654
1710
|
override: directive.method ? { method: directive.method, command: directive.command } : null,
|
|
1655
1711
|
});
|
|
1656
1712
|
await reportOutcome(client, card, projectPath, res);
|
|
@@ -1768,7 +1824,21 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1768
1824
|
log.info('card.start', { card: card.id, title: card.title, project: projectPath, provider });
|
|
1769
1825
|
try {
|
|
1770
1826
|
// 1. Claim the card (automatically moves it from ToDo -> In Progress on the board!)
|
|
1771
|
-
|
|
1827
|
+
const runRef = sessionRunRef('background');
|
|
1828
|
+
try {
|
|
1829
|
+
await client.claimTask(card.id, provider, runRef, card.board_id);
|
|
1830
|
+
}
|
|
1831
|
+
catch (err) {
|
|
1832
|
+
if (isClaimConflict(err)) {
|
|
1833
|
+
// Another session (an IDE agent, or a teammate's machine with the same folder) holds this card.
|
|
1834
|
+
// That is the claim doing its job, not a failure: leave the card and the board alone.
|
|
1835
|
+
removeTaskLock(lockFile);
|
|
1836
|
+
log.info('card.claim_lost', { card: card.id, project: projectPath, reason: errorMessage(err) });
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
throw err;
|
|
1840
|
+
}
|
|
1841
|
+
holdClaim(card.id, runRef);
|
|
1772
1842
|
sendDesktopNotification(`Agile Monster -> ${workerLabel}`, `Claimed & moved to In Progress: "${card.title}"`);
|
|
1773
1843
|
// 1b. Deploy-only resume: the change exists, only shipping it is outstanding
|
|
1774
1844
|
const priorState = getCardState(projectPath, card.id);
|
|
@@ -1788,7 +1858,7 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
1788
1858
|
if (isRedeployOnlyCard(card)) {
|
|
1789
1859
|
return await runRedeployOnlyCard(client, card, projectPath, provider, workerLabel, lockFile);
|
|
1790
1860
|
}
|
|
1791
|
-
const profile = resolveDeployProfile(projectPath, { bound: boundProfileOf(card), memory: loadMemory(projectPath) });
|
|
1861
|
+
const profile = resolveDeployProfile(projectPath, { bound: boundProfileOf(card, await machineOwnerId(client)), memory: loadMemory(projectPath) });
|
|
1792
1862
|
await client.addProgress(card.id, provider, `Autonomous background runner claimed task in \`${projectPath}\` and moved card to **In Progress**. Scanning repository files...`);
|
|
1793
1863
|
// 2. Build keyword list from card title + description and collect project source files
|
|
1794
1864
|
const rawText = `${card.title || ''} ${card.description || ''}`.toLowerCase();
|
|
@@ -2095,7 +2165,7 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
2095
2165
|
title: card.title || 'update',
|
|
2096
2166
|
cardId: card.id,
|
|
2097
2167
|
files: commitFiles,
|
|
2098
|
-
bound: boundProfileOf(card),
|
|
2168
|
+
bound: boundProfileOf(card, await machineOwnerId(client)),
|
|
2099
2169
|
});
|
|
2100
2170
|
await reportOutcome(client, card, projectPath, deployInfo);
|
|
2101
2171
|
if (!deployInfo.ok) {
|
|
@@ -2187,6 +2257,7 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
|
|
|
2187
2257
|
return false;
|
|
2188
2258
|
}
|
|
2189
2259
|
finally {
|
|
2260
|
+
releaseClaim(card.id);
|
|
2190
2261
|
activeAutonomousCards.delete(card.id);
|
|
2191
2262
|
activeWork--;
|
|
2192
2263
|
}
|
|
@@ -2288,7 +2359,7 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
2288
2359
|
const earlierSuccess = memory.deployed_cards[card.id] || null;
|
|
2289
2360
|
// The deployment started at review time is still running in CI: wait for its verdict rather than deploying again
|
|
2290
2361
|
if (stillPending && !isContainerRestartCard) {
|
|
2291
|
-
const profile = resolveDeployProfile(projectPath, { bound: boundProfileOf(card), memory });
|
|
2362
|
+
const profile = resolveDeployProfile(projectPath, { bound: boundProfileOf(card, await machineOwnerId(client)), memory });
|
|
2292
2363
|
const check = await checkCiOnce(projectPath, stillPending.sha, stillPending.workflow_names, profile.workflows.some((w) => w.isDeploy && w.workflowRun));
|
|
2293
2364
|
if (check.verdict === 'success') {
|
|
2294
2365
|
removePendingVerification(projectPath, card.id);
|
|
@@ -2335,7 +2406,7 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
|
|
|
2335
2406
|
cardId: card.id,
|
|
2336
2407
|
files: state?.files,
|
|
2337
2408
|
redeploy: isContainerRestartCard,
|
|
2338
|
-
bound: boundProfileOf(card),
|
|
2409
|
+
bound: boundProfileOf(card, await machineOwnerId(client)),
|
|
2339
2410
|
});
|
|
2340
2411
|
await reportOutcome(client, card, projectPath, res);
|
|
2341
2412
|
if (!res.ok) {
|
|
@@ -2559,6 +2630,12 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
|
|
|
2559
2630
|
if (pulseRunning) {
|
|
2560
2631
|
if (holdsWorkerLock)
|
|
2561
2632
|
acquireWorkerLock(role);
|
|
2633
|
+
// The same goes for the cards being worked on: while this pulse is busy nothing else tells the server
|
|
2634
|
+
// they are still held, and a build or deploy step can outlast the claim's lease.
|
|
2635
|
+
const held = activeClaims();
|
|
2636
|
+
if (held.length > 0) {
|
|
2637
|
+
void client.heartbeat(held).catch((err) => log.warn('claim.heartbeat_failed', { error: errorMessage(err) }));
|
|
2638
|
+
}
|
|
2562
2639
|
return;
|
|
2563
2640
|
}
|
|
2564
2641
|
pulseRunning = true;
|
|
@@ -2577,6 +2654,7 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role =
|
|
|
2577
2654
|
projectPath: config.project_path || process.cwd(),
|
|
2578
2655
|
discoveredProjects: includeDiscovery ? client.discoverLocalProjects() : undefined,
|
|
2579
2656
|
discoveredModels: includeDiscovery ? client.discoverLocalAgentModels() : undefined,
|
|
2657
|
+
activeClaims: activeClaims(),
|
|
2580
2658
|
});
|
|
2581
2659
|
if (consecutiveFailures > 0) {
|
|
2582
2660
|
log.info('pulse.recovered', { after_failures: consecutiveFailures });
|
package/dist/types.d.ts
CHANGED
|
@@ -42,6 +42,25 @@ export interface Card {
|
|
|
42
42
|
deploy_stage?: string | null;
|
|
43
43
|
/** 'user' = set by a human, 'verified' = confirmed by a successful deploy, 'auto' = a guess. */
|
|
44
44
|
deploy_source?: string | null;
|
|
45
|
+
/** The account that set the deploy route on the board binding. */
|
|
46
|
+
deploy_set_by?: string | null;
|
|
47
|
+
/**
|
|
48
|
+
* A deploy route that another account set. The server withholds it from deploy_method / deploy_command
|
|
49
|
+
* and sends it here instead: it is never run on this machine without the owner's confirmation.
|
|
50
|
+
*/
|
|
51
|
+
deploy_proposal?: {
|
|
52
|
+
method?: string | null;
|
|
53
|
+
command?: string | null;
|
|
54
|
+
live_url?: string | null;
|
|
55
|
+
source?: string | null;
|
|
56
|
+
set_by?: string | null;
|
|
57
|
+
set_by_name?: string | null;
|
|
58
|
+
} | null;
|
|
59
|
+
}
|
|
60
|
+
/** A card this process has claimed, reported with every pulse so the server keeps the claim alive. */
|
|
61
|
+
export interface ActiveClaim {
|
|
62
|
+
card_id: string;
|
|
63
|
+
run_ref: string;
|
|
45
64
|
}
|
|
46
65
|
export interface CardComment {
|
|
47
66
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilemonster/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
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",
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
"publishConfig": {
|
|
9
9
|
"access": "public"
|
|
10
10
|
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/nieuwsolutions/agilemonster.git",
|
|
14
|
+
"directory": "packages/mcp-server"
|
|
15
|
+
},
|
|
11
16
|
"bin": {
|
|
12
17
|
"mcp-server": "bin/mcp.js",
|
|
13
18
|
"agilemonster": "bin/mcp.js",
|