agentloopkit 0.1.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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1144 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/schema/agentloop.config.schema.json +142 -0
- package/dist/templates/agents/claude-code.md +18 -0
- package/dist/templates/agents/codex.md +27 -0
- package/dist/templates/agents/cursor.md +17 -0
- package/dist/templates/agents/gemini-cli.md +19 -0
- package/dist/templates/agents/generic.md +27 -0
- package/dist/templates/agents/github-copilot-cli.md +18 -0
- package/dist/templates/agents/opencode.md +18 -0
- package/dist/templates/gates/dependency-gate.md +24 -0
- package/dist/templates/gates/docs-gate.md +24 -0
- package/dist/templates/gates/implementation-gate.md +28 -0
- package/dist/templates/gates/regression-gate.md +25 -0
- package/dist/templates/gates/review-gate.md +24 -0
- package/dist/templates/gates/security-gate.md +26 -0
- package/dist/templates/gates/test-gate.md +24 -0
- package/dist/templates/handoffs/decision-log.md +13 -0
- package/dist/templates/handoffs/pr-summary.md +36 -0
- package/dist/templates/handoffs/release-notes.md +15 -0
- package/dist/templates/handoffs/reviewer-brief.md +13 -0
- package/dist/templates/handoffs/rollback-plan.md +11 -0
- package/dist/templates/handoffs/verification-report.md +13 -0
- package/dist/templates/harness/autonomous-work-rules.md +22 -0
- package/dist/templates/harness/commands.md +16 -0
- package/dist/templates/harness/definition-of-done.md +16 -0
- package/dist/templates/harness/release-checklist.md +11 -0
- package/dist/templates/harness/repo-map.md +16 -0
- package/dist/templates/harness/review-checklist.md +9 -0
- package/dist/templates/harness/working-agreement.md +11 -0
- package/dist/templates/loops/bugfix.md +36 -0
- package/dist/templates/loops/dependency-upgrade.md +34 -0
- package/dist/templates/loops/docs.md +35 -0
- package/dist/templates/loops/feature.md +38 -0
- package/dist/templates/loops/migration.md +35 -0
- package/dist/templates/loops/refactor.md +35 -0
- package/dist/templates/loops/release.md +35 -0
- package/dist/templates/loops/security-review.md +35 -0
- package/dist/templates/loops/test-generation.md +34 -0
- package/dist/templates/policies/database-change-policy.md +11 -0
- package/dist/templates/policies/dependency-change-policy.md +11 -0
- package/dist/templates/policies/git-policy.md +11 -0
- package/dist/templates/policies/no-destructive-actions.md +17 -0
- package/dist/templates/policies/public-api-change-policy.md +11 -0
- package/dist/templates/policies/secrets-policy.md +11 -0
- package/dist/templates/policies/security-policy.md +11 -0
- package/dist/templates/policies/ui-change-policy.md +11 -0
- package/dist/templates/root/AGENTLOOP.md +39 -0
- package/dist/templates/root/AGENTS.md +24 -0
- package/dist/templates/root/agentloop-directory-readme.md +36 -0
- package/dist/templates/root/agentloop.config.json +37 -0
- package/dist/templates/tasks/README.md +9 -0
- package/package.json +71 -0
- package/schema/agentloop.config.schema.json +142 -0
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command as Command9 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/cli/commands/init.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/core/init.ts
|
|
10
|
+
import path6 from "path";
|
|
11
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
12
|
+
|
|
13
|
+
// src/core/constants.ts
|
|
14
|
+
var CONFIG_FILE = "agentloop.config.json";
|
|
15
|
+
var AGENTLOOP_DIR = ".agentloop";
|
|
16
|
+
var AGENTS_FILE = "AGENTS.md";
|
|
17
|
+
var AGENTLOOP_FILE = "AGENTLOOP.md";
|
|
18
|
+
var TEMPLATE_GROUPS = [
|
|
19
|
+
"loops",
|
|
20
|
+
"gates",
|
|
21
|
+
"handoffs",
|
|
22
|
+
"agents",
|
|
23
|
+
"policies",
|
|
24
|
+
"tasks",
|
|
25
|
+
"harness"
|
|
26
|
+
];
|
|
27
|
+
var SUPPORTED_AGENTS = [
|
|
28
|
+
"codex",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"cursor",
|
|
31
|
+
"opencode",
|
|
32
|
+
"gemini-cli",
|
|
33
|
+
"github-copilot-cli",
|
|
34
|
+
"generic"
|
|
35
|
+
];
|
|
36
|
+
var TASK_TYPES = [
|
|
37
|
+
"feature",
|
|
38
|
+
"bugfix",
|
|
39
|
+
"refactor",
|
|
40
|
+
"tests",
|
|
41
|
+
"docs",
|
|
42
|
+
"release",
|
|
43
|
+
"security-review",
|
|
44
|
+
"dependency-upgrade",
|
|
45
|
+
"migration"
|
|
46
|
+
];
|
|
47
|
+
var DEFAULT_COMMAND_KEYS = ["test", "lint", "typecheck", "build", "format"];
|
|
48
|
+
|
|
49
|
+
// src/core/config.ts
|
|
50
|
+
import { readFile } from "fs/promises";
|
|
51
|
+
import path from "path";
|
|
52
|
+
import { z } from "zod";
|
|
53
|
+
|
|
54
|
+
// src/core/errors.ts
|
|
55
|
+
var AgentLoopError = class extends Error {
|
|
56
|
+
constructor(message, code = "AGENTLOOP_ERROR") {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.name = "AgentLoopError";
|
|
60
|
+
}
|
|
61
|
+
code;
|
|
62
|
+
};
|
|
63
|
+
var ConfigError = class extends AgentLoopError {
|
|
64
|
+
constructor(message) {
|
|
65
|
+
super(message, "CONFIG_ERROR");
|
|
66
|
+
this.name = "ConfigError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/core/config.ts
|
|
71
|
+
var ProjectTypeSchema = z.enum([
|
|
72
|
+
"nextjs",
|
|
73
|
+
"react-vite",
|
|
74
|
+
"node",
|
|
75
|
+
"typescript-package",
|
|
76
|
+
"python",
|
|
77
|
+
"docs-only",
|
|
78
|
+
"generic"
|
|
79
|
+
]);
|
|
80
|
+
var PackageManagerSchema = z.enum(["pnpm", "npm", "yarn", "bun"]);
|
|
81
|
+
var CommandConfigSchema = z.object({
|
|
82
|
+
test: z.string().default(""),
|
|
83
|
+
lint: z.string().default(""),
|
|
84
|
+
typecheck: z.string().default(""),
|
|
85
|
+
build: z.string().default(""),
|
|
86
|
+
format: z.string().default("")
|
|
87
|
+
});
|
|
88
|
+
var AgentLoopConfigSchema = z.object({
|
|
89
|
+
$schema: z.string().optional(),
|
|
90
|
+
version: z.literal(1),
|
|
91
|
+
project: z.object({
|
|
92
|
+
name: z.string(),
|
|
93
|
+
type: ProjectTypeSchema,
|
|
94
|
+
packageManager: PackageManagerSchema
|
|
95
|
+
}),
|
|
96
|
+
paths: z.object({
|
|
97
|
+
root: z.string(),
|
|
98
|
+
agentloopDir: z.string(),
|
|
99
|
+
tasksDir: z.string(),
|
|
100
|
+
reportsDir: z.string(),
|
|
101
|
+
handoffsDir: z.string()
|
|
102
|
+
}),
|
|
103
|
+
commands: CommandConfigSchema,
|
|
104
|
+
safety: z.object({
|
|
105
|
+
requireCleanWorkingTree: z.boolean(),
|
|
106
|
+
warnOnDirtyWorkingTree: z.boolean(),
|
|
107
|
+
protectEnvFiles: z.boolean(),
|
|
108
|
+
protectMigrations: z.boolean(),
|
|
109
|
+
protectLockfiles: z.boolean()
|
|
110
|
+
}),
|
|
111
|
+
summary: z.object({
|
|
112
|
+
includeDiffStats: z.boolean(),
|
|
113
|
+
includeChangedFiles: z.boolean(),
|
|
114
|
+
includeVerification: z.boolean(),
|
|
115
|
+
includeRisks: z.boolean(),
|
|
116
|
+
includeRollback: z.boolean()
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
function createDefaultConfig(input = {}) {
|
|
120
|
+
return {
|
|
121
|
+
$schema: "https://agentloopkit.dev/schema/agentloop.config.schema.json",
|
|
122
|
+
version: 1,
|
|
123
|
+
project: {
|
|
124
|
+
name: input.name ?? "",
|
|
125
|
+
type: input.type ?? "generic",
|
|
126
|
+
packageManager: input.packageManager ?? "npm"
|
|
127
|
+
},
|
|
128
|
+
paths: {
|
|
129
|
+
root: ".",
|
|
130
|
+
agentloopDir: ".agentloop",
|
|
131
|
+
tasksDir: ".agentloop/tasks",
|
|
132
|
+
reportsDir: ".agentloop/reports",
|
|
133
|
+
handoffsDir: ".agentloop/handoffs"
|
|
134
|
+
},
|
|
135
|
+
commands: {
|
|
136
|
+
test: input.commands?.test ?? "",
|
|
137
|
+
lint: input.commands?.lint ?? "",
|
|
138
|
+
typecheck: input.commands?.typecheck ?? "",
|
|
139
|
+
build: input.commands?.build ?? "",
|
|
140
|
+
format: input.commands?.format ?? ""
|
|
141
|
+
},
|
|
142
|
+
safety: {
|
|
143
|
+
requireCleanWorkingTree: false,
|
|
144
|
+
warnOnDirtyWorkingTree: true,
|
|
145
|
+
protectEnvFiles: true,
|
|
146
|
+
protectMigrations: true,
|
|
147
|
+
protectLockfiles: false
|
|
148
|
+
},
|
|
149
|
+
summary: {
|
|
150
|
+
includeDiffStats: true,
|
|
151
|
+
includeChangedFiles: true,
|
|
152
|
+
includeVerification: true,
|
|
153
|
+
includeRisks: true,
|
|
154
|
+
includeRollback: true
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function parseAgentLoopConfig(value) {
|
|
159
|
+
const parsed = AgentLoopConfigSchema.safeParse(value);
|
|
160
|
+
if (!parsed.success) {
|
|
161
|
+
throw new ConfigError(`Invalid AgentLoopKit config: ${parsed.error.message}`);
|
|
162
|
+
}
|
|
163
|
+
return parsed.data;
|
|
164
|
+
}
|
|
165
|
+
async function loadAgentLoopConfig(cwd) {
|
|
166
|
+
const filePath = path.join(cwd, CONFIG_FILE);
|
|
167
|
+
const raw = await readFile(filePath, "utf8");
|
|
168
|
+
return parseAgentLoopConfig(JSON.parse(raw));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/core/file-system.ts
|
|
172
|
+
import { access, mkdir, readFile as readFile2, readdir, stat, writeFile } from "fs/promises";
|
|
173
|
+
import path2 from "path";
|
|
174
|
+
async function pathExists(filePath) {
|
|
175
|
+
try {
|
|
176
|
+
await access(filePath);
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function readTextIfExists(filePath) {
|
|
183
|
+
if (!await pathExists(filePath)) return "";
|
|
184
|
+
return readFile2(filePath, "utf8");
|
|
185
|
+
}
|
|
186
|
+
async function writeTextFile(filePath, content) {
|
|
187
|
+
await mkdir(path2.dirname(filePath), { recursive: true });
|
|
188
|
+
await writeFile(filePath, content);
|
|
189
|
+
}
|
|
190
|
+
async function listFilesRecursive(root, options = {}) {
|
|
191
|
+
const ignore = new Set(
|
|
192
|
+
options.ignore ?? [".git", ".agentloop", "node_modules", "dist", "coverage"]
|
|
193
|
+
);
|
|
194
|
+
const files = [];
|
|
195
|
+
async function walk(current) {
|
|
196
|
+
if (!await pathExists(current)) return;
|
|
197
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (ignore.has(entry.name)) continue;
|
|
200
|
+
const absolute = path2.join(current, entry.name);
|
|
201
|
+
if (entry.isDirectory()) {
|
|
202
|
+
await walk(absolute);
|
|
203
|
+
} else if (entry.isFile()) {
|
|
204
|
+
files.push(absolute);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const rootStat = await stat(root).catch(() => void 0);
|
|
209
|
+
if (!rootStat?.isDirectory()) return [];
|
|
210
|
+
await walk(root);
|
|
211
|
+
return files;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/core/package-manager.ts
|
|
215
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
216
|
+
import path3 from "path";
|
|
217
|
+
async function detectPackageManager(cwd) {
|
|
218
|
+
if (await pathExists(path3.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
219
|
+
if (await pathExists(path3.join(cwd, "bun.lockb"))) return "bun";
|
|
220
|
+
if (await pathExists(path3.join(cwd, "bun.lock"))) return "bun";
|
|
221
|
+
if (await pathExists(path3.join(cwd, "yarn.lock"))) return "yarn";
|
|
222
|
+
if (await pathExists(path3.join(cwd, "package-lock.json"))) return "npm";
|
|
223
|
+
const packageJsonPath = path3.join(cwd, "package.json");
|
|
224
|
+
if (await pathExists(packageJsonPath)) {
|
|
225
|
+
const packageJson = JSON.parse(await readFile3(packageJsonPath, "utf8"));
|
|
226
|
+
const manager = packageJson.packageManager?.split("@")[0];
|
|
227
|
+
const parsed = PackageManagerSchema.safeParse(manager);
|
|
228
|
+
if (parsed.success) return parsed.data;
|
|
229
|
+
}
|
|
230
|
+
return "npm";
|
|
231
|
+
}
|
|
232
|
+
function packageManagerRunCommand(manager, scriptName) {
|
|
233
|
+
return `${manager} run ${scriptName}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/core/project-detection.ts
|
|
237
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
238
|
+
import path4 from "path";
|
|
239
|
+
async function readPackageJson(cwd) {
|
|
240
|
+
const filePath = path4.join(cwd, "package.json");
|
|
241
|
+
if (!await pathExists(filePath)) return void 0;
|
|
242
|
+
return JSON.parse(await readFile4(filePath, "utf8"));
|
|
243
|
+
}
|
|
244
|
+
async function detectProjectName(cwd) {
|
|
245
|
+
const packageJson = await readPackageJson(cwd);
|
|
246
|
+
if (packageJson?.name) return packageJson.name;
|
|
247
|
+
return path4.basename(cwd);
|
|
248
|
+
}
|
|
249
|
+
async function detectProjectType(cwd) {
|
|
250
|
+
const packageJson = await readPackageJson(cwd);
|
|
251
|
+
const deps = { ...packageJson?.dependencies, ...packageJson?.devDependencies };
|
|
252
|
+
if (deps.next) return "nextjs";
|
|
253
|
+
if (deps.vite && deps.react) return "react-vite";
|
|
254
|
+
if (deps.typescript && packageJson) return "typescript-package";
|
|
255
|
+
if (packageJson) return "node";
|
|
256
|
+
if (await pathExists(path4.join(cwd, "pyproject.toml")) || await pathExists(path4.join(cwd, "requirements.txt"))) {
|
|
257
|
+
return "python";
|
|
258
|
+
}
|
|
259
|
+
const files = await listFilesRecursive(cwd);
|
|
260
|
+
const relativeFiles = files.map((file) => path4.relative(cwd, file));
|
|
261
|
+
const hasCode = relativeFiles.some(
|
|
262
|
+
(file) => /\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|cs)$/.test(file)
|
|
263
|
+
);
|
|
264
|
+
const hasDocs = relativeFiles.some((file) => file.endsWith(".md") || file.startsWith("docs/"));
|
|
265
|
+
if (hasDocs && !hasCode) return "docs-only";
|
|
266
|
+
return "generic";
|
|
267
|
+
}
|
|
268
|
+
async function detectPackageScripts(cwd, packageManager) {
|
|
269
|
+
const packageJson = await readPackageJson(cwd);
|
|
270
|
+
const commands = { test: "", lint: "", typecheck: "", build: "", format: "" };
|
|
271
|
+
for (const key of DEFAULT_COMMAND_KEYS) {
|
|
272
|
+
if (packageJson?.scripts?.[key]) {
|
|
273
|
+
commands[key] = packageManagerRunCommand(packageManager, key);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return commands;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/core/template-renderer.ts
|
|
280
|
+
import { cp, readFile as readFile5, readdir as readdir2 } from "fs/promises";
|
|
281
|
+
import path5 from "path";
|
|
282
|
+
import { fileURLToPath } from "url";
|
|
283
|
+
function renderTemplateString(template, values) {
|
|
284
|
+
return template.replace(/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g, (match, key) => {
|
|
285
|
+
const value = values[key];
|
|
286
|
+
return value === void 0 ? match : String(value);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function getTemplateRoot() {
|
|
290
|
+
return fileURLToPath(new URL("../templates", import.meta.url));
|
|
291
|
+
}
|
|
292
|
+
async function readTemplate(relativePath, values = {}) {
|
|
293
|
+
const raw = await readFile5(path5.join(getTemplateRoot(), relativePath), "utf8");
|
|
294
|
+
return renderTemplateString(raw, values);
|
|
295
|
+
}
|
|
296
|
+
async function listTemplateFiles() {
|
|
297
|
+
const root = getTemplateRoot();
|
|
298
|
+
const groups = await readdir2(root, { withFileTypes: true });
|
|
299
|
+
const result = {};
|
|
300
|
+
for (const group of groups) {
|
|
301
|
+
if (!group.isDirectory()) continue;
|
|
302
|
+
const entries = await readdir2(path5.join(root, group.name), { withFileTypes: true });
|
|
303
|
+
result[group.name] = entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/core/init.ts
|
|
309
|
+
async function writeGeneratedFile(filePath, content, result) {
|
|
310
|
+
if (await pathExists(filePath)) {
|
|
311
|
+
result.skipped.push(filePath);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (result.dryRun) {
|
|
315
|
+
result.created.push(filePath);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
await writeTextFile(filePath, content);
|
|
319
|
+
result.created.push(filePath);
|
|
320
|
+
}
|
|
321
|
+
async function writeRenderedTemplateGroup(cwd, group, values, result) {
|
|
322
|
+
const templateDir = path6.join(getTemplateRoot(), group);
|
|
323
|
+
const entries = await readdir3(templateDir, { withFileTypes: true });
|
|
324
|
+
for (const entry of entries) {
|
|
325
|
+
if (!entry.isFile()) continue;
|
|
326
|
+
const relative2 = `${group}/${entry.name}`;
|
|
327
|
+
await writeGeneratedFile(
|
|
328
|
+
path6.join(cwd, AGENTLOOP_DIR, group, entry.name),
|
|
329
|
+
await readTemplate(relative2, values),
|
|
330
|
+
result
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function upsertAgentsFile(cwd, content, result) {
|
|
335
|
+
const filePath = path6.join(cwd, AGENTS_FILE);
|
|
336
|
+
const existing = await readTextIfExists(filePath);
|
|
337
|
+
const marker = "<!-- agentloopkit:start -->";
|
|
338
|
+
if (!existing) {
|
|
339
|
+
if (result.dryRun) {
|
|
340
|
+
result.created.push(filePath);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
await writeTextFile(filePath, content);
|
|
344
|
+
result.created.push(filePath);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (existing.includes(marker)) {
|
|
348
|
+
result.skipped.push(filePath);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const section = content.replace(/^# AGENTS\s*/i, "## AgentLoopKit\n\n").replace("<!-- agentloopkit:start -->", marker);
|
|
352
|
+
if (result.dryRun) {
|
|
353
|
+
result.updated.push(filePath);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
await writeTextFile(filePath, `${existing.trimEnd()}
|
|
357
|
+
|
|
358
|
+
${section.trim()}
|
|
359
|
+
`);
|
|
360
|
+
result.updated.push(filePath);
|
|
361
|
+
}
|
|
362
|
+
async function initializeAgentLoop(options) {
|
|
363
|
+
const result = {
|
|
364
|
+
created: [],
|
|
365
|
+
updated: [],
|
|
366
|
+
skipped: [],
|
|
367
|
+
dryRun: Boolean(options.dryRun)
|
|
368
|
+
};
|
|
369
|
+
const cwd = options.cwd;
|
|
370
|
+
const packageManager = await detectPackageManager(cwd);
|
|
371
|
+
const projectType = await detectProjectType(cwd);
|
|
372
|
+
const projectName = await detectProjectName(cwd);
|
|
373
|
+
const commands = await detectPackageScripts(cwd, packageManager);
|
|
374
|
+
const config = createDefaultConfig({
|
|
375
|
+
name: projectName,
|
|
376
|
+
type: projectType,
|
|
377
|
+
packageManager,
|
|
378
|
+
commands
|
|
379
|
+
});
|
|
380
|
+
const values = {
|
|
381
|
+
projectName,
|
|
382
|
+
projectType,
|
|
383
|
+
packageManager,
|
|
384
|
+
testCommand: commands.test || "not configured",
|
|
385
|
+
lintCommand: commands.lint || "not configured",
|
|
386
|
+
typecheckCommand: commands.typecheck || "not configured",
|
|
387
|
+
buildCommand: commands.build || "not configured",
|
|
388
|
+
formatCommand: commands.format || "not configured"
|
|
389
|
+
};
|
|
390
|
+
for (const group of TEMPLATE_GROUPS) {
|
|
391
|
+
if (group === "tasks") continue;
|
|
392
|
+
await writeRenderedTemplateGroup(cwd, group, values, result);
|
|
393
|
+
}
|
|
394
|
+
await writeGeneratedFile(
|
|
395
|
+
path6.join(cwd, AGENTLOOP_DIR, "README.md"),
|
|
396
|
+
await readTemplate("root/agentloop-directory-readme.md", values),
|
|
397
|
+
result
|
|
398
|
+
);
|
|
399
|
+
await writeGeneratedFile(
|
|
400
|
+
path6.join(cwd, AGENTLOOP_DIR, "tasks", "README.md"),
|
|
401
|
+
await readTemplate("tasks/README.md", values),
|
|
402
|
+
result
|
|
403
|
+
);
|
|
404
|
+
await writeGeneratedFile(
|
|
405
|
+
path6.join(cwd, AGENTLOOP_DIR, "reports", "README.md"),
|
|
406
|
+
"# Verification Reports\n\nAgentLoopKit writes verification reports here when you run `agentloop verify`.\n",
|
|
407
|
+
result
|
|
408
|
+
);
|
|
409
|
+
const agentsContent = await readTemplate("root/AGENTS.md", values);
|
|
410
|
+
await upsertAgentsFile(cwd, agentsContent, result);
|
|
411
|
+
await writeGeneratedFile(
|
|
412
|
+
path6.join(cwd, AGENTLOOP_FILE),
|
|
413
|
+
await readTemplate("root/AGENTLOOP.md", values),
|
|
414
|
+
result
|
|
415
|
+
);
|
|
416
|
+
const configPath = path6.join(cwd, CONFIG_FILE);
|
|
417
|
+
if (await pathExists(configPath)) {
|
|
418
|
+
result.skipped.push(configPath);
|
|
419
|
+
} else if (result.dryRun) {
|
|
420
|
+
result.created.push(configPath);
|
|
421
|
+
} else {
|
|
422
|
+
await writeTextFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
423
|
+
`);
|
|
424
|
+
result.created.push(configPath);
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/core/logger.ts
|
|
430
|
+
var consoleLogger = {
|
|
431
|
+
info: (message) => console.log(message),
|
|
432
|
+
warn: (message) => console.warn(message),
|
|
433
|
+
error: (message) => console.error(message)
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/cli/commands/init.ts
|
|
437
|
+
function initCommand() {
|
|
438
|
+
return new Command("init").description("Initialize AgentLoopKit in the current repository").option("--dry-run", "show planned changes without writing files").option("--json", "print machine-readable output").action(async (options) => {
|
|
439
|
+
const result = await initializeAgentLoop({ cwd: process.cwd(), dryRun: options.dryRun });
|
|
440
|
+
if (options.json) {
|
|
441
|
+
consoleLogger.info(JSON.stringify(result, null, 2));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
consoleLogger.info(
|
|
445
|
+
options.dryRun ? "AgentLoopKit init dry run complete." : "AgentLoopKit initialized."
|
|
446
|
+
);
|
|
447
|
+
consoleLogger.info(`Created: ${result.created.length}`);
|
|
448
|
+
consoleLogger.info(`Updated: ${result.updated.length}`);
|
|
449
|
+
consoleLogger.info(`Skipped: ${result.skipped.length}`);
|
|
450
|
+
if (!options.dryRun) {
|
|
451
|
+
consoleLogger.info("\nNext steps:");
|
|
452
|
+
consoleLogger.info("- Review AGENTS.md and AGENTLOOP.md");
|
|
453
|
+
consoleLogger.info("- Run agentloop doctor");
|
|
454
|
+
consoleLogger.info("- Create a task with agentloop create-task");
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// src/cli/commands/doctor.ts
|
|
460
|
+
import { Command as Command2 } from "commander";
|
|
461
|
+
|
|
462
|
+
// src/core/doctor.ts
|
|
463
|
+
import path8 from "path";
|
|
464
|
+
|
|
465
|
+
// src/core/git.ts
|
|
466
|
+
import { execa } from "execa";
|
|
467
|
+
async function commandExists(command) {
|
|
468
|
+
const result = await execa(command, ["--version"], { reject: false });
|
|
469
|
+
return result.exitCode === 0;
|
|
470
|
+
}
|
|
471
|
+
async function isInsideGitRepo(cwd) {
|
|
472
|
+
const result = await execa("git", ["rev-parse", "--is-inside-work-tree"], { cwd, reject: false });
|
|
473
|
+
return result.exitCode === 0 && result.stdout.trim() === "true";
|
|
474
|
+
}
|
|
475
|
+
async function getGitBranch(cwd) {
|
|
476
|
+
const result = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd, reject: false });
|
|
477
|
+
return result.exitCode === 0 ? result.stdout.trim() : "";
|
|
478
|
+
}
|
|
479
|
+
async function getGitCommit(cwd) {
|
|
480
|
+
const result = await execa("git", ["rev-parse", "--short", "HEAD"], { cwd, reject: false });
|
|
481
|
+
return result.exitCode === 0 ? result.stdout.trim() : "";
|
|
482
|
+
}
|
|
483
|
+
async function getGitStatus(cwd) {
|
|
484
|
+
const result = await execa("git", ["status", "--short"], { cwd, reject: false });
|
|
485
|
+
return result.exitCode === 0 ? result.stdout : "";
|
|
486
|
+
}
|
|
487
|
+
async function getGitDiffStat(cwd) {
|
|
488
|
+
const result = await execa("git", ["diff", "--stat"], { cwd, reject: false });
|
|
489
|
+
return result.exitCode === 0 ? result.stdout : "";
|
|
490
|
+
}
|
|
491
|
+
async function parseGitStatus(status) {
|
|
492
|
+
return status.split("\n").map((line) => line.trimEnd()).filter(Boolean).map((line) => ({
|
|
493
|
+
status: line.slice(0, 2).trim() || "?",
|
|
494
|
+
path: line.slice(3).trim()
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/core/safety.ts
|
|
499
|
+
import path7 from "path";
|
|
500
|
+
function relative(cwd, file) {
|
|
501
|
+
return path7.relative(cwd, file).replaceAll(path7.sep, "/");
|
|
502
|
+
}
|
|
503
|
+
async function detectRiskFiles(cwd) {
|
|
504
|
+
const files = (await listFilesRecursive(cwd)).map((file) => relative(cwd, file));
|
|
505
|
+
const includes = (needles) => files.filter((file) => needles.some((needle) => file.toLowerCase().includes(needle)));
|
|
506
|
+
return {
|
|
507
|
+
migrations: includes(["migration", "migrations/"]),
|
|
508
|
+
auth: includes(["auth", "oauth", "session", "passport"]),
|
|
509
|
+
security: includes(["security", "crypto", "secret", "permission", "policy"]),
|
|
510
|
+
billing: includes(["billing", "stripe", "payment", "invoice"]),
|
|
511
|
+
deployment: files.filter(
|
|
512
|
+
(file) => /(^|\/)(Dockerfile|docker-compose|vercel\.json|netlify\.toml|fly\.toml|render\.yaml|\.github\/workflows\/)/i.test(
|
|
513
|
+
file
|
|
514
|
+
)
|
|
515
|
+
),
|
|
516
|
+
lockfiles: files.filter(
|
|
517
|
+
(file) => ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"].includes(
|
|
518
|
+
path7.basename(file)
|
|
519
|
+
)
|
|
520
|
+
),
|
|
521
|
+
envFiles: files.filter((file) => /^\.env($|\.)|\/\.env($|\.)/.test(file))
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/core/doctor.ts
|
|
526
|
+
function check(name, status, message) {
|
|
527
|
+
return { name, status, message };
|
|
528
|
+
}
|
|
529
|
+
async function runDoctor(options) {
|
|
530
|
+
const cwd = options.cwd;
|
|
531
|
+
const checks = [];
|
|
532
|
+
checks.push(check("Current directory", "pass", cwd));
|
|
533
|
+
const gitInstalled = await commandExists("git");
|
|
534
|
+
checks.push(
|
|
535
|
+
check(
|
|
536
|
+
"Git installed",
|
|
537
|
+
gitInstalled ? "pass" : "warn",
|
|
538
|
+
gitInstalled ? "git is available" : "git not found"
|
|
539
|
+
)
|
|
540
|
+
);
|
|
541
|
+
const inGit = gitInstalled ? await isInsideGitRepo(cwd) : false;
|
|
542
|
+
checks.push(
|
|
543
|
+
check(
|
|
544
|
+
"Git repository",
|
|
545
|
+
inGit ? "pass" : "warn",
|
|
546
|
+
inGit ? "inside a git repo" : "not inside a git repo"
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
const status = inGit ? await getGitStatus(cwd) : "";
|
|
550
|
+
checks.push(
|
|
551
|
+
check(
|
|
552
|
+
"Working tree",
|
|
553
|
+
status.trim() ? "warn" : "pass",
|
|
554
|
+
status.trim() ? "working tree has changes" : "clean"
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
for (const file of ["AGENTS.md", "AGENTLOOP.md", ".agentloop"]) {
|
|
558
|
+
const exists = await pathExists(path8.join(cwd, file));
|
|
559
|
+
checks.push(check(file, exists ? "pass" : "warn", exists ? "found" : "missing"));
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
await loadAgentLoopConfig(cwd);
|
|
563
|
+
checks.push(check(CONFIG_FILE, "pass", "valid"));
|
|
564
|
+
} catch (error) {
|
|
565
|
+
checks.push(
|
|
566
|
+
check(CONFIG_FILE, "fail", error instanceof Error ? error.message : "invalid config")
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const agents = await readTextIfExists(path8.join(cwd, "AGENTS.md"));
|
|
570
|
+
if (agents && !agents.includes("AgentLoopKit")) {
|
|
571
|
+
checks.push(
|
|
572
|
+
check("AGENTS.md AgentLoopKit section", "warn", "AGENTS.md does not mention AgentLoopKit")
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
const packageManager = await detectPackageManager(cwd);
|
|
576
|
+
const projectType = await detectProjectType(cwd);
|
|
577
|
+
const commands = await detectPackageScripts(cwd, packageManager);
|
|
578
|
+
checks.push(check("Package manager", "pass", packageManager));
|
|
579
|
+
checks.push(check("Project type", "pass", projectType));
|
|
580
|
+
for (const key of ["test", "lint", "typecheck", "build"]) {
|
|
581
|
+
checks.push(
|
|
582
|
+
check(`${key} command`, commands[key] ? "pass" : "warn", commands[key] || "not detected")
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
const risks = await detectRiskFiles(cwd);
|
|
586
|
+
const riskCount = Object.values(risks).reduce((count, files) => count + files.length, 0);
|
|
587
|
+
checks.push(
|
|
588
|
+
check(
|
|
589
|
+
"Potential risk files",
|
|
590
|
+
riskCount ? "warn" : "pass",
|
|
591
|
+
riskCount ? `${riskCount} risk file(s) detected` : "none detected"
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
if (!commands.test) {
|
|
595
|
+
checks.push(check("Tests", "warn", "no test command detected"));
|
|
596
|
+
}
|
|
597
|
+
const warnings = checks.filter((item) => item.status === "warn");
|
|
598
|
+
const serious = checks.filter((item) => item.status === "fail");
|
|
599
|
+
const markdown = `# AgentLoopKit Doctor
|
|
600
|
+
|
|
601
|
+
${checks.map((item) => {
|
|
602
|
+
const icon = item.status === "pass" ? "[pass]" : item.status === "warn" ? "[warn]" : "[fail]";
|
|
603
|
+
return `- ${icon} ${item.name}: ${item.message}`;
|
|
604
|
+
}).join("\n")}
|
|
605
|
+
`;
|
|
606
|
+
return { checks, warnings, serious, markdown };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/cli/commands/doctor.ts
|
|
610
|
+
function doctorCommand() {
|
|
611
|
+
return new Command2("doctor").description("Check whether this repo is ready for agentic engineering").option("--json", "print machine-readable output").action(async (options) => {
|
|
612
|
+
const result = await runDoctor({ cwd: process.cwd() });
|
|
613
|
+
if (options.json) {
|
|
614
|
+
console.log(JSON.stringify(result, null, 2));
|
|
615
|
+
} else {
|
|
616
|
+
console.log(result.markdown);
|
|
617
|
+
}
|
|
618
|
+
if (result.serious.length > 0) process.exitCode = 1;
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/cli/commands/create-task.ts
|
|
623
|
+
import { Command as Command3 } from "commander";
|
|
624
|
+
import prompts from "prompts";
|
|
625
|
+
|
|
626
|
+
// src/core/task-contract.ts
|
|
627
|
+
import path9 from "path";
|
|
628
|
+
|
|
629
|
+
// src/core/dates.ts
|
|
630
|
+
function pad(value) {
|
|
631
|
+
return String(value).padStart(2, "0");
|
|
632
|
+
}
|
|
633
|
+
function formatDate(date = /* @__PURE__ */ new Date()) {
|
|
634
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
635
|
+
}
|
|
636
|
+
function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
637
|
+
return `${formatDate(date)}-${pad(date.getHours())}-${pad(date.getMinutes())}`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/core/slug.ts
|
|
641
|
+
function slugify(value) {
|
|
642
|
+
const slug = value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
|
|
643
|
+
return slug || "task";
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/core/task-contract.ts
|
|
647
|
+
function list(values, fallback = "None recorded yet.") {
|
|
648
|
+
const clean = values?.map((value) => value.trim()).filter(Boolean) ?? [];
|
|
649
|
+
if (clean.length === 0) return `- ${fallback}`;
|
|
650
|
+
return clean.map((value) => `- ${value}`).join("\n");
|
|
651
|
+
}
|
|
652
|
+
function generateTaskContract(input) {
|
|
653
|
+
const createdDate = input.createdDate ?? formatDate();
|
|
654
|
+
return `# ${input.title}
|
|
655
|
+
|
|
656
|
+
- Created date: ${createdDate}
|
|
657
|
+
- Task type: ${input.type}
|
|
658
|
+
- Status: proposed
|
|
659
|
+
|
|
660
|
+
## Problem Statement
|
|
661
|
+
${input.problemStatement || "Describe the problem this task should solve."}
|
|
662
|
+
|
|
663
|
+
## Desired Outcome
|
|
664
|
+
${input.desiredOutcome || "Describe the concrete result expected from this task."}
|
|
665
|
+
|
|
666
|
+
## Constraints
|
|
667
|
+
${list(input.constraints)}
|
|
668
|
+
|
|
669
|
+
## Non-Goals
|
|
670
|
+
${list(input.nonGoals)}
|
|
671
|
+
|
|
672
|
+
## Assumptions
|
|
673
|
+
${list(input.assumptions)}
|
|
674
|
+
|
|
675
|
+
## Likely Files or Areas
|
|
676
|
+
${list(input.likelyFiles)}
|
|
677
|
+
|
|
678
|
+
## Files or Areas Not to Touch
|
|
679
|
+
${list(input.forbiddenFiles)}
|
|
680
|
+
|
|
681
|
+
## Acceptance Criteria
|
|
682
|
+
${list(input.acceptanceCriteria, "Add acceptance criteria before implementation starts.")}
|
|
683
|
+
|
|
684
|
+
## Verification Commands
|
|
685
|
+
${list(input.verificationCommands, "No verification command recorded.")}
|
|
686
|
+
|
|
687
|
+
## Implementation Plan
|
|
688
|
+
- Inspect relevant files before editing.
|
|
689
|
+
- Keep changes focused on this contract.
|
|
690
|
+
- Record any architecture decision in DECISIONS.md.
|
|
691
|
+
|
|
692
|
+
## Risk Notes
|
|
693
|
+
- Re-check protected areas before changing migrations, auth, secrets, billing, deployment, or public APIs.
|
|
694
|
+
|
|
695
|
+
## Rollback Notes
|
|
696
|
+
${input.rollbackNotes || "Document how to revert or disable this change."}
|
|
697
|
+
|
|
698
|
+
## Handoff Requirements
|
|
699
|
+
- Summarize files changed.
|
|
700
|
+
- Include verification commands and results.
|
|
701
|
+
- State unverified areas honestly.
|
|
702
|
+
- Include risks, rollback notes, and reviewer checklist.
|
|
703
|
+
`;
|
|
704
|
+
}
|
|
705
|
+
async function createTaskContractFile(options) {
|
|
706
|
+
const createdDate = options.input.createdDate ?? formatDate();
|
|
707
|
+
const relativePath = options.out ?? path9.join(options.config.paths.tasksDir, `${createdDate}-${slugify(options.input.title)}.md`);
|
|
708
|
+
const absolutePath = path9.isAbsolute(relativePath) ? relativePath : path9.join(options.cwd, relativePath);
|
|
709
|
+
const markdown = generateTaskContract({ ...options.input, createdDate });
|
|
710
|
+
await writeTextFile(absolutePath, markdown);
|
|
711
|
+
return { path: absolutePath, markdown };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/cli/commands/create-task.ts
|
|
715
|
+
function lines(value) {
|
|
716
|
+
return value ? value.split("\n").map((line) => line.trim()).filter(Boolean) : [];
|
|
717
|
+
}
|
|
718
|
+
async function collectInteractive(initial) {
|
|
719
|
+
const answers = await prompts([
|
|
720
|
+
{
|
|
721
|
+
type: initial.title ? null : "text",
|
|
722
|
+
name: "title",
|
|
723
|
+
message: "Task title"
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
type: initial.type ? null : "select",
|
|
727
|
+
name: "type",
|
|
728
|
+
message: "Task type",
|
|
729
|
+
choices: TASK_TYPES.map((type) => ({ title: type, value: type }))
|
|
730
|
+
},
|
|
731
|
+
{ type: "text", name: "problemStatement", message: "Problem statement" },
|
|
732
|
+
{ type: "text", name: "desiredOutcome", message: "Desired outcome" },
|
|
733
|
+
{ type: "text", name: "constraints", message: "Constraints (separate with semicolons)" },
|
|
734
|
+
{ type: "text", name: "nonGoals", message: "Non-goals (separate with semicolons)" },
|
|
735
|
+
{
|
|
736
|
+
type: "text",
|
|
737
|
+
name: "likelyFiles",
|
|
738
|
+
message: "Likely files or areas (separate with semicolons)"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
type: "text",
|
|
742
|
+
name: "forbiddenFiles",
|
|
743
|
+
message: "Files or areas not to touch (separate with semicolons)"
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
type: "text",
|
|
747
|
+
name: "acceptanceCriteria",
|
|
748
|
+
message: "Acceptance criteria (separate with semicolons)"
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
type: "text",
|
|
752
|
+
name: "verificationCommands",
|
|
753
|
+
message: "Verification commands (separate with semicolons)"
|
|
754
|
+
},
|
|
755
|
+
{ type: "text", name: "rollbackNotes", message: "Rollback notes" }
|
|
756
|
+
]);
|
|
757
|
+
return {
|
|
758
|
+
title: initial.title ?? answers.title,
|
|
759
|
+
type: initial.type ?? answers.type,
|
|
760
|
+
problemStatement: answers.problemStatement,
|
|
761
|
+
desiredOutcome: answers.desiredOutcome,
|
|
762
|
+
constraints: String(answers.constraints ?? "").split(";").filter(Boolean),
|
|
763
|
+
nonGoals: String(answers.nonGoals ?? "").split(";").filter(Boolean),
|
|
764
|
+
likelyFiles: String(answers.likelyFiles ?? "").split(";").filter(Boolean),
|
|
765
|
+
forbiddenFiles: String(answers.forbiddenFiles ?? "").split(";").filter(Boolean),
|
|
766
|
+
acceptanceCriteria: String(answers.acceptanceCriteria ?? "").split(";").filter(Boolean),
|
|
767
|
+
verificationCommands: String(answers.verificationCommands ?? "").split(";").filter(Boolean),
|
|
768
|
+
rollbackNotes: answers.rollbackNotes
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
function createTaskCommand() {
|
|
772
|
+
return new Command3("create-task").description("Create a task contract for an agentic coding session").option("--title <title>", "task title").option("--type <type>", "task type").option("--out <path>", "output file path").option("--problem <text>", "problem statement").option("--outcome <text>", "desired outcome").option("--constraint <text>", "constraint; repeat or use newlines", lines, []).option("--non-goal <text>", "non-goal; repeat or use newlines", lines, []).option("--acceptance <text>", "acceptance criterion; repeat or use newlines", lines, []).option("--verify-command <command>", "verification command; repeat or use newlines", lines, []).action(async (options) => {
|
|
773
|
+
const type = typeof options.type === "string" && TASK_TYPES.includes(options.type) ? options.type : void 0;
|
|
774
|
+
const title = typeof options.title === "string" ? options.title : void 0;
|
|
775
|
+
const config = await loadAgentLoopConfig(process.cwd());
|
|
776
|
+
const input = title && type ? {
|
|
777
|
+
title,
|
|
778
|
+
type,
|
|
779
|
+
problemStatement: String(options.problem ?? ""),
|
|
780
|
+
desiredOutcome: String(options.outcome ?? ""),
|
|
781
|
+
constraints: options.constraint,
|
|
782
|
+
nonGoals: options.nonGoal,
|
|
783
|
+
acceptanceCriteria: options.acceptance,
|
|
784
|
+
verificationCommands: options.verifyCommand
|
|
785
|
+
} : await collectInteractive({ title, type });
|
|
786
|
+
const result = await createTaskContractFile({
|
|
787
|
+
cwd: process.cwd(),
|
|
788
|
+
config,
|
|
789
|
+
input,
|
|
790
|
+
out: typeof options.out === "string" ? options.out : void 0
|
|
791
|
+
});
|
|
792
|
+
console.log(`Task contract created: ${result.path}`);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/cli/commands/verify.ts
|
|
797
|
+
import { Command as Command4 } from "commander";
|
|
798
|
+
|
|
799
|
+
// src/core/verification.ts
|
|
800
|
+
import path10 from "path";
|
|
801
|
+
import { execa as execa2 } from "execa";
|
|
802
|
+
function excerpt(output, limit = 5e3) {
|
|
803
|
+
if (output.length <= limit) return output;
|
|
804
|
+
return `${output.slice(0, limit)}
|
|
805
|
+
|
|
806
|
+
[output truncated to ${limit} characters]`;
|
|
807
|
+
}
|
|
808
|
+
function commandEntries(config, options) {
|
|
809
|
+
const configured = [
|
|
810
|
+
["test", config.commands.test],
|
|
811
|
+
["lint", config.commands.lint],
|
|
812
|
+
["typecheck", config.commands.typecheck],
|
|
813
|
+
["build", config.commands.build]
|
|
814
|
+
];
|
|
815
|
+
const active = configured.filter(([key, command]) => {
|
|
816
|
+
if (key === "custom") return false;
|
|
817
|
+
return command && !options.skip?.[key];
|
|
818
|
+
});
|
|
819
|
+
for (const command of options.customCommands ?? []) {
|
|
820
|
+
if (command.trim()) active.push(["custom", command.trim()]);
|
|
821
|
+
}
|
|
822
|
+
return active;
|
|
823
|
+
}
|
|
824
|
+
async function runVerification(options) {
|
|
825
|
+
const timestamp = options.reportTimestamp ?? formatTimestamp();
|
|
826
|
+
const nowIso = options.nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
827
|
+
const commands = commandEntries(options.config, options);
|
|
828
|
+
const notRun = [
|
|
829
|
+
...["test", "lint", "typecheck", "build"].filter((key) => {
|
|
830
|
+
if (options.skip?.[key]) return true;
|
|
831
|
+
return !options.config.commands[key];
|
|
832
|
+
})
|
|
833
|
+
];
|
|
834
|
+
const results = [];
|
|
835
|
+
for (const [key, command] of commands) {
|
|
836
|
+
const result = await execa2(command, {
|
|
837
|
+
cwd: options.cwd,
|
|
838
|
+
shell: true,
|
|
839
|
+
all: true,
|
|
840
|
+
reject: false,
|
|
841
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
842
|
+
});
|
|
843
|
+
results.push({
|
|
844
|
+
key,
|
|
845
|
+
command,
|
|
846
|
+
exitCode: result.exitCode ?? 1,
|
|
847
|
+
passed: result.exitCode === 0,
|
|
848
|
+
output: result.all ?? result.stdout ?? result.stderr ?? ""
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
const overallStatus = results.length === 0 ? "not-run" : results.every((result) => result.passed) ? "pass" : "fail";
|
|
852
|
+
const reportPath = path10.join(
|
|
853
|
+
options.cwd,
|
|
854
|
+
options.config.paths.reportsDir,
|
|
855
|
+
`${timestamp}-verification-report.md`
|
|
856
|
+
);
|
|
857
|
+
const branch = await getGitBranch(options.cwd);
|
|
858
|
+
const commit = await getGitCommit(options.cwd);
|
|
859
|
+
const status = await getGitStatus(options.cwd);
|
|
860
|
+
const markdown = `# Verification Report
|
|
861
|
+
|
|
862
|
+
- Timestamp: ${nowIso}
|
|
863
|
+
- Repo: ${path10.basename(options.cwd)}
|
|
864
|
+
- Git branch: ${branch || "not available"}
|
|
865
|
+
- Git commit: ${commit || "not available"}
|
|
866
|
+
- Working tree: ${status.trim() ? "dirty" : "clean or unavailable"}
|
|
867
|
+
- Overall status: ${overallStatus}
|
|
868
|
+
|
|
869
|
+
## Commands Run
|
|
870
|
+
${results.length === 0 ? "No verification commands were configured or selected." : results.map(
|
|
871
|
+
(result) => `### ${result.key}: \`${result.command}\`
|
|
872
|
+
|
|
873
|
+
- Exit code: ${result.exitCode}
|
|
874
|
+
- Status: ${result.passed ? "pass" : "fail"}
|
|
875
|
+
|
|
876
|
+
\`\`\`text
|
|
877
|
+
${excerpt(result.output || "(no output)")}
|
|
878
|
+
\`\`\``
|
|
879
|
+
).join("\n\n")}
|
|
880
|
+
|
|
881
|
+
## Not Run
|
|
882
|
+
${notRun.length ? notRun.map((item) => `- ${item}`).join("\n") : "- Nothing skipped."}
|
|
883
|
+
|
|
884
|
+
## Recommended Next Actions
|
|
885
|
+
${overallStatus === "pass" ? "- Review the diff and prepare a handoff summary." : overallStatus === "fail" ? "- Fix failing commands before claiming completion." : "- Add test, lint, typecheck, or build commands to agentloop.config.json."}
|
|
886
|
+
`;
|
|
887
|
+
await writeTextFile(reportPath, markdown);
|
|
888
|
+
return { overallStatus, commands: results, notRun, markdown, reportPath };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/cli/commands/verify.ts
|
|
892
|
+
function collect(value, previous) {
|
|
893
|
+
previous.push(value);
|
|
894
|
+
return previous;
|
|
895
|
+
}
|
|
896
|
+
function verifyCommand() {
|
|
897
|
+
return new Command4("verify").description("Run configured verification commands and write a report").option("--task <path>", "task contract path for humans to cross-reference").option("--json", "print machine-readable output").option("--no-build", "skip build command").option("--no-test", "skip test command").option("--no-lint", "skip lint command").option("--no-typecheck", "skip typecheck command").option("--command <command>", "custom command to run", collect, []).action(async (options) => {
|
|
898
|
+
const config = await loadAgentLoopConfig(process.cwd());
|
|
899
|
+
const result = await runVerification({
|
|
900
|
+
cwd: process.cwd(),
|
|
901
|
+
config,
|
|
902
|
+
skip: {
|
|
903
|
+
build: options.build === false,
|
|
904
|
+
test: options.test === false,
|
|
905
|
+
lint: options.lint === false,
|
|
906
|
+
typecheck: options.typecheck === false
|
|
907
|
+
},
|
|
908
|
+
customCommands: options.command
|
|
909
|
+
});
|
|
910
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
911
|
+
else
|
|
912
|
+
console.log(
|
|
913
|
+
`Verification report written: ${result.reportPath}
|
|
914
|
+
Overall status: ${result.overallStatus}`
|
|
915
|
+
);
|
|
916
|
+
if (result.overallStatus === "fail") process.exitCode = 1;
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/cli/commands/summarize.ts
|
|
921
|
+
import { Command as Command5 } from "commander";
|
|
922
|
+
|
|
923
|
+
// src/core/pr-summary.ts
|
|
924
|
+
import path11 from "path";
|
|
925
|
+
import { readdir as readdir4, readFile as readFile6 } from "fs/promises";
|
|
926
|
+
function extractLine(markdown, pattern, fallback) {
|
|
927
|
+
if (!markdown) return fallback;
|
|
928
|
+
const match = markdown.match(pattern);
|
|
929
|
+
return match?.[1]?.trim() || fallback;
|
|
930
|
+
}
|
|
931
|
+
function generatePrSummary(input) {
|
|
932
|
+
const taskTitle = extractLine(input.taskMarkdown, /^#\s+(.+)$/m, "No task contract found.");
|
|
933
|
+
const verification = extractLine(
|
|
934
|
+
input.verificationMarkdown,
|
|
935
|
+
/Overall status:\s*([a-z-]+)/i,
|
|
936
|
+
"No verification report found."
|
|
937
|
+
);
|
|
938
|
+
const verificationLine = verification === "No verification report found." ? verification : `Overall status: ${verification}`;
|
|
939
|
+
const markdown = `# PR Summary
|
|
940
|
+
|
|
941
|
+
- Generated: ${input.timestamp}
|
|
942
|
+
- Task context: ${taskTitle}
|
|
943
|
+
- Verification status: ${verificationLine}
|
|
944
|
+
|
|
945
|
+
## Summary
|
|
946
|
+
This summary was generated deterministically from git status, the latest task contract, and the latest verification report.
|
|
947
|
+
|
|
948
|
+
## Changed Files
|
|
949
|
+
${input.changedFiles.length ? input.changedFiles.map((file) => `- ${file.status} \`${file.path}\``).join("\n") : "- No changed files detected."}
|
|
950
|
+
|
|
951
|
+
## Diff Stats
|
|
952
|
+
${input.diffStat?.trim() || "No diff stats available."}
|
|
953
|
+
|
|
954
|
+
## Behaviour Changed
|
|
955
|
+
- Review changed files and task contract to confirm intended behavior.
|
|
956
|
+
|
|
957
|
+
## Verification Performed
|
|
958
|
+
- ${verificationLine}
|
|
959
|
+
|
|
960
|
+
## Verification Not Performed
|
|
961
|
+
- Check the verification report for skipped commands.
|
|
962
|
+
|
|
963
|
+
## Risks
|
|
964
|
+
- Re-check protected files such as migrations, secrets, auth, billing, deployment, and public APIs before merge.
|
|
965
|
+
|
|
966
|
+
## Rollback Notes
|
|
967
|
+
- Revert the changed files or revert the merge commit if this lands as a PR.
|
|
968
|
+
|
|
969
|
+
## Reviewer Checklist
|
|
970
|
+
- [ ] Acceptance criteria match the task contract.
|
|
971
|
+
- [ ] Verification evidence is adequate for the change.
|
|
972
|
+
- [ ] Risk areas have been reviewed.
|
|
973
|
+
- [ ] Rollback plan is clear.
|
|
974
|
+
|
|
975
|
+
## Follow-Ups
|
|
976
|
+
- Capture any deferred work in ROADMAP.md or a new task contract.
|
|
977
|
+
`;
|
|
978
|
+
return { markdown };
|
|
979
|
+
}
|
|
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
|
+
async function summarizeRepository(options) {
|
|
989
|
+
const timestamp = options.timestamp ?? formatTimestamp();
|
|
990
|
+
const status = await getGitStatus(options.cwd);
|
|
991
|
+
const changedFiles = await parseGitStatus(status);
|
|
992
|
+
const diffStat = await getGitDiffStat(options.cwd);
|
|
993
|
+
const taskPath = options.taskPath ?? await latestMarkdownFile(path11.join(options.cwd, options.config.paths.tasksDir));
|
|
994
|
+
const reportPath = options.reportPath ?? await latestMarkdownFile(path11.join(options.cwd, options.config.paths.reportsDir));
|
|
995
|
+
const taskMarkdown = taskPath && await pathExists(taskPath) ? await readFile6(taskPath, "utf8") : void 0;
|
|
996
|
+
const verificationMarkdown = reportPath && await pathExists(reportPath) ? await readFile6(reportPath, "utf8") : void 0;
|
|
997
|
+
const summary = generatePrSummary({
|
|
998
|
+
timestamp,
|
|
999
|
+
status,
|
|
1000
|
+
changedFiles,
|
|
1001
|
+
taskMarkdown,
|
|
1002
|
+
verificationMarkdown,
|
|
1003
|
+
diffStat
|
|
1004
|
+
});
|
|
1005
|
+
const outPath = path11.join(
|
|
1006
|
+
options.cwd,
|
|
1007
|
+
options.config.paths.handoffsDir,
|
|
1008
|
+
`${timestamp}-pr-summary.md`
|
|
1009
|
+
);
|
|
1010
|
+
if (options.write) await writeTextFile(outPath, summary.markdown);
|
|
1011
|
+
return { ...summary, outPath, changedFiles };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/cli/commands/summarize.ts
|
|
1015
|
+
function summarizeCommand() {
|
|
1016
|
+
return new Command5("summarize").description("Generate a deterministic PR/reviewer summary").option("--task <path>", "task contract path").option("--report <path>", "verification report path").option("--format <format>", "markdown or json", "markdown").option("--write", "write summary to .agentloop/handoffs").option("--json", "print JSON output").action(async (options) => {
|
|
1017
|
+
const config = await loadAgentLoopConfig(process.cwd());
|
|
1018
|
+
const result = await summarizeRepository({
|
|
1019
|
+
cwd: process.cwd(),
|
|
1020
|
+
config,
|
|
1021
|
+
taskPath: typeof options.task === "string" ? options.task : void 0,
|
|
1022
|
+
reportPath: typeof options.report === "string" ? options.report : void 0,
|
|
1023
|
+
write: Boolean(options.write)
|
|
1024
|
+
});
|
|
1025
|
+
if (options.json || options.format === "json") {
|
|
1026
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1027
|
+
} else {
|
|
1028
|
+
console.log(result.markdown);
|
|
1029
|
+
if (options.write) console.log(`
|
|
1030
|
+
Summary written: ${result.outPath}`);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/cli/commands/install-agent.ts
|
|
1036
|
+
import { Command as Command6 } from "commander";
|
|
1037
|
+
|
|
1038
|
+
// src/core/agent-installation.ts
|
|
1039
|
+
import path12 from "path";
|
|
1040
|
+
function isSupportedAgent(value) {
|
|
1041
|
+
return SUPPORTED_AGENTS.includes(value);
|
|
1042
|
+
}
|
|
1043
|
+
var displayNames = {
|
|
1044
|
+
codex: "Codex",
|
|
1045
|
+
"claude-code": "Claude Code",
|
|
1046
|
+
cursor: "Cursor",
|
|
1047
|
+
opencode: "OpenCode",
|
|
1048
|
+
"gemini-cli": "Gemini CLI",
|
|
1049
|
+
"github-copilot-cli": "GitHub Copilot CLI",
|
|
1050
|
+
generic: "Generic Coding Agent"
|
|
1051
|
+
};
|
|
1052
|
+
async function installAgentInstructions(options) {
|
|
1053
|
+
const agentFilePath = path12.join(options.cwd, ".agentloop", "agents", `${options.agent}.md`);
|
|
1054
|
+
const content = await readTemplate(`agents/${options.agent}.md`, {
|
|
1055
|
+
agentName: displayNames[options.agent]
|
|
1056
|
+
});
|
|
1057
|
+
await writeTextFile(agentFilePath, content);
|
|
1058
|
+
const agentsPath = path12.join(options.cwd, "AGENTS.md");
|
|
1059
|
+
const existing = await readTextIfExists(agentsPath);
|
|
1060
|
+
const marker = `<!-- agentloopkit-agent:${options.agent} -->`;
|
|
1061
|
+
if (!existing.includes(marker)) {
|
|
1062
|
+
const block = `
|
|
1063
|
+
|
|
1064
|
+
${marker}
|
|
1065
|
+
## AgentLoopKit: ${displayNames[options.agent]}
|
|
1066
|
+
|
|
1067
|
+
- Agent instructions: .agentloop/agents/${options.agent}.md
|
|
1068
|
+
- Read AGENTLOOP.md before changing code.
|
|
1069
|
+
- Use task contracts, verification reports, and handoff summaries.
|
|
1070
|
+
<!-- /agentloopkit-agent:${options.agent} -->
|
|
1071
|
+
`;
|
|
1072
|
+
await writeTextFile(
|
|
1073
|
+
agentsPath,
|
|
1074
|
+
existing ? `${existing.trimEnd()}
|
|
1075
|
+
${block}` : block.trimStart()
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
return { agentFilePath, agentsPath };
|
|
1079
|
+
}
|
|
1080
|
+
async function installAllAgentInstructions(options) {
|
|
1081
|
+
const results = [];
|
|
1082
|
+
for (const agent of SUPPORTED_AGENTS) {
|
|
1083
|
+
results.push(await installAgentInstructions({ cwd: options.cwd, agent }));
|
|
1084
|
+
}
|
|
1085
|
+
return results;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/cli/commands/install-agent.ts
|
|
1089
|
+
function installAgentCommand() {
|
|
1090
|
+
return new Command6("install-agent").description("Install agent-specific instruction files").argument("<agent>", `one of: ${SUPPORTED_AGENTS.join(", ")}, all`).action(async (agent) => {
|
|
1091
|
+
if (agent === "all") {
|
|
1092
|
+
const results = await installAllAgentInstructions({ cwd: process.cwd() });
|
|
1093
|
+
console.log(`Agent instructions written: ${results.length}`);
|
|
1094
|
+
console.log("AGENTS.md now references all bundled agent instructions.");
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (!isSupportedAgent(agent)) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Unsupported agent "${agent}". Supported agents: ${SUPPORTED_AGENTS.join(", ")}, all`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const result = await installAgentInstructions({ cwd: process.cwd(), agent });
|
|
1103
|
+
console.log(`Agent instructions written: ${result.agentFilePath}`);
|
|
1104
|
+
console.log("AGENTS.md now references the agent instructions.");
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/cli/commands/list-templates.ts
|
|
1109
|
+
import { Command as Command7 } from "commander";
|
|
1110
|
+
function listTemplatesCommand() {
|
|
1111
|
+
return new Command7("list-templates").description("List available AgentLoopKit templates").action(async () => {
|
|
1112
|
+
const templates = await listTemplateFiles();
|
|
1113
|
+
for (const [group, files] of Object.entries(templates)) {
|
|
1114
|
+
console.log(`${group}:`);
|
|
1115
|
+
for (const file of files) console.log(` - ${file}`);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/cli/commands/version.ts
|
|
1121
|
+
import { Command as Command8 } from "commander";
|
|
1122
|
+
function versionCommand() {
|
|
1123
|
+
return new Command8("version").description("Print CLI version").action(() => {
|
|
1124
|
+
console.log("0.1.0");
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/cli/index.ts
|
|
1129
|
+
var program = new Command9();
|
|
1130
|
+
program.name("agentloop").description("A drop-in engineering loop for coding agents.").version("0.1.0", "-V, --version", "print CLI version");
|
|
1131
|
+
program.addCommand(initCommand());
|
|
1132
|
+
program.addCommand(doctorCommand());
|
|
1133
|
+
program.addCommand(createTaskCommand());
|
|
1134
|
+
program.addCommand(verifyCommand());
|
|
1135
|
+
program.addCommand(summarizeCommand());
|
|
1136
|
+
program.addCommand(installAgentCommand());
|
|
1137
|
+
program.addCommand(listTemplatesCommand());
|
|
1138
|
+
program.addCommand(versionCommand());
|
|
1139
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
1140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1141
|
+
console.error(`agentloop: ${message}`);
|
|
1142
|
+
process.exitCode = 1;
|
|
1143
|
+
});
|
|
1144
|
+
//# sourceMappingURL=index.js.map
|