@paigy/harness 0.3.38 → 0.3.40

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 +348 -169
  2. package/dist/main.js +3613 -3447
  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() {
@@ -23699,35 +23696,6 @@ function parseDef(def, refs, forceResolution = false) {
23699
23696
  newItem.jsonSchema = jsonSchema;
23700
23697
  return jsonSchema;
23701
23698
  }
23702
- function draft2020(node) {
23703
- if (Array.isArray(node)) return node.map(draft2020);
23704
- if (node && typeof node === "object") {
23705
- const o = node;
23706
- if (Array.isArray(o.items)) {
23707
- o.prefixItems = o.items;
23708
- if ("additionalItems" in o) {
23709
- o.items = o.additionalItems;
23710
- delete o.additionalItems;
23711
- } else delete o.items;
23712
- }
23713
- for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
23714
- if (typeof o[excl] === "boolean") {
23715
- if (o[excl] === true && typeof o[lim] === "number") {
23716
- o[excl] = o[lim];
23717
- delete o[lim];
23718
- } else delete o[excl];
23719
- }
23720
- }
23721
- for (const k of Object.keys(o)) o[k] = draft2020(o[k]);
23722
- return o;
23723
- }
23724
- return node;
23725
- }
23726
- function mcpInputSchema(s) {
23727
- const schema = zodToJsonSchema(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
23728
- delete schema.$schema;
23729
- return draft2020(schema);
23730
- }
23731
23699
  function capRefusals(text, units22, field) {
23732
23700
  const out = [];
23733
23701
  const name = field === "text" ? "answer" : field;
@@ -23769,6 +23737,35 @@ function capRefusals(text, units22, field) {
23769
23737
  function refuseOverCaps(text, units22, field, ctx) {
23770
23738
  for (const { path, message, ...params } of capRefusals(text, units22, field)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message, params });
23771
23739
  }
23740
+ function draft2020(node) {
23741
+ if (Array.isArray(node)) return node.map(draft2020);
23742
+ if (node && typeof node === "object") {
23743
+ const o = node;
23744
+ if (Array.isArray(o.items)) {
23745
+ o.prefixItems = o.items;
23746
+ if ("additionalItems" in o) {
23747
+ o.items = o.additionalItems;
23748
+ delete o.additionalItems;
23749
+ } else delete o.items;
23750
+ }
23751
+ for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
23752
+ if (typeof o[excl] === "boolean") {
23753
+ if (o[excl] === true && typeof o[lim] === "number") {
23754
+ o[excl] = o[lim];
23755
+ delete o[lim];
23756
+ } else delete o[excl];
23757
+ }
23758
+ }
23759
+ for (const k of Object.keys(o)) o[k] = draft2020(o[k]);
23760
+ return o;
23761
+ }
23762
+ return node;
23763
+ }
23764
+ function mcpInputSchema(s) {
23765
+ const schema = zodToJsonSchema(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
23766
+ delete schema.$schema;
23767
+ return draft2020(schema);
23768
+ }
23772
23769
  function entryUnit(entry) {
23773
23770
  if (entry.kind !== "contribution" || !entry.aboutId) return null;
23774
23771
  const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
@@ -23901,15 +23898,15 @@ async function manageGoals(input, opts = {}) {
23901
23898
  if (!res.ok) await fail("manage_goals", res);
23902
23899
  return await res.json();
23903
23900
  }
23904
- async function getGoal(goalId, opts = {}, read3 = {}) {
23901
+ async function getGoals(goalIds, opts = {}, read3 = {}) {
23905
23902
  const token = authToken(opts.token) ?? "";
23906
- const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
23907
- const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${query ? `?${query}` : ""}`, {
23908
- method: "GET",
23909
- headers: { authorization: `Bearer ${token}` }
23903
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/read`, {
23904
+ method: "POST",
23905
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
23906
+ body: JSON.stringify({ goalIds, ...read3 })
23910
23907
  }));
23911
- if (!res.ok) await fail("get_goal", res);
23912
- return await res.json();
23908
+ if (!res.ok) await fail("get_goals", res);
23909
+ return (await res.json()).goals;
23913
23910
  }
23914
23911
  async function hatch(name, voice = null, opts = {}) {
23915
23912
  const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/hatch`, {
@@ -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)}` }
@@ -24044,6 +24045,7 @@ async function contact(input, opts = {}) {
24044
24045
  body: JSON.stringify({
24045
24046
  operationId: opts.operationId ?? randomUUID2(),
24046
24047
  ...parsed.updates ? { updates: parsed.updates } : {},
24048
+ ...parsed.completions ? { completions: parsed.completions } : {},
24047
24049
  ...questions ? { questions } : {},
24048
24050
  ...parsed.answers ? { answers: parsed.answers } : {},
24049
24051
  ...parsed.withdrawQuestionIds ? { withdrawQuestionIds: parsed.withdrawQuestionIds } : {},
@@ -24294,13 +24296,15 @@ ${block2}`;
24294
24296
  }
24295
24297
  return text.slice(0, start) + updated + text.slice(end);
24296
24298
  }
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;
24299
+ 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, COMPLETION_MESSAGE_MAX, CompletionSchema, CompleteChangeSchema, ASKING_RULES, 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, GetGoalsSchema, GetDeliverySchema, GET_DELIVERY_DESCRIPTION, GET_GOALS_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
24300
  var init_dist = __esm({
24299
24301
  "../../packages/sdk/dist/index.js"() {
24300
24302
  "use strict";
24301
24303
  init_zod();
24302
24304
  init_zod();
24303
24305
  init_zod();
24306
+ init_zod();
24307
+ init_zod();
24304
24308
  init_v3();
24305
24309
  init_v3();
24306
24310
  init_v3();
@@ -24310,6 +24314,7 @@ var init_dist = __esm({
24310
24314
  init_zod();
24311
24315
  init_zod();
24312
24316
  init_zod();
24317
+ init_zod();
24313
24318
  import_undici = __toESM(require_undici(), 1);
24314
24319
  require2 = __sdkCreateRequire(import.meta.url);
24315
24320
  CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
@@ -24726,6 +24731,31 @@ var init_dist = __esm({
24726
24731
  title: external_exports.string().trim().min(1).describe(`What this unit is about, at most ${UNIT_TITLE_MAX} characters. The app lists the titles; the person taps one to read its body.`),
24727
24732
  body: external_exports.string().trim().min(1).describe(`The unit itself, at most ${UNIT_BODY_MAX} characters.`)
24728
24733
  }).strict();
24734
+ COMPLETION_MESSAGE_MAX = 50;
24735
+ CompletionSchema = external_exports.object({
24736
+ goalId: external_exports.string().uuid().describe("The existing Goal to complete."),
24737
+ message: external_exports.string().trim().min(1).max(COMPLETION_MESSAGE_MAX, "completion_message_too_long: at most 50 characters; put detail in units.").describe("Completion headline, at most 50 characters."),
24738
+ units: external_exports.array(UnitInputSchema.extend({
24739
+ title: external_exports.string().trim().min(1).max(UNIT_TITLE_MAX),
24740
+ body: external_exports.string().trim().min(1).max(UNIT_BODY_MAX)
24741
+ })).max(UNITS_MAX).optional().describe("Suggest initial state, changes, current state with verification and limits, and next steps / leftover work with its owner when useful. One idea per unit; omit unnecessary sections."),
24742
+ reply: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX).describe("One acknowledgement in the person's voice, at most 50 characters. Acknowledgement alone requests no follow-up and does not claim they tested the work."),
24743
+ options: external_exports.array(external_exports.object({
24744
+ label: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX),
24745
+ hint: external_exports.string().trim().min(1).max(OPTION_HINT_MAX)
24746
+ }).strict()).max(5).optional().describe("Optional next steps the person may initiate: label (50 characters) and consequence in hint (200). Selecting one acknowledges the report and requests that step from its author; it never reopens this Goal.")
24747
+ }).strict();
24748
+ CompleteChangeSchema = CompletionSchema.extend({ kind: external_exports.literal("complete") });
24749
+ ASKING_RULES = `HOW TO ASK:
24750
+ 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.
24751
+ 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.
24752
+ 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.
24753
+ 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_goals\` on that wake.
24754
+ 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\`.
24755
+ 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.
24756
+ 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.
24757
+ 8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
24758
+ 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.`;
24729
24759
  questionHandle = external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
24730
24760
  NodeRef = external_exports.object({
24731
24761
  type: external_exports.enum(["goal", "question"]),
@@ -24766,10 +24796,18 @@ var init_dist = __esm({
24766
24796
  // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
24767
24797
  // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
24768
24798
  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.`
24799
+ `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
24800
  ).optional(),
24771
24801
  userExplicitlyRequested
24772
24802
  }).strict().superRefine((u, ctx) => {
24803
+ if (u.userExplicitlyRequested && !u.reply) {
24804
+ ctx.addIssue({
24805
+ code: external_exports.ZodIssueCode.custom,
24806
+ path: ["reply"],
24807
+ params: { refusal: "reply_required" },
24808
+ message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
24809
+ });
24810
+ }
24773
24811
  refuseOverCaps(u.message, u.units, "message", ctx);
24774
24812
  if (u.reply && u.reply.length > OPTION_LABEL_MAX) {
24775
24813
  ctx.addIssue({
@@ -24828,6 +24866,7 @@ var init_dist = __esm({
24828
24866
  reason: reason.optional()
24829
24867
  }).strict().refine(actionFits, ACTION_FITS);
24830
24868
  sendFields = {
24869
+ completions: external_exports.array(CompletionSchema).min(1).max(20).optional().describe("Close existing Goals and deliver their completion reports atomically. Ordinary updates never close work."),
24831
24870
  updates: external_exports.array(UpdateInputSchema).min(1).max(20).optional().describe(
24832
24871
  "News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress and nobody is notified."
24833
24872
  ),
@@ -24850,24 +24889,22 @@ var init_dist = __esm({
24850
24889
  "Only controls when this returns. Sending: false (the default) returns at once; true waits up to about 45 seconds for a response to what you sent. Sending nothing: true (the default) waits up to about 45 seconds for anything addressed to you; false returns what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived; wait again without sending again), or `not_waited`."
24851
24890
  )
24852
24891
  }).strict();
24853
- sendsAnything = (c) => !!(c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
24892
+ sendsAnything = (c) => !!(c.completions?.length || c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
24854
24893
  ContactWireSchema = external_exports.object({
24855
24894
  operationId: external_exports.string().uuid(),
24856
24895
  ...sendFields,
24857
24896
  questions: external_exports.array(QuestionWireSchema).min(1).max(20).optional()
24858
24897
  }).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
24859
24898
  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.
24899
+ CONTACT_DESCRIPTION = "Send to and receive from the person: completions, 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
24900
 
24864
24901
  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
24902
 
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".
24903
+ 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. Ordinary updates never close a Goal. 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.
24867
24904
 
24868
- 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.
24905
+ COMPLETIONS close and report in one operation: completions:[{goalId,message,units?,reply,options?}]. The same contract works in manage_goals changes with kind:complete. message is at most 50 characters; use units when useful for initial state, changes, current state (verification and limits), and next steps / leftover work with its owner. reply is one acknowledgement in the person's voice. Optional options:[{label,hint}] suggest next steps: a tap acknowledges the report and requests that step from its author. It never reopens the completed Goal; follow-up work needs a new Goal. Closure and the report commit together or neither does. Required children and dependencies must already be finished. No Reviewer rejudges your completion; say only what your evidence supports.
24869
24906
 
24870
- 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\`).`;
24907
+ 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_goals); \`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\`).`;
24871
24908
  CreateGoalSchema = external_exports.object({
24872
24909
  outcome: external_exports.string().trim().min(1).max(1e4),
24873
24910
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -24940,6 +24977,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24940
24977
  read = { revision: external_exports.number().int().positive().optional() };
24941
24978
  GoalChangeSchema = external_exports.discriminatedUnion("kind", [
24942
24979
  CreateChange,
24980
+ CompleteChangeSchema.extend(read),
24943
24981
  EditChange.extend(read),
24944
24982
  StateChange.extend(read),
24945
24983
  AssignChange.extend(read),
@@ -24957,20 +24995,20 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24957
24995
  changes: external_exports.array(GoalChangeSchema).min(1).max(50)
24958
24996
  }).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
24959
24997
  ManageGoalsToolSchema = external_exports.object({
24960
- changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
24998
+ changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange, CompleteChangeSchema, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
24961
24999
  }).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
24962
- MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
24963
- GetGoalSchema = external_exports.object({
24964
- goalId: external_exports.string().uuid(),
25000
+ MANAGE_GOALS_DESCRIPTION = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: complete (goalId, message capped at 50 characters, units, reply, optional next-step options {label, hint}) closes and reports atomically, exactly as contact.completions; create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goals) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
25001
+ GetGoalsSchema = external_exports.object({
25002
+ goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals to read, in order; one to ten IDs."),
24965
25003
  /** Every entry in full. Without it the read carries the person's words, open questions and your
24966
25004
  * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
24967
- history: external_exports.boolean().optional(),
24968
- /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
24969
- * about this work, each answer attributed to the reader that gave it, and every disagreement
24970
- * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
24971
- diagnose: external_exports.boolean().optional()
25005
+ history: external_exports.boolean().optional()
24972
25006
  }).strict();
24973
- 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.";
25007
+ GetDeliverySchema = external_exports.object({
25008
+ deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
25009
+ }).strict();
25010
+ 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.";
25011
+ GET_GOALS_DESCRIPTION = "Read one to ten Goals by goalIds, returned in the same order under goals (a read never puts you on them). history applies to every requested Goal. A refused read fails the call with its original refusal; no partial result is presented as complete. Each Goal includes its ten most recent other contributors (names, latest entry headlines and times), 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.";
24974
25012
  SearchToolSchema = external_exports.object({
24975
25013
  query: external_exports.string().trim().min(1).max(500),
24976
25014
  types: external_exports.array(external_exports.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -24978,6 +25016,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24978
25016
  limit: external_exports.number().int().min(1).max(20).optional()
24979
25017
  }).strict();
24980
25018
  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.";
25019
+ ConfigureIdentitySchema = external_exports.object({}).strict();
25020
+ 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
25021
  CheckActivitySchema = external_exports.object({}).strict();
24982
25022
  FEEDBACK_TEXT_MAX = 5e4;
24983
25023
  SendFeedbackSchema = external_exports.object({
@@ -24987,11 +25027,13 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
24987
25027
  }).strict();
24988
25028
  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
25029
  AGENT_TOOLS = [
25030
+ { name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION, inputSchema: mcpInputSchema(ConfigureIdentitySchema) },
24990
25031
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
24991
25032
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
24992
- { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
25033
+ { name: "get_delivery", description: GET_DELIVERY_DESCRIPTION, inputSchema: mcpInputSchema(GetDeliverySchema) },
25034
+ { name: "get_goals", description: GET_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalsSchema) },
24993
25035
  { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
24994
- { 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) },
25036
+ { 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_goals to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckActivitySchema) },
24995
25037
  { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
24996
25038
  ];
24997
25039
  AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
@@ -25255,6 +25297,42 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25255
25297
  WORKING_MS = 60 * 6e4;
25256
25298
  WORKING_TIER_MS = 2 * 60 * 6e4;
25257
25299
  RECENT_TIER_MS = 24 * 60 * 6e4;
25300
+ json = external_exports.lazy(() => external_exports.union([
25301
+ external_exports.string(),
25302
+ external_exports.number().finite(),
25303
+ external_exports.boolean(),
25304
+ external_exports.null(),
25305
+ external_exports.array(json),
25306
+ external_exports.record(json)
25307
+ ]));
25308
+ clock = external_exports.number().int().nonnegative().safe();
25309
+ HeardEventSchema = external_exports.object({
25310
+ eventId: external_exports.string().uuid(),
25311
+ source: external_exports.enum(["phone", "bot"]),
25312
+ sequence: clock,
25313
+ kind: external_exports.string().min(1),
25314
+ phoneAtMs: clock.optional(),
25315
+ botAtMs: clock.optional(),
25316
+ payload: external_exports.record(json)
25317
+ }).strict();
25318
+ HeardCursorSchema = HeardEventSchema.pick({ source: true, sequence: true, eventId: true });
25319
+ HeardPageSchema = external_exports.object({
25320
+ events: external_exports.array(HeardEventSchema).max(200),
25321
+ next: HeardCursorSchema.nullable()
25322
+ }).strict();
25323
+ MAX_EVENTS = 50;
25324
+ MAX_BYTES = 8e4;
25325
+ batchBytes = (events) => {
25326
+ let bytes = 0;
25327
+ for (const char of JSON.stringify({ events })) {
25328
+ const point = char.codePointAt(0);
25329
+ bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
25330
+ }
25331
+ return bytes;
25332
+ };
25333
+ HeardBatchSchema = external_exports.object({
25334
+ events: external_exports.array(HeardEventSchema).min(1).max(MAX_EVENTS)
25335
+ }).strict().refine((value) => batchBytes(value.events) <= MAX_BYTES, "heard_batch_too_large");
25258
25336
  ContextSchema = external_exports.object({
25259
25337
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
25260
25338
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -25811,8 +25889,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
25811
25889
  "inbox",
25812
25890
  "dismiss"
25813
25891
  ]);
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.`;
25892
+ clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
25893
+ QUIET = ` Nothing rings from ${clock2(NIGHT.from)} to ${clock2(NIGHT.to)} your time; the count waits for morning.`;
25816
25894
  BrokerTuningSchema = external_exports.object({
25817
25895
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
25818
25896
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -26127,7 +26205,13 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26127
26205
  /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
26128
26206
  * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
26129
26207
  * none was. The Work row reads it before `said`. */
26130
- summary: external_exports.string().optional()
26208
+ summary: external_exports.string().optional(),
26209
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
26210
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
26211
+ titles: external_exports.array(external_exports.string()).optional(),
26212
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
26213
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
26214
+ line: external_exports.string().optional()
26131
26215
  }).nullable().optional(),
26132
26216
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
26133
26217
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -26457,6 +26541,8 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26457
26541
  status: external_exports.string(),
26458
26542
  startedAt: external_exports.string(),
26459
26543
  durationMs: external_exports.number().nullable(),
26544
+ /** Bot-clock pickup offset; absent before the timestamp release, null before its report lands. */
26545
+ pickupMs: external_exports.number().nullable().optional(),
26460
26546
  agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
26461
26547
  }),
26462
26548
  topics: external_exports.array(external_exports.object({
@@ -26501,7 +26587,9 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26501
26587
  words: external_exports.string(),
26502
26588
  atMs: external_exports.number()
26503
26589
  })),
26504
- more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
26590
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
26591
+ /** Last stored position on this page when another exists; absent before paging shipped. */
26592
+ next: external_exports.number().int().nonnegative().nullable().optional()
26505
26593
  });
26506
26594
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
26507
26595
  KEY_PATH = join22(homedir22(), ".paigy", "key.json");
@@ -26537,7 +26625,7 @@ RECEIVING: a contact that sends nothing returns \`events\`, a limited batch of w
26537
26625
  tokenOverride = null;
26538
26626
  authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
26539
26627
  AWAIT_WINDOW_MS = 45e3;
26540
- NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, mark the Goal done first, then send the one update saying what is done and anything they need to do or check. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
26628
+ NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, send contact.completions or manage_goals kind:complete with the shared completion report, closing and reporting together. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
26541
26629
  APPLIED = "Applied; nothing was sent to the person.";
26542
26630
  DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again.";
26543
26631
  HEARTBEAT_MS = 25e3;
@@ -31430,11 +31518,11 @@ function extractJson(raw) {
31430
31518
  return null;
31431
31519
  }
31432
31520
  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 };
31521
+ const json3 = extractJson(raw);
31522
+ if (!json3) return { ok: false, error: "no JSON found in the model's answer", raw };
31435
31523
  let parsed;
31436
31524
  try {
31437
- parsed = JSON.parse(json);
31525
+ parsed = JSON.parse(json3);
31438
31526
  } catch (e) {
31439
31527
  return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
31440
31528
  }
@@ -31680,13 +31768,17 @@ var init_triage = __esm({
31680
31768
  init_catalog();
31681
31769
 
31682
31770
  // src/cli-args.ts
31771
+ init_zod();
31683
31772
  var TRIAGE_VIA = ["ollama", "claude", "codex", "agy"];
31684
31773
  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
31774
  function parseArgs(argv) {
31686
31775
  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
31776
  const words2 = [];
31777
+ let commandPosition = true;
31688
31778
  for (let i = 0; i < argv.length; i++) {
31689
31779
  const arg = argv[i];
31780
+ const command = commandPosition && !!arg && !arg.startsWith("-");
31781
+ if (command) commandPosition = false;
31690
31782
  const next = () => argv[++i];
31691
31783
  if (arg === "--harness") {
31692
31784
  const v = next();
@@ -31702,7 +31794,7 @@ function parseArgs(argv) {
31702
31794
  args.cwd = v;
31703
31795
  } else if (arg === "--doctor") {
31704
31796
  args.doctor = true;
31705
- } else if (arg === "-h" || arg === "--help" || arg === "help") {
31797
+ } else if (arg === "-h" || arg === "--help" || command && arg === "help") {
31706
31798
  args.help = true;
31707
31799
  } else if (arg === "--scope") {
31708
31800
  const v = next();
@@ -31712,21 +31804,21 @@ function parseArgs(argv) {
31712
31804
  const v = Number(next());
31713
31805
  if (!Number.isFinite(v) || v < 0) return { error: "--grace needs seconds (0 = straight to phone)" };
31714
31806
  args.graceSeconds = v;
31715
- } else if (arg === "host") {
31807
+ } else if (command && arg === "host") {
31716
31808
  args.host = true;
31717
- } else if (arg === "pair") {
31809
+ } else if (command && arg === "pair") {
31718
31810
  args.pair = true;
31719
- } else if (arg === "setup") {
31811
+ } else if (command && arg === "setup") {
31720
31812
  args.setup = true;
31721
- } else if (arg === "service") {
31813
+ } else if (command && arg === "service") {
31722
31814
  args.service = true;
31723
31815
  } else if (arg === "--grant") {
31724
31816
  const v = next();
31725
31817
  if (!v) return { error: "--grant needs a folder to allow" };
31726
31818
  args.grant.push(v);
31727
- } else if (arg === "handoff") {
31819
+ } else if (command && arg === "handoff") {
31728
31820
  args.handoff = true;
31729
- } else if (arg === "triage") {
31821
+ } else if (command && arg === "triage") {
31730
31822
  args.triage = true;
31731
31823
  } else if (arg === "--via") {
31732
31824
  const v = next();
@@ -31743,9 +31835,9 @@ function parseArgs(argv) {
31743
31835
  args.yesStale = true;
31744
31836
  } else if (arg === "--yes-assign") {
31745
31837
  args.yesAssign = true;
31746
- } else if (arg === "enable-tools") {
31838
+ } else if (command && arg === "enable-tools") {
31747
31839
  args.enableTools = true;
31748
- } else if (arg === "hatch") {
31840
+ } else if (command && arg === "hatch") {
31749
31841
  const v = next();
31750
31842
  if (!v) return { error: 'hatch needs a name: paigy-harness hatch "Voice bug hunter"' };
31751
31843
  args.hatch = v;
@@ -31755,12 +31847,14 @@ function parseArgs(argv) {
31755
31847
  args.slot = next() ?? null;
31756
31848
  } else if (arg === "--identity") {
31757
31849
  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") {
31850
+ } else if (command && arg === "launch") {
31759
31851
  const v = next();
31760
31852
  if (!v) return { error: 'launch needs a name: paigy-harness launch "Deploy check" "brief"' };
31761
31853
  args.launch = v;
31762
31854
  } else if (arg === "--under") {
31763
- args.under = next() ?? null;
31855
+ const v = next();
31856
+ if (!v || !external_exports.string().uuid().safeParse(v).success) return { error: "--under needs a Goal UUID" };
31857
+ args.under = v;
31764
31858
  } else if (arg?.startsWith("--")) {
31765
31859
  return { error: `unknown flag ${arg}` };
31766
31860
  } else if (arg) {
@@ -31769,6 +31863,7 @@ function parseArgs(argv) {
31769
31863
  }
31770
31864
  args.prompt = words2.join(" ");
31771
31865
  if (args.help) return args;
31866
+ if (args.under && !args.launch) return { error: "--under requires launch NAME" };
31772
31867
  if (args.launch && !args.prompt) return { error: 'launch needs a brief: paigy-harness launch "Deploy check" "brief"' };
31773
31868
  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
31869
  if ((args.yesClose || args.yesStale || args.yesAssign) && !args.apply)
@@ -32192,8 +32287,7 @@ function startSession(opts) {
32192
32287
  if (!existsSync3(cwd)) {
32193
32288
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
32194
32289
  }
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 };
32290
+ const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
32197
32291
  if (codexConfig) {
32198
32292
  const key3 = "mcp_servers.paigy.env_vars";
32199
32293
  const configPath = join5(process.env.CODEX_HOME || join5(homedir4(), ".codex"), "config.toml");
@@ -32219,8 +32313,6 @@ function startSession(opts) {
32219
32313
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
32220
32314
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
32221
32315
  ...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
32316
  ...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
32225
32317
  ...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
32226
32318
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
@@ -32300,6 +32392,8 @@ function startSession(opts) {
32300
32392
  init_zod();
32301
32393
  init_zod();
32302
32394
  init_zod();
32395
+ init_zod();
32396
+ init_zod();
32303
32397
 
32304
32398
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
32305
32399
  var ignoreOverride2 = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
@@ -33599,6 +33693,7 @@ init_zod();
33599
33693
  init_zod();
33600
33694
  init_zod();
33601
33695
  init_zod();
33696
+ init_zod();
33602
33697
  var OPTIONS_MIN2 = 2;
33603
33698
  var OPTIONS_MAX2 = 6;
33604
33699
  var OPTION_LABEL_MAX2 = 50;
@@ -33628,35 +33723,6 @@ var OptionInputSchema2 = external_exports.object({
33628
33723
  imgUrl: OptionSchema2.shape.image
33629
33724
  }).strict();
33630
33725
  var NIGHT2 = { from: 23, to: 7 };
33631
- function draft20202(node) {
33632
- if (Array.isArray(node)) return node.map(draft20202);
33633
- if (node && typeof node === "object") {
33634
- const o = node;
33635
- if (Array.isArray(o.items)) {
33636
- o.prefixItems = o.items;
33637
- if ("additionalItems" in o) {
33638
- o.items = o.additionalItems;
33639
- delete o.additionalItems;
33640
- } else delete o.items;
33641
- }
33642
- for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
33643
- if (typeof o[excl] === "boolean") {
33644
- if (o[excl] === true && typeof o[lim] === "number") {
33645
- o[excl] = o[lim];
33646
- delete o[lim];
33647
- } else delete o[excl];
33648
- }
33649
- }
33650
- for (const k of Object.keys(o)) o[k] = draft20202(o[k]);
33651
- return o;
33652
- }
33653
- return node;
33654
- }
33655
- function mcpInputSchema2(s) {
33656
- const schema = zodToJsonSchema2(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
33657
- delete schema.$schema;
33658
- return draft20202(schema);
33659
- }
33660
33726
  var ASK_MAX2 = 250;
33661
33727
  var UNITS_MAX2 = 12;
33662
33728
  var UNIT_TITLE_MAX2 = 50;
@@ -33706,6 +33772,60 @@ function capRefusals2(text, units22, field) {
33706
33772
  function refuseOverCaps2(text, units22, field, ctx) {
33707
33773
  for (const { path, message, ...params } of capRefusals2(text, units22, field)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message, params });
33708
33774
  }
33775
+ var COMPLETION_MESSAGE_MAX2 = 50;
33776
+ var CompletionSchema2 = external_exports.object({
33777
+ goalId: external_exports.string().uuid().describe("The existing Goal to complete."),
33778
+ message: external_exports.string().trim().min(1).max(COMPLETION_MESSAGE_MAX2, "completion_message_too_long: at most 50 characters; put detail in units.").describe("Completion headline, at most 50 characters."),
33779
+ units: external_exports.array(UnitInputSchema2.extend({
33780
+ title: external_exports.string().trim().min(1).max(UNIT_TITLE_MAX2),
33781
+ body: external_exports.string().trim().min(1).max(UNIT_BODY_MAX2)
33782
+ })).max(UNITS_MAX2).optional().describe("Suggest initial state, changes, current state with verification and limits, and next steps / leftover work with its owner when useful. One idea per unit; omit unnecessary sections."),
33783
+ reply: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX2).describe("One acknowledgement in the person's voice, at most 50 characters. Acknowledgement alone requests no follow-up and does not claim they tested the work."),
33784
+ options: external_exports.array(external_exports.object({
33785
+ label: external_exports.string().trim().min(1).max(OPTION_LABEL_MAX2),
33786
+ hint: external_exports.string().trim().min(1).max(OPTION_HINT_MAX2)
33787
+ }).strict()).max(5).optional().describe("Optional next steps the person may initiate: label (50 characters) and consequence in hint (200). Selecting one acknowledges the report and requests that step from its author; it never reopens this Goal.")
33788
+ }).strict();
33789
+ var CompleteChangeSchema2 = CompletionSchema2.extend({ kind: external_exports.literal("complete") });
33790
+ var ASKING_RULES2 = `HOW TO ASK:
33791
+ 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.
33792
+ 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.
33793
+ 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.
33794
+ 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_goals\` on that wake.
33795
+ 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\`.
33796
+ 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.
33797
+ 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.
33798
+ 8. A diagnosis says when, why and how it happens, then proposes one fix. Never options first.
33799
+ 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.`;
33800
+ function draft20202(node) {
33801
+ if (Array.isArray(node)) return node.map(draft20202);
33802
+ if (node && typeof node === "object") {
33803
+ const o = node;
33804
+ if (Array.isArray(o.items)) {
33805
+ o.prefixItems = o.items;
33806
+ if ("additionalItems" in o) {
33807
+ o.items = o.additionalItems;
33808
+ delete o.additionalItems;
33809
+ } else delete o.items;
33810
+ }
33811
+ for (const [excl, lim] of [["exclusiveMinimum", "minimum"], ["exclusiveMaximum", "maximum"]]) {
33812
+ if (typeof o[excl] === "boolean") {
33813
+ if (o[excl] === true && typeof o[lim] === "number") {
33814
+ o[excl] = o[lim];
33815
+ delete o[lim];
33816
+ } else delete o[excl];
33817
+ }
33818
+ }
33819
+ for (const k of Object.keys(o)) o[k] = draft20202(o[k]);
33820
+ return o;
33821
+ }
33822
+ return node;
33823
+ }
33824
+ function mcpInputSchema2(s) {
33825
+ const schema = zodToJsonSchema2(s, { target: "jsonSchema2019-09", $refStrategy: "none" });
33826
+ delete schema.$schema;
33827
+ return draft20202(schema);
33828
+ }
33709
33829
  var questionHandle2 = external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
33710
33830
  var NodeRef2 = external_exports.object({
33711
33831
  type: external_exports.enum(["goal", "question"]),
@@ -33746,10 +33866,18 @@ var UpdateInputSchema2 = external_exports.object({
33746
33866
  // MECHANISM). It rides the door as the report's single stored option (`storedOption`,
33747
33867
  // `apps/api/src/goal/router.ts`), so no reader below the door learns a second shape.
33748
33868
  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.`
33869
+ `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
33870
  ).optional(),
33751
33871
  userExplicitlyRequested: userExplicitlyRequested2
33752
33872
  }).strict().superRefine((u, ctx) => {
33873
+ if (u.userExplicitlyRequested && !u.reply) {
33874
+ ctx.addIssue({
33875
+ code: external_exports.ZodIssueCode.custom,
33876
+ path: ["reply"],
33877
+ params: { refusal: "reply_required" },
33878
+ message: "reply_required: an update requested for delivery needs one reply, written in the person\u2019s voice for this report."
33879
+ });
33880
+ }
33753
33881
  refuseOverCaps2(u.message, u.units, "message", ctx);
33754
33882
  if (u.reply && u.reply.length > OPTION_LABEL_MAX2) {
33755
33883
  ctx.addIssue({
@@ -33808,6 +33936,7 @@ var QuestionDependencySchema2 = external_exports.object({
33808
33936
  reason: reason2.optional()
33809
33937
  }).strict().refine(actionFits2, ACTION_FITS2);
33810
33938
  var sendFields2 = {
33939
+ completions: external_exports.array(CompletionSchema2).min(1).max(20).optional().describe("Close existing Goals and deliver their completion reports atomically. Ordinary updates never close work."),
33811
33940
  updates: external_exports.array(UpdateInputSchema2).min(1).max(20).optional().describe(
33812
33941
  "News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress and nobody is notified."
33813
33942
  ),
@@ -33830,24 +33959,22 @@ var ContactSchema2 = external_exports.object({
33830
33959
  "Only controls when this returns. Sending: false (the default) returns at once; true waits up to about 45 seconds for a response to what you sent. Sending nothing: true (the default) waits up to about 45 seconds for anything addressed to you; false returns what is waiting now. It does not mean work is blocked, set urgency, or ask for a call. `waitOutcome` says which happened: `available`, `expired` (nothing arrived; wait again without sending again), or `not_waited`."
33831
33960
  )
33832
33961
  }).strict();
33833
- var sendsAnything2 = (c) => !!(c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
33962
+ var sendsAnything2 = (c) => !!(c.completions?.length || c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
33834
33963
  var ContactWireSchema2 = external_exports.object({
33835
33964
  operationId: external_exports.string().uuid(),
33836
33965
  ...sendFields2,
33837
33966
  questions: external_exports.array(QuestionWireSchema2).min(1).max(20).optional()
33838
33967
  }).strict().refine(sendsAnything2, "a contact sends at least one update, question, answer, withdrawal or question dependency");
33839
33968
  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.
33969
+ var CONTACT_DESCRIPTION2 = "Send to and receive from the person: completions, 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
33970
 
33844
33971
  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
33972
 
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".
33973
+ 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. Ordinary updates never close a Goal. 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.
33847
33974
 
33848
- 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.
33975
+ COMPLETIONS close and report in one operation: completions:[{goalId,message,units?,reply,options?}]. The same contract works in manage_goals changes with kind:complete. message is at most 50 characters; use units when useful for initial state, changes, current state (verification and limits), and next steps / leftover work with its owner. reply is one acknowledgement in the person's voice. Optional options:[{label,hint}] suggest next steps: a tap acknowledges the report and requests that step from its author. It never reopens the completed Goal; follow-up work needs a new Goal. Closure and the report commit together or neither does. Required children and dependencies must already be finished. No Reviewer rejudges your completion; say only what your evidence supports.
33849
33976
 
33850
- 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\`).`;
33977
+ 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_goals); \`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\`).`;
33851
33978
  var CreateGoalSchema2 = external_exports.object({
33852
33979
  outcome: external_exports.string().trim().min(1).max(1e4),
33853
33980
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -33920,6 +34047,7 @@ var DependencyChange2 = external_exports.object({
33920
34047
  var read2 = { revision: external_exports.number().int().positive().optional() };
33921
34048
  var GoalChangeSchema2 = external_exports.discriminatedUnion("kind", [
33922
34049
  CreateChange2,
34050
+ CompleteChangeSchema2.extend(read2),
33923
34051
  EditChange2.extend(read2),
33924
34052
  StateChange2.extend(read2),
33925
34053
  AssignChange2.extend(read2),
@@ -33937,20 +34065,20 @@ var ManageGoalsSchema2 = external_exports.object({
33937
34065
  changes: external_exports.array(GoalChangeSchema2).min(1).max(50)
33938
34066
  }).strict().superRefine((v, ctx) => editNames2(v.changes, ctx));
33939
34067
  var ManageGoalsToolSchema2 = external_exports.object({
33940
- changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange2, EditChange2, StateChange2, AssignChange2, DeferChange2, MoveChange2, DependencyChange2])).min(1).max(50).describe("The changes, applied in order, each on its own.")
34068
+ changes: external_exports.array(external_exports.discriminatedUnion("kind", [CreateChange2, CompleteChangeSchema2, EditChange2, StateChange2, AssignChange2, DeferChange2, MoveChange2, DependencyChange2])).min(1).max(50).describe("The changes, applied in order, each on its own.")
33941
34069
  }).strict().superRefine((v, ctx) => editNames2(v.changes, ctx));
33942
- var MANAGE_GOALS_DESCRIPTION2 = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
33943
- var GetGoalSchema2 = external_exports.object({
33944
- goalId: external_exports.string().uuid(),
34070
+ var MANAGE_GOALS_DESCRIPTION2 = "Create, edit, assign, organize or close work. Every change here changes Goals; answering, withdrawing or asking Questions stays in contact. Each change applies independently: a refused one names why (`results[i].error`, e.g. goal_revision_conflict, goal_children_open, goal_not_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: complete (goalId, message capped at 50 characters, units, reply, optional next-step options {label, hint}) closes and reports atomically, exactly as contact.completions; create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goals) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
34071
+ var GetGoalsSchema2 = external_exports.object({
34072
+ goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals to read, in order; one to ten IDs."),
33945
34073
  /** Every entry in full. Without it the read carries the person's words, open questions and your
33946
34074
  * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
33947
- history: external_exports.boolean().optional(),
33948
- /** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
33949
- * about this work, each answer attributed to the reader that gave it, and every disagreement
33950
- * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
33951
- diagnose: external_exports.boolean().optional()
34075
+ history: external_exports.boolean().optional()
34076
+ }).strict();
34077
+ var GetDeliverySchema2 = external_exports.object({
34078
+ deliveryId: external_exports.string().uuid().describe("The Delivery id returned by contact or receive.")
33952
34079
  }).strict();
33953
- 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.";
34080
+ 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.";
34081
+ var GET_GOALS_DESCRIPTION2 = "Read one to ten Goals by goalIds, returned in the same order under goals (a read never puts you on them). history applies to every requested Goal. A refused read fails the call with its original refusal; no partial result is presented as complete. Each Goal includes its ten most recent other contributors (names, latest entry headlines and times), 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.";
33954
34082
  var SearchToolSchema2 = external_exports.object({
33955
34083
  query: external_exports.string().trim().min(1).max(500),
33956
34084
  types: external_exports.array(external_exports.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -33958,6 +34086,8 @@ var SearchToolSchema2 = external_exports.object({
33958
34086
  limit: external_exports.number().int().min(1).max(20).optional()
33959
34087
  }).strict();
33960
34088
  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.";
34089
+ var ConfigureIdentitySchema2 = external_exports.object({}).strict();
34090
+ 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
34091
  var CheckActivitySchema2 = external_exports.object({}).strict();
33962
34092
  var FEEDBACK_TEXT_MAX2 = 5e4;
33963
34093
  var SendFeedbackSchema2 = external_exports.object({
@@ -33967,11 +34097,13 @@ var SendFeedbackSchema2 = external_exports.object({
33967
34097
  }).strict();
33968
34098
  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
34099
  var AGENT_TOOLS2 = [
34100
+ { name: "configure", description: CONFIGURE_IDENTITY_DESCRIPTION2, inputSchema: mcpInputSchema2(ConfigureIdentitySchema2) },
33970
34101
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
33971
34102
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(ManageGoalsToolSchema2) },
33972
- { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
34103
+ { name: "get_delivery", description: GET_DELIVERY_DESCRIPTION2, inputSchema: mcpInputSchema2(GetDeliverySchema2) },
34104
+ { name: "get_goals", description: GET_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalsSchema2) },
33973
34105
  { name: "search", description: SEARCH_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchToolSchema2) },
33974
- { 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) },
34106
+ { 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_goals to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema2(CheckActivitySchema2) },
33975
34107
  { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION2, inputSchema: mcpInputSchema2(SendFeedbackSchema2) }
33976
34108
  ];
33977
34109
  var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
@@ -34250,8 +34382,45 @@ function entryWords(entry) {
34250
34382
  }
34251
34383
  var LIVE_MS2 = 3 * 6e4;
34252
34384
  var WORKING_MS2 = 60 * 6e4;
34385
+ var isWorking = (c, now) => !!c.lastWorkedAt && now - Date.parse(c.lastWorkedAt) < WORKING_MS2;
34253
34386
  var WORKING_TIER_MS2 = 2 * 60 * 6e4;
34254
34387
  var RECENT_TIER_MS2 = 24 * 60 * 6e4;
34388
+ var json2 = external_exports.lazy(() => external_exports.union([
34389
+ external_exports.string(),
34390
+ external_exports.number().finite(),
34391
+ external_exports.boolean(),
34392
+ external_exports.null(),
34393
+ external_exports.array(json2),
34394
+ external_exports.record(json2)
34395
+ ]));
34396
+ var clock3 = external_exports.number().int().nonnegative().safe();
34397
+ var HeardEventSchema2 = external_exports.object({
34398
+ eventId: external_exports.string().uuid(),
34399
+ source: external_exports.enum(["phone", "bot"]),
34400
+ sequence: clock3,
34401
+ kind: external_exports.string().min(1),
34402
+ phoneAtMs: clock3.optional(),
34403
+ botAtMs: clock3.optional(),
34404
+ payload: external_exports.record(json2)
34405
+ }).strict();
34406
+ var HeardCursorSchema2 = HeardEventSchema2.pick({ source: true, sequence: true, eventId: true });
34407
+ var HeardPageSchema2 = external_exports.object({
34408
+ events: external_exports.array(HeardEventSchema2).max(200),
34409
+ next: HeardCursorSchema2.nullable()
34410
+ }).strict();
34411
+ var MAX_EVENTS2 = 50;
34412
+ var MAX_BYTES2 = 8e4;
34413
+ var batchBytes2 = (events) => {
34414
+ let bytes = 0;
34415
+ for (const char of JSON.stringify({ events })) {
34416
+ const point = char.codePointAt(0);
34417
+ bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
34418
+ }
34419
+ return bytes;
34420
+ };
34421
+ var HeardBatchSchema2 = external_exports.object({
34422
+ events: external_exports.array(HeardEventSchema2).min(1).max(MAX_EVENTS2)
34423
+ }).strict().refine((value) => batchBytes2(value.events) <= MAX_BYTES2, "heard_batch_too_large");
34255
34424
  var ContextSchema2 = external_exports.object({
34256
34425
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
34257
34426
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -34808,8 +34977,8 @@ var MissedCallSchema2 = external_exports.enum([
34808
34977
  "inbox",
34809
34978
  "dismiss"
34810
34979
  ]);
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.`;
34980
+ var clock22 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
34981
+ var QUIET2 = ` Nothing rings from ${clock22(NIGHT2.from)} to ${clock22(NIGHT2.to)} your time; the count waits for morning.`;
34813
34982
  var BrokerTuningSchema2 = external_exports.object({
34814
34983
  /** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
34815
34984
  ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
@@ -35124,7 +35293,13 @@ var QueueItemSchema2 = external_exports.object({
35124
35293
  /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
35125
35294
  * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
35126
35295
  * none was. The Work row reads it before `said`. */
35127
- summary: external_exports.string().optional()
35296
+ summary: external_exports.string().optional(),
35297
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
35298
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
35299
+ titles: external_exports.array(external_exports.string()).optional(),
35300
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
35301
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
35302
+ line: external_exports.string().optional()
35128
35303
  }).nullable().optional(),
35129
35304
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
35130
35305
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
@@ -35452,6 +35627,8 @@ var CallRecapSchema2 = external_exports.object({
35452
35627
  status: external_exports.string(),
35453
35628
  startedAt: external_exports.string(),
35454
35629
  durationMs: external_exports.number().nullable(),
35630
+ /** Bot-clock pickup offset; absent before the timestamp release, null before its report lands. */
35631
+ pickupMs: external_exports.number().nullable().optional(),
35455
35632
  agents: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable() }))
35456
35633
  }),
35457
35634
  topics: external_exports.array(external_exports.object({
@@ -35496,7 +35673,9 @@ var CallRecapSchema2 = external_exports.object({
35496
35673
  words: external_exports.string(),
35497
35674
  atMs: external_exports.number()
35498
35675
  })),
35499
- more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
35676
+ more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() }),
35677
+ /** Last stored position on this page when another exists; absent before paging shipped. */
35678
+ next: external_exports.number().int().nonnegative().nullable().optional()
35500
35679
  });
35501
35680
 
35502
35681
  // src/paigy/activity.ts
@@ -35570,9 +35749,9 @@ function limitIn(text, now) {
35570
35749
  if (!line) return null;
35571
35750
  const reset = resetOf(line, now);
35572
35751
  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) };
35752
+ return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock4(reset) };
35574
35753
  }
35575
- return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
35754
+ return { until: now + RETRY_MS2, resets: clock4(now + RETRY_MS2) };
35576
35755
  }
35577
35756
  function resetOf(line, now) {
35578
35757
  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 +35795,7 @@ function wallToEpoch(year, month, day, hour, minute, zone) {
35616
35795
  const first = wall - offset(wall);
35617
35796
  return wall - offset(first);
35618
35797
  }
35619
- function clock3(t) {
35798
+ function clock4(t) {
35620
35799
  return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
35621
35800
  }
35622
35801
  var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
@@ -35660,7 +35839,7 @@ function episodes(opts = {}) {
35660
35839
  episode.called = true;
35661
35840
  const rest = episode.paused.filter((n) => !episode.running.includes(n));
35662
35841
  const text = [
35663
- `Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
35842
+ `Claude's usage limit paused ${names(episode.paused)} from ${clock4(episode.since)}.`,
35664
35843
  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
35844
  ].join(" ");
35666
35845
  void call(text).then(
@@ -35679,9 +35858,9 @@ function railFor(opts) {
35679
35858
  next: async () => {
35680
35859
  const waiting = await receive(opts);
35681
35860
  const goalId = waiting.claimable?.goalId ?? waiting.review?.[0]?.goalId;
35682
- return goalId ? getGoal(goalId, opts) : { state: "idle", message: "" };
35861
+ return goalId ? getGoals([goalId], opts).then(([goal]) => goal) : { state: "idle", message: "" };
35683
35862
  },
35684
- read: (goalId) => getGoal(goalId, opts),
35863
+ read: (goalId) => getGoals([goalId], opts).then(([goal]) => goal),
35685
35864
  complete: async (goalId, revision) => {
35686
35865
  const done = await manageGoals({ changes: [{ kind: "state", goalId, state: "completed", ...revision !== void 0 ? { revision } : {} }] }, opts);
35687
35866
  const result = done.results[0];
@@ -36107,7 +36286,7 @@ async function look(session, state, rail, opts) {
36107
36286
  const outcome2 = brief2.outcome?.trim() ?? "";
36108
36287
  const said = (pendingWords ?? (brief2.entries ?? []).map((e) => entryWords(e).trim())).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
36109
36288
  const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
36110
- const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
36289
+ const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goals with history: true reads them in full.)` : "";
36111
36290
  const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
36112
36291
  send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
36113
36292
  if (ended(brief2.state)) await acknowledge2(brief2.goalId, brief2, [], state, rail, opts?.log);
@@ -36153,7 +36332,7 @@ function send2(session, state, text) {
36153
36332
  session.send(text);
36154
36333
  }
36155
36334
  async function pump(session, state, rail, opts) {
36156
- const clock4 = opts.beat ?? beat();
36335
+ const clock5 = opts.beat ?? beat();
36157
36336
  const now = opts.now ?? Date.now;
36158
36337
  const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
36159
36338
  const tally = { noted: 0, seen: 0, announced: 0 };
@@ -36185,7 +36364,7 @@ async function pump(session, state, rail, opts) {
36185
36364
  counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
36186
36365
  since = now();
36187
36366
  }
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);
36367
+ 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
36368
  if (!await opts.onIdle()) return;
36190
36369
  }
36191
36370
  }
@@ -36195,7 +36374,7 @@ function runHarness(opts) {
36195
36374
  let running = true;
36196
36375
  let tail = [];
36197
36376
  const state = { resting: true };
36198
- const clock4 = beat();
36377
+ const clock5 = beat();
36199
36378
  let session = null;
36200
36379
  const speaksAs = opts.session ?? opts.resume ?? sessionId();
36201
36380
  const instance = randomUUID4();
@@ -36256,7 +36435,7 @@ ${event.message}`, state, rail, { log: opts.log });
36256
36435
  stop2();
36257
36436
  return;
36258
36437
  } else if (carried) runningAgain();
36259
- clock4.nudge("rest");
36438
+ clock5.nudge("rest");
36260
36439
  }
36261
36440
  }
36262
36441
  session = startSession({
@@ -36275,13 +36454,13 @@ ${event.message}`, state, rail, { log: opts.log });
36275
36454
  },
36276
36455
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
36277
36456
  });
36278
- void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
36457
+ void pump(session, state, rail, { onIdle: () => running, beat: clock5, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
36279
36458
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
36280
36459
  );
36281
36460
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
36282
36461
  function stop2() {
36283
36462
  running = false;
36284
- clock4.nudge("wake");
36463
+ clock5.nudge("wake");
36285
36464
  tail = [];
36286
36465
  cancelAsks(state);
36287
36466
  session?.stop();
@@ -36296,7 +36475,7 @@ ${event.message}`, state, rail, { log: opts.log });
36296
36475
  working: () => running && state.resting !== true,
36297
36476
  // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
36298
36477
  tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
36299
- nudge: () => clock4.nudge("wake"),
36478
+ nudge: () => clock5.nudge("wake"),
36300
36479
  stop: stop2
36301
36480
  };
36302
36481
  }
@@ -36727,7 +36906,7 @@ var claimedRun = (sessionId2) => {
36727
36906
  const slot = sessionSlot(sessionId2);
36728
36907
  return { slot, key: runKey(slot) };
36729
36908
  };
36730
- var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
36909
+ var launchRun = (spec) => ({ session: spec.sessionId });
36731
36910
  var BOUND_ELSEWHERE_MS = 10 * 6e4;
36732
36911
  var boundElsewhere = /* @__PURE__ */ new Map();
36733
36912
  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 +36936,12 @@ function evict(runs, key3, run) {
36757
36936
  if (runs.get(key3)?.run === run) runs.delete(key3);
36758
36937
  }
36759
36938
  var PRESENCE_FRESH_MS = 3 * 6e4;
36760
- var IDLE_HOLDER_MS = 30 * 6e4;
36761
36939
  function wakeAction(live, now, serverSeenAt, here = {}) {
36762
36940
  if (live) return { action: "skip", why: "run" };
36763
36941
  if (here.holder) return { action: "skip", why: "holder" };
36764
36942
  if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
36765
36943
  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" };
36944
+ if (here.movedAt !== void 0 && now - here.movedAt >= WORKING_MS2 && !isWorking(here, now)) return { action: "spawn", why: "idle" };
36767
36945
  return { action: "skip", why: "presence" };
36768
36946
  }
36769
36947
  return { action: "spawn", why: "nobody" };
@@ -36836,7 +37014,7 @@ function startHost(opts) {
36836
37014
  });
36837
37015
  void heartbeat({
36838
37016
  version: VERSION,
36839
- harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
37017
+ harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
36840
37018
  workspaces: listWorkspaces(opts.wsDeps),
36841
37019
  // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
36842
37020
  repos: listRepos(opts.wsDeps)
@@ -36918,7 +37096,7 @@ function startHost(opts) {
36918
37096
  const presence = /* @__PURE__ */ new Map();
36919
37097
  try {
36920
37098
  for (const c of (await listConnections(asHost)).items) {
36921
- if (c.lastSeenAt) presence.set(c.id, Date.parse(c.lastSeenAt));
37099
+ presence.set(c.id, c);
36922
37100
  }
36923
37101
  lastPresence = presence;
36924
37102
  } catch {
@@ -36951,11 +37129,12 @@ function startHost(opts) {
36951
37129
  }
36952
37130
  }
36953
37131
  const tokenId = slotIdentity(slot).tokenId;
36954
- const seenByServer = tokenId ? presence.get(tokenId) : void 0;
37132
+ const connection = tokenId ? presence.get(tokenId) : void 0;
37133
+ const seenByServer = connection?.lastSeenAt ? Date.parse(connection.lastSeenAt) : void 0;
36955
37134
  const holder = holders.get(slot);
36956
37135
  if (holder) heldHere.add(slot);
36957
37136
  else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
36958
- const here = { holder, goneAt: goneAt.get(slot) };
37137
+ const here = { holder, goneAt: goneAt.get(slot), lastWorkedAt: connection?.lastWorkedAt };
36959
37138
  let decided = wakeAction(!!live, Date.now(), seenByServer, here);
36960
37139
  const label = slotName(slot) ?? slot;
36961
37140
  if (decided.action === "skip" && decided.why !== "presence") {
@@ -36998,7 +37177,7 @@ function startHost(opts) {
36998
37177
  const since = found.since;
36999
37178
  decided = wakeAction(false, Date.now(), seenByServer, { ...here, ...since ? { movedAt: Date.parse(since) } : {} });
37000
37179
  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"}`;
37180
+ 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
37181
  opts.log(`\u25CC ${label} ${said} \u2014 not spawning`);
37003
37182
  decide(slot, "stood_back", found, seenByServer, said);
37004
37183
  return;
@@ -37167,7 +37346,7 @@ function startHost(opts) {
37167
37346
  // sessions we started ourselves. The server already knows: `last_seen_at` on the
37168
37347
  // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
37169
37348
  // 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,
37349
+ running: live.has(name) || !!slot && lastHolders.has(slot) || !!id3.tokenId && Date.now() - Date.parse(lastPresence.get(id3.tokenId)?.lastSeenAt ?? "") < PRESENCE_FRESH_MS,
37171
37350
  // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
37172
37351
  // thing a tail can tell us. A process we did not spawn has no tail here to read.
37173
37352
  working: live.get(name)?.working() ?? false