agentloopkit 0.1.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -24
- package/dist/cli/index.js +2709 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/schema/agentloop.config.schema.json +1 -1
- package/dist/templates/agents/claude-code.md +6 -0
- package/dist/templates/agents/codex.md +6 -0
- package/dist/templates/agents/cursor.md +6 -0
- package/dist/templates/agents/gemini-cli.md +6 -0
- package/dist/templates/agents/generic.md +6 -0
- package/dist/templates/agents/github-copilot-cli.md +6 -0
- package/dist/templates/agents/opencode.md +6 -0
- package/dist/templates/harness/commands.md +41 -0
- package/dist/templates/harness/review-checklist.md +1 -0
- package/dist/templates/root/AGENTLOOP.md +5 -0
- package/dist/templates/root/AGENTS.md +15 -0
- package/dist/templates/root/agentloop-directory-readme.md +103 -3
- package/dist/templates/root/agentloop.config.json +1 -1
- package/dist/templates/tasks/README.md +26 -0
- package/package.json +3 -2
- package/schema/agentloop.config.schema.json +1 -1
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
|
|
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:
|
|
125
|
+
$schema: CONFIG_SCHEMA_URL,
|
|
122
126
|
version: 1,
|
|
123
127
|
project: {
|
|
124
128
|
name: input.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(
|
|
293
|
-
const raw = await readFile5(path5.join(getTemplateRoot(),
|
|
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
|
|
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:
|
|
508
|
-
auth:
|
|
509
|
-
security:
|
|
510
|
-
billing:
|
|
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
|
|
708
|
-
const absolutePath = path9.isAbsolute(
|
|
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
|
-
|
|
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("--
|
|
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:
|
|
780
|
-
desiredOutcome:
|
|
781
|
-
constraints: options
|
|
782
|
-
nonGoals: options
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: { ...
|
|
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
|
|
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: ${
|
|
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
|
-
${
|
|
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,
|
|
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(
|
|
994
|
-
const reportPath = options.reportPath ?? await latestMarkdownFile(
|
|
995
|
-
|
|
996
|
-
|
|
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 =
|
|
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
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
|
1130
|
-
program.name("agentloop").description("A drop-in engineering loop for coding agents.").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);
|