agentloopkit 0.24.1 → 0.24.3

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 CHANGED
@@ -39,18 +39,21 @@ Coding agents often move fast but leave reviewers with weak evidence: unclear sc
39
39
 
40
40
  ## Install
41
41
 
42
- Use it with `npx` inside an existing repository:
42
+ Use it with `npx` from the root of the repository you want to configure:
43
43
 
44
44
  ```bash
45
- npx agentloopkit init
45
+ cd /path/to/your/repo
46
46
  npx agentloopkit init --dry-run
47
+ npx agentloopkit init
47
48
  ```
48
49
 
50
+ `init` writes files into the current directory. Do not run it from `~` unless you intend to configure your home directory. `--dry-run` previews the file changes without writing them.
51
+
49
52
  Pin the current version when you need repeatable CI or team setup:
50
53
 
51
54
  ```bash
52
- npx --yes agentloopkit@0.24.1 version
53
- npx --yes agentloopkit@0.24.1 init
55
+ npx --yes agentloopkit@0.24.2 version
56
+ npx --yes agentloopkit@0.24.2 init
54
57
  ```
55
58
 
56
59
  Run the CLI after install:
@@ -112,6 +115,7 @@ pnpm build
112
115
  | --------------------------------------- | ------------------------------------------------------------------------------ |
113
116
  | `agentloop init` | Generate the repo harness and config |
114
117
  | `agentloop init --dry-run` | Preview generated files without writing them |
118
+ | `agentloop init --force` | Allow initialization when the current directory is your home directory |
115
119
  | `agentloop doctor` | Check setup health, template version, commands, git state, and risk categories |
116
120
  | `agentloop create-task` | Create a task contract in `.agentloop/tasks/` |
117
121
  | `agentloop task list` | List task contracts and show the pinned active task |
@@ -265,7 +269,7 @@ Each contract records:
265
269
  .agentloop/reports/YYYY-MM-DD-HH-mm-verification-report.md
266
270
  ```
267
271
 
268
- Pass `--task .agentloop/tasks/file.md` to include task title, type, status, and path in the report. AgentLoopKit reads Markdown task contracts for this flag. It reports `.env`-style paths as unavailable instead of reading them.
272
+ Pass `--task .agentloop/tasks/file.md` to include task title, type, status, and path in the report. The path must point to a Markdown task contract inside the configured task directory. Invalid paths are reported as unavailable instead of being read.
269
273
 
270
274
  It does not hide failures. Failed reports include a short failure summary with each failed command, exit code, and final useful output lines before the full command output. If long logs are truncated, the report keeps the first and last output so the final error stays visible. If no commands are configured, it writes a report saying nothing was verified.
271
275
 
@@ -380,7 +384,7 @@ See `docs/ci-summary.md`.
380
384
  ```bash
381
385
  agentloop release-notes
382
386
  agentloop release-notes --from v0.19.0 --to HEAD
383
- agentloop release-notes --release-version 0.24.0
387
+ agentloop release-notes --release-version 0.24.2
384
388
  agentloop release-notes --json
385
389
  agentloop release-notes --write
386
390
  ```
@@ -446,7 +450,7 @@ Use `agentloop check-gates --strict` as a review-evidence gate in pull request C
446
450
 
447
451
  CI-generated verification reports include GitHub Actions provenance when available, so reviewers can trace an artifact back to the workflow run that created it.
448
452
 
449
- See `docs/github-actions.md`, `examples/github-actions/`, `examples/gitlab-ci/`, and `examples/buildkite/` for copy-pasteable workflows. Pin `agentloopkit@0.24.1` or a newer vetted release when reproducibility matters.
453
+ See `docs/github-actions.md`, `examples/github-actions/`, `examples/gitlab-ci/`, and `examples/buildkite/` for copy-pasteable workflows. Pin `agentloopkit@0.24.2` or a newer vetted release when reproducibility matters.
450
454
 
451
455
  ## PR Summaries
452
456
 
package/dist/cli/index.js CHANGED
@@ -8,7 +8,8 @@ import { Command } from "commander";
8
8
 
9
9
  // src/core/init.ts
10
10
  import path6 from "path";
11
- import { readdir as readdir3 } from "fs/promises";
11
+ import { readdir as readdir3, realpath } from "fs/promises";
12
+ import { homedir } from "os";
12
13
 
13
14
  // src/core/constants.ts
14
15
  var PACKAGE_NAME = "agentloopkit";
@@ -196,14 +197,20 @@ async function listFilesRecursive(root, options = {}) {
196
197
  options.ignore ?? [".git", ".agentloop", "node_modules", "dist", "coverage"]
197
198
  );
198
199
  const files = [];
199
- async function walk(current) {
200
+ let inspectedEntries = 0;
201
+ async function walk(current, depth) {
200
202
  if (!await pathExists(current)) return;
203
+ if (options.maxEntries !== void 0 && inspectedEntries >= options.maxEntries) return;
201
204
  const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
202
205
  for (const entry of entries) {
206
+ if (options.maxEntries !== void 0 && inspectedEntries >= options.maxEntries) break;
207
+ inspectedEntries += 1;
203
208
  if (ignore.has(entry.name)) continue;
204
209
  const absolute = path2.join(current, entry.name);
205
210
  if (entry.isDirectory()) {
206
- await walk(absolute);
211
+ if (options.maxDepth === void 0 || depth < options.maxDepth) {
212
+ await walk(absolute, depth + 1);
213
+ }
207
214
  } else if (entry.isFile()) {
208
215
  files.push(absolute);
209
216
  }
@@ -211,7 +218,7 @@ async function listFilesRecursive(root, options = {}) {
211
218
  }
212
219
  const rootStat = await stat(root).catch(() => void 0);
213
220
  if (!rootStat?.isDirectory()) return [];
214
- await walk(root);
221
+ await walk(root, 0);
215
222
  return files;
216
223
  }
217
224
 
@@ -247,6 +254,8 @@ var MONOREPO_FILE_MARKERS = [
247
254
  "lerna.json",
248
255
  "rush.json"
249
256
  ];
257
+ var FALLBACK_PROJECT_DETECTION_MAX_DEPTH = 2;
258
+ var FALLBACK_PROJECT_DETECTION_MAX_ENTRIES = 500;
250
259
  async function readPackageJson(cwd) {
251
260
  const filePath = path4.join(cwd, "package.json");
252
261
  if (!await pathExists(filePath)) return void 0;
@@ -267,7 +276,10 @@ async function detectProjectType(cwd) {
267
276
  if (await pathExists(path4.join(cwd, "pyproject.toml")) || await pathExists(path4.join(cwd, "requirements.txt"))) {
268
277
  return "python";
269
278
  }
270
- const files = await listFilesRecursive(cwd);
279
+ const files = await listFilesRecursive(cwd, {
280
+ maxDepth: FALLBACK_PROJECT_DETECTION_MAX_DEPTH,
281
+ maxEntries: FALLBACK_PROJECT_DETECTION_MAX_ENTRIES
282
+ });
271
283
  const relativeFiles = files.map((file) => path4.relative(cwd, file));
272
284
  const hasCode = relativeFiles.some(
273
285
  (file) => /\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|cs)$/.test(file)
@@ -383,6 +395,13 @@ ${section.trim()}
383
395
  `);
384
396
  result.updated.push(filePath);
385
397
  }
398
+ async function resolveComparablePath(filePath) {
399
+ try {
400
+ return await realpath(filePath);
401
+ } catch {
402
+ return path6.resolve(filePath);
403
+ }
404
+ }
386
405
  async function initializeAgentLoop(options) {
387
406
  const result = {
388
407
  created: [],
@@ -391,6 +410,16 @@ async function initializeAgentLoop(options) {
391
410
  dryRun: Boolean(options.dryRun)
392
411
  };
393
412
  const cwd = options.cwd;
413
+ const homeDirectory = options.homeDirectory ?? homedir();
414
+ const [resolvedCwd, resolvedHomeDirectory] = await Promise.all([
415
+ resolveComparablePath(cwd),
416
+ resolveComparablePath(homeDirectory)
417
+ ]);
418
+ if (!options.force && resolvedCwd === resolvedHomeDirectory) {
419
+ throw new Error(
420
+ "Refusing to initialize your home directory. Run this inside a project repository, or pass --force if you intentionally want AgentLoopKit files in your home directory."
421
+ );
422
+ }
394
423
  const packageManager = await detectPackageManager(cwd);
395
424
  const projectType = await detectProjectType(cwd);
396
425
  const projectName = await detectProjectName(cwd);
@@ -473,8 +502,12 @@ var consoleLogger = {
473
502
 
474
503
  // src/cli/commands/init.ts
475
504
  function initCommand() {
476
- 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) => {
477
- const result = await initializeAgentLoop({ cwd: process.cwd(), dryRun: options.dryRun });
505
+ return new Command("init").description("Initialize AgentLoopKit in the current repository").option("--dry-run", "show planned changes without writing files").option("--json", "print machine-readable output").option("--force", "allow initialization when the current directory is your home directory").action(async (options) => {
506
+ const result = await initializeAgentLoop({
507
+ cwd: process.cwd(),
508
+ dryRun: options.dryRun,
509
+ force: options.force
510
+ });
478
511
  if (options.json) {
479
512
  consoleLogger.info(JSON.stringify(result, null, 2));
480
513
  return;
@@ -821,7 +854,17 @@ ${input.rollbackNotes || "Document how to revert or disable this change."}
821
854
  async function createTaskContractFile(options) {
822
855
  const createdDate = options.input.createdDate ?? formatDate();
823
856
  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);
857
+ const absolutePath = path9.isAbsolute(relativePath2) ? path9.resolve(relativePath2) : path9.resolve(options.cwd, relativePath2);
858
+ const tasksRoot2 = path9.resolve(options.cwd, options.config.paths.tasksDir);
859
+ const relativeToTasks = path9.relative(tasksRoot2, absolutePath);
860
+ if (relativeToTasks === "" || relativeToTasks.startsWith("..") || path9.isAbsolute(relativeToTasks)) {
861
+ throw new AgentLoopError(
862
+ `Task output path must stay inside ${options.config.paths.tasksDir}.`
863
+ );
864
+ }
865
+ if (!absolutePath.endsWith(".md")) {
866
+ throw new AgentLoopError("Task output path must be a Markdown file.");
867
+ }
825
868
  const markdown = generateTaskContract({ ...options.input, createdDate });
826
869
  await writeTextFile(absolutePath, markdown);
827
870
  return { path: absolutePath, markdown };
@@ -1076,10 +1119,16 @@ function isMarkdownTaskPath(taskPath) {
1076
1119
  const segments = normalized.split("/").filter(Boolean);
1077
1120
  return normalized.endsWith(".md") && !segments.some((segment) => segment === ".env" || segment.startsWith(".env."));
1078
1121
  }
1079
- async function renderTaskContext(cwd, taskPath) {
1122
+ function isInside(parent, child) {
1123
+ const relative2 = path10.relative(parent, child);
1124
+ return relative2 === "" || !relative2.startsWith("..") && !path10.isAbsolute(relative2);
1125
+ }
1126
+ async function renderTaskContext(cwd, config, taskPath) {
1080
1127
  if (!taskPath?.trim()) return "";
1081
1128
  const cleanPath = taskPath.trim();
1082
- if (!isMarkdownTaskPath(cleanPath)) {
1129
+ const absolutePath = path10.isAbsolute(cleanPath) ? path10.resolve(cleanPath) : path10.resolve(cwd, cleanPath);
1130
+ const tasksRoot2 = path10.resolve(cwd, config.paths.tasksDir);
1131
+ if (!isMarkdownTaskPath(cleanPath) || !isInside(tasksRoot2, absolutePath)) {
1083
1132
  return `## Task Context
1084
1133
  - Path: ${cleanPath}
1085
1134
  - Status: unavailable
@@ -1087,7 +1136,6 @@ async function renderTaskContext(cwd, taskPath) {
1087
1136
 
1088
1137
  `;
1089
1138
  }
1090
- const absolutePath = path10.isAbsolute(cleanPath) ? cleanPath : path10.join(cwd, cleanPath);
1091
1139
  try {
1092
1140
  const markdown = await readFile6(absolutePath, "utf8");
1093
1141
  const metadata = parseTaskMetadata(markdown);
@@ -1146,7 +1194,7 @@ async function runVerification(options) {
1146
1194
  const branch = await getGitBranch(options.cwd);
1147
1195
  const commit = await getGitCommit(options.cwd);
1148
1196
  const status = await getGitStatus(options.cwd);
1149
- const taskContext = await renderTaskContext(options.cwd, options.taskPath);
1197
+ const taskContext = await renderTaskContext(options.cwd, options.config, options.taskPath);
1150
1198
  const markdown = `# Verification Report
1151
1199
 
1152
1200
  - Timestamp: ${nowIso}
@@ -1252,7 +1300,7 @@ function statePath(cwd, config) {
1252
1300
  function toStoredPath(cwd, absolutePath) {
1253
1301
  return path12.relative(cwd, absolutePath).split(path12.sep).join("/");
1254
1302
  }
1255
- function isInside(parent, child) {
1303
+ function isInside2(parent, child) {
1256
1304
  const relative2 = path12.relative(parent, child);
1257
1305
  return relative2 === "" || !relative2.startsWith("..") && !path12.isAbsolute(relative2);
1258
1306
  }
@@ -1281,7 +1329,7 @@ async function resolveTaskPath(options) {
1281
1329
  const absolutePath = path12.isAbsolute(options.taskPath) ? path12.resolve(options.taskPath) : path12.resolve(options.cwd, options.taskPath);
1282
1330
  const root = tasksRoot(options.cwd, options.config);
1283
1331
  const displayRoot = options.config.paths.tasksDir;
1284
- if (!isInside(root, absolutePath)) {
1332
+ if (!isInside2(root, absolutePath)) {
1285
1333
  if (!options.strict) return void 0;
1286
1334
  throw new AgentLoopError(`Active task must be inside ${displayRoot}.`);
1287
1335
  }