@paigy/harness 0.3.35 → 0.3.36

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 +767 -381
  2. package/dist/main.js +693 -312
  3. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -901,14 +901,14 @@ var require_regex = __commonJS({
901
901
  var TEST_KANJI = new RegExp("^" + kanji + "$");
902
902
  var TEST_NUMERIC = new RegExp("^" + numeric + "$");
903
903
  var TEST_ALPHANUMERIC = new RegExp("^[A-Z0-9 $%*+\\-./:]+$");
904
- exports.testKanji = function testKanji(str3) {
905
- return TEST_KANJI.test(str3);
904
+ exports.testKanji = function testKanji(str5) {
905
+ return TEST_KANJI.test(str5);
906
906
  };
907
- exports.testNumeric = function testNumeric(str3) {
908
- return TEST_NUMERIC.test(str3);
907
+ exports.testNumeric = function testNumeric(str5) {
908
+ return TEST_NUMERIC.test(str5);
909
909
  };
910
- exports.testAlphanumeric = function testAlphanumeric(str3) {
911
- return TEST_ALPHANUMERIC.test(str3);
910
+ exports.testAlphanumeric = function testAlphanumeric(str5) {
911
+ return TEST_ALPHANUMERIC.test(str5);
912
912
  };
913
913
  }
914
914
  });
@@ -1410,13 +1410,13 @@ var require_segments = __commonJS({
1410
1410
  var Regex = require_regex();
1411
1411
  var Utils = require_utils();
1412
1412
  var dijkstra = require_dijkstra();
1413
- function getStringByteLength(str3) {
1414
- return unescape(encodeURIComponent(str3)).length;
1413
+ function getStringByteLength(str5) {
1414
+ return unescape(encodeURIComponent(str5)).length;
1415
1415
  }
1416
- function getSegments(regex, mode, str3) {
1416
+ function getSegments(regex, mode, str5) {
1417
1417
  const segments = [];
1418
1418
  let result;
1419
- while ((result = regex.exec(str3)) !== null) {
1419
+ while ((result = regex.exec(str5)) !== null) {
1420
1420
  segments.push({
1421
1421
  data: result[0],
1422
1422
  index: result.index,
@@ -4250,13 +4250,13 @@ var require_svg_tag = __commonJS({
4250
4250
  var Utils = require_utils2();
4251
4251
  function getColorAttrib(color, attrib) {
4252
4252
  const alpha = color.a / 255;
4253
- const str3 = attrib + '="' + color.hex + '"';
4254
- return alpha < 1 ? str3 + " " + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"' : str3;
4253
+ const str5 = attrib + '="' + color.hex + '"';
4254
+ return alpha < 1 ? str5 + " " + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"' : str5;
4255
4255
  }
4256
4256
  function svgCmd(cmd, x, y) {
4257
- let str3 = cmd + x;
4258
- if (typeof y !== "undefined") str3 += " " + y;
4259
- return str3;
4257
+ let str5 = cmd + x;
4258
+ if (typeof y !== "undefined") str5 += " " + y;
4259
+ return str5;
4260
4260
  }
4261
4261
  function qrToPath(data, size, margin) {
4262
4262
  let path = "";
@@ -4589,7 +4589,7 @@ import { createHash, randomUUID } from "crypto";
4589
4589
  import { readFileSync } from "fs";
4590
4590
  import { homedir } from "os";
4591
4591
  import { join } from "path";
4592
- import { randomUUID as randomUUID3 } from "crypto";
4592
+ import { randomUUID as randomUUID2 } from "crypto";
4593
4593
  import { setTimeout as sleep2 } from "timers/promises";
4594
4594
 
4595
4595
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
@@ -8634,11 +8634,9 @@ var coerce = {
8634
8634
  var NEVER = INVALID;
8635
8635
 
8636
8636
  // ../../packages/sdk/dist/index.js
8637
- import { randomUUID as randomUUID2 } from "crypto";
8638
8637
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
8639
8638
  import { homedir as homedir2 } from "os";
8640
8639
  import { join as join2 } from "path";
8641
- import { execFileSync as execFileSync2 } from "child_process";
8642
8640
  var import_undici = __toESM(require_undici(), 1);
8643
8641
  var require2 = __sdkCreateRequire(import.meta.url);
8644
8642
  var CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "CODEX_HOME", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID", "PAIGY_ON_WAKE"];
@@ -9932,14 +9930,20 @@ var OptionSchema = external_exports.object({
9932
9930
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
9933
9931
  // the constraints below are what an agent reads when deciding to use these.
9934
9932
  html: external_exports.string().max(16384).describe(
9935
- "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
9933
+ "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use the image URL instead."
9936
9934
  ).optional(),
9937
9935
  image: external_exports.string().url().describe(
9938
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
9936
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use the HTML preview instead."
9939
9937
  ).optional()
9940
9938
  });
9941
- var OptionInputSchema = OptionSchema.omit({ id: true }).extend({
9942
- label: external_exports.string().trim().min(1).max(1e3)
9939
+ var OptionInputSchema = external_exports.object({
9940
+ id: external_exports.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
9941
+ `Optional: this option's own id, which answers name it by (selectedOptionIds). Give every option one, or none: without them the options are numbered "1", "2", \u2026 in order.`
9942
+ ),
9943
+ label: external_exports.string().trim().min(1).max(1e3),
9944
+ hint: OptionSchema.shape.hint,
9945
+ htmlPreview: OptionSchema.shape.html,
9946
+ imgUrl: OptionSchema.shape.image
9943
9947
  }).strict();
9944
9948
  var NIGHT = { from: 23, to: 7 };
9945
9949
  function draft2020(node) {
@@ -9971,41 +9975,162 @@ function mcpInputSchema(s) {
9971
9975
  delete schema.$schema;
9972
9976
  return draft2020(schema);
9973
9977
  }
9974
- var AskInputSchema = external_exports.object({
9975
- id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
9976
- parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and the question starts a new Goal of yours."),
9977
- repo: external_exports.string().optional().describe("Optional repository context."),
9978
- ask: external_exports.string().trim().min(1).max(1e4).describe(
9979
- "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy sends each ask exactly as you wrote it: a bundled ask arrives as one card. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
9978
+ var ASK_MAX = 250;
9979
+ var UNITS_MAX = 12;
9980
+ var UNIT_TITLE_MAX = 50;
9981
+ var UNIT_BODY_MAX = 200;
9982
+ var UnitInputSchema = external_exports.object({
9983
+ 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.`),
9984
+ body: external_exports.string().trim().min(1).describe(`The unit itself, at most ${UNIT_BODY_MAX} characters.`)
9985
+ }).strict();
9986
+ function capRefusals(text, units22, field) {
9987
+ const out = [];
9988
+ const name = field === "text" ? "answer" : field;
9989
+ if (text.length > ASK_MAX) out.push({
9990
+ refusal: "ask_too_long",
9991
+ length: text.length,
9992
+ cap: ASK_MAX,
9993
+ path: field === "text" ? ["answer", "text"] : [field],
9994
+ message: `ask_too_long: the ${name} is ${text.length} characters; the cap is ${ASK_MAX}. Keep the one point in the ${name} and move the context into units.`
9995
+ });
9996
+ const all = units22 ?? [];
9997
+ if (all.length > UNITS_MAX) out.push({
9998
+ refusal: "too_many_units",
9999
+ length: all.length,
10000
+ cap: UNITS_MAX,
10001
+ path: ["units"],
10002
+ message: `too_many_units: ${all.length} units; the cap is ${UNITS_MAX}. Keep only the context this ${name} needs.`
10003
+ });
10004
+ all.forEach((unit, index) => {
10005
+ if (unit.title.length > UNIT_TITLE_MAX) out.push({
10006
+ refusal: "unit_title_too_long",
10007
+ index,
10008
+ length: unit.title.length,
10009
+ cap: UNIT_TITLE_MAX,
10010
+ path: ["units", index, "title"],
10011
+ message: `unit_title_too_long: units[${index}]'s title is ${unit.title.length} characters; the cap is ${UNIT_TITLE_MAX}.`
10012
+ });
10013
+ if (unit.body.length > UNIT_BODY_MAX) out.push({
10014
+ refusal: "unit_body_too_long",
10015
+ index,
10016
+ length: unit.body.length,
10017
+ cap: UNIT_BODY_MAX,
10018
+ path: ["units", index, "body"],
10019
+ message: `unit_body_too_long: units[${index}]'s body is ${unit.body.length} characters; the cap is ${UNIT_BODY_MAX}. Cut it into two units.`
10020
+ });
10021
+ });
10022
+ return out;
10023
+ }
10024
+ function refuseOverCaps(text, units22, field, ctx) {
10025
+ for (const { path, message, ...params } of capRefusals(text, units22, field)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message, params });
10026
+ }
10027
+ var questionHandle = external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
10028
+ var NodeRef = external_exports.object({
10029
+ type: external_exports.enum(["goal", "question"]),
10030
+ id: external_exports.string().uuid()
10031
+ }).strict().describe("A Goal or a Question, by its id.");
10032
+ var BlockedAction = external_exports.enum(["start", "complete", "answer"]).describe("What waits: starting or completing a Goal, or answering a Question.");
10033
+ var reason = external_exports.string().trim().min(1).max(500).describe("Why, as an explanation, never as policy.");
10034
+ var actionFits = (b) => b.blocked.type === "goal" === (b.action !== "answer");
10035
+ var ACTION_FITS = "a goal is blocked from start or complete; a question from answer";
10036
+ var userExplicitlyRequested = external_exports.enum(["any", "call"]).optional().describe(
10037
+ `Only what the person said about THIS message, in so many words: "any" when they asked you to send it to them ("send it to my phone", "message me the result"), "call" when they asked to be called about it. It is then delivered (a call rings for the whole contact, and carries everything waiting on them); omitted, an update is only recorded as the Goal's progress unless the person needs it. Never set it to make a message louder.`
10038
+ );
10039
+ var units = external_exports.array(UnitInputSchema).optional().describe(
10040
+ `The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit. Leave it out when the main text says it all.`
10041
+ );
10042
+ var UpdateInputSchema = external_exports.object({
10043
+ message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
10044
+ goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
10045
+ units,
10046
+ userExplicitlyRequested
10047
+ }).strict().superRefine((u, ctx) => refuseOverCaps(u.message, u.units, "message", ctx));
10048
+ var questionFields = {
10049
+ question: external_exports.string().trim().min(1).describe(
10050
+ `ONE question, in at most ${ASK_MAX} characters, and only what is needed to answer it; the context goes in units. Several questions are several objects, one each: an answer settles the one question it was given, so a person who answers the part of a bundled question that interested them settles nothing and is asked again. Separate questions cost nothing: ANY contact for a person already on a call JOINS that call, so several arrive as one call.`
9980
10051
  ),
9981
- options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
10052
+ goalId: external_exports.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
10053
+ units,
10054
+ options: external_exports.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
9982
10055
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
9983
- // brain_prompts.md §1: structured agent questions need no model). Every card was pick-one while no
9984
- // agent sent this; no read reshapes cards any more, so this is the card's shape, and the
9985
- // description says so plainly.
9986
- select: external_exports.enum(["one", "many"]).optional().describe(
9987
- 'How the options are answered: "many" makes a checklist (they may want several, e.g. independent fixes), "one" (the default) a pick (choosing one rules out the others). Send "many" whenever more than one option could be wanted at once. The person can always answer in their own words as well. Ignored without options.'
10056
+ // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
10057
+ pickMode: external_exports.enum(["one", "many", "rank"]).optional().describe(
10058
+ 'How the options are answered: "one" (the default) a pick, for alternatives; "many" a checklist, for independent options they may want several of; "rank" an order of all of them. Only with options.'
9988
10059
  ),
9989
- answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
9990
- "Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. parentId is optional here: the question's own Goal is used. The reply is sent to them as it is; to also ask something new, send that as its own ask."
9991
- )
9992
- }).strict();
9993
- var StartContactSchema = external_exports.object({
9994
- asks: external_exports.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; an ask naming no Goal starts a new Goal of yours."),
9995
- waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
9996
- channel: external_exports.enum(["notification", "call"]).default("notification")
9997
- }).strict();
9998
- var ReceiveContactSchema = external_exports.object({
9999
- wait: external_exports.boolean().optional().describe(
10000
- "Only controls when this returns. true (the default when you send and acknowledge nothing): wait up to about 45 seconds for incoming communication addressed to you, and return as soon as some is available. false: return 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 in the window; wait again without sending again), or `not_waited` (a hosted connection never holds a wait)."
10060
+ // WHAT THE QUESTION HOLDS UP (brain_prompts.md §2.1): replaces `waiting: 'hard'`. A question that
10061
+ // blocks work asks for a call (the person's settings and ring cooldowns still decide).
10062
+ workItBlocks: external_exports.array(external_exports.object({ blocked: NodeRef, action: BlockedAction, reason: reason.optional() }).strict().refine(actionFits, ACTION_FITS)).min(1).max(10).optional().describe(
10063
+ "The work that cannot go on until this is answered: a Goal's start or completion, or another Question's answer. Only for work truly blocked; it asks for a call. Leave it out for a question you can keep working around."
10064
+ ),
10065
+ userExplicitlyRequested
10066
+ };
10067
+ var refineQuestion = (q, ctx) => {
10068
+ refuseOverCaps(q.question, q.units, "question", ctx);
10069
+ if (q.pickMode && !q.options) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["pickMode"], message: "pickMode comes only with options" });
10070
+ const ids = (q.options ?? []).map((o) => o.id);
10071
+ if (ids.some((id22) => id22) && (ids.some((id22) => !id22) || new Set(ids).size !== ids.length)) {
10072
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: "give every option a distinct id, or none" });
10073
+ }
10074
+ };
10075
+ var QuestionInputSchema = external_exports.object({
10076
+ questionId: external_exports.string().uuid().optional().describe("Optional: your own stable id for this question (a UUID), so a resend is the same question. Minted for you when omitted; the result returns it."),
10077
+ ...questionFields
10078
+ }).strict().superRefine(refineQuestion);
10079
+ var QuestionWireSchema = external_exports.object({ questionId: external_exports.string().uuid(), ...questionFields }).strict().superRefine(refineQuestion);
10080
+ var AnswerInputSchema = external_exports.object({
10081
+ questionId: questionHandle.describe("The Question you answer: its id as the event or the conversation shows it (8 characters or whole)."),
10082
+ answer: external_exports.object({
10083
+ text: external_exports.string().trim().min(1).describe(`Your answer in your own words, at most ${ASK_MAX} characters; the context goes in units.`),
10084
+ selectedOptionIds: external_exports.array(external_exports.string().trim().min(1)).min(1).max(6).optional().describe("The ids of the options you choose, when the Question offered options (in order, for a ranking).")
10085
+ }).strict(),
10086
+ units
10087
+ }).strict().superRefine((a, ctx) => refuseOverCaps(a.answer.text, a.units, "text", ctx));
10088
+ var QuestionDependencySchema = external_exports.object({
10089
+ questionId: questionHandle.describe("A Question you asked."),
10090
+ change: external_exports.enum(["add", "remove"]),
10091
+ blocked: NodeRef,
10092
+ action: BlockedAction,
10093
+ reason: reason.optional()
10094
+ }).strict().refine(actionFits, ACTION_FITS);
10095
+ var sendFields = {
10096
+ updates: external_exports.array(UpdateInputSchema).min(1).max(20).optional().describe(
10097
+ "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."
10098
+ ),
10099
+ answers: external_exports.array(AnswerInputSchema).min(1).max(20).optional().describe("Answers to Questions addressed to you."),
10100
+ withdrawQuestionIds: external_exports.array(questionHandle).min(1).max(10).optional().describe(
10101
+ "Questions you asked that no longer need an answer: you acted on them yourself, they no longer matter, or you asked wrongly. Each is cancelled, not answered: its card closes and nothing rings for it."
10001
10102
  ),
10103
+ questionDependencies: external_exports.array(QuestionDependencySchema).min(1).max(20).optional().describe(
10104
+ "Add or remove what a Question you asked holds up, after asking it."
10105
+ )
10106
+ };
10107
+ var ContactSchema = external_exports.object({
10108
+ ...sendFields,
10109
+ questions: external_exports.array(QuestionInputSchema).min(1).max(20).optional().describe("Questions for the person, one per object."),
10110
+ /** RECEIVING (2I, I2; brain_prompts.md §2.2): the events this agent handled. */
10002
10111
  ackEventIds: external_exports.array(external_exports.string().uuid()).min(1).max(100).optional().describe(
10003
- "The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (an ask with `answers`), never acknowledged away: that id is refused until it is answered."
10112
+ "The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (answers), never acknowledged away."
10113
+ ),
10114
+ wait: external_exports.boolean().optional().describe(
10115
+ "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`."
10004
10116
  )
10005
10117
  }).strict();
10006
- var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict(), ReceiveContactSchema]);
10118
+ var sendsAnything = (c) => !!(c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
10119
+ var ContactWireSchema = external_exports.object({
10120
+ operationId: external_exports.string().uuid(),
10121
+ ...sendFields,
10122
+ questions: external_exports.array(QuestionWireSchema).min(1).max(20).optional()
10123
+ }).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
10007
10124
  var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
10008
- var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none starts a new Goal of yours. Notification returns immediately; collect answers by receiving (below). On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. Each ask is sent exactly as written, with no reading in between: a bundled ask arrives as one card, so separate questions are separate asks. An ask with no options that is not blocking is a report, never a claim on them, so no answer is owed and none should be awaited. A report reaches the person only when it answers something they asked you, or once the Goal is done; any other report is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first, then send one message saying what is done and anything they need to do or check. On a Goal whose report card is still open, a report that does reach them is added to that card, with no new push. Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). select:'many' makes the card a checklist, for independent options they may want several of; select:'one' (the default) a pick, for alternatives. The person can always answer in their own words, so never add an 'Other' option. If the person is already on a call, a contact that reaches them joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.\n\nRECEIVING: contact with no asks sends nothing and returns `events`, a limited batch of what is addressed to you (not a history page): `question`, a Question you owe (answer it with an ask whose `answers` is 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`; claim_goal starts it) and your work gone quiet (`stalled`).";
10125
+ var 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.
10126
+
10127
+ 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.
10128
+
10129
+ 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.
10130
+
10131
+ 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.
10132
+
10133
+ 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\`).`;
10009
10134
  var CreateGoalSchema = external_exports.object({
10010
10135
  outcome: external_exports.string().trim().min(1).max(1e4),
10011
10136
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -10039,21 +10164,11 @@ var UpdateGoalSchema = external_exports.object({
10039
10164
  state: external_exports.enum(["active", "done", "cancelled"]).optional(),
10040
10165
  progress: external_exports.string().trim().min(1).max(1e4).optional(),
10041
10166
  reviewed: external_exports.literal(true).optional(),
10042
- dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
10043
- /** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
10044
- * (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
10045
- * by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
10046
- withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
10167
+ dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
10047
10168
  }).strict().refine((v) => Object.keys(v).length > 0),
10048
10169
  reason: external_exports.string().trim().min(1).max(2e3),
10049
10170
  operationId: external_exports.string().uuid().optional()
10050
10171
  }).strict();
10051
- var UpdateGoalToolSchema = external_exports.object({
10052
- goalId: external_exports.string().uuid(),
10053
- revision: UpdateGoalSchema.shape.revision,
10054
- changes: UpdateGoalSchema.shape.changes.innerType().pick({ progress: true, reviewed: true, dueAt: true, withdraw: true }).strict().refine((v) => Object.keys(v).length > 0),
10055
- reason: UpdateGoalSchema.shape.reason
10056
- }).strict();
10057
10172
  var changedGoal = external_exports.string().uuid().describe("The Goal to change, as a read shows it.");
10058
10173
  var goalTitle = external_exports.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
10059
10174
  var goalOutcome = external_exports.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
@@ -10061,7 +10176,7 @@ var CreateChange = external_exports.object({
10061
10176
  kind: external_exports.literal("create"),
10062
10177
  title: goalTitle,
10063
10178
  outcome: goalOutcome,
10064
- ownerId: external_exports.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as who_is_working shows it) or the person's. Omitted: you."),
10179
+ ownerId: external_exports.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as check_activity shows it) or the person's. Omitted: you."),
10065
10180
  parentGoalId: external_exports.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
10066
10181
  sourceEntryIds: external_exports.array(external_exports.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
10067
10182
  }).strict();
@@ -10071,7 +10186,7 @@ var StateChange = external_exports.object({ kind: external_exports.literal("stat
10071
10186
  ) }).strict();
10072
10187
  var AssignChange = external_exports.object({ kind: external_exports.literal("assign"), goalId: changedGoal, ownerId: external_exports.string().trim().min(1).describe("The new owner's participant.") }).strict();
10073
10188
  var DeferChange = external_exports.object({ kind: external_exports.literal("defer"), goalId: changedGoal, until: external_exports.string().datetime({ offset: true }).nullable().describe(
10074
- "Postpone it until this instant (its owner is woken then), or null to take the postponement back."
10189
+ "Postpone it until this instant: it waits until then, and its owner is woken when it passes (a promise to follow up later). null takes the postponement back."
10075
10190
  ) }).strict();
10076
10191
  var MoveChange = external_exports.object({ kind: external_exports.literal("move"), goalId: changedGoal, parentGoalId: external_exports.string().uuid().nullable().describe(
10077
10192
  "Its new parent, or null for a root. Only this Goal moves; its new parent's other children stay."
@@ -10106,8 +10221,7 @@ var ManageGoalsSchema = external_exports.object({
10106
10221
  var ManageGoalsToolSchema = external_exports.object({
10107
10222
  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.")
10108
10223
  }).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
10109
- var 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_joined, 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; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until, or null); 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 a Goal you own or have written on. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
10110
- var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
10224
+ var 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; 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.";
10111
10225
  var GetGoalSchema = external_exports.object({
10112
10226
  goalId: external_exports.string().uuid(),
10113
10227
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -10118,9 +10232,7 @@ var GetGoalSchema = external_exports.object({
10118
10232
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
10119
10233
  diagnose: external_exports.boolean().optional()
10120
10234
  }).strict();
10121
- var GET_GOAL_DESCRIPTION = "Read one Goal without claiming 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.";
10122
- var UPDATE_GOAL_DESCRIPTION = "Report work on a Goal you are on, at an exact revision: progress, review acknowledgement, when to wake for it, and withdrawing a question of yours. What the Goal is (its title, outcome, state, owner, parent, dependencies) changes with manage_goals. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). Stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. progress says where the work stands. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision (contact({ackEventIds}) does the same for an `update` event). dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. Returns the Goal as get_goal reads it, at its new revision.";
10123
- var CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change it; use manage_goals (assign) when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
10235
+ var 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.";
10124
10236
  var SearchToolSchema = external_exports.object({
10125
10237
  query: external_exports.string().trim().min(1).max(500),
10126
10238
  types: external_exports.array(external_exports.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -10128,15 +10240,21 @@ var SearchToolSchema = external_exports.object({
10128
10240
  limit: external_exports.number().int().min(1).max(20).optional()
10129
10241
  }).strict();
10130
10242
  var 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 Answers (found by their Question 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, its Questions' Answers, 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 (an Answer carries its Question, 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.";
10131
- var WhoIsWorkingSchema = external_exports.object({}).strict();
10243
+ var CheckActivitySchema = external_exports.object({}).strict();
10244
+ var FEEDBACK_TEXT_MAX = 5e4;
10245
+ var SendFeedbackSchema = external_exports.object({
10246
+ text: external_exports.string().trim().min(1).max(FEEDBACK_TEXT_MAX).describe(`The report, in markdown: what happened, what was expected, how to reproduce it, versions, evidence. At most ${FEEDBACK_TEXT_MAX} characters.`),
10247
+ title: external_exports.string().trim().min(1).max(200).optional().describe("One line naming it."),
10248
+ kind: external_exports.enum(["bug", "idea", "other"]).default("other").describe("bug: something Paigy does wrong. idea: something it should do. other: anything else.")
10249
+ }).strict();
10250
+ var 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.";
10132
10251
  var AGENT_TOOLS = [
10133
- { name: "who_is_working", 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(WhoIsWorkingSchema) },
10134
10252
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
10135
10253
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
10136
- { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
10137
10254
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
10138
- { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) },
10139
- { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) }
10255
+ { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
10256
+ { 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) },
10257
+ { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
10140
10258
  ];
10141
10259
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
10142
10260
  var id = external_exports.string().uuid();
@@ -10275,13 +10393,83 @@ var CompactResultSchema = external_exports.object({
10275
10393
  var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10276
10394
  zodToJsonSchema(CompactResultSchema, { $refStrategy: "none" })
10277
10395
  );
10396
+ var str2 = external_exports.string();
10397
+ var strs2 = external_exports.array(external_exports.string());
10398
+ var TalkerReplySchema = external_exports.object({
10399
+ /** What to say and send, messages to the person first: spoken in the order written. `to` is "user"
10400
+ * or an agent's handle; `about` names handles; `blocks` is the item a question to an agent must be
10401
+ * answered before ("" for none). */
10402
+ messages: external_exports.array(external_exports.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
10403
+ /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
10404
+ then: external_exports.enum(["listen", "hold", "end"]),
10405
+ /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
10406
+ answers: external_exports.array(external_exports.object({ item: str2, options: strs2, lines: strs2 }).strict()),
10407
+ /** The line handles that hold a new instruction from the person. */
10408
+ instruction: strs2,
10409
+ /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
10410
+ * reads only these (3e). */
10411
+ file: strs2,
10412
+ /** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
10413
+ need: external_exports.object({ kind: external_exports.enum(["search", "item", "agent", ""]), text: str2 }).strict(),
10414
+ /** The handle of the item to explain ("" for none). */
10415
+ explain: str2
10416
+ }).strict();
10417
+ var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10418
+ zodToJsonSchema(TalkerReplySchema, { $refStrategy: "none" })
10419
+ );
10420
+ var str3 = external_exports.string();
10421
+ var strs3 = external_exports.array(external_exports.string());
10422
+ var FilerReplySchema = external_exports.object({
10423
+ /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
10424
+ filed: external_exports.array(external_exports.object({ line: str3, goals: strs3 }).strict()),
10425
+ goals: external_exports.array(external_exports.object({
10426
+ op: external_exports.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
10427
+ key: str3,
10428
+ goal: str3,
10429
+ title: str3,
10430
+ outcome: str3,
10431
+ owner: str3,
10432
+ under: str3,
10433
+ until: str3,
10434
+ on: str3,
10435
+ gate: external_exports.enum(["start", "complete", "answer", ""]),
10436
+ why: str3
10437
+ }).strict()),
10438
+ questions: external_exports.array(external_exports.object({
10439
+ op: external_exports.enum(["create", "edit", "assign", "withdraw"]),
10440
+ key: str3,
10441
+ question: str3,
10442
+ text: str3,
10443
+ to: str3,
10444
+ goal: str3,
10445
+ options: external_exports.array(external_exports.object({ id: str3, label: str3 }).strict()),
10446
+ pick: external_exports.enum(["one", "many", "rank", "words", ""]),
10447
+ blocks: str3
10448
+ }).strict()),
10449
+ lessons: external_exports.array(external_exports.object({ op: external_exports.enum(["remember", "revise", "withdraw"]), lesson: str3, text: str3, scope: str3 }).strict()),
10450
+ /** Line handles whose words comment on Paigy itself. */
10451
+ feedback: strs3
10452
+ }).strict();
10453
+ var FILER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
10454
+ zodToJsonSchema(FilerReplySchema, { $refStrategy: "none" })
10455
+ );
10456
+ function entryUnit(entry) {
10457
+ if (entry.kind !== "contribution" || !entry.aboutId) return null;
10458
+ const plain = entry.content && "plain" in entry.content ? entry.content.plain : null;
10459
+ if (!plain || typeof plain !== "object") return null;
10460
+ const { title, text } = plain;
10461
+ return typeof title === "string" && typeof text === "string" ? { title, body: text } : null;
10462
+ }
10278
10463
  var LIVE_MS = 3 * 6e4;
10279
10464
  var WORKING_MS = 60 * 6e4;
10280
10465
  var ContextSchema = external_exports.object({
10281
10466
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
10282
10467
  description: external_exports.array(external_exports.string().min(1)).describe(
10283
10468
  "Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
10284
- )
10469
+ ),
10470
+ /** THE ASK'S UNITS (3c): its context as titled units, in order. The app lists the titles and opens
10471
+ * a body on a tap. Absent on an ask sent without units, which renders as before. */
10472
+ units: external_exports.array(external_exports.object({ title: external_exports.string(), body: external_exports.string() })).optional()
10285
10473
  });
10286
10474
  var ParticipantSchema = external_exports.object({
10287
10475
  kind: external_exports.enum(["human", "agent"]),
@@ -10356,7 +10544,7 @@ var NotifyRequestFields = external_exports.object({
10356
10544
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
10357
10545
  * superRefine at the bottom enforces exactly one of the two. */
10358
10546
  context: ContextSchema.optional(),
10359
- options: external_exports.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
10547
+ options: external_exports.array(OptionSchema.omit({ id: true }).extend({ label: external_exports.string().trim().min(1).max(1e3) }).strict()).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
10360
10548
  "The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
10361
10549
  ),
10362
10550
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -11117,8 +11305,8 @@ var QueueItemSchema = external_exports.object({
11117
11305
  repo: external_exports.string().nullable().optional(),
11118
11306
  createdAt: external_exports.string(),
11119
11307
  updatedAt: external_exports.string().nullable().default(null),
11120
- /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
11121
- * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
11308
+ /** When its owner last SAID something about it (the newest `progress` Entry: a contact update kept
11309
+ * as progress). `updatedAt` moves for reasons nobody chose — a
11122
11310
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
11123
11311
  lastProgressAt: external_exports.string().nullable().optional(),
11124
11312
  /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
@@ -11276,7 +11464,7 @@ var HostDecisionSchema = external_exports.object({
11276
11464
  /** The agent's token id: the row's `recipient`. */
11277
11465
  agent: external_exports.string().uuid(),
11278
11466
  decision: external_exports.enum(["stood_back", "took_over"]),
11279
- /** The work it was about: the Goal `claim_goal` would hand that agent next. */
11467
+ /** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
11280
11468
  goalId: external_exports.string().uuid().nullable().optional(),
11281
11469
  /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
11282
11470
  seenAt: external_exports.string().datetime().nullable().optional(),
@@ -11633,12 +11821,6 @@ async function manageGoals(input, opts = {}) {
11633
11821
  if (!res.ok) await fail("manage_goals", res);
11634
11822
  return await res.json();
11635
11823
  }
11636
- async function claimGoal(goalId, opts = {}) {
11637
- const token = authToken(opts.token) ?? "";
11638
- const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify(goalId ? { goalId } : {}) }));
11639
- if (!res.ok) await fail("claim_goal", res);
11640
- return await res.json();
11641
- }
11642
11824
  async function getGoal(goalId, opts = {}, read3 = {}) {
11643
11825
  const token = authToken(opts.token) ?? "";
11644
11826
  const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
@@ -11649,13 +11831,6 @@ async function getGoal(goalId, opts = {}, read3 = {}) {
11649
11831
  if (!res.ok) await fail("get_goal", res);
11650
11832
  return await res.json();
11651
11833
  }
11652
- async function updateGoal(goalId, input, opts = {}) {
11653
- const token = authToken(opts.token) ?? "";
11654
- const operationId = input.operationId ?? randomUUID2();
11655
- const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify({ ...input, operationId }) }));
11656
- if (!res.ok) await fail("update_goal", res);
11657
- return await res.json();
11658
- }
11659
11834
  var AWAIT_WINDOW_MS = 45e3;
11660
11835
  async function whoAmI(opts = {}) {
11661
11836
  try {
@@ -11769,99 +11944,67 @@ async function dismissTriage(proposalId, opts = {}) {
11769
11944
  if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
11770
11945
  return await res.json();
11771
11946
  }
11772
- function repoFromRemote(url) {
11773
- const said = (url ?? "").trim();
11774
- if (!said) return null;
11775
- const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
11776
- const parts = path.split("/").filter(Boolean);
11777
- if (parts.length < 2) return null;
11778
- const [owner, name] = parts.slice(-2);
11779
- if (!owner || !name) return null;
11780
- return `${owner}/${name}`.toLowerCase();
11781
- }
11782
- var asked;
11783
- function currentRepo(cwd = process.cwd()) {
11784
- if (asked !== void 0) return asked;
11785
- try {
11786
- const url = execFileSync2("git", ["remote", "get-url", "origin"], {
11787
- cwd,
11788
- encoding: "utf8",
11789
- timeout: 2e3,
11790
- stdio: ["ignore", "pipe", "ignore"]
11791
- });
11792
- asked = repoFromRemote(url);
11793
- } catch {
11794
- asked = null;
11795
- }
11796
- return asked;
11947
+ var 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.`;
11948
+ var APPLIED = "Applied; nothing was sent to the person.";
11949
+ var 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.";
11950
+ async function readDelivery(deliveryId, opts = {}) {
11951
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, {
11952
+ headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` },
11953
+ signal: opts.signal
11954
+ }));
11955
+ if (!res.ok) await fail("read_delivery", res);
11956
+ return await res.json();
11797
11957
  }
11798
- var 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 message saying what is done and anything they need to do or check.";
11799
- var 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, with or without a Goal.";
11800
11958
  async function contact(input, opts = {}) {
11801
11959
  const parsed = ContactSchema.parse(input);
11802
- if (!("asks" in parsed) && !("deliveryId" in parsed)) throw new Error("contact with nothing to send receives: call receiveContact");
11960
+ if (!sendsAnything(parsed)) throw new Error("contact with nothing to send receives: call receiveContact");
11803
11961
  opts.signal?.throwIfAborted();
11804
11962
  const send22 = opts.reach ?? reach;
11805
- const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` };
11806
- let deliveryId;
11807
- let deliveries = [];
11808
- let recorded = [];
11809
- let demoted;
11810
- if ("deliveryId" in parsed) {
11811
- deliveryId = parsed.deliveryId;
11812
- } else {
11813
- const reqAsks = parsed.asks.map((a) => ({
11814
- id: a.id,
11815
- parentId: a.parentId,
11816
- // Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
11817
- // work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
11818
- // still wins — normalised the same way — because a caller that names one knows something
11819
- // this cannot see.
11820
- repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
11821
- ask: a.ask,
11822
- options: a.options ?? [],
11823
- ...a.select ? { select: a.select } : {},
11824
- ...a.answers ? { answers: a.answers } : {}
11825
- }));
11826
- const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
11827
- method: "POST",
11828
- headers,
11829
- signal: opts.signal,
11830
- body: JSON.stringify({
11831
- operationId: opts.operationId ?? randomUUID3(),
11832
- asks: reqAsks,
11833
- waiting: parsed.waiting,
11834
- channel: parsed.channel === "call" ? "call" : "message"
11835
- })
11836
- }));
11837
- if (!res.ok) await fail("batch contact", res);
11838
- const receipt = await res.json();
11839
- deliveries = receipt.deliveries ?? [];
11840
- if (parsed.channel === "call" && receipt.channel && receipt.channel !== "call") demoted = DEMOTED;
11841
- recorded = receipt.recorded ?? [];
11842
- if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
11843
- if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
11844
- deliveryId = deliveries[0]?.deliveryId ?? "";
11845
- if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
11846
- }
11847
- const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id: id22, deliveryId: deliveryId2, goalId }) => ({ ...id22 ? { id: id22 } : {}, deliveryId: deliveryId2, goalId }));
11848
- const joinedCard = deliveries[0]?.joinedCard === true;
11849
- const joinedCall = deliveries[0]?.joinedCall === true;
11963
+ const acknowledged = parsed.ackEventIds?.length ? await acknowledge(parsed.ackEventIds, opts) : void 0;
11964
+ const questions = parsed.questions?.map((q) => ({ ...q, questionId: q.questionId ?? randomUUID2() }));
11965
+ const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
11966
+ method: "POST",
11967
+ signal: opts.signal,
11968
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
11969
+ body: JSON.stringify({
11970
+ operationId: opts.operationId ?? randomUUID2(),
11971
+ ...parsed.updates ? { updates: parsed.updates } : {},
11972
+ ...questions ? { questions } : {},
11973
+ ...parsed.answers ? { answers: parsed.answers } : {},
11974
+ ...parsed.withdrawQuestionIds ? { withdrawQuestionIds: parsed.withdrawQuestionIds } : {},
11975
+ ...parsed.questionDependencies ? { questionDependencies: parsed.questionDependencies } : {}
11976
+ })
11977
+ }));
11978
+ if (!res.ok) await fail("contact", res);
11979
+ const receipt = await res.json();
11980
+ const results = receipt.results ?? [];
11981
+ const ok = receipt.ok ?? results.every((r) => r.status !== "failed");
11982
+ const deliveries = receipt.deliveries ?? [];
11983
+ const recorded = results.filter((r) => r.status === "recorded" && r.goalId).map((r) => ({ goalId: r.goalId }));
11984
+ const askedCall = [...parsed.updates ?? [], ...questions ?? []].some((m) => m.userExplicitlyRequested === "call") || !!questions?.some((q) => q.workItBlocks?.length);
11985
+ const demoted = askedCall && receipt.channel && receipt.channel !== "call" ? DEMOTED : void 0;
11986
+ const acked = acknowledged ? { acknowledged } : {};
11987
+ if (!deliveries.length) {
11988
+ return { sent: false, ok, results, ...recorded.length ? { recorded } : {}, message: recorded.length ? NOT_SENT : APPLIED, waitOutcome: "not_waited", ...acked };
11989
+ }
11990
+ const lead = deliveries.find((d) => d.kind === "question") ?? deliveries[0];
11850
11991
  const read3 = async (signal2) => {
11851
- const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
11852
- if (!res.ok) await fail("read_delivery", res);
11853
- const one = await res.json();
11992
+ const one = await readDelivery(lead.deliveryId, { ...opts, signal: signal2 });
11854
11993
  return {
11855
11994
  ...one,
11856
- ...all.length > 1 ? { deliveries: all } : {},
11995
+ ok,
11996
+ results,
11997
+ ...deliveries.length > 1 ? { deliveries: deliveries.map(({ deliveryId, goalId, kind, questionId }) => ({ deliveryId, goalId, kind, ...questionId ? { questionId } : {} })) } : {},
11857
11998
  ...recorded.length ? { recorded, notSent: NOT_SENT } : {},
11858
- ...joinedCard ? { joinedCard: true } : {},
11859
- ...joinedCall ? { joinedCall: true } : {},
11860
- ...demoted ? { demoted } : {}
11999
+ ...lead.joinedCard ? { joinedCard: true } : {},
12000
+ // IT WENT ONTO A CALL ALREADY HAPPENING (owner, 2026-09-28): the view words it as that call.
12001
+ ...lead.joinedCall ? { joinedCall: true } : {},
12002
+ ...demoted ? { demoted } : {},
12003
+ ...acked
11861
12004
  };
11862
12005
  };
11863
- const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
11864
- if (opts.waits === false) return read3(opts.signal);
12006
+ const settled = (d) => d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution" && !entryUnit(e));
12007
+ if (parsed.wait !== true || opts.waits === false) return { ...await read3(opts.signal), waitOutcome: "not_waited" };
11865
12008
  const window = AbortSignal.timeout(AWAIT_WINDOW_MS);
11866
12009
  const signal = opts.signal ? AbortSignal.any([opts.signal, window]) : window;
11867
12010
  let latest;
@@ -11869,12 +12012,12 @@ async function contact(input, opts = {}) {
11869
12012
  while (true) {
11870
12013
  signal.throwIfAborted();
11871
12014
  latest = await read3(signal);
11872
- if (settled(latest)) return latest;
12015
+ if (settled(latest)) return { ...latest, waitOutcome: "available" };
11873
12016
  await sleep2(5e3, void 0, { signal });
11874
12017
  }
11875
12018
  } catch (error) {
11876
12019
  opts.signal?.throwIfAborted();
11877
- if (window.aborted && latest) return latest;
12020
+ if (window.aborted && latest) return { ...latest, waitOutcome: "expired" };
11878
12021
  throw error;
11879
12022
  }
11880
12023
  }
@@ -11885,6 +12028,15 @@ async function receive(opts = {}) {
11885
12028
  if (!res.ok) await fail("receive", res);
11886
12029
  return await res.json();
11887
12030
  }
12031
+ async function acknowledge(eventIds, opts = {}) {
12032
+ const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries/ack`, {
12033
+ method: "POST",
12034
+ headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}` },
12035
+ body: JSON.stringify({ eventIds })
12036
+ }));
12037
+ if (!res.ok) await fail("acknowledge", res);
12038
+ return (await res.json()).results ?? [];
12039
+ }
11888
12040
  var HEARTBEAT_MS = 25e3;
11889
12041
  var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
11890
12042
  var JOIN = { config: { broadcast: { self: false } } };
@@ -12365,9 +12517,9 @@ async function available(deps = {}) {
12365
12517
  });
12366
12518
  return [ollama, ...harnesses];
12367
12519
  }
12368
- function refusal(options, asked2) {
12520
+ function refusal(options, asked) {
12369
12521
  const lines2 = [
12370
- asked2 ? `Can't judge with --via ${asked2}: ${options.find((o) => o.provider === asked2)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
12522
+ asked ? `Can't judge with --via ${asked}: ${options.find((o) => o.provider === asked)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
12371
12523
  "",
12372
12524
  "Triage runs the model where your queue already lives (#1106). Your options:"
12373
12525
  ];
@@ -12391,10 +12543,10 @@ var RUNNERS = {
12391
12543
  agy: { cli: "agy", argv: () => ["-p"] }
12392
12544
  };
12393
12545
  var RUN_TIMEOUT_MS = 5 * 6e4;
12394
- async function judgeFor(asked2, deps = {}) {
12546
+ async function judgeFor(asked, deps = {}) {
12395
12547
  const options = await available(deps);
12396
- const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
12397
- if (!chosen?.ready) return { error: refusal(options, asked2) };
12548
+ const chosen = asked ? options.find((o) => o.provider === asked) : options.find((o) => o.ready);
12549
+ if (!chosen?.ready) return { error: refusal(options, asked) };
12398
12550
  if (chosen.provider === "ollama") {
12399
12551
  const model = chosen.model;
12400
12552
  return {
@@ -12415,9 +12567,9 @@ async function judgeFor(asked2, deps = {}) {
12415
12567
  };
12416
12568
  }
12417
12569
  const runner = RUNNERS[chosen.provider];
12418
- if (!runner) return { error: refusal(options, asked2) };
12570
+ if (!runner) return { error: refusal(options, asked) };
12419
12571
  const bin = which(runner.cli);
12420
- if (!bin) return { error: refusal(options, asked2) };
12572
+ if (!bin) return { error: refusal(options, asked) };
12421
12573
  return {
12422
12574
  provider: chosen.provider,
12423
12575
  model: null,
@@ -12686,10 +12838,10 @@ function ago(iso, now) {
12686
12838
  return `${plural2(Math.floor(hours / 24), "day")} ago`;
12687
12839
  }
12688
12840
  function standing(queue, proposal, now) {
12689
- const head = queue.length === 0 ? "Nothing open \u2014 the queue is clear" : `Holding ${plural2(queue.length, "open note")}`;
12690
- if (!proposal) return `${head} \xB7 no sweep waiting`;
12841
+ const head2 = queue.length === 0 ? "Nothing open \u2014 the queue is clear" : `Holding ${plural2(queue.length, "open note")}`;
12842
+ if (!proposal) return `${head2} \xB7 no sweep waiting`;
12691
12843
  const n = suggested(proposal);
12692
- return n === 0 ? `${head} \xB7 swept ${ago(proposal.runAt, now)}, nothing to suggest` : `${head} \xB7 ${plural2(n, "suggestion")} waiting on you, swept ${ago(proposal.runAt, now)}`;
12844
+ return n === 0 ? `${head2} \xB7 swept ${ago(proposal.runAt, now)}, nothing to suggest` : `${head2} \xB7 ${plural2(n, "suggestion")} waiting on you, swept ${ago(proposal.runAt, now)}`;
12693
12845
  }
12694
12846
  async function brokerView(deps) {
12695
12847
  const now = (deps.now ?? Date.now)();
@@ -12789,7 +12941,7 @@ function listRepos(deps) {
12789
12941
  }
12790
12942
 
12791
12943
  // src/holder.ts
12792
- import { execFileSync as execFileSync3 } from "child_process";
12944
+ import { execFileSync as execFileSync2 } from "child_process";
12793
12945
  import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
12794
12946
  import { homedir as homedir6 } from "os";
12795
12947
  import { join as join6 } from "path";
@@ -12804,7 +12956,7 @@ function isAlive(pid) {
12804
12956
  function psStart(pid, zone) {
12805
12957
  try {
12806
12958
  const env = zone === "UTC" ? { ...process.env, TZ: "UTC", LC_ALL: "C" } : { ...process.env, LC_ALL: "C" };
12807
- return execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], { env, encoding: "utf8", timeout: 2e3 }).trim() || null;
12959
+ return execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], { env, encoding: "utf8", timeout: 2e3 }).trim() || null;
12808
12960
  } catch {
12809
12961
  return null;
12810
12962
  }
@@ -12859,7 +13011,7 @@ function localHolderPids(deps = {}) {
12859
13011
  }
12860
13012
  function psParent(pid) {
12861
13013
  try {
12862
- const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
13014
+ const out = execFileSync2("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
12863
13015
  const n = Number(out);
12864
13016
  return Number.isInteger(n) && n > 0 ? n : null;
12865
13017
  } catch {
@@ -12983,7 +13135,7 @@ function ownerSession(slot, deps = {}) {
12983
13135
  const prefix = m[1];
12984
13136
  const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
12985
13137
  const list = deps.list ?? ((dir) => readdirSync3(dir));
12986
- const head = deps.head ?? readHead;
13138
+ const head2 = deps.head ?? readHead;
12987
13139
  let folders;
12988
13140
  try {
12989
13141
  folders = list(root);
@@ -13001,7 +13153,7 @@ function ownerSession(slot, deps = {}) {
13001
13153
  if (!file) continue;
13002
13154
  let text;
13003
13155
  try {
13004
- text = head(join7(root, folder, file));
13156
+ text = head2(join7(root, folder, file));
13005
13157
  } catch {
13006
13158
  continue;
13007
13159
  }
@@ -14312,14 +14464,20 @@ var OptionSchema2 = external_exports.object({
14312
14464
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
14313
14465
  // the constraints below are what an agent reads when deciding to use these.
14314
14466
  html: external_exports.string().max(16384).describe(
14315
- "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
14467
+ "Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use the image URL instead."
14316
14468
  ).optional(),
14317
14469
  image: external_exports.string().url().describe(
14318
- "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
14470
+ "Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use the HTML preview instead."
14319
14471
  ).optional()
14320
14472
  });
14321
- var OptionInputSchema2 = OptionSchema2.omit({ id: true }).extend({
14322
- label: external_exports.string().trim().min(1).max(1e3)
14473
+ var OptionInputSchema2 = external_exports.object({
14474
+ id: external_exports.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
14475
+ `Optional: this option's own id, which answers name it by (selectedOptionIds). Give every option one, or none: without them the options are numbered "1", "2", \u2026 in order.`
14476
+ ),
14477
+ label: external_exports.string().trim().min(1).max(1e3),
14478
+ hint: OptionSchema2.shape.hint,
14479
+ htmlPreview: OptionSchema2.shape.html,
14480
+ imgUrl: OptionSchema2.shape.image
14323
14481
  }).strict();
14324
14482
  var NIGHT2 = { from: 23, to: 7 };
14325
14483
  function draft20202(node) {
@@ -14351,41 +14509,162 @@ function mcpInputSchema2(s) {
14351
14509
  delete schema.$schema;
14352
14510
  return draft20202(schema);
14353
14511
  }
14354
- var AskInputSchema2 = external_exports.object({
14355
- id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
14356
- parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and the question starts a new Goal of yours."),
14357
- repo: external_exports.string().optional().describe("Optional repository context."),
14358
- ask: external_exports.string().trim().min(1).max(1e4).describe(
14359
- "ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy sends each ask exactly as you wrote it: a bundled ask arrives as one card. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
14512
+ var ASK_MAX2 = 250;
14513
+ var UNITS_MAX2 = 12;
14514
+ var UNIT_TITLE_MAX2 = 50;
14515
+ var UNIT_BODY_MAX2 = 200;
14516
+ var UnitInputSchema2 = external_exports.object({
14517
+ title: external_exports.string().trim().min(1).describe(`What this unit is about, at most ${UNIT_TITLE_MAX2} characters. The app lists the titles; the person taps one to read its body.`),
14518
+ body: external_exports.string().trim().min(1).describe(`The unit itself, at most ${UNIT_BODY_MAX2} characters.`)
14519
+ }).strict();
14520
+ function capRefusals2(text, units22, field) {
14521
+ const out = [];
14522
+ const name = field === "text" ? "answer" : field;
14523
+ if (text.length > ASK_MAX2) out.push({
14524
+ refusal: "ask_too_long",
14525
+ length: text.length,
14526
+ cap: ASK_MAX2,
14527
+ path: field === "text" ? ["answer", "text"] : [field],
14528
+ message: `ask_too_long: the ${name} is ${text.length} characters; the cap is ${ASK_MAX2}. Keep the one point in the ${name} and move the context into units.`
14529
+ });
14530
+ const all = units22 ?? [];
14531
+ if (all.length > UNITS_MAX2) out.push({
14532
+ refusal: "too_many_units",
14533
+ length: all.length,
14534
+ cap: UNITS_MAX2,
14535
+ path: ["units"],
14536
+ message: `too_many_units: ${all.length} units; the cap is ${UNITS_MAX2}. Keep only the context this ${name} needs.`
14537
+ });
14538
+ all.forEach((unit, index) => {
14539
+ if (unit.title.length > UNIT_TITLE_MAX2) out.push({
14540
+ refusal: "unit_title_too_long",
14541
+ index,
14542
+ length: unit.title.length,
14543
+ cap: UNIT_TITLE_MAX2,
14544
+ path: ["units", index, "title"],
14545
+ message: `unit_title_too_long: units[${index}]'s title is ${unit.title.length} characters; the cap is ${UNIT_TITLE_MAX2}.`
14546
+ });
14547
+ if (unit.body.length > UNIT_BODY_MAX2) out.push({
14548
+ refusal: "unit_body_too_long",
14549
+ index,
14550
+ length: unit.body.length,
14551
+ cap: UNIT_BODY_MAX2,
14552
+ path: ["units", index, "body"],
14553
+ message: `unit_body_too_long: units[${index}]'s body is ${unit.body.length} characters; the cap is ${UNIT_BODY_MAX2}. Cut it into two units.`
14554
+ });
14555
+ });
14556
+ return out;
14557
+ }
14558
+ function refuseOverCaps2(text, units22, field, ctx) {
14559
+ for (const { path, message, ...params } of capRefusals2(text, units22, field)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path, message, params });
14560
+ }
14561
+ var questionHandle2 = external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/, "a question id, whole or its first eight characters");
14562
+ var NodeRef2 = external_exports.object({
14563
+ type: external_exports.enum(["goal", "question"]),
14564
+ id: external_exports.string().uuid()
14565
+ }).strict().describe("A Goal or a Question, by its id.");
14566
+ var BlockedAction2 = external_exports.enum(["start", "complete", "answer"]).describe("What waits: starting or completing a Goal, or answering a Question.");
14567
+ var reason2 = external_exports.string().trim().min(1).max(500).describe("Why, as an explanation, never as policy.");
14568
+ var actionFits2 = (b) => b.blocked.type === "goal" === (b.action !== "answer");
14569
+ var ACTION_FITS2 = "a goal is blocked from start or complete; a question from answer";
14570
+ var userExplicitlyRequested2 = external_exports.enum(["any", "call"]).optional().describe(
14571
+ `Only what the person said about THIS message, in so many words: "any" when they asked you to send it to them ("send it to my phone", "message me the result"), "call" when they asked to be called about it. It is then delivered (a call rings for the whole contact, and carries everything waiting on them); omitted, an update is only recorded as the Goal's progress unless the person needs it. Never set it to make a message louder.`
14572
+ );
14573
+ var units2 = external_exports.array(UnitInputSchema2).optional().describe(
14574
+ `The context this message needs, as up to ${UNITS_MAX2} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX2} characters and a body of at most ${UNIT_BODY_MAX2}. One idea per unit. Leave it out when the main text says it all.`
14575
+ );
14576
+ var UpdateInputSchema2 = external_exports.object({
14577
+ message: external_exports.string().trim().min(1).describe(`The one point, at most ${ASK_MAX2} characters; the context goes in units.`),
14578
+ goalIds: external_exports.array(external_exports.string().uuid()).min(1).max(10).describe("The Goals it is about, each one that exists: the one it is mainly about first."),
14579
+ units: units2,
14580
+ userExplicitlyRequested: userExplicitlyRequested2
14581
+ }).strict().superRefine((u, ctx) => refuseOverCaps2(u.message, u.units, "message", ctx));
14582
+ var questionFields2 = {
14583
+ question: external_exports.string().trim().min(1).describe(
14584
+ `ONE question, in at most ${ASK_MAX2} characters, and only what is needed to answer it; the context goes in units. Several questions are several objects, one each: an answer settles the one question it was given, so a person who answers the part of a bundled question that interested them settles nothing and is asked again. Separate questions cost nothing: ANY contact for a person already on a call JOINS that call, so several arrive as one call.`
14360
14585
  ),
14361
- options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
14586
+ goalId: external_exports.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
14587
+ units: units2,
14588
+ options: external_exports.array(OptionInputSchema2).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
14362
14589
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
14363
- // brain_prompts.md §1: structured agent questions need no model). Every card was pick-one while no
14364
- // agent sent this; no read reshapes cards any more, so this is the card's shape, and the
14365
- // description says so plainly.
14366
- select: external_exports.enum(["one", "many"]).optional().describe(
14367
- 'How the options are answered: "many" makes a checklist (they may want several, e.g. independent fixes), "one" (the default) a pick (choosing one rules out the others). Send "many" whenever more than one option could be wanted at once. The person can always answer in their own words as well. Ignored without options.'
14590
+ // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
14591
+ pickMode: external_exports.enum(["one", "many", "rank"]).optional().describe(
14592
+ 'How the options are answered: "one" (the default) a pick, for alternatives; "many" a checklist, for independent options they may want several of; "rank" an order of all of them. Only with options.'
14368
14593
  ),
14369
- answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
14370
- "Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. parentId is optional here: the question's own Goal is used. The reply is sent to them as it is; to also ask something new, send that as its own ask."
14371
- )
14372
- }).strict();
14373
- var StartContactSchema2 = external_exports.object({
14374
- asks: external_exports.array(AskInputSchema2).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; an ask naming no Goal starts a new Goal of yours."),
14375
- waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
14376
- channel: external_exports.enum(["notification", "call"]).default("notification")
14377
- }).strict();
14378
- var ReceiveContactSchema2 = external_exports.object({
14379
- wait: external_exports.boolean().optional().describe(
14380
- "Only controls when this returns. true (the default when you send and acknowledge nothing): wait up to about 45 seconds for incoming communication addressed to you, and return as soon as some is available. false: return 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 in the window; wait again without sending again), or `not_waited` (a hosted connection never holds a wait)."
14594
+ // WHAT THE QUESTION HOLDS UP (brain_prompts.md §2.1): replaces `waiting: 'hard'`. A question that
14595
+ // blocks work asks for a call (the person's settings and ring cooldowns still decide).
14596
+ workItBlocks: external_exports.array(external_exports.object({ blocked: NodeRef2, action: BlockedAction2, reason: reason2.optional() }).strict().refine(actionFits2, ACTION_FITS2)).min(1).max(10).optional().describe(
14597
+ "The work that cannot go on until this is answered: a Goal's start or completion, or another Question's answer. Only for work truly blocked; it asks for a call. Leave it out for a question you can keep working around."
14598
+ ),
14599
+ userExplicitlyRequested: userExplicitlyRequested2
14600
+ };
14601
+ var refineQuestion2 = (q, ctx) => {
14602
+ refuseOverCaps2(q.question, q.units, "question", ctx);
14603
+ if (q.pickMode && !q.options) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["pickMode"], message: "pickMode comes only with options" });
14604
+ const ids = (q.options ?? []).map((o) => o.id);
14605
+ if (ids.some((id22) => id22) && (ids.some((id22) => !id22) || new Set(ids).size !== ids.length)) {
14606
+ ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["options"], message: "give every option a distinct id, or none" });
14607
+ }
14608
+ };
14609
+ var QuestionInputSchema2 = external_exports.object({
14610
+ questionId: external_exports.string().uuid().optional().describe("Optional: your own stable id for this question (a UUID), so a resend is the same question. Minted for you when omitted; the result returns it."),
14611
+ ...questionFields2
14612
+ }).strict().superRefine(refineQuestion2);
14613
+ var QuestionWireSchema2 = external_exports.object({ questionId: external_exports.string().uuid(), ...questionFields2 }).strict().superRefine(refineQuestion2);
14614
+ var AnswerInputSchema2 = external_exports.object({
14615
+ questionId: questionHandle2.describe("The Question you answer: its id as the event or the conversation shows it (8 characters or whole)."),
14616
+ answer: external_exports.object({
14617
+ text: external_exports.string().trim().min(1).describe(`Your answer in your own words, at most ${ASK_MAX2} characters; the context goes in units.`),
14618
+ selectedOptionIds: external_exports.array(external_exports.string().trim().min(1)).min(1).max(6).optional().describe("The ids of the options you choose, when the Question offered options (in order, for a ranking).")
14619
+ }).strict(),
14620
+ units: units2
14621
+ }).strict().superRefine((a, ctx) => refuseOverCaps2(a.answer.text, a.units, "text", ctx));
14622
+ var QuestionDependencySchema2 = external_exports.object({
14623
+ questionId: questionHandle2.describe("A Question you asked."),
14624
+ change: external_exports.enum(["add", "remove"]),
14625
+ blocked: NodeRef2,
14626
+ action: BlockedAction2,
14627
+ reason: reason2.optional()
14628
+ }).strict().refine(actionFits2, ACTION_FITS2);
14629
+ var sendFields2 = {
14630
+ updates: external_exports.array(UpdateInputSchema2).min(1).max(20).optional().describe(
14631
+ "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."
14381
14632
  ),
14633
+ answers: external_exports.array(AnswerInputSchema2).min(1).max(20).optional().describe("Answers to Questions addressed to you."),
14634
+ withdrawQuestionIds: external_exports.array(questionHandle2).min(1).max(10).optional().describe(
14635
+ "Questions you asked that no longer need an answer: you acted on them yourself, they no longer matter, or you asked wrongly. Each is cancelled, not answered: its card closes and nothing rings for it."
14636
+ ),
14637
+ questionDependencies: external_exports.array(QuestionDependencySchema2).min(1).max(20).optional().describe(
14638
+ "Add or remove what a Question you asked holds up, after asking it."
14639
+ )
14640
+ };
14641
+ var ContactSchema2 = external_exports.object({
14642
+ ...sendFields2,
14643
+ questions: external_exports.array(QuestionInputSchema2).min(1).max(20).optional().describe("Questions for the person, one per object."),
14644
+ /** RECEIVING (2I, I2; brain_prompts.md §2.2): the events this agent handled. */
14382
14645
  ackEventIds: external_exports.array(external_exports.string().uuid()).min(1).max(100).optional().describe(
14383
- "The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (an ask with `answers`), never acknowledged away: that id is refused until it is answered."
14646
+ "The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (answers), never acknowledged away."
14647
+ ),
14648
+ wait: external_exports.boolean().optional().describe(
14649
+ "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`."
14384
14650
  )
14385
14651
  }).strict();
14386
- var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict(), ReceiveContactSchema2]);
14652
+ var sendsAnything2 = (c) => !!(c.updates?.length || c.questions?.length || c.answers?.length || c.withdrawQuestionIds?.length || c.questionDependencies?.length);
14653
+ var ContactWireSchema2 = external_exports.object({
14654
+ operationId: external_exports.string().uuid(),
14655
+ ...sendFields2,
14656
+ questions: external_exports.array(QuestionWireSchema2).min(1).max(20).optional()
14657
+ }).strict().refine(sendsAnything2, "a contact sends at least one update, question, answer, withdrawal or question dependency");
14387
14658
  var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
14388
- var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none starts a new Goal of yours. Notification returns immediately; collect answers by receiving (below). On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. Each ask is sent exactly as written, with no reading in between: a bundled ask arrives as one card, so separate questions are separate asks. An ask with no options that is not blocking is a report, never a claim on them, so no answer is owed and none should be awaited. A report reaches the person only when it answers something they asked you, or once the Goal is done; any other report is recorded as the Goal's progress and nobody is notified. So when the work is finished, mark the Goal done first, then send one message saying what is done and anything they need to do or check. On a Goal whose report card is still open, a report that does reach them is added to that card, with no new push. Send options (or waiting:'hard') when you actually need an answer. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). select:'many' makes the card a checklist, for independent options they may want several of; select:'one' (the default) a pick, for alternatives. The person can always answer in their own words, so never add an 'Other' option. If the person is already on a call, a contact that reaches them joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.\n\nRECEIVING: contact with no asks sends nothing and returns `events`, a limited batch of what is addressed to you (not a history page): `question`, a Question you owe (answer it with an ask whose `answers` is 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`; claim_goal starts it) and your work gone quiet (`stalled`).";
14659
+ 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.
14660
+
14661
+ 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.
14662
+
14663
+ 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.
14664
+
14665
+ 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.
14666
+
14667
+ 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\`).`;
14389
14668
  var CreateGoalSchema2 = external_exports.object({
14390
14669
  outcome: external_exports.string().trim().min(1).max(1e4),
14391
14670
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -14419,21 +14698,11 @@ var UpdateGoalSchema2 = external_exports.object({
14419
14698
  state: external_exports.enum(["active", "done", "cancelled"]).optional(),
14420
14699
  progress: external_exports.string().trim().min(1).max(1e4).optional(),
14421
14700
  reviewed: external_exports.literal(true).optional(),
14422
- dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
14423
- /** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
14424
- * (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
14425
- * by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
14426
- withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
14701
+ dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
14427
14702
  }).strict().refine((v) => Object.keys(v).length > 0),
14428
14703
  reason: external_exports.string().trim().min(1).max(2e3),
14429
14704
  operationId: external_exports.string().uuid().optional()
14430
14705
  }).strict();
14431
- var UpdateGoalToolSchema2 = external_exports.object({
14432
- goalId: external_exports.string().uuid(),
14433
- revision: UpdateGoalSchema2.shape.revision,
14434
- changes: UpdateGoalSchema2.shape.changes.innerType().pick({ progress: true, reviewed: true, dueAt: true, withdraw: true }).strict().refine((v) => Object.keys(v).length > 0),
14435
- reason: UpdateGoalSchema2.shape.reason
14436
- }).strict();
14437
14706
  var changedGoal2 = external_exports.string().uuid().describe("The Goal to change, as a read shows it.");
14438
14707
  var goalTitle2 = external_exports.string().trim().min(1).max(80).describe("The Goal's short display name: one to five words, how a person refers to it out loud.");
14439
14708
  var goalOutcome2 = external_exports.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
@@ -14441,7 +14710,7 @@ var CreateChange2 = external_exports.object({
14441
14710
  kind: external_exports.literal("create"),
14442
14711
  title: goalTitle2,
14443
14712
  outcome: goalOutcome2,
14444
- ownerId: external_exports.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as who_is_working shows it) or the person's. Omitted: you."),
14713
+ ownerId: external_exports.string().trim().min(1).optional().describe("Who owns the work: an agent's participant (as check_activity shows it) or the person's. Omitted: you."),
14445
14714
  parentGoalId: external_exports.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
14446
14715
  sourceEntryIds: external_exports.array(external_exports.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
14447
14716
  }).strict();
@@ -14451,7 +14720,7 @@ var StateChange2 = external_exports.object({ kind: external_exports.literal("sta
14451
14720
  ) }).strict();
14452
14721
  var AssignChange2 = external_exports.object({ kind: external_exports.literal("assign"), goalId: changedGoal2, ownerId: external_exports.string().trim().min(1).describe("The new owner's participant.") }).strict();
14453
14722
  var DeferChange2 = external_exports.object({ kind: external_exports.literal("defer"), goalId: changedGoal2, until: external_exports.string().datetime({ offset: true }).nullable().describe(
14454
- "Postpone it until this instant (its owner is woken then), or null to take the postponement back."
14723
+ "Postpone it until this instant: it waits until then, and its owner is woken when it passes (a promise to follow up later). null takes the postponement back."
14455
14724
  ) }).strict();
14456
14725
  var MoveChange2 = external_exports.object({ kind: external_exports.literal("move"), goalId: changedGoal2, parentGoalId: external_exports.string().uuid().nullable().describe(
14457
14726
  "Its new parent, or null for a root. Only this Goal moves; its new parent's other children stay."
@@ -14486,8 +14755,7 @@ var ManageGoalsSchema2 = external_exports.object({
14486
14755
  var ManageGoalsToolSchema2 = external_exports.object({
14487
14756
  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.")
14488
14757
  }).strict().superRefine((v, ctx) => editNames2(v.changes, ctx));
14489
- 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_joined, 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; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until, or null); 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 a Goal you own or have written on. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
14490
- var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
14758
+ 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; 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.";
14491
14759
  var GetGoalSchema2 = external_exports.object({
14492
14760
  goalId: external_exports.string().uuid(),
14493
14761
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -14498,9 +14766,7 @@ var GetGoalSchema2 = external_exports.object({
14498
14766
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
14499
14767
  diagnose: external_exports.boolean().optional()
14500
14768
  }).strict();
14501
- var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming 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.";
14502
- var UPDATE_GOAL_DESCRIPTION2 = "Report work on a Goal you are on, at an exact revision: progress, review acknowledgement, when to wake for it, and withdrawing a question of yours. What the Goal is (its title, outcome, state, owner, parent, dependencies) changes with manage_goals. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). Stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. progress says where the work stands. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision (contact({ackEventIds}) does the same for an `update` event). dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. Returns the Goal as get_goal reads it, at its new revision.";
14503
- var CLAIM_GOAL_DESCRIPTION2 = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change it; use manage_goals (assign) when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
14769
+ 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.";
14504
14770
  var SearchToolSchema2 = external_exports.object({
14505
14771
  query: external_exports.string().trim().min(1).max(500),
14506
14772
  types: external_exports.array(external_exports.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -14508,15 +14774,21 @@ var SearchToolSchema2 = external_exports.object({
14508
14774
  limit: external_exports.number().int().min(1).max(20).optional()
14509
14775
  }).strict();
14510
14776
  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 Answers (found by their Question 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, its Questions' Answers, 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 (an Answer carries its Question, 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.";
14511
- var WhoIsWorkingSchema2 = external_exports.object({}).strict();
14777
+ var CheckActivitySchema2 = external_exports.object({}).strict();
14778
+ var FEEDBACK_TEXT_MAX2 = 5e4;
14779
+ var SendFeedbackSchema2 = external_exports.object({
14780
+ text: external_exports.string().trim().min(1).max(FEEDBACK_TEXT_MAX2).describe(`The report, in markdown: what happened, what was expected, how to reproduce it, versions, evidence. At most ${FEEDBACK_TEXT_MAX2} characters.`),
14781
+ title: external_exports.string().trim().min(1).max(200).optional().describe("One line naming it."),
14782
+ kind: external_exports.enum(["bug", "idea", "other"]).default("other").describe("bug: something Paigy does wrong. idea: something it should do. other: anything else.")
14783
+ }).strict();
14784
+ 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.";
14512
14785
  var AGENT_TOOLS2 = [
14513
- { name: "who_is_working", 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(WhoIsWorkingSchema2) },
14514
14786
  { name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
14515
14787
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION2, inputSchema: mcpInputSchema2(ManageGoalsToolSchema2) },
14516
- { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
14517
14788
  { name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
14518
- { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) },
14519
- { name: "search", description: SEARCH_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchToolSchema2) }
14789
+ { name: "search", description: SEARCH_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchToolSchema2) },
14790
+ { 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) },
14791
+ { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION2, inputSchema: mcpInputSchema2(SendFeedbackSchema2) }
14520
14792
  ];
14521
14793
  var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
14522
14794
  var id2 = external_exports.string().uuid();
@@ -14628,33 +14900,93 @@ var BrainResultSchema2 = external_exports.object({
14628
14900
  next: BrainNextSchema2,
14629
14901
  search: BrainSearchSchema2.optional()
14630
14902
  }).strict();
14631
- var str2 = external_exports.string();
14632
- var strs2 = external_exports.array(external_exports.string());
14903
+ var str4 = external_exports.string();
14904
+ var strs4 = external_exports.array(external_exports.string());
14633
14905
  var CompactResultSchema2 = external_exports.object({
14634
- next: external_exports.object({ say: strs2, then: external_exports.enum(["listen", "hold", "end", "none"]), waitFor: strs2, reason: str2, instructions: strs2 }).strict(),
14635
- messages: external_exports.array(external_exports.object({ key: str2, to: str2, text: str2, about: strs2, entries: strs2, owed: strs2, inviteToCall: external_exports.boolean() }).strict()),
14636
- entries: external_exports.array(external_exports.object({ key: str2, from: str2, on: strs2 }).strict()),
14637
- answers: external_exports.array(external_exports.object({ question: str2, entries: strs2, summary: str2, picked: strs2 }).strict()),
14906
+ next: external_exports.object({ say: strs4, then: external_exports.enum(["listen", "hold", "end", "none"]), waitFor: strs4, reason: str4, instructions: strs4 }).strict(),
14907
+ messages: external_exports.array(external_exports.object({ key: str4, to: str4, text: str4, about: strs4, entries: strs4, owed: strs4, inviteToCall: external_exports.boolean() }).strict()),
14908
+ entries: external_exports.array(external_exports.object({ key: str4, from: str4, on: strs4 }).strict()),
14909
+ answers: external_exports.array(external_exports.object({ question: str4, entries: strs4, summary: str4, picked: strs4 }).strict()),
14638
14910
  changes: external_exports.array(external_exports.object({
14639
- key: str2,
14911
+ key: str4,
14640
14912
  what: external_exports.enum(["goal", "question", "lesson"]),
14641
14913
  op: external_exports.enum(["create", "edit", "assign", "open", "complete", "cancel", "defer", "move", "block", "unblock", "withdraw", "remember", "revise"]),
14642
- id: str2,
14643
- text: str2,
14644
- title: str2,
14645
- who: str2,
14646
- under: str2,
14914
+ id: str4,
14915
+ text: str4,
14916
+ title: str4,
14917
+ who: str4,
14918
+ under: str4,
14647
14919
  gate: external_exports.enum(["start", "complete", "answer", "none"]),
14648
- options: external_exports.array(external_exports.object({ id: str2, label: str2 }).strict()),
14920
+ options: external_exports.array(external_exports.object({ id: str4, label: str4 }).strict()),
14649
14921
  pick: external_exports.enum(["one", "many", "rank", "words"]),
14650
- entries: strs2
14922
+ entries: strs4
14651
14923
  }).strict()),
14652
- feedback: external_exports.array(strs2),
14653
- search: external_exports.object({ query: str2, within: strs2, goal: str2 }).strict()
14924
+ feedback: external_exports.array(strs4),
14925
+ search: external_exports.object({ query: str4, within: strs4, goal: str4 }).strict()
14654
14926
  }).strict();
14655
14927
  var COMPACT_RESULT_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
14656
14928
  zodToJsonSchema2(CompactResultSchema2, { $refStrategy: "none" })
14657
14929
  );
14930
+ var str22 = external_exports.string();
14931
+ var strs22 = external_exports.array(external_exports.string());
14932
+ var TalkerReplySchema2 = external_exports.object({
14933
+ /** What to say and send, messages to the person first: spoken in the order written. `to` is "user"
14934
+ * or an agent's handle; `about` names handles; `blocks` is the item a question to an agent must be
14935
+ * answered before ("" for none). */
14936
+ messages: external_exports.array(external_exports.object({ key: str22, to: str22, text: str22, about: strs22, blocks: str22 }).strict()),
14937
+ /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
14938
+ then: external_exports.enum(["listen", "hold", "end"]),
14939
+ /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
14940
+ answers: external_exports.array(external_exports.object({ item: str22, options: strs22, lines: strs22 }).strict()),
14941
+ /** The line handles that hold a new instruction from the person. */
14942
+ instruction: strs22,
14943
+ /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
14944
+ * reads only these (3e). */
14945
+ file: strs22,
14946
+ /** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
14947
+ need: external_exports.object({ kind: external_exports.enum(["search", "item", "agent", ""]), text: str22 }).strict(),
14948
+ /** The handle of the item to explain ("" for none). */
14949
+ explain: str22
14950
+ }).strict();
14951
+ var TALKER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
14952
+ zodToJsonSchema2(TalkerReplySchema2, { $refStrategy: "none" })
14953
+ );
14954
+ var str32 = external_exports.string();
14955
+ var strs32 = external_exports.array(external_exports.string());
14956
+ var FilerReplySchema2 = external_exports.object({
14957
+ /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
14958
+ filed: external_exports.array(external_exports.object({ line: str32, goals: strs32 }).strict()),
14959
+ goals: external_exports.array(external_exports.object({
14960
+ op: external_exports.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
14961
+ key: str32,
14962
+ goal: str32,
14963
+ title: str32,
14964
+ outcome: str32,
14965
+ owner: str32,
14966
+ under: str32,
14967
+ until: str32,
14968
+ on: str32,
14969
+ gate: external_exports.enum(["start", "complete", "answer", ""]),
14970
+ why: str32
14971
+ }).strict()),
14972
+ questions: external_exports.array(external_exports.object({
14973
+ op: external_exports.enum(["create", "edit", "assign", "withdraw"]),
14974
+ key: str32,
14975
+ question: str32,
14976
+ text: str32,
14977
+ to: str32,
14978
+ goal: str32,
14979
+ options: external_exports.array(external_exports.object({ id: str32, label: str32 }).strict()),
14980
+ pick: external_exports.enum(["one", "many", "rank", "words", ""]),
14981
+ blocks: str32
14982
+ }).strict()),
14983
+ lessons: external_exports.array(external_exports.object({ op: external_exports.enum(["remember", "revise", "withdraw"]), lesson: str32, text: str32, scope: str32 }).strict()),
14984
+ /** Line handles whose words comment on Paigy itself. */
14985
+ feedback: strs32
14986
+ }).strict();
14987
+ var FILER_REPLY_JSON_SCHEMA2 = (({ $schema: _, ...rest }) => rest)(
14988
+ zodToJsonSchema2(FilerReplySchema2, { $refStrategy: "none" })
14989
+ );
14658
14990
  function entryWords(entry) {
14659
14991
  const content = entry.content;
14660
14992
  if (content && "sealed" in content) return "";
@@ -14678,7 +15010,10 @@ var ContextSchema2 = external_exports.object({
14678
15010
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
14679
15011
  description: external_exports.array(external_exports.string().min(1)).describe(
14680
15012
  "Semantic chunks of detail (each a standalone, non-empty piece). The user can select chunks to ask you to expand. MAY BE EMPTY: a claim whose whole content is its heading \u2014 a single sentence \u2014 has no body, and saying so beats repeating the heading underneath itself. That repeat is what `min(1)` used to force, at 2x the storage, with every reader subtracting it back out at render time."
14681
- )
15013
+ ),
15014
+ /** THE ASK'S UNITS (3c): its context as titled units, in order. The app lists the titles and opens
15015
+ * a body on a tap. Absent on an ask sent without units, which renders as before. */
15016
+ units: external_exports.array(external_exports.object({ title: external_exports.string(), body: external_exports.string() })).optional()
14682
15017
  });
14683
15018
  var ParticipantSchema2 = external_exports.object({
14684
15019
  kind: external_exports.enum(["human", "agent"]),
@@ -14753,7 +15088,7 @@ var NotifyRequestFields2 = external_exports.object({
14753
15088
  * ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
14754
15089
  * superRefine at the bottom enforces exactly one of the two. */
14755
15090
  context: ContextSchema2.optional(),
14756
- options: external_exports.array(OptionInputSchema2).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
15091
+ options: external_exports.array(OptionSchema2.omit({ id: true }).extend({ label: external_exports.string().trim().min(1).max(1e3) }).strict()).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
14757
15092
  "The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
14758
15093
  ),
14759
15094
  points: external_exports.array(external_exports.string().min(1)).optional().describe(
@@ -15514,8 +15849,8 @@ var QueueItemSchema2 = external_exports.object({
15514
15849
  repo: external_exports.string().nullable().optional(),
15515
15850
  createdAt: external_exports.string(),
15516
15851
  updatedAt: external_exports.string().nullable().default(null),
15517
- /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
15518
- * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
15852
+ /** When its owner last SAID something about it (the newest `progress` Entry: a contact update kept
15853
+ * as progress). `updatedAt` moves for reasons nobody chose — a
15519
15854
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
15520
15855
  lastProgressAt: external_exports.string().nullable().optional(),
15521
15856
  /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
@@ -15671,7 +16006,7 @@ var HostDecisionSchema2 = external_exports.object({
15671
16006
  /** The agent's token id: the row's `recipient`. */
15672
16007
  agent: external_exports.string().uuid(),
15673
16008
  decision: external_exports.enum(["stood_back", "took_over"]),
15674
- /** The work it was about: the Goal `claim_goal` would hand that agent next. */
16009
+ /** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
15675
16010
  goalId: external_exports.string().uuid().nullable().optional(),
15676
16011
  /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
15677
16012
  seenAt: external_exports.string().datetime().nullable().optional(),
@@ -15898,7 +16233,7 @@ function sameTail(a, b) {
15898
16233
  }
15899
16234
 
15900
16235
  // src/run.ts
15901
- import { randomUUID as randomUUID5 } from "crypto";
16236
+ import { randomUUID as randomUUID4 } from "crypto";
15902
16237
 
15903
16238
  // src/harness/session.ts
15904
16239
  import { spawn } from "child_process";
@@ -16182,7 +16517,7 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
16182
16517
  import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
16183
16518
  import { join as join8 } from "path";
16184
16519
  import { homedir as homedir8 } from "os";
16185
- import { randomUUID as randomUUID4 } from "crypto";
16520
+ import { randomUUID as randomUUID3 } from "crypto";
16186
16521
  var MIDTURN_HOOK = `d="$PAIGY_INBOX"; [ -n "$d" ] || { cat >/dev/null; exit 0; }; set -- "$d"/*.note; [ -e "$1" ] || { cat >/dev/null; exit 0; }; grep '"agent_id"' >/dev/null && exit 0; mkdir -p "$d/taken"; o=""; for f in "$@"; do n=\${f##*/}; mv "$f" "$d/taken/$n" 2>/dev/null || continue; touch "$d/taken/$n"; o="$o\${o:+\\n\\n}$(cat "$d/taken/$n")"; done; [ -n "$o" ] || exit 0; printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}' "$o"`;
16187
16522
  var missing = (error) => error.code === "ENOENT";
16188
16523
  function openInbox(dir) {
@@ -16242,7 +16577,7 @@ function inboxFor(root = INBOX_ROOT) {
16242
16577
  } catch (error) {
16243
16578
  if (!missing(error)) throw error;
16244
16579
  }
16245
- return openInbox(join8(root, `${process.pid}-${randomUUID4().slice(0, 8)}`));
16580
+ return openInbox(join8(root, `${process.pid}-${randomUUID3().slice(0, 8)}`));
16246
16581
  }
16247
16582
 
16248
16583
  // src/harness/session.ts
@@ -16376,6 +16711,32 @@ function startSession(opts) {
16376
16711
  };
16377
16712
  }
16378
16713
 
16714
+ // src/paigy/cut.ts
16715
+ function cut(text, title = "Details") {
16716
+ const all = text.trim();
16717
+ if (all.length <= ASK_MAX2) return { main: all };
16718
+ const main = head(all, ASK_MAX2);
16719
+ const units3 = [];
16720
+ let rest = all.slice(main.length).trim();
16721
+ while (rest && units3.length < UNITS_MAX2) {
16722
+ const last = units3.length === UNITS_MAX2 - 1;
16723
+ const body = last && rest.length > UNIT_BODY_MAX2 ? `${head(rest, UNIT_BODY_MAX2 - 1).trimEnd()}\u2026` : head(rest, UNIT_BODY_MAX2);
16724
+ units3.push({ title: units3.length ? `${title} (${units3.length + 1})` : title, body: body.trim() });
16725
+ rest = last ? "" : rest.slice(body.length).trim();
16726
+ }
16727
+ return { main: main.trim(), units: units3 };
16728
+ }
16729
+ function head(text, max) {
16730
+ if (text.length <= max) return text;
16731
+ const window = text.slice(0, max + 1);
16732
+ for (const boundary of [/\n\s*\n/g, /[.!?](\s)/g, /\s/g]) {
16733
+ let at = -1;
16734
+ for (const m of window.matchAll(boundary)) if (m.index > max / 3 && m.index < max) at = m.index + (boundary.source.startsWith("[") ? 1 : 0);
16735
+ if (at > 0) return text.slice(0, at);
16736
+ }
16737
+ return text.slice(0, max);
16738
+ }
16739
+
16379
16740
  // src/paigy/limit.ts
16380
16741
  var LIMIT_PREFIXES = [
16381
16742
  "You've hit your",
@@ -16504,10 +16865,19 @@ var LIMITS = episodes();
16504
16865
  // src/paigy/bridge.ts
16505
16866
  function railFor(opts) {
16506
16867
  return {
16507
- claim: (goalId) => claimGoal(goalId, opts),
16868
+ next: async () => {
16869
+ const waiting = await receive(opts);
16870
+ const goalId = waiting.claimable?.goalId ?? waiting.review?.[0]?.goalId;
16871
+ return goalId ? getGoal(goalId, opts) : { state: "idle", message: "" };
16872
+ },
16508
16873
  read: (goalId) => getGoal(goalId, opts),
16509
- update: (goalId, input) => updateGoal(goalId, input, opts),
16510
- // One read and back: the pump owns the waiting, so a Call must not hold a window here.
16874
+ complete: async (goalId, revision) => {
16875
+ const done = await manageGoals({ changes: [{ kind: "state", goalId, state: "completed", ...revision !== void 0 ? { revision } : {} }] }, opts);
16876
+ const result = done.results[0];
16877
+ if (result?.status !== "applied") throw new Error(`manage_goals failed: ${result?.error ?? "no result"}`);
16878
+ },
16879
+ ack: (eventIds) => acknowledge(eventIds, opts),
16880
+ // One read and back: the pump owns the waiting, so no wait is held here.
16511
16881
  contact: (input) => contact(input, { ...opts, waits: false }),
16512
16882
  create: async (input) => {
16513
16883
  const made = await manageGoals({ changes: [{ kind: "create", ...input }] }, opts);
@@ -16536,13 +16906,18 @@ function decisionFrom(result) {
16536
16906
  }
16537
16907
  async function ask(state, rail, input) {
16538
16908
  if (!state.goal) return null;
16909
+ const { main, units: units3 } = cut(input.ask);
16539
16910
  try {
16540
16911
  const sent = await rail.contact({
16541
- asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
16542
- waiting: "hard",
16543
- channel: "call"
16912
+ questions: [{
16913
+ question: main,
16914
+ ...units3 ? { units: units3 } : {},
16915
+ goalId: state.goal.id,
16916
+ options: input.options,
16917
+ workItBlocks: [{ blocked: { type: "goal", id: state.goal.id }, action: "complete", reason: "The agent cannot go on without this approval." }]
16918
+ }]
16544
16919
  });
16545
- if ("recorded" in sent) return null;
16920
+ if ("sent" in sent) return null;
16546
16921
  const entryId = sent.requestEntryIds[0];
16547
16922
  if (!entryId) return null;
16548
16923
  (state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
@@ -16560,57 +16935,54 @@ async function askPermission(event, state, rail) {
16560
16935
  ]
16561
16936
  }));
16562
16937
  }
16563
- async function finish(result, state, rail, opts) {
16938
+ async function finish(state, rail, opts) {
16564
16939
  const goal = state.goal;
16565
16940
  if (!goal) return;
16566
- const progress = result?.trim().slice(0, 1e4) || void 0;
16567
- let revision = goal.revision;
16568
16941
  try {
16569
16942
  const held = await rail.read(goal.id);
16570
- revision = held.revision ?? revision;
16571
16943
  const owed = state.noted?.some((b) => b.goals.includes(goal.id));
16572
16944
  if (held.state === "done" || held.state === "cancelled") {
16573
16945
  if (!owed) state.goal = void 0;
16574
16946
  return;
16575
16947
  }
16576
16948
  if (!held.openDecisionNeedIds?.length && !owed) {
16577
- await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
16949
+ await rail.complete(goal.id, held.revision ?? goal.revision);
16578
16950
  state.goal = void 0;
16579
- return;
16580
16951
  }
16581
16952
  } catch (error) {
16582
16953
  opts?.log?.(`\u26A0 the Goal did not close, still holding it: ${error.message}`);
16583
16954
  }
16584
- if (!progress) return;
16585
- try {
16586
- await rail.update(goal.id, { revision, changes: { progress }, reason: "The agent's turn ended; its Goal is not closed yet." });
16587
- } catch (error) {
16588
- opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
16589
- }
16590
16955
  }
16591
16956
  async function cannotWork(report, state, rail, opts) {
16592
16957
  state.resting = true;
16593
- const say = (parentId) => rail.contact({ asks: [{ ask: report, ...parentId ? { parentId } : {} }], waiting: "none", channel: "notification" });
16958
+ const { main, units: units3 } = cut(report);
16959
+ const say = (goalId) => rail.contact({ updates: [{ message: main, ...units3 ? { units: units3 } : {}, goalIds: [goalId] }] });
16960
+ const own = async () => (await rail.create({ title: "Agent cannot work", outcome: main })).goalId;
16594
16961
  try {
16595
16962
  if (!state.goal) {
16596
- const claimed = await rail.claim().catch(() => void 0);
16597
- if (claimed?.goalId && claimed.revision !== void 0) state.goal = { id: claimed.goalId, revision: claimed.revision };
16963
+ const next = await rail.next().catch(() => void 0);
16964
+ if (next?.goalId && next.revision !== void 0) state.goal = { id: next.goalId, revision: next.revision };
16598
16965
  }
16599
16966
  if (!state.goal) {
16600
- await say();
16967
+ await say(await own());
16601
16968
  return;
16602
16969
  }
16603
16970
  try {
16604
16971
  await say(state.goal.id);
16605
16972
  } catch (error) {
16606
- if (!/goal_parent_invalid/.test(error.message)) throw error;
16607
- opts?.log?.(`\u26A0 the held Goal ${state.goal.id.slice(0, 8)} refused the notice (goal_parent_invalid) \u2014 saying it unplaced`);
16608
- await say();
16973
+ if (!/goal_not_found/.test(error.message)) throw error;
16974
+ opts?.log?.(`\u26A0 the held Goal ${state.goal.id.slice(0, 8)} is gone (goal_not_found) \u2014 saying it on a Goal of its own`);
16975
+ await say(await own());
16609
16976
  }
16610
16977
  } catch (error) {
16611
16978
  opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
16612
16979
  }
16613
16980
  }
16981
+ async function callForResume(text, state, rail) {
16982
+ const { main, units: units3 } = cut(text);
16983
+ const goalId = state.goal?.id ?? (await rail.create({ title: "Usage limit reset", outcome: main })).goalId;
16984
+ return rail.contact({ updates: [{ message: main, ...units3 ? { units: units3 } : {}, goalIds: [goalId], userExplicitlyRequested: "call" }] });
16985
+ }
16614
16986
  function conversationFull(result) {
16615
16987
  return /prompt is too long/i.test(result ?? "");
16616
16988
  }
@@ -16637,14 +17009,14 @@ async function afterTurn(event, state, rail, opts) {
16637
17009
  ${event.result?.trim() || "No result reported."}`, state, rail, opts);
16638
17010
  return void 0;
16639
17011
  }
16640
- await finish(event.result, state, rail, opts);
17012
+ await finish(state, rail, opts);
16641
17013
  state.typed = void 0;
16642
17014
  state.resting = true;
16643
17015
  return void 0;
16644
17016
  }
16645
17017
 
16646
17018
  // src/paigy/conversation.ts
16647
- var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message: it is kept as progress on this Goal. Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's parentId).";
17019
+ var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message, and it is not kept: report where the work stands with a contact update on this Goal (one the person does not need yet is kept as its progress). Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's goalId).";
16648
17020
  var SAFETY_MS = 6e4;
16649
17021
  var ASK_BEAT_MS = 5e3;
16650
17022
  function beat() {
@@ -16744,18 +17116,23 @@ async function gather(state, rail, held) {
16744
17116
  if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
16745
17117
  };
16746
17118
  const waiting = await rail.waiting().catch(() => null);
16747
- const listed = /* @__PURE__ */ new Set();
17119
+ const listed = /* @__PURE__ */ new Map();
17120
+ const list = (goalId, eventId) => {
17121
+ const ids = listed.get(goalId) ?? [];
17122
+ if (eventId && !ids.includes(eventId)) ids.push(eventId);
17123
+ listed.set(goalId, ids);
17124
+ };
16748
17125
  const announced = state.announced ??= /* @__PURE__ */ new Set();
16749
17126
  if (waiting) {
16750
17127
  for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
16751
17128
  }
16752
17129
  for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
16753
17130
  for (const g of waiting?.review ?? []) {
16754
- listed.add(g.goalId);
17131
+ list(g.goalId, g.eventId);
16755
17132
  look2(g.goalId, "waiting work", g.title);
16756
17133
  }
16757
17134
  for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
16758
- listed.add(id3);
17135
+ list(id3, d.deliveryId);
16759
17136
  look2(id3, "waiting work");
16760
17137
  }
16761
17138
  const moved = movedBelow(held, state);
@@ -16780,7 +17157,7 @@ async function gather(state, rail, held) {
16780
17157
  for (const { goal, since } of moved) if (read3.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
16781
17158
  return { heard, listed };
16782
17159
  }
16783
- var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; claim_goal with its id when you take it up.";
17160
+ var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; your first write to it (a contact update naming it) starts it when you take it up.";
16784
17161
  var says = (h) => h.words.length > 0 || h.via === "assigned";
16785
17162
  function whatItIs(h) {
16786
17163
  const outcome2 = h.brief.outcome?.trim() ?? "";
@@ -16810,7 +17187,10 @@ async function hear(session, state, rail, held, opts) {
16810
17187
  if (opts?.tally) opts.tally.announced += goals.length;
16811
17188
  }
16812
17189
  }
16813
- for (const h of heard) if (listed.has(h.goalId)) await acknowledge(h.goalId, h.brief, state, rail, log2);
17190
+ for (const h of heard) {
17191
+ const events = listed.get(h.goalId);
17192
+ if (events) await acknowledge2(h.goalId, h.brief, events, state, rail, log2);
17193
+ }
16814
17194
  }
16815
17195
  var NOTED = "New from the person while you work. Carry on unless it changes what you're doing: it comes back to you as your next turn when this one ends.";
16816
17196
  var SEEN_MID_TURN = "While you worked, the person said this, and you saw it mid-task. Act on it now, unless you already have.";
@@ -16856,15 +17236,16 @@ function settle(session, state, inbox, log2) {
16856
17236
  send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
16857
17237
  log2?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
16858
17238
  }
16859
- async function acknowledge(goalId, brief, state, rail, log2) {
17239
+ async function acknowledge2(goalId, brief, events, state, rail, log2) {
17240
+ const ids = [.../* @__PURE__ */ new Set([...brief.reviewEventId ? [brief.reviewEventId] : [], ...events])];
17241
+ if (!ids.length) return;
16860
17242
  const acked = state.acked ??= /* @__PURE__ */ new Map();
16861
- if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
16862
- acked.set(goalId, brief.revision);
17243
+ const key3 = ids.join(",");
17244
+ if (acked.get(goalId) === key3) return;
17245
+ acked.set(goalId, key3);
16863
17246
  try {
16864
- if (brief.revision === void 0) throw new Error("review_revision_missing");
16865
- const done = await rail.update(goalId, { revision: brief.revision, changes: { reviewed: true }, reason: "Read by the harness session; the person's words on it are typed in." });
16866
- acked.set(goalId, done.revision);
16867
- if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
17247
+ const refused = (await rail.ack(ids)).filter((a) => a.refused);
17248
+ if (refused.length) log2?.(`\u26A0 could not acknowledge ${goalId.slice(0, 8)}: ${refused.map((a) => `${a.eventId.slice(0, 8)} ${a.refused}`).join(", ")} \u2014 its words are typed in; it stays listed until acknowledged`);
16868
17249
  } catch (error) {
16869
17250
  log2?.(`\u26A0 could not acknowledge ${goalId.slice(0, 8)}: ${error.message} \u2014 its words are typed in; it stays listed until acknowledged`);
16870
17251
  }
@@ -16890,13 +17271,13 @@ async function look(session, state, rail, opts) {
16890
17271
  if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
16891
17272
  if (!state.goal) {
16892
17273
  if (!state.resting) return;
16893
- let brief2 = await rail.claim();
17274
+ let brief2 = await rail.next();
16894
17275
  if (!brief2.goalId || brief2.revision === void 0) {
16895
17276
  if (opts?.initialPrompt?.trim()) {
16896
17277
  const prompt = opts.initialPrompt.trim();
16897
17278
  opts.initialPrompt = void 0;
16898
17279
  const admitted = await rail.create({ title: prompt.split(/\s+/).slice(0, 5).join(" ").slice(0, 80), outcome: prompt });
16899
- brief2 = await rail.claim();
17280
+ brief2 = await rail.next();
16900
17281
  if (!brief2.goalId || brief2.revision === void 0) {
16901
17282
  state.goal = { id: admitted.goalId, revision: admitted.revision };
16902
17283
  send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
@@ -16918,7 +17299,7 @@ async function look(session, state, rail, opts) {
16918
17299
  const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
16919
17300
  const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
16920
17301
  send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
16921
- if (ended(brief2.state)) await acknowledge(brief2.goalId, brief2, state, rail, opts?.log);
17302
+ if (ended(brief2.state)) await acknowledge2(brief2.goalId, brief2, [], state, rail, opts?.log);
16922
17303
  return;
16923
17304
  }
16924
17305
  const brief = await rail.read(state.goal.id);
@@ -17006,7 +17387,7 @@ function runHarness(opts) {
17006
17387
  const clock4 = beat();
17007
17388
  let session = null;
17008
17389
  const speaksAs = opts.session ?? opts.resume ?? sessionId();
17009
- const instance = randomUUID5();
17390
+ const instance = randomUUID4();
17010
17391
  const rail = railFor({ token: opts.token, reach: reachAs(speaksAs, instance) });
17011
17392
  let inbox;
17012
17393
  if (opts.harness === "claude") {
@@ -17020,7 +17401,7 @@ function runHarness(opts) {
17020
17401
  function runningAgain() {
17021
17402
  state.carriedOn = false;
17022
17403
  opts.log("\u25B6 running again after the limit");
17023
- LIMITS.running(name, (text) => rail.contact({ asks: [{ ask: text, ...state.goal ? { parentId: state.goal.id } : {} }], waiting: "none", channel: "call" }), opts.log);
17404
+ LIMITS.running(name, (text) => callForResume(text, state, rail), opts.log);
17024
17405
  }
17025
17406
  async function handle(event) {
17026
17407
  if (event.kind === "error") {