@paigy/harness 0.3.36 → 0.3.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +419 -73
  2. package/dist/main.js +414 -72
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -8680,10 +8680,47 @@ function claudeConversation() {
8680
8680
  if (!envSession("CLAUDE_CODE_SESSION_ID") || process.env.CLAUDE_CODE_CHILD_SESSION) return void 0;
8681
8681
  const dir = envSession("CLAUDE_CONFIG_DIR") ?? join(homedir(), ".claude");
8682
8682
  for (const pid of ancestors()) {
8683
+ let text;
8683
8684
  try {
8684
- const { sessionId: sessionId2 } = JSON.parse(readFileSync(join(dir, "sessions", `${pid}.json`), "utf8"));
8685
- return typeof sessionId2 === "string" && sessionId2.trim() ? sessionId2 : void 0;
8685
+ text = readFileSync(join(dir, "sessions", `${pid}.json`), "utf8");
8686
8686
  } catch {
8687
+ continue;
8688
+ }
8689
+ const record = parseSessionRecord(text);
8690
+ if (!record) continue;
8691
+ return typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId : void 0;
8692
+ }
8693
+ return void 0;
8694
+ }
8695
+ function parseSessionRecord(text) {
8696
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
8697
+ try {
8698
+ return object(JSON.parse(text));
8699
+ } catch {
8700
+ }
8701
+ let depth = 0;
8702
+ let start = -1;
8703
+ let quoted = false;
8704
+ let escaped = false;
8705
+ for (let i = 0; i < text.length; i += 1) {
8706
+ const c = text[i];
8707
+ if (quoted) {
8708
+ if (escaped) escaped = false;
8709
+ else if (c === "\\") escaped = true;
8710
+ else if (c === '"') quoted = false;
8711
+ } else if (c === '"') quoted = true;
8712
+ else if (c === "{") {
8713
+ if (depth === 0) start = i;
8714
+ depth += 1;
8715
+ } else if (c === "}" && depth > 0) {
8716
+ depth -= 1;
8717
+ if (depth === 0) {
8718
+ try {
8719
+ return object(JSON.parse(text.slice(start, i + 1)));
8720
+ } catch {
8721
+ return void 0;
8722
+ }
8723
+ }
8687
8724
  }
8688
8725
  }
8689
8726
  return void 0;
@@ -9923,10 +9960,12 @@ var zodToJsonSchema = (schema, options) => {
9923
9960
  };
9924
9961
  var OPTIONS_MIN = 2;
9925
9962
  var OPTIONS_MAX = 6;
9963
+ var OPTION_LABEL_MAX = 50;
9964
+ var OPTION_HINT_MAX = 200;
9926
9965
  var OptionSchema = external_exports.object({
9927
9966
  id: external_exports.string(),
9928
9967
  label: external_exports.string(),
9929
- hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
9968
+ hint: external_exports.string().max(500).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
9930
9969
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
9931
9970
  // the constraints below are what an agent reads when deciding to use these.
9932
9971
  html: external_exports.string().max(16384).describe(
@@ -9940,8 +9979,10 @@ var OptionInputSchema = external_exports.object({
9940
9979
  id: external_exports.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
9941
9980
  `Optional: this option's own id, which answers name it by (selectedOptionIds). Give every option one, or none: without them the options are numbered "1", "2", \u2026 in order.`
9942
9981
  ),
9943
- label: external_exports.string().trim().min(1).max(1e3),
9944
- hint: OptionSchema.shape.hint,
9982
+ // SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
9983
+ // a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
9984
+ label: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX, `option_label_too_long: an option's label is at most ${OPTION_LABEL_MAX} characters; say the choice, and put its consequence in the hint.`).describe(`The choice, at most ${OPTION_LABEL_MAX} characters, standing on its own.`),
9985
+ hint: external_exports.string().max(OPTION_HINT_MAX, `option_hint_too_long: an option's hint is at most ${OPTION_HINT_MAX} characters.`).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
9945
9986
  htmlPreview: OptionSchema.shape.html,
9946
9987
  imgUrl: OptionSchema.shape.image
9947
9988
  }).strict();
@@ -10043,15 +10084,48 @@ var UpdateInputSchema = external_exports.object({
10043
10084
  message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
10044
10085
  goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
10045
10086
  units,
10087
+ // A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
10088
+ // until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
10089
+ // still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
10090
+ // tap lands as a contribution, the person's own words on the Goal, because there is no Question
10091
+ // for it to answer.
10092
+ //
10093
+ // AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
10094
+ // acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
10095
+ // it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
10096
+ // work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
10097
+ // call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
10098
+ // tested yet*. The words are worth writing because the agent reads them back.
10099
+ //
10100
+ // ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
10101
+ // write up to four of them: "just on the cards, but it should be more like just they should only
10102
+ // get one basically string value that they can enter that replaces got it"). A report has exactly
10103
+ // one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
10104
+ // `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
10105
+ // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
10106
+ // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
10107
+ reply: external_exports.string().trim().min(1).describe(
10108
+ `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. Left out, the button says "Got it", as every card did.`
10109
+ ).optional(),
10046
10110
  userExplicitlyRequested
10047
- }).strict().superRefine((u, ctx) => refuseOverCaps(u.message, u.units, "message", ctx));
10111
+ }).strict().superRefine((u, ctx) => {
10112
+ refuseOverCaps(u.message, u.units, "message", ctx);
10113
+ if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
10114
+ ctx.addIssue({
10115
+ code: external_exports.ZodIssueCode.custom,
10116
+ path: ["reply"],
10117
+ params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX },
10118
+ 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.`
10119
+ });
10120
+ }
10121
+ });
10048
10122
  var questionFields = {
10049
10123
  question: external_exports.string().trim().min(1).describe(
10050
10124
  `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.`
10051
10125
  ),
10052
10126
  goalId: external_exports.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
10053
10127
  units,
10054
- options: external_exports.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
10128
+ options: external_exports.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
10055
10129
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
10056
10130
  // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
10057
10131
  pickMode: external_exports.enum(["one", "many", "rank"]).optional().describe(
@@ -10128,6 +10202,8 @@ QUESTIONS: one question per object; several questions are several objects in one
10128
10202
 
10129
10203
  CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX} characters; everything else it needs goes in 'units', up to ${UNITS_MAX} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX} characters and every body at most ${UNIT_BODY_MAX}. The person sees the main text and the titles, and opens a unit to read its body. 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.
10130
10204
 
10205
+ GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. It is what their button says instead of "Got it", and what they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Nothing is owed on it either way, and a report that writes none offers "Got it".
10206
+
10131
10207
  UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first (manage_goals), then send one update saying what is done and anything they need to do or check. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
10132
10208
 
10133
10209
  RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
@@ -10171,7 +10247,8 @@ var UpdateGoalSchema = external_exports.object({
10171
10247
  }).strict();
10172
10248
  var changedGoal = external_exports.string().uuid().describe("The Goal to change, as a read shows it.");
10173
10249
  var goalTitle = external_exports.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.");
10174
- var goalOutcome = external_exports.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
10250
+ var GOAL_OUTCOME_MAX = 500;
10251
+ var goalOutcome = external_exports.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.`);
10175
10252
  var CreateChange = external_exports.object({
10176
10253
  kind: external_exports.literal("create"),
10177
10254
  title: goalTitle,
@@ -10197,7 +10274,7 @@ var DependencyChange = external_exports.object({
10197
10274
  goalId: changedGoal,
10198
10275
  dependsOnGoalId: external_exports.string().uuid().describe("The Goal it waits on."),
10199
10276
  action: external_exports.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
10200
- reason: external_exports.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
10277
+ reason: external_exports.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
10201
10278
  }).strict();
10202
10279
  var read = { revision: external_exports.number().int().positive().optional() };
10203
10280
  var GoalChangeSchema = external_exports.discriminatedUnion("kind", [
@@ -10221,7 +10298,7 @@ var ManageGoalsSchema = external_exports.object({
10221
10298
  var ManageGoalsToolSchema = external_exports.object({
10222
10299
  changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
10223
10300
  }).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
10224
- 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: 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; 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_goal) 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.";
10301
+ 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: 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_goal) 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.";
10225
10302
  var GetGoalSchema = external_exports.object({
10226
10303
  goalId: external_exports.string().uuid(),
10227
10304
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -10239,7 +10316,7 @@ var SearchToolSchema = external_exports.object({
10239
10316
  goalId: external_exports.string().uuid().optional(),
10240
10317
  limit: external_exports.number().int().min(1).max(20).optional()
10241
10318
  }).strict();
10242
- 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 Answers (found by their Question 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, its Questions' Answers, 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 (an Answer carries its Question, 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.";
10319
+ 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.";
10243
10320
  var CheckActivitySchema = external_exports.object({}).strict();
10244
10321
  var FEEDBACK_TEXT_MAX = 5e4;
10245
10322
  var SendFeedbackSchema = external_exports.object({
@@ -10302,6 +10379,7 @@ var QuestionChangeSchema = external_exports.union([
10302
10379
  external_exports.object({
10303
10380
  kind: external_exports.literal("create"),
10304
10381
  text: external_exports.string().min(1),
10382
+ summary: external_exports.string().optional(),
10305
10383
  answererId: participant,
10306
10384
  goal: ResultRefSchema.optional(),
10307
10385
  blocks: external_exports.array(external_exports.object({ question: ResultRefSchema }).strict()),
@@ -10352,12 +10430,12 @@ var BrainSearchSchema = external_exports.object({
10352
10430
  }).strict();
10353
10431
  var BrainResultSchema = external_exports.object({
10354
10432
  messages: external_exports.array(BrainMessageSchema),
10355
- entries: external_exports.array(external_exports.object({ key, sources: external_exports.array(SourceSchema).min(1), goals: external_exports.array(ResultRefSchema) }).strict()),
10433
+ entries: external_exports.array(external_exports.object({ key, sources: external_exports.array(SourceSchema).min(1), goals: external_exports.array(ResultRefSchema), summary: external_exports.string().optional() }).strict()),
10356
10434
  questions: external_exports.array(external_exports.object({ key, entryKeys: external_exports.array(key), change: QuestionChangeSchema }).strict()),
10357
10435
  answers: external_exports.array(external_exports.object({
10358
10436
  question: ResultRefSchema,
10359
10437
  entryKeys: external_exports.array(key).min(1),
10360
- summary: external_exports.string().min(1),
10438
+ summary: external_exports.string(),
10361
10439
  selectedOptionIds: external_exports.array(external_exports.string().min(1)).optional()
10362
10440
  }).strict()),
10363
10441
  goals: external_exports.array(external_exports.object({ key, entryKeys: external_exports.array(key), change: BrainGoalChangeSchema }).strict()),
@@ -10402,12 +10480,15 @@ var TalkerReplySchema = external_exports.object({
10402
10480
  messages: external_exports.array(external_exports.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
10403
10481
  /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
10404
10482
  then: external_exports.enum(["listen", "hold", "end"]),
10405
- /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
10406
- answers: external_exports.array(external_exports.object({ item: str2, options: strs2, lines: strs2 }).strict()),
10407
- /** The line handles that hold a new instruction from the person. */
10483
+ /** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
10484
+ * IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
10485
+ * "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
10486
+ * end of this call), a question's handle (that question's answer) or an ISO time. */
10487
+ replies: external_exports.array(external_exports.object({ item: str2, options: strs2, lines: strs2, summary: str2, defer: str2 }).strict()),
10488
+ /** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
10408
10489
  instruction: strs2,
10409
- /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
10410
- * reads only these (3e). */
10490
+ /** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
10491
+ * work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
10411
10492
  file: strs2,
10412
10493
  /** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
10413
10494
  need: external_exports.object({ kind: external_exports.enum(["search", "item", "agent", ""]), text: str2 }).strict(),
@@ -10420,8 +10501,9 @@ var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10420
10501
  var str3 = external_exports.string();
10421
10502
  var strs3 = external_exports.array(external_exports.string());
10422
10503
  var FilerReplySchema = external_exports.object({
10423
- /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
10424
- filed: external_exports.array(external_exports.object({ line: str3, goals: strs3 }).strict()),
10504
+ /** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
10505
+ * the line says in at most ten words. */
10506
+ filed: external_exports.array(external_exports.object({ line: str3, goals: strs3, summary: str3 }).strict()),
10425
10507
  goals: external_exports.array(external_exports.object({
10426
10508
  op: external_exports.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
10427
10509
  key: str3,
@@ -10440,6 +10522,7 @@ var FilerReplySchema = external_exports.object({
10440
10522
  key: str3,
10441
10523
  question: str3,
10442
10524
  text: str3,
10525
+ summary: str3,
10443
10526
  to: str3,
10444
10527
  goal: str3,
10445
10528
  options: external_exports.array(external_exports.object({ id: str3, label: str3 }).strict()),
@@ -10453,6 +10536,60 @@ var FilerReplySchema = external_exports.object({
10453
10536
  var FILER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10454
10537
  zodToJsonSchema(FilerReplySchema, { $refStrategy: "none" })
10455
10538
  );
10539
+ var ExplainerReplySchema = external_exports.object({
10540
+ /** The explanation, as sentences, spoken in order. */
10541
+ say: external_exports.array(external_exports.string()),
10542
+ /** Questions to an agent about gaps in what it read: the agent's handle and the question. */
10543
+ followUps: external_exports.array(external_exports.object({ to: external_exports.string(), text: external_exports.string() }).strict())
10544
+ }).strict();
10545
+ var EXPLAINER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10546
+ zodToJsonSchema(ExplainerReplySchema, { $refStrategy: "none" })
10547
+ );
10548
+ var DISTILLER_DEFECTS = [
10549
+ "reasked",
10550
+ // a question put to the person again after they answered it
10551
+ "unsupported",
10552
+ // a fact stated that nothing in the input supports
10553
+ "unsaid",
10554
+ // an agent's answer arrived during the call and was never said
10555
+ "skipped",
10556
+ // an agenda item never put to the person
10557
+ "unsaved",
10558
+ // something the person answered or asked for did not save, was refused or dropped
10559
+ "ignored",
10560
+ // the person asked or said something and was never answered (the critic's ignored_a_question)
10561
+ "promised",
10562
+ // said it did or would do something, and nothing shows it done (the critic's broken_promise)
10563
+ "dropped"
10564
+ // something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
10565
+ ];
10566
+ var DistillerReplySchema = external_exports.object({
10567
+ defects: external_exports.array(external_exports.object({
10568
+ kind: external_exports.enum(DISTILLER_DEFECTS),
10569
+ /** The handles of the lines it shows in. */
10570
+ lines: external_exports.array(external_exports.string()),
10571
+ /** The handle of the record it concerns, or "". */
10572
+ about: external_exports.string(),
10573
+ /** One plain sentence, in words, never handles: it is what the feedback row keeps. */
10574
+ why: external_exports.string()
10575
+ }).strict())
10576
+ }).strict();
10577
+ var DISTILLER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10578
+ zodToJsonSchema(DistillerReplySchema, { $refStrategy: "none" })
10579
+ );
10580
+ var TIDY_AUDIENCES = ["talker", "filer", "agents"];
10581
+ var TidyReplySchema = external_exports.object({
10582
+ lessons: external_exports.array(external_exports.object({
10583
+ lesson: external_exports.string(),
10584
+ op: external_exports.enum(["keep", "revise", "withdraw"]),
10585
+ text: external_exports.string(),
10586
+ for: external_exports.enum([...TIDY_AUDIENCES, ""]),
10587
+ into: external_exports.string()
10588
+ }).strict())
10589
+ }).strict();
10590
+ var TIDY_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10591
+ zodToJsonSchema(TidyReplySchema, { $refStrategy: "none" })
10592
+ );
10456
10593
  function entryUnit(entry) {
10457
10594
  if (entry.kind !== "contribution" || !entry.aboutId) return null;
10458
10595
  const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
@@ -10462,6 +10599,8 @@ function entryUnit(entry) {
10462
10599
  }
10463
10600
  var LIVE_MS = 3 * 6e4;
10464
10601
  var WORKING_MS = 60 * 6e4;
10602
+ var WORKING_TIER_MS = 2 * 60 * 6e4;
10603
+ var RECENT_TIER_MS = 24 * 60 * 6e4;
10465
10604
  var ContextSchema = external_exports.object({
10466
10605
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
10467
10606
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -10806,6 +10945,10 @@ var AgendaTurnSchema = external_exports.object({
10806
10945
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
10807
10946
  * speaks for several agents — the turn is the only place that knows which. */
10808
10947
  agentId: external_exports.string().optional(),
10948
+ /** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
10949
+ * the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
10950
+ * question turn — the talker owns re-asking and the waits, so `question` stays null. */
10951
+ onTable: external_exports.boolean().optional(),
10809
10952
  select: SelectShapeSchema.optional(),
10810
10953
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
10811
10954
  /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
@@ -10880,9 +11023,16 @@ var InboxItemSchema = external_exports.object({
10880
11023
  /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
10881
11024
  * its need offers, exactly as its own card carries them, present only when one of them has a
10882
11025
  * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
10883
- * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
10884
- * says those, and the list stays small (an `html` is up to 16 KB). */
10885
- options: external_exports.array(OptionSchema).optional()
11026
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
11027
+ options: external_exports.array(OptionSchema).optional(),
11028
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
11029
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
11030
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
11031
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
11032
+ * preview layout for any `options`, and must see exactly what it did. */
11033
+ choices: external_exports.array(OptionSchema).optional(),
11034
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
11035
+ select: external_exports.enum(["one", "many", "rank"]).optional()
10886
11036
  })).optional(),
10887
11037
  visuals: external_exports.array(VisualSchema).optional(),
10888
11038
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -11142,7 +11292,7 @@ var ConnectionSummarySchema = external_exports.object({
11142
11292
  * desktop app while open. Null = never seen; stale = offline. */
11143
11293
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
11144
11294
  /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
11145
- * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
11295
+ * its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
11146
11296
  * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
11147
11297
  lastWorkedAt: external_exports.string().datetime().nullable().optional(),
11148
11298
  /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
@@ -11319,7 +11469,11 @@ var QueueItemSchema = external_exports.object({
11319
11469
  from: external_exports.enum(["person", "agent"]),
11320
11470
  said: external_exports.string(),
11321
11471
  at: external_exports.string(),
11322
- entryId: external_exports.string()
11472
+ entryId: external_exports.string(),
11473
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
11474
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
11475
+ * none was. The Work row reads it before `said`. */
11476
+ summary: external_exports.string().optional()
11323
11477
  }).nullable().optional(),
11324
11478
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
11325
11479
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -11340,7 +11494,7 @@ var QueueItemSchema = external_exports.object({
11340
11494
  *
11341
11495
  * IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
11342
11496
  * writes change which field — the comparison is over the bytes the phone is holding, so a fact
11343
- * the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
11497
+ * the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
11344
11498
  * sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
11345
11499
  * queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
11346
11500
  rev: external_exports.string().optional()
@@ -11656,17 +11810,43 @@ var CallRecapSchema = external_exports.object({
11656
11810
  title: external_exports.string(),
11657
11811
  owner: external_exports.string(),
11658
11812
  state: external_exports.string(),
11813
+ /** Whether the call put it to the person. An item it carried and never said has no row on the
11814
+ * review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
11815
+ presented: external_exports.boolean(),
11659
11816
  questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
11660
- /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
11661
- * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
11662
- * summarized like a pre-made option is"), so the row scans like a chosen option and their
11663
- * own words stay under it. Absent on every line stored before the read wrote them, and on
11664
- * anything that is not an answer. */
11817
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
11818
+ * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
11819
+ * gave, so the row scans like a chosen option and their own words stay under it. Absent when
11820
+ * neither wrote one. */
11665
11821
  /** `about` is the request the line answered (its question), null for words that answered none —
11666
11822
  * the key the screen groups on, so one question is one row however many times it was answered. */
11667
- lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
11823
+ /** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
11824
+ * however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
11825
+ * said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
11826
+ lines: external_exports.array(external_exports.object({
11827
+ entryId: external_exports.string().uuid(),
11828
+ words: external_exports.string(),
11829
+ summary: external_exports.string().optional(),
11830
+ about: external_exports.string().nullable().optional(),
11831
+ reply: external_exports.string().nullable().optional(),
11832
+ replyKind: external_exports.string().nullable().optional(),
11833
+ at: external_exports.array(external_exports.number()).optional()
11834
+ }))
11668
11835
  })),
11669
11836
  unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
11837
+ /** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
11838
+ * (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
11839
+ * it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
11840
+ * person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
11841
+ * theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
11842
+ * BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
11843
+ said: external_exports.array(external_exports.object({
11844
+ lineId: external_exports.string().uuid(),
11845
+ speaker: external_exports.enum(["user", "agent"]),
11846
+ agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
11847
+ words: external_exports.string(),
11848
+ atMs: external_exports.number()
11849
+ })),
11670
11850
  more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
11671
11851
  });
11672
11852
  function sessionSlot(sessionId2) {
@@ -12221,7 +12401,8 @@ var CATALOG = [
12221
12401
  authProbe: ["claude", "auth", "status"],
12222
12402
  loginHint: "Run `claude` once in a terminal and sign in.",
12223
12403
  installCli: "curl -fsSL https://claude.ai/install.sh | bash",
12224
- installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
12404
+ installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp",
12405
+ models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
12225
12406
  },
12226
12407
  {
12227
12408
  name: "codex",
@@ -12231,7 +12412,8 @@ var CATALOG = [
12231
12412
  authProbe: ["codex", "login", "status"],
12232
12413
  loginHint: "Run `codex login` to authenticate.",
12233
12414
  installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
12234
- installAdapter: "npm install -g @agentclientprotocol/codex-acp"
12415
+ installAdapter: "npm install -g @agentclientprotocol/codex-acp",
12416
+ models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"]
12235
12417
  },
12236
12418
  {
12237
12419
  name: "agy",
@@ -12241,7 +12423,8 @@ var CATALOG = [
12241
12423
  adapter: "paigy-agy-acp",
12242
12424
  authProbe: ["agy", "help"],
12243
12425
  loginHint: "Install and set up Antigravity (`agy`).",
12244
- installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
12426
+ installCli: "curl -fsSL https://antigravity.dev/install.sh | bash",
12427
+ models: ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-pro-exp"]
12245
12428
  }
12246
12429
  ];
12247
12430
  function probeDirs(home = homedir3()) {
@@ -12277,7 +12460,7 @@ function runProbe(argv) {
12277
12460
  function detect(entry, deps = {}) {
12278
12461
  const find = deps.which ?? which;
12279
12462
  const probe = deps.probe ?? runProbe;
12280
- const base = { name: entry.name, label: entry.label };
12463
+ const base = { name: entry.name, label: entry.label, models: entry.models };
12281
12464
  const cliPath = find(entry.cli);
12282
12465
  if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
12283
12466
  const adapterPath = entry.adapter ? find(entry.adapter) : null;
@@ -12997,10 +13180,14 @@ function localHolderPids(deps = {}) {
12997
13180
  if (!/^\d+\.json$/.test(file)) continue;
12998
13181
  let row;
12999
13182
  try {
13000
- row = JSON.parse(read3(join6(dir, file)));
13183
+ row = parseSessionRecord(read3(join6(dir, file)));
13001
13184
  } catch {
13002
13185
  continue;
13003
13186
  }
13187
+ if (!row) {
13188
+ deps.unreadable?.(file);
13189
+ continue;
13190
+ }
13004
13191
  const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
13005
13192
  if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
13006
13193
  if (!holds(pid, row.procStart, check)) continue;
@@ -13042,10 +13229,14 @@ function localHolders(deps = {}) {
13042
13229
  if (!/^\d+\.json$/.test(file)) continue;
13043
13230
  let row;
13044
13231
  try {
13045
- row = JSON.parse(read3(join6(dir, file)));
13232
+ row = parseSessionRecord(read3(join6(dir, file)));
13046
13233
  } catch {
13047
13234
  continue;
13048
13235
  }
13236
+ if (!row) {
13237
+ deps.unreadable?.(file);
13238
+ continue;
13239
+ }
13049
13240
  const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
13050
13241
  if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
13051
13242
  const slot = sessionSlot(row.sessionId);
@@ -14457,10 +14648,12 @@ var zodToJsonSchema2 = (schema, options) => {
14457
14648
  // ../../packages/schema/dist/index.js
14458
14649
  var OPTIONS_MIN2 = 2;
14459
14650
  var OPTIONS_MAX2 = 6;
14651
+ var OPTION_LABEL_MAX2 = 50;
14652
+ var OPTION_HINT_MAX2 = 200;
14460
14653
  var OptionSchema2 = external_exports.object({
14461
14654
  id: external_exports.string(),
14462
14655
  label: external_exports.string(),
14463
- hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
14656
+ hint: external_exports.string().max(500).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX2} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
14464
14657
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
14465
14658
  // the constraints below are what an agent reads when deciding to use these.
14466
14659
  html: external_exports.string().max(16384).describe(
@@ -14474,8 +14667,10 @@ var OptionInputSchema2 = external_exports.object({
14474
14667
  id: external_exports.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
14475
14668
  `Optional: this option's own id, which answers name it by (selectedOptionIds). Give every option one, or none: without them the options are numbered "1", "2", \u2026 in order.`
14476
14669
  ),
14477
- label: external_exports.string().trim().min(1).max(1e3),
14478
- hint: OptionSchema2.shape.hint,
14670
+ // SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
14671
+ // a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
14672
+ label: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX2, `option_label_too_long: an option's label is at most ${OPTION_LABEL_MAX2} characters; say the choice, and put its consequence in the hint.`).describe(`The choice, at most ${OPTION_LABEL_MAX2} characters, standing on its own.`),
14673
+ hint: external_exports.string().max(OPTION_HINT_MAX2, `option_hint_too_long: an option's hint is at most ${OPTION_HINT_MAX2} characters.`).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX2} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
14479
14674
  htmlPreview: OptionSchema2.shape.html,
14480
14675
  imgUrl: OptionSchema2.shape.image
14481
14676
  }).strict();
@@ -14577,15 +14772,48 @@ var UpdateInputSchema2 = external_exports.object({
14577
14772
  message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX2} characters; the context goes in units.`),
14578
14773
  goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
14579
14774
  units: units2,
14775
+ // A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
14776
+ // until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
14777
+ // still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
14778
+ // tap lands as a contribution, the person's own words on the Goal, because there is no Question
14779
+ // for it to answer.
14780
+ //
14781
+ // AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
14782
+ // acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
14783
+ // it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
14784
+ // work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
14785
+ // call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
14786
+ // tested yet*. The words are worth writing because the agent reads them back.
14787
+ //
14788
+ // ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
14789
+ // write up to four of them: "just on the cards, but it should be more like just they should only
14790
+ // get one basically string value that they can enter that replaces got it"). A report has exactly
14791
+ // one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
14792
+ // `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
14793
+ // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
14794
+ // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
14795
+ reply: external_exports.string().trim().min(1).describe(
14796
+ `What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX2} 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. Left out, the button says "Got it", as every card did.`
14797
+ ).optional(),
14580
14798
  userExplicitlyRequested: userExplicitlyRequested2
14581
- }).strict().superRefine((u, ctx) => refuseOverCaps2(u.message, u.units, "message", ctx));
14799
+ }).strict().superRefine((u, ctx) => {
14800
+ refuseOverCaps2(u.message, u.units, "message", ctx);
14801
+ if (u.reply && u.reply.length > OPTION_LABEL_MAX2) {
14802
+ ctx.addIssue({
14803
+ code: external_exports.ZodIssueCode.custom,
14804
+ path: ["reply"],
14805
+ params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX2 },
14806
+ message: `reply_too_long: the reply is ${u.reply.length} characters; the cap is ${OPTION_LABEL_MAX2}. It is a button's words, so say it in a breath.`
14807
+ });
14808
+ }
14809
+ });
14582
14810
  var questionFields2 = {
14583
14811
  question: external_exports.string().trim().min(1).describe(
14584
14812
  `ONE question, in at most ${ASK_MAX2} 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.`
14585
14813
  ),
14586
14814
  goalId: external_exports.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
14587
14815
  units: units2,
14588
- options: external_exports.array(OptionInputSchema2).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
14816
+ options: external_exports.array(OptionInputSchema2).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
14589
14817
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
14590
14818
  // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
14591
14819
  pickMode: external_exports.enum(["one", "many", "rank"]).optional().describe(
@@ -14662,6 +14890,8 @@ QUESTIONS: one question per object; several questions are several objects in one
14662
14890
 
14663
14891
  CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX2} characters; everything else it needs goes in 'units', up to ${UNITS_MAX2} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX2} characters and every body at most ${UNIT_BODY_MAX2}. The person sees the main text and the titles, and opens a unit to read its body. 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.
14664
14892
 
14893
+ GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. It is what their button says instead of "Got it", and what they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Nothing is owed on it either way, and a report that writes none offers "Got it".
14894
+
14665
14895
  UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first (manage_goals), then send one update saying what is done and anything they need to do or check. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
14666
14896
 
14667
14897
  RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
@@ -14705,7 +14935,8 @@ var UpdateGoalSchema2 = external_exports.object({
14705
14935
  }).strict();
14706
14936
  var changedGoal2 = external_exports.string().uuid().describe("The Goal to change, as a read shows it.");
14707
14937
  var goalTitle2 = external_exports.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.");
14708
- var goalOutcome2 = external_exports.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
14938
+ var GOAL_OUTCOME_MAX2 = 500;
14939
+ var goalOutcome2 = external_exports.string().trim().min(1).max(GOAL_OUTCOME_MAX2, `outcome_too_long: a Goal's outcome is one paragraph, at most ${GOAL_OUTCOME_MAX2} characters; split the rest into child Goals (create with parentGoalId).`).describe(`The desired result, one paragraph of at most ${GOAL_OUTCOME_MAX2} characters. More than that is more work: create child Goals under it (parentGoalId). There is no third description field.`);
14709
14940
  var CreateChange2 = external_exports.object({
14710
14941
  kind: external_exports.literal("create"),
14711
14942
  title: goalTitle2,
@@ -14731,7 +14962,7 @@ var DependencyChange2 = external_exports.object({
14731
14962
  goalId: changedGoal2,
14732
14963
  dependsOnGoalId: external_exports.string().uuid().describe("The Goal it waits on."),
14733
14964
  action: external_exports.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
14734
- reason: external_exports.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
14965
+ reason: external_exports.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
14735
14966
  }).strict();
14736
14967
  var read2 = { revision: external_exports.number().int().positive().optional() };
14737
14968
  var GoalChangeSchema2 = external_exports.discriminatedUnion("kind", [
@@ -14755,7 +14986,7 @@ var ManageGoalsSchema2 = external_exports.object({
14755
14986
  var ManageGoalsToolSchema2 = external_exports.object({
14756
14987
  changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange2, EditChange2, StateChange2, AssignChange2, DeferChange2, MoveChange2, DependencyChange2])).min(1).max(50).describe("The changes, applied in order, each on its own.")
14757
14988
  }).strict().superRefine((v, ctx) => editNames2(v.changes, ctx));
14758
- var MANAGE_GOALS_DESCRIPTION2 = "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: 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; 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_goal) 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.";
14989
+ var MANAGE_GOALS_DESCRIPTION2 = "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: 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_goal) 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.";
14759
14990
  var GetGoalSchema2 = external_exports.object({
14760
14991
  goalId: external_exports.string().uuid(),
14761
14992
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -14773,7 +15004,7 @@ var SearchToolSchema2 = external_exports.object({
14773
15004
  goalId: external_exports.string().uuid().optional(),
14774
15005
  limit: external_exports.number().int().min(1).max(20).optional()
14775
15006
  }).strict();
14776
- var SEARCH_DESCRIPTION2 = "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 Answers (found by their Question 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, its Questions' Answers, 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 (an Answer carries its Question, 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.";
15007
+ var SEARCH_DESCRIPTION2 = "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.";
14777
15008
  var CheckActivitySchema2 = external_exports.object({}).strict();
14778
15009
  var FEEDBACK_TEXT_MAX2 = 5e4;
14779
15010
  var SendFeedbackSchema2 = external_exports.object({
@@ -14836,6 +15067,7 @@ var QuestionChangeSchema2 = external_exports.union([
14836
15067
  external_exports.object({
14837
15068
  kind: external_exports.literal("create"),
14838
15069
  text: external_exports.string().min(1),
15070
+ summary: external_exports.string().optional(),
14839
15071
  answererId: participant2,
14840
15072
  goal: ResultRefSchema2.optional(),
14841
15073
  blocks: external_exports.array(external_exports.object({ question: ResultRefSchema2 }).strict()),
@@ -14886,12 +15118,12 @@ var BrainSearchSchema2 = external_exports.object({
14886
15118
  }).strict();
14887
15119
  var BrainResultSchema2 = external_exports.object({
14888
15120
  messages: external_exports.array(BrainMessageSchema2),
14889
- entries: external_exports.array(external_exports.object({ key: key2, sources: external_exports.array(SourceSchema2).min(1), goals: external_exports.array(ResultRefSchema2) }).strict()),
15121
+ entries: external_exports.array(external_exports.object({ key: key2, sources: external_exports.array(SourceSchema2).min(1), goals: external_exports.array(ResultRefSchema2), summary: external_exports.string().optional() }).strict()),
14890
15122
  questions: external_exports.array(external_exports.object({ key: key2, entryKeys: external_exports.array(key2), change: QuestionChangeSchema2 }).strict()),
14891
15123
  answers: external_exports.array(external_exports.object({
14892
15124
  question: ResultRefSchema2,
14893
15125
  entryKeys: external_exports.array(key2).min(1),
14894
- summary: external_exports.string().min(1),
15126
+ summary: external_exports.string(),
14895
15127
  selectedOptionIds: external_exports.array(external_exports.string().min(1)).optional()
14896
15128
  }).strict()),
14897
15129
  goals: external_exports.array(external_exports.object({ key: key2, entryKeys: external_exports.array(key2), change: BrainGoalChangeSchema2 }).strict()),
@@ -14936,12 +15168,15 @@ var TalkerReplySchema2 = external_exports.object({
14936
15168
  messages: external_exports.array(external_exports.object({ key: str22, to: str22, text: str22, about: strs22, blocks: str22 }).strict()),
14937
15169
  /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
14938
15170
  then: external_exports.enum(["listen", "hold", "end"]),
14939
- /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
14940
- answers: external_exports.array(external_exports.object({ item: str22, options: strs22, lines: strs22 }).strict()),
14941
- /** The line handles that hold a new instruction from the person. */
15171
+ /** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
15172
+ * IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
15173
+ * "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
15174
+ * end of this call), a question's handle (that question's answer) or an ISO time. */
15175
+ replies: external_exports.array(external_exports.object({ item: str22, options: strs22, lines: strs22, summary: str22, defer: str22 }).strict()),
15176
+ /** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
14942
15177
  instruction: strs22,
14943
- /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
14944
- * reads only these (3e). */
15178
+ /** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
15179
+ * work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
14945
15180
  file: strs22,
14946
15181
  /** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
14947
15182
  need: external_exports.object({ kind: external_exports.enum(["search", "item", "agent", ""]), text: str22 }).strict(),
@@ -14954,8 +15189,9 @@ var TALKER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
14954
15189
  var str32 = external_exports.string();
14955
15190
  var strs32 = external_exports.array(external_exports.string());
14956
15191
  var FilerReplySchema2 = external_exports.object({
14957
- /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
14958
- filed: external_exports.array(external_exports.object({ line: str32, goals: strs32 }).strict()),
15192
+ /** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
15193
+ * the line says in at most ten words. */
15194
+ filed: external_exports.array(external_exports.object({ line: str32, goals: strs32, summary: str32 }).strict()),
14959
15195
  goals: external_exports.array(external_exports.object({
14960
15196
  op: external_exports.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
14961
15197
  key: str32,
@@ -14974,6 +15210,7 @@ var FilerReplySchema2 = external_exports.object({
14974
15210
  key: str32,
14975
15211
  question: str32,
14976
15212
  text: str32,
15213
+ summary: str32,
14977
15214
  to: str32,
14978
15215
  goal: str32,
14979
15216
  options: external_exports.array(external_exports.object({ id: str32, label: str32 }).strict()),
@@ -14987,6 +15224,60 @@ var FilerReplySchema2 = external_exports.object({
14987
15224
  var FILER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
14988
15225
  zodToJsonSchema2(FilerReplySchema2, { $refStrategy: "none" })
14989
15226
  );
15227
+ var ExplainerReplySchema2 = external_exports.object({
15228
+ /** The explanation, as sentences, spoken in order. */
15229
+ say: external_exports.array(external_exports.string()),
15230
+ /** Questions to an agent about gaps in what it read: the agent's handle and the question. */
15231
+ followUps: external_exports.array(external_exports.object({ to: external_exports.string(), text: external_exports.string() }).strict())
15232
+ }).strict();
15233
+ var EXPLAINER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
15234
+ zodToJsonSchema2(ExplainerReplySchema2, { $refStrategy: "none" })
15235
+ );
15236
+ var DISTILLER_DEFECTS2 = [
15237
+ "reasked",
15238
+ // a question put to the person again after they answered it
15239
+ "unsupported",
15240
+ // a fact stated that nothing in the input supports
15241
+ "unsaid",
15242
+ // an agent's answer arrived during the call and was never said
15243
+ "skipped",
15244
+ // an agenda item never put to the person
15245
+ "unsaved",
15246
+ // something the person answered or asked for did not save, was refused or dropped
15247
+ "ignored",
15248
+ // the person asked or said something and was never answered (the critic's ignored_a_question)
15249
+ "promised",
15250
+ // said it did or would do something, and nothing shows it done (the critic's broken_promise)
15251
+ "dropped"
15252
+ // something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
15253
+ ];
15254
+ var DistillerReplySchema2 = external_exports.object({
15255
+ defects: external_exports.array(external_exports.object({
15256
+ kind: external_exports.enum(DISTILLER_DEFECTS2),
15257
+ /** The handles of the lines it shows in. */
15258
+ lines: external_exports.array(external_exports.string()),
15259
+ /** The handle of the record it concerns, or "". */
15260
+ about: external_exports.string(),
15261
+ /** One plain sentence, in words, never handles: it is what the feedback row keeps. */
15262
+ why: external_exports.string()
15263
+ }).strict())
15264
+ }).strict();
15265
+ var DISTILLER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
15266
+ zodToJsonSchema2(DistillerReplySchema2, { $refStrategy: "none" })
15267
+ );
15268
+ var TIDY_AUDIENCES2 = ["talker", "filer", "agents"];
15269
+ var TidyReplySchema2 = external_exports.object({
15270
+ lessons: external_exports.array(external_exports.object({
15271
+ lesson: external_exports.string(),
15272
+ op: external_exports.enum(["keep", "revise", "withdraw"]),
15273
+ text: external_exports.string(),
15274
+ for: external_exports.enum([...TIDY_AUDIENCES2, ""]),
15275
+ into: external_exports.string()
15276
+ }).strict())
15277
+ }).strict();
15278
+ var TIDY_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
15279
+ zodToJsonSchema2(TidyReplySchema2, { $refStrategy: "none" })
15280
+ );
14990
15281
  function entryWords(entry) {
14991
15282
  const content = entry.content;
14992
15283
  if (content && "sealed" in content) return "";
@@ -15006,6 +15297,8 @@ function entryWords(entry) {
15006
15297
  }
15007
15298
  var LIVE_MS2 = 3 * 6e4;
15008
15299
  var WORKING_MS2 = 60 * 6e4;
15300
+ var WORKING_TIER_MS2 = 2 * 60 * 6e4;
15301
+ var RECENT_TIER_MS2 = 24 * 60 * 6e4;
15009
15302
  var ContextSchema2 = external_exports.object({
15010
15303
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
15011
15304
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -15350,6 +15643,10 @@ var AgendaTurnSchema2 = external_exports.object({
15350
15643
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
15351
15644
  * speaks for several agents — the turn is the only place that knows which. */
15352
15645
  agentId: external_exports.string().optional(),
15646
+ /** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
15647
+ * the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
15648
+ * question turn — the talker owns re-asking and the waits, so `question` stays null. */
15649
+ onTable: external_exports.boolean().optional(),
15353
15650
  select: SelectShapeSchema2.optional(),
15354
15651
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
15355
15652
  /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
@@ -15424,9 +15721,16 @@ var InboxItemSchema2 = external_exports.object({
15424
15721
  /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
15425
15722
  * its need offers, exactly as its own card carries them, present only when one of them has a
15426
15723
  * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
15427
- * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
15428
- * says those, and the list stays small (an `html` is up to 16 KB). */
15429
- options: external_exports.array(OptionSchema2).optional()
15724
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
15725
+ options: external_exports.array(OptionSchema2).optional(),
15726
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
15727
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
15728
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
15729
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
15730
+ * preview layout for any `options`, and must see exactly what it did. */
15731
+ choices: external_exports.array(OptionSchema2).optional(),
15732
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
15733
+ select: external_exports.enum(["one", "many", "rank"]).optional()
15430
15734
  })).optional(),
15431
15735
  visuals: external_exports.array(VisualSchema2).optional(),
15432
15736
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -15686,7 +15990,7 @@ var ConnectionSummarySchema2 = external_exports.object({
15686
15990
  * desktop app while open. Null = never seen; stale = offline. */
15687
15991
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
15688
15992
  /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
15689
- * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
15993
+ * its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
15690
15994
  * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
15691
15995
  lastWorkedAt: external_exports.string().datetime().nullable().optional(),
15692
15996
  /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
@@ -15863,7 +16167,11 @@ var QueueItemSchema2 = external_exports.object({
15863
16167
  from: external_exports.enum(["person", "agent"]),
15864
16168
  said: external_exports.string(),
15865
16169
  at: external_exports.string(),
15866
- entryId: external_exports.string()
16170
+ entryId: external_exports.string(),
16171
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
16172
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
16173
+ * none was. The Work row reads it before `said`. */
16174
+ summary: external_exports.string().optional()
15867
16175
  }).nullable().optional(),
15868
16176
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
15869
16177
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -15884,7 +16192,7 @@ var QueueItemSchema2 = external_exports.object({
15884
16192
  *
15885
16193
  * IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
15886
16194
  * writes change which field — the comparison is over the bytes the phone is holding, so a fact
15887
- * the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
16195
+ * the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
15888
16196
  * sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
15889
16197
  * queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
15890
16198
  rev: external_exports.string().optional()
@@ -16198,17 +16506,43 @@ var CallRecapSchema2 = external_exports.object({
16198
16506
  title: external_exports.string(),
16199
16507
  owner: external_exports.string(),
16200
16508
  state: external_exports.string(),
16509
+ /** Whether the call put it to the person. An item it carried and never said has no row on the
16510
+ * review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
16511
+ presented: external_exports.boolean(),
16201
16512
  questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
16202
- /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
16203
- * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
16204
- * summarized like a pre-made option is"), so the row scans like a chosen option and their
16205
- * own words stay under it. Absent on every line stored before the read wrote them, and on
16206
- * anything that is not an answer. */
16513
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
16514
+ * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
16515
+ * gave, so the row scans like a chosen option and their own words stay under it. Absent when
16516
+ * neither wrote one. */
16207
16517
  /** `about` is the request the line answered (its question), null for words that answered none —
16208
16518
  * the key the screen groups on, so one question is one row however many times it was answered. */
16209
- lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
16519
+ /** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
16520
+ * however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
16521
+ * said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
16522
+ lines: external_exports.array(external_exports.object({
16523
+ entryId: external_exports.string().uuid(),
16524
+ words: external_exports.string(),
16525
+ summary: external_exports.string().optional(),
16526
+ about: external_exports.string().nullable().optional(),
16527
+ reply: external_exports.string().nullable().optional(),
16528
+ replyKind: external_exports.string().nullable().optional(),
16529
+ at: external_exports.array(external_exports.number()).optional()
16530
+ }))
16210
16531
  })),
16211
16532
  unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
16533
+ /** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
16534
+ * (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
16535
+ * it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
16536
+ * person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
16537
+ * theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
16538
+ * BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
16539
+ said: external_exports.array(external_exports.object({
16540
+ lineId: external_exports.string().uuid(),
16541
+ speaker: external_exports.enum(["user", "agent"]),
16542
+ agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
16543
+ words: external_exports.string(),
16544
+ atMs: external_exports.number()
16545
+ })),
16212
16546
  more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
16213
16547
  });
16214
16548
 
@@ -16610,7 +16944,8 @@ function startSession(opts) {
16610
16944
  if (!existsSync4(cwd)) {
16611
16945
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
16612
16946
  }
16613
- const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
16947
+ let codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
16948
+ if (opts.model && opts.harness === "codex") codexConfig = { ...codexConfig, model: opts.model };
16614
16949
  if (codexConfig) {
16615
16950
  const key3 = "mcp_servers.paigy.env_vars";
16616
16951
  const configPath = join9(process.env.CODEX_HOME || join9(homedir9(), ".codex"), "config.toml");
@@ -16636,6 +16971,8 @@ function startSession(opts) {
16636
16971
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter3),
16637
16972
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
16638
16973
  ...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
16974
+ ...opts.model && opts.harness === "claude" ? { CLAUDE_MODEL_CONFIG: JSON.stringify({ provider: "anthropic", model: opts.model }) } : {},
16975
+ ...opts.model && opts.harness === "agy" ? { AGY_MODEL_CONFIG: JSON.stringify({ model: opts.model }) } : {},
16639
16976
  ...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
16640
16977
  ...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
16641
16978
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
@@ -17604,7 +17941,7 @@ var claimedRun = (sessionId2) => {
17604
17941
  const slot = sessionSlot(sessionId2);
17605
17942
  return { slot, key: runKey(slot) };
17606
17943
  };
17607
- var launchRun = (spec) => ({ session: spec.sessionId });
17944
+ var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
17608
17945
  var BOUND_ELSEWHERE_MS = 10 * 6e4;
17609
17946
  var boundElsewhere = /* @__PURE__ */ new Map();
17610
17947
  var BOUND_SKIP = "is bound to the session it was paired in, which this host cannot name \u2014 run `paigy-harness handoff` as it, in its folder, to record it";
@@ -17713,7 +18050,7 @@ function startHost(opts) {
17713
18050
  });
17714
18051
  void heartbeat({
17715
18052
  version: VERSION,
17716
- harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
18053
+ harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
17717
18054
  workspaces: listWorkspaces(opts.wsDeps),
17718
18055
  // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
17719
18056
  repos: listRepos(opts.wsDeps)
@@ -17805,9 +18142,14 @@ function startHost(opts) {
17805
18142
  let lastPresence = /* @__PURE__ */ new Map();
17806
18143
  async function evidence() {
17807
18144
  const presence = await presenceMap();
17808
- lastHolders = localHolders();
18145
+ lastHolders = localHolders({ unreadable: (file) => {
18146
+ if (unreadable.has(file)) return;
18147
+ unreadable.add(file);
18148
+ opts.log(`\u25CC session record ${file} has no object in it \u2014 the process it names reads as not running here`);
18149
+ } });
17809
18150
  return { presence, holders: lastHolders };
17810
18151
  }
18152
+ const unreadable = /* @__PURE__ */ new Set();
17811
18153
  let lastHolders = /* @__PURE__ */ new Map();
17812
18154
  async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
17813
18155
  {