@wenrwa/marketplace-cli 0.6.6 → 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 (74) 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-server.test.js +1 -1
  25. package/dist/__tests__/config.test.js +1 -1
  26. package/dist/__tests__/devices.test.js +1 -1
  27. package/dist/__tests__/local.test.js +1 -1
  28. package/dist/__tests__/openBrowser.test.js +1 -1
  29. package/dist/__tests__/websocket.test.js +3 -3
  30. package/dist/auth/identity.d.ts +1 -1
  31. package/dist/auth/login.d.ts +2 -2
  32. package/dist/bridge/apiClient.js +2 -2
  33. package/dist/bridge/autostart.d.ts +1 -1
  34. package/dist/bridge/autostart.js +1 -1
  35. package/dist/bridge/checkoutInventory.d.ts +48 -17
  36. package/dist/bridge/checkoutInventory.js +190 -44
  37. package/dist/bridge/checkoutParkAndRemove.d.ts +22 -1
  38. package/dist/bridge/checkoutParkAndRemove.js +37 -7
  39. package/dist/bridge/claudeTrust.d.ts +37 -0
  40. package/dist/bridge/claudeTrust.js +85 -0
  41. package/dist/bridge/config.d.ts +1 -1
  42. package/dist/bridge/contextWriter.d.ts +2 -0
  43. package/dist/bridge/contextWriter.js +3 -2
  44. package/dist/bridge/deviceRegistration.d.ts +1 -1
  45. package/dist/bridge/envSanitize.d.ts +1 -1
  46. package/dist/bridge/envSanitize.js +1 -1
  47. package/dist/bridge/folderRegistry.js +1 -1
  48. package/dist/bridge/localAdd.js +1 -1
  49. package/dist/bridge/localContextWriter.d.ts +2 -0
  50. package/dist/bridge/localContextWriter.js +3 -2
  51. package/dist/bridge/memoryBrief.d.ts +16 -4
  52. package/dist/bridge/memoryBrief.js +17 -9
  53. package/dist/bridge/pairing.d.ts +1 -1
  54. package/dist/bridge/relayClient.js +11 -1
  55. package/dist/bridge/server.d.ts +1 -1
  56. package/dist/bridge/workGraphPointer.d.ts +19 -0
  57. package/dist/bridge/workGraphPointer.js +26 -1
  58. package/dist/commands/apps.js +1 -1
  59. package/dist/commands/auth.d.ts +1 -1
  60. package/dist/commands/auth.js +6 -6
  61. package/dist/commands/bridge.js +40 -13
  62. package/dist/commands/credits.js +1 -1
  63. package/dist/commands/devices.d.ts +1 -1
  64. package/dist/commands/devices.js +4 -4
  65. package/dist/commands/local.d.ts +1 -1
  66. package/dist/commands/local.js +5 -5
  67. package/dist/commands/pair.d.ts +1 -1
  68. package/dist/commands/pair.js +4 -4
  69. package/dist/index.js +2 -2
  70. package/dist/utils/config.js +2 -2
  71. package/dist/utils/openBrowser.d.ts +1 -1
  72. package/dist/utils/openBrowser.js +1 -1
  73. package/dist/utils/websocket.js +1 -1
  74. package/package.json +3 -2
@@ -29,23 +29,40 @@ function makeDeps(opts) {
29
29
  runCalls.push({ path: id, args });
30
30
  const fixture = opts.git?.[id] ?? {};
31
31
  const sub = args[2];
32
+ const fail = (msg) => {
33
+ if (fixture.rejectOnFailure)
34
+ throw new Error(`Command failed: git ${args.join(' ')}\n${msg}`);
35
+ return { stdout: '', code: 128 };
36
+ };
32
37
  if (sub === 'status') {
33
38
  if (fixture.failStatus)
34
- return { stdout: '', code: 128 };
39
+ return fail('fatal: not a git repository');
35
40
  return { stdout: fixture.status ?? '', code: 0 };
36
41
  }
37
42
  if (sub === 'stash') {
38
43
  if (fixture.failStash)
39
- return { stdout: '', code: 128 };
44
+ return fail('fatal: not a git repository');
40
45
  return { stdout: fixture.stash ?? '', code: 0 };
41
46
  }
42
47
  if (sub === 'rev-list') {
48
+ expect(args.slice(3)).toEqual(['HEAD', '--not', '--remotes', '--count']);
43
49
  if (fixture.ahead === undefined)
44
- return { stdout: '', code: 128 }; // no upstream
50
+ return fail("fatal: ambiguous argument 'HEAD'");
45
51
  return { stdout: fixture.ahead, code: 0 };
46
52
  }
53
+ if (sub === 'rev-parse') {
54
+ if (fixture.upstream === false)
55
+ return fail("fatal: no upstream configured for branch 'backup/x'");
56
+ return { stdout: 'origin/main\n', code: 0 };
57
+ }
58
+ if (sub === 'show') {
59
+ if (fixture.headClaudeMd === undefined)
60
+ return fail("fatal: path 'CLAUDE.md' does not exist in 'HEAD'");
61
+ return { stdout: fixture.headClaudeMd, code: 0 };
62
+ }
47
63
  throw new Error(`unexpected git call: ${args.join(' ')}`);
48
64
  },
65
+ readText: (p) => opts.files?.[p] ?? null,
49
66
  };
50
67
  return { deps, runCalls, sizeCalls };
51
68
  }
@@ -57,7 +74,10 @@ describe('scanCheckoutInventory — dirty detection (independent conditions)', (
57
74
  git: { 'ws-clean': { status: '', stash: '', ahead: '0' } },
58
75
  });
59
76
  const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
60
- expect(entry).toEqual({ workspaceId: 'ws-clean', unparseable: false, bytes: 4096, dirty: false, mtimeMs: 1000 });
77
+ expect(entry).toEqual({
78
+ workspaceId: 'ws-clean', unparseable: false, bytes: 4096, dirty: false, mtimeMs: 1000,
79
+ dirtyKinds: [], noUpstream: false, scanVersion: 2,
80
+ });
61
81
  });
62
82
  it('uncommitted changes alone mark the checkout dirty', async () => {
63
83
  const { deps } = makeDeps({
@@ -86,15 +106,107 @@ describe('scanCheckoutInventory — dirty detection (independent conditions)', (
86
106
  const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
87
107
  expect(entry).toMatchObject({ dirty: true, unparseable: false });
88
108
  });
89
- it('no upstream configured (rev-list fails) is treated as not-unpushed, not as a parse failure', async () => {
109
+ it('a repo with no commits (rev-list fails) is not unpushed, and not a parse failure', async () => {
90
110
  const { deps } = makeDeps({
91
- workspaceIds: ['ws-no-upstream'],
92
- sizes: { 'ws-no-upstream': 100 },
93
- git: { 'ws-no-upstream': { status: '', stash: '' } }, // ahead undefined -> rev-list fails
111
+ workspaceIds: ['ws-empty'],
112
+ sizes: { 'ws-empty': 100 },
113
+ git: { 'ws-empty': { status: '', stash: '' } }, // ahead undefined -> rev-list fails
94
114
  });
95
115
  const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
96
116
  expect(entry).toMatchObject({ dirty: false, unparseable: false });
97
117
  });
118
+ it('names WHICH conditions made it dirty', async () => {
119
+ const { deps } = makeDeps({
120
+ workspaceIds: ['ws'],
121
+ git: { ws: { status: ' M a.ts\n', stash: 'stash@{0}: x\n', ahead: '2' } },
122
+ files: {},
123
+ });
124
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
125
+ expect(entry).toMatchObject({ dirty: true, dirtyKinds: ['uncommitted', 'unpushed', 'stashed'] });
126
+ });
127
+ });
128
+ /*
129
+ Owner report 2026-10-02 — the Storage view said "Could not read this
130
+ checkout: Command failed: git -C … rev-list @{u}..HEAD --count fatal: no
131
+ upstream configured for branch 'backup/botsim-swee…'". The production runner
132
+ REJECTS on a failed command; the old check let that escape.
133
+ */
134
+ describe('scanCheckoutInventory — a branch with no upstream (production runner rejects)', () => {
135
+ it('is a readable checkout with unpushed work and noUpstream, never unparseable', async () => {
136
+ const { deps } = makeDeps({
137
+ workspaceIds: ['ws-backup'],
138
+ sizes: { 'ws-backup': 100 },
139
+ git: { 'ws-backup': { status: '', stash: '', ahead: '3', upstream: false, rejectOnFailure: true } },
140
+ });
141
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
142
+ expect(entry).toMatchObject({ unparseable: false, dirty: true, dirtyKinds: ['unpushed'], noUpstream: true });
143
+ });
144
+ it('with every commit already on a remote, it is clean even without an upstream', async () => {
145
+ const { deps } = makeDeps({
146
+ workspaceIds: ['ws-gone'],
147
+ git: { 'ws-gone': { status: '', stash: '', ahead: '0', upstream: false, rejectOnFailure: true } },
148
+ });
149
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
150
+ expect(entry).toMatchObject({ unparseable: false, dirty: false, noUpstream: true });
151
+ });
152
+ it('a status that rejects is still unparseable (not a git checkout)', async () => {
153
+ const { deps } = makeDeps({
154
+ workspaceIds: ['ws-notgit'],
155
+ git: { 'ws-notgit': { failStatus: true, rejectOnFailure: true } },
156
+ });
157
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
158
+ expect(entry).toMatchObject({ unparseable: true, reason: 'git status failed — not a usable checkout' });
159
+ });
160
+ });
161
+ /*
162
+ Owner report 2026-10-02 — nearly every row, finished match copies that were
163
+ pushed included, said "Unsaved work". The bridge itself rewrites a managed
164
+ region of CLAUDE.md whenever a session opens (wmr-07).
165
+ */
166
+ describe('scanCheckoutInventory — the bridge’s own writes are not unsaved work', () => {
167
+ const REGION = '<!-- HIVEOP:CONTEXT:START -->\n## Work graph\nstuff\n<!-- HIVEOP:CONTEXT:END -->\n';
168
+ it('an untracked CLAUDE.md holding only the managed region is clean', async () => {
169
+ const { deps } = makeDeps({
170
+ workspaceIds: ['ws'],
171
+ git: { ws: { status: '?? CLAUDE.md\n', stash: '', ahead: '0' } },
172
+ files: { [`${BASE}/ws/CLAUDE.md`]: REGION },
173
+ });
174
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
175
+ expect(entry).toMatchObject({ dirty: false, dirtyKinds: [] });
176
+ });
177
+ it('a committed CLAUDE.md that changed only inside the region is clean', async () => {
178
+ const { deps } = makeDeps({
179
+ workspaceIds: ['ws'],
180
+ git: { ws: { status: ' M CLAUDE.md\n', stash: '', ahead: '0', headClaudeMd: '# Project\n\nRules.\n' } },
181
+ files: { [`${BASE}/ws/CLAUDE.md`]: `# Project\n\nRules.\n\n${REGION}` },
182
+ });
183
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
184
+ expect(entry).toMatchObject({ dirty: false });
185
+ });
186
+ it('user text in CLAUDE.md outside the region IS unsaved work', async () => {
187
+ const { deps } = makeDeps({
188
+ workspaceIds: ['ws'],
189
+ git: { ws: { status: ' M CLAUDE.md\n', stash: '', ahead: '0', headClaudeMd: '# Project\n' } },
190
+ files: { [`${BASE}/ws/CLAUDE.md`]: `# Project\n\nMy new rule.\n${REGION}` },
191
+ });
192
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
193
+ expect(entry).toMatchObject({ dirty: true, dirtyKinds: ['uncommitted'] });
194
+ });
195
+ it('an untracked CLAUDE.md with user text is unsaved work', async () => {
196
+ const { deps } = makeDeps({
197
+ workspaceIds: ['ws'],
198
+ git: { ws: { status: '?? CLAUDE.md\n', stash: '', ahead: '0' } },
199
+ files: { [`${BASE}/ws/CLAUDE.md`]: `Notes\n${REGION}` },
200
+ });
201
+ const [entry] = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
202
+ expect(entry).toMatchObject({ dirty: true });
203
+ });
204
+ it('.hiveop/ is the bridge’s; any other file still counts', async () => {
205
+ const clean = makeDeps({ workspaceIds: ['ws'], git: { ws: { status: '?? .hiveop/\n', stash: '', ahead: '0' } } });
206
+ expect((await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, clean.deps))[0]).toMatchObject({ dirty: false });
207
+ const dirty = makeDeps({ workspaceIds: ['ws'], git: { ws: { status: '?? .hiveop/\n?? view/index.html\n', stash: '', ahead: '0' } } });
208
+ expect((await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, dirty.deps))[0]).toMatchObject({ dirty: true });
209
+ });
98
210
  });
99
211
  describe('scanCheckoutInventory — reporting shape', () => {
100
212
  it('one record per checkout, with workspace id, bytes, dirty flag, and mtime', async () => {
@@ -109,8 +221,8 @@ describe('scanCheckoutInventory — reporting shape', () => {
109
221
  });
110
222
  const entries = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps);
111
223
  expect(entries).toEqual([
112
- { workspaceId: 'ws-a', unparseable: false, bytes: 1000, dirty: false, mtimeMs: 111 },
113
- { workspaceId: 'ws-b', unparseable: false, bytes: 2000, dirty: true, mtimeMs: 222 },
224
+ { workspaceId: 'ws-a', unparseable: false, bytes: 1000, dirty: false, mtimeMs: 111, dirtyKinds: [], noUpstream: false, scanVersion: 2 },
225
+ { workspaceId: 'ws-b', unparseable: false, bytes: 2000, dirty: true, mtimeMs: 222, dirtyKinds: ['uncommitted'], noUpstream: false, scanVersion: 2 },
114
226
  ]);
115
227
  });
116
228
  it('an unreadable checkouts root yields an empty inventory, not a crash', async () => {
@@ -162,8 +274,8 @@ describe('scanCheckoutInventory — malformed/unreadable checkouts', () => {
162
274
  expect(entries.find((e) => e.workspaceId === 'ws-fine')?.unparseable).toBe(false);
163
275
  });
164
276
  });
165
- describe('scanCheckoutInventory — mtime cache (unchanged checkouts are not re-scanned)', () => {
166
- it('reuses the cached entry when the mtime has not changed, without calling sizeBytes/run again', async () => {
277
+ describe('scanCheckoutInventory — mtime cache (size only)', () => {
278
+ it('reuses the cached SIZE when the mtime has not changed, but re-checks dirty every scan', async () => {
167
279
  const { deps, runCalls, sizeCalls } = makeDeps({
168
280
  workspaceIds: ['ws-a'],
169
281
  mtimes: { 'ws-a': 500 },
@@ -176,8 +288,10 @@ describe('scanCheckoutInventory — mtime cache (unchanged checkouts are not re-
176
288
  expect(runCalls.length).toBeGreaterThan(0);
177
289
  const runCallsAfterFirst = runCalls.length;
178
290
  const second = await (0, checkoutInventory_1.scanCheckoutInventory)(BASE, deps, cache);
179
- expect(sizeCalls).toEqual(['ws-a']); // not called again
180
- expect(runCalls.length).toBe(runCallsAfterFirst); // no new git calls
291
+ expect(sizeCalls).toEqual(['ws-a']); // du not run again
292
+ // …but git is: a push lands inside .git and never moves the top-level
293
+ // mtime, so a cached dirty flag outlived the push that cleared it.
294
+ expect(runCalls.length).toBeGreaterThan(runCallsAfterFirst);
181
295
  expect(second).toEqual(first);
182
296
  });
183
297
  it('rescans when the mtime has changed', async () => {
@@ -266,26 +266,43 @@ describe('checkoutParkAndRemove (hcs-06)', () => {
266
266
  expect(calls.some((c) => c.args.includes('commit'))).toBe(false);
267
267
  expect(calls.some((c) => c.args.includes('push'))).toBe(false);
268
268
  });
269
- it('nothing to commit and nothing unpushed -> nothing-to-park, deletes nothing, never pushes', async () => {
269
+ it('remove with nothing to commit and nothing unpushed -> removed through the reclaimer, never pushes', async () => {
270
+ // Everything here is already on a remote. Refusing left clean match copies
271
+ // with no way out but the 14-day automatic sweep; the reclaimer's own
272
+ // re-check (clean, unattached) still guards the delete.
270
273
  const reclaimer = fakeReclaimer();
271
274
  const script = (args) => {
272
275
  if (args.includes('add'))
273
276
  return { stdout: '', code: 0 };
274
277
  if (args.includes('status'))
275
- return { stdout: '', code: 0 }; // clean working tree after add
278
+ return { stdout: '', code: 0 };
276
279
  if (args.includes('rev-list'))
277
- return { stdout: '0\n', code: 0 }; // nothing unpushed either
280
+ return { stdout: '0\n', code: 0 };
278
281
  return { stdout: '', code: 0 };
279
282
  };
280
283
  const { deps, calls } = baseDeps(script, reclaimer);
281
- const parker = (0, checkoutParkAndRemove_1.createCheckoutParker)(deps);
282
- const result = await parker.park(WS);
283
- expect(result.outcome).toBe('nothing-to-park');
284
- expect(result.branch).toBe((0, checkoutParkAndRemove_1.parkingBranchName)(WS));
285
- expect(reclaimer.calls).toEqual([]);
284
+ const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS);
285
+ expect(result).toEqual({ workspaceId: WS, outcome: 'removed', branch: (0, checkoutParkAndRemove_1.parkingBranchName)(WS), bytesFreed: 4096 });
286
+ expect(reclaimer.calls).toHaveLength(1);
286
287
  expect(calls.some((c) => c.args.includes('push'))).toBe(false);
287
288
  expect(calls.some((c) => c.args.includes('commit'))).toBe(false);
288
289
  });
290
+ it('remove with nothing unsaved, but the reclaimer refuses (a session attached) -> retained with its reason', async () => {
291
+ const reclaimer = fakeReclaimer({
292
+ reclaim: async (v) => v.map((x) => ({ workspaceId: x.workspaceId, outcome: 'aborted', reason: 'a session attached' })),
293
+ });
294
+ const script = (args) => args.includes('rev-list') ? { stdout: '0\n', code: 0 } : { stdout: '', code: 0 };
295
+ const { deps } = baseDeps(script, reclaimer);
296
+ const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS);
297
+ expect(result).toMatchObject({ outcome: 'retained', phase: 'removing', reason: 'a session attached' });
298
+ });
299
+ it('unpushed is "commits no remote ref contains", not @{u}..HEAD', async () => {
300
+ const reclaimer = fakeReclaimer();
301
+ const { deps, calls } = baseDeps(defaultScript(), reclaimer);
302
+ await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS);
303
+ const revList = calls.find((c) => c.args.includes('rev-list'));
304
+ expect(revList.args.slice(3)).toEqual(['HEAD', '--not', '--remotes', '--count']);
305
+ });
289
306
  it('nothing to commit but there ARE unpushed commits -> still pushes and removes', async () => {
290
307
  const reclaimer = fakeReclaimer();
291
308
  const script = (args) => {
@@ -383,4 +400,36 @@ describe('checkoutParkAndRemove (hcs-06)', () => {
383
400
  expect(firstResult.outcome).toBe('removed');
384
401
  expect(pushCalls).toBe(1); // never raced into a second push
385
402
  });
403
+ describe('Save (park without remove)', () => {
404
+ it('commits, pushes, verifies, records the tracking ref — and keeps the checkout', async () => {
405
+ const reclaimer = fakeReclaimer();
406
+ const sha = 'b'.repeat(40);
407
+ const { deps, calls } = baseDeps(defaultScript(sha), reclaimer);
408
+ const phases = [];
409
+ const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS, (p) => phases.push(p), { remove: false });
410
+ expect(result).toEqual({ workspaceId: WS, outcome: 'saved', branch: (0, checkoutParkAndRemove_1.parkingBranchName)(WS) });
411
+ expect(phases).toEqual(['committing', 'pushing']);
412
+ expect(reclaimer.calls).toEqual([]);
413
+ const updateRef = calls.find((c) => c.args.includes('update-ref'));
414
+ expect(updateRef.args.slice(2)).toEqual(['update-ref', `refs/remotes/origin/park/${WS}`, sha]);
415
+ });
416
+ it('an unverified push is never reported as saved', async () => {
417
+ const reclaimer = fakeReclaimer();
418
+ const base = defaultScript();
419
+ const script = (args) => (args.includes('ls-remote') ? { stdout: '', code: 0 } : base(args));
420
+ const { deps, calls } = baseDeps(script, reclaimer);
421
+ const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS, undefined, { remove: false });
422
+ expect(result.outcome).toBe('retained');
423
+ expect(calls.some((c) => c.args.includes('update-ref'))).toBe(false);
424
+ });
425
+ it('nothing unsaved -> nothing-to-park, nothing pushed, nothing removed', async () => {
426
+ const reclaimer = fakeReclaimer();
427
+ const script = (args) => args.includes('rev-list') ? { stdout: '0\n', code: 0 } : { stdout: '', code: 0 };
428
+ const { deps, calls } = baseDeps(script, reclaimer);
429
+ const result = await (0, checkoutParkAndRemove_1.createCheckoutParker)(deps).park(WS, undefined, { remove: false });
430
+ expect(result.outcome).toBe('nothing-to-park');
431
+ expect(reclaimer.calls).toEqual([]);
432
+ expect(calls.some((c) => c.args.includes('push'))).toBe(false);
433
+ });
434
+ });
386
435
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const fs = __importStar(require("fs"));
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const claudeTrust_1 = require("../bridge/claudeTrust");
40
+ /**
41
+ * Playtest 2026-10-01 (Qubit Forge, hosted lane): Claude Code 2.1.28x shows
42
+ * its folder-trust dialog even under --dangerously-skip-permissions, on every
43
+ * fresh /data/repos/<workspaceId> checkout — and its default answer is
44
+ * "No, exit". The container is ours and the bridge cloned the folder, so the
45
+ * bridge marks the checkout trusted in the container's Claude config
46
+ * ($CLAUDE_CONFIG_DIR/.claude.json → projects[<dir>].hasTrustDialogAccepted),
47
+ * the key Claude Code reads (verified against 2.1.286: the entry suppresses
48
+ * the dialog; a parent entry does NOT count inside a git repo — Claude only
49
+ * walks up to the repo root, so the entry must name the checkout itself).
50
+ *
51
+ * Real files in a temp dir: the contract is the file Claude reads.
52
+ */
53
+ describe('trustClaudeProjectDir', () => {
54
+ let root;
55
+ let configDir;
56
+ let reposBaseDir;
57
+ const configFile = () => path.join(configDir, '.claude.json');
58
+ const read = () => JSON.parse(fs.readFileSync(configFile(), 'utf-8'));
59
+ const trust = (dir) => (0, claudeTrust_1.trustClaudeProjectDir)(dir, { configDir, reposBaseDir });
60
+ beforeEach(() => {
61
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-trust-'));
62
+ configDir = path.join(root, '.claude');
63
+ reposBaseDir = path.join(root, 'repos');
64
+ fs.mkdirSync(configDir, { recursive: true });
65
+ fs.mkdirSync(path.join(reposBaseDir, 'ws-1'), { recursive: true });
66
+ });
67
+ afterEach(() => fs.rmSync(root, { recursive: true, force: true }));
68
+ it('marks a bridge-managed checkout trusted, keeping every other key', () => {
69
+ const repo = path.join(reposBaseDir, 'ws-1');
70
+ fs.writeFileSync(configFile(), JSON.stringify({
71
+ hasCompletedOnboarding: true,
72
+ bypassPermissionsModeAccepted: true,
73
+ mcpServers: { x: { command: 'y' } },
74
+ projects: { '/elsewhere': { allowedTools: ['Bash'], hasTrustDialogAccepted: false } },
75
+ }));
76
+ expect(trust(repo)).toBe('trusted');
77
+ const cfg = read();
78
+ expect(cfg.projects[repo].hasTrustDialogAccepted).toBe(true);
79
+ expect(cfg.projects['/elsewhere']).toEqual({ allowedTools: ['Bash'], hasTrustDialogAccepted: false });
80
+ expect(cfg.hasCompletedOnboarding).toBe(true);
81
+ expect(cfg.bypassPermissionsModeAccepted).toBe(true);
82
+ expect(cfg.mcpServers).toEqual({ x: { command: 'y' } });
83
+ });
84
+ it("keeps the project's existing entry (allowed tools, history) and only adds the flag", () => {
85
+ const repo = path.join(reposBaseDir, 'ws-1');
86
+ fs.writeFileSync(configFile(), JSON.stringify({ projects: { [repo]: { allowedTools: ['Edit'], lastCost: 1 } } }));
87
+ expect(trust(repo)).toBe('trusted');
88
+ expect(read().projects[repo]).toEqual({ allowedTools: ['Edit'], lastCost: 1, hasTrustDialogAccepted: true });
89
+ });
90
+ it('is idempotent: an already-trusted checkout is not rewritten', () => {
91
+ const repo = path.join(reposBaseDir, 'ws-1');
92
+ const raw = JSON.stringify({ projects: { [repo]: { hasTrustDialogAccepted: true } } });
93
+ fs.writeFileSync(configFile(), raw);
94
+ expect(trust(repo)).toBe('already');
95
+ expect(fs.readFileSync(configFile(), 'utf-8')).toBe(raw);
96
+ });
97
+ it('creates the config when Claude has not written one yet', () => {
98
+ const repo = path.join(reposBaseDir, 'ws-1');
99
+ expect(trust(repo)).toBe('trusted');
100
+ expect(read()).toEqual({ projects: { [repo]: { hasTrustDialogAccepted: true } } });
101
+ });
102
+ it('never clobbers a config it cannot parse (that file holds the login state too)', () => {
103
+ const repo = path.join(reposBaseDir, 'ws-1');
104
+ fs.writeFileSync(configFile(), '{ half-written');
105
+ expect(trust(repo)).toBe('skipped');
106
+ expect(fs.readFileSync(configFile(), 'utf-8')).toBe('{ half-written');
107
+ });
108
+ it('only trusts folders the bridge manages: never the repos root itself, never outside it', () => {
109
+ expect(trust(reposBaseDir)).toBe('skipped');
110
+ expect(trust(path.join(root, 'home', 'project'))).toBe('skipped');
111
+ expect(trust(path.join(reposBaseDir, '..', 'escape'))).toBe('skipped');
112
+ expect(trust(`${reposBaseDir}-sibling/ws`)).toBe('skipped');
113
+ expect(fs.existsSync(configFile())).toBe(false);
114
+ });
115
+ it('leaves no temp file behind', () => {
116
+ trust(path.join(reposBaseDir, 'ws-1'));
117
+ expect(fs.readdirSync(configDir)).toEqual(['.claude.json']);
118
+ });
119
+ });
120
+ /**
121
+ * Wiring: the HOSTED bridge's clone-on-demand path (resolveWorkspaceCwd in
122
+ * commands/bridge.ts) pre-trusts the checkout, gated on CLAUDE_CONFIG_DIR.
123
+ * Code only (comments stripped) so prose cannot satisfy it.
124
+ */
125
+ describe('hosted bridge wiring', () => {
126
+ const code = fs
127
+ .readFileSync(path.join(__dirname, '..', 'commands', 'bridge.ts'), 'utf-8')
128
+ .replace(/\/\*[\s\S]*?\*\//g, '')
129
+ .replace(/^\s*\/\/.*$/gm, '');
130
+ it('resolveWorkspaceCwd trusts the checkout it returns, only with CLAUDE_CONFIG_DIR set', () => {
131
+ const body = code.match(/resolveWorkspaceCwd: async[\s\S]*?return result\.path \?\? null;/)?.[0] ?? '';
132
+ expect(body).toMatch(/if \(result\.path && process\.env\.CLAUDE_CONFIG_DIR\)[\s\S]*?trustClaudeProjectDir\(result\.path, \{ configDir: process\.env\.CLAUDE_CONFIG_DIR, reposBaseDir \}\)/);
133
+ });
134
+ it('is wired exactly once — never into the local BYO bridge', () => {
135
+ expect(code.match(/trustClaudeProjectDir\(/g)).toHaveLength(1);
136
+ });
137
+ });
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /**
4
- * cbs-07 — pure resolver for the Chromium options the `wen-market bridge`
4
+ * cbs-07 — pure resolver for the Chromium options the `hiveop bridge`
5
5
  * (relay mode) command wires into `createBrowserDriver`. Mirrors
6
6
  * resolveRelayMode's existing pattern in bridge.ts: the command shell itself
7
7
  * is thin/untested, so the actual decision logic lives in a pure,
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /**
4
4
  * Autostart PATH/command fix (2026-08-13, live bug) — pure resolver for
5
- * `wen-market bridge install`. Mirrors buildBrowserDriverOptions'/
5
+ * `hiveop bridge install`. Mirrors buildBrowserDriverOptions'/
6
6
  * resolveRelayMode's existing pattern in bridge.ts: the actual decision
7
7
  * (which PATH, which command) lives in a pure, inputs-in/options-out
8
8
  * helper, unit-tested directly instead of forcing a harness onto the
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /**
4
4
  * TDD — lws-03: bridge/deviceRegistration.ts. "Connect this machine" — on
5
- * first `wen-market bridge` run with a Connect Token configured, auto-
5
+ * first `hiveop bridge` run with a Connect Token configured, auto-
6
6
  * register the device; idempotent thereafter (persisted deviceId short-
7
7
  * circuits re-registration).
8
8
  */
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  /**
37
37
  * TDD — lws-03: bridge/folderRegistry.ts. Persisted path→workspaceId map
38
38
  * (`~/.wen-market/local-workspaces.json`), survives daemon restart, backs
39
- * `wen-market local add/list/remove` and the discovery advertisement.
39
+ * `hiveop local add/list/remove` and the discovery advertisement.
40
40
  */
41
41
  const fs = __importStar(require("fs"));
42
42
  const os = __importStar(require("os"));
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /**
4
4
  * TDD — lws-05: shared idempotent-add core behind POST /local/add (the
5
- * bridge-side twin of `wen-market local add`, PRD D3). Deps-injected — no
5
+ * bridge-side twin of `hiveop local add`, PRD D3). Deps-injected — no
6
6
  * real fs/network — server.ts wires the real folderRegistry + apiClient.
7
7
  */
8
8
  const localAdd_1 = require("../bridge/localAdd");
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,136 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * Qubit Forge playtest 2026-10-02 — in a MATCH workspace an agent said
38
+ * "CLAUDE.md says work should go through the HiveOp queue" and went looking
39
+ * for issues. A match has no queue to work: missions are scored when the
40
+ * pilot presses Submit on the committed, pushed code. The bridge learns a
41
+ * workspace is a match from the memory brief reply (`match: true`) and writes
42
+ * match guidance IN PLACE of the work-queue pointer, on both lanes. Ordinary
43
+ * workspaces are byte-identical to before.
44
+ */
45
+ const fs = __importStar(require("fs"));
46
+ const os = __importStar(require("os"));
47
+ const path = __importStar(require("path"));
48
+ const contextWriter_1 = require("../bridge/contextWriter");
49
+ const localContextWriter_1 = require("../bridge/localContextWriter");
50
+ const workGraphPointer_1 = require("../bridge/workGraphPointer");
51
+ const memoryBrief_1 = require("../bridge/memoryBrief");
52
+ const fakeFs = () => {
53
+ const files = new Map();
54
+ return {
55
+ files,
56
+ deps: {
57
+ reposBaseDir: '/data/repos',
58
+ exists: (p) => files.has(p),
59
+ readFile: (p) => files.get(p) ?? '',
60
+ writeFile: (p, c) => { files.set(p, c); },
61
+ mkdirp: () => { },
62
+ },
63
+ };
64
+ };
65
+ describe('MATCH_POINTER', () => {
66
+ it('says how a match is scored and what not to do — the runtime value', () => {
67
+ expect(workGraphPointer_1.MATCH_POINTER).toMatch(/^## Match\n/);
68
+ expect(workGraphPointer_1.MATCH_POINTER).toMatch(/scored when the pilot presses Submit/);
69
+ expect(workGraphPointer_1.MATCH_POINTER).toMatch(/[Cc]ommit and push/);
70
+ expect(workGraphPointer_1.MATCH_POINTER).toMatch(/list_ready_issues/);
71
+ expect(workGraphPointer_1.MATCH_POINTER).toMatch(/do not use/i);
72
+ expect(workGraphPointer_1.MATCH_POINTER).not.toMatch(/claim_issue` before starting/);
73
+ });
74
+ });
75
+ describe('hosted lane — writeContext', () => {
76
+ it('a match workspace gets the match guidance and NOT the work-queue pointer', () => {
77
+ const { files, deps } = fakeFs();
78
+ (0, contextWriter_1.writeContext)({ workspaceId: 'ws-m', pointersOnly: true, workGraphAvailable: true, match: true }, deps);
79
+ const out = files.get('/data/repos/ws-m/CLAUDE.md') ?? '';
80
+ expect(out).toContain(workGraphPointer_1.MATCH_POINTER);
81
+ expect(out).not.toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
82
+ });
83
+ it('an ordinary workspace is unchanged', () => {
84
+ const { files, deps } = fakeFs();
85
+ (0, contextWriter_1.writeContext)({ workspaceId: 'ws-n', pointersOnly: true, workGraphAvailable: true }, deps);
86
+ const out = files.get('/data/repos/ws-n/CLAUDE.md') ?? '';
87
+ expect(out).toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
88
+ expect(out).not.toContain(workGraphPointer_1.MATCH_POINTER);
89
+ });
90
+ it('a workspace that was a match and is kept loses the match guidance on the next write', () => {
91
+ const { files, deps } = fakeFs();
92
+ (0, contextWriter_1.writeContext)({ workspaceId: 'ws-k', pointersOnly: true, workGraphAvailable: true, match: true }, deps);
93
+ (0, contextWriter_1.writeContext)({ workspaceId: 'ws-k', pointersOnly: true, workGraphAvailable: true }, deps);
94
+ const out = files.get('/data/repos/ws-k/CLAUDE.md') ?? '';
95
+ expect(out).not.toContain(workGraphPointer_1.MATCH_POINTER);
96
+ expect(out).toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
97
+ });
98
+ });
99
+ describe('local lane — writeLocalContext', () => {
100
+ it('a match workspace gets the match guidance and NOT the work-queue pointer', () => {
101
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'match-ptr-'));
102
+ try {
103
+ (0, localContextWriter_1.writeLocalContext)({ projectRoot: root, pointersOnly: true, workGraphAvailable: true, match: true }, {
104
+ exists: (p) => fs.existsSync(p),
105
+ readFile: (p) => fs.readFileSync(p, 'utf8'),
106
+ writeFileAtomic: (p, c) => fs.writeFileSync(p, c),
107
+ mkdirp: (d) => fs.mkdirSync(d, { recursive: true }),
108
+ remove: (p) => fs.rmSync(p, { force: true }),
109
+ });
110
+ const out = fs.readFileSync(path.join(root, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf8');
111
+ expect(out).toContain(workGraphPointer_1.MATCH_POINTER);
112
+ expect(out).not.toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
113
+ }
114
+ finally {
115
+ fs.rmSync(root, { recursive: true, force: true });
116
+ }
117
+ });
118
+ });
119
+ describe('fetchWorkspaceBrief — the match flag rides the brief reply', () => {
120
+ const ENV = { ...process.env };
121
+ beforeEach(() => { process.env.WENRWA_CONNECT_TOKEN = 'wm_ct_' + 'a'.repeat(32); });
122
+ afterEach(() => { process.env = { ...ENV }; });
123
+ const reply = (body) => async () => ({ ok: true, status: 200, json: async () => body });
124
+ it('match: true → match', async () => {
125
+ await expect((0, memoryBrief_1.fetchWorkspaceBrief)('ws', { fetchImpl: reply({ success: true, brief: 'B', match: true }) }))
126
+ .resolves.toEqual({ brief: 'B', match: true });
127
+ });
128
+ it('no flag → not a match; an empty brief is still a reply', async () => {
129
+ await expect((0, memoryBrief_1.fetchWorkspaceBrief)('ws', { fetchImpl: reply({ success: true, brief: '' }) }))
130
+ .resolves.toEqual({ brief: null, match: false });
131
+ });
132
+ it('any failure → null (unknown), and fetchMemoryBrief keeps its contract', async () => {
133
+ await expect((0, memoryBrief_1.fetchWorkspaceBrief)('ws', { fetchImpl: async () => { throw new Error('down'); } })).resolves.toBeNull();
134
+ await expect((0, memoryBrief_1.fetchMemoryBrief)('ws', { fetchImpl: reply({ success: true, brief: 'B', match: true }) })).resolves.toBe('B');
135
+ });
136
+ });
@@ -52,7 +52,7 @@ describe('bridge apiClient — pairing', () => {
52
52
  catch (err) {
53
53
  expect(err).toBeInstanceOf(apiClient_1.BridgeApiError);
54
54
  expect(err.code).toBe('PAT_FORBIDDEN');
55
- expect(err.message).toBe('your pairing is out of date — run `wen-market pair` again');
55
+ expect(err.message).toBe('your pairing is out of date — run `hiveop pair` again');
56
56
  expect(err.message).not.toContain('Allowed:');
57
57
  }
58
58
  });
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  * TDD — lws-14: bridge/pairing.ts pairCore. Orchestrates request → poll →
5
5
  * config write as a pure core (network + fs both injected), mirroring
6
6
  * addLocalWorkspaceCore / ensureDeviceRegistered's "pure core, thin command
7
- * shell" shape so `wen-market pair` itself stays a thin wrapper.
7
+ * shell" shape so `hiveop pair` itself stays a thin wrapper.
8
8
  */
9
9
  const pairing_1 = require("../bridge/pairing");
10
10
  describe('pairCore', () => {