@paigy/harness 0.2.5 → 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 +108 -22
  2. package/dist/main.js +128 -19
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -29763,7 +29763,12 @@ var IntentSchema = external_exports.object({
29763
29763
  * "after lunch") against the agent machine's clock — the user's — and attaches
29764
29764
  * the seconds, ready to pass straight to schedule_callback. Absent when the
29765
29765
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
29766
- dueInSeconds: external_exports.number().int().positive().optional()
29766
+ dueInSeconds: external_exports.number().int().positive().optional(),
29767
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
29768
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
29769
+ * 'other' on every row. A table that records that something was wrong and nothing
29770
+ * about what cannot answer "is the bot looping less this week?". */
29771
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
29767
29772
  });
29768
29773
  var AwaitItemSchema = external_exports.discriminatedUnion("type", [
29769
29774
  external_exports.object({
@@ -29933,6 +29938,11 @@ var AgendaTurnSchema = external_exports.object({
29933
29938
  claimId: external_exports.string().optional(),
29934
29939
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
29935
29940
  voice: external_exports.string().optional(),
29941
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
29942
+ * whose request this is: an item that folded in from another agent arrived as a bare
29943
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
29944
+ * and the owner answered "What?". The bot names the agent before its first turn. */
29945
+ agent: external_exports.string().optional(),
29936
29946
  select: SelectShapeSchema.optional(),
29937
29947
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
29938
29948
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -30683,6 +30693,9 @@ function readToken(agent2 = AGENT_NAME) {
30683
30693
  function listSlots() {
30684
30694
  return Object.keys(readTokenFile());
30685
30695
  }
30696
+ function slotName(agent2) {
30697
+ return readTokenFile()[agent2]?.name ?? null;
30698
+ }
30686
30699
  async function requestCode(suggestedName = AGENT_NAME, e2ee) {
30687
30700
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
30688
30701
  method: "POST",
@@ -30849,6 +30862,16 @@ async function hatch(name, voice = null) {
30849
30862
  if (!res.ok) throw new Error(`hatch failed: ${res.status} ${await res.text()}`);
30850
30863
  return await res.json();
30851
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
+ }
30852
30875
  async function claimSessions(opts = {}) {
30853
30876
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
30854
30877
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -30878,10 +30901,10 @@ async function setTaskState(notificationId, state, opts = {}) {
30878
30901
  // src/cli.ts
30879
30902
  var import_qrcode = __toESM(require_lib(), 1);
30880
30903
  import { hostname } from "os";
30881
- 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";
30882
30905
  import { execSync } from "child_process";
30883
- import { homedir as homedir5 } from "os";
30884
- import { join as join4 } from "path";
30906
+ import { homedir as homedir6 } from "os";
30907
+ import { join as join5 } from "path";
30885
30908
 
30886
30909
  // ../../packages/schema/dist/index.js
30887
30910
  var ContextSchema2 = external_exports.object({
@@ -31117,7 +31140,12 @@ var IntentSchema2 = external_exports.object({
31117
31140
  * "after lunch") against the agent machine's clock — the user's — and attaches
31118
31141
  * the seconds, ready to pass straight to schedule_callback. Absent when the
31119
31142
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
31120
- dueInSeconds: external_exports.number().int().positive().optional()
31143
+ dueInSeconds: external_exports.number().int().positive().optional(),
31144
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
31145
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
31146
+ * 'other' on every row. A table that records that something was wrong and nothing
31147
+ * about what cannot answer "is the bot looping less this week?". */
31148
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
31121
31149
  });
31122
31150
  var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
31123
31151
  external_exports.object({
@@ -31287,6 +31315,11 @@ var AgendaTurnSchema2 = external_exports.object({
31287
31315
  claimId: external_exports.string().optional(),
31288
31316
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
31289
31317
  voice: external_exports.string().optional(),
31318
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
31319
+ * whose request this is: an item that folded in from another agent arrived as a bare
31320
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
31321
+ * and the owner answered "What?". The bot names the agent before its first turn. */
31322
+ agent: external_exports.string().optional(),
31290
31323
  select: SelectShapeSchema2.optional(),
31291
31324
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
31292
31325
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -31755,7 +31788,8 @@ async function drainInput(state, deps = {}) {
31755
31788
  return [];
31756
31789
  }
31757
31790
  const ours = (parentId) => state.exclusive || parentId === state.parentId;
31758
- const claimable = (notificationId) => !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
31791
+ const opening = !!state.exclusive && !state.parentId;
31792
+ const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
31759
31793
  const mine = [
31760
31794
  ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
31761
31795
  ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
@@ -31788,7 +31822,10 @@ async function pump(session, state, deps = {}) {
31788
31822
  continue;
31789
31823
  }
31790
31824
  const words = spokenText(item.answer);
31791
- if (words) session.send(words);
31825
+ if (words) {
31826
+ state.resting = false;
31827
+ session.send(words);
31828
+ }
31792
31829
  }
31793
31830
  const keepGoing = await deps.onIdle?.() ?? false;
31794
31831
  if (!keepGoing) return;
@@ -32304,6 +32341,7 @@ function runHarness(opts) {
32304
32341
  state.parentId ??= parentId;
32305
32342
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
32306
32343
  if (event.kind === "idle") {
32344
+ state.resting = true;
32307
32345
  if (endsWithQuestion(event.result)) {
32308
32346
  const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
32309
32347
  if (local?.trim() && session && running) {
@@ -32340,6 +32378,7 @@ function runHarness(opts) {
32340
32378
  send(text) {
32341
32379
  session?.send(text);
32342
32380
  },
32381
+ working: () => running && state.resting !== true,
32343
32382
  stop() {
32344
32383
  running = false;
32345
32384
  cancelAsks(state);
@@ -32350,6 +32389,11 @@ function runHarness(opts) {
32350
32389
  };
32351
32390
  }
32352
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
+
32353
32397
  // src/workspaces.ts
32354
32398
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
32355
32399
  import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
@@ -32377,10 +32421,20 @@ function addWorkspace(dir, deps) {
32377
32421
  }
32378
32422
 
32379
32423
  // src/host.ts
32424
+ var HOST_FILE = join4(homedir5(), ".paigy", "host.json");
32380
32425
  function startHost(opts) {
32381
32426
  const runs = /* @__PURE__ */ new Map();
32382
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
+ };
32383
32436
  const beat = () => {
32437
+ void refreshNames();
32384
32438
  void heartbeat({
32385
32439
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
32386
32440
  workspaces: listWorkspaces(opts.wsDeps)
@@ -32406,11 +32460,15 @@ function startHost(opts) {
32406
32460
  log
32407
32461
  });
32408
32462
  runs.set(spec.sessionId, { run, label });
32463
+ try {
32464
+ saveToken({ access_token: spec.token, name: label, device: null }, `session:${spec.sessionId.slice(0, 8)}`);
32465
+ } catch {
32466
+ }
32409
32467
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
32410
32468
  }
32411
32469
  }
32412
32470
  const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
32413
- 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.";
32414
32472
  async function sweepSlots() {
32415
32473
  const workspace = listWorkspaces(opts.wsDeps)[0];
32416
32474
  if (!workspace) return;
@@ -32422,7 +32480,8 @@ function startHost(opts) {
32422
32480
  if (!token) continue;
32423
32481
  const work = await checkReplies({ token }).catch(() => null);
32424
32482
  if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
32425
- const log = (line) => opts.log(`[${slot}] ${line}`);
32483
+ const label = slotName(slot) ?? slot;
32484
+ const log = (line) => opts.log(`[${label}] ${line}`);
32426
32485
  const run = runHarness({
32427
32486
  harness,
32428
32487
  cwd: workspace,
@@ -32433,19 +32492,26 @@ function startHost(opts) {
32433
32492
  // A dead run must not squat the slot — evict so the next wake can respawn.
32434
32493
  onExit: () => {
32435
32494
  runs.delete(key);
32436
- opts.log(`\u2716 ${slot} ended`);
32495
+ opts.log(`\u2716 ${label} ended`);
32437
32496
  },
32438
32497
  log
32439
32498
  });
32440
- runs.set(key, { run, label: slot });
32441
- 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}`);
32442
32501
  }
32443
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
+ };
32444
32509
  beat();
32445
32510
  const pulse = setInterval(beat, 6e4);
32446
32511
  const spawnPoll = setInterval(() => {
32447
32512
  void claimAndSpawn();
32448
32513
  void sweepSlots();
32514
+ publish();
32449
32515
  }, 5e3);
32450
32516
  const stopSessions = () => {
32451
32517
  for (const { run, label } of runs.values()) {
@@ -32454,15 +32520,35 @@ function startHost(opts) {
32454
32520
  }
32455
32521
  runs.clear();
32456
32522
  };
32457
- return {
32523
+ const api = {
32458
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
+ },
32459
32539
  stopSessions,
32460
32540
  stop() {
32461
32541
  clearInterval(pulse);
32462
32542
  clearInterval(spawnPoll);
32463
32543
  stopSessions();
32544
+ try {
32545
+ writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
32546
+ } catch {
32547
+ }
32464
32548
  }
32465
32549
  };
32550
+ publish();
32551
+ return api;
32466
32552
  }
32467
32553
 
32468
32554
  // src/cli.ts
@@ -32574,7 +32660,7 @@ async function main() {
32574
32660
  const wsDeps = { file: workspacesFile() };
32575
32661
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
32576
32662
  if (!listWorkspaces(wsDeps).length) {
32577
- const guess = join4(homedir5(), "projects");
32663
+ const guess = join5(homedir6(), "projects");
32578
32664
  if (existsSync5(guess)) {
32579
32665
  addWorkspace(guess, wsDeps);
32580
32666
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -32612,12 +32698,12 @@ async function main() {
32612
32698
  }
32613
32699
  }
32614
32700
  try {
32615
- const settings = join4(homedir5(), ".claude", "settings.json");
32701
+ const settings = join5(homedir6(), ".claude", "settings.json");
32616
32702
  if (which("claude") && existsSync5(settings)) {
32617
- const cfg = JSON.parse(readFileSync3(settings, "utf8"));
32703
+ const cfg = JSON.parse(readFileSync4(settings, "utf8"));
32618
32704
  if (!cfg["statusLine"]) {
32619
32705
  cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
32620
- writeFileSync3(settings, JSON.stringify(cfg, null, 2));
32706
+ writeFileSync4(settings, JSON.stringify(cfg, null, 2));
32621
32707
  console.log("\u2713 statusline wired");
32622
32708
  }
32623
32709
  }
@@ -32667,14 +32753,14 @@ async function main() {
32667
32753
  </array>
32668
32754
  <key>RunAtLoad</key><true/>
32669
32755
  <key>KeepAlive</key><true/>
32670
- <key>StandardOutPath</key><string>${join4(homedir5(), ".paigy", "host.log")}</string>
32671
- <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>
32672
32758
  </dict></plist>
32673
32759
  `;
32674
- const dir = join4(homedir5(), "Library", "LaunchAgents");
32760
+ const dir = join5(homedir6(), "Library", "LaunchAgents");
32675
32761
  mkdirSync3(dir, { recursive: true });
32676
- const path = join4(dir, "ai.paigy.harness.plist");
32677
- writeFileSync3(path, plist);
32762
+ const path = join5(dir, "ai.paigy.harness.plist");
32763
+ writeFileSync4(path, plist);
32678
32764
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
32679
32765
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
32680
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 {
@@ -11175,7 +11176,12 @@ var IntentSchema = external_exports.object({
11175
11176
  * "after lunch") against the agent machine's clock — the user's — and attaches
11176
11177
  * the seconds, ready to pass straight to schedule_callback. Absent when the
11177
11178
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
11178
- dueInSeconds: external_exports.number().int().positive().optional()
11179
+ dueInSeconds: external_exports.number().int().positive().optional(),
11180
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
11181
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
11182
+ * 'other' on every row. A table that records that something was wrong and nothing
11183
+ * about what cannot answer "is the bot looping less this week?". */
11184
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
11179
11185
  });
11180
11186
  var AwaitItemSchema = external_exports.discriminatedUnion("type", [
11181
11187
  external_exports.object({
@@ -11345,6 +11351,11 @@ var AgendaTurnSchema = external_exports.object({
11345
11351
  claimId: external_exports.string().optional(),
11346
11352
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
11347
11353
  voice: external_exports.string().optional(),
11354
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
11355
+ * whose request this is: an item that folded in from another agent arrived as a bare
11356
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
11357
+ * and the owner answered "What?". The bot names the agent before its first turn. */
11358
+ agent: external_exports.string().optional(),
11348
11359
  select: SelectShapeSchema.optional(),
11349
11360
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
11350
11361
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -12095,6 +12106,9 @@ function readToken(agent2 = AGENT_NAME) {
12095
12106
  function listSlots() {
12096
12107
  return Object.keys(readTokenFile());
12097
12108
  }
12109
+ function slotName(agent2) {
12110
+ return readTokenFile()[agent2]?.name ?? null;
12111
+ }
12098
12112
  async function requestCode(suggestedName = AGENT_NAME, e2ee) {
12099
12113
  const res = await reach(`${BACKEND_URL}/api/device/code`, {
12100
12114
  method: "POST",
@@ -12249,6 +12263,16 @@ async function checkReplies(opts = {}) {
12249
12263
  }
12250
12264
  var tokenOverride = null;
12251
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
+ }
12252
12276
  async function claimSessions(opts = {}) {
12253
12277
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
12254
12278
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -12359,6 +12383,11 @@ function detectAll(deps = {}) {
12359
12383
  return CATALOG.map((entry) => detect(entry, deps));
12360
12384
  }
12361
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
+
12362
12391
  // src/workspaces.ts
12363
12392
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
12364
12393
  import { dirname as dirname2, join as join3, resolve } from "path";
@@ -12909,7 +12938,12 @@ var IntentSchema2 = external_exports.object({
12909
12938
  * "after lunch") against the agent machine's clock — the user's — and attaches
12910
12939
  * the seconds, ready to pass straight to schedule_callback. Absent when the
12911
12940
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
12912
- dueInSeconds: external_exports.number().int().positive().optional()
12941
+ dueInSeconds: external_exports.number().int().positive().optional(),
12942
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
12943
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
12944
+ * 'other' on every row. A table that records that something was wrong and nothing
12945
+ * about what cannot answer "is the bot looping less this week?". */
12946
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
12913
12947
  });
12914
12948
  var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
12915
12949
  external_exports.object({
@@ -13079,6 +13113,11 @@ var AgendaTurnSchema2 = external_exports.object({
13079
13113
  claimId: external_exports.string().optional(),
13080
13114
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
13081
13115
  voice: external_exports.string().optional(),
13116
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
13117
+ * whose request this is: an item that folded in from another agent arrived as a bare
13118
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
13119
+ * and the owner answered "What?". The bot names the agent before its first turn. */
13120
+ agent: external_exports.string().optional(),
13082
13121
  select: SelectShapeSchema2.optional(),
13083
13122
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
13084
13123
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -13547,7 +13586,8 @@ async function drainInput(state, deps = {}) {
13547
13586
  return [];
13548
13587
  }
13549
13588
  const ours = (parentId) => state.exclusive || parentId === state.parentId;
13550
- const claimable = (notificationId) => !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
13589
+ const opening = !!state.exclusive && !state.parentId;
13590
+ const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
13551
13591
  const mine = [
13552
13592
  ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
13553
13593
  ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
@@ -13580,7 +13620,10 @@ async function pump(session, state, deps = {}) {
13580
13620
  continue;
13581
13621
  }
13582
13622
  const words = spokenText(item.answer);
13583
- if (words) session.send(words);
13623
+ if (words) {
13624
+ state.resting = false;
13625
+ session.send(words);
13626
+ }
13584
13627
  }
13585
13628
  const keepGoing = await deps.onIdle?.() ?? false;
13586
13629
  if (!keepGoing) return;
@@ -13801,6 +13844,7 @@ function runHarness(opts) {
13801
13844
  state.parentId ??= parentId;
13802
13845
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
13803
13846
  if (event.kind === "idle") {
13847
+ state.resting = true;
13804
13848
  if (endsWithQuestion(event.result)) {
13805
13849
  const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
13806
13850
  if (local?.trim() && session && running) {
@@ -13837,6 +13881,7 @@ function runHarness(opts) {
13837
13881
  send(text) {
13838
13882
  session?.send(text);
13839
13883
  },
13884
+ working: () => running && state.resting !== true,
13840
13885
  stop() {
13841
13886
  running = false;
13842
13887
  cancelAsks(state);
@@ -13848,10 +13893,38 @@ function runHarness(opts) {
13848
13893
  }
13849
13894
 
13850
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
+ }
13851
13915
  function startHost(opts) {
13852
13916
  const runs = /* @__PURE__ */ new Map();
13853
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
+ };
13854
13926
  const beat = () => {
13927
+ void refreshNames();
13855
13928
  void heartbeat({
13856
13929
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
13857
13930
  workspaces: listWorkspaces(opts.wsDeps)
@@ -13877,11 +13950,15 @@ function startHost(opts) {
13877
13950
  log: log2
13878
13951
  });
13879
13952
  runs.set(spec.sessionId, { run, label });
13953
+ try {
13954
+ saveToken({ access_token: spec.token, name: label, device: null }, `session:${spec.sessionId.slice(0, 8)}`);
13955
+ } catch {
13956
+ }
13880
13957
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
13881
13958
  }
13882
13959
  }
13883
13960
  const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex" };
13884
- 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.";
13885
13962
  async function sweepSlots() {
13886
13963
  const workspace = listWorkspaces(opts.wsDeps)[0];
13887
13964
  if (!workspace) return;
@@ -13893,7 +13970,8 @@ function startHost(opts) {
13893
13970
  if (!token) continue;
13894
13971
  const work = await checkReplies({ token }).catch(() => null);
13895
13972
  if (!work || work.requests.length === 0 && work.replies.length === 0) continue;
13896
- const log2 = (line) => opts.log(`[${slot}] ${line}`);
13973
+ const label = slotName(slot) ?? slot;
13974
+ const log2 = (line) => opts.log(`[${label}] ${line}`);
13897
13975
  const run = runHarness({
13898
13976
  harness,
13899
13977
  cwd: workspace,
@@ -13904,19 +13982,26 @@ function startHost(opts) {
13904
13982
  // A dead run must not squat the slot — evict so the next wake can respawn.
13905
13983
  onExit: () => {
13906
13984
  runs.delete(key);
13907
- opts.log(`\u2716 ${slot} ended`);
13985
+ opts.log(`\u2716 ${label} ended`);
13908
13986
  },
13909
13987
  log: log2
13910
13988
  });
13911
- runs.set(key, { run, label: slot });
13912
- 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}`);
13913
13991
  }
13914
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
+ };
13915
13999
  beat();
13916
14000
  const pulse = setInterval(beat, 6e4);
13917
14001
  const spawnPoll = setInterval(() => {
13918
14002
  void claimAndSpawn();
13919
14003
  void sweepSlots();
14004
+ publish();
13920
14005
  }, 5e3);
13921
14006
  const stopSessions = () => {
13922
14007
  for (const { run, label } of runs.values()) {
@@ -13925,15 +14010,35 @@ function startHost(opts) {
13925
14010
  }
13926
14011
  runs.clear();
13927
14012
  };
13928
- return {
14013
+ const api = {
13929
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
+ },
13930
14029
  stopSessions,
13931
14030
  stop() {
13932
14031
  clearInterval(pulse);
13933
14032
  clearInterval(spawnPoll);
13934
14033
  stopSessions();
14034
+ try {
14035
+ writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
14036
+ } catch {
14037
+ }
13935
14038
  }
13936
14039
  };
14040
+ publish();
14041
+ return api;
13937
14042
  }
13938
14043
 
13939
14044
  // src/main.ts
@@ -13945,22 +14050,23 @@ var win = null;
13945
14050
  function log(message) {
13946
14051
  win?.webContents.send("paigy:log", message);
13947
14052
  }
13948
- var ICON = join4(here, "..", "assets", "icon.png");
14053
+ var ICON = join5(here, "..", "assets", "icon.png");
13949
14054
  function createWindow() {
13950
14055
  win = new BrowserWindow({
13951
14056
  width: 720,
13952
14057
  height: 640,
13953
14058
  icon: ICON,
13954
14059
  // Windows/Linux window icon; macOS uses the dock icon below
13955
- webPreferences: { preload: join4(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
14060
+ webPreferences: { preload: join5(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
13956
14061
  });
13957
- void win.loadFile(join4(here, "..", "renderer", "index.html"));
14062
+ void win.loadFile(join5(here, "..", "renderer", "index.html"));
13958
14063
  }
13959
14064
  var host = null;
13960
14065
  app.whenReady().then(() => {
13961
14066
  if (process.platform === "darwin") app.dock?.setIcon(ICON);
13962
14067
  createWindow();
13963
- 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");
13964
14070
  app.on("activate", () => {
13965
14071
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
13966
14072
  });
@@ -13978,7 +14084,10 @@ ipcMain.handle("paigy:status", () => ({
13978
14084
  // (works, but the beacon upgrades it); none = show the beacon.
13979
14085
  paired: Boolean(deviceToken()),
13980
14086
  device: Boolean(readToken(DEVICE_SLOT)),
13981
- 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 ?? []
13982
14091
  }));
13983
14092
  var beaconLive = false;
13984
14093
  ipcMain.handle("paigy:beacon", async () => {
@@ -13993,7 +14102,7 @@ ipcMain.handle("paigy:beacon", async () => {
13993
14102
  saveToken(token, DEVICE_SLOT);
13994
14103
  beaconLive = false;
13995
14104
  host?.stop();
13996
- host = startHost({ wsDeps, token: deviceToken(), log });
14105
+ if (!hostElsewhere()) host = startHost({ wsDeps, token: deviceToken(), log });
13997
14106
  win?.webContents.send("paigy:paired", { name: token.name });
13998
14107
  log(`\u2713 paired as "${token.name}" \u2014 this machine is now launchable from your phone`);
13999
14108
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.2.5",
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",