@paigy/harness 0.3.14 → 0.3.15
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 +216 -121
- package/dist/main.js +212 -118
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -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
|
"The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
|
|
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}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
|
|
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
|
|
@@ -10026,19 +10035,9 @@ var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State,
|
|
|
10026
10035
|
var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
10027
10036
|
var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
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(),
|
|
@@ -10446,10 +10432,6 @@ var InboxItemSchema = external_exports.object({
|
|
|
10446
10432
|
options: external_exports.array(OptionSchema).optional(),
|
|
10447
10433
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
10448
10434
|
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
10435
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
10454
10436
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
10455
10437
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
@@ -10470,7 +10452,26 @@ var InboxItemSchema = external_exports.object({
|
|
|
10470
10452
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
10471
10453
|
* `createdAt`. */
|
|
10472
10454
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
10473
|
-
agenda
|
|
10455
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
10456
|
+
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
10457
|
+
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
10458
|
+
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
10459
|
+
* once, off the one read it already makes (`GET /api/inbox/:callId`).
|
|
10460
|
+
*
|
|
10461
|
+
* It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
|
|
10462
|
+
* every cycle, so a spine drawn from them would change length under the caller mid-call.
|
|
10463
|
+
*
|
|
10464
|
+
* `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
|
|
10465
|
+
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
10466
|
+
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
10467
|
+
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
10468
|
+
questions: external_exports.array(external_exports.object({
|
|
10469
|
+
id: external_exports.string(),
|
|
10470
|
+
entryId: external_exports.string(),
|
|
10471
|
+
title: external_exports.string(),
|
|
10472
|
+
state: external_exports.enum(["open", "answered"]),
|
|
10473
|
+
answer: external_exports.string().nullable()
|
|
10474
|
+
})).optional(),
|
|
10474
10475
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
10475
10476
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
10476
10477
|
* agent's suggestion, or a default silly name). */
|
|
@@ -10491,12 +10492,21 @@ var InboxItemSchema = external_exports.object({
|
|
|
10491
10492
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
10492
10493
|
error: external_exports.string().optional(),
|
|
10493
10494
|
clarifies: external_exports.string().optional(),
|
|
10494
|
-
/**
|
|
10495
|
-
*
|
|
10496
|
-
*
|
|
10497
|
-
*
|
|
10498
|
-
*
|
|
10499
|
-
|
|
10495
|
+
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
10496
|
+
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
10497
|
+
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
10498
|
+
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
10499
|
+
* `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
|
|
10500
|
+
* has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
|
|
10501
|
+
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
10502
|
+
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
10503
|
+
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
10504
|
+
ring: external_exports.object({
|
|
10505
|
+
declinedAt: external_exports.string().datetime(),
|
|
10506
|
+
anchorAt: external_exports.string().datetime(),
|
|
10507
|
+
nextRingAt: external_exports.string().datetime().nullable(),
|
|
10508
|
+
step: external_exports.number().int()
|
|
10509
|
+
}).optional(),
|
|
10500
10510
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
10501
10511
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
10502
10512
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
@@ -10561,6 +10571,8 @@ var MissedCallSchema = external_exports.enum([
|
|
|
10561
10571
|
"inbox",
|
|
10562
10572
|
"dismiss"
|
|
10563
10573
|
]);
|
|
10574
|
+
var clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
10575
|
+
var QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
10564
10576
|
var BrokerTuningSchema = external_exports.object({
|
|
10565
10577
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
10566
10578
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -10595,6 +10607,12 @@ var UserSettingsSchema = external_exports.object({
|
|
|
10595
10607
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
10596
10608
|
* write, 'hosted' on read (see store.ts). */
|
|
10597
10609
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
10610
|
+
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
10611
|
+
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
10612
|
+
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
10613
|
+
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
10614
|
+
* read (see store.ts). */
|
|
10615
|
+
talk: external_exports.boolean().optional(),
|
|
10598
10616
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
10599
10617
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
10600
10618
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -10792,6 +10810,9 @@ var QueueItemSchema = external_exports.object({
|
|
|
10792
10810
|
assignee: external_exports.string().nullable().default(null),
|
|
10793
10811
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
10794
10812
|
progress: external_exports.string().nullable().default(null),
|
|
10813
|
+
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
10814
|
+
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
10815
|
+
progressLine: external_exports.string().nullable().optional(),
|
|
10795
10816
|
reviewPending: external_exports.boolean().default(false),
|
|
10796
10817
|
dueAt: external_exports.string().nullable().default(null),
|
|
10797
10818
|
/** The Goal this one was opened under; null at the root. */
|
|
@@ -10807,8 +10828,13 @@ var QueueItemSchema = external_exports.object({
|
|
|
10807
10828
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
10808
10829
|
repo: external_exports.string().nullable().optional(),
|
|
10809
10830
|
createdAt: external_exports.string(),
|
|
10810
|
-
updatedAt: external_exports.string().nullable().default(null)
|
|
10831
|
+
updatedAt: external_exports.string().nullable().default(null),
|
|
10832
|
+
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
10833
|
+
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
10834
|
+
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
10835
|
+
lastProgressAt: external_exports.string().nullable().optional()
|
|
10811
10836
|
});
|
|
10837
|
+
var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
10812
10838
|
var NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
10813
10839
|
var NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
10814
10840
|
var NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
|
|
@@ -11109,6 +11135,8 @@ var ApiError = class extends Error {
|
|
|
11109
11135
|
this.body = body;
|
|
11110
11136
|
}
|
|
11111
11137
|
};
|
|
11138
|
+
var tokenOverride = null;
|
|
11139
|
+
var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
11112
11140
|
async function fail(what, res) {
|
|
11113
11141
|
throw new ApiError(what, res.status, await res.text());
|
|
11114
11142
|
}
|
|
@@ -11145,8 +11173,6 @@ async function updateGoal(goalId, input, opts = {}) {
|
|
|
11145
11173
|
return await res.json();
|
|
11146
11174
|
}
|
|
11147
11175
|
var AWAIT_WINDOW_MS = 45e3;
|
|
11148
|
-
var tokenOverride = null;
|
|
11149
|
-
var authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
11150
11176
|
async function whoAmI(opts = {}) {
|
|
11151
11177
|
try {
|
|
11152
11178
|
const res = await reach(`${BACKEND_URL}/api/identity`, {
|
|
@@ -11233,6 +11259,32 @@ async function dismissTriage(proposalId, opts = {}) {
|
|
|
11233
11259
|
if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
|
|
11234
11260
|
return await res.json();
|
|
11235
11261
|
}
|
|
11262
|
+
function repoFromRemote(url) {
|
|
11263
|
+
const said = (url ?? "").trim();
|
|
11264
|
+
if (!said) return null;
|
|
11265
|
+
const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
11266
|
+
const parts = path.split("/").filter(Boolean);
|
|
11267
|
+
if (parts.length < 2) return null;
|
|
11268
|
+
const [owner, name] = parts.slice(-2);
|
|
11269
|
+
if (!owner || !name) return null;
|
|
11270
|
+
return `${owner}/${name}`.toLowerCase();
|
|
11271
|
+
}
|
|
11272
|
+
var asked;
|
|
11273
|
+
function currentRepo(cwd = process.cwd()) {
|
|
11274
|
+
if (asked !== void 0) return asked;
|
|
11275
|
+
try {
|
|
11276
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
11277
|
+
cwd,
|
|
11278
|
+
encoding: "utf8",
|
|
11279
|
+
timeout: 2e3,
|
|
11280
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
11281
|
+
});
|
|
11282
|
+
asked = repoFromRemote(url);
|
|
11283
|
+
} catch {
|
|
11284
|
+
asked = null;
|
|
11285
|
+
}
|
|
11286
|
+
return asked;
|
|
11287
|
+
}
|
|
11236
11288
|
async function contact(input, opts = {}) {
|
|
11237
11289
|
const parsed = ContactSchema.parse(input);
|
|
11238
11290
|
opts.signal?.throwIfAborted();
|
|
@@ -11246,7 +11298,11 @@ async function contact(input, opts = {}) {
|
|
|
11246
11298
|
const reqAsks = parsed.asks.map((a) => ({
|
|
11247
11299
|
id: a.id,
|
|
11248
11300
|
parentId: a.parentId,
|
|
11249
|
-
|
|
11301
|
+
// Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
|
|
11302
|
+
// work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
|
|
11303
|
+
// still wins — normalised the same way — because a caller that names one knows something
|
|
11304
|
+
// this cannot see.
|
|
11305
|
+
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
11250
11306
|
ask: a.ask,
|
|
11251
11307
|
options: a.options ?? []
|
|
11252
11308
|
}));
|
|
@@ -11268,10 +11324,12 @@ async function contact(input, opts = {}) {
|
|
|
11268
11324
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
11269
11325
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
11270
11326
|
}
|
|
11327
|
+
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId);
|
|
11271
11328
|
const read = async (signal2) => {
|
|
11272
11329
|
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
11273
11330
|
if (!res.ok) await fail("read_delivery", res);
|
|
11274
|
-
|
|
11331
|
+
const one = await res.json();
|
|
11332
|
+
return all.length > 1 ? { ...one, deliveries: all } : one;
|
|
11275
11333
|
};
|
|
11276
11334
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
11277
11335
|
if (opts.waits === false) return read(opts.signal);
|
|
@@ -11375,7 +11433,8 @@ function subscribeRealtime(args) {
|
|
|
11375
11433
|
};
|
|
11376
11434
|
}
|
|
11377
11435
|
async function subscribeWake(onNudge, opts = {}) {
|
|
11378
|
-
const
|
|
11436
|
+
const { release = true, ...client } = opts;
|
|
11437
|
+
const cfg = await registerDelivery("self_hosted", client);
|
|
11379
11438
|
const { url, anonKey } = cfg.realtime;
|
|
11380
11439
|
const channel = wakeChannel(cfg.tokenId);
|
|
11381
11440
|
const ch = subscribeRealtime({
|
|
@@ -11390,7 +11449,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
11390
11449
|
channel,
|
|
11391
11450
|
close: async () => {
|
|
11392
11451
|
ch.close();
|
|
11393
|
-
await registerDelivery("poll",
|
|
11452
|
+
if (release) await registerDelivery("poll", client).catch(() => {
|
|
11394
11453
|
});
|
|
11395
11454
|
}
|
|
11396
11455
|
};
|
|
@@ -12881,6 +12940,23 @@ var zodToJsonSchema2 = (schema, options) => {
|
|
|
12881
12940
|
// ../../packages/schema/dist/index.js
|
|
12882
12941
|
var OPTIONS_MIN2 = 2;
|
|
12883
12942
|
var OPTIONS_MAX2 = 6;
|
|
12943
|
+
var OptionSchema2 = external_exports.object({
|
|
12944
|
+
id: external_exports.string(),
|
|
12945
|
+
label: external_exports.string(),
|
|
12946
|
+
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(),
|
|
12947
|
+
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
12948
|
+
// the constraints below are what an agent reads when deciding to use these.
|
|
12949
|
+
html: external_exports.string().max(16384).describe(
|
|
12950
|
+
"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."
|
|
12951
|
+
).optional(),
|
|
12952
|
+
image: external_exports.string().url().describe(
|
|
12953
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
12954
|
+
).optional()
|
|
12955
|
+
});
|
|
12956
|
+
var OptionInputSchema2 = OptionSchema2.omit({ id: true }).extend({
|
|
12957
|
+
label: external_exports.string().trim().min(1).max(1e3)
|
|
12958
|
+
}).strict();
|
|
12959
|
+
var NIGHT2 = { from: 23, to: 7 };
|
|
12884
12960
|
function draft20202(node) {
|
|
12885
12961
|
if (Array.isArray(node)) return node.map(draft20202);
|
|
12886
12962
|
if (node && typeof node === "object") {
|
|
@@ -12912,26 +12988,21 @@ function mcpInputSchema2(s) {
|
|
|
12912
12988
|
}
|
|
12913
12989
|
var AskInputSchema2 = external_exports.object({
|
|
12914
12990
|
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("
|
|
12991
|
+
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
12992
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
12917
12993
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
12918
12994
|
"The question, and only what is needed to answer it. News, progress and findings are their own contact \u2014 a call contact JOINS a call already happening, so several arrive as one call. Do not bundle: a DecisionNeed is settled only by an answer in the shape this ask declares, so someone who answers the part that interested them settles nothing and is asked again."
|
|
12919
12995
|
),
|
|
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()
|
|
12996
|
+
options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
|
|
12926
12997
|
}).strict();
|
|
12927
12998
|
var StartContactSchema2 = external_exports.object({
|
|
12928
|
-
asks: external_exports.array(AskInputSchema2).min(1).describe("The
|
|
12999
|
+
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
13000
|
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
12930
13001
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
12931
13002
|
}).strict();
|
|
12932
13003
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
12933
13004
|
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'.
|
|
13005
|
+
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'id', 'parentId', 'repo'), plus channel:'notification'|'call', and waiting:'none'|'hard'. An ask that names a Goal (parentId) goes onto that Goal; one that names none is placed in the person's tree by Paigy. Notification returns immediately; collect durable answers with check_replies. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Never bundle multiple questions into a single 'ask' string; pass them as separate objects in the 'asks' array.";
|
|
12935
13006
|
var CreateGoalSchema2 = external_exports.object({
|
|
12936
13007
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
12937
13008
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -12939,10 +13010,6 @@ var CreateGoalSchema2 = external_exports.object({
|
|
|
12939
13010
|
title: external_exports.string().trim().min(1).max(80).optional(),
|
|
12940
13011
|
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
12941
13012
|
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
13013
|
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
12947
13014
|
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
12948
13015
|
* worked"). It could not have been: this door took no parent, so the only route was
|
|
@@ -12986,19 +13053,9 @@ var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State
|
|
|
12986
13053
|
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
|
|
12987
13054
|
var CHECK_REPLIES_DESCRIPTION2 = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
|
|
12988
13055
|
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
13056
|
var AGENT_TOOLS2 = [
|
|
12998
13057
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
12999
13058
|
{ 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
13059
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
|
|
13003
13060
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
|
|
13004
13061
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
|
|
@@ -13047,19 +13104,6 @@ var TransformSchema2 = external_exports.enum([
|
|
|
13047
13104
|
"summarize"
|
|
13048
13105
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
13049
13106
|
]);
|
|
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
13107
|
var VisualSchema2 = external_exports.object({
|
|
13064
13108
|
url: external_exports.string().url(),
|
|
13065
13109
|
label: external_exports.string().optional()
|
|
@@ -13113,7 +13157,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
13113
13157
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
13114
13158
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
13115
13159
|
context: ContextSchema2.optional(),
|
|
13116
|
-
options: external_exports.array(
|
|
13160
|
+
options: external_exports.array(OptionInputSchema2).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
|
|
13117
13161
|
"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
13162
|
),
|
|
13119
13163
|
points: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
@@ -13307,7 +13351,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
13307
13351
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
13308
13352
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
13309
13353
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
13310
|
-
* re-orient via
|
|
13354
|
+
* re-orient via check_replies). */
|
|
13311
13355
|
external_exports.object({
|
|
13312
13356
|
type: external_exports.literal("superseded"),
|
|
13313
13357
|
parentId: external_exports.string(),
|
|
@@ -13421,10 +13465,6 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13421
13465
|
options: external_exports.array(OptionSchema2).optional(),
|
|
13422
13466
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
13423
13467
|
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
13468
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
13429
13469
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
13430
13470
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
@@ -13445,7 +13485,26 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13445
13485
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
13446
13486
|
* `createdAt`. */
|
|
13447
13487
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
13448
|
-
agenda
|
|
13488
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
13489
|
+
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
13490
|
+
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
13491
|
+
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
13492
|
+
* once, off the one read it already makes (`GET /api/inbox/:callId`).
|
|
13493
|
+
*
|
|
13494
|
+
* It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
|
|
13495
|
+
* every cycle, so a spine drawn from them would change length under the caller mid-call.
|
|
13496
|
+
*
|
|
13497
|
+
* `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
|
|
13498
|
+
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
13499
|
+
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
13500
|
+
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
13501
|
+
questions: external_exports.array(external_exports.object({
|
|
13502
|
+
id: external_exports.string(),
|
|
13503
|
+
entryId: external_exports.string(),
|
|
13504
|
+
title: external_exports.string(),
|
|
13505
|
+
state: external_exports.enum(["open", "answered"]),
|
|
13506
|
+
answer: external_exports.string().nullable()
|
|
13507
|
+
})).optional(),
|
|
13449
13508
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
13450
13509
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
13451
13510
|
* agent's suggestion, or a default silly name). */
|
|
@@ -13466,12 +13525,21 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
13466
13525
|
* client may still flag a stall by age. Drives the inbox error badge + Retry. */
|
|
13467
13526
|
error: external_exports.string().optional(),
|
|
13468
13527
|
clarifies: external_exports.string().optional(),
|
|
13469
|
-
/**
|
|
13470
|
-
*
|
|
13471
|
-
*
|
|
13472
|
-
*
|
|
13473
|
-
*
|
|
13474
|
-
|
|
13528
|
+
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
13529
|
+
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
13530
|
+
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
13531
|
+
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
13532
|
+
* `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
|
|
13533
|
+
* has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
|
|
13534
|
+
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
13535
|
+
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
13536
|
+
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
13537
|
+
ring: external_exports.object({
|
|
13538
|
+
declinedAt: external_exports.string().datetime(),
|
|
13539
|
+
anchorAt: external_exports.string().datetime(),
|
|
13540
|
+
nextRingAt: external_exports.string().datetime().nullable(),
|
|
13541
|
+
step: external_exports.number().int()
|
|
13542
|
+
}).optional(),
|
|
13475
13543
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
13476
13544
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
13477
13545
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
@@ -13536,6 +13604,8 @@ var MissedCallSchema2 = external_exports.enum([
|
|
|
13536
13604
|
"inbox",
|
|
13537
13605
|
"dismiss"
|
|
13538
13606
|
]);
|
|
13607
|
+
var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
13608
|
+
var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
|
|
13539
13609
|
var BrokerTuningSchema2 = external_exports.object({
|
|
13540
13610
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
13541
13611
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -13570,6 +13640,12 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
13570
13640
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
13571
13641
|
* write, 'hosted' on read (see store.ts). */
|
|
13572
13642
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
13643
|
+
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
13644
|
+
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
13645
|
+
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
13646
|
+
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
13647
|
+
* read (see store.ts). */
|
|
13648
|
+
talk: external_exports.boolean().optional(),
|
|
13573
13649
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
13574
13650
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
13575
13651
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -13767,6 +13843,9 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13767
13843
|
assignee: external_exports.string().nullable().default(null),
|
|
13768
13844
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
13769
13845
|
progress: external_exports.string().nullable().default(null),
|
|
13846
|
+
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
13847
|
+
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
13848
|
+
progressLine: external_exports.string().nullable().optional(),
|
|
13770
13849
|
reviewPending: external_exports.boolean().default(false),
|
|
13771
13850
|
dueAt: external_exports.string().nullable().default(null),
|
|
13772
13851
|
/** The Goal this one was opened under; null at the root. */
|
|
@@ -13782,8 +13861,13 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
13782
13861
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
13783
13862
|
repo: external_exports.string().nullable().optional(),
|
|
13784
13863
|
createdAt: external_exports.string(),
|
|
13785
|
-
updatedAt: external_exports.string().nullable().default(null)
|
|
13864
|
+
updatedAt: external_exports.string().nullable().default(null),
|
|
13865
|
+
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
13866
|
+
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
13867
|
+
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
13868
|
+
lastProgressAt: external_exports.string().nullable().optional()
|
|
13786
13869
|
});
|
|
13870
|
+
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
13787
13871
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
13788
13872
|
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
13789
13873
|
var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
|
|
@@ -14232,7 +14316,13 @@ function startSession(opts) {
|
|
|
14232
14316
|
...process.env,
|
|
14233
14317
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
14234
14318
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
14235
|
-
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
|
|
14319
|
+
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
|
|
14320
|
+
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
14321
|
+
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
14322
|
+
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
14323
|
+
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
14324
|
+
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
14325
|
+
PAIGY_HARNESS: "1"
|
|
14236
14326
|
},
|
|
14237
14327
|
stdio: ["pipe", "pipe", "pipe"]
|
|
14238
14328
|
});
|
|
@@ -14722,9 +14812,9 @@ async function slotHasWaitingWork(token, session) {
|
|
|
14722
14812
|
}
|
|
14723
14813
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
14724
14814
|
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";
|
|
14815
|
+
if (live) return { action: "skip", why: "run" };
|
|
14816
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
|
|
14817
|
+
return { action: "spawn", why: "nobody" };
|
|
14728
14818
|
}
|
|
14729
14819
|
function startHost(opts) {
|
|
14730
14820
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -14814,7 +14904,11 @@ function startHost(opts) {
|
|
|
14814
14904
|
const live = runs.get(key);
|
|
14815
14905
|
const tokenId = slotIdentity(slot).tokenId;
|
|
14816
14906
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
14817
|
-
|
|
14907
|
+
const decided = wakeAction(!!live, Date.now(), seenByServer);
|
|
14908
|
+
if (decided.action === "skip") {
|
|
14909
|
+
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`);
|
|
14910
|
+
return;
|
|
14911
|
+
}
|
|
14818
14912
|
const token = readToken(slot);
|
|
14819
14913
|
if (!token) return;
|
|
14820
14914
|
const owner = ownerSession(slot);
|
|
@@ -15194,9 +15288,9 @@ async function available(deps = {}) {
|
|
|
15194
15288
|
});
|
|
15195
15289
|
return [ollama, ...harnesses];
|
|
15196
15290
|
}
|
|
15197
|
-
function refusal(options,
|
|
15291
|
+
function refusal(options, asked2) {
|
|
15198
15292
|
const lines2 = [
|
|
15199
|
-
|
|
15293
|
+
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
15294
|
"",
|
|
15201
15295
|
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
15202
15296
|
];
|
|
@@ -15220,10 +15314,10 @@ var RUNNERS = {
|
|
|
15220
15314
|
agy: { cli: "agy", argv: () => ["-p"] }
|
|
15221
15315
|
};
|
|
15222
15316
|
var RUN_TIMEOUT_MS = 5 * 6e4;
|
|
15223
|
-
async function judgeFor(
|
|
15317
|
+
async function judgeFor(asked2, deps = {}) {
|
|
15224
15318
|
const options = await available(deps);
|
|
15225
|
-
const chosen =
|
|
15226
|
-
if (!chosen?.ready) return { error: refusal(options,
|
|
15319
|
+
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
15320
|
+
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
15227
15321
|
if (chosen.provider === "ollama") {
|
|
15228
15322
|
const model = chosen.model;
|
|
15229
15323
|
return {
|
|
@@ -15244,9 +15338,9 @@ async function judgeFor(asked, deps = {}) {
|
|
|
15244
15338
|
};
|
|
15245
15339
|
}
|
|
15246
15340
|
const runner = RUNNERS[chosen.provider];
|
|
15247
|
-
if (!runner) return { error: refusal(options,
|
|
15341
|
+
if (!runner) return { error: refusal(options, asked2) };
|
|
15248
15342
|
const bin = which(runner.cli);
|
|
15249
|
-
if (!bin) return { error: refusal(options,
|
|
15343
|
+
if (!bin) return { error: refusal(options, asked2) };
|
|
15250
15344
|
return {
|
|
15251
15345
|
provider: chosen.provider,
|
|
15252
15346
|
model: null,
|