@paigy/harness 0.3.29 → 0.3.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -9975,7 +9975,20 @@ var AskInputSchema = external_exports.object({
9975
9975
  ask: external_exports.string().trim().min(1).max(1e4).describe(
9976
9976
  "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
9977
9977
  ),
9978
- options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
9978
+ options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
9979
+ // A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
9980
+ // checklist mode of answering"). The answer shape was taken off this surface with the wire form
9981
+ // (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
9982
+ // only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
9983
+ // as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
9984
+ // could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
9985
+ // not: whether its own options exclude each other.
9986
+ select: external_exports.enum(["one", "many"]).optional().describe(
9987
+ 'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
9988
+ ),
9989
+ answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
9990
+ "Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
9991
+ )
9979
9992
  }).strict();
9980
9993
  var StartContactSchema = external_exports.object({
9981
9994
  asks: external_exports.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
@@ -9984,7 +9997,7 @@ var StartContactSchema = external_exports.object({
9984
9997
  }).strict();
9985
9998
  var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
9986
9999
  var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
9987
- var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
10000
+ var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. 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'); when the options are independent and they may want several, pass select:'many' and they get a checklist. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
9988
10001
  var CreateGoalSchema = external_exports.object({
9989
10002
  outcome: external_exports.string().trim().min(1).max(1e4),
9990
10003
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -10040,11 +10053,11 @@ var GetGoalSchema = external_exports.object({
10040
10053
  history: external_exports.boolean().optional(),
10041
10054
  /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
10042
10055
  * about this work, each answer attributed to the reader that gave it, and every disagreement
10043
- * between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
10056
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
10044
10057
  diagnose: external_exports.boolean().optional()
10045
10058
  }).strict();
10046
- var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. 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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
10047
- var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
10059
+ var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
10060
+ var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
10048
10061
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
10049
10062
  var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
10050
10063
  var CheckRepliesSchema = external_exports.object({}).strict();
@@ -10058,6 +10071,7 @@ var AGENT_TOOLS = [
10058
10071
  ];
10059
10072
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
10060
10073
  var LIVE_MS = 3 * 6e4;
10074
+ var WORKING_MS = 30 * 6e4;
10061
10075
  var ContextSchema = external_exports.object({
10062
10076
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
10063
10077
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -10128,7 +10142,7 @@ var AttentionSchema = external_exports.object({
10128
10142
  points: external_exports.array(external_exports.string()).nullable(),
10129
10143
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
10130
10144
  blocking: external_exports.boolean(),
10131
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
10145
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
10132
10146
  * slice wires it; optional so today's rows/callers project cleanly. */
10133
10147
  deadline: external_exports.string().datetime().nullable().optional()
10134
10148
  });
@@ -10179,7 +10193,7 @@ var NotifyRequestFields = external_exports.object({
10179
10193
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
10180
10194
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
10181
10195
  // the ask into topics and gives each one at most three sentences and one question
10182
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
10196
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
10183
10197
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
10184
10198
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
10185
10199
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -10256,7 +10270,7 @@ var UserAnswerSchema = external_exports.discriminatedUnion("kind", [
10256
10270
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
10257
10271
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
10258
10272
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
10259
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
10273
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
10260
10274
  * delivered through the same settle/await path as a human answer, carrying the judge's
10261
10275
  * derivation and the precedent ids it grew from. Always paired with a visible trail
10262
10276
  * card the user can reply to — the broker never overrides the user. */
@@ -10305,7 +10319,7 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
10305
10319
  * and now knows exactly which call to make. Absent when either half is missing —
10306
10320
  * a sentence with a hole in it is worse than no sentence. */
10307
10321
  note: external_exports.string().optional(),
10308
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
10322
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
10309
10323
  * shaped answer was mapped from, filtered to its own claims. There is no second list
10310
10324
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
10311
10325
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -10401,10 +10415,11 @@ var AgendaTurnSchema = external_exports.object({
10401
10415
  agentId: external_exports.string().optional(),
10402
10416
  select: SelectShapeSchema.optional(),
10403
10417
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
10404
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
10405
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
10406
- * (the beat for context, the answer window for asks). Clamped bot-side. */
10407
- pace: external_exports.number().positive().optional(),
10418
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
10419
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
10420
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
10421
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
10422
+ anything. A caller's own `beat_s` tuning is what it used to override. */
10408
10423
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
10409
10424
  * (a question blocks, context flows). blocking:false on a question = ask and move
10410
10425
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -10439,7 +10454,7 @@ var InboxItemSchema = external_exports.object({
10439
10454
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
10440
10455
  * `createdAt`. */
10441
10456
  agentStateAt: external_exports.string().datetime().optional(),
10442
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
10457
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
10443
10458
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
10444
10459
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
10445
10460
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -10500,7 +10515,7 @@ var InboxItemSchema = external_exports.object({
10500
10515
  * off the same open list the card came from, so it clears when the Call does. A card is the
10501
10516
  * backup for a call not taken; while the call has it, the call is where it is answered. */
10502
10517
  onCall: external_exports.literal(true).optional(),
10503
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
10518
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
10504
10519
  * declined, and what the ladder will do next — read off the cron's own row, never computed
10505
10520
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
10506
10521
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -10531,7 +10546,7 @@ var InboxItemSchema = external_exports.object({
10531
10546
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
10532
10547
  ),
10533
10548
  /** Real downstream work is stuck behind this one — set by the agent, independent of
10534
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
10549
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
10535
10550
  * inbox's blocking badge and the extra confirm step before dismissing it. */
10536
10551
  blocking: external_exports.boolean().default(false),
10537
10552
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -10605,9 +10620,17 @@ var UserSettingsSchema = external_exports.object({
10605
10620
  banner: external_exports.boolean(),
10606
10621
  push: external_exports.boolean()
10607
10622
  }),
10623
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
10624
+ * they are an enum on a live column across every account, and the rename is a rename of
10625
+ * what people read (owner, 2026-09-30). */
10608
10626
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
10609
- silentPush: external_exports.boolean(),
10610
- autoCallback: external_exports.boolean(),
10627
+ /** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
10628
+ * optional long after DateNight stopped reading it, on the theory that a phone on an older
10629
+ * bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
10630
+ * wanting. That theory was wrong about this schema: these are plain `z.object`s with no
10631
+ * `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
10632
+ * an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
10633
+ * next dead field for the same reason. */
10611
10634
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
10612
10635
  improveConsent: external_exports.boolean(),
10613
10636
  missedCall: MissedCallSchema.default("backoff_standard"),
@@ -10618,12 +10641,16 @@ var UserSettingsSchema = external_exports.object({
10618
10641
  * must not silently reset this privacy choice. Absent = leave unchanged on
10619
10642
  * write, 'hosted' on read (see store.ts). */
10620
10643
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
10621
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
10644
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
10622
10645
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
10623
10646
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
10624
10647
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
10625
10648
  * read (see store.ts). */
10626
10649
  talk: external_exports.boolean().optional(),
10650
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
10651
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
10652
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
10653
+ callDiagnostics: external_exports.boolean().optional(),
10627
10654
  /** Per-user ring budget (#603): calls per rolling day before further calls
10628
10655
  * degrade to banner. Absent = the global default (25). A number, never a
10629
10656
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -10632,9 +10659,6 @@ var UserSettingsSchema = external_exports.object({
10632
10659
  * slower speaker). No API-side semantics; the bot resolves each key with its
10633
10660
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
10634
10661
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
10635
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
10636
- * defaulted — an older client PATCHing the full object must not clobber it. */
10637
- pstnCalls: external_exports.boolean().optional(),
10638
10662
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
10639
10663
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
10640
10664
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -10678,7 +10702,7 @@ var ConnectionSummarySchema = external_exports.object({
10678
10702
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
10679
10703
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
10680
10704
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
10681
- * apps/api/src/tokens/devices-vs-agents-design.md. */
10705
+ * docs/server/tokens/devices-vs-agents-design.md. */
10682
10706
  kind: external_exports.enum(["device", "agent"]).optional(),
10683
10707
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
10684
10708
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -10696,7 +10720,7 @@ var ConnectionSummarySchema = external_exports.object({
10696
10720
  * user on the agent's own page. null = no ceiling (today's behaviour for every
10697
10721
  * connection). Android binds importance to a relationship rather than to each message,
10698
10722
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
10699
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
10723
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
10700
10724
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
10701
10725
  * set once must not overrule a rule they set about one agent. */
10702
10726
  reach: NotifyLevelSchema.nullable().optional(),
@@ -10707,7 +10731,14 @@ var ConnectionSummarySchema = external_exports.object({
10707
10731
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
10708
10732
  * desktop app while open. Null = never seen; stale = offline. */
10709
10733
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
10710
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
10734
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
10735
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
10736
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
10737
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
10738
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
10739
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
10740
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
10741
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
10711
10742
  * harness availabilities + granted workspaces — the option set the phone's
10712
10743
  * "new session" sheet offers. Absent for ordinary MCP agents. */
10713
10744
  runtime: external_exports.object({
@@ -10715,7 +10746,12 @@ var ConnectionSummarySchema = external_exports.object({
10715
10746
  * shows its age here (`apps/desktop/src/update.ts`). */
10716
10747
  version: external_exports.string().optional(),
10717
10748
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
10718
- workspaces: external_exports.array(external_exports.string()).optional()
10749
+ workspaces: external_exports.array(external_exports.string()).optional(),
10750
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
10751
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
10752
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
10753
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
10754
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
10719
10755
  }).optional(),
10720
10756
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
10721
10757
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -10728,7 +10764,8 @@ var ConnectionSummarySchema = external_exports.object({
10728
10764
  });
10729
10765
  var LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
10730
10766
  var AgentLedgerSchema = external_exports.object({
10731
- agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
10767
+ /** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
10768
+ agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
10732
10769
  /** Its own questions you have not answered. */
10733
10770
  asks: external_exports.array(LedgerItemSchema),
10734
10771
  /** Its questions you answered that nobody acted on — still owed to somebody. */
@@ -10798,7 +10835,7 @@ var QueueQuestionSchema = external_exports.object({
10798
10835
  * for a settled question whose reply carried nothing readable. */
10799
10836
  answer: external_exports.string().nullable().default(null),
10800
10837
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
10801
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
10838
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
10802
10839
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
10803
10840
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
10804
10841
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -10890,7 +10927,19 @@ var QueueItemSchema = external_exports.object({
10890
10927
  said: external_exports.string(),
10891
10928
  at: external_exports.string(),
10892
10929
  entryId: external_exports.string()
10893
- }).nullable().optional()
10930
+ }).nullable().optional(),
10931
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
10932
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
10933
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
10934
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
10935
+ * 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
10936
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
10937
+ *
10938
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
10939
+ * minute after the person speaks erases their instant from it, and the durable traces the client
10940
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
10941
+ * Entry with no `about_id` is in neither. */
10942
+ lastPersonAt: external_exports.string().nullable().optional()
10894
10943
  });
10895
10944
  var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
10896
10945
  var NoteSourceSchema = external_exports.enum(["app", "call"]);
@@ -10920,7 +10969,7 @@ var NoteSchema = external_exports.object({
10920
10969
  assignee: external_exports.string().nullable(),
10921
10970
  /** The request thread minted at assignment; null until assigned. */
10922
10971
  parentId: external_exports.string().nullable(),
10923
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
10972
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
10924
10973
  * call — never a deadline. It only ever comes from the user's own words, so when it
10925
10974
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
10926
10975
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -11182,6 +11231,31 @@ var SnapshotSchema = external_exports.object({
11182
11231
  computers: external_exports.array(ComputerRowSchema).nullable()
11183
11232
  }).nullable()
11184
11233
  });
11234
+ var CallRecapSchema = external_exports.object({
11235
+ call: external_exports.object({
11236
+ status: external_exports.string(),
11237
+ startedAt: external_exports.string(),
11238
+ durationMs: external_exports.number().nullable(),
11239
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
11240
+ }),
11241
+ topics: external_exports.array(external_exports.object({
11242
+ goalId: external_exports.string().uuid(),
11243
+ title: external_exports.string(),
11244
+ owner: external_exports.string(),
11245
+ state: external_exports.string(),
11246
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
11247
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
11248
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
11249
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
11250
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
11251
+ * anything that is not an answer. */
11252
+ /** `about` is the request the line answered (its question), null for words that answered none —
11253
+ * the key the screen groups on, so one question is one row however many times it was answered. */
11254
+ 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() }))
11255
+ })),
11256
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
11257
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
11258
+ });
11185
11259
  function sessionSlot(sessionId2) {
11186
11260
  const id = sessionId2 ?? sessionId();
11187
11261
  return `session:${id.slice(0, 8)}`;
@@ -11376,6 +11450,22 @@ async function claimSessions(opts = {}) {
11376
11450
  if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
11377
11451
  return (await res.json()).sessions;
11378
11452
  }
11453
+ async function claimSessionEnds(opts = {}) {
11454
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/claim`, {
11455
+ headers: { authorization: `Bearer ${authToken(opts.token)}` }
11456
+ // the HOST's identity
11457
+ }));
11458
+ if (!res.ok) throw new Error(`claim_session_ends failed: ${res.status}`);
11459
+ return (await res.json()).ends ?? [];
11460
+ }
11461
+ async function finishSessionEnd(endId, result, opts = {}) {
11462
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/${encodeURIComponent(endId)}`, {
11463
+ method: "POST",
11464
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
11465
+ body: JSON.stringify({ result })
11466
+ }));
11467
+ if (!res.ok) await fail("finish_session_end", res);
11468
+ }
11379
11469
  async function recordDecision(decision, opts = {}) {
11380
11470
  const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
11381
11471
  method: "POST",
@@ -11499,7 +11589,9 @@ async function contact(input, opts = {}) {
11499
11589
  // this cannot see.
11500
11590
  repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
11501
11591
  ask: a.ask,
11502
- options: a.options ?? []
11592
+ options: a.options ?? [],
11593
+ ...a.select ? { select: a.select } : {},
11594
+ ...a.answers ? { answers: a.answers } : {}
11503
11595
  }));
11504
11596
  const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
11505
11597
  method: "POST",
@@ -12391,7 +12483,7 @@ async function brokerView(deps) {
12391
12483
  }
12392
12484
 
12393
12485
  // src/workspaces.ts
12394
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
12486
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
12395
12487
  import { dirname as dirname2, join as join5, resolve } from "path";
12396
12488
  import { homedir as homedir5 } from "os";
12397
12489
  function workspacesFile() {
@@ -12432,10 +12524,41 @@ function resolveWakeDir(pinned, deps) {
12432
12524
  }
12433
12525
  return listWorkspaces(deps)[0];
12434
12526
  }
12527
+ function originOf(dir) {
12528
+ try {
12529
+ if (!statSync2(join5(dir, ".git")).isDirectory()) return null;
12530
+ const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
12531
+ const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
12532
+ return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
12533
+ } catch {
12534
+ return null;
12535
+ }
12536
+ }
12537
+ var MAX_CHILDREN = 300;
12538
+ var MAX_REPOS = 100;
12539
+ function listRepos(deps) {
12540
+ const repos = [];
12541
+ for (const root of listWorkspaces(deps)) {
12542
+ const own = originOf(root);
12543
+ if (own) repos.push({ path: root, remote: own });
12544
+ let children = [];
12545
+ try {
12546
+ children = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, MAX_CHILDREN);
12547
+ } catch {
12548
+ }
12549
+ for (const name of children) {
12550
+ if (repos.length >= MAX_REPOS) return repos;
12551
+ const path = join5(root, name);
12552
+ const remote = originOf(path);
12553
+ if (remote) repos.push({ path, remote });
12554
+ }
12555
+ }
12556
+ return repos.slice(0, MAX_REPOS);
12557
+ }
12435
12558
 
12436
12559
  // src/holder.ts
12437
12560
  import { execFileSync as execFileSync3 } from "child_process";
12438
- import { readFileSync as readFileSync4, readdirSync } from "fs";
12561
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
12439
12562
  import { homedir as homedir6 } from "os";
12440
12563
  import { join as join6 } from "path";
12441
12564
  function isAlive(pid) {
@@ -12466,9 +12589,62 @@ function holds(pid, procStart, deps) {
12466
12589
  }
12467
12590
  return false;
12468
12591
  }
12592
+ function proven(pid, procStart, deps) {
12593
+ if (!deps.alive(pid) || typeof procStart !== "string" || !procStart.trim()) return false;
12594
+ const want = squash(procStart);
12595
+ return ["UTC", "local"].some((zone) => {
12596
+ const got = deps.started(pid, zone);
12597
+ return got !== null && squash(got) === want;
12598
+ });
12599
+ }
12600
+ function localHolderPids(deps = {}) {
12601
+ const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
12602
+ const list = deps.list ?? ((d) => readdirSync2(d));
12603
+ const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
12604
+ const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
12605
+ const held = /* @__PURE__ */ new Map();
12606
+ let files;
12607
+ try {
12608
+ files = list(dir);
12609
+ } catch {
12610
+ return held;
12611
+ }
12612
+ for (const file of files) {
12613
+ if (!/^\d+\.json$/.test(file)) continue;
12614
+ let row;
12615
+ try {
12616
+ row = JSON.parse(read(join6(dir, file)));
12617
+ } catch {
12618
+ continue;
12619
+ }
12620
+ const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
12621
+ if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
12622
+ if (!holds(pid, row.procStart, check)) continue;
12623
+ const slot = sessionSlot(row.sessionId);
12624
+ held.set(slot, [...held.get(slot) ?? [], { pid, proven: proven(pid, row.procStart, check) }]);
12625
+ }
12626
+ return held;
12627
+ }
12628
+ function psParent(pid) {
12629
+ try {
12630
+ const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
12631
+ const n = Number(out);
12632
+ return Number.isInteger(n) && n > 0 ? n : null;
12633
+ } catch {
12634
+ return null;
12635
+ }
12636
+ }
12637
+ function descendsFrom(pid, ancestor, parent = psParent) {
12638
+ let at = pid;
12639
+ for (let i = 0; i < 32 && at !== null && at > 1; i += 1) {
12640
+ if (at === ancestor) return true;
12641
+ at = parent(at);
12642
+ }
12643
+ return false;
12644
+ }
12469
12645
  function localHolders(deps = {}) {
12470
12646
  const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
12471
- const list = deps.list ?? ((d) => readdirSync(d));
12647
+ const list = deps.list ?? ((d) => readdirSync2(d));
12472
12648
  const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
12473
12649
  const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
12474
12650
  const held = /* @__PURE__ */ new Map();
@@ -12495,8 +12671,67 @@ function localHolders(deps = {}) {
12495
12671
  return held;
12496
12672
  }
12497
12673
 
12674
+ // src/end.ts
12675
+ var GRACE_MS = 5e3;
12676
+ async function endTerminal(pid, slot, deps) {
12677
+ try {
12678
+ deps.kill(pid, "SIGTERM");
12679
+ } catch (e) {
12680
+ return { ok: false, kind: "terminal", pid, why: `SIGTERM refused: ${e.message}`.slice(0, 200) };
12681
+ }
12682
+ const grace = deps.graceMs ?? GRACE_MS;
12683
+ for (let waited = 0; waited < grace; waited += 250) {
12684
+ if (!deps.alive(pid)) {
12685
+ deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGTERM" })}`);
12686
+ return { ok: true, kind: "terminal", pid, signal: "SIGTERM" };
12687
+ }
12688
+ await deps.sleep(250);
12689
+ }
12690
+ try {
12691
+ deps.kill(pid, "SIGKILL");
12692
+ } catch (e) {
12693
+ return { ok: false, kind: "terminal", pid, why: `SIGKILL refused: ${e.message}`.slice(0, 200) };
12694
+ }
12695
+ deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGKILL" })}`);
12696
+ return { ok: true, kind: "terminal", pid, signal: "SIGKILL" };
12697
+ }
12698
+ async function endSession(end, deps) {
12699
+ const slots = deps.slotsFor(end.agent);
12700
+ if (!slots.length) return { ok: false, why: "not_on_this_host" };
12701
+ let ended2 = null;
12702
+ if (end.which === "harness" || end.which === "any") {
12703
+ for (const slot of slots) {
12704
+ const run = deps.ownRun(slot);
12705
+ if (!run) continue;
12706
+ run.stop();
12707
+ deps.log(`session_ended ${JSON.stringify({ sessionId: slot, kind: "harness", signal: "stop" })}`);
12708
+ ended2 = { ok: true, kind: "harness", signal: "stop" };
12709
+ }
12710
+ if (end.which === "harness" && !ended2) return { ok: false, kind: "harness", why: "no_harness_copy_here" };
12711
+ }
12712
+ if (end.which === "terminal" || end.which === "any") {
12713
+ let terminal = null;
12714
+ let unproven = null;
12715
+ for (const slot of slots) {
12716
+ for (const { pid, proven: proven2 } of deps.pidsFor(slot).filter((p) => !deps.isOwn(p.pid))) {
12717
+ if (!proven2) {
12718
+ deps.log(`session_end_refused ${JSON.stringify({ sessionId: slot, pid, why: "no_proc_start" })}`);
12719
+ unproven = { ok: false, kind: "terminal", pid, why: "no_proc_start" };
12720
+ continue;
12721
+ }
12722
+ terminal = await endTerminal(pid, slot, deps);
12723
+ if (!terminal.ok) return terminal;
12724
+ }
12725
+ }
12726
+ if (terminal) return terminal;
12727
+ if (unproven) return unproven;
12728
+ if (end.which === "terminal") return { ok: false, kind: "terminal", why: "no_terminal_copy_here" };
12729
+ }
12730
+ return ended2 ?? { ok: false, why: "nothing_running_here" };
12731
+ }
12732
+
12498
12733
  // src/owner.ts
12499
- import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync2 } from "fs";
12734
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
12500
12735
  import { homedir as homedir7 } from "os";
12501
12736
  import { join as join7 } from "path";
12502
12737
  var HEAD_BYTES = 64e3;
@@ -12515,7 +12750,7 @@ function ownerSession(slot, deps = {}) {
12515
12750
  if (!m) return null;
12516
12751
  const prefix = m[1];
12517
12752
  const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
12518
- const list = deps.list ?? ((dir) => readdirSync2(dir));
12753
+ const list = deps.list ?? ((dir) => readdirSync3(dir));
12519
12754
  const head = deps.head ?? readHead;
12520
12755
  let folders;
12521
12756
  try {
@@ -13891,7 +14126,20 @@ var AskInputSchema2 = external_exports.object({
13891
14126
  ask: external_exports.string().trim().min(1).max(1e4).describe(
13892
14127
  "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
13893
14128
  ),
13894
- options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
14129
+ options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
14130
+ // A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
14131
+ // checklist mode of answering"). The answer shape was taken off this surface with the wire form
14132
+ // (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
14133
+ // only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
14134
+ // as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
14135
+ // could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
14136
+ // not: whether its own options exclude each other.
14137
+ select: external_exports.enum(["one", "many"]).optional().describe(
14138
+ 'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
14139
+ ),
14140
+ answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
14141
+ "Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
14142
+ )
13895
14143
  }).strict();
13896
14144
  var StartContactSchema2 = external_exports.object({
13897
14145
  asks: external_exports.array(AskInputSchema2).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
@@ -13900,7 +14148,7 @@ var StartContactSchema2 = external_exports.object({
13900
14148
  }).strict();
13901
14149
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
13902
14150
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
13903
- var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
14151
+ var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. On a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so). Send options (or waiting:'hard') when you actually need an answer. 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'); when the options are independent and they may want several, pass select:'many' and they get a checklist. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
13904
14152
  var CreateGoalSchema2 = external_exports.object({
13905
14153
  outcome: external_exports.string().trim().min(1).max(1e4),
13906
14154
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -13956,11 +14204,11 @@ var GetGoalSchema2 = external_exports.object({
13956
14204
  history: external_exports.boolean().optional(),
13957
14205
  /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
13958
14206
  * about this work, each answer attributed to the reader that gave it, and every disagreement
13959
- * between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
14207
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
13960
14208
  diagnose: external_exports.boolean().optional()
13961
14209
  }).strict();
13962
- var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. 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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
13963
- var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
14210
+ var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `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. Another account's Goals, and another agent's Goal that is not below one of yours, 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.";
14211
+ var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
13964
14212
  var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
13965
14213
  var CHECK_REPLIES_DESCRIPTION2 = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
13966
14214
  var CheckRepliesSchema2 = external_exports.object({}).strict();
@@ -13989,6 +14237,7 @@ function entryWords(entry) {
13989
14237
  return entry.sources.map((source) => source.text).join("\n");
13990
14238
  }
13991
14239
  var LIVE_MS2 = 3 * 6e4;
14240
+ var WORKING_MS2 = 30 * 6e4;
13992
14241
  var ContextSchema2 = external_exports.object({
13993
14242
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
13994
14243
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -14059,7 +14308,7 @@ var AttentionSchema2 = external_exports.object({
14059
14308
  points: external_exports.array(external_exports.string()).nullable(),
14060
14309
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
14061
14310
  blocking: external_exports.boolean(),
14062
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
14311
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
14063
14312
  * slice wires it; optional so today's rows/callers project cleanly. */
14064
14313
  deadline: external_exports.string().datetime().nullable().optional()
14065
14314
  });
@@ -14110,7 +14359,7 @@ var NotifyRequestFields2 = external_exports.object({
14110
14359
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
14111
14360
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
14112
14361
  // the ask into topics and gives each one at most three sentences and one question
14113
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
14362
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
14114
14363
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
14115
14364
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
14116
14365
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -14187,7 +14436,7 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
14187
14436
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
14188
14437
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
14189
14438
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
14190
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
14439
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
14191
14440
  * delivered through the same settle/await path as a human answer, carrying the judge's
14192
14441
  * derivation and the precedent ids it grew from. Always paired with a visible trail
14193
14442
  * card the user can reply to — the broker never overrides the user. */
@@ -14236,7 +14485,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
14236
14485
  * and now knows exactly which call to make. Absent when either half is missing —
14237
14486
  * a sentence with a hole in it is worse than no sentence. */
14238
14487
  note: external_exports.string().optional(),
14239
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
14488
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
14240
14489
  * shaped answer was mapped from, filtered to its own claims. There is no second list
14241
14490
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
14242
14491
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -14332,10 +14581,11 @@ var AgendaTurnSchema2 = external_exports.object({
14332
14581
  agentId: external_exports.string().optional(),
14333
14582
  select: SelectShapeSchema2.optional(),
14334
14583
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
14335
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
14336
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
14337
- * (the beat for context, the answer window for asks). Clamped bot-side. */
14338
- pace: external_exports.number().positive().optional(),
14584
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
14585
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
14586
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
14587
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
14588
+ anything. A caller's own `beat_s` tuning is what it used to override. */
14339
14589
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
14340
14590
  * (a question blocks, context flows). blocking:false on a question = ask and move
14341
14591
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -14370,7 +14620,7 @@ var InboxItemSchema2 = external_exports.object({
14370
14620
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
14371
14621
  * `createdAt`. */
14372
14622
  agentStateAt: external_exports.string().datetime().optional(),
14373
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
14623
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
14374
14624
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
14375
14625
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
14376
14626
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -14431,7 +14681,7 @@ var InboxItemSchema2 = external_exports.object({
14431
14681
  * off the same open list the card came from, so it clears when the Call does. A card is the
14432
14682
  * backup for a call not taken; while the call has it, the call is where it is answered. */
14433
14683
  onCall: external_exports.literal(true).optional(),
14434
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
14684
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
14435
14685
  * declined, and what the ladder will do next — read off the cron's own row, never computed
14436
14686
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
14437
14687
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -14462,7 +14712,7 @@ var InboxItemSchema2 = external_exports.object({
14462
14712
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
14463
14713
  ),
14464
14714
  /** Real downstream work is stuck behind this one — set by the agent, independent of
14465
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
14715
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
14466
14716
  * inbox's blocking badge and the extra confirm step before dismissing it. */
14467
14717
  blocking: external_exports.boolean().default(false),
14468
14718
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -14536,9 +14786,17 @@ var UserSettingsSchema2 = external_exports.object({
14536
14786
  banner: external_exports.boolean(),
14537
14787
  push: external_exports.boolean()
14538
14788
  }),
14789
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
14790
+ * they are an enum on a live column across every account, and the rename is a rename of
14791
+ * what people read (owner, 2026-09-30). */
14539
14792
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
14540
- silentPush: external_exports.boolean(),
14541
- autoCallback: external_exports.boolean(),
14793
+ /** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
14794
+ * optional long after DateNight stopped reading it, on the theory that a phone on an older
14795
+ * bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
14796
+ * wanting. That theory was wrong about this schema: these are plain `z.object`s with no
14797
+ * `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
14798
+ * an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
14799
+ * next dead field for the same reason. */
14542
14800
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
14543
14801
  improveConsent: external_exports.boolean(),
14544
14802
  missedCall: MissedCallSchema2.default("backoff_standard"),
@@ -14549,12 +14807,16 @@ var UserSettingsSchema2 = external_exports.object({
14549
14807
  * must not silently reset this privacy choice. Absent = leave unchanged on
14550
14808
  * write, 'hosted' on read (see store.ts). */
14551
14809
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
14552
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
14810
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
14553
14811
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
14554
14812
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
14555
14813
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
14556
14814
  * read (see store.ts). */
14557
14815
  talk: external_exports.boolean().optional(),
14816
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
14817
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
14818
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
14819
+ callDiagnostics: external_exports.boolean().optional(),
14558
14820
  /** Per-user ring budget (#603): calls per rolling day before further calls
14559
14821
  * degrade to banner. Absent = the global default (25). A number, never a
14560
14822
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -14563,9 +14825,6 @@ var UserSettingsSchema2 = external_exports.object({
14563
14825
  * slower speaker). No API-side semantics; the bot resolves each key with its
14564
14826
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
14565
14827
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
14566
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
14567
- * defaulted — an older client PATCHing the full object must not clobber it. */
14568
- pstnCalls: external_exports.boolean().optional(),
14569
14828
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
14570
14829
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
14571
14830
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -14609,7 +14868,7 @@ var ConnectionSummarySchema2 = external_exports.object({
14609
14868
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
14610
14869
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
14611
14870
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
14612
- * apps/api/src/tokens/devices-vs-agents-design.md. */
14871
+ * docs/server/tokens/devices-vs-agents-design.md. */
14613
14872
  kind: external_exports.enum(["device", "agent"]).optional(),
14614
14873
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
14615
14874
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -14627,7 +14886,7 @@ var ConnectionSummarySchema2 = external_exports.object({
14627
14886
  * user on the agent's own page. null = no ceiling (today's behaviour for every
14628
14887
  * connection). Android binds importance to a relationship rather than to each message,
14629
14888
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
14630
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
14889
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
14631
14890
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
14632
14891
  * set once must not overrule a rule they set about one agent. */
14633
14892
  reach: NotifyLevelSchema2.nullable().optional(),
@@ -14638,7 +14897,14 @@ var ConnectionSummarySchema2 = external_exports.object({
14638
14897
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
14639
14898
  * desktop app while open. Null = never seen; stale = offline. */
14640
14899
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
14641
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
14900
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
14901
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
14902
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
14903
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
14904
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
14905
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
14906
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
14907
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
14642
14908
  * harness availabilities + granted workspaces — the option set the phone's
14643
14909
  * "new session" sheet offers. Absent for ordinary MCP agents. */
14644
14910
  runtime: external_exports.object({
@@ -14646,7 +14912,12 @@ var ConnectionSummarySchema2 = external_exports.object({
14646
14912
  * shows its age here (`apps/desktop/src/update.ts`). */
14647
14913
  version: external_exports.string().optional(),
14648
14914
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
14649
- workspaces: external_exports.array(external_exports.string()).optional()
14915
+ workspaces: external_exports.array(external_exports.string()).optional(),
14916
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
14917
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
14918
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
14919
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
14920
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
14650
14921
  }).optional(),
14651
14922
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
14652
14923
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -14659,7 +14930,8 @@ var ConnectionSummarySchema2 = external_exports.object({
14659
14930
  });
14660
14931
  var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
14661
14932
  var AgentLedgerSchema2 = external_exports.object({
14662
- agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
14933
+ /** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
14934
+ agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
14663
14935
  /** Its own questions you have not answered. */
14664
14936
  asks: external_exports.array(LedgerItemSchema2),
14665
14937
  /** Its questions you answered that nobody acted on — still owed to somebody. */
@@ -14729,7 +15001,7 @@ var QueueQuestionSchema2 = external_exports.object({
14729
15001
  * for a settled question whose reply carried nothing readable. */
14730
15002
  answer: external_exports.string().nullable().default(null),
14731
15003
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
14732
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
15004
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
14733
15005
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
14734
15006
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
14735
15007
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -14821,7 +15093,19 @@ var QueueItemSchema2 = external_exports.object({
14821
15093
  said: external_exports.string(),
14822
15094
  at: external_exports.string(),
14823
15095
  entryId: external_exports.string()
14824
- }).nullable().optional()
15096
+ }).nullable().optional(),
15097
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
15098
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
15099
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
15100
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
15101
+ * 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
15102
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
15103
+ *
15104
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
15105
+ * minute after the person speaks erases their instant from it, and the durable traces the client
15106
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
15107
+ * Entry with no `about_id` is in neither. */
15108
+ lastPersonAt: external_exports.string().nullable().optional()
14825
15109
  });
14826
15110
  var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
14827
15111
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
@@ -14851,7 +15135,7 @@ var NoteSchema2 = external_exports.object({
14851
15135
  assignee: external_exports.string().nullable(),
14852
15136
  /** The request thread minted at assignment; null until assigned. */
14853
15137
  parentId: external_exports.string().nullable(),
14854
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
15138
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
14855
15139
  * call — never a deadline. It only ever comes from the user's own words, so when it
14856
15140
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
14857
15141
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -15111,6 +15395,31 @@ var SnapshotSchema2 = external_exports.object({
15111
15395
  computers: external_exports.array(ComputerRowSchema2).nullable()
15112
15396
  }).nullable()
15113
15397
  });
15398
+ var CallRecapSchema2 = external_exports.object({
15399
+ call: external_exports.object({
15400
+ status: external_exports.string(),
15401
+ startedAt: external_exports.string(),
15402
+ durationMs: external_exports.number().nullable(),
15403
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
15404
+ }),
15405
+ topics: external_exports.array(external_exports.object({
15406
+ goalId: external_exports.string().uuid(),
15407
+ title: external_exports.string(),
15408
+ owner: external_exports.string(),
15409
+ state: external_exports.string(),
15410
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
15411
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
15412
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
15413
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
15414
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
15415
+ * anything that is not an answer. */
15416
+ /** `about` is the request the line answered (its question), null for words that answered none —
15417
+ * the key the screen groups on, so one question is one row however many times it was answered. */
15418
+ 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() }))
15419
+ })),
15420
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
15421
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
15422
+ });
15114
15423
 
15115
15424
  // src/paigy/activity.ts
15116
15425
  function shortenPaths(s) {
@@ -15411,7 +15720,7 @@ var AcpDriver = class {
15411
15720
  var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
15412
15721
 
15413
15722
  // src/paigy/inbox.ts
15414
- import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
15723
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
15415
15724
  import { join as join8 } from "path";
15416
15725
  import { homedir as homedir8 } from "os";
15417
15726
  import { randomUUID as randomUUID4 } from "crypto";
@@ -15423,7 +15732,7 @@ function openInbox(dir) {
15423
15732
  const receipt = (name) => join8(dir, "taken", `${name}.note`);
15424
15733
  const taken = (name) => {
15425
15734
  try {
15426
- return statSync2(receipt(name)).mtimeMs;
15735
+ return statSync3(receipt(name)).mtimeMs;
15427
15736
  } catch (error) {
15428
15737
  if (missing(error)) return null;
15429
15738
  throw error;
@@ -15467,7 +15776,7 @@ var alive = (pid) => {
15467
15776
  };
15468
15777
  function inboxFor(root = INBOX_ROOT) {
15469
15778
  try {
15470
- for (const name of readdirSync3(root)) {
15779
+ for (const name of readdirSync4(root)) {
15471
15780
  const pid = Number(name.split("-")[0]);
15472
15781
  if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join8(root, name), { recursive: true, force: true });
15473
15782
  }
@@ -16494,7 +16803,9 @@ function startHost(opts) {
16494
16803
  void heartbeat({
16495
16804
  version: VERSION,
16496
16805
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
16497
- workspaces: listWorkspaces(opts.wsDeps)
16806
+ workspaces: listWorkspaces(opts.wsDeps),
16807
+ // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
16808
+ repos: listRepos(opts.wsDeps)
16498
16809
  }, asHost).catch(() => {
16499
16810
  });
16500
16811
  void listenSlots().then(sweepSlots).catch(() => {
@@ -16540,6 +16851,31 @@ function startHost(opts) {
16540
16851
  void refreshIdentities();
16541
16852
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
16542
16853
  }
16854
+ const ends = await claimSessionEnds(asHost).catch(() => []);
16855
+ for (const end of ends) {
16856
+ const result = await endSession(end, {
16857
+ slotsFor: (agent2) => listSlots().filter((slot) => slot !== "Desktop" && slotIdentity(slot).tokenId === agent2),
16858
+ pidsFor: (slot) => localHolderPids().get(slot) ?? [],
16859
+ ownRun: (slot) => runs.get(runKey(slot))?.run ?? null,
16860
+ isOwn: (pid) => descendsFrom(pid, process.pid),
16861
+ kill: (pid, signal) => {
16862
+ process.kill(pid, signal);
16863
+ },
16864
+ alive: (pid) => {
16865
+ try {
16866
+ process.kill(pid, 0);
16867
+ return true;
16868
+ } catch (e) {
16869
+ return e.code === "EPERM";
16870
+ }
16871
+ },
16872
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
16873
+ log: opts.log
16874
+ }).catch((e) => ({ ok: false, why: `end_threw: ${e.message}`.slice(0, 200) }));
16875
+ opts.log(result.ok ? `\u25A0 ended ${end.which} copy of ${end.agent.slice(0, 8)}` : `\u25CC end of ${end.agent.slice(0, 8)} not done here: ${result.why}`);
16876
+ await finishSessionEnd(end.endId, result, asHost).catch(() => {
16877
+ });
16878
+ }
16543
16879
  }
16544
16880
  async function presenceMap() {
16545
16881
  const presence = /* @__PURE__ */ new Map();
@@ -16564,6 +16900,14 @@ function startHost(opts) {
16564
16900
  const key = runKey(slot);
16565
16901
  if (slot === "Desktop" || isRevoked(slot)) return;
16566
16902
  const live = runs.get(key);
16903
+ if (live) {
16904
+ const theirs = (localHolderPids().get(slot) ?? []).map((p) => p.pid).filter((pid) => !descendsFrom(pid, process.pid));
16905
+ if (theirs.length) {
16906
+ opts.log(`session_stood_back ${JSON.stringify({ sessionId: slot, livePid: theirs[0] })} \u2014 a terminal runs this conversation; stopping this host's copy`);
16907
+ live.run.stop();
16908
+ return;
16909
+ }
16910
+ }
16567
16911
  const tokenId = slotIdentity(slot).tokenId;
16568
16912
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
16569
16913
  const holder = holders.get(slot);
@@ -16849,7 +17193,7 @@ ipcMain.handle("paigy:status", () => ({
16849
17193
  paired: Boolean(deviceToken()),
16850
17194
  device: Boolean(readToken(DEVICE_SLOT)),
16851
17195
  sessions: host?.sessions().map(([, label]) => label) ?? [],
16852
- // The party (companion.md / party design.md): who lives on this machine and what
17196
+ // The party (docs/clients/desktop/companion.md / party design.md): who lives on this machine and what
16853
17197
  // each one is doing THIS SECOND — the one thing the laptop knows better than the phone.
16854
17198
  roster: host?.roster() ?? readHostState()?.roster ?? []
16855
17199
  }));