@paigy/mcp 0.40.26 → 0.40.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,7 +84,7 @@ args = ["-y", "@paigy/mcp@latest"]
84
84
  env = { PAIGY_AGENT = "codex" }
85
85
  ```
86
86
 
87
- Then pair: `PAIGY_AGENT=codex npx -p @paigy/mcp@latest paigy-mcp-onboard` (or have the agent call the `pair` tool — it pairs under its own name automatically).
87
+ Then pair: `PAIGY_AGENT=codex npx -p @paigy/mcp@latest paigy-mcp-onboard` (or have the agent call the `configure` tool — it pairs under its own name automatically).
88
88
 
89
89
  ### Gemini CLI
90
90
 
@@ -117,11 +117,12 @@ Then pair: `PAIGY_AGENT=gemini npx -p @paigy/mcp@latest paigy-mcp-onboard`.
117
117
  ## Tools
118
118
 
119
119
  One catalog, on every transport (2026-09-11): the agent tools are `AGENT_TOOLS` in
120
- `packages/schema/src/tools.ts` — who_is_working, contact, manage_goals, claim_goal,
121
- get_goal, search, send_feedback — and both this server and the hosted MCP
120
+ `packages/schema/src/tools.ts` — contact, manage_goals, get_goal, search, check_activity,
121
+ send_feedback — and both this server and the hosted MCP
122
122
  (`apps/api/src/mcp`) publish that list and dispatch it through the SDK's one `runTool`.
123
- This server adds only onboard, pair and unpair, the tools that mint and delete the token
124
- file this machine holds, and listen. See the [SDK contract](../../packages/sdk/README.md) for the
123
+ This server adds only `configure`, the one identity tool, which pairs, reports who you are,
124
+ renames and logs this agent out (`logOut: true`) over the token file this machine holds, and listen.
125
+ `onboard` is now `configure`; `pair` and `unpair` are folded into it (2026-10-07). See the [SDK contract](../../packages/sdk/README.md) for the
125
126
  contact shape. Sending returns immediately unless it asks to `wait`: `wait: true` holds one
126
127
  cancellable ~45-second window here for a response to what was sent (the hosted transport
127
128
  returns after one read), and `contact({})` holds one for anything addressed to you; after an
@@ -134,8 +135,10 @@ acknowledged with `ackEventIds`, and a Goal is postponed with manage_goals `defe
134
135
  when its time comes). Durable Entries and accepted answers can
135
136
  repeat on reads; contact with nothing to send receives what is addressed to you (a request the
136
137
  user started toward you, an answer relayed to something you asked, a handoff, a question you owe) without
137
- consuming any of it until you acknowledge it (`ackEventIds`), and claim_goal is the catch-up for
138
- your own Goals. manage_goals creates, edits, assigns, organizes and closes work, a change at a
138
+ consuming any of it until you acknowledge it (`ackEventIds`), and get_goal reads a Goal.
139
+ `claim_goal` is gone (2026-10-07): you are on a Goal from your first write to it (a contact
140
+ naming it, or a manage_goals change), never from a read, so there is nothing to claim or join;
141
+ `who_is_working` is now `check_activity`. manage_goals creates, edits, assigns, organizes and closes work, a change at a
139
142
  time. Bookkeeping (`operationId`, the manage request id, the revision an edit was read at) is
140
143
  minted here, never asked of the model. Retired
141
144
  reply-lease/ACK/work/callback tools and hidden notification aliases are rejected.
@@ -165,10 +168,10 @@ with `paigy-listen --install` (launchd on macOS, systemd user unit on Linux;
165
168
 
166
169
  The read is `receive` (behind `contact({})`) — the open Deliveries addressed to this agent — and it
167
170
  **consumes nothing**: no lease, no acknowledgement, and the same read twice returns the
168
- same list. So the daemon hands the launcher what it found and stops there; claiming is
169
- the agent's own first act (`claim_goal`), because the daemon shares this machine's
170
- token with the agent it launches and a second claimer on one identity eats the first
171
- one's claim.
171
+ same list. So the daemon hands the launcher what it found and stops there; it never
172
+ writes, because an agent is on a Goal from its first write and the daemon shares this
173
+ machine's token with the agent it launches: a write here would put that agent on work it
174
+ never started.
172
175
 
173
176
  To launch an agent — any agent, not just Claude — when work arrives, set
174
177
  `PAIGY_ON_WAKE` to a command before `--install`:
@@ -179,7 +182,7 @@ To launch an agent — any agent, not just Claude — when work arrives, set
179
182
  | `PAIGY_EVENT` | the wake that caused this run — `boot`, `wake:reply`, `wake:request`… |
180
183
  | `PAIGY_PARENT_ID` | the thread to continue on (`PAIGY_THREAD_ID` is the same value, for existing scripts) |
181
184
  | `PAIGY_DELIVERY_ID` | the Delivery being acted on (`get_goal` on `PAIGY_GOAL_ID` reads its conversation) |
182
- | `PAIGY_GOAL_ID` | the Goal it belongs to — `claim_goal` it **first** |
185
+ | `PAIGY_GOAL_ID` | the Goal it belongs to — read it with `get_goal` **first** |
183
186
  | `PAIGY_TEXT` | what the person said, in prose |
184
187
 
185
188
  Hand `$PAIGY_WORK` to a harness that can read JSON and decide for itself; use the
@@ -190,7 +193,7 @@ Goal" from "an empty id".
190
193
 
191
194
  ```sh
192
195
  # Claude Code — hand it everything and let it plan:
193
- PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — claim_goal first, and read a Goal you do not recognize with get_goal."' \
196
+ PAIGY_ON_WAKE='claude -p "Handle the Paigy work in $PAIGY_WORK — read each Goal it names with get_goal first."' \
194
197
  npx -y -p @paigy/mcp paigy-listen --install
195
198
 
196
199
  # Codex terminal session: no custom script and no new conversation.
@@ -234,7 +237,7 @@ script and branch on `$PAIGY_EVENT` there:
234
237
  ```sh
235
238
  #!/bin/sh
236
239
  # ~/.paigy/on-wake.sh — chmod +x, then PAIGY_ON_WAKE=~/.paigy/on-wake.sh
237
- codex exec "Paigy goal $PAIGY_GOAL_ID. claim_goal it first. The user said: $PAIGY_TEXT. Reply with contact when done."
240
+ codex exec "Paigy goal $PAIGY_GOAL_ID. Read it with get_goal first. The user said: $PAIGY_TEXT. Reply with contact when done."
238
241
  ```
239
242
 
240
243
  ## Stalled work (turn-end hook — Claude Code, Codex, Gemini CLI, Antigravity)
@@ -12,10 +12,12 @@ import { z as z6 } from "zod";
12
12
  import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
13
13
  var OPTIONS_MIN = 2;
14
14
  var OPTIONS_MAX = 6;
15
+ var OPTION_LABEL_MAX = 50;
16
+ var OPTION_HINT_MAX = 200;
15
17
  var OptionSchema = z.object({
16
18
  id: z.string(),
17
19
  label: z.string(),
18
- hint: z.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
20
+ hint: z.string().max(500).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
19
21
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
20
22
  // the constraints below are what an agent reads when deciding to use these.
21
23
  html: z.string().max(16384).describe(
@@ -29,8 +31,10 @@ var OptionInputSchema = z.object({
29
31
  id: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
30
32
  `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.`
31
33
  ),
32
- label: z.string().trim().min(1).max(1e3),
33
- hint: OptionSchema.shape.hint,
34
+ // SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
35
+ // a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
36
+ label: z.string().trim().min(1).max(OPTION_LABEL_MAX, `option_label_too_long: an option's label is at most ${OPTION_LABEL_MAX} characters; say the choice, and put its consequence in the hint.`).describe(`The choice, at most ${OPTION_LABEL_MAX} characters, standing on its own.`),
37
+ hint: z.string().max(OPTION_HINT_MAX, `option_hint_too_long: an option's hint is at most ${OPTION_HINT_MAX} characters.`).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
34
38
  htmlPreview: OptionSchema.shape.html,
35
39
  imgUrl: OptionSchema.shape.image
36
40
  }).strict();
@@ -140,7 +144,7 @@ var questionFields = {
140
144
  ),
141
145
  goalId: z2.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
142
146
  units,
143
- options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
147
+ options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
144
148
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
145
149
  // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
146
150
  pickMode: z2.enum(["one", "many", "rank"]).optional().describe(
@@ -219,7 +223,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
219
223
 
220
224
  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.
221
225
 
222
- 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\`; claim_goal starts it) and your work gone quiet (\`stalled\`).`;
226
+ 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\`).`;
223
227
  var CreateGoalSchema = z3.object({
224
228
  outcome: z3.string().trim().min(1).max(1e4),
225
229
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -260,12 +264,13 @@ var UpdateGoalSchema = z3.object({
260
264
  }).strict();
261
265
  var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
262
266
  var goalTitle = z3.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.");
263
- var goalOutcome = z3.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
267
+ var GOAL_OUTCOME_MAX = 500;
268
+ var goalOutcome = z3.string().trim().min(1).max(GOAL_OUTCOME_MAX, `outcome_too_long: a Goal's outcome is one paragraph, at most ${GOAL_OUTCOME_MAX} characters; split the rest into child Goals (create with parentGoalId).`).describe(`The desired result, one paragraph of at most ${GOAL_OUTCOME_MAX} characters. More than that is more work: create child Goals under it (parentGoalId). There is no third description field.`);
264
269
  var CreateChange = z3.object({
265
270
  kind: z3.literal("create"),
266
271
  title: goalTitle,
267
272
  outcome: goalOutcome,
268
- ownerId: z3.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."),
273
+ ownerId: z3.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."),
269
274
  parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
270
275
  sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
271
276
  }).strict();
@@ -286,7 +291,7 @@ var DependencyChange = z3.object({
286
291
  goalId: changedGoal,
287
292
  dependsOnGoalId: z3.string().uuid().describe("The Goal it waits on."),
288
293
  action: z3.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
289
- reason: z3.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
294
+ reason: z3.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
290
295
  }).strict();
291
296
  var read = { revision: z3.number().int().positive().optional() };
292
297
  var GoalChangeSchema = z3.discriminatedUnion("kind", [
@@ -310,8 +315,7 @@ var ManageGoalsSchema = z3.object({
310
315
  var ManageGoalsToolSchema = z3.object({
311
316
  changes: z3.array(z3.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
312
317
  }).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
313
- 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: 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 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.";
314
- var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
318
+ 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 (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
315
319
  var GetGoalSchema = z3.object({
316
320
  goalId: z3.string().uuid(),
317
321
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -322,8 +326,7 @@ var GetGoalSchema = z3.object({
322
326
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
323
327
  diagnose: z3.boolean().optional()
324
328
  }).strict();
325
- 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.";
326
- 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).";
329
+ 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.";
327
330
  var SearchToolSchema = z3.object({
328
331
  query: z3.string().trim().min(1).max(500),
329
332
  types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -331,7 +334,7 @@ var SearchToolSchema = z3.object({
331
334
  limit: z3.number().int().min(1).max(20).optional()
332
335
  }).strict();
333
336
  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.";
334
- var WhoIsWorkingSchema = z3.object({}).strict();
337
+ var CheckActivitySchema = z3.object({}).strict();
335
338
  var FEEDBACK_TEXT_MAX = 5e4;
336
339
  var SendFeedbackSchema = z3.object({
337
340
  text: z3.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.`),
@@ -340,24 +343,23 @@ var SendFeedbackSchema = z3.object({
340
343
  }).strict();
341
344
  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.";
342
345
  var AGENT_TOOLS = [
343
- { 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) },
344
346
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
345
347
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
346
- { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
347
348
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
348
349
  { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
350
+ { 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) },
349
351
  { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
350
352
  ];
351
353
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
352
354
  function serverInstructions(opts) {
353
355
  const waits = opts.waits ? "contact({..., wait:true}) holds one bounded ~45 s window for a response to what you sent; contact({}) holds one for anything addressed to you." : "A contact here returns after one read (`waitOutcome: not_waited`); answers wake you, and contact({wait:false}) collects them.";
354
- return `On startup and after a wake, call contact({wait:false}) for the events addressed to you (acknowledge the ones you handled with contact({ackEventIds})), and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every Goal of your person is open to you, whichever of their agents owns it: read it with get_goal, write on it (a contact naming its id), and once you have written on it you may change it too (manage_goals). Create, edit, assign, organize or close work with manage_goals, never as a contact; a contact only names Goals that already exist. Every read ends in \`next\`, the one step to take. Sending returns immediately unless you ask it to wait: keep working and collect answers by receiving or through claim_goal/get_goal. ${waits} Events repeat until you acknowledge them: reading alone acknowledges nothing; an \`update\` event acknowledged is a review cleared. Report progress as a contact update: one the person does not need yet is kept as the Goal's progress and reaches no one. To follow up later, defer the Goal with manage_goals: you are woken when its time comes. Never infer ringing from an open Call Delivery. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
356
+ return `On startup and after a wake, call contact({wait:false}) for the events addressed to you (acknowledge the ones you handled with contact({ackEventIds})), which hand you your work: answers, assignments, reviews and due Goals. get_goal reads a Goal and the conversation on it; a read never puts you on it. You are on a Goal from your first write to it (a contact update, question or answer naming it, or a manage_goals change); there is nothing to claim or join. Every Goal of your person is open to you, whichever of their agents owns it: read it with get_goal, write on it (a contact naming its id) and change it (manage_goals). Create, edit, assign, organize or close work with manage_goals, never as a contact; a contact only names Goals that already exist. Every read ends in \`next\`, the one step to take. Sending returns immediately unless you ask it to wait: keep working and collect answers by receiving or through get_goal. ${waits} Events repeat until you acknowledge them: reading alone acknowledges nothing; an \`update\` event acknowledged is a review cleared. Report progress as a contact update: one the person does not need yet is kept as the Goal's progress and reaches no one. To follow up later, defer the Goal with manage_goals: you are woken when its time comes. Never infer ringing from an open Call Delivery. Soft waiting and re-presentation are unsupported. Your user is remote. Always interact with the user through Paigy. For decisions, approvals, or questions, contact them with structured options. Never assume anyone is reading the terminal stdout.
355
357
 
356
358
  HOW TO ASK:
357
359
  1. One question per question object. Five questions are five objects in \`questions\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
358
360
  2. Name the Goal you are working on as the question's \`goalId\`: the question goes onto that Goal and its answer comes back there. It must exist: create it with manage_goals first.
359
361
  3. \`workItBlocks\` only for work you are truly blocked on (it asks for a call); leave it out for a question you can keep working around.
360
- 4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`claim_goal\` on that wake.
362
+ 4. Never hold the process open with while-loops. A wait that comes back \`expired\` is not a finished conversation: wait again with contact({wait:true}) only while you have nothing else to do. STOP WAITING when a window comes back with nothing new \u2014 the question stays open, and its answer reaches you on the Goal. Otherwise, yield only with a working listener or scheduled wakeup, and collect answers with \`contact({wait:false})\` or \`get_goal\` on that wake.
361
363
  5. One question, one row. Never restate a question that is still waiting inside a new contact: keep waiting on the original, or the answer lands on one copy and the other stays open. Take back one that no longer matters with \`withdrawQuestionIds\`.
362
364
  6. Read the Goal's conversation before asking. Never ask again what was answered or already shipped \u2014 and tell its states apart: \`answered\` carries the choice in \`answer\`, while \`replied\` means they said something and chose none of your options, so that question is still yours to settle and their words are the line beside it.
363
365
  7. A question carries its options. Without them it reaches the person as a bare title nobody can answer. Each option names its choice in full -- never \`All three\` or \`Option 2\`. \`pickMode: "many"\` makes the card a checklist (options they may want several of); \`"one"\`, the default, a pick (one rules out the others); \`"rank"\` an ordering. Your question is sent exactly as you wrote it. They can always answer in their own words, so no "Other" option.
@@ -408,6 +410,7 @@ var QuestionChangeSchema = z4.union([
408
410
  z4.object({
409
411
  kind: z4.literal("create"),
410
412
  text: z4.string().min(1),
413
+ summary: z4.string().optional(),
411
414
  answererId: participant,
412
415
  goal: ResultRefSchema.optional(),
413
416
  blocks: z4.array(z4.object({ question: ResultRefSchema }).strict()),
@@ -458,12 +461,12 @@ var BrainSearchSchema = z4.object({
458
461
  }).strict();
459
462
  var BrainResultSchema = z4.object({
460
463
  messages: z4.array(BrainMessageSchema),
461
- entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema) }).strict()),
464
+ entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema), summary: z4.string().optional() }).strict()),
462
465
  questions: z4.array(z4.object({ key, entryKeys: z4.array(key), change: QuestionChangeSchema }).strict()),
463
466
  answers: z4.array(z4.object({
464
467
  question: ResultRefSchema,
465
468
  entryKeys: z4.array(key).min(1),
466
- summary: z4.string().min(1),
469
+ summary: z4.string(),
467
470
  selectedOptionIds: z4.array(z4.string().min(1)).optional()
468
471
  }).strict()),
469
472
  goals: z4.array(z4.object({ key, entryKeys: z4.array(key), change: BrainGoalChangeSchema }).strict()),
@@ -508,8 +511,9 @@ var TalkerReplySchema = z5.object({
508
511
  messages: z5.array(z5.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
509
512
  /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
510
513
  then: z5.enum(["listen", "hold", "end"]),
511
- /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
512
- answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2 }).strict()),
514
+ /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles, and
515
+ * the answer in at most ten words, keeping any condition. */
516
+ answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2, summary: str2 }).strict()),
513
517
  /** The line handles that hold a new instruction from the person. */
514
518
  instruction: strs2,
515
519
  /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
@@ -526,8 +530,9 @@ var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
526
530
  var str3 = z6.string();
527
531
  var strs3 = z6.array(z6.string());
528
532
  var FilerReplySchema = z6.object({
529
- /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
530
- filed: z6.array(z6.object({ line: str3, goals: strs3 }).strict()),
533
+ /** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
534
+ * the line says in at most ten words. */
535
+ filed: z6.array(z6.object({ line: str3, goals: strs3, summary: str3 }).strict()),
531
536
  goals: z6.array(z6.object({
532
537
  op: z6.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
533
538
  key: str3,
@@ -546,6 +551,7 @@ var FilerReplySchema = z6.object({
546
551
  key: str3,
547
552
  question: str3,
548
553
  text: str3,
554
+ summary: str3,
549
555
  to: str3,
550
556
  goal: str3,
551
557
  options: z6.array(z6.object({ id: str3, label: str3 }).strict()),
@@ -1265,7 +1271,7 @@ var ConnectionSummarySchema = z7.object({
1265
1271
  * desktop app while open. Null = never seen; stale = offline. */
1266
1272
  lastSeenAt: z7.string().datetime().nullable().optional(),
1267
1273
  /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
1268
- * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
1274
+ * its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
1269
1275
  * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
1270
1276
  lastWorkedAt: z7.string().datetime().nullable().optional(),
1271
1277
  /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
@@ -1463,7 +1469,7 @@ var QueueItemSchema = z7.object({
1463
1469
  *
1464
1470
  * IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
1465
1471
  * writes change which field — the comparison is over the bytes the phone is holding, so a fact
1466
- * the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
1472
+ * the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
1467
1473
  * sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
1468
1474
  * queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
1469
1475
  rev: z7.string().optional()
@@ -1585,7 +1591,7 @@ var HostDecisionSchema = z7.object({
1585
1591
  /** The agent's token id: the row's `recipient`. */
1586
1592
  agent: z7.string().uuid(),
1587
1593
  decision: z7.enum(["stood_back", "took_over"]),
1588
- /** The work it was about: the Goal `claim_goal` would hand that agent next. */
1594
+ /** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
1589
1595
  goalId: z7.string().uuid().nullable().optional(),
1590
1596
  /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
1591
1597
  seenAt: z7.string().datetime().nullable().optional(),
@@ -1778,14 +1784,13 @@ var CallRecapSchema = z7.object({
1778
1784
  owner: z7.string(),
1779
1785
  state: z7.string(),
1780
1786
  questions: z7.array(z7.object({ id: z7.string().uuid(), state: z7.string(), title: z7.string() })),
1781
- /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
1782
- * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
1783
- * summarized like a pre-made option is"), so the row scans like a chosen option and their
1784
- * own words stay under it. Absent on every line stored before the read wrote them, and on
1785
- * anything that is not an answer. */
1787
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
1788
+ * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
1789
+ * gave, so the row scans like a chosen option and their own words stay under it. Absent when
1790
+ * neither wrote one. */
1786
1791
  /** `about` is the request the line answered (its question), null for words that answered none —
1787
1792
  * the key the screen groups on, so one question is one row however many times it was answered. */
1788
- lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), headline: z7.string().optional(), about: z7.string().nullable().optional() }))
1793
+ lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), summary: z7.string().optional(), about: z7.string().nullable().optional() }))
1789
1794
  })),
1790
1795
  unfiled: z7.array(z7.object({ lineId: z7.string().uuid(), words: z7.string(), atMs: z7.number() })),
1791
1796
  more: z7.object({ lines: z7.number(), entries: z7.number(), topics: z7.number() })
@@ -1816,7 +1821,7 @@ next contact({ wait: false }). Start your own listener now, as a BACKGROUND task
1816
1821
 
1817
1822
  Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
1818
1823
  the answer needs a decision. Start it once; if it is already running, do nothing. Then call
1819
- claim_goal once: work handed to you before this session started waits there.
1824
+ contact({ wait: false }) once: work handed to you before this session started waits there.
1820
1825
  PAIGY
1821
1826
  exit 0
1822
1827
  `;
@@ -1852,7 +1857,7 @@ function decideListen(f) {
1852
1857
  return {
1853
1858
  status: "start",
1854
1859
  command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${f.env.CODEX_THREAD_ID ? `CODEX_THREAD_ID=${word(f.env.CODEX_THREAD_ID)} ` : ""}${f.env.PAIGY_ON_WAKE ? `PAIGY_ON_WAKE=${word(f.env.PAIGY_ON_WAKE)} ` : ""}${word(f.execPath)} ${word(f.listenJs)} --brief`,
1855
- how: "Run this command NOW as a watched background task (your harness's background-task/monitor tool), and leave it running for this session. Each line it prints is a Delivery waiting for you: claim_goal the Goal it names, read it, and answer with contact. Start it once."
1860
+ how: "Run this command NOW as a watched background task (your harness's background-task/monitor tool), and leave it running for this session. Each line it prints is a Delivery waiting for you: read the Goal it names with get_goal, and answer with contact. Start it once."
1856
1861
  };
1857
1862
  }
1858
1863
  function listenMark(slot, home = homedir()) {
@@ -1314,10 +1314,12 @@ var zodToJsonSchema = (schema, options) => {
1314
1314
  };
1315
1315
  var OPTIONS_MIN = 2;
1316
1316
  var OPTIONS_MAX = 6;
1317
+ var OPTION_LABEL_MAX = 50;
1318
+ var OPTION_HINT_MAX = 200;
1317
1319
  var OptionSchema = z.object({
1318
1320
  id: z.string(),
1319
1321
  label: z.string(),
1320
- hint: z.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
1322
+ hint: z.string().max(500).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
1321
1323
  // .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
1322
1324
  // the constraints below are what an agent reads when deciding to use these.
1323
1325
  html: z.string().max(16384).describe(
@@ -1331,8 +1333,10 @@ var OptionInputSchema = z.object({
1331
1333
  id: z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9_.:-]+$/).optional().describe(
1332
1334
  `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.`
1333
1335
  ),
1334
- label: z.string().trim().min(1).max(1e3),
1335
- hint: OptionSchema.shape.hint,
1336
+ // SHORT CHOICES (owner, 2026-10-07: "options label should be maybe max 50 characters and the hint 200"):
1337
+ // a label is one choice the person reads, or hears on a call; a hint says its consequence in a line.
1338
+ label: z.string().trim().min(1).max(OPTION_LABEL_MAX, `option_label_too_long: an option's label is at most ${OPTION_LABEL_MAX} characters; say the choice, and put its consequence in the hint.`).describe(`The choice, at most ${OPTION_LABEL_MAX} characters, standing on its own.`),
1339
+ hint: z.string().max(OPTION_HINT_MAX, `option_hint_too_long: an option's hint is at most ${OPTION_HINT_MAX} characters.`).describe(`Optional: what choosing it does, at most ${OPTION_HINT_MAX} characters (e.g. 'Reruns test suite', 'Merges to main').`).optional(),
1336
1340
  htmlPreview: OptionSchema.shape.html,
1337
1341
  imgUrl: OptionSchema.shape.image
1338
1342
  }).strict();
@@ -1610,7 +1614,7 @@ var questionFields = {
1610
1614
  ),
1611
1615
  goalId: z2.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
1612
1616
  units,
1613
- options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own."),
1617
+ options: z2.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
1614
1618
  // HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
1615
1619
  // brain_prompts.md §1: structured agent questions need no model). The app draws all three.
1616
1620
  pickMode: z2.enum(["one", "many", "rank"]).optional().describe(
@@ -1689,7 +1693,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
1689
1693
 
1690
1694
  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.
1691
1695
 
1692
- 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\`; claim_goal starts it) and your work gone quiet (\`stalled\`).`;
1696
+ 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\`).`;
1693
1697
  var CreateGoalSchema = z3.object({
1694
1698
  outcome: z3.string().trim().min(1).max(1e4),
1695
1699
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -1730,12 +1734,13 @@ var UpdateGoalSchema = z3.object({
1730
1734
  }).strict();
1731
1735
  var changedGoal = z3.string().uuid().describe("The Goal to change, as a read shows it.");
1732
1736
  var goalTitle = z3.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.");
1733
- var goalOutcome = z3.string().trim().min(1).max(1e4).describe("The full desired result. There is no third description field.");
1737
+ var GOAL_OUTCOME_MAX = 500;
1738
+ var goalOutcome = z3.string().trim().min(1).max(GOAL_OUTCOME_MAX, `outcome_too_long: a Goal's outcome is one paragraph, at most ${GOAL_OUTCOME_MAX} characters; split the rest into child Goals (create with parentGoalId).`).describe(`The desired result, one paragraph of at most ${GOAL_OUTCOME_MAX} characters. More than that is more work: create child Goals under it (parentGoalId). There is no third description field.`);
1734
1739
  var CreateChange = z3.object({
1735
1740
  kind: z3.literal("create"),
1736
1741
  title: goalTitle,
1737
1742
  outcome: goalOutcome,
1738
- ownerId: z3.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."),
1743
+ ownerId: z3.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."),
1739
1744
  parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
1740
1745
  sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
1741
1746
  }).strict();
@@ -1756,7 +1761,7 @@ var DependencyChange = z3.object({
1756
1761
  goalId: changedGoal,
1757
1762
  dependsOnGoalId: z3.string().uuid().describe("The Goal it waits on."),
1758
1763
  action: z3.enum(["start", "complete"]).describe("What waits: starting goalId, or completing it."),
1759
- reason: z3.string().trim().min(1).max(2e3).optional().describe("Why, as an explanation, never as policy.")
1764
+ reason: z3.string().trim().min(1).max(250).optional().describe("Why, at most 250 characters, as an explanation, never as policy.")
1760
1765
  }).strict();
1761
1766
  var read = { revision: z3.number().int().positive().optional() };
1762
1767
  var GoalChangeSchema = z3.discriminatedUnion("kind", [
@@ -1780,8 +1785,7 @@ var ManageGoalsSchema = z3.object({
1780
1785
  var ManageGoalsToolSchema = z3.object({
1781
1786
  changes: z3.array(z3.discriminatedUnion("kind", [CreateChange, EditChange, StateChange, AssignChange, DeferChange, MoveChange, DependencyChange])).min(1).max(50).describe("The changes, applied in order, each on its own.")
1782
1787
  }).strict().superRefine((v, ctx) => editNames(v.changes, ctx));
1783
- 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: 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 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.";
1784
- var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
1788
+ 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 (an outcome is one paragraph, at most 500 characters: split more into child Goals with parentGoalId); edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
1785
1789
  var GetGoalSchema = z3.object({
1786
1790
  goalId: z3.string().uuid(),
1787
1791
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -1792,8 +1796,7 @@ var GetGoalSchema = z3.object({
1792
1796
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
1793
1797
  diagnose: z3.boolean().optional()
1794
1798
  }).strict();
1795
- 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.";
1796
- 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).";
1799
+ 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.";
1797
1800
  var SearchToolSchema = z3.object({
1798
1801
  query: z3.string().trim().min(1).max(500),
1799
1802
  types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -1801,7 +1804,7 @@ var SearchToolSchema = z3.object({
1801
1804
  limit: z3.number().int().min(1).max(20).optional()
1802
1805
  }).strict();
1803
1806
  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.";
1804
- var WhoIsWorkingSchema = z3.object({}).strict();
1807
+ var CheckActivitySchema = z3.object({}).strict();
1805
1808
  var FEEDBACK_TEXT_MAX = 5e4;
1806
1809
  var SendFeedbackSchema = z3.object({
1807
1810
  text: z3.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.`),
@@ -1810,12 +1813,11 @@ var SendFeedbackSchema = z3.object({
1810
1813
  }).strict();
1811
1814
  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.";
1812
1815
  var AGENT_TOOLS = [
1813
- { 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) },
1814
1816
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
1815
1817
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
1816
- { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
1817
1818
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
1818
1819
  { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
1820
+ { 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) },
1819
1821
  { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
1820
1822
  ];
1821
1823
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
@@ -1864,6 +1866,7 @@ var QuestionChangeSchema = z4.union([
1864
1866
  z4.object({
1865
1867
  kind: z4.literal("create"),
1866
1868
  text: z4.string().min(1),
1869
+ summary: z4.string().optional(),
1867
1870
  answererId: participant,
1868
1871
  goal: ResultRefSchema.optional(),
1869
1872
  blocks: z4.array(z4.object({ question: ResultRefSchema }).strict()),
@@ -1914,12 +1917,12 @@ var BrainSearchSchema = z4.object({
1914
1917
  }).strict();
1915
1918
  var BrainResultSchema = z4.object({
1916
1919
  messages: z4.array(BrainMessageSchema),
1917
- entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema) }).strict()),
1920
+ entries: z4.array(z4.object({ key, sources: z4.array(SourceSchema).min(1), goals: z4.array(ResultRefSchema), summary: z4.string().optional() }).strict()),
1918
1921
  questions: z4.array(z4.object({ key, entryKeys: z4.array(key), change: QuestionChangeSchema }).strict()),
1919
1922
  answers: z4.array(z4.object({
1920
1923
  question: ResultRefSchema,
1921
1924
  entryKeys: z4.array(key).min(1),
1922
- summary: z4.string().min(1),
1925
+ summary: z4.string(),
1923
1926
  selectedOptionIds: z4.array(z4.string().min(1)).optional()
1924
1927
  }).strict()),
1925
1928
  goals: z4.array(z4.object({ key, entryKeys: z4.array(key), change: BrainGoalChangeSchema }).strict()),
@@ -1964,8 +1967,9 @@ var TalkerReplySchema = z5.object({
1964
1967
  messages: z5.array(z5.object({ key: str2, to: str2, text: str2, about: strs2, blocks: str2 }).strict()),
1965
1968
  /** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
1966
1969
  then: z5.enum(["listen", "hold", "end"]),
1967
- /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles. */
1968
- answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2 }).strict()),
1970
+ /** Items the person's words settle: the item's handle, the chosen option IDs, the line handles, and
1971
+ * the answer in at most ten words, keeping any condition. */
1972
+ answers: z5.array(z5.object({ item: str2, options: strs2, lines: strs2, summary: str2 }).strict()),
1969
1973
  /** The line handles that hold a new instruction from the person. */
1970
1974
  instruction: strs2,
1971
1975
  /** The line handles that ask for new work, a change to existing work, or a standing rule: the filer
@@ -1982,8 +1986,9 @@ var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
1982
1986
  var str3 = z6.string();
1983
1987
  var strs3 = z6.array(z6.string());
1984
1988
  var FilerReplySchema = z6.object({
1985
- /** Which Goals each new line goes on: a line's handle and Goal handles (or keys of new Goals). */
1986
- filed: z6.array(z6.object({ line: str3, goals: strs3 }).strict()),
1989
+ /** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
1990
+ * the line says in at most ten words. */
1991
+ filed: z6.array(z6.object({ line: str3, goals: strs3, summary: str3 }).strict()),
1987
1992
  goals: z6.array(z6.object({
1988
1993
  op: z6.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
1989
1994
  key: str3,
@@ -2002,6 +2007,7 @@ var FilerReplySchema = z6.object({
2002
2007
  key: str3,
2003
2008
  question: str3,
2004
2009
  text: str3,
2010
+ summary: str3,
2005
2011
  to: str3,
2006
2012
  goal: str3,
2007
2013
  options: z6.array(z6.object({ id: str3, label: str3 }).strict()),
@@ -2735,7 +2741,7 @@ var ConnectionSummarySchema = z7.object({
2735
2741
  * desktop app while open. Null = never seen; stale = offline. */
2736
2742
  lastSeenAt: z7.string().datetime().nullable().optional(),
2737
2743
  /** WORKING, NOT JUST CONNECTED (owner, 2026-09-30): the last time the agent itself acted on one of
2738
- * its Goals — took its lease or recorded an operation (`tokens.last_worked_at`). Within
2744
+ * its Goals — wrote on one or recorded an operation (`tokens.last_worked_at`). Within
2739
2745
  * `WORKING_MS` it is working; otherwise it is connected but idle. Null = not seen working yet. */
2740
2746
  lastWorkedAt: z7.string().datetime().nullable().optional(),
2741
2747
  /** The oldest of its Goals that is `ready` for it — work handed to it that nobody has started.
@@ -2933,7 +2939,7 @@ var QueueItemSchema = z7.object({
2933
2939
  *
2934
2940
  * IT IS THE PAYLOAD'S OWN HASH, NEVER A STAMP ON THE WORK. Nothing here reasons about which
2935
2941
  * writes change which field — the comparison is over the bytes the phone is holding, so a fact
2936
- * the row shows that no `updated_at` moves for (a lease lapsing, a dependency's state, a
2942
+ * the row shows that no `updated_at` moves for (`active` lapsing, a dependency's state, a
2937
2943
  * sibling appearing in `childGoalIds`) cannot go unnoticed. Optional because a hand-built
2938
2944
  * queue (a fixture, the demo) spells none, and a row with no rev is simply always re-sent. */
2939
2945
  rev: z7.string().optional()
@@ -3057,7 +3063,7 @@ var HostDecisionSchema = z7.object({
3057
3063
  /** The agent's token id: the row's `recipient`. */
3058
3064
  agent: z7.string().uuid(),
3059
3065
  decision: z7.enum(["stood_back", "took_over"]),
3060
- /** The work it was about: the Goal `claim_goal` would hand that agent next. */
3066
+ /** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
3061
3067
  goalId: z7.string().uuid().nullable().optional(),
3062
3068
  /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
3063
3069
  seenAt: z7.string().datetime().nullable().optional(),
@@ -3250,14 +3256,13 @@ var CallRecapSchema = z7.object({
3250
3256
  owner: z7.string(),
3251
3257
  state: z7.string(),
3252
3258
  questions: z7.array(z7.object({ id: z7.string().uuid(), state: z7.string(), title: z7.string() })),
3253
- /** `words` is always what they SAID, verbatim — the record, never replaced. `headline` is
3254
- * their answer on one line when the call's read wrote one (owner, 2026-10-01: "render them
3255
- * summarized like a pre-made option is"), so the row scans like a chosen option and their
3256
- * own words stay under it. Absent on every line stored before the read wrote them, and on
3257
- * anything that is not an answer. */
3259
+ /** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
3260
+ * the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
3261
+ * gave, so the row scans like a chosen option and their own words stay under it. Absent when
3262
+ * neither wrote one. */
3258
3263
  /** `about` is the request the line answered (its question), null for words that answered none —
3259
3264
  * the key the screen groups on, so one question is one row however many times it was answered. */
3260
- lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), headline: z7.string().optional(), about: z7.string().nullable().optional() }))
3265
+ lines: z7.array(z7.object({ entryId: z7.string().uuid(), words: z7.string(), summary: z7.string().optional(), about: z7.string().nullable().optional() }))
3261
3266
  })),
3262
3267
  unfiled: z7.array(z7.object({ lineId: z7.string().uuid(), words: z7.string(), atMs: z7.number() })),
3263
3268
  more: z7.object({ lines: z7.number(), entries: z7.number(), topics: z7.number() })
@@ -3438,12 +3443,6 @@ async function manageGoals(input, opts = {}) {
3438
3443
  if (!res.ok) await fail("manage_goals", res);
3439
3444
  return await res.json();
3440
3445
  }
3441
- async function claimGoal(goalId, opts = {}) {
3442
- const token = authToken(opts.token) ?? "";
3443
- 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 } : {}) }));
3444
- if (!res.ok) await fail("claim_goal", res);
3445
- return await res.json();
3446
- }
3447
3446
  async function getGoal(goalId, opts = {}, read3 = {}) {
3448
3447
  const token = authToken(opts.token) ?? "";
3449
3448
  const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
@@ -3622,11 +3621,11 @@ async function sendFeedback(input, opts = {}) {
3622
3621
  if (!res.ok) await fail("send_feedback", res);
3623
3622
  return await res.json();
3624
3623
  }
3625
- async function whoIsWorking(opts = {}) {
3624
+ async function checkActivity(opts = {}) {
3626
3625
  const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
3627
3626
  headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
3628
3627
  }));
3629
- if (!res.ok) await fail("who_is_working", res);
3628
+ if (!res.ok) await fail("check_activity", res);
3630
3629
  return await res.json();
3631
3630
  }
3632
3631
  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.`;
@@ -3861,7 +3860,9 @@ function goalView(g) {
3861
3860
  })) : void 0,
3862
3861
  blockedBy: g.blockedDependencyGoalIds,
3863
3862
  dueAt: g.dueAt,
3864
- leaseExpiresAt: g.leaseExpiresAt,
3863
+ // Who last wrote on it, and when: any agent can work on any Goal, and working on it is a write.
3864
+ workedBy: g.workedBy,
3865
+ workedAt: g.workedAt,
3865
3866
  conversation: lines2,
3866
3867
  next: owes ? `${owes}${g.message ?? ""}`.trim() : g.message,
3867
3868
  more: more(g)
@@ -3905,7 +3906,7 @@ function deliveryView(d) {
3905
3906
  acknowledged: d.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
3906
3907
  next: [
3907
3908
  failures(d.results),
3908
- d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? `${d.demoted ? `${d.demoted} ` : ""}${d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers}` : pending ? `${d.demoted ? `${d.demoted} ` : ""}Decision pending on this call. ${d.waitOutcome === "expired" ? "Nothing was said in the window. " : ""}Wait for the answer with contact({wait:true}) \u2014 one bounded window each time, and never resend the question. When a window comes back with nothing new, they are not typing: stop waiting, leave the question open, and collect the answer with contact({wait:false}) or claim_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
3909
+ d.joinedCall ? `${JOINED_CALL}${d.message ? ` ${d.message}` : ""}` : d.kind === "notification" ? `${d.demoted ? `${d.demoted} ` : ""}${d.joinedCard ? `Added to the open report card on its Goal; no new push was sent. ${answers}` : answers}` : pending ? `${d.demoted ? `${d.demoted} ` : ""}Decision pending on this call. ${d.waitOutcome === "expired" ? "Nothing was said in the window. " : ""}Wait for the answer with contact({wait:true}) \u2014 one bounded window each time, and never resend the question. When a window comes back with nothing new, they are not typing: stop waiting, leave the question open, and collect the answer with contact({wait:false}) or get_goal on your next wake. ${d.message ?? ""}`.trim() : d.message
3909
3910
  ].filter(Boolean).join(" ")
3910
3911
  });
3911
3912
  }
@@ -3942,8 +3943,8 @@ function receivedView(r, now = Date.now()) {
3942
3943
  notAcknowledged: refused.length ? refused.map((a) => ({ eventId: a.eventId, refused: a.refused })) : void 0,
3943
3944
  // WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they became yours.
3944
3945
  assigned,
3945
- // WHAT claim_goal HANDS YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
3946
- // sleeping agent, so "nothing is waiting" is never said while a claim would hand you work.
3946
+ // THE WORK WAITING ON YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
3947
+ // sleeping agent, so "nothing is waiting" is never said while work is.
3947
3948
  claimable: w.claimable,
3948
3949
  // YOUR STALLED WORK (#2257), and OTHER AGENTS' (owner, 2026-09-23): any agent may take it over.
3949
3950
  stalled,
@@ -3953,10 +3954,10 @@ function receivedView(r, now = Date.now()) {
3953
3954
  ...owed.length ? [`You owe ${owed.length === 1 ? "an answer" : `${owed.length} answers`}: contact({ answers: [{ questionId: "<questionId>", answer: { text: <your answer> } }] }) for each question; a question is answered, never acknowledged.`] : [],
3954
3955
  ...r.hasMore ? ["More are waiting: acknowledge these, then receive again."] : [],
3955
3956
  ...r.waitOutcome === "expired" ? ["Nothing arrived in the window; receive again to keep waiting, without sending again."] : [],
3956
- ...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): claim_goal({goalId}) to start one.`] : [],
3957
- ...w.claimable && !assigned.length ? [`claim_goal() hands you ${w.claimable.title ?? w.claimable.goalId} next.`] : [],
3957
+ ...assigned.length ? [`${assigned.length} of your Goals are assigned to you and not started (assigned): read one with get_goal({goalId}); your first write to it starts it.`] : [],
3958
+ ...w.claimable && !assigned.length ? [`Next waiting on you: ${w.claimable.title ?? w.claimable.goalId} (get_goal({ goalId: "${w.claimable.goalId}" })).`] : [],
3958
3959
  ...stalled.length ? [`${stalled.length} of your Goals have had no progress${stalled.length === 1 ? " " : ""}${quietFor(stalled, now)}: report on each with a contact update, or finish or cancel it with manage_goals.`] : [],
3959
- ...others.length ? [`${others.length} of other agents' Goals have gone quiet${others.length === 1 ? " " : ""}${quietFor(others, now)}: claim_goal({goalId}) joins one without changing its owner.`] : [],
3960
+ ...others.length ? [`${others.length} of other agents' Goals have gone quiet${others.length === 1 ? " " : ""}${quietFor(others, now)}: read one with get_goal({goalId}); a write to it puts you on it without changing its owner.`] : [],
3960
3961
  // No name of its own (#2525): the API's ask, verbatim.
3961
3962
  ...w.unnamed ? [w.unnamed] : []
3962
3963
  ].join(" ")
@@ -3993,9 +3994,9 @@ async function runTool(name, args, opts) {
3993
3994
  const sent = await contact(parsed, { ...client, signal, waits });
3994
3995
  return "sent" in sent ? notSentView(sent) : deliveryView(sent);
3995
3996
  }
3996
- case "who_is_working":
3997
- WhoIsWorkingSchema.parse(input);
3998
- return whoIsWorking(client);
3997
+ case "check_activity":
3998
+ CheckActivitySchema.parse(input);
3999
+ return checkActivity(client);
3999
4000
  case "send_feedback":
4000
4001
  return sendFeedback(SendFeedbackSchema.parse(input), client);
4001
4002
  case "manage_goals": {
@@ -4009,10 +4010,6 @@ async function runTool(name, args, opts) {
4009
4010
  for (const g of managed.goals ?? []) remember(client.token, g.goalId, g.revision);
4010
4011
  return managedView(managed, changes);
4011
4012
  }
4012
- case "claim_goal": {
4013
- const { goalId } = ClaimGoalSchema.parse(input);
4014
- return goalView(read2(client.token, await claimGoal(goalId, client)));
4015
- }
4016
4013
  case "get_goal": {
4017
4014
  const { goalId, history, diagnose } = GetGoalSchema.parse(input);
4018
4015
  const goal = read2(client.token, await getGoal(goalId, client, { history, diagnose }));
@@ -4287,7 +4284,6 @@ export {
4287
4284
  overrideToken,
4288
4285
  authToken,
4289
4286
  manageGoals,
4290
- claimGoal,
4291
4287
  getGoal,
4292
4288
  AWAIT_WINDOW_MS,
4293
4289
  hatch,
@@ -4307,7 +4303,7 @@ export {
4307
4303
  acceptTriage,
4308
4304
  dismissTriage,
4309
4305
  searchRecords,
4310
- whoIsWorking,
4306
+ checkActivity,
4311
4307
  readDelivery,
4312
4308
  contact,
4313
4309
  receive,
@@ -6,7 +6,7 @@ import {
6
6
  saveToken,
7
7
  setIdentity,
8
8
  sleep
9
- } from "./chunk-OVSAQW3K.js";
9
+ } from "./chunk-AHLJPVEP.js";
10
10
 
11
11
  // src/identity.ts
12
12
  var CLIENT_LABELS = {
@@ -34,12 +34,12 @@ async function identify(who, ask) {
34
34
  who = await setIdentity({ ...ask.name ? { name: ask.name } : {}, ...ask.voice ? { voice: ask.voice } : {} });
35
35
  renamed = true;
36
36
  } catch (e) {
37
- const message2 = e instanceof NameTakenError ? e.message : `The rename did not go through (${e.message}); nothing changed. Call onboard again to retry, or carry on.`;
37
+ const message2 = e instanceof NameTakenError ? e.message : `The rename did not go through (${e.message}); nothing changed. Call configure again to retry, or carry on.`;
38
38
  return { name: who.name, voice: who.voice, message: message2 };
39
39
  }
40
40
  }
41
41
  const voice = who.voice ? ` (voice: ${who.voice})` : "";
42
- const message = !who.name ? [renamed && who.voice ? `Your voice is ${who.voice} now.` : "", who.unnamed ?? "Set up, with no name yet."].filter(Boolean).join(" ") : renamed ? `Renamed \u2014 this agent is "${who.name}"${voice} now.` : `Already set up as "${who.name}"${voice}. Ask the user if they want to rename it or unpair; otherwise carry on.`;
42
+ const message = !who.name ? [renamed && who.voice ? `Your voice is ${who.voice} now.` : "", who.unnamed ?? "Set up, with no name yet."].filter(Boolean).join(" ") : renamed ? `Renamed \u2014 this agent is "${who.name}"${voice} now.` : `Already set up as "${who.name}"${voice}. Ask the user if they want to rename it or log it out; otherwise carry on.`;
43
43
  return { name: who.name, voice: who.voice, message };
44
44
  }
45
45
 
@@ -4,54 +4,38 @@ import {
4
4
  serverInstructions,
5
5
  sessionStartHook,
6
6
  withSessionStartHook
7
- } from "./chunk-7LQKX73K.js";
7
+ } from "./chunk-632JM6QA.js";
8
8
  import {
9
9
  CODEX_ENV,
10
10
  agentName,
11
11
  withCodexEnv
12
- } from "./chunk-OVSAQW3K.js";
12
+ } from "./chunk-AHLJPVEP.js";
13
13
 
14
14
  // src/toolset.ts
15
- var ONBOARD_DESCRIPTION = "Get this agent talking to Paigy \u2014 call it FIRST, before contact, and any time you're unsure who you are. One call, and it does whatever the situation needs: NOT SET UP \u2192 hatches an identity instantly if this machine holds a device credential (the user ran the Paigy desktop app or harness), otherwise starts the code ceremony; ALREADY SET UP \u2192 returns your current identity and offers the two things left to decide, renaming it or unpairing; TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only. If it returns a `user_code`, print it to the user immediately and call onboard again with the `device_code`. If it returns `enable_command`, PRINT that command for the user to run \u2014 you cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.";
15
+ var CONFIGURE_DESCRIPTION = "Set this agent up with Paigy \u2014 call it FIRST, before contact, and any time you're unsure who you are. One call does whatever the situation needs: NOT SET UP \u2192 if this machine holds a device credential (the user ran the Paigy desktop app or harness) it hatches an identity INSTANTLY, no code and no approval; otherwise it starts the code ceremony. ALREADY SET UP \u2192 returns your current identity and the two things left to decide, renaming it or logging out. TOKEN NO LONGER VALID \u2192 says so, then re-pairs. Pass { name, voice } to choose who you are when hatching, or to RENAME yourself when already set up (pick your own; voices: rachel, george, jessica, brian, lily). Safe to call any time: idempotent, and it never writes settings \u2014 the tool-allowlist state it reports is read-only.\n\nTHE CODE CEREMONY does not open a browser; the user enters the code in the Paigy app (or scans `qr`). A result with { user_code, device_code, qr, user_message } has already started polling for approval in the background: you MUST immediately print the `user_message` (the bare code) to the user, AND in that same turn call configure again with that `device_code` to collect the result. It returns the moment the user approves; on { status:'pending' } call again to keep waiting; on { status:'awaiting_confirmation' } (E2EE) show the bare `user_message` verify code and call again to finish. The leading text block of every result states the code plainly, so it shows even if you emit no prose.\n\nOn { status:'paired' }, or whenever it returns `enable_command`, ALWAYS follow the `enable_prompt`: PRINT that command for the user to run, so Paigy's tools stop prompting each time. You cannot apply it yourself (it writes your own permission allowlist, which hosts block as privilege escalation), so print it, don't wait for it, and carry on.\n\nLOG OUT with { logOut: true }: it logs out THIS agent only \u2014 its token is revoked server-side and its local entry removed. Other agents and this device's credential are untouched, and a later configure re-hatches instantly while the device credential exists. Disconnecting the device itself is not an agent tool.";
16
16
  var SERVER_INSTRUCTIONS = serverInstructions({ waits: true });
17
17
 
18
18
  // src/tools.ts
19
19
  import { z } from "zod";
20
- var OnboardSchema = z.object({
21
- name: z.string().max(60).optional(),
22
- voice: z.string().max(40).optional(),
23
- /** Continue a code ceremony already in flight — same meaning as `pair`'s. */
24
- device_code: z.string().optional(),
25
- /** Which allowlist to REPORT on (never written by `start`). */
26
- scope: z.enum(["user", "project"]).optional()
27
- });
28
- var PairSchema = z.object({
29
- device_code: z.string().optional().describe("Omit to start pairing (returns an approval link to show the user). Pass the device_code from that first call to finish, once the user has approved."),
30
- name: z.string().min(1).max(60).optional().describe("Hatch path only: the name you choose for this identity. Pick your own \u2014 ONE or TWO words, the way you'd introduce yourself on a call (it is spoken aloud and shown in lists). 'Piper', 'Blue Heron' \u2014 never a sentence or a task description."),
31
- voice: z.string().optional().describe("Hatch path only: your voice on calls \u2014 one of rachel, george, jessica, brian, lily.")
20
+ var ConfigureSchema = z.object({
21
+ name: z.string().min(1).max(60).optional().describe("Who you are: the name you choose when hatching, or the new one to RENAME yourself to when already set up. Pick your own \u2014 ONE or TWO words, the way you'd introduce yourself on a call (it is spoken aloud and shown in lists). 'Piper', 'Blue Heron' \u2014 never a sentence or a task description."),
22
+ voice: z.string().max(40).optional().describe("Your voice on calls \u2014 one of rachel, george, jessica, brian, lily."),
23
+ device_code: z.string().optional().describe("Continue a code ceremony already in flight: the device_code a previous configure returned. Omit it otherwise."),
24
+ /** Which allowlist to REPORT on (never written). */
25
+ scope: z.enum(["user", "project"]).optional().describe("Which tool allowlist to report on (user or project settings). Read-only."),
26
+ logOut: z.boolean().optional().describe("true: log THIS agent out \u2014 its token is revoked and its local entry removed. Other agents and this device's credential are untouched.")
32
27
  });
33
28
  var IDENTITY_TOOLS = [
34
29
  {
35
- // ONE rail for starting a session (#875). `pair`, the `paigy-mcp-onboard` CLI and
36
- // `enable_tools` were three doors into one flow, and "onboard" was the word people
37
- // reached for attached to the tool that did the least. It's the door now.
38
- name: "onboard",
39
- description: ONBOARD_DESCRIPTION,
40
- inputSchema: mcpInputSchema(OnboardSchema)
41
- },
42
- {
43
- name: "pair",
44
- description: "Pair this agent with the user's Paigy account (one-time) \u2014 required before contact work. FAST PATH: if this machine already holds a device credential (the user ran the Paigy desktop harness or app), calling pair hatches a fresh identity INSTANTLY \u2014 no code, no approval. Pass { name, voice } to choose who you are (pick your own; voices: rachel, george, jessica, brian, lily). Only when no device credential exists does the code ceremony below run. It does NOT open a browser; the user enters the code in the Paigy app (or scans `qr`). Step 1: call with NO args \u2014 returns { user_code, device_code, qr, user_message } AND starts polling for approval in the background. REQUIRED: You MUST immediately print the `user_message` (the bare code) as a text message to the user, AND in that same turn call step 2 (pair with the device_code). This ensures the user sees the code in chat while the tool blocks/polls in the background for approval. Step 2: call with that device_code to collect the result. Because approval is already being polled in the background, this returns the moment the user approves; on { status:'pending' } just call again to keep waiting; on { status:'awaiting_confirmation' } (E2EE) show the bare `user_message` verify code and call again to finish. The leading text block of every result states the code plainly, so it shows even if you emit no prose. On { status:'paired' } ALWAYS follow the `enable_prompt` \u2014 PRINT the returned `enable_command` so the user can allowlist Paigy's tools and notify/await stop prompting each time. Printing is the whole job: that command writes your own permission allowlist, so you must not run it and a host will block you if you try.",
45
- inputSchema: mcpInputSchema(PairSchema)
46
- },
47
- {
48
- name: "unpair",
49
- description: "Log out / unpair this agent from the user's Paigy account: revokes the token server-side (it stops working everywhere) and deletes the local ~/.paigy/token.json. Takes no arguments. After this, contact won't work until the user pairs again with the pair tool.",
50
- inputSchema: mcpInputSchema(z.object({}))
30
+ // ONE rail for starting a session (#875), and since 2026-10-07 the only identity tool: `onboard`
31
+ // renamed, and `pair` (which it already did everything of) and `unpair` (`logOut`) folded in.
32
+ name: "configure",
33
+ description: CONFIGURE_DESCRIPTION,
34
+ inputSchema: mcpInputSchema(ConfigureSchema)
51
35
  },
52
36
  {
53
37
  name: "listen",
54
- description: "Am I listening for answers, and can Paigy resume this session? Takes no arguments. Returns status `listening` for harness delivery or a running listener with a ready wake adapter; `receiving` means a process is running but automatic resume is not confirmed (read its wake error or watch its output); `unpaired` means call onboard; `start` gives the exact command to run NOW as a watched background task for this session. Start it once. Codex queues into CODEX_THREAD_ID automatically when its CLI supports queue; PAIGY_ON_WAKE overrides it. A successful queue is not evidence the agent handled the answer: receive with contact({ wait: false }) and read the relevant Goal, then respond with contact. This tool never subscribes or starts anything itself.",
38
+ description: "Am I listening for answers, and can Paigy resume this session? Takes no arguments. Returns status `listening` for harness delivery or a running listener with a ready wake adapter; `receiving` means a process is running but automatic resume is not confirmed (read its wake error or watch its output); `unpaired` means call configure; `start` gives the exact command to run NOW as a watched background task for this session. Start it once. Codex queues into CODEX_THREAD_ID automatically when its CLI supports queue; PAIGY_ON_WAKE overrides it. A successful queue is not evidence the agent handled the answer: receive with contact({ wait: false }) and read the relevant Goal, then respond with contact. This tool never subscribes or starts anything itself.",
55
39
  inputSchema: mcpInputSchema(z.object({}))
56
40
  }
57
41
  ];
@@ -218,9 +202,8 @@ function claudeInstallHint(skip = agentName()) {
218
202
  return "Claude Code detected \u2014 to add Paigy there, run /plugin marketplace add paigy-ai/mcp then /plugin install paigy (pairing carries over; no need to pair again). Note: If you run this inside a live session, type /reload-plugins afterward so the agent connects to the new tools.";
219
203
  }
220
204
  var ENABLE_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
221
- var PROMPTS_ON_PURPOSE = /* @__PURE__ */ new Set(["pair", "unpair"]);
222
205
  var PAIGY_TOOL_IDS = [
223
- ...TOOL_NAMES.filter((n) => !PROMPTS_ON_PURPOSE.has(n)),
206
+ ...TOOL_NAMES,
224
207
  ...HIDDEN_ALIASES
225
208
  ].map((n) => `mcp__paigy__${n}`);
226
209
  function withPaigyAllowlist(existing, tools) {
@@ -332,8 +315,7 @@ function installSessionListening() {
332
315
 
333
316
  export {
334
317
  SERVER_INSTRUCTIONS,
335
- OnboardSchema,
336
- PairSchema,
318
+ ConfigureSchema,
337
319
  TOOLS,
338
320
  openBrowser,
339
321
  autoConfigureClients,
@@ -17,7 +17,7 @@ import {
17
17
  acknowledge,
18
18
  agentName,
19
19
  authToken,
20
- claimGoal,
20
+ checkActivity,
21
21
  claimSessionEnds,
22
22
  claimSessions,
23
23
  contact,
@@ -64,9 +64,8 @@ import {
64
64
  subscribeWake,
65
65
  updateSlot,
66
66
  whoAmI,
67
- whoIsWorking,
68
67
  withCodexEnv
69
- } from "./chunk-OVSAQW3K.js";
68
+ } from "./chunk-AHLJPVEP.js";
70
69
  export {
71
70
  AGENT_TOOLS,
72
71
  AGENT_TOOL_NAMES,
@@ -86,7 +85,7 @@ export {
86
85
  acknowledge,
87
86
  agentName,
88
87
  authToken,
89
- claimGoal,
88
+ checkActivity,
90
89
  claimSessionEnds,
91
90
  claimSessions,
92
91
  contact,
@@ -133,6 +132,5 @@ export {
133
132
  subscribeWake,
134
133
  updateSlot,
135
134
  whoAmI,
136
- whoIsWorking,
137
135
  withCodexEnv
138
136
  };
package/dist/enable.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  PAIGY_TOOL_IDS,
4
4
  enablePaigyTools,
5
5
  installSessionListening
6
- } from "./chunk-5MQ3J6S3.js";
7
- import "./chunk-7LQKX73K.js";
8
- import "./chunk-OVSAQW3K.js";
6
+ } from "./chunk-ZP6NBVEE.js";
7
+ import "./chunk-632JM6QA.js";
8
+ import "./chunk-AHLJPVEP.js";
9
9
 
10
10
  // src/enable.ts
11
11
  function main() {
@@ -16,14 +16,14 @@ function main() {
16
16
  if (arg === "-h" || arg === "--help") {
17
17
  console.log("usage: paigy-enable-tools [--scope user|project]");
18
18
  console.log("");
19
- console.log("Allowlist Paigy's notify/await tools in Claude Code so they run without an");
19
+ console.log("Allowlist Paigy's tools in Claude Code so they run without an");
20
20
  console.log("approval prompt each time \u2014 an unattended session can't stall on a dialog.");
21
21
  console.log("");
22
22
  console.log(" --scope user ~/.claude/settings.json (default) \u2014 every project");
23
23
  console.log(" --scope project ./.claude/settings.json \u2014 just this repo");
24
24
  console.log("");
25
25
  console.log(`Adds: ${PAIGY_TOOL_IDS.join(", ")}`);
26
- console.log("Leaves pair/unpair out, so they stay human-approved. Safe to re-run.");
26
+ console.log("Safe to re-run.");
27
27
  return;
28
28
  }
29
29
  if (arg === "--scope") {
package/dist/index.js CHANGED
@@ -9,23 +9,22 @@ import {
9
9
  resolvePairing,
10
10
  startPairing,
11
11
  suggestedAgentName
12
- } from "./chunk-BLWC44CQ.js";
12
+ } from "./chunk-WKAXAJEM.js";
13
13
  import {
14
+ ConfigureSchema,
14
15
  ENABLE_COMMAND,
15
- OnboardSchema,
16
16
  PAIGY_TOOL_IDS,
17
- PairSchema,
18
17
  SERVER_INSTRUCTIONS,
19
18
  TOOLS,
20
19
  paigyToolsAllowlisted
21
- } from "./chunk-5MQ3J6S3.js";
20
+ } from "./chunk-ZP6NBVEE.js";
22
21
  import {
23
22
  decideListen,
24
23
  existingListener,
25
24
  harnessDriven,
26
25
  listenerKey,
27
26
  wakeStatusPath
28
- } from "./chunk-7LQKX73K.js";
27
+ } from "./chunk-632JM6QA.js";
29
28
  import {
30
29
  clearSurface,
31
30
  writeSurface
@@ -46,7 +45,7 @@ import {
46
45
  setClient,
47
46
  slotName,
48
47
  whoAmI
49
- } from "./chunk-OVSAQW3K.js";
48
+ } from "./chunk-AHLJPVEP.js";
50
49
 
51
50
  // src/index.ts
52
51
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -113,7 +112,7 @@ function pairedResult(token, sas, note) {
113
112
  if (note) base.note = note;
114
113
  base.enable_tool_ids = PAIGY_TOOL_IDS;
115
114
  base.enable_command = ENABLE_COMMAND;
116
- base.enable_prompt = "REQUIRED \u2014 before you do anything else, print `enable_command` to the user and say: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists the notify/await tools; pair and unpair still ask.\" Print it every time you pair; don't skip it.";
115
+ base.enable_prompt = "REQUIRED \u2014 before you do anything else, print `enable_command` to the user and say: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists Paigy's own tools.\" Print it every time you pair; don't skip it.";
117
116
  base.enable_apply = "You cannot apply this yourself, and must not try: it writes YOUR OWN permission allowlist, which hosts block as privilege escalation no matter what the user said here. PRINT the command and move on \u2014 don't wait for them to run it, and don't re-ask. Paigy's tools still work meanwhile; they just prompt for approval each time until the user runs it. If the host isn't Claude Code, tell them to add `enable_tool_ids` to that tool's own allowlist instead, merging \u2014 never clobbering.";
118
117
  return { content: [{ type: "text", text: JSON.stringify(base) }] };
119
118
  }
@@ -130,7 +129,7 @@ Check it matches the code on the phone, then type it there to confirm.` },
130
129
  sas,
131
130
  device_code,
132
131
  user_message: `# ${sas}`,
133
- verify_message: `STOP HERE: end your turn now with \`user_message\` (the bare code) as your entire reply \u2014 do NOT call pair again in this same turn, or the code text is dropped before the user sees it. This is the verification code they compare + type on their phone; it REPLACES any earlier one. Poll for their confirmation by calling pair with this device_code on your NEXT turn.`,
132
+ verify_message: `STOP HERE: end your turn now with \`user_message\` (the bare code) as your entire reply \u2014 do NOT call configure again in this same turn, or the code text is dropped before the user sees it. This is the verification code they compare + type on their phone; it REPLACES any earlier one. Poll for their confirmation by calling configure with this device_code on your NEXT turn.`,
134
133
  message: "No token is issued until the user confirms the code match on their phone (E2EE account)."
135
134
  })
136
135
  }
@@ -152,7 +151,7 @@ function renderPairOutcome(outcome, device_code) {
152
151
  text: JSON.stringify({
153
152
  status: "pending",
154
153
  device_code,
155
- message: "Still awaiting approval \u2014 the user hasn't entered the code yet. Call pair again with this device_code RIGHT NOW to keep waiting; each call returns after ~45s so it can't be killed by a host tool-call timeout. Nothing is lost between calls: the code stays valid and approval keeps being polled in the background, so a re-call returns the instant they approve."
154
+ message: "Still awaiting approval \u2014 the user hasn't entered the code yet. Call configure again with this device_code RIGHT NOW to keep waiting; each call returns after ~45s so it can't be killed by a host tool-call timeout. Nothing is lost between calls: the code stays valid and approval keeps being polled in the background, so a re-call returns the instant they approve."
156
155
  })
157
156
  }]
158
157
  };
@@ -162,7 +161,7 @@ function unidentifiedResult() {
162
161
  return { isError: true, content: [{ type: "text", text: JSON.stringify({
163
162
  error: "no_identity",
164
163
  slot: agentName(),
165
- message: `This session has no Paigy identity yet, so nothing was sent \u2014 it will not speak as another session. This machine holds a device credential but hatching under it just failed (it may have been revoked), so call \`pair\` (no arguments) to set this session up, then retry \u2014 it re-hatches if the credential recovered and runs the code ceremony if it didn't. To reuse an existing identity instead, start the session with PAIGY_AGENT set to its slot (${listSlots().filter((s) => s !== "Desktop").join(", ") || "none yet"}).`
164
+ message: `This session has no Paigy identity yet, so nothing was sent \u2014 it will not speak as another session. This machine holds a device credential but hatching under it just failed (it may have been revoked), so call \`configure\` (no arguments) to set this session up, then retry \u2014 it re-hatches if the credential recovered and runs the code ceremony if it didn't. To reuse an existing identity instead, start the session with PAIGY_AGENT set to its slot (${listSlots().filter((s) => s !== "Desktop").join(", ") || "none yet"}).`
166
165
  }) }] };
167
166
  }
168
167
  function pairStartResult(start, loggedOut) {
@@ -204,7 +203,7 @@ This session isn't on a connected computer, so it asks by code.`;
204
203
  expires_in: start.expiresIn,
205
204
  qr: ascii,
206
205
  user_message: userMessage,
207
- message: "REQUIRED: print user_message for the user \u2014 the LINK is the primary action, the code is the fallback, and `qr` is there for a phone when they ask. Then call pair again with device_code to collect approval. Do not open a browser yourself."
206
+ message: "REQUIRED: print user_message for the user \u2014 the LINK is the primary action, the code is the fallback, and `qr` is there for a phone when they ask. Then call configure again with device_code to collect approval. Do not open a browser yourself."
208
207
  }) }
209
208
  ] };
210
209
  }
@@ -241,7 +240,7 @@ async function runPair(device_code, name, voice, note) {
241
240
  if ("ok" in dt) return pairedResult(
242
241
  dt,
243
242
  void 0,
244
- "Hatched instantly under this device's credential \u2014 no code needed. " + (dt.message ? `${dt.message} Set it with onboard({ name }).` : name ? "" : `You were given a default name ("${dt.name}") \u2014 the user never chose it, so don't announce it as their agent's identity. They can rename it in the app, or you can re-call pair with { name, voice }.`)
243
+ "Hatched instantly under this device's credential \u2014 no code needed. " + (dt.message ? `${dt.message} Set it with configure({ name }).` : name ? "" : `You were given a default name ("${dt.name}") \u2014 the user never chose it, so don't announce it as their agent's identity. They can rename it in the app, or you can call configure with { name, voice }.`)
245
244
  );
246
245
  miss = dt;
247
246
  }
@@ -260,7 +259,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
260
259
  return await handleTool(request, extra?.signal);
261
260
  } catch (e) {
262
261
  if (e instanceof UnpairedError) {
263
- const IDENTITY_TOOLS = /* @__PURE__ */ new Set(["pair", "onboard", "unpair"]);
262
+ const IDENTITY_TOOLS = /* @__PURE__ */ new Set(["configure"]);
264
263
  const hatched = IDENTITY_TOOLS.has(request.params.name) ? null : await hatchUnderDevice();
265
264
  const next = afterUnpaired(hatched, listSlots().includes("Desktop"));
266
265
  if (next === "retry") return await handleTool(request, extra?.signal);
@@ -273,26 +272,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
273
272
  function suggestedAgentName2() {
274
273
  return suggestedAgentName(server.getClientVersion?.()?.name);
275
274
  }
275
+ async function logOutResult() {
276
+ const token = readToken();
277
+ let revoked = false;
278
+ if (token) {
279
+ try {
280
+ revoked = await revokeToken(token);
281
+ } catch {
282
+ revoked = false;
283
+ }
284
+ }
285
+ const removed = deleteToken();
286
+ clearPairing();
287
+ return {
288
+ content: [{
289
+ type: "text",
290
+ text: JSON.stringify({
291
+ ok: true,
292
+ revoked,
293
+ removed,
294
+ message: token ? `Logged out. Server token ${revoked ? "revoked" : "revoke not confirmed"}; local token ${removed ? "deleted" : "was already absent"}.` : "No token found \u2014 already logged out."
295
+ })
296
+ }]
297
+ };
298
+ }
276
299
  async function handleTool(request, signal) {
277
300
  switch (request.params.name) {
278
- case "pair": {
279
- const a = PairSchema.parse(request.params.arguments ?? {});
280
- return runPair(a.device_code, a.name, a.voice);
281
- }
282
301
  // ONE rail for "get this agent talking to Paigy" (#875). `pair`, the `paigy-mcp-onboard`
283
302
  // CLI and `enable_tools` were three doors into one flow, each knowing a different slice
284
- // of the state — so "onboard" was the word people reached for and the tool that did the
285
- // least, and a half-configured agent (paired but not allowlisted, or holding a token for
286
- // a slot it can no longer resolve) had nothing to ask.
303
+ // of the state, and a half-configured agent (paired but not allowlisted, or holding a token
304
+ // for a slot it can no longer resolve) had nothing to ask. Since 2026-10-07 (owner) it is
305
+ // `configure`, the only identity tool: `onboard` renamed, `pair` gone (this did all of it),
306
+ // and `unpair` folded in as `logOut`.
287
307
  //
288
308
  // It reports state and does the next thing, and it is IDEMPOTENT: calling it when
289
309
  // everything is fine costs one round trip and returns who you are. Reporting never
290
310
  // mutates — the allowlist check is read-only on purpose, because a status call that
291
311
  // quietly edits the user's settings is the opposite of what this is for.
292
- // `start` stays accepted, unlisted — it shipped under that name for one afternoon.
293
- case "start":
294
- case "onboard": {
295
- const { name, voice, device_code, scope } = OnboardSchema.parse(request.params.arguments ?? {});
312
+ case "configure": {
313
+ const { name, voice, device_code, scope, logOut } = ConfigureSchema.parse(request.params.arguments ?? {});
314
+ if (logOut) return logOutResult();
296
315
  if (device_code) return runPair(device_code, name, voice);
297
316
  const slot = agentName();
298
317
  const secret = readToken();
@@ -311,14 +330,14 @@ async function handleTool(request, signal) {
311
330
  ...allowlisted ? {} : {
312
331
  enable_tool_ids: PAIGY_TOOL_IDS,
313
332
  enable_command: ENABLE_COMMAND,
314
- enable_prompt: "Print `enable_command` to the user: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists the notify/await tools; pair and unpair still ask.\" You cannot run it yourself (it writes your own permission allowlist, which hosts block as privilege escalation) \u2014 print it, don't wait for it, and carry on."
333
+ enable_prompt: "Print `enable_command` to the user: \"Run this to let Paigy's tools work without an approval prompt each time \u2014 it only allowlists Paigy's own tools.\" You cannot run it yourself (it writes your own permission allowlist, which hosts block as privilege escalation) \u2014 print it, don't wait for it, and carry on."
315
334
  },
316
335
  // Setup is done, so the only things left are the user's to decide. Named here
317
336
  // rather than left implicit: an agent that reports "ready" and stops gives the
318
337
  // user no way to act on an identity they may not want.
319
338
  actions: {
320
- rename: "Call onboard again with { name } (and optionally { voice }) to change it.",
321
- unpair: "Call the `unpair` tool to disconnect this agent from the account."
339
+ rename: "Call configure again with { name } (and optionally { voice }) to change it.",
340
+ logOut: "Call configure({ logOut: true }) to log this agent out (other agents and this device stay paired)."
322
341
  },
323
342
  message: me.message
324
343
  }) }]
@@ -328,30 +347,6 @@ async function handleTool(request, signal) {
328
347
  }
329
348
  return runPair(void 0, name, voice);
330
349
  }
331
- case "unpair": {
332
- const token = readToken();
333
- let revoked = false;
334
- if (token) {
335
- try {
336
- revoked = await revokeToken(token);
337
- } catch {
338
- revoked = false;
339
- }
340
- }
341
- const removed = deleteToken();
342
- clearPairing();
343
- return {
344
- content: [{
345
- type: "text",
346
- text: JSON.stringify({
347
- ok: true,
348
- revoked,
349
- removed,
350
- message: token ? `Unpaired. Server token ${revoked ? "revoked" : "revoke not confirmed"}; local token ${removed ? "deleted" : "was already absent"}.` : "No token found \u2014 already unpaired."
351
- })
352
- }]
353
- };
354
- }
355
350
  // AM I LISTENING? (#2265) Decides and hands over — `listening.ts` owns the rule and the four
356
351
  // answers. The facts are read here, once: the harness's session mark, a token to listen as,
357
352
  // a live listener's mark for this slot, and where THIS server's node and daemon live. The
package/dist/listen.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  removeListenMark,
12
12
  wakeStatusPath,
13
13
  writeListenMark
14
- } from "./chunk-7LQKX73K.js";
14
+ } from "./chunk-632JM6QA.js";
15
15
  import {
16
16
  agentName,
17
17
  getGoal,
@@ -20,7 +20,7 @@ import {
20
20
  receive,
21
21
  slotName,
22
22
  subscribeWake
23
- } from "./chunk-OVSAQW3K.js";
23
+ } from "./chunk-AHLJPVEP.js";
24
24
 
25
25
  // src/listen.ts
26
26
  import { createHash } from "crypto";
@@ -145,9 +145,9 @@ function brief(waiting, reason, as, goalId) {
145
145
  const stalled = waiting.stalled?.length ? ` \xB7 ${waiting.stalled.length} of your Goals stalled 3+ days: update, finish or cancel each` : "";
146
146
  const review = waiting.review ?? [];
147
147
  const named = (goalId ? review.find((g) => g.goalId === goalId) : void 0) ?? review[0];
148
- const fresh = review.length ? ` \xB7 new on ${review.length === 1 ? "your Goal" : `${review.length} of your Goals`}, first ${named.goalId} ("${named.title}"): claim_goal to read it` : "";
148
+ const fresh = review.length ? ` \xB7 new on ${review.length === 1 ? "your Goal" : `${review.length} of your Goals`}, first ${named.goalId} ("${named.title}"): get_goal to read it` : "";
149
149
  const assigned = waiting.assigned ?? [];
150
- const mine = assigned.length ? ` \xB7 assigned to you: ${assigned.length === 1 ? "1 Goal" : `${assigned.length} Goals`}, first ${assigned[0].goalId} ("${assigned[0].title}"): claim_goal({goalId}) to start it` : "";
150
+ const mine = assigned.length ? ` \xB7 assigned to you: ${assigned.length === 1 ? "1 Goal" : `${assigned.length} Goals`}, first ${assigned[0].goalId} ("${assigned[0].title}"): get_goal({goalId}) to read it; your first write starts it` : "";
151
151
  if (!lead) return `Paigy \xB7 ${reason === "boot" ? `listening as ${as}` : reason}${mine}${fresh || (mine ? "" : " \xB7 nothing waiting")}${stalled}`;
152
152
  const said = saidIn(lead) || ("about" in lead ? lead.about : "");
153
153
  const shown = said.length > 160 ? `${said.slice(0, 160)}\u2026` : said;
package/dist/onboard.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  resolvePairing,
4
4
  startPairing,
5
5
  suggestedAgentName
6
- } from "./chunk-BLWC44CQ.js";
6
+ } from "./chunk-WKAXAJEM.js";
7
7
  import {
8
8
  autoConfigureClients,
9
9
  claudeInstallHint,
10
10
  openBrowser
11
- } from "./chunk-5MQ3J6S3.js";
12
- import "./chunk-7LQKX73K.js";
11
+ } from "./chunk-ZP6NBVEE.js";
12
+ import "./chunk-632JM6QA.js";
13
13
  import {
14
14
  TOKEN_PATH,
15
15
  agentName,
@@ -20,7 +20,7 @@ import {
20
20
  saveToken,
21
21
  setIdentity,
22
22
  whoAmI
23
- } from "./chunk-OVSAQW3K.js";
23
+ } from "./chunk-AHLJPVEP.js";
24
24
 
25
25
  // src/onboard.ts
26
26
  function reportRegistered() {
@@ -58,8 +58,8 @@ Paigy MCP onboarding (agent: "${agentName()}")
58
58
  console.log(`Already set up as "${who.name}"${who.voice ? ` (voice: ${who.voice})` : ""}.
59
59
  `);
60
60
  console.log("Nothing to do. What's left is yours to decide:");
61
- console.log(' rename \u2014 PAIGY_NAME="New name" paigy-mcp-onboard (or the `onboard` MCP tool)');
62
- console.log(" unpair \u2014 the MCP `unpair` tool, or delete this slot from ~/.paigy/token.json\n");
61
+ console.log(' rename \u2014 PAIGY_NAME="New name" paigy-mcp-onboard (or the `configure` MCP tool)');
62
+ console.log(" log out \u2014 the MCP `configure` tool with { logOut: true }, or delete this slot from ~/.paigy/token.json\n");
63
63
  return;
64
64
  }
65
65
  console.log("The saved token is no longer valid \u2014 re-pairing.\n");
package/dist/slot.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  agentName
4
- } from "./chunk-OVSAQW3K.js";
4
+ } from "./chunk-AHLJPVEP.js";
5
5
 
6
6
  // src/slot.ts
7
7
  process.stdout.write(agentName());
package/dist/stalled.js CHANGED
@@ -29,7 +29,7 @@ async function main() {
29
29
  const dir = join(homedir(), ".paigy", "stalled-reminded");
30
30
  const mark = join(dir, `${session || "session"}-${day}`);
31
31
  if (existsSync(mark)) return;
32
- const { receive } = await import("./dist-WGFUXFS4.js");
32
+ const { receive } = await import("./dist-JTZJFUWD.js");
33
33
  const reply = stopReply((await receive()).stalled ?? [], input.hook_event_name ?? "Stop");
34
34
  if (!reply) return;
35
35
  mkdirSync(dir, { recursive: true });
@@ -7,7 +7,7 @@ import {
7
7
  readToken,
8
8
  sessionSlot,
9
9
  slotName
10
- } from "./chunk-OVSAQW3K.js";
10
+ } from "./chunk-AHLJPVEP.js";
11
11
 
12
12
  // src/statusline.ts
13
13
  import { realpathSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/mcp",
3
- "version": "0.40.26",
3
+ "version": "0.40.28",
4
4
  "description": "Paigy MCP server — the AI agent harness that calls you. Lets an agent notify a user and await their reply.",
5
5
  "license": "MIT",
6
6
  "type": "module",