@paigy/harness 0.2.8 → 0.2.12

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 (3) hide show
  1. package/dist/cli.js +75 -18
  2. package/dist/main.js +95 -15
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -30693,6 +30693,9 @@ function readToken(agent2 = AGENT_NAME) {
30693
30693
  function listSlots() {
30694
30694
  return Object.keys(readTokenFile());
30695
30695
  }
30696
+ function slotName(agent2) {
30697
+ return readTokenFile()[agent2]?.name ?? null;
30698
+ }
30696
30699
  async function requestCode(suggestedName = AGENT_NAME, e2ee) {
30697
30700
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
30698
30701
  method: "POST",
@@ -30859,6 +30862,16 @@ async function hatch(name, voice = null) {
30859
30862
  if (!res.ok) throw new Error(`hatch failed: ${res.status} ${await res.text()}`);
30860
30863
  return await res.json();
30861
30864
  }
30865
+ async function whoAmI(opts = {}) {
30866
+ try {
30867
+ const res = await reach(`${BACKEND_URL}/api/identity`, {
30868
+ headers: { authorization: `Bearer ${authToken(opts.token)}` }
30869
+ });
30870
+ return res.ok ? await res.json() : null;
30871
+ } catch {
30872
+ return null;
30873
+ }
30874
+ }
30862
30875
  async function claimSessions(opts = {}) {
30863
30876
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
30864
30877
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -30888,10 +30901,10 @@ async function setTaskState(notificationId, state, opts = {}) {
30888
30901
  // src/cli.ts
30889
30902
  var import_qrcode = __toESM(require_lib(), 1);
30890
30903
  import { hostname } from "os";
30891
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
30904
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
30892
30905
  import { execSync } from "child_process";
30893
- import { homedir as homedir5 } from "os";
30894
- import { join as join4 } from "path";
30906
+ import { homedir as homedir6 } from "os";
30907
+ import { join as join5 } from "path";
30895
30908
 
30896
30909
  // ../../packages/schema/dist/index.js
30897
30910
  var ContextSchema2 = external_exports.object({
@@ -32365,6 +32378,7 @@ function runHarness(opts) {
32365
32378
  send(text) {
32366
32379
  session?.send(text);
32367
32380
  },
32381
+ working: () => running && state.resting !== true,
32368
32382
  stop() {
32369
32383
  running = false;
32370
32384
  cancelAsks(state);
@@ -32375,6 +32389,11 @@ function runHarness(opts) {
32375
32389
  };
32376
32390
  }
32377
32391
 
32392
+ // src/host.ts
32393
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
32394
+ import { homedir as homedir5 } from "os";
32395
+ import { join as join4 } from "path";
32396
+
32378
32397
  // src/workspaces.ts
32379
32398
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
32380
32399
  import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
@@ -32402,10 +32421,20 @@ function addWorkspace(dir, deps) {
32402
32421
  }
32403
32422
 
32404
32423
  // src/host.ts
32424
+ var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
32405
32425
  function startHost(opts) {
32406
32426
  const runs = /* @__PURE__ */ new Map();
32407
32427
  const asHost = { token: opts.token };
32428
+ const refreshNames = async () => {
32429
+ for (const slot of listSlots()) {
32430
+ const token = readToken(slot);
32431
+ if (!token) continue;
32432
+ const me = await whoAmI({ token }).catch(() => null);
32433
+ if (me?.name && me.name !== slotName(slot)) saveToken({ access_token: token, name: me.name, device: null }, slot);
32434
+ }
32435
+ };
32408
32436
  const beat = () => {
32437
+ void refreshNames();
32409
32438
  void heartbeat({
32410
32439
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
32411
32440
  workspaces: listWorkspaces(opts.wsDeps)
@@ -32439,7 +32468,7 @@ function startHost(opts) {
32439
32468
  }
32440
32469
  }
32441
32470
  const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
32442
- const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. Call check_replies now and handle everything it returns, then follow your paigy instructions to stay in the loop.";
32471
+ const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop.";
32443
32472
  async function sweepSlots() {
32444
32473
  const workspace = listWorkspaces(opts.wsDeps)[0];
32445
32474
  if (!workspace) return;
@@ -32451,7 +32480,8 @@ function startHost(opts) {
32451
32480
  if (!token) continue;
32452
32481
  const work = await checkReplies({ token }).catch(() => null);
32453
32482
  if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
32454
- const log = (line) => opts.log(`[${slot}] ${line}`);
32483
+ const label = slotName(slot) ?? slot;
32484
+ const log = (line) => opts.log(`[${label}] ${line}`);
32455
32485
  const run = runHarness({
32456
32486
  harness,
32457
32487
  cwd: workspace,
@@ -32462,19 +32492,26 @@ function startHost(opts) {
32462
32492
  // A dead run must not squat the slot — evict so the next wake can respawn.
32463
32493
  onExit: () => {
32464
32494
  runs.delete(key);
32465
- opts.log(`\u2716 ${slot} ended`);
32495
+ opts.log(`\u2716 ${label} ended`);
32466
32496
  },
32467
32497
  log
32468
32498
  });
32469
- runs.set(key, { run, label: slot });
32470
- opts.log(`\u25B6 woke ${slot} (${harness}) \u2014 work was waiting in ${workspace}`);
32499
+ runs.set(key, { run, label });
32500
+ opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
32471
32501
  }
32472
32502
  }
32503
+ const publish = () => {
32504
+ try {
32505
+ writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
32506
+ } catch {
32507
+ }
32508
+ };
32473
32509
  beat();
32474
32510
  const pulse = setInterval(beat, 6e4);
32475
32511
  const spawnPoll = setInterval(() => {
32476
32512
  void claimAndSpawn();
32477
32513
  void sweepSlots();
32514
+ publish();
32478
32515
  }, 5e3);
32479
32516
  const stopSessions = () => {
32480
32517
  for (const { run, label } of runs.values()) {
@@ -32483,15 +32520,35 @@ function startHost(opts) {
32483
32520
  }
32484
32521
  runs.clear();
32485
32522
  };
32486
- return {
32523
+ const api = {
32487
32524
  sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
32525
+ // Every identity this machine holds, plus what it's doing right now. Slots are the
32526
+ // roster (hatched agents, the terminal tools, claimed sessions); a run makes one of
32527
+ // them live, and a run mid-turn makes it working.
32528
+ roster: () => {
32529
+ const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
32530
+ const names = /* @__PURE__ */ new Map();
32531
+ for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
32532
+ for (const label of live.keys()) if (!names.has(label)) names.set(label, label);
32533
+ return [...names.keys()].map((name) => ({
32534
+ name,
32535
+ running: live.has(name),
32536
+ working: live.get(name)?.working() ?? false
32537
+ }));
32538
+ },
32488
32539
  stopSessions,
32489
32540
  stop() {
32490
32541
  clearInterval(pulse);
32491
32542
  clearInterval(spawnPoll);
32492
32543
  stopSessions();
32544
+ try {
32545
+ writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
32546
+ } catch {
32547
+ }
32493
32548
  }
32494
32549
  };
32550
+ publish();
32551
+ return api;
32495
32552
  }
32496
32553
 
32497
32554
  // src/cli.ts
@@ -32603,7 +32660,7 @@ async function main() {
32603
32660
  const wsDeps = { file: workspacesFile() };
32604
32661
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
32605
32662
  if (!listWorkspaces(wsDeps).length) {
32606
- const guess = join4(homedir5(), "projects");
32663
+ const guess = join5(homedir6(), "projects");
32607
32664
  if (existsSync5(guess)) {
32608
32665
  addWorkspace(guess, wsDeps);
32609
32666
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -32641,12 +32698,12 @@ async function main() {
32641
32698
  }
32642
32699
  }
32643
32700
  try {
32644
- const settings = join4(homedir5(), ".claude", "settings.json");
32701
+ const settings = join5(homedir6(), ".claude", "settings.json");
32645
32702
  if (which("claude") && existsSync5(settings)) {
32646
- const cfg = JSON.parse(readFileSync3(settings, "utf8"));
32703
+ const cfg = JSON.parse(readFileSync4(settings, "utf8"));
32647
32704
  if (!cfg["statusLine"]) {
32648
32705
  cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
32649
- writeFileSync3(settings, JSON.stringify(cfg, null, 2));
32706
+ writeFileSync4(settings, JSON.stringify(cfg, null, 2));
32650
32707
  console.log("\u2713 statusline wired");
32651
32708
  }
32652
32709
  }
@@ -32696,14 +32753,14 @@ async function main() {
32696
32753
  </array>
32697
32754
  <key>RunAtLoad</key><true/>
32698
32755
  <key>KeepAlive</key><true/>
32699
- <key>StandardOutPath</key><string>${join4(homedir5(), ".paigy", "host.log")}</string>
32700
- <key>StandardErrorPath</key><string>${join4(homedir5(), ".paigy", "host.log")}</string>
32756
+ <key>StandardOutPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
32757
+ <key>StandardErrorPath</key><string>${join5(homedir6(), ".paigy", "host.log")}</string>
32701
32758
  </dict></plist>
32702
32759
  `;
32703
- const dir = join4(homedir5(), "Library", "LaunchAgents");
32760
+ const dir = join5(homedir6(), "Library", "LaunchAgents");
32704
32761
  mkdirSync3(dir, { recursive: true });
32705
- const path = join4(dir, "ai.paigy.harness.plist");
32706
- writeFileSync3(path, plist);
32762
+ const path = join5(dir, "ai.paigy.harness.plist");
32763
+ writeFileSync4(path, plist);
32707
32764
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
32708
32765
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
32709
32766
  return;
package/dist/main.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
1
2
  import {
2
3
  __commonJS,
3
4
  __export,
4
5
  __require,
5
6
  __toESM
6
- } from "./chunk-4VNS5WPM.js";
7
+ } from "./chunk-OBWUN26Y.js";
7
8
 
8
9
  // ../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/can-promise.js
9
10
  var require_can_promise = __commonJS({
@@ -4577,7 +4578,7 @@ var require_lib = __commonJS({
4577
4578
  import { app, BrowserWindow, dialog, ipcMain } from "electron";
4578
4579
  import { spawn as spawn2 } from "child_process";
4579
4580
  import { fileURLToPath } from "url";
4580
- import { dirname as dirname4, join as join4 } from "path";
4581
+ import { dirname as dirname4, join as join5 } from "path";
4581
4582
 
4582
4583
  // ../../packages/sdk/dist/index.js
4583
4584
  import { createRequire as __sdkCreateRequire } from "module";
@@ -10888,7 +10889,7 @@ var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
10888
10889
  var agent;
10889
10890
  async function proxy() {
10890
10891
  if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
10891
- return agent ??= new (await import("./undici-VGSXV7MF.js")).EnvHttpProxyAgent();
10892
+ return agent ??= new (await import("./undici-GCFUZISI.js")).EnvHttpProxyAgent();
10892
10893
  }
10893
10894
  async function reach(url, init) {
10894
10895
  try {
@@ -12105,6 +12106,9 @@ function readToken(agent2 = AGENT_NAME) {
12105
12106
  function listSlots() {
12106
12107
  return Object.keys(readTokenFile());
12107
12108
  }
12109
+ function slotName(agent2) {
12110
+ return readTokenFile()[agent2]?.name ?? null;
12111
+ }
12108
12112
  async function requestCode(suggestedName = AGENT_NAME, e2ee) {
12109
12113
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
12110
12114
  method: "POST",
@@ -12259,6 +12263,16 @@ async function checkReplies(opts = {}) {
12259
12263
  }
12260
12264
  var tokenOverride = null;
12261
12265
  var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
12266
+ async function whoAmI(opts = {}) {
12267
+ try {
12268
+ const res = await reach(`${BACKEND_URL}/api/identity`, {
12269
+ headers: { authorization: `Bearer ${authToken(opts.token)}` }
12270
+ });
12271
+ return res.ok ? await res.json() : null;
12272
+ } catch {
12273
+ return null;
12274
+ }
12275
+ }
12262
12276
  async function claimSessions(opts = {}) {
12263
12277
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
12264
12278
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -12369,6 +12383,11 @@ function detectAll(deps = {}) {
12369
12383
  return CATALOG.map((entry) => detect(entry, deps));
12370
12384
  }
12371
12385
 
12386
+ // src/host.ts
12387
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
12388
+ import { homedir as homedir5 } from "os";
12389
+ import { join as join4 } from "path";
12390
+
12372
12391
  // src/workspaces.ts
12373
12392
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
12374
12393
  import { dirname as dirname2, join as join3, resolve } from "path";
@@ -13862,6 +13881,7 @@ function runHarness(opts) {
13862
13881
  send(text) {
13863
13882
  session?.send(text);
13864
13883
  },
13884
+ working: () => running && state.resting !== true,
13865
13885
  stop() {
13866
13886
  running = false;
13867
13887
  cancelAsks(state);
@@ -13873,10 +13893,38 @@ function runHarness(opts) {
13873
13893
  }
13874
13894
 
13875
13895
  // src/host.ts
13896
+ var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
13897
+ function readHostState() {
13898
+ try {
13899
+ const s = JSON.parse(readFileSync3(HOST_FILE, "utf8"));
13900
+ if (Date.now() - s.at > 2e4) return null;
13901
+ try {
13902
+ process.kill(s.pid, 0);
13903
+ } catch {
13904
+ return null;
13905
+ }
13906
+ return s;
13907
+ } catch {
13908
+ return null;
13909
+ }
13910
+ }
13911
+ function hostElsewhere() {
13912
+ const s = readHostState();
13913
+ return !!s && s.pid !== process.pid;
13914
+ }
13876
13915
  function startHost(opts) {
13877
13916
  const runs = /* @__PURE__ */ new Map();
13878
13917
  const asHost = { token: opts.token };
13918
+ const refreshNames = async () => {
13919
+ for (const slot of listSlots()) {
13920
+ const token = readToken(slot);
13921
+ if (!token) continue;
13922
+ const me = await whoAmI({ token }).catch(() => null);
13923
+ if (me?.name && me.name !== slotName(slot)) saveToken({ access_token: token, name: me.name, device: null }, slot);
13924
+ }
13925
+ };
13879
13926
  const beat = () => {
13927
+ void refreshNames();
13880
13928
  void heartbeat({
13881
13929
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
13882
13930
  workspaces: listWorkspaces(opts.wsDeps)
@@ -13910,7 +13958,7 @@ function startHost(opts) {
13910
13958
  }
13911
13959
  }
13912
13960
  const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
13913
- const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. Call check_replies now and handle everything it returns, then follow your paigy instructions to stay in the loop.";
13961
+ const WAKE_PROMPT = "You were woken because Paigy work is waiting for you. This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. Call check_replies FIRST, then get_thread on each conversation it returns and read it before you touch anything: it holds what you were doing, where (a repo or worktree may not be this folder), and what the owner already decided. Then handle what's waiting and follow your paigy instructions to stay in the loop.";
13914
13962
  async function sweepSlots() {
13915
13963
  const workspace = listWorkspaces(opts.wsDeps)[0];
13916
13964
  if (!workspace) return;
@@ -13922,7 +13970,8 @@ function startHost(opts) {
13922
13970
  if (!token) continue;
13923
13971
  const work = await checkReplies({ token }).catch(() => null);
13924
13972
  if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
13925
- const log2 = (line) => opts.log(`[${slot}] ${line}`);
13973
+ const label = slotName(slot) ?? slot;
13974
+ const log2 = (line) => opts.log(`[${label}] ${line}`);
13926
13975
  const run = runHarness({
13927
13976
  harness,
13928
13977
  cwd: workspace,
@@ -13933,19 +13982,26 @@ function startHost(opts) {
13933
13982
  // A dead run must not squat the slot — evict so the next wake can respawn.
13934
13983
  onExit: () => {
13935
13984
  runs.delete(key);
13936
- opts.log(`\u2716 ${slot} ended`);
13985
+ opts.log(`\u2716 ${label} ended`);
13937
13986
  },
13938
13987
  log: log2
13939
13988
  });
13940
- runs.set(key, { run, label: slot });
13941
- opts.log(`\u25B6 woke ${slot} (${harness}) \u2014 work was waiting in ${workspace}`);
13989
+ runs.set(key, { run, label });
13990
+ opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
13942
13991
  }
13943
13992
  }
13993
+ const publish = () => {
13994
+ try {
13995
+ writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
13996
+ } catch {
13997
+ }
13998
+ };
13944
13999
  beat();
13945
14000
  const pulse = setInterval(beat, 6e4);
13946
14001
  const spawnPoll = setInterval(() => {
13947
14002
  void claimAndSpawn();
13948
14003
  void sweepSlots();
14004
+ publish();
13949
14005
  }, 5e3);
13950
14006
  const stopSessions = () => {
13951
14007
  for (const { run, label } of runs.values()) {
@@ -13954,15 +14010,35 @@ function startHost(opts) {
13954
14010
  }
13955
14011
  runs.clear();
13956
14012
  };
13957
- return {
14013
+ const api = {
13958
14014
  sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
14015
+ // Every identity this machine holds, plus what it's doing right now. Slots are the
14016
+ // roster (hatched agents, the terminal tools, claimed sessions); a run makes one of
14017
+ // them live, and a run mid-turn makes it working.
14018
+ roster: () => {
14019
+ const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
14020
+ const names = /* @__PURE__ */ new Map();
14021
+ for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
14022
+ for (const label of live.keys()) if (!names.has(label)) names.set(label, label);
14023
+ return [...names.keys()].map((name) => ({
14024
+ name,
14025
+ running: live.has(name),
14026
+ working: live.get(name)?.working() ?? false
14027
+ }));
14028
+ },
13959
14029
  stopSessions,
13960
14030
  stop() {
13961
14031
  clearInterval(pulse);
13962
14032
  clearInterval(spawnPoll);
13963
14033
  stopSessions();
14034
+ try {
14035
+ writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
14036
+ } catch {
14037
+ }
13964
14038
  }
13965
14039
  };
14040
+ publish();
14041
+ return api;
13966
14042
  }
13967
14043
 
13968
14044
  // src/main.ts
@@ -13974,22 +14050,23 @@ var win = null;
13974
14050
  function log(message) {
13975
14051
  win?.webContents.send("paigy:log", message);
13976
14052
  }
13977
- var ICON = join4(here, "..", "assets", "icon.png");
14053
+ var ICON = join5(here, "..", "assets", "icon.png");
13978
14054
  function createWindow() {
13979
14055
  win = new BrowserWindow({
13980
14056
  width: 720,
13981
14057
  height: 640,
13982
14058
  icon: ICON,
13983
14059
  // Windows/Linux window icon; macOS uses the dock icon below
13984
- webPreferences: { preload: join4(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
14060
+ webPreferences: { preload: join5(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
13985
14061
  });
13986
- void win.loadFile(join4(here, "..", "renderer", "index.html"));
14062
+ void win.loadFile(join5(here, "..", "renderer", "index.html"));
13987
14063
  }
13988
14064
  var host = null;
13989
14065
  app.whenReady().then(() => {
13990
14066
  if (process.platform === "darwin") app.dock?.setIcon(ICON);
13991
14067
  createWindow();
13992
- if (deviceToken()) host = startHost({ wsDeps, token: deviceToken(), log });
14068
+ if (deviceToken() && !hostElsewhere()) host = startHost({ wsDeps, token: deviceToken(), log });
14069
+ else if (hostElsewhere()) log("\u25C9 this Mac is already hosting (the background service) \u2014 showing its agents");
13993
14070
  app.on("activate", () => {
13994
14071
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
13995
14072
  });
@@ -14007,7 +14084,10 @@ ipcMain.handle("paigy:status", () => ({
14007
14084
  // (works, but the beacon upgrades it); none = show the beacon.
14008
14085
  paired: Boolean(deviceToken()),
14009
14086
  device: Boolean(readToken(DEVICE_SLOT)),
14010
- sessions: host?.sessions().map(([, label]) => label) ?? []
14087
+ sessions: host?.sessions().map(([, label]) => label) ?? [],
14088
+ // The party (companion.md / party design.md): who lives on this machine and what
14089
+ // each one is doing THIS SECOND — the one thing the laptop knows better than the phone.
14090
+ roster: host?.roster() ?? readHostState()?.roster ?? []
14011
14091
  }));
14012
14092
  var beaconLive = false;
14013
14093
  ipcMain.handle("paigy:beacon", async () => {
@@ -14022,7 +14102,7 @@ ipcMain.handle("paigy:beacon", async () => {
14022
14102
  saveToken(token, DEVICE_SLOT);
14023
14103
  beaconLive = false;
14024
14104
  host?.stop();
14025
- host = startHost({ wsDeps, token: deviceToken(), log });
14105
+ if (!hostElsewhere()) host = startHost({ wsDeps, token: deviceToken(), log });
14026
14106
  win?.webContents.send("paigy:paired", { name: token.name });
14027
14107
  log(`\u2713 paired as "${token.name}" \u2014 this machine is now launchable from your phone`);
14028
14108
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.2.8",
3
+ "version": "0.2.12",
4
4
  "description": "Run Claude Code / Codex on this machine, bridged to Paigy — launchable from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",