agentxchain 2.155.10 → 2.155.12
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/bin/agentxchain.js +7 -0
- package/package.json +1 -1
- package/src/commands/run.js +1 -1
- package/src/commands/serve-mcp.js +40 -0
- package/src/lib/continuous-run.js +44 -0
- package/src/lib/dispatch-bundle.js +26 -2
- package/src/lib/export.js +85 -8
- package/src/lib/governed-state.js +12 -0
- package/src/lib/mcp-server.js +305 -0
- package/src/lib/normalized-config.js +12 -3
- package/src/lib/repo-observer.js +12 -1
- package/src/lib/schemas/agentxchain-config.schema.json +4 -0
- package/src/templates/governed/enterprise-app.json +1 -1
- package/src/templates/governed/full-local-cli.json +1 -1
- package/src/templates/governed/generic.json +1 -1
package/bin/agentxchain.js
CHANGED
|
@@ -130,6 +130,7 @@ import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, sche
|
|
|
130
130
|
import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
|
|
131
131
|
import { missionAttachChainCommand, missionBindCoordinatorCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
|
|
132
132
|
import { workflowKitDescribeCommand } from '../src/commands/workflow-kit.js';
|
|
133
|
+
import { serveMcpCommand } from '../src/commands/serve-mcp.js';
|
|
133
134
|
|
|
134
135
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
135
136
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -782,6 +783,12 @@ program
|
|
|
782
783
|
.option('--dry-run', 'Show configured gate actions without executing approval')
|
|
783
784
|
.action(approveCompletionCommand);
|
|
784
785
|
|
|
786
|
+
program
|
|
787
|
+
.command('serve-mcp')
|
|
788
|
+
.description('Start an MCP server exposing governance tools (stdio transport)')
|
|
789
|
+
.option('--root <path>', 'Project root directory (default: cwd)')
|
|
790
|
+
.action(serveMcpCommand);
|
|
791
|
+
|
|
785
792
|
program
|
|
786
793
|
.command('dashboard')
|
|
787
794
|
.description('Open the live governance dashboard for the current repo/workspace')
|
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -673,7 +673,7 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
673
673
|
const reportsDir = join(root, '.agentxchain', 'reports');
|
|
674
674
|
mkdirSync(reportsDir, { recursive: true });
|
|
675
675
|
|
|
676
|
-
const exportResult = buildRunExport(root);
|
|
676
|
+
const exportResult = buildRunExport(root, { maxJsonlEntries: 1000, maxBase64Bytes: 1024 * 1024 });
|
|
677
677
|
if (exportResult.ok) {
|
|
678
678
|
const runId = result.state.run_id || 'unknown';
|
|
679
679
|
const exportPath = join(reportsDir, `export-${runId}.json`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentxchain serve-mcp — start an MCP server exposing governance tools.
|
|
3
|
+
*
|
|
4
|
+
* Runs a stdio-based MCP server that lets any MCP-compatible client
|
|
5
|
+
* (Claude Code, Cursor, Windsurf, VS Code extensions, etc.) interact
|
|
6
|
+
* with a governed AgentXchain project.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* agentxchain serve-mcp [--root <project-root>]
|
|
10
|
+
*
|
|
11
|
+
* The server reads JSON-RPC from stdin and writes responses to stdout,
|
|
12
|
+
* per the MCP specification.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolve } from 'path';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { createAgentXchainMcpServer } from '../lib/mcp-server.js';
|
|
18
|
+
import { findProjectRoot } from '../lib/config.js';
|
|
19
|
+
|
|
20
|
+
export async function serveMcpCommand(opts) {
|
|
21
|
+
const rootHint = opts.root || process.cwd();
|
|
22
|
+
const root = findProjectRoot(resolve(rootHint));
|
|
23
|
+
|
|
24
|
+
if (!root) {
|
|
25
|
+
// Even without a governed project, start the server so MCP clients
|
|
26
|
+
// get descriptive tool errors instead of a connection-refused.
|
|
27
|
+
process.stderr.write(
|
|
28
|
+
`[agentxchain serve-mcp] Warning: no agentxchain.json found at ${rootHint}. ` +
|
|
29
|
+
`Tools will return errors until a governed project is available.\n`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const projectRoot = root || resolve(rootHint);
|
|
34
|
+
const server = createAgentXchainMcpServer(projectRoot);
|
|
35
|
+
const transport = new StdioServerTransport();
|
|
36
|
+
|
|
37
|
+
process.stderr.write(`[agentxchain serve-mcp] Starting MCP server for ${projectRoot}\n`);
|
|
38
|
+
|
|
39
|
+
await server.connect(transport);
|
|
40
|
+
}
|
|
@@ -157,6 +157,32 @@ function getAcceptedIdleExpansionEntries(execution) {
|
|
|
157
157
|
return entries.filter((entry) => entry?.turn_result?.idle_expansion_result);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
function readIdleExpansionPrompt(root, config) {
|
|
161
|
+
const configuredPath = config?.run_loop?.continuous?.idle_expansion?.pm_prompt_path
|
|
162
|
+
?? config?.continuous?.idle_expansion?.pm_prompt_path;
|
|
163
|
+
const promptPath = typeof configuredPath === 'string' && configuredPath.trim().length > 0
|
|
164
|
+
? configuredPath.trim()
|
|
165
|
+
: '.agentxchain/prompts/pm-idle-expansion.md';
|
|
166
|
+
|
|
167
|
+
const absPromptPath = join(root, promptPath);
|
|
168
|
+
if (!existsSync(absPromptPath)) {
|
|
169
|
+
return { promptPath, content: '' };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
return {
|
|
174
|
+
promptPath,
|
|
175
|
+
content: readFileSync(absPromptPath, 'utf8').trim(),
|
|
176
|
+
};
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return {
|
|
179
|
+
promptPath,
|
|
180
|
+
content: '',
|
|
181
|
+
warning: `Failed to load PM idle-expansion prompt "${promptPath}": ${err.message}`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
160
186
|
function ingestAcceptedIdleExpansionsFromExecution(context, session, execution, log = console.log) {
|
|
161
187
|
const entries = getAcceptedIdleExpansionEntries(execution);
|
|
162
188
|
if (entries.length === 0) {
|
|
@@ -750,6 +776,19 @@ async function dispatchIdleExpansion(context, session, contOpts, absVisionPath,
|
|
|
750
776
|
.map(e => ` - ${e.path} (${e.headings.length} headings, ${e.byte_count} bytes${e.warning ? `, warning: ${e.warning}` : ''})`)
|
|
751
777
|
.join('\n');
|
|
752
778
|
const visionHeadings = (session.vision_headings_snapshot || []).map(h => ` - ${h}`).join('\n');
|
|
779
|
+
const idleExpansionPrompt = readIdleExpansionPrompt(root, context.config);
|
|
780
|
+
const promptBlock = idleExpansionPrompt.content
|
|
781
|
+
? [
|
|
782
|
+
``,
|
|
783
|
+
`PM idle-expansion prompt from ${idleExpansionPrompt.promptPath}:`,
|
|
784
|
+
idleExpansionPrompt.content,
|
|
785
|
+
]
|
|
786
|
+
: idleExpansionPrompt.warning
|
|
787
|
+
? [
|
|
788
|
+
``,
|
|
789
|
+
`PM idle-expansion prompt warning: ${idleExpansionPrompt.warning}`,
|
|
790
|
+
]
|
|
791
|
+
: [];
|
|
753
792
|
|
|
754
793
|
const charter = [
|
|
755
794
|
`[idle-expansion #${currentIteration}] Inspect VISION.md, ROADMAP.md, SYSTEM_SPEC.md, and current project state.`,
|
|
@@ -786,6 +825,7 @@ async function dispatchIdleExpansion(context, session, contOpts, absVisionPath,
|
|
|
786
825
|
``,
|
|
787
826
|
`Source manifest:`,
|
|
788
827
|
sourceList || ' (no sources available)',
|
|
828
|
+
...promptBlock,
|
|
789
829
|
].join('\n');
|
|
790
830
|
|
|
791
831
|
// Use a placeholder accepted_turn_id for the signal — it will be the turn assigned by intake
|
|
@@ -1072,6 +1112,10 @@ export function resolveContinuousOptions(opts, config) {
|
|
|
1072
1112
|
maxExpansions: configIdleExpansion.max_expansions ?? 5,
|
|
1073
1113
|
role: configIdleExpansion.role ?? 'pm',
|
|
1074
1114
|
malformedRetryLimit: configIdleExpansion.malformed_retry_limit ?? 1,
|
|
1115
|
+
pmPromptPath: typeof configIdleExpansion.pm_prompt_path === 'string'
|
|
1116
|
+
&& configIdleExpansion.pm_prompt_path.trim().length > 0
|
|
1117
|
+
? configIdleExpansion.pm_prompt_path.trim()
|
|
1118
|
+
: '.agentxchain/prompts/pm-idle-expansion.md',
|
|
1075
1119
|
} : null;
|
|
1076
1120
|
|
|
1077
1121
|
return {
|
|
@@ -51,6 +51,14 @@ const RESERVED_PATHS = [
|
|
|
51
51
|
'.agentxchain/lock.json',
|
|
52
52
|
];
|
|
53
53
|
|
|
54
|
+
function phaseTransitionAutoApprovalApplies(config) {
|
|
55
|
+
return config?.approval_policy?.phase_transitions?.default === 'auto_approve';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function runCompletionAutoApprovalApplies(config) {
|
|
59
|
+
return config?.approval_policy?.run_completion?.action === 'auto_approve';
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
/**
|
|
55
63
|
* Write a dispatch bundle for the currently assigned turn.
|
|
56
64
|
*
|
|
@@ -261,6 +269,14 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
261
269
|
lines.push('- Your artifact type should be `workspace` or `commit`.');
|
|
262
270
|
lines.push('- You must accurately declare all files you changed.');
|
|
263
271
|
lines.push('');
|
|
272
|
+
if (phase === 'implementation') {
|
|
273
|
+
lines.push('### Implementation Phase: Code Production Required');
|
|
274
|
+
lines.push('');
|
|
275
|
+
lines.push('- Your PRIMARY deliverable in this phase is **working source code** — source files, test files, configurations, or executable scripts that change the project\'s runtime behavior.');
|
|
276
|
+
lines.push('- Planning documents (`.planning/*.md`, `IMPLEMENTATION_NOTES.md`) are supplementary evidence of what you built, not your primary output.');
|
|
277
|
+
lines.push('- A completed turn in the implementation phase MUST include actual product code changes in `files_changed`, not only documentation or planning artifacts.');
|
|
278
|
+
lines.push('');
|
|
279
|
+
}
|
|
264
280
|
} else if (role.write_authority === 'proposed') {
|
|
265
281
|
lines.push('### Write Authority: proposed');
|
|
266
282
|
lines.push('');
|
|
@@ -326,7 +342,10 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
326
342
|
if (gateConfig.requires_verification_pass) {
|
|
327
343
|
lines.push('- Requires verification pass');
|
|
328
344
|
}
|
|
329
|
-
if (gateConfig.requires_human_approval) {
|
|
345
|
+
if (gateConfig.requires_human_approval && phaseTransitionAutoApprovalApplies(config)) {
|
|
346
|
+
lines.push('- Requires approval, but `approval_policy.phase_transitions.default` is `auto_approve` for this run.');
|
|
347
|
+
lines.push('- Do NOT set `status: "needs_human"` solely to request phase-gate approval. If the required artifacts are complete, set the appropriate `phase_transition_request`; the orchestrator will evaluate and auto-approve the gate.');
|
|
348
|
+
} else if (gateConfig.requires_human_approval) {
|
|
330
349
|
lines.push('- Requires human approval');
|
|
331
350
|
}
|
|
332
351
|
lines.push('');
|
|
@@ -504,7 +523,12 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
504
523
|
const isTerminal = currentPhase && phaseNames.indexOf(currentPhase) === phaseNames.length - 1;
|
|
505
524
|
if (isTerminal) {
|
|
506
525
|
lines.push(`- **You are in the \`${currentPhase}\` phase (final phase).**`);
|
|
507
|
-
|
|
526
|
+
if (runCompletionAutoApprovalApplies(config)) {
|
|
527
|
+
lines.push('- **If your review verdict is ship-ready (no blocking issues):** set `run_completion_request: true` and `status: "completed"`. This triggers orchestrator run-completion evaluation and auto-approval under `approval_policy.run_completion.action: "auto_approve"`.');
|
|
528
|
+
lines.push('- Do NOT use `status: "needs_human"` solely to request final approval when the approval policy is auto-approve. Use `needs_human` only for genuine blockers.');
|
|
529
|
+
} else {
|
|
530
|
+
lines.push('- **If your review verdict is ship-ready (no blocking issues):** set `run_completion_request: true` and `status: "completed"`. This triggers the human approval gate — it does NOT bypass human review.');
|
|
531
|
+
}
|
|
508
532
|
lines.push('- **If you found genuine blocking issues that prevent shipping:** set `status: "needs_human"` and explain the blockers in `needs_human_reason`.');
|
|
509
533
|
lines.push('- Do NOT use `status: "needs_human"` to mean "human should approve the release." That is what `run_completion_request: true` is for.');
|
|
510
534
|
lines.push('- Do NOT set `phase_transition_request` to the exit gate name.');
|
package/src/lib/export.js
CHANGED
|
@@ -107,15 +107,64 @@ function parseJsonl(relPath, raw) {
|
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function
|
|
110
|
+
function parseJsonlBuffer(relPath, buffer, maxEntries) {
|
|
111
|
+
const shouldWindow = Number.isInteger(maxEntries) && maxEntries > 0;
|
|
112
|
+
const retained = [];
|
|
113
|
+
let totalEntries = 0;
|
|
114
|
+
let physicalLine = 1;
|
|
115
|
+
let lineStart = 0;
|
|
116
|
+
|
|
117
|
+
const retainLine = (line, lineNumber) => {
|
|
118
|
+
if (!line.trim()) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
totalEntries += 1;
|
|
122
|
+
if (shouldWindow && retained.length >= maxEntries) {
|
|
123
|
+
retained.shift();
|
|
124
|
+
}
|
|
125
|
+
retained.push({ line, lineNumber });
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i <= buffer.length; i += 1) {
|
|
129
|
+
if (i !== buffer.length && buffer[i] !== 10) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let lineEnd = i;
|
|
134
|
+
if (lineEnd > lineStart && buffer[lineEnd - 1] === 13) {
|
|
135
|
+
lineEnd -= 1;
|
|
136
|
+
}
|
|
137
|
+
retainLine(buffer.toString('utf8', lineStart, lineEnd), physicalLine);
|
|
138
|
+
lineStart = i + 1;
|
|
139
|
+
physicalLine += 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const entries = retained.map(({ line, lineNumber }) => {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(line);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(`${relPath}: invalid JSONL at line ${lineNumber}: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
entries,
|
|
152
|
+
totalEntries,
|
|
153
|
+
truncated: shouldWindow && totalEntries > maxEntries,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseFile(root, relPath, opts = {}) {
|
|
111
158
|
const absPath = join(root, relPath);
|
|
112
159
|
const buffer = readFileSync(absPath);
|
|
113
|
-
const raw = buffer.toString('utf8');
|
|
114
160
|
|
|
115
161
|
let format = 'text';
|
|
116
|
-
let data =
|
|
162
|
+
let data = null;
|
|
163
|
+
let truncated = false;
|
|
164
|
+
let totalEntries = null;
|
|
117
165
|
|
|
118
166
|
if (relPath.endsWith('.json')) {
|
|
167
|
+
const raw = buffer.toString('utf8');
|
|
119
168
|
try {
|
|
120
169
|
data = JSON.parse(raw);
|
|
121
170
|
format = 'json';
|
|
@@ -123,17 +172,37 @@ function parseFile(root, relPath) {
|
|
|
123
172
|
throw new Error(`${relPath}: invalid JSON: ${error.message}`);
|
|
124
173
|
}
|
|
125
174
|
} else if (relPath.endsWith('.jsonl')) {
|
|
126
|
-
|
|
175
|
+
const maxEntries = opts.maxJsonlEntries;
|
|
176
|
+
const parsed = Number.isInteger(maxEntries) && maxEntries > 0
|
|
177
|
+
? parseJsonlBuffer(relPath, buffer, maxEntries)
|
|
178
|
+
: { entries: parseJsonl(relPath, buffer.toString('utf8')), totalEntries: null, truncated: false };
|
|
127
179
|
format = 'jsonl';
|
|
180
|
+
data = parsed.entries;
|
|
181
|
+
truncated = parsed.truncated;
|
|
182
|
+
totalEntries = parsed.totalEntries;
|
|
183
|
+
} else {
|
|
184
|
+
data = buffer.toString('utf8');
|
|
128
185
|
}
|
|
129
186
|
|
|
130
|
-
|
|
187
|
+
const skipBase64 = truncated || (opts.maxBase64Bytes && buffer.byteLength > opts.maxBase64Bytes);
|
|
188
|
+
const result = {
|
|
131
189
|
format,
|
|
132
190
|
bytes: buffer.byteLength,
|
|
133
191
|
sha256: sha256(buffer),
|
|
134
|
-
content_base64: buffer.toString('base64'),
|
|
192
|
+
content_base64: skipBase64 ? null : buffer.toString('base64'),
|
|
135
193
|
data,
|
|
136
194
|
};
|
|
195
|
+
|
|
196
|
+
if (truncated) {
|
|
197
|
+
result.truncated = true;
|
|
198
|
+
result.total_entries = totalEntries;
|
|
199
|
+
result.retained_entries = data.length;
|
|
200
|
+
}
|
|
201
|
+
if (skipBase64 && !truncated) {
|
|
202
|
+
result.content_base64_skipped = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return result;
|
|
137
206
|
}
|
|
138
207
|
|
|
139
208
|
function countJsonl(files, relPath) {
|
|
@@ -366,7 +435,7 @@ export function buildRunWorkspaceMetadata(root) {
|
|
|
366
435
|
};
|
|
367
436
|
}
|
|
368
437
|
|
|
369
|
-
export function buildRunExport(startDir = process.cwd()) {
|
|
438
|
+
export function buildRunExport(startDir = process.cwd(), exportOpts = {}) {
|
|
370
439
|
const context = loadProjectContext(startDir);
|
|
371
440
|
if (!context) {
|
|
372
441
|
return {
|
|
@@ -388,9 +457,17 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
388
457
|
const collectedPaths = [...new Set(RUN_EXPORT_INCLUDED_ROOTS.flatMap((relPath) => collectPaths(root, relPath)))]
|
|
389
458
|
.sort((a, b) => a.localeCompare(b, 'en'));
|
|
390
459
|
|
|
460
|
+
const parseOpts = {};
|
|
461
|
+
if (exportOpts.maxJsonlEntries) {
|
|
462
|
+
parseOpts.maxJsonlEntries = exportOpts.maxJsonlEntries;
|
|
463
|
+
}
|
|
464
|
+
if (exportOpts.maxBase64Bytes) {
|
|
465
|
+
parseOpts.maxBase64Bytes = exportOpts.maxBase64Bytes;
|
|
466
|
+
}
|
|
467
|
+
|
|
391
468
|
const files = {};
|
|
392
469
|
for (const relPath of collectedPaths) {
|
|
393
|
-
files[relPath] = parseFile(root, relPath);
|
|
470
|
+
files[relPath] = parseFile(root, relPath, parseOpts);
|
|
394
471
|
}
|
|
395
472
|
|
|
396
473
|
const activeTurns = Object.keys(state?.active_turns || {}).sort((a, b) => a.localeCompare(b, 'en'));
|
|
@@ -1385,6 +1385,17 @@ function classifyAcceptanceOverlap(targetTurn, conflictFiles, historyEntries, co
|
|
|
1385
1385
|
const forwardRevisionTurns = new Map();
|
|
1386
1386
|
|
|
1387
1387
|
for (const entry of historyEntries) {
|
|
1388
|
+
// BUG-66: skip reissued turns — they are superseded, not competing.
|
|
1389
|
+
// A reissued turn's archived observation must not trigger overlap against
|
|
1390
|
+
// its replacement turn (or any subsequent turn).
|
|
1391
|
+
if (entry.status === 'reissued') {
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
// Also skip if this turn is the direct replacement for the history entry
|
|
1395
|
+
if (targetTurn?.reissued_from && targetTurn.reissued_from === entry.turn_id) {
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1388
1399
|
if (targetTurn?.run_id && entry.run_id && entry.run_id !== targetTurn.run_id) {
|
|
1389
1400
|
continue;
|
|
1390
1401
|
}
|
|
@@ -6818,4 +6829,5 @@ export {
|
|
|
6818
6829
|
LEDGER_PATH,
|
|
6819
6830
|
STAGING_PATH,
|
|
6820
6831
|
TALK_PATH,
|
|
6832
|
+
classifyAcceptanceOverlap as _classifyAcceptanceOverlap,
|
|
6821
6833
|
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentXchain MCP Server — exposes governance operations as MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* This module creates an MCP server that lets any MCP-compatible client
|
|
5
|
+
* (Claude Code, Cursor, Windsurf, VS Code extensions, etc.) natively
|
|
6
|
+
* query run status, approve gates, read events/history, and record
|
|
7
|
+
* intake events against a governed AgentXchain project.
|
|
8
|
+
*
|
|
9
|
+
* The existing mcp-adapter.js is the inverse: AgentXchain as an MCP client
|
|
10
|
+
* dispatching turns TO an MCP server. This module makes AgentXchain itself
|
|
11
|
+
* the server.
|
|
12
|
+
*
|
|
13
|
+
* Spec: .planning/SPEC-MCP-SERVER.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync } from 'fs';
|
|
17
|
+
import { join } from 'path';
|
|
18
|
+
import * as z from 'zod/v4';
|
|
19
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
20
|
+
import { findProjectRoot, loadProjectContext, loadProjectState } from './config.js';
|
|
21
|
+
import { readRunEvents } from './run-events.js';
|
|
22
|
+
import { queryRunHistory } from './run-history.js';
|
|
23
|
+
import { getActiveTurns } from './governed-state.js';
|
|
24
|
+
import { getOpenHumanEscalation, findCurrentHumanEscalation } from './human-escalations.js';
|
|
25
|
+
import { readContinuousSession } from './continuous-run.js';
|
|
26
|
+
|
|
27
|
+
const SERVER_NAME = 'agentxchain';
|
|
28
|
+
const SERVER_VERSION = '1.0.0';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create and configure the AgentXchain MCP server.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} root - Absolute path to the project root.
|
|
34
|
+
* @returns {McpServer} Configured MCP server (not yet connected to a transport).
|
|
35
|
+
*/
|
|
36
|
+
export function createAgentXchainMcpServer(root) {
|
|
37
|
+
const server = new McpServer(
|
|
38
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
39
|
+
{
|
|
40
|
+
capabilities: {
|
|
41
|
+
tools: {},
|
|
42
|
+
resources: {},
|
|
43
|
+
},
|
|
44
|
+
instructions:
|
|
45
|
+
'AgentXchain governance server. Use tools to query governed run status, ' +
|
|
46
|
+
'read events and history, approve human gates, and record intake events.',
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
registerTools(server, root);
|
|
51
|
+
registerResources(server, root);
|
|
52
|
+
|
|
53
|
+
return server;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Tools ──────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function registerTools(server, root) {
|
|
59
|
+
server.tool(
|
|
60
|
+
'agentxchain_status',
|
|
61
|
+
'Read current governed project and run status',
|
|
62
|
+
{},
|
|
63
|
+
() => handleStatus(root),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.tool(
|
|
67
|
+
'agentxchain_events',
|
|
68
|
+
'Read recent events from the governed project event log',
|
|
69
|
+
{ limit: z.number().optional().describe('Max events to return (default 50)') },
|
|
70
|
+
(args) => handleEvents(root, args),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
'agentxchain_history',
|
|
75
|
+
'Read run history entries',
|
|
76
|
+
{ limit: z.number().optional().describe('Max history entries to return (default 20)') },
|
|
77
|
+
(args) => handleHistory(root, args),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.tool(
|
|
81
|
+
'agentxchain_approve_gate',
|
|
82
|
+
'Approve a pending human gate/escalation to unblock a governed run',
|
|
83
|
+
{
|
|
84
|
+
gate_id: z.string().describe('The escalation ID to approve (e.g. hesc_abc123)'),
|
|
85
|
+
reason: z.string().optional().describe('Optional reason for approval'),
|
|
86
|
+
},
|
|
87
|
+
(args) => handleApproveGate(root, args),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
server.tool(
|
|
91
|
+
'agentxchain_intake_record',
|
|
92
|
+
'Record a new intake event for the governed project',
|
|
93
|
+
{
|
|
94
|
+
source: z.string().describe('Event source (e.g. manual, ci_failure, schedule)'),
|
|
95
|
+
title: z.string().describe('Short title for the intake event'),
|
|
96
|
+
description: z.string().optional().describe('Detailed description'),
|
|
97
|
+
priority: z.string().optional().describe('Priority level (e.g. high, medium, low)'),
|
|
98
|
+
},
|
|
99
|
+
(args) => handleIntakeRecord(root, args),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Resources ──────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function registerResources(server, root) {
|
|
106
|
+
server.resource(
|
|
107
|
+
'governed-state',
|
|
108
|
+
'agentxchain://state',
|
|
109
|
+
{ description: 'Current .agentxchain/state.json contents', mimeType: 'application/json' },
|
|
110
|
+
() => handleReadState(root),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
server.resource(
|
|
114
|
+
'continuous-session',
|
|
115
|
+
'agentxchain://session',
|
|
116
|
+
{ description: 'Current .agentxchain/continuous-session.json contents', mimeType: 'application/json' },
|
|
117
|
+
() => handleReadSession(root),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Tool handlers ──────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function handleStatus(root) {
|
|
124
|
+
const context = loadProjectContext(root);
|
|
125
|
+
if (!context) {
|
|
126
|
+
return toolResult({ ok: false, error: `No governed project found at ${root}` });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { config } = context;
|
|
130
|
+
const state = loadProjectState(root, config);
|
|
131
|
+
|
|
132
|
+
const result = {
|
|
133
|
+
ok: true,
|
|
134
|
+
project: config.project || null,
|
|
135
|
+
protocol_mode: config.protocol_mode || null,
|
|
136
|
+
run_id: state?.run_id || null,
|
|
137
|
+
phase: state?.phase || null,
|
|
138
|
+
status: state?.status || null,
|
|
139
|
+
active_turns: state ? getActiveTurnsSummary(state) : [],
|
|
140
|
+
pending_gates: state?.blocked_reason?.type || null,
|
|
141
|
+
blocked: state?.status === 'blocked',
|
|
142
|
+
blocked_on: state?.blocked_on || null,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const session = readContinuousSession(root);
|
|
146
|
+
if (session) {
|
|
147
|
+
result.continuous = {
|
|
148
|
+
session_id: session.session_id || null,
|
|
149
|
+
status: session.status || null,
|
|
150
|
+
runs_completed: session.runs_completed || 0,
|
|
151
|
+
idle_cycles: session.idle_cycles || 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return toolResult(result);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleEvents(root, args) {
|
|
159
|
+
const context = loadProjectContext(root);
|
|
160
|
+
if (!context) {
|
|
161
|
+
return toolResult({ ok: false, error: `No governed project found at ${root}` });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const limit = typeof args.limit === 'number' && args.limit > 0 ? args.limit : 50;
|
|
165
|
+
const events = readRunEvents(root, { limit });
|
|
166
|
+
|
|
167
|
+
return toolResult({ ok: true, count: events.length, events });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleHistory(root, args) {
|
|
171
|
+
const context = loadProjectContext(root);
|
|
172
|
+
if (!context) {
|
|
173
|
+
return toolResult({ ok: false, error: `No governed project found at ${root}` });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const limit = typeof args.limit === 'number' && args.limit > 0 ? args.limit : 20;
|
|
177
|
+
const entries = queryRunHistory(root, { limit });
|
|
178
|
+
|
|
179
|
+
return toolResult({ ok: true, count: entries.length, entries });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleApproveGate(root, args) {
|
|
183
|
+
const context = loadProjectContext(root);
|
|
184
|
+
if (!context) {
|
|
185
|
+
return toolResult({ ok: false, error: `No governed project found at ${root}` });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const { config } = context;
|
|
189
|
+
const state = loadProjectState(root, config);
|
|
190
|
+
if (!state) {
|
|
191
|
+
return toolResult({ ok: false, error: 'No governed state found.' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const gateId = typeof args.gate_id === 'string' ? args.gate_id.trim() : '';
|
|
195
|
+
if (!gateId) {
|
|
196
|
+
return toolResult({ ok: false, error: 'gate_id is required.' });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (state.status !== 'blocked') {
|
|
200
|
+
return toolResult({
|
|
201
|
+
ok: false,
|
|
202
|
+
error: `Run is not blocked (status: "${state.status}"). Nothing to approve.`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const escalation = getOpenHumanEscalation(root, gateId);
|
|
207
|
+
if (!escalation) {
|
|
208
|
+
return toolResult({ ok: false, error: `No open escalation found for ${gateId}.` });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const current = findCurrentHumanEscalation(root, state);
|
|
212
|
+
if (!current || current.escalation_id !== escalation.escalation_id) {
|
|
213
|
+
return toolResult({
|
|
214
|
+
ok: false,
|
|
215
|
+
error: `${gateId} is not the current blocker for this run.`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// We return the gate info rather than performing the unblock directly,
|
|
220
|
+
// because unblock has side-effects (resume) that should be explicit.
|
|
221
|
+
// The caller should use the CLI `agentxchain unblock <id>` for the actual mutation.
|
|
222
|
+
return toolResult({
|
|
223
|
+
ok: true,
|
|
224
|
+
gate_id: gateId,
|
|
225
|
+
type: escalation.type,
|
|
226
|
+
detail: escalation.detail || null,
|
|
227
|
+
instruction: `Gate ${gateId} is the current blocker. Run "agentxchain unblock ${gateId}" to approve and resume.`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function handleIntakeRecord(root, args) {
|
|
232
|
+
const context = loadProjectContext(root);
|
|
233
|
+
if (!context) {
|
|
234
|
+
return toolResult({ ok: false, error: `No governed project found at ${root}` });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const source = typeof args.source === 'string' ? args.source.trim() : '';
|
|
238
|
+
const title = typeof args.title === 'string' ? args.title.trim() : '';
|
|
239
|
+
|
|
240
|
+
if (!source || !title) {
|
|
241
|
+
return toolResult({ ok: false, error: 'Both source and title are required.' });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Similar to approve_gate: we expose the information needed to record,
|
|
245
|
+
// but defer the actual mutation to the intake pipeline.
|
|
246
|
+
// Direct mutation via MCP would bypass the CLI's validation and event emission.
|
|
247
|
+
return toolResult({
|
|
248
|
+
ok: true,
|
|
249
|
+
instruction: `Use "agentxchain intake record --source ${source} --title '${title}'${args.description ? ` --description '${args.description}'` : ''}${args.priority ? ` --priority ${args.priority}` : ''}" to record this event.`,
|
|
250
|
+
source,
|
|
251
|
+
title,
|
|
252
|
+
description: args.description || null,
|
|
253
|
+
priority: args.priority || null,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Resource handlers ──────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
function handleReadState(root) {
|
|
260
|
+
const statePath = join(root, '.agentxchain', 'state.json');
|
|
261
|
+
if (!existsSync(statePath)) {
|
|
262
|
+
return { contents: [{ uri: 'agentxchain://state', text: '{}', mimeType: 'application/json' }] };
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const text = readFileSync(statePath, 'utf8');
|
|
266
|
+
return { contents: [{ uri: 'agentxchain://state', text, mimeType: 'application/json' }] };
|
|
267
|
+
} catch {
|
|
268
|
+
return { contents: [{ uri: 'agentxchain://state', text: '{}', mimeType: 'application/json' }] };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function handleReadSession(root) {
|
|
273
|
+
const sessionPath = join(root, '.agentxchain', 'continuous-session.json');
|
|
274
|
+
if (!existsSync(sessionPath)) {
|
|
275
|
+
return { contents: [{ uri: 'agentxchain://session', text: '{}', mimeType: 'application/json' }] };
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const text = readFileSync(sessionPath, 'utf8');
|
|
279
|
+
return { contents: [{ uri: 'agentxchain://session', text, mimeType: 'application/json' }] };
|
|
280
|
+
} catch {
|
|
281
|
+
return { contents: [{ uri: 'agentxchain://session', text: '{}', mimeType: 'application/json' }] };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
function getActiveTurnsSummary(state) {
|
|
288
|
+
try {
|
|
289
|
+
const turns = getActiveTurns(state);
|
|
290
|
+
return turns.map((t) => ({
|
|
291
|
+
turn_id: t.turn_id,
|
|
292
|
+
role: t.assigned_role,
|
|
293
|
+
status: t.status,
|
|
294
|
+
phase: t.phase,
|
|
295
|
+
}));
|
|
296
|
+
} catch {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function toolResult(data) {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -694,6 +694,11 @@ function validateRunLoopContinuousConfig(path, continuous, errors) {
|
|
|
694
694
|
errors.push(`${path}.idle_expansion.malformed_retry_limit must be a non-negative integer`);
|
|
695
695
|
}
|
|
696
696
|
}
|
|
697
|
+
if (continuous.idle_expansion.pm_prompt_path !== undefined
|
|
698
|
+
&& continuous.idle_expansion.pm_prompt_path !== null
|
|
699
|
+
&& typeof continuous.idle_expansion.pm_prompt_path !== 'string') {
|
|
700
|
+
errors.push(`${path}.idle_expansion.pm_prompt_path must be a string`);
|
|
701
|
+
}
|
|
697
702
|
}
|
|
698
703
|
}
|
|
699
704
|
}
|
|
@@ -1365,9 +1370,10 @@ function normalizeIdleExpansion(raw) {
|
|
|
1365
1370
|
sources: ['.planning/VISION.md', '.planning/ROADMAP.md', '.planning/SYSTEM_SPEC.md'],
|
|
1366
1371
|
max_expansions: 5,
|
|
1367
1372
|
role: 'pm',
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1373
|
+
output: 'intake_intent_or_vision_exhausted',
|
|
1374
|
+
malformed_retry_limit: 1,
|
|
1375
|
+
pm_prompt_path: '.agentxchain/prompts/pm-idle-expansion.md',
|
|
1376
|
+
};
|
|
1371
1377
|
}
|
|
1372
1378
|
const sources = Array.isArray(raw.sources) && raw.sources.length > 0
|
|
1373
1379
|
? raw.sources.filter(s => typeof s === 'string' && s.length > 0)
|
|
@@ -1379,6 +1385,9 @@ function normalizeIdleExpansion(raw) {
|
|
|
1379
1385
|
output: 'intake_intent_or_vision_exhausted',
|
|
1380
1386
|
malformed_retry_limit: Number.isInteger(raw.malformed_retry_limit) && raw.malformed_retry_limit >= 0
|
|
1381
1387
|
? raw.malformed_retry_limit : 1,
|
|
1388
|
+
pm_prompt_path: typeof raw.pm_prompt_path === 'string' && raw.pm_prompt_path.trim().length > 0
|
|
1389
|
+
? raw.pm_prompt_path.trim()
|
|
1390
|
+
: '.agentxchain/prompts/pm-idle-expansion.md',
|
|
1382
1391
|
};
|
|
1383
1392
|
}
|
|
1384
1393
|
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -69,6 +69,12 @@ export const BASELINE_EXEMPT_PATH_PREFIXES = Object.freeze([
|
|
|
69
69
|
'.agentxchain/proposed/',
|
|
70
70
|
]);
|
|
71
71
|
|
|
72
|
+
const GENERATED_REPORT_PATH_PATTERNS = Object.freeze([
|
|
73
|
+
/^\.agentxchain\/reports\/report-[^/]+\.md$/,
|
|
74
|
+
/^\.agentxchain\/reports\/export-[^/]+\.json$/,
|
|
75
|
+
/^\.agentxchain\/reports\/chain-[^/]+\.json$/,
|
|
76
|
+
]);
|
|
77
|
+
|
|
72
78
|
// Continuity export/restore must stay aligned with orchestrator ownership,
|
|
73
79
|
// but only for the subset that represents governed run state.
|
|
74
80
|
export const RUN_CONTINUITY_STATE_FILES = Object.freeze([
|
|
@@ -93,6 +99,10 @@ function pathMatchesAnyRoot(filePath, roots) {
|
|
|
93
99
|
return roots.some((root) => filePath === root || filePath.startsWith(`${root}/`));
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
function isGeneratedReportPath(filePath) {
|
|
103
|
+
return GENERATED_REPORT_PATH_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
104
|
+
}
|
|
105
|
+
|
|
96
106
|
/**
|
|
97
107
|
* Return the repo-owned vs framework-owned classification flags for a path.
|
|
98
108
|
*
|
|
@@ -106,7 +116,8 @@ function pathMatchesAnyRoot(filePath, roots) {
|
|
|
106
116
|
*/
|
|
107
117
|
export function classifyRepoPath(filePath) {
|
|
108
118
|
const operational = pathMatchesAnyPrefix(filePath, OPERATIONAL_PATH_PREFIXES)
|
|
109
|
-
|| ORCHESTRATOR_STATE_FILES.includes(filePath)
|
|
119
|
+
|| ORCHESTRATOR_STATE_FILES.includes(filePath)
|
|
120
|
+
|| isGeneratedReportPath(filePath);
|
|
110
121
|
const continuityState = RUN_CONTINUITY_STATE_FILES.includes(filePath)
|
|
111
122
|
|| pathMatchesAnyRoot(filePath, RUN_CONTINUITY_DIRECTORY_ROOTS);
|
|
112
123
|
const baselineExempt = operational
|
|
@@ -136,6 +136,10 @@
|
|
|
136
136
|
"type": "integer",
|
|
137
137
|
"minimum": 0,
|
|
138
138
|
"description": "Reserved retry budget for malformed idle_expansion_result outputs. Default 1."
|
|
139
|
+
},
|
|
140
|
+
"pm_prompt_path": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "Project-relative prompt supplement loaded into PM idle-expansion charters. Default .agentxchain/prompts/pm-idle-expansion.md."
|
|
139
143
|
}
|
|
140
144
|
},
|
|
141
145
|
"additionalProperties": true
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dev": {
|
|
55
55
|
"title": "Developer",
|
|
56
|
-
"mandate": "
|
|
56
|
+
"mandate": "Write source code that implements approved work, then verify it passes tests.",
|
|
57
57
|
"write_authority": "authoritative",
|
|
58
58
|
"runtime": "local-dev"
|
|
59
59
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dev": {
|
|
26
26
|
"title": "Developer",
|
|
27
|
-
"mandate": "
|
|
27
|
+
"mandate": "Write source code that implements approved work, then verify it passes tests.",
|
|
28
28
|
"write_authority": "authoritative",
|
|
29
29
|
"runtime": "local-dev"
|
|
30
30
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dev": {
|
|
16
16
|
"title": "Developer",
|
|
17
|
-
"mandate": "
|
|
17
|
+
"mandate": "Write source code that implements approved work, then verify it passes tests.",
|
|
18
18
|
"write_authority": "authoritative",
|
|
19
19
|
"runtime": "manual-dev"
|
|
20
20
|
},
|