compound-agent 1.7.4 → 1.8.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/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { z } from 'zod';
8
8
  import * as fs from 'fs/promises';
9
9
  import { readFile, mkdir, appendFile, writeFile, chmod, rm, rename, readdir } from 'fs/promises';
10
10
  import { createRequire } from 'module';
11
- import { execSync, execFileSync, spawn } from 'child_process';
11
+ import { execSync, spawnSync, execFileSync, spawn } from 'child_process';
12
12
  import { fileURLToPath } from 'url';
13
13
  import { Command } from 'commander';
14
14
  import chalk5 from 'chalk';
@@ -3132,7 +3132,7 @@ function checkBeadsAvailable() {
3132
3132
  } catch {
3133
3133
  return {
3134
3134
  available: false,
3135
- message: "Beads CLI not found. Recommended for full workflow (issue tracking, deps, TDD pipeline). Install: https://github.com/Nathandela/beads"
3135
+ message: "Beads CLI not found. Recommended for full workflow (issue tracking, deps, TDD pipeline).\nInstall: curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash\nOr run: ca install-beads"
3136
3136
  };
3137
3137
  }
3138
3138
  }
@@ -3313,7 +3313,7 @@ This project uses compound-agent for session memory via **CLI commands**.
3313
3313
  | Command | Purpose |
3314
3314
  |---------|---------|
3315
3315
  | \`npx ca search "query"\` | Search lessons - MUST call before architectural decisions; use anytime you need context |
3316
- | \`npx ca knowledge "query"\` | Ask the project docs any question - MUST call before architectural decisions; use freely |
3316
+ | \`npx ca knowledge "query"\` | Semantic search over project docs - MUST call before architectural decisions; use keyword phrases, not questions |
3317
3317
  | \`npx ca learn "insight"\` | Capture lessons - use AFTER corrections or discoveries |
3318
3318
  | \`npx ca list\` | List all stored lessons |
3319
3319
  | \`npx ca show <id>\` | Show details of a specific lesson |
@@ -3680,6 +3680,8 @@ function printBeadsFullStatus(check) {
3680
3680
  if (check.initialized) {
3681
3681
  console.log(` Beads health: ${check.healthy ? "OK" : `issues found${check.healthMessage ? ` \u2014 ${check.healthMessage}` : ""}`}`);
3682
3682
  }
3683
+ } else if (check.healthMessage) {
3684
+ console.log(` ${check.healthMessage}`);
3683
3685
  }
3684
3686
  }
3685
3687
  function printScopeStatus(scope) {
@@ -5694,7 +5696,7 @@ npx ca loop # Generate infinity loop script for autonomous pr
5694
5696
  npx ca loop --epics epic-1 epic-2
5695
5697
  npx ca loop --output my-loop.sh
5696
5698
  npx ca loop --max-retries 5
5697
- npx ca loop --model claude-opus-4-6
5699
+ npx ca loop --model claude-opus-4-6[1m]
5698
5700
  npx ca loop --force # Overwrite existing script
5699
5701
  \`\`\`
5700
5702
 
@@ -9434,6 +9436,8 @@ var LESSON_COUNT_WARNING_THRESHOLD = 20;
9434
9436
  var AGE_FLAG_THRESHOLD_DAYS = 90;
9435
9437
  var ISO_DATE_PREFIX_LENGTH = 10;
9436
9438
  var AVG_DECIMAL_PLACES = 1;
9439
+ var DEFAULT_LOOP_MODEL = "claude-opus-4-6[1m]";
9440
+ var MODEL_PATTERN = /^[a-zA-Z0-9_.:[\]/-]+$/;
9437
9441
 
9438
9442
  // src/commands/management-helpers.ts
9439
9443
  function formatLessonHuman(lesson) {
@@ -9651,7 +9655,7 @@ async function runDoctor(repoRoot) {
9651
9655
  checks.push(pnpmCheck);
9652
9656
  }
9653
9657
  const beadsResult = checkBeadsAvailable();
9654
- checks.push(beadsResult.available ? { name: "Beads CLI", status: "pass" } : { name: "Beads CLI", status: "warn", fix: "Install beads: https://github.com/Nathandela/beads" });
9658
+ checks.push(beadsResult.available ? { name: "Beads CLI", status: "pass" } : { name: "Beads CLI", status: "warn", fix: "Run: ca install-beads" });
9655
9659
  checks.push(checkGitignoreHealth(repoRoot) ? { name: ".gitignore health", status: "pass" } : { name: ".gitignore health", status: "warn", fix: "Run: npx ca setup --update" });
9656
9660
  const docPath = join(repoRoot, "docs", "compound", "README.md");
9657
9661
  checks.push(existsSync(docPath) ? { name: "Usage documentation", status: "pass" } : { name: "Usage documentation", status: "warn", fix: "Run: npx ca setup" });
@@ -9986,14 +9990,13 @@ var FETCH_TIMEOUT_MS = 3e3;
9986
9990
  var CACHE_FILENAME = "update-check.json";
9987
9991
  async function fetchLatestVersion(packageName = "compound-agent") {
9988
9992
  try {
9989
- const res = await fetch(`https://registry.npmjs.org/${packageName}`, {
9990
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
9991
- });
9993
+ const res = await fetch(
9994
+ `https://registry.npmjs.org/-/package/${packageName}/dist-tags`,
9995
+ { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }
9996
+ );
9992
9997
  if (!res.ok) return null;
9993
9998
  const data = await res.json();
9994
- const tags = data["dist-tags"];
9995
- if (typeof tags !== "object" || tags === null) return null;
9996
- const latest = tags["latest"];
9999
+ const latest = data["latest"];
9997
10000
  return typeof latest === "string" ? latest : null;
9998
10001
  } catch {
9999
10002
  return null;
@@ -10027,13 +10030,41 @@ async function checkForUpdate(cacheDir) {
10027
10030
  return null;
10028
10031
  }
10029
10032
  }
10033
+ function isMajorUpdate(current, latest) {
10034
+ return parseInt(latest.split(".")[0], 10) > parseInt(current.split(".")[0], 10);
10035
+ }
10030
10036
  function formatUpdateNotification(current, latest) {
10031
- return `Update available: ${current} -> ${latest}
10032
- Run: pnpm update --latest compound-agent`;
10037
+ const label = isMajorUpdate(current, latest) ? "Major update" : "Update available";
10038
+ const warning = isMajorUpdate(current, latest) ? "\n May contain breaking changes -- check the changelog." : "";
10039
+ return [
10040
+ `${label}: ${current} -> ${latest}${warning}`,
10041
+ `Run: npm update -g compound-agent (global)`,
10042
+ ` pnpm add -D compound-agent@latest (dev dependency)`
10043
+ ].join("\n");
10044
+ }
10045
+ function formatUpdateNotificationMarkdown(current, latest) {
10046
+ const urgency = isMajorUpdate(current, latest) ? " (MAJOR - may contain breaking changes)" : "";
10047
+ return `
10048
+ ---
10049
+ # Update Available
10050
+ compound-agent v${latest} is available (current: v${current})${urgency}.
10051
+ Run: \`npm update -g compound-agent\` (global) or \`pnpm add -D compound-agent@latest\` (dev dependency)
10052
+ `;
10053
+ }
10054
+ function shouldCheckForUpdate() {
10055
+ if (!process.stdout.isTTY) return false;
10056
+ if (process.env["CI"]) return false;
10057
+ if (process.env["NO_UPDATE_NOTIFIER"]) return false;
10058
+ if (process.env["NODE_ENV"] === "test") return false;
10059
+ return true;
10033
10060
  }
10034
10061
  function semverGt(a, b) {
10035
10062
  const parse = (v) => {
10036
- const parts = v.split(".").map((n) => parseInt(n, 10) || 0);
10063
+ const clean = v.split("-")[0];
10064
+ const parts = clean.split(".").map((n) => {
10065
+ const num = parseInt(n, 10);
10066
+ return isNaN(num) ? 0 : num;
10067
+ });
10037
10068
  return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
10038
10069
  };
10039
10070
  const [aMaj, aMin, aPat] = parse(a);
@@ -10048,7 +10079,7 @@ function readCache(cachePath) {
10048
10079
  if (Date.now() - stat.mtimeMs > CACHE_TTL_MS) return null;
10049
10080
  const raw = readFileSync(cachePath, "utf-8");
10050
10081
  const data = JSON.parse(raw);
10051
- if (!data.latest) return null;
10082
+ if (typeof data.latest !== "string" || !data.latest) return null;
10052
10083
  return data;
10053
10084
  } catch {
10054
10085
  return null;
@@ -10067,7 +10098,7 @@ var TRUST_LANGUAGE_TEMPLATE = `# Compound Agent Active
10067
10098
  | Command | Purpose |
10068
10099
  |---------|---------|
10069
10100
  | \`npx ca search "query"\` | Search lessons - MUST call before architectural decisions; use anytime you need context |
10070
- | \`npx ca knowledge "query"\` | Ask the project docs any question - MUST call before architectural decisions; use freely |
10101
+ | \`npx ca knowledge "query"\` | Semantic search over project docs - MUST call before architectural decisions; use keyword phrases, not questions |
10071
10102
  | \`npx ca learn "insight"\` | Capture lessons - call AFTER corrections or discoveries |
10072
10103
 
10073
10104
  ## Core Constraints
@@ -10167,15 +10198,11 @@ ${formattedLessons}
10167
10198
  if (cookitSection !== null) {
10168
10199
  output += cookitSection;
10169
10200
  }
10170
- if (!process.stdout.isTTY) {
10201
+ if (!process.stdout.isTTY && !process.env["CI"] && !process.env["NO_UPDATE_NOTIFIER"]) {
10171
10202
  try {
10172
10203
  const updateResult = await checkForUpdate(join(root, ".claude", ".cache"));
10173
10204
  if (updateResult?.updateAvailable) {
10174
- output += `
10175
- ---
10176
- # Update Available
10177
- compound-agent v${updateResult.latest} is available (current: v${updateResult.current}). Run \`pnpm update --latest compound-agent\` to update.
10178
- `;
10205
+ output += formatUpdateNotificationMarkdown(updateResult.current, updateResult.latest);
10179
10206
  }
10180
10207
  } catch {
10181
10208
  }
@@ -10905,70 +10932,59 @@ function registerVerifyGatesCommand(program) {
10905
10932
  }
10906
10933
 
10907
10934
  // src/changelog-data.ts
10908
- var CHANGELOG_RECENT = `## [1.7.4] - 2026-03-11
10935
+ var CHANGELOG_RECENT = `## [1.8.0] - 2026-03-15
10909
10936
 
10910
10937
  ### Added
10911
10938
 
10912
- - **Research-enriched phase skills**: Applied insights from 3 PhD-level research documents (Science of Decomposition, Architecture Under Uncertainty, Emergent Behavior in Composed Systems) across all 6 core phase skills:
10913
- - **Architect**: reversibility analysis (Baldwin & Clark), change volatility, 6-subagent convoy (added STPA control structure analyst + structural-semantic gap analyst), implicit interface contracts (threading, backpressure, delivery guarantees), organizational alignment (Team Topologies), multi-criteria validation gate (structural/semantic/organizational/economic), assumption capture with fitness functions and re-decomposition triggers
10914
- - **Spec-dev**: Cynefin classification (Clear/Complicated/Complex), composition EARS templates (timeout/retry interactions), change volatility assessment
10915
- - **Plan**: boundary stability check, Last Responsible Moment identification, change coupling prevention
10916
- - **Work**: Fowler technical debt quadrant (only Prudent/Deliberate accepted), composition boundary verification with metastable failure checks
10917
- - **Review**: composition-specific reviewers (boundary-reviewer, control-structure-reviewer, observability-reviewer), architect assumption validation
10918
- - **Compound**: decomposition quality assessment, assumption tracking (predicted vs actual), emergence root cause classification (Garlan/STPA/phase transition)
10919
- - **Lint graduation in compound phase**: The compound phase (step 10) now spawns a \`lint-classifier\` subagent that classifies each captured insight as LINTABLE, PARTIAL, or NOT_LINTABLE. High-confidence lintable insights are promoted to beads tasks under a "Linting Improvement" epic with self-contained rule specifications. Two rule classes: Class A (native \`rules.json\` \u2014 regex/glob) and Class B (external linter \u2014 AST analysis).
10920
- - **Linter detection module** (\`src/lint/\`): Scans repos for ESLint (flat + legacy configs including TypeScript variants), Ruff (including \`pyproject.toml\`), Clippy, golangci-lint, ast-grep, and Semgrep. Exported from the package as \`detectLinter()\`, \`LinterInfoSchema\`, \`LinterNameSchema\`.
10921
- - **Lint-classifier agent template**: Ships via \`npx ca init\` to \`.claude/agents/compound/lint-classifier.md\`. Includes 7 few-shot examples, Class A/B routing, and linter-aware task creation.
10939
+ - **\`ca improve\` command**: Generates a bash script that autonomously improves the codebase using \`improve/*.md\` program files. Each program defines what to improve, how to find work, and how to validate changes. Options: \`--topics\` (filter specific topics), \`--max-iters\` (iterations per topic, default 5), \`--time-budget\` (total seconds, 0=unlimited), \`--model\`, \`--force\`, \`--dry-run\`. Includes \`ca improve init\` subcommand to scaffold an example program file.
10940
+ - **\`ca watch\` command**: Tails and pretty-prints live trace JSONL from infinity loop and improvement loop sessions. Supports \`--epic <id>\` to watch a specific epic, \`--improve\` to watch improvement loop traces, and \`--no-follow\` to print existing trace and exit. Formats tool calls, thinking blocks, token usage, and result markers into a compact, color-coded stream.
10922
10941
 
10923
10942
  ### Fixed
10924
10943
 
10925
- - **PhD research output path**: \`/compound:get-a-phd\` now writes user-generated research to \`docs/research/\` instead of \`docs/compound/research/\`. The \`docs/compound/\` directory is reserved for shipped library content; project-specific research no longer pollutes it. Overlap scanning checks both directories.
10944
+ - **\`git clean\` scoping in improvement loop**: Bare \`git clean -fd\` on rollback was removing all untracked files including the script's own log directory, causing crashes. All three rollback paths now use \`git clean -fd -e "$LOG_DIR/"\` to exclude agent logs.
10945
+ - **Embedded dirty-worktree guard fallthrough**: In embedded mode (when improvement loop runs inside \`ca loop --improve\`), setting \`IMPROVE_RESULT=1\` on a dirty worktree did not prevent the loop body from executing. Restructured to use \`if/else\` so the loop body only runs inside the \`else\` branch.
10946
+ - **\`ca watch --improve\` ignoring \`.latest\` symlink**: The \`--improve\` code path had inline logic that only did reverse filename sort, bypassing the \`.latest\` symlink that the improvement loop maintains. Refactored \`findLatestTraceFile()\` with a \`prefix\` parameter to unify both code paths.
10947
+ - **\`--topics\` flag ignored in \`get_topics()\`**: The \`TOPIC_FILTER\` variable from the CLI \`--topics\` flag was not used in the generated bash \`get_topics()\` function, causing all topics to run regardless of filtering.
10948
+ - **Update-check hardening**: Switched to a lightweight npm registry endpoint, added CI environment guards, and corrected the update command shown to users.
10926
10949
 
10927
- ## [1.7.3] - 2026-03-09
10950
+ ## [1.7.6] - 2026-03-12
10928
10951
 
10929
10952
  ### Added
10930
10953
 
10931
- - **Update notification**: CLI checks npm registry for newer versions on startup (24h file-based cache, non-blocking). Notification displays after command output (TTY only) and in \`ca prime\` context.
10954
+ - **\`ca install-beads\` command**: Standalone subcommand to install the beads CLI via the official script. Includes a platform guard (skips on Windows with \`exitCode 1\`), an "already installed" short-circuit, a \`--yes\` flag to bypass the confirmation hint (safe: never runs \`curl | bash\` without explicit opt-in), \`spawnSync\` with a 60-second timeout, and a post-install shell-reload warning. Non-TTY mode without \`--yes\` prints the install command as a copy-pasteable hint rather than silently doing nothing.
10932
10955
 
10933
10956
  ### Fixed
10934
10957
 
10935
- - **Spec-dev epic type**: \`bd create\` in spec-dev Phase 4 now explicitly uses \`--type=epic\`, preventing epics from defaulting to task type. Plan phase also validates the epic type and corrects it if needed.
10936
- - **Update-check hardening**: Added explicit \`res.ok\` check in \`fetchLatestVersion\`, removed dead \`checkedAt\` cache field, removed redundant type cast.
10958
+ - **Beads hint display**: \`printBeadsFullStatus\` was silently swallowing the install hint message when the beads CLI was not found. The curl install command is now printed below the "not found" line.
10959
+ - **Beads hint text**: \`checkBeadsAvailable\` now returns the actual \`curl -sSL ... | bash\` install command in its message instead of a bare repo URL.
10960
+ - **Doctor fix message**: \`ca doctor\` now shows \`Run: ca install-beads\` for the missing-beads check instead of pointing to a URL.
10961
+ - **\`ca knowledge\` description**: Reframed from "Ask the project docs any question" to "Semantic search over project docs \u2014 use keyword phrases, not questions" in both the live prime template and the setup template, reflecting the underlying embedding RAG retrieval mechanism.
10937
10962
 
10938
- ## [1.7.2] - 2026-03-09
10963
+ ## [1.7.5] - 2026-03-12
10939
10964
 
10940
10965
  ### Added
10941
10966
 
10942
- - **Loop review phase**: \`ca loop\` can now spawn independent AI reviewers (Claude Sonnet, Claude Opus, Gemini, Codex) in parallel after every N completed epics. Reviewers produce severity-tagged reports, an implementer session fixes findings, and reviewers are resumed (not fresh) to verify fixes. Iterates until all approve or max cycles reached. New CLI options: \`--reviewers\`, \`--review-every\`, \`--max-review-cycles\`, \`--review-blocking\`, \`--review-model\`. Gracefully skips unavailable CLIs.
10943
-
10944
- ### Fixed
10945
-
10946
- - **Security: command injection in \`ca test-summary --cmd\`**: User-supplied test command is now validated against an allowlist of safe prefixes (\`pnpm\`, \`npm\`, \`vitest\`, etc.) and shell metacharacters are rejected.
10947
- - **Security: shell injection in \`ca doctor\`**: Replaced \`execSync(cmd, {shell})\` with \`execFileSync('bd', ['doctor'])\` to avoid shell interpretation.
10948
- - **Portable timeout for macOS**: Generated loop scripts now use a \`portable_timeout()\` wrapper that tries GNU \`timeout\`, then \`gtimeout\` (Homebrew coreutils), then a shell-based kill/watchdog fallback. Previously failed silently on macOS.
10949
- - **Session ID python3 fallback**: Review phase session ID management now falls back to python3 when \`jq\` is unavailable, with a centralized \`read_session_id()\` helper.
10950
- - **Git diff window stability**: Replaced fragile \`HEAD~$N..HEAD\` commit-count arithmetic with SHA-based \`$REVIEW_BASE_SHA..HEAD\` diff ranges, immune to rebases and cherry-picks.
10951
- - **ID collision risk**: Memory item IDs now use 64-bit entropy (16 hex chars) instead of 32-bit (8 hex chars).
10952
- - **JSONL resilience**: Malformed lines in JSONL files are now skipped with try/catch per line instead of crashing the entire read.
10953
- - **Stdin timeout leak**: \`clearTimeout\` now called in \`finally\` block for stdin reads in retrieval and hooks.
10954
- - **Double JSONL read eliminated**: \`readMemoryItems()\` now returns \`deletedIds\` set, removing the need for a separate \`wasLessonDeleted()\` file read.
10955
- - **FTS5 trigger optimization**: SQLite update trigger now scoped to FTS-indexed columns only, reducing unnecessary FTS rebuilds.
10956
- - **Clustering noise accuracy**: Single-item clusters now correctly returned as \`noise\` instead of an always-empty noise array.
10957
- - **Embed-worker path validation**: \`embed-worker\` command now validates that \`repoRoot\` exists and is a directory before proceeding.
10958
- - **Script check timeout**: Rule-based script checks now have a 30-second default timeout, configurable via \`check.timeout\`.
10967
+ - **\`ca feedback\` command**: Surfaces the GitHub Discussions URL for bug reports and feature requests. \`ca feedback --open\` opens the page directly in the browser. Cross-platform (macOS \`open\`, Windows \`start\`, Linux \`xdg-open\`).
10968
+ - **Star and feedback prompt in \`ca about\`**: TTY sessions now see a star-us link and the GitHub Discussions URL after the changelog output.
10959
10969
 
10960
10970
  ### Changed
10961
10971
 
10962
- - **Anchored approval detection**: Review loop now uses \`^REVIEW_APPROVED\` anchored grep to prevent false positives from partial-line matches.
10963
- - **Numeric option validation**: \`--review-every\` and \`--max-review-cycles\` now reject NaN, negative, and non-integer values.
10964
- - **\`isModelUsable()\` replaced**: \`compound\` command now uses lightweight \`isModelAvailable()\` (fs check) instead of loading the 278MB model just to probe.
10965
- - **Dead code removed**: \`addCompoundAgentHook()\`, back-compat hook aliases (\`CLAUDE_READ_TRACKER_HOOK_CONFIG\`, \`CLAUDE_STOP_AUDIT_HOOK_CONFIG\`), and \`wasLessonDeleted()\` removed.
10966
- - **Hardcoded model extracted**: Five occurrences of \`'claude-opus-4-6'\` in loop.ts extracted to \`DEFAULT_MODEL\` constant.
10967
- - **EPIC_ID_PATTERN deduplicated**: \`watch.ts\` now imports \`LOOP_EPIC_ID_PATTERN\` from \`loop.ts\` instead of maintaining a duplicate.
10968
- - **\`warn()\` output corrected**: \`shared.ts\` warn helper now writes to \`stderr\` instead of \`stdout\`.
10969
- - **Templates import fixed**: \`templates.ts\` now imports \`VERSION\` from \`../version.js\` instead of barrel re-export.`;
10972
+ - **README overhaul**: Complete rewrite to present compound-agent as a full agentic development environment rather than a memory plugin.
10973
+ - New thesis-driven one-liner that names category, mechanism, and benefit
10974
+ - "What gets installed" inventory table (15 commands, 24 agent role skills, 7 hooks, 5 phase skills, 5 docs)
10975
+ - Three principles section mapping each architecture layer to the problem it solves (Memory / Feedback Loops / Navigable Structure)
10976
+ - "Agents are interchangeable" design principle explained in the overview
10977
+ - Levels of use replacing flat Quick Start: memory-only, structured workflow, and factory mode with code examples
10978
+ - \`/compound:architect\` promoted to its own section with 4-phase description and context-window motivation
10979
+ - Infinity loop elevated from CLI table row to its own section with full flag examples and honest maturity note
10980
+ - Automatic hooks table with per-hook descriptions
10981
+ - Architecture diagram updated to reflect three-principle mapping and accurate counts
10982
+ - Compound loop diagram updated with architect as optional upstream entry point
10983
+ - "Open with an AI agent" entry point in the Documentation section`;
10970
10984
 
10971
10985
  // src/commands/about.ts
10986
+ var REPO_URL = "https://github.com/Nathandela/compound-agent";
10987
+ var DISCUSSIONS_URL = `${REPO_URL}/discussions`;
10972
10988
  function registerAboutCommand(program) {
10973
10989
  program.command("about").description("Show version, animation, and recent changelog").action(async () => {
10974
10990
  if (process.stdout.isTTY) {
@@ -10978,6 +10994,30 @@ function registerAboutCommand(program) {
10978
10994
  }
10979
10995
  console.log("");
10980
10996
  console.log(CHANGELOG_RECENT);
10997
+ if (process.stdout.isTTY) {
10998
+ console.log("");
10999
+ console.log(`Find this useful? Star us: ${REPO_URL}`);
11000
+ console.log(`Feedback & discussions: ${DISCUSSIONS_URL}`);
11001
+ }
11002
+ });
11003
+ }
11004
+ var REPO_URL2 = "https://github.com/Nathandela/compound-agent";
11005
+ var DISCUSSIONS_URL2 = `${REPO_URL2}/discussions`;
11006
+ function openUrl(url) {
11007
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
11008
+ spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
11009
+ }
11010
+ function registerFeedbackCommand(program) {
11011
+ program.command("feedback").description("Open GitHub Discussions to share feedback or report issues").option("--open", "Open the Discussions page in your browser").action((opts) => {
11012
+ console.log(`Feedback & discussions: ${DISCUSSIONS_URL2}`);
11013
+ console.log(`Repository: ${REPO_URL2}`);
11014
+ if (opts.open && process.stdout.isTTY) {
11015
+ openUrl(DISCUSSIONS_URL2);
11016
+ console.log("Opening in browser...");
11017
+ } else if (!opts.open) {
11018
+ console.log("");
11019
+ console.log("Run `ca feedback --open` to open in your browser.");
11020
+ }
10981
11021
  });
10982
11022
  }
10983
11023
 
@@ -11160,8 +11200,8 @@ function registerKnowledgeIndexCommand(program) {
11160
11200
  }
11161
11201
  });
11162
11202
  program.command("embed-worker <repoRoot>", { hidden: true }).description("Internal: background embedding worker").action(async (repoRoot) => {
11163
- const { existsSync: existsSync24, statSync: statSync7 } = await import('fs');
11164
- if (!existsSync24(repoRoot) || !statSync7(repoRoot).isDirectory()) {
11203
+ const { existsSync: existsSync25, statSync: statSync7 } = await import('fs');
11204
+ if (!existsSync25(repoRoot) || !statSync7(repoRoot).isDirectory()) {
11165
11205
  out.error(`Invalid repoRoot: "${repoRoot}" is not a directory`);
11166
11206
  process.exitCode = 1;
11167
11207
  return;
@@ -11264,6 +11304,41 @@ async function cleanLessonsAction() {
11264
11304
  function registerCleanLessonsCommand(program) {
11265
11305
  program.command("clean-lessons").description("Analyze lessons for semantic duplicates and contradictions").action(cleanLessonsAction);
11266
11306
  }
11307
+ var INSTALL_SCRIPT_URL = "https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh";
11308
+ var INSTALL_CMD = `curl -sSL ${INSTALL_SCRIPT_URL} | bash`;
11309
+ function registerInstallBeadsCommand(program) {
11310
+ program.command("install-beads").description("Install the beads CLI via the official install script").option("--yes", "Skip confirmation prompt and install immediately").action((opts) => {
11311
+ if (process.platform === "win32") {
11312
+ console.error("Beads installation is not supported on Windows.");
11313
+ process.exitCode = 1;
11314
+ return;
11315
+ }
11316
+ if (checkBeadsAvailable().available) {
11317
+ console.log("Beads CLI (bd) is already installed.");
11318
+ return;
11319
+ }
11320
+ console.log(`Install script: ${INSTALL_SCRIPT_URL}`);
11321
+ if (!opts.yes) {
11322
+ console.log(`Run manually: ${INSTALL_CMD}`);
11323
+ return;
11324
+ }
11325
+ const result = spawnSync("bash", ["-c", INSTALL_CMD], {
11326
+ stdio: "inherit",
11327
+ timeout: 6e4
11328
+ });
11329
+ if (result.error) {
11330
+ console.error(`Installation error: ${result.error.message}`);
11331
+ process.exitCode = 1;
11332
+ return;
11333
+ }
11334
+ if (result.status !== 0) {
11335
+ console.error(`Install error: process exited with code ${result.status}.`);
11336
+ process.exitCode = 1;
11337
+ return;
11338
+ }
11339
+ console.log("Restart your shell or run: source ~/.bashrc");
11340
+ });
11341
+ }
11267
11342
 
11268
11343
  // src/commands/capture.ts
11269
11344
  function createLessonFromFlags(trigger, insight, confirmed) {
@@ -11879,15 +11954,304 @@ function registerManagementCommands(program) {
11879
11954
  registerTestSummaryCommand(program);
11880
11955
  registerVerifyGatesCommand(program);
11881
11956
  registerAboutCommand(program);
11957
+ registerFeedbackCommand(program);
11882
11958
  registerKnowledgeCommand(program);
11883
11959
  registerKnowledgeIndexCommand(program);
11884
11960
  registerCleanLessonsCommand(program);
11961
+ registerInstallBeadsCommand(program);
11885
11962
  program.command("worktree").description("(removed) Use Claude Code native worktree support").action(() => {
11886
11963
  console.error("ca worktree has been removed. Use Claude Code's native EnterWorktree support instead.");
11887
11964
  process.exitCode = 1;
11888
11965
  });
11889
11966
  }
11890
11967
 
11968
+ // src/commands/improve-templates.ts
11969
+ function buildTopicDiscovery() {
11970
+ return `
11971
+ get_topics() {
11972
+ local improve_dir="\${IMPROVE_DIR:-improve}"
11973
+ local topics=""
11974
+ if [ -n "$TOPIC_FILTER" ]; then
11975
+ # Use explicit topic list from CLI --topics
11976
+ for topic in $TOPIC_FILTER; do
11977
+ if [ -f "$improve_dir/\${topic}.md" ]; then
11978
+ topics="$topics $topic"
11979
+ else
11980
+ log "WARN: $improve_dir/\${topic}.md not found, skipping"
11981
+ fi
11982
+ done
11983
+ else
11984
+ for f in "$improve_dir"/*.md; do
11985
+ [ -f "$f" ] || continue
11986
+ local topic
11987
+ topic=$(basename "$f" .md)
11988
+ topics="$topics $topic"
11989
+ done
11990
+ fi
11991
+ topics="\${topics# }"
11992
+ if [ -z "$topics" ]; then
11993
+ log "No improve/*.md files found"
11994
+ return 1
11995
+ fi
11996
+ echo "$topics"
11997
+ return 0
11998
+ }
11999
+ `;
12000
+ }
12001
+ function buildImprovePrompt() {
12002
+ return `
12003
+ build_improve_prompt() {
12004
+ local topic="$1"
12005
+ local improve_dir="\${IMPROVE_DIR:-improve}"
12006
+ local program_file="$improve_dir/\${topic}.md"
12007
+
12008
+ if [ ! -f "$program_file" ]; then
12009
+ log "ERROR: $program_file not found"
12010
+ return 1
12011
+ fi
12012
+
12013
+ # Stream static parts via quoted heredoc (no expansion) + file content via cat
12014
+ # Avoids heredoc delimiter collision if .md file contains the delimiter string
12015
+ cat <<'IMPROVE_PROMPT_HEADER'
12016
+ You are running in an autonomous improvement loop. Your task is to make ONE improvement to the codebase.
12017
+
12018
+ ## Your Program
12019
+ IMPROVE_PROMPT_HEADER
12020
+
12021
+ cat "$program_file"
12022
+
12023
+ cat <<'IMPROVE_PROMPT_FOOTER'
12024
+
12025
+ ## Rules
12026
+ - Make ONE focused improvement per iteration.
12027
+ - Run the validation described in your program.
12028
+ - If you successfully improved something and validation passes, commit your changes then output on its own line:
12029
+ IMPROVED
12030
+ - If you tried but found nothing to improve (or improvements don't pass validation), output:
12031
+ NO_IMPROVEMENT
12032
+ - If you encountered an error that prevents you from working, output:
12033
+ FAILED
12034
+ - Do NOT ask questions -- there is no human.
12035
+ - Commit your changes before outputting the marker.
12036
+ - You can inspect what changed with git diff before committing.
12037
+ IMPROVE_PROMPT_FOOTER
12038
+ }
12039
+ `;
12040
+ }
12041
+ function buildImproveMarkerDetection() {
12042
+ return `
12043
+ # detect_improve_marker() - Check for improvement markers in log and trace
12044
+ # Primary: macro log (anchored patterns). Fallback: trace JSONL (unanchored).
12045
+ # Usage: MARKER=$(detect_improve_marker "$LOGFILE" "$TRACEFILE")
12046
+ # Returns: "improved", "no_improvement", "failed", or "none"
12047
+ detect_improve_marker() {
12048
+ local logfile="$1" tracefile="$2"
12049
+
12050
+ # Primary: check extracted text with anchored patterns
12051
+ if [ -s "$logfile" ]; then
12052
+ if grep -q "^IMPROVED$" "$logfile"; then
12053
+ echo "improved"; return 0
12054
+ elif grep -q "^NO_IMPROVEMENT$" "$logfile"; then
12055
+ echo "no_improvement"; return 0
12056
+ elif grep -q "^FAILED$" "$logfile"; then
12057
+ echo "failed"; return 0
12058
+ fi
12059
+ fi
12060
+
12061
+ # Fallback: check raw trace JSONL (unanchored -- markers are inside JSON strings)
12062
+ if [ -s "$tracefile" ]; then
12063
+ if grep -q "IMPROVED" "$tracefile"; then
12064
+ echo "improved"; return 0
12065
+ elif grep -q "NO_IMPROVEMENT" "$tracefile"; then
12066
+ echo "no_improvement"; return 0
12067
+ elif grep -q "FAILED" "$tracefile"; then
12068
+ echo "failed"; return 0
12069
+ fi
12070
+ fi
12071
+
12072
+ echo "none"
12073
+ }
12074
+ `;
12075
+ }
12076
+ function buildImproveObservability() {
12077
+ return `
12078
+ # Observability: status file and execution log
12079
+ IMPROVE_STATUS_FILE="$LOG_DIR/.improve-status.json"
12080
+ IMPROVE_EXEC_LOG="$LOG_DIR/improvement-log.jsonl"
12081
+
12082
+ write_improve_status() {
12083
+ local status="$1"
12084
+ local topic="\${2:-}"
12085
+ local iteration="\${3:-0}"
12086
+ if [ "$status" = "idle" ]; then
12087
+ echo "{\\"status\\":\\"idle\\",\\"timestamp\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\"}" > "$IMPROVE_STATUS_FILE"
12088
+ else
12089
+ echo "{\\"topic\\":\\"$topic\\",\\"iteration\\":$iteration,\\"started_at\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\",\\"status\\":\\"$status\\"}" > "$IMPROVE_STATUS_FILE"
12090
+ fi
12091
+ }
12092
+
12093
+ log_improve_result() {
12094
+ local topic="$1" result="$2" improvements="$3" duration="$4"
12095
+ echo "{\\"topic\\":\\"$topic\\",\\"result\\":\\"$result\\",\\"improvements\\":$improvements,\\"duration_s\\":$duration,\\"timestamp\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\"}" >> "$IMPROVE_EXEC_LOG"
12096
+ }
12097
+ `;
12098
+ }
12099
+ function buildImproveSessionRunner() {
12100
+ return `
12101
+ # Run claude session with two-scope logging
12102
+ PROMPT=$(build_improve_prompt "$TOPIC")
12103
+
12104
+ claude --dangerously-skip-permissions \\
12105
+ --model "$MODEL" \\
12106
+ --output-format stream-json \\
12107
+ --verbose \\
12108
+ -p "$PROMPT" \\
12109
+ 2>"$LOGFILE.stderr" | tee "$TRACEFILE" | extract_text > "$LOGFILE" || true
12110
+
12111
+ # Append stderr to macro log
12112
+ [ -f "$LOGFILE.stderr" ] && cat "$LOGFILE.stderr" >> "$LOGFILE" && rm -f "$LOGFILE.stderr"
12113
+
12114
+ # Health check: warn if macro log extraction failed
12115
+ if [ -s "$TRACEFILE" ] && [ ! -s "$LOGFILE" ]; then
12116
+ log "WARN: Macro log is empty but trace has content (extract_text may have failed)"
12117
+ fi
12118
+
12119
+ MARKER=$(detect_improve_marker "$LOGFILE" "$TRACEFILE")
12120
+ `;
12121
+ }
12122
+ function buildImproveIterationBody() {
12123
+ return buildImproveSessionRunner() + `
12124
+
12125
+ case "$MARKER" in
12126
+ (improved)
12127
+ # Verify the agent actually committed
12128
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
12129
+ log "WARN: Uncommitted changes detected after IMPROVED marker"
12130
+ fi
12131
+ log "Topic $TOPIC improved (iter $ITER)"
12132
+ TOPIC_IMPROVED=$((TOPIC_IMPROVED + 1))
12133
+ CONSECUTIVE_NO_IMPROVE=0
12134
+ git tag -d "$TAG" 2>/dev/null || true
12135
+ ;;
12136
+ (no_improvement)
12137
+ log "Topic $TOPIC: no improvement (iter $ITER), reverting"
12138
+ git reset --hard "$TAG"
12139
+ git clean -fd -e "$LOG_DIR/" 2>/dev/null || true
12140
+ git tag -d "$TAG" 2>/dev/null || true
12141
+ CONSECUTIVE_NO_IMPROVE=$((CONSECUTIVE_NO_IMPROVE + 1))
12142
+ if [ $CONSECUTIVE_NO_IMPROVE -ge 2 ]; then
12143
+ log "Diminishing returns for $TOPIC, moving on"
12144
+ break
12145
+ fi
12146
+ ;;
12147
+ (failed)
12148
+ log "Topic $TOPIC failed (iter $ITER), reverting"
12149
+ git reset --hard "$TAG"
12150
+ git clean -fd -e "$LOG_DIR/" 2>/dev/null || true
12151
+ git tag -d "$TAG" 2>/dev/null || true
12152
+ TOPIC_FAILED=1
12153
+ break
12154
+ ;;
12155
+ (*)
12156
+ log "Topic $TOPIC: no marker detected (iter $ITER), reverting"
12157
+ git reset --hard "$TAG"
12158
+ git clean -fd -e "$LOG_DIR/" 2>/dev/null || true
12159
+ git tag -d "$TAG" 2>/dev/null || true
12160
+ TOPIC_FAILED=1
12161
+ break
12162
+ ;;
12163
+ esac
12164
+ done
12165
+
12166
+ TOPIC_DURATION=$(( $(date +%s) - TOPIC_START ))
12167
+
12168
+ if [ $TOPIC_IMPROVED -gt 0 ]; then
12169
+ IMPROVED_COUNT=$((IMPROVED_COUNT + TOPIC_IMPROVED))
12170
+ log_improve_result "$TOPIC" "improved" "$TOPIC_IMPROVED" "$TOPIC_DURATION"
12171
+ elif [ $TOPIC_FAILED -eq 1 ]; then
12172
+ FAILED_TOPICS=$((FAILED_TOPICS + 1))
12173
+ log_improve_result "$TOPIC" "failed" "0" "$TOPIC_DURATION"
12174
+ else
12175
+ SKIPPED_TOPICS=$((SKIPPED_TOPICS + 1))
12176
+ log_improve_result "$TOPIC" "no_improvement" "0" "$TOPIC_DURATION"
12177
+ fi
12178
+ done`;
12179
+ }
12180
+ function buildImproveMainLoop(options) {
12181
+ const embedded = options.embedded ?? false;
12182
+ const guardOpen = embedded ? "IMPROVE_RESULT=1\nelse" : "exit 1\nfi";
12183
+ const guardClose = embedded ? "fi" : "";
12184
+ return `
12185
+ # Improve loop
12186
+ MAX_ITERS=${options.maxIters}
12187
+ TIME_BUDGET=${options.timeBudget}
12188
+ IMPROVED_COUNT=0
12189
+ FAILED_TOPICS=0
12190
+ SKIPPED_TOPICS=0
12191
+ IMPROVE_START=$(date +%s)
12192
+
12193
+ # Worktree-clean preflight: refuse to run with dirty working tree
12194
+ if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then
12195
+ log "ERROR: Working tree is dirty. Commit or stash changes before running the improvement loop."
12196
+ log " git status:"
12197
+ git status --short
12198
+ ${guardOpen}
12199
+
12200
+ TOPICS=$(get_topics) || { log "No topics found, exiting"; ${embedded ? "IMPROVE_RESULT=0" : "exit 0"}; }
12201
+ log "Improve loop starting"
12202
+ log "Config: max_iters=$MAX_ITERS time_budget=$TIME_BUDGET model=$MODEL"
12203
+ log "Topics: $TOPICS"
12204
+
12205
+ for TOPIC in $TOPICS; do
12206
+ log "Starting topic: $TOPIC"
12207
+ TOPIC_IMPROVED=0
12208
+ TOPIC_FAILED=0
12209
+ CONSECUTIVE_NO_IMPROVE=0
12210
+ TOPIC_START=$(date +%s)
12211
+
12212
+ ITER=0
12213
+ while [ $ITER -lt $MAX_ITERS ]; do
12214
+ ITER=$((ITER + 1))
12215
+
12216
+ # Time budget check
12217
+ if [ $TIME_BUDGET -gt 0 ]; then
12218
+ ELAPSED=$(( $(date +%s) - IMPROVE_START ))
12219
+ if [ $ELAPSED -ge $TIME_BUDGET ]; then
12220
+ log "Time budget exhausted ($ELAPSED >= $TIME_BUDGET seconds)"
12221
+ break 2
12222
+ fi
12223
+ fi
12224
+
12225
+ # Dry-run check BEFORE any side effects (tags, sessions)
12226
+ if [ -n "\${IMPROVE_DRY_RUN:-}" ]; then
12227
+ log "[DRY RUN] Would run claude session for $TOPIC (iter $ITER)"
12228
+ TOPIC_IMPROVED=$((TOPIC_IMPROVED + 1))
12229
+ continue
12230
+ fi
12231
+
12232
+ TS=$(timestamp)
12233
+ LOGFILE="$LOG_DIR/loop_improve_\${TOPIC}-\${TS}.log"
12234
+ TRACEFILE="$LOG_DIR/trace_improve_\${TOPIC}-\${TS}.jsonl"
12235
+ TAG="improve/\${TOPIC}/iter-\${ITER}/pre"
12236
+
12237
+ git tag -f "$TAG"
12238
+ write_improve_status "running" "$TOPIC" "$ITER"
12239
+ ln -sf "$(basename "$TRACEFILE")" "$LOG_DIR/.latest"
12240
+
12241
+ log "Iteration $ITER/$MAX_ITERS for $TOPIC"
12242
+
12243
+ ` + buildImproveIterationBody() + `
12244
+
12245
+ # Summary
12246
+ TOTAL_DURATION=$(( $(date +%s) - IMPROVE_START ))
12247
+ echo "{\\"type\\":\\"summary\\",\\"improved\\":$IMPROVED_COUNT,\\"failed_topics\\":$FAILED_TOPICS,\\"skipped_topics\\":$SKIPPED_TOPICS,\\"total_duration_s\\":$TOTAL_DURATION}" >> "$IMPROVE_EXEC_LOG"
12248
+ write_improve_status "idle"
12249
+ log "Improve loop finished. Improvements: $IMPROVED_COUNT, Failed topics: $FAILED_TOPICS, Skipped: $SKIPPED_TOPICS"
12250
+ ${embedded ? "IMPROVE_RESULT=$( [ $FAILED_TOPICS -eq 0 ] && echo 0 || echo 1 )" : "[ $FAILED_TOPICS -eq 0 ] && exit 0 || exit 1"}
12251
+ ${guardClose}
12252
+ `;
12253
+ }
12254
+
11891
12255
  // src/commands/loop-templates.ts
11892
12256
  function buildDependencyCheck() {
11893
12257
  return `
@@ -12203,7 +12567,7 @@ fi
12203
12567
  `
12204
12568
  };
12205
12569
  }
12206
- function buildMainLoop(reviewOptions) {
12570
+ function buildMainLoop(reviewOptions, skipExit) {
12207
12571
  const { counterInit, periodic, final } = buildReviewTriggers(
12208
12572
  reviewOptions?.hasReview ?? false,
12209
12573
  reviewOptions?.reviewEvery ?? 0
@@ -12282,7 +12646,162 @@ TOTAL_DURATION=$(( $(date +%s) - LOOP_START ))
12282
12646
  echo "{\\"type\\":\\"summary\\",\\"completed\\":$COMPLETED,\\"failed\\":$FAILED,\\"skipped\\":$SKIPPED,\\"total_duration_s\\":$TOTAL_DURATION}" >> "$EXEC_LOG"
12283
12647
  write_status "idle"
12284
12648
  log "Loop finished. Completed: $COMPLETED, Failed: $FAILED, Skipped: $SKIPPED"
12285
- [ $FAILED -eq 0 ] && exit 0 || exit 1`;
12649
+ ${skipExit ? "" : "[ $FAILED -eq 0 ] && exit 0 || exit 1"}`;
12650
+ }
12651
+
12652
+ // src/commands/improve.ts
12653
+ var TOPIC_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
12654
+ function buildImproveScriptHeader(timestamp, maxIters, timeBudget, model, topicFilter) {
12655
+ return `#!/usr/bin/env bash
12656
+ # Improvement Loop - Generated by: ca improve
12657
+ # Date: ${timestamp}
12658
+ # Iterates over improve/*.md programs to autonomously improve the codebase.
12659
+ #
12660
+ # Usage:
12661
+ # ./improvement-loop.sh
12662
+ # IMPROVE_DRY_RUN=1 ./improvement-loop.sh # Preview without executing
12663
+
12664
+ set -euo pipefail
12665
+
12666
+ # Config
12667
+ MAX_ITERS=${maxIters}
12668
+ TIME_BUDGET=${timeBudget}
12669
+ MODEL="${model}"
12670
+ TOPIC_FILTER="${topicFilter}"
12671
+ LOG_DIR="agent_logs"
12672
+
12673
+ # Helpers
12674
+ timestamp() { date '+%Y-%m-%d_%H-%M-%S'; }
12675
+ log() { echo "[$(timestamp)] $*"; }
12676
+ die() { log "FATAL: $*"; exit 1; }
12677
+
12678
+ command -v claude >/dev/null || die "claude CLI required"
12679
+ command -v git >/dev/null || die "git required"
12680
+
12681
+ # Detect JSON parser: prefer jq, fall back to python3
12682
+ HAS_JQ=false
12683
+ command -v jq >/dev/null 2>&1 && HAS_JQ=true
12684
+ if [ "$HAS_JQ" = false ]; then
12685
+ command -v python3 >/dev/null 2>&1 || die "jq or python3 required for JSON parsing"
12686
+ fi
12687
+
12688
+ mkdir -p "$LOG_DIR"
12689
+ `;
12690
+ }
12691
+ function validateImproveOptions(options) {
12692
+ if (!Number.isInteger(options.maxIters) || options.maxIters <= 0) {
12693
+ throw new Error(`Invalid maxIters: must be a positive integer, got ${options.maxIters}`);
12694
+ }
12695
+ if (!Number.isInteger(options.timeBudget) || options.timeBudget < 0) {
12696
+ throw new Error(`Invalid timeBudget: must be a non-negative integer, got ${options.timeBudget}`);
12697
+ }
12698
+ if (!MODEL_PATTERN.test(options.model)) {
12699
+ throw new Error(`Invalid model "${options.model}": must match ${MODEL_PATTERN}`);
12700
+ }
12701
+ if (options.topics) {
12702
+ for (const topic of options.topics) {
12703
+ if (!TOPIC_NAME_PATTERN.test(topic)) {
12704
+ throw new Error(`Invalid topic "${topic}": must match ${TOPIC_NAME_PATTERN}`);
12705
+ }
12706
+ }
12707
+ }
12708
+ }
12709
+ function generateImproveScript(options) {
12710
+ validateImproveOptions(options);
12711
+ const topicFilter = options.topics?.join(" ") ?? "";
12712
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
12713
+ return buildImproveScriptHeader(timestamp, options.maxIters, options.timeBudget, options.model, topicFilter) + buildTopicDiscovery() + buildImprovePrompt() + buildStreamExtractor() + buildImproveMarkerDetection() + buildImproveObservability() + buildImproveMainLoop({ maxIters: options.maxIters, timeBudget: options.timeBudget });
12714
+ }
12715
+ async function handleImprove(cmd, options) {
12716
+ const maxIters = Number(options.maxIters ?? 5);
12717
+ const timeBudget = Number(options.timeBudget ?? 0);
12718
+ const scriptOptions = {
12719
+ topics: options.topics,
12720
+ maxIters,
12721
+ timeBudget,
12722
+ model: options.model ?? DEFAULT_LOOP_MODEL
12723
+ };
12724
+ try {
12725
+ validateImproveOptions(scriptOptions);
12726
+ } catch (err) {
12727
+ out.error(err.message);
12728
+ process.exitCode = 1;
12729
+ return;
12730
+ }
12731
+ if (options.dryRun) {
12732
+ out.info("Dry run - improvement loop plan:");
12733
+ out.info(` Topics: ${scriptOptions.topics?.join(", ") ?? "(all improve/*.md)"}`);
12734
+ out.info(` Max iterations per topic: ${scriptOptions.maxIters}`);
12735
+ out.info(` Time budget: ${scriptOptions.timeBudget === 0 ? "unlimited" : `${scriptOptions.timeBudget}s`}`);
12736
+ out.info(` Model: ${scriptOptions.model}`);
12737
+ if (existsSync("improve")) {
12738
+ out.info(" improve/ directory exists");
12739
+ } else {
12740
+ out.warn(" improve/ directory not found - create it with improve/*.md files");
12741
+ }
12742
+ return;
12743
+ }
12744
+ const outputPath = resolve(options.output ?? "./improvement-loop.sh");
12745
+ if (existsSync(outputPath) && !options.force) {
12746
+ out.error(`File already exists: ${outputPath}`);
12747
+ out.info("Use --force to overwrite");
12748
+ process.exitCode = 1;
12749
+ return;
12750
+ }
12751
+ let script;
12752
+ try {
12753
+ script = generateImproveScript(scriptOptions);
12754
+ } catch (err) {
12755
+ out.error(err.message);
12756
+ process.exitCode = 1;
12757
+ return;
12758
+ }
12759
+ await mkdir(dirname(outputPath), { recursive: true });
12760
+ await writeFile(outputPath, script, "utf-8");
12761
+ await chmod(outputPath, 493);
12762
+ out.success(`Generated improvement loop script: ${outputPath}`);
12763
+ out.info("Run it with: " + outputPath);
12764
+ out.info("Preview with: IMPROVE_DRY_RUN=1 " + outputPath);
12765
+ }
12766
+ var EXAMPLE_PROGRAM = `# Linting
12767
+
12768
+ ## What to improve
12769
+ Find and fix lint violations in the codebase.
12770
+
12771
+ ## How to find work
12772
+ Run the project linter and look for violations:
12773
+ \`\`\`bash
12774
+ pnpm lint 2>&1 | head -50
12775
+ \`\`\`
12776
+
12777
+ ## How to validate
12778
+ After fixing, run the linter again and confirm fewer violations:
12779
+ \`\`\`bash
12780
+ pnpm lint
12781
+ pnpm test
12782
+ \`\`\`
12783
+ `;
12784
+ async function handleImproveInit(cmd) {
12785
+ const improveDir = "improve";
12786
+ const examplePath = resolve(improveDir, "example.md");
12787
+ if (existsSync(examplePath)) {
12788
+ out.info(`improve/ directory already exists with ${examplePath}`);
12789
+ out.info("Add more .md files to define additional improvement topics.");
12790
+ return;
12791
+ }
12792
+ await mkdir(improveDir, { recursive: true });
12793
+ await writeFile(examplePath, EXAMPLE_PROGRAM, "utf-8");
12794
+ out.success(`Created ${examplePath}`);
12795
+ out.info("Edit this file or add more .md files to define improvement topics.");
12796
+ out.info("Then run: ca improve");
12797
+ }
12798
+ function registerImproveCommands(program) {
12799
+ const improveCmd = program.command("improve").description("Generate improvement loop script").option("--topics <names...>", "Specific topics to process").option("-o, --output <path>", "Output script path", "./improvement-loop.sh").option("--max-iters <n>", "Max iterations per topic", "5").option("--time-budget <seconds>", "Total time budget, 0=unlimited", "0").option("--model <model>", "Claude model to use", DEFAULT_LOOP_MODEL).option("--force", "Overwrite existing script").option("--dry-run", "Validate and print plan without generating").action(async function(options) {
12800
+ await handleImprove(this, options);
12801
+ });
12802
+ improveCmd.command("init").description("Print guidance about creating improve/*.md files").action(async function() {
12803
+ await handleImproveInit();
12804
+ });
12286
12805
  }
12287
12806
 
12288
12807
  // src/commands/loop-review-templates.ts
@@ -12453,8 +12972,8 @@ spawn_reviewers() {
12453
12972
  case "$reviewer" in
12454
12973
  (claude-sonnet|claude-opus)
12455
12974
  local model_name
12456
- if [ "$reviewer" = "claude-sonnet" ]; then model_name="claude-sonnet-4-6"
12457
- else model_name="claude-opus-4-6"; fi
12975
+ if [ "$reviewer" = "claude-sonnet" ]; then model_name="claude-sonnet-4-6[1m]"
12976
+ else model_name="claude-opus-4-6[1m]"; fi
12458
12977
  local sid=""
12459
12978
  sid=$(read_session_id "$reviewer" "$REVIEW_DIR/sessions.json")
12460
12979
  if [ "$cycle" -eq 1 ]; then
@@ -12580,8 +13099,6 @@ run_review_phase() {
12580
13099
 
12581
13100
  // src/commands/loop.ts
12582
13101
  var LOOP_EPIC_ID_PATTERN = /^[a-zA-Z0-9_.-]+$/;
12583
- var DEFAULT_MODEL = "claude-opus-4-6";
12584
- var MODEL_PATTERN = /^[a-zA-Z0-9_.:/-]+$/;
12585
13102
  function buildScriptHeader(timestamp, maxRetries, model, epicIds) {
12586
13103
  return `#!/usr/bin/env bash
12587
13104
  # Infinity Loop - Generated by: ca loop
@@ -12675,6 +13192,14 @@ function validateOptions(options) {
12675
13192
  throw new Error(`Invalid maxReviewCycles: must be a positive integer, got ${options.maxReviewCycles}`);
12676
13193
  }
12677
13194
  }
13195
+ if (options.improve) {
13196
+ if (!Number.isInteger(options.improve.maxIters) || options.improve.maxIters <= 0) {
13197
+ throw new Error(`Invalid improve maxIters: must be a positive integer, got ${options.improve.maxIters}`);
13198
+ }
13199
+ if (!Number.isInteger(options.improve.timeBudget) || options.improve.timeBudget < 0) {
13200
+ throw new Error(`Invalid improve timeBudget: must be a non-negative integer, got ${options.improve.timeBudget}`);
13201
+ }
13202
+ }
12678
13203
  }
12679
13204
  function generateLoopScript(options) {
12680
13205
  validateOptions(options);
@@ -12687,7 +13212,7 @@ function generateLoopScript(options) {
12687
13212
  reviewers: options.reviewers,
12688
13213
  maxReviewCycles: options.maxReviewCycles ?? 3,
12689
13214
  reviewBlocking: options.reviewBlocking ?? false,
12690
- reviewModel: options.reviewModel ?? DEFAULT_MODEL,
13215
+ reviewModel: options.reviewModel ?? DEFAULT_LOOP_MODEL,
12691
13216
  reviewEvery: options.reviewEvery ?? 0
12692
13217
  });
12693
13218
  script += buildReviewerDetection();
@@ -12697,7 +13222,24 @@ function generateLoopScript(options) {
12697
13222
  script += buildImplementerPhase();
12698
13223
  script += buildReviewLoop();
12699
13224
  }
12700
- script += buildMainLoop(hasReview ? { hasReview: true, reviewEvery: options.reviewEvery ?? 0 } : void 0);
13225
+ const hasImprove = !!options.improve;
13226
+ script += buildMainLoop(
13227
+ hasReview ? { hasReview: true, reviewEvery: options.reviewEvery ?? 0 } : void 0,
13228
+ hasImprove
13229
+ // skipExit when improvement phase follows
13230
+ );
13231
+ if (options.improve) {
13232
+ script += "\n# Improvement phase (runs after epic loop completes successfully)\n";
13233
+ script += "if [ $FAILED -eq 0 ]; then\n";
13234
+ script += ' log "Epic loop completed successfully, starting improvement phase"\n';
13235
+ script += buildTopicDiscovery();
13236
+ script += buildImprovePrompt();
13237
+ script += buildImproveMarkerDetection();
13238
+ script += buildImproveObservability();
13239
+ script += buildImproveMainLoop({ ...options.improve, embedded: true });
13240
+ script += "\nfi\n";
13241
+ script += '[ $FAILED -eq 0 ] && [ "${IMPROVE_RESULT:-0}" -eq 0 ] && exit 0 || exit 1\n';
13242
+ }
12701
13243
  return script;
12702
13244
  }
12703
13245
  async function handleLoop(cmd, options) {
@@ -12716,17 +13258,20 @@ async function handleLoop(cmd, options) {
12716
13258
  }
12717
13259
  const reviewEvery = Number(options.reviewEvery ?? 0);
12718
13260
  const maxReviewCycles = Number(options.maxReviewCycles ?? 3);
13261
+ const improveMaxIters = Number(options.improveMaxIters ?? 5);
13262
+ const improveTimeBudget = Number(options.improveTimeBudget ?? 0);
12719
13263
  let script;
12720
13264
  try {
12721
13265
  script = generateLoopScript({
12722
13266
  epics: options.epics,
12723
13267
  maxRetries,
12724
- model: options.model ?? DEFAULT_MODEL,
13268
+ model: options.model ?? DEFAULT_LOOP_MODEL,
12725
13269
  reviewers: options.reviewers,
12726
13270
  reviewEvery,
12727
13271
  maxReviewCycles,
12728
13272
  reviewBlocking: options.reviewBlocking,
12729
- reviewModel: options.reviewModel ?? DEFAULT_MODEL
13273
+ reviewModel: options.reviewModel ?? DEFAULT_LOOP_MODEL,
13274
+ improve: options.improve ? { maxIters: improveMaxIters, timeBudget: improveTimeBudget } : void 0
12730
13275
  });
12731
13276
  } catch (err) {
12732
13277
  out.error(err.message);
@@ -12741,7 +13286,7 @@ async function handleLoop(cmd, options) {
12741
13286
  out.info("Preview with: LOOP_DRY_RUN=1 " + outputPath);
12742
13287
  }
12743
13288
  function registerLoopCommands(program) {
12744
- program.command("loop").description("Generate infinity loop script for epic tasks").option("--epics <ids...>", "Specific epic IDs to process").option("-o, --output <path>", "Output script path", "./infinity-loop.sh").option("--max-retries <n>", "Max retries per epic on failure", "1").option("--model <model>", "Claude model to use", DEFAULT_MODEL).option("--force", "Overwrite existing script").option("--review-every <n>", "Review every N completed epics (0=end-only)", "0").option("--reviewers <names...>", "Reviewers to use (claude-sonnet claude-opus gemini codex)").option("--max-review-cycles <n>", "Max review/fix iterations", "3").option("--review-blocking", "Fail loop if review not approved after max cycles").option("--review-model <model>", "Model for implementer fix sessions", DEFAULT_MODEL).action(async function(options) {
13289
+ program.command("loop").description("Generate infinity loop script for epic tasks").option("--epics <ids...>", "Specific epic IDs to process").option("-o, --output <path>", "Output script path", "./infinity-loop.sh").option("--max-retries <n>", "Max retries per epic on failure", "1").option("--model <model>", "Claude model to use", DEFAULT_LOOP_MODEL).option("--force", "Overwrite existing script").option("--review-every <n>", "Review every N completed epics (0=end-only)", "0").option("--reviewers <names...>", "Reviewers to use (claude-sonnet claude-opus gemini codex)").option("--max-review-cycles <n>", "Max review/fix iterations", "3").option("--review-blocking", "Fail loop if review not approved after max cycles").option("--review-model <model>", "Model for implementer fix sessions", DEFAULT_LOOP_MODEL).option("--improve", "Run improvement phase after all epics complete").option("--improve-max-iters <n>", "Max improvement iterations per topic", "5").option("--improve-time-budget <seconds>", "Total improvement time budget, 0=unlimited", "0").action(async function(options) {
12745
13290
  await handleLoop(this, options);
12746
13291
  });
12747
13292
  }
@@ -12798,7 +13343,7 @@ function formatStreamEvent(event) {
12798
13343
  }
12799
13344
  case "result": {
12800
13345
  const text = typeof event.result === "string" ? event.result : "";
12801
- const markers = ["EPIC_COMPLETE", "EPIC_FAILED", "HUMAN_REQUIRED"];
13346
+ const markers = ["EPIC_COMPLETE", "EPIC_FAILED", "HUMAN_REQUIRED", "NO_IMPROVEMENT", "IMPROVED", "FAILED"];
12802
13347
  const found = markers.find((m) => text.includes(m));
12803
13348
  if (found) {
12804
13349
  const markerLine = text.split("\n").find((l) => l.includes(found)) ?? found;
@@ -12811,19 +13356,19 @@ function formatStreamEvent(event) {
12811
13356
  return null;
12812
13357
  }
12813
13358
  }
12814
- function findLatestTraceFile(logDir) {
13359
+ function findLatestTraceFile(logDir, prefix = "trace_") {
12815
13360
  if (!existsSync(logDir)) return null;
12816
13361
  const latestPath = join(logDir, ".latest");
12817
13362
  if (existsSync(latestPath)) {
12818
13363
  try {
12819
13364
  const target = readlinkSync(latestPath);
12820
13365
  const resolved = resolve(logDir, target);
12821
- if (existsSync(resolved)) return resolved;
13366
+ if (existsSync(resolved) && target.startsWith(prefix)) return resolved;
12822
13367
  } catch {
12823
13368
  }
12824
13369
  }
12825
13370
  try {
12826
- const files = readdirSync(logDir).filter((f) => f.startsWith("trace_") && f.endsWith(".jsonl")).sort().reverse();
13371
+ const files = readdirSync(logDir).filter((f) => f.startsWith(prefix) && f.endsWith(".jsonl")).sort().reverse();
12827
13372
  const first = files[0];
12828
13373
  if (first) return join(logDir, first);
12829
13374
  } catch {
@@ -12888,7 +13433,7 @@ async function handleWatch(cmd, options) {
12888
13433
  }
12889
13434
  if (existsSync(logDir)) {
12890
13435
  try {
12891
- const files = readdirSync(logDir).filter((f) => f.startsWith(`trace_${options.epic}`) && f.endsWith(".jsonl")).sort().reverse();
13436
+ const files = readdirSync(logDir).filter((f) => f.startsWith(`trace_${options.epic}-`) && f.endsWith(".jsonl")).sort().reverse();
12892
13437
  const first = files[0];
12893
13438
  if (first) traceFile = join(logDir, first);
12894
13439
  } catch {
@@ -12899,6 +13444,13 @@ async function handleWatch(cmd, options) {
12899
13444
  process.exitCode = 1;
12900
13445
  return;
12901
13446
  }
13447
+ } else if (options.improve) {
13448
+ traceFile = findLatestTraceFile(logDir, "trace_improve_");
13449
+ if (!traceFile) {
13450
+ out.info("No improvement trace found. Run `ca improve` to generate an improvement loop script first.");
13451
+ process.exitCode = 0;
13452
+ return;
13453
+ }
12902
13454
  } else {
12903
13455
  traceFile = findLatestTraceFile(logDir);
12904
13456
  if (!traceFile) {
@@ -12911,7 +13463,7 @@ async function handleWatch(cmd, options) {
12911
13463
  await tailFile(traceFile, follow);
12912
13464
  }
12913
13465
  function registerWatchCommand(program) {
12914
- program.command("watch").description("Tail and pretty-print live trace from infinity loop sessions").option("--epic <id>", "Watch a specific epic trace").option("--no-follow", "Print existing trace and exit (no live tail)").action(async function(options) {
13466
+ program.command("watch").description("Tail and pretty-print live trace from infinity loop sessions").option("--epic <id>", "Watch a specific epic trace").option("--improve", "Watch improvement loop traces").option("--no-follow", "Print existing trace and exit (no live tail)").action(async function(options) {
12915
13467
  await handleWatch(this, options);
12916
13468
  });
12917
13469
  }
@@ -13051,6 +13603,7 @@ function createProgram() {
13051
13603
  registerManagementCommands(program);
13052
13604
  registerSetupCommands(program);
13053
13605
  registerCompoundCommands(program);
13606
+ registerImproveCommands(program);
13054
13607
  registerLoopCommands(program);
13055
13608
  registerWatchCommand(program);
13056
13609
  registerPhaseCheckCommand(program);
@@ -13072,7 +13625,7 @@ function createProgram() {
13072
13625
  }
13073
13626
  async function runProgram(program, argv = process.argv) {
13074
13627
  let updatePromise = null;
13075
- if (process.stdout.isTTY) {
13628
+ if (shouldCheckForUpdate()) {
13076
13629
  try {
13077
13630
  const cacheDir = join(getRepoRoot(), ".claude", ".cache");
13078
13631
  updatePromise = checkForUpdate(cacheDir);