@agilemonster/mcp-server 1.0.25 ā 1.0.27
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 +43 -8
- package/bin/mcp.js +25 -1
- package/bin/orchestrator.js +1 -0
- package/dist/agent-engine.d.ts +4 -0
- package/dist/agent-engine.js +14 -2
- package/dist/branch-health.d.ts +118 -0
- package/dist/branch-health.js +302 -0
- package/dist/client.d.ts +17 -3
- package/dist/client.js +24 -5
- package/dist/deploy-memory.d.ts +17 -0
- package/dist/deploy-memory.js +20 -0
- package/dist/deploy-runner.d.ts +9 -0
- package/dist/deploy-runner.js +43 -4
- 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 +13 -1
- package/dist/server.js +197 -34
- package/dist/service.d.ts +82 -0
- package/dist/service.js +327 -0
- package/package.json +4 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import type { MachineReport } from './machine.js';
|
|
1
2
|
import { ActiveClaim, AgileMonsterProjectConfig, AutonomousStepResponse, CardComment, DeployReport, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
|
|
2
|
-
export declare const MCP_SERVER_VERSION = "1.0.
|
|
3
|
+
export declare const MCP_SERVER_VERSION = "1.0.27";
|
|
4
|
+
export type QuestionOption = string | {
|
|
5
|
+
label: string;
|
|
6
|
+
reply: string;
|
|
7
|
+
};
|
|
3
8
|
/** An API call the server answered with an error status. `status` lets callers tell "refused" from "broken". */
|
|
4
9
|
export declare class ApiError extends Error {
|
|
5
10
|
readonly status: number;
|
|
@@ -22,6 +27,8 @@ export declare class AgileMonsterClient {
|
|
|
22
27
|
setApiKey(key: string): void;
|
|
23
28
|
loadProjectConfig(): AgileMonsterProjectConfig | null;
|
|
24
29
|
saveProjectConfig(config: AgileMonsterProjectConfig, dir?: string): string;
|
|
30
|
+
/** True when ~/.agilemonster/config.json already holds a key (what a background service reads). */
|
|
31
|
+
static hasGlobalApiKey(): boolean;
|
|
25
32
|
static saveGlobalConfig(data: {
|
|
26
33
|
api_key?: string;
|
|
27
34
|
}): string;
|
|
@@ -38,7 +45,8 @@ export declare class AgileMonsterClient {
|
|
|
38
45
|
agents: BoardAgentConfig[];
|
|
39
46
|
}>;
|
|
40
47
|
pulse(options: {
|
|
41
|
-
|
|
48
|
+
/** Omitted by a background service that was not started inside a project folder. */
|
|
49
|
+
boardId?: string;
|
|
42
50
|
provider?: string;
|
|
43
51
|
projectName?: string;
|
|
44
52
|
projectPath?: string;
|
|
@@ -50,6 +58,8 @@ export declare class AgileMonsterClient {
|
|
|
50
58
|
installedAgents?: string[];
|
|
51
59
|
/** Cards this process holds: the server extends their claim (heartbeat). */
|
|
52
60
|
activeClaims?: ActiveClaim[];
|
|
61
|
+
/** This computer and the agents on it that can take a card (shown on the board next to a waiting card). */
|
|
62
|
+
machine?: MachineReport;
|
|
53
63
|
}): Promise<PulseResponse>;
|
|
54
64
|
/** Extend the claims this process holds. Used while a long job keeps the pulse loop busy. */
|
|
55
65
|
heartbeat(activeClaims: ActiveClaim[]): Promise<{
|
|
@@ -93,7 +103,11 @@ export declare class AgileMonsterClient {
|
|
|
93
103
|
cards: Card[];
|
|
94
104
|
}>;
|
|
95
105
|
claimTask(cardId: string, provider: string, runRef?: string, expectedBoardId?: string): Promise<Card>;
|
|
96
|
-
|
|
106
|
+
/**
|
|
107
|
+
* `options` become reply buttons on the card: a plain string is both the button and the reply, a
|
|
108
|
+
* {label, reply} pair shows one thing and sends another ("Yes, deploy this way" -> "deploy: ./deploy.sh").
|
|
109
|
+
*/
|
|
110
|
+
askQuestion(cardId: string, provider: string, question: string, options?: QuestionOption[]): Promise<Card>;
|
|
97
111
|
addProgress(cardId: string, provider: string, update: string, tokensUsed?: number, costUsd?: number): Promise<Card>;
|
|
98
112
|
recordSpend(cardId: string, provider: string, tokens: number, costUsd: number, note?: string): Promise<Card>;
|
|
99
113
|
setBudget(cardId: string, maxBudgetUsd?: number | null, maxTokens?: number | null): Promise<Card>;
|
package/dist/client.js
CHANGED
|
@@ -4,7 +4,7 @@ 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.27';
|
|
8
8
|
/** An API call the server answered with an error status. `status` lets callers tell "refused" from "broken". */
|
|
9
9
|
export class ApiError extends Error {
|
|
10
10
|
status;
|
|
@@ -111,6 +111,19 @@ export class AgileMonsterClient {
|
|
|
111
111
|
this.cachedConfig = config;
|
|
112
112
|
return configPath;
|
|
113
113
|
}
|
|
114
|
+
/** True when ~/.agilemonster/config.json already holds a key (what a background service reads). */
|
|
115
|
+
static hasGlobalApiKey() {
|
|
116
|
+
try {
|
|
117
|
+
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
|
118
|
+
if (!homedir)
|
|
119
|
+
return false;
|
|
120
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(homedir, '.agilemonster', 'config.json'), 'utf8'));
|
|
121
|
+
return typeof parsed?.api_key === 'string' && parsed.api_key.trim() !== '';
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
114
127
|
static saveGlobalConfig(data) {
|
|
115
128
|
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
|
116
129
|
if (!homedir)
|
|
@@ -216,6 +229,7 @@ export class AgileMonsterClient {
|
|
|
216
229
|
discovered_models: options.discoveredModels,
|
|
217
230
|
installed_agents: installed,
|
|
218
231
|
active_claims: options.activeClaims && options.activeClaims.length > 0 ? options.activeClaims : undefined,
|
|
232
|
+
machine: options.machine,
|
|
219
233
|
}),
|
|
220
234
|
});
|
|
221
235
|
}
|
|
@@ -453,11 +467,12 @@ export class AgileMonsterClient {
|
|
|
453
467
|
*/
|
|
454
468
|
discoverLocalProjects() {
|
|
455
469
|
const roots = new Set();
|
|
470
|
+
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
|
456
471
|
const parentDir = path.dirname(this.cwd);
|
|
457
|
-
|
|
472
|
+
// A background service runs from the home folder: its parent holds other people's homes, not projects
|
|
473
|
+
if (parentDir && parentDir !== '/' && parentDir !== this.cwd && path.resolve(this.cwd) !== path.resolve(homedir || '/')) {
|
|
458
474
|
roots.add(parentDir);
|
|
459
475
|
}
|
|
460
|
-
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
|
461
476
|
if (homedir) {
|
|
462
477
|
for (const sub of ['dev', 'Projects', 'Code', 'repos', 'Developer']) {
|
|
463
478
|
const candidate = path.join(homedir, sub);
|
|
@@ -555,10 +570,14 @@ export class AgileMonsterClient {
|
|
|
555
570
|
body: JSON.stringify({ provider, run_ref: runRef, board_id: boardId }),
|
|
556
571
|
});
|
|
557
572
|
}
|
|
558
|
-
|
|
573
|
+
/**
|
|
574
|
+
* `options` become reply buttons on the card: a plain string is both the button and the reply, a
|
|
575
|
+
* {label, reply} pair shows one thing and sends another ("Yes, deploy this way" -> "deploy: ./deploy.sh").
|
|
576
|
+
*/
|
|
577
|
+
async askQuestion(cardId, provider, question, options) {
|
|
559
578
|
return this.request(`/api/agent/cards/${cardId}/ask-question`, {
|
|
560
579
|
method: 'POST',
|
|
561
|
-
body: JSON.stringify({ provider, question }),
|
|
580
|
+
body: JSON.stringify({ provider, question, options: options && options.length > 0 ? options.slice(0, 4) : undefined }),
|
|
562
581
|
});
|
|
563
582
|
}
|
|
564
583
|
async addProgress(cardId, provider, update, tokensUsed, costUsd) {
|
package/dist/deploy-memory.d.ts
CHANGED
|
@@ -50,6 +50,11 @@ export interface CardDeployState {
|
|
|
50
50
|
at: string;
|
|
51
51
|
attempts: number;
|
|
52
52
|
last_error?: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Where the last attempt stopped. 'git' = the change never reached the remote, so a retry only has to push it
|
|
55
|
+
* (or find that a person already did) ā it must not add an empty "redeploy" commit.
|
|
56
|
+
*/
|
|
57
|
+
failed_stage?: 'git' | 'deploy' | null;
|
|
53
58
|
/** Set when the agent asked "may I deploy with X?": a plain "yes"/"retry" then confirms X. */
|
|
54
59
|
pending_confirmation?: {
|
|
55
60
|
method: string;
|
|
@@ -89,6 +94,13 @@ export interface ProjectMemory {
|
|
|
89
94
|
method: string;
|
|
90
95
|
verified: boolean;
|
|
91
96
|
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Cards that are waiting for the owner to decide what to do with a branch that cannot be pushed
|
|
99
|
+
* (asked BEFORE any work started). Only a reply written after `at` answers the question.
|
|
100
|
+
*/
|
|
101
|
+
branch_questions: Record<string, {
|
|
102
|
+
at: string;
|
|
103
|
+
}>;
|
|
92
104
|
}
|
|
93
105
|
export declare function memoryFileFor(projectPath: string): string;
|
|
94
106
|
/**
|
|
@@ -118,5 +130,10 @@ export declare function setExplicitProfile(projectPath: string, explicit: {
|
|
|
118
130
|
}): ProjectMemory;
|
|
119
131
|
export declare function setCardState(projectPath: string, cardId: string, state: CardDeployState | null): void;
|
|
120
132
|
export declare function getCardState(projectPath: string, cardId: string): CardDeployState | null;
|
|
133
|
+
/** Remember (or, with null, forget) that this card is waiting for a decision about its branch. */
|
|
134
|
+
export declare function setBranchQuestion(projectPath: string, cardId: string, at: string | null): void;
|
|
135
|
+
export declare function getBranchQuestion(projectPath: string, cardId: string): {
|
|
136
|
+
at: string;
|
|
137
|
+
} | null;
|
|
121
138
|
export declare function addPendingVerification(projectPath: string, pending: PendingVerification): void;
|
|
122
139
|
export declare function removePendingVerification(projectPath: string, cardId: string): void;
|
package/dist/deploy-memory.js
CHANGED
|
@@ -71,6 +71,7 @@ export function loadMemory(projectPath) {
|
|
|
71
71
|
pending: [],
|
|
72
72
|
cards: {},
|
|
73
73
|
deployed_cards: {},
|
|
74
|
+
branch_questions: {},
|
|
74
75
|
};
|
|
75
76
|
saveMemory(memory);
|
|
76
77
|
return memory;
|
|
@@ -86,6 +87,7 @@ export function loadMemory(projectPath) {
|
|
|
86
87
|
pending: Array.isArray(stored?.pending) ? stored.pending : [],
|
|
87
88
|
cards: stored?.cards && typeof stored.cards === 'object' ? stored.cards : {},
|
|
88
89
|
deployed_cards: stored?.deployed_cards && typeof stored.deployed_cards === 'object' ? stored.deployed_cards : {},
|
|
90
|
+
branch_questions: stored?.branch_questions && typeof stored.branch_questions === 'object' ? stored.branch_questions : {},
|
|
89
91
|
};
|
|
90
92
|
}
|
|
91
93
|
export function saveMemory(memory) {
|
|
@@ -110,6 +112,10 @@ export function saveMemory(memory) {
|
|
|
110
112
|
.slice(0, deployedIds.length - DEPLOYED_CARDS_LIMIT)
|
|
111
113
|
.forEach((id) => delete memory.deployed_cards[id]);
|
|
112
114
|
}
|
|
115
|
+
for (const id of Object.keys(memory.branch_questions)) {
|
|
116
|
+
if (Date.now() - (Date.parse(memory.branch_questions[id].at) || 0) > STALE_CARD_STATE_MS)
|
|
117
|
+
delete memory.branch_questions[id];
|
|
118
|
+
}
|
|
113
119
|
memory.history = memory.history.slice(-HISTORY_LIMIT);
|
|
114
120
|
try {
|
|
115
121
|
writeJsonAtomic(memoryFileFor(memory.project_path), memory);
|
|
@@ -176,6 +182,20 @@ export function setCardState(projectPath, cardId, state) {
|
|
|
176
182
|
export function getCardState(projectPath, cardId) {
|
|
177
183
|
return loadMemory(projectPath).cards[cardId] || null;
|
|
178
184
|
}
|
|
185
|
+
/** Remember (or, with null, forget) that this card is waiting for a decision about its branch. */
|
|
186
|
+
export function setBranchQuestion(projectPath, cardId, at) {
|
|
187
|
+
const memory = loadMemory(projectPath);
|
|
188
|
+
if (at)
|
|
189
|
+
memory.branch_questions[cardId] = { at };
|
|
190
|
+
else if (memory.branch_questions[cardId])
|
|
191
|
+
delete memory.branch_questions[cardId];
|
|
192
|
+
else
|
|
193
|
+
return;
|
|
194
|
+
saveMemory(memory);
|
|
195
|
+
}
|
|
196
|
+
export function getBranchQuestion(projectPath, cardId) {
|
|
197
|
+
return loadMemory(projectPath).branch_questions[cardId] || null;
|
|
198
|
+
}
|
|
179
199
|
export function addPendingVerification(projectPath, pending) {
|
|
180
200
|
const memory = loadMemory(projectPath);
|
|
181
201
|
memory.pending = memory.pending.filter((p) => p.card_id !== pending.card_id);
|
package/dist/deploy-runner.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BoundProfile, DeployMethod, DeployProfile } from './deploy-profile.js';
|
|
2
2
|
import { PendingVerification } from './deploy-memory.js';
|
|
3
|
+
import { PushConflict } from './branch-health.js';
|
|
3
4
|
/**
|
|
4
5
|
* Runs a project's deployment and proves it worked.
|
|
5
6
|
*
|
|
@@ -39,6 +40,12 @@ export interface DeployResult {
|
|
|
39
40
|
commitHash: string;
|
|
40
41
|
fullSha: string;
|
|
41
42
|
branch: string;
|
|
43
|
+
/** The commit reached the remote in this attempt (or was already there). */
|
|
44
|
+
pushed: boolean;
|
|
45
|
+
/** Where a failed attempt stopped: 'git' = before the change reached the remote; 'deploy' = after. */
|
|
46
|
+
stage: 'git' | 'deploy' | null;
|
|
47
|
+
/** A refused push, worked out: what clashes and which ways out exist. */
|
|
48
|
+
conflict?: PushConflict;
|
|
42
49
|
deployedNote: string;
|
|
43
50
|
errorDetail: string;
|
|
44
51
|
errorKind: string | null;
|
|
@@ -59,6 +66,8 @@ export interface DeployDirective {
|
|
|
59
66
|
method: DeployMethod | null;
|
|
60
67
|
command: string | null;
|
|
61
68
|
liveUrl: string | null;
|
|
69
|
+
/** "Ship only this change": leave older unpushed commits out and push just this card's commit. */
|
|
70
|
+
onlyThis?: boolean;
|
|
62
71
|
}
|
|
63
72
|
/**
|
|
64
73
|
* Parse a human reply to a "deployment failed / may I deploy with X?" question.
|
package/dist/deploy-runner.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
4
|
import { composeArgs, isRunnableCommand, methodLabel, normalizeMethod, pushPathsMatch, requiresConfirmation, resolveDeployProfile, } from './deploy-profile.js';
|
|
5
5
|
import { loadMemory, recordAttempt } from './deploy-memory.js';
|
|
6
|
+
import { describePushConflict, explainPushConflict } from './branch-health.js';
|
|
6
7
|
import { ensureUserPath, exec, execShell, log, sleep, which } from './runtime.js';
|
|
7
8
|
const FAILURE_TABLE = [
|
|
8
9
|
[
|
|
@@ -69,6 +70,7 @@ export function classifyFailure(text) {
|
|
|
69
70
|
}
|
|
70
71
|
return { kind: 'unknown', hint: '', healable: false };
|
|
71
72
|
}
|
|
73
|
+
const ONLY_THIS_RE = /^(ship |push |deploy )?(only|just) this( one)?( change| card| commit)?$/i;
|
|
72
74
|
const AFFIRMATIVE_RE = /^(retry|re-?deploy|try again|deploy again|go ahead|fixed|yes|yep|ok|okay|confirm(ed)?|do it)[\s.!]*$/i;
|
|
73
75
|
/** A directive value only counts as a command when it starts like one ā prose after "Deployment:" never runs. */
|
|
74
76
|
const COMMAND_START_RE = /^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:\.{1,2}\/|\/|~\/|(?:docker|docker-compose|make|npm|pnpm|yarn|npx|bun|bunx|node|deno|python3?|bash|sh|zsh|fly|flyctl|vercel|netlify|wrangler|firebase|gh|git|rsync|scp|ssh|ansible-playbook|kubectl|helm|terraform|cap|bundle|php|composer|gcloud|aws|az|heroku|task|just|cargo|go|dotnet|mvn|gradle|rake|fab|serverless|sls|cdk|pulumi)(?=\s|$))/;
|
|
@@ -91,7 +93,12 @@ export function parseDeployDirective(text) {
|
|
|
91
93
|
if (directive) {
|
|
92
94
|
const value = directive[1].replace(/^`+|`+$/g, '').trim();
|
|
93
95
|
const asMethod = normalizeMethod(value);
|
|
94
|
-
if (
|
|
96
|
+
if (ONLY_THIS_RE.test(value.replace(/[\s.!]+$/, ''))) {
|
|
97
|
+
out.onlyThis = true;
|
|
98
|
+
out.retry = true;
|
|
99
|
+
matched = true;
|
|
100
|
+
}
|
|
101
|
+
else if (AFFIRMATIVE_RE.test(value)) {
|
|
95
102
|
// "deploy: yes" confirms; it is not a program called `yes`
|
|
96
103
|
out.retry = true;
|
|
97
104
|
matched = true;
|
|
@@ -298,12 +305,21 @@ async function commitAndPush(req, profile, pushPolicy) {
|
|
|
298
305
|
push = await git(['push', state.remote, state.branch], 90000);
|
|
299
306
|
}
|
|
300
307
|
else {
|
|
308
|
+
const unmergedFiles = unmerged.split(/\r?\n/).filter(Boolean);
|
|
301
309
|
await git(['rebase', '--abort']);
|
|
310
|
+
// The raw Git output is for the log. The card gets what clashes, in words, and the ways out.
|
|
311
|
+
log.warn('deploy.rebase_conflict', { card: req.cardId, project: cwd, branch: state.branch, files: unmergedFiles, output: pull.tail.slice(0, 600) });
|
|
312
|
+
if (unmergedFiles.length === 0)
|
|
313
|
+
return fail(pull, `Could not bring in the newer commits from ${state.remote}/${state.branch}`);
|
|
314
|
+
const conflict = await explainPushConflict(cwd, req.cardId, unmergedFiles);
|
|
302
315
|
return {
|
|
303
316
|
...state,
|
|
304
317
|
ok: false,
|
|
305
318
|
errorKind: 'rebase_conflict',
|
|
306
|
-
|
|
319
|
+
conflict: conflict || undefined,
|
|
320
|
+
errorDetail: conflict
|
|
321
|
+
? describePushConflict(conflict).detail
|
|
322
|
+
: `The change is committed on this computer, but the remote branch \`${state.branch}\` has newer commits that clash with it in ${unmergedFiles.slice(0, 3).map((f) => `\`${f}\``).join(', ')}, so it could not be pushed.`,
|
|
307
323
|
};
|
|
308
324
|
}
|
|
309
325
|
}
|
|
@@ -959,6 +975,8 @@ export async function runDeployment(req) {
|
|
|
959
975
|
commitHash: (git?.fullSha || '').slice(0, 7),
|
|
960
976
|
fullSha: git?.fullSha || '',
|
|
961
977
|
branch: git?.branch || '',
|
|
978
|
+
pushed: Boolean(git?.pushed),
|
|
979
|
+
stage: null,
|
|
962
980
|
deployedNote: '',
|
|
963
981
|
errorDetail: '',
|
|
964
982
|
errorKind: null,
|
|
@@ -983,7 +1001,14 @@ export async function runDeployment(req) {
|
|
|
983
1001
|
const kind = git.errorKind || 'git';
|
|
984
1002
|
recordAttempt(req.projectPath, { card_id: req.cardId, method: profile.method, command: profile.command, ok: false, kind, error: git.errorDetail, duration_ms: Date.now() - startedAt });
|
|
985
1003
|
log.warn('deploy.git_failed', { card: req.cardId, project: req.projectPath, kind, error: git.errorDetail });
|
|
986
|
-
return result({
|
|
1004
|
+
return result({
|
|
1005
|
+
errorDetail: git.errorDetail || 'Git failed',
|
|
1006
|
+
errorKind: kind,
|
|
1007
|
+
stage: 'git',
|
|
1008
|
+
conflict: git.conflict,
|
|
1009
|
+
hint: git.conflict ? describePushConflict(git.conflict).hint : classifyFailure(git.errorDetail || '').hint || HINTS[kind] || '',
|
|
1010
|
+
healed: git.healed,
|
|
1011
|
+
}, git);
|
|
987
1012
|
}
|
|
988
1013
|
let step = await runMethod(req, profile, git, changedFiles);
|
|
989
1014
|
const healed = [...git.healed, ...step.healed];
|
|
@@ -1019,6 +1044,7 @@ export async function runDeployment(req) {
|
|
|
1019
1044
|
return result({
|
|
1020
1045
|
errorDetail: step.errorDetail || 'Deployment failed',
|
|
1021
1046
|
errorKind: kind,
|
|
1047
|
+
stage: 'deploy',
|
|
1022
1048
|
hint: HINTS[kind] || classifyFailure(step.errorDetail || '').hint,
|
|
1023
1049
|
actions: [...git.actions, ...step.actions],
|
|
1024
1050
|
healed,
|
|
@@ -1035,7 +1061,7 @@ export async function runDeployment(req) {
|
|
|
1035
1061
|
else if (trusted) {
|
|
1036
1062
|
const detail = `The deployment ran, but ${profile.liveUrl} returned ${status === 0 ? 'no response' : `HTTP ${status}`}.`;
|
|
1037
1063
|
recordAttempt(req.projectPath, { card_id: req.cardId, method: profile.method, command: profile.command, ok: false, kind: 'live_check_failed', error: detail, commit: git.fullSha.slice(0, 7), duration_ms: Date.now() - startedAt });
|
|
1038
|
-
return result({ errorDetail: detail, errorKind: 'live_check_failed', hint: 'Check the site, then reply "retry".', actions, healed }, git);
|
|
1064
|
+
return result({ errorDetail: detail, errorKind: 'live_check_failed', stage: 'deploy', hint: 'Check the site, then reply "retry".', actions, healed }, git);
|
|
1039
1065
|
}
|
|
1040
1066
|
}
|
|
1041
1067
|
// A human-pinned route is its own proof of intent; anything else only becomes "what worked" once verified
|
|
@@ -1097,6 +1123,19 @@ export function describeFailureForHuman(res) {
|
|
|
1097
1123
|
`Or reply \`deploy: manual\` if you deploy this project yourself${others.length > 0 ? `, or pick another route I found: ${others.map((a) => `\`deploy: ${a.command || a.method}\``).join(', ')}` : ''}.`,
|
|
1098
1124
|
].join('\n');
|
|
1099
1125
|
}
|
|
1126
|
+
if (res.conflict) {
|
|
1127
|
+
return [res.errorDetail, '', 'You can choose:', res.hint].join('\n');
|
|
1128
|
+
}
|
|
1129
|
+
if (res.stage === 'git') {
|
|
1130
|
+
// The change never reached the remote. Which deploy route was picked is beside the point here:
|
|
1131
|
+
// say what is in the way first, and keep Git's own words for whoever wants them.
|
|
1132
|
+
const text = res.hint
|
|
1133
|
+
? [`The change is made, but Git stopped before it reached the remote. ${res.hint}`, '', `Technical detail: ${res.errorDetail.replace(/\s+/g, ' ').slice(0, 400)}`]
|
|
1134
|
+
: [res.errorDetail.slice(0, 900)];
|
|
1135
|
+
if (!/\bretry\b/i.test(text.join(' ')))
|
|
1136
|
+
text.push('', 'Reply **retry** once it is fixed: I will not redo the work, only push it and check the deployment.');
|
|
1137
|
+
return text.join('\n');
|
|
1138
|
+
}
|
|
1100
1139
|
const lines = [`What failed: ${res.errorDetail.slice(0, 900)}`];
|
|
1101
1140
|
if (res.hint)
|
|
1102
1141
|
lines.push('', res.hint);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this computer tells the board with every pulse: which machine it is, whether the runner is the
|
|
3
|
+
* background service or a coding tool that happens to be open, and which agents on it can take a card.
|
|
4
|
+
* The board turns that into what a person reads next to a waiting card ("Codex on Studio Mac is ready",
|
|
5
|
+
* "Cursor is not signed in").
|
|
6
|
+
*/
|
|
7
|
+
export interface MachineReport {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
platform: string;
|
|
11
|
+
runner: 'service' | 'ide';
|
|
12
|
+
mode: 'agents' | 'builtin' | 'custom';
|
|
13
|
+
agents: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
ready: boolean;
|
|
17
|
+
problem: string | null;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/** A stable id for this computer, made once and kept in ~/.agilemonster/machine.json. */
|
|
21
|
+
export declare function machineIdentity(): {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function machineReport(role: 'daemon' | 'stdio'): Promise<MachineReport>;
|
package/dist/machine.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { agentMode, engineStatuses } from './agent-engine.js';
|
|
5
|
+
import { agileMonsterHome, readJson, writeJsonAtomic } from './runtime.js';
|
|
6
|
+
/** A stable id for this computer, made once and kept in ~/.agilemonster/machine.json. */
|
|
7
|
+
export function machineIdentity() {
|
|
8
|
+
const file = path.join(agileMonsterHome(), 'machine.json');
|
|
9
|
+
const saved = readJson(file);
|
|
10
|
+
let id = typeof saved?.id === 'string' && /^[A-Za-z0-9_-]{8,64}$/.test(saved.id) ? saved.id : '';
|
|
11
|
+
if (!id) {
|
|
12
|
+
id = crypto.randomUUID();
|
|
13
|
+
try {
|
|
14
|
+
writeJsonAtomic(file, { id });
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
}
|
|
18
|
+
const name = (os.hostname() || 'computer').replace(/\.(local|lan|home|localdomain)$/i, '').slice(0, 120);
|
|
19
|
+
return { id, name };
|
|
20
|
+
}
|
|
21
|
+
export async function machineReport(role) {
|
|
22
|
+
const { id, name } = machineIdentity();
|
|
23
|
+
const agents = await engineStatuses();
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
name,
|
|
27
|
+
platform: process.platform,
|
|
28
|
+
runner: role === 'daemon' ? 'service' : 'ide',
|
|
29
|
+
mode: agentMode(),
|
|
30
|
+
agents: agents.map((a) => ({ id: a.id, label: a.label, ready: !a.problem, problem: a.problem })),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export declare class AgileMonsterOrchestrator {
|
|
|
21
21
|
provider?: string;
|
|
22
22
|
ide?: string;
|
|
23
23
|
board?: string;
|
|
24
|
+
/** False skips installing the background runner (`--no-service`). */
|
|
25
|
+
service?: boolean;
|
|
24
26
|
}): Promise<void>;
|
|
25
27
|
bootstrapInteractive(options?: {
|
|
26
28
|
provider?: string;
|
package/dist/orchestrator.js
CHANGED
|
@@ -6,6 +6,9 @@ import { AGENT_GUIDELINES_MARKDOWN } from './guidelines.js';
|
|
|
6
6
|
import { configureIdeTargets } from './ide-config.js';
|
|
7
7
|
import { ensureLocalGitExcludes } from './deploy-runner.js';
|
|
8
8
|
import { stripRemoteCredentials } from './redact.js';
|
|
9
|
+
import { describeAgents } from './agent-engine.js';
|
|
10
|
+
import { ensureUserPath } from './runtime.js';
|
|
11
|
+
import { installService } from './service.js';
|
|
9
12
|
export class AgileMonsterOrchestrator {
|
|
10
13
|
client;
|
|
11
14
|
constructor(client = new AgileMonsterClient()) {
|
|
@@ -313,15 +316,38 @@ export class AgileMonsterOrchestrator {
|
|
|
313
316
|
// Configure IDEs (Cursor, Claude Code, Windsurf, Antigravity, VS Code)
|
|
314
317
|
this.configureIdes(apiKey, options?.ide || options?.provider);
|
|
315
318
|
this.ensureGitignore();
|
|
316
|
-
|
|
319
|
+
// The background runner: without it cards are only worked while a coding tool is open in this folder
|
|
320
|
+
let background = false;
|
|
321
|
+
if (options?.service !== false && process.env.AGILEMONSTER_NO_SERVICE !== '1') {
|
|
322
|
+
console.log('\nāļø Setting up the background runner, so cards are worked with no coding tool open...');
|
|
323
|
+
try {
|
|
324
|
+
await ensureUserPath();
|
|
325
|
+
// The service is not started inside a project folder, so it reads the key from the home config
|
|
326
|
+
if (!AgileMonsterClient.hasGlobalApiKey())
|
|
327
|
+
AgileMonsterClient.saveGlobalConfig({ api_key: apiKey });
|
|
328
|
+
const svc = await installService({ hasApiKey: true });
|
|
329
|
+
background = svc.outcome === 'installed' || svc.outcome === 'kept';
|
|
330
|
+
console.log(`${svc.ok ? 'ā
' : 'ā ļø '} ${svc.message}`);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
console.log(`ā ļø The background runner could not be set up: ${err instanceof Error ? err.message : String(err)}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// Which coding agent will actually do the work, and the one step that fixes one that is not ready
|
|
337
|
+
try {
|
|
338
|
+
console.log(`\n${await describeAgents()}`);
|
|
339
|
+
}
|
|
340
|
+
catch { }
|
|
341
|
+
console.log('\nš This folder is connected to Agile Monster.');
|
|
317
342
|
console.log('\n======================================================');
|
|
318
|
-
console.log(' š
|
|
343
|
+
console.log(' š WHAT HAPPENS NOW ');
|
|
319
344
|
console.log('======================================================');
|
|
320
|
-
console.log(`1. Open board "${targetBoard.name}" on https://app.agilemonster.com`);
|
|
321
|
-
console.log(`2.
|
|
322
|
-
console.log(
|
|
323
|
-
|
|
324
|
-
|
|
345
|
+
console.log(`1. Open the board "${targetBoard.name}" on https://app.agilemonster.com`);
|
|
346
|
+
console.log(`2. Add a card that says what you want, open it, and under "Assign to AI Agent or Team Member" choose "${shortTag ? `Cursor-${shortTag}` : `Cursor: ${targetBoard.name}`}" (or another agent).`);
|
|
347
|
+
console.log(background
|
|
348
|
+
? '3. That is all. The card is picked up within about 20 seconds, worked in this folder, and comes back to you for review.'
|
|
349
|
+
: '3. Keep your AI coding tool open in this folder: the card is picked up within about 20 seconds while it is open, and comes back to you for review.');
|
|
350
|
+
console.log('\nTo connect another project folder, run the same command there:');
|
|
325
351
|
console.log(' cd /path/to/other-project && npx -y @agilemonster/mcp-server bootstrap\n');
|
|
326
352
|
}
|
|
327
353
|
async bootstrapInteractive(options) {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import { AgileMonsterClient } from './client.js';
|
|
2
|
+
import { AgileMonsterClient, QuestionOption } from './client.js';
|
|
3
3
|
import { ActiveClaim, Card } from './types.js';
|
|
4
4
|
import { BoundProfile } from './deploy-profile.js';
|
|
5
|
+
import { PushConflict } from './branch-health.js';
|
|
6
|
+
/**
|
|
7
|
+
* The answers a deploy question accepts, as reply buttons: nobody should have to type `deploy: ./deploy.sh`.
|
|
8
|
+
* A first-time route gets "yes, this way" and "I deploy it myself"; a failure gets "try again".
|
|
9
|
+
*/
|
|
10
|
+
export declare function deployReplyOptions(res: {
|
|
11
|
+
errorKind?: string | null;
|
|
12
|
+
method?: string | null;
|
|
13
|
+
command?: string | null;
|
|
14
|
+
stage?: 'git' | 'deploy' | null;
|
|
15
|
+
conflict?: PushConflict;
|
|
16
|
+
}): QuestionOption[];
|
|
5
17
|
export declare function createServer(client?: AgileMonsterClient): Server<{
|
|
6
18
|
method: string;
|
|
7
19
|
params?: {
|