@paigy/mcp 0.40.16 → 0.40.19

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
@@ -215,19 +215,48 @@ script and branch on `$PAIGY_EVENT` there:
215
215
  codex exec "Paigy goal $PAIGY_GOAL_ID. claim_goal it first. The user said: $PAIGY_TEXT. Reply with contact when done."
216
216
  ```
217
217
 
218
- ## Stalled work (Claude Code stop hook)
218
+ ## Stalled work (turn-end hook — Claude Code, Codex, Gemini CLI, Antigravity)
219
219
 
220
220
  An agent that goes quiet leaves its Goals behind. Three things hand it that work back, all
221
221
  reading one rule (no progress for 3 days, `COLD_AFTER_MS`): `check_replies` lists it under
222
222
  `stalled`, `paigy-listen` carries it in `PAIGY_WORK` (and launches for it on boot), and this
223
- hook holds a Claude Code session's stop once a day to list it:
223
+ hook holds a session's turn end once a day to list it. One program, four tools:
224
+
225
+ **Claude Code** (`~/.claude/settings.json`) — installed by the paigy plugin:
224
226
 
225
227
  ```json
226
228
  { "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "npx -y -p @paigy/mcp paigy-stalled" }] }] } }
227
229
  ```
228
230
 
231
+ **Codex** (`~/.codex/config.toml`) — its `Stop` event takes the same `{ decision, reason }`, so
232
+ the same program serves it. Written for you when you pair from another tool:
233
+
234
+ ```toml
235
+ [[hooks.Stop]]
236
+
237
+ [[hooks.Stop.hooks]]
238
+ type = "command"
239
+ command = "npx -y -p @paigy/mcp@latest paigy-stalled --agent codex"
240
+ timeout = 60
241
+ ```
242
+
243
+ **Gemini CLI / Antigravity** (`~/.gemini/settings.json`, `~/.gemini/antigravity-cli/settings.json`)
244
+ — the turn-end event there is `AfterAgent`, and the reminder also arrives as a `systemMessage`,
245
+ which is the field that event promises will reach somebody:
246
+
247
+ ```json
248
+ { "hooks": { "AfterAgent": [{ "matcher": "*", "hooks": [{ "name": "paigy-stalled", "type": "command",
249
+ "command": "npx -y -p @paigy/mcp@latest paigy-stalled --agent antigravity", "timeout": 60000 }] }] } }
250
+ ```
251
+
252
+ `--agent <slot>` is not optional outside Claude Code: a hook is not the MCP server, so it never
253
+ inherits the `PAIGY_AGENT` that server was registered with, and without the flag it reads this
254
+ session's own slot — a different identity, with no stalled work of its own. Pass the same value as
255
+ the tool's MCP registration (`codex`, `antigravity`, or whatever you set `PAIGY_AGENT` to).
256
+
229
257
  It is silent when nothing is stalled, when the stop is already being continued by a hook, and
230
- after it has reminded that session once today.
258
+ after it has reminded that session once today. Codex's older `notify` program is not a place for
259
+ this: it is spawned fire-and-forget with stdio null, so nothing it prints can reach the agent.
231
260
 
232
261
  ## Statusline (Claude Code)
233
262
 
@@ -4,10 +4,10 @@ import {
4
4
  serverInstructions,
5
5
  sessionStartHook,
6
6
  withSessionStartHook
7
- } from "./chunk-SFGGZLVO.js";
7
+ } from "./chunk-HGXXGOGG.js";
8
8
  import {
9
9
  agentName
10
- } from "./chunk-6OBUH2FB.js";
10
+ } from "./chunk-VYWIPFEY.js";
11
11
 
12
12
  // src/toolset.ts
13
13
  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.";
@@ -99,6 +99,55 @@ function withPaigyMcpToml(existing, agent) {
99
99
  ].join("\n");
100
100
  return existing + entry;
101
101
  }
102
+ function stalledHookCommand(agent) {
103
+ return `npx -y -p @paigy/mcp@latest paigy-stalled --agent ${agent}`;
104
+ }
105
+ function withPaigyStopHookToml(existing, agent) {
106
+ if (existing.includes("paigy-stalled")) return null;
107
+ const entry = [
108
+ "",
109
+ "[[hooks.Stop]]",
110
+ "",
111
+ "[[hooks.Stop.hooks]]",
112
+ 'type = "command"',
113
+ `command = "${stalledHookCommand(agent)}"`,
114
+ "timeout = 60",
115
+ // seconds, in Codex — a cold `npx` resolve is slow the first time
116
+ ""
117
+ ].join("\n");
118
+ return existing + entry;
119
+ }
120
+ function withPaigyStopHookJson(existing, agent) {
121
+ let config = {};
122
+ if (existing) {
123
+ try {
124
+ config = JSON.parse(existing);
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+ const hooks = config.hooks ?? {};
130
+ const kept = (hooks.AfterAgent ?? []).map((group) => ({ ...group, hooks: (group.hooks ?? []).filter((hook) => hook.name !== "paigy-stalled") })).filter((group) => group.hooks.length > 0);
131
+ config.hooks = {
132
+ ...hooks,
133
+ AfterAgent: [
134
+ ...kept,
135
+ {
136
+ matcher: "*",
137
+ hooks: [
138
+ {
139
+ name: "paigy-stalled",
140
+ type: "command",
141
+ command: stalledHookCommand(agent),
142
+ timeout: 6e4
143
+ // milliseconds, in Gemini CLI
144
+ }
145
+ ]
146
+ }
147
+ ]
148
+ };
149
+ return JSON.stringify(config, null, 2);
150
+ }
102
151
  function autoConfigureClients(skip = agentName()) {
103
152
  const home = homedir();
104
153
  const registered = [];
@@ -118,6 +167,20 @@ function autoConfigureClients(skip = agentName()) {
118
167
  } catch {
119
168
  }
120
169
  }
170
+ for (const path of [
171
+ join(home, ".gemini", "settings.json"),
172
+ join(home, ".gemini", "antigravity-cli", "settings.json")
173
+ ]) {
174
+ try {
175
+ if (!existsSync(dirname(path))) continue;
176
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
177
+ const updated = withPaigyStopHookJson(existing, "antigravity");
178
+ if (updated === null) continue;
179
+ writeFileSync(path, updated);
180
+ registered.push(`Paigy's turn-end reminder (${path})`);
181
+ } catch {
182
+ }
183
+ }
121
184
  }
122
185
  if (skip !== "codex") {
123
186
  const codexConfig = join(home, ".codex", "config.toml");
@@ -129,6 +192,12 @@ function autoConfigureClients(skip = agentName()) {
129
192
  writeFileSync(codexConfig, updated);
130
193
  registered.push(`OpenAI Codex (${codexConfig})`);
131
194
  }
195
+ const afterMcp = existsSync(codexConfig) ? readFileSync(codexConfig, "utf8") : "";
196
+ const withHook = withPaigyStopHookToml(afterMcp, "codex");
197
+ if (withHook !== null) {
198
+ writeFileSync(codexConfig, withHook);
199
+ registered.push(`Paigy's turn-end reminder (${codexConfig})`);
200
+ }
132
201
  }
133
202
  } catch {
134
203
  }
@@ -68,7 +68,7 @@ var StartContactSchema = z2.object({
68
68
  }).strict();
69
69
  var ContactSchema = z2.union([StartContactSchema, z2.object({ deliveryId: z2.string().uuid() }).strict()]);
70
70
  var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
71
- var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals.";
71
+ var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: on a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so).";
72
72
  var CreateGoalSchema = z3.object({
73
73
  outcome: z3.string().trim().min(1).max(1e4),
74
74
  /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
@@ -113,11 +113,16 @@ var UpdateGoalSchema = z3.object({
113
113
  }).strict();
114
114
  var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: z3.string().uuid() }).strict();
115
115
  var ClaimGoalSchema = z3.object({ goalId: z3.string().uuid().optional() }).strict();
116
- var GetGoalSchema = z3.object({ goalId: z3.string().uuid() }).strict();
117
- var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
118
- var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
116
+ var GetGoalSchema = z3.object({
117
+ goalId: z3.string().uuid(),
118
+ /** Every entry in full. Without it the read carries the person's words, open questions and your
119
+ * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
120
+ history: z3.boolean().optional()
121
+ }).strict();
122
+ var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: 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. 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. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
123
+ var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
119
124
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
120
- var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
125
+ var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
121
126
  var CheckRepliesSchema = z3.object({}).strict();
122
127
  var AGENT_TOOLS = [
123
128
  { name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
@@ -128,9 +133,10 @@ var AGENT_TOOLS = [
128
133
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
129
134
  ];
130
135
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
136
+ var DONE_MESSAGE = "When the work is done, send one message through contact saying what is done and anything the person needs to do or check.";
131
137
  function serverInstructions(opts) {
132
138
  const calls = opts.waits ? "A Call holds one bounded window here; continue with contact({deliveryId}) to hold the next or reread that exact Delivery." : "A Call returns after one read; continue with contact({deliveryId}) to reread that exact Delivery.";
133
- return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Evidence can repeat: reads do not acknowledge or hide it. Report progress and acknowledge review with update_goal, never as a contact. 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.
139
+ return `On startup and after a wake, call check_replies for the open Deliveries addressed to you, and claim_goal for your runnable or review-pending Goal and the conversation on it. get_goal rereads it without claiming. Every read ends in \`next\`, the one step to take. Notifications return immediately: keep working and collect answers through claim_goal/get_goal. ${calls} Evidence can repeat: reads do not acknowledge or hide it. Report progress and acknowledge review with update_goal, never as a contact. ${DONE_MESSAGE} 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.
134
140
 
135
141
  HOW TO ASK:
136
142
  1. One question per ask. Five questions are five objects in \`asks\`, in one contact, so each can be answered on its own; one question with five parts settles nothing until all five are answered.
@@ -472,9 +478,9 @@ var AgendaTurnSchema = z4.object({
472
478
  /** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
473
479
  * the planner declares duplicates instead of asking them twice. */
474
480
  coveredIds: z4.array(z4.string()).optional(),
475
- /** At most three short spoken sentences. Capped because a turn is a breath: a 1031-char
476
- * line went out on 2026-07-28 and the caller could not answer it at all. */
477
- info: z4.array(z4.string().min(1)).max(3).default([]),
481
+ /** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
482
+ * (owner, 2026-09-25), and a count here refused whole plans. */
483
+ info: z4.array(z4.string().min(1)).default([]),
478
484
  question: z4.string().min(1).nullable(),
479
485
  /** True on the one turn carrying the agent's own declared question. */
480
486
  asks: z4.boolean().optional(),
@@ -558,7 +564,13 @@ var InboxItemSchema = z4.object({
558
564
  * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
559
565
  * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
560
566
  * author is not an agent this account holds (unpaired since, or a person). */
561
- agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional()
567
+ agent: z4.object({ tokenId: z4.string(), name: z4.string(), voice: VoiceKeySchema.optional() }).optional(),
568
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
569
+ * its need offers, exactly as its own card carries them, present only when one of them has a
570
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
571
+ * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
572
+ * says those, and the list stays small (an `html` is up to 16 KB). */
573
+ options: z4.array(OptionSchema).optional()
562
574
  })).optional(),
563
575
  visuals: z4.array(VisualSchema).optional(),
564
576
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -585,6 +597,11 @@ var InboxItemSchema = z4.object({
585
597
  * and "dismiss the stalled ones" cancelled 37 pieces of live work). */
586
598
  cold: z4.string().datetime().optional(),
587
599
  clarifies: z4.string().optional(),
600
+ /** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
601
+ * live"). Present only while an open Call Delivery carries the card's request Entry — read
602
+ * off the same open list the card came from, so it clears when the Call does. A card is the
603
+ * backup for a call not taken; while the call has it, the call is where it is answered. */
604
+ onCall: z4.literal(true).optional(),
588
605
  /** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
589
606
  * declined, and what the ladder will do next — read off the cron's own row, never computed
590
607
  * on the phone. Present only while a `declined` receipt stands on the card's last Call.
@@ -949,7 +966,19 @@ var QueueItemSchema = z4.object({
949
966
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
950
967
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
951
968
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
952
- lastProgressAt: z4.string().nullable().optional()
969
+ lastProgressAt: z4.string().nullable().optional(),
970
+ /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
971
+ * kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
972
+ * note. A progress note is an Entry, so this is already the newer of the two: the person's note
973
+ * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
974
+ * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
975
+ * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
976
+ latest: z4.object({
977
+ from: z4.enum(["person", "agent"]),
978
+ said: z4.string(),
979
+ at: z4.string(),
980
+ entryId: z4.string()
981
+ }).nullable().optional()
953
982
  });
954
983
  var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
955
984
  var NoteSourceSchema = z4.enum(["app", "call"]);
@@ -1125,6 +1154,104 @@ var NotificationFeedbackKindSchema = z4.enum([
1125
1154
  "other"
1126
1155
  // anything else — the note carries it.
1127
1156
  ]);
1157
+ var SlimOptionSchema = OptionSchema.omit({ html: true });
1158
+ var QuestionRowSchema = z4.object({
1159
+ /** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
1160
+ id: z4.string(),
1161
+ deliveryId: z4.string(),
1162
+ entryId: z4.string(),
1163
+ /** The decision it waits on; null for an update, which asks nothing. */
1164
+ needId: z4.string().nullable(),
1165
+ goalIds: z4.array(z4.string()),
1166
+ /** The name of the work it is about, when the read could word it. */
1167
+ goalTitle: z4.string().optional(),
1168
+ tokenId: z4.string().optional(),
1169
+ name: z4.string(),
1170
+ title: z4.string(),
1171
+ body: z4.string(),
1172
+ select: z4.enum(["one", "many", "rank", "confirm", "text"]),
1173
+ options: z4.array(SlimOptionSchema),
1174
+ hasPreview: z4.boolean(),
1175
+ blocking: z4.boolean(),
1176
+ askedAt: z4.string().datetime(),
1177
+ ring: InboxItemSchema.shape.ring,
1178
+ onCall: z4.literal(true).optional(),
1179
+ sealed: z4.boolean()
1180
+ });
1181
+ var WorkStateSchema = z4.enum(["ready", "active", "waiting", "done", "cancelled"]);
1182
+ var WorkRowSchema = z4.object({
1183
+ id: z4.string(),
1184
+ parentId: z4.string().nullable(),
1185
+ title: z4.string(),
1186
+ /** Straight off the Goal. */
1187
+ state: WorkStateSchema,
1188
+ owner: z4.string().nullable(),
1189
+ revision: z4.number().int(),
1190
+ /** Open questions on it, counted to 100. */
1191
+ waiting: z4.number().int(),
1192
+ /** Held by a gate on work that is not done. */
1193
+ blocked: z4.boolean(),
1194
+ lastProgressAt: z4.string().datetime().nullable(),
1195
+ /** The line written for its newest progress note, else that note's first words. */
1196
+ line: z4.string().nullable(),
1197
+ /** Work directly under it, counted to 100; the list carries up to 12 of them. */
1198
+ children: z4.number().int(),
1199
+ createdAt: z4.string().datetime(),
1200
+ updatedAt: z4.string().datetime(),
1201
+ /** When anything at or under it last moved — the order the list is in. */
1202
+ activeAt: z4.string().datetime(),
1203
+ /** A sealed outcome has no title here; the work's page opens it. */
1204
+ sealed: z4.boolean()
1205
+ });
1206
+ var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
1207
+ var AgentRowSchema = ComputerRowSchema.extend({
1208
+ /** Open questions it is asking the person, over every open card; null when that read failed. */
1209
+ asking: z4.number().int().nullable(),
1210
+ oldestAskAt: z4.string().datetime().nullable(),
1211
+ /** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
1212
+ * many in all among the account's 200 most recently active agent-held live Goals
1213
+ * (`agent_holds`); null when that read failed. */
1214
+ holds: z4.array(z4.object({ id: z4.string(), title: z4.string() })).nullable(),
1215
+ held: z4.number().int().nullable(),
1216
+ /** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
1217
+ * while none has, or when that read failed. */
1218
+ cold: z4.string().datetime().nullable(),
1219
+ /** The newest line of its working log, and when the harness saw it. */
1220
+ line: z4.string().nullable(),
1221
+ lineAt: z4.string().datetime().nullable()
1222
+ });
1223
+ var SnapshotSchema = z4.object({
1224
+ /** The API's clock, taken before the first read: what a later delta will start from. */
1225
+ at: z4.string().datetime(),
1226
+ questions: z4.object({
1227
+ /** The newest 30 open cards, questions before updates. */
1228
+ items: z4.array(QuestionRowSchema),
1229
+ /** Every open question, and apart from them every update, and what was put off. */
1230
+ total: z4.number().int(),
1231
+ updates: z4.number().int(),
1232
+ putOff: z4.number().int()
1233
+ }).nullable(),
1234
+ agents: z4.object({
1235
+ /** Up to 60, most recently seen first. */
1236
+ items: z4.array(AgentRowSchema),
1237
+ more: z4.boolean()
1238
+ }).nullable(),
1239
+ work: z4.object({
1240
+ /** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
1241
+ items: z4.array(WorkRowSchema),
1242
+ /** How much work is behind each of the Work tab's four filters, each counted to 100, read with
1243
+ * the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
1244
+ * `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
1245
+ * is `ready`; Done is `done` or `cancelled`. */
1246
+ counts: z4.object({ live: z4.number().int(), waiting: z4.number().int(), notStarted: z4.number().int(), done: z4.number().int() })
1247
+ }).nullable(),
1248
+ you: z4.object({
1249
+ settings: UserSettingsSchema,
1250
+ callable: z4.boolean(),
1251
+ /** Up to 20 paired computers; null when the roster read failed. */
1252
+ computers: z4.array(ComputerRowSchema).nullable()
1253
+ }).nullable()
1254
+ });
1128
1255
 
1129
1256
  // src/listening.ts
1130
1257
  import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -1149,7 +1276,8 @@ next check_replies. Start your own listener now, as a BACKGROUND task whose outp
1149
1276
  npx -y -p @paigy/mcp@latest paigy-listen
1150
1277
 
1151
1278
  Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
1152
- the answer needs a decision. Start it once; if it is already running, do nothing.
1279
+ the answer needs a decision. Start it once; if it is already running, do nothing. Then call
1280
+ claim_goal once: work handed to you before this session started waits there.
1153
1281
  PAIGY
1154
1282
  exit 0
1155
1283
  `;
@@ -1,10 +1,12 @@
1
1
  import {
2
+ NameTakenError,
2
3
  isNetworkError,
3
4
  pollToken,
4
5
  requestCode,
5
6
  saveToken,
7
+ setIdentity,
6
8
  sleep
7
- } from "./chunk-6OBUH2FB.js";
9
+ } from "./chunk-VYWIPFEY.js";
8
10
 
9
11
  // src/identity.ts
10
12
  var CLIENT_LABELS = {
@@ -25,6 +27,21 @@ function suggestedAgentName(clientName) {
25
27
  if (!clientName) return void 0;
26
28
  return CLIENT_LABELS[clientName.toLowerCase()] ?? clientName.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
27
29
  }
30
+ async function identify(who, ask) {
31
+ let renamed = false;
32
+ if (ask.name || ask.voice) {
33
+ try {
34
+ who = await setIdentity({ ...ask.name ? { name: ask.name } : {}, ...ask.voice ? { voice: ask.voice } : {} });
35
+ renamed = true;
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.`;
38
+ return { name: who.name, voice: who.voice, message: message2 };
39
+ }
40
+ }
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.`;
43
+ return { name: who.name, voice: who.voice, message };
44
+ }
28
45
 
29
46
  // src/pairing.ts
30
47
  async function resolvePairing(deviceCode, capMs, pollMs = 2e3) {
@@ -102,6 +119,7 @@ function clearPairing(deviceCode) {
102
119
 
103
120
  export {
104
121
  suggestedAgentName,
122
+ identify,
105
123
  resolvePairing,
106
124
  startPairing,
107
125
  joinBackgroundPair,