compound-agent 1.3.2 → 1.3.3

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,11 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [1.3.3] - 2026-02-21
13
+
14
+ ### Changed
15
+
16
+ - **Banner audio**: Rewritten as vaporwave composition with PolyBLEP anti-aliased sawtooth synthesis, biquad filters, Schroeder reverb, and delay. E minor ambiguity resolves to E major at bloom, synced 300ms ahead of the visual climax (neural brain lighting up). Post-animation reverb tail holds 1.8s for natural dissolution.
17
+
12
18
  ## [1.3.2] - 2026-02-21
13
19
 
14
20
  ### Added
15
21
 
16
- - **Banner audio**: Pure TypeScript WAV synthesis plays a rising pentatonic melody during the tendril animation. Cross-platform: `afplay` (macOS), `aplay` (Linux), PowerShell (Windows). Silently skips if player unavailable. Zero dependencies.
22
+ - **Banner audio**: Pure TypeScript WAV synthesis during the tendril animation. Cross-platform: `afplay` (macOS), `aplay` (Linux), PowerShell (Windows). Silently skips if player unavailable. Zero dependencies.
17
23
  - **Test coverage**: 19 new tests for `ca about` command, changelog extraction/escaping, and `--update` doc migration path
18
24
 
19
25
  ### Fixed
package/dist/cli.js CHANGED
@@ -1135,114 +1135,254 @@ function registerCompoundCommands(program2) {
1135
1135
  var _require = createRequire(import.meta.url);
1136
1136
  var _pkg = _require("../package.json");
1137
1137
  var VERSION = _pkg.version;
1138
- var SAMPLE_RATE = 22050;
1139
- var BITS = 16;
1140
- var MAX_AMP = 24576;
1141
- var NOTE = {
1142
- C3: 131,
1143
- E3: 165,
1144
- G3: 196,
1145
- C4: 262,
1146
- E4: 330,
1147
- G4: 392,
1148
- C5: 523,
1149
- E5: 659,
1150
- G5: 784,
1151
- C6: 1047
1138
+ var SR = 44100;
1139
+ var MAX_AMP = 28672;
1140
+ var E2 = 82.41;
1141
+ var B2 = 123.47;
1142
+ var E3 = 164.81;
1143
+ var B3 = 246.94;
1144
+ var E4 = 329.63;
1145
+ var Fs4 = 369.99;
1146
+ var Gs4 = 415.3;
1147
+ var B4 = 493.88;
1148
+ function polyBlep(phase, dt) {
1149
+ if (phase < dt) {
1150
+ const t = phase / dt;
1151
+ return t + t - t * t - 1;
1152
+ }
1153
+ if (phase > 1 - dt) {
1154
+ const t = (phase - 1) / dt;
1155
+ return t * t + t + t + 1;
1156
+ }
1157
+ return 0;
1158
+ }
1159
+ function sawBL(freq, t, offset = 0) {
1160
+ const dt = freq / SR;
1161
+ const phase = ((freq * t + offset) % 1 + 1) % 1;
1162
+ return 2 * phase - 1 - polyBlep(phase, dt);
1163
+ }
1164
+ function wideSaw(freq, t, cents = 10) {
1165
+ const r = Math.pow(2, cents / 1200);
1166
+ return (sawBL(freq, t, 0) + sawBL(freq * r, t, 0.33) + sawBL(freq / r, t, 0.66)) / 3;
1167
+ }
1168
+ var BiquadLPF = class {
1169
+ constructor(Q = 0.707) {
1170
+ this.Q = Q;
1171
+ }
1172
+ x1 = 0;
1173
+ x2 = 0;
1174
+ y1 = 0;
1175
+ y2 = 0;
1176
+ b0 = 0;
1177
+ b1 = 0;
1178
+ b2 = 0;
1179
+ a1 = 0;
1180
+ a2 = 0;
1181
+ lastCut = -1;
1182
+ process(x, cutoff) {
1183
+ if (cutoff !== this.lastCut) {
1184
+ this.lastCut = cutoff;
1185
+ const w0 = 2 * Math.PI * Math.min(cutoff, SR * 0.45) / SR;
1186
+ const sin0 = Math.sin(w0), cos0 = Math.cos(w0);
1187
+ const alpha = sin0 / (2 * this.Q);
1188
+ const a0 = 1 + alpha;
1189
+ this.b0 = (1 - cos0) / 2 / a0;
1190
+ this.b1 = (1 - cos0) / a0;
1191
+ this.b2 = this.b0;
1192
+ this.a1 = -2 * cos0 / a0;
1193
+ this.a2 = (1 - alpha) / a0;
1194
+ }
1195
+ const y = this.b0 * x + this.b1 * this.x1 + this.b2 * this.x2 - this.a1 * this.y1 - this.a2 * this.y2;
1196
+ this.x2 = this.x1;
1197
+ this.x1 = x;
1198
+ this.y2 = this.y1;
1199
+ this.y1 = y;
1200
+ return y;
1201
+ }
1152
1202
  };
1153
- function envelope(i, total, attack, release) {
1154
- if (i < attack) return i / attack;
1155
- if (i > total - release) return (total - i) / release;
1156
- return 1;
1157
- }
1158
- function tone(freq, durationMs, amp = 1) {
1159
- const samples = Math.floor(SAMPLE_RATE * durationMs / 1e3);
1160
- const attack = Math.min(Math.floor(samples * 0.05), 200);
1161
- const release = Math.min(Math.floor(samples * 0.15), 400);
1203
+ function applyDelay(samples, ms, fb, wet, lpHz = 3e3) {
1204
+ const len = Math.floor(SR * ms / 1e3);
1205
+ const buf = new Float64Array(len);
1206
+ const lpf = new BiquadLPF();
1162
1207
  const out2 = [];
1163
- for (let i = 0; i < samples; i++) {
1164
- const env = envelope(i, samples, attack, release);
1165
- out2.push(Math.sin(2 * Math.PI * freq * i / SAMPLE_RATE) * env * amp);
1208
+ let i = 0;
1209
+ for (const s of samples) {
1210
+ const d = buf[i];
1211
+ buf[i] = s + lpf.process(d, lpHz) * fb;
1212
+ i = (i + 1) % len;
1213
+ out2.push(s + d * wet);
1166
1214
  }
1167
1215
  return out2;
1168
1216
  }
1169
- function chord(freqs, durationMs, amp = 1) {
1170
- const tones = freqs.map((f) => tone(f, durationMs, 1));
1171
- const len = tones[0].length;
1217
+ function applyReverb(samples, decayMs, wet) {
1218
+ const combDs = [1116, 1188, 1277, 1356, 1422, 1491];
1219
+ const apDs = [225, 341, 556];
1220
+ function comb(input, delay, decay) {
1221
+ const fb = Math.pow(1e-3, delay / (decay * SR / 1e3));
1222
+ const buf = new Float64Array(delay);
1223
+ const damp = new BiquadLPF(0.5);
1224
+ const out3 = [];
1225
+ let i = 0;
1226
+ for (const s of input) {
1227
+ const d = buf[i];
1228
+ buf[i] = s + damp.process(d, 4500) * fb;
1229
+ i = (i + 1) % delay;
1230
+ out3.push(d);
1231
+ }
1232
+ return out3;
1233
+ }
1234
+ function allpass(input, delay, c = 0.5) {
1235
+ const buf = new Float64Array(delay);
1236
+ const out3 = [];
1237
+ let i = 0;
1238
+ for (const s of input) {
1239
+ const d = buf[i];
1240
+ const v = s + d * c;
1241
+ buf[i] = v;
1242
+ i = (i + 1) % delay;
1243
+ out3.push(d - v * c);
1244
+ }
1245
+ return out3;
1246
+ }
1247
+ const pre = Math.floor(SR * 0.04);
1248
+ const delayed = new Array(pre).fill(0);
1249
+ for (const s of samples) delayed.push(s);
1250
+ const combs = combDs.map((d) => comb(delayed, d, decayMs));
1251
+ const sum = new Array(delayed.length).fill(0);
1252
+ for (const c of combs)
1253
+ for (let i = 0; i < sum.length; i++) sum[i] += (c[i] ?? 0) / combDs.length;
1254
+ let ap = sum;
1255
+ for (const d of apDs) ap = allpass(ap, d);
1256
+ let hpY = 0;
1257
+ const hpAlpha = 1 / SR / (1 / (2 * Math.PI * 120) + 1 / SR);
1258
+ const clean = [];
1259
+ for (const s of ap) {
1260
+ hpY += hpAlpha * (s - hpY);
1261
+ clean.push(s - hpY);
1262
+ }
1263
+ const outLen = Math.max(samples.length, clean.length);
1172
1264
  const out2 = [];
1173
- const scale = amp / freqs.length;
1174
- for (let i = 0; i < len; i++) {
1175
- let sum = 0;
1176
- for (const t of tones) sum += t[i];
1177
- out2.push(sum * scale);
1265
+ for (let i = 0; i < outLen; i++) {
1266
+ const dry = i < samples.length ? samples[i] : 0;
1267
+ const w = i < clean.length ? clean[i] : 0;
1268
+ out2.push(dry * (1 - wet * 0.3) + w * wet);
1178
1269
  }
1179
1270
  return out2;
1180
1271
  }
1181
- function silence(durationMs) {
1182
- return new Array(Math.floor(SAMPLE_RATE * durationMs / 1e3)).fill(0);
1183
- }
1184
- function composeMelody() {
1185
- const samples = [];
1186
- samples.push(...tone(NOTE.C3, 250, 0.3));
1187
- samples.push(...silence(150));
1188
- samples.push(...tone(NOTE.C3, 250, 0.35));
1189
- samples.push(...silence(100));
1190
- samples.push(...tone(NOTE.E3, 200, 0.3));
1191
- samples.push(...silence(80));
1192
- samples.push(...tone(NOTE.C4, 140, 0.45));
1193
- samples.push(...silence(30));
1194
- samples.push(...tone(NOTE.E4, 140, 0.5));
1195
- samples.push(...silence(30));
1196
- samples.push(...tone(NOTE.G4, 140, 0.5));
1197
- samples.push(...silence(30));
1198
- samples.push(...tone(NOTE.C5, 160, 0.55));
1199
- samples.push(...silence(30));
1200
- samples.push(...tone(NOTE.E5, 160, 0.55));
1201
- samples.push(...silence(30));
1202
- samples.push(...tone(NOTE.G5, 180, 0.6));
1203
- samples.push(...silence(60));
1204
- samples.push(...tone(NOTE.C4, 100, 0.4));
1205
- samples.push(...tone(NOTE.E4, 100, 0.45));
1206
- samples.push(...tone(NOTE.G4, 100, 0.45));
1207
- samples.push(...tone(NOTE.C5, 120, 0.5));
1208
- samples.push(...tone(NOTE.E5, 120, 0.55));
1209
- samples.push(...tone(NOTE.G5, 140, 0.6));
1210
- samples.push(...silence(60));
1211
- samples.push(...tone(NOTE.C6, 80, 0.3));
1212
- samples.push(...silence(40));
1213
- samples.push(...tone(NOTE.G5, 80, 0.35));
1214
- samples.push(...silence(40));
1215
- samples.push(...tone(NOTE.C6, 80, 0.3));
1216
- samples.push(...silence(40));
1217
- samples.push(...tone(NOTE.E5, 100, 0.35));
1218
- samples.push(...silence(60));
1219
- samples.push(...chord([NOTE.C4, NOTE.E4, NOTE.G4], 600, 0.7));
1220
- samples.push(...chord([NOTE.C4, NOTE.E4, NOTE.G4, NOTE.C5], 500, 0.6));
1221
- samples.push(...chord([NOTE.C3, NOTE.G3, NOTE.C4], 1200, 0.35));
1222
- return samples;
1223
- }
1224
- function encodeWav(samples) {
1225
- const dataSize = samples.length * (BITS / 8);
1226
- const fileSize = 44 + dataSize;
1227
- const buf = Buffer.alloc(fileSize);
1272
+ function expCurve(x, pow = 2) {
1273
+ return Math.pow(Math.max(0, Math.min(1, x)), pow);
1274
+ }
1275
+ function saturate(x) {
1276
+ return Math.tanh(x * 1.15) / Math.tanh(1.15);
1277
+ }
1278
+ function voiceRoot(i, t, f) {
1279
+ const atk = i < SR * 8e-3 ? i / (SR * 8e-3) : 1;
1280
+ const dec = Math.exp(-t * 2.5);
1281
+ const amp = atk * dec * 0.35;
1282
+ if (amp < 1e-3) return 0;
1283
+ const s = wideSaw(E2, t, 6) * 0.7 + Math.sin(2 * Math.PI * B2 * t) * 0.3;
1284
+ return f.process(s, 800 + dec * 1200) * amp;
1285
+ }
1286
+ function voicePad(t, fE, fB) {
1287
+ if (t < 1) return 0;
1288
+ const padT = t - 1;
1289
+ const fadeIn = expCurve(Math.min(1, padT / 1.8));
1290
+ const fadeOut = t < 4 ? 1 : expCurve(1 - (t - 4) / 3.5, 1.5);
1291
+ const amp = fadeIn * fadeOut * 0.2;
1292
+ if (amp < 1e-3) return 0;
1293
+ let cutoff;
1294
+ if (t < 2.7) cutoff = 350 + padT / 1.7 * 850;
1295
+ else if (t < 3.1) cutoff = 1200 + expCurve((t - 2.7) / 0.4) * 2800;
1296
+ else cutoff = 4e3 - expCurve((t - 3.1) / 4, 1.3) * 3200;
1297
+ cutoff *= 1 + 0.04 * Math.sin(2 * Math.PI * 0.11 * t);
1298
+ const dE = 1 + 8e-4 * Math.sin(2 * Math.PI * 0.09 * t);
1299
+ const dB = 1 + 7e-4 * Math.sin(2 * Math.PI * 0.12 * t + 0.5);
1300
+ const e3 = fE.process(wideSaw(E3 * dE, t, 12), cutoff);
1301
+ const b3 = fB.process(wideSaw(B3 * dB, t, 12), cutoff);
1302
+ return (e3 * 0.55 + b3 * 0.45) * amp;
1303
+ }
1304
+ function voiceForeshadow(t, f) {
1305
+ if (t < 2.3 || t >= 3.5) return 0;
1306
+ const fT = t - 2.3;
1307
+ const fadeIn = expCurve(Math.min(1, fT / 0.7));
1308
+ const fadeOut = fT > 0.7 ? expCurve((1.2 - fT) / 0.5) : 1;
1309
+ const amp = fadeIn * fadeOut * 0.07;
1310
+ if (amp < 1e-3) return 0;
1311
+ return f.process(Math.sin(2 * Math.PI * Fs4 * t), 3e3) * amp;
1312
+ }
1313
+ function voiceBloom(t, fE, fG, fB) {
1314
+ if (t < 2.9) return 0;
1315
+ const bT = t - 2.9;
1316
+ const atk = Math.min(1, bT / 0.08);
1317
+ const sus = bT < 1.5 ? 1 : Math.exp(-(bT - 1.5) * 0.8);
1318
+ const amp = atk * sus;
1319
+ if (amp < 2e-3) return 0;
1320
+ const bloomF = bT < 0.3 ? 1500 + expCurve(bT / 0.3) * 3500 : 5e3 - expCurve(Math.min(1, (bT - 0.3) / 4), 1.2) * 3500;
1321
+ const dE4 = 1 + 5e-4 * Math.sin(2 * Math.PI * 0.08 * t);
1322
+ const e4 = fE.process(wideSaw(E4 * dE4, t, 8), bloomF);
1323
+ let gs4 = 0;
1324
+ if (bT > 0.03) {
1325
+ const gsA = Math.min(1, (bT - 0.03) / 0.1);
1326
+ const gsD = 1 + 6e-4 * Math.sin(2 * Math.PI * 0.1 * t + 1);
1327
+ gs4 = fG.process(wideSaw(Gs4 * gsD, t, 9), bloomF) * gsA;
1328
+ }
1329
+ let b4 = 0;
1330
+ if (bT > 0.05 && bT < 3) {
1331
+ const bA = Math.min(1, (bT - 0.05) / 0.15) * (bT > 2 ? expCurve(1 - (bT - 2)) : 1);
1332
+ b4 = fB.process(
1333
+ Math.sin(2 * Math.PI * B4 * t) * 0.5 + wideSaw(B4, t, 6) * 0.5,
1334
+ bloomF
1335
+ ) * bA;
1336
+ }
1337
+ return (e4 * 0.35 + gs4 * 0.35 + b4 * 0.15) * amp * 0.28;
1338
+ }
1339
+ function voiceSub(t) {
1340
+ const subIn = expCurve(Math.min(1, t / 2));
1341
+ const subOut = t < 4.5 ? 1 : expCurve(1 - (t - 4.5) / 2.5, 1.5);
1342
+ return Math.sin(2 * Math.PI * E2 * t) * subIn * subOut * 0.08;
1343
+ }
1344
+ function compose() {
1345
+ const TOTAL = Math.floor(SR * 7.5);
1346
+ const out2 = new Float64Array(TOTAL);
1347
+ const f = [];
1348
+ for (let i = 0; i < 7; i++) f.push(new BiquadLPF(0.6));
1349
+ for (let i = 0; i < TOTAL; i++) {
1350
+ const t = i / SR;
1351
+ out2[i] = saturate(
1352
+ voiceRoot(i, t, f[0]) + voicePad(t, f[1], f[2]) + voiceForeshadow(t, f[3]) + voiceBloom(t, f[4], f[5], f[6]) + voiceSub(t)
1353
+ );
1354
+ }
1355
+ let result = Array.from(out2);
1356
+ result = applyDelay(result, 520, 0.22, 0.18, 2800);
1357
+ result = applyReverb(result, 4800, 0.4);
1358
+ return result;
1359
+ }
1360
+ function encodeWav(raw) {
1361
+ let peak = 0;
1362
+ for (const s of raw) {
1363
+ const a = Math.abs(s);
1364
+ if (a > peak) peak = a;
1365
+ }
1366
+ const scale = peak > 0 ? 0.92 / peak : 1;
1367
+ const dataSize = raw.length * 2;
1368
+ const buf = Buffer.alloc(44 + dataSize);
1228
1369
  buf.write("RIFF", 0);
1229
- buf.writeUInt32LE(fileSize - 8, 4);
1370
+ buf.writeUInt32LE(36 + dataSize, 4);
1230
1371
  buf.write("WAVE", 8);
1231
1372
  buf.write("fmt ", 12);
1232
1373
  buf.writeUInt32LE(16, 16);
1233
1374
  buf.writeUInt16LE(1, 20);
1234
1375
  buf.writeUInt16LE(1, 22);
1235
- buf.writeUInt32LE(SAMPLE_RATE, 24);
1236
- buf.writeUInt32LE(SAMPLE_RATE * BITS / 8, 28);
1237
- buf.writeUInt16LE(BITS / 8, 32);
1238
- buf.writeUInt16LE(BITS, 34);
1376
+ buf.writeUInt32LE(SR, 24);
1377
+ buf.writeUInt32LE(SR * 2, 28);
1378
+ buf.writeUInt16LE(2, 32);
1379
+ buf.writeUInt16LE(16, 34);
1239
1380
  buf.write("data", 36);
1240
1381
  buf.writeUInt32LE(dataSize, 40);
1241
- let offset = 44;
1242
- for (const s of samples) {
1243
- const clamped = Math.max(-1, Math.min(1, s));
1244
- buf.writeInt16LE(Math.round(clamped * MAX_AMP), offset);
1245
- offset += 2;
1382
+ let off = 44;
1383
+ for (const s of raw) {
1384
+ buf.writeInt16LE(Math.round(Math.max(-1, Math.min(1, s * scale)) * MAX_AMP), off);
1385
+ off += 2;
1246
1386
  }
1247
1387
  return buf;
1248
1388
  }
@@ -1267,7 +1407,7 @@ function spawnPlayer(filePath) {
1267
1407
  }
1268
1408
  function playBannerAudio() {
1269
1409
  try {
1270
- const wav = encodeWav(composeMelody());
1410
+ const wav = encodeWav(compose());
1271
1411
  const tmpPath = join(tmpdir(), `ca-banner-${process.pid}.wav`);
1272
1412
  writeFileSync(tmpPath, wav);
1273
1413
  const proc = spawnPlayer(tmpPath);
@@ -1639,10 +1779,13 @@ async function playInstallBanner() {
1639
1779
  await sleep(120);
1640
1780
  }
1641
1781
  } finally {
1642
- audio?.stop();
1643
1782
  process.removeListener("exit", restoreCursor);
1644
1783
  restoreCursor();
1645
1784
  write("\n");
1785
+ if (audio) {
1786
+ await sleep(1800);
1787
+ audio.stop();
1788
+ }
1646
1789
  }
1647
1790
  }
1648
1791
  function checkBeadsAvailable() {
@@ -7532,11 +7675,17 @@ function registerVerifyGatesCommand(program2) {
7532
7675
  }
7533
7676
 
7534
7677
  // src/changelog-data.ts
7535
- var CHANGELOG_RECENT = `## [1.3.2] - 2026-02-21
7678
+ var CHANGELOG_RECENT = `## [1.3.3] - 2026-02-21
7679
+
7680
+ ### Changed
7681
+
7682
+ - **Banner audio**: Rewritten as vaporwave composition with PolyBLEP anti-aliased sawtooth synthesis, biquad filters, Schroeder reverb, and delay. E minor ambiguity resolves to E major at bloom, synced 300ms ahead of the visual climax (neural brain lighting up). Post-animation reverb tail holds 1.8s for natural dissolution.
7683
+
7684
+ ## [1.3.2] - 2026-02-21
7536
7685
 
7537
7686
  ### Added
7538
7687
 
7539
- - **Banner audio**: Pure TypeScript WAV synthesis plays a rising pentatonic melody during the tendril animation. Cross-platform: \`afplay\` (macOS), \`aplay\` (Linux), PowerShell (Windows). Silently skips if player unavailable. Zero dependencies.
7688
+ - **Banner audio**: Pure TypeScript WAV synthesis during the tendril animation. Cross-platform: \`afplay\` (macOS), \`aplay\` (Linux), PowerShell (Windows). Silently skips if player unavailable. Zero dependencies.
7540
7689
  - **Test coverage**: 19 new tests for \`ca about\` command, changelog extraction/escaping, and \`--update\` doc migration path
7541
7690
 
7542
7691
  ### Fixed
@@ -7566,47 +7715,7 @@ var CHANGELOG_RECENT = `## [1.3.2] - 2026-02-21
7566
7715
  - **Test-cleaner Phase 3 strengthened**: Adversarial review phase now mandates iteration loop until both reviewers give unconditional approval. Heading, emphasis, and quality criteria updated.
7567
7716
  - **Update hint on upgrade**: When \`ca init\` or \`ca setup\` detects an existing install, displays tip to run with \`--update\` to regenerate managed files
7568
7717
  - **HOW_TO_COMPOUND.md migration**: \`ca setup --update\` automatically removes old monolithic \`HOW_TO_COMPOUND.md\` if it has version frontmatter (generated by compound-agent)
7569
- - **Doctor doc check**: Now checks for \`docs/compound/README.md\` instead of \`HOW_TO_COMPOUND.md\`
7570
-
7571
- ## [1.3.0] - 2026-02-21
7572
-
7573
- ### Added
7574
-
7575
- - **Setup hardening**: Four new pre-flight checks during \`ca init\` and \`ca setup\`:
7576
- - **Beads CLI check** (\`beads-check.ts\`): Detects if \`bd\` is available, shows install URL if missing (informational, non-blocking)
7577
- - **User-scope detection** (\`scope-check.ts\`): Warns when installing at home directory level where lessons are shared across projects
7578
- - **.gitignore injection** (\`gitignore.ts\`): Ensures \`node_modules/\` and \`.claude/.cache/\` patterns exist in \`.gitignore\`
7579
- - **Upgrade detection** (\`upgrade.ts\`): Detects existing installs and runs migration pipeline (deprecated command removal, header stripping, doc version update)
7580
- - **Upgrade engine**: Automated migration from v1.2.x to v1.3.0:
7581
- - Removes 5 deprecated CLI wrapper commands (\`search.md\`, \`list.md\`, \`show.md\`, \`stats.md\`, \`wrong.md\`)
7582
- - Strips legacy \`<!-- generated by compound-agent -->\` headers from installed files
7583
- - Updates \`HOW_TO_COMPOUND.md\` version during upgrade
7584
- - **\`/compound:research\` skill**: PhD-depth research producing structured survey documents following \`TEMPLATE_FOR_RESEARCH.md\` format
7585
- - **\`/compound:test-clean\` skill**: 5-phase test suite optimization with adversarial review (audit, design, implement, verify, report)
7586
- - **Documentation template**: \`HOW_TO_COMPOUND.md\` deployed to \`docs/compound/\` during setup with version and date placeholders
7587
- - **Test scripts**: \`test:segment\` (run tests for specific module), \`test:random\` (seeded random subset), \`test:critical\` (*.critical.test.ts convention)
7588
- - **3 new doctor checks**: Beads CLI availability, \`.gitignore\` health, usage documentation presence
7589
-
7590
- ### Fixed
7591
-
7592
- - **\`setup --update --dry-run\` no longer mutates files**: \`runUpgrade()\` now accepts a \`dryRun\` parameter propagated to all sub-functions (removeDeprecatedCommands, stripGeneratedHeaders, upgradeDocVersion)
7593
- - **\`setup --uninstall\` respects plugin.json ownership**: Checks \`name === "compound-agent"\` before deleting; user-owned plugin manifests are preserved
7594
- - **Upgrade ownership guard**: \`removeDeprecatedCommands\` checks file content for compound-agent markers before deleting, preventing silent removal of user-authored files with the same name
7595
- - **Malformed settings.json no longer silently clobbered**: On parse error, \`configureClaudeSettings\` warns and skips instead of overwriting with empty config
7596
-
7597
- ### Changed
7598
-
7599
- - **\`setup --update\` overhaul**: Now uses path-based file detection (compound/ = managed) instead of marker-based. Runs upgrade pipeline and \`.gitignore\` remediation during update
7600
- - **JSON-first \`bd\` parsing in loop**: \`jq\` primary with \`python3\` fallback via \`parse_json()\` helper
7601
- - **CLI test helpers hardened**: Replaced shell string interpolation with \`execFileSync\` for safety and reliability
7602
- - **Beads check portable**: Uses POSIX \`command -v\` instead of non-portable \`which\`
7603
- - **Template expansion**: Brainstorm and plan skills now cross-reference researcher skill; 9 total skills, 11 total commands
7604
- - **Code organization**: Extracted display utilities to \`display-utils.ts\`, uninstall logic to \`uninstall.ts\`
7605
-
7606
- ### Removed
7607
-
7608
- - **5 deprecated CLI wrapper commands**: \`search.md\`, \`list.md\`, \`show.md\`, \`stats.md\`, \`wrong.md\` (redundant wrappers around \`npx ca <cmd>\`)
7609
- - **\`GENERATED_MARKER\` on new installs**: New installs use path-based detection; marker retained only for backward-compatible \`--update\` detection`;
7718
+ - **Doctor doc check**: Now checks for \`docs/compound/README.md\` instead of \`HOW_TO_COMPOUND.md\``;
7610
7719
 
7611
7720
  // src/commands/about.ts
7612
7721
  function registerAboutCommand(program2) {