@paigy/harness 0.3.29 → 0.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -10040,7 +10040,7 @@ var GetGoalSchema = external_exports.object({
10040
10040
  history: external_exports.boolean().optional(),
10041
10041
  /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
10042
10042
  * about this work, each answer attributed to the reader that gave it, and every disagreement
10043
- * between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
10043
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
10044
10044
  diagnose: external_exports.boolean().optional()
10045
10045
  }).strict();
10046
10046
  var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
@@ -10058,6 +10058,7 @@ var AGENT_TOOLS = [
10058
10058
  ];
10059
10059
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
10060
10060
  var LIVE_MS = 3 * 6e4;
10061
+ var WORKING_MS = 30 * 6e4;
10061
10062
  var ContextSchema = external_exports.object({
10062
10063
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
10063
10064
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -10128,7 +10129,7 @@ var AttentionSchema = external_exports.object({
10128
10129
  points: external_exports.array(external_exports.string()).nullable(),
10129
10130
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
10130
10131
  blocking: external_exports.boolean(),
10131
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
10132
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
10132
10133
  * slice wires it; optional so today's rows/callers project cleanly. */
10133
10134
  deadline: external_exports.string().datetime().nullable().optional()
10134
10135
  });
@@ -10179,7 +10180,7 @@ var NotifyRequestFields = external_exports.object({
10179
10180
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
10180
10181
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
10181
10182
  // the ask into topics and gives each one at most three sentences and one question
10182
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
10183
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
10183
10184
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
10184
10185
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
10185
10186
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -10256,7 +10257,7 @@ var UserAnswerSchema = external_exports.discriminatedUnion("kind", [
10256
10257
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
10257
10258
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
10258
10259
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
10259
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
10260
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
10260
10261
  * delivered through the same settle/await path as a human answer, carrying the judge's
10261
10262
  * derivation and the precedent ids it grew from. Always paired with a visible trail
10262
10263
  * card the user can reply to — the broker never overrides the user. */
@@ -10305,7 +10306,7 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
10305
10306
  * and now knows exactly which call to make. Absent when either half is missing —
10306
10307
  * a sentence with a hole in it is worse than no sentence. */
10307
10308
  note: external_exports.string().optional(),
10308
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
10309
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
10309
10310
  * shaped answer was mapped from, filtered to its own claims. There is no second list
10310
10311
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
10311
10312
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -10401,10 +10402,11 @@ var AgendaTurnSchema = external_exports.object({
10401
10402
  agentId: external_exports.string().optional(),
10402
10403
  select: SelectShapeSchema.optional(),
10403
10404
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
10404
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
10405
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
10406
- * (the beat for context, the answer window for asks). Clamped bot-side. */
10407
- pace: external_exports.number().positive().optional(),
10405
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
10406
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
10407
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
10408
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
10409
+ anything. A caller's own `beat_s` tuning is what it used to override. */
10408
10410
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
10409
10411
  * (a question blocks, context flows). blocking:false on a question = ask and move
10410
10412
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -10439,7 +10441,7 @@ var InboxItemSchema = external_exports.object({
10439
10441
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
10440
10442
  * `createdAt`. */
10441
10443
  agentStateAt: external_exports.string().datetime().optional(),
10442
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
10444
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
10443
10445
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
10444
10446
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
10445
10447
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -10500,7 +10502,7 @@ var InboxItemSchema = external_exports.object({
10500
10502
  * off the same open list the card came from, so it clears when the Call does. A card is the
10501
10503
  * backup for a call not taken; while the call has it, the call is where it is answered. */
10502
10504
  onCall: external_exports.literal(true).optional(),
10503
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
10505
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
10504
10506
  * declined, and what the ladder will do next — read off the cron's own row, never computed
10505
10507
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
10506
10508
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -10531,7 +10533,7 @@ var InboxItemSchema = external_exports.object({
10531
10533
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
10532
10534
  ),
10533
10535
  /** Real downstream work is stuck behind this one — set by the agent, independent of
10534
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
10536
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
10535
10537
  * inbox's blocking badge and the extra confirm step before dismissing it. */
10536
10538
  blocking: external_exports.boolean().default(false),
10537
10539
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -10605,9 +10607,16 @@ var UserSettingsSchema = external_exports.object({
10605
10607
  banner: external_exports.boolean(),
10606
10608
  push: external_exports.boolean()
10607
10609
  }),
10610
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
10611
+ * they are an enum on a live column across every account, and the rename is a rename of
10612
+ * what people read (owner, 2026-09-30). */
10608
10613
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
10609
- silentPush: external_exports.boolean(),
10610
- autoCallback: external_exports.boolean(),
10614
+ /** DEAD, AND ACCEPTED ANYWAY (#2813). DateNight is inbox-only with no way back, so nothing
10615
+ * reads this. It stays OPTIONAL rather than deleted because a phone on the old bundle
10616
+ * PATCHes the whole settings object and would be refused for sending a key we stopped
10617
+ * wanting — the same trap `voiceMode` below already documents. #2813 drops the field and
10618
+ * its column once no installed build still sends it. */
10619
+ silentPush: external_exports.boolean().optional(),
10611
10620
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
10612
10621
  improveConsent: external_exports.boolean(),
10613
10622
  missedCall: MissedCallSchema.default("backoff_standard"),
@@ -10618,12 +10627,16 @@ var UserSettingsSchema = external_exports.object({
10618
10627
  * must not silently reset this privacy choice. Absent = leave unchanged on
10619
10628
  * write, 'hosted' on read (see store.ts). */
10620
10629
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
10621
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
10630
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
10622
10631
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
10623
10632
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
10624
10633
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
10625
10634
  * read (see store.ts). */
10626
10635
  talk: external_exports.boolean().optional(),
10636
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
10637
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
10638
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
10639
+ callDiagnostics: external_exports.boolean().optional(),
10627
10640
  /** Per-user ring budget (#603): calls per rolling day before further calls
10628
10641
  * degrade to banner. Absent = the global default (25). A number, never a
10629
10642
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -10632,9 +10645,6 @@ var UserSettingsSchema = external_exports.object({
10632
10645
  * slower speaker). No API-side semantics; the bot resolves each key with its
10633
10646
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
10634
10647
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
10635
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
10636
- * defaulted — an older client PATCHing the full object must not clobber it. */
10637
- pstnCalls: external_exports.boolean().optional(),
10638
10648
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
10639
10649
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
10640
10650
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -10678,7 +10688,7 @@ var ConnectionSummarySchema = external_exports.object({
10678
10688
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
10679
10689
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
10680
10690
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
10681
- * apps/api/src/tokens/devices-vs-agents-design.md. */
10691
+ * docs/server/tokens/devices-vs-agents-design.md. */
10682
10692
  kind: external_exports.enum(["device", "agent"]).optional(),
10683
10693
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
10684
10694
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -10696,7 +10706,7 @@ var ConnectionSummarySchema = external_exports.object({
10696
10706
  * user on the agent's own page. null = no ceiling (today's behaviour for every
10697
10707
  * connection). Android binds importance to a relationship rather than to each message,
10698
10708
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
10699
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
10709
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
10700
10710
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
10701
10711
  * set once must not overrule a rule they set about one agent. */
10702
10712
  reach: NotifyLevelSchema.nullable().optional(),
@@ -10707,7 +10717,14 @@ var ConnectionSummarySchema = external_exports.object({
10707
10717
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
10708
10718
  * desktop app while open. Null = never seen; stale = offline. */
10709
10719
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
10710
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
10720
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
10721
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
10722
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
10723
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
10724
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
10725
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
10726
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
10727
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
10711
10728
  * harness availabilities + granted workspaces — the option set the phone's
10712
10729
  * "new session" sheet offers. Absent for ordinary MCP agents. */
10713
10730
  runtime: external_exports.object({
@@ -10715,7 +10732,12 @@ var ConnectionSummarySchema = external_exports.object({
10715
10732
  * shows its age here (`apps/desktop/src/update.ts`). */
10716
10733
  version: external_exports.string().optional(),
10717
10734
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
10718
- workspaces: external_exports.array(external_exports.string()).optional()
10735
+ workspaces: external_exports.array(external_exports.string()).optional(),
10736
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
10737
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
10738
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
10739
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
10740
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
10719
10741
  }).optional(),
10720
10742
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
10721
10743
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -10798,7 +10820,7 @@ var QueueQuestionSchema = external_exports.object({
10798
10820
  * for a settled question whose reply carried nothing readable. */
10799
10821
  answer: external_exports.string().nullable().default(null),
10800
10822
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
10801
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
10823
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
10802
10824
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
10803
10825
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
10804
10826
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -10890,7 +10912,19 @@ var QueueItemSchema = external_exports.object({
10890
10912
  said: external_exports.string(),
10891
10913
  at: external_exports.string(),
10892
10914
  entryId: external_exports.string()
10893
- }).nullable().optional()
10915
+ }).nullable().optional(),
10916
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
10917
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
10918
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
10919
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
10920
+ * 20260930222356 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
10921
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
10922
+ *
10923
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
10924
+ * minute after the person speaks erases their instant from it, and the durable traces the client
10925
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
10926
+ * Entry with no `about_id` is in neither. */
10927
+ lastPersonAt: external_exports.string().nullable().optional()
10894
10928
  });
10895
10929
  var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
10896
10930
  var NoteSourceSchema = external_exports.enum(["app", "call"]);
@@ -10920,7 +10954,7 @@ var NoteSchema = external_exports.object({
10920
10954
  assignee: external_exports.string().nullable(),
10921
10955
  /** The request thread minted at assignment; null until assigned. */
10922
10956
  parentId: external_exports.string().nullable(),
10923
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
10957
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
10924
10958
  * call — never a deadline. It only ever comes from the user's own words, so when it
10925
10959
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
10926
10960
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -11182,6 +11216,29 @@ var SnapshotSchema = external_exports.object({
11182
11216
  computers: external_exports.array(ComputerRowSchema).nullable()
11183
11217
  }).nullable()
11184
11218
  });
11219
+ var CallRecapSchema = external_exports.object({
11220
+ call: external_exports.object({
11221
+ status: external_exports.string(),
11222
+ startedAt: external_exports.string(),
11223
+ durationMs: external_exports.number().nullable(),
11224
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
11225
+ }),
11226
+ topics: external_exports.array(external_exports.object({
11227
+ goalId: external_exports.string().uuid(),
11228
+ title: external_exports.string(),
11229
+ owner: external_exports.string(),
11230
+ state: external_exports.string(),
11231
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
11232
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
11233
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
11234
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
11235
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
11236
+ * anything that is not an answer. */
11237
+ lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional() }))
11238
+ })),
11239
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
11240
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
11241
+ });
11185
11242
  function sessionSlot(sessionId2) {
11186
11243
  const id = sessionId2 ?? sessionId();
11187
11244
  return `session:${id.slice(0, 8)}`;
@@ -12391,7 +12448,7 @@ async function brokerView(deps) {
12391
12448
  }
12392
12449
 
12393
12450
  // src/workspaces.ts
12394
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
12451
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
12395
12452
  import { dirname as dirname2, join as join5, resolve } from "path";
12396
12453
  import { homedir as homedir5 } from "os";
12397
12454
  function workspacesFile() {
@@ -12432,10 +12489,41 @@ function resolveWakeDir(pinned, deps) {
12432
12489
  }
12433
12490
  return listWorkspaces(deps)[0];
12434
12491
  }
12492
+ function originOf(dir) {
12493
+ try {
12494
+ if (!statSync2(join5(dir, ".git")).isDirectory()) return null;
12495
+ const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
12496
+ const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
12497
+ return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
12498
+ } catch {
12499
+ return null;
12500
+ }
12501
+ }
12502
+ var MAX_CHILDREN = 300;
12503
+ var MAX_REPOS = 100;
12504
+ function listRepos(deps) {
12505
+ const repos = [];
12506
+ for (const root of listWorkspaces(deps)) {
12507
+ const own = originOf(root);
12508
+ if (own) repos.push({ path: root, remote: own });
12509
+ let children = [];
12510
+ try {
12511
+ children = readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, MAX_CHILDREN);
12512
+ } catch {
12513
+ }
12514
+ for (const name of children) {
12515
+ if (repos.length >= MAX_REPOS) return repos;
12516
+ const path = join5(root, name);
12517
+ const remote = originOf(path);
12518
+ if (remote) repos.push({ path, remote });
12519
+ }
12520
+ }
12521
+ return repos.slice(0, MAX_REPOS);
12522
+ }
12435
12523
 
12436
12524
  // src/holder.ts
12437
12525
  import { execFileSync as execFileSync3 } from "child_process";
12438
- import { readFileSync as readFileSync4, readdirSync } from "fs";
12526
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
12439
12527
  import { homedir as homedir6 } from "os";
12440
12528
  import { join as join6 } from "path";
12441
12529
  function isAlive(pid) {
@@ -12468,7 +12556,7 @@ function holds(pid, procStart, deps) {
12468
12556
  }
12469
12557
  function localHolders(deps = {}) {
12470
12558
  const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
12471
- const list = deps.list ?? ((d) => readdirSync(d));
12559
+ const list = deps.list ?? ((d) => readdirSync2(d));
12472
12560
  const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
12473
12561
  const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
12474
12562
  const held = /* @__PURE__ */ new Map();
@@ -12496,7 +12584,7 @@ function localHolders(deps = {}) {
12496
12584
  }
12497
12585
 
12498
12586
  // src/owner.ts
12499
- import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync2 } from "fs";
12587
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
12500
12588
  import { homedir as homedir7 } from "os";
12501
12589
  import { join as join7 } from "path";
12502
12590
  var HEAD_BYTES = 64e3;
@@ -12515,7 +12603,7 @@ function ownerSession(slot, deps = {}) {
12515
12603
  if (!m) return null;
12516
12604
  const prefix = m[1];
12517
12605
  const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
12518
- const list = deps.list ?? ((dir) => readdirSync2(dir));
12606
+ const list = deps.list ?? ((dir) => readdirSync3(dir));
12519
12607
  const head = deps.head ?? readHead;
12520
12608
  let folders;
12521
12609
  try {
@@ -13956,7 +14044,7 @@ var GetGoalSchema2 = external_exports.object({
13956
14044
  history: external_exports.boolean().optional(),
13957
14045
  /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
13958
14046
  * about this work, each answer attributed to the reader that gave it, and every disagreement
13959
- * between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
14047
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
13960
14048
  diagnose: external_exports.boolean().optional()
13961
14049
  }).strict();
13962
14050
  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.";
@@ -13989,6 +14077,7 @@ function entryWords(entry) {
13989
14077
  return entry.sources.map((source) => source.text).join("\n");
13990
14078
  }
13991
14079
  var LIVE_MS2 = 3 * 6e4;
14080
+ var WORKING_MS2 = 30 * 6e4;
13992
14081
  var ContextSchema2 = external_exports.object({
13993
14082
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
13994
14083
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -14059,7 +14148,7 @@ var AttentionSchema2 = external_exports.object({
14059
14148
  points: external_exports.array(external_exports.string()).nullable(),
14060
14149
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
14061
14150
  blocking: external_exports.boolean(),
14062
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
14151
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
14063
14152
  * slice wires it; optional so today's rows/callers project cleanly. */
14064
14153
  deadline: external_exports.string().datetime().nullable().optional()
14065
14154
  });
@@ -14110,7 +14199,7 @@ var NotifyRequestFields2 = external_exports.object({
14110
14199
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
14111
14200
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
14112
14201
  // the ask into topics and gives each one at most three sentences and one question
14113
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
14202
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
14114
14203
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
14115
14204
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
14116
14205
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -14187,7 +14276,7 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
14187
14276
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
14188
14277
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
14189
14278
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
14190
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
14279
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
14191
14280
  * delivered through the same settle/await path as a human answer, carrying the judge's
14192
14281
  * derivation and the precedent ids it grew from. Always paired with a visible trail
14193
14282
  * card the user can reply to — the broker never overrides the user. */
@@ -14236,7 +14325,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
14236
14325
  * and now knows exactly which call to make. Absent when either half is missing —
14237
14326
  * a sentence with a hole in it is worse than no sentence. */
14238
14327
  note: external_exports.string().optional(),
14239
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
14328
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
14240
14329
  * shaped answer was mapped from, filtered to its own claims. There is no second list
14241
14330
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
14242
14331
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -14332,10 +14421,11 @@ var AgendaTurnSchema2 = external_exports.object({
14332
14421
  agentId: external_exports.string().optional(),
14333
14422
  select: SelectShapeSchema2.optional(),
14334
14423
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
14335
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
14336
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
14337
- * (the beat for context, the answer window for asks). Clamped bot-side. */
14338
- pace: external_exports.number().positive().optional(),
14424
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
14425
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
14426
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
14427
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
14428
+ anything. A caller's own `beat_s` tuning is what it used to override. */
14339
14429
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
14340
14430
  * (a question blocks, context flows). blocking:false on a question = ask and move
14341
14431
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -14370,7 +14460,7 @@ var InboxItemSchema2 = external_exports.object({
14370
14460
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
14371
14461
  * `createdAt`. */
14372
14462
  agentStateAt: external_exports.string().datetime().optional(),
14373
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
14463
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
14374
14464
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
14375
14465
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
14376
14466
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -14431,7 +14521,7 @@ var InboxItemSchema2 = external_exports.object({
14431
14521
  * off the same open list the card came from, so it clears when the Call does. A card is the
14432
14522
  * backup for a call not taken; while the call has it, the call is where it is answered. */
14433
14523
  onCall: external_exports.literal(true).optional(),
14434
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
14524
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
14435
14525
  * declined, and what the ladder will do next — read off the cron's own row, never computed
14436
14526
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
14437
14527
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -14462,7 +14552,7 @@ var InboxItemSchema2 = external_exports.object({
14462
14552
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
14463
14553
  ),
14464
14554
  /** Real downstream work is stuck behind this one — set by the agent, independent of
14465
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
14555
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
14466
14556
  * inbox's blocking badge and the extra confirm step before dismissing it. */
14467
14557
  blocking: external_exports.boolean().default(false),
14468
14558
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -14536,9 +14626,16 @@ var UserSettingsSchema2 = external_exports.object({
14536
14626
  banner: external_exports.boolean(),
14537
14627
  push: external_exports.boolean()
14538
14628
  }),
14629
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
14630
+ * they are an enum on a live column across every account, and the rename is a rename of
14631
+ * what people read (owner, 2026-09-30). */
14539
14632
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
14540
- silentPush: external_exports.boolean(),
14541
- autoCallback: external_exports.boolean(),
14633
+ /** DEAD, AND ACCEPTED ANYWAY (#2813). DateNight is inbox-only with no way back, so nothing
14634
+ * reads this. It stays OPTIONAL rather than deleted because a phone on the old bundle
14635
+ * PATCHes the whole settings object and would be refused for sending a key we stopped
14636
+ * wanting — the same trap `voiceMode` below already documents. #2813 drops the field and
14637
+ * its column once no installed build still sends it. */
14638
+ silentPush: external_exports.boolean().optional(),
14542
14639
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
14543
14640
  improveConsent: external_exports.boolean(),
14544
14641
  missedCall: MissedCallSchema2.default("backoff_standard"),
@@ -14549,12 +14646,16 @@ var UserSettingsSchema2 = external_exports.object({
14549
14646
  * must not silently reset this privacy choice. Absent = leave unchanged on
14550
14647
  * write, 'hosted' on read (see store.ts). */
14551
14648
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
14552
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
14649
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
14553
14650
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
14554
14651
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
14555
14652
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
14556
14653
  * read (see store.ts). */
14557
14654
  talk: external_exports.boolean().optional(),
14655
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
14656
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
14657
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
14658
+ callDiagnostics: external_exports.boolean().optional(),
14558
14659
  /** Per-user ring budget (#603): calls per rolling day before further calls
14559
14660
  * degrade to banner. Absent = the global default (25). A number, never a
14560
14661
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -14563,9 +14664,6 @@ var UserSettingsSchema2 = external_exports.object({
14563
14664
  * slower speaker). No API-side semantics; the bot resolves each key with its
14564
14665
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
14565
14666
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
14566
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
14567
- * defaulted — an older client PATCHing the full object must not clobber it. */
14568
- pstnCalls: external_exports.boolean().optional(),
14569
14667
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
14570
14668
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
14571
14669
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -14609,7 +14707,7 @@ var ConnectionSummarySchema2 = external_exports.object({
14609
14707
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
14610
14708
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
14611
14709
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
14612
- * apps/api/src/tokens/devices-vs-agents-design.md. */
14710
+ * docs/server/tokens/devices-vs-agents-design.md. */
14613
14711
  kind: external_exports.enum(["device", "agent"]).optional(),
14614
14712
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
14615
14713
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -14627,7 +14725,7 @@ var ConnectionSummarySchema2 = external_exports.object({
14627
14725
  * user on the agent's own page. null = no ceiling (today's behaviour for every
14628
14726
  * connection). Android binds importance to a relationship rather than to each message,
14629
14727
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
14630
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
14728
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
14631
14729
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
14632
14730
  * set once must not overrule a rule they set about one agent. */
14633
14731
  reach: NotifyLevelSchema2.nullable().optional(),
@@ -14638,7 +14736,14 @@ var ConnectionSummarySchema2 = external_exports.object({
14638
14736
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
14639
14737
  * desktop app while open. Null = never seen; stale = offline. */
14640
14738
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
14641
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
14739
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
14740
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
14741
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
14742
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
14743
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
14744
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
14745
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
14746
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
14642
14747
  * harness availabilities + granted workspaces — the option set the phone's
14643
14748
  * "new session" sheet offers. Absent for ordinary MCP agents. */
14644
14749
  runtime: external_exports.object({
@@ -14646,7 +14751,12 @@ var ConnectionSummarySchema2 = external_exports.object({
14646
14751
  * shows its age here (`apps/desktop/src/update.ts`). */
14647
14752
  version: external_exports.string().optional(),
14648
14753
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
14649
- workspaces: external_exports.array(external_exports.string()).optional()
14754
+ workspaces: external_exports.array(external_exports.string()).optional(),
14755
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
14756
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
14757
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
14758
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
14759
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
14650
14760
  }).optional(),
14651
14761
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
14652
14762
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -14729,7 +14839,7 @@ var QueueQuestionSchema2 = external_exports.object({
14729
14839
  * for a settled question whose reply carried nothing readable. */
14730
14840
  answer: external_exports.string().nullable().default(null),
14731
14841
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
14732
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
14842
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
14733
14843
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
14734
14844
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
14735
14845
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -14821,7 +14931,19 @@ var QueueItemSchema2 = external_exports.object({
14821
14931
  said: external_exports.string(),
14822
14932
  at: external_exports.string(),
14823
14933
  entryId: external_exports.string()
14824
- }).nullable().optional()
14934
+ }).nullable().optional(),
14935
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
14936
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
14937
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
14938
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
14939
+ * 20260930222356 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
14940
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
14941
+ *
14942
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
14943
+ * minute after the person speaks erases their instant from it, and the durable traces the client
14944
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
14945
+ * Entry with no `about_id` is in neither. */
14946
+ lastPersonAt: external_exports.string().nullable().optional()
14825
14947
  });
14826
14948
  var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
14827
14949
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
@@ -14851,7 +14973,7 @@ var NoteSchema2 = external_exports.object({
14851
14973
  assignee: external_exports.string().nullable(),
14852
14974
  /** The request thread minted at assignment; null until assigned. */
14853
14975
  parentId: external_exports.string().nullable(),
14854
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
14976
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
14855
14977
  * call — never a deadline. It only ever comes from the user's own words, so when it
14856
14978
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
14857
14979
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -15111,6 +15233,29 @@ var SnapshotSchema2 = external_exports.object({
15111
15233
  computers: external_exports.array(ComputerRowSchema2).nullable()
15112
15234
  }).nullable()
15113
15235
  });
15236
+ var CallRecapSchema2 = external_exports.object({
15237
+ call: external_exports.object({
15238
+ status: external_exports.string(),
15239
+ startedAt: external_exports.string(),
15240
+ durationMs: external_exports.number().nullable(),
15241
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
15242
+ }),
15243
+ topics: external_exports.array(external_exports.object({
15244
+ goalId: external_exports.string().uuid(),
15245
+ title: external_exports.string(),
15246
+ owner: external_exports.string(),
15247
+ state: external_exports.string(),
15248
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
15249
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
15250
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
15251
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
15252
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
15253
+ * anything that is not an answer. */
15254
+ lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional() }))
15255
+ })),
15256
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
15257
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
15258
+ });
15114
15259
 
15115
15260
  // src/paigy/activity.ts
15116
15261
  function shortenPaths(s) {
@@ -15411,7 +15556,7 @@ var AcpDriver = class {
15411
15556
  var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
15412
15557
 
15413
15558
  // src/paigy/inbox.ts
15414
- import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
15559
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
15415
15560
  import { join as join8 } from "path";
15416
15561
  import { homedir as homedir8 } from "os";
15417
15562
  import { randomUUID as randomUUID4 } from "crypto";
@@ -15423,7 +15568,7 @@ function openInbox(dir) {
15423
15568
  const receipt = (name) => join8(dir, "taken", `${name}.note`);
15424
15569
  const taken = (name) => {
15425
15570
  try {
15426
- return statSync2(receipt(name)).mtimeMs;
15571
+ return statSync3(receipt(name)).mtimeMs;
15427
15572
  } catch (error) {
15428
15573
  if (missing(error)) return null;
15429
15574
  throw error;
@@ -15467,7 +15612,7 @@ var alive = (pid) => {
15467
15612
  };
15468
15613
  function inboxFor(root = INBOX_ROOT) {
15469
15614
  try {
15470
- for (const name of readdirSync3(root)) {
15615
+ for (const name of readdirSync4(root)) {
15471
15616
  const pid = Number(name.split("-")[0]);
15472
15617
  if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join8(root, name), { recursive: true, force: true });
15473
15618
  }
@@ -16494,7 +16639,9 @@ function startHost(opts) {
16494
16639
  void heartbeat({
16495
16640
  version: VERSION,
16496
16641
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
16497
- workspaces: listWorkspaces(opts.wsDeps)
16642
+ workspaces: listWorkspaces(opts.wsDeps),
16643
+ // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
16644
+ repos: listRepos(opts.wsDeps)
16498
16645
  }, asHost).catch(() => {
16499
16646
  });
16500
16647
  void listenSlots().then(sweepSlots).catch(() => {
@@ -16849,7 +16996,7 @@ ipcMain.handle("paigy:status", () => ({
16849
16996
  paired: Boolean(deviceToken()),
16850
16997
  device: Boolean(readToken(DEVICE_SLOT)),
16851
16998
  sessions: host?.sessions().map(([, label]) => label) ?? [],
16852
- // The party (companion.md / party design.md): who lives on this machine and what
16999
+ // The party (docs/clients/desktop/companion.md / party design.md): who lives on this machine and what
16853
17000
  // each one is doing THIS SECOND — the one thing the laptop knows better than the phone.
16854
17001
  roster: host?.roster() ?? readHostState()?.roster ?? []
16855
17002
  }));