@paigy/harness 0.3.14 → 0.3.16

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 +434 -235
  2. package/dist/main.js +431 -233
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -22755,6 +22755,7 @@ import { randomUUID as randomUUID2 } from "crypto";
22755
22755
  import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
22756
22756
  import { homedir as homedir22 } from "os";
22757
22757
  import { join as join22 } from "path";
22758
+ import { execFileSync as execFileSync2 } from "child_process";
22758
22759
  async function proxy() {
22759
22760
  if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
22760
22761
  return agent ??= new (await Promise.resolve().then(() => __toESM(require_undici(), 1))).EnvHttpProxyAgent();
@@ -23786,6 +23787,9 @@ function ensureAuthed(res) {
23786
23787
  if (res.status === 401) throw new UnpairedError();
23787
23788
  return res;
23788
23789
  }
23790
+ function overrideToken(secret) {
23791
+ tokenOverride = secret;
23792
+ }
23789
23793
  async function fail(what, res) {
23790
23794
  throw new ApiError(what, res.status, await res.text());
23791
23795
  }
@@ -23821,9 +23825,6 @@ async function updateGoal(goalId, input, opts = {}) {
23821
23825
  if (!res.ok) await fail("update_goal", res);
23822
23826
  return await res.json();
23823
23827
  }
23824
- function overrideToken(secret) {
23825
- tokenOverride = secret;
23826
- }
23827
23828
  async function hatch(name, voice = null) {
23828
23829
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
23829
23830
  method: "POST",
@@ -23904,6 +23905,31 @@ async function acceptTriage(proposalId, body, opts = {}) {
23904
23905
  if (!res.ok) throw new Error(`accept_triage failed: ${res.status} ${await res.text()}`);
23905
23906
  return await res.json();
23906
23907
  }
23908
+ function repoFromRemote(url) {
23909
+ const said = (url ?? "").trim();
23910
+ if (!said) return null;
23911
+ const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
23912
+ const parts = path.split("/").filter(Boolean);
23913
+ if (parts.length < 2) return null;
23914
+ const [owner, name] = parts.slice(-2);
23915
+ if (!owner || !name) return null;
23916
+ return `${owner}/${name}`.toLowerCase();
23917
+ }
23918
+ function currentRepo(cwd = process.cwd()) {
23919
+ if (asked !== void 0) return asked;
23920
+ try {
23921
+ const url = execFileSync2("git", ["remote", "get-url", "origin"], {
23922
+ cwd,
23923
+ encoding: "utf8",
23924
+ timeout: 2e3,
23925
+ stdio: ["ignore", "pipe", "ignore"]
23926
+ });
23927
+ asked = repoFromRemote(url);
23928
+ } catch {
23929
+ asked = null;
23930
+ }
23931
+ return asked;
23932
+ }
23907
23933
  async function contact(input, opts = {}) {
23908
23934
  const parsed = ContactSchema.parse(input);
23909
23935
  opts.signal?.throwIfAborted();
@@ -23911,13 +23937,18 @@ async function contact(input, opts = {}) {
23911
23937
  const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
23912
23938
  let deliveryId;
23913
23939
  let deliveries = [];
23940
+ let recorded = [];
23914
23941
  if ("deliveryId" in parsed) {
23915
23942
  deliveryId = parsed.deliveryId;
23916
23943
  } else {
23917
23944
  const reqAsks = parsed.asks.map((a) => ({
23918
23945
  id: a.id,
23919
23946
  parentId: a.parentId,
23920
- repo: a.repo,
23947
+ // Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
23948
+ // work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
23949
+ // still wins — normalised the same way — because a caller that names one knows something
23950
+ // this cannot see.
23951
+ repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
23921
23952
  ask: a.ask,
23922
23953
  options: a.options ?? []
23923
23954
  }));
@@ -23935,14 +23966,24 @@ async function contact(input, opts = {}) {
23935
23966
  if (!res.ok) await fail("batch contact", res);
23936
23967
  const receipt = await res.json();
23937
23968
  deliveries = receipt.deliveries ?? [];
23969
+ recorded = receipt.recorded ?? [];
23970
+ if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
23938
23971
  if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
23939
23972
  deliveryId = deliveries[0]?.deliveryId ?? "";
23940
23973
  if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
23941
23974
  }
23975
+ const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
23976
+ const joinedCard = deliveries[0]?.joinedCard === true;
23942
23977
  const read = async (signal2) => {
23943
23978
  const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23944
23979
  if (!res.ok) await fail("read_delivery", res);
23945
- return await res.json();
23980
+ const one = await res.json();
23981
+ return {
23982
+ ...one,
23983
+ ...all.length > 1 ? { deliveries: all } : {},
23984
+ ...recorded.length ? { recorded, notSent: NOT_SENT } : {},
23985
+ ...joinedCard ? { joinedCard: true } : {}
23986
+ };
23946
23987
  };
23947
23988
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
23948
23989
  if (opts.waits === false) return read(opts.signal);
@@ -23970,45 +24011,105 @@ async function checkReplies(opts = {}) {
23970
24011
  return await res.json();
23971
24012
  }
23972
24013
  function frame(line, topic, event, payload) {
24014
+ const ref = String(++line.ref);
23973
24015
  try {
23974
- line.ws?.send(JSON.stringify({ topic, event, payload, ref: String(++line.ref) }));
24016
+ line.ws?.send(JSON.stringify({ topic, event, payload, ref }));
23975
24017
  } catch {
23976
24018
  }
24019
+ return ref;
23977
24020
  }
23978
24021
  function stop(line) {
23979
24022
  line.open = false;
24023
+ line.beat = void 0;
23980
24024
  clearInterval(line.heart);
23981
24025
  clearTimeout(line.retry);
24026
+ for (const t of line.topics.values()) {
24027
+ clearTimeout(t.timer);
24028
+ t.joined = false;
24029
+ t.ref = void 0;
24030
+ }
24031
+ }
24032
+ function join3(line, name, t) {
24033
+ clearTimeout(t.timer);
24034
+ t.joined = false;
24035
+ t.ref = frame(line, `realtime:${name}`, "phx_join", JOIN);
24036
+ t.timer = setTimeout(() => rejoin(line, name, t), JOIN_TIMEOUT_MS);
24037
+ }
24038
+ function rejoin(line, name, t) {
24039
+ clearTimeout(t.timer);
24040
+ t.joined = false;
24041
+ t.ref = void 0;
24042
+ if (!line.open || line.topics.get(name) !== t) return;
24043
+ t.timer = setTimeout(() => {
24044
+ if (line.open && line.topics.get(name) === t) join3(line, name, t);
24045
+ }, RETRY_MS[Math.min(t.attempt++, RETRY_MS.length - 1)]);
24046
+ }
24047
+ function joined(t) {
24048
+ clearTimeout(t.timer);
24049
+ t.joined = true;
24050
+ t.attempt = 0;
24051
+ for (const l of t.listeners) l.onJoined?.(++l.joins);
23982
24052
  }
23983
24053
  function connect(line) {
23984
24054
  const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
23985
24055
  const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
24056
+ let gone = false;
24057
+ const again = () => {
24058
+ if (gone) return;
24059
+ gone = true;
24060
+ stop(line);
24061
+ if (!line.topics.size) return;
24062
+ line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
24063
+ };
23986
24064
  ws.addEventListener("open", () => {
24065
+ if (gone) return;
23987
24066
  line.open = true;
23988
24067
  line.attempt = 0;
23989
- for (const topic of line.topics.keys()) frame(line, `realtime:${topic}`, "phx_join", JOIN);
23990
- line.heart = setInterval(() => frame(line, "phoenix", "heartbeat", {}), HEARTBEAT_MS);
24068
+ for (const [name, t] of line.topics) join3(line, name, t);
24069
+ line.heart = setInterval(() => {
24070
+ if (line.beat) {
24071
+ try {
24072
+ ws.close();
24073
+ } catch {
24074
+ }
24075
+ again();
24076
+ return;
24077
+ }
24078
+ line.beat = frame(line, "phoenix", "heartbeat", {});
24079
+ }, HEARTBEAT_MS);
23991
24080
  });
23992
24081
  ws.addEventListener("message", (e) => {
24082
+ if (gone) return;
23993
24083
  let m;
23994
24084
  try {
23995
24085
  m = JSON.parse(String(e.data));
23996
24086
  } catch {
23997
24087
  return;
23998
24088
  }
23999
- if (m.event !== "broadcast" || !m.topic?.startsWith("realtime:")) return;
24000
- for (const l of line.topics.get(m.topic.slice("realtime:".length)) ?? []) {
24089
+ if (m.topic === "phoenix") {
24090
+ if (m.event === "phx_reply" && m.ref === line.beat) line.beat = void 0;
24091
+ return;
24092
+ }
24093
+ if (!m.topic?.startsWith("realtime:")) return;
24094
+ const name = m.topic.slice("realtime:".length);
24095
+ const t = line.topics.get(name);
24096
+ if (!t) return;
24097
+ if (m.event === "phx_reply") {
24098
+ if (!t.ref || m.ref !== t.ref) return;
24099
+ if (m.payload?.status === "ok") joined(t);
24100
+ else rejoin(line, name, t);
24101
+ return;
24102
+ }
24103
+ if (m.event === "phx_error" || m.event === "phx_close") {
24104
+ if (m.ref && m.ref !== t.ref) return;
24105
+ rejoin(line, name, t);
24106
+ return;
24107
+ }
24108
+ if (m.event !== "broadcast") return;
24109
+ for (const l of t.listeners) {
24001
24110
  if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
24002
24111
  }
24003
24112
  });
24004
- let gone = false;
24005
- const again = () => {
24006
- if (gone) return;
24007
- gone = true;
24008
- stop(line);
24009
- if (!line.topics.size) return;
24010
- line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
24011
- };
24012
24113
  ws.addEventListener("close", again);
24013
24114
  ws.addEventListener("error", again);
24014
24115
  }
@@ -24016,17 +24117,22 @@ function subscribeRealtime(args) {
24016
24117
  const key = `${args.url} ${args.anonKey}`;
24017
24118
  let line = lines.get(key);
24018
24119
  if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
24019
- let heard = line.topics.get(args.topic);
24020
- if (!heard) {
24021
- line.topics.set(args.topic, heard = /* @__PURE__ */ new Set());
24022
- if (line.open) frame(line, `realtime:${args.topic}`, "phx_join", JOIN);
24023
- }
24024
- const me = { event: args.event, onMessage: args.onMessage };
24025
- heard.add(me);
24120
+ let t = line.topics.get(args.topic);
24121
+ if (!t) {
24122
+ line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
24123
+ if (line.open) join3(line, args.topic, t);
24124
+ }
24125
+ const topic = t;
24126
+ const me = { event: args.event, onMessage: args.onMessage, onJoined: args.onJoined, joins: 0 };
24127
+ topic.listeners.add(me);
24128
+ if (topic.joined && me.onJoined) queueMicrotask(() => {
24129
+ if (topic.joined && topic.listeners.has(me)) me.onJoined?.(++me.joins);
24130
+ });
24026
24131
  if (!line.ws) connect(line);
24027
24132
  return {
24028
24133
  close: () => {
24029
- if (!heard.delete(me) || heard.size) return;
24134
+ if (!topic.listeners.delete(me) || topic.listeners.size) return;
24135
+ clearTimeout(topic.timer);
24030
24136
  line.topics.delete(args.topic);
24031
24137
  if (line.topics.size) {
24032
24138
  if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
@@ -24042,7 +24148,8 @@ function subscribeRealtime(args) {
24042
24148
  };
24043
24149
  }
24044
24150
  async function subscribeWake(onNudge, opts = {}) {
24045
- const cfg = await registerDelivery("self_hosted", opts);
24151
+ const { release = true, onJoined, ...client } = opts;
24152
+ const cfg = await registerDelivery("self_hosted", client);
24046
24153
  const { url, anonKey } = cfg.realtime;
24047
24154
  const channel = wakeChannel(cfg.tokenId);
24048
24155
  const ch = subscribeRealtime({
@@ -24050,24 +24157,26 @@ async function subscribeWake(onNudge, opts = {}) {
24050
24157
  anonKey,
24051
24158
  topic: channel,
24052
24159
  event: WAKE_EVENT,
24053
- onMessage: (payload) => onNudge(payload ?? {})
24160
+ onMessage: (payload) => onNudge(payload ?? {}),
24161
+ onJoined
24054
24162
  });
24055
24163
  return {
24056
24164
  tokenId: cfg.tokenId,
24057
24165
  channel,
24058
24166
  close: async () => {
24059
24167
  ch.close();
24060
- await registerDelivery("poll", opts).catch(() => {
24168
+ if (release) await registerDelivery("poll", client).catch(() => {
24061
24169
  });
24062
24170
  }
24063
24171
  };
24064
24172
  }
24065
- var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, GetThreadSchema, GET_THREAD_DESCRIPTION, SearchThreadsSchema, SEARCH_THREADS_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, OptionSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueItemSchema, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, AWAIT_WINDOW_MS, tokenOverride, authToken, HEARTBEAT_MS, RETRY_MS, JOIN, lines;
24173
+ var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24066
24174
  var init_dist = __esm({
24067
24175
  "../../packages/sdk/dist/index.js"() {
24068
24176
  "use strict";
24069
24177
  init_zod();
24070
24178
  init_zod();
24179
+ init_zod();
24071
24180
  init_v3();
24072
24181
  init_v3();
24073
24182
  init_v3();
@@ -24452,28 +24561,40 @@ var init_dist = __esm({
24452
24561
  };
24453
24562
  OPTIONS_MIN = 2;
24454
24563
  OPTIONS_MAX = 6;
24564
+ OptionSchema = external_exports.object({
24565
+ id: external_exports.string(),
24566
+ label: external_exports.string(),
24567
+ 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(),
24568
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
24569
+ // the constraints below are what an agent reads when deciding to use these.
24570
+ html: external_exports.string().max(16384).describe(
24571
+ "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."
24572
+ ).optional(),
24573
+ image: external_exports.string().url().describe(
24574
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
24575
+ ).optional()
24576
+ });
24577
+ OptionInputSchema = OptionSchema.omit({ id: true }).extend({
24578
+ label: external_exports.string().trim().min(1).max(1e3)
24579
+ }).strict();
24580
+ NIGHT = { from: 23, to: 7 };
24455
24581
  AskInputSchema = external_exports.object({
24456
24582
  id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
24457
- parentId: external_exports.string().uuid().optional().describe("Optional explicit parent Goal ID. If omitted, the platform's semantic router will auto-cluster it."),
24583
+ 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."),
24458
24584
  repo: external_exports.string().optional().describe("Optional repository context."),
24459
24585
  ask: external_exports.string().trim().min(1).max(1e4).describe(
24460
- "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."
24586
+ "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
24461
24587
  ),
24462
- options: external_exports.array(external_exports.object({
24463
- label: external_exports.string().trim().min(1).max(1e3),
24464
- hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
24465
- image: external_exports.string().url().optional(),
24466
- 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()
24467
- }).strict()).min(2).max(6).optional()
24588
+ options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
24468
24589
  }).strict();
24469
24590
  StartContactSchema = external_exports.object({
24470
- asks: external_exports.array(AskInputSchema).min(1).describe("The list of asks/questions to pose. They will be semantically clustered by the API."),
24591
+ 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."),
24471
24592
  waiting: external_exports.enum(["none", "hard"]).default("none"),
24472
24593
  channel: external_exports.enum(["notification", "call"]).default("notification")
24473
24594
  }).strict();
24474
24595
  ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
24475
24596
  CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
24476
- 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.";
24597
+ 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}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: on a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so).";
24477
24598
  CreateGoalSchema = external_exports.object({
24478
24599
  outcome: external_exports.string().trim().min(1).max(1e4),
24479
24600
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -24481,10 +24602,6 @@ var init_dist = __esm({
24481
24602
  title: external_exports.string().trim().min(1).max(80).optional(),
24482
24603
  ownerParticipant: external_exports.string().trim().min(1).optional(),
24483
24604
  idempotencyKey: external_exports.string().trim().min(1).max(200),
24484
- /** A past conversation this Goal should be read against — History's "new session from this"
24485
- * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
24486
- * reference only: the owner reads it through `get_thread`, which does its own scoping, and
24487
- * the writer refuses a thread belonging to another account. */
24488
24605
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
24489
24606
  * didn't get created as a child goal of the voice UI goal, which is how it should've
24490
24607
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -24524,23 +24641,13 @@ var init_dist = __esm({
24524
24641
  ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
24525
24642
  GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
24526
24643
  GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
24527
- UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
24528
- 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.";
24529
- 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.";
24644
+ UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
24645
+ CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
24646
+ CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. 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 a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
24530
24647
  CheckRepliesSchema = external_exports.object({}).strict();
24531
- GetThreadSchema = external_exports.object({
24532
- parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
24533
- }).strict();
24534
- 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.";
24535
- SearchThreadsSchema = external_exports.object({
24536
- q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
24537
- }).strict();
24538
- 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.`;
24539
24648
  AGENT_TOOLS = [
24540
24649
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
24541
24650
  { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
24542
- { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
24543
- { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
24544
24651
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
24545
24652
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
24546
24653
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
@@ -24574,19 +24681,6 @@ var init_dist = __esm({
24574
24681
  "summarize"
24575
24682
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
24576
24683
  ]);
24577
- OptionSchema = external_exports.object({
24578
- id: external_exports.string(),
24579
- label: external_exports.string(),
24580
- 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(),
24581
- // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
24582
- // the constraints below are what an agent reads when deciding to use these.
24583
- html: external_exports.string().max(16384).describe(
24584
- "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."
24585
- ).optional(),
24586
- image: external_exports.string().url().describe(
24587
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
24588
- ).optional()
24589
- });
24590
24684
  VisualSchema = external_exports.object({
24591
24685
  url: external_exports.string().url(),
24592
24686
  label: external_exports.string().optional()
@@ -24640,7 +24734,7 @@ var init_dist = __esm({
24640
24734
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
24641
24735
  * superRefine at the bottom enforces exactly one of the two. */
24642
24736
  context: ContextSchema.optional(),
24643
- options: external_exports.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
24737
+ options: external_exports.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
24644
24738
  "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)."
24645
24739
  ),
24646
24740
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -24834,7 +24928,7 @@ var init_dist = __esm({
24834
24928
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
24835
24929
  * post-feedback revision, #633) — the user will never answer this id. Stop
24836
24930
  * awaiting it; the live ask is the thread's newest turn (await that one, or
24837
- * re-orient via get_thread / check_replies). */
24931
+ * re-orient via check_replies). */
24838
24932
  external_exports.object({
24839
24933
  type: external_exports.literal("superseded"),
24840
24934
  parentId: external_exports.string(),
@@ -24879,9 +24973,9 @@ var init_dist = __esm({
24879
24973
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
24880
24974
  * the planner declares duplicates instead of asking them twice. */
24881
24975
  coveredIds: external_exports.array(external_exports.string()).optional(),
24882
- /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
24883
- * line went out on 2026-07-28 and the caller could not answer it at all. */
24884
- info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
24976
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
24977
+ * (owner, 2026-09-25), and a count here refused whole plans. */
24978
+ info: external_exports.array(external_exports.string().min(1)).default([]),
24885
24979
  question: external_exports.string().min(1).nullable(),
24886
24980
  /** True on the one turn carrying the agent's own declared question. */
24887
24981
  asks: external_exports.boolean().optional(),
@@ -24916,42 +25010,12 @@ var init_dist = __esm({
24916
25010
  CLAIM_STALE_MS = 30 * 6e4;
24917
25011
  InboxItemSchema = external_exports.object({
24918
25012
  id: external_exports.string(),
24919
- /** The conversation thread + connection this item lives on. Present on the replied
24920
- * detail — they power History's "Continue" / "New session from this" (#57/#251). */
24921
- parentId: external_exports.string().optional(),
24922
- /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
24923
- * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
24924
- * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
24925
- * accumulates every message an agent ever sent, so grouping by it renders a day of
24926
- * unrelated updates as a single "12-part request". Absent on rows written before the
24927
- * `asks` table, and on anything that never went through `notify` — both fall back to the
24928
- * thread, which is what the client did for all rows until now. */
24929
- askId: external_exports.string().optional(),
24930
- /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
24931
- * shares one `created_at` to the microsecond, so without it the author's order is
24932
- * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
24933
- * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
24934
- * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
24935
- seq: external_exports.number().int().optional(),
24936
- /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
24937
- * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
24938
- * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
24939
- * screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
24940
- * only evidence of itself. How big an arrival is, is a fact about the arrival, so the
24941
- * server that can still see every row states it. Absent on any row with no `askId`: a
24942
- * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
24943
- * the one part of its own arrival a single row cannot answer. */
24944
- units: external_exports.number().int().positive().optional(),
24945
25013
  tokenId: external_exports.string().optional(),
24946
25014
  status: NotifyStatusSchema,
24947
25015
  context: ContextSchema,
24948
25016
  options: external_exports.array(OptionSchema).optional(),
24949
25017
  /** The ask's declared coverage points (#396), when the agent sent them. */
24950
25018
  points: external_exports.array(external_exports.string()).optional(),
24951
- /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
24952
- * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
24953
- * between them could not express a call as a sequence. `question: null` is a real turn —
24954
- * a status update stays a statement instead of being shaped into a yes/no. */
24955
25019
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
24956
25020
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
24957
25021
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
@@ -24972,7 +25036,31 @@ var init_dist = __esm({
24972
25036
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
24973
25037
  * `createdAt`. */
24974
25038
  agentStateAt: external_exports.string().datetime().optional(),
24975
- agenda: external_exports.array(AgendaTurnSchema).optional(),
25039
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
25040
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
25041
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
25042
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
25043
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
25044
+ *
25045
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
25046
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
25047
+ *
25048
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
25049
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
25050
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
25051
+ * REPLACED `agenda` (turns), which nothing ever filled. */
25052
+ questions: external_exports.array(external_exports.object({
25053
+ id: external_exports.string(),
25054
+ entryId: external_exports.string(),
25055
+ title: external_exports.string(),
25056
+ state: external_exports.enum(["open", "answered"]),
25057
+ answer: external_exports.string().nullable(),
25058
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
25059
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
25060
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
25061
+ * author is not an agent this account holds (unpaired since, or a person). */
25062
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional()
25063
+ })).optional(),
24976
25064
  visuals: external_exports.array(VisualSchema).optional(),
24977
25065
  /** The connected agent's name (the single pairing name — user-typed, or the
24978
25066
  * agent's suggestion, or a default silly name). */
@@ -24989,16 +25077,35 @@ var init_dist = __esm({
24989
25077
  * agent (held in the inbox until the agent replies on the thread). */
24990
25078
  turn: external_exports.enum(["you", "agent"]).default("you"),
24991
25079
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
24992
- * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
24993
- * client may still flag a stall by age. Drives the inbox error badge + Retry. */
25080
+ * the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
25081
+ * error badge + Retry. */
24994
25082
  error: external_exports.string().optional(),
25083
+ /** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
25084
+ * with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
25085
+ * this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
25086
+ * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
25087
+ cold: external_exports.string().datetime().optional(),
24995
25088
  clarifies: external_exports.string().optional(),
24996
- /** The ring ladder ran out while this was still pending — we tried to reach you and
24997
- * STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
24998
- * agent with nothing to say, which the roster drew identically until now: "nothing to
24999
- * say" and "gave up saying it" are opposite situations wearing the same face
25000
- * (navigation-design.md, gap 1). False for anything that never rang. */
25001
- gaveUp: external_exports.boolean().default(false),
25089
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
25090
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
25091
+ * off the same open list the card came from, so it clears when the Call does. A card is the
25092
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
25093
+ onCall: external_exports.literal(true).optional(),
25094
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
25095
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
25096
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
25097
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
25098
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
25099
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
25100
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
25101
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
25102
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
25103
+ ring: external_exports.object({
25104
+ declinedAt: external_exports.string().datetime(),
25105
+ anchorAt: external_exports.string().datetime(),
25106
+ nextRingAt: external_exports.string().datetime().nullable(),
25107
+ step: external_exports.number().int()
25108
+ }).optional(),
25002
25109
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
25003
25110
  * Absent for anything never delivered through a push, and for older rows written before
25004
25111
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
@@ -25032,7 +25139,10 @@ var init_dist = __esm({
25032
25139
  goalIds: external_exports.array(external_exports.string()),
25033
25140
  decisionNeedId: external_exports.string().optional(),
25034
25141
  sealed: external_exports.boolean(),
25035
- goalState: external_exports.string().optional()
25142
+ goalState: external_exports.string().optional(),
25143
+ /** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
25144
+ * and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
25145
+ goalTitle: external_exports.string().optional()
25036
25146
  }).optional()
25037
25147
  });
25038
25148
  APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
@@ -25063,6 +25173,8 @@ var init_dist = __esm({
25063
25173
  "inbox",
25064
25174
  "dismiss"
25065
25175
  ]);
25176
+ clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
25177
+ QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
25066
25178
  BrokerTuningSchema = external_exports.object({
25067
25179
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
25068
25180
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -25097,6 +25209,12 @@ var init_dist = __esm({
25097
25209
  * must not silently reset this privacy choice. Absent = leave unchanged on
25098
25210
  * write, 'hosted' on read (see store.ts). */
25099
25211
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
25212
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
25213
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
25214
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
25215
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
25216
+ * read (see store.ts). */
25217
+ talk: external_exports.boolean().optional(),
25100
25218
  /** Per-user ring budget (#603): calls per rolling day before further calls
25101
25219
  * degrade to banner. Absent = the global default (25). A number, never a
25102
25220
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -25123,7 +25241,6 @@ var init_dist = __esm({
25123
25241
  });
25124
25242
  HistoryItemSchema = external_exports.object({
25125
25243
  id: external_exports.string(),
25126
- parentId: external_exports.string(),
25127
25244
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
25128
25245
  initiator: external_exports.enum(["user", "agent"]),
25129
25246
  title: external_exports.string(),
@@ -25282,6 +25399,24 @@ var init_dist = __esm({
25282
25399
  * gate at all. */
25283
25400
  blocked: external_exports.boolean().default(false)
25284
25401
  });
25402
+ QueueReplySchema = external_exports.object({
25403
+ /** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
25404
+ * id) — so the phone can tell a reply it just sent from one the queue already carries, and the
25405
+ * walk can name it in its zoom. */
25406
+ id: external_exports.string(),
25407
+ /** The Goal the note is on. */
25408
+ goalId: external_exports.string(),
25409
+ /** What the note said. */
25410
+ note: external_exports.string(),
25411
+ /** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
25412
+ deliveryId: external_exports.string(),
25413
+ requestEntryId: external_exports.string(),
25414
+ askedAt: external_exports.string(),
25415
+ /** When the person last replied — the window's start. */
25416
+ repliedAt: external_exports.string(),
25417
+ /** The person's latest words about it; null when there is nothing readable in them. */
25418
+ reply: external_exports.string().nullable()
25419
+ });
25285
25420
  QueueItemSchema = external_exports.object({
25286
25421
  id: external_exports.string(),
25287
25422
  /** One-line headline — the first sentence of the outcome. */
@@ -25294,6 +25429,9 @@ var init_dist = __esm({
25294
25429
  assignee: external_exports.string().nullable().default(null),
25295
25430
  /** What the agent last said it was doing; null if it has said nothing. */
25296
25431
  progress: external_exports.string().nullable().default(null),
25432
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
25433
+ * served only while it was written for the current note. Null means show the Goal's name. */
25434
+ progressLine: external_exports.string().nullable().optional(),
25297
25435
  reviewPending: external_exports.boolean().default(false),
25298
25436
  dueAt: external_exports.string().nullable().default(null),
25299
25437
  /** The Goal this one was opened under; null at the root. */
@@ -25306,11 +25444,20 @@ var init_dist = __esm({
25306
25444
  blocked: external_exports.boolean().default(false),
25307
25445
  /** Every decision need on it, open or settled — the page decides which to show. */
25308
25446
  questions: external_exports.array(QueueQuestionSchema).default([]),
25447
+ /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
25448
+ * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
25449
+ * not spell an empty list. */
25450
+ replies: external_exports.array(QueueReplySchema).optional(),
25309
25451
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
25310
25452
  repo: external_exports.string().nullable().optional(),
25311
25453
  createdAt: external_exports.string(),
25312
- updatedAt: external_exports.string().nullable().default(null)
25454
+ updatedAt: external_exports.string().nullable().default(null),
25455
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
25456
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
25457
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
25458
+ lastProgressAt: external_exports.string().nullable().optional()
25313
25459
  });
25460
+ COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
25314
25461
  NoteSourceSchema = external_exports.enum(["app", "call"]);
25315
25462
  NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
25316
25463
  NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
@@ -25510,12 +25657,14 @@ var init_dist = __esm({
25510
25657
  this.body = body;
25511
25658
  }
25512
25659
  };
25513
- AWAIT_WINDOW_MS = 45e3;
25514
25660
  tokenOverride = null;
25515
25661
  authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
25662
+ AWAIT_WINDOW_MS = 45e3;
25663
+ NOT_SENT = "Recorded as progress, not sent: nobody was notified. If the person should see it (a result, something to try or check, a finding), contact again and say so; report progress with update_goal.";
25516
25664
  HEARTBEAT_MS = 25e3;
25517
25665
  RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
25518
25666
  JOIN = { config: { broadcast: { self: false } } };
25667
+ JOIN_TIMEOUT_MS = 1e4;
25519
25668
  lines = /* @__PURE__ */ new Map();
25520
25669
  }
25521
25670
  });
@@ -30126,9 +30275,9 @@ async function available(deps = {}) {
30126
30275
  });
30127
30276
  return [ollama, ...harnesses];
30128
30277
  }
30129
- function refusal(options, asked) {
30278
+ function refusal(options, asked2) {
30130
30279
  const lines2 = [
30131
- 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.",
30280
+ 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.",
30132
30281
  "",
30133
30282
  "Triage runs the model where your queue already lives (#1106). Your options:"
30134
30283
  ];
@@ -30139,10 +30288,10 @@ function refusal(options, asked) {
30139
30288
  lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
30140
30289
  return lines2.join("\n");
30141
30290
  }
30142
- async function judgeFor(asked, deps = {}) {
30291
+ async function judgeFor(asked2, deps = {}) {
30143
30292
  const options = await available(deps);
30144
- const chosen = asked ? options.find((o) => o.provider === asked) : options.find((o) => o.ready);
30145
- if (!chosen?.ready) return { error: refusal(options, asked) };
30293
+ const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
30294
+ if (!chosen?.ready) return { error: refusal(options, asked2) };
30146
30295
  if (chosen.provider === "ollama") {
30147
30296
  const model = chosen.model;
30148
30297
  return {
@@ -30163,9 +30312,9 @@ async function judgeFor(asked, deps = {}) {
30163
30312
  };
30164
30313
  }
30165
30314
  const runner = RUNNERS[chosen.provider];
30166
- if (!runner) return { error: refusal(options, asked) };
30315
+ if (!runner) return { error: refusal(options, asked2) };
30167
30316
  const bin = which(runner.cli);
30168
- if (!bin) return { error: refusal(options, asked) };
30317
+ if (!bin) return { error: refusal(options, asked2) };
30169
30318
  return {
30170
30319
  provider: chosen.provider,
30171
30320
  model: null,
@@ -30764,7 +30913,7 @@ import { hostname } from "os";
30764
30913
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
30765
30914
  import { execFile as execFile2, execSync } from "child_process";
30766
30915
  import { homedir as homedir8 } from "os";
30767
- import { join as join7 } from "path";
30916
+ import { join as join8 } from "path";
30768
30917
 
30769
30918
  // src/run.ts
30770
30919
  init_dist();
@@ -31024,7 +31173,13 @@ function startSession(opts) {
31024
31173
  ...process.env,
31025
31174
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
31026
31175
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
31027
- ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
31176
+ ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
31177
+ // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
31178
+ // bridge's pump typing answers in, so nothing inside it may start a second delivery path.
31179
+ // Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
31180
+ // fresh spawn has none, which is how a fresh run read as "not harness-started" to the
31181
+ // session-start hook and the `listen` tool (they now test either mark; listening.ts).
31182
+ PAIGY_HARNESS: "1"
31028
31183
  },
31029
31184
  stdio: ["pipe", "pipe", "pipe"]
31030
31185
  });
@@ -31073,6 +31228,7 @@ function startSession(opts) {
31073
31228
  // ../../packages/schema/dist/index.js
31074
31229
  init_zod();
31075
31230
  init_zod();
31231
+ init_zod();
31076
31232
 
31077
31233
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
31078
31234
  var ignoreOverride2 = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
@@ -32369,6 +32525,23 @@ var zodToJsonSchema2 = (schema, options) => {
32369
32525
  init_zod();
32370
32526
  var OPTIONS_MIN2 = 2;
32371
32527
  var OPTIONS_MAX2 = 6;
32528
+ var OptionSchema2 = external_exports.object({
32529
+ id: external_exports.string(),
32530
+ label: external_exports.string(),
32531
+ 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(),
32532
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
32533
+ // the constraints below are what an agent reads when deciding to use these.
32534
+ html: external_exports.string().max(16384).describe(
32535
+ "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."
32536
+ ).optional(),
32537
+ image: external_exports.string().url().describe(
32538
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
32539
+ ).optional()
32540
+ });
32541
+ var OptionInputSchema2 = OptionSchema2.omit({ id: true }).extend({
32542
+ label: external_exports.string().trim().min(1).max(1e3)
32543
+ }).strict();
32544
+ var NIGHT2 = { from: 23, to: 7 };
32372
32545
  function draft20202(node) {
32373
32546
  if (Array.isArray(node)) return node.map(draft20202);
32374
32547
  if (node && typeof node === "object") {
@@ -32400,26 +32573,21 @@ function mcpInputSchema2(s) {
32400
32573
  }
32401
32574
  var AskInputSchema2 = external_exports.object({
32402
32575
  id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
32403
- parentId: external_exports.string().uuid().optional().describe("Optional explicit parent Goal ID. If omitted, the platform's semantic router will auto-cluster it."),
32576
+ 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."),
32404
32577
  repo: external_exports.string().optional().describe("Optional repository context."),
32405
32578
  ask: external_exports.string().trim().min(1).max(1e4).describe(
32406
- "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."
32579
+ "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
32407
32580
  ),
32408
- options: external_exports.array(external_exports.object({
32409
- label: external_exports.string().trim().min(1).max(1e3),
32410
- hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
32411
- image: external_exports.string().url().optional(),
32412
- 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()
32413
- }).strict()).min(2).max(6).optional()
32581
+ options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
32414
32582
  }).strict();
32415
32583
  var StartContactSchema2 = external_exports.object({
32416
- asks: external_exports.array(AskInputSchema2).min(1).describe("The list of asks/questions to pose. They will be semantically clustered by the API."),
32584
+ 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."),
32417
32585
  waiting: external_exports.enum(["none", "hard"]).default("none"),
32418
32586
  channel: external_exports.enum(["notification", "call"]).default("notification")
32419
32587
  }).strict();
32420
32588
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
32421
32589
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
32422
- 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.";
32590
+ 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}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: on a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so).";
32423
32591
  var CreateGoalSchema2 = external_exports.object({
32424
32592
  outcome: external_exports.string().trim().min(1).max(1e4),
32425
32593
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -32427,10 +32595,6 @@ var CreateGoalSchema2 = external_exports.object({
32427
32595
  title: external_exports.string().trim().min(1).max(80).optional(),
32428
32596
  ownerParticipant: external_exports.string().trim().min(1).optional(),
32429
32597
  idempotencyKey: external_exports.string().trim().min(1).max(200),
32430
- /** A past conversation this Goal should be read against — History's "new session from this"
32431
- * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
32432
- * reference only: the owner reads it through `get_thread`, which does its own scoping, and
32433
- * the writer refuses a thread belonging to another account. */
32434
32598
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
32435
32599
  * didn't get created as a child goal of the voice UI goal, which is how it should've
32436
32600
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -32470,23 +32634,13 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
32470
32634
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
32471
32635
  var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
32472
32636
  var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
32473
- var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
32474
- 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.";
32475
- 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.";
32637
+ var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
32638
+ var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
32639
+ var CHECK_REPLIES_DESCRIPTION2 = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. 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 a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
32476
32640
  var CheckRepliesSchema2 = external_exports.object({}).strict();
32477
- var GetThreadSchema2 = external_exports.object({
32478
- parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
32479
- }).strict();
32480
- 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.";
32481
- var SearchThreadsSchema2 = external_exports.object({
32482
- q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
32483
- }).strict();
32484
- 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.`;
32485
32641
  var AGENT_TOOLS2 = [
32486
32642
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
32487
32643
  { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
32488
- { name: "get_thread", description: GET_THREAD_DESCRIPTION2, inputSchema: mcpInputSchema2(GetThreadSchema2) },
32489
- { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchThreadsSchema2) },
32490
32644
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
32491
32645
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
32492
32646
  { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
@@ -32535,19 +32689,6 @@ var TransformSchema2 = external_exports.enum([
32535
32689
  "summarize"
32536
32690
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
32537
32691
  ]);
32538
- var OptionSchema2 = external_exports.object({
32539
- id: external_exports.string(),
32540
- label: external_exports.string(),
32541
- 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(),
32542
- // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
32543
- // the constraints below are what an agent reads when deciding to use these.
32544
- html: external_exports.string().max(16384).describe(
32545
- "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."
32546
- ).optional(),
32547
- image: external_exports.string().url().describe(
32548
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
32549
- ).optional()
32550
- });
32551
32692
  var VisualSchema2 = external_exports.object({
32552
32693
  url: external_exports.string().url(),
32553
32694
  label: external_exports.string().optional()
@@ -32601,7 +32742,7 @@ var NotifyRequestFields2 = external_exports.object({
32601
32742
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
32602
32743
  * superRefine at the bottom enforces exactly one of the two. */
32603
32744
  context: ContextSchema2.optional(),
32604
- options: external_exports.array(OptionSchema2.omit({ id: true })).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
32745
+ options: external_exports.array(OptionInputSchema2).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
32605
32746
  "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)."
32606
32747
  ),
32607
32748
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -32795,7 +32936,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
32795
32936
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
32796
32937
  * post-feedback revision, #633) — the user will never answer this id. Stop
32797
32938
  * awaiting it; the live ask is the thread's newest turn (await that one, or
32798
- * re-orient via get_thread / check_replies). */
32939
+ * re-orient via check_replies). */
32799
32940
  external_exports.object({
32800
32941
  type: external_exports.literal("superseded"),
32801
32942
  parentId: external_exports.string(),
@@ -32840,9 +32981,9 @@ var AgendaTurnSchema2 = external_exports.object({
32840
32981
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
32841
32982
  * the planner declares duplicates instead of asking them twice. */
32842
32983
  coveredIds: external_exports.array(external_exports.string()).optional(),
32843
- /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
32844
- * line went out on 2026-07-28 and the caller could not answer it at all. */
32845
- info: external_exports.array(external_exports.string().min(1)).max(3).default([]),
32984
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
32985
+ * (owner, 2026-09-25), and a count here refused whole plans. */
32986
+ info: external_exports.array(external_exports.string().min(1)).default([]),
32846
32987
  question: external_exports.string().min(1).nullable(),
32847
32988
  /** True on the one turn carrying the agent's own declared question. */
32848
32989
  asks: external_exports.boolean().optional(),
@@ -32877,42 +33018,12 @@ var AgendaTurnSchema2 = external_exports.object({
32877
33018
  var CLAIM_STALE_MS2 = 30 * 6e4;
32878
33019
  var InboxItemSchema2 = external_exports.object({
32879
33020
  id: external_exports.string(),
32880
- /** The conversation thread + connection this item lives on. Present on the replied
32881
- * detail — they power History's "Continue" / "New session from this" (#57/#251). */
32882
- parentId: external_exports.string().optional(),
32883
- /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
32884
- * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
32885
- * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
32886
- * accumulates every message an agent ever sent, so grouping by it renders a day of
32887
- * unrelated updates as a single "12-part request". Absent on rows written before the
32888
- * `asks` table, and on anything that never went through `notify` — both fall back to the
32889
- * thread, which is what the client did for all rows until now. */
32890
- askId: external_exports.string().optional(),
32891
- /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
32892
- * shares one `created_at` to the microsecond, so without it the author's order is
32893
- * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
32894
- * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
32895
- * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
32896
- seq: external_exports.number().int().optional(),
32897
- /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
32898
- * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
32899
- * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
32900
- * screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
32901
- * only evidence of itself. How big an arrival is, is a fact about the arrival, so the
32902
- * server that can still see every row states it. Absent on any row with no `askId`: a
32903
- * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
32904
- * the one part of its own arrival a single row cannot answer. */
32905
- units: external_exports.number().int().positive().optional(),
32906
33021
  tokenId: external_exports.string().optional(),
32907
33022
  status: NotifyStatusSchema2,
32908
33023
  context: ContextSchema2,
32909
33024
  options: external_exports.array(OptionSchema2).optional(),
32910
33025
  /** The ask's declared coverage points (#396), when the agent sent them. */
32911
33026
  points: external_exports.array(external_exports.string()).optional(),
32912
- /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
32913
- * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
32914
- * between them could not express a call as a sequence. `question: null` is a real turn —
32915
- * a status update stays a statement instead of being shaped into a yes/no. */
32916
33027
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
32917
33028
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
32918
33029
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
@@ -32933,7 +33044,31 @@ var InboxItemSchema2 = external_exports.object({
32933
33044
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
32934
33045
  * `createdAt`. */
32935
33046
  agentStateAt: external_exports.string().datetime().optional(),
32936
- agenda: external_exports.array(AgendaTurnSchema2).optional(),
33047
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
33048
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
33049
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
33050
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
33051
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
33052
+ *
33053
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
33054
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
33055
+ *
33056
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
33057
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
33058
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
33059
+ * REPLACED `agenda` (turns), which nothing ever filled. */
33060
+ questions: external_exports.array(external_exports.object({
33061
+ id: external_exports.string(),
33062
+ entryId: external_exports.string(),
33063
+ title: external_exports.string(),
33064
+ state: external_exports.enum(["open", "answered"]),
33065
+ answer: external_exports.string().nullable(),
33066
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
33067
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
33068
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
33069
+ * author is not an agent this account holds (unpaired since, or a person). */
33070
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional()
33071
+ })).optional(),
32937
33072
  visuals: external_exports.array(VisualSchema2).optional(),
32938
33073
  /** The connected agent's name (the single pairing name — user-typed, or the
32939
33074
  * agent's suggestion, or a default silly name). */
@@ -32950,16 +33085,35 @@ var InboxItemSchema2 = external_exports.object({
32950
33085
  * agent (held in the inbox until the agent replies on the thread). */
32951
33086
  turn: external_exports.enum(["you", "agent"]).default("you"),
32952
33087
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
32953
- * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
32954
- * client may still flag a stall by age. Drives the inbox error badge + Retry. */
33088
+ * the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
33089
+ * error badge + Retry. */
32955
33090
  error: external_exports.string().optional(),
33091
+ /** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
33092
+ * with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
33093
+ * this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
33094
+ * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
33095
+ cold: external_exports.string().datetime().optional(),
32956
33096
  clarifies: external_exports.string().optional(),
32957
- /** The ring ladder ran out while this was still pending — we tried to reach you and
32958
- * STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
32959
- * agent with nothing to say, which the roster drew identically until now: "nothing to
32960
- * say" and "gave up saying it" are opposite situations wearing the same face
32961
- * (navigation-design.md, gap 1). False for anything that never rang. */
32962
- gaveUp: external_exports.boolean().default(false),
33097
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
33098
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
33099
+ * off the same open list the card came from, so it clears when the Call does. A card is the
33100
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
33101
+ onCall: external_exports.literal(true).optional(),
33102
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
33103
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
33104
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
33105
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
33106
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
33107
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
33108
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
33109
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
33110
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
33111
+ ring: external_exports.object({
33112
+ declinedAt: external_exports.string().datetime(),
33113
+ anchorAt: external_exports.string().datetime(),
33114
+ nextRingAt: external_exports.string().datetime().nullable(),
33115
+ step: external_exports.number().int()
33116
+ }).optional(),
32963
33117
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
32964
33118
  * Absent for anything never delivered through a push, and for older rows written before
32965
33119
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
@@ -32993,7 +33147,10 @@ var InboxItemSchema2 = external_exports.object({
32993
33147
  goalIds: external_exports.array(external_exports.string()),
32994
33148
  decisionNeedId: external_exports.string().optional(),
32995
33149
  sealed: external_exports.boolean(),
32996
- goalState: external_exports.string().optional()
33150
+ goalState: external_exports.string().optional(),
33151
+ /** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
33152
+ * and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
33153
+ goalTitle: external_exports.string().optional()
32997
33154
  }).optional()
32998
33155
  });
32999
33156
  var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
@@ -33024,6 +33181,8 @@ var MissedCallSchema2 = external_exports.enum([
33024
33181
  "inbox",
33025
33182
  "dismiss"
33026
33183
  ]);
33184
+ var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
33185
+ var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
33027
33186
  var BrokerTuningSchema2 = external_exports.object({
33028
33187
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
33029
33188
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -33058,6 +33217,12 @@ var UserSettingsSchema2 = external_exports.object({
33058
33217
  * must not silently reset this privacy choice. Absent = leave unchanged on
33059
33218
  * write, 'hosted' on read (see store.ts). */
33060
33219
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
33220
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
33221
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
33222
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
33223
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
33224
+ * read (see store.ts). */
33225
+ talk: external_exports.boolean().optional(),
33061
33226
  /** Per-user ring budget (#603): calls per rolling day before further calls
33062
33227
  * degrade to banner. Absent = the global default (25). A number, never a
33063
33228
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -33084,7 +33249,6 @@ var UserSettingsSchema2 = external_exports.object({
33084
33249
  });
33085
33250
  var HistoryItemSchema2 = external_exports.object({
33086
33251
  id: external_exports.string(),
33087
- parentId: external_exports.string(),
33088
33252
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
33089
33253
  initiator: external_exports.enum(["user", "agent"]),
33090
33254
  title: external_exports.string(),
@@ -33243,6 +33407,24 @@ var QueueQuestionSchema2 = external_exports.object({
33243
33407
  * gate at all. */
33244
33408
  blocked: external_exports.boolean().default(false)
33245
33409
  });
33410
+ var QueueReplySchema2 = external_exports.object({
33411
+ /** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
33412
+ * id) — so the phone can tell a reply it just sent from one the queue already carries, and the
33413
+ * walk can name it in its zoom. */
33414
+ id: external_exports.string(),
33415
+ /** The Goal the note is on. */
33416
+ goalId: external_exports.string(),
33417
+ /** What the note said. */
33418
+ note: external_exports.string(),
33419
+ /** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
33420
+ deliveryId: external_exports.string(),
33421
+ requestEntryId: external_exports.string(),
33422
+ askedAt: external_exports.string(),
33423
+ /** When the person last replied — the window's start. */
33424
+ repliedAt: external_exports.string(),
33425
+ /** The person's latest words about it; null when there is nothing readable in them. */
33426
+ reply: external_exports.string().nullable()
33427
+ });
33246
33428
  var QueueItemSchema2 = external_exports.object({
33247
33429
  id: external_exports.string(),
33248
33430
  /** One-line headline — the first sentence of the outcome. */
@@ -33255,6 +33437,9 @@ var QueueItemSchema2 = external_exports.object({
33255
33437
  assignee: external_exports.string().nullable().default(null),
33256
33438
  /** What the agent last said it was doing; null if it has said nothing. */
33257
33439
  progress: external_exports.string().nullable().default(null),
33440
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
33441
+ * served only while it was written for the current note. Null means show the Goal's name. */
33442
+ progressLine: external_exports.string().nullable().optional(),
33258
33443
  reviewPending: external_exports.boolean().default(false),
33259
33444
  dueAt: external_exports.string().nullable().default(null),
33260
33445
  /** The Goal this one was opened under; null at the root. */
@@ -33267,11 +33452,20 @@ var QueueItemSchema2 = external_exports.object({
33267
33452
  blocked: external_exports.boolean().default(false),
33268
33453
  /** Every decision need on it, open or settled — the page decides which to show. */
33269
33454
  questions: external_exports.array(QueueQuestionSchema2).default([]),
33455
+ /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
33456
+ * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
33457
+ * not spell an empty list. */
33458
+ replies: external_exports.array(QueueReplySchema2).optional(),
33270
33459
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
33271
33460
  repo: external_exports.string().nullable().optional(),
33272
33461
  createdAt: external_exports.string(),
33273
- updatedAt: external_exports.string().nullable().default(null)
33462
+ updatedAt: external_exports.string().nullable().default(null),
33463
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
33464
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
33465
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
33466
+ lastProgressAt: external_exports.string().nullable().optional()
33274
33467
  });
33468
+ var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
33275
33469
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
33276
33470
  var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
33277
33471
  var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
@@ -33630,6 +33824,7 @@ async function ask(state, rail, input) {
33630
33824
  waiting: "hard",
33631
33825
  channel: input.call ? "call" : "notification"
33632
33826
  });
33827
+ if ("recorded" in sent) return null;
33633
33828
  const entryId = sent.requestEntryIds[0];
33634
33829
  if (!entryId) return null;
33635
33830
  (state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
@@ -33808,14 +34003,14 @@ init_dist();
33808
34003
  init_catalog();
33809
34004
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
33810
34005
  import { homedir as homedir6 } from "os";
33811
- import { join as join6 } from "path";
34006
+ import { join as join7 } from "path";
33812
34007
 
33813
34008
  // src/workspaces.ts
33814
34009
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
33815
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
34010
+ import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
33816
34011
  import { homedir as homedir4 } from "os";
33817
34012
  function workspacesFile() {
33818
- return join3(homedir4(), ".paigy", "workspaces.json");
34013
+ return join4(homedir4(), ".paigy", "workspaces.json");
33819
34014
  }
33820
34015
  function listWorkspaces(deps) {
33821
34016
  try {
@@ -33850,7 +34045,7 @@ function resolveWakeDir(pinned, deps) {
33850
34045
  // src/owner.ts
33851
34046
  import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
33852
34047
  import { homedir as homedir5 } from "os";
33853
- import { join as join4 } from "path";
34048
+ import { join as join5 } from "path";
33854
34049
  var HEAD_BYTES = 64e3;
33855
34050
  function readHead(file) {
33856
34051
  const fd = openSync2(file, "r");
@@ -33866,7 +34061,7 @@ function ownerSession(slot, deps = {}) {
33866
34061
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
33867
34062
  if (!m) return null;
33868
34063
  const prefix = m[1];
33869
- const root = deps.projectsDir ?? join4(homedir5(), ".claude", "projects");
34064
+ const root = deps.projectsDir ?? join5(homedir5(), ".claude", "projects");
33870
34065
  const list = deps.list ?? ((dir) => readdirSync(dir));
33871
34066
  const head = deps.head ?? readHead;
33872
34067
  let folders;
@@ -33878,7 +34073,7 @@ function ownerSession(slot, deps = {}) {
33878
34073
  for (const folder of folders) {
33879
34074
  let files;
33880
34075
  try {
33881
- files = list(join4(root, folder));
34076
+ files = list(join5(root, folder));
33882
34077
  } catch {
33883
34078
  continue;
33884
34079
  }
@@ -33886,7 +34081,7 @@ function ownerSession(slot, deps = {}) {
33886
34081
  if (!file) continue;
33887
34082
  let text;
33888
34083
  try {
33889
- text = head(join4(root, folder, file));
34084
+ text = head(join5(root, folder, file));
33890
34085
  } catch {
33891
34086
  continue;
33892
34087
  }
@@ -33906,7 +34101,7 @@ function ownerSession(slot, deps = {}) {
33906
34101
  // src/update.ts
33907
34102
  import { execFile } from "child_process";
33908
34103
  import { createRequire } from "module";
33909
- import { dirname as dirname3, join as join5 } from "path";
34104
+ import { dirname as dirname3, join as join6 } from "path";
33910
34105
  import { promisify } from "util";
33911
34106
  var SERVICE_LABEL = "ai.paigy.harness";
33912
34107
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -33933,7 +34128,7 @@ async function latestVersion() {
33933
34128
  }
33934
34129
  async function install(version) {
33935
34130
  const bin = dirname3(process.execPath);
33936
- await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34131
+ await promisify(execFile)(join6(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
33937
34132
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
33938
34133
  timeout: 5 * 6e4
33939
34134
  });
@@ -33984,7 +34179,7 @@ function selfUpdater(deps) {
33984
34179
  }
33985
34180
 
33986
34181
  // src/host.ts
33987
- var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
34182
+ var HOST_FILE = join7(homedir6(), ".paigy", "host.json");
33988
34183
  var runKey = (slot) => `slot:${slot}`;
33989
34184
  var claimedRun = (sessionId2) => {
33990
34185
  const slot = sessionSlot(sessionId2);
@@ -33995,9 +34190,9 @@ async function slotHasWaitingWork(token, session) {
33995
34190
  }
33996
34191
  var PRESENCE_FRESH_MS = 3 * 6e4;
33997
34192
  function wakeAction(live, now, serverSeenAt) {
33998
- if (live) return "skip";
33999
- if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
34000
- return "spawn";
34193
+ if (live) return { action: "skip", why: "run" };
34194
+ if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
34195
+ return { action: "spawn", why: "nobody" };
34001
34196
  }
34002
34197
  function startHost(opts) {
34003
34198
  const runs = /* @__PURE__ */ new Map();
@@ -34087,7 +34282,11 @@ function startHost(opts) {
34087
34282
  const live = runs.get(key);
34088
34283
  const tokenId = slotIdentity(slot).tokenId;
34089
34284
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
34090
- if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
34285
+ const decided = wakeAction(!!live, Date.now(), seenByServer);
34286
+ if (decided.action === "skip") {
34287
+ 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`);
34288
+ return;
34289
+ }
34091
34290
  const token = readToken(slot);
34092
34291
  if (!token) return;
34093
34292
  const owner = ownerSession(slot);
@@ -34346,7 +34545,7 @@ async function pairThisMachine() {
34346
34545
  }
34347
34546
  }
34348
34547
  function forwardCodexThreadId() {
34349
- const cfg = join7(process.env.CODEX_HOME || join7(homedir8(), ".codex"), "config.toml");
34548
+ const cfg = join8(process.env.CODEX_HOME || join8(homedir8(), ".codex"), "config.toml");
34350
34549
  if (!existsSync5(cfg)) return;
34351
34550
  const text = readFileSync5(cfg, "utf8");
34352
34551
  const head = "[mcp_servers.paigy]\n";
@@ -34448,7 +34647,7 @@ async function main() {
34448
34647
  const wsDeps = { file: workspacesFile() };
34449
34648
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
34450
34649
  if (!listWorkspaces(wsDeps).length) {
34451
- const guess = join7(homedir8(), "projects");
34650
+ const guess = join8(homedir8(), "projects");
34452
34651
  if (existsSync5(guess)) {
34453
34652
  addWorkspace(guess, wsDeps);
34454
34653
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -34489,7 +34688,7 @@ async function main() {
34489
34688
  }
34490
34689
  }
34491
34690
  try {
34492
- const settings = join7(homedir8(), ".claude", "settings.json");
34691
+ const settings = join8(homedir8(), ".claude", "settings.json");
34493
34692
  if (which("claude") && existsSync5(settings)) {
34494
34693
  const cfg = JSON.parse(readFileSync5(settings, "utf8"));
34495
34694
  if (!cfg["statusLine"]) {
@@ -34547,13 +34746,13 @@ async function main() {
34547
34746
  <key>SoftResourceLimits</key><dict>
34548
34747
  <key>NumberOfFiles</key><integer>65536</integer>
34549
34748
  </dict>
34550
- <key>StandardOutPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
34551
- <key>StandardErrorPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
34749
+ <key>StandardOutPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34750
+ <key>StandardErrorPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34552
34751
  </dict></plist>
34553
34752
  `;
34554
- const dir = join7(homedir8(), "Library", "LaunchAgents");
34753
+ const dir = join8(homedir8(), "Library", "LaunchAgents");
34555
34754
  mkdirSync3(dir, { recursive: true });
34556
- const path = join7(dir, `${SERVICE_LABEL}.plist`);
34755
+ const path = join8(dir, `${SERVICE_LABEL}.plist`);
34557
34756
  writeFileSync4(path, plist);
34558
34757
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
34559
34758
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);