compound-agent 2.5.1 → 2.6.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,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [2.6.0] - 2026-04-01
13
+
14
+ ### Added
15
+
16
+ - **`--compact-pct` flag** for `ca loop`, `ca improve`, and `ca polish`: Sets `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` in generated scripts to trigger context compaction earlier during autonomous workflows. Default 0 (use Claude Code default). Suggested value: 50 for Opus 1M sessions. Only affects generated scripts, not interactive sessions. Validates range 0-100.
17
+ - **Windows Native Support**: Native Windows binaries (amd64 + arm64) distributed via npm. Pure-Go SQLite driver eliminates CGO requirement. Real `LockFileEx`/`UnlockFileEx` file locking, `OpenProcess`/`GetExitCodeProcess` process detection, and `cmd /c start` URL opening with command injection prevention. Search gracefully degrades to keyword-only FTS5 (embed daemon is Unix-only). CI matrix includes `windows-latest`.
18
+ - **Self-Explaining System (`ca info`)** (Epic 4): New CLI command displaying comprehensive system health — version, hooks, skills, phase state, telemetry, and lesson corpus stats. 13 tests.
19
+ - **Skill Phase Metadata** (Epic 3): Structured `phase` field in YAML frontmatter of all SKILL.md files with pre-compiled `skills_index.json` for fast runtime skill lookup. Phase guard uses `ResolveSkillPath` for phase-aware routing.
20
+ - **Telemetry Foundation** (Epic 2): Schema v7 with telemetry table, `ca health` command, file-based lock for concurrent access, and hook execution instrumentation.
21
+ - **V3.0 Harness Overhaul Specification**: Spec and advisory brief for upcoming harness overhaul.
22
+ - **GOTCHA.md for architect skill**: Documented common pitfalls for architect workflows.
23
+
24
+ ### Changed
25
+
26
+ - **SQLite driver**: Replaced `mattn/go-sqlite3` (CGO) with `modernc.org/sqlite` (pure Go). Enables `CGO_ENABLED=0` builds and Windows cross-compilation. DSN format uses `_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)`. FTS5 included by default — no build tags required.
27
+ - **Build pipeline**: All builds now use `CGO_ENABLED=0`. Removed `-tags sqlite_fts5` from Makefile, CI, GoReleaser, and lint config. Added `windows-amd64` and `windows-arm64` targets to GoReleaser, CI matrix, and Makefile.
28
+ - **npm distribution**: Added `@syottos/win32-x64` and `@syottos/win32-arm64` platform packages. Updated `bin/ca` wrapper, `postinstall.cjs`, and `publish-platforms.cjs` for `.exe` handling and embed daemon exclusion on Windows.
29
+
30
+ ### Fixed
31
+
32
+ - **Documentation inaccuracies** (Epic 1): Fixed TypeScript/npm references across docs and templates, corrected hook counts, rewrote README with Mermaid diagram, added WSL2 doctor check.
33
+ - **Screen session name collisions**: Unique session names using `compound-loop-$(basename $(pwd))` pattern to avoid host-level collisions (PR #10).
34
+ - **6 golangci-lint violations**: gofmt alignment in map literals, extracted `checkHooks`/`printDoctorResults` helpers to reduce cyclop/funlen, handled `flockUnlock` error return.
35
+ - **Review findings**: Multiple rounds of P0-P3 fixes from external reviewers across Epics 1-4.
36
+ - **Command injection in `openURL`**: Windows `cmd /c start` now validates URL scheme (`http://`/`https://` only) and uses `exec.Command` argument splitting to prevent shell metacharacter injection.
37
+
38
+ ### Dependencies
39
+
40
+ - **modernc.org/sqlite**: v1.48.0 — pure-Go SQLite driver replacing mattn/go-sqlite3 (CGO). Enables Windows native builds.
41
+ - **golang.org/x/sys**: v0.42.0 — Windows `LockFileEx`/`UnlockFileEx` and process APIs.
42
+ - **thiserror**: 1.0.69 to 2.0.18 in Rust embed daemon — major version bump with no-std support and improved diagnostics (PR #8).
43
+ - **tokenizers**: 0.21.4 to 0.22.2 in Rust embed daemon — PyO3 0.26, faster vocab loading, GIL-free (PR #7).
44
+
45
+ ## [2.5.2] - 2026-03-31
46
+
47
+ ### Added
48
+
49
+ - **Stale output watchdog (Layer 4)**: New background watchdog monitors trace file for output inactivity during Claude sessions. If no output is written for `SESSION_STALE_TIMEOUT` seconds (default: 1800s/30min), the session is killed and the loop proceeds. Prevents indefinite hangs when the Claude CLI completes its API work but fails to exit.
50
+
51
+ ### Fixed
52
+
53
+ - **Polish loop deadlock**: Architect prompt no longer uses `Parent: $META_EPIC` label, which caused the architect to wire blocking dependencies to a meta-epic that never closes. Replaced with context-only reference and explicit prohibition against `--parent` and `bd dep add` to the meta-epic.
54
+ - **Silent zero-work exit**: Infinity loop now exits with code 2 (distinct from success=0 and failure=1) when zero epics are completed and zero failed, signaling that all epics were blocked or skipped.
55
+ - **Inner loop exit code swallowed**: Polish loop's `run_inner_loop` used `|| true` which discarded the exit code. Now captures exit code properly and detects zero-work (exit 2) to surface blocked-epic deadlocks.
56
+ - **Inner loop `set -e` cascade**: `run_inner_loop` call in main polish loop now guarded with `||` handler, matching the pattern used for `run_polish_architect`, preventing a single failed cycle from aborting the entire polish script.
57
+
58
+ ### Removed
59
+
60
+ - **Improve loop references from architect skill**: Removed `ca improve` references from shipped architect SKILL.md, infinity-loop README, pre-flight docs, and deleted the `references/improve-loop/` directory. The improve loop remains available as a standalone CLI command but is not part of the architect workflow.
61
+
12
62
  ## [2.5.1] - 2026-03-28
13
63
 
14
64
  ### Added
package/README.md CHANGED
@@ -14,12 +14,12 @@ AI coding agents forget everything between sessions. Each session starts with wh
14
14
 
15
15
  ## What gets installed
16
16
 
17
- `npx ca setup` injects a complete development environment into your repository:
17
+ `ca setup` injects a complete development environment into your repository:
18
18
 
19
19
  | Component | What ships |
20
20
  |-----------|-----------|
21
- | 15 slash commands | `/compound:architect`, `cook-it`, `spec-dev`, `plan`, `work`, `review`, `compound`, `learn-that`, `check-that`, and more |
22
- | 24 agent role skills | Security reviewers, TDD pair, decomposition convoy, spec writers, test analysers, drift detectors, and more |
21
+ | 16 slash commands | `/compound:architect`, `cook-it`, `spec-dev`, `plan`, `work`, `review`, `compound`, `learn-that`, `check-that`, and more |
22
+ | 26 agent role skills | TDD pair, drift detector, audit, research specialist, external reviewers, and more |
23
23
  | 7 automatic hooks | Fire on session start, prompt submit, tool use, tool failure, pre-compact, phase guard, and session stop |
24
24
  | 5 phase skill files | Full workflow instructions for `architect`, `spec-dev`, `cook-it`, `work`, and `review` |
25
25
  | 5 deployed docs | Workflow reference, CLI reference, skills guide, integration guide, and overview |
@@ -202,9 +202,9 @@ Once installed, seven Claude Code hooks fire without any commands:
202
202
  | `PreToolUse` | During cook-it | Enforces phase gates — prevents jumping ahead |
203
203
  | `PostToolUse` | After tool success | Clears failure tracking state |
204
204
  | `PostToolUseFailure` | After tool failure | Tracks failures; suggests memory search after repeated errors |
205
- | `Stop` | Session end | Audits session for uncaptured lessons and unclosed issues |
205
+ | `Stop` | Session end | Enforces phase gates prevents skipping required steps |
206
206
 
207
- No configuration needed. `npx ca setup` wires them into your `.claude/settings.json`.
207
+ No configuration needed. `ca setup` wires them into your `.claude/settings.json`.
208
208
 
209
209
  ## `/compound:architect`
210
210
 
@@ -235,25 +235,20 @@ npx ca setup
235
235
 
236
236
  ### Requirements
237
237
 
238
- - Node.js >= 20
238
+ - Node.js >= 20 (for `npx` wrapper — the CLI itself is a Go binary)
239
239
  - ~278MB disk space for the embedding model (one-time download, shared across projects)
240
- - ~23MB RAM during embedding operations (nomic-embed-text-v1.5 via Transformers.js)
240
+ - Embedding runs via `ca-embed` Rust daemon (nomic-embed-text-v1.5 ONNX)
241
241
 
242
- ### pnpm Users
242
+ ### Windows Users
243
243
 
244
- pnpm v9+ blocks native addon builds by default. Running `npx ca setup` automatically detects pnpm and adds the required config to your `package.json`.
244
+ Compound-agent runs natively on Windows (amd64 and arm64). Install and use it the same way as on macOS/Linux:
245
245
 
246
- If you prefer to configure manually, add to your `package.json`:
247
-
248
- ```json
249
- {
250
- "pnpm": {
251
- "onlyBuiltDependencies": ["better-sqlite3", "onnxruntime-node"]
252
- }
253
- }
246
+ ```bash
247
+ pnpm add -D compound-agent
248
+ npx ca setup
254
249
  ```
255
250
 
256
- Then run `pnpm install`.
251
+ **Note**: The embedding daemon (`ca-embed`) is not available on Windows. Search automatically falls back to keyword-only mode (FTS5). All other features work identically. WSL2 users get full functionality including vector search.
257
252
 
258
253
  ## CLI Reference
259
254
 
@@ -380,16 +375,16 @@ confirmation_boost: confirmed=1.3, unconfirmed=1.0
380
375
  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.
381
376
 
382
377
  **Q: Does this work offline?**
383
- A: Yes, completely. Embeddings run locally via @huggingface/transformers (Transformers.js). No network requests after the initial model download.
378
+ A: Yes, completely. Embeddings run locally via the `ca-embed` Rust daemon (nomic-embed-text-v1.5 ONNX). No network requests after the initial model download.
384
379
 
385
380
  **Q: How much disk space does it need?**
386
381
  A: ~278MB for the embedding model (one-time download, shared across projects) plus negligible space for lessons.
387
382
 
388
383
  **Q: Can I use it with other AI coding tools?**
389
- A: The CLI (`ca`) works standalone with any tool. Full hook integration is available for Claude Code and Gemini CLI. The TypeScript API can be integrated into other tools.
384
+ A: The CLI (`ca`) works standalone with any tool. Full hook integration is available for Claude Code and Gemini CLI.
390
385
 
391
386
  **Q: What happens if the embedding model isn't available?**
392
- A: Search gracefully falls back to keyword-only mode. Other commands that require embeddings will tell you what's missing. Run `npx ca doctor` to diagnose issues.
387
+ A: Search gracefully falls back to keyword-only mode. Other commands that require embeddings will tell you what's missing. Run `ca doctor` to diagnose issues.
393
388
 
394
389
  **Q: Is the loop production-ready?**
395
390
  A: The loop works and has been used to ship real projects, including compound-agent itself. Long-duration autonomous runs across many epics are the current area of hardening. For 3–5 epic sequences, it is reliable today.
@@ -397,9 +392,9 @@ A: The loop works and has been used to ship real projects, including compound-ag
397
392
  ## Development
398
393
 
399
394
  ```bash
400
- cd go && go build -tags sqlite_fts5 ./cmd/ca # Build CLI binary
401
- cd go && go test -tags sqlite_fts5 ./... # Full test suite
402
- cd go && go vet -tags sqlite_fts5 ./... # Static analysis
395
+ cd go && go build ./cmd/ca # Build CLI binary
396
+ cd go && go test ./... # Full test suite
397
+ cd go && go vet ./... # Static analysis
403
398
  ```
404
399
 
405
400
  ## Technology Stack
@@ -408,14 +403,56 @@ cd go && go vet -tags sqlite_fts5 ./... # Static analysis
408
403
  |-----------|------------|
409
404
  | Language | Go |
410
405
  | Package Manager | Go modules (+ pnpm for npm wrapper) |
411
- | Build | go build with CGO + sqlite_fts5 tag |
406
+ | Build | go build with CGO_ENABLED=0 (pure Go) |
412
407
  | Testing | go test + table-driven tests |
413
- | Storage | mattn/go-sqlite3 + FTS5 |
408
+ | Storage | modernc.org/sqlite + FTS5 (pure Go, no CGO) |
414
409
  | Embeddings | ca-embed (Rust daemon via IPC) |
415
410
  | CLI | Cobra |
416
411
  | Release | GoReleaser |
417
412
  | Issue Tracking | Beads (bd) |
418
413
 
414
+ ## Architecture
415
+
416
+ ```mermaid
417
+ graph TD
418
+ subgraph "Claude Code Session"
419
+ H[Hooks] -->|SessionStart| P[ca prime]
420
+ H -->|UserPromptSubmit| UP[user-prompt hook]
421
+ H -->|PostToolUseFailure| TF[failure tracker]
422
+ H -->|PreToolUse| PG[phase guard]
423
+ H -->|Stop| SA[stop audit]
424
+ end
425
+
426
+ subgraph "CLI (Go + Cobra)"
427
+ CA[ca binary] --> LEARN[ca learn]
428
+ CA --> SEARCH[ca search]
429
+ CA --> LOOP[ca loop]
430
+ CA --> SETUP[ca setup]
431
+ CA --> DOCTOR[ca doctor]
432
+ end
433
+
434
+ subgraph "Storage"
435
+ JSONL[".claude/lessons/index.jsonl<br/>(git-tracked source of truth)"]
436
+ SQLITE[".claude/.cache/lessons.sqlite<br/>(FTS5 search index)"]
437
+ JSONL -->|rebuild| SQLITE
438
+ end
439
+
440
+ subgraph "Embeddings"
441
+ EMBED["ca-embed (Rust daemon)"] -->|IPC via Unix socket| VEC[Vector similarity]
442
+ end
443
+
444
+ UP -->|inject lessons| SEARCH
445
+ TF -->|suggest search| SEARCH
446
+ LEARN --> JSONL
447
+ SEARCH --> SQLITE
448
+ SEARCH --> VEC
449
+ ```
450
+
451
+ Three layers work together:
452
+ - **Portable storage**: JSONL in git for conflict-free collaboration
453
+ - **Fast index**: SQLite + FTS5 for keyword search, rebuilt from JSONL on demand
454
+ - **Semantic search**: Rust embedding daemon for vector similarity, falls back to keyword-only if unavailable
455
+
419
456
  ## Documentation
420
457
 
421
458
  | Document | Purpose |
@@ -447,4 +484,4 @@ Bug reports and feature requests are welcome via [Issues](https://github.com/Nat
447
484
 
448
485
  MIT — see [LICENSE](LICENSE) for details.
449
486
 
450
- > The embedding model (EmbeddingGemma-300M) is downloaded on-demand and subject to Google's [Gemma Terms of Use](https://ai.google.dev/gemma/terms). See [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md) for full dependency license information.
487
+ > The embedding model (nomic-embed-text-v1.5) is downloaded on-demand from Hugging Face under the Apache 2.0 license. See [THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md) for full dependency license information.
package/bin/ca CHANGED
@@ -19,9 +19,10 @@ const require = createRequire(import.meta.url);
19
19
 
20
20
  function resolvePlatformBinary() {
21
21
  const pkg = `@syottos/${process.platform}-${process.arch}`;
22
+ const ext = process.platform === "win32" ? ".exe" : "";
22
23
  try {
23
24
  const pkgDir = dirname(require.resolve(`${pkg}/package.json`));
24
- const bin = resolve(pkgDir, "bin", "ca");
25
+ const bin = resolve(pkgDir, "bin", `ca${ext}`);
25
26
  if (existsSync(bin)) return bin;
26
27
  } catch {
27
28
  // Platform package not installed
@@ -30,11 +31,12 @@ function resolvePlatformBinary() {
30
31
  }
31
32
 
32
33
  function findBinary() {
34
+ const ext = process.platform === "win32" ? ".exe" : "";
33
35
  const candidates = [
34
36
  process.env.CA_BINARY_PATH,
35
37
  resolvePlatformBinary(),
36
- resolve(__dirname, "ca-binary"),
37
- resolve(__dirname, "..", "go", "dist", "ca"),
38
+ resolve(__dirname, `ca-binary${ext}`),
39
+ resolve(__dirname, "..", "go", "dist", `ca${ext}`),
38
40
  ].filter(Boolean);
39
41
 
40
42
  return candidates.find((p) => existsSync(p)) || null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compound-agent",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "description": "Learning system for Claude Code — avoids repeating mistakes across sessions",
6
6
  "bin": {
@@ -21,7 +21,8 @@
21
21
  ],
22
22
  "os": [
23
23
  "darwin",
24
- "linux"
24
+ "linux",
25
+ "win32"
25
26
  ],
26
27
  "cpu": [
27
28
  "x64",
@@ -50,10 +51,12 @@
50
51
  "knowledge-management"
51
52
  ],
52
53
  "optionalDependencies": {
53
- "@syottos/darwin-arm64": "2.5.1",
54
- "@syottos/darwin-x64": "2.5.1",
55
- "@syottos/linux-arm64": "2.5.1",
56
- "@syottos/linux-x64": "2.5.1"
54
+ "@syottos/darwin-arm64": "2.6.0",
55
+ "@syottos/darwin-x64": "2.6.0",
56
+ "@syottos/linux-arm64": "2.6.0",
57
+ "@syottos/linux-x64": "2.6.0",
58
+ "@syottos/win32-x64": "2.6.0",
59
+ "@syottos/win32-arm64": "2.6.0"
57
60
  },
58
61
  "author": "Nathan Delacrétaz",
59
62
  "license": "MIT",
@@ -12,7 +12,7 @@ const path = require("path");
12
12
  const { execFileSync } = require("child_process");
13
13
  const { createHash } = require("crypto");
14
14
 
15
- const PLATFORM_MAP = { darwin: "darwin", linux: "linux" };
15
+ const PLATFORM_MAP = { darwin: "darwin", linux: "linux", win32: "windows" };
16
16
  const ARCH_MAP = { x64: "amd64", arm64: "arm64" };
17
17
  const REPO = "Nathandela/compound-agent";
18
18
 
@@ -21,7 +21,7 @@ function getPlatformKey(platform, arch) {
21
21
  const a = ARCH_MAP[arch];
22
22
  if (!p || !a) {
23
23
  throw new Error(
24
- `Unsupported platform: ${platform}-${arch}. Supported: darwin-amd64, darwin-arm64, linux-amd64, linux-arm64`
24
+ `Unsupported platform: ${platform}-${arch}. Supported: darwin-amd64, darwin-arm64, linux-amd64, linux-arm64, windows-amd64, windows-arm64`
25
25
  );
26
26
  }
27
27
  return `${p}-${a}`;
@@ -51,12 +51,13 @@ function verifyChecksum(filePath, artifactName, checksumsPath) {
51
51
  }
52
52
 
53
53
  function shouldSkipDownload(binDir, expectedVersion) {
54
- const caPath = path.join(binDir, "ca-binary");
54
+ const isWindows = require("os").platform() === "win32";
55
+ const caPath = path.join(binDir, isWindows ? "ca-binary.exe" : "ca-binary");
55
56
  const embedPath = path.join(binDir, "ca-embed");
56
57
 
57
- if (!fs.existsSync(caPath) || !fs.existsSync(embedPath)) {
58
- return false;
59
- }
58
+ if (!fs.existsSync(caPath)) return false;
59
+ // ca-embed is not available on Windows — only require it on Unix
60
+ if (!isWindows && !fs.existsSync(embedPath)) return false;
60
61
 
61
62
  try {
62
63
  const output = execFileSync(caPath, ["version"], { stdio: "pipe", encoding: "utf-8" });
@@ -133,16 +134,21 @@ async function downloadBinary(binDir, url, destName, label) {
133
134
  }
134
135
 
135
136
  function cleanupBinaries(binDir) {
136
- for (const name of ["ca-binary", "ca-binary.tmp", "ca-embed", "ca-embed.tmp", "checksums.txt"]) {
137
+ for (const name of [
138
+ "ca-binary", "ca-binary.tmp", "ca-binary.exe", "ca-binary.exe.tmp",
139
+ "ca-embed", "ca-embed.tmp", "checksums.txt",
140
+ ]) {
137
141
  try { fs.unlinkSync(path.join(binDir, name)); } catch { /* ignore */ }
138
142
  }
139
143
  }
140
144
 
141
145
  function platformPackageInstalled() {
142
- const pkg = `@syottos/${require("os").platform()}-${require("os").arch()}`;
146
+ const platform = require("os").platform();
147
+ const pkg = `@syottos/${platform}-${require("os").arch()}`;
148
+ const ext = platform === "win32" ? ".exe" : "";
143
149
  try {
144
150
  const pkgDir = path.dirname(require.resolve(`${pkg}/package.json`));
145
- return fs.existsSync(path.join(pkgDir, "bin", "ca"));
151
+ return fs.existsSync(path.join(pkgDir, "bin", `ca${ext}`));
146
152
  } catch {
147
153
  return false;
148
154
  }
@@ -187,20 +193,34 @@ async function main() {
187
193
  const checksumsPath = path.join(binDir, "checksums.txt");
188
194
  await downloadFile(`${baseUrl}/checksums.txt`, checksumsPath);
189
195
 
190
- // Download both binaries in parallel (to .tmp names)
191
- const caArtifact = `ca-${platformKey}`;
192
- // No x86_64 macOS embed build (ort-sys limitation) — use arm64 via Rosetta
193
- const embedKey = platformKey === "darwin-amd64" ? "darwin-arm64" : platformKey;
194
- const embedArtifact = `ca-embed-${embedKey}`;
196
+ // Download binaries in parallel (to .tmp names)
197
+ const isWindows = require("os").platform() === "win32";
198
+ const caExt = isWindows ? ".exe" : "";
199
+ const caArtifact = `ca-${platformKey}${caExt}`;
200
+
201
+ const downloads = [
202
+ downloadBinary(binDir, `${baseUrl}/${caArtifact}`, `ca-binary${caExt}`, "CLI binary"),
203
+ ];
204
+
205
+ // ca-embed is not available on Windows
206
+ let embedArtifact = null;
207
+ if (!isWindows) {
208
+ // No x86_64 macOS embed build (ort-sys limitation) — use arm64 via Rosetta
209
+ const embedKey = platformKey === "darwin-amd64" ? "darwin-arm64" : platformKey;
210
+ embedArtifact = `ca-embed-${embedKey}`;
211
+ downloads.push(
212
+ downloadBinary(binDir, `${baseUrl}/${embedArtifact}`, "ca-embed", "Embed daemon"),
213
+ );
214
+ }
195
215
 
196
- await Promise.all([
197
- downloadBinary(binDir, `${baseUrl}/${caArtifact}`, "ca-binary", "CLI binary"),
198
- downloadBinary(binDir, `${baseUrl}/${embedArtifact}`, "ca-embed", "Embed daemon"),
199
- ]);
216
+ await Promise.all(downloads);
200
217
 
201
218
  // Verify checksums against .tmp files
202
- const caOk = verifyChecksum(path.join(binDir, "ca-binary.tmp"), caArtifact, checksumsPath);
203
- const embedOk = verifyChecksum(path.join(binDir, "ca-embed.tmp"), embedArtifact, checksumsPath);
219
+ const caOk = verifyChecksum(path.join(binDir, `ca-binary${caExt}.tmp`), caArtifact, checksumsPath);
220
+ let embedOk = true;
221
+ if (embedArtifact) {
222
+ embedOk = verifyChecksum(path.join(binDir, "ca-embed.tmp"), embedArtifact, checksumsPath);
223
+ }
204
224
 
205
225
  if (!caOk || !embedOk) {
206
226
  const failed = [];
@@ -214,14 +234,17 @@ async function main() {
214
234
  console.log("[compound-agent] Checksums verified");
215
235
 
216
236
  // Checksums passed — rename .tmp to final names and set executable
217
- fs.renameSync(path.join(binDir, "ca-binary.tmp"), path.join(binDir, "ca-binary"));
218
- fs.chmodSync(path.join(binDir, "ca-binary"), 0o755);
219
- fs.renameSync(path.join(binDir, "ca-embed.tmp"), path.join(binDir, "ca-embed"));
220
- fs.chmodSync(path.join(binDir, "ca-embed"), 0o755);
237
+ const caFinal = path.join(binDir, `ca-binary${caExt}`);
238
+ fs.renameSync(path.join(binDir, `ca-binary${caExt}.tmp`), caFinal);
239
+ fs.chmodSync(caFinal, 0o755);
240
+ if (embedArtifact) {
241
+ fs.renameSync(path.join(binDir, "ca-embed.tmp"), path.join(binDir, "ca-embed"));
242
+ fs.chmodSync(path.join(binDir, "ca-embed"), 0o755);
243
+ }
221
244
 
222
245
  // Functional verification (P1-2 fix: use execFileSync)
223
246
  try {
224
- execFileSync(path.join(binDir, "ca-binary"), ["version"], { stdio: "pipe" });
247
+ execFileSync(caFinal, ["version"], { stdio: "pipe" });
225
248
  console.log("[compound-agent] Functional check passed");
226
249
  } catch {
227
250
  cleanupBinaries(binDir);