@xn-intenton-z2a/agentic-lib 7.1.57 → 7.1.59
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.
|
@@ -198,23 +198,8 @@ jobs:
|
|
|
198
198
|
core.warning(`Could not merge PR #${pr.number}: ${e.message}`);
|
|
199
199
|
}
|
|
200
200
|
} else if (fullPr.mergeable_state === 'dirty' || fullPr.mergeable === false) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// Close stale conflicting PR
|
|
204
|
-
core.info(`Closing stale conflicting PR #${pr.number}`);
|
|
205
|
-
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' });
|
|
206
|
-
try {
|
|
207
|
-
await github.rest.git.deleteRef({ owner, repo, ref: `heads/${pr.head.ref}` });
|
|
208
|
-
} catch (e) { /* branch may already be gone */ }
|
|
209
|
-
closedPRs.push(pr.number);
|
|
210
|
-
} else {
|
|
211
|
-
// Remove automerge label from conflicting but fresh PR
|
|
212
|
-
try {
|
|
213
|
-
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: 'automerge' });
|
|
214
|
-
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number,
|
|
215
|
-
body: 'Automerge label removed — PR has conflicts. Resolve and re-add label to retry.' });
|
|
216
|
-
} catch (e) { /* label may not exist */ }
|
|
217
|
-
}
|
|
201
|
+
// Conflicting PR — leave it for fix-stuck to resolve instead of deleting
|
|
202
|
+
core.info(`PR #${pr.number} has merge conflicts — fix-stuck job will attempt resolution`);
|
|
218
203
|
}
|
|
219
204
|
// If unstable/unknown: skip, next run handles
|
|
220
205
|
}
|
|
@@ -450,7 +435,7 @@ jobs:
|
|
|
450
435
|
return;
|
|
451
436
|
}
|
|
452
437
|
|
|
453
|
-
// Find PRs with failing checks on agentic branches
|
|
438
|
+
// Find PRs with failing checks or merge conflicts on agentic branches
|
|
454
439
|
const { data: openPRs } = await github.rest.pulls.list({
|
|
455
440
|
owner, repo, state: 'open', per_page: 10,
|
|
456
441
|
});
|
|
@@ -460,12 +445,19 @@ jobs:
|
|
|
460
445
|
if (!hasAutomerge) continue;
|
|
461
446
|
if (!pr.head.ref.startsWith('agentic-lib-') && !pr.head.ref.startsWith('copilot/')) continue;
|
|
462
447
|
|
|
448
|
+
// Check for merge conflicts
|
|
449
|
+
const { data: fullPr } = await github.rest.pulls.get({
|
|
450
|
+
owner, repo, pull_number: pr.number,
|
|
451
|
+
});
|
|
452
|
+
const hasConflicts = fullPr.mergeable_state === 'dirty' || fullPr.mergeable === false;
|
|
453
|
+
|
|
463
454
|
// Check if checks are failing
|
|
464
455
|
const { data: checkRuns } = await github.rest.checks.listForRef({
|
|
465
456
|
owner, repo, ref: pr.head.sha,
|
|
466
457
|
});
|
|
467
458
|
const hasFailing = checkRuns.check_runs.some(c => c.conclusion === 'failure');
|
|
468
|
-
|
|
459
|
+
|
|
460
|
+
if (!hasFailing && !hasConflicts) continue;
|
|
469
461
|
|
|
470
462
|
// Check fix attempt count
|
|
471
463
|
const { data: fixRuns } = await github.rest.actions.listWorkflowRuns({
|
|
@@ -478,24 +470,60 @@ jobs:
|
|
|
478
470
|
continue;
|
|
479
471
|
}
|
|
480
472
|
|
|
481
|
-
|
|
473
|
+
const reason = hasConflicts ? 'merge conflicts' : 'failing checks';
|
|
474
|
+
core.info(`Will attempt to fix PR #${pr.number} (${reason})`);
|
|
482
475
|
core.exportVariable('FIX_PR_NUMBER', String(pr.number));
|
|
476
|
+
core.exportVariable('FIX_REASON', reason);
|
|
483
477
|
break;
|
|
484
478
|
}
|
|
485
479
|
|
|
486
|
-
- name: Checkout PR branch
|
|
480
|
+
- name: Checkout PR branch
|
|
487
481
|
if: env.FIX_PR_NUMBER != ''
|
|
488
482
|
run: |
|
|
489
483
|
gh pr checkout ${{ env.FIX_PR_NUMBER }}
|
|
490
484
|
env:
|
|
491
485
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
492
486
|
|
|
493
|
-
- name:
|
|
494
|
-
if: env.FIX_PR_NUMBER != ''
|
|
487
|
+
- name: "Tier 1: Auto-resolve trivial merge conflicts"
|
|
488
|
+
if: env.FIX_PR_NUMBER != '' && env.FIX_REASON == 'merge conflicts'
|
|
489
|
+
id: trivial-resolve
|
|
490
|
+
run: |
|
|
491
|
+
git fetch origin main
|
|
492
|
+
if git merge origin/main --no-edit 2>/dev/null; then
|
|
493
|
+
echo "resolved=clean" >> $GITHUB_OUTPUT
|
|
494
|
+
else
|
|
495
|
+
CONFLICTED=$(git diff --name-only --diff-filter=U)
|
|
496
|
+
TRIVIAL_PATTERN='intentïon\.md|intention\.md|package-lock\.json'
|
|
497
|
+
NON_TRIVIAL=$(echo "$CONFLICTED" | grep -vE "$TRIVIAL_PATTERN" || true)
|
|
498
|
+
if [ -z "$NON_TRIVIAL" ]; then
|
|
499
|
+
echo "All conflicts are trivial — auto-resolving"
|
|
500
|
+
for f in $CONFLICTED; do
|
|
501
|
+
git checkout --theirs "$f"
|
|
502
|
+
git add "$f"
|
|
503
|
+
done
|
|
504
|
+
npm install 2>/dev/null || true
|
|
505
|
+
git add package-lock.json 2>/dev/null || true
|
|
506
|
+
git commit --no-edit
|
|
507
|
+
echo "resolved=trivial" >> $GITHUB_OUTPUT
|
|
508
|
+
else
|
|
509
|
+
git merge --abort
|
|
510
|
+
echo "resolved=none" >> $GITHUB_OUTPUT
|
|
511
|
+
echo "non_trivial<<EOF" >> $GITHUB_OUTPUT
|
|
512
|
+
echo "$NON_TRIVIAL" >> $GITHUB_OUTPUT
|
|
513
|
+
echo "EOF" >> $GITHUB_OUTPUT
|
|
514
|
+
fi
|
|
515
|
+
fi
|
|
516
|
+
|
|
517
|
+
- name: "Tier 2: LLM fix (conflicts or failing checks)"
|
|
518
|
+
if: |
|
|
519
|
+
env.FIX_PR_NUMBER != '' &&
|
|
520
|
+
(env.FIX_REASON != 'merge conflicts' ||
|
|
521
|
+
steps.trivial-resolve.outputs.resolved == 'none')
|
|
495
522
|
uses: ./.github/agentic-lib/actions/agentic-step
|
|
496
523
|
env:
|
|
497
524
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
498
525
|
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
|
526
|
+
NON_TRIVIAL_FILES: ${{ steps.trivial-resolve.outputs.non_trivial }}
|
|
499
527
|
with:
|
|
500
528
|
task: "fix-code"
|
|
501
529
|
config: ${{ needs.params.outputs.config-path }}
|
|
@@ -508,7 +536,7 @@ jobs:
|
|
|
508
536
|
if: env.FIX_PR_NUMBER != ''
|
|
509
537
|
uses: ./.github/agentic-lib/actions/commit-if-changed
|
|
510
538
|
with:
|
|
511
|
-
commit-message: "agentic-step: fix failing tests"
|
|
539
|
+
commit-message: "agentic-step: fix failing tests / resolve conflicts"
|
|
512
540
|
|
|
513
541
|
# ─── Review: close resolved issues, enhance with criteria ──────────
|
|
514
542
|
review-features:
|
package/bin/agentic-lib.js
CHANGED
|
@@ -31,7 +31,7 @@ const flags = args.slice(1);
|
|
|
31
31
|
let initChanges = 0;
|
|
32
32
|
const TASK_COMMANDS = ["transform", "maintain-features", "maintain-library", "fix-code"];
|
|
33
33
|
const INIT_COMMANDS = ["init", "update", "reset"];
|
|
34
|
-
const ALL_COMMANDS = [...INIT_COMMANDS, ...TASK_COMMANDS, "version"];
|
|
34
|
+
const ALL_COMMANDS = [...INIT_COMMANDS, ...TASK_COMMANDS, "version", "mcp"];
|
|
35
35
|
|
|
36
36
|
const HELP = `
|
|
37
37
|
@xn-intenton-z2a/agentic-lib — Agentic Coding Systems SDK
|
|
@@ -50,6 +50,9 @@ Tasks (run Copilot SDK transformations):
|
|
|
50
50
|
maintain-library Update library docs from SOURCES.md
|
|
51
51
|
fix-code Fix failing tests
|
|
52
52
|
|
|
53
|
+
MCP Server:
|
|
54
|
+
mcp Start MCP server (for Claude Code, Cursor, etc.)
|
|
55
|
+
|
|
53
56
|
Options:
|
|
54
57
|
--purge Full reset — clear features, activity log, source code
|
|
55
58
|
--reseed Clear features + activity log (keep source code)
|
|
@@ -88,6 +91,13 @@ const mission = missionIdx >= 0 ? flags[missionIdx + 1] : "hamming-distance";
|
|
|
88
91
|
|
|
89
92
|
// ─── Task Commands ───────────────────────────────────────────────────
|
|
90
93
|
|
|
94
|
+
if (command === "mcp") {
|
|
95
|
+
const { startServer } = await import("../src/mcp/server.js");
|
|
96
|
+
await startServer();
|
|
97
|
+
// Server runs until stdin closes — don't exit
|
|
98
|
+
await new Promise(() => {}); // block forever
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
if (TASK_COMMANDS.includes(command)) {
|
|
92
102
|
process.exit(await runTask(command));
|
|
93
103
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xn-intenton-z2a/agentic-lib",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.59",
|
|
4
4
|
"description": "Agentic-lib Agentic Coding Systems SDK powering automated GitHub workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -81,7 +81,8 @@
|
|
|
81
81
|
"src/actions/setup-npmrc/",
|
|
82
82
|
"src/agents/",
|
|
83
83
|
"src/seeds/",
|
|
84
|
-
"src/scripts/"
|
|
84
|
+
"src/scripts/",
|
|
85
|
+
"src/mcp/"
|
|
85
86
|
],
|
|
86
87
|
"overrides": {
|
|
87
88
|
"minimatch": ">=10.2.3"
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
"@actions/core": "^3.0.0",
|
|
100
101
|
"@actions/github": "^9.0.0",
|
|
101
102
|
"@github/copilot-sdk": "^0.1.29",
|
|
103
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
102
104
|
"smol-toml": "^1.6.0"
|
|
103
105
|
}
|
|
104
106
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
2
|
// Copyright (C) 2025-2026 Polycode Limited
|
|
3
|
-
// tasks/fix-code.js — Fix failing tests on a PR
|
|
3
|
+
// tasks/fix-code.js — Fix failing tests or resolve merge conflicts on a PR
|
|
4
4
|
//
|
|
5
|
-
// Given a PR number
|
|
6
|
-
//
|
|
5
|
+
// Given a PR number, detects merge conflicts or failing checks and resolves them.
|
|
6
|
+
// Conflict resolution: reads files with conflict markers, asks the LLM to resolve.
|
|
7
|
+
// Failing checks: analyzes test output, generates code fixes.
|
|
7
8
|
|
|
8
9
|
import * as core from "@actions/core";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
9
11
|
import { execSync } from "child_process";
|
|
10
12
|
import { runCopilotTask, formatPathsSection } from "../copilot.js";
|
|
11
13
|
|
|
@@ -37,7 +39,90 @@ function fetchRunLog(runId) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
42
|
+
* Resolve merge conflicts on a PR using the Copilot SDK.
|
|
43
|
+
* Called when the workflow has started a `git merge origin/main` that left
|
|
44
|
+
* conflict markers in non-trivial files (listed in NON_TRIVIAL_FILES env var).
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} params
|
|
47
|
+
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
48
|
+
*/
|
|
49
|
+
async function resolveConflicts({ config, pr, prNumber, instructions, model, writablePaths, testCommand }) {
|
|
50
|
+
const nonTrivialEnv = process.env.NON_TRIVIAL_FILES || "";
|
|
51
|
+
const conflictedPaths = nonTrivialEnv
|
|
52
|
+
.split("\n")
|
|
53
|
+
.map((f) => f.trim())
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
|
|
56
|
+
if (conflictedPaths.length === 0) {
|
|
57
|
+
core.info(`PR #${prNumber} has conflicts but no non-trivial files listed. Returning nop.`);
|
|
58
|
+
return { outcome: "nop", details: "No non-trivial conflict files to resolve" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
core.info(`Resolving ${conflictedPaths.length} conflicted file(s) on PR #${prNumber}`);
|
|
62
|
+
|
|
63
|
+
const conflicts = conflictedPaths.map((f) => {
|
|
64
|
+
try {
|
|
65
|
+
return { name: f, content: readFileSync(f, "utf8") };
|
|
66
|
+
} catch (err) {
|
|
67
|
+
core.warning(`Could not read conflicted file ${f}: ${err.message}`);
|
|
68
|
+
return { name: f, content: "(could not read)" };
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const agentInstructions = instructions || "Resolve the merge conflicts while preserving the PR's intended changes.";
|
|
73
|
+
const readOnlyPaths = config.readOnlyPaths;
|
|
74
|
+
|
|
75
|
+
const prompt = [
|
|
76
|
+
"## Instructions",
|
|
77
|
+
agentInstructions,
|
|
78
|
+
"",
|
|
79
|
+
`## Pull Request #${prNumber}: ${pr.title}`,
|
|
80
|
+
"",
|
|
81
|
+
pr.body || "(no description)",
|
|
82
|
+
"",
|
|
83
|
+
"## Task: Resolve Merge Conflicts",
|
|
84
|
+
"The PR branch has been merged with main but has conflicts in the files below.",
|
|
85
|
+
"Each file contains git conflict markers (<<<<<<< / ======= / >>>>>>>).",
|
|
86
|
+
"Resolve each conflict by keeping the PR's intended changes while incorporating",
|
|
87
|
+
"any non-conflicting updates from main.",
|
|
88
|
+
"",
|
|
89
|
+
`## Conflicted Files (${conflicts.length})`,
|
|
90
|
+
...conflicts.map((f) => `### ${f.name}\n\`\`\`\n${f.content}\n\`\`\``),
|
|
91
|
+
"",
|
|
92
|
+
formatPathsSection(writablePaths, readOnlyPaths, config),
|
|
93
|
+
"",
|
|
94
|
+
"## Constraints",
|
|
95
|
+
"- Remove ALL conflict markers (<<<<<<, =======, >>>>>>>)",
|
|
96
|
+
"- Preserve the PR's feature/fix intent",
|
|
97
|
+
`- Run \`${testCommand}\` to validate your resolution`,
|
|
98
|
+
].join("\n");
|
|
99
|
+
|
|
100
|
+
const t = config.tuning || {};
|
|
101
|
+
const { tokensUsed, inputTokens, outputTokens, cost } = await runCopilotTask({
|
|
102
|
+
model,
|
|
103
|
+
systemMessage: `You are resolving git merge conflicts on PR #${prNumber}. Write resolved versions of each conflicted file, removing all conflict markers. Preserve the PR's feature intent while incorporating main's updates.`,
|
|
104
|
+
prompt,
|
|
105
|
+
writablePaths,
|
|
106
|
+
tuning: t,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
core.info(`Conflict resolution completed (${tokensUsed} tokens)`);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
outcome: "conflicts-resolved",
|
|
113
|
+
tokensUsed,
|
|
114
|
+
inputTokens,
|
|
115
|
+
outputTokens,
|
|
116
|
+
cost,
|
|
117
|
+
model,
|
|
118
|
+
details: `Resolved ${conflicts.length} conflicted file(s) on PR #${prNumber}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fix failing code or resolve merge conflicts on a pull request.
|
|
124
|
+
*
|
|
125
|
+
* Priority: conflicts first (if NON_TRIVIAL_FILES env is set), then failing checks.
|
|
41
126
|
*
|
|
42
127
|
* @param {Object} context - Task context from index.js
|
|
43
128
|
* @returns {Promise<Object>} Result with outcome, tokensUsed, model
|
|
@@ -49,8 +134,15 @@ export async function fixCode(context) {
|
|
|
49
134
|
throw new Error("fix-code task requires pr-number input");
|
|
50
135
|
}
|
|
51
136
|
|
|
52
|
-
// Fetch the PR
|
|
137
|
+
// Fetch the PR
|
|
53
138
|
const { data: pr } = await octokit.rest.pulls.get({ ...repo, pull_number: Number(prNumber) });
|
|
139
|
+
|
|
140
|
+
// If we have non-trivial conflict files from the workflow's Tier 1 step, resolve them
|
|
141
|
+
if (process.env.NON_TRIVIAL_FILES) {
|
|
142
|
+
return resolveConflicts({ config, pr, prNumber, instructions, model, writablePaths, testCommand });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Otherwise, check for failing checks
|
|
54
146
|
const { data: checkRuns } = await octokit.rest.checks.listForRef({ ...repo, ref: pr.head.sha, per_page: 10 });
|
|
55
147
|
|
|
56
148
|
const failedChecks = checkRuns.check_runs.filter((cr) => cr.conclusion === "failure");
|
|
@@ -11,3 +11,15 @@ and maintain the integrity of the codebase's primary purpose.
|
|
|
11
11
|
You may complete the implementation of a feature and/or bring the code output in line with the README
|
|
12
12
|
or other documentation. Do as much as you can all at once so that the build runs (even with nothing
|
|
13
13
|
to build) and the tests pass and the main at least doesn't output an error.
|
|
14
|
+
|
|
15
|
+
## Merge Conflict Resolution
|
|
16
|
+
|
|
17
|
+
When resolving merge conflicts (files containing <<<<<<< / ======= / >>>>>>> markers):
|
|
18
|
+
|
|
19
|
+
1. **Understand both sides**: The HEAD side (above =======) is the PR's changes. The incoming side
|
|
20
|
+
(below =======) is from main. Understand what each side intended before choosing.
|
|
21
|
+
2. **Preserve PR intent**: The PR was created for a reason — keep its feature/fix changes.
|
|
22
|
+
3. **Incorporate main's updates**: If main added new code that doesn't conflict with the PR's
|
|
23
|
+
purpose, include it.
|
|
24
|
+
4. **Remove ALL markers**: Every <<<<<<, =======, and >>>>>>> line must be removed.
|
|
25
|
+
5. **Run tests**: After resolving, run the test command to validate the resolution compiles and passes.
|
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
3
|
+
// Copyright (C) 2025-2026 Polycode Limited
|
|
4
|
+
// src/mcp/server.js — MCP server for agentic-lib
|
|
5
|
+
//
|
|
6
|
+
// Exposes agentic-lib capabilities as MCP tools for Claude Code and other MCP clients.
|
|
7
|
+
// Usage: npx @xn-intenton-z2a/agentic-lib mcp
|
|
8
|
+
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "fs";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { resolve, dirname, join } from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { tmpdir, homedir } from "os";
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pkgRoot = resolve(__dirname, "../..");
|
|
20
|
+
const binPath = resolve(pkgRoot, "bin/agentic-lib.js");
|
|
21
|
+
const seedsDir = resolve(pkgRoot, "src/seeds/missions");
|
|
22
|
+
|
|
23
|
+
// Workspace base directory
|
|
24
|
+
const workspacesBase =
|
|
25
|
+
process.env.AGENTIC_LIB_WORKSPACES || join(homedir(), ".agentic-lib", "workspaces");
|
|
26
|
+
|
|
27
|
+
// ─── Workspace Helpers ──────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function ensureWorkspacesDir() {
|
|
30
|
+
if (!existsSync(workspacesBase)) {
|
|
31
|
+
mkdirSync(workspacesBase, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function workspacePath(id) {
|
|
36
|
+
return join(workspacesBase, id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function metadataPath(wsPath) {
|
|
40
|
+
return join(wsPath, ".agentic-lib-workspace.json");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readMetadata(wsPath) {
|
|
44
|
+
const mp = metadataPath(wsPath);
|
|
45
|
+
if (!existsSync(mp)) return null;
|
|
46
|
+
return JSON.parse(readFileSync(mp, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeMetadata(wsPath, metadata) {
|
|
50
|
+
writeFileSync(metadataPath(wsPath), JSON.stringify(metadata, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listWorkspaces() {
|
|
54
|
+
ensureWorkspacesDir();
|
|
55
|
+
const entries = readdirSync(workspacesBase, { withFileTypes: true });
|
|
56
|
+
return entries
|
|
57
|
+
.filter((e) => e.isDirectory())
|
|
58
|
+
.map((e) => {
|
|
59
|
+
const wsPath = workspacePath(e.name);
|
|
60
|
+
const meta = readMetadata(wsPath);
|
|
61
|
+
return meta || { id: e.name, status: "unknown" };
|
|
62
|
+
})
|
|
63
|
+
.filter((m) => m.status !== "unknown");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runCli(args, wsPath, timeoutMs = 300000) {
|
|
67
|
+
const cmd = `node ${binPath} ${args}`;
|
|
68
|
+
try {
|
|
69
|
+
const stdout = execSync(cmd, {
|
|
70
|
+
cwd: wsPath || pkgRoot,
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
timeout: timeoutMs,
|
|
73
|
+
env: { ...process.env },
|
|
74
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
75
|
+
});
|
|
76
|
+
return { success: true, output: stdout };
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
output: `STDOUT:\n${err.stdout || ""}\nSTDERR:\n${err.stderr || ""}`,
|
|
81
|
+
exitCode: err.status || 1,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runInWorkspace(command, wsPath, timeoutMs = 120000) {
|
|
87
|
+
try {
|
|
88
|
+
const stdout = execSync(command, {
|
|
89
|
+
cwd: wsPath,
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
timeout: timeoutMs,
|
|
92
|
+
env: { ...process.env },
|
|
93
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
94
|
+
});
|
|
95
|
+
return { success: true, output: stdout, exitCode: 0 };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
output: `STDOUT:\n${err.stdout || ""}\nSTDERR:\n${err.stderr || ""}`,
|
|
100
|
+
exitCode: err.status || 1,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Tool Definitions ───────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
const TOOLS = [
|
|
108
|
+
{
|
|
109
|
+
name: "list_missions",
|
|
110
|
+
description:
|
|
111
|
+
"List available mission seeds that can be used to create workspaces. Each mission is a MISSION.md template describing what the autonomous code should build.",
|
|
112
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "workspace_create",
|
|
116
|
+
description:
|
|
117
|
+
"Create a new workspace from a mission seed. Runs init --purge, npm install, and sets up the Copilot SDK. Returns the workspace ID and path.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
mission: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Mission seed name (e.g. 'hamming-distance', 'fizz-buzz')",
|
|
124
|
+
},
|
|
125
|
+
profile: {
|
|
126
|
+
type: "string",
|
|
127
|
+
enum: ["min", "recommended", "max"],
|
|
128
|
+
description: "Tuning profile: min (fast/cheap), recommended (balanced), max (thorough). Default: min",
|
|
129
|
+
},
|
|
130
|
+
model: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "LLM model override (e.g. 'gpt-5-mini', 'claude-sonnet-4'). Uses profile default if omitted.",
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
required: ["mission"],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "workspace_list",
|
|
140
|
+
description: "List all active workspaces with their current status, mission, profile, and iteration count.",
|
|
141
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "workspace_status",
|
|
145
|
+
description:
|
|
146
|
+
"Get detailed status of a workspace: mission content, features, test results, iteration history, and current configuration.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
151
|
+
},
|
|
152
|
+
required: ["workspace"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "workspace_destroy",
|
|
157
|
+
description: "Delete a workspace and all its contents.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
162
|
+
},
|
|
163
|
+
required: ["workspace"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "iterate",
|
|
168
|
+
description:
|
|
169
|
+
"Run N cycles of the autonomous development loop (maintain-features -> transform -> test -> fix-code). Stops early if tests pass for 2 consecutive iterations or if no files change for 2 consecutive iterations. Returns iteration-by-iteration results.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
174
|
+
cycles: {
|
|
175
|
+
type: "number",
|
|
176
|
+
description: "Maximum number of iterations to run. Default: 3",
|
|
177
|
+
},
|
|
178
|
+
steps: {
|
|
179
|
+
type: "array",
|
|
180
|
+
items: { type: "string", enum: ["maintain-features", "transform", "fix-code"] },
|
|
181
|
+
description:
|
|
182
|
+
"Which steps to run per cycle. Default: ['maintain-features', 'transform', 'fix-code']. Use ['transform'] for transform-only cycles.",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
required: ["workspace"],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "run_tests",
|
|
190
|
+
description: "Run tests in a workspace and return pass/fail status with output.",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
195
|
+
},
|
|
196
|
+
required: ["workspace"],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "config_get",
|
|
201
|
+
description: "Read the agentic-lib.toml configuration from a workspace.",
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: "object",
|
|
204
|
+
properties: {
|
|
205
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
206
|
+
},
|
|
207
|
+
required: ["workspace"],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: "config_set",
|
|
212
|
+
description:
|
|
213
|
+
"Update configuration in a workspace. Can change the tuning profile, model, or individual tuning knobs. Changes take effect on the next iteration.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
218
|
+
profile: {
|
|
219
|
+
type: "string",
|
|
220
|
+
enum: ["min", "recommended", "max"],
|
|
221
|
+
description: "Tuning profile to switch to",
|
|
222
|
+
},
|
|
223
|
+
model: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "LLM model to use (e.g. 'gpt-5-mini', 'claude-sonnet-4', 'gpt-4.1')",
|
|
226
|
+
},
|
|
227
|
+
overrides: {
|
|
228
|
+
type: "object",
|
|
229
|
+
description:
|
|
230
|
+
"Individual tuning knob overrides. Keys: reasoning-effort, infinite-sessions, features-scan, source-scan, source-content, issues-scan, document-summary, discussion-comments",
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
required: ["workspace"],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "prepare_iteration",
|
|
238
|
+
description:
|
|
239
|
+
"Gather full context for a workspace iteration: mission, features, source code, test results, and transformation instructions. Use this when YOU (Claude/the MCP client) are the LLM — read the returned context, write code via workspace_write_file, then verify with run_tests. No Copilot token needed.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
244
|
+
focus: {
|
|
245
|
+
type: "string",
|
|
246
|
+
enum: ["transform", "maintain-features", "fix-code"],
|
|
247
|
+
description: "What kind of iteration to prepare context for. Default: transform",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
required: ["workspace"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "workspace_read_file",
|
|
255
|
+
description: "Read a file from a workspace. Use with prepare_iteration when Claude is the LLM.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
260
|
+
path: { type: "string", description: "Relative path within the workspace (e.g. 'src/lib/main.js')" },
|
|
261
|
+
},
|
|
262
|
+
required: ["workspace", "path"],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "workspace_write_file",
|
|
267
|
+
description:
|
|
268
|
+
"Write a file to a workspace. Parent directories are created automatically. Use with prepare_iteration when Claude is the LLM.",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
273
|
+
path: { type: "string", description: "Relative path within the workspace (e.g. 'src/lib/main.js')" },
|
|
274
|
+
content: { type: "string", description: "File content to write" },
|
|
275
|
+
},
|
|
276
|
+
required: ["workspace", "path", "content"],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "workspace_exec",
|
|
281
|
+
description:
|
|
282
|
+
"Run a shell command in a workspace. Use for builds, custom test commands, or inspecting workspace state. Git write commands are blocked.",
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: "object",
|
|
285
|
+
properties: {
|
|
286
|
+
workspace: { type: "string", description: "Workspace ID" },
|
|
287
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
288
|
+
},
|
|
289
|
+
required: ["workspace", "command"],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
// ─── Tool Handlers ──────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
async function handleListMissions() {
|
|
297
|
+
if (!existsSync(seedsDir)) {
|
|
298
|
+
return text("No missions directory found.");
|
|
299
|
+
}
|
|
300
|
+
const missions = readdirSync(seedsDir)
|
|
301
|
+
.filter((f) => f.endsWith(".md"))
|
|
302
|
+
.map((f) => {
|
|
303
|
+
const name = f.replace(/\.md$/, "");
|
|
304
|
+
const content = readFileSync(join(seedsDir, f), "utf8");
|
|
305
|
+
const firstLine = content.split("\n").find((l) => l.trim()) || "";
|
|
306
|
+
return `- **${name}**: ${firstLine.replace(/^#\s*/, "")}`;
|
|
307
|
+
});
|
|
308
|
+
return text(`Available missions (${missions.length}):\n\n${missions.join("\n")}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function handleWorkspaceCreate({ mission, profile = "min", model }) {
|
|
312
|
+
// Validate mission exists
|
|
313
|
+
const missionFile = join(seedsDir, `${mission}.md`);
|
|
314
|
+
if (!existsSync(missionFile)) {
|
|
315
|
+
const available = readdirSync(seedsDir)
|
|
316
|
+
.filter((f) => f.endsWith(".md"))
|
|
317
|
+
.map((f) => f.replace(/\.md$/, ""));
|
|
318
|
+
return text(`Unknown mission "${mission}". Available: ${available.join(", ")}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Create workspace
|
|
322
|
+
ensureWorkspacesDir();
|
|
323
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "").substring(0, 15);
|
|
324
|
+
const id = `${mission}-${timestamp}`;
|
|
325
|
+
const wsPath = workspacePath(id);
|
|
326
|
+
mkdirSync(wsPath, { recursive: true });
|
|
327
|
+
|
|
328
|
+
// Initialize git repo (required for init)
|
|
329
|
+
runInWorkspace("git init", wsPath);
|
|
330
|
+
|
|
331
|
+
// Run init --purge
|
|
332
|
+
const initResult = runCli(`init --purge --mission ${mission} --target ${wsPath}`, wsPath);
|
|
333
|
+
if (!initResult.success) {
|
|
334
|
+
return text(`Failed to initialize workspace:\n${initResult.output}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Update config with profile and model
|
|
338
|
+
const tomlPath = join(wsPath, "agentic-lib.toml");
|
|
339
|
+
if (existsSync(tomlPath)) {
|
|
340
|
+
let toml = readFileSync(tomlPath, "utf8");
|
|
341
|
+
toml = toml.replace(/^profile\s*=\s*"[^"]*"/m, `profile = "${profile}"`);
|
|
342
|
+
if (model) {
|
|
343
|
+
toml = toml.replace(/^model\s*=\s*"[^"]*"/m, `model = "${model}"`);
|
|
344
|
+
}
|
|
345
|
+
writeFileSync(tomlPath, toml);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// npm install
|
|
349
|
+
const npmResult = runInWorkspace("npm install --ignore-scripts 2>&1", wsPath, 120000);
|
|
350
|
+
|
|
351
|
+
// Install Copilot SDK in action dir
|
|
352
|
+
const actionDir = join(wsPath, ".github/agentic-lib/actions/agentic-step");
|
|
353
|
+
let sdkResult = { success: true, output: "skipped" };
|
|
354
|
+
if (existsSync(join(actionDir, "package.json"))) {
|
|
355
|
+
sdkResult = runInWorkspace(`cd "${actionDir}" && npm ci 2>&1`, wsPath, 120000);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Write metadata
|
|
359
|
+
const metadata = {
|
|
360
|
+
id,
|
|
361
|
+
mission,
|
|
362
|
+
created: new Date().toISOString(),
|
|
363
|
+
profile,
|
|
364
|
+
model: model || profileDefaultModel(profile),
|
|
365
|
+
iterations: [],
|
|
366
|
+
status: "ready",
|
|
367
|
+
path: wsPath,
|
|
368
|
+
};
|
|
369
|
+
writeMetadata(wsPath, metadata);
|
|
370
|
+
|
|
371
|
+
const summary = [
|
|
372
|
+
`Workspace created: **${id}**`,
|
|
373
|
+
`Path: ${wsPath}`,
|
|
374
|
+
`Mission: ${mission}`,
|
|
375
|
+
`Profile: ${profile}`,
|
|
376
|
+
`Model: ${metadata.model}`,
|
|
377
|
+
"",
|
|
378
|
+
`Init: ${initResult.success ? "OK" : "FAILED"}`,
|
|
379
|
+
`npm install: ${npmResult.success ? "OK" : "FAILED"}`,
|
|
380
|
+
`Copilot SDK: ${sdkResult.success ? "OK" : "FAILED"}`,
|
|
381
|
+
"",
|
|
382
|
+
"Ready for `iterate` or `run_tests`.",
|
|
383
|
+
];
|
|
384
|
+
return text(summary.join("\n"));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function handleWorkspaceList() {
|
|
388
|
+
const workspaces = listWorkspaces();
|
|
389
|
+
if (workspaces.length === 0) {
|
|
390
|
+
return text("No workspaces found. Use `workspace_create` to create one.");
|
|
391
|
+
}
|
|
392
|
+
const lines = workspaces.map((w) => {
|
|
393
|
+
const iters = w.iterations?.length || 0;
|
|
394
|
+
return `- **${w.id}** | mission: ${w.mission} | profile: ${w.profile} | model: ${w.model} | iterations: ${iters} | status: ${w.status}`;
|
|
395
|
+
});
|
|
396
|
+
return text(`Workspaces (${workspaces.length}):\n\n${lines.join("\n")}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function handleWorkspaceStatus({ workspace }) {
|
|
400
|
+
const wsPath = workspacePath(workspace);
|
|
401
|
+
const meta = readMetadata(wsPath);
|
|
402
|
+
if (!meta) {
|
|
403
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const sections = [`# Workspace: ${meta.id}`, ""];
|
|
407
|
+
|
|
408
|
+
// Mission
|
|
409
|
+
const missionPath = join(wsPath, "MISSION.md");
|
|
410
|
+
if (existsSync(missionPath)) {
|
|
411
|
+
const mission = readFileSync(missionPath, "utf8");
|
|
412
|
+
sections.push("## Mission", mission.substring(0, 1000), "");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Config
|
|
416
|
+
sections.push("## Configuration");
|
|
417
|
+
sections.push(`- Profile: ${meta.profile}`);
|
|
418
|
+
sections.push(`- Model: ${meta.model}`);
|
|
419
|
+
sections.push(`- Status: ${meta.status}`);
|
|
420
|
+
sections.push("");
|
|
421
|
+
|
|
422
|
+
// Features
|
|
423
|
+
const featuresDir = join(wsPath, "features");
|
|
424
|
+
if (existsSync(featuresDir)) {
|
|
425
|
+
const features = readdirSync(featuresDir).filter((f) => f.endsWith(".md"));
|
|
426
|
+
sections.push(`## Features (${features.length})`);
|
|
427
|
+
for (const f of features.slice(0, 10)) {
|
|
428
|
+
const content = readFileSync(join(featuresDir, f), "utf8");
|
|
429
|
+
sections.push(`### ${f}`, content.substring(0, 300), "");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Source files
|
|
434
|
+
const srcDir = join(wsPath, "src/lib");
|
|
435
|
+
if (existsSync(srcDir)) {
|
|
436
|
+
const srcFiles = readdirSync(srcDir, { recursive: true }).filter((f) =>
|
|
437
|
+
String(f).match(/\.(js|ts)$/),
|
|
438
|
+
);
|
|
439
|
+
sections.push(`## Source files (${srcFiles.length})`);
|
|
440
|
+
for (const f of srcFiles.slice(0, 5)) {
|
|
441
|
+
const content = readFileSync(join(srcDir, String(f)), "utf8");
|
|
442
|
+
sections.push(`### ${f}`, "```js", content.substring(0, 2000), "```", "");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Iteration history
|
|
447
|
+
if (meta.iterations && meta.iterations.length > 0) {
|
|
448
|
+
sections.push(`## Iteration History (${meta.iterations.length} cycles)`);
|
|
449
|
+
for (const iter of meta.iterations) {
|
|
450
|
+
sections.push(
|
|
451
|
+
`### Iteration ${iter.number} (${iter.profile}/${iter.model})`,
|
|
452
|
+
`- Steps: ${iter.steps?.map((s) => `${s.step}: ${s.success ? "OK" : "FAIL"}`).join(", ") || "none"}`,
|
|
453
|
+
`- Tests: ${iter.testsPassed ? "PASS" : "FAIL"}`,
|
|
454
|
+
`- Elapsed: ${iter.elapsed || "?"}s`,
|
|
455
|
+
"",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Run tests for current status
|
|
461
|
+
const testResult = runInWorkspace("npm test 2>&1", wsPath, 60000);
|
|
462
|
+
sections.push("## Current Test Status");
|
|
463
|
+
sections.push(testResult.success ? "All tests passing." : "Tests failing.");
|
|
464
|
+
sections.push("```", testResult.output.substring(0, 2000), "```");
|
|
465
|
+
|
|
466
|
+
return text(sections.join("\n"));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function handleWorkspaceDestroy({ workspace }) {
|
|
470
|
+
const wsPath = workspacePath(workspace);
|
|
471
|
+
if (!existsSync(wsPath)) {
|
|
472
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
473
|
+
}
|
|
474
|
+
rmSync(wsPath, { recursive: true, force: true });
|
|
475
|
+
return text(`Workspace "${workspace}" destroyed.`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function handleIterate({ workspace, cycles = 3, steps }) {
|
|
479
|
+
const wsPath = workspacePath(workspace);
|
|
480
|
+
const meta = readMetadata(wsPath);
|
|
481
|
+
if (!meta) {
|
|
482
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const stepsToRun = steps || ["maintain-features", "transform", "fix-code"];
|
|
486
|
+
const results = [];
|
|
487
|
+
let consecutivePasses = 0;
|
|
488
|
+
let consecutiveNoChanges = 0;
|
|
489
|
+
const startIterNum = (meta.iterations?.length || 0) + 1;
|
|
490
|
+
|
|
491
|
+
meta.status = "iterating";
|
|
492
|
+
writeMetadata(wsPath, meta);
|
|
493
|
+
|
|
494
|
+
for (let i = 0; i < cycles; i++) {
|
|
495
|
+
const iterNum = startIterNum + i;
|
|
496
|
+
const iterStart = Date.now();
|
|
497
|
+
const iterSteps = [];
|
|
498
|
+
|
|
499
|
+
// Snapshot source files before
|
|
500
|
+
const srcBefore = snapshotDir(join(wsPath, "src/lib"));
|
|
501
|
+
|
|
502
|
+
for (const step of stepsToRun) {
|
|
503
|
+
const stepStart = Date.now();
|
|
504
|
+
const result = runCli(
|
|
505
|
+
`${step} --target ${wsPath} --model ${meta.model}`,
|
|
506
|
+
wsPath,
|
|
507
|
+
300000,
|
|
508
|
+
);
|
|
509
|
+
const stepElapsed = ((Date.now() - stepStart) / 1000).toFixed(1);
|
|
510
|
+
iterSteps.push({
|
|
511
|
+
step,
|
|
512
|
+
success: result.success,
|
|
513
|
+
elapsed: stepElapsed,
|
|
514
|
+
output: result.output.substring(0, 500),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Run tests
|
|
519
|
+
const testResult = runInWorkspace("npm test 2>&1", wsPath, 60000);
|
|
520
|
+
const testsPassed = testResult.success;
|
|
521
|
+
|
|
522
|
+
// Snapshot source files after
|
|
523
|
+
const srcAfter = snapshotDir(join(wsPath, "src/lib"));
|
|
524
|
+
const filesChanged = countChanges(srcBefore, srcAfter);
|
|
525
|
+
|
|
526
|
+
const iterElapsed = ((Date.now() - iterStart) / 1000).toFixed(1);
|
|
527
|
+
|
|
528
|
+
const iterRecord = {
|
|
529
|
+
number: iterNum,
|
|
530
|
+
profile: meta.profile,
|
|
531
|
+
model: meta.model,
|
|
532
|
+
steps: iterSteps,
|
|
533
|
+
testsPassed,
|
|
534
|
+
filesChanged,
|
|
535
|
+
elapsed: iterElapsed,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
meta.iterations.push(iterRecord);
|
|
539
|
+
writeMetadata(wsPath, meta);
|
|
540
|
+
|
|
541
|
+
results.push(iterRecord);
|
|
542
|
+
|
|
543
|
+
// Check stop conditions
|
|
544
|
+
if (testsPassed) {
|
|
545
|
+
consecutivePasses++;
|
|
546
|
+
if (consecutivePasses >= 2) {
|
|
547
|
+
results.push({ stopped: true, reason: "tests passed for 2 consecutive iterations" });
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
} else {
|
|
551
|
+
consecutivePasses = 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (filesChanged === 0) {
|
|
555
|
+
consecutiveNoChanges++;
|
|
556
|
+
if (consecutiveNoChanges >= 2) {
|
|
557
|
+
results.push({ stopped: true, reason: "no files changed for 2 consecutive iterations (stalled)" });
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
consecutiveNoChanges = 0;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
meta.status = "ready";
|
|
566
|
+
writeMetadata(wsPath, meta);
|
|
567
|
+
|
|
568
|
+
// Format results
|
|
569
|
+
const lines = [`# Iterate Results: ${workspace}`, `Cycles requested: ${cycles}`, ""];
|
|
570
|
+
for (const r of results) {
|
|
571
|
+
if (r.stopped) {
|
|
572
|
+
lines.push(`**Stopped early:** ${r.reason}`);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
lines.push(`## Iteration ${r.number} (${r.model}, ${r.profile})`);
|
|
576
|
+
lines.push(`- Elapsed: ${r.elapsed}s`);
|
|
577
|
+
lines.push(`- Files changed: ${r.filesChanged}`);
|
|
578
|
+
lines.push(`- Tests: ${r.testsPassed ? "PASS" : "FAIL"}`);
|
|
579
|
+
for (const s of r.steps) {
|
|
580
|
+
lines.push(` - ${s.step}: ${s.success ? "OK" : "FAIL"} (${s.elapsed}s)`);
|
|
581
|
+
}
|
|
582
|
+
lines.push("");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const totalIters = results.filter((r) => !r.stopped).length;
|
|
586
|
+
const lastPassed = results.filter((r) => !r.stopped).slice(-1)[0]?.testsPassed;
|
|
587
|
+
lines.push("## Summary");
|
|
588
|
+
lines.push(`- Iterations completed: ${totalIters}`);
|
|
589
|
+
lines.push(`- Final test status: ${lastPassed ? "PASS" : "FAIL"}`);
|
|
590
|
+
lines.push(`- Total iterations for this workspace: ${meta.iterations.length}`);
|
|
591
|
+
lines.push(`- Profile: ${meta.profile} | Model: ${meta.model}`);
|
|
592
|
+
|
|
593
|
+
return text(lines.join("\n"));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function handleRunTests({ workspace }) {
|
|
597
|
+
const wsPath = workspacePath(workspace);
|
|
598
|
+
if (!readMetadata(wsPath)) {
|
|
599
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
600
|
+
}
|
|
601
|
+
const result = runInWorkspace("npm test 2>&1", wsPath, 120000);
|
|
602
|
+
return text(
|
|
603
|
+
[
|
|
604
|
+
`## Test Results: ${workspace}`,
|
|
605
|
+
"",
|
|
606
|
+
result.success ? "**PASS** — all tests passing." : "**FAIL** — tests are failing.",
|
|
607
|
+
"",
|
|
608
|
+
"```",
|
|
609
|
+
result.output.substring(0, 5000),
|
|
610
|
+
"```",
|
|
611
|
+
].join("\n"),
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function handleConfigGet({ workspace }) {
|
|
616
|
+
const wsPath = workspacePath(workspace);
|
|
617
|
+
if (!readMetadata(wsPath)) {
|
|
618
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
619
|
+
}
|
|
620
|
+
const tomlPath = join(wsPath, "agentic-lib.toml");
|
|
621
|
+
if (!existsSync(tomlPath)) {
|
|
622
|
+
return text("No agentic-lib.toml found in workspace.");
|
|
623
|
+
}
|
|
624
|
+
const content = readFileSync(tomlPath, "utf8");
|
|
625
|
+
return text(`## Configuration: ${workspace}\n\n\`\`\`toml\n${content}\n\`\`\``);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function handleConfigSet({ workspace, profile, model, overrides }) {
|
|
629
|
+
const wsPath = workspacePath(workspace);
|
|
630
|
+
const meta = readMetadata(wsPath);
|
|
631
|
+
if (!meta) {
|
|
632
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const tomlPath = join(wsPath, "agentic-lib.toml");
|
|
636
|
+
if (!existsSync(tomlPath)) {
|
|
637
|
+
return text("No agentic-lib.toml found in workspace.");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
let toml = readFileSync(tomlPath, "utf8");
|
|
641
|
+
const changes = [];
|
|
642
|
+
|
|
643
|
+
if (profile) {
|
|
644
|
+
toml = toml.replace(/^profile\s*=\s*"[^"]*"/m, `profile = "${profile}"`);
|
|
645
|
+
meta.profile = profile;
|
|
646
|
+
if (!model) {
|
|
647
|
+
meta.model = profileDefaultModel(profile);
|
|
648
|
+
}
|
|
649
|
+
changes.push(`profile -> ${profile}`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (model) {
|
|
653
|
+
toml = toml.replace(/^model\s*=\s*"[^"]*"/m, `model = "${model}"`);
|
|
654
|
+
meta.model = model;
|
|
655
|
+
changes.push(`model -> ${model}`);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (overrides) {
|
|
659
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
660
|
+
const regex = new RegExp(`^${key}\\s*=\\s*.*$`, "m");
|
|
661
|
+
const line = typeof value === "string" ? `${key} = "${value}"` : `${key} = ${value}`;
|
|
662
|
+
if (regex.test(toml)) {
|
|
663
|
+
toml = toml.replace(regex, line);
|
|
664
|
+
} else {
|
|
665
|
+
// Add under [tuning] section
|
|
666
|
+
toml = toml.replace(/^\[tuning\]/m, `[tuning]\n${line}`);
|
|
667
|
+
}
|
|
668
|
+
changes.push(`${key} -> ${value}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
writeFileSync(tomlPath, toml);
|
|
673
|
+
writeMetadata(wsPath, meta);
|
|
674
|
+
|
|
675
|
+
return text(`Configuration updated for ${workspace}:\n${changes.map((c) => `- ${c}`).join("\n")}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function handlePrepareIteration({ workspace, focus = "transform" }) {
|
|
679
|
+
const wsPath = workspacePath(workspace);
|
|
680
|
+
const meta = readMetadata(wsPath);
|
|
681
|
+
if (!meta) {
|
|
682
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const sections = [];
|
|
686
|
+
const iterNum = (meta.iterations?.length || 0) + 1;
|
|
687
|
+
|
|
688
|
+
sections.push(`# Iteration ${iterNum} — ${focus}`);
|
|
689
|
+
sections.push(`Workspace: ${meta.id} | Profile: ${meta.profile}`);
|
|
690
|
+
sections.push("");
|
|
691
|
+
|
|
692
|
+
// Mission
|
|
693
|
+
const missionFile = join(wsPath, "MISSION.md");
|
|
694
|
+
if (existsSync(missionFile)) {
|
|
695
|
+
sections.push("## Mission", readFileSync(missionFile, "utf8"), "");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Features
|
|
699
|
+
const featuresDir = join(wsPath, "features");
|
|
700
|
+
if (existsSync(featuresDir)) {
|
|
701
|
+
const features = readdirSync(featuresDir).filter((f) => f.endsWith(".md"));
|
|
702
|
+
if (features.length > 0) {
|
|
703
|
+
sections.push(`## Features (${features.length})`);
|
|
704
|
+
for (const f of features.slice(0, 10)) {
|
|
705
|
+
sections.push(`### ${f}`, readFileSync(join(featuresDir, f), "utf8"), "");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Source files
|
|
711
|
+
const srcDir = join(wsPath, "src/lib");
|
|
712
|
+
if (existsSync(srcDir)) {
|
|
713
|
+
const srcFiles = readdirSync(srcDir, { recursive: true })
|
|
714
|
+
.filter((f) => String(f).match(/\.(js|ts)$/))
|
|
715
|
+
.slice(0, 20);
|
|
716
|
+
sections.push(`## Source Files (${srcFiles.length})`);
|
|
717
|
+
for (const f of srcFiles) {
|
|
718
|
+
const content = readFileSync(join(srcDir, String(f)), "utf8");
|
|
719
|
+
sections.push(`### src/lib/${f}`, "```js", content.substring(0, 5000), "```", "");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Test files
|
|
724
|
+
const testsDir = join(wsPath, "tests/unit");
|
|
725
|
+
if (existsSync(testsDir)) {
|
|
726
|
+
const testFiles = readdirSync(testsDir, { recursive: true })
|
|
727
|
+
.filter((f) => String(f).match(/\.(js|ts)$/))
|
|
728
|
+
.slice(0, 10);
|
|
729
|
+
sections.push(`## Test Files (${testFiles.length})`);
|
|
730
|
+
for (const f of testFiles) {
|
|
731
|
+
const content = readFileSync(join(testsDir, String(f)), "utf8");
|
|
732
|
+
sections.push(`### tests/unit/${f}`, "```js", content.substring(0, 3000), "```", "");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Current test results
|
|
737
|
+
const testResult = runInWorkspace("npm test 2>&1", wsPath, 60000);
|
|
738
|
+
sections.push("## Current Test Results");
|
|
739
|
+
sections.push(testResult.success ? "**All tests passing.**" : "**Tests failing.**");
|
|
740
|
+
sections.push("```", testResult.output.substring(0, 3000), "```", "");
|
|
741
|
+
|
|
742
|
+
// Instructions based on focus
|
|
743
|
+
sections.push("## Your Task");
|
|
744
|
+
if (focus === "transform") {
|
|
745
|
+
sections.push(
|
|
746
|
+
"Analyze the mission, features, and source code above.",
|
|
747
|
+
"Determine the single most impactful next step to advance the code toward the mission.",
|
|
748
|
+
"Then implement it by calling `workspace_write_file` to modify source files.",
|
|
749
|
+
"After writing, call `run_tests` to verify your changes.",
|
|
750
|
+
);
|
|
751
|
+
} else if (focus === "maintain-features") {
|
|
752
|
+
sections.push(
|
|
753
|
+
"Review the mission and current features.",
|
|
754
|
+
"Create, update, or prune feature files to keep the project focused on its mission.",
|
|
755
|
+
"Use `workspace_write_file` to write feature files to `features/<name>.md`.",
|
|
756
|
+
"Each feature should have clear, testable acceptance criteria.",
|
|
757
|
+
);
|
|
758
|
+
} else if (focus === "fix-code") {
|
|
759
|
+
sections.push(
|
|
760
|
+
"The test output above shows failing tests.",
|
|
761
|
+
"Analyze the failures and fix the source code.",
|
|
762
|
+
"Make minimal, targeted changes using `workspace_write_file`.",
|
|
763
|
+
"Then call `run_tests` to verify the fix.",
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
sections.push("");
|
|
767
|
+
sections.push("## Writable Paths");
|
|
768
|
+
sections.push("- src/lib/ (source code)");
|
|
769
|
+
sections.push("- tests/unit/ (test files)");
|
|
770
|
+
sections.push("- features/ (feature specs)");
|
|
771
|
+
sections.push("- examples/ (output artifacts)");
|
|
772
|
+
sections.push("- README.md, package.json");
|
|
773
|
+
|
|
774
|
+
return text(sections.join("\n"));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async function handleWorkspaceReadFile({ workspace, path }) {
|
|
778
|
+
const wsPath = workspacePath(workspace);
|
|
779
|
+
if (!readMetadata(wsPath)) {
|
|
780
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
781
|
+
}
|
|
782
|
+
const filePath = resolve(wsPath, path);
|
|
783
|
+
// Prevent path traversal
|
|
784
|
+
if (!filePath.startsWith(wsPath)) {
|
|
785
|
+
return text(`Path traversal not allowed: ${path}`);
|
|
786
|
+
}
|
|
787
|
+
if (!existsSync(filePath)) {
|
|
788
|
+
return text(`File not found: ${path}`);
|
|
789
|
+
}
|
|
790
|
+
const content = readFileSync(filePath, "utf8");
|
|
791
|
+
return text(`## ${path}\n\n\`\`\`\n${content}\n\`\`\``);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function handleWorkspaceWriteFile({ workspace, path, content }) {
|
|
795
|
+
const wsPath = workspacePath(workspace);
|
|
796
|
+
if (!readMetadata(wsPath)) {
|
|
797
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
798
|
+
}
|
|
799
|
+
const filePath = resolve(wsPath, path);
|
|
800
|
+
if (!filePath.startsWith(wsPath)) {
|
|
801
|
+
return text(`Path traversal not allowed: ${path}`);
|
|
802
|
+
}
|
|
803
|
+
const dir = dirname(filePath);
|
|
804
|
+
if (!existsSync(dir)) {
|
|
805
|
+
mkdirSync(dir, { recursive: true });
|
|
806
|
+
}
|
|
807
|
+
writeFileSync(filePath, content, "utf8");
|
|
808
|
+
return text(`Written ${content.length} chars to ${path}`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function handleWorkspaceExec({ workspace, command }) {
|
|
812
|
+
const wsPath = workspacePath(workspace);
|
|
813
|
+
if (!readMetadata(wsPath)) {
|
|
814
|
+
return text(`Workspace "${workspace}" not found.`);
|
|
815
|
+
}
|
|
816
|
+
// Block git write commands
|
|
817
|
+
const blocked = /\bgit\s+(commit|push|add|reset|checkout|rebase|merge|stash)\b/;
|
|
818
|
+
if (blocked.test(command)) {
|
|
819
|
+
return text(`Git write commands are not allowed: ${command}`);
|
|
820
|
+
}
|
|
821
|
+
const result = runInWorkspace(command, wsPath, 120000);
|
|
822
|
+
return text(
|
|
823
|
+
[
|
|
824
|
+
`## exec: ${command}`,
|
|
825
|
+
`Exit code: ${result.exitCode || 0}`,
|
|
826
|
+
"```",
|
|
827
|
+
result.output.substring(0, 5000),
|
|
828
|
+
"```",
|
|
829
|
+
].join("\n"),
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
834
|
+
|
|
835
|
+
function text(content) {
|
|
836
|
+
return { content: [{ type: "text", text: content }] };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function profileDefaultModel(profile) {
|
|
840
|
+
const models = { min: "gpt-5-mini", recommended: "claude-sonnet-4", max: "gpt-4.1" };
|
|
841
|
+
return models[profile] || "gpt-5-mini";
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function snapshotDir(dirPath) {
|
|
845
|
+
const snapshot = {};
|
|
846
|
+
if (!existsSync(dirPath)) return snapshot;
|
|
847
|
+
try {
|
|
848
|
+
const files = readdirSync(dirPath, { recursive: true });
|
|
849
|
+
for (const f of files) {
|
|
850
|
+
const fp = join(dirPath, String(f));
|
|
851
|
+
try {
|
|
852
|
+
snapshot[String(f)] = readFileSync(fp, "utf8");
|
|
853
|
+
} catch {
|
|
854
|
+
// skip non-readable files
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
} catch {
|
|
858
|
+
// skip unreadable dirs
|
|
859
|
+
}
|
|
860
|
+
return snapshot;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function countChanges(before, after) {
|
|
864
|
+
let changes = 0;
|
|
865
|
+
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
866
|
+
for (const key of allKeys) {
|
|
867
|
+
if (before[key] !== after[key]) changes++;
|
|
868
|
+
}
|
|
869
|
+
return changes;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ─── MCP Server ─────────────────────────────────────────────────────
|
|
873
|
+
|
|
874
|
+
const toolHandlers = {
|
|
875
|
+
list_missions: handleListMissions,
|
|
876
|
+
workspace_create: handleWorkspaceCreate,
|
|
877
|
+
workspace_list: handleWorkspaceList,
|
|
878
|
+
workspace_status: handleWorkspaceStatus,
|
|
879
|
+
workspace_destroy: handleWorkspaceDestroy,
|
|
880
|
+
iterate: handleIterate,
|
|
881
|
+
run_tests: handleRunTests,
|
|
882
|
+
config_get: handleConfigGet,
|
|
883
|
+
config_set: handleConfigSet,
|
|
884
|
+
prepare_iteration: handlePrepareIteration,
|
|
885
|
+
workspace_read_file: handleWorkspaceReadFile,
|
|
886
|
+
workspace_write_file: handleWorkspaceWriteFile,
|
|
887
|
+
workspace_exec: handleWorkspaceExec,
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
export async function startServer() {
|
|
891
|
+
const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
|
|
892
|
+
|
|
893
|
+
const server = new Server(
|
|
894
|
+
{ name: "agentic-lib", version: pkg.version },
|
|
895
|
+
{ capabilities: { tools: {} } },
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
899
|
+
|
|
900
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
901
|
+
const { name, arguments: args } = request.params;
|
|
902
|
+
const handler = toolHandlers[name];
|
|
903
|
+
if (!handler) {
|
|
904
|
+
return text(`Unknown tool: ${name}`);
|
|
905
|
+
}
|
|
906
|
+
try {
|
|
907
|
+
return await handler(args || {});
|
|
908
|
+
} catch (err) {
|
|
909
|
+
return text(`Error in ${name}: ${err.message}\n${err.stack || ""}`);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
const transport = new StdioServerTransport();
|
|
914
|
+
await server.connect(transport);
|
|
915
|
+
}
|