@paigy/harness 0.3.15 → 0.3.16
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/dist/cli.js +226 -122
- package/dist/main.js +226 -122
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -4580,7 +4580,7 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron";
|
|
|
4580
4580
|
import { spawn as spawn2 } from "child_process";
|
|
4581
4581
|
import { readFileSync as readFileSync5 } from "fs";
|
|
4582
4582
|
import { fileURLToPath } from "url";
|
|
4583
|
-
import { dirname as dirname4, join as
|
|
4583
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
4584
4584
|
|
|
4585
4585
|
// ../../packages/sdk/dist/index.js
|
|
4586
4586
|
import { createRequire as __sdkCreateRequire } from "module";
|
|
@@ -9973,7 +9973,7 @@ var AskInputSchema = external_exports.object({
|
|
|
9973
9973
|
parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
9974
9974
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
9975
9975
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
9976
|
-
"
|
|
9976
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
|
|
9977
9977
|
),
|
|
9978
9978
|
options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
|
|
9979
9979
|
}).strict();
|
|
@@ -9984,7 +9984,7 @@ var StartContactSchema = external_exports.object({
|
|
|
9984
9984
|
}).strict();
|
|
9985
9985
|
var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
9986
9986
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
9987
|
-
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}.
|
|
9987
|
+
var CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: on a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so).";
|
|
9988
9988
|
var CreateGoalSchema = external_exports.object({
|
|
9989
9989
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
9990
9990
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -10031,9 +10031,9 @@ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({
|
|
|
10031
10031
|
var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
10032
10032
|
var GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
10033
10033
|
var GET_GOAL_DESCRIPTION = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
|
|
10034
|
-
var UPDATE_GOAL_DESCRIPTION =
|
|
10035
|
-
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
10036
|
-
var CHECK_REPLIES_DESCRIPTION = "
|
|
10034
|
+
var UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
10035
|
+
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
10036
|
+
var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
10037
10037
|
var CheckRepliesSchema = external_exports.object({}).strict();
|
|
10038
10038
|
var AGENT_TOOLS = [
|
|
10039
10039
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
@@ -10363,9 +10363,9 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10363
10363
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
10364
10364
|
* the planner declares duplicates instead of asking them twice. */
|
|
10365
10365
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
10366
|
-
/**
|
|
10367
|
-
*
|
|
10368
|
-
info: external_exports.array(external_exports.string().min(1)).
|
|
10366
|
+
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
10367
|
+
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
10368
|
+
info: external_exports.array(external_exports.string().min(1)).default([]),
|
|
10369
10369
|
question: external_exports.string().min(1).nullable(),
|
|
10370
10370
|
/** True on the one turn carrying the agent's own declared question. */
|
|
10371
10371
|
asks: external_exports.boolean().optional(),
|
|
@@ -10400,32 +10400,6 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10400
10400
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
10401
10401
|
var InboxItemSchema = external_exports.object({
|
|
10402
10402
|
id: external_exports.string(),
|
|
10403
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
10404
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
10405
|
-
parentId: external_exports.string().optional(),
|
|
10406
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
10407
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
10408
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
10409
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
10410
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
10411
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
10412
|
-
* thread, which is what the client did for all rows until now. */
|
|
10413
|
-
askId: external_exports.string().optional(),
|
|
10414
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
10415
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
10416
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
10417
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
10418
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
10419
|
-
seq: external_exports.number().int().optional(),
|
|
10420
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
10421
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
10422
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
10423
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
10424
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
10425
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
10426
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
10427
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
10428
|
-
units: external_exports.number().int().positive().optional(),
|
|
10429
10403
|
tokenId: external_exports.string().optional(),
|
|
10430
10404
|
status: NotifyStatusSchema,
|
|
10431
10405
|
context: ContextSchema,
|
|
@@ -10470,7 +10444,12 @@ var InboxItemSchema = external_exports.object({
|
|
|
10470
10444
|
entryId: external_exports.string(),
|
|
10471
10445
|
title: external_exports.string(),
|
|
10472
10446
|
state: external_exports.enum(["open", "answered"]),
|
|
10473
|
-
answer: external_exports.string().nullable()
|
|
10447
|
+
answer: external_exports.string().nullable(),
|
|
10448
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
10449
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
10450
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
10451
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
10452
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional()
|
|
10474
10453
|
})).optional(),
|
|
10475
10454
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
10476
10455
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -10488,10 +10467,20 @@ var InboxItemSchema = external_exports.object({
|
|
|
10488
10467
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
10489
10468
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
10490
10469
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
10491
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
10492
|
-
*
|
|
10470
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
10471
|
+
* error badge + Retry. */
|
|
10493
10472
|
error: external_exports.string().optional(),
|
|
10473
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
10474
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
10475
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
10476
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
10477
|
+
cold: external_exports.string().datetime().optional(),
|
|
10494
10478
|
clarifies: external_exports.string().optional(),
|
|
10479
|
+
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
10480
|
+
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
10481
|
+
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
10482
|
+
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
10483
|
+
onCall: external_exports.literal(true).optional(),
|
|
10495
10484
|
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
10496
10485
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
10497
10486
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -10540,7 +10529,10 @@ var InboxItemSchema = external_exports.object({
|
|
|
10540
10529
|
goalIds: external_exports.array(external_exports.string()),
|
|
10541
10530
|
decisionNeedId: external_exports.string().optional(),
|
|
10542
10531
|
sealed: external_exports.boolean(),
|
|
10543
|
-
goalState: external_exports.string().optional()
|
|
10532
|
+
goalState: external_exports.string().optional(),
|
|
10533
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
10534
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
10535
|
+
goalTitle: external_exports.string().optional()
|
|
10544
10536
|
}).optional()
|
|
10545
10537
|
});
|
|
10546
10538
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
@@ -10639,7 +10631,6 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10639
10631
|
});
|
|
10640
10632
|
var HistoryItemSchema = external_exports.object({
|
|
10641
10633
|
id: external_exports.string(),
|
|
10642
|
-
parentId: external_exports.string(),
|
|
10643
10634
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
10644
10635
|
initiator: external_exports.enum(["user", "agent"]),
|
|
10645
10636
|
title: external_exports.string(),
|
|
@@ -10798,6 +10789,24 @@ var QueueQuestionSchema = external_exports.object({
|
|
|
10798
10789
|
* gate at all. */
|
|
10799
10790
|
blocked: external_exports.boolean().default(false)
|
|
10800
10791
|
});
|
|
10792
|
+
var QueueReplySchema = external_exports.object({
|
|
10793
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
10794
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
10795
|
+
* walk can name it in its zoom. */
|
|
10796
|
+
id: external_exports.string(),
|
|
10797
|
+
/** The Goal the note is on. */
|
|
10798
|
+
goalId: external_exports.string(),
|
|
10799
|
+
/** What the note said. */
|
|
10800
|
+
note: external_exports.string(),
|
|
10801
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
10802
|
+
deliveryId: external_exports.string(),
|
|
10803
|
+
requestEntryId: external_exports.string(),
|
|
10804
|
+
askedAt: external_exports.string(),
|
|
10805
|
+
/** When the person last replied — the window's start. */
|
|
10806
|
+
repliedAt: external_exports.string(),
|
|
10807
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
10808
|
+
reply: external_exports.string().nullable()
|
|
10809
|
+
});
|
|
10801
10810
|
var QueueItemSchema = external_exports.object({
|
|
10802
10811
|
id: external_exports.string(),
|
|
10803
10812
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -10825,6 +10834,10 @@ var QueueItemSchema = external_exports.object({
|
|
|
10825
10834
|
blocked: external_exports.boolean().default(false),
|
|
10826
10835
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
10827
10836
|
questions: external_exports.array(QueueQuestionSchema).default([]),
|
|
10837
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
10838
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
10839
|
+
* not spell an empty list. */
|
|
10840
|
+
replies: external_exports.array(QueueReplySchema).optional(),
|
|
10828
10841
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
10829
10842
|
repo: external_exports.string().nullable().optional(),
|
|
10830
10843
|
createdAt: external_exports.string(),
|
|
@@ -11285,6 +11298,7 @@ function currentRepo(cwd = process.cwd()) {
|
|
|
11285
11298
|
}
|
|
11286
11299
|
return asked;
|
|
11287
11300
|
}
|
|
11301
|
+
var NOT_SENT = "Recorded as progress, not sent: nobody was notified. If the person should see it (a result, something to try or check, a finding), contact again and say so; report progress with update_goal.";
|
|
11288
11302
|
async function contact(input, opts = {}) {
|
|
11289
11303
|
const parsed = ContactSchema.parse(input);
|
|
11290
11304
|
opts.signal?.throwIfAborted();
|
|
@@ -11292,6 +11306,7 @@ async function contact(input, opts = {}) {
|
|
|
11292
11306
|
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
|
|
11293
11307
|
let deliveryId;
|
|
11294
11308
|
let deliveries = [];
|
|
11309
|
+
let recorded = [];
|
|
11295
11310
|
if ("deliveryId" in parsed) {
|
|
11296
11311
|
deliveryId = parsed.deliveryId;
|
|
11297
11312
|
} else {
|
|
@@ -11320,16 +11335,24 @@ async function contact(input, opts = {}) {
|
|
|
11320
11335
|
if (!res.ok) await fail("batch contact", res);
|
|
11321
11336
|
const receipt = await res.json();
|
|
11322
11337
|
deliveries = receipt.deliveries ?? [];
|
|
11338
|
+
recorded = receipt.recorded ?? [];
|
|
11339
|
+
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
11323
11340
|
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
11324
11341
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
11325
11342
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
11326
11343
|
}
|
|
11327
|
-
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId);
|
|
11344
|
+
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
|
|
11345
|
+
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
11328
11346
|
const read = async (signal2) => {
|
|
11329
11347
|
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
11330
11348
|
if (!res.ok) await fail("read_delivery", res);
|
|
11331
11349
|
const one = await res.json();
|
|
11332
|
-
return
|
|
11350
|
+
return {
|
|
11351
|
+
...one,
|
|
11352
|
+
...all.length > 1 ? { deliveries: all } : {},
|
|
11353
|
+
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
11354
|
+
...joinedCard ? { joinedCard: true } : {}
|
|
11355
|
+
};
|
|
11333
11356
|
};
|
|
11334
11357
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
11335
11358
|
if (opts.waits === false) return read(opts.signal);
|
|
@@ -11359,47 +11382,108 @@ async function checkReplies(opts = {}) {
|
|
|
11359
11382
|
var HEARTBEAT_MS = 25e3;
|
|
11360
11383
|
var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
11361
11384
|
var JOIN = { config: { broadcast: { self: false } } };
|
|
11385
|
+
var JOIN_TIMEOUT_MS = 1e4;
|
|
11362
11386
|
var lines = /* @__PURE__ */ new Map();
|
|
11363
11387
|
function frame(line, topic, event, payload) {
|
|
11388
|
+
const ref = String(++line.ref);
|
|
11364
11389
|
try {
|
|
11365
|
-
line.ws?.send(JSON.stringify({ topic, event, payload, ref
|
|
11390
|
+
line.ws?.send(JSON.stringify({ topic, event, payload, ref }));
|
|
11366
11391
|
} catch {
|
|
11367
11392
|
}
|
|
11393
|
+
return ref;
|
|
11368
11394
|
}
|
|
11369
11395
|
function stop(line) {
|
|
11370
11396
|
line.open = false;
|
|
11397
|
+
line.beat = void 0;
|
|
11371
11398
|
clearInterval(line.heart);
|
|
11372
11399
|
clearTimeout(line.retry);
|
|
11400
|
+
for (const t of line.topics.values()) {
|
|
11401
|
+
clearTimeout(t.timer);
|
|
11402
|
+
t.joined = false;
|
|
11403
|
+
t.ref = void 0;
|
|
11404
|
+
}
|
|
11405
|
+
}
|
|
11406
|
+
function join3(line, name, t) {
|
|
11407
|
+
clearTimeout(t.timer);
|
|
11408
|
+
t.joined = false;
|
|
11409
|
+
t.ref = frame(line, `realtime:${name}`, "phx_join", JOIN);
|
|
11410
|
+
t.timer = setTimeout(() => rejoin(line, name, t), JOIN_TIMEOUT_MS);
|
|
11411
|
+
}
|
|
11412
|
+
function rejoin(line, name, t) {
|
|
11413
|
+
clearTimeout(t.timer);
|
|
11414
|
+
t.joined = false;
|
|
11415
|
+
t.ref = void 0;
|
|
11416
|
+
if (!line.open || line.topics.get(name) !== t) return;
|
|
11417
|
+
t.timer = setTimeout(() => {
|
|
11418
|
+
if (line.open && line.topics.get(name) === t) join3(line, name, t);
|
|
11419
|
+
}, RETRY_MS[Math.min(t.attempt++, RETRY_MS.length - 1)]);
|
|
11420
|
+
}
|
|
11421
|
+
function joined(t) {
|
|
11422
|
+
clearTimeout(t.timer);
|
|
11423
|
+
t.joined = true;
|
|
11424
|
+
t.attempt = 0;
|
|
11425
|
+
for (const l of t.listeners) l.onJoined?.(++l.joins);
|
|
11373
11426
|
}
|
|
11374
11427
|
function connect(line) {
|
|
11375
11428
|
const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
11376
11429
|
const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
|
|
11430
|
+
let gone = false;
|
|
11431
|
+
const again = () => {
|
|
11432
|
+
if (gone) return;
|
|
11433
|
+
gone = true;
|
|
11434
|
+
stop(line);
|
|
11435
|
+
if (!line.topics.size) return;
|
|
11436
|
+
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
11437
|
+
};
|
|
11377
11438
|
ws.addEventListener("open", () => {
|
|
11439
|
+
if (gone) return;
|
|
11378
11440
|
line.open = true;
|
|
11379
11441
|
line.attempt = 0;
|
|
11380
|
-
for (const
|
|
11381
|
-
line.heart = setInterval(() =>
|
|
11442
|
+
for (const [name, t] of line.topics) join3(line, name, t);
|
|
11443
|
+
line.heart = setInterval(() => {
|
|
11444
|
+
if (line.beat) {
|
|
11445
|
+
try {
|
|
11446
|
+
ws.close();
|
|
11447
|
+
} catch {
|
|
11448
|
+
}
|
|
11449
|
+
again();
|
|
11450
|
+
return;
|
|
11451
|
+
}
|
|
11452
|
+
line.beat = frame(line, "phoenix", "heartbeat", {});
|
|
11453
|
+
}, HEARTBEAT_MS);
|
|
11382
11454
|
});
|
|
11383
11455
|
ws.addEventListener("message", (e) => {
|
|
11456
|
+
if (gone) return;
|
|
11384
11457
|
let m;
|
|
11385
11458
|
try {
|
|
11386
11459
|
m = JSON.parse(String(e.data));
|
|
11387
11460
|
} catch {
|
|
11388
11461
|
return;
|
|
11389
11462
|
}
|
|
11390
|
-
if (m.
|
|
11391
|
-
|
|
11463
|
+
if (m.topic === "phoenix") {
|
|
11464
|
+
if (m.event === "phx_reply" && m.ref === line.beat) line.beat = void 0;
|
|
11465
|
+
return;
|
|
11466
|
+
}
|
|
11467
|
+
if (!m.topic?.startsWith("realtime:")) return;
|
|
11468
|
+
const name = m.topic.slice("realtime:".length);
|
|
11469
|
+
const t = line.topics.get(name);
|
|
11470
|
+
if (!t) return;
|
|
11471
|
+
if (m.event === "phx_reply") {
|
|
11472
|
+
if (!t.ref || m.ref !== t.ref) return;
|
|
11473
|
+
if (m.payload?.status === "ok") joined(t);
|
|
11474
|
+
else rejoin(line, name, t);
|
|
11475
|
+
return;
|
|
11476
|
+
}
|
|
11477
|
+
if (m.event === "phx_error" || m.event === "phx_close") {
|
|
11478
|
+
if (m.ref && m.ref !== t.ref) return;
|
|
11479
|
+
rejoin(line, name, t);
|
|
11480
|
+
return;
|
|
11481
|
+
}
|
|
11482
|
+
if (m.event !== "broadcast") return;
|
|
11483
|
+
for (const l of t.listeners) {
|
|
11392
11484
|
if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
|
|
11393
11485
|
}
|
|
11394
11486
|
});
|
|
11395
|
-
let gone = false;
|
|
11396
|
-
const again = () => {
|
|
11397
|
-
if (gone) return;
|
|
11398
|
-
gone = true;
|
|
11399
|
-
stop(line);
|
|
11400
|
-
if (!line.topics.size) return;
|
|
11401
|
-
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
11402
|
-
};
|
|
11403
11487
|
ws.addEventListener("close", again);
|
|
11404
11488
|
ws.addEventListener("error", again);
|
|
11405
11489
|
}
|
|
@@ -11407,17 +11491,22 @@ function subscribeRealtime(args) {
|
|
|
11407
11491
|
const key = `${args.url} ${args.anonKey}`;
|
|
11408
11492
|
let line = lines.get(key);
|
|
11409
11493
|
if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
|
|
11410
|
-
let
|
|
11411
|
-
if (!
|
|
11412
|
-
line.topics.set(args.topic,
|
|
11413
|
-
if (line.open)
|
|
11414
|
-
}
|
|
11415
|
-
const
|
|
11416
|
-
|
|
11494
|
+
let t = line.topics.get(args.topic);
|
|
11495
|
+
if (!t) {
|
|
11496
|
+
line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
|
|
11497
|
+
if (line.open) join3(line, args.topic, t);
|
|
11498
|
+
}
|
|
11499
|
+
const topic = t;
|
|
11500
|
+
const me = { event: args.event, onMessage: args.onMessage, onJoined: args.onJoined, joins: 0 };
|
|
11501
|
+
topic.listeners.add(me);
|
|
11502
|
+
if (topic.joined && me.onJoined) queueMicrotask(() => {
|
|
11503
|
+
if (topic.joined && topic.listeners.has(me)) me.onJoined?.(++me.joins);
|
|
11504
|
+
});
|
|
11417
11505
|
if (!line.ws) connect(line);
|
|
11418
11506
|
return {
|
|
11419
11507
|
close: () => {
|
|
11420
|
-
if (!
|
|
11508
|
+
if (!topic.listeners.delete(me) || topic.listeners.size) return;
|
|
11509
|
+
clearTimeout(topic.timer);
|
|
11421
11510
|
line.topics.delete(args.topic);
|
|
11422
11511
|
if (line.topics.size) {
|
|
11423
11512
|
if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
|
|
@@ -11433,7 +11522,7 @@ function subscribeRealtime(args) {
|
|
|
11433
11522
|
};
|
|
11434
11523
|
}
|
|
11435
11524
|
async function subscribeWake(onNudge, opts = {}) {
|
|
11436
|
-
const { release = true, ...client } = opts;
|
|
11525
|
+
const { release = true, onJoined, ...client } = opts;
|
|
11437
11526
|
const cfg = await registerDelivery("self_hosted", client);
|
|
11438
11527
|
const { url, anonKey } = cfg.realtime;
|
|
11439
11528
|
const channel = wakeChannel(cfg.tokenId);
|
|
@@ -11442,7 +11531,8 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
11442
11531
|
anonKey,
|
|
11443
11532
|
topic: channel,
|
|
11444
11533
|
event: WAKE_EVENT,
|
|
11445
|
-
onMessage: (payload) => onNudge(payload ?? {})
|
|
11534
|
+
onMessage: (payload) => onNudge(payload ?? {}),
|
|
11535
|
+
onJoined
|
|
11446
11536
|
});
|
|
11447
11537
|
return {
|
|
11448
11538
|
tokenId: cfg.tokenId,
|
|
@@ -11463,7 +11553,7 @@ import { homedir as homedir9, hostname } from "os";
|
|
|
11463
11553
|
import { spawnSync } from "child_process";
|
|
11464
11554
|
import { existsSync as existsSync2 } from "fs";
|
|
11465
11555
|
import { homedir as homedir3 } from "os";
|
|
11466
|
-
import { delimiter, dirname, join as
|
|
11556
|
+
import { delimiter, dirname, join as join4 } from "path";
|
|
11467
11557
|
var CATALOG = [
|
|
11468
11558
|
{
|
|
11469
11559
|
name: "claude",
|
|
@@ -11499,10 +11589,10 @@ var CATALOG = [
|
|
|
11499
11589
|
function probeDirs(home = homedir3()) {
|
|
11500
11590
|
return [
|
|
11501
11591
|
dirname(process.execPath),
|
|
11502
|
-
|
|
11592
|
+
join4(home, ".local", "bin"),
|
|
11503
11593
|
"/usr/local/bin",
|
|
11504
11594
|
"/opt/homebrew/bin",
|
|
11505
|
-
|
|
11595
|
+
join4(home, ".npm-global", "bin")
|
|
11506
11596
|
];
|
|
11507
11597
|
}
|
|
11508
11598
|
function which(bin) {
|
|
@@ -11510,7 +11600,7 @@ function which(bin) {
|
|
|
11510
11600
|
const fromPath = hit.status === 0 ? hit.stdout.trim() : "";
|
|
11511
11601
|
if (fromPath) return fromPath;
|
|
11512
11602
|
for (const dir of probeDirs()) {
|
|
11513
|
-
const candidate =
|
|
11603
|
+
const candidate = join4(dir, bin);
|
|
11514
11604
|
if (existsSync2(candidate)) return candidate;
|
|
11515
11605
|
}
|
|
11516
11606
|
return null;
|
|
@@ -11552,14 +11642,14 @@ function detectAll(deps = {}) {
|
|
|
11552
11642
|
// src/host.ts
|
|
11553
11643
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
11554
11644
|
import { homedir as homedir7 } from "os";
|
|
11555
|
-
import { join as
|
|
11645
|
+
import { join as join8 } from "path";
|
|
11556
11646
|
|
|
11557
11647
|
// src/workspaces.ts
|
|
11558
11648
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
11559
|
-
import { dirname as dirname2, join as
|
|
11649
|
+
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
11560
11650
|
import { homedir as homedir4 } from "os";
|
|
11561
11651
|
function workspacesFile() {
|
|
11562
|
-
return
|
|
11652
|
+
return join5(homedir4(), ".paigy", "workspaces.json");
|
|
11563
11653
|
}
|
|
11564
11654
|
function listWorkspaces(deps) {
|
|
11565
11655
|
try {
|
|
@@ -11600,7 +11690,7 @@ function resolveWakeDir(pinned, deps) {
|
|
|
11600
11690
|
// src/owner.ts
|
|
11601
11691
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
|
|
11602
11692
|
import { homedir as homedir5 } from "os";
|
|
11603
|
-
import { join as
|
|
11693
|
+
import { join as join6 } from "path";
|
|
11604
11694
|
var HEAD_BYTES = 64e3;
|
|
11605
11695
|
function readHead(file) {
|
|
11606
11696
|
const fd = openSync2(file, "r");
|
|
@@ -11616,7 +11706,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11616
11706
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
11617
11707
|
if (!m) return null;
|
|
11618
11708
|
const prefix = m[1];
|
|
11619
|
-
const root = deps.projectsDir ??
|
|
11709
|
+
const root = deps.projectsDir ?? join6(homedir5(), ".claude", "projects");
|
|
11620
11710
|
const list = deps.list ?? ((dir) => readdirSync(dir));
|
|
11621
11711
|
const head = deps.head ?? readHead;
|
|
11622
11712
|
let folders;
|
|
@@ -11628,7 +11718,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11628
11718
|
for (const folder of folders) {
|
|
11629
11719
|
let files;
|
|
11630
11720
|
try {
|
|
11631
|
-
files = list(
|
|
11721
|
+
files = list(join6(root, folder));
|
|
11632
11722
|
} catch {
|
|
11633
11723
|
continue;
|
|
11634
11724
|
}
|
|
@@ -11636,7 +11726,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11636
11726
|
if (!file) continue;
|
|
11637
11727
|
let text;
|
|
11638
11728
|
try {
|
|
11639
|
-
text = head(
|
|
11729
|
+
text = head(join6(root, folder, file));
|
|
11640
11730
|
} catch {
|
|
11641
11731
|
continue;
|
|
11642
11732
|
}
|
|
@@ -12991,7 +13081,7 @@ var AskInputSchema2 = external_exports.object({
|
|
|
12991
13081
|
parentId: external_exports.string().uuid().optional().describe("The Goal this question is about \u2014 usually the one you are working on. The question goes onto that Goal and its answer comes back there. Omit it and Paigy places the question in the tree itself."),
|
|
12992
13082
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
12993
13083
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
12994
|
-
"
|
|
13084
|
+
"ONE question, and only what is needed to answer it. Several questions are several asks in the array, one each: an answer settles the one ask it was given in the shape that ask declares, so a person who answers the part of a bundled ask that interested them settles nothing and is asked again. Paigy reads every ask once and takes a bundled one apart into its own cards anyway \u2014 parts of one piece of work under one Goal, separate things as separate Goals \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; a call contact JOINS a call already happening, so several arrive as one call."
|
|
12995
13085
|
),
|
|
12996
13086
|
options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
|
|
12997
13087
|
}).strict();
|
|
@@ -13002,7 +13092,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
13002
13092
|
}).strict();
|
|
13003
13093
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
13004
13094
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
13005
|
-
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}.
|
|
13095
|
+
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. One question per 'ask'; several questions are several objects in 'asks'. A bundled ask is taken apart into its own cards by Paigy's one intake read \u2014 parts of one piece of work under one Goal, separate things as separate Goals. An ask with no options that is not blocking is a report: on a Goal whose report card is still open it is added to that card, with no new push; one that only says where your work stands while under way is recorded as the Goal's progress, not sent (the result says so).";
|
|
13006
13096
|
var CreateGoalSchema2 = external_exports.object({
|
|
13007
13097
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
13008
13098
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -13049,9 +13139,9 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
|
|
|
13049
13139
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
13050
13140
|
var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
13051
13141
|
var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
|
|
13052
|
-
var UPDATE_GOAL_DESCRIPTION2 =
|
|
13053
|
-
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
13054
|
-
var CHECK_REPLIES_DESCRIPTION2 = "
|
|
13142
|
+
var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
13143
|
+
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
13144
|
+
var CHECK_REPLIES_DESCRIPTION2 = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
|
|
13055
13145
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
13056
13146
|
var AGENT_TOOLS2 = [
|
|
13057
13147
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
@@ -13396,9 +13486,9 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13396
13486
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
13397
13487
|
* the planner declares duplicates instead of asking them twice. */
|
|
13398
13488
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
13399
|
-
/**
|
|
13400
|
-
*
|
|
13401
|
-
info: external_exports.array(external_exports.string().min(1)).
|
|
13489
|
+
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
13490
|
+
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
13491
|
+
info: external_exports.array(external_exports.string().min(1)).default([]),
|
|
13402
13492
|
question: external_exports.string().min(1).nullable(),
|
|
13403
13493
|
/** True on the one turn carrying the agent's own declared question. */
|
|
13404
13494
|
asks: external_exports.boolean().optional(),
|
|
@@ -13433,32 +13523,6 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13433
13523
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
13434
13524
|
var InboxItemSchema2 = external_exports.object({
|
|
13435
13525
|
id: external_exports.string(),
|
|
13436
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
13437
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
13438
|
-
parentId: external_exports.string().optional(),
|
|
13439
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
13440
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
13441
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
13442
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
13443
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
13444
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
13445
|
-
* thread, which is what the client did for all rows until now. */
|
|
13446
|
-
askId: external_exports.string().optional(),
|
|
13447
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
13448
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
13449
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
13450
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
13451
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
13452
|
-
seq: external_exports.number().int().optional(),
|
|
13453
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
13454
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
13455
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
13456
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
13457
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
13458
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
13459
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
13460
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
13461
|
-
units: external_exports.number().int().positive().optional(),
|
|
13462
13526
|
tokenId: external_exports.string().optional(),
|
|
13463
13527
|
status: NotifyStatusSchema2,
|
|
13464
13528
|
context: ContextSchema2,
|
|
@@ -13503,7 +13567,12 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13503
13567
|
entryId: external_exports.string(),
|
|
13504
13568
|
title: external_exports.string(),
|
|
13505
13569
|
state: external_exports.enum(["open", "answered"]),
|
|
13506
|
-
answer: external_exports.string().nullable()
|
|
13570
|
+
answer: external_exports.string().nullable(),
|
|
13571
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
13572
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
13573
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
13574
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
13575
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional()
|
|
13507
13576
|
})).optional(),
|
|
13508
13577
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
13509
13578
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
@@ -13521,10 +13590,20 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13521
13590
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
13522
13591
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
13523
13592
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
13524
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
13525
|
-
*
|
|
13593
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
13594
|
+
* error badge + Retry. */
|
|
13526
13595
|
error: external_exports.string().optional(),
|
|
13596
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
13597
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
13598
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
13599
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
13600
|
+
cold: external_exports.string().datetime().optional(),
|
|
13527
13601
|
clarifies: external_exports.string().optional(),
|
|
13602
|
+
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
13603
|
+
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
13604
|
+
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
13605
|
+
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
13606
|
+
onCall: external_exports.literal(true).optional(),
|
|
13528
13607
|
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
13529
13608
|
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
13530
13609
|
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
@@ -13573,7 +13652,10 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13573
13652
|
goalIds: external_exports.array(external_exports.string()),
|
|
13574
13653
|
decisionNeedId: external_exports.string().optional(),
|
|
13575
13654
|
sealed: external_exports.boolean(),
|
|
13576
|
-
goalState: external_exports.string().optional()
|
|
13655
|
+
goalState: external_exports.string().optional(),
|
|
13656
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
13657
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
13658
|
+
goalTitle: external_exports.string().optional()
|
|
13577
13659
|
}).optional()
|
|
13578
13660
|
});
|
|
13579
13661
|
var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
|
|
@@ -13672,7 +13754,6 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
13672
13754
|
});
|
|
13673
13755
|
var HistoryItemSchema2 = external_exports.object({
|
|
13674
13756
|
id: external_exports.string(),
|
|
13675
|
-
parentId: external_exports.string(),
|
|
13676
13757
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
13677
13758
|
initiator: external_exports.enum(["user", "agent"]),
|
|
13678
13759
|
title: external_exports.string(),
|
|
@@ -13831,6 +13912,24 @@ var QueueQuestionSchema2 = external_exports.object({
|
|
|
13831
13912
|
* gate at all. */
|
|
13832
13913
|
blocked: external_exports.boolean().default(false)
|
|
13833
13914
|
});
|
|
13915
|
+
var QueueReplySchema2 = external_exports.object({
|
|
13916
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
13917
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
13918
|
+
* walk can name it in its zoom. */
|
|
13919
|
+
id: external_exports.string(),
|
|
13920
|
+
/** The Goal the note is on. */
|
|
13921
|
+
goalId: external_exports.string(),
|
|
13922
|
+
/** What the note said. */
|
|
13923
|
+
note: external_exports.string(),
|
|
13924
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
13925
|
+
deliveryId: external_exports.string(),
|
|
13926
|
+
requestEntryId: external_exports.string(),
|
|
13927
|
+
askedAt: external_exports.string(),
|
|
13928
|
+
/** When the person last replied — the window's start. */
|
|
13929
|
+
repliedAt: external_exports.string(),
|
|
13930
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
13931
|
+
reply: external_exports.string().nullable()
|
|
13932
|
+
});
|
|
13834
13933
|
var QueueItemSchema2 = external_exports.object({
|
|
13835
13934
|
id: external_exports.string(),
|
|
13836
13935
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -13858,6 +13957,10 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13858
13957
|
blocked: external_exports.boolean().default(false),
|
|
13859
13958
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
13860
13959
|
questions: external_exports.array(QueueQuestionSchema2).default([]),
|
|
13960
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
13961
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
13962
|
+
* not spell an empty list. */
|
|
13963
|
+
replies: external_exports.array(QueueReplySchema2).optional(),
|
|
13861
13964
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
13862
13965
|
repo: external_exports.string().nullable().optional(),
|
|
13863
13966
|
createdAt: external_exports.string(),
|
|
@@ -14529,6 +14632,7 @@ async function ask(state, rail, input) {
|
|
|
14529
14632
|
waiting: "hard",
|
|
14530
14633
|
channel: input.call ? "call" : "notification"
|
|
14531
14634
|
});
|
|
14635
|
+
if ("recorded" in sent) return null;
|
|
14532
14636
|
const entryId = sent.requestEntryIds[0];
|
|
14533
14637
|
if (!entryId) return null;
|
|
14534
14638
|
(state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
|
|
@@ -14705,7 +14809,7 @@ function runHarness(opts) {
|
|
|
14705
14809
|
// src/update.ts
|
|
14706
14810
|
import { execFile } from "child_process";
|
|
14707
14811
|
import { createRequire } from "module";
|
|
14708
|
-
import { dirname as dirname3, join as
|
|
14812
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
14709
14813
|
import { promisify } from "util";
|
|
14710
14814
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
14711
14815
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -14732,7 +14836,7 @@ async function latestVersion() {
|
|
|
14732
14836
|
}
|
|
14733
14837
|
async function install(version) {
|
|
14734
14838
|
const bin = dirname3(process.execPath);
|
|
14735
|
-
await promisify(execFile)(
|
|
14839
|
+
await promisify(execFile)(join7(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
14736
14840
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
14737
14841
|
timeout: 5 * 6e4
|
|
14738
14842
|
});
|
|
@@ -14783,7 +14887,7 @@ function selfUpdater(deps) {
|
|
|
14783
14887
|
}
|
|
14784
14888
|
|
|
14785
14889
|
// src/host.ts
|
|
14786
|
-
var HOST_FILE =
|
|
14890
|
+
var HOST_FILE = join8(homedir7(), ".paigy", "host.json");
|
|
14787
14891
|
function readHostState() {
|
|
14788
14892
|
try {
|
|
14789
14893
|
const s = JSON.parse(readFileSync4(HOST_FILE, "utf8"));
|
|
@@ -15646,7 +15750,7 @@ var win = null;
|
|
|
15646
15750
|
function log(message) {
|
|
15647
15751
|
win?.webContents.send("paigy:log", message);
|
|
15648
15752
|
}
|
|
15649
|
-
var ICON =
|
|
15753
|
+
var ICON = join9(here, "..", "assets", "icon.png");
|
|
15650
15754
|
function createWindow() {
|
|
15651
15755
|
win = new BrowserWindow({
|
|
15652
15756
|
// Sized for the working log, which is now the window's centerpiece; the layout is a
|
|
@@ -15658,9 +15762,9 @@ function createWindow() {
|
|
|
15658
15762
|
minHeight: 520,
|
|
15659
15763
|
icon: ICON,
|
|
15660
15764
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
15661
|
-
webPreferences: { preload:
|
|
15765
|
+
webPreferences: { preload: join9(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
15662
15766
|
});
|
|
15663
|
-
void win.loadFile(
|
|
15767
|
+
void win.loadFile(join9(here, "..", "renderer", "index.html"));
|
|
15664
15768
|
}
|
|
15665
15769
|
var host = null;
|
|
15666
15770
|
app.whenReady().then(() => {
|
|
@@ -15729,7 +15833,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
15729
15833
|
});
|
|
15730
15834
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
15731
15835
|
try {
|
|
15732
|
-
const all = readFileSync5(
|
|
15836
|
+
const all = readFileSync5(join9(homedir9(), ".paigy", "host.log"), "utf8").split("\n");
|
|
15733
15837
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
15734
15838
|
return mine.slice(-500).join("\n");
|
|
15735
15839
|
} catch {
|