@paigy/harness 0.3.14 → 0.3.15

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 +216 -121
  2. package/dist/main.js +212 -118
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -8638,6 +8638,7 @@ import { randomUUID as randomUUID2 } from "crypto";
8638
8638
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
8639
8639
  import { homedir as homedir2 } from "os";
8640
8640
  import { join as join2 } from "path";
8641
+ import { execFileSync as execFileSync2 } from "child_process";
8641
8642
  var import_undici = __toESM(require_undici(), 1);
8642
8643
  var require2 = __sdkCreateRequire(import.meta.url);
8643
8644
  var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
@@ -9921,6 +9922,23 @@ var zodToJsonSchema = (schema, options) => {
9921
9922
  };
9922
9923
  var OPTIONS_MIN = 2;
9923
9924
  var OPTIONS_MAX = 6;
9925
+ var OptionSchema = external_exports.object({
9926
+ id: external_exports.string(),
9927
+ label: external_exports.string(),
9928
+ hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
9929
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
9930
+ // the constraints below are what an agent reads when deciding to use these.
9931
+ html: external_exports.string().max(16384).describe(
9932
+ "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
9933
+ ).optional(),
9934
+ image: external_exports.string().url().describe(
9935
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
9936
+ ).optional()
9937
+ });
9938
+ var OptionInputSchema = OptionSchema.omit({ id: true }).extend({
9939
+ label: external_exports.string().trim().min(1).max(1e3)
9940
+ }).strict();
9941
+ var NIGHT = { from: 23, to: 7 };
9924
9942
  function draft2020(node) {
9925
9943
  if (Array.isArray(node)) return node.map(draft2020);
9926
9944
  if (node && typeof node === "object") {
@@ -9952,26 +9970,21 @@ function mcpInputSchema(s) {
9952
9970
  }
9953
9971
  var AskInputSchema = external_exports.object({
9954
9972
  id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
9955
- parentId: external_exports.string().uuid().optional().describe("Optional explicit parent Goal ID. If omitted, the platform's semantic router will auto-cluster it."),
9973
+ parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
9956
9974
  repo: external_exports.string().optional().describe("Optional repository context."),
9957
9975
  ask: external_exports.string().trim().min(1).max(1e4).describe(
9958
9976
  "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."
9959
9977
  ),
9960
- options: external_exports.array(external_exports.object({
9961
- label: external_exports.string().trim().min(1).max(1e3),
9962
- hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
9963
- image: external_exports.string().url().optional(),
9964
- html: external_exports.string().max(16384).describe("Optional sandboxed HTML/CSS preview. No JavaScript or network; inline CSS and data: URIs only. It renders edge-to-edge in a responsive card 200pt tall (about 320pt wide on a phone, with the next option peeking beside it), so fit the HTML to that viewport.").optional()
9965
- }).strict()).min(2).max(6).optional()
9978
+ options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
9966
9979
  }).strict();
9967
9980
  var StartContactSchema = external_exports.object({
9968
- asks: external_exports.array(AskInputSchema).min(1).describe("The list of asks/questions to pose. They will be semantically clustered by the API."),
9981
+ asks: external_exports.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
9969
9982
  waiting: external_exports.enum(["none", "hard"]).default("none"),
9970
9983
  channel: external_exports.enum(["notification", "call"]).default("notification")
9971
9984
  }).strict();
9972
9985
  var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
9973
9986
  var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
9974
- var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. The platform will semantically cluster your asks under the correct Goals automatically. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
9987
+ var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
9975
9988
  var CreateGoalSchema = external_exports.object({
9976
9989
  outcome: external_exports.string().trim().min(1).max(1e4),
9977
9990
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -9979,10 +9992,6 @@ var CreateGoalSchema = external_exports.object({
9979
9992
  title: external_exports.string().trim().min(1).max(80).optional(),
9980
9993
  ownerParticipant: external_exports.string().trim().min(1).optional(),
9981
9994
  idempotencyKey: external_exports.string().trim().min(1).max(200),
9982
- /** A past conversation this Goal should be read against — History's "new session from this"
9983
- * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
9984
- * reference only: the owner reads it through `get_thread`, which does its own scoping, and
9985
- * the writer refuses a thread belonging to another account. */
9986
9995
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
9987
9996
  * didn't get created as a child goal of the voice UI goal, which is how it should've
9988
9997
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -10026,19 +10035,9 @@ var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State,
10026
10035
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
10027
10036
  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 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
10028
10037
  var CheckRepliesSchema = external_exports.object({}).strict();
10029
- var GetThreadSchema = external_exports.object({
10030
- parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
10031
- }).strict();
10032
- 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.";
10033
- var SearchThreadsSchema = external_exports.object({
10034
- q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
10035
- }).strict();
10036
- 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.`;
10037
10038
  var AGENT_TOOLS = [
10038
10039
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
10039
10040
  { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
10040
- { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
10041
- { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
10042
10041
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
10043
10042
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
10044
10043
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
@@ -10072,19 +10071,6 @@ var TransformSchema = external_exports.enum([
10072
10071
  "summarize"
10073
10072
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
10074
10073
  ]);
10075
- var OptionSchema = external_exports.object({
10076
- id: external_exports.string(),
10077
- label: external_exports.string(),
10078
- hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
10079
- // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
10080
- // the constraints below are what an agent reads when deciding to use these.
10081
- html: external_exports.string().max(16384).describe(
10082
- "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
10083
- ).optional(),
10084
- image: external_exports.string().url().describe(
10085
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
10086
- ).optional()
10087
- });
10088
10074
  var VisualSchema = external_exports.object({
10089
10075
  url: external_exports.string().url(),
10090
10076
  label: external_exports.string().optional()
@@ -10138,7 +10124,7 @@ var NotifyRequestFields = external_exports.object({
10138
10124
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
10139
10125
  * superRefine at the bottom enforces exactly one of the two. */
10140
10126
  context: ContextSchema.optional(),
10141
- options: external_exports.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
10127
+ options: external_exports.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
10142
10128
  "The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
10143
10129
  ),
10144
10130
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -10332,7 +10318,7 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
10332
10318
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
10333
10319
  * post-feedback revision, #633) — the user will never answer this id. Stop
10334
10320
  * awaiting it; the live ask is the thread's newest turn (await that one, or
10335
- * re-orient via get_thread / check_replies). */
10321
+ * re-orient via check_replies). */
10336
10322
  external_exports.object({
10337
10323
  type: external_exports.literal("superseded"),
10338
10324
  parentId: external_exports.string(),
@@ -10446,10 +10432,6 @@ var InboxItemSchema = external_exports.object({
10446
10432
  options: external_exports.array(OptionSchema).optional(),
10447
10433
  /** The ask's declared coverage points (#396), when the agent sent them. */
10448
10434
  points: external_exports.array(external_exports.string()).optional(),
10449
- /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
10450
- * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
10451
- * between them could not express a call as a sequence. `question: null` is a real turn —
10452
- * a status update stays a statement instead of being shaped into a yes/no. */
10453
10435
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
10454
10436
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
10455
10437
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
@@ -10470,7 +10452,26 @@ var InboxItemSchema = external_exports.object({
10470
10452
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
10471
10453
  * `createdAt`. */
10472
10454
  agentStateAt: external_exports.string().datetime().optional(),
10473
- agenda: external_exports.array(AgendaTurnSchema).optional(),
10455
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
10456
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
10457
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
10458
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
10459
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
10460
+ *
10461
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
10462
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
10463
+ *
10464
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
10465
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
10466
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
10467
+ * REPLACED `agenda` (turns), which nothing ever filled. */
10468
+ questions: external_exports.array(external_exports.object({
10469
+ id: external_exports.string(),
10470
+ entryId: external_exports.string(),
10471
+ title: external_exports.string(),
10472
+ state: external_exports.enum(["open", "answered"]),
10473
+ answer: external_exports.string().nullable()
10474
+ })).optional(),
10474
10475
  visuals: external_exports.array(VisualSchema).optional(),
10475
10476
  /** The connected agent's name (the single pairing name — user-typed, or the
10476
10477
  * agent's suggestion, or a default silly name). */
@@ -10491,12 +10492,21 @@ var InboxItemSchema = external_exports.object({
10491
10492
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
10492
10493
  error: external_exports.string().optional(),
10493
10494
  clarifies: external_exports.string().optional(),
10494
- /** The ring ladder ran out while this was still pending — we tried to reach you and
10495
- * STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
10496
- * agent with nothing to say, which the roster drew identically until now: "nothing to
10497
- * say" and "gave up saying it" are opposite situations wearing the same face
10498
- * (navigation-design.md, gap 1). False for anything that never rang. */
10499
- gaveUp: external_exports.boolean().default(false),
10495
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
10496
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
10497
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
10498
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
10499
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
10500
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
10501
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
10502
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
10503
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
10504
+ ring: external_exports.object({
10505
+ declinedAt: external_exports.string().datetime(),
10506
+ anchorAt: external_exports.string().datetime(),
10507
+ nextRingAt: external_exports.string().datetime().nullable(),
10508
+ step: external_exports.number().int()
10509
+ }).optional(),
10500
10510
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
10501
10511
  * Absent for anything never delivered through a push, and for older rows written before
10502
10512
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
@@ -10561,6 +10571,8 @@ var MissedCallSchema = external_exports.enum([
10561
10571
  "inbox",
10562
10572
  "dismiss"
10563
10573
  ]);
10574
+ var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
10575
+ var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
10564
10576
  var BrokerTuningSchema = external_exports.object({
10565
10577
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
10566
10578
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -10595,6 +10607,12 @@ var UserSettingsSchema = external_exports.object({
10595
10607
  * must not silently reset this privacy choice. Absent = leave unchanged on
10596
10608
  * write, 'hosted' on read (see store.ts). */
10597
10609
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
10610
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
10611
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
10612
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
10613
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
10614
+ * read (see store.ts). */
10615
+ talk: external_exports.boolean().optional(),
10598
10616
  /** Per-user ring budget (#603): calls per rolling day before further calls
10599
10617
  * degrade to banner. Absent = the global default (25). A number, never a
10600
10618
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -10792,6 +10810,9 @@ var QueueItemSchema = external_exports.object({
10792
10810
  assignee: external_exports.string().nullable().default(null),
10793
10811
  /** What the agent last said it was doing; null if it has said nothing. */
10794
10812
  progress: external_exports.string().nullable().default(null),
10813
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
10814
+ * served only while it was written for the current note. Null means show the Goal's name. */
10815
+ progressLine: external_exports.string().nullable().optional(),
10795
10816
  reviewPending: external_exports.boolean().default(false),
10796
10817
  dueAt: external_exports.string().nullable().default(null),
10797
10818
  /** The Goal this one was opened under; null at the root. */
@@ -10807,8 +10828,13 @@ var QueueItemSchema = external_exports.object({
10807
10828
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
10808
10829
  repo: external_exports.string().nullable().optional(),
10809
10830
  createdAt: external_exports.string(),
10810
- updatedAt: external_exports.string().nullable().default(null)
10831
+ updatedAt: external_exports.string().nullable().default(null),
10832
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
10833
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
10834
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
10835
+ lastProgressAt: external_exports.string().nullable().optional()
10811
10836
  });
10837
+ var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
10812
10838
  var NoteSourceSchema = external_exports.enum(["app", "call"]);
10813
10839
  var NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
10814
10840
  var NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
@@ -11109,6 +11135,8 @@ var ApiError = class extends Error {
11109
11135
  this.body = body;
11110
11136
  }
11111
11137
  };
11138
+ var tokenOverride = null;
11139
+ var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
11112
11140
  async function fail(what, res) {
11113
11141
  throw new ApiError(what, res.status, await res.text());
11114
11142
  }
@@ -11145,8 +11173,6 @@ async function updateGoal(goalId, input, opts = {}) {
11145
11173
  return await res.json();
11146
11174
  }
11147
11175
  var AWAIT_WINDOW_MS = 45e3;
11148
- var tokenOverride = null;
11149
- var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
11150
11176
  async function whoAmI(opts = {}) {
11151
11177
  try {
11152
11178
  const res = await reach(`${BACKEND_URL}/api/identity`, {
@@ -11233,6 +11259,32 @@ async function dismissTriage(proposalId, opts = {}) {
11233
11259
  if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
11234
11260
  return await res.json();
11235
11261
  }
11262
+ function repoFromRemote(url) {
11263
+ const said = (url ?? "").trim();
11264
+ if (!said) return null;
11265
+ const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
11266
+ const parts = path.split("/").filter(Boolean);
11267
+ if (parts.length < 2) return null;
11268
+ const [owner, name] = parts.slice(-2);
11269
+ if (!owner || !name) return null;
11270
+ return `${owner}/${name}`.toLowerCase();
11271
+ }
11272
+ var asked;
11273
+ function currentRepo(cwd = process.cwd()) {
11274
+ if (asked !== void 0) return asked;
11275
+ try {
11276
+ const url = execFileSync2("git", ["remote", "get-url", "origin"], {
11277
+ cwd,
11278
+ encoding: "utf8",
11279
+ timeout: 2e3,
11280
+ stdio: ["ignore", "pipe", "ignore"]
11281
+ });
11282
+ asked = repoFromRemote(url);
11283
+ } catch {
11284
+ asked = null;
11285
+ }
11286
+ return asked;
11287
+ }
11236
11288
  async function contact(input, opts = {}) {
11237
11289
  const parsed = ContactSchema.parse(input);
11238
11290
  opts.signal?.throwIfAborted();
@@ -11246,7 +11298,11 @@ async function contact(input, opts = {}) {
11246
11298
  const reqAsks = parsed.asks.map((a) => ({
11247
11299
  id: a.id,
11248
11300
  parentId: a.parentId,
11249
- repo: a.repo,
11301
+ // Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
11302
+ // work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
11303
+ // still wins — normalised the same way — because a caller that names one knows something
11304
+ // this cannot see.
11305
+ repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
11250
11306
  ask: a.ask,
11251
11307
  options: a.options ?? []
11252
11308
  }));
@@ -11268,10 +11324,12 @@ async function contact(input, opts = {}) {
11268
11324
  deliveryId = deliveries[0]?.deliveryId ?? "";
11269
11325
  if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
11270
11326
  }
11327
+ const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId);
11271
11328
  const read = async (signal2) => {
11272
11329
  const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
11273
11330
  if (!res.ok) await fail("read_delivery", res);
11274
- return await res.json();
11331
+ const one = await res.json();
11332
+ return all.length > 1 ? { ...one, deliveries: all } : one;
11275
11333
  };
11276
11334
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
11277
11335
  if (opts.waits === false) return read(opts.signal);
@@ -11375,7 +11433,8 @@ function subscribeRealtime(args) {
11375
11433
  };
11376
11434
  }
11377
11435
  async function subscribeWake(onNudge, opts = {}) {
11378
- const cfg = await registerDelivery("self_hosted", opts);
11436
+ const { release = true, ...client } = opts;
11437
+ const cfg = await registerDelivery("self_hosted", client);
11379
11438
  const { url, anonKey } = cfg.realtime;
11380
11439
  const channel = wakeChannel(cfg.tokenId);
11381
11440
  const ch = subscribeRealtime({
@@ -11390,7 +11449,7 @@ async function subscribeWake(onNudge, opts = {}) {
11390
11449
  channel,
11391
11450
  close: async () => {
11392
11451
  ch.close();
11393
- await registerDelivery("poll", opts).catch(() => {
11452
+ if (release) await registerDelivery("poll", client).catch(() => {
11394
11453
  });
11395
11454
  }
11396
11455
  };
@@ -12881,6 +12940,23 @@ var zodToJsonSchema2 = (schema, options) => {
12881
12940
  // ../../packages/schema/dist/index.js
12882
12941
  var OPTIONS_MIN2 = 2;
12883
12942
  var OPTIONS_MAX2 = 6;
12943
+ var OptionSchema2 = external_exports.object({
12944
+ id: external_exports.string(),
12945
+ label: external_exports.string(),
12946
+ hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
12947
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
12948
+ // the constraints below are what an agent reads when deciding to use these.
12949
+ html: external_exports.string().max(16384).describe(
12950
+ "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
12951
+ ).optional(),
12952
+ image: external_exports.string().url().describe(
12953
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
12954
+ ).optional()
12955
+ });
12956
+ var OptionInputSchema2 = OptionSchema2.omit({ id: true }).extend({
12957
+ label: external_exports.string().trim().min(1).max(1e3)
12958
+ }).strict();
12959
+ var NIGHT2 = { from: 23, to: 7 };
12884
12960
  function draft20202(node) {
12885
12961
  if (Array.isArray(node)) return node.map(draft20202);
12886
12962
  if (node && typeof node === "object") {
@@ -12912,26 +12988,21 @@ function mcpInputSchema2(s) {
12912
12988
  }
12913
12989
  var AskInputSchema2 = external_exports.object({
12914
12990
  id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
12915
- parentId: external_exports.string().uuid().optional().describe("Optional explicit parent Goal ID. If omitted, the platform's semantic router will auto-cluster it."),
12991
+ parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
12916
12992
  repo: external_exports.string().optional().describe("Optional repository context."),
12917
12993
  ask: external_exports.string().trim().min(1).max(1e4).describe(
12918
12994
  "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."
12919
12995
  ),
12920
- options: external_exports.array(external_exports.object({
12921
- label: external_exports.string().trim().min(1).max(1e3),
12922
- hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
12923
- image: external_exports.string().url().optional(),
12924
- html: external_exports.string().max(16384).describe("Optional sandboxed HTML/CSS preview. No JavaScript or network; inline CSS and data: URIs only. It renders edge-to-edge in a responsive card 200pt tall (about 320pt wide on a phone, with the next option peeking beside it), so fit the HTML to that viewport.").optional()
12925
- }).strict()).min(2).max(6).optional()
12996
+ options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
12926
12997
  }).strict();
12927
12998
  var StartContactSchema2 = external_exports.object({
12928
- asks: external_exports.array(AskInputSchema2).min(1).describe("The list of asks/questions to pose. They will be semantically clustered by the API."),
12999
+ asks: external_exports.array(AskInputSchema2).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
12929
13000
  waiting: external_exports.enum(["none", "hard"]).default("none"),
12930
13001
  channel: external_exports.enum(["notification", "call"]).default("notification")
12931
13002
  }).strict();
12932
13003
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
12933
13004
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
12934
- var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. The platform will semantically cluster your asks under the correct Goals automatically. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
13005
+ var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
12935
13006
  var CreateGoalSchema2 = external_exports.object({
12936
13007
  outcome: external_exports.string().trim().min(1).max(1e4),
12937
13008
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -12939,10 +13010,6 @@ var CreateGoalSchema2 = external_exports.object({
12939
13010
  title: external_exports.string().trim().min(1).max(80).optional(),
12940
13011
  ownerParticipant: external_exports.string().trim().min(1).optional(),
12941
13012
  idempotencyKey: external_exports.string().trim().min(1).max(200),
12942
- /** A past conversation this Goal should be read against — History's "new session from this"
12943
- * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
12944
- * reference only: the owner reads it through `get_thread`, which does its own scoping, and
12945
- * the writer refuses a thread belonging to another account. */
12946
13013
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
12947
13014
  * didn't get created as a child goal of the voice UI goal, which is how it should've
12948
13015
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -12986,19 +13053,9 @@ var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State
12986
13053
  var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
12987
13054
  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 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
12988
13055
  var CheckRepliesSchema2 = external_exports.object({}).strict();
12989
- var GetThreadSchema2 = external_exports.object({
12990
- parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
12991
- }).strict();
12992
- 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.";
12993
- var SearchThreadsSchema2 = external_exports.object({
12994
- q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
12995
- }).strict();
12996
- 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.`;
12997
13056
  var AGENT_TOOLS2 = [
12998
13057
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
12999
13058
  { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
13000
- { name: "get_thread", description: GET_THREAD_DESCRIPTION2, inputSchema: mcpInputSchema2(GetThreadSchema2) },
13001
- { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchThreadsSchema2) },
13002
13059
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
13003
13060
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
13004
13061
  { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
@@ -13047,19 +13104,6 @@ var TransformSchema2 = external_exports.enum([
13047
13104
  "summarize"
13048
13105
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
13049
13106
  ]);
13050
- var OptionSchema2 = external_exports.object({
13051
- id: external_exports.string(),
13052
- label: external_exports.string(),
13053
- hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
13054
- // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
13055
- // the constraints below are what an agent reads when deciding to use these.
13056
- html: external_exports.string().max(16384).describe(
13057
- "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
13058
- ).optional(),
13059
- image: external_exports.string().url().describe(
13060
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
13061
- ).optional()
13062
- });
13063
13107
  var VisualSchema2 = external_exports.object({
13064
13108
  url: external_exports.string().url(),
13065
13109
  label: external_exports.string().optional()
@@ -13113,7 +13157,7 @@ var NotifyRequestFields2 = external_exports.object({
13113
13157
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
13114
13158
  * superRefine at the bottom enforces exactly one of the two. */
13115
13159
  context: ContextSchema2.optional(),
13116
- options: external_exports.array(OptionSchema2.omit({ id: true })).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
13160
+ options: external_exports.array(OptionInputSchema2).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
13117
13161
  "The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
13118
13162
  ),
13119
13163
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -13307,7 +13351,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
13307
13351
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
13308
13352
  * post-feedback revision, #633) — the user will never answer this id. Stop
13309
13353
  * awaiting it; the live ask is the thread's newest turn (await that one, or
13310
- * re-orient via get_thread / check_replies). */
13354
+ * re-orient via check_replies). */
13311
13355
  external_exports.object({
13312
13356
  type: external_exports.literal("superseded"),
13313
13357
  parentId: external_exports.string(),
@@ -13421,10 +13465,6 @@ var InboxItemSchema2 = external_exports.object({
13421
13465
  options: external_exports.array(OptionSchema2).optional(),
13422
13466
  /** The ask's declared coverage points (#396), when the agent sent them. */
13423
13467
  points: external_exports.array(external_exports.string()).optional(),
13424
- /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
13425
- * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
13426
- * between them could not express a call as a sequence. `question: null` is a real turn —
13427
- * a status update stays a statement instead of being shaped into a yes/no. */
13428
13468
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
13429
13469
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
13430
13470
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
@@ -13445,7 +13485,26 @@ var InboxItemSchema2 = external_exports.object({
13445
13485
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
13446
13486
  * `createdAt`. */
13447
13487
  agentStateAt: external_exports.string().datetime().optional(),
13448
- agenda: external_exports.array(AgendaTurnSchema2).optional(),
13488
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
13489
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
13490
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
13491
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
13492
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
13493
+ *
13494
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
13495
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
13496
+ *
13497
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
13498
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
13499
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
13500
+ * REPLACED `agenda` (turns), which nothing ever filled. */
13501
+ questions: external_exports.array(external_exports.object({
13502
+ id: external_exports.string(),
13503
+ entryId: external_exports.string(),
13504
+ title: external_exports.string(),
13505
+ state: external_exports.enum(["open", "answered"]),
13506
+ answer: external_exports.string().nullable()
13507
+ })).optional(),
13449
13508
  visuals: external_exports.array(VisualSchema2).optional(),
13450
13509
  /** The connected agent's name (the single pairing name — user-typed, or the
13451
13510
  * agent's suggestion, or a default silly name). */
@@ -13466,12 +13525,21 @@ var InboxItemSchema2 = external_exports.object({
13466
13525
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
13467
13526
  error: external_exports.string().optional(),
13468
13527
  clarifies: external_exports.string().optional(),
13469
- /** The ring ladder ran out while this was still pending — we tried to reach you and
13470
- * STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
13471
- * agent with nothing to say, which the roster drew identically until now: "nothing to
13472
- * say" and "gave up saying it" are opposite situations wearing the same face
13473
- * (navigation-design.md, gap 1). False for anything that never rang. */
13474
- gaveUp: external_exports.boolean().default(false),
13528
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
13529
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
13530
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
13531
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
13532
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
13533
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
13534
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
13535
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
13536
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
13537
+ ring: external_exports.object({
13538
+ declinedAt: external_exports.string().datetime(),
13539
+ anchorAt: external_exports.string().datetime(),
13540
+ nextRingAt: external_exports.string().datetime().nullable(),
13541
+ step: external_exports.number().int()
13542
+ }).optional(),
13475
13543
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
13476
13544
  * Absent for anything never delivered through a push, and for older rows written before
13477
13545
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
@@ -13536,6 +13604,8 @@ var MissedCallSchema2 = external_exports.enum([
13536
13604
  "inbox",
13537
13605
  "dismiss"
13538
13606
  ]);
13607
+ var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
13608
+ var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
13539
13609
  var BrokerTuningSchema2 = external_exports.object({
13540
13610
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
13541
13611
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -13570,6 +13640,12 @@ var UserSettingsSchema2 = external_exports.object({
13570
13640
  * must not silently reset this privacy choice. Absent = leave unchanged on
13571
13641
  * write, 'hosted' on read (see store.ts). */
13572
13642
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
13643
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
13644
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
13645
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
13646
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
13647
+ * read (see store.ts). */
13648
+ talk: external_exports.boolean().optional(),
13573
13649
  /** Per-user ring budget (#603): calls per rolling day before further calls
13574
13650
  * degrade to banner. Absent = the global default (25). A number, never a
13575
13651
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -13767,6 +13843,9 @@ var QueueItemSchema2 = external_exports.object({
13767
13843
  assignee: external_exports.string().nullable().default(null),
13768
13844
  /** What the agent last said it was doing; null if it has said nothing. */
13769
13845
  progress: external_exports.string().nullable().default(null),
13846
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
13847
+ * served only while it was written for the current note. Null means show the Goal's name. */
13848
+ progressLine: external_exports.string().nullable().optional(),
13770
13849
  reviewPending: external_exports.boolean().default(false),
13771
13850
  dueAt: external_exports.string().nullable().default(null),
13772
13851
  /** The Goal this one was opened under; null at the root. */
@@ -13782,8 +13861,13 @@ var QueueItemSchema2 = external_exports.object({
13782
13861
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
13783
13862
  repo: external_exports.string().nullable().optional(),
13784
13863
  createdAt: external_exports.string(),
13785
- updatedAt: external_exports.string().nullable().default(null)
13864
+ updatedAt: external_exports.string().nullable().default(null),
13865
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
13866
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
13867
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
13868
+ lastProgressAt: external_exports.string().nullable().optional()
13786
13869
  });
13870
+ var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
13787
13871
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
13788
13872
  var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
13789
13873
  var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
@@ -14232,7 +14316,13 @@ function startSession(opts) {
14232
14316
  ...process.env,
14233
14317
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
14234
14318
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
14235
- ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
14319
+ ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
14320
+ // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
14321
+ // bridge's pump typing answers in, so nothing inside it may start a second delivery path.
14322
+ // Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
14323
+ // fresh spawn has none, which is how a fresh run read as "not harness-started" to the
14324
+ // session-start hook and the `listen` tool (they now test either mark; listening.ts).
14325
+ PAIGY_HARNESS: "1"
14236
14326
  },
14237
14327
  stdio: ["pipe", "pipe", "pipe"]
14238
14328
  });
@@ -14722,9 +14812,9 @@ async function slotHasWaitingWork(token, session) {
14722
14812
  }
14723
14813
  var PRESENCE_FRESH_MS = 3 * 6e4;
14724
14814
  function wakeAction(live, now, serverSeenAt) {
14725
- if (live) return "skip";
14726
- if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
14727
- return "spawn";
14815
+ if (live) return { action: "skip", why: "run" };
14816
+ if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
14817
+ return { action: "spawn", why: "nobody" };
14728
14818
  }
14729
14819
  function startHost(opts) {
14730
14820
  const runs = /* @__PURE__ */ new Map();
@@ -14814,7 +14904,11 @@ function startHost(opts) {
14814
14904
  const live = runs.get(key);
14815
14905
  const tokenId = slotIdentity(slot).tokenId;
14816
14906
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
14817
- if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
14907
+ const decided = wakeAction(!!live, Date.now(), seenByServer);
14908
+ if (decided.action === "skip") {
14909
+ opts.log(decided.why === "run" ? `\u25CC ${slotName(slot) ?? slot} is running here \u2014 its pump types the work in; not spawning` : `\u25CC ${slotName(slot) ?? slot} is working somewhere this host cannot see (heartbeat) \u2014 not spawning`);
14910
+ return;
14911
+ }
14818
14912
  const token = readToken(slot);
14819
14913
  if (!token) return;
14820
14914
  const owner = ownerSession(slot);
@@ -15194,9 +15288,9 @@ async function available(deps = {}) {
15194
15288
  });
15195
15289
  return [ollama, ...harnesses];
15196
15290
  }
15197
- function refusal(options, asked) {
15291
+ function refusal(options, asked2) {
15198
15292
  const lines2 = [
15199
- 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.",
15293
+ asked2 ? `Can't judge with --via ${asked2}: ${options.find((o) => o.provider === asked2)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
15200
15294
  "",
15201
15295
  "Triage runs the model where your queue already lives (#1106). Your options:"
15202
15296
  ];
@@ -15220,10 +15314,10 @@ var RUNNERS = {
15220
15314
  agy: { cli: "agy", argv: () => ["-p"] }
15221
15315
  };
15222
15316
  var RUN_TIMEOUT_MS = 5 * 6e4;
15223
- async function judgeFor(asked, deps = {}) {
15317
+ async function judgeFor(asked2, deps = {}) {
15224
15318
  const options = await available(deps);
15225
- const chosen = asked ? options.find((o) => o.provider === asked) : options.find((o) => o.ready);
15226
- if (!chosen?.ready) return { error: refusal(options, asked) };
15319
+ const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
15320
+ if (!chosen?.ready) return { error: refusal(options, asked2) };
15227
15321
  if (chosen.provider === "ollama") {
15228
15322
  const model = chosen.model;
15229
15323
  return {
@@ -15244,9 +15338,9 @@ async function judgeFor(asked, deps = {}) {
15244
15338
  };
15245
15339
  }
15246
15340
  const runner = RUNNERS[chosen.provider];
15247
- if (!runner) return { error: refusal(options, asked) };
15341
+ if (!runner) return { error: refusal(options, asked2) };
15248
15342
  const bin = which(runner.cli);
15249
- if (!bin) return { error: refusal(options, asked) };
15343
+ if (!bin) return { error: refusal(options, asked2) };
15250
15344
  return {
15251
15345
  provider: chosen.provider,
15252
15346
  model: null,