@wenrwa/marketplace-cli 0.6.5 → 0.7.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 (80) hide show
  1. package/README.md +85 -85
  2. package/dist/__tests__/auth-login-core.test.js +2 -2
  3. package/dist/__tests__/auth.test.js +4 -4
  4. package/dist/__tests__/bridge-autostart.test.js +9 -9
  5. package/dist/__tests__/bridge-checkout-dirty-real-git.test.d.ts +1 -0
  6. package/dist/__tests__/bridge-checkout-dirty-real-git.test.js +145 -0
  7. package/dist/__tests__/bridge-checkout-inventory-reporter.test.js +6 -2
  8. package/dist/__tests__/bridge-checkout-inventory.test.js +128 -14
  9. package/dist/__tests__/bridge-checkout-park-and-remove.test.js +57 -8
  10. package/dist/__tests__/bridge-claude-trust.test.d.ts +1 -0
  11. package/dist/__tests__/bridge-claude-trust.test.js +137 -0
  12. package/dist/__tests__/bridge-command-browser-options.test.js +1 -1
  13. package/dist/__tests__/bridge-command-install-env.test.js +1 -1
  14. package/dist/__tests__/bridge-device-registration.test.js +1 -1
  15. package/dist/__tests__/bridge-folder-registry.test.js +1 -1
  16. package/dist/__tests__/bridge-local-add-core.test.js +1 -1
  17. package/dist/__tests__/bridge-match-pointer.test.d.ts +1 -0
  18. package/dist/__tests__/bridge-match-pointer.test.js +136 -0
  19. package/dist/__tests__/bridge-pairing-api.test.js +1 -1
  20. package/dist/__tests__/bridge-pairing-core.test.js +1 -1
  21. package/dist/__tests__/bridge-park-remove-frame.test.js +61 -0
  22. package/dist/__tests__/bridge-relay-client-checkout-inventory.test.js +1 -1
  23. package/dist/__tests__/bridge-relay-mode.test.js +1 -1
  24. package/dist/__tests__/bridge-rival-touch-forwarder.test.d.ts +1 -0
  25. package/dist/__tests__/bridge-rival-touch-forwarder.test.js +266 -0
  26. package/dist/__tests__/bridge-server.test.js +1 -1
  27. package/dist/__tests__/config.test.js +1 -1
  28. package/dist/__tests__/devices.test.js +1 -1
  29. package/dist/__tests__/local.test.js +1 -1
  30. package/dist/__tests__/openBrowser.test.js +1 -1
  31. package/dist/__tests__/websocket.test.js +3 -3
  32. package/dist/auth/identity.d.ts +1 -1
  33. package/dist/auth/login.d.ts +2 -2
  34. package/dist/bridge/apiClient.d.ts +14 -0
  35. package/dist/bridge/apiClient.js +19 -2
  36. package/dist/bridge/autostart.d.ts +1 -1
  37. package/dist/bridge/autostart.js +1 -1
  38. package/dist/bridge/checkoutInventory.d.ts +48 -17
  39. package/dist/bridge/checkoutInventory.js +190 -44
  40. package/dist/bridge/checkoutParkAndRemove.d.ts +22 -1
  41. package/dist/bridge/checkoutParkAndRemove.js +37 -7
  42. package/dist/bridge/claudeTrust.d.ts +37 -0
  43. package/dist/bridge/claudeTrust.js +85 -0
  44. package/dist/bridge/config.d.ts +1 -1
  45. package/dist/bridge/contextWriter.d.ts +2 -0
  46. package/dist/bridge/contextWriter.js +3 -2
  47. package/dist/bridge/deviceRegistration.d.ts +1 -1
  48. package/dist/bridge/envSanitize.d.ts +1 -1
  49. package/dist/bridge/envSanitize.js +1 -1
  50. package/dist/bridge/folderRegistry.js +1 -1
  51. package/dist/bridge/localAdd.js +1 -1
  52. package/dist/bridge/localContextWriter.d.ts +2 -0
  53. package/dist/bridge/localContextWriter.js +3 -2
  54. package/dist/bridge/memoryBrief.d.ts +16 -4
  55. package/dist/bridge/memoryBrief.js +17 -9
  56. package/dist/bridge/pairing.d.ts +1 -1
  57. package/dist/bridge/relayClient.js +11 -1
  58. package/dist/bridge/rivalTouchForwarder.d.ts +48 -0
  59. package/dist/bridge/rivalTouchForwarder.js +211 -0
  60. package/dist/bridge/server.d.ts +14 -1
  61. package/dist/bridge/server.js +24 -0
  62. package/dist/bridge/workGraphPointer.d.ts +19 -0
  63. package/dist/bridge/workGraphPointer.js +26 -1
  64. package/dist/commands/apps.js +1 -1
  65. package/dist/commands/auth.d.ts +1 -1
  66. package/dist/commands/auth.js +6 -6
  67. package/dist/commands/bridge.js +50 -13
  68. package/dist/commands/credits.js +1 -1
  69. package/dist/commands/devices.d.ts +1 -1
  70. package/dist/commands/devices.js +4 -4
  71. package/dist/commands/local.d.ts +1 -1
  72. package/dist/commands/local.js +5 -5
  73. package/dist/commands/pair.d.ts +1 -1
  74. package/dist/commands/pair.js +4 -4
  75. package/dist/index.js +2 -2
  76. package/dist/utils/config.js +2 -2
  77. package/dist/utils/openBrowser.d.ts +1 -1
  78. package/dist/utils/openBrowser.js +1 -1
  79. package/dist/utils/websocket.js +1 -1
  80. package/package.json +3 -2
@@ -349,4 +349,65 @@ describe('relay client — park-remove frame (hcs-06)', () => {
349
349
  await closeRelay(relay.wss);
350
350
  }
351
351
  });
352
+ it('park-save backs up and KEEPS the checkout: committing, pushing, then a saved result — no remove', async () => {
353
+ const relay = await startFakeRelay();
354
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
355
+ const gitFixture = makeGitFixture();
356
+ const reclaimFs = makeReclaimFsFixture([`${REPOS}/ws-1`]);
357
+ const handle = (0, relayClient_1.startRelayClient)({
358
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
359
+ relayToken: 'tok',
360
+ sessions,
361
+ enableReclaim: true,
362
+ reposBaseDir: REPOS,
363
+ checkoutReclaimDeps: reclaimFs,
364
+ enableParkAndRemove: true,
365
+ checkoutParkAndRemoveDeps: { exists: reclaimFs.exists, run: gitFixture.run },
366
+ });
367
+ try {
368
+ const ws = await relay.nextConnection();
369
+ const q = frameQueue(ws);
370
+ await q.next(); // auth
371
+ ws.send(JSON.stringify({ t: 'park-save', workspaceId: 'ws-1' }));
372
+ expect(await q.next()).toMatchObject({ t: 'park-remove-progress', phase: 'committing' });
373
+ expect(await q.next()).toMatchObject({ t: 'park-remove-progress', phase: 'pushing' });
374
+ expect(await q.next()).toMatchObject({ t: 'park-remove-result', workspaceId: 'ws-1', outcome: 'saved', branch: 'park/ws-1' });
375
+ expect(reclaimFs.removed).toEqual([]);
376
+ }
377
+ finally {
378
+ handle.close();
379
+ await closeRelay(relay.wss);
380
+ }
381
+ });
382
+ it('inventory-scan re-scans and sends a fresh checkout-inventory frame', async () => {
383
+ const relay = await startFakeRelay();
384
+ const sessions = new sessionManager_1.SessionManager(makeFactory());
385
+ let lists = 0;
386
+ const handle = (0, relayClient_1.startRelayClient)({
387
+ relayUrl: `ws://127.0.0.1:${relay.port}`,
388
+ relayToken: 'tok',
389
+ sessions,
390
+ reportCheckoutInventory: true,
391
+ reposBaseDir: REPOS,
392
+ checkoutInventoryDeps: {
393
+ listCheckouts: () => { lists += 1; return []; },
394
+ statMtimeMs: () => 1,
395
+ sizeBytes: async () => 0,
396
+ run: async () => ({ stdout: '', code: 0 }),
397
+ },
398
+ });
399
+ try {
400
+ const ws = await relay.nextConnection();
401
+ const q = frameQueue(ws);
402
+ await q.next(); // auth
403
+ expect(await q.next()).toMatchObject({ t: 'checkout-inventory' }); // on-connect scan
404
+ ws.send(JSON.stringify({ t: 'inventory-scan' }));
405
+ expect(await q.next()).toEqual({ t: 'checkout-inventory', checkouts: [] });
406
+ expect(lists).toBe(2);
407
+ }
408
+ finally {
409
+ handle.close();
410
+ await closeRelay(relay.wss);
411
+ }
412
+ });
352
413
  });
@@ -127,7 +127,7 @@ describe('relay client — checkout inventory scan never blocks connect (hcs-02
127
127
  const inventory = await q.next();
128
128
  expect(inventory).toEqual({
129
129
  t: 'checkout-inventory',
130
- checkouts: [{ workspaceId: 'ws-a', unparseable: false, bytes: 4096, dirty: false, mtimeMs: 100 }],
130
+ checkouts: [{ workspaceId: 'ws-a', unparseable: false, bytes: 4096, dirty: false, mtimeMs: 100, dirtyKinds: [], noUpstream: false, scanVersion: 2 }],
131
131
  });
132
132
  handle.close();
133
133
  await closeRelay(relay.wss);
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const bridge_1 = require("../commands/bridge");
4
- describe('resolveRelayMode — CLI/env wiring for `wen-market bridge --relay`', () => {
4
+ describe('resolveRelayMode — CLI/env wiring for `hiveop bridge --relay`', () => {
5
5
  it('no --relay flag and no RELAY_URL env → local mode', () => {
6
6
  expect((0, bridge_1.resolveRelayMode)({}, {})).toEqual({ mode: 'local' });
7
7
  });
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ /**
40
+ * rival-10 — a LOCAL agent's live file touches reach a ranked rival, like a
41
+ * hosted agent's. What leaves the laptop: agent id + file paths, only for a
42
+ * workspace the server says is a live ranked match this user owns. Nothing
43
+ * else, and nothing at all for any other workspace.
44
+ */
45
+ const ws_1 = __importDefault(require("ws"));
46
+ const fs = __importStar(require("fs"));
47
+ const os = __importStar(require("os"));
48
+ const path = __importStar(require("path"));
49
+ const server_1 = require("../bridge/server");
50
+ const sessionManager_1 = require("../bridge/sessionManager");
51
+ const rivalTouchForwarder_1 = require("../bridge/rivalTouchForwarder");
52
+ const apiClient_1 = require("../bridge/apiClient");
53
+ const MATCH_WS = 'bbbbbbbb-0000-4000-8000-00000000000b';
54
+ const OTHER_WS = 'dddddddd-0000-4000-8000-00000000000d';
55
+ const CHECKOUT = '/Users/bob/wenrwa-workspaces/m-11111111-b';
56
+ function harness(statusOf, opts = {}) {
57
+ let now = 1_000_000;
58
+ const asked = [];
59
+ const sent = [];
60
+ const fwd = (0, rivalTouchForwarder_1.createRivalTouchForwarder)({
61
+ status: async (ws) => {
62
+ asked.push(ws);
63
+ const s = statusOf[ws] ?? { live: false, recheckMs: 600_000 };
64
+ if (s instanceof Error)
65
+ throw s;
66
+ return s;
67
+ },
68
+ send: async (workspaceId, touches) => {
69
+ if (opts.sendFails)
70
+ throw new Error('409 RANKED_RIVAL_CLOSED');
71
+ sent.push({ workspaceId, touches });
72
+ },
73
+ now: () => now,
74
+ scheduleInterval: () => 'h',
75
+ cancelInterval: () => undefined,
76
+ });
77
+ return { fwd, asked, sent, advance: (ms) => void (now += ms) };
78
+ }
79
+ describe('forwardablePaths — only paths, and only inside the checkout', () => {
80
+ it('relativises an absolute path under the checkout; keeps relative paths', () => {
81
+ expect((0, rivalTouchForwarder_1.forwardablePaths)([`${CHECKOUT}/src/planner.ts`, 'src/b.ts'], CHECKOUT)).toEqual(['src/planner.ts', 'src/b.ts']);
82
+ });
83
+ it('drops a path outside the checkout, climbing out, non-strings, duplicates', () => {
84
+ expect((0, rivalTouchForwarder_1.forwardablePaths)(['/etc/hosts', `${CHECKOUT}/../x.ts`, '../up.ts', 42, null, 'a.ts', 'a.ts'], CHECKOUT)).toEqual(['a.ts']);
85
+ expect((0, rivalTouchForwarder_1.forwardablePaths)('src/a.ts', CHECKOUT)).toEqual([]);
86
+ });
87
+ it('no known checkout: an absolute path is kept for the server to place (or drop)', () => {
88
+ expect((0, rivalTouchForwarder_1.forwardablePaths)(['/home/x/m-b/src/a.ts'])).toEqual(['/home/x/m-b/src/a.ts']);
89
+ });
90
+ it('caps paths per touch', () => {
91
+ expect((0, rivalTouchForwarder_1.forwardablePaths)(Array.from({ length: 40 }, (_, i) => `f${i}.ts`))).toHaveLength(20);
92
+ });
93
+ });
94
+ describe('createRivalTouchForwarder', () => {
95
+ it('a live match workspace: the touch goes up as agent id + paths — nothing else', async () => {
96
+ const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } });
97
+ h.fwd.touch({
98
+ workspaceId: MATCH_WS,
99
+ agentId: 'agent-1',
100
+ filePaths: [`${CHECKOUT}/src/planner.ts`],
101
+ cwd: CHECKOUT,
102
+ // A caller passing more than it should: never forwarded.
103
+ ...{ thinking: 'Editing planner.ts', tool: 'Edit', content: 'secret code' },
104
+ });
105
+ await h.fwd.flush();
106
+ expect(h.asked).toEqual([MATCH_WS]);
107
+ expect(h.sent).toEqual([{ workspaceId: MATCH_WS, touches: [{ agentId: 'agent-1', filePaths: ['src/planner.ts'] }] }]);
108
+ const json = JSON.stringify(h.sent);
109
+ for (const w of ['Editing', 'Edit"', 'secret code', '/Users/bob'])
110
+ expect(json).not.toContain(w);
111
+ });
112
+ it('any other workspace: asked once (workspace id only), then nothing sent — and not asked again until recheck', async () => {
113
+ const h = harness({ [OTHER_WS]: { live: false, recheckMs: 600_000 } });
114
+ h.fwd.touch({ workspaceId: OTHER_WS, agentId: 'agent-1', filePaths: ['src/a.ts'] });
115
+ await h.fwd.flush();
116
+ h.fwd.touch({ workspaceId: OTHER_WS, agentId: 'agent-1', filePaths: ['src/b.ts'] });
117
+ await h.fwd.flush();
118
+ expect(h.sent).toEqual([]);
119
+ expect(h.asked).toEqual([OTHER_WS]);
120
+ });
121
+ it('before GO the server says "ask again soon"; touches then are dropped, the ones after GO fly', async () => {
122
+ const status = { [MATCH_WS]: { live: false, recheckMs: 5_000 } };
123
+ const h = harness(status);
124
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['early.ts'] });
125
+ await h.fwd.flush();
126
+ expect(h.sent).toEqual([]);
127
+ status[MATCH_WS] = { live: true, recheckMs: 15_000 };
128
+ h.advance(5_001);
129
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['late.ts'] });
130
+ await h.fwd.flush();
131
+ expect(h.sent.flatMap((s) => s.touches)).toEqual([{ agentId: 'a', filePaths: ['late.ts'] }]);
132
+ });
133
+ it('the match ends: the next status says no, and forwarding stops', async () => {
134
+ const status = { [MATCH_WS]: { live: true, recheckMs: 15_000 } };
135
+ const h = harness(status);
136
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['one.ts'] });
137
+ await h.fwd.flush();
138
+ status[MATCH_WS] = { live: false, recheckMs: 600_000 };
139
+ h.advance(15_001);
140
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['two.ts'] });
141
+ await h.fwd.flush();
142
+ expect(h.sent.flatMap((s) => s.touches.flatMap((t) => t.filePaths))).toEqual(['one.ts']);
143
+ });
144
+ it('a status failure is "not live": nothing is sent', async () => {
145
+ const h = harness({ [MATCH_WS]: new Error('offline') });
146
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['x.ts'] });
147
+ await h.fwd.flush();
148
+ expect(h.sent).toEqual([]);
149
+ });
150
+ it('a refused POST stops forwarding for that workspace until it is asked again', async () => {
151
+ const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } }, { sendFails: true });
152
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['x.ts'] });
153
+ await h.fwd.flush();
154
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['y.ts'] });
155
+ await h.fwd.flush();
156
+ expect(h.asked).toEqual([MATCH_WS]); // backed off: y.ts was dropped, not re-asked at once
157
+ });
158
+ it('a touch with no forwardable path, or no workspace, leaves nothing and asks nothing', async () => {
159
+ const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } });
160
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: ['/etc/hosts'], cwd: CHECKOUT });
161
+ h.fwd.touch({ workspaceId: '', agentId: 'a', filePaths: ['x.ts'] });
162
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: undefined });
163
+ await h.fwd.flush();
164
+ expect(h.asked).toEqual([]);
165
+ expect(h.sent).toEqual([]);
166
+ });
167
+ it('batches: many touches in one flush go in requests of at most 50', async () => {
168
+ const h = harness({ [MATCH_WS]: { live: true, recheckMs: 15_000 } });
169
+ for (let i = 0; i < 120; i++)
170
+ h.fwd.touch({ workspaceId: MATCH_WS, agentId: 'a', filePaths: [`f${i}.ts`] });
171
+ await h.fwd.flush();
172
+ expect(h.sent.map((s) => s.touches.length)).toEqual([50, 50, 20]);
173
+ });
174
+ });
175
+ describe('apiClient — the two calls, over the Connect Token', () => {
176
+ it('status sends the workspace id only; POST sends { touches: [{ agentId, filePaths }] } only', async () => {
177
+ const calls = [];
178
+ const fetchImpl = async (url, init) => {
179
+ calls.push({ url, init });
180
+ return { ok: true, status: 200, json: async () => ({ success: true, data: { live: true, recheckMs: 15_000 } }) };
181
+ };
182
+ const api = { apiUrl: 'https://api.example/api/v1', connectToken: 'wm_ct_x', fetchImpl };
183
+ await expect((0, apiClient_1.getRivalTouchStatus)(api, MATCH_WS)).resolves.toEqual({ live: true, recheckMs: 15_000 });
184
+ await (0, apiClient_1.postRivalTouches)(api, MATCH_WS, [{ agentId: 'a', filePaths: ['src/x.ts'], thinking: 'no' }]);
185
+ expect(calls[0].url).toBe(`https://api.example/api/v1/ranked/workspaces/${MATCH_WS}/touches`);
186
+ expect(calls[0].init.method).toBe('GET');
187
+ expect(calls[0].init.body).toBeUndefined();
188
+ expect(calls[0].init.headers.Authorization).toBe('Bearer wm_ct_x');
189
+ expect(calls[1].init.method).toBe('POST');
190
+ expect(JSON.parse(calls[1].init.body)).toEqual({ touches: [{ agentId: 'a', filePaths: ['src/x.ts'] }] });
191
+ });
192
+ });
193
+ // ── the loopback server: which records reach the forwarder ──────────────────
194
+ class FakePty {
195
+ write() { }
196
+ resize() { }
197
+ kill() { }
198
+ onData() { }
199
+ onExit() { }
200
+ }
201
+ const ORIGIN = 'https://app.hiveop.io';
202
+ const TOKEN = 'session-token-123';
203
+ describe('bridge server → forwardRivalTouch', () => {
204
+ it("a local workspace agent's tailed record hands on workspace + agent + paths (never thinking/tool); a record for any other session hands on nothing", async () => {
205
+ const file = fs.mkdtempSync(path.join(os.tmpdir(), 'rival10-activity-')) + '/activity.jsonl';
206
+ fs.writeFileSync(file, '');
207
+ const tick = { fn: null };
208
+ const forwarded = [];
209
+ const handle = await (0, server_1.startBridgeServer)({
210
+ sessionToken: TOKEN,
211
+ allowedOrigin: ORIGIN,
212
+ sessions: new sessionManager_1.SessionManager({ spawn: () => new FakePty() }),
213
+ resolveLocalAgentCwd: () => CHECKOUT,
214
+ activityFile: file,
215
+ hiveOpSettingsPath: '/Users/t/.wen-market/claude-hooks-settings.json',
216
+ activityScheduleInterval: (cb) => { tick.fn = cb; return 'h'; },
217
+ activityCancelInterval: () => { },
218
+ forwardRivalTouch: (t) => forwarded.push(t),
219
+ });
220
+ try {
221
+ const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: ORIGIN } });
222
+ const frames = [];
223
+ ws.on('message', (raw) => frames.push(JSON.parse(String(raw))));
224
+ await new Promise((r) => ws.on('open', r));
225
+ ws.send(JSON.stringify({ v: 1, token: TOKEN }));
226
+ ws.send(JSON.stringify({ t: 'open', cols: 80, rows: 24, agentId: 'agent-1', workspaceId: MATCH_WS }));
227
+ await waitFor(() => frames.some((f) => f.t === 'opened'));
228
+ fs.appendFileSync(file, [
229
+ { sessionId: 'agent-1', filePaths: [`${CHECKOUT}/src/planner.ts`], thinking: 'Editing planner.ts', tool: 'Edit' },
230
+ { sessionId: 'agent-1', thinking: 'Running npm test', tool: 'Bash' }, // no paths: nothing to forward
231
+ { sessionId: 'crew-member-9', filePaths: ['src/other.ts'] }, // not a loopback agent of a workspace
232
+ ].map((r) => JSON.stringify(r)).join('\n') + '\n');
233
+ tick.fn?.();
234
+ await waitFor(() => frames.filter((f) => f.t === 'activity').length === 3);
235
+ expect(forwarded).toEqual([
236
+ { workspaceId: MATCH_WS, agentId: 'agent-1', filePaths: [`${CHECKOUT}/src/planner.ts`], cwd: CHECKOUT },
237
+ ]);
238
+ // The pilot's own browser still gets the full frame, unchanged.
239
+ expect(frames.find((f) => f.t === 'activity')).toEqual({
240
+ t: 'activity',
241
+ sessionId: 'agent-1',
242
+ filePaths: [`${CHECKOUT}/src/planner.ts`],
243
+ thinking: 'Editing planner.ts',
244
+ });
245
+ // Closed: its later records are not forwarded.
246
+ ws.send(JSON.stringify({ t: 'close', sessionId: 'agent-1' }));
247
+ await new Promise((r) => setTimeout(r, 30));
248
+ fs.appendFileSync(file, JSON.stringify({ sessionId: 'agent-1', filePaths: ['src/late.ts'] }) + '\n');
249
+ tick.fn?.();
250
+ await new Promise((r) => setTimeout(r, 30));
251
+ expect(forwarded).toHaveLength(1);
252
+ ws.close();
253
+ }
254
+ finally {
255
+ await handle.close();
256
+ }
257
+ });
258
+ });
259
+ async function waitFor(cond, ms = 2_000) {
260
+ const until = Date.now() + ms;
261
+ while (!cond()) {
262
+ if (Date.now() > until)
263
+ throw new Error('timed out');
264
+ await new Promise((r) => setTimeout(r, 5));
265
+ }
266
+ }
@@ -182,7 +182,7 @@ describe('bridge WebSocket server', () => {
182
182
  });
183
183
  const before = await (await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: ORIGIN } })).json();
184
184
  expect(before.localWorkspaces).toEqual([]);
185
- // Simulate `wen-market local add` mutating the registry mid-run.
185
+ // Simulate `hiveop local add` mutating the registry mid-run.
186
186
  entries = [{ workspaceId: 'ws-1', name: 'my-app', path: 'my-app' }];
187
187
  const after = await (await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: ORIGIN } })).json();
188
188
  expect(after.localWorkspaces).toEqual([{ workspaceId: 'ws-1', name: 'my-app', path: 'my-app' }]);
@@ -98,7 +98,7 @@ describe('requireConfig()', () => {
98
98
  (0, config_1.requireConfig)();
99
99
  expect(exitSpy).toHaveBeenCalledWith(1);
100
100
  const messages = errSpy.mock.calls.map(c => c.join(' ')).join('\n');
101
- expect(messages).toContain('wen-market auth login');
101
+ expect(messages).toContain('hiveop auth login');
102
102
  exitSpy.mockRestore();
103
103
  errSpy.mockRestore();
104
104
  });
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  /**
7
- * TDD — lws-03: `wen-market devices` list/revoke. Uses this machine's own
7
+ * TDD — lws-03: `hiveop devices` list/revoke. Uses this machine's own
8
8
  * Connect Token (bridge-config.json) — no interactive login required.
9
9
  */
10
10
  const chalk_1 = __importDefault(require("chalk"));
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  /**
7
- * TDD — lws-03: `wen-market local add/list/remove`. Folder→workspace
7
+ * TDD — lws-03: `hiveop local add/list/remove`. Folder→workspace
8
8
  * registry commands. Mocks the bridge config/apiClient/folderRegistry
9
9
  * modules — no real network, no real fs (prior art: workspace.test.ts).
10
10
  */
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /**
4
- * TDD — wha-08: `wen-market auth login` tries to open the approval URL in a
4
+ * TDD — wha-08: `hiveop auth login` tries to open the approval URL in a
5
5
  * browser as best-effort convenience. The code + URL are always printed
6
6
  * regardless (works over SSH), so any failure here must be swallowed, never
7
7
  * surfaced. spawnFn is injectable so this never touches a real process.
@@ -117,7 +117,7 @@ describe('connectWebSocket() handshake (sio-07)', () => {
117
117
  err.data = { code: 'CREDENTIAL_INVALID' };
118
118
  fake.emit('connect_error', err);
119
119
  const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
120
- expect(printed).toMatch(/wen-market auth login/);
120
+ expect(printed).toMatch(/hiveop auth login/);
121
121
  errorSpy.mockRestore();
122
122
  });
123
123
  it('shows a clear message and tells the user to log in again on SESSION_REVOKED', () => {
@@ -129,7 +129,7 @@ describe('connectWebSocket() handshake (sio-07)', () => {
129
129
  err.data = { code: 'SESSION_REVOKED' };
130
130
  fake.emit('connect_error', err);
131
131
  const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
132
- expect(printed).toMatch(/wen-market auth login/);
132
+ expect(printed).toMatch(/hiveop auth login/);
133
133
  errorSpy.mockRestore();
134
134
  });
135
135
  it('falls back to a generic message for other connect errors', () => {
@@ -140,7 +140,7 @@ describe('connectWebSocket() handshake (sio-07)', () => {
140
140
  fake.emit('connect_error', new Error('ECONNREFUSED'));
141
141
  const printed = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n');
142
142
  expect(printed).toContain('ECONNREFUSED');
143
- expect(printed).not.toMatch(/wen-market auth login/);
143
+ expect(printed).not.toMatch(/hiveop auth login/);
144
144
  errorSpy.mockRestore();
145
145
  });
146
146
  it('subscribes to the requested channel on connect, unaffected by the auth change', () => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * wha-08 — the one authenticated call `wen-market auth login` (device flow,
2
+ * wha-08 — the one authenticated call `hiveop auth login` (device flow,
3
3
  * to print "Signed in as <name>") and `auth login --api-key` (CI, to verify
4
4
  * the key before saving it) both need. Sends the key as X-API-Key — never
5
5
  * X-Wallet-Pubkey, the retired header this whole PRD exists to remove.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * wha-08 — `wen-market auth login`: CLI browser login. Device-code pairing
2
+ * wha-08 — `hiveop auth login`: CLI browser login. Device-code pairing
3
3
  * (purpose=cli_login on the shared lws-14 mechanism), same GitHub-CLI /
4
4
  * Apple-TV pattern as bridge pairing (bridge/pairing.ts) — a code is minted,
5
5
  * printed with the approval URL, and polled until a human decides in the
6
6
  * browser.
7
7
  *
8
- * Deliberately NOT shared with bridge/pairing.ts: `wen-market pair` (bridge
8
+ * Deliberately NOT shared with bridge/pairing.ts: `hiveop pair` (bridge
9
9
  * pairing) must keep working byte-for-byte unchanged, and cli_login has an
10
10
  * outcome bridge pairing's loop never handles — the approval screen lets the
11
11
  * human DENY a CLI login, and this CLI needs to say so plainly rather than
@@ -107,3 +107,17 @@ export declare function createLocalWorkspace(opts: BridgeApiOpts, params: {
107
107
  export declare function deleteWorkspace(opts: BridgeApiOpts, workspaceId: string): Promise<{
108
108
  success: boolean;
109
109
  }>;
110
+ /**
111
+ * rival-10 — should this bridge forward its agents' file touches in
112
+ * `workspaceId`? Sends the workspace id only. `live` is true only for a ranked
113
+ * match workspace this user owns while the match is in play.
114
+ */
115
+ export declare function getRivalTouchStatus(opts: BridgeApiOpts, workspaceId: string): Promise<{
116
+ live: boolean;
117
+ recheckMs: number;
118
+ }>;
119
+ /** rival-10 — forward live touches: agent id + file paths per touch, nothing else. */
120
+ export declare function postRivalTouches(opts: BridgeApiOpts, workspaceId: string, touches: Array<{
121
+ agentId: string;
122
+ filePaths: string[];
123
+ }>): Promise<void>;
@@ -8,6 +8,8 @@ exports.listDevices = listDevices;
8
8
  exports.revokeDevice = revokeDevice;
9
9
  exports.createLocalWorkspace = createLocalWorkspace;
10
10
  exports.deleteWorkspace = deleteWorkspace;
11
+ exports.getRivalTouchStatus = getRivalTouchStatus;
12
+ exports.postRivalTouches = postRivalTouches;
11
13
  class BridgeApiError extends Error {
12
14
  code;
13
15
  status;
@@ -25,12 +27,12 @@ exports.BridgeApiError = BridgeApiError;
25
27
  * machines) comes back PAT_FORBIDDEN with a route-allowlist message that
26
28
  * means nothing to an end user. Map it here, at the one place every bridge
27
29
  * API error is constructed, so EVERY call site (registerDevice, MCP
28
- * registration, local workspace create/delete, `wen-market devices`, ...)
30
+ * registration, local workspace create/delete, `hiveop devices`, ...)
29
31
  * gets the same actionable hint with no per-call-site plumbing.
30
32
  */
31
33
  function errorMessageFor(code, rawMessage, status) {
32
34
  if (code === 'PAT_FORBIDDEN') {
33
- return 'your pairing is out of date — run `wen-market pair` again';
35
+ return 'your pairing is out of date — run `hiveop pair` again';
34
36
  }
35
37
  return rawMessage || `HTTP ${status}`;
36
38
  }
@@ -96,3 +98,18 @@ function createLocalWorkspace(opts, params) {
96
98
  function deleteWorkspace(opts, workspaceId) {
97
99
  return bridgeRequest(opts, 'DELETE', `/workspaces/${workspaceId}`);
98
100
  }
101
+ /**
102
+ * rival-10 — should this bridge forward its agents' file touches in
103
+ * `workspaceId`? Sends the workspace id only. `live` is true only for a ranked
104
+ * match workspace this user owns while the match is in play.
105
+ */
106
+ async function getRivalTouchStatus(opts, workspaceId) {
107
+ const json = await bridgeRequest(opts, 'GET', `/ranked/workspaces/${encodeURIComponent(workspaceId)}/touches`);
108
+ return { live: json.data?.live === true, recheckMs: Number(json.data?.recheckMs) || 30_000 };
109
+ }
110
+ /** rival-10 — forward live touches: agent id + file paths per touch, nothing else. */
111
+ async function postRivalTouches(opts, workspaceId, touches) {
112
+ await bridgeRequest(opts, 'POST', `/ranked/workspaces/${encodeURIComponent(workspaceId)}/touches`, {
113
+ touches: touches.map((t) => ({ agentId: t.agentId, filePaths: t.filePaths })),
114
+ });
115
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Bridge autostart (bct follow-up, 2026-07-30; PATH/pin fix, 2026-08-13) —
3
- * `wen-market bridge install`.
3
+ * `hiveop bridge install`.
4
4
  *
5
5
  * Turns the one-liner into one-time-EVER setup: a macOS LaunchAgent starts
6
6
  * the bridge at login and keeps it alive, so the Terminal pane always finds
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Bridge autostart (bct follow-up, 2026-07-30; PATH/pin fix, 2026-08-13) —
4
- * `wen-market bridge install`.
4
+ * `hiveop bridge install`.
5
5
  *
6
6
  * Turns the one-liner into one-time-EVER setup: a macOS LaunchAgent starts
7
7
  * the bridge at login and keeps it alive, so the Terminal pane always finds
@@ -38,9 +38,6 @@
38
38
  * (Promise.all) per checkout, and why the whole scan is fire-and-forget from
39
39
  * relayClient's point of view.
40
40
  */
41
- /** `cmd args...` runner — same shape used across repoCheckout.ts /
42
- * changesReport.ts's injected `run` (git invoked as `git -C <path> ...`,
43
- * never via a shell cwd option, so this stays a flat argv runner). */
44
41
  export type InventoryRun = (cmd: string, args: string[]) => Promise<{
45
42
  stdout: string;
46
43
  code: number;
@@ -63,37 +60,71 @@ export interface CheckoutInventoryDeps {
63
60
  /** git runner, invoked as `run('git', ['-C', path, ...])` — see the module
64
61
  * doc comment. */
65
62
  run: InventoryRun;
63
+ /** Reads a checkout file (CLAUDE.md, for the managed-region check).
64
+ * Defaults to the real filesystem. */
65
+ readText?: ReadText;
66
66
  }
67
+ /**
68
+ * Inventory format version. 2 (cli 0.6.9) adds `dirtyKinds`/`noUpstream`, a
69
+ * dirty check that ignores the bridge's own CLAUDE.md region, and the
70
+ * `park-save` / `inventory-scan` frames. Stamped on EVERY entry so the arena
71
+ * and backend can tell a box that understands Save from one that does not —
72
+ * an old bridge would treat an unknown frame as an error, never as a park.
73
+ */
74
+ export declare const INVENTORY_SCAN_VERSION = 2;
75
+ export type DirtyKind = 'uncommitted' | 'unpushed' | 'stashed';
67
76
  export interface ParsedCheckoutEntry {
68
77
  workspaceId: string;
69
78
  unparseable: false;
70
79
  bytes: number;
71
80
  dirty: boolean;
72
81
  mtimeMs: number;
82
+ /** Which of the three conditions made it dirty (empty when clean). */
83
+ dirtyKinds: DirtyKind[];
84
+ /** The branch tracks nothing (never pushed, or its remote branch is gone). */
85
+ noUpstream: boolean;
86
+ scanVersion: number;
73
87
  }
74
88
  export interface UnparseableCheckoutEntry {
75
89
  workspaceId: string;
76
90
  unparseable: true;
77
91
  reason: string;
92
+ scanVersion: number;
78
93
  }
79
94
  export type CheckoutInventoryEntry = ParsedCheckoutEntry | UnparseableCheckoutEntry;
95
+ /** Reads a text file, or null when it cannot. Injected in tests. */
96
+ export type ReadText = (absPath: string) => string | null;
97
+ /** CLAUDE.md with the bridge's managed region (contextWriter.ts) cut out. */
98
+ export declare function stripManagedRegion(text: string): string;
99
+ export interface DirtyReport {
100
+ dirty: boolean;
101
+ kinds: DirtyKind[];
102
+ noUpstream: boolean;
103
+ }
80
104
  /**
81
- * PRD "Dirty means" section: uncommitted changes (`git status --porcelain`
82
- * non-empty), unpushed commits (`git rev-list @{u}..HEAD --count` > 0), OR
83
- * stashes (`git stash list` non-empty) — any ONE independently marks the
84
- * checkout dirty. `status`/`stash` failing outright (non-zero exit) means the
85
- * directory isn't a usable git checkout at all — 'unparseable', not clean.
86
- * `rev-list @{u}..HEAD` failing is the ordinary "no upstream configured"
87
- * case (a branch with nothing to compare against) and is treated as simply
88
- * "not unpushed" rather than a parse failure — every real checkout has a
89
- * working `status`/`stash`, so those two staying reliable is what actually
90
- * distinguishes a healthy checkout from a broken one.
105
+ * PRD "Dirty means" section: uncommitted changes, unpushed commits, OR
106
+ * stashes — any ONE independently marks the checkout dirty.
91
107
  *
92
- * Exported (hcs-05) — checkoutReclaim.ts reuses this EXACT function for its
93
- * immediate-pre-delete dirty re-check, rather than reimplementing the three
94
- * conditions a second time. One dirty definition, two callers.
108
+ * uncommitted — `git status --porcelain`, minus the bridge's own writes
109
+ * (isBridgeManagedChange).
110
+ * unpushed — commits on HEAD that no remote-tracking ref contains
111
+ * (`rev-list HEAD --not --remotes`). NOT `@{u}..HEAD`: that
112
+ * failed outright with no upstream, and counted work pushed
113
+ * to any other branch (a park branch included) as unpushed —
114
+ * so park-and-remove's own pre-delete re-check aborted every
115
+ * park that had committed something.
116
+ * stashed — `git stash list` non-empty.
117
+ *
118
+ * `status`/`stash` failing means the directory isn't a usable git checkout —
119
+ * 'unparseable', not clean.
120
+ */
121
+ export declare function inspectDirty(path: string, run: InventoryRun, readText?: ReadText): Promise<DirtyReport | 'unparseable'>;
122
+ /**
123
+ * The boolean form — checkoutReclaim.ts's immediate-pre-delete re-check uses
124
+ * this EXACT definition rather than reimplementing it. One dirty definition,
125
+ * every caller.
95
126
  */
96
- export declare function checkDirty(path: string, run: InventoryRun): Promise<boolean | 'unparseable'>;
127
+ export declare function checkDirty(path: string, run: InventoryRun, readText?: ReadText): Promise<boolean | 'unparseable'>;
97
128
  /**
98
129
  * PURE full-inventory scan. One record per checkout under `reposBaseDir`; a
99
130
  * checkout that can't be read/parsed is reported as `unparseable: true`