@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,323 @@
1
+ import { tx } from '../../db/schema.mjs';
2
+ import { replaceCleanupFailureRoot } from '../../shared/cleanup-error.mjs';
3
+ import {
4
+ CLOCK_GRACE_SEC,
5
+ readClockGraceUntil,
6
+ writeClockGraceUntil
7
+ } from '../../shared/clock-grace.mjs';
8
+ import { CliError } from '../../shared/errors.mjs';
9
+ import { renewOwnedLocks } from './lease-renewal.mjs';
10
+
11
+ const BACKWARD_TOLERANCE_SEC = 5;
12
+ // CS-01: a gap is only meaningful for a real wall-clock discontinuity. Ordinary
13
+ // forward progress between observations (the 30s reaper tick, on-demand lock
14
+ // ops) must NOT create or extend a gap — otherwise the merged window grows
15
+ // monotonically, every candidate boundary inside it re-crosses on every
16
+ // observation, unknown evidence opens a spurious grace window every 120s, and
17
+ // age-based GC is deferred forever. Steps larger than this threshold (a sleep,
18
+ // an NTP step, or a CLI invoked minutes apart) still produce a gap.
19
+ const FORWARD_GAP_THRESHOLD_SEC = 120;
20
+ const OPERATIONS = new Set(['ownership', 'gc']);
21
+ const EVIDENCE_STATES = new Set(['live', 'dead', 'unknown']);
22
+
23
+ export const CLOCK_SAFETY_PUBLIC_MESSAGE =
24
+ 'Clock safety state could not be persisted; ownership was left unchanged';
25
+
26
+ export function clockSafetyUnavailable(error) {
27
+ const publicError = new CliError(
28
+ 'CLOCK_SAFETY_UNAVAILABLE',
29
+ CLOCK_SAFETY_PUBLIC_MESSAGE
30
+ );
31
+ return replaceCleanupFailureRoot(error, publicError) || publicError;
32
+ }
33
+
34
+ function crossedBoundary(previous, current, boundary) {
35
+ return previous < boundary && current >= boundary;
36
+ }
37
+
38
+ function requireTimestamp(value, name) {
39
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
40
+ throw new TypeError(`${name} must be a non-negative safe integer`);
41
+ }
42
+ return value;
43
+ }
44
+
45
+ function validateCandidates(candidates) {
46
+ if (!Array.isArray(candidates)) {
47
+ throw new TypeError('candidates must be an array');
48
+ }
49
+ return candidates.map((candidate, index) => {
50
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
51
+ throw new TypeError(`candidate ${index} must be an object`);
52
+ }
53
+ const boundary = requireTimestamp(candidate.boundary, `candidate ${index} boundary`);
54
+ if (!EVIDENCE_STATES.has(candidate.evidence)) {
55
+ throw new TypeError(`candidate ${index} evidence must be live, dead, or unknown`);
56
+ }
57
+ return { boundary, evidence: candidate.evidence };
58
+ });
59
+ }
60
+
61
+ function validateGcCutoffs(gcCutoffs) {
62
+ if (!Array.isArray(gcCutoffs)) {
63
+ throw new TypeError('gcCutoffs must be an array');
64
+ }
65
+ return gcCutoffs.map((cutoff, index) => requireTimestamp(cutoff, `gcCutoff ${index}`));
66
+ }
67
+
68
+ export function decideClockSafety(observation) {
69
+ if (!observation || typeof observation !== 'object' || Array.isArray(observation)) {
70
+ throw new TypeError('clock observation must be an object');
71
+ }
72
+
73
+ const { previous, operation } = observation;
74
+ const current = requireTimestamp(observation.current, 'current');
75
+ if (!OPERATIONS.has(operation)) {
76
+ throw new TypeError('operation must be ownership or gc');
77
+ }
78
+ const candidates = validateCandidates(
79
+ observation.candidates === undefined ? [] : observation.candidates
80
+ );
81
+ const gcCutoffs = validateGcCutoffs(
82
+ observation.gcCutoffs === undefined ? [] : observation.gcCutoffs
83
+ );
84
+ const firstObservation = previous === undefined;
85
+ const previousSec = firstObservation ? undefined : requireTimestamp(previous, 'previous');
86
+
87
+ const backward = !firstObservation
88
+ && current < previousSec - BACKWARD_TOLERANCE_SEC;
89
+
90
+ const hasUnknown = candidates.some(({ evidence }) => evidence === 'unknown');
91
+ if (backward && (operation === 'gc' || hasUnknown)) {
92
+ return { enterGrace: true, renewOwners: false, reason: 'clock-backward' };
93
+ }
94
+
95
+ const crossedCandidates = candidates.filter(({ boundary }) => firstObservation
96
+ ? current >= boundary
97
+ : crossedBoundary(previousSec, current, boundary));
98
+ const renewOwners = crossedCandidates.some(({ evidence }) => evidence === 'live');
99
+
100
+ if (crossedCandidates.some(({ evidence }) => evidence === 'unknown')) {
101
+ return {
102
+ enterGrace: true,
103
+ renewOwners,
104
+ reason: firstObservation ? 'first-observation' : 'unknown-evidence'
105
+ };
106
+ }
107
+
108
+ if (operation === 'gc' && gcCutoffs.some((boundary) => (
109
+ firstObservation ? current >= boundary : crossedBoundary(previousSec, current, boundary)
110
+ ))) {
111
+ return {
112
+ enterGrace: true,
113
+ renewOwners: false,
114
+ reason: firstObservation ? 'first-observation' : 'gc-cutoff'
115
+ };
116
+ }
117
+
118
+ if (renewOwners) {
119
+ return { enterGrace: false, renewOwners: true, reason: 'verified-live' };
120
+ }
121
+
122
+ if (crossedCandidates.some(({ evidence }) => evidence === 'dead')) {
123
+ return { enterGrace: false, renewOwners: false, reason: 'verified-dead' };
124
+ }
125
+
126
+ return { enterGrace: false, renewOwners: false, reason: 'no-boundary-crossing' };
127
+ }
128
+
129
+ function readObservedAt(db) {
130
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'clock_last_observed_at'").get();
131
+ if (!row) return undefined;
132
+ if (typeof row.value !== 'string' || !/^(0|[1-9][0-9]*)$/.test(row.value)) {
133
+ throw new Error('invalid persisted clock observation watermark');
134
+ }
135
+ const value = Number(row.value);
136
+ if (!Number.isSafeInteger(value) || value < 0) {
137
+ throw new Error('invalid persisted clock observation watermark');
138
+ }
139
+ return value;
140
+ }
141
+
142
+ function writeObservedAt(db, nowSec) {
143
+ db.prepare(`
144
+ INSERT INTO meta(key, value) VALUES ('clock_last_observed_at', ?)
145
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
146
+ `).run(String(nowSec));
147
+ }
148
+
149
+ function readPendingGap(db) {
150
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'clock_pending_gap'").get();
151
+ if (!row) return null;
152
+ let gap;
153
+ try {
154
+ gap = JSON.parse(row.value);
155
+ } catch {
156
+ throw new Error('invalid persisted pending clock gap');
157
+ }
158
+ if (!gap || typeof gap !== 'object' || Array.isArray(gap) ||
159
+ !Number.isSafeInteger(gap.from) || gap.from < 0 ||
160
+ !Number.isSafeInteger(gap.to) || gap.to < gap.from ||
161
+ typeof gap.backward !== 'boolean' || typeof gap.first !== 'boolean') {
162
+ throw new Error('invalid persisted pending clock gap');
163
+ }
164
+ return { from: gap.from, to: gap.to, backward: gap.backward, first: gap.first };
165
+ }
166
+
167
+ function writePendingGap(db, gap) {
168
+ db.prepare(`
169
+ INSERT INTO meta(key, value) VALUES ('clock_pending_gap', ?)
170
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
171
+ `).run(JSON.stringify(gap));
172
+ }
173
+
174
+ function clearPendingGap(db) {
175
+ db.prepare("DELETE FROM meta WHERE key = 'clock_pending_gap'").run();
176
+ }
177
+
178
+ function clockJumpSpanSec(clockJump) {
179
+ const spanMs = Math.max(
180
+ Number(clockJump?.wallDeltaMs || 0),
181
+ Number(clockJump?.deltaMs || 0),
182
+ Math.abs(Number(clockJump?.driftMs || 0))
183
+ );
184
+ return Number.isFinite(spanMs) ? Math.ceil(spanMs / 1000) : 0;
185
+ }
186
+
187
+ function nextObservationGap(previous, current, clockJump) {
188
+ if (previous === undefined) {
189
+ return { from: 0, to: current, backward: false, first: true };
190
+ }
191
+ if (clockJump?.kind === 'forward') {
192
+ const spanSec = clockJumpSpanSec(clockJump);
193
+ return {
194
+ from: spanSec > 0 ? Math.max(0, current - spanSec) : 0,
195
+ to: current,
196
+ backward: false,
197
+ first: false
198
+ };
199
+ }
200
+ if (clockJump?.kind === 'backward' || current < previous - BACKWARD_TOLERANCE_SEC) {
201
+ return {
202
+ from: Math.min(previous, current),
203
+ to: Math.max(previous, current),
204
+ backward: true,
205
+ first: false
206
+ };
207
+ }
208
+ if (current > previous && current - previous > FORWARD_GAP_THRESHOLD_SEC) {
209
+ return { from: previous, to: current, backward: false, first: false };
210
+ }
211
+ return null;
212
+ }
213
+
214
+ function mergePendingGap(persisted, observed) {
215
+ if (!persisted) return observed;
216
+ if (!observed) return persisted;
217
+ return {
218
+ from: Math.min(persisted.from, observed.from),
219
+ to: Math.max(persisted.to, observed.to),
220
+ backward: persisted.backward || observed.backward,
221
+ first: persisted.first || observed.first
222
+ };
223
+ }
224
+
225
+ function candidateCrossed(previous, current, candidate) {
226
+ return previous === undefined
227
+ ? current >= candidate.boundary
228
+ : crossedBoundary(previous, current, candidate.boundary);
229
+ }
230
+
231
+ function clockSafetyPreviewState(db, {
232
+ operation,
233
+ candidates = [],
234
+ gcCutoffs = [],
235
+ nowSec,
236
+ clockJump = null
237
+ } = {}) {
238
+ requireTimestamp(nowSec, 'nowSec');
239
+ if (clockJump !== null &&
240
+ (!clockJump || typeof clockJump !== 'object' ||
241
+ !['forward', 'backward'].includes(clockJump.kind))) {
242
+ throw new TypeError('clockJump must be a forward or backward observation');
243
+ }
244
+ const previous = readObservedAt(db);
245
+ const pendingGap = mergePendingGap(
246
+ readPendingGap(db),
247
+ nextObservationGap(previous, nowSec, clockJump)
248
+ );
249
+ const decisionCurrent = pendingGap?.backward ? nowSec : (pendingGap?.to ?? nowSec);
250
+ const decisionPrevious = pendingGap?.backward
251
+ ? decisionCurrent + BACKWARD_TOLERANCE_SEC + 1
252
+ : pendingGap?.first
253
+ ? undefined
254
+ : pendingGap?.from ?? previous;
255
+ let decision = decideClockSafety({
256
+ previous: decisionPrevious,
257
+ current: decisionCurrent,
258
+ operation,
259
+ candidates,
260
+ gcCutoffs
261
+ });
262
+ const liveCandidateCrossed = (candidate) => pendingGap?.backward
263
+ ? candidate.boundary <= nowSec
264
+ : candidateCrossed(decisionPrevious, decisionCurrent, candidate);
265
+ const renewableLiveCandidates = candidates.filter((candidate) =>
266
+ candidate?.evidence === 'live' && liveCandidateCrossed(candidate));
267
+ if (renewableLiveCandidates.length > 0 && !decision.renewOwners) {
268
+ decision = {
269
+ ...decision,
270
+ renewOwners: true,
271
+ reason: decision.reason === 'no-boundary-crossing' ? 'verified-live' : decision.reason
272
+ };
273
+ }
274
+ let graceUntil = readClockGraceUntil(db);
275
+ if (decision.enterGrace) graceUntil = Math.max(graceUntil, nowSec + CLOCK_GRACE_SEC);
276
+ return { decision, graceUntil, pendingGap, renewableLiveCandidates };
277
+ }
278
+
279
+ export function previewClockSafety(db, options = {}) {
280
+ const { decision, graceUntil } = clockSafetyPreviewState(db, options);
281
+ return { decision, graceUntil };
282
+ }
283
+
284
+ export function observeClockSafetyInTransaction(db, options = {}) {
285
+ if (!db?.isTransaction) {
286
+ throw new Error('clock safety observation requires an active transaction');
287
+ }
288
+ const nowSec = options.nowSec;
289
+ const {
290
+ decision,
291
+ graceUntil: previewGraceUntil,
292
+ pendingGap,
293
+ renewableLiveCandidates
294
+ } = clockSafetyPreviewState(db, options);
295
+ let graceUntil = readClockGraceUntil(db);
296
+ if (decision.enterGrace) {
297
+ graceUntil = writeClockGraceUntil(db, previewGraceUntil);
298
+ clearPendingGap(db);
299
+ } else if (pendingGap) {
300
+ writePendingGap(db, pendingGap);
301
+ }
302
+
303
+ let renewed = 0;
304
+ const liveOwners = new Set(renewableLiveCandidates
305
+ .filter((candidate) => typeof candidate.owner === 'string' && candidate.owner)
306
+ .map((candidate) => candidate.owner));
307
+ if (liveOwners.size > 0) {
308
+ for (const owner of liveOwners) {
309
+ renewed += renewOwnedLocks(db, {
310
+ owner,
311
+ nowSec,
312
+ includeExpired: true
313
+ });
314
+ }
315
+ }
316
+
317
+ writeObservedAt(db, nowSec);
318
+ return { decision, graceUntil, renewed };
319
+ }
320
+
321
+ export function observeClockSafety(db, options = {}) {
322
+ return tx(db, () => observeClockSafetyInTransaction(db, options));
323
+ }
@@ -0,0 +1,17 @@
1
+ const CURRENT_TMUX_AUTHORITIES = `
2
+ SELECT MAX(e.id)
3
+ FROM events e
4
+ JOIN peer_bindings b
5
+ ON b.peer = json_extract(e.payload, '$.target_peer')
6
+ WHERE e.type = 'tmux.session.attached'
7
+ AND b.transport = 'tmux'
8
+ AND (b.runtime_target IS NULL OR b.runtime_target = json_extract(e.payload, '$.pane'))
9
+ GROUP BY b.peer, b.runtime_target
10
+ `;
11
+
12
+ export function pruneOldEventsPreservingTmuxAuthority(db, cutoff, options = {}) {
13
+ const predicate = `created_at < ? AND id NOT IN (${CURRENT_TMUX_AUTHORITIES})`;
14
+ const count = db.prepare(`SELECT COUNT(*) AS n FROM events WHERE ${predicate}`).get(cutoff).n;
15
+ if (!options.dryRun) db.prepare(`DELETE FROM events WHERE ${predicate}`).run(cutoff);
16
+ return Number(count || 0);
17
+ }