pi-odd-worktree 0.0.0-stage → 0.1.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 (60) hide show
  1. package/README.md +223 -2
  2. package/extensions/odd-worktree-tools.ts +7593 -0
  3. package/lib/checkout-facts.js +598 -0
  4. package/lib/clipboard.js +226 -0
  5. package/lib/gh-issues.js +1555 -0
  6. package/lib/issue-preview.js +273 -0
  7. package/lib/issues-view.js +2274 -0
  8. package/lib/odd-docs.js +505 -0
  9. package/lib/odd-markdown.js +413 -0
  10. package/lib/odd-view.js +990 -0
  11. package/lib/open-external.js +106 -0
  12. package/lib/preview-scroll-keys.js +192 -0
  13. package/lib/session-auto-name.js +292 -0
  14. package/lib/session-worktree-register.js +62 -0
  15. package/lib/tui-paint.js +273 -0
  16. package/lib/view-layout.js +368 -0
  17. package/lib/warp-create-panel.js +722 -0
  18. package/lib/warp-create-picker.js +906 -0
  19. package/lib/warp-delete-confirm.js +465 -0
  20. package/lib/warp-selector-view.js +1265 -0
  21. package/lib/worktree-card.js +1117 -0
  22. package/lib/worktree-changes.js +1395 -0
  23. package/lib/worktree-close-herdr.js +73 -0
  24. package/lib/worktree-git.js +1048 -0
  25. package/lib/worktree-hooks.js +738 -0
  26. package/lib/worktree-model-result.js +291 -0
  27. package/lib/worktree-registry.js +886 -0
  28. package/lib/worktree-session-guard.js +595 -0
  29. package/lib/worktree-symlinks.js +336 -0
  30. package/lib/worktree-warp-sessions.js +566 -0
  31. package/package.json +36 -4
  32. package/tools/commands.test.js +176 -0
  33. package/tools/fixtures/gh-api-user.json +1 -0
  34. package/tools/fixtures/gh-issue-list.json +1 -0
  35. package/tools/fixtures/gh-issue-view.json +1 -0
  36. package/tools/fixtures/gh-label-list.json +1 -0
  37. package/tools/fixtures/gh-repo-view.json +1 -0
  38. package/tools/fixtures/load-extension.js +66 -0
  39. package/tools/gh-issues-feedback.test.js +359 -0
  40. package/tools/gh-issues.test.js +1502 -0
  41. package/tools/issue-preview.test.js +216 -0
  42. package/tools/issues-command.test.js +1217 -0
  43. package/tools/issues-view.test.js +4401 -0
  44. package/tools/odd-docs.test.js +803 -0
  45. package/tools/odd-markdown.test.js +236 -0
  46. package/tools/odd-view.test.js +2328 -0
  47. package/tools/package-surface.test.js +138 -0
  48. package/tools/t5-session-identity.test.js +306 -0
  49. package/tools/warp-create-panel.test.js +185 -0
  50. package/tools/warp-create-picker.test.js +1357 -0
  51. package/tools/warp-delete-confirm.test.js +616 -0
  52. package/tools/warp-selector-view.test.js +1463 -0
  53. package/tools/worktree-changes.test.js +951 -0
  54. package/tools/worktree-close-herdr-async.test.js +204 -0
  55. package/tools/worktree-odd-migration.test.js +224 -0
  56. package/tools/worktree-registry-cache.test.js +163 -0
  57. package/tools/worktree-registry-odd.test.js +231 -0
  58. package/tools/worktree-registry.test.js +244 -0
  59. package/tools/worktree-session-guard.test.js +507 -0
  60. package/tools/worktree-tools.test.js +434 -0
@@ -0,0 +1,507 @@
1
+ 'use strict';
2
+ // S7/S8/S9: lib/worktree-session-guard.js — the ONE active-session guard the
3
+ // `worktree_close` tool and the warp panel's Ctrl+D both run.
4
+ //
5
+ // Gentle Shell publishes its session presence store under
6
+ // `<agent home>/gentle-agents/presence`: a bounded `<sessionHash>.<incarnation>
7
+ // .header.json` heartbeat plus an optional discovery sidecar carrying the
8
+ // session's workspace (`orchestrator-presence.ts`). The guard consumes that
9
+ // published metadata — never a session's transcript — and answers one question
10
+ // per checkout: is a live Pi session working there?
11
+ //
12
+ // What is pinned here:
13
+ // - only RECENT heartbeats count as live (upstream's 15 s TTL), and a
14
+ // heartbeat stamped in the future is not recent either;
15
+ // - the current session is recognised by the hash of its own id and by the
16
+ // cwd it stands on, and both refuse with "warp first";
17
+ // - a foreign live session refuses and names the identity the store
18
+ // publishes (label + session hash prefix; the raw session id is never
19
+ // published there);
20
+ // - a stale or unreadable record is DISCLOSED, never silently promoted to a
21
+ // block and never silently dropped: Gentle Shell's store is bounded, its
22
+ // files can outlive a crashed session, and this guard is best effort;
23
+ // - the scan is bounded, refuses a linked presence directory or a linked
24
+ // record, and never writes, moves or deletes anything.
25
+ //
26
+ // Every fixture is a throwaway agent home under the system temp directory:
27
+ // nothing here reads the developer's real presence store or a real checkout.
28
+
29
+ const { test, describe, after } = require('node:test');
30
+ const assert = require('node:assert/strict');
31
+ const crypto = require('node:crypto');
32
+ const fs = require('node:fs');
33
+ const os = require('node:os');
34
+ const path = require('node:path');
35
+
36
+ const guard = require('../lib/worktree-session-guard.js');
37
+
38
+ const NOW = 1_700_000_000_000;
39
+ const roots = [];
40
+
41
+ after(() => {
42
+ for (const dir of roots) {
43
+ try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
44
+ }
45
+ });
46
+
47
+ function track(dir) {
48
+ roots.push(dir);
49
+ return dir;
50
+ }
51
+
52
+ function agentHome() {
53
+ return track(fs.mkdtempSync(path.join(os.tmpdir(), 'wt-guard-home-')));
54
+ }
55
+
56
+ function checkout(tag) {
57
+ const dir = track(fs.mkdtempSync(path.join(os.tmpdir(), `wt-guard-${tag}-`)));
58
+ return fs.realpathSync(dir);
59
+ }
60
+
61
+ const hash = (id) => crypto.createHash('sha256').update(id).digest('hex');
62
+ const incarnation = (n) => `0000000${n % 16}-0000-4000-8000-00000000000${n % 16}`;
63
+
64
+ function presenceDir(home) {
65
+ return path.join(home, 'gentle-agents', 'presence');
66
+ }
67
+
68
+ // One published session: the header heartbeat plus the discovery sidecar that
69
+ // carries its workspace. `discovery: false` writes a header-only (legacy) peer.
70
+ function publish(home, {
71
+ sessionId = 'session-1', label = 'Worker', workspace = null,
72
+ heartbeat = NOW, incarnationIndex = 1, generation = 3, discovery = true,
73
+ } = {}) {
74
+ const dir = presenceDir(home);
75
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
76
+ try { fs.chmodSync(dir, 0o700); } catch { /* explicit mode is best effort */ }
77
+ const inc = incarnation(incarnationIndex);
78
+ const sessionHash = hash(sessionId);
79
+ const header = {
80
+ schema: 1,
81
+ sessionHash,
82
+ incarnation: inc,
83
+ label,
84
+ heartbeat,
85
+ generation,
86
+ counts: { running: 0, queued: 0, waiting: 0, finished: 0 },
87
+ digest: 'a'.repeat(64),
88
+ unavailable: null,
89
+ };
90
+ fs.writeFileSync(path.join(dir, `${sessionHash}.${inc}.header.json`), JSON.stringify(header));
91
+ if (discovery) {
92
+ fs.writeFileSync(
93
+ path.join(dir, `${sessionHash}.${inc}.discovery.json`),
94
+ JSON.stringify({
95
+ schema: 1,
96
+ sessionHash,
97
+ incarnation: inc,
98
+ generation,
99
+ metadata: { activation: 'b'.repeat(64), workspace: workspace ?? '', tasks: [], omitted: 0 },
100
+ }),
101
+ );
102
+ }
103
+ return { sessionHash, incarnation: inc, label };
104
+ }
105
+
106
+ // One transport activation: `<agent home>/gentle-agents/transport/presence/
107
+ // <sessionId>.<22-char token>.json` with the four published keys. This is the
108
+ // ONLY place a raw session id is published, and it is what the guard joins the
109
+ // presence hash to.
110
+ const TRANSPORT_TOKEN = 'AbCdEfGhIjKlMnOpQrStUv';
111
+ function publishTransport(home, {
112
+ sessionId = 'worker-9', token = TRANSPORT_TOKEN, createdAt = NOW - 1000, record = null,
113
+ } = {}) {
114
+ const dir = path.join(home, 'gentle-agents', 'transport', 'presence');
115
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
116
+ try {
117
+ fs.chmodSync(path.join(home, 'gentle-agents'), 0o700);
118
+ fs.chmodSync(path.join(home, 'gentle-agents', 'transport'), 0o700);
119
+ fs.chmodSync(dir, 0o700);
120
+ } catch { /* explicit modes are best effort */ }
121
+ const body = record ?? {
122
+ version: 1,
123
+ sessionId,
124
+ endpoint: `/tmp/gentle-pi-1000/0123456789abcdef0123456789abcdef/${token}.sock`,
125
+ createdAt,
126
+ };
127
+ const file = path.join(dir, `${sessionId}.${token}.json`);
128
+ fs.writeFileSync(file, JSON.stringify(body), { mode: 0o600 });
129
+ try { fs.chmodSync(file, 0o600); } catch { /* best effort */ }
130
+ return file;
131
+ }
132
+
133
+ async function verdictFor(target, options = {}) {
134
+ const home = options.home ?? agentHome();
135
+ const env = { PI_CODING_AGENT_DIR: home };
136
+ const presence = await guard.readGentlePresence({ env, now: NOW, maxEntries: options.maxEntries });
137
+ const transport = await guard.readTransportSessions({ env, maxEntries: options.maxTransportEntries });
138
+ return guard.evaluateCheckoutSessions({
139
+ targetPath: target,
140
+ presence,
141
+ transport,
142
+ currentSessionId: options.currentSessionId ?? null,
143
+ currentCwd: options.currentCwd ?? null,
144
+ now: NOW,
145
+ });
146
+ }
147
+
148
+ describe('readGentlePresence: bounded, validated, read-only', () => {
149
+ test('no presence directory is reported as unavailable evidence', async () => {
150
+ const home = agentHome();
151
+ const read = await guard.readGentlePresence({ env: { PI_CODING_AGENT_DIR: home }, now: NOW });
152
+ assert.equal(read.status, 'unavailable');
153
+ assert.equal(read.entries.length, 0);
154
+ });
155
+
156
+ test('a published session is read with its workspace and recency', async () => {
157
+ const home = agentHome();
158
+ const target = checkout('live');
159
+ const published = publish(home, { sessionId: 'a', label: 'Alpha', workspace: target, heartbeat: NOW - 1000 });
160
+ const read = await guard.readGentlePresence({ env: { PI_CODING_AGENT_DIR: home }, now: NOW });
161
+ assert.equal(read.status, 'ok');
162
+ assert.equal(read.entries.length, 1);
163
+ assert.equal(read.entries[0].sessionHash, published.sessionHash);
164
+ assert.equal(read.entries[0].label, 'Alpha');
165
+ assert.equal(read.entries[0].workspace, target);
166
+ assert.equal(read.entries[0].recent, true);
167
+ });
168
+
169
+ test('a heartbeat older than the TTL, or stamped in the future, is not recent', async () => {
170
+ const home = agentHome();
171
+ const target = checkout('stale');
172
+ publish(home, { sessionId: 'old', workspace: target, heartbeat: NOW - guard.PRESENCE_FRESH_MS - 1 });
173
+ publish(home, { sessionId: 'future', workspace: target, heartbeat: NOW + 5000, incarnationIndex: 2 });
174
+ const read = await guard.readGentlePresence({ env: { PI_CODING_AGENT_DIR: home }, now: NOW });
175
+ assert.equal(read.status, 'ok');
176
+ assert.deepEqual(read.entries.map((entry) => entry.recent).sort(), [false, false]);
177
+ });
178
+
179
+ test('a linked presence directory is refused instead of followed', async () => {
180
+ const home = agentHome();
181
+ const elsewhere = agentHome();
182
+ publish(elsewhere, { sessionId: 'a', workspace: checkout('link'), heartbeat: NOW });
183
+ const shared = path.join(home, 'gentle-agents');
184
+ fs.mkdirSync(shared, { recursive: true, mode: 0o700 });
185
+ try { fs.symlinkSync(presenceDir(elsewhere), presenceDir(home), 'dir'); } catch { return; }
186
+ const read = await guard.readGentlePresence({ env: { PI_CODING_AGENT_DIR: home }, now: NOW });
187
+ assert.equal(read.status, 'unavailable');
188
+ assert.match(read.reason, /symbolic|link/i);
189
+ });
190
+
191
+ test('a malformed header, a linked header and a directory named like one are rejected, never thrown', async () => {
192
+ const home = agentHome();
193
+ const target = checkout('junk');
194
+ const good = publish(home, { sessionId: 'good', workspace: target, heartbeat: NOW });
195
+ const dir = presenceDir(home);
196
+ const malformed = hash('bad');
197
+ fs.writeFileSync(path.join(dir, `${malformed}.${incarnation(2)}.header.json`), '{"schema":1}');
198
+ const linked = hash('linked');
199
+ const linkSource = path.join(track(fs.mkdtempSync(path.join(os.tmpdir(), 'wt-guard-src-'))), 'header.json');
200
+ fs.writeFileSync(linkSource, JSON.stringify({ schema: 1 }));
201
+ try {
202
+ fs.symlinkSync(linkSource, path.join(dir, `${linked}.${incarnation(3)}.header.json`));
203
+ } catch { /* a host without symlinks skips that half */ }
204
+ fs.mkdirSync(path.join(dir, `${hash('folder')}.${incarnation(4)}.header.json`), { recursive: true });
205
+
206
+ const read = await guard.readGentlePresence({ env: { PI_CODING_AGENT_DIR: home }, now: NOW });
207
+ assert.equal(read.status, 'ok');
208
+ assert.deepEqual(read.entries.map((entry) => entry.sessionHash), [good.sessionHash]);
209
+ assert.ok(read.rejected >= 2, `every unusable record is counted, not thrown: ${read.rejected}`);
210
+ });
211
+
212
+ test('the scan is bounded and says so instead of reading forever', async () => {
213
+ const home = agentHome();
214
+ for (let index = 1; index <= 5; index += 1) {
215
+ publish(home, { sessionId: `s${index}`, workspace: checkout(`bounded-${index}`), heartbeat: NOW, incarnationIndex: index });
216
+ }
217
+ const read = await guard.readGentlePresence({ env: { PI_CODING_AGENT_DIR: home }, now: NOW, maxEntries: 2 });
218
+ assert.equal(read.status, 'ok');
219
+ assert.equal(read.entries.length, 2);
220
+ assert.equal(read.truncated, true);
221
+ });
222
+
223
+ test('reading publishes nothing and removes nothing', async () => {
224
+ const home = agentHome();
225
+ const target = checkout('readonly');
226
+ publish(home, { sessionId: 'a', workspace: target, heartbeat: NOW });
227
+ const dir = presenceDir(home);
228
+ const snapshot = () => fs.readdirSync(dir).sort()
229
+ .map((name) => [name, fs.readFileSync(path.join(dir, name), 'utf8')]);
230
+ const before = snapshot();
231
+ await guard.readGentlePresence({ env: { PI_CODING_AGENT_DIR: home }, now: NOW });
232
+ assert.deepEqual(snapshot(), before);
233
+ });
234
+ });
235
+
236
+ describe('evaluateCheckoutSessions: who is working in the checkout', () => {
237
+ test('a live session in an unrelated checkout never blocks', async () => {
238
+ const home = agentHome();
239
+ const target = checkout('mine');
240
+ publish(home, { sessionId: 'elsewhere', workspace: checkout('other'), heartbeat: NOW });
241
+ const result = await verdictFor(target, { home });
242
+ assert.equal(result.verdict, 'clear');
243
+ assert.equal(result.text, null);
244
+ });
245
+
246
+ test('a live foreign session refuses and names the published identity', async () => {
247
+ const home = agentHome();
248
+ const target = checkout('foreign');
249
+ const published = publish(home, { sessionId: 'worker-9', label: 'Worker', workspace: target, heartbeat: NOW - 2000 });
250
+ const result = await verdictFor(target, { home, currentSessionId: 'mine' });
251
+ assert.equal(result.verdict, 'foreign');
252
+ assert.match(result.text, /working in that checkout/);
253
+ assert.ok(result.text.includes('Worker'), result.text);
254
+ assert.ok(result.text.includes(published.sessionHash.slice(0, 12)), result.text);
255
+ assert.match(result.text, /finish/);
256
+ assert.match(result.text, /\/warp/);
257
+ assert.match(result.text, /left untouched/);
258
+ assert.equal(result.blockers.length, 1);
259
+ assert.equal(result.blockers[0].kind, 'foreign');
260
+ });
261
+
262
+ test('several live foreign sessions are all named, bounded', async () => {
263
+ const home = agentHome();
264
+ const target = checkout('crowd');
265
+ for (let index = 1; index <= 4; index += 1) {
266
+ publish(home, { sessionId: `w${index}`, label: `W${index}`, workspace: target, heartbeat: NOW, incarnationIndex: index });
267
+ }
268
+ const result = await verdictFor(target, { home });
269
+ assert.equal(result.verdict, 'foreign');
270
+ assert.equal(result.blockers.length, 4);
271
+ assert.match(result.text, /and 1 more/);
272
+ });
273
+
274
+ test('this session is recognised by the hash of its own id and told to warp first', async () => {
275
+ const home = agentHome();
276
+ const target = checkout('current');
277
+ publish(home, { sessionId: 'mine', label: 'Me', workspace: target, heartbeat: NOW });
278
+ const result = await verdictFor(target, { home, currentSessionId: 'mine' });
279
+ assert.equal(result.verdict, 'current');
280
+ assert.match(result.text, /this session is working in that checkout/);
281
+ assert.match(result.text, /warp/i);
282
+ assert.match(result.text, /left untouched/);
283
+ });
284
+
285
+ test('this session is caught by its cwd even with no presence at all', async () => {
286
+ const home = agentHome();
287
+ const target = checkout('cwd');
288
+ const result = await verdictFor(target, { home, currentSessionId: 'mine', currentCwd: path.join(target, 'packages', 'app') });
289
+ assert.equal(result.verdict, 'current');
290
+ assert.match(result.text, /warp/i);
291
+ });
292
+
293
+ test('a session inside a SUBDIRECTORY of the checkout counts as working there', async () => {
294
+ const home = agentHome();
295
+ const target = checkout('sub');
296
+ fs.mkdirSync(path.join(target, 'src'), { recursive: true });
297
+ publish(home, { sessionId: 'worker', label: 'W', workspace: path.join(target, 'src'), heartbeat: NOW });
298
+ const result = await verdictFor(target, { home });
299
+ assert.equal(result.verdict, 'foreign');
300
+ });
301
+
302
+ test('a sibling checkout sharing a name prefix is a different checkout', async () => {
303
+ const home = agentHome();
304
+ const parent = checkout('prefix');
305
+ const target = path.join(parent, 'feat-doomed');
306
+ const sibling = path.join(parent, 'feat-doomed-other');
307
+ fs.mkdirSync(target, { recursive: true });
308
+ fs.mkdirSync(sibling, { recursive: true });
309
+ publish(home, { sessionId: 'worker', label: 'W', workspace: sibling, heartbeat: NOW });
310
+ const result = await verdictFor(target, { home });
311
+ assert.equal(result.verdict, 'clear');
312
+ });
313
+
314
+ test('a stale record is disclosed, never promoted to a live session', async () => {
315
+ const home = agentHome();
316
+ const target = checkout('stalematch');
317
+ publish(home, { sessionId: 'gone', label: 'Gone', workspace: target, heartbeat: NOW - guard.PRESENCE_FRESH_MS - 1 });
318
+ const result = await verdictFor(target, { home, currentSessionId: 'mine' });
319
+ assert.equal(result.verdict, 'clear');
320
+ assert.equal(result.blockers.length, 0);
321
+ assert.equal(result.stale.length, 1);
322
+ assert.match(result.notice, /stale/);
323
+ assert.match(result.notice, /best effort/i);
324
+ });
325
+
326
+ test('a header whose discovery sidecar is missing or has no usable workspace is unknown evidence', async () => {
327
+ const home = agentHome();
328
+ const target = checkout('legacy');
329
+ publish(home, { sessionId: 'legacy', label: 'Legacy', workspace: target, heartbeat: NOW, discovery: false });
330
+ publish(home, { sessionId: 'empty', label: 'Empty', workspace: '', heartbeat: NOW, incarnationIndex: 2 });
331
+ publish(home, { sessionId: 'relative', label: 'Relative', workspace: 'relative/path', heartbeat: NOW, incarnationIndex: 3 });
332
+ const result = await verdictFor(target, { home });
333
+ assert.equal(result.verdict, 'clear', 'an unknown workspace can never be attributed to this checkout');
334
+ assert.equal(result.blockers.length, 0);
335
+ assert.ok(result.evidence.unknown >= 3, JSON.stringify(result.evidence));
336
+ });
337
+
338
+ test('a missing presence store is silent (best effort), a broken one is disclosed', async () => {
339
+ const home = agentHome();
340
+ const target = checkout('quiet');
341
+ const absent = await verdictFor(target, { home });
342
+ assert.equal(absent.verdict, 'clear');
343
+ assert.equal(absent.notice, null);
344
+
345
+ const brokenHome = agentHome();
346
+ fs.mkdirSync(path.join(brokenHome, 'gentle-agents'), { recursive: true });
347
+ fs.writeFileSync(presenceDir(brokenHome), 'not a directory');
348
+ const broken = await verdictFor(target, { home: brokenHome });
349
+ assert.equal(broken.verdict, 'clear');
350
+ assert.match(broken.notice, /best effort/i);
351
+ });
352
+
353
+ test('the verdict is stable and the returned details carry no raw session content', async () => {
354
+ const home = agentHome();
355
+ const target = checkout('shape');
356
+ const published = publish(home, { sessionId: 'worker', label: 'Worker', workspace: target, heartbeat: NOW });
357
+ const result = await verdictFor(target, { home });
358
+ assert.deepEqual(Object.keys(result).sort(), ['blockers', 'evidence', 'notice', 'notices', 'stale', 'text', 'verdict']);
359
+ assert.deepEqual(result.blockers[0].label, 'Worker');
360
+ assert.equal(result.blockers[0].sessionHash, published.sessionHash);
361
+ assert.equal(JSON.stringify(result.evidence).includes('Worker'), false, 'the evidence counts carry no session text');
362
+ });
363
+ });
364
+
365
+ describe('S7: a refusal names the RAW session id, joined through transport', () => {
366
+ test('a live foreign session is named by the id Gentle Shell published for it', async () => {
367
+ const home = agentHome();
368
+ const target = checkout('joined');
369
+ const published = publish(home, { sessionId: 'worker-9', label: 'Worker', workspace: target, heartbeat: NOW });
370
+ publishTransport(home, { sessionId: 'worker-9' });
371
+ const result = await verdictFor(target, { home, currentSessionId: 'mine' });
372
+ assert.equal(result.verdict, 'foreign');
373
+ assert.equal(result.blockers[0].rawSessionId, 'worker-9');
374
+ assert.ok(result.text.includes('id worker-9'), result.text);
375
+ assert.equal(result.text.includes(published.sessionHash.slice(0, 12)), false, 'the hash is not used when the id resolves');
376
+ assert.equal(result.text.includes('could not be resolved'), false);
377
+ assert.equal(result.evidence.resolved, 1);
378
+ assert.equal(result.evidence.unresolved, 0);
379
+ });
380
+
381
+ test('the join is deterministic: only the matching transport record names the hash', async () => {
382
+ const home = agentHome();
383
+ const target = checkout('deterministic');
384
+ publish(home, { sessionId: 'worker-9', label: 'Worker', workspace: target, heartbeat: NOW });
385
+ publishTransport(home, { sessionId: 'someone-else', token: 'ZbCdEfGhIjKlMnOpQrStUv' });
386
+ const result = await verdictFor(target, { home });
387
+ assert.equal(result.verdict, 'foreign');
388
+ assert.equal(result.blockers[0].rawSessionId, null);
389
+ assert.match(result.text, /could not be resolved/);
390
+ assert.equal(result.text.includes('someone-else'), false, 'another session never lends its id');
391
+ });
392
+
393
+ test('with no transport record the refusal shows the presence hash and discloses the fallback', async () => {
394
+ const home = agentHome();
395
+ const target = checkout('unjoined');
396
+ const published = publish(home, { sessionId: 'worker-9', label: 'Worker', workspace: target, heartbeat: NOW });
397
+ const result = await verdictFor(target, { home });
398
+ assert.equal(result.verdict, 'foreign');
399
+ assert.equal(result.blockers[0].rawSessionId, null);
400
+ assert.ok(result.text.includes(published.sessionHash.slice(0, 12)), result.text);
401
+ assert.match(result.text, /could not be resolved/);
402
+ assert.match(result.text, /presence/);
403
+ assert.equal(result.evidence.unresolved, 1);
404
+ });
405
+
406
+ test('the current session still refuses with warp guidance, joined or not', async () => {
407
+ const joinedHome = agentHome();
408
+ const target = checkout('current-joined');
409
+ publish(joinedHome, { sessionId: 'mine', label: 'Me', workspace: target, heartbeat: NOW });
410
+ publishTransport(joinedHome, { sessionId: 'mine' });
411
+ const joined = await verdictFor(target, { home: joinedHome, currentSessionId: 'mine' });
412
+ assert.equal(joined.verdict, 'current');
413
+ assert.match(joined.text, /this session is working in that checkout/);
414
+ assert.match(joined.text, /warp/i);
415
+
416
+ const plainHome = agentHome();
417
+ const plainTarget = checkout('current-plain');
418
+ publish(plainHome, { sessionId: 'mine', label: 'Me', workspace: plainTarget, heartbeat: NOW });
419
+ const plain = await verdictFor(plainTarget, { home: plainHome, currentSessionId: 'mine' });
420
+ assert.equal(plain.verdict, 'current');
421
+ assert.match(plain.text, /warp/i);
422
+ });
423
+
424
+ test('a malformed, mismatched, linked or non-private transport record never joins', async () => {
425
+ const home = agentHome();
426
+ const target = checkout('transport-junk');
427
+ publish(home, { sessionId: 'worker-9', label: 'Worker', workspace: target, heartbeat: NOW });
428
+ const dir = path.join(home, 'gentle-agents', 'transport', 'presence');
429
+ // A record whose filename says worker-9 but whose body names another session.
430
+ publishTransport(home, { sessionId: 'worker-9', record: { version: 1, sessionId: 'impostor', endpoint: '/tmp/x.sock', createdAt: NOW } });
431
+ // A body with an unexpected key.
432
+ publishTransport(home, { sessionId: 'worker-9', token: 'CbCdEfGhIjKlMnOpQrStUv', record: { version: 1, sessionId: 'worker-9', endpoint: '/tmp/x.sock', createdAt: NOW, extra: 1 } });
433
+ // A symbolic link named like a record.
434
+ const linkSource = path.join(track(fs.mkdtempSync(path.join(os.tmpdir(), 'wt-guard-transport-src-'))), 'record.json');
435
+ fs.writeFileSync(linkSource, JSON.stringify({ version: 1, sessionId: 'worker-9', endpoint: '/tmp/x.sock', createdAt: NOW }));
436
+ try {
437
+ fs.symlinkSync(linkSource, path.join(dir, `worker-9.DdCdEfGhIjKlMnOpQrStUv.json`));
438
+ } catch { /* a host without symlinks skips that half */ }
439
+
440
+ const result = await verdictFor(target, { home });
441
+ assert.equal(result.verdict, 'foreign');
442
+ assert.equal(result.blockers[0].rawSessionId, null, 'no malformed record is trusted');
443
+ assert.match(result.text, /could not be resolved/);
444
+ });
445
+
446
+ test('a transport scan over its bound is disclosed and never joins on partial evidence', async () => {
447
+ const home = agentHome();
448
+ const target = checkout('transport-bound');
449
+ publish(home, { sessionId: 'worker-9', label: 'Worker', workspace: target, heartbeat: NOW });
450
+ for (const [index, token] of ['EbCdEfGhIjKlMnOpQrStUv', 'FbCdEfGhIjKlMnOpQrStUv'].entries()) {
451
+ publishTransport(home, { sessionId: `other-${index}`, token });
452
+ }
453
+ const read = await guard.readTransportSessions({ env: { PI_CODING_AGENT_DIR: home }, maxEntries: 1 });
454
+ assert.equal(read.status, 'ok');
455
+ assert.equal(read.sessionIds.length, 1);
456
+ assert.equal(read.truncated, true);
457
+ const result = await verdictFor(target, { home, maxTransportEntries: 1 });
458
+ assert.equal(result.verdict, 'foreign');
459
+ assert.match(result.text, /could not be resolved/);
460
+ assert.match(result.text, /bounded transport scan/, 'a stopped scan never claims the peer published nothing');
461
+ assert.equal(/published no transport record/.test(result.text), false);
462
+ });
463
+
464
+ test('reading the transport registry publishes and removes nothing', async () => {
465
+ const home = agentHome();
466
+ publishTransport(home, { sessionId: 'worker-9' });
467
+ const dir = path.join(home, 'gentle-agents', 'transport', 'presence');
468
+ const snapshot = () => fs.readdirSync(dir).sort().map((name) => [name, fs.readFileSync(path.join(dir, name), 'utf8')]);
469
+ const before = snapshot();
470
+ await guard.readTransportSessions({ env: { PI_CODING_AGENT_DIR: home } });
471
+ assert.deepEqual(snapshot(), before);
472
+ });
473
+ });
474
+
475
+ describe('advisory: unattributable and truncated evidence is disclosed, never blocking', () => {
476
+ test('a live record with no published workspace is disclosed in the clear path', async () => {
477
+ const home = agentHome();
478
+ const target = checkout('advisory-unknown');
479
+ publish(home, { sessionId: 'legacy-peer', label: 'Legacy', workspace: target, heartbeat: NOW, discovery: false });
480
+ const result = await verdictFor(target, { home });
481
+ assert.equal(result.verdict, 'clear');
482
+ assert.ok(result.notices.some((line) => /published no workspace/.test(line)), JSON.stringify(result.notices));
483
+ assert.match(result.notice, /best effort|published no workspace/);
484
+ });
485
+
486
+ test('a truncated presence scan is disclosed in the clear path', async () => {
487
+ const home = agentHome();
488
+ const target = checkout('advisory-truncated');
489
+ for (let index = 1; index <= 3; index += 1) {
490
+ publish(home, { sessionId: `peer-${index}`, label: `P${index}`, workspace: checkout(`adv-${index}`), heartbeat: NOW, incarnationIndex: index });
491
+ }
492
+ const result = await verdictFor(target, { home, maxEntries: 1 });
493
+ assert.equal(result.verdict, 'clear');
494
+ assert.ok(result.notices.some((line) => /truncated|first 1 records/.test(line)), JSON.stringify(result.notices));
495
+ });
496
+
497
+ test('a truncated transport scan is disclosed in the clear path', async () => {
498
+ const home = agentHome();
499
+ const target = checkout('advisory-transport');
500
+ for (const [index, token] of ['GbCdEfGhIjKlMnOpQrStUv', 'HbCdEfGhIjKlMnOpQrStUv'].entries()) {
501
+ publishTransport(home, { sessionId: `peer-${index}`, token });
502
+ }
503
+ const result = await verdictFor(target, { home, maxTransportEntries: 1 });
504
+ assert.equal(result.verdict, 'clear');
505
+ assert.ok(result.notices.some((line) => /transport/i.test(line)), JSON.stringify(result.notices));
506
+ });
507
+ });