@paigy/harness 0.3.38 → 0.3.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +212 -72
  2. package/dist/main.js +2506 -2375
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -77,7 +77,7 @@ function runProbe(argv) {
77
77
  function detect(entry, deps = {}) {
78
78
  const find = deps.which ?? which;
79
79
  const probe = deps.probe ?? runProbe;
80
- const base = { name: entry.name, label: entry.label, models: entry.models };
80
+ const base = { name: entry.name, label: entry.label };
81
81
  const cliPath = find(entry.cli);
82
82
  if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
83
83
  const adapterPath = entry.adapter ? find(entry.adapter) : null;
@@ -109,8 +109,7 @@ var init_catalog = __esm({
109
109
  authProbe: ["claude", "auth", "status"],
110
110
  loginHint: "Run `claude` once in a terminal and sign in.",
111
111
  installCli: "curl -fsSL https://claude.ai/install.sh | bash",
112
- installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp",
113
- models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
112
+ installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
114
113
  },
115
114
  {
116
115
  name: "codex",
@@ -120,8 +119,7 @@ var init_catalog = __esm({
120
119
  authProbe: ["codex", "login", "status"],
121
120
  loginHint: "Run `codex login` to authenticate.",
122
121
  installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
123
- installAdapter: "npm install -g @agentclientprotocol/codex-acp",
124
- models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"]
122
+ installAdapter: "npm install -g @agentclientprotocol/codex-acp"
125
123
  },
126
124
  {
127
125
  name: "agy",
@@ -131,8 +129,7 @@ var init_catalog = __esm({
131
129
  adapter: "paigy-agy-acp",
132
130
  authProbe: ["agy", "help"],
133
131
  loginHint: "Install and set up Antigravity (`agy`).",
134
- installCli: "curl -fsSL https://antigravity.dev/install.sh | bash",
135
- models: ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-pro-exp"]
132
+ installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
136
133
  }
137
134
  ];
138
135
  }
@@ -301,8 +298,8 @@ var init_ZodError = __esm({
301
298
  "not_finite"
302
299
  ]);
303
300
  quotelessJson = (obj) => {
304
- const json = JSON.stringify(obj, null, 2);
305
- return json.replace(/"([^"]+)":/g, "$1:");
301
+ const json3 = JSON.stringify(obj, null, 2);
302
+ return json3.replace(/"([^"]+)":/g, "$1:");
306
303
  };
307
304
  ZodError = class _ZodError extends Error {
308
305
  get errors() {
@@ -23927,14 +23924,18 @@ async function hatch(name, voice = null, opts = {}) {
23927
23924
  }
23928
23925
  async function whoAmI(opts = {}) {
23929
23926
  try {
23930
- const res = await reach(`${BACKEND_URL}/api/identity`, {
23931
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
23932
- });
23933
- return res.ok ? await res.json() : null;
23927
+ return await readIdentity(opts);
23934
23928
  } catch {
23935
23929
  return null;
23936
23930
  }
23937
23931
  }
23932
+ async function readIdentity(opts = {}) {
23933
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/identity`, {
23934
+ headers: { authorization: `Bearer ${authToken(opts.token)}` }
23935
+ }));
23936
+ if (!res.ok) await fail("read_identity", res);
23937
+ return await res.json();
23938
+ }
23938
23939
  async function claimSessions(opts = {}) {
23939
23940
  const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
23940
23941
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -24294,7 +24295,7 @@ ${block2}`;
24294
24295
  }
24295
24296
  return text.slice(0, start) + updated + text.slice(end);
24296
24297
  }
24297
- var import_undici, require2, CODEX_ENV, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, CLIENT, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OPTION_LABEL_MAX, OPTION_HINT_MAX, OptionSchema, OptionInputSchema, NIGHT, ASK_MAX, UNITS_MAX, UNIT_TITLE_MAX, UNIT_BODY_MAX, UnitInputSchema, questionHandle, NodeRef, BlockedAction, reason, actionFits, ACTION_FITS, userExplicitlyRequested, units, UpdateInputSchema, questionFields, refineQuestion, QuestionInputSchema, QuestionWireSchema, AnswerInputSchema, QuestionDependencySchema, sendFields, ContactSchema, sendsAnything, ContactWireSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, UpdateGoalSchema, changedGoal, goalTitle, GOAL_OUTCOME_MAX, goalOutcome, CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange, read, GoalChangeSchema, editNames, ManageGoalsSchema, ManageGoalsToolSchema, MANAGE_GOALS_DESCRIPTION, GetGoalSchema, GET_GOAL_DESCRIPTION, SearchToolSchema, SEARCH_DESCRIPTION, CheckActivitySchema, FEEDBACK_TEXT_MAX, SendFeedbackSchema, SEND_FEEDBACK_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, id, key, participant, SourceSchema, ResultRefSchema, NodeRefSchema, DependencySchema, goalState, existingGoalChanges, BrainGoalChangeSchema, option, QuestionChangeSchema, lessonScope, LessonChangeSchema, BrainNextSchema, messageBase, BrainMessageSchema, BrainSearchSchema, BrainResultSchema, str, strs, CompactResultSchema, COMPACT_RESULT_JSON_SCHEMA, str2, strs2, TalkerReplySchema, TALKER_REPLY_JSON_SCHEMA, str3, strs3, FilerReplySchema, FILER_REPLY_JSON_SCHEMA, ExplainerReplySchema, EXPLAINER_REPLY_JSON_SCHEMA, DISTILLER_DEFECTS, DistillerReplySchema, DISTILLER_REPLY_JSON_SCHEMA, TIDY_AUDIENCES, TidyReplySchema, TIDY_REPLY_JSON_SCHEMA, LIVE_MS, WORKING_MS, WORKING_TIER_MS, RECENT_TIER_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, LessonStateSchema, LessonViewSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, QueueDeltaSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, CallRecapSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, NOT_SENT, APPLIED, DEMOTED, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24298
+ var import_undici, require2, CODEX_ENV, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, CLIENT, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OPTION_LABEL_MAX, OPTION_HINT_MAX, OptionSchema, OptionInputSchema, NIGHT, ASKING_RULES, ASK_MAX, UNITS_MAX, UNIT_TITLE_MAX, UNIT_BODY_MAX, UnitInputSchema, questionHandle, NodeRef, BlockedAction, reason, actionFits, ACTION_FITS, userExplicitlyRequested, units, UpdateInputSchema, questionFields, refineQuestion, QuestionInputSchema, QuestionWireSchema, AnswerInputSchema, QuestionDependencySchema, sendFields, ContactSchema, sendsAnything, ContactWireSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, UpdateGoalSchema, changedGoal, goalTitle, GOAL_OUTCOME_MAX, goalOutcome, CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange, read, GoalChangeSchema, editNames, ManageGoalsSchema, ManageGoalsToolSchema, MANAGE_GOALS_DESCRIPTION, GetGoalSchema, GetDeliverySchema, GET_DELIVERY_DESCRIPTION, GET_GOAL_DESCRIPTION, SearchToolSchema, SEARCH_DESCRIPTION, ConfigureIdentitySchema, CONFIGURE_IDENTITY_DESCRIPTION, CheckActivitySchema, FEEDBACK_TEXT_MAX, SendFeedbackSchema, SEND_FEEDBACK_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, id, key, participant, SourceSchema, ResultRefSchema, NodeRefSchema, DependencySchema, goalState, existingGoalChanges, BrainGoalChangeSchema, option, QuestionChangeSchema, lessonScope, LessonChangeSchema, BrainNextSchema, messageBase, BrainMessageSchema, BrainSearchSchema, BrainResultSchema, str, strs, CompactResultSchema, COMPACT_RESULT_JSON_SCHEMA, str2, strs2, TalkerReplySchema, TALKER_REPLY_JSON_SCHEMA, str3, strs3, FilerReplySchema, FILER_REPLY_JSON_SCHEMA, ExplainerReplySchema, EXPLAINER_REPLY_JSON_SCHEMA, DISTILLER_DEFECTS, DistillerReplySchema, DISTILLER_REPLY_JSON_SCHEMA, TIDY_AUDIENCES, TidyReplySchema, TIDY_REPLY_JSON_SCHEMA, LIVE_MS, WORKING_MS, WORKING_TIER_MS, RECENT_TIER_MS, json, clock, HeardEventSchema, HeardCursorSchema, HeardPageSchema, MAX_EVENTS, MAX_BYTES, batchBytes, HeardBatchSchema, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock2, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, LessonStateSchema, LessonViewSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, QueueDeltaSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, CallRecapSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, NOT_SENT, APPLIED, DEMOTED, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24298
24299
  var init_dist = __esm({
24299
24300
  "../../packages/sdk/dist/index.js"() {
24300
24301
  "use strict";
@@ -24310,6 +24311,7 @@ var init_dist = __esm({
24310
24311
  init_zod();
24311
24312
  init_zod();
24312
24313
  init_zod();
24314
+ init_zod();
24313
24315
  import_undici = __toESM(require_undici(), 1);
24314
24316
  require2 = __sdkCreateRequire(import.meta.url);
24315
24317
  CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
@@ -24718,6 +24720,16 @@ var init_dist = __esm({
24718
24720
  imgUrl: OptionSchema.shape.image
24719
24721
  }).strict();
24720
24722
  NIGHT = { from: 23, to: 7 };
24723
+ ASKING_RULES = `HOW TO ASK:
24724
+ 1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
24725
+ 2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
24726
+ 3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
24727
+ 4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
24728
+ 5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
24729
+ 6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
24730
+ 7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
24731
+ 8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
24732
+ 9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
24721
24733
  ASK_MAX = 250;
24722
24734
  UNITS_MAX = 12;
24723
24735
  UNIT_TITLE_MAX = 50;
@@ -24766,10 +24778,18 @@ var init_dist = __esm({
24766
24778
  // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
24767
24779
  // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
24768
24780
  reply: external_exports.string().trim().min(1).describe(
24769
- `What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. Left out, the button says "Got it", as every card did.`
24781
+ `What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. The reply is required for requested delivery and completed Goals; ordinary progress may omit it.`
24770
24782
  ).optional(),
24771
24783
  userExplicitlyRequested
24772
24784
  }).strict().superRefine((u, ctx) => {
24785
+ if (u.userExplicitlyRequested && !u.reply) {
24786
+ ctx.addIssue({
24787
+ code: external_exports.ZodIssueCode.custom,
24788
+ path: ["reply"],
24789
+ params: { refusal: "reply_required" },
24790
+ message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
24791
+ });
24792
+ }
24773
24793
  refuseOverCaps(u.message, u.units, "message", ctx);
24774
24794
  if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
24775
24795
  ctx.addIssue({
@@ -24857,14 +24877,10 @@ var init_dist = __esm({
24857
24877
  questions: external_exports.array(QuestionWireSchema).min(1).max(20).optional()
24858
24878
  }).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
24859
24879
  CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
24860
- CONTACT_DESCRIPTION = `Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read \`results\`, where a failed item names why while the rest still apply.
24861
-
24862
- QUESTIONS: one question per object; several questions are several objects in one contact, so each can be answered on its own. Each is sent exactly as written, with no reading in between: a bundled question arrives as one card. Give options when you have them. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). pickMode:'one' (the default) makes a pick, for alternatives; 'many' a checklist, for independent options they may want several of; 'rank' an ordering. The person can always answer in their own words, so never add an 'Other' option. workItBlocks names the work a question holds up, only when you are truly blocked: it asks for a call. Answers land on the question's Goal: collect them by receiving (below) or with get_goal.
24880
+ CONTACT_DESCRIPTION = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES + `
24863
24881
 
24864
24882
  CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX} characters; everything else it needs goes in 'units', up to ${UNITS_MAX} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX} characters and every body at most ${UNIT_BODY_MAX}. The person sees the main text and the titles, and opens a unit to read its body. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.
24865
24883
 
24866
- GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. It is what their button says instead of "Got it", and what they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Nothing is owed on it either way, and a report that writes none offers "Got it".
24867
-
24868
24884
  UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first (manage_goals), then send one update saying what is done and anything they need to do or check. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
24869
24885
 
24870
24886
  RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
@@ -24970,6 +24986,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24970
24986
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
24971
24987
  diagnose: external_exports.boolean().optional()
24972
24988
  }).strict();
24989
+ GetDeliverySchema = external_exports.object({
24990
+ deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
24991
+ }).strict();
24992
+ GET_DELIVERY_DESCRIPTION = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
24973
24993
  GET_GOAL_DESCRIPTION = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
24974
24994
  SearchToolSchema = external_exports.object({
24975
24995
  query: external_exports.string().trim().min(1).max(500),
@@ -24978,6 +24998,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24978
24998
  limit: external_exports.number().int().min(1).max(20).optional()
24979
24999
  }).strict();
24980
25000
  SEARCH_DESCRIPTION = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and replies -- the person's answers, acknowledgements and deferrals (type answer; found by their summary, by the question or report they reply to, or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, the replies to its items, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (a reply carries its kind -- answered, acknowledged or deferred -- whether it is closed (a deferral is not), its summary in under ten words, its question or report, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
25001
+ ConfigureIdentitySchema = external_exports.object({}).strict();
25002
+ CONFIGURE_IDENTITY_DESCRIPTION = "Read this authenticated agent\u2019s current name, voice and token ID. Read-only; no arguments. Pairing, renaming and logout are available only through the local configure driver.";
24981
25003
  CheckActivitySchema = external_exports.object({}).strict();
24982
25004
  FEEDBACK_TEXT_MAX = 5e4;
24983
25005
  SendFeedbackSchema = external_exports.object({
@@ -24987,8 +25009,10 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24987
25009
  }).strict();
24988
25010
  SEND_FEEDBACK_DESCRIPTION = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
24989
25011
  AGENT_TOOLS = [
25012
+ { name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION, inputSchema: mcpInputSchema(ConfigureIdentitySchema) },
24990
25013
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
24991
25014
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
25015
+ { name: "get_delivery", description: GET_DELIVERY_DESCRIPTION, inputSchema: mcpInputSchema(GetDeliverySchema) },
24992
25016
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
24993
25017
  { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
24994
25018
  { name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
@@ -25255,6 +25279,42 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25255
25279
  WORKING_MS = 60 * 6e4;
25256
25280
  WORKING_TIER_MS = 2 * 60 * 6e4;
25257
25281
  RECENT_TIER_MS = 24 * 60 * 6e4;
25282
+ json = external_exports.lazy(() => external_exports.union([
25283
+ external_exports.string(),
25284
+ external_exports.number().finite(),
25285
+ external_exports.boolean(),
25286
+ external_exports.null(),
25287
+ external_exports.array(json),
25288
+ external_exports.record(json)
25289
+ ]));
25290
+ clock = external_exports.number().int().nonnegative().safe();
25291
+ HeardEventSchema = external_exports.object({
25292
+ eventId: external_exports.string().uuid(),
25293
+ source: external_exports.enum(["phone", "bot"]),
25294
+ sequence: clock,
25295
+ kind: external_exports.string().min(1),
25296
+ phoneAtMs: clock.optional(),
25297
+ botAtMs: clock.optional(),
25298
+ payload: external_exports.record(json)
25299
+ }).strict();
25300
+ HeardCursorSchema = HeardEventSchema.pick({ source: true, sequence: true, eventId: true });
25301
+ HeardPageSchema = external_exports.object({
25302
+ events: external_exports.array(HeardEventSchema).max(200),
25303
+ next: HeardCursorSchema.nullable()
25304
+ }).strict();
25305
+ MAX_EVENTS = 50;
25306
+ MAX_BYTES = 8e4;
25307
+ batchBytes = (events) => {
25308
+ let bytes = 0;
25309
+ for (const char of JSON.stringify({ events })) {
25310
+ const point = char.codePointAt(0);
25311
+ bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
25312
+ }
25313
+ return bytes;
25314
+ };
25315
+ HeardBatchSchema = external_exports.object({
25316
+ events: external_exports.array(HeardEventSchema).min(1).max(MAX_EVENTS)
25317
+ }).strict().refine((value) => batchBytes(value.events) <= MAX_BYTES, "heard_batch_too_large");
25258
25318
  ContextSchema = external_exports.object({
25259
25319
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
25260
25320
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -25811,8 +25871,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25811
25871
  "inbox",
25812
25872
  "dismiss"
25813
25873
  ]);
25814
- clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
25815
- QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
25874
+ clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
25875
+ QUIET = ` Nothing rings from ${clock2(NIGHT.from)} to ${clock2(NIGHT.to)} your time; the count waits for morning.`;
25816
25876
  BrokerTuningSchema = external_exports.object({
25817
25877
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
25818
25878
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -26127,7 +26187,13 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26127
26187
  /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
26128
26188
  * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
26129
26189
  * none was. The Work row reads it before `said`. */
26130
- summary: external_exports.string().optional()
26190
+ summary: external_exports.string().optional(),
26191
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
26192
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
26193
+ titles: external_exports.array(external_exports.string()).optional(),
26194
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
26195
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
26196
+ line: external_exports.string().optional()
26131
26197
  }).nullable().optional(),
26132
26198
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
26133
26199
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -26501,7 +26567,9 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26501
26567
  words: external_exports.string(),
26502
26568
  atMs: external_exports.number()
26503
26569
  })),
26504
- more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
26570
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
26571
+ /** Last stored position on this page when another exists; absent before paging shipped. */
26572
+ next: external_exports.number().int().nonnegative().nullable().optional()
26505
26573
  });
26506
26574
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
26507
26575
  KEY_PATH = join22(homedir22(), ".paigy", "key.json");
@@ -31430,11 +31498,11 @@ function extractJson(raw) {
31430
31498
  return null;
31431
31499
  }
31432
31500
  function parseVerdicts(raw, knownIds, knownAgents) {
31433
- const json = extractJson(raw);
31434
- if (!json) return { ok: false, error: "no JSON found in the model's answer", raw };
31501
+ const json3 = extractJson(raw);
31502
+ if (!json3) return { ok: false, error: "no JSON found in the model's answer", raw };
31435
31503
  let parsed;
31436
31504
  try {
31437
- parsed = JSON.parse(json);
31505
+ parsed = JSON.parse(json3);
31438
31506
  } catch (e) {
31439
31507
  return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
31440
31508
  }
@@ -31680,13 +31748,17 @@ var init_triage = __esm({
31680
31748
  init_catalog();
31681
31749
 
31682
31750
  // src/cli-args.ts
31751
+ init_zod();
31683
31752
  var TRIAGE_VIA = ["ollama", "claude", "codex", "agy"];
31684
31753
  var USAGE = "usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | enable-tools [--scope user|project] | hatch NAME [--voice KEY] [--slot SLOT] | launch NAME [--under GOAL] [--voice KEY] [--harness \u2026] [--mode \u2026] [--cwd DIR] BRIEF\u2026 | handoff | triage [--via ollama|claude|codex|agy] [--json] [--apply [--yes-close] [--yes-stale] [--yes-assign]] | [--harness claude|codex|agy] [--mode bypass|ask] [--cwd DIR] [--grace SECONDS] [--doctor] PROMPT\u2026";
31685
31754
  function parseArgs(argv) {
31686
31755
  const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, help: false, host: false, pair: false, service: false, setup: false, enableTools: false, scope: "user", grant: [], hatch: null, voice: null, slot: null, launch: null, under: null, graceSeconds: 90, prompt: "", handoff: false, triage: false, via: null, json: false, apply: false, yesClose: false, yesStale: false, yesAssign: false };
31687
31756
  const words2 = [];
31757
+ let commandPosition = true;
31688
31758
  for (let i = 0; i < argv.length; i++) {
31689
31759
  const arg = argv[i];
31760
+ const command = commandPosition && !!arg && !arg.startsWith("-");
31761
+ if (command) commandPosition = false;
31690
31762
  const next = () => argv[++i];
31691
31763
  if (arg === "--harness") {
31692
31764
  const v = next();
@@ -31702,7 +31774,7 @@ function parseArgs(argv) {
31702
31774
  args.cwd = v;
31703
31775
  } else if (arg === "--doctor") {
31704
31776
  args.doctor = true;
31705
- } else if (arg === "-h" || arg === "--help" || arg === "help") {
31777
+ } else if (arg === "-h" || arg === "--help" || command && arg === "help") {
31706
31778
  args.help = true;
31707
31779
  } else if (arg === "--scope") {
31708
31780
  const v = next();
@@ -31712,21 +31784,21 @@ function parseArgs(argv) {
31712
31784
  const v = Number(next());
31713
31785
  if (!Number.isFinite(v) || v < 0) return { error: "--grace needs seconds (0 = straight to phone)" };
31714
31786
  args.graceSeconds = v;
31715
- } else if (arg === "host") {
31787
+ } else if (command && arg === "host") {
31716
31788
  args.host = true;
31717
- } else if (arg === "pair") {
31789
+ } else if (command && arg === "pair") {
31718
31790
  args.pair = true;
31719
- } else if (arg === "setup") {
31791
+ } else if (command && arg === "setup") {
31720
31792
  args.setup = true;
31721
- } else if (arg === "service") {
31793
+ } else if (command && arg === "service") {
31722
31794
  args.service = true;
31723
31795
  } else if (arg === "--grant") {
31724
31796
  const v = next();
31725
31797
  if (!v) return { error: "--grant needs a folder to allow" };
31726
31798
  args.grant.push(v);
31727
- } else if (arg === "handoff") {
31799
+ } else if (command && arg === "handoff") {
31728
31800
  args.handoff = true;
31729
- } else if (arg === "triage") {
31801
+ } else if (command && arg === "triage") {
31730
31802
  args.triage = true;
31731
31803
  } else if (arg === "--via") {
31732
31804
  const v = next();
@@ -31743,9 +31815,9 @@ function parseArgs(argv) {
31743
31815
  args.yesStale = true;
31744
31816
  } else if (arg === "--yes-assign") {
31745
31817
  args.yesAssign = true;
31746
- } else if (arg === "enable-tools") {
31818
+ } else if (command && arg === "enable-tools") {
31747
31819
  args.enableTools = true;
31748
- } else if (arg === "hatch") {
31820
+ } else if (command && arg === "hatch") {
31749
31821
  const v = next();
31750
31822
  if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
31751
31823
  args.hatch = v;
@@ -31755,12 +31827,14 @@ function parseArgs(argv) {
31755
31827
  args.slot = next() ?? null;
31756
31828
  } else if (arg === "--identity") {
31757
31829
  return { error: '--identity is gone: start a helper with `paigy-harness launch NAME "brief"`, which gives it its own session' };
31758
- } else if (arg === "launch") {
31830
+ } else if (command && arg === "launch") {
31759
31831
  const v = next();
31760
31832
  if (!v) return { error: 'launch needs a name: paigy-harness launch "Deploy check" "brief"' };
31761
31833
  args.launch = v;
31762
31834
  } else if (arg === "--under") {
31763
- args.under = next() ?? null;
31835
+ const v = next();
31836
+ if (!v || !external_exports.string().uuid().safeParse(v).success) return { error: "--under needs a Goal UUID" };
31837
+ args.under = v;
31764
31838
  } else if (arg?.startsWith("--")) {
31765
31839
  return { error: `unknown flag ${arg}` };
31766
31840
  } else if (arg) {
@@ -31769,6 +31843,7 @@ function parseArgs(argv) {
31769
31843
  }
31770
31844
  args.prompt = words2.join(" ");
31771
31845
  if (args.help) return args;
31846
+ if (args.under && !args.launch) return { error: "--under requires launch NAME" };
31772
31847
  if (args.launch && !args.prompt) return { error: 'launch needs a brief: paigy-harness launch "Deploy check" "brief"' };
31773
31848
  if (!args.doctor && !args.handoff && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.enableTools && !args.triage && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / enable-tools / hatch NAME / handoff / triage)" };
31774
31849
  if ((args.yesClose || args.yesStale || args.yesAssign) && !args.apply)
@@ -32192,8 +32267,7 @@ function startSession(opts) {
32192
32267
  if (!existsSync3(cwd)) {
32193
32268
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
32194
32269
  }
32195
- let codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
32196
- if (opts.model && opts.harness === "codex") codexConfig = { ...codexConfig, model: opts.model };
32270
+ const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
32197
32271
  if (codexConfig) {
32198
32272
  const key3 = "mcp_servers.paigy.env_vars";
32199
32273
  const configPath = join5(process.env.CODEX_HOME || join5(homedir4(), ".codex"), "config.toml");
@@ -32219,8 +32293,6 @@ function startSession(opts) {
32219
32293
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
32220
32294
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
32221
32295
  ...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
32222
- ...opts.model && opts.harness === "claude" ? { CLAUDE_MODEL_CONFIG: JSON.stringify({ provider: "anthropic", model: opts.model }) } : {},
32223
- ...opts.model && opts.harness === "agy" ? { AGY_MODEL_CONFIG: JSON.stringify({ model: opts.model }) } : {},
32224
32296
  ...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
32225
32297
  ...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
32226
32298
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
@@ -33599,6 +33671,7 @@ init_zod();
33599
33671
  init_zod();
33600
33672
  init_zod();
33601
33673
  init_zod();
33674
+ init_zod();
33602
33675
  var OPTIONS_MIN2 = 2;
33603
33676
  var OPTIONS_MAX2 = 6;
33604
33677
  var OPTION_LABEL_MAX2 = 50;
@@ -33628,6 +33701,16 @@ var OptionInputSchema2 = external_exports.object({
33628
33701
  imgUrl: OptionSchema2.shape.image
33629
33702
  }).strict();
33630
33703
  var NIGHT2 = { from: 23, to: 7 };
33704
+ var ASKING_RULES2 = `HOW TO ASK:
33705
+ 1. Write one question per object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
33706
+ 2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
33707
+ 3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
33708
+ 4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. While a Call you placed is open, keep receiving window after window until it ends, even when a window is empty: the person may give follow-ups or instructions. With no open Call, STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
33709
+ 5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
33710
+ 6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
33711
+ 7. A question carries its options when you have them; a free-text question may omit them. Each option\u2019s label must stand on its own and name its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Each question is sent exactly as written, with no reading in between. They can always answer in their own words, so never add an 'Other' option.
33712
+ 8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
33713
+ 9. GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. What they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Requested delivery and completed-Goal reports require a reply (reply_required); ordinary progress may omit it.`;
33631
33714
  function draft20202(node) {
33632
33715
  if (Array.isArray(node)) return node.map(draft20202);
33633
33716
  if (node && typeof node === "object") {
@@ -33746,10 +33829,18 @@ var UpdateInputSchema2 = external_exports.object({
33746
33829
  // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
33747
33830
  // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
33748
33831
  reply: external_exports.string().trim().min(1).describe(
33749
- `What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX2} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. Left out, the button says "Got it", as every card did.`
33832
+ `What this report's button says instead of "Got it", at most ${OPTION_LABEL_MAX2} characters, written in THEIR voice for THIS report ("Sounds good, I'll test later") -- the care you give a question's options, so what they tap tells you whether they verified your work or only received it. One tap sends exactly these words back to you. Words that would CHANGE what happens next are a question, not a reply to a report: it still asks nothing and still owes you no answer. The reply is required for requested delivery and completed Goals; ordinary progress may omit it.`
33750
33833
  ).optional(),
33751
33834
  userExplicitlyRequested: userExplicitlyRequested2
33752
33835
  }).strict().superRefine((u, ctx) => {
33836
+ if (u.userExplicitlyRequested && !u.reply) {
33837
+ ctx.addIssue({
33838
+ code: external_exports.ZodIssueCode.custom,
33839
+ path: ["reply"],
33840
+ params: { refusal: "reply_required" },
33841
+ message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
33842
+ });
33843
+ }
33753
33844
  refuseOverCaps2(u.message, u.units, "message", ctx);
33754
33845
  if (u.reply && u.reply.length > OPTION_LABEL_MAX2) {
33755
33846
  ctx.addIssue({
@@ -33837,14 +33928,10 @@ var ContactWireSchema2 = external_exports.object({
33837
33928
  questions: external_exports.array(QuestionWireSchema2).min(1).max(20).optional()
33838
33929
  }).strict().refine(sendsAnything2, "a contact sends at least one update, question, answer, withdrawal or question dependency");
33839
33930
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
33840
- var CONTACT_DESCRIPTION2 = `Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read \`results\`, where a failed item names why while the rest still apply.
33841
-
33842
- QUESTIONS: one question per object; several questions are several objects in one contact, so each can be answered on its own. Each is sent exactly as written, with no reading in between: a bundled question arrives as one card. Give options when you have them. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). pickMode:'one' (the default) makes a pick, for alternatives; 'many' a checklist, for independent options they may want several of; 'rank' an ordering. The person can always answer in their own words, so never add an 'Other' option. workItBlocks names the work a question holds up, only when you are truly blocked: it asks for a call. Answers land on the question's Goal: collect them by receiving (below) or with get_goal.
33931
+ var CONTACT_DESCRIPTION2 = "Send to and receive from the person: updates, questions, answers to questions addressed to you, withdrawals of your own questions, and what a question holds up. Every Goal you name must already exist: create it with manage_goals first; an unknown Goal is refused and nothing is sent. Every item applies on its own: read `results`, where a failed item names why while the rest still apply.\n\n" + ASKING_RULES2 + `
33843
33932
 
33844
33933
  CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX2} characters; everything else it needs goes in 'units', up to ${UNITS_MAX2} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX2} characters and every body at most ${UNIT_BODY_MAX2}. The person sees the main text and the titles, and opens a unit to read its body. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.
33845
33934
 
33846
- GIVE A REPORT ITS REPLY: an update takes \`reply\` too, one line the person can tap instead of typing, written in THEIR voice for THAT report ("Sounds good, I'll test later") \u2014 the same care you give a question's options. It is what their button says instead of "Got it", and what they tap is what you read back, which tells you whether they verified your work or only received it. One line, because a report has one way to close, and it may not change what happens next: a reply that would is a question. Nothing is owed on it either way, and a report that writes none offers "Got it".
33847
-
33848
33935
  UPDATES ask nothing, so no answer is owed and none should be awaited. An update reaches the person only when they asked you for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; any other is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first (manage_goals), then send one update saying what is done and anything they need to do or check. On a Goal whose report card is still open, an update that reaches them is added to that card, with no new push. If the person is already on a call, anything that reaches them joins that call, with no ring.
33849
33936
 
33850
33937
  RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of what is addressed to you (not a history page): \`question\`, a Question you owe (answer it in answers, with its questionId); \`update\`, something new on one of your Goals (a reply, an answer: read it with get_goal); \`instruction\`, a request or note sent to you. \`hasMore\` says more are waiting. Reading acknowledges nothing: once you have handled events, confirm their eventIds with contact({ackEventIds}), and the next batch can come. contact({}) waits up to about 45 seconds for something to arrive; contact({wait:false}) returns at once. It also lists work given to you that nobody has started (\`assigned\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
@@ -33950,6 +34037,10 @@ var GetGoalSchema2 = external_exports.object({
33950
34037
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
33951
34038
  diagnose: external_exports.boolean().optional()
33952
34039
  }).strict();
34040
+ var GetDeliverySchema2 = external_exports.object({
34041
+ deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
34042
+ }).strict();
34043
+ var GET_DELIVERY_DESCRIPTION2 = "Read a Delivery in full: its whole ask, units, options and answers, plus current callState and callOutcome for a Call. Any agent of the same account may read it; another account is not disclosed. Reading never acknowledges or resends anything. Use this after a call or when receive names a Call you are waiting on.";
33953
34044
  var GET_GOAL_DESCRIPTION2 = "Read one Goal (a read never puts you on it), including others (the ten most recent other contributors, with names, latest entry headlines and times): its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
33954
34045
  var SearchToolSchema2 = external_exports.object({
33955
34046
  query: external_exports.string().trim().min(1).max(500),
@@ -33958,6 +34049,8 @@ var SearchToolSchema2 = external_exports.object({
33958
34049
  limit: external_exports.number().int().min(1).max(20).optional()
33959
34050
  }).strict();
33960
34051
  var SEARCH_DESCRIPTION2 = "Search your person's history across all of their agents: Entries (what anyone said or wrote, typed or spoken on a call), Goals (by title and outcome) and replies -- the person's answers, acknowledgements and deferrals (type answer; found by their summary, by the question or report they reply to, or by the words that gave them). query is words to look for; records sharing more of its words rank first, and exact names work. types narrows it to entry, goal and/or answer (default: all three). goalId searches under one Goal: its Entries, the replies to its items, and it and its immediate children. limit is matches per type, 1 to 20 (default 8). Read-only. Each match carries its whole saved words, its ID and its links (a reply carries its kind -- answered, acknowledged or deferred -- whether it is closed (a deferral is not), its summary in under ten words, its question or report, the choice made and the Entries that support it); `omitted` counts what matched but was left out, so narrow the words or add a goalId to see it. Nothing found is not proof that nothing exists; a refused search says why. Sealed (encrypted) content is never searched or returned.";
34052
+ var ConfigureIdentitySchema2 = external_exports.object({}).strict();
34053
+ var CONFIGURE_IDENTITY_DESCRIPTION2 = "Read this authenticated agent\u2019s current name, voice and token ID. Read-only; no arguments. Pairing, renaming and logout are available only through the local configure driver.";
33961
34054
  var CheckActivitySchema2 = external_exports.object({}).strict();
33962
34055
  var FEEDBACK_TEXT_MAX2 = 5e4;
33963
34056
  var SendFeedbackSchema2 = external_exports.object({
@@ -33967,8 +34060,10 @@ var SendFeedbackSchema2 = external_exports.object({
33967
34060
  }).strict();
33968
34061
  var SEND_FEEDBACK_DESCRIPTION2 = "Send feedback about Paigy itself (a bug report, an idea) straight to the Paigy team. Send it only when the person asked you to, or agreed when you offered: it goes to Paigy, never to the person, and nobody answers it here. Not for your work or a question for the person: use contact for those. Returns whether it was stored.";
33969
34062
  var AGENT_TOOLS2 = [
34063
+ { name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION2, inputSchema: mcpInputSchema2(ConfigureIdentitySchema2) },
33970
34064
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
33971
34065
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(ManageGoalsToolSchema2) },
34066
+ { name: "get_delivery", description: GET_DELIVERY_DESCRIPTION2, inputSchema: mcpInputSchema2(GetDeliverySchema2) },
33972
34067
  { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
33973
34068
  { name: "search", description: SEARCH_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchToolSchema2) },
33974
34069
  { name: "check_activity", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema2(CheckActivitySchema2) },
@@ -34250,8 +34345,45 @@ function entryWords(entry) {
34250
34345
  }
34251
34346
  var LIVE_MS2 = 3 * 6e4;
34252
34347
  var WORKING_MS2 = 60 * 6e4;
34348
+ var isWorking = (c, now) => !!c.lastWorkedAt && now - Date.parse(c.lastWorkedAt) < WORKING_MS2;
34253
34349
  var WORKING_TIER_MS2 = 2 * 60 * 6e4;
34254
34350
  var RECENT_TIER_MS2 = 24 * 60 * 6e4;
34351
+ var json2 = external_exports.lazy(() => external_exports.union([
34352
+ external_exports.string(),
34353
+ external_exports.number().finite(),
34354
+ external_exports.boolean(),
34355
+ external_exports.null(),
34356
+ external_exports.array(json2),
34357
+ external_exports.record(json2)
34358
+ ]));
34359
+ var clock3 = external_exports.number().int().nonnegative().safe();
34360
+ var HeardEventSchema2 = external_exports.object({
34361
+ eventId: external_exports.string().uuid(),
34362
+ source: external_exports.enum(["phone", "bot"]),
34363
+ sequence: clock3,
34364
+ kind: external_exports.string().min(1),
34365
+ phoneAtMs: clock3.optional(),
34366
+ botAtMs: clock3.optional(),
34367
+ payload: external_exports.record(json2)
34368
+ }).strict();
34369
+ var HeardCursorSchema2 = HeardEventSchema2.pick({ source: true, sequence: true, eventId: true });
34370
+ var HeardPageSchema2 = external_exports.object({
34371
+ events: external_exports.array(HeardEventSchema2).max(200),
34372
+ next: HeardCursorSchema2.nullable()
34373
+ }).strict();
34374
+ var MAX_EVENTS2 = 50;
34375
+ var MAX_BYTES2 = 8e4;
34376
+ var batchBytes2 = (events) => {
34377
+ let bytes = 0;
34378
+ for (const char of JSON.stringify({ events })) {
34379
+ const point = char.codePointAt(0);
34380
+ bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
34381
+ }
34382
+ return bytes;
34383
+ };
34384
+ var HeardBatchSchema2 = external_exports.object({
34385
+ events: external_exports.array(HeardEventSchema2).min(1).max(MAX_EVENTS2)
34386
+ }).strict().refine((value) => batchBytes2(value.events) <= MAX_BYTES2, "heard_batch_too_large");
34255
34387
  var ContextSchema2 = external_exports.object({
34256
34388
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
34257
34389
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -34808,8 +34940,8 @@ var MissedCallSchema2 = external_exports.enum([
34808
34940
  "inbox",
34809
34941
  "dismiss"
34810
34942
  ]);
34811
- var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
34812
- var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
34943
+ var clock22 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
34944
+ var QUIET2 = ` Nothing rings from ${clock22(NIGHT2.from)} to ${clock22(NIGHT2.to)} your time; the count waits for morning.`;
34813
34945
  var BrokerTuningSchema2 = external_exports.object({
34814
34946
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
34815
34947
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -35124,7 +35256,13 @@ var QueueItemSchema2 = external_exports.object({
35124
35256
  /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
35125
35257
  * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
35126
35258
  * none was. The Work row reads it before `said`. */
35127
- summary: external_exports.string().optional()
35259
+ summary: external_exports.string().optional(),
35260
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
35261
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
35262
+ titles: external_exports.array(external_exports.string()).optional(),
35263
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
35264
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
35265
+ line: external_exports.string().optional()
35128
35266
  }).nullable().optional(),
35129
35267
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
35130
35268
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -35496,7 +35634,9 @@ var CallRecapSchema2 = external_exports.object({
35496
35634
  words: external_exports.string(),
35497
35635
  atMs: external_exports.number()
35498
35636
  })),
35499
- more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
35637
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
35638
+ /** Last stored position on this page when another exists; absent before paging shipped. */
35639
+ next: external_exports.number().int().nonnegative().nullable().optional()
35500
35640
  });
35501
35641
 
35502
35642
  // src/paigy/activity.ts
@@ -35570,9 +35710,9 @@ function limitIn(text, now) {
35570
35710
  if (!line) return null;
35571
35711
  const reset = resetOf(line, now);
35572
35712
  if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
35573
- return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock3(reset) };
35713
+ return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock4(reset) };
35574
35714
  }
35575
- return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
35715
+ return { until: now + RETRY_MS2, resets: clock4(now + RETRY_MS2) };
35576
35716
  }
35577
35717
  function resetOf(line, now) {
35578
35718
  const m = /resets\s+(?:at\s+)?(?:([a-z]{3})[a-z]*\s+(\d{1,2}),?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b(?:\s*\(([^)]+)\))?/i.exec(line);
@@ -35616,7 +35756,7 @@ function wallToEpoch(year, month, day, hour, minute, zone) {
35616
35756
  const first = wall - offset(wall);
35617
35757
  return wall - offset(first);
35618
35758
  }
35619
- function clock3(t) {
35759
+ function clock4(t) {
35620
35760
  return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
35621
35761
  }
35622
35762
  var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
@@ -35660,7 +35800,7 @@ function episodes(opts = {}) {
35660
35800
  episode.called = true;
35661
35801
  const rest = episode.paused.filter((n) => !episode.running.includes(n));
35662
35802
  const text = [
35663
- `Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
35803
+ `Claude's usage limit paused ${names(episode.paused)} from ${clock4(episode.since)}.`,
35664
35804
  rest.length ? `It has reset: ${names(episode.running)} ${episode.running.length > 1 ? "are" : "is"} running again, and ${names(rest)} ${rest.length > 1 ? "haven't" : "hasn't"} started again yet.` : `It has reset, and ${episode.paused.length > 1 ? "they have all" : "it has"} carried on.`
35665
35805
  ].join(" ");
35666
35806
  void call(text).then(
@@ -36153,7 +36293,7 @@ function send2(session, state, text) {
36153
36293
  session.send(text);
36154
36294
  }
36155
36295
  async function pump(session, state, rail, opts) {
36156
- const clock4 = opts.beat ?? beat();
36296
+ const clock5 = opts.beat ?? beat();
36157
36297
  const now = opts.now ?? Date.now;
36158
36298
  const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
36159
36299
  const tally = { noted: 0, seen: 0, announced: 0 };
@@ -36185,7 +36325,7 @@ async function pump(session, state, rail, opts) {
36185
36325
  counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
36186
36326
  since = now();
36187
36327
  }
36188
- cause = await clock4.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
36328
+ cause = await clock5.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
36189
36329
  if (!await opts.onIdle()) return;
36190
36330
  }
36191
36331
  }
@@ -36195,7 +36335,7 @@ function runHarness(opts) {
36195
36335
  let running = true;
36196
36336
  let tail = [];
36197
36337
  const state = { resting: true };
36198
- const clock4 = beat();
36338
+ const clock5 = beat();
36199
36339
  let session = null;
36200
36340
  const speaksAs = opts.session ?? opts.resume ?? sessionId();
36201
36341
  const instance = randomUUID4();
@@ -36256,7 +36396,7 @@ ${event.message}`, state, rail, { log: opts.log });
36256
36396
  stop2();
36257
36397
  return;
36258
36398
  } else if (carried) runningAgain();
36259
- clock4.nudge("rest");
36399
+ clock5.nudge("rest");
36260
36400
  }
36261
36401
  }
36262
36402
  session = startSession({
@@ -36275,13 +36415,13 @@ ${event.message}`, state, rail, { log: opts.log });
36275
36415
  },
36276
36416
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
36277
36417
  });
36278
- void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
36418
+ void pump(session, state, rail, { onIdle: () => running, beat: clock5, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
36279
36419
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
36280
36420
  );
36281
36421
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
36282
36422
  function stop2() {
36283
36423
  running = false;
36284
- clock4.nudge("wake");
36424
+ clock5.nudge("wake");
36285
36425
  tail = [];
36286
36426
  cancelAsks(state);
36287
36427
  session?.stop();
@@ -36296,7 +36436,7 @@ ${event.message}`, state, rail, { log: opts.log });
36296
36436
  working: () => running && state.resting !== true,
36297
36437
  // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
36298
36438
  tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
36299
- nudge: () => clock4.nudge("wake"),
36439
+ nudge: () => clock5.nudge("wake"),
36300
36440
  stop: stop2
36301
36441
  };
36302
36442
  }
@@ -36727,7 +36867,7 @@ var claimedRun = (sessionId2) => {
36727
36867
  const slot = sessionSlot(sessionId2);
36728
36868
  return { slot, key: runKey(slot) };
36729
36869
  };
36730
- var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
36870
+ var launchRun = (spec) => ({ session: spec.sessionId });
36731
36871
  var BOUND_ELSEWHERE_MS = 10 * 6e4;
36732
36872
  var boundElsewhere = /* @__PURE__ */ new Map();
36733
36873
  var BOUND_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";
@@ -36757,13 +36897,12 @@ function evict(runs, key3, run) {
36757
36897
  if (runs.get(key3)?.run === run) runs.delete(key3);
36758
36898
  }
36759
36899
  var PRESENCE_FRESH_MS = 3 * 6e4;
36760
- var IDLE_HOLDER_MS = 30 * 6e4;
36761
36900
  function wakeAction(live, now, serverSeenAt, here = {}) {
36762
36901
  if (live) return { action: "skip", why: "run" };
36763
36902
  if (here.holder) return { action: "skip", why: "holder" };
36764
36903
  if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
36765
36904
  if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
36766
- if (here.movedAt !== void 0 && now - here.movedAt >= IDLE_HOLDER_MS) return { action: "spawn", why: "idle" };
36905
+ if (here.movedAt !== void 0 && now - here.movedAt >= WORKING_MS2 && !isWorking(here, now)) return { action: "spawn", why: "idle" };
36767
36906
  return { action: "skip", why: "presence" };
36768
36907
  }
36769
36908
  return { action: "spawn", why: "nobody" };
@@ -36836,7 +36975,7 @@ function startHost(opts) {
36836
36975
  });
36837
36976
  void heartbeat({
36838
36977
  version: VERSION,
36839
- harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
36978
+ harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
36840
36979
  workspaces: listWorkspaces(opts.wsDeps),
36841
36980
  // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
36842
36981
  repos: listRepos(opts.wsDeps)
@@ -36918,7 +37057,7 @@ function startHost(opts) {
36918
37057
  const presence = /* @__PURE__ */ new Map();
36919
37058
  try {
36920
37059
  for (const c of (await listConnections(asHost)).items) {
36921
- if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
37060
+ presence.set(c.id, c);
36922
37061
  }
36923
37062
  lastPresence = presence;
36924
37063
  } catch {
@@ -36951,11 +37090,12 @@ function startHost(opts) {
36951
37090
  }
36952
37091
  }
36953
37092
  const tokenId = slotIdentity(slot).tokenId;
36954
- const seenByServer = tokenId ? presence.get(tokenId) : void 0;
37093
+ const connection = tokenId ? presence.get(tokenId) : void 0;
37094
+ const seenByServer = connection?.lastSeenAt ? Date.parse(connection.lastSeenAt) : void 0;
36955
37095
  const holder = holders.get(slot);
36956
37096
  if (holder) heldHere.add(slot);
36957
37097
  else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
36958
- const here = { holder, goneAt: goneAt.get(slot) };
37098
+ const here = { holder, goneAt: goneAt.get(slot), lastWorkedAt: connection?.lastWorkedAt };
36959
37099
  let decided = wakeAction(!!live, Date.now(), seenByServer, here);
36960
37100
  const label = slotName(slot) ?? slot;
36961
37101
  if (decided.action === "skip" && decided.why !== "presence") {
@@ -36998,7 +37138,7 @@ function startHost(opts) {
36998
37138
  const since = found.since;
36999
37139
  decided = wakeAction(false, Date.now(), seenByServer, { ...here, ...since ? { movedAt: Date.parse(since) } : {} });
37000
37140
  if (decided.action === "skip") {
37001
- const said = `is working somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; taken over once it sits ${IDLE_HOLDER_MS / 6e4} min untouched` : ", with nothing to say when it last moved"}`;
37141
+ const said = `is connected somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; takeover requires ${WORKING_MS2 / 6e4} min without own activity or waiting-work movement` : ", with nothing to say when it last moved"}`;
37002
37142
  opts.log(`\u25CC ${label} ${said} \u2014 not spawning`);
37003
37143
  decide(slot, "stood_back", found, seenByServer, said);
37004
37144
  return;
@@ -37167,7 +37307,7 @@ function startHost(opts) {
37167
37307
  // sessions we started ourselves. The server already knows: `last_seen_at` on the
37168
37308
  // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
37169
37309
  // hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
37170
- running: live.has(name) || !!slot && lastHolders.has(slot) || !!id3.tokenId && Date.now() - (lastPresence.get(id3.tokenId) ?? 0) < PRESENCE_FRESH_MS,
37310
+ running: live.has(name) || !!slot && lastHolders.has(slot) || !!id3.tokenId && Date.now() - Date.parse(lastPresence.get(id3.tokenId)?.lastSeenAt ?? "") < PRESENCE_FRESH_MS,
37171
37311
  // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
37172
37312
  // thing a tail can tell us. A process we did not spawn has no tail here to read.
37173
37313
  working: live.get(name)?.working() ?? false