@logicseek/hello-cc 1.0.0 → 1.0.1

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 (35) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +32 -0
  3. package/README.zh-CN.md +31 -0
  4. package/docs/guide.md +58 -0
  5. package/docs/guide.zh-CN.md +55 -0
  6. package/lib/cli/app.mjs +2 -0
  7. package/lib/cli/commands/coordination.mjs +3 -1
  8. package/lib/cli/commands/gc.mjs +182 -32
  9. package/lib/cli/commands/misc.mjs +15 -0
  10. package/lib/cli/commands/peer.mjs +21 -5
  11. package/lib/process/identity.mjs +30 -0
  12. package/lib/runtime/buffer-directory-lease.mjs +2 -1
  13. package/lib/runtime/client.mjs +3 -0
  14. package/lib/shared/file-lock.mjs +11 -4
  15. package/lib/shared/socket-lock-worker.mjs +73 -24
  16. package/lib/terminal/tmux.mjs +53 -23
  17. package/lib/web/buffer-gc-runtime.mjs +2 -2
  18. package/lib/web/project-contexts.mjs +6 -1
  19. package/lib/web/runtime-main.mjs +3 -0
  20. package/lib/web/startup.mjs +4 -2
  21. package/package.json +2 -2
  22. package/scripts/regression.mjs +95 -98
  23. package/docs/architecture-split-review.md +0 -217
  24. package/docs/current-state-review-2026-08.md +0 -341
  25. package/docs/defect-review-2026-07.md +0 -382
  26. package/docs/layout-split-stack-audit.md +0 -298
  27. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +0 -294
  28. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +0 -400
  29. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +0 -284
  30. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +0 -303
  31. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +0 -71
  32. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +0 -117
  33. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +0 -120
  34. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +0 -117
  35. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +0 -104
@@ -26,6 +26,7 @@ const outDir = path.join(os.tmpdir(), `hcc-reg-out-${testId}`);
26
26
  const tmuxSession = `hcc-reg-${process.pid}`;
27
27
  const tmuxSocketName = `hcc-reg-${testId}`.replace(/[^A-Za-z0-9_-]/g, '-');
28
28
  const secondProjectRoot = path.join(root, 'second-project');
29
+ const TERMINAL_WEBSOCKET_TIMEOUT_MS = 15_000;
29
30
  const realHome = process.env.HOME || os.homedir();
30
31
  const realRegistryFile = path.join(realHome, '.hello-cc', 'projects.json');
31
32
  const realTmuxBin = spawnSync('sh', ['-lc', 'command -v tmux || true'], {
@@ -432,6 +433,13 @@ async function assertTmuxGcPolicy() {
432
433
  managedTmuxSessions.add(rootEventSession);
433
434
  managedTmuxSessions.add(manualSession);
434
435
 
436
+ // Create the one runtime-managed authority before the bulk synthetic tmux
437
+ // fixtures. Leaving the runtime up while those sessions are created lets its
438
+ // five-second orphan scan adopt them and contend with this start on slow CI.
439
+ const eventActivePane = parsePane(hcc(['peer', 'start', eventPeer, '--kind', 'shell', '--', 'bash', '--noprofile', '--norc']));
440
+ const eventActiveSession = run('tmux', ['display-message', '-p', '-t', eventActivePane, '#{session_name}']).trim();
441
+ await stopRuntime();
442
+
435
443
  run('tmux', ['new-session', '-d', '-s', staleSession, '-e', `HCC_ROOT=${root}`, '-c', root, 'bash', '--noprofile', '--norc']);
436
444
  run('tmux', ['new-session', '-d', '-s', peerFilterSession, '-e', `HCC_ROOT=${root}`, '-c', root, 'bash', '--noprofile', '--norc']);
437
445
  run('tmux', ['new-session', '-d', '-s', attachedSession, '-e', `HCC_ROOT=${root}`, '-c', root, 'bash', '--noprofile', '--norc']);
@@ -489,10 +497,27 @@ async function assertTmuxGcPolicy() {
489
497
  insertRuntimeTargetBinding(deadBindingPeer, 'shell', 'tmux-gc-binding-dead-session', deadBindingPane);
490
498
  insertRuntimeTargetBinding(reusedBindingPeer, 'shell', 'tmux-gc-binding-reused-session', reusedBindingPane);
491
499
  insertRuntimeTargetBinding(clientUnknownBindingPeer, 'shell', 'tmux-gc-binding-client-unknown-session', clientUnknownBindingPane);
492
- const eventActivePane = parsePane(hcc(['peer', 'start', eventPeer, '--kind', 'shell', '--', 'bash', '--noprofile', '--norc']));
493
- const eventActiveSession = run('tmux', ['display-message', '-p', '-t', eventActivePane, '#{session_name}']).trim();
494
500
  withMeshDb((db) => {
495
501
  const t = Math.floor(Date.now() / 1000) - 30 * 86400;
502
+ for (const [peer, pane] of [
503
+ [stalePeer, stalePane],
504
+ [peerFilterPeer, peerFilterPane],
505
+ [attachedPeer, attachedPane],
506
+ [deadBindingPeer, deadBindingPane],
507
+ [reusedBindingPeer, reusedBindingPane],
508
+ [clientUnknownBindingPeer, clientUnknownBindingPane]
509
+ ]) {
510
+ db.prepare(`
511
+ UPDATE peer_bindings
512
+ SET transport = 'tmux', runtime_target = ?, updated_at = ?
513
+ WHERE peer = ?
514
+ `).run(pane, t, peer);
515
+ }
516
+ db.prepare(`
517
+ UPDATE peer_bindings
518
+ SET transport = 'tmux', runtime_target = ?
519
+ WHERE peer = ?
520
+ `).run(eventActivePane, eventPeer);
496
521
  db.prepare('UPDATE peers SET last_seen_at = ? WHERE id IN (?, ?, ?)').run(t, stalePeer, peerFilterPeer, attachedPeer);
497
522
  db.prepare("UPDATE peers SET status = 'exited' WHERE id IN (?, ?)").run(stalePeer, peerFilterPeer);
498
523
  db.prepare('UPDATE peer_bindings SET updated_at = ? WHERE peer IN (?, ?, ?)').run(t, stalePeer, peerFilterPeer, attachedPeer);
@@ -1582,7 +1607,10 @@ async function expectWebSocketMarker(peer, marker) {
1582
1607
  let sawSnapshot = false;
1583
1608
  let sawMarker = false;
1584
1609
  const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1585
- const timer = setTimeout(() => reject(new Error(`${peer} websocket timeout`)), 5000);
1610
+ const timer = setTimeout(() => {
1611
+ try { ws.terminate(); } catch {}
1612
+ reject(new Error(`${peer} websocket timeout`));
1613
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1586
1614
  ws.on('open', () => {
1587
1615
  const result = hccMaybe(['inject', peer, `echo ${marker}`]);
1588
1616
  if (result.status !== 0) reject(new Error(result.stderr || result.stdout || 'inject failed'));
@@ -1599,14 +1627,17 @@ async function expectWebSocketMarker(peer, marker) {
1599
1627
  resolve();
1600
1628
  }
1601
1629
  });
1602
- ws.on('error', reject);
1630
+ ws.on('error', (err) => { clearTimeout(timer); reject(err); });
1603
1631
  });
1604
1632
  }
1605
1633
 
1606
1634
  async function openTerminalWebSocket(peer) {
1607
1635
  return new Promise((resolve, reject) => {
1608
1636
  const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1609
- const timer = setTimeout(() => reject(new Error(`${peer} websocket open timeout`)), 5000);
1637
+ const timer = setTimeout(() => {
1638
+ try { ws.terminate(); } catch {}
1639
+ reject(new Error(`${peer} websocket open timeout`));
1640
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1610
1641
  ws.on('open', () => {
1611
1642
  clearTimeout(timer);
1612
1643
  resolve(ws);
@@ -1627,7 +1658,10 @@ async function openSessionActionChannel(peer, params = {}) {
1627
1658
  if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
1628
1659
  }
1629
1660
  const ws = new WebSocket(url, runtimeWsOptions());
1630
- const timer = setTimeout(() => reject(new Error(`${peer} action token fetch timeout`)), 5000);
1661
+ const timer = setTimeout(() => {
1662
+ try { ws.terminate(); } catch {}
1663
+ reject(new Error(`${peer} action token fetch timeout`));
1664
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1631
1665
  ws.on('message', (raw) => {
1632
1666
  const msg = JSON.parse(String(raw));
1633
1667
  if (msg.type === 'snapshot') {
@@ -1668,7 +1702,10 @@ async function fetchTerminalSnapshot(peer, params = {}) {
1668
1702
  if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
1669
1703
  }
1670
1704
  const ws = new WebSocket(url, runtimeWsOptions());
1671
- const timer = setTimeout(() => reject(new Error(`${peer} terminal snapshot timeout`)), 5000);
1705
+ const timer = setTimeout(() => {
1706
+ try { ws.terminate(); } catch {}
1707
+ reject(new Error(`${peer} terminal snapshot timeout`));
1708
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1672
1709
  ws.on('message', (raw) => {
1673
1710
  const msg = JSON.parse(String(raw));
1674
1711
  if (msg.type !== 'snapshot') return;
@@ -1886,7 +1923,10 @@ async function expectResizeReplaceSnapshot(peer, marker) {
1886
1923
  await new Promise((resolve, reject) => {
1887
1924
  let sawSnapshot = false;
1888
1925
  const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1889
- const timer = setTimeout(() => reject(new Error(`${peer} resize replace timeout`)), 5000);
1926
+ const timer = setTimeout(() => {
1927
+ try { ws.terminate(); } catch {}
1928
+ reject(new Error(`${peer} resize replace timeout`));
1929
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1890
1930
  ws.on('open', () => {
1891
1931
  const result = hccMaybe(['inject', peer, `echo ${marker}`]);
1892
1932
  if (result.status !== 0) reject(new Error(result.stderr || result.stdout || 'inject failed'));
@@ -1903,7 +1943,7 @@ async function expectResizeReplaceSnapshot(peer, marker) {
1903
1943
  resolve();
1904
1944
  }
1905
1945
  });
1906
- ws.on('error', reject);
1946
+ ws.on('error', (err) => { clearTimeout(timer); reject(err); });
1907
1947
  });
1908
1948
  }
1909
1949
 
@@ -1915,7 +1955,10 @@ async function expectWebSocketInputVisible(peer, marker) {
1915
1955
  let sawFrameAfterInput = false;
1916
1956
  let actionToken = '';
1917
1957
  const ws = new WebSocket(runtimeWsUrl(peer), runtimeWsOptions());
1918
- const timer = setTimeout(() => reject(new Error(`${peer} websocket input visibility timeout`)), 5000);
1958
+ const timer = setTimeout(() => {
1959
+ try { ws.terminate(); } catch {}
1960
+ reject(new Error(`${peer} websocket input visibility timeout`));
1961
+ }, TERMINAL_WEBSOCKET_TIMEOUT_MS);
1919
1962
  ws.on('message', (raw) => {
1920
1963
  const msg = JSON.parse(String(raw));
1921
1964
  const data = String(msg.data || '');
@@ -1938,7 +1981,7 @@ async function expectWebSocketInputVisible(peer, marker) {
1938
1981
  resolve();
1939
1982
  }
1940
1983
  });
1941
- ws.on('error', reject);
1984
+ ws.on('error', (err) => { clearTimeout(timer); reject(err); });
1942
1985
  });
1943
1986
  }
1944
1987
 
@@ -4031,6 +4074,10 @@ async function multiProjectWebWorkflow() {
4031
4074
  !arbitraryRuntime.process_identity?.commandHash) {
4032
4075
  fail(`project API runtime pointer omitted the shared runtime fingerprint:\n${JSON.stringify(arbitraryRuntime, null, 2)}`);
4033
4076
  }
4077
+ const arbitraryBufs = path.join(arbitraryRoot, '.hello-cc', 'bufs');
4078
+ if (fs.existsSync(arbitraryBufs)) {
4079
+ fail('project registration eagerly created the optional buffer directory');
4080
+ }
4034
4081
  fs.rmSync(path.join(arbitraryRoot, '.hello-cc'), { recursive: true, force: true });
4035
4082
  await sleep(1250);
4036
4083
  if (fs.existsSync(path.join(arbitraryRoot, '.hello-cc'))) {
@@ -5277,50 +5324,6 @@ async function statusServer(status) {
5277
5324
  };
5278
5325
  }
5279
5326
 
5280
- async function stalledRuntimeServer(phase) {
5281
- const child = spawn(process.execPath, ['-e', `
5282
- const http = require('node:http');
5283
- const sockets = new Set();
5284
- const server = http.createServer((req, res) => {
5285
- if (${JSON.stringify(phase)} === 'body') {
5286
- res.writeHead(200, { 'Content-Type': 'application/json' });
5287
- res.write('{"deleted":0');
5288
- }
5289
- });
5290
- server.on('connection', (socket) => {
5291
- sockets.add(socket);
5292
- socket.on('close', () => sockets.delete(socket));
5293
- });
5294
- server.listen(0, '127.0.0.1', () => process.stdout.write(String(server.address().port) + '\\n'));
5295
- process.on('SIGTERM', () => {
5296
- for (const socket of sockets) socket.destroy();
5297
- server.close(() => process.exit(0));
5298
- });
5299
- `], { stdio: ['ignore', 'pipe', 'inherit'] });
5300
- const port = await new Promise((resolve, reject) => {
5301
- let output = '';
5302
- const timer = setTimeout(() => reject(new Error(`timed out starting ${phase} stalled runtime`)), 5000);
5303
- child.stdout.on('data', (chunk) => {
5304
- output += chunk;
5305
- const match = output.match(/^(\d+)\n/);
5306
- if (!match) return;
5307
- clearTimeout(timer);
5308
- resolve(Number(match[1]));
5309
- });
5310
- child.once('exit', (code) => {
5311
- clearTimeout(timer);
5312
- reject(new Error(`${phase} stalled runtime exited early with ${code}`));
5313
- });
5314
- });
5315
- return {
5316
- baseUrl: `http://127.0.0.1:${port}`,
5317
- close: async () => {
5318
- child.kill('SIGTERM');
5319
- try { await waitForProcessExit(child.pid, `${phase} stalled runtime exit`, 3000); } catch {}
5320
- }
5321
- };
5322
- }
5323
-
5324
5327
  function bufferGcCanonicalizationRaceWorkflow() {
5325
5328
  for (const phase of ['before', 'after']) {
5326
5329
  const raceRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-realpath-${phase}-${testId}-`));
@@ -5445,7 +5448,10 @@ async function bufferGcArbitrationWorkflow() {
5445
5448
  if (!firstApply.ok || replayApply.ok) fail('buffer GC token was not exactly once');
5446
5449
 
5447
5450
  const cutoffMs = Date.now() - 60_000;
5448
- const oldTime = new Date(cutoffMs - 60_000);
5451
+ // Ordinary deletion checks use a non-zero retention window so crossing a
5452
+ // wall-clock second cannot legitimately activate clock-jump protection.
5453
+ const stableGcRetentionDays = 1;
5454
+ const oldTime = new Date(Date.now() - 2 * 86400_000);
5449
5455
  const rootBufs = path.join(root, '.hello-cc', 'bufs');
5450
5456
  const siblingBufs = path.join(secondProjectRoot, '.hello-cc', 'bufs');
5451
5457
  fs.mkdirSync(rootBufs, { recursive: true });
@@ -5586,7 +5592,9 @@ async function bufferGcArbitrationWorkflow() {
5586
5592
  const parentAlias = path.join(aliasContainer, 'parent');
5587
5593
  fs.symlinkSync(path.dirname(secondProjectRoot), parentAlias);
5588
5594
  const siblingAliasRoot = path.join(parentAlias, path.basename(secondProjectRoot));
5589
- const gcOutput = hccFrom(['--json', 'gc', '--older-than', '0', '--yes'], siblingAliasRoot);
5595
+ const gcOutput = hccFrom([
5596
+ '--json', 'gc', '--older-than', String(stableGcRetentionDays), '--yes'
5597
+ ], siblingAliasRoot);
5590
5598
  fs.rmSync(aliasContainer, { recursive: true, force: true });
5591
5599
  const gcPayload = JSON.parse(gcOutput);
5592
5600
  const result = gcPayload.data || {};
@@ -5609,7 +5617,9 @@ async function bufferGcArbitrationWorkflow() {
5609
5617
  } finally {
5610
5618
  rootGcClockDb.close();
5611
5619
  }
5612
- const rootGcPayload = JSON.parse(hcc(['--json', 'gc', '--older-than', '0', '--yes']));
5620
+ const rootGcPayload = JSON.parse(hcc([
5621
+ '--json', 'gc', '--older-than', String(stableGcRetentionDays), '--yes'
5622
+ ]));
5613
5623
  const rootGcResult = rootGcPayload.data || {};
5614
5624
  if (rootGcResult.buf_files !== 1 || rootGcResult.protected_buf_files < liveFiles.length ||
5615
5625
  rootGcResult.deferred_buf_files < legacyFiles.length || fs.existsSync(rootOrphan)) {
@@ -5735,7 +5745,9 @@ async function bufferGcArbitrationWorkflow() {
5735
5745
  const normalOrphan = path.join(siblingBufs, `gc-actual-dir-normal-${testId}.out`);
5736
5746
  fs.writeFileSync(normalOrphan, 'actual directory normal baseline');
5737
5747
  fs.utimesSync(normalOrphan, oldTime, oldTime);
5738
- const normalGc = JSON.parse(hccFrom(['--json', 'gc', '--older-than', '0', '--yes'], secondProjectRoot)).data;
5748
+ const normalGc = JSON.parse(hccFrom([
5749
+ '--json', 'gc', '--older-than', String(stableGcRetentionDays), '--yes'
5750
+ ], secondProjectRoot)).data;
5739
5751
  if (fs.existsSync(gapOrphan) || fs.existsSync(normalOrphan) || Number(normalGc.buf_files || 0) < 2) {
5740
5752
  fail(`actual runtime buffer directory did not delete against a live baseline: ${JSON.stringify(normalGc)}`);
5741
5753
  }
@@ -5812,42 +5824,6 @@ async function bufferGcArbitrationWorkflow() {
5812
5824
  }
5813
5825
  }
5814
5826
 
5815
- for (const phase of ['headers', 'body']) {
5816
- const isolatedRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-stall-${phase}-${testId}-`));
5817
- const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-stall-home-${phase}-${testId}-`));
5818
- let server = null;
5819
- try {
5820
- const isolatedEnv = { ...env, HOME: isolatedHome };
5821
- run(process.execPath, [hccBin, '--root', isolatedRoot, 'init', '--no-guidance'], { env: isolatedEnv });
5822
- const directory = path.join(isolatedRoot, '.hello-cc', 'bufs');
5823
- fs.mkdirSync(directory, { recursive: true });
5824
- const orphan = path.join(directory, 'old-orphan.out');
5825
- fs.writeFileSync(orphan, 'orphan');
5826
- fs.utimesSync(orphan, oldTime, oldTime);
5827
- server = await stalledRuntimeServer(phase);
5828
- fs.writeFileSync(path.join(isolatedRoot, '.hello-cc', 'runtime.json'), JSON.stringify({
5829
- pid: process.pid,
5830
- base_url: server.baseUrl,
5831
- token: 'regression-token'
5832
- }));
5833
-
5834
- const startedAt = Date.now();
5835
- const gc = run(process.execPath, [hccBin, '--root', isolatedRoot, '--json', 'gc', '--older-than', '0', '--yes'], { env: isolatedEnv });
5836
- const elapsedMs = Date.now() - startedAt;
5837
- const payload = JSON.parse(gc);
5838
- if (elapsedMs >= 9000 ||
5839
- Number(payload.data?.buf_files || 0) !== 0 ||
5840
- Number(payload.data?.deferred_buf_files || 0) < 1 ||
5841
- !fs.existsSync(orphan)) {
5842
- fail(`runtime ${phase} stall did not fail closed within deadline (${elapsedMs}ms): ${gc}`);
5843
- }
5844
- } finally {
5845
- await server?.close();
5846
- fs.rmSync(isolatedRoot, { recursive: true, force: true });
5847
- fs.rmSync(isolatedHome, { recursive: true, force: true });
5848
- }
5849
- }
5850
-
5851
5827
  const localRoot = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-local-${testId}-`));
5852
5828
  const localHome = fs.mkdtempSync(path.join(os.tmpdir(), `hcc-buffer-gc-local-home-${testId}-`));
5853
5829
  try {
@@ -5991,9 +5967,24 @@ async function tmuxBackedStartWorkflow() {
5991
5967
  if (!restoredAuthority?.payload?.tmux_session_created || !restoredAuthority?.payload?.tmux_session_id) {
5992
5968
  fail(`startup auto GC left no complete immutable tmux authority: ${JSON.stringify({ authorityBindingAfterStop, restoredAuthority })}`);
5993
5969
  }
5994
- withMeshDb((db) => db.prepare('UPDATE events SET created_at = ? WHERE id = ?')
5995
- .run(oldAuthorityAt, restoredAuthority.id));
5996
- hcc(['gc', '--older-than', '14', '--yes']);
5970
+ const authorityGcNow = Math.floor(Date.now() / 1000);
5971
+ withMeshDb((db) => {
5972
+ db.prepare('UPDATE events SET created_at = ? WHERE id = ?')
5973
+ .run(oldAuthorityAt, restoredAuthority.id);
5974
+ // Backdating the fixture must not itself look like a real wall-clock jump.
5975
+ // Establish the same current baseline used by the other explicit GC tests.
5976
+ db.prepare("DELETE FROM meta WHERE key IN ('clock_grace_until', 'clock_pending_gap')").run();
5977
+ db.prepare(`
5978
+ INSERT INTO meta(key, value) VALUES ('clock_last_observed_at', ?)
5979
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
5980
+ `).run(String(authorityGcNow));
5981
+ });
5982
+ // This assertion exercises manual history pruning. Request it explicitly
5983
+ // instead of racing the runtime's asynchronous startup auto-GC.
5984
+ const authorityGc = JSON.parse(hcc(['--json', 'gc', '--older-than', '14', '--history', '--yes']));
5985
+ if (authorityGc.data?.deferred_age_based || Number(authorityGc.data?.deferred_old_events || 0) !== 0) {
5986
+ fail(`tmux authority history GC was unexpectedly deferred: ${JSON.stringify(authorityGc.data)}`);
5987
+ }
5997
5988
  const authorityRowsAfterFullGc = withMeshDb((db) => db.prepare(`
5998
5989
  SELECT id FROM events
5999
5990
  WHERE type = 'tmux.session.attached'
@@ -9726,6 +9717,10 @@ function uninstallWorkflow() {
9726
9717
 
9727
9718
  async function processEvidenceWorkflow() {
9728
9719
  log('process evidence: live, reused, legacy, dead, root mismatch');
9720
+ // These fixtures deliberately backdate peers beyond the production reaper's
9721
+ // grace window. Keep the runtime stopped until the external-adoption cases so
9722
+ // a 30-second scheduler tick cannot turn an evidence snapshot into exited.
9723
+ await stopRuntime();
9729
9724
 
9730
9725
  const createOwnedTask = (peer, title) => {
9731
9726
  const output = hcc(['task', 'create', '--from', 'human', '--to', peer, '--title', title]);
@@ -9831,6 +9826,8 @@ async function processEvidenceWorkflow() {
9831
9826
  cleanupBindingPeers(rootPeer);
9832
9827
  }
9833
9828
 
9829
+ startRuntime();
9830
+ await waitRuntime();
9834
9831
  const externalDir = path.join(root, '.hello-cc', 'bufs');
9835
9832
  const shortExternalId = `evidence-external-short-${testId}`;
9836
9833
  const shortProducer = spawn(process.execPath, [
@@ -1,217 +0,0 @@
1
- # Architecture Split Review
2
-
3
- Date: 2026-06-14
4
-
5
- This review covers the local commits on `master` that are ahead of
6
- `origin/master` after `0.1.5`.
7
-
8
- It is written from the separate review branch
9
- `arch-review/split-layout-from-origin`. The main worktree is intentionally left
10
- untouched because other peers may still be finishing scoped implementation work.
11
-
12
- ## Recommendation
13
-
14
- Keep the current architecture direction. Do not merge the new product-boundary
15
- modules back into `bin/hcc.mjs`, and do not collapse them into another large
16
- generic helper file.
17
-
18
- If the goal is to publish or synchronize soon, keep the current local commit
19
- history and add one or more cleanup commits. Those cleanup commits should focus
20
- on remaining boundary issues and compatibility-shim policy.
21
-
22
- If the goal is clean commit history, rebuild the stack in a separate worktree
23
- from `origin/master`. Do not rewrite the active `master` worktree while peers
24
- are coordinating through it.
25
-
26
- ## What Happened
27
-
28
- The first local split commits were mostly flat helper extraction from
29
- `bin/hcc.mjs` into top-level `lib/*.mjs` files. They reduced the entrypoint size
30
- but did not yet express the product architecture:
31
-
32
- - provider and tmux helpers
33
- - lock and team planning helpers
34
- - peer identity, peer format, and peer binding helpers
35
- - project registry and project context helpers
36
- - handoff, timeline, runtime state, task liveness, automation, and render helpers
37
- - help text, message store, runtime request, task store, task CLI, and
38
- coordination-state helpers
39
-
40
- Later commits introduced `docs/architecture.md` and started moving primary
41
- implementations into product boundaries:
42
-
43
- - `lib/core/coordination`
44
- - `lib/core/peers`
45
- - `lib/core/sessions`
46
- - `lib/db`
47
- - `lib/db/stores`
48
- - `lib/runtime`
49
- - `lib/terminal`
50
- - `lib/web`
51
- - `lib/integrations`
52
- - `lib/release`
53
- - `lib/shared`
54
- - `lib/ui`
55
-
56
- That second phase is the correct direction.
57
-
58
- ## Keep
59
-
60
- These boundaries are worth keeping as the primary implementation shape:
61
-
62
- - `lib/core/coordination/*` for product coordination semantics.
63
- - `lib/core/peers/*` for pure peer semantics.
64
- - `lib/db/schema.mjs` and `lib/db/stores/*` for SQLite schema and stores.
65
- - `lib/runtime/*` for runtime files, runtime client access, project registry,
66
- and project context.
67
- - `lib/terminal/tmux.mjs` for tmux adapter behavior.
68
- - `lib/web/*` for Web runtime, HTTP helpers, peer actions, and UI template.
69
- - `lib/integrations/*` for provider, hook, shim, and process integration
70
- details.
71
- - `lib/release/*` for release metadata and release-note logic.
72
- - `lib/shared/*` only for small dependency-light utilities.
73
- - `lib/ui/*` for text/help/state rendering.
74
-
75
- ## Cleanup Candidates
76
-
77
- The remaining cleanup should be deliberate, not another broad helper extraction.
78
-
79
- ### Top-Level Compatibility Shims
80
-
81
- Many top-level `lib/*.mjs` files are now compatibility re-export shims. Examples
82
- include `lib/tmux.mjs`, `lib/provider-commands.mjs`, `lib/web-runtime.mjs`,
83
- `lib/web-http.mjs`, `lib/web-ui-template.mjs`, `lib/task-store.mjs`,
84
- `lib/messages.mjs`, `lib/locks.mjs`, `lib/db-schema.mjs`, and
85
- `lib/session-launch.mjs`.
86
-
87
- Before publish:
88
-
89
- - Treat the installed CLI commands as the public API. Do not document new
90
- `@logicseek/hello-cc/lib/*` imports as user-facing API.
91
- - `@logicseek/hello-cc@0.1.5` is already published from git head `4969100`, and
92
- that package exposed several top-level `lib/*.mjs` helper paths because the
93
- package includes `lib/` and has no `exports` map.
94
- - Keep already-published top-level `lib/*.mjs` paths as compatibility
95
- re-exports for at least one release cycle unless the project explicitly
96
- chooses a breaking package-surface change.
97
- - For compatibility shims added only in local commits after `0.1.5`, either
98
- remove them before the next publish or document them in the release notes as
99
- compatibility-only and revisit removal before the following release.
100
- - Do not let compatibility shims become the target architecture. Primary
101
- implementations should live under the product-boundary directories.
102
-
103
- Classify top-level shims before release:
104
-
105
- - Group A: already-published or CLI-facing helper paths that stay stable for
106
- this release.
107
- - Group B: migration compatibility paths that re-export moved implementations;
108
- keep only if needed and describe them as compatibility-only deep-import paths.
109
- - Group C: unpublished shims with no internal use and no compatibility value;
110
- remove before publishing.
111
-
112
- ### CLI Boundary
113
-
114
- `bin/hcc.mjs` is smaller but still mixes command dispatch, command handlers, Web
115
- startup, session lifecycle, and terminal/Web orchestration. Future work should
116
- move by command or subsystem boundary, not by helper name.
117
-
118
- Good next boundaries:
119
-
120
- - `lib/cli/args.mjs`
121
- - `lib/cli/context.mjs`
122
- - `lib/cli/dispatch.mjs`
123
- - `lib/cli/commands/*`
124
-
125
- ### Regression Runner
126
-
127
- `scripts/regression.mjs` has grown too large. It should become a runner over
128
- domain-specific files:
129
-
130
- - `scripts/regression/cli.mjs`
131
- - `scripts/regression/db.mjs`
132
- - `scripts/regression/runtime.mjs`
133
- - `scripts/regression/web.mjs`
134
- - `scripts/regression/integrations.mjs`
135
- - `scripts/regression/release.mjs`
136
-
137
- This should be done after the current layout work is stable, because regression
138
- coverage is the safety net for the refactor.
139
-
140
- ### Setup, Discover, Guidance, and Shims
141
-
142
- `lib/setup.mjs`, `lib/discover.mjs`, `lib/guidance.mjs`, and
143
- `lib/shim-script.mjs` still carry mixed responsibilities. Continue splitting
144
- these by product boundary:
145
-
146
- - hooks under `lib/integrations/hooks*`
147
- - shims under `lib/integrations/shims*`
148
- - shell and terminal adapters under `lib/terminal`
149
- - runtime/project concerns under `lib/runtime`
150
- - guidance text generation under `lib/ui` or a dedicated guidance boundary
151
-
152
- ### Handoff Boundary
153
-
154
- `lib/handoff.mjs` is still a top-level helper. Decide whether it belongs in
155
- `lib/core/coordination/handoff.mjs` as product semantics or under
156
- `lib/cli/commands/handoff.mjs` if it is mostly CLI formatting and file listing.
157
-
158
- ## Branch Strategy
159
-
160
- Use one of two paths.
161
-
162
- Path A: publish-oriented cleanup
163
-
164
- 1. Keep current `master` history.
165
- 2. Add focused cleanup commits.
166
- 3. Run full regression, release checks, GitHub release dry-run, and pack dry-run.
167
- 4. Publish/synchronize only after the cleanup decision is complete.
168
-
169
- Path B: clean-history rebuild
170
-
171
- 1. Keep `master` untouched.
172
- 2. Use a separate worktree from `origin/master`.
173
- 3. Rebuild the split as fewer architectural commits:
174
- - architecture docs
175
- - core coordination and peer semantics
176
- - db schema/stores
177
- - runtime/project/session helpers
178
- - terminal adapters
179
- - web runtime and UI
180
- - integrations providers/hooks/shims
181
- - release/shared/ui helpers
182
- - regression restructuring
183
- 4. Run full regression and release checks before replacing the active stack.
184
-
185
- ## Current Decision
186
-
187
- The safer default is Path A: keep current history and add cleanup commits.
188
-
189
- Choose Path B only if the project explicitly values a clean local commit stack
190
- more than the time and risk of rebuilding and revalidating the entire series.
191
-
192
- The current safety refs are:
193
-
194
- - `safety/layout-split-20260614-at-ffc42a7`, protecting the earlier split stack.
195
- - `safety/layout-split-20260614-at-11fd4f6`, protecting the split stack through
196
- `11fd4f6` before this package-surface policy update.
197
-
198
- Because `0.1.5` is already on npm, the next publish that includes the current
199
- local commits must bump the package version. Do not republish `0.1.5`.
200
-
201
- ## Coordination Notes
202
-
203
- Continue finishing any already-started layout lanes that are clearly correcting
204
- the architecture boundary. For example, moving setup hooks and shim
205
- implementation into `lib/integrations` is still part of the same boundary
206
- cleanup, not a new random helper extraction.
207
-
208
- After those active lanes finish, pause new implementation splits and make a
209
- cleanup decision:
210
-
211
- 1. Decide whether this review document should be copied into `master` as a
212
- durable design record.
213
- 2. Decide whether top-level compatibility shims are public for this release.
214
- 3. If shims are kept, document that they are compatibility paths.
215
- 4. If shims are removed, do it before publish and run full pack/release checks.
216
- 5. Do not start broad `bin/hcc.mjs` command extraction or regression runner
217
- splitting until the shim policy is settled.