@paigy/harness 0.3.28 → 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/cli.js CHANGED
@@ -7585,8 +7585,8 @@ var require_webidl = __commonJS({
7585
7585
  return new TypeError(`${message.header}: ${message.message}`);
7586
7586
  };
7587
7587
  webidl.errors.conversionFailed = function(context) {
7588
- const plural2 = context.types.length === 1 ? "" : " one of";
7589
- const message = `${context.argument} could not be converted to${plural2}: ${context.types.join(", ")}.`;
7588
+ const plural3 = context.types.length === 1 ? "" : " one of";
7589
+ const message = `${context.argument} could not be converted to${plural3}: ${context.types.join(", ")}.`;
7590
7590
  return webidl.errors.exception({
7591
7591
  header: context.prefix,
7592
7592
  message
@@ -15050,9 +15050,9 @@ var require_pluralizer = __commonJS({
15050
15050
  this: "these"
15051
15051
  };
15052
15052
  module.exports = class Pluralizer {
15053
- constructor(singular, plural2) {
15053
+ constructor(singular, plural3) {
15054
15054
  this.singular = singular;
15055
- this.plural = plural2;
15055
+ this.plural = plural3;
15056
15056
  }
15057
15057
  pluralize(count) {
15058
15058
  const one = count === 1;
@@ -23743,7 +23743,7 @@ function updateSlot(agent2, patch) {
23743
23743
  }
23744
23744
  function slotIdentity(agent2) {
23745
23745
  const t = readTokenFile()[agent2];
23746
- return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
23746
+ return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null };
23747
23747
  }
23748
23748
  function readToken(agent2 = agentName()) {
23749
23749
  if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
@@ -23783,9 +23783,14 @@ async function pollToken(deviceCode) {
23783
23783
  if (err === "access_denied") throw new Error("Access denied by user. Pairing cancelled.");
23784
23784
  throw new Error(`Unexpected error from /api/device/token: ${err}`);
23785
23785
  }
23786
- function ensureAuthed(res) {
23787
- if (res.status === 401) throw new UnpairedError();
23788
- return res;
23786
+ async function ensureAuthed(res) {
23787
+ if (res.status !== 401) return res;
23788
+ let body = null;
23789
+ try {
23790
+ body = await res.json();
23791
+ } catch {
23792
+ }
23793
+ throw body?.error === "revoked" ? new RevokedError() : new UnpairedError();
23789
23794
  }
23790
23795
  function overrideToken(secret) {
23791
23796
  tokenOverride = secret;
@@ -23795,7 +23800,7 @@ async function fail(what, res) {
23795
23800
  }
23796
23801
  async function createGoal(input, opts = {}) {
23797
23802
  const token = authToken(opts.token) ?? "";
23798
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
23803
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
23799
23804
  method: "POST",
23800
23805
  headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
23801
23806
  body: JSON.stringify(input)
@@ -23805,13 +23810,14 @@ async function createGoal(input, opts = {}) {
23805
23810
  }
23806
23811
  async function claimGoal(goalId, opts = {}) {
23807
23812
  const token = authToken(opts.token) ?? "";
23808
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
23813
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
23809
23814
  if (!res.ok) await fail("claim_goal", res);
23810
23815
  return await res.json();
23811
23816
  }
23812
23817
  async function getGoal(goalId, opts = {}, read = {}) {
23813
23818
  const token = authToken(opts.token) ?? "";
23814
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${read.history ? "?history=1" : ""}`, {
23819
+ const query = [read.history ? "history=1" : "", read.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
23820
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${query ? `?${query}` : ""}`, {
23815
23821
  method: "GET",
23816
23822
  headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
23817
23823
  }));
@@ -23821,12 +23827,12 @@ async function getGoal(goalId, opts = {}, read = {}) {
23821
23827
  async function updateGoal(goalId, input, opts = {}) {
23822
23828
  const token = authToken(opts.token) ?? "";
23823
23829
  const operationId = input.operationId ?? randomUUID2();
23824
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
23830
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
23825
23831
  if (!res.ok) await fail("update_goal", res);
23826
23832
  return await res.json();
23827
23833
  }
23828
23834
  async function hatch(name, voice = null) {
23829
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
23835
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
23830
23836
  method: "POST",
23831
23837
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
23832
23838
  body: JSON.stringify({ name, voice })
@@ -23845,19 +23851,27 @@ async function whoAmI(opts = {}) {
23845
23851
  }
23846
23852
  }
23847
23853
  async function claimSessions(opts = {}) {
23848
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
23854
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
23849
23855
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
23850
23856
  // the HOST's identity
23851
23857
  }));
23852
23858
  if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
23853
23859
  return (await res.json()).sessions;
23854
23860
  }
23861
+ async function recordDecision(decision, opts = {}) {
23862
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
23863
+ method: "POST",
23864
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
23865
+ body: JSON.stringify(decision)
23866
+ }));
23867
+ if (!res.ok) await fail("record_decision", res);
23868
+ }
23855
23869
  async function heartbeat(runtime, opts = {}) {
23856
23870
  const body = {
23857
23871
  ...runtime !== void 0 ? { runtime } : {},
23858
23872
  ...opts.activity !== void 0 ? { activity: opts.activity } : {}
23859
23873
  };
23860
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
23874
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
23861
23875
  method: "POST",
23862
23876
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
23863
23877
  ...Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}
@@ -23865,7 +23879,7 @@ async function heartbeat(runtime, opts = {}) {
23865
23879
  if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
23866
23880
  }
23867
23881
  async function registerDelivery(mode, opts = {}) {
23868
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
23882
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
23869
23883
  method: "POST",
23870
23884
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
23871
23885
  body: JSON.stringify({ mode })
@@ -23874,21 +23888,21 @@ async function registerDelivery(mode, opts = {}) {
23874
23888
  return await res.json();
23875
23889
  }
23876
23890
  async function listNotes(opts = {}) {
23877
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
23891
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
23878
23892
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
23879
23893
  }));
23880
23894
  if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
23881
23895
  return await res.json();
23882
23896
  }
23883
23897
  async function listConnections(opts = {}) {
23884
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
23898
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
23885
23899
  headers: { authorization: `Bearer ${authToken(opts.token)}`, "x-paigy-model": "goal-entry-v1" }
23886
23900
  }));
23887
23901
  if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
23888
23902
  return await res.json();
23889
23903
  }
23890
23904
  async function submitTriage(run, opts = {}) {
23891
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
23905
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
23892
23906
  method: "POST",
23893
23907
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
23894
23908
  body: JSON.stringify(run)
@@ -23897,7 +23911,7 @@ async function submitTriage(run, opts = {}) {
23897
23911
  return await res.json();
23898
23912
  }
23899
23913
  async function acceptTriage(proposalId, body, opts = {}) {
23900
- const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
23914
+ const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
23901
23915
  method: "POST",
23902
23916
  headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
23903
23917
  body: JSON.stringify(body)
@@ -23952,7 +23966,7 @@ async function contact(input, opts = {}) {
23952
23966
  ask: a.ask,
23953
23967
  options: a.options ?? []
23954
23968
  }));
23955
- const res = ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
23969
+ const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
23956
23970
  method: "POST",
23957
23971
  headers,
23958
23972
  signal: opts.signal,
@@ -23974,15 +23988,17 @@ async function contact(input, opts = {}) {
23974
23988
  }
23975
23989
  const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
23976
23990
  const joinedCard = deliveries[0]?.joinedCard === true;
23991
+ const joinedCall = deliveries[0]?.joinedCall === true;
23977
23992
  const read = async (signal2) => {
23978
- const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23993
+ const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23979
23994
  if (!res.ok) await fail("read_delivery", res);
23980
23995
  const one = await res.json();
23981
23996
  return {
23982
23997
  ...one,
23983
23998
  ...all.length > 1 ? { deliveries: all } : {},
23984
23999
  ...recorded.length ? { recorded, notSent: NOT_SENT } : {},
23985
- ...joinedCard ? { joinedCard: true } : {}
24000
+ ...joinedCard ? { joinedCard: true } : {},
24001
+ ...joinedCall ? { joinedCall: true } : {}
23986
24002
  };
23987
24003
  };
23988
24004
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
@@ -24004,7 +24020,7 @@ async function contact(input, opts = {}) {
24004
24020
  }
24005
24021
  }
24006
24022
  async function checkReplies(opts = {}) {
24007
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
24023
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
24008
24024
  headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
24009
24025
  }));
24010
24026
  if (!res.ok) await fail("check_replies", res);
@@ -24170,7 +24186,7 @@ async function subscribeWake(onNudge, opts = {}) {
24170
24186
  }
24171
24187
  };
24172
24188
  }
24173
- 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, HistoryItemSchema, 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, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
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, 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;
24174
24190
  var init_dist = __esm({
24175
24191
  "../../packages/sdk/dist/index.js"() {
24176
24192
  "use strict";
@@ -24583,18 +24599,18 @@ var init_dist = __esm({
24583
24599
  parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
24584
24600
  repo: external_exports.string().optional().describe("Optional repository context."),
24585
24601
  ask: external_exports.string().trim().min(1).max(1e4).describe(
24586
- "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 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
24602
+ "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."
24587
24603
  ),
24588
- options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
24604
+ options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
24589
24605
  }).strict();
24590
24606
  StartContactSchema = external_exports.object({
24591
24607
  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."),
24592
- waiting: external_exports.enum(["none", "hard"]).default("none"),
24608
+ waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
24593
24609
  channel: external_exports.enum(["notification", "call"]).default("notification")
24594
24610
  }).strict();
24595
24611
  ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
24596
24612
  CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
24597
- 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 \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: 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).";
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.";
24598
24614
  CreateGoalSchema = external_exports.object({
24599
24615
  outcome: external_exports.string().trim().min(1).max(1e4),
24600
24616
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -24632,7 +24648,11 @@ var init_dist = __esm({
24632
24648
  state: external_exports.enum(["active", "done", "cancelled"]).optional(),
24633
24649
  progress: external_exports.string().trim().min(1).max(1e4).optional(),
24634
24650
  reviewed: external_exports.literal(true).optional(),
24635
- dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
24651
+ dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
24652
+ /** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
24653
+ * (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
24654
+ * by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
24655
+ withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
24636
24656
  }).strict().refine((v) => Object.keys(v).length > 0),
24637
24657
  reason: external_exports.string().trim().min(1).max(2e3),
24638
24658
  operationId: external_exports.string().uuid().optional()
@@ -24643,10 +24663,14 @@ var init_dist = __esm({
24643
24663
  goalId: external_exports.string().uuid(),
24644
24664
  /** Every entry in full. Without it the read carries the person's words, open questions and your
24645
24665
  * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
24646
- history: external_exports.boolean().optional()
24666
+ history: external_exports.boolean().optional(),
24667
+ /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
24668
+ * about this work, each answer attributed to the reader that gave it, and every disagreement
24669
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
24670
+ diagnose: external_exports.boolean().optional()
24647
24671
  }).strict();
24648
- 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.";
24649
- 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. 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. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
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.`;
24650
24674
  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.";
24651
24675
  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.";
24652
24676
  CheckRepliesSchema = external_exports.object({}).strict();
@@ -24660,6 +24684,7 @@ var init_dist = __esm({
24660
24684
  ];
24661
24685
  AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
24662
24686
  LIVE_MS = 3 * 6e4;
24687
+ WORKING_MS = 30 * 6e4;
24663
24688
  ContextSchema = external_exports.object({
24664
24689
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
24665
24690
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -24730,7 +24755,7 @@ var init_dist = __esm({
24730
24755
  points: external_exports.array(external_exports.string()).nullable(),
24731
24756
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
24732
24757
  blocking: external_exports.boolean(),
24733
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
24758
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
24734
24759
  * slice wires it; optional so today's rows/callers project cleanly. */
24735
24760
  deadline: external_exports.string().datetime().nullable().optional()
24736
24761
  });
@@ -24781,7 +24806,7 @@ var init_dist = __esm({
24781
24806
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
24782
24807
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
24783
24808
  // the ask into topics and gives each one at most three sentences and one question
24784
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
24809
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
24785
24810
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
24786
24811
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
24787
24812
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -24858,7 +24883,7 @@ var init_dist = __esm({
24858
24883
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
24859
24884
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
24860
24885
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
24861
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
24886
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
24862
24887
  * delivered through the same settle/await path as a human answer, carrying the judge's
24863
24888
  * derivation and the precedent ids it grew from. Always paired with a visible trail
24864
24889
  * card the user can reply to — the broker never overrides the user. */
@@ -24907,7 +24932,7 @@ var init_dist = __esm({
24907
24932
  * and now knows exactly which call to make. Absent when either half is missing —
24908
24933
  * a sentence with a hole in it is worse than no sentence. */
24909
24934
  note: external_exports.string().optional(),
24910
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
24935
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
24911
24936
  * shaped answer was mapped from, filtered to its own claims. There is no second list
24912
24937
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
24913
24938
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -25003,10 +25028,11 @@ var init_dist = __esm({
25003
25028
  agentId: external_exports.string().optional(),
25004
25029
  select: SelectShapeSchema.optional(),
25005
25030
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
25006
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
25007
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
25008
- * (the beat for context, the answer window for asks). Clamped bot-side. */
25009
- pace: external_exports.number().positive().optional(),
25031
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
25032
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
25033
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
25034
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
25035
+ anything. A caller's own `beat_s` tuning is what it used to override. */
25010
25036
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
25011
25037
  * (a question blocks, context flows). blocking:false on a question = ask and move
25012
25038
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -25041,7 +25067,7 @@ var init_dist = __esm({
25041
25067
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
25042
25068
  * `createdAt`. */
25043
25069
  agentStateAt: external_exports.string().datetime().optional(),
25044
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
25070
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
25045
25071
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
25046
25072
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
25047
25073
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -25102,7 +25128,7 @@ var init_dist = __esm({
25102
25128
  * off the same open list the card came from, so it clears when the Call does. A card is the
25103
25129
  * backup for a call not taken; while the call has it, the call is where it is answered. */
25104
25130
  onCall: external_exports.literal(true).optional(),
25105
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
25131
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
25106
25132
  * declined, and what the ladder will do next — read off the cron's own row, never computed
25107
25133
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
25108
25134
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -25133,7 +25159,7 @@ var init_dist = __esm({
25133
25159
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
25134
25160
  ),
25135
25161
  /** Real downstream work is stuck behind this one — set by the agent, independent of
25136
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
25162
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
25137
25163
  * inbox's blocking badge and the extra confirm step before dismissing it. */
25138
25164
  blocking: external_exports.boolean().default(false),
25139
25165
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -25207,9 +25233,16 @@ var init_dist = __esm({
25207
25233
  banner: external_exports.boolean(),
25208
25234
  push: external_exports.boolean()
25209
25235
  }),
25236
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
25237
+ * they are an enum on a live column across every account, and the rename is a rename of
25238
+ * what people read (owner, 2026-09-30). */
25210
25239
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
25211
- silentPush: external_exports.boolean(),
25212
- autoCallback: external_exports.boolean(),
25240
+ /** DEAD, AND ACCEPTED ANYWAY (#2813). DateNight is inbox-only with no way back, so nothing
25241
+ * reads this. It stays OPTIONAL rather than deleted because a phone on the old bundle
25242
+ * PATCHes the whole settings object and would be refused for sending a key we stopped
25243
+ * wanting — the same trap `voiceMode` below already documents. #2813 drops the field and
25244
+ * its column once no installed build still sends it. */
25245
+ silentPush: external_exports.boolean().optional(),
25213
25246
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
25214
25247
  improveConsent: external_exports.boolean(),
25215
25248
  missedCall: MissedCallSchema.default("backoff_standard"),
@@ -25220,12 +25253,16 @@ var init_dist = __esm({
25220
25253
  * must not silently reset this privacy choice. Absent = leave unchanged on
25221
25254
  * write, 'hosted' on read (see store.ts). */
25222
25255
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
25223
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
25256
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
25224
25257
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
25225
25258
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
25226
25259
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
25227
25260
  * read (see store.ts). */
25228
25261
  talk: external_exports.boolean().optional(),
25262
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
25263
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
25264
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
25265
+ callDiagnostics: external_exports.boolean().optional(),
25229
25266
  /** Per-user ring budget (#603): calls per rolling day before further calls
25230
25267
  * degrade to banner. Absent = the global default (25). A number, never a
25231
25268
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -25234,9 +25271,6 @@ var init_dist = __esm({
25234
25271
  * slower speaker). No API-side semantics; the bot resolves each key with its
25235
25272
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
25236
25273
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
25237
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
25238
- * defaulted — an older client PATCHing the full object must not clobber it. */
25239
- pstnCalls: external_exports.boolean().optional(),
25240
25274
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
25241
25275
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
25242
25276
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -25250,18 +25284,20 @@ var init_dist = __esm({
25250
25284
  * clobber guard as voiceMode: absent = leave unchanged on write. */
25251
25285
  broker: BrokerTuningSchema.optional()
25252
25286
  });
25253
- HistoryItemSchema = external_exports.object({
25287
+ HistoryWorkSchema = external_exports.object({
25254
25288
  id: external_exports.string(),
25255
- /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
25256
- initiator: external_exports.enum(["user", "agent"]),
25257
25289
  title: external_exports.string(),
25258
- /** The agent on the other end (its name). */
25259
- name: external_exports.string(),
25260
- createdAt: external_exports.string(),
25261
- /** When the agent fetched your request (user→agent only). */
25262
- agentAckedAt: external_exports.string().nullable(),
25263
- /** When you answered the agent's notification (agent→user only). */
25264
- humanAckedAt: external_exports.string().nullable()
25290
+ state: external_exports.enum(["done", "cancelled"]),
25291
+ /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
25292
+ assignee: external_exports.string()
25293
+ });
25294
+ HistoryEntrySchema = external_exports.union([
25295
+ external_exports.object({ at: external_exports.string(), card: InboxItemSchema }),
25296
+ external_exports.object({ at: external_exports.string(), work: HistoryWorkSchema })
25297
+ ]);
25298
+ HistoryPageSchema = external_exports.object({
25299
+ entries: external_exports.array(HistoryEntrySchema),
25300
+ next: external_exports.string().nullable()
25265
25301
  });
25266
25302
  ACTIVITY_LINES = 2;
25267
25303
  ACTIVITY_LINE_MAX = 80;
@@ -25278,7 +25314,7 @@ var init_dist = __esm({
25278
25314
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
25279
25315
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
25280
25316
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
25281
- * apps/api/src/tokens/devices-vs-agents-design.md. */
25317
+ * docs/server/tokens/devices-vs-agents-design.md. */
25282
25318
  kind: external_exports.enum(["device", "agent"]).optional(),
25283
25319
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
25284
25320
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -25296,7 +25332,7 @@ var init_dist = __esm({
25296
25332
  * user on the agent's own page. null = no ceiling (today's behaviour for every
25297
25333
  * connection). Android binds importance to a relationship rather than to each message,
25298
25334
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
25299
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
25335
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
25300
25336
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
25301
25337
  * set once must not overrule a rule they set about one agent. */
25302
25338
  reach: NotifyLevelSchema.nullable().optional(),
@@ -25307,7 +25343,14 @@ var init_dist = __esm({
25307
25343
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
25308
25344
  * desktop app while open. Null = never seen; stale = offline. */
25309
25345
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
25310
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
25346
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
25347
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
25348
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
25349
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
25350
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
25351
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
25352
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
25353
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
25311
25354
  * harness availabilities + granted workspaces — the option set the phone's
25312
25355
  * "new session" sheet offers. Absent for ordinary MCP agents. */
25313
25356
  runtime: external_exports.object({
@@ -25315,7 +25358,12 @@ var init_dist = __esm({
25315
25358
  * shows its age here (`apps/desktop/src/update.ts`). */
25316
25359
  version: external_exports.string().optional(),
25317
25360
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
25318
- workspaces: external_exports.array(external_exports.string()).optional()
25361
+ workspaces: external_exports.array(external_exports.string()).optional(),
25362
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
25363
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
25364
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
25365
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
25366
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
25319
25367
  }).optional(),
25320
25368
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
25321
25369
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -25398,7 +25446,7 @@ var init_dist = __esm({
25398
25446
  * for a settled question whose reply carried nothing readable. */
25399
25447
  answer: external_exports.string().nullable().default(null),
25400
25448
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
25401
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
25449
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
25402
25450
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
25403
25451
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
25404
25452
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -25445,6 +25493,9 @@ var init_dist = __esm({
25445
25493
  progressLine: external_exports.string().nullable().optional(),
25446
25494
  reviewPending: external_exports.boolean().default(false),
25447
25495
  dueAt: external_exports.string().nullable().default(null),
25496
+ /** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
25497
+ * with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
25498
+ finishedAt: external_exports.string().nullable().optional(),
25448
25499
  /** The Goal this one was opened under; null at the root. */
25449
25500
  parentGoalId: external_exports.string().nullable().default(null),
25450
25501
  /** Goals opened under this one — only those the same list holds. */
@@ -25453,8 +25504,17 @@ var init_dist = __esm({
25453
25504
  dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
25454
25505
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
25455
25506
  blocked: external_exports.boolean().default(false),
25456
- /** Every decision need on it, open or settled — the page decides which to show. */
25507
+ /** Its questions: every OPEN one, and at most ten settled, newest settled first
25508
+ * (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
25509
+ * `answered` are, and a settled one's words are a line (280 characters), its body read when the
25510
+ * question is opened. */
25457
25511
  questions: external_exports.array(QueueQuestionSchema).default([]),
25512
+ /** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
25513
+ * bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
25514
+ * right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
25515
+ * by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
25516
+ asked: external_exports.number().optional(),
25517
+ answered: external_exports.number().optional(),
25458
25518
  /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
25459
25519
  * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
25460
25520
  * not spell an empty list. */
@@ -25478,7 +25538,19 @@ var init_dist = __esm({
25478
25538
  said: external_exports.string(),
25479
25539
  at: external_exports.string(),
25480
25540
  entryId: external_exports.string()
25481
- }).nullable().optional()
25541
+ }).nullable().optional(),
25542
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
25543
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
25544
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
25545
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
25546
+ * 20260930222356 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
25547
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
25548
+ *
25549
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
25550
+ * minute after the person speaks erases their instant from it, and the durable traces the client
25551
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
25552
+ * Entry with no `about_id` is in neither. */
25553
+ lastPersonAt: external_exports.string().nullable().optional()
25482
25554
  });
25483
25555
  COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
25484
25556
  NoteSourceSchema = external_exports.enum(["app", "call"]);
@@ -25508,7 +25580,7 @@ var init_dist = __esm({
25508
25580
  assignee: external_exports.string().nullable(),
25509
25581
  /** The request thread minted at assignment; null until assigned. */
25510
25582
  parentId: external_exports.string().nullable(),
25511
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
25583
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
25512
25584
  * call — never a deadline. It only ever comes from the user's own words, so when it
25513
25585
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
25514
25586
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -25591,6 +25663,19 @@ var init_dist = __esm({
25591
25663
  * carries credentials; only a `poll` registration can come back with null here. */
25592
25664
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
25593
25665
  });
25666
+ HostDecisionSchema = external_exports.object({
25667
+ /** The agent's token id: the row's `recipient`. */
25668
+ agent: external_exports.string().uuid(),
25669
+ decision: external_exports.enum(["stood_back", "took_over"]),
25670
+ /** The work it was about: the Goal `claim_goal` would hand that agent next. */
25671
+ goalId: external_exports.string().uuid().nullable().optional(),
25672
+ /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
25673
+ seenAt: external_exports.string().datetime().nullable().optional(),
25674
+ /** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
25675
+ since: external_exports.string().datetime().nullable().optional(),
25676
+ /** What the host said, in its log's own words: why it stood back, or what the take-over did. */
25677
+ said: external_exports.string().max(300).optional()
25678
+ });
25594
25679
  WakeNudgeSchema = external_exports.object({
25595
25680
  kind: external_exports.enum(["reply", "request", "callback"]),
25596
25681
  notificationId: external_exports.string().optional(),
@@ -25635,6 +25720,9 @@ var init_dist = __esm({
25635
25720
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
25636
25721
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
25637
25722
  workspace: external_exports.string().nullable().optional(),
25723
+ /** Local host recovery must preserve the launch's runtime and Paigy identity. */
25724
+ harness: external_exports.enum(["claude", "codex", "agy"]).optional(),
25725
+ session_id: external_exports.string().uuid().optional(),
25638
25726
  uik_pub: external_exports.string().nullable().optional()
25639
25727
  });
25640
25728
  SupportRequestSchema = external_exports.object({
@@ -25754,6 +25842,29 @@ var init_dist = __esm({
25754
25842
  computers: external_exports.array(ComputerRowSchema).nullable()
25755
25843
  }).nullable()
25756
25844
  });
25845
+ CallRecapSchema = external_exports.object({
25846
+ call: external_exports.object({
25847
+ status: external_exports.string(),
25848
+ startedAt: external_exports.string(),
25849
+ durationMs: external_exports.number().nullable(),
25850
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
25851
+ }),
25852
+ topics: external_exports.array(external_exports.object({
25853
+ goalId: external_exports.string().uuid(),
25854
+ title: external_exports.string(),
25855
+ owner: external_exports.string(),
25856
+ state: external_exports.string(),
25857
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
25858
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
25859
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
25860
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
25861
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
25862
+ * anything that is not an answer. */
25863
+ lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional() }))
25864
+ })),
25865
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
25866
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
25867
+ });
25757
25868
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
25758
25869
  KEY_PATH = join22(homedir22(), ".paigy", "key.json");
25759
25870
  sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -25763,6 +25874,13 @@ var init_dist = __esm({
25763
25874
  this.name = "UnpairedError";
25764
25875
  }
25765
25876
  };
25877
+ RevokedError = class extends UnpairedError {
25878
+ constructor() {
25879
+ super();
25880
+ this.message = "This identity was revoked (the API answered 401 revoked): its credential is gone for good. Pairing again makes a new identity.";
25881
+ this.name = "RevokedError";
25882
+ }
25883
+ };
25766
25884
  ApiError = class extends Error {
25767
25885
  status;
25768
25886
  body;
@@ -30358,132 +30476,6 @@ var require_lib = __commonJS({
30358
30476
  }
30359
30477
  });
30360
30478
 
30361
- // src/triage/judge.ts
30362
- import { spawnSync as spawnSync2 } from "child_process";
30363
- import { homedir as homedir9 } from "os";
30364
- import { delimiter as delimiter3 } from "path";
30365
- function pickOllamaModel(tags) {
30366
- if (tags.length === 0) return null;
30367
- const score = (tag) => {
30368
- const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
30369
- return i === -1 ? PREFERRED.length : i;
30370
- };
30371
- return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
30372
- }
30373
- function parseOllamaTags(body) {
30374
- const models = body?.models;
30375
- if (!Array.isArray(models)) return [];
30376
- return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
30377
- }
30378
- async function fetchTags(host) {
30379
- try {
30380
- const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
30381
- if (!res.ok) return null;
30382
- return parseOllamaTags(await res.json());
30383
- } catch {
30384
- return null;
30385
- }
30386
- }
30387
- async function available(deps = {}) {
30388
- const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
30389
- const model = tags ? pickOllamaModel(tags) : null;
30390
- const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
30391
- const detected = (deps.detect ?? detectAll)();
30392
- const harnesses = CATALOG.map((entry) => {
30393
- const state = detected.find((d) => d.name === entry.name);
30394
- const usable = state?.status === "ready" || state?.status === "adapter-missing";
30395
- return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
30396
- });
30397
- return [ollama, ...harnesses];
30398
- }
30399
- function refusal(options, asked2) {
30400
- const lines2 = [
30401
- asked2 ? `Can't judge with --via ${asked2}: ${options.find((o) => o.provider === asked2)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
30402
- "",
30403
- "Triage runs the model where your queue already lives (#1106). Your options:"
30404
- ];
30405
- for (const o of options) {
30406
- lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
30407
- if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
30408
- }
30409
- lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
30410
- return lines2.join("\n");
30411
- }
30412
- async function judgeFor(asked2, deps = {}) {
30413
- const options = await available(deps);
30414
- const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
30415
- if (!chosen?.ready) return { error: refusal(options, asked2) };
30416
- if (chosen.provider === "ollama") {
30417
- const model = chosen.model;
30418
- return {
30419
- provider: "ollama",
30420
- model,
30421
- run: async (prompt) => {
30422
- const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
30423
- method: "POST",
30424
- headers: { "content-type": "application/json" },
30425
- // Non-streaming: we want the whole answer once, and the parser is the same
30426
- // either way. `temperature: 0` because this is a classification, not prose.
30427
- body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
30428
- signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
30429
- });
30430
- if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
30431
- return (await res.json()).response ?? "";
30432
- }
30433
- };
30434
- }
30435
- const runner = RUNNERS[chosen.provider];
30436
- if (!runner) return { error: refusal(options, asked2) };
30437
- const bin = which(runner.cli);
30438
- if (!bin) return { error: refusal(options, asked2) };
30439
- return {
30440
- provider: chosen.provider,
30441
- model: null,
30442
- run: async (prompt) => {
30443
- const out = spawnSync2(bin, runner.argv(null), {
30444
- input: prompt,
30445
- encoding: "utf8",
30446
- cwd: homedir9(),
30447
- timeout: RUN_TIMEOUT_MS,
30448
- maxBuffer: 32 * 1024 * 1024,
30449
- env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
30450
- });
30451
- if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
30452
- if (out.status !== 0 && !out.stdout?.trim())
30453
- throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
30454
- return out.stdout ?? "";
30455
- }
30456
- };
30457
- }
30458
- var PRIVACY, OLLAMA_HOST, PREFERRED, RUNNERS, RUN_TIMEOUT_MS;
30459
- var init_judge = __esm({
30460
- "src/triage/judge.ts"() {
30461
- "use strict";
30462
- init_catalog();
30463
- PRIVACY = {
30464
- ollama: "never leaves this machine",
30465
- claude: "this machine + your own Claude subscription; Paigy's servers never see it",
30466
- codex: "this machine + your own Codex subscription; Paigy's servers never see it",
30467
- agy: "this machine + your own Antigravity account; Paigy's servers never see it"
30468
- };
30469
- OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
30470
- PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
30471
- RUNNERS = {
30472
- // Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
30473
- ollama: null,
30474
- // `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
30475
- claude: { cli: "claude", argv: () => ["-p"] },
30476
- // `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
30477
- // when none is given as an argument. It prints its own preamble around the answer, which
30478
- // is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
30479
- codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
30480
- // `agy --print` (alias -p) is Antigravity's print mode, text output by default.
30481
- agy: { cli: "agy", argv: () => ["-p"] }
30482
- };
30483
- RUN_TIMEOUT_MS = 5 * 6e4;
30484
- }
30485
- });
30486
-
30487
30479
  // src/triage/evidence.ts
30488
30480
  function assigneeToken(note2) {
30489
30481
  const ref = note2.assignee ?? "";
@@ -30659,6 +30651,132 @@ var init_evidence = __esm({
30659
30651
  }
30660
30652
  });
30661
30653
 
30654
+ // src/triage/judge.ts
30655
+ import { spawnSync as spawnSync2 } from "child_process";
30656
+ import { homedir as homedir5 } from "os";
30657
+ import { delimiter as delimiter3 } from "path";
30658
+ function pickOllamaModel(tags) {
30659
+ if (tags.length === 0) return null;
30660
+ const score = (tag) => {
30661
+ const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
30662
+ return i === -1 ? PREFERRED.length : i;
30663
+ };
30664
+ return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
30665
+ }
30666
+ function parseOllamaTags(body) {
30667
+ const models = body?.models;
30668
+ if (!Array.isArray(models)) return [];
30669
+ return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
30670
+ }
30671
+ async function fetchTags(host) {
30672
+ try {
30673
+ const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
30674
+ if (!res.ok) return null;
30675
+ return parseOllamaTags(await res.json());
30676
+ } catch {
30677
+ return null;
30678
+ }
30679
+ }
30680
+ async function available(deps = {}) {
30681
+ const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
30682
+ const model = tags ? pickOllamaModel(tags) : null;
30683
+ const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
30684
+ const detected = (deps.detect ?? detectAll)();
30685
+ const harnesses = CATALOG.map((entry) => {
30686
+ const state = detected.find((d) => d.name === entry.name);
30687
+ const usable = state?.status === "ready" || state?.status === "adapter-missing";
30688
+ return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
30689
+ });
30690
+ return [ollama, ...harnesses];
30691
+ }
30692
+ function refusal(options, asked2) {
30693
+ const lines2 = [
30694
+ asked2 ? `Can't judge with --via ${asked2}: ${options.find((o) => o.provider === asked2)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
30695
+ "",
30696
+ "Triage runs the model where your queue already lives (#1106). Your options:"
30697
+ ];
30698
+ for (const o of options) {
30699
+ lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
30700
+ if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
30701
+ }
30702
+ lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
30703
+ return lines2.join("\n");
30704
+ }
30705
+ async function judgeFor(asked2, deps = {}) {
30706
+ const options = await available(deps);
30707
+ const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
30708
+ if (!chosen?.ready) return { error: refusal(options, asked2) };
30709
+ if (chosen.provider === "ollama") {
30710
+ const model = chosen.model;
30711
+ return {
30712
+ provider: "ollama",
30713
+ model,
30714
+ run: async (prompt) => {
30715
+ const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
30716
+ method: "POST",
30717
+ headers: { "content-type": "application/json" },
30718
+ // Non-streaming: we want the whole answer once, and the parser is the same
30719
+ // either way. `temperature: 0` because this is a classification, not prose.
30720
+ body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
30721
+ signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
30722
+ });
30723
+ if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
30724
+ return (await res.json()).response ?? "";
30725
+ }
30726
+ };
30727
+ }
30728
+ const runner = RUNNERS[chosen.provider];
30729
+ if (!runner) return { error: refusal(options, asked2) };
30730
+ const bin = which(runner.cli);
30731
+ if (!bin) return { error: refusal(options, asked2) };
30732
+ return {
30733
+ provider: chosen.provider,
30734
+ model: null,
30735
+ run: async (prompt) => {
30736
+ const out = spawnSync2(bin, runner.argv(null), {
30737
+ input: prompt,
30738
+ encoding: "utf8",
30739
+ cwd: homedir5(),
30740
+ timeout: RUN_TIMEOUT_MS,
30741
+ maxBuffer: 32 * 1024 * 1024,
30742
+ env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
30743
+ });
30744
+ if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
30745
+ if (out.status !== 0 && !out.stdout?.trim())
30746
+ throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
30747
+ return out.stdout ?? "";
30748
+ }
30749
+ };
30750
+ }
30751
+ var PRIVACY, OLLAMA_HOST, PREFERRED, RUNNERS, RUN_TIMEOUT_MS;
30752
+ var init_judge = __esm({
30753
+ "src/triage/judge.ts"() {
30754
+ "use strict";
30755
+ init_catalog();
30756
+ PRIVACY = {
30757
+ ollama: "never leaves this machine",
30758
+ claude: "this machine + your own Claude subscription; Paigy's servers never see it",
30759
+ codex: "this machine + your own Codex subscription; Paigy's servers never see it",
30760
+ agy: "this machine + your own Antigravity account; Paigy's servers never see it"
30761
+ };
30762
+ OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
30763
+ PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
30764
+ RUNNERS = {
30765
+ // Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
30766
+ ollama: null,
30767
+ // `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
30768
+ claude: { cli: "claude", argv: () => ["-p"] },
30769
+ // `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
30770
+ // when none is given as an argument. It prints its own preamble around the answer, which
30771
+ // is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
30772
+ codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
30773
+ // `agy --print` (alias -p) is Antigravity's print mode, text output by default.
30774
+ agy: { cli: "agy", argv: () => ["-p"] }
30775
+ };
30776
+ RUN_TIMEOUT_MS = 5 * 6e4;
30777
+ }
30778
+ });
30779
+
30662
30780
  // src/triage/verdict.ts
30663
30781
  function extractJson(raw) {
30664
30782
  const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
@@ -32861,18 +32979,18 @@ var AskInputSchema2 = external_exports.object({
32861
32979
  parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
32862
32980
  repo: external_exports.string().optional().describe("Optional repository context."),
32863
32981
  ask: external_exports.string().trim().min(1).max(1e4).describe(
32864
- "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 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
32982
+ "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."
32865
32983
  ),
32866
- options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
32984
+ options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
32867
32985
  }).strict();
32868
32986
  var StartContactSchema2 = external_exports.object({
32869
32987
  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."),
32870
- waiting: external_exports.enum(["none", "hard"]).default("none"),
32988
+ waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
32871
32989
  channel: external_exports.enum(["notification", "call"]).default("notification")
32872
32990
  }).strict();
32873
32991
  var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
32874
32992
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
32875
- 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 \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: 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).";
32993
+ 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.";
32876
32994
  var CreateGoalSchema2 = external_exports.object({
32877
32995
  outcome: external_exports.string().trim().min(1).max(1e4),
32878
32996
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -32910,7 +33028,11 @@ var UpdateGoalSchema2 = external_exports.object({
32910
33028
  state: external_exports.enum(["active", "done", "cancelled"]).optional(),
32911
33029
  progress: external_exports.string().trim().min(1).max(1e4).optional(),
32912
33030
  reviewed: external_exports.literal(true).optional(),
32913
- dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
33031
+ dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
33032
+ /** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
33033
+ * (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
33034
+ * by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
33035
+ withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
32914
33036
  }).strict().refine((v) => Object.keys(v).length > 0),
32915
33037
  reason: external_exports.string().trim().min(1).max(2e3),
32916
33038
  operationId: external_exports.string().uuid().optional()
@@ -32921,10 +33043,14 @@ var GetGoalSchema2 = external_exports.object({
32921
33043
  goalId: external_exports.string().uuid(),
32922
33044
  /** Every entry in full. Without it the read carries the person's words, open questions and your
32923
33045
  * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
32924
- history: external_exports.boolean().optional()
33046
+ history: external_exports.boolean().optional(),
33047
+ /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
33048
+ * about this work, each answer attributed to the reader that gave it, and every disagreement
33049
+ * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
33050
+ diagnose: external_exports.boolean().optional()
32925
33051
  }).strict();
32926
- 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.";
32927
- 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. 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. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
33052
+ 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.";
33053
+ 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.`;
32928
33054
  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.";
32929
33055
  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.";
32930
33056
  var CheckRepliesSchema2 = external_exports.object({}).strict();
@@ -32937,7 +33063,6 @@ var AGENT_TOOLS2 = [
32937
33063
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
32938
33064
  ];
32939
33065
  var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
32940
- var DONE_MESSAGE = "When the work is done, send one message through contact saying what is done and anything the person needs to do or check.";
32941
33066
  function entryWords(entry) {
32942
33067
  const content = entry.content;
32943
33068
  if (content && "sealed" in content) return "";
@@ -32954,6 +33079,7 @@ function entryWords(entry) {
32954
33079
  return entry.sources.map((source) => source.text).join("\n");
32955
33080
  }
32956
33081
  var LIVE_MS2 = 3 * 6e4;
33082
+ var WORKING_MS2 = 30 * 6e4;
32957
33083
  var ContextSchema2 = external_exports.object({
32958
33084
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
32959
33085
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -33024,7 +33150,7 @@ var AttentionSchema2 = external_exports.object({
33024
33150
  points: external_exports.array(external_exports.string()).nullable(),
33025
33151
  /** Whether the ask blocks the sender — what lets arbitration escalate it on silence. */
33026
33152
  blocking: external_exports.boolean(),
33027
- /** Reserved (MODEL.md lists it): a response deadline. No row column yet — a later Phase 2
33153
+ /** Reserved (docs/model/model.md lists it): a response deadline. No row column yet — a later Phase 2
33028
33154
  * slice wires it; optional so today's rows/callers project cleanly. */
33029
33155
  deadline: external_exports.string().datetime().nullable().optional()
33030
33156
  });
@@ -33075,7 +33201,7 @@ var NotifyRequestFields2 = external_exports.object({
33075
33201
  * derive agent-side before sealing, so the server only ever shapes plaintext). */
33076
33202
  // 10k, not a sentence budget. What the human hears is bounded by the BROKER — it splits
33077
33203
  // the ask into topics and gives each one at most three sentences and one question
33078
- // (broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
33204
+ // (docs/brain/broker/agenda-design.md) — not by a wire cap the agent has to pre-summarize under.
33079
33205
  // Owner, 2026-07-28: "our actual limitation on how long something is to the user should
33080
33206
  // come from the broker splitting and summarizing." The cap that remains is a size guard.
33081
33207
  ask: external_exports.string().min(1).max(1e4).optional().describe(
@@ -33152,7 +33278,7 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
33152
33278
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
33153
33279
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
33154
33280
  external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
33155
- /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
33281
+ /** An auto-answer derived from the user's PAST decisions (docs/brain/broker/precedent-design.md §2):
33156
33282
  * delivered through the same settle/await path as a human answer, carrying the judge's
33157
33283
  * derivation and the precedent ids it grew from. Always paired with a visible trail
33158
33284
  * card the user can reply to — the broker never overrides the user. */
@@ -33201,7 +33327,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
33201
33327
  * and now knows exactly which call to make. Absent when either half is missing —
33202
33328
  * a sentence with a hole in it is worse than no sentence. */
33203
33329
  note: external_exports.string().optional(),
33204
- /** The call record rendered for THIS agent (`voice/record-design.md`): the words the
33330
+ /** The call record rendered for THIS agent (`docs/brain/voice/record-design.md`): the words the
33205
33331
  * shaped answer was mapped from, filtered to its own claims. There is no second list
33206
33332
  * of labels beside it — the acts went 2026-09-04 and `intents` went with them (owner,
33207
33333
  * 2026-09-04): the agent reads the sentence and decides. */
@@ -33297,10 +33423,11 @@ var AgendaTurnSchema2 = external_exports.object({
33297
33423
  agentId: external_exports.string().optional(),
33298
33424
  select: SelectShapeSchema2.optional(),
33299
33425
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
33300
- /** Pacing (#826, owner 2026-08-03: "how fast we move through them ... are parameters"):
33301
- * seconds the floor stays open after this turn speaks. Absent = the bot's defaults
33302
- * (the beat for context, the answer window for asks). Clamped bot-side. */
33303
- pace: external_exports.number().positive().optional(),
33426
+ /* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
33427
+ paces itself now — a short beat between the sentences of a turn, the longer one at its end
33428
+ (owner, 2026-09-30: "remove the bot deciding pace") — and it does that where the words are
33429
+ spoken, not where the plan is written, so nothing between the brain and the walk decides
33430
+ anything. A caller's own `beat_s` tuning is what it used to override. */
33304
33431
  /** Whether the walk WAITS for an answer before moving on. Absent = derived as today
33305
33432
  * (a question blocks, context flows). blocking:false on a question = ask and move
33306
33433
  * on, the claim stays pending; blocking:true on context = hold for a reply. */
@@ -33335,7 +33462,7 @@ var InboxItemSchema2 = external_exports.object({
33335
33462
  * instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
33336
33463
  * `createdAt`. */
33337
33464
  agentStateAt: external_exports.string().datetime().optional(),
33338
- /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
33465
+ /** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (docs/clients/app/walk/design.md §11, owner
33339
33466
  * 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
33340
33467
  * superseded or cancelled need is no longer a question anyone is asked). Present only on a
33341
33468
  * Call's cards, and every card of that Call carries the same list: the call screen reads it
@@ -33396,7 +33523,7 @@ var InboxItemSchema2 = external_exports.object({
33396
33523
  * off the same open list the card came from, so it clears when the Call does. A card is the
33397
33524
  * backup for a call not taken; while the call has it, the call is where it is answered. */
33398
33525
  onCall: external_exports.literal(true).optional(),
33399
- /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
33526
+ /** THE RING, ON THE ITEM (docs/clients/app/walk/design.md §12 §17, #2251): the last ring on this card was
33400
33527
  * declined, and what the ladder will do next — read off the cron's own row, never computed
33401
33528
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
33402
33529
  * The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
@@ -33427,7 +33554,7 @@ var InboxItemSchema2 = external_exports.object({
33427
33554
  "Labels for a select:'confirm' paige \u2014 'yesno' (Yes/No) or 'approve' (Approve/Deny). Ignored unless select is 'confirm'."
33428
33555
  ),
33429
33556
  /** Real downstream work is stuck behind this one — set by the agent, independent of
33430
- * urgency (see the main README's "premier use case" + notify/states.md). Drives the
33557
+ * urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
33431
33558
  * inbox's blocking badge and the extra confirm step before dismissing it. */
33432
33559
  blocking: external_exports.boolean().default(false),
33433
33560
  /** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
@@ -33501,9 +33628,16 @@ var UserSettingsSchema2 = external_exports.object({
33501
33628
  banner: external_exports.boolean(),
33502
33629
  push: external_exports.boolean()
33503
33630
  }),
33631
+ /** LockedIn / Default / DateNight on screen; the stored words are unchanged on purpose —
33632
+ * they are an enum on a live column across every account, and the rename is a rename of
33633
+ * what people read (owner, 2026-09-30). */
33504
33634
  sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
33505
- silentPush: external_exports.boolean(),
33506
- autoCallback: external_exports.boolean(),
33635
+ /** DEAD, AND ACCEPTED ANYWAY (#2813). DateNight is inbox-only with no way back, so nothing
33636
+ * reads this. It stays OPTIONAL rather than deleted because a phone on the old bundle
33637
+ * PATCHes the whole settings object and would be refused for sending a key we stopped
33638
+ * wanting — the same trap `voiceMode` below already documents. #2813 drops the field and
33639
+ * its column once no installed build still sends it. */
33640
+ silentPush: external_exports.boolean().optional(),
33507
33641
  /** Opt-in (default false) to using your content to improve Paigy and train models. */
33508
33642
  improveConsent: external_exports.boolean(),
33509
33643
  missedCall: MissedCallSchema2.default("backoff_standard"),
@@ -33514,12 +33648,16 @@ var UserSettingsSchema2 = external_exports.object({
33514
33648
  * must not silently reset this privacy choice. Absent = leave unchanged on
33515
33649
  * write, 'hosted' on read (see store.ts). */
33516
33650
  voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
33517
- /** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
33651
+ /** Talk — after you answer, the next step is read aloud (docs/clients/app/walk/design.md §6). ALWAYS ON until
33518
33652
  * turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
33519
33653
  * defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
33520
33654
  * object must not silently turn it back on. Absent = leave unchanged on write, true on
33521
33655
  * read (see store.ts). */
33522
33656
  talk: external_exports.boolean().optional(),
33657
+ /** CALL DIAGNOSTICS (owner, 2026-10-01): the call report carries each listen and the bot's own
33658
+ * load timings. SERVER-SET, no UI — on for every account that existed on 2026-10-01, off for
33659
+ * newer ones (migration 20261001132859). Read-only here: the settings PATCH never writes it. */
33660
+ callDiagnostics: external_exports.boolean().optional(),
33523
33661
  /** Per-user ring budget (#603): calls per rolling day before further calls
33524
33662
  * degrade to banner. Absent = the global default (25). A number, never a
33525
33663
  * bypass — every account keeps a ceiling. No UI; set per user for testing. */
@@ -33528,9 +33666,6 @@ var UserSettingsSchema2 = external_exports.object({
33528
33666
  * slower speaker). No API-side semantics; the bot resolves each key with its
33529
33667
  * own defaults. Set per user (no UI yet); absent = bot defaults. */
33530
33668
  voiceTuning: external_exports.record(external_exports.string(), external_exports.union([external_exports.number(), external_exports.string()])).optional(),
33531
- /** Opt-in to real-phone (PSTN) calls when the app can't ring. Optional, not
33532
- * defaulted — an older client PATCHing the full object must not clobber it. */
33533
- pstnCalls: external_exports.boolean().optional(),
33534
33669
  /** The user's IANA timezone (e.g. "America/Bogota"), recorded by the app — it is the
33535
33670
  * only party that knows it. REMINDERS are why it exists: "remind me at ten" becomes
33536
33671
  * an absolute `due_at` only if we know whose ten. Optional and never defaulted, for
@@ -33544,18 +33679,20 @@ var UserSettingsSchema2 = external_exports.object({
33544
33679
  * clobber guard as voiceMode: absent = leave unchanged on write. */
33545
33680
  broker: BrokerTuningSchema2.optional()
33546
33681
  });
33547
- var HistoryItemSchema2 = external_exports.object({
33682
+ var HistoryWorkSchema2 = external_exports.object({
33548
33683
  id: external_exports.string(),
33549
- /** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
33550
- initiator: external_exports.enum(["user", "agent"]),
33551
33684
  title: external_exports.string(),
33552
- /** The agent on the other end (its name). */
33553
- name: external_exports.string(),
33554
- createdAt: external_exports.string(),
33555
- /** When the agent fetched your request (user→agent only). */
33556
- agentAckedAt: external_exports.string().nullable(),
33557
- /** When you answered the agent's notification (agent→user only). */
33558
- humanAckedAt: external_exports.string().nullable()
33685
+ state: external_exports.enum(["done", "cancelled"]),
33686
+ /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
33687
+ assignee: external_exports.string()
33688
+ });
33689
+ var HistoryEntrySchema2 = external_exports.union([
33690
+ external_exports.object({ at: external_exports.string(), card: InboxItemSchema2 }),
33691
+ external_exports.object({ at: external_exports.string(), work: HistoryWorkSchema2 })
33692
+ ]);
33693
+ var HistoryPageSchema2 = external_exports.object({
33694
+ entries: external_exports.array(HistoryEntrySchema2),
33695
+ next: external_exports.string().nullable()
33559
33696
  });
33560
33697
  var ACTIVITY_LINES2 = 2;
33561
33698
  var ACTIVITY_LINE_MAX2 = 80;
@@ -33572,7 +33709,7 @@ var ConnectionSummarySchema2 = external_exports.object({
33572
33709
  /** The credential kind: "device" = a paired machine (mint-only — it hosts and mints, it
33573
33710
  * never talks); "agent" = an identity that sends. The roster and devices surfaces split
33574
33711
  * on this. Optional/absent reads as "agent" (a row predating the kind column). See
33575
- * apps/api/src/tokens/devices-vs-agents-design.md. */
33712
+ * docs/server/tokens/devices-vs-agents-design.md. */
33576
33713
  kind: external_exports.enum(["device", "agent"]).optional(),
33577
33714
  /** For an agent, the token id of the DEVICE that minted it — so agents group under their
33578
33715
  * machine, and revoking a device cascades to them. Null on devices, and on unlinked
@@ -33590,7 +33727,7 @@ var ConnectionSummarySchema2 = external_exports.object({
33590
33727
  * user on the agent's own page. null = no ceiling (today's behaviour for every
33591
33728
  * connection). Android binds importance to a relationship rather than to each message,
33592
33729
  * and that is the thing our roster could not say: "Marlow may always call me; Otto
33593
- * never may" (navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
33730
+ * never may" (docs/clients/app/party/navigation-design.md, gap 2). Clamped in `arbitrateLevel`, so it binds
33594
33731
  * every surface at once and outranks even `sessionMode: all_calls` — a mode the user
33595
33732
  * set once must not overrule a rule they set about one agent. */
33596
33733
  reach: NotifyLevelSchema2.nullable().optional(),
@@ -33601,7 +33738,14 @@ var ConnectionSummarySchema2 = external_exports.object({
33601
33738
  /** Last presence heartbeat from a running agent process (POST /api/presence) — the
33602
33739
  * desktop app while open. Null = never seen; stale = offline. */
33603
33740
  lastSeenAt: external_exports.string().datetime().nullable().optional(),
33604
- /** What a live desktop can run (companion.md §2.2), advertised on its heartbeat:
33741
+ /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
33742
+ * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
33743
+ * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
33744
+ lastWorkedAt: external_exports.string().datetime().nullable().optional(),
33745
+ /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
33746
+ * With no work of its own for `WORKING_MS`, an agent sitting on this is not taking its work. */
33747
+ oldestReadyAt: external_exports.string().datetime().nullable().optional(),
33748
+ /** What a live desktop can run (docs/clients/desktop/companion.md §2.2), advertised on its heartbeat:
33605
33749
  * harness availabilities + granted workspaces — the option set the phone's
33606
33750
  * "new session" sheet offers. Absent for ordinary MCP agents. */
33607
33751
  runtime: external_exports.object({
@@ -33609,7 +33753,12 @@ var ConnectionSummarySchema2 = external_exports.object({
33609
33753
  * shows its age here (`apps/desktop/src/update.ts`). */
33610
33754
  version: external_exports.string().optional(),
33611
33755
  harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
33612
- workspaces: external_exports.array(external_exports.string()).optional()
33756
+ workspaces: external_exports.array(external_exports.string()).optional(),
33757
+ /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
33758
+ * repo, and each repo directly inside one, with its `origin` remote. A session started for
33759
+ * work on `mauurda/paigy` opens in that repo rather than the folder above it, where the repo's
33760
+ * own AGENTS.md is never read (`workspaceForRepo`). Absent on hosts that predate it. */
33761
+ repos: external_exports.array(external_exports.object({ path: external_exports.string(), remote: external_exports.string() })).optional()
33613
33762
  }).optional(),
33614
33763
  /** The tail of this agent's working log, when a harness is driving it — the agent page's
33615
33764
  * live strip. Absent for anything the desktop harness isn't running (a hatched identity
@@ -33692,7 +33841,7 @@ var QueueQuestionSchema2 = external_exports.object({
33692
33841
  * for a settled question whose reply carried nothing readable. */
33693
33842
  answer: external_exports.string().nullable().default(null),
33694
33843
  /** The Goal this question belongs to — a step knows its Goal on its own, not only through
33695
- * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
33844
+ * an `InboxItem`'s `communication.goalIds[0]` (docs/clients/app/walk/design.md §12 item 3).
33696
33845
  * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
33697
33846
  * order, its route, home's trees and the list of steps all take a step's Goal from here, so
33698
33847
  * this is the field they agree through rather than each re-deriving it from the row it
@@ -33739,6 +33888,9 @@ var QueueItemSchema2 = external_exports.object({
33739
33888
  progressLine: external_exports.string().nullable().optional(),
33740
33889
  reviewPending: external_exports.boolean().default(false),
33741
33890
  dueAt: external_exports.string().nullable().default(null),
33891
+ /** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
33892
+ * with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
33893
+ finishedAt: external_exports.string().nullable().optional(),
33742
33894
  /** The Goal this one was opened under; null at the root. */
33743
33895
  parentGoalId: external_exports.string().nullable().default(null),
33744
33896
  /** Goals opened under this one — only those the same list holds. */
@@ -33747,8 +33899,17 @@ var QueueItemSchema2 = external_exports.object({
33747
33899
  dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
33748
33900
  /** True while any gate is on a Goal that is not done — the walk draws it dashed. */
33749
33901
  blocked: external_exports.boolean().default(false),
33750
- /** Every decision need on it, open or settled — the page decides which to show. */
33902
+ /** Its questions: every OPEN one, and at most ten settled, newest settled first
33903
+ * (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
33904
+ * `answered` are, and a settled one's words are a line (280 characters), its body read when the
33905
+ * question is opened. */
33751
33906
  questions: external_exports.array(QueueQuestionSchema2).default([]),
33907
+ /** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
33908
+ * bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
33909
+ * right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
33910
+ * by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
33911
+ asked: external_exports.number().optional(),
33912
+ answered: external_exports.number().optional(),
33752
33913
  /** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
33753
33914
  * Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
33754
33915
  * not spell an empty list. */
@@ -33772,7 +33933,19 @@ var QueueItemSchema2 = external_exports.object({
33772
33933
  said: external_exports.string(),
33773
33934
  at: external_exports.string(),
33774
33935
  entryId: external_exports.string()
33775
- }).nullable().optional()
33936
+ }).nullable().optional(),
33937
+ /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
33938
+ * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
33939
+ * answer to a question, a voice note filed as work. Null when the only hands on it have been its
33940
+ * agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
33941
+ * 20260930222356 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
33942
+ * Work tab orders as it did before the migration rather than throwing on the missing key.
33943
+ *
33944
+ * It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
33945
+ * minute after the person speaks erases their instant from it, and the durable traces the client
33946
+ * can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
33947
+ * Entry with no `about_id` is in neither. */
33948
+ lastPersonAt: external_exports.string().nullable().optional()
33776
33949
  });
33777
33950
  var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
33778
33951
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
@@ -33802,7 +33975,7 @@ var NoteSchema2 = external_exports.object({
33802
33975
  assignee: external_exports.string().nullable(),
33803
33976
  /** The request thread minted at assignment; null until assigned. */
33804
33977
  parentId: external_exports.string().nullable(),
33805
- /** REMINDERS (reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
33978
+ /** REMINDERS (docs/model/notes/reminders-design.md): the NOT-BEFORE this becomes eligible to ride a
33806
33979
  * call — never a deadline. It only ever comes from the user's own words, so when it
33807
33980
  * passes Paigy rings ONCE (#1293, owner 2026-08-26: a time said out loud is consent to
33808
33981
  * a ring at that time); after that it rides like any other. Null = "the very next
@@ -33883,6 +34056,19 @@ var DeliveryConfigSchema2 = external_exports.object({
33883
34056
  * carries credentials; only a `poll` registration can come back with null here. */
33884
34057
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
33885
34058
  });
34059
+ var HostDecisionSchema2 = external_exports.object({
34060
+ /** The agent's token id: the row's `recipient`. */
34061
+ agent: external_exports.string().uuid(),
34062
+ decision: external_exports.enum(["stood_back", "took_over"]),
34063
+ /** The work it was about: the Goal `claim_goal` would hand that agent next. */
34064
+ goalId: external_exports.string().uuid().nullable().optional(),
34065
+ /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
34066
+ seenAt: external_exports.string().datetime().nullable().optional(),
34067
+ /** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
34068
+ since: external_exports.string().datetime().nullable().optional(),
34069
+ /** What the host said, in its log's own words: why it stood back, or what the take-over did. */
34070
+ said: external_exports.string().max(300).optional()
34071
+ });
33886
34072
  var WakeNudgeSchema2 = external_exports.object({
33887
34073
  kind: external_exports.enum(["reply", "request", "callback"]),
33888
34074
  notificationId: external_exports.string().optional(),
@@ -33927,6 +34113,9 @@ var DeviceTokenSchema2 = external_exports.object({
33927
34113
  * landed in the FIRST granted workspace and the agent rediscovered its own repo from
33928
34114
  * the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
33929
34115
  workspace: external_exports.string().nullable().optional(),
34116
+ /** Local host recovery must preserve the launch's runtime and Paigy identity. */
34117
+ harness: external_exports.enum(["claude", "codex", "agy"]).optional(),
34118
+ session_id: external_exports.string().uuid().optional(),
33930
34119
  uik_pub: external_exports.string().nullable().optional()
33931
34120
  });
33932
34121
  var SupportRequestSchema2 = external_exports.object({
@@ -34046,6 +34235,29 @@ var SnapshotSchema2 = external_exports.object({
34046
34235
  computers: external_exports.array(ComputerRowSchema2).nullable()
34047
34236
  }).nullable()
34048
34237
  });
34238
+ var CallRecapSchema2 = external_exports.object({
34239
+ call: external_exports.object({
34240
+ status: external_exports.string(),
34241
+ startedAt: external_exports.string(),
34242
+ durationMs: external_exports.number().nullable(),
34243
+ agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
34244
+ }),
34245
+ topics: external_exports.array(external_exports.object({
34246
+ goalId: external_exports.string().uuid(),
34247
+ title: external_exports.string(),
34248
+ owner: external_exports.string(),
34249
+ state: external_exports.string(),
34250
+ questions: external_exports.array(external_exports.object({ id: external_exports.string().uuid(), state: external_exports.string(), title: external_exports.string() })),
34251
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
34252
+ * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
34253
+ * summarized like a pre-made option is"), so the row scans like a chosen option and their
34254
+ * own words stay under it. Absent on every line stored before the read wrote them, and on
34255
+ * anything that is not an answer. */
34256
+ lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional() }))
34257
+ })),
34258
+ unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
34259
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
34260
+ });
34049
34261
 
34050
34262
  // src/paigy/activity.ts
34051
34263
  function shortenPaths(s) {
@@ -34146,6 +34358,14 @@ var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on
34146
34358
  var OWED = "That turn was given this when the limit stopped it, and may not have got to it. Act on it now, unless you already have:";
34147
34359
  var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
34148
34360
  var GATHER_MS = 9e4;
34361
+ function outcome(result, paused) {
34362
+ const r = result ?? {};
34363
+ const who = `${paused.length} paused agent${paused.length === 1 ? "" : "s"}: ${names(paused)}`;
34364
+ if (r.sent === false) return `\u26A0 the limit call did not ring (${who}): it was filed as progress, nobody was notified`;
34365
+ if (r.kind === "call" && typeof r.deliveryId === "string") return `\u260E one call for ${who}, placed (delivery ${r.deliveryId})`;
34366
+ if (typeof r.deliveryId === "string") return `\u26A0 the limit call did not ring (${who}): it went out as a card (delivery ${r.deliveryId})`;
34367
+ return `\u26A0 the limit call did not ring (${who}): no delivery came back`;
34368
+ }
34149
34369
  function episodes(opts = {}) {
34150
34370
  const gather2 = opts.gatherMs ?? GATHER_MS;
34151
34371
  const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
@@ -34177,8 +34397,10 @@ function episodes(opts = {}) {
34177
34397
  `Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
34178
34398
  rest.length ? `It has reset: ${names(episode.running)} ${episode.running.length > 1 ? "are" : "is"} running again, and ${names(rest)} ${rest.length > 1 ? "haven't" : "hasn't"} started again yet.` : `It has reset, and ${episode.paused.length > 1 ? "they have all" : "it has"} carried on.`
34179
34399
  ].join(" ");
34180
- log?.(`\u260E one call for ${episode.paused.length} paused agent${episode.paused.length === 1 ? "" : "s"}: ${names(episode.paused)}`);
34181
- void call(text).catch((error) => log?.(`\u26A0 could not place the limit call: ${error.message}`));
34400
+ void call(text).then(
34401
+ (result) => log?.(outcome(result, episode.paused)),
34402
+ (error) => log?.(`\u26A0 could not place the limit call: ${error.message}`)
34403
+ );
34182
34404
  }, gather2);
34183
34405
  }
34184
34406
  };
@@ -34301,7 +34523,7 @@ ${event.result?.trim() || "No result reported."}`, state, rail, opts);
34301
34523
 
34302
34524
  // src/paigy/conversation.ts
34303
34525
  import { randomUUID as randomUUID5 } from "crypto";
34304
- var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message: it is kept as progress on this Goal. Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's parentId). " + DONE_MESSAGE;
34526
+ var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message: it is kept as progress on this Goal. Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's parentId).";
34305
34527
  var SAFETY_MS = 6e4;
34306
34528
  var ASK_BEAT_MS = 5e3;
34307
34529
  function beat() {
@@ -34429,9 +34651,9 @@ async function gather(state, rail, held) {
34429
34651
  var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; claim_goal with its id when you take it up.";
34430
34652
  var says = (h) => h.words.length > 0 || h.via === "assigned";
34431
34653
  function whatItIs(h) {
34432
- const outcome = h.brief.outcome?.trim() ?? "";
34433
- const name = (h.title ?? outcome).trim().split("\n")[0].trim();
34434
- return outcome && outcome !== name ? [outcome] : [];
34654
+ const outcome2 = h.brief.outcome?.trim() ?? "";
34655
+ const name = (h.title ?? outcome2).trim().split("\n")[0].trim();
34656
+ return outcome2 && outcome2 !== name ? [outcome2] : [];
34435
34657
  }
34436
34658
  var block = (h) => [
34437
34659
  ...h.via === "assigned" ? [GIVEN] : [],
@@ -34558,11 +34780,11 @@ async function look(session, state, rail, opts) {
34558
34780
  state.goal = { id: brief2.goalId, revision: brief2.revision };
34559
34781
  state.below = baseline(brief2.goalId, brief2.subtree);
34560
34782
  }
34561
- const outcome = brief2.outcome?.trim() ?? "";
34562
- const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
34783
+ const outcome2 = brief2.outcome?.trim() ?? "";
34784
+ const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
34563
34785
  const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
34564
34786
  const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
34565
- const fullText = [outcome, earlier, ...said].filter(Boolean).join("\n\n");
34787
+ const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
34566
34788
  send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
34567
34789
  return;
34568
34790
  }
@@ -34748,17 +34970,33 @@ ${event.message}`, state, rail, { log: opts.log });
34748
34970
 
34749
34971
  // src/host.ts
34750
34972
  init_dist();
34751
- init_catalog();
34752
34973
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
34753
- import { homedir as homedir8 } from "os";
34974
+ import { homedir as homedir9 } from "os";
34754
34975
  import { join as join9 } from "path";
34755
34976
 
34977
+ // src/broker.ts
34978
+ init_evidence();
34979
+ init_triage();
34980
+ var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
34981
+ function ago(iso, now) {
34982
+ const ms = now - Date.parse(iso);
34983
+ if (!Number.isFinite(ms) || ms < 6e4) return "just now";
34984
+ const minutes = Math.floor(ms / 6e4);
34985
+ if (minutes < 60) return `${plural2(minutes, "minute")} ago`;
34986
+ const hours = Math.floor(minutes / 60);
34987
+ if (hours < 24) return `${plural2(hours, "hour")} ago`;
34988
+ return `${plural2(Math.floor(hours / 24), "day")} ago`;
34989
+ }
34990
+
34991
+ // src/host.ts
34992
+ init_catalog();
34993
+
34756
34994
  // src/workspaces.ts
34757
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
34995
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
34758
34996
  import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
34759
- import { homedir as homedir5 } from "os";
34997
+ import { homedir as homedir6 } from "os";
34760
34998
  function workspacesFile() {
34761
- return join5(homedir5(), ".paigy", "workspaces.json");
34999
+ return join5(homedir6(), ".paigy", "workspaces.json");
34762
35000
  }
34763
35001
  function listWorkspaces(deps) {
34764
35002
  try {
@@ -34769,7 +35007,7 @@ function listWorkspaces(deps) {
34769
35007
  }
34770
35008
  }
34771
35009
  function addWorkspace(dir, deps) {
34772
- const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir5()));
35010
+ const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir6()));
34773
35011
  const all = listWorkspaces(deps);
34774
35012
  if (!all.includes(next)) {
34775
35013
  all.push(next);
@@ -34779,22 +35017,53 @@ function addWorkspace(dir, deps) {
34779
35017
  return all;
34780
35018
  }
34781
35019
  function allowed(cwd, deps) {
34782
- const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir5()));
35020
+ const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir6()));
34783
35021
  return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
34784
35022
  }
34785
35023
  function resolveWakeDir(pinned, deps) {
34786
35024
  if (pinned && allowed(pinned, deps)) {
34787
- const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir5()));
35025
+ const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir6()));
34788
35026
  if ((deps.exists ?? existsSync4)(dir)) return dir;
34789
35027
  }
34790
35028
  return listWorkspaces(deps)[0];
34791
35029
  }
35030
+ function originOf(dir) {
35031
+ try {
35032
+ if (!statSync3(join5(dir, ".git")).isDirectory()) return null;
35033
+ const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
35034
+ const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
35035
+ return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
35036
+ } catch {
35037
+ return null;
35038
+ }
35039
+ }
35040
+ var MAX_CHILDREN = 300;
35041
+ var MAX_REPOS = 100;
35042
+ function listRepos(deps) {
35043
+ const repos = [];
35044
+ for (const root of listWorkspaces(deps)) {
35045
+ const own = originOf(root);
35046
+ if (own) repos.push({ path: root, remote: own });
35047
+ let children = [];
35048
+ try {
35049
+ children = readdirSync2(root, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, MAX_CHILDREN);
35050
+ } catch {
35051
+ }
35052
+ for (const name of children) {
35053
+ if (repos.length >= MAX_REPOS) return repos;
35054
+ const path = join5(root, name);
35055
+ const remote = originOf(path);
35056
+ if (remote) repos.push({ path, remote });
35057
+ }
35058
+ }
35059
+ return repos.slice(0, MAX_REPOS);
35060
+ }
34792
35061
 
34793
35062
  // src/holder.ts
34794
35063
  init_dist();
34795
35064
  import { execFileSync as execFileSync3 } from "child_process";
34796
- import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
34797
- import { homedir as homedir6 } from "os";
35065
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
35066
+ import { homedir as homedir7 } from "os";
34798
35067
  import { join as join6 } from "path";
34799
35068
  function isAlive(pid) {
34800
35069
  try {
@@ -34825,8 +35094,8 @@ function holds(pid, procStart, deps) {
34825
35094
  return false;
34826
35095
  }
34827
35096
  function localHolders(deps = {}) {
34828
- const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
34829
- const list = deps.list ?? ((d) => readdirSync2(d));
35097
+ const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
35098
+ const list = deps.list ?? ((d) => readdirSync3(d));
34830
35099
  const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
34831
35100
  const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
34832
35101
  const held = /* @__PURE__ */ new Map();
@@ -34854,8 +35123,8 @@ function localHolders(deps = {}) {
34854
35123
  }
34855
35124
 
34856
35125
  // src/owner.ts
34857
- import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
34858
- import { homedir as homedir7 } from "os";
35126
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync4 } from "fs";
35127
+ import { homedir as homedir8 } from "os";
34859
35128
  import { join as join7 } from "path";
34860
35129
  var HEAD_BYTES = 64e3;
34861
35130
  function readHead(file) {
@@ -34872,8 +35141,8 @@ function ownerSession(slot, deps = {}) {
34872
35141
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
34873
35142
  if (!m) return null;
34874
35143
  const prefix = m[1];
34875
- const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
34876
- const list = deps.list ?? ((dir) => readdirSync3(dir));
35144
+ const root = deps.projectsDir ?? join7(homedir8(), ".claude", "projects");
35145
+ const list = deps.list ?? ((dir) => readdirSync4(dir));
34877
35146
  const head = deps.head ?? readHead;
34878
35147
  let folders;
34879
35148
  try {
@@ -34990,34 +35259,51 @@ function selfUpdater(deps) {
34990
35259
  }
34991
35260
 
34992
35261
  // src/host.ts
34993
- var HOST_FILE = join9(homedir8(), ".paigy", "host.json");
35262
+ var HOST_FILE = join9(homedir9(), ".paigy", "host.json");
35263
+ function wakeHarness(slot, saved, claudeTranscript) {
35264
+ if (slot === "Desktop") return void 0;
35265
+ if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
35266
+ if (claudeTranscript) return "claude";
35267
+ if (slot === "mcp-agent" || slot === "Claude Code") return "claude";
35268
+ if (slot === "codex") return "codex";
35269
+ if (slot === "antigravity") return "agy";
35270
+ return void 0;
35271
+ }
34994
35272
  var runKey = (slot) => `slot:${slot}`;
34995
35273
  var claimedRun = (sessionId2) => {
34996
35274
  const slot = sessionSlot(sessionId2);
34997
35275
  return { slot, key: runKey(slot) };
34998
35276
  };
34999
35277
  var launchRun = (spec) => ({ session: spec.sessionId });
35278
+ var NOTHING_WAITING = "has nothing waiting";
35000
35279
  async function waitingWork(token, session) {
35001
35280
  let found;
35002
35281
  try {
35003
35282
  found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
35004
35283
  } catch (error) {
35284
+ if (error instanceof RevokedError) return { skip: "was revoked: its credential is gone for good", revoked: true };
35285
+ if (error instanceof ApiError && error.body?.error === "identity_session_mismatch") {
35286
+ return { skip: "is bound to the session it was paired in, which this host cannot name \u2014 run `paigy-harness handoff` as it, in its folder, to record it" };
35287
+ }
35005
35288
  return { skip: `could not read what is waiting (${error.message})` };
35006
35289
  }
35007
- const n = found.deliveries.length;
35008
- if (n) return { waiting: n === 1 ? "a message" : `${n} messages` };
35009
35290
  const next = found.claimable;
35010
- return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}` } : { skip: "has nothing waiting" };
35291
+ const about = next ? { goalId: next.goalId, ...next.since ? { since: next.since } : {} } : {};
35292
+ const n = found.deliveries.length;
35293
+ if (n) return { waiting: n === 1 ? "a message" : `${n} messages`, ...about };
35294
+ return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}`, ...about } : { skip: NOTHING_WAITING };
35011
35295
  }
35012
35296
  function evict(runs, key, run) {
35013
35297
  if (runs.get(key)?.run === run) runs.delete(key);
35014
35298
  }
35015
35299
  var PRESENCE_FRESH_MS = 3 * 6e4;
35300
+ var IDLE_HOLDER_MS = 30 * 6e4;
35016
35301
  function wakeAction(live, now, serverSeenAt, here = {}) {
35017
35302
  if (live) return { action: "skip", why: "run" };
35018
35303
  if (here.holder) return { action: "skip", why: "holder" };
35019
35304
  if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
35020
35305
  if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
35306
+ if (here.movedAt !== void 0 && now - here.movedAt >= IDLE_HOLDER_MS) return { action: "spawn", why: "idle" };
35021
35307
  return { action: "skip", why: "presence" };
35022
35308
  }
35023
35309
  return { action: "spawn", why: "nobody" };
@@ -35027,9 +35313,45 @@ function startHost(opts) {
35027
35313
  const asHost = { token: opts.token };
35028
35314
  const goneAt = /* @__PURE__ */ new Map();
35029
35315
  const heldHere = /* @__PURE__ */ new Set();
35316
+ const revoked = /* @__PURE__ */ new Map();
35317
+ const isRevoked = (slot) => {
35318
+ const dead = revoked.get(slot);
35319
+ if (dead === void 0) return false;
35320
+ if (readToken(slot) === dead) return true;
35321
+ revoked.delete(slot);
35322
+ return false;
35323
+ };
35324
+ function drop(slot, token) {
35325
+ if (revoked.get(slot) === token) return;
35326
+ revoked.set(slot, token);
35327
+ const sub = listening.get(slot);
35328
+ if (sub) {
35329
+ listening.delete(slot);
35330
+ void sub.close().catch(() => {
35331
+ });
35332
+ }
35333
+ opts.log(`\u2716 ${slotName(slot) ?? slot} was revoked \u2014 its credential is gone for good, so this host stops presenting it (dropped from the roster; pairing again makes a new identity)`);
35334
+ }
35335
+ const recorded = /* @__PURE__ */ new Map();
35336
+ function decide(slot, decision, found, seenAt, said) {
35337
+ const agent2 = slotIdentity(slot).tokenId;
35338
+ if (!agent2) return;
35339
+ const goalId = found?.goalId;
35340
+ const key = `${decision}:${goalId ?? ""}`;
35341
+ if (recorded.get(slot) === key) return;
35342
+ recorded.set(slot, key);
35343
+ void recordDecision({
35344
+ agent: agent2,
35345
+ decision,
35346
+ goalId: goalId ?? null,
35347
+ seenAt: seenAt === void 0 ? null : new Date(seenAt).toISOString(),
35348
+ since: found?.since ?? null,
35349
+ said: said.slice(0, 300)
35350
+ }, asHost).catch((e) => opts.log(`\u25CC could not record that ${slotName(slot) ?? slot} ${decision.replace("_", " ")} (${e.message})`));
35351
+ }
35030
35352
  const refreshIdentities = async () => {
35031
35353
  for (const slot of listSlots()) {
35032
- if (slot === "Desktop") continue;
35354
+ if (slot === "Desktop" || isRevoked(slot)) continue;
35033
35355
  const token = readToken(slot);
35034
35356
  if (!token) continue;
35035
35357
  const me = await whoAmI({ token }).catch(() => null);
@@ -35055,7 +35377,9 @@ function startHost(opts) {
35055
35377
  void heartbeat({
35056
35378
  version: VERSION,
35057
35379
  harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
35058
- workspaces: listWorkspaces(opts.wsDeps)
35380
+ workspaces: listWorkspaces(opts.wsDeps),
35381
+ // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
35382
+ repos: listRepos(opts.wsDeps)
35059
35383
  }, asHost).catch(() => {
35060
35384
  });
35061
35385
  void listenSlots().then(sweepSlots).catch(() => {
@@ -35067,6 +35391,19 @@ function startHost(opts) {
35067
35391
  const label = spec.name ?? spec.harness;
35068
35392
  const { slot, key } = claimedRun(spec.sessionId);
35069
35393
  const log = (line) => opts.log(`[${label}] ${line}`);
35394
+ try {
35395
+ saveToken({
35396
+ access_token: spec.token,
35397
+ name: label,
35398
+ device: null,
35399
+ workspace: spec.workspace,
35400
+ harness: spec.harness,
35401
+ session_id: spec.sessionId
35402
+ }, slot);
35403
+ } catch {
35404
+ log("Cannot persist launch identity/runtime; session was not started. Check local storage and relaunch.");
35405
+ continue;
35406
+ }
35070
35407
  const run = runHarness({
35071
35408
  harness: spec.harness,
35072
35409
  cwd: spec.workspace,
@@ -35086,14 +35423,9 @@ function startHost(opts) {
35086
35423
  void heartbeat(void 0, { token: spec.token }).catch(() => {
35087
35424
  });
35088
35425
  void refreshIdentities();
35089
- try {
35090
- saveToken({ access_token: spec.token, name: label, device: null, workspace: spec.workspace }, slot);
35091
- } catch {
35092
- }
35093
35426
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
35094
35427
  }
35095
35428
  }
35096
- const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
35097
35429
  async function presenceMap() {
35098
35430
  const presence = /* @__PURE__ */ new Map();
35099
35431
  try {
@@ -35114,30 +35446,64 @@ function startHost(opts) {
35114
35446
  let lastHolders = /* @__PURE__ */ new Map();
35115
35447
  async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
35116
35448
  {
35117
- const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
35118
35449
  const key = runKey(slot);
35119
- if (!harness) return;
35450
+ if (slot === "Desktop" || isRevoked(slot)) return;
35120
35451
  const live = runs.get(key);
35121
35452
  const tokenId = slotIdentity(slot).tokenId;
35122
35453
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
35123
35454
  const holder = holders.get(slot);
35124
35455
  if (holder) heldHere.add(slot);
35125
35456
  else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
35126
- const decided = wakeAction(!!live, Date.now(), seenByServer, { holder, goneAt: goneAt.get(slot) });
35127
- if (decided.action === "skip") {
35457
+ const here = { holder, goneAt: goneAt.get(slot) };
35458
+ let decided = wakeAction(!!live, Date.now(), seenByServer, here);
35459
+ const label = slotName(slot) ?? slot;
35460
+ if (decided.action === "skip" && decided.why !== "presence") {
35128
35461
  if (decided.why === "run" && cause === "wake") live?.run.nudge();
35129
- const name = slotName(slot) ?? slot;
35130
- opts.log(decided.why === "run" ? `\u25CC ${name} is running here \u2014 ${cause === "wake" ? "woke its pump" : "its pump types the work in"}; not spawning` : decided.why === "holder" ? `\u25B8 ${name} already running (pid ${holder?.pid ?? "?"}) \u2014 not starting a second` : `\u25CC ${name} is working somewhere this host cannot see (heartbeat) \u2014 not spawning`);
35462
+ opts.log(decided.why === "run" ? `\u25CC ${label} is running here \u2014 ${cause === "wake" ? "woke its pump" : "its pump types the work in"}; not spawning` : `\u25B8 ${label} already running (pid ${holder?.pid ?? "?"}) \u2014 not starting a second`);
35131
35463
  return;
35132
35464
  }
35133
- const label = slotName(slot) ?? slot;
35134
- const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
35465
+ let taking = "";
35466
+ let found;
35467
+ const skip = (why) => {
35468
+ opts.log(`\u25CC ${label} ${why} \u2014 not spawning${taking}`);
35469
+ if (taking && found) decide(slot, "took_over", found, seenByServer, `${why} \u2014 not spawning${taking}`);
35470
+ };
35135
35471
  const token = readToken(slot);
35136
35472
  if (!token) {
35137
35473
  skip("has no token on this machine");
35138
35474
  return;
35139
35475
  }
35140
- const owner = ownerSession(slot);
35476
+ const identity = slotIdentity(slot);
35477
+ const owner = !identity.harness || identity.harness === "claude" ? ownerSession(slot) : null;
35478
+ const read = async () => {
35479
+ const got = await waitingWork(token, identity.sessionId ?? owner?.sessionId);
35480
+ if (!("skip" in got)) return got;
35481
+ if (got.revoked) {
35482
+ drop(slot, token);
35483
+ return null;
35484
+ }
35485
+ skip(got.skip);
35486
+ if (decided.why === "presence" && got.skip !== NOTHING_WAITING) decide(slot, "stood_back", null, seenByServer, got.skip);
35487
+ return null;
35488
+ };
35489
+ if (decided.why === "presence") {
35490
+ found = await read() ?? void 0;
35491
+ if (!found) return;
35492
+ const since = found.since;
35493
+ decided = wakeAction(false, Date.now(), seenByServer, { ...here, ...since ? { movedAt: Date.parse(since) } : {} });
35494
+ if (decided.action === "skip") {
35495
+ const said = `is working somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; taken over once it sits ${IDLE_HOLDER_MS / 6e4} min untouched` : ", with nothing to say when it last moved"}`;
35496
+ opts.log(`\u25CC ${label} ${said} \u2014 not spawning`);
35497
+ decide(slot, "stood_back", found, seenByServer, said);
35498
+ return;
35499
+ }
35500
+ taking = ` (taking over: ${found.waiting} last moved ${ago(since, Date.now())} while something this host cannot see heartbeats as ${label} and has not touched it)`;
35501
+ }
35502
+ const harness = wakeHarness(slot, identity.harness, !!owner);
35503
+ if (!harness) {
35504
+ skip("has no recorded harness; select its runtime before recovery");
35505
+ return;
35506
+ }
35141
35507
  if (owner && !allowed(owner.cwd, opts.wsDeps)) {
35142
35508
  skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
35143
35509
  return;
@@ -35147,11 +35513,8 @@ function startHost(opts) {
35147
35513
  skip("has no granted folder to wake in");
35148
35514
  return;
35149
35515
  }
35150
- const found = await waitingWork(token, owner?.sessionId);
35151
- if ("skip" in found) {
35152
- skip(found.skip);
35153
- return;
35154
- }
35516
+ found ??= await read() ?? void 0;
35517
+ if (!found) return;
35155
35518
  if (runs.has(key)) {
35156
35519
  skip("was started by the wake that read it first");
35157
35520
  return;
@@ -35163,6 +35526,7 @@ function startHost(opts) {
35163
35526
  mode: "bypass",
35164
35527
  prompt: "",
35165
35528
  token,
35529
+ ...identity.sessionId ? { session: identity.sessionId } : {},
35166
35530
  ...owner ? { resume: owner.sessionId } : {},
35167
35531
  // A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
35168
35532
  // a run that is no longer the slot's must not take the live one's place away with it.
@@ -35177,13 +35541,15 @@ function startHost(opts) {
35177
35541
  runs.set(key, { run, label, token });
35178
35542
  void heartbeat(void 0, { token }).catch(() => {
35179
35543
  });
35180
- opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 ${found.waiting} was waiting, in ${workspace}${decided.why === "gone" ? " (its process here ended; its last heartbeat was not waited out)" : ""}`);
35544
+ const woke = `\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 ${found.waiting} was waiting, in ${workspace}${decided.why === "gone" ? " (its process here ended; its last heartbeat was not waited out)" : taking}`;
35545
+ opts.log(woke);
35546
+ if (taking) decide(slot, "took_over", found, seenByServer, woke);
35181
35547
  }
35182
35548
  }
35183
35549
  const listening = /* @__PURE__ */ new Map();
35184
35550
  const joining = /* @__PURE__ */ new Set();
35185
35551
  async function listenSlots() {
35186
- const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot)));
35552
+ const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot) && !isRevoked(slot)));
35187
35553
  for (const [slot, sub] of [...listening]) {
35188
35554
  if (want.has(slot)) continue;
35189
35555
  listening.delete(slot);
@@ -35204,6 +35570,10 @@ function startHost(opts) {
35204
35570
  listening.set(slot, sub);
35205
35571
  opts.log(`\u25C9 listening for ${slotName(slot) ?? slot}`);
35206
35572
  } catch (e) {
35573
+ if (e instanceof RevokedError) {
35574
+ drop(slot, token);
35575
+ continue;
35576
+ }
35207
35577
  opts.log(`\u25CC not listening for ${slotName(slot) ?? slot} \u2014 ${e.message}`);
35208
35578
  } finally {
35209
35579
  joining.delete(slot);
@@ -35269,7 +35639,7 @@ function startHost(opts) {
35269
35639
  roster: () => {
35270
35640
  const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
35271
35641
  const names2 = /* @__PURE__ */ new Map();
35272
- for (const key of listSlots()) if (key !== "Desktop") names2.set(slotName(key) ?? key, key);
35642
+ for (const key of listSlots()) if (key !== "Desktop" && !isRevoked(key)) names2.set(slotName(key) ?? key, key);
35273
35643
  for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
35274
35644
  return [...names2.entries()].map(([name, slot]) => {
35275
35645
  const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
@@ -35337,17 +35707,17 @@ function startBridgeInstalls(bridges, deps = {}) {
35337
35707
  const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
35338
35708
  const running = bridges.map((b) => {
35339
35709
  let proc = null;
35340
- let outcome;
35710
+ let outcome2;
35341
35711
  try {
35342
35712
  proc = spawn2(b.install);
35343
- outcome = proc.exited.then(
35713
+ outcome2 = proc.exited.then(
35344
35714
  (code) => code === 0 ? "installed" : "failed",
35345
35715
  () => "failed"
35346
35716
  );
35347
35717
  } catch {
35348
- outcome = Promise.resolve("failed");
35718
+ outcome2 = Promise.resolve("failed");
35349
35719
  }
35350
- return { ...b, proc, outcome };
35720
+ return { ...b, proc, outcome: outcome2 };
35351
35721
  });
35352
35722
  return {
35353
35723
  started: running.length,
@@ -35625,7 +35995,7 @@ async function main() {
35625
35995
  process.exit(1);
35626
35996
  }
35627
35997
  const cwd = process.cwd();
35628
- if (!updateSlot(slot, { workspace: cwd })) {
35998
+ if (!updateSlot(slot, { workspace: cwd, session_id: sessionId() })) {
35629
35999
  console.error(`Slot "${slot}" isn't in this machine's slot file \u2014 this session was spawned by the host, which already owns its wake dir. Run handoff from the terminal that hatched the identity.`);
35630
36000
  process.exit(1);
35631
36001
  }