@paigy/mcp 0.40.38 → 0.40.40
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 +11 -12
- package/dist/{chunk-2OMTJ3JN.js → chunk-6FG3PZR2.js} +30 -7
- package/dist/{chunk-3YXZUGFL.js → chunk-H2EBILBE.js} +323 -115
- package/dist/{chunk-FXPZQN3E.js → chunk-PKJJSHLW.js} +41 -44
- package/dist/{chunk-4PP522IN.js → chunk-V4OVLFMP.js} +242 -113
- package/dist/{dist-JDIRRDOJ.js → dist-EJWDUF5J.js} +3 -1
- package/dist/enable.js +3 -3
- package/dist/index.js +45 -21
- package/dist/listen.js +32 -6
- package/dist/onboard.js +18 -6
- package/dist/slot.js +1 -1
- package/dist/stalled.js +10 -5
- package/dist/statusline.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ../../packages/schema/dist/index.js
|
|
2
2
|
import { z as z12 } from "zod";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { z as z3 } from "zod";
|
|
@@ -17,6 +17,7 @@ import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
|
|
|
17
17
|
import { z as z10 } from "zod";
|
|
18
18
|
import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
|
|
19
19
|
import { z as z11 } from "zod";
|
|
20
|
+
import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
|
|
20
21
|
var OPTIONS_MIN = 2;
|
|
21
22
|
var OPTIONS_MAX = 6;
|
|
22
23
|
var OPTION_LABEL_MAX = 50;
|
|
@@ -46,7 +47,7 @@ var OptionInputSchema = z.object({
|
|
|
46
47
|
imgUrl: OptionSchema.shape.image
|
|
47
48
|
}).strict();
|
|
48
49
|
var NIGHT = { from: 23, to: 7 };
|
|
49
|
-
var ASK_MAX =
|
|
50
|
+
var ASK_MAX = 80;
|
|
50
51
|
var UNITS_MAX = 12;
|
|
51
52
|
var UNIT_TITLE_MAX = 50;
|
|
52
53
|
var UNIT_BODY_MAX = 200;
|
|
@@ -111,15 +112,15 @@ var CompletionSchema = z3.object({
|
|
|
111
112
|
}).strict();
|
|
112
113
|
var CompleteChangeSchema = CompletionSchema.extend({ kind: z3.literal("complete") });
|
|
113
114
|
var ASKING_RULES = `HOW TO ASK:
|
|
114
|
-
1.
|
|
115
|
-
2.
|
|
116
|
-
3. \`workItBlocks\` only
|
|
117
|
-
4.
|
|
118
|
-
5. One question, one row
|
|
119
|
-
6. Read the Goal
|
|
120
|
-
7.
|
|
121
|
-
8. A diagnosis says when, why and how it happens, then proposes one fix.
|
|
122
|
-
9.
|
|
115
|
+
1. One question per object in \`questions\`, so each is answered on its own.
|
|
116
|
+
2. \`goalId\` names the existing Goal it is about (create one with manage_goals first); the answer comes back there.
|
|
117
|
+
3. \`workItBlocks\` only when you are truly blocked: it asks for a call.
|
|
118
|
+
4. No while-loops. While any Call is open, even one your question joined from their items, keep receiving through empty windows; otherwise stop at one: the answer reaches you on the Goal. Yield only with a listener or wakeup.
|
|
119
|
+
5. One question, one row: never re-send one still waiting; take back one that no longer matters with \`withdrawQuestionIds\`.
|
|
120
|
+
6. Read the Goal before asking; never re-ask what was answered or shipped. \`replied\` means they chose no option: it is still yours to settle.
|
|
121
|
+
7. Give options when you have them. Each label names its choice in full (never "Option 2"); never add "Other". \`pickMode\`: one (default), many, rank.
|
|
122
|
+
8. A diagnosis says when, why and how it happens, then proposes one fix.
|
|
123
|
+
9. Give a report its \`reply\`: one line in THEIR voice for THAT report, so you learn whether they verified it or only received it. Required when it reaches them or completes a Goal.`;
|
|
123
124
|
function draft2020(node) {
|
|
124
125
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
125
126
|
if (node && typeof node === "object") {
|
|
@@ -162,7 +163,7 @@ var userExplicitlyRequested = z4.enum(["any", "call"]).optional().describe(
|
|
|
162
163
|
`Only what the person said about THIS message, in so many words: "any" when they asked you to send it to them ("send it to my phone", "message me the result"), "call" when they asked to be called about it. It is then delivered (a call rings for the whole contact, and carries everything waiting on them); omitted, an update is only recorded as the Goal's progress unless the person needs it. Never set it to make a message louder.`
|
|
163
164
|
);
|
|
164
165
|
var units = z4.array(UnitInputSchema).optional().describe(
|
|
165
|
-
`The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit. Leave it out when the main text says it all.`
|
|
166
|
+
`The context this message needs, as up to ${UNITS_MAX} units in reading order, each {title, body}: a title of at most ${UNIT_TITLE_MAX} characters and a body of at most ${UNIT_BODY_MAX}. One idea per unit. Leave it out when the main text says it all. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.`
|
|
166
167
|
);
|
|
167
168
|
var UpdateInputSchema = z4.object({
|
|
168
169
|
message: z4.string().trim().min(1).describe(`The one point, at most ${ASK_MAX} characters; the context goes in units.`),
|
|
@@ -217,7 +218,7 @@ var questionFields = {
|
|
|
217
218
|
),
|
|
218
219
|
goalId: z4.string().uuid().describe("The Goal the question is about, which must exist (create it with manage_goals first). Its answer comes back there."),
|
|
219
220
|
units,
|
|
220
|
-
options: z4.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200."),
|
|
221
|
+
options: z4.array(OptionInputSchema).min(1).max(6).optional().describe("The choices, when you have them. Each label stands on its own, at most 50 characters; a hint, at most 200. They can always answer in their own words, so no 'Other' or 'None of these'."),
|
|
221
222
|
// HOW THE OPTIONS ARE ANSWERED, SAID BY THE AGENT (owner, 2026-10-02: "one and many makes sense";
|
|
222
223
|
// brain_prompts.md §1: structured agent questions need no model). The app draws all three.
|
|
223
224
|
pickMode: z4.enum(["one", "many", "rank"]).optional().describe(
|
|
@@ -259,7 +260,9 @@ var QuestionDependencySchema = z4.object({
|
|
|
259
260
|
reason: reason.optional()
|
|
260
261
|
}).strict().refine(actionFits, ACTION_FITS);
|
|
261
262
|
var sendFields = {
|
|
262
|
-
completions: z4.array(CompletionSchema).min(1).max(20).optional().describe(
|
|
263
|
+
completions: z4.array(CompletionSchema).min(1).max(20).optional().describe(
|
|
264
|
+
"COMPLETIONS close and report in one operation, the same contract as manage_goals kind:complete: closure and the report commit together or neither does. Required children and dependencies must already be finished. options suggest next steps: a tap acknowledges the report and requests that step, and never reopens the Goal (follow-up work needs a new one). No reviewer rejudges it: say only what your evidence supports. Ordinary updates never close a Goal."
|
|
265
|
+
),
|
|
263
266
|
updates: z4.array(UpdateInputSchema).min(1).max(20).optional().describe(
|
|
264
267
|
"News: progress, findings, a result. An update asks nothing. It reaches the person only when they asked for it (userExplicitlyRequested), when it answers something they said, or once its Goal is done; otherwise it is recorded as the Goal's progress and nobody is notified."
|
|
265
268
|
),
|
|
@@ -273,7 +276,7 @@ var sendFields = {
|
|
|
273
276
|
};
|
|
274
277
|
var ContactSchema = z4.object({
|
|
275
278
|
...sendFields,
|
|
276
|
-
questions: z4.array(QuestionInputSchema).min(1).max(20).optional().describe("Questions for the person, one per object."),
|
|
279
|
+
questions: z4.array(QuestionInputSchema).min(1).max(20).optional().describe("Questions for the person, one per object, each sent exactly as written, with no reading in between."),
|
|
277
280
|
/** RECEIVING (2I, I2; brain_prompts.md §2.2): the events this agent handled. */
|
|
278
281
|
ackEventIds: z4.array(z4.string().uuid()).min(1).max(100).optional().describe(
|
|
279
282
|
"The exact eventIds of the events you have handled: an acknowledgment (ACK). Reading alone acknowledges nothing, so an event comes back until you acknowledge it; after that the next batch can come. Each recipient acknowledges separately, for itself only. A Question you owe is answered (answers), never acknowledged away."
|
|
@@ -289,15 +292,13 @@ var ContactWireSchema = z4.object({
|
|
|
289
292
|
questions: z4.array(QuestionWireSchema).min(1).max(20).optional()
|
|
290
293
|
}).strict().refine(sendsAnything, "a contact sends at least one update, question, answer, withdrawal or question dependency");
|
|
291
294
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
292
|
-
var CONTACT_DESCRIPTION = "Send to and receive from the person: completions, updates, questions, answers to questions addressed to you, withdrawals
|
|
293
|
-
|
|
294
|
-
CUT A LONG MESSAGE BEFORE YOU SEND IT: a question, an update's message or an answer is the one point, in at most ${ASK_MAX} characters; everything else it needs goes in 'units', up to ${UNITS_MAX} {title, body} in reading order, one idea each, every title at most ${UNIT_TITLE_MAX} characters and every body at most ${UNIT_BODY_MAX}. The person sees the main text and the titles, and opens a unit to read its body. Over a cap is refused by name (ask_too_long, too_many_units, unit_title_too_long, unit_body_too_long) and nothing is sent.
|
|
295
|
+
var CONTACT_DESCRIPTION = "Send to and receive from the person: completions, updates, questions, answers to questions addressed to you, withdrawals, and what a question holds up. Every Goal named must exist: an unknown one refuses the whole contact. Each item applies on its own: read `results`, where a failed one names why.\n\n" + ASKING_RULES + `
|
|
295
296
|
|
|
296
|
-
|
|
297
|
+
LENGTH: the question, message or answer is the one point, at most ${ASK_MAX} characters; the rest goes in \`units\`, one idea each. Over a cap is refused by name and nothing is sent. Several results are several updates.
|
|
297
298
|
|
|
298
|
-
|
|
299
|
+
UPDATES ask nothing. One reaches the person only when they asked for it, it answers something they said, or its Goal is done; else it is kept as progress. One that reaches them joins an open report card or a live call.
|
|
299
300
|
|
|
300
|
-
RECEIVING:
|
|
301
|
+
RECEIVING: contact({}) waits ~45 s; contact({wait:false}) returns \`events\` at once. Confirm the ones you handled with contact({ackEventIds}).`;
|
|
301
302
|
var CreateGoalSchema = z5.object({
|
|
302
303
|
outcome: z5.string().trim().min(1).max(1e4),
|
|
303
304
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -431,8 +432,8 @@ var AGENT_TOOLS = [
|
|
|
431
432
|
];
|
|
432
433
|
var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
433
434
|
function serverInstructions(opts) {
|
|
434
|
-
const waits = opts.waits ? "contact({
|
|
435
|
-
return `On startup and after a wake,
|
|
435
|
+
const waits = opts.waits ? "contact({wait:true}) waits ~45 s for a reply to what you sent; contact({}) for anything addressed to you." : "A contact here returns after one read; answers wake you, and contact({wait:false}) collects them.";
|
|
436
|
+
return `On startup and after a wake, contact({wait:false}) returns your events. They repeat until you acknowledge them with contact({ackEventIds}); reading acknowledges nothing. get_goals reads a Goal and its conversation; your first write to one puts you on it, and every Goal of your person is open to you. manage_goals creates, assigns and organizes work; to follow up later, defer the Goal with manage_goals (you're woken when due). Complete and report in one step: contact.completions or manage_goals kind:complete. Every read ends in \`next\`: your next step. ${waits} Report progress as a contact update; one they don't need yet stays on the Goal. An open Call is not proof it rang. Your user is remote: reach them only through Paigy, never the terminal; ask decisions with options.
|
|
436
437
|
|
|
437
438
|
${ASKING_RULES}`;
|
|
438
439
|
}
|
|
@@ -518,11 +519,28 @@ var messageBase = {
|
|
|
518
519
|
obligationIds: z6.array(id)
|
|
519
520
|
};
|
|
520
521
|
var BrainMessageSchema = z6.union([
|
|
521
|
-
|
|
522
|
+
// A MESSAGE TO THE PERSON KEEPS ITS SENTENCES (turns.md): the talker writes them one by one, and each is
|
|
523
|
+
// its own unit of speech (`<cycle>:<key>:<n>`). `text` is the same words joined, for every reader of a
|
|
524
|
+
// message's words. Absent on results written before sentences.
|
|
525
|
+
z6.object({
|
|
526
|
+
...messageBase,
|
|
527
|
+
to: z6.object({ kind: z6.literal("user") }).strict(),
|
|
528
|
+
sentences: z6.array(z6.string().min(1)).min(1).optional(),
|
|
529
|
+
// THE REPORTS IT TELLS, BY THEIR OWN IDENTITY (call 9d1c096c, 2026-10-15): the talker's message names the report
|
|
530
|
+
// Entries it speaks of, so the plan's line names that report rather than re-finding it by its Goal. Absent on
|
|
531
|
+
// results written before it.
|
|
532
|
+
tells: z6.array(z6.object({ id }).strict()).optional()
|
|
533
|
+
}).strict(),
|
|
522
534
|
z6.object({
|
|
523
535
|
...messageBase,
|
|
524
536
|
to: z6.object({ kind: z6.literal("agent"), agentId: z6.string().regex(/^agent:(?!paigy$)\S+$/) }).strict(),
|
|
525
|
-
call: z6.object({ callId: id, listen: z6.literal(true) }).strict().optional()
|
|
537
|
+
call: z6.object({ callId: id, listen: z6.literal(true) }).strict().optional(),
|
|
538
|
+
// WHAT IT ASKS THE AGENT, IN UNDER TEN WORDS (owner, 2026-10-15: "Yes, add the summary"): the talker's own, stored
|
|
539
|
+
// as the sent Entry's summary, which the recap's "Sent to <agent>: …" row shows. Absent on results written before it.
|
|
540
|
+
summary: z6.string().optional(),
|
|
541
|
+
// THE PERSON ASKED TO BE CALLED BACK ONCE THIS AGENT'S WORK IS DONE (turns.md, step 0): the agent owes
|
|
542
|
+
// the call, and its next contact about it rings them (`public.callbacks`).
|
|
543
|
+
callback: z6.literal(true).optional()
|
|
526
544
|
}).strict()
|
|
527
545
|
]);
|
|
528
546
|
var BrainSearchSchema = z6.object({
|
|
@@ -546,92 +564,97 @@ var BrainResultSchema = z6.object({
|
|
|
546
564
|
next: BrainNextSchema,
|
|
547
565
|
search: BrainSearchSchema.optional()
|
|
548
566
|
}).strict();
|
|
549
|
-
var
|
|
567
|
+
var str2 = z6.string();
|
|
550
568
|
var strs = z6.array(z6.string());
|
|
551
569
|
var CompactResultSchema = z6.object({
|
|
552
|
-
next: z6.object({ say: strs, then: z6.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason:
|
|
553
|
-
messages: z6.array(z6.object({ key:
|
|
554
|
-
entries: z6.array(z6.object({ key:
|
|
555
|
-
answers: z6.array(z6.object({ question:
|
|
570
|
+
next: z6.object({ say: strs, then: z6.enum(["listen", "hold", "end", "none"]), waitFor: strs, reason: str2, instructions: strs }).strict(),
|
|
571
|
+
messages: z6.array(z6.object({ key: str2, to: str2, text: str2, about: strs, entries: strs, owed: strs, inviteToCall: z6.boolean() }).strict()),
|
|
572
|
+
entries: z6.array(z6.object({ key: str2, from: str2, on: strs }).strict()),
|
|
573
|
+
answers: z6.array(z6.object({ question: str2, entries: strs, summary: str2, picked: strs }).strict()),
|
|
556
574
|
changes: z6.array(z6.object({
|
|
557
|
-
key:
|
|
575
|
+
key: str2,
|
|
558
576
|
what: z6.enum(["goal", "question", "lesson"]),
|
|
559
577
|
op: z6.enum(["create", "edit", "assign", "open", "complete", "cancel", "defer", "move", "block", "unblock", "withdraw", "remember", "revise"]),
|
|
560
|
-
id:
|
|
561
|
-
text:
|
|
562
|
-
title:
|
|
563
|
-
who:
|
|
564
|
-
under:
|
|
578
|
+
id: str2,
|
|
579
|
+
text: str2,
|
|
580
|
+
title: str2,
|
|
581
|
+
who: str2,
|
|
582
|
+
under: str2,
|
|
565
583
|
gate: z6.enum(["start", "complete", "answer", "none"]),
|
|
566
|
-
options: z6.array(z6.object({ id:
|
|
584
|
+
options: z6.array(z6.object({ id: str2, label: str2 }).strict()),
|
|
567
585
|
pick: z6.enum(["one", "many", "rank", "words"]),
|
|
568
586
|
entries: strs
|
|
569
587
|
}).strict()),
|
|
570
588
|
feedback: z6.array(strs),
|
|
571
|
-
search: z6.object({ query:
|
|
589
|
+
search: z6.object({ query: str2, within: strs, goal: str2 }).strict()
|
|
572
590
|
}).strict();
|
|
573
591
|
var COMPACT_RESULT_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
574
592
|
zodToJsonSchema2(CompactResultSchema, { $refStrategy: "none" })
|
|
575
593
|
);
|
|
576
|
-
var
|
|
594
|
+
var str3 = z7.string();
|
|
577
595
|
var strs2 = z7.array(z7.string());
|
|
578
596
|
var TalkerReplySchema = z7.object({
|
|
579
597
|
/** What to say and send, messages to the person first: spoken in the order written. `to` is "user"
|
|
580
|
-
* or an agent's handle; `
|
|
581
|
-
*
|
|
582
|
-
|
|
598
|
+
* or an agent's handle; `sentences` is what a message to the person says, one short sentence each
|
|
599
|
+
* ([] to an agent); `text` is what a message to an agent says ("" to the person); `summary` is what it
|
|
600
|
+
* asks the agent, in under ten words, as the person would put it ("" to the person; owner, 2026-10-15:
|
|
601
|
+
* "Yes, add the summary"), which the recap's "Sent to <agent>: …" row shows; `about` names
|
|
602
|
+
* handles; `blocks` is the item a question to an agent must be answered before ("" for none);
|
|
603
|
+
* `callback` is true on a message to the agent doing work the person asked to be called back about
|
|
604
|
+
* once it is done (false otherwise): that agent then owes the call (turns.md, step 0). */
|
|
605
|
+
messages: z7.array(z7.object({ key: str3, to: str3, sentences: strs2, text: str3, summary: str3, about: strs2, blocks: str3, callback: z7.boolean() }).strict()),
|
|
583
606
|
/** What the call does next: listen, hold (the person asked for a moment) or end (they asked to). */
|
|
584
607
|
then: z7.enum(["listen", "hold", "end"]),
|
|
585
608
|
/** The person's replies to items (replies, owner 2026-10-07): the item's handle, the chosen option
|
|
586
609
|
* IDs, the line handles, how they replied in under ten words (keeping any condition), and `defer`:
|
|
587
610
|
* "" when their words settle the item, else what a reply that puts it off waits for -- "call" (the
|
|
588
611
|
* end of this call), a question's handle (that question's answer) or an ISO time. */
|
|
589
|
-
replies: z7.array(z7.object({ item:
|
|
612
|
+
replies: z7.array(z7.object({ item: str3, options: strs2, lines: strs2, summary: str3, defer: str3 }).strict()),
|
|
590
613
|
/** The line handles that tell the talker how to run this call; they last until it ends and reach no agent. */
|
|
591
614
|
instruction: strs2,
|
|
592
615
|
/** The line handles the filer acts on, and the only ones it reads (3e): new work, a change to existing
|
|
593
616
|
* work, feedback on Paigy, anything said to an agent, and any rule meant to outlast the call. */
|
|
594
617
|
file: strs2,
|
|
595
618
|
/** Evidence to fetch for a second round: a search, an item's full text, or an agent by name. */
|
|
596
|
-
need: z7.object({ kind: z7.enum(["search", "item", "agent", ""]), text:
|
|
619
|
+
need: z7.object({ kind: z7.enum(["search", "item", "agent", ""]), text: str3 }).strict(),
|
|
597
620
|
/** The handle of the item to explain ("" for none). */
|
|
598
|
-
explain:
|
|
621
|
+
explain: str3
|
|
599
622
|
}).strict();
|
|
600
623
|
var TALKER_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
601
624
|
zodToJsonSchema3(TalkerReplySchema, { $refStrategy: "none" })
|
|
602
625
|
);
|
|
603
|
-
var
|
|
626
|
+
var str4 = z8.string();
|
|
604
627
|
var strs3 = z8.array(z8.string());
|
|
605
628
|
var FilerReplySchema = z8.object({
|
|
606
629
|
/** Which Goals each new line goes on: a line's handle, Goal handles (or keys of new Goals), and what
|
|
607
630
|
* the line says in at most ten words. */
|
|
608
|
-
filed: z8.array(z8.object({ line:
|
|
631
|
+
filed: z8.array(z8.object({ line: str4, goals: strs3, summary: str4 }).strict()),
|
|
609
632
|
goals: z8.array(z8.object({
|
|
610
633
|
op: z8.enum(["create", "edit", "complete", "cancel", "reopen", "assign", "defer", "move", "block", "unblock"]),
|
|
611
|
-
key:
|
|
612
|
-
goal:
|
|
613
|
-
title:
|
|
614
|
-
outcome:
|
|
615
|
-
owner:
|
|
616
|
-
under:
|
|
617
|
-
until:
|
|
618
|
-
on:
|
|
634
|
+
key: str4,
|
|
635
|
+
goal: str4,
|
|
636
|
+
title: str4,
|
|
637
|
+
outcome: str4,
|
|
638
|
+
owner: str4,
|
|
639
|
+
under: str4,
|
|
640
|
+
until: str4,
|
|
641
|
+
on: str4,
|
|
619
642
|
gate: z8.enum(["start", "complete", "answer", ""]),
|
|
620
|
-
why:
|
|
643
|
+
why: str4
|
|
621
644
|
}).strict()),
|
|
622
645
|
questions: z8.array(z8.object({
|
|
623
646
|
op: z8.enum(["create", "edit", "assign", "withdraw"]),
|
|
624
|
-
key:
|
|
625
|
-
question:
|
|
626
|
-
text:
|
|
627
|
-
summary:
|
|
628
|
-
to:
|
|
629
|
-
goal:
|
|
630
|
-
options: z8.array(z8.object({ id:
|
|
647
|
+
key: str4,
|
|
648
|
+
question: str4,
|
|
649
|
+
text: str4,
|
|
650
|
+
summary: str4,
|
|
651
|
+
to: str4,
|
|
652
|
+
goal: str4,
|
|
653
|
+
options: z8.array(z8.object({ id: str4, label: str4 }).strict()),
|
|
631
654
|
pick: z8.enum(["one", "many", "rank", "words", ""]),
|
|
632
|
-
blocks:
|
|
655
|
+
blocks: str4
|
|
633
656
|
}).strict()),
|
|
634
|
-
lessons: z8.array(z8.object({ op: z8.enum(["remember", "revise", "withdraw"]), lesson:
|
|
657
|
+
lessons: z8.array(z8.object({ op: z8.enum(["remember", "revise", "withdraw"]), lesson: str4, text: str4, scope: str4 }).strict()),
|
|
635
658
|
/** Line handles whose words comment on Paigy itself. */
|
|
636
659
|
feedback: strs3
|
|
637
660
|
}).strict();
|
|
@@ -692,6 +715,27 @@ var TidyReplySchema = z10.object({
|
|
|
692
715
|
var TIDY_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
693
716
|
zodToJsonSchema6(TidyReplySchema, { $refStrategy: "none" })
|
|
694
717
|
);
|
|
718
|
+
var HandoutReplySchema = z11.object({
|
|
719
|
+
assign: z11.array(z11.object({
|
|
720
|
+
/** The Goal's handle (G). */
|
|
721
|
+
goal: z11.string(),
|
|
722
|
+
/** The agent's handle (A). */
|
|
723
|
+
to: z11.string(),
|
|
724
|
+
/** At most ten words, naming the Goal of theirs it is most like: the reason the hand-out records. */
|
|
725
|
+
why: z11.string()
|
|
726
|
+
}).strict()),
|
|
727
|
+
/** The Goals no listed agent's work is even loosely like: Paigy starts a session for them (owner, 2026-10-24,
|
|
728
|
+
* Goal 37603006, "One per family of related Goals"). */
|
|
729
|
+
new: z11.array(z11.object({
|
|
730
|
+
/** The Goal's handle (G). */
|
|
731
|
+
goal: z11.string(),
|
|
732
|
+
/** At most ten words: why nothing active fits it. */
|
|
733
|
+
why: z11.string()
|
|
734
|
+
}).strict())
|
|
735
|
+
}).strict();
|
|
736
|
+
var HANDOUT_REPLY_JSON_SCHEMA = (({ $schema: _, ...rest }) => rest)(
|
|
737
|
+
zodToJsonSchema7(HandoutReplySchema, { $refStrategy: "none" })
|
|
738
|
+
);
|
|
695
739
|
function entryWords(entry) {
|
|
696
740
|
const content = entry.content;
|
|
697
741
|
if (content && "sealed" in content) return "";
|
|
@@ -720,42 +764,6 @@ var LIVE_MS = 3 * 6e4;
|
|
|
720
764
|
var WORKING_MS = 60 * 6e4;
|
|
721
765
|
var WORKING_TIER_MS = 2 * 60 * 6e4;
|
|
722
766
|
var RECENT_TIER_MS = 24 * 60 * 6e4;
|
|
723
|
-
var json = z11.lazy(() => z11.union([
|
|
724
|
-
z11.string(),
|
|
725
|
-
z11.number().finite(),
|
|
726
|
-
z11.boolean(),
|
|
727
|
-
z11.null(),
|
|
728
|
-
z11.array(json),
|
|
729
|
-
z11.record(json)
|
|
730
|
-
]));
|
|
731
|
-
var clock = z11.number().int().nonnegative().safe();
|
|
732
|
-
var HeardEventSchema = z11.object({
|
|
733
|
-
eventId: z11.string().uuid(),
|
|
734
|
-
source: z11.enum(["phone", "bot"]),
|
|
735
|
-
sequence: clock,
|
|
736
|
-
kind: z11.string().min(1),
|
|
737
|
-
phoneAtMs: clock.optional(),
|
|
738
|
-
botAtMs: clock.optional(),
|
|
739
|
-
payload: z11.record(json)
|
|
740
|
-
}).strict();
|
|
741
|
-
var HeardCursorSchema = HeardEventSchema.pick({ source: true, sequence: true, eventId: true });
|
|
742
|
-
var HeardPageSchema = z11.object({
|
|
743
|
-
events: z11.array(HeardEventSchema).max(200),
|
|
744
|
-
next: HeardCursorSchema.nullable()
|
|
745
|
-
}).strict();
|
|
746
|
-
var MAX_EVENTS = 50;
|
|
747
|
-
var MAX_BYTES = 8e4;
|
|
748
|
-
var batchBytes = (events) => {
|
|
749
|
-
let bytes = 0;
|
|
750
|
-
for (const char of JSON.stringify({ events })) {
|
|
751
|
-
const point = char.codePointAt(0);
|
|
752
|
-
bytes += point < 128 ? 1 : point < 2048 ? 2 : point < 65536 ? 3 : 4;
|
|
753
|
-
}
|
|
754
|
-
return bytes;
|
|
755
|
-
};
|
|
756
|
-
var HeardBatchSchema = z11.object({
|
|
757
|
-
events: z11.array(HeardEventSchema).min(1).max(MAX_EVENTS)
|
|
758
|
-
}).strict().refine((value) => batchBytes(value.events) <= MAX_BYTES, "heard_batch_too_large");
|
|
759
767
|
var ContextSchema = z12.object({
|
|
760
768
|
title: z12.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
761
769
|
description: z12.array(z12.string().min(1)).describe(
|
|
@@ -1080,6 +1088,10 @@ var AgendaTurnSchema = z12.object({
|
|
|
1080
1088
|
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
1081
1089
|
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
1082
1090
|
info: z12.array(z12.string().min(1)).default([]),
|
|
1091
|
+
/** THE ID OF EACH SENTENCE, when the plan names them (turns.md): a talker message's sentences are its
|
|
1092
|
+
* `info`, one piece each, and `units[i]` is the id the bot's line receipt carries for `info[i]`
|
|
1093
|
+
* (`<cycle>:<key>:<n>`), so what was heard is known per sentence. Absent on every other turn. */
|
|
1094
|
+
units: z12.array(z12.string().min(1)).optional(),
|
|
1083
1095
|
question: z12.string().min(1).nullable(),
|
|
1084
1096
|
/** True on the one turn carrying the agent's own declared question. */
|
|
1085
1097
|
asks: z12.boolean().optional(),
|
|
@@ -1104,6 +1116,21 @@ var AgendaTurnSchema = z12.object({
|
|
|
1104
1116
|
* the phone (`asking`, so the agenda goes gold and its options show) without treating the line as a
|
|
1105
1117
|
* question turn — the talker owns re-asking and the waits, so `question` stays null. */
|
|
1106
1118
|
onTable: z12.boolean().optional(),
|
|
1119
|
+
/** THE CARRIED ITEM THIS LINE TELLS (Goal 2a1e50eb, owner 2026-10-14: "Build the whole design") — a
|
|
1120
|
+
* request the Call carries that asks NOTHING: a report, a progress note, another agent's waiting news.
|
|
1121
|
+
* Its request Entry id, so the phone can make that agenda row the current one while it is read
|
|
1122
|
+
* (`telling` on the `turn` topic, `docs/clients/app/walk/design.md` §11).
|
|
1123
|
+
*
|
|
1124
|
+
* A FIELD OF ITS OWN, NEVER `claimId`: the claim is the RETURN identity — answers route by it and the
|
|
1125
|
+
* record tags the line with it (`workId`) — and an answer must never route to an Entry that asks
|
|
1126
|
+
* nothing. Absent on a turn speaking for a question (`claimId` says that) and on one speaking for no
|
|
1127
|
+
* carried item at all (an opener, a sign-off, Paigy's own line). */
|
|
1128
|
+
telling: z12.string().optional(),
|
|
1129
|
+
/** PAIGY'S OWN LINE (owner, 2026-10-15; calls ed2cbdce, ad50ad25): a turn that speaks for no item -- an
|
|
1130
|
+
* opener, a sign-off, a reply about nothing an agent asked or told -- is recorded as Paigy's, not as the
|
|
1131
|
+
* agent that rang. Absent on every turn that speaks for an item (`agentId` names whose), and on plans
|
|
1132
|
+
* from before it: a bot that does not know it records the ringing agent, as before. */
|
|
1133
|
+
speaksFor: z12.literal("paigy").optional(),
|
|
1107
1134
|
select: SelectShapeSchema.optional(),
|
|
1108
1135
|
options: z12.array(OptionSchema.omit({ id: true })).optional(),
|
|
1109
1136
|
/* `pace` STOOD HERE (#826). A turn could carry seconds and the model chose them. The walk
|
|
@@ -1192,7 +1219,7 @@ var InboxItemSchema = z12.object({
|
|
|
1192
1219
|
questions: z12.array(CallQuestionSchema).optional(),
|
|
1193
1220
|
/** The same call carrying order, including request Entries with no DecisionNeed as reports.
|
|
1194
1221
|
* Additive: questions stays decision-only for every other surface and older bundles. */
|
|
1195
|
-
callItems: z12.array(CallQuestionSchema.extend({ state: z12.enum(["open", "answered", "report"]) })).optional(),
|
|
1222
|
+
callItems: z12.array(CallQuestionSchema.extend({ state: z12.enum(["open", "answered", "report"]), acknowledged: z12.boolean().optional() })).optional(),
|
|
1196
1223
|
visuals: z12.array(VisualSchema).optional(),
|
|
1197
1224
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
1198
1225
|
* agent's suggestion, or a default silly name). */
|
|
@@ -1257,6 +1284,21 @@ var InboxItemSchema = z12.object({
|
|
|
1257
1284
|
* urgency (see the main README's "premier use case" + docs/delivery/notify/states.md). Drives the
|
|
1258
1285
|
* inbox's blocking badge and the extra confirm step before dismissing it. */
|
|
1259
1286
|
blocking: z12.boolean().default(false),
|
|
1287
|
+
/** WHAT FREED THIS QUESTION (owner, 2026-10-16, Goal 2383ef14: "we don't really show the question
|
|
1288
|
+
* at the top until the question it could reply to with is resolved. And then we show it again
|
|
1289
|
+
* with the context of the other thing that was resolved"; asked how it should carry that, "The
|
|
1290
|
+
* answer above it"). A question they answered with a question of their own waits for theirs
|
|
1291
|
+
* (`private.ask_back`) and drops out of the front while it waits; when it comes back, the card
|
|
1292
|
+
* draws their question and the answer it got ABOVE the ask, as one card. Absent on a question
|
|
1293
|
+
* nothing ever held back. */
|
|
1294
|
+
freedBy: z12.object({
|
|
1295
|
+
questionId: z12.string(),
|
|
1296
|
+
asked: z12.string(),
|
|
1297
|
+
answer: z12.string(),
|
|
1298
|
+
answeredAt: z12.string(),
|
|
1299
|
+
/** The answer in ten words, when the talker wrote one beside it. */
|
|
1300
|
+
summary: z12.string().optional()
|
|
1301
|
+
}).optional(),
|
|
1260
1302
|
/** The user's locked-in answer; present only for replied items (GET /api/replied/:id). */
|
|
1261
1303
|
answer: UserAnswerSchema.optional(),
|
|
1262
1304
|
/** THE TARGET FACTS A CARD RENDERS (#1796 point 5, 2026-09-11): the Delivery it is a view of,
|
|
@@ -1316,8 +1358,8 @@ var MissedCallSchema = z12.enum([
|
|
|
1316
1358
|
"inbox",
|
|
1317
1359
|
"dismiss"
|
|
1318
1360
|
]);
|
|
1319
|
-
var
|
|
1320
|
-
var QUIET = ` Nothing rings from ${
|
|
1361
|
+
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
1362
|
+
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
1321
1363
|
var BrokerTuningSchema = z12.object({
|
|
1322
1364
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
1323
1365
|
ackVerbosity: z12.enum(["normal", "none"]).optional(),
|
|
@@ -1654,6 +1696,13 @@ var QueueItemSchema = z12.object({
|
|
|
1654
1696
|
* can see (`replies`, `questions[].answeredAt`) miss a spontaneous note entirely — a `request`
|
|
1655
1697
|
* Entry with no `about_id` is in neither. */
|
|
1656
1698
|
lastPersonAt: z12.string().nullable().optional(),
|
|
1699
|
+
/** EACH SIDE'S NEWEST WORD IN THE LAST DAY (owner, 2026-10-13, the Work tab's order): the person's
|
|
1700
|
+
* (`yours`) and the agents' (`theirs`), each null when that side wrote nothing on it in 24 hours.
|
|
1701
|
+
* `latest` holds one Entry, so whichever side wrote first is hidden there; Work orders a Goal by
|
|
1702
|
+
* these two and draws the side it was placed by under its title. Optional, so hand-built queues
|
|
1703
|
+
* and a door older than 20261013045542 need not spell them. */
|
|
1704
|
+
yours: GoalLatestSchema.nullable().optional(),
|
|
1705
|
+
theirs: GoalLatestSchema.nullable().optional(),
|
|
1657
1706
|
/** WHAT THIS ROW IS, IN TWELVE CHARACTERS (#2928) — the hash of every other field on it, stamped
|
|
1658
1707
|
* by the one projection that builds the row (`apps/api/src/goal/queue.ts`). It is how the
|
|
1659
1708
|
* incremental read knows a row has not moved: the phone echoes back the revs it holds
|
|
@@ -1925,10 +1974,20 @@ var CallRecapSchema = z12.object({
|
|
|
1925
1974
|
title: z12.string(),
|
|
1926
1975
|
owner: z12.string(),
|
|
1927
1976
|
state: z12.string(),
|
|
1977
|
+
/** Presented Entry content and its author, separately from the current Goal owner. */
|
|
1978
|
+
items: z12.array(z12.object({ entryId: z12.string(), author: z12.string(), text: z12.string(), summary: z12.string().nullable(), acknowledged: z12.boolean().optional() })).optional(),
|
|
1928
1979
|
/** Whether the call put it to the person. An item it carried and never said has no row on the
|
|
1929
1980
|
* review (owner, 2026-10-08: "why do we get the third one when it didn't happen in the call?"). */
|
|
1930
1981
|
presented: z12.boolean(),
|
|
1931
|
-
|
|
1982
|
+
/** Whether THIS call created it — the third way a call touches work, beside carrying a request
|
|
1983
|
+
* there and filing words there (owner, 2026-10-08, Goal `5b4b1a0a`: "Show the new work, keep my
|
|
1984
|
+
* words under it"). Created work is never carried, so it is never `presented`; without this the
|
|
1985
|
+
* row is dropped by the "only what happened on the call" filter. False on a database before
|
|
1986
|
+
* 20261013080133. */
|
|
1987
|
+
created: z12.boolean(),
|
|
1988
|
+
/** `request` is the Entry that asked it — what a line's `about` names when the line answered or put
|
|
1989
|
+
* it off. Optional: an older API sent none. */
|
|
1990
|
+
questions: z12.array(z12.object({ id: z12.string().uuid(), state: z12.string(), title: z12.string(), request: z12.string().optional() })),
|
|
1932
1991
|
/** `words` is always what they SAID, verbatim — the record, never replaced. `summary` is what
|
|
1933
1992
|
* the line says in ten words (owner, 2026-10-07): the filer's, or the talker's for the answer it
|
|
1934
1993
|
* gave, so the row scans like a chosen option and their own words stay under it. Absent when
|
|
@@ -1964,7 +2023,44 @@ var CallRecapSchema = z12.object({
|
|
|
1964
2023
|
})),
|
|
1965
2024
|
more: z12.object({ lines: z12.number(), entries: z12.number(), topics: z12.number() }),
|
|
1966
2025
|
/** Last stored position on this page when another exists; absent before paging shipped. */
|
|
1967
|
-
next: z12.number().int().nonnegative().nullable().optional()
|
|
2026
|
+
next: z12.number().int().nonnegative().nullable().optional(),
|
|
2027
|
+
/** THE CALL AS ONE LIST OF ROWS (owner, 2026-10-14: "we only really want a thing like transcript but
|
|
2028
|
+
* built from the summaries", then "the transcript tabs should show the same but with full words").
|
|
2029
|
+
* Both tabs draw these rows, in this order: Summary from `summary`, Transcript from `words`.
|
|
2030
|
+
* - `put`: an item put to the person, at the first agent line that voiced it. Paigy's sentences
|
|
2031
|
+
* that voiced it are folded into `spoken`, never rows of their own.
|
|
2032
|
+
* - `said`: the person's words, one row per reply or per filed line no reply covers.
|
|
2033
|
+
* - `did`: what Paigy did ("Sent to Mouth: …", "Recorded: …", "Filed on …").
|
|
2034
|
+
* `by` is the row's real author, never inferred from `kind`: a relay Paigy sent to an agent is
|
|
2035
|
+
* Paigy's, not the person's. Optional and additive (owner: "make sure whatever you do is backward
|
|
2036
|
+
* compatible"): an API before it sends none, and the screen falls back to `topics` and `said`. */
|
|
2037
|
+
timeline: z12.array(z12.object({
|
|
2038
|
+
id: z12.string(),
|
|
2039
|
+
position: z12.number(),
|
|
2040
|
+
atMs: z12.number(),
|
|
2041
|
+
kind: z12.enum(["put", "said", "did"]),
|
|
2042
|
+
by: z12.object({ participant: z12.string().nullable(), agentId: z12.string().nullable(), name: z12.string().nullable() }),
|
|
2043
|
+
goalId: z12.string().uuid().nullable(),
|
|
2044
|
+
requestId: z12.string().nullable(),
|
|
2045
|
+
summary: z12.string().nullable(),
|
|
2046
|
+
words: z12.string(),
|
|
2047
|
+
/** Paigy's sentences folded into this row: on a `put` row, what it said putting the item to the
|
|
2048
|
+
* person (before their next row); on a `said` row, what it said in reply (until their next row).
|
|
2049
|
+
* `atMs` is when each was said; optional, absent before 2026-10-14's fix. */
|
|
2050
|
+
spoken: z12.array(z12.object({ lineId: z12.string(), words: z12.string(), atMs: z12.number().optional() })),
|
|
2051
|
+
/** On a `said` row: the work these words were filed on, and whether they answered (recorded) a
|
|
2052
|
+
* question. They replace the "Filed on ..." and "Recorded: ..." rows that repeated the person's own
|
|
2053
|
+
* summary (Brain, 2026-10-14). Optional: absent from an API before it. */
|
|
2054
|
+
filedOn: z12.array(z12.object({ goalId: z12.string().uuid(), title: z12.string() })).optional(),
|
|
2055
|
+
recorded: z12.boolean().optional(),
|
|
2056
|
+
settled: z12.object({ requestId: z12.string(), answer: z12.string() }).nullable().optional(),
|
|
2057
|
+
acknowledged: z12.boolean().optional(),
|
|
2058
|
+
/** On a `put` row: still waiting for the person's answer (not settled, not answered, not put off).
|
|
2059
|
+
* The server's judgement; the screen draws "Still waiting for your answer" from it. Optional: absent
|
|
2060
|
+
* from an API before it, where the screen falls back to its own reading. */
|
|
2061
|
+
waiting: z12.boolean().optional(),
|
|
2062
|
+
lineIds: z12.array(z12.string())
|
|
2063
|
+
})).optional()
|
|
1968
2064
|
});
|
|
1969
2065
|
|
|
1970
2066
|
// src/listening.ts
|
|
@@ -1985,14 +2081,21 @@ function sessionStartHook() {
|
|
|
1985
2081
|
[ -f "\${HOME}/.paigy/token.json" ] || exit 0 # not paired on this machine: nothing to listen as
|
|
1986
2082
|
cat <<'PAIGY'
|
|
1987
2083
|
Paigy: nothing pushes answers into a session started in a terminal \u2014 an answer waits for your
|
|
1988
|
-
next contact({ wait: false }). Start your own listener now
|
|
1989
|
-
|
|
2084
|
+
next contact({ wait: false }). Start your own listener now with your harness's MONITOR tool, the kind
|
|
2085
|
+
that wakes you on every line it prints (in Claude Code: Monitor, not a background Bash command, which
|
|
2086
|
+
only tells you when it exits). When the monitor expires, start it again:
|
|
1990
2087
|
|
|
1991
|
-
npx -y -p @paigy/mcp@latest paigy-listen
|
|
2088
|
+
npx -y -p @paigy/mcp@latest paigy-listen --brief
|
|
1992
2089
|
|
|
1993
2090
|
Each line it prints is a Delivery waiting for you: read the Goal, and reply with contact when
|
|
1994
|
-
the answer needs a decision.
|
|
1995
|
-
|
|
2091
|
+
the answer needs a decision.
|
|
2092
|
+
|
|
2093
|
+
ONE LISTENER PER SESSION, AND ANOTHER SESSION'S IS NOT YOURS. Identities here are per session, so
|
|
2094
|
+
a paigy-listen you find in ps is almost certainly another session listening as itself, and
|
|
2095
|
+
starting yours takes nothing from it. Never decide this from ps: the listen tool reads the mark
|
|
2096
|
+
for YOUR OWN slot and answers in one call. Ask it, or just start the listener above; only a
|
|
2097
|
+
second one for the same slot is the mistake. Then call contact({ wait: false }) once: work handed
|
|
2098
|
+
to you before this session started waits there.
|
|
1996
2099
|
PAIGY
|
|
1997
2100
|
exit 0
|
|
1998
2101
|
`;
|
|
@@ -2028,7 +2131,7 @@ function decideListen(f) {
|
|
|
2028
2131
|
return {
|
|
2029
2132
|
status: "start",
|
|
2030
2133
|
command: `PAIGY_AGENT=${word(f.slot)} PAIGY_SESSION_ID=${word(f.session)} ${f.env.CODEX_THREAD_ID ? `CODEX_THREAD_ID=${word(f.env.CODEX_THREAD_ID)} ` : ""}${f.env.PAIGY_ON_WAKE ? `PAIGY_ON_WAKE=${word(f.env.PAIGY_ON_WAKE)} ` : ""}${word(f.execPath)} ${word(f.listenJs)} --brief`,
|
|
2031
|
-
how: "Run this command NOW
|
|
2134
|
+
how: "Run this command NOW with your harness's MONITOR tool, the kind that wakes you on every line it prints (in Claude Code: Monitor, not a background Bash command, which only tells you when it exits); start it again when the monitor expires. Each line it prints is a Delivery waiting for you: read the Goal it names with get_goals, and answer with contact. Start it once."
|
|
2032
2135
|
};
|
|
2033
2136
|
}
|
|
2034
2137
|
function listenMark(slot, home = homedir()) {
|
|
@@ -2044,6 +2147,12 @@ function writeListenMark(slot, pid, home = homedir()) {
|
|
|
2044
2147
|
function removeListenMark(slot, home = homedir()) {
|
|
2045
2148
|
rmSync(listenMark(slot, home), { force: true });
|
|
2046
2149
|
}
|
|
2150
|
+
function clear(path) {
|
|
2151
|
+
try {
|
|
2152
|
+
rmSync(path, { force: true });
|
|
2153
|
+
} catch {
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2047
2156
|
function listenerAlive(slot, home = homedir()) {
|
|
2048
2157
|
const path = listenMark(slot, home);
|
|
2049
2158
|
let pid;
|
|
@@ -2053,7 +2162,7 @@ function listenerAlive(slot, home = homedir()) {
|
|
|
2053
2162
|
return null;
|
|
2054
2163
|
}
|
|
2055
2164
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
2056
|
-
|
|
2165
|
+
clear(path);
|
|
2057
2166
|
return null;
|
|
2058
2167
|
}
|
|
2059
2168
|
try {
|
|
@@ -2061,7 +2170,7 @@ function listenerAlive(slot, home = homedir()) {
|
|
|
2061
2170
|
return pid;
|
|
2062
2171
|
} catch (e) {
|
|
2063
2172
|
if (e.code === "EPERM") return pid;
|
|
2064
|
-
|
|
2173
|
+
clear(path);
|
|
2065
2174
|
return null;
|
|
2066
2175
|
}
|
|
2067
2176
|
}
|
|
@@ -2074,6 +2183,100 @@ function wakeStatusPath(slot, home = homedir()) {
|
|
|
2074
2183
|
function existingListener(slot, env, home = homedir()) {
|
|
2075
2184
|
return listenerAlive(listenerKey(slot, env), home) ?? (env.CODEX_THREAD_ID ? listenerAlive(slot, home) : null);
|
|
2076
2185
|
}
|
|
2186
|
+
var UNLISTENED_EVERY_MS = 10 * 6e4;
|
|
2187
|
+
var WATCHING_CLIENTS = /* @__PURE__ */ new Set(["claude-code"]);
|
|
2188
|
+
var NON_WATCHING_CLIENTS = /* @__PURE__ */ new Set(["claude-ai", "cursor", "cursor-vscode", "windsurf", "cline", "roo-cline", "continue", "vscode", "visual studio code", "zed", "antigravity"]);
|
|
2189
|
+
function canWatch(f) {
|
|
2190
|
+
if (harnessDriven(f.env) || f.env.PAIGY_ON_WAKE || f.env.CODEX_THREAD_ID) return "yes";
|
|
2191
|
+
const c = f.client?.toLowerCase();
|
|
2192
|
+
if (c && WATCHING_CLIENTS.has(c)) return "yes";
|
|
2193
|
+
if (c && NON_WATCHING_CLIENTS.has(c)) return "no";
|
|
2194
|
+
return "unknown";
|
|
2195
|
+
}
|
|
2196
|
+
function unlistenedNote(f, _now) {
|
|
2197
|
+
if (harnessDriven(f.env) || f.pidAlive) return null;
|
|
2198
|
+
const decision = decideListen(f);
|
|
2199
|
+
if (decision.status !== "start") return null;
|
|
2200
|
+
const head = `NOTHING CAN REACH THIS SESSION WHILE IT RESTS. No listener runs for your slot (${f.slot}) and no harness pump delivers to it`;
|
|
2201
|
+
switch (canWatch(f)) {
|
|
2202
|
+
case "no":
|
|
2203
|
+
return { unlistened: `${head}, and this host cannot run a listener. Work reaches you only as you act \u2014 it rides your tool calls, as it did this one \u2014 so call contact({ wait: false }) before you stop.` };
|
|
2204
|
+
case "yes":
|
|
2205
|
+
return { unlistened: `${head}. Start the listener below as a watched background task; a listener you see in ps belongs to another session's slot, never to yours.`, listen_command: decision.command };
|
|
2206
|
+
default:
|
|
2207
|
+
return { unlistened: `${head}. If this host can run a watched background task, start the listener below; if it cannot, work reaches you only as you act, so call contact({ wait: false }) before you stop.`, listen_command: decision.command };
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
var ReachTeller = class {
|
|
2211
|
+
constructor(every = UNLISTENED_EVERY_MS) {
|
|
2212
|
+
this.every = every;
|
|
2213
|
+
}
|
|
2214
|
+
every;
|
|
2215
|
+
checked = /* @__PURE__ */ new Map();
|
|
2216
|
+
async note(slot, tool, d) {
|
|
2217
|
+
const now = d.now();
|
|
2218
|
+
const last = this.checked.get(slot);
|
|
2219
|
+
if (last !== void 0 && now - last < this.every) return null;
|
|
2220
|
+
this.checked.set(slot, now);
|
|
2221
|
+
let note;
|
|
2222
|
+
try {
|
|
2223
|
+
note = unlistenedNote(d.facts(), now);
|
|
2224
|
+
} catch {
|
|
2225
|
+
return null;
|
|
2226
|
+
}
|
|
2227
|
+
if (!note) return null;
|
|
2228
|
+
let waiting = null;
|
|
2229
|
+
if (tool !== "contact") {
|
|
2230
|
+
try {
|
|
2231
|
+
waiting = waitingSummary(await d.poll());
|
|
2232
|
+
} catch {
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
return { paigy_reach: { ...note, ...waiting ?? {} } };
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
function withReach(shaped, reach) {
|
|
2239
|
+
if (!reach || !shaped || typeof shaped !== "object" || Array.isArray(shaped)) return shaped;
|
|
2240
|
+
return { ...reach, ...shaped };
|
|
2241
|
+
}
|
|
2242
|
+
var HOOKED_HARNESSES = "Claude Code and Codex";
|
|
2243
|
+
function listenBlock(decision, f, stale) {
|
|
2244
|
+
const block = {
|
|
2245
|
+
listen: decision,
|
|
2246
|
+
listen_permanent: `\`${ENABLE_LISTEN_COMMAND}\` installs the SessionStart hook for ${HOOKED_HARNESSES}, so every future session there is told this at its start. Other hosts are not hooked; their sessions learn it as they act.`
|
|
2247
|
+
};
|
|
2248
|
+
if (decision.status === "start") {
|
|
2249
|
+
block.listen_prompt = canWatch(f) === "no" ? "This host cannot run a listener, so nothing reaches this session while it rests. Work arrives with your tool calls; call contact({ wait: false }) before you stop." : "Start your own listener NOW, as described in `listen` \u2014 a watched background task, left running for this session. Without it, every answer waits until you call contact({ wait: false }).";
|
|
2250
|
+
}
|
|
2251
|
+
if (stale) {
|
|
2252
|
+
block.hook_refresh = `The SessionStart hook on this machine is an older text. Ask the user to re-run \`${ENABLE_LISTEN_COMMAND}\` to refresh it \u2014 the old one tells a session to skip its listener when it sees another session's.`;
|
|
2253
|
+
}
|
|
2254
|
+
return block;
|
|
2255
|
+
}
|
|
2256
|
+
var ENABLE_LISTEN_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
2257
|
+
function hookStale(home = homedir()) {
|
|
2258
|
+
try {
|
|
2259
|
+
return readFileSync(join(home, ".paigy", "hooks", "session-start.sh"), "utf8") !== sessionStartHook();
|
|
2260
|
+
} catch {
|
|
2261
|
+
return false;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function waitingSummary(received) {
|
|
2265
|
+
const r = received ?? {};
|
|
2266
|
+
const events = Array.isArray(r.events) ? r.events : [];
|
|
2267
|
+
const ids = events.map((e) => String(e?.eventId ?? "")).filter(Boolean);
|
|
2268
|
+
const claimable = r.claimable && typeof r.claimable === "object" ? String(r.claimable.title ?? "") : "";
|
|
2269
|
+
if (!ids.length && !claimable) return null;
|
|
2270
|
+
const kinds = events.map((e) => String(e?.kind ?? "event")).join(", ");
|
|
2271
|
+
const parts = [
|
|
2272
|
+
ids.length ? `${ids.length} event${ids.length === 1 ? "" : "s"} addressed to you (${kinds})` : "",
|
|
2273
|
+
claimable ? `work waiting: ${claimable}` : ""
|
|
2274
|
+
].filter(Boolean);
|
|
2275
|
+
return {
|
|
2276
|
+
waiting: `${parts.join("; ")}. Nothing acknowledged them \u2014 handle them with contact, and acknowledge with contact({ ackEventIds }) so they stop repeating. Read a Goal with get_goals before acting on a summary of it.`,
|
|
2277
|
+
...ids.length ? { waiting_events: ids } : {}
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2077
2280
|
|
|
2078
2281
|
export {
|
|
2079
2282
|
mcpInputSchema,
|
|
@@ -2089,5 +2292,10 @@ export {
|
|
|
2089
2292
|
removeListenMark,
|
|
2090
2293
|
listenerKey,
|
|
2091
2294
|
wakeStatusPath,
|
|
2092
|
-
existingListener
|
|
2295
|
+
existingListener,
|
|
2296
|
+
ReachTeller,
|
|
2297
|
+
withReach,
|
|
2298
|
+
listenBlock,
|
|
2299
|
+
ENABLE_LISTEN_COMMAND,
|
|
2300
|
+
hookStale
|
|
2093
2301
|
};
|