pendingyou 0.36.0 → 0.36.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.36.1 (2026-10-20)
4
+
5
+ - Fixed: when you replied on an answer card, the Claude Code session that posted the answer wasn't woken: the wake mod
6
+ didn't know `post_answer`, so your reply reached it only with your next message, or at the next session start. Now
7
+ the mod follows a session's answers as it does its questions, and your reply starts a turn even while the session
8
+ sits idle. Got it, reading the answer in the chat, a newer answer replacing it, or the agent withdrawing it wakes
9
+ nothing, and the mod forgets the answer (after one more look when you mark it Got it, since you can take that back
10
+ for a few seconds).
11
+ - An answer asks you nothing, so it's no longer counted as waiting on you: not in “2 waiting on you” under Claude
12
+ Code's prompt, and not on Herdr's badges for Claude Code, Codex, OpenCode or Pi.
13
+ - A reply on an answer wakes Codex, OpenCode and Pi in the same words as Claude Code: “Pending You: your person replied
14
+ to your answer “…” (req_…).”, then to read it with get_request, act on it and ack_answer, or post_answer with
15
+ follows for a long one.
16
+
3
17
  ## 0.36.0 (2026-10-19)
4
18
 
5
19
  - Your agents' answers to your questions land in Pending You too (guide 2.54). When you ask an agent something and
@@ -181,6 +181,7 @@ export async function posted(io, options) {
181
181
  const urgency = isUrgency(args.urgency) ? args.urgency : before?.urgency;
182
182
  const blocking = typeof args.blocking === 'boolean' ? args.blocking : before?.blocking;
183
183
  const askedFirst = call === 'post_request' ? askedIn(args) : before?.askedFirst;
184
+ const answer = call === 'post_answer' || before?.kind === 'answer';
184
185
  waiting.cards[requestId] = {
185
186
  ...(name ? { name } : {}),
186
187
  ...(title ? { title } : {}),
@@ -188,6 +189,7 @@ export async function posted(io, options) {
188
189
  ...(urgency ? { urgency } : {}),
189
190
  ...(blocking ? { blocking } : {}),
190
191
  ...(askedFirst ? { askedFirst } : {}),
192
+ ...(answer ? { kind: 'answer' } : {}),
191
193
  };
192
194
  }
193
195
  }
@@ -251,7 +253,9 @@ export async function reportThreads(io, app, options) {
251
253
  if (here.length === 0 && options.closed)
252
254
  return await clearHerdr(io, options.at, pane);
253
255
  const newest = [...here].sort((a, b) => b.at - a.at)[0];
254
- const cards = here.flatMap((thread) => Object.entries(thread.cards).map(([requestId, card]) => ({
256
+ const cards = here.flatMap((thread) => Object.entries(thread.cards)
257
+ .filter(([, card]) => card.kind !== 'answer')
258
+ .map(([requestId, card]) => ({
255
259
  requestId,
256
260
  ...(typeof card.title === 'string' ? { title: card.title } : {}),
257
261
  ...(isUrgency(card.urgency) ? { urgency: card.urgency } : {}),
@@ -418,6 +422,13 @@ function line(heard, card) {
418
422
  `${from} followed up on ${titled}, which it answered for your person`,
419
423
  `call ${get} to read it, act on it if it needs anything, then ack_answer with its version to mark it heard`,
420
424
  ];
425
+ if (heard.kind === 'answer')
426
+ return heard.status === 'resolved' || heard.status === 'cancelled' || heard.status === 'expired'
427
+ ? null
428
+ : [
429
+ `your person replied to your answer ${titled}`,
430
+ `call ${get}, act on their words, then ack_answer with its version and a one-line outcome (it reaches them as your reply), or post_answer with follows: its requestId for a long one`,
431
+ ];
421
432
  switch (heard.status) {
422
433
  case 'expired':
423
434
  return [
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.36.0';
1
+ export const VERSION = '0.36.1';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pendingyou-wake",
3
3
  "displayName": "Pending You",
4
- "version": "2.54.0",
4
+ "version": "2.54.1",
5
5
  "description": "Wakes Claude Code when your person answers a Pending You card it posted, even while it sits idle. Installed by `npx pendingyou init`.",
6
6
  "author": {
7
7
  "name": "RecordPlane",
package/mod/README.md CHANGED
@@ -4,4 +4,4 @@ The pendingyou command line’s copy of the Pending You plugin’s wake mod, for
4
4
 
5
5
  In each session it learns the cards the session posts to Pending You, checks them through the session’s own Pending You connection, and starts a turn when your person answers, writes back or a fallback comes due, or hands the session a question from another of their assistants (CLI 0.14.0), even while it sits idle. It answers `npx pendingyou hold` itself, so nothing waits in the background, and shows “2 waiting on you” under the prompt, after the plugin’s name. With the Pending You plugin installed too, only one copy acts.
6
6
 
7
- Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.54.0; send changes there.
7
+ Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.54.1; send changes there.
@@ -534,7 +534,8 @@ async function answerHold($, e) {
534
534
  if ((await check($, requestId)) !== 'ok')
535
535
  return null;
536
536
  showCards($);
537
- return { result: { stdout: holdReply(requestId), stderr: '', interrupted: false } };
537
+ const answer = live.book.cards[requestId]?.kind === 'answer';
538
+ return { result: { stdout: holdReply(requestId, answer), stderr: '', interrupted: false } };
538
539
  }
539
540
  export function register(on) {
540
541
  on('session.start', async ($, e, next) => {
@@ -564,7 +565,7 @@ export function register(on) {
564
565
  return next(e);
565
566
  });
566
567
  on('tool.call', {
567
- tool: /^mcp__.+__(post_request|get_request|update_request|reply_in_thread|ack_answer|cancel_request|whoami|list_pending|answer_delegated|hand_back)$/,
568
+ tool: /^mcp__.+__(post_request|post_answer|get_request|update_request|reply_in_thread|ack_answer|cancel_request|whoami|list_pending|answer_delegated|hand_back)$/,
568
569
  }, async ($, e, next) => {
569
570
  const result = await next(e);
570
571
  try {
package/mod/hooks/wake.js CHANGED
@@ -21,6 +21,7 @@ const URGENCIES = new Set(['now', 'today', 'whenever']);
21
21
  const isUrgency = (value) => typeof value === 'string' && URGENCIES.has(value);
22
22
  const READS = new Set([
23
23
  'post_request',
24
+ 'post_answer',
24
25
  'get_request',
25
26
  'update_request',
26
27
  'reply_in_thread',
@@ -32,6 +33,7 @@ const READS = new Set([
32
33
  'hand_back',
33
34
  ]);
34
35
  export const HANDED_READS = new Set(['get_request', 'answer_delegated', 'hand_back']);
36
+ const POSTS = new Set(['post_request', 'post_answer']);
35
37
  export const APPROVED_DRAFT_NOTICE = 'The approved words are in get_request’s answer.approvedDrafts: send them exactly.';
36
38
  const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
37
39
  const parseJson = (text) => {
@@ -99,7 +101,7 @@ export function resultObject(result) {
99
101
  }
100
102
  export function nameOf(tool, input) {
101
103
  const name = text(input.name, 120);
102
- if (name || tool !== 'post_request')
104
+ if (name || !POSTS.has(tool))
103
105
  return name;
104
106
  const session = typeof input.session === 'string' ? parseJson(input.session) : input.session;
105
107
  return isObject(session) ? text(session.label, 120) : undefined;
@@ -111,6 +113,7 @@ export function argumentsOf(call) {
111
113
  export const isClosed = (status) => CLOSED.has(status);
112
114
  export const isReady = (state) => state.turn === 'agent' || isClosed(state.status);
113
115
  export const isWaitingOnYou = (state) => state.turn === 'you' && OPEN.has(state.status);
116
+ const waitsOnYou = (card) => card.kind !== 'answer' && card.state !== undefined && isWaitingOnYou(card.state);
114
117
  export const momentOf = (state) => `${state.status}/${state.turn}/${state.version}/${state.cursor}`;
115
118
  export function stateOf(output) {
116
119
  const status = text(output.status, 40);
@@ -157,6 +160,7 @@ export function readBook(value) {
157
160
  ...(isUrgency(card.urgency) ? { urgency: card.urgency } : {}),
158
161
  ...(card.blocking === true ? { blocking: true } : {}),
159
162
  ...(card.askedFirst === true ? { askedFirst: true } : {}),
163
+ ...(card.kind === 'answer' ? { kind: 'answer' } : {}),
160
164
  ...(state ? { state } : {}),
161
165
  ...(typeof card.told === 'string' ? { told: card.told } : {}),
162
166
  ...(typeof card.seen === 'string' ? { seen: card.seen } : {}),
@@ -220,12 +224,14 @@ function tracked(book, facts, now) {
220
224
  const card = book.cards[facts.requestId];
221
225
  const name = facts.name ?? card?.name;
222
226
  const title = facts.title ?? card?.title;
227
+ const kind = facts.kind ?? card?.kind;
223
228
  return put(book, {
224
229
  requestId: facts.requestId,
225
230
  server: facts.server,
226
231
  ...(name ? { name } : {}),
227
232
  ...(title ? { title } : {}),
228
233
  ...(facts.pressing ?? pressingOfCard(card)),
234
+ ...(kind ? { kind } : {}),
229
235
  learnedAt: card?.learnedAt ?? now,
230
236
  changedAt: now,
231
237
  state: facts.state,
@@ -269,6 +275,22 @@ export function learn(book, call, now) {
269
275
  }, now);
270
276
  return tool === 'post_request' ? followedUp(posted, requestIdOf(input.follows), now) : posted;
271
277
  }
278
+ case 'post_answer': {
279
+ const status = text(output.status, 40);
280
+ if (!status || !OPEN.has(status) || version === undefined)
281
+ return next;
282
+ const title = text(input.title, 400);
283
+ const posted = tracked(next, {
284
+ requestId,
285
+ server,
286
+ ...(name ? { name } : {}),
287
+ ...(title ? { title: clip(title) } : {}),
288
+ pressing: {},
289
+ kind: 'answer',
290
+ state: { status, turn: 'you', version, cursor: card?.state?.cursor ?? '' },
291
+ }, now);
292
+ return followedUp(posted, requestIdOf(input.follows), now);
293
+ }
272
294
  case 'reply_in_thread': {
273
295
  const reopen = isObject(input.reopen) ? input.reopen : null;
274
296
  if (version === undefined || (!reopen && output.turn !== 'you'))
@@ -304,7 +326,9 @@ export function learn(book, call, now) {
304
326
  case 'ack_answer':
305
327
  return output.status === 'resolved' ? drop(next, requestId) : next;
306
328
  case 'cancel_request':
307
- return output.status === 'cancelled' ? drop(next, requestId) : next;
329
+ return output.status === 'cancelled' || output.status === 'resolved'
330
+ ? drop(next, requestId)
331
+ : next;
308
332
  default:
309
333
  return next;
310
334
  }
@@ -332,15 +356,22 @@ export function failed(card, now, blocked) {
332
356
  nextAt: now + Math.min(MAX_BACKOFF_MS, FAST_MS * 2 ** (failures - 1)),
333
357
  };
334
358
  }
359
+ export const ANSWER_UNDO_MS = 20_000;
360
+ const mayComeBack = (card) => card.kind === 'answer' &&
361
+ card.state?.status === 'resolved' &&
362
+ !(card.checkedAt !== undefined && card.checkedAt - card.changedAt >= ANSWER_UNDO_MS);
335
363
  export function dueCards(book, now) {
336
364
  return Object.values(book.cards)
337
- .filter((card) => !(card.state && isClosed(card.state.status)) && (card.nextAt ?? 0) <= now)
365
+ .filter((card) => !(card.state && isClosed(card.state.status) && !mayComeBack(card)) &&
366
+ (card.nextAt ?? 0) <= now)
338
367
  .sort((a, b) => (a.nextAt ?? 0) - (b.nextAt ?? 0));
339
368
  }
340
369
  export function needsTelling(card) {
341
370
  const state = card.state;
342
371
  if (!state || !isReady(state))
343
372
  return false;
373
+ if (card.kind === 'answer' && isClosed(state.status))
374
+ return false;
344
375
  if (state.status === 'resolved')
345
376
  return false;
346
377
  const moment = momentOf(state);
@@ -377,7 +408,7 @@ export function untold(book, before) {
377
408
  export function settle(book, now) {
378
409
  let next = book;
379
410
  for (const card of Object.values(book.cards)) {
380
- const done = card.state && isClosed(card.state.status) && !needsTelling(card);
411
+ const done = card.state && isClosed(card.state.status) && !needsTelling(card) && !mayComeBack(card);
381
412
  if (done || now - Math.max(card.learnedAt, card.changedAt) > KEEP_MS)
382
413
  next = drop(next, card.requestId);
383
414
  }
@@ -389,6 +420,11 @@ const named = (card) => (card.name ? `name “${card.name}”` : 'your name');
389
420
  function line(card) {
390
421
  const state = card.state;
391
422
  const get = `get_request (requestId ${card.requestId}, ${named(card)})`;
423
+ if (card.kind === 'answer')
424
+ return [
425
+ `your person replied to your answer ${titled(card)}`,
426
+ `call ${get}, act on their words, then ack_answer with its version and a one-line outcome (it reaches them as your reply), or post_answer with follows: its requestId for a long one`,
427
+ ];
392
428
  switch (state.status) {
393
429
  case 'expired':
394
430
  return [
@@ -449,7 +485,7 @@ export function wakeText(cards) {
449
485
  export function statusText(book, blocked) {
450
486
  if (blocked)
451
487
  return `to hear answers here, allow mcp__${blocked} in /permissions`;
452
- const waiting = Object.values(book.cards).filter((card) => card.state && isWaitingOnYou(card.state)).length;
488
+ const waiting = Object.values(book.cards).filter(waitsOnYou).length;
453
489
  return waiting ? `${waiting} waiting on you` : undefined;
454
490
  }
455
491
  export const blockedNote = (server) => `Pending You can’t wake this session when your person answers: Claude Code doesn’t let it check your cards yet. Ask them to allow mcp__${server} in /permissions (Allow, user settings). Until then, check get_request at breakpoints.`;
@@ -572,7 +608,7 @@ export function holdOf(command) {
572
608
  }
573
609
  return held ? requestId : null;
574
610
  }
575
- export const holdReply = (requestId) => `Pending You: no hold is needed in this session. It’s woken when your person answers ${requestId}, writes to you on it, or its fallback runs, even while it’s idle. Nothing is running in the background and there’s nothing to wait on: keep working, or end your turn.`;
611
+ export const holdReply = (requestId, answer = false) => `Pending You: no hold is needed in this session. It’s woken when your person ${answer ? `replies to your answer ${requestId}` : `answers ${requestId}, writes to you on it, or its fallback runs`}, even while it’s idle. Nothing is running in the background and there’s nothing to wait on: keep working, or end your turn.`;
576
612
  function leaseOf(value) {
577
613
  const match = /^(\S+) (\d+)$/.exec(value ?? '');
578
614
  return match ? { holder: match[1], until: Number(match[2]) } : null;
@@ -677,7 +713,7 @@ export function graceEnds(handed, folders) {
677
713
  return Number.isFinite(at) ? at + HANDED_GRACE_MS : null;
678
714
  }
679
715
  export function folderOf(tool, input) {
680
- if (tool !== 'post_request')
716
+ if (!POSTS.has(tool))
681
717
  return undefined;
682
718
  const session = typeof input.session === 'string' ? parseJson(input.session) : input.session;
683
719
  return isObject(session) ? text(session.cwd, 1000) : undefined;
@@ -919,7 +955,7 @@ export function inHerdr(on, bin, pane) {
919
955
  }
920
956
  export function herdrCards(book) {
921
957
  return Object.values(book.cards)
922
- .filter((card) => card.state && isWaitingOnYou(card.state))
958
+ .filter(waitsOnYou)
923
959
  .sort((a, b) => (a.requestId < b.requestId ? -1 : a.requestId > b.requestId ? 1 : 0))
924
960
  .map((card) => ({
925
961
  requestId: card.requestId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pendingyou",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "description": "Pending You for your coding agents: one command sets up Claude Code, Codex, OpenCode and Pi to ask you through Pending You and hear your answers, even while they sit idle.",
5
5
  "keywords": [
6
6
  "pendingyou",