@paigy/mcp 0.40.10 → 0.40.14

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.
@@ -1,10 +1,28 @@
1
1
  // ../../packages/schema/dist/index.js
2
+ import { z as z4 } from "zod";
3
+ import { z } from "zod";
2
4
  import { z as z3 } from "zod";
3
- import { z as z2 } from "zod";
4
5
  import { zodToJsonSchema } from "zod-to-json-schema";
5
- import { z } from "zod";
6
+ import { z as z2 } from "zod";
6
7
  var OPTIONS_MIN = 2;
7
8
  var OPTIONS_MAX = 6;
9
+ var OptionSchema = z.object({
10
+ id: z.string(),
11
+ label: z.string(),
12
+ hint: z.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(),
13
+ // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
14
+ // the constraints below are what an agent reads when deciding to use these.
15
+ html: z.string().max(16384).describe(
16
+ "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."
17
+ ).optional(),
18
+ image: z.string().url().describe(
19
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
20
+ ).optional()
21
+ });
22
+ var OptionInputSchema = OptionSchema.omit({ id: true }).extend({
23
+ label: z.string().trim().min(1).max(1e3)
24
+ }).strict();
25
+ var NIGHT = { from: 23, to: 7 };
8
26
  function draft2020(node) {
9
27
  if (Array.isArray(node)) return node.map(draft2020);
10
28
  if (node && typeof node === "object") {
@@ -34,39 +52,30 @@ function mcpInputSchema(s) {
34
52
  delete schema.$schema;
35
53
  return draft2020(schema);
36
54
  }
37
- var AskInputSchema = z.object({
38
- id: z.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
39
- parentId: z.string().uuid().optional().describe("Optional explicit parent Goal ID. If omitted, the platform's semantic router will auto-cluster it."),
40
- repo: z.string().optional().describe("Optional repository context."),
41
- ask: z.string().trim().min(1).max(1e4).describe(
55
+ var AskInputSchema = z2.object({
56
+ id: z2.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
57
+ parentId: z2.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."),
58
+ repo: z2.string().optional().describe("Optional repository context."),
59
+ ask: z2.string().trim().min(1).max(1e4).describe(
42
60
  "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."
43
61
  ),
44
- options: z.array(z.object({
45
- label: z.string().trim().min(1).max(1e3),
46
- hint: z.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
47
- image: z.string().url().optional(),
48
- html: z.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()
49
- }).strict()).min(2).max(6).optional()
62
+ options: z2.array(OptionInputSchema).min(2).max(6).optional()
50
63
  }).strict();
51
- var StartContactSchema = z.object({
52
- asks: z.array(AskInputSchema).min(1).describe("The list of asks/questions to pose. They will be semantically clustered by the API."),
53
- waiting: z.enum(["none", "hard"]).default("none"),
54
- channel: z.enum(["notification", "call"]).default("notification")
64
+ var StartContactSchema = z2.object({
65
+ asks: z2.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."),
66
+ waiting: z2.enum(["none", "hard"]).default("none"),
67
+ channel: z2.enum(["notification", "call"]).default("notification")
55
68
  }).strict();
56
- var ContactSchema = z.union([StartContactSchema, z.object({ deliveryId: z.string().uuid() }).strict()]);
69
+ var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
57
70
  var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
58
- var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. The platform will semantically cluster your asks under the correct Goals automatically. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
59
- var CreateGoalSchema = z2.object({
60
- outcome: z2.string().trim().min(1).max(1e4),
71
+ var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
72
+ var CreateGoalSchema = z3.object({
73
+ outcome: z3.string().trim().min(1).max(1e4),
61
74
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
62
75
  * rings"). Omit it and the brain writes one at admission from the outcome. */
63
- title: z2.string().trim().min(1).max(80).optional(),
64
- ownerParticipant: z2.string().trim().min(1).optional(),
65
- idempotencyKey: z2.string().trim().min(1).max(200),
66
- /** A past conversation this Goal should be read against — History's "new session from this"
67
- * (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
68
- * reference only: the owner reads it through `get_thread`, which does its own scoping, and
69
- * the writer refuses a thread belonging to another account. */
76
+ title: z3.string().trim().min(1).max(80).optional(),
77
+ ownerParticipant: z3.string().trim().min(1).optional(),
78
+ idempotencyKey: z3.string().trim().min(1).max(200),
70
79
  /** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
71
80
  * didn't get created as a child goal of the voice UI goal, which is how it should've
72
81
  * worked"). It could not have been: this door took no parent, so the only route was
@@ -74,69 +83,60 @@ var CreateGoalSchema = z2.object({
74
83
  * took the short one. The hierarchy has been modelled since Goals existed and had been used
75
84
  * ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
76
85
  * (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
77
- parentGoalId: z2.string().uuid().optional(),
86
+ parentGoalId: z3.string().uuid().optional(),
78
87
  /** The repository or project identifier this Goal belongs to (#2280) — e.g. "owner/repo" or
79
88
  * repo name. Delegated work inherits this from its parent Goal when omitted. */
80
- repo: z2.string().trim().min(1).max(200).optional()
89
+ repo: z3.string().trim().min(1).max(200).optional()
81
90
  });
82
91
  var CreateGoalToolSchema = CreateGoalSchema.extend({
83
92
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
84
93
  }).strict();
85
94
  var CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: the owner must claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
86
- var UpdateGoalSchema = z2.object({
87
- revision: z2.number().int().positive(),
88
- changes: z2.object({
89
- outcome: z2.string().trim().min(1).max(1e4).optional(),
95
+ var UpdateGoalSchema = z3.object({
96
+ revision: z3.number().int().positive(),
97
+ changes: z3.object({
98
+ outcome: z3.string().trim().min(1).max(1e4).optional(),
90
99
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
91
100
  * brain writes one at admission; this is the owner saying it better. Null clears it. */
92
- title: z2.string().trim().min(1).max(80).nullable().optional(),
93
- ownerParticipant: z2.string().trim().min(1).optional(),
94
- parentGoalId: z2.string().uuid().nullable().optional(),
95
- dependencies: z2.array(z2.object({ goalId: z2.string().uuid(), gate: z2.enum(["start", "finish"]) }).strict()).optional(),
96
- children: z2.array(z2.object({ outcome: z2.string().trim().min(1).max(1e4), ownerParticipant: z2.string().trim().min(1), gate: z2.enum(["start", "finish"]).optional() }).strict()).optional(),
97
- state: z2.enum(["active", "done", "cancelled"]).optional(),
98
- progress: z2.string().trim().min(1).max(1e4).optional(),
99
- reviewed: z2.literal(true).optional(),
100
- dueAt: z2.string().datetime({ offset: true }).nullable().optional()
101
+ title: z3.string().trim().min(1).max(80).nullable().optional(),
102
+ ownerParticipant: z3.string().trim().min(1).optional(),
103
+ parentGoalId: z3.string().uuid().nullable().optional(),
104
+ dependencies: z3.array(z3.object({ goalId: z3.string().uuid(), gate: z3.enum(["start", "finish"]) }).strict()).optional(),
105
+ children: z3.array(z3.object({ outcome: z3.string().trim().min(1).max(1e4), ownerParticipant: z3.string().trim().min(1), gate: z3.enum(["start", "finish"]).optional() }).strict()).optional(),
106
+ state: z3.enum(["active", "done", "cancelled"]).optional(),
107
+ progress: z3.string().trim().min(1).max(1e4).optional(),
108
+ reviewed: z3.literal(true).optional(),
109
+ dueAt: z3.string().datetime({ offset: true }).nullable().optional()
101
110
  }).strict().refine((v) => Object.keys(v).length > 0),
102
- reason: z2.string().trim().min(1).max(2e3),
103
- operationId: z2.string().uuid().optional()
111
+ reason: z3.string().trim().min(1).max(2e3),
112
+ operationId: z3.string().uuid().optional()
104
113
  }).strict();
105
- var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z2.string().uuid() }).strict();
106
- var ClaimGoalSchema = z2.object({ goalId: z2.string().uuid().optional() }).strict();
107
- var GetGoalSchema = z2.object({ goalId: z2.string().uuid() }).strict();
114
+ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z3.string().uuid() }).strict();
115
+ var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
116
+ var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
108
117
  var 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.";
109
118
  var 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.";
110
119
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
111
120
  var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
112
- var CheckRepliesSchema = z2.object({}).strict();
113
- var GetThreadSchema = z2.object({
114
- parentId: z2.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
115
- }).strict();
116
- var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
117
- var SearchThreadsSchema = z2.object({
118
- q: z2.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
119
- }).strict();
120
- var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
121
+ var CheckRepliesSchema = z3.object({}).strict();
121
122
  var AGENT_TOOLS = [
122
123
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
123
124
  { name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
124
- { name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
125
- { name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
126
125
  { name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
127
126
  { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
128
127
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
129
128
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
130
129
  ];
131
130
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
132
- function serverInstructions(_opts) {
133
- return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Contact only for an existing Goal. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. On stdio, Calls hold one bounded window; on hosted MCP, Calls return after one read; continue with contact({deliveryId}) to reread that exact Delivery. Evidence can repeat: reads do not acknowledge or hide it. Use update_goal to report progress and explicitly acknowledge review. Never infer ringing from an open Call Delivery. Soft waiting, multipart contact, and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
131
+ function serverInstructions(opts) {
132
+ const calls = opts.waits ? "A Call holds one bounded window here; continue with contact({deliveryId}) to hold the next or reread that exact Delivery." : "A Call returns after one read; continue with contact({deliveryId}) to reread that exact Delivery.";
133
+ return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Evidence can repeat: reads do not acknowledge or hide it. Report progress and acknowledge review with update_goal, never as a contact. Never infer ringing from an open Call Delivery. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
134
134
 
135
- CRITICAL PAIGY WORKFLOW PHILOSOPHY:
136
- 1. Atomicity: Never dump a 5-part question into a single \`ask\`. Emit 5 separate objects in the \`asks\` array during a single \`contact\` call so the user can answer them independently.
137
- 2. Tree Structure: Always pass \`parentId\` to deliberately nest sub-decisions under the overarching Goal, ensuring the user workspace remains a clean tree rather than a flat mess.
138
- 3. Urgency: Use \`waiting: hard\` strictly for immediate, blocking decisions, and \`waiting: none\` for async, long-running progress updates or non-blocking questions.
139
- 4. Asynchrony: Never hold the process open with while-loops. Let the current process finish or explicitly schedule wakeups, and rely on \`check_replies\` or \`claim_goal\` on the next wake to process asynchronous answers.`;
135
+ HOW TO ASK:
136
+ 1. One question per ask. Five questions are five objects in \`asks\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
137
+ 2. Name the Goal you are working on as the ask's \`parentId\`: the question goes onto that Goal and its answer comes back there. Without one, Paigy places it.
138
+ 3. \`waiting: hard\` only for a decision you are blocked on; \`waiting: none\` for a question you can keep working around.
139
+ 4. Never hold the process open with while-loops. Let the current process finish or schedule a wakeup, and collect answers with \`check_replies\` or \`claim_goal\` on the next wake.`;
140
140
  }
141
141
  function entryWords(entry) {
142
142
  const content = entry.content;
@@ -154,17 +154,17 @@ function entryWords(entry) {
154
154
  return entry.sources.map((source) => source.text).join("\n");
155
155
  }
156
156
  var LIVE_MS = 3 * 6e4;
157
- var ContextSchema = z3.object({
158
- title: z3.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
159
- description: z3.array(z3.string().min(1)).describe(
157
+ var ContextSchema = z4.object({
158
+ title: z4.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
159
+ description: z4.array(z4.string().min(1)).describe(
160
160
  "Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
161
161
  )
162
162
  });
163
- var ParticipantSchema = z3.object({
164
- kind: z3.enum(["human", "agent"]),
165
- id: z3.string()
163
+ var ParticipantSchema = z4.object({
164
+ kind: z4.enum(["human", "agent"]),
165
+ id: z4.string()
166
166
  });
167
- var TransformSchema = z3.enum([
167
+ var TransformSchema = z4.enum([
168
168
  "structure",
169
169
  // shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
170
170
  "request_more",
@@ -180,26 +180,13 @@ var TransformSchema = z3.enum([
180
180
  "summarize"
181
181
  // reduce volume, keep decision value — 30-turn cap, spoken briefing
182
182
  ]);
183
- var OptionSchema = z3.object({
184
- id: z3.string(),
185
- label: z3.string(),
186
- hint: z3.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(),
187
- // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
188
- // the constraints below are what an agent reads when deciding to use these.
189
- html: z3.string().max(16384).describe(
190
- "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."
191
- ).optional(),
192
- image: z3.string().url().describe(
193
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
194
- ).optional()
195
- });
196
- var VisualSchema = z3.object({
197
- url: z3.string().url(),
198
- label: z3.string().optional()
183
+ var VisualSchema = z4.object({
184
+ url: z4.string().url(),
185
+ label: z4.string().optional()
199
186
  });
200
- var NotifyLevelSchema = z3.enum(["inbox", "push", "banner", "call"]);
201
- var SelectShapeSchema = z3.enum(["one", "many", "rank", "confirm", "text"]);
202
- var ReceiptEventSchema = z3.enum([
187
+ var NotifyLevelSchema = z4.enum(["inbox", "push", "banner", "call"]);
188
+ var SelectShapeSchema = z4.enum(["one", "many", "rank", "confirm", "text"]);
189
+ var ReceiptEventSchema = z4.enum([
203
190
  "delivered",
204
191
  // the bundle reached the recipient at some level
205
192
  "seen",
@@ -229,47 +216,47 @@ var ReceiptEventSchema = z3.enum([
229
216
  // be rewound by a writer that forgot to advance it.
230
217
  "restarted"
231
218
  ]);
232
- var AttentionSchema = z3.object({
219
+ var AttentionSchema = z4.object({
233
220
  urgency: NotifyLevelSchema,
234
221
  /** The required answer shape, or null for a plain notify that asks nothing back. */
235
222
  select: SelectShapeSchema.nullable(),
236
223
  /** Coverage contract (#396) — points the answer must address; null = none declared. */
237
- points: z3.array(z3.string()).nullable(),
224
+ points: z4.array(z4.string()).nullable(),
238
225
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
239
- blocking: z3.boolean(),
226
+ blocking: z4.boolean(),
240
227
  /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
241
228
  * slice wires it; optional so today's rows/callers project cleanly. */
242
- deadline: z3.string().datetime().nullable().optional()
229
+ deadline: z4.string().datetime().nullable().optional()
243
230
  });
244
- var NotifyRequestFields = z3.object({
231
+ var NotifyRequestFields = z4.object({
245
232
  /** Plaintext message content. Present on the plaintext path (today's shape);
246
233
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
247
234
  * superRefine at the bottom enforces exactly one of the two. */
248
235
  context: ContextSchema.optional(),
249
- options: z3.array(OptionSchema.omit({ id: true })).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
236
+ options: z4.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
250
237
  "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)."
251
238
  ),
252
- points: z3.array(z3.string().min(1)).optional().describe(
239
+ points: z4.array(z4.string().min(1)).optional().describe(
253
240
  "The distinct things you need answered, each a short phrase \u2014 on a call the broker keeps the conversation going until each is addressed, and the reply reports which were covered, so a half-answer is never silently returned as final. Omit for single-part asks."
254
241
  ),
255
- visuals: z3.array(VisualSchema).optional().describe(
242
+ visuals: z4.array(VisualSchema).optional().describe(
256
243
  "Images attached to the message itself \u2014 context for the whole question (a screenshot, a chart). For a preview on one selectable choice, use that option's `html`/`image` instead."
257
244
  ),
258
245
  /** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
259
- repo: z3.string().optional(),
246
+ repo: z4.string().optional(),
260
247
  /** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
261
- branch: z3.string().optional(),
248
+ branch: z4.string().optional(),
262
249
  /** Continue an existing conversation — the id of any notification in it (its root
263
250
  * is the conversation's identity). Omitted = start a new conversation. Renamed
264
251
  * from `parentId` (2026-08-03): one linkage system, the parent; the API edge
265
252
  * still accepts the old name from older clients. */
266
- parentId: z3.string().uuid().optional(),
253
+ parentId: z4.string().uuid().optional(),
267
254
  /** The durable outcome this contact advances. Optional during the notification-to-Work
268
255
  * migration; when present, a blocking ask creates a DecisionNeed for this Work. */
269
- workId: z3.string().uuid().optional(),
256
+ workId: z4.string().uuid().optional(),
270
257
  /** Target Goal scope. During staged migration this is accepted by the shared contract but
271
258
  * target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
272
- goalId: z3.string().uuid().optional(),
259
+ goalId: z4.string().uuid().optional(),
273
260
  urgency: NotifyLevelSchema.default("inbox").describe(
274
261
  "The level you're requesting \u2014 the user's account permissions + session mode can lower it. 'inbox' (default) = sits silently in the inbox for the user to get to. 'push' = a quiet passive push (lands in Notification Center, no sound) \u2014 a gentle heads-up. 'banner' = a time-sensitive banner/lock-screen push with sound (a 'paige') they tap to open \u2014 use when you need them soon-ish but it's not worth ringing them. 'call' = rings the user's phone now (a CallKit voice call) \u2014 use only when you genuinely need them in the moment (blocked and waiting, time-sensitive). context.title is what they see on the banner/ring, so make it specific."
275
262
  ),
@@ -277,7 +264,7 @@ var NotifyRequestFields = z3.object({
277
264
  * visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
278
265
  * when `parentId` became the conversation handle: `parentId` says WHERE, this
279
266
  * says HOW. */
280
- clarifies: z3.string().optional(),
267
+ clarifies: z4.string().optional(),
281
268
  select: SelectShapeSchema.optional().describe(
282
269
  "How the user answers \u2014 required on the fully-shaped form, pick the shape that fits the question: 'one' = pick one option, 'many' = pick several, 'rank' = pick & order (each needs `options`); 'confirm' = yes/no or approve/deny; 'text' = free-form reply only (status updates, open questions). 'confirm' and 'text' take no options. Omit only when sending the simplified `ask` form \u2014 the broker picks the shape."
283
270
  ),
@@ -291,20 +278,20 @@ var NotifyRequestFields = z3.object({
291
278
  // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
292
279
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
293
280
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
294
- ask: z3.string().min(1).max(1e4).optional().describe(
281
+ ask: z4.string().min(1).max(1e4).optional().describe(
295
282
  'SIMPLIFIED FORM \u2014 state in plain prose what you need to learn from the user and why it matters now (e.g. "I need to know whether to deploy the auth fix \u2014 tests are green, staging verified"). Write as much as the situation needs (up to 10k characters) \u2014 Paigy breaks it into topics and reads it back a few sentences at a time; do NOT pre-summarize it into one line. Paigy derives the title, answer shape, options, and delivery channel for you. Mutually exclusive with context/select/options \u2014 send one form or the other.'
296
283
  ),
297
- needs: z3.array(z3.string().min(1)).optional().describe(
284
+ needs: z4.array(z4.string().min(1)).optional().describe(
298
285
  "With `ask` only: the distinct things you need answered when the ask is multi-part \u2014 becomes the coverage contract (`points`), so a half-answer is never silently final."
299
286
  ),
300
- urgencyHint: z3.enum(["whenever", "soon", "now"]).optional().describe(
287
+ urgencyHint: z4.enum(["whenever", "soon", "now"]).optional().describe(
301
288
  "With `ask` only: how urgently you need the answer \u2014 'whenever' (inbox), 'soon' (worth a heads-up), 'now' (you're blocked this minute). A hint, not a command: the user's settings still have the final word."
302
289
  ),
303
290
  /** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
304
291
  * to the agent's work while it waits. Normalized server-side into those two
305
292
  * fields (normalizeWaiting) so everything downstream is untouched; explicit
306
293
  * urgencyHint/blocking win when both are sent. */
307
- waiting: z3.enum(["none", "soft", "hard"]).optional().describe(
294
+ waiting: z4.enum(["none", "soft", "hard"]).optional().describe(
308
295
  "With `ask`: what happens to your work while you wait. 'none' = you're just informing the user. 'soft' = you'd like an answer but can keep working. 'hard' = you are stopped until they answer (reaches them urgently and escalates to a real phone call if unanswered). Replaces urgencyHint + blocking \u2014 send this one field."
309
296
  ),
310
297
  /** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
@@ -312,98 +299,98 @@ var NotifyRequestFields = z3.object({
312
299
  * holding by default would charge every quiet claim that minute before any agent could
313
300
  * correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
314
301
  * and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
315
- confirm: z3.boolean().optional().describe(
302
+ confirm: z4.boolean().optional().describe(
316
303
  "Hold this one so you can correct the plan before the user is interrupted. The response comes back with `held: true` and the plan; POST the confirm route to release it (with options/visuals/urgency corrections, or nothing at all). If you never do, it is announced anyway a couple of minutes later. Ignored when waiting is 'hard'."
317
304
  ),
318
305
  /** #575: a RELAY of the user's explicitly stated preference, never the agent's
319
306
  * choice. Outranks waiting in both directions: 'call' rings even for a
320
307
  * waiting:'none' "call me when it's done"; 'message' never rings even for
321
308
  * waiting:'hard'. */
322
- channel: z3.enum(["call", "message"]).optional().describe(
309
+ channel: z4.enum(["call", "message"]).optional().describe(
323
310
  "Only if the user explicitly said how to reach them \u2014 'call me' \u2192 'call', 'just message/text me' \u2192 'message'. Omit otherwise; Paigy picks."
324
311
  ),
325
- confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
312
+ confirmStyle: z4.enum(["yesno", "approve"]).default("yesno").describe(
326
313
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
327
314
  ),
328
- blocking: z3.boolean().default(false).describe(
315
+ blocking: z4.boolean().default(false).describe(
329
316
  "Set true when real downstream work is stuck behind this specific decision \u2014 you can't make meaningful progress until it's answered. This is the real signal for how urgently the user should be reached; it's what the premier use case (an agent that stays unblocked instead of going idle) depends on. Independent of `urgency`: a `banner`-level question can still be `blocking` (something IS stuck, just not time-critical enough to ring for immediately) \u2014 if it goes unanswered a while, Paigy escalates it to a real call using this flag rather than guessing from how many other things happen to be pending. Leave false for anything you could work around, defer, or where other useful work exists meanwhile."
330
317
  )
331
318
  });
332
319
  var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
333
- if (r.workId && r.goalId) ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
320
+ if (r.workId && r.goalId) ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
334
321
  if (r.ask !== void 0) {
335
322
  for (const f of ["context", "select", "points"]) {
336
323
  if (r[f] !== void 0)
337
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
324
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: [f], message: `the simplified \`ask\` form takes no ${f} \u2014 the broker derives the answer shape from your prose. Drop ${f} and say it in \`ask\` instead ("should I\u2026" for approve/deny, "which of these\u2026" for a pick), passing \`options\` when you're offering concrete alternatives.` });
338
325
  }
339
326
  return;
340
327
  }
341
328
  if (r.needs !== void 0 || r.urgencyHint !== void 0)
342
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
329
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
343
330
  if (!r.context)
344
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
331
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
345
332
  if (!r.select)
346
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
333
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
347
334
  const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
348
335
  if (needsOptions && !r.options?.length)
349
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
336
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
350
337
  if (!needsOptions && r.options?.length)
351
- ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
338
+ ctx.addIssue({ code: z4.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
352
339
  });
353
- var NotifyStatusSchema = z3.enum(["pending", "answered", "ignored"]);
354
- var AgentStateSchema = z3.enum(["idle", "in_progress", "completed", "needs_input"]);
355
- var TurnSchema = z3.object({
356
- prompt: z3.string(),
357
- reply: z3.string()
340
+ var NotifyStatusSchema = z4.enum(["pending", "answered", "ignored"]);
341
+ var AgentStateSchema = z4.enum(["idle", "in_progress", "completed", "needs_input"]);
342
+ var TurnSchema = z4.object({
343
+ prompt: z4.string(),
344
+ reply: z4.string()
358
345
  });
359
- var UserAnswerSchema = z3.discriminatedUnion("kind", [
360
- z3.object({ kind: z3.literal("option"), optionId: z3.string(), label: z3.string().optional() }),
361
- z3.object({ kind: z3.literal("text"), text: z3.string() }),
362
- z3.object({ kind: z3.literal("ignored") }),
363
- z3.object({ kind: z3.literal("multi"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
364
- z3.object({ kind: z3.literal("ranked"), optionIds: z3.array(z3.string()), labels: z3.array(z3.string()).optional() }),
365
- z3.object({ kind: z3.literal("clarify"), chunks: z3.array(z3.string()).min(1) }),
366
- z3.object({ kind: z3.literal("confirm"), approved: z3.boolean() }),
367
- z3.object({ kind: z3.literal("turns"), turns: z3.array(TurnSchema).min(1) }),
346
+ var UserAnswerSchema = z4.discriminatedUnion("kind", [
347
+ z4.object({ kind: z4.literal("option"), optionId: z4.string(), label: z4.string().optional() }),
348
+ z4.object({ kind: z4.literal("text"), text: z4.string() }),
349
+ z4.object({ kind: z4.literal("ignored") }),
350
+ z4.object({ kind: z4.literal("multi"), optionIds: z4.array(z4.string()), labels: z4.array(z4.string()).optional() }),
351
+ z4.object({ kind: z4.literal("ranked"), optionIds: z4.array(z4.string()), labels: z4.array(z4.string()).optional() }),
352
+ z4.object({ kind: z4.literal("clarify"), chunks: z4.array(z4.string()).min(1) }),
353
+ z4.object({ kind: z4.literal("confirm"), approved: z4.boolean() }),
354
+ z4.object({ kind: z4.literal("turns"), turns: z4.array(TurnSchema).min(1) }),
368
355
  /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
369
356
  * delivered through the same settle/await path as a human answer, carrying the judge's
370
357
  * derivation and the precedent ids it grew from. Always paired with a visible trail
371
358
  * card the user can reply to — the broker never overrides the user. */
372
- z3.object({ kind: z3.literal("precedent"), answer: z3.string(), derivation: z3.string(), sources: z3.array(z3.string()).min(1) })
359
+ z4.object({ kind: z4.literal("precedent"), answer: z4.string(), derivation: z4.string(), sources: z4.array(z4.string()).min(1) })
373
360
  ]);
374
- var IntentSchema = z3.object({
361
+ var IntentSchema = z4.object({
375
362
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
376
363
  // it by two ("detail", "feedback"), and because the settle handler parsed the array
377
364
  // all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
378
365
  // questions included. Found auditing five calls' stored feedback, 2026-08-01.
379
- kind: z3.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
380
- detail: z3.string(),
366
+ kind: z4.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
367
+ detail: z4.string(),
381
368
  /** Defer only: seconds until the callback the caller asked for, when something upstream
382
369
  * already read the time. Nothing sets it today (#397 documented an MCP parser that was
383
370
  * never written) — the API reads the defer's `detail` itself with `notes/when.ts`
384
371
  * (`parseDelay`, #1292), and a value here simply wins over that reading. */
385
- dueInSeconds: z3.number().int().positive().optional(),
372
+ dueInSeconds: z4.number().int().positive().optional(),
386
373
  /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
387
374
  * already read the utterance, so `feedback_from_call.kind` stops defaulting to
388
375
  * 'other' on every row. A table that records that something was wrong and nothing
389
376
  * about what cannot answer "is the bot looping less this week?". */
390
- fault: z3.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
377
+ fault: z4.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
391
378
  });
392
- var RideAlongSchema = z3.object({
379
+ var RideAlongSchema = z4.object({
393
380
  /** The note this came from — assign/clarify/close it through /api/notes/:id. */
394
- noteId: z3.string(),
381
+ noteId: z4.string(),
395
382
  /** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
396
- text: z3.string(),
383
+ text: z4.string(),
397
384
  /** The thread to report back on, when the note was dispatched over the request rail. */
398
- parentId: z3.string().nullable()
385
+ parentId: z4.string().nullable()
399
386
  });
400
- var AwaitItemSchema = z3.discriminatedUnion("type", [
401
- z3.object({
402
- type: z3.literal("reply"),
403
- parentId: z3.string(),
404
- notificationId: z3.string(),
405
- workId: z3.string().uuid().optional(),
406
- decisionId: z3.string().uuid().optional(),
387
+ var AwaitItemSchema = z4.discriminatedUnion("type", [
388
+ z4.object({
389
+ type: z4.literal("reply"),
390
+ parentId: z4.string(),
391
+ notificationId: z4.string(),
392
+ workId: z4.string().uuid().optional(),
393
+ decisionId: z4.string().uuid().optional(),
407
394
  answer: UserAnswerSchema,
408
395
  /** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
409
396
  * #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
@@ -413,118 +400,118 @@ var AwaitItemSchema = z3.discriminatedUnion("type", [
413
400
  * "call me back after you merge" in their own words decides for itself what to do,
414
401
  * and now knows exactly which call to make. Absent when either half is missing —
415
402
  * a sentence with a hole in it is worse than no sentence. */
416
- note: z3.string().optional(),
403
+ note: z4.string().optional(),
417
404
  /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
418
405
  * shaped answer was mapped from, filtered to its own claims. There is no second list
419
406
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
420
407
  * 2026-09-04): the agent reads the sentence and decides. */
421
- transcript: z3.string().optional(),
408
+ transcript: z4.string().optional(),
422
409
  /** Coverage report (#396), when the ask declared `points`: which of them this
423
410
  * answer addressed. Missing points = re-ask or proceed knowingly partial. */
424
- covered: z3.array(z3.string()).optional(),
411
+ covered: z4.array(z4.string()).optional(),
425
412
  /** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
426
413
  * became free. Only `reply` and `idle` carry it: those are the two outcomes that
427
414
  * END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
428
415
  * agent a side-quest while it is still holding the line is how the main thing gets
429
416
  * dropped. Absent/empty = nothing owed. */
430
- also: z3.array(RideAlongSchema).optional()
417
+ also: z4.array(RideAlongSchema).optional()
431
418
  }),
432
- z3.object({
433
- type: z3.literal("remind"),
434
- parentId: z3.string(),
435
- notificationId: z3.string(),
436
- remindAt: z3.string().datetime({ offset: true }),
419
+ z4.object({
420
+ type: z4.literal("remind"),
421
+ parentId: z4.string(),
422
+ notificationId: z4.string(),
423
+ remindAt: z4.string().datetime({ offset: true }),
437
424
  /** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
438
- remindInSeconds: z3.number()
425
+ remindInSeconds: z4.number()
439
426
  }),
440
427
  /** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
441
428
  * post-feedback revision, #633) — the user will never answer this id. Stop
442
429
  * awaiting it; the live ask is the thread's newest turn (await that one, or
443
- * re-orient via get_thread / check_replies). */
444
- z3.object({
445
- type: z3.literal("superseded"),
446
- parentId: z3.string(),
447
- notificationId: z3.string()
430
+ * re-orient via check_replies). */
431
+ z4.object({
432
+ type: z4.literal("superseded"),
433
+ parentId: z4.string(),
434
+ notificationId: z4.string()
448
435
  }),
449
436
  /** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
450
437
  * revise any of these until the final reply arrives — partial = intelligence,
451
438
  * settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
452
439
  * irreversibly. If `acts` carries a question aimed at you and you know the answer,
453
440
  * contact on the same thread right away — the caller hears it on the same call. */
454
- z3.object({
455
- type: z3.literal("partial"),
456
- notificationId: z3.string(),
457
- inFlight: z3.literal(true),
458
- turn: z3.object({
459
- idx: z3.number(),
460
- prompt: z3.string(),
461
- reply: z3.string(),
462
- acts: z3.array(IntentSchema).nullable().optional()
441
+ z4.object({
442
+ type: z4.literal("partial"),
443
+ notificationId: z4.string(),
444
+ inFlight: z4.literal(true),
445
+ turn: z4.object({
446
+ idx: z4.number(),
447
+ prompt: z4.string(),
448
+ reply: z4.string(),
449
+ acts: z4.array(IntentSchema).nullable().optional()
463
450
  })
464
451
  }),
465
- z3.object({
466
- type: z3.literal("idle"),
467
- also: z3.array(RideAlongSchema).optional(),
452
+ z4.object({
453
+ type: z4.literal("idle"),
454
+ also: z4.array(RideAlongSchema).optional(),
468
455
  /** Is a call live for this agent's user right now? The SDK polls the partial stream
469
456
  * (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
470
457
  * during a live call, and polling for one on a banner/message was a wasted HTTP call +
471
458
  * 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
472
459
  * Absent = an older API → the SDK keeps polling, exactly as before. */
473
- inFlight: z3.boolean().optional()
460
+ inFlight: z4.boolean().optional()
474
461
  })
475
462
  ]);
476
- var VoiceKeySchema = z3.enum(["rachel", "george", "jessica", "brian", "lily"]);
477
- var AgendaTurnSchema = z3.object({
463
+ var VoiceKeySchema = z4.enum(["rachel", "george", "jessica", "brian", "lily"]);
464
+ var AgendaTurnSchema = z4.object({
478
465
  /** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
479
466
  * it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
480
467
  * sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
481
468
  * completion of a streamed re-plan carries the published sentence again, and the walk
482
469
  * drops what it already said by identity, never by the API's guess of what was polled.
483
470
  * Absent on plans nothing streams (a ring plan, a floor). */
484
- id: z3.string().optional(),
471
+ id: z4.string().optional(),
485
472
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
486
473
  * the planner declares duplicates instead of asking them twice. */
487
- coveredIds: z3.array(z3.string()).optional(),
474
+ coveredIds: z4.array(z4.string()).optional(),
488
475
  /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
489
476
  * line went out on 2026-07-28 and the caller could not answer it at all. */
490
- info: z3.array(z3.string().min(1)).max(3).default([]),
491
- question: z3.string().min(1).nullable(),
477
+ info: z4.array(z4.string().min(1)).max(3).default([]),
478
+ question: z4.string().min(1).nullable(),
492
479
  /** True on the one turn carrying the agent's own declared question. */
493
- asks: z3.boolean().optional(),
480
+ asks: z4.boolean().optional(),
494
481
  /** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
495
482
  * Absent on a single-claim plan (the session's own claim) and on shared context turns,
496
483
  * which route nothing. */
497
- claimId: z3.string().optional(),
484
+ claimId: z4.string().optional(),
498
485
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
499
- voice: z3.string().optional(),
486
+ voice: z4.string().optional(),
500
487
  /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
501
488
  * whose request this is: an item that folded in from another agent arrived as a bare
502
489
  * non-sequitur ("First real production sign-in is yours to make whenever you want.")
503
490
  * and the owner answered "What?". The bot names the agent before its first turn. */
504
- agent: z3.string().optional(),
491
+ agent: z4.string().optional(),
505
492
  /** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
506
493
  * same id a face is minted from. A name is not an identity: two pairings may be called
507
494
  * "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
508
495
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
509
496
  * speaks for several agents — the turn is the only place that knows which. */
510
- agentId: z3.string().optional(),
497
+ agentId: z4.string().optional(),
511
498
  select: SelectShapeSchema.optional(),
512
- options: z3.array(OptionSchema.omit({ id: true })).optional(),
499
+ options: z4.array(OptionSchema.omit({ id: true })).optional(),
513
500
  /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
514
501
  * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
515
502
  * (the beat for context, the answer window for asks). Clamped bot-side. */
516
- pace: z3.number().positive().optional(),
503
+ pace: z4.number().positive().optional(),
517
504
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
518
505
  * (a question blocks, context flows). blocking:false on a question = ask and move
519
506
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
520
- blocking: z3.boolean().optional()
507
+ blocking: z4.boolean().optional()
521
508
  });
522
509
  var CLAIM_STALE_MS = 30 * 6e4;
523
- var InboxItemSchema = z3.object({
524
- id: z3.string(),
510
+ var InboxItemSchema = z4.object({
511
+ id: z4.string(),
525
512
  /** The conversation thread + connection this item lives on. Present on the replied
526
513
  * detail — they power History's "Continue" / "New session from this" (#57/#251). */
527
- parentId: z3.string().optional(),
514
+ parentId: z4.string().optional(),
528
515
  /** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
529
516
  * arrival and its units are N rows of it, so this — not `parentId` — is what makes a
530
517
  * multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
@@ -532,13 +519,13 @@ var InboxItemSchema = z3.object({
532
519
  * unrelated updates as a single "12-part request". Absent on rows written before the
533
520
  * `asks` table, and on anything that never went through `notify` — both fall back to the
534
521
  * thread, which is what the client did for all rows until now. */
535
- askId: z3.string().optional(),
522
+ askId: z4.string().optional(),
536
523
  /** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
537
524
  * shares one `created_at` to the microsecond, so without it the author's order is
538
525
  * unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
539
526
  * (live 2026-08-10, D35). The API already orders by it; this lets a reader that
540
527
  * re-sorts (grouping, filtering) put an arrival back in the order it was written. */
541
- seq: z3.number().int().optional(),
528
+ seq: z4.number().int().optional(),
542
529
  /** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
543
530
  * `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
544
531
  * counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
@@ -547,27 +534,23 @@ var InboxItemSchema = z3.object({
547
534
  * server that can still see every row states it. Absent on any row with no `askId`: a
548
535
  * unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
549
536
  * the one part of its own arrival a single row cannot answer. */
550
- units: z3.number().int().positive().optional(),
551
- tokenId: z3.string().optional(),
537
+ units: z4.number().int().positive().optional(),
538
+ tokenId: z4.string().optional(),
552
539
  status: NotifyStatusSchema,
553
540
  context: ContextSchema,
554
- options: z3.array(OptionSchema).optional(),
541
+ options: z4.array(OptionSchema).optional(),
555
542
  /** The ask's declared coverage points (#396), when the agent sent them. */
556
- points: z3.array(z3.string()).optional(),
557
- /** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
558
- * ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
559
- * between them could not express a call as a sequence. `question: null` is a real turn —
560
- * a status update stays a statement instead of being shaped into a yes/no. */
543
+ points: z4.array(z4.string()).optional(),
561
544
  /** Does this claim want an ANSWER, or is it telling you something? Written per row from
562
545
  * `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
563
546
  * its acknowledge affordance: without it a status update offers a text box and a dismiss,
564
547
  * and neither of those is "got it" (owner, 2026-08-10). */
565
- asks: z3.boolean().optional(),
548
+ asks: z4.boolean().optional(),
566
549
  /** When a live process last pulsed for this row's agent — the liveness input for
567
550
  * "working requires a pulse" (#928): the list said "Working…" from agent_state alone
568
551
  * while the party called the same dead claim stalled. Absent = no token/no data,
569
552
  * which must never CLAIM stalled. */
570
- lastSeenAt: z3.string().optional(),
553
+ lastSeenAt: z4.string().optional(),
571
554
  /** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
572
555
  * the `notification_events` ledger (trigger-written since 20260621010000, so every row a
573
556
  * user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
@@ -577,53 +560,81 @@ var InboxItemSchema = z3.object({
577
560
  * work. Reading the row's birth as the claim's age brands that "No update in 8h" the
578
561
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
579
562
  * `createdAt`. */
580
- agentStateAt: z3.string().datetime().optional(),
581
- agenda: z3.array(AgendaTurnSchema).optional(),
582
- visuals: z3.array(VisualSchema).optional(),
563
+ agentStateAt: z4.string().datetime().optional(),
564
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
565
+ * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
566
+ * superseded or cancelled need is no longer a question anyone is asked). Present only on a
567
+ * Call's cards, and every card of that Call carries the same list: the call screen reads it
568
+ * once, off the one read it already makes (`GET /api/inbox/:callId`).
569
+ *
570
+ * It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
571
+ * every cycle, so a spine drawn from them would change length under the caller mid-call.
572
+ *
573
+ * `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
574
+ * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
575
+ * the card's own concise heading; `answer` the accepted answer in words, null while open. It
576
+ * REPLACED `agenda` (turns), which nothing ever filled. */
577
+ questions: z4.array(z4.object({
578
+ id: z4.string(),
579
+ entryId: z4.string(),
580
+ title: z4.string(),
581
+ state: z4.enum(["open", "answered"]),
582
+ answer: z4.string().nullable()
583
+ })).optional(),
584
+ visuals: z4.array(VisualSchema).optional(),
583
585
  /** The connected agent's name (the single pairing name — user-typed, or the
584
586
  * agent's suggestion, or a default silly name). */
585
- name: z3.string(),
587
+ name: z4.string(),
586
588
  /** The pairing's assigned voice (#462); absent = the default voice. */
587
589
  voice: VoiceKeySchema.optional(),
588
- repo: z3.string().optional(),
589
- branch: z3.string().optional(),
590
- createdAt: z3.string().datetime(),
591
- snoozedUntil: z3.string().datetime().optional(),
590
+ repo: z4.string().optional(),
591
+ branch: z4.string().optional(),
592
+ createdAt: z4.string().datetime(),
593
+ snoozedUntil: z4.string().datetime().optional(),
592
594
  agentState: AgentStateSchema.default("idle"),
593
595
  /** Whose action the item is waiting on: "you" = an agent asked you (the default,
594
596
  * every agent→user notification); "agent" = you sent a request and it's awaiting the
595
597
  * agent (held in the inbox until the agent replies on the thread). */
596
- turn: z3.enum(["you", "agent"]).default("you"),
598
+ turn: z4.enum(["you", "agent"]).default("you"),
597
599
  /** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
598
600
  * the agent (provider-agnostic; set server-side). Absent = no hard error, though the
599
601
  * client may still flag a stall by age. Drives the inbox error badge + Retry. */
600
- error: z3.string().optional(),
601
- clarifies: z3.string().optional(),
602
- /** The ring ladder ran out while this was still pending — we tried to reach you and
603
- * STOPPED trying (`arbitration/arbitrate.ts` `nextRing` → `stop`). Distinct from an
604
- * agent with nothing to say, which the roster drew identically until now: "nothing to
605
- * say" and "gave up saying it" are opposite situations wearing the same face
606
- * (navigation-design.md, gap 1). False for anything that never rang. */
607
- gaveUp: z3.boolean().default(false),
602
+ error: z4.string().optional(),
603
+ clarifies: z4.string().optional(),
604
+ /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
605
+ * declined, and what the ladder will do next — read off the cron's own row, never computed
606
+ * on the phone. Present only while a `declined` receipt stands on the card's last Call.
607
+ * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
608
+ * `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
609
+ * has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
610
+ * It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
611
+ * never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
612
+ * and the roster's `unreached` read `declinedAt`, and stand while it does. */
613
+ ring: z4.object({
614
+ declinedAt: z4.string().datetime(),
615
+ anchorAt: z4.string().datetime(),
616
+ nextRingAt: z4.string().datetime().nullable(),
617
+ step: z4.number().int()
618
+ }).optional(),
608
619
  /** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
609
620
  * Absent for anything never delivered through a push, and for older rows written before
610
621
  * the reason was recorded. Deliberately a debug affordance, shown small (owner,
611
622
  * 2026-08-07) — its real job is to give "this didn't need a call" something to be
612
623
  * feedback ABOUT. */
613
- why: z3.object({
624
+ why: z4.object({
614
625
  asked: NotifyLevelSchema,
615
626
  got: NotifyLevelSchema,
616
- because: z3.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
617
- line: z3.string()
627
+ because: z4.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
628
+ line: z4.string()
618
629
  }).optional(),
619
- select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
620
- confirmStyle: z3.enum(["yesno", "approve"]).default("yesno").describe(
630
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
631
+ confirmStyle: z4.enum(["yesno", "approve"]).default("yesno").describe(
621
632
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
622
633
  ),
623
634
  /** Real downstream work is stuck behind this one — set by the agent, independent of
624
635
  * urgency (see the main README's "premier use case" + notify/states.md). Drives the
625
636
  * inbox's blocking badge and the extra confirm step before dismissing it. */
626
- blocking: z3.boolean().default(false),
637
+ blocking: z4.boolean().default(false),
627
638
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
628
639
  answer: UserAnswerSchema.optional(),
629
640
  /** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
@@ -631,35 +642,35 @@ var InboxItemSchema = z3.object({
631
642
  * for a request that asks nothing), whether its content is sealed, and that Goal's state. The
632
643
  * answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
633
644
  * here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
634
- communication: z3.object({
635
- deliveryId: z3.string(),
636
- kind: z3.enum(["notification", "call"]),
637
- entryId: z3.string(),
638
- goalIds: z3.array(z3.string()),
639
- decisionNeedId: z3.string().optional(),
640
- sealed: z3.boolean(),
641
- goalState: z3.string().optional()
645
+ communication: z4.object({
646
+ deliveryId: z4.string(),
647
+ kind: z4.enum(["notification", "call"]),
648
+ entryId: z4.string(),
649
+ goalIds: z4.array(z4.string()),
650
+ decisionNeedId: z4.string().optional(),
651
+ sealed: z4.boolean(),
652
+ goalState: z4.string().optional()
642
653
  }).optional()
643
654
  });
644
655
  var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
645
- var PushTokenSchema = z3.object({
646
- voipToken: z3.string().min(1).optional(),
647
- alertToken: z3.string().min(1).optional(),
648
- fcmToken: z3.string().min(1).optional(),
649
- platform: z3.enum(["ios", "android"])
656
+ var PushTokenSchema = z4.object({
657
+ voipToken: z4.string().min(1).optional(),
658
+ alertToken: z4.string().min(1).optional(),
659
+ fcmToken: z4.string().min(1).optional(),
660
+ platform: z4.enum(["ios", "android"])
650
661
  }).superRefine((v, ctx) => {
651
662
  if (v.platform !== "ios") return;
652
663
  for (const field of ["voipToken", "alertToken"]) {
653
664
  const token = v[field];
654
665
  if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
655
666
  ctx.addIssue({
656
- code: z3.ZodIssueCode.custom,
667
+ code: z4.ZodIssueCode.custom,
657
668
  path: [field],
658
669
  message: `not an APNs device token (want 64 hex chars, got ${token.length})`
659
670
  });
660
671
  }
661
672
  });
662
- var MissedCallSchema = z3.enum([
673
+ var MissedCallSchema = z4.enum([
663
674
  "retry_10m",
664
675
  "retry_30m",
665
676
  "retry_60m",
@@ -669,32 +680,34 @@ var MissedCallSchema = z3.enum([
669
680
  "inbox",
670
681
  "dismiss"
671
682
  ]);
672
- var BrokerTuningSchema = z3.object({
683
+ var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
684
+ var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
685
+ var BrokerTuningSchema = z4.object({
673
686
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
674
- ackVerbosity: z3.enum(["normal", "none"]).optional(),
687
+ ackVerbosity: z4.enum(["normal", "none"]).optional(),
675
688
  /** How readily the mapper asks its one clarification: 'low' = only when truly
676
689
  * uninterpretable, 'high' = whenever not fully certain. */
677
- clarifyEagerness: z3.enum(["low", "normal", "high"]).optional(),
690
+ clarifyEagerness: z4.enum(["low", "normal", "high"]).optional(),
678
691
  /** The user's own shorthand: when they say `say`, they mean `mean`. */
679
- phrasebook: z3.array(z3.object({ say: z3.string().min(1).max(60), mean: z3.string().min(1).max(120) })).max(24).optional(),
692
+ phrasebook: z4.array(z4.object({ say: z4.string().min(1).max(60), mean: z4.string().min(1).max(120) })).max(24).optional(),
680
693
  /** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
681
694
  * which is every account today — means the agent's own words decide, per ask: a call
682
695
  * about an English ask opens in English. This is the only thing that overrides that,
683
696
  * and a live caller who switches language mid-call still outranks it (broker/lang.ts).
684
697
  * Set per user (no UI yet), like `voiceTuning`. */
685
- language: z3.enum(["en", "es"]).optional()
698
+ language: z4.enum(["en", "es"]).optional()
686
699
  });
687
- var UserSettingsSchema = z3.object({
688
- permissions: z3.object({
689
- call: z3.boolean(),
690
- banner: z3.boolean(),
691
- push: z3.boolean()
700
+ var UserSettingsSchema = z4.object({
701
+ permissions: z4.object({
702
+ call: z4.boolean(),
703
+ banner: z4.boolean(),
704
+ push: z4.boolean()
692
705
  }),
693
- sessionMode: z3.enum(["default", "all_calls", "silent"]),
694
- silentPush: z3.boolean(),
695
- autoCallback: z3.boolean(),
706
+ sessionMode: z4.enum(["default", "all_calls", "silent"]),
707
+ silentPush: z4.boolean(),
708
+ autoCallback: z4.boolean(),
696
709
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
697
- improveConsent: z3.boolean(),
710
+ improveConsent: z4.boolean(),
698
711
  missedCall: MissedCallSchema.default("backoff_standard"),
699
712
  /** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
700
713
  * (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
@@ -702,7 +715,13 @@ var UserSettingsSchema = z3.object({
702
715
  * Optional, NOT defaulted: a stale client PATCHing the full settings object
703
716
  * must not silently reset this privacy choice. Absent = leave unchanged on
704
717
  * write, 'hosted' on read (see store.ts). */
705
- voiceMode: z3.enum(["hosted", "on_device"]).optional(),
718
+ voiceMode: z4.enum(["hosted", "on_device"]).optional(),
719
+ /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
720
+ * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
721
+ * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
722
+ * object must not silently turn it back on. Absent = leave unchanged on write, true on
723
+ * read (see store.ts). */
724
+ talk: z4.boolean().optional(),
706
725
  /** Per-user ring budget (#603): calls per rolling day before further calls
707
726
  * degrade to banner. Absent = the global default (25). A number, never a
708
727
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -710,10 +729,10 @@ var UserSettingsSchema = z3.object({
710
729
  * payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
711
730
  * slower speaker). No API-side semantics; the bot resolves each key with its
712
731
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
713
- voiceTuning: z3.record(z3.string(), z3.union([z3.number(), z3.string()])).optional(),
732
+ voiceTuning: z4.record(z4.string(), z4.union([z4.number(), z4.string()])).optional(),
714
733
  /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
715
734
  * defaulted — an older client PATCHing the full object must not clobber it. */
716
- pstnCalls: z3.boolean().optional(),
735
+ pstnCalls: z4.boolean().optional(),
717
736
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
718
737
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
719
738
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -722,52 +741,52 @@ var UserSettingsSchema = z3.object({
722
741
  * that failure reads as the reminder rail being unreliable rather than as a missing
723
742
  * setting. Absent = a spoken time can't be landed, so the reminder rides the next
724
743
  * call — honest about what we know. */
725
- timezone: z3.string().min(1).max(64).optional(),
744
+ timezone: z4.string().min(1).max(64).optional(),
726
745
  /** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
727
746
  * clobber guard as voiceMode: absent = leave unchanged on write. */
728
747
  broker: BrokerTuningSchema.optional()
729
748
  });
730
- var HistoryItemSchema = z3.object({
731
- id: z3.string(),
732
- parentId: z3.string(),
749
+ var HistoryItemSchema = z4.object({
750
+ id: z4.string(),
751
+ parentId: z4.string(),
733
752
  /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
734
- initiator: z3.enum(["user", "agent"]),
735
- title: z3.string(),
753
+ initiator: z4.enum(["user", "agent"]),
754
+ title: z4.string(),
736
755
  /** The agent on the other end (its name). */
737
- name: z3.string(),
738
- createdAt: z3.string(),
756
+ name: z4.string(),
757
+ createdAt: z4.string(),
739
758
  /** When the agent fetched your request (user→agent only). */
740
- agentAckedAt: z3.string().nullable(),
759
+ agentAckedAt: z4.string().nullable(),
741
760
  /** When you answered the agent's notification (agent→user only). */
742
- humanAckedAt: z3.string().nullable()
761
+ humanAckedAt: z4.string().nullable()
743
762
  });
744
763
  var ACTIVITY_LINES = 2;
745
764
  var ACTIVITY_LINE_MAX = 80;
746
- var AgentActivitySchema = z3.object({
765
+ var AgentActivitySchema = z4.object({
747
766
  /** Oldest first, so the newest line is last — the one that replaces in place. */
748
- lines: z3.array(z3.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
767
+ lines: z4.array(z4.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
749
768
  /** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
750
769
  * that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
751
- at: z3.string().datetime()
770
+ at: z4.string().datetime()
752
771
  });
753
- var ConnectionSummarySchema = z3.object({
772
+ var ConnectionSummarySchema = z4.object({
754
773
  /** The connection = the agent's token id (used to address a request). */
755
- id: z3.string(),
774
+ id: z4.string(),
756
775
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
757
776
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
758
777
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
759
778
  * apps/api/src/tokens/devices-vs-agents-design.md. */
760
- kind: z3.enum(["device", "agent"]).optional(),
779
+ kind: z4.enum(["device", "agent"]).optional(),
761
780
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
762
781
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
763
782
  * agents (phone-launched, provider-managed, or minted before the link existed). */
764
- mintedByDevice: z3.string().nullable().optional(),
765
- device: z3.string().nullable(),
783
+ mintedByDevice: z4.string().nullable().optional(),
784
+ device: z4.string().nullable(),
766
785
  /** The agent's display name (the single pairing name). */
767
- name: z3.string(),
786
+ name: z4.string(),
768
787
  /** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
769
788
  * label; null for a local connection. Sourced from the token's provider, not the name. */
770
- provider: z3.string().nullable(),
789
+ provider: z4.string().nullable(),
771
790
  /** The pairing's assigned voice (#462); null = the default voice. */
772
791
  voice: VoiceKeySchema.nullable(),
773
792
  /** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
@@ -778,22 +797,22 @@ var ConnectionSummarySchema = z3.object({
778
797
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
779
798
  * set once must not overrule a rule they set about one agent. */
780
799
  reach: NotifyLevelSchema.nullable().optional(),
781
- createdAt: z3.string().datetime(),
800
+ createdAt: z4.string().datetime(),
782
801
  /** Most recent notification on this connection, either direction. Null = no contact yet.
783
802
  * Drives the agents-page recency grouping (Today / This week / …). */
784
- lastContactAt: z3.string().datetime().nullable(),
803
+ lastContactAt: z4.string().datetime().nullable(),
785
804
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
786
805
  * desktop app while open. Null = never seen; stale = offline. */
787
- lastSeenAt: z3.string().datetime().nullable().optional(),
806
+ lastSeenAt: z4.string().datetime().nullable().optional(),
788
807
  /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
789
808
  * harness availabilities + granted workspaces — the option set the phone's
790
809
  * "new session" sheet offers. Absent for ordinary MCP agents. */
791
- runtime: z3.object({
810
+ runtime: z4.object({
792
811
  /** The @paigy/harness this host is running — a machine the self-update has not reached
793
812
  * shows its age here (`apps/desktop/src/update.ts`). */
794
- version: z3.string().optional(),
795
- harnesses: z3.array(z3.object({ name: z3.string(), label: z3.string(), status: z3.string() })).optional(),
796
- workspaces: z3.array(z3.string()).optional()
813
+ version: z4.string().optional(),
814
+ harnesses: z4.array(z4.object({ name: z4.string(), label: z4.string(), status: z4.string() })).optional(),
815
+ workspaces: z4.array(z4.string()).optional()
797
816
  }).optional(),
798
817
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
799
818
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -802,148 +821,156 @@ var ConnectionSummarySchema = z3.object({
802
821
  activity: AgentActivitySchema.optional(),
803
822
  /** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
804
823
  * false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
805
- managed: z3.boolean()
824
+ managed: z4.boolean()
806
825
  });
807
- var LedgerItemSchema = z3.object({ id: z3.string(), parentId: z3.string(), title: z3.string(), createdAt: z3.string() });
808
- var AgentLedgerSchema = z3.object({
809
- agent: z3.object({ id: z3.string(), name: z3.string(), revokedAt: z3.string().nullable() }),
826
+ var LedgerItemSchema = z4.object({ id: z4.string(), parentId: z4.string(), title: z4.string(), createdAt: z4.string() });
827
+ var AgentLedgerSchema = z4.object({
828
+ agent: z4.object({ id: z4.string(), name: z4.string(), revokedAt: z4.string().nullable() }),
810
829
  /** Its own questions you have not answered. */
811
- asks: z3.array(LedgerItemSchema),
830
+ asks: z4.array(LedgerItemSchema),
812
831
  /** Its questions you answered that nobody acted on — still owed to somebody. */
813
- answered: z3.array(LedgerItemSchema),
832
+ answered: z4.array(LedgerItemSchema),
814
833
  /** Requests you sent it that it never took. */
815
- requests: z3.array(LedgerItemSchema),
816
- goals: z3.array(z3.object({ id: z3.string(), outcome: z3.string(), state: z3.string() })),
817
- callbacks: z3.array(z3.object({ id: z3.string(), parentId: z3.string(), trigger: z3.string(), note: z3.string(), dueAt: z3.string().nullable() }))
834
+ requests: z4.array(LedgerItemSchema),
835
+ goals: z4.array(z4.object({ id: z4.string(), outcome: z4.string(), state: z4.string() })),
836
+ callbacks: z4.array(z4.object({ id: z4.string(), parentId: z4.string(), trigger: z4.string(), note: z4.string(), dueAt: z4.string().nullable() }))
818
837
  });
819
- var ReassignResultSchema = z3.object({
820
- moved: z3.object({ asks: z3.number(), answered: z3.number(), requests: z3.number(), goals: z3.number(), callbacks: z3.number() }),
821
- parentId: z3.string().nullable()
838
+ var ReassignResultSchema = z4.object({
839
+ moved: z4.object({ asks: z4.number(), answered: z4.number(), requests: z4.number(), goals: z4.number(), callbacks: z4.number() }),
840
+ parentId: z4.string().nullable()
822
841
  });
823
- var MoveRingSchema = z3.enum(["home", "travels", "retired", "quarantined"]);
824
- var MoveSchema = z3.object({
825
- id: z3.string(),
842
+ var MoveRingSchema = z4.enum(["home", "travels", "retired", "quarantined"]);
843
+ var MoveSchema = z4.object({
844
+ id: z4.string(),
826
845
  /** The reusable question, as distill normalized it. */
827
- question: z3.string(),
846
+ question: z4.string(),
828
847
  /** The operative ruling. Editable by the user (PATCH) — which resets the ledger. */
829
- answer: z3.string(),
848
+ answer: z4.string(),
830
849
  /** The user's stated reason, when they gave one. Null = inherently narrow: the judge is
831
850
  * told so, and the ruling only derives essentially the same question in the same scope. */
832
- rationale: z3.string().nullable(),
851
+ rationale: z4.string().nullable(),
833
852
  /** Where the ruling lives: a repo/workspace, or 'global'. */
834
- scope: z3.string(),
853
+ scope: z4.string(),
835
854
  ring: MoveRingSchema,
836
855
  /** True = the user pinned it with `always` (travel granted by hand, not by evidence). */
837
- pinned: z3.boolean(),
856
+ pinned: z4.boolean(),
838
857
  /** True = a pin the user placed was BROKEN by later counter-evidence. Surfaced so the
839
858
  * break is visible instead of a pin silently disappearing. */
840
- pinBroken: z3.boolean(),
859
+ pinBroken: z4.boolean(),
841
860
  /** When the ruling was distilled. */
842
- learnedAt: z3.string(),
861
+ learnedAt: z4.string(),
843
862
  /** Last time it answered an ask. Null = never fired. */
844
- lastUsedAt: z3.string().nullable(),
863
+ lastUsedAt: z4.string().nullable(),
845
864
  /** How many asks it has answered. Instrumentation — deliberately NOT an input to the
846
865
  * evidence curve: firing says the question keeps arising, not that the ruling is right. */
847
- usedCount: z3.number(),
866
+ usedCount: z4.number(),
848
867
  /** Ledger: outcomes that said it held up. Saturating — the tenth is worth almost nothing. */
849
- confirms: z3.number(),
868
+ confirms: z4.number(),
850
869
  /** Ledger: contradictions, in signal units (a full override = 1, weaker signals less).
851
870
  * Linear and priced above the entire confirmation budget, so any full counter wins. */
852
- counters: z3.number(),
871
+ counters: z4.number(),
853
872
  /** The agent that asked the question this move came from, when known. Null for a move
854
873
  * distilled from a clarify ruling (those carry no agent) or one whose source rows are gone. */
855
- learnedFrom: z3.object({ id: z3.string(), name: z3.string() }).nullable()
874
+ learnedFrom: z4.object({ id: z4.string(), name: z4.string() }).nullable()
856
875
  });
857
- var QueueQuestionSchema = z3.object({
876
+ var QueueQuestionSchema = z4.object({
858
877
  /** The decision need's id — what an answer is accepted against. */
859
- id: z3.string(),
878
+ id: z4.string(),
860
879
  /** The words that were asked, from the request Entry that asked them. */
861
- question: z3.string(),
880
+ question: z4.string(),
862
881
  /** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
863
882
  * for a need whose request Entry is carried by no interactive Delivery, which nothing
864
883
  * can answer. */
865
- deliveryId: z3.string().nullable().default(null),
884
+ deliveryId: z4.string().nullable().default(null),
866
885
  /** The Entry the ruling is about. */
867
- aboutId: z3.string().nullable().default(null),
886
+ aboutId: z4.string().nullable().default(null),
868
887
  /** Empty for a free-text question. */
869
- options: z3.array(OptionSchema).default([]),
870
- select: z3.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
871
- askedAt: z3.string(),
888
+ options: z4.array(OptionSchema).default([]),
889
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
890
+ askedAt: z4.string(),
872
891
  /** Null while the question is open — which is how the page tells the two apart. */
873
- answeredAt: z3.string().nullable().default(null),
892
+ answeredAt: z4.string().nullable().default(null),
874
893
  /** The ruling in the person's own words, from the contribution that replied — not the
875
894
  * option id, which is not something anyone reads back. Null while it is open, and null
876
895
  * for a settled question whose reply carried nothing readable. */
877
- answer: z3.string().nullable().default(null),
896
+ answer: z4.string().nullable().default(null),
878
897
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
879
898
  * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
880
899
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
881
900
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
882
901
  * this is the field they agree through rather than each re-deriving it from the row it
883
902
  * arrived under. Required because the API projects it on every need it sends. */
884
- goalId: z3.string(),
903
+ goalId: z4.string(),
885
904
  /** True only while an unmet START gate holds the Goal — a Goal that merely waits to
886
905
  * *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
887
906
  * the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
888
907
  * gate at all. */
889
- blocked: z3.boolean().default(false)
908
+ blocked: z4.boolean().default(false)
890
909
  });
891
- var QueueItemSchema = z3.object({
892
- id: z3.string(),
910
+ var QueueItemSchema = z4.object({
911
+ id: z4.string(),
893
912
  /** One-line headline — the first sentence of the outcome. */
894
- title: z3.string(),
913
+ title: z4.string(),
895
914
  /** The outcome in full, verbatim: the person's own words are what an assignee sees. */
896
- intent: z3.string(),
915
+ intent: z4.string(),
897
916
  /** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
898
- state: z3.string(),
917
+ state: z4.string(),
899
918
  /** Who holds it (a participant ref); null when nobody does yet. */
900
- assignee: z3.string().nullable().default(null),
919
+ assignee: z4.string().nullable().default(null),
901
920
  /** What the agent last said it was doing; null if it has said nothing. */
902
- progress: z3.string().nullable().default(null),
903
- reviewPending: z3.boolean().default(false),
904
- dueAt: z3.string().nullable().default(null),
921
+ progress: z4.string().nullable().default(null),
922
+ /** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
923
+ * served only while it was written for the current note. Null means show the Goal's name. */
924
+ progressLine: z4.string().nullable().optional(),
925
+ reviewPending: z4.boolean().default(false),
926
+ dueAt: z4.string().nullable().default(null),
905
927
  /** The Goal this one was opened under; null at the root. */
906
- parentGoalId: z3.string().nullable().default(null),
928
+ parentGoalId: z4.string().nullable().default(null),
907
929
  /** Goals opened under this one — only those the same list holds. */
908
- childGoalIds: z3.array(z3.string()).default([]),
930
+ childGoalIds: z4.array(z4.string()).default([]),
909
931
  /** Goals this one waits on (start or finish gates). */
910
- dependencyGoalIds: z3.array(z3.string()).default([]),
932
+ dependencyGoalIds: z4.array(z4.string()).default([]),
911
933
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
912
- blocked: z3.boolean().default(false),
934
+ blocked: z4.boolean().default(false),
913
935
  /** Every decision need on it, open or settled — the page decides which to show. */
914
- questions: z3.array(QueueQuestionSchema).default([]),
936
+ questions: z4.array(QueueQuestionSchema).default([]),
915
937
  /** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
916
- repo: z3.string().nullable().optional(),
917
- createdAt: z3.string(),
918
- updatedAt: z3.string().nullable().default(null)
938
+ repo: z4.string().nullable().optional(),
939
+ createdAt: z4.string(),
940
+ updatedAt: z4.string().nullable().default(null),
941
+ /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
942
+ * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
943
+ * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
944
+ lastProgressAt: z4.string().nullable().optional()
919
945
  });
920
- var NoteSourceSchema = z3.enum(["app", "call"]);
921
- var NoteStatusSchema = z3.enum(["open", "assigned", "in_progress", "done"]);
922
- var NoteRepeatSchema = z3.enum(["once", "until_done"]);
923
- var DecisionSchema = z3.object({
924
- id: z3.string(),
946
+ var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
947
+ var NoteSourceSchema = z4.enum(["app", "call"]);
948
+ var NoteStatusSchema = z4.enum(["open", "assigned", "in_progress", "done"]);
949
+ var NoteRepeatSchema = z4.enum(["once", "until_done"]);
950
+ var DecisionSchema = z4.object({
951
+ id: z4.string(),
925
952
  /** The note this decision refines; null = recorded on a bare thread (the
926
953
  * extensibility seam — any conversation can accrue decisions). */
927
- noteId: z3.string().nullable(),
954
+ noteId: z4.string().nullable(),
928
955
  /** What was ambiguous — the broker's (or the user's own) question. */
929
- question: z3.string(),
956
+ question: z4.string(),
930
957
  /** The user's ruling; null while the question is open. */
931
- answer: z3.string().nullable(),
932
- decidedAt: z3.string().nullable(),
933
- createdAt: z3.string()
958
+ answer: z4.string().nullable(),
959
+ decidedAt: z4.string().nullable(),
960
+ createdAt: z4.string()
934
961
  });
935
- var NoteSchema = z3.object({
936
- id: z3.string(),
962
+ var NoteSchema = z4.object({
963
+ id: z4.string(),
937
964
  /** One-line headline (broker-titled; deterministic floor). */
938
- title: z3.string(),
965
+ title: z4.string(),
939
966
  /** The original intent, verbatim — assignees always see the user's own words. */
940
- intent: z3.string(),
967
+ intent: z4.string(),
941
968
  source: NoteSourceSchema,
942
969
  status: NoteStatusSchema,
943
970
  /** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
944
- assignee: z3.string().nullable(),
971
+ assignee: z4.string().nullable(),
945
972
  /** The request thread minted at assignment; null until assigned. */
946
- parentId: z3.string().nullable(),
973
+ parentId: z4.string().nullable(),
947
974
  /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
948
975
  * call — never a deadline. It only ever comes from the user's own words, so when it
949
976
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
@@ -952,131 +979,131 @@ var NoteSchema = z3.object({
952
979
  // Defaulted, not required: a Note from an API deploy older than the reminders
953
980
  // migration has none of these, and the defaults ARE what it means — no not-before,
954
981
  // one ride, never ridden. Parsing must not fail across a rolling deploy.
955
- dueAt: z3.string().nullable().default(null),
982
+ dueAt: z4.string().nullable().default(null),
956
983
  repeat: NoteRepeatSchema.default("once"),
957
984
  /** How many calls have already carried it — the fatigue cap counts rides, not days. */
958
- rides: z3.number().int().default(0),
959
- lastRideAt: z3.string().nullable().default(null),
960
- createdAt: z3.string()
985
+ rides: z4.number().int().default(0),
986
+ lastRideAt: z4.string().nullable().default(null),
987
+ createdAt: z4.string()
961
988
  });
962
- var TriageItemSchema = z3.object({
963
- noteId: z3.string(),
989
+ var TriageItemSchema = z4.object({
990
+ noteId: z4.string(),
964
991
  /** The note's headline at run time. */
965
- title: z3.string(),
992
+ title: z4.string(),
966
993
  /** WHY, in one short human line, evidence first — this is read on a phone underneath
967
994
  * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
968
995
  * Never a model's reasoning transcript, never an id. */
969
- why: z3.string()
996
+ why: z4.string()
970
997
  });
971
- var TriageAssignmentSchema = z3.object({
998
+ var TriageAssignmentSchema = z4.object({
972
999
  /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
973
- agent: z3.string(),
1000
+ agent: z4.string(),
974
1001
  /** Its display name at run time (the name on the hatchling's card). Denormalized for the
975
1002
  * same reason as `title`: the card must render from the proposal alone. */
976
- agentName: z3.string(),
977
- notes: z3.array(TriageItemSchema)
1003
+ agentName: z4.string(),
1004
+ notes: z4.array(TriageItemSchema)
978
1005
  });
979
- var TriageStatusSchema = z3.enum(["open", "superseded", "dismissed"]);
980
- var SubmitTriageSchema = z3.object({
1006
+ var TriageStatusSchema = z4.enum(["open", "superseded", "dismissed"]);
1007
+ var SubmitTriageSchema = z4.object({
981
1008
  /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
982
1009
  * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
983
1010
  * so the phone can say where the content went — an unattributed privacy claim is worth
984
1011
  * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
985
- provider: z3.string().min(1).max(60),
1012
+ provider: z4.string().min(1).max(60),
986
1013
  /** The concrete model when the provider names one (an ollama tag); null otherwise. */
987
- model: z3.string().max(200).nullable().optional(),
1014
+ model: z4.string().max(200).nullable().optional(),
988
1015
  /** How many open notes the run actually looked at — the denominator on the phone
989
1016
  * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
990
- reviewed: z3.number().int().min(0).max(1e4).default(0),
991
- close: z3.array(TriageItemSchema).max(200).default([]),
992
- stale: z3.array(TriageItemSchema).max(200).default([]),
993
- assign: z3.array(TriageAssignmentSchema).max(50).default([])
1017
+ reviewed: z4.number().int().min(0).max(1e4).default(0),
1018
+ close: z4.array(TriageItemSchema).max(200).default([]),
1019
+ stale: z4.array(TriageItemSchema).max(200).default([]),
1020
+ assign: z4.array(TriageAssignmentSchema).max(50).default([])
994
1021
  });
995
1022
  var TriageProposalSchema = SubmitTriageSchema.extend({
996
- id: z3.string(),
997
- runAt: z3.string(),
1023
+ id: z4.string(),
1024
+ runAt: z4.string(),
998
1025
  status: TriageStatusSchema,
999
- model: z3.string().nullable().default(null)
1026
+ model: z4.string().nullable().default(null)
1000
1027
  });
1001
- var AcceptTriageSchema = z3.discriminatedUnion("group", [
1002
- z3.object({ group: z3.literal("close"), noteIds: z3.array(z3.string()).max(200).optional() }),
1003
- z3.object({ group: z3.literal("stale"), noteIds: z3.array(z3.string()).max(200).optional() }),
1004
- z3.object({
1005
- group: z3.literal("assign"),
1006
- agent: z3.string().min(1),
1007
- noteIds: z3.array(z3.string()).max(200).optional()
1028
+ var AcceptTriageSchema = z4.discriminatedUnion("group", [
1029
+ z4.object({ group: z4.literal("close"), noteIds: z4.array(z4.string()).max(200).optional() }),
1030
+ z4.object({ group: z4.literal("stale"), noteIds: z4.array(z4.string()).max(200).optional() }),
1031
+ z4.object({
1032
+ group: z4.literal("assign"),
1033
+ agent: z4.string().min(1),
1034
+ noteIds: z4.array(z4.string()).max(200).optional()
1008
1035
  })
1009
1036
  ]);
1010
- var AcceptTriageResultSchema = z3.object({
1011
- accepted: z3.array(z3.string()),
1012
- failed: z3.array(z3.object({ noteId: z3.string(), reason: z3.string() }))
1037
+ var AcceptTriageResultSchema = z4.object({
1038
+ accepted: z4.array(z4.string()),
1039
+ failed: z4.array(z4.object({ noteId: z4.string(), reason: z4.string() }))
1013
1040
  });
1014
- var DeliveryModeSchema = z3.enum(["poll", "self_hosted"]);
1015
- var RegisterDeliverySchema = z3.object({ mode: DeliveryModeSchema });
1016
- var OAuthStartSchema = z3.object({
1017
- provider: z3.enum(["cma"]),
1018
- returnTo: z3.string().min(1)
1041
+ var DeliveryModeSchema = z4.enum(["poll", "self_hosted"]);
1042
+ var RegisterDeliverySchema = z4.object({ mode: DeliveryModeSchema });
1043
+ var OAuthStartSchema = z4.object({
1044
+ provider: z4.enum(["cma"]),
1045
+ returnTo: z4.string().min(1)
1019
1046
  });
1020
- var DeliveryConfigSchema = z3.object({
1021
- tokenId: z3.string(),
1047
+ var DeliveryConfigSchema = z4.object({
1048
+ tokenId: z4.string(),
1022
1049
  mode: DeliveryModeSchema,
1023
1050
  /** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
1024
1051
  * (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
1025
1052
  * carries credentials; only a `poll` registration can come back with null here. */
1026
- realtime: z3.object({ url: z3.string(), anonKey: z3.string() }).nullable()
1053
+ realtime: z4.object({ url: z4.string(), anonKey: z4.string() }).nullable()
1027
1054
  });
1028
- var WakeNudgeSchema = z3.object({
1029
- kind: z3.enum(["reply", "request", "callback"]),
1030
- notificationId: z3.string().optional(),
1031
- parentId: z3.string()
1055
+ var WakeNudgeSchema = z4.object({
1056
+ kind: z4.enum(["reply", "request", "callback"]),
1057
+ notificationId: z4.string().optional(),
1058
+ parentId: z4.string()
1032
1059
  });
1033
- var PairingStatusSchema = z3.enum(["pending", "approved", "denied", "expired"]);
1034
- var DeviceCodeSchema = z3.object({
1035
- device_code: z3.string(),
1036
- user_code: z3.string(),
1037
- verification_uri: z3.string().url(),
1038
- verification_uri_complete: z3.string().url(),
1039
- interval: z3.number(),
1040
- expires_in: z3.number()
1060
+ var PairingStatusSchema = z4.enum(["pending", "approved", "denied", "expired"]);
1061
+ var DeviceCodeSchema = z4.object({
1062
+ device_code: z4.string(),
1063
+ user_code: z4.string(),
1064
+ verification_uri: z4.string().url(),
1065
+ verification_uri_complete: z4.string().url(),
1066
+ interval: z4.number(),
1067
+ expires_in: z4.number()
1041
1068
  });
1042
- var DeviceInfoSchema = z3.object({
1043
- code: z3.string(),
1069
+ var DeviceInfoSchema = z4.object({
1070
+ code: z4.string(),
1044
1071
  /** The agent's suggested name (from /device/code) — shown on the approval screen,
1045
1072
  * pre-filling the name field the human can edit. */
1046
- name: z3.string(),
1073
+ name: z4.string(),
1047
1074
  /** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
1048
1075
  * build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
1049
1076
  * install crashes on the pairing screen on first launch, before the OTA lands
1050
1077
  * (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
1051
- agent: z3.string().optional(),
1052
- device: z3.string().nullable(),
1078
+ agent: z4.string().optional(),
1079
+ device: z4.string().nullable(),
1053
1080
  status: PairingStatusSchema
1054
1081
  });
1055
- var DeviceTokenSchema = z3.object({
1056
- access_token: z3.string(),
1082
+ var DeviceTokenSchema = z4.object({
1083
+ access_token: z4.string(),
1057
1084
  /** The pairing's single name (user-typed at approval, the agent's suggestion, or
1058
1085
  * a default silly name). */
1059
- name: z3.string(),
1060
- device: z3.string().nullable(),
1086
+ name: z4.string(),
1087
+ device: z4.string().nullable(),
1061
1088
  /** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
1062
1089
  * draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
1063
- voice: z3.string().nullable().optional(),
1090
+ voice: z4.string().nullable().optional(),
1064
1091
  /** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
1065
1092
  * that survives a rename. Cached by the host's identity beat. */
1066
- token_id: z3.string().nullable().optional(),
1093
+ token_id: z4.string().nullable().optional(),
1067
1094
  /** WHERE this identity works — the folder a wake should land it in. Written by the host
1068
1095
  * at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
1069
1096
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
1070
1097
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
1071
- workspace: z3.string().nullable().optional(),
1072
- uik_pub: z3.string().nullable().optional()
1098
+ workspace: z4.string().nullable().optional(),
1099
+ uik_pub: z4.string().nullable().optional()
1073
1100
  });
1074
- var SupportRequestSchema = z3.object({
1075
- email: z3.string().email().max(320),
1076
- message: z3.string().trim().min(1).max(5e3),
1077
- name: z3.string().trim().max(120).optional()
1101
+ var SupportRequestSchema = z4.object({
1102
+ email: z4.string().email().max(320),
1103
+ message: z4.string().trim().min(1).max(5e3),
1104
+ name: z4.string().trim().max(120).optional()
1078
1105
  });
1079
- var NotificationFeedbackKindSchema = z3.enum([
1106
+ var NotificationFeedbackKindSchema = z4.enum([
1080
1107
  "break_down",
1081
1108
  // "This should be more than one ask — break it down."
1082
1109
  "regenerate_options",
@@ -1091,9 +1118,117 @@ var NotificationFeedbackKindSchema = z3.enum([
1091
1118
  // anything else — the note carries it.
1092
1119
  ]);
1093
1120
 
1121
+ // src/listening.ts
1122
+ import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
1123
+ import { homedir } from "os";
1124
+ import { dirname, join } from "path";
1125
+ var HARNESS_MARKS = ["PAIGY_HARNESS", "PAIGY_SESSION_ID"];
1126
+ function harnessDriven(env) {
1127
+ return HARNESS_MARKS.some((mark) => !!env[mark]);
1128
+ }
1129
+ function sessionStartHook() {
1130
+ return `#!/bin/sh
1131
+ # Paigy \u2014 written by \`paigy-enable-tools\`. Prints one line into a starting session.
1132
+ # Never edits, never calls the network, and exits 0 whatever happens: a hook that fails
1133
+ # is a session that fails to start.
1134
+ [ -n "${HARNESS_MARKS.map((m) => `\${${m}}`).join("")}" ] && exit 0 # harness-spawned (either mark): its pump already delivers
1135
+ [ -f "\${HOME}/.paigy/token.json" ] || exit 0 # not paired on this machine: nothing to listen as
1136
+ cat <<'PAIGY'
1137
+ Paigy: nothing pushes answers into a session started in a terminal \u2014 an answer waits for your
1138
+ next check_replies. Start your own listener now, as a BACKGROUND task whose output you watch
1139
+ (your harness's monitor/background-task tool), and leave it running for this session:
1140
+
1141
+ npx -y -p @paigy/mcp@latest paigy-listen
1142
+
1143
+ Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
1144
+ the answer needs a decision. Start it once; if it is already running, do nothing.
1145
+ PAIGY
1146
+ exit 0
1147
+ `;
1148
+ }
1149
+ function withSessionStartHook(existing, command) {
1150
+ let config = {};
1151
+ if (existing) {
1152
+ try {
1153
+ config = JSON.parse(existing);
1154
+ } catch {
1155
+ return null;
1156
+ }
1157
+ }
1158
+ const hooks = config.hooks ??= {};
1159
+ const starts = hooks.SessionStart ??= [];
1160
+ const already = starts.some((group) => (group.hooks ?? []).some((h) => h.command === command));
1161
+ if (!already) starts.push({ hooks: [{ type: "command", command }] });
1162
+ return JSON.stringify(config, null, 2);
1163
+ }
1164
+ var word = (s) => `'${s.replace(/'/g, "'\\''")}'`;
1165
+ function decideListen(f) {
1166
+ if (harnessDriven(f.env)) {
1167
+ return {
1168
+ status: "listening",
1169
+ via: "harness",
1170
+ message: "Already listening: this session was started by the Paigy harness, whose pump delivers every answer into it as it lands. Nothing to start."
1171
+ };
1172
+ }
1173
+ if (!f.token) {
1174
+ return {
1175
+ status: "unpaired",
1176
+ message: "Not paired: no token in this session's slot, so there is no identity to listen as \u2014 call onboard first."
1177
+ };
1178
+ }
1179
+ if (f.pidAlive && f.pid !== void 0) {
1180
+ return { status: "listening", via: "daemon", pid: f.pid, message: `Already listening (paigy-listen pid ${f.pid}). Do nothing.` };
1181
+ }
1182
+ return {
1183
+ status: "start",
1184
+ command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${word(f.execPath)} ${word(f.listenJs)} --brief`,
1185
+ how: "Run this command NOW as a watched background task (your harness's background-task/monitor tool), and leave it running for this session. Each line it prints is a Delivery waiting for you: claim_goal the Goal it names, read it, and answer with contact. Start it once."
1186
+ };
1187
+ }
1188
+ function listenMark(slot, home = homedir()) {
1189
+ return join(home, ".paigy", "listen", `${slot.replace(/[^\w.-]/g, "_")}.pid`);
1190
+ }
1191
+ function writeListenMark(slot, pid, home = homedir()) {
1192
+ const path = listenMark(slot, home);
1193
+ mkdirSync(dirname(path), { recursive: true });
1194
+ writeFileSync(path, `${pid}
1195
+ `, { mode: 384 });
1196
+ chmodSync(path, 384);
1197
+ }
1198
+ function removeListenMark(slot, home = homedir()) {
1199
+ rmSync(listenMark(slot, home), { force: true });
1200
+ }
1201
+ function listenerAlive(slot, home = homedir()) {
1202
+ const path = listenMark(slot, home);
1203
+ let pid;
1204
+ try {
1205
+ pid = Number.parseInt(readFileSync(path, "utf8").trim(), 10);
1206
+ } catch {
1207
+ return null;
1208
+ }
1209
+ if (!Number.isInteger(pid) || pid <= 0) {
1210
+ rmSync(path, { force: true });
1211
+ return null;
1212
+ }
1213
+ try {
1214
+ process.kill(pid, 0);
1215
+ return pid;
1216
+ } catch (e) {
1217
+ if (e.code === "EPERM") return pid;
1218
+ rmSync(path, { force: true });
1219
+ return null;
1220
+ }
1221
+ }
1222
+
1094
1223
  export {
1095
1224
  mcpInputSchema,
1096
1225
  AGENT_TOOLS,
1097
1226
  serverInstructions,
1098
- entryWords
1227
+ entryWords,
1228
+ sessionStartHook,
1229
+ withSessionStartHook,
1230
+ decideListen,
1231
+ writeListenMark,
1232
+ removeListenMark,
1233
+ listenerAlive
1099
1234
  };