agentloopkit 0.1.1 → 0.24.1

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command9 } from "commander";
4
+ import { Command as Command20 } from "commander";
5
5
 
6
6
  // src/cli/commands/init.ts
7
7
  import { Command } from "commander";
@@ -11,8 +11,11 @@ import path6 from "path";
11
11
  import { readdir as readdir3 } from "fs/promises";
12
12
 
13
13
  // src/core/constants.ts
14
+ var PACKAGE_NAME = "agentloopkit";
14
15
  var CONFIG_FILE = "agentloop.config.json";
15
16
  var AGENTLOOP_DIR = ".agentloop";
17
+ var AGENTLOOP_MANIFEST_FILE = ".agentloop/manifest.json";
18
+ var CURRENT_TEMPLATE_VERSION = 1;
16
19
  var AGENTS_FILE = "AGENTS.md";
17
20
  var AGENTLOOP_FILE = "AGENTLOOP.md";
18
21
  var TEMPLATE_GROUPS = [
@@ -116,9 +119,10 @@ var AgentLoopConfigSchema = z.object({
116
119
  includeRollback: z.boolean()
117
120
  })
118
121
  });
122
+ var CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/abhiyoheswaran1/AgentLoopKit/main/schema/agentloop.config.schema.json";
119
123
  function createDefaultConfig(input = {}) {
120
124
  return {
121
- $schema: "https://agentloopkit.dev/schema/agentloop.config.schema.json",
125
+ $schema: CONFIG_SCHEMA_URL,
122
126
  version: 1,
123
127
  project: {
124
128
  name: input.name ?? "",
@@ -194,7 +198,7 @@ async function listFilesRecursive(root, options = {}) {
194
198
  const files = [];
195
199
  async function walk(current) {
196
200
  if (!await pathExists(current)) return;
197
- const entries = await readdir(current, { withFileTypes: true });
201
+ const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
198
202
  for (const entry of entries) {
199
203
  if (ignore.has(entry.name)) continue;
200
204
  const absolute = path2.join(current, entry.name);
@@ -236,6 +240,13 @@ function packageManagerRunCommand(manager, scriptName) {
236
240
  // src/core/project-detection.ts
237
241
  import { readFile as readFile4 } from "fs/promises";
238
242
  import path4 from "path";
243
+ var MONOREPO_FILE_MARKERS = [
244
+ "pnpm-workspace.yaml",
245
+ "turbo.json",
246
+ "nx.json",
247
+ "lerna.json",
248
+ "rush.json"
249
+ ];
239
250
  async function readPackageJson(cwd) {
240
251
  const filePath = path4.join(cwd, "package.json");
241
252
  if (!await pathExists(filePath)) return void 0;
@@ -265,6 +276,19 @@ async function detectProjectType(cwd) {
265
276
  if (hasDocs && !hasCode) return "docs-only";
266
277
  return "generic";
267
278
  }
279
+ async function detectMonorepo(cwd) {
280
+ const markers = [];
281
+ const packageJson = await readPackageJson(cwd);
282
+ if (packageJson?.workspaces) {
283
+ markers.push("package.json workspaces");
284
+ }
285
+ for (const marker of MONOREPO_FILE_MARKERS) {
286
+ if (await pathExists(path4.join(cwd, marker))) {
287
+ markers.push(marker);
288
+ }
289
+ }
290
+ return { detected: markers.length > 0, markers };
291
+ }
268
292
  async function detectPackageScripts(cwd, packageManager) {
269
293
  const packageJson = await readPackageJson(cwd);
270
294
  const commands = { test: "", lint: "", typecheck: "", build: "", format: "" };
@@ -289,8 +313,8 @@ function renderTemplateString(template, values) {
289
313
  function getTemplateRoot() {
290
314
  return fileURLToPath(new URL("../templates", import.meta.url));
291
315
  }
292
- async function readTemplate(relativePath, values = {}) {
293
- const raw = await readFile5(path5.join(getTemplateRoot(), relativePath), "utf8");
316
+ async function readTemplate(relativePath2, values = {}) {
317
+ const raw = await readFile5(path5.join(getTemplateRoot(), relativePath2), "utf8");
294
318
  return renderTemplateString(raw, values);
295
319
  }
296
320
  async function listTemplateFiles() {
@@ -396,6 +420,20 @@ async function initializeAgentLoop(options) {
396
420
  await readTemplate("root/agentloop-directory-readme.md", values),
397
421
  result
398
422
  );
423
+ await writeGeneratedFile(
424
+ path6.join(cwd, AGENTLOOP_MANIFEST_FILE),
425
+ `${JSON.stringify(
426
+ {
427
+ version: 1,
428
+ templateVersion: CURRENT_TEMPLATE_VERSION,
429
+ generatedBy: PACKAGE_NAME
430
+ },
431
+ null,
432
+ 2
433
+ )}
434
+ `,
435
+ result
436
+ );
399
437
  await writeGeneratedFile(
400
438
  path6.join(cwd, AGENTLOOP_DIR, "tasks", "README.md"),
401
439
  await readTemplate("tasks/README.md", values),
@@ -500,14 +538,25 @@ import path7 from "path";
500
538
  function relative(cwd, file) {
501
539
  return path7.relative(cwd, file).replaceAll(path7.sep, "/");
502
540
  }
541
+ function isSemanticRiskCandidate(file) {
542
+ const extension = path7.extname(file).toLowerCase();
543
+ if ([".md", ".mdx", ".txt", ".rst", ".adoc"].includes(extension)) return false;
544
+ return true;
545
+ }
503
546
  async function detectRiskFiles(cwd) {
504
547
  const files = (await listFilesRecursive(cwd)).map((file) => relative(cwd, file));
505
- const includes = (needles) => files.filter((file) => needles.some((needle) => file.toLowerCase().includes(needle)));
548
+ const semanticIncludes = (needles) => files.filter(
549
+ (file) => isSemanticRiskCandidate(file) && needles.some((needle) => file.toLowerCase().includes(needle))
550
+ );
551
+ const migrationFiles = files.filter((file) => {
552
+ const normalized = file.toLowerCase();
553
+ return isSemanticRiskCandidate(file) && (normalized.includes("migrations/") || normalized.includes("/migration/") || path7.basename(normalized).includes("migration"));
554
+ });
506
555
  return {
507
- migrations: includes(["migration", "migrations/"]),
508
- auth: includes(["auth", "oauth", "session", "passport"]),
509
- security: includes(["security", "crypto", "secret", "permission", "policy"]),
510
- billing: includes(["billing", "stripe", "payment", "invoice"]),
556
+ migrations: migrationFiles,
557
+ auth: semanticIncludes(["auth", "oauth", "session", "passport"]),
558
+ security: semanticIncludes(["security", "crypto", "secret", "permission", "policy"]),
559
+ billing: semanticIncludes(["billing", "stripe", "payment", "invoice"]),
511
560
  deployment: files.filter(
512
561
  (file) => /(^|\/)(Dockerfile|docker-compose|vercel\.json|netlify\.toml|fly\.toml|render\.yaml|\.github\/workflows\/)/i.test(
513
562
  file
@@ -523,9 +572,57 @@ async function detectRiskFiles(cwd) {
523
572
  }
524
573
 
525
574
  // src/core/doctor.ts
575
+ var MONOREPO_VERIFICATION_GUIDANCE = "Root checks may not cover every package; add package-specific verification commands to the task contract, such as pnpm --filter <package> test, npm --workspace <package> test, or cd packages/<name> && npm test. AgentLoopKit does not run workspace commands automatically.";
576
+ var MISSING_MANIFEST_MESSAGE = "missing .agentloop/manifest.json; run agentloop init with the current CLI to add missing files without overwriting existing harness files";
577
+ var INVALID_MANIFEST_MESSAGE = "invalid .agentloop/manifest.json; review docs/template-migrations.md and recreate the manifest with agentloop init if needed";
526
578
  function check(name, status, message) {
527
579
  return { name, status, message };
528
580
  }
581
+ var RISK_CATEGORY_LABELS = {
582
+ migrations: "migrations",
583
+ auth: "auth",
584
+ security: "security",
585
+ billing: "billing",
586
+ deployment: "deployment",
587
+ lockfiles: "lockfiles",
588
+ envFiles: "env files"
589
+ };
590
+ function formatRiskFiles(files) {
591
+ const preview = files.slice(0, 3).join(", ");
592
+ const remaining = files.length - 3;
593
+ return `${files.length} detected: ${preview}${remaining > 0 ? ` (+${remaining} more)` : ""}`;
594
+ }
595
+ async function checkTemplateManifest(cwd) {
596
+ const manifest = await readTextIfExists(path8.join(cwd, AGENTLOOP_MANIFEST_FILE));
597
+ if (!manifest) return check("Template manifest", "warn", MISSING_MANIFEST_MESSAGE);
598
+ try {
599
+ const parsed = JSON.parse(manifest);
600
+ if (parsed.version !== 1 || typeof parsed.templateVersion !== "number" || parsed.generatedBy !== "agentloopkit") {
601
+ return check("Template manifest", "warn", INVALID_MANIFEST_MESSAGE);
602
+ }
603
+ if (parsed.templateVersion < CURRENT_TEMPLATE_VERSION) {
604
+ return check(
605
+ "Template manifest",
606
+ "warn",
607
+ `template version ${parsed.templateVersion} is older than current version ${CURRENT_TEMPLATE_VERSION}; review docs/template-migrations.md and rerun agentloop init to add missing files`
608
+ );
609
+ }
610
+ if (parsed.templateVersion > CURRENT_TEMPLATE_VERSION) {
611
+ return check(
612
+ "Template manifest",
613
+ "warn",
614
+ `template version ${parsed.templateVersion} is newer than this CLI supports; upgrade AgentLoopKit before changing generated harness files`
615
+ );
616
+ }
617
+ return check(
618
+ "Template manifest",
619
+ "pass",
620
+ `template version ${CURRENT_TEMPLATE_VERSION} is current`
621
+ );
622
+ } catch {
623
+ return check("Template manifest", "warn", INVALID_MANIFEST_MESSAGE);
624
+ }
625
+ }
529
626
  async function runDoctor(options) {
530
627
  const cwd = options.cwd;
531
628
  const checks = [];
@@ -558,6 +655,7 @@ async function runDoctor(options) {
558
655
  const exists = await pathExists(path8.join(cwd, file));
559
656
  checks.push(check(file, exists ? "pass" : "warn", exists ? "found" : "missing"));
560
657
  }
658
+ checks.push(await checkTemplateManifest(cwd));
561
659
  try {
562
660
  await loadAgentLoopConfig(cwd);
563
661
  checks.push(check(CONFIG_FILE, "pass", "valid"));
@@ -574,9 +672,17 @@ async function runDoctor(options) {
574
672
  }
575
673
  const packageManager = await detectPackageManager(cwd);
576
674
  const projectType = await detectProjectType(cwd);
675
+ const monorepo = await detectMonorepo(cwd);
577
676
  const commands = await detectPackageScripts(cwd, packageManager);
578
677
  checks.push(check("Package manager", "pass", packageManager));
579
678
  checks.push(check("Project type", "pass", projectType));
679
+ checks.push(
680
+ check(
681
+ "Monorepo",
682
+ monorepo.detected ? "warn" : "pass",
683
+ monorepo.detected ? `workspace markers detected: ${monorepo.markers.join(", ")}. ${MONOREPO_VERIFICATION_GUIDANCE}` : "not detected"
684
+ )
685
+ );
580
686
  for (const key of ["test", "lint", "typecheck", "build"]) {
581
687
  checks.push(
582
688
  check(`${key} command`, commands[key] ? "pass" : "warn", commands[key] || "not detected")
@@ -591,6 +697,16 @@ async function runDoctor(options) {
591
697
  riskCount ? `${riskCount} risk file(s) detected` : "none detected"
592
698
  )
593
699
  );
700
+ for (const [category, files] of Object.entries(risks)) {
701
+ if (files.length === 0) continue;
702
+ checks.push(
703
+ check(
704
+ `Risk files: ${RISK_CATEGORY_LABELS[category]}`,
705
+ "warn",
706
+ formatRiskFiles(files)
707
+ )
708
+ );
709
+ }
594
710
  if (!commands.test) {
595
711
  checks.push(check("Tests", "warn", "no test command detected"));
596
712
  }
@@ -704,16 +820,26 @@ ${input.rollbackNotes || "Document how to revert or disable this change."}
704
820
  }
705
821
  async function createTaskContractFile(options) {
706
822
  const createdDate = options.input.createdDate ?? formatDate();
707
- const relativePath = options.out ?? path9.join(options.config.paths.tasksDir, `${createdDate}-${slugify(options.input.title)}.md`);
708
- const absolutePath = path9.isAbsolute(relativePath) ? relativePath : path9.join(options.cwd, relativePath);
823
+ const relativePath2 = options.out ?? path9.join(options.config.paths.tasksDir, `${createdDate}-${slugify(options.input.title)}.md`);
824
+ const absolutePath = path9.isAbsolute(relativePath2) ? relativePath2 : path9.join(options.cwd, relativePath2);
709
825
  const markdown = generateTaskContract({ ...options.input, createdDate });
710
826
  await writeTextFile(absolutePath, markdown);
711
827
  return { path: absolutePath, markdown };
712
828
  }
713
829
 
714
830
  // src/cli/commands/create-task.ts
715
- function lines(value) {
716
- return value ? value.split("\n").map((line) => line.trim()).filter(Boolean) : [];
831
+ function lines(value, previous) {
832
+ const current = value ? value.split("\n").map((line) => line.trim()).filter(Boolean) : [];
833
+ return [...previous, ...current];
834
+ }
835
+ function stringOption(options, ...keys) {
836
+ for (const key of keys) {
837
+ if (typeof options[key] === "string") return options[key];
838
+ }
839
+ return "";
840
+ }
841
+ function listOption(options, ...keys) {
842
+ return keys.flatMap((key) => Array.isArray(options[key]) ? options[key] : []);
717
843
  }
718
844
  async function collectInteractive(initial) {
719
845
  const answers = await prompts([
@@ -769,19 +895,28 @@ async function collectInteractive(initial) {
769
895
  };
770
896
  }
771
897
  function createTaskCommand() {
772
- return new Command3("create-task").description("Create a task contract for an agentic coding session").option("--title <title>", "task title").option("--type <type>", "task type").option("--out <path>", "output file path").option("--problem <text>", "problem statement").option("--outcome <text>", "desired outcome").option("--constraint <text>", "constraint; repeat or use newlines", lines, []).option("--non-goal <text>", "non-goal; repeat or use newlines", lines, []).option("--acceptance <text>", "acceptance criterion; repeat or use newlines", lines, []).option("--verify-command <command>", "verification command; repeat or use newlines", lines, []).action(async (options) => {
898
+ return new Command3("create-task").description("Create a task contract for an agentic coding session").option("--title <title>", "task title").option("--type <type>", "task type").option("--out <path>", "output file path").option("--problem <text>", "problem statement").option("--problem-statement <text>", "problem statement").option("--outcome <text>", "desired outcome").option("--desired-outcome <text>", "desired outcome").option("--constraint <text>", "constraint; repeat or use newlines", lines, []).option("--non-goal <text>", "non-goal; repeat or use newlines", lines, []).option("--assumption <text>", "assumption; repeat or use newlines", lines, []).option("--likely-file <path>", "likely file or area; repeat or use newlines", lines, []).option(
899
+ "--forbidden-file <path>",
900
+ "file or area not to touch; repeat or use newlines",
901
+ lines,
902
+ []
903
+ ).option("--acceptance <text>", "acceptance criterion; repeat or use newlines", lines, []).option("--verify-command <command>", "verification command; repeat or use newlines", lines, []).option("--verification <command>", "verification command; repeat or use newlines", lines, []).option("--rollback <text>", "rollback notes").option("--json", "print machine-readable output").action(async (options) => {
773
904
  const type = typeof options.type === "string" && TASK_TYPES.includes(options.type) ? options.type : void 0;
774
905
  const title = typeof options.title === "string" ? options.title : void 0;
775
906
  const config = await loadAgentLoopConfig(process.cwd());
776
907
  const input = title && type ? {
777
908
  title,
778
909
  type,
779
- problemStatement: String(options.problem ?? ""),
780
- desiredOutcome: String(options.outcome ?? ""),
781
- constraints: options.constraint,
782
- nonGoals: options.nonGoal,
783
- acceptanceCriteria: options.acceptance,
784
- verificationCommands: options.verifyCommand
910
+ problemStatement: stringOption(options, "problemStatement", "problem"),
911
+ desiredOutcome: stringOption(options, "desiredOutcome", "outcome"),
912
+ constraints: listOption(options, "constraint"),
913
+ nonGoals: listOption(options, "nonGoal"),
914
+ assumptions: listOption(options, "assumption"),
915
+ likelyFiles: listOption(options, "likelyFile"),
916
+ forbiddenFiles: listOption(options, "forbiddenFile"),
917
+ acceptanceCriteria: listOption(options, "acceptance"),
918
+ verificationCommands: listOption(options, "verifyCommand", "verification"),
919
+ rollbackNotes: stringOption(options, "rollback")
785
920
  } : await collectInteractive({ title, type });
786
921
  const result = await createTaskContractFile({
787
922
  cwd: process.cwd(),
@@ -789,6 +924,10 @@ function createTaskCommand() {
789
924
  input,
790
925
  out: typeof options.out === "string" ? options.out : void 0
791
926
  });
927
+ if (options.json) {
928
+ console.log(JSON.stringify({ task: result }, null, 2));
929
+ return;
930
+ }
792
931
  console.log(`Task contract created: ${result.path}`);
793
932
  });
794
933
  }
@@ -798,12 +937,40 @@ import { Command as Command4 } from "commander";
798
937
 
799
938
  // src/core/verification.ts
800
939
  import path10 from "path";
940
+ import { readFile as readFile6 } from "fs/promises";
801
941
  import { execa as execa2 } from "execa";
802
942
  function excerpt(output, limit = 5e3) {
803
943
  if (output.length <= limit) return output;
804
- return `${output.slice(0, limit)}
944
+ const headLimit = Math.ceil(limit / 2);
945
+ const tailLimit = Math.floor(limit / 2);
946
+ return `${output.slice(0, headLimit)}
947
+
948
+ [output truncated: showing first ${headLimit} and last ${tailLimit} characters of ${output.length} total]
949
+
950
+ ${output.slice(-tailLimit)}`;
951
+ }
952
+ function failureSnippet(output, maxLines = 12, maxChars = 2e3) {
953
+ const lines2 = output.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim());
954
+ const tail = lines2.slice(-maxLines).join("\n") || "(no output)";
955
+ if (tail.length <= maxChars) return tail;
956
+ return `${tail.slice(0, maxChars)}
957
+ [failure summary truncated]`;
958
+ }
959
+ function renderFailureSummary(results) {
960
+ const failures = results.filter((result) => !result.passed);
961
+ if (!failures.length) return "";
962
+ return `## Failure Summary
963
+ ${failures.map(
964
+ (result) => `### ${result.key}: \`${result.command}\`
965
+
966
+ - Exit code: ${result.exitCode}
967
+
968
+ \`\`\`text
969
+ ${failureSnippet(result.output)}
970
+ \`\`\``
971
+ ).join("\n\n")}
805
972
 
806
- [output truncated to ${limit} characters]`;
973
+ `;
807
974
  }
808
975
  function commandEntries(config, options) {
809
976
  const configured = [
@@ -821,9 +988,131 @@ function commandEntries(config, options) {
821
988
  }
822
989
  return active;
823
990
  }
991
+ function singleLine(value, limit = 300) {
992
+ const clean = value?.replace(/\s+/g, " ").trim();
993
+ if (!clean) return void 0;
994
+ return clean.length > limit ? `${clean.slice(0, limit)}...` : clean;
995
+ }
996
+ function withoutTrailingSlash(value) {
997
+ return value.replace(/\/+$/, "");
998
+ }
999
+ function isEnabledCi(value) {
1000
+ const clean = value?.trim().toLowerCase();
1001
+ return clean === "true" || clean === "1";
1002
+ }
1003
+ function detectCiContext(env) {
1004
+ if (env.GITHUB_ACTIONS === "true") {
1005
+ const serverUrl = withoutTrailingSlash(
1006
+ singleLine(env.GITHUB_SERVER_URL) ?? "https://github.com"
1007
+ );
1008
+ const repository = singleLine(env.GITHUB_REPOSITORY);
1009
+ const runId = singleLine(env.GITHUB_RUN_ID);
1010
+ const runUrl = repository && runId ? `${serverUrl}/${repository}/actions/runs/${runId}` : void 0;
1011
+ return {
1012
+ provider: "github-actions",
1013
+ providerName: "GitHub Actions",
1014
+ workflow: singleLine(env.GITHUB_WORKFLOW),
1015
+ event: singleLine(env.GITHUB_EVENT_NAME),
1016
+ ref: singleLine(env.GITHUB_REF),
1017
+ commit: singleLine(env.GITHUB_SHA),
1018
+ runUrl,
1019
+ runAttempt: singleLine(env.GITHUB_RUN_ATTEMPT)
1020
+ };
1021
+ }
1022
+ if (env.GITLAB_CI === "true") {
1023
+ return {
1024
+ provider: "gitlab-ci",
1025
+ providerName: "GitLab CI",
1026
+ workflow: singleLine(env.CI_PROJECT_PATH),
1027
+ event: singleLine(env.CI_PIPELINE_SOURCE),
1028
+ ref: singleLine(env.CI_COMMIT_REF_NAME),
1029
+ commit: singleLine(env.CI_COMMIT_SHA),
1030
+ runUrl: singleLine(env.CI_PIPELINE_URL)
1031
+ };
1032
+ }
1033
+ if (env.BUILDKITE === "true") {
1034
+ return {
1035
+ provider: "buildkite",
1036
+ providerName: "Buildkite",
1037
+ workflow: singleLine(env.BUILDKITE_PIPELINE_SLUG),
1038
+ event: singleLine(env.BUILDKITE_SOURCE),
1039
+ ref: singleLine(env.BUILDKITE_BRANCH),
1040
+ commit: singleLine(env.BUILDKITE_COMMIT),
1041
+ runUrl: singleLine(env.BUILDKITE_BUILD_URL)
1042
+ };
1043
+ }
1044
+ if (isEnabledCi(env.CI)) {
1045
+ return {
1046
+ provider: "generic-ci",
1047
+ providerName: "Generic CI"
1048
+ };
1049
+ }
1050
+ return void 0;
1051
+ }
1052
+ function renderCiContext(ciContext) {
1053
+ if (!ciContext) return "";
1054
+ const lines2 = [`- Provider: ${ciContext.providerName}`];
1055
+ if (ciContext.workflow) lines2.push(`- Workflow: ${ciContext.workflow}`);
1056
+ if (ciContext.event) lines2.push(`- Event: ${ciContext.event}`);
1057
+ if (ciContext.ref) lines2.push(`- Ref: ${ciContext.ref}`);
1058
+ if (ciContext.commit) lines2.push(`- Commit: ${ciContext.commit}`);
1059
+ if (ciContext.runUrl) lines2.push(`- Run URL: ${ciContext.runUrl}`);
1060
+ if (ciContext.runAttempt) lines2.push(`- Run attempt: ${ciContext.runAttempt}`);
1061
+ return `## CI Context
1062
+ ${lines2.join("\n")}
1063
+
1064
+ `;
1065
+ }
1066
+ function parseTaskMetadata(markdown) {
1067
+ const lines2 = markdown.split(/\r?\n/);
1068
+ return {
1069
+ title: lines2.find((line) => line.startsWith("# "))?.replace(/^#\s+/, "").trim(),
1070
+ type: lines2.find((line) => line.startsWith("- Task type:"))?.replace("- Task type:", "").trim(),
1071
+ status: lines2.find((line) => line.startsWith("- Status:"))?.replace("- Status:", "").trim()
1072
+ };
1073
+ }
1074
+ function isMarkdownTaskPath(taskPath) {
1075
+ const normalized = taskPath.replace(/\\/g, "/").toLowerCase();
1076
+ const segments = normalized.split("/").filter(Boolean);
1077
+ return normalized.endsWith(".md") && !segments.some((segment) => segment === ".env" || segment.startsWith(".env."));
1078
+ }
1079
+ async function renderTaskContext(cwd, taskPath) {
1080
+ if (!taskPath?.trim()) return "";
1081
+ const cleanPath = taskPath.trim();
1082
+ if (!isMarkdownTaskPath(cleanPath)) {
1083
+ return `## Task Context
1084
+ - Path: ${cleanPath}
1085
+ - Status: unavailable
1086
+ - Note: Task path must point to a Markdown task contract.
1087
+
1088
+ `;
1089
+ }
1090
+ const absolutePath = path10.isAbsolute(cleanPath) ? cleanPath : path10.join(cwd, cleanPath);
1091
+ try {
1092
+ const markdown = await readFile6(absolutePath, "utf8");
1093
+ const metadata = parseTaskMetadata(markdown);
1094
+ const lines2 = [`- Path: ${cleanPath}`];
1095
+ if (metadata.title) lines2.push(`- Title: ${metadata.title}`);
1096
+ if (metadata.type) lines2.push(`- Task type: ${metadata.type}`);
1097
+ if (metadata.status) lines2.push(`- Status: ${metadata.status}`);
1098
+ return `## Task Context
1099
+ ${lines2.join("\n")}
1100
+
1101
+ `;
1102
+ } catch {
1103
+ return `## Task Context
1104
+ - Path: ${cleanPath}
1105
+ - Status: unavailable
1106
+ - Note: Task file could not be read.
1107
+
1108
+ `;
1109
+ }
1110
+ }
824
1111
  async function runVerification(options) {
825
1112
  const timestamp = options.reportTimestamp ?? formatTimestamp();
826
1113
  const nowIso = options.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
1114
+ const env = options.env ?? process.env;
1115
+ const ciContext = detectCiContext(env);
827
1116
  const commands = commandEntries(options.config, options);
828
1117
  const notRun = [
829
1118
  ...["test", "lint", "typecheck", "build"].filter((key) => {
@@ -838,7 +1127,7 @@ async function runVerification(options) {
838
1127
  shell: true,
839
1128
  all: true,
840
1129
  reject: false,
841
- env: { ...process.env, FORCE_COLOR: "0" }
1130
+ env: { ...env, FORCE_COLOR: "0" }
842
1131
  });
843
1132
  results.push({
844
1133
  key,
@@ -848,7 +1137,7 @@ async function runVerification(options) {
848
1137
  output: result.all ?? result.stdout ?? result.stderr ?? ""
849
1138
  });
850
1139
  }
851
- const overallStatus = results.length === 0 ? "not-run" : results.every((result) => result.passed) ? "pass" : "fail";
1140
+ const overallStatus2 = results.length === 0 ? "not-run" : results.every((result) => result.passed) ? "pass" : "fail";
852
1141
  const reportPath = path10.join(
853
1142
  options.cwd,
854
1143
  options.config.paths.reportsDir,
@@ -857,6 +1146,7 @@ async function runVerification(options) {
857
1146
  const branch = await getGitBranch(options.cwd);
858
1147
  const commit = await getGitCommit(options.cwd);
859
1148
  const status = await getGitStatus(options.cwd);
1149
+ const taskContext = await renderTaskContext(options.cwd, options.taskPath);
860
1150
  const markdown = `# Verification Report
861
1151
 
862
1152
  - Timestamp: ${nowIso}
@@ -864,8 +1154,11 @@ async function runVerification(options) {
864
1154
  - Git branch: ${branch || "not available"}
865
1155
  - Git commit: ${commit || "not available"}
866
1156
  - Working tree: ${status.trim() ? "dirty" : "clean or unavailable"}
867
- - Overall status: ${overallStatus}
1157
+ - Overall status: ${overallStatus2}
868
1158
 
1159
+ ${renderCiContext(ciContext)}
1160
+ ${taskContext}
1161
+ ${renderFailureSummary(results)}
869
1162
  ## Commands Run
870
1163
  ${results.length === 0 ? "No verification commands were configured or selected." : results.map(
871
1164
  (result) => `### ${result.key}: \`${result.command}\`
@@ -882,10 +1175,10 @@ ${excerpt(result.output || "(no output)")}
882
1175
  ${notRun.length ? notRun.map((item) => `- ${item}`).join("\n") : "- Nothing skipped."}
883
1176
 
884
1177
  ## Recommended Next Actions
885
- ${overallStatus === "pass" ? "- Review the diff and prepare a handoff summary." : overallStatus === "fail" ? "- Fix failing commands before claiming completion." : "- Add test, lint, typecheck, or build commands to agentloop.config.json."}
1178
+ ${overallStatus2 === "pass" ? "- Review the diff and prepare a handoff summary." : overallStatus2 === "fail" ? "- Fix failing commands before claiming completion." : "- Add test, lint, typecheck, or build commands to agentloop.config.json."}
886
1179
  `;
887
1180
  await writeTextFile(reportPath, markdown);
888
- return { overallStatus, commands: results, notRun, markdown, reportPath };
1181
+ return { overallStatus: overallStatus2, commands: results, notRun, ciContext, markdown, reportPath };
889
1182
  }
890
1183
 
891
1184
  // src/cli/commands/verify.ts
@@ -899,6 +1192,7 @@ function verifyCommand() {
899
1192
  const result = await runVerification({
900
1193
  cwd: process.cwd(),
901
1194
  config,
1195
+ taskPath: typeof options.task === "string" ? options.task : void 0,
902
1196
  skip: {
903
1197
  build: options.build === false,
904
1198
  test: options.test === false,
@@ -921,13 +1215,302 @@ Overall status: ${result.overallStatus}`
921
1215
  import { Command as Command5 } from "commander";
922
1216
 
923
1217
  // src/core/pr-summary.ts
1218
+ import path13 from "path";
1219
+ import { readFile as readFile8 } from "fs/promises";
1220
+
1221
+ // src/core/artifacts.ts
924
1222
  import path11 from "path";
925
- import { readdir as readdir4, readFile as readFile6 } from "fs/promises";
1223
+ import { readdir as readdir4, stat as stat2 } from "fs/promises";
1224
+ var verificationReportPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-verification-report\.md$/;
1225
+ var prSummaryPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-pr-summary\.md$/;
1226
+ var ciSummaryPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-ci-summary\.md$/;
1227
+ async function latestMarkdownFile(dir, options = {}) {
1228
+ if (!await pathExists(dir)) return void 0;
1229
+ const entries = await Promise.all(
1230
+ (await readdir4(dir, { withFileTypes: true })).filter(
1231
+ (entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name.toLowerCase() !== "readme.md"
1232
+ ).filter((entry) => !options.pattern || options.pattern.test(entry.name)).map(async (entry) => {
1233
+ const filePath = path11.join(dir, entry.name);
1234
+ const fileStat = await stat2(filePath);
1235
+ return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
1236
+ })
1237
+ );
1238
+ entries.sort((left, right) => {
1239
+ if (left.mtimeMs !== right.mtimeMs) return left.mtimeMs - right.mtimeMs;
1240
+ return left.name.localeCompare(right.name);
1241
+ });
1242
+ return entries.at(-1)?.filePath;
1243
+ }
1244
+
1245
+ // src/core/task-state.ts
1246
+ import path12 from "path";
1247
+ import { mkdir as mkdir2, readdir as readdir5, readFile as readFile7, rename, rm, stat as stat3 } from "fs/promises";
1248
+ var TASK_STATUSES = ["proposed", "in-progress", "blocked", "review", "done"];
1249
+ function statePath(cwd, config) {
1250
+ return path12.join(cwd, config.paths.agentloopDir, "state.json");
1251
+ }
1252
+ function toStoredPath(cwd, absolutePath) {
1253
+ return path12.relative(cwd, absolutePath).split(path12.sep).join("/");
1254
+ }
1255
+ function isInside(parent, child) {
1256
+ const relative2 = path12.relative(parent, child);
1257
+ return relative2 === "" || !relative2.startsWith("..") && !path12.isAbsolute(relative2);
1258
+ }
1259
+ function tasksRoot(cwd, config) {
1260
+ return path12.resolve(cwd, config.paths.tasksDir);
1261
+ }
1262
+ async function readState(cwd, config) {
1263
+ const filePath = statePath(cwd, config);
1264
+ if (!await pathExists(filePath)) return { version: 1 };
1265
+ const raw = await readFile7(filePath, "utf8");
1266
+ try {
1267
+ const parsed = JSON.parse(raw);
1268
+ return {
1269
+ version: 1,
1270
+ activeTaskPath: typeof parsed.activeTaskPath === "string" && parsed.activeTaskPath.trim() ? parsed.activeTaskPath : void 0
1271
+ };
1272
+ } catch {
1273
+ return { version: 1 };
1274
+ }
1275
+ }
1276
+ async function writeState(cwd, config, state) {
1277
+ await writeTextFile(statePath(cwd, config), `${JSON.stringify(state, null, 2)}
1278
+ `);
1279
+ }
1280
+ async function resolveTaskPath(options) {
1281
+ const absolutePath = path12.isAbsolute(options.taskPath) ? path12.resolve(options.taskPath) : path12.resolve(options.cwd, options.taskPath);
1282
+ const root = tasksRoot(options.cwd, options.config);
1283
+ const displayRoot = options.config.paths.tasksDir;
1284
+ if (!isInside(root, absolutePath)) {
1285
+ if (!options.strict) return void 0;
1286
+ throw new AgentLoopError(`Active task must be inside ${displayRoot}.`);
1287
+ }
1288
+ if (!absolutePath.endsWith(".md")) {
1289
+ if (!options.strict) return void 0;
1290
+ throw new AgentLoopError("Active task must be a Markdown file.");
1291
+ }
1292
+ const fileStat = await stat3(absolutePath).catch(() => void 0);
1293
+ if (!fileStat?.isFile()) {
1294
+ if (!options.strict) return void 0;
1295
+ throw new AgentLoopError(`Task contract not found: ${options.taskPath}`);
1296
+ }
1297
+ return absolutePath;
1298
+ }
1299
+ function extractHeading(markdown, fallback) {
1300
+ return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
1301
+ }
1302
+ function extractTaskStatus(markdown) {
1303
+ return markdown.match(/^- Status:\s*(.+)$/im)?.[1]?.trim() || "unknown";
1304
+ }
1305
+ function parseTaskStatus(status) {
1306
+ const clean = status.trim().toLowerCase();
1307
+ if (TASK_STATUSES.includes(clean)) return clean;
1308
+ throw new AgentLoopError(
1309
+ `Unsupported task status "${status}". Use one of: ${TASK_STATUSES.join(", ")}.`
1310
+ );
1311
+ }
1312
+ async function readTaskMetadata(cwd, filePath) {
1313
+ const markdown = await readFile7(filePath, "utf8");
1314
+ return {
1315
+ path: toStoredPath(cwd, filePath),
1316
+ title: extractHeading(markdown, path12.basename(filePath, ".md")),
1317
+ status: extractTaskStatus(markdown)
1318
+ };
1319
+ }
1320
+ async function readTaskContract(options) {
1321
+ const absolutePath = await resolveTaskPath({ ...options, strict: true });
1322
+ if (!absolutePath) throw new AgentLoopError(`Task contract not found: ${options.taskPath}`);
1323
+ const content = await readFile7(absolutePath, "utf8");
1324
+ return {
1325
+ ...await readTaskMetadata(options.cwd, absolutePath),
1326
+ content
1327
+ };
1328
+ }
1329
+ async function setActiveTask(options) {
1330
+ const absolutePath = await resolveTaskPath({ ...options, strict: true });
1331
+ if (!absolutePath) throw new AgentLoopError(`Task contract not found: ${options.taskPath}`);
1332
+ const activeTaskPath = toStoredPath(options.cwd, absolutePath);
1333
+ await writeState(options.cwd, options.config, { version: 1, activeTaskPath });
1334
+ return readTaskMetadata(options.cwd, absolutePath);
1335
+ }
1336
+ async function updateTaskStatus(options) {
1337
+ const absolutePath = await resolveTaskPath({ ...options, strict: true });
1338
+ if (!absolutePath) throw new AgentLoopError(`Task contract not found: ${options.taskPath}`);
1339
+ const status = parseTaskStatus(options.status);
1340
+ const content = await readFile7(absolutePath, "utf8");
1341
+ if (!/^- Status:\s*.+$/im.test(content)) {
1342
+ throw new AgentLoopError(`Task contract does not contain a Status line: ${options.taskPath}`);
1343
+ }
1344
+ await writeTextFile(absolutePath, content.replace(/^(- Status:\s*).+$/im, `$1${status}`));
1345
+ return readTaskMetadata(options.cwd, absolutePath);
1346
+ }
1347
+ async function archiveTask(options) {
1348
+ const absolutePath = await resolveTaskPath({ ...options, strict: true });
1349
+ if (!absolutePath) throw new AgentLoopError(`Task contract not found: ${options.taskPath}`);
1350
+ const root = tasksRoot(options.cwd, options.config);
1351
+ const archiveRoot = path12.join(root, "archive");
1352
+ if (path12.dirname(absolutePath) === archiveRoot) {
1353
+ throw new AgentLoopError(`Task contract is already archived: ${options.taskPath}`);
1354
+ }
1355
+ const destinationPath = path12.join(archiveRoot, path12.basename(absolutePath));
1356
+ if (await pathExists(destinationPath)) {
1357
+ throw new AgentLoopError(
1358
+ `Archived task already exists: ${toStoredPath(options.cwd, destinationPath)}`
1359
+ );
1360
+ }
1361
+ const previousPath = toStoredPath(options.cwd, absolutePath);
1362
+ const activeTaskPath = await getActiveTaskPath(options);
1363
+ await mkdir2(archiveRoot, { recursive: true });
1364
+ await rename(absolutePath, destinationPath);
1365
+ if (activeTaskPath === absolutePath) {
1366
+ await clearActiveTask(options);
1367
+ }
1368
+ return {
1369
+ ...await readTaskMetadata(options.cwd, destinationPath),
1370
+ previousPath
1371
+ };
1372
+ }
1373
+ async function getActiveTaskPath(options) {
1374
+ const state = await readState(options.cwd, options.config);
1375
+ if (!state.activeTaskPath) return void 0;
1376
+ return resolveTaskPath({
1377
+ cwd: options.cwd,
1378
+ config: options.config,
1379
+ taskPath: state.activeTaskPath,
1380
+ strict: false
1381
+ });
1382
+ }
1383
+ async function getActiveTask(options) {
1384
+ const activeTaskPath = await getActiveTaskPath(options);
1385
+ return activeTaskPath ? readTaskMetadata(options.cwd, activeTaskPath) : void 0;
1386
+ }
1387
+ async function clearActiveTask(options) {
1388
+ await rm(statePath(options.cwd, options.config), { force: true });
1389
+ }
1390
+ async function listTasks(options) {
1391
+ const root = tasksRoot(options.cwd, options.config);
1392
+ const entries = await readdir5(root, { withFileTypes: true }).catch(() => []);
1393
+ const activeTaskPath = await getActiveTaskPath(options);
1394
+ const tasks = await Promise.all(
1395
+ entries.filter((entry) => entry.isFile()).filter((entry) => entry.name.endsWith(".md") && entry.name !== "README.md").map(async (entry) => {
1396
+ const filePath = path12.join(root, entry.name);
1397
+ const [metadata, fileStat] = await Promise.all([
1398
+ readTaskMetadata(options.cwd, filePath),
1399
+ stat3(filePath)
1400
+ ]);
1401
+ return {
1402
+ ...metadata,
1403
+ active: activeTaskPath === filePath,
1404
+ modifiedAt: fileStat.mtime.toISOString(),
1405
+ modifiedMs: fileStat.mtimeMs
1406
+ };
1407
+ })
1408
+ );
1409
+ return tasks.sort((left, right) => {
1410
+ if (left.active !== right.active) return left.active ? -1 : 1;
1411
+ if (left.modifiedMs !== right.modifiedMs) return right.modifiedMs - left.modifiedMs;
1412
+ return left.path.localeCompare(right.path);
1413
+ }).map((task) => ({
1414
+ path: task.path,
1415
+ title: task.title,
1416
+ status: task.status,
1417
+ active: task.active,
1418
+ modifiedAt: task.modifiedAt
1419
+ }));
1420
+ }
1421
+
1422
+ // src/core/pr-summary.ts
1423
+ var CHANGE_AREAS = [
1424
+ {
1425
+ key: "risk",
1426
+ title: "Risk-Sensitive",
1427
+ matches: (filePath) => /(^|\/)(migrations?|migration|auth|security|billing|deploy|deployment)(\/|\.|-|_|$)/.test(
1428
+ filePath
1429
+ ) || /(^|\/)\.env(\.|$)/.test(filePath) || /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|Cargo\.lock|poetry\.lock)$/.test(
1430
+ filePath
1431
+ )
1432
+ },
1433
+ { key: "source", title: "Source", matches: (filePath) => /^src\//.test(filePath) },
1434
+ {
1435
+ key: "tests",
1436
+ title: "Tests",
1437
+ matches: (filePath) => /(^|\/)(tests?|__tests__)\//.test(filePath) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(filePath)
1438
+ },
1439
+ {
1440
+ key: "agentloop",
1441
+ title: "AgentLoop",
1442
+ matches: (filePath) => /^\.agentloop\//.test(filePath) || /(^|\/)(AGENTS\.md|AGENTLOOP\.md|agentloop\.config\.json)$/.test(filePath)
1443
+ },
1444
+ {
1445
+ key: "docs",
1446
+ title: "Documentation",
1447
+ matches: (filePath) => /(^|\/)docs\//.test(filePath) || /(^|\/)(README|CHANGELOG|CONTRIBUTING|CODE_OF_CONDUCT|SECURITY|ROADMAP|DECISIONS)\.md$/i.test(
1448
+ filePath
1449
+ ) || /\.mdx?$/.test(filePath)
1450
+ },
1451
+ {
1452
+ key: "ci",
1453
+ title: "CI / Automation",
1454
+ matches: (filePath) => /^\.github\//.test(filePath) || /^scripts\//.test(filePath) || /(^|\/)(Dockerfile|Makefile)$/.test(filePath)
1455
+ },
1456
+ {
1457
+ key: "config",
1458
+ title: "Config / Package",
1459
+ matches: (filePath) => /(^|\/)(package\.json|tsconfig\.json|tsup\.config\.ts|vitest\.config\.ts|eslint\.config\.js|prettier\.config\.[cm]?js|pnpm-workspace\.yaml)$/.test(
1460
+ filePath
1461
+ ) || /^schema\//.test(filePath)
1462
+ }
1463
+ ];
926
1464
  function extractLine(markdown, pattern, fallback) {
927
1465
  if (!markdown) return fallback;
928
1466
  const match = markdown.match(pattern);
929
1467
  return match?.[1]?.trim() || fallback;
930
1468
  }
1469
+ function classifyChangedFiles(changedFiles) {
1470
+ const areas = CHANGE_AREAS.map((area) => ({
1471
+ key: area.key,
1472
+ title: area.title,
1473
+ files: []
1474
+ }));
1475
+ const other = { key: "other", title: "Other", files: [] };
1476
+ for (const file of changedFiles) {
1477
+ const normalizedPath = file.path.replace(/\\/g, "/");
1478
+ const areaIndex = CHANGE_AREAS.findIndex((area) => area.matches(normalizedPath));
1479
+ if (areaIndex === -1) other.files.push(file);
1480
+ else areas[areaIndex]?.files.push(file);
1481
+ }
1482
+ return [...areas.filter((area) => area.files.length), ...other.files.length ? [other] : []];
1483
+ }
1484
+ function renderChangeAreas(changedFiles) {
1485
+ if (!changedFiles.length) return "- No changed files detected.";
1486
+ const areas = classifyChangedFiles(changedFiles);
1487
+ return areas.map((area) => {
1488
+ return `### ${area.title}
1489
+ ${area.files.map((file) => `- ${file.status} \`${file.path}\``).join("\n")}`;
1490
+ }).join("\n\n");
1491
+ }
1492
+ function renderReviewFocus(changedFiles) {
1493
+ if (!changedFiles.length) return "- No changed files detected.";
1494
+ const keys = new Set(classifyChangedFiles(changedFiles).map((area) => area.key));
1495
+ const lines2 = [];
1496
+ if (keys.has("source")) lines2.push("- Review source changes for behavior and public API impact.");
1497
+ if (keys.has("tests")) lines2.push("- Check tests cover the changed behavior.");
1498
+ if (keys.has("docs")) lines2.push("- Check docs match the implemented command behavior.");
1499
+ if (keys.has("ci"))
1500
+ lines2.push("- Review CI or automation changes for permissions and secret handling.");
1501
+ if (keys.has("config"))
1502
+ lines2.push("- Review package and config changes for install, build, and publish impact.");
1503
+ if (keys.has("agentloop"))
1504
+ lines2.push(
1505
+ "- Review AgentLoop artifacts for accurate task, verification, and handoff evidence."
1506
+ );
1507
+ if (keys.has("risk"))
1508
+ lines2.push(
1509
+ "- Review risk-sensitive paths such as migrations, auth, security, billing, env, deployment, and lockfiles with extra care."
1510
+ );
1511
+ if (keys.has("other")) lines2.push("- Review uncategorized files for ownership and scope.");
1512
+ return lines2.join("\n");
1513
+ }
931
1514
  function generatePrSummary(input) {
932
1515
  const taskTitle = extractLine(input.taskMarkdown, /^#\s+(.+)$/m, "No task contract found.");
933
1516
  const verification = extractLine(
@@ -948,12 +1531,18 @@ This summary was generated deterministically from git status, the latest task co
948
1531
  ## Changed Files
949
1532
  ${input.changedFiles.length ? input.changedFiles.map((file) => `- ${file.status} \`${file.path}\``).join("\n") : "- No changed files detected."}
950
1533
 
1534
+ ## Change Areas
1535
+ ${renderChangeAreas(input.changedFiles)}
1536
+
951
1537
  ## Diff Stats
952
1538
  ${input.diffStat?.trim() || "No diff stats available."}
953
1539
 
954
1540
  ## Behaviour Changed
955
1541
  - Review changed files and task contract to confirm intended behavior.
956
1542
 
1543
+ ## Review Focus
1544
+ ${renderReviewFocus(input.changedFiles)}
1545
+
957
1546
  ## Verification Performed
958
1547
  - ${verificationLine}
959
1548
 
@@ -977,23 +1566,17 @@ ${input.diffStat?.trim() || "No diff stats available."}
977
1566
  `;
978
1567
  return { markdown };
979
1568
  }
980
- async function latestMarkdownFile(dir) {
981
- if (!await pathExists(dir)) return void 0;
982
- const entries = (await readdir4(dir, { withFileTypes: true })).filter(
983
- (entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name.toLowerCase() !== "readme.md"
984
- ).map((entry) => entry.name).sort();
985
- const latest = entries.at(-1);
986
- return latest ? path11.join(dir, latest) : void 0;
987
- }
988
1569
  async function summarizeRepository(options) {
989
1570
  const timestamp = options.timestamp ?? formatTimestamp();
990
1571
  const status = await getGitStatus(options.cwd);
991
1572
  const changedFiles = await parseGitStatus(status);
992
1573
  const diffStat = await getGitDiffStat(options.cwd);
993
- const taskPath = options.taskPath ?? await latestMarkdownFile(path11.join(options.cwd, options.config.paths.tasksDir));
994
- const reportPath = options.reportPath ?? await latestMarkdownFile(path11.join(options.cwd, options.config.paths.reportsDir));
995
- const taskMarkdown = taskPath && await pathExists(taskPath) ? await readFile6(taskPath, "utf8") : void 0;
996
- const verificationMarkdown = reportPath && await pathExists(reportPath) ? await readFile6(reportPath, "utf8") : void 0;
1574
+ const taskPath = options.taskPath ?? await getActiveTaskPath({ cwd: options.cwd, config: options.config }) ?? await latestMarkdownFile(path13.join(options.cwd, options.config.paths.tasksDir));
1575
+ const reportPath = options.reportPath ?? await latestMarkdownFile(path13.join(options.cwd, options.config.paths.reportsDir), {
1576
+ pattern: verificationReportPattern
1577
+ });
1578
+ const taskMarkdown = taskPath && await pathExists(taskPath) ? await readFile8(taskPath, "utf8") : void 0;
1579
+ const verificationMarkdown = reportPath && await pathExists(reportPath) ? await readFile8(reportPath, "utf8") : void 0;
997
1580
  const summary = generatePrSummary({
998
1581
  timestamp,
999
1582
  status,
@@ -1002,7 +1585,7 @@ async function summarizeRepository(options) {
1002
1585
  verificationMarkdown,
1003
1586
  diffStat
1004
1587
  });
1005
- const outPath = path11.join(
1588
+ const outPath = path13.join(
1006
1589
  options.cwd,
1007
1590
  options.config.paths.handoffsDir,
1008
1591
  `${timestamp}-pr-summary.md`
@@ -1012,31 +1595,36 @@ async function summarizeRepository(options) {
1012
1595
  }
1013
1596
 
1014
1597
  // src/cli/commands/summarize.ts
1015
- function summarizeCommand() {
1016
- return new Command5("summarize").description("Generate a deterministic PR/reviewer summary").option("--task <path>", "task contract path").option("--report <path>", "verification report path").option("--format <format>", "markdown or json", "markdown").option("--write", "write summary to .agentloop/handoffs").option("--json", "print JSON output").action(async (options) => {
1017
- const config = await loadAgentLoopConfig(process.cwd());
1018
- const result = await summarizeRepository({
1019
- cwd: process.cwd(),
1020
- config,
1021
- taskPath: typeof options.task === "string" ? options.task : void 0,
1022
- reportPath: typeof options.report === "string" ? options.report : void 0,
1023
- write: Boolean(options.write)
1024
- });
1025
- if (options.json || options.format === "json") {
1026
- console.log(JSON.stringify(result, null, 2));
1027
- } else {
1028
- console.log(result.markdown);
1029
- if (options.write) console.log(`
1030
- Summary written: ${result.outPath}`);
1031
- }
1598
+ async function runSummaryCommand(options, defaultWrite) {
1599
+ const config = await loadAgentLoopConfig(process.cwd());
1600
+ const writeOption = typeof options.write === "boolean" ? options.write : defaultWrite;
1601
+ const result = await summarizeRepository({
1602
+ cwd: process.cwd(),
1603
+ config,
1604
+ taskPath: typeof options.task === "string" ? options.task : void 0,
1605
+ reportPath: typeof options.report === "string" ? options.report : void 0,
1606
+ write: writeOption
1032
1607
  });
1608
+ if (options.json || options.format === "json") {
1609
+ console.log(JSON.stringify(result, null, 2));
1610
+ } else {
1611
+ console.log(result.markdown);
1612
+ if (writeOption) console.log(`
1613
+ Summary written: ${result.outPath}`);
1614
+ }
1615
+ }
1616
+ function summarizeCommand() {
1617
+ return new Command5("summarize").description("Generate a deterministic PR/reviewer summary").option("--task <path>", "task contract path").option("--report <path>", "verification report path").option("--format <format>", "markdown or json", "markdown").option("--write", "write summary to .agentloop/handoffs").option("--json", "print JSON output").action((options) => runSummaryCommand(options, false));
1618
+ }
1619
+ function handoffCommand() {
1620
+ return new Command5("handoff").description("Generate and write a deterministic reviewer handoff").option("--task <path>", "task contract path").option("--report <path>", "verification report path").option("--format <format>", "markdown or json", "markdown").option("--no-write", "print handoff without writing a file").option("--json", "print JSON output").action((options) => runSummaryCommand(options, true));
1033
1621
  }
1034
1622
 
1035
1623
  // src/cli/commands/install-agent.ts
1036
1624
  import { Command as Command6 } from "commander";
1037
1625
 
1038
1626
  // src/core/agent-installation.ts
1039
- import path12 from "path";
1627
+ import path14 from "path";
1040
1628
  function isSupportedAgent(value) {
1041
1629
  return SUPPORTED_AGENTS.includes(value);
1042
1630
  }
@@ -1050,12 +1638,12 @@ var displayNames = {
1050
1638
  generic: "Generic Coding Agent"
1051
1639
  };
1052
1640
  async function installAgentInstructions(options) {
1053
- const agentFilePath = path12.join(options.cwd, ".agentloop", "agents", `${options.agent}.md`);
1641
+ const agentFilePath = path14.join(options.cwd, ".agentloop", "agents", `${options.agent}.md`);
1054
1642
  const content = await readTemplate(`agents/${options.agent}.md`, {
1055
1643
  agentName: displayNames[options.agent]
1056
1644
  });
1057
1645
  await writeTextFile(agentFilePath, content);
1058
- const agentsPath = path12.join(options.cwd, "AGENTS.md");
1646
+ const agentsPath = path14.join(options.cwd, "AGENTS.md");
1059
1647
  const existing = await readTextIfExists(agentsPath);
1060
1648
  const marker = `<!-- agentloopkit-agent:${options.agent} -->`;
1061
1649
  if (!existing.includes(marker)) {
@@ -1119,22 +1707,2079 @@ function listTemplatesCommand() {
1119
1707
 
1120
1708
  // src/cli/commands/version.ts
1121
1709
  import { Command as Command8 } from "commander";
1710
+
1711
+ // src/core/version.ts
1712
+ import { readFileSync } from "fs";
1713
+ function getPackageVersion() {
1714
+ const packageJsonUrl = new URL("../../package.json", import.meta.url);
1715
+ const packageJson = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
1716
+ return packageJson.version;
1717
+ }
1718
+
1719
+ // src/cli/commands/version.ts
1122
1720
  function versionCommand() {
1123
1721
  return new Command8("version").description("Print CLI version").action(() => {
1124
- console.log("0.1.0");
1722
+ console.log(getPackageVersion());
1723
+ });
1724
+ }
1725
+
1726
+ // src/cli/commands/status.ts
1727
+ import { Command as Command9 } from "commander";
1728
+
1729
+ // src/core/status.ts
1730
+ import path15 from "path";
1731
+ import { readFile as readFile9, stat as stat4 } from "fs/promises";
1732
+ function extractHeading2(markdown, fallback) {
1733
+ return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
1734
+ }
1735
+ function extractTaskStatus2(markdown) {
1736
+ return markdown.match(/^- Status:\s*(.+)$/im)?.[1]?.trim() || "unknown";
1737
+ }
1738
+ function extractOverallStatus(markdown) {
1739
+ return markdown.match(/Overall status:\s*([a-z-]+)/i)?.[1]?.trim() || "unknown";
1740
+ }
1741
+ async function readTask(cwd, filePath) {
1742
+ if (!filePath) return void 0;
1743
+ const markdown = await readFile9(filePath, "utf8");
1744
+ const fileStat = await stat4(filePath);
1745
+ return {
1746
+ path: path15.relative(cwd, filePath),
1747
+ title: extractHeading2(markdown, path15.basename(filePath, ".md")),
1748
+ status: extractTaskStatus2(markdown),
1749
+ modifiedAtMs: fileStat.mtimeMs
1750
+ };
1751
+ }
1752
+ async function readReport(cwd, filePath) {
1753
+ if (!filePath) return void 0;
1754
+ const markdown = await readFile9(filePath, "utf8");
1755
+ const fileStat = await stat4(filePath);
1756
+ return {
1757
+ path: path15.relative(cwd, filePath),
1758
+ title: extractHeading2(markdown, path15.basename(filePath, ".md")),
1759
+ overallStatus: extractOverallStatus(markdown),
1760
+ modifiedAtMs: fileStat.mtimeMs
1761
+ };
1762
+ }
1763
+ function stripTaskTimestamp(task) {
1764
+ if (!task) return void 0;
1765
+ return {
1766
+ path: task.path,
1767
+ title: task.title,
1768
+ status: task.status
1769
+ };
1770
+ }
1771
+ function stripReportTimestamp(report) {
1772
+ if (!report) return void 0;
1773
+ return {
1774
+ path: report.path,
1775
+ title: report.title,
1776
+ overallStatus: report.overallStatus
1777
+ };
1778
+ }
1779
+ function isPostVerificationTaskState(task) {
1780
+ const status = task?.status.trim().toLowerCase();
1781
+ return status === "review" || status === "done";
1782
+ }
1783
+ function chooseNextAction(input) {
1784
+ if (!input.activeTask) {
1785
+ return {
1786
+ command: "agentloop create-task",
1787
+ reason: "No task contract was found."
1788
+ };
1789
+ }
1790
+ if (!input.latestReport) {
1791
+ return {
1792
+ command: "agentloop verify",
1793
+ reason: "A task exists, but no verification report was found."
1794
+ };
1795
+ }
1796
+ if (input.latestReport.overallStatus === "fail") {
1797
+ return {
1798
+ command: "agentloop verify",
1799
+ reason: "The latest verification report failed. Fix the failures and rerun verification."
1800
+ };
1801
+ }
1802
+ if (input.dirty) {
1803
+ return {
1804
+ command: "agentloop handoff",
1805
+ reason: "Task and verification evidence exist, and the working tree has changes."
1806
+ };
1807
+ }
1808
+ return {
1809
+ command: "agentloop create-task",
1810
+ reason: "The repo is clean. Start the next task contract when ready."
1811
+ };
1812
+ }
1813
+ function formatList(values) {
1814
+ return values.length ? values.join(", ") : "none";
1815
+ }
1816
+ function renderMarkdown(result) {
1817
+ const gitLine = result.git.isRepository ? `${result.git.branch || "unknown branch"}${result.git.commit ? ` @ ${result.git.commit}` : ""}` : "not inside a git repository";
1818
+ const workingTree = result.workingTree.dirty ? `dirty (${result.workingTree.changedFileCount} changed file(s))` : "clean";
1819
+ const activeTask = result.activeTask ? `${result.activeTask.title} (${result.activeTask.status}) - ${result.activeTask.path}` : "No task contract found.";
1820
+ const latestReport = result.latestReport ? `${result.latestReport.overallStatus} - ${result.latestReport.path}` : "No verification report found.";
1821
+ return `# AgentLoopKit Status
1822
+
1823
+ - Project: ${result.project.name || "unnamed"} (${result.project.type})
1824
+ - Package manager: ${result.project.packageManager}
1825
+ - Git: ${gitLine}
1826
+ - Working tree: ${workingTree}
1827
+ - Active task: ${activeTask}
1828
+ - Latest verification: ${latestReport}
1829
+ - Configured commands: ${formatList(result.commands.configured)}
1830
+ - Missing commands: ${formatList(result.commands.missing)}
1831
+
1832
+ ## Next Action
1833
+
1834
+ Run \`${result.nextAction.command}\`.
1835
+
1836
+ ${result.nextAction.reason}
1837
+ `;
1838
+ }
1839
+ async function getAgentLoopStatus(options) {
1840
+ const inGit = await isInsideGitRepo(options.cwd);
1841
+ const rawStatus = inGit ? await getGitStatus(options.cwd) : "";
1842
+ const changedFiles = await parseGitStatus(rawStatus);
1843
+ const timestampedTask = await readTask(
1844
+ options.cwd,
1845
+ await getActiveTaskPath(options) ?? await latestMarkdownFile(path15.join(options.cwd, options.config.paths.tasksDir))
1846
+ );
1847
+ const timestampedReport = await readReport(
1848
+ options.cwd,
1849
+ await latestMarkdownFile(path15.join(options.cwd, options.config.paths.reportsDir), {
1850
+ pattern: verificationReportPattern
1851
+ })
1852
+ );
1853
+ const currentReport = timestampedTask && timestampedReport && timestampedReport.modifiedAtMs < timestampedTask.modifiedAtMs && !isPostVerificationTaskState(timestampedTask) ? void 0 : timestampedReport;
1854
+ const activeTask = stripTaskTimestamp(timestampedTask);
1855
+ const latestReport = stripReportTimestamp(currentReport);
1856
+ const configured = DEFAULT_COMMAND_KEYS.filter((key) => options.config.commands[key]);
1857
+ const missing = DEFAULT_COMMAND_KEYS.filter((key) => !options.config.commands[key]);
1858
+ const nextAction = chooseNextAction({
1859
+ activeTask,
1860
+ latestReport,
1861
+ dirty: changedFiles.length > 0
1862
+ });
1863
+ const withoutMarkdown = {
1864
+ project: options.config.project,
1865
+ git: {
1866
+ isRepository: inGit,
1867
+ branch: inGit ? await getGitBranch(options.cwd) : "",
1868
+ commit: inGit ? await getGitCommit(options.cwd) : ""
1869
+ },
1870
+ workingTree: {
1871
+ dirty: changedFiles.length > 0,
1872
+ changedFileCount: changedFiles.length,
1873
+ changedFiles
1874
+ },
1875
+ activeTask,
1876
+ latestReport,
1877
+ commands: {
1878
+ configured,
1879
+ missing
1880
+ },
1881
+ nextAction
1882
+ };
1883
+ return {
1884
+ ...withoutMarkdown,
1885
+ markdown: renderMarkdown(withoutMarkdown)
1886
+ };
1887
+ }
1888
+
1889
+ // src/cli/commands/status.ts
1890
+ function statusCommand() {
1891
+ return new Command9("status").description("Show active task, latest verification, dirty files, and next action").option("--json", "print machine-readable output").action(async (options) => {
1892
+ const config = await loadAgentLoopConfig(process.cwd());
1893
+ const result = await getAgentLoopStatus({ cwd: process.cwd(), config });
1894
+ if (options.json) {
1895
+ console.log(JSON.stringify(result, null, 2));
1896
+ } else {
1897
+ console.log(result.markdown);
1898
+ }
1899
+ });
1900
+ }
1901
+
1902
+ // src/cli/commands/next.ts
1903
+ import { Command as Command10 } from "commander";
1904
+ function formatTask(result) {
1905
+ if (!result.activeTask) return "none";
1906
+ return `${result.activeTask.title} (${result.activeTask.status}) - ${result.activeTask.path}`;
1907
+ }
1908
+ function formatReport(result) {
1909
+ if (!result.latestReport) return "none";
1910
+ return `${result.latestReport.overallStatus} - ${result.latestReport.path}`;
1911
+ }
1912
+ function toNextActionResult(status) {
1913
+ return {
1914
+ command: status.nextAction.command,
1915
+ reason: status.nextAction.reason,
1916
+ activeTask: status.activeTask ?? null,
1917
+ latestReport: status.latestReport ?? null,
1918
+ workingTree: {
1919
+ dirty: status.workingTree.dirty,
1920
+ changedFileCount: status.workingTree.changedFileCount
1921
+ },
1922
+ commands: status.commands
1923
+ };
1924
+ }
1925
+ function renderNextAction(result) {
1926
+ const workingTree = result.workingTree.dirty ? `dirty (${result.workingTree.changedFileCount} changed file(s))` : "clean";
1927
+ return `# AgentLoopKit Next Action
1928
+
1929
+ Run \`${result.command}\`.
1930
+
1931
+ ${result.reason}
1932
+
1933
+ - Active task: ${formatTask(result)}
1934
+ - Latest verification: ${formatReport(result)}
1935
+ - Working tree: ${workingTree}
1936
+ `;
1937
+ }
1938
+ function nextCommand() {
1939
+ return new Command10("next").description("Show the next recommended loop action").option("--json", "print machine-readable output").action(async (options) => {
1940
+ const config = await loadAgentLoopConfig(process.cwd());
1941
+ const status = await getAgentLoopStatus({ cwd: process.cwd(), config });
1942
+ const result = toNextActionResult(status);
1943
+ if (options.json) {
1944
+ console.log(JSON.stringify(result, null, 2));
1945
+ } else {
1946
+ console.log(renderNextAction(result));
1947
+ }
1125
1948
  });
1126
1949
  }
1127
1950
 
1951
+ // src/cli/commands/task.ts
1952
+ import { Command as Command11 } from "commander";
1953
+ function printTask(task, options) {
1954
+ if (options.json) {
1955
+ console.log(JSON.stringify({ activeTask: task ?? null }, null, 2));
1956
+ return;
1957
+ }
1958
+ if (!task) {
1959
+ console.log("No active task set.");
1960
+ console.log("Run `agentloop task set <path>` to pin one.");
1961
+ return;
1962
+ }
1963
+ console.log(`Active task: ${task.title} (${task.status})`);
1964
+ console.log(task.path);
1965
+ }
1966
+ function printTasks(tasks, options) {
1967
+ if (options.json) {
1968
+ console.log(JSON.stringify({ tasks }, null, 2));
1969
+ return;
1970
+ }
1971
+ if (tasks.length === 0) {
1972
+ console.log("No task contracts found.");
1973
+ console.log('Run `agentloop create-task --title "Your task"` to create one.');
1974
+ return;
1975
+ }
1976
+ console.log("Task contracts:");
1977
+ for (const task of tasks) {
1978
+ const marker = task.active ? "*" : "-";
1979
+ const activeLabel = task.active ? " active" : "";
1980
+ console.log(`${marker} ${task.title} (${task.status})${activeLabel}`);
1981
+ console.log(` ${task.path}`);
1982
+ }
1983
+ }
1984
+ function printTaskContract(task, options) {
1985
+ if (options.json) {
1986
+ console.log(JSON.stringify({ task }, null, 2));
1987
+ return;
1988
+ }
1989
+ process.stdout.write(task.content);
1990
+ }
1991
+ function printUpdatedTask(task, options) {
1992
+ if (options.json) {
1993
+ console.log(JSON.stringify({ task }, null, 2));
1994
+ return;
1995
+ }
1996
+ console.log(`Updated task status: ${task.title} (${task.status})`);
1997
+ console.log(task.path);
1998
+ }
1999
+ function printArchivedTask(task, options) {
2000
+ if (options.json) {
2001
+ console.log(JSON.stringify({ task }, null, 2));
2002
+ return;
2003
+ }
2004
+ console.log(`Archived task: ${task.title} (${task.status})`);
2005
+ console.log(`${task.previousPath} -> ${task.path}`);
2006
+ }
2007
+ function taskCommand() {
2008
+ const command = new Command11("task").description(
2009
+ "List, inspect, update, or archive task contracts"
2010
+ );
2011
+ command.command("list").option("--json", "print machine-readable output").description("List task contracts").action(async (options) => {
2012
+ const config = await loadAgentLoopConfig(process.cwd());
2013
+ const tasks = await listTasks({ cwd: process.cwd(), config });
2014
+ printTasks(tasks, options);
2015
+ });
2016
+ command.command("show").argument("<path>", "task contract path under .agentloop/tasks").option("--json", "print machine-readable output").description("Show a task contract").action(async (taskPath, options) => {
2017
+ const config = await loadAgentLoopConfig(process.cwd());
2018
+ const task = await readTaskContract({ cwd: process.cwd(), config, taskPath });
2019
+ printTaskContract(task, options);
2020
+ });
2021
+ command.command("set").argument("<path>", "task contract path under .agentloop/tasks").option("--json", "print machine-readable output").description("Set the active task contract").action(async (taskPath, options) => {
2022
+ const config = await loadAgentLoopConfig(process.cwd());
2023
+ const activeTask = await setActiveTask({ cwd: process.cwd(), config, taskPath });
2024
+ printTask(activeTask, options);
2025
+ });
2026
+ command.command("status").argument("<path>", "task contract path under .agentloop/tasks").argument("<status>", "one of: proposed, in-progress, blocked, review, done").option("--json", "print machine-readable output").description("Update a task contract status").action(async (taskPath, status, options) => {
2027
+ const config = await loadAgentLoopConfig(process.cwd());
2028
+ const task = await updateTaskStatus({ cwd: process.cwd(), config, taskPath, status });
2029
+ printUpdatedTask(task, options);
2030
+ });
2031
+ command.command("archive").argument("<path>", "task contract path under .agentloop/tasks").option("--json", "print machine-readable output").description("Archive a task contract").action(async (taskPath, options) => {
2032
+ const config = await loadAgentLoopConfig(process.cwd());
2033
+ const task = await archiveTask({ cwd: process.cwd(), config, taskPath });
2034
+ printArchivedTask(task, options);
2035
+ });
2036
+ command.command("current").option("--json", "print machine-readable output").description("Print the active task contract").action(async (options) => {
2037
+ const config = await loadAgentLoopConfig(process.cwd());
2038
+ const activeTask = await getActiveTask({ cwd: process.cwd(), config });
2039
+ printTask(activeTask ?? null, options);
2040
+ });
2041
+ command.command("clear").option("--json", "print machine-readable output").description("Clear the active task pointer").action(async (options) => {
2042
+ const config = await loadAgentLoopConfig(process.cwd());
2043
+ await clearActiveTask({ cwd: process.cwd(), config });
2044
+ printTask(null, options);
2045
+ });
2046
+ return command;
2047
+ }
2048
+
2049
+ // src/cli/commands/completion.ts
2050
+ import { Command as Command12 } from "commander";
2051
+
2052
+ // src/core/completions.ts
2053
+ var COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell", "pwsh"];
2054
+ var topLevelCommands = [
2055
+ ["init", "Generate the repo harness and config"],
2056
+ ["doctor", "Check setup health"],
2057
+ ["create-task", "Create a task contract"],
2058
+ ["verify", "Run configured verification commands"],
2059
+ ["summarize", "Preview a reviewer summary"],
2060
+ ["handoff", "Write a reviewer handoff"],
2061
+ ["status", "Show current loop state"],
2062
+ ["next", "Show the next recommended loop action"],
2063
+ ["check-gates", "Check review gate evidence"],
2064
+ ["report", "Write a local HTML evidence report"],
2065
+ ["badge", "Write a local SVG evidence badge"],
2066
+ ["ci-summary", "Summarize CI context and AgentLoop evidence"],
2067
+ ["release-notes", "Generate deterministic release notes"],
2068
+ ["npm-status", "Check npm registry catch-up status"],
2069
+ ["policy", "List or inspect local AgentLoopKit policies"],
2070
+ ["task", "List, inspect, update, or archive task contracts"],
2071
+ ["install-agent", "Install agent-specific instructions"],
2072
+ ["list-templates", "List bundled templates"],
2073
+ ["completion", "Print shell completion scripts"],
2074
+ ["version", "Print the CLI version"]
2075
+ ];
2076
+ var taskCommandSpecs = [
2077
+ ["list", "List task contracts"],
2078
+ ["show", "Show a task contract"],
2079
+ ["set", "Set the active task contract"],
2080
+ ["status", "Update a task contract status"],
2081
+ ["archive", "Archive a task contract"],
2082
+ ["current", "Print the active task contract"],
2083
+ ["clear", "Clear the active task pointer"]
2084
+ ];
2085
+ var taskCommands = taskCommandSpecs.map(([name]) => name);
2086
+ var policyCommandSpecs = [
2087
+ ["list", "List local policies"],
2088
+ ["show", "Show a local policy"],
2089
+ ["status", "Show local policy template status"]
2090
+ ];
2091
+ var policyCommands = policyCommandSpecs.map(([name]) => name);
2092
+ var taskStatuses = ["proposed", "in-progress", "blocked", "review", "done"];
2093
+ var agentNames = [
2094
+ "codex",
2095
+ "claude-code",
2096
+ "cursor",
2097
+ "opencode",
2098
+ "gemini-cli",
2099
+ "github-copilot-cli",
2100
+ "generic",
2101
+ "all"
2102
+ ];
2103
+ function parseShell(shell) {
2104
+ const clean = shell.trim().toLowerCase();
2105
+ if (COMPLETION_SHELLS.includes(clean)) return clean;
2106
+ throw new AgentLoopError(
2107
+ `Unsupported shell "${shell}". Use one of: ${COMPLETION_SHELLS.join(", ")}.`
2108
+ );
2109
+ }
2110
+ function renderBash() {
2111
+ const commands = topLevelCommands.map(([name]) => name).join(" ");
2112
+ return `# AgentLoopKit bash completion
2113
+ # Save with: agentloop completion bash > ~/.agentloop-completion.bash
2114
+ # Then source it from your shell profile.
2115
+ _agentloop_completion() {
2116
+ local current previous
2117
+ COMPREPLY=()
2118
+ current="\${COMP_WORDS[COMP_CWORD]}"
2119
+ previous="\${COMP_WORDS[COMP_CWORD-1]}"
2120
+
2121
+ case "\${COMP_WORDS[1]}" in
2122
+ task)
2123
+ case "\${COMP_WORDS[2]}" in
2124
+ status)
2125
+ if [[ \${COMP_CWORD} -eq 4 ]]; then
2126
+ COMPREPLY=( $(compgen -W "${taskStatuses.join(" ")}" -- "$current") )
2127
+ return 0
2128
+ fi
2129
+ ;;
2130
+ *)
2131
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
2132
+ COMPREPLY=( $(compgen -W "${taskCommands.join(" ")}" -- "$current") )
2133
+ return 0
2134
+ fi
2135
+ ;;
2136
+ esac
2137
+ ;;
2138
+ policy)
2139
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
2140
+ COMPREPLY=( $(compgen -W "${policyCommands.join(" ")}" -- "$current") )
2141
+ return 0
2142
+ fi
2143
+ ;;
2144
+ install-agent)
2145
+ COMPREPLY=( $(compgen -W "${agentNames.join(" ")}" -- "$current") )
2146
+ return 0
2147
+ ;;
2148
+ completion)
2149
+ COMPREPLY=( $(compgen -W "${COMPLETION_SHELLS.join(" ")}" -- "$current") )
2150
+ return 0
2151
+ ;;
2152
+ esac
2153
+
2154
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
2155
+ COMPREPLY=( $(compgen -W "${commands}" -- "$current") )
2156
+ fi
2157
+ }
2158
+
2159
+ complete -F _agentloop_completion agentloop
2160
+ complete -F _agentloop_completion agentloopkit
2161
+ `;
2162
+ }
2163
+ function renderZsh() {
2164
+ const commandSpecs = topLevelCommands.map(([name, description]) => `${name}:${description}`);
2165
+ return `#compdef agentloop agentloopkit
2166
+ # AgentLoopKit zsh completion
2167
+ # Save with: agentloop completion zsh > ~/.zsh/completions/_agentloop
2168
+
2169
+ _agentloop() {
2170
+ local context state line
2171
+ typeset -A opt_args
2172
+
2173
+ _arguments -C \\
2174
+ '1:command:->commands' \\
2175
+ '*::arg:->args'
2176
+
2177
+ case "$state" in
2178
+ commands)
2179
+ _describe 'agentloop command' commandSpecs
2180
+ ;;
2181
+ args)
2182
+ case "$words[2]" in
2183
+ task)
2184
+ if [[ "$words[3]" == "status" && CURRENT -eq 5 ]]; then
2185
+ _values 'task status' ${taskStatuses.map((status) => `"${status}"`).join(" ")}
2186
+ else
2187
+ _describe 'task command' taskCommandSpecs
2188
+ fi
2189
+ ;;
2190
+ policy)
2191
+ _describe 'policy command' policyCommandSpecs
2192
+ ;;
2193
+ install-agent)
2194
+ _values 'agent' ${agentNames.map((agent) => `"${agent}"`).join(" ")}
2195
+ ;;
2196
+ completion)
2197
+ _values 'shell' ${COMPLETION_SHELLS.map((shell) => `"${shell}"`).join(" ")}
2198
+ ;;
2199
+ esac
2200
+ ;;
2201
+ esac
2202
+ }
2203
+
2204
+ commandSpecs=(
2205
+ ${commandSpecs.map((spec) => ` '${spec}'`).join("\n")}
2206
+ )
2207
+
2208
+ taskCommandSpecs=(
2209
+ ${taskCommandSpecs.map(([name, description]) => ` '${name}:${description}'`).join("\n")}
2210
+ )
2211
+
2212
+ policyCommandSpecs=(
2213
+ ${policyCommandSpecs.map(([name, description]) => ` '${name}:${description}'`).join("\n")}
2214
+ )
2215
+
2216
+ _agentloop "$@"
2217
+ `;
2218
+ }
2219
+ function fishLine(command, args, description) {
2220
+ return `complete -c agentloop -n '${command}' -a '${args.join(" ")}' -d '${description}'`;
2221
+ }
2222
+ function renderFish() {
2223
+ const topCommands = topLevelCommands.map(([name]) => name);
2224
+ return `# AgentLoopKit fish completion
2225
+ # Save with: agentloop completion fish > ~/.config/fish/completions/agentloop.fish
2226
+
2227
+ complete -c agentloop -f
2228
+ complete -c agentloopkit -f
2229
+ ${fishLine("__fish_use_subcommand", topCommands, "AgentLoopKit command")}
2230
+ complete -c agentloop -n '__fish_seen_subcommand_from task' -a '${taskCommands.join(" ")}' -d 'Task command'
2231
+ complete -c agentloop -n '__fish_seen_subcommand_from policy' -a '${policyCommands.join(" ")}' -d 'Policy command'
2232
+ complete -c agentloop -n '__fish_seen_subcommand_from status' -a '${taskStatuses.join(" ")}' -d 'Task status'
2233
+ complete -c agentloop -n '__fish_seen_subcommand_from install-agent' -a '${agentNames.join(" ")}' -d 'Agent name'
2234
+ complete -c agentloop -n '__fish_seen_subcommand_from completion' -a '${COMPLETION_SHELLS.join(" ")}' -d 'Shell'
2235
+ complete -c agentloopkit -n '__fish_use_subcommand' -a '${topCommands.join(" ")}' -d 'AgentLoopKit command'
2236
+ complete -c agentloopkit -n '__fish_seen_subcommand_from task' -a '${taskCommands.join(" ")}' -d 'Task command'
2237
+ complete -c agentloopkit -n '__fish_seen_subcommand_from policy' -a '${policyCommands.join(" ")}' -d 'Policy command'
2238
+ complete -c agentloopkit -n '__fish_seen_subcommand_from status' -a '${taskStatuses.join(" ")}' -d 'Task status'
2239
+ complete -c agentloopkit -n '__fish_seen_subcommand_from install-agent' -a '${agentNames.join(" ")}' -d 'Agent name'
2240
+ complete -c agentloopkit -n '__fish_seen_subcommand_from completion' -a '${COMPLETION_SHELLS.join(" ")}' -d 'Shell'
2241
+ `;
2242
+ }
2243
+ function powerShellArray(values) {
2244
+ return `@(${values.map((value) => `'${value}'`).join(", ")})`;
2245
+ }
2246
+ function renderPowerShell() {
2247
+ const topCommands = topLevelCommands.map(([name]) => name);
2248
+ return `# AgentLoopKit PowerShell completion
2249
+ # Save with: agentloop completion powershell > agentloop-completion.ps1
2250
+ # Review the script before dot-sourcing it from your PowerShell startup file.
2251
+
2252
+ $AgentLoopCommands = ${powerShellArray(topCommands)}
2253
+ $AgentLoopTaskCommands = ${powerShellArray(taskCommands)}
2254
+ $AgentLoopPolicyCommands = ${powerShellArray(policyCommands)}
2255
+ $AgentLoopTaskStatuses = ${powerShellArray(taskStatuses)}
2256
+ $AgentLoopAgents = ${powerShellArray(agentNames)}
2257
+ $AgentLoopShells = ${powerShellArray(COMPLETION_SHELLS)}
2258
+
2259
+ Register-ArgumentCompleter -Native -CommandName agentloop, agentloopkit -ScriptBlock {
2260
+ param($wordToComplete, $commandAst, $cursorPosition)
2261
+
2262
+ $words = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text })
2263
+ $prefix = if ($null -eq $wordToComplete) { '' } else { $wordToComplete }
2264
+ $values = $AgentLoopCommands
2265
+
2266
+ if ($words.Count -gt 1) {
2267
+ switch ($words[1]) {
2268
+ 'task' {
2269
+ if ($words.Count -gt 2 -and $words[2] -eq 'status') {
2270
+ $values = $AgentLoopTaskStatuses
2271
+ } else {
2272
+ $values = $AgentLoopTaskCommands
2273
+ }
2274
+ }
2275
+ 'policy' { $values = $AgentLoopPolicyCommands }
2276
+ 'install-agent' { $values = $AgentLoopAgents }
2277
+ 'completion' { $values = $AgentLoopShells }
2278
+ }
2279
+ }
2280
+
2281
+ $values |
2282
+ Where-Object { $_.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase) } |
2283
+ ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
2284
+ }
2285
+ `;
2286
+ }
2287
+ function renderCompletionScript(shell) {
2288
+ switch (parseShell(shell)) {
2289
+ case "bash":
2290
+ return renderBash();
2291
+ case "zsh":
2292
+ return renderZsh();
2293
+ case "fish":
2294
+ return renderFish();
2295
+ case "powershell":
2296
+ case "pwsh":
2297
+ return renderPowerShell();
2298
+ }
2299
+ }
2300
+
2301
+ // src/cli/commands/completion.ts
2302
+ function completionCommand() {
2303
+ return new Command12("completion").description("Print shell completion scripts").argument("<shell>", "one of: bash, zsh, fish, powershell, pwsh").action((shell) => {
2304
+ process.stdout.write(renderCompletionScript(shell));
2305
+ });
2306
+ }
2307
+
2308
+ // src/cli/commands/check-gates.ts
2309
+ import { Command as Command13 } from "commander";
2310
+
2311
+ // src/core/check-gates.ts
2312
+ import path16 from "path";
2313
+ import { readFile as readFile10 } from "fs/promises";
2314
+ var requiredRootFiles = ["AGENTS.md", "AGENTLOOP.md", "agentloop.config.json"];
2315
+ var requiredHarnessFiles = [
2316
+ ".agentloop/harness/commands.md",
2317
+ ".agentloop/harness/definition-of-done.md",
2318
+ ".agentloop/harness/review-checklist.md",
2319
+ ".agentloop/harness/autonomous-work-rules.md"
2320
+ ];
2321
+ var requiredPolicyFiles = [
2322
+ ".agentloop/policies/no-destructive-actions.md",
2323
+ ".agentloop/policies/git-policy.md",
2324
+ ".agentloop/policies/secrets-policy.md"
2325
+ ];
2326
+ function extractHeading3(markdown, fallback) {
2327
+ return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
2328
+ }
2329
+ function extractOverallStatus2(markdown) {
2330
+ return markdown.match(/Overall status:\s*([a-z-]+)/i)?.[1]?.trim() || "unknown";
2331
+ }
2332
+ function relativePath(cwd, filePath) {
2333
+ return path16.relative(cwd, filePath) || ".";
2334
+ }
2335
+ async function missingFiles(cwd, files) {
2336
+ const missing = [];
2337
+ for (const file of files) {
2338
+ if (!await pathExists(path16.join(cwd, file))) missing.push(file);
2339
+ }
2340
+ return missing;
2341
+ }
2342
+ function gate(id, name, status, message, filePath) {
2343
+ return { id, name, status, message, ...filePath ? { path: filePath } : {} };
2344
+ }
2345
+ function overallStatus(gates, strict) {
2346
+ if (gates.some((item) => item.status === "fail")) return "fail";
2347
+ if (strict && gates.some((item) => item.status === "warn")) return "fail";
2348
+ if (gates.some((item) => item.status === "warn")) return "warn";
2349
+ return "pass";
2350
+ }
2351
+ function chooseNextAction2(gates) {
2352
+ const task = gates.find((item) => item.id === "task-contract");
2353
+ const report = gates.find((item) => item.id === "verification-report");
2354
+ const handoff = gates.find((item) => item.id === "handoff-summary");
2355
+ if (task?.status === "fail") {
2356
+ return {
2357
+ command: "agentloop create-task",
2358
+ reason: "Create a task contract before review gates can pass."
2359
+ };
2360
+ }
2361
+ if (report?.status === "fail") {
2362
+ return {
2363
+ command: "agentloop verify",
2364
+ reason: "Run verification and fix failures before review."
2365
+ };
2366
+ }
2367
+ if (handoff?.status !== "pass") {
2368
+ return {
2369
+ command: "agentloop handoff",
2370
+ reason: "Write a reviewer handoff after verification."
2371
+ };
2372
+ }
2373
+ return {
2374
+ command: "agentloop handoff",
2375
+ reason: "Gate evidence is present. Refresh the reviewer handoff if the diff changed."
2376
+ };
2377
+ }
2378
+ function renderMarkdown2(result) {
2379
+ const gateLines = result.gates.map((item) => {
2380
+ const suffix = item.path ? ` - ${item.path}` : "";
2381
+ return `- [${item.status}] ${item.name}: ${item.message}${suffix}`;
2382
+ }).join("\n");
2383
+ const gitLine = result.git.isRepository ? `${result.git.branch || "unknown branch"}${result.git.commit ? ` @ ${result.git.commit}` : ""}` : "not inside a git repository";
2384
+ return `# AgentLoopKit Gates
2385
+
2386
+ - Overall status: ${result.overallStatus}
2387
+ - Strict mode: ${result.strict ? "enabled (warnings fail)" : "disabled"}
2388
+ - Git: ${gitLine}
2389
+ - Changed files: ${result.git.changedFileCount}
2390
+
2391
+ ## Gates
2392
+
2393
+ ${gateLines}
2394
+
2395
+ ## Next Action
2396
+
2397
+ Run \`${result.nextAction.command}\`.
2398
+
2399
+ ${result.nextAction.reason}
2400
+ `;
2401
+ }
2402
+ async function checkGates(options) {
2403
+ const strict = options.strict ?? false;
2404
+ const taskPath = await getActiveTaskPath(options) ?? await latestMarkdownFile(path16.join(options.cwd, options.config.paths.tasksDir));
2405
+ const reportPath = await latestMarkdownFile(path16.join(options.cwd, options.config.paths.reportsDir), {
2406
+ pattern: verificationReportPattern
2407
+ });
2408
+ const handoffPath = await latestMarkdownFile(path16.join(options.cwd, options.config.paths.handoffsDir), {
2409
+ pattern: prSummaryPattern
2410
+ });
2411
+ const gates = [];
2412
+ if (taskPath) {
2413
+ const taskMarkdown = await readFile10(taskPath, "utf8");
2414
+ gates.push(
2415
+ gate(
2416
+ "task-contract",
2417
+ "Task contract",
2418
+ "pass",
2419
+ extractHeading3(taskMarkdown, path16.basename(taskPath, ".md")),
2420
+ relativePath(options.cwd, taskPath)
2421
+ )
2422
+ );
2423
+ } else {
2424
+ gates.push(gate("task-contract", "Task contract", "fail", "No task contract found."));
2425
+ }
2426
+ if (reportPath) {
2427
+ const reportMarkdown = await readFile10(reportPath, "utf8");
2428
+ const status = extractOverallStatus2(reportMarkdown);
2429
+ gates.push(
2430
+ gate(
2431
+ "verification-report",
2432
+ "Verification report",
2433
+ status === "pass" ? "pass" : "fail",
2434
+ `Overall status: ${status}`,
2435
+ relativePath(options.cwd, reportPath)
2436
+ )
2437
+ );
2438
+ } else {
2439
+ gates.push(
2440
+ gate("verification-report", "Verification report", "fail", "No verification report found.")
2441
+ );
2442
+ }
2443
+ if (handoffPath) {
2444
+ gates.push(
2445
+ gate(
2446
+ "handoff-summary",
2447
+ "Handoff summary",
2448
+ "pass",
2449
+ "Reviewer handoff found.",
2450
+ relativePath(options.cwd, handoffPath)
2451
+ )
2452
+ );
2453
+ } else {
2454
+ gates.push(gate("handoff-summary", "Handoff summary", "warn", "No handoff summary found."));
2455
+ }
2456
+ const missingHarness = await missingFiles(options.cwd, [
2457
+ ...requiredRootFiles,
2458
+ ...requiredHarnessFiles
2459
+ ]);
2460
+ gates.push(
2461
+ gate(
2462
+ "repo-harness",
2463
+ "Repo harness",
2464
+ missingHarness.length ? "warn" : "pass",
2465
+ missingHarness.length ? `Missing harness files: ${missingHarness.join(", ")}.` : "Required repo and harness files exist."
2466
+ )
2467
+ );
2468
+ const missingPolicies = await missingFiles(options.cwd, requiredPolicyFiles);
2469
+ gates.push(
2470
+ gate(
2471
+ "safety-policies",
2472
+ "Safety policies",
2473
+ missingPolicies.length ? "warn" : "pass",
2474
+ missingPolicies.length ? `Missing policy files: ${missingPolicies.join(", ")}.` : "Core safety policy files exist."
2475
+ )
2476
+ );
2477
+ const inGit = await isInsideGitRepo(options.cwd);
2478
+ const changedFiles = inGit ? await parseGitStatus(await getGitStatus(options.cwd)) : [];
2479
+ gates.push(
2480
+ gate(
2481
+ "git-context",
2482
+ "Git context",
2483
+ !inGit || changedFiles.length === 0 ? "warn" : "pass",
2484
+ !inGit ? "Not inside a git repository." : changedFiles.length === 0 ? "No changed files detected." : `${changedFiles.length} changed file(s) detected.`
2485
+ )
2486
+ );
2487
+ const withoutMarkdown = {
2488
+ strict,
2489
+ overallStatus: overallStatus(gates, strict),
2490
+ gates,
2491
+ git: {
2492
+ isRepository: inGit,
2493
+ branch: inGit ? await getGitBranch(options.cwd) : "",
2494
+ commit: inGit ? await getGitCommit(options.cwd) : "",
2495
+ changedFileCount: changedFiles.length
2496
+ },
2497
+ nextAction: chooseNextAction2(gates)
2498
+ };
2499
+ return { ...withoutMarkdown, markdown: renderMarkdown2(withoutMarkdown) };
2500
+ }
2501
+
2502
+ // src/cli/commands/check-gates.ts
2503
+ function checkGatesCommand() {
2504
+ return new Command13("check-gates").description("Check whether task, verification, handoff, harness, policy, and git gates pass").option("--json", "print machine-readable output").option("--strict", "treat warning gates as failures").action(async (options) => {
2505
+ const config = await loadAgentLoopConfig(process.cwd());
2506
+ const result = await checkGates({ cwd: process.cwd(), config, strict: options.strict });
2507
+ if (options.json) {
2508
+ console.log(JSON.stringify(result, null, 2));
2509
+ } else {
2510
+ console.log(result.markdown);
2511
+ }
2512
+ if (result.overallStatus === "fail") process.exitCode = 1;
2513
+ });
2514
+ }
2515
+
2516
+ // src/cli/commands/report.ts
2517
+ import { Command as Command14 } from "commander";
2518
+
2519
+ // src/core/html-report.ts
2520
+ import path17 from "path";
2521
+ import { readFile as readFile11, readdir as readdir6, stat as stat5 } from "fs/promises";
2522
+ function escapeHtml(value) {
2523
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2524
+ }
2525
+ function extractLine2(markdown, pattern, fallback) {
2526
+ if (!markdown) return fallback;
2527
+ return markdown.match(pattern)?.[1]?.trim() || fallback;
2528
+ }
2529
+ function extractTitle(markdown, fallback) {
2530
+ return extractLine2(markdown, /^#\s+(.+)$/m, fallback);
2531
+ }
2532
+ function extractVerificationStatus(markdown) {
2533
+ return extractLine2(markdown, /Overall status:\s*([a-z-]+)/i, "not available");
2534
+ }
2535
+ function normalizeDisplayPath(cwd, filePath) {
2536
+ if (!filePath) return void 0;
2537
+ const absolutePath = path17.isAbsolute(filePath) ? filePath : path17.resolve(cwd, filePath);
2538
+ return path17.relative(cwd, absolutePath).split(path17.sep).join("/") || ".";
2539
+ }
2540
+ function resolveUserPath(cwd, filePath) {
2541
+ if (!filePath) return void 0;
2542
+ return path17.isAbsolute(filePath) ? path17.resolve(filePath) : path17.resolve(cwd, filePath);
2543
+ }
2544
+ async function readMarkdownIfExists(filePath) {
2545
+ if (!filePath || !await pathExists(filePath)) return void 0;
2546
+ return readFile11(filePath, "utf8");
2547
+ }
2548
+ async function latestMatchingMarkdownFile(dir, pattern) {
2549
+ if (!await pathExists(dir)) return void 0;
2550
+ const entries = await Promise.all(
2551
+ (await readdir6(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && pattern.test(entry.name)).map(async (entry) => {
2552
+ const filePath = path17.join(dir, entry.name);
2553
+ const fileStat = await stat5(filePath);
2554
+ return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
2555
+ })
2556
+ );
2557
+ entries.sort((left, right) => {
2558
+ if (left.mtimeMs !== right.mtimeMs) return left.mtimeMs - right.mtimeMs;
2559
+ return left.name.localeCompare(right.name);
2560
+ });
2561
+ return entries.at(-1)?.filePath;
2562
+ }
2563
+ function renderSourcePath(label, filePath) {
2564
+ return `<li><span>${escapeHtml(label)}</span><code>${escapeHtml(filePath ?? "not found")}</code></li>`;
2565
+ }
2566
+ function renderChangedFiles(changedFiles) {
2567
+ if (!changedFiles.length) return '<p class="muted">No changed files detected.</p>';
2568
+ return `<table>
2569
+ <thead><tr><th>Status</th><th>Path</th></tr></thead>
2570
+ <tbody>
2571
+ ${changedFiles.map(
2572
+ (file) => ` <tr><td><code>${escapeHtml(file.status)}</code></td><td><code>${escapeHtml(
2573
+ file.path
2574
+ )}</code></td></tr>`
2575
+ ).join("\n")}
2576
+ </tbody>
2577
+ </table>`;
2578
+ }
2579
+ function renderMarkdownBlock(label, markdown, emptyText) {
2580
+ return `<section>
2581
+ <h2>${escapeHtml(label)}</h2>
2582
+ ${markdown?.trim() ? `<pre>${escapeHtml(markdown.trim())}</pre>` : `<p class="muted">${escapeHtml(emptyText)}</p>`}
2583
+ </section>`;
2584
+ }
2585
+ function generateHtmlReport(input) {
2586
+ const taskTitle = extractTitle(input.taskMarkdown, "No task contract found");
2587
+ const verificationStatus = extractVerificationStatus(input.verificationMarkdown);
2588
+ const generatedAt = input.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
2589
+ const metadata = {
2590
+ timestamp: input.timestamp,
2591
+ repoName: input.repoName,
2592
+ taskTitle,
2593
+ verificationStatus,
2594
+ changedFileCount: input.changedFiles.length
2595
+ };
2596
+ const html = `<!doctype html>
2597
+ <html lang="en">
2598
+ <head>
2599
+ <meta charset="utf-8">
2600
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2601
+ <title>AgentLoopKit Report - ${escapeHtml(input.repoName)}</title>
2602
+ <style>
2603
+ :root { color-scheme: light; --ink: oklch(22% 0.026 245); --muted: oklch(48% 0.023 245); --line: oklch(84% 0.018 245); --panel: oklch(97% 0.009 245); --paper: oklch(99% 0.006 245); --accent: oklch(49% 0.09 183); --warn-ink: oklch(42% 0.12 52); --warn-bg: oklch(96% 0.035 72); --terminal: oklch(19% 0.024 245); --terminal-ink: oklch(91% 0.014 245); }
2604
+ * { box-sizing: border-box; }
2605
+ body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: var(--paper); line-height: 1.55; }
2606
+ main { max-width: 1040px; margin: 0 auto; padding: 44px 24px 56px; }
2607
+ header { border-bottom: 1px solid var(--line); padding-bottom: 24px; margin-bottom: 28px; }
2608
+ .eyebrow { color: var(--accent); font-size: 13px; font-weight: 700; letter-spacing: 0; text-transform: uppercase; }
2609
+ h1 { margin: 8px 0 12px; font-size: clamp(32px, 5vw, 54px); line-height: 1.03; letter-spacing: 0; }
2610
+ h2 { margin: 0 0 12px; font-size: 21px; letter-spacing: 0; }
2611
+ section { border-top: 1px solid var(--line); padding: 24px 0; }
2612
+ .summary { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); margin-top: 18px; }
2613
+ .metric { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 14px; }
2614
+ .metric span { display: block; color: var(--muted); font-size: 13px; }
2615
+ .metric strong { display: block; margin-top: 5px; font-size: 16px; overflow-wrap: anywhere; }
2616
+ ul.sources { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; }
2617
+ ul.sources li { display: grid; gap: 4px; }
2618
+ .muted { color: var(--muted); }
2619
+ .safety { border: 1px solid oklch(78% 0.08 62); color: var(--warn-ink); background: var(--warn-bg); padding: 14px 16px; border-radius: 6px; }
2620
+ table { width: 100%; border-collapse: collapse; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
2621
+ th, td { text-align: left; border-bottom: 1px solid var(--line); padding: 10px 12px; vertical-align: top; }
2622
+ th { background: var(--panel); font-size: 13px; color: var(--muted); }
2623
+ tr:last-child td { border-bottom: 0; }
2624
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 0.92em; overflow-wrap: anywhere; }
2625
+ pre { margin: 0; padding: 16px; overflow: auto; border: 1px solid var(--line); border-radius: 8px; background: var(--terminal); color: var(--terminal-ink); font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
2626
+ </style>
2627
+ </head>
2628
+ <body>
2629
+ <main>
2630
+ <header>
2631
+ <div class="eyebrow">AgentLoopKit local evidence report</div>
2632
+ <h1>${escapeHtml(taskTitle)}</h1>
2633
+ <p class="muted">Generated ${escapeHtml(generatedAt)} from local repository artifacts. No LLM, network call, telemetry, or command execution is required to render this report.</p>
2634
+ <div class="summary">
2635
+ <div class="metric"><span>Repository</span><strong>${escapeHtml(input.repoName)}</strong></div>
2636
+ <div class="metric"><span>Branch</span><strong>${escapeHtml(input.branch || "not available")}</strong></div>
2637
+ <div class="metric"><span>Commit</span><strong>${escapeHtml(input.commit || "not available")}</strong></div>
2638
+ <div class="metric"><span>Working tree</span><strong>${escapeHtml(input.workingTreeStatus)}</strong></div>
2639
+ <div class="metric"><span>Verification</span><strong>${escapeHtml(verificationStatus)}</strong></div>
2640
+ <div class="metric"><span>Changed files</span><strong>${input.changedFiles.length}</strong></div>
2641
+ </div>
2642
+ </header>
2643
+
2644
+ <section>
2645
+ <h2>Source Artifacts</h2>
2646
+ <ul class="sources">
2647
+ ${renderSourcePath("Task contract", input.taskPath)}
2648
+ ${renderSourcePath("Verification report", input.verificationPath)}
2649
+ ${renderSourcePath("Handoff summary", input.handoffPath)}
2650
+ </ul>
2651
+ </section>
2652
+
2653
+ <section>
2654
+ <h2>Changed Files</h2>
2655
+ ${renderChangedFiles(input.changedFiles)}
2656
+ </section>
2657
+
2658
+ ${renderMarkdownBlock("Task Contract", input.taskMarkdown, "No task contract was found.")}
2659
+ ${renderMarkdownBlock("Verification Evidence", input.verificationMarkdown, "No verification report was found.")}
2660
+ ${renderMarkdownBlock("Reviewer Handoff", input.handoffMarkdown, "No handoff summary was found.")}
2661
+ ${renderMarkdownBlock("Current Deterministic Summary", input.summaryMarkdown, "No deterministic summary was generated.")}
2662
+
2663
+ <section>
2664
+ <h2>Diff Stats</h2>
2665
+ <pre>${escapeHtml(input.diffStat?.trim() || "No diff stats available.")}</pre>
2666
+ </section>
2667
+
2668
+ <section>
2669
+ <h2>Safety Notes</h2>
2670
+ <div class="safety">This report is a local static artifact. It must not be treated as proof that verification passed unless the verification section says so. Review protected areas such as secrets, auth, billing, deployment, migrations, lockfiles, and public APIs before merge.</div>
2671
+ </section>
2672
+ </main>
2673
+ </body>
2674
+ </html>
2675
+ `;
2676
+ return { html, metadata };
2677
+ }
2678
+ async function writeHtmlReport(options) {
2679
+ const timestamp = options.timestamp ?? formatTimestamp();
2680
+ const cwd = options.cwd;
2681
+ const taskPath = resolveUserPath(cwd, options.taskPath) ?? await getActiveTaskPath({ cwd, config: options.config }) ?? await latestMarkdownFile(path17.join(cwd, options.config.paths.tasksDir));
2682
+ const verificationPath = resolveUserPath(cwd, options.reportPath) ?? await latestMatchingMarkdownFile(
2683
+ path17.join(cwd, options.config.paths.reportsDir),
2684
+ /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-verification-report\.md$/
2685
+ );
2686
+ const handoffPath = resolveUserPath(cwd, options.handoffPath) ?? await latestMatchingMarkdownFile(
2687
+ path17.join(cwd, options.config.paths.handoffsDir),
2688
+ /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-pr-summary\.md$/
2689
+ );
2690
+ const [status, branch, commit, diffStat, taskMarkdown, verificationMarkdown, handoffMarkdown] = await Promise.all([
2691
+ getGitStatus(cwd),
2692
+ getGitBranch(cwd),
2693
+ getGitCommit(cwd),
2694
+ getGitDiffStat(cwd),
2695
+ readMarkdownIfExists(taskPath),
2696
+ readMarkdownIfExists(verificationPath),
2697
+ readMarkdownIfExists(handoffPath)
2698
+ ]);
2699
+ const changedFiles = await parseGitStatus(status);
2700
+ const summary = await summarizeRepository({
2701
+ cwd,
2702
+ config: options.config,
2703
+ taskPath,
2704
+ reportPath: verificationPath,
2705
+ timestamp,
2706
+ write: false
2707
+ });
2708
+ const report = generateHtmlReport({
2709
+ timestamp,
2710
+ nowIso: options.nowIso,
2711
+ repoName: options.config.project.name || path17.basename(cwd),
2712
+ branch,
2713
+ commit,
2714
+ workingTreeStatus: status.trim() ? "dirty" : "clean or unavailable",
2715
+ taskPath: normalizeDisplayPath(cwd, taskPath),
2716
+ verificationPath: normalizeDisplayPath(cwd, verificationPath),
2717
+ handoffPath: normalizeDisplayPath(cwd, handoffPath),
2718
+ changedFiles,
2719
+ diffStat,
2720
+ taskMarkdown,
2721
+ verificationMarkdown,
2722
+ handoffMarkdown,
2723
+ summaryMarkdown: summary.markdown
2724
+ });
2725
+ const outPath = resolveUserPath(cwd, options.outPath) ?? path17.join(cwd, options.config.paths.reportsDir, `${timestamp}-agentloop-report.html`);
2726
+ await writeTextFile(outPath, report.html);
2727
+ return {
2728
+ ...report,
2729
+ outPath,
2730
+ sourcePaths: {
2731
+ task: normalizeDisplayPath(cwd, taskPath),
2732
+ verification: normalizeDisplayPath(cwd, verificationPath),
2733
+ handoff: normalizeDisplayPath(cwd, handoffPath)
2734
+ }
2735
+ };
2736
+ }
2737
+
2738
+ // src/cli/commands/report.ts
2739
+ function reportCommand() {
2740
+ return new Command14("report").description("Generate a local static HTML evidence report").option("--task <path>", "task contract path").option("--report <path>", "verification report path").option("--handoff <path>", "handoff summary path").option("--out <path>", "output HTML path").option("--json", "print machine-readable output").action(
2741
+ async (options) => {
2742
+ const config = await loadAgentLoopConfig(process.cwd());
2743
+ const result = await writeHtmlReport({
2744
+ cwd: process.cwd(),
2745
+ config,
2746
+ taskPath: options.task,
2747
+ reportPath: options.report,
2748
+ handoffPath: options.handoff,
2749
+ outPath: options.out
2750
+ });
2751
+ if (options.json) {
2752
+ console.log(
2753
+ JSON.stringify(
2754
+ {
2755
+ outPath: result.outPath,
2756
+ metadata: result.metadata,
2757
+ sourcePaths: result.sourcePaths
2758
+ },
2759
+ null,
2760
+ 2
2761
+ )
2762
+ );
2763
+ } else {
2764
+ console.log(`# AgentLoopKit HTML Report
2765
+
2766
+ Report written: ${result.outPath}
2767
+ Task: ${result.metadata.taskTitle}
2768
+ Verification: ${result.metadata.verificationStatus}
2769
+ Changed files: ${result.metadata.changedFileCount}
2770
+
2771
+ Next step: open the HTML file locally or attach it as a CI/PR artifact.`);
2772
+ }
2773
+ }
2774
+ );
2775
+ }
2776
+
2777
+ // src/cli/commands/badge.ts
2778
+ import { Command as Command15 } from "commander";
2779
+
2780
+ // src/core/badge.ts
2781
+ import path18 from "path";
2782
+ import { readFile as readFile12, readdir as readdir7, stat as stat6 } from "fs/promises";
2783
+ var verificationReportPattern2 = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-verification-report\.md$/;
2784
+ function escapeXml(value) {
2785
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2786
+ }
2787
+ function statusColor(status) {
2788
+ switch (status) {
2789
+ case "pass":
2790
+ return "#2f855a";
2791
+ case "fail":
2792
+ return "#c2410c";
2793
+ case "warn":
2794
+ case "not-run":
2795
+ case "missing":
2796
+ case "unknown":
2797
+ return "#a16207";
2798
+ }
2799
+ }
2800
+ function textWidth(value) {
2801
+ return Math.max(44, value.length * 7 + 18);
2802
+ }
2803
+ function parseSource(source) {
2804
+ const clean = source?.trim().toLowerCase() || "verification";
2805
+ if (clean === "verification" || clean === "gates") return clean;
2806
+ throw new AgentLoopError("Unsupported badge source. Use one of: verification, gates.");
2807
+ }
2808
+ async function latestVerificationReport(dir) {
2809
+ if (!await pathExists(dir)) return void 0;
2810
+ const entries = await Promise.all(
2811
+ (await readdir7(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && verificationReportPattern2.test(entry.name)).map(async (entry) => {
2812
+ const filePath = path18.join(dir, entry.name);
2813
+ const fileStat = await stat6(filePath);
2814
+ return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
2815
+ })
2816
+ );
2817
+ entries.sort((left, right) => {
2818
+ if (left.mtimeMs !== right.mtimeMs) return left.mtimeMs - right.mtimeMs;
2819
+ return left.name.localeCompare(right.name);
2820
+ });
2821
+ return entries.at(-1)?.filePath;
2822
+ }
2823
+ function extractVerificationStatus2(markdown) {
2824
+ const status = markdown.match(/Overall status:\s*([a-z-]+)/i)?.[1]?.trim().toLowerCase();
2825
+ if (status === "pass" || status === "fail" || status === "not-run") return status;
2826
+ return "unknown";
2827
+ }
2828
+ function normalizeDisplayPath2(cwd, filePath) {
2829
+ if (!filePath) return void 0;
2830
+ return path18.relative(cwd, filePath).split(path18.sep).join("/") || ".";
2831
+ }
2832
+ function generateSvgBadge(input) {
2833
+ const labelWidth = textWidth(input.label);
2834
+ const messageWidth = textWidth(input.message);
2835
+ const totalWidth = labelWidth + messageWidth;
2836
+ const color = statusColor(input.status);
2837
+ const label = escapeXml(input.label);
2838
+ const message = escapeXml(input.message);
2839
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${label}: ${message}">
2840
+ <title>${label}: ${message}</title>
2841
+ <linearGradient id="s" x2="0" y2="100%">
2842
+ <stop offset="0" stop-color="#fff" stop-opacity=".08"/>
2843
+ <stop offset="1" stop-opacity=".08"/>
2844
+ </linearGradient>
2845
+ <clipPath id="r"><rect width="${totalWidth}" height="20" rx="4"/></clipPath>
2846
+ <g clip-path="url(#r)">
2847
+ <rect width="${labelWidth}" height="20" fill="#334155"/>
2848
+ <rect x="${labelWidth}" width="${messageWidth}" height="20" fill="${color}"/>
2849
+ <rect width="${totalWidth}" height="20" fill="url(#s)"/>
2850
+ </g>
2851
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
2852
+ <text x="${Math.floor(labelWidth / 2)}" y="15" fill="#010101" fill-opacity=".3">${label}</text>
2853
+ <text x="${Math.floor(labelWidth / 2)}" y="14">${label}</text>
2854
+ <text x="${labelWidth + Math.floor(messageWidth / 2)}" y="15" fill="#010101" fill-opacity=".3">${message}</text>
2855
+ <text x="${labelWidth + Math.floor(messageWidth / 2)}" y="14">${message}</text>
2856
+ </g>
2857
+ </svg>
2858
+ `;
2859
+ return { ...input, svg };
2860
+ }
2861
+ async function writeEvidenceBadge(options) {
2862
+ const source = parseSource(options.source);
2863
+ let status;
2864
+ let label;
2865
+ let message;
2866
+ let sourcePath;
2867
+ if (source === "verification") {
2868
+ const reportPath = await latestVerificationReport(
2869
+ path18.join(options.cwd, options.config.paths.reportsDir)
2870
+ );
2871
+ sourcePath = normalizeDisplayPath2(options.cwd, reportPath);
2872
+ if (!reportPath) {
2873
+ status = "missing";
2874
+ label = "verification";
2875
+ message = "missing";
2876
+ } else {
2877
+ const markdown = await readFile12(reportPath, "utf8");
2878
+ status = extractVerificationStatus2(markdown);
2879
+ label = "verification";
2880
+ message = status;
2881
+ }
2882
+ } else {
2883
+ const gates = await checkGates({
2884
+ cwd: options.cwd,
2885
+ config: options.config,
2886
+ strict: options.strict
2887
+ });
2888
+ status = gates.overallStatus;
2889
+ label = "gates";
2890
+ message = status;
2891
+ }
2892
+ const badge = generateSvgBadge({ label, message, status });
2893
+ const outPath = options.outPath ?? path18.join(options.cwd, options.config.paths.reportsDir, `agentloop-${source}.svg`);
2894
+ const absoluteOutPath = path18.isAbsolute(outPath) ? outPath : path18.resolve(options.cwd, outPath);
2895
+ await writeTextFile(absoluteOutPath, badge.svg);
2896
+ return {
2897
+ outPath: absoluteOutPath,
2898
+ source,
2899
+ status,
2900
+ label,
2901
+ message,
2902
+ sourcePath,
2903
+ svg: badge.svg
2904
+ };
2905
+ }
2906
+
2907
+ // src/cli/commands/badge.ts
2908
+ function badgeCommand() {
2909
+ return new Command15("badge").description("Generate a local SVG evidence badge").option("--source <source>", "badge source: verification or gates", "verification").option("--out <path>", "output SVG path").option("--strict", "use strict gate status when source is gates").option("--json", "print machine-readable output").action(
2910
+ async (options) => {
2911
+ const config = await loadAgentLoopConfig(process.cwd());
2912
+ const result = await writeEvidenceBadge({
2913
+ cwd: process.cwd(),
2914
+ config,
2915
+ source: options.source,
2916
+ outPath: options.out,
2917
+ strict: options.strict
2918
+ });
2919
+ if (options.json) {
2920
+ console.log(
2921
+ JSON.stringify(
2922
+ {
2923
+ outPath: result.outPath,
2924
+ source: result.source,
2925
+ status: result.status,
2926
+ label: result.label,
2927
+ message: result.message,
2928
+ sourcePath: result.sourcePath
2929
+ },
2930
+ null,
2931
+ 2
2932
+ )
2933
+ );
2934
+ } else {
2935
+ console.log(`# AgentLoopKit Badge
2936
+
2937
+ Badge written: ${result.outPath}
2938
+ Source: ${result.source}
2939
+ Status: ${result.status}
2940
+ Message: ${result.message}`);
2941
+ }
2942
+ }
2943
+ );
2944
+ }
2945
+
2946
+ // src/cli/commands/policy.ts
2947
+ import { Command as Command16 } from "commander";
2948
+
2949
+ // src/core/policy.ts
2950
+ import path19 from "path";
2951
+ import { readdir as readdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
2952
+ function policyRoot(cwd, config) {
2953
+ return path19.resolve(cwd, config.paths.agentloopDir, "policies");
2954
+ }
2955
+ function toStoredPath2(cwd, absolutePath) {
2956
+ return path19.relative(cwd, absolutePath).split(path19.sep).join("/");
2957
+ }
2958
+ function extractHeading4(markdown, fallback) {
2959
+ return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
2960
+ }
2961
+ function normalizePolicyName(policyName) {
2962
+ return policyName.trim().replace(/\.md$/i, "").replace(/-policy$/i, "").toLowerCase();
2963
+ }
2964
+ function normalizeContent(content) {
2965
+ return content.replace(/\r\n/g, "\n");
2966
+ }
2967
+ async function ensurePolicyRoot(root) {
2968
+ const rootStat = await stat7(root).catch(() => void 0);
2969
+ if (!rootStat?.isDirectory()) {
2970
+ throw new AgentLoopError(
2971
+ "No AgentLoopKit policy files found. Run `agentloop init` to generate .agentloop/policies/."
2972
+ );
2973
+ }
2974
+ }
2975
+ async function readMarkdownFiles(root) {
2976
+ const entries = await readdir8(root, { withFileTypes: true });
2977
+ const files = [];
2978
+ for (const entry of entries) {
2979
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2980
+ const absolutePath = path19.join(root, entry.name);
2981
+ const content = await readFile13(absolutePath, "utf8");
2982
+ files.push({
2983
+ name: path19.basename(entry.name, ".md"),
2984
+ fileName: entry.name,
2985
+ absolutePath,
2986
+ content,
2987
+ title: extractHeading4(content, path19.basename(entry.name, ".md"))
2988
+ });
2989
+ }
2990
+ return files.sort((left, right) => left.name.localeCompare(right.name));
2991
+ }
2992
+ async function listPolicies(options) {
2993
+ const root = policyRoot(options.cwd, options.config);
2994
+ await ensurePolicyRoot(root);
2995
+ const policies = await readMarkdownFiles(root);
2996
+ return policies.map((policy) => ({
2997
+ name: policy.name,
2998
+ title: policy.title,
2999
+ path: toStoredPath2(options.cwd, policy.absolutePath)
3000
+ }));
3001
+ }
3002
+ async function readPolicy(options) {
3003
+ const policies = await listPolicies(options);
3004
+ const requested = normalizePolicyName(options.policyName);
3005
+ const policy = policies.find((candidate) => normalizePolicyName(candidate.name) === requested);
3006
+ if (!policy) {
3007
+ throw new AgentLoopError(`Policy not found: ${options.policyName}`);
3008
+ }
3009
+ const root = policyRoot(options.cwd, options.config);
3010
+ const absolutePath = path19.join(root, path19.basename(policy.path));
3011
+ const content = await readFile13(absolutePath, "utf8");
3012
+ return {
3013
+ ...policy,
3014
+ content
3015
+ };
3016
+ }
3017
+ async function getPolicyStatus(options) {
3018
+ const root = policyRoot(options.cwd, options.config);
3019
+ await ensurePolicyRoot(root);
3020
+ const templateRoot = path19.join(getTemplateRoot(), "policies");
3021
+ const [localPolicies, templatePolicies] = await Promise.all([
3022
+ readMarkdownFiles(root),
3023
+ readMarkdownFiles(templateRoot)
3024
+ ]);
3025
+ const localByName = new Map(localPolicies.map((policy) => [policy.name, policy]));
3026
+ const templateByName = new Map(templatePolicies.map((policy) => [policy.name, policy]));
3027
+ const names = [.../* @__PURE__ */ new Set([...templateByName.keys(), ...localByName.keys()])].sort(
3028
+ (left, right) => left.localeCompare(right)
3029
+ );
3030
+ const summary = {
3031
+ current: 0,
3032
+ modified: 0,
3033
+ missing: 0,
3034
+ extra: 0
3035
+ };
3036
+ const policies = names.map((name) => {
3037
+ const local = localByName.get(name);
3038
+ const template = templateByName.get(name);
3039
+ const status = !template ? "extra" : !local ? "missing" : normalizeContent(local.content) === normalizeContent(template.content) ? "current" : "modified";
3040
+ summary[status] += 1;
3041
+ return {
3042
+ name,
3043
+ title: local?.title ?? template?.title ?? name,
3044
+ path: toStoredPath2(options.cwd, path19.join(root, `${name}.md`)),
3045
+ status,
3046
+ templatePath: template ? `templates/policies/${template.fileName}` : null
3047
+ };
3048
+ });
3049
+ return {
3050
+ policies,
3051
+ summary
3052
+ };
3053
+ }
3054
+
3055
+ // src/cli/commands/policy.ts
3056
+ function printPolicyList(policies, options) {
3057
+ if (options.json) {
3058
+ console.log(JSON.stringify({ policies }, null, 2));
3059
+ return;
3060
+ }
3061
+ if (policies.length === 0) {
3062
+ console.log("No AgentLoopKit policies found.");
3063
+ console.log("Run `agentloop init` to generate .agentloop/policies/.");
3064
+ return;
3065
+ }
3066
+ console.log("AgentLoopKit policies:");
3067
+ for (const policy of policies) {
3068
+ console.log(`- ${policy.title}`);
3069
+ console.log(` ${policy.path}`);
3070
+ }
3071
+ }
3072
+ function printPolicy(policy, options) {
3073
+ if (options.json) {
3074
+ console.log(JSON.stringify({ policy }, null, 2));
3075
+ return;
3076
+ }
3077
+ process.stdout.write(policy.content);
3078
+ }
3079
+ function printPolicyStatus(report, options) {
3080
+ if (options.json) {
3081
+ console.log(JSON.stringify(report, null, 2));
3082
+ return;
3083
+ }
3084
+ console.log("AgentLoopKit policy status:");
3085
+ console.log(
3086
+ `Summary: ${report.summary.current} current, ${report.summary.modified} modified, ${report.summary.missing} missing, ${report.summary.extra} extra`
3087
+ );
3088
+ for (const policy of report.policies) {
3089
+ console.log(`- ${policy.status}: ${policy.title}`);
3090
+ console.log(` ${policy.path}`);
3091
+ }
3092
+ const hasDrift = report.summary.modified > 0 || report.summary.missing > 0 || report.summary.extra > 0;
3093
+ if (hasDrift) {
3094
+ console.log("");
3095
+ console.log("Next step: review policy differences before changing repo rules.");
3096
+ }
3097
+ }
3098
+ function policyCommand() {
3099
+ const command = new Command16("policy").description("List or inspect local AgentLoopKit policies");
3100
+ command.command("list").description("List local policies under .agentloop/policies").option("--json", "print machine-readable output").action(async (options) => {
3101
+ const config = await loadAgentLoopConfig(process.cwd());
3102
+ const policies = await listPolicies({ cwd: process.cwd(), config });
3103
+ printPolicyList(policies, options);
3104
+ });
3105
+ command.command("status").description("Show local policy template status").option("--json", "print machine-readable output").action(async (options) => {
3106
+ const config = await loadAgentLoopConfig(process.cwd());
3107
+ const status = await getPolicyStatus({ cwd: process.cwd(), config });
3108
+ printPolicyStatus(status, options);
3109
+ });
3110
+ command.command("show").argument("<policy>", "policy name, such as security or security-policy.md").description("Show a local policy").option("--json", "print machine-readable output").action(async (policyName, options) => {
3111
+ const config = await loadAgentLoopConfig(process.cwd());
3112
+ const policy = await readPolicy({ cwd: process.cwd(), config, policyName });
3113
+ printPolicy(policy, options);
3114
+ });
3115
+ return command;
3116
+ }
3117
+
3118
+ // src/cli/commands/ci-summary.ts
3119
+ import { Command as Command17 } from "commander";
3120
+
3121
+ // src/core/ci-summary.ts
3122
+ import path20 from "path";
3123
+ import { readFile as readFile14 } from "fs/promises";
3124
+ function extractHeading5(markdown, fallback) {
3125
+ return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
3126
+ }
3127
+ function extractTaskStatus3(markdown) {
3128
+ return markdown.match(/^- Status:\s*(.+)$/im)?.[1]?.trim() || "unknown";
3129
+ }
3130
+ function extractOverallStatus3(markdown) {
3131
+ return markdown.match(/Overall status:\s*([a-z-]+)/i)?.[1]?.trim() || "unknown";
3132
+ }
3133
+ function displayPath(cwd, filePath) {
3134
+ if (!filePath) return void 0;
3135
+ return path20.relative(cwd, filePath).split(path20.sep).join("/") || ".";
3136
+ }
3137
+ function resolveUserPath2(cwd, filePath) {
3138
+ if (!filePath) return void 0;
3139
+ return path20.isAbsolute(filePath) ? path20.resolve(filePath) : path20.resolve(cwd, filePath);
3140
+ }
3141
+ async function readTask2(cwd, filePath) {
3142
+ if (!filePath || !await pathExists(filePath)) return void 0;
3143
+ const markdown = await readFile14(filePath, "utf8");
3144
+ return {
3145
+ path: displayPath(cwd, filePath) ?? filePath,
3146
+ title: extractHeading5(markdown, path20.basename(filePath, ".md")),
3147
+ status: extractTaskStatus3(markdown)
3148
+ };
3149
+ }
3150
+ async function readVerification(cwd, filePath) {
3151
+ if (!filePath || !await pathExists(filePath)) return void 0;
3152
+ const markdown = await readFile14(filePath, "utf8");
3153
+ return {
3154
+ path: displayPath(cwd, filePath) ?? filePath,
3155
+ title: extractHeading5(markdown, path20.basename(filePath, ".md")),
3156
+ overallStatus: extractOverallStatus3(markdown)
3157
+ };
3158
+ }
3159
+ async function readHandoff(cwd, filePath) {
3160
+ if (!filePath || !await pathExists(filePath)) return void 0;
3161
+ const markdown = await readFile14(filePath, "utf8");
3162
+ return {
3163
+ path: displayPath(cwd, filePath) ?? filePath,
3164
+ title: extractHeading5(markdown, path20.basename(filePath, ".md"))
3165
+ };
3166
+ }
3167
+ function chooseNextAction3(input) {
3168
+ if (!input.verification) {
3169
+ return {
3170
+ command: "agentloop verify",
3171
+ reason: "No verification report was found for this CI summary."
3172
+ };
3173
+ }
3174
+ if (input.verification.overallStatus === "fail") {
3175
+ return {
3176
+ command: "agentloop verify",
3177
+ reason: "The latest verification report failed. Fix failures and rerun verification."
3178
+ };
3179
+ }
3180
+ return {
3181
+ command: "agentloop check-gates --strict",
3182
+ reason: "Use strict gates in CI after verification and handoff artifacts exist."
3183
+ };
3184
+ }
3185
+ function renderCiLines(ci) {
3186
+ const lines2 = [`- Provider: ${ci.providerName}`];
3187
+ if ("workflow" in ci && ci.workflow) lines2.push(`- Workflow: ${ci.workflow}`);
3188
+ if ("event" in ci && ci.event) lines2.push(`- Event: ${ci.event}`);
3189
+ if ("ref" in ci && ci.ref) lines2.push(`- Ref: ${ci.ref}`);
3190
+ if ("commit" in ci && ci.commit) lines2.push(`- Commit: ${ci.commit}`);
3191
+ if ("runUrl" in ci && ci.runUrl) lines2.push(`- Run URL: ${ci.runUrl}`);
3192
+ if ("runAttempt" in ci && ci.runAttempt) lines2.push(`- Run attempt: ${ci.runAttempt}`);
3193
+ return lines2.join("\n");
3194
+ }
3195
+ function renderArtifactLine(label, artifact) {
3196
+ return artifact ? `- ${label}: ${artifact.title} - ${artifact.path}` : `- ${label}: not found`;
3197
+ }
3198
+ function renderMarkdown3(result) {
3199
+ return `# AgentLoopKit CI Summary
3200
+
3201
+ - Generated: ${result.timestamp}
3202
+ - Overall status: ${result.gates.overallStatus}
3203
+
3204
+ ## CI Context
3205
+ ${renderCiLines(result.ci)}
3206
+
3207
+ ## AgentLoop Evidence
3208
+ ${renderArtifactLine("Task", result.evidence.task)}
3209
+ - Verification: ${result.evidence.verification ? `${result.evidence.verification.overallStatus} - ${result.evidence.verification.path}` : "not found"}
3210
+ ${renderArtifactLine("Handoff", result.evidence.handoff)}
3211
+ - Gates: ${result.gates.overallStatus}
3212
+
3213
+ ## Gate Details
3214
+ ${result.gates.gates.map((gate2) => `- [${gate2.status}] ${gate2.name}: ${gate2.message}`).join("\n")}
3215
+
3216
+ ## Next Action
3217
+
3218
+ Run \`${result.nextAction.command}\`.
3219
+
3220
+ ${result.nextAction.reason}
3221
+
3222
+ ## Safety
3223
+
3224
+ This command reads allowlisted CI metadata and local AgentLoop artifacts only. It does not call CI provider APIs, read secrets, upload files, run verification commands, or inspect environment variables beyond supported CI provenance fields.
3225
+ `;
3226
+ }
3227
+ async function getCiSummary(options) {
3228
+ const timestamp = options.timestamp ?? formatTimestamp();
3229
+ const nowIso = options.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
3230
+ const reportsDir = path20.join(options.cwd, options.config.paths.reportsDir);
3231
+ const handoffsDir = path20.join(options.cwd, options.config.paths.handoffsDir);
3232
+ const taskPath = await getActiveTaskPath(options) ?? await latestMarkdownFile(path20.join(options.cwd, options.config.paths.tasksDir));
3233
+ const verificationPath = await latestMarkdownFile(reportsDir, {
3234
+ pattern: verificationReportPattern
3235
+ });
3236
+ const handoffPath = await latestMarkdownFile(handoffsDir, { pattern: prSummaryPattern });
3237
+ const [task, verification, handoff, gates] = await Promise.all([
3238
+ readTask2(options.cwd, taskPath),
3239
+ readVerification(options.cwd, verificationPath),
3240
+ readHandoff(options.cwd, handoffPath),
3241
+ checkGates({ cwd: options.cwd, config: options.config })
3242
+ ]);
3243
+ const ci = detectCiContext(options.env ?? process.env) ?? {
3244
+ provider: "none",
3245
+ providerName: "No CI detected"
3246
+ };
3247
+ const withoutMarkdown = {
3248
+ timestamp: nowIso,
3249
+ ci,
3250
+ evidence: {
3251
+ task,
3252
+ verification,
3253
+ handoff
3254
+ },
3255
+ gates: {
3256
+ overallStatus: gates.overallStatus,
3257
+ gates: gates.gates
3258
+ },
3259
+ nextAction: chooseNextAction3({ verification, gates })
3260
+ };
3261
+ const markdown = renderMarkdown3(withoutMarkdown);
3262
+ const writtenPath = options.write ? resolveUserPath2(options.cwd, options.outPath) ?? path20.join(options.cwd, options.config.paths.reportsDir, `${timestamp}-ci-summary.md`) : void 0;
3263
+ if (writtenPath) await writeTextFile(writtenPath, markdown);
3264
+ return {
3265
+ ...withoutMarkdown,
3266
+ markdown,
3267
+ writtenPath
3268
+ };
3269
+ }
3270
+
3271
+ // src/cli/commands/ci-summary.ts
3272
+ function ciSummaryCommand() {
3273
+ return new Command17("ci-summary").description("Summarize CI context and AgentLoop evidence").option("--write", "write summary to .agentloop/reports").option("--out <path>", "output Markdown path when using --write").option("--json", "print machine-readable output").action(async (options) => {
3274
+ const config = await loadAgentLoopConfig(process.cwd());
3275
+ const result = await getCiSummary({
3276
+ cwd: process.cwd(),
3277
+ config,
3278
+ write: options.write,
3279
+ outPath: options.out
3280
+ });
3281
+ if (options.json) {
3282
+ console.log(JSON.stringify(result, null, 2));
3283
+ } else {
3284
+ console.log(result.markdown);
3285
+ if (result.writtenPath) console.log(`
3286
+ CI summary written: ${result.writtenPath}`);
3287
+ }
3288
+ });
3289
+ }
3290
+
3291
+ // src/cli/commands/release-notes.ts
3292
+ import { Command as Command18 } from "commander";
3293
+
3294
+ // src/core/release-notes.ts
3295
+ import path21 from "path";
3296
+ import { readFile as readFile15 } from "fs/promises";
3297
+ import { execa as execa3 } from "execa";
3298
+ function displayPath2(cwd, filePath) {
3299
+ if (!filePath) return void 0;
3300
+ return path21.relative(cwd, filePath).split(path21.sep).join("/") || ".";
3301
+ }
3302
+ function resolveUserPath3(cwd, filePath) {
3303
+ if (!filePath) return void 0;
3304
+ return path21.isAbsolute(filePath) ? path21.resolve(filePath) : path21.resolve(cwd, filePath);
3305
+ }
3306
+ async function gitLines(cwd, args) {
3307
+ const result = await execa3("git", args, { cwd, reject: false });
3308
+ if (result.exitCode !== 0) return [];
3309
+ return result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
3310
+ }
3311
+ async function gitRefExists(cwd, ref) {
3312
+ const result = await execa3("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
3313
+ cwd,
3314
+ reject: false
3315
+ });
3316
+ return result.exitCode === 0;
3317
+ }
3318
+ async function readPackageMetadata(cwd) {
3319
+ const packagePath = path21.join(cwd, "package.json");
3320
+ if (!await pathExists(packagePath)) return { name: path21.basename(cwd), version: "0.0.0" };
3321
+ const parsed = JSON.parse(await readFile15(packagePath, "utf8"));
3322
+ return {
3323
+ name: typeof parsed.name === "string" && parsed.name ? parsed.name : path21.basename(cwd),
3324
+ version: typeof parsed.version === "string" && parsed.version ? parsed.version : "0.0.0"
3325
+ };
3326
+ }
3327
+ function extractHeading6(markdown, fallback) {
3328
+ return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
3329
+ }
3330
+ function extractTaskStatus4(markdown) {
3331
+ return markdown.match(/^- Status:\s*(.+)$/im)?.[1]?.trim() || "unknown";
3332
+ }
3333
+ function extractOverallStatus4(markdown) {
3334
+ return markdown.match(/Overall status:\s*([a-z-]+)/i)?.[1]?.trim() || "unknown";
3335
+ }
3336
+ async function readTask3(cwd, filePath) {
3337
+ if (!filePath || !await pathExists(filePath)) return void 0;
3338
+ const markdown = await readFile15(filePath, "utf8");
3339
+ return {
3340
+ path: displayPath2(cwd, filePath) ?? filePath,
3341
+ title: extractHeading6(markdown, path21.basename(filePath, ".md")),
3342
+ status: extractTaskStatus4(markdown)
3343
+ };
3344
+ }
3345
+ async function readVerification2(cwd, filePath) {
3346
+ if (!filePath || !await pathExists(filePath)) return void 0;
3347
+ const markdown = await readFile15(filePath, "utf8");
3348
+ return {
3349
+ path: displayPath2(cwd, filePath) ?? filePath,
3350
+ overallStatus: extractOverallStatus4(markdown)
3351
+ };
3352
+ }
3353
+ async function readCiSummary(cwd, filePath) {
3354
+ if (!filePath || !await pathExists(filePath)) return void 0;
3355
+ const markdown = await readFile15(filePath, "utf8");
3356
+ return {
3357
+ path: displayPath2(cwd, filePath) ?? filePath,
3358
+ title: extractHeading6(markdown, path21.basename(filePath, ".md"))
3359
+ };
3360
+ }
3361
+ async function readChangelogSection(cwd, version) {
3362
+ const changelogPath = path21.join(cwd, "CHANGELOG.md");
3363
+ if (!await pathExists(changelogPath)) return "";
3364
+ const changelog = await readFile15(changelogPath, "utf8");
3365
+ const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3366
+ const heading = new RegExp(`^##\\s+\\[?${escapedVersion}\\]?\\s*$`, "im");
3367
+ const match = heading.exec(changelog);
3368
+ if (!match || match.index === void 0) return "";
3369
+ const sectionStart = match.index + match[0].length;
3370
+ const rest = changelog.slice(sectionStart);
3371
+ const nextHeading = rest.search(/^##\s+/m);
3372
+ return (nextHeading === -1 ? rest : rest.slice(0, nextHeading)).trim();
3373
+ }
3374
+ async function findPreviousTag(cwd, version) {
3375
+ const currentTags = /* @__PURE__ */ new Set([`v${version}`, version]);
3376
+ const tags = await gitLines(cwd, ["tag", "--list", "--sort=-creatordate"]);
3377
+ return tags.find((tag) => !currentTags.has(tag));
3378
+ }
3379
+ function parseNameStatus(lines2) {
3380
+ return lines2.map((line) => {
3381
+ const [status = "?", ...rest] = line.split(/\s+/);
3382
+ return { status, path: rest.join(" ") };
3383
+ });
3384
+ }
3385
+ function renderList(lines2, empty) {
3386
+ return lines2.length ? lines2.map((line) => `- ${line}`).join("\n") : `- ${empty}`;
3387
+ }
3388
+ function renderChangedFiles2(files) {
3389
+ return files.length ? files.map((file) => `- ${file.status} \`${file.path}\``).join("\n") : "- No changed files detected for the selected range.";
3390
+ }
3391
+ function renderWorkingTree(files) {
3392
+ return files.length ? [
3393
+ "- Uncommitted changes detected. Commit or stash them before publishing a release.",
3394
+ ...files.map((file) => `- \`${file}\``)
3395
+ ].join("\n") : "- No uncommitted changes detected.";
3396
+ }
3397
+ function renderEvidence(result) {
3398
+ const lines2 = [];
3399
+ lines2.push(
3400
+ result.evidence.task ? `- Task: ${result.evidence.task.title} (${result.evidence.task.status}) - ${result.evidence.task.path}` : "- Task: not found"
3401
+ );
3402
+ lines2.push(
3403
+ result.evidence.verification ? `- Verification: Overall status: ${result.evidence.verification.overallStatus} - ${result.evidence.verification.path}` : "- Verification: not found"
3404
+ );
3405
+ lines2.push(
3406
+ result.evidence.ciSummary ? `- CI summary: ${result.evidence.ciSummary.title} - ${result.evidence.ciSummary.path}` : "- CI summary: not found"
3407
+ );
3408
+ return lines2.join("\n");
3409
+ }
3410
+ function renderMarkdown4(result) {
3411
+ const changelogLines = [
3412
+ ...result.gitRange.fallbackReason ? [`- ${result.gitRange.fallbackReason}`] : [],
3413
+ result.changelogSection || "- No matching CHANGELOG.md section found."
3414
+ ];
3415
+ return `# Release Notes
3416
+
3417
+ - Generated: ${result.timestamp}
3418
+ - Package: ${result.packageName}
3419
+ - Version: ${result.version}
3420
+ - Range: ${result.gitRange.label}
3421
+ - Branch: ${result.branch || "unknown"}
3422
+ - Commit: ${result.commit || "unknown"}
3423
+
3424
+ ## Changelog
3425
+ ${changelogLines.join("\n")}
3426
+
3427
+ ## Commits
3428
+ ${renderList(result.commits, "No commits found for the selected range.")}
3429
+
3430
+ ## Changed Files
3431
+ ${renderChangedFiles2(result.changedFiles)}
3432
+
3433
+ ## Working Tree
3434
+ ${renderWorkingTree(result.workingTree.files)}
3435
+
3436
+ ## AgentLoop Evidence
3437
+ ${renderEvidence(result)}
3438
+
3439
+ ## Release Checklist
3440
+ - Review these notes before publishing.
3441
+ - Confirm npm and GitHub release status from the registry or release page.
3442
+ - Attach verification evidence when creating a release.
3443
+ - Keep npm catch-up notes honest if the registry still serves an older version.
3444
+
3445
+ ## Safety
3446
+ This command is local and deterministic. Does not create tags, publish packages, call external APIs, read tokens, upload files, or rewrite changelogs.
3447
+ `;
3448
+ }
3449
+ async function generateReleaseNotes(options) {
3450
+ const timestamp = options.timestamp ?? formatTimestamp();
3451
+ const packageMetadata = await readPackageMetadata(options.cwd);
3452
+ const version = options.version ?? packageMetadata.version;
3453
+ const to = options.to ?? "HEAD";
3454
+ const requestedFrom = options.from ?? await findPreviousTag(options.cwd, version);
3455
+ const from = requestedFrom && await gitRefExists(options.cwd, requestedFrom) ? requestedFrom : void 0;
3456
+ const fallbackReason = requestedFrom ? from ? void 0 : `Git ref "${requestedFrom}" was not found; using available local context.` : "No previous version tag was found; using available local context.";
3457
+ const gitRange = {
3458
+ from,
3459
+ to,
3460
+ label: from ? `${from}..${to}` : requestedFrom ? `${to} (missing from: ${requestedFrom})` : `${to} (no previous tag)`,
3461
+ fallbackReason
3462
+ };
3463
+ const commitArgs = from ? ["log", "--pretty=format:%s", `${from}..${to}`] : ["log", "--pretty=format:%s", "-n", "10", to];
3464
+ const diffArgs = from ? ["diff", "--name-status", `${from}..${to}`] : [];
3465
+ const reportsDir = path21.join(options.cwd, options.config.paths.reportsDir);
3466
+ const taskPath = await getActiveTaskPath(options) ?? await latestMarkdownFile(path21.join(options.cwd, options.config.paths.tasksDir));
3467
+ const verificationPath = await latestMarkdownFile(reportsDir, {
3468
+ pattern: verificationReportPattern
3469
+ });
3470
+ const ciSummaryPath = await latestMarkdownFile(reportsDir, {
3471
+ pattern: ciSummaryPattern
3472
+ });
3473
+ const [
3474
+ branch,
3475
+ commit,
3476
+ commits,
3477
+ diffLines,
3478
+ statusLines,
3479
+ changelogSection,
3480
+ task,
3481
+ verification,
3482
+ ciSummary
3483
+ ] = await Promise.all([
3484
+ getGitBranch(options.cwd),
3485
+ getGitCommit(options.cwd),
3486
+ gitLines(options.cwd, commitArgs),
3487
+ diffArgs.length ? gitLines(options.cwd, diffArgs) : Promise.resolve([]),
3488
+ gitLines(options.cwd, ["status", "--short"]),
3489
+ readChangelogSection(options.cwd, version),
3490
+ readTask3(options.cwd, taskPath),
3491
+ readVerification2(options.cwd, verificationPath),
3492
+ readCiSummary(options.cwd, ciSummaryPath)
3493
+ ]);
3494
+ const withoutMarkdown = {
3495
+ timestamp,
3496
+ packageName: packageMetadata.name,
3497
+ version,
3498
+ gitRange,
3499
+ branch,
3500
+ commit,
3501
+ commits,
3502
+ changedFiles: parseNameStatus(diffLines),
3503
+ workingTree: {
3504
+ clean: statusLines.length === 0,
3505
+ files: statusLines
3506
+ },
3507
+ changelogSection,
3508
+ evidence: {
3509
+ task,
3510
+ verification,
3511
+ ciSummary
3512
+ }
3513
+ };
3514
+ const markdown = renderMarkdown4(withoutMarkdown);
3515
+ const writtenPath = options.write ? resolveUserPath3(options.cwd, options.outPath) ?? path21.join(options.cwd, options.config.paths.handoffsDir, `${timestamp}-release-notes.md`) : void 0;
3516
+ if (writtenPath) await writeTextFile(writtenPath, markdown);
3517
+ return {
3518
+ ...withoutMarkdown,
3519
+ markdown,
3520
+ writtenPath
3521
+ };
3522
+ }
3523
+
3524
+ // src/cli/commands/release-notes.ts
3525
+ function releaseNotesCommand() {
3526
+ return new Command18("release-notes").description("Generate deterministic release notes").option("--from <ref>", "git ref to compare from").option("--to <ref>", "git ref to compare to", "HEAD").option("--release-version <version>", "release version; defaults to package.json version").option("--write", "write notes to .agentloop/handoffs").option("--out <path>", "output Markdown path when using --write").option("--json", "print machine-readable output").action(
3527
+ async (options) => {
3528
+ const config = await loadAgentLoopConfig(process.cwd());
3529
+ const result = await generateReleaseNotes({
3530
+ cwd: process.cwd(),
3531
+ config,
3532
+ from: options.from,
3533
+ to: options.to,
3534
+ version: options.releaseVersion,
3535
+ write: options.write,
3536
+ outPath: options.out
3537
+ });
3538
+ if (options.json) {
3539
+ console.log(JSON.stringify(result, null, 2));
3540
+ return;
3541
+ }
3542
+ console.log(result.markdown);
3543
+ if (result.writtenPath) {
3544
+ console.log(`
3545
+ Release notes written: ${result.writtenPath}`);
3546
+ } else {
3547
+ console.log("\nNo release notes file was written. Use --write to create one.");
3548
+ }
3549
+ }
3550
+ );
3551
+ }
3552
+
3553
+ // src/cli/commands/npm-status.ts
3554
+ import { readFile as readFile17 } from "fs/promises";
3555
+ import { Command as Command19 } from "commander";
3556
+
3557
+ // src/core/npm-status.ts
3558
+ import path22 from "path";
3559
+ import { readFile as readFile16 } from "fs/promises";
3560
+ import { execa as execa4 } from "execa";
3561
+ function uniqueStrings(values) {
3562
+ return Array.from(new Set(values.filter(Boolean)));
3563
+ }
3564
+ function valueAsString(value) {
3565
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
3566
+ }
3567
+ function isString(value) {
3568
+ return typeof value === "string";
3569
+ }
3570
+ function valueAsVersions(value) {
3571
+ if (Array.isArray(value)) return uniqueStrings(value.map(valueAsString).filter(isString));
3572
+ const single = valueAsString(value);
3573
+ return single ? [single] : [];
3574
+ }
3575
+ function parseNpmViewJson(json) {
3576
+ const parsed = JSON.parse(json);
3577
+ if (typeof parsed === "string") return { latest: parsed, versions: [parsed] };
3578
+ if (!parsed || typeof parsed !== "object") {
3579
+ throw new AgentLoopError("npm view JSON must be an object or string.");
3580
+ }
3581
+ const record = parsed;
3582
+ const versions = valueAsVersions(record.versions);
3583
+ const latest = valueAsString(record.version) ?? versions.at(-1);
3584
+ if (!latest) throw new AgentLoopError("npm view JSON did not include a version.");
3585
+ return {
3586
+ latest,
3587
+ versions: versions.includes(latest) ? versions : uniqueStrings([...versions, latest])
3588
+ };
3589
+ }
3590
+ async function readPackageMetadata2(cwd) {
3591
+ const packagePath = path22.join(cwd, "package.json");
3592
+ if (!await pathExists(packagePath)) {
3593
+ return { name: path22.basename(cwd), version: "0.0.0" };
3594
+ }
3595
+ const parsed = JSON.parse(await readFile16(packagePath, "utf8"));
3596
+ return {
3597
+ name: typeof parsed.name === "string" && parsed.name ? parsed.name : path22.basename(cwd),
3598
+ version: typeof parsed.version === "string" && parsed.version ? parsed.version : "0.0.0"
3599
+ };
3600
+ }
3601
+ function defaultNpmViewRunner(cwd, timeoutMs) {
3602
+ return async (packageName) => {
3603
+ const result = await execa4("npm", ["view", packageName, "version", "versions", "--json"], {
3604
+ cwd,
3605
+ reject: false,
3606
+ timeout: timeoutMs
3607
+ });
3608
+ return {
3609
+ exitCode: result.exitCode ?? 1,
3610
+ stdout: result.stdout,
3611
+ stderr: result.stderr
3612
+ };
3613
+ };
3614
+ }
3615
+ function statusLabel(status) {
3616
+ switch (status) {
3617
+ case "current":
3618
+ return "npm latest matches local package version";
3619
+ case "catch-up-needed":
3620
+ return "npm latest differs from local package version";
3621
+ case "unknown":
3622
+ return "npm status could not be determined";
3623
+ }
3624
+ }
3625
+ function recommendation(status) {
3626
+ switch (status) {
3627
+ case "current":
3628
+ return "npm has caught up. Remove temporary GitHub tarball fallback docs if they are still present.";
3629
+ case "catch-up-needed":
3630
+ return "Publish the current source version after npm authentication or trusted publishing works. Do not backfill stale intermediate versions from the current checkout.";
3631
+ case "unknown":
3632
+ return "Fix the npm registry check and rerun this command before claiming npm availability.";
3633
+ }
3634
+ }
3635
+ function renderVersions(versions) {
3636
+ return versions.length ? versions.map((version) => `\`${version}\``).join(", ") : "not available";
3637
+ }
3638
+ function renderMarkdown5(result) {
3639
+ const latest = result.registry.latest ? `\`${result.registry.latest}\`` : "not available";
3640
+ const errorLine = result.source.error ? `
3641
+ - Registry error: ${result.source.error}` : "";
3642
+ return `# npm Status
3643
+
3644
+ - Package: \`${result.packageName}\`
3645
+ - Local version: \`${result.localVersion}\`
3646
+ - npm latest: ${latest}
3647
+ - Registry contains local version: ${result.registry.hasLocalVersion ? "yes" : "no"}
3648
+ - Registry versions: ${renderVersions(result.registry.versions)}
3649
+ - Status: ${statusLabel(result.status)}${errorLine}
3650
+
3651
+ ## Recommendation
3652
+
3653
+ ${result.recommendation}
3654
+
3655
+ ## Safety
3656
+
3657
+ This command only runs \`npm view ${result.packageName} version versions --json\` unless \`--registry-json\` is provided. It does not publish packages, create tags, create GitHub releases, read npm tokens, read .env files, upload files, or change package metadata.
3658
+ `;
3659
+ }
3660
+ async function checkNpmStatus(options) {
3661
+ const packageMetadata = await readPackageMetadata2(options.cwd);
3662
+ const packageName = options.packageName ?? packageMetadata.name;
3663
+ const localVersion = options.localVersion ?? packageMetadata.version;
3664
+ const source = options.registryJson ? {
3665
+ command: "captured npm view JSON",
3666
+ exitCode: 0
3667
+ } : {
3668
+ command: `npm view ${packageName} version versions --json`,
3669
+ exitCode: 0
3670
+ };
3671
+ const runner = options.npmView ?? defaultNpmViewRunner(options.cwd, options.timeoutMs ?? 15e3);
3672
+ let registry = { versions: [] };
3673
+ let sourceResult = source;
3674
+ try {
3675
+ if (options.registryJson) {
3676
+ registry = parseNpmViewJson(options.registryJson);
3677
+ } else {
3678
+ const result = await runner(packageName);
3679
+ sourceResult = {
3680
+ ...source,
3681
+ exitCode: result.exitCode,
3682
+ ...result.exitCode === 0 ? {} : { error: result.stderr || result.stdout || "npm view failed" }
3683
+ };
3684
+ if (result.exitCode === 0) registry = parseNpmViewJson(result.stdout);
3685
+ }
3686
+ } catch (error) {
3687
+ sourceResult = {
3688
+ ...source,
3689
+ exitCode: source.exitCode || 1,
3690
+ error: error instanceof Error ? error.message : String(error)
3691
+ };
3692
+ }
3693
+ const hasLocalVersion = registry.versions.includes(localVersion);
3694
+ const status = sourceResult.error ? "unknown" : registry.latest === localVersion ? "current" : "catch-up-needed";
3695
+ const withoutMarkdown = {
3696
+ packageName,
3697
+ localVersion,
3698
+ status,
3699
+ registry: {
3700
+ ...registry,
3701
+ hasLocalVersion
3702
+ },
3703
+ source: sourceResult,
3704
+ recommendation: recommendation(status),
3705
+ safety: {
3706
+ does: ["runs npm view when no captured registry JSON is provided"],
3707
+ doesNot: [
3708
+ "publish packages",
3709
+ "create tags",
3710
+ "create GitHub releases",
3711
+ "read npm tokens",
3712
+ "read .env files",
3713
+ "upload files",
3714
+ "change package metadata"
3715
+ ]
3716
+ }
3717
+ };
3718
+ return {
3719
+ ...withoutMarkdown,
3720
+ markdown: renderMarkdown5(withoutMarkdown)
3721
+ };
3722
+ }
3723
+ function shouldFailNpmStatusExpectation(result) {
3724
+ return result.status !== "current";
3725
+ }
3726
+
3727
+ // src/cli/commands/npm-status.ts
3728
+ function parseTimeout(value) {
3729
+ const parsed = Number.parseInt(value, 10);
3730
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3731
+ throw new Error("timeout must be a positive integer");
3732
+ }
3733
+ return parsed;
3734
+ }
3735
+ function npmStatusCommand() {
3736
+ return new Command19("npm-status").description("Check npm registry catch-up status").option("--package-name <name>", "package name; defaults to package.json name").option("--local-version <version>", "local version; defaults to package.json version").option(
3737
+ "--registry-json <path>",
3738
+ "read captured `npm view <package> version versions --json` output instead of running npm"
3739
+ ).option("--timeout-ms <ms>", "npm view timeout in milliseconds", parseTimeout, 15e3).option("--expect-current", "exit 1 unless npm latest matches the local version").option("--json", "print machine-readable output").action(
3740
+ async (options) => {
3741
+ const registryJson = options.registryJson ? await readFile17(options.registryJson, "utf8") : void 0;
3742
+ const result = await checkNpmStatus({
3743
+ cwd: process.cwd(),
3744
+ packageName: options.packageName,
3745
+ localVersion: options.localVersion,
3746
+ registryJson,
3747
+ timeoutMs: options.timeoutMs
3748
+ });
3749
+ if (options.json) {
3750
+ console.log(JSON.stringify(result, null, 2));
3751
+ } else {
3752
+ console.log(result.markdown);
3753
+ }
3754
+ if (options.expectCurrent && shouldFailNpmStatusExpectation(result)) {
3755
+ process.exitCode = 1;
3756
+ }
3757
+ }
3758
+ );
3759
+ }
3760
+
1128
3761
  // src/cli/index.ts
1129
- var program = new Command9();
1130
- program.name("agentloop").description("A drop-in engineering loop for coding agents.").version("0.1.0", "-V, --version", "print CLI version");
3762
+ var program = new Command20();
3763
+ program.name("agentloop").description("A drop-in engineering loop for coding agents.").version(getPackageVersion(), "-V, --version", "print CLI version");
1131
3764
  program.addCommand(initCommand());
1132
3765
  program.addCommand(doctorCommand());
1133
3766
  program.addCommand(createTaskCommand());
1134
3767
  program.addCommand(verifyCommand());
1135
3768
  program.addCommand(summarizeCommand());
3769
+ program.addCommand(handoffCommand());
3770
+ program.addCommand(statusCommand());
3771
+ program.addCommand(nextCommand());
3772
+ program.addCommand(checkGatesCommand());
3773
+ program.addCommand(reportCommand());
3774
+ program.addCommand(badgeCommand());
3775
+ program.addCommand(ciSummaryCommand());
3776
+ program.addCommand(releaseNotesCommand());
3777
+ program.addCommand(npmStatusCommand());
3778
+ program.addCommand(policyCommand());
3779
+ program.addCommand(taskCommand());
1136
3780
  program.addCommand(installAgentCommand());
1137
3781
  program.addCommand(listTemplatesCommand());
3782
+ program.addCommand(completionCommand());
1138
3783
  program.addCommand(versionCommand());
1139
3784
  program.parseAsync(process.argv).catch((error) => {
1140
3785
  const message = error instanceof Error ? error.message : String(error);