compound-agent 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.2.0] - 2026-02-15
13
+
14
+ ### Added
15
+
16
+ - **`ca loop` command**: Generate autonomous infinity loop scripts that process beads epics end-to-end via chained Claude Code sessions
17
+ - **HUMAN_REQUIRED marker**: Loop detects human-blocking issues, logs reason to beads, skips epic without stopping the loop
18
+ - **Review+compound blocking tasks**: Plan phase now creates review and compound beads issues with dependencies, ensuring these phases survive context compaction and surface via `bd ready`
19
+
20
+ ### Fixed
21
+
22
+ - **Loop script `set -u` crash**: `LOOP_DRY_RUN` now uses safe expansion (`${VAR:-}`) for `set -u` compatibility
23
+ - **Infinite reprocessing**: Loop tracks processed epics to prevent re-selecting the same epic in dry-run or human-required paths
24
+ - **Input validation**: `--max-retries` rejects non-integer values; epic IDs validated against safe pattern to prevent shell injection
25
+ - **Exit codes**: `ca loop` now returns non-zero on errors (overwrite refusal, invalid options)
26
+
12
27
  ## [1.1.0] - 2026-02-15
13
28
 
14
29
  ### Added
@@ -423,7 +438,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
423
438
  - Vitest test suite
424
439
  - tsup build configuration
425
440
 
426
- [Unreleased]: https://github.com/Nathandela/learning_agent/compare/v1.1.0...HEAD
441
+ [Unreleased]: https://github.com/Nathandela/learning_agent/compare/v1.2.0...HEAD
442
+ [1.2.0]: https://github.com/Nathandela/learning_agent/compare/v1.1.0...v1.2.0
427
443
  [1.1.0]: https://github.com/Nathandela/learning_agent/compare/v1.0.0...v1.1.0
428
444
  [1.0.0]: https://github.com/Nathandela/learning_agent/compare/v0.2.9...v1.0.0
429
445
  [0.2.9]: https://github.com/Nathandela/learning_agent/compare/v0.2.8...v0.2.9
package/README.md CHANGED
@@ -170,6 +170,18 @@ The CLI binary is `ca` (alias: `compound-agent`).
170
170
  | `ca rules check` | Run repository-defined rule checks |
171
171
  | `ca test-summary` | Run tests and output a compact summary |
172
172
 
173
+ ### Automation
174
+
175
+ | Command | Description |
176
+ |---------|-------------|
177
+ | `ca loop` | Generate infinity loop script for autonomous epic processing |
178
+ | `ca loop --epics <ids...>` | Target specific epic IDs |
179
+ | `ca loop -o <path>` | Custom output path (default: `./infinity-loop.sh`) |
180
+ | `ca loop --max-retries <n>` | Max retries per epic on failure (default: 1) |
181
+ | `ca loop --force` | Overwrite existing script |
182
+
183
+ Generated scripts detect three markers: `EPIC_COMPLETE` (success), `EPIC_FAILED` (retry then stop), `HUMAN_REQUIRED: <reason>` (skip and continue). Run with `LOOP_DRY_RUN=1` to preview.
184
+
173
185
  ### Setup
174
186
 
175
187
  | Command | Description |
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@ import { Command } from 'commander';
3
3
  import { getLlama, resolveModelFile } from 'node-llama-cpp';
4
4
  import { mkdirSync, writeFileSync, statSync, existsSync, readFileSync, unlinkSync, chmodSync, readdirSync } from 'fs';
5
5
  import { homedir } from 'os';
6
- import { join, dirname, relative } from 'path';
6
+ import { join, dirname, resolve, relative } from 'path';
7
7
  import * as fs from 'fs/promises';
8
- import { readFile, mkdir, appendFile, rm, writeFile, rename } from 'fs/promises';
8
+ import { readFile, mkdir, appendFile, writeFile, chmod, rm, rename } from 'fs/promises';
9
9
  import { createHash } from 'crypto';
10
10
  import { z } from 'zod';
11
11
  import { createRequire } from 'module';
@@ -2971,7 +2971,15 @@ Create a structured implementation plan enriched by semantic memory and existing
2971
2971
  bd create --title="<task>" --type=task --priority=<1-4>
2972
2972
  bd dep add <dependent-task> <blocking-task>
2973
2973
  \`\`\`
2974
- 9. Output the plan as a structured list with task IDs and dependency graph.
2974
+ 9. **Create review and compound blocking tasks** so they survive compaction:
2975
+ \`\`\`bash
2976
+ bd create --title="Review: /compound:review" --type=task --priority=1
2977
+ bd create --title="Compound: /compound:compound" --type=task --priority=1
2978
+ bd dep add <review-id> <last-work-task> # review depends on work
2979
+ bd dep add <compound-id> <review-id> # compound depends on review
2980
+ \`\`\`
2981
+ These tasks surface via \`bd ready\` after work completes, ensuring review and compound phases are never skipped \u2014 even after context compaction.
2982
+ 10. Output the plan as a structured list with task IDs and dependency graph.
2975
2983
 
2976
2984
  ## Memory Integration
2977
2985
  - Call \`memory_search\` before planning to learn from past approaches.
@@ -3168,6 +3176,7 @@ Chain all phases: brainstorm, plan, work, review, compound. End-to-end delivery.
3168
3176
  - \`TeamCreate\` team "plan-<slug>", spawn docs-analyst + repo-analyst + memory-analyst as parallel teammates.
3169
3177
  - Break into tasks with dependencies and acceptance criteria.
3170
3178
  - Create beads issues with \`bd create\` and map dependencies with \`bd dep add\`.
3179
+ - Create review and compound blocking tasks (\`bd create\` + \`bd dep add\`) so they survive compaction and surface via \`bd ready\` after work completes.
3171
3180
  - Shut down plan team before next phase.
3172
3181
 
3173
3182
  3. **Work phase**: Implement with adaptive TDD.
@@ -3348,6 +3357,7 @@ Create a concrete implementation plan by decomposing work into small, testable t
3348
3357
  7. Define acceptance criteria for each task
3349
3358
  8. Map dependencies between tasks
3350
3359
  9. Create beads issues: \`bd create --title="..." --type=task\`
3360
+ 10. Create review and compound blocking tasks (\`bd create\` + \`bd dep add\`) that depend on work tasks \u2014 these survive compaction and surface via \`bd ready\` after work completes
3351
3361
 
3352
3362
  ## Memory Integration
3353
3363
  - Call \`memory_search\` for patterns related to the feature area
@@ -5612,6 +5622,269 @@ function registerCaptureCommands(program2) {
5612
5622
  await handleCapture(this, options);
5613
5623
  });
5614
5624
  }
5625
+ var EPIC_ID_PATTERN = /^[a-zA-Z0-9_.-]+$/;
5626
+ function buildScriptHeader(timestamp, maxRetries, model, epicIds) {
5627
+ return `#!/usr/bin/env bash
5628
+ # Infinity Loop - Generated by: ca loop
5629
+ # Date: ${timestamp}
5630
+ # Autonomously processes beads epics via Claude Code sessions.
5631
+ #
5632
+ # Usage:
5633
+ # ./infinity-loop.sh
5634
+ # LOOP_DRY_RUN=1 ./infinity-loop.sh # Preview without executing
5635
+
5636
+ set -euo pipefail
5637
+
5638
+ # Config
5639
+ MAX_RETRIES=${maxRetries}
5640
+ MODEL="${model}"
5641
+ EPIC_IDS="${epicIds}"
5642
+ LOG_DIR="agent_logs"
5643
+
5644
+ # Helpers
5645
+ timestamp() { date '+%Y-%m-%d_%H-%M-%S'; }
5646
+ log() { echo "[$(timestamp)] $*"; }
5647
+ die() { log "FATAL: $*"; exit 1; }
5648
+
5649
+ command -v python3 >/dev/null || die "python3 required for JSON parsing"
5650
+ command -v claude >/dev/null || die "claude CLI required"
5651
+ command -v bd >/dev/null || die "bd (beads) CLI required"
5652
+
5653
+ mkdir -p "$LOG_DIR"
5654
+ ` + buildEpicSelector() + buildPromptFunction();
5655
+ }
5656
+ function buildEpicSelector() {
5657
+ return `
5658
+ get_next_epic() {
5659
+ if [ -n "$EPIC_IDS" ]; then
5660
+ # From explicit list, find first still-open epic not yet processed
5661
+ for epic_id in $EPIC_IDS; do
5662
+ case " $PROCESSED " in *" $epic_id "*) continue ;; esac
5663
+ local status
5664
+ status=$(bd show "$epic_id" --json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status',''))" 2>/dev/null || echo "")
5665
+ if [ "$status" = "open" ]; then
5666
+ echo "$epic_id"
5667
+ return 0
5668
+ fi
5669
+ done
5670
+ return 1
5671
+ else
5672
+ # Dynamic: get next ready epic from dependency graph, filtering processed
5673
+ local epic_id
5674
+ epic_id=$(bd list --type=epic --ready --json --limit=10 2>/dev/null | python3 -c "
5675
+ import sys,json
5676
+ processed = set('$PROCESSED'.split())
5677
+ items = json.load(sys.stdin)
5678
+ for item in items:
5679
+ if item['id'] not in processed:
5680
+ print(item['id'])
5681
+ break" 2>/dev/null || echo "")
5682
+ if [ -z "$epic_id" ]; then
5683
+ return 1
5684
+ fi
5685
+ echo "$epic_id"
5686
+ return 0
5687
+ fi
5688
+ }
5689
+ `;
5690
+ }
5691
+ function buildPromptFunction() {
5692
+ return `
5693
+ build_prompt() {
5694
+ local epic_id="$1"
5695
+ cat <<'PROMPT_HEADER'
5696
+ You are running in an autonomous infinity loop. Your task is to fully implement a beads epic.
5697
+
5698
+ ## Step 1: Load context
5699
+ Run these commands to prime your session:
5700
+ PROMPT_HEADER
5701
+ cat <<PROMPT_BODY
5702
+ \\\`\\\`\\\`bash
5703
+ npx ca load-session
5704
+ bd show $epic_id
5705
+ \\\`\\\`\\\`
5706
+
5707
+ Read the epic details carefully. Understand scope, acceptance criteria, and sub-tasks.
5708
+
5709
+ ## Step 2: Execute the workflow
5710
+ Run the full compound workflow for this epic, starting from the plan phase
5711
+ (brainstorm is already done -- the epic exists):
5712
+
5713
+ /compound:lfg from plan -- Epic: $epic_id
5714
+
5715
+ Work through all phases: plan, work, review, compound.
5716
+
5717
+ ## Step 3: On completion
5718
+ When all work is done and tests pass:
5719
+ 1. Close the epic: \`bd close $epic_id\`
5720
+ 2. Sync beads: \`bd sync\`
5721
+ 3. Commit and push all changes
5722
+ 4. Output this exact marker on its own line:
5723
+
5724
+ EPIC_COMPLETE
5725
+
5726
+ ## Step 4: On failure
5727
+ If you cannot complete the epic after reasonable effort:
5728
+ 1. Add a note: \`bd update $epic_id --notes "Loop failed: <reason>"\`
5729
+ 2. Output this exact marker on its own line:
5730
+
5731
+ EPIC_FAILED
5732
+
5733
+ ## Step 5: On human required
5734
+ If you hit a blocker that REQUIRES human action (account creation, API keys,
5735
+ external service setup, design decisions you cannot make, etc.):
5736
+ 1. Add a note: \`bd update $epic_id --notes "Human required: <reason>"\`
5737
+ 2. Output this exact marker followed by a short reason on the SAME line:
5738
+
5739
+ HUMAN_REQUIRED: <reason>
5740
+
5741
+ Example: HUMAN_REQUIRED: Need AWS credentials configured in .env
5742
+
5743
+ ## Rules
5744
+ - Do NOT ask questions -- there is no human. Make reasonable decisions.
5745
+ - Do NOT stop early -- complete the full workflow.
5746
+ - If tests fail, fix them. Retry up to 3 times before declaring failure.
5747
+ - Use HUMAN_REQUIRED only for true blockers that no amount of retrying can solve.
5748
+ - Commit incrementally as you make progress.
5749
+ PROMPT_BODY
5750
+ }`;
5751
+ }
5752
+ function buildMainLoop() {
5753
+ return `
5754
+ # Main loop
5755
+ COMPLETED=0
5756
+ FAILED=0
5757
+ SKIPPED=0
5758
+ PROCESSED=""
5759
+
5760
+ log "Infinity loop starting"
5761
+ log "Config: max_retries=$MAX_RETRIES model=$MODEL"
5762
+ [ -n "$EPIC_IDS" ] && log "Targeting epics: $EPIC_IDS" || log "Targeting: all ready epics"
5763
+
5764
+ while true; do
5765
+ EPIC_ID=$(get_next_epic) || break
5766
+
5767
+ log "Processing epic: $EPIC_ID"
5768
+
5769
+ ATTEMPT=0
5770
+ SUCCESS=false
5771
+
5772
+ while [ $ATTEMPT -le $MAX_RETRIES ]; do
5773
+ ATTEMPT=$((ATTEMPT + 1))
5774
+ LOGFILE="$LOG_DIR/loop_$EPIC_ID-$(timestamp).log"
5775
+
5776
+ log "Attempt $ATTEMPT/$((MAX_RETRIES + 1)) for $EPIC_ID (log: $LOGFILE)"
5777
+
5778
+ if [ -n "\${LOOP_DRY_RUN:-}" ]; then
5779
+ log "[DRY RUN] Would run claude session for $EPIC_ID"
5780
+ SUCCESS=true
5781
+ break
5782
+ fi
5783
+
5784
+ PROMPT=$(build_prompt "$EPIC_ID")
5785
+
5786
+ claude --dangerously-skip-permissions \\
5787
+ --model "$MODEL" \\
5788
+ -p "$PROMPT" \\
5789
+ &> "$LOGFILE" || true
5790
+
5791
+ if grep -q "EPIC_COMPLETE" "$LOGFILE"; then
5792
+ log "Epic $EPIC_ID completed successfully"
5793
+ SUCCESS=true
5794
+ break
5795
+ elif grep -q "HUMAN_REQUIRED" "$LOGFILE"; then
5796
+ REASON=$(grep "HUMAN_REQUIRED:" "$LOGFILE" | head -1 | sed 's/.*HUMAN_REQUIRED: *//')
5797
+ log "Epic $EPIC_ID needs human action: $REASON"
5798
+ bd update "$EPIC_ID" --notes "Human required: $REASON" 2>/dev/null || true
5799
+ SKIPPED=$((SKIPPED + 1))
5800
+ SUCCESS=skip
5801
+ break
5802
+ elif grep -q "EPIC_FAILED" "$LOGFILE"; then
5803
+ log "Epic $EPIC_ID reported failure (attempt $ATTEMPT)"
5804
+ else
5805
+ log "Epic $EPIC_ID session ended without marker (attempt $ATTEMPT)"
5806
+ fi
5807
+
5808
+ if [ $ATTEMPT -le $MAX_RETRIES ]; then
5809
+ log "Retrying $EPIC_ID..."
5810
+ sleep 5
5811
+ fi
5812
+ done
5813
+
5814
+ if [ "$SUCCESS" = true ]; then
5815
+ COMPLETED=$((COMPLETED + 1))
5816
+ log "Epic $EPIC_ID done. Completed so far: $COMPLETED"
5817
+ elif [ "$SUCCESS" = skip ]; then
5818
+ log "Epic $EPIC_ID skipped (human required). Continuing."
5819
+ else
5820
+ FAILED=$((FAILED + 1))
5821
+ log "Epic $EPIC_ID failed after $((MAX_RETRIES + 1)) attempts. Stopping loop."
5822
+ PROCESSED="$PROCESSED $EPIC_ID"
5823
+ break
5824
+ fi
5825
+
5826
+ PROCESSED="$PROCESSED $EPIC_ID"
5827
+ done
5828
+
5829
+ log "Loop finished. Completed: $COMPLETED, Failed: $FAILED, Skipped: $SKIPPED"
5830
+ [ $FAILED -eq 0 ] && exit 0 || exit 1`;
5831
+ }
5832
+ function validateOptions(options) {
5833
+ if (!Number.isInteger(options.maxRetries) || options.maxRetries < 0) {
5834
+ throw new Error(`Invalid maxRetries: must be a non-negative integer, got ${options.maxRetries}`);
5835
+ }
5836
+ if (options.epics) {
5837
+ for (const id of options.epics) {
5838
+ if (!EPIC_ID_PATTERN.test(id)) {
5839
+ throw new Error(`Invalid epic ID "${id}": must match ${EPIC_ID_PATTERN}`);
5840
+ }
5841
+ }
5842
+ }
5843
+ }
5844
+ function generateLoopScript(options) {
5845
+ validateOptions(options);
5846
+ const epicIds = options.epics?.join(" ") ?? "";
5847
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5848
+ return buildScriptHeader(timestamp, options.maxRetries, options.model, epicIds) + buildMainLoop();
5849
+ }
5850
+ async function handleLoop(cmd, options) {
5851
+ const outputPath = resolve(options.output ?? "./infinity-loop.sh");
5852
+ if (existsSync(outputPath) && !options.force) {
5853
+ out.error(`File already exists: ${outputPath}`);
5854
+ out.info("Use --force to overwrite");
5855
+ process.exitCode = 1;
5856
+ return;
5857
+ }
5858
+ const maxRetries = Number(options.maxRetries ?? 1);
5859
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
5860
+ out.error(`Invalid --max-retries: must be a non-negative integer, got "${options.maxRetries}"`);
5861
+ process.exitCode = 1;
5862
+ return;
5863
+ }
5864
+ let script;
5865
+ try {
5866
+ script = generateLoopScript({
5867
+ epics: options.epics,
5868
+ maxRetries,
5869
+ model: options.model ?? "claude-opus-4-6"
5870
+ });
5871
+ } catch (err) {
5872
+ out.error(err.message);
5873
+ process.exitCode = 1;
5874
+ return;
5875
+ }
5876
+ await mkdir(dirname(outputPath), { recursive: true });
5877
+ await writeFile(outputPath, script, "utf-8");
5878
+ await chmod(outputPath, 493);
5879
+ out.success(`Generated infinity loop script: ${outputPath}`);
5880
+ out.info("Run it with: " + outputPath);
5881
+ out.info("Preview with: LOOP_DRY_RUN=1 " + outputPath);
5882
+ }
5883
+ function registerLoopCommands(program2) {
5884
+ program2.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", "claude-opus-4-6").option("--force", "Overwrite existing script").action(async function(options) {
5885
+ await handleLoop(this, options);
5886
+ });
5887
+ }
5615
5888
  function parseLimitOrExit(rawLimit, optionName, commandName) {
5616
5889
  try {
5617
5890
  return parseLimit(rawLimit, optionName);
@@ -5897,6 +6170,7 @@ registerRetrievalCommands(program);
5897
6170
  registerManagementCommands(program);
5898
6171
  registerSetupCommands(program);
5899
6172
  registerCompoundCommands(program);
6173
+ registerLoopCommands(program);
5900
6174
  program.parse();
5901
6175
  //# sourceMappingURL=cli.js.map
5902
6176
  //# sourceMappingURL=cli.js.map