@paigy/harness 0.3.14 → 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 +434 -235
- package/dist/main.js +431 -233
- 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";
|
|
@@ -8638,6 +8638,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
8638
8638
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
8639
8639
|
import { homedir as homedir2 } from "os";
|
|
8640
8640
|
import { join as join2 } from "path";
|
|
8641
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
8641
8642
|
var import_undici = __toESM(require_undici(), 1);
|
|
8642
8643
|
var require2 = __sdkCreateRequire(import.meta.url);
|
|
8643
8644
|
var BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
@@ -9921,6 +9922,23 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
9921
9922
|
};
|
|
9922
9923
|
var OPTIONS_MIN = 2;
|
|
9923
9924
|
var OPTIONS_MAX = 6;
|
|
9925
|
+
var OptionSchema = external_exports.object({
|
|
9926
|
+
id: external_exports.string(),
|
|
9927
|
+
label: external_exports.string(),
|
|
9928
|
+
hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
|
|
9929
|
+
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
9930
|
+
// the constraints below are what an agent reads when deciding to use these.
|
|
9931
|
+
html: external_exports.string().max(16384).describe(
|
|
9932
|
+
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
9933
|
+
).optional(),
|
|
9934
|
+
image: external_exports.string().url().describe(
|
|
9935
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
9936
|
+
).optional()
|
|
9937
|
+
});
|
|
9938
|
+
var OptionInputSchema = OptionSchema.omit({ id: true }).extend({
|
|
9939
|
+
label: external_exports.string().trim().min(1).max(1e3)
|
|
9940
|
+
}).strict();
|
|
9941
|
+
var NIGHT = { from: 23, to: 7 };
|
|
9924
9942
|
function draft2020(node) {
|
|
9925
9943
|
if (Array.isArray(node)) return node.map(draft2020);
|
|
9926
9944
|
if (node && typeof node === "object") {
|
|
@@ -9952,26 +9970,21 @@ function mcpInputSchema(s) {
|
|
|
9952
9970
|
}
|
|
9953
9971
|
var AskInputSchema = external_exports.object({
|
|
9954
9972
|
id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
|
|
9955
|
-
parentId: external_exports.string().uuid().optional().describe("
|
|
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."),
|
|
9956
9974
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
9957
9975
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
9958
|
-
"
|
|
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."
|
|
9959
9977
|
),
|
|
9960
|
-
options: external_exports.array(
|
|
9961
|
-
label: external_exports.string().trim().min(1).max(1e3),
|
|
9962
|
-
hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
|
|
9963
|
-
image: external_exports.string().url().optional(),
|
|
9964
|
-
html: external_exports.string().max(16384).describe("Optional sandboxed HTML/CSS preview. No JavaScript or network; inline CSS and data: URIs only. It renders edge-to-edge in a responsive card 200pt tall (about 320pt wide on a phone, with the next option peeking beside it), so fit the HTML to that viewport.").optional()
|
|
9965
|
-
}).strict()).min(2).max(6).optional()
|
|
9978
|
+
options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
|
|
9966
9979
|
}).strict();
|
|
9967
9980
|
var StartContactSchema = external_exports.object({
|
|
9968
|
-
asks: external_exports.array(AskInputSchema).min(1).describe("The
|
|
9981
|
+
asks: external_exports.array(AskInputSchema).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
9969
9982
|
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
9970
9983
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
9971
9984
|
}).strict();
|
|
9972
9985
|
var ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
9973
9986
|
var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
9974
|
-
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'.
|
|
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).";
|
|
9975
9988
|
var CreateGoalSchema = external_exports.object({
|
|
9976
9989
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
9977
9990
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -9979,10 +9992,6 @@ var CreateGoalSchema = external_exports.object({
|
|
|
9979
9992
|
title: external_exports.string().trim().min(1).max(80).optional(),
|
|
9980
9993
|
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
9981
9994
|
idempotencyKey: external_exports.string().trim().min(1).max(200),
|
|
9982
|
-
/** A past conversation this Goal should be read against — History's "new session from this"
|
|
9983
|
-
* (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
|
|
9984
|
-
* reference only: the owner reads it through `get_thread`, which does its own scoping, and
|
|
9985
|
-
* the writer refuses a thread belonging to another account. */
|
|
9986
9995
|
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
9987
9996
|
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
9988
9997
|
* worked"). It could not have been: this door took no parent, so the only route was
|
|
@@ -10022,23 +10031,13 @@ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({
|
|
|
10022
10031
|
var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
10023
10032
|
var GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
10024
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.";
|
|
10025
|
-
var UPDATE_GOAL_DESCRIPTION =
|
|
10026
|
-
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.";
|
|
10027
|
-
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.";
|
|
10028
10037
|
var CheckRepliesSchema = external_exports.object({}).strict();
|
|
10029
|
-
var GetThreadSchema = external_exports.object({
|
|
10030
|
-
parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
10031
|
-
}).strict();
|
|
10032
|
-
var GET_THREAD_DESCRIPTION = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
|
|
10033
|
-
var SearchThreadsSchema = external_exports.object({
|
|
10034
|
-
q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
10035
|
-
}).strict();
|
|
10036
|
-
var SEARCH_THREADS_DESCRIPTION = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
|
|
10037
10038
|
var AGENT_TOOLS = [
|
|
10038
10039
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
10039
10040
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
10040
|
-
{ name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
|
|
10041
|
-
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
|
|
10042
10041
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
10043
10042
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
10044
10043
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
@@ -10072,19 +10071,6 @@ var TransformSchema = external_exports.enum([
|
|
|
10072
10071
|
"summarize"
|
|
10073
10072
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
10074
10073
|
]);
|
|
10075
|
-
var OptionSchema = external_exports.object({
|
|
10076
|
-
id: external_exports.string(),
|
|
10077
|
-
label: external_exports.string(),
|
|
10078
|
-
hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
|
|
10079
|
-
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
10080
|
-
// the constraints below are what an agent reads when deciding to use these.
|
|
10081
|
-
html: external_exports.string().max(16384).describe(
|
|
10082
|
-
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
10083
|
-
).optional(),
|
|
10084
|
-
image: external_exports.string().url().describe(
|
|
10085
|
-
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
10086
|
-
).optional()
|
|
10087
|
-
});
|
|
10088
10074
|
var VisualSchema = external_exports.object({
|
|
10089
10075
|
url: external_exports.string().url(),
|
|
10090
10076
|
label: external_exports.string().optional()
|
|
@@ -10138,7 +10124,7 @@ var NotifyRequestFields = external_exports.object({
|
|
|
10138
10124
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
10139
10125
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
10140
10126
|
context: ContextSchema.optional(),
|
|
10141
|
-
options: external_exports.array(
|
|
10127
|
+
options: external_exports.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
10142
10128
|
"The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
|
|
10143
10129
|
),
|
|
10144
10130
|
points: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
@@ -10332,7 +10318,7 @@ var AwaitItemSchema = external_exports.discriminatedUnion("type", [
|
|
|
10332
10318
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
10333
10319
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
10334
10320
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
10335
|
-
* re-orient via
|
|
10321
|
+
* re-orient via check_replies). */
|
|
10336
10322
|
external_exports.object({
|
|
10337
10323
|
type: external_exports.literal("superseded"),
|
|
10338
10324
|
parentId: external_exports.string(),
|
|
@@ -10377,9 +10363,9 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10377
10363
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
10378
10364
|
* the planner declares duplicates instead of asking them twice. */
|
|
10379
10365
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
10380
|
-
/**
|
|
10381
|
-
*
|
|
10382
|
-
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([]),
|
|
10383
10369
|
question: external_exports.string().min(1).nullable(),
|
|
10384
10370
|
/** True on the one turn carrying the agent's own declared question. */
|
|
10385
10371
|
asks: external_exports.boolean().optional(),
|
|
@@ -10414,42 +10400,12 @@ var AgendaTurnSchema = external_exports.object({
|
|
|
10414
10400
|
var CLAIM_STALE_MS = 30 * 6e4;
|
|
10415
10401
|
var InboxItemSchema = external_exports.object({
|
|
10416
10402
|
id: external_exports.string(),
|
|
10417
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
10418
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
10419
|
-
parentId: external_exports.string().optional(),
|
|
10420
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
10421
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
10422
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
10423
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
10424
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
10425
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
10426
|
-
* thread, which is what the client did for all rows until now. */
|
|
10427
|
-
askId: external_exports.string().optional(),
|
|
10428
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
10429
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
10430
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
10431
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
10432
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
10433
|
-
seq: external_exports.number().int().optional(),
|
|
10434
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
10435
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
10436
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
10437
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
10438
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
10439
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
10440
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
10441
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
10442
|
-
units: external_exports.number().int().positive().optional(),
|
|
10443
10403
|
tokenId: external_exports.string().optional(),
|
|
10444
10404
|
status: NotifyStatusSchema,
|
|
10445
10405
|
context: ContextSchema,
|
|
10446
10406
|
options: external_exports.array(OptionSchema).optional(),
|
|
10447
10407
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
10448
10408
|
points: external_exports.array(external_exports.string()).optional(),
|
|
10449
|
-
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
10450
|
-
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
10451
|
-
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
10452
|
-
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
10453
10409
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
10454
10410
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
10455
10411
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
@@ -10470,7 +10426,31 @@ var InboxItemSchema = external_exports.object({
|
|
|
10470
10426
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
10471
10427
|
* `createdAt`. */
|
|
10472
10428
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
10473
|
-
agenda
|
|
10429
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
10430
|
+
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
10431
|
+
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
10432
|
+
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
10433
|
+
* once, off the one read it already makes (`GET /api/inbox/:callId`).
|
|
10434
|
+
*
|
|
10435
|
+
* It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
|
|
10436
|
+
* every cycle, so a spine drawn from them would change length under the caller mid-call.
|
|
10437
|
+
*
|
|
10438
|
+
* `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
|
|
10439
|
+
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
10440
|
+
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
10441
|
+
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
10442
|
+
questions: external_exports.array(external_exports.object({
|
|
10443
|
+
id: external_exports.string(),
|
|
10444
|
+
entryId: external_exports.string(),
|
|
10445
|
+
title: external_exports.string(),
|
|
10446
|
+
state: external_exports.enum(["open", "answered"]),
|
|
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()
|
|
10453
|
+
})).optional(),
|
|
10474
10454
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
10475
10455
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
10476
10456
|
* agent's suggestion, or a default silly name). */
|
|
@@ -10487,16 +10467,35 @@ var InboxItemSchema = external_exports.object({
|
|
|
10487
10467
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
10488
10468
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
10489
10469
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
10490
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
10491
|
-
*
|
|
10470
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
10471
|
+
* error badge + Retry. */
|
|
10492
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(),
|
|
10493
10478
|
clarifies: external_exports.string().optional(),
|
|
10494
|
-
/**
|
|
10495
|
-
*
|
|
10496
|
-
*
|
|
10497
|
-
*
|
|
10498
|
-
|
|
10499
|
-
|
|
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(),
|
|
10484
|
+
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
10485
|
+
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
10486
|
+
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
10487
|
+
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
10488
|
+
* `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
|
|
10489
|
+
* has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
|
|
10490
|
+
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
10491
|
+
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
10492
|
+
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
10493
|
+
ring: external_exports.object({
|
|
10494
|
+
declinedAt: external_exports.string().datetime(),
|
|
10495
|
+
anchorAt: external_exports.string().datetime(),
|
|
10496
|
+
nextRingAt: external_exports.string().datetime().nullable(),
|
|
10497
|
+
step: external_exports.number().int()
|
|
10498
|
+
}).optional(),
|
|
10500
10499
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
10501
10500
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
10502
10501
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
@@ -10530,7 +10529,10 @@ var InboxItemSchema = external_exports.object({
|
|
|
10530
10529
|
goalIds: external_exports.array(external_exports.string()),
|
|
10531
10530
|
decisionNeedId: external_exports.string().optional(),
|
|
10532
10531
|
sealed: external_exports.boolean(),
|
|
10533
|
-
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()
|
|
10534
10536
|
}).optional()
|
|
10535
10537
|
});
|
|
10536
10538
|
var APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
@@ -10561,6 +10563,8 @@ var MissedCallSchema = external_exports.enum([
|
|
|
10561
10563
|
"inbox",
|
|
10562
10564
|
"dismiss"
|
|
10563
10565
|
]);
|
|
10566
|
+
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
10567
|
+
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
10564
10568
|
var BrokerTuningSchema = external_exports.object({
|
|
10565
10569
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
10566
10570
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -10595,6 +10599,12 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10595
10599
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
10596
10600
|
* write, 'hosted' on read (see store.ts). */
|
|
10597
10601
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
10602
|
+
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
10603
|
+
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
10604
|
+
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
10605
|
+
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
10606
|
+
* read (see store.ts). */
|
|
10607
|
+
talk: external_exports.boolean().optional(),
|
|
10598
10608
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
10599
10609
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
10600
10610
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -10621,7 +10631,6 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10621
10631
|
});
|
|
10622
10632
|
var HistoryItemSchema = external_exports.object({
|
|
10623
10633
|
id: external_exports.string(),
|
|
10624
|
-
parentId: external_exports.string(),
|
|
10625
10634
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
10626
10635
|
initiator: external_exports.enum(["user", "agent"]),
|
|
10627
10636
|
title: external_exports.string(),
|
|
@@ -10780,6 +10789,24 @@ var QueueQuestionSchema = external_exports.object({
|
|
|
10780
10789
|
* gate at all. */
|
|
10781
10790
|
blocked: external_exports.boolean().default(false)
|
|
10782
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
|
+
});
|
|
10783
10810
|
var QueueItemSchema = external_exports.object({
|
|
10784
10811
|
id: external_exports.string(),
|
|
10785
10812
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -10792,6 +10819,9 @@ var QueueItemSchema = external_exports.object({
|
|
|
10792
10819
|
assignee: external_exports.string().nullable().default(null),
|
|
10793
10820
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
10794
10821
|
progress: external_exports.string().nullable().default(null),
|
|
10822
|
+
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
10823
|
+
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
10824
|
+
progressLine: external_exports.string().nullable().optional(),
|
|
10795
10825
|
reviewPending: external_exports.boolean().default(false),
|
|
10796
10826
|
dueAt: external_exports.string().nullable().default(null),
|
|
10797
10827
|
/** The Goal this one was opened under; null at the root. */
|
|
@@ -10804,11 +10834,20 @@ var QueueItemSchema = external_exports.object({
|
|
|
10804
10834
|
blocked: external_exports.boolean().default(false),
|
|
10805
10835
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
10806
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(),
|
|
10807
10841
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
10808
10842
|
repo: external_exports.string().nullable().optional(),
|
|
10809
10843
|
createdAt: external_exports.string(),
|
|
10810
|
-
updatedAt: external_exports.string().nullable().default(null)
|
|
10844
|
+
updatedAt: external_exports.string().nullable().default(null),
|
|
10845
|
+
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
10846
|
+
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
10847
|
+
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
10848
|
+
lastProgressAt: external_exports.string().nullable().optional()
|
|
10811
10849
|
});
|
|
10850
|
+
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
10812
10851
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
10813
10852
|
var NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
10814
10853
|
var NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
|
|
@@ -11109,6 +11148,8 @@ var ApiError = class extends Error {
|
|
|
11109
11148
|
this.body = body;
|
|
11110
11149
|
}
|
|
11111
11150
|
};
|
|
11151
|
+
var tokenOverride = null;
|
|
11152
|
+
var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
11112
11153
|
async function fail(what, res) {
|
|
11113
11154
|
throw new ApiError(what, res.status, await res.text());
|
|
11114
11155
|
}
|
|
@@ -11145,8 +11186,6 @@ async function updateGoal(goalId, input, opts = {}) {
|
|
|
11145
11186
|
return await res.json();
|
|
11146
11187
|
}
|
|
11147
11188
|
var AWAIT_WINDOW_MS = 45e3;
|
|
11148
|
-
var tokenOverride = null;
|
|
11149
|
-
var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
11150
11189
|
async function whoAmI(opts = {}) {
|
|
11151
11190
|
try {
|
|
11152
11191
|
const res = await reach(`${BACKEND_URL}/api/identity`, {
|
|
@@ -11233,6 +11272,33 @@ async function dismissTriage(proposalId, opts = {}) {
|
|
|
11233
11272
|
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
11234
11273
|
return await res.json();
|
|
11235
11274
|
}
|
|
11275
|
+
function repoFromRemote(url) {
|
|
11276
|
+
const said = (url ?? "").trim();
|
|
11277
|
+
if (!said) return null;
|
|
11278
|
+
const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
11279
|
+
const parts = path.split("/").filter(Boolean);
|
|
11280
|
+
if (parts.length < 2) return null;
|
|
11281
|
+
const [owner, name] = parts.slice(-2);
|
|
11282
|
+
if (!owner || !name) return null;
|
|
11283
|
+
return `${owner}/${name}`.toLowerCase();
|
|
11284
|
+
}
|
|
11285
|
+
var asked;
|
|
11286
|
+
function currentRepo(cwd = process.cwd()) {
|
|
11287
|
+
if (asked !== void 0) return asked;
|
|
11288
|
+
try {
|
|
11289
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
11290
|
+
cwd,
|
|
11291
|
+
encoding: "utf8",
|
|
11292
|
+
timeout: 2e3,
|
|
11293
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11294
|
+
});
|
|
11295
|
+
asked = repoFromRemote(url);
|
|
11296
|
+
} catch {
|
|
11297
|
+
asked = null;
|
|
11298
|
+
}
|
|
11299
|
+
return asked;
|
|
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.";
|
|
11236
11302
|
async function contact(input, opts = {}) {
|
|
11237
11303
|
const parsed = ContactSchema.parse(input);
|
|
11238
11304
|
opts.signal?.throwIfAborted();
|
|
@@ -11240,13 +11306,18 @@ async function contact(input, opts = {}) {
|
|
|
11240
11306
|
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
|
|
11241
11307
|
let deliveryId;
|
|
11242
11308
|
let deliveries = [];
|
|
11309
|
+
let recorded = [];
|
|
11243
11310
|
if ("deliveryId" in parsed) {
|
|
11244
11311
|
deliveryId = parsed.deliveryId;
|
|
11245
11312
|
} else {
|
|
11246
11313
|
const reqAsks = parsed.asks.map((a) => ({
|
|
11247
11314
|
id: a.id,
|
|
11248
11315
|
parentId: a.parentId,
|
|
11249
|
-
|
|
11316
|
+
// Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
|
|
11317
|
+
// work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
|
|
11318
|
+
// still wins — normalised the same way — because a caller that names one knows something
|
|
11319
|
+
// this cannot see.
|
|
11320
|
+
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
11250
11321
|
ask: a.ask,
|
|
11251
11322
|
options: a.options ?? []
|
|
11252
11323
|
}));
|
|
@@ -11264,14 +11335,24 @@ async function contact(input, opts = {}) {
|
|
|
11264
11335
|
if (!res.ok) await fail("batch contact", res);
|
|
11265
11336
|
const receipt = await res.json();
|
|
11266
11337
|
deliveries = receipt.deliveries ?? [];
|
|
11338
|
+
recorded = receipt.recorded ?? [];
|
|
11339
|
+
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
11267
11340
|
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
11268
11341
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
11269
11342
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
11270
11343
|
}
|
|
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;
|
|
11271
11346
|
const read = async (signal2) => {
|
|
11272
11347
|
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
11273
11348
|
if (!res.ok) await fail("read_delivery", res);
|
|
11274
|
-
|
|
11349
|
+
const one = await res.json();
|
|
11350
|
+
return {
|
|
11351
|
+
...one,
|
|
11352
|
+
...all.length > 1 ? { deliveries: all } : {},
|
|
11353
|
+
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
11354
|
+
...joinedCard ? { joinedCard: true } : {}
|
|
11355
|
+
};
|
|
11275
11356
|
};
|
|
11276
11357
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
11277
11358
|
if (opts.waits === false) return read(opts.signal);
|
|
@@ -11301,47 +11382,108 @@ async function checkReplies(opts = {}) {
|
|
|
11301
11382
|
var HEARTBEAT_MS = 25e3;
|
|
11302
11383
|
var RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
11303
11384
|
var JOIN = { config: { broadcast: { self: false } } };
|
|
11385
|
+
var JOIN_TIMEOUT_MS = 1e4;
|
|
11304
11386
|
var lines = /* @__PURE__ */ new Map();
|
|
11305
11387
|
function frame(line, topic, event, payload) {
|
|
11388
|
+
const ref = String(++line.ref);
|
|
11306
11389
|
try {
|
|
11307
|
-
line.ws?.send(JSON.stringify({ topic, event, payload, ref
|
|
11390
|
+
line.ws?.send(JSON.stringify({ topic, event, payload, ref }));
|
|
11308
11391
|
} catch {
|
|
11309
11392
|
}
|
|
11393
|
+
return ref;
|
|
11310
11394
|
}
|
|
11311
11395
|
function stop(line) {
|
|
11312
11396
|
line.open = false;
|
|
11397
|
+
line.beat = void 0;
|
|
11313
11398
|
clearInterval(line.heart);
|
|
11314
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);
|
|
11315
11426
|
}
|
|
11316
11427
|
function connect(line) {
|
|
11317
11428
|
const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
11318
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
|
+
};
|
|
11319
11438
|
ws.addEventListener("open", () => {
|
|
11439
|
+
if (gone) return;
|
|
11320
11440
|
line.open = true;
|
|
11321
11441
|
line.attempt = 0;
|
|
11322
|
-
for (const
|
|
11323
|
-
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);
|
|
11324
11454
|
});
|
|
11325
11455
|
ws.addEventListener("message", (e) => {
|
|
11456
|
+
if (gone) return;
|
|
11326
11457
|
let m;
|
|
11327
11458
|
try {
|
|
11328
11459
|
m = JSON.parse(String(e.data));
|
|
11329
11460
|
} catch {
|
|
11330
11461
|
return;
|
|
11331
11462
|
}
|
|
11332
|
-
if (m.
|
|
11333
|
-
|
|
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) {
|
|
11334
11484
|
if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
|
|
11335
11485
|
}
|
|
11336
11486
|
});
|
|
11337
|
-
let gone = false;
|
|
11338
|
-
const again = () => {
|
|
11339
|
-
if (gone) return;
|
|
11340
|
-
gone = true;
|
|
11341
|
-
stop(line);
|
|
11342
|
-
if (!line.topics.size) return;
|
|
11343
|
-
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
11344
|
-
};
|
|
11345
11487
|
ws.addEventListener("close", again);
|
|
11346
11488
|
ws.addEventListener("error", again);
|
|
11347
11489
|
}
|
|
@@ -11349,17 +11491,22 @@ function subscribeRealtime(args) {
|
|
|
11349
11491
|
const key = `${args.url} ${args.anonKey}`;
|
|
11350
11492
|
let line = lines.get(key);
|
|
11351
11493
|
if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
|
|
11352
|
-
let
|
|
11353
|
-
if (!
|
|
11354
|
-
line.topics.set(args.topic,
|
|
11355
|
-
if (line.open)
|
|
11356
|
-
}
|
|
11357
|
-
const
|
|
11358
|
-
|
|
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
|
+
});
|
|
11359
11505
|
if (!line.ws) connect(line);
|
|
11360
11506
|
return {
|
|
11361
11507
|
close: () => {
|
|
11362
|
-
if (!
|
|
11508
|
+
if (!topic.listeners.delete(me) || topic.listeners.size) return;
|
|
11509
|
+
clearTimeout(topic.timer);
|
|
11363
11510
|
line.topics.delete(args.topic);
|
|
11364
11511
|
if (line.topics.size) {
|
|
11365
11512
|
if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
|
|
@@ -11375,7 +11522,8 @@ function subscribeRealtime(args) {
|
|
|
11375
11522
|
};
|
|
11376
11523
|
}
|
|
11377
11524
|
async function subscribeWake(onNudge, opts = {}) {
|
|
11378
|
-
const
|
|
11525
|
+
const { release = true, onJoined, ...client } = opts;
|
|
11526
|
+
const cfg = await registerDelivery("self_hosted", client);
|
|
11379
11527
|
const { url, anonKey } = cfg.realtime;
|
|
11380
11528
|
const channel = wakeChannel(cfg.tokenId);
|
|
11381
11529
|
const ch = subscribeRealtime({
|
|
@@ -11383,14 +11531,15 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
11383
11531
|
anonKey,
|
|
11384
11532
|
topic: channel,
|
|
11385
11533
|
event: WAKE_EVENT,
|
|
11386
|
-
onMessage: (payload) => onNudge(payload ?? {})
|
|
11534
|
+
onMessage: (payload) => onNudge(payload ?? {}),
|
|
11535
|
+
onJoined
|
|
11387
11536
|
});
|
|
11388
11537
|
return {
|
|
11389
11538
|
tokenId: cfg.tokenId,
|
|
11390
11539
|
channel,
|
|
11391
11540
|
close: async () => {
|
|
11392
11541
|
ch.close();
|
|
11393
|
-
await registerDelivery("poll",
|
|
11542
|
+
if (release) await registerDelivery("poll", client).catch(() => {
|
|
11394
11543
|
});
|
|
11395
11544
|
}
|
|
11396
11545
|
};
|
|
@@ -11404,7 +11553,7 @@ import { homedir as homedir9, hostname } from "os";
|
|
|
11404
11553
|
import { spawnSync } from "child_process";
|
|
11405
11554
|
import { existsSync as existsSync2 } from "fs";
|
|
11406
11555
|
import { homedir as homedir3 } from "os";
|
|
11407
|
-
import { delimiter, dirname, join as
|
|
11556
|
+
import { delimiter, dirname, join as join4 } from "path";
|
|
11408
11557
|
var CATALOG = [
|
|
11409
11558
|
{
|
|
11410
11559
|
name: "claude",
|
|
@@ -11440,10 +11589,10 @@ var CATALOG = [
|
|
|
11440
11589
|
function probeDirs(home = homedir3()) {
|
|
11441
11590
|
return [
|
|
11442
11591
|
dirname(process.execPath),
|
|
11443
|
-
|
|
11592
|
+
join4(home, ".local", "bin"),
|
|
11444
11593
|
"/usr/local/bin",
|
|
11445
11594
|
"/opt/homebrew/bin",
|
|
11446
|
-
|
|
11595
|
+
join4(home, ".npm-global", "bin")
|
|
11447
11596
|
];
|
|
11448
11597
|
}
|
|
11449
11598
|
function which(bin) {
|
|
@@ -11451,7 +11600,7 @@ function which(bin) {
|
|
|
11451
11600
|
const fromPath = hit.status === 0 ? hit.stdout.trim() : "";
|
|
11452
11601
|
if (fromPath) return fromPath;
|
|
11453
11602
|
for (const dir of probeDirs()) {
|
|
11454
|
-
const candidate =
|
|
11603
|
+
const candidate = join4(dir, bin);
|
|
11455
11604
|
if (existsSync2(candidate)) return candidate;
|
|
11456
11605
|
}
|
|
11457
11606
|
return null;
|
|
@@ -11493,14 +11642,14 @@ function detectAll(deps = {}) {
|
|
|
11493
11642
|
// src/host.ts
|
|
11494
11643
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
11495
11644
|
import { homedir as homedir7 } from "os";
|
|
11496
|
-
import { join as
|
|
11645
|
+
import { join as join8 } from "path";
|
|
11497
11646
|
|
|
11498
11647
|
// src/workspaces.ts
|
|
11499
11648
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
11500
|
-
import { dirname as dirname2, join as
|
|
11649
|
+
import { dirname as dirname2, join as join5, resolve } from "path";
|
|
11501
11650
|
import { homedir as homedir4 } from "os";
|
|
11502
11651
|
function workspacesFile() {
|
|
11503
|
-
return
|
|
11652
|
+
return join5(homedir4(), ".paigy", "workspaces.json");
|
|
11504
11653
|
}
|
|
11505
11654
|
function listWorkspaces(deps) {
|
|
11506
11655
|
try {
|
|
@@ -11541,7 +11690,7 @@ function resolveWakeDir(pinned, deps) {
|
|
|
11541
11690
|
// src/owner.ts
|
|
11542
11691
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
|
|
11543
11692
|
import { homedir as homedir5 } from "os";
|
|
11544
|
-
import { join as
|
|
11693
|
+
import { join as join6 } from "path";
|
|
11545
11694
|
var HEAD_BYTES = 64e3;
|
|
11546
11695
|
function readHead(file) {
|
|
11547
11696
|
const fd = openSync2(file, "r");
|
|
@@ -11557,7 +11706,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11557
11706
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
11558
11707
|
if (!m) return null;
|
|
11559
11708
|
const prefix = m[1];
|
|
11560
|
-
const root = deps.projectsDir ??
|
|
11709
|
+
const root = deps.projectsDir ?? join6(homedir5(), ".claude", "projects");
|
|
11561
11710
|
const list = deps.list ?? ((dir) => readdirSync(dir));
|
|
11562
11711
|
const head = deps.head ?? readHead;
|
|
11563
11712
|
let folders;
|
|
@@ -11569,7 +11718,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11569
11718
|
for (const folder of folders) {
|
|
11570
11719
|
let files;
|
|
11571
11720
|
try {
|
|
11572
|
-
files = list(
|
|
11721
|
+
files = list(join6(root, folder));
|
|
11573
11722
|
} catch {
|
|
11574
11723
|
continue;
|
|
11575
11724
|
}
|
|
@@ -11577,7 +11726,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
11577
11726
|
if (!file) continue;
|
|
11578
11727
|
let text;
|
|
11579
11728
|
try {
|
|
11580
|
-
text = head(
|
|
11729
|
+
text = head(join6(root, folder, file));
|
|
11581
11730
|
} catch {
|
|
11582
11731
|
continue;
|
|
11583
11732
|
}
|
|
@@ -12881,6 +13030,23 @@ var zodToJsonSchema2 = (schema, options) => {
|
|
|
12881
13030
|
// ../../packages/schema/dist/index.js
|
|
12882
13031
|
var OPTIONS_MIN2 = 2;
|
|
12883
13032
|
var OPTIONS_MAX2 = 6;
|
|
13033
|
+
var OptionSchema2 = external_exports.object({
|
|
13034
|
+
id: external_exports.string(),
|
|
13035
|
+
label: external_exports.string(),
|
|
13036
|
+
hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
|
|
13037
|
+
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
13038
|
+
// the constraints below are what an agent reads when deciding to use these.
|
|
13039
|
+
html: external_exports.string().max(16384).describe(
|
|
13040
|
+
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
13041
|
+
).optional(),
|
|
13042
|
+
image: external_exports.string().url().describe(
|
|
13043
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
13044
|
+
).optional()
|
|
13045
|
+
});
|
|
13046
|
+
var OptionInputSchema2 = OptionSchema2.omit({ id: true }).extend({
|
|
13047
|
+
label: external_exports.string().trim().min(1).max(1e3)
|
|
13048
|
+
}).strict();
|
|
13049
|
+
var NIGHT2 = { from: 23, to: 7 };
|
|
12884
13050
|
function draft20202(node) {
|
|
12885
13051
|
if (Array.isArray(node)) return node.map(draft20202);
|
|
12886
13052
|
if (node && typeof node === "object") {
|
|
@@ -12912,26 +13078,21 @@ function mcpInputSchema2(s) {
|
|
|
12912
13078
|
}
|
|
12913
13079
|
var AskInputSchema2 = external_exports.object({
|
|
12914
13080
|
id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
|
|
12915
|
-
parentId: external_exports.string().uuid().optional().describe("
|
|
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."),
|
|
12916
13082
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
12917
13083
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
12918
|
-
"
|
|
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."
|
|
12919
13085
|
),
|
|
12920
|
-
options: external_exports.array(
|
|
12921
|
-
label: external_exports.string().trim().min(1).max(1e3),
|
|
12922
|
-
hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
|
|
12923
|
-
image: external_exports.string().url().optional(),
|
|
12924
|
-
html: external_exports.string().max(16384).describe("Optional sandboxed HTML/CSS preview. No JavaScript or network; inline CSS and data: URIs only. It renders edge-to-edge in a responsive card 200pt tall (about 320pt wide on a phone, with the next option peeking beside it), so fit the HTML to that viewport.").optional()
|
|
12925
|
-
}).strict()).min(2).max(6).optional()
|
|
13086
|
+
options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
|
|
12926
13087
|
}).strict();
|
|
12927
13088
|
var StartContactSchema2 = external_exports.object({
|
|
12928
|
-
asks: external_exports.array(AskInputSchema2).min(1).describe("The
|
|
13089
|
+
asks: external_exports.array(AskInputSchema2).min(1).describe("The questions to pose, one per object. An ask with a parentId is filed on that Goal as it is; only an ask naming no Goal is placed in the tree by Paigy."),
|
|
12929
13090
|
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
12930
13091
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
12931
13092
|
}).strict();
|
|
12932
13093
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
12933
13094
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
12934
|
-
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'.
|
|
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).";
|
|
12935
13096
|
var CreateGoalSchema2 = external_exports.object({
|
|
12936
13097
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
12937
13098
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -12939,10 +13100,6 @@ var CreateGoalSchema2 = external_exports.object({
|
|
|
12939
13100
|
title: external_exports.string().trim().min(1).max(80).optional(),
|
|
12940
13101
|
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
12941
13102
|
idempotencyKey: external_exports.string().trim().min(1).max(200),
|
|
12942
|
-
/** A past conversation this Goal should be read against — History's "new session from this"
|
|
12943
|
-
* (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
|
|
12944
|
-
* reference only: the owner reads it through `get_thread`, which does its own scoping, and
|
|
12945
|
-
* the writer refuses a thread belonging to another account. */
|
|
12946
13103
|
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
12947
13104
|
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
12948
13105
|
* worked"). It could not have been: this door took no parent, so the only route was
|
|
@@ -12982,23 +13139,13 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
|
|
|
12982
13139
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
12983
13140
|
var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
12984
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.";
|
|
12985
|
-
var UPDATE_GOAL_DESCRIPTION2 =
|
|
12986
|
-
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.";
|
|
12987
|
-
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.";
|
|
12988
13145
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
12989
|
-
var GetThreadSchema2 = external_exports.object({
|
|
12990
|
-
parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
12991
|
-
}).strict();
|
|
12992
|
-
var GET_THREAD_DESCRIPTION2 = "Read the authorized durable Entries on one conversation Thread \u2014 what you wrote there and what was delivered to you, oldest first. Use claim_goal to find the work to resume; use this to rehydrate a Thread that a search hit or a Delivery named.";
|
|
12993
|
-
var SearchThreadsSchema2 = external_exports.object({
|
|
12994
|
-
q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
12995
|
-
}).strict();
|
|
12996
|
-
var SEARCH_THREADS_DESCRIPTION2 = `Search your PAST conversations before asking \u2014 "have we discussed this before?". Full-text over your own threads (the asks you sent + the user's answers); returns ranked threads with highlighted snippets, NOT rows: { hits: [{ parentId, at, agentLabel, matches: [{ notificationId, role, snippet }] }] }. The loop this exists for: search first \u2192 get_thread the best hit to rehydrate it \u2192 THEN continue or contact, so you answer with receipts ("last week you said ship it") instead of re-asking. Read-only, safe to call anytime; scoped to your own account's threads.`;
|
|
12997
13146
|
var AGENT_TOOLS2 = [
|
|
12998
13147
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
12999
13148
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
13000
|
-
{ name: "get_thread", description: GET_THREAD_DESCRIPTION2, inputSchema: mcpInputSchema2(GetThreadSchema2) },
|
|
13001
|
-
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchThreadsSchema2) },
|
|
13002
13149
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
|
|
13003
13150
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
|
|
13004
13151
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
|
|
@@ -13047,19 +13194,6 @@ var TransformSchema2 = external_exports.enum([
|
|
|
13047
13194
|
"summarize"
|
|
13048
13195
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
13049
13196
|
]);
|
|
13050
|
-
var OptionSchema2 = external_exports.object({
|
|
13051
|
-
id: external_exports.string(),
|
|
13052
|
-
label: external_exports.string(),
|
|
13053
|
-
hint: external_exports.string().max(500).describe("Optional short projection of consequence or action if this option is chosen (e.g. 'Reruns test suite', 'Merges to main').").optional(),
|
|
13054
|
-
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
13055
|
-
// the constraints below are what an agent reads when deciding to use these.
|
|
13056
|
-
html: external_exports.string().max(16384).describe(
|
|
13057
|
-
"Optional sandboxed HTML/CSS preview for a visual 'pick one' (shown in the option card). Untrusted-sandboxed: NO JavaScript, NO external network or images \u2014 inline CSS and data: URIs only; <=16KB. Rendered edge-to-edge in a responsive card that is 200pt tall (about 320pt wide on a phone, with the next option peeking beside it); make your HTML fit that viewport. Use for layout/CSS mockups, tables, diffs. For a hosted image use `image` instead."
|
|
13058
|
-
).optional(),
|
|
13059
|
-
image: external_exports.string().url().describe(
|
|
13060
|
-
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
13061
|
-
).optional()
|
|
13062
|
-
});
|
|
13063
13197
|
var VisualSchema2 = external_exports.object({
|
|
13064
13198
|
url: external_exports.string().url(),
|
|
13065
13199
|
label: external_exports.string().optional()
|
|
@@ -13113,7 +13247,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
13113
13247
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
13114
13248
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
13115
13249
|
context: ContextSchema2.optional(),
|
|
13116
|
-
options: external_exports.array(
|
|
13250
|
+
options: external_exports.array(OptionInputSchema2).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
|
|
13117
13251
|
"The choices, in order \u2014 required when select is 'one'/'many'/'rank', omitted otherwise. Ids are assigned automatically by position ('1', '2', \u2026); the user's answer references them as optionId(s)."
|
|
13118
13252
|
),
|
|
13119
13253
|
points: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
@@ -13307,7 +13441,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
13307
13441
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
13308
13442
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
13309
13443
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
13310
|
-
* re-orient via
|
|
13444
|
+
* re-orient via check_replies). */
|
|
13311
13445
|
external_exports.object({
|
|
13312
13446
|
type: external_exports.literal("superseded"),
|
|
13313
13447
|
parentId: external_exports.string(),
|
|
@@ -13352,9 +13486,9 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13352
13486
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
13353
13487
|
* the planner declares duplicates instead of asking them twice. */
|
|
13354
13488
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
13355
|
-
/**
|
|
13356
|
-
*
|
|
13357
|
-
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([]),
|
|
13358
13492
|
question: external_exports.string().min(1).nullable(),
|
|
13359
13493
|
/** True on the one turn carrying the agent's own declared question. */
|
|
13360
13494
|
asks: external_exports.boolean().optional(),
|
|
@@ -13389,42 +13523,12 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
13389
13523
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
13390
13524
|
var InboxItemSchema2 = external_exports.object({
|
|
13391
13525
|
id: external_exports.string(),
|
|
13392
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
13393
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
13394
|
-
parentId: external_exports.string().optional(),
|
|
13395
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
13396
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
13397
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
13398
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
13399
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
13400
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
13401
|
-
* thread, which is what the client did for all rows until now. */
|
|
13402
|
-
askId: external_exports.string().optional(),
|
|
13403
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
13404
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
13405
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
13406
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
13407
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
13408
|
-
seq: external_exports.number().int().optional(),
|
|
13409
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
13410
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
13411
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
13412
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
13413
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
13414
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
13415
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
13416
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
13417
|
-
units: external_exports.number().int().positive().optional(),
|
|
13418
13526
|
tokenId: external_exports.string().optional(),
|
|
13419
13527
|
status: NotifyStatusSchema2,
|
|
13420
13528
|
context: ContextSchema2,
|
|
13421
13529
|
options: external_exports.array(OptionSchema2).optional(),
|
|
13422
13530
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
13423
13531
|
points: external_exports.array(external_exports.string()).optional(),
|
|
13424
|
-
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
13425
|
-
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
13426
|
-
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
13427
|
-
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
13428
13532
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
13429
13533
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
13430
13534
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
@@ -13445,7 +13549,31 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13445
13549
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
13446
13550
|
* `createdAt`. */
|
|
13447
13551
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
13448
|
-
agenda
|
|
13552
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
13553
|
+
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
13554
|
+
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
13555
|
+
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
13556
|
+
* once, off the one read it already makes (`GET /api/inbox/:callId`).
|
|
13557
|
+
*
|
|
13558
|
+
* It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
|
|
13559
|
+
* every cycle, so a spine drawn from them would change length under the caller mid-call.
|
|
13560
|
+
*
|
|
13561
|
+
* `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
|
|
13562
|
+
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
13563
|
+
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
13564
|
+
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
13565
|
+
questions: external_exports.array(external_exports.object({
|
|
13566
|
+
id: external_exports.string(),
|
|
13567
|
+
entryId: external_exports.string(),
|
|
13568
|
+
title: external_exports.string(),
|
|
13569
|
+
state: external_exports.enum(["open", "answered"]),
|
|
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()
|
|
13576
|
+
})).optional(),
|
|
13449
13577
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
13450
13578
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
13451
13579
|
* agent's suggestion, or a default silly name). */
|
|
@@ -13462,16 +13590,35 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13462
13590
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
13463
13591
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
13464
13592
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
13465
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
13466
|
-
*
|
|
13593
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
13594
|
+
* error badge + Retry. */
|
|
13467
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(),
|
|
13468
13601
|
clarifies: external_exports.string().optional(),
|
|
13469
|
-
/**
|
|
13470
|
-
*
|
|
13471
|
-
*
|
|
13472
|
-
*
|
|
13473
|
-
|
|
13474
|
-
|
|
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(),
|
|
13607
|
+
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
13608
|
+
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
13609
|
+
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
13610
|
+
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
13611
|
+
* `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
|
|
13612
|
+
* has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
|
|
13613
|
+
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
13614
|
+
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
13615
|
+
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
13616
|
+
ring: external_exports.object({
|
|
13617
|
+
declinedAt: external_exports.string().datetime(),
|
|
13618
|
+
anchorAt: external_exports.string().datetime(),
|
|
13619
|
+
nextRingAt: external_exports.string().datetime().nullable(),
|
|
13620
|
+
step: external_exports.number().int()
|
|
13621
|
+
}).optional(),
|
|
13475
13622
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
13476
13623
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
13477
13624
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
@@ -13505,7 +13652,10 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13505
13652
|
goalIds: external_exports.array(external_exports.string()),
|
|
13506
13653
|
decisionNeedId: external_exports.string().optional(),
|
|
13507
13654
|
sealed: external_exports.boolean(),
|
|
13508
|
-
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()
|
|
13509
13659
|
}).optional()
|
|
13510
13660
|
});
|
|
13511
13661
|
var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
|
|
@@ -13536,6 +13686,8 @@ var MissedCallSchema2 = external_exports.enum([
|
|
|
13536
13686
|
"inbox",
|
|
13537
13687
|
"dismiss"
|
|
13538
13688
|
]);
|
|
13689
|
+
var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
13690
|
+
var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
|
|
13539
13691
|
var BrokerTuningSchema2 = external_exports.object({
|
|
13540
13692
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
13541
13693
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -13570,6 +13722,12 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
13570
13722
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
13571
13723
|
* write, 'hosted' on read (see store.ts). */
|
|
13572
13724
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
13725
|
+
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
13726
|
+
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
13727
|
+
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
13728
|
+
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
13729
|
+
* read (see store.ts). */
|
|
13730
|
+
talk: external_exports.boolean().optional(),
|
|
13573
13731
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
13574
13732
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
13575
13733
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -13596,7 +13754,6 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
13596
13754
|
});
|
|
13597
13755
|
var HistoryItemSchema2 = external_exports.object({
|
|
13598
13756
|
id: external_exports.string(),
|
|
13599
|
-
parentId: external_exports.string(),
|
|
13600
13757
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
13601
13758
|
initiator: external_exports.enum(["user", "agent"]),
|
|
13602
13759
|
title: external_exports.string(),
|
|
@@ -13755,6 +13912,24 @@ var QueueQuestionSchema2 = external_exports.object({
|
|
|
13755
13912
|
* gate at all. */
|
|
13756
13913
|
blocked: external_exports.boolean().default(false)
|
|
13757
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
|
+
});
|
|
13758
13933
|
var QueueItemSchema2 = external_exports.object({
|
|
13759
13934
|
id: external_exports.string(),
|
|
13760
13935
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -13767,6 +13942,9 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13767
13942
|
assignee: external_exports.string().nullable().default(null),
|
|
13768
13943
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
13769
13944
|
progress: external_exports.string().nullable().default(null),
|
|
13945
|
+
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
13946
|
+
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
13947
|
+
progressLine: external_exports.string().nullable().optional(),
|
|
13770
13948
|
reviewPending: external_exports.boolean().default(false),
|
|
13771
13949
|
dueAt: external_exports.string().nullable().default(null),
|
|
13772
13950
|
/** The Goal this one was opened under; null at the root. */
|
|
@@ -13779,11 +13957,20 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13779
13957
|
blocked: external_exports.boolean().default(false),
|
|
13780
13958
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
13781
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(),
|
|
13782
13964
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
13783
13965
|
repo: external_exports.string().nullable().optional(),
|
|
13784
13966
|
createdAt: external_exports.string(),
|
|
13785
|
-
updatedAt: external_exports.string().nullable().default(null)
|
|
13967
|
+
updatedAt: external_exports.string().nullable().default(null),
|
|
13968
|
+
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
13969
|
+
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
13970
|
+
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
13971
|
+
lastProgressAt: external_exports.string().nullable().optional()
|
|
13786
13972
|
});
|
|
13973
|
+
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
13787
13974
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
13788
13975
|
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
13789
13976
|
var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
|
|
@@ -14232,7 +14419,13 @@ function startSession(opts) {
|
|
|
14232
14419
|
...process.env,
|
|
14233
14420
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
14234
14421
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
14235
|
-
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
|
|
14422
|
+
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
|
|
14423
|
+
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
14424
|
+
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
14425
|
+
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
14426
|
+
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
14427
|
+
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
14428
|
+
PAIGY_HARNESS: "1"
|
|
14236
14429
|
},
|
|
14237
14430
|
stdio: ["pipe", "pipe", "pipe"]
|
|
14238
14431
|
});
|
|
@@ -14439,6 +14632,7 @@ async function ask(state, rail, input) {
|
|
|
14439
14632
|
waiting: "hard",
|
|
14440
14633
|
channel: input.call ? "call" : "notification"
|
|
14441
14634
|
});
|
|
14635
|
+
if ("recorded" in sent) return null;
|
|
14442
14636
|
const entryId = sent.requestEntryIds[0];
|
|
14443
14637
|
if (!entryId) return null;
|
|
14444
14638
|
(state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
|
|
@@ -14615,7 +14809,7 @@ function runHarness(opts) {
|
|
|
14615
14809
|
// src/update.ts
|
|
14616
14810
|
import { execFile } from "child_process";
|
|
14617
14811
|
import { createRequire } from "module";
|
|
14618
|
-
import { dirname as dirname3, join as
|
|
14812
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
14619
14813
|
import { promisify } from "util";
|
|
14620
14814
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
14621
14815
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -14642,7 +14836,7 @@ async function latestVersion() {
|
|
|
14642
14836
|
}
|
|
14643
14837
|
async function install(version) {
|
|
14644
14838
|
const bin = dirname3(process.execPath);
|
|
14645
|
-
await promisify(execFile)(
|
|
14839
|
+
await promisify(execFile)(join7(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
14646
14840
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
14647
14841
|
timeout: 5 * 6e4
|
|
14648
14842
|
});
|
|
@@ -14693,7 +14887,7 @@ function selfUpdater(deps) {
|
|
|
14693
14887
|
}
|
|
14694
14888
|
|
|
14695
14889
|
// src/host.ts
|
|
14696
|
-
var HOST_FILE =
|
|
14890
|
+
var HOST_FILE = join8(homedir7(), ".paigy", "host.json");
|
|
14697
14891
|
function readHostState() {
|
|
14698
14892
|
try {
|
|
14699
14893
|
const s = JSON.parse(readFileSync4(HOST_FILE, "utf8"));
|
|
@@ -14722,9 +14916,9 @@ async function slotHasWaitingWork(token, session) {
|
|
|
14722
14916
|
}
|
|
14723
14917
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
14724
14918
|
function wakeAction(live, now, serverSeenAt) {
|
|
14725
|
-
if (live) return "skip";
|
|
14726
|
-
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
|
|
14727
|
-
return "spawn";
|
|
14919
|
+
if (live) return { action: "skip", why: "run" };
|
|
14920
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
|
|
14921
|
+
return { action: "spawn", why: "nobody" };
|
|
14728
14922
|
}
|
|
14729
14923
|
function startHost(opts) {
|
|
14730
14924
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -14814,7 +15008,11 @@ function startHost(opts) {
|
|
|
14814
15008
|
const live = runs.get(key);
|
|
14815
15009
|
const tokenId = slotIdentity(slot).tokenId;
|
|
14816
15010
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
14817
|
-
|
|
15011
|
+
const decided = wakeAction(!!live, Date.now(), seenByServer);
|
|
15012
|
+
if (decided.action === "skip") {
|
|
15013
|
+
opts.log(decided.why === "run" ? `\u25CC ${slotName(slot) ?? slot} is running here \u2014 its pump types the work in; not spawning` : `\u25CC ${slotName(slot) ?? slot} is working somewhere this host cannot see (heartbeat) \u2014 not spawning`);
|
|
15014
|
+
return;
|
|
15015
|
+
}
|
|
14818
15016
|
const token = readToken(slot);
|
|
14819
15017
|
if (!token) return;
|
|
14820
15018
|
const owner = ownerSession(slot);
|
|
@@ -15194,9 +15392,9 @@ async function available(deps = {}) {
|
|
|
15194
15392
|
});
|
|
15195
15393
|
return [ollama, ...harnesses];
|
|
15196
15394
|
}
|
|
15197
|
-
function refusal(options,
|
|
15395
|
+
function refusal(options, asked2) {
|
|
15198
15396
|
const lines2 = [
|
|
15199
|
-
|
|
15397
|
+
asked2 ? `Can't judge with --via ${asked2}: ${options.find((o) => o.provider === asked2)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
|
|
15200
15398
|
"",
|
|
15201
15399
|
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
15202
15400
|
];
|
|
@@ -15220,10 +15418,10 @@ var RUNNERS = {
|
|
|
15220
15418
|
agy: { cli: "agy", argv: () => ["-p"] }
|
|
15221
15419
|
};
|
|
15222
15420
|
var RUN_TIMEOUT_MS = 5 * 6e4;
|
|
15223
|
-
async function judgeFor(
|
|
15421
|
+
async function judgeFor(asked2, deps = {}) {
|
|
15224
15422
|
const options = await available(deps);
|
|
15225
|
-
const chosen =
|
|
15226
|
-
if (!chosen?.ready) return { error: refusal(options,
|
|
15423
|
+
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
15424
|
+
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
15227
15425
|
if (chosen.provider === "ollama") {
|
|
15228
15426
|
const model = chosen.model;
|
|
15229
15427
|
return {
|
|
@@ -15244,9 +15442,9 @@ async function judgeFor(asked, deps = {}) {
|
|
|
15244
15442
|
};
|
|
15245
15443
|
}
|
|
15246
15444
|
const runner = RUNNERS[chosen.provider];
|
|
15247
|
-
if (!runner) return { error: refusal(options,
|
|
15445
|
+
if (!runner) return { error: refusal(options, asked2) };
|
|
15248
15446
|
const bin = which(runner.cli);
|
|
15249
|
-
if (!bin) return { error: refusal(options,
|
|
15447
|
+
if (!bin) return { error: refusal(options, asked2) };
|
|
15250
15448
|
return {
|
|
15251
15449
|
provider: chosen.provider,
|
|
15252
15450
|
model: null,
|
|
@@ -15552,7 +15750,7 @@ var win = null;
|
|
|
15552
15750
|
function log(message) {
|
|
15553
15751
|
win?.webContents.send("paigy:log", message);
|
|
15554
15752
|
}
|
|
15555
|
-
var ICON =
|
|
15753
|
+
var ICON = join9(here, "..", "assets", "icon.png");
|
|
15556
15754
|
function createWindow() {
|
|
15557
15755
|
win = new BrowserWindow({
|
|
15558
15756
|
// Sized for the working log, which is now the window's centerpiece; the layout is a
|
|
@@ -15564,9 +15762,9 @@ function createWindow() {
|
|
|
15564
15762
|
minHeight: 520,
|
|
15565
15763
|
icon: ICON,
|
|
15566
15764
|
// Windows/Linux window icon; macOS uses the dock icon below
|
|
15567
|
-
webPreferences: { preload:
|
|
15765
|
+
webPreferences: { preload: join9(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
|
|
15568
15766
|
});
|
|
15569
|
-
void win.loadFile(
|
|
15767
|
+
void win.loadFile(join9(here, "..", "renderer", "index.html"));
|
|
15570
15768
|
}
|
|
15571
15769
|
var host = null;
|
|
15572
15770
|
app.whenReady().then(() => {
|
|
@@ -15635,7 +15833,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
|
|
|
15635
15833
|
});
|
|
15636
15834
|
ipcMain.handle("paigy:agent-log", (_e, name) => {
|
|
15637
15835
|
try {
|
|
15638
|
-
const all = readFileSync5(
|
|
15836
|
+
const all = readFileSync5(join9(homedir9(), ".paigy", "host.log"), "utf8").split("\n");
|
|
15639
15837
|
const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
|
|
15640
15838
|
return mine.slice(-500).join("\n");
|
|
15641
15839
|
} catch {
|