agentloopkit 0.24.4 → 0.25.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 CHANGED
@@ -49,11 +49,19 @@ npx agentloopkit init
49
49
 
50
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
51
 
52
+ If you want AgentLoopKit for your local agent workflow but do not want to commit the generated harness:
53
+
54
+ ```bash
55
+ npx agentloopkit init --local-only
56
+ ```
57
+
58
+ `--local-only` writes the same files, then adds a marked block to this clone's `.git/info/exclude` for `.agentloop/`, `AGENTS.md`, `AGENTLOOP.md`, and `agentloop.config.json`. It does not edit `.gitignore`, global Git config, shell profiles, or files outside the current repo.
59
+
52
60
  Pin the current version when you need repeatable CI or team setup:
53
61
 
54
62
  ```bash
55
- npx --yes agentloopkit@0.24.4 version
56
- npx --yes agentloopkit@0.24.4 init
63
+ npx --yes agentloopkit@0.25.0 version
64
+ npx --yes agentloopkit@0.25.0 init
57
65
  ```
58
66
 
59
67
  Run the CLI after install:
@@ -87,10 +95,10 @@ npx agentloopkit completion powershell
87
95
  ```
88
96
 
89
97
  <p align="center">
90
- <img src="https://raw.githubusercontent.com/abhiyoheswaran1/AgentLoopKit/main/docs/assets/readme/agentloopkit-cli.gif" alt="Terminal demo running AgentLoopKit init, task contracts, active task status, task-linked verify, handoff, reports, badges, release notes, gates, and task archive commands" width="100%">
98
+ <img src="https://raw.githubusercontent.com/abhiyoheswaran1/AgentLoopKit/main/docs/assets/readme/agentloopkit-cli.gif" alt="Terminal demo running AgentLoopKit init, doctor, task contract creation, task status, verification, handoff, gates, HTML report, and badge commands" width="100%">
91
99
  </p>
92
100
 
93
- The VHS demo is generated from committed sources in this repository.
101
+ The terminal demo is generated from committed VHS sources in this repository.
94
102
 
95
103
  Pinned team usage:
96
104
 
@@ -116,6 +124,7 @@ pnpm build
116
124
  | `agentloop init` | Generate the repo harness and config |
117
125
  | `agentloop init --dry-run` | Preview generated files without writing them |
118
126
  | `agentloop init --force` | Allow initialization when the current directory is your home directory |
127
+ | `agentloop init --local-only` | Generate the harness but exclude it from this clone's git status |
119
128
  | `agentloop doctor` | Check setup health, template version, commands, git state, and risk categories |
120
129
  | `agentloop create-task` | Create a task contract in `.agentloop/tasks/` |
121
130
  | `agentloop task list` | List task contracts and show the pinned active task |
@@ -384,7 +393,7 @@ See `docs/ci-summary.md`.
384
393
  ```bash
385
394
  agentloop release-notes
386
395
  agentloop release-notes --from v0.19.0 --to HEAD
387
- agentloop release-notes --release-version 0.24.4
396
+ agentloop release-notes --release-version 0.25.0
388
397
  agentloop release-notes --json
389
398
  agentloop release-notes --write
390
399
  ```
@@ -450,7 +459,7 @@ Use `agentloop check-gates --strict` as a review-evidence gate in pull request C
450
459
 
451
460
  CI-generated verification reports include GitHub Actions provenance when available, so reviewers can trace an artifact back to the workflow run that created it.
452
461
 
453
- See `docs/github-actions.md`, `examples/github-actions/`, `examples/gitlab-ci/`, and `examples/buildkite/` for copy-pasteable workflows. Pin `agentloopkit@0.24.4` or a newer vetted release when reproducibility matters.
462
+ See `docs/github-actions.md`, `examples/github-actions/`, `examples/gitlab-ci/`, and `examples/buildkite/` for copy-pasteable workflows. Pin `agentloopkit@0.25.0` or a newer vetted release when reproducibility matters.
454
463
 
455
464
  ## PR Summaries
456
465
 
package/dist/cli/index.js CHANGED
@@ -8,7 +8,7 @@ import { Command } from "commander";
8
8
 
9
9
  // src/core/init.ts
10
10
  import path6 from "path";
11
- import { readdir as readdir3, realpath } from "fs/promises";
11
+ import { readdir as readdir3, realpath, stat as stat2 } from "fs/promises";
12
12
  import { homedir } from "os";
13
13
 
14
14
  // src/core/constants.ts
@@ -342,6 +342,21 @@ async function listTemplateFiles() {
342
342
  }
343
343
 
344
344
  // src/core/init.ts
345
+ var LOCAL_ONLY_EXCLUDE_START = "# agentloopkit:local-only:start";
346
+ var LOCAL_ONLY_EXCLUDE_END = "# agentloopkit:local-only:end";
347
+ var LOCAL_ONLY_NOTICE_START = "<!-- agentloopkit:local-only:start -->";
348
+ var LOCAL_ONLY_NOTICE_END = "<!-- agentloopkit:local-only:end -->";
349
+ var LOCAL_ONLY_EXCLUDE_PATTERNS = [
350
+ `${AGENTLOOP_DIR}/`,
351
+ AGENTS_FILE,
352
+ AGENTLOOP_FILE,
353
+ CONFIG_FILE
354
+ ];
355
+ var LOCAL_ONLY_NOTICE = `${LOCAL_ONLY_NOTICE_START}
356
+ ## Local-only AgentLoopKit harness
357
+
358
+ This AgentLoopKit setup is excluded by this clone's \`.git/info/exclude\`. Use these files for local agent work. Do not commit these AgentLoopKit files unless a maintainer intentionally converts the repo to a shared harness.
359
+ ${LOCAL_ONLY_NOTICE_END}`;
345
360
  async function writeGeneratedFile(filePath, content, result) {
346
361
  if (await pathExists(filePath)) {
347
362
  result.skipped.push(filePath);
@@ -395,6 +410,65 @@ ${section.trim()}
395
410
  `);
396
411
  result.updated.push(filePath);
397
412
  }
413
+ async function resolveGitInfoExcludePath(cwd) {
414
+ const dotGitPath = path6.join(cwd, ".git");
415
+ const dotGitStat = await stat2(dotGitPath).catch(() => void 0);
416
+ if (!dotGitStat) return void 0;
417
+ if (dotGitStat.isDirectory()) {
418
+ return path6.join(dotGitPath, "info", "exclude");
419
+ }
420
+ if (!dotGitStat.isFile()) return void 0;
421
+ const gitFile = await readTextIfExists(dotGitPath);
422
+ const match = /^gitdir:\s*(.+)\s*$/m.exec(gitFile);
423
+ if (!match) return void 0;
424
+ const gitDir = match[1].trim();
425
+ const resolvedGitDir = path6.isAbsolute(gitDir) ? gitDir : path6.resolve(cwd, gitDir);
426
+ return path6.join(resolvedGitDir, "info", "exclude");
427
+ }
428
+ async function upsertLocalOnlyGitExclude(cwd, result) {
429
+ const excludePath = await resolveGitInfoExcludePath(cwd);
430
+ if (!excludePath) {
431
+ throw new Error(
432
+ "Local-only mode requires a Git repository because it writes to .git/info/exclude. Run git init first, or run agentloop init without --local-only."
433
+ );
434
+ }
435
+ result.localOnly = {
436
+ excludePath,
437
+ patterns: [...LOCAL_ONLY_EXCLUDE_PATTERNS]
438
+ };
439
+ const excludeExists = await pathExists(excludePath);
440
+ const existing = await readTextIfExists(excludePath);
441
+ if (existing.includes(LOCAL_ONLY_EXCLUDE_START)) return;
442
+ const block = [
443
+ LOCAL_ONLY_EXCLUDE_START,
444
+ "# AgentLoopKit local-only harness files for this clone.",
445
+ ...LOCAL_ONLY_EXCLUDE_PATTERNS,
446
+ LOCAL_ONLY_EXCLUDE_END
447
+ ].join("\n");
448
+ if (result.dryRun) {
449
+ (excludeExists ? result.updated : result.created).push(excludePath);
450
+ return;
451
+ }
452
+ const prefix = existing.trimEnd();
453
+ await writeTextFile(excludePath, `${prefix ? `${prefix}
454
+
455
+ ` : ""}${block}
456
+ `);
457
+ (excludeExists ? result.updated : result.created).push(excludePath);
458
+ }
459
+ async function upsertLocalOnlyNotice(filePath, result) {
460
+ const existing = await readTextIfExists(filePath);
461
+ if (existing.includes(LOCAL_ONLY_NOTICE_START)) return;
462
+ if (result.dryRun) {
463
+ (existing ? result.updated : result.created).push(filePath);
464
+ return;
465
+ }
466
+ await writeTextFile(filePath, `${existing.trimEnd()}
467
+
468
+ ${LOCAL_ONLY_NOTICE}
469
+ `);
470
+ (existing ? result.updated : result.created).push(filePath);
471
+ }
398
472
  async function resolveComparablePath(filePath) {
399
473
  try {
400
474
  return await realpath(filePath);
@@ -420,6 +494,9 @@ async function initializeAgentLoop(options) {
420
494
  "Refusing to initialize your home directory. Run this inside a project repository, or pass --force if you intentionally want AgentLoopKit files in your home directory."
421
495
  );
422
496
  }
497
+ if (options.localOnly) {
498
+ await upsertLocalOnlyGitExclude(cwd, result);
499
+ }
423
500
  const packageManager = await detectPackageManager(cwd);
424
501
  const projectType = await detectProjectType(cwd);
425
502
  const projectName = await detectProjectName(cwd);
@@ -438,7 +515,8 @@ async function initializeAgentLoop(options) {
438
515
  lintCommand: commands.lint || "not configured",
439
516
  typecheckCommand: commands.typecheck || "not configured",
440
517
  buildCommand: commands.build || "not configured",
441
- formatCommand: commands.format || "not configured"
518
+ formatCommand: commands.format || "not configured",
519
+ localOnlyInstructions: options.localOnly ? LOCAL_ONLY_NOTICE : ""
442
520
  };
443
521
  for (const group of TEMPLATE_GROUPS) {
444
522
  if (group === "tasks") continue;
@@ -480,6 +558,10 @@ async function initializeAgentLoop(options) {
480
558
  await readTemplate("root/AGENTLOOP.md", values),
481
559
  result
482
560
  );
561
+ if (options.localOnly) {
562
+ await upsertLocalOnlyNotice(path6.join(cwd, AGENTS_FILE), result);
563
+ await upsertLocalOnlyNotice(path6.join(cwd, AGENTLOOP_FILE), result);
564
+ }
483
565
  const configPath = path6.join(cwd, CONFIG_FILE);
484
566
  if (await pathExists(configPath)) {
485
567
  result.skipped.push(configPath);
@@ -502,29 +584,43 @@ var consoleLogger = {
502
584
 
503
585
  // src/cli/commands/init.ts
504
586
  function initCommand() {
505
- return new Command("init").description("Initialize AgentLoopKit in the current repository").option("--dry-run", "show planned changes without writing files").option("--json", "print machine-readable output").option("--force", "allow initialization when the current directory is your home directory").action(async (options) => {
506
- const result = await initializeAgentLoop({
507
- cwd: process.cwd(),
508
- dryRun: options.dryRun,
509
- force: options.force
510
- });
511
- if (options.json) {
512
- consoleLogger.info(JSON.stringify(result, null, 2));
513
- return;
514
- }
515
- consoleLogger.info(
516
- options.dryRun ? "AgentLoopKit init dry run complete." : "AgentLoopKit initialized."
517
- );
518
- consoleLogger.info(`Created: ${result.created.length}`);
519
- consoleLogger.info(`Updated: ${result.updated.length}`);
520
- consoleLogger.info(`Skipped: ${result.skipped.length}`);
521
- if (!options.dryRun) {
522
- consoleLogger.info("\nNext steps:");
523
- consoleLogger.info("- Review AGENTS.md and AGENTLOOP.md");
524
- consoleLogger.info("- Run agentloop doctor");
525
- consoleLogger.info("- Create a task with agentloop create-task");
587
+ return new Command("init").description("Initialize AgentLoopKit in the current repository").option("--dry-run", "show planned changes without writing files").option("--json", "print machine-readable output").option("--force", "allow initialization when the current directory is your home directory").option(
588
+ "--local-only",
589
+ "keep generated AgentLoopKit files out of git by updating this repo clone .git/info/exclude"
590
+ ).action(
591
+ async (options) => {
592
+ const result = await initializeAgentLoop({
593
+ cwd: process.cwd(),
594
+ dryRun: options.dryRun,
595
+ force: options.force,
596
+ localOnly: options.localOnly
597
+ });
598
+ if (options.json) {
599
+ consoleLogger.info(JSON.stringify(result, null, 2));
600
+ return;
601
+ }
602
+ consoleLogger.info(
603
+ options.dryRun ? "AgentLoopKit init dry run complete." : "AgentLoopKit initialized."
604
+ );
605
+ consoleLogger.info(`Created: ${result.created.length}`);
606
+ consoleLogger.info(`Updated: ${result.updated.length}`);
607
+ consoleLogger.info(`Skipped: ${result.skipped.length}`);
608
+ if (result.localOnly) {
609
+ consoleLogger.info("\nLocal-only mode:");
610
+ consoleLogger.info(`- Updated exclude file: ${result.localOnly.excludePath}`);
611
+ consoleLogger.info(
612
+ "- AgentLoopKit files stay on disk for local agents but stay out of git status."
613
+ );
614
+ consoleLogger.info("- Undo: remove the agentloopkit:local-only block from .git/info/exclude.");
615
+ }
616
+ if (!options.dryRun) {
617
+ consoleLogger.info("\nNext steps:");
618
+ consoleLogger.info("- Review AGENTS.md and AGENTLOOP.md");
619
+ consoleLogger.info("- Run agentloop doctor");
620
+ consoleLogger.info("- Create a task with agentloop create-task");
621
+ }
526
622
  }
527
- });
623
+ );
528
624
  }
529
625
 
530
626
  // src/cli/commands/doctor.ts
@@ -1268,7 +1364,7 @@ import { readFile as readFile8 } from "fs/promises";
1268
1364
 
1269
1365
  // src/core/artifacts.ts
1270
1366
  import path11 from "path";
1271
- import { readdir as readdir4, stat as stat2 } from "fs/promises";
1367
+ import { readdir as readdir4, stat as stat3 } from "fs/promises";
1272
1368
  var verificationReportPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-verification-report\.md$/;
1273
1369
  var prSummaryPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-pr-summary\.md$/;
1274
1370
  var ciSummaryPattern = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-ci-summary\.md$/;
@@ -1279,7 +1375,7 @@ async function latestMarkdownFile(dir, options = {}) {
1279
1375
  (entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name.toLowerCase() !== "readme.md"
1280
1376
  ).filter((entry) => !options.pattern || options.pattern.test(entry.name)).map(async (entry) => {
1281
1377
  const filePath = path11.join(dir, entry.name);
1282
- const fileStat = await stat2(filePath);
1378
+ const fileStat = await stat3(filePath);
1283
1379
  return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
1284
1380
  })
1285
1381
  );
@@ -1292,7 +1388,7 @@ async function latestMarkdownFile(dir, options = {}) {
1292
1388
 
1293
1389
  // src/core/task-state.ts
1294
1390
  import path12 from "path";
1295
- import { mkdir as mkdir2, readdir as readdir5, readFile as readFile7, rename, rm, stat as stat3 } from "fs/promises";
1391
+ import { mkdir as mkdir2, readdir as readdir5, readFile as readFile7, rename, rm, stat as stat4 } from "fs/promises";
1296
1392
  var TASK_STATUSES = ["proposed", "in-progress", "blocked", "review", "done"];
1297
1393
  function statePath(cwd, config) {
1298
1394
  return path12.join(cwd, config.paths.agentloopDir, "state.json");
@@ -1337,7 +1433,7 @@ async function resolveTaskPath(options) {
1337
1433
  if (!options.strict) return void 0;
1338
1434
  throw new AgentLoopError("Active task must be a Markdown file.");
1339
1435
  }
1340
- const fileStat = await stat3(absolutePath).catch(() => void 0);
1436
+ const fileStat = await stat4(absolutePath).catch(() => void 0);
1341
1437
  if (!fileStat?.isFile()) {
1342
1438
  if (!options.strict) return void 0;
1343
1439
  throw new AgentLoopError(`Task contract not found: ${options.taskPath}`);
@@ -1444,7 +1540,7 @@ async function listTasks(options) {
1444
1540
  const filePath = path12.join(root, entry.name);
1445
1541
  const [metadata, fileStat] = await Promise.all([
1446
1542
  readTaskMetadata(options.cwd, filePath),
1447
- stat3(filePath)
1543
+ stat4(filePath)
1448
1544
  ]);
1449
1545
  return {
1450
1546
  ...metadata,
@@ -1776,7 +1872,7 @@ import { Command as Command9 } from "commander";
1776
1872
 
1777
1873
  // src/core/status.ts
1778
1874
  import path15 from "path";
1779
- import { readFile as readFile9, stat as stat4 } from "fs/promises";
1875
+ import { readFile as readFile9, stat as stat5 } from "fs/promises";
1780
1876
  function extractHeading2(markdown, fallback) {
1781
1877
  return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() || fallback;
1782
1878
  }
@@ -1789,7 +1885,7 @@ function extractOverallStatus(markdown) {
1789
1885
  async function readTask(cwd, filePath) {
1790
1886
  if (!filePath) return void 0;
1791
1887
  const markdown = await readFile9(filePath, "utf8");
1792
- const fileStat = await stat4(filePath);
1888
+ const fileStat = await stat5(filePath);
1793
1889
  return {
1794
1890
  path: path15.relative(cwd, filePath),
1795
1891
  title: extractHeading2(markdown, path15.basename(filePath, ".md")),
@@ -1800,7 +1896,7 @@ async function readTask(cwd, filePath) {
1800
1896
  async function readReport(cwd, filePath) {
1801
1897
  if (!filePath) return void 0;
1802
1898
  const markdown = await readFile9(filePath, "utf8");
1803
- const fileStat = await stat4(filePath);
1899
+ const fileStat = await stat5(filePath);
1804
1900
  return {
1805
1901
  path: path15.relative(cwd, filePath),
1806
1902
  title: extractHeading2(markdown, path15.basename(filePath, ".md")),
@@ -2566,7 +2662,7 @@ import { Command as Command14 } from "commander";
2566
2662
 
2567
2663
  // src/core/html-report.ts
2568
2664
  import path17 from "path";
2569
- import { readFile as readFile11, readdir as readdir6, stat as stat5 } from "fs/promises";
2665
+ import { readFile as readFile11, readdir as readdir6, stat as stat6 } from "fs/promises";
2570
2666
  function escapeHtml(value) {
2571
2667
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2572
2668
  }
@@ -2598,7 +2694,7 @@ async function latestMatchingMarkdownFile(dir, pattern) {
2598
2694
  const entries = await Promise.all(
2599
2695
  (await readdir6(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && pattern.test(entry.name)).map(async (entry) => {
2600
2696
  const filePath = path17.join(dir, entry.name);
2601
- const fileStat = await stat5(filePath);
2697
+ const fileStat = await stat6(filePath);
2602
2698
  return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
2603
2699
  })
2604
2700
  );
@@ -2827,7 +2923,7 @@ import { Command as Command15 } from "commander";
2827
2923
 
2828
2924
  // src/core/badge.ts
2829
2925
  import path18 from "path";
2830
- import { readFile as readFile12, readdir as readdir7, stat as stat6 } from "fs/promises";
2926
+ import { readFile as readFile12, readdir as readdir7, stat as stat7 } from "fs/promises";
2831
2927
  var verificationReportPattern2 = /^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-verification-report\.md$/;
2832
2928
  function escapeXml(value) {
2833
2929
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -2858,7 +2954,7 @@ async function latestVerificationReport(dir) {
2858
2954
  const entries = await Promise.all(
2859
2955
  (await readdir7(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && verificationReportPattern2.test(entry.name)).map(async (entry) => {
2860
2956
  const filePath = path18.join(dir, entry.name);
2861
- const fileStat = await stat6(filePath);
2957
+ const fileStat = await stat7(filePath);
2862
2958
  return { filePath, name: entry.name, mtimeMs: fileStat.mtimeMs };
2863
2959
  })
2864
2960
  );
@@ -2996,7 +3092,7 @@ import { Command as Command16 } from "commander";
2996
3092
 
2997
3093
  // src/core/policy.ts
2998
3094
  import path19 from "path";
2999
- import { readdir as readdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
3095
+ import { readdir as readdir8, readFile as readFile13, stat as stat8 } from "fs/promises";
3000
3096
  function policyRoot(cwd, config) {
3001
3097
  return path19.resolve(cwd, config.paths.agentloopDir, "policies");
3002
3098
  }
@@ -3013,7 +3109,7 @@ function normalizeContent(content) {
3013
3109
  return content.replace(/\r\n/g, "\n");
3014
3110
  }
3015
3111
  async function ensurePolicyRoot(root) {
3016
- const rootStat = await stat7(root).catch(() => void 0);
3112
+ const rootStat = await stat8(root).catch(() => void 0);
3017
3113
  if (!rootStat?.isDirectory()) {
3018
3114
  throw new AgentLoopError(
3019
3115
  "No AgentLoopKit policy files found. Run `agentloop init` to generate .agentloop/policies/."