@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.
- package/CHANGELOG.md +77 -0
- package/README.md +32 -0
- package/README.zh-CN.md +31 -0
- package/docs/guide.md +58 -0
- package/docs/guide.zh-CN.md +55 -0
- package/lib/cli/app.mjs +2 -0
- package/lib/cli/commands/coordination.mjs +3 -1
- package/lib/cli/commands/gc.mjs +182 -32
- package/lib/cli/commands/misc.mjs +15 -0
- package/lib/cli/commands/peer.mjs +21 -5
- package/lib/process/identity.mjs +30 -0
- package/lib/runtime/buffer-directory-lease.mjs +2 -1
- package/lib/runtime/client.mjs +3 -0
- package/lib/shared/file-lock.mjs +11 -4
- package/lib/shared/socket-lock-worker.mjs +73 -24
- package/lib/terminal/tmux.mjs +53 -23
- package/lib/web/buffer-gc-runtime.mjs +2 -2
- package/lib/web/project-contexts.mjs +6 -1
- package/lib/web/runtime-main.mjs +3 -0
- package/lib/web/startup.mjs +4 -2
- package/package.json +2 -2
- package/scripts/regression.mjs +95 -98
- package/docs/architecture-split-review.md +0 -217
- package/docs/current-state-review-2026-08.md +0 -341
- package/docs/defect-review-2026-07.md +0 -382
- package/docs/layout-split-stack-audit.md +0 -298
- package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +0 -294
- package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +0 -400
- package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +0 -284
- package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +0 -303
- package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +0 -71
- package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +0 -117
- package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +0 -120
- package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +0 -117
- package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +0 -104
package/scripts/regression.mjs
CHANGED
|
@@ -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(() =>
|
|
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(() =>
|
|
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(() =>
|
|
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(() =>
|
|
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(() =>
|
|
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(() =>
|
|
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
|
-
|
|
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([
|
|
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([
|
|
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([
|
|
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
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
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.
|