@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/cli.js
CHANGED
|
@@ -22755,6 +22755,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
22755
22755
|
import { closeSync, existsSync as existsSync2, mkdirSync, openSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
22756
22756
|
import { homedir as homedir22 } from "os";
|
|
22757
22757
|
import { join as join22 } from "path";
|
|
22758
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
22758
22759
|
async function proxy() {
|
|
22759
22760
|
if (!PROXY_ENV.some((k) => process.env[k])) return void 0;
|
|
22760
22761
|
return agent ??= new (await Promise.resolve().then(() => __toESM(require_undici(), 1))).EnvHttpProxyAgent();
|
|
@@ -23786,6 +23787,9 @@ function ensureAuthed(res) {
|
|
|
23786
23787
|
if (res.status === 401) throw new UnpairedError();
|
|
23787
23788
|
return res;
|
|
23788
23789
|
}
|
|
23790
|
+
function overrideToken(secret) {
|
|
23791
|
+
tokenOverride = secret;
|
|
23792
|
+
}
|
|
23789
23793
|
async function fail(what, res) {
|
|
23790
23794
|
throw new ApiError(what, res.status, await res.text());
|
|
23791
23795
|
}
|
|
@@ -23821,9 +23825,6 @@ async function updateGoal(goalId, input, opts = {}) {
|
|
|
23821
23825
|
if (!res.ok) await fail("update_goal", res);
|
|
23822
23826
|
return await res.json();
|
|
23823
23827
|
}
|
|
23824
|
-
function overrideToken(secret) {
|
|
23825
|
-
tokenOverride = secret;
|
|
23826
|
-
}
|
|
23827
23828
|
async function hatch(name, voice = null) {
|
|
23828
23829
|
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
|
|
23829
23830
|
method: "POST",
|
|
@@ -23904,6 +23905,31 @@ async function acceptTriage(proposalId, body, opts = {}) {
|
|
|
23904
23905
|
if (!res.ok) throw new Error(`accept_triage failed: ${res.status} ${await res.text()}`);
|
|
23905
23906
|
return await res.json();
|
|
23906
23907
|
}
|
|
23908
|
+
function repoFromRemote(url) {
|
|
23909
|
+
const said = (url ?? "").trim();
|
|
23910
|
+
if (!said) return null;
|
|
23911
|
+
const path = said.replace(/^[a-z+]+:\/\/[^/]+\//i, "").replace(/^[^@]+@[^:]+:/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
23912
|
+
const parts = path.split("/").filter(Boolean);
|
|
23913
|
+
if (parts.length < 2) return null;
|
|
23914
|
+
const [owner, name] = parts.slice(-2);
|
|
23915
|
+
if (!owner || !name) return null;
|
|
23916
|
+
return `${owner}/${name}`.toLowerCase();
|
|
23917
|
+
}
|
|
23918
|
+
function currentRepo(cwd = process.cwd()) {
|
|
23919
|
+
if (asked !== void 0) return asked;
|
|
23920
|
+
try {
|
|
23921
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
23922
|
+
cwd,
|
|
23923
|
+
encoding: "utf8",
|
|
23924
|
+
timeout: 2e3,
|
|
23925
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
23926
|
+
});
|
|
23927
|
+
asked = repoFromRemote(url);
|
|
23928
|
+
} catch {
|
|
23929
|
+
asked = null;
|
|
23930
|
+
}
|
|
23931
|
+
return asked;
|
|
23932
|
+
}
|
|
23907
23933
|
async function contact(input, opts = {}) {
|
|
23908
23934
|
const parsed = ContactSchema.parse(input);
|
|
23909
23935
|
opts.signal?.throwIfAborted();
|
|
@@ -23911,13 +23937,18 @@ async function contact(input, opts = {}) {
|
|
|
23911
23937
|
const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
|
|
23912
23938
|
let deliveryId;
|
|
23913
23939
|
let deliveries = [];
|
|
23940
|
+
let recorded = [];
|
|
23914
23941
|
if ("deliveryId" in parsed) {
|
|
23915
23942
|
deliveryId = parsed.deliveryId;
|
|
23916
23943
|
} else {
|
|
23917
23944
|
const reqAsks = parsed.asks.map((a) => ({
|
|
23918
23945
|
id: a.id,
|
|
23919
23946
|
parentId: a.parentId,
|
|
23920
|
-
|
|
23947
|
+
// Derived, not asked for (#2315): the agent's own remote, in one spelling, so a project's
|
|
23948
|
+
// work does not grow three trees for "paigy", "mauurda/paigy" and "Paigy". An explicit one
|
|
23949
|
+
// still wins — normalised the same way — because a caller that names one knows something
|
|
23950
|
+
// this cannot see.
|
|
23951
|
+
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
23921
23952
|
ask: a.ask,
|
|
23922
23953
|
options: a.options ?? []
|
|
23923
23954
|
}));
|
|
@@ -23935,14 +23966,24 @@ async function contact(input, opts = {}) {
|
|
|
23935
23966
|
if (!res.ok) await fail("batch contact", res);
|
|
23936
23967
|
const receipt = await res.json();
|
|
23937
23968
|
deliveries = receipt.deliveries ?? [];
|
|
23969
|
+
recorded = receipt.recorded ?? [];
|
|
23970
|
+
if (deliveries.length === 0 && recorded.length) return { sent: false, recorded, message: NOT_SENT };
|
|
23938
23971
|
if (deliveries.length === 0) throw new Error("Goal contact returned no Delivery identities");
|
|
23939
23972
|
deliveryId = deliveries[0]?.deliveryId ?? "";
|
|
23940
23973
|
if (!deliveryId) throw new Error("Goal contact returned no valid Delivery identity");
|
|
23941
23974
|
}
|
|
23975
|
+
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
|
|
23976
|
+
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
23942
23977
|
const read = async (signal2) => {
|
|
23943
23978
|
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
23944
23979
|
if (!res.ok) await fail("read_delivery", res);
|
|
23945
|
-
|
|
23980
|
+
const one = await res.json();
|
|
23981
|
+
return {
|
|
23982
|
+
...one,
|
|
23983
|
+
...all.length > 1 ? { deliveries: all } : {},
|
|
23984
|
+
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
23985
|
+
...joinedCard ? { joinedCard: true } : {}
|
|
23986
|
+
};
|
|
23946
23987
|
};
|
|
23947
23988
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
23948
23989
|
if (opts.waits === false) return read(opts.signal);
|
|
@@ -23970,45 +24011,105 @@ async function checkReplies(opts = {}) {
|
|
|
23970
24011
|
return await res.json();
|
|
23971
24012
|
}
|
|
23972
24013
|
function frame(line, topic, event, payload) {
|
|
24014
|
+
const ref = String(++line.ref);
|
|
23973
24015
|
try {
|
|
23974
|
-
line.ws?.send(JSON.stringify({ topic, event, payload, ref
|
|
24016
|
+
line.ws?.send(JSON.stringify({ topic, event, payload, ref }));
|
|
23975
24017
|
} catch {
|
|
23976
24018
|
}
|
|
24019
|
+
return ref;
|
|
23977
24020
|
}
|
|
23978
24021
|
function stop(line) {
|
|
23979
24022
|
line.open = false;
|
|
24023
|
+
line.beat = void 0;
|
|
23980
24024
|
clearInterval(line.heart);
|
|
23981
24025
|
clearTimeout(line.retry);
|
|
24026
|
+
for (const t of line.topics.values()) {
|
|
24027
|
+
clearTimeout(t.timer);
|
|
24028
|
+
t.joined = false;
|
|
24029
|
+
t.ref = void 0;
|
|
24030
|
+
}
|
|
24031
|
+
}
|
|
24032
|
+
function join3(line, name, t) {
|
|
24033
|
+
clearTimeout(t.timer);
|
|
24034
|
+
t.joined = false;
|
|
24035
|
+
t.ref = frame(line, `realtime:${name}`, "phx_join", JOIN);
|
|
24036
|
+
t.timer = setTimeout(() => rejoin(line, name, t), JOIN_TIMEOUT_MS);
|
|
24037
|
+
}
|
|
24038
|
+
function rejoin(line, name, t) {
|
|
24039
|
+
clearTimeout(t.timer);
|
|
24040
|
+
t.joined = false;
|
|
24041
|
+
t.ref = void 0;
|
|
24042
|
+
if (!line.open || line.topics.get(name) !== t) return;
|
|
24043
|
+
t.timer = setTimeout(() => {
|
|
24044
|
+
if (line.open && line.topics.get(name) === t) join3(line, name, t);
|
|
24045
|
+
}, RETRY_MS[Math.min(t.attempt++, RETRY_MS.length - 1)]);
|
|
24046
|
+
}
|
|
24047
|
+
function joined(t) {
|
|
24048
|
+
clearTimeout(t.timer);
|
|
24049
|
+
t.joined = true;
|
|
24050
|
+
t.attempt = 0;
|
|
24051
|
+
for (const l of t.listeners) l.onJoined?.(++l.joins);
|
|
23982
24052
|
}
|
|
23983
24053
|
function connect(line) {
|
|
23984
24054
|
const base = line.url.replace(/^http/, "ws").replace(/\/+$/, "");
|
|
23985
24055
|
const ws = line.ws = new import_undici.WebSocket(`${base}/realtime/v1/websocket?apikey=${encodeURIComponent(line.anonKey)}&vsn=1.0.0`);
|
|
24056
|
+
let gone = false;
|
|
24057
|
+
const again = () => {
|
|
24058
|
+
if (gone) return;
|
|
24059
|
+
gone = true;
|
|
24060
|
+
stop(line);
|
|
24061
|
+
if (!line.topics.size) return;
|
|
24062
|
+
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
24063
|
+
};
|
|
23986
24064
|
ws.addEventListener("open", () => {
|
|
24065
|
+
if (gone) return;
|
|
23987
24066
|
line.open = true;
|
|
23988
24067
|
line.attempt = 0;
|
|
23989
|
-
for (const
|
|
23990
|
-
line.heart = setInterval(() =>
|
|
24068
|
+
for (const [name, t] of line.topics) join3(line, name, t);
|
|
24069
|
+
line.heart = setInterval(() => {
|
|
24070
|
+
if (line.beat) {
|
|
24071
|
+
try {
|
|
24072
|
+
ws.close();
|
|
24073
|
+
} catch {
|
|
24074
|
+
}
|
|
24075
|
+
again();
|
|
24076
|
+
return;
|
|
24077
|
+
}
|
|
24078
|
+
line.beat = frame(line, "phoenix", "heartbeat", {});
|
|
24079
|
+
}, HEARTBEAT_MS);
|
|
23991
24080
|
});
|
|
23992
24081
|
ws.addEventListener("message", (e) => {
|
|
24082
|
+
if (gone) return;
|
|
23993
24083
|
let m;
|
|
23994
24084
|
try {
|
|
23995
24085
|
m = JSON.parse(String(e.data));
|
|
23996
24086
|
} catch {
|
|
23997
24087
|
return;
|
|
23998
24088
|
}
|
|
23999
|
-
if (m.
|
|
24000
|
-
|
|
24089
|
+
if (m.topic === "phoenix") {
|
|
24090
|
+
if (m.event === "phx_reply" && m.ref === line.beat) line.beat = void 0;
|
|
24091
|
+
return;
|
|
24092
|
+
}
|
|
24093
|
+
if (!m.topic?.startsWith("realtime:")) return;
|
|
24094
|
+
const name = m.topic.slice("realtime:".length);
|
|
24095
|
+
const t = line.topics.get(name);
|
|
24096
|
+
if (!t) return;
|
|
24097
|
+
if (m.event === "phx_reply") {
|
|
24098
|
+
if (!t.ref || m.ref !== t.ref) return;
|
|
24099
|
+
if (m.payload?.status === "ok") joined(t);
|
|
24100
|
+
else rejoin(line, name, t);
|
|
24101
|
+
return;
|
|
24102
|
+
}
|
|
24103
|
+
if (m.event === "phx_error" || m.event === "phx_close") {
|
|
24104
|
+
if (m.ref && m.ref !== t.ref) return;
|
|
24105
|
+
rejoin(line, name, t);
|
|
24106
|
+
return;
|
|
24107
|
+
}
|
|
24108
|
+
if (m.event !== "broadcast") return;
|
|
24109
|
+
for (const l of t.listeners) {
|
|
24001
24110
|
if (m.payload?.event === l.event) l.onMessage(m.payload.payload);
|
|
24002
24111
|
}
|
|
24003
24112
|
});
|
|
24004
|
-
let gone = false;
|
|
24005
|
-
const again = () => {
|
|
24006
|
-
if (gone) return;
|
|
24007
|
-
gone = true;
|
|
24008
|
-
stop(line);
|
|
24009
|
-
if (!line.topics.size) return;
|
|
24010
|
-
line.retry = setTimeout(() => connect(line), RETRY_MS[Math.min(line.attempt++, RETRY_MS.length - 1)]);
|
|
24011
|
-
};
|
|
24012
24113
|
ws.addEventListener("close", again);
|
|
24013
24114
|
ws.addEventListener("error", again);
|
|
24014
24115
|
}
|
|
@@ -24016,17 +24117,22 @@ function subscribeRealtime(args) {
|
|
|
24016
24117
|
const key = `${args.url} ${args.anonKey}`;
|
|
24017
24118
|
let line = lines.get(key);
|
|
24018
24119
|
if (!line) lines.set(key, line = { url: args.url, anonKey: args.anonKey, open: false, attempt: 0, ref: 0, topics: /* @__PURE__ */ new Map() });
|
|
24019
|
-
let
|
|
24020
|
-
if (!
|
|
24021
|
-
line.topics.set(args.topic,
|
|
24022
|
-
if (line.open)
|
|
24023
|
-
}
|
|
24024
|
-
const
|
|
24025
|
-
|
|
24120
|
+
let t = line.topics.get(args.topic);
|
|
24121
|
+
if (!t) {
|
|
24122
|
+
line.topics.set(args.topic, t = { listeners: /* @__PURE__ */ new Set(), joined: false, attempt: 0 });
|
|
24123
|
+
if (line.open) join3(line, args.topic, t);
|
|
24124
|
+
}
|
|
24125
|
+
const topic = t;
|
|
24126
|
+
const me = { event: args.event, onMessage: args.onMessage, onJoined: args.onJoined, joins: 0 };
|
|
24127
|
+
topic.listeners.add(me);
|
|
24128
|
+
if (topic.joined && me.onJoined) queueMicrotask(() => {
|
|
24129
|
+
if (topic.joined && topic.listeners.has(me)) me.onJoined?.(++me.joins);
|
|
24130
|
+
});
|
|
24026
24131
|
if (!line.ws) connect(line);
|
|
24027
24132
|
return {
|
|
24028
24133
|
close: () => {
|
|
24029
|
-
if (!
|
|
24134
|
+
if (!topic.listeners.delete(me) || topic.listeners.size) return;
|
|
24135
|
+
clearTimeout(topic.timer);
|
|
24030
24136
|
line.topics.delete(args.topic);
|
|
24031
24137
|
if (line.topics.size) {
|
|
24032
24138
|
if (line.open) frame(line, `realtime:${args.topic}`, "phx_leave", {});
|
|
@@ -24042,7 +24148,8 @@ function subscribeRealtime(args) {
|
|
|
24042
24148
|
};
|
|
24043
24149
|
}
|
|
24044
24150
|
async function subscribeWake(onNudge, opts = {}) {
|
|
24045
|
-
const
|
|
24151
|
+
const { release = true, onJoined, ...client } = opts;
|
|
24152
|
+
const cfg = await registerDelivery("self_hosted", client);
|
|
24046
24153
|
const { url, anonKey } = cfg.realtime;
|
|
24047
24154
|
const channel = wakeChannel(cfg.tokenId);
|
|
24048
24155
|
const ch = subscribeRealtime({
|
|
@@ -24050,24 +24157,26 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
24050
24157
|
anonKey,
|
|
24051
24158
|
topic: channel,
|
|
24052
24159
|
event: WAKE_EVENT,
|
|
24053
|
-
onMessage: (payload) => onNudge(payload ?? {})
|
|
24160
|
+
onMessage: (payload) => onNudge(payload ?? {}),
|
|
24161
|
+
onJoined
|
|
24054
24162
|
});
|
|
24055
24163
|
return {
|
|
24056
24164
|
tokenId: cfg.tokenId,
|
|
24057
24165
|
channel,
|
|
24058
24166
|
close: async () => {
|
|
24059
24167
|
ch.close();
|
|
24060
|
-
await registerDelivery("poll",
|
|
24168
|
+
if (release) await registerDelivery("poll", client).catch(() => {
|
|
24061
24169
|
});
|
|
24062
24170
|
}
|
|
24063
24171
|
};
|
|
24064
24172
|
}
|
|
24065
|
-
var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema,
|
|
24173
|
+
var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24066
24174
|
var init_dist = __esm({
|
|
24067
24175
|
"../../packages/sdk/dist/index.js"() {
|
|
24068
24176
|
"use strict";
|
|
24069
24177
|
init_zod();
|
|
24070
24178
|
init_zod();
|
|
24179
|
+
init_zod();
|
|
24071
24180
|
init_v3();
|
|
24072
24181
|
init_v3();
|
|
24073
24182
|
init_v3();
|
|
@@ -24452,28 +24561,40 @@ var init_dist = __esm({
|
|
|
24452
24561
|
};
|
|
24453
24562
|
OPTIONS_MIN = 2;
|
|
24454
24563
|
OPTIONS_MAX = 6;
|
|
24564
|
+
OptionSchema = external_exports.object({
|
|
24565
|
+
id: external_exports.string(),
|
|
24566
|
+
label: external_exports.string(),
|
|
24567
|
+
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(),
|
|
24568
|
+
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
24569
|
+
// the constraints below are what an agent reads when deciding to use these.
|
|
24570
|
+
html: external_exports.string().max(16384).describe(
|
|
24571
|
+
"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."
|
|
24572
|
+
).optional(),
|
|
24573
|
+
image: external_exports.string().url().describe(
|
|
24574
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
24575
|
+
).optional()
|
|
24576
|
+
});
|
|
24577
|
+
OptionInputSchema = OptionSchema.omit({ id: true }).extend({
|
|
24578
|
+
label: external_exports.string().trim().min(1).max(1e3)
|
|
24579
|
+
}).strict();
|
|
24580
|
+
NIGHT = { from: 23, to: 7 };
|
|
24455
24581
|
AskInputSchema = external_exports.object({
|
|
24456
24582
|
id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
|
|
24457
|
-
parentId: external_exports.string().uuid().optional().describe("
|
|
24583
|
+
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."),
|
|
24458
24584
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
24459
24585
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
24460
|
-
"
|
|
24586
|
+
"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."
|
|
24461
24587
|
),
|
|
24462
|
-
options: external_exports.array(
|
|
24463
|
-
label: external_exports.string().trim().min(1).max(1e3),
|
|
24464
|
-
hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
|
|
24465
|
-
image: external_exports.string().url().optional(),
|
|
24466
|
-
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()
|
|
24467
|
-
}).strict()).min(2).max(6).optional()
|
|
24588
|
+
options: external_exports.array(OptionInputSchema).min(2).max(6).optional()
|
|
24468
24589
|
}).strict();
|
|
24469
24590
|
StartContactSchema = external_exports.object({
|
|
24470
|
-
asks: external_exports.array(AskInputSchema).min(1).describe("The
|
|
24591
|
+
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."),
|
|
24471
24592
|
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
24472
24593
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
24473
24594
|
}).strict();
|
|
24474
24595
|
ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
24475
24596
|
CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
24476
|
-
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'.
|
|
24597
|
+
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).";
|
|
24477
24598
|
CreateGoalSchema = external_exports.object({
|
|
24478
24599
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
24479
24600
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -24481,10 +24602,6 @@ var init_dist = __esm({
|
|
|
24481
24602
|
title: external_exports.string().trim().min(1).max(80).optional(),
|
|
24482
24603
|
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
24483
24604
|
idempotencyKey: external_exports.string().trim().min(1).max(200),
|
|
24484
|
-
/** A past conversation this Goal should be read against — History's "new session from this"
|
|
24485
|
-
* (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
|
|
24486
|
-
* reference only: the owner reads it through `get_thread`, which does its own scoping, and
|
|
24487
|
-
* the writer refuses a thread belonging to another account. */
|
|
24488
24605
|
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
24489
24606
|
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
24490
24607
|
* worked"). It could not have been: this door took no parent, so the only route was
|
|
@@ -24524,23 +24641,13 @@ var init_dist = __esm({
|
|
|
24524
24641
|
ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
24525
24642
|
GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
24526
24643
|
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.";
|
|
24527
|
-
UPDATE_GOAL_DESCRIPTION =
|
|
24528
|
-
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.";
|
|
24529
|
-
CHECK_REPLIES_DESCRIPTION = "
|
|
24644
|
+
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.`;
|
|
24645
|
+
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.";
|
|
24646
|
+
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.";
|
|
24530
24647
|
CheckRepliesSchema = external_exports.object({}).strict();
|
|
24531
|
-
GetThreadSchema = external_exports.object({
|
|
24532
|
-
parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
24533
|
-
}).strict();
|
|
24534
|
-
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.";
|
|
24535
|
-
SearchThreadsSchema = external_exports.object({
|
|
24536
|
-
q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
24537
|
-
}).strict();
|
|
24538
|
-
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.`;
|
|
24539
24648
|
AGENT_TOOLS = [
|
|
24540
24649
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
24541
24650
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
24542
|
-
{ name: "get_thread", description: GET_THREAD_DESCRIPTION, inputSchema: mcpInputSchema(GetThreadSchema) },
|
|
24543
|
-
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION, inputSchema: mcpInputSchema(SearchThreadsSchema) },
|
|
24544
24651
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
24545
24652
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(ClaimGoalSchema) },
|
|
24546
24653
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(GetGoalSchema) },
|
|
@@ -24574,19 +24681,6 @@ var init_dist = __esm({
|
|
|
24574
24681
|
"summarize"
|
|
24575
24682
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
24576
24683
|
]);
|
|
24577
|
-
OptionSchema = external_exports.object({
|
|
24578
|
-
id: external_exports.string(),
|
|
24579
|
-
label: external_exports.string(),
|
|
24580
|
-
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(),
|
|
24581
|
-
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
24582
|
-
// the constraints below are what an agent reads when deciding to use these.
|
|
24583
|
-
html: external_exports.string().max(16384).describe(
|
|
24584
|
-
"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."
|
|
24585
|
-
).optional(),
|
|
24586
|
-
image: external_exports.string().url().describe(
|
|
24587
|
-
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
24588
|
-
).optional()
|
|
24589
|
-
});
|
|
24590
24684
|
VisualSchema = external_exports.object({
|
|
24591
24685
|
url: external_exports.string().url(),
|
|
24592
24686
|
label: external_exports.string().optional()
|
|
@@ -24640,7 +24734,7 @@ var init_dist = __esm({
|
|
|
24640
24734
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
24641
24735
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
24642
24736
|
context: ContextSchema.optional(),
|
|
24643
|
-
options: external_exports.array(
|
|
24737
|
+
options: external_exports.array(OptionInputSchema).min(OPTIONS_MIN).max(OPTIONS_MAX).optional().describe(
|
|
24644
24738
|
"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)."
|
|
24645
24739
|
),
|
|
24646
24740
|
points: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
@@ -24834,7 +24928,7 @@ var init_dist = __esm({
|
|
|
24834
24928
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
24835
24929
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
24836
24930
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
24837
|
-
* re-orient via
|
|
24931
|
+
* re-orient via check_replies). */
|
|
24838
24932
|
external_exports.object({
|
|
24839
24933
|
type: external_exports.literal("superseded"),
|
|
24840
24934
|
parentId: external_exports.string(),
|
|
@@ -24879,9 +24973,9 @@ var init_dist = __esm({
|
|
|
24879
24973
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
24880
24974
|
* the planner declares duplicates instead of asking them twice. */
|
|
24881
24975
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
24882
|
-
/**
|
|
24883
|
-
*
|
|
24884
|
-
info: external_exports.array(external_exports.string().min(1)).
|
|
24976
|
+
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
24977
|
+
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
24978
|
+
info: external_exports.array(external_exports.string().min(1)).default([]),
|
|
24885
24979
|
question: external_exports.string().min(1).nullable(),
|
|
24886
24980
|
/** True on the one turn carrying the agent's own declared question. */
|
|
24887
24981
|
asks: external_exports.boolean().optional(),
|
|
@@ -24916,42 +25010,12 @@ var init_dist = __esm({
|
|
|
24916
25010
|
CLAIM_STALE_MS = 30 * 6e4;
|
|
24917
25011
|
InboxItemSchema = external_exports.object({
|
|
24918
25012
|
id: external_exports.string(),
|
|
24919
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
24920
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
24921
|
-
parentId: external_exports.string().optional(),
|
|
24922
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
24923
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
24924
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
24925
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
24926
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
24927
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
24928
|
-
* thread, which is what the client did for all rows until now. */
|
|
24929
|
-
askId: external_exports.string().optional(),
|
|
24930
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
24931
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
24932
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
24933
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
24934
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
24935
|
-
seq: external_exports.number().int().optional(),
|
|
24936
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
24937
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
24938
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
24939
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
24940
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
24941
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
24942
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
24943
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
24944
|
-
units: external_exports.number().int().positive().optional(),
|
|
24945
25013
|
tokenId: external_exports.string().optional(),
|
|
24946
25014
|
status: NotifyStatusSchema,
|
|
24947
25015
|
context: ContextSchema,
|
|
24948
25016
|
options: external_exports.array(OptionSchema).optional(),
|
|
24949
25017
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
24950
25018
|
points: external_exports.array(external_exports.string()).optional(),
|
|
24951
|
-
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
24952
|
-
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
24953
|
-
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
24954
|
-
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
24955
25019
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
24956
25020
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
24957
25021
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
@@ -24972,7 +25036,31 @@ var init_dist = __esm({
|
|
|
24972
25036
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
24973
25037
|
* `createdAt`. */
|
|
24974
25038
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
24975
|
-
agenda
|
|
25039
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
25040
|
+
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
25041
|
+
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
25042
|
+
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
25043
|
+
* once, off the one read it already makes (`GET /api/inbox/:callId`).
|
|
25044
|
+
*
|
|
25045
|
+
* It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
|
|
25046
|
+
* every cycle, so a spine drawn from them would change length under the caller mid-call.
|
|
25047
|
+
*
|
|
25048
|
+
* `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
|
|
25049
|
+
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
25050
|
+
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
25051
|
+
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
25052
|
+
questions: external_exports.array(external_exports.object({
|
|
25053
|
+
id: external_exports.string(),
|
|
25054
|
+
entryId: external_exports.string(),
|
|
25055
|
+
title: external_exports.string(),
|
|
25056
|
+
state: external_exports.enum(["open", "answered"]),
|
|
25057
|
+
answer: external_exports.string().nullable(),
|
|
25058
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
25059
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
25060
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
25061
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
25062
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional()
|
|
25063
|
+
})).optional(),
|
|
24976
25064
|
visuals: external_exports.array(VisualSchema).optional(),
|
|
24977
25065
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
24978
25066
|
* agent's suggestion, or a default silly name). */
|
|
@@ -24989,16 +25077,35 @@ var init_dist = __esm({
|
|
|
24989
25077
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
24990
25078
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
24991
25079
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
24992
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
24993
|
-
*
|
|
25080
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
25081
|
+
* error badge + Retry. */
|
|
24994
25082
|
error: external_exports.string().optional(),
|
|
25083
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
25084
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
25085
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
25086
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
25087
|
+
cold: external_exports.string().datetime().optional(),
|
|
24995
25088
|
clarifies: external_exports.string().optional(),
|
|
24996
|
-
/**
|
|
24997
|
-
*
|
|
24998
|
-
*
|
|
24999
|
-
*
|
|
25000
|
-
|
|
25001
|
-
|
|
25089
|
+
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
25090
|
+
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
25091
|
+
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
25092
|
+
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
25093
|
+
onCall: external_exports.literal(true).optional(),
|
|
25094
|
+
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
25095
|
+
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
25096
|
+
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
25097
|
+
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
25098
|
+
* `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
|
|
25099
|
+
* has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
|
|
25100
|
+
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
25101
|
+
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
25102
|
+
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
25103
|
+
ring: external_exports.object({
|
|
25104
|
+
declinedAt: external_exports.string().datetime(),
|
|
25105
|
+
anchorAt: external_exports.string().datetime(),
|
|
25106
|
+
nextRingAt: external_exports.string().datetime().nullable(),
|
|
25107
|
+
step: external_exports.number().int()
|
|
25108
|
+
}).optional(),
|
|
25002
25109
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
25003
25110
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
25004
25111
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
@@ -25032,7 +25139,10 @@ var init_dist = __esm({
|
|
|
25032
25139
|
goalIds: external_exports.array(external_exports.string()),
|
|
25033
25140
|
decisionNeedId: external_exports.string().optional(),
|
|
25034
25141
|
sealed: external_exports.boolean(),
|
|
25035
|
-
goalState: external_exports.string().optional()
|
|
25142
|
+
goalState: external_exports.string().optional(),
|
|
25143
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
25144
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
25145
|
+
goalTitle: external_exports.string().optional()
|
|
25036
25146
|
}).optional()
|
|
25037
25147
|
});
|
|
25038
25148
|
APNS_TOKEN_RE = /^[0-9a-fA-F]{64}$/;
|
|
@@ -25063,6 +25173,8 @@ var init_dist = __esm({
|
|
|
25063
25173
|
"inbox",
|
|
25064
25174
|
"dismiss"
|
|
25065
25175
|
]);
|
|
25176
|
+
clock = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
25177
|
+
QUIET = ` Nothing rings from ${clock(NIGHT.from)} to ${clock(NIGHT.to)} your time; the count waits for morning.`;
|
|
25066
25178
|
BrokerTuningSchema = external_exports.object({
|
|
25067
25179
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
25068
25180
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -25097,6 +25209,12 @@ var init_dist = __esm({
|
|
|
25097
25209
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
25098
25210
|
* write, 'hosted' on read (see store.ts). */
|
|
25099
25211
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
25212
|
+
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
25213
|
+
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
25214
|
+
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
25215
|
+
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
25216
|
+
* read (see store.ts). */
|
|
25217
|
+
talk: external_exports.boolean().optional(),
|
|
25100
25218
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
25101
25219
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
25102
25220
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -25123,7 +25241,6 @@ var init_dist = __esm({
|
|
|
25123
25241
|
});
|
|
25124
25242
|
HistoryItemSchema = external_exports.object({
|
|
25125
25243
|
id: external_exports.string(),
|
|
25126
|
-
parentId: external_exports.string(),
|
|
25127
25244
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
25128
25245
|
initiator: external_exports.enum(["user", "agent"]),
|
|
25129
25246
|
title: external_exports.string(),
|
|
@@ -25282,6 +25399,24 @@ var init_dist = __esm({
|
|
|
25282
25399
|
* gate at all. */
|
|
25283
25400
|
blocked: external_exports.boolean().default(false)
|
|
25284
25401
|
});
|
|
25402
|
+
QueueReplySchema = external_exports.object({
|
|
25403
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
25404
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
25405
|
+
* walk can name it in its zoom. */
|
|
25406
|
+
id: external_exports.string(),
|
|
25407
|
+
/** The Goal the note is on. */
|
|
25408
|
+
goalId: external_exports.string(),
|
|
25409
|
+
/** What the note said. */
|
|
25410
|
+
note: external_exports.string(),
|
|
25411
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
25412
|
+
deliveryId: external_exports.string(),
|
|
25413
|
+
requestEntryId: external_exports.string(),
|
|
25414
|
+
askedAt: external_exports.string(),
|
|
25415
|
+
/** When the person last replied — the window's start. */
|
|
25416
|
+
repliedAt: external_exports.string(),
|
|
25417
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
25418
|
+
reply: external_exports.string().nullable()
|
|
25419
|
+
});
|
|
25285
25420
|
QueueItemSchema = external_exports.object({
|
|
25286
25421
|
id: external_exports.string(),
|
|
25287
25422
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -25294,6 +25429,9 @@ var init_dist = __esm({
|
|
|
25294
25429
|
assignee: external_exports.string().nullable().default(null),
|
|
25295
25430
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
25296
25431
|
progress: external_exports.string().nullable().default(null),
|
|
25432
|
+
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
25433
|
+
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
25434
|
+
progressLine: external_exports.string().nullable().optional(),
|
|
25297
25435
|
reviewPending: external_exports.boolean().default(false),
|
|
25298
25436
|
dueAt: external_exports.string().nullable().default(null),
|
|
25299
25437
|
/** The Goal this one was opened under; null at the root. */
|
|
@@ -25306,11 +25444,20 @@ var init_dist = __esm({
|
|
|
25306
25444
|
blocked: external_exports.boolean().default(false),
|
|
25307
25445
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
25308
25446
|
questions: external_exports.array(QueueQuestionSchema).default([]),
|
|
25447
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
25448
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
25449
|
+
* not spell an empty list. */
|
|
25450
|
+
replies: external_exports.array(QueueReplySchema).optional(),
|
|
25309
25451
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
25310
25452
|
repo: external_exports.string().nullable().optional(),
|
|
25311
25453
|
createdAt: external_exports.string(),
|
|
25312
|
-
updatedAt: external_exports.string().nullable().default(null)
|
|
25454
|
+
updatedAt: external_exports.string().nullable().default(null),
|
|
25455
|
+
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
25456
|
+
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
25457
|
+
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
25458
|
+
lastProgressAt: external_exports.string().nullable().optional()
|
|
25313
25459
|
});
|
|
25460
|
+
COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
25314
25461
|
NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
25315
25462
|
NoteStatusSchema = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
25316
25463
|
NoteRepeatSchema = external_exports.enum(["once", "until_done"]);
|
|
@@ -25510,12 +25657,14 @@ var init_dist = __esm({
|
|
|
25510
25657
|
this.body = body;
|
|
25511
25658
|
}
|
|
25512
25659
|
};
|
|
25513
|
-
AWAIT_WINDOW_MS = 45e3;
|
|
25514
25660
|
tokenOverride = null;
|
|
25515
25661
|
authToken = (explicit) => explicit ?? tokenOverride ?? readToken();
|
|
25662
|
+
AWAIT_WINDOW_MS = 45e3;
|
|
25663
|
+
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.";
|
|
25516
25664
|
HEARTBEAT_MS = 25e3;
|
|
25517
25665
|
RETRY_MS = [1e3, 2e3, 5e3, 1e4, 3e4];
|
|
25518
25666
|
JOIN = { config: { broadcast: { self: false } } };
|
|
25667
|
+
JOIN_TIMEOUT_MS = 1e4;
|
|
25519
25668
|
lines = /* @__PURE__ */ new Map();
|
|
25520
25669
|
}
|
|
25521
25670
|
});
|
|
@@ -30126,9 +30275,9 @@ async function available(deps = {}) {
|
|
|
30126
30275
|
});
|
|
30127
30276
|
return [ollama, ...harnesses];
|
|
30128
30277
|
}
|
|
30129
|
-
function refusal(options,
|
|
30278
|
+
function refusal(options, asked2) {
|
|
30130
30279
|
const lines2 = [
|
|
30131
|
-
|
|
30280
|
+
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.",
|
|
30132
30281
|
"",
|
|
30133
30282
|
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
30134
30283
|
];
|
|
@@ -30139,10 +30288,10 @@ function refusal(options, asked) {
|
|
|
30139
30288
|
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
30140
30289
|
return lines2.join("\n");
|
|
30141
30290
|
}
|
|
30142
|
-
async function judgeFor(
|
|
30291
|
+
async function judgeFor(asked2, deps = {}) {
|
|
30143
30292
|
const options = await available(deps);
|
|
30144
|
-
const chosen =
|
|
30145
|
-
if (!chosen?.ready) return { error: refusal(options,
|
|
30293
|
+
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
30294
|
+
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
30146
30295
|
if (chosen.provider === "ollama") {
|
|
30147
30296
|
const model = chosen.model;
|
|
30148
30297
|
return {
|
|
@@ -30163,9 +30312,9 @@ async function judgeFor(asked, deps = {}) {
|
|
|
30163
30312
|
};
|
|
30164
30313
|
}
|
|
30165
30314
|
const runner = RUNNERS[chosen.provider];
|
|
30166
|
-
if (!runner) return { error: refusal(options,
|
|
30315
|
+
if (!runner) return { error: refusal(options, asked2) };
|
|
30167
30316
|
const bin = which(runner.cli);
|
|
30168
|
-
if (!bin) return { error: refusal(options,
|
|
30317
|
+
if (!bin) return { error: refusal(options, asked2) };
|
|
30169
30318
|
return {
|
|
30170
30319
|
provider: chosen.provider,
|
|
30171
30320
|
model: null,
|
|
@@ -30764,7 +30913,7 @@ import { hostname } from "os";
|
|
|
30764
30913
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
30765
30914
|
import { execFile as execFile2, execSync } from "child_process";
|
|
30766
30915
|
import { homedir as homedir8 } from "os";
|
|
30767
|
-
import { join as
|
|
30916
|
+
import { join as join8 } from "path";
|
|
30768
30917
|
|
|
30769
30918
|
// src/run.ts
|
|
30770
30919
|
init_dist();
|
|
@@ -31024,7 +31173,13 @@ function startSession(opts) {
|
|
|
31024
31173
|
...process.env,
|
|
31025
31174
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
31026
31175
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
31027
|
-
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
|
|
31176
|
+
...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
|
|
31177
|
+
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
31178
|
+
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
31179
|
+
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
31180
|
+
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
31181
|
+
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
31182
|
+
PAIGY_HARNESS: "1"
|
|
31028
31183
|
},
|
|
31029
31184
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31030
31185
|
});
|
|
@@ -31073,6 +31228,7 @@ function startSession(opts) {
|
|
|
31073
31228
|
// ../../packages/schema/dist/index.js
|
|
31074
31229
|
init_zod();
|
|
31075
31230
|
init_zod();
|
|
31231
|
+
init_zod();
|
|
31076
31232
|
|
|
31077
31233
|
// ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
|
|
31078
31234
|
var ignoreOverride2 = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
|
|
@@ -32369,6 +32525,23 @@ var zodToJsonSchema2 = (schema, options) => {
|
|
|
32369
32525
|
init_zod();
|
|
32370
32526
|
var OPTIONS_MIN2 = 2;
|
|
32371
32527
|
var OPTIONS_MAX2 = 6;
|
|
32528
|
+
var OptionSchema2 = external_exports.object({
|
|
32529
|
+
id: external_exports.string(),
|
|
32530
|
+
label: external_exports.string(),
|
|
32531
|
+
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(),
|
|
32532
|
+
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
32533
|
+
// the constraints below are what an agent reads when deciding to use these.
|
|
32534
|
+
html: external_exports.string().max(16384).describe(
|
|
32535
|
+
"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."
|
|
32536
|
+
).optional(),
|
|
32537
|
+
image: external_exports.string().url().describe(
|
|
32538
|
+
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
32539
|
+
).optional()
|
|
32540
|
+
});
|
|
32541
|
+
var OptionInputSchema2 = OptionSchema2.omit({ id: true }).extend({
|
|
32542
|
+
label: external_exports.string().trim().min(1).max(1e3)
|
|
32543
|
+
}).strict();
|
|
32544
|
+
var NIGHT2 = { from: 23, to: 7 };
|
|
32372
32545
|
function draft20202(node) {
|
|
32373
32546
|
if (Array.isArray(node)) return node.map(draft20202);
|
|
32374
32547
|
if (node && typeof node === "object") {
|
|
@@ -32400,26 +32573,21 @@ function mcpInputSchema2(s) {
|
|
|
32400
32573
|
}
|
|
32401
32574
|
var AskInputSchema2 = external_exports.object({
|
|
32402
32575
|
id: external_exports.string().optional().describe("Optional idempotency key or client-side ID for this specific ask."),
|
|
32403
|
-
parentId: external_exports.string().uuid().optional().describe("
|
|
32576
|
+
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."),
|
|
32404
32577
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
32405
32578
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
32406
|
-
"
|
|
32579
|
+
"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."
|
|
32407
32580
|
),
|
|
32408
|
-
options: external_exports.array(
|
|
32409
|
-
label: external_exports.string().trim().min(1).max(1e3),
|
|
32410
|
-
hint: external_exports.string().trim().max(500).describe("Optional short projection of consequence or action if this option is chosen.").optional(),
|
|
32411
|
-
image: external_exports.string().url().optional(),
|
|
32412
|
-
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()
|
|
32413
|
-
}).strict()).min(2).max(6).optional()
|
|
32581
|
+
options: external_exports.array(OptionInputSchema2).min(2).max(6).optional()
|
|
32414
32582
|
}).strict();
|
|
32415
32583
|
var StartContactSchema2 = external_exports.object({
|
|
32416
|
-
asks: external_exports.array(AskInputSchema2).min(1).describe("The
|
|
32584
|
+
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."),
|
|
32417
32585
|
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
32418
32586
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
32419
32587
|
}).strict();
|
|
32420
32588
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
32421
32589
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
32422
|
-
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'.
|
|
32590
|
+
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).";
|
|
32423
32591
|
var CreateGoalSchema2 = external_exports.object({
|
|
32424
32592
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
32425
32593
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -32427,10 +32595,6 @@ var CreateGoalSchema2 = external_exports.object({
|
|
|
32427
32595
|
title: external_exports.string().trim().min(1).max(80).optional(),
|
|
32428
32596
|
ownerParticipant: external_exports.string().trim().min(1).optional(),
|
|
32429
32597
|
idempotencyKey: external_exports.string().trim().min(1).max(200),
|
|
32430
|
-
/** A past conversation this Goal should be read against — History's "new session from this"
|
|
32431
|
-
* (owner, on the call of 2026-09-14: "let's do the reference with the threading"). A
|
|
32432
|
-
* reference only: the owner reads it through `get_thread`, which does its own scoping, and
|
|
32433
|
-
* the writer refuses a thread belonging to another account. */
|
|
32434
32598
|
/** THE GOAL THIS ONE BELONGS UNDER (owner, 2026-09-15: "the ask I gave for the design doc
|
|
32435
32599
|
* didn't get created as a child goal of the voice UI goal, which is how it should've
|
|
32436
32600
|
* worked"). It could not have been: this door took no parent, so the only route was
|
|
@@ -32470,23 +32634,13 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
|
|
|
32470
32634
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
32471
32635
|
var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
|
|
32472
32636
|
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.";
|
|
32473
|
-
var UPDATE_GOAL_DESCRIPTION2 =
|
|
32474
|
-
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.";
|
|
32475
|
-
var CHECK_REPLIES_DESCRIPTION2 = "
|
|
32637
|
+
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.`;
|
|
32638
|
+
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.";
|
|
32639
|
+
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.";
|
|
32476
32640
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
32477
|
-
var GetThreadSchema2 = external_exports.object({
|
|
32478
|
-
parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
|
|
32479
|
-
}).strict();
|
|
32480
|
-
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.";
|
|
32481
|
-
var SearchThreadsSchema2 = external_exports.object({
|
|
32482
|
-
q: external_exports.string().describe("What to look for \u2014 plain words or a phrase (e.g. 'the livekit timeout', 'deploy to prod').")
|
|
32483
|
-
}).strict();
|
|
32484
|
-
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.`;
|
|
32485
32641
|
var AGENT_TOOLS2 = [
|
|
32486
32642
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
32487
32643
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
32488
|
-
{ name: "get_thread", description: GET_THREAD_DESCRIPTION2, inputSchema: mcpInputSchema2(GetThreadSchema2) },
|
|
32489
|
-
{ name: "search_threads", description: SEARCH_THREADS_DESCRIPTION2, inputSchema: mcpInputSchema2(SearchThreadsSchema2) },
|
|
32490
32644
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
|
|
32491
32645
|
{ name: "claim_goal", description: CLAIM_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(ClaimGoalSchema2) },
|
|
32492
32646
|
{ name: "get_goal", description: GET_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(GetGoalSchema2) },
|
|
@@ -32535,19 +32689,6 @@ var TransformSchema2 = external_exports.enum([
|
|
|
32535
32689
|
"summarize"
|
|
32536
32690
|
// reduce volume, keep decision value — 30-turn cap, spoken briefing
|
|
32537
32691
|
]);
|
|
32538
|
-
var OptionSchema2 = external_exports.object({
|
|
32539
|
-
id: external_exports.string(),
|
|
32540
|
-
label: external_exports.string(),
|
|
32541
|
-
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(),
|
|
32542
|
-
// .describe() flows into the MCP contact JSON schema (zodToJsonSchema), so
|
|
32543
|
-
// the constraints below are what an agent reads when deciding to use these.
|
|
32544
|
-
html: external_exports.string().max(16384).describe(
|
|
32545
|
-
"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."
|
|
32546
|
-
).optional(),
|
|
32547
|
-
image: external_exports.string().url().describe(
|
|
32548
|
-
"Optional image URL rendered as the option's preview (plain image, not sandboxed). For agent-generated HTML/CSS mockups, use `html` instead."
|
|
32549
|
-
).optional()
|
|
32550
|
-
});
|
|
32551
32692
|
var VisualSchema2 = external_exports.object({
|
|
32552
32693
|
url: external_exports.string().url(),
|
|
32553
32694
|
label: external_exports.string().optional()
|
|
@@ -32601,7 +32742,7 @@ var NotifyRequestFields2 = external_exports.object({
|
|
|
32601
32742
|
* ABSENT on the E2EE path, where the sealed `envelope` below carries it. The
|
|
32602
32743
|
* superRefine at the bottom enforces exactly one of the two. */
|
|
32603
32744
|
context: ContextSchema2.optional(),
|
|
32604
|
-
options: external_exports.array(
|
|
32745
|
+
options: external_exports.array(OptionInputSchema2).min(OPTIONS_MIN2).max(OPTIONS_MAX2).optional().describe(
|
|
32605
32746
|
"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)."
|
|
32606
32747
|
),
|
|
32607
32748
|
points: external_exports.array(external_exports.string().min(1)).optional().describe(
|
|
@@ -32795,7 +32936,7 @@ var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
|
|
|
32795
32936
|
/** The awaited ask was REPLACED by a newer notification on its thread (e.g. a
|
|
32796
32937
|
* post-feedback revision, #633) — the user will never answer this id. Stop
|
|
32797
32938
|
* awaiting it; the live ask is the thread's newest turn (await that one, or
|
|
32798
|
-
* re-orient via
|
|
32939
|
+
* re-orient via check_replies). */
|
|
32799
32940
|
external_exports.object({
|
|
32800
32941
|
type: external_exports.literal("superseded"),
|
|
32801
32942
|
parentId: external_exports.string(),
|
|
@@ -32840,9 +32981,9 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
32840
32981
|
/** Twin coverage (#1089): sibling claim ids this asking turn's answer ALSO settles —
|
|
32841
32982
|
* the planner declares duplicates instead of asking them twice. */
|
|
32842
32983
|
coveredIds: external_exports.array(external_exports.string()).optional(),
|
|
32843
|
-
/**
|
|
32844
|
-
*
|
|
32845
|
-
info: external_exports.array(external_exports.string().min(1)).
|
|
32984
|
+
/** The spoken sentences of the turn, in order. No count: how long a turn is is the brain's call
|
|
32985
|
+
* (owner, 2026-09-25), and a count here refused whole plans. */
|
|
32986
|
+
info: external_exports.array(external_exports.string().min(1)).default([]),
|
|
32846
32987
|
question: external_exports.string().min(1).nullable(),
|
|
32847
32988
|
/** True on the one turn carrying the agent's own declared question. */
|
|
32848
32989
|
asks: external_exports.boolean().optional(),
|
|
@@ -32877,42 +33018,12 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
32877
33018
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
32878
33019
|
var InboxItemSchema2 = external_exports.object({
|
|
32879
33020
|
id: external_exports.string(),
|
|
32880
|
-
/** The conversation thread + connection this item lives on. Present on the replied
|
|
32881
|
-
* detail — they power History's "Continue" / "New session from this" (#57/#251). */
|
|
32882
|
-
parentId: external_exports.string().optional(),
|
|
32883
|
-
/** THE ARRIVAL this row is one unit of (`notifications.ask_id` → `asks`). A claim is one
|
|
32884
|
-
* arrival and its units are N rows of it, so this — not `parentId` — is what makes a
|
|
32885
|
-
* multi-part notification one thing on screen. The thread is the whole CONVERSATION: it
|
|
32886
|
-
* accumulates every message an agent ever sent, so grouping by it renders a day of
|
|
32887
|
-
* unrelated updates as a single "12-part request". Absent on rows written before the
|
|
32888
|
-
* `asks` table, and on anything that never went through `notify` — both fall back to the
|
|
32889
|
-
* thread, which is what the client did for all rows until now. */
|
|
32890
|
-
askId: external_exports.string().optional(),
|
|
32891
|
-
/** WHERE this unit sat in the message it was cut from (`notifications.seq`). The batch
|
|
32892
|
-
* shares one `created_at` to the microsecond, so without it the author's order is
|
|
32893
|
-
* unrecoverable client-side — a four-paragraph briefing rendered opening-paragraph-last
|
|
32894
|
-
* (live 2026-08-10, D35). The API already orders by it; this lets a reader that
|
|
32895
|
-
* re-sorts (grouping, filtering) put an arrival back in the order it was written. */
|
|
32896
|
-
seq: external_exports.number().int().optional(),
|
|
32897
|
-
/** HOW MANY units the arrival was cut into. A device reads a LENS, never the arrival —
|
|
32898
|
-
* `/api/inbox` serves `open`, so the units already settled are gone from it — and a client
|
|
32899
|
-
* counting what it can see is counting what is LEFT. Walking a three-unit ask on the answer
|
|
32900
|
-
* screen read "1 of 3", then "1 of 2", then no chip at all, each answer having removed the
|
|
32901
|
-
* only evidence of itself. How big an arrival is, is a fact about the arrival, so the
|
|
32902
|
-
* server that can still see every row states it. Absent on any row with no `askId`: a
|
|
32903
|
-
* unit knows WHICH ask it came from and WHERE it sat in it, and how many there were is
|
|
32904
|
-
* the one part of its own arrival a single row cannot answer. */
|
|
32905
|
-
units: external_exports.number().int().positive().optional(),
|
|
32906
33021
|
tokenId: external_exports.string().optional(),
|
|
32907
33022
|
status: NotifyStatusSchema2,
|
|
32908
33023
|
context: ContextSchema2,
|
|
32909
33024
|
options: external_exports.array(OptionSchema2).optional(),
|
|
32910
33025
|
/** The ask's declared coverage points (#396), when the agent sent them. */
|
|
32911
33026
|
points: external_exports.array(external_exports.string()).optional(),
|
|
32912
|
-
/** The call's AGENDA (broker/agenda-design.md): the ordered turns it is made of, built at
|
|
32913
|
-
* ring/enqueue time. Replaces the condensed line + index-aligned phrased points, which
|
|
32914
|
-
* between them could not express a call as a sequence. `question: null` is a real turn —
|
|
32915
|
-
* a status update stays a statement instead of being shaped into a yes/no. */
|
|
32916
33027
|
/** Does this claim want an ANSWER, or is it telling you something? Written per row from
|
|
32917
33028
|
* `requestAsks` — the agent's own declaration, not a guess. `false` is what earns a card
|
|
32918
33029
|
* its acknowledge affordance: without it a status update offers a text box and a dismiss,
|
|
@@ -32933,7 +33044,31 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
32933
33044
|
* instant the agent picks it up (#997). Absent = pre-trigger row; fall back to
|
|
32934
33045
|
* `createdAt`. */
|
|
32935
33046
|
agentStateAt: external_exports.string().datetime().optional(),
|
|
32936
|
-
agenda
|
|
33047
|
+
/** THE QUESTIONS A CALL CARRIES — the call screen's agenda spine (walk/design.md §11, owner
|
|
33048
|
+
* 2026-09-22). One per DecisionNeed on the Call, in the Call's order, answered or open (a
|
|
33049
|
+
* superseded or cancelled need is no longer a question anyone is asked). Present only on a
|
|
33050
|
+
* Call's cards, and every card of that Call carries the same list: the call screen reads it
|
|
33051
|
+
* once, off the one read it already makes (`GET /api/inbox/:callId`).
|
|
33052
|
+
*
|
|
33053
|
+
* It counts DECISIONS, not agenda turns: turns include context-only lines and are re-planned
|
|
33054
|
+
* every cycle, so a spine drawn from them would change length under the caller mid-call.
|
|
33055
|
+
*
|
|
33056
|
+
* `entryId` is the request Entry — the id the bot calls a CLAIM, and the one it names on the
|
|
33057
|
+
* `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
|
|
33058
|
+
* the card's own concise heading; `answer` the accepted answer in words, null while open. It
|
|
33059
|
+
* REPLACED `agenda` (turns), which nothing ever filled. */
|
|
33060
|
+
questions: external_exports.array(external_exports.object({
|
|
33061
|
+
id: external_exports.string(),
|
|
33062
|
+
entryId: external_exports.string(),
|
|
33063
|
+
title: external_exports.string(),
|
|
33064
|
+
state: external_exports.enum(["open", "answered"]),
|
|
33065
|
+
answer: external_exports.string().nullable(),
|
|
33066
|
+
/** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
|
|
33067
|
+
* request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
|
|
33068
|
+
* carry for the call's one agent, so the phone draws it with the same seed. Absent when the
|
|
33069
|
+
* author is not an agent this account holds (unpaired since, or a person). */
|
|
33070
|
+
agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional()
|
|
33071
|
+
})).optional(),
|
|
32937
33072
|
visuals: external_exports.array(VisualSchema2).optional(),
|
|
32938
33073
|
/** The connected agent's name (the single pairing name — user-typed, or the
|
|
32939
33074
|
* agent's suggestion, or a default silly name). */
|
|
@@ -32950,16 +33085,35 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
32950
33085
|
* agent (held in the inbox until the agent replies on the thread). */
|
|
32951
33086
|
turn: external_exports.enum(["you", "agent"]).default("you"),
|
|
32952
33087
|
/** Hard error reason on an awaiting request (turn="agent") — the wake failed to reach
|
|
32953
|
-
* the agent (provider-agnostic; set server-side). Absent = no hard error
|
|
32954
|
-
*
|
|
33088
|
+
* the agent (provider-agnostic; set server-side). Absent = no hard error. Drives the inbox
|
|
33089
|
+
* error badge + Retry. */
|
|
32955
33090
|
error: external_exports.string().optional(),
|
|
33091
|
+
/** WHEN THIS AGENT WORK WENT QUIET (turn="agent"), by the one rule (`coldSince`: three days
|
|
33092
|
+
* with nothing said), or absent while it is not stalled. The inbox's stalled badge reads
|
|
33093
|
+
* this and nothing else (2026-09-23: a 3-minute age rule badged every live Goal stalled,
|
|
33094
|
+
* and "dismiss the stalled ones" cancelled 37 pieces of live work). */
|
|
33095
|
+
cold: external_exports.string().datetime().optional(),
|
|
32956
33096
|
clarifies: external_exports.string().optional(),
|
|
32957
|
-
/**
|
|
32958
|
-
*
|
|
32959
|
-
*
|
|
32960
|
-
*
|
|
32961
|
-
|
|
32962
|
-
|
|
33097
|
+
/** THIS CARD'S QUESTION IS ON A LIVE CALL (owner, 2026-09-24: "Mark it while the call is
|
|
33098
|
+
* live"). Present only while an open Call Delivery carries the card's request Entry — read
|
|
33099
|
+
* off the same open list the card came from, so it clears when the Call does. A card is the
|
|
33100
|
+
* backup for a call not taken; while the call has it, the call is where it is answered. */
|
|
33101
|
+
onCall: external_exports.literal(true).optional(),
|
|
33102
|
+
/** THE RING, ON THE ITEM (walk/design.md §12 §17, #2251): the last ring on this card was
|
|
33103
|
+
* declined, and what the ladder will do next — read off the cron's own row, never computed
|
|
33104
|
+
* on the phone. Present only while a `declined` receipt stands on the card's last Call.
|
|
33105
|
+
* The ladder is ACCOUNT-WIDE (#2259): `anchorAt` and `step` are the account's position;
|
|
33106
|
+
* `nextRingAt` is this card's armed instant (`deliveries.next_ring_at`), null once the cron
|
|
33107
|
+
* has disarmed it — the curve's end, `inbox`/`dismiss`, or a setting that said no more rings.
|
|
33108
|
+
* It replaced `gaveUp` (deleted 2026-09-22): "the ladder spent" was a boolean the projection
|
|
33109
|
+
* never set, and it is `nextRingAt === null` here — the party's *Missed you* (`party/dress.ts`)
|
|
33110
|
+
* and the roster's `unreached` read `declinedAt`, and stand while it does. */
|
|
33111
|
+
ring: external_exports.object({
|
|
33112
|
+
declinedAt: external_exports.string().datetime(),
|
|
33113
|
+
anchorAt: external_exports.string().datetime(),
|
|
33114
|
+
nextRingAt: external_exports.string().datetime().nullable(),
|
|
33115
|
+
step: external_exports.number().int()
|
|
33116
|
+
}).optional(),
|
|
32963
33117
|
/** Why this arrived the way it did, read back off the delivery receipt (`notify/why.ts`).
|
|
32964
33118
|
* Absent for anything never delivered through a push, and for older rows written before
|
|
32965
33119
|
* the reason was recorded. Deliberately a debug affordance, shown small (owner,
|
|
@@ -32993,7 +33147,10 @@ var InboxItemSchema2 = external_exports.object({
|
|
|
32993
33147
|
goalIds: external_exports.array(external_exports.string()),
|
|
32994
33148
|
decisionNeedId: external_exports.string().optional(),
|
|
32995
33149
|
sealed: external_exports.boolean(),
|
|
32996
|
-
goalState: external_exports.string().optional()
|
|
33150
|
+
goalState: external_exports.string().optional(),
|
|
33151
|
+
/** THAT GOAL'S NAME (#2416) — what Activity's row is headed by, since a row there is one Goal
|
|
33152
|
+
* and the cards it holds sit behind it. Stamped by the same read as `goalState`. */
|
|
33153
|
+
goalTitle: external_exports.string().optional()
|
|
32997
33154
|
}).optional()
|
|
32998
33155
|
});
|
|
32999
33156
|
var APNS_TOKEN_RE2 = /^[0-9a-fA-F]{64}$/;
|
|
@@ -33024,6 +33181,8 @@ var MissedCallSchema2 = external_exports.enum([
|
|
|
33024
33181
|
"inbox",
|
|
33025
33182
|
"dismiss"
|
|
33026
33183
|
]);
|
|
33184
|
+
var clock2 = (h) => h === 0 ? "midnight" : h === 12 ? "noon" : h < 12 ? `${h} am` : `${h - 12} pm`;
|
|
33185
|
+
var QUIET2 = ` Nothing rings from ${clock2(NIGHT2.from)} to ${clock2(NIGHT2.to)} your time; the count waits for morning.`;
|
|
33027
33186
|
var BrokerTuningSchema2 = external_exports.object({
|
|
33028
33187
|
/** 'none' = skip the spoken ack after a mapped answer (power users find it slow). */
|
|
33029
33188
|
ackVerbosity: external_exports.enum(["normal", "none"]).optional(),
|
|
@@ -33058,6 +33217,12 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33058
33217
|
* must not silently reset this privacy choice. Absent = leave unchanged on
|
|
33059
33218
|
* write, 'hosted' on read (see store.ts). */
|
|
33060
33219
|
voiceMode: external_exports.enum(["hosted", "on_device"]).optional(),
|
|
33220
|
+
/** Talk — after you answer, the next step is read aloud (walk/design.md §6). ALWAYS ON until
|
|
33221
|
+
* turned off (owner, 2026-09-18, #2249): a setting, not a per-walk toggle. Optional, NOT
|
|
33222
|
+
* defaulted, for the same reason `voiceMode` is: a stale client PATCHing the full settings
|
|
33223
|
+
* object must not silently turn it back on. Absent = leave unchanged on write, true on
|
|
33224
|
+
* read (see store.ts). */
|
|
33225
|
+
talk: external_exports.boolean().optional(),
|
|
33061
33226
|
/** Per-user ring budget (#603): calls per rolling day before further calls
|
|
33062
33227
|
* degrade to banner. Absent = the global default (25). A number, never a
|
|
33063
33228
|
* bypass — every account keeps a ceiling. No UI; set per user for testing. */
|
|
@@ -33084,7 +33249,6 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33084
33249
|
});
|
|
33085
33250
|
var HistoryItemSchema2 = external_exports.object({
|
|
33086
33251
|
id: external_exports.string(),
|
|
33087
|
-
parentId: external_exports.string(),
|
|
33088
33252
|
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
33089
33253
|
initiator: external_exports.enum(["user", "agent"]),
|
|
33090
33254
|
title: external_exports.string(),
|
|
@@ -33243,6 +33407,24 @@ var QueueQuestionSchema2 = external_exports.object({
|
|
|
33243
33407
|
* gate at all. */
|
|
33244
33408
|
blocked: external_exports.boolean().default(false)
|
|
33245
33409
|
});
|
|
33410
|
+
var QueueReplySchema2 = external_exports.object({
|
|
33411
|
+
/** The card this note was (`deliveryId:requestEntryId`, minted by the server like every card
|
|
33412
|
+
* id) — so the phone can tell a reply it just sent from one the queue already carries, and the
|
|
33413
|
+
* walk can name it in its zoom. */
|
|
33414
|
+
id: external_exports.string(),
|
|
33415
|
+
/** The Goal the note is on. */
|
|
33416
|
+
goalId: external_exports.string(),
|
|
33417
|
+
/** What the note said. */
|
|
33418
|
+
note: external_exports.string(),
|
|
33419
|
+
/** Where it was carried — where a second reply goes (`POST /api/entries`, #2252). */
|
|
33420
|
+
deliveryId: external_exports.string(),
|
|
33421
|
+
requestEntryId: external_exports.string(),
|
|
33422
|
+
askedAt: external_exports.string(),
|
|
33423
|
+
/** When the person last replied — the window's start. */
|
|
33424
|
+
repliedAt: external_exports.string(),
|
|
33425
|
+
/** The person's latest words about it; null when there is nothing readable in them. */
|
|
33426
|
+
reply: external_exports.string().nullable()
|
|
33427
|
+
});
|
|
33246
33428
|
var QueueItemSchema2 = external_exports.object({
|
|
33247
33429
|
id: external_exports.string(),
|
|
33248
33430
|
/** One-line headline — the first sentence of the outcome. */
|
|
@@ -33255,6 +33437,9 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33255
33437
|
assignee: external_exports.string().nullable().default(null),
|
|
33256
33438
|
/** What the agent last said it was doing; null if it has said nothing. */
|
|
33257
33439
|
progress: external_exports.string().nullable().default(null),
|
|
33440
|
+
/** HOME'S LINE FOR THAT NOTE (owner, 2026-09-23): a few plain words one read wrote from `progress`,
|
|
33441
|
+
* served only while it was written for the current note. Null means show the Goal's name. */
|
|
33442
|
+
progressLine: external_exports.string().nullable().optional(),
|
|
33258
33443
|
reviewPending: external_exports.boolean().default(false),
|
|
33259
33444
|
dueAt: external_exports.string().nullable().default(null),
|
|
33260
33445
|
/** The Goal this one was opened under; null at the root. */
|
|
@@ -33267,11 +33452,20 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33267
33452
|
blocked: external_exports.boolean().default(false),
|
|
33268
33453
|
/** Every decision need on it, open or settled — the page decides which to show. */
|
|
33269
33454
|
questions: external_exports.array(QueueQuestionSchema2).default([]),
|
|
33455
|
+
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
33456
|
+
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
33457
|
+
* not spell an empty list. */
|
|
33458
|
+
replies: external_exports.array(QueueReplySchema2).optional(),
|
|
33270
33459
|
/** The repository or project identifier this Goal belongs to (#2280), null if untracked. */
|
|
33271
33460
|
repo: external_exports.string().nullable().optional(),
|
|
33272
33461
|
createdAt: external_exports.string(),
|
|
33273
|
-
updatedAt: external_exports.string().nullable().default(null)
|
|
33462
|
+
updatedAt: external_exports.string().nullable().default(null),
|
|
33463
|
+
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
33464
|
+
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
33465
|
+
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
33466
|
+
lastProgressAt: external_exports.string().nullable().optional()
|
|
33274
33467
|
});
|
|
33468
|
+
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
33275
33469
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
33276
33470
|
var NoteStatusSchema2 = external_exports.enum(["open", "assigned", "in_progress", "done"]);
|
|
33277
33471
|
var NoteRepeatSchema2 = external_exports.enum(["once", "until_done"]);
|
|
@@ -33630,6 +33824,7 @@ async function ask(state, rail, input) {
|
|
|
33630
33824
|
waiting: "hard",
|
|
33631
33825
|
channel: input.call ? "call" : "notification"
|
|
33632
33826
|
});
|
|
33827
|
+
if ("recorded" in sent) return null;
|
|
33633
33828
|
const entryId = sent.requestEntryIds[0];
|
|
33634
33829
|
if (!entryId) return null;
|
|
33635
33830
|
(state.seen ??= /* @__PURE__ */ new Set()).add(entryId);
|
|
@@ -33808,14 +34003,14 @@ init_dist();
|
|
|
33808
34003
|
init_catalog();
|
|
33809
34004
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
33810
34005
|
import { homedir as homedir6 } from "os";
|
|
33811
|
-
import { join as
|
|
34006
|
+
import { join as join7 } from "path";
|
|
33812
34007
|
|
|
33813
34008
|
// src/workspaces.ts
|
|
33814
34009
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
33815
|
-
import { dirname as dirname2, join as
|
|
34010
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
|
|
33816
34011
|
import { homedir as homedir4 } from "os";
|
|
33817
34012
|
function workspacesFile() {
|
|
33818
|
-
return
|
|
34013
|
+
return join4(homedir4(), ".paigy", "workspaces.json");
|
|
33819
34014
|
}
|
|
33820
34015
|
function listWorkspaces(deps) {
|
|
33821
34016
|
try {
|
|
@@ -33850,7 +34045,7 @@ function resolveWakeDir(pinned, deps) {
|
|
|
33850
34045
|
// src/owner.ts
|
|
33851
34046
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
|
|
33852
34047
|
import { homedir as homedir5 } from "os";
|
|
33853
|
-
import { join as
|
|
34048
|
+
import { join as join5 } from "path";
|
|
33854
34049
|
var HEAD_BYTES = 64e3;
|
|
33855
34050
|
function readHead(file) {
|
|
33856
34051
|
const fd = openSync2(file, "r");
|
|
@@ -33866,7 +34061,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
33866
34061
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
33867
34062
|
if (!m) return null;
|
|
33868
34063
|
const prefix = m[1];
|
|
33869
|
-
const root = deps.projectsDir ??
|
|
34064
|
+
const root = deps.projectsDir ?? join5(homedir5(), ".claude", "projects");
|
|
33870
34065
|
const list = deps.list ?? ((dir) => readdirSync(dir));
|
|
33871
34066
|
const head = deps.head ?? readHead;
|
|
33872
34067
|
let folders;
|
|
@@ -33878,7 +34073,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
33878
34073
|
for (const folder of folders) {
|
|
33879
34074
|
let files;
|
|
33880
34075
|
try {
|
|
33881
|
-
files = list(
|
|
34076
|
+
files = list(join5(root, folder));
|
|
33882
34077
|
} catch {
|
|
33883
34078
|
continue;
|
|
33884
34079
|
}
|
|
@@ -33886,7 +34081,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
33886
34081
|
if (!file) continue;
|
|
33887
34082
|
let text;
|
|
33888
34083
|
try {
|
|
33889
|
-
text = head(
|
|
34084
|
+
text = head(join5(root, folder, file));
|
|
33890
34085
|
} catch {
|
|
33891
34086
|
continue;
|
|
33892
34087
|
}
|
|
@@ -33906,7 +34101,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
33906
34101
|
// src/update.ts
|
|
33907
34102
|
import { execFile } from "child_process";
|
|
33908
34103
|
import { createRequire } from "module";
|
|
33909
|
-
import { dirname as dirname3, join as
|
|
34104
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
33910
34105
|
import { promisify } from "util";
|
|
33911
34106
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
33912
34107
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -33933,7 +34128,7 @@ async function latestVersion() {
|
|
|
33933
34128
|
}
|
|
33934
34129
|
async function install(version) {
|
|
33935
34130
|
const bin = dirname3(process.execPath);
|
|
33936
|
-
await promisify(execFile)(
|
|
34131
|
+
await promisify(execFile)(join6(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
33937
34132
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
33938
34133
|
timeout: 5 * 6e4
|
|
33939
34134
|
});
|
|
@@ -33984,7 +34179,7 @@ function selfUpdater(deps) {
|
|
|
33984
34179
|
}
|
|
33985
34180
|
|
|
33986
34181
|
// src/host.ts
|
|
33987
|
-
var HOST_FILE =
|
|
34182
|
+
var HOST_FILE = join7(homedir6(), ".paigy", "host.json");
|
|
33988
34183
|
var runKey = (slot) => `slot:${slot}`;
|
|
33989
34184
|
var claimedRun = (sessionId2) => {
|
|
33990
34185
|
const slot = sessionSlot(sessionId2);
|
|
@@ -33995,9 +34190,9 @@ async function slotHasWaitingWork(token, session) {
|
|
|
33995
34190
|
}
|
|
33996
34191
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
33997
34192
|
function wakeAction(live, now, serverSeenAt) {
|
|
33998
|
-
if (live) return "skip";
|
|
33999
|
-
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return "skip";
|
|
34000
|
-
return "spawn";
|
|
34193
|
+
if (live) return { action: "skip", why: "run" };
|
|
34194
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
|
|
34195
|
+
return { action: "spawn", why: "nobody" };
|
|
34001
34196
|
}
|
|
34002
34197
|
function startHost(opts) {
|
|
34003
34198
|
const runs = /* @__PURE__ */ new Map();
|
|
@@ -34087,7 +34282,11 @@ function startHost(opts) {
|
|
|
34087
34282
|
const live = runs.get(key);
|
|
34088
34283
|
const tokenId = slotIdentity(slot).tokenId;
|
|
34089
34284
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
34090
|
-
|
|
34285
|
+
const decided = wakeAction(!!live, Date.now(), seenByServer);
|
|
34286
|
+
if (decided.action === "skip") {
|
|
34287
|
+
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`);
|
|
34288
|
+
return;
|
|
34289
|
+
}
|
|
34091
34290
|
const token = readToken(slot);
|
|
34092
34291
|
if (!token) return;
|
|
34093
34292
|
const owner = ownerSession(slot);
|
|
@@ -34346,7 +34545,7 @@ async function pairThisMachine() {
|
|
|
34346
34545
|
}
|
|
34347
34546
|
}
|
|
34348
34547
|
function forwardCodexThreadId() {
|
|
34349
|
-
const cfg =
|
|
34548
|
+
const cfg = join8(process.env.CODEX_HOME || join8(homedir8(), ".codex"), "config.toml");
|
|
34350
34549
|
if (!existsSync5(cfg)) return;
|
|
34351
34550
|
const text = readFileSync5(cfg, "utf8");
|
|
34352
34551
|
const head = "[mcp_servers.paigy]\n";
|
|
@@ -34448,7 +34647,7 @@ async function main() {
|
|
|
34448
34647
|
const wsDeps = { file: workspacesFile() };
|
|
34449
34648
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
34450
34649
|
if (!listWorkspaces(wsDeps).length) {
|
|
34451
|
-
const guess =
|
|
34650
|
+
const guess = join8(homedir8(), "projects");
|
|
34452
34651
|
if (existsSync5(guess)) {
|
|
34453
34652
|
addWorkspace(guess, wsDeps);
|
|
34454
34653
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -34489,7 +34688,7 @@ async function main() {
|
|
|
34489
34688
|
}
|
|
34490
34689
|
}
|
|
34491
34690
|
try {
|
|
34492
|
-
const settings =
|
|
34691
|
+
const settings = join8(homedir8(), ".claude", "settings.json");
|
|
34493
34692
|
if (which("claude") && existsSync5(settings)) {
|
|
34494
34693
|
const cfg = JSON.parse(readFileSync5(settings, "utf8"));
|
|
34495
34694
|
if (!cfg["statusLine"]) {
|
|
@@ -34547,13 +34746,13 @@ async function main() {
|
|
|
34547
34746
|
<key>SoftResourceLimits</key><dict>
|
|
34548
34747
|
<key>NumberOfFiles</key><integer>65536</integer>
|
|
34549
34748
|
</dict>
|
|
34550
|
-
<key>StandardOutPath</key><string>${
|
|
34551
|
-
<key>StandardErrorPath</key><string>${
|
|
34749
|
+
<key>StandardOutPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
|
|
34750
|
+
<key>StandardErrorPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
|
|
34552
34751
|
</dict></plist>
|
|
34553
34752
|
`;
|
|
34554
|
-
const dir =
|
|
34753
|
+
const dir = join8(homedir8(), "Library", "LaunchAgents");
|
|
34555
34754
|
mkdirSync3(dir, { recursive: true });
|
|
34556
|
-
const path =
|
|
34755
|
+
const path = join8(dir, `${SERVICE_LABEL}.plist`);
|
|
34557
34756
|
writeFileSync4(path, plist);
|
|
34558
34757
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
34559
34758
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|