@paigy/harness 0.3.5 → 0.3.7

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 +1148 -1570
  2. package/dist/main.js +957 -1352
  3. package/package.json +12 -13
package/dist/main.js CHANGED
@@ -3,8 +3,9 @@ import {
3
3
  __commonJS,
4
4
  __export,
5
5
  __require,
6
- __toESM
7
- } from "./chunk-OBWUN26Y.js";
6
+ __toESM,
7
+ require_undici
8
+ } from "./chunk-LS24S3RM.js";
8
9
 
9
10
  // ../../node_modules/.pnpm/qrcode@1.5.4/node_modules/qrcode/lib/can-promise.js
10
11
  var require_can_promise = __commonJS({
@@ -1305,11 +1306,11 @@ var require_dijkstra = __commonJS({
1305
1306
  var predecessors = {};
1306
1307
  var costs = {};
1307
1308
  costs[s] = 0;
1308
- var open2 = dijkstra.PriorityQueue.make();
1309
- open2.push(s, 0);
1309
+ var open = dijkstra.PriorityQueue.make();
1310
+ open.push(s, 0);
1310
1311
  var closest, u, v, cost_of_s_to_u, adjacent_nodes, cost_of_e, cost_of_s_to_u_plus_cost_of_e, cost_of_s_to_v, first_visit;
1311
- while (!open2.empty()) {
1312
- closest = open2.pop();
1312
+ while (!open.empty()) {
1313
+ closest = open.pop();
1313
1314
  u = closest.value;
1314
1315
  cost_of_s_to_u = closest.cost;
1315
1316
  adjacent_nodes = graph[u] || {};
@@ -1321,7 +1322,7 @@ var require_dijkstra = __commonJS({
1321
1322
  first_visit = typeof costs[v] === "undefined";
1322
1323
  if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
1323
1324
  costs[v] = cost_of_s_to_u_plus_cost_of_e;
1324
- open2.push(v, cost_of_s_to_u_plus_cost_of_e);
1325
+ open.push(v, cost_of_s_to_u_plus_cost_of_e);
1325
1326
  predecessors[v] = u;
1326
1327
  }
1327
1328
  }
@@ -4579,11 +4580,13 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron";
4579
4580
  import { spawn as spawn2 } from "child_process";
4580
4581
  import { readFileSync as readFileSync4 } from "fs";
4581
4582
  import { fileURLToPath } from "url";
4582
- import { dirname as dirname4, join as join5 } from "path";
4583
+ import { dirname as dirname3, join as join5 } from "path";
4583
4584
 
4584
4585
  // ../../packages/sdk/dist/index.js
4585
4586
  import { createRequire as __sdkCreateRequire } from "module";
4586
4587
  import { createHash, randomUUID } from "crypto";
4588
+ import { randomUUID as randomUUID3 } from "crypto";
4589
+ import { setTimeout as sleep2 } from "timers/promises";
4587
4590
 
4588
4591
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
4589
4592
  var external_exports = {};
@@ -6149,12 +6152,12 @@ ZodString.create = (params) => {
6149
6152
  ...processCreateParams(params)
6150
6153
  });
6151
6154
  };
6152
- function floatSafeRemainder(val, step) {
6155
+ function floatSafeRemainder(val, step2) {
6153
6156
  const valDecCount = (val.toString().split(".")[1] || "").length;
6154
- const stepDecCount = (step.toString().split(".")[1] || "").length;
6157
+ const stepDecCount = (step2.toString().split(".")[1] || "").length;
6155
6158
  const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount;
6156
6159
  const valInt = Number.parseInt(val.toFixed(decCount).replace(".", ""));
6157
- const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", ""));
6160
+ const stepInt = Number.parseInt(step2.toFixed(decCount).replace(".", ""));
6158
6161
  return valInt % stepInt / 10 ** decCount;
6159
6162
  }
6160
6163
  var ZodNumber = class _ZodNumber extends ZodType {
@@ -8631,6 +8634,7 @@ import { randomUUID as randomUUID2 } from "crypto";
8631
8634
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
8632
8635
  import { homedir } from "os";
8633
8636
  import { join } from "path";
8637
+ var import_undici = __toESM(require_undici(), 1);
8634
8638
  var require2 = __sdkCreateRequire(import.meta.url);
8635
8639
  var __create = Object.create;
8636
8640
  var __defProp = Object.defineProperty;
@@ -8961,17 +8965,17 @@ var require_nacl_fast = __commonJS2({
8961
8965
  }
8962
8966
  var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]);
8963
8967
  function crypto_stream_salsa20_xor(c, cpos, m, mpos, b, n, k) {
8964
- var z3 = new Uint8Array(16), x = new Uint8Array(64);
8968
+ var z4 = new Uint8Array(16), x = new Uint8Array(64);
8965
8969
  var u, i;
8966
- for (i = 0; i < 16; i++) z3[i] = 0;
8967
- for (i = 0; i < 8; i++) z3[i] = n[i];
8970
+ for (i = 0; i < 16; i++) z4[i] = 0;
8971
+ for (i = 0; i < 8; i++) z4[i] = n[i];
8968
8972
  while (b >= 64) {
8969
- crypto_core_salsa20(x, z3, k, sigma);
8973
+ crypto_core_salsa20(x, z4, k, sigma);
8970
8974
  for (i = 0; i < 64; i++) c[cpos + i] = m[mpos + i] ^ x[i];
8971
8975
  u = 1;
8972
8976
  for (i = 8; i < 16; i++) {
8973
- u = u + (z3[i] & 255) | 0;
8974
- z3[i] = u & 255;
8977
+ u = u + (z4[i] & 255) | 0;
8978
+ z4[i] = u & 255;
8975
8979
  u >>>= 8;
8976
8980
  }
8977
8981
  b -= 64;
@@ -8979,30 +8983,30 @@ var require_nacl_fast = __commonJS2({
8979
8983
  mpos += 64;
8980
8984
  }
8981
8985
  if (b > 0) {
8982
- crypto_core_salsa20(x, z3, k, sigma);
8986
+ crypto_core_salsa20(x, z4, k, sigma);
8983
8987
  for (i = 0; i < b; i++) c[cpos + i] = m[mpos + i] ^ x[i];
8984
8988
  }
8985
8989
  return 0;
8986
8990
  }
8987
8991
  function crypto_stream_salsa20(c, cpos, b, n, k) {
8988
- var z3 = new Uint8Array(16), x = new Uint8Array(64);
8992
+ var z4 = new Uint8Array(16), x = new Uint8Array(64);
8989
8993
  var u, i;
8990
- for (i = 0; i < 16; i++) z3[i] = 0;
8991
- for (i = 0; i < 8; i++) z3[i] = n[i];
8994
+ for (i = 0; i < 16; i++) z4[i] = 0;
8995
+ for (i = 0; i < 8; i++) z4[i] = n[i];
8992
8996
  while (b >= 64) {
8993
- crypto_core_salsa20(x, z3, k, sigma);
8997
+ crypto_core_salsa20(x, z4, k, sigma);
8994
8998
  for (i = 0; i < 64; i++) c[cpos + i] = x[i];
8995
8999
  u = 1;
8996
9000
  for (i = 8; i < 16; i++) {
8997
- u = u + (z3[i] & 255) | 0;
8998
- z3[i] = u & 255;
9001
+ u = u + (z4[i] & 255) | 0;
9002
+ z4[i] = u & 255;
8999
9003
  u >>>= 8;
9000
9004
  }
9001
9005
  b -= 64;
9002
9006
  cpos += 64;
9003
9007
  }
9004
9008
  if (b > 0) {
9005
- crypto_core_salsa20(x, z3, k, sigma);
9009
+ crypto_core_salsa20(x, z4, k, sigma);
9006
9010
  for (i = 0; i < b; i++) c[cpos + i] = x[i];
9007
9011
  }
9008
9012
  return 0;
@@ -9882,12 +9886,12 @@ var require_nacl_fast = __commonJS2({
9882
9886
  for (a = 0; a < 16; a++) o[a] = c[a];
9883
9887
  }
9884
9888
  function crypto_scalarmult(q, n, p) {
9885
- var z3 = new Uint8Array(32);
9889
+ var z4 = new Uint8Array(32);
9886
9890
  var x = new Float64Array(80), r, i;
9887
9891
  var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf();
9888
- for (i = 0; i < 31; i++) z3[i] = n[i];
9889
- z3[31] = n[31] & 127 | 64;
9890
- z3[0] &= 248;
9892
+ for (i = 0; i < 31; i++) z4[i] = n[i];
9893
+ z4[31] = n[31] & 127 | 64;
9894
+ z4[0] &= 248;
9891
9895
  unpack25519(x, p);
9892
9896
  for (i = 0; i < 16; i++) {
9893
9897
  b[i] = x[i];
@@ -9895,7 +9899,7 @@ var require_nacl_fast = __commonJS2({
9895
9899
  }
9896
9900
  a[0] = d[0] = 1;
9897
9901
  for (i = 254; i >= 0; --i) {
9898
- r = z3[i >>> 3] >>> (i & 7) & 1;
9902
+ r = z4[i >>> 3] >>> (i & 7) & 1;
9899
9903
  sel25519(a, b, r);
9900
9904
  sel25519(c, d, r);
9901
9905
  A(e, a, c);
@@ -10892,7 +10896,7 @@ var PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
10892
10896
  var agent;
10893
10897
  async function proxy() {
10894
10898
  if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
10895
- return agent ??= new (await import("./undici-GCFUZISI.js")).EnvHttpProxyAgent();
10899
+ return agent ??= new (await import("./undici-FEMMVKAA.js")).EnvHttpProxyAgent();
10896
10900
  }
10897
10901
  var INSTANCE_ID = randomUUID();
10898
10902
  var WORKSPACE_ID = (() => {
@@ -12134,20 +12138,17 @@ var zodToJsonSchema = (schema, options) => {
12134
12138
  };
12135
12139
  var OPTIONS_MIN = 2;
12136
12140
  var OPTIONS_MAX = 6;
12137
- var MISSED_CALL_PLAN = {
12138
- retry_10m: { kind: "every", minutes: 10 },
12139
- retry_30m: { kind: "every", minutes: 30 },
12140
- retry_60m: { kind: "every", minutes: 60 },
12141
- backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
12142
- backoff_standard: { kind: "at", minutes: [10, 30, 120] },
12143
- backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
12144
- inbox: { kind: "once" },
12145
- dismiss: { kind: "grace", minutes: 2 }
12146
- };
12147
12141
  function draft2020(node) {
12148
12142
  if (Array.isArray(node)) return node.map(draft2020);
12149
12143
  if (node && typeof node === "object") {
12150
12144
  const o = node;
12145
+ if (Array.isArray(o.items)) {
12146
+ o.prefixItems = o.items;
12147
+ if ("additionalItems" in o) {
12148
+ o.items = o.additionalItems;
12149
+ delete o.additionalItems;
12150
+ } else delete o.items;
12151
+ }
12151
12152
  for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
12152
12153
  if (typeof o[excl] === "boolean") {
12153
12154
  if (o[excl] === true && typeof o[lim] === "number") {
@@ -12166,11 +12167,54 @@ function mcpInputSchema(s) {
12166
12167
  delete schema.$schema;
12167
12168
  return draft2020(schema);
12168
12169
  }
12170
+ var StartContactSchema = external_exports.object({
12171
+ goalIds: external_exports.tuple([external_exports.string().uuid()]),
12172
+ // THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
12173
+ //
12174
+ // A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
12175
+ // findings and a decision into one, and the person answers whichever part engaged them —
12176
+ // which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
12177
+ // one ask carried a briefing, a side question ("the ring should show the agent's name now —
12178
+ // what did you actually see?") and a one-of-four about how far an erase reaches. The owner
12179
+ // answered the side question. The need stayed open; the card asked again. Of the 45 call
12180
+ // asks carrying a decision, 27 are over 600 characters.
12181
+ //
12182
+ // Sending them separately costs nothing, because a CALL contact joins the call already
12183
+ // happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
12184
+ // one call, planned as its turns, and each keeps its own settleable need.
12185
+ ask: external_exports.string().trim().min(1).max(1e4).describe(
12186
+ "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
12187
+ ),
12188
+ waiting: external_exports.enum(["none", "hard"]).default("none"),
12189
+ channel: external_exports.enum(["notification", "call"]).default("notification"),
12190
+ options: external_exports.array(external_exports.object({ label: external_exports.string().trim().min(1).max(1e3), image: external_exports.string().url().optional(), html: external_exports.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
12191
+ threadId: external_exports.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
12192
+ }).strict();
12193
+ var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
12194
+ var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
12195
+ var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
12169
12196
  var CreateGoalSchema = external_exports.object({
12170
12197
  outcome: external_exports.string().trim().min(1).max(1e4),
12171
12198
  ownerParticipant: external_exports.string().trim().min(1).optional(),
12172
- idempotencyKey: external_exports.string().trim().min(1).max(200)
12199
+ idempotencyKey: external_exports.string().trim().min(1).max(200),
12200
+ /** A past conversation this Goal should be read against — History's "new session from this"
12201
+ * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
12202
+ * reference only: the owner reads it through `get_thread`, which does its own scoping, and
12203
+ * the writer refuses a thread belonging to another account. */
12204
+ contextThreadId: external_exports.string().uuid().optional(),
12205
+ /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
12206
+ * didn't get created as a child goal of the voice UI goal, which is how it should've
12207
+ * worked"). It could not have been: this door took no parent, so the only route was
12208
+ * create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
12209
+ * took the short one. The hierarchy has been modelled since Goals existed and had been used
12210
+ * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
12211
+ * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
12212
+ parentGoalId: external_exports.string().uuid().optional()
12173
12213
  });
12214
+ var CreateGoalToolSchema = CreateGoalSchema.extend({
12215
+ idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
12216
+ }).strict();
12217
+ var CREATE_GOAL_DESCRIPTION = "Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
12174
12218
  var UpdateGoalSchema = external_exports.object({
12175
12219
  revision: external_exports.number().int().positive(),
12176
12220
  changes: external_exports.object({
@@ -12186,52 +12230,33 @@ var UpdateGoalSchema = external_exports.object({
12186
12230
  reason: external_exports.string().trim().min(1).max(2e3),
12187
12231
  operationId: external_exports.string().uuid().optional()
12188
12232
  }).strict();
12189
- var UpdateGoalToolSchema = UpdateGoalSchema.extend({ goalId: external_exports.string().uuid() }).strict();
12233
+ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
12190
12234
  var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
12191
- var fmtMin = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
12192
- var STANDARD_MEANS = (() => {
12193
- const plan = MISSED_CALL_PLAN.backoff_standard;
12194
- const mins = plan.kind === "at" ? plan.minutes : [];
12195
- const parts = mins.map(fmtMin);
12196
- const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
12197
- return `Rings again ${list} after the missed call, then leaves it in your inbox`;
12198
- })();
12199
- function contactSchemaFrom(fields) {
12200
- const surface = external_exports.object({
12201
- ask: fields.ask.describe(
12202
- `What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs). Contact at exactly two moments: BLOCKED on a decision only they can make, or DONE (one short report \u2014 what shipped, how you verified it, what you flagged). DONE IS SAID ONCE: "all set", "nothing open on my end", "that thread is complete" are the same report in new words, and each one reaches them separately (live 2026-08-12: three of them in three minutes). After the first, you are finished speaking; if they acknowledge it, stop rather than confirming the acknowledgement. Progress is never a contact: set_work_state carries it, and working narration stays in your own terminal \u2014 the user sees you're working without being interrupted by it.`
12203
- ),
12204
- waiting: fields.waiting.describe(
12205
- "What happens to your work while you wait. 'none': you're just informing them. 'soft': you'd like an answer but can keep working. 'hard': you are STOPPED until they answer \u2014 reaches them urgently and escalates to a real phone call if unanswered."
12206
- ),
12207
- options: fields.options.describe(
12208
- `The choices the user picks from, when you have them \u2014 ${OPTIONS_MIN} to ${OPTIONS_MAX}, drawn from your own sentence.`
12209
- ),
12210
- channel: fields.channel.describe(
12211
- "Relay how the user explicitly said to reach them ('call me' \u2192 'call', 'just message/text me' \u2192 'message'), or use 'call' when promoting the same quiet ask after it becomes a substantial blocker. Omit otherwise; Paigy picks."
12212
- ),
12213
- parentId: fields.parentId.describe(
12214
- "To continue an earlier conversation, pass the parentId a previous contact or reply returned. Omit to start a new one."
12215
- ),
12216
- workId: fields.workId.describe(
12217
- "The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
12218
- ),
12219
- goalId: fields.goalId.describe(
12220
- "The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
12221
- ),
12222
- // THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
12223
- // A contact that rang holds its first window itself; the host caps one tool call at
12224
- // ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
12225
- wait: external_exports.string().uuid().optional().describe(
12226
- "KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
12227
- )
12228
- });
12229
- const out = mcpInputSchema(surface);
12230
- delete out.required;
12231
- out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
12232
- return out;
12233
- }
12234
- var CONTACT_DESCRIPTION = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN}-${OPTIONS_MAX} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
12235
+ var GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
12236
+ var GET_GOAL_DESCRIPTION = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
12237
+ var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
12238
+ var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
12239
+ var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
12240
+ var CheckRepliesSchema = external_exports.object({}).strict();
12241
+ var GetThreadSchema = external_exports.object({
12242
+ parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
12243
+ }).strict();
12244
+ var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
12245
+ var SearchThreadsSchema = external_exports.object({
12246
+ q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
12247
+ }).strict();
12248
+ var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
12249
+ var AGENT_TOOLS = [
12250
+ { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
12251
+ { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
12252
+ { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
12253
+ { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
12254
+ { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
12255
+ { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
12256
+ { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
12257
+ { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
12258
+ ];
12259
+ var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
12235
12260
  var ContextSchema = external_exports.object({
12236
12261
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
12237
12262
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -12517,16 +12542,6 @@ function deriveAsk(req) {
12517
12542
  }
12518
12543
  var NotifyStatusSchema = external_exports.enum(["pending", "answered", "ignored"]);
12519
12544
  var AgentStateSchema = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
12520
- var SetTaskStateSchema = external_exports.object({
12521
- state: external_exports.enum(["in_progress", "completed", "needs_input"])
12522
- });
12523
- var SetWorkStateSchema = external_exports.object({
12524
- workId: external_exports.string().uuid().optional(),
12525
- notificationId: external_exports.string().optional(),
12526
- state: SetTaskStateSchema.shape.state
12527
- }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
12528
- message: "exactly one of workId or notificationId is required"
12529
- });
12530
12545
  var TurnSchema = external_exports.object({
12531
12546
  prompt: external_exports.string(),
12532
12547
  reply: external_exports.string()
@@ -12656,108 +12671,6 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
12656
12671
  })
12657
12672
  ]);
12658
12673
  var CallbackTriggerSchema = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
12659
- var ScheduleCallbackSchema = external_exports.object({
12660
- parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
12661
- goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
12662
- trigger: CallbackTriggerSchema,
12663
- dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
12664
- note: external_exports.string().optional().describe("What to tell the user when you follow up.")
12665
- });
12666
- var PendingRepliesSchema = external_exports.object({
12667
- replies: external_exports.array(
12668
- external_exports.object({
12669
- parentId: external_exports.string(),
12670
- notificationId: external_exports.string(),
12671
- workId: external_exports.string().uuid().optional(),
12672
- decisionId: external_exports.string().uuid().optional(),
12673
- answer: UserAnswerSchema,
12674
- /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
12675
- * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
12676
- * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
12677
- sealed: external_exports.lazy(() => SealedAnswerSchema).optional(),
12678
- /** The call record rendered for THIS agent: the raw words the shaped answer was
12679
- * mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
12680
- transcript: external_exports.string().optional(),
12681
- /** Coverage report (#396): which declared `points` this answer addressed. */
12682
- covered: external_exports.array(external_exports.string()).optional()
12683
- })
12684
- ),
12685
- pending: external_exports.array(
12686
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
12687
- ),
12688
- /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
12689
- * sees for this session's identity. From inside a session there was no way to find out —
12690
- * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
12691
- * agent that cannot tell which agent it is cannot tell whether work addressed to
12692
- * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
12693
- you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
12694
- /** User-initiated requests addressed to this agent; act on them and reply via
12695
- * contact on the same parentId. Keeps reappearing until you call
12696
- * set_work_state on its workId (or notificationId once while bootstrapping). */
12697
- requests: external_exports.array(
12698
- external_exports.object({
12699
- parentId: external_exports.string(),
12700
- notificationId: external_exports.string(),
12701
- workId: external_exports.string().uuid().optional(),
12702
- text: external_exports.string(),
12703
- createdAt: external_exports.string(),
12704
- /** The user seeded this request with a past conversation — call get_thread on it
12705
- * FIRST and treat the transcript as prior context (#57/#251). */
12706
- contextParentId: external_exports.string().optional(),
12707
- /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
12708
- * the account — the name here — which has not been seen since it landed, so nobody
12709
- * came for it. Handed to you because you are the session that is here. Take it like
12710
- * any request (set_work_state claims it, reply with contact on its parentId), and say
12711
- * whose it was, because the user chose that agent on purpose. */
12712
- stranded: external_exports.string().optional()
12713
- })
12714
- ),
12715
- /** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
12716
- * notification lists remain during migration so old clients keep their existing view. */
12717
- work: external_exports.array(external_exports.object({
12718
- workId: external_exports.string().uuid(),
12719
- parentId: external_exports.string().optional(),
12720
- objective: external_exports.string().nullable(),
12721
- state: external_exports.enum(["active", "blocked", "waiting_external"]),
12722
- blockedOn: external_exports.array(external_exports.string().uuid()),
12723
- updatedAt: external_exports.string()
12724
- })).optional(),
12725
- /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
12726
- * if blocked, or at a time that has passed). Re-surfaced every sweep until you
12727
- * fulfill one by calling contact on its parentId. */
12728
- /** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
12729
- * reach. Same array the contact/await replies carry — one queue, every carrier. */
12730
- also: external_exports.array(RideAlongSchema).optional(),
12731
- owedCallbacks: external_exports.array(
12732
- external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema, note: external_exports.string() })
12733
- ),
12734
- /** Work (either direction) you reported in_progress a while ago and never reported
12735
- * completed — likely left half-done by this session or a prior one that crashed or
12736
- * went idle. Report a real state (set_work_state) or continue the work. */
12737
- stalled: external_exports.array(
12738
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), title: external_exports.string().nullable(), startedAt: external_exports.string() })
12739
- ),
12740
- /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
12741
- * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
12742
- * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
12743
- * thread already has a turn in progress (younger than the stall cutoff); let it finish and
12744
- * ride the next turn. `items` are that thread's replies/requests in arrival order; the
12745
- * full payload for each is in the flat `replies`/`requests` arrays (matched by
12746
- * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
12747
- threads: external_exports.array(
12748
- external_exports.object({
12749
- parentId: external_exports.string(),
12750
- busy: external_exports.boolean(),
12751
- items: external_exports.array(
12752
- external_exports.object({
12753
- kind: external_exports.enum(["reply", "request"]),
12754
- notificationId: external_exports.string(),
12755
- at: external_exports.string()
12756
- })
12757
- )
12758
- })
12759
- )
12760
- });
12761
12674
  var NotifyResponseSchema = external_exports.object({
12762
12675
  notificationId: external_exports.string(),
12763
12676
  workId: external_exports.string().uuid().optional(),
@@ -12820,6 +12733,13 @@ var UserResponseSchema = external_exports.object({
12820
12733
  });
12821
12734
  var VoiceKeySchema = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
12822
12735
  var AgendaTurnSchema = external_exports.object({
12736
+ /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
12737
+ * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
12738
+ * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
12739
+ * completion of a streamed re-plan carries the published sentence again, and the walk
12740
+ * drops what it already said by identity, never by the API's guess of what was polled.
12741
+ * Absent on plans nothing streams (a ring plan, a floor). */
12742
+ id: external_exports.string().optional(),
12823
12743
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
12824
12744
  * the planner declares duplicates instead of asking them twice. */
12825
12745
  coveredIds: external_exports.array(external_exports.string()).optional(),
@@ -12978,7 +12898,21 @@ var InboxItemSchema = external_exports.object({
12978
12898
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
12979
12899
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
12980
12900
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
12981
- agentX25519: external_exports.string().optional()
12901
+ agentX25519: external_exports.string().optional(),
12902
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
12903
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
12904
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
12905
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
12906
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
12907
+ communication: external_exports.object({
12908
+ deliveryId: external_exports.string(),
12909
+ kind: external_exports.enum(["notification", "call"]),
12910
+ entryId: external_exports.string(),
12911
+ goalIds: external_exports.array(external_exports.string()),
12912
+ decisionNeedId: external_exports.string().optional(),
12913
+ sealed: external_exports.boolean(),
12914
+ goalState: external_exports.string().optional()
12915
+ }).optional()
12982
12916
  });
12983
12917
  var SnoozeRequestSchema = external_exports.object({
12984
12918
  requestId: external_exports.string(),
@@ -13214,26 +13148,54 @@ var CreateRequestSchema = external_exports.object({
13214
13148
  * the agent reads it via get_thread. Must belong to the requesting user. */
13215
13149
  contextParentId: external_exports.string().optional()
13216
13150
  });
13217
- var HandoffSchema = external_exports.object({
13218
- /** Move this existing outcome to `target` without reminting it. Requires `target`. */
13219
- workId: external_exports.string().uuid().optional(),
13220
- /** Land the note on an existing thread; omitted mints a fresh one. */
13221
- parentId: external_exports.string().uuid().optional(),
13222
- /** One-line headline of the working context handed off. */
13223
- title: external_exports.string().min(1),
13224
- /** The brief — standalone notes the successor reads (what was done, what's left, links). */
13225
- notes: external_exports.array(external_exports.string().min(1)).min(1),
13226
- /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
13227
- * only; omit to leave the thread for the user to hand off in the app. */
13228
- target: external_exports.string().optional(),
13229
- /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
13230
- * thread's earlier turns for rehydration — get_thread returns the latest recap + only
13231
- * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
13232
- * same primitive; a recap is one whose audience includes you. */
13233
- recap: external_exports.boolean().optional()
13234
- }).refine((value) => !value.workId || Boolean(value.target), {
13235
- message: "target is required when handing off Work",
13236
- path: ["target"]
13151
+ var QueueQuestionSchema = external_exports.object({
13152
+ /** The decision need's id what an answer is accepted against. */
13153
+ id: external_exports.string(),
13154
+ /** The words that were asked, from the request Entry that asked them. */
13155
+ question: external_exports.string(),
13156
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
13157
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
13158
+ * can answer. */
13159
+ deliveryId: external_exports.string().nullable().default(null),
13160
+ /** The Entry the ruling is about. */
13161
+ aboutId: external_exports.string().nullable().default(null),
13162
+ /** Empty for a free-text question. */
13163
+ options: external_exports.array(OptionSchema).default([]),
13164
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
13165
+ askedAt: external_exports.string(),
13166
+ /** Null while the question is open which is how the page tells the two apart. */
13167
+ answeredAt: external_exports.string().nullable().default(null),
13168
+ /** The ruling in the person's own words, from the contribution that replied — not the
13169
+ * option id, which is not something anyone reads back. Null while it is open, and null
13170
+ * for a settled question whose reply carried nothing readable. */
13171
+ answer: external_exports.string().nullable().default(null)
13172
+ });
13173
+ var QueueItemSchema = external_exports.object({
13174
+ id: external_exports.string(),
13175
+ /** One-line headline — the first sentence of the outcome. */
13176
+ title: external_exports.string(),
13177
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
13178
+ intent: external_exports.string(),
13179
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
13180
+ state: external_exports.string(),
13181
+ /** Who holds it (a participant ref); null when nobody does yet. */
13182
+ assignee: external_exports.string().nullable().default(null),
13183
+ /** What the agent last said it was doing; null if it has said nothing. */
13184
+ progress: external_exports.string().nullable().default(null),
13185
+ reviewPending: external_exports.boolean().default(false),
13186
+ dueAt: external_exports.string().nullable().default(null),
13187
+ /** The Goal this one was opened under; null at the root. */
13188
+ parentGoalId: external_exports.string().nullable().default(null),
13189
+ /** Goals opened under this one — only those the same list holds. */
13190
+ childGoalIds: external_exports.array(external_exports.string()).default([]),
13191
+ /** Goals this one waits on (start or finish gates). */
13192
+ dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
13193
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
13194
+ blocked: external_exports.boolean().default(false),
13195
+ /** Every decision need on it, open or settled — the page decides which to show. */
13196
+ questions: external_exports.array(QueueQuestionSchema).default([]),
13197
+ createdAt: external_exports.string(),
13198
+ updatedAt: external_exports.string().nullable().default(null)
13237
13199
  });
13238
13200
  var NoteSourceSchema = external_exports.enum(["app", "call"]);
13239
13201
  var NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
@@ -13362,6 +13324,8 @@ var AcceptTriageResultSchema = external_exports.object({
13362
13324
  failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
13363
13325
  });
13364
13326
  var DeliveryModeSchema = external_exports.enum(["poll", "self_hosted"]);
13327
+ var WAKE_EVENT = "wake";
13328
+ var wakeChannel = (tokenId) => `wake:${tokenId}`;
13365
13329
  var RegisterDeliverySchema = external_exports.object({ mode: DeliveryModeSchema });
13366
13330
  var OAuthStartSchema = external_exports.object({
13367
13331
  provider: external_exports.enum(["cma"]),
@@ -13370,15 +13334,29 @@ var OAuthStartSchema = external_exports.object({
13370
13334
  var DeliveryConfigSchema = external_exports.object({
13371
13335
  tokenId: external_exports.string(),
13372
13336
  mode: DeliveryModeSchema,
13373
- /** null when the server has no SUPABASE_ANON_KEY set the listener then falls
13374
- * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
13337
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
13338
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
13339
+ * carries credentials; only a `poll` registration can come back with null here. */
13375
13340
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
13376
13341
  });
13377
13342
  var StatusSchema = external_exports.object({
13378
13343
  name: external_exports.string(),
13379
13344
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
13380
13345
  /** A phone is registered for push/ring (any push token on the account). */
13381
- phone: external_exports.boolean()
13346
+ phone: external_exports.boolean(),
13347
+ /** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
13348
+ * it never picked up. THE SAME NUMBER the harness's wake gate reads
13349
+ * (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
13350
+ * zero while the sweep sees one is two ideas of "waiting".
13351
+ *
13352
+ * Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
13353
+ * that nobody spawned, so a terminal session only learns of work by asking. The harness
13354
+ * used to paper over that by spawning a SECOND process on the identity; now it stands
13355
+ * back, correctly, and the person sitting at the terminal is the one who can act. A
13356
+ * coffee-beans request sat unread for three days.
13357
+ *
13358
+ * Optional: an older API sends no field, and the statusline then renders exactly as before. */
13359
+ waiting: external_exports.number().int().nonnegative().optional()
13382
13360
  });
13383
13361
  var EnvelopeRecipientSchema = external_exports.object({
13384
13362
  keyId: external_exports.string(),
@@ -13544,238 +13522,11 @@ var FeedbackOutcomeSchema = external_exports.object({
13544
13522
  message: external_exports.string(),
13545
13523
  childIds: external_exports.array(external_exports.string()).optional()
13546
13524
  });
13547
- var CONTACT_SCHEMA = contactSchemaFrom({
13548
- ask: NotifyRequestFields.shape.ask,
13549
- waiting: NotifyRequestFields.shape.waiting,
13550
- options: NotifyRequestFields.shape.options,
13551
- channel: NotifyRequestFields.shape.channel,
13552
- parentId: NotifyRequestFields.shape.parentId,
13553
- workId: NotifyRequestFields.shape.workId,
13554
- goalId: NotifyRequestFields.shape.goalId
13555
- });
13556
13525
  var import_tweetnacl = __toESM2(require_nacl_fast(), 1);
13557
13526
  var import_tweetnacl2 = __toESM2(require_nacl_fast(), 1);
13558
13527
  var import_tweetnacl3 = __toESM2(require_nacl_fast(), 1);
13559
13528
  var import_tweetnacl4 = __toESM2(require_nacl_fast(), 1);
13560
13529
  var import_tweetnacl5 = __toESM2(require_nacl_fast(), 1);
13561
- function utf8ToBytes(s) {
13562
- const out = [];
13563
- for (let i = 0; i < s.length; i++) {
13564
- const c = s.charCodeAt(i);
13565
- if (c < 128) out.push(c);
13566
- else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63);
13567
- else if (c >= 55296 && c <= 56319) {
13568
- const c2 = s.charCodeAt(i + 1);
13569
- if (c2 >= 56320 && c2 <= 57343) {
13570
- i++;
13571
- const cp = 65536 + ((c & 1023) << 10) + (c2 & 1023);
13572
- out.push(240 | cp >> 18, 128 | cp >> 12 & 63, 128 | cp >> 6 & 63, 128 | cp & 63);
13573
- } else out.push(239, 191, 189);
13574
- } else if (c >= 56320 && c <= 57343) out.push(239, 191, 189);
13575
- else out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
13576
- }
13577
- return Uint8Array.from(out);
13578
- }
13579
- function bytesToUtf8(b) {
13580
- let out = "";
13581
- for (let i = 0; i < b.length; ) {
13582
- const c = b[i++];
13583
- if (c < 128) out += String.fromCharCode(c);
13584
- else if (c < 224) out += String.fromCharCode((c & 31) << 6 | b[i++] & 63);
13585
- else if (c < 240) out += String.fromCharCode((c & 15) << 12 | (b[i++] & 63) << 6 | b[i++] & 63);
13586
- else {
13587
- const cp = (c & 7) << 18 | (b[i++] & 63) << 12 | (b[i++] & 63) << 6 | b[i++] & 63;
13588
- const u = cp - 65536;
13589
- out += String.fromCharCode(55296 + (u >> 10), 56320 + (u & 1023));
13590
- }
13591
- }
13592
- return out;
13593
- }
13594
- var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
13595
- function toB64(bytes) {
13596
- let out = "";
13597
- for (let i = 0; i < bytes.length; i += 3) {
13598
- const b0 = bytes[i];
13599
- const has1 = i + 1 < bytes.length;
13600
- const has2 = i + 2 < bytes.length;
13601
- const b1 = has1 ? bytes[i + 1] : 0;
13602
- const b2 = has2 ? bytes[i + 2] : 0;
13603
- out += B64[b0 >> 2] + B64[(b0 & 3) << 4 | b1 >> 4];
13604
- out += has1 ? B64[(b1 & 15) << 2 | b2 >> 6] : "=";
13605
- out += has2 ? B64[b2 & 63] : "=";
13606
- }
13607
- return out;
13608
- }
13609
- function fromB64Lenient(s) {
13610
- const clean = s.replace(/[^A-Za-z0-9+/]/g, "");
13611
- const out = [];
13612
- for (let i = 0; i < clean.length; i += 4) {
13613
- const c0 = B64.indexOf(clean[i]);
13614
- const c1 = B64.indexOf(clean[i + 1] ?? "A");
13615
- const c2 = clean[i + 2] !== void 0 ? B64.indexOf(clean[i + 2]) : -1;
13616
- const c3 = clean[i + 3] !== void 0 ? B64.indexOf(clean[i + 3]) : -1;
13617
- out.push(c0 << 2 | c1 >> 4);
13618
- if (c2 >= 0) out.push((c1 & 15) << 4 | c2 >> 2);
13619
- if (c3 >= 0) out.push((c2 & 3) << 6 | c3);
13620
- }
13621
- return Uint8Array.from(out);
13622
- }
13623
- function fromB64(s) {
13624
- const bytes = fromB64Lenient(s);
13625
- if (toB64(bytes) !== s) throw new Error("non-canonical base64");
13626
- return bytes;
13627
- }
13628
- function concat(parts) {
13629
- let n = 0;
13630
- for (const p of parts) n += p.length;
13631
- const out = new Uint8Array(n);
13632
- let o = 0;
13633
- for (const p of parts) {
13634
- out.set(p, o);
13635
- o += p.length;
13636
- }
13637
- return out;
13638
- }
13639
- function u16be(n) {
13640
- return Uint8Array.of(n >>> 8 & 255, n & 255);
13641
- }
13642
- function u32be(n) {
13643
- return Uint8Array.of(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255);
13644
- }
13645
- function readU32be(b, o) {
13646
- return (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
13647
- }
13648
- function lp(b) {
13649
- return concat([u32be(b.length), b]);
13650
- }
13651
- function eqCt(a, b) {
13652
- if (a.length !== b.length) return false;
13653
- let d = 0;
13654
- for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
13655
- return d === 0;
13656
- }
13657
- var ok = false;
13658
- function ensureCsprng() {
13659
- if (ok) return;
13660
- let a;
13661
- let b;
13662
- try {
13663
- a = import_tweetnacl.default.randomBytes(32);
13664
- b = import_tweetnacl.default.randomBytes(32);
13665
- } catch {
13666
- throw new Error("paigy-crypto: no secure RNG available \u2014 refusing to generate keys (fail-closed)");
13667
- }
13668
- if (a.every((x) => x === 0) || eqCt(a, b)) {
13669
- throw new Error("paigy-crypto: RNG self-test failed (constant/low-entropy output)");
13670
- }
13671
- ok = true;
13672
- }
13673
- function randomBytes(n) {
13674
- ensureCsprng();
13675
- return import_tweetnacl.default.randomBytes(n);
13676
- }
13677
- function keyId(publicKeyB64) {
13678
- return toB64(import_tweetnacl2.default.hash(fromB64(publicKeyB64)).slice(0, 8));
13679
- }
13680
- function credentialBody(c) {
13681
- return concat([
13682
- lp(utf8ToBytes("paigy-device-credential-v1")),
13683
- lp(utf8ToBytes(c.deviceId)),
13684
- lp(utf8ToBytes(c.kind)),
13685
- lp(fromB64(c.x25519Pub)),
13686
- lp(fromB64(c.ed25519Pub))
13687
- ]);
13688
- }
13689
- function verifyDeviceCredential(cred, uikPublicB64) {
13690
- const { sig, ...fields } = cred;
13691
- try {
13692
- return import_tweetnacl2.default.sign.detached.verify(credentialBody(fields), fromB64(sig), fromB64(uikPublicB64));
13693
- } catch {
13694
- return false;
13695
- }
13696
- }
13697
- var ENVELOPE_VERSION = 1;
13698
- var ENVELOPE_ALG = "x25519-xsalsa20poly1305";
13699
- function padme(L) {
13700
- if (L < 2) return L;
13701
- const E = Math.floor(Math.log2(L));
13702
- const S = Math.floor(Math.log2(E)) + 1;
13703
- const lastBits = E - S;
13704
- if (lastBits <= 0) return L;
13705
- const mask = (1 << lastBits) - 1;
13706
- return L + mask & ~mask;
13707
- }
13708
- function canonicalHeader(msgId, hdr) {
13709
- const parts = [
13710
- u16be(ENVELOPE_VERSION),
13711
- lp(utf8ToBytes(ENVELOPE_ALG)),
13712
- lp(msgId),
13713
- lp(utf8ToBytes(hdr.field)),
13714
- lp(utf8ToBytes(hdr.kind)),
13715
- lp(utf8ToBytes(hdr.senderRole)),
13716
- u32be(hdr.seq),
13717
- u32be(hdr.recipientKeyIds.length),
13718
- ...hdr.recipientKeyIds.map((k) => lp(utf8ToBytes(k)))
13719
- ];
13720
- return concat(parts);
13721
- }
13722
- function seal(input) {
13723
- ensureCsprng();
13724
- if (input.recipients.length === 0) throw new Error("seal: no recipients");
13725
- const kind = input.kind ?? "";
13726
- const recs = input.recipients.map((pub) => ({ pub, id: keyId(pub) })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
13727
- const hdr = {
13728
- field: input.field,
13729
- kind,
13730
- senderRole: input.senderRole,
13731
- recipientKeyIds: recs.map((r) => r.id),
13732
- seq: input.seq
13733
- };
13734
- const msgIdBytes = input.msgId ? fromB64(input.msgId) : randomBytes(16);
13735
- const cek = randomBytes(32);
13736
- const canon = canonicalHeader(msgIdBytes, hdr);
13737
- const plaintext = concat([u32be(canon.length), canon, u32be(input.body.length), input.body]);
13738
- const padded = new Uint8Array(padme(plaintext.length));
13739
- padded.set(plaintext);
13740
- const nonce = randomBytes(24);
13741
- const ct = import_tweetnacl3.default.secretbox(padded, nonce, cek);
13742
- const recipients = recs.map((r) => {
13743
- const eph = import_tweetnacl3.default.box.keyPair();
13744
- const wnonce = randomBytes(24);
13745
- const wrap = import_tweetnacl3.default.box(cek, wnonce, fromB64(r.pub), eph.secretKey);
13746
- return { keyId: r.id, epk: toB64(eph.publicKey), wnonce: toB64(wnonce), wrap: toB64(wrap) };
13747
- });
13748
- return {
13749
- v: ENVELOPE_VERSION,
13750
- alg: ENVELOPE_ALG,
13751
- msgId: toB64(msgIdBytes),
13752
- hdr,
13753
- recipients,
13754
- nonce: toB64(nonce),
13755
- ct: toB64(ct)
13756
- };
13757
- }
13758
- function open(envelope, myKeyId, mySecretKeyB64) {
13759
- if (envelope.v !== ENVELOPE_VERSION) throw new Error(`unsupported envelope version ${String(envelope.v)}`);
13760
- if (envelope.alg !== ENVELOPE_ALG) throw new Error(`unsupported alg ${String(envelope.alg)}`);
13761
- const rec = envelope.recipients.find((r) => r.keyId === myKeyId);
13762
- if (!rec) throw new Error("not a recipient of this envelope");
13763
- const cek = import_tweetnacl3.default.box.open(fromB64(rec.wrap), fromB64(rec.wnonce), fromB64(rec.epk), fromB64(mySecretKeyB64));
13764
- if (!cek) throw new Error("CEK unwrap failed (wrong key or tampered)");
13765
- const padded = import_tweetnacl3.default.secretbox.open(fromB64(envelope.ct), fromB64(envelope.nonce), cek);
13766
- if (!padded) throw new Error("body authentication failed (tampered ciphertext)");
13767
- if (padded.length < 8) throw new Error("malformed sealed plaintext");
13768
- const canonLen = readU32be(padded, 0);
13769
- if (canonLen < 0 || 8 + canonLen > padded.length) throw new Error("malformed sealed plaintext");
13770
- const sealedCanon = padded.slice(4, 4 + canonLen);
13771
- const bodyLen = readU32be(padded, 4 + canonLen);
13772
- const bodyStart = 8 + canonLen;
13773
- if (bodyStart + bodyLen > padded.length) throw new Error("malformed sealed plaintext");
13774
- const body = padded.slice(bodyStart, bodyStart + bodyLen);
13775
- const expected = canonicalHeader(fromB64(envelope.msgId), envelope.hdr);
13776
- if (!eqCt(sealedCanon, expected)) throw new Error("header mismatch (tampered metadata)");
13777
- return { header: envelope.hdr, body };
13778
- }
13779
13530
  function sessionSlot(sessionId2) {
13780
13531
  const id = sessionId2 ?? sessionId();
13781
13532
  return `session:${id.slice(0, 8)}`;
@@ -13883,45 +13634,6 @@ function readKeyFile() {
13883
13634
  return null;
13884
13635
  }
13885
13636
  }
13886
- async function fetchRoster(token) {
13887
- const res = await reach(`${BACKEND_URL}/api/roster`, { headers: { authorization: `Bearer ${token}` } });
13888
- if (!res.ok) return null;
13889
- return await res.json().catch(() => null);
13890
- }
13891
- function verifiedRecipients(roster, pinnedUikPub) {
13892
- return roster.devices.filter((cred) => verifyDeviceCredential(cred, pinnedUikPub)).map((cred) => cred.x25519Pub);
13893
- }
13894
- function sealFields(content, recipients) {
13895
- if (recipients.length === 0) throw new Error("sealFields: no verified recipients");
13896
- const seq = Math.floor(Date.now() / 1e3);
13897
- const out = {};
13898
- for (const field of ["context", "options", "visuals"]) {
13899
- if (content[field] === void 0) continue;
13900
- out[field] = seal({
13901
- body: utf8ToBytes(JSON.stringify(content[field])),
13902
- field,
13903
- senderRole: "agent",
13904
- seq,
13905
- recipients
13906
- });
13907
- }
13908
- return out;
13909
- }
13910
- function openSealedAnswer(sealed, keypair) {
13911
- const myKeyId = keyId(keypair.x25519.publicKey);
13912
- const { header, body } = open(sealed.envelope, myKeyId, keypair.x25519.secretKey);
13913
- if (header.field !== "answer" || header.senderRole !== "user") {
13914
- throw new Error("sealed answer has the wrong envelope field/role \u2014 refusing (possible replay/misattribution)");
13915
- }
13916
- const parsed = UserAnswerSchema.safeParse(JSON.parse(bytesToUtf8(body)));
13917
- if (!parsed.success) throw new Error("sealed answer body is not a valid UserAnswer");
13918
- const answer = parsed.data;
13919
- const bodyIsIgnored = answer.kind === "ignored";
13920
- if (sealed.ignored !== bodyIsIgnored) {
13921
- throw new Error("sealed answer tamper: `ignored` hint disagrees with the decrypted body");
13922
- }
13923
- return answer;
13924
- }
13925
13637
  var UnpairedError = class extends Error {
13926
13638
  constructor() {
13927
13639
  super("Not paired with Paigy \u2014 no valid token (the API answered 401). Run the pairing flow, then retry.");
@@ -13932,85 +13644,47 @@ function ensureAuthed(res) {
13932
13644
  if (res.status === 401) throw new UnpairedError();
13933
13645
  return res;
13934
13646
  }
13935
- async function sealForE2ee(req, token, deps = {}) {
13936
- const keyFile = (deps.readKeyFile ?? readKeyFile)();
13937
- if (!keyFile?.e2ee) return req;
13938
- req = deriveAsk(req);
13939
- if (!keyFile.uikPub) {
13940
- throw new Error("E2EE pairing is missing its pinned identity key (key.json.uikPub) \u2014 re-pair; refusing to send plaintext.");
13941
- }
13942
- const roster = await (deps.fetchRoster ?? fetchRoster)(token);
13943
- if (!roster) {
13944
- throw new Error("E2EE pairing has no device roster to seal to \u2014 refusing to send plaintext. Approve a device on your phone, then retry.");
13945
- }
13946
- const recipients = verifiedRecipients(roster, keyFile.uikPub);
13947
- if (recipients.length === 0) {
13948
- throw new Error("No roster device verified against the pinned identity key \u2014 refusing to send plaintext (a server-injected device is dropped).");
13949
- }
13950
- const sealedContext = {
13951
- ...req.context,
13952
- ...req.repo !== void 0 ? { repo: req.repo } : {},
13953
- ...req.branch !== void 0 ? { branch: req.branch } : {}
13954
- };
13955
- const envelope = sealFields({ context: sealedContext, options: req.options, visuals: req.visuals }, recipients);
13956
- const { context: _c, options: _o, visuals: _v, repo: _r, branch: _b, ...meta } = req;
13957
- return { ...meta, envelope };
13647
+ var ApiError = class extends Error {
13648
+ status;
13649
+ body;
13650
+ constructor(what, status, text) {
13651
+ super(`${what} failed: ${status} ${text}`);
13652
+ this.name = "ApiError";
13653
+ this.status = status;
13654
+ let body = void 0;
13655
+ try {
13656
+ body = JSON.parse(text);
13657
+ } catch {
13658
+ }
13659
+ this.body = body;
13660
+ }
13661
+ };
13662
+ async function fail(what, res) {
13663
+ throw new ApiError(what, res.status, await res.text());
13958
13664
  }
13959
- async function submitNotification(req, opts = {}) {
13665
+ async function claimGoal(goalId, opts = {}) {
13960
13666
  const token = authToken(opts.token) ?? "";
13961
- if (req.goalId) {
13962
- if (req.envelope) throw new Error("Goal-scoped contact does not support E2EE envelopes yet");
13963
- const shaped = deriveAsk(req);
13964
- if (!shaped.context || !shaped.select) throw new Error("Goal-scoped contact requires a shaped request or ask");
13965
- if (shaped.channel === "call" || shaped.urgency === "call") {
13966
- throw new Error("Goal-scoped call contact is not available yet; use a message contact");
13967
- }
13968
- if (shaped.urgency && shaped.urgency !== "inbox") throw new Error("Goal-scoped contact currently supports inbox delivery only");
13969
- if (shaped.points?.length) throw new Error("Goal-scoped multi-part contact is not available yet; send one decision at a time");
13970
- const content = [shaped.context.title, ...shaped.context.description].filter(Boolean).join("\n\n");
13971
- const res2 = ensureAuthed(await reach(`${BACKEND_URL}/api/goals/${encodeURIComponent(req.goalId)}/contact`, {
13972
- method: "POST",
13973
- headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
13974
- body: JSON.stringify({
13975
- operationId: randomUUID2(),
13976
- threadId: shaped.parentId ?? null,
13977
- content,
13978
- select: shaped.select,
13979
- options: shaped.options ?? [],
13980
- blocking: shaped.blocking,
13981
- channel: "message"
13982
- })
13983
- }));
13984
- if (!res2.ok) throw new Error(`goal contact failed: ${res2.status} ${await res2.text()}`);
13985
- return await res2.json();
13986
- }
13987
- const body = await sealForE2ee(req, token);
13988
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notify`, {
13989
- method: "POST",
13990
- headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
13991
- body: JSON.stringify(body)
13992
- }));
13993
- if (!res.ok) throw new Error(`notify failed: ${res.status} ${await res.text()}`);
13667
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
13668
+ if (!res.ok) await fail("claim_goal", res);
13994
13669
  return await res.json();
13995
13670
  }
13996
- async function sweepLeased(opts = {}) {
13997
- const token = authToken(opts.token);
13998
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending`, {
13999
- headers: {
14000
- authorization: `Bearer ${token}`,
14001
- "x-paigy-await-ack": "1"
14002
- }
13671
+ async function getGoal(goalId, opts = {}) {
13672
+ const token = authToken(opts.token) ?? "";
13673
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
13674
+ method: "GET",
13675
+ headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
14003
13676
  }));
14004
- if (!res.ok) throw new Error(`check_replies failed: ${res.status} ${await res.text()}`);
13677
+ if (!res.ok) await fail("get_goal", res);
14005
13678
  return await res.json();
14006
13679
  }
14007
- async function pendingSummary(opts = {}) {
14008
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/pending/summary`, {
14009
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
14010
- }));
14011
- if (!res.ok) throw new Error(`pending_summary failed: ${res.status} ${await res.text()}`);
13680
+ async function updateGoal(goalId, input, opts = {}) {
13681
+ const token = authToken(opts.token) ?? "";
13682
+ const operationId = input.operationId ?? randomUUID2();
13683
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
13684
+ if (!res.ok) await fail("update_goal", res);
14012
13685
  return await res.json();
14013
13686
  }
13687
+ var AWAIT_WINDOW_MS = 45e3;
14014
13688
  var tokenOverride = null;
14015
13689
  var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
14016
13690
  async function whoAmI(opts = {}) {
@@ -14043,6 +13717,15 @@ async function heartbeat(runtime, opts = {}) {
14043
13717
  }));
14044
13718
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
14045
13719
  }
13720
+ async function registerDelivery(mode, opts = {}) {
13721
+ const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
13722
+ method: "POST",
13723
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
13724
+ body: JSON.stringify({ mode })
13725
+ }));
13726
+ if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
13727
+ return await res.json();
13728
+ }
14046
13729
  async function listNotes(opts = {}) {
14047
13730
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
14048
13731
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -14090,293 +13773,155 @@ async function dismissTriage(proposalId, opts = {}) {
14090
13773
  if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
14091
13774
  return await res.json();
14092
13775
  }
14093
- var sleep3 = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
14094
- var ReplyLeaseExpiredError = class extends Error {
14095
- constructor() {
14096
- super(
14097
- "The Paigy reply lease expired before acknowledgement. The answer remains durable; wait on it again (contact with only { wait: notificationId }) to reclaim it before acting."
14098
- );
14099
- this.name = "ReplyLeaseExpiredError";
14100
- }
14101
- };
14102
- var ReplyAckError = class extends Error {
14103
- constructor(message, retryable) {
14104
- super(message);
14105
- this.retryable = retryable;
14106
- this.name = "ReplyAckError";
14107
- }
14108
- retryable;
14109
- };
14110
- function decryptAnswer(reply, deps = {}) {
14111
- if (!reply.sealed) return reply.answer;
14112
- const keyFile = (deps.readKeyFile ?? readKeyFile)();
14113
- if (!keyFile?.keypair) {
14114
- throw new Error(
14115
- "received a sealed answer but this agent has no device key to open it \u2014 re-pair."
14116
- );
14117
- }
14118
- return openSealedAnswer(reply.sealed, keyFile.keypair);
14119
- }
14120
- function decodeReply(item, deps = {}) {
14121
- const { delivery: _delivery, ...withoutDelivery } = item;
14122
- const enriched = { ...withoutDelivery };
14123
- if (!enriched.sealed) return enriched;
14124
- const answer = decryptAnswer(enriched, deps);
14125
- const { sealed: _sealed, ...plaintext } = enriched;
14126
- return { ...plaintext, answer };
14127
- }
14128
- function deliveryOf(item) {
14129
- if (item.type !== "reply" || !("delivery" in item) || item.delivery === void 0) {
14130
- return null;
14131
- }
14132
- const delivery = item.delivery;
14133
- if (!delivery || typeof delivery.leaseId !== "string" || !delivery.leaseId || typeof delivery.expiresAt !== "string" || !delivery.expiresAt) {
14134
- throw new Error(
14135
- "Paigy returned invalid reply-delivery metadata; the answer was not acknowledged and remains recoverable."
14136
- );
14137
- }
14138
- return delivery;
14139
- }
14140
- async function responseDetail(res) {
13776
+ async function contact(input, opts = {}) {
13777
+ const parsed = ContactSchema.parse(input);
13778
+ opts.signal?.throwIfAborted();
13779
+ const send2 = opts.reach ?? reach;
13780
+ const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
13781
+ let deliveryId;
13782
+ if ("deliveryId" in parsed) deliveryId = parsed.deliveryId;
13783
+ else {
13784
+ if (!opts.reach && readKeyFile()?.e2ee) throw new Error("Goal contact does not support E2EE yet; refusing to send plaintext.");
13785
+ const shaped = deriveAsk(NotifyRequestSchema.parse({ ask: parsed.ask, waiting: parsed.waiting, options: parsed.options }));
13786
+ const res = ensureAuthed(await send2(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
13787
+ method: "POST",
13788
+ headers,
13789
+ signal: opts.signal,
13790
+ body: JSON.stringify({ operationId: opts.operationId ?? randomUUID3(), threadId: parsed.threadId ?? null, content: parsed.ask, select: shaped.select, options: shaped.options ?? [], blocking: parsed.waiting === "hard", channel: parsed.channel === "call" ? "call" : "message" })
13791
+ }));
13792
+ if (!res.ok) await fail("goal contact", res);
13793
+ const receipt = await res.json();
13794
+ deliveryId = receipt.deliveryId ?? receipt.callId ?? receipt.notificationId ?? "";
13795
+ if (!deliveryId) throw new Error("Goal contact returned no Delivery identity");
13796
+ }
13797
+ const read = async (signal2) => {
13798
+ const res = ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
13799
+ if (!res.ok) await fail("read_delivery", res);
13800
+ const delivery = await res.json();
13801
+ return delivery.kind === "notification" ? { ...delivery, message: `${delivery.message}
13802
+ Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answers. Do not poll this Notification.` } : delivery;
13803
+ };
13804
+ const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
13805
+ if (opts.waits === false) return read(opts.signal);
13806
+ const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
13807
+ const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
13808
+ let latest;
14141
13809
  try {
14142
- return (await res.text()).slice(0, 300);
14143
- } catch {
14144
- return "";
14145
- }
14146
- }
14147
- async function acknowledge(token, leaseId, opts = {}) {
14148
- const attempts = Math.max(1, Math.min(opts.attempts ?? 3, 5));
14149
- const backoffMs = Math.max(0, opts.backoffMs ?? 100);
14150
- const doSleep = opts.doSleep ?? sleep3;
14151
- let lastError;
14152
- for (let attempt = 0; attempt < attempts; attempt += 1) {
14153
- try {
14154
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/await/ack`, {
14155
- method: "POST",
14156
- headers: {
14157
- authorization: `Bearer ${token}`,
14158
- "content-type": "application/json"
14159
- },
14160
- body: JSON.stringify({ leaseId })
14161
- }));
14162
- if (res.ok) return;
14163
- const detail2 = await responseDetail(res);
14164
- if (res.status === 409) throw new ReplyLeaseExpiredError();
14165
- const retryable = res.status === 408 || res.status === 425 || res.status === 429 || res.status >= 500;
14166
- throw new ReplyAckError(
14167
- `Paigy reply acknowledgement failed: ${res.status}${detail2 ? ` ${detail2}` : ""}`,
14168
- retryable
14169
- );
14170
- } catch (error) {
14171
- if (error instanceof UnpairedError || error instanceof ReplyLeaseExpiredError || error instanceof ReplyAckError && !error.retryable) {
14172
- throw error;
14173
- }
14174
- lastError = error;
14175
- if (attempt + 1 < attempts) {
14176
- await doSleep(backoffMs * 2 ** attempt);
14177
- }
13810
+ while (true) {
13811
+ signal.throwIfAborted();
13812
+ latest = await read(signal);
13813
+ if (settled(latest)) return latest;
13814
+ await sleep2(5e3, void 0, { signal });
14178
13815
  }
13816
+ } catch (error) {
13817
+ opts.signal?.throwIfAborted();
13818
+ if (window.aborted && latest) return latest;
13819
+ throw error;
14179
13820
  }
14180
- const detail = lastError instanceof Error ? lastError.message : String(lastError);
14181
- throw new Error(
14182
- `Could not acknowledge the Paigy reply after ${attempts} attempts (${detail}). The answer was not returned to the agent and remains recoverable after the lease expires.`
14183
- );
14184
- }
14185
- async function receiveReply(item, token, opts = {}) {
14186
- const decoded = decodeReply(item, opts);
14187
- const delivery = deliveryOf(item);
14188
- if (!delivery) {
14189
- if (opts.requireDelivery) {
14190
- throw new Error("Paigy returned a catch-up reply without a delivery lease; the answer was not exposed.");
14191
- }
14192
- return decoded;
14193
- }
14194
- await acknowledge(token, delivery.leaseId, {
14195
- attempts: opts.ackAttempts,
14196
- backoffMs: opts.ackBackoffMs,
14197
- doSleep: opts.ackSleep
14198
- });
14199
- return decoded;
14200
13821
  }
14201
13822
  async function checkReplies(opts = {}) {
14202
- const pending = await sweepLeased(opts);
14203
- const token = authToken(opts.token) ?? "";
14204
- const replies = await Promise.all(pending.replies.map(async (reply) => {
14205
- const received = await receiveReply(
14206
- { type: "reply", ...reply },
14207
- token,
14208
- { requireDelivery: true }
14209
- );
14210
- const { type: _type, ...withoutType } = received;
14211
- return withoutType;
13823
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
13824
+ headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
14212
13825
  }));
14213
- return {
14214
- ...pending,
14215
- replies,
14216
- work: pending.work ?? []
14217
- };
14218
- }
14219
- async function patchState(path, state, opts, operation) {
14220
- const token = authToken(opts.token) ?? "";
14221
- const res = ensureAuthed(await reach(
14222
- `${BACKEND_URL}${path}`,
14223
- {
14224
- method: "PATCH",
14225
- headers: {
14226
- "content-type": "application/json",
14227
- authorization: `Bearer ${token}`,
14228
- "x-paigy-await-ack": "1"
14229
- },
14230
- body: JSON.stringify({ state })
14231
- }
14232
- ));
14233
- if (!res.ok) {
14234
- throw new Error(`${operation} failed: ${res.status} ${await res.text()}`);
14235
- }
13826
+ if (!res.ok) await fail("check_replies", res);
14236
13827
  return await res.json();
14237
13828
  }
14238
- function setTaskState(notificationId, state, opts = {}) {
14239
- return patchState(`/api/notify/${encodeURIComponent(notificationId)}/state`, state, opts, "set_task_state");
14240
- }
14241
-
14242
- // src/main.ts
14243
- var import_qrcode = __toESM(require_lib(), 1);
14244
- import { homedir as homedir7, hostname } from "os";
14245
-
14246
- // src/harness/catalog.ts
14247
- import { spawnSync } from "child_process";
14248
- import { existsSync as existsSync2 } from "fs";
14249
- import { homedir as homedir2 } from "os";
14250
- import { delimiter, dirname, join as join2 } from "path";
14251
- var CATALOG = [
14252
- {
14253
- name: "claude",
14254
- label: "Claude Code",
14255
- cli: "claude",
14256
- adapter: "claude-agent-acp",
14257
- authProbe: ["claude", "auth", "status"],
14258
- loginHint: "Run `claude` once in a terminal and sign in.",
14259
- installCli: "curl -fsSL https://claude.ai/install.sh | bash",
14260
- installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
14261
- },
14262
- {
14263
- name: "codex",
14264
- label: "Codex",
14265
- cli: "codex",
14266
- adapter: "codex-acp",
14267
- authProbe: ["codex", "login", "status"],
14268
- loginHint: "Run `codex login` to authenticate.",
14269
- installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
14270
- installAdapter: "npm install -g @agentclientprotocol/codex-acp"
14271
- },
14272
- {
14273
- name: "agy",
14274
- label: "Antigravity",
14275
- cli: "agy",
14276
- authProbe: ["agy", "help"],
14277
- loginHint: "Install and set up Antigravity (`agy`).",
14278
- installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
14279
- }
14280
- ];
14281
- function probeDirs(home = homedir2()) {
14282
- return [
14283
- dirname(process.execPath),
14284
- join2(home, ".local", "bin"),
14285
- "/usr/local/bin",
14286
- "/opt/homebrew/bin",
14287
- join2(home, ".npm-global", "bin")
14288
- ];
14289
- }
14290
- function which(bin) {
14291
- const hit = spawnSync("which", [bin], { encoding: "utf8" });
14292
- const fromPath = hit.status === 0 ? hit.stdout.trim() : "";
14293
- if (fromPath) return fromPath;
14294
- for (const dir of probeDirs()) {
14295
- const candidate = join2(dir, bin);
14296
- if (existsSync2(candidate)) return candidate;
14297
- }
14298
- return null;
14299
- }
14300
- function runProbe(argv) {
14301
- const [cmd, ...args] = argv;
14302
- if (!cmd) return false;
14303
- const resolved = which(cmd);
14304
- if (!resolved) return false;
14305
- return spawnSync(resolved, args, {
14306
- timeout: 1e4,
14307
- stdio: "ignore",
14308
- env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) }
14309
- }).status === 0;
14310
- }
14311
- function detect(entry, deps = {}) {
14312
- const find = deps.which ?? which;
14313
- const probe = deps.probe ?? runProbe;
14314
- const base = { name: entry.name, label: entry.label };
14315
- const cliPath = find(entry.cli);
14316
- if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
14317
- const adapterPath = entry.adapter ? find(entry.adapter) : null;
14318
- if (entry.adapter && !adapterPath) {
14319
- return {
14320
- ...base,
14321
- status: "adapter-missing",
14322
- hint: `${entry.label} is installed, but its ACP adapter (${entry.adapter}) is not.`,
14323
- ...entry.installAdapter ? { install: entry.installAdapter } : {}
14324
- };
14325
- }
14326
- const bin = adapterPath ?? cliPath;
14327
- if (!probe(entry.authProbe)) return { ...base, status: "login", bin, hint: entry.loginHint };
14328
- return { ...base, status: "ready", bin };
14329
- }
14330
- function detectAll(deps = {}) {
14331
- return CATALOG.map((entry) => detect(entry, deps));
14332
- }
14333
-
14334
- // src/host.ts
14335
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
14336
- import { homedir as homedir5 } from "os";
14337
- import { join as join4 } from "path";
14338
-
14339
- // src/workspaces.ts
14340
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
14341
- import { dirname as dirname2, join as join3, resolve } from "path";
14342
- import { homedir as homedir3 } from "os";
14343
- function workspacesFile() {
14344
- return join3(homedir3(), ".paigy", "workspaces.json");
14345
- }
14346
- function listWorkspaces(deps) {
13829
+ var HEARTBEAT_MS = 25e3;
13830
+ var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
13831
+ var JOIN = { config: { broadcast: { self: false } } };
13832
+ var lines = /* @__PURE__ */ new Map();
13833
+ function frame(line, topic, event, payload) {
14347
13834
  try {
14348
- const parsed = JSON.parse(readFileSync2(deps.file, "utf8"));
14349
- return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
13835
+ line.ws?.send(JSON.stringify({ topic, event, payload, ref: String(++line.ref) }));
14350
13836
  } catch {
14351
- return [];
14352
13837
  }
14353
13838
  }
14354
- function addWorkspace(dir, deps) {
14355
- const next = resolve(dir.replace(/^~(?=$|\/)/, homedir3()));
14356
- const all = listWorkspaces(deps);
14357
- if (!all.includes(next)) {
14358
- all.push(next);
14359
- mkdirSync2(dirname2(deps.file), { recursive: true });
14360
- writeFileSync2(deps.file, JSON.stringify(all, null, 2));
14361
- }
14362
- return all;
13839
+ function stop(line) {
13840
+ line.open = false;
13841
+ clearInterval(line.heart);
13842
+ clearTimeout(line.retry);
14363
13843
  }
14364
- function removeWorkspace(dir, deps) {
14365
- const all = listWorkspaces(deps).filter((w) => w !== dir);
14366
- mkdirSync2(dirname2(deps.file), { recursive: true });
14367
- writeFileSync2(deps.file, JSON.stringify(all, null, 2));
14368
- return all;
13844
+ function connect(line) {
13845
+ const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
13846
+ const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
13847
+ ws.addEventListener("open", () => {
13848
+ line.open = true;
13849
+ line.attempt = 0;
13850
+ for (const topic of line.topics.keys()) frame(line, `realtime:${topic}`, "phx_join", JOIN);
13851
+ line.heart = setInterval(() => frame(line, "phoenix", "heartbeat", {}), HEARTBEAT_MS);
13852
+ });
13853
+ ws.addEventListener("message", (e) => {
13854
+ let m;
13855
+ try {
13856
+ m = JSON.parse(String(e.data));
13857
+ } catch {
13858
+ return;
13859
+ }
13860
+ if (m.event !== "broadcast" || !m.topic?.startsWith("realtime:")) return;
13861
+ for (const l of line.topics.get(m.topic.slice("realtime:".length)) ?? []) {
13862
+ if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
13863
+ }
13864
+ });
13865
+ let gone = false;
13866
+ const again = () => {
13867
+ if (gone) return;
13868
+ gone = true;
13869
+ stop(line);
13870
+ if (!line.topics.size) return;
13871
+ line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
13872
+ };
13873
+ ws.addEventListener("close", again);
13874
+ ws.addEventListener("error", again);
14369
13875
  }
14370
- function allowed(cwd, deps) {
14371
- const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir3()));
14372
- return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
13876
+ function subscribeRealtime(args) {
13877
+ const key = `${args.url} ${args.anonKey}`;
13878
+ let line = lines.get(key);
13879
+ if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
13880
+ let heard = line.topics.get(args.topic);
13881
+ if (!heard) {
13882
+ line.topics.set(args.topic, heard = /* @__PURE__ */ new Set());
13883
+ if (line.open) frame(line, `realtime:${args.topic}`, "phx_join", JOIN);
13884
+ }
13885
+ const me = { event: args.event, onMessage: args.onMessage };
13886
+ heard.add(me);
13887
+ if (!line.ws) connect(line);
13888
+ return {
13889
+ close: () => {
13890
+ if (!heard.delete(me) || heard.size) return;
13891
+ line.topics.delete(args.topic);
13892
+ if (line.topics.size) {
13893
+ if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
13894
+ return;
13895
+ }
13896
+ lines.delete(key);
13897
+ stop(line);
13898
+ try {
13899
+ line.ws?.close();
13900
+ } catch {
13901
+ }
13902
+ }
13903
+ };
14373
13904
  }
14374
- function resolveWakeDir(pinned, deps) {
14375
- if (pinned && allowed(pinned, deps)) {
14376
- const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir3()));
14377
- if ((deps.exists ?? existsSync3)(dir)) return dir;
14378
- }
14379
- return listWorkspaces(deps)[0];
13905
+ async function subscribeWake(onNudge, opts = {}) {
13906
+ const cfg = await registerDelivery("self_hosted", opts);
13907
+ const { url, anonKey } = cfg.realtime;
13908
+ const channel = wakeChannel(cfg.tokenId);
13909
+ const ch = subscribeRealtime({
13910
+ url,
13911
+ anonKey,
13912
+ topic: channel,
13913
+ event: WAKE_EVENT,
13914
+ onMessage: (payload) => onNudge(payload ?? {})
13915
+ });
13916
+ return {
13917
+ tokenId: cfg.tokenId,
13918
+ channel,
13919
+ close: async () => {
13920
+ ch.close();
13921
+ await registerDelivery("poll", opts).catch(() => {
13922
+ });
13923
+ }
13924
+ };
14380
13925
  }
14381
13926
 
14382
13927
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
@@ -15666,20 +15211,17 @@ var zodToJsonSchema2 = (schema, options) => {
15666
15211
  // ../../packages/schema/dist/index.js
15667
15212
  var OPTIONS_MIN2 = 2;
15668
15213
  var OPTIONS_MAX2 = 6;
15669
- var MISSED_CALL_PLAN2 = {
15670
- retry_10m: { kind: "every", minutes: 10 },
15671
- retry_30m: { kind: "every", minutes: 30 },
15672
- retry_60m: { kind: "every", minutes: 60 },
15673
- backoff_gentle: { kind: "at", minutes: [30, 120, 360] },
15674
- backoff_standard: { kind: "at", minutes: [10, 30, 120] },
15675
- backoff_aggressive: { kind: "at", minutes: [5, 15, 45] },
15676
- inbox: { kind: "once" },
15677
- dismiss: { kind: "grace", minutes: 2 }
15678
- };
15679
15214
  function draft20202(node) {
15680
15215
  if (Array.isArray(node)) return node.map(draft20202);
15681
15216
  if (node && typeof node === "object") {
15682
15217
  const o = node;
15218
+ if (Array.isArray(o.items)) {
15219
+ o.prefixItems = o.items;
15220
+ if ("additionalItems" in o) {
15221
+ o.items = o.additionalItems;
15222
+ delete o.additionalItems;
15223
+ } else delete o.items;
15224
+ }
15683
15225
  for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
15684
15226
  if (typeof o[excl] === "boolean") {
15685
15227
  if (o[excl] === true && typeof o[lim] === "number") {
@@ -15698,11 +15240,54 @@ function mcpInputSchema2(s) {
15698
15240
  delete schema.$schema;
15699
15241
  return draft20202(schema);
15700
15242
  }
15243
+ var StartContactSchema2 = external_exports.object({
15244
+ goalIds: external_exports.tuple([external_exports.string().uuid()]),
15245
+ // THE ASK IS THE QUESTION, not a bulletin with a question at the end (2026-09-16).
15246
+ //
15247
+ // A DecisionNeed is settled only by an answer in the shape THIS ask declares. Bundle news,
15248
+ // findings and a decision into one, and the person answers whichever part engaged them —
15249
+ // which settles nothing, leaves the need open, and gets them asked again. On call 619c5a92
15250
+ // one ask carried a briefing, a side question ("the ring should show the agent's name now —
15251
+ // what did you actually see?") and a one-of-four about how far an erase reaches. The owner
15252
+ // answered the side question. The need stayed open; the card asked again. Of the 45 call
15253
+ // asks carrying a decision, 27 are over 600 characters.
15254
+ //
15255
+ // Sending them separately costs nothing, because a CALL contact joins the call already
15256
+ // happening (`goal/store.ts`, JOIN BEFORE MINTING) — several contacts on one Goal arrive as
15257
+ // one call, planned as its turns, and each keeps its own settleable need.
15258
+ ask: external_exports.string().trim().min(1).max(1e4).describe(
15259
+ "The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
15260
+ ),
15261
+ waiting: external_exports.enum(["none", "hard"]).default("none"),
15262
+ channel: external_exports.enum(["notification", "call"]).default("notification"),
15263
+ options: external_exports.array(external_exports.object({ label: external_exports.string().trim().min(1).max(1e3), image: external_exports.string().url().optional(), html: external_exports.string().max(16384).optional() }).strict()).min(2).max(6).optional(),
15264
+ threadId: external_exports.string().uuid().optional().describe("Continue an existing Thread: the threadId a prior Delivery returned. Omit to start a new Thread.")
15265
+ }).strict();
15266
+ var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
15267
+ var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
15268
+ var CONTACT_DESCRIPTION2 = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
15701
15269
  var CreateGoalSchema2 = external_exports.object({
15702
15270
  outcome: external_exports.string().trim().min(1).max(1e4),
15703
15271
  ownerParticipant: external_exports.string().trim().min(1).optional(),
15704
- idempotencyKey: external_exports.string().trim().min(1).max(200)
15272
+ idempotencyKey: external_exports.string().trim().min(1).max(200),
15273
+ /** A past conversation this Goal should be read against — History's "new session from this"
15274
+ * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
15275
+ * reference only: the owner reads it through `get_thread`, which does its own scoping, and
15276
+ * the writer refuses a thread belonging to another account. */
15277
+ contextThreadId: external_exports.string().uuid().optional(),
15278
+ /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
15279
+ * didn't get created as a child goal of the voice UI goal, which is how it should've
15280
+ * worked"). It could not have been: this door took no parent, so the only route was
15281
+ * create -> claim -> `update_goal`, three calls with a lease in the middle, and every agent
15282
+ * took the short one. The hierarchy has been modelled since Goals existed and had been used
15283
+ * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
15284
+ * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
15285
+ parentGoalId: external_exports.string().uuid().optional()
15705
15286
  });
15287
+ var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
15288
+ idempotencyKey: CreateGoalSchema2.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
15289
+ }).strict();
15290
+ var CREATE_GOAL_DESCRIPTION2 = "Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.";
15706
15291
  var UpdateGoalSchema2 = external_exports.object({
15707
15292
  revision: external_exports.number().int().positive(),
15708
15293
  changes: external_exports.object({
@@ -15718,52 +15303,48 @@ var UpdateGoalSchema2 = external_exports.object({
15718
15303
  reason: external_exports.string().trim().min(1).max(2e3),
15719
15304
  operationId: external_exports.string().uuid().optional()
15720
15305
  }).strict();
15721
- var UpdateGoalToolSchema2 = UpdateGoalSchema2.extend({ goalId: external_exports.string().uuid() }).strict();
15306
+ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
15722
15307
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
15723
- var fmtMin2 = (m) => m >= 60 ? `${m / 60} hr` : `${m} min`;
15724
- var STANDARD_MEANS2 = (() => {
15725
- const plan = MISSED_CALL_PLAN2.backoff_standard;
15726
- const mins = plan.kind === "at" ? plan.minutes : [];
15727
- const parts = mins.map(fmtMin2);
15728
- const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}` : parts[0] ?? "";
15729
- return `Rings again ${list} after the missed call, then leaves it in your inbox`;
15730
- })();
15731
- function contactSchemaFrom2(fields) {
15732
- const surface = external_exports.object({
15733
- ask: fields.ask.describe(
15734
- `What to tell the user, or what you need to find out from them. Plain prose \u2014 as long as it needs to be (up to 10k characters); Paigy splits it into topics and reads back a few sentences at a time, so do NOT compress a briefing into one line. May be spoken aloud on a call, so write natural speech and name things (not IDs). Contact at exactly two moments: BLOCKED on a decision only they can make, or DONE (one short report \u2014 what shipped, how you verified it, what you flagged). DONE IS SAID ONCE: "all set", "nothing open on my end", "that thread is complete" are the same report in new words, and each one reaches them separately (live 2026-08-12: three of them in three minutes). After the first, you are finished speaking; if they acknowledge it, stop rather than confirming the acknowledgement. Progress is never a contact: set_work_state carries it, and working narration stays in your own terminal \u2014 the user sees you're working without being interrupted by it.`
15735
- ),
15736
- waiting: fields.waiting.describe(
15737
- "What happens to your work while you wait. 'none': you're just informing them. 'soft': you'd like an answer but can keep working. 'hard': you are STOPPED until they answer \u2014 reaches them urgently and escalates to a real phone call if unanswered."
15738
- ),
15739
- options: fields.options.describe(
15740
- `The choices the user picks from, when you have them \u2014 ${OPTIONS_MIN2} to ${OPTIONS_MAX2}, drawn from your own sentence.`
15741
- ),
15742
- channel: fields.channel.describe(
15743
- "Relay how the user explicitly said to reach them ('call me' \u2192 'call', 'just message/text me' \u2192 'message'), or use 'call' when promoting the same quiet ask after it becomes a substantial blocker. Omit otherwise; Paigy picks."
15744
- ),
15745
- parentId: fields.parentId.describe(
15746
- "To continue an earlier conversation, pass the parentId a previous contact or reply returned. Omit to start a new one."
15747
- ),
15748
- workId: fields.workId.describe(
15749
- "The durable Work this contact advances. Pass the workId from check_replies or a prior reply when asking for a decision that blocks that work."
15750
- ),
15751
- goalId: fields.goalId.describe(
15752
- "The target Goal this contact advances. Use goalId for the Goal model; do not combine it with workId."
15753
- ),
15754
- // THE WAIT, CONTINUED (owner, 2026-09-06: "await should have been folded into contact").
15755
- // A contact that rang holds its first window itself; the host caps one tool call at
15756
- // ~60 s, so keeping the line is another contact — with ONLY this field. Nothing is sent.
15757
- wait: external_exports.string().uuid().optional().describe(
15758
- "KEEP WAITING on a live call: the notificationId a previous contact returned. Send it ALONE \u2014 no ask, nothing new goes to the user; contact just holds the next ~45 s window and returns the outcome in `wait`."
15759
- )
15760
- });
15761
- const out = mcpInputSchema2(surface);
15762
- delete out.required;
15763
- out.anyOf = [{ required: ["ask"] }, { required: ["wait"] }];
15764
- return out;
15308
+ var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
15309
+ var GET_GOAL_DESCRIPTION2 = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
15310
+ var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. Returns the new revision and a prose summary.";
15311
+ var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
15312
+ var CHECK_REPLIES_DESCRIPTION2 = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 each with its durable Entries, accepted decisions and open decision needs, in the same shape a contact read returns. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it with contact({deliveryId}). Your runnable and review-pending Goals come from claim_goal, not from here.";
15313
+ var CheckRepliesSchema2 = external_exports.object({}).strict();
15314
+ var GetThreadSchema2 = external_exports.object({
15315
+ parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
15316
+ }).strict();
15317
+ var GET_THREAD_DESCRIPTION2 = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
15318
+ var SearchThreadsSchema2 = external_exports.object({
15319
+ q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
15320
+ }).strict();
15321
+ var SEARCH_THREADS_DESCRIPTION2 = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
15322
+ var AGENT_TOOLS2 = [
15323
+ { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
15324
+ { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
15325
+ { name: "get_thread", description: GET_THREAD_DESCRIPTION2, inputSchema: mcpInputSchema2(GetThreadSchema2) },
15326
+ { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchThreadsSchema2) },
15327
+ { name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
15328
+ { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
15329
+ { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
15330
+ { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
15331
+ ];
15332
+ var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
15333
+ function entryWords(entry) {
15334
+ const content = entry.content;
15335
+ if (content && "sealed" in content) return "";
15336
+ const plain = content?.plain;
15337
+ if (typeof plain === "string") return plain;
15338
+ if (plain && typeof plain === "object") {
15339
+ const text = plain.text;
15340
+ if (typeof text === "string") return text;
15341
+ const title = plain.title;
15342
+ const description = plain.description;
15343
+ const parts = [title, ...Array.isArray(description) ? description : []].filter((v) => typeof v === "string");
15344
+ if (parts.length) return parts.join("\n\n");
15345
+ }
15346
+ return entry.sources.map((source) => source.text).join("\n");
15765
15347
  }
15766
- var CONTACT_DESCRIPTION2 = `Reach the user through Paigy \u2014 tell them something, or ask and get their answer. State what you need in \`ask\`, say what happens to your work while you wait in \`waiting\`, and Paigy handles the rest (channel, phrasing, answer format). If the user explicitly asks you to CALL them, send waiting:'hard' and say so in the ask. Returns { notificationId, parentId, workId?, goalId?, entryId?, decisionId?, wait? }. WHEN IT RANG, contact holds the first ~45 s window ITSELF and \`wait\` carries the outcome: { type:'reply', answer } to act on; { type:'partial', inFlight:true, turn } \u2014 what the user is saying to each turn, provisional: use it to PREPARE (fetch, draft, warm the build), never to act irreversibly, they can still revise it until the final reply (partial = intelligence, settled = authorization; if a partial's acts carry a question aimed at you and you know the answer, contact on the SAME parentId right away \u2014 they hear it on the same call); { type:'remind', remindInSeconds } \u2014 schedule a wake-up; { type:'idle' } \u2014 still waiting. TO KEEP WAITING, call contact again with ONLY { wait: notificationId } \u2014 no ask, nothing new is sent; it holds the next scoped ~45 s window (under the 60 s host cap, so it always returns) and never returns another notification's reply. Keep doing that until the reply \u2014 THAT one is the decision \u2014 so the user steps away and comes back to find you already continued; stop only to do other work and check back, or after an unreasonably long stretch worth telling them about. A message delivery has NO \`wait\`: never poll for it \u2014 the reply arrives through check_replies or your wake. Pass parentId to a later contact to continue the conversation. A Goal-targeted contact also returns goalId and entryId for the durable Goal entry. When it rang, the reply also carries { ifMissed: { mode, means } }: what the user's own policy does with a call they don't take ("${STANDARD_MEANS2}"), so a no-answer tells you how long to wait before coming back. THREADING REPLACES: a threaded follow-up SUPERSEDES your earlier pending items on that thread \u2014 right for updates to one ask, WRONG for a checklist (send independent to-dos un-threaded). A threaded re-send with IDENTICAL content escalates the pending ask in place. If a reply comes back as {kind:'clarify', chunks:[...]}, the user wants more detail \u2014 contact again on the SAME parentId with an expanded ask. BLOCKED ON A DECISION for existing work? Pass that work's \`workId\`; the reply returns the same workId plus a decisionId, so the answer resumes the right outcome. ONE ASK, ONE ROW: never restate a still-pending ask's question inside a NEW contact (e.g. weaving it into a briefing) \u2014 the whole answer settles on the new row and the original can never receive it. Keep waiting on the original (a live call reads every pending ask out separately, each answer routes to its own row), and use \`needs\` for a genuinely multi-part NEW ask. ANSWERABLE, NOT JUST ASKED: when the reply comes back carrying \`plan.units[].needs\`, that unit asked for something it gave the user no way to answer \u2014 'options' means it posed a choice with nothing to choose from, 'visuals' means it asked about something to look at with nothing to look at. Send it again on the SAME parentId with ${OPTIONS_MIN2}-${OPTIONS_MAX2} options (or the image), drawn from your own sentence. Paigy will not add them for you: a shape it guessed wrong cannot be undone, and you are the one who knows what the real alternatives are. \`units\` reports WHAT BECAME OF YOUR PROSE \u2014 { kept, raw, why }: how many topics Paigy compressed for delivery, how many kept your exact words, and the reason when it kept them (e.g. 'no_output' = compression produced nothing usable, so the user got your raw sentence). It needs no action and is not an error \u2014 read it only when the delivered wording matters to you; a high \`raw\` count means the user is hearing you verbatim. READING A CALL'S REPLY: it can come back as {kind:'turns', turns:[{prompt,reply}]} \u2014 the ordered log of that call. Read turns[0].reply as the user's main instruction. Usually that's the only turn; if there are more (e.g. an end-of-call 'call me back when it's done / I have a question that blocks me'), read each one in order as a further follow-up instruction, not a single combined one. If they asked for a callback, re-engage in the SAME thread (contact with the reply's parentId) when the task is done or you hit a blocker \u2014 waiting:'hard' for a blocker, waiting:'none' for done. Paigy has no scheduler; the callback is yours to send (use ScheduleWakeup/cron for timing). A call-mapped answer may carry \`intents\` \u2014 next steps the user attached, each { kind, detail } with detail quoting their words. ACT on them, don't just read them: 'defer' ("call me after lunch") \u2192 register it NOW with schedule_callback \u2014 when the intent carries \`dueInSeconds\` (Paigy pre-parsed the spoken time against the user's clock) pass it straight through; otherwise derive it from the detail yourself \u2014 then follow up on the same thread; 'delegate' ("you pick") \u2192 make the call yourself and tell them what you chose; 'channel' ("text me next time") \u2192 honor it on your next contact (channel:'message'); 'question' (an open question aimed back at you that the call couldn't answer) \u2192 you OWE them the answer \u2014 work it out and follow up on the same thread without being asked, the call deliberately skipped "should I call you back?" because the follow-up is implied. \`transcript\` is the user's raw words behind a shaped answer \u2014 read it for hedges and conditions ("yes, IF tests pass") before acting. If your ask declared \`points\`, the reply carries \`covered\` \u2014 the points actually addressed. Compare against what you declared: a missing point is STILL unanswered \u2014 re-ask it (contact on the same parentId) or proceed knowingly partial; never treat a partial answer as complete.`;
15767
15348
  var ContextSchema2 = external_exports.object({
15768
15349
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
15769
15350
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -15989,38 +15570,11 @@ var NotifyRequestSchema2 = NotifyRequestFields2.superRefine((r, ctx) => {
15989
15570
  const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
15990
15571
  if (needsOptions && !r.options?.length)
15991
15572
  ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
15992
- if (!needsOptions && r.options?.length)
15993
- ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
15994
- });
15995
- function unitsOf2(text) {
15996
- const spans = [];
15997
- const re = /\n\s*\n+/g;
15998
- let cursor = 0;
15999
- const push = (from, to) => {
16000
- const slice = text.slice(from, to);
16001
- const lead = slice.length - slice.trimStart().length;
16002
- const tail = slice.length - slice.trimEnd().length;
16003
- if (from + lead < to - tail) spans.push({ start: from + lead, end: to - tail });
16004
- };
16005
- for (let m = re.exec(text); m; m = re.exec(text)) {
16006
- push(cursor, m.index);
16007
- cursor = m.index + m[0].length;
16008
- }
16009
- push(cursor, text.length);
16010
- return spans;
16011
- }
15573
+ if (!needsOptions && r.options?.length)
15574
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
15575
+ });
16012
15576
  var NotifyStatusSchema2 = external_exports.enum(["pending", "answered", "ignored"]);
16013
15577
  var AgentStateSchema2 = external_exports.enum(["idle", "in_progress", "completed", "needs_input"]);
16014
- var SetTaskStateSchema2 = external_exports.object({
16015
- state: external_exports.enum(["in_progress", "completed", "needs_input"])
16016
- });
16017
- var SetWorkStateSchema2 = external_exports.object({
16018
- workId: external_exports.string().uuid().optional(),
16019
- notificationId: external_exports.string().optional(),
16020
- state: SetTaskStateSchema2.shape.state
16021
- }).refine((value) => Number(Boolean(value.workId)) + Number(Boolean(value.notificationId)) === 1, {
16022
- message: "exactly one of workId or notificationId is required"
16023
- });
16024
15578
  var TurnSchema2 = external_exports.object({
16025
15579
  prompt: external_exports.string(),
16026
15580
  reply: external_exports.string()
@@ -16150,108 +15704,6 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
16150
15704
  })
16151
15705
  ]);
16152
15706
  var CallbackTriggerSchema2 = external_exports.enum(["on_done", "on_blocked", "scheduled"]);
16153
- var ScheduleCallbackSchema2 = external_exports.object({
16154
- parentId: external_exports.string().describe("The thread to call back on (from a prior contact / reply / request)."),
16155
- goalId: external_exports.string().uuid().optional().describe("The Goal this callback advances; preferred for Goal-owned work."),
16156
- trigger: CallbackTriggerSchema2,
16157
- dueInSeconds: external_exports.number().int().positive().optional().describe("For 'scheduled' only: how many seconds from now to fire."),
16158
- note: external_exports.string().optional().describe("What to tell the user when you follow up.")
16159
- });
16160
- var PendingRepliesSchema2 = external_exports.object({
16161
- replies: external_exports.array(
16162
- external_exports.object({
16163
- parentId: external_exports.string(),
16164
- notificationId: external_exports.string(),
16165
- workId: external_exports.string().uuid().optional(),
16166
- decisionId: external_exports.string().uuid().optional(),
16167
- answer: UserAnswerSchema2,
16168
- /** E2EE: the sealed answer (opaque envelope + plaintext `ignored` hint) when the
16169
- * pairing is E2EE — the agent opens it and re-derives the real answer. Absent =
16170
- * plaintext answer (in `answer`). See AwaitItemSchema's reply variant. */
16171
- sealed: external_exports.lazy(() => SealedAnswerSchema2).optional(),
16172
- /** The call record rendered for THIS agent: the raw words the shaped answer was
16173
- * mapped from, filtered to its own claims. See AwaitItemSchema's reply variant. */
16174
- transcript: external_exports.string().optional(),
16175
- /** Coverage report (#396): which declared `points` this answer addressed. */
16176
- covered: external_exports.array(external_exports.string()).optional()
16177
- })
16178
- ),
16179
- pending: external_exports.array(
16180
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), createdAt: external_exports.string() })
16181
- ),
16182
- /** WHO YOU ARE on this account (field report 2026-08-28): the name and device the user
16183
- * sees for this session's identity. From inside a session there was no way to find out —
16184
- * `pair` with no arguments can HATCH a fresh identity, so it is not a safe probe — and an
16185
- * agent that cannot tell which agent it is cannot tell whether work addressed to
16186
- * "Reta" was addressed to it. Absent only for a token with no pairing behind it. */
16187
- you: external_exports.object({ name: external_exports.string(), device: external_exports.string().nullable(), tokenId: external_exports.string() }).optional(),
16188
- /** User-initiated requests addressed to this agent; act on them and reply via
16189
- * contact on the same parentId. Keeps reappearing until you call
16190
- * set_work_state on its workId (or notificationId once while bootstrapping). */
16191
- requests: external_exports.array(
16192
- external_exports.object({
16193
- parentId: external_exports.string(),
16194
- notificationId: external_exports.string(),
16195
- workId: external_exports.string().uuid().optional(),
16196
- text: external_exports.string(),
16197
- createdAt: external_exports.string(),
16198
- /** The user seeded this request with a past conversation — call get_thread on it
16199
- * FIRST and treat the transcript as prior context (#57/#251). */
16200
- contextParentId: external_exports.string().optional(),
16201
- /** STRANDED (field report 2026-08-28): this request was addressed to ANOTHER agent on
16202
- * the account — the name here — which has not been seen since it landed, so nobody
16203
- * came for it. Handed to you because you are the session that is here. Take it like
16204
- * any request (set_work_state claims it, reply with contact on its parentId), and say
16205
- * whose it was, because the user chose that agent on purpose. */
16206
- stranded: external_exports.string().optional()
16207
- })
16208
- ),
16209
- /** Durable outcomes currently owned by this agent. This is the Work-native queue; flat
16210
- * notification lists remain during migration so old clients keep their existing view. */
16211
- work: external_exports.array(external_exports.object({
16212
- workId: external_exports.string().uuid(),
16213
- parentId: external_exports.string().optional(),
16214
- objective: external_exports.string().nullable(),
16215
- state: external_exports.enum(["active", "blocked", "waiting_external"]),
16216
- blockedOn: external_exports.array(external_exports.string().uuid()),
16217
- updatedAt: external_exports.string()
16218
- })).optional(),
16219
- /** Callbacks you owe the user that are now DUE (you said you'd follow up when done,
16220
- * if blocked, or at a time that has passed). Re-surfaced every sweep until you
16221
- * fulfill one by calling contact on its parentId. */
16222
- /** Ride-alongs (RideAlongSchema): notes assigned to this agent that no wake could
16223
- * reach. Same array the contact/await replies carry — one queue, every carrier. */
16224
- also: external_exports.array(RideAlongSchema2).optional(),
16225
- owedCallbacks: external_exports.array(
16226
- external_exports.object({ parentId: external_exports.string(), trigger: CallbackTriggerSchema2, note: external_exports.string() })
16227
- ),
16228
- /** Work (either direction) you reported in_progress a while ago and never reported
16229
- * completed — likely left half-done by this session or a prior one that crashed or
16230
- * went idle. Report a real state (set_work_state) or continue the work. */
16231
- stalled: external_exports.array(
16232
- external_exports.object({ parentId: external_exports.string(), notificationId: external_exports.string(), title: external_exports.string().nullable(), startedAt: external_exports.string() })
16233
- ),
16234
- /** The queue rail (#614, pending/design.md): the same replies + requests, grouped by
16235
- * thread and ordered oldest-thread-first, so you work ONE thread at a time — fold all of
16236
- * a thread's `items` into a single turn rather than interleaving threads. `busy` = the
16237
- * thread already has a turn in progress (younger than the stall cutoff); let it finish and
16238
- * ride the next turn. `items` are that thread's replies/requests in arrival order; the
16239
- * full payload for each is in the flat `replies`/`requests` arrays (matched by
16240
- * notificationId). Derived, never stored — a crashed agent recomputes it exactly. */
16241
- threads: external_exports.array(
16242
- external_exports.object({
16243
- parentId: external_exports.string(),
16244
- busy: external_exports.boolean(),
16245
- items: external_exports.array(
16246
- external_exports.object({
16247
- kind: external_exports.enum(["reply", "request"]),
16248
- notificationId: external_exports.string(),
16249
- at: external_exports.string()
16250
- })
16251
- )
16252
- })
16253
- )
16254
- });
16255
15707
  var NotifyResponseSchema2 = external_exports.object({
16256
15708
  notificationId: external_exports.string(),
16257
15709
  workId: external_exports.string().uuid().optional(),
@@ -16314,6 +15766,13 @@ var UserResponseSchema2 = external_exports.object({
16314
15766
  });
16315
15767
  var VoiceKeySchema2 = external_exports.enum(["rachel", "george", "jessica", "brian", "lily"]);
16316
15768
  var AgendaTurnSchema2 = external_exports.object({
15769
+ /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
15770
+ * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
15771
+ * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
15772
+ * completion of a streamed re-plan carries the published sentence again, and the walk
15773
+ * drops what it already said by identity, never by the API's guess of what was polled.
15774
+ * Absent on plans nothing streams (a ring plan, a floor). */
15775
+ id: external_exports.string().optional(),
16317
15776
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
16318
15777
  * the planner declares duplicates instead of asking them twice. */
16319
15778
  coveredIds: external_exports.array(external_exports.string()).optional(),
@@ -16472,7 +15931,21 @@ var InboxItemSchema2 = external_exports.object({
16472
15931
  /** E2EE: the agent's device X25519 public key to seal the user's answer BACK to (the
16473
15932
  * sender the phone replies to). Sourced server-side from this item's pairing credential.
16474
15933
  * Present only alongside `envelope`; the phone seals via sealAnswer(answer, this, id). */
16475
- agentX25519: external_exports.string().optional()
15934
+ agentX25519: external_exports.string().optional(),
15935
+ /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
15936
+ * that Delivery's kind, the request Entry, the Goals it answers for, the exact DecisionNeed (none
15937
+ * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
15938
+ * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
15939
+ * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
15940
+ communication: external_exports.object({
15941
+ deliveryId: external_exports.string(),
15942
+ kind: external_exports.enum(["notification", "call"]),
15943
+ entryId: external_exports.string(),
15944
+ goalIds: external_exports.array(external_exports.string()),
15945
+ decisionNeedId: external_exports.string().optional(),
15946
+ sealed: external_exports.boolean(),
15947
+ goalState: external_exports.string().optional()
15948
+ }).optional()
16476
15949
  });
16477
15950
  var SnoozeRequestSchema2 = external_exports.object({
16478
15951
  requestId: external_exports.string(),
@@ -16708,26 +16181,54 @@ var CreateRequestSchema2 = external_exports.object({
16708
16181
  * the agent reads it via get_thread. Must belong to the requesting user. */
16709
16182
  contextParentId: external_exports.string().optional()
16710
16183
  });
16711
- var HandoffSchema2 = external_exports.object({
16712
- /** Move this existing outcome to `target` without reminting it. Requires `target`. */
16713
- workId: external_exports.string().uuid().optional(),
16714
- /** Land the note on an existing thread; omitted mints a fresh one. */
16715
- parentId: external_exports.string().uuid().optional(),
16716
- /** One-line headline of the working context handed off. */
16717
- title: external_exports.string().min(1),
16718
- /** The brief — standalone notes the successor reads (what was done, what's left, links). */
16719
- notes: external_exports.array(external_exports.string().min(1)).min(1),
16720
- /** A sibling connection to dispatch directly to (token id or agent nickname). Same-account
16721
- * only; omit to leave the thread for the user to hand off in the app. */
16722
- target: external_exports.string().optional(),
16723
- /** Write the note as a RECAP (kind:'recap', #617): a summary turn that supersedes the
16724
- * thread's earlier turns for rehydration — get_thread returns the latest recap + only
16725
- * the turns after it. Handoff-to-a-successor and handoff-to-yourself-later are the
16726
- * same primitive; a recap is one whose audience includes you. */
16727
- recap: external_exports.boolean().optional()
16728
- }).refine((value) => !value.workId || Boolean(value.target), {
16729
- message: "target is required when handing off Work",
16730
- path: ["target"]
16184
+ var QueueQuestionSchema2 = external_exports.object({
16185
+ /** The decision need's id what an answer is accepted against. */
16186
+ id: external_exports.string(),
16187
+ /** The words that were asked, from the request Entry that asked them. */
16188
+ question: external_exports.string(),
16189
+ /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
16190
+ * for a need whose request Entry is carried by no interactive Delivery, which nothing
16191
+ * can answer. */
16192
+ deliveryId: external_exports.string().nullable().default(null),
16193
+ /** The Entry the ruling is about. */
16194
+ aboutId: external_exports.string().nullable().default(null),
16195
+ /** Empty for a free-text question. */
16196
+ options: external_exports.array(OptionSchema2).default([]),
16197
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
16198
+ askedAt: external_exports.string(),
16199
+ /** Null while the question is open which is how the page tells the two apart. */
16200
+ answeredAt: external_exports.string().nullable().default(null),
16201
+ /** The ruling in the person's own words, from the contribution that replied — not the
16202
+ * option id, which is not something anyone reads back. Null while it is open, and null
16203
+ * for a settled question whose reply carried nothing readable. */
16204
+ answer: external_exports.string().nullable().default(null)
16205
+ });
16206
+ var QueueItemSchema2 = external_exports.object({
16207
+ id: external_exports.string(),
16208
+ /** One-line headline — the first sentence of the outcome. */
16209
+ title: external_exports.string(),
16210
+ /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
16211
+ intent: external_exports.string(),
16212
+ /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
16213
+ state: external_exports.string(),
16214
+ /** Who holds it (a participant ref); null when nobody does yet. */
16215
+ assignee: external_exports.string().nullable().default(null),
16216
+ /** What the agent last said it was doing; null if it has said nothing. */
16217
+ progress: external_exports.string().nullable().default(null),
16218
+ reviewPending: external_exports.boolean().default(false),
16219
+ dueAt: external_exports.string().nullable().default(null),
16220
+ /** The Goal this one was opened under; null at the root. */
16221
+ parentGoalId: external_exports.string().nullable().default(null),
16222
+ /** Goals opened under this one — only those the same list holds. */
16223
+ childGoalIds: external_exports.array(external_exports.string()).default([]),
16224
+ /** Goals this one waits on (start or finish gates). */
16225
+ dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
16226
+ /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
16227
+ blocked: external_exports.boolean().default(false),
16228
+ /** Every decision need on it, open or settled — the page decides which to show. */
16229
+ questions: external_exports.array(QueueQuestionSchema2).default([]),
16230
+ createdAt: external_exports.string(),
16231
+ updatedAt: external_exports.string().nullable().default(null)
16731
16232
  });
16732
16233
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
16733
16234
  var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
@@ -16864,15 +16365,29 @@ var OAuthStartSchema2 = external_exports.object({
16864
16365
  var DeliveryConfigSchema2 = external_exports.object({
16865
16366
  tokenId: external_exports.string(),
16866
16367
  mode: DeliveryModeSchema2,
16867
- /** null when the server has no SUPABASE_ANON_KEY set the listener then falls
16868
- * back to its own PAIGY_SUPABASE_URL / PAIGY_SUPABASE_ANON_KEY env. */
16368
+ /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
16369
+ * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
16370
+ * carries credentials; only a `poll` registration can come back with null here. */
16869
16371
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
16870
16372
  });
16871
16373
  var StatusSchema2 = external_exports.object({
16872
16374
  name: external_exports.string(),
16873
16375
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
16874
16376
  /** A phone is registered for push/ring (any push token on the account). */
16875
- phone: external_exports.boolean()
16377
+ phone: external_exports.boolean(),
16378
+ /** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
16379
+ * it never picked up. THE SAME NUMBER the harness's wake gate reads
16380
+ * (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
16381
+ * zero while the sweep sees one is two ideas of "waiting".
16382
+ *
16383
+ * Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
16384
+ * that nobody spawned, so a terminal session only learns of work by asking. The harness
16385
+ * used to paper over that by spawning a SECOND process on the identity; now it stands
16386
+ * back, correctly, and the person sitting at the terminal is the one who can act. A
16387
+ * coffee-beans request sat unread for three days.
16388
+ *
16389
+ * Optional: an older API sends no field, and the statusline then renders exactly as before. */
16390
+ waiting: external_exports.number().int().nonnegative().optional()
16876
16391
  });
16877
16392
  var EnvelopeRecipientSchema2 = external_exports.object({
16878
16393
  keyId: external_exports.string(),
@@ -17038,15 +16553,146 @@ var FeedbackOutcomeSchema2 = external_exports.object({
17038
16553
  message: external_exports.string(),
17039
16554
  childIds: external_exports.array(external_exports.string()).optional()
17040
16555
  });
17041
- var CONTACT_SCHEMA2 = contactSchemaFrom2({
17042
- ask: NotifyRequestFields2.shape.ask,
17043
- waiting: NotifyRequestFields2.shape.waiting,
17044
- options: NotifyRequestFields2.shape.options,
17045
- channel: NotifyRequestFields2.shape.channel,
17046
- parentId: NotifyRequestFields2.shape.parentId,
17047
- workId: NotifyRequestFields2.shape.workId,
17048
- goalId: NotifyRequestFields2.shape.goalId
17049
- });
16556
+
16557
+ // src/main.ts
16558
+ var import_qrcode = __toESM(require_lib(), 1);
16559
+ import { homedir as homedir7, hostname } from "os";
16560
+
16561
+ // src/harness/catalog.ts
16562
+ import { spawnSync } from "child_process";
16563
+ import { existsSync as existsSync2 } from "fs";
16564
+ import { homedir as homedir2 } from "os";
16565
+ import { delimiter, dirname, join as join2 } from "path";
16566
+ var CATALOG = [
16567
+ {
16568
+ name: "claude",
16569
+ label: "Claude Code",
16570
+ cli: "claude",
16571
+ adapter: "claude-agent-acp",
16572
+ authProbe: ["claude", "auth", "status"],
16573
+ loginHint: "Run `claude` once in a terminal and sign in.",
16574
+ installCli: "curl -fsSL https://claude.ai/install.sh | bash",
16575
+ installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
16576
+ },
16577
+ {
16578
+ name: "codex",
16579
+ label: "Codex",
16580
+ cli: "codex",
16581
+ adapter: "codex-acp",
16582
+ authProbe: ["codex", "login", "status"],
16583
+ loginHint: "Run `codex login` to authenticate.",
16584
+ installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
16585
+ installAdapter: "npm install -g @agentclientprotocol/codex-acp"
16586
+ },
16587
+ {
16588
+ name: "agy",
16589
+ label: "Antigravity",
16590
+ cli: "agy",
16591
+ authProbe: ["agy", "help"],
16592
+ loginHint: "Install and set up Antigravity (`agy`).",
16593
+ installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
16594
+ }
16595
+ ];
16596
+ function probeDirs(home = homedir2()) {
16597
+ return [
16598
+ dirname(process.execPath),
16599
+ join2(home, ".local", "bin"),
16600
+ "/usr/local/bin",
16601
+ "/opt/homebrew/bin",
16602
+ join2(home, ".npm-global", "bin")
16603
+ ];
16604
+ }
16605
+ function which(bin) {
16606
+ const hit = spawnSync("which", [bin], { encoding: "utf8" });
16607
+ const fromPath = hit.status === 0 ? hit.stdout.trim() : "";
16608
+ if (fromPath) return fromPath;
16609
+ for (const dir of probeDirs()) {
16610
+ const candidate = join2(dir, bin);
16611
+ if (existsSync2(candidate)) return candidate;
16612
+ }
16613
+ return null;
16614
+ }
16615
+ function runProbe(argv) {
16616
+ const [cmd, ...args] = argv;
16617
+ if (!cmd) return false;
16618
+ const resolved = which(cmd);
16619
+ if (!resolved) return false;
16620
+ return spawnSync(resolved, args, {
16621
+ timeout: 1e4,
16622
+ stdio: "ignore",
16623
+ env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) }
16624
+ }).status === 0;
16625
+ }
16626
+ function detect(entry, deps = {}) {
16627
+ const find = deps.which ?? which;
16628
+ const probe = deps.probe ?? runProbe;
16629
+ const base = { name: entry.name, label: entry.label };
16630
+ const cliPath = find(entry.cli);
16631
+ if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
16632
+ const adapterPath = entry.adapter ? find(entry.adapter) : null;
16633
+ if (entry.adapter && !adapterPath) {
16634
+ return {
16635
+ ...base,
16636
+ status: "adapter-missing",
16637
+ hint: `${entry.label} is installed, but its ACP adapter (${entry.adapter}) is not.`,
16638
+ ...entry.installAdapter ? { install: entry.installAdapter } : {}
16639
+ };
16640
+ }
16641
+ const bin = adapterPath ?? cliPath;
16642
+ if (!probe(entry.authProbe)) return { ...base, status: "login", bin, hint: entry.loginHint };
16643
+ return { ...base, status: "ready", bin };
16644
+ }
16645
+ function detectAll(deps = {}) {
16646
+ return CATALOG.map((entry) => detect(entry, deps));
16647
+ }
16648
+
16649
+ // src/host.ts
16650
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
16651
+ import { homedir as homedir5 } from "os";
16652
+ import { join as join4 } from "path";
16653
+
16654
+ // src/workspaces.ts
16655
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
16656
+ import { dirname as dirname2, join as join3, resolve } from "path";
16657
+ import { homedir as homedir3 } from "os";
16658
+ function workspacesFile() {
16659
+ return join3(homedir3(), ".paigy", "workspaces.json");
16660
+ }
16661
+ function listWorkspaces(deps) {
16662
+ try {
16663
+ const parsed = JSON.parse(readFileSync2(deps.file, "utf8"));
16664
+ return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
16665
+ } catch {
16666
+ return [];
16667
+ }
16668
+ }
16669
+ function addWorkspace(dir, deps) {
16670
+ const next = resolve(dir.replace(/^~(?=$|\/)/, homedir3()));
16671
+ const all = listWorkspaces(deps);
16672
+ if (!all.includes(next)) {
16673
+ all.push(next);
16674
+ mkdirSync2(dirname2(deps.file), { recursive: true });
16675
+ writeFileSync2(deps.file, JSON.stringify(all, null, 2));
16676
+ }
16677
+ return all;
16678
+ }
16679
+ function removeWorkspace(dir, deps) {
16680
+ const all = listWorkspaces(deps).filter((w) => w !== dir);
16681
+ mkdirSync2(dirname2(deps.file), { recursive: true });
16682
+ writeFileSync2(deps.file, JSON.stringify(all, null, 2));
16683
+ return all;
16684
+ }
16685
+ function allowed(cwd, deps) {
16686
+ const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir3()));
16687
+ return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
16688
+ }
16689
+ function resolveWakeDir(pinned, deps) {
16690
+ if (pinned && allowed(pinned, deps)) {
16691
+ const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir3()));
16692
+ if ((deps.exists ?? existsSync3)(dir)) return dir;
16693
+ }
16694
+ return listWorkspaces(deps)[0];
16695
+ }
17050
16696
 
17051
16697
  // src/paigy/activity.ts
17052
16698
  function shortenPaths(s) {
@@ -17060,9 +16706,9 @@ function workLine(event) {
17060
16706
  const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
17061
16707
  return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
17062
16708
  }
17063
- function pushWork(lines, line) {
17064
- if (!line || lines[lines.length - 1] === line) return [...lines];
17065
- return [...lines, line].slice(-ACTIVITY_LINES2);
16709
+ function pushWork(lines2, line) {
16710
+ if (!line || lines2[lines2.length - 1] === line) return [...lines2];
16711
+ return [...lines2, line].slice(-ACTIVITY_LINES2);
17066
16712
  }
17067
16713
  function sameTail(a, b) {
17068
16714
  return a.length === b.length && a.every((l, i) => l === b[i]);
@@ -17081,17 +16727,15 @@ function optionFor(options, decision) {
17081
16727
  return options.find((o) => o.kind === want)?.optionId ?? null;
17082
16728
  }
17083
16729
  function createAcpDriver(opts) {
17084
- return new AcpDriver(opts.cwd, opts.mode, opts.mcp ?? []);
16730
+ return new AcpDriver(opts.cwd, opts.mode);
17085
16731
  }
17086
16732
  var AcpDriver = class {
17087
- constructor(cwd, mode, mcp = []) {
16733
+ constructor(cwd, mode) {
17088
16734
  this.cwd = cwd;
17089
16735
  this.mode = mode;
17090
- this.mcp = mcp;
17091
16736
  }
17092
16737
  cwd;
17093
16738
  mode;
17094
- mcp;
17095
16739
  nextId = 1;
17096
16740
  initId;
17097
16741
  sessionNewId;
@@ -17115,7 +16759,7 @@ var AcpDriver = class {
17115
16759
  open() {
17116
16760
  this.initId = this.nextId++;
17117
16761
  return [
17118
- frame({
16762
+ frame2({
17119
16763
  id: this.initId,
17120
16764
  method: "initialize",
17121
16765
  params: {
@@ -17139,13 +16783,13 @@ var AcpDriver = class {
17139
16783
  this.pending.delete(id);
17140
16784
  const optionId = optionFor(request.options, decision);
17141
16785
  return [
17142
- optionId ? frame({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
16786
+ optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
17143
16787
  ];
17144
16788
  }
17145
16789
  /** Cancel anything still blocked — called on stop so the process can exit cleanly. */
17146
16790
  close() {
17147
16791
  const writes = [...this.pending.values()].map(
17148
- (r) => frame({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
16792
+ (r) => frame2({ id: r.id, result: { outcome: { outcome: "cancelled" } } })
17149
16793
  );
17150
16794
  this.pending.clear();
17151
16795
  return writes;
@@ -17175,7 +16819,7 @@ var AcpDriver = class {
17175
16819
  this.sessionNewId = this.nextId++;
17176
16820
  return {
17177
16821
  events: [],
17178
- writes: [frame({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: this.mcp } })]
16822
+ writes: [frame2({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
17179
16823
  };
17180
16824
  }
17181
16825
  if (msg.id === this.sessionNewId) {
@@ -17190,12 +16834,12 @@ var AcpDriver = class {
17190
16834
  this.sessionId = sessionId2;
17191
16835
  const value = this.mode === "bypass" ? "bypassPermissions" : "default";
17192
16836
  const writes = [
17193
- frame({
16837
+ frame2({
17194
16838
  id: this.nextId++,
17195
16839
  method: "session/set_config_option",
17196
16840
  params: { sessionId: sessionId2, configId: "mode", value }
17197
16841
  }),
17198
- frame({ id: this.nextId++, method: "session/set_mode", params: { sessionId: sessionId2, modeId: value } })
16842
+ frame2({ id: this.nextId++, method: "session/set_mode", params: { sessionId: sessionId2, modeId: value } })
17199
16843
  ];
17200
16844
  writes.push(...this.flush());
17201
16845
  return { events: [], writes };
@@ -17252,7 +16896,7 @@ var AcpDriver = class {
17252
16896
  if (msg.method !== "session/request_permission") {
17253
16897
  return {
17254
16898
  events: [],
17255
- writes: [frame({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
16899
+ writes: [frame2({ id: msg.id, error: { code: -32601, message: `Method not found: ${msg.method}` } })]
17256
16900
  };
17257
16901
  }
17258
16902
  const id = msg.id;
@@ -17266,7 +16910,7 @@ var AcpDriver = class {
17266
16910
  // means "don't stall the agent", never "don't tell the user".
17267
16911
  events: [{ kind: "turn", role: "agent", text: `auto-approved: ${title}`, tools: [tool] }],
17268
16912
  writes: [
17269
- optionId ? frame({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame({ id, result: { outcome: { outcome: "cancelled" } } })
16913
+ optionId ? frame2({ id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id, result: { outcome: { outcome: "cancelled" } } })
17270
16914
  ]
17271
16915
  };
17272
16916
  }
@@ -17283,7 +16927,7 @@ var AcpDriver = class {
17283
16927
  this.queued = [];
17284
16928
  this.promptId = this.nextId++;
17285
16929
  return [
17286
- frame({
16930
+ frame2({
17287
16931
  id: this.promptId,
17288
16932
  method: "session/prompt",
17289
16933
  params: { sessionId: this.sessionId, prompt: blocks }
@@ -17291,7 +16935,7 @@ var AcpDriver = class {
17291
16935
  ];
17292
16936
  }
17293
16937
  };
17294
- var frame = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
16938
+ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
17295
16939
 
17296
16940
  // src/harness/session.ts
17297
16941
  var ADAPTER_BIN = {
@@ -17339,12 +16983,12 @@ function startSession(opts) {
17339
16983
  opts.onEvent({ kind: "error", message: e.message });
17340
16984
  opts.onExit?.();
17341
16985
  });
17342
- const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.mcp ? { mcp: opts.mcp } : {} });
16986
+ const driver = createAcpDriver({ cwd, mode: opts.mode });
17343
16987
  let buffer = "";
17344
16988
  child.stdout?.on("data", (chunk) => {
17345
- const { lines, rest } = splitLines(buffer, String(chunk));
16989
+ const { lines: lines2, rest } = splitLines(buffer, String(chunk));
17346
16990
  buffer = rest;
17347
- for (const line of lines) {
16991
+ for (const line of lines2) {
17348
16992
  const { events, writes } = driver.handleLine(line);
17349
16993
  write(writes);
17350
16994
  for (const event of events) opts.onEvent(event);
@@ -17378,57 +17022,60 @@ function endsWithQuestion(text) {
17378
17022
  }
17379
17023
  var POLL_MS = 5e3;
17380
17024
  var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
17381
- async function drainInput(state, deps = {}) {
17382
- if (!state.parentId && !state.exclusive) return [];
17383
- let work;
17384
- try {
17385
- work = await (deps.check ?? checkReplies)();
17386
- } catch {
17387
- return [];
17025
+ var ended = (state) => state === "done" || state === "cancelled";
17026
+ function unheard(brief, state) {
17027
+ const seen = state.seen ??= /* @__PURE__ */ new Set();
17028
+ const words2 = [];
17029
+ for (const e of brief.entries ?? []) {
17030
+ if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
17031
+ seen.add(e.entryId);
17032
+ if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
17033
+ const text = entryWords(e).trim();
17034
+ if (text) words2.push(text);
17035
+ }
17036
+ return words2;
17037
+ }
17038
+ async function step(session, state, rail) {
17039
+ if (!state.goal) {
17040
+ if (!state.resting) return;
17041
+ const brief2 = await rail.claim();
17042
+ if (!brief2.goalId || brief2.revision === void 0) return;
17043
+ unheard(brief2, state);
17044
+ if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
17045
+ else state.goal = { id: brief2.goalId, revision: brief2.revision };
17046
+ send(session, state, brief2.message);
17047
+ return;
17388
17048
  }
17389
- const ours = (parentId) => state.exclusive || parentId === state.parentId;
17390
- const opening = !!state.exclusive && !state.parentId;
17391
- const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
17392
- const mine = [
17393
- ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
17394
- ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
17395
- ];
17396
- const claimed = [];
17397
- for (const item of mine) {
17398
- try {
17399
- const ack = await (deps.ack ?? setTaskState)(item.notificationId, "in_progress");
17400
- if (ack?.won === false) continue;
17401
- claimed.push(item);
17402
- } catch {
17403
- }
17049
+ const brief = await rail.read(state.goal.id);
17050
+ state.goal.revision = brief.revision ?? state.goal.revision;
17051
+ for (const answer of brief.answers ?? []) {
17052
+ const resolve3 = state.asks?.get(answer.requestEntryId);
17053
+ if (!resolve3) continue;
17054
+ state.asks.delete(answer.requestEntryId);
17055
+ resolve3(answer.result);
17056
+ }
17057
+ if (ended(brief.state)) {
17058
+ state.goal = void 0;
17059
+ for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
17060
+ state.asks?.clear();
17061
+ return;
17404
17062
  }
17405
- return claimed;
17063
+ if (state.asks?.size || !state.resting) return;
17064
+ const words2 = unheard(brief, state);
17065
+ if (words2.length) send(session, state, words2.join("\n\n"));
17406
17066
  }
17407
- async function nextTurnFrom(event, state, deps = {}) {
17408
- if (!endsWithQuestion(event.result)) return null;
17409
- const answer = await askQuestion(event.result ?? "", state, deps);
17410
- return answer?.trim() ? answer : null;
17067
+ function send(session, state, text) {
17068
+ state.resting = false;
17069
+ session.send(text);
17411
17070
  }
17412
- async function pump(session, state, deps = {}) {
17071
+ async function pump(session, state, rail, opts) {
17413
17072
  for (; ; ) {
17414
- for (const item of await drainInput(state, deps)) {
17415
- state.parentId ??= item.parentId;
17416
- const key = state.asks?.has(item.notificationId) ? item.notificationId : state.asks?.keys().next().value;
17417
- const resolve3 = key !== void 0 ? state.asks?.get(key) : void 0;
17418
- if (key !== void 0 && resolve3) {
17419
- state.asks?.delete(key);
17420
- resolve3(item.answer);
17421
- continue;
17422
- }
17423
- const words2 = spokenText(item.answer);
17424
- if (words2) {
17425
- state.resting = false;
17426
- session.send(words2);
17427
- }
17073
+ try {
17074
+ await step(session, state, rail);
17075
+ } catch {
17428
17076
  }
17429
- const keepGoing = await deps.onIdle?.() ?? false;
17430
- if (!keepGoing) return;
17431
- await (deps.pause ?? wait)();
17077
+ if (!await opts.onIdle()) return;
17078
+ await (opts.pause ?? wait)();
17432
17079
  }
17433
17080
  }
17434
17081
 
@@ -17450,195 +17097,107 @@ var DESTRUCTIVE = [
17450
17097
  function isDestructive(summary) {
17451
17098
  return DESTRUCTIVE.some((re) => re.test(summary));
17452
17099
  }
17453
- function levelFor(event) {
17454
- switch (event.kind) {
17455
- case "turn":
17456
- return "inbox";
17457
- case "permission":
17458
- return isDestructive(event.summary) ? "call" : "banner";
17459
- case "idle":
17460
- if (endsWithQuestion(event.result)) return "banner";
17461
- return event.failed ? "push" : "inbox";
17462
- case "work":
17463
- return "inbox";
17464
- // moot — entryFor drops work before a level ever matters
17465
- case "error":
17466
- return "inbox";
17467
- }
17468
- }
17469
17100
 
17470
17101
  // src/paigy/bridge.ts
17102
+ function railFor(opts) {
17103
+ return {
17104
+ claim: () => claimGoal(void 0, opts),
17105
+ read: (goalId) => getGoal(goalId, opts),
17106
+ update: (goalId, input) => updateGoal(goalId, input, opts),
17107
+ // One read and back: the pump owns the waiting, so a Call must not hold a window here.
17108
+ contact: (input) => contact(input, { ...opts, waits: false })
17109
+ };
17110
+ }
17471
17111
  function cancelAsks(state) {
17472
- for (const resolve3 of state.asks?.values() ?? []) resolve3({ kind: "ignored" });
17112
+ for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
17473
17113
  state.asks?.clear();
17474
17114
  }
17475
- function entryFor(event, state = {}) {
17476
- const common = {
17477
- ...state.parentId ? { parentId: state.parentId } : {},
17478
- ...state.repo ? { repo: state.repo } : {},
17479
- ...state.branch ? { branch: state.branch } : {},
17480
- urgency: levelFor(event)
17481
- };
17482
- switch (event.kind) {
17483
- case "turn": {
17484
- const who = event.role === "agent" ? "Agent" : "You";
17485
- const body = event.text ? event.role === "agent" ? event.text : `${who}: ${event.text}` : `${who} ran ${(event.tools ?? []).join(", ")}.`;
17486
- return {
17487
- ...common,
17488
- ask: body
17489
- // No `select`: the contract refuses it on the `ask` form ("the broker derives it"),
17490
- // and with no options it derives "text" anyway — the shape this wants, since any
17491
- // inbox row can be replied to and a reply to history is just the user initiating
17492
- // (the pump feeds it back in).
17493
- };
17494
- }
17495
- case "permission":
17496
- return {
17497
- ...common,
17498
- context: {
17499
- title: clip(event.summary),
17500
- description: [
17501
- `The agent is blocked waiting to run: ${event.summary}`,
17502
- ...event.reason ? [event.reason] : []
17503
- ]
17504
- },
17505
- select: "confirm",
17506
- confirmStyle: "approve",
17507
- // Literally true, and the inbox uses it: a blocking item gets the badge and the
17508
- // extra confirm-before-dismiss, which is exactly right for something holding a
17509
- // real process still.
17510
- blocking: true
17511
- };
17512
- case "idle": {
17513
- const asking = endsWithQuestion(event.result);
17514
- const result = event.result?.trim();
17515
- const norm = (t) => (t ?? "").replace(/\s+/g, " ").trim();
17516
- if (result && !event.failed && norm(result) === norm(state.lastAgentText)) {
17517
- if (!asking) return null;
17518
- const spans = unitsOf2(result);
17519
- const tail = spans.map((sp) => result.slice(sp.start, sp.end)).reverse().find((t) => t.includes("?"));
17520
- return { ...common, ask: tail ?? result, blocking: true };
17521
- }
17522
- const lead = asking ? "" : event.failed ? "The agent stopped without finishing. " : "Turn complete. ";
17523
- return {
17524
- ...common,
17525
- ask: `${lead}${result || (event.failed ? "No result reported." : "Done.")}`,
17526
- ...asking ? { blocking: true } : {}
17527
- };
17115
+ var ALLOW = "Allow";
17116
+ function decisionFrom(result) {
17117
+ if (result && "kind" in result) {
17118
+ if (result.kind === "one") return result.option.label === ALLOW ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
17119
+ if (result.kind === "confirm") return result.approved ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
17120
+ if (result.kind === "text") {
17121
+ if (/^(yes|y|ok|okay|approve[d]?|allow|go ahead|do it)\b/.test(result.text.trim().toLowerCase())) return { allow: true };
17122
+ return { allow: false, reason: result.text };
17528
17123
  }
17529
- case "work":
17530
- return null;
17531
- // log-only by contract — the live texture of the working log
17532
- case "error":
17533
- return null;
17534
- }
17535
- }
17536
- function decisionFrom(answer) {
17537
- if (answer.kind === "confirm") {
17538
- return answer.approved ? { allow: true } : { allow: false, reason: "Denied from Paigy" };
17539
- }
17540
- if (answer.kind === "text") {
17541
- const said = answer.text.trim().toLowerCase();
17542
- if (/^(yes|y|ok|okay|approve[d]?|allow|go ahead|do it)\b/.test(said)) return { allow: true };
17543
- return { allow: false, reason: answer.text };
17544
- }
17545
- if (answer.kind === "turns") {
17546
- const first = answer.turns[0]?.reply ?? "";
17547
- return decisionFrom({ kind: "text", text: first });
17548
17124
  }
17549
17125
  return { allow: false, reason: "No approval given" };
17550
17126
  }
17551
- async function mirror(event, state, deps = {}) {
17552
- const entry = entryFor(event, state);
17553
- if (event.kind === "turn" && event.role === "agent" && event.text) state.lastAgentText = event.text;
17554
- if (!entry) return {};
17555
- let req;
17556
- try {
17557
- req = NotifyRequestSchema2.parse(entry);
17558
- } catch (e) {
17559
- console.error(`paigy: mirror entry failed the contract \u2014 a bridge bug, not the network: ${e instanceof Error ? e.message.slice(0, 300) : String(e)}`);
17560
- return {};
17127
+ function wordsOf(result) {
17128
+ if (!result || !("kind" in result)) return null;
17129
+ switch (result.kind) {
17130
+ case "text":
17131
+ return result.text.trim() || null;
17132
+ case "confirm":
17133
+ return result.approved ? "yes" : "no";
17134
+ case "one":
17135
+ return result.option.label;
17136
+ case "many":
17137
+ case "rank":
17138
+ return result.options.map((o) => o.label).join(", ");
17561
17139
  }
17140
+ }
17141
+ async function ask(state, rail, input) {
17142
+ if (!state.goal) return null;
17562
17143
  try {
17563
- const { notificationId, parentId } = await (deps.submit ?? submitNotification)(req);
17564
- (state.mine ??= /* @__PURE__ */ new Set()).add(notificationId);
17565
- return { parentId, notificationId };
17144
+ const sent = await rail.contact({
17145
+ goalIds: [state.goal.id],
17146
+ ask: input.ask,
17147
+ waiting: "hard",
17148
+ channel: input.call ? "call" : "notification",
17149
+ ...input.options ? { options: input.options } : {}
17150
+ });
17151
+ const entryId = sent.requestEntryIds[0];
17152
+ if (!entryId) return null;
17153
+ (state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
17154
+ return await new Promise((resolve3) => (state.asks ??= /* @__PURE__ */ new Map()).set(entryId, resolve3));
17566
17155
  } catch {
17567
- return {};
17156
+ return null;
17568
17157
  }
17569
17158
  }
17570
- function awaitAnswer(state, notificationId) {
17571
- state.asks ??= /* @__PURE__ */ new Map();
17572
- return new Promise((resolve3) => state.asks.set(notificationId, resolve3));
17159
+ async function askPermission(event, state, rail) {
17160
+ return decisionFrom(await ask(state, rail, {
17161
+ ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
17162
+ options: [{ label: ALLOW }, { label: "Deny" }],
17163
+ call: isDestructive(event.summary)
17164
+ }));
17573
17165
  }
17574
- async function askPermission(event, state, deps = {}) {
17575
- const entry = entryFor(event, state);
17576
- if (!entry) return { decision: { allow: false, reason: "Could not ask" } };
17577
- try {
17578
- const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
17579
- (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
17580
- const answer = await awaitAnswer(state, sent.notificationId);
17581
- return { decision: decisionFrom(answer), parentId: sent.parentId };
17582
- } catch (e) {
17583
- return { decision: { allow: false, reason: `Could not reach Paigy: ${e.message}` } };
17584
- }
17166
+ async function askQuestion(question, state, rail) {
17167
+ return wordsOf(await ask(state, rail, { ask: question }));
17585
17168
  }
17586
- async function askQuestion(question, state, deps = {}) {
17587
- const entry = {
17588
- ...state.parentId ? { parentId: state.parentId } : {},
17589
- ...state.repo ? { repo: state.repo } : {},
17590
- ...state.branch ? { branch: state.branch } : {},
17591
- // Prose in — see `entryFor`'s `turn` case. An agent's question is the case most likely
17592
- // to run long, and its title was a clipped prefix of itself.
17593
- ask: question,
17594
- blocking: true,
17595
- urgency: "banner"
17596
- };
17169
+ async function finish(result, state, rail) {
17170
+ const goal = state.goal;
17171
+ if (!goal) return;
17172
+ state.goal = void 0;
17173
+ const report = result?.trim() || "Done.";
17597
17174
  try {
17598
- const sent = await (deps.submit ?? submitNotification)(NotifyRequestSchema2.parse(entry));
17599
- (state.mine ??= /* @__PURE__ */ new Set()).add(sent.notificationId);
17600
- return spokenText(await awaitAnswer(state, sent.notificationId));
17175
+ await rail.contact({ goalIds: [goal.id], ask: report, waiting: "none", channel: "notification" });
17176
+ const { revision } = await rail.read(goal.id);
17177
+ await rail.update(goal.id, { revision: revision ?? goal.revision, changes: { state: "done", progress: report.slice(0, 1e4) }, reason: "The agent finished its turn without asking anything." });
17601
17178
  } catch {
17602
- return null;
17603
17179
  }
17604
17180
  }
17605
- function spokenText(answer) {
17606
- switch (answer.kind) {
17607
- case "text":
17608
- return answer.text.trim() || null;
17609
- case "option":
17610
- return answer.label ?? answer.optionId;
17611
- case "multi":
17612
- case "ranked":
17613
- return (answer.labels ?? answer.optionIds).join(", ");
17614
- case "confirm":
17615
- return answer.approved ? "yes" : "no";
17616
- case "clarify":
17617
- return answer.chunks.join(" ");
17618
- // A call log: the user's replies are the instruction, the bot's prompts are not.
17619
- case "turns":
17620
- return answer.turns.map((t) => t.reply).join(" ").trim() || null;
17621
- // An auto-answer derived from the user's past decisions — reads like a fast human.
17622
- case "precedent":
17623
- return answer.answer;
17624
- case "ignored":
17625
- return null;
17181
+ async function afterTurn(event, state, rail) {
17182
+ if (!event.failed && !endsWithQuestion(event.result)) {
17183
+ await finish(event.result, state, rail);
17184
+ state.resting = true;
17185
+ return null;
17626
17186
  }
17187
+ const reply = await askQuestion(event.failed ? `The agent stopped without finishing.
17188
+
17189
+ ${event.result?.trim() || "No result reported."}` : event.result ?? "", state, rail);
17190
+ if (!reply) state.resting = true;
17191
+ return reply;
17627
17192
  }
17628
- var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text)";
17629
17193
 
17630
17194
  // src/run.ts
17631
17195
  function runHarness(opts) {
17632
17196
  let running = true;
17633
17197
  let tail = [];
17634
- const state = { ...opts.exclusive ? { exclusive: true } : {} };
17198
+ const state = { resting: true };
17635
17199
  let session = null;
17636
- const asMe = { token: opts.token };
17637
- const deps = {
17638
- submit: (req) => submitNotification(req, asMe),
17639
- check: () => checkReplies(asMe),
17640
- ack: (id, st) => setTaskState(id, st, asMe)
17641
- };
17200
+ const rail = railFor({ token: opts.token });
17642
17201
  async function handle(event) {
17643
17202
  if (event.kind === "error") {
17644
17203
  opts.log(`\u26A0 ${event.message}`);
@@ -17652,8 +17211,7 @@ function runHarness(opts) {
17652
17211
  return;
17653
17212
  }
17654
17213
  opts.log(`\u23F8 blocked: ${event.summary} \u2014 asking your phone`);
17655
- const { decision, parentId: parentId2 } = await askPermission(event, state, deps);
17656
- state.parentId ??= parentId2;
17214
+ const decision = await askPermission(event, state, rail);
17657
17215
  session?.respond(event.id, decision);
17658
17216
  opts.log(decision.allow ? `\u2713 approved: ${event.summary}` : `\u2717 denied: ${event.summary}`);
17659
17217
  return;
@@ -17663,41 +17221,39 @@ function runHarness(opts) {
17663
17221
  tail = pushWork(tail, workLine(event));
17664
17222
  return;
17665
17223
  }
17666
- const { parentId } = await mirror(event, state, deps);
17667
- state.parentId ??= parentId;
17668
17224
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
17669
17225
  if (event.kind === "idle") {
17670
- state.resting = true;
17671
17226
  tail = [];
17672
17227
  if (endsWithQuestion(event.result)) {
17673
17228
  const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
17674
17229
  if (local?.trim() && session && running) {
17675
17230
  opts.log(`you (here): ${local.split("\n")[0] ?? ""}`);
17231
+ state.resting = false;
17676
17232
  session.send(local);
17677
17233
  return;
17678
17234
  }
17679
17235
  }
17680
- const reply = await nextTurnFrom(event, state, deps);
17236
+ const reply = await afterTurn(event, state, rail);
17681
17237
  if (reply && session && running) {
17682
17238
  opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
17683
17239
  session.send(reply);
17684
17240
  }
17685
17241
  }
17686
17242
  }
17687
- const paigyMcp = opts.exclusive && opts.token ? [{ name: "paigy", command: "npx", args: ["-y", "@paigy/mcp@latest"], env: { PAIGY_TOKEN: opts.token } }] : void 0;
17688
- if (paigyMcp) state.sharedWithAgent = true;
17689
17243
  session = startSession({
17690
17244
  harness: opts.harness,
17691
17245
  cwd: opts.cwd,
17692
17246
  mode: opts.mode,
17693
- ...paigyMcp ? { mcp: paigyMcp } : {},
17694
17247
  ...opts.token ? { token: opts.token } : {},
17695
17248
  ...opts.bin ? { bin: opts.bin } : {},
17696
17249
  ...opts.onExit ? { onExit: opts.onExit } : {},
17697
17250
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
17698
17251
  });
17699
- if (opts.prompt) session.send(opts.prompt);
17700
- void pump(session, state, { ...deps, onIdle: () => running }).catch(
17252
+ if (opts.prompt) {
17253
+ state.resting = false;
17254
+ session.send(opts.prompt);
17255
+ }
17256
+ void pump(session, state, rail, { onIdle: () => running }).catch(
17701
17257
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
17702
17258
  );
17703
17259
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
@@ -17738,21 +17294,17 @@ function hostElsewhere() {
17738
17294
  const s = readHostState();
17739
17295
  return !!s && s.pid !== process.pid;
17740
17296
  }
17741
- var IDLE_WAKE_MS = 10 * 6e4;
17742
17297
  var runKey = (slot) => `slot:${slot}`;
17743
17298
  var claimedRun = (sessionId2) => {
17744
17299
  const slot = sessionSlot(sessionId2);
17745
17300
  return { slot, key: runKey(slot) };
17746
17301
  };
17747
17302
  async function slotHasWaitingWork(token) {
17748
- return (await pendingSummary({ token })).unacknowledged > 0;
17303
+ return (await checkReplies({ token })).deliveries.length > 0;
17749
17304
  }
17750
17305
  var PRESENCE_FRESH_MS = 3 * 6e4;
17751
- function wakeAction(live, seenAt, now, serverSeenAt) {
17752
- if (live) {
17753
- if (seenAt === void 0) return "skip";
17754
- return now - seenAt >= IDLE_WAKE_MS ? "prompt" : "skip";
17755
- }
17306
+ function wakeAction(live, now, serverSeenAt) {
17307
+ if (live) return "skip";
17756
17308
  if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
17757
17309
  return "spawn";
17758
17310
  }
@@ -17780,6 +17332,8 @@ function startHost(opts) {
17780
17332
  workspaces: listWorkspaces(opts.wsDeps)
17781
17333
  }, asHost).catch(() => {
17782
17334
  });
17335
+ void listenSlots().then(sweepSlots).catch(() => {
17336
+ });
17783
17337
  };
17784
17338
  async function claimAndSpawn() {
17785
17339
  const specs = await claimSessions(asHost).catch(() => []);
@@ -17791,8 +17345,7 @@ function startHost(opts) {
17791
17345
  harness: spec.harness,
17792
17346
  cwd: spec.workspace,
17793
17347
  mode: "bypass",
17794
- prompt: spec.prompt ?? "",
17795
- exclusive: true,
17348
+ prompt: "",
17796
17349
  token: spec.token,
17797
17350
  onExit: () => {
17798
17351
  runs.delete(key);
@@ -17812,45 +17365,39 @@ function startHost(opts) {
17812
17365
  }
17813
17366
  }
17814
17367
  const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
17815
- const wakePrompt = (fresh) => "You were woken because Paigy work is waiting for you. " + (fresh ? "This is a fresh session with your existing identity, so you may already have work in flight that you can't remember. " : "You have been running, so some of this may already be yours \u2014 but not what arrived while you were idle. ") + "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. Speak to the owner at exactly TWO moments: when you are BLOCKED on a decision only they can make (contact with waiting:'hard', the decision as the ask, options if you have real ones), and when you are DONE (one short report: what shipped, how you verified it, anything you flagged). Progress is never a contact \u2014 call set_task_state('in_progress') when you pick work up and the app shows you working; narrate to the terminal, not to the human.";
17816
- async function sweepSlots() {
17368
+ async function presenceMap() {
17817
17369
  const presence = /* @__PURE__ */ new Map();
17818
17370
  try {
17819
17371
  for (const c of (await listConnections(asHost)).items) {
17820
17372
  if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
17821
17373
  }
17374
+ lastPresence = presence;
17822
17375
  } catch {
17823
17376
  }
17824
- for (const slot of listSlots()) {
17377
+ return presence;
17378
+ }
17379
+ let lastPresence = /* @__PURE__ */ new Map();
17380
+ async function wakeSlot(slot, presence) {
17381
+ {
17825
17382
  const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
17826
17383
  const key = runKey(slot);
17827
- if (!harness) continue;
17384
+ if (!harness) return;
17828
17385
  const live = runs.get(key);
17829
17386
  const tokenId = slotIdentity(slot).tokenId;
17830
17387
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
17831
- const action = wakeAction(!!live, published.get(key)?.movedAt, Date.now(), seenByServer);
17832
- if (action === "skip") continue;
17388
+ if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
17833
17389
  const token = readToken(slot);
17834
- if (!token) continue;
17390
+ if (!token) return;
17835
17391
  const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
17836
- if (!workspace) continue;
17837
- if (!await slotHasWaitingWork(token).catch(() => false)) continue;
17392
+ if (!workspace) return;
17393
+ if (!await slotHasWaitingWork(token).catch(() => false)) return;
17838
17394
  const label = slotName(slot) ?? slot;
17839
- if (live) {
17840
- const seen = published.get(key);
17841
- const quiet = seen ? Math.round((Date.now() - seen.movedAt) / 6e4) : 0;
17842
- live.run.send(wakePrompt(false));
17843
- if (seen) seen.movedAt = Date.now();
17844
- opts.log(`\u25B6 re-woke ${label} \u2014 quiet ${quiet}m with work waiting`);
17845
- continue;
17846
- }
17847
17395
  const log2 = (line) => opts.log(`[${label}] ${line}`);
17848
17396
  const run = runHarness({
17849
17397
  harness,
17850
17398
  cwd: workspace,
17851
17399
  mode: "bypass",
17852
- prompt: wakePrompt(true),
17853
- exclusive: true,
17400
+ prompt: "",
17854
17401
  token,
17855
17402
  // A dead run must not squat the slot — evict so the next wake can respawn.
17856
17403
  onExit: () => {
@@ -17865,20 +17412,54 @@ function startHost(opts) {
17865
17412
  opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
17866
17413
  }
17867
17414
  }
17415
+ const listening = /* @__PURE__ */ new Map();
17416
+ const joining = /* @__PURE__ */ new Set();
17417
+ async function listenSlots() {
17418
+ const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot)));
17419
+ for (const [slot, sub] of [...listening]) {
17420
+ if (want.has(slot)) continue;
17421
+ listening.delete(slot);
17422
+ void sub.close().catch(() => {
17423
+ });
17424
+ }
17425
+ for (const slot of want) {
17426
+ if (listening.has(slot) || joining.has(slot)) continue;
17427
+ const token = readToken(slot);
17428
+ if (!token) continue;
17429
+ joining.add(slot);
17430
+ try {
17431
+ const sub = await subscribeWake(
17432
+ () => void presenceMap().then((presence) => wakeSlot(slot, presence)).catch(() => {
17433
+ }),
17434
+ { token }
17435
+ );
17436
+ listening.set(slot, sub);
17437
+ opts.log(`\u25C9 listening for ${slotName(slot) ?? slot}`);
17438
+ } catch (e) {
17439
+ opts.log(`\u25CC not listening for ${slotName(slot) ?? slot} \u2014 ${e.message}`);
17440
+ } finally {
17441
+ joining.delete(slot);
17442
+ }
17443
+ }
17444
+ }
17445
+ async function sweepSlots() {
17446
+ const presence = await presenceMap();
17447
+ for (const slot of listSlots()) await wakeSlot(slot, presence);
17448
+ }
17868
17449
  const ACTIVITY_MS = 2e3;
17869
17450
  const published = /* @__PURE__ */ new Map();
17870
17451
  const streamActivity = () => {
17871
- const publishTail = (token, lines) => {
17872
- void heartbeat(void 0, { token, activity: { lines, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
17452
+ const publishTail = (token, lines2) => {
17453
+ void heartbeat(void 0, { token, activity: { lines: lines2, at: (/* @__PURE__ */ new Date()).toISOString() } }).catch(() => {
17873
17454
  });
17874
17455
  };
17875
17456
  for (const [key, r] of runs) {
17876
17457
  if (!r.token) continue;
17877
- const lines = r.run.tail();
17458
+ const lines2 = r.run.tail();
17878
17459
  const was = published.get(key);
17879
- if (was && sameTail(was.lines, lines)) continue;
17880
- published.set(key, { token: r.token, lines, movedAt: Date.now() });
17881
- publishTail(r.token, lines);
17460
+ if (was && sameTail(was.lines, lines2)) continue;
17461
+ published.set(key, { token: r.token, lines: lines2 });
17462
+ publishTail(r.token, lines2);
17882
17463
  }
17883
17464
  for (const [key, was] of published) {
17884
17465
  if (runs.has(key)) continue;
@@ -17896,7 +17477,6 @@ function startHost(opts) {
17896
17477
  const pulse = setInterval(beat, 6e4);
17897
17478
  const spawnPoll = setInterval(() => {
17898
17479
  void claimAndSpawn();
17899
- void sweepSlots();
17900
17480
  publish();
17901
17481
  }, 5e3);
17902
17482
  const activityTick = setInterval(streamActivity, ACTIVITY_MS);
@@ -17907,6 +17487,12 @@ function startHost(opts) {
17907
17487
  }
17908
17488
  runs.clear();
17909
17489
  };
17490
+ const stopListening = async () => {
17491
+ const subs = [...listening.values()];
17492
+ listening.clear();
17493
+ await Promise.all(subs.map((sub) => sub.close().catch(() => {
17494
+ })));
17495
+ };
17910
17496
  const api = {
17911
17497
  sessions: () => [...runs.entries()].map(([id, r]) => [id, r.label]),
17912
17498
  // Every identity this machine holds, plus what it's doing right now. Slots are the
@@ -17927,7 +17513,16 @@ function startHost(opts) {
17927
17513
  slot,
17928
17514
  tokenId: id.tokenId,
17929
17515
  voice: id.voice,
17930
- running: live.has(name),
17516
+ // RUNNING IS ABOUT THE IDENTITY, NOT ABOUT US (owner, 2026-09-14). This was
17517
+ // `live.has(name)` — "this host spawned it and it has not exited" — which is a fact
17518
+ // about our own process table, not about the agent. An ordinary terminal session
17519
+ // heartbeating every 60 s read as NOT running, and after a host restart so did the
17520
+ // sessions we started ourselves. The server already knows: `last_seen_at` on the
17521
+ // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
17522
+ // hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
17523
+ running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
17524
+ // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
17525
+ // thing a tail can tell us. A process we did not spawn has no tail here to read.
17931
17526
  working: live.get(name)?.working() ?? false
17932
17527
  };
17933
17528
  });
@@ -17937,6 +17532,7 @@ function startHost(opts) {
17937
17532
  clearInterval(pulse);
17938
17533
  clearInterval(spawnPoll);
17939
17534
  stopSessions();
17535
+ void stopListening();
17940
17536
  streamActivity();
17941
17537
  clearInterval(activityTick);
17942
17538
  try {
@@ -17946,6 +17542,8 @@ function startHost(opts) {
17946
17542
  }
17947
17543
  };
17948
17544
  publish();
17545
+ void listenSlots().then(sweepSlots).catch(() => {
17546
+ });
17949
17547
  return api;
17950
17548
  }
17951
17549
 
@@ -18071,8 +17669,8 @@ function agentSignals(notes, connections, now) {
18071
17669
  }
18072
17670
  function noteSignals(notes, connections, now) {
18073
17671
  const nameOf = new Map(connections.map((c) => [c.id, c.name]));
18074
- const open2 = notes.filter((n) => OPEN_STATUSES.includes(n.status));
18075
- return open2.map((n) => {
17672
+ const open = notes.filter((n) => OPEN_STATUSES.includes(n.status));
17673
+ return open.map((n) => {
18076
17674
  const superseded = supersededBy(n, notes);
18077
17675
  const echo = doneEcho(n, notes);
18078
17676
  const token = assigneeToken(n);
@@ -18165,17 +17763,17 @@ async function available(deps = {}) {
18165
17763
  return [ollama, ...harnesses];
18166
17764
  }
18167
17765
  function refusal(options, asked) {
18168
- const lines = [
17766
+ const lines2 = [
18169
17767
  asked ? `Can't judge with --via ${asked}: ${options.find((o) => o.provider === asked)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
18170
17768
  "",
18171
17769
  "Triage runs the model where your queue already lives (#1106). Your options:"
18172
17770
  ];
18173
17771
  for (const o of options) {
18174
- lines.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
18175
- if (!o.ready && o.hint) lines.push(` ${o.hint}`);
17772
+ lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
17773
+ if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
18176
17774
  }
18177
- lines.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
18178
- return lines.join("\n");
17775
+ lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
17776
+ return lines2.join("\n");
18179
17777
  }
18180
17778
  var RUNNERS = {
18181
17779
  // Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
@@ -18371,27 +17969,27 @@ function suggested(run) {
18371
17969
  return run.close.length + run.stale.length + run.assign.reduce((n, a) => n + a.notes.length, 0);
18372
17970
  }
18373
17971
  function renderTable(run) {
18374
- const lines = [];
17972
+ const lines2 = [];
18375
17973
  const width = Math.max(
18376
17974
  24,
18377
17975
  ...[...run.close, ...run.stale, ...run.assign.flatMap((a) => a.notes)].map((n) => Math.min(n.title.length, 48))
18378
17976
  );
18379
17977
  const row = (n) => ` ${n.title.slice(0, width).padEnd(width)} ${n.why}`;
18380
17978
  if (run.close.length) {
18381
- lines.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
18382
- lines.push(...run.close.map(row), "");
17979
+ lines2.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
17980
+ lines2.push(...run.close.map(row), "");
18383
17981
  }
18384
17982
  if (run.stale.length) {
18385
- lines.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
18386
- lines.push(...run.stale.map(row), "");
17983
+ lines2.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
17984
+ lines2.push(...run.stale.map(row), "");
18387
17985
  }
18388
17986
  for (const group of run.assign) {
18389
- lines.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
18390
- lines.push(...group.notes.map(row), "");
17987
+ lines2.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
17988
+ lines2.push(...group.notes.map(row), "");
18391
17989
  }
18392
17990
  const acted = suggested(run);
18393
- lines.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
18394
- return lines.join("\n");
17991
+ lines2.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
17992
+ return lines2.join("\n");
18395
17993
  }
18396
17994
  async function confirm(question) {
18397
17995
  if (!process.stdin.isTTY) return false;
@@ -18514,7 +18112,7 @@ async function brokerView(deps) {
18514
18112
  }
18515
18113
 
18516
18114
  // src/main.ts
18517
- var here = dirname4(fileURLToPath(import.meta.url));
18115
+ var here = dirname3(fileURLToPath(import.meta.url));
18518
18116
  var wsDeps = { file: workspacesFile() };
18519
18117
  var DEVICE_SLOT = "Desktop";
18520
18118
  var deviceToken = () => readToken(DEVICE_SLOT);
@@ -18589,20 +18187,27 @@ ipcMain.handle("paigy:beacon", async () => {
18589
18187
  })();
18590
18188
  return { qr, code: code.user_code };
18591
18189
  });
18190
+ function waitingLine(d) {
18191
+ for (let i = d.entries.length - 1; i >= 0; i -= 1) {
18192
+ const words2 = entryWords(d.entries[i]).trim();
18193
+ if (words2) return words2.split("\n")[0];
18194
+ }
18195
+ return d.message.split("\n")[0] ?? "";
18196
+ }
18592
18197
  ipcMain.handle("paigy:agent", async (_e, name) => {
18593
18198
  const entry = (host?.roster() ?? readHostState()?.roster ?? []).find((a) => a.name === name);
18594
18199
  if (!entry) return null;
18595
18200
  const token = entry.slot ? readToken(entry.slot) : "";
18596
- const pending = token ? await pendingSummary({ token }).catch(() => null) : null;
18201
+ const open = token ? await checkReplies({ token }).then((r) => r.deliveries).catch(() => null) : null;
18597
18202
  return {
18598
18203
  ...entry,
18599
- pending: pending ? {
18600
- // The same two facts, from the counting read: what is waiting, and how much.
18601
- requests: pending.unacknowledgedItems.slice(0, 5).map((r) => ({
18602
- line: r.title.slice(0, 140),
18603
- at: r.createdAt
18204
+ pending: open ? {
18205
+ // The same two facts, from the one list: what is waiting, and how much.
18206
+ requests: open.slice(0, 5).map((d) => ({
18207
+ line: waitingLine(d).slice(0, 140),
18208
+ at: d.entries[d.entries.length - 1]?.createdAt ?? ""
18604
18209
  })),
18605
- replies: pending.unacknowledged
18210
+ replies: open.length
18606
18211
  } : null
18607
18212
  };
18608
18213
  });
@@ -18637,10 +18242,10 @@ ipcMain.handle("paigy:triage-run", async () => {
18637
18242
  if (!token) return { code: 1, output: "This Mac isn't paired yet \u2014 scan the pairing code first." };
18638
18243
  if (sweeping) return { code: 1, output: "A sweep is already running." };
18639
18244
  sweeping = true;
18640
- const lines = [];
18245
+ const lines2 = [];
18641
18246
  const say = (line) => {
18642
18247
  for (const l of line.split("\n")) {
18643
- lines.push(l);
18248
+ lines2.push(l);
18644
18249
  log(l);
18645
18250
  }
18646
18251
  };
@@ -18653,11 +18258,11 @@ ipcMain.handle("paigy:triage-run", async () => {
18653
18258
  submit: (run) => submitTriage(run, { token })
18654
18259
  }
18655
18260
  );
18656
- return { code, output: lines.join("\n") };
18261
+ return { code, output: lines2.join("\n") };
18657
18262
  } catch (e) {
18658
18263
  const message = e instanceof Error ? e.message : String(e);
18659
18264
  log(`\u26A0 the sweep failed: ${message}`);
18660
- return { code: 1, output: [...lines, `\u26A0 the sweep failed: ${message}`].join("\n") };
18265
+ return { code: 1, output: [...lines2, `\u26A0 the sweep failed: ${message}`].join("\n") };
18661
18266
  } finally {
18662
18267
  sweeping = false;
18663
18268
  }