pendingyou 0.15.0 → 0.16.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 +36 -0
- package/README.md +8 -0
- package/dist/apps/claude-code.js +3 -1
- package/dist/apps/codex-wake.js +14 -2
- package/dist/args.js +4 -0
- package/dist/claude.js +12 -2
- package/dist/format.js +36 -1
- package/dist/main.js +2 -1
- package/dist/permission.js +171 -50
- package/dist/version.js +1 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/wake.js +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 (2026-10-05)
|
|
4
|
+
|
|
5
|
+
- A word on one of your own cards that's neither an answer nor your person's (guide 2.32): `/mcp/cli/answers` marks it
|
|
6
|
+
`notice` (`{ kind, from }`), and the hold, the pickup, the hooks and `watch` say what it is instead of “they wrote
|
|
7
|
+
back”:
|
|
8
|
+
- `follow_up`: the helper that answered it for your person followed up afterwards (“shipped in #231”): “Pending You:
|
|
9
|
+
pending-you-chief followed up on req_…, which it answered for your person.”, the question, its words, then “Act on
|
|
10
|
+
it if it needs anything, then call ack_answer … to mark it heard.” On an answer you haven't picked up yet, its
|
|
11
|
+
words come with the answer (“Also: …”).
|
|
12
|
+
- `handed_over`: your person handed your open cards to another assistant: “Pending You: your person handed your open
|
|
13
|
+
cards to pending-you-chief, req_… among them.” and “Stop working on them: pending-you-chief answers them and
|
|
14
|
+
follows up. Call ack_answer … to say you’ve stopped.”
|
|
15
|
+
`watch` gives both as `message_ready`. A command line before 0.16.0 hears the same words as a message from them.
|
|
16
|
+
- Claude Code's permission prompts get a card in bypassPermissions mode too: a dangerous request from a background
|
|
17
|
+
agent there waited minutes for an OK with no card. Claude Code still asks for a few things in that mode (a dangerous
|
|
18
|
+
`rm`, an ask rule, a safety check it can't verify), but the PermissionRequest hook skipped bypassPermissions and
|
|
19
|
+
dontAsk mode outright. It also runs for a call Claude Code then denies by itself, showing
|
|
20
|
+
nothing, so in those two modes it now writes the prompt down quietly, and its card goes up only once a dialog is on
|
|
21
|
+
screen. A new Notification hook says so: `pendingyou-hook notify --app claude-code`, with the matcher
|
|
22
|
+
`permission_prompt`, so Claude Code runs it only for a dialog that has waited about six seconds with nobody typing,
|
|
23
|
+
and never for its other notifications. A call denied without a dialog never gets a card: its PostToolUseFailure, the
|
|
24
|
+
same agent's next call, your next message or the end of the turn settles it. In the other modes a card still goes up
|
|
25
|
+
after 10 seconds, or as soon as the Notification comes.
|
|
26
|
+
- A dialog no PermissionRequest hook ran for (a sandboxed command's network request; one a Claude Code ran none for)
|
|
27
|
+
gets a card from its Notification alone: “Claude Code is waiting for your OK”, with Claude Code's own words, redacted,
|
|
28
|
+
in its summary (“It says: “Claude needs your permission”.”). Whose dialog it was isn't known, so only a turn that ends
|
|
29
|
+
with no subagent running, or the session's end, takes it down.
|
|
30
|
+
- A background agent's prompt says so: “Claude Code is waiting for your OK: pnpm lint && git push (a background
|
|
31
|
+
agent)”, and its summary names the agent's type (“A background agent (general-purpose) of Claude Code
|
|
32
|
+
(billing-webhooks), in ~/… on work-mbp, is waiting for your OK to run a command.”). It stays on the session's one card,
|
|
33
|
+
and only that agent's own calls settle it, not the session's or another agent's.
|
|
34
|
+
- `init` adds the Notification hook at the end of Claude Code's Notification hooks, after any of your own, and says so
|
|
35
|
+
on a computer set up before (“Added a Notification hook to the permission-prompt hooks: …”); `status` marks the
|
|
36
|
+
Permission prompts line missing without it; `uninstall` and `--no-permission-cards` take it out with the others.
|
|
37
|
+
Codex is unchanged: it has no hook that says a dialog is on screen (its `notify` runs only when a turn completes).
|
|
38
|
+
|
|
3
39
|
## 0.15.0 (2026-10-05)
|
|
4
40
|
|
|
5
41
|
- Presence: Pending You hears which of your agents' sessions are open right now. Each app tells it with its own
|
package/README.md
CHANGED
|
@@ -246,6 +246,14 @@ a key are blanked (`curl -H "Authorization: …" https://api.example.com/items?t
|
|
|
246
246
|
shown safely (several lines, inline code like `node -e`, one that takes a secret) shows only as “Bash: node …”. No card
|
|
247
247
|
for `claude -p` or the Agent SDK, where nobody sees a prompt.
|
|
248
248
|
|
|
249
|
+
In bypassPermissions mode (`--dangerously-skip-permissions`) Claude Code still asks for a few things: a dangerous `rm`,
|
|
250
|
+
an ask rule of yours, a safety check it can't verify. Since 0.16.0 those get a card too, once Claude Code says the
|
|
251
|
+
dialog has waited a few seconds with nobody typing (its Notification hook, which `init` adds for permission dialogs
|
|
252
|
+
only): a call it denies by itself, showing you nothing, never gets one. The same hook puts up a card for a dialog no
|
|
253
|
+
other hook sees (a sandboxed command asking for the network), in Claude Code's own words, and in the other modes puts
|
|
254
|
+
a card up as soon as Claude Code says so, without waiting the full 10 seconds. A background agent's prompt says so:
|
|
255
|
+
“Claude Code is waiting for your OK: pnpm lint && git push (a background agent)”.
|
|
256
|
+
|
|
249
257
|
It needs Claude Code 2.1.119 or later, signing in through this computer's sign-in (not `--oauth`), and isn't on
|
|
250
258
|
Windows yet. Don't want it? `npx pendingyou init --no-permission-cards` (later runs keep it off; `--permission-cards`
|
|
251
259
|
turns it back on). `npx pendingyou status` says which.
|
package/dist/apps/claude-code.js
CHANGED
|
@@ -199,8 +199,10 @@ export const claudeCode = {
|
|
|
199
199
|
};
|
|
200
200
|
function permissionLine(io, claude, manifest, helper, flag) {
|
|
201
201
|
const label = 'Permission prompts:';
|
|
202
|
-
if (claude.permission)
|
|
202
|
+
if (claude.permission && claude.notify)
|
|
203
203
|
return ` ok ${label} a card when Claude Code waits for your OK`;
|
|
204
|
+
if (claude.permission)
|
|
205
|
+
return ` missing ${label} no card for a dialog in bypassPermissions mode, or one the other hooks don’t see; run npx pendingyou@latest init${flag}`;
|
|
204
206
|
if (manifest?.permissionCards === false)
|
|
205
207
|
return ` ${label} off, as you chose; for a card when Claude Code waits for your OK, run npx pendingyou init --permission-cards${flag}`;
|
|
206
208
|
const blocker = claude.claude
|
package/dist/apps/codex-wake.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getJson, SignInNeeded, Unavailable } from "../api.js";
|
|
|
5
5
|
import { DEFAULT_ORIGIN } from "../args.js";
|
|
6
6
|
import { connectionMachine, readCredential } from "../credentials.js";
|
|
7
7
|
import { configDir, readJson, withLock, writeWhole } from "../files.js";
|
|
8
|
-
import { clip, delegatedOf, isClosed } from "../format.js";
|
|
8
|
+
import { clip, delegatedOf, isClosed, noticeOf } from "../format.js";
|
|
9
9
|
import { chooseThread, goesBy, HANDED_FOR_MS, handedCard, handedMessage, namesAnotherComputer, } from "../handed.js";
|
|
10
10
|
import { macName, tailscaleName } from "../remote.js";
|
|
11
11
|
import { claimHanded, markHanded, momentOf, releaseHanded, wasHanded } from "../state.js";
|
|
@@ -310,6 +310,18 @@ function line(heard, card) {
|
|
|
310
310
|
.replaceAll('”', '’')}” (${heard.requestId})`;
|
|
311
311
|
const named = card.name ?? heard.sessionLabel;
|
|
312
312
|
const get = `get_request (requestId ${heard.requestId}, ${named ? `name “${named}”` : 'your name'})`;
|
|
313
|
+
const notice = noticeOf(heard);
|
|
314
|
+
const from = notice ? clip(notice.from, 60).text : '';
|
|
315
|
+
if (notice?.kind === 'handed_over')
|
|
316
|
+
return [
|
|
317
|
+
`your person handed your open cards to ${from}, ${titled} among them`,
|
|
318
|
+
`stop working on them: ${from} answers them and follows up; ack_answer ${heard.requestId} with its version to say you’ve stopped`,
|
|
319
|
+
];
|
|
320
|
+
if (notice?.kind === 'follow_up' && heard.status !== 'answered')
|
|
321
|
+
return [
|
|
322
|
+
`${from} followed up on ${titled}, which it answered for your person`,
|
|
323
|
+
`call ${get} to read it, act on it if it needs anything, then ack_answer with its version to mark it heard`,
|
|
324
|
+
];
|
|
313
325
|
switch (heard.status) {
|
|
314
326
|
case 'expired':
|
|
315
327
|
return [
|
|
@@ -339,7 +351,7 @@ export function wakeMessage(ready) {
|
|
|
339
351
|
});
|
|
340
352
|
if (lines.length === 0)
|
|
341
353
|
return null;
|
|
342
|
-
const asked = lines.some(({ heard }) => heard.turn === 'agent' && heard.status !== 'expired');
|
|
354
|
+
const asked = lines.some(({ heard }) => heard.turn === 'agent' && heard.status !== 'expired' && !noticeOf(heard));
|
|
343
355
|
const reopen = asked
|
|
344
356
|
? ' If they asked you something or you need more from them, reply_in_thread with reopen instead.'
|
|
345
357
|
: '';
|
package/dist/args.js
CHANGED
|
@@ -63,6 +63,7 @@ const COMMANDS = new Set([
|
|
|
63
63
|
'mcp',
|
|
64
64
|
'permission',
|
|
65
65
|
'permission-done',
|
|
66
|
+
'notify',
|
|
66
67
|
'permission-card',
|
|
67
68
|
'presence',
|
|
68
69
|
]);
|
|
@@ -88,6 +89,7 @@ const FLAGS = {
|
|
|
88
89
|
listen: { origin: true, app: true, thread: true, handed: false },
|
|
89
90
|
permission: { origin: true, app: true },
|
|
90
91
|
'permission-done': { origin: true, app: true },
|
|
92
|
+
notify: { origin: true, app: true },
|
|
91
93
|
'permission-card': { origin: true, session: true, worker: true },
|
|
92
94
|
presence: {
|
|
93
95
|
origin: true,
|
|
@@ -154,6 +156,7 @@ export const HOOK_COMMANDS = new Set([
|
|
|
154
156
|
'posted',
|
|
155
157
|
'permission',
|
|
156
158
|
'permission-done',
|
|
159
|
+
'notify',
|
|
157
160
|
'presence',
|
|
158
161
|
]);
|
|
159
162
|
const THREAD = /^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/;
|
|
@@ -315,6 +318,7 @@ export function parseArgs(argv, env = {}) {
|
|
|
315
318
|
}
|
|
316
319
|
case 'permission':
|
|
317
320
|
case 'permission-done':
|
|
321
|
+
case 'notify':
|
|
318
322
|
none();
|
|
319
323
|
return { name: first, origin, ...app() };
|
|
320
324
|
case 'permission-card': {
|
package/dist/claude.js
CHANGED
|
@@ -26,6 +26,7 @@ export const PERMISSION_HOOKS = [
|
|
|
26
26
|
{ event: 'PostToolUse', sub: 'permission-done' },
|
|
27
27
|
{ event: 'PostToolUseFailure', sub: 'permission-done' },
|
|
28
28
|
{ event: 'SessionEnd', sub: 'permission-done', timeout: null },
|
|
29
|
+
{ event: 'Notification', sub: 'notify', matcher: 'permission_prompt' },
|
|
29
30
|
];
|
|
30
31
|
export const PERMISSION_CLAUDE = '2.1.119';
|
|
31
32
|
export const PERMISSIONS = ['Bash(npx pendingyou hold:*)', `mcp__${MCP_NAME}`];
|
|
@@ -337,7 +338,11 @@ export async function installClaude(io, origin, options) {
|
|
|
337
338
|
const permissionLines = wanted && !blocker
|
|
338
339
|
? Object.fromEntries(PERMISSION_HOOKS.map((spec) => [spec.event, options.hookLine(spec.sub)]))
|
|
339
340
|
: null;
|
|
340
|
-
const
|
|
341
|
+
const settings = await readSettings(io);
|
|
342
|
+
const had = hooksOf(settings, 'Claude Code’s settings');
|
|
343
|
+
const upgrade = Boolean(findHook(had, 'PermissionRequest', 'permission')) &&
|
|
344
|
+
!findHook(had, 'Notification', 'notify');
|
|
345
|
+
const merged = mergeSettings(settings, commands, permissionLines);
|
|
341
346
|
report({
|
|
342
347
|
ok: true,
|
|
343
348
|
text: merged.hooksChanged
|
|
@@ -349,7 +354,9 @@ export async function installClaude(io, origin, options) {
|
|
|
349
354
|
ok: true,
|
|
350
355
|
text: permissionLines
|
|
351
356
|
? merged.permissionChanged
|
|
352
|
-
?
|
|
357
|
+
? upgrade
|
|
358
|
+
? 'Added a Notification hook to the permission-prompt hooks: a dialog in bypassPermissions mode gets a card too, and so does one the other hooks don’t see, once Claude Code says it has waited a few seconds.'
|
|
359
|
+
: 'Added the permission-prompt hooks: when Claude Code waits for your OK and you don’t answer within a few seconds, a card says so (a push only after your hand-off wait), and it goes once you answer in Claude Code.'
|
|
353
360
|
: 'The permission-prompt hooks were already installed.'
|
|
354
361
|
: !wanted
|
|
355
362
|
? `${merged.permissionChanged ? 'Took out the permission-prompt hooks' : 'Left out the permission-prompt hooks'}, as you asked: no card when Claude Code waits for your OK. To turn them on: npx pendingyou init --permission-cards${flag}`
|
|
@@ -470,6 +477,7 @@ export async function claudeState(io) {
|
|
|
470
477
|
const skill = (await readText(skillPath(io))) !== null;
|
|
471
478
|
let ours = (_event) => undefined;
|
|
472
479
|
let permission = false;
|
|
480
|
+
let notify = false;
|
|
473
481
|
const mod = { loads: false, dirs: [], present: false, plugin: false };
|
|
474
482
|
const loads = claude ? claudeVersion(claude) : null;
|
|
475
483
|
mod.loads = loads !== null && loadsMods(loads);
|
|
@@ -482,6 +490,7 @@ export async function claudeState(io) {
|
|
|
482
490
|
const all = objectAt(settings, 'hooks') ?? {};
|
|
483
491
|
ours = (event) => findHook(all, event, HOOKS[event]);
|
|
484
492
|
permission = Boolean(findHook(all, 'PermissionRequest', 'permission'));
|
|
493
|
+
notify = Boolean(findHook(all, 'Notification', 'notify'));
|
|
485
494
|
}
|
|
486
495
|
catch { }
|
|
487
496
|
const start = ours('SessionStart');
|
|
@@ -496,6 +505,7 @@ export async function claudeState(io) {
|
|
|
496
505
|
hooks: Boolean(start && ours('UserPromptSubmit')),
|
|
497
506
|
stopcheck: Boolean(ours('Stop')),
|
|
498
507
|
permission,
|
|
508
|
+
notify,
|
|
499
509
|
presence: Boolean(ours('SessionEnd')),
|
|
500
510
|
form: form ? { ...form, runs: await hookRuns(form), shim } : null,
|
|
501
511
|
mod,
|
package/dist/format.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export function noticeOf(heard) {
|
|
2
|
+
const given = heard.notice;
|
|
3
|
+
if (typeof given !== 'object' || given === null)
|
|
4
|
+
return null;
|
|
5
|
+
const { kind, from } = given;
|
|
6
|
+
if ((kind !== 'follow_up' && kind !== 'handed_over') || typeof from !== 'string')
|
|
7
|
+
return null;
|
|
8
|
+
return { kind, from };
|
|
9
|
+
}
|
|
1
10
|
export const HANDED_PREFIX = 'Pending You: your person handed you';
|
|
2
11
|
export const HANDED_REPLY = 'on the question handed to you';
|
|
3
12
|
export const HANDED_TALK_TODO = 'If you know now, answer_delegated with your answer and how you know; if you need more, reply_in_thread to “asker”; if it’s your person’s to decide, reply_in_thread with escalate true; if not, hand_back with what you checked.';
|
|
@@ -81,11 +90,37 @@ function describeHanded(heard, delegated) {
|
|
|
81
90
|
...(note?.cut ? [`(Cut short: get_request ${id} has all of it.)`] : []),
|
|
82
91
|
];
|
|
83
92
|
}
|
|
93
|
+
function describeNotice(heard, notice) {
|
|
94
|
+
const id = heard.requestId;
|
|
95
|
+
const from = clip(notice.from, MAX_NAME).text;
|
|
96
|
+
const ack = `ack_answer with requestId ${id}, name ${named(heard)} and expectedVersion ${heard.version}`;
|
|
97
|
+
if (notice.kind === 'handed_over')
|
|
98
|
+
return [
|
|
99
|
+
`Pending You: your person handed your open cards to ${from}, ${id} among them.`,
|
|
100
|
+
`Stop working on them: ${from} answers them and follows up. Call ${ack} to say you’ve stopped.`,
|
|
101
|
+
];
|
|
102
|
+
let cut = false;
|
|
103
|
+
const wrote = heard.messages.map((message) => {
|
|
104
|
+
const said = quoted(message.body, MAX_MESSAGE);
|
|
105
|
+
cut ||= said.cut;
|
|
106
|
+
return said.line;
|
|
107
|
+
});
|
|
108
|
+
return [
|
|
109
|
+
`Pending You: ${from} followed up on ${id}, which it answered for your person.`,
|
|
110
|
+
`Question: ${clip(heard.title, MAX_TITLE).text}`,
|
|
111
|
+
...wrote,
|
|
112
|
+
`Act on it if it needs anything, then call ${ack} to mark it heard.`,
|
|
113
|
+
...(cut ? [`(Cut short: get_request ${id} has all of it.)`] : []),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
84
116
|
export function describe(heard, again) {
|
|
85
117
|
const id = heard.requestId;
|
|
86
118
|
const delegated = delegatedOf(heard);
|
|
87
119
|
if (delegated)
|
|
88
120
|
return describeHanded(heard, delegated);
|
|
121
|
+
const notice = noticeOf(heard);
|
|
122
|
+
if (notice && heard.status !== 'answered')
|
|
123
|
+
return describeNotice(heard, notice);
|
|
89
124
|
const question = `Question: ${clip(heard.title, MAX_TITLE).text}`;
|
|
90
125
|
let cut = false;
|
|
91
126
|
const wrote = heard.messages.map((message) => {
|
|
@@ -111,7 +146,7 @@ export function describe(heard, again) {
|
|
|
111
146
|
lines.push(`Pending You: ${id} is answered.`, question, `Answer: ${answer.text}`);
|
|
112
147
|
}
|
|
113
148
|
if (wrote.length)
|
|
114
|
-
lines.push(
|
|
149
|
+
lines.push(`${noticeOf(heard) ? 'Also' : 'They also wrote'}: ${wrote.join(' ')}`);
|
|
115
150
|
lines.push(`Act on it, then call ack_answer with requestId ${id}, name ${named(heard)}, expectedVersion ${heard.version} and a one-line outcome.`);
|
|
116
151
|
break;
|
|
117
152
|
}
|
package/dist/main.js
CHANGED
|
@@ -46,7 +46,8 @@ async function run(io, command, hook = { fallback: '' }) {
|
|
|
46
46
|
case 'posted':
|
|
47
47
|
return posted(io, { origin: command.origin, app: command.app ?? DEFAULT_APP });
|
|
48
48
|
case 'permission':
|
|
49
|
-
case 'permission-done':
|
|
49
|
+
case 'permission-done':
|
|
50
|
+
case 'notify': {
|
|
50
51
|
const app = command.app ?? DEFAULT_APP;
|
|
51
52
|
if (!isPromptApp(app))
|
|
52
53
|
return 0;
|
package/dist/permission.js
CHANGED
|
@@ -13,6 +13,10 @@ import { machineOf } from "./remote.js";
|
|
|
13
13
|
export const GRACE_MS = 10_000;
|
|
14
14
|
export const TITLE_MAX = 90;
|
|
15
15
|
export const WAITING = 'Claude Code is waiting for your OK';
|
|
16
|
+
export const BACKGROUND_AGENT = ' (a background agent)';
|
|
17
|
+
const QUIET_MODES = new Set(['bypassPermissions', 'dontAsk']);
|
|
18
|
+
export const NOTICE_MS = 4000;
|
|
19
|
+
const BATCH_MS = 1000;
|
|
16
20
|
export const PROMPT_APPS = ['claude-code', 'codex'];
|
|
17
21
|
const CLAUDE = 'claude-code';
|
|
18
22
|
export const isPromptApp = (app) => PROMPT_APPS.includes(app);
|
|
@@ -47,9 +51,10 @@ function canonical(value) {
|
|
|
47
51
|
return JSON.stringify(value) ?? 'null';
|
|
48
52
|
}
|
|
49
53
|
const ANSWERED_IN_INPUT = new Set(['AskUserQuestion', 'ExitPlanMode']);
|
|
50
|
-
export function promptKey(tool, input) {
|
|
54
|
+
export function promptKey(tool, input, agent) {
|
|
51
55
|
const what = ANSWERED_IN_INPUT.has(tool) ? '' : canonical(input ?? {});
|
|
52
|
-
|
|
56
|
+
const whose = agent ? `${agent}\n` : '';
|
|
57
|
+
return createHash('sha256').update(`${whose}${tool}\n${what}`).digest('hex').slice(0, 32);
|
|
53
58
|
}
|
|
54
59
|
const text = (value) => (typeof value === 'string' ? value : '');
|
|
55
60
|
function commandText(value) {
|
|
@@ -134,6 +139,20 @@ export function promptWords(tool, input, home, app = CLAUDE) {
|
|
|
134
139
|
}
|
|
135
140
|
return said(toolName(tool), `to use ${toolName(tool)}`);
|
|
136
141
|
}
|
|
142
|
+
export const fromAgent = (title) => `${cut(title, TITLE_MAX - [...BACKGROUND_AGENT].length)}${BACKGROUND_AGENT}`;
|
|
143
|
+
const noticeWords = (message) => redactText(text(message), 120);
|
|
144
|
+
function noticeOf(message, now) {
|
|
145
|
+
return {
|
|
146
|
+
key: promptKey('Notification', { message }),
|
|
147
|
+
title: cut(WAITING, TITLE_MAX),
|
|
148
|
+
what: message || 'a dialog in Claude Code',
|
|
149
|
+
doing: '',
|
|
150
|
+
...(message ? { reason: message } : {}),
|
|
151
|
+
notice: true,
|
|
152
|
+
shown: now,
|
|
153
|
+
at: now,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
137
156
|
async function load(io, session) {
|
|
138
157
|
const file = await readJson(pathOf(io, session)).catch(() => null);
|
|
139
158
|
if (!isObject(file) || !Array.isArray(file.prompts) || typeof file.origin !== 'string')
|
|
@@ -152,6 +171,7 @@ async function change(io, session, edit) {
|
|
|
152
171
|
});
|
|
153
172
|
}
|
|
154
173
|
const alive = (state, now) => Boolean(state.worker && now - state.worker.at < LEASE_STALE_MS);
|
|
174
|
+
const seen = (prompt) => !prompt.quiet || prompt.shown !== undefined;
|
|
155
175
|
function claim(state, now) {
|
|
156
176
|
if (alive(state, now))
|
|
157
177
|
return null;
|
|
@@ -169,10 +189,19 @@ function startWorker(io, origin, session, worker) {
|
|
|
169
189
|
...(origin === DEFAULT_ORIGIN ? [] : ['--origin', origin]),
|
|
170
190
|
]);
|
|
171
191
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
192
|
+
const SUBAGENT_TRANSCRIPT = /^(.*)([/\\])([A-Za-z0-9][A-Za-z0-9_-]{0,99})[/\\]subagents[/\\][^/\\]+\.jsonl$/;
|
|
193
|
+
function whereOf(input) {
|
|
194
|
+
const transcript = text(input.transcript_path);
|
|
195
|
+
const parent = text(input.agent_id) ? SUBAGENT_TRANSCRIPT.exec(transcript) : null;
|
|
196
|
+
if (parent) {
|
|
197
|
+
const [, folder = '', slash = '/', session = ''] = parent;
|
|
198
|
+
return { session, transcript: `${folder}${slash}${session}.jsonl` };
|
|
199
|
+
}
|
|
200
|
+
const session = text(input.session_id);
|
|
201
|
+
return SESSION.test(session) ? { session, transcript } : null;
|
|
202
|
+
}
|
|
203
|
+
const agentOf = (input) => text(input.agent_id).slice(0, 100);
|
|
204
|
+
const agentTypeOf = (input) => cut(text(input.agent_type).replace(/[^\w.:-]/g, ''), 40);
|
|
176
205
|
const keyInput = (app, input) => {
|
|
177
206
|
if (app !== 'codex' || !isObject(input) || !('description' in input))
|
|
178
207
|
return input;
|
|
@@ -185,59 +214,76 @@ function reasonOf(app, input) {
|
|
|
185
214
|
const reason = redactText(text(input.description).replace(/\s+/g, ' ').trim(), 120);
|
|
186
215
|
return reason || null;
|
|
187
216
|
}
|
|
217
|
+
const wentOn = (agent, now) => (prompt) => Boolean(prompt.quiet) && (prompt.agent ?? '') === agent && prompt.at <= now - BATCH_MS;
|
|
188
218
|
async function asked(io, origin, input, app) {
|
|
189
219
|
if (io.platform === 'win32')
|
|
190
220
|
return;
|
|
191
|
-
if (app === 'claude-code')
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (input.permission_mode === 'dontAsk' || input.permission_mode === 'bypassPermissions')
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const session = sessionOf(input);
|
|
221
|
+
if (app === 'claude-code' && /^sdk-/.test(io.env.CLAUDE_CODE_ENTRYPOINT ?? ''))
|
|
222
|
+
return;
|
|
223
|
+
const where = whereOf(input);
|
|
198
224
|
const tool = text(input.tool_name);
|
|
199
|
-
if (!
|
|
225
|
+
if (!where || !tool)
|
|
200
226
|
return;
|
|
201
227
|
if ((await readCredential(io, origin, app))?.kind !== 'connection')
|
|
202
228
|
return;
|
|
203
229
|
const now = io.now();
|
|
204
|
-
const agent =
|
|
230
|
+
const agent = agentOf(input);
|
|
231
|
+
const subagent = app === 'claude-code' ? agent : '';
|
|
232
|
+
const agentType = subagent ? agentTypeOf(input) : '';
|
|
233
|
+
const quiet = app === 'claude-code' && QUIET_MODES.has(text(input.permission_mode));
|
|
234
|
+
const words = promptWords(tool, isObject(input.tool_input) ? input.tool_input : {}, io.home, app);
|
|
205
235
|
const prompt = {
|
|
206
|
-
key: promptKey(tool, keyInput(app, input.tool_input)),
|
|
236
|
+
key: promptKey(tool, keyInput(app, input.tool_input), subagent || undefined),
|
|
207
237
|
...(app === 'codex' ? { tool: tool.slice(0, 100) } : {}),
|
|
208
238
|
...(reasonOf(app, input.tool_input)
|
|
209
239
|
? { reason: reasonOf(app, input.tool_input) }
|
|
210
240
|
: {}),
|
|
211
|
-
...
|
|
212
|
-
...(
|
|
241
|
+
...words,
|
|
242
|
+
...(subagent ? { title: fromAgent(words.title) } : {}),
|
|
243
|
+
...(agent ? { agent } : {}),
|
|
244
|
+
...(agentType ? { agentType } : {}),
|
|
245
|
+
...(quiet ? { quiet: true } : {}),
|
|
213
246
|
at: now,
|
|
214
247
|
};
|
|
215
248
|
const cwd = text(input.cwd) || io.cwd;
|
|
216
|
-
const transcript = text(input.transcript_path);
|
|
217
249
|
const link = app === 'claude-code' ? sessionLink(io.env) : null;
|
|
218
250
|
let worker = null;
|
|
219
|
-
await change(io, session, (state) => {
|
|
220
|
-
const next = state ?? {
|
|
251
|
+
await change(io, where.session, (state) => {
|
|
252
|
+
const next = state ?? {
|
|
253
|
+
version: 1,
|
|
254
|
+
origin,
|
|
255
|
+
session: where.session,
|
|
256
|
+
cwd,
|
|
257
|
+
prompts: [],
|
|
258
|
+
at: now,
|
|
259
|
+
};
|
|
221
260
|
next.origin = origin;
|
|
222
261
|
if (app === 'claude-code')
|
|
223
262
|
delete next.app;
|
|
224
263
|
else
|
|
225
264
|
next.app = app;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
265
|
+
if (!state || !subagent)
|
|
266
|
+
next.cwd = cwd;
|
|
267
|
+
if (where.transcript && (!subagent || !next.transcript))
|
|
268
|
+
next.transcript = where.transcript;
|
|
229
269
|
if (link)
|
|
230
270
|
next.link = link;
|
|
271
|
+
const kept = app === 'claude-code'
|
|
272
|
+
? next.prompts.filter((each) => !wentOn(agent, now)(each))
|
|
273
|
+
: next.prompts;
|
|
274
|
+
const dropped = kept.length < next.prompts.length;
|
|
275
|
+
next.prompts = kept;
|
|
231
276
|
if (!next.prompts.some((each) => each.key === prompt.key))
|
|
232
277
|
next.prompts.push(prompt);
|
|
233
278
|
next.prompts = next.prompts.slice(-MAX_PROMPTS);
|
|
234
279
|
delete next.settled;
|
|
235
280
|
next.at = now;
|
|
236
|
-
|
|
281
|
+
if (!quiet || (dropped && next.card))
|
|
282
|
+
worker = claim(next, now);
|
|
237
283
|
return next;
|
|
238
284
|
});
|
|
239
285
|
if (worker)
|
|
240
|
-
startWorker(io, origin, session, worker);
|
|
286
|
+
startWorker(io, origin, where.session, worker);
|
|
241
287
|
}
|
|
242
288
|
async function settle(io, session, which, how) {
|
|
243
289
|
const before = await load(io, session);
|
|
@@ -256,8 +302,8 @@ async function settle(io, session, which, how) {
|
|
|
256
302
|
state.prompts = left;
|
|
257
303
|
state.settled = how;
|
|
258
304
|
state.at = now;
|
|
259
|
-
if (!state.card && left.
|
|
260
|
-
return alive(state, now) ? state : null;
|
|
305
|
+
if (!state.card && !left.some(seen))
|
|
306
|
+
return left.length > 0 || alive(state, now) ? state : null;
|
|
261
307
|
worker = claim(state, now);
|
|
262
308
|
return state;
|
|
263
309
|
});
|
|
@@ -265,16 +311,18 @@ async function settle(io, session, which, how) {
|
|
|
265
311
|
startWorker(io, origin, session, worker);
|
|
266
312
|
}
|
|
267
313
|
async function ran(io, input, app) {
|
|
268
|
-
const
|
|
314
|
+
const where = whereOf(input);
|
|
269
315
|
const tool = text(input.tool_name);
|
|
270
|
-
if (!
|
|
316
|
+
if (!where || !tool)
|
|
271
317
|
return;
|
|
272
|
-
const
|
|
273
|
-
const
|
|
318
|
+
const agent = agentOf(input);
|
|
319
|
+
const key = promptKey(tool, keyInput(app, input.tool_input), app === 'claude-code' ? agent || undefined : undefined);
|
|
320
|
+
const before = await load(io, where.session);
|
|
274
321
|
if (!before?.prompts.length)
|
|
275
322
|
return;
|
|
276
323
|
const exact = before.prompts.some((prompt) => prompt.key === key);
|
|
277
|
-
|
|
324
|
+
const movedOn = wentOn(agent, io.now());
|
|
325
|
+
await settle(io, where.session, (prompt) => prompt.key === key || (app === 'codex' ? !exact && prompt.tool === tool : movedOn(prompt)), 'answered');
|
|
278
326
|
}
|
|
279
327
|
export async function endPrompts(io, session) {
|
|
280
328
|
try {
|
|
@@ -288,7 +336,7 @@ export async function settlePrompts(io, input) {
|
|
|
288
336
|
if (!input.session || !SESSION.test(input.session))
|
|
289
337
|
return;
|
|
290
338
|
const subagentsDone = input.event === 'stop' && !input.subagents;
|
|
291
|
-
await settle(io, input.session, (prompt) => !prompt.agent || subagentsDone, 'answered');
|
|
339
|
+
await settle(io, input.session, (prompt) => (!prompt.agent && !prompt.notice) || subagentsDone, 'answered');
|
|
292
340
|
}
|
|
293
341
|
catch { }
|
|
294
342
|
}
|
|
@@ -299,6 +347,64 @@ export function subagentsRunning(input) {
|
|
|
299
347
|
task.type === 'subagent' &&
|
|
300
348
|
!['completed', 'failed', 'killed', 'cancelled', 'stopped'].includes(text(task.status)));
|
|
301
349
|
}
|
|
350
|
+
function noticeMsOf(env) {
|
|
351
|
+
const given = Number(env.PENDINGYOU_PERMISSION_NOTICE_MS);
|
|
352
|
+
return Number.isInteger(given) && given >= 0 ? given : NOTICE_MS;
|
|
353
|
+
}
|
|
354
|
+
async function notified(io, origin, input) {
|
|
355
|
+
if (io.platform === 'win32')
|
|
356
|
+
return;
|
|
357
|
+
if (/^sdk-/.test(io.env.CLAUDE_CODE_ENTRYPOINT ?? ''))
|
|
358
|
+
return;
|
|
359
|
+
if (input.notification_type !== 'permission_prompt')
|
|
360
|
+
return;
|
|
361
|
+
const where = whereOf(input);
|
|
362
|
+
if (!where)
|
|
363
|
+
return;
|
|
364
|
+
if ((await readCredential(io, origin, CLAUDE))?.kind !== 'connection')
|
|
365
|
+
return;
|
|
366
|
+
const now = io.now();
|
|
367
|
+
const before = now - noticeMsOf(io.env);
|
|
368
|
+
const message = noticeWords(input.message);
|
|
369
|
+
const cwd = text(input.cwd) || io.cwd;
|
|
370
|
+
const link = sessionLink(io.env);
|
|
371
|
+
let worker = null;
|
|
372
|
+
await change(io, where.session, (state) => {
|
|
373
|
+
const next = state ?? {
|
|
374
|
+
version: 1,
|
|
375
|
+
origin,
|
|
376
|
+
session: where.session,
|
|
377
|
+
cwd,
|
|
378
|
+
prompts: [],
|
|
379
|
+
at: now,
|
|
380
|
+
};
|
|
381
|
+
next.origin = origin;
|
|
382
|
+
delete next.app;
|
|
383
|
+
next.cwd = cwd;
|
|
384
|
+
if (where.transcript)
|
|
385
|
+
next.transcript = where.transcript;
|
|
386
|
+
if (link)
|
|
387
|
+
next.link = link;
|
|
388
|
+
const shows = next.prompts
|
|
389
|
+
.filter((prompt) => prompt.shown === undefined && !prompt.notice && prompt.at <= before)
|
|
390
|
+
.at(-1);
|
|
391
|
+
if (shows) {
|
|
392
|
+
shows.shown = now;
|
|
393
|
+
delete shows.failed;
|
|
394
|
+
}
|
|
395
|
+
else if (next.prompts.some((prompt) => prompt.shown !== undefined)) {
|
|
396
|
+
return state;
|
|
397
|
+
}
|
|
398
|
+
else
|
|
399
|
+
next.prompts = [...next.prompts, noticeOf(message, now)].slice(-MAX_PROMPTS);
|
|
400
|
+
delete next.settled;
|
|
401
|
+
next.at = now;
|
|
402
|
+
worker = claim(next, now);
|
|
403
|
+
return next;
|
|
404
|
+
});
|
|
405
|
+
if (worker)
|
|
406
|
+
startWorker(io, origin, where.session, worker);
|
|
407
|
+
}
|
|
302
408
|
export async function permissionHook(io, options) {
|
|
303
409
|
const app = options.app ?? CLAUDE;
|
|
304
410
|
try {
|
|
@@ -313,10 +419,14 @@ export async function permissionHook(io, options) {
|
|
|
313
419
|
case 'PostToolUseFailure':
|
|
314
420
|
await ran(io, input, app);
|
|
315
421
|
break;
|
|
422
|
+
case 'Notification':
|
|
423
|
+
if (app === CLAUDE)
|
|
424
|
+
await notified(io, options.origin, input);
|
|
425
|
+
break;
|
|
316
426
|
case 'SessionEnd': {
|
|
317
|
-
const
|
|
318
|
-
if (
|
|
319
|
-
await settle(io, session, () => true, 'ended');
|
|
427
|
+
const where = whereOf(input);
|
|
428
|
+
if (where)
|
|
429
|
+
await settle(io, where.session, () => true, 'ended');
|
|
320
430
|
break;
|
|
321
431
|
}
|
|
322
432
|
}
|
|
@@ -324,22 +434,24 @@ export async function permissionHook(io, options) {
|
|
|
324
434
|
catch { }
|
|
325
435
|
return 0;
|
|
326
436
|
}
|
|
437
|
+
const dueOf = (prompt, grace) => Math.min(prompt.quiet ? Number.POSITIVE_INFINITY : prompt.at + grace, prompt.shown ?? Number.POSITIVE_INFINITY);
|
|
438
|
+
const shownOf = (prompts) => (prompts.filter((prompt) => !prompt.notice).at(-1) ?? prompts.at(-1));
|
|
327
439
|
export function planFor(state, now, grace) {
|
|
328
|
-
|
|
440
|
+
const prompts = state.prompts.filter(seen);
|
|
441
|
+
if (prompts.length === 0)
|
|
329
442
|
return state.card ? { kind: 'cancel' } : { kind: 'done' };
|
|
330
|
-
const newest = state.prompts.at(-1);
|
|
331
443
|
if (state.card) {
|
|
444
|
+
const newest = shownOf(prompts);
|
|
332
445
|
if (state.card.key === newest.key)
|
|
333
446
|
return { kind: 'done' };
|
|
334
|
-
const due = newest
|
|
447
|
+
const due = dueOf(newest, grace) - now;
|
|
335
448
|
return due > 0 ? { kind: 'wait', ms: due } : { kind: 'update', prompt: newest };
|
|
336
449
|
}
|
|
337
|
-
const live =
|
|
338
|
-
|
|
339
|
-
if (!oldest)
|
|
450
|
+
const live = prompts.filter((prompt) => !prompt.failed);
|
|
451
|
+
if (live.length === 0)
|
|
340
452
|
return { kind: 'done' };
|
|
341
|
-
const due =
|
|
342
|
-
return due > 0 ? { kind: 'wait', ms: due } : { kind: 'post', prompt: live
|
|
453
|
+
const due = Math.min(...live.map((prompt) => dueOf(prompt, grace))) - now;
|
|
454
|
+
return due > 0 ? { kind: 'wait', ms: due } : { kind: 'post', prompt: shownOf(live) };
|
|
343
455
|
}
|
|
344
456
|
function graceOf(env) {
|
|
345
457
|
const given = Number(env.PENDINGYOU_PERMISSION_GRACE_MS);
|
|
@@ -470,14 +582,25 @@ async function areaFor(io, client, state, name) {
|
|
|
470
582
|
throw new McpFailure(-32000, 'Pending You made no area.');
|
|
471
583
|
return areaId;
|
|
472
584
|
}
|
|
585
|
+
const byAgent = (state, prompt) => appOf(state) === 'claude-code' && Boolean(prompt.agent);
|
|
586
|
+
function changeNoteOf(state, prompt) {
|
|
587
|
+
if (prompt.doing === 'to answer its question')
|
|
588
|
+
return cut(`Now waiting for your answer: ${prompt.what}`, 120);
|
|
589
|
+
const agent = byAgent(state, prompt) ? BACKGROUND_AGENT : '';
|
|
590
|
+
return `${cut(`Now waiting for your OK: ${prompt.what}`, 120 - [...agent].length)}${agent}`;
|
|
591
|
+
}
|
|
473
592
|
function cardWords(io, state, prompt, name, machine) {
|
|
474
593
|
const app = appOf(state);
|
|
475
594
|
const who = name === defaultName(app) ? APP_NAMES[app] : `${APP_NAMES[app]} (${name})`;
|
|
476
595
|
const waiting = prompt.doing === 'to answer its question'
|
|
477
596
|
? 'is waiting for your answer to its question'
|
|
478
|
-
: `is waiting for your OK ${prompt.doing}`;
|
|
597
|
+
: `is waiting for your OK${prompt.doing ? ` ${prompt.doing}` : ''}`;
|
|
479
598
|
const why = prompt.reason ? ` It says: “${prompt.reason}”.` : '';
|
|
480
|
-
const
|
|
599
|
+
const where = `in ${tilde(io, state.cwd)} on ${machine}`;
|
|
600
|
+
const asker = byAgent(state, prompt)
|
|
601
|
+
? `A background agent${prompt.agentType ? ` (${prompt.agentType})` : ''} of ${who}, ${where},`
|
|
602
|
+
: `${who} ${where}`;
|
|
603
|
+
const summary = cut(`${asker} ${waiting}.${why} It goes on once you answer it there.`, 400);
|
|
481
604
|
const place = `${APP_NAMES[app]} on ${machine}`;
|
|
482
605
|
const linked = state.link ? `${place}, ${state.link}` : place;
|
|
483
606
|
return {
|
|
@@ -548,9 +671,7 @@ async function update(io, client, state, prompt, worker) {
|
|
|
548
671
|
requestId: card.requestId,
|
|
549
672
|
name: card.name,
|
|
550
673
|
expectedVersion,
|
|
551
|
-
changeNote:
|
|
552
|
-
? `Now waiting for your answer: ${prompt.what}`
|
|
553
|
-
: `Now waiting for your OK: ${prompt.what}`, 120),
|
|
674
|
+
changeNote: changeNoteOf(state, prompt),
|
|
554
675
|
title: prompt.title,
|
|
555
676
|
summary: words.summary,
|
|
556
677
|
action: words.action,
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.16.0';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou-wake",
|
|
3
3
|
"displayName": "Pending You",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.32.0",
|
|
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.
|
|
7
|
+
Generated from recordplane/pendingyou (packages/claude-plugin/src/wake) with Pending You’s guide 2.32.0; send changes there.
|
package/mod/hooks/wake.js
CHANGED
|
@@ -346,6 +346,11 @@ function line(card) {
|
|
|
346
346
|
`your person answered ${titled(card)}`,
|
|
347
347
|
`call ${get}, act on their words, then ack_answer with its version and a one-line outcome`,
|
|
348
348
|
];
|
|
349
|
+
case 'delegated':
|
|
350
|
+
return [
|
|
351
|
+
`there’s word on ${titled(card)}, which your person handed to another assistant`,
|
|
352
|
+
`call ${get} and do what its helper.next says`,
|
|
353
|
+
];
|
|
349
354
|
default:
|
|
350
355
|
return [
|
|
351
356
|
`your person wrote to you on ${titled(card)}`,
|
|
@@ -355,7 +360,9 @@ function line(card) {
|
|
|
355
360
|
}
|
|
356
361
|
const capital = (sentence) => sentence.charAt(0).toUpperCase() + sentence.slice(1);
|
|
357
362
|
export function wakeText(cards) {
|
|
358
|
-
const asked = cards.some((card) => card.state?.turn === 'agent' &&
|
|
363
|
+
const asked = cards.some((card) => card.state?.turn === 'agent' &&
|
|
364
|
+
!isClosed(card.state.status) &&
|
|
365
|
+
card.state.status !== 'delegated');
|
|
359
366
|
const reopen = asked
|
|
360
367
|
? ' If they asked you something or you need more from them, reply_in_thread with reopen instead.'
|
|
361
368
|
: '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pendingyou",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://www.pendingyou.com",
|