agentloopkit 0.24.5 → 0.26.0
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 +39 -4
- package/dist/cli/index.js +391 -40
- package/dist/cli/index.js.map +1 -1
- package/dist/templates/root/AGENTLOOP.md +2 -0
- package/dist/templates/root/AGENTS.md +2 -0
- package/package.json +4 -1
- package/server.json +20 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command21 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/cli/commands/init.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/core/init.ts
|
|
10
10
|
import path6 from "path";
|
|
11
|
-
import { readdir as readdir3, realpath } from "fs/promises";
|
|
11
|
+
import { readdir as readdir3, realpath, stat as stat2 } from "fs/promises";
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
|
|
14
14
|
// src/core/constants.ts
|
|
@@ -342,6 +342,21 @@ async function listTemplateFiles() {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
// src/core/init.ts
|
|
345
|
+
var LOCAL_ONLY_EXCLUDE_START = "# agentloopkit:local-only:start";
|
|
346
|
+
var LOCAL_ONLY_EXCLUDE_END = "# agentloopkit:local-only:end";
|
|
347
|
+
var LOCAL_ONLY_NOTICE_START = "<!-- agentloopkit:local-only:start -->";
|
|
348
|
+
var LOCAL_ONLY_NOTICE_END = "<!-- agentloopkit:local-only:end -->";
|
|
349
|
+
var LOCAL_ONLY_EXCLUDE_PATTERNS = [
|
|
350
|
+
`${AGENTLOOP_DIR}/`,
|
|
351
|
+
AGENTS_FILE,
|
|
352
|
+
AGENTLOOP_FILE,
|
|
353
|
+
CONFIG_FILE
|
|
354
|
+
];
|
|
355
|
+
var LOCAL_ONLY_NOTICE = `${LOCAL_ONLY_NOTICE_START}
|
|
356
|
+
## Local-only AgentLoopKit harness
|
|
357
|
+
|
|
358
|
+
This AgentLoopKit setup is excluded by this clone's \`.git/info/exclude\`. Use these files for local agent work. Do not commit these AgentLoopKit files unless a maintainer intentionally converts the repo to a shared harness.
|
|
359
|
+
${LOCAL_ONLY_NOTICE_END}`;
|
|
345
360
|
async function writeGeneratedFile(filePath, content, result) {
|
|
346
361
|
if (await pathExists(filePath)) {
|
|
347
362
|
result.skipped.push(filePath);
|
|
@@ -395,6 +410,65 @@ ${section.trim()}
|
|
|
395
410
|
`);
|
|
396
411
|
result.updated.push(filePath);
|
|
397
412
|
}
|
|
413
|
+
async function resolveGitInfoExcludePath(cwd) {
|
|
414
|
+
const dotGitPath = path6.join(cwd, ".git");
|
|
415
|
+
const dotGitStat = await stat2(dotGitPath).catch(() => void 0);
|
|
416
|
+
if (!dotGitStat) return void 0;
|
|
417
|
+
if (dotGitStat.isDirectory()) {
|
|
418
|
+
return path6.join(dotGitPath, "info", "exclude");
|
|
419
|
+
}
|
|
420
|
+
if (!dotGitStat.isFile()) return void 0;
|
|
421
|
+
const gitFile = await readTextIfExists(dotGitPath);
|
|
422
|
+
const match = /^gitdir:\s*(.+)\s*$/m.exec(gitFile);
|
|
423
|
+
if (!match) return void 0;
|
|
424
|
+
const gitDir = match[1].trim();
|
|
425
|
+
const resolvedGitDir = path6.isAbsolute(gitDir) ? gitDir : path6.resolve(cwd, gitDir);
|
|
426
|
+
return path6.join(resolvedGitDir, "info", "exclude");
|
|
427
|
+
}
|
|
428
|
+
async function upsertLocalOnlyGitExclude(cwd, result) {
|
|
429
|
+
const excludePath = await resolveGitInfoExcludePath(cwd);
|
|
430
|
+
if (!excludePath) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
"Local-only mode requires a Git repository because it writes to .git/info/exclude. Run git init first, or run agentloop init without --local-only."
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
result.localOnly = {
|
|
436
|
+
excludePath,
|
|
437
|
+
patterns: [...LOCAL_ONLY_EXCLUDE_PATTERNS]
|
|
438
|
+
};
|
|
439
|
+
const excludeExists = await pathExists(excludePath);
|
|
440
|
+
const existing = await readTextIfExists(excludePath);
|
|
441
|
+
if (existing.includes(LOCAL_ONLY_EXCLUDE_START)) return;
|
|
442
|
+
const block = [
|
|
443
|
+
LOCAL_ONLY_EXCLUDE_START,
|
|
444
|
+
"# AgentLoopKit local-only harness files for this clone.",
|
|
445
|
+
...LOCAL_ONLY_EXCLUDE_PATTERNS,
|
|
446
|
+
LOCAL_ONLY_EXCLUDE_END
|
|
447
|
+
].join("\n");
|
|
448
|
+
if (result.dryRun) {
|
|
449
|
+
(excludeExists ? result.updated : result.created).push(excludePath);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
const prefix = existing.trimEnd();
|
|
453
|
+
await writeTextFile(excludePath, `${prefix ? `${prefix}
|
|
454
|
+
|
|
455
|
+
` : ""}${block}
|
|
456
|
+
`);
|
|
457
|
+
(excludeExists ? result.updated : result.created).push(excludePath);
|
|
458
|
+
}
|
|
459
|
+
async function upsertLocalOnlyNotice(filePath, result) {
|
|
460
|
+
const existing = await readTextIfExists(filePath);
|
|
461
|
+
if (existing.includes(LOCAL_ONLY_NOTICE_START)) return;
|
|
462
|
+
if (result.dryRun) {
|
|
463
|
+
(existing ? result.updated : result.created).push(filePath);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
await writeTextFile(filePath, `${existing.trimEnd()}
|
|
467
|
+
|
|
468
|
+
${LOCAL_ONLY_NOTICE}
|
|
469
|
+
`);
|
|
470
|
+
(existing ? result.updated : result.created).push(filePath);
|
|
471
|
+
}
|
|
398
472
|
async function resolveComparablePath(filePath) {
|
|
399
473
|
try {
|
|
400
474
|
return await realpath(filePath);
|
|
@@ -420,6 +494,9 @@ async function initializeAgentLoop(options) {
|
|
|
420
494
|
"Refusing to initialize your home directory. Run this inside a project repository, or pass --force if you intentionally want AgentLoopKit files in your home directory."
|
|
421
495
|
);
|
|
422
496
|
}
|
|
497
|
+
if (options.localOnly) {
|
|
498
|
+
await upsertLocalOnlyGitExclude(cwd, result);
|
|
499
|
+
}
|
|
423
500
|
const packageManager = await detectPackageManager(cwd);
|
|
424
501
|
const projectType = await detectProjectType(cwd);
|
|
425
502
|
const projectName = await detectProjectName(cwd);
|
|
@@ -438,7 +515,8 @@ async function initializeAgentLoop(options) {
|
|
|
438
515
|
lintCommand: commands.lint || "not configured",
|
|
439
516
|
typecheckCommand: commands.typecheck || "not configured",
|
|
440
517
|
buildCommand: commands.build || "not configured",
|
|
441
|
-
formatCommand: commands.format || "not configured"
|
|
518
|
+
formatCommand: commands.format || "not configured",
|
|
519
|
+
localOnlyInstructions: options.localOnly ? LOCAL_ONLY_NOTICE : ""
|
|
442
520
|
};
|
|
443
521
|
for (const group of TEMPLATE_GROUPS) {
|
|
444
522
|
if (group === "tasks") continue;
|
|
@@ -480,6 +558,10 @@ async function initializeAgentLoop(options) {
|
|
|
480
558
|
await readTemplate("root/AGENTLOOP.md", values),
|
|
481
559
|
result
|
|
482
560
|
);
|
|
561
|
+
if (options.localOnly) {
|
|
562
|
+
await upsertLocalOnlyNotice(path6.join(cwd, AGENTS_FILE), result);
|
|
563
|
+
await upsertLocalOnlyNotice(path6.join(cwd, AGENTLOOP_FILE), result);
|
|
564
|
+
}
|
|
483
565
|
const configPath = path6.join(cwd, CONFIG_FILE);
|
|
484
566
|
if (await pathExists(configPath)) {
|
|
485
567
|
result.skipped.push(configPath);
|
|
@@ -502,29 +584,43 @@ var consoleLogger = {
|
|
|
502
584
|
|
|
503
585
|
// src/cli/commands/init.ts
|
|
504
586
|
function initCommand() {
|
|
505
|
-
return new Command("init").description("Initialize AgentLoopKit in the current repository").option("--dry-run", "show planned changes without writing files").option("--json", "print machine-readable output").option("--force", "allow initialization when the current directory is your home directory").
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
options.
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
consoleLogger.info(
|
|
524
|
-
consoleLogger.info(
|
|
525
|
-
consoleLogger.info(
|
|
587
|
+
return new Command("init").description("Initialize AgentLoopKit in the current repository").option("--dry-run", "show planned changes without writing files").option("--json", "print machine-readable output").option("--force", "allow initialization when the current directory is your home directory").option(
|
|
588
|
+
"--local-only",
|
|
589
|
+
"keep generated AgentLoopKit files out of git by updating this repo clone .git/info/exclude"
|
|
590
|
+
).action(
|
|
591
|
+
async (options) => {
|
|
592
|
+
const result = await initializeAgentLoop({
|
|
593
|
+
cwd: process.cwd(),
|
|
594
|
+
dryRun: options.dryRun,
|
|
595
|
+
force: options.force,
|
|
596
|
+
localOnly: options.localOnly
|
|
597
|
+
});
|
|
598
|
+
if (options.json) {
|
|
599
|
+
consoleLogger.info(JSON.stringify(result, null, 2));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
consoleLogger.info(
|
|
603
|
+
options.dryRun ? "AgentLoopKit init dry run complete." : "AgentLoopKit initialized."
|
|
604
|
+
);
|
|
605
|
+
consoleLogger.info(`Created: ${result.created.length}`);
|
|
606
|
+
consoleLogger.info(`Updated: ${result.updated.length}`);
|
|
607
|
+
consoleLogger.info(`Skipped: ${result.skipped.length}`);
|
|
608
|
+
if (result.localOnly) {
|
|
609
|
+
consoleLogger.info("\nLocal-only mode:");
|
|
610
|
+
consoleLogger.info(`- Updated exclude file: ${result.localOnly.excludePath}`);
|
|
611
|
+
consoleLogger.info(
|
|
612
|
+
"- AgentLoopKit files stay on disk for local agents but stay out of git status."
|
|
613
|
+
);
|
|
614
|
+
consoleLogger.info("- Undo: remove the agentloopkit:local-only block from .git/info/exclude.");
|
|
615
|
+
}
|
|
616
|
+
if (!options.dryRun) {
|
|
617
|
+
consoleLogger.info("\nNext steps:");
|
|
618
|
+
consoleLogger.info("- Review AGENTS.md and AGENTLOOP.md");
|
|
619
|
+
consoleLogger.info("- Run agentloop doctor");
|
|
620
|
+
consoleLogger.info("- Create a task with agentloop create-task");
|
|
621
|
+
}
|
|
526
622
|
}
|
|
527
|
-
|
|
623
|
+
);
|
|
528
624
|
}
|
|
529
625
|
|
|
530
626
|
// src/cli/commands/doctor.ts
|
|
@@ -1268,7 +1364,7 @@ import { readFile as readFile8 } from "fs/promises";
|
|
|
1268
1364
|
|
|
1269
1365
|
// src/core/artifacts.ts
|
|
1270
1366
|
import path11 from "path";
|
|
1271
|
-
import { readdir as readdir4, stat as
|
|
1367
|
+
import { readdir as readdir4, stat as stat3 } from "fs/promises";
|
|
1272
1368
|
var verificationReportPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-verification-report\.md$/;
|
|
1273
1369
|
var prSummaryPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-pr-summary\.md$/;
|
|
1274
1370
|
var ciSummaryPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-ci-summary\.md$/;
|
|
@@ -1279,7 +1375,7 @@ async function latestMarkdownFile(dir, options = {}) {
|
|
|
1279
1375
|
(entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name.toLowerCase() !== "readme.md"
|
|
1280
1376
|
).filter((entry) => !options.pattern || options.pattern.test(entry.name)).map(async (entry) => {
|
|
1281
1377
|
const filePath = path11.join(dir, entry.name);
|
|
1282
|
-
const fileStat = await
|
|
1378
|
+
const fileStat = await stat3(filePath);
|
|
1283
1379
|
return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
|
|
1284
1380
|
})
|
|
1285
1381
|
);
|
|
@@ -1292,7 +1388,7 @@ async function latestMarkdownFile(dir, options = {}) {
|
|
|
1292
1388
|
|
|
1293
1389
|
// src/core/task-state.ts
|
|
1294
1390
|
import path12 from "path";
|
|
1295
|
-
import { mkdir as mkdir2, readdir as readdir5, readFile as readFile7, rename, rm, stat as
|
|
1391
|
+
import { mkdir as mkdir2, readdir as readdir5, readFile as readFile7, rename, rm, stat as stat4 } from "fs/promises";
|
|
1296
1392
|
var TASK_STATUSES = ["proposed", "in-progress", "blocked", "review", "done"];
|
|
1297
1393
|
function statePath(cwd, config) {
|
|
1298
1394
|
return path12.join(cwd, config.paths.agentloopDir, "state.json");
|
|
@@ -1337,7 +1433,7 @@ async function resolveTaskPath(options) {
|
|
|
1337
1433
|
if (!options.strict) return void 0;
|
|
1338
1434
|
throw new AgentLoopError("Active task must be a Markdown file.");
|
|
1339
1435
|
}
|
|
1340
|
-
const fileStat = await
|
|
1436
|
+
const fileStat = await stat4(absolutePath).catch(() => void 0);
|
|
1341
1437
|
if (!fileStat?.isFile()) {
|
|
1342
1438
|
if (!options.strict) return void 0;
|
|
1343
1439
|
throw new AgentLoopError(`Task contract not found: ${options.taskPath}`);
|
|
@@ -1444,7 +1540,7 @@ async function listTasks(options) {
|
|
|
1444
1540
|
const filePath = path12.join(root, entry.name);
|
|
1445
1541
|
const [metadata, fileStat] = await Promise.all([
|
|
1446
1542
|
readTaskMetadata(options.cwd, filePath),
|
|
1447
|
-
|
|
1543
|
+
stat4(filePath)
|
|
1448
1544
|
]);
|
|
1449
1545
|
return {
|
|
1450
1546
|
...metadata,
|
|
@@ -1776,7 +1872,7 @@ import { Command as Command9 } from "commander";
|
|
|
1776
1872
|
|
|
1777
1873
|
// src/core/status.ts
|
|
1778
1874
|
import path15 from "path";
|
|
1779
|
-
import { readFile as readFile9, stat as
|
|
1875
|
+
import { readFile as readFile9, stat as stat5 } from "fs/promises";
|
|
1780
1876
|
function extractHeading2(markdown, fallback) {
|
|
1781
1877
|
return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
|
|
1782
1878
|
}
|
|
@@ -1789,7 +1885,7 @@ function extractOverallStatus(markdown) {
|
|
|
1789
1885
|
async function readTask(cwd, filePath) {
|
|
1790
1886
|
if (!filePath) return void 0;
|
|
1791
1887
|
const markdown = await readFile9(filePath, "utf8");
|
|
1792
|
-
const fileStat = await
|
|
1888
|
+
const fileStat = await stat5(filePath);
|
|
1793
1889
|
return {
|
|
1794
1890
|
path: path15.relative(cwd, filePath),
|
|
1795
1891
|
title: extractHeading2(markdown, path15.basename(filePath, ".md")),
|
|
@@ -1800,7 +1896,7 @@ async function readTask(cwd, filePath) {
|
|
|
1800
1896
|
async function readReport(cwd, filePath) {
|
|
1801
1897
|
if (!filePath) return void 0;
|
|
1802
1898
|
const markdown = await readFile9(filePath, "utf8");
|
|
1803
|
-
const fileStat = await
|
|
1899
|
+
const fileStat = await stat5(filePath);
|
|
1804
1900
|
return {
|
|
1805
1901
|
path: path15.relative(cwd, filePath),
|
|
1806
1902
|
title: extractHeading2(markdown, path15.basename(filePath, ".md")),
|
|
@@ -2114,6 +2210,7 @@ var topLevelCommands = [
|
|
|
2114
2210
|
["ci-summary", "Summarize CI context and AgentLoop evidence"],
|
|
2115
2211
|
["release-notes", "Generate deterministic release notes"],
|
|
2116
2212
|
["npm-status", "Check npm registry catch-up status"],
|
|
2213
|
+
["mcp-server", "Start the read-only MCP stdio server"],
|
|
2117
2214
|
["policy", "List or inspect local AgentLoopKit policies"],
|
|
2118
2215
|
["task", "List, inspect, update, or archive task contracts"],
|
|
2119
2216
|
["install-agent", "Install agent-specific instructions"],
|
|
@@ -2566,7 +2663,7 @@ import { Command as Command14 } from "commander";
|
|
|
2566
2663
|
|
|
2567
2664
|
// src/core/html-report.ts
|
|
2568
2665
|
import path17 from "path";
|
|
2569
|
-
import { readFile as readFile11, readdir as readdir6, stat as
|
|
2666
|
+
import { readFile as readFile11, readdir as readdir6, stat as stat6 } from "fs/promises";
|
|
2570
2667
|
function escapeHtml(value) {
|
|
2571
2668
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2572
2669
|
}
|
|
@@ -2598,7 +2695,7 @@ async function latestMatchingMarkdownFile(dir, pattern) {
|
|
|
2598
2695
|
const entries = await Promise.all(
|
|
2599
2696
|
(await readdir6(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && pattern.test(entry.name)).map(async (entry) => {
|
|
2600
2697
|
const filePath = path17.join(dir, entry.name);
|
|
2601
|
-
const fileStat = await
|
|
2698
|
+
const fileStat = await stat6(filePath);
|
|
2602
2699
|
return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
|
|
2603
2700
|
})
|
|
2604
2701
|
);
|
|
@@ -2827,7 +2924,7 @@ import { Command as Command15 } from "commander";
|
|
|
2827
2924
|
|
|
2828
2925
|
// src/core/badge.ts
|
|
2829
2926
|
import path18 from "path";
|
|
2830
|
-
import { readFile as readFile12, readdir as readdir7, stat as
|
|
2927
|
+
import { readFile as readFile12, readdir as readdir7, stat as stat7 } from "fs/promises";
|
|
2831
2928
|
var verificationReportPattern2 = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-verification-report\.md$/;
|
|
2832
2929
|
function escapeXml(value) {
|
|
2833
2930
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -2858,7 +2955,7 @@ async function latestVerificationReport(dir) {
|
|
|
2858
2955
|
const entries = await Promise.all(
|
|
2859
2956
|
(await readdir7(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && verificationReportPattern2.test(entry.name)).map(async (entry) => {
|
|
2860
2957
|
const filePath = path18.join(dir, entry.name);
|
|
2861
|
-
const fileStat = await
|
|
2958
|
+
const fileStat = await stat7(filePath);
|
|
2862
2959
|
return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
|
|
2863
2960
|
})
|
|
2864
2961
|
);
|
|
@@ -2996,7 +3093,7 @@ import { Command as Command16 } from "commander";
|
|
|
2996
3093
|
|
|
2997
3094
|
// src/core/policy.ts
|
|
2998
3095
|
import path19 from "path";
|
|
2999
|
-
import { readdir as readdir8, readFile as readFile13, stat as
|
|
3096
|
+
import { readdir as readdir8, readFile as readFile13, stat as stat8 } from "fs/promises";
|
|
3000
3097
|
function policyRoot(cwd, config) {
|
|
3001
3098
|
return path19.resolve(cwd, config.paths.agentloopDir, "policies");
|
|
3002
3099
|
}
|
|
@@ -3013,7 +3110,7 @@ function normalizeContent(content) {
|
|
|
3013
3110
|
return content.replace(/\r\n/g, "\n");
|
|
3014
3111
|
}
|
|
3015
3112
|
async function ensurePolicyRoot(root) {
|
|
3016
|
-
const rootStat = await
|
|
3113
|
+
const rootStat = await stat8(root).catch(() => void 0);
|
|
3017
3114
|
if (!rootStat?.isDirectory()) {
|
|
3018
3115
|
throw new AgentLoopError(
|
|
3019
3116
|
"No AgentLoopKit policy files found. Run `agentloop init` to generate .agentloop/policies/."
|
|
@@ -3806,8 +3903,261 @@ function npmStatusCommand() {
|
|
|
3806
3903
|
);
|
|
3807
3904
|
}
|
|
3808
3905
|
|
|
3906
|
+
// src/cli/commands/mcp-server.ts
|
|
3907
|
+
import { Command as Command20 } from "commander";
|
|
3908
|
+
|
|
3909
|
+
// src/mcp/server.ts
|
|
3910
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3911
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3912
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3913
|
+
|
|
3914
|
+
// src/core/mcp-tools.ts
|
|
3915
|
+
import path23 from "path";
|
|
3916
|
+
import { readdir as readdir9, readFile as readFile18, stat as stat9 } from "fs/promises";
|
|
3917
|
+
var emptyInputSchema = {
|
|
3918
|
+
type: "object",
|
|
3919
|
+
properties: {},
|
|
3920
|
+
additionalProperties: false
|
|
3921
|
+
};
|
|
3922
|
+
var tools = [
|
|
3923
|
+
{
|
|
3924
|
+
name: "agentloop_status",
|
|
3925
|
+
description: "Read AgentLoopKit status, active task, latest verification, git state, and next action.",
|
|
3926
|
+
inputSchema: emptyInputSchema
|
|
3927
|
+
},
|
|
3928
|
+
{
|
|
3929
|
+
name: "agentloop_next",
|
|
3930
|
+
description: "Read only the next recommended AgentLoopKit action and reason.",
|
|
3931
|
+
inputSchema: emptyInputSchema
|
|
3932
|
+
},
|
|
3933
|
+
{
|
|
3934
|
+
name: "agentloop_list_tasks",
|
|
3935
|
+
description: "List local task contracts under the configured AgentLoopKit task directory.",
|
|
3936
|
+
inputSchema: emptyInputSchema
|
|
3937
|
+
},
|
|
3938
|
+
{
|
|
3939
|
+
name: "agentloop_show_active_task",
|
|
3940
|
+
description: "Read the active task contract content when one is pinned.",
|
|
3941
|
+
inputSchema: emptyInputSchema
|
|
3942
|
+
},
|
|
3943
|
+
{
|
|
3944
|
+
name: "agentloop_list_policies",
|
|
3945
|
+
description: "List local AgentLoopKit safety policy files.",
|
|
3946
|
+
inputSchema: emptyInputSchema
|
|
3947
|
+
},
|
|
3948
|
+
{
|
|
3949
|
+
name: "agentloop_read_policy",
|
|
3950
|
+
description: "Read one local AgentLoopKit policy by policy name.",
|
|
3951
|
+
inputSchema: {
|
|
3952
|
+
type: "object",
|
|
3953
|
+
properties: {
|
|
3954
|
+
policyName: {
|
|
3955
|
+
type: "string",
|
|
3956
|
+
description: 'Policy name, for example "security" or "security-policy.md".'
|
|
3957
|
+
}
|
|
3958
|
+
},
|
|
3959
|
+
required: ["policyName"],
|
|
3960
|
+
additionalProperties: false
|
|
3961
|
+
}
|
|
3962
|
+
},
|
|
3963
|
+
{
|
|
3964
|
+
name: "agentloop_latest_verification_report",
|
|
3965
|
+
description: "Read the latest local verification report metadata and Markdown content.",
|
|
3966
|
+
inputSchema: emptyInputSchema
|
|
3967
|
+
},
|
|
3968
|
+
{
|
|
3969
|
+
name: "agentloop_list_handoffs",
|
|
3970
|
+
description: "List recent local reviewer handoff summaries.",
|
|
3971
|
+
inputSchema: {
|
|
3972
|
+
type: "object",
|
|
3973
|
+
properties: {
|
|
3974
|
+
limit: {
|
|
3975
|
+
type: "number",
|
|
3976
|
+
minimum: 1,
|
|
3977
|
+
maximum: 50,
|
|
3978
|
+
description: "Maximum handoff summaries to return. Defaults to 20."
|
|
3979
|
+
}
|
|
3980
|
+
},
|
|
3981
|
+
additionalProperties: false
|
|
3982
|
+
}
|
|
3983
|
+
},
|
|
3984
|
+
{
|
|
3985
|
+
name: "agentloop_latest_handoff",
|
|
3986
|
+
description: "Read the latest local reviewer handoff summary Markdown content.",
|
|
3987
|
+
inputSchema: emptyInputSchema
|
|
3988
|
+
}
|
|
3989
|
+
];
|
|
3990
|
+
function listMcpTools() {
|
|
3991
|
+
return [...tools];
|
|
3992
|
+
}
|
|
3993
|
+
function textResult(payload) {
|
|
3994
|
+
return {
|
|
3995
|
+
payload,
|
|
3996
|
+
content: [
|
|
3997
|
+
{
|
|
3998
|
+
type: "text",
|
|
3999
|
+
text: JSON.stringify(payload, null, 2)
|
|
4000
|
+
}
|
|
4001
|
+
]
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
function extractHeading7(markdown, fallback) {
|
|
4005
|
+
return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
|
|
4006
|
+
}
|
|
4007
|
+
function toStoredPath3(cwd, absolutePath) {
|
|
4008
|
+
return path23.relative(cwd, absolutePath).split(path23.sep).join("/");
|
|
4009
|
+
}
|
|
4010
|
+
async function readMarkdownArtifact(cwd, filePath, key) {
|
|
4011
|
+
if (!filePath) return { [key]: null };
|
|
4012
|
+
const content = await readFile18(filePath, "utf8");
|
|
4013
|
+
const fileStat = await stat9(filePath);
|
|
4014
|
+
return {
|
|
4015
|
+
[key]: {
|
|
4016
|
+
path: toStoredPath3(cwd, filePath),
|
|
4017
|
+
title: extractHeading7(content, path23.basename(filePath, ".md")),
|
|
4018
|
+
modifiedAt: fileStat.mtime.toISOString(),
|
|
4019
|
+
content
|
|
4020
|
+
}
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
async function listHandoffs(cwd, handoffsDir, limit) {
|
|
4024
|
+
if (!await pathExists(handoffsDir)) return [];
|
|
4025
|
+
const entries = await readdir9(handoffsDir, { withFileTypes: true });
|
|
4026
|
+
const handoffs = await Promise.all(
|
|
4027
|
+
entries.filter((entry) => entry.isFile() && prSummaryPattern.test(entry.name)).map(async (entry) => {
|
|
4028
|
+
const filePath = path23.join(handoffsDir, entry.name);
|
|
4029
|
+
const [content, fileStat] = await Promise.all([readFile18(filePath, "utf8"), stat9(filePath)]);
|
|
4030
|
+
return {
|
|
4031
|
+
path: toStoredPath3(cwd, filePath),
|
|
4032
|
+
title: extractHeading7(content, path23.basename(filePath, ".md")),
|
|
4033
|
+
modifiedAt: fileStat.mtime.toISOString(),
|
|
4034
|
+
modifiedMs: fileStat.mtimeMs
|
|
4035
|
+
};
|
|
4036
|
+
})
|
|
4037
|
+
);
|
|
4038
|
+
return handoffs.sort((left, right) => {
|
|
4039
|
+
if (left.modifiedMs !== right.modifiedMs) return right.modifiedMs - left.modifiedMs;
|
|
4040
|
+
return left.path.localeCompare(right.path);
|
|
4041
|
+
}).slice(0, limit).map((handoff) => ({
|
|
4042
|
+
path: handoff.path,
|
|
4043
|
+
title: handoff.title,
|
|
4044
|
+
modifiedAt: handoff.modifiedAt
|
|
4045
|
+
}));
|
|
4046
|
+
}
|
|
4047
|
+
function readStringArgument(args, key) {
|
|
4048
|
+
const value = args?.[key];
|
|
4049
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
4050
|
+
throw new AgentLoopError(`MCP tool argument "${key}" must be a non-empty string.`);
|
|
4051
|
+
}
|
|
4052
|
+
return value;
|
|
4053
|
+
}
|
|
4054
|
+
function readLimitArgument(args) {
|
|
4055
|
+
const value = args?.limit;
|
|
4056
|
+
if (value === void 0) return 20;
|
|
4057
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 50) {
|
|
4058
|
+
throw new AgentLoopError('MCP tool argument "limit" must be an integer from 1 to 50.');
|
|
4059
|
+
}
|
|
4060
|
+
return value;
|
|
4061
|
+
}
|
|
4062
|
+
async function callMcpTool(options) {
|
|
4063
|
+
const config = await loadAgentLoopConfig(options.cwd);
|
|
4064
|
+
switch (options.name) {
|
|
4065
|
+
case "agentloop_status": {
|
|
4066
|
+
const status = await getAgentLoopStatus({ cwd: options.cwd, config });
|
|
4067
|
+
return textResult(status);
|
|
4068
|
+
}
|
|
4069
|
+
case "agentloop_next": {
|
|
4070
|
+
const status = await getAgentLoopStatus({ cwd: options.cwd, config });
|
|
4071
|
+
return textResult(status.nextAction);
|
|
4072
|
+
}
|
|
4073
|
+
case "agentloop_list_tasks": {
|
|
4074
|
+
return textResult({ tasks: await listTasks({ cwd: options.cwd, config }) });
|
|
4075
|
+
}
|
|
4076
|
+
case "agentloop_show_active_task": {
|
|
4077
|
+
const activeTask = await getActiveTask({ cwd: options.cwd, config });
|
|
4078
|
+
if (!activeTask) return textResult({ task: null });
|
|
4079
|
+
const task = await readTaskContract({ cwd: options.cwd, config, taskPath: activeTask.path });
|
|
4080
|
+
return textResult({ task });
|
|
4081
|
+
}
|
|
4082
|
+
case "agentloop_list_policies": {
|
|
4083
|
+
return textResult({ policies: await listPolicies({ cwd: options.cwd, config }) });
|
|
4084
|
+
}
|
|
4085
|
+
case "agentloop_read_policy": {
|
|
4086
|
+
const policyName = readStringArgument(options.arguments, "policyName");
|
|
4087
|
+
return textResult({
|
|
4088
|
+
policy: await readPolicy({ cwd: options.cwd, config, policyName })
|
|
4089
|
+
});
|
|
4090
|
+
}
|
|
4091
|
+
case "agentloop_latest_verification_report": {
|
|
4092
|
+
const reportPath = await latestMarkdownFile(
|
|
4093
|
+
path23.join(options.cwd, config.paths.reportsDir),
|
|
4094
|
+
{
|
|
4095
|
+
pattern: verificationReportPattern
|
|
4096
|
+
}
|
|
4097
|
+
);
|
|
4098
|
+
return textResult(await readMarkdownArtifact(options.cwd, reportPath, "report"));
|
|
4099
|
+
}
|
|
4100
|
+
case "agentloop_list_handoffs": {
|
|
4101
|
+
const limit = readLimitArgument(options.arguments);
|
|
4102
|
+
return textResult({
|
|
4103
|
+
handoffs: await listHandoffs(
|
|
4104
|
+
options.cwd,
|
|
4105
|
+
path23.join(options.cwd, config.paths.handoffsDir),
|
|
4106
|
+
limit
|
|
4107
|
+
)
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
4110
|
+
case "agentloop_latest_handoff": {
|
|
4111
|
+
const handoffPath = await latestMarkdownFile(
|
|
4112
|
+
path23.join(options.cwd, config.paths.handoffsDir),
|
|
4113
|
+
{
|
|
4114
|
+
pattern: prSummaryPattern
|
|
4115
|
+
}
|
|
4116
|
+
);
|
|
4117
|
+
return textResult(await readMarkdownArtifact(options.cwd, handoffPath, "handoff"));
|
|
4118
|
+
}
|
|
4119
|
+
default:
|
|
4120
|
+
throw new AgentLoopError(`Unknown MCP tool: ${options.name}`);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
// src/mcp/server.ts
|
|
4125
|
+
async function startAgentLoopMcpServer(options) {
|
|
4126
|
+
const server = new Server(
|
|
4127
|
+
{
|
|
4128
|
+
name: "agentloopkit",
|
|
4129
|
+
version: getPackageVersion()
|
|
4130
|
+
},
|
|
4131
|
+
{
|
|
4132
|
+
capabilities: {
|
|
4133
|
+
tools: {}
|
|
4134
|
+
},
|
|
4135
|
+
instructions: "Read-only AgentLoopKit server. Exposes local task contracts, policies, verification reports, handoffs, status, and next action. It does not run verification commands, read env file contents, call external APIs, upload files, or mutate repository files."
|
|
4136
|
+
}
|
|
4137
|
+
);
|
|
4138
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
4139
|
+
tools: listMcpTools()
|
|
4140
|
+
}));
|
|
4141
|
+
server.setRequestHandler(
|
|
4142
|
+
CallToolRequestSchema,
|
|
4143
|
+
async (request) => callMcpTool({
|
|
4144
|
+
cwd: options.cwd,
|
|
4145
|
+
name: request.params.name,
|
|
4146
|
+
arguments: request.params.arguments
|
|
4147
|
+
})
|
|
4148
|
+
);
|
|
4149
|
+
await server.connect(new StdioServerTransport());
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
// src/cli/commands/mcp-server.ts
|
|
4153
|
+
function mcpServerCommand() {
|
|
4154
|
+
return new Command20("mcp-server").description("Start the read-only AgentLoopKit MCP stdio server").action(async () => {
|
|
4155
|
+
await startAgentLoopMcpServer({ cwd: process.cwd() });
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
|
|
3809
4159
|
// src/cli/index.ts
|
|
3810
|
-
var program = new
|
|
4160
|
+
var program = new Command21();
|
|
3811
4161
|
program.name("agentloop").description("A drop-in engineering loop for coding agents.").version(getPackageVersion(), "-V, --version", "print CLI version");
|
|
3812
4162
|
program.addCommand(initCommand());
|
|
3813
4163
|
program.addCommand(doctorCommand());
|
|
@@ -3823,6 +4173,7 @@ program.addCommand(badgeCommand());
|
|
|
3823
4173
|
program.addCommand(ciSummaryCommand());
|
|
3824
4174
|
program.addCommand(releaseNotesCommand());
|
|
3825
4175
|
program.addCommand(npmStatusCommand());
|
|
4176
|
+
program.addCommand(mcpServerCommand());
|
|
3826
4177
|
program.addCommand(policyCommand());
|
|
3827
4178
|
program.addCommand(taskCommand());
|
|
3828
4179
|
program.addCommand(installAgentCommand());
|