@logicseek/hello-cc 0.1.7 → 0.1.8
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 +49 -0
- package/README.md +12 -6
- package/README.zh-CN.md +10 -6
- package/bin/hcc.mjs +341 -54
- package/docs/commands.md +13 -6
- package/docs/commands.zh-CN.md +11 -6
- package/docs/guide.md +10 -5
- package/docs/guide.zh-CN.md +8 -5
- package/lib/core/coordination/timeline.mjs +1 -0
- package/lib/core/sessions/launch.mjs +1 -0
- package/lib/integrations/shims/script.mjs +42 -22
- package/lib/integrations/shims.mjs +45 -6
- package/lib/setup.mjs +1 -0
- package/lib/shell-path.mjs +31 -16
- package/lib/ui/help.mjs +36 -1
- package/package.json +1 -1
- package/scripts/regression.mjs +401 -48
package/scripts/regression.mjs
CHANGED
|
@@ -38,7 +38,8 @@ const managedTmuxSessions = new Set();
|
|
|
38
38
|
const env = {
|
|
39
39
|
...process.env,
|
|
40
40
|
HOME: home,
|
|
41
|
-
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}
|
|
41
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH || ''}`,
|
|
42
|
+
SHELL: '/bin/bash'
|
|
42
43
|
};
|
|
43
44
|
for (const key of Object.keys(env)) {
|
|
44
45
|
if (key.startsWith('HCC_')) delete env[key];
|
|
@@ -51,6 +52,13 @@ function log(message) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function fail(message) {
|
|
55
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
56
|
+
const escaped = String(message)
|
|
57
|
+
.replace(/%/g, '%25')
|
|
58
|
+
.replace(/\r/g, '%0D')
|
|
59
|
+
.replace(/\n/g, '%0A');
|
|
60
|
+
process.stderr.write(`::error::${escaped}\n`);
|
|
61
|
+
}
|
|
54
62
|
throw new Error(message);
|
|
55
63
|
}
|
|
56
64
|
|
|
@@ -62,6 +70,20 @@ function sh(value) {
|
|
|
62
70
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
function canonicalPath(value) {
|
|
74
|
+
try { return fs.realpathSync(value); }
|
|
75
|
+
catch { return path.resolve(value); }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function samePath(a, b) {
|
|
79
|
+
return canonicalPath(a) === canonicalPath(b);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function statusValue(output, key) {
|
|
83
|
+
const match = String(output || '').match(new RegExp(`^${key}:\\s*(.*)$`, 'm'));
|
|
84
|
+
return match ? match[1].trim() : '';
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
function shortHash(value) {
|
|
66
88
|
return crypto.createHash('sha1').update(String(value)).digest('hex').slice(0, 8);
|
|
67
89
|
}
|
|
@@ -400,7 +422,8 @@ async function assertTmuxGcPolicy() {
|
|
|
400
422
|
}), t);
|
|
401
423
|
});
|
|
402
424
|
|
|
403
|
-
const
|
|
425
|
+
const attachTmux = realTmuxBin ? `${sh(realTmuxBin)} -L ${sh(tmuxSocketName)}` : 'tmux';
|
|
426
|
+
const client = spawnSync('tmux', ['new-session', '-d', '-s', `${tmuxSession}-gc-client`, '-e', `HCC_ROOT=${root}`, 'sh', '-lc', `unset TMUX; exec ${attachTmux} attach-session -t ${sh(attachedSession)}`], {
|
|
404
427
|
env,
|
|
405
428
|
encoding: 'utf8',
|
|
406
429
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
@@ -1023,19 +1046,34 @@ async function waitForFile(file, expected, label = file) {
|
|
|
1023
1046
|
}
|
|
1024
1047
|
|
|
1025
1048
|
async function waitForFileContent(file, expected, label = file) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1049
|
+
const deadline = Date.now() + 10000;
|
|
1050
|
+
let actual = '(missing)';
|
|
1051
|
+
while (Date.now() < deadline) {
|
|
1052
|
+
if (fs.existsSync(file)) {
|
|
1053
|
+
actual = fs.readFileSync(file, 'utf8').trim();
|
|
1054
|
+
if (actual === expected) break;
|
|
1055
|
+
}
|
|
1056
|
+
await sleep(100);
|
|
1057
|
+
}
|
|
1058
|
+
if (actual !== expected) {
|
|
1059
|
+
fail(`timed out waiting for ${label}\nexpected: ${expected}\nactual: ${actual}`);
|
|
1060
|
+
}
|
|
1030
1061
|
ensureFile(file, expected);
|
|
1031
1062
|
}
|
|
1032
1063
|
|
|
1033
1064
|
async function waitForFileLineCount(file, expected, label = file) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1065
|
+
const deadline = Date.now() + 10000;
|
|
1066
|
+
let actual = '(missing)';
|
|
1067
|
+
while (Date.now() < deadline) {
|
|
1068
|
+
if (fs.existsSync(file)) {
|
|
1069
|
+
actual = fs.readFileSync(file, 'utf8').trim();
|
|
1070
|
+
const lines = actual.split('\n').filter(Boolean);
|
|
1071
|
+
if (lines.length === expected) return;
|
|
1072
|
+
}
|
|
1073
|
+
await sleep(100);
|
|
1074
|
+
}
|
|
1075
|
+
const count = actual === '(missing)' ? 0 : actual.split('\n').filter(Boolean).length;
|
|
1076
|
+
fail(`timed out waiting for ${label}\nexpected lines: ${expected}\nactual lines: ${count}\nactual: ${actual}`);
|
|
1039
1077
|
}
|
|
1040
1078
|
|
|
1041
1079
|
async function waitRuntime() {
|
|
@@ -1051,6 +1089,19 @@ async function waitRuntime() {
|
|
|
1051
1089
|
}, 'runtime');
|
|
1052
1090
|
}
|
|
1053
1091
|
|
|
1092
|
+
function processIsAlive(pid) {
|
|
1093
|
+
try {
|
|
1094
|
+
process.kill(pid, 0);
|
|
1095
|
+
return true;
|
|
1096
|
+
} catch {
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function waitForProcessExit(pid, label, timeoutMs = 5000) {
|
|
1102
|
+
await waitFor(() => !processIsAlive(pid), label, timeoutMs);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1054
1105
|
async function expectWebSocketMarker(peer, marker) {
|
|
1055
1106
|
await new Promise((resolve, reject) => {
|
|
1056
1107
|
let sawSnapshot = false;
|
|
@@ -1077,6 +1128,21 @@ async function expectWebSocketMarker(peer, marker) {
|
|
|
1077
1128
|
});
|
|
1078
1129
|
}
|
|
1079
1130
|
|
|
1131
|
+
async function openTerminalWebSocket(peer) {
|
|
1132
|
+
return new Promise((resolve, reject) => {
|
|
1133
|
+
const ws = new WebSocket(runtimeWsUrl(peer));
|
|
1134
|
+
const timer = setTimeout(() => reject(new Error(`${peer} websocket open timeout`)), 5000);
|
|
1135
|
+
ws.on('open', () => {
|
|
1136
|
+
clearTimeout(timer);
|
|
1137
|
+
resolve(ws);
|
|
1138
|
+
});
|
|
1139
|
+
ws.on('error', (err) => {
|
|
1140
|
+
clearTimeout(timer);
|
|
1141
|
+
reject(err);
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1080
1146
|
async function expectResizeReplaceSnapshot(peer, marker) {
|
|
1081
1147
|
await new Promise((resolve, reject) => {
|
|
1082
1148
|
let sawSnapshot = false;
|
|
@@ -1196,17 +1262,20 @@ function startRuntime(options = {}) {
|
|
|
1196
1262
|
|
|
1197
1263
|
async function stopRuntime() {
|
|
1198
1264
|
if (!runtimePid) return;
|
|
1265
|
+
const pid = runtimePid;
|
|
1199
1266
|
hccMaybe(['down']);
|
|
1200
|
-
|
|
1267
|
+
try {
|
|
1268
|
+
await waitForProcessExit(pid, 'runtime process exit', 5000);
|
|
1269
|
+
} catch (err) {
|
|
1270
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
1201
1271
|
try {
|
|
1202
|
-
|
|
1203
|
-
return false;
|
|
1272
|
+
await waitForProcessExit(pid, 'runtime process exit after SIGTERM', 2000);
|
|
1204
1273
|
} catch {
|
|
1205
|
-
|
|
1274
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
1275
|
+
try { await waitForProcessExit(pid, 'runtime process exit after SIGKILL', 2000); } catch {}
|
|
1276
|
+
throw err;
|
|
1206
1277
|
}
|
|
1207
|
-
}
|
|
1208
|
-
try { process.kill(runtimePid, 'SIGTERM'); } catch {}
|
|
1209
|
-
});
|
|
1278
|
+
}
|
|
1210
1279
|
runtimePid = null;
|
|
1211
1280
|
}
|
|
1212
1281
|
|
|
@@ -1315,7 +1384,12 @@ async function setupRegression() {
|
|
|
1315
1384
|
fail(`interactive bash did not keep codex shim first: ${interactiveCodex}`);
|
|
1316
1385
|
}
|
|
1317
1386
|
if (!hcc(['install-hooks', '--status']).includes('claude=yes codex=yes')) fail('hooks not installed');
|
|
1318
|
-
|
|
1387
|
+
const shimStatusOutput = hcc(['shim', 'status']);
|
|
1388
|
+
if (!shimStatusOutput.includes('claude: installed') ||
|
|
1389
|
+
!shimStatusOutput.includes('codex: installed') ||
|
|
1390
|
+
!shimStatusOutput.includes('status: complete')) {
|
|
1391
|
+
fail(`shims not fully installed:\n${shimStatusOutput}`);
|
|
1392
|
+
}
|
|
1319
1393
|
const staleShim = path.join(home, '.hcc-shims', 'claude');
|
|
1320
1394
|
fs.writeFileSync(staleShim, '#!/usr/bin/env bash\necho stale --web-managed\n', { mode: 0o755 });
|
|
1321
1395
|
const ensured = hccMaybe(['shim', 'ensure', 'claude', staleShim]);
|
|
@@ -1326,6 +1400,25 @@ async function setupRegression() {
|
|
|
1326
1400
|
if (!refreshedShim.includes('shim ensure "claude"') || refreshedShim.includes('stale --web-managed')) {
|
|
1327
1401
|
fail(`shim ensure did not refresh stale shim:\n${refreshedShim}`);
|
|
1328
1402
|
}
|
|
1403
|
+
const deadRealBin = path.join(outDir, 'removed-claude');
|
|
1404
|
+
fs.rmSync(deadRealBin, { force: true });
|
|
1405
|
+
fs.writeFileSync(staleShim, [
|
|
1406
|
+
'#!/usr/bin/env bash',
|
|
1407
|
+
'# hello-cc shim for claude (auto-generated; do not edit manually)',
|
|
1408
|
+
`# Real binary: ${deadRealBin}`,
|
|
1409
|
+
`REAL_BIN=${sh(deadRealBin)}`,
|
|
1410
|
+
'exec "$REAL_BIN" "$@"',
|
|
1411
|
+
''
|
|
1412
|
+
].join('\n'), { mode: 0o755 });
|
|
1413
|
+
const rediscovered = hccMaybe(['shim', 'ensure', 'claude', staleShim, deadRealBin]);
|
|
1414
|
+
if (rediscovered.status !== 75) {
|
|
1415
|
+
fail(`shim ensure did not request re-exec for stale real binary:\n${rediscovered.stdout}\n${rediscovered.stderr}`);
|
|
1416
|
+
}
|
|
1417
|
+
const rediscoveredShim = fs.readFileSync(staleShim, 'utf8');
|
|
1418
|
+
const expectedRealBin = path.join(fakeBin, 'claude');
|
|
1419
|
+
if (!rediscoveredShim.includes(`# Real binary: ${expectedRealBin}`) || rediscoveredShim.includes(deadRealBin)) {
|
|
1420
|
+
fail(`shim ensure preserved stale real binary instead of rediscovering PATH binary:\n${rediscoveredShim}`);
|
|
1421
|
+
}
|
|
1329
1422
|
await stopRuntime();
|
|
1330
1423
|
assertPeerBindingUniqueConstraints();
|
|
1331
1424
|
assertLegacySchemaMigration();
|
|
@@ -1417,16 +1510,18 @@ async function setupRegression() {
|
|
|
1417
1510
|
const childDir = path.join(root, 'packages', 'child');
|
|
1418
1511
|
fs.mkdirSync(childDir, { recursive: true });
|
|
1419
1512
|
const childFindRoot = hccFrom(['find-root'], childDir).trim();
|
|
1420
|
-
if (childFindRoot
|
|
1513
|
+
if (!samePath(childFindRoot, childDir)) fail(`child find-root mismatch: ${childFindRoot} !== ${childDir}`);
|
|
1421
1514
|
const childStatus = hccFrom(['status'], childDir);
|
|
1422
|
-
if (!
|
|
1515
|
+
if (!samePath(statusValue(childStatus, 'root'), childDir) ||
|
|
1516
|
+
!samePath(statusValue(childStatus, 'db'), path.join(childDir, '.hello-cc', 'mesh.db'))) {
|
|
1423
1517
|
fail(`child command did not stay on current path:\n${childStatus}`);
|
|
1424
1518
|
}
|
|
1425
1519
|
ensureFile(path.join(childDir, '.hello-cc', 'mesh.db'));
|
|
1426
1520
|
const explicitChildRoot = hccFrom(['--root', root, 'find-root'], childDir).trim();
|
|
1427
|
-
if (explicitChildRoot
|
|
1521
|
+
if (!samePath(explicitChildRoot, root)) fail(`explicit child find-root mismatch: ${explicitChildRoot} !== ${root}`);
|
|
1428
1522
|
const explicitChildStatus = hccFrom(['--root', root, 'status'], childDir);
|
|
1429
|
-
if (!
|
|
1523
|
+
if (!samePath(statusValue(explicitChildStatus, 'root'), root) ||
|
|
1524
|
+
!samePath(statusValue(explicitChildStatus, 'db'), path.join(root, '.hello-cc', 'mesh.db'))) {
|
|
1430
1525
|
fail(`explicit child command did not use requested root:\n${explicitChildStatus}`);
|
|
1431
1526
|
}
|
|
1432
1527
|
const childHookPayload = JSON.stringify({ session_id: 'child-hook-session', cwd: childDir, hook_event_name: 'UserPromptSubmit', prompt: 'status?' });
|
|
@@ -1565,6 +1660,38 @@ async function dbWorkflow() {
|
|
|
1565
1660
|
hcc(['task', 'done', '--peer', 'batch-b', '--id', String(task.id), '--summary', 'batch next cleanup']);
|
|
1566
1661
|
}
|
|
1567
1662
|
hcc(['task', 'update', '--peer', 'human', '--id', batchNextIds[2], '--status', 'abandoned', '--summary', 'batch next leftover cleanup']);
|
|
1663
|
+
|
|
1664
|
+
const dispatched = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'dispatch-a', '--title', 'dispatch new task', '--body', 'dispatch body', '--no-inject']);
|
|
1665
|
+
if (!dispatched.task || dispatched.task.assignee !== 'dispatch-a' || dispatched.task.owner ||
|
|
1666
|
+
dispatched.injected !== false || dispatched.injection_reason !== 'no_inject' || !dispatched.message_id) {
|
|
1667
|
+
fail(`task dispatch did not create message-only assigned task:\n${JSON.stringify(dispatched, null, 2)}`);
|
|
1668
|
+
}
|
|
1669
|
+
const dispatchInbox = hcc(['msg', 'inbox', '--peer', 'dispatch-a', '--all']);
|
|
1670
|
+
if (!dispatchInbox.includes(`task #${dispatched.task.id}`) || !dispatchInbox.includes('dispatch new task')) {
|
|
1671
|
+
fail(`task dispatch durable message missing:\n${dispatchInbox}`);
|
|
1672
|
+
}
|
|
1673
|
+
const dispatchAudit = eventPayloads('task.dispatched', 20)
|
|
1674
|
+
.find((event) => Number(event.payload?.message_id) === Number(dispatched.message_id));
|
|
1675
|
+
if (!dispatchAudit ||
|
|
1676
|
+
dispatchAudit.actor !== 'human' ||
|
|
1677
|
+
dispatchAudit.payload.actor_peer !== 'human' ||
|
|
1678
|
+
dispatchAudit.payload.target_peer !== 'dispatch-a' ||
|
|
1679
|
+
dispatchAudit.payload.source !== 'cli' ||
|
|
1680
|
+
dispatchAudit.payload.injected !== false ||
|
|
1681
|
+
dispatchAudit.payload.injection_reason !== 'no_inject') {
|
|
1682
|
+
fail(`task dispatch audit payload wrong:\n${JSON.stringify(dispatchAudit, null, 2)}`);
|
|
1683
|
+
}
|
|
1684
|
+
const dispatchExistingCreated = hcc(['task', 'create', '--from', 'human', '--title', 'dispatch existing task']);
|
|
1685
|
+
const dispatchExistingMatch = dispatchExistingCreated.match(/created task #(\d+):/);
|
|
1686
|
+
if (!dispatchExistingMatch) fail(`cannot parse dispatch existing task id: ${dispatchExistingCreated}`);
|
|
1687
|
+
const dispatchExisting = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'dispatch-b', '--id', dispatchExistingMatch[1], '--message', 'custom dispatch existing', '--no-inject']);
|
|
1688
|
+
if (String(dispatchExisting.task?.id) !== dispatchExistingMatch[1] ||
|
|
1689
|
+
dispatchExisting.task.assignee !== 'dispatch-b' ||
|
|
1690
|
+
dispatchExisting.message !== 'custom dispatch existing' ||
|
|
1691
|
+
dispatchExisting.injected !== false) {
|
|
1692
|
+
fail(`task dispatch --id did not assign existing task:\n${JSON.stringify(dispatchExisting, null, 2)}`);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1568
1695
|
const takeoverOpen = hcc(['task', 'create', '--from', 'human', '--to', 'takeover-owner', '--title', 'takeover open']);
|
|
1569
1696
|
const takeoverOpenMatch = takeoverOpen.match(/created task #(\d+):/);
|
|
1570
1697
|
if (!takeoverOpenMatch) fail(`cannot parse takeover open id: ${takeoverOpen}`);
|
|
@@ -1846,8 +1973,9 @@ async function multiProjectWebWorkflow() {
|
|
|
1846
1973
|
fs.mkdirSync(otherRoot, { recursive: true });
|
|
1847
1974
|
const output = hccFrom(['web', '--local', '--port', String(port), '--no-discover', '--no-guidance'], otherRoot);
|
|
1848
1975
|
if (!output.includes('web already running in background')) fail(`second project did not reuse global web:\n${output}`);
|
|
1849
|
-
|
|
1850
|
-
if (!
|
|
1976
|
+
const outputRoot = statusValue(output, 'project');
|
|
1977
|
+
if (!samePath(outputRoot, otherRoot)) fail(`second project output did not show its root:\n${output}`);
|
|
1978
|
+
if (!output.includes(encodeURIComponent(outputRoot))) fail(`second project URL did not include project query:\n${output}`);
|
|
1851
1979
|
|
|
1852
1980
|
const otherRuntimeFile = path.join(otherRoot, '.hello-cc', 'runtime.json');
|
|
1853
1981
|
ensureFile(otherRuntimeFile);
|
|
@@ -1859,8 +1987,9 @@ async function multiProjectWebWorkflow() {
|
|
|
1859
1987
|
const projectsResponse = await runtimeFetch('/api/projects', {}, { root: otherRoot });
|
|
1860
1988
|
const projects = await projectsResponse.json();
|
|
1861
1989
|
if (!projectsResponse.ok) fail(`projects API failed: ${JSON.stringify(projects)}`);
|
|
1862
|
-
const roots =
|
|
1863
|
-
if (!roots.
|
|
1990
|
+
const roots = (projects.projects || []).map((p) => p.root);
|
|
1991
|
+
if (!roots.some((projectRoot) => samePath(projectRoot, root)) ||
|
|
1992
|
+
!roots.some((projectRoot) => samePath(projectRoot, otherRoot))) {
|
|
1864
1993
|
fail(`projects API did not include both roots:\n${JSON.stringify(projects, null, 2)}`);
|
|
1865
1994
|
}
|
|
1866
1995
|
|
|
@@ -2305,11 +2434,18 @@ async function tmuxBackedStartWorkflow() {
|
|
|
2305
2434
|
await expectWebSocketInputVisible('shell-a', 'WS_INPUT_VISIBLE_OK');
|
|
2306
2435
|
await expectBoundedTmuxStream('tmux-backed FIFO stream');
|
|
2307
2436
|
|
|
2308
|
-
await
|
|
2437
|
+
const heldWs = await openTerminalWebSocket('shell-a');
|
|
2438
|
+
try {
|
|
2439
|
+
await stopRuntime();
|
|
2440
|
+
await waitFor(() => heldWs.readyState === WebSocket.CLOSED || heldWs.readyState === WebSocket.CLOSING, 'runtime websocket close', 2000);
|
|
2441
|
+
} finally {
|
|
2442
|
+
try { heldWs.close(); } catch {}
|
|
2443
|
+
}
|
|
2309
2444
|
run('tmux', ['display-message', '-p', '-t', pane, '#{pane_id}']);
|
|
2310
2445
|
startRuntime();
|
|
2311
2446
|
await waitRuntime();
|
|
2312
|
-
await waitFor(() => hcc(['peer', 'list']).includes('shell-a'), 'tmux-backed peer restore');
|
|
2447
|
+
await waitFor(() => hcc(['peer', 'list']).includes('shell-a'), 'tmux-backed peer restore after websocket shutdown');
|
|
2448
|
+
|
|
2313
2449
|
const restoredFile = path.join(outDir, 'pty-restored-ok');
|
|
2314
2450
|
hcc(['inject', 'shell-a', `echo PTY_RESTORED_OK > ${restoredFile}`]);
|
|
2315
2451
|
await waitForFile(restoredFile, 'PTY_RESTORED_OK', 'tmux restore injection');
|
|
@@ -2471,6 +2607,9 @@ async function shimTmuxWorkflow() {
|
|
|
2471
2607
|
const firstEnvFile = path.join(outDir, 'shim-env-first');
|
|
2472
2608
|
hcc(['inject', peer, `printf '%s\\n' "$HCC_REG_VALUE" > ${firstEnvFile}`]);
|
|
2473
2609
|
await waitForFile(firstEnvFile, 'shim-first', 'shim first env');
|
|
2610
|
+
const shimInternalEnvFile = path.join(outDir, 'shim-internal-env');
|
|
2611
|
+
hcc(['inject', peer, `printf '%s|%s\\n' "\${HCC_SHIM_ENSURED-unset}" "\${HCC_SKIP_SHIM_INSTALL-unset}" > ${shimInternalEnvFile}`]);
|
|
2612
|
+
await waitForFile(shimInternalEnvFile, 'unset|unset', 'shim internal env cleanup');
|
|
2474
2613
|
|
|
2475
2614
|
const restarted = run(shim, ['--resume', 'shim-regression-session'], {
|
|
2476
2615
|
cwd: root,
|
|
@@ -2492,12 +2631,34 @@ async function shimTmuxWorkflow() {
|
|
|
2492
2631
|
HCC_FAKE_STAY_ALIVE: '0',
|
|
2493
2632
|
HCC_FAKE_LOG: exitedLog
|
|
2494
2633
|
};
|
|
2495
|
-
const
|
|
2634
|
+
const exitedFirstResult = runMaybe(shim, ['--resume', exitedResume], { cwd: root, env: exitedEnv });
|
|
2635
|
+
if (exitedFirstResult.status !== 0) {
|
|
2636
|
+
const allShimLines = fs.readFileSync(shim, 'utf8').split('\n');
|
|
2637
|
+
const shimLines = allShimLines
|
|
2638
|
+
.slice(0, 35)
|
|
2639
|
+
.concat(['...'])
|
|
2640
|
+
.concat(allShimLines.slice(139))
|
|
2641
|
+
.map((line, index) => {
|
|
2642
|
+
if (index < 35) return `${index + 1}: ${line}`;
|
|
2643
|
+
if (index === 35) return line;
|
|
2644
|
+
return `${index + 104}: ${line}`;
|
|
2645
|
+
})
|
|
2646
|
+
.join('\n');
|
|
2647
|
+
fail(`${sh([shim, '--resume', exitedResume].join(' '))} failed\n${exitedFirstResult.stdout || ''}${exitedFirstResult.stderr || ''}\nshim excerpt:\n${shimLines}`);
|
|
2648
|
+
}
|
|
2649
|
+
const exitedFirst = exitedFirstResult.stdout || '';
|
|
2496
2650
|
const exitedPeerMatch = exitedFirst.match(/started\s+(\S+)\s+\(/);
|
|
2497
2651
|
if (!exitedPeerMatch) fail(`exited shim did not start a peer:\n${exitedFirst}`);
|
|
2498
2652
|
const exitedPeer = exitedPeerMatch[1];
|
|
2499
|
-
parsePane(exitedFirst);
|
|
2500
|
-
|
|
2653
|
+
const exitedPane = parsePane(exitedFirst);
|
|
2654
|
+
try {
|
|
2655
|
+
await waitForFileLineCount(exitedLog, 1, 'shim exited first provider launch');
|
|
2656
|
+
} catch (err) {
|
|
2657
|
+
const paneCapture = runMaybe('tmux', ['capture-pane', '-p', '-S', '-80', '-t', exitedPane]);
|
|
2658
|
+
const peerList = hccMaybe(['peer', 'list']);
|
|
2659
|
+
const shimHead = fs.readFileSync(shim, 'utf8').split('\n').slice(0, 18).join('\n');
|
|
2660
|
+
fail(`${err.message}\nshim output:\n${exitedFirst}\nshim stderr:\n${exitedFirstResult.stderr || ''}\npeer list:\n${peerList.stdout || ''}${peerList.stderr || ''}\npane ${exitedPane}:\n${paneCapture.stdout || ''}${paneCapture.stderr || ''}\nshim head:\n${shimHead}`);
|
|
2661
|
+
}
|
|
2501
2662
|
const fallbackFile = path.join(outDir, 'shim-exited-fallback');
|
|
2502
2663
|
hcc(['inject', exitedPeer, `printf '%s\\n' fallback > ${sh(fallbackFile)}`]);
|
|
2503
2664
|
await waitForFile(fallbackFile, 'fallback', 'shim exited fallback shell');
|
|
@@ -2546,6 +2707,16 @@ async function askBroadcastWorkflow() {
|
|
|
2546
2707
|
hcc(['ask', 'shell-b', `echo ASK_OK > ${askFile}`, '--from', 'human', '--inject']);
|
|
2547
2708
|
await waitForFile(askFile, 'ASK_OK', 'ask injection');
|
|
2548
2709
|
if (!hcc(['msg', 'inbox', '--peer', 'shell-b', '--all']).includes('ASK_OK')) fail('ask durable message missing');
|
|
2710
|
+
const shellDefaultDispatch = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'shell-b', '--title', 'shell default dispatch']);
|
|
2711
|
+
if (shellDefaultDispatch.injected !== false || shellDefaultDispatch.injection_reason !== 'unsupported_session_kind') {
|
|
2712
|
+
fail(`task dispatch injected default natural-language prompt into shell session:\n${JSON.stringify(shellDefaultDispatch, null, 2)}`);
|
|
2713
|
+
}
|
|
2714
|
+
const dispatchFile = path.join(outDir, 'dispatch-ok');
|
|
2715
|
+
const shellCommandDispatch = hccJson(['task', 'dispatch', '--from', 'human', '--to', 'shell-b', '--title', 'shell command dispatch', '--message', `echo DISPATCH_OK > ${dispatchFile}`, '--force']);
|
|
2716
|
+
if (!shellCommandDispatch.injected || shellCommandDispatch.delivery !== 'message+inject') {
|
|
2717
|
+
fail(`task dispatch did not inject explicit shell-safe message:\n${JSON.stringify(shellCommandDispatch, null, 2)}`);
|
|
2718
|
+
}
|
|
2719
|
+
await waitForFile(dispatchFile, 'DISPATCH_OK', 'task dispatch injection');
|
|
2549
2720
|
const broadcastFile = path.join(outDir, 'broadcast-ok');
|
|
2550
2721
|
hcc(['broadcast', `echo BROADCAST_OK > ${broadcastFile}`, '--from', 'human', '--inject']);
|
|
2551
2722
|
await waitForFile(broadcastFile, 'BROADCAST_OK', 'broadcast injection');
|
|
@@ -2596,8 +2767,8 @@ async function downGcPackWorkflow() {
|
|
|
2596
2767
|
"tags:\n - 'v*'",
|
|
2597
2768
|
'workflow_dispatch:',
|
|
2598
2769
|
'contents: write',
|
|
2599
|
-
'actions/checkout@
|
|
2600
|
-
'actions/setup-node@
|
|
2770
|
+
'actions/checkout@v5',
|
|
2771
|
+
'actions/setup-node@v5',
|
|
2601
2772
|
'node-version: 24',
|
|
2602
2773
|
'npm run release:check',
|
|
2603
2774
|
'npm run release:github:dry-run',
|
|
@@ -2620,16 +2791,23 @@ function oldNameScan() {
|
|
|
2620
2791
|
'ACTIVE_' + 'AGENT_' + 'TTL',
|
|
2621
2792
|
'--' + 'agent\\b'
|
|
2622
2793
|
].join('|');
|
|
2623
|
-
const
|
|
2624
|
-
|
|
2625
|
-
'
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2794
|
+
const oldNameRegex = new RegExp(oldNamePattern);
|
|
2795
|
+
const files = run('git', ['ls-files'])
|
|
2796
|
+
.split('\n')
|
|
2797
|
+
.map((file) => file.trim())
|
|
2798
|
+
.filter(Boolean)
|
|
2799
|
+
.filter((file) => !file.includes('/node_modules/') && !file.endsWith('.tgz'));
|
|
2800
|
+
const matches = [];
|
|
2801
|
+
for (const file of files) {
|
|
2802
|
+
const fullPath = path.join(repoRoot, file);
|
|
2803
|
+
let content = '';
|
|
2804
|
+
try { content = fs.readFileSync(fullPath, 'utf8'); } catch { continue; }
|
|
2805
|
+
const lines = content.split('\n');
|
|
2806
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
2807
|
+
if (oldNameRegex.test(lines[i])) matches.push(`${file}:${i + 1}: ${lines[i]}`);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
if (matches.length) fail(`old names found:\n${matches.join('\n')}`);
|
|
2633
2811
|
}
|
|
2634
2812
|
|
|
2635
2813
|
function identityEnforcementWorkflow() {
|
|
@@ -3132,6 +3310,7 @@ async function syntaxAndHelp() {
|
|
|
3132
3310
|
'function installShims',
|
|
3133
3311
|
'function uninstallShims',
|
|
3134
3312
|
'function verifyShims',
|
|
3313
|
+
'function shimStatus',
|
|
3135
3314
|
'function findRealBinary',
|
|
3136
3315
|
'function fsExists'
|
|
3137
3316
|
]) {
|
|
@@ -3159,7 +3338,8 @@ async function syntaxAndHelp() {
|
|
|
3159
3338
|
}
|
|
3160
3339
|
if (!integrationShimScriptSource.includes('function generateShim') ||
|
|
3161
3340
|
!integrationShimScriptSource.includes('hello-cc shim for ${tool.name}') ||
|
|
3162
|
-
!integrationShimScriptSource.includes('HCC_SHIM_ENSURED')
|
|
3341
|
+
!integrationShimScriptSource.includes('HCC_SHIM_ENSURED') ||
|
|
3342
|
+
!integrationShimScriptSource.includes('HCC_SKIP_SHIM_INSTALL')) {
|
|
3163
3343
|
fail('integrations shim script module is missing expected generateShim wiring');
|
|
3164
3344
|
}
|
|
3165
3345
|
if (!shellPathSource.includes('function installPathEntry') ||
|
|
@@ -3184,7 +3364,7 @@ async function syntaxAndHelp() {
|
|
|
3184
3364
|
if (typeof integrationHooks[name] !== 'function') fail(`integrations hook module missing export: ${name}`);
|
|
3185
3365
|
if (setupModule[name] !== integrationHooks[name]) fail(`setup hook export mismatch: ${name}`);
|
|
3186
3366
|
}
|
|
3187
|
-
for (const name of ['installShims', 'uninstallShims', 'verifyShims', 'findRealBinary']) {
|
|
3367
|
+
for (const name of ['installShims', 'uninstallShims', 'verifyShims', 'shimStatus', 'findRealBinary']) {
|
|
3188
3368
|
if (typeof integrationShims[name] !== 'function') fail(`integrations shim module missing export: ${name}`);
|
|
3189
3369
|
if (setupModule[name] !== integrationShims[name]) fail(`setup shim export mismatch: ${name}`);
|
|
3190
3370
|
}
|
|
@@ -3198,6 +3378,147 @@ async function syntaxAndHelp() {
|
|
|
3198
3378
|
if (typeof shellPath[name] !== 'function') fail(`shell path module missing export: ${name}`);
|
|
3199
3379
|
if (setupModule[name] !== shellPath[name]) fail(`setup shell path export mismatch: ${name}`);
|
|
3200
3380
|
}
|
|
3381
|
+
const bashRc = '# regression rc\n[ -z "$PS1" ] && return\nexport PATH="/late:$PATH"\n';
|
|
3382
|
+
const bashPath = shellPath.insertPathEntry(bashRc, 'bash');
|
|
3383
|
+
if ((bashPath.match(/# hello-cc shims/g) || []).length !== 2 ||
|
|
3384
|
+
bashPath.indexOf('# hello-cc shims (early)') > bashPath.indexOf('[ -z "$PS1" ] && return') ||
|
|
3385
|
+
bashPath.lastIndexOf('# hello-cc shims (final)') < bashPath.indexOf('export PATH="/late:$PATH"')) {
|
|
3386
|
+
fail(`bash shell PATH insertion lost early/final placement:\n${bashPath}`);
|
|
3387
|
+
}
|
|
3388
|
+
const zshPath = shellPath.insertPathEntry('export PATH="$HOME/bin:$PATH"\n', 'zsh');
|
|
3389
|
+
if ((zshPath.match(/# hello-cc shims/g) || []).length !== 1 ||
|
|
3390
|
+
zshPath.includes('# hello-cc shims (early)') ||
|
|
3391
|
+
!zshPath.includes('# hello-cc shims (final)')) {
|
|
3392
|
+
fail(`zsh shell PATH insertion should install one final entry:\n${zshPath}`);
|
|
3393
|
+
}
|
|
3394
|
+
const fishPath = shellPath.insertPathEntry('set -gx PATH $HOME/bin $PATH\n', 'fish');
|
|
3395
|
+
if ((fishPath.match(/# hello-cc shims/g) || []).length !== 1 ||
|
|
3396
|
+
fishPath.includes('# hello-cc shims (early)') ||
|
|
3397
|
+
!fishPath.includes('# hello-cc shims (final)')) {
|
|
3398
|
+
fail(`fish shell PATH insertion should install one final entry:\n${fishPath}`);
|
|
3399
|
+
}
|
|
3400
|
+
const customRc = [
|
|
3401
|
+
'export PATH="$HOME/.hcc-shims:$PATH" # user custom shim dir',
|
|
3402
|
+
shellPath.pathEntryLine('bash', 'final')
|
|
3403
|
+
].join('\n');
|
|
3404
|
+
const cleanedRc = shellPath.removePathEntryLines(customRc);
|
|
3405
|
+
if (!cleanedRc.includes('user custom shim dir') || cleanedRc.includes('# hello-cc shims')) {
|
|
3406
|
+
fail(`shell PATH cleanup removed a non-hello-cc custom line or kept hello-cc line:\n${cleanedRc}`);
|
|
3407
|
+
}
|
|
3408
|
+
const noProviderHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-no-provider-home-'));
|
|
3409
|
+
try {
|
|
3410
|
+
const noProviderEnv = {
|
|
3411
|
+
...env,
|
|
3412
|
+
HOME: noProviderHome,
|
|
3413
|
+
PATH: '/usr/bin:/bin',
|
|
3414
|
+
SHELL: '/bin/bash'
|
|
3415
|
+
};
|
|
3416
|
+
const noProviderInstall = run(process.execPath, [hccBin, '--root', root, 'shim', 'install'], { env: noProviderEnv });
|
|
3417
|
+
if (!noProviderInstall.includes('no shims installed')) {
|
|
3418
|
+
fail(`shim install without providers did not report no shims:\n${noProviderInstall}`);
|
|
3419
|
+
}
|
|
3420
|
+
const noProviderBashrc = path.join(noProviderHome, '.bashrc');
|
|
3421
|
+
if (fs.existsSync(noProviderBashrc) && fs.readFileSync(noProviderBashrc, 'utf8').includes('.hcc-shims')) {
|
|
3422
|
+
fail(`shim install without providers modified shell PATH:\n${fs.readFileSync(noProviderBashrc, 'utf8')}`);
|
|
3423
|
+
}
|
|
3424
|
+
} finally {
|
|
3425
|
+
try { fs.rmSync(noProviderHome, { recursive: true, force: true }); } catch {}
|
|
3426
|
+
}
|
|
3427
|
+
const fishHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-fish-home-'));
|
|
3428
|
+
try {
|
|
3429
|
+
const fishEnv = {
|
|
3430
|
+
...env,
|
|
3431
|
+
HOME: fishHome,
|
|
3432
|
+
SHELL: '/usr/bin/fish'
|
|
3433
|
+
};
|
|
3434
|
+
const fishInstall = run(process.execPath, [hccBin, '--root', root, 'shim', 'install'], { env: fishEnv });
|
|
3435
|
+
const fishRc = path.join(fishHome, '.config', 'fish', 'config.fish');
|
|
3436
|
+
if (!fishInstall.includes('PATH updated') ||
|
|
3437
|
+
!fs.existsSync(fishRc) ||
|
|
3438
|
+
!fs.readFileSync(fishRc, 'utf8').includes('# hello-cc shims (final)')) {
|
|
3439
|
+
fail(`fish shim install did not create config.fish PATH entry:\n${fishInstall}\n${fs.existsSync(fishRc) ? fs.readFileSync(fishRc, 'utf8') : '(missing)'}`);
|
|
3440
|
+
}
|
|
3441
|
+
} finally {
|
|
3442
|
+
try { fs.rmSync(fishHome, { recursive: true, force: true }); } catch {}
|
|
3443
|
+
}
|
|
3444
|
+
const missingRcHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-missing-rc-home-'));
|
|
3445
|
+
try {
|
|
3446
|
+
const missingRcEnv = {
|
|
3447
|
+
...env,
|
|
3448
|
+
HOME: missingRcHome,
|
|
3449
|
+
SHELL: '/bin/bash'
|
|
3450
|
+
};
|
|
3451
|
+
const uninstallOutput = run(process.execPath, [hccBin, '--root', root, 'shim', 'uninstall'], { env: missingRcEnv });
|
|
3452
|
+
if (uninstallOutput.includes('PATH entry not removed') || !uninstallOutput.includes('PATH entry not present')) {
|
|
3453
|
+
fail(`shim uninstall reported missing shell rc as a failure:\n${uninstallOutput}`);
|
|
3454
|
+
}
|
|
3455
|
+
} finally {
|
|
3456
|
+
try { fs.rmSync(missingRcHome, { recursive: true, force: true }); } catch {}
|
|
3457
|
+
}
|
|
3458
|
+
const partialStatusHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-partial-status-home-'));
|
|
3459
|
+
try {
|
|
3460
|
+
const partialStatusDir = path.join(partialStatusHome, '.hcc-shims');
|
|
3461
|
+
fs.mkdirSync(partialStatusDir, { recursive: true });
|
|
3462
|
+
fs.writeFileSync(path.join(partialStatusDir, 'claude'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
|
|
3463
|
+
const partialStatusEnv = {
|
|
3464
|
+
...env,
|
|
3465
|
+
HOME: partialStatusHome,
|
|
3466
|
+
SHELL: '/bin/bash'
|
|
3467
|
+
};
|
|
3468
|
+
const statusOutput = run(process.execPath, [hccBin, '--root', root, 'shim', 'status'], { env: partialStatusEnv });
|
|
3469
|
+
if (!statusOutput.includes('claude: installed') ||
|
|
3470
|
+
!statusOutput.includes('codex: missing') ||
|
|
3471
|
+
!statusOutput.includes('status: partial')) {
|
|
3472
|
+
fail(`shim status did not report partial install per tool:\n${statusOutput}`);
|
|
3473
|
+
}
|
|
3474
|
+
} finally {
|
|
3475
|
+
try { fs.rmSync(partialStatusHome, { recursive: true, force: true }); } catch {}
|
|
3476
|
+
}
|
|
3477
|
+
const partialShimHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-partial-shim-home-'));
|
|
3478
|
+
const partialShimRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-partial-shim-root-'));
|
|
3479
|
+
try {
|
|
3480
|
+
const partialShimDir = path.join(partialShimHome, '.hcc-shims');
|
|
3481
|
+
fs.mkdirSync(partialShimDir, { recursive: true });
|
|
3482
|
+
fs.writeFileSync(path.join(partialShimDir, 'claude'), '#!/usr/bin/env bash\nexit 0\n', { mode: 0o755 });
|
|
3483
|
+
const partialShimEnv = {
|
|
3484
|
+
...env,
|
|
3485
|
+
HOME: partialShimHome,
|
|
3486
|
+
SHELL: '/bin/bash'
|
|
3487
|
+
};
|
|
3488
|
+
run(process.execPath, [hccBin, '--root', partialShimRoot, 'setup', '--quiet'], { env: partialShimEnv });
|
|
3489
|
+
for (const name of ['claude', 'codex']) {
|
|
3490
|
+
if (!fs.existsSync(path.join(partialShimDir, name))) {
|
|
3491
|
+
fail(`setup did not repair partial shim install; missing ${name}`);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
} finally {
|
|
3495
|
+
try { fs.rmSync(partialShimHome, { recursive: true, force: true }); } catch {}
|
|
3496
|
+
try { fs.rmSync(partialShimRoot, { recursive: true, force: true }); } catch {}
|
|
3497
|
+
}
|
|
3498
|
+
const scanHome = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-scan-home-'));
|
|
3499
|
+
const scanRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-scan-root-'));
|
|
3500
|
+
try {
|
|
3501
|
+
const scanRealRoot = fs.realpathSync(scanRoot);
|
|
3502
|
+
const claudeSessionsDir = path.join(scanHome, '.claude', 'sessions');
|
|
3503
|
+
fs.mkdirSync(claudeSessionsDir, { recursive: true });
|
|
3504
|
+
fs.writeFileSync(path.join(claudeSessionsDir, 'scan-realpath.json'), JSON.stringify({
|
|
3505
|
+
pid: process.pid,
|
|
3506
|
+
sessionId: 'scan-realpath-session',
|
|
3507
|
+
cwd: scanRoot,
|
|
3508
|
+
status: 'running'
|
|
3509
|
+
}));
|
|
3510
|
+
const scanEnv = {
|
|
3511
|
+
...env,
|
|
3512
|
+
HOME: scanHome
|
|
3513
|
+
};
|
|
3514
|
+
const scanOutput = run(process.execPath, [hccBin, '--root', scanRealRoot, 'scan'], { env: scanEnv });
|
|
3515
|
+
if (!scanOutput.includes('claude') || !scanOutput.includes('scan-realpath')) {
|
|
3516
|
+
fail(`scan did not match discovered hccRoot through realpath comparison:\n${scanOutput}`);
|
|
3517
|
+
}
|
|
3518
|
+
} finally {
|
|
3519
|
+
try { fs.rmSync(scanHome, { recursive: true, force: true }); } catch {}
|
|
3520
|
+
try { fs.rmSync(scanRoot, { recursive: true, force: true }); } catch {}
|
|
3521
|
+
}
|
|
3201
3522
|
const cliRuntime = await import(path.join(repoRoot, 'lib', 'cli-runtime.mjs'));
|
|
3202
3523
|
for (const name of ['commandPath', 'createContext', 'packageRoot', 'shellCommand', 'tailFile']) {
|
|
3203
3524
|
if (typeof cliRuntime[name] !== 'function') fail(`CLI runtime module missing function: ${name}`);
|
|
@@ -3588,11 +3909,12 @@ async function syntaxAndHelp() {
|
|
|
3588
3909
|
events: [
|
|
3589
3910
|
{ id: 7, type: 'message.sent', actor: 'peer-a', task_id: 2, payload: '{}', created_at: 7 },
|
|
3590
3911
|
{ id: 8, type: 'task.done', actor: 'peer-a', task_id: 2, payload: JSON.stringify({ owner: 'peer-b', summary: 'done summary' }), created_at: 8 },
|
|
3591
|
-
{ id: 9, type: 'web.session.stop_requested', actor: 'web', task_id: null, payload: JSON.stringify({ actor_peer: 'web', target_peer: 'peer-b', peer: 'peer-b' }), created_at: 9 }
|
|
3912
|
+
{ id: 9, type: 'web.session.stop_requested', actor: 'web', task_id: null, payload: JSON.stringify({ actor_peer: 'web', target_peer: 'peer-b', peer: 'peer-b' }), created_at: 9 },
|
|
3913
|
+
{ id: 10, type: 'task.dispatched', actor: 'peer-a', task_id: 2, payload: JSON.stringify({ actor_peer: 'peer-a', target_peer: 'peer-b', peer: 'peer-b', title: 'dispatch timeline' }), created_at: 10 }
|
|
3592
3914
|
]
|
|
3593
3915
|
}, 'peer-b');
|
|
3594
3916
|
const timelineIds = timelineItems.map((item) => item.id);
|
|
3595
|
-
if (JSON.stringify(timelineIds) !== JSON.stringify(['message:2', 'message:3', 'handoff:4', 'task:5', 'event:8', 'event:9'])) {
|
|
3917
|
+
if (JSON.stringify(timelineIds) !== JSON.stringify(['message:2', 'message:3', 'handoff:4', 'task:5', 'event:8', 'event:9', 'event:10'])) {
|
|
3596
3918
|
fail(`timelineFromRows filtering/order changed: ${JSON.stringify(timelineItems)}`);
|
|
3597
3919
|
}
|
|
3598
3920
|
const inbound = timelineItems.find((item) => item.id === 'message:2');
|
|
@@ -5143,12 +5465,25 @@ async function syntaxAndHelp() {
|
|
|
5143
5465
|
if (!taskHelp.includes('task next [--peer ID] [--force]') ||
|
|
5144
5466
|
!taskHelp.includes('[--count N]') ||
|
|
5145
5467
|
!taskHelp.includes('existing claimed/running/review/blocked task') ||
|
|
5468
|
+
!taskHelp.includes('task dispatch --to ID --title TEXT') ||
|
|
5469
|
+
!taskHelp.includes('task dispatch --to ID --id N') ||
|
|
5470
|
+
!taskHelp.includes('message-only') ||
|
|
5471
|
+
!taskHelp.includes('running managed Claude/Codex terminal') ||
|
|
5146
5472
|
!taskHelp.includes('task takeover [--peer ID] --id N --reason TEXT') ||
|
|
5147
5473
|
!taskHelp.includes('[--policy any|blocked|stale|blocked-or-stale]') ||
|
|
5148
5474
|
!taskHelp.includes('task claim [--peer ID] --id N[,N]') ||
|
|
5149
5475
|
!taskHelp.includes('task create --title TEXT --parent N')) {
|
|
5150
5476
|
fail(`task help missing current-task task next semantics:\n${taskHelp}`);
|
|
5151
5477
|
}
|
|
5478
|
+
for (const expected of [
|
|
5479
|
+
"if (sub === 'dispatch') return taskDispatch",
|
|
5480
|
+
'async function taskDispatch(ctx, args)',
|
|
5481
|
+
"addEvent(eventDb, 'task.dispatched'",
|
|
5482
|
+
'function sessionLooksProviderInteractive(session)',
|
|
5483
|
+
'!customMessage && !sessionLooksProviderInteractive(session)'
|
|
5484
|
+
]) {
|
|
5485
|
+
if (!hccSource.includes(expected)) fail(`task dispatch source guard missing: ${expected}`);
|
|
5486
|
+
}
|
|
5152
5487
|
const teamHelp = run(process.execPath, [hccBin, 'team', '--help']);
|
|
5153
5488
|
if (!teamHelp.includes('hcc team') || !teamHelp.includes('team plan') || !teamHelp.includes('team start') || !teamHelp.includes('team status')) {
|
|
5154
5489
|
fail(`team help missing expected content:\n${teamHelp}`);
|
|
@@ -5177,6 +5512,19 @@ async function syntaxAndHelp() {
|
|
|
5177
5512
|
!tmuxHelp.includes('deletion requires --yes')) {
|
|
5178
5513
|
fail(`tmux help missing DB-backed gc semantics:\n${tmuxHelp}`);
|
|
5179
5514
|
}
|
|
5515
|
+
const shimHelp = run(process.execPath, [hccBin, 'shim', '--help']);
|
|
5516
|
+
if (!shimHelp.includes('hcc shim') ||
|
|
5517
|
+
!shimHelp.includes('shim install') ||
|
|
5518
|
+
!shimHelp.includes('shim status') ||
|
|
5519
|
+
!shimHelp.includes('shell PATH entry')) {
|
|
5520
|
+
fail(`shim help missing maintenance command content:\n${shimHelp}`);
|
|
5521
|
+
}
|
|
5522
|
+
const installHooksHelp = run(process.execPath, [hccBin, 'install-hooks', '--help']);
|
|
5523
|
+
if (!installHooksHelp.includes('hcc install-hooks') ||
|
|
5524
|
+
!installHooksHelp.includes('install-hooks --status') ||
|
|
5525
|
+
!installHooksHelp.includes('install-hooks --uninstall')) {
|
|
5526
|
+
fail(`install-hooks help missing maintenance command content:\n${installHooksHelp}`);
|
|
5527
|
+
}
|
|
5180
5528
|
const gcHelp = run(process.execPath, [hccBin, 'gc', '--help']);
|
|
5181
5529
|
if (!gcHelp.includes('hcc gc') ||
|
|
5182
5530
|
!gcHelp.includes('gc [--older-than DAYS] [--yes]') ||
|
|
@@ -5246,11 +5594,16 @@ function uninstallWorkflow() {
|
|
|
5246
5594
|
ensureFile(path.join(uninstallHome, '.claude', 'settings.json'));
|
|
5247
5595
|
ensureFile(path.join(uninstallHome, '.codex', 'hooks.json'));
|
|
5248
5596
|
ensureFile(path.join(uninstallHome, '.hcc-shims', 'claude'));
|
|
5597
|
+
const uninstallBashrc = path.join(uninstallHome, '.bashrc');
|
|
5598
|
+
if (!fs.readFileSync(uninstallBashrc, 'utf8').includes('.hcc-shims')) {
|
|
5599
|
+
fail('setup did not install shim PATH entry before uninstall workflow');
|
|
5600
|
+
}
|
|
5249
5601
|
|
|
5250
5602
|
const kept = run(process.execPath, [hccBin, '--root', uninstallRoot, 'uninstall'], { env: uninstallEnv });
|
|
5251
5603
|
if (!kept.includes('project data kept')) fail(`uninstall did not keep project data by default:\n${kept}`);
|
|
5252
5604
|
if (!fs.existsSync(path.join(uninstallRoot, '.hello-cc', 'mesh.db'))) fail('default uninstall removed project db');
|
|
5253
5605
|
if (fs.existsSync(path.join(uninstallHome, '.hcc-shims', 'claude'))) fail('default uninstall did not remove shim');
|
|
5606
|
+
if (fs.readFileSync(uninstallBashrc, 'utf8').includes('.hcc-shims')) fail('default uninstall did not remove shim PATH entry');
|
|
5254
5607
|
if (!run(process.execPath, [hccBin, '--root', uninstallRoot, 'install-hooks', '--status'], { env: uninstallEnv }).includes('claude=no codex=no')) {
|
|
5255
5608
|
fail('default uninstall did not remove hooks');
|
|
5256
5609
|
}
|