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/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 Command20 } from "commander";
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").action(async (options) => {
506
- const result = await initializeAgentLoop({
507
- cwd: process.cwd(),
508
- dryRun: options.dryRun,
509
- force: options.force
510
- });
511
- if (options.json) {
512
- consoleLogger.info(JSON.stringify(result, null, 2));
513
- return;
514
- }
515
- consoleLogger.info(
516
- options.dryRun ? "AgentLoopKit init dry run complete." : "AgentLoopKit initialized."
517
- );
518
- consoleLogger.info(`Created: ${result.created.length}`);
519
- consoleLogger.info(`Updated: ${result.updated.length}`);
520
- consoleLogger.info(`Skipped: ${result.skipped.length}`);
521
- if (!options.dryRun) {
522
- consoleLogger.info("\nNext steps:");
523
- consoleLogger.info("- Review AGENTS.md and AGENTLOOP.md");
524
- consoleLogger.info("- Run agentloop doctor");
525
- consoleLogger.info("- Create a task with agentloop create-task");
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 stat2 } from "fs/promises";
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 stat2(filePath);
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 stat3 } from "fs/promises";
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 stat3(absolutePath).catch(() => void 0);
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
- stat3(filePath)
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 stat4 } from "fs/promises";
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 stat4(filePath);
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 stat4(filePath);
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 stat5 } from "fs/promises";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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 stat5(filePath);
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 stat6 } from "fs/promises";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -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 stat6(filePath);
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 stat7 } from "fs/promises";
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 stat7(root).catch(() => void 0);
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 Command20();
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());