compound-agent 1.8.0 → 2.0.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +30 -47
  3. package/bin/ca +32 -0
  4. package/package.json +19 -78
  5. package/scripts/postinstall.cjs +221 -0
  6. package/dist/cli.d.ts +0 -1
  7. package/dist/cli.js +0 -13655
  8. package/dist/cli.js.map +0 -1
  9. package/dist/index.d.ts +0 -3730
  10. package/dist/index.js +0 -3251
  11. package/dist/index.js.map +0 -1
  12. package/docs/research/AgenticAiCodebaseGuide.md +0 -1206
  13. package/docs/research/BuildingACCompilerAnthropic.md +0 -116
  14. package/docs/research/HarnessEngineeringOpenAi.md +0 -220
  15. package/docs/research/code-review/systematic-review-methodology.md +0 -409
  16. package/docs/research/index.md +0 -76
  17. package/docs/research/learning-systems/knowledge-compounding-for-agents.md +0 -695
  18. package/docs/research/property-testing/property-based-testing-and-invariants.md +0 -742
  19. package/docs/research/scenario-testing/advanced-and-emerging.md +0 -470
  20. package/docs/research/scenario-testing/core-foundations.md +0 -507
  21. package/docs/research/scenario-testing/domain-specific-and-human-factors.md +0 -474
  22. package/docs/research/security/auth-patterns.md +0 -138
  23. package/docs/research/security/data-exposure.md +0 -185
  24. package/docs/research/security/dependency-security.md +0 -91
  25. package/docs/research/security/injection-patterns.md +0 -249
  26. package/docs/research/security/overview.md +0 -81
  27. package/docs/research/security/secrets-checklist.md +0 -92
  28. package/docs/research/security/secure-coding-failure.md +0 -297
  29. package/docs/research/software_architecture/01-science-of-decomposition.md +0 -615
  30. package/docs/research/software_architecture/02-architecture-under-uncertainty.md +0 -649
  31. package/docs/research/software_architecture/03-emergent-behavior-in-composed-systems.md +0 -644
  32. package/docs/research/spec_design/decision_theory_specifications_and_multi_criteria_tradeoffs.md +0 -0
  33. package/docs/research/spec_design/design_by_contract.md +0 -251
  34. package/docs/research/spec_design/domain_driven_design_strategic_modeling.md +0 -183
  35. package/docs/research/spec_design/formal_specification_methods.md +0 -161
  36. package/docs/research/spec_design/logic_and_proof_theory_under_the_curry_howard_correspondence.md +0 -250
  37. package/docs/research/spec_design/natural_language_formal_semantics_abuguity_in_specifications.md +0 -259
  38. package/docs/research/spec_design/requirements_engineering.md +0 -234
  39. package/docs/research/spec_design/systems_engineering_specifications_emergent_behavior_interface_contracts.md +0 -149
  40. package/docs/research/spec_design/what_is_this_about.md +0 -305
  41. package/docs/research/tdd/test-driven-development-methodology.md +0 -547
  42. package/docs/research/test-optimization-strategies.md +0 -401
  43. package/scripts/postinstall.mjs +0 -102
package/CHANGELOG.md CHANGED
@@ -7,6 +7,35 @@ All notable changes to this project will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ ## [Unreleased]
11
+
12
+ ### Changed
13
+
14
+ - **Replace node-llama-cpp with Transformers.js**: Swap EmbeddingGemma-300M (node-llama-cpp, 431MB RSS) for nomic-embed-text-v1.5 (@huggingface/transformers, 23MB RSS) — 95% memory reduction (E5b).
15
+ - **Remove all node-llama-cpp residue**: Update setup templates, doctor diagnostics, comments, and vitest config to reference Transformers.js and onnxruntime-node instead of node-llama-cpp (E5c).
16
+ - **Gemini adapter is now opt-in**: `installGeminiAdapter()` no longer runs automatically during setup. Users enable it explicitly via `npx ca setup gemini` (sets `gemini: true` in `compound-agent.json`). Use `npx ca setup gemini --disable` / `cleanGeminiCompoundFiles()` for clean removal.
17
+ - **Stale cleanup refactored**: Removed hardcoded deprecation lists from upgrade logic, replaced with `cleanStaleArtifacts()` pattern that declaratively defines what to remove.
18
+
19
+ ### Added
20
+
21
+ - **Research-specialist shipped agent**: New general-purpose research subagent (`research-specialist.md`) shipped via `npx ca init`. Has full tool access (Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch) so it can conduct deep PhD-level research, write survey papers, run experiments, and validate claims with code. Referenced by the `get-a-phd` workflow for parallel research execution.
22
+ - **`model-info.ts` module**: Extracted embedding model metadata (name, repo, dimensions, file) into a standalone module with zero native imports, decoupling the import graph so that CLI entry points no longer transitively load `node-llama-cpp` or `better-sqlite3` at parse time.
23
+ - **Architect decomposition spec**: Added specification for embedding memory pressure remediation (`embedding-memory-pressure-remediation.md`).
24
+ - **Hypothesis validation protocol**: Added to spec-dev skill — specs can now define falsifiable hypotheses with validation criteria.
25
+ - **`cleanStaleArtifacts` and `cleanStaleGeminiArtifacts`**: New setup utilities that remove deprecated files and directories during upgrades instead of relying on hardcoded deprecation lists.
26
+ - **LinkedIn architecture diagrams**: Integrated visual architecture diagrams into README (`docs/assets/`).
27
+ - **Independent reviews**: Added Opus and Sonnet independent review documents for embedding memory pressure analysis.
28
+ - **Embedding memory pressure investigation**: Added root-cause analysis, measurement data, and proposal documents in `docs/research/`.
29
+
30
+ ### Fixed
31
+
32
+ - **Embedding memory pressure remediation**: Lazy-load native modules (@huggingface/transformers (onnxruntime-node), better-sqlite3) behind dynamic `import()`, reducing CLI cold-start RSS. Singleton embedding model uses explicit `dispose()`. Added RSS measurement script and integration tests for memory lifecycle.
33
+ - **Review phase resilience**: Fixed jq stdin pipe handling, added auth health checks, and improved error isolation in loop review templates.
34
+ - **Quality-filter-before-storage test ordering**: Resolved flaky test ordering in compound skill tests.
35
+ - **Merged worktree review findings**: Addressed Opus/Sonnet review findings for worktree merges (loop-review-templates, stale-cleanup tests).
36
+ - **Knowledge index integration tests**: Fixed test configuration for embedding integration tests in vitest workspace.
37
+
38
+
10
39
  ## [1.8.0] - 2026-03-15
11
40
 
12
41
  ### Added
@@ -264,7 +293,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
264
293
  - **Eliminate double model initialization**: `ca search` now uses `isModelAvailable()` (fs.existsSync, zero cost) instead of `isModelUsable()` which loaded the 278MB native model just to probe availability, then loaded it again for actual embedding
265
294
  - **Bulk-read cached embeddings**: `getCachedEmbeddingsBulk()` replaces N individual `getCachedEmbedding()` SQLite queries with a single bulk read
266
295
  - **Eliminate redundant JSONL parsing**: `searchVector()` and `findSimilarLessons()` now use `readAllFromSqlite()` after `syncIfNeeded()` instead of re-parsing the JSONL file
267
- - **Float32Array consistency**: Lesson embedding path now keeps `Float32Array` from node-llama-cpp instead of converting via `Array.from()` (4x memory savings per vector)
296
+ - **Float32Array consistency**: Lesson embedding path now keeps `Float32Array` from the embedding pipeline instead of converting via `Array.from()` (4x memory savings per vector)
268
297
  - **Pre-warm lesson embedding cache**: `ca init` now pre-computes embeddings for all lessons with missing or stale cache entries, eliminating cold-start latency on first search
269
298
  - **Graceful embedding fallback**: `ca search` falls back to keyword-only search on runtime embedding failures instead of crashing
270
299
 
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
  [![license](https://img.shields.io/npm/l/compound-agent)](LICENSE)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue)](https://www.typescriptlang.org/)
8
8
 
9
+ <p align="center">
10
+ <img src="docs/assets/diagram-4.png" alt="Compound-agent ecosystem overview: Architect phase decomposes work via Socratic dialogue into a dependency graph. ca loop chains tasks with cross-model review, retry, and fresh sessions. Scenario evaluation validates changes with iterative refinement. All backed by persistent memory (lessons + knowledge across all sessions) and verification gates (tests, lint, type checks on every task)." width="700">
11
+ </p>
12
+
9
13
  AI coding agents forget everything between sessions. Each session starts with whatever context was prepared for it — nothing more. Because agents carry no persistent state, that state must live in the codebase itself, and any agent that reads the same well-structured context should be able to pick up where another left off. Compound Agent implements this: it captures mistakes once, retrieves them precisely when relevant, and can hand entire systems to an autonomous loop that processes epic by epic with no human intervention.
10
14
 
11
15
  ## What gets installed
@@ -24,43 +28,22 @@ This is not a memory plugin bolted onto a text editor. It is the environment you
24
28
 
25
29
  ## How it works
26
30
 
27
- ```mermaid
28
- flowchart TD
29
- A["/compound:architect\nDecompose large system\ninto epics via DDD"] -->|produces epics| L
31
+ Two memory systems persist across sessions:
30
32
 
31
- subgraph L["Compound Loop — one cycle per epic"]
32
- direction LR
33
- S[SPEC-DEV] --> P[PLAN]
34
- P --> W[WORK]
35
- W --> R[REVIEW]
36
- R --> C[COMPOUND]
37
- end
33
+ <p align="center">
34
+ <img src="docs/assets/diagram-1.png" alt="A task session between two memory systems: Lessons (JSONL + SQLite with semantic + keyword search) are retrieved before and captured after each task. Knowledge (project docs chunked and embedded) is queried on demand." width="700">
35
+ </p>
38
36
 
39
- C -->|writes lessons| M[(MEMORY\nJSONL + SQLite\n+ embeddings)]
40
- M -->|injects context| P
37
+ - **Lessons** mistakes, corrections, and patterns stored as git-tracked JSONL, indexed in SQLite FTS5 with local embeddings for hybrid search. Retrieved at the start of each task, captured at the end.
38
+ - **Knowledge** project documentation chunked and embedded for semantic retrieval. Any phase can query it on demand.
41
39
 
42
- style M fill:#f9f,stroke:#333
43
- style A fill:#e8f4fd,stroke:#4a9ede
44
- ```
40
+ Each task runs through five phases, with review findings looping back to rework. Each phase runs as its own slash command so instructions are re-injected fresh (surviving context compaction):
45
41
 
46
- Each cycle through the loop makes the next one smarter. The architect step is optional — use it for systems too large for a single feature cycle.
42
+ <p align="center">
43
+ <img src="docs/assets/diagram-2.png" alt="Inside a task: five phases (Spec, Plan, Work, Review, Compound) connected in sequence with a feedback loop from Review back to Work. Each phase runs as its own slash command with fresh instructions. Lessons are retrieved at start and captured at end. Knowledge is queryable from any phase." width="700">
44
+ </p>
47
45
 
48
- ```mermaid
49
- block-beta
50
- columns 1
51
- block:L3["Workflows · Feedback Loops"]
52
- A["15 slash commands"] B["24 specialized agents"] C["Autonomous loop"]
53
- end
54
- block:L2["Semantic Memory · Codebase Memory"]
55
- D["Vector search"] E["Hybrid retrieval"] F["Cross-session persistence"]
56
- end
57
- block:L1["Beads Foundation · Navigable Structure"]
58
- G["Issue tracking"] H["Git-backed sync"] I["Dependency graphs"]
59
- end
60
-
61
- L3 --> L2
62
- L2 --> L1
63
- ```
46
+ Each cycle through the loop makes the next one smarter. The architect step is optional — use it for systems too large for a single feature cycle.
64
47
 
65
48
  ## Three principles
66
49
 
@@ -154,6 +137,10 @@ ca loop --reviewers claude-sonnet --review-every 3
154
137
 
155
138
  ## The infinity loop
156
139
 
140
+ <p align="center">
141
+ <img src="docs/assets/diagram-3.png" alt="ca loop chains tasks in dependency order: Task 1 through Task 4, each running a full cycle in a fresh session. Cross-model review (R) gates between tasks. Failed tasks retry automatically. Tasks can escalate to human-required. Generated bash script with deterministic orchestration." width="700">
142
+ </p>
143
+
157
144
  `ca loop` generates a bash script that processes your beads epics sequentially, running the full cook-it cycle on each one. No human intervention required between epics.
158
145
 
159
146
  ```bash
@@ -242,18 +229,15 @@ Three human approval gates separate the phases. Each output epic is sized for on
242
229
  # Install as dev dependency
243
230
  pnpm add -D compound-agent
244
231
 
245
- # One-shot setup (creates dirs, hooks, downloads model)
232
+ # One-shot setup (creates dirs, hooks, templates)
246
233
  npx ca setup
247
-
248
- # Skip the ~278MB model download (do it later)
249
- npx ca setup --skip-model
250
234
  ```
251
235
 
252
236
  ### Requirements
253
237
 
254
238
  - Node.js >= 20
255
239
  - ~278MB disk space for the embedding model (one-time download, shared across projects)
256
- - ~150MB RAM during embedding operations
240
+ - ~23MB RAM during embedding operations (nomic-embed-text-v1.5 via Transformers.js)
257
241
 
258
242
  ### pnpm Users
259
243
 
@@ -264,7 +248,7 @@ If you prefer to configure manually, add to your `package.json`:
264
248
  ```json
265
249
  {
266
250
  "pnpm": {
267
- "onlyBuiltDependencies": ["better-sqlite3", "node-llama-cpp"]
251
+ "onlyBuiltDependencies": ["better-sqlite3", "onnxruntime-node"]
268
252
  }
269
253
  }
270
254
  ```
@@ -355,15 +339,14 @@ The CLI binary is `ca` (alias: `compound-agent`).
355
339
 
356
340
  | Command | Description |
357
341
  |---------|-------------|
358
- | `ca setup` | One-shot setup (hooks + git pre-commit + model) |
359
- | `ca setup --skip-model` | Setup without model download |
360
- | `ca setup --uninstall` | Remove all generated files |
361
- | `ca setup --update` | Regenerate files (preserves user customisations) |
362
- | `ca setup --status` | Show installation status |
363
- | `ca setup --dry-run` | Show what would change without changing |
342
+ | `ca setup` | One-shot setup (hooks + templates) |
343
+ | `ca setup --skip-hooks` | Setup without installing hooks |
344
+ | `ca setup --json` | Output result as JSON |
345
+ | `ca setup --repo-root <path>` | Specify repository root |
346
+ | `ca setup claude` | Install Claude Code hooks only |
364
347
  | `ca setup claude --status` | Check Claude Code integration health |
365
348
  | `ca setup claude --uninstall` | Remove Claude hooks only |
366
- | `ca download-model` | Download the embedding model |
349
+ | `ca init` | Initialize compound-agent in current repo |
367
350
  | `ca about` | Show version, animation, and recent changelog |
368
351
  | `ca doctor` | Verify external dependencies and project health |
369
352
 
@@ -394,7 +377,7 @@ confirmation_boost: confirmed=1.3, unconfirmed=1.0
394
377
  A: mem0 is a cloud memory layer for general AI agents. Compound Agent is local-first with git-tracked storage and local embeddings — no API keys or cloud services needed. It also goes beyond memory with structured workflows, multi-agent review, and issue tracking.
395
378
 
396
379
  **Q: Does this work offline?**
397
- A: Yes, completely. Embeddings run locally via node-llama-cpp. No network requests after the initial model download.
380
+ A: Yes, completely. Embeddings run locally via @huggingface/transformers (Transformers.js). No network requests after the initial model download.
398
381
 
399
382
  **Q: How much disk space does it need?**
400
383
  A: ~278MB for the embedding model (one-time download, shared across projects) plus negligible space for lessons.
@@ -434,7 +417,7 @@ pnpm lint # Type check + ESLint
434
417
  | Build | tsup |
435
418
  | Testing | Vitest + fast-check (property tests) |
436
419
  | Storage | better-sqlite3 + FTS5 |
437
- | Embeddings | node-llama-cpp + EmbeddingGemma-300M |
420
+ | Embeddings | @huggingface/transformers + nomic-embed-text-v1.5 (Q8 ONNX) |
438
421
  | CLI | Commander.js |
439
422
  | Schema | Zod |
440
423
  | Issue Tracking | Beads (bd) |
package/bin/ca ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Thin wrapper that spawns the Go binary.
4
+ // Uses Node.js only for locating the binary; all work is done by the Go process.
5
+
6
+ import { execFileSync } from "node:child_process";
7
+ import { existsSync } from "node:fs";
8
+ import { resolve, dirname } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+
13
+ // Resolution order: env override → postinstall binary → local Go build
14
+ const candidates = [
15
+ process.env.CA_BINARY_PATH,
16
+ resolve(__dirname, "ca-binary"),
17
+ resolve(__dirname, "..", "go", "dist", "ca"),
18
+ ].filter(Boolean);
19
+
20
+ const binaryPath = candidates.find((p) => existsSync(p));
21
+
22
+ if (!binaryPath) {
23
+ console.error("[compound-agent] Binary not found. Try reinstalling compound-agent or run: cd go && make build");
24
+ process.exit(1);
25
+ }
26
+
27
+ try {
28
+ execFileSync(binaryPath, process.argv.slice(2), { stdio: "inherit" });
29
+ } catch (err) {
30
+ // execFileSync throws on non-zero exit codes; forward the exit code
31
+ process.exit(err.status || 1);
32
+ }
package/package.json CHANGED
@@ -1,28 +1,31 @@
1
1
  {
2
2
  "name": "compound-agent",
3
- "version": "1.8.0",
4
- "description": "Semantically-intelligent workflow plugin for Claude Code",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
3
+ "version": "2.0.0",
4
+ "description": "Learning system for Claude Code — avoids repeating mistakes across sessions",
8
5
  "bin": {
9
- "compound-agent": "./dist/cli.js",
10
- "ca": "./dist/cli.js"
6
+ "ca": "./bin/ca",
7
+ "compound-agent": "./bin/ca"
11
8
  },
12
- "exports": {
13
- ".": {
14
- "import": "./dist/index.js",
15
- "types": "./dist/index.d.ts"
16
- }
9
+ "scripts": {
10
+ "postinstall": "node scripts/postinstall.cjs"
17
11
  },
18
12
  "files": [
19
- "dist",
20
- "docs/research",
21
- "scripts/postinstall.mjs",
13
+ "bin/",
14
+ "scripts/postinstall.cjs",
15
+ "README.md",
16
+ "LICENSE",
22
17
  "CHANGELOG.md",
23
18
  "llms.txt",
24
19
  "context7.json"
25
20
  ],
21
+ "os": [
22
+ "darwin",
23
+ "linux"
24
+ ],
25
+ "cpu": [
26
+ "x64",
27
+ "arm64"
28
+ ],
26
29
  "repository": {
27
30
  "type": "git",
28
31
  "url": "git+https://github.com/Nathandela/compound-agent.git"
@@ -32,84 +35,22 @@
32
35
  },
33
36
  "homepage": "https://github.com/Nathandela/compound-agent#readme",
34
37
  "llms": "https://raw.githubusercontent.com/Nathandela/compound-agent/main/llms.txt",
35
- "scripts": {
36
- "postinstall": "node scripts/postinstall.mjs",
37
- "prebuild": "tsx scripts/extract-changelog.ts",
38
- "build": "tsup",
39
- "dev": "tsup --watch",
40
- "test": "pnpm build && vitest run",
41
- "test:fast": "vitest run --project unit --project embedding",
42
- "test:unit": "vitest run --project unit",
43
- "test:integration": "pnpm build && vitest run --project integration",
44
- "test:watch": "vitest",
45
- "test:changed": "vitest run --changed HEAD~1",
46
- "test:all": "pnpm build && pnpm download-model && vitest run",
47
- "test:segment": "tsx src/test-utils/run-segment.ts",
48
- "test:random": "tsx src/test-utils/run-random.ts",
49
- "test:critical": "vitest run --project unit -- critical",
50
- "lint": "tsc --noEmit && eslint . --max-warnings=0",
51
- "download-model": "node ./dist/cli.js download-model",
52
- "prepublishOnly": "pnpm build"
53
- },
54
38
  "keywords": [
55
39
  "claude",
56
40
  "claude-code",
57
41
  "compound-agent",
58
- "semantic-memory",
59
- "memory",
60
- "embeddings",
61
- "vector-search",
62
42
  "ai",
63
43
  "agent",
64
44
  "llm",
65
- "plugin",
66
45
  "cli",
67
46
  "developer-tools",
68
47
  "workflow",
69
48
  "tdd",
70
- "sqlite",
71
49
  "knowledge-management"
72
50
  ],
73
51
  "author": "Nathan Delacrétaz",
74
52
  "license": "MIT",
75
- "packageManager": "pnpm@10.28.2",
76
53
  "engines": {
77
- "node": ">=20"
78
- },
79
- "devDependencies": {
80
- "@eslint/js": "^9.39.2",
81
- "@fast-check/vitest": "0.2.4",
82
- "@types/better-sqlite3": "^7.6.13",
83
- "@types/node": "^20.11.0",
84
- "@typescript-eslint/rule-tester": "8.55.0",
85
- "@vitest/coverage-v8": "2.1.9",
86
- "eslint": "^9.39.2",
87
- "eslint-config-prettier": "10.1.8",
88
- "eslint-plugin-import-x": "4.16.1",
89
- "eslint-plugin-vitest": "0.5.4",
90
- "fast-check": "4.5.3",
91
- "tsup": "^8.0.0",
92
- "tsx": "^4.0.0",
93
- "typescript": "^5.3.0",
94
- "typescript-eslint": "8.55.0",
95
- "vitest": "^2.0.0"
96
- },
97
- "dependencies": {
98
- "better-sqlite3": "^11.0.0",
99
- "chalk": "5.6.2",
100
- "commander": "^12.0.0",
101
- "node-llama-cpp": "^3.0.0",
102
- "zod": "^3.22.0"
103
- },
104
- "pnpm": {
105
- "onlyBuiltDependencies": [
106
- "better-sqlite3",
107
- "node-llama-cpp",
108
- "esbuild"
109
- ],
110
- "overrides": {
111
- "tar": ">=7.5.7",
112
- "axios": ">=1.13.5"
113
- }
54
+ "node": ">=18"
114
55
  }
115
56
  }
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Downloads platform-specific binaries (ca + ca-embed) from GitHub Releases.
4
+ // Uses Node.js platform detection (NOT Go's runtime.GOARCH) to handle
5
+ // Rosetta/emulation correctly on Apple Silicon.
6
+ //
7
+ // Exports getPlatformKey, verifyChecksum, shouldSkipDownload for testability.
8
+
9
+ const https = require("https");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { execFileSync } = require("child_process");
13
+ const { createHash } = require("crypto");
14
+
15
+ const PLATFORM_MAP = { darwin: "darwin", linux: "linux" };
16
+ const ARCH_MAP = { x64: "amd64", arm64: "arm64" };
17
+ const REPO = "Nathandela/compound-agent";
18
+
19
+ function getPlatformKey(platform, arch) {
20
+ const p = PLATFORM_MAP[platform];
21
+ const a = ARCH_MAP[arch];
22
+ if (!p || !a) {
23
+ throw new Error(
24
+ `Unsupported platform: ${platform}-${arch}. Supported: darwin-amd64, darwin-arm64, linux-amd64, linux-arm64`
25
+ );
26
+ }
27
+ return `${p}-${a}`;
28
+ }
29
+
30
+ function verifyChecksum(filePath, artifactName, checksumsPath) {
31
+ const checksums = fs.readFileSync(checksumsPath, "utf-8");
32
+ const lines = checksums.trim().split("\n");
33
+
34
+ let expectedHash = null;
35
+ for (const line of lines) {
36
+ // GoReleaser format: <sha256> <filename>
37
+ const parts = line.trim().split(/\s+/);
38
+ if (parts.length >= 2 && parts[1] === artifactName) {
39
+ expectedHash = parts[0];
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (!expectedHash) {
45
+ throw new Error(`${artifactName} not found in checksums.txt`);
46
+ }
47
+
48
+ const fileData = fs.readFileSync(filePath);
49
+ const actualHash = createHash("sha256").update(fileData).digest("hex");
50
+ return actualHash === expectedHash;
51
+ }
52
+
53
+ function shouldSkipDownload(binDir) {
54
+ const caPath = path.join(binDir, "ca-binary");
55
+ const embedPath = path.join(binDir, "ca-embed");
56
+
57
+ if (!fs.existsSync(caPath) || !fs.existsSync(embedPath)) {
58
+ return false;
59
+ }
60
+
61
+ try {
62
+ // P1-2 fix: use execFileSync (no shell) instead of execSync
63
+ execFileSync(caPath, ["version"], { stdio: "pipe" });
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ function downloadFile(url, dest) {
71
+ return new Promise((resolve, reject) => {
72
+ const follow = (currentUrl, redirects) => {
73
+ if (redirects > 5) {
74
+ reject(new Error("Too many redirects"));
75
+ return;
76
+ }
77
+
78
+ // P0-2 fix: validate redirect URLs stay on HTTPS
79
+ if (!currentUrl.startsWith("https://")) {
80
+ reject(new Error(`Refusing non-HTTPS redirect: ${currentUrl}`));
81
+ return;
82
+ }
83
+
84
+ https
85
+ .get(currentUrl, { timeout: 60000 }, (res) => {
86
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
87
+ follow(res.headers.location, redirects + 1);
88
+ return;
89
+ }
90
+
91
+ if (res.statusCode !== 200) {
92
+ reject(new Error(`Download failed: HTTP ${res.statusCode} from ${currentUrl}`));
93
+ return;
94
+ }
95
+
96
+ const file = fs.createWriteStream(dest);
97
+ res.pipe(file);
98
+ file.on("finish", resolve);
99
+ file.on("error", (err) => {
100
+ fs.unlink(dest, () => {});
101
+ reject(err);
102
+ });
103
+ res.on("error", (err) => {
104
+ file.destroy();
105
+ fs.unlink(dest, () => {});
106
+ reject(err);
107
+ });
108
+ })
109
+ .on("timeout", () => {
110
+ reject(new Error(`Download timed out: ${currentUrl}`));
111
+ })
112
+ .on("error", reject);
113
+ };
114
+
115
+ follow(url, 0);
116
+ });
117
+ }
118
+
119
+ // P1-3 fix: download to .tmp name, rename after checksum verification
120
+ async function downloadBinary(binDir, url, destName, label) {
121
+ const tmpPath = path.join(binDir, destName + ".tmp");
122
+
123
+ await downloadFile(url, tmpPath);
124
+
125
+ const stats = fs.statSync(tmpPath);
126
+ const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
127
+ console.log(`[compound-agent] ${label}: ${sizeMB} MB`);
128
+ }
129
+
130
+ function cleanupBinaries(binDir) {
131
+ for (const name of ["ca-binary", "ca-binary.tmp", "ca-embed", "ca-embed.tmp", "checksums.txt"]) {
132
+ try { fs.unlinkSync(path.join(binDir, name)); } catch { /* ignore */ }
133
+ }
134
+ }
135
+
136
+ async function main() {
137
+ // Skip self-install (when running pnpm install inside compound-agent itself)
138
+ if (process.env.npm_package_name === "compound-agent") return;
139
+
140
+ const platformKey = getPlatformKey(
141
+ require("os").platform(),
142
+ require("os").arch()
143
+ );
144
+
145
+ const pkg = require("../package.json");
146
+ const version = pkg.version;
147
+
148
+ const binDir = path.resolve(__dirname, "../bin");
149
+
150
+ if (shouldSkipDownload(binDir)) {
151
+ console.log("[compound-agent] Binaries already installed, skipping download");
152
+ return;
153
+ }
154
+
155
+ console.log(`[compound-agent] Platform: ${platformKey}`);
156
+ console.log(`[compound-agent] Downloading: v${version}`);
157
+
158
+ if (!fs.existsSync(binDir)) {
159
+ fs.mkdirSync(binDir, { recursive: true });
160
+ }
161
+
162
+ const baseUrl = `https://github.com/${REPO}/releases/download/v${version}`;
163
+
164
+ try {
165
+ // Download checksums first
166
+ const checksumsPath = path.join(binDir, "checksums.txt");
167
+ await downloadFile(`${baseUrl}/checksums.txt`, checksumsPath);
168
+
169
+ // Download both binaries in parallel (to .tmp names)
170
+ const caArtifact = `ca-${platformKey}`;
171
+ const embedArtifact = `ca-embed-${platformKey}`;
172
+
173
+ await Promise.all([
174
+ downloadBinary(binDir, `${baseUrl}/${caArtifact}`, "ca-binary", "CLI binary"),
175
+ downloadBinary(binDir, `${baseUrl}/${embedArtifact}`, "ca-embed", "Embed daemon"),
176
+ ]);
177
+
178
+ // Verify checksums against .tmp files
179
+ const caOk = verifyChecksum(path.join(binDir, "ca-binary.tmp"), caArtifact, checksumsPath);
180
+ const embedOk = verifyChecksum(path.join(binDir, "ca-embed.tmp"), embedArtifact, checksumsPath);
181
+
182
+ if (!caOk || !embedOk) {
183
+ const failed = [];
184
+ if (!caOk) failed.push("ca");
185
+ if (!embedOk) failed.push("ca-embed");
186
+ // P1-4 fix: clean up bad binaries before throwing
187
+ cleanupBinaries(binDir);
188
+ throw new Error(`Checksum verification failed for: ${failed.join(", ")}`);
189
+ }
190
+
191
+ console.log("[compound-agent] Checksums verified");
192
+
193
+ // Checksums passed — rename .tmp to final names and set executable
194
+ fs.renameSync(path.join(binDir, "ca-binary.tmp"), path.join(binDir, "ca-binary"));
195
+ fs.chmodSync(path.join(binDir, "ca-binary"), 0o755);
196
+ fs.renameSync(path.join(binDir, "ca-embed.tmp"), path.join(binDir, "ca-embed"));
197
+ fs.chmodSync(path.join(binDir, "ca-embed"), 0o755);
198
+
199
+ // Functional verification (P1-2 fix: use execFileSync)
200
+ try {
201
+ execFileSync(path.join(binDir, "ca-binary"), ["version"], { stdio: "pipe" });
202
+ console.log("[compound-agent] Functional check passed");
203
+ } catch {
204
+ cleanupBinaries(binDir);
205
+ throw new Error("Binary downloaded but functional check failed (ca version exited non-zero)");
206
+ }
207
+ } catch (err) {
208
+ console.error(`[compound-agent] Installation failed: ${err.message}`);
209
+ console.error("[compound-agent] You can manually download binaries from:");
210
+ console.error(`[compound-agent] https://github.com/${REPO}/releases/tag/v${version}`);
211
+ process.exit(1);
212
+ }
213
+ }
214
+
215
+ // Export for testing
216
+ module.exports = { getPlatformKey, verifyChecksum, shouldSkipDownload };
217
+
218
+ // Run main only when executed directly (not when required for testing)
219
+ if (require.main === module) {
220
+ main();
221
+ }
package/dist/cli.d.ts DELETED
@@ -1 +0,0 @@
1
- #!/usr/bin/env node