pendingyou 0.36.0 → 0.37.0
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 +25 -0
- package/README.md +13 -0
- package/dist/apps/codex-wake.js +12 -1
- package/dist/confirm.js +3 -3
- package/dist/step-now.js +133 -0
- package/dist/stopcheck.js +4 -0
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/register.js +3 -2
- package/mod/hooks/wake.js +44 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.37.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
- A step your agent hands you now goes on a card (guide 2.55). When its last message gives you a step to do now (“Go:
|
|
6
|
+
approve it now”, “you can merge it now”, “now open the run link”, “go ahead and approve it”) and no card from that
|
|
7
|
+
turn has it, the check at the end of its turn asks it once to put it on a card, even while an older card of its waits
|
|
8
|
+
on you. A card from the turn has it when it holds the message’s link, or, without one, two of its words. Not a step
|
|
9
|
+
now: the agent's own act (“I'll go ahead and merge it now”), a question whether it should, an offer (“whenever you
|
|
10
|
+
like”), a look (“now open … to see it”), what a page or a step says, or a heading such as “Open now:”. Patterns
|
|
11
|
+
only, on your computer; it asks Pending You nothing. Run `npx pendingyou@latest init` to switch: the hooks don’t
|
|
12
|
+
change, so Codex keeps trusting them.
|
|
13
|
+
|
|
14
|
+
## 0.36.1 (2026-10-20)
|
|
15
|
+
|
|
16
|
+
- Fixed: when you replied on an answer card, the Claude Code session that posted the answer wasn't woken: the wake mod
|
|
17
|
+
didn't know `post_answer`, so your reply reached it only with your next message, or at the next session start. Now
|
|
18
|
+
the mod follows a session's answers as it does its questions, and your reply starts a turn even while the session
|
|
19
|
+
sits idle. Got it, reading the answer in the chat, a newer answer replacing it, or the agent withdrawing it wakes
|
|
20
|
+
nothing, and the mod forgets the answer (after one more look when you mark it Got it, since you can take that back
|
|
21
|
+
for a few seconds).
|
|
22
|
+
- An answer asks you nothing, so it's no longer counted as waiting on you: not in “2 waiting on you” under Claude
|
|
23
|
+
Code's prompt, and not on Herdr's badges for Claude Code, Codex, OpenCode or Pi.
|
|
24
|
+
- A reply on an answer wakes Codex, OpenCode and Pi in the same words as Claude Code: “Pending You: your person replied
|
|
25
|
+
to your answer “…” (req_…).”, then to read it with get_request, act on it and ack_answer, or post_answer with
|
|
26
|
+
follows for a long one.
|
|
27
|
+
|
|
3
28
|
## 0.36.0 (2026-10-19)
|
|
4
29
|
|
|
5
30
|
- Your agents' answers to your questions land in Pending You too (guide 2.54). When you ask an agent something and
|
package/README.md
CHANGED
|
@@ -214,6 +214,19 @@ beside a recap still goes on a card. And anything else for you, in a message tha
|
|
|
214
214
|
recap while this folder has a card waiting on you: only such a message costs a request (this folder's open cards,
|
|
215
215
|
given up after 1.5 seconds), and when Pending You can't say, the turn ends.
|
|
216
216
|
|
|
217
|
+
Since 0.37.0 a step it hands you to do now goes on a card, whatever card is open (guide 2.55). An agent that ends a
|
|
218
|
+
card at a step that waits for it ("on my go, open the link") and then gives the go only in its chat leaves you to find
|
|
219
|
+
it there. So when the last message hands you a step now ("Go: approve it now", "you can merge it now", "now open the
|
|
220
|
+
run link", "go ahead and approve it", "Your turn: …"), a card call from that turn has to carry it: one that holds the
|
|
221
|
+
message's link, or, without one, two of its words. Otherwise the agent is asked once (about 49 tokens): *Your last
|
|
222
|
+
message hands them a step now, and no card from this turn has it: put it on a card (follows: the card they answered;
|
|
223
|
+
one still waiting: update_request it), or say it doesn’t need one.* A card about something else, a recap of open cards
|
|
224
|
+
and the message's word that it's on a card don't count: the card waiting on you is the one whose step said "on my go".
|
|
225
|
+
Not a step now: the agent's own act ("I'll go ahead and merge it now"), a question whether it should ("Merge and deploy
|
|
226
|
+
now?"), an offer ("whenever you like"), a look ("now open … to see it"), news ("you can now run the tests yourself"),
|
|
227
|
+
what a page or a step says, a heading or label ("Open now:", "Open questions: …"), or anything in code or quotes. It
|
|
228
|
+
costs no request.
|
|
229
|
+
|
|
217
230
|
Since 0.9.0 it also catches a message asking you to type or reply something in Claude Code's own chat ("Reply “yes,
|
|
218
231
|
erase disk12” here", "To confirm, type `yes`", "Confirm here"). Some steps an agent takes only on your own words in
|
|
219
232
|
its conversation, never on a card answer; that's fine, but then a card has to send you there. Unless a card posted in
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -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)
|
|
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/confirm.js
CHANGED
|
@@ -10,7 +10,7 @@ const FILLER = /^(?:\s|[,:]|back\b|me\b|it\b|to me\b|with\b|exactly\b|precisely\
|
|
|
10
10
|
const ELSEWHERE = /\b(?:in|into|at|on)\s+(?:your|a|the|that|their)\s+(?:terminal|shell|browser|console|prompt|form|field|dialog|email|inbox|page|app|site|website|portal|dashboard)\b|\bin\s+(?:Terminal|iTerm)\b|\bwhen\s+(?:prompted|asked|it\s+asks)\b|\bat\s+the\s+prompt\b|https?:\/\/(?!claude\.ai\/code\/)/iu;
|
|
11
11
|
const THIS_CHAT = /\bin\s+(?:this|our)\s+(?:chat|conversation|session)\b/iu;
|
|
12
12
|
const OFFER = /\b(?:any\s?time|whenever|at\s+any\s+(?:point|time)|if\s+you\s+change\s+your\s+mind|if\s+(?:anything|something)\s+(?:looks|seems|goes))\b/iu;
|
|
13
|
-
const DESCRIBED = /\b(?:says?|said|reads?|ends?\s+with|starts?\s+with|shows?|prints?|displays?|asks?|prompts?|tells?\s+\w+|text|message|copy|label|button|template)\s*[:,]?$/iu;
|
|
13
|
+
export const DESCRIBED = /\b(?:says?|said|reads?|ends?\s+with|starts?\s+with|shows?|prints?|displays?|asks?|prompts?|tells?\s+\w+|text|message|copy|label|button|template)\s*[:,]?$/iu;
|
|
14
14
|
const COMMAND_LIKE = /^\s*(?:[!/]|\$\s|(?:cd|npm|npx|pnpm|yarn|bun|git|gh|curl|wget|brew|node|python3?|pip|docker|ssh|scp|sudo|claude|codex|wrangler)\s)/u;
|
|
15
15
|
const QUOTED = /“([^“”\n]{1,160})”|"([^"\n]{1,160})"|`([^`\n]{1,160})`|‘([^‘’\n]{1,160})’|(?<![\p{L}\p{N}])'([^'\n]{1,160})'(?![\p{L}\p{N}])/gu;
|
|
16
16
|
export function proseOf(text) {
|
|
@@ -36,7 +36,7 @@ export function proseOf(text) {
|
|
|
36
36
|
}
|
|
37
37
|
return kept.join('\n');
|
|
38
38
|
}
|
|
39
|
-
function marked(text) {
|
|
39
|
+
export function marked(text) {
|
|
40
40
|
const quotes = [];
|
|
41
41
|
const out = text.replace(QUOTED, (...groups) => {
|
|
42
42
|
const inner = groups.slice(1, 6).find((group) => typeof group === 'string');
|
|
@@ -128,7 +128,7 @@ const COMMON = new Set([
|
|
|
128
128
|
'this',
|
|
129
129
|
'that',
|
|
130
130
|
]);
|
|
131
|
-
const wordsOf = (text) => text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
131
|
+
export const wordsOf = (text) => text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
132
132
|
export const telling = (words) => words === null ? [] : [...new Set(wordsOf(words).filter((word) => !COMMON.has(word)))];
|
|
133
133
|
export function cardFits(text, words, where) {
|
|
134
134
|
const wanted = telling(words);
|
package/dist/step-now.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { DESCRIBED, marked, proseOf, wordsOf } from "./confirm.js";
|
|
2
|
+
const NOW_VERB = String.raw `(?:approve|open(?!\s+(?:to|for|now)\b)|press|click|tap|run|paste|merge|publish|deploy|release|restart|confirm|authori[sz]e|accept|allow|(?:sign|log)\s+in)`;
|
|
3
|
+
const GATE_VERB = String.raw `(?:approve|merge|publish|deploy|release|confirm|authori[sz]e|accept|allow|(?:sign|log)\s+in)`;
|
|
4
|
+
const LEAD = String.raw `(?:(?:ok(?:ay)?|alright|all\s+right|right|so|please|just)[,!]?\s+)*`;
|
|
5
|
+
const NOT_THAT = String.raw `(?!\s+(?:that|on)\b)`;
|
|
6
|
+
const NOT_FOR = String.raw `(?<!\bfor\s+)`;
|
|
7
|
+
export const STEP_NOW = new RegExp([
|
|
8
|
+
String.raw `^${LEAD}go(?:\s+ahead)?(?:\s+now)?\s*(?:[:!—–]|-\s)`,
|
|
9
|
+
String.raw `^${LEAD}(?:it(?:['’]s|\s+is)\s+)?your\s+turn\s*[:!—–]`,
|
|
10
|
+
String.raw `\b(?:that|this|here)(?:['’]s|\s+is)\s+(?:your|the)\s+go\b(?!-)`,
|
|
11
|
+
String.raw `(?:^${LEAD}|[:;—–]\s*|\byou\s+can\s+)go\s+ahead\s+and\s+${NOW_VERB}\b`,
|
|
12
|
+
String.raw `(?:^${LEAD}|[:;,—–]\s*|\b(?:and|so|then)\s+)now[,:]?\s+(?:please\s+)?${NOW_VERB}\b`,
|
|
13
|
+
String.raw `^(?![^\n]*\?\s*$)${LEAD}${NOW_VERB}\b[^.!?\n:]{0,80}?${NOT_FOR}\bnow\b${NOT_THAT}`,
|
|
14
|
+
String.raw `\b(?:you\s+can|can\s+you|could\s+you)\s+(?:go\s+ahead\s+and\s+)?${GATE_VERB}\b[^.!?\n]{0,60}?${NOT_FOR}\bnow\b${NOT_THAT}`,
|
|
15
|
+
String.raw `\byou\s+can\s+now\s+${GATE_VERB}\b`,
|
|
16
|
+
].join('|'), 'iu');
|
|
17
|
+
const STEP_NOW_EACH = new RegExp(STEP_NOW.source, 'giu');
|
|
18
|
+
export const NOT_NOW = /\b(?:any\s?time|whenever|at\s+any\s+(?:point|time)|if\s+you\s+(?:want|like|prefer|['’]d\s+like)|when\s+you\s+get\s+a\s+(?:chance|minute)|no\s+rush|or\s+later|to\s+(?:see|look|check|try|test|compare))\b/iu;
|
|
19
|
+
export const STEP_NOW_REASON = 'Your last message hands them a step now, and no card from this turn has it: put it on a card (follows: the card they answered; one still waiting: update_request it), or say it doesn’t need one.';
|
|
20
|
+
const SENTENCE_END = /(?<=[.!?])\s+|\n+/u;
|
|
21
|
+
const MARK = /⟦(\d+)⟧/gu;
|
|
22
|
+
const LINK = /\bhttps?:\/\/[^\s<>"'`“”‘’⟦⟧]+/giu;
|
|
23
|
+
const LINK_END = /[.,;:!?)]+$/u;
|
|
24
|
+
const LINK_GOES_ON = /[\p{L}\p{N}_-]/u;
|
|
25
|
+
const CARD_PAGE = /\/app\/r\/req_/iu;
|
|
26
|
+
const STEP_COMMON = new Set([
|
|
27
|
+
'go',
|
|
28
|
+
'ahead',
|
|
29
|
+
'now',
|
|
30
|
+
'you',
|
|
31
|
+
'your',
|
|
32
|
+
'yours',
|
|
33
|
+
'can',
|
|
34
|
+
'could',
|
|
35
|
+
'please',
|
|
36
|
+
'just',
|
|
37
|
+
'ok',
|
|
38
|
+
'okay',
|
|
39
|
+
'alright',
|
|
40
|
+
'all',
|
|
41
|
+
'right',
|
|
42
|
+
'so',
|
|
43
|
+
'then',
|
|
44
|
+
'still',
|
|
45
|
+
'card',
|
|
46
|
+
'cards',
|
|
47
|
+
'done',
|
|
48
|
+
'step',
|
|
49
|
+
'steps',
|
|
50
|
+
'turn',
|
|
51
|
+
'it',
|
|
52
|
+
'its',
|
|
53
|
+
's',
|
|
54
|
+
't',
|
|
55
|
+
'll',
|
|
56
|
+
're',
|
|
57
|
+
've',
|
|
58
|
+
'd',
|
|
59
|
+
'm',
|
|
60
|
+
'i',
|
|
61
|
+
'me',
|
|
62
|
+
'my',
|
|
63
|
+
'we',
|
|
64
|
+
'our',
|
|
65
|
+
'the',
|
|
66
|
+
'a',
|
|
67
|
+
'an',
|
|
68
|
+
'and',
|
|
69
|
+
'or',
|
|
70
|
+
'to',
|
|
71
|
+
'of',
|
|
72
|
+
'on',
|
|
73
|
+
'in',
|
|
74
|
+
'at',
|
|
75
|
+
'for',
|
|
76
|
+
'with',
|
|
77
|
+
'from',
|
|
78
|
+
'by',
|
|
79
|
+
'is',
|
|
80
|
+
'are',
|
|
81
|
+
'be',
|
|
82
|
+
'this',
|
|
83
|
+
'that',
|
|
84
|
+
'here',
|
|
85
|
+
'there',
|
|
86
|
+
]);
|
|
87
|
+
function handsOver(sentence) {
|
|
88
|
+
if (NOT_NOW.test(sentence))
|
|
89
|
+
return false;
|
|
90
|
+
for (const found of sentence.matchAll(STEP_NOW_EACH))
|
|
91
|
+
if (!DESCRIBED.test(sentence.slice(0, found.index).trimEnd()))
|
|
92
|
+
return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function linksIn(said) {
|
|
96
|
+
const links = (said.match(LINK) ?? [])
|
|
97
|
+
.map((link) => link.replace(LINK_END, '').replace(/\/$/u, '').toLowerCase())
|
|
98
|
+
.filter((link) => !CARD_PAGE.test(link));
|
|
99
|
+
return [...new Set(links)];
|
|
100
|
+
}
|
|
101
|
+
export function stepsNow(text) {
|
|
102
|
+
const { text: prose, quotes } = marked(proseOf(text));
|
|
103
|
+
const steps = [];
|
|
104
|
+
for (const each of prose.split(SENTENCE_END)) {
|
|
105
|
+
const sentence = each.trim();
|
|
106
|
+
if (!sentence || !handsOver(sentence))
|
|
107
|
+
continue;
|
|
108
|
+
const said = sentence.replace(MARK, (_, index) => quotes[Number(index)] ?? '');
|
|
109
|
+
steps.push({
|
|
110
|
+
text: said,
|
|
111
|
+
links: linksIn(said),
|
|
112
|
+
words: [
|
|
113
|
+
...new Set(wordsOf(said.replace(LINK, ' ')).filter((word) => !STEP_COMMON.has(word))),
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return steps;
|
|
118
|
+
}
|
|
119
|
+
function holds(said, link) {
|
|
120
|
+
for (let at = said.indexOf(link); at !== -1; at = said.indexOf(link, at + 1))
|
|
121
|
+
if (!LINK_GOES_ON.test(said.charAt(at + link.length)))
|
|
122
|
+
return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
export function carriesStep(card, step) {
|
|
126
|
+
if (step.links.length) {
|
|
127
|
+
const said = card.toLowerCase();
|
|
128
|
+
return step.links.every((link) => holds(said, link));
|
|
129
|
+
}
|
|
130
|
+
const has = new Set(wordsOf(card));
|
|
131
|
+
const shared = step.words.filter((word) => has.has(word)).length;
|
|
132
|
+
return shared >= Math.min(2, step.words.length);
|
|
133
|
+
}
|
package/dist/stopcheck.js
CHANGED
|
@@ -9,6 +9,7 @@ import { readCredential } from "./credentials.js";
|
|
|
9
9
|
import { isHandedWake } from "./format.js";
|
|
10
10
|
import { isPermissionCard, isPromptApp, sessionName, settlePrompts, subagentsRunning, } from "./permission.js";
|
|
11
11
|
import { askerParams, folderForms } from "./pickup.js";
|
|
12
|
+
import { carriesStep, STEP_NOW_REASON, stepsNow } from "./step-now.js";
|
|
12
13
|
export const READ_CAP = 4 * 1024 * 1024;
|
|
13
14
|
const CHUNK = 64 * 1024;
|
|
14
15
|
export const STOP_REASON = 'Items for your person are in chat but not in Pending You: post them as cards (ask in both places), or say they don’t need one.';
|
|
@@ -448,6 +449,9 @@ export async function stopcheck(io, options) {
|
|
|
448
449
|
return 0;
|
|
449
450
|
return block(confirmReason(ask.words, sessionLink(io.env), APP_NAMES[app]));
|
|
450
451
|
}
|
|
452
|
+
const loose = stepsNow(text).filter((step) => !turn.cards.some((card) => carriesStep(card, step)));
|
|
453
|
+
if (loose.length && !saysNoCard(text))
|
|
454
|
+
return block(STEP_NOW_REASON);
|
|
451
455
|
if (turn.posted || saysPosted(text) || saysNoCard(text))
|
|
452
456
|
return 0;
|
|
453
457
|
if (turn.asked && isAnswer(text) && (await seesAnswers(io, app)))
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.37.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.54.
|
|
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.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.54.1; send changes there.
|
package/mod/hooks/register.js
CHANGED
|
@@ -534,7 +534,8 @@ async function answerHold($, e) {
|
|
|
534
534
|
if ((await check($, requestId)) !== 'ok')
|
|
535
535
|
return null;
|
|
536
536
|
showCards($);
|
|
537
|
-
|
|
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
|
|
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'
|
|
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)
|
|
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(
|
|
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
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.37.0",
|
|
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",
|