@paigy/harness 0.3.37 → 0.3.39

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 +428 -70
  2. package/dist/main.js +2694 -2345
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -298,8 +298,8 @@ var init_ZodError = __esm({
298
298
  "not_finite"
299
299
  ]);
300
300
  quotelessJson = (obj) => {
301
- const json = JSON.stringify(obj, null, 2);
302
- return json.replace(/"([^"]+)":/g, "$1:");
301
+ const json3 = JSON.stringify(obj, null, 2);
302
+ return json3.replace(/"([^"]+)":/g, "$1:");
303
303
  };
304
304
  ZodError = class _ZodError extends Error {
305
305
  get errors() {
@@ -22777,10 +22777,47 @@ function claudeConversation() {
22777
22777
  if (!envSession("CLAUDE_CODE_SESSION_ID") || process.env.CLAUDE_CODE_CHILD_SESSION) return void 0;
22778
22778
  const dir = envSession("CLAUDE_CONFIG_DIR") ?? join2(homedir2(), ".claude");
22779
22779
  for (const pid of ancestors()) {
22780
+ let text;
22780
22781
  try {
22781
- const { sessionId: sessionId2 } = JSON.parse(readFileSync(join2(dir, "sessions", `${pid}.json`), "utf8"));
22782
- return typeof sessionId2 === "string" && sessionId2.trim() ? sessionId2 : void 0;
22782
+ text = readFileSync(join2(dir, "sessions", `${pid}.json`), "utf8");
22783
22783
  } catch {
22784
+ continue;
22785
+ }
22786
+ const record = parseSessionRecord(text);
22787
+ if (!record) continue;
22788
+ return typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId : void 0;
22789
+ }
22790
+ return void 0;
22791
+ }
22792
+ function parseSessionRecord(text) {
22793
+ const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
22794
+ try {
22795
+ return object(JSON.parse(text));
22796
+ } catch {
22797
+ }
22798
+ let depth = 0;
22799
+ let start = -1;
22800
+ let quoted = false;
22801
+ let escaped = false;
22802
+ for (let i = 0; i < text.length; i += 1) {
22803
+ const c = text[i];
22804
+ if (quoted) {
22805
+ if (escaped) escaped = false;
22806
+ else if (c === "\\") escaped = true;
22807
+ else if (c === '"') quoted = false;
22808
+ } else if (c === '"') quoted = true;
22809
+ else if (c === "{") {
22810
+ if (depth === 0) start = i;
22811
+ depth += 1;
22812
+ } else if (c === "}" && depth > 0) {
22813
+ depth -= 1;
22814
+ if (depth === 0) {
22815
+ try {
22816
+ return object(JSON.parse(text.slice(start, i + 1)));
22817
+ } catch {
22818
+ return void 0;
22819
+ }
22820
+ }
22784
22821
  }
22785
22822
  }
22786
22823
  return void 0;
@@ -23887,14 +23924,18 @@ async function hatch(name, voice = null, opts = {}) {
23887
23924
  }
23888
23925
  async function whoAmI(opts = {}) {
23889
23926
  try {
23890
- const res = await reach(`${BACKEND_URL}/api/identity`, {
23891
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
23892
- });
23893
- return res.ok ? await res.json() : null;
23927
+ return await readIdentity(opts);
23894
23928
  } catch {
23895
23929
  return null;
23896
23930
  }
23897
23931
  }
23932
+ async function readIdentity(opts = {}) {
23933
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/identity`, {
23934
+ headers: { authorization: `Bearer ${authToken(opts.token)}` }
23935
+ }));
23936
+ if (!res.ok) await fail("read_identity", res);
23937
+ return await res.json();
23938
+ }
23898
23939
  async function claimSessions(opts = {}) {
23899
23940
  const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
23900
23941
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -24254,7 +24295,7 @@ ${block2}`;
24254
24295
  }
24255
24296
  return text.slice(0, start) + updated + text.slice(end);
24256
24297
  }
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, 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, 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;
24298
+ 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, ASKING_RULES, 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, GetDeliverySchema, GET_DELIVERY_DESCRIPTION, GET_GOAL_DESCRIPTION, SearchToolSchema, SEARCH_DESCRIPTION, ConfigureIdentitySchema, CONFIGURE_IDENTITY_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, json, clock, HeardEventSchema, HeardCursorSchema, HeardPageSchema, MAX_EVENTS, MAX_BYTES, batchBytes, HeardBatchSchema, 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, clock2, 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
24299
  var init_dist = __esm({
24259
24300
  "../../packages/sdk/dist/index.js"() {
24260
24301
  "use strict";
@@ -24270,6 +24311,7 @@ var init_dist = __esm({
24270
24311
  init_zod();
24271
24312
  init_zod();
24272
24313
  init_zod();
24314
+ init_zod();
24273
24315
  import_undici = __toESM(require_undici(), 1);
24274
24316
  require2 = __sdkCreateRequire(import.meta.url);
24275
24317
  CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
@@ -24678,6 +24720,16 @@ var init_dist = __esm({
24678
24720
  imgUrl: OptionSchema.shape.image
24679
24721
  }).strict();
24680
24722
  NIGHT = { from: 23, to: 7 };
24723
+ ASKING_RULES = `HOW TO ASK:
24724
+ 1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
24725
+ 2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
24726
+ 3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
24727
+ 4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
24728
+ 5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
24729
+ 6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
24730
+ 7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
24731
+ 8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
24732
+ 9. 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. 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. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
24681
24733
  ASK_MAX = 250;
24682
24734
  UNITS_MAX = 12;
24683
24735
  UNIT_TITLE_MAX = 50;
@@ -24705,8 +24757,49 @@ var init_dist = __esm({
24705
24757
  message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
24706
24758
  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."),
24707
24759
  units,
24760
+ // A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
24761
+ // until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
24762
+ // still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
24763
+ // tap lands as a contribution, the person's own words on the Goal, because there is no Question
24764
+ // for it to answer.
24765
+ //
24766
+ // AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
24767
+ // acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
24768
+ // it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
24769
+ // work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
24770
+ // call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
24771
+ // tested yet*. The words are worth writing because the agent reads them back.
24772
+ //
24773
+ // ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
24774
+ // write up to four of them: "just on the cards, but it should be more like just they should only
24775
+ // get one basically string value that they can enter that replaces got it"). A report has exactly
24776
+ // one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
24777
+ // `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
24778
+ // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
24779
+ // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
24780
+ reply: external_exports.string().trim().min(1).describe(
24781
+ `What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. The reply is required for requested delivery and completed Goals; ordinary progress may omit it.`
24782
+ ).optional(),
24708
24783
  userExplicitlyRequested
24709
- }).strict().superRefine((u, ctx) => refuseOverCaps(u.message, u.units, "message", ctx));
24784
+ }).strict().superRefine((u, ctx) => {
24785
+ if (u.userExplicitlyRequested && !u.reply) {
24786
+ ctx.addIssue({
24787
+ code: external_exports.ZodIssueCode.custom,
24788
+ path: ["reply"],
24789
+ params: { refusal: "reply_required" },
24790
+ message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
24791
+ });
24792
+ }
24793
+ refuseOverCaps(u.message, u.units, "message", ctx);
24794
+ if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
24795
+ ctx.addIssue({
24796
+ code: external_exports.ZodIssueCode.custom,
24797
+ path: ["reply"],
24798
+ params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX },
24799
+ 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.`
24800
+ });
24801
+ }
24802
+ });
24710
24803
  questionFields = {
24711
24804
  question: external_exports.string().trim().min(1).describe(
24712
24805
  `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.`
@@ -24784,9 +24877,7 @@ var init_dist = __esm({
24784
24877
  questions: external_exports.array(QuestionWireSchema).min(1).max(20).optional()
24785
24878
  }).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
24786
24879
  CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
24787
- CONTACT_DESCRIPTION = `Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read \`results\`, where a failed item names why while the rest still apply.
24788
-
24789
- QUESTIONS: one question per object; several questions are several objects in one contact, so each can be answered on its own. Each is sent exactly as written, with no reading in between: a bundled question arrives as one card. Give options when you have them. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). pickMode:'one' (the default) makes a pick, for alternatives; 'many' a checklist, for independent options they may want several of; 'rank' an ordering. The person can always answer in their own words, so never add an 'Other' option. workItBlocks names the work a question holds up, only when you are truly blocked: it asks for a call. Answers land on the question's Goal: collect them by receiving (below) or with get_goal.
24880
+ CONTACT_DESCRIPTION = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES + `
24790
24881
 
24791
24882
  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.
24792
24883
 
@@ -24895,6 +24986,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24895
24986
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
24896
24987
  diagnose: external_exports.boolean().optional()
24897
24988
  }).strict();
24989
+ GetDeliverySchema = external_exports.object({
24990
+ deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
24991
+ }).strict();
24992
+ GET_DELIVERY_DESCRIPTION = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
24898
24993
  GET_GOAL_DESCRIPTION = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
24899
24994
  SearchToolSchema = external_exports.object({
24900
24995
  query: external_exports.string().trim().min(1).max(500),
@@ -24903,6 +24998,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24903
24998
  limit: external_exports.number().int().min(1).max(20).optional()
24904
24999
  }).strict();
24905
25000
  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.";
25001
+ ConfigureIdentitySchema = external_exports.object({}).strict();
25002
+ CONFIGURE_IDENTITY_DESCRIPTION = "Read this authenticated agent\u2019s current name, voice and token ID. Read-only; no arguments. Pairing, renaming and logout are available only through the local configure driver.";
24906
25003
  CheckActivitySchema = external_exports.object({}).strict();
24907
25004
  FEEDBACK_TEXT_MAX = 5e4;
24908
25005
  SendFeedbackSchema = external_exports.object({
@@ -24912,8 +25009,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24912
25009
  }).strict();
24913
25010
  SEND_FEEDBACK_DESCRIPTION = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
24914
25011
  AGENT_TOOLS = [
25012
+ { name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION, inputSchema: mcpInputSchema(ConfigureIdentitySchema) },
24915
25013
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
24916
25014
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
25015
+ { name: "get_delivery", description: GET_DELIVERY_DESCRIPTION, inputSchema: mcpInputSchema(GetDeliverySchema) },
24917
25016
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
24918
25017
  { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
24919
25018
  { name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
@@ -25144,8 +25243,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25144
25243
  // something the person answered or asked for did not save, was refused or dropped
25145
25244
  "ignored",
25146
25245
  // the person asked or said something and was never answered (the critic's ignored_a_question)
25147
- "promised"
25246
+ "promised",
25148
25247
  // said it did or would do something, and nothing shows it done (the critic's broken_promise)
25248
+ "dropped"
25249
+ // something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
25149
25250
  ];
25150
25251
  DistillerReplySchema = external_exports.object({
25151
25252
  defects: external_exports.array(external_exports.object({
@@ -25176,6 +25277,44 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25176
25277
  );
25177
25278
  LIVE_MS = 3 * 6e4;
25178
25279
  WORKING_MS = 60 * 6e4;
25280
+ WORKING_TIER_MS = 2 * 60 * 6e4;
25281
+ RECENT_TIER_MS = 24 * 60 * 6e4;
25282
+ json = external_exports.lazy(() => external_exports.union([
25283
+ external_exports.string(),
25284
+ external_exports.number().finite(),
25285
+ external_exports.boolean(),
25286
+ external_exports.null(),
25287
+ external_exports.array(json),
25288
+ external_exports.record(json)
25289
+ ]));
25290
+ clock = external_exports.number().int().nonnegative().safe();
25291
+ HeardEventSchema = external_exports.object({
25292
+ eventId: external_exports.string().uuid(),
25293
+ source: external_exports.enum(["phone", "bot"]),
25294
+ sequence: clock,
25295
+ kind: external_exports.string().min(1),
25296
+ phoneAtMs: clock.optional(),
25297
+ botAtMs: clock.optional(),
25298
+ payload: external_exports.record(json)
25299
+ }).strict();
25300
+ HeardCursorSchema = HeardEventSchema.pick({ source: true, sequence: true, eventId: true });
25301
+ HeardPageSchema = external_exports.object({
25302
+ events: external_exports.array(HeardEventSchema).max(200),
25303
+ next: HeardCursorSchema.nullable()
25304
+ }).strict();
25305
+ MAX_EVENTS = 50;
25306
+ MAX_BYTES = 8e4;
25307
+ batchBytes = (events) => {
25308
+ let bytes = 0;
25309
+ for (const char of JSON.stringify({ events })) {
25310
+ const point = char.codePointAt(0);
25311
+ bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
25312
+ }
25313
+ return bytes;
25314
+ };
25315
+ HeardBatchSchema = external_exports.object({
25316
+ events: external_exports.array(HeardEventSchema).min(1).max(MAX_EVENTS)
25317
+ }).strict().refine((value) => batchBytes(value.events) <= MAX_BYTES, "heard_batch_too_large");
25179
25318
  ContextSchema = external_exports.object({
25180
25319
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
25181
25320
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -25520,6 +25659,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25520
25659
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
25521
25660
  * speaks for several agents — the turn is the only place that knows which. */
25522
25661
  agentId: external_exports.string().optional(),
25662
+ /** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
25663
+ * the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
25664
+ * question turn — the talker owns re-asking and the waits, so `question` stays null. */
25665
+ onTable: external_exports.boolean().optional(),
25523
25666
  select: SelectShapeSchema.optional(),
25524
25667
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
25525
25668
  /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
@@ -25594,9 +25737,16 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25594
25737
  /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
25595
25738
  * its need offers, exactly as its own card carries them, present only when one of them has a
25596
25739
  * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
25597
- * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
25598
- * says those, and the list stays small (an `html` is up to 16 KB). */
25599
- options: external_exports.array(OptionSchema).optional()
25740
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
25741
+ options: external_exports.array(OptionSchema).optional(),
25742
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
25743
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
25744
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
25745
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
25746
+ * preview layout for any `options`, and must see exactly what it did. */
25747
+ choices: external_exports.array(OptionSchema).optional(),
25748
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
25749
+ select: external_exports.enum(["one", "many", "rank"]).optional()
25600
25750
  })).optional(),
25601
25751
  visuals: external_exports.array(VisualSchema).optional(),
25602
25752
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -25721,8 +25871,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25721
25871
  "inbox",
25722
25872
  "dismiss"
25723
25873
  ]);
25724
- clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
25725
- QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
25874
+ clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
25875
+ QUIET = ` Nothing rings from ${clock2(NIGHT.from)} to ${clock2(NIGHT.to)} your time; the count waits for morning.`;
25726
25876
  BrokerTuningSchema = external_exports.object({
25727
25877
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
25728
25878
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -26033,7 +26183,17 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26033
26183
  from: external_exports.enum(["person", "agent"]),
26034
26184
  said: external_exports.string(),
26035
26185
  at: external_exports.string(),
26036
- entryId: external_exports.string()
26186
+ entryId: external_exports.string(),
26187
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
26188
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
26189
+ * none was. The Work row reads it before `said`. */
26190
+ summary: external_exports.string().optional(),
26191
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
26192
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
26193
+ titles: external_exports.array(external_exports.string()).optional(),
26194
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
26195
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
26196
+ line: external_exports.string().optional()
26037
26197
  }).nullable().optional(),
26038
26198
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
26039
26199
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -26370,6 +26530,9 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26370
26530
  title: external_exports.string(),
26371
26531
  owner: external_exports.string(),
26372
26532
  state: external_exports.string(),
26533
+ /** Whether the call put it to the person. An item it carried and never said has no row on the
26534
+ * review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
26535
+ presented: external_exports.boolean(),
26373
26536
  questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
26374
26537
  /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
26375
26538
  * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
@@ -26377,10 +26540,36 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26377
26540
  * neither wrote one. */
26378
26541
  /** `about` is the request the line answered (its question), null for words that answered none —
26379
26542
  * the key the screen groups on, so one question is one row however many times it was answered. */
26380
- lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), summary: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
26543
+ /** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
26544
+ * however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
26545
+ * said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
26546
+ lines: external_exports.array(external_exports.object({
26547
+ entryId: external_exports.string().uuid(),
26548
+ words: external_exports.string(),
26549
+ summary: external_exports.string().optional(),
26550
+ about: external_exports.string().nullable().optional(),
26551
+ reply: external_exports.string().nullable().optional(),
26552
+ replyKind: external_exports.string().nullable().optional(),
26553
+ at: external_exports.array(external_exports.number()).optional()
26554
+ }))
26381
26555
  })),
26382
26556
  unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
26383
- more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
26557
+ /** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
26558
+ * (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
26559
+ * it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
26560
+ * person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
26561
+ * theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
26562
+ * BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
26563
+ said: external_exports.array(external_exports.object({
26564
+ lineId: external_exports.string().uuid(),
26565
+ speaker: external_exports.enum(["user", "agent"]),
26566
+ agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
26567
+ words: external_exports.string(),
26568
+ atMs: external_exports.number()
26569
+ })),
26570
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
26571
+ /** Last stored position on this page when another exists; absent before paging shipped. */
26572
+ next: external_exports.number().int().nonnegative().nullable().optional()
26384
26573
  });
26385
26574
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
26386
26575
  KEY_PATH = join22(homedir22(), ".paigy", "key.json");
@@ -31309,11 +31498,11 @@ function extractJson(raw) {
31309
31498
  return null;
31310
31499
  }
31311
31500
  function parseVerdicts(raw, knownIds, knownAgents) {
31312
- const json = extractJson(raw);
31313
- if (!json) return { ok: false, error: "no JSON found in the model's answer", raw };
31501
+ const json3 = extractJson(raw);
31502
+ if (!json3) return { ok: false, error: "no JSON found in the model's answer", raw };
31314
31503
  let parsed;
31315
31504
  try {
31316
- parsed = JSON.parse(json);
31505
+ parsed = JSON.parse(json3);
31317
31506
  } catch (e) {
31318
31507
  return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
31319
31508
  }
@@ -31559,13 +31748,17 @@ var init_triage = __esm({
31559
31748
  init_catalog();
31560
31749
 
31561
31750
  // src/cli-args.ts
31751
+ init_zod();
31562
31752
  var TRIAGE_VIA = ["ollama", "claude", "codex", "agy"];
31563
31753
  var USAGE = "usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | enable-tools [--scope user|project] | hatch NAME [--voice KEY] [--slot SLOT] | launch NAME [--under GOAL] [--voice KEY] [--harness \u2026] [--mode \u2026] [--cwd DIR] BRIEF\u2026 | handoff | triage [--via ollama|claude|codex|agy] [--json] [--apply [--yes-close] [--yes-stale] [--yes-assign]] | [--harness claude|codex|agy] [--mode bypass|ask] [--cwd DIR] [--grace SECONDS] [--doctor] PROMPT\u2026";
31564
31754
  function parseArgs(argv) {
31565
31755
  const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, help: false, host: false, pair: false, service: false, setup: false, enableTools: false, scope: "user", grant: [], hatch: null, voice: null, slot: null, launch: null, under: null, graceSeconds: 90, prompt: "", handoff: false, triage: false, via: null, json: false, apply: false, yesClose: false, yesStale: false, yesAssign: false };
31566
31756
  const words2 = [];
31757
+ let commandPosition = true;
31567
31758
  for (let i = 0; i < argv.length; i++) {
31568
31759
  const arg = argv[i];
31760
+ const command = commandPosition && !!arg && !arg.startsWith("-");
31761
+ if (command) commandPosition = false;
31569
31762
  const next = () => argv[++i];
31570
31763
  if (arg === "--harness") {
31571
31764
  const v = next();
@@ -31581,7 +31774,7 @@ function parseArgs(argv) {
31581
31774
  args.cwd = v;
31582
31775
  } else if (arg === "--doctor") {
31583
31776
  args.doctor = true;
31584
- } else if (arg === "-h" || arg === "--help" || arg === "help") {
31777
+ } else if (arg === "-h" || arg === "--help" || command && arg === "help") {
31585
31778
  args.help = true;
31586
31779
  } else if (arg === "--scope") {
31587
31780
  const v = next();
@@ -31591,21 +31784,21 @@ function parseArgs(argv) {
31591
31784
  const v = Number(next());
31592
31785
  if (!Number.isFinite(v) || v < 0) return { error: "--grace needs seconds (0 = straight to phone)" };
31593
31786
  args.graceSeconds = v;
31594
- } else if (arg === "host") {
31787
+ } else if (command && arg === "host") {
31595
31788
  args.host = true;
31596
- } else if (arg === "pair") {
31789
+ } else if (command && arg === "pair") {
31597
31790
  args.pair = true;
31598
- } else if (arg === "setup") {
31791
+ } else if (command && arg === "setup") {
31599
31792
  args.setup = true;
31600
- } else if (arg === "service") {
31793
+ } else if (command && arg === "service") {
31601
31794
  args.service = true;
31602
31795
  } else if (arg === "--grant") {
31603
31796
  const v = next();
31604
31797
  if (!v) return { error: "--grant needs a folder to allow" };
31605
31798
  args.grant.push(v);
31606
- } else if (arg === "handoff") {
31799
+ } else if (command && arg === "handoff") {
31607
31800
  args.handoff = true;
31608
- } else if (arg === "triage") {
31801
+ } else if (command && arg === "triage") {
31609
31802
  args.triage = true;
31610
31803
  } else if (arg === "--via") {
31611
31804
  const v = next();
@@ -31622,9 +31815,9 @@ function parseArgs(argv) {
31622
31815
  args.yesStale = true;
31623
31816
  } else if (arg === "--yes-assign") {
31624
31817
  args.yesAssign = true;
31625
- } else if (arg === "enable-tools") {
31818
+ } else if (command && arg === "enable-tools") {
31626
31819
  args.enableTools = true;
31627
- } else if (arg === "hatch") {
31820
+ } else if (command && arg === "hatch") {
31628
31821
  const v = next();
31629
31822
  if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
31630
31823
  args.hatch = v;
@@ -31634,12 +31827,14 @@ function parseArgs(argv) {
31634
31827
  args.slot = next() ?? null;
31635
31828
  } else if (arg === "--identity") {
31636
31829
  return { error: '--identity is gone: start a helper with `paigy-harness launch NAME "brief"`, which gives it its own session' };
31637
- } else if (arg === "launch") {
31830
+ } else if (command && arg === "launch") {
31638
31831
  const v = next();
31639
31832
  if (!v) return { error: 'launch needs a name: paigy-harness launch "Deploy check" "brief"' };
31640
31833
  args.launch = v;
31641
31834
  } else if (arg === "--under") {
31642
- args.under = next() ?? null;
31835
+ const v = next();
31836
+ if (!v || !external_exports.string().uuid().safeParse(v).success) return { error: "--under needs a Goal UUID" };
31837
+ args.under = v;
31643
31838
  } else if (arg?.startsWith("--")) {
31644
31839
  return { error: `unknown flag ${arg}` };
31645
31840
  } else if (arg) {
@@ -31648,6 +31843,7 @@ function parseArgs(argv) {
31648
31843
  }
31649
31844
  args.prompt = words2.join(" ");
31650
31845
  if (args.help) return args;
31846
+ if (args.under && !args.launch) return { error: "--under requires launch NAME" };
31651
31847
  if (args.launch && !args.prompt) return { error: 'launch needs a brief: paigy-harness launch "Deploy check" "brief"' };
31652
31848
  if (!args.doctor && !args.handoff && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.enableTools && !args.triage && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / enable-tools / hatch NAME / handoff / triage)" };
31653
31849
  if ((args.yesClose || args.yesStale || args.yesAssign) && !args.apply)
@@ -33475,6 +33671,7 @@ init_zod();
33475
33671
  init_zod();
33476
33672
  init_zod();
33477
33673
  init_zod();
33674
+ init_zod();
33478
33675
  var OPTIONS_MIN2 = 2;
33479
33676
  var OPTIONS_MAX2 = 6;
33480
33677
  var OPTION_LABEL_MAX2 = 50;
@@ -33504,6 +33701,16 @@ var OptionInputSchema2 = external_exports.object({
33504
33701
  imgUrl: OptionSchema2.shape.image
33505
33702
  }).strict();
33506
33703
  var NIGHT2 = { from: 23, to: 7 };
33704
+ var ASKING_RULES2 = `HOW TO ASK:
33705
+ 1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
33706
+ 2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
33707
+ 3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
33708
+ 4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
33709
+ 5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
33710
+ 6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
33711
+ 7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
33712
+ 8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
33713
+ 9. 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. 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. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
33507
33714
  function draft20202(node) {
33508
33715
  if (Array.isArray(node)) return node.map(draft20202);
33509
33716
  if (node && typeof node === "object") {
@@ -33601,8 +33808,49 @@ var UpdateInputSchema2 = external_exports.object({
33601
33808
  message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX2} characters; the context goes in units.`),
33602
33809
  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."),
33603
33810
  units: units2,
33811
+ // A REPORT KEEPS ITS TAP (#2799, #3260; owner, 2026-10-08, settling #2776: "keep the reports open
33812
+ // until the user acknowledges them"). A report asks nothing, so it opens no Question -- but it
33813
+ // still waits to be acknowledged, and it carries the ONE tap that closes it. Never a decision: the
33814
+ // tap lands as a contribution, the person's own words on the Goal, because there is no Question
33815
+ // for it to answer.
33816
+ //
33817
+ // AND ITS WORDS ARE WRITTEN FOR THIS REPORT (owner, 2026-10-08, Goal 3c12d3c6: "Agent
33818
+ // acknowledgments vary (e.g. 'sounds good, I will test later') instead of always 'got it'"). `Got
33819
+ // it` and `Noted` say only that a card was cleared, where "Sounds good, I'll test later" says the
33820
+ // work was RECEIVED and not verified -- the distinction a session spent an hour recovering from a
33821
+ // call transcript on 2026-10-07, having read "not quite" as a defect report when it meant *not
33822
+ // tested yet*. The words are worth writing because the agent reads them back.
33823
+ //
33824
+ // ONE STRING, NOT A LIST (owner, 2026-10-08 15:47, Goal 31a1e2ac, asked whether an agent should
33825
+ // write up to four of them: "just on the cards, but it should be more like just they should only
33826
+ // get one basically string value that they can enter that replaces got it"). A report has exactly
33827
+ // one way to close, so the only thing an agent writes is what that button SAYS. #3260's 1..6
33828
+ // `options` and this field are one job: the list is deleted, not deprecated (ONE JOB, ONE
33829
+ // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
33830
+ // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
33831
+ reply: external_exports.string().trim().min(1).describe(
33832
+ `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. The reply is required for requested delivery and completed Goals; ordinary progress may omit it.`
33833
+ ).optional(),
33604
33834
  userExplicitlyRequested: userExplicitlyRequested2
33605
- }).strict().superRefine((u, ctx) => refuseOverCaps2(u.message, u.units, "message", ctx));
33835
+ }).strict().superRefine((u, ctx) => {
33836
+ if (u.userExplicitlyRequested && !u.reply) {
33837
+ ctx.addIssue({
33838
+ code: external_exports.ZodIssueCode.custom,
33839
+ path: ["reply"],
33840
+ params: { refusal: "reply_required" },
33841
+ message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
33842
+ });
33843
+ }
33844
+ refuseOverCaps2(u.message, u.units, "message", ctx);
33845
+ if (u.reply && u.reply.length > OPTION_LABEL_MAX2) {
33846
+ ctx.addIssue({
33847
+ code: external_exports.ZodIssueCode.custom,
33848
+ path: ["reply"],
33849
+ params: { refusal: "reply_too_long", length: u.reply.length, cap: OPTION_LABEL_MAX2 },
33850
+ 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.`
33851
+ });
33852
+ }
33853
+ });
33606
33854
  var questionFields2 = {
33607
33855
  question: external_exports.string().trim().min(1).describe(
33608
33856
  `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.`
@@ -33680,9 +33928,7 @@ var ContactWireSchema2 = external_exports.object({
33680
33928
  questions: external_exports.array(QuestionWireSchema2).min(1).max(20).optional()
33681
33929
  }).strict().refine(sendsAnything2, "a contact sends at least one update, question, answer, withdrawal or question dependency");
33682
33930
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
33683
- var CONTACT_DESCRIPTION2 = `Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read \`results\`, where a failed item names why while the rest still apply.
33684
-
33685
- QUESTIONS: one question per object; several questions are several objects in one contact, so each can be answered on its own. Each is sent exactly as written, with no reading in between: a bundled question arrives as one card. Give options when you have them. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). pickMode:'one' (the default) makes a pick, for alternatives; 'many' a checklist, for independent options they may want several of; 'rank' an ordering. The person can always answer in their own words, so never add an 'Other' option. workItBlocks names the work a question holds up, only when you are truly blocked: it asks for a call. Answers land on the question's Goal: collect them by receiving (below) or with get_goal.
33931
+ var CONTACT_DESCRIPTION2 = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES2 + `
33686
33932
 
33687
33933
  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.
33688
33934
 
@@ -33791,6 +34037,10 @@ var GetGoalSchema2 = external_exports.object({
33791
34037
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
33792
34038
  diagnose: external_exports.boolean().optional()
33793
34039
  }).strict();
34040
+ var GetDeliverySchema2 = external_exports.object({
34041
+ deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
34042
+ }).strict();
34043
+ var GET_DELIVERY_DESCRIPTION2 = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
33794
34044
  var GET_GOAL_DESCRIPTION2 = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
33795
34045
  var SearchToolSchema2 = external_exports.object({
33796
34046
  query: external_exports.string().trim().min(1).max(500),
@@ -33799,6 +34049,8 @@ var SearchToolSchema2 = external_exports.object({
33799
34049
  limit: external_exports.number().int().min(1).max(20).optional()
33800
34050
  }).strict();
33801
34051
  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.";
34052
+ var ConfigureIdentitySchema2 = external_exports.object({}).strict();
34053
+ var CONFIGURE_IDENTITY_DESCRIPTION2 = "Read this authenticated agent\u2019s current name, voice and token ID. Read-only; no arguments. Pairing, renaming and logout are available only through the local configure driver.";
33802
34054
  var CheckActivitySchema2 = external_exports.object({}).strict();
33803
34055
  var FEEDBACK_TEXT_MAX2 = 5e4;
33804
34056
  var SendFeedbackSchema2 = external_exports.object({
@@ -33808,8 +34060,10 @@ var SendFeedbackSchema2 = external_exports.object({
33808
34060
  }).strict();
33809
34061
  var SEND_FEEDBACK_DESCRIPTION2 = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
33810
34062
  var AGENT_TOOLS2 = [
34063
+ { name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION2, inputSchema: mcpInputSchema2(ConfigureIdentitySchema2) },
33811
34064
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
33812
34065
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(ManageGoalsToolSchema2) },
34066
+ { name: "get_delivery", description: GET_DELIVERY_DESCRIPTION2, inputSchema: mcpInputSchema2(GetDeliverySchema2) },
33813
34067
  { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
33814
34068
  { name: "search", description: SEARCH_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchToolSchema2) },
33815
34069
  { name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema2(CheckActivitySchema2) },
@@ -34040,8 +34294,10 @@ var DISTILLER_DEFECTS2 = [
34040
34294
  // something the person answered or asked for did not save, was refused or dropped
34041
34295
  "ignored",
34042
34296
  // the person asked or said something and was never answered (the critic's ignored_a_question)
34043
- "promised"
34297
+ "promised",
34044
34298
  // said it did or would do something, and nothing shows it done (the critic's broken_promise)
34299
+ "dropped"
34300
+ // something it had to say never played and was never said later (owner, 2026-10-08, call 65433a9d)
34045
34301
  ];
34046
34302
  var DistillerReplySchema2 = external_exports.object({
34047
34303
  defects: external_exports.array(external_exports.object({
@@ -34089,6 +34345,45 @@ function entryWords(entry) {
34089
34345
  }
34090
34346
  var LIVE_MS2 = 3 * 6e4;
34091
34347
  var WORKING_MS2 = 60 * 6e4;
34348
+ var isWorking = (c, now) => !!c.lastWorkedAt && now - Date.parse(c.lastWorkedAt) < WORKING_MS2;
34349
+ var WORKING_TIER_MS2 = 2 * 60 * 6e4;
34350
+ var RECENT_TIER_MS2 = 24 * 60 * 6e4;
34351
+ var json2 = external_exports.lazy(() => external_exports.union([
34352
+ external_exports.string(),
34353
+ external_exports.number().finite(),
34354
+ external_exports.boolean(),
34355
+ external_exports.null(),
34356
+ external_exports.array(json2),
34357
+ external_exports.record(json2)
34358
+ ]));
34359
+ var clock3 = external_exports.number().int().nonnegative().safe();
34360
+ var HeardEventSchema2 = external_exports.object({
34361
+ eventId: external_exports.string().uuid(),
34362
+ source: external_exports.enum(["phone", "bot"]),
34363
+ sequence: clock3,
34364
+ kind: external_exports.string().min(1),
34365
+ phoneAtMs: clock3.optional(),
34366
+ botAtMs: clock3.optional(),
34367
+ payload: external_exports.record(json2)
34368
+ }).strict();
34369
+ var HeardCursorSchema2 = HeardEventSchema2.pick({ source: true, sequence: true, eventId: true });
34370
+ var HeardPageSchema2 = external_exports.object({
34371
+ events: external_exports.array(HeardEventSchema2).max(200),
34372
+ next: HeardCursorSchema2.nullable()
34373
+ }).strict();
34374
+ var MAX_EVENTS2 = 50;
34375
+ var MAX_BYTES2 = 8e4;
34376
+ var batchBytes2 = (events) => {
34377
+ let bytes = 0;
34378
+ for (const char of JSON.stringify({ events })) {
34379
+ const point = char.codePointAt(0);
34380
+ bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
34381
+ }
34382
+ return bytes;
34383
+ };
34384
+ var HeardBatchSchema2 = external_exports.object({
34385
+ events: external_exports.array(HeardEventSchema2).min(1).max(MAX_EVENTS2)
34386
+ }).strict().refine((value) => batchBytes2(value.events) <= MAX_BYTES2, "heard_batch_too_large");
34092
34387
  var ContextSchema2 = external_exports.object({
34093
34388
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
34094
34389
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -34433,6 +34728,10 @@ var AgendaTurnSchema2 = external_exports.object({
34433
34728
  * so "who said that" survives the call, and it rides PER TURN because a coalesced call
34434
34729
  * speaks for several agents — the turn is the only place that knows which. */
34435
34730
  agentId: external_exports.string().optional(),
34731
+ /** ITS QUESTION IS ON THE TABLE (#3292): a talker line about a question the person owes. The walk tells
34732
+ * the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
34733
+ * question turn — the talker owns re-asking and the waits, so `question` stays null. */
34734
+ onTable: external_exports.boolean().optional(),
34436
34735
  select: SelectShapeSchema2.optional(),
34437
34736
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
34438
34737
  /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
@@ -34507,9 +34806,16 @@ var InboxItemSchema2 = external_exports.object({
34507
34806
  /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
34508
34807
  * its need offers, exactly as its own card carries them, present only when one of them has a
34509
34808
  * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
34510
- * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
34511
- * says those, and the list stays small (an `html` is up to 16 KB). */
34512
- options: external_exports.array(OptionSchema2).optional()
34809
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
34810
+ options: external_exports.array(OptionSchema2).optional(),
34811
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
34812
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
34813
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
34814
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
34815
+ * preview layout for any `options`, and must see exactly what it did. */
34816
+ choices: external_exports.array(OptionSchema2).optional(),
34817
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
34818
+ select: external_exports.enum(["one", "many", "rank"]).optional()
34513
34819
  })).optional(),
34514
34820
  visuals: external_exports.array(VisualSchema2).optional(),
34515
34821
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -34634,8 +34940,8 @@ var MissedCallSchema2 = external_exports.enum([
34634
34940
  "inbox",
34635
34941
  "dismiss"
34636
34942
  ]);
34637
- var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
34638
- var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
34943
+ var clock22 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
34944
+ var QUIET2 = ` Nothing rings from ${clock22(NIGHT2.from)} to ${clock22(NIGHT2.to)} your time; the count waits for morning.`;
34639
34945
  var BrokerTuningSchema2 = external_exports.object({
34640
34946
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
34641
34947
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -34946,7 +35252,17 @@ var QueueItemSchema2 = external_exports.object({
34946
35252
  from: external_exports.enum(["person", "agent"]),
34947
35253
  said: external_exports.string(),
34948
35254
  at: external_exports.string(),
34949
- entryId: external_exports.string()
35255
+ entryId: external_exports.string(),
35256
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
35257
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
35258
+ * none was. The Work row reads it before `said`. */
35259
+ summary: external_exports.string().optional(),
35260
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
35261
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
35262
+ titles: external_exports.array(external_exports.string()).optional(),
35263
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
35264
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
35265
+ line: external_exports.string().optional()
34950
35266
  }).nullable().optional(),
34951
35267
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
34952
35268
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -35281,6 +35597,9 @@ var CallRecapSchema2 = external_exports.object({
35281
35597
  title: external_exports.string(),
35282
35598
  owner: external_exports.string(),
35283
35599
  state: external_exports.string(),
35600
+ /** Whether the call put it to the person. An item it carried and never said has no row on the
35601
+ * review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
35602
+ presented: external_exports.boolean(),
35284
35603
  questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
35285
35604
  /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
35286
35605
  * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
@@ -35288,10 +35607,36 @@ var CallRecapSchema2 = external_exports.object({
35288
35607
  * neither wrote one. */
35289
35608
  /** `about` is the request the line answered (its question), null for words that answered none —
35290
35609
  * the key the screen groups on, so one question is one row however many times it was answered. */
35291
- lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), summary: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
35610
+ /** `reply` is the reply the line makes up (an answer, an ok, a deferral), so one reply is one row
35611
+ * however many lines it cites; `at` is the call lines it came from, so a filed line an answer already
35612
+ * said is not drawn again (owner, 2026-10-07: "fix them all"); `replyKind` says which replies are answers. */
35613
+ lines: external_exports.array(external_exports.object({
35614
+ entryId: external_exports.string().uuid(),
35615
+ words: external_exports.string(),
35616
+ summary: external_exports.string().optional(),
35617
+ about: external_exports.string().nullable().optional(),
35618
+ reply: external_exports.string().nullable().optional(),
35619
+ replyKind: external_exports.string().nullable().optional(),
35620
+ at: external_exports.array(external_exports.number()).optional()
35621
+ }))
35292
35622
  })),
35293
35623
  unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
35294
- more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
35624
+ /** THE PUBLIC TRANSCRIPT — every line said on the call, in the order it was said, and WHO SAID IT
35625
+ * (owner, 2026-10-08: "the public view should only have the lines and who said them, and that's
35626
+ * it"; `docs/brain/phase-three/modular.md` build step 3). `speaker` is the role: "user" is the
35627
+ * person, "agent" an agent speaking, named in `agent`. The timeline's action lines are ours, not
35628
+ * theirs, and are not here; nor is a sealed line, which the server has no words for. It stands
35629
+ * BESIDE `topics` and `unfiled` — what the call did with the words — never instead of them. */
35630
+ said: external_exports.array(external_exports.object({
35631
+ lineId: external_exports.string().uuid(),
35632
+ speaker: external_exports.enum(["user", "agent"]),
35633
+ agent: external_exports.object({ id: external_exports.string().nullable(), name: external_exports.string().nullable() }).nullable(),
35634
+ words: external_exports.string(),
35635
+ atMs: external_exports.number()
35636
+ })),
35637
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
35638
+ /** Last stored position on this page when another exists; absent before paging shipped. */
35639
+ next: external_exports.number().int().nonnegative().nullable().optional()
35295
35640
  });
35296
35641
 
35297
35642
  // src/paigy/activity.ts
@@ -35365,9 +35710,9 @@ function limitIn(text, now) {
35365
35710
  if (!line) return null;
35366
35711
  const reset = resetOf(line, now);
35367
35712
  if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
35368
- return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock3(reset) };
35713
+ return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock4(reset) };
35369
35714
  }
35370
- return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
35715
+ return { until: now + RETRY_MS2, resets: clock4(now + RETRY_MS2) };
35371
35716
  }
35372
35717
  function resetOf(line, now) {
35373
35718
  const m = /resets\s+(?:at\s+)?(?:([a-z]{3})[a-z]*\s+(\d{1,2}),?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b(?:\s*\(([^)]+)\))?/i.exec(line);
@@ -35411,7 +35756,7 @@ function wallToEpoch(year, month, day, hour, minute, zone) {
35411
35756
  const first = wall - offset(wall);
35412
35757
  return wall - offset(first);
35413
35758
  }
35414
- function clock3(t) {
35759
+ function clock4(t) {
35415
35760
  return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
35416
35761
  }
35417
35762
  var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
@@ -35455,7 +35800,7 @@ function episodes(opts = {}) {
35455
35800
  episode.called = true;
35456
35801
  const rest = episode.paused.filter((n) => !episode.running.includes(n));
35457
35802
  const text = [
35458
- `Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
35803
+ `Claude's usage limit paused ${names(episode.paused)} from ${clock4(episode.since)}.`,
35459
35804
  rest.length ? `It has reset: ${names(episode.running)} ${episode.running.length > 1 ? "are" : "is"} running again, and ${names(rest)} ${rest.length > 1 ? "haven't" : "hasn't"} started again yet.` : `It has reset, and ${episode.paused.length > 1 ? "they have all" : "it has"} carried on.`
35460
35805
  ].join(" ");
35461
35806
  void call(text).then(
@@ -35948,7 +36293,7 @@ function send2(session, state, text) {
35948
36293
  session.send(text);
35949
36294
  }
35950
36295
  async function pump(session, state, rail, opts) {
35951
- const clock4 = opts.beat ?? beat();
36296
+ const clock5 = opts.beat ?? beat();
35952
36297
  const now = opts.now ?? Date.now;
35953
36298
  const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
35954
36299
  const tally = { noted: 0, seen: 0, announced: 0 };
@@ -35980,7 +36325,7 @@ async function pump(session, state, rail, opts) {
35980
36325
  counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
35981
36326
  since = now();
35982
36327
  }
35983
- cause = await clock4.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
36328
+ cause = await clock5.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
35984
36329
  if (!await opts.onIdle()) return;
35985
36330
  }
35986
36331
  }
@@ -35990,7 +36335,7 @@ function runHarness(opts) {
35990
36335
  let running = true;
35991
36336
  let tail = [];
35992
36337
  const state = { resting: true };
35993
- const clock4 = beat();
36338
+ const clock5 = beat();
35994
36339
  let session = null;
35995
36340
  const speaksAs = opts.session ?? opts.resume ?? sessionId();
35996
36341
  const instance = randomUUID4();
@@ -36051,7 +36396,7 @@ ${event.message}`, state, rail, { log: opts.log });
36051
36396
  stop2();
36052
36397
  return;
36053
36398
  } else if (carried) runningAgain();
36054
- clock4.nudge("rest");
36399
+ clock5.nudge("rest");
36055
36400
  }
36056
36401
  }
36057
36402
  session = startSession({
@@ -36070,13 +36415,13 @@ ${event.message}`, state, rail, { log: opts.log });
36070
36415
  },
36071
36416
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
36072
36417
  });
36073
- void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
36418
+ void pump(session, state, rail, { onIdle: () => running, beat: clock5, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
36074
36419
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
36075
36420
  );
36076
36421
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
36077
36422
  function stop2() {
36078
36423
  running = false;
36079
- clock4.nudge("wake");
36424
+ clock5.nudge("wake");
36080
36425
  tail = [];
36081
36426
  cancelAsks(state);
36082
36427
  session?.stop();
@@ -36091,7 +36436,7 @@ ${event.message}`, state, rail, { log: opts.log });
36091
36436
  working: () => running && state.resting !== true,
36092
36437
  // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
36093
36438
  tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
36094
- nudge: () => clock4.nudge("wake"),
36439
+ nudge: () => clock5.nudge("wake"),
36095
36440
  stop: stop2
36096
36441
  };
36097
36442
  }
@@ -36245,10 +36590,14 @@ function localHolderPids(deps = {}) {
36245
36590
  if (!/^\d+\.json$/.test(file)) continue;
36246
36591
  let row;
36247
36592
  try {
36248
- row = JSON.parse(read3(join7(dir, file)));
36593
+ row = parseSessionRecord(read3(join7(dir, file)));
36249
36594
  } catch {
36250
36595
  continue;
36251
36596
  }
36597
+ if (!row) {
36598
+ deps.unreadable?.(file);
36599
+ continue;
36600
+ }
36252
36601
  const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
36253
36602
  if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
36254
36603
  if (!holds(pid, row.procStart, check)) continue;
@@ -36290,10 +36639,14 @@ function localHolders(deps = {}) {
36290
36639
  if (!/^\d+\.json$/.test(file)) continue;
36291
36640
  let row;
36292
36641
  try {
36293
- row = JSON.parse(read3(join7(dir, file)));
36642
+ row = parseSessionRecord(read3(join7(dir, file)));
36294
36643
  } catch {
36295
36644
  continue;
36296
36645
  }
36646
+ if (!row) {
36647
+ deps.unreadable?.(file);
36648
+ continue;
36649
+ }
36297
36650
  const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
36298
36651
  if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
36299
36652
  const slot = sessionSlot(row.sessionId);
@@ -36544,13 +36897,12 @@ function evict(runs, key3, run) {
36544
36897
  if (runs.get(key3)?.run === run) runs.delete(key3);
36545
36898
  }
36546
36899
  var PRESENCE_FRESH_MS = 3 * 6e4;
36547
- var IDLE_HOLDER_MS = 30 * 6e4;
36548
36900
  function wakeAction(live, now, serverSeenAt, here = {}) {
36549
36901
  if (live) return { action: "skip", why: "run" };
36550
36902
  if (here.holder) return { action: "skip", why: "holder" };
36551
36903
  if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
36552
36904
  if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
36553
- if (here.movedAt !== void 0 && now - here.movedAt >= IDLE_HOLDER_MS) return { action: "spawn", why: "idle" };
36905
+ if (here.movedAt !== void 0 && now - here.movedAt >= WORKING_MS2 && !isWorking(here, now)) return { action: "spawn", why: "idle" };
36554
36906
  return { action: "skip", why: "presence" };
36555
36907
  }
36556
36908
  return { action: "spawn", why: "nobody" };
@@ -36705,7 +37057,7 @@ function startHost(opts) {
36705
37057
  const presence = /* @__PURE__ */ new Map();
36706
37058
  try {
36707
37059
  for (const c of (await listConnections(asHost)).items) {
36708
- if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
37060
+ presence.set(c.id, c);
36709
37061
  }
36710
37062
  lastPresence = presence;
36711
37063
  } catch {
@@ -36715,9 +37067,14 @@ function startHost(opts) {
36715
37067
  let lastPresence = /* @__PURE__ */ new Map();
36716
37068
  async function evidence() {
36717
37069
  const presence = await presenceMap();
36718
- lastHolders = localHolders();
37070
+ lastHolders = localHolders({ unreadable: (file) => {
37071
+ if (unreadable.has(file)) return;
37072
+ unreadable.add(file);
37073
+ opts.log(`\u25CC session record ${file} has no object in it \u2014 the process it names reads as not running here`);
37074
+ } });
36719
37075
  return { presence, holders: lastHolders };
36720
37076
  }
37077
+ const unreadable = /* @__PURE__ */ new Set();
36721
37078
  let lastHolders = /* @__PURE__ */ new Map();
36722
37079
  async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
36723
37080
  {
@@ -36733,11 +37090,12 @@ function startHost(opts) {
36733
37090
  }
36734
37091
  }
36735
37092
  const tokenId = slotIdentity(slot).tokenId;
36736
- const seenByServer = tokenId ? presence.get(tokenId) : void 0;
37093
+ const connection = tokenId ? presence.get(tokenId) : void 0;
37094
+ const seenByServer = connection?.lastSeenAt ? Date.parse(connection.lastSeenAt) : void 0;
36737
37095
  const holder = holders.get(slot);
36738
37096
  if (holder) heldHere.add(slot);
36739
37097
  else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
36740
- const here = { holder, goneAt: goneAt.get(slot) };
37098
+ const here = { holder, goneAt: goneAt.get(slot), lastWorkedAt: connection?.lastWorkedAt };
36741
37099
  let decided = wakeAction(!!live, Date.now(), seenByServer, here);
36742
37100
  const label = slotName(slot) ?? slot;
36743
37101
  if (decided.action === "skip" && decided.why !== "presence") {
@@ -36780,7 +37138,7 @@ function startHost(opts) {
36780
37138
  const since = found.since;
36781
37139
  decided = wakeAction(false, Date.now(), seenByServer, { ...here, ...since ? { movedAt: Date.parse(since) } : {} });
36782
37140
  if (decided.action === "skip") {
36783
- const said = `is working somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; taken over once it sits ${IDLE_HOLDER_MS / 6e4} min untouched` : ", with nothing to say when it last moved"}`;
37141
+ const said = `is connected somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; takeover requires ${WORKING_MS2 / 6e4} min without own activity or waiting-work movement` : ", with nothing to say when it last moved"}`;
36784
37142
  opts.log(`\u25CC ${label} ${said} \u2014 not spawning`);
36785
37143
  decide(slot, "stood_back", found, seenByServer, said);
36786
37144
  return;
@@ -36949,7 +37307,7 @@ function startHost(opts) {
36949
37307
  // sessions we started ourselves. The server already knows: `last_seen_at` on the
36950
37308
  // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
36951
37309
  // hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
36952
- running: live.has(name) || !!slot && lastHolders.has(slot) || !!id3.tokenId && Date.now() - (lastPresence.get(id3.tokenId) ?? 0) < PRESENCE_FRESH_MS,
37310
+ running: live.has(name) || !!slot && lastHolders.has(slot) || !!id3.tokenId && Date.now() - Date.parse(lastPresence.get(id3.tokenId)?.lastSeenAt ?? "") < PRESENCE_FRESH_MS,
36953
37311
  // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
36954
37312
  // thing a tail can tell us. A process we did not spawn has no tail here to read.
36955
37313
  working: live.get(name)?.working() ?? false