@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
@@ -39,41 +39,178 @@
39
39
  * (Promise.all) per checkout, and why the whole scan is fire-and-forget from
40
40
  * relayClient's point of view.
41
41
  */
42
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
43
+ if (k2 === undefined) k2 = k;
44
+ var desc = Object.getOwnPropertyDescriptor(m, k);
45
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
46
+ desc = { enumerable: true, get: function() { return m[k]; } };
47
+ }
48
+ Object.defineProperty(o, k2, desc);
49
+ }) : (function(o, m, k, k2) {
50
+ if (k2 === undefined) k2 = k;
51
+ o[k2] = m[k];
52
+ }));
53
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
54
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
55
+ }) : function(o, v) {
56
+ o["default"] = v;
57
+ });
58
+ var __importStar = (this && this.__importStar) || (function () {
59
+ var ownKeys = function(o) {
60
+ ownKeys = Object.getOwnPropertyNames || function (o) {
61
+ var ar = [];
62
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
63
+ return ar;
64
+ };
65
+ return ownKeys(o);
66
+ };
67
+ return function (mod) {
68
+ if (mod && mod.__esModule) return mod;
69
+ var result = {};
70
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
71
+ __setModuleDefault(result, mod);
72
+ return result;
73
+ };
74
+ })();
42
75
  Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.INVENTORY_SCAN_VERSION = void 0;
77
+ exports.stripManagedRegion = stripManagedRegion;
78
+ exports.inspectDirty = inspectDirty;
43
79
  exports.checkDirty = checkDirty;
44
80
  exports.scanCheckoutInventory = scanCheckoutInventory;
45
81
  exports.createCheckoutInventoryReporter = createCheckoutInventoryReporter;
82
+ /** `cmd args...` runner — same shape used across repoCheckout.ts /
83
+ * changesReport.ts's injected `run` (git invoked as `git -C <path> ...`,
84
+ * never via a shell cwd option, so this stays a flat argv runner). */
85
+ const fs = __importStar(require("fs"));
86
+ const contextWriter_1 = require("./contextWriter");
87
+ /**
88
+ * Inventory format version. 2 (cli 0.6.9) adds `dirtyKinds`/`noUpstream`, a
89
+ * dirty check that ignores the bridge's own CLAUDE.md region, and the
90
+ * `park-save` / `inventory-scan` frames. Stamped on EVERY entry so the arena
91
+ * and backend can tell a box that understands Save from one that does not —
92
+ * an old bridge would treat an unknown frame as an error, never as a park.
93
+ */
94
+ exports.INVENTORY_SCAN_VERSION = 2;
95
+ const defaultReadText = (absPath) => {
96
+ try {
97
+ return fs.readFileSync(absPath, 'utf8');
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ };
103
+ /** `run`, with a rejection folded into a non-zero exit. The production
104
+ * runner (commands/bridge.ts execRunner) REJECTS on a failed command with no
105
+ * stdout — the old dirty check let that escape, so a branch with no upstream
106
+ * surfaced as "Could not read this checkout: Command failed: git … rev-list
107
+ * @{u}..HEAD" instead of as a checkout with unpushed work. */
108
+ async function tryRun(run, args) {
109
+ try {
110
+ return await run('git', args);
111
+ }
112
+ catch (err) {
113
+ return { stdout: err instanceof Error ? err.message : String(err), code: 1 };
114
+ }
115
+ }
116
+ /** CLAUDE.md with the bridge's managed region (contextWriter.ts) cut out. */
117
+ function stripManagedRegion(text) {
118
+ const start = text.indexOf(contextWriter_1.CONTEXT_START_MARKER);
119
+ const end = text.indexOf(contextWriter_1.CONTEXT_END_MARKER);
120
+ if (start === -1 || end === -1 || end < start)
121
+ return text.trim();
122
+ return (text.slice(0, start) + text.slice(end + contextWriter_1.CONTEXT_END_MARKER.length)).trim();
123
+ }
124
+ /**
125
+ * Is this porcelain line the bridge's OWN write, not the user's work?
126
+ *
127
+ * The bridge rewrites a fenced region of `CLAUDE.md` every time a session
128
+ * opens (wmr-07) and keeps `.hiveop/` for itself. Counting either as "unsaved
129
+ * work" marked nearly every checkout dirty — owner report 2026-10-02: finished
130
+ * Practice match copies, all pushed, all "Unsaved work". Only the managed
131
+ * region is forgiven: any user text in CLAUDE.md still counts.
132
+ */
133
+ async function isBridgeManagedChange(path, line, run, readText) {
134
+ const code = line.slice(0, 2);
135
+ const file = line.slice(3).trim().replace(/^"|"$/g, '');
136
+ if (file === '.hiveop' || file.startsWith('.hiveop/'))
137
+ return true;
138
+ if (file !== 'CLAUDE.md')
139
+ return false;
140
+ const working = readText(`${path}/CLAUDE.md`);
141
+ if (working === null)
142
+ return false;
143
+ const rest = stripManagedRegion(working);
144
+ if (code === '??')
145
+ return rest === '';
146
+ if (!/^[ M][ M]$/.test(code))
147
+ return false; // added/deleted/renamed — the user's
148
+ const head = await tryRun(run, ['-C', path, 'show', 'HEAD:CLAUDE.md']);
149
+ if (head.code !== 0)
150
+ return false;
151
+ return stripManagedRegion(head.stdout) === rest;
152
+ }
46
153
  /**
47
- * PRD "Dirty means" section: uncommitted changes (`git status --porcelain`
48
- * non-empty), unpushed commits (`git rev-list @{u}..HEAD --count` > 0), OR
49
- * stashes (`git stash list` non-empty) — any ONE independently marks the
50
- * checkout dirty. `status`/`stash` failing outright (non-zero exit) means the
51
- * directory isn't a usable git checkout at all — 'unparseable', not clean.
52
- * `rev-list @{u}..HEAD` failing is the ordinary "no upstream configured"
53
- * case (a branch with nothing to compare against) and is treated as simply
54
- * "not unpushed" rather than a parse failure — every real checkout has a
55
- * working `status`/`stash`, so those two staying reliable is what actually
56
- * distinguishes a healthy checkout from a broken one.
154
+ * PRD "Dirty means" section: uncommitted changes, unpushed commits, OR
155
+ * stashes — any ONE independently marks the checkout dirty.
156
+ *
157
+ * uncommitted — `git status --porcelain`, minus the bridge's own writes
158
+ * (isBridgeManagedChange).
159
+ * unpushed — commits on HEAD that no remote-tracking ref contains
160
+ * (`rev-list HEAD --not --remotes`). NOT `@{u}..HEAD`: that
161
+ * failed outright with no upstream, and counted work pushed
162
+ * to any other branch (a park branch included) as unpushed —
163
+ * so park-and-remove's own pre-delete re-check aborted every
164
+ * park that had committed something.
165
+ * stashed — `git stash list` non-empty.
57
166
  *
58
- * Exported (hcs-05) — checkoutReclaim.ts reuses this EXACT function for its
59
- * immediate-pre-delete dirty re-check, rather than reimplementing the three
60
- * conditions a second time. One dirty definition, two callers.
167
+ * `status`/`stash` failing means the directory isn't a usable git checkout —
168
+ * 'unparseable', not clean.
61
169
  */
62
- async function checkDirty(path, run) {
63
- const status = await run('git', ['-C', path, 'status', '--porcelain']);
170
+ async function inspectDirty(path, run, readText = defaultReadText) {
171
+ const status = await tryRun(run, ['-C', path, 'status', '--porcelain']);
64
172
  if (status.code !== 0)
65
173
  return 'unparseable';
66
- const stash = await run('git', ['-C', path, 'stash', 'list']);
174
+ const stash = await tryRun(run, ['-C', path, 'stash', 'list']);
67
175
  if (stash.code !== 0)
68
176
  return 'unparseable';
69
- const ahead = await run('git', ['-C', path, 'rev-list', '@{u}..HEAD', '--count']);
177
+ const lines = status.stdout.split('\n').filter((l) => l.trim().length > 0);
178
+ let uncommitted = false;
179
+ for (const line of lines) {
180
+ if (!(await isBridgeManagedChange(path, line, run, readText))) {
181
+ uncommitted = true;
182
+ break;
183
+ }
184
+ }
185
+ const ahead = await tryRun(run, ['-C', path, 'rev-list', 'HEAD', '--not', '--remotes', '--count']);
186
+ // A repo with no commits at all has no HEAD — nothing to push.
70
187
  const unpushed = ahead.code === 0 && parseInt(ahead.stdout.trim(), 10) > 0;
71
- const uncommitted = status.stdout.trim().length > 0;
188
+ const upstream = await tryRun(run, ['-C', path, 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']);
72
189
  const stashed = stash.stdout.trim().length > 0;
73
- return uncommitted || unpushed || stashed;
190
+ const kinds = [];
191
+ if (uncommitted)
192
+ kinds.push('uncommitted');
193
+ if (unpushed)
194
+ kinds.push('unpushed');
195
+ if (stashed)
196
+ kinds.push('stashed');
197
+ return { dirty: kinds.length > 0, kinds, noUpstream: upstream.code !== 0 };
198
+ }
199
+ /**
200
+ * The boolean form — checkoutReclaim.ts's immediate-pre-delete re-check uses
201
+ * this EXACT definition rather than reimplementing it. One dirty definition,
202
+ * every caller.
203
+ */
204
+ async function checkDirty(path, run, readText) {
205
+ const r = await inspectDirty(path, run, readText);
206
+ return r === 'unparseable' ? r : r.dirty;
74
207
  }
75
208
  async function scanOne(reposBaseDir, workspaceId, deps, cache) {
76
209
  const path = `${reposBaseDir}/${workspaceId}`;
210
+ const unparseable = (reason) => {
211
+ cache?.delete(workspaceId);
212
+ return { workspaceId, unparseable: true, reason, scanVersion: exports.INVENTORY_SCAN_VERSION };
213
+ };
77
214
  let mtimeMs;
78
215
  try {
79
216
  mtimeMs = deps.statMtimeMs(path);
@@ -81,37 +218,35 @@ async function scanOne(reposBaseDir, workspaceId, deps, cache) {
81
218
  catch {
82
219
  mtimeMs = null;
83
220
  }
84
- if (mtimeMs === null) {
85
- cache?.delete(workspaceId);
86
- return { workspaceId, unparseable: true, reason: 'checkout directory is missing or unreadable' };
87
- }
88
- // Unchanged since the last scan — reuse the cached entry rather than
89
- // re-running du/git for it (PRD: "Unchanged checkouts should not be
90
- // re-scanned — cache keyed on mtime").
221
+ if (mtimeMs === null)
222
+ return unparseable('checkout directory is missing or unreadable');
223
+ // The SIZE is reused while the directory mtime is unchanged (PRD: "cache
224
+ // keyed on mtime" — `du` is the expensive half). The dirty state is NOT:
225
+ // a commit or a push lands inside .git and never touches the top-level
226
+ // mtime, so a cached dirty flag outlived the push that cleared it.
91
227
  const cached = cache?.get(workspaceId);
92
- if (cached && cached.mtimeMs === mtimeMs) {
93
- return cached;
94
- }
95
228
  try {
96
229
  const [bytes, dirty] = await Promise.all([
97
- deps.sizeBytes(path),
98
- checkDirty(path, deps.run),
230
+ cached && cached.mtimeMs === mtimeMs ? Promise.resolve(cached.bytes) : deps.sizeBytes(path),
231
+ inspectDirty(path, deps.run, deps.readText),
99
232
  ]);
100
- if (dirty === 'unparseable') {
101
- cache?.delete(workspaceId);
102
- return { workspaceId, unparseable: true, reason: 'git status failed — not a usable checkout' };
103
- }
104
- const entry = { workspaceId, unparseable: false, bytes, dirty, mtimeMs };
233
+ if (dirty === 'unparseable')
234
+ return unparseable('git status failed — not a usable checkout');
235
+ const entry = {
236
+ workspaceId,
237
+ unparseable: false,
238
+ bytes,
239
+ dirty: dirty.dirty,
240
+ mtimeMs,
241
+ dirtyKinds: dirty.kinds,
242
+ noUpstream: dirty.noUpstream,
243
+ scanVersion: exports.INVENTORY_SCAN_VERSION,
244
+ };
105
245
  cache?.set(workspaceId, entry);
106
246
  return entry;
107
247
  }
108
248
  catch (err) {
109
- cache?.delete(workspaceId);
110
- return {
111
- workspaceId,
112
- unparseable: true,
113
- reason: err instanceof Error ? err.message : String(err),
114
- };
249
+ return unparseable(err instanceof Error ? err.message : String(err));
115
250
  }
116
251
  }
117
252
  /**
@@ -149,10 +284,17 @@ async function scanCheckoutInventory(reposBaseDir, deps, cache) {
149
284
  */
150
285
  function createCheckoutInventoryReporter(opts) {
151
286
  let inFlight = false;
287
+ // A trigger that lands mid-scan (a Save finishing, the arena asking for a
288
+ // fresh report) is remembered and run ONCE after the current pass — the
289
+ // pass in flight may have read the checkout before the change, and dropping
290
+ // the request left the Storage view showing the old state for good.
291
+ let again = false;
152
292
  const cache = new Map();
153
293
  function trigger() {
154
- if (inFlight)
155
- return; // already running — not restarted
294
+ if (inFlight) {
295
+ again = true;
296
+ return;
297
+ } // not restarted — queued once
156
298
  inFlight = true;
157
299
  scanCheckoutInventory(opts.reposBaseDir, opts.deps, cache)
158
300
  .then((checkouts) => {
@@ -163,6 +305,10 @@ function createCheckoutInventoryReporter(opts) {
163
305
  })
164
306
  .finally(() => {
165
307
  inFlight = false;
308
+ if (again) {
309
+ again = false;
310
+ trigger();
311
+ }
166
312
  });
167
313
  }
168
314
  return { trigger };
@@ -37,6 +37,19 @@
37
37
  * against the same working tree would race git itself, not merely the
38
38
  * delete — hcs-05's set alone only covers the delete step.
39
39
  *
40
+ * SAVE (cli 0.6.9, owner request "why can't I get these things to save
41
+ * without removing?"): `park(id, onPhase, { remove: false })` runs the same
42
+ * commit -> push -> verify and stops there — outcome `saved`, checkout kept.
43
+ * Remove on a checkout with nothing to park now removes it (through the
44
+ * reclaimer, which re-checks clean + unattached first): everything in it is
45
+ * already on a remote, so refusing left clean match copies with no way out
46
+ * but the 14-day automatic sweep.
47
+ *
48
+ * After a verified push the parking branch is recorded as a remote-tracking
49
+ * ref (`refs/remotes/origin/park/<id>`). The clone is `--single-branch`, so
50
+ * git does not do this itself, and without it the dirty check (commits no
51
+ * remote ref contains) could never see that the work is now saved.
52
+ *
40
53
  * PHASES: `committing` / `pushing` / `removing`, reported through the
41
54
  * optional `onPhase` callback as each begins — the wire layer
42
55
  * (relayClient.ts) turns these into `park-remove-progress` frames so a UI
@@ -55,6 +68,10 @@ export type ParkAndRemoveOutcome = {
55
68
  workspaceId: string;
56
69
  outcome: 'nothing-to-park';
57
70
  branch: string;
71
+ } | {
72
+ workspaceId: string;
73
+ outcome: 'saved';
74
+ branch: string;
58
75
  } | {
59
76
  workspaceId: string;
60
77
  outcome: 'retained';
@@ -87,13 +104,17 @@ export interface CheckoutParkAndRemoveDeps {
87
104
  * hcs-05 already proved. */
88
105
  reclaimer: CheckoutReclaimer;
89
106
  }
107
+ export interface ParkOptions {
108
+ /** false = Save: back the work up and keep the checkout. Default true. */
109
+ remove?: boolean;
110
+ }
90
111
  export interface CheckoutParker {
91
112
  /** Park (commit + push + remove-on-verified-push) one checkout.
92
113
  * `onPhase`, when given, is called once per phase as it BEGINS —
93
114
  * 'committing' first, always; 'pushing' and 'removing' only if the
94
115
  * operation reaches them. Never throws — every failure mode is a typed
95
116
  * outcome, same discipline as CheckoutReclaimer#reclaim. */
96
- park(workspaceId: string, onPhase?: (phase: ParkPhase) => void): Promise<ParkAndRemoveOutcome>;
117
+ park(workspaceId: string, onPhase?: (phase: ParkPhase) => void, opts?: ParkOptions): Promise<ParkAndRemoveOutcome>;
97
118
  }
98
119
  /**
99
120
  * Deterministic from the workspace id alone (no timestamp) — PURE, so a
@@ -41,6 +41,19 @@ exports.createCheckoutParker = createCheckoutParker;
41
41
  * against the same working tree would race git itself, not merely the
42
42
  * delete — hcs-05's set alone only covers the delete step.
43
43
  *
44
+ * SAVE (cli 0.6.9, owner request "why can't I get these things to save
45
+ * without removing?"): `park(id, onPhase, { remove: false })` runs the same
46
+ * commit -> push -> verify and stops there — outcome `saved`, checkout kept.
47
+ * Remove on a checkout with nothing to park now removes it (through the
48
+ * reclaimer, which re-checks clean + unattached first): everything in it is
49
+ * already on a remote, so refusing left clean match copies with no way out
50
+ * but the 14-day automatic sweep.
51
+ *
52
+ * After a verified push the parking branch is recorded as a remote-tracking
53
+ * ref (`refs/remotes/origin/park/<id>`). The clone is `--single-branch`, so
54
+ * git does not do this itself, and without it the dirty check (commits no
55
+ * remote ref contains) could never see that the work is now saved.
56
+ *
44
57
  * PHASES: `committing` / `pushing` / `removing`, reported through the
45
58
  * optional `onPhase` callback as each begins — the wire layer
46
59
  * (relayClient.ts) turns these into `park-remove-progress` frames so a UI
@@ -81,7 +94,8 @@ function createCheckoutParker(deps) {
81
94
  // must cover the WHOLE operation (commit+push+remove) and not merely the
82
95
  // delete step, which is a SEPARATE lock (hcs-05's reclaimer, reused below).
83
96
  const inFlight = new Set();
84
- async function park(workspaceId, onPhase) {
97
+ async function park(workspaceId, onPhase, opts = {}) {
98
+ const remove = opts.remove !== false;
85
99
  // Path safety — resolved from workspaceId ALONE, reusing hcs-05's exact
86
100
  // resolver rather than a second implementation. There is no path on the
87
101
  // wire to trust in the first place.
@@ -132,14 +146,25 @@ function createCheckoutParker(deps) {
132
146
  // A failure here (no upstream configured, detached HEAD, ...) is NOT
133
147
  // a hard failure — it just means "assume there might be something to
134
148
  // push" and let the push step itself be the source of truth.
135
- const ahead = await runGit(deps.run, target, ['rev-list', '@{u}..HEAD', '--count']);
149
+ // Commits no remote-tracking ref contains — the same definition the
150
+ // dirty check uses (checkoutInventory.ts inspectDirty).
151
+ const ahead = await runGit(deps.run, target, ['rev-list', 'HEAD', '--not', '--remotes', '--count']);
136
152
  const hasUnpushed = ahead.code === 0 ? parseInt(ahead.stdout.trim(), 10) > 0 : true;
137
153
  if (!committedSomething && !hasUnpushed) {
138
- // Genuinely nothing to park. Per the issue's acceptance criterion,
139
- // this reports "nothing to park" rather than guessing at a delete —
140
- // the invariant is "delete only after a PROVEN push", and there is
141
- // no push to prove here, so the checkout is retained.
142
- return { workspaceId, outcome: 'nothing-to-park', branch };
154
+ // Genuinely nothing to park: everything here is already on a remote.
155
+ // Save reports that. Remove goes straight to the reclaimer, whose
156
+ // own immediate re-check (clean, unattached) still guards the delete.
157
+ if (!remove)
158
+ return { workspaceId, outcome: 'nothing-to-park', branch };
159
+ phase = 'removing';
160
+ onPhase?.(phase);
161
+ const [cleanResult] = await deps.reclaimer.reclaim([
162
+ { workspaceId, verdict: 'RECLAIM', reason: 'park-and-remove: nothing unsaved' },
163
+ ]);
164
+ if (cleanResult.outcome === 'removed') {
165
+ return { workspaceId, outcome: 'removed', branch, bytesFreed: cleanResult.bytesFreed };
166
+ }
167
+ return { workspaceId, outcome: 'retained', branch, phase, reason: cleanResult.reason };
143
168
  }
144
169
  // ---- PUSHING --------------------------------------------------------
145
170
  phase = 'pushing';
@@ -167,6 +192,11 @@ function createCheckoutParker(deps) {
167
192
  if (remoteSha !== localSha) {
168
193
  return { workspaceId, outcome: 'retained', branch, phase, reason: `push reported success but the remote ref points at a different commit (${remoteSha.slice(0, 8)} != ${localSha.slice(0, 8)}) — refusing to trust it` };
169
194
  }
195
+ // Record what the remote just proved, so the dirty check sees the work
196
+ // as saved. Best-effort: the push is already verified either way.
197
+ await runGit(deps.run, target, ['update-ref', `refs/remotes/origin/${branch}`, localSha]);
198
+ if (!remove)
199
+ return { workspaceId, outcome: 'saved', branch };
170
200
  // ---- REMOVING -------------------------------------------------------
171
201
  // The work is now PROVABLY on the remote. Only past this point may
172
202
  // anything be deleted, and only via hcs-05's own executor — see the
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Playtest 2026-10-01 (Qubit Forge, hosted lane) — pre-trust the workspace
3
+ * checkouts this bridge itself clones, so Claude Code never opens on its
4
+ * folder-trust dialog in them.
5
+ *
6
+ * Claude Code (2.1.28x) shows "Quick safety check: Is this a project you
7
+ * created or one you trust? … ❯ No, exit / Yes, I trust this folder" on any
8
+ * folder its config has not trusted — EVEN under --dangerously-skip-
9
+ * permissions — and the default is "No, exit". Every match / workspace gets a
10
+ * fresh /data/repos/<workspaceId>, so every first agent there met it, and a
11
+ * queued first message (Send to agent) answered it with the default: Claude
12
+ * exited to a bare shell.
13
+ *
14
+ * The container is ours, the bridge cloned the folder from the workspace's own
15
+ * repo, and the agent was launched in bypass mode by the pilot's own spawn
16
+ * choice — the trust question has nobody meaningful to ask. So the bridge
17
+ * records the answer Claude would: `projects[<dir>].hasTrustDialogAccepted =
18
+ * true` in `$CLAUDE_CONFIG_DIR/.claude.json` (where Claude keeps its global
19
+ * config when CLAUDE_CONFIG_DIR is set — verified against 2.1.286).
20
+ *
21
+ * The entry must name the checkout itself: inside a git repo Claude only walks
22
+ * up to the repo ROOT looking for a trusted entry, so trusting /data/repos
23
+ * would not count.
24
+ *
25
+ * Scope: HOSTED LANE ONLY (commands/bridge.ts wires it into the container's
26
+ * clone-on-demand path, and only when CLAUDE_CONFIG_DIR is set), and only for
27
+ * folders strictly inside the bridge's repos dir. A local-lane user's own
28
+ * Claude config is never touched.
29
+ *
30
+ * Never clobbers: an unparseable config (it also carries onboarding/login
31
+ * state) is left alone; every other key is preserved; the write is tmp+rename.
32
+ */
33
+ export type ClaudeTrustResult = 'trusted' | 'already' | 'skipped';
34
+ export declare function trustClaudeProjectDir(dir: string, opts: {
35
+ configDir: string;
36
+ reposBaseDir: string;
37
+ }): ClaudeTrustResult;
@@ -0,0 +1,85 @@
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
+ exports.trustClaudeProjectDir = trustClaudeProjectDir;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ function trustClaudeProjectDir(dir, opts) {
40
+ const base = path.resolve(opts.reposBaseDir);
41
+ const target = path.resolve(dir);
42
+ if (!target.startsWith(base + path.sep))
43
+ return 'skipped';
44
+ const file = path.join(opts.configDir, '.claude.json');
45
+ let cfg = {};
46
+ let raw = null;
47
+ try {
48
+ raw = fs.readFileSync(file, 'utf-8');
49
+ }
50
+ catch {
51
+ raw = null; // no config yet — Claude merges its defaults over what we write
52
+ }
53
+ if (raw !== null) {
54
+ try {
55
+ const parsed = JSON.parse(raw);
56
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
57
+ return 'skipped';
58
+ cfg = parsed;
59
+ }
60
+ catch {
61
+ return 'skipped';
62
+ }
63
+ }
64
+ const projects = cfg.projects && typeof cfg.projects === 'object' && !Array.isArray(cfg.projects)
65
+ ? cfg.projects
66
+ : {};
67
+ const entry = projects[target] && typeof projects[target] === 'object' ? projects[target] : {};
68
+ if (entry.hasTrustDialogAccepted === true)
69
+ return 'already';
70
+ const next = { ...cfg, projects: { ...projects, [target]: { ...entry, hasTrustDialogAccepted: true } } };
71
+ fs.mkdirSync(opts.configDir, { recursive: true });
72
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
73
+ try {
74
+ fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
75
+ fs.renameSync(tmp, file);
76
+ }
77
+ catch (err) {
78
+ try {
79
+ fs.rmSync(tmp, { force: true });
80
+ }
81
+ catch { /* best effort */ }
82
+ throw err;
83
+ }
84
+ return 'trusted';
85
+ }
@@ -1,7 +1,7 @@
1
1
  export interface BridgeConfig {
2
2
  connectToken: string;
3
3
  /** lws-03: set once this machine auto-registers as a device (first
4
- * `wen-market bridge` run) — persisted so re-registration is idempotent. */
4
+ * `hiveop bridge` run) — persisted so re-registration is idempotent. */
5
5
  deviceId?: string;
6
6
  }
7
7
  export interface DiscoveryInfo {
@@ -32,6 +32,8 @@ export declare function writeContext(input: {
32
32
  /** Absent only for a pointers-only refresh (wmr-07). */
33
33
  content?: string;
34
34
  workGraphAvailable?: boolean;
35
+ /** 2026-10-02 — a match workspace: MATCH_POINTER replaces the work-queue pointer (workSection). */
36
+ match?: boolean;
35
37
  /**
36
38
  * wmr-01 — whether to carry the workspace-memory guidance. Decided by the
37
39
  * caller on the same signal as workGraphAvailable (WENRWA_CONNECT_TOKEN),
@@ -148,8 +148,9 @@ function writeContext(input, deps) {
148
148
  return existing.slice(s0 + exports.CONTEXT_START_MARKER.length, e0).trim();
149
149
  })();
150
150
  const pointers = [];
151
- if (input.workGraphAvailable)
152
- pointers.push(workGraphPointer_1.WORK_GRAPH_POINTER);
151
+ const work = (0, workGraphPointer_1.workSection)(input);
152
+ if (work)
153
+ pointers.push(work);
153
154
  if (input.memoryAvailable)
154
155
  pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
155
156
  // wmr-07: a pointers-only refresh keeps the region's existing file content
@@ -1,5 +1,5 @@
1
1
  /**
2
- * lws-03 — "Connect this machine": on first `wen-market bridge` run with a
2
+ * lws-03 — "Connect this machine": on first `hiveop bridge` run with a
3
3
  * Connect Token configured, auto-register the device and persist its id
4
4
  * into bridge-config.json so re-registration is skipped thereafter (a
5
5
  * pure resolver, mirroring resolveRelayMode in commands/bridge.ts, so the
@@ -20,7 +20,7 @@ export declare const STRIPPED_ENV_PREFIXES: readonly ["CLAUDE_CODE_"];
20
20
  * fcct-27 — the RELAY CREDENTIALS are stripped here too, for a different and
21
21
  * sharper reason than the session markers above.
22
22
  *
23
- * `wen-market bridge` reads `CREW_RELAY_TOKEN` / `RELAY_TOKEN` from its own
23
+ * `hiveop bridge` reads `CREW_RELAY_TOKEN` / `RELAY_TOKEN` from its own
24
24
  * environment (the launch agent sets them), and every PTY it spawns inherits
25
25
  * that environment. Seen live 2026-08-16: a crew member's `claude` process had
26
26
  * the device-bound connect token sitting in plain view in `ps eww`. Nothing
@@ -24,7 +24,7 @@ exports.STRIPPED_ENV_PREFIXES = ['CLAUDE_CODE_'];
24
24
  * fcct-27 — the RELAY CREDENTIALS are stripped here too, for a different and
25
25
  * sharper reason than the session markers above.
26
26
  *
27
- * `wen-market bridge` reads `CREW_RELAY_TOKEN` / `RELAY_TOKEN` from its own
27
+ * `hiveop bridge` reads `CREW_RELAY_TOKEN` / `RELAY_TOKEN` from its own
28
28
  * environment (the launch agent sets them), and every PTY it spawns inherits
29
29
  * that environment. Seen live 2026-08-16: a crew member's `claude` process had
30
30
  * the device-bound connect token sitting in plain view in `ps eww`. Nothing
@@ -41,7 +41,7 @@ exports.removeEntryByWorkspaceId = removeEntryByWorkspaceId;
41
41
  exports.findEntryByPath = findEntryByPath;
42
42
  exports.findEntryByWorkspaceId = findEntryByWorkspaceId;
43
43
  /**
44
- * lws-03 — folder→workspace registry. Backs `wen-market local add/list/
44
+ * lws-03 — folder→workspace registry. Backs `hiveop local add/list/
45
45
  * remove` and the discovery advertisement (server.ts). Lives alongside the
46
46
  * bridge's other dotfiles under ~/.wen-market/ rather than a second dotdir,
47
47
  * matching config.ts's convention. File-backed so it survives daemon
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.addLocalWorkspaceCore = addLocalWorkspaceCore;
37
37
  /**
38
38
  * lws-05 — bridge-side idempotent add core, behind POST /local/add. Mirrors
39
- * `wen-market local add`'s behavior (commands/local.ts) so the arena
39
+ * `hiveop local add`'s behavior (commands/local.ts) so the arena
40
40
  * create-modal's Browse/typed-path attach mechanisms (PRD D3b/D3c) end at the
41
41
  * exact same registered-workspace outcome as the CLI mechanism (D3a) —
42
42
  * deps-injected so server.ts can wire it without this module knowing about
@@ -29,6 +29,8 @@ export declare function writeLocalContext(input: {
29
29
  /** Absent only for a pointers-only refresh (wmr-07). */
30
30
  content?: string;
31
31
  workGraphAvailable?: boolean;
32
+ /** 2026-10-02 — a match workspace: MATCH_POINTER replaces the work-queue pointer (workSection). */
33
+ match?: boolean;
32
34
  /** wmr-04 — two-lane parity with contextWriter.ts. Gated by the caller on
33
35
  * the local lane's own honest signal (did THIS bridge run register the
34
36
  * MCP server), not on a container env var that does not exist here. */
@@ -89,8 +89,9 @@ function writeLocalContext(input, deps) {
89
89
  // this would be a two-lane bug nobody would notice until a local pilot
90
90
  // reported missing memory.
91
91
  const pointers = [];
92
- if (input.workGraphAvailable)
93
- pointers.push(workGraphPointer_1.WORK_GRAPH_POINTER);
92
+ const work = (0, workGraphPointer_1.workSection)(input);
93
+ if (work)
94
+ pointers.push(work);
94
95
  if (input.memoryAvailable)
95
96
  pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
96
97
  // wmr-07 — the whole local file is bridge-owned, so "preserve the pushed