@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/cli.js CHANGED
@@ -77,7 +77,7 @@ function runProbe(argv) {
77
77
  function detect(entry, deps = {}) {
78
78
  const find = deps.which ?? which;
79
79
  const probe = deps.probe ?? runProbe;
80
- const base = { name: entry.name, label: entry.label };
80
+ const base = { name: entry.name, label: entry.label, models: entry.models };
81
81
  const cliPath = find(entry.cli);
82
82
  if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
83
83
  const adapterPath = entry.adapter ? find(entry.adapter) : null;
@@ -109,7 +109,8 @@ var init_catalog = __esm({
109
109
  authProbe: ["claude", "auth", "status"],
110
110
  loginHint: "Run `claude` once in a terminal and sign in.",
111
111
  installCli: "curl -fsSL https://claude.ai/install.sh | bash",
112
- installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
112
+ installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp",
113
+ models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
113
114
  },
114
115
  {
115
116
  name: "codex",
@@ -119,7 +120,8 @@ var init_catalog = __esm({
119
120
  authProbe: ["codex", "login", "status"],
120
121
  loginHint: "Run `codex login` to authenticate.",
121
122
  installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
122
- installAdapter: "npm install -g @agentclientprotocol/codex-acp"
123
+ installAdapter: "npm install -g @agentclientprotocol/codex-acp",
124
+ models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"]
123
125
  },
124
126
  {
125
127
  name: "agy",
@@ -129,7 +131,8 @@ var init_catalog = __esm({
129
131
  adapter: "paigy-agy-acp",
130
132
  authProbe: ["agy", "help"],
131
133
  loginHint: "Install and set up Antigravity (`agy`).",
132
- installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
134
+ installCli: "curl -fsSL https://antigravity.dev/install.sh | bash",
135
+ models: ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-pro-exp"]
133
136
  }
134
137
  ];
135
138
  }
@@ -22777,10 +22780,47 @@ function claudeConversation() {
22777
22780
  if (!envSession("CLAUDE_CODE_SESSION_ID") || process.env.CLAUDE_CODE_CHILD_SESSION) return void 0;
22778
22781
  const dir = envSession("CLAUDE_CONFIG_DIR") ?? join2(homedir2(), ".claude");
22779
22782
  for (const pid of ancestors()) {
22783
+ let text;
22780
22784
  try {
22781
- const { sessionId: sessionId2 } = JSON.parse(readFileSync(join2(dir, "sessions", `${pid}.json`), "utf8"));
22782
- return typeof sessionId2 === "string" && sessionId2.trim() ? sessionId2 : void 0;
22785
+ text = readFileSync(join2(dir, "sessions", `${pid}.json`), "utf8");
22783
22786
  } catch {
22787
+ continue;
22788
+ }
22789
+ const record = parseSessionRecord(text);
22790
+ if (!record) continue;
22791
+ return typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId : void 0;
22792
+ }
22793
+ return void 0;
22794
+ }
22795
+ function parseSessionRecord(text) {
22796
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
22797
+ try {
22798
+ return object(JSON.parse(text));
22799
+ } catch {
22800
+ }
22801
+ let depth = 0;
22802
+ let start = -1;
22803
+ let quoted = false;
22804
+ let escaped = false;
22805
+ for (let i = 0; i < text.length; i += 1) {
22806
+ const c = text[i];
22807
+ if (quoted) {
22808
+ if (escaped) escaped = false;
22809
+ else if (c === "\\") escaped = true;
22810
+ else if (c === '"') quoted = false;
22811
+ } else if (c === '"') quoted = true;
22812
+ else if (c === "{") {
22813
+ if (depth === 0) start = i;
22814
+ depth += 1;
22815
+ } else if (c === "}" && depth > 0) {
22816
+ depth -= 1;
22817
+ if (depth === 0) {
22818
+ try {
22819
+ return object(JSON.parse(text.slice(start, i + 1)));
22820
+ } catch {
22821
+ return void 0;
22822
+ }
22823
+ }
22784
22824
  }
22785
22825
  }
22786
22826
  return void 0;
@@ -24254,7 +24294,7 @@ ${block2}`;
24254
24294
  }
24255
24295
  return text.slice(0, start) + updated + text.slice(end);
24256
24296
  }
24257
- var import_undici, require2, CODEX_ENV, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, CLIENT, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, ASK_MAX, UNITS_MAX, UNIT_TITLE_MAX, UNIT_BODY_MAX, UnitInputSchema, questionHandle, NodeRef, BlockedAction, reason, actionFits, ACTION_FITS, userExplicitlyRequested, units, UpdateInputSchema, questionFields, refineQuestion, QuestionInputSchema, QuestionWireSchema, AnswerInputSchema, QuestionDependencySchema, sendFields, ContactSchema, sendsAnything, ContactWireSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, UpdateGoalSchema, changedGoal, goalTitle, goalOutcome, CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange, read, GoalChangeSchema, editNames, ManageGoalsSchema, ManageGoalsToolSchema, MANAGE_GOALS_DESCRIPTION, GetGoalSchema, GET_GOAL_DESCRIPTION, SearchToolSchema, SEARCH_DESCRIPTION, CheckActivitySchema, FEEDBACK_TEXT_MAX, SendFeedbackSchema, SEND_FEEDBACK_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, id, key, participant, SourceSchema, ResultRefSchema, NodeRefSchema, DependencySchema, goalState, existingGoalChanges, BrainGoalChangeSchema, option, QuestionChangeSchema, lessonScope, LessonChangeSchema, BrainNextSchema, messageBase, BrainMessageSchema, BrainSearchSchema, BrainResultSchema, str, strs, CompactResultSchema, COMPACT_RESULT_JSON_SCHEMA, str2, strs2, TalkerReplySchema, TALKER_REPLY_JSON_SCHEMA, str3, strs3, FilerReplySchema, FILER_REPLY_JSON_SCHEMA, LIVE_MS, WORKING_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, LessonStateSchema, LessonViewSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, QueueDeltaSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, CallRecapSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, NOT_SENT, APPLIED, DEMOTED, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24297
+ var import_undici, require2, CODEX_ENV, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, CLIENT, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OPTION_LABEL_MAX, OPTION_HINT_MAX, OptionSchema, OptionInputSchema, NIGHT, ASK_MAX, UNITS_MAX, UNIT_TITLE_MAX, UNIT_BODY_MAX, UnitInputSchema, questionHandle, NodeRef, BlockedAction, reason, actionFits, ACTION_FITS, userExplicitlyRequested, units, UpdateInputSchema, questionFields, refineQuestion, QuestionInputSchema, QuestionWireSchema, AnswerInputSchema, QuestionDependencySchema, sendFields, ContactSchema, sendsAnything, ContactWireSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, UpdateGoalSchema, changedGoal, goalTitle, GOAL_OUTCOME_MAX, goalOutcome, CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange, read, GoalChangeSchema, editNames, ManageGoalsSchema, ManageGoalsToolSchema, MANAGE_GOALS_DESCRIPTION, GetGoalSchema, GET_GOAL_DESCRIPTION, SearchToolSchema, SEARCH_DESCRIPTION, CheckActivitySchema, FEEDBACK_TEXT_MAX, SendFeedbackSchema, SEND_FEEDBACK_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, id, key, participant, SourceSchema, ResultRefSchema, NodeRefSchema, DependencySchema, goalState, existingGoalChanges, BrainGoalChangeSchema, option, QuestionChangeSchema, lessonScope, LessonChangeSchema, BrainNextSchema, messageBase, BrainMessageSchema, BrainSearchSchema, BrainResultSchema, str, strs, CompactResultSchema, COMPACT_RESULT_JSON_SCHEMA, str2, strs2, TalkerReplySchema, TALKER_REPLY_JSON_SCHEMA, str3, strs3, FilerReplySchema, FILER_REPLY_JSON_SCHEMA, ExplainerReplySchema, EXPLAINER_REPLY_JSON_SCHEMA, DISTILLER_DEFECTS, DistillerReplySchema, DISTILLER_REPLY_JSON_SCHEMA, TIDY_AUDIENCES, TidyReplySchema, TIDY_REPLY_JSON_SCHEMA, LIVE_MS, WORKING_MS, WORKING_TIER_MS, RECENT_TIER_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, LessonStateSchema, LessonViewSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, QueueDeltaSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, CallRecapSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, NOT_SENT, APPLIED, DEMOTED, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24258
24298
  var init_dist = __esm({
24259
24299
  "../../packages/sdk/dist/index.js"() {
24260
24300
  "use strict";
@@ -24268,6 +24308,8 @@ var init_dist = __esm({
24268
24308
  init_zod();
24269
24309
  init_zod();
24270
24310
  init_zod();
24311
+ init_zod();
24312
+ init_zod();
24271
24313
  import_undici = __toESM(require_undici(), 1);
24272
24314
  require2 = __sdkCreateRequire(import.meta.url);
24273
24315
  CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
@@ -24649,10 +24691,12 @@ var init_dist = __esm({
24649
24691
  };
24650
24692
  OPTIONS_MIN = 2;
24651
24693
  OPTIONS_MAX = 6;
24694
+ OPTION_LABEL_MAX = 50;
24695
+ OPTION_HINT_MAX = 200;
24652
24696
  OptionSchema = external_exports.object({
24653
24697
  id: external_exports.string(),
24654
24698
  label: external_exports.string(),
24655
- 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(),
24699
+ 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(),
24656
24700
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
24657
24701
  // the constraints below are what an agent reads when deciding to use these.
24658
24702
  html: external_exports.string().max(16384).describe(
@@ -24666,8 +24710,10 @@ var init_dist = __esm({
24666
24710
  id: external_exports.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
24667
24711
  `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.`
24668
24712
  ),
24669
- label: external_exports.string().trim().min(1).max(1e3),
24670
- hint: OptionSchema.shape.hint,
24713
+ // SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
24714
+ // a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
24715
+ 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.`),
24716
+ 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(),
24671
24717
  htmlPreview: OptionSchema.shape.html,
24672
24718
  imgUrl: OptionSchema.shape.image
24673
24719
  }).strict();
@@ -24699,15 +24745,48 @@ var init_dist = __esm({
24699
24745
  message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
24700
24746
  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."),
24701
24747
  units,
24748
+ // A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
24749
+ // until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
24750
+ // still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
24751
+ // tap lands as a contribution, the person's own words on the Goal, because there is no Question
24752
+ // for it to answer.
24753
+ //
24754
+ // AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
24755
+ // acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
24756
+ // it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
24757
+ // work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
24758
+ // call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
24759
+ // tested yet*. The words are worth writing because the agent reads them back.
24760
+ //
24761
+ // ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
24762
+ // write up to four of them: "just on the cards, but it should be more like just they should only
24763
+ // get one basically string value that they can enter that replaces got it"). A report has exactly
24764
+ // one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
24765
+ // `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
24766
+ // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
24767
+ // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
24768
+ reply: external_exports.string().trim().min(1).describe(
24769
+ `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.`
24770
+ ).optional(),
24702
24771
  userExplicitlyRequested
24703
- }).strict().superRefine((u, ctx) => refuseOverCaps(u.message, u.units, "message", ctx));
24772
+ }).strict().superRefine((u, ctx) => {
24773
+ refuseOverCaps(u.message, u.units, "message", ctx);
24774
+ if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
24775
+ ctx.addIssue({
24776
+ code: external_exports.ZodIssueCode.custom,
24777
+ path: ["reply"],
24778
+ params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX },
24779
+ 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.`
24780
+ });
24781
+ }
24782
+ });
24704
24783
  questionFields = {
24705
24784
  question: external_exports.string().trim().min(1).describe(
24706
24785
  `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.`
24707
24786
  ),
24708
24787
  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."),
24709
24788
  units,
24710
- options: external_exports.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
24789
+ 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."),
24711
24790
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
24712
24791
  // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
24713
24792
  pickMode: external_exports.enum(["one", "many", "rank"]).optional().describe(
@@ -24784,6 +24863,8 @@ QUESTIONS: one question per object; several questions are several objects in one
24784
24863
 
24785
24864
  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.
24786
24865
 
24866
+ 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".
24867
+
24787
24868
  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.
24788
24869
 
24789
24870
  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\`).`;
@@ -24827,7 +24908,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24827
24908
  }).strict();
24828
24909
  changedGoal = external_exports.string().uuid().describe("The Goal to change, as a read shows it.");
24829
24910
  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.");
24830
- goalOutcome = external_exports.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
24911
+ GOAL_OUTCOME_MAX = 500;
24912
+ 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.`);
24831
24913
  CreateChange = external_exports.object({
24832
24914
  kind: external_exports.literal("create"),
24833
24915
  title: goalTitle,
@@ -24853,7 +24935,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24853
24935
  goalId: changedGoal,
24854
24936
  dependsOnGoalId: external_exports.string().uuid().describe("The Goal it waits on."),
24855
24937
  action: external_exports.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
24856
- reason: external_exports.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
24938
+ reason: external_exports.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
24857
24939
  }).strict();
24858
24940
  read = { revision: external_exports.number().int().positive().optional() };
24859
24941
  GoalChangeSchema = external_exports.discriminatedUnion("kind", [
@@ -24877,7 +24959,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24877
24959
  ManageGoalsToolSchema = external_exports.object({
24878
24960
  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.")
24879
24961
  }).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
24880
- 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.";
24962
+ 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.";
24881
24963
  GetGoalSchema = external_exports.object({
24882
24964
  goalId: external_exports.string().uuid(),
24883
24965
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -24895,7 +24977,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24895
24977
  goalId: external_exports.string().uuid().optional(),
24896
24978
  limit: external_exports.number().int().min(1).max(20).optional()
24897
24979
  }).strict();
24898
- 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.";
24980
+ 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.";
24899
24981
  CheckActivitySchema = external_exports.object({}).strict();
24900
24982
  FEEDBACK_TEXT_MAX = 5e4;
24901
24983
  SendFeedbackSchema = external_exports.object({
@@ -24958,6 +25040,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24958
25040
  external_exports.object({
24959
25041
  kind: external_exports.literal("create"),
24960
25042
  text: external_exports.string().min(1),
25043
+ summary: external_exports.string().optional(),
24961
25044
  answererId: participant,
24962
25045
  goal: ResultRefSchema.optional(),
24963
25046
  blocks: external_exports.array(external_exports.object({ question: ResultRefSchema }).strict()),
@@ -25008,12 +25091,12 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25008
25091
  }).strict();
25009
25092
  BrainResultSchema = external_exports.object({
25010
25093
  messages: external_exports.array(BrainMessageSchema),
25011
- entries: external_exports.array(external_exports.object({ key, sources: external_exports.array(SourceSchema).min(1), goals: external_exports.array(ResultRefSchema) }).strict()),
25094
+ 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()),
25012
25095
  questions: external_exports.array(external_exports.object({ key, entryKeys: external_exports.array(key), change: QuestionChangeSchema }).strict()),
25013
25096
  answers: external_exports.array(external_exports.object({
25014
25097
  question: ResultRefSchema,
25015
25098
  entryKeys: external_exports.array(key).min(1),
25016
- summary: external_exports.string().min(1),
25099
+ summary: external_exports.string(),
25017
25100
  selectedOptionIds: external_exports.array(external_exports.string().min(1)).optional()
25018
25101
  }).strict()),
25019
25102
  goals: external_exports.array(external_exports.object({ key, entryKeys: external_exports.array(key), change: BrainGoalChangeSchema }).strict()),
@@ -25058,12 +25141,15 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25058
25141
  messages: external_exports.array(external_exports.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
25059
25142
  /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
25060
25143
  then: external_exports.enum(["listen", "hold", "end"]),
25061
- /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
25062
- answers: external_exports.array(external_exports.object({ item: str2, options: strs2, lines: strs2 }).strict()),
25063
- /** The line handles that hold a new instruction from the person. */
25144
+ /** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
25145
+ * IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
25146
+ * "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
25147
+ * end of this call), a question's handle (that question's answer) or an ISO time. */
25148
+ replies: external_exports.array(external_exports.object({ item: str2, options: strs2, lines: strs2, summary: str2, defer: str2 }).strict()),
25149
+ /** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
25064
25150
  instruction: strs2,
25065
- /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
25066
- * reads only these (3e). */
25151
+ /** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
25152
+ * work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
25067
25153
  file: strs2,
25068
25154
  /** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
25069
25155
  need: external_exports.object({ kind: external_exports.enum(["search", "item", "agent", ""]), text: str2 }).strict(),
@@ -25076,8 +25162,9 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25076
25162
  str3 = external_exports.string();
25077
25163
  strs3 = external_exports.array(external_exports.string());
25078
25164
  FilerReplySchema = external_exports.object({
25079
- /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
25080
- filed: external_exports.array(external_exports.object({ line: str3, goals: strs3 }).strict()),
25165
+ /** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
25166
+ * the line says in at most ten words. */
25167
+ filed: external_exports.array(external_exports.object({ line: str3, goals: strs3, summary: str3 }).strict()),
25081
25168
  goals: external_exports.array(external_exports.object({
25082
25169
  op: external_exports.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
25083
25170
  key: str3,
@@ -25096,6 +25183,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25096
25183
  key: str3,
25097
25184
  question: str3,
25098
25185
  text: str3,
25186
+ summary: str3,
25099
25187
  to: str3,
25100
25188
  goal: str3,
25101
25189
  options: external_exports.array(external_exports.object({ id: str3, label: str3 }).strict()),
@@ -25109,8 +25197,64 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25109
25197
  FILER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
25110
25198
  zodToJsonSchema(FilerReplySchema, { $refStrategy: "none" })
25111
25199
  );
25200
+ ExplainerReplySchema = external_exports.object({
25201
+ /** The explanation, as sentences, spoken in order. */
25202
+ say: external_exports.array(external_exports.string()),
25203
+ /** Questions to an agent about gaps in what it read: the agent's handle and the question. */
25204
+ followUps: external_exports.array(external_exports.object({ to: external_exports.string(), text: external_exports.string() }).strict())
25205
+ }).strict();
25206
+ EXPLAINER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
25207
+ zodToJsonSchema(ExplainerReplySchema, { $refStrategy: "none" })
25208
+ );
25209
+ DISTILLER_DEFECTS = [
25210
+ "reasked",
25211
+ // a question put to the person again after they answered it
25212
+ "unsupported",
25213
+ // a fact stated that nothing in the input supports
25214
+ "unsaid",
25215
+ // an agent's answer arrived during the call and was never said
25216
+ "skipped",
25217
+ // an agenda item never put to the person
25218
+ "unsaved",
25219
+ // something the person answered or asked for did not save, was refused or dropped
25220
+ "ignored",
25221
+ // the person asked or said something and was never answered (the critic's ignored_a_question)
25222
+ "promised",
25223
+ // said it did or would do something, and nothing shows it done (the critic's broken_promise)
25224
+ "dropped"
25225
+ // something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
25226
+ ];
25227
+ DistillerReplySchema = external_exports.object({
25228
+ defects: external_exports.array(external_exports.object({
25229
+ kind: external_exports.enum(DISTILLER_DEFECTS),
25230
+ /** The handles of the lines it shows in. */
25231
+ lines: external_exports.array(external_exports.string()),
25232
+ /** The handle of the record it concerns, or "". */
25233
+ about: external_exports.string(),
25234
+ /** One plain sentence, in words, never handles: it is what the feedback row keeps. */
25235
+ why: external_exports.string()
25236
+ }).strict())
25237
+ }).strict();
25238
+ DISTILLER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
25239
+ zodToJsonSchema(DistillerReplySchema, { $refStrategy: "none" })
25240
+ );
25241
+ TIDY_AUDIENCES = ["talker", "filer", "agents"];
25242
+ TidyReplySchema = external_exports.object({
25243
+ lessons: external_exports.array(external_exports.object({
25244
+ lesson: external_exports.string(),
25245
+ op: external_exports.enum(["keep", "revise", "withdraw"]),
25246
+ text: external_exports.string(),
25247
+ for: external_exports.enum([...TIDY_AUDIENCES, ""]),
25248
+ into: external_exports.string()
25249
+ }).strict())
25250
+ }).strict();
25251
+ TIDY_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
25252
+ zodToJsonSchema(TidyReplySchema, { $refStrategy: "none" })
25253
+ );
25112
25254
  LIVE_MS = 3 * 6e4;
25113
25255
  WORKING_MS = 60 * 6e4;
25256
+ WORKING_TIER_MS = 2 * 60 * 6e4;
25257
+ RECENT_TIER_MS = 24 * 60 * 6e4;
25114
25258
  ContextSchema = external_exports.object({
25115
25259
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
25116
25260
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -25455,6 +25599,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25455
25599
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
25456
25600
  * speaks for several agents — the turn is the only place that knows which. */
25457
25601
  agentId: external_exports.string().optional(),
25602
+ /** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
25603
+ * the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
25604
+ * question turn — the talker owns re-asking and the waits, so `question` stays null. */
25605
+ onTable: external_exports.boolean().optional(),
25458
25606
  select: SelectShapeSchema.optional(),
25459
25607
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
25460
25608
  /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
@@ -25529,9 +25677,16 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25529
25677
  /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
25530
25678
  * its need offers, exactly as its own card carries them, present only when one of them has a
25531
25679
  * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
25532
- * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
25533
- * says those, and the list stays small (an `html` is up to 16 KB). */
25534
- options: external_exports.array(OptionSchema).optional()
25680
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
25681
+ options: external_exports.array(OptionSchema).optional(),
25682
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
25683
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
25684
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
25685
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
25686
+ * preview layout for any `options`, and must see exactly what it did. */
25687
+ choices: external_exports.array(OptionSchema).optional(),
25688
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
25689
+ select: external_exports.enum(["one", "many", "rank"]).optional()
25535
25690
  })).optional(),
25536
25691
  visuals: external_exports.array(VisualSchema).optional(),
25537
25692
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -25791,7 +25946,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25791
25946
  * desktop app while open. Null = never seen; stale = offline. */
25792
25947
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
25793
25948
  /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
25794
- * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
25949
+ * its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
25795
25950
  * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
25796
25951
  lastWorkedAt: external_exports.string().datetime().nullable().optional(),
25797
25952
  /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
@@ -25968,7 +26123,11 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25968
26123
  from: external_exports.enum(["person", "agent"]),
25969
26124
  said: external_exports.string(),
25970
26125
  at: external_exports.string(),
25971
- entryId: external_exports.string()
26126
+ entryId: external_exports.string(),
26127
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
26128
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
26129
+ * none was. The Work row reads it before `said`. */
26130
+ summary: external_exports.string().optional()
25972
26131
  }).nullable().optional(),
25973
26132
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
25974
26133
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -25989,7 +26148,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25989
26148
  *
25990
26149
  * IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
25991
26150
  * writes change which field — the comparison is over the bytes the phone is holding, so a fact
25992
- * the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
26151
+ * the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
25993
26152
  * sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
25994
26153
  * queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
25995
26154
  rev: external_exports.string().optional()
@@ -26305,17 +26464,43 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26305
26464
  title: external_exports.string(),
26306
26465
  owner: external_exports.string(),
26307
26466
  state: external_exports.string(),
26467
+ /** Whether the call put it to the person. An item it carried and never said has no row on the
26468
+ * review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
26469
+ presented: external_exports.boolean(),
26308
26470
  questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
26309
- /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
26310
- * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
26311
- * summarized like a pre-made option is"), so the row scans like a chosen option and their
26312
- * own words stay under it. Absent on every line stored before the read wrote them, and on
26313
- * anything that is not an answer. */
26471
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
26472
+ * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
26473
+ * gave, so the row scans like a chosen option and their own words stay under it. Absent when
26474
+ * neither wrote one. */
26314
26475
  /** `about` is the request the line answered (its question), null for words that answered none —
26315
26476
  * the key the screen groups on, so one question is one row however many times it was answered. */
26316
- 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() }))
26477
+ /** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
26478
+ * however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
26479
+ * said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
26480
+ lines: external_exports.array(external_exports.object({
26481
+ entryId: external_exports.string().uuid(),
26482
+ words: external_exports.string(),
26483
+ summary: external_exports.string().optional(),
26484
+ about: external_exports.string().nullable().optional(),
26485
+ reply: external_exports.string().nullable().optional(),
26486
+ replyKind: external_exports.string().nullable().optional(),
26487
+ at: external_exports.array(external_exports.number()).optional()
26488
+ }))
26317
26489
  })),
26318
26490
  unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
26491
+ /** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
26492
+ * (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
26493
+ * it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
26494
+ * person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
26495
+ * theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
26496
+ * BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
26497
+ said: external_exports.array(external_exports.object({
26498
+ lineId: external_exports.string().uuid(),
26499
+ speaker: external_exports.enum(["user", "agent"]),
26500
+ agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
26501
+ words: external_exports.string(),
26502
+ atMs: external_exports.number()
26503
+ })),
26319
26504
  more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
26320
26505
  });
26321
26506
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
@@ -32007,7 +32192,8 @@ function startSession(opts) {
32007
32192
  if (!existsSync3(cwd)) {
32008
32193
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
32009
32194
  }
32010
- const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
32195
+ let codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
32196
+ if (opts.model && opts.harness === "codex") codexConfig = { ...codexConfig, model: opts.model };
32011
32197
  if (codexConfig) {
32012
32198
  const key3 = "mcp_servers.paigy.env_vars";
32013
32199
  const configPath = join5(process.env.CODEX_HOME || join5(homedir4(), ".codex"), "config.toml");
@@ -32033,6 +32219,8 @@ function startSession(opts) {
32033
32219
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
32034
32220
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
32035
32221
  ...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
32222
+ ...opts.model && opts.harness === "claude" ? { CLAUDE_MODEL_CONFIG: JSON.stringify({ provider: "anthropic", model: opts.model }) } : {},
32223
+ ...opts.model && opts.harness === "agy" ? { AGY_MODEL_CONFIG: JSON.stringify({ model: opts.model }) } : {},
32036
32224
  ...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
32037
32225
  ...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
32038
32226
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
@@ -33409,12 +33597,16 @@ init_zod();
33409
33597
  init_zod();
33410
33598
  init_zod();
33411
33599
  init_zod();
33600
+ init_zod();
33601
+ init_zod();
33412
33602
  var OPTIONS_MIN2 = 2;
33413
33603
  var OPTIONS_MAX2 = 6;
33604
+ var OPTION_LABEL_MAX2 = 50;
33605
+ var OPTION_HINT_MAX2 = 200;
33414
33606
  var OptionSchema2 = external_exports.object({
33415
33607
  id: external_exports.string(),
33416
33608
  label: external_exports.string(),
33417
- 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(),
33609
+ 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(),
33418
33610
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
33419
33611
  // the constraints below are what an agent reads when deciding to use these.
33420
33612
  html: external_exports.string().max(16384).describe(
@@ -33428,8 +33620,10 @@ var OptionInputSchema2 = external_exports.object({
33428
33620
  id: external_exports.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
33429
33621
  `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.`
33430
33622
  ),
33431
- label: external_exports.string().trim().min(1).max(1e3),
33432
- hint: OptionSchema2.shape.hint,
33623
+ // SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
33624
+ // a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
33625
+ 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.`),
33626
+ 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(),
33433
33627
  htmlPreview: OptionSchema2.shape.html,
33434
33628
  imgUrl: OptionSchema2.shape.image
33435
33629
  }).strict();
@@ -33531,15 +33725,48 @@ var UpdateInputSchema2 = external_exports.object({
33531
33725
  message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX2} characters; the context goes in units.`),
33532
33726
  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."),
33533
33727
  units: units2,
33728
+ // A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
33729
+ // until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
33730
+ // still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
33731
+ // tap lands as a contribution, the person's own words on the Goal, because there is no Question
33732
+ // for it to answer.
33733
+ //
33734
+ // AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
33735
+ // acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
33736
+ // it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
33737
+ // work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
33738
+ // call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
33739
+ // tested yet*. The words are worth writing because the agent reads them back.
33740
+ //
33741
+ // ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
33742
+ // write up to four of them: "just on the cards, but it should be more like just they should only
33743
+ // get one basically string value that they can enter that replaces got it"). A report has exactly
33744
+ // one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
33745
+ // `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
33746
+ // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
33747
+ // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
33748
+ reply: external_exports.string().trim().min(1).describe(
33749
+ `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.`
33750
+ ).optional(),
33534
33751
  userExplicitlyRequested: userExplicitlyRequested2
33535
- }).strict().superRefine((u, ctx) => refuseOverCaps2(u.message, u.units, "message", ctx));
33752
+ }).strict().superRefine((u, ctx) => {
33753
+ refuseOverCaps2(u.message, u.units, "message", ctx);
33754
+ if (u.reply && u.reply.length > OPTION_LABEL_MAX2) {
33755
+ ctx.addIssue({
33756
+ code: external_exports.ZodIssueCode.custom,
33757
+ path: ["reply"],
33758
+ params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX2 },
33759
+ 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.`
33760
+ });
33761
+ }
33762
+ });
33536
33763
  var questionFields2 = {
33537
33764
  question: external_exports.string().trim().min(1).describe(
33538
33765
  `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.`
33539
33766
  ),
33540
33767
  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."),
33541
33768
  units: units2,
33542
- options: external_exports.array(OptionInputSchema2).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
33769
+ 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."),
33543
33770
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
33544
33771
  // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
33545
33772
  pickMode: external_exports.enum(["one", "many", "rank"]).optional().describe(
@@ -33616,6 +33843,8 @@ QUESTIONS: one question per object; several questions are several objects in one
33616
33843
 
33617
33844
  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.
33618
33845
 
33846
+ 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".
33847
+
33619
33848
  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.
33620
33849
 
33621
33850
  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\`).`;
@@ -33659,7 +33888,8 @@ var UpdateGoalSchema2 = external_exports.object({
33659
33888
  }).strict();
33660
33889
  var changedGoal2 = external_exports.string().uuid().describe("The Goal to change, as a read shows it.");
33661
33890
  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.");
33662
- var goalOutcome2 = external_exports.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
33891
+ var GOAL_OUTCOME_MAX2 = 500;
33892
+ 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.`);
33663
33893
  var CreateChange2 = external_exports.object({
33664
33894
  kind: external_exports.literal("create"),
33665
33895
  title: goalTitle2,
@@ -33685,7 +33915,7 @@ var DependencyChange2 = external_exports.object({
33685
33915
  goalId: changedGoal2,
33686
33916
  dependsOnGoalId: external_exports.string().uuid().describe("The Goal it waits on."),
33687
33917
  action: external_exports.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
33688
- reason: external_exports.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
33918
+ reason: external_exports.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
33689
33919
  }).strict();
33690
33920
  var read2 = { revision: external_exports.number().int().positive().optional() };
33691
33921
  var GoalChangeSchema2 = external_exports.discriminatedUnion("kind", [
@@ -33709,7 +33939,7 @@ var ManageGoalsSchema2 = external_exports.object({
33709
33939
  var ManageGoalsToolSchema2 = external_exports.object({
33710
33940
  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.")
33711
33941
  }).strict().superRefine((v, ctx) => editNames2(v.changes, ctx));
33712
- 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.";
33942
+ 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.";
33713
33943
  var GetGoalSchema2 = external_exports.object({
33714
33944
  goalId: external_exports.string().uuid(),
33715
33945
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -33727,7 +33957,7 @@ var SearchToolSchema2 = external_exports.object({
33727
33957
  goalId: external_exports.string().uuid().optional(),
33728
33958
  limit: external_exports.number().int().min(1).max(20).optional()
33729
33959
  }).strict();
33730
- 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.";
33960
+ 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.";
33731
33961
  var CheckActivitySchema2 = external_exports.object({}).strict();
33732
33962
  var FEEDBACK_TEXT_MAX2 = 5e4;
33733
33963
  var SendFeedbackSchema2 = external_exports.object({
@@ -33790,6 +34020,7 @@ var QuestionChangeSchema2 = external_exports.union([
33790
34020
  external_exports.object({
33791
34021
  kind: external_exports.literal("create"),
33792
34022
  text: external_exports.string().min(1),
34023
+ summary: external_exports.string().optional(),
33793
34024
  answererId: participant2,
33794
34025
  goal: ResultRefSchema2.optional(),
33795
34026
  blocks: external_exports.array(external_exports.object({ question: ResultRefSchema2 }).strict()),
@@ -33840,12 +34071,12 @@ var BrainSearchSchema2 = external_exports.object({
33840
34071
  }).strict();
33841
34072
  var BrainResultSchema2 = external_exports.object({
33842
34073
  messages: external_exports.array(BrainMessageSchema2),
33843
- entries: external_exports.array(external_exports.object({ key: key2, sources: external_exports.array(SourceSchema2).min(1), goals: external_exports.array(ResultRefSchema2) }).strict()),
34074
+ 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()),
33844
34075
  questions: external_exports.array(external_exports.object({ key: key2, entryKeys: external_exports.array(key2), change: QuestionChangeSchema2 }).strict()),
33845
34076
  answers: external_exports.array(external_exports.object({
33846
34077
  question: ResultRefSchema2,
33847
34078
  entryKeys: external_exports.array(key2).min(1),
33848
- summary: external_exports.string().min(1),
34079
+ summary: external_exports.string(),
33849
34080
  selectedOptionIds: external_exports.array(external_exports.string().min(1)).optional()
33850
34081
  }).strict()),
33851
34082
  goals: external_exports.array(external_exports.object({ key: key2, entryKeys: external_exports.array(key2), change: BrainGoalChangeSchema2 }).strict()),
@@ -33890,12 +34121,15 @@ var TalkerReplySchema2 = external_exports.object({
33890
34121
  messages: external_exports.array(external_exports.object({ key: str22, to: str22, text: str22, about: strs22, blocks: str22 }).strict()),
33891
34122
  /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
33892
34123
  then: external_exports.enum(["listen", "hold", "end"]),
33893
- /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
33894
- answers: external_exports.array(external_exports.object({ item: str22, options: strs22, lines: strs22 }).strict()),
33895
- /** The line handles that hold a new instruction from the person. */
34124
+ /** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
34125
+ * IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
34126
+ * "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
34127
+ * end of this call), a question's handle (that question's answer) or an ISO time. */
34128
+ replies: external_exports.array(external_exports.object({ item: str22, options: strs22, lines: strs22, summary: str22, defer: str22 }).strict()),
34129
+ /** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
33896
34130
  instruction: strs22,
33897
- /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
33898
- * reads only these (3e). */
34131
+ /** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
34132
+ * work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
33899
34133
  file: strs22,
33900
34134
  /** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
33901
34135
  need: external_exports.object({ kind: external_exports.enum(["search", "item", "agent", ""]), text: str22 }).strict(),
@@ -33908,8 +34142,9 @@ var TALKER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
33908
34142
  var str32 = external_exports.string();
33909
34143
  var strs32 = external_exports.array(external_exports.string());
33910
34144
  var FilerReplySchema2 = external_exports.object({
33911
- /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
33912
- filed: external_exports.array(external_exports.object({ line: str32, goals: strs32 }).strict()),
34145
+ /** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
34146
+ * the line says in at most ten words. */
34147
+ filed: external_exports.array(external_exports.object({ line: str32, goals: strs32, summary: str32 }).strict()),
33913
34148
  goals: external_exports.array(external_exports.object({
33914
34149
  op: external_exports.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
33915
34150
  key: str32,
@@ -33928,6 +34163,7 @@ var FilerReplySchema2 = external_exports.object({
33928
34163
  key: str32,
33929
34164
  question: str32,
33930
34165
  text: str32,
34166
+ summary: str32,
33931
34167
  to: str32,
33932
34168
  goal: str32,
33933
34169
  options: external_exports.array(external_exports.object({ id: str32, label: str32 }).strict()),
@@ -33941,6 +34177,60 @@ var FilerReplySchema2 = external_exports.object({
33941
34177
  var FILER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
33942
34178
  zodToJsonSchema2(FilerReplySchema2, { $refStrategy: "none" })
33943
34179
  );
34180
+ var ExplainerReplySchema2 = external_exports.object({
34181
+ /** The explanation, as sentences, spoken in order. */
34182
+ say: external_exports.array(external_exports.string()),
34183
+ /** Questions to an agent about gaps in what it read: the agent's handle and the question. */
34184
+ followUps: external_exports.array(external_exports.object({ to: external_exports.string(), text: external_exports.string() }).strict())
34185
+ }).strict();
34186
+ var EXPLAINER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
34187
+ zodToJsonSchema2(ExplainerReplySchema2, { $refStrategy: "none" })
34188
+ );
34189
+ var DISTILLER_DEFECTS2 = [
34190
+ "reasked",
34191
+ // a question put to the person again after they answered it
34192
+ "unsupported",
34193
+ // a fact stated that nothing in the input supports
34194
+ "unsaid",
34195
+ // an agent's answer arrived during the call and was never said
34196
+ "skipped",
34197
+ // an agenda item never put to the person
34198
+ "unsaved",
34199
+ // something the person answered or asked for did not save, was refused or dropped
34200
+ "ignored",
34201
+ // the person asked or said something and was never answered (the critic's ignored_a_question)
34202
+ "promised",
34203
+ // said it did or would do something, and nothing shows it done (the critic's broken_promise)
34204
+ "dropped"
34205
+ // something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
34206
+ ];
34207
+ var DistillerReplySchema2 = external_exports.object({
34208
+ defects: external_exports.array(external_exports.object({
34209
+ kind: external_exports.enum(DISTILLER_DEFECTS2),
34210
+ /** The handles of the lines it shows in. */
34211
+ lines: external_exports.array(external_exports.string()),
34212
+ /** The handle of the record it concerns, or "". */
34213
+ about: external_exports.string(),
34214
+ /** One plain sentence, in words, never handles: it is what the feedback row keeps. */
34215
+ why: external_exports.string()
34216
+ }).strict())
34217
+ }).strict();
34218
+ var DISTILLER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
34219
+ zodToJsonSchema2(DistillerReplySchema2, { $refStrategy: "none" })
34220
+ );
34221
+ var TIDY_AUDIENCES2 = ["talker", "filer", "agents"];
34222
+ var TidyReplySchema2 = external_exports.object({
34223
+ lessons: external_exports.array(external_exports.object({
34224
+ lesson: external_exports.string(),
34225
+ op: external_exports.enum(["keep", "revise", "withdraw"]),
34226
+ text: external_exports.string(),
34227
+ for: external_exports.enum([...TIDY_AUDIENCES2, ""]),
34228
+ into: external_exports.string()
34229
+ }).strict())
34230
+ }).strict();
34231
+ var TIDY_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
34232
+ zodToJsonSchema2(TidyReplySchema2, { $refStrategy: "none" })
34233
+ );
33944
34234
  function entryWords(entry) {
33945
34235
  const content = entry.content;
33946
34236
  if (content && "sealed" in content) return "";
@@ -33960,6 +34250,8 @@ function entryWords(entry) {
33960
34250
  }
33961
34251
  var LIVE_MS2 = 3 * 6e4;
33962
34252
  var WORKING_MS2 = 60 * 6e4;
34253
+ var WORKING_TIER_MS2 = 2 * 60 * 6e4;
34254
+ var RECENT_TIER_MS2 = 24 * 60 * 6e4;
33963
34255
  var ContextSchema2 = external_exports.object({
33964
34256
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
33965
34257
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -34304,6 +34596,10 @@ var AgendaTurnSchema2 = external_exports.object({
34304
34596
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
34305
34597
  * speaks for several agents — the turn is the only place that knows which. */
34306
34598
  agentId: external_exports.string().optional(),
34599
+ /** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
34600
+ * the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
34601
+ * question turn — the talker owns re-asking and the waits, so `question` stays null. */
34602
+ onTable: external_exports.boolean().optional(),
34307
34603
  select: SelectShapeSchema2.optional(),
34308
34604
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
34309
34605
  /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
@@ -34378,9 +34674,16 @@ var InboxItemSchema2 = external_exports.object({
34378
34674
  /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
34379
34675
  * its need offers, exactly as its own card carries them, present only when one of them has a
34380
34676
  * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
34381
- * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
34382
- * says those, and the list stays small (an `html` is up to 16 KB). */
34383
- options: external_exports.array(OptionSchema2).optional()
34677
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
34678
+ options: external_exports.array(OptionSchema2).optional(),
34679
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
34680
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
34681
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
34682
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
34683
+ * preview layout for any `options`, and must see exactly what it did. */
34684
+ choices: external_exports.array(OptionSchema2).optional(),
34685
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
34686
+ select: external_exports.enum(["one", "many", "rank"]).optional()
34384
34687
  })).optional(),
34385
34688
  visuals: external_exports.array(VisualSchema2).optional(),
34386
34689
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -34640,7 +34943,7 @@ var ConnectionSummarySchema2 = external_exports.object({
34640
34943
  * desktop app while open. Null = never seen; stale = offline. */
34641
34944
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
34642
34945
  /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
34643
- * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
34946
+ * its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
34644
34947
  * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
34645
34948
  lastWorkedAt: external_exports.string().datetime().nullable().optional(),
34646
34949
  /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
@@ -34817,7 +35120,11 @@ var QueueItemSchema2 = external_exports.object({
34817
35120
  from: external_exports.enum(["person", "agent"]),
34818
35121
  said: external_exports.string(),
34819
35122
  at: external_exports.string(),
34820
- entryId: external_exports.string()
35123
+ entryId: external_exports.string(),
35124
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
35125
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
35126
+ * none was. The Work row reads it before `said`. */
35127
+ summary: external_exports.string().optional()
34821
35128
  }).nullable().optional(),
34822
35129
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
34823
35130
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -34838,7 +35145,7 @@ var QueueItemSchema2 = external_exports.object({
34838
35145
  *
34839
35146
  * IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
34840
35147
  * writes change which field — the comparison is over the bytes the phone is holding, so a fact
34841
- * the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
35148
+ * the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
34842
35149
  * sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
34843
35150
  * queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
34844
35151
  rev: external_exports.string().optional()
@@ -35152,17 +35459,43 @@ var CallRecapSchema2 = external_exports.object({
35152
35459
  title: external_exports.string(),
35153
35460
  owner: external_exports.string(),
35154
35461
  state: external_exports.string(),
35462
+ /** Whether the call put it to the person. An item it carried and never said has no row on the
35463
+ * review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
35464
+ presented: external_exports.boolean(),
35155
35465
  questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
35156
- /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
35157
- * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
35158
- * summarized like a pre-made option is"), so the row scans like a chosen option and their
35159
- * own words stay under it. Absent on every line stored before the read wrote them, and on
35160
- * anything that is not an answer. */
35466
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
35467
+ * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
35468
+ * gave, so the row scans like a chosen option and their own words stay under it. Absent when
35469
+ * neither wrote one. */
35161
35470
  /** `about` is the request the line answered (its question), null for words that answered none —
35162
35471
  * the key the screen groups on, so one question is one row however many times it was answered. */
35163
- 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() }))
35472
+ /** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
35473
+ * however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
35474
+ * said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
35475
+ lines: external_exports.array(external_exports.object({
35476
+ entryId: external_exports.string().uuid(),
35477
+ words: external_exports.string(),
35478
+ summary: external_exports.string().optional(),
35479
+ about: external_exports.string().nullable().optional(),
35480
+ reply: external_exports.string().nullable().optional(),
35481
+ replyKind: external_exports.string().nullable().optional(),
35482
+ at: external_exports.array(external_exports.number()).optional()
35483
+ }))
35164
35484
  })),
35165
35485
  unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
35486
+ /** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
35487
+ * (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
35488
+ * it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
35489
+ * person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
35490
+ * theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
35491
+ * BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
35492
+ said: external_exports.array(external_exports.object({
35493
+ lineId: external_exports.string().uuid(),
35494
+ speaker: external_exports.enum(["user", "agent"]),
35495
+ agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
35496
+ words: external_exports.string(),
35497
+ atMs: external_exports.number()
35498
+ })),
35166
35499
  more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
35167
35500
  });
35168
35501
 
@@ -36117,10 +36450,14 @@ function localHolderPids(deps = {}) {
36117
36450
  if (!/^\d+\.json$/.test(file)) continue;
36118
36451
  let row;
36119
36452
  try {
36120
- row = JSON.parse(read3(join7(dir, file)));
36453
+ row = parseSessionRecord(read3(join7(dir, file)));
36121
36454
  } catch {
36122
36455
  continue;
36123
36456
  }
36457
+ if (!row) {
36458
+ deps.unreadable?.(file);
36459
+ continue;
36460
+ }
36124
36461
  const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
36125
36462
  if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
36126
36463
  if (!holds(pid, row.procStart, check)) continue;
@@ -36162,10 +36499,14 @@ function localHolders(deps = {}) {
36162
36499
  if (!/^\d+\.json$/.test(file)) continue;
36163
36500
  let row;
36164
36501
  try {
36165
- row = JSON.parse(read3(join7(dir, file)));
36502
+ row = parseSessionRecord(read3(join7(dir, file)));
36166
36503
  } catch {
36167
36504
  continue;
36168
36505
  }
36506
+ if (!row) {
36507
+ deps.unreadable?.(file);
36508
+ continue;
36509
+ }
36169
36510
  const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
36170
36511
  if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
36171
36512
  const slot = sessionSlot(row.sessionId);
@@ -36386,7 +36727,7 @@ var claimedRun = (sessionId2) => {
36386
36727
  const slot = sessionSlot(sessionId2);
36387
36728
  return { slot, key: runKey(slot) };
36388
36729
  };
36389
- var launchRun = (spec) => ({ session: spec.sessionId });
36730
+ var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
36390
36731
  var BOUND_ELSEWHERE_MS = 10 * 6e4;
36391
36732
  var boundElsewhere = /* @__PURE__ */ new Map();
36392
36733
  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";
@@ -36495,7 +36836,7 @@ function startHost(opts) {
36495
36836
  });
36496
36837
  void heartbeat({
36497
36838
  version: VERSION,
36498
- harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
36839
+ harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
36499
36840
  workspaces: listWorkspaces(opts.wsDeps),
36500
36841
  // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
36501
36842
  repos: listRepos(opts.wsDeps)
@@ -36587,9 +36928,14 @@ function startHost(opts) {
36587
36928
  let lastPresence = /* @__PURE__ */ new Map();
36588
36929
  async function evidence() {
36589
36930
  const presence = await presenceMap();
36590
- lastHolders = localHolders();
36931
+ lastHolders = localHolders({ unreadable: (file) => {
36932
+ if (unreadable.has(file)) return;
36933
+ unreadable.add(file);
36934
+ opts.log(`\u25CC session record ${file} has no object in it \u2014 the process it names reads as not running here`);
36935
+ } });
36591
36936
  return { presence, holders: lastHolders };
36592
36937
  }
36938
+ const unreadable = /* @__PURE__ */ new Set();
36593
36939
  let lastHolders = /* @__PURE__ */ new Map();
36594
36940
  async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
36595
36941
  {