@paigy/harness 0.3.28 → 0.3.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/agy-acp.js +44 -8
- package/dist/cli.js +465 -242
- package/dist/main.js +4746 -4539
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -7585,8 +7585,8 @@ var require_webidl = __commonJS({
|
|
|
7585
7585
|
return new TypeError(`${message.header}: ${message.message}`);
|
|
7586
7586
|
};
|
|
7587
7587
|
webidl.errors.conversionFailed = function(context) {
|
|
7588
|
-
const
|
|
7589
|
-
const message = `${context.argument} could not be converted to${
|
|
7588
|
+
const plural3 = context.types.length === 1 ? "" : " one of";
|
|
7589
|
+
const message = `${context.argument} could not be converted to${plural3}: ${context.types.join(", ")}.`;
|
|
7590
7590
|
return webidl.errors.exception({
|
|
7591
7591
|
header: context.prefix,
|
|
7592
7592
|
message
|
|
@@ -15050,9 +15050,9 @@ var require_pluralizer = __commonJS({
|
|
|
15050
15050
|
this: "these"
|
|
15051
15051
|
};
|
|
15052
15052
|
module.exports = class Pluralizer {
|
|
15053
|
-
constructor(singular,
|
|
15053
|
+
constructor(singular, plural3) {
|
|
15054
15054
|
this.singular = singular;
|
|
15055
|
-
this.plural =
|
|
15055
|
+
this.plural = plural3;
|
|
15056
15056
|
}
|
|
15057
15057
|
pluralize(count) {
|
|
15058
15058
|
const one = count === 1;
|
|
@@ -23743,7 +23743,7 @@ function updateSlot(agent2, patch) {
|
|
|
23743
23743
|
}
|
|
23744
23744
|
function slotIdentity(agent2) {
|
|
23745
23745
|
const t = readTokenFile()[agent2];
|
|
23746
|
-
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null };
|
|
23746
|
+
return { name: t?.name ?? null, voice: t?.voice ?? null, tokenId: t?.token_id ?? null, workspace: t?.workspace ?? null, harness: t?.harness ?? null, sessionId: t?.session_id ?? null };
|
|
23747
23747
|
}
|
|
23748
23748
|
function readToken(agent2 = agentName()) {
|
|
23749
23749
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
@@ -23783,9 +23783,14 @@ async function pollToken(deviceCode) {
|
|
|
23783
23783
|
if (err === "access_denied") throw new Error("Access denied by user. Pairing cancelled.");
|
|
23784
23784
|
throw new Error(`Unexpected error from /api/device/token: ${err}`);
|
|
23785
23785
|
}
|
|
23786
|
-
function ensureAuthed(res) {
|
|
23787
|
-
if (res.status
|
|
23788
|
-
|
|
23786
|
+
async function ensureAuthed(res) {
|
|
23787
|
+
if (res.status !== 401) return res;
|
|
23788
|
+
let body = null;
|
|
23789
|
+
try {
|
|
23790
|
+
body = await res.json();
|
|
23791
|
+
} catch {
|
|
23792
|
+
}
|
|
23793
|
+
throw body?.error === "revoked" ? new RevokedError() : new UnpairedError();
|
|
23789
23794
|
}
|
|
23790
23795
|
function overrideToken(secret) {
|
|
23791
23796
|
tokenOverride = secret;
|
|
@@ -23795,7 +23800,7 @@ async function fail(what, res) {
|
|
|
23795
23800
|
}
|
|
23796
23801
|
async function createGoal(input, opts = {}) {
|
|
23797
23802
|
const token = authToken(opts.token) ?? "";
|
|
23798
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
|
|
23803
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals`, {
|
|
23799
23804
|
method: "POST",
|
|
23800
23805
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" },
|
|
23801
23806
|
body: JSON.stringify(input)
|
|
@@ -23805,13 +23810,14 @@ async function createGoal(input, opts = {}) {
|
|
|
23805
23810
|
}
|
|
23806
23811
|
async function claimGoal(goalId, opts = {}) {
|
|
23807
23812
|
const token = authToken(opts.token) ?? "";
|
|
23808
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
23813
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/claim`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify(goalId ? { goalId } : {}) }));
|
|
23809
23814
|
if (!res.ok) await fail("claim_goal", res);
|
|
23810
23815
|
return await res.json();
|
|
23811
23816
|
}
|
|
23812
23817
|
async function getGoal(goalId, opts = {}, read = {}) {
|
|
23813
23818
|
const token = authToken(opts.token) ?? "";
|
|
23814
|
-
const
|
|
23819
|
+
const query = [read.history ? "history=1" : "", read.diagnose ? "diagnose=1" : ""].filter(Boolean).join("&");
|
|
23820
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${query ? `?${query}` : ""}`, {
|
|
23815
23821
|
method: "GET",
|
|
23816
23822
|
headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
|
|
23817
23823
|
}));
|
|
@@ -23821,12 +23827,12 @@ async function getGoal(goalId, opts = {}, read = {}) {
|
|
|
23821
23827
|
async function updateGoal(goalId, input, opts = {}) {
|
|
23822
23828
|
const token = authToken(opts.token) ?? "";
|
|
23823
23829
|
const operationId = input.operationId ?? randomUUID2();
|
|
23824
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
|
|
23830
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, { method: "PATCH", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }, body: JSON.stringify({ ...input, operationId }) }));
|
|
23825
23831
|
if (!res.ok) await fail("update_goal", res);
|
|
23826
23832
|
return await res.json();
|
|
23827
23833
|
}
|
|
23828
23834
|
async function hatch(name, voice = null) {
|
|
23829
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
|
|
23835
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
|
|
23830
23836
|
method: "POST",
|
|
23831
23837
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
|
|
23832
23838
|
body: JSON.stringify({ name, voice })
|
|
@@ -23845,19 +23851,27 @@ async function whoAmI(opts = {}) {
|
|
|
23845
23851
|
}
|
|
23846
23852
|
}
|
|
23847
23853
|
async function claimSessions(opts = {}) {
|
|
23848
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
23854
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/claim`, {
|
|
23849
23855
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23850
23856
|
// the HOST's identity
|
|
23851
23857
|
}));
|
|
23852
23858
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
23853
23859
|
return (await res.json()).sessions;
|
|
23854
23860
|
}
|
|
23861
|
+
async function recordDecision(decision, opts = {}) {
|
|
23862
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
23863
|
+
method: "POST",
|
|
23864
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23865
|
+
body: JSON.stringify(decision)
|
|
23866
|
+
}));
|
|
23867
|
+
if (!res.ok) await fail("record_decision", res);
|
|
23868
|
+
}
|
|
23855
23869
|
async function heartbeat(runtime, opts = {}) {
|
|
23856
23870
|
const body = {
|
|
23857
23871
|
...runtime !== void 0 ? { runtime } : {},
|
|
23858
23872
|
...opts.activity !== void 0 ? { activity: opts.activity } : {}
|
|
23859
23873
|
};
|
|
23860
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
23874
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/presence`, {
|
|
23861
23875
|
method: "POST",
|
|
23862
23876
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23863
23877
|
...Object.keys(body).length > 0 ? { body: JSON.stringify(body) } : {}
|
|
@@ -23865,7 +23879,7 @@ async function heartbeat(runtime, opts = {}) {
|
|
|
23865
23879
|
if (!res.ok) throw new Error(`heartbeat failed: ${res.status}`);
|
|
23866
23880
|
}
|
|
23867
23881
|
async function registerDelivery(mode, opts = {}) {
|
|
23868
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
23882
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/delivery`, {
|
|
23869
23883
|
method: "POST",
|
|
23870
23884
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23871
23885
|
body: JSON.stringify({ mode })
|
|
@@ -23874,21 +23888,21 @@ async function registerDelivery(mode, opts = {}) {
|
|
|
23874
23888
|
return await res.json();
|
|
23875
23889
|
}
|
|
23876
23890
|
async function listNotes(opts = {}) {
|
|
23877
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
23891
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
|
|
23878
23892
|
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23879
23893
|
}));
|
|
23880
23894
|
if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
|
|
23881
23895
|
return await res.json();
|
|
23882
23896
|
}
|
|
23883
23897
|
async function listConnections(opts = {}) {
|
|
23884
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
23898
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
|
|
23885
23899
|
headers: { authorization: `Bearer ${authToken(opts.token)}`, "x-paigy-model": "goal-entry-v1" }
|
|
23886
23900
|
}));
|
|
23887
23901
|
if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
|
|
23888
23902
|
return await res.json();
|
|
23889
23903
|
}
|
|
23890
23904
|
async function submitTriage(run, opts = {}) {
|
|
23891
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
23905
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
|
|
23892
23906
|
method: "POST",
|
|
23893
23907
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23894
23908
|
body: JSON.stringify(run)
|
|
@@ -23897,7 +23911,7 @@ async function submitTriage(run, opts = {}) {
|
|
|
23897
23911
|
return await res.json();
|
|
23898
23912
|
}
|
|
23899
23913
|
async function acceptTriage(proposalId, body, opts = {}) {
|
|
23900
|
-
const res = ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
|
|
23914
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
|
|
23901
23915
|
method: "POST",
|
|
23902
23916
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23903
23917
|
body: JSON.stringify(body)
|
|
@@ -23952,7 +23966,7 @@ async function contact(input, opts = {}) {
|
|
|
23952
23966
|
ask: a.ask,
|
|
23953
23967
|
options: a.options ?? []
|
|
23954
23968
|
}));
|
|
23955
|
-
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
23969
|
+
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
23956
23970
|
method: "POST",
|
|
23957
23971
|
headers,
|
|
23958
23972
|
signal: opts.signal,
|
|
@@ -23974,15 +23988,17 @@ async function contact(input, opts = {}) {
|
|
|
23974
23988
|
}
|
|
23975
23989
|
const all = deliveries.filter((d) => !!d.deliveryId && !!d.goalId).map(({ id, deliveryId: deliveryId2, goalId }) => ({ ...id ? { id } : {}, deliveryId: deliveryId2, goalId }));
|
|
23976
23990
|
const joinedCard = deliveries[0]?.joinedCard === true;
|
|
23991
|
+
const joinedCall = deliveries[0]?.joinedCall === true;
|
|
23977
23992
|
const read = async (signal2) => {
|
|
23978
|
-
const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
23993
|
+
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
|
|
23979
23994
|
if (!res.ok) await fail("read_delivery", res);
|
|
23980
23995
|
const one = await res.json();
|
|
23981
23996
|
return {
|
|
23982
23997
|
...one,
|
|
23983
23998
|
...all.length > 1 ? { deliveries: all } : {},
|
|
23984
23999
|
...recorded.length ? { recorded, notSent: NOT_SENT } : {},
|
|
23985
|
-
...joinedCard ? { joinedCard: true } : {}
|
|
24000
|
+
...joinedCard ? { joinedCard: true } : {},
|
|
24001
|
+
...joinedCall ? { joinedCall: true } : {}
|
|
23986
24002
|
};
|
|
23987
24003
|
};
|
|
23988
24004
|
const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
|
|
@@ -24004,7 +24020,7 @@ async function contact(input, opts = {}) {
|
|
|
24004
24020
|
}
|
|
24005
24021
|
}
|
|
24006
24022
|
async function checkReplies(opts = {}) {
|
|
24007
|
-
const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
24023
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/deliveries`, {
|
|
24008
24024
|
headers: { authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" }
|
|
24009
24025
|
}));
|
|
24010
24026
|
if (!res.ok) await fail("check_replies", res);
|
|
@@ -24170,7 +24186,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
24170
24186
|
}
|
|
24171
24187
|
};
|
|
24172
24188
|
}
|
|
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,
|
|
24189
|
+
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, HistoryWorkSchema, HistoryEntrySchema, HistoryPageSchema, 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, HostDecisionSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, RevokedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
|
|
24174
24190
|
var init_dist = __esm({
|
|
24175
24191
|
"../../packages/sdk/dist/index.js"() {
|
|
24176
24192
|
"use strict";
|
|
@@ -24583,18 +24599,18 @@ var init_dist = __esm({
|
|
|
24583
24599
|
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."),
|
|
24584
24600
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
24585
24601
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
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
|
|
24602
|
+
"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 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
24587
24603
|
),
|
|
24588
|
-
options: external_exports.array(OptionInputSchema).min(
|
|
24604
|
+
options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
|
|
24589
24605
|
}).strict();
|
|
24590
24606
|
StartContactSchema = external_exports.object({
|
|
24591
24607
|
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."),
|
|
24592
|
-
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
24608
|
+
waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
24593
24609
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
24594
24610
|
}).strict();
|
|
24595
24611
|
ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
24596
24612
|
CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
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
|
|
24613
|
+
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, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. 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). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
24598
24614
|
CreateGoalSchema = external_exports.object({
|
|
24599
24615
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
24600
24616
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -24632,7 +24648,11 @@ var init_dist = __esm({
|
|
|
24632
24648
|
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
24633
24649
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
24634
24650
|
reviewed: external_exports.literal(true).optional(),
|
|
24635
|
-
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
|
|
24651
|
+
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
|
|
24652
|
+
/** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
|
|
24653
|
+
* (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
|
|
24654
|
+
* by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
|
|
24655
|
+
withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
|
|
24636
24656
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
24637
24657
|
reason: external_exports.string().trim().min(1).max(2e3),
|
|
24638
24658
|
operationId: external_exports.string().uuid().optional()
|
|
@@ -24643,10 +24663,14 @@ var init_dist = __esm({
|
|
|
24643
24663
|
goalId: external_exports.string().uuid(),
|
|
24644
24664
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
24645
24665
|
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
24646
|
-
history: external_exports.boolean().optional()
|
|
24666
|
+
history: external_exports.boolean().optional(),
|
|
24667
|
+
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
24668
|
+
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
24669
|
+
* between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
|
|
24670
|
+
diagnose: external_exports.boolean().optional()
|
|
24647
24671
|
}).strict();
|
|
24648
|
-
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), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
|
|
24649
|
-
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.`;
|
|
24672
|
+
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), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
24673
|
+
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. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. 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. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
24650
24674
|
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.";
|
|
24651
24675
|
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.";
|
|
24652
24676
|
CheckRepliesSchema = external_exports.object({}).strict();
|
|
@@ -25250,18 +25274,20 @@ var init_dist = __esm({
|
|
|
25250
25274
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
25251
25275
|
broker: BrokerTuningSchema.optional()
|
|
25252
25276
|
});
|
|
25253
|
-
|
|
25277
|
+
HistoryWorkSchema = external_exports.object({
|
|
25254
25278
|
id: external_exports.string(),
|
|
25255
|
-
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
25256
|
-
initiator: external_exports.enum(["user", "agent"]),
|
|
25257
25279
|
title: external_exports.string(),
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25280
|
+
state: external_exports.enum(["done", "cancelled"]),
|
|
25281
|
+
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
25282
|
+
assignee: external_exports.string()
|
|
25283
|
+
});
|
|
25284
|
+
HistoryEntrySchema = external_exports.union([
|
|
25285
|
+
external_exports.object({ at: external_exports.string(), card: InboxItemSchema }),
|
|
25286
|
+
external_exports.object({ at: external_exports.string(), work: HistoryWorkSchema })
|
|
25287
|
+
]);
|
|
25288
|
+
HistoryPageSchema = external_exports.object({
|
|
25289
|
+
entries: external_exports.array(HistoryEntrySchema),
|
|
25290
|
+
next: external_exports.string().nullable()
|
|
25265
25291
|
});
|
|
25266
25292
|
ACTIVITY_LINES = 2;
|
|
25267
25293
|
ACTIVITY_LINE_MAX = 80;
|
|
@@ -25445,6 +25471,9 @@ var init_dist = __esm({
|
|
|
25445
25471
|
progressLine: external_exports.string().nullable().optional(),
|
|
25446
25472
|
reviewPending: external_exports.boolean().default(false),
|
|
25447
25473
|
dueAt: external_exports.string().nullable().default(null),
|
|
25474
|
+
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
25475
|
+
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
25476
|
+
finishedAt: external_exports.string().nullable().optional(),
|
|
25448
25477
|
/** The Goal this one was opened under; null at the root. */
|
|
25449
25478
|
parentGoalId: external_exports.string().nullable().default(null),
|
|
25450
25479
|
/** Goals opened under this one — only those the same list holds. */
|
|
@@ -25453,8 +25482,17 @@ var init_dist = __esm({
|
|
|
25453
25482
|
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
25454
25483
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
25455
25484
|
blocked: external_exports.boolean().default(false),
|
|
25456
|
-
/**
|
|
25485
|
+
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
25486
|
+
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
25487
|
+
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
25488
|
+
* question is opened. */
|
|
25457
25489
|
questions: external_exports.array(QueueQuestionSchema).default([]),
|
|
25490
|
+
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
25491
|
+
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
25492
|
+
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
25493
|
+
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
25494
|
+
asked: external_exports.number().optional(),
|
|
25495
|
+
answered: external_exports.number().optional(),
|
|
25458
25496
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
25459
25497
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
25460
25498
|
* not spell an empty list. */
|
|
@@ -25591,6 +25629,19 @@ var init_dist = __esm({
|
|
|
25591
25629
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
25592
25630
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
25593
25631
|
});
|
|
25632
|
+
HostDecisionSchema = external_exports.object({
|
|
25633
|
+
/** The agent's token id: the row's `recipient`. */
|
|
25634
|
+
agent: external_exports.string().uuid(),
|
|
25635
|
+
decision: external_exports.enum(["stood_back", "took_over"]),
|
|
25636
|
+
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
25637
|
+
goalId: external_exports.string().uuid().nullable().optional(),
|
|
25638
|
+
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
25639
|
+
seenAt: external_exports.string().datetime().nullable().optional(),
|
|
25640
|
+
/** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
|
|
25641
|
+
since: external_exports.string().datetime().nullable().optional(),
|
|
25642
|
+
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
25643
|
+
said: external_exports.string().max(300).optional()
|
|
25644
|
+
});
|
|
25594
25645
|
WakeNudgeSchema = external_exports.object({
|
|
25595
25646
|
kind: external_exports.enum(["reply", "request", "callback"]),
|
|
25596
25647
|
notificationId: external_exports.string().optional(),
|
|
@@ -25635,6 +25686,9 @@ var init_dist = __esm({
|
|
|
25635
25686
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
25636
25687
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
25637
25688
|
workspace: external_exports.string().nullable().optional(),
|
|
25689
|
+
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
25690
|
+
harness: external_exports.enum(["claude", "codex", "agy"]).optional(),
|
|
25691
|
+
session_id: external_exports.string().uuid().optional(),
|
|
25638
25692
|
uik_pub: external_exports.string().nullable().optional()
|
|
25639
25693
|
});
|
|
25640
25694
|
SupportRequestSchema = external_exports.object({
|
|
@@ -25763,6 +25817,13 @@ var init_dist = __esm({
|
|
|
25763
25817
|
this.name = "UnpairedError";
|
|
25764
25818
|
}
|
|
25765
25819
|
};
|
|
25820
|
+
RevokedError = class extends UnpairedError {
|
|
25821
|
+
constructor() {
|
|
25822
|
+
super();
|
|
25823
|
+
this.message = "This identity was revoked (the API answered 401 revoked): its credential is gone for good. Pairing again makes a new identity.";
|
|
25824
|
+
this.name = "RevokedError";
|
|
25825
|
+
}
|
|
25826
|
+
};
|
|
25766
25827
|
ApiError = class extends Error {
|
|
25767
25828
|
status;
|
|
25768
25829
|
body;
|
|
@@ -30358,132 +30419,6 @@ var require_lib = __commonJS({
|
|
|
30358
30419
|
}
|
|
30359
30420
|
});
|
|
30360
30421
|
|
|
30361
|
-
// src/triage/judge.ts
|
|
30362
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
30363
|
-
import { homedir as homedir9 } from "os";
|
|
30364
|
-
import { delimiter as delimiter3 } from "path";
|
|
30365
|
-
function pickOllamaModel(tags) {
|
|
30366
|
-
if (tags.length === 0) return null;
|
|
30367
|
-
const score = (tag) => {
|
|
30368
|
-
const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
|
|
30369
|
-
return i === -1 ? PREFERRED.length : i;
|
|
30370
|
-
};
|
|
30371
|
-
return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
|
|
30372
|
-
}
|
|
30373
|
-
function parseOllamaTags(body) {
|
|
30374
|
-
const models = body?.models;
|
|
30375
|
-
if (!Array.isArray(models)) return [];
|
|
30376
|
-
return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
|
|
30377
|
-
}
|
|
30378
|
-
async function fetchTags(host) {
|
|
30379
|
-
try {
|
|
30380
|
-
const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
30381
|
-
if (!res.ok) return null;
|
|
30382
|
-
return parseOllamaTags(await res.json());
|
|
30383
|
-
} catch {
|
|
30384
|
-
return null;
|
|
30385
|
-
}
|
|
30386
|
-
}
|
|
30387
|
-
async function available(deps = {}) {
|
|
30388
|
-
const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
|
|
30389
|
-
const model = tags ? pickOllamaModel(tags) : null;
|
|
30390
|
-
const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
|
|
30391
|
-
const detected = (deps.detect ?? detectAll)();
|
|
30392
|
-
const harnesses = CATALOG.map((entry) => {
|
|
30393
|
-
const state = detected.find((d) => d.name === entry.name);
|
|
30394
|
-
const usable = state?.status === "ready" || state?.status === "adapter-missing";
|
|
30395
|
-
return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
|
|
30396
|
-
});
|
|
30397
|
-
return [ollama, ...harnesses];
|
|
30398
|
-
}
|
|
30399
|
-
function refusal(options, asked2) {
|
|
30400
|
-
const lines2 = [
|
|
30401
|
-
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.",
|
|
30402
|
-
"",
|
|
30403
|
-
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
30404
|
-
];
|
|
30405
|
-
for (const o of options) {
|
|
30406
|
-
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
30407
|
-
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
30408
|
-
}
|
|
30409
|
-
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
30410
|
-
return lines2.join("\n");
|
|
30411
|
-
}
|
|
30412
|
-
async function judgeFor(asked2, deps = {}) {
|
|
30413
|
-
const options = await available(deps);
|
|
30414
|
-
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
30415
|
-
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
30416
|
-
if (chosen.provider === "ollama") {
|
|
30417
|
-
const model = chosen.model;
|
|
30418
|
-
return {
|
|
30419
|
-
provider: "ollama",
|
|
30420
|
-
model,
|
|
30421
|
-
run: async (prompt) => {
|
|
30422
|
-
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
|
30423
|
-
method: "POST",
|
|
30424
|
-
headers: { "content-type": "application/json" },
|
|
30425
|
-
// Non-streaming: we want the whole answer once, and the parser is the same
|
|
30426
|
-
// either way. `temperature: 0` because this is a classification, not prose.
|
|
30427
|
-
body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
|
|
30428
|
-
signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
|
|
30429
|
-
});
|
|
30430
|
-
if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
|
|
30431
|
-
return (await res.json()).response ?? "";
|
|
30432
|
-
}
|
|
30433
|
-
};
|
|
30434
|
-
}
|
|
30435
|
-
const runner = RUNNERS[chosen.provider];
|
|
30436
|
-
if (!runner) return { error: refusal(options, asked2) };
|
|
30437
|
-
const bin = which(runner.cli);
|
|
30438
|
-
if (!bin) return { error: refusal(options, asked2) };
|
|
30439
|
-
return {
|
|
30440
|
-
provider: chosen.provider,
|
|
30441
|
-
model: null,
|
|
30442
|
-
run: async (prompt) => {
|
|
30443
|
-
const out = spawnSync2(bin, runner.argv(null), {
|
|
30444
|
-
input: prompt,
|
|
30445
|
-
encoding: "utf8",
|
|
30446
|
-
cwd: homedir9(),
|
|
30447
|
-
timeout: RUN_TIMEOUT_MS,
|
|
30448
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
30449
|
-
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
30450
|
-
});
|
|
30451
|
-
if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
|
|
30452
|
-
if (out.status !== 0 && !out.stdout?.trim())
|
|
30453
|
-
throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
|
|
30454
|
-
return out.stdout ?? "";
|
|
30455
|
-
}
|
|
30456
|
-
};
|
|
30457
|
-
}
|
|
30458
|
-
var PRIVACY, OLLAMA_HOST, PREFERRED, RUNNERS, RUN_TIMEOUT_MS;
|
|
30459
|
-
var init_judge = __esm({
|
|
30460
|
-
"src/triage/judge.ts"() {
|
|
30461
|
-
"use strict";
|
|
30462
|
-
init_catalog();
|
|
30463
|
-
PRIVACY = {
|
|
30464
|
-
ollama: "never leaves this machine",
|
|
30465
|
-
claude: "this machine + your own Claude subscription; Paigy's servers never see it",
|
|
30466
|
-
codex: "this machine + your own Codex subscription; Paigy's servers never see it",
|
|
30467
|
-
agy: "this machine + your own Antigravity account; Paigy's servers never see it"
|
|
30468
|
-
};
|
|
30469
|
-
OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
30470
|
-
PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
|
|
30471
|
-
RUNNERS = {
|
|
30472
|
-
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
30473
|
-
ollama: null,
|
|
30474
|
-
// `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
|
|
30475
|
-
claude: { cli: "claude", argv: () => ["-p"] },
|
|
30476
|
-
// `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
|
|
30477
|
-
// when none is given as an argument. It prints its own preamble around the answer, which
|
|
30478
|
-
// is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
|
|
30479
|
-
codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
|
|
30480
|
-
// `agy --print` (alias -p) is Antigravity's print mode, text output by default.
|
|
30481
|
-
agy: { cli: "agy", argv: () => ["-p"] }
|
|
30482
|
-
};
|
|
30483
|
-
RUN_TIMEOUT_MS = 5 * 6e4;
|
|
30484
|
-
}
|
|
30485
|
-
});
|
|
30486
|
-
|
|
30487
30422
|
// src/triage/evidence.ts
|
|
30488
30423
|
function assigneeToken(note2) {
|
|
30489
30424
|
const ref = note2.assignee ?? "";
|
|
@@ -30659,6 +30594,132 @@ var init_evidence = __esm({
|
|
|
30659
30594
|
}
|
|
30660
30595
|
});
|
|
30661
30596
|
|
|
30597
|
+
// src/triage/judge.ts
|
|
30598
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
30599
|
+
import { homedir as homedir5 } from "os";
|
|
30600
|
+
import { delimiter as delimiter3 } from "path";
|
|
30601
|
+
function pickOllamaModel(tags) {
|
|
30602
|
+
if (tags.length === 0) return null;
|
|
30603
|
+
const score = (tag) => {
|
|
30604
|
+
const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
|
|
30605
|
+
return i === -1 ? PREFERRED.length : i;
|
|
30606
|
+
};
|
|
30607
|
+
return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
|
|
30608
|
+
}
|
|
30609
|
+
function parseOllamaTags(body) {
|
|
30610
|
+
const models = body?.models;
|
|
30611
|
+
if (!Array.isArray(models)) return [];
|
|
30612
|
+
return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
|
|
30613
|
+
}
|
|
30614
|
+
async function fetchTags(host) {
|
|
30615
|
+
try {
|
|
30616
|
+
const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
30617
|
+
if (!res.ok) return null;
|
|
30618
|
+
return parseOllamaTags(await res.json());
|
|
30619
|
+
} catch {
|
|
30620
|
+
return null;
|
|
30621
|
+
}
|
|
30622
|
+
}
|
|
30623
|
+
async function available(deps = {}) {
|
|
30624
|
+
const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
|
|
30625
|
+
const model = tags ? pickOllamaModel(tags) : null;
|
|
30626
|
+
const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
|
|
30627
|
+
const detected = (deps.detect ?? detectAll)();
|
|
30628
|
+
const harnesses = CATALOG.map((entry) => {
|
|
30629
|
+
const state = detected.find((d) => d.name === entry.name);
|
|
30630
|
+
const usable = state?.status === "ready" || state?.status === "adapter-missing";
|
|
30631
|
+
return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
|
|
30632
|
+
});
|
|
30633
|
+
return [ollama, ...harnesses];
|
|
30634
|
+
}
|
|
30635
|
+
function refusal(options, asked2) {
|
|
30636
|
+
const lines2 = [
|
|
30637
|
+
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.",
|
|
30638
|
+
"",
|
|
30639
|
+
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
30640
|
+
];
|
|
30641
|
+
for (const o of options) {
|
|
30642
|
+
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
30643
|
+
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
30644
|
+
}
|
|
30645
|
+
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
30646
|
+
return lines2.join("\n");
|
|
30647
|
+
}
|
|
30648
|
+
async function judgeFor(asked2, deps = {}) {
|
|
30649
|
+
const options = await available(deps);
|
|
30650
|
+
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
30651
|
+
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
30652
|
+
if (chosen.provider === "ollama") {
|
|
30653
|
+
const model = chosen.model;
|
|
30654
|
+
return {
|
|
30655
|
+
provider: "ollama",
|
|
30656
|
+
model,
|
|
30657
|
+
run: async (prompt) => {
|
|
30658
|
+
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
|
30659
|
+
method: "POST",
|
|
30660
|
+
headers: { "content-type": "application/json" },
|
|
30661
|
+
// Non-streaming: we want the whole answer once, and the parser is the same
|
|
30662
|
+
// either way. `temperature: 0` because this is a classification, not prose.
|
|
30663
|
+
body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
|
|
30664
|
+
signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
|
|
30665
|
+
});
|
|
30666
|
+
if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
|
|
30667
|
+
return (await res.json()).response ?? "";
|
|
30668
|
+
}
|
|
30669
|
+
};
|
|
30670
|
+
}
|
|
30671
|
+
const runner = RUNNERS[chosen.provider];
|
|
30672
|
+
if (!runner) return { error: refusal(options, asked2) };
|
|
30673
|
+
const bin = which(runner.cli);
|
|
30674
|
+
if (!bin) return { error: refusal(options, asked2) };
|
|
30675
|
+
return {
|
|
30676
|
+
provider: chosen.provider,
|
|
30677
|
+
model: null,
|
|
30678
|
+
run: async (prompt) => {
|
|
30679
|
+
const out = spawnSync2(bin, runner.argv(null), {
|
|
30680
|
+
input: prompt,
|
|
30681
|
+
encoding: "utf8",
|
|
30682
|
+
cwd: homedir5(),
|
|
30683
|
+
timeout: RUN_TIMEOUT_MS,
|
|
30684
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
30685
|
+
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
30686
|
+
});
|
|
30687
|
+
if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
|
|
30688
|
+
if (out.status !== 0 && !out.stdout?.trim())
|
|
30689
|
+
throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
|
|
30690
|
+
return out.stdout ?? "";
|
|
30691
|
+
}
|
|
30692
|
+
};
|
|
30693
|
+
}
|
|
30694
|
+
var PRIVACY, OLLAMA_HOST, PREFERRED, RUNNERS, RUN_TIMEOUT_MS;
|
|
30695
|
+
var init_judge = __esm({
|
|
30696
|
+
"src/triage/judge.ts"() {
|
|
30697
|
+
"use strict";
|
|
30698
|
+
init_catalog();
|
|
30699
|
+
PRIVACY = {
|
|
30700
|
+
ollama: "never leaves this machine",
|
|
30701
|
+
claude: "this machine + your own Claude subscription; Paigy's servers never see it",
|
|
30702
|
+
codex: "this machine + your own Codex subscription; Paigy's servers never see it",
|
|
30703
|
+
agy: "this machine + your own Antigravity account; Paigy's servers never see it"
|
|
30704
|
+
};
|
|
30705
|
+
OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
30706
|
+
PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
|
|
30707
|
+
RUNNERS = {
|
|
30708
|
+
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
30709
|
+
ollama: null,
|
|
30710
|
+
// `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
|
|
30711
|
+
claude: { cli: "claude", argv: () => ["-p"] },
|
|
30712
|
+
// `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
|
|
30713
|
+
// when none is given as an argument. It prints its own preamble around the answer, which
|
|
30714
|
+
// is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
|
|
30715
|
+
codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
|
|
30716
|
+
// `agy --print` (alias -p) is Antigravity's print mode, text output by default.
|
|
30717
|
+
agy: { cli: "agy", argv: () => ["-p"] }
|
|
30718
|
+
};
|
|
30719
|
+
RUN_TIMEOUT_MS = 5 * 6e4;
|
|
30720
|
+
}
|
|
30721
|
+
});
|
|
30722
|
+
|
|
30662
30723
|
// src/triage/verdict.ts
|
|
30663
30724
|
function extractJson(raw) {
|
|
30664
30725
|
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
@@ -32861,18 +32922,18 @@ var AskInputSchema2 = external_exports.object({
|
|
|
32861
32922
|
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."),
|
|
32862
32923
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
32863
32924
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
32864
|
-
"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
|
|
32925
|
+
"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 each question a Question on the one Goal the ask lands on, never a Goal of its own \u2014 but the words it splits are its reading, not yours. News, progress and findings are their own contact; ANY contact for a person already on a call JOINS that call, whatever channel you asked for, so several arrive as one call."
|
|
32865
32926
|
),
|
|
32866
|
-
options: external_exports.array(OptionInputSchema2).min(
|
|
32927
|
+
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
|
|
32867
32928
|
}).strict();
|
|
32868
32929
|
var StartContactSchema2 = external_exports.object({
|
|
32869
32930
|
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."),
|
|
32870
|
-
waiting: external_exports.enum(["none", "hard"]).default("none"),
|
|
32931
|
+
waiting: external_exports.enum(["none", "hard"]).default("none").describe("hard requests a call even when channel is notification; user permissions and ring cooldowns still apply."),
|
|
32871
32932
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
32872
32933
|
}).strict();
|
|
32873
32934
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
32874
32935
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
32875
|
-
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
|
|
32936
|
+
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, every one on the Goal the ask lands on; separate pieces of work are separate asks. An ask with no options that is not blocking is a report, and a report is an UPDATE, never a claim on them: it reaches them and is listed apart from what waits on them, so no answer is owed and none should be awaited. 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). Send options (or waiting:'hard') when you actually need an answer. If the person is already on a call, your contact joins that call automatically \u2014 whatever channel you asked for, with no ring \u2014 and the Delivery it returns IS that call: reread it with contact({deliveryId}) to see everything answered on it so far, and contact again while it is live to add information or a further question to the same call.";
|
|
32876
32937
|
var CreateGoalSchema2 = external_exports.object({
|
|
32877
32938
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
32878
32939
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -32910,7 +32971,11 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
32910
32971
|
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
32911
32972
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
32912
32973
|
reviewed: external_exports.literal(true).optional(),
|
|
32913
|
-
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional()
|
|
32974
|
+
dueAt: external_exports.string().datetime({ offset: true }).nullable().optional(),
|
|
32975
|
+
/** WITHDRAW YOUR OWN QUESTION (#2777, owner 2026-09-30). The question's id as every read shows it
|
|
32976
|
+
* (the conversation's `id`, eight characters, or the whole request Entry id), on THIS Goal, asked
|
|
32977
|
+
* by you and still open. It is cancelled, not answered: its cards close and nothing rings for it. */
|
|
32978
|
+
withdraw: external_exports.array(external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/)).min(1).max(10).optional()
|
|
32914
32979
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
32915
32980
|
reason: external_exports.string().trim().min(1).max(2e3),
|
|
32916
32981
|
operationId: external_exports.string().uuid().optional()
|
|
@@ -32921,10 +32986,14 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
32921
32986
|
goalId: external_exports.string().uuid(),
|
|
32922
32987
|
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
32923
32988
|
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
32924
|
-
history: external_exports.boolean().optional()
|
|
32989
|
+
history: external_exports.boolean().optional(),
|
|
32990
|
+
/** WHEN A READ HAS CONFUSED YOU. Adds `diagnosis`: every reader that already answers a question
|
|
32991
|
+
* about this work, each answer attributed to the reader that gave it, and every disagreement
|
|
32992
|
+
* between two of them named. Off by default; `apps/api/src/goal/diagnose-design.md`. */
|
|
32993
|
+
diagnose: external_exports.boolean().optional()
|
|
32925
32994
|
}).strict();
|
|
32926
|
-
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), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
|
|
32927
|
-
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.`;
|
|
32995
|
+
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), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
|
|
32996
|
+
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. withdraw: [questionId] takes back a question YOU asked on this Goal that is still open \u2014 because you acted on it yourself, it no longer matters, or you asked it wrongly (e.g. waiting: hard when nothing was blocked): it is cancelled, not answered, its card closes and it stops ringing; the id is the one the conversation shows. 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. state: done while children are still open records your part as done: the Goal waits and closes by itself when its last open child closes. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
|
|
32928
32997
|
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.";
|
|
32929
32998
|
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.";
|
|
32930
32999
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
@@ -32937,7 +33006,6 @@ var AGENT_TOOLS2 = [
|
|
|
32937
33006
|
{ name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
|
|
32938
33007
|
];
|
|
32939
33008
|
var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
|
|
32940
|
-
var DONE_MESSAGE = "When the work is done, send one message through contact saying what is done and anything the person needs to do or check.";
|
|
32941
33009
|
function entryWords(entry) {
|
|
32942
33010
|
const content = entry.content;
|
|
32943
33011
|
if (content && "sealed" in content) return "";
|
|
@@ -33544,18 +33612,20 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33544
33612
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
33545
33613
|
broker: BrokerTuningSchema2.optional()
|
|
33546
33614
|
});
|
|
33547
|
-
var
|
|
33615
|
+
var HistoryWorkSchema2 = external_exports.object({
|
|
33548
33616
|
id: external_exports.string(),
|
|
33549
|
-
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
33550
|
-
initiator: external_exports.enum(["user", "agent"]),
|
|
33551
33617
|
title: external_exports.string(),
|
|
33552
|
-
|
|
33553
|
-
|
|
33554
|
-
|
|
33555
|
-
|
|
33556
|
-
|
|
33557
|
-
|
|
33558
|
-
|
|
33618
|
+
state: external_exports.enum(["done", "cancelled"]),
|
|
33619
|
+
/** Who held it (`agent:<tokenId>` or `human:<userId>`). */
|
|
33620
|
+
assignee: external_exports.string()
|
|
33621
|
+
});
|
|
33622
|
+
var HistoryEntrySchema2 = external_exports.union([
|
|
33623
|
+
external_exports.object({ at: external_exports.string(), card: InboxItemSchema2 }),
|
|
33624
|
+
external_exports.object({ at: external_exports.string(), work: HistoryWorkSchema2 })
|
|
33625
|
+
]);
|
|
33626
|
+
var HistoryPageSchema2 = external_exports.object({
|
|
33627
|
+
entries: external_exports.array(HistoryEntrySchema2),
|
|
33628
|
+
next: external_exports.string().nullable()
|
|
33559
33629
|
});
|
|
33560
33630
|
var ACTIVITY_LINES2 = 2;
|
|
33561
33631
|
var ACTIVITY_LINE_MAX2 = 80;
|
|
@@ -33739,6 +33809,9 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33739
33809
|
progressLine: external_exports.string().nullable().optional(),
|
|
33740
33810
|
reviewPending: external_exports.boolean().default(false),
|
|
33741
33811
|
dueAt: external_exports.string().nullable().default(null),
|
|
33812
|
+
/** WHEN ITS OWNER SAID DONE WHILE CHILDREN WERE OPEN (#2704): its own work is finished and it closes
|
|
33813
|
+
* with its last open child. Null otherwise; optional, so hand-built queues need not spell it. */
|
|
33814
|
+
finishedAt: external_exports.string().nullable().optional(),
|
|
33742
33815
|
/** The Goal this one was opened under; null at the root. */
|
|
33743
33816
|
parentGoalId: external_exports.string().nullable().default(null),
|
|
33744
33817
|
/** Goals opened under this one — only those the same list holds. */
|
|
@@ -33747,8 +33820,17 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33747
33820
|
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
33748
33821
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
33749
33822
|
blocked: external_exports.boolean().default(false),
|
|
33750
|
-
/**
|
|
33823
|
+
/** Its questions: every OPEN one, and at most ten settled, newest settled first
|
|
33824
|
+
* (20260929133308) — the page decides which of them to show. NOT the whole set: `asked` and
|
|
33825
|
+
* `answered` are, and a settled one's words are a line (280 characters), its body read when the
|
|
33826
|
+
* question is opened. */
|
|
33751
33827
|
questions: external_exports.array(QueueQuestionSchema2).default([]),
|
|
33828
|
+
/** HOW MANY QUESTIONS THIS WORK HAS ASKED, and how many are answered — the Goal's own totals,
|
|
33829
|
+
* bounded at 100 server-side. A tally counted off `questions` is a wrong number that looks
|
|
33830
|
+
* right once the cap bites (`walk/trees.ts` `tallyOf`). Optional, and defaulted from the array
|
|
33831
|
+
* by the projection, so hand-built queues (fixtures, the demo) need not spell them. */
|
|
33832
|
+
asked: external_exports.number().optional(),
|
|
33833
|
+
answered: external_exports.number().optional(),
|
|
33752
33834
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
33753
33835
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
33754
33836
|
* not spell an empty list. */
|
|
@@ -33883,6 +33965,19 @@ var DeliveryConfigSchema2 = external_exports.object({
|
|
|
33883
33965
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
33884
33966
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
33885
33967
|
});
|
|
33968
|
+
var HostDecisionSchema2 = external_exports.object({
|
|
33969
|
+
/** The agent's token id: the row's `recipient`. */
|
|
33970
|
+
agent: external_exports.string().uuid(),
|
|
33971
|
+
decision: external_exports.enum(["stood_back", "took_over"]),
|
|
33972
|
+
/** The work it was about: the Goal `claim_goal` would hand that agent next. */
|
|
33973
|
+
goalId: external_exports.string().uuid().nullable().optional(),
|
|
33974
|
+
/** When the server last heard from the agent, as the host read it: the presence it stood back for. */
|
|
33975
|
+
seenAt: external_exports.string().datetime().nullable().optional(),
|
|
33976
|
+
/** When that work last moved (`claimable.since` on `check_replies`), the fact the bound is judged on. */
|
|
33977
|
+
since: external_exports.string().datetime().nullable().optional(),
|
|
33978
|
+
/** What the host said, in its log's own words: why it stood back, or what the take-over did. */
|
|
33979
|
+
said: external_exports.string().max(300).optional()
|
|
33980
|
+
});
|
|
33886
33981
|
var WakeNudgeSchema2 = external_exports.object({
|
|
33887
33982
|
kind: external_exports.enum(["reply", "request", "callback"]),
|
|
33888
33983
|
notificationId: external_exports.string().optional(),
|
|
@@ -33927,6 +34022,9 @@ var DeviceTokenSchema2 = external_exports.object({
|
|
|
33927
34022
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
33928
34023
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
33929
34024
|
workspace: external_exports.string().nullable().optional(),
|
|
34025
|
+
/** Local host recovery must preserve the launch's runtime and Paigy identity. */
|
|
34026
|
+
harness: external_exports.enum(["claude", "codex", "agy"]).optional(),
|
|
34027
|
+
session_id: external_exports.string().uuid().optional(),
|
|
33930
34028
|
uik_pub: external_exports.string().nullable().optional()
|
|
33931
34029
|
});
|
|
33932
34030
|
var SupportRequestSchema2 = external_exports.object({
|
|
@@ -34146,6 +34244,14 @@ var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on
|
|
|
34146
34244
|
var OWED = "That turn was given this when the limit stopped it, and may not have got to it. Act on it now, unless you already have:";
|
|
34147
34245
|
var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
34148
34246
|
var GATHER_MS = 9e4;
|
|
34247
|
+
function outcome(result, paused) {
|
|
34248
|
+
const r = result ?? {};
|
|
34249
|
+
const who = `${paused.length} paused agent${paused.length === 1 ? "" : "s"}: ${names(paused)}`;
|
|
34250
|
+
if (r.sent === false) return `\u26A0 the limit call did not ring (${who}): it was filed as progress, nobody was notified`;
|
|
34251
|
+
if (r.kind === "call" && typeof r.deliveryId === "string") return `\u260E one call for ${who}, placed (delivery ${r.deliveryId})`;
|
|
34252
|
+
if (typeof r.deliveryId === "string") return `\u26A0 the limit call did not ring (${who}): it went out as a card (delivery ${r.deliveryId})`;
|
|
34253
|
+
return `\u26A0 the limit call did not ring (${who}): no delivery came back`;
|
|
34254
|
+
}
|
|
34149
34255
|
function episodes(opts = {}) {
|
|
34150
34256
|
const gather2 = opts.gatherMs ?? GATHER_MS;
|
|
34151
34257
|
const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
|
|
@@ -34177,8 +34283,10 @@ function episodes(opts = {}) {
|
|
|
34177
34283
|
`Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
|
|
34178
34284
|
rest.length ? `It has reset: ${names(episode.running)} ${episode.running.length > 1 ? "are" : "is"} running again, and ${names(rest)} ${rest.length > 1 ? "haven't" : "hasn't"} started again yet.` : `It has reset, and ${episode.paused.length > 1 ? "they have all" : "it has"} carried on.`
|
|
34179
34285
|
].join(" ");
|
|
34180
|
-
|
|
34181
|
-
|
|
34286
|
+
void call(text).then(
|
|
34287
|
+
(result) => log?.(outcome(result, episode.paused)),
|
|
34288
|
+
(error) => log?.(`\u26A0 could not place the limit call: ${error.message}`)
|
|
34289
|
+
);
|
|
34182
34290
|
}, gather2);
|
|
34183
34291
|
}
|
|
34184
34292
|
};
|
|
@@ -34301,7 +34409,7 @@ ${event.result?.trim() || "No result reported."}`, state, rail, opts);
|
|
|
34301
34409
|
|
|
34302
34410
|
// src/paigy/conversation.ts
|
|
34303
34411
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
34304
|
-
var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message: it is kept as progress on this Goal. Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's parentId).
|
|
34412
|
+
var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message: it is kept as progress on this Goal. Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's parentId).";
|
|
34305
34413
|
var SAFETY_MS = 6e4;
|
|
34306
34414
|
var ASK_BEAT_MS = 5e3;
|
|
34307
34415
|
function beat() {
|
|
@@ -34429,9 +34537,9 @@ async function gather(state, rail, held) {
|
|
|
34429
34537
|
var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; claim_goal with its id when you take it up.";
|
|
34430
34538
|
var says = (h) => h.words.length > 0 || h.via === "assigned";
|
|
34431
34539
|
function whatItIs(h) {
|
|
34432
|
-
const
|
|
34433
|
-
const name = (h.title ??
|
|
34434
|
-
return
|
|
34540
|
+
const outcome2 = h.brief.outcome?.trim() ?? "";
|
|
34541
|
+
const name = (h.title ?? outcome2).trim().split("\n")[0].trim();
|
|
34542
|
+
return outcome2 && outcome2 !== name ? [outcome2] : [];
|
|
34435
34543
|
}
|
|
34436
34544
|
var block = (h) => [
|
|
34437
34545
|
...h.via === "assigned" ? [GIVEN] : [],
|
|
@@ -34558,11 +34666,11 @@ async function look(session, state, rail, opts) {
|
|
|
34558
34666
|
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
34559
34667
|
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
34560
34668
|
}
|
|
34561
|
-
const
|
|
34562
|
-
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !==
|
|
34669
|
+
const outcome2 = brief2.outcome?.trim() ?? "";
|
|
34670
|
+
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
34563
34671
|
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
34564
34672
|
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
|
|
34565
|
-
const fullText = [
|
|
34673
|
+
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
34566
34674
|
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
34567
34675
|
return;
|
|
34568
34676
|
}
|
|
@@ -34748,17 +34856,33 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
34748
34856
|
|
|
34749
34857
|
// src/host.ts
|
|
34750
34858
|
init_dist();
|
|
34751
|
-
init_catalog();
|
|
34752
34859
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
34753
|
-
import { homedir as
|
|
34860
|
+
import { homedir as homedir9 } from "os";
|
|
34754
34861
|
import { join as join9 } from "path";
|
|
34755
34862
|
|
|
34863
|
+
// src/broker.ts
|
|
34864
|
+
init_evidence();
|
|
34865
|
+
init_triage();
|
|
34866
|
+
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
34867
|
+
function ago(iso, now) {
|
|
34868
|
+
const ms = now - Date.parse(iso);
|
|
34869
|
+
if (!Number.isFinite(ms) || ms < 6e4) return "just now";
|
|
34870
|
+
const minutes = Math.floor(ms / 6e4);
|
|
34871
|
+
if (minutes < 60) return `${plural2(minutes, "minute")} ago`;
|
|
34872
|
+
const hours = Math.floor(minutes / 60);
|
|
34873
|
+
if (hours < 24) return `${plural2(hours, "hour")} ago`;
|
|
34874
|
+
return `${plural2(Math.floor(hours / 24), "day")} ago`;
|
|
34875
|
+
}
|
|
34876
|
+
|
|
34877
|
+
// src/host.ts
|
|
34878
|
+
init_catalog();
|
|
34879
|
+
|
|
34756
34880
|
// src/workspaces.ts
|
|
34757
34881
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
34758
34882
|
import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
|
|
34759
|
-
import { homedir as
|
|
34883
|
+
import { homedir as homedir6 } from "os";
|
|
34760
34884
|
function workspacesFile() {
|
|
34761
|
-
return join5(
|
|
34885
|
+
return join5(homedir6(), ".paigy", "workspaces.json");
|
|
34762
34886
|
}
|
|
34763
34887
|
function listWorkspaces(deps) {
|
|
34764
34888
|
try {
|
|
@@ -34769,7 +34893,7 @@ function listWorkspaces(deps) {
|
|
|
34769
34893
|
}
|
|
34770
34894
|
}
|
|
34771
34895
|
function addWorkspace(dir, deps) {
|
|
34772
|
-
const next = resolve2(dir.replace(/^~(?=$|\/)/,
|
|
34896
|
+
const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir6()));
|
|
34773
34897
|
const all = listWorkspaces(deps);
|
|
34774
34898
|
if (!all.includes(next)) {
|
|
34775
34899
|
all.push(next);
|
|
@@ -34779,12 +34903,12 @@ function addWorkspace(dir, deps) {
|
|
|
34779
34903
|
return all;
|
|
34780
34904
|
}
|
|
34781
34905
|
function allowed(cwd, deps) {
|
|
34782
|
-
const target = resolve2(cwd.replace(/^~(?=$|\/)/,
|
|
34906
|
+
const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir6()));
|
|
34783
34907
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
34784
34908
|
}
|
|
34785
34909
|
function resolveWakeDir(pinned, deps) {
|
|
34786
34910
|
if (pinned && allowed(pinned, deps)) {
|
|
34787
|
-
const dir = resolve2(pinned.replace(/^~(?=$|\/)/,
|
|
34911
|
+
const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir6()));
|
|
34788
34912
|
if ((deps.exists ?? existsSync4)(dir)) return dir;
|
|
34789
34913
|
}
|
|
34790
34914
|
return listWorkspaces(deps)[0];
|
|
@@ -34794,7 +34918,7 @@ function resolveWakeDir(pinned, deps) {
|
|
|
34794
34918
|
init_dist();
|
|
34795
34919
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
34796
34920
|
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
34797
|
-
import { homedir as
|
|
34921
|
+
import { homedir as homedir7 } from "os";
|
|
34798
34922
|
import { join as join6 } from "path";
|
|
34799
34923
|
function isAlive(pid) {
|
|
34800
34924
|
try {
|
|
@@ -34825,7 +34949,7 @@ function holds(pid, procStart, deps) {
|
|
|
34825
34949
|
return false;
|
|
34826
34950
|
}
|
|
34827
34951
|
function localHolders(deps = {}) {
|
|
34828
|
-
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(
|
|
34952
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
|
|
34829
34953
|
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
34830
34954
|
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
34831
34955
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
@@ -34855,7 +34979,7 @@ function localHolders(deps = {}) {
|
|
|
34855
34979
|
|
|
34856
34980
|
// src/owner.ts
|
|
34857
34981
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
34858
|
-
import { homedir as
|
|
34982
|
+
import { homedir as homedir8 } from "os";
|
|
34859
34983
|
import { join as join7 } from "path";
|
|
34860
34984
|
var HEAD_BYTES = 64e3;
|
|
34861
34985
|
function readHead(file) {
|
|
@@ -34872,7 +34996,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34872
34996
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
34873
34997
|
if (!m) return null;
|
|
34874
34998
|
const prefix = m[1];
|
|
34875
|
-
const root = deps.projectsDir ?? join7(
|
|
34999
|
+
const root = deps.projectsDir ?? join7(homedir8(), ".claude", "projects");
|
|
34876
35000
|
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
34877
35001
|
const head = deps.head ?? readHead;
|
|
34878
35002
|
let folders;
|
|
@@ -34990,34 +35114,51 @@ function selfUpdater(deps) {
|
|
|
34990
35114
|
}
|
|
34991
35115
|
|
|
34992
35116
|
// src/host.ts
|
|
34993
|
-
var HOST_FILE = join9(
|
|
35117
|
+
var HOST_FILE = join9(homedir9(), ".paigy", "host.json");
|
|
35118
|
+
function wakeHarness(slot, saved, claudeTranscript) {
|
|
35119
|
+
if (slot === "Desktop") return void 0;
|
|
35120
|
+
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
35121
|
+
if (claudeTranscript) return "claude";
|
|
35122
|
+
if (slot === "mcp-agent" || slot === "Claude Code") return "claude";
|
|
35123
|
+
if (slot === "codex") return "codex";
|
|
35124
|
+
if (slot === "antigravity") return "agy";
|
|
35125
|
+
return void 0;
|
|
35126
|
+
}
|
|
34994
35127
|
var runKey = (slot) => `slot:${slot}`;
|
|
34995
35128
|
var claimedRun = (sessionId2) => {
|
|
34996
35129
|
const slot = sessionSlot(sessionId2);
|
|
34997
35130
|
return { slot, key: runKey(slot) };
|
|
34998
35131
|
};
|
|
34999
35132
|
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
35133
|
+
var NOTHING_WAITING = "has nothing waiting";
|
|
35000
35134
|
async function waitingWork(token, session) {
|
|
35001
35135
|
let found;
|
|
35002
35136
|
try {
|
|
35003
35137
|
found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
|
|
35004
35138
|
} catch (error) {
|
|
35139
|
+
if (error instanceof RevokedError) return { skip: "was revoked: its credential is gone for good", revoked: true };
|
|
35140
|
+
if (error instanceof ApiError && error.body?.error === "identity_session_mismatch") {
|
|
35141
|
+
return { skip: "is bound to the session it was paired in, which this host cannot name \u2014 run `paigy-harness handoff` as it, in its folder, to record it" };
|
|
35142
|
+
}
|
|
35005
35143
|
return { skip: `could not read what is waiting (${error.message})` };
|
|
35006
35144
|
}
|
|
35007
|
-
const n = found.deliveries.length;
|
|
35008
|
-
if (n) return { waiting: n === 1 ? "a message" : `${n} messages` };
|
|
35009
35145
|
const next = found.claimable;
|
|
35010
|
-
|
|
35146
|
+
const about = next ? { goalId: next.goalId, ...next.since ? { since: next.since } : {} } : {};
|
|
35147
|
+
const n = found.deliveries.length;
|
|
35148
|
+
if (n) return { waiting: n === 1 ? "a message" : `${n} messages`, ...about };
|
|
35149
|
+
return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}`, ...about } : { skip: NOTHING_WAITING };
|
|
35011
35150
|
}
|
|
35012
35151
|
function evict(runs, key, run) {
|
|
35013
35152
|
if (runs.get(key)?.run === run) runs.delete(key);
|
|
35014
35153
|
}
|
|
35015
35154
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
35155
|
+
var IDLE_HOLDER_MS = 30 * 6e4;
|
|
35016
35156
|
function wakeAction(live, now, serverSeenAt, here = {}) {
|
|
35017
35157
|
if (live) return { action: "skip", why: "run" };
|
|
35018
35158
|
if (here.holder) return { action: "skip", why: "holder" };
|
|
35019
35159
|
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
|
|
35020
35160
|
if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
|
|
35161
|
+
if (here.movedAt !== void 0 && now - here.movedAt >= IDLE_HOLDER_MS) return { action: "spawn", why: "idle" };
|
|
35021
35162
|
return { action: "skip", why: "presence" };
|
|
35022
35163
|
}
|
|
35023
35164
|
return { action: "spawn", why: "nobody" };
|
|
@@ -35027,9 +35168,45 @@ function startHost(opts) {
|
|
|
35027
35168
|
const asHost = { token: opts.token };
|
|
35028
35169
|
const goneAt = /* @__PURE__ */ new Map();
|
|
35029
35170
|
const heldHere = /* @__PURE__ */ new Set();
|
|
35171
|
+
const revoked = /* @__PURE__ */ new Map();
|
|
35172
|
+
const isRevoked = (slot) => {
|
|
35173
|
+
const dead = revoked.get(slot);
|
|
35174
|
+
if (dead === void 0) return false;
|
|
35175
|
+
if (readToken(slot) === dead) return true;
|
|
35176
|
+
revoked.delete(slot);
|
|
35177
|
+
return false;
|
|
35178
|
+
};
|
|
35179
|
+
function drop(slot, token) {
|
|
35180
|
+
if (revoked.get(slot) === token) return;
|
|
35181
|
+
revoked.set(slot, token);
|
|
35182
|
+
const sub = listening.get(slot);
|
|
35183
|
+
if (sub) {
|
|
35184
|
+
listening.delete(slot);
|
|
35185
|
+
void sub.close().catch(() => {
|
|
35186
|
+
});
|
|
35187
|
+
}
|
|
35188
|
+
opts.log(`\u2716 ${slotName(slot) ?? slot} was revoked \u2014 its credential is gone for good, so this host stops presenting it (dropped from the roster; pairing again makes a new identity)`);
|
|
35189
|
+
}
|
|
35190
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
35191
|
+
function decide(slot, decision, found, seenAt, said) {
|
|
35192
|
+
const agent2 = slotIdentity(slot).tokenId;
|
|
35193
|
+
if (!agent2) return;
|
|
35194
|
+
const goalId = found?.goalId;
|
|
35195
|
+
const key = `${decision}:${goalId ?? ""}`;
|
|
35196
|
+
if (recorded.get(slot) === key) return;
|
|
35197
|
+
recorded.set(slot, key);
|
|
35198
|
+
void recordDecision({
|
|
35199
|
+
agent: agent2,
|
|
35200
|
+
decision,
|
|
35201
|
+
goalId: goalId ?? null,
|
|
35202
|
+
seenAt: seenAt === void 0 ? null : new Date(seenAt).toISOString(),
|
|
35203
|
+
since: found?.since ?? null,
|
|
35204
|
+
said: said.slice(0, 300)
|
|
35205
|
+
}, asHost).catch((e) => opts.log(`\u25CC could not record that ${slotName(slot) ?? slot} ${decision.replace("_", " ")} (${e.message})`));
|
|
35206
|
+
}
|
|
35030
35207
|
const refreshIdentities = async () => {
|
|
35031
35208
|
for (const slot of listSlots()) {
|
|
35032
|
-
if (slot === "Desktop") continue;
|
|
35209
|
+
if (slot === "Desktop" || isRevoked(slot)) continue;
|
|
35033
35210
|
const token = readToken(slot);
|
|
35034
35211
|
if (!token) continue;
|
|
35035
35212
|
const me = await whoAmI({ token }).catch(() => null);
|
|
@@ -35067,6 +35244,19 @@ function startHost(opts) {
|
|
|
35067
35244
|
const label = spec.name ?? spec.harness;
|
|
35068
35245
|
const { slot, key } = claimedRun(spec.sessionId);
|
|
35069
35246
|
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
35247
|
+
try {
|
|
35248
|
+
saveToken({
|
|
35249
|
+
access_token: spec.token,
|
|
35250
|
+
name: label,
|
|
35251
|
+
device: null,
|
|
35252
|
+
workspace: spec.workspace,
|
|
35253
|
+
harness: spec.harness,
|
|
35254
|
+
session_id: spec.sessionId
|
|
35255
|
+
}, slot);
|
|
35256
|
+
} catch {
|
|
35257
|
+
log("Cannot persist launch identity/runtime; session was not started. Check local storage and relaunch.");
|
|
35258
|
+
continue;
|
|
35259
|
+
}
|
|
35070
35260
|
const run = runHarness({
|
|
35071
35261
|
harness: spec.harness,
|
|
35072
35262
|
cwd: spec.workspace,
|
|
@@ -35086,14 +35276,9 @@ function startHost(opts) {
|
|
|
35086
35276
|
void heartbeat(void 0, { token: spec.token }).catch(() => {
|
|
35087
35277
|
});
|
|
35088
35278
|
void refreshIdentities();
|
|
35089
|
-
try {
|
|
35090
|
-
saveToken({ access_token: spec.token, name: label, device: null, workspace: spec.workspace }, slot);
|
|
35091
|
-
} catch {
|
|
35092
|
-
}
|
|
35093
35279
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
35094
35280
|
}
|
|
35095
35281
|
}
|
|
35096
|
-
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
35097
35282
|
async function presenceMap() {
|
|
35098
35283
|
const presence = /* @__PURE__ */ new Map();
|
|
35099
35284
|
try {
|
|
@@ -35114,30 +35299,64 @@ function startHost(opts) {
|
|
|
35114
35299
|
let lastHolders = /* @__PURE__ */ new Map();
|
|
35115
35300
|
async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
|
|
35116
35301
|
{
|
|
35117
|
-
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
35118
35302
|
const key = runKey(slot);
|
|
35119
|
-
if (
|
|
35303
|
+
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
35120
35304
|
const live = runs.get(key);
|
|
35121
35305
|
const tokenId = slotIdentity(slot).tokenId;
|
|
35122
35306
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
35123
35307
|
const holder = holders.get(slot);
|
|
35124
35308
|
if (holder) heldHere.add(slot);
|
|
35125
35309
|
else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
|
|
35126
|
-
const
|
|
35127
|
-
|
|
35310
|
+
const here = { holder, goneAt: goneAt.get(slot) };
|
|
35311
|
+
let decided = wakeAction(!!live, Date.now(), seenByServer, here);
|
|
35312
|
+
const label = slotName(slot) ?? slot;
|
|
35313
|
+
if (decided.action === "skip" && decided.why !== "presence") {
|
|
35128
35314
|
if (decided.why === "run" && cause === "wake") live?.run.nudge();
|
|
35129
|
-
|
|
35130
|
-
opts.log(decided.why === "run" ? `\u25CC ${name} is running here \u2014 ${cause === "wake" ? "woke its pump" : "its pump types the work in"}; not spawning` : decided.why === "holder" ? `\u25B8 ${name} already running (pid ${holder?.pid ?? "?"}) \u2014 not starting a second` : `\u25CC ${name} is working somewhere this host cannot see (heartbeat) \u2014 not spawning`);
|
|
35315
|
+
opts.log(decided.why === "run" ? `\u25CC ${label} is running here \u2014 ${cause === "wake" ? "woke its pump" : "its pump types the work in"}; not spawning` : `\u25B8 ${label} already running (pid ${holder?.pid ?? "?"}) \u2014 not starting a second`);
|
|
35131
35316
|
return;
|
|
35132
35317
|
}
|
|
35133
|
-
|
|
35134
|
-
|
|
35318
|
+
let taking = "";
|
|
35319
|
+
let found;
|
|
35320
|
+
const skip = (why) => {
|
|
35321
|
+
opts.log(`\u25CC ${label} ${why} \u2014 not spawning${taking}`);
|
|
35322
|
+
if (taking && found) decide(slot, "took_over", found, seenByServer, `${why} \u2014 not spawning${taking}`);
|
|
35323
|
+
};
|
|
35135
35324
|
const token = readToken(slot);
|
|
35136
35325
|
if (!token) {
|
|
35137
35326
|
skip("has no token on this machine");
|
|
35138
35327
|
return;
|
|
35139
35328
|
}
|
|
35140
|
-
const
|
|
35329
|
+
const identity = slotIdentity(slot);
|
|
35330
|
+
const owner = !identity.harness || identity.harness === "claude" ? ownerSession(slot) : null;
|
|
35331
|
+
const read = async () => {
|
|
35332
|
+
const got = await waitingWork(token, identity.sessionId ?? owner?.sessionId);
|
|
35333
|
+
if (!("skip" in got)) return got;
|
|
35334
|
+
if (got.revoked) {
|
|
35335
|
+
drop(slot, token);
|
|
35336
|
+
return null;
|
|
35337
|
+
}
|
|
35338
|
+
skip(got.skip);
|
|
35339
|
+
if (decided.why === "presence" && got.skip !== NOTHING_WAITING) decide(slot, "stood_back", null, seenByServer, got.skip);
|
|
35340
|
+
return null;
|
|
35341
|
+
};
|
|
35342
|
+
if (decided.why === "presence") {
|
|
35343
|
+
found = await read() ?? void 0;
|
|
35344
|
+
if (!found) return;
|
|
35345
|
+
const since = found.since;
|
|
35346
|
+
decided = wakeAction(false, Date.now(), seenByServer, { ...here, ...since ? { movedAt: Date.parse(since) } : {} });
|
|
35347
|
+
if (decided.action === "skip") {
|
|
35348
|
+
const said = `is working somewhere this host cannot see (heartbeat) \u2014 ${found.waiting}${since ? ` last moved ${ago(since, Date.now())}; taken over once it sits ${IDLE_HOLDER_MS / 6e4} min untouched` : ", with nothing to say when it last moved"}`;
|
|
35349
|
+
opts.log(`\u25CC ${label} ${said} \u2014 not spawning`);
|
|
35350
|
+
decide(slot, "stood_back", found, seenByServer, said);
|
|
35351
|
+
return;
|
|
35352
|
+
}
|
|
35353
|
+
taking = ` (taking over: ${found.waiting} last moved ${ago(since, Date.now())} while something this host cannot see heartbeats as ${label} and has not touched it)`;
|
|
35354
|
+
}
|
|
35355
|
+
const harness = wakeHarness(slot, identity.harness, !!owner);
|
|
35356
|
+
if (!harness) {
|
|
35357
|
+
skip("has no recorded harness; select its runtime before recovery");
|
|
35358
|
+
return;
|
|
35359
|
+
}
|
|
35141
35360
|
if (owner && !allowed(owner.cwd, opts.wsDeps)) {
|
|
35142
35361
|
skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
|
|
35143
35362
|
return;
|
|
@@ -35147,11 +35366,8 @@ function startHost(opts) {
|
|
|
35147
35366
|
skip("has no granted folder to wake in");
|
|
35148
35367
|
return;
|
|
35149
35368
|
}
|
|
35150
|
-
|
|
35151
|
-
if (
|
|
35152
|
-
skip(found.skip);
|
|
35153
|
-
return;
|
|
35154
|
-
}
|
|
35369
|
+
found ??= await read() ?? void 0;
|
|
35370
|
+
if (!found) return;
|
|
35155
35371
|
if (runs.has(key)) {
|
|
35156
35372
|
skip("was started by the wake that read it first");
|
|
35157
35373
|
return;
|
|
@@ -35163,6 +35379,7 @@ function startHost(opts) {
|
|
|
35163
35379
|
mode: "bypass",
|
|
35164
35380
|
prompt: "",
|
|
35165
35381
|
token,
|
|
35382
|
+
...identity.sessionId ? { session: identity.sessionId } : {},
|
|
35166
35383
|
...owner ? { resume: owner.sessionId } : {},
|
|
35167
35384
|
// A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
|
|
35168
35385
|
// a run that is no longer the slot's must not take the live one's place away with it.
|
|
@@ -35177,13 +35394,15 @@ function startHost(opts) {
|
|
|
35177
35394
|
runs.set(key, { run, label, token });
|
|
35178
35395
|
void heartbeat(void 0, { token }).catch(() => {
|
|
35179
35396
|
});
|
|
35180
|
-
|
|
35397
|
+
const woke = `\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 ${found.waiting} was waiting, in ${workspace}${decided.why === "gone" ? " (its process here ended; its last heartbeat was not waited out)" : taking}`;
|
|
35398
|
+
opts.log(woke);
|
|
35399
|
+
if (taking) decide(slot, "took_over", found, seenByServer, woke);
|
|
35181
35400
|
}
|
|
35182
35401
|
}
|
|
35183
35402
|
const listening = /* @__PURE__ */ new Map();
|
|
35184
35403
|
const joining = /* @__PURE__ */ new Set();
|
|
35185
35404
|
async function listenSlots() {
|
|
35186
|
-
const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot)));
|
|
35405
|
+
const want = new Set(listSlots().filter((slot) => slot !== "Desktop" && !!readToken(slot) && !isRevoked(slot)));
|
|
35187
35406
|
for (const [slot, sub] of [...listening]) {
|
|
35188
35407
|
if (want.has(slot)) continue;
|
|
35189
35408
|
listening.delete(slot);
|
|
@@ -35204,6 +35423,10 @@ function startHost(opts) {
|
|
|
35204
35423
|
listening.set(slot, sub);
|
|
35205
35424
|
opts.log(`\u25C9 listening for ${slotName(slot) ?? slot}`);
|
|
35206
35425
|
} catch (e) {
|
|
35426
|
+
if (e instanceof RevokedError) {
|
|
35427
|
+
drop(slot, token);
|
|
35428
|
+
continue;
|
|
35429
|
+
}
|
|
35207
35430
|
opts.log(`\u25CC not listening for ${slotName(slot) ?? slot} \u2014 ${e.message}`);
|
|
35208
35431
|
} finally {
|
|
35209
35432
|
joining.delete(slot);
|
|
@@ -35269,7 +35492,7 @@ function startHost(opts) {
|
|
|
35269
35492
|
roster: () => {
|
|
35270
35493
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
35271
35494
|
const names2 = /* @__PURE__ */ new Map();
|
|
35272
|
-
for (const key of listSlots()) if (key !== "Desktop") names2.set(slotName(key) ?? key, key);
|
|
35495
|
+
for (const key of listSlots()) if (key !== "Desktop" && !isRevoked(key)) names2.set(slotName(key) ?? key, key);
|
|
35273
35496
|
for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
|
|
35274
35497
|
return [...names2.entries()].map(([name, slot]) => {
|
|
35275
35498
|
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
@@ -35337,17 +35560,17 @@ function startBridgeInstalls(bridges, deps = {}) {
|
|
|
35337
35560
|
const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
35338
35561
|
const running = bridges.map((b) => {
|
|
35339
35562
|
let proc = null;
|
|
35340
|
-
let
|
|
35563
|
+
let outcome2;
|
|
35341
35564
|
try {
|
|
35342
35565
|
proc = spawn2(b.install);
|
|
35343
|
-
|
|
35566
|
+
outcome2 = proc.exited.then(
|
|
35344
35567
|
(code) => code === 0 ? "installed" : "failed",
|
|
35345
35568
|
() => "failed"
|
|
35346
35569
|
);
|
|
35347
35570
|
} catch {
|
|
35348
|
-
|
|
35571
|
+
outcome2 = Promise.resolve("failed");
|
|
35349
35572
|
}
|
|
35350
|
-
return { ...b, proc, outcome };
|
|
35573
|
+
return { ...b, proc, outcome: outcome2 };
|
|
35351
35574
|
});
|
|
35352
35575
|
return {
|
|
35353
35576
|
started: running.length,
|
|
@@ -35625,7 +35848,7 @@ async function main() {
|
|
|
35625
35848
|
process.exit(1);
|
|
35626
35849
|
}
|
|
35627
35850
|
const cwd = process.cwd();
|
|
35628
|
-
if (!updateSlot(slot, { workspace: cwd })) {
|
|
35851
|
+
if (!updateSlot(slot, { workspace: cwd, session_id: sessionId() })) {
|
|
35629
35852
|
console.error(`Slot "${slot}" isn't in this machine's slot file \u2014 this session was spawned by the host, which already owns its wake dir. Run handoff from the terminal that hatched the identity.`);
|
|
35630
35853
|
process.exit(1);
|
|
35631
35854
|
}
|