@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/cli.js CHANGED
@@ -23858,6 +23858,22 @@ async function claimSessions(opts = {}) {
23858
23858
  if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
23859
23859
  return (await res.json()).sessions;
23860
23860
  }
23861
+ async function claimSessionEnds(opts = {}) {
23862
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/claim`, {
23863
+ headers: { authorization: `Bearer ${authToken(opts.token)}` }
23864
+ // the HOST's identity
23865
+ }));
23866
+ if (!res.ok) throw new Error(`claim_session_ends failed: ${res.status}`);
23867
+ return (await res.json()).ends ?? [];
23868
+ }
23869
+ async function finishSessionEnd(endId, result, opts = {}) {
23870
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/${encodeURIComponent(endId)}`, {
23871
+ method: "POST",
23872
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
23873
+ body: JSON.stringify({ result })
23874
+ }));
23875
+ if (!res.ok) await fail("finish_session_end", res);
23876
+ }
23861
23877
  async function recordDecision(decision, opts = {}) {
23862
23878
  const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
23863
23879
  method: "POST",
@@ -23964,7 +23980,9 @@ async function contact(input, opts = {}) {
23964
23980
  // this cannot see.
23965
23981
  repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
23966
23982
  ask: a.ask,
23967
- options: a.options ?? []
23983
+ options: a.options ?? [],
23984
+ ...a.select ? { select: a.select } : {},
23985
+ ...a.answers ? { answers: a.answers } : {}
23968
23986
  }));
23969
23987
  const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
23970
23988
  method: "POST",
@@ -24186,7 +24204,7 @@ async function subscribeWake(onNudge, opts = {}) {
24186
24204
  }
24187
24205
  };
24188
24206
  }
24189
- var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24207
+ var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, WORKING_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, CallRecapSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24190
24208
  var init_dist = __esm({
24191
24209
  "../../packages/sdk/dist/index.js"() {
24192
24210
  "use strict";
@@ -24601,7 +24619,20 @@ var init_dist = __esm({
24601
24619
  ask: external_exports.string().trim().min(1).max(1e4).describe(
24602
24620
  "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."
24603
24621
  ),
24604
- options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
24622
+ options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
24623
+ // A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
24624
+ // checklist mode of answering"). The answer shape was taken off this surface with the wire form
24625
+ // (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
24626
+ // only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
24627
+ // as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
24628
+ // could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
24629
+ // not: whether its own options exclude each other.
24630
+ select: external_exports.enum(["one", "many"]).optional().describe(
24631
+ '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.'
24632
+ ),
24633
+ answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
24634
+ "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."
24635
+ )
24605
24636
  }).strict();
24606
24637
  StartContactSchema = external_exports.object({
24607
24638
  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."),
@@ -24610,7 +24641,7 @@ var init_dist = __esm({
24610
24641
  }).strict();
24611
24642
  ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
24612
24643
  CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
24613
- 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.";
24644
+ 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.";
24614
24645
  CreateGoalSchema = external_exports.object({
24615
24646
  outcome: external_exports.string().trim().min(1).max(1e4),
24616
24647
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -24666,11 +24697,11 @@ var init_dist = __esm({
24666
24697
  history: external_exports.boolean().optional(),
24667
24698
  /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
24668
24699
  * about this work, each answer attributed to the reader that gave it, and every disagreement
24669
- * between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
24700
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
24670
24701
  diagnose: external_exports.boolean().optional()
24671
24702
  }).strict();
24672
- 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.";
24673
- 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.`;
24703
+ 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.";
24704
+ 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.`;
24674
24705
  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.";
24675
24706
  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.";
24676
24707
  CheckRepliesSchema = external_exports.object({}).strict();
@@ -24684,6 +24715,7 @@ var init_dist = __esm({
24684
24715
  ];
24685
24716
  AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
24686
24717
  LIVE_MS = 3 * 6e4;
24718
+ WORKING_MS = 30 * 6e4;
24687
24719
  ContextSchema = external_exports.object({
24688
24720
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
24689
24721
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -24754,7 +24786,7 @@ var init_dist = __esm({
24754
24786
  points: external_exports.array(external_exports.string()).nullable(),
24755
24787
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
24756
24788
  blocking: external_exports.boolean(),
24757
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
24789
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
24758
24790
  * slice wires it; optional so today's rows/callers project cleanly. */
24759
24791
  deadline: external_exports.string().datetime().nullable().optional()
24760
24792
  });
@@ -24805,7 +24837,7 @@ var init_dist = __esm({
24805
24837
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
24806
24838
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
24807
24839
  // the ask into topics and gives each one at most three sentences and one question
24808
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
24840
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
24809
24841
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
24810
24842
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
24811
24843
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -24882,7 +24914,7 @@ var init_dist = __esm({
24882
24914
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
24883
24915
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
24884
24916
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
24885
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
24917
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
24886
24918
  * delivered through the same settle/await path as a human answer, carrying the judge's
24887
24919
  * derivation and the precedent ids it grew from. Always paired with a visible trail
24888
24920
  * card the user can reply to — the broker never overrides the user. */
@@ -24931,7 +24963,7 @@ var init_dist = __esm({
24931
24963
  * and now knows exactly which call to make. Absent when either half is missing —
24932
24964
  * a sentence with a hole in it is worse than no sentence. */
24933
24965
  note: external_exports.string().optional(),
24934
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
24966
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
24935
24967
  * shaped answer was mapped from, filtered to its own claims. There is no second list
24936
24968
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
24937
24969
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -25027,10 +25059,11 @@ var init_dist = __esm({
25027
25059
  agentId: external_exports.string().optional(),
25028
25060
  select: SelectShapeSchema.optional(),
25029
25061
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
25030
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
25031
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
25032
- * (the beat for context, the answer window for asks). Clamped bot-side. */
25033
- pace: external_exports.number().positive().optional(),
25062
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
25063
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
25064
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
25065
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
25066
+ anything. A caller's own `beat_s` tuning is what it used to override. */
25034
25067
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
25035
25068
  * (a question blocks, context flows). blocking:false on a question = ask and move
25036
25069
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -25065,7 +25098,7 @@ var init_dist = __esm({
25065
25098
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
25066
25099
  * `createdAt`. */
25067
25100
  agentStateAt: external_exports.string().datetime().optional(),
25068
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
25101
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
25069
25102
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
25070
25103
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
25071
25104
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -25126,7 +25159,7 @@ var init_dist = __esm({
25126
25159
  * off the same open list the card came from, so it clears when the Call does. A card is the
25127
25160
  * backup for a call not taken; while the call has it, the call is where it is answered. */
25128
25161
  onCall: external_exports.literal(true).optional(),
25129
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
25162
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
25130
25163
  * declined, and what the ladder will do next — read off the cron's own row, never computed
25131
25164
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
25132
25165
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -25157,7 +25190,7 @@ var init_dist = __esm({
25157
25190
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
25158
25191
  ),
25159
25192
  /** Real downstream work is stuck behind this one — set by the agent, independent of
25160
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
25193
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
25161
25194
  * inbox's blocking badge and the extra confirm step before dismissing it. */
25162
25195
  blocking: external_exports.boolean().default(false),
25163
25196
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -25231,9 +25264,17 @@ var init_dist = __esm({
25231
25264
  banner: external_exports.boolean(),
25232
25265
  push: external_exports.boolean()
25233
25266
  }),
25267
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
25268
+ * they are an enum on a live column across every account, and the rename is a rename of
25269
+ * what people read (owner, 2026-09-30). */
25234
25270
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
25235
- silentPush: external_exports.boolean(),
25236
- autoCallback: external_exports.boolean(),
25271
+ /** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
25272
+ * optional long after DateNight stopped reading it, on the theory that a phone on an older
25273
+ * bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
25274
+ * wanting. That theory was wrong about this schema: these are plain `z.object`s with no
25275
+ * `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
25276
+ * an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
25277
+ * next dead field for the same reason. */
25237
25278
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
25238
25279
  improveConsent: external_exports.boolean(),
25239
25280
  missedCall: MissedCallSchema.default("backoff_standard"),
@@ -25244,12 +25285,16 @@ var init_dist = __esm({
25244
25285
  * must not silently reset this privacy choice. Absent = leave unchanged on
25245
25286
  * write, 'hosted' on read (see store.ts). */
25246
25287
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
25247
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
25288
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
25248
25289
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
25249
25290
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
25250
25291
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
25251
25292
  * read (see store.ts). */
25252
25293
  talk: external_exports.boolean().optional(),
25294
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
25295
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
25296
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
25297
+ callDiagnostics: external_exports.boolean().optional(),
25253
25298
  /** Per-user ring budget (#603): calls per rolling day before further calls
25254
25299
  * degrade to banner. Absent = the global default (25). A number, never a
25255
25300
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -25258,9 +25303,6 @@ var init_dist = __esm({
25258
25303
  * slower speaker). No API-side semantics; the bot resolves each key with its
25259
25304
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
25260
25305
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
25261
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
25262
- * defaulted — an older client PATCHing the full object must not clobber it. */
25263
- pstnCalls: external_exports.boolean().optional(),
25264
25306
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
25265
25307
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
25266
25308
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -25304,7 +25346,7 @@ var init_dist = __esm({
25304
25346
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
25305
25347
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
25306
25348
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
25307
- * apps/api/src/tokens/devices-vs-agents-design.md. */
25349
+ * docs/server/tokens/devices-vs-agents-design.md. */
25308
25350
  kind: external_exports.enum(["device", "agent"]).optional(),
25309
25351
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
25310
25352
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -25322,7 +25364,7 @@ var init_dist = __esm({
25322
25364
  * user on the agent's own page. null = no ceiling (today's behaviour for every
25323
25365
  * connection). Android binds importance to a relationship rather than to each message,
25324
25366
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
25325
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
25367
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
25326
25368
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
25327
25369
  * set once must not overrule a rule they set about one agent. */
25328
25370
  reach: NotifyLevelSchema.nullable().optional(),
@@ -25333,7 +25375,14 @@ var init_dist = __esm({
25333
25375
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
25334
25376
  * desktop app while open. Null = never seen; stale = offline. */
25335
25377
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
25336
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
25378
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
25379
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
25380
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
25381
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
25382
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
25383
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
25384
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
25385
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
25337
25386
  * harness availabilities + granted workspaces — the option set the phone's
25338
25387
  * "new session" sheet offers. Absent for ordinary MCP agents. */
25339
25388
  runtime: external_exports.object({
@@ -25341,7 +25390,12 @@ var init_dist = __esm({
25341
25390
  * shows its age here (`apps/desktop/src/update.ts`). */
25342
25391
  version: external_exports.string().optional(),
25343
25392
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
25344
- workspaces: external_exports.array(external_exports.string()).optional()
25393
+ workspaces: external_exports.array(external_exports.string()).optional(),
25394
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
25395
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
25396
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
25397
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
25398
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
25345
25399
  }).optional(),
25346
25400
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
25347
25401
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -25354,7 +25408,8 @@ var init_dist = __esm({
25354
25408
  });
25355
25409
  LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
25356
25410
  AgentLedgerSchema = external_exports.object({
25357
- agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
25411
+ /** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
25412
+ agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
25358
25413
  /** Its own questions you have not answered. */
25359
25414
  asks: external_exports.array(LedgerItemSchema),
25360
25415
  /** Its questions you answered that nobody acted on — still owed to somebody. */
@@ -25424,7 +25479,7 @@ var init_dist = __esm({
25424
25479
  * for a settled question whose reply carried nothing readable. */
25425
25480
  answer: external_exports.string().nullable().default(null),
25426
25481
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
25427
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
25482
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
25428
25483
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
25429
25484
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
25430
25485
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -25516,7 +25571,19 @@ var init_dist = __esm({
25516
25571
  said: external_exports.string(),
25517
25572
  at: external_exports.string(),
25518
25573
  entryId: external_exports.string()
25519
- }).nullable().optional()
25574
+ }).nullable().optional(),
25575
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
25576
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
25577
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
25578
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
25579
+ * 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
25580
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
25581
+ *
25582
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
25583
+ * minute after the person speaks erases their instant from it, and the durable traces the client
25584
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
25585
+ * Entry with no `about_id` is in neither. */
25586
+ lastPersonAt: external_exports.string().nullable().optional()
25520
25587
  });
25521
25588
  COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
25522
25589
  NoteSourceSchema = external_exports.enum(["app", "call"]);
@@ -25546,7 +25613,7 @@ var init_dist = __esm({
25546
25613
  assignee: external_exports.string().nullable(),
25547
25614
  /** The request thread minted at assignment; null until assigned. */
25548
25615
  parentId: external_exports.string().nullable(),
25549
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
25616
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
25550
25617
  * call — never a deadline. It only ever comes from the user's own words, so when it
25551
25618
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
25552
25619
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -25808,6 +25875,31 @@ var init_dist = __esm({
25808
25875
  computers: external_exports.array(ComputerRowSchema).nullable()
25809
25876
  }).nullable()
25810
25877
  });
25878
+ CallRecapSchema = external_exports.object({
25879
+ call: external_exports.object({
25880
+ status: external_exports.string(),
25881
+ startedAt: external_exports.string(),
25882
+ durationMs: external_exports.number().nullable(),
25883
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
25884
+ }),
25885
+ topics: external_exports.array(external_exports.object({
25886
+ goalId: external_exports.string().uuid(),
25887
+ title: external_exports.string(),
25888
+ owner: external_exports.string(),
25889
+ state: external_exports.string(),
25890
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
25891
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
25892
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
25893
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
25894
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
25895
+ * anything that is not an answer. */
25896
+ /** `about` is the request the line answered (its question), null for words that answered none —
25897
+ * the key the screen groups on, so one question is one row however many times it was answered. */
25898
+ 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() }))
25899
+ })),
25900
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
25901
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
25902
+ });
25811
25903
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
25812
25904
  KEY_PATH = join22(homedir22(), ".paigy", "key.json");
25813
25905
  sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -32924,7 +33016,20 @@ var AskInputSchema2 = external_exports.object({
32924
33016
  ask: external_exports.string().trim().min(1).max(1e4).describe(
32925
33017
  "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."
32926
33018
  ),
32927
- options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
33019
+ options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
33020
+ // A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
33021
+ // checklist mode of answering"). The answer shape was taken off this surface with the wire form
33022
+ // (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
33023
+ // only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
33024
+ // as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
33025
+ // could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
33026
+ // not: whether its own options exclude each other.
33027
+ select: external_exports.enum(["one", "many"]).optional().describe(
33028
+ '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.'
33029
+ ),
33030
+ answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
33031
+ "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."
33032
+ )
32928
33033
  }).strict();
32929
33034
  var StartContactSchema2 = external_exports.object({
32930
33035
  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."),
@@ -32933,7 +33038,7 @@ var StartContactSchema2 = external_exports.object({
32933
33038
  }).strict();
32934
33039
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
32935
33040
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
32936
- 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.";
33041
+ 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.";
32937
33042
  var CreateGoalSchema2 = external_exports.object({
32938
33043
  outcome: external_exports.string().trim().min(1).max(1e4),
32939
33044
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -32989,11 +33094,11 @@ var GetGoalSchema2 = external_exports.object({
32989
33094
  history: external_exports.boolean().optional(),
32990
33095
  /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
32991
33096
  * about this work, each answer attributed to the reader that gave it, and every disagreement
32992
- * between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
33097
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
32993
33098
  diagnose: external_exports.boolean().optional()
32994
33099
  }).strict();
32995
- 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.";
32996
- 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.`;
33100
+ 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.";
33101
+ 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.`;
32997
33102
  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.";
32998
33103
  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.";
32999
33104
  var CheckRepliesSchema2 = external_exports.object({}).strict();
@@ -33022,6 +33127,7 @@ function entryWords(entry) {
33022
33127
  return entry.sources.map((source) => source.text).join("\n");
33023
33128
  }
33024
33129
  var LIVE_MS2 = 3 * 6e4;
33130
+ var WORKING_MS2 = 30 * 6e4;
33025
33131
  var ContextSchema2 = external_exports.object({
33026
33132
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
33027
33133
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -33092,7 +33198,7 @@ var AttentionSchema2 = external_exports.object({
33092
33198
  points: external_exports.array(external_exports.string()).nullable(),
33093
33199
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
33094
33200
  blocking: external_exports.boolean(),
33095
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
33201
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
33096
33202
  * slice wires it; optional so today's rows/callers project cleanly. */
33097
33203
  deadline: external_exports.string().datetime().nullable().optional()
33098
33204
  });
@@ -33143,7 +33249,7 @@ var NotifyRequestFields2 = external_exports.object({
33143
33249
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
33144
33250
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
33145
33251
  // the ask into topics and gives each one at most three sentences and one question
33146
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
33252
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
33147
33253
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
33148
33254
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
33149
33255
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -33220,7 +33326,7 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
33220
33326
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
33221
33327
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
33222
33328
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
33223
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
33329
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
33224
33330
  * delivered through the same settle/await path as a human answer, carrying the judge's
33225
33331
  * derivation and the precedent ids it grew from. Always paired with a visible trail
33226
33332
  * card the user can reply to — the broker never overrides the user. */
@@ -33269,7 +33375,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
33269
33375
  * and now knows exactly which call to make. Absent when either half is missing —
33270
33376
  * a sentence with a hole in it is worse than no sentence. */
33271
33377
  note: external_exports.string().optional(),
33272
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
33378
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
33273
33379
  * shaped answer was mapped from, filtered to its own claims. There is no second list
33274
33380
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
33275
33381
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -33365,10 +33471,11 @@ var AgendaTurnSchema2 = external_exports.object({
33365
33471
  agentId: external_exports.string().optional(),
33366
33472
  select: SelectShapeSchema2.optional(),
33367
33473
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
33368
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
33369
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
33370
- * (the beat for context, the answer window for asks). Clamped bot-side. */
33371
- pace: external_exports.number().positive().optional(),
33474
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
33475
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
33476
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
33477
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
33478
+ anything. A caller's own `beat_s` tuning is what it used to override. */
33372
33479
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
33373
33480
  * (a question blocks, context flows). blocking:false on a question = ask and move
33374
33481
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -33403,7 +33510,7 @@ var InboxItemSchema2 = external_exports.object({
33403
33510
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
33404
33511
  * `createdAt`. */
33405
33512
  agentStateAt: external_exports.string().datetime().optional(),
33406
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
33513
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
33407
33514
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
33408
33515
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
33409
33516
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -33464,7 +33571,7 @@ var InboxItemSchema2 = external_exports.object({
33464
33571
  * off the same open list the card came from, so it clears when the Call does. A card is the
33465
33572
  * backup for a call not taken; while the call has it, the call is where it is answered. */
33466
33573
  onCall: external_exports.literal(true).optional(),
33467
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
33574
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
33468
33575
  * declined, and what the ladder will do next — read off the cron's own row, never computed
33469
33576
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
33470
33577
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -33495,7 +33602,7 @@ var InboxItemSchema2 = external_exports.object({
33495
33602
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
33496
33603
  ),
33497
33604
  /** Real downstream work is stuck behind this one — set by the agent, independent of
33498
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
33605
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
33499
33606
  * inbox's blocking badge and the extra confirm step before dismissing it. */
33500
33607
  blocking: external_exports.boolean().default(false),
33501
33608
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -33569,9 +33676,17 @@ var UserSettingsSchema2 = external_exports.object({
33569
33676
  banner: external_exports.boolean(),
33570
33677
  push: external_exports.boolean()
33571
33678
  }),
33679
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
33680
+ * they are an enum on a live column across every account, and the rename is a rename of
33681
+ * what people read (owner, 2026-09-30). */
33572
33682
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
33573
- silentPush: external_exports.boolean(),
33574
- autoCallback: external_exports.boolean(),
33683
+ /** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
33684
+ * optional long after DateNight stopped reading it, on the theory that a phone on an older
33685
+ * bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
33686
+ * wanting. That theory was wrong about this schema: these are plain `z.object`s with no
33687
+ * `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
33688
+ * an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
33689
+ * next dead field for the same reason. */
33575
33690
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
33576
33691
  improveConsent: external_exports.boolean(),
33577
33692
  missedCall: MissedCallSchema2.default("backoff_standard"),
@@ -33582,12 +33697,16 @@ var UserSettingsSchema2 = external_exports.object({
33582
33697
  * must not silently reset this privacy choice. Absent = leave unchanged on
33583
33698
  * write, 'hosted' on read (see store.ts). */
33584
33699
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
33585
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
33700
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
33586
33701
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
33587
33702
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
33588
33703
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
33589
33704
  * read (see store.ts). */
33590
33705
  talk: external_exports.boolean().optional(),
33706
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
33707
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
33708
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
33709
+ callDiagnostics: external_exports.boolean().optional(),
33591
33710
  /** Per-user ring budget (#603): calls per rolling day before further calls
33592
33711
  * degrade to banner. Absent = the global default (25). A number, never a
33593
33712
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -33596,9 +33715,6 @@ var UserSettingsSchema2 = external_exports.object({
33596
33715
  * slower speaker). No API-side semantics; the bot resolves each key with its
33597
33716
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
33598
33717
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
33599
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
33600
- * defaulted — an older client PATCHing the full object must not clobber it. */
33601
- pstnCalls: external_exports.boolean().optional(),
33602
33718
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
33603
33719
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
33604
33720
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -33642,7 +33758,7 @@ var ConnectionSummarySchema2 = external_exports.object({
33642
33758
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
33643
33759
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
33644
33760
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
33645
- * apps/api/src/tokens/devices-vs-agents-design.md. */
33761
+ * docs/server/tokens/devices-vs-agents-design.md. */
33646
33762
  kind: external_exports.enum(["device", "agent"]).optional(),
33647
33763
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
33648
33764
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -33660,7 +33776,7 @@ var ConnectionSummarySchema2 = external_exports.object({
33660
33776
  * user on the agent's own page. null = no ceiling (today's behaviour for every
33661
33777
  * connection). Android binds importance to a relationship rather than to each message,
33662
33778
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
33663
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
33779
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
33664
33780
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
33665
33781
  * set once must not overrule a rule they set about one agent. */
33666
33782
  reach: NotifyLevelSchema2.nullable().optional(),
@@ -33671,7 +33787,14 @@ var ConnectionSummarySchema2 = external_exports.object({
33671
33787
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
33672
33788
  * desktop app while open. Null = never seen; stale = offline. */
33673
33789
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
33674
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
33790
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
33791
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
33792
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
33793
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
33794
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
33795
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
33796
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
33797
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
33675
33798
  * harness availabilities + granted workspaces — the option set the phone's
33676
33799
  * "new session" sheet offers. Absent for ordinary MCP agents. */
33677
33800
  runtime: external_exports.object({
@@ -33679,7 +33802,12 @@ var ConnectionSummarySchema2 = external_exports.object({
33679
33802
  * shows its age here (`apps/desktop/src/update.ts`). */
33680
33803
  version: external_exports.string().optional(),
33681
33804
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
33682
- workspaces: external_exports.array(external_exports.string()).optional()
33805
+ workspaces: external_exports.array(external_exports.string()).optional(),
33806
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
33807
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
33808
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
33809
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
33810
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
33683
33811
  }).optional(),
33684
33812
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
33685
33813
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -33692,7 +33820,8 @@ var ConnectionSummarySchema2 = external_exports.object({
33692
33820
  });
33693
33821
  var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
33694
33822
  var AgentLedgerSchema2 = external_exports.object({
33695
- agent: external_exports.object({ id: external_exports.string(), name: external_exports.string(), revokedAt: external_exports.string().nullable() }),
33823
+ /** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
33824
+ agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
33696
33825
  /** Its own questions you have not answered. */
33697
33826
  asks: external_exports.array(LedgerItemSchema2),
33698
33827
  /** Its questions you answered that nobody acted on — still owed to somebody. */
@@ -33762,7 +33891,7 @@ var QueueQuestionSchema2 = external_exports.object({
33762
33891
  * for a settled question whose reply carried nothing readable. */
33763
33892
  answer: external_exports.string().nullable().default(null),
33764
33893
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
33765
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
33894
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
33766
33895
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
33767
33896
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
33768
33897
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -33854,7 +33983,19 @@ var QueueItemSchema2 = external_exports.object({
33854
33983
  said: external_exports.string(),
33855
33984
  at: external_exports.string(),
33856
33985
  entryId: external_exports.string()
33857
- }).nullable().optional()
33986
+ }).nullable().optional(),
33987
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
33988
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
33989
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
33990
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
33991
+ * 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
33992
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
33993
+ *
33994
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
33995
+ * minute after the person speaks erases their instant from it, and the durable traces the client
33996
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
33997
+ * Entry with no `about_id` is in neither. */
33998
+ lastPersonAt: external_exports.string().nullable().optional()
33858
33999
  });
33859
34000
  var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
33860
34001
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
@@ -33884,7 +34025,7 @@ var NoteSchema2 = external_exports.object({
33884
34025
  assignee: external_exports.string().nullable(),
33885
34026
  /** The request thread minted at assignment; null until assigned. */
33886
34027
  parentId: external_exports.string().nullable(),
33887
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
34028
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
33888
34029
  * call — never a deadline. It only ever comes from the user's own words, so when it
33889
34030
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
33890
34031
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -34144,6 +34285,31 @@ var SnapshotSchema2 = external_exports.object({
34144
34285
  computers: external_exports.array(ComputerRowSchema2).nullable()
34145
34286
  }).nullable()
34146
34287
  });
34288
+ var CallRecapSchema2 = external_exports.object({
34289
+ call: external_exports.object({
34290
+ status: external_exports.string(),
34291
+ startedAt: external_exports.string(),
34292
+ durationMs: external_exports.number().nullable(),
34293
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
34294
+ }),
34295
+ topics: external_exports.array(external_exports.object({
34296
+ goalId: external_exports.string().uuid(),
34297
+ title: external_exports.string(),
34298
+ owner: external_exports.string(),
34299
+ state: external_exports.string(),
34300
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
34301
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
34302
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
34303
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
34304
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
34305
+ * anything that is not an answer. */
34306
+ /** `about` is the request the line answered (its question), null for words that answered none —
34307
+ * the key the screen groups on, so one question is one row however many times it was answered. */
34308
+ 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() }))
34309
+ })),
34310
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
34311
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
34312
+ });
34147
34313
 
34148
34314
  // src/paigy/activity.ts
34149
34315
  function shortenPaths(s) {
@@ -34878,7 +35044,7 @@ function ago(iso, now) {
34878
35044
  init_catalog();
34879
35045
 
34880
35046
  // src/workspaces.ts
34881
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
35047
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
34882
35048
  import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
34883
35049
  import { homedir as homedir6 } from "os";
34884
35050
  function workspacesFile() {
@@ -34913,11 +35079,42 @@ function resolveWakeDir(pinned, deps) {
34913
35079
  }
34914
35080
  return listWorkspaces(deps)[0];
34915
35081
  }
35082
+ function originOf(dir) {
35083
+ try {
35084
+ if (!statSync3(join5(dir, ".git")).isDirectory()) return null;
35085
+ const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
35086
+ const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
35087
+ return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
35088
+ } catch {
35089
+ return null;
35090
+ }
35091
+ }
35092
+ var MAX_CHILDREN = 300;
35093
+ var MAX_REPOS = 100;
35094
+ function listRepos(deps) {
35095
+ const repos = [];
35096
+ for (const root of listWorkspaces(deps)) {
35097
+ const own = originOf(root);
35098
+ if (own) repos.push({ path: root, remote: own });
35099
+ let children = [];
35100
+ try {
35101
+ children = readdirSync2(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, MAX_CHILDREN);
35102
+ } catch {
35103
+ }
35104
+ for (const name of children) {
35105
+ if (repos.length >= MAX_REPOS) return repos;
35106
+ const path = join5(root, name);
35107
+ const remote = originOf(path);
35108
+ if (remote) repos.push({ path, remote });
35109
+ }
35110
+ }
35111
+ return repos.slice(0, MAX_REPOS);
35112
+ }
34916
35113
 
34917
35114
  // src/holder.ts
34918
35115
  init_dist();
34919
35116
  import { execFileSync as execFileSync3 } from "child_process";
34920
- import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
35117
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
34921
35118
  import { homedir as homedir7 } from "os";
34922
35119
  import { join as join6 } from "path";
34923
35120
  function isAlive(pid) {
@@ -34948,9 +35145,62 @@ function holds(pid, procStart, deps) {
34948
35145
  }
34949
35146
  return false;
34950
35147
  }
35148
+ function proven(pid, procStart, deps) {
35149
+ if (!deps.alive(pid) || typeof procStart !== "string" || !procStart.trim()) return false;
35150
+ const want = squash(procStart);
35151
+ return ["UTC", "local"].some((zone) => {
35152
+ const got = deps.started(pid, zone);
35153
+ return got !== null && squash(got) === want;
35154
+ });
35155
+ }
35156
+ function localHolderPids(deps = {}) {
35157
+ const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
35158
+ const list = deps.list ?? ((d) => readdirSync3(d));
35159
+ const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
35160
+ const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
35161
+ const held = /* @__PURE__ */ new Map();
35162
+ let files;
35163
+ try {
35164
+ files = list(dir);
35165
+ } catch {
35166
+ return held;
35167
+ }
35168
+ for (const file of files) {
35169
+ if (!/^\d+\.json$/.test(file)) continue;
35170
+ let row;
35171
+ try {
35172
+ row = JSON.parse(read(join6(dir, file)));
35173
+ } catch {
35174
+ continue;
35175
+ }
35176
+ const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
35177
+ if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
35178
+ if (!holds(pid, row.procStart, check)) continue;
35179
+ const slot = sessionSlot(row.sessionId);
35180
+ held.set(slot, [...held.get(slot) ?? [], { pid, proven: proven(pid, row.procStart, check) }]);
35181
+ }
35182
+ return held;
35183
+ }
35184
+ function psParent(pid) {
35185
+ try {
35186
+ const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
35187
+ const n = Number(out);
35188
+ return Number.isInteger(n) && n > 0 ? n : null;
35189
+ } catch {
35190
+ return null;
35191
+ }
35192
+ }
35193
+ function descendsFrom(pid, ancestor, parent = psParent) {
35194
+ let at = pid;
35195
+ for (let i = 0; i < 32 && at !== null && at > 1; i += 1) {
35196
+ if (at === ancestor) return true;
35197
+ at = parent(at);
35198
+ }
35199
+ return false;
35200
+ }
34951
35201
  function localHolders(deps = {}) {
34952
35202
  const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
34953
- const list = deps.list ?? ((d) => readdirSync2(d));
35203
+ const list = deps.list ?? ((d) => readdirSync3(d));
34954
35204
  const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
34955
35205
  const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
34956
35206
  const held = /* @__PURE__ */ new Map();
@@ -34977,8 +35227,67 @@ function localHolders(deps = {}) {
34977
35227
  return held;
34978
35228
  }
34979
35229
 
35230
+ // src/end.ts
35231
+ var GRACE_MS = 5e3;
35232
+ async function endTerminal(pid, slot, deps) {
35233
+ try {
35234
+ deps.kill(pid, "SIGTERM");
35235
+ } catch (e) {
35236
+ return { ok: false, kind: "terminal", pid, why: `SIGTERM refused: ${e.message}`.slice(0, 200) };
35237
+ }
35238
+ const grace = deps.graceMs ?? GRACE_MS;
35239
+ for (let waited = 0; waited < grace; waited += 250) {
35240
+ if (!deps.alive(pid)) {
35241
+ deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGTERM" })}`);
35242
+ return { ok: true, kind: "terminal", pid, signal: "SIGTERM" };
35243
+ }
35244
+ await deps.sleep(250);
35245
+ }
35246
+ try {
35247
+ deps.kill(pid, "SIGKILL");
35248
+ } catch (e) {
35249
+ return { ok: false, kind: "terminal", pid, why: `SIGKILL refused: ${e.message}`.slice(0, 200) };
35250
+ }
35251
+ deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGKILL" })}`);
35252
+ return { ok: true, kind: "terminal", pid, signal: "SIGKILL" };
35253
+ }
35254
+ async function endSession(end, deps) {
35255
+ const slots = deps.slotsFor(end.agent);
35256
+ if (!slots.length) return { ok: false, why: "not_on_this_host" };
35257
+ let ended2 = null;
35258
+ if (end.which === "harness" || end.which === "any") {
35259
+ for (const slot of slots) {
35260
+ const run = deps.ownRun(slot);
35261
+ if (!run) continue;
35262
+ run.stop();
35263
+ deps.log(`session_ended ${JSON.stringify({ sessionId: slot, kind: "harness", signal: "stop" })}`);
35264
+ ended2 = { ok: true, kind: "harness", signal: "stop" };
35265
+ }
35266
+ if (end.which === "harness" && !ended2) return { ok: false, kind: "harness", why: "no_harness_copy_here" };
35267
+ }
35268
+ if (end.which === "terminal" || end.which === "any") {
35269
+ let terminal = null;
35270
+ let unproven = null;
35271
+ for (const slot of slots) {
35272
+ for (const { pid, proven: proven2 } of deps.pidsFor(slot).filter((p) => !deps.isOwn(p.pid))) {
35273
+ if (!proven2) {
35274
+ deps.log(`session_end_refused ${JSON.stringify({ sessionId: slot, pid, why: "no_proc_start" })}`);
35275
+ unproven = { ok: false, kind: "terminal", pid, why: "no_proc_start" };
35276
+ continue;
35277
+ }
35278
+ terminal = await endTerminal(pid, slot, deps);
35279
+ if (!terminal.ok) return terminal;
35280
+ }
35281
+ }
35282
+ if (terminal) return terminal;
35283
+ if (unproven) return unproven;
35284
+ if (end.which === "terminal") return { ok: false, kind: "terminal", why: "no_terminal_copy_here" };
35285
+ }
35286
+ return ended2 ?? { ok: false, why: "nothing_running_here" };
35287
+ }
35288
+
34980
35289
  // src/owner.ts
34981
- import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
35290
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync4 } from "fs";
34982
35291
  import { homedir as homedir8 } from "os";
34983
35292
  import { join as join7 } from "path";
34984
35293
  var HEAD_BYTES = 64e3;
@@ -34997,7 +35306,7 @@ function ownerSession(slot, deps = {}) {
34997
35306
  if (!m) return null;
34998
35307
  const prefix = m[1];
34999
35308
  const root = deps.projectsDir ?? join7(homedir8(), ".claude", "projects");
35000
- const list = deps.list ?? ((dir) => readdirSync3(dir));
35309
+ const list = deps.list ?? ((dir) => readdirSync4(dir));
35001
35310
  const head = deps.head ?? readHead;
35002
35311
  let folders;
35003
35312
  try {
@@ -35232,7 +35541,9 @@ function startHost(opts) {
35232
35541
  void heartbeat({
35233
35542
  version: VERSION,
35234
35543
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
35235
- workspaces: listWorkspaces(opts.wsDeps)
35544
+ workspaces: listWorkspaces(opts.wsDeps),
35545
+ // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
35546
+ repos: listRepos(opts.wsDeps)
35236
35547
  }, asHost).catch(() => {
35237
35548
  });
35238
35549
  void listenSlots().then(sweepSlots).catch(() => {
@@ -35278,6 +35589,31 @@ function startHost(opts) {
35278
35589
  void refreshIdentities();
35279
35590
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
35280
35591
  }
35592
+ const ends = await claimSessionEnds(asHost).catch(() => []);
35593
+ for (const end of ends) {
35594
+ const result = await endSession(end, {
35595
+ slotsFor: (agent2) => listSlots().filter((slot) => slot !== "Desktop" && slotIdentity(slot).tokenId === agent2),
35596
+ pidsFor: (slot) => localHolderPids().get(slot) ?? [],
35597
+ ownRun: (slot) => runs.get(runKey(slot))?.run ?? null,
35598
+ isOwn: (pid) => descendsFrom(pid, process.pid),
35599
+ kill: (pid, signal) => {
35600
+ process.kill(pid, signal);
35601
+ },
35602
+ alive: (pid) => {
35603
+ try {
35604
+ process.kill(pid, 0);
35605
+ return true;
35606
+ } catch (e) {
35607
+ return e.code === "EPERM";
35608
+ }
35609
+ },
35610
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
35611
+ log: opts.log
35612
+ }).catch((e) => ({ ok: false, why: `end_threw: ${e.message}`.slice(0, 200) }));
35613
+ 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}`);
35614
+ await finishSessionEnd(end.endId, result, asHost).catch(() => {
35615
+ });
35616
+ }
35281
35617
  }
35282
35618
  async function presenceMap() {
35283
35619
  const presence = /* @__PURE__ */ new Map();
@@ -35302,6 +35638,14 @@ function startHost(opts) {
35302
35638
  const key = runKey(slot);
35303
35639
  if (slot === "Desktop" || isRevoked(slot)) return;
35304
35640
  const live = runs.get(key);
35641
+ if (live) {
35642
+ const theirs = (localHolderPids().get(slot) ?? []).map((p) => p.pid).filter((pid) => !descendsFrom(pid, process.pid));
35643
+ if (theirs.length) {
35644
+ opts.log(`session_stood_back ${JSON.stringify({ sessionId: slot, livePid: theirs[0] })} \u2014 a terminal runs this conversation; stopping this host's copy`);
35645
+ live.run.stop();
35646
+ return;
35647
+ }
35648
+ }
35305
35649
  const tokenId = slotIdentity(slot).tokenId;
35306
35650
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
35307
35651
  const holder = holders.get(slot);