@logicseek/hello-cc 0.1.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +26 -4
  3. package/README.zh-CN.md +22 -3
  4. package/bin/hcc.mjs +3 -5622
  5. package/docs/README.md +8 -0
  6. package/docs/README.zh-CN.md +6 -0
  7. package/docs/architecture.md +29 -18
  8. package/docs/architecture.zh-CN.md +20 -10
  9. package/docs/commands.md +16 -5
  10. package/docs/commands.zh-CN.md +16 -5
  11. package/docs/current-state-review-2026-08.md +341 -0
  12. package/docs/defect-review-2026-07.md +382 -0
  13. package/docs/guide.md +53 -5
  14. package/docs/guide.zh-CN.md +46 -3
  15. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +294 -0
  16. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +400 -0
  17. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +284 -0
  18. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +303 -0
  19. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +71 -0
  20. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +117 -0
  21. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +120 -0
  22. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +117 -0
  23. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +104 -0
  24. package/lib/cli/app.mjs +642 -0
  25. package/lib/cli/commands/coordination.mjs +247 -0
  26. package/lib/cli/commands/doctor.mjs +81 -0
  27. package/lib/cli/commands/gc.mjs +852 -0
  28. package/lib/cli/commands/hook.mjs +205 -0
  29. package/lib/cli/commands/install.mjs +294 -0
  30. package/lib/cli/commands/lifecycle.mjs +101 -0
  31. package/lib/cli/commands/lock.mjs +197 -0
  32. package/lib/cli/commands/misc.mjs +61 -0
  33. package/lib/cli/commands/msg.mjs +152 -0
  34. package/lib/cli/commands/peer.mjs +233 -0
  35. package/lib/cli/commands/query.mjs +96 -0
  36. package/lib/cli/commands/run.mjs +463 -0
  37. package/lib/cli/commands/task.mjs +405 -0
  38. package/lib/cli/commands/team.mjs +131 -0
  39. package/lib/cli/commands/tmux.mjs +838 -0
  40. package/lib/cli/commands/up.mjs +135 -0
  41. package/lib/cli-args.mjs +16 -2
  42. package/lib/coordination-state.mjs +112 -51
  43. package/lib/core/coordination/automation.mjs +5 -8
  44. package/lib/core/coordination/clock-safety.mjs +323 -0
  45. package/lib/core/coordination/event-retention.mjs +17 -0
  46. package/lib/core/coordination/gc-plan.mjs +614 -0
  47. package/lib/core/coordination/lease-renewal.mjs +102 -0
  48. package/lib/core/coordination/lock-evidence.mjs +77 -0
  49. package/lib/core/coordination/optimistic-evidence.mjs +26 -0
  50. package/lib/core/coordination/tasks.mjs +156 -36
  51. package/lib/core/peers/evidence-runtime.mjs +312 -0
  52. package/lib/core/peers/evidence.mjs +103 -0
  53. package/lib/core/peers/hook-owner.mjs +32 -0
  54. package/lib/core/peers/liveness.mjs +44 -4
  55. package/lib/core/peers/peer-helpers.mjs +121 -0
  56. package/lib/core/peers/session.mjs +8 -2
  57. package/lib/core/peers/tmux-safety.mjs +290 -0
  58. package/lib/core/sessions/launch.mjs +15 -1
  59. package/lib/db/connection.mjs +104 -0
  60. package/lib/db/events.mjs +34 -0
  61. package/lib/db/migration-backup.mjs +225 -0
  62. package/lib/db/schema.mjs +102 -5
  63. package/lib/db/stores/peers.mjs +34 -0
  64. package/lib/integrations/peers/identity.mjs +62 -16
  65. package/lib/integrations/shims/script.mjs +139 -15
  66. package/lib/process/identity.mjs +192 -0
  67. package/lib/process/pty-lifecycle.mjs +149 -0
  68. package/lib/runtime/buffer-directory-lease.mjs +47 -0
  69. package/lib/runtime/buffer-evidence.mjs +336 -0
  70. package/lib/runtime/buffer-gc-protocol.mjs +167 -0
  71. package/lib/runtime/buffer-gc.mjs +270 -0
  72. package/lib/runtime/client.mjs +61 -8
  73. package/lib/runtime/fatal-shutdown.mjs +54 -0
  74. package/lib/runtime/project-path.mjs +139 -0
  75. package/lib/runtime/projects.mjs +320 -40
  76. package/lib/runtime/state.mjs +232 -29
  77. package/lib/shared/cleanup-error.mjs +54 -0
  78. package/lib/shared/clock-grace.mjs +106 -0
  79. package/lib/shared/errors.mjs +9 -0
  80. package/lib/shared/file-lock.mjs +370 -0
  81. package/lib/shared/json-file.mjs +6 -2
  82. package/lib/shared/redact.mjs +60 -0
  83. package/lib/shared/socket-lock-worker.mjs +201 -0
  84. package/lib/terminal/tmux.mjs +78 -4
  85. package/lib/tmux.mjs +4 -0
  86. package/lib/ui/help.mjs +53 -15
  87. package/lib/web/api-version.mjs +42 -0
  88. package/lib/web/auto-attach.mjs +185 -0
  89. package/lib/web/buffer-gc-runtime.mjs +175 -0
  90. package/lib/web/cookie-auth.mjs +119 -0
  91. package/lib/web/external-sessions.mjs +254 -0
  92. package/lib/web/http-routes.mjs +542 -0
  93. package/lib/web/http.mjs +143 -7
  94. package/lib/web/liveness-reaper.mjs +145 -0
  95. package/lib/web/peer-actions.mjs +132 -23
  96. package/lib/web/project-contexts.mjs +178 -0
  97. package/lib/web/pty-sessions.mjs +203 -0
  98. package/lib/web/runtime-main.mjs +714 -0
  99. package/lib/web/runtime.mjs +91 -36
  100. package/lib/web/session-serialize.mjs +0 -0
  101. package/lib/web/startup.mjs +333 -0
  102. package/lib/web/tls.mjs +365 -0
  103. package/lib/web/tmux-sessions.mjs +1361 -0
  104. package/lib/web/tmux-stream.mjs +166 -0
  105. package/lib/web/ui-template.mjs +152 -20
  106. package/package.json +5 -2
  107. package/scripts/check-free-identifiers.mjs +290 -0
  108. package/scripts/regression.mjs +5646 -262
@@ -0,0 +1,192 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+
5
+ function hashCommand(command) {
6
+ return createHash('sha256').update(String(command ?? '').trim()).digest('hex');
7
+ }
8
+
9
+ function parseLinuxStat(text) {
10
+ const stat = String(text ?? '');
11
+ const prefix = stat.match(/^\s*\d+\s+\(/);
12
+ if (!prefix) return null;
13
+
14
+ const close = stat.lastIndexOf(')');
15
+ if (close < prefix[0].length - 1 || !/^\s/.test(stat.slice(close + 1))) return null;
16
+
17
+ const fields = stat.slice(close + 1).trim().split(/\s+/);
18
+ const state = fields[0];
19
+ const startTicks = fields[19];
20
+ if (fields.length < 20 || !/^[RSDZTWXxKtPI]$/.test(state) ||
21
+ !/^\d+$/.test(startTicks) || BigInt(startTicks) <= 0n) return null;
22
+ return { state, startTicks };
23
+ }
24
+
25
+ export function parseLinuxStatStartTicks(text) {
26
+ return parseLinuxStat(text)?.startTicks || null;
27
+ }
28
+
29
+ export function parsePsStartIdentity(text) {
30
+ const line = String(text ?? '').trimEnd();
31
+ if (line.includes('\n') || line.includes('\r')) return null;
32
+
33
+ const tab = line.indexOf('\t');
34
+ if (tab < 0) return null;
35
+
36
+ const startToken = line.slice(0, tab).trim();
37
+ const command = line.slice(tab + 1).trim();
38
+ return startToken && command ? { startToken, command } : null;
39
+ }
40
+
41
+ function probeProcess(pid) {
42
+ try {
43
+ process.kill(pid, 0);
44
+ return 'present';
45
+ } catch (error) {
46
+ if (error?.code === 'ESRCH') return 'dead';
47
+ if (error?.code === 'EPERM') return 'present';
48
+ return 'unknown';
49
+ }
50
+ }
51
+
52
+ function linuxObservation(pid) {
53
+ const first = parseLinuxStat(fs.readFileSync(`/proc/${pid}/stat`, 'utf8'));
54
+ if (!first) return { state: 'unknown', identity: null };
55
+ if (['Z', 'X', 'x'].includes(first.state)) return { state: 'dead', identity: null };
56
+ const bootId = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
57
+ const command = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8')
58
+ .split('\0')
59
+ .filter(Boolean)
60
+ .join(' ');
61
+ const confirmed = parseLinuxStat(fs.readFileSync(`/proc/${pid}/stat`, 'utf8'));
62
+
63
+ if (!confirmed) return { state: 'unknown', identity: null };
64
+ if (['Z', 'X', 'x'].includes(confirmed.state)) return { state: 'dead', identity: null };
65
+ if (!bootId || !command.trim() || confirmed.startTicks !== first.startTicks) {
66
+ return { state: 'unknown', identity: null };
67
+ }
68
+ return {
69
+ state: 'live',
70
+ identity: {
71
+ pid,
72
+ startToken: `${bootId}:${first.startTicks}`,
73
+ commandHash: hashCommand(command)
74
+ }
75
+ };
76
+ }
77
+
78
+ function run(command, args, environment) {
79
+ const options = {
80
+ encoding: 'utf8',
81
+ stdio: ['ignore', 'pipe', 'pipe']
82
+ };
83
+ if (environment) options.env = environment;
84
+ return spawnSync(command, args, options);
85
+ }
86
+
87
+ function parseMacBootToken(text) {
88
+ const match = String(text ?? '').match(
89
+ /^\s*\{\s*sec\s*=\s*(\d+)\s*,\s*usec\s*=\s*(\d+)\s*\}/
90
+ );
91
+ if (!match) return null;
92
+
93
+ const seconds = BigInt(match[1]);
94
+ const microseconds = Number(match[2]);
95
+ if (seconds <= 0n || !Number.isInteger(microseconds) ||
96
+ microseconds < 0 || microseconds > 999999) return null;
97
+ return `${seconds}:${microseconds}`;
98
+ }
99
+
100
+ function macIdentity(pid) {
101
+ const boot = run('sysctl', ['-n', 'kern.boottime']);
102
+ if (boot.error || boot.status !== 0) return null;
103
+ const bootToken = parseMacBootToken(boot.stdout);
104
+ if (!bootToken) return null;
105
+
106
+ const psEnvironment = { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' };
107
+ const started = run('ps', ['-p', String(pid), '-o', 'lstart='], psEnvironment);
108
+ const command = run('ps', ['-p', String(pid), '-o', 'command='], psEnvironment);
109
+ const confirmedStarted = run('ps', ['-p', String(pid), '-o', 'lstart='], psEnvironment);
110
+ if (started.error || command.error || confirmedStarted.error) return null;
111
+ if (started.status !== 0 ||
112
+ command.status !== 0 || confirmedStarted.status !== 0) return null;
113
+
114
+ const commandOutput = String(command.stdout ?? '').trim();
115
+ const parsed = parsePsStartIdentity(
116
+ `${String(started.stdout ?? '').trim()}\t${commandOutput}\n`
117
+ );
118
+ const confirmed = parsePsStartIdentity(
119
+ `${String(confirmedStarted.stdout ?? '').trim()}\t${commandOutput}\n`
120
+ );
121
+ if (!parsed || !confirmed || confirmed.startToken !== parsed.startToken) return null;
122
+
123
+ return {
124
+ pid,
125
+ startToken: `${bootToken}:${parsed.startToken}`,
126
+ commandHash: hashCommand(parsed.command)
127
+ };
128
+ }
129
+
130
+ function isCompleteIdentity(value) {
131
+ return Number.isInteger(value?.pid) && value.pid > 0 &&
132
+ typeof value.startToken === 'string' && value.startToken.trim().length > 0 &&
133
+ typeof value.commandHash === 'string' && /^[a-f0-9]{64}$/.test(value.commandHash);
134
+ }
135
+
136
+ export async function waitForLiveProcessIdentity(value, options = {}) {
137
+ const inspect = options.inspect || inspectProcessIdentity;
138
+ const monotonicNow = options.monotonicNow || (() => performance.now());
139
+ const wait = options.sleep || ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
140
+ const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 250));
141
+ const intervalMs = Math.max(1, Number(options.intervalMs ?? 5));
142
+ const deadline = monotonicNow() + timeoutMs;
143
+
144
+ while (true) {
145
+ const observed = inspect(value);
146
+ if (observed?.state === 'dead') return { state: 'dead', identity: null };
147
+ if (observed?.state === 'live' && isCompleteIdentity(observed.identity)) return observed;
148
+
149
+ const remainingMs = deadline - monotonicNow();
150
+ if (remainingMs <= 0) return { state: 'unknown', identity: null };
151
+ await wait(Math.min(intervalMs, remainingMs));
152
+ }
153
+ }
154
+
155
+ export function inspectProcessIdentity(value) {
156
+ let pid;
157
+ try {
158
+ pid = Number(value);
159
+ } catch {
160
+ return { state: 'unknown', identity: null };
161
+ }
162
+ if (!Number.isInteger(pid) || pid <= 0) return { state: 'unknown', identity: null };
163
+
164
+ const initialState = probeProcess(pid);
165
+ if (initialState === 'dead') return { state: 'dead', identity: null };
166
+ if (initialState === 'unknown') return { state: 'unknown', identity: null };
167
+
168
+ let identity = null;
169
+ try {
170
+ if (process.platform === 'linux') {
171
+ const observed = linuxObservation(pid);
172
+ if (observed.state === 'dead') return observed;
173
+ identity = observed.identity;
174
+ }
175
+ else if (process.platform === 'darwin') identity = macIdentity(pid);
176
+ } catch {
177
+ identity = null;
178
+ }
179
+
180
+ const finalState = probeProcess(pid);
181
+ if (finalState === 'dead') return { state: 'dead', identity: null };
182
+ if (finalState === 'unknown' || !isCompleteIdentity(identity)) {
183
+ return { state: 'unknown', identity: null };
184
+ }
185
+ return { state: 'live', identity };
186
+ }
187
+
188
+ export function compareProcessIdentity(stored, current) {
189
+ if (!isCompleteIdentity(stored) || !isCompleteIdentity(current)) return 'unknown';
190
+ return stored.pid === current.pid &&
191
+ stored.startToken === current.startToken ? 'live' : 'dead';
192
+ }
@@ -0,0 +1,149 @@
1
+ import { waitForLiveProcessIdentity } from './identity.mjs';
2
+
3
+ export function trackPtyExit(child) {
4
+ let exitEvent = null;
5
+ let resolveExit;
6
+ const promise = new Promise((resolve) => { resolveExit = resolve; });
7
+
8
+ child.onExit((event) => {
9
+ if (exitEvent !== null) return;
10
+ exitEvent = event;
11
+ resolveExit(event);
12
+ });
13
+
14
+ return {
15
+ promise,
16
+ get event() {
17
+ return exitEvent;
18
+ }
19
+ };
20
+ }
21
+
22
+ export function installPtyTerminationHandlers(child, options = {}) {
23
+ const emitter = options.emitter || process;
24
+ const setTimer = options.setTimer || setTimeout;
25
+ const clearTimer = options.clearTimer || clearTimeout;
26
+ const forceKillMs = Math.max(0, Number(options.forceKillMs ?? 1200));
27
+ let signal = null;
28
+ let forceKillTimer = null;
29
+
30
+ const onTerminate = (requestedSignal) => {
31
+ if (signal !== null) return;
32
+ signal = requestedSignal;
33
+ try { child.kill(requestedSignal === 'SIGTERM' ? 'SIGHUP' : requestedSignal); } catch {}
34
+ forceKillTimer = setTimer(() => {
35
+ try { child.kill('SIGKILL'); } catch {}
36
+ }, forceKillMs);
37
+ forceKillTimer?.unref?.();
38
+ };
39
+ const onSigint = () => onTerminate('SIGINT');
40
+ const onSigterm = () => onTerminate('SIGTERM');
41
+ emitter.once('SIGINT', onSigint);
42
+ emitter.once('SIGTERM', onSigterm);
43
+
44
+ return {
45
+ get signal() {
46
+ return signal;
47
+ },
48
+ dispose() {
49
+ emitter.off('SIGINT', onSigint);
50
+ emitter.off('SIGTERM', onSigterm);
51
+ if (forceKillTimer !== null) clearTimer(forceKillTimer);
52
+ forceKillTimer = null;
53
+ }
54
+ };
55
+ }
56
+
57
+ export function ptyTerminationSignal(wrapperSignal, childSignal) {
58
+ return wrapperSignal || childSignal || null;
59
+ }
60
+
61
+ export function ptyStartupFailureDisposition({ termination, childPid, childIdentity = null }) {
62
+ return {
63
+ status: termination.exited ? 'exited' : 'blocked',
64
+ eventType: termination.exited
65
+ ? 'run.session.exited'
66
+ : 'run.session.termination_unconfirmed',
67
+ preserveEvidence: !termination.exited,
68
+ childPid,
69
+ childIdentity
70
+ };
71
+ }
72
+
73
+ async function waitForTrackedExit(exit, timeoutMs, sleep) {
74
+ if (exit.event !== null) return { exited: true, event: exit.event };
75
+ const timedOut = Symbol('timed-out');
76
+ const result = await Promise.race([
77
+ exit.promise,
78
+ sleep(timeoutMs).then(() => timedOut)
79
+ ]);
80
+ return result === timedOut
81
+ ? { exited: false, event: null }
82
+ : { exited: true, event: result };
83
+ }
84
+
85
+ export async function stopPtyAfterStartupFailure(child, exit, options = {}) {
86
+ const graceMs = Math.max(0, Number(options.graceMs ?? 150));
87
+ const killTimeoutMs = Math.max(0, Number(options.killTimeoutMs ?? 1000));
88
+ const sleep = options.sleep || ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
89
+
90
+ if (exit.event !== null) return { exited: true, event: exit.event };
91
+ try { child.kill('SIGHUP'); } catch {}
92
+ const graceful = await waitForTrackedExit(exit, graceMs, sleep);
93
+ if (graceful.exited) return graceful;
94
+
95
+ try { child.kill('SIGKILL'); } catch {}
96
+ return waitForTrackedExit(exit, killTimeoutMs, sleep);
97
+ }
98
+
99
+ export async function capturePtyStartupEvidence({
100
+ childPid,
101
+ wrapperPid,
102
+ exit,
103
+ waitForIdentity = waitForLiveProcessIdentity,
104
+ timeoutMs = 2000
105
+ }) {
106
+ const identityCapture = Promise.all([
107
+ waitForIdentity(wrapperPid, { timeoutMs }),
108
+ waitForIdentity(childPid, { timeoutMs })
109
+ ]).then(([wrapperEvidence, childEvidence]) => ({
110
+ type: 'identity',
111
+ wrapperEvidence,
112
+ childEvidence
113
+ }));
114
+ const earlyExit = exit.promise.then((exitEvent) => ({ type: 'exit', exitEvent }));
115
+ const first = await Promise.race([identityCapture, earlyExit]);
116
+
117
+ if (first.type === 'exit' || exit.event !== null) {
118
+ return {
119
+ state: 'failed',
120
+ reason: 'child_exited_before_identity',
121
+ exitEvent: first.exitEvent || exit.event
122
+ };
123
+ }
124
+ if (first.wrapperEvidence.state !== 'live' || !first.wrapperEvidence.identity) {
125
+ return {
126
+ state: 'failed',
127
+ reason: 'wrapper_identity_unavailable',
128
+ exitEvent: null,
129
+ childIdentity: first.childEvidence.state === 'live'
130
+ ? first.childEvidence.identity
131
+ : null
132
+ };
133
+ }
134
+ if (first.childEvidence.state === 'dead') {
135
+ return {
136
+ state: 'failed',
137
+ reason: 'child_exited_before_identity',
138
+ exitEvent: null
139
+ };
140
+ }
141
+
142
+ return {
143
+ state: 'ready',
144
+ wrapperIdentity: first.wrapperEvidence.identity,
145
+ childIdentity: first.childEvidence.state === 'live'
146
+ ? first.childEvidence.identity
147
+ : null
148
+ };
149
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { withFileLock } from '../shared/file-lock.mjs';
5
+
6
+ const BUFFER_DIRECTORY_LEASE_NAME = '.hcc-buffer-directory.lease';
7
+
8
+ function canonicalDirectory(value) {
9
+ if (typeof value !== 'string' || value.length === 0) {
10
+ throw new TypeError('buffer directory must be a non-empty path');
11
+ }
12
+ const resolved = path.resolve(value);
13
+ const canonical = fs.realpathSync.native(resolved);
14
+ const stat = fs.statSync(canonical);
15
+ if (!stat.isDirectory()) throw new TypeError(`buffer directory is not a directory: ${resolved}`);
16
+ return canonical;
17
+ }
18
+
19
+ export function bufferDirectoryLeaseTarget(directory) {
20
+ return path.join(canonicalDirectory(directory), BUFFER_DIRECTORY_LEASE_NAME);
21
+ }
22
+
23
+ export function withBufferDirectoryLease(directory, fn, options = {}) {
24
+ if (typeof fn !== 'function') throw new TypeError('buffer directory lease callback must be a function');
25
+ const target = bufferDirectoryLeaseTarget(directory);
26
+ const lockOptions = {
27
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
28
+ ...(options.retryMs === undefined ? {} : { retryMs: options.retryMs }),
29
+ ...(options.nonblocking === undefined ? {} : { nonblocking: options.nonblocking })
30
+ };
31
+ return withFileLock(target, () => fn(path.dirname(target)), lockOptions);
32
+ }
33
+
34
+ export function withBufferDirectoryLeases(directories, fn, options = {}) {
35
+ if (typeof fn !== 'function') throw new TypeError('buffer directory leases callback must be a function');
36
+ const targets = [...new Set((directories || []).map(bufferDirectoryLeaseTarget))].sort();
37
+ const acquire = options.withLease || ((target, nested) => withFileLock(target, nested, {
38
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
39
+ ...(options.retryMs === undefined ? {} : { retryMs: options.retryMs }),
40
+ ...(options.nonblocking === undefined ? {} : { nonblocking: options.nonblocking })
41
+ }));
42
+
43
+ const visit = (index) => index >= targets.length
44
+ ? fn()
45
+ : acquire(targets[index], () => visit(index + 1));
46
+ return visit(0);
47
+ }