@paigy/harness 0.3.14 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +216 -121
  2. package/dist/main.js +212 -118
  3. package/package.json +1 -1
package/dist/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();
@@ -23917,7 +23943,11 @@ async function contact(input, opts = {}) {
23917
23943
  const reqAsks = parsed.asks.map((a) => ({
23918
23944
  id: a.id,
23919
23945
  parentId: a.parentId,
23920
- repo: a.repo,
23946
+ // Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
23947
+ // work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
23948
+ // still wins — normalised the same way — because a caller that names one knows something
23949
+ // this cannot see.
23950
+ repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
23921
23951
  ask: a.ask,
23922
23952
  options: a.options ?? []
23923
23953
  }));
@@ -23939,10 +23969,12 @@ async function contact(input, opts = {}) {
23939
23969
  deliveryId = deliveries[0]?.deliveryId ?? "";
23940
23970
  if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
23941
23971
  }
23972
+ const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId);
23942
23973
  const read = async (signal2) => {
23943
23974
  const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23944
23975
  if (!res.ok) await fail("read_delivery", res);
23945
- return await res.json();
23976
+ const one = await res.json();
23977
+ return all.length > 1 ? { ...one, deliveries: all } : one;
23946
23978
  };
23947
23979
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
23948
23980
  if (opts.waits === false) return read(opts.signal);
@@ -24042,7 +24074,8 @@ function subscribeRealtime(args) {
24042
24074
  };
24043
24075
  }
24044
24076
  async function subscribeWake(onNudge, opts = {}) {
24045
- const cfg = await registerDelivery("self_hosted", opts);
24077
+ const { release = true, ...client } = opts;
24078
+ const cfg = await registerDelivery("self_hosted", client);
24046
24079
  const { url, anonKey } = cfg.realtime;
24047
24080
  const channel = wakeChannel(cfg.tokenId);
24048
24081
  const ch = subscribeRealtime({
@@ -24057,17 +24090,18 @@ async function subscribeWake(onNudge, opts = {}) {
24057
24090
  channel,
24058
24091
  close: async () => {
24059
24092
  ch.close();
24060
- await registerDelivery("poll", opts).catch(() => {
24093
+ if (release) await registerDelivery("poll", client).catch(() => {
24061
24094
  });
24062
24095
  }
24063
24096
  };
24064
24097
  }
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;
24098
+ 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, 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, HEARTBEAT_MS, RETRY_MS, JOIN, lines;
24066
24099
  var init_dist = __esm({
24067
24100
  "../../packages/sdk/dist/index.js"() {
24068
24101
  "use strict";
24069
24102
  init_zod();
24070
24103
  init_zod();
24104
+ init_zod();
24071
24105
  init_v3();
24072
24106
  init_v3();
24073
24107
  init_v3();
@@ -24452,28 +24486,40 @@ var init_dist = __esm({
24452
24486
  };
24453
24487
  OPTIONS_MIN = 2;
24454
24488
  OPTIONS_MAX = 6;
24489
+ OptionSchema = external_exports.object({
24490
+ id: external_exports.string(),
24491
+ label: external_exports.string(),
24492
+ 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(),
24493
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
24494
+ // the constraints below are what an agent reads when deciding to use these.
24495
+ html: external_exports.string().max(16384).describe(
24496
+ "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."
24497
+ ).optional(),
24498
+ image: external_exports.string().url().describe(
24499
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
24500
+ ).optional()
24501
+ });
24502
+ OptionInputSchema = OptionSchema.omit({ id: true }).extend({
24503
+ label: external_exports.string().trim().min(1).max(1e3)
24504
+ }).strict();
24505
+ NIGHT = { from: 23, to: 7 };
24455
24506
  AskInputSchema = external_exports.object({
24456
24507
  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."),
24508
+ 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
24509
  repo: external_exports.string().optional().describe("Optional repository context."),
24459
24510
  ask: external_exports.string().trim().min(1).max(1e4).describe(
24460
24511
  "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."
24461
24512
  ),
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()
24513
+ options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
24468
24514
  }).strict();
24469
24515
  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."),
24516
+ 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
24517
  waiting: external_exports.enum(["none", "hard"]).default("none"),
24472
24518
  channel: external_exports.enum(["notification", "call"]).default("notification")
24473
24519
  }).strict();
24474
24520
  ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
24475
24521
  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.";
24522
+ CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
24477
24523
  CreateGoalSchema = external_exports.object({
24478
24524
  outcome: external_exports.string().trim().min(1).max(1e4),
24479
24525
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -24481,10 +24527,6 @@ var init_dist = __esm({
24481
24527
  title: external_exports.string().trim().min(1).max(80).optional(),
24482
24528
  ownerParticipant: external_exports.string().trim().min(1).optional(),
24483
24529
  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
24530
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
24489
24531
  * didn't get created as a child goal of the voice UI goal, which is how it should've
24490
24532
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -24528,19 +24570,9 @@ var init_dist = __esm({
24528
24570
  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
24571
  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.";
24530
24572
  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
24573
  AGENT_TOOLS = [
24540
24574
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
24541
24575
  { 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
24576
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
24545
24577
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
24546
24578
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
@@ -24574,19 +24606,6 @@ var init_dist = __esm({
24574
24606
  "summarize"
24575
24607
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
24576
24608
  ]);
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
24609
  VisualSchema = external_exports.object({
24591
24610
  url: external_exports.string().url(),
24592
24611
  label: external_exports.string().optional()
@@ -24640,7 +24659,7 @@ var init_dist = __esm({
24640
24659
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
24641
24660
  * superRefine at the bottom enforces exactly one of the two. */
24642
24661
  context: ContextSchema.optional(),
24643
- options: external_exports.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
24662
+ options: external_exports.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
24644
24663
  "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
24664
  ),
24646
24665
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -24834,7 +24853,7 @@ var init_dist = __esm({
24834
24853
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
24835
24854
  * post-feedback revision, #633) — the user will never answer this id. Stop
24836
24855
  * awaiting it; the live ask is the thread's newest turn (await that one, or
24837
- * re-orient via get_thread / check_replies). */
24856
+ * re-orient via check_replies). */
24838
24857
  external_exports.object({
24839
24858
  type: external_exports.literal("superseded"),
24840
24859
  parentId: external_exports.string(),
@@ -24948,10 +24967,6 @@ var init_dist = __esm({
24948
24967
  options: external_exports.array(OptionSchema).optional(),
24949
24968
  /** The ask's declared coverage points (#396), when the agent sent them. */
24950
24969
  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
24970
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
24956
24971
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
24957
24972
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
@@ -24972,7 +24987,26 @@ var init_dist = __esm({
24972
24987
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
24973
24988
  * `createdAt`. */
24974
24989
  agentStateAt: external_exports.string().datetime().optional(),
24975
- agenda: external_exports.array(AgendaTurnSchema).optional(),
24990
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
24991
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
24992
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
24993
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
24994
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
24995
+ *
24996
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
24997
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
24998
+ *
24999
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
25000
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
25001
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
25002
+ * REPLACED `agenda` (turns), which nothing ever filled. */
25003
+ questions: external_exports.array(external_exports.object({
25004
+ id: external_exports.string(),
25005
+ entryId: external_exports.string(),
25006
+ title: external_exports.string(),
25007
+ state: external_exports.enum(["open", "answered"]),
25008
+ answer: external_exports.string().nullable()
25009
+ })).optional(),
24976
25010
  visuals: external_exports.array(VisualSchema).optional(),
24977
25011
  /** The connected agent's name (the single pairing name — user-typed, or the
24978
25012
  * agent's suggestion, or a default silly name). */
@@ -24993,12 +25027,21 @@ var init_dist = __esm({
24993
25027
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
24994
25028
  error: external_exports.string().optional(),
24995
25029
  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),
25030
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
25031
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
25032
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
25033
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
25034
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
25035
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
25036
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
25037
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
25038
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
25039
+ ring: external_exports.object({
25040
+ declinedAt: external_exports.string().datetime(),
25041
+ anchorAt: external_exports.string().datetime(),
25042
+ nextRingAt: external_exports.string().datetime().nullable(),
25043
+ step: external_exports.number().int()
25044
+ }).optional(),
25002
25045
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
25003
25046
  * Absent for anything never delivered through a push, and for older rows written before
25004
25047
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
@@ -25063,6 +25106,8 @@ var init_dist = __esm({
25063
25106
  "inbox",
25064
25107
  "dismiss"
25065
25108
  ]);
25109
+ clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
25110
+ QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
25066
25111
  BrokerTuningSchema = external_exports.object({
25067
25112
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
25068
25113
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -25097,6 +25142,12 @@ var init_dist = __esm({
25097
25142
  * must not silently reset this privacy choice. Absent = leave unchanged on
25098
25143
  * write, 'hosted' on read (see store.ts). */
25099
25144
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
25145
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
25146
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
25147
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
25148
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
25149
+ * read (see store.ts). */
25150
+ talk: external_exports.boolean().optional(),
25100
25151
  /** Per-user ring budget (#603): calls per rolling day before further calls
25101
25152
  * degrade to banner. Absent = the global default (25). A number, never a
25102
25153
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -25294,6 +25345,9 @@ var init_dist = __esm({
25294
25345
  assignee: external_exports.string().nullable().default(null),
25295
25346
  /** What the agent last said it was doing; null if it has said nothing. */
25296
25347
  progress: external_exports.string().nullable().default(null),
25348
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
25349
+ * served only while it was written for the current note. Null means show the Goal's name. */
25350
+ progressLine: external_exports.string().nullable().optional(),
25297
25351
  reviewPending: external_exports.boolean().default(false),
25298
25352
  dueAt: external_exports.string().nullable().default(null),
25299
25353
  /** The Goal this one was opened under; null at the root. */
@@ -25309,8 +25363,13 @@ var init_dist = __esm({
25309
25363
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
25310
25364
  repo: external_exports.string().nullable().optional(),
25311
25365
  createdAt: external_exports.string(),
25312
- updatedAt: external_exports.string().nullable().default(null)
25366
+ updatedAt: external_exports.string().nullable().default(null),
25367
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
25368
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
25369
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
25370
+ lastProgressAt: external_exports.string().nullable().optional()
25313
25371
  });
25372
+ COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
25314
25373
  NoteSourceSchema = external_exports.enum(["app", "call"]);
25315
25374
  NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
25316
25375
  NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
@@ -25510,9 +25569,9 @@ var init_dist = __esm({
25510
25569
  this.body = body;
25511
25570
  }
25512
25571
  };
25513
- AWAIT_WINDOW_MS = 45e3;
25514
25572
  tokenOverride = null;
25515
25573
  authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
25574
+ AWAIT_WINDOW_MS = 45e3;
25516
25575
  HEARTBEAT_MS = 25e3;
25517
25576
  RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
25518
25577
  JOIN = { config: { broadcast: { self: false } } };
@@ -30126,9 +30185,9 @@ async function available(deps = {}) {
30126
30185
  });
30127
30186
  return [ollama, ...harnesses];
30128
30187
  }
30129
- function refusal(options, asked) {
30188
+ function refusal(options, asked2) {
30130
30189
  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.",
30190
+ 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
30191
  "",
30133
30192
  "Triage runs the model where your queue already lives (#1106). Your options:"
30134
30193
  ];
@@ -30139,10 +30198,10 @@ function refusal(options, asked) {
30139
30198
  lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
30140
30199
  return lines2.join("\n");
30141
30200
  }
30142
- async function judgeFor(asked, deps = {}) {
30201
+ async function judgeFor(asked2, deps = {}) {
30143
30202
  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) };
30203
+ const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
30204
+ if (!chosen?.ready) return { error: refusal(options, asked2) };
30146
30205
  if (chosen.provider === "ollama") {
30147
30206
  const model = chosen.model;
30148
30207
  return {
@@ -30163,9 +30222,9 @@ async function judgeFor(asked, deps = {}) {
30163
30222
  };
30164
30223
  }
30165
30224
  const runner = RUNNERS[chosen.provider];
30166
- if (!runner) return { error: refusal(options, asked) };
30225
+ if (!runner) return { error: refusal(options, asked2) };
30167
30226
  const bin = which(runner.cli);
30168
- if (!bin) return { error: refusal(options, asked) };
30227
+ if (!bin) return { error: refusal(options, asked2) };
30169
30228
  return {
30170
30229
  provider: chosen.provider,
30171
30230
  model: null,
@@ -31024,7 +31083,13 @@ function startSession(opts) {
31024
31083
  ...process.env,
31025
31084
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
31026
31085
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
31027
- ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
31086
+ ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
31087
+ // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
31088
+ // bridge's pump typing answers in, so nothing inside it may start a second delivery path.
31089
+ // Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
31090
+ // fresh spawn has none, which is how a fresh run read as "not harness-started" to the
31091
+ // session-start hook and the `listen` tool (they now test either mark; listening.ts).
31092
+ PAIGY_HARNESS: "1"
31028
31093
  },
31029
31094
  stdio: ["pipe", "pipe", "pipe"]
31030
31095
  });
@@ -31073,6 +31138,7 @@ function startSession(opts) {
31073
31138
  // ../../packages/schema/dist/index.js
31074
31139
  init_zod();
31075
31140
  init_zod();
31141
+ init_zod();
31076
31142
 
31077
31143
  // ../../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
31144
  var ignoreOverride2 = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
@@ -32369,6 +32435,23 @@ var zodToJsonSchema2 = (schema, options) => {
32369
32435
  init_zod();
32370
32436
  var OPTIONS_MIN2 = 2;
32371
32437
  var OPTIONS_MAX2 = 6;
32438
+ var OptionSchema2 = external_exports.object({
32439
+ id: external_exports.string(),
32440
+ label: external_exports.string(),
32441
+ 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(),
32442
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
32443
+ // the constraints below are what an agent reads when deciding to use these.
32444
+ html: external_exports.string().max(16384).describe(
32445
+ "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."
32446
+ ).optional(),
32447
+ image: external_exports.string().url().describe(
32448
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
32449
+ ).optional()
32450
+ });
32451
+ var OptionInputSchema2 = OptionSchema2.omit({ id: true }).extend({
32452
+ label: external_exports.string().trim().min(1).max(1e3)
32453
+ }).strict();
32454
+ var NIGHT2 = { from: 23, to: 7 };
32372
32455
  function draft20202(node) {
32373
32456
  if (Array.isArray(node)) return node.map(draft20202);
32374
32457
  if (node && typeof node === "object") {
@@ -32400,26 +32483,21 @@ function mcpInputSchema2(s) {
32400
32483
  }
32401
32484
  var AskInputSchema2 = external_exports.object({
32402
32485
  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."),
32486
+ 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
32487
  repo: external_exports.string().optional().describe("Optional repository context."),
32405
32488
  ask: external_exports.string().trim().min(1).max(1e4).describe(
32406
32489
  "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."
32407
32490
  ),
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()
32491
+ options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
32414
32492
  }).strict();
32415
32493
  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."),
32494
+ 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
32495
  waiting: external_exports.enum(["none", "hard"]).default("none"),
32418
32496
  channel: external_exports.enum(["notification", "call"]).default("notification")
32419
32497
  }).strict();
32420
32498
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
32421
32499
  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.";
32500
+ var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
32423
32501
  var CreateGoalSchema2 = external_exports.object({
32424
32502
  outcome: external_exports.string().trim().min(1).max(1e4),
32425
32503
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -32427,10 +32505,6 @@ var CreateGoalSchema2 = external_exports.object({
32427
32505
  title: external_exports.string().trim().min(1).max(80).optional(),
32428
32506
  ownerParticipant: external_exports.string().trim().min(1).optional(),
32429
32507
  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
32508
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
32435
32509
  * didn't get created as a child goal of the voice UI goal, which is how it should've
32436
32510
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -32474,19 +32548,9 @@ var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State
32474
32548
  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
32549
  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.";
32476
32550
  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
32551
  var AGENT_TOOLS2 = [
32486
32552
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
32487
32553
  { 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
32554
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
32491
32555
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
32492
32556
  { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
@@ -32535,19 +32599,6 @@ var TransformSchema2 = external_exports.enum([
32535
32599
  "summarize"
32536
32600
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
32537
32601
  ]);
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
32602
  var VisualSchema2 = external_exports.object({
32552
32603
  url: external_exports.string().url(),
32553
32604
  label: external_exports.string().optional()
@@ -32601,7 +32652,7 @@ var NotifyRequestFields2 = external_exports.object({
32601
32652
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
32602
32653
  * superRefine at the bottom enforces exactly one of the two. */
32603
32654
  context: ContextSchema2.optional(),
32604
- options: external_exports.array(OptionSchema2.omit({ id: true })).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
32655
+ options: external_exports.array(OptionInputSchema2).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
32605
32656
  "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
32657
  ),
32607
32658
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -32795,7 +32846,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
32795
32846
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
32796
32847
  * post-feedback revision, #633) — the user will never answer this id. Stop
32797
32848
  * awaiting it; the live ask is the thread's newest turn (await that one, or
32798
- * re-orient via get_thread / check_replies). */
32849
+ * re-orient via check_replies). */
32799
32850
  external_exports.object({
32800
32851
  type: external_exports.literal("superseded"),
32801
32852
  parentId: external_exports.string(),
@@ -32909,10 +32960,6 @@ var InboxItemSchema2 = external_exports.object({
32909
32960
  options: external_exports.array(OptionSchema2).optional(),
32910
32961
  /** The ask's declared coverage points (#396), when the agent sent them. */
32911
32962
  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
32963
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
32917
32964
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
32918
32965
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
@@ -32933,7 +32980,26 @@ var InboxItemSchema2 = external_exports.object({
32933
32980
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
32934
32981
  * `createdAt`. */
32935
32982
  agentStateAt: external_exports.string().datetime().optional(),
32936
- agenda: external_exports.array(AgendaTurnSchema2).optional(),
32983
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
32984
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
32985
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
32986
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
32987
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
32988
+ *
32989
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
32990
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
32991
+ *
32992
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
32993
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
32994
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
32995
+ * REPLACED `agenda` (turns), which nothing ever filled. */
32996
+ questions: external_exports.array(external_exports.object({
32997
+ id: external_exports.string(),
32998
+ entryId: external_exports.string(),
32999
+ title: external_exports.string(),
33000
+ state: external_exports.enum(["open", "answered"]),
33001
+ answer: external_exports.string().nullable()
33002
+ })).optional(),
32937
33003
  visuals: external_exports.array(VisualSchema2).optional(),
32938
33004
  /** The connected agent's name (the single pairing name — user-typed, or the
32939
33005
  * agent's suggestion, or a default silly name). */
@@ -32954,12 +33020,21 @@ var InboxItemSchema2 = external_exports.object({
32954
33020
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
32955
33021
  error: external_exports.string().optional(),
32956
33022
  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),
33023
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
33024
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
33025
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
33026
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
33027
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
33028
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
33029
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
33030
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
33031
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
33032
+ ring: external_exports.object({
33033
+ declinedAt: external_exports.string().datetime(),
33034
+ anchorAt: external_exports.string().datetime(),
33035
+ nextRingAt: external_exports.string().datetime().nullable(),
33036
+ step: external_exports.number().int()
33037
+ }).optional(),
32963
33038
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
32964
33039
  * Absent for anything never delivered through a push, and for older rows written before
32965
33040
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
@@ -33024,6 +33099,8 @@ var MissedCallSchema2 = external_exports.enum([
33024
33099
  "inbox",
33025
33100
  "dismiss"
33026
33101
  ]);
33102
+ var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
33103
+ var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
33027
33104
  var BrokerTuningSchema2 = external_exports.object({
33028
33105
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
33029
33106
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -33058,6 +33135,12 @@ var UserSettingsSchema2 = external_exports.object({
33058
33135
  * must not silently reset this privacy choice. Absent = leave unchanged on
33059
33136
  * write, 'hosted' on read (see store.ts). */
33060
33137
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
33138
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
33139
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
33140
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
33141
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
33142
+ * read (see store.ts). */
33143
+ talk: external_exports.boolean().optional(),
33061
33144
  /** Per-user ring budget (#603): calls per rolling day before further calls
33062
33145
  * degrade to banner. Absent = the global default (25). A number, never a
33063
33146
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -33255,6 +33338,9 @@ var QueueItemSchema2 = external_exports.object({
33255
33338
  assignee: external_exports.string().nullable().default(null),
33256
33339
  /** What the agent last said it was doing; null if it has said nothing. */
33257
33340
  progress: external_exports.string().nullable().default(null),
33341
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
33342
+ * served only while it was written for the current note. Null means show the Goal's name. */
33343
+ progressLine: external_exports.string().nullable().optional(),
33258
33344
  reviewPending: external_exports.boolean().default(false),
33259
33345
  dueAt: external_exports.string().nullable().default(null),
33260
33346
  /** The Goal this one was opened under; null at the root. */
@@ -33270,8 +33356,13 @@ var QueueItemSchema2 = external_exports.object({
33270
33356
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
33271
33357
  repo: external_exports.string().nullable().optional(),
33272
33358
  createdAt: external_exports.string(),
33273
- updatedAt: external_exports.string().nullable().default(null)
33359
+ updatedAt: external_exports.string().nullable().default(null),
33360
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
33361
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
33362
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
33363
+ lastProgressAt: external_exports.string().nullable().optional()
33274
33364
  });
33365
+ var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
33275
33366
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
33276
33367
  var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
33277
33368
  var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
@@ -33995,9 +34086,9 @@ async function slotHasWaitingWork(token, session) {
33995
34086
  }
33996
34087
  var PRESENCE_FRESH_MS = 3 * 6e4;
33997
34088
  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";
34089
+ if (live) return { action: "skip", why: "run" };
34090
+ if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
34091
+ return { action: "spawn", why: "nobody" };
34001
34092
  }
34002
34093
  function startHost(opts) {
34003
34094
  const runs = /* @__PURE__ */ new Map();
@@ -34087,7 +34178,11 @@ function startHost(opts) {
34087
34178
  const live = runs.get(key);
34088
34179
  const tokenId = slotIdentity(slot).tokenId;
34089
34180
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
34090
- if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
34181
+ const decided = wakeAction(!!live, Date.now(), seenByServer);
34182
+ if (decided.action === "skip") {
34183
+ 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`);
34184
+ return;
34185
+ }
34091
34186
  const token = readToken(slot);
34092
34187
  if (!token) return;
34093
34188
  const owner = ownerSession(slot);