@logicseek/hello-cc 0.1.9 → 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.
- package/CHANGELOG.md +59 -0
- package/README.md +18 -3
- package/README.zh-CN.md +17 -3
- package/bin/hcc.mjs +3 -5656
- package/docs/README.md +8 -0
- package/docs/README.zh-CN.md +6 -0
- package/docs/architecture.md +29 -18
- package/docs/architecture.zh-CN.md +20 -10
- package/docs/commands.md +13 -4
- package/docs/commands.zh-CN.md +13 -4
- package/docs/current-state-review-2026-08.md +341 -0
- package/docs/defect-review-2026-07.md +382 -0
- package/docs/guide.md +39 -3
- package/docs/guide.zh-CN.md +34 -2
- package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +294 -0
- package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +400 -0
- package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +284 -0
- package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +303 -0
- package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +71 -0
- package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +117 -0
- package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +120 -0
- package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +117 -0
- package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +104 -0
- package/lib/cli/app.mjs +642 -0
- package/lib/cli/commands/coordination.mjs +247 -0
- package/lib/cli/commands/doctor.mjs +81 -0
- package/lib/cli/commands/gc.mjs +852 -0
- package/lib/cli/commands/hook.mjs +205 -0
- package/lib/cli/commands/install.mjs +294 -0
- package/lib/cli/commands/lifecycle.mjs +101 -0
- package/lib/cli/commands/lock.mjs +197 -0
- package/lib/cli/commands/misc.mjs +61 -0
- package/lib/cli/commands/msg.mjs +152 -0
- package/lib/cli/commands/peer.mjs +233 -0
- package/lib/cli/commands/query.mjs +96 -0
- package/lib/cli/commands/run.mjs +463 -0
- package/lib/cli/commands/task.mjs +405 -0
- package/lib/cli/commands/team.mjs +131 -0
- package/lib/cli/commands/tmux.mjs +838 -0
- package/lib/cli/commands/up.mjs +135 -0
- package/lib/cli-args.mjs +16 -2
- package/lib/coordination-state.mjs +112 -51
- package/lib/core/coordination/automation.mjs +5 -8
- package/lib/core/coordination/clock-safety.mjs +323 -0
- package/lib/core/coordination/event-retention.mjs +17 -0
- package/lib/core/coordination/gc-plan.mjs +614 -0
- package/lib/core/coordination/lease-renewal.mjs +102 -0
- package/lib/core/coordination/lock-evidence.mjs +77 -0
- package/lib/core/coordination/optimistic-evidence.mjs +26 -0
- package/lib/core/coordination/tasks.mjs +156 -36
- package/lib/core/peers/evidence-runtime.mjs +312 -0
- package/lib/core/peers/evidence.mjs +103 -0
- package/lib/core/peers/hook-owner.mjs +32 -0
- package/lib/core/peers/liveness.mjs +44 -4
- package/lib/core/peers/peer-helpers.mjs +121 -0
- package/lib/core/peers/session.mjs +8 -2
- package/lib/core/peers/tmux-safety.mjs +290 -0
- package/lib/core/sessions/launch.mjs +13 -1
- package/lib/db/connection.mjs +104 -0
- package/lib/db/events.mjs +34 -0
- package/lib/db/migration-backup.mjs +225 -0
- package/lib/db/schema.mjs +102 -5
- package/lib/db/stores/peers.mjs +34 -0
- package/lib/integrations/peers/identity.mjs +62 -16
- package/lib/integrations/shims/script.mjs +111 -9
- package/lib/process/identity.mjs +192 -0
- package/lib/process/pty-lifecycle.mjs +149 -0
- package/lib/runtime/buffer-directory-lease.mjs +47 -0
- package/lib/runtime/buffer-evidence.mjs +336 -0
- package/lib/runtime/buffer-gc-protocol.mjs +167 -0
- package/lib/runtime/buffer-gc.mjs +270 -0
- package/lib/runtime/client.mjs +60 -7
- package/lib/runtime/fatal-shutdown.mjs +54 -0
- package/lib/runtime/project-path.mjs +139 -0
- package/lib/runtime/projects.mjs +320 -40
- package/lib/runtime/state.mjs +227 -27
- package/lib/shared/cleanup-error.mjs +54 -0
- package/lib/shared/clock-grace.mjs +106 -0
- package/lib/shared/errors.mjs +9 -0
- package/lib/shared/file-lock.mjs +370 -0
- package/lib/shared/json-file.mjs +6 -2
- package/lib/shared/redact.mjs +60 -0
- package/lib/shared/socket-lock-worker.mjs +201 -0
- package/lib/terminal/tmux.mjs +78 -4
- package/lib/tmux.mjs +4 -0
- package/lib/ui/help.mjs +53 -15
- package/lib/web/api-version.mjs +42 -0
- package/lib/web/auto-attach.mjs +185 -0
- package/lib/web/buffer-gc-runtime.mjs +175 -0
- package/lib/web/cookie-auth.mjs +119 -0
- package/lib/web/external-sessions.mjs +254 -0
- package/lib/web/http-routes.mjs +542 -0
- package/lib/web/http.mjs +135 -6
- package/lib/web/liveness-reaper.mjs +145 -0
- package/lib/web/peer-actions.mjs +132 -23
- package/lib/web/project-contexts.mjs +178 -0
- package/lib/web/pty-sessions.mjs +203 -0
- package/lib/web/runtime-main.mjs +714 -0
- package/lib/web/runtime.mjs +91 -36
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/startup.mjs +333 -0
- package/lib/web/tls.mjs +365 -0
- package/lib/web/tmux-sessions.mjs +1361 -0
- package/lib/web/tmux-stream.mjs +166 -0
- package/lib/web/ui-template.mjs +152 -20
- package/package.json +5 -2
- package/scripts/check-free-identifiers.mjs +290 -0
- package/scripts/regression.mjs +5397 -263
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Runtime Safety, Clock, GC, and Registry Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make clock, takeover, garbage collection, and project-registry decisions safe under sleep, PID reuse, runtime failure, and concurrent writers.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A clock-safety module owns persistent observation and grace. A buffer-GC planner is shared by CLI and runtime, while an authenticated runtime endpoint serializes deletion with active sessions. Registry updates use an owner-identified lock directory.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js 24 ESM, `node:test`, `node:sqlite`, filesystem atomics, HTTP runtime API, tmux integration.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Map
|
|
14
|
+
|
|
15
|
+
- Create `lib/core/coordination/clock-safety.mjs` and `test/clock-safety.test.mjs`.
|
|
16
|
+
- Modify `lib/shared/clock-grace.mjs` to make grace extension atomic and observable.
|
|
17
|
+
- Create `lib/runtime/buffer-gc.mjs` and `test/buffer-gc.test.mjs`.
|
|
18
|
+
- Create `lib/shared/file-lock.mjs` and `test/file-lock.test.mjs`.
|
|
19
|
+
- Modify `bin/hcc.mjs` for clock observation, runtime GC endpoint, safe CLI GC, and history flags.
|
|
20
|
+
- Modify `lib/runtime/projects.mjs` for lock-protected registry merging.
|
|
21
|
+
- Modify `lib/runtime/client.mjs` for the internal buffer-GC request.
|
|
22
|
+
- Modify `lib/coordination-state.mjs`, task/lock paths, and `scripts/regression.mjs` for integration behavior.
|
|
23
|
+
|
|
24
|
+
### Task 1: Make grace extension atomic and classify observation decisions
|
|
25
|
+
|
|
26
|
+
**Files:**
|
|
27
|
+
- Create: `lib/core/coordination/clock-safety.mjs`
|
|
28
|
+
- Create: `test/clock-safety.test.mjs`
|
|
29
|
+
- Modify: `lib/shared/clock-grace.mjs:1-46`
|
|
30
|
+
|
|
31
|
+
- [ ] **Step 1: Write failing pure-decision tests**
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import test from 'node:test';
|
|
35
|
+
import assert from 'node:assert/strict';
|
|
36
|
+
import { decideClockSafety } from '../lib/core/coordination/clock-safety.mjs';
|
|
37
|
+
|
|
38
|
+
test('live evidence crosses expiry without grace and requests renewal', () => {
|
|
39
|
+
assert.deepEqual(decideClockSafety({ previous: 100, current: 1000, operation: 'ownership', candidates: [{ boundary: 500, evidence: 'live' }] }), {
|
|
40
|
+
enterGrace: false,
|
|
41
|
+
renewOwners: true,
|
|
42
|
+
reason: 'verified-live'
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('unknown evidence crossing expiry enters grace', () => {
|
|
47
|
+
assert.equal(decideClockSafety({ previous: 100, current: 1000, operation: 'ownership', candidates: [{ boundary: 500, evidence: 'unknown' }] }).enterGrace, true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('GC crossing an age cutoff enters grace without owner candidates', () => {
|
|
51
|
+
assert.equal(decideClockSafety({ previous: 100, current: 1000, operation: 'gc', gcCutoffs: [500] }).enterGrace, true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('verified dead evidence does not delay ownership', () => {
|
|
55
|
+
assert.equal(decideClockSafety({ previous: 100, current: 1000, operation: 'ownership', candidates: [{ boundary: 500, evidence: 'dead' }] }).enterGrace, false);
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- [ ] **Step 2: Run to verify RED**
|
|
60
|
+
|
|
61
|
+
Run: `node --test test/clock-safety.test.mjs`
|
|
62
|
+
|
|
63
|
+
Expected: FAIL because the decision module is missing.
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 3: Implement the pure decision and atomic SQL extension**
|
|
66
|
+
|
|
67
|
+
`decideClockSafety` must return deterministic `{ enterGrace, renewOwners, reason }` from the supplied observation. Backward movement beyond five seconds always enters grace for unknown and GC age decisions. Long forward gaps enter grace only when they cross the relevant boundary.
|
|
68
|
+
|
|
69
|
+
Replace grace overwrite SQL with:
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
INSERT INTO meta(key, value) VALUES ('clock_grace_until', ?)
|
|
73
|
+
ON CONFLICT(key) DO UPDATE SET value = CAST(
|
|
74
|
+
MAX(CAST(meta.value AS INTEGER), CAST(excluded.value AS INTEGER)) AS TEXT
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`writeClockGraceUntil` returns the persisted deadline and throws on write failure; it must not swallow exceptions.
|
|
79
|
+
|
|
80
|
+
- [ ] **Step 4: Verify GREEN**
|
|
81
|
+
|
|
82
|
+
Run: `node --test test/clock-safety.test.mjs`.
|
|
83
|
+
|
|
84
|
+
Add a real in-memory SQLite test with two shorter/longer writes and assert the final value is the maximum. Run again and require PASS.
|
|
85
|
+
|
|
86
|
+
- [ ] **Step 5: Commit**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git add lib/core/coordination/clock-safety.mjs lib/shared/clock-grace.mjs test/clock-safety.test.mjs
|
|
90
|
+
git commit -m "fix: make clock grace monotonic"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Task 2: Observe clock safety before destructive ownership decisions
|
|
94
|
+
|
|
95
|
+
**Files:**
|
|
96
|
+
- Modify: `bin/hcc.mjs:413-471,728-751,1053-1120,1752-1810,2926-3005,5709-5725,6426-6538`
|
|
97
|
+
- Modify: `lib/core/coordination/clock-safety.mjs`
|
|
98
|
+
- Modify: `lib/coordination-state.mjs:37-105,191-220`
|
|
99
|
+
- Modify: `lib/core/coordination/tasks.mjs:73-90`
|
|
100
|
+
- Modify: `scripts/regression.mjs:2616-2820`
|
|
101
|
+
|
|
102
|
+
- [ ] **Step 1: Add the CLI-only failing integration fixture**
|
|
103
|
+
|
|
104
|
+
Create an isolated project with no local or global runtime pointer. Register a peer using the regression process PID and stored fingerprint, acquire a lock, move only wall-clock DB timestamps one day into the past, remove `clock_grace_until`, and attempt acquisition from another peer.
|
|
105
|
+
|
|
106
|
+
Write the final expected assertion from the start:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
if (attempt.status === 0 || !attempt.stderr.includes('LOCK_HELD')) {
|
|
110
|
+
fail(`live owner was not protected after a CLI-only clock gap:\n${attempt.stderr}`);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
From the same initial test, require an unchanged owner, renewed `expires_at`, and
|
|
115
|
+
no blanket grace for the verified live owner. Add unknown-evidence and
|
|
116
|
+
verified-dead variants before running RED.
|
|
117
|
+
|
|
118
|
+
- [ ] **Step 2: Run to verify RED**
|
|
119
|
+
|
|
120
|
+
Run: `npm run test:regression`.
|
|
121
|
+
|
|
122
|
+
Expected: live-owner fixture demonstrates takeover/replacement under the current code.
|
|
123
|
+
|
|
124
|
+
- [ ] **Step 3: Add the observation transaction**
|
|
125
|
+
|
|
126
|
+
Export `observeClockSafety(db, { operation, candidates, gcCutoffs, nowSec })`. In one immediate transaction, read `clock_last_observed_at`, call `decideClockSafety`, extend grace to `nowSec + 120` only when `enterGrace` is true, and write the new watermark. Persist the maximum of the existing and proposed deadline. Return the decision and persisted grace. Verified-live evidence renews immediately without grace, verified-dead evidence proceeds without grace, and only unknown evidence receives the 120-second delay.
|
|
127
|
+
|
|
128
|
+
Call it before lock acquire, task takeover, heartbeat/hook recovery, reaper, and GC. Do not call it from pure status/list/doctor paths. When it reports verified live owners, renew retained locks from `ttl_sec`. When grace persistence throws, abort the destructive command with `CLOCK_SAFETY_UNAVAILABLE`.
|
|
129
|
+
|
|
130
|
+
The Web poller passes its wall/monotonic jump signal into the same observer and no longer maintains a separate grace algorithm.
|
|
131
|
+
|
|
132
|
+
- [ ] **Step 4: Verify GREEN and nearby behavior**
|
|
133
|
+
|
|
134
|
+
Run: `node --test test/clock-safety.test.mjs`.
|
|
135
|
+
|
|
136
|
+
Run: `npm run test:regression`.
|
|
137
|
+
|
|
138
|
+
Expected: live evidence renews immediately, dead evidence expires, unknown evidence enters grace, status/doctor remain read-only.
|
|
139
|
+
|
|
140
|
+
- [ ] **Step 5: Commit**
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git add bin/hcc.mjs lib/core/coordination/clock-safety.mjs lib/coordination-state.mjs lib/core/coordination/tasks.mjs scripts/regression.mjs
|
|
144
|
+
git commit -m "fix: protect CLI-only ownership after clock gaps"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Task 3: Extract a buffer-GC planner and runtime arbitration endpoint
|
|
148
|
+
|
|
149
|
+
**Files:**
|
|
150
|
+
- Create: `lib/runtime/buffer-gc.mjs`
|
|
151
|
+
- Create: `test/buffer-gc.test.mjs`
|
|
152
|
+
- Modify: `bin/hcc.mjs:3005-3063,6426-6538,4675-5050`
|
|
153
|
+
- Modify: `lib/runtime/client.mjs:7-35`
|
|
154
|
+
- Modify: `scripts/regression.mjs:1841-1880,2866-3300,7019-7070`
|
|
155
|
+
|
|
156
|
+
- [ ] **Step 1: Write failing planner tests with real files**
|
|
157
|
+
|
|
158
|
+
In a temporary `bufs` directory create old protected, old dead, old unknown, and new files. Call:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
const result = pruneBufferFiles({
|
|
162
|
+
directories: [bufsDir],
|
|
163
|
+
cutoffMs,
|
|
164
|
+
protectedPaths: new Set([protectedFile]),
|
|
165
|
+
unknownPaths: new Set([unknownFile]),
|
|
166
|
+
dryRun: false
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Assert protected/unknown/new remain, dead old is deleted, and counts are `{ deleted: 1, protected: 1, deferred: 1 }`.
|
|
171
|
+
|
|
172
|
+
- [ ] **Step 2: Run to verify RED**
|
|
173
|
+
|
|
174
|
+
Run: `node --test test/buffer-gc.test.mjs`.
|
|
175
|
+
|
|
176
|
+
Expected: missing-module failure.
|
|
177
|
+
|
|
178
|
+
- [ ] **Step 3: Implement the planner and endpoint**
|
|
179
|
+
|
|
180
|
+
Implement `pruneBufferFiles` with absolute resolved paths, `lstat` regular-file/FIFO checks, one supplied cutoff, and identical dry-run/apply planning. It must not follow symlinks.
|
|
181
|
+
|
|
182
|
+
Add authenticated `POST /api/runtime/gc-buffers`. The handler snapshots actual `outFile`, `inFile`, `resizeFile`, `pipeFile`, and external meta paths from every running session, groups them by actual directory, and calls the synchronous planner before returning counts.
|
|
183
|
+
|
|
184
|
+
Manual CLI GC fixes `cutoffMs` before the request. If a runtime pointer exists and the endpoint returns unreachable/404/426, mark eligible buffers deferred and do not fall back to blind local deletion. With no runtime pointer, build live/unknown protection from external metadata fingerprints and verified tmux panes.
|
|
185
|
+
|
|
186
|
+
- [ ] **Step 4: Verify RED-GREEN at the real boundary**
|
|
187
|
+
|
|
188
|
+
Run planner tests. Then add runtime integration cases for active external files, active tmux FIFO, sibling-project actual directories, unreachable runtime, legacy metadata, and orphan old files.
|
|
189
|
+
|
|
190
|
+
Run: `npm run test:regression`.
|
|
191
|
+
|
|
192
|
+
Expected: active/unknown files remain, only proven old orphans are removed, and runtime failure reports deferred counts.
|
|
193
|
+
|
|
194
|
+
- [ ] **Step 5: Commit**
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
git add lib/runtime/buffer-gc.mjs test/buffer-gc.test.mjs lib/runtime/client.mjs bin/hcc.mjs scripts/regression.mjs
|
|
198
|
+
git commit -m "fix: arbitrate buffer cleanup through runtime"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Task 4: Make GC retention explicit and clock-safe
|
|
202
|
+
|
|
203
|
+
**Files:**
|
|
204
|
+
- Modify: `bin/hcc.mjs:6420-6557`
|
|
205
|
+
- Modify: `lib/ui/help.mjs:40-50,285-305`
|
|
206
|
+
- Modify: `scripts/regression.mjs:3689-3776,7019-7070`
|
|
207
|
+
|
|
208
|
+
- [ ] **Step 1: Add failing CLI cases**
|
|
209
|
+
|
|
210
|
+
Add fixtures proving:
|
|
211
|
+
|
|
212
|
+
```text
|
|
213
|
+
hcc gc --older-than -1 --yes -> BAD_ARGS, no row/file changes
|
|
214
|
+
hcc gc --older-than 0 --yes -> technical cleanup only
|
|
215
|
+
hcc gc --older-than 0 --history -> dry-run only
|
|
216
|
+
hcc gc --older-than 0 --history --yes -> eligible history removed
|
|
217
|
+
clock grace + any form -> every age-based category deferred
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Keep an open-task handoff and assert it survives even with `--history --yes`.
|
|
221
|
+
|
|
222
|
+
- [ ] **Step 2: Run to verify RED**
|
|
223
|
+
|
|
224
|
+
Run: `npm run test:regression`.
|
|
225
|
+
|
|
226
|
+
Expected: negative retention mutates/plans broadly, and ordinary `--yes` currently removes history.
|
|
227
|
+
|
|
228
|
+
- [ ] **Step 3: Implement the command contract**
|
|
229
|
+
|
|
230
|
+
Reject `olderThanDays < 0` before `connect()`. Parse a `history` boolean. Split the planner into technical and history SQL. During grace, produce deferred counts for all age-based categories and execute no age-based delete. Always preserve handoffs linked to tasks outside `done`/`abandoned`.
|
|
231
|
+
|
|
232
|
+
Keep WAL checkpoint only after an applied safe plan. Update text and JSON output with `deleted`, `protected`, and `deferred` category counts.
|
|
233
|
+
|
|
234
|
+
- [ ] **Step 4: Verify GREEN**
|
|
235
|
+
|
|
236
|
+
Run: `npm run test:regression`.
|
|
237
|
+
|
|
238
|
+
Run: `node ./bin/hcc.mjs gc --help`.
|
|
239
|
+
|
|
240
|
+
Expected: every CLI contract case passes and help documents `--history`.
|
|
241
|
+
|
|
242
|
+
- [ ] **Step 5: Commit**
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
git add bin/hcc.mjs lib/ui/help.mjs scripts/regression.mjs
|
|
246
|
+
git commit -m "fix: make GC retention explicit and safe"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Task 5: Serialize project-registry writers
|
|
250
|
+
|
|
251
|
+
**Files:**
|
|
252
|
+
- Create: `lib/shared/file-lock.mjs`
|
|
253
|
+
- Create: `test/file-lock.test.mjs`
|
|
254
|
+
- Modify: `lib/runtime/projects.mjs:1-86`
|
|
255
|
+
- Modify: `scripts/regression.mjs:1813-1840,4500-4700`
|
|
256
|
+
|
|
257
|
+
- [ ] **Step 1: Write failing real-filesystem tests**
|
|
258
|
+
|
|
259
|
+
Spawn two Node child processes that wait on a barrier file, then concurrently call `registerProject` for different roots under one isolated `HOME`. Assert both records remain. Add a same-root/different-DB update inside 60 seconds and assert the DB changes immediately.
|
|
260
|
+
|
|
261
|
+
Add a stale-lock fixture with a dead owner fingerprint and an unknown-owner fixture. Dead lock must be reclaimed; unknown lock must time out without deletion.
|
|
262
|
+
|
|
263
|
+
- [ ] **Step 2: Run to verify RED**
|
|
264
|
+
|
|
265
|
+
Run: `node --test test/file-lock.test.mjs`.
|
|
266
|
+
|
|
267
|
+
Expected: missing-module failure, then the concurrent registry assertion fails before integration.
|
|
268
|
+
|
|
269
|
+
- [ ] **Step 3: Implement atomic lock-directory ownership**
|
|
270
|
+
|
|
271
|
+
Implement:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
export function withFileLock(target, fn, { timeoutMs = 5000, retryMs = 25 } = {})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Acquire `${target}.lock` with `mkdirSync`. Write owner metadata atomically with the current process fingerprint. On `EEXIST`, inspect the owner: reclaim only `dead`, retry `live`, and time out `unknown`. Always remove only the lock whose owner token still matches the current holder.
|
|
278
|
+
|
|
279
|
+
Wrap the entire registry re-read, merge, throttle decision, and atomic write. Compare both canonical root and resolved DB before throttling.
|
|
280
|
+
|
|
281
|
+
- [ ] **Step 4: Verify GREEN**
|
|
282
|
+
|
|
283
|
+
Run: `node --test test/file-lock.test.mjs`.
|
|
284
|
+
|
|
285
|
+
Run: `npm run test:regression`.
|
|
286
|
+
|
|
287
|
+
Expected: concurrent writers retain both records; changed DB updates immediately; unknown locks are not stolen.
|
|
288
|
+
|
|
289
|
+
- [ ] **Step 5: Commit**
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
git add lib/shared/file-lock.mjs test/file-lock.test.mjs lib/runtime/projects.mjs scripts/regression.mjs
|
|
293
|
+
git commit -m "fix: serialize project registry updates"
|
|
294
|
+
```
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# hello-cc v1 Data and Identity Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Introduce schema v7, safe pre-migration backups, process fingerprints, and the breaking v1 provider identity contract.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Put process inspection and migration backup logic in focused modules. Persist opaque process fingerprints on peers and external/tmux session metadata, then route all liveness consumers through one three-state resolver.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js 24 ESM, `node:test`, `node:sqlite`, macOS `ps`, Linux `/proc`, tmux.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Map
|
|
14
|
+
|
|
15
|
+
- Create `test/process-identity.test.mjs`: real-process and parser behavior.
|
|
16
|
+
- Create `test/schema-v7.test.mjs`: real SQLite backup and migration behavior.
|
|
17
|
+
- Create `lib/process/identity.mjs`: inspect and compare process fingerprints.
|
|
18
|
+
- Create `lib/db/migration-backup.mjs`: consistent `VACUUM INTO` backup.
|
|
19
|
+
- Create `lib/core/peers/evidence.mjs`: `live`/`dead`/`unknown` resolver.
|
|
20
|
+
- Modify `package.json`: unit-test scripts and v1 version.
|
|
21
|
+
- Modify `lib/db/schema.mjs`: schema v7 process columns and backup hook.
|
|
22
|
+
- Modify `bin/hcc.mjs`: migration hook, peer identity persistence, liveness wiring.
|
|
23
|
+
- Modify `lib/core/peers/liveness.mjs`: consume evidence rather than age alone.
|
|
24
|
+
- Modify `lib/core/coordination/tasks.mjs`: use evidence for takeover.
|
|
25
|
+
- Modify `lib/core/peers/session.mjs`: finalize the breaking v1 ID algorithm.
|
|
26
|
+
- Modify `lib/integrations/shims/script.mjs`: keep shim hashing byte-identical.
|
|
27
|
+
- Modify `scripts/regression.mjs`: real CLI migration and session evidence coverage.
|
|
28
|
+
|
|
29
|
+
### Task 1: Add the focused unit-test entry point
|
|
30
|
+
|
|
31
|
+
**Files:**
|
|
32
|
+
- Modify: `package.json:24-32`
|
|
33
|
+
- Create: `test/process-identity.test.mjs`
|
|
34
|
+
|
|
35
|
+
- [ ] **Step 1: Write the failing test**
|
|
36
|
+
|
|
37
|
+
Create the test with the intended module API:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import test from 'node:test';
|
|
41
|
+
import assert from 'node:assert/strict';
|
|
42
|
+
import {
|
|
43
|
+
compareProcessIdentity,
|
|
44
|
+
inspectProcessIdentity,
|
|
45
|
+
parseLinuxStatStartTicks,
|
|
46
|
+
parsePsStartIdentity
|
|
47
|
+
} from '../lib/process/identity.mjs';
|
|
48
|
+
|
|
49
|
+
test('reads a stable identity for the current process', () => {
|
|
50
|
+
const first = inspectProcessIdentity(process.pid);
|
|
51
|
+
const second = inspectProcessIdentity(process.pid);
|
|
52
|
+
assert.equal(first.state, 'live');
|
|
53
|
+
assert.deepEqual(second.identity, first.identity);
|
|
54
|
+
assert.equal(compareProcessIdentity(first.identity, second.identity), 'live');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('parses Linux stat when command contains spaces and parentheses', () => {
|
|
58
|
+
const fields = Array.from({ length: 30 }, (_, i) => String(i + 1));
|
|
59
|
+
fields[19] = '987654';
|
|
60
|
+
assert.equal(parseLinuxStatStartTicks(`42 (worker (one)) ${fields.join(' ')}`), '987654');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('parses macOS ps start and command identity', () => {
|
|
64
|
+
assert.deepEqual(parsePsStartIdentity('Mon Aug 3 06:10:11 2026\t/usr/bin/node app.mjs\n'), {
|
|
65
|
+
startToken: 'Mon Aug 3 06:10:11 2026',
|
|
66
|
+
command: '/usr/bin/node app.mjs'
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('rejects a reused PID fingerprint', () => {
|
|
71
|
+
const stored = { pid: 42, startToken: 'boot-a:100', commandHash: 'aaa' };
|
|
72
|
+
const current = { pid: 42, startToken: 'boot-a:200', commandHash: 'aaa' };
|
|
73
|
+
assert.equal(compareProcessIdentity(stored, current), 'dead');
|
|
74
|
+
assert.equal(compareProcessIdentity(null, current), 'unknown');
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- [ ] **Step 2: Run the test to verify RED**
|
|
79
|
+
|
|
80
|
+
Run: `node --test test/process-identity.test.mjs`
|
|
81
|
+
|
|
82
|
+
Expected: FAIL with `ERR_MODULE_NOT_FOUND` for `lib/process/identity.mjs`.
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 3: Add the unit-test scripts**
|
|
85
|
+
|
|
86
|
+
Set the scripts to:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
"pretest": "npm run test:unit",
|
|
90
|
+
"test": "npm run test:regression",
|
|
91
|
+
"test:unit": "node --test test/*.test.mjs",
|
|
92
|
+
"test:regression": "node ./scripts/regression.mjs"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Do not change the production version in this task.
|
|
96
|
+
|
|
97
|
+
- [ ] **Step 4: Verify the intended failure through npm**
|
|
98
|
+
|
|
99
|
+
Run: `npm run test:unit`
|
|
100
|
+
|
|
101
|
+
Expected: the same missing-module failure, proving the new test entry point runs.
|
|
102
|
+
|
|
103
|
+
- [ ] **Step 5: Commit**
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git add package.json test/process-identity.test.mjs
|
|
107
|
+
git commit -m "test: add v1 focused test entry point"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Task 2: Implement cross-platform process fingerprints
|
|
111
|
+
|
|
112
|
+
**Files:**
|
|
113
|
+
- Create: `lib/process/identity.mjs`
|
|
114
|
+
- Test: `test/process-identity.test.mjs`
|
|
115
|
+
|
|
116
|
+
- [ ] **Step 1: Complete the failing boundary tests before implementation**
|
|
117
|
+
|
|
118
|
+
Append tests that inspect PID `2147483647` as dead, treat a partial stored
|
|
119
|
+
fingerprint as unknown, reject a changed command hash, and parse malformed Linux
|
|
120
|
+
and macOS fixture rows as unknown/null.
|
|
121
|
+
|
|
122
|
+
- [ ] **Step 2: Run the complete test to verify RED**
|
|
123
|
+
|
|
124
|
+
Run: `node --test test/process-identity.test.mjs`
|
|
125
|
+
|
|
126
|
+
Expected: the module is still missing and every final identity expectation is
|
|
127
|
+
already present before production code exists.
|
|
128
|
+
|
|
129
|
+
- [ ] **Step 3: Implement the minimal identity module**
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import fs from 'node:fs';
|
|
133
|
+
import { createHash } from 'node:crypto';
|
|
134
|
+
import { spawnSync } from 'node:child_process';
|
|
135
|
+
|
|
136
|
+
function commandHash(command) {
|
|
137
|
+
return createHash('sha256').update(String(command || '').trim()).digest('hex');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function parseLinuxStatStartTicks(text) {
|
|
141
|
+
const close = String(text).lastIndexOf(')');
|
|
142
|
+
if (close < 0) return null;
|
|
143
|
+
const rest = String(text).slice(close + 2).trim().split(/\s+/);
|
|
144
|
+
return rest[19] || null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function parsePsStartIdentity(text) {
|
|
148
|
+
const line = String(text).trimEnd();
|
|
149
|
+
const tab = line.indexOf('\t');
|
|
150
|
+
if (tab < 0) return null;
|
|
151
|
+
const startToken = line.slice(0, tab).trim();
|
|
152
|
+
const command = line.slice(tab + 1).trim();
|
|
153
|
+
return startToken && command ? { startToken, command } : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function linuxIdentity(pid) {
|
|
157
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
158
|
+
const startTicks = parseLinuxStatStartTicks(stat);
|
|
159
|
+
const bootId = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
|
|
160
|
+
const command = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean).join(' ');
|
|
161
|
+
if (!startTicks || !bootId) throw new Error('incomplete Linux identity');
|
|
162
|
+
return { pid, startToken: `${bootId}:${startTicks}`, commandHash: commandHash(command) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function psIdentity(pid) {
|
|
166
|
+
const boot = spawnSync('sysctl', ['-n', 'kern.boottime'], { encoding: 'utf8' });
|
|
167
|
+
const started = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8' });
|
|
168
|
+
const command = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8' });
|
|
169
|
+
if (started.status !== 0 || command.status !== 0 || boot.status !== 0) return null;
|
|
170
|
+
const parsed = parsePsStartIdentity(`${started.stdout.trim()}\t${command.stdout.trim()}\n`);
|
|
171
|
+
if (!parsed) throw new Error('incomplete ps identity');
|
|
172
|
+
return {
|
|
173
|
+
pid,
|
|
174
|
+
startToken: `${String(boot.stdout || '').trim()}:${parsed.startToken}`,
|
|
175
|
+
commandHash: commandHash(parsed.command)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function inspectProcessIdentity(value) {
|
|
180
|
+
const pid = Number(value);
|
|
181
|
+
if (!Number.isInteger(pid) || pid <= 0) return { state: 'unknown', identity: null };
|
|
182
|
+
try {
|
|
183
|
+
process.kill(pid, 0);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err?.code === 'ESRCH') return { state: 'dead', identity: null };
|
|
186
|
+
if (err?.code !== 'EPERM') return { state: 'unknown', identity: null };
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const identity = process.platform === 'linux' ? linuxIdentity(pid) : psIdentity(pid);
|
|
190
|
+
return identity ? { state: 'live', identity } : { state: 'dead', identity: null };
|
|
191
|
+
} catch {
|
|
192
|
+
return { state: 'unknown', identity: null };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isCompleteIdentity(value) {
|
|
197
|
+
return Number.isInteger(Number(value?.pid)) && Number(value.pid) > 0 &&
|
|
198
|
+
typeof value?.startToken === 'string' && value.startToken.length > 0 &&
|
|
199
|
+
typeof value?.commandHash === 'string' && /^[a-f0-9]{64}$/.test(value.commandHash);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function compareProcessIdentity(stored, current) {
|
|
203
|
+
if (!isCompleteIdentity(stored) || !isCompleteIdentity(current)) return 'unknown';
|
|
204
|
+
return Number(stored.pid) === Number(current.pid) &&
|
|
205
|
+
stored.startToken === current.startToken &&
|
|
206
|
+
stored.commandHash === current.commandHash ? 'live' : 'dead';
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- [ ] **Step 4: Run focused tests to verify GREEN**
|
|
211
|
+
|
|
212
|
+
Run: `node --test test/process-identity.test.mjs`
|
|
213
|
+
|
|
214
|
+
Expected: all boundary tests PASS on macOS and Linux. If the macOS parser fixture exposes a spacing mismatch, fix the parser, not the assertions.
|
|
215
|
+
|
|
216
|
+
- [ ] **Step 5: Commit**
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
git add lib/process/identity.mjs test/process-identity.test.mjs
|
|
220
|
+
git commit -m "feat: add process identity fingerprints"
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Task 3: Add schema v7 and consistent migration backups
|
|
224
|
+
|
|
225
|
+
**Files:**
|
|
226
|
+
- Create: `lib/db/migration-backup.mjs`
|
|
227
|
+
- Create: `test/schema-v7.test.mjs`
|
|
228
|
+
- Modify: `lib/db/schema.mjs:3,76-88,169-179,318-355`
|
|
229
|
+
- Modify: `bin/hcc.mjs:413-466`
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 1: Write real SQLite failing tests**
|
|
232
|
+
|
|
233
|
+
Create temporary v5 and v6 databases using `DatabaseSync`. Assert that `ensureMigrationBackup(db, dbPath, 5, 7)` returns a new path, the backup passes `quick_check`, contains the original row, and a second call returns a different non-overwriting path. Add a failure case with an unwritable destination and assert the source `schema_version` remains unchanged.
|
|
234
|
+
|
|
235
|
+
Use this call shape:
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
const backup = ensureMigrationBackup(db, dbPath, 5, 7, { timestamp: () => '20260803T120000000Z' });
|
|
239
|
+
assert.match(backup, /mesh\.db\.pre-v5-to-v7\.20260803T120000000Z\.[a-f0-9]{8}\.bak$/);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- [ ] **Step 2: Run to verify RED**
|
|
243
|
+
|
|
244
|
+
Run: `node --test test/schema-v7.test.mjs`
|
|
245
|
+
|
|
246
|
+
Expected: FAIL because `lib/db/migration-backup.mjs` does not exist.
|
|
247
|
+
|
|
248
|
+
- [ ] **Step 3: Implement backup and migration hook**
|
|
249
|
+
|
|
250
|
+
Implement `ensureMigrationBackup` with a SQL-string quoting helper, `VACUUM INTO`, an exclusive new filename, read-only `quick_check`, and cleanup of an invalid backup. Export `initSchema(db, { beforeMigration, ... })`; call `beforeMigration({ fromVersion, toVersion })` before the migration transaction when `fromVersion < 7`.
|
|
251
|
+
|
|
252
|
+
Set:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
export const DB_SCHEMA_VERSION = 7;
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Add to `peers` and migration 7:
|
|
259
|
+
|
|
260
|
+
```sql
|
|
261
|
+
pid_start_token TEXT,
|
|
262
|
+
pid_command_hash TEXT
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
In every production connection path, pass:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
beforeMigration: ({ fromVersion, toVersion }) =>
|
|
269
|
+
ensureMigrationBackup(db, dbPath, fromVersion, toVersion)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The sibling-project migration catch remains isolated and logs the backup failure.
|
|
273
|
+
|
|
274
|
+
- [ ] **Step 4: Verify GREEN and migration integration**
|
|
275
|
+
|
|
276
|
+
Run: `node --test test/schema-v7.test.mjs`
|
|
277
|
+
|
|
278
|
+
Then add v7 expectations to `createLegacySchemaDb`, `assertLegacySchemaMigration`, `assertRegisteredProjectDbMigration`, and the future-schema rejection workflow in `scripts/regression.mjs`.
|
|
279
|
+
|
|
280
|
+
Run: `npm run test:regression`
|
|
281
|
+
|
|
282
|
+
Expected: `FULL_REGRESSION_OK` with v5 and v6 fixtures backed up before v7.
|
|
283
|
+
|
|
284
|
+
- [ ] **Step 5: Commit**
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
git add lib/db/migration-backup.mjs lib/db/schema.mjs bin/hcc.mjs test/schema-v7.test.mjs scripts/regression.mjs
|
|
288
|
+
git commit -m "feat: back up databases before schema v7"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Task 4: Persist identities and unify liveness evidence
|
|
292
|
+
|
|
293
|
+
**Files:**
|
|
294
|
+
- Create: `lib/core/peers/evidence.mjs`
|
|
295
|
+
- Create: `test/peer-evidence.test.mjs`
|
|
296
|
+
- Modify: `bin/hcc.mjs:524-575,2610-2720,4369-4515,5620-5735`
|
|
297
|
+
- Modify: `lib/core/peers/liveness.mjs:1-65`
|
|
298
|
+
- Modify: `lib/core/coordination/tasks.mjs:65-90`
|
|
299
|
+
- Modify: `scripts/regression.mjs:2616-2820,6834-6918`
|
|
300
|
+
|
|
301
|
+
- [ ] **Step 1: Write failing evidence tests**
|
|
302
|
+
|
|
303
|
+
Use complete peer/session fixtures and assert:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
assert.equal(resolvePeerEvidence({ peer: { status: 'exited' } }).state, 'dead');
|
|
307
|
+
assert.equal(resolvePeerEvidence({ peer: detachedPeer, process: matchingProcess, tmuxManaged: false }).state, 'live');
|
|
308
|
+
assert.equal(resolvePeerEvidence({ peer: detachedPeer, tmux: matchingPane, tmuxManaged: true }).state, 'live');
|
|
309
|
+
assert.equal(resolvePeerEvidence({ peer: workingPeer, process: reusedPid, tmuxManaged: false }).state, 'dead');
|
|
310
|
+
assert.equal(resolvePeerEvidence({ peer: legacyPeer, process: { state: 'live', identity: null }, tmuxManaged: false }).state, 'unknown');
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Run: `node --test test/peer-evidence.test.mjs`
|
|
314
|
+
|
|
315
|
+
Expected: FAIL because the resolver module is missing.
|
|
316
|
+
|
|
317
|
+
- [ ] **Step 2: Implement the resolver**
|
|
318
|
+
|
|
319
|
+
```js
|
|
320
|
+
export function resolvePeerEvidence({ peer, process: processEvidence, tmux = null, tmuxManaged = false } = {}) {
|
|
321
|
+
if (peer?.status === 'exited') return { state: 'dead', reason: 'explicit-exit' };
|
|
322
|
+
if (tmux?.state === 'live') return { state: 'live', reason: 'tmux-identity' };
|
|
323
|
+
if (processEvidence?.state === 'live' && processEvidence.identityMatched) {
|
|
324
|
+
return { state: 'live', reason: 'process-identity' };
|
|
325
|
+
}
|
|
326
|
+
const processDead = processEvidence?.state === 'dead' ||
|
|
327
|
+
(processEvidence?.state === 'live' && processEvidence.identityMatched === false);
|
|
328
|
+
if (tmux?.state === 'unknown' || processEvidence?.state === 'unknown') {
|
|
329
|
+
return { state: 'unknown', reason: 'incomplete-evidence' };
|
|
330
|
+
}
|
|
331
|
+
if (tmuxManaged && tmux?.state === 'dead' && processDead) {
|
|
332
|
+
return { state: 'dead', reason: 'process-and-tmux-dead' };
|
|
333
|
+
}
|
|
334
|
+
if (!tmuxManaged && processDead) return { state: 'dead', reason: 'process-dead' };
|
|
335
|
+
return { state: 'unknown', reason: 'insufficient-evidence' };
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
- [ ] **Step 3: Persist and consume fingerprints**
|
|
340
|
+
|
|
341
|
+
When registering/upserting a peer, inspect its PID and persist `pid_start_token` and `pid_command_hash` only for verified identity. Store full wrapper/child identity objects in external `.meta`. Store pane identity when adopting or starting tmux.
|
|
342
|
+
|
|
343
|
+
Replace age-only decisions in task annotations, takeover, reaper, lock recovery, and GC peer selection with the shared evidence result. `detached` must no longer be included in a hard-coded dead-status set.
|
|
344
|
+
|
|
345
|
+
- [ ] **Step 4: Verify real behavior**
|
|
346
|
+
|
|
347
|
+
Run: `node --test test/peer-evidence.test.mjs`
|
|
348
|
+
|
|
349
|
+
Add integration fixtures for a detached live tmux pane, live non-tmux child, reused PID fingerprint, and legacy unknown metadata. Run: `npm run test:regression`.
|
|
350
|
+
|
|
351
|
+
Expected: live evidence blocks takeover; reused/dead evidence permits it; legacy evidence is unknown.
|
|
352
|
+
|
|
353
|
+
- [ ] **Step 5: Commit**
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
git add lib/core/peers/evidence.mjs test/peer-evidence.test.mjs lib/core/peers/liveness.mjs lib/core/coordination/tasks.mjs bin/hcc.mjs scripts/regression.mjs
|
|
357
|
+
git commit -m "fix: resolve ownership from process evidence"
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Task 5: Prove and freeze the v1 provider identity break
|
|
361
|
+
|
|
362
|
+
**Files:**
|
|
363
|
+
- Modify: `lib/core/peers/session.mjs:1-12`
|
|
364
|
+
- Modify: `lib/integrations/shims/script.mjs:60-149,335-415`
|
|
365
|
+
- Modify: `scripts/regression.mjs:1385-1584,5660-5745`
|
|
366
|
+
|
|
367
|
+
- [ ] **Step 1: Add failing upgrade-contract assertions**
|
|
368
|
+
|
|
369
|
+
Assert exact new IDs for a UUID and two same-prefix names, assert they differ from the remote v0.1.9 prefix algorithm, and assert every generated shim returns the JavaScript ID. Add an invalid hash-command fixture and assert transparent provider passthrough.
|
|
370
|
+
|
|
371
|
+
- [ ] **Step 2: Run the identity contract**
|
|
372
|
+
|
|
373
|
+
Run: `npm run test:regression`
|
|
374
|
+
|
|
375
|
+
Expected: if the current worktree already satisfies every final identity
|
|
376
|
+
assertion, record this item as `no_change` and do not modify production code. If
|
|
377
|
+
any assertion fails, the failure becomes the RED proof for Step 3.
|
|
378
|
+
|
|
379
|
+
- [ ] **Step 3: Correct only a demonstrated algorithm mismatch**
|
|
380
|
+
|
|
381
|
+
When Step 2 is RED, keep `providerSessionPeerId(kind, providerId)` as full-input
|
|
382
|
+
SHA-1 first-eight hex and make the failing shim path byte-identical. Keep strict
|
|
383
|
+
40-hex validation and passthrough on failure. When Step 2 is already green, skip
|
|
384
|
+
this production step.
|
|
385
|
+
|
|
386
|
+
- [ ] **Step 4: Verify GREEN**
|
|
387
|
+
|
|
388
|
+
Run: `npm run test:unit`.
|
|
389
|
+
|
|
390
|
+
Run: `npm run test:regression`.
|
|
391
|
+
|
|
392
|
+
Expected: all identity fixtures PASS and no old-name scan regression.
|
|
393
|
+
|
|
394
|
+
- [ ] **Step 5: Commit**
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
git add scripts/regression.mjs
|
|
398
|
+
# Add the two production files only when Step 2 proved a mismatch.
|
|
399
|
+
git commit -m "feat: finalize v1 provider identities"
|
|
400
|
+
```
|