@paigy/mcp 0.40.26 → 0.40.27

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)
@@ -1689,7 +1689,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
1689
1689
 
1690
1690
  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
1691
 
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\`).`;
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\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
1693
1693
  var CreateGoalSchema = z3.object({
1694
1694
  outcome: z3.string().trim().min(1).max(1e4),
1695
1695
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -1735,7 +1735,7 @@ var CreateChange = z3.object({
1735
1735
  kind: z3.literal("create"),
1736
1736
  title: goalTitle,
1737
1737
  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."),
1738
+ 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
1739
  parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
1740
1740
  sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
1741
1741
  }).strict();
@@ -1780,8 +1780,7 @@ var ManageGoalsSchema = z3.object({
1780
1780
  var ManageGoalsToolSchema = z3.object({
1781
1781
  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
1782
  }).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();
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_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
1785
1784
  var GetGoalSchema = z3.object({
1786
1785
  goalId: z3.string().uuid(),
1787
1786
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -1792,8 +1791,7 @@ var GetGoalSchema = z3.object({
1792
1791
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
1793
1792
  diagnose: z3.boolean().optional()
1794
1793
  }).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).";
1794
+ 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
1795
  var SearchToolSchema = z3.object({
1798
1796
  query: z3.string().trim().min(1).max(500),
1799
1797
  types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -1801,7 +1799,7 @@ var SearchToolSchema = z3.object({
1801
1799
  limit: z3.number().int().min(1).max(20).optional()
1802
1800
  }).strict();
1803
1801
  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();
1802
+ var CheckActivitySchema = z3.object({}).strict();
1805
1803
  var FEEDBACK_TEXT_MAX = 5e4;
1806
1804
  var SendFeedbackSchema = z3.object({
1807
1805
  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 +1808,11 @@ var SendFeedbackSchema = z3.object({
1810
1808
  }).strict();
1811
1809
  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
1810
  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
1811
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
1815
1812
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
1816
- { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
1817
1813
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
1818
1814
  { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
1815
+ { 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
1816
  { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
1820
1817
  ];
1821
1818
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
@@ -3057,7 +3054,7 @@ var HostDecisionSchema = z7.object({
3057
3054
  /** The agent's token id: the row's `recipient`. */
3058
3055
  agent: z7.string().uuid(),
3059
3056
  decision: z7.enum(["stood_back", "took_over"]),
3060
- /** The work it was about: the Goal `claim_goal` would hand that agent next. */
3057
+ /** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
3061
3058
  goalId: z7.string().uuid().nullable().optional(),
3062
3059
  /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
3063
3060
  seenAt: z7.string().datetime().nullable().optional(),
@@ -3438,12 +3435,6 @@ async function manageGoals(input, opts = {}) {
3438
3435
  if (!res.ok) await fail("manage_goals", res);
3439
3436
  return await res.json();
3440
3437
  }
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
3438
  async function getGoal(goalId, opts = {}, read3 = {}) {
3448
3439
  const token = authToken(opts.token) ?? "";
3449
3440
  const query = [read3.history ? "history=1" : "", read3.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
@@ -3622,11 +3613,11 @@ async function sendFeedback(input, opts = {}) {
3622
3613
  if (!res.ok) await fail("send_feedback", res);
3623
3614
  return await res.json();
3624
3615
  }
3625
- async function whoIsWorking(opts = {}) {
3616
+ async function checkActivity(opts = {}) {
3626
3617
  const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/agents/working`, {
3627
3618
  headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}` }
3628
3619
  }));
3629
- if (!res.ok) await fail("who_is_working", res);
3620
+ if (!res.ok) await fail("check_activity", res);
3630
3621
  return await res.json();
3631
3622
  }
3632
3623
  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.`;
@@ -3905,7 +3896,7 @@ function deliveryView(d) {
3905
3896
  acknowledged: d.acknowledged?.filter((a) => a.acknowledged).map((a) => a.eventId),
3906
3897
  next: [
3907
3898
  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
3899
+ 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
3900
  ].filter(Boolean).join(" ")
3910
3901
  });
3911
3902
  }
@@ -3942,8 +3933,8 @@ function receivedView(r, now = Date.now()) {
3942
3933
  notAcknowledged: refused.length ? refused.map((a) => ({ eventId: a.eventId, refused: a.refused })) : void 0,
3943
3934
  // WORK ASSIGNED TO YOU (owner, 2026-09-24): your Goals nobody has started, however they became yours.
3944
3935
  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.
3936
+ // THE WORK WAITING ON YOU NEXT (owner, 2026-09-27: "Wake on any work"): the predicate that starts a
3937
+ // sleeping agent, so "nothing is waiting" is never said while work is.
3947
3938
  claimable: w.claimable,
3948
3939
  // YOUR STALLED WORK (#2257), and OTHER AGENTS' (owner, 2026-09-23): any agent may take it over.
3949
3940
  stalled,
@@ -3953,10 +3944,10 @@ function receivedView(r, now = Date.now()) {
3953
3944
  ...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
3945
  ...r.hasMore ? ["More are waiting: acknowledge these, then receive again."] : [],
3955
3946
  ...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.`] : [],
3947
+ ...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.`] : [],
3948
+ ...w.claimable && !assigned.length ? [`Next waiting on you: ${w.claimable.title ?? w.claimable.goalId} (get_goal({ goalId: "${w.claimable.goalId}" })).`] : [],
3958
3949
  ...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.`] : [],
3950
+ ...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
3951
  // No name of its own (#2525): the API's ask, verbatim.
3961
3952
  ...w.unnamed ? [w.unnamed] : []
3962
3953
  ].join(" ")
@@ -3993,9 +3984,9 @@ async function runTool(name, args, opts) {
3993
3984
  const sent = await contact(parsed, { ...client, signal, waits });
3994
3985
  return "sent" in sent ? notSentView(sent) : deliveryView(sent);
3995
3986
  }
3996
- case "who_is_working":
3997
- WhoIsWorkingSchema.parse(input);
3998
- return whoIsWorking(client);
3987
+ case "check_activity":
3988
+ CheckActivitySchema.parse(input);
3989
+ return checkActivity(client);
3999
3990
  case "send_feedback":
4000
3991
  return sendFeedback(SendFeedbackSchema.parse(input), client);
4001
3992
  case "manage_goals": {
@@ -4009,10 +4000,6 @@ async function runTool(name, args, opts) {
4009
4000
  for (const g of managed.goals ?? []) remember(client.token, g.goalId, g.revision);
4010
4001
  return managedView(managed, changes);
4011
4002
  }
4012
- case "claim_goal": {
4013
- const { goalId } = ClaimGoalSchema.parse(input);
4014
- return goalView(read2(client.token, await claimGoal(goalId, client)));
4015
- }
4016
4003
  case "get_goal": {
4017
4004
  const { goalId, history, diagnose } = GetGoalSchema.parse(input);
4018
4005
  const goal = read2(client.token, await getGoal(goalId, client, { history, diagnose }));
@@ -4287,7 +4274,6 @@ export {
4287
4274
  overrideToken,
4288
4275
  authToken,
4289
4276
  manageGoals,
4290
- claimGoal,
4291
4277
  getGoal,
4292
4278
  AWAIT_WINDOW_MS,
4293
4279
  hatch,
@@ -4307,7 +4293,7 @@ export {
4307
4293
  acceptTriage,
4308
4294
  dismissTriage,
4309
4295
  searchRecords,
4310
- whoIsWorking,
4296
+ checkActivity,
4311
4297
  readDelivery,
4312
4298
  contact,
4313
4299
  receive,
@@ -4,54 +4,38 @@ import {
4
4
  serverInstructions,
5
5
  sessionStartHook,
6
6
  withSessionStartHook
7
- } from "./chunk-7LQKX73K.js";
7
+ } from "./chunk-X3GTDW5G.js";
8
8
  import {
9
9
  CODEX_ENV,
10
10
  agentName,
11
11
  withCodexEnv
12
- } from "./chunk-OVSAQW3K.js";
12
+ } from "./chunk-4C6SDQ7A.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,
@@ -6,7 +6,7 @@ import {
6
6
  saveToken,
7
7
  setIdentity,
8
8
  sleep
9
- } from "./chunk-OVSAQW3K.js";
9
+ } from "./chunk-4C6SDQ7A.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
 
@@ -219,7 +219,7 @@ CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an ans
219
219
 
220
220
  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
221
 
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\`).`;
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\`; your first write to it starts it) and your work gone quiet (\`stalled\`).`;
223
223
  var CreateGoalSchema = z3.object({
224
224
  outcome: z3.string().trim().min(1).max(1e4),
225
225
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -265,7 +265,7 @@ var CreateChange = z3.object({
265
265
  kind: z3.literal("create"),
266
266
  title: goalTitle,
267
267
  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."),
268
+ 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
269
  parentGoalId: z3.string().uuid().optional().describe("The Goal it belongs under. Omitted: a root."),
270
270
  sourceEntryIds: z3.array(z3.string().uuid()).max(20).optional().describe("The whole Entries the work came from, ones you can read.")
271
271
  }).strict();
@@ -310,8 +310,7 @@ var ManageGoalsSchema = z3.object({
310
310
  var ManageGoalsToolSchema = z3.object({
311
311
  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
312
  }).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();
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_found) and the rest still apply, so read every result: a partial result is never a complete success. Kinds: create (title, outcome, ownerId, parentGoalId, sourceEntryIds) returns the new Goal's id in `results[i].goalId`, in the order requested, to use in later calls; edit (title and/or outcome); state (open, completed, canceled); assign (ownerId); defer (until: the Goal waits until then and its owner is woken when it passes; null takes it back); move (parentGoalId, or null for a root: only this Goal moves); dependency (add or remove: goalId waits on dependsOnGoalId to start or to complete). A Goal cannot be completed while its required children or dependencies remain open: finish or move them first. Finishing the children does not prove the parent's own work is done. An edit applies at the version you last read: if someone changed the Goal since, it is refused as goal_revision_conflict, so read it again (get_goal) and reconsider. You may change any Goal of your person; a change puts you on it. Returns `ok` (every change applied), `results` per change (applied or failed), and `goals`, each changed Goal's id, state, title, outcome and revision.";
315
314
  var GetGoalSchema = z3.object({
316
315
  goalId: z3.string().uuid(),
317
316
  /** Every entry in full. Without it the read carries the person's words, open questions and your
@@ -322,8 +321,7 @@ var GetGoalSchema = z3.object({
322
321
  * between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
323
322
  diagnose: z3.boolean().optional()
324
323
  }).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).";
324
+ 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
325
  var SearchToolSchema = z3.object({
328
326
  query: z3.string().trim().min(1).max(500),
329
327
  types: z3.array(z3.enum(["entry", "goal", "answer"])).min(1).optional(),
@@ -331,7 +329,7 @@ var SearchToolSchema = z3.object({
331
329
  limit: z3.number().int().min(1).max(20).optional()
332
330
  }).strict();
333
331
  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();
332
+ var CheckActivitySchema = z3.object({}).strict();
335
333
  var FEEDBACK_TEXT_MAX = 5e4;
336
334
  var SendFeedbackSchema = z3.object({
337
335
  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 +338,23 @@ var SendFeedbackSchema = z3.object({
340
338
  }).strict();
341
339
  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
340
  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
341
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
345
342
  { name: "manage_goals", description: MANAGE_GOALS_DESCRIPTION, inputSchema: mcpInputSchema(ManageGoalsToolSchema) },
346
- { name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
347
343
  { name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
348
344
  { name: "search", description: SEARCH_DESCRIPTION, inputSchema: mcpInputSchema(SearchToolSchema) },
345
+ { 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
346
  { name: "send_feedback", description: SEND_FEEDBACK_DESCRIPTION, inputSchema: mcpInputSchema(SendFeedbackSchema) }
350
347
  ];
351
348
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
352
349
  function serverInstructions(opts) {
353
350
  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.
351
+ 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
352
 
356
353
  HOW TO ASK:
357
354
  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
355
  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
356
  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.
357
+ 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
358
  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
359
  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
360
  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.
@@ -1585,7 +1582,7 @@ var HostDecisionSchema = z7.object({
1585
1582
  /** The agent's token id: the row's `recipient`. */
1586
1583
  agent: z7.string().uuid(),
1587
1584
  decision: z7.enum(["stood_back", "took_over"]),
1588
- /** The work it was about: the Goal `claim_goal` would hand that agent next. */
1585
+ /** The work it was about: the Goal waiting on that agent next (`claimable` on its `contact({})` read). */
1589
1586
  goalId: z7.string().uuid().nullable().optional(),
1590
1587
  /** When the server last heard from the agent, as the host read it: the presence it stood back for. */
1591
1588
  seenAt: z7.string().datetime().nullable().optional(),
@@ -1816,7 +1813,7 @@ next contact({ wait: false }). Start your own listener now, as a BACKGROUND task
1816
1813
 
1817
1814
  Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
1818
1815
  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.
1816
+ contact({ wait: false }) once: work handed to you before this session started waits there.
1820
1817
  PAIGY
1821
1818
  exit 0
1822
1819
  `;
@@ -1852,7 +1849,7 @@ function decideListen(f) {
1852
1849
  return {
1853
1850
  status: "start",
1854
1851
  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."
1852
+ 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
1853
  };
1857
1854
  }
1858
1855
  function listenMark(slot, home = homedir()) {
@@ -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-4C6SDQ7A.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-A5FYZK6P.js";
7
+ import "./chunk-X3GTDW5G.js";
8
+ import "./chunk-4C6SDQ7A.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-UQDA7X3W.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-A5FYZK6P.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-X3GTDW5G.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-4C6SDQ7A.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-X3GTDW5G.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-4C6SDQ7A.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-UQDA7X3W.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-A5FYZK6P.js";
12
+ import "./chunk-X3GTDW5G.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-4C6SDQ7A.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-4C6SDQ7A.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-N6ZJUJV7.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-4C6SDQ7A.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.27",
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",