@paigy/mcp 0.40.41 → 0.40.43
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.
- package/README.md +29 -1
- package/dist/{chunk-AX55W57P.js → chunk-DMQHSNFJ.js} +1 -1
- package/dist/{chunk-UVVW32KH.js → chunk-LGG4IKAW.js} +35 -3
- package/dist/{chunk-CPDYOHSY.js → chunk-ULCS3CYO.js} +956 -797
- package/dist/{chunk-NYSDESST.js → chunk-ZCWFBY4B.js} +1039 -810
- package/dist/{dist-VYRT3PHE.js → dist-6D3WD3H6.js} +5 -1
- package/dist/enable.js +19 -8
- package/dist/index.js +22 -5
- package/dist/listen.js +97 -28
- package/dist/onboard.js +4 -4
- package/dist/slot.js +1 -1
- package/dist/stalled.js +1 -1
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
// ../../packages/schema/dist/index.js
|
|
2
|
-
import { z as
|
|
2
|
+
import { z as z14 } from "zod";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { z as
|
|
4
|
+
import { z as z4 } from "zod";
|
|
5
5
|
import { z as z2 } from "zod";
|
|
6
|
-
import { z as
|
|
6
|
+
import { z as z3 } from "zod";
|
|
7
|
+
import { z as z7 } from "zod";
|
|
7
8
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
8
|
-
import { z as
|
|
9
|
+
import { z as z5 } from "zod";
|
|
9
10
|
import { z as z6 } from "zod";
|
|
11
|
+
import { z as z8 } from "zod";
|
|
10
12
|
import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
|
|
11
|
-
import { z as
|
|
13
|
+
import { z as z9 } from "zod";
|
|
12
14
|
import { zodToJsonSchema as zodToJsonSchema3 } from "zod-to-json-schema";
|
|
13
|
-
import { z as
|
|
15
|
+
import { z as z10 } from "zod";
|
|
14
16
|
import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
|
|
15
|
-
import { z as
|
|
17
|
+
import { z as z11 } from "zod";
|
|
16
18
|
import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
17
|
-
import { z as
|
|
19
|
+
import { z as z12 } from "zod";
|
|
18
20
|
import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
|
|
19
|
-
import { z as
|
|
21
|
+
import { z as z13 } from "zod";
|
|
20
22
|
import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
|
|
21
23
|
var OPTIONS_MIN = 2;
|
|
22
24
|
var OPTIONS_MAX = 6;
|
|
@@ -96,21 +98,35 @@ function capRefusals(text, units2, field) {
|
|
|
96
98
|
function refuseOverCaps(text, units2, field, ctx) {
|
|
97
99
|
for (const { path, message, ...params } of capRefusals(text, units2, field)) ctx.addIssue({ code: z2.ZodIssueCode.custom, path, message, params });
|
|
98
100
|
}
|
|
101
|
+
var SHIPPED = ["phone", "calls", "server", "store", "agents"];
|
|
102
|
+
var CAUSES_MAX = 10;
|
|
103
|
+
var shipped = z3.enum(SHIPPED, {
|
|
104
|
+
errorMap: () => ({ message: `shipped_unknown: one of ${SHIPPED.join(", ")}.` })
|
|
105
|
+
}).optional().describe(
|
|
106
|
+
"Only when this reports a result that LANDED somewhere the person can see it: phone (an app update reached their phone), calls (the voice bot), server, store (the App Store) or agents (the MCP or SDK). Leave it out for work in progress."
|
|
107
|
+
);
|
|
108
|
+
var causedBy = z3.array(
|
|
109
|
+
z3.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "cause_not_found: an Entry id, or the short id get_goals prints for the person's words.")
|
|
110
|
+
).min(1).max(CAUSES_MAX, `too_many_causes: at most ${CAUSES_MAX}.`).optional().describe(
|
|
111
|
+
`The person's own words this acts on: the ids (or the short ids get_goals prints) of their answers or notes that led to it, at most ${CAUSES_MAX}. Their app shows each result under the words that caused it, so name every one this follows from. An id that is not their words is refused by name (cause_not_found, cause_ambiguous) and nothing is written.`
|
|
112
|
+
);
|
|
99
113
|
var COMPLETION_MESSAGE_MAX = 50;
|
|
100
|
-
var CompletionSchema =
|
|
101
|
-
goalId:
|
|
102
|
-
message:
|
|
103
|
-
units:
|
|
104
|
-
title:
|
|
105
|
-
body:
|
|
114
|
+
var CompletionSchema = z4.object({
|
|
115
|
+
goalId: z4.string().uuid().describe("The existing Goal to complete."),
|
|
116
|
+
message: z4.string().trim().min(1).max(COMPLETION_MESSAGE_MAX, "completion_message_too_long: at most 50 characters; put detail in units.").describe("Completion headline, at most 50 characters: the one thing that would go wrong or stay unclear later if they missed it."),
|
|
117
|
+
units: z4.array(UnitInputSchema.extend({
|
|
118
|
+
title: z4.string().trim().min(1).max(UNIT_TITLE_MAX),
|
|
119
|
+
body: z4.string().trim().min(1).max(UNIT_BODY_MAX)
|
|
106
120
|
})).max(UNITS_MAX).optional().describe("Suggest initial state, changes, current state with verification and limits, and next steps / leftover work with its owner when useful. One idea per unit; omit unnecessary sections."),
|
|
107
|
-
reply:
|
|
108
|
-
options:
|
|
109
|
-
label:
|
|
110
|
-
hint:
|
|
111
|
-
}).strict()).max(5).optional().describe("Optional next steps the person may initiate: label (50 characters) and consequence in hint (200). Selecting one acknowledges the report and requests that step from its author; it never reopens this Goal.")
|
|
121
|
+
reply: z4.string().trim().min(1).max(OPTION_LABEL_MAX).describe("One acknowledgement in the person's voice, at most 50 characters. Acknowledgement alone requests no follow-up and does not claim they tested the work."),
|
|
122
|
+
options: z4.array(z4.object({
|
|
123
|
+
label: z4.string().trim().min(1).max(OPTION_LABEL_MAX),
|
|
124
|
+
hint: z4.string().trim().min(1).max(OPTION_HINT_MAX)
|
|
125
|
+
}).strict()).max(5).optional().describe("Optional next steps the person may initiate: label (50 characters) and consequence in hint (200). Selecting one acknowledges the report and requests that step from its author; it never reopens this Goal."),
|
|
126
|
+
causedBy,
|
|
127
|
+
shipped
|
|
112
128
|
}).strict();
|
|
113
|
-
var CompleteChangeSchema = CompletionSchema.extend({ kind:
|
|
129
|
+
var CompleteChangeSchema = CompletionSchema.omit({ causedBy: true, shipped: true }).extend({ kind: z4.literal("complete") });
|
|
114
130
|
var ASKING_RULES = `HOW TO ASK:
|
|
115
131
|
1. One question per object in \`questions\`, so each is answered on its own.
|
|
116
132
|
2. \`goalId\` names the existing Goal it is about (create one with manage_goals first); the answer comes back there.
|
|
@@ -120,7 +136,7 @@ var ASKING_RULES = `HOW TO ASK:
|
|
|
120
136
|
6. Read the Goal before asking; never re-ask what was answered or shipped. \`replied\` means they chose no option: it is still yours to settle.
|
|
121
137
|
7. Give options when you have them. Each label names its choice in full (never "Option 2"); never add "Other". \`pickMode\`: one (default), many, rank.
|
|
122
138
|
8. A diagnosis says when, why and how it happens, then proposes one fix.
|
|
123
|
-
9. Give a report its \`reply\`: one line in THEIR voice for THAT report, so you learn whether they verified it or only received it. Required when it
|
|
139
|
+
9. Give a report its \`reply\`: one line in THEIR voice for THAT report, so you learn whether they verified it or only received it. Required when it is delivered or its Goal is done.`;
|
|
124
140
|
function draft2020(node) {
|
|
125
141
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
126
142
|
if (node && typeof node === "object") {
|
|
@@ -150,25 +166,68 @@ function mcpInputSchema(s) {
|
|
|
150
166
|
delete schema.$schema;
|
|
151
167
|
return draft2020(schema);
|
|
152
168
|
}
|
|
153
|
-
var questionHandle =
|
|
154
|
-
var NodeRef =
|
|
155
|
-
type:
|
|
156
|
-
id:
|
|
169
|
+
var questionHandle = z5.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
|
|
170
|
+
var NodeRef = z5.object({
|
|
171
|
+
type: z5.enum(["goal", "question"]),
|
|
172
|
+
id: z5.string().uuid()
|
|
157
173
|
}).strict().describe("A Goal or a Question, by its id.");
|
|
158
|
-
var BlockedAction =
|
|
159
|
-
var reason =
|
|
174
|
+
var BlockedAction = z5.enum(["start", "complete", "answer"]).describe("What waits: starting or completing a Goal, or answering a Question.");
|
|
175
|
+
var reason = z5.string().trim().min(1).max(500).describe("Why, as an explanation, never as policy.");
|
|
160
176
|
var actionFits = (b) => b.blocked.type === "goal" === (b.action !== "answer");
|
|
161
177
|
var ACTION_FITS = "a goal is blocked from start or complete; a question from answer";
|
|
162
|
-
var userExplicitlyRequested =
|
|
178
|
+
var userExplicitlyRequested = z5.enum(["any", "call"]).optional().describe(
|
|
163
179
|
`Only what the person said about THIS message, in so many words: "any" when they asked you to send it to them ("send it to my phone", "message me the result"), "call" when they asked to be called about it. It is then delivered (a call rings for the whole contact, and carries everything waiting on them); omitted, an update is only recorded as the Goal's progress unless the person needs it. Never set it to make a message louder.`
|
|
164
180
|
);
|
|
165
|
-
var units =
|
|
166
|
-
`The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit. Leave it out when the main text says it all. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.`
|
|
181
|
+
var units = z5.array(UnitInputSchema).optional().describe(
|
|
182
|
+
`The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit: context they may open, never something they must hear. Leave it out when the main text says it all. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.`
|
|
167
183
|
);
|
|
168
|
-
var
|
|
169
|
-
|
|
170
|
-
|
|
184
|
+
var KIND_IDS = { "handing": "none", "replaced-by": "goal", "blocked-by": "node", "asking": "none", "found": "none" };
|
|
185
|
+
var KIND_GRAMMAR = "handing, replaced-by <goalId>, blocked-by <goalId|questionId>, asking, found";
|
|
186
|
+
var uuid = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
187
|
+
var handle = /^[0-9a-fA-F-]{8,36}$/;
|
|
188
|
+
function refuseKind(u, ctx) {
|
|
189
|
+
if (!u.kind) return;
|
|
190
|
+
const refuse = (refusal, kind, message) => ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["kind"], params: { refusal, kind }, message });
|
|
191
|
+
const [name, ...rest] = u.kind.trim().split(/\s+/);
|
|
192
|
+
const shape = KIND_IDS[name];
|
|
193
|
+
if (!shape) {
|
|
194
|
+
refuse("kind_unknown", name, `kind_unknown: "${name}" is not a kind. One of: ${KIND_GRAMMAR}.`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const id2 = rest.join(" ");
|
|
198
|
+
if (shape === "none" && id2) {
|
|
199
|
+
refuse("kind_takes_no_id", name, `kind_takes_no_id: "${name}" is the whole kind, so drop the "${id2}" after it.`);
|
|
200
|
+
}
|
|
201
|
+
if (shape !== "none" && !(shape === "goal" ? uuid.test(id2) : handle.test(id2))) {
|
|
202
|
+
refuse("kind_needs_id", name, shape === "goal" ? `kind_needs_id: "${name}" names the Goal it is about, as "${name} <goalId>".` : `kind_needs_id: "${name}" names the Goal or Question it is about, as "${name} <goalId|questionId>".`);
|
|
203
|
+
}
|
|
204
|
+
if (name === "asking") {
|
|
205
|
+
refuse("kind_is_a_question", name, 'kind_is_a_question: "asking" owes them an answer, so send it in `questions`, where it opens one.');
|
|
206
|
+
}
|
|
207
|
+
if (u.userExplicitlyRequested) {
|
|
208
|
+
refuse("kind_not_for_the_person", name, `kind_not_for_the_person: "${name}" is written for the other agents on the Goal, and userExplicitlyRequested delivers these words to the person. Split it: one message for them, one for the agent.`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
var UpdateInputSchema = z5.object({
|
|
212
|
+
// ONE LOAD-BEARING POINT PER REPORT (owner, 2026-10-27 14:10 on call, Goal 1b1750e9, settling
|
|
213
|
+
// #3633's pacing: "one point at a time is better, but make sure that it's [load-]bearing ... if
|
|
214
|
+
// there is a lot of key points that I need to be aware of ... meaning something bad would happen
|
|
215
|
+
// if I didn't know them or I wouldn't be able to understand something in the future ... They
|
|
216
|
+
// should go separately. Basically, it should be like one load-bearing thing per report").
|
|
217
|
+
//
|
|
218
|
+
// The call's half of that ruling shipped in #3771: the talker settles an item only once the
|
|
219
|
+
// transcript shows he heard its load-bearing point. This is the author's half, and without it the
|
|
220
|
+
// talker has to guess which of a report's parts was load-bearing. Measured 2026-11-01 on prod:
|
|
221
|
+
// 458 of 866 reports in 21 days carried two or more units, and since the pacing shipped on 10-23
|
|
222
|
+
// only 18 of 72 units that reached a call were ever said -- so a point written as a unit has about
|
|
223
|
+
// a one-in-four chance of being heard, and one ok after the headline settles the whole report.
|
|
224
|
+
// Hence the wording, not a `mustHear` flag (the-heard-record.md P3): he asked for less mechanism
|
|
225
|
+
// and more judgment from the agent, so a point he must not miss is simply its own report.
|
|
226
|
+
message: z5.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters: the one thing that would go wrong or stay unclear later if they missed it, said as what changed for them, in their words ("The screen reader speaks Spanish too", not "Merged #3896"). A second point like that is its own update; the rest goes in units.`),
|
|
227
|
+
goalIds: z5.array(z5.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
|
|
171
228
|
units,
|
|
229
|
+
causedBy,
|
|
230
|
+
shipped,
|
|
172
231
|
// A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
|
|
173
232
|
// until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
|
|
174
233
|
// still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
|
|
@@ -189,23 +248,30 @@ var UpdateInputSchema = z4.object({
|
|
|
189
248
|
// `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
|
|
190
249
|
// MECHANISM). It rides the door as the report's single stored option (`storedOption`,
|
|
191
250
|
// `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
|
|
192
|
-
reply:
|
|
251
|
+
reply: z5.string().trim().min(1).describe(
|
|
193
252
|
`What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. The reply is required for requested delivery and completed Goals; ordinary progress may omit it.`
|
|
194
253
|
).optional(),
|
|
254
|
+
// THE KIND, WHICH WAS THE FIRST LINE OF THE MESSAGE UNTIL NOW (`KIND_IDS` above). Optional on
|
|
255
|
+
// purpose: every agent writing updates today sends none, and a door that refused them would turn a
|
|
256
|
+
// convention nobody was obliged to follow into a 400 on live traffic.
|
|
257
|
+
kind: z5.string().trim().min(1).optional().describe(
|
|
258
|
+
`What an agent writing on ANOTHER agent's Goal expects of its reader, as one token: ${KIND_GRAMMAR}. \`handing\` the work is yours now, nothing is asked; \`replaced-by <goalId>\` this Goal is dead and that one carries its work; \`blocked-by <goalId|questionId>\` I cannot go on until that moves, no action unless you own it; \`found\` evidence on your Goal, nothing is asked and nothing has changed. \`asking\` is a question, so it belongs in \`questions\`, not here. It is written for the other agents on the Goal, never the person, so it does not go with userExplicitlyRequested; leave it out for your own progress. Refused by name (kind_unknown, kind_needs_id, kind_takes_no_id, kind_is_a_question, kind_not_for_the_person).`
|
|
259
|
+
),
|
|
195
260
|
userExplicitlyRequested
|
|
196
261
|
}).strict().superRefine((u, ctx) => {
|
|
197
262
|
if (u.userExplicitlyRequested && !u.reply) {
|
|
198
263
|
ctx.addIssue({
|
|
199
|
-
code:
|
|
264
|
+
code: z5.ZodIssueCode.custom,
|
|
200
265
|
path: ["reply"],
|
|
201
266
|
params: { refusal: "reply_required" },
|
|
202
267
|
message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
|
|
203
268
|
});
|
|
204
269
|
}
|
|
205
270
|
refuseOverCaps(u.message, u.units, "message", ctx);
|
|
271
|
+
refuseKind(u, ctx);
|
|
206
272
|
if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
|
|
207
273
|
ctx.addIssue({
|
|
208
|
-
code:
|
|
274
|
+
code: z5.ZodIssueCode.custom,
|
|
209
275
|
path: ["reply"],
|
|
210
276
|
params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX },
|
|
211
277
|
message: `reply_too_long: the reply is ${u.reply.length} characters; the cap is ${OPTION_LABEL_MAX}. It is a button's words, so say it in a breath.`
|
|
@@ -213,83 +279,114 @@ var UpdateInputSchema = z4.object({
|
|
|
213
279
|
}
|
|
214
280
|
});
|
|
215
281
|
var questionFields = {
|
|
216
|
-
question:
|
|
282
|
+
question: z5.string().trim().min(1).describe(
|
|
217
283
|
`ONE question, in at most ${ASK_MAX} characters, and only what is needed to answer it; the context goes in units. Several questions are several objects, one each: an answer settles the one question it was given, so a person who answers the part of a bundled question that interested them settles nothing and is asked again. Separate questions cost nothing: ANY contact for a person already on a call JOINS that call, so several arrive as one call.`
|
|
218
284
|
),
|
|
219
|
-
goalId:
|
|
285
|
+
goalId: z5.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
|
|
220
286
|
units,
|
|
221
|
-
options:
|
|
287
|
+
options: z5.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own and differs from the others (duplicate_option_label), at most 50 characters; a hint, at most 200. They can always answer in their own words, so no 'Other' or 'None of these'."),
|
|
222
288
|
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
223
289
|
// brain_prompts.md §1: structured agent questions need no model). The app draws all three.
|
|
224
|
-
pickMode:
|
|
225
|
-
'How the options are answered: "one" (the default) a pick, for alternatives; "many" a checklist, for independent options they may want several of; "rank" an order of all of them. Only with options.'
|
|
290
|
+
pickMode: z5.enum(["one", "many", "rank"]).optional().describe(
|
|
291
|
+
'How the options are answered: "one" (the default) a pick, for alternatives; "many" a checklist, for independent options they may want several of; "rank" an order of all of them. Only with options; "many" and "rank" need at least two (too_few_options).'
|
|
226
292
|
),
|
|
227
293
|
// WHAT THE QUESTION HOLDS UP (brain_prompts.md §2.1): replaces `waiting: 'hard'`. A question that
|
|
228
294
|
// blocks work asks for a call (the person's settings and ring cooldowns still decide).
|
|
229
|
-
workItBlocks:
|
|
295
|
+
workItBlocks: z5.array(z5.object({ blocked: NodeRef, action: BlockedAction, reason: reason.optional() }).strict().refine(actionFits, ACTION_FITS)).min(1).max(10).optional().describe(
|
|
230
296
|
"The work that cannot go on until this is answered: a Goal's start or completion, or another Question's answer. Only for work truly blocked; it asks for a call. Leave it out for a question you can keep working around."
|
|
231
297
|
),
|
|
232
298
|
userExplicitlyRequested
|
|
233
299
|
};
|
|
234
300
|
var refineQuestion = (q, ctx) => {
|
|
235
301
|
refuseOverCaps(q.question, q.units, "question", ctx);
|
|
236
|
-
if (q.pickMode && !q.options) ctx.addIssue({ code:
|
|
302
|
+
if (q.pickMode && !q.options) ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["pickMode"], message: "pickMode comes only with options" });
|
|
303
|
+
refuseThinOptions(q, ctx);
|
|
237
304
|
const ids = (q.options ?? []).map((o) => o.id);
|
|
238
305
|
if (ids.some((id2) => id2) && (ids.some((id2) => !id2) || new Set(ids).size !== ids.length)) {
|
|
239
|
-
ctx.addIssue({ code:
|
|
306
|
+
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["options"], message: "give every option a distinct id, or none" });
|
|
240
307
|
}
|
|
241
308
|
};
|
|
242
|
-
var
|
|
243
|
-
|
|
309
|
+
var MIN_LIST_OPTIONS = 2;
|
|
310
|
+
function refuseThinOptions(q, ctx) {
|
|
311
|
+
const options = q.options ?? [];
|
|
312
|
+
if ((q.pickMode === "many" || q.pickMode === "rank") && options.length < MIN_LIST_OPTIONS) {
|
|
313
|
+
ctx.addIssue({
|
|
314
|
+
code: z5.ZodIssueCode.custom,
|
|
315
|
+
path: ["options"],
|
|
316
|
+
params: { refusal: "too_few_options", length: options.length, min: MIN_LIST_OPTIONS },
|
|
317
|
+
message: `too_few_options: pickMode "${q.pickMode}" needs at least ${MIN_LIST_OPTIONS} options; this question has ${options.length}. Add the other choices, or leave pickMode out.`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const seen = /* @__PURE__ */ new Map();
|
|
321
|
+
options.forEach((o, index) => {
|
|
322
|
+
const key2 = o.label.trim().toLowerCase();
|
|
323
|
+
const first = seen.get(key2);
|
|
324
|
+
if (first === void 0) {
|
|
325
|
+
seen.set(key2, index);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
ctx.addIssue({
|
|
329
|
+
code: z5.ZodIssueCode.custom,
|
|
330
|
+
path: ["options", index, "label"],
|
|
331
|
+
params: { refusal: "duplicate_option_label", index, first },
|
|
332
|
+
message: `duplicate_option_label: options[${index}] repeats the label of options[${first}]. Each option names a different choice.`
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
var QuestionInputSchema = z5.object({
|
|
337
|
+
questionId: z5.string().uuid().optional().describe("Optional: your own stable id for this question (a UUID), so a resend is the same question. Minted for you when omitted; the result returns it."),
|
|
244
338
|
...questionFields
|
|
245
339
|
}).strict().superRefine(refineQuestion);
|
|
246
|
-
var QuestionWireSchema =
|
|
247
|
-
var AnswerInputSchema =
|
|
340
|
+
var QuestionWireSchema = z5.object({ questionId: z5.string().uuid(), ...questionFields }).strict().superRefine(refineQuestion);
|
|
341
|
+
var AnswerInputSchema = z5.object({
|
|
248
342
|
questionId: questionHandle.describe("The Question you answer: its id as the event or the conversation shows it (8 characters or whole)."),
|
|
249
|
-
answer:
|
|
250
|
-
text:
|
|
251
|
-
selectedOptionIds:
|
|
343
|
+
answer: z5.object({
|
|
344
|
+
text: z5.string().trim().min(1).describe(`Your answer in your own words, at most ${ASK_MAX} characters; the context goes in units.`),
|
|
345
|
+
selectedOptionIds: z5.array(z5.string().trim().min(1)).min(1).max(6).optional().describe("The ids of the options you choose, when the Question offered options (in order, for a ranking).")
|
|
252
346
|
}).strict(),
|
|
253
347
|
units
|
|
254
348
|
}).strict().superRefine((a, ctx) => refuseOverCaps(a.answer.text, a.units, "text", ctx));
|
|
255
|
-
var QuestionDependencySchema =
|
|
349
|
+
var QuestionDependencySchema = z5.object({
|
|
256
350
|
questionId: questionHandle.describe("A Question you asked."),
|
|
257
|
-
change:
|
|
351
|
+
change: z5.enum(["add", "remove"]),
|
|
258
352
|
blocked: NodeRef,
|
|
259
353
|
action: BlockedAction,
|
|
260
354
|
reason: reason.optional()
|
|
261
355
|
}).strict().refine(actionFits, ACTION_FITS);
|
|
262
356
|
var sendFields = {
|
|
263
|
-
completions:
|
|
357
|
+
completions: z5.array(CompletionSchema).min(1).max(20).optional().describe(
|
|
264
358
|
"COMPLETIONS close and report in one operation, the same contract as manage_goals kind:complete: closure and the report commit together or neither does. Required children and dependencies must already be finished. options suggest next steps: a tap acknowledges the report and requests that step, and never reopens the Goal (follow-up work needs a new one). No reviewer rejudges it: say only what your evidence supports. Ordinary updates never close a Goal."
|
|
265
359
|
),
|
|
266
|
-
updates:
|
|
267
|
-
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress, which the person is not told. Every update reaches the other agents that wrote on its Goal in the last hour: to tell another agent something, write it as an update on that agent's Goal."
|
|
360
|
+
updates: z5.array(UpdateInputSchema).min(1).max(20).optional().describe(
|
|
361
|
+
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress, which the person is not told. Every update reaches the other agents that wrote on its Goal in the last hour: to tell another agent something, write it as an update on that agent's Goal, with its `kind`.\n\nONE LOAD-BEARING POINT PER REPORT: the message is the point they must not miss; a second point that would go wrong or stay unclear later if they missed it is its own update, never a unit."
|
|
268
362
|
),
|
|
269
|
-
answers:
|
|
270
|
-
withdrawQuestionIds:
|
|
363
|
+
answers: z5.array(AnswerInputSchema).min(1).max(20).optional().describe("Answers to Questions addressed to you."),
|
|
364
|
+
withdrawQuestionIds: z5.array(questionHandle).min(1).max(10).optional().describe(
|
|
271
365
|
"Questions you asked that no longer need an answer: you acted on them yourself, they no longer matter, or you asked wrongly. Each is cancelled, not answered: its card closes and nothing rings for it."
|
|
272
366
|
),
|
|
273
|
-
|
|
367
|
+
bumpQuestionIds: z5.array(questionHandle).min(1).max(10).optional().describe(
|
|
368
|
+
"Your own questions still waiting on the person that you want them to notice again. Each moves back to the top of its card with one new notification; nothing is resent and no second card is made."
|
|
369
|
+
),
|
|
370
|
+
questionDependencies: z5.array(QuestionDependencySchema).min(1).max(20).optional().describe(
|
|
274
371
|
"Add or remove what a Question you asked holds up, after asking it."
|
|
275
372
|
)
|
|
276
373
|
};
|
|
277
|
-
var ContactSchema =
|
|
374
|
+
var ContactSchema = z5.object({
|
|
278
375
|
...sendFields,
|
|
279
|
-
questions:
|
|
376
|
+
questions: z5.array(QuestionInputSchema).min(1).max(20).optional().describe("Questions for the person, one per object, each sent exactly as written, with no reading in between."),
|
|
280
377
|
/** RECEIVING (2I, I2; brain_prompts.md §2.2): the events this agent handled. */
|
|
281
|
-
ackEventIds:
|
|
378
|
+
ackEventIds: z5.array(z5.string().uuid()).min(1).max(100).optional().describe(
|
|
282
379
|
"The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (answers), never acknowledged away."
|
|
283
380
|
),
|
|
284
|
-
wait:
|
|
381
|
+
wait: z5.boolean().optional().describe(
|
|
285
382
|
"Only controls when this returns. Sending: false (the default) returns at once; true waits up to about 45 seconds for a response to what you sent. Sending nothing: true (the default) waits up to about 45 seconds for anything addressed to you; false returns what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived; wait again without sending again), or `not_waited`."
|
|
286
383
|
)
|
|
287
384
|
}).strict();
|
|
288
|
-
var sendsAnything = (c) => !!(c.completions?.length || c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
|
|
289
|
-
var ContactWireSchema =
|
|
290
|
-
operationId:
|
|
385
|
+
var sendsAnything = (c) => !!(c.completions?.length || c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.bumpQuestionIds?.length || c.questionDependencies?.length);
|
|
386
|
+
var ContactWireSchema = z5.object({
|
|
387
|
+
operationId: z5.string().uuid(),
|
|
291
388
|
...sendFields,
|
|
292
|
-
questions:
|
|
389
|
+
questions: z5.array(QuestionWireSchema).min(1).max(20).optional()
|
|
293
390
|
}).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
294
391
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
295
392
|
var CONTACT_DESCRIPTION = "Send to and receive from the person: completions, updates, questions, answers to questions addressed to you, withdrawals, and what a question holds up. Every Goal named must exist: an unknown one refuses the whole contact. Each item applies on its own: read `results`, where a failed one names why.\n\n" + ASKING_RULES + `
|
|
@@ -299,13 +396,70 @@ LENGTH: the question, message or answer is the one point, at most ${ASK_MAX} cha
|
|
|
299
396
|
UPDATES ask nothing. One reaches the person only when they asked for it, it answers something they said, or its Goal is done; else it is kept as progress. One that reaches them joins an open report card or a live call.
|
|
300
397
|
|
|
301
398
|
RECEIVING: contact({}) waits ~45 s; contact({wait:false}) returns \`events\` at once. Confirm the ones you handled with contact({ackEventIds}).`;
|
|
302
|
-
var
|
|
303
|
-
|
|
399
|
+
var GUIDE = '# Paigy\n\nThe user is remote. Reach them only through Paigy\'s tools, never the terminal. The server\'s own instructions and each tool\'s description are the contract: read them. Every read ends in `next`, the one step to take.\n\nThis same guide is inside the MCP: the `help` tool (whole, or one `topic`) and the `/mcp__paigy__help` prompt. `/mcp__paigy__welcome` starts a session.\n\nHosted (no machine): skip Pairing and Listening; answers wake you.\n\n## Pairing\n\n- Call `configure` first. With a device credential on this machine it hatches you at once.\n- Otherwise print its `user_message` to the user, then do what the result says:\n - `awaiting_approval`: call `configure` again with its `device_code` in the same turn. On `pending`, call again.\n - `awaiting_confirmation`: end your turn with the code as your whole reply; call `configure` with the `device_code` next turn. Calling it in the same turn drops the code.\n- When it returns `enable_command`, print it for the user to run. Do not run it yourself.\n- `configure({ logOut: true })` logs out only this agent, and only when the user asks.\n\n## Listening\n\n- Call `listen` once per session.\n - `listening`: done.\n - `receiving`: a listener runs but cannot wake you. Do not start a second.\n - `unpaired`: call `configure`.\n - `start`: run its `command` exactly, now.\n- Claude Code: run it with the **Monitor** tool, never a background Bash command. Start it again whenever the monitor expires.\n- Codex: run its `command` as a long-running background process. It queues each answer into this thread (`CODEX_THREAD_ID`).\n- Grok: run it with the **monitor** tool, never a background shell command. Start it again whenever the monitor expires.\n- With no `listen` tool, run `npx -y -p @paigy/mcp@latest paigy-listen --brief` the same way.\n- One listener per session. A `paigy-listen` in `ps` belongs to another session; ask `listen`, never `ps`.\n- Each line it prints is work: receive, read the Goal, act.\n\n## Receiving\n\n- On startup and after every wake or listener line: `contact({ wait: false })`.\n- Events: `question` (you owe an answer: send `answers`), `update` (something new on your Goal: an answer, a reply, a review), `instruction` (work sent to you).\n- Read the Goal with `get_goals` before acting. An answer arrives as an `update` event and lands on the question\'s Goal. A question reading `replied` means they chose no option: settle it from their words.\n- Acknowledge each event you handled: `contact({ ackEventIds })`. Reading acknowledges nothing; events repeat until acknowledged. A `question` event is answered, never acknowledged away.\n- `contact({ wait: true })` waits about 45 s. On `expired`, wait again without re-sending, or stop. No while-loops: while any Call is open, keep receiving through empty windows; otherwise stop at one empty window.\n\n## Asking\n\n- One question per object in `questions`, at most 80 characters. Context goes in `units`: at most 12, title \u226450, body \u2264200, one idea each.\n- `goalId` names an existing Goal. Create it with `manage_goals` first.\n- Give options when you have them: up to 6, label \u226450 naming the choice in full, `hint` \u2264200 saying what it does. Never "Other" or "Option 2". `pickMode`: `one` (default), `many`, `rank`.\n- `workItBlocks` only when you are truly stopped: it asks for a call.\n- Read the Goal before asking. Never re-ask an open or answered question, or one whose work shipped.\n- Take back a question that no longer matters: `withdrawQuestionIds`.\n- An answer you give (`answers`) is at most 80 characters.\n\n## Updates and completions\n\n- An update (`updates`: `message` \u226480, `goalIds`) asks nothing. It reaches the user only when they asked for it, when it answers something they said, or when its Goal is done; otherwise it stays on the Goal as progress.\n- Updates never complete a Goal.\n- One load-bearing point per report: `message` is the point they must not miss, and a second point that would go wrong or stay unclear later if they missed it is its own update. `units` are context they may open, never something they must hear.\n- `reply` is the button in the user\'s voice instead of "Got it", \u226450 characters ("Sounds good, I\'ll test later"). Required (`reply_required`) when the update is delivered on request (`userExplicitlyRequested`) or any Goal it names is already done.\n- Set `userExplicitlyRequested` only to relay the user\'s own words: `"any"` for "send it to me", `"call"` for "call me". Never to make a message louder.\n- Finish with a completion (`contact.completions`, or `manage_goals` kind `complete`): `goalId`, `message` \u226450, `reply` \u226450, optional `units`, optional next-step `options` {label, hint}. It closes and reports in one step. Say only what your evidence supports.\n- Read `results`: each item applies on its own, and a failed one names why.\n\n## Goals\n\n- `manage_goals` `changes`, applied in order:\n - `create`: `title` (1-5 words) and `outcome` (one paragraph, \u2264500). More work is child Goals: `parentGoalId`. New ids come back in `results[i].goalId`.\n - `assign`: `ownerId`, a participant as `check_activity` shows it.\n - `defer`: `until`. You are woken when it passes. This is how you follow up later.\n - `dependency`: `add`/`remove`; `goalId` waits on `dependsOnGoalId` to `start` or `complete`.\n - `edit`, `move`, `state` (open, completed, canceled).\n- Any open question on a Goal, anyone\'s, blocks its completion (`goal_blocked`). Before completing, withdraw yours, finish or move its required children, and clear its dependencies.\n- On `goal_revision_conflict`, read the Goal again and reconsider.\n- Before overlapping work, read `check_activity` and the Goal\'s other contributors. Every Goal of your person is open to you; your first write puts you on it.\n- `search` finds earlier words, Goals and answers. Nothing found is not proof of absence.\n\n## Calls\n\n- Call only when the user asked: `userExplicitlyRequested: "call"` on that item. A question with `workItBlocks` may ring too.\n- Any contact while the user is on a call joins that call (`joinedCall` in `results`). Several questions arrive as one call.\n- While any Call is open, keep receiving.\n- An open Call Delivery is not proof it rang. `get_delivery` reads a Call\'s state, and with `transcript: true` what was said.\n';
|
|
400
|
+
var HELP_TOPICS = ["pairing", "listening", "receiving", "asking", "updates", "goals", "calls"];
|
|
401
|
+
var unknownTopic = (topic) => `unknown_topic: ${JSON.stringify(topic)} is not a section of the guide. Topics: ${HELP_TOPICS.join(", ")}; or omit topic for the whole guide.`;
|
|
402
|
+
var HelpSchema = z6.object({
|
|
403
|
+
topic: z6.enum(HELP_TOPICS, { errorMap: (_issue, ctx) => ({ message: unknownTopic(ctx.data) }) }).optional().describe("One section of the guide. Omit it for the whole guide.")
|
|
404
|
+
}).strict();
|
|
405
|
+
var HELP_DESCRIPTION = "Start here: how to use Paigy, in full \u2014 pairing, listening, receiving, asking, updates and completions, Goals and calls. Pass a topic for one section. Read-only; needs no setup.";
|
|
406
|
+
function section(topic) {
|
|
407
|
+
const parts = GUIDE.split(/^(?=## )/m);
|
|
408
|
+
const found = parts.find((p) => p.slice(3).toLowerCase().startsWith(topic));
|
|
409
|
+
if (!found) throw new Error(`help_section_missing: the guide has no "## ${topic}" section`);
|
|
410
|
+
return found.trim() + "\n";
|
|
411
|
+
}
|
|
412
|
+
function helpView({ topic }) {
|
|
413
|
+
return {
|
|
414
|
+
topic: topic ?? "all",
|
|
415
|
+
guide: topic ? section(topic) : GUIDE,
|
|
416
|
+
topics: [...HELP_TOPICS],
|
|
417
|
+
next: "Not set up? Call configure. Then contact({wait:false}) for the work addressed to you."
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
var AGENT_PROMPTS = [
|
|
421
|
+
{
|
|
422
|
+
name: "help",
|
|
423
|
+
description: "How to use Paigy: the whole guide, or one topic of it.",
|
|
424
|
+
arguments: [{ name: "topic", description: `One section: ${HELP_TOPICS.join(", ")}. Omit it for the whole guide.`, required: false }]
|
|
425
|
+
},
|
|
426
|
+
{ name: "welcome", description: "Start here: who you are on Paigy, then listen and collect your open work." }
|
|
427
|
+
];
|
|
428
|
+
function promptText(name, facts) {
|
|
429
|
+
if (name === "help") {
|
|
430
|
+
const parsed = HelpSchema.safeParse(facts.topic ? { topic: facts.topic } : {});
|
|
431
|
+
if (!parsed.success) throw new Error(parsed.error.issues[0].message);
|
|
432
|
+
return `Here is how to use Paigy. Follow it from now on.
|
|
433
|
+
|
|
434
|
+
${helpView(parsed.data).guide}`;
|
|
435
|
+
}
|
|
436
|
+
if (name === "welcome") {
|
|
437
|
+
const who = facts.identity;
|
|
438
|
+
const steps = [];
|
|
439
|
+
if (!who) steps.push("Call `configure` and do what it says: it hatches this session at once on a paired machine, or shows a code to approve.");
|
|
440
|
+
if (facts.listens) steps.push("Call `listen` once and do what it says (`start`: run its command with your Monitor tool, now).");
|
|
441
|
+
steps.push("Call contact({wait:false}) for the work addressed to you: answers, assignments, reviews and due Goals. Read each Goal with get_goals, act, and acknowledge what you handled with contact({ackEventIds}).");
|
|
442
|
+
steps.push("Tell me in a sentence or two what is waiting, or that nothing is.");
|
|
443
|
+
const head = !who ? "This session is not set up with Paigy yet." : who.name ? `You're connected to Paigy as ${who.name}.` : "You're connected to Paigy, with no name yet (configure({ name }) gives you one).";
|
|
444
|
+
return `${head} Start here:
|
|
445
|
+
|
|
446
|
+
${steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}
|
|
447
|
+
|
|
448
|
+
The whole guide is the \`help\` tool (or /mcp__paigy__help).`;
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`unknown_prompt: ${JSON.stringify(name)}. Prompts: ${AGENT_PROMPTS.map((p) => p.name).join(", ")}.`);
|
|
451
|
+
}
|
|
452
|
+
function promptResult(name, facts) {
|
|
453
|
+
const text = promptText(name, facts);
|
|
454
|
+
return { description: AGENT_PROMPTS.find((p) => p.name === name).description, messages: [{ role: "user", content: { type: "text", text } }] };
|
|
455
|
+
}
|
|
456
|
+
var CreateGoalSchema = z7.object({
|
|
457
|
+
outcome: z7.string().trim().min(1).max(1e4),
|
|
304
458
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
305
459
|
* rings"). Omit it and the brain writes one at admission from the outcome. */
|
|
306
|
-
title:
|
|
307
|
-
ownerParticipant:
|
|
308
|
-
idempotencyKey:
|
|
460
|
+
title: z7.string().trim().min(1).max(80).optional(),
|
|
461
|
+
ownerParticipant: z7.string().trim().min(1).optional(),
|
|
462
|
+
idempotencyKey: z7.string().trim().min(1).max(200),
|
|
309
463
|
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
310
464
|
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
311
465
|
* worked"). It could not have been: this door took no parent, so the only route was
|
|
@@ -313,63 +467,63 @@ var CreateGoalSchema = z5.object({
|
|
|
313
467
|
* took the short one. The hierarchy has been modelled since Goals existed and had been used
|
|
314
468
|
* ZERO times in 2,031 of them. Absent, the server judges it against the caller's open Goals
|
|
315
469
|
* (`apps/api/src/goal/intake.ts`). The writer refuses a Goal belonging to another account. */
|
|
316
|
-
parentGoalId:
|
|
470
|
+
parentGoalId: z7.string().uuid().optional(),
|
|
317
471
|
/** The repository or project identifier this Goal belongs to (#2280) — e.g. "owner/repo" or
|
|
318
472
|
* repo name. Delegated work inherits this from its parent Goal when omitted. */
|
|
319
|
-
repo:
|
|
473
|
+
repo: z7.string().trim().min(1).max(200).optional()
|
|
320
474
|
});
|
|
321
|
-
var UpdateGoalSchema =
|
|
322
|
-
revision:
|
|
323
|
-
changes:
|
|
324
|
-
outcome:
|
|
475
|
+
var UpdateGoalSchema = z7.object({
|
|
476
|
+
revision: z7.number().int().positive(),
|
|
477
|
+
changes: z7.object({
|
|
478
|
+
outcome: z7.string().trim().min(1).max(1e4).optional(),
|
|
325
479
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
|
|
326
480
|
* brain writes one at admission; this is the owner saying it better. Null clears it. */
|
|
327
|
-
title:
|
|
328
|
-
ownerParticipant:
|
|
329
|
-
parentGoalId:
|
|
330
|
-
dependencies:
|
|
331
|
-
children:
|
|
332
|
-
state:
|
|
333
|
-
progress:
|
|
334
|
-
reviewed:
|
|
335
|
-
dueAt:
|
|
481
|
+
title: z7.string().trim().min(1).max(80).nullable().optional(),
|
|
482
|
+
ownerParticipant: z7.string().trim().min(1).optional(),
|
|
483
|
+
parentGoalId: z7.string().uuid().nullable().optional(),
|
|
484
|
+
dependencies: z7.array(z7.object({ goalId: z7.string().uuid(), gate: z7.enum(["start", "finish"]) }).strict()).optional(),
|
|
485
|
+
children: z7.array(z7.object({ outcome: z7.string().trim().min(1).max(1e4), ownerParticipant: z7.string().trim().min(1), gate: z7.enum(["start", "finish"]).optional() }).strict()).optional(),
|
|
486
|
+
state: z7.enum(["active", "done", "cancelled"]).optional(),
|
|
487
|
+
progress: z7.string().trim().min(1).max(1e4).optional(),
|
|
488
|
+
reviewed: z7.literal(true).optional(),
|
|
489
|
+
dueAt: z7.string().datetime({ offset: true }).nullable().optional()
|
|
336
490
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
337
|
-
reason:
|
|
338
|
-
operationId:
|
|
491
|
+
reason: z7.string().trim().min(1).max(2e3),
|
|
492
|
+
operationId: z7.string().uuid().optional()
|
|
339
493
|
}).strict();
|
|
340
|
-
var changedGoal =
|
|
341
|
-
var goalTitle =
|
|
494
|
+
var changedGoal = z7.string().uuid().describe("The Goal to change, as a read shows it.");
|
|
495
|
+
var goalTitle = z7.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
|
|
342
496
|
var GOAL_OUTCOME_MAX = 500;
|
|
343
|
-
var goalOutcome =
|
|
344
|
-
var CreateChange =
|
|
345
|
-
kind:
|
|
497
|
+
var goalOutcome = z7.string().trim().min(1).max(GOAL_OUTCOME_MAX, `outcome_too_long: a Goal's outcome is one paragraph, at most ${GOAL_OUTCOME_MAX} characters; split the rest into child Goals (create with parentGoalId).`).describe(`The desired result, one paragraph of at most ${GOAL_OUTCOME_MAX} characters. More than that is more work: create child Goals under it (parentGoalId). There is no third description field.`);
|
|
498
|
+
var CreateChange = z7.object({
|
|
499
|
+
kind: z7.literal("create"),
|
|
346
500
|
title: goalTitle,
|
|
347
501
|
outcome: goalOutcome,
|
|
348
|
-
ownerId:
|
|
349
|
-
parentGoalId:
|
|
350
|
-
sourceEntryIds:
|
|
502
|
+
ownerId: z7.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as check_activity shows it) or the person's. Omitted: you."),
|
|
503
|
+
parentGoalId: z7.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
|
|
504
|
+
sourceEntryIds: z7.array(z7.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
|
|
351
505
|
}).strict();
|
|
352
|
-
var EditChange =
|
|
353
|
-
var StateChange =
|
|
506
|
+
var EditChange = z7.object({ kind: z7.literal("edit"), goalId: changedGoal, title: goalTitle.optional(), outcome: goalOutcome.optional() }).strict();
|
|
507
|
+
var StateChange = z7.object({ kind: z7.literal("state"), goalId: changedGoal, state: z7.enum(["open", "completed", "canceled"]).describe(
|
|
354
508
|
"completed: the Goal's own work is done now (refused while required children are open); open: reopen it; canceled: it will not be done."
|
|
355
509
|
) }).strict();
|
|
356
|
-
var AssignChange =
|
|
357
|
-
var DeferChange =
|
|
510
|
+
var AssignChange = z7.object({ kind: z7.literal("assign"), goalId: changedGoal, ownerId: z7.string().trim().min(1).describe("The new owner's participant.") }).strict();
|
|
511
|
+
var DeferChange = z7.object({ kind: z7.literal("defer"), goalId: changedGoal, until: z7.string().datetime({ offset: true }).nullable().describe(
|
|
358
512
|
"Postpone it until this instant: it waits until then, and its owner is woken when it passes (a promise to follow up later). null takes the postponement back."
|
|
359
513
|
) }).strict();
|
|
360
|
-
var MoveChange =
|
|
514
|
+
var MoveChange = z7.object({ kind: z7.literal("move"), goalId: changedGoal, parentGoalId: z7.string().uuid().nullable().describe(
|
|
361
515
|
"Its new parent, or null for a root. Only this Goal moves; its new parent's other children stay."
|
|
362
516
|
) }).strict();
|
|
363
|
-
var DependencyChange =
|
|
364
|
-
kind:
|
|
365
|
-
change:
|
|
517
|
+
var DependencyChange = z7.object({
|
|
518
|
+
kind: z7.literal("dependency"),
|
|
519
|
+
change: z7.enum(["add", "remove"]),
|
|
366
520
|
goalId: changedGoal,
|
|
367
|
-
dependsOnGoalId:
|
|
368
|
-
action:
|
|
369
|
-
reason:
|
|
521
|
+
dependsOnGoalId: z7.string().uuid().describe("The Goal it waits on."),
|
|
522
|
+
action: z7.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
|
|
523
|
+
reason: z7.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
|
|
370
524
|
}).strict();
|
|
371
|
-
var read = { revision:
|
|
372
|
-
var GoalChangeSchema =
|
|
525
|
+
var read = { revision: z7.number().int().positive().optional() };
|
|
526
|
+
var GoalChangeSchema = z7.discriminatedUnion("kind", [
|
|
373
527
|
CreateChange,
|
|
374
528
|
CompleteChangeSchema.extend(read),
|
|
375
529
|
EditChange.extend(read),
|
|
@@ -381,222 +535,228 @@ var GoalChangeSchema = z5.discriminatedUnion("kind", [
|
|
|
381
535
|
]);
|
|
382
536
|
var editNames = (changes, ctx) => changes.forEach((c, i) => {
|
|
383
537
|
if (c.kind === "edit" && c.title === void 0 && c.outcome === void 0) {
|
|
384
|
-
ctx.addIssue({ code:
|
|
538
|
+
ctx.addIssue({ code: z7.ZodIssueCode.custom, path: ["changes", i], message: "an edit supplies title, outcome, or both" });
|
|
385
539
|
}
|
|
386
540
|
});
|
|
387
|
-
var ManageGoalsSchema =
|
|
388
|
-
requestId:
|
|
389
|
-
changes:
|
|
541
|
+
var ManageGoalsSchema = z7.object({
|
|
542
|
+
requestId: z7.string().uuid().optional(),
|
|
543
|
+
changes: z7.array(GoalChangeSchema).min(1).max(50)
|
|
390
544
|
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
391
|
-
var ManageGoalsToolSchema =
|
|
392
|
-
changes:
|
|
545
|
+
var ManageGoalsToolSchema = z7.object({
|
|
546
|
+
changes: z7.array(z7.discriminatedUnion("kind", [CreateChange, CompleteChangeSchema, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
|
|
393
547
|
}).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
|
|
394
548
|
var MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: complete (goalId, message capped at 50 characters, units, reply, optional next-step options {label, hint}) closes and reports atomically, exactly as contact.completions; create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goals) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
|
|
395
|
-
var GetGoalsSchema =
|
|
396
|
-
goalIds:
|
|
549
|
+
var GetGoalsSchema = z7.object({
|
|
550
|
+
goalIds: z7.array(z7.string().uuid()).min(1).max(10).describe("The Goals to read, in order; one to ten IDs."),
|
|
397
551
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
398
552
|
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
399
|
-
history:
|
|
553
|
+
history: z7.boolean().optional()
|
|
400
554
|
}).strict();
|
|
401
|
-
var GetDeliverySchema =
|
|
402
|
-
deliveryId:
|
|
555
|
+
var GetDeliverySchema = z7.object({
|
|
556
|
+
deliveryId: z7.string().uuid().describe("The Delivery id returned by contact or receive."),
|
|
403
557
|
// WHAT WAS SAID ON A CALL (owner, 2026-10-26, Goal c743f4a4: "A flag on get_delivery", "Any of my agents").
|
|
404
|
-
transcript:
|
|
405
|
-
after:
|
|
558
|
+
transcript: z7.boolean().optional().describe("For a Call: also return what was said on it, one page of spoken lines."),
|
|
559
|
+
after: z7.string().max(40).optional().describe("With transcript: the next value the previous page returned, to read the next page.")
|
|
406
560
|
}).strict();
|
|
407
561
|
var GET_DELIVERY_DESCRIPTION = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. For a Call, transcript: true adds what was said: each spoken line in order, who said it, and when from pickup, a page at a time. A line from the room, not the person, is marked room. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
|
|
408
562
|
var GET_GOALS_DESCRIPTION = "Read one to ten Goals by goalIds, returned in the same order under goals (a read never puts you on them). history applies to every requested Goal. A refused read fails the call with its original refusal; no partial result is presented as complete. Each Goal includes its ten most recent other contributors (names, latest entry headlines and times), outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.";
|
|
409
|
-
var SearchToolSchema =
|
|
410
|
-
query:
|
|
411
|
-
types:
|
|
412
|
-
goalId:
|
|
413
|
-
limit:
|
|
563
|
+
var SearchToolSchema = z7.object({
|
|
564
|
+
query: z7.string().trim().min(1).max(500),
|
|
565
|
+
types: z7.array(z7.enum(["entry", "goal", "answer"])).min(1).optional(),
|
|
566
|
+
goalId: z7.string().uuid().optional(),
|
|
567
|
+
limit: z7.number().int().min(1).max(20).optional()
|
|
414
568
|
}).strict();
|
|
415
569
|
var SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and replies -- the person's answers, acknowledgements and deferrals (type answer; found by their summary, by the question or report they reply to, or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, the replies to its items, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (a reply carries its kind -- answered, acknowledged or deferred -- whether it is closed (a deferral is not), its summary in under ten words, its question or report, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
|
|
416
|
-
var
|
|
570
|
+
var READS = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
571
|
+
var SENDS = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
|
|
572
|
+
var ConfigureIdentitySchema = z7.object({}).strict();
|
|
417
573
|
var CONFIGURE_IDENTITY_DESCRIPTION = "Read this authenticated agent\u2019s current name, voice and token ID. Read-only; no arguments. Pairing, renaming and logout are available only through the local configure driver.";
|
|
418
|
-
var CheckActivitySchema =
|
|
574
|
+
var CheckActivitySchema = z7.object({}).strict();
|
|
419
575
|
var FEEDBACK_TEXT_MAX = 5e4;
|
|
420
|
-
var SendFeedbackSchema =
|
|
421
|
-
text:
|
|
422
|
-
title:
|
|
423
|
-
kind:
|
|
576
|
+
var SendFeedbackSchema = z7.object({
|
|
577
|
+
text: z7.string().trim().min(1).max(FEEDBACK_TEXT_MAX).describe(`The report, in markdown: what happened, what was expected, how to reproduce it, versions, evidence. At most ${FEEDBACK_TEXT_MAX} characters.`),
|
|
578
|
+
title: z7.string().trim().min(1).max(200).optional().describe("One line naming it."),
|
|
579
|
+
kind: z7.enum(["bug", "idea", "other"]).default("other").describe("bug: something Paigy does wrong. idea: something it should do. other: anything else.")
|
|
424
580
|
}).strict();
|
|
425
581
|
var SEND_FEEDBACK_DESCRIPTION = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
|
|
426
582
|
var AGENT_TOOLS = [
|
|
427
|
-
{ name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION, inputSchema: mcpInputSchema(ConfigureIdentitySchema) },
|
|
428
|
-
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
429
|
-
{ name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
430
|
-
{ name: "get_delivery", description: GET_DELIVERY_DESCRIPTION, inputSchema: mcpInputSchema(GetDeliverySchema) },
|
|
431
|
-
{ name: "get_goals", description: GET_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalsSchema) },
|
|
432
|
-
{ name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
433
|
-
{ name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goals to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
|
|
434
|
-
{ name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
|
|
583
|
+
{ name: "configure", title: "Who am I", annotations: READS, description: CONFIGURE_IDENTITY_DESCRIPTION, inputSchema: mcpInputSchema(ConfigureIdentitySchema) },
|
|
584
|
+
{ name: "contact", title: "Contact the person", annotations: SENDS, description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
585
|
+
{ name: "manage_goals", title: "Manage Goals", annotations: SENDS, description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
|
|
586
|
+
{ name: "get_delivery", title: "Read a delivery", annotations: READS, description: GET_DELIVERY_DESCRIPTION, inputSchema: mcpInputSchema(GetDeliverySchema) },
|
|
587
|
+
{ name: "get_goals", title: "Read Goals", annotations: READS, description: GET_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalsSchema) },
|
|
588
|
+
{ name: "search", title: "Search history", annotations: READS, description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
|
|
589
|
+
{ name: "check_activity", title: "Recent agent activity", annotations: READS, description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goals to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
|
|
590
|
+
{ name: "send_feedback", title: "Send feedback to Paigy", annotations: SENDS, description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) },
|
|
591
|
+
// THE GUIDE FROM INSIDE THE MCP (owner, 2026-10-27): the skill's text, needing no identity (`help.ts`).
|
|
592
|
+
{ name: "help", title: "Paigy guide", annotations: READS, description: HELP_DESCRIPTION, inputSchema: mcpInputSchema(HelpSchema) }
|
|
435
593
|
];
|
|
436
594
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
595
|
+
var LISTEN_COMMAND = "npx -y -p @paigy/mcp@latest paigy-listen --brief";
|
|
596
|
+
var LISTEN_MONITOR = "your harness's MONITOR tool, the kind that wakes you on every line it prints (in Claude Code: Monitor, not a background Bash command, which only tells you when it exits; in Grok: the monitor tool, the same way)";
|
|
437
597
|
function serverInstructions(opts) {
|
|
438
|
-
const waits = opts.waits ?
|
|
439
|
-
return `On startup and after a wake, contact({wait:false}) returns your events. They repeat until
|
|
598
|
+
const waits = opts.waits ? `contact({wait:true}) waits ~45 s for a reply to what you sent; contact({}) for anything addressed to you. Call \`listen\` once to wake at rest.` : "A contact here returns after one read; answers wake you, and contact({wait:false}) collects them.";
|
|
599
|
+
return `New here? Call help. On startup and after a wake, contact({wait:false}) returns your events. They repeat until acknowledged with contact({ackEventIds}); reading does not. get_goals reads a Goal's conversation; your first write puts you on it; every Goal of your person is open to you. manage_goals creates, assigns and organizes work; to follow up, defer the Goal (woken when due). Complete and report at once: contact.completions or manage_goals kind:complete. Every read ends in \`next\`: your next step. ${waits} Report progress as a contact update (what changed for them; causedBy, shipped); one they don't need yet stays on the Goal. An open Call is not proof it rang. Your user is remote: reach them only through Paigy, never the terminal.
|
|
440
600
|
|
|
441
601
|
${ASKING_RULES}`;
|
|
442
602
|
}
|
|
443
|
-
var id =
|
|
444
|
-
var key =
|
|
445
|
-
var participant =
|
|
446
|
-
var SourceSchema =
|
|
447
|
-
var ResultRefSchema =
|
|
448
|
-
var NodeRefSchema =
|
|
449
|
-
var DependencySchema =
|
|
603
|
+
var id = z8.string().uuid();
|
|
604
|
+
var key = z8.string().min(1).max(40);
|
|
605
|
+
var participant = z8.string().regex(/^(human|agent):.+$/, "a participant ID such as agent:<uuid>");
|
|
606
|
+
var SourceSchema = z8.object({ entryId: id }).strict();
|
|
607
|
+
var ResultRefSchema = z8.union([z8.object({ id }).strict(), z8.object({ local: key }).strict()]);
|
|
608
|
+
var NodeRefSchema = z8.object({ type: z8.enum(["goal", "question"]), id }).strict();
|
|
609
|
+
var DependencySchema = z8.object({
|
|
450
610
|
blocker: NodeRefSchema,
|
|
451
611
|
blocked: NodeRefSchema,
|
|
452
|
-
action:
|
|
453
|
-
reason:
|
|
612
|
+
action: z8.enum(["start", "complete", "answer"]),
|
|
613
|
+
reason: z8.string().max(500).optional()
|
|
454
614
|
}).strict();
|
|
455
|
-
var goalState =
|
|
615
|
+
var goalState = z8.enum(["open", "completed", "canceled"]);
|
|
456
616
|
var existingGoalChanges = [
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
kind:
|
|
464
|
-
change:
|
|
617
|
+
z8.object({ kind: z8.literal("edit"), goalId: id, title: z8.string().min(1).max(120).optional(), outcome: z8.string().min(1).optional() }).strict().refine((c) => c.title !== void 0 || c.outcome !== void 0, "an edit changes the title or the outcome"),
|
|
618
|
+
z8.object({ kind: z8.literal("state"), goalId: id, state: goalState }).strict(),
|
|
619
|
+
z8.object({ kind: z8.literal("assign"), goalId: id, ownerId: participant }).strict(),
|
|
620
|
+
z8.object({ kind: z8.literal("defer"), goalId: id, until: z8.string().datetime().nullable() }).strict(),
|
|
621
|
+
z8.object({ kind: z8.literal("move"), goalId: id, parentGoalId: id.nullable() }).strict(),
|
|
622
|
+
z8.object({
|
|
623
|
+
kind: z8.literal("dependency"),
|
|
624
|
+
change: z8.enum(["add", "remove"]),
|
|
465
625
|
goalId: id,
|
|
466
626
|
dependsOnGoalId: id,
|
|
467
|
-
action:
|
|
468
|
-
reason:
|
|
627
|
+
action: z8.enum(["start", "complete"]),
|
|
628
|
+
reason: z8.string().max(500).optional()
|
|
469
629
|
}).strict()
|
|
470
630
|
];
|
|
471
|
-
var BrainGoalChangeSchema =
|
|
631
|
+
var BrainGoalChangeSchema = z8.union([
|
|
472
632
|
// newSession: Paigy starts a session to own it (owner, 2026-10-06); until it has, the person owns it.
|
|
473
|
-
|
|
474
|
-
kind:
|
|
475
|
-
outcome:
|
|
476
|
-
title:
|
|
633
|
+
z8.object({
|
|
634
|
+
kind: z8.literal("create"),
|
|
635
|
+
outcome: z8.string().min(1),
|
|
636
|
+
title: z8.string().min(1).max(120),
|
|
477
637
|
ownerId: participant,
|
|
478
638
|
parentGoal: ResultRefSchema.nullable(),
|
|
479
|
-
newSession:
|
|
639
|
+
newSession: z8.literal(true).optional()
|
|
480
640
|
}).strict(),
|
|
481
641
|
...existingGoalChanges
|
|
482
642
|
]);
|
|
483
|
-
var option =
|
|
484
|
-
var QuestionChangeSchema =
|
|
485
|
-
|
|
486
|
-
kind:
|
|
487
|
-
text:
|
|
488
|
-
summary:
|
|
643
|
+
var option = z8.object({ id: z8.string().min(1).max(40), label: z8.string().min(1), hint: z8.string().optional() }).strict();
|
|
644
|
+
var QuestionChangeSchema = z8.union([
|
|
645
|
+
z8.object({
|
|
646
|
+
kind: z8.literal("create"),
|
|
647
|
+
text: z8.string().min(1),
|
|
648
|
+
summary: z8.string().optional(),
|
|
489
649
|
answererId: participant,
|
|
490
650
|
goal: ResultRefSchema.optional(),
|
|
491
|
-
blocks:
|
|
492
|
-
options:
|
|
493
|
-
pickMode:
|
|
651
|
+
blocks: z8.array(z8.object({ question: ResultRefSchema }).strict()),
|
|
652
|
+
options: z8.array(option).min(2).optional(),
|
|
653
|
+
pickMode: z8.enum(["one", "many", "rank"]).optional()
|
|
494
654
|
}).strict().refine((c) => c.options === void 0 === (c.pickMode === void 0), "options and pickMode come together"),
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
655
|
+
z8.object({ kind: z8.literal("edit"), questionId: id, text: z8.string().min(1) }).strict(),
|
|
656
|
+
z8.object({ kind: z8.literal("assign"), questionId: id, answererId: participant }).strict(),
|
|
657
|
+
z8.object({ kind: z8.literal("withdraw"), questionId: id }).strict(),
|
|
658
|
+
z8.object({ kind: z8.literal("dependency"), change: z8.enum(["add", "remove"]), edge: DependencySchema }).strict()
|
|
499
659
|
]);
|
|
500
|
-
var lessonScope =
|
|
501
|
-
|
|
502
|
-
|
|
660
|
+
var lessonScope = z8.union([
|
|
661
|
+
z8.object({ kind: z8.literal("user") }).strict(),
|
|
662
|
+
z8.object({ kind: z8.literal("goal"), goal: ResultRefSchema }).strict()
|
|
503
663
|
]);
|
|
504
|
-
var LessonChangeSchema =
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
664
|
+
var LessonChangeSchema = z8.union([
|
|
665
|
+
z8.object({ kind: z8.literal("remember"), text: z8.string().min(1), scope: lessonScope }).strict(),
|
|
666
|
+
z8.object({ kind: z8.literal("revise"), lessonId: id, text: z8.string().min(1), scope: lessonScope }).strict(),
|
|
667
|
+
z8.object({ kind: z8.literal("withdraw"), lessonId: id }).strict()
|
|
508
668
|
]);
|
|
509
|
-
var BrainNextSchema =
|
|
510
|
-
instructions:
|
|
511
|
-
say:
|
|
512
|
-
then:
|
|
513
|
-
waitFor:
|
|
514
|
-
reason:
|
|
669
|
+
var BrainNextSchema = z8.object({
|
|
670
|
+
instructions: z8.array(z8.object({ entryKeys: z8.array(key).min(1) }).strict()),
|
|
671
|
+
say: z8.array(key),
|
|
672
|
+
then: z8.enum(["listen", "hold", "end", "none"]),
|
|
673
|
+
waitFor: z8.array(ResultRefSchema),
|
|
674
|
+
reason: z8.string()
|
|
515
675
|
}).strict();
|
|
516
676
|
var messageBase = {
|
|
517
677
|
key,
|
|
518
|
-
text:
|
|
519
|
-
goals:
|
|
520
|
-
questions:
|
|
521
|
-
entryKeys:
|
|
522
|
-
obligationIds:
|
|
678
|
+
text: z8.string().min(1),
|
|
679
|
+
goals: z8.array(ResultRefSchema),
|
|
680
|
+
questions: z8.array(ResultRefSchema),
|
|
681
|
+
entryKeys: z8.array(key),
|
|
682
|
+
obligationIds: z8.array(id)
|
|
523
683
|
};
|
|
524
|
-
var BrainMessageSchema =
|
|
684
|
+
var BrainMessageSchema = z8.union([
|
|
525
685
|
// A MESSAGE TO THE PERSON KEEPS ITS SENTENCES (turns.md): the talker writes them one by one, and each is
|
|
526
686
|
// its own unit of speech (`<cycle>:<key>:<n>`). `text` is the same words joined, for every reader of a
|
|
527
687
|
// message's words. Absent on results written before sentences.
|
|
528
|
-
|
|
688
|
+
z8.object({
|
|
529
689
|
...messageBase,
|
|
530
|
-
to:
|
|
531
|
-
sentences:
|
|
690
|
+
to: z8.object({ kind: z8.literal("user") }).strict(),
|
|
691
|
+
sentences: z8.array(z8.string().min(1)).min(1).optional(),
|
|
532
692
|
// THE REPORTS IT TELLS, BY THEIR OWN IDENTITY (call 9d1c096c, 2026-10-15): the talker's message names the report
|
|
533
693
|
// Entries it speaks of, so the plan's line names that report rather than re-finding it by its Goal. Absent on
|
|
534
694
|
// results written before it.
|
|
535
|
-
tells:
|
|
695
|
+
tells: z8.array(z8.object({ id }).strict()).optional()
|
|
536
696
|
}).strict(),
|
|
537
|
-
|
|
697
|
+
z8.object({
|
|
538
698
|
...messageBase,
|
|
539
|
-
to:
|
|
540
|
-
call:
|
|
699
|
+
to: z8.object({ kind: z8.literal("agent"), agentId: z8.string().regex(/^agent:(?!paigy$)\S+$/) }).strict(),
|
|
700
|
+
call: z8.object({ callId: id, listen: z8.literal(true) }).strict().optional(),
|
|
541
701
|
// WHAT IT ASKS THE AGENT, IN UNDER TEN WORDS (owner, 2026-10-15: "Yes, add the summary"): the talker's own, stored
|
|
542
702
|
// as the sent Entry's summary, which the recap's "Sent to <agent>: …" row shows. Absent on results written before it.
|
|
543
|
-
summary:
|
|
703
|
+
summary: z8.string().optional(),
|
|
544
704
|
// THE PERSON ASKED TO BE CALLED BACK ONCE THIS AGENT'S WORK IS DONE (turns.md, step 0): the agent owes
|
|
545
705
|
// the call, and its next contact about it rings them (`public.callbacks`).
|
|
546
|
-
callback:
|
|
706
|
+
callback: z8.literal(true).optional()
|
|
547
707
|
}).strict()
|
|
548
708
|
]);
|
|
549
|
-
var BrainSearchSchema =
|
|
550
|
-
query:
|
|
551
|
-
within:
|
|
709
|
+
var BrainSearchSchema = z8.object({
|
|
710
|
+
query: z8.string().min(1).max(500),
|
|
711
|
+
within: z8.array(z8.enum(["entries", "goals", "answers"])).min(1),
|
|
552
712
|
goalId: id.optional()
|
|
553
713
|
}).strict();
|
|
554
|
-
var BrainResultSchema =
|
|
555
|
-
messages:
|
|
556
|
-
entries:
|
|
557
|
-
questions:
|
|
558
|
-
answers:
|
|
714
|
+
var BrainResultSchema = z8.object({
|
|
715
|
+
messages: z8.array(BrainMessageSchema),
|
|
716
|
+
entries: z8.array(z8.object({ key, sources: z8.array(SourceSchema).min(1), goals: z8.array(ResultRefSchema), summary: z8.string().optional() }).strict()),
|
|
717
|
+
questions: z8.array(z8.object({ key, entryKeys: z8.array(key), change: QuestionChangeSchema }).strict()),
|
|
718
|
+
answers: z8.array(z8.object({
|
|
559
719
|
question: ResultRefSchema,
|
|
560
|
-
entryKeys:
|
|
561
|
-
summary:
|
|
562
|
-
selectedOptionIds:
|
|
720
|
+
entryKeys: z8.array(key).min(1),
|
|
721
|
+
summary: z8.string(),
|
|
722
|
+
selectedOptionIds: z8.array(z8.string().min(1)).optional()
|
|
563
723
|
}).strict()),
|
|
564
|
-
goals:
|
|
565
|
-
feedback:
|
|
566
|
-
lessons:
|
|
724
|
+
goals: z8.array(z8.object({ key, entryKeys: z8.array(key), change: BrainGoalChangeSchema }).strict()),
|
|
725
|
+
feedback: z8.array(z8.object({ entryKeys: z8.array(key).min(1) }).strict()),
|
|
726
|
+
lessons: z8.array(z8.object({ entryKeys: z8.array(key).min(1), change: LessonChangeSchema }).strict()),
|
|
567
727
|
next: BrainNextSchema,
|
|
568
728
|
search: BrainSearchSchema.optional()
|
|
569
729
|
}).strict();
|
|
570
|
-
var str2 =
|
|
571
|
-
var strs =
|
|
572
|
-
var CompactResultSchema =
|
|
573
|
-
next:
|
|
574
|
-
messages:
|
|
575
|
-
entries:
|
|
576
|
-
answers:
|
|
577
|
-
changes:
|
|
730
|
+
var str2 = z8.string();
|
|
731
|
+
var strs = z8.array(z8.string());
|
|
732
|
+
var CompactResultSchema = z8.object({
|
|
733
|
+
next: z8.object({ say: strs, then: z8.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason: str2, instructions: strs }).strict(),
|
|
734
|
+
messages: z8.array(z8.object({ key: str2, to: str2, text: str2, about: strs, entries: strs, owed: strs, inviteToCall: z8.boolean() }).strict()),
|
|
735
|
+
entries: z8.array(z8.object({ key: str2, from: str2, on: strs }).strict()),
|
|
736
|
+
answers: z8.array(z8.object({ question: str2, entries: strs, summary: str2, picked: strs }).strict()),
|
|
737
|
+
changes: z8.array(z8.object({
|
|
578
738
|
key: str2,
|
|
579
|
-
what:
|
|
580
|
-
op:
|
|
739
|
+
what: z8.enum(["goal", "question", "lesson"]),
|
|
740
|
+
op: z8.enum(["create", "edit", "assign", "open", "complete", "cancel", "defer", "move", "block", "unblock", "withdraw", "remember", "revise"]),
|
|
581
741
|
id: str2,
|
|
582
742
|
text: str2,
|
|
583
743
|
title: str2,
|
|
584
744
|
who: str2,
|
|
585
745
|
under: str2,
|
|
586
|
-
gate:
|
|
587
|
-
options:
|
|
588
|
-
pick:
|
|
746
|
+
gate: z8.enum(["start", "complete", "answer", "none"]),
|
|
747
|
+
options: z8.array(z8.object({ id: str2, label: str2 }).strict()),
|
|
748
|
+
pick: z8.enum(["one", "many", "rank", "words"]),
|
|
589
749
|
entries: strs
|
|
590
750
|
}).strict()),
|
|
591
|
-
feedback:
|
|
592
|
-
search:
|
|
751
|
+
feedback: z8.array(strs),
|
|
752
|
+
search: z8.object({ query: str2, within: strs, goal: str2 }).strict()
|
|
593
753
|
}).strict();
|
|
594
754
|
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
595
755
|
zodToJsonSchema2(CompactResultSchema, { $refStrategy: "none" })
|
|
596
756
|
);
|
|
597
|
-
var str3 =
|
|
598
|
-
var strs2 =
|
|
599
|
-
var TalkerReplySchema =
|
|
757
|
+
var str3 = z9.string();
|
|
758
|
+
var strs2 = z9.array(z9.string());
|
|
759
|
+
var TalkerReplySchema = z9.object({
|
|
600
760
|
/** What to say and send, messages to the person first: spoken in the order written. `to` is "user"
|
|
601
761
|
* or an agent's handle; `sentences` is what a message to the person says, one short sentence each
|
|
602
762
|
* ([] to an agent); `text` is what a message to an agent says ("" to the person); `summary` is what it
|
|
@@ -605,36 +765,36 @@ var TalkerReplySchema = z7.object({
|
|
|
605
765
|
* handles; `blocks` is the item a question to an agent must be answered before ("" for none);
|
|
606
766
|
* `callback` is true on a message to the agent doing work the person asked to be called back about
|
|
607
767
|
* once it is done (false otherwise): that agent then owes the call (turns.md, step 0). */
|
|
608
|
-
messages:
|
|
768
|
+
messages: z9.array(z9.object({ key: str3, to: str3, sentences: strs2, text: str3, summary: str3, about: strs2, blocks: str3, callback: z9.boolean() }).strict()),
|
|
609
769
|
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
610
|
-
then:
|
|
770
|
+
then: z9.enum(["listen", "hold", "end"]),
|
|
611
771
|
/** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
|
|
612
772
|
* IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
|
|
613
773
|
* "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
|
|
614
774
|
* end of this call), a question's handle (that question's answer) or an ISO time. */
|
|
615
|
-
replies:
|
|
775
|
+
replies: z9.array(z9.object({ item: str3, options: strs2, lines: strs2, summary: str3, defer: str3 }).strict()),
|
|
616
776
|
/** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
|
|
617
777
|
instruction: strs2,
|
|
618
778
|
/** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
|
|
619
779
|
* work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
|
|
620
780
|
file: strs2,
|
|
621
781
|
/** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
|
|
622
|
-
need:
|
|
782
|
+
need: z9.object({ kind: z9.enum(["search", "item", "agent", ""]), text: str3 }).strict(),
|
|
623
783
|
/** The handle of the item to explain ("" for none). */
|
|
624
784
|
explain: str3
|
|
625
785
|
}).strict();
|
|
626
786
|
var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
627
787
|
zodToJsonSchema3(TalkerReplySchema, { $refStrategy: "none" })
|
|
628
788
|
);
|
|
629
|
-
var str4 =
|
|
630
|
-
var strs3 =
|
|
631
|
-
var FilerReplySchema =
|
|
789
|
+
var str4 = z10.string();
|
|
790
|
+
var strs3 = z10.array(z10.string());
|
|
791
|
+
var FilerReplySchema = z10.object({
|
|
632
792
|
/** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals: a line that asks
|
|
633
793
|
* for new work goes on the Goal it creates, owner 2026-10-26), and what the line says in at most twenty
|
|
634
794
|
* words (owner, 2026-10-26: "bump it from 10 to 20 words"). */
|
|
635
|
-
filed:
|
|
636
|
-
goals:
|
|
637
|
-
op:
|
|
795
|
+
filed: z10.array(z10.object({ line: str4, goals: strs3, summary: str4 }).strict()),
|
|
796
|
+
goals: z10.array(z10.object({
|
|
797
|
+
op: z10.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
|
|
638
798
|
key: str4,
|
|
639
799
|
goal: str4,
|
|
640
800
|
title: str4,
|
|
@@ -643,33 +803,33 @@ var FilerReplySchema = z8.object({
|
|
|
643
803
|
under: str4,
|
|
644
804
|
until: str4,
|
|
645
805
|
on: str4,
|
|
646
|
-
gate:
|
|
806
|
+
gate: z10.enum(["start", "complete", "answer", ""]),
|
|
647
807
|
why: str4
|
|
648
808
|
}).strict()),
|
|
649
|
-
questions:
|
|
650
|
-
op:
|
|
809
|
+
questions: z10.array(z10.object({
|
|
810
|
+
op: z10.enum(["create", "edit", "assign", "withdraw"]),
|
|
651
811
|
key: str4,
|
|
652
812
|
question: str4,
|
|
653
813
|
text: str4,
|
|
654
814
|
summary: str4,
|
|
655
815
|
to: str4,
|
|
656
816
|
goal: str4,
|
|
657
|
-
options:
|
|
658
|
-
pick:
|
|
817
|
+
options: z10.array(z10.object({ id: str4, label: str4 }).strict()),
|
|
818
|
+
pick: z10.enum(["one", "many", "rank", "words", ""]),
|
|
659
819
|
blocks: str4
|
|
660
820
|
}).strict()),
|
|
661
|
-
lessons:
|
|
821
|
+
lessons: z10.array(z10.object({ op: z10.enum(["remember", "revise", "withdraw"]), lesson: str4, text: str4, scope: str4 }).strict()),
|
|
662
822
|
/** Line handles whose words comment on Paigy itself. */
|
|
663
823
|
feedback: strs3
|
|
664
824
|
}).strict();
|
|
665
825
|
var FILER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
666
826
|
zodToJsonSchema4(FilerReplySchema, { $refStrategy: "none" })
|
|
667
827
|
);
|
|
668
|
-
var ExplainerReplySchema =
|
|
828
|
+
var ExplainerReplySchema = z11.object({
|
|
669
829
|
/** The explanation, as sentences, spoken in order. */
|
|
670
|
-
say:
|
|
830
|
+
say: z11.array(z11.string()),
|
|
671
831
|
/** Questions to an agent about gaps in what it read: the agent's handle and the question. */
|
|
672
|
-
followUps:
|
|
832
|
+
followUps: z11.array(z11.object({ to: z11.string(), text: z11.string() }).strict())
|
|
673
833
|
}).strict();
|
|
674
834
|
var EXPLAINER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
675
835
|
zodToJsonSchema5(ExplainerReplySchema, { $refStrategy: "none" })
|
|
@@ -692,49 +852,49 @@ var DISTILLER_DEFECTS = [
|
|
|
692
852
|
"dropped"
|
|
693
853
|
// something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
|
|
694
854
|
];
|
|
695
|
-
var DistillerReplySchema =
|
|
696
|
-
defects:
|
|
697
|
-
kind:
|
|
855
|
+
var DistillerReplySchema = z12.object({
|
|
856
|
+
defects: z12.array(z12.object({
|
|
857
|
+
kind: z12.enum(DISTILLER_DEFECTS),
|
|
698
858
|
/** The handles of the lines it shows in. */
|
|
699
|
-
lines:
|
|
859
|
+
lines: z12.array(z12.string()),
|
|
700
860
|
/** The handle of the record it concerns, or "". */
|
|
701
|
-
about:
|
|
861
|
+
about: z12.string(),
|
|
702
862
|
/** One plain sentence, in words, never handles: it is what the feedback row keeps. */
|
|
703
|
-
why:
|
|
863
|
+
why: z12.string()
|
|
704
864
|
}).strict())
|
|
705
865
|
}).strict();
|
|
706
866
|
var DISTILLER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
707
867
|
zodToJsonSchema6(DistillerReplySchema, { $refStrategy: "none" })
|
|
708
868
|
);
|
|
709
869
|
var TIDY_AUDIENCES = ["talker", "filer", "agents"];
|
|
710
|
-
var TidyReplySchema =
|
|
711
|
-
lessons:
|
|
712
|
-
lesson:
|
|
713
|
-
op:
|
|
714
|
-
text:
|
|
715
|
-
for:
|
|
716
|
-
into:
|
|
870
|
+
var TidyReplySchema = z12.object({
|
|
871
|
+
lessons: z12.array(z12.object({
|
|
872
|
+
lesson: z12.string(),
|
|
873
|
+
op: z12.enum(["keep", "revise", "withdraw"]),
|
|
874
|
+
text: z12.string(),
|
|
875
|
+
for: z12.enum([...TIDY_AUDIENCES, ""]),
|
|
876
|
+
into: z12.string()
|
|
717
877
|
}).strict())
|
|
718
878
|
}).strict();
|
|
719
879
|
var TIDY_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
720
880
|
zodToJsonSchema6(TidyReplySchema, { $refStrategy: "none" })
|
|
721
881
|
);
|
|
722
|
-
var HandoutReplySchema =
|
|
723
|
-
assign:
|
|
882
|
+
var HandoutReplySchema = z13.object({
|
|
883
|
+
assign: z13.array(z13.object({
|
|
724
884
|
/** The Goal's handle (G). */
|
|
725
|
-
goal:
|
|
885
|
+
goal: z13.string(),
|
|
726
886
|
/** The agent's handle (A). */
|
|
727
|
-
to:
|
|
887
|
+
to: z13.string(),
|
|
728
888
|
/** At most ten words, naming the Goal of theirs it is most like: the reason the hand-out records. */
|
|
729
|
-
why:
|
|
889
|
+
why: z13.string()
|
|
730
890
|
}).strict()),
|
|
731
891
|
/** The Goals no listed agent's work is even loosely like: Paigy starts a session for them (owner, 2026-10-24,
|
|
732
892
|
* Goal 37603006, "One per family of related Goals"). */
|
|
733
|
-
new:
|
|
893
|
+
new: z13.array(z13.object({
|
|
734
894
|
/** The Goal's handle (G). */
|
|
735
|
-
goal:
|
|
895
|
+
goal: z13.string(),
|
|
736
896
|
/** At most ten words: why nothing active fits it. */
|
|
737
|
-
why:
|
|
897
|
+
why: z13.string()
|
|
738
898
|
}).strict())
|
|
739
899
|
}).strict();
|
|
740
900
|
var HANDOUT_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
@@ -768,20 +928,20 @@ var LIVE_MS = 3 * 6e4;
|
|
|
768
928
|
var WORKING_MS = 60 * 6e4;
|
|
769
929
|
var WORKING_TIER_MS = 2 * 60 * 6e4;
|
|
770
930
|
var RECENT_TIER_MS = 24 * 60 * 6e4;
|
|
771
|
-
var ContextSchema =
|
|
772
|
-
title:
|
|
773
|
-
description:
|
|
931
|
+
var ContextSchema = z14.object({
|
|
932
|
+
title: z14.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
933
|
+
description: z14.array(z14.string().min(1)).describe(
|
|
774
934
|
"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."
|
|
775
935
|
),
|
|
776
936
|
/** THE ASK'S UNITS (3c): its context as titled units, in order. The app lists the titles and opens
|
|
777
937
|
* a body on a tap. Absent on an ask sent without units, which renders as before. */
|
|
778
|
-
units:
|
|
938
|
+
units: z14.array(z14.object({ title: z14.string(), body: z14.string() })).optional()
|
|
779
939
|
});
|
|
780
|
-
var ParticipantSchema =
|
|
781
|
-
kind:
|
|
782
|
-
id:
|
|
940
|
+
var ParticipantSchema = z14.object({
|
|
941
|
+
kind: z14.enum(["human", "agent"]),
|
|
942
|
+
id: z14.string()
|
|
783
943
|
});
|
|
784
|
-
var TransformSchema =
|
|
944
|
+
var TransformSchema = z14.enum([
|
|
785
945
|
"structure",
|
|
786
946
|
// shape intent into an answer contract; pick channel/urgency — broker `ask`, `select` shapes, `points`
|
|
787
947
|
"request_more",
|
|
@@ -797,13 +957,13 @@ var TransformSchema = z12.enum([
|
|
|
797
957
|
"summarize"
|
|
798
958
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
799
959
|
]);
|
|
800
|
-
var VisualSchema =
|
|
801
|
-
url:
|
|
802
|
-
label:
|
|
960
|
+
var VisualSchema = z14.object({
|
|
961
|
+
url: z14.string().url(),
|
|
962
|
+
label: z14.string().optional()
|
|
803
963
|
});
|
|
804
|
-
var NotifyLevelSchema =
|
|
805
|
-
var SelectShapeSchema =
|
|
806
|
-
var ReceiptEventSchema =
|
|
964
|
+
var NotifyLevelSchema = z14.enum(["inbox", "push", "banner", "call"]);
|
|
965
|
+
var SelectShapeSchema = z14.enum(["one", "many", "rank", "confirm", "text"]);
|
|
966
|
+
var ReceiptEventSchema = z14.enum([
|
|
807
967
|
"delivered",
|
|
808
968
|
// the bundle reached the recipient at some level
|
|
809
969
|
"seen",
|
|
@@ -833,47 +993,47 @@ var ReceiptEventSchema = z12.enum([
|
|
|
833
993
|
// be rewound by a writer that forgot to advance it.
|
|
834
994
|
"restarted"
|
|
835
995
|
]);
|
|
836
|
-
var AttentionSchema =
|
|
996
|
+
var AttentionSchema = z14.object({
|
|
837
997
|
urgency: NotifyLevelSchema,
|
|
838
998
|
/** The required answer shape, or null for a plain notify that asks nothing back. */
|
|
839
999
|
select: SelectShapeSchema.nullable(),
|
|
840
1000
|
/** Coverage contract (#396) — points the answer must address; null = none declared. */
|
|
841
|
-
points:
|
|
1001
|
+
points: z14.array(z14.string()).nullable(),
|
|
842
1002
|
/** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
|
|
843
|
-
blocking:
|
|
1003
|
+
blocking: z14.boolean(),
|
|
844
1004
|
/** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
|
|
845
1005
|
* slice wires it; optional so today's rows/callers project cleanly. */
|
|
846
|
-
deadline:
|
|
1006
|
+
deadline: z14.string().datetime().nullable().optional()
|
|
847
1007
|
});
|
|
848
|
-
var NotifyRequestFields =
|
|
1008
|
+
var NotifyRequestFields = z14.object({
|
|
849
1009
|
/** Plaintext message content. Present on the plaintext path (today's shape);
|
|
850
1010
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
851
1011
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
852
1012
|
context: ContextSchema.optional(),
|
|
853
|
-
options:
|
|
1013
|
+
options: z14.array(OptionSchema.omit({ id: true }).extend({ label: z14.string().trim().min(1).max(1e3) }).strict()).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
854
1014
|
"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)."
|
|
855
1015
|
),
|
|
856
|
-
points:
|
|
1016
|
+
points: z14.array(z14.string().min(1)).optional().describe(
|
|
857
1017
|
"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."
|
|
858
1018
|
),
|
|
859
|
-
visuals:
|
|
1019
|
+
visuals: z14.array(VisualSchema).optional().describe(
|
|
860
1020
|
"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."
|
|
861
1021
|
),
|
|
862
1022
|
/** Git repo the agent is working in ("owner/name"). Local MCP fills this from the checkout — omit unless overriding. */
|
|
863
|
-
repo:
|
|
1023
|
+
repo: z14.string().optional(),
|
|
864
1024
|
/** Git branch the agent is on. Local MCP fills this from the checkout — omit unless overriding. */
|
|
865
|
-
branch:
|
|
1025
|
+
branch: z14.string().optional(),
|
|
866
1026
|
/** Continue an existing conversation — the id of any notification in it (its root
|
|
867
1027
|
* is the conversation's identity). Omitted = start a new conversation. Renamed
|
|
868
1028
|
* from `parentId` (2026-08-03): one linkage system, the parent; the API edge
|
|
869
1029
|
* still accepts the old name from older clients. */
|
|
870
|
-
parentId:
|
|
1030
|
+
parentId: z14.string().uuid().optional(),
|
|
871
1031
|
/** The durable outcome this contact advances. Optional during the notification-to-Work
|
|
872
1032
|
* migration; when present, a blocking ask creates a DecisionNeed for this Work. */
|
|
873
|
-
workId:
|
|
1033
|
+
workId: z14.string().uuid().optional(),
|
|
874
1034
|
/** Target Goal scope. During staged migration this is accepted by the shared contract but
|
|
875
1035
|
* target delivery activation remains model-gated; workId and goalId are mutually exclusive. */
|
|
876
|
-
goalId:
|
|
1036
|
+
goalId: z14.string().uuid().optional(),
|
|
877
1037
|
urgency: NotifyLevelSchema.default("inbox").describe(
|
|
878
1038
|
"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."
|
|
879
1039
|
),
|
|
@@ -881,7 +1041,7 @@ var NotifyRequestFields = z12.object({
|
|
|
881
1041
|
* visible and marks it needs_input. Renamed from the old `parentId` (2026-08-03)
|
|
882
1042
|
* when `parentId` became the conversation handle: `parentId` says WHERE, this
|
|
883
1043
|
* says HOW. */
|
|
884
|
-
clarifies:
|
|
1044
|
+
clarifies: z14.string().optional(),
|
|
885
1045
|
select: SelectShapeSchema.optional().describe(
|
|
886
1046
|
"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."
|
|
887
1047
|
),
|
|
@@ -895,20 +1055,20 @@ var NotifyRequestFields = z12.object({
|
|
|
895
1055
|
// (docs/history/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
|
|
896
1056
|
// Owner, 2026-07-28: "our actual limitation on how long something is to the user should
|
|
897
1057
|
// come from the broker splitting and summarizing." The cap that remains is a size guard.
|
|
898
|
-
ask:
|
|
1058
|
+
ask: z14.string().min(1).max(1e4).optional().describe(
|
|
899
1059
|
'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.'
|
|
900
1060
|
),
|
|
901
|
-
needs:
|
|
1061
|
+
needs: z14.array(z14.string().min(1)).optional().describe(
|
|
902
1062
|
"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."
|
|
903
1063
|
),
|
|
904
|
-
urgencyHint:
|
|
1064
|
+
urgencyHint: z14.enum(["whenever", "soon", "now"]).optional().describe(
|
|
905
1065
|
"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."
|
|
906
1066
|
),
|
|
907
1067
|
/** #575: the ONE self-report that replaces urgencyHint + blocking — what happens
|
|
908
1068
|
* to the agent's work while it waits. Normalized server-side into those two
|
|
909
1069
|
* fields (normalizeWaiting) so everything downstream is untouched; explicit
|
|
910
1070
|
* urgencyHint/blocking win when both are sent. */
|
|
911
|
-
waiting:
|
|
1071
|
+
waiting: z14.enum(["none", "soft", "hard"]).optional().describe(
|
|
912
1072
|
"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."
|
|
913
1073
|
),
|
|
914
1074
|
/** Δ9b (#895): HOLD this claim so the sender can correct the plan before anyone is
|
|
@@ -916,98 +1076,98 @@ var NotifyRequestFields = z12.object({
|
|
|
916
1076
|
* holding by default would charge every quiet claim that minute before any agent could
|
|
917
1077
|
* correct anything. Ignored for `waiting: 'hard'`: a blocking ask rings on what we have,
|
|
918
1078
|
* and the enrichment can still land mid-call (#781 re-plans the unspoken tail). */
|
|
919
|
-
confirm:
|
|
1079
|
+
confirm: z14.boolean().optional().describe(
|
|
920
1080
|
"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'."
|
|
921
1081
|
),
|
|
922
1082
|
/** #575: a RELAY of the user's explicitly stated preference, never the agent's
|
|
923
1083
|
* choice. Outranks waiting in both directions: 'call' rings even for a
|
|
924
1084
|
* waiting:'none' "call me when it's done"; 'message' never rings even for
|
|
925
1085
|
* waiting:'hard'. */
|
|
926
|
-
channel:
|
|
1086
|
+
channel: z14.enum(["call", "message"]).optional().describe(
|
|
927
1087
|
"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."
|
|
928
1088
|
),
|
|
929
|
-
confirmStyle:
|
|
1089
|
+
confirmStyle: z14.enum(["yesno", "approve"]).default("yesno").describe(
|
|
930
1090
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
931
1091
|
),
|
|
932
|
-
blocking:
|
|
1092
|
+
blocking: z14.boolean().default(false).describe(
|
|
933
1093
|
"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."
|
|
934
1094
|
)
|
|
935
1095
|
});
|
|
936
1096
|
var NotifyRequestSchema = NotifyRequestFields.superRefine((r, ctx) => {
|
|
937
|
-
if (r.workId && r.goalId) ctx.addIssue({ code:
|
|
1097
|
+
if (r.workId && r.goalId) ctx.addIssue({ code: z14.ZodIssueCode.custom, path: ["goalId"], message: "pass goalId or workId, not both" });
|
|
938
1098
|
if (r.ask !== void 0) {
|
|
939
1099
|
for (const f of ["context", "select", "points"]) {
|
|
940
1100
|
if (r[f] !== void 0)
|
|
941
|
-
ctx.addIssue({ code:
|
|
1101
|
+
ctx.addIssue({ code: z14.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.` });
|
|
942
1102
|
}
|
|
943
1103
|
return;
|
|
944
1104
|
}
|
|
945
1105
|
if (r.needs !== void 0 || r.urgencyHint !== void 0)
|
|
946
|
-
ctx.addIssue({ code:
|
|
1106
|
+
ctx.addIssue({ code: z14.ZodIssueCode.custom, path: ["needs"], message: "needs/urgencyHint belong to the simplified `ask` form \u2014 with a shaped request use points/urgency" });
|
|
947
1107
|
if (!r.context)
|
|
948
|
-
ctx.addIssue({ code:
|
|
1108
|
+
ctx.addIssue({ code: z14.ZodIssueCode.custom, path: ["context"], message: "context is required (plaintext path)" });
|
|
949
1109
|
if (!r.select)
|
|
950
|
-
ctx.addIssue({ code:
|
|
1110
|
+
ctx.addIssue({ code: z14.ZodIssueCode.custom, path: ["select"], message: "select is required on the shaped form" });
|
|
951
1111
|
const needsOptions = r.select === "one" || r.select === "many" || r.select === "rank";
|
|
952
1112
|
if (needsOptions && !r.options?.length)
|
|
953
|
-
ctx.addIssue({ code:
|
|
1113
|
+
ctx.addIssue({ code: z14.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' requires options` });
|
|
954
1114
|
if (!needsOptions && r.options?.length)
|
|
955
|
-
ctx.addIssue({ code:
|
|
1115
|
+
ctx.addIssue({ code: z14.ZodIssueCode.custom, path: ["options"], message: `select:'${r.select}' takes no options` });
|
|
956
1116
|
});
|
|
957
|
-
var NotifyStatusSchema =
|
|
958
|
-
var AgentStateSchema =
|
|
959
|
-
var TurnSchema =
|
|
960
|
-
prompt:
|
|
961
|
-
reply:
|
|
1117
|
+
var NotifyStatusSchema = z14.enum(["pending", "answered", "ignored"]);
|
|
1118
|
+
var AgentStateSchema = z14.enum(["idle", "in_progress", "completed", "needs_input"]);
|
|
1119
|
+
var TurnSchema = z14.object({
|
|
1120
|
+
prompt: z14.string(),
|
|
1121
|
+
reply: z14.string()
|
|
962
1122
|
});
|
|
963
|
-
var UserAnswerSchema =
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1123
|
+
var UserAnswerSchema = z14.discriminatedUnion("kind", [
|
|
1124
|
+
z14.object({ kind: z14.literal("option"), optionId: z14.string(), label: z14.string().optional() }),
|
|
1125
|
+
z14.object({ kind: z14.literal("text"), text: z14.string() }),
|
|
1126
|
+
z14.object({ kind: z14.literal("ignored") }),
|
|
1127
|
+
z14.object({ kind: z14.literal("multi"), optionIds: z14.array(z14.string()), labels: z14.array(z14.string()).optional() }),
|
|
1128
|
+
z14.object({ kind: z14.literal("ranked"), optionIds: z14.array(z14.string()), labels: z14.array(z14.string()).optional() }),
|
|
1129
|
+
z14.object({ kind: z14.literal("clarify"), chunks: z14.array(z14.string()).min(1) }),
|
|
1130
|
+
z14.object({ kind: z14.literal("confirm"), approved: z14.boolean() }),
|
|
1131
|
+
z14.object({ kind: z14.literal("turns"), turns: z14.array(TurnSchema).min(1) }),
|
|
972
1132
|
/** An auto-answer derived from the user's PAST decisions (docs/history/precedent-design.md §2):
|
|
973
1133
|
* delivered through the same settle/await path as a human answer, carrying the judge's
|
|
974
1134
|
* derivation and the precedent ids it grew from. Always paired with a visible trail
|
|
975
1135
|
* card the user can reply to — the broker never overrides the user. */
|
|
976
|
-
|
|
1136
|
+
z14.object({ kind: z14.literal("precedent"), answer: z14.string(), derivation: z14.string(), sources: z14.array(z14.string()).min(1) })
|
|
977
1137
|
]);
|
|
978
|
-
var IntentSchema =
|
|
1138
|
+
var IntentSchema = z14.object({
|
|
979
1139
|
// The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
|
|
980
1140
|
// it by two ("detail", "feedback"), and because the settle handler parsed the array
|
|
981
1141
|
// all-or-nothing, ONE feedback act silently dropped EVERY intent on the call,
|
|
982
1142
|
// questions included. Found auditing five calls' stored feedback, 2026-08-01.
|
|
983
|
-
kind:
|
|
984
|
-
detail:
|
|
1143
|
+
kind: z14.enum(["defer", "delegate", "channel", "question", "detail", "feedback", "command", "control"]),
|
|
1144
|
+
detail: z14.string(),
|
|
985
1145
|
/** Defer only: seconds until the callback the caller asked for, when something upstream
|
|
986
1146
|
* already read the time. Nothing sets it today (#397 documented an MCP parser that was
|
|
987
1147
|
* never written) — the API reads the defer's `detail` itself with `notes/when.ts`
|
|
988
1148
|
* (`parseDelay`, #1292), and a value here simply wins over that reading. */
|
|
989
|
-
dueInSeconds:
|
|
1149
|
+
dueInSeconds: z14.number().int().positive().optional(),
|
|
990
1150
|
/** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
|
|
991
1151
|
* already read the utterance, so `signals.kind` stops defaulting to
|
|
992
1152
|
* 'other' on every row. A table that records that something was wrong and nothing
|
|
993
1153
|
* about what cannot answer "is the bot looping less this week?". */
|
|
994
|
-
fault:
|
|
1154
|
+
fault: z14.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
|
|
995
1155
|
});
|
|
996
|
-
var RideAlongSchema =
|
|
1156
|
+
var RideAlongSchema = z14.object({
|
|
997
1157
|
/** The note this came from — assign/clarify/close it through /api/notes/:id. */
|
|
998
|
-
noteId:
|
|
1158
|
+
noteId: z14.string(),
|
|
999
1159
|
/** What to do, in the owner's own words (the note's headline). Never model-rewritten. */
|
|
1000
|
-
text:
|
|
1160
|
+
text: z14.string(),
|
|
1001
1161
|
/** The thread to report back on, when the note was dispatched over the request rail. */
|
|
1002
|
-
parentId:
|
|
1162
|
+
parentId: z14.string().nullable()
|
|
1003
1163
|
});
|
|
1004
|
-
var AwaitItemSchema =
|
|
1005
|
-
|
|
1006
|
-
type:
|
|
1007
|
-
parentId:
|
|
1008
|
-
notificationId:
|
|
1009
|
-
workId:
|
|
1010
|
-
decisionId:
|
|
1164
|
+
var AwaitItemSchema = z14.discriminatedUnion("type", [
|
|
1165
|
+
z14.object({
|
|
1166
|
+
type: z14.literal("reply"),
|
|
1167
|
+
parentId: z14.string(),
|
|
1168
|
+
notificationId: z14.string(),
|
|
1169
|
+
workId: z14.string().uuid().optional(),
|
|
1170
|
+
decisionId: z14.string().uuid().optional(),
|
|
1011
1171
|
answer: UserAnswerSchema,
|
|
1012
1172
|
/** WHAT THE AGENT CANNOT KNOW FROM THE FIELDS BESIDE IT (owner, 2026-09-04, issue
|
|
1013
1173
|
* #1537). One line, built from the record: the ask and the caller's reply VERBATIM,
|
|
@@ -1017,109 +1177,109 @@ var AwaitItemSchema = z12.discriminatedUnion("type", [
|
|
|
1017
1177
|
* "call me back after you merge" in their own words decides for itself what to do,
|
|
1018
1178
|
* and now knows exactly which call to make. Absent when either half is missing —
|
|
1019
1179
|
* a sentence with a hole in it is worse than no sentence. */
|
|
1020
|
-
note:
|
|
1180
|
+
note: z14.string().optional(),
|
|
1021
1181
|
/** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
|
|
1022
1182
|
* shaped answer was mapped from, filtered to its own claims. There is no second list
|
|
1023
1183
|
* of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
|
|
1024
1184
|
* 2026-09-04): the agent reads the sentence and decides. */
|
|
1025
|
-
transcript:
|
|
1185
|
+
transcript: z14.string().optional(),
|
|
1026
1186
|
/** Coverage report (#396), when the ask declared `points`: which of them this
|
|
1027
1187
|
* answer addressed. Missing points = re-ask or proceed knowingly partial. */
|
|
1028
|
-
covered:
|
|
1188
|
+
covered: z14.array(z14.string()).optional(),
|
|
1029
1189
|
/** Ride-alongs (RideAlongSchema) — pending work for you, attached to the moment you
|
|
1030
1190
|
* became free. Only `reply` and `idle` carry it: those are the two outcomes that
|
|
1031
1191
|
* END a wait. `remind`, `superseded` and `turn` are mid-flight, and handing an
|
|
1032
1192
|
* agent a side-quest while it is still holding the line is how the main thing gets
|
|
1033
1193
|
* dropped. Absent/empty = nothing owed. */
|
|
1034
|
-
also:
|
|
1194
|
+
also: z14.array(RideAlongSchema).optional()
|
|
1035
1195
|
}),
|
|
1036
|
-
|
|
1037
|
-
type:
|
|
1038
|
-
parentId:
|
|
1039
|
-
notificationId:
|
|
1040
|
-
remindAt:
|
|
1196
|
+
z14.object({
|
|
1197
|
+
type: z14.literal("remind"),
|
|
1198
|
+
parentId: z14.string(),
|
|
1199
|
+
notificationId: z14.string(),
|
|
1200
|
+
remindAt: z14.string().datetime({ offset: true }),
|
|
1041
1201
|
/** Seconds until remindAt, server-computed — pass straight to ScheduleWakeup. */
|
|
1042
|
-
remindInSeconds:
|
|
1202
|
+
remindInSeconds: z14.number()
|
|
1043
1203
|
}),
|
|
1044
1204
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
1045
1205
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
1046
1206
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
1047
1207
|
* re-orient via contact({})). */
|
|
1048
|
-
|
|
1049
|
-
type:
|
|
1050
|
-
parentId:
|
|
1051
|
-
notificationId:
|
|
1208
|
+
z14.object({
|
|
1209
|
+
type: z14.literal("superseded"),
|
|
1210
|
+
parentId: z14.string(),
|
|
1211
|
+
notificationId: z14.string()
|
|
1052
1212
|
}),
|
|
1053
1213
|
/** A LIVE call's turn, streamed as it lands (#783). PROVISIONAL: the user can still
|
|
1054
1214
|
* revise any of these until the final reply arrives — partial = intelligence,
|
|
1055
1215
|
* settled = authorization. Use it to PREPARE (fetch, draft, warm), never to act
|
|
1056
1216
|
* irreversibly. If `acts` carries a question aimed at you and you know the answer,
|
|
1057
1217
|
* contact on the same thread right away — the caller hears it on the same call. */
|
|
1058
|
-
|
|
1059
|
-
type:
|
|
1060
|
-
notificationId:
|
|
1061
|
-
inFlight:
|
|
1062
|
-
turn:
|
|
1063
|
-
idx:
|
|
1064
|
-
prompt:
|
|
1065
|
-
reply:
|
|
1066
|
-
acts:
|
|
1218
|
+
z14.object({
|
|
1219
|
+
type: z14.literal("partial"),
|
|
1220
|
+
notificationId: z14.string(),
|
|
1221
|
+
inFlight: z14.literal(true),
|
|
1222
|
+
turn: z14.object({
|
|
1223
|
+
idx: z14.number(),
|
|
1224
|
+
prompt: z14.string(),
|
|
1225
|
+
reply: z14.string(),
|
|
1226
|
+
acts: z14.array(IntentSchema).nullable().optional()
|
|
1067
1227
|
})
|
|
1068
1228
|
}),
|
|
1069
|
-
|
|
1070
|
-
type:
|
|
1071
|
-
also:
|
|
1229
|
+
z14.object({
|
|
1230
|
+
type: z14.literal("idle"),
|
|
1231
|
+
also: z14.array(RideAlongSchema).optional(),
|
|
1072
1232
|
/** Is a call live for this agent's user right now? The SDK polls the partial stream
|
|
1073
1233
|
* (#783) between idle ticks ONLY while this is not `false` — a partial can only exist
|
|
1074
1234
|
* during a live call, and polling for one on a banner/message was a wasted HTTP call +
|
|
1075
1235
|
* 3 queries on every idle tick of every waiting agent (~80% of all traffic at scale).
|
|
1076
1236
|
* Absent = an older API → the SDK keeps polling, exactly as before. */
|
|
1077
|
-
inFlight:
|
|
1237
|
+
inFlight: z14.boolean().optional()
|
|
1078
1238
|
})
|
|
1079
1239
|
]);
|
|
1080
|
-
var VoiceKeySchema =
|
|
1081
|
-
var AgendaTurnSchema =
|
|
1240
|
+
var VoiceKeySchema = z14.enum(["rachel", "george", "jessica", "brian", "lily"]);
|
|
1241
|
+
var AgendaTurnSchema = z14.object({
|
|
1082
1242
|
/** THE TURN'S IDENTITY (the first-sentence stream, 2026-09-09): the brain call that wrote
|
|
1083
1243
|
* it and its place in that reply — `<brainCallId>:<index>`, with `:p` on the first
|
|
1084
1244
|
* sentence a re-plan publishes ahead of the rest. A turn is spoken once, by this id: the
|
|
1085
1245
|
* completion of a streamed re-plan carries the published sentence again, and the walk
|
|
1086
1246
|
* drops what it already said by identity, never by the API's guess of what was polled.
|
|
1087
1247
|
* Absent on plans nothing streams (a ring plan, a floor). */
|
|
1088
|
-
id:
|
|
1248
|
+
id: z14.string().optional(),
|
|
1089
1249
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
1090
1250
|
* the planner declares duplicates instead of asking them twice. */
|
|
1091
|
-
coveredIds:
|
|
1251
|
+
coveredIds: z14.array(z14.string()).optional(),
|
|
1092
1252
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
1093
1253
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
1094
|
-
info:
|
|
1254
|
+
info: z14.array(z14.string().min(1)).default([]),
|
|
1095
1255
|
/** THE ID OF EACH SENTENCE, when the plan names them (turns.md): a talker message's sentences are its
|
|
1096
1256
|
* `info`, one piece each, and `units[i]` is the id the bot's line receipt carries for `info[i]`
|
|
1097
1257
|
* (`<cycle>:<key>:<n>`), so what was heard is known per sentence. Absent on every other turn. */
|
|
1098
|
-
units:
|
|
1099
|
-
question:
|
|
1258
|
+
units: z14.array(z14.string().min(1)).optional(),
|
|
1259
|
+
question: z14.string().min(1).nullable(),
|
|
1100
1260
|
/** True on the one turn carrying the agent's own declared question. */
|
|
1101
|
-
asks:
|
|
1261
|
+
asks: z14.boolean().optional(),
|
|
1102
1262
|
/** The claim this turn belongs to (#781) — the RETURN identity: answers route by it.
|
|
1103
1263
|
* Absent on a single-claim plan (the session's own claim) and on shared context turns,
|
|
1104
1264
|
* which route nothing. */
|
|
1105
|
-
claimId:
|
|
1265
|
+
claimId: z14.string().optional(),
|
|
1106
1266
|
/** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
|
|
1107
|
-
voice:
|
|
1267
|
+
voice: z14.string().optional(),
|
|
1108
1268
|
/** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
|
|
1109
1269
|
* whose request this is: an item that folded in from another agent arrived as a bare
|
|
1110
1270
|
* non-sequitur ("First real production sign-in is yours to make whenever you want.")
|
|
1111
1271
|
* and the owner answered "What?". The bot names the agent before its first turn. */
|
|
1112
|
-
agent:
|
|
1272
|
+
agent: z14.string().optional(),
|
|
1113
1273
|
/** The claim's agent by ID — the pairing's connection id (`notifications.token_id`), the
|
|
1114
1274
|
* same id a face is minted from. A name is not an identity: two pairings may be called
|
|
1115
1275
|
* "Claude", and a name cannot be joined on. The record's entries carry it (`agent_id`)
|
|
1116
1276
|
* so "who said that" survives the call, and it rides PER TURN because a coalesced call
|
|
1117
1277
|
* speaks for several agents — the turn is the only place that knows which. */
|
|
1118
|
-
agentId:
|
|
1278
|
+
agentId: z14.string().optional(),
|
|
1119
1279
|
/** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
|
|
1120
1280
|
* the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
|
|
1121
1281
|
* question turn — the talker owns re-asking and the waits, so `question` stays null. */
|
|
1122
|
-
onTable:
|
|
1282
|
+
onTable: z14.boolean().optional(),
|
|
1123
1283
|
/** THE CARRIED ITEM THIS LINE TELLS (Goal 2a1e50eb, owner 2026-10-14: "Build the whole design") — a
|
|
1124
1284
|
* request the Call carries that asks NOTHING: a report, a progress note, another agent's waiting news.
|
|
1125
1285
|
* Its request Entry id, so the phone can make that agenda row the current one while it is read
|
|
@@ -1129,14 +1289,14 @@ var AgendaTurnSchema = z12.object({
|
|
|
1129
1289
|
* record tags the line with it (`workId`) — and an answer must never route to an Entry that asks
|
|
1130
1290
|
* nothing. Absent on a turn speaking for a question (`claimId` says that) and on one speaking for no
|
|
1131
1291
|
* carried item at all (an opener, a sign-off, Paigy's own line). */
|
|
1132
|
-
telling:
|
|
1292
|
+
telling: z14.string().optional(),
|
|
1133
1293
|
/** PAIGY'S OWN LINE (owner, 2026-10-15; calls ed2cbdce, ad50ad25): a turn that speaks for no item -- an
|
|
1134
1294
|
* opener, a sign-off, a reply about nothing an agent asked or told -- is recorded as Paigy's, not as the
|
|
1135
1295
|
* agent that rang. Absent on every turn that speaks for an item (`agentId` names whose), and on plans
|
|
1136
1296
|
* from before it: a bot that does not know it records the ringing agent, as before. */
|
|
1137
|
-
speaksFor:
|
|
1297
|
+
speaksFor: z14.literal("paigy").optional(),
|
|
1138
1298
|
select: SelectShapeSchema.optional(),
|
|
1139
|
-
options:
|
|
1299
|
+
options: z14.array(OptionSchema.omit({ id: true })).optional(),
|
|
1140
1300
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
1141
1301
|
paces itself now — a short beat between the sentences of a turn, the longer one at its end
|
|
1142
1302
|
(owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
|
|
@@ -1145,58 +1305,58 @@ var AgendaTurnSchema = z12.object({
|
|
|
1145
1305
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
1146
1306
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
1147
1307
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
1148
|
-
blocking:
|
|
1308
|
+
blocking: z14.boolean().optional(),
|
|
1149
1309
|
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
1150
1310
|
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
1151
1311
|
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
1152
1312
|
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
1153
1313
|
* speak, it is dropped and their words are taken like any reply. */
|
|
1154
|
-
ifSilent:
|
|
1314
|
+
ifSilent: z14.boolean().optional()
|
|
1155
1315
|
});
|
|
1156
1316
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
1157
|
-
var CallQuestionSchema =
|
|
1158
|
-
id:
|
|
1159
|
-
entryId:
|
|
1160
|
-
title:
|
|
1161
|
-
state:
|
|
1162
|
-
answer:
|
|
1317
|
+
var CallQuestionSchema = z14.object({
|
|
1318
|
+
id: z14.string(),
|
|
1319
|
+
entryId: z14.string(),
|
|
1320
|
+
title: z14.string(),
|
|
1321
|
+
state: z14.enum(["open", "answered"]),
|
|
1322
|
+
answer: z14.string().nullable(),
|
|
1163
1323
|
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
1164
1324
|
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
1165
1325
|
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
1166
1326
|
* author is not an agent this account holds (unpaired since, or a person). */
|
|
1167
|
-
agent:
|
|
1327
|
+
agent: z14.object({ tokenId: z14.string(), name: z14.string(), voice: VoiceKeySchema.optional() }).optional(),
|
|
1168
1328
|
/** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
|
|
1169
1329
|
* its need offers, exactly as its own card carries them, present only when one of them has a
|
|
1170
1330
|
* preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
|
|
1171
1331
|
* never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
|
|
1172
|
-
options:
|
|
1332
|
+
options: z14.array(OptionSchema).optional(),
|
|
1173
1333
|
/** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
|
|
1174
1334
|
* options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
|
|
1175
1335
|
* numbered chips above the call's controls, so a checklist is never only something read aloud. A
|
|
1176
1336
|
* field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
|
|
1177
1337
|
* preview layout for any `options`, and must see exactly what it did. */
|
|
1178
|
-
choices:
|
|
1338
|
+
choices: z14.array(OptionSchema).optional(),
|
|
1179
1339
|
/** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
|
|
1180
|
-
select:
|
|
1340
|
+
select: z14.enum(["one", "many", "rank"]).optional()
|
|
1181
1341
|
});
|
|
1182
|
-
var InboxItemSchema =
|
|
1183
|
-
id:
|
|
1184
|
-
tokenId:
|
|
1342
|
+
var InboxItemSchema = z14.object({
|
|
1343
|
+
id: z14.string(),
|
|
1344
|
+
tokenId: z14.string().optional(),
|
|
1185
1345
|
status: NotifyStatusSchema,
|
|
1186
1346
|
context: ContextSchema,
|
|
1187
|
-
options:
|
|
1347
|
+
options: z14.array(OptionSchema).optional(),
|
|
1188
1348
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
1189
|
-
points:
|
|
1349
|
+
points: z14.array(z14.string()).optional(),
|
|
1190
1350
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
1191
1351
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
1192
1352
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
1193
1353
|
* and neither of those is "got it" (owner, 2026-08-10). */
|
|
1194
|
-
asks:
|
|
1354
|
+
asks: z14.boolean().optional(),
|
|
1195
1355
|
/** When a live process last pulsed for this row's agent — the liveness input for
|
|
1196
1356
|
* "working requires a pulse" (#928): the list said "Working…" from agent_state alone
|
|
1197
1357
|
* while the party called the same dead claim stalled. Absent = no token/no data,
|
|
1198
1358
|
* which must never CLAIM stalled. */
|
|
1199
|
-
lastSeenAt:
|
|
1359
|
+
lastSeenAt: z14.string().optional(),
|
|
1200
1360
|
/** WHEN THE AGENT LAST SAID ANYTHING ABOUT THIS CLAIM — the newest `agent_state` row in
|
|
1201
1361
|
* the `notification_events` ledger (trigger-written since 20260621010000, so every row a
|
|
1202
1362
|
* user can see has one). The age input for `CLAIM_STALE_MS`, and it has to be this rather
|
|
@@ -1206,7 +1366,7 @@ var InboxItemSchema = z12.object({
|
|
|
1206
1366
|
* work. Reading the row's birth as the claim's age brands that "No update in 8h" the
|
|
1207
1367
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
1208
1368
|
* `createdAt`. */
|
|
1209
|
-
agentStateAt:
|
|
1369
|
+
agentStateAt: z14.string().datetime().optional(),
|
|
1210
1370
|
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
|
|
1211
1371
|
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
1212
1372
|
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
@@ -1220,40 +1380,39 @@ var InboxItemSchema = z12.object({
|
|
|
1220
1380
|
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
1221
1381
|
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
1222
1382
|
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
1223
|
-
questions:
|
|
1383
|
+
questions: z14.array(CallQuestionSchema).optional(),
|
|
1224
1384
|
/** The same call carrying order, including request Entries with no DecisionNeed as reports.
|
|
1225
1385
|
* Additive: questions stays decision-only for every other surface and older bundles. */
|
|
1226
|
-
callItems:
|
|
1227
|
-
visuals:
|
|
1386
|
+
callItems: z14.array(CallQuestionSchema.extend({ state: z14.enum(["open", "answered", "report"]), acknowledged: z14.boolean().optional() })).optional(),
|
|
1387
|
+
visuals: z14.array(VisualSchema).optional(),
|
|
1228
1388
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
1229
1389
|
* agent's suggestion, or a default silly name). */
|
|
1230
|
-
name:
|
|
1390
|
+
name: z14.string(),
|
|
1231
1391
|
/** The pairing's assigned voice (#462); absent = the default voice. */
|
|
1232
1392
|
voice: VoiceKeySchema.optional(),
|
|
1233
|
-
repo:
|
|
1234
|
-
branch:
|
|
1235
|
-
createdAt:
|
|
1236
|
-
snoozedUntil:
|
|
1393
|
+
repo: z14.string().optional(),
|
|
1394
|
+
branch: z14.string().optional(),
|
|
1395
|
+
createdAt: z14.string().datetime(),
|
|
1396
|
+
snoozedUntil: z14.string().datetime().optional(),
|
|
1397
|
+
/** A past card the person dismissed that Restore can take back (#2699): its newest close is their
|
|
1398
|
+
* dismissal, with what it withdrew recorded (`private.restore_delivery`). Absent on everything else —
|
|
1399
|
+
* a put-off, a card an ended Goal or a settled question closed, and every open card. */
|
|
1400
|
+
restorable: z14.boolean().optional(),
|
|
1237
1401
|
agentState: AgentStateSchema.default("idle"),
|
|
1238
1402
|
/** Whose action the item is waiting on: "you" = an agent asked you (the default,
|
|
1239
1403
|
* every agent→user notification); "agent" = you sent a request and it's awaiting the
|
|
1240
1404
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
1241
|
-
turn:
|
|
1405
|
+
turn: z14.enum(["you", "agent"]).default("you"),
|
|
1242
1406
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
1243
1407
|
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
1244
1408
|
* error badge + Retry. */
|
|
1245
|
-
error:
|
|
1246
|
-
|
|
1247
|
-
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
1248
|
-
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
1249
|
-
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
1250
|
-
cold: z12.string().datetime().optional(),
|
|
1251
|
-
clarifies: z12.string().optional(),
|
|
1409
|
+
error: z14.string().optional(),
|
|
1410
|
+
clarifies: z14.string().optional(),
|
|
1252
1411
|
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
1253
1412
|
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
1254
1413
|
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
1255
1414
|
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
1256
|
-
onCall:
|
|
1415
|
+
onCall: z14.literal(true).optional(),
|
|
1257
1416
|
/** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
|
|
1258
1417
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
1259
1418
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -1263,31 +1422,31 @@ var InboxItemSchema = z12.object({
|
|
|
1263
1422
|
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
1264
1423
|
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
1265
1424
|
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
1266
|
-
ring:
|
|
1267
|
-
declinedAt:
|
|
1268
|
-
anchorAt:
|
|
1269
|
-
nextRingAt:
|
|
1270
|
-
step:
|
|
1425
|
+
ring: z14.object({
|
|
1426
|
+
declinedAt: z14.string().datetime(),
|
|
1427
|
+
anchorAt: z14.string().datetime(),
|
|
1428
|
+
nextRingAt: z14.string().datetime().nullable(),
|
|
1429
|
+
step: z14.number().int()
|
|
1271
1430
|
}).optional(),
|
|
1272
1431
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
1273
1432
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
1274
1433
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
1275
1434
|
* 2026-08-07) — its real job is to give "this didn't need a call" something to be
|
|
1276
1435
|
* feedback ABOUT. */
|
|
1277
|
-
why:
|
|
1436
|
+
why: z14.object({
|
|
1278
1437
|
asked: NotifyLevelSchema,
|
|
1279
1438
|
got: NotifyLevelSchema,
|
|
1280
|
-
because:
|
|
1281
|
-
line:
|
|
1439
|
+
because: z14.enum(["unresponsive", "dismissed", "not_permitted", "silent", "coalesced", "agent_capped", "unplanned", "learned_raise"]).optional(),
|
|
1440
|
+
line: z14.string()
|
|
1282
1441
|
}).optional(),
|
|
1283
|
-
select:
|
|
1284
|
-
confirmStyle:
|
|
1442
|
+
select: z14.enum(["one", "many", "rank", "confirm", "text"]).default("one"),
|
|
1443
|
+
confirmStyle: z14.enum(["yesno", "approve"]).default("yesno").describe(
|
|
1285
1444
|
"Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
|
|
1286
1445
|
),
|
|
1287
1446
|
/** Real downstream work is stuck behind this one — set by the agent, independent of
|
|
1288
1447
|
* urgency (see the main README's "premier use case" + docs/history/notify-states.md). Drives the
|
|
1289
1448
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
1290
|
-
blocking:
|
|
1449
|
+
blocking: z14.boolean().default(false),
|
|
1291
1450
|
/** WHAT FREED THIS QUESTION (owner, 2026-10-16, Goal 2383ef14: "we don't really show the question
|
|
1292
1451
|
* at the top until the question it could reply to with is resolved. And then we show it again
|
|
1293
1452
|
* with the context of the other thing that was resolved"; asked how it should carry that, "The
|
|
@@ -1295,20 +1454,25 @@ var InboxItemSchema = z12.object({
|
|
|
1295
1454
|
* (`private.ask_back`) and drops out of the front while it waits; when it comes back, the card
|
|
1296
1455
|
* draws their question and the answer it got ABOVE the ask, as one card. Absent on a question
|
|
1297
1456
|
* nothing ever held back. */
|
|
1298
|
-
freedBy:
|
|
1299
|
-
questionId:
|
|
1300
|
-
asked:
|
|
1301
|
-
answer:
|
|
1302
|
-
answeredAt:
|
|
1457
|
+
freedBy: z14.object({
|
|
1458
|
+
questionId: z14.string(),
|
|
1459
|
+
asked: z14.string(),
|
|
1460
|
+
answer: z14.string(),
|
|
1461
|
+
answeredAt: z14.string(),
|
|
1303
1462
|
/** The answer in ten words, when the talker wrote one beside it. */
|
|
1304
|
-
summary:
|
|
1463
|
+
summary: z14.string().optional()
|
|
1305
1464
|
}).optional(),
|
|
1306
1465
|
/** WHAT THIS CARD ANSWERS (owner, 2026-10-27, Goal 81b6a9d6: "this reads very clearly as an answer to
|
|
1307
1466
|
* something I asked, but I don't see the question… To show the question near the top of the card").
|
|
1308
1467
|
* An agent's card that is its answer to one of the person's own questions carries that question's
|
|
1309
1468
|
* words, drawn above it as "You asked" (`public.questions_answered_by`, read beside the card read).
|
|
1310
1469
|
* Absent on every card that answers nothing of theirs, and on a held question, which has `freedBy`. */
|
|
1311
|
-
answering:
|
|
1470
|
+
answering: z14.object({ asked: z14.string() }).optional(),
|
|
1471
|
+
/** WHEN ITS AGENT LAST BUMPED THIS QUESTION (#2855, `contact`'s `bumpQuestionIds`): the agent asked
|
|
1472
|
+
* the person to notice it again, and For you puts it first, the latest bump on top
|
|
1473
|
+
* (`question/order.ts`). Absent on a question never bumped, and on every card from an API older
|
|
1474
|
+
* than the bump. */
|
|
1475
|
+
bumpedAt: z14.string().optional(),
|
|
1312
1476
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
1313
1477
|
answer: UserAnswerSchema.optional(),
|
|
1314
1478
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -1316,17 +1480,17 @@ var InboxItemSchema = z12.object({
|
|
|
1316
1480
|
* for a request that asks nothing), whether its content is sealed, and that Goal's state. The
|
|
1317
1481
|
* answer writer (`POST /api/entries`) and the disposition (`close_delivery`) take their ids from
|
|
1318
1482
|
* here. The server projects it (`apps/api/src/inbox/project.ts`); a client never builds it. */
|
|
1319
|
-
communication:
|
|
1320
|
-
deliveryId:
|
|
1321
|
-
kind:
|
|
1322
|
-
entryId:
|
|
1323
|
-
goalIds:
|
|
1324
|
-
decisionNeedId:
|
|
1325
|
-
sealed:
|
|
1326
|
-
goalState:
|
|
1483
|
+
communication: z14.object({
|
|
1484
|
+
deliveryId: z14.string(),
|
|
1485
|
+
kind: z14.enum(["notification", "call"]),
|
|
1486
|
+
entryId: z14.string(),
|
|
1487
|
+
goalIds: z14.array(z14.string()),
|
|
1488
|
+
decisionNeedId: z14.string().optional(),
|
|
1489
|
+
sealed: z14.boolean(),
|
|
1490
|
+
goalState: z14.string().optional(),
|
|
1327
1491
|
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
1328
1492
|
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
1329
|
-
goalTitle:
|
|
1493
|
+
goalTitle: z14.string().optional()
|
|
1330
1494
|
}).optional(),
|
|
1331
1495
|
/** WHAT THIS CARD IS, IN TWELVE CHARACTERS (#3019) — the hash of every other field on it, stamped
|
|
1332
1496
|
* by the one read that serves the open list (`apps/api/src/inbox/project.ts` `inboxFor`). It is
|
|
@@ -1335,30 +1499,30 @@ var InboxItemSchema = z12.object({
|
|
|
1335
1499
|
*
|
|
1336
1500
|
* THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK — the same mechanism as `QueueItem.rev`
|
|
1337
1501
|
* (#2928) and an entry's (#3018), for the same reason: a card shows facts no `updated_at` of its
|
|
1338
|
-
* own moves (its Goal's state and name,
|
|
1502
|
+
* own moves (its Goal's state and name, whether a ring is
|
|
1339
1503
|
* live). Optional, so a fixture, the demo and the archive lens need not spell one, and a card
|
|
1340
1504
|
* with no rev is simply always re-sent. */
|
|
1341
|
-
rev:
|
|
1505
|
+
rev: z14.string().optional()
|
|
1342
1506
|
});
|
|
1343
1507
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
1344
|
-
var PushTokenSchema =
|
|
1345
|
-
voipToken:
|
|
1346
|
-
alertToken:
|
|
1347
|
-
fcmToken:
|
|
1348
|
-
platform:
|
|
1508
|
+
var PushTokenSchema = z14.object({
|
|
1509
|
+
voipToken: z14.string().min(1).optional(),
|
|
1510
|
+
alertToken: z14.string().min(1).optional(),
|
|
1511
|
+
fcmToken: z14.string().min(1).optional(),
|
|
1512
|
+
platform: z14.enum(["ios", "android"])
|
|
1349
1513
|
}).superRefine((v, ctx) => {
|
|
1350
1514
|
if (v.platform !== "ios") return;
|
|
1351
1515
|
for (const field of ["voipToken", "alertToken"]) {
|
|
1352
1516
|
const token = v[field];
|
|
1353
1517
|
if (token === void 0 || APNS_TOKEN_RE.test(token)) continue;
|
|
1354
1518
|
ctx.addIssue({
|
|
1355
|
-
code:
|
|
1519
|
+
code: z14.ZodIssueCode.custom,
|
|
1356
1520
|
path: [field],
|
|
1357
1521
|
message: `not an APNs device token (want 64 hex chars, got ${token.length})`
|
|
1358
1522
|
});
|
|
1359
1523
|
}
|
|
1360
1524
|
});
|
|
1361
|
-
var MissedCallSchema =
|
|
1525
|
+
var MissedCallSchema = z14.enum([
|
|
1362
1526
|
"retry_10m",
|
|
1363
1527
|
"retry_30m",
|
|
1364
1528
|
"retry_60m",
|
|
@@ -1371,31 +1535,31 @@ var MissedCallSchema = z12.enum([
|
|
|
1371
1535
|
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
1372
1536
|
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
1373
1537
|
var THEIR_QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} their time; the count waits for morning.`;
|
|
1374
|
-
var BrokerTuningSchema =
|
|
1538
|
+
var BrokerTuningSchema = z14.object({
|
|
1375
1539
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
1376
|
-
ackVerbosity:
|
|
1540
|
+
ackVerbosity: z14.enum(["normal", "none"]).optional(),
|
|
1377
1541
|
/** How readily the mapper asks its one clarification: 'low' = only when truly
|
|
1378
1542
|
* uninterpretable, 'high' = whenever not fully certain. */
|
|
1379
|
-
clarifyEagerness:
|
|
1543
|
+
clarifyEagerness: z14.enum(["low", "normal", "high"]).optional(),
|
|
1380
1544
|
/** The user's own shorthand: when they say `say`, they mean `mean`. */
|
|
1381
|
-
phrasebook:
|
|
1545
|
+
phrasebook: z14.array(z14.object({ say: z14.string().min(1).max(60), mean: z14.string().min(1).max(120) })).max(24).optional(),
|
|
1382
1546
|
/** The language calls are PLANNED in, when the account has chosen one (#1272). Absent —
|
|
1383
1547
|
* which is every account today — means the agent's own words decide, per ask: a call
|
|
1384
1548
|
* about an English ask opens in English. This is the only thing that overrides that,
|
|
1385
1549
|
* and a live caller who switches language mid-call still outranks it (broker/lang.ts).
|
|
1386
1550
|
* Set per user (no UI yet), like `voiceTuning`. */
|
|
1387
|
-
language:
|
|
1551
|
+
language: z14.enum(["en", "es"]).optional()
|
|
1388
1552
|
});
|
|
1389
|
-
var UserSettingsSchema =
|
|
1390
|
-
permissions:
|
|
1391
|
-
call:
|
|
1392
|
-
banner:
|
|
1393
|
-
push:
|
|
1553
|
+
var UserSettingsSchema = z14.object({
|
|
1554
|
+
permissions: z14.object({
|
|
1555
|
+
call: z14.boolean(),
|
|
1556
|
+
banner: z14.boolean(),
|
|
1557
|
+
push: z14.boolean()
|
|
1394
1558
|
}),
|
|
1395
1559
|
/** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
|
|
1396
1560
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
1397
1561
|
* what people read (owner, 2026-09-30). */
|
|
1398
|
-
sessionMode:
|
|
1562
|
+
sessionMode: z14.enum(["default", "all_calls", "silent"]),
|
|
1399
1563
|
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
1400
1564
|
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
1401
1565
|
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
@@ -1404,7 +1568,7 @@ var UserSettingsSchema = z12.object({
|
|
|
1404
1568
|
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
1405
1569
|
* next dead field for the same reason. */
|
|
1406
1570
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
1407
|
-
improveConsent:
|
|
1571
|
+
improveConsent: z14.boolean(),
|
|
1408
1572
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
1409
1573
|
/** Where voice audio is processed. 'hosted' (default) = Paigy's voice services
|
|
1410
1574
|
* (ElevenLabs TTS, faster-whisper STT, the call bot); 'on_device' = the phone
|
|
@@ -1412,17 +1576,14 @@ var UserSettingsSchema = z12.object({
|
|
|
1412
1576
|
* Optional, NOT defaulted: a stale client PATCHing the full settings object
|
|
1413
1577
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
1414
1578
|
* write, 'hosted' on read (see store.ts). */
|
|
1415
|
-
voiceMode:
|
|
1416
|
-
/**
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1419
|
-
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
1420
|
-
* read (see store.ts). */
|
|
1421
|
-
talk: z12.boolean().optional(),
|
|
1579
|
+
voiceMode: z14.enum(["hosted", "on_device"]).optional(),
|
|
1580
|
+
/** `talk` lived here until #2373: it gated auto-play, which went in #2374. An older bundle's
|
|
1581
|
+
* `talk` is stripped like `silentPush` was, and the `user_settings.talk` column went in
|
|
1582
|
+
* 20261028142255. */
|
|
1422
1583
|
/** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
|
|
1423
1584
|
* load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
|
|
1424
1585
|
* newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
|
|
1425
|
-
callDiagnostics:
|
|
1586
|
+
callDiagnostics: z14.boolean().optional(),
|
|
1426
1587
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
1427
1588
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
1428
1589
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -1430,7 +1591,7 @@ var UserSettingsSchema = z12.object({
|
|
|
1430
1591
|
* payload['tuning'] (e.g. { silence_s: 3.5 } — a longer pause window for a
|
|
1431
1592
|
* slower speaker). No API-side semantics; the bot resolves each key with its
|
|
1432
1593
|
* own defaults. Set per user (no UI yet); absent = bot defaults. */
|
|
1433
|
-
voiceTuning:
|
|
1594
|
+
voiceTuning: z14.record(z14.string(), z14.union([z14.number(), z14.string()])).optional(),
|
|
1434
1595
|
/** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
|
|
1435
1596
|
* only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
|
|
1436
1597
|
* an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
|
|
@@ -1439,71 +1600,71 @@ var UserSettingsSchema = z12.object({
|
|
|
1439
1600
|
* that failure reads as the reminder rail being unreliable rather than as a missing
|
|
1440
1601
|
* setting. Absent = a spoken time can't be landed, so the reminder rides the next
|
|
1441
1602
|
* call — honest about what we know. */
|
|
1442
|
-
timezone:
|
|
1603
|
+
timezone: z14.string().min(1).max(64).optional(),
|
|
1443
1604
|
/** Rung-2 broker tuning (#381). Optional and NOT defaulted, same stale-client
|
|
1444
1605
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
1445
1606
|
broker: BrokerTuningSchema.optional()
|
|
1446
1607
|
});
|
|
1447
|
-
var GoalLatestSchema =
|
|
1448
|
-
from:
|
|
1449
|
-
said:
|
|
1450
|
-
at:
|
|
1451
|
-
entryId:
|
|
1608
|
+
var GoalLatestSchema = z14.object({
|
|
1609
|
+
from: z14.enum(["person", "agent"]),
|
|
1610
|
+
said: z14.string(),
|
|
1611
|
+
at: z14.string(),
|
|
1612
|
+
entryId: z14.string(),
|
|
1452
1613
|
/** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
|
|
1453
1614
|
* at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
|
|
1454
1615
|
* none was. The Work row reads it before `said`. */
|
|
1455
|
-
summary:
|
|
1616
|
+
summary: z14.string().optional(),
|
|
1456
1617
|
/** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
|
|
1457
1618
|
* of each unit"): each at most 50 characters, in order; absent when it has none. */
|
|
1458
|
-
titles:
|
|
1619
|
+
titles: z14.array(z14.string()).optional(),
|
|
1459
1620
|
/** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
|
|
1460
1621
|
* update's own line, before its units; the row caps it at 200. Absent on anything else. */
|
|
1461
|
-
line:
|
|
1622
|
+
line: z14.string().optional()
|
|
1462
1623
|
});
|
|
1463
|
-
var HistoryWorkSchema =
|
|
1464
|
-
id:
|
|
1465
|
-
title:
|
|
1466
|
-
state:
|
|
1624
|
+
var HistoryWorkSchema = z14.object({
|
|
1625
|
+
id: z14.string(),
|
|
1626
|
+
title: z14.string(),
|
|
1627
|
+
state: z14.enum(["done", "cancelled"]),
|
|
1467
1628
|
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
1468
|
-
assignee:
|
|
1629
|
+
assignee: z14.string(),
|
|
1469
1630
|
latest: GoalLatestSchema.nullable().optional()
|
|
1470
1631
|
});
|
|
1471
|
-
var HistoryEntrySchema =
|
|
1472
|
-
|
|
1473
|
-
|
|
1632
|
+
var HistoryEntrySchema = z14.union([
|
|
1633
|
+
z14.object({ at: z14.string(), card: InboxItemSchema }),
|
|
1634
|
+
z14.object({ at: z14.string(), work: HistoryWorkSchema })
|
|
1474
1635
|
]);
|
|
1475
|
-
var HistoryPageSchema =
|
|
1476
|
-
entries:
|
|
1477
|
-
next:
|
|
1636
|
+
var HistoryPageSchema = z14.object({
|
|
1637
|
+
entries: z14.array(HistoryEntrySchema),
|
|
1638
|
+
next: z14.string().nullable()
|
|
1478
1639
|
});
|
|
1479
1640
|
var ACTIVITY_LINES = 2;
|
|
1480
1641
|
var ACTIVITY_LINE_MAX = 80;
|
|
1481
|
-
var AgentActivitySchema =
|
|
1642
|
+
var AgentActivitySchema = z14.object({
|
|
1482
1643
|
/** Oldest first, so the newest line is last — the one that replaces in place. */
|
|
1483
|
-
lines:
|
|
1644
|
+
lines: z14.array(z14.string().max(ACTIVITY_LINE_MAX)).max(ACTIVITY_LINES),
|
|
1484
1645
|
/** When the harness observed this tail. Its own timestamp, not the heartbeat's: a beat
|
|
1485
1646
|
* that carries an UNCHANGED tail must not make a stalled agent look like it just moved. */
|
|
1486
|
-
at:
|
|
1647
|
+
at: z14.string().datetime()
|
|
1487
1648
|
});
|
|
1488
|
-
var ConnectionSummarySchema =
|
|
1649
|
+
var ConnectionSummarySchema = z14.object({
|
|
1489
1650
|
/** The connection = the agent's token id (used to address a request). */
|
|
1490
|
-
id:
|
|
1651
|
+
id: z14.string(),
|
|
1491
1652
|
/** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
|
|
1492
1653
|
* never talks); "agent" = an identity that sends. The roster and devices surfaces split
|
|
1493
1654
|
* on this. Optional/absent reads as "agent" (a row predating the kind column). See
|
|
1494
1655
|
* docs/server/tokens/devices-vs-agents-design.md. */
|
|
1495
|
-
kind:
|
|
1656
|
+
kind: z14.enum(["device", "agent"]).optional(),
|
|
1496
1657
|
/** For an agent, the token id of the DEVICE that minted it — so agents group under their
|
|
1497
1658
|
* machine, and revoking a device cascades to them. Null on devices, and on unlinked
|
|
1498
1659
|
* agents (provider-managed, hatched by another agent, or minted before the link existed);
|
|
1499
1660
|
* a phone-launched agent is linked to the computer it runs on since #1602. */
|
|
1500
|
-
mintedByDevice:
|
|
1501
|
-
device:
|
|
1661
|
+
mintedByDevice: z14.string().nullable().optional(),
|
|
1662
|
+
device: z14.string().nullable(),
|
|
1502
1663
|
/** The agent's display name (the single pairing name). */
|
|
1503
|
-
name:
|
|
1664
|
+
name: z14.string(),
|
|
1504
1665
|
/** For a managed connection, the provider key (e.g. "cma") that agentOrigin maps to a
|
|
1505
1666
|
* label; null for a local connection. Sourced from the token's provider, not the name. */
|
|
1506
|
-
provider:
|
|
1667
|
+
provider: z14.string().nullable(),
|
|
1507
1668
|
/** The pairing's assigned voice (#462); null = the default voice. */
|
|
1508
1669
|
voice: VoiceKeySchema.nullable(),
|
|
1509
1670
|
/** The LOUDEST this agent may ever reach you — a ceiling on `NOTIFY_LADDER`, set by the
|
|
@@ -1514,34 +1675,40 @@ var ConnectionSummarySchema = z12.object({
|
|
|
1514
1675
|
* every surface at once and outranks even `sessionMode: all_calls` — a mode the user
|
|
1515
1676
|
* set once must not overrule a rule they set about one agent. */
|
|
1516
1677
|
reach: NotifyLevelSchema.nullable().optional(),
|
|
1517
|
-
createdAt:
|
|
1678
|
+
createdAt: z14.string().datetime(),
|
|
1518
1679
|
/** Most recent notification on this connection, either direction. Null = no contact yet.
|
|
1519
1680
|
* Drives the agents-page recency grouping (Today / This week / …). */
|
|
1520
|
-
lastContactAt:
|
|
1681
|
+
lastContactAt: z14.string().datetime().nullable(),
|
|
1521
1682
|
/** Last presence heartbeat from a running agent process (POST /api/presence) — the
|
|
1522
1683
|
* desktop app while open. Null = never seen; stale = offline. */
|
|
1523
|
-
lastSeenAt:
|
|
1684
|
+
lastSeenAt: z14.string().datetime().nullable().optional(),
|
|
1524
1685
|
/** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
|
|
1525
1686
|
* its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
|
|
1526
1687
|
* `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
|
|
1527
|
-
lastWorkedAt:
|
|
1688
|
+
lastWorkedAt: z14.string().datetime().nullable().optional(),
|
|
1689
|
+
/** A TERMINAL SESSION'S ONLY PULSE (`tokens.listened_at`): `paigy-listen` stamps it on every sweep
|
|
1690
|
+
* (90 s). It is deliberately NOT presence — `last_seen_at` is the desktop host's beat, and a
|
|
1691
|
+
* listener orphaned from a dead conversation must not light the live dot — but it is the one fact
|
|
1692
|
+
* that says a terminal session's process is UP, which is why the wake already reads it
|
|
1693
|
+
* (`wake.ts`, `listener`). Null = no listener sweeping. */
|
|
1694
|
+
listenedAt: z14.string().datetime().nullable().optional(),
|
|
1528
1695
|
/** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
|
|
1529
1696
|
* With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
|
|
1530
|
-
oldestReadyAt:
|
|
1697
|
+
oldestReadyAt: z14.string().datetime().nullable().optional(),
|
|
1531
1698
|
/** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
|
|
1532
1699
|
* harness availabilities + granted workspaces — the option set the phone's
|
|
1533
1700
|
* "new session" sheet offers. Absent for ordinary MCP agents. */
|
|
1534
|
-
runtime:
|
|
1701
|
+
runtime: z14.object({
|
|
1535
1702
|
/** The @paigy/harness this host is running — a machine the self-update has not reached
|
|
1536
1703
|
* shows its age here (`apps/desktop/src/update.ts`). */
|
|
1537
|
-
version:
|
|
1538
|
-
harnesses:
|
|
1539
|
-
workspaces:
|
|
1704
|
+
version: z14.string().optional(),
|
|
1705
|
+
harnesses: z14.array(z14.object({ name: z14.string(), label: z14.string(), status: z14.string(), models: z14.array(z14.string()).optional() })).optional(),
|
|
1706
|
+
workspaces: z14.array(z14.string()).optional(),
|
|
1540
1707
|
/** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
|
|
1541
1708
|
* repo, and each repo directly inside one, with its `origin` remote. A session started for
|
|
1542
1709
|
* work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
|
|
1543
1710
|
* own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
|
|
1544
|
-
repos:
|
|
1711
|
+
repos: z14.array(z14.object({ path: z14.string(), remote: z14.string() })).optional()
|
|
1545
1712
|
}).optional(),
|
|
1546
1713
|
/** The tail of this agent's working log, when a harness is driving it — the agent page's
|
|
1547
1714
|
* live strip. Absent for anything the desktop harness isn't running (a hatched identity
|
|
@@ -1550,151 +1717,151 @@ var ConnectionSummarySchema = z12.object({
|
|
|
1550
1717
|
activity: AgentActivitySchema.optional(),
|
|
1551
1718
|
/** True = a provider-managed agent running in the provider's cloud (e.g. Anthropic CMA);
|
|
1552
1719
|
* false = a local MCP connection running on the user's computer (Claude Code/Codex/…). */
|
|
1553
|
-
managed:
|
|
1720
|
+
managed: z14.boolean()
|
|
1554
1721
|
});
|
|
1555
|
-
var LedgerItemSchema =
|
|
1556
|
-
var AgentLedgerSchema =
|
|
1722
|
+
var LedgerItemSchema = z14.object({ id: z14.string(), parentId: z14.string(), title: z14.string(), createdAt: z14.string() });
|
|
1723
|
+
var AgentLedgerSchema = z14.object({
|
|
1557
1724
|
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
1558
|
-
agent:
|
|
1725
|
+
agent: z14.object({ id: z14.string(), name: z14.string().nullable(), revokedAt: z14.string().nullable() }),
|
|
1559
1726
|
/** Its own questions you have not answered. */
|
|
1560
|
-
asks:
|
|
1727
|
+
asks: z14.array(LedgerItemSchema),
|
|
1561
1728
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
1562
|
-
answered:
|
|
1729
|
+
answered: z14.array(LedgerItemSchema),
|
|
1563
1730
|
/** Requests you sent it that it never took. */
|
|
1564
|
-
requests:
|
|
1565
|
-
goals:
|
|
1566
|
-
callbacks:
|
|
1731
|
+
requests: z14.array(LedgerItemSchema),
|
|
1732
|
+
goals: z14.array(z14.object({ id: z14.string(), outcome: z14.string(), state: z14.string() })),
|
|
1733
|
+
callbacks: z14.array(z14.object({ id: z14.string(), parentId: z14.string(), trigger: z14.string(), note: z14.string(), dueAt: z14.string().nullable() }))
|
|
1567
1734
|
});
|
|
1568
|
-
var ReassignResultSchema =
|
|
1569
|
-
moved:
|
|
1570
|
-
parentId:
|
|
1735
|
+
var ReassignResultSchema = z14.object({
|
|
1736
|
+
moved: z14.object({ asks: z14.number(), answered: z14.number(), requests: z14.number(), goals: z14.number(), callbacks: z14.number() }),
|
|
1737
|
+
parentId: z14.string().nullable()
|
|
1571
1738
|
});
|
|
1572
|
-
var LessonStateSchema =
|
|
1573
|
-
var LessonViewSchema =
|
|
1574
|
-
id:
|
|
1575
|
-
text:
|
|
1739
|
+
var LessonStateSchema = z14.enum(["active", "proposed", "retired"]);
|
|
1740
|
+
var LessonViewSchema = z14.object({
|
|
1741
|
+
id: z14.string(),
|
|
1742
|
+
text: z14.string(),
|
|
1576
1743
|
state: LessonStateSchema,
|
|
1577
1744
|
/** The Goal it is scoped to; null = the whole account. */
|
|
1578
|
-
scopeGoalId:
|
|
1579
|
-
goalTitle:
|
|
1580
|
-
version:
|
|
1581
|
-
pinned:
|
|
1745
|
+
scopeGoalId: z14.string().nullable(),
|
|
1746
|
+
goalTitle: z14.string().nullable(),
|
|
1747
|
+
version: z14.number(),
|
|
1748
|
+
pinned: z14.boolean(),
|
|
1582
1749
|
/** When the person last wrote its text themselves. */
|
|
1583
|
-
editedAt:
|
|
1584
|
-
createdAt:
|
|
1585
|
-
updatedAt:
|
|
1750
|
+
editedAt: z14.string().nullable(),
|
|
1751
|
+
createdAt: z14.string(),
|
|
1752
|
+
updatedAt: z14.string(),
|
|
1586
1753
|
/** The Entries it came from, oldest first; `words` is null when an Entry has none to show (sealed). */
|
|
1587
|
-
sources:
|
|
1754
|
+
sources: z14.array(z14.object({ entryId: z14.string(), words: z14.string().nullable(), at: z14.string() }))
|
|
1588
1755
|
});
|
|
1589
|
-
var QueueQuestionSchema =
|
|
1756
|
+
var QueueQuestionSchema = z14.object({
|
|
1590
1757
|
/** The decision need's id — what an answer is accepted against. */
|
|
1591
|
-
id:
|
|
1758
|
+
id: z14.string(),
|
|
1592
1759
|
/** The words that were asked, from the request Entry that asked them. */
|
|
1593
|
-
question:
|
|
1760
|
+
question: z14.string(),
|
|
1594
1761
|
/** Where it was asked — which is where the ruling goes (`POST /api/entries`). Null only
|
|
1595
1762
|
* for a need whose request Entry is carried by no interactive Delivery, which nothing
|
|
1596
1763
|
* can answer. */
|
|
1597
|
-
deliveryId:
|
|
1764
|
+
deliveryId: z14.string().nullable().default(null),
|
|
1598
1765
|
/** The Entry the ruling is about. */
|
|
1599
|
-
aboutId:
|
|
1766
|
+
aboutId: z14.string().nullable().default(null),
|
|
1600
1767
|
/** Empty for a free-text question. */
|
|
1601
|
-
options:
|
|
1602
|
-
select:
|
|
1603
|
-
askedAt:
|
|
1768
|
+
options: z14.array(OptionSchema).default([]),
|
|
1769
|
+
select: z14.enum(["one", "many", "rank", "confirm", "text"]).default("text"),
|
|
1770
|
+
askedAt: z14.string(),
|
|
1604
1771
|
/** Null while the question is open — which is how the page tells the two apart. */
|
|
1605
|
-
answeredAt:
|
|
1772
|
+
answeredAt: z14.string().nullable().default(null),
|
|
1606
1773
|
/** The ruling in the person's own words, from the contribution that replied — not the
|
|
1607
1774
|
* option id, which is not something anyone reads back. Null while it is open, and null
|
|
1608
1775
|
* for a settled question whose reply carried nothing readable. */
|
|
1609
|
-
answer:
|
|
1776
|
+
answer: z14.string().nullable().default(null),
|
|
1610
1777
|
/** The Goal this question belongs to — a step knows its Goal on its own, not only through
|
|
1611
1778
|
* an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
|
|
1612
1779
|
* READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
|
|
1613
1780
|
* order, its route, home's trees and the list of steps all take a step's Goal from here, so
|
|
1614
1781
|
* this is the field they agree through rather than each re-deriving it from the row it
|
|
1615
1782
|
* arrived under. Required because the API projects it on every need it sends. */
|
|
1616
|
-
goalId:
|
|
1783
|
+
goalId: z14.string(),
|
|
1617
1784
|
/** True only while an unmet START gate holds the Goal — a Goal that merely waits to
|
|
1618
1785
|
* *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
|
|
1619
1786
|
* the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
|
|
1620
1787
|
* gate at all. */
|
|
1621
|
-
blocked:
|
|
1788
|
+
blocked: z14.boolean().default(false)
|
|
1622
1789
|
});
|
|
1623
|
-
var QueueReplySchema =
|
|
1790
|
+
var QueueReplySchema = z14.object({
|
|
1624
1791
|
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
1625
1792
|
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
1626
1793
|
* walk can name it in its zoom. */
|
|
1627
|
-
id:
|
|
1794
|
+
id: z14.string(),
|
|
1628
1795
|
/** The Goal the note is on. */
|
|
1629
|
-
goalId:
|
|
1796
|
+
goalId: z14.string(),
|
|
1630
1797
|
/** What the note said. */
|
|
1631
|
-
note:
|
|
1798
|
+
note: z14.string(),
|
|
1632
1799
|
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
1633
|
-
deliveryId:
|
|
1634
|
-
requestEntryId:
|
|
1635
|
-
askedAt:
|
|
1800
|
+
deliveryId: z14.string(),
|
|
1801
|
+
requestEntryId: z14.string(),
|
|
1802
|
+
askedAt: z14.string(),
|
|
1636
1803
|
/** When the person last replied — the window's start. */
|
|
1637
|
-
repliedAt:
|
|
1804
|
+
repliedAt: z14.string(),
|
|
1638
1805
|
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
1639
|
-
reply:
|
|
1806
|
+
reply: z14.string().nullable()
|
|
1640
1807
|
});
|
|
1641
|
-
var QueueItemSchema =
|
|
1642
|
-
id:
|
|
1808
|
+
var QueueItemSchema = z14.object({
|
|
1809
|
+
id: z14.string(),
|
|
1643
1810
|
/** One-line headline — the first sentence of the outcome. */
|
|
1644
|
-
title:
|
|
1811
|
+
title: z14.string(),
|
|
1645
1812
|
/** The outcome in full, verbatim: the person's own words are what an assignee sees. */
|
|
1646
|
-
intent:
|
|
1813
|
+
intent: z14.string(),
|
|
1647
1814
|
/** `ready` | `active` | `waiting` | `done` | `cancelled`, straight off the Goal. */
|
|
1648
|
-
state:
|
|
1815
|
+
state: z14.string(),
|
|
1649
1816
|
/** Who holds it (a participant ref); null when nobody does yet. */
|
|
1650
|
-
assignee:
|
|
1817
|
+
assignee: z14.string().nullable().default(null),
|
|
1651
1818
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
1652
|
-
progress:
|
|
1819
|
+
progress: z14.string().nullable().default(null),
|
|
1653
1820
|
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
1654
1821
|
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
1655
|
-
progressLine:
|
|
1656
|
-
reviewPending:
|
|
1657
|
-
dueAt:
|
|
1822
|
+
progressLine: z14.string().nullable().optional(),
|
|
1823
|
+
reviewPending: z14.boolean().default(false),
|
|
1824
|
+
dueAt: z14.string().nullable().default(null),
|
|
1658
1825
|
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
1659
1826
|
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
1660
|
-
finishedAt:
|
|
1827
|
+
finishedAt: z14.string().nullable().optional(),
|
|
1661
1828
|
/** The Goal this one was opened under; null at the root. */
|
|
1662
|
-
parentGoalId:
|
|
1829
|
+
parentGoalId: z14.string().nullable().default(null),
|
|
1663
1830
|
/** Goals opened under this one — only those the same list holds. */
|
|
1664
|
-
childGoalIds:
|
|
1831
|
+
childGoalIds: z14.array(z14.string()).default([]),
|
|
1665
1832
|
/** Goals this one waits on (start or finish gates). */
|
|
1666
|
-
dependencyGoalIds:
|
|
1833
|
+
dependencyGoalIds: z14.array(z14.string()).default([]),
|
|
1667
1834
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
1668
|
-
blocked:
|
|
1835
|
+
blocked: z14.boolean().default(false),
|
|
1669
1836
|
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
1670
1837
|
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
1671
1838
|
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
1672
1839
|
* question is opened. Never a CANCELLED one (withdrawn, dismissed, or its Goal or call ended): it
|
|
1673
1840
|
* asks nothing of anyone (#3723). */
|
|
1674
|
-
questions:
|
|
1841
|
+
questions: z14.array(QueueQuestionSchema).default([]),
|
|
1675
1842
|
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
1676
1843
|
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
1677
1844
|
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
1678
1845
|
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. A cancelled
|
|
1679
1846
|
* question is not asked (#3725). */
|
|
1680
|
-
asked:
|
|
1681
|
-
answered:
|
|
1847
|
+
asked: z14.number().optional(),
|
|
1848
|
+
answered: z14.number().optional(),
|
|
1682
1849
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
1683
1850
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
1684
1851
|
* not spell an empty list. */
|
|
1685
|
-
replies:
|
|
1852
|
+
replies: z14.array(QueueReplySchema).optional(),
|
|
1686
1853
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
1687
|
-
repo:
|
|
1688
|
-
createdAt:
|
|
1689
|
-
updatedAt:
|
|
1854
|
+
repo: z14.string().nullable().optional(),
|
|
1855
|
+
createdAt: z14.string(),
|
|
1856
|
+
updatedAt: z14.string().nullable().default(null),
|
|
1690
1857
|
/** When its owner last SAID something about it (the newest `progress` Entry: a contact update kept
|
|
1691
1858
|
* as progress). `updatedAt` moves for reasons nobody chose — a
|
|
1692
1859
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
1693
|
-
lastProgressAt:
|
|
1860
|
+
lastProgressAt: z14.string().nullable().optional(),
|
|
1694
1861
|
/** WHEN AN AGENT LAST ACTED ON IT (owner, 2026-10-27: "Move it now", Goal 567258dc): its newest agent write, an
|
|
1695
1862
|
* Entry or a Goal change, Paigy's excluded (`goals.worked_at`). What keeps work an agent is writing on from
|
|
1696
1863
|
* reading as gone quiet (`coldSince`); absent on a door that does not carry it. */
|
|
1697
|
-
workedAt:
|
|
1864
|
+
workedAt: z14.string().nullable().optional(),
|
|
1698
1865
|
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
1699
1866
|
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
1700
1867
|
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
@@ -1713,7 +1880,7 @@ var QueueItemSchema = z12.object({
|
|
|
1713
1880
|
* minute after the person speaks erases their instant from it, and the durable traces the client
|
|
1714
1881
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
1715
1882
|
* Entry with no `about_id` is in neither. */
|
|
1716
|
-
lastPersonAt:
|
|
1883
|
+
lastPersonAt: z14.string().nullable().optional(),
|
|
1717
1884
|
/** EACH SIDE'S NEWEST WORD IN THE LAST DAY (owner, 2026-10-13, the Work tab's order): the person's
|
|
1718
1885
|
* (`yours`) and the agents' (`theirs`), each null when that side wrote nothing on it in 24 hours.
|
|
1719
1886
|
* `latest` holds one Entry, so whichever side wrote first is hidden there; Work orders a Goal by
|
|
@@ -1731,40 +1898,40 @@ var QueueItemSchema = z12.object({
|
|
|
1731
1898
|
* the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
|
|
1732
1899
|
* sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
|
|
1733
1900
|
* queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
|
|
1734
|
-
rev:
|
|
1901
|
+
rev: z14.string().optional()
|
|
1735
1902
|
});
|
|
1736
|
-
var QueueDeltaSchema =
|
|
1737
|
-
ids:
|
|
1738
|
-
items:
|
|
1903
|
+
var QueueDeltaSchema = z14.object({
|
|
1904
|
+
ids: z14.array(z14.string()),
|
|
1905
|
+
items: z14.array(QueueItemSchema)
|
|
1739
1906
|
});
|
|
1740
1907
|
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
1741
|
-
var NoteSourceSchema =
|
|
1742
|
-
var NoteStatusSchema =
|
|
1743
|
-
var NoteRepeatSchema =
|
|
1744
|
-
var DecisionSchema =
|
|
1745
|
-
id:
|
|
1908
|
+
var NoteSourceSchema = z14.enum(["app", "call"]);
|
|
1909
|
+
var NoteStatusSchema = z14.enum(["open", "assigned", "in_progress", "done"]);
|
|
1910
|
+
var NoteRepeatSchema = z14.enum(["once", "until_done"]);
|
|
1911
|
+
var DecisionSchema = z14.object({
|
|
1912
|
+
id: z14.string(),
|
|
1746
1913
|
/** The note this decision refines; null = recorded on a bare thread (the
|
|
1747
1914
|
* extensibility seam — any conversation can accrue decisions). */
|
|
1748
|
-
noteId:
|
|
1915
|
+
noteId: z14.string().nullable(),
|
|
1749
1916
|
/** What was ambiguous — the broker's (or the user's own) question. */
|
|
1750
|
-
question:
|
|
1917
|
+
question: z14.string(),
|
|
1751
1918
|
/** The user's ruling; null while the question is open. */
|
|
1752
|
-
answer:
|
|
1753
|
-
decidedAt:
|
|
1754
|
-
createdAt:
|
|
1919
|
+
answer: z14.string().nullable(),
|
|
1920
|
+
decidedAt: z14.string().nullable(),
|
|
1921
|
+
createdAt: z14.string()
|
|
1755
1922
|
});
|
|
1756
|
-
var NoteSchema =
|
|
1757
|
-
id:
|
|
1923
|
+
var NoteSchema = z14.object({
|
|
1924
|
+
id: z14.string(),
|
|
1758
1925
|
/** One-line headline (broker-titled; deterministic floor). */
|
|
1759
|
-
title:
|
|
1926
|
+
title: z14.string(),
|
|
1760
1927
|
/** The original intent, verbatim — assignees always see the user's own words. */
|
|
1761
|
-
intent:
|
|
1928
|
+
intent: z14.string(),
|
|
1762
1929
|
source: NoteSourceSchema,
|
|
1763
1930
|
status: NoteStatusSchema,
|
|
1764
1931
|
/** Who it was assigned to (a participant ref, 'agent:<tokenId>'); null = unassigned. */
|
|
1765
|
-
assignee:
|
|
1932
|
+
assignee: z14.string().nullable(),
|
|
1766
1933
|
/** The request thread minted at assignment; null until assigned. */
|
|
1767
|
-
parentId:
|
|
1934
|
+
parentId: z14.string().nullable(),
|
|
1768
1935
|
/** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
|
|
1769
1936
|
* call — never a deadline. It only ever comes from the user's own words, so when it
|
|
1770
1937
|
* passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
|
|
@@ -1773,100 +1940,100 @@ var NoteSchema = z12.object({
|
|
|
1773
1940
|
// Defaulted, not required: a Note from an API deploy older than the reminders
|
|
1774
1941
|
// migration has none of these, and the defaults ARE what it means — no not-before,
|
|
1775
1942
|
// one ride, never ridden. Parsing must not fail across a rolling deploy.
|
|
1776
|
-
dueAt:
|
|
1943
|
+
dueAt: z14.string().nullable().default(null),
|
|
1777
1944
|
repeat: NoteRepeatSchema.default("once"),
|
|
1778
1945
|
/** How many calls have already carried it — the fatigue cap counts rides, not days. */
|
|
1779
|
-
rides:
|
|
1780
|
-
lastRideAt:
|
|
1781
|
-
createdAt:
|
|
1946
|
+
rides: z14.number().int().default(0),
|
|
1947
|
+
lastRideAt: z14.string().nullable().default(null),
|
|
1948
|
+
createdAt: z14.string()
|
|
1782
1949
|
});
|
|
1783
|
-
var DeliveryModeSchema =
|
|
1784
|
-
var RegisterDeliverySchema =
|
|
1785
|
-
var OAuthStartSchema =
|
|
1786
|
-
provider:
|
|
1787
|
-
returnTo:
|
|
1950
|
+
var DeliveryModeSchema = z14.enum(["poll", "self_hosted"]);
|
|
1951
|
+
var RegisterDeliverySchema = z14.object({ mode: DeliveryModeSchema });
|
|
1952
|
+
var OAuthStartSchema = z14.object({
|
|
1953
|
+
provider: z14.enum(["cma"]),
|
|
1954
|
+
returnTo: z14.string().min(1)
|
|
1788
1955
|
});
|
|
1789
|
-
var DeliveryConfigSchema =
|
|
1790
|
-
tokenId:
|
|
1956
|
+
var DeliveryConfigSchema = z14.object({
|
|
1957
|
+
tokenId: z14.string(),
|
|
1791
1958
|
mode: DeliveryModeSchema,
|
|
1792
1959
|
/** null when the deployment has no anon key configured. `self_hosted` is then REFUSED
|
|
1793
1960
|
* (503 `self_hosted_unavailable`) rather than registered, so a self_hosted config always
|
|
1794
1961
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
1795
|
-
realtime:
|
|
1962
|
+
realtime: z14.object({ url: z14.string(), anonKey: z14.string() }).nullable()
|
|
1796
1963
|
});
|
|
1797
|
-
var HostDecisionSchema =
|
|
1964
|
+
var HostDecisionSchema = z14.object({
|
|
1798
1965
|
/** The agent's token id: the row's `recipient`. */
|
|
1799
|
-
agent:
|
|
1800
|
-
decision:
|
|
1966
|
+
agent: z14.string().uuid(),
|
|
1967
|
+
decision: z14.enum(["stood_back", "took_over"]),
|
|
1801
1968
|
/** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
|
|
1802
|
-
goalId:
|
|
1969
|
+
goalId: z14.string().uuid().nullable().optional(),
|
|
1803
1970
|
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
1804
|
-
seenAt:
|
|
1971
|
+
seenAt: z14.string().datetime().nullable().optional(),
|
|
1805
1972
|
/** When that work last moved (`claimable.since` on an agent's `contact({})` read), the fact the bound is judged on. */
|
|
1806
|
-
since:
|
|
1973
|
+
since: z14.string().datetime().nullable().optional(),
|
|
1807
1974
|
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
1808
|
-
said:
|
|
1975
|
+
said: z14.string().max(300).optional()
|
|
1809
1976
|
});
|
|
1810
|
-
var WakeNudgeSchema =
|
|
1811
|
-
kind:
|
|
1812
|
-
notificationId:
|
|
1813
|
-
parentId:
|
|
1977
|
+
var WakeNudgeSchema = z14.object({
|
|
1978
|
+
kind: z14.enum(["reply", "request", "callback"]),
|
|
1979
|
+
notificationId: z14.string().optional(),
|
|
1980
|
+
parentId: z14.string()
|
|
1814
1981
|
});
|
|
1815
|
-
var PairingStatusSchema =
|
|
1816
|
-
var DeviceCodeSchema =
|
|
1817
|
-
device_code:
|
|
1818
|
-
user_code:
|
|
1819
|
-
verification_uri:
|
|
1820
|
-
verification_uri_complete:
|
|
1821
|
-
interval:
|
|
1822
|
-
expires_in:
|
|
1982
|
+
var PairingStatusSchema = z14.enum(["pending", "approved", "denied", "expired"]);
|
|
1983
|
+
var DeviceCodeSchema = z14.object({
|
|
1984
|
+
device_code: z14.string(),
|
|
1985
|
+
user_code: z14.string(),
|
|
1986
|
+
verification_uri: z14.string().url(),
|
|
1987
|
+
verification_uri_complete: z14.string().url(),
|
|
1988
|
+
interval: z14.number(),
|
|
1989
|
+
expires_in: z14.number()
|
|
1823
1990
|
});
|
|
1824
|
-
var DeviceInfoSchema =
|
|
1825
|
-
code:
|
|
1991
|
+
var DeviceInfoSchema = z14.object({
|
|
1992
|
+
code: z14.string(),
|
|
1826
1993
|
/** The agent's suggested name (from /device/code) — shown on the approval screen,
|
|
1827
1994
|
* pre-filling the name field the human can edit. */
|
|
1828
|
-
name:
|
|
1995
|
+
name: z14.string(),
|
|
1829
1996
|
/** @deprecated Legacy alias of `name` for the pre-#531 embedded bundle in App Store
|
|
1830
1997
|
* build 35, whose DeviceFlow renders `info.agent.slice(0, 2)` — without this a FRESH
|
|
1831
1998
|
* install crashes on the pairing screen on first launch, before the OTA lands
|
|
1832
1999
|
* (seen live: PAIGY-5T, 2026-07-21). Remove once a newer binary is the floor. */
|
|
1833
|
-
agent:
|
|
1834
|
-
device:
|
|
2000
|
+
agent: z14.string().optional(),
|
|
2001
|
+
device: z14.string().nullable(),
|
|
1835
2002
|
status: PairingStatusSchema
|
|
1836
2003
|
});
|
|
1837
|
-
var DeviceTokenSchema =
|
|
1838
|
-
access_token:
|
|
2004
|
+
var DeviceTokenSchema = z14.object({
|
|
2005
|
+
access_token: z14.string(),
|
|
1839
2006
|
/** The pairing's single name (user-typed at approval, the agent's suggestion, or
|
|
1840
2007
|
* a default silly name). */
|
|
1841
|
-
name:
|
|
1842
|
-
device:
|
|
2008
|
+
name: z14.string(),
|
|
2009
|
+
device: z14.string().nullable(),
|
|
1843
2010
|
/** The pairing's assigned voice, cached so the desktop can seed the SAME face the phone
|
|
1844
2011
|
* draws — voice is the third ingredient of a hatchling's build (party/traits.ts). */
|
|
1845
|
-
voice:
|
|
2012
|
+
voice: z14.string().nullable().optional(),
|
|
1846
2013
|
/** The token's server-side id — the face's COLOUR anchor, and the only seed ingredient
|
|
1847
2014
|
* that survives a rename. Cached by the host's identity beat. */
|
|
1848
|
-
token_id:
|
|
2015
|
+
token_id: z14.string().nullable().optional(),
|
|
1849
2016
|
/** WHERE this identity works — the folder a wake should land it in. Written by the host
|
|
1850
2017
|
* at spawn and by `paigy-harness handoff` from a live terminal. Without it every wake
|
|
1851
2018
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
1852
2019
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
1853
|
-
workspace:
|
|
2020
|
+
workspace: z14.string().nullable().optional(),
|
|
1854
2021
|
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
1855
|
-
harness:
|
|
1856
|
-
session_id:
|
|
2022
|
+
harness: z14.enum(["claude", "codex", "agy", "grok"]).optional(),
|
|
2023
|
+
session_id: z14.string().uuid().optional(),
|
|
1857
2024
|
/** A conversation the host must not resume: its context is full, so every turn fails
|
|
1858
2025
|
* ("Prompt is too long"). Written when a run hits it (`run.ts` `onFull`); the host skips a slot
|
|
1859
2026
|
* whose resumable session is this one, so its Goals reach the dead-agent handoff instead of a
|
|
1860
2027
|
* copy that types the person's words into a turn that cannot run (Calls, 2026-10-06). */
|
|
1861
|
-
full_session:
|
|
1862
|
-
uik_pub:
|
|
2028
|
+
full_session: z14.string().optional(),
|
|
2029
|
+
uik_pub: z14.string().nullable().optional()
|
|
1863
2030
|
});
|
|
1864
|
-
var SupportRequestSchema =
|
|
1865
|
-
email:
|
|
1866
|
-
message:
|
|
1867
|
-
name:
|
|
2031
|
+
var SupportRequestSchema = z14.object({
|
|
2032
|
+
email: z14.string().email().max(320),
|
|
2033
|
+
message: z14.string().trim().min(1).max(5e3),
|
|
2034
|
+
name: z14.string().trim().max(120).optional()
|
|
1868
2035
|
});
|
|
1869
|
-
var NotificationFeedbackKindSchema =
|
|
2036
|
+
var NotificationFeedbackKindSchema = z14.enum([
|
|
1870
2037
|
"break_down",
|
|
1871
2038
|
// "This should be more than one ask — break it down."
|
|
1872
2039
|
"regenerate_options",
|
|
@@ -1881,136 +2048,133 @@ var NotificationFeedbackKindSchema = z12.enum([
|
|
|
1881
2048
|
// anything else — the note carries it.
|
|
1882
2049
|
]);
|
|
1883
2050
|
var SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
1884
|
-
var QuestionRowSchema =
|
|
2051
|
+
var QuestionRowSchema = z14.object({
|
|
1885
2052
|
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
1886
|
-
id:
|
|
1887
|
-
deliveryId:
|
|
1888
|
-
entryId:
|
|
2053
|
+
id: z14.string(),
|
|
2054
|
+
deliveryId: z14.string(),
|
|
2055
|
+
entryId: z14.string(),
|
|
1889
2056
|
/** The decision it waits on; null for an update, which asks nothing. */
|
|
1890
|
-
needId:
|
|
1891
|
-
goalIds:
|
|
2057
|
+
needId: z14.string().nullable(),
|
|
2058
|
+
goalIds: z14.array(z14.string()),
|
|
1892
2059
|
/** The name of the work it is about, when the read could word it. */
|
|
1893
|
-
goalTitle:
|
|
1894
|
-
tokenId:
|
|
1895
|
-
name:
|
|
1896
|
-
title:
|
|
1897
|
-
body:
|
|
1898
|
-
select:
|
|
1899
|
-
options:
|
|
1900
|
-
hasPreview:
|
|
1901
|
-
blocking:
|
|
1902
|
-
askedAt:
|
|
2060
|
+
goalTitle: z14.string().optional(),
|
|
2061
|
+
tokenId: z14.string().optional(),
|
|
2062
|
+
name: z14.string(),
|
|
2063
|
+
title: z14.string(),
|
|
2064
|
+
body: z14.string(),
|
|
2065
|
+
select: z14.enum(["one", "many", "rank", "confirm", "text"]),
|
|
2066
|
+
options: z14.array(SlimOptionSchema),
|
|
2067
|
+
hasPreview: z14.boolean(),
|
|
2068
|
+
blocking: z14.boolean(),
|
|
2069
|
+
askedAt: z14.string().datetime(),
|
|
1903
2070
|
ring: InboxItemSchema.shape.ring,
|
|
1904
|
-
onCall:
|
|
1905
|
-
sealed:
|
|
2071
|
+
onCall: z14.literal(true).optional(),
|
|
2072
|
+
sealed: z14.boolean()
|
|
1906
2073
|
});
|
|
1907
|
-
var WorkStateSchema =
|
|
1908
|
-
var WorkRowSchema =
|
|
1909
|
-
id:
|
|
1910
|
-
parentId:
|
|
1911
|
-
title:
|
|
2074
|
+
var WorkStateSchema = z14.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
2075
|
+
var WorkRowSchema = z14.object({
|
|
2076
|
+
id: z14.string(),
|
|
2077
|
+
parentId: z14.string().nullable(),
|
|
2078
|
+
title: z14.string(),
|
|
1912
2079
|
/** Straight off the Goal. */
|
|
1913
2080
|
state: WorkStateSchema,
|
|
1914
|
-
owner:
|
|
1915
|
-
revision:
|
|
2081
|
+
owner: z14.string().nullable(),
|
|
2082
|
+
revision: z14.number().int(),
|
|
1916
2083
|
/** Open questions on it, counted to 100. */
|
|
1917
|
-
waiting:
|
|
2084
|
+
waiting: z14.number().int(),
|
|
1918
2085
|
/** Held by a gate on work that is not done. */
|
|
1919
|
-
blocked:
|
|
1920
|
-
lastProgressAt:
|
|
2086
|
+
blocked: z14.boolean(),
|
|
2087
|
+
lastProgressAt: z14.string().datetime().nullable(),
|
|
1921
2088
|
/** The line written for its newest progress note, else that note's first words. */
|
|
1922
|
-
line:
|
|
2089
|
+
line: z14.string().nullable(),
|
|
1923
2090
|
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
1924
|
-
children:
|
|
1925
|
-
createdAt:
|
|
1926
|
-
updatedAt:
|
|
2091
|
+
children: z14.number().int(),
|
|
2092
|
+
createdAt: z14.string().datetime(),
|
|
2093
|
+
updatedAt: z14.string().datetime(),
|
|
1927
2094
|
/** When anything at or under it last moved — the order the list is in. */
|
|
1928
|
-
activeAt:
|
|
2095
|
+
activeAt: z14.string().datetime(),
|
|
1929
2096
|
/** A sealed outcome has no title here; the work's page opens it. */
|
|
1930
|
-
sealed:
|
|
2097
|
+
sealed: z14.boolean()
|
|
1931
2098
|
});
|
|
1932
2099
|
var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
1933
2100
|
var AgentRowSchema = ComputerRowSchema.extend({
|
|
1934
2101
|
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
1935
|
-
asking:
|
|
1936
|
-
oldestAskAt:
|
|
2102
|
+
asking: z14.number().int().nullable(),
|
|
2103
|
+
oldestAskAt: z14.string().datetime().nullable(),
|
|
1937
2104
|
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
1938
2105
|
* many in all among the account's 200 most recently active agent-held live Goals
|
|
1939
2106
|
* (`agent_holds`); null when that read failed. */
|
|
1940
|
-
holds:
|
|
1941
|
-
held:
|
|
1942
|
-
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
1943
|
-
* while none has, or when that read failed. */
|
|
1944
|
-
cold: z12.string().datetime().nullable(),
|
|
2107
|
+
holds: z14.array(z14.object({ id: z14.string(), title: z14.string() })).nullable(),
|
|
2108
|
+
held: z14.number().int().nullable(),
|
|
1945
2109
|
/** The newest line of its working log, and when the harness saw it. */
|
|
1946
|
-
line:
|
|
1947
|
-
lineAt:
|
|
2110
|
+
line: z14.string().nullable(),
|
|
2111
|
+
lineAt: z14.string().datetime().nullable()
|
|
1948
2112
|
});
|
|
1949
|
-
var SnapshotSchema =
|
|
2113
|
+
var SnapshotSchema = z14.object({
|
|
1950
2114
|
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
1951
|
-
at:
|
|
1952
|
-
questions:
|
|
2115
|
+
at: z14.string().datetime(),
|
|
2116
|
+
questions: z14.object({
|
|
1953
2117
|
/** The newest 30 open cards, questions before updates. */
|
|
1954
|
-
items:
|
|
2118
|
+
items: z14.array(QuestionRowSchema),
|
|
1955
2119
|
/** Every open question, and apart from them every update, and what was put off. */
|
|
1956
|
-
total:
|
|
1957
|
-
updates:
|
|
1958
|
-
putOff:
|
|
2120
|
+
total: z14.number().int(),
|
|
2121
|
+
updates: z14.number().int(),
|
|
2122
|
+
putOff: z14.number().int()
|
|
1959
2123
|
}).nullable(),
|
|
1960
|
-
agents:
|
|
2124
|
+
agents: z14.object({
|
|
1961
2125
|
/** Up to 60, most recently seen first. */
|
|
1962
|
-
items:
|
|
1963
|
-
more:
|
|
2126
|
+
items: z14.array(AgentRowSchema),
|
|
2127
|
+
more: z14.boolean()
|
|
1964
2128
|
}).nullable(),
|
|
1965
|
-
work:
|
|
2129
|
+
work: z14.object({
|
|
1966
2130
|
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
1967
|
-
items:
|
|
2131
|
+
items: z14.array(WorkRowSchema),
|
|
1968
2132
|
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
1969
2133
|
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
1970
2134
|
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
1971
2135
|
* is `ready`; Done is `done` or `cancelled`. */
|
|
1972
|
-
counts:
|
|
2136
|
+
counts: z14.object({ live: z14.number().int(), waiting: z14.number().int(), notStarted: z14.number().int(), done: z14.number().int() })
|
|
1973
2137
|
}).nullable(),
|
|
1974
|
-
you:
|
|
2138
|
+
you: z14.object({
|
|
1975
2139
|
settings: UserSettingsSchema,
|
|
1976
|
-
callable:
|
|
2140
|
+
callable: z14.boolean(),
|
|
1977
2141
|
/** Up to 20 paired computers; null when the roster read failed. */
|
|
1978
|
-
computers:
|
|
2142
|
+
computers: z14.array(ComputerRowSchema).nullable()
|
|
1979
2143
|
}).nullable()
|
|
1980
2144
|
});
|
|
1981
|
-
var CallRecapSchema =
|
|
1982
|
-
call:
|
|
1983
|
-
status:
|
|
1984
|
-
startedAt:
|
|
1985
|
-
durationMs:
|
|
2145
|
+
var CallRecapSchema = z14.object({
|
|
2146
|
+
call: z14.object({
|
|
2147
|
+
status: z14.string(),
|
|
2148
|
+
startedAt: z14.string(),
|
|
2149
|
+
durationMs: z14.number().nullable(),
|
|
1986
2150
|
/** Bot-clock pickup offset; absent before the timestamp release, null before its report lands. */
|
|
1987
|
-
pickupMs:
|
|
2151
|
+
pickupMs: z14.number().nullable().optional(),
|
|
1988
2152
|
/** STILL FILING (20261026182117): the call has not ended on the server, or it has and a brain cycle on it is
|
|
1989
2153
|
* still pending, the next writer, which may still settle, file or start something. What the close itself
|
|
1990
2154
|
* waits for (`private.close_call`). `status` cannot say it: nothing writes "finalizing", and a call reads
|
|
1991
2155
|
* "open" until the sweep closes it, after nothing is pending. Absent from an API before it. */
|
|
1992
|
-
filing:
|
|
1993
|
-
agents:
|
|
2156
|
+
filing: z14.boolean().optional(),
|
|
2157
|
+
agents: z14.array(z14.object({ id: z14.string(), name: z14.string().nullable() }))
|
|
1994
2158
|
}),
|
|
1995
|
-
topics:
|
|
1996
|
-
goalId:
|
|
1997
|
-
title:
|
|
1998
|
-
owner:
|
|
1999
|
-
state:
|
|
2159
|
+
topics: z14.array(z14.object({
|
|
2160
|
+
goalId: z14.string().uuid(),
|
|
2161
|
+
title: z14.string(),
|
|
2162
|
+
owner: z14.string(),
|
|
2163
|
+
state: z14.string(),
|
|
2000
2164
|
/** Presented Entry content and its author, separately from the current Goal owner. */
|
|
2001
|
-
items:
|
|
2165
|
+
items: z14.array(z14.object({ entryId: z14.string(), author: z14.string(), text: z14.string(), summary: z14.string().nullable(), acknowledged: z14.boolean().optional() })).optional(),
|
|
2002
2166
|
/** Whether the call put it to the person. An item it carried and never said has no row on the
|
|
2003
2167
|
* review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
|
|
2004
|
-
presented:
|
|
2168
|
+
presented: z14.boolean(),
|
|
2005
2169
|
/** Whether THIS call created it — the third way a call touches work, beside carrying a request
|
|
2006
2170
|
* there and filing words there (owner, 2026-10-08, Goal `5b4b1a0a`: "Show the new work, keep my
|
|
2007
2171
|
* words under it"). Created work is never carried, so it is never `presented`; without this the
|
|
2008
2172
|
* row is dropped by the "only what happened on the call" filter. False on a database before
|
|
2009
2173
|
* 20261013080133. */
|
|
2010
|
-
created:
|
|
2174
|
+
created: z14.boolean(),
|
|
2011
2175
|
/** `request` is the Entry that asked it — what a line's `about` names when the line answered or put
|
|
2012
2176
|
* it off. Optional: an older API sent none. */
|
|
2013
|
-
questions:
|
|
2177
|
+
questions: z14.array(z14.object({ id: z14.string().uuid(), state: z14.string(), title: z14.string(), request: z14.string().optional() })),
|
|
2014
2178
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
2015
2179
|
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
2016
2180
|
* gave, so the row scans like a chosen option and their own words stay under it. Absent when
|
|
@@ -2020,34 +2184,34 @@ var CallRecapSchema = z12.object({
|
|
|
2020
2184
|
/** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
|
|
2021
2185
|
* however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
|
|
2022
2186
|
* said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
|
|
2023
|
-
lines:
|
|
2024
|
-
entryId:
|
|
2025
|
-
words:
|
|
2026
|
-
summary:
|
|
2027
|
-
about:
|
|
2028
|
-
reply:
|
|
2029
|
-
replyKind:
|
|
2030
|
-
at:
|
|
2187
|
+
lines: z14.array(z14.object({
|
|
2188
|
+
entryId: z14.string().uuid(),
|
|
2189
|
+
words: z14.string(),
|
|
2190
|
+
summary: z14.string().optional(),
|
|
2191
|
+
about: z14.string().nullable().optional(),
|
|
2192
|
+
reply: z14.string().nullable().optional(),
|
|
2193
|
+
replyKind: z14.string().nullable().optional(),
|
|
2194
|
+
at: z14.array(z14.number()).optional()
|
|
2031
2195
|
}))
|
|
2032
2196
|
})),
|
|
2033
|
-
unfiled:
|
|
2197
|
+
unfiled: z14.array(z14.object({ lineId: z14.string().uuid(), words: z14.string(), atMs: z14.number() })),
|
|
2034
2198
|
/** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
|
|
2035
2199
|
* (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
|
|
2036
2200
|
* it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
|
|
2037
2201
|
* person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
|
|
2038
2202
|
* theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
|
|
2039
2203
|
* BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
|
|
2040
|
-
said:
|
|
2041
|
-
lineId:
|
|
2042
|
-
speaker:
|
|
2043
|
-
agent:
|
|
2044
|
-
words:
|
|
2045
|
-
atMs:
|
|
2204
|
+
said: z14.array(z14.object({
|
|
2205
|
+
lineId: z14.string().uuid(),
|
|
2206
|
+
speaker: z14.enum(["user", "agent"]),
|
|
2207
|
+
agent: z14.object({ id: z14.string().nullable(), name: z14.string().nullable() }).nullable(),
|
|
2208
|
+
words: z14.string(),
|
|
2209
|
+
atMs: z14.number()
|
|
2046
2210
|
})),
|
|
2047
2211
|
/** What did not fit, counted. `settles` counts items past the recap's 100; absent before 20261026165518. */
|
|
2048
|
-
more:
|
|
2212
|
+
more: z14.object({ lines: z14.number(), entries: z14.number(), topics: z14.number(), settles: z14.number().optional() }),
|
|
2049
2213
|
/** Last stored position on this page when another exists; absent before paging shipped. */
|
|
2050
|
-
next:
|
|
2214
|
+
next: z14.number().int().nonnegative().nullable().optional(),
|
|
2051
2215
|
/** THE CALL AS ONE LIST OF ROWS (owner, 2026-10-14: "we only really want a thing like transcript but
|
|
2052
2216
|
* built from the summaries", then "the transcript tabs should show the same but with full words").
|
|
2053
2217
|
* Both tabs draw these rows, in this order: Summary from `summary`, Transcript from `words`.
|
|
@@ -2058,32 +2222,32 @@ var CallRecapSchema = z12.object({
|
|
|
2058
2222
|
* `by` is the row's real author, never inferred from `kind`: a relay Paigy sent to an agent is
|
|
2059
2223
|
* Paigy's, not the person's. Optional and additive (owner: "make sure whatever you do is backward
|
|
2060
2224
|
* compatible"): an API before it sends none, and the screen falls back to `topics` and `said`. */
|
|
2061
|
-
timeline:
|
|
2062
|
-
id:
|
|
2063
|
-
position:
|
|
2064
|
-
atMs:
|
|
2065
|
-
kind:
|
|
2066
|
-
by:
|
|
2067
|
-
goalId:
|
|
2068
|
-
requestId:
|
|
2069
|
-
summary:
|
|
2070
|
-
words:
|
|
2225
|
+
timeline: z14.array(z14.object({
|
|
2226
|
+
id: z14.string(),
|
|
2227
|
+
position: z14.number(),
|
|
2228
|
+
atMs: z14.number(),
|
|
2229
|
+
kind: z14.enum(["put", "said", "did"]),
|
|
2230
|
+
by: z14.object({ participant: z14.string().nullable(), agentId: z14.string().nullable(), name: z14.string().nullable() }),
|
|
2231
|
+
goalId: z14.string().uuid().nullable(),
|
|
2232
|
+
requestId: z14.string().nullable(),
|
|
2233
|
+
summary: z14.string().nullable(),
|
|
2234
|
+
words: z14.string(),
|
|
2071
2235
|
/** Paigy's sentences folded into this row: on a `put` row, what it said putting the item to the
|
|
2072
2236
|
* person (before their next row); on a `said` row, what it said in reply (until their next row).
|
|
2073
2237
|
* `atMs` is when each was said; optional, absent before 2026-10-14's fix. */
|
|
2074
|
-
spoken:
|
|
2238
|
+
spoken: z14.array(z14.object({ lineId: z14.string(), words: z14.string(), atMs: z14.number().optional() })),
|
|
2075
2239
|
/** On a `said` row: the work these words were filed on, and whether they answered (recorded) a
|
|
2076
2240
|
* question. They replace the "Filed on ..." and "Recorded: ..." rows that repeated the person's own
|
|
2077
2241
|
* summary (Brain, 2026-10-14). Optional: absent from an API before it. */
|
|
2078
|
-
filedOn:
|
|
2079
|
-
recorded:
|
|
2080
|
-
settled:
|
|
2081
|
-
acknowledged:
|
|
2242
|
+
filedOn: z14.array(z14.object({ goalId: z14.string().uuid(), title: z14.string() })).optional(),
|
|
2243
|
+
recorded: z14.boolean().optional(),
|
|
2244
|
+
settled: z14.object({ requestId: z14.string(), answer: z14.string() }).nullable().optional(),
|
|
2245
|
+
acknowledged: z14.boolean().optional(),
|
|
2082
2246
|
/** On a `put` row: still waiting for the person's answer (not settled, not answered, not put off).
|
|
2083
2247
|
* The server's judgement; the screen draws "Still waiting for your answer" from it. Optional: absent
|
|
2084
2248
|
* from an API before it, where the screen falls back to its own reading. */
|
|
2085
|
-
waiting:
|
|
2086
|
-
lineIds:
|
|
2249
|
+
waiting: z14.boolean().optional(),
|
|
2250
|
+
lineIds: z14.array(z14.string())
|
|
2087
2251
|
})).optional(),
|
|
2088
2252
|
/** THE RECAP (owner, 2026-10-26: "the transcript should just be the entries that settled or
|
|
2089
2253
|
* acknowledged the items; that's it"). Each item the person's words settled on this call, in the
|
|
@@ -2095,42 +2259,42 @@ var CallRecapSchema = z12.object({
|
|
|
2095
2259
|
* with `requestId` null and the Goal's name as `title`; a started Goal may have no `lines` yet. `kind`
|
|
2096
2260
|
* is a string, not an enum, so a later kind never fails an older build's parse; `requestId` is the
|
|
2097
2261
|
* request the words settled. On the first page only; absent from an API before it. */
|
|
2098
|
-
settles:
|
|
2099
|
-
requestId:
|
|
2100
|
-
kind:
|
|
2101
|
-
by:
|
|
2102
|
-
goalId:
|
|
2103
|
-
title:
|
|
2104
|
-
lines:
|
|
2262
|
+
settles: z14.array(z14.object({
|
|
2263
|
+
requestId: z14.string().nullable(),
|
|
2264
|
+
kind: z14.string(),
|
|
2265
|
+
by: z14.object({ participant: z14.string(), agentId: z14.string().nullable(), name: z14.string() }),
|
|
2266
|
+
goalId: z14.string().uuid().nullable(),
|
|
2267
|
+
title: z14.string(),
|
|
2268
|
+
lines: z14.array(z14.object({ lineId: z14.string(), words: z14.string(), atMs: z14.number() }))
|
|
2105
2269
|
})).optional()
|
|
2106
2270
|
});
|
|
2107
2271
|
|
|
2108
2272
|
// src/listening.ts
|
|
2109
2273
|
import { createHash } from "crypto";
|
|
2110
|
-
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2274
|
+
import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "fs";
|
|
2111
2275
|
import { homedir } from "os";
|
|
2112
2276
|
import { dirname, join } from "path";
|
|
2113
2277
|
var HARNESS_MARKS = ["PAIGY_HARNESS"];
|
|
2114
2278
|
function harnessDriven(env) {
|
|
2115
2279
|
return HARNESS_MARKS.some((mark) => !!env[mark]);
|
|
2116
2280
|
}
|
|
2281
|
+
var LISTEN_LINES = "Each line it prints is something new addressed to you (a Delivery, an answer to your question, new words on your Goal, a Goal assigned to you): read the Goal it names with get_goals, and answer with contact.";
|
|
2282
|
+
var HOOK_SERIAL = 2;
|
|
2117
2283
|
function sessionStartHook() {
|
|
2118
2284
|
return `#!/bin/sh
|
|
2119
|
-
# Paigy \u2014 written by \`paigy-enable-tools
|
|
2285
|
+
# Paigy \u2014 written by \`paigy-enable-tools\` (hook v${HOOK_SERIAL}). Prints one line into a starting session.
|
|
2120
2286
|
# Never edits, never calls the network, and exits 0 whatever happens: a hook that fails
|
|
2121
2287
|
# is a session that fails to start.
|
|
2122
2288
|
[ -n "${HARNESS_MARKS.map((m) => `\${${m}}`).join("")}" ] && exit 0 # harness-spawned (either mark): its pump already delivers
|
|
2123
2289
|
[ -f "\${HOME}/.paigy/token.json" ] || exit 0 # not paired on this machine: nothing to listen as
|
|
2124
2290
|
cat <<'PAIGY'
|
|
2125
2291
|
Paigy: nothing pushes answers into a session started in a terminal \u2014 an answer waits for your
|
|
2126
|
-
next contact({ wait: false }). Start your own listener now with
|
|
2127
|
-
|
|
2128
|
-
only tells you when it exits). When the monitor expires, start it again:
|
|
2292
|
+
next contact({ wait: false }). Start your own listener now with ${LISTEN_MONITOR}.
|
|
2293
|
+
When the monitor expires, start it again:
|
|
2129
2294
|
|
|
2130
|
-
|
|
2295
|
+
${LISTEN_COMMAND}
|
|
2131
2296
|
|
|
2132
|
-
|
|
2133
|
-
the answer needs a decision.
|
|
2297
|
+
${LISTEN_LINES}
|
|
2134
2298
|
|
|
2135
2299
|
ONE LISTENER PER SESSION, AND ANOTHER SESSION'S IS NOT YOURS. Identities here are per session, so
|
|
2136
2300
|
a paigy-listen you find in ps is almost certainly another session listening as itself, and
|
|
@@ -2142,6 +2306,7 @@ PAIGY
|
|
|
2142
2306
|
exit 0
|
|
2143
2307
|
`;
|
|
2144
2308
|
}
|
|
2309
|
+
var GROK_STALLED_HOOK = "npx -y -p @paigy/mcp@latest paigy-stalled";
|
|
2145
2310
|
function withSessionStartHook(existing, command) {
|
|
2146
2311
|
let config = {};
|
|
2147
2312
|
if (existing) {
|
|
@@ -2157,6 +2322,16 @@ function withSessionStartHook(existing, command) {
|
|
|
2157
2322
|
if (!already) starts.push({ hooks: [{ type: "command", command }] });
|
|
2158
2323
|
return JSON.stringify(config, null, 2);
|
|
2159
2324
|
}
|
|
2325
|
+
function withGrokPaigyHooks(existing, sessionStart) {
|
|
2326
|
+
const withStart = withSessionStartHook(existing, sessionStart);
|
|
2327
|
+
if (withStart === null) return null;
|
|
2328
|
+
const config = JSON.parse(withStart);
|
|
2329
|
+
const hooks = config.hooks ??= {};
|
|
2330
|
+
const stops = hooks.Stop ??= [];
|
|
2331
|
+
const already = stops.some((group) => (group.hooks ?? []).some((h) => (h.command ?? "").includes("paigy-stalled")));
|
|
2332
|
+
if (!already) stops.push({ hooks: [{ type: "command", command: GROK_STALLED_HOOK, timeout: 60 }] });
|
|
2333
|
+
return JSON.stringify(config, null, 2);
|
|
2334
|
+
}
|
|
2160
2335
|
var word = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
2161
2336
|
function decideListen(f) {
|
|
2162
2337
|
if (harnessDriven(f.env)) {
|
|
@@ -2173,7 +2348,7 @@ function decideListen(f) {
|
|
|
2173
2348
|
return {
|
|
2174
2349
|
status: "start",
|
|
2175
2350
|
command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${f.env.CODEX_THREAD_ID ? `CODEX_THREAD_ID=${word(f.env.CODEX_THREAD_ID)} ` : ""}${f.env.PAIGY_ON_WAKE ? `PAIGY_ON_WAKE=${word(f.env.PAIGY_ON_WAKE)} ` : ""}${word(f.execPath)} ${word(f.listenJs)} --brief`,
|
|
2176
|
-
how:
|
|
2351
|
+
how: `Run this command NOW with ${LISTEN_MONITOR}; start it again when the monitor expires. ${LISTEN_LINES} Start it once.`
|
|
2177
2352
|
};
|
|
2178
2353
|
}
|
|
2179
2354
|
function listenMark(slot, home = homedir()) {
|
|
@@ -2244,9 +2419,9 @@ function unlistenedNote(f, _now) {
|
|
|
2244
2419
|
case "no":
|
|
2245
2420
|
return { unlistened: `${head}, and this host cannot run a listener. Work reaches you only as you act \u2014 it rides your tool calls, as it did this one \u2014 so call contact({ wait: false }) before you stop.` };
|
|
2246
2421
|
case "yes":
|
|
2247
|
-
return { unlistened: `${head}. Start the listener below
|
|
2422
|
+
return { unlistened: `${head}. Start the listener below with ${LISTEN_MONITOR}; a listener you see in ps belongs to another session's slot, never to yours.`, listen_command: decision.command };
|
|
2248
2423
|
default:
|
|
2249
|
-
return { unlistened: `${head}. If this host can run
|
|
2424
|
+
return { unlistened: `${head}. If this host can run one, start the listener below with ${LISTEN_MONITOR}; if it cannot, work reaches you only as you act, so call contact({ wait: false }) before you stop.`, listen_command: decision.command };
|
|
2250
2425
|
}
|
|
2251
2426
|
}
|
|
2252
2427
|
var ReachTeller = class {
|
|
@@ -2281,27 +2456,74 @@ function withReach(shaped, reach) {
|
|
|
2281
2456
|
if (!reach || !shaped || typeof shaped !== "object" || Array.isArray(shaped)) return shaped;
|
|
2282
2457
|
return { ...reach, ...shaped };
|
|
2283
2458
|
}
|
|
2284
|
-
var HOOKED_HARNESSES = "Claude Code and
|
|
2459
|
+
var HOOKED_HARNESSES = "Claude Code, Codex, and Grok";
|
|
2285
2460
|
function listenBlock(decision, f, stale) {
|
|
2286
2461
|
const block = {
|
|
2287
2462
|
listen: decision,
|
|
2288
2463
|
listen_permanent: `\`${ENABLE_LISTEN_COMMAND}\` installs the SessionStart hook for ${HOOKED_HARNESSES}, so every future session there is told this at its start. Other hosts are not hooked; their sessions learn it as they act.`
|
|
2289
2464
|
};
|
|
2290
2465
|
if (decision.status === "start") {
|
|
2291
|
-
block.listen_prompt = canWatch(f) === "no" ? "This host cannot run a listener, so nothing reaches this session while it rests. Work arrives with your tool calls; call contact({ wait: false }) before you stop." : "Start your own listener NOW, as described in `listen` \u2014
|
|
2466
|
+
block.listen_prompt = canWatch(f) === "no" ? "This host cannot run a listener, so nothing reaches this session while it rests. Work arrives with your tool calls; call contact({ wait: false }) before you stop." : "Start your own listener NOW, as described in `listen` \u2014 under your harness's Monitor tool, restarted whenever the monitor expires. Without it, every answer waits until you call contact({ wait: false }).";
|
|
2292
2467
|
}
|
|
2293
2468
|
if (stale) {
|
|
2294
|
-
block.hook_refresh = `The SessionStart hook on this machine is an older text. Ask the user to re-run \`${ENABLE_LISTEN_COMMAND}\` to refresh it \u2014
|
|
2469
|
+
block.hook_refresh = `The SessionStart hook on this machine is an older text this server could not refresh. Ask the user to re-run \`${ENABLE_LISTEN_COMMAND}\` to refresh it \u2014 an old one tells a session to skip its listener, or to run it where it never wakes the session.`;
|
|
2295
2470
|
}
|
|
2296
2471
|
return block;
|
|
2297
2472
|
}
|
|
2298
2473
|
var ENABLE_LISTEN_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
2474
|
+
var hookScript = (home = homedir()) => join(home, ".paigy", "hooks", "session-start.sh");
|
|
2299
2475
|
function hookStale(home = homedir()) {
|
|
2476
|
+
let text;
|
|
2300
2477
|
try {
|
|
2301
|
-
|
|
2478
|
+
text = readFileSync(hookScript(home), "utf8");
|
|
2302
2479
|
} catch {
|
|
2303
2480
|
return false;
|
|
2304
2481
|
}
|
|
2482
|
+
if (text === sessionStartHook()) return false;
|
|
2483
|
+
return !(text.startsWith(OURS) && serialOf(text) >= HOOK_SERIAL);
|
|
2484
|
+
}
|
|
2485
|
+
var OURS = "#!/bin/sh\n# Paigy \u2014 written by ";
|
|
2486
|
+
function serialOf(text) {
|
|
2487
|
+
const m = /\(hook v(\d+)\)/.exec(text.split("\n", 2)[1] ?? "");
|
|
2488
|
+
return m ? Number(m[1]) : 1;
|
|
2489
|
+
}
|
|
2490
|
+
function refreshSessionHook(home = homedir()) {
|
|
2491
|
+
const path = hookScript(home);
|
|
2492
|
+
let text;
|
|
2493
|
+
try {
|
|
2494
|
+
text = readFileSync(path, "utf8");
|
|
2495
|
+
} catch {
|
|
2496
|
+
return "absent";
|
|
2497
|
+
}
|
|
2498
|
+
const current = sessionStartHook();
|
|
2499
|
+
if (text === current) return "current";
|
|
2500
|
+
if (!text.startsWith(OURS)) return "foreign";
|
|
2501
|
+
if (serialOf(text) >= HOOK_SERIAL) return "newer";
|
|
2502
|
+
let real;
|
|
2503
|
+
let next = "";
|
|
2504
|
+
try {
|
|
2505
|
+
real = realpathSync(path);
|
|
2506
|
+
const mode = statSync(real).mode & 511;
|
|
2507
|
+
next = `${real}.${process.pid}.tmp`;
|
|
2508
|
+
const fd = openSync(next, "w", 384);
|
|
2509
|
+
try {
|
|
2510
|
+
writeSync(fd, current);
|
|
2511
|
+
fsyncSync(fd);
|
|
2512
|
+
} finally {
|
|
2513
|
+
closeSync(fd);
|
|
2514
|
+
}
|
|
2515
|
+
renameSync(next, real);
|
|
2516
|
+
chmodSync(real, mode);
|
|
2517
|
+
return "refreshed";
|
|
2518
|
+
} catch {
|
|
2519
|
+
if (next) {
|
|
2520
|
+
try {
|
|
2521
|
+
rmSync(next, { force: true });
|
|
2522
|
+
} catch {
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
return "failed";
|
|
2526
|
+
}
|
|
2305
2527
|
}
|
|
2306
2528
|
function waitingSummary(received) {
|
|
2307
2529
|
const r = received ?? {};
|
|
@@ -2322,13 +2544,19 @@ function waitingSummary(received) {
|
|
|
2322
2544
|
|
|
2323
2545
|
export {
|
|
2324
2546
|
mcpInputSchema,
|
|
2547
|
+
AGENT_PROMPTS,
|
|
2548
|
+
promptResult,
|
|
2549
|
+
READS,
|
|
2550
|
+
SENDS,
|
|
2325
2551
|
AGENT_TOOLS,
|
|
2552
|
+
LISTEN_COMMAND,
|
|
2326
2553
|
serverInstructions,
|
|
2327
2554
|
entryWords,
|
|
2328
2555
|
entryUnit,
|
|
2329
2556
|
harnessDriven,
|
|
2330
2557
|
sessionStartHook,
|
|
2331
2558
|
withSessionStartHook,
|
|
2559
|
+
withGrokPaigyHooks,
|
|
2332
2560
|
decideListen,
|
|
2333
2561
|
writeListenMark,
|
|
2334
2562
|
removeListenMark,
|
|
@@ -2339,5 +2567,6 @@ export {
|
|
|
2339
2567
|
withReach,
|
|
2340
2568
|
listenBlock,
|
|
2341
2569
|
ENABLE_LISTEN_COMMAND,
|
|
2342
|
-
hookStale
|
|
2570
|
+
hookStale,
|
|
2571
|
+
refreshSessionHook
|
|
2343
2572
|
};
|