@paigy/harness 0.3.17 → 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 +26 -14
- package/dist/agy-acp.js +44 -8
- package/dist/cli.js +1647 -627
- package/dist/main.js +4917 -3915
- 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;
|
|
@@ -16098,13 +16098,13 @@ var require_headers = __commonJS({
|
|
|
16098
16098
|
return this.#headersList[kHeadersSortedMap];
|
|
16099
16099
|
}
|
|
16100
16100
|
const headers = [];
|
|
16101
|
-
const
|
|
16101
|
+
const names2 = this.#headersList.toSortedArray();
|
|
16102
16102
|
const cookies = this.#headersList.cookies;
|
|
16103
16103
|
if (cookies === null || cookies.length === 1) {
|
|
16104
|
-
return this.#headersList[kHeadersSortedMap] =
|
|
16104
|
+
return this.#headersList[kHeadersSortedMap] = names2;
|
|
16105
16105
|
}
|
|
16106
|
-
for (let i = 0; i <
|
|
16107
|
-
const { 0: name, 1: value } =
|
|
16106
|
+
for (let i = 0; i < names2.length; ++i) {
|
|
16107
|
+
const { 0: name, 1: value } = names2[i];
|
|
16108
16108
|
if (name === "set-cookie") {
|
|
16109
16109
|
for (let j = 0; j < cookies.length; ++j) {
|
|
16110
16110
|
headers.push([name, cookies[j]]);
|
|
@@ -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
|
-
async function getGoal(goalId, opts = {}) {
|
|
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 = {}) {
|
|
|
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,16 +24648,29 @@ 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()
|
|
24639
24659
|
}).strict();
|
|
24640
24660
|
UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
24641
24661
|
ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
24642
|
-
GetGoalSchema = external_exports.object({
|
|
24643
|
-
|
|
24644
|
-
|
|
24662
|
+
GetGoalSchema = external_exports.object({
|
|
24663
|
+
goalId: external_exports.string().uuid(),
|
|
24664
|
+
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
24665
|
+
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
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()
|
|
24671
|
+
}).strict();
|
|
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.`;
|
|
24645
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.";
|
|
24646
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.";
|
|
24647
24676
|
CheckRepliesSchema = external_exports.object({}).strict();
|
|
@@ -25245,18 +25274,20 @@ var init_dist = __esm({
|
|
|
25245
25274
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
25246
25275
|
broker: BrokerTuningSchema.optional()
|
|
25247
25276
|
});
|
|
25248
|
-
|
|
25277
|
+
HistoryWorkSchema = external_exports.object({
|
|
25249
25278
|
id: external_exports.string(),
|
|
25250
|
-
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
25251
|
-
initiator: external_exports.enum(["user", "agent"]),
|
|
25252
25279
|
title: external_exports.string(),
|
|
25253
|
-
|
|
25254
|
-
|
|
25255
|
-
|
|
25256
|
-
|
|
25257
|
-
|
|
25258
|
-
|
|
25259
|
-
|
|
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()
|
|
25260
25291
|
});
|
|
25261
25292
|
ACTIVITY_LINES = 2;
|
|
25262
25293
|
ACTIVITY_LINE_MAX = 80;
|
|
@@ -25440,6 +25471,9 @@ var init_dist = __esm({
|
|
|
25440
25471
|
progressLine: external_exports.string().nullable().optional(),
|
|
25441
25472
|
reviewPending: external_exports.boolean().default(false),
|
|
25442
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(),
|
|
25443
25477
|
/** The Goal this one was opened under; null at the root. */
|
|
25444
25478
|
parentGoalId: external_exports.string().nullable().default(null),
|
|
25445
25479
|
/** Goals opened under this one — only those the same list holds. */
|
|
@@ -25448,8 +25482,17 @@ var init_dist = __esm({
|
|
|
25448
25482
|
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
25449
25483
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
25450
25484
|
blocked: external_exports.boolean().default(false),
|
|
25451
|
-
/**
|
|
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. */
|
|
25452
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(),
|
|
25453
25496
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
25454
25497
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
25455
25498
|
* not spell an empty list. */
|
|
@@ -25461,7 +25504,19 @@ var init_dist = __esm({
|
|
|
25461
25504
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
25462
25505
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
25463
25506
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
25464
|
-
lastProgressAt: external_exports.string().nullable().optional()
|
|
25507
|
+
lastProgressAt: external_exports.string().nullable().optional(),
|
|
25508
|
+
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
25509
|
+
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
25510
|
+
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
25511
|
+
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
25512
|
+
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
25513
|
+
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
25514
|
+
latest: external_exports.object({
|
|
25515
|
+
from: external_exports.enum(["person", "agent"]),
|
|
25516
|
+
said: external_exports.string(),
|
|
25517
|
+
at: external_exports.string(),
|
|
25518
|
+
entryId: external_exports.string()
|
|
25519
|
+
}).nullable().optional()
|
|
25465
25520
|
});
|
|
25466
25521
|
COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
25467
25522
|
NoteSourceSchema = external_exports.enum(["app", "call"]);
|
|
@@ -25574,6 +25629,19 @@ var init_dist = __esm({
|
|
|
25574
25629
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
25575
25630
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
25576
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
|
+
});
|
|
25577
25645
|
WakeNudgeSchema = external_exports.object({
|
|
25578
25646
|
kind: external_exports.enum(["reply", "request", "callback"]),
|
|
25579
25647
|
notificationId: external_exports.string().optional(),
|
|
@@ -25618,6 +25686,9 @@ var init_dist = __esm({
|
|
|
25618
25686
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
25619
25687
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
25620
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(),
|
|
25621
25692
|
uik_pub: external_exports.string().nullable().optional()
|
|
25622
25693
|
});
|
|
25623
25694
|
SupportRequestSchema = external_exports.object({
|
|
@@ -25639,6 +25710,104 @@ var init_dist = __esm({
|
|
|
25639
25710
|
"other"
|
|
25640
25711
|
// anything else — the note carries it.
|
|
25641
25712
|
]);
|
|
25713
|
+
SlimOptionSchema = OptionSchema.omit({ html: true });
|
|
25714
|
+
QuestionRowSchema = external_exports.object({
|
|
25715
|
+
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
25716
|
+
id: external_exports.string(),
|
|
25717
|
+
deliveryId: external_exports.string(),
|
|
25718
|
+
entryId: external_exports.string(),
|
|
25719
|
+
/** The decision it waits on; null for an update, which asks nothing. */
|
|
25720
|
+
needId: external_exports.string().nullable(),
|
|
25721
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
25722
|
+
/** The name of the work it is about, when the read could word it. */
|
|
25723
|
+
goalTitle: external_exports.string().optional(),
|
|
25724
|
+
tokenId: external_exports.string().optional(),
|
|
25725
|
+
name: external_exports.string(),
|
|
25726
|
+
title: external_exports.string(),
|
|
25727
|
+
body: external_exports.string(),
|
|
25728
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
|
|
25729
|
+
options: external_exports.array(SlimOptionSchema),
|
|
25730
|
+
hasPreview: external_exports.boolean(),
|
|
25731
|
+
blocking: external_exports.boolean(),
|
|
25732
|
+
askedAt: external_exports.string().datetime(),
|
|
25733
|
+
ring: InboxItemSchema.shape.ring,
|
|
25734
|
+
onCall: external_exports.literal(true).optional(),
|
|
25735
|
+
sealed: external_exports.boolean()
|
|
25736
|
+
});
|
|
25737
|
+
WorkStateSchema = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
25738
|
+
WorkRowSchema = external_exports.object({
|
|
25739
|
+
id: external_exports.string(),
|
|
25740
|
+
parentId: external_exports.string().nullable(),
|
|
25741
|
+
title: external_exports.string(),
|
|
25742
|
+
/** Straight off the Goal. */
|
|
25743
|
+
state: WorkStateSchema,
|
|
25744
|
+
owner: external_exports.string().nullable(),
|
|
25745
|
+
revision: external_exports.number().int(),
|
|
25746
|
+
/** Open questions on it, counted to 100. */
|
|
25747
|
+
waiting: external_exports.number().int(),
|
|
25748
|
+
/** Held by a gate on work that is not done. */
|
|
25749
|
+
blocked: external_exports.boolean(),
|
|
25750
|
+
lastProgressAt: external_exports.string().datetime().nullable(),
|
|
25751
|
+
/** The line written for its newest progress note, else that note's first words. */
|
|
25752
|
+
line: external_exports.string().nullable(),
|
|
25753
|
+
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
25754
|
+
children: external_exports.number().int(),
|
|
25755
|
+
createdAt: external_exports.string().datetime(),
|
|
25756
|
+
updatedAt: external_exports.string().datetime(),
|
|
25757
|
+
/** When anything at or under it last moved — the order the list is in. */
|
|
25758
|
+
activeAt: external_exports.string().datetime(),
|
|
25759
|
+
/** A sealed outcome has no title here; the work's page opens it. */
|
|
25760
|
+
sealed: external_exports.boolean()
|
|
25761
|
+
});
|
|
25762
|
+
ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
|
|
25763
|
+
AgentRowSchema = ComputerRowSchema.extend({
|
|
25764
|
+
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
25765
|
+
asking: external_exports.number().int().nullable(),
|
|
25766
|
+
oldestAskAt: external_exports.string().datetime().nullable(),
|
|
25767
|
+
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
25768
|
+
* many in all among the account's 200 most recently active agent-held live Goals
|
|
25769
|
+
* (`agent_holds`); null when that read failed. */
|
|
25770
|
+
holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
|
|
25771
|
+
held: external_exports.number().int().nullable(),
|
|
25772
|
+
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
25773
|
+
* while none has, or when that read failed. */
|
|
25774
|
+
cold: external_exports.string().datetime().nullable(),
|
|
25775
|
+
/** The newest line of its working log, and when the harness saw it. */
|
|
25776
|
+
line: external_exports.string().nullable(),
|
|
25777
|
+
lineAt: external_exports.string().datetime().nullable()
|
|
25778
|
+
});
|
|
25779
|
+
SnapshotSchema = external_exports.object({
|
|
25780
|
+
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
25781
|
+
at: external_exports.string().datetime(),
|
|
25782
|
+
questions: external_exports.object({
|
|
25783
|
+
/** The newest 30 open cards, questions before updates. */
|
|
25784
|
+
items: external_exports.array(QuestionRowSchema),
|
|
25785
|
+
/** Every open question, and apart from them every update, and what was put off. */
|
|
25786
|
+
total: external_exports.number().int(),
|
|
25787
|
+
updates: external_exports.number().int(),
|
|
25788
|
+
putOff: external_exports.number().int()
|
|
25789
|
+
}).nullable(),
|
|
25790
|
+
agents: external_exports.object({
|
|
25791
|
+
/** Up to 60, most recently seen first. */
|
|
25792
|
+
items: external_exports.array(AgentRowSchema),
|
|
25793
|
+
more: external_exports.boolean()
|
|
25794
|
+
}).nullable(),
|
|
25795
|
+
work: external_exports.object({
|
|
25796
|
+
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
25797
|
+
items: external_exports.array(WorkRowSchema),
|
|
25798
|
+
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
25799
|
+
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
25800
|
+
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
25801
|
+
* is `ready`; Done is `done` or `cancelled`. */
|
|
25802
|
+
counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
|
|
25803
|
+
}).nullable(),
|
|
25804
|
+
you: external_exports.object({
|
|
25805
|
+
settings: UserSettingsSchema,
|
|
25806
|
+
callable: external_exports.boolean(),
|
|
25807
|
+
/** Up to 20 paired computers; null when the roster read failed. */
|
|
25808
|
+
computers: external_exports.array(ComputerRowSchema).nullable()
|
|
25809
|
+
}).nullable()
|
|
25810
|
+
});
|
|
25642
25811
|
TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
|
|
25643
25812
|
KEY_PATH = join22(homedir22(), ".paigy", "key.json");
|
|
25644
25813
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -25648,6 +25817,13 @@ var init_dist = __esm({
|
|
|
25648
25817
|
this.name = "UnpairedError";
|
|
25649
25818
|
}
|
|
25650
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
|
+
};
|
|
25651
25827
|
ApiError = class extends Error {
|
|
25652
25828
|
status;
|
|
25653
25829
|
body;
|
|
@@ -30243,135 +30419,9 @@ var require_lib = __commonJS({
|
|
|
30243
30419
|
}
|
|
30244
30420
|
});
|
|
30245
30421
|
|
|
30246
|
-
// src/triage/judge.ts
|
|
30247
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
30248
|
-
import { homedir as homedir7 } from "os";
|
|
30249
|
-
import { delimiter as delimiter3 } from "path";
|
|
30250
|
-
function pickOllamaModel(tags) {
|
|
30251
|
-
if (tags.length === 0) return null;
|
|
30252
|
-
const score = (tag) => {
|
|
30253
|
-
const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
|
|
30254
|
-
return i === -1 ? PREFERRED.length : i;
|
|
30255
|
-
};
|
|
30256
|
-
return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
|
|
30257
|
-
}
|
|
30258
|
-
function parseOllamaTags(body) {
|
|
30259
|
-
const models = body?.models;
|
|
30260
|
-
if (!Array.isArray(models)) return [];
|
|
30261
|
-
return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
|
|
30262
|
-
}
|
|
30263
|
-
async function fetchTags(host) {
|
|
30264
|
-
try {
|
|
30265
|
-
const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(2e3) });
|
|
30266
|
-
if (!res.ok) return null;
|
|
30267
|
-
return parseOllamaTags(await res.json());
|
|
30268
|
-
} catch {
|
|
30269
|
-
return null;
|
|
30270
|
-
}
|
|
30271
|
-
}
|
|
30272
|
-
async function available(deps = {}) {
|
|
30273
|
-
const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
|
|
30274
|
-
const model = tags ? pickOllamaModel(tags) : null;
|
|
30275
|
-
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 };
|
|
30276
|
-
const detected = (deps.detect ?? detectAll)();
|
|
30277
|
-
const harnesses = CATALOG.map((entry) => {
|
|
30278
|
-
const state = detected.find((d) => d.name === entry.name);
|
|
30279
|
-
const usable = state?.status === "ready" || state?.status === "adapter-missing";
|
|
30280
|
-
return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
|
|
30281
|
-
});
|
|
30282
|
-
return [ollama, ...harnesses];
|
|
30283
|
-
}
|
|
30284
|
-
function refusal(options, asked2) {
|
|
30285
|
-
const lines2 = [
|
|
30286
|
-
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.",
|
|
30287
|
-
"",
|
|
30288
|
-
"Triage runs the model where your queue already lives (#1106). Your options:"
|
|
30289
|
-
];
|
|
30290
|
-
for (const o of options) {
|
|
30291
|
-
lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
|
|
30292
|
-
if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
|
|
30293
|
-
}
|
|
30294
|
-
lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
|
|
30295
|
-
return lines2.join("\n");
|
|
30296
|
-
}
|
|
30297
|
-
async function judgeFor(asked2, deps = {}) {
|
|
30298
|
-
const options = await available(deps);
|
|
30299
|
-
const chosen = asked2 ? options.find((o) => o.provider === asked2) : options.find((o) => o.ready);
|
|
30300
|
-
if (!chosen?.ready) return { error: refusal(options, asked2) };
|
|
30301
|
-
if (chosen.provider === "ollama") {
|
|
30302
|
-
const model = chosen.model;
|
|
30303
|
-
return {
|
|
30304
|
-
provider: "ollama",
|
|
30305
|
-
model,
|
|
30306
|
-
run: async (prompt) => {
|
|
30307
|
-
const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
|
30308
|
-
method: "POST",
|
|
30309
|
-
headers: { "content-type": "application/json" },
|
|
30310
|
-
// Non-streaming: we want the whole answer once, and the parser is the same
|
|
30311
|
-
// either way. `temperature: 0` because this is a classification, not prose.
|
|
30312
|
-
body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
|
|
30313
|
-
signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
|
|
30314
|
-
});
|
|
30315
|
-
if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
|
|
30316
|
-
return (await res.json()).response ?? "";
|
|
30317
|
-
}
|
|
30318
|
-
};
|
|
30319
|
-
}
|
|
30320
|
-
const runner = RUNNERS[chosen.provider];
|
|
30321
|
-
if (!runner) return { error: refusal(options, asked2) };
|
|
30322
|
-
const bin = which(runner.cli);
|
|
30323
|
-
if (!bin) return { error: refusal(options, asked2) };
|
|
30324
|
-
return {
|
|
30325
|
-
provider: chosen.provider,
|
|
30326
|
-
model: null,
|
|
30327
|
-
run: async (prompt) => {
|
|
30328
|
-
const out = spawnSync2(bin, runner.argv(null), {
|
|
30329
|
-
input: prompt,
|
|
30330
|
-
encoding: "utf8",
|
|
30331
|
-
cwd: homedir7(),
|
|
30332
|
-
timeout: RUN_TIMEOUT_MS,
|
|
30333
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
30334
|
-
env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
|
|
30335
|
-
});
|
|
30336
|
-
if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
|
|
30337
|
-
if (out.status !== 0 && !out.stdout?.trim())
|
|
30338
|
-
throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
|
|
30339
|
-
return out.stdout ?? "";
|
|
30340
|
-
}
|
|
30341
|
-
};
|
|
30342
|
-
}
|
|
30343
|
-
var PRIVACY, OLLAMA_HOST, PREFERRED, RUNNERS, RUN_TIMEOUT_MS;
|
|
30344
|
-
var init_judge = __esm({
|
|
30345
|
-
"src/triage/judge.ts"() {
|
|
30346
|
-
"use strict";
|
|
30347
|
-
init_catalog();
|
|
30348
|
-
PRIVACY = {
|
|
30349
|
-
ollama: "never leaves this machine",
|
|
30350
|
-
claude: "this machine + your own Claude subscription; Paigy's servers never see it",
|
|
30351
|
-
codex: "this machine + your own Codex subscription; Paigy's servers never see it",
|
|
30352
|
-
agy: "this machine + your own Antigravity account; Paigy's servers never see it"
|
|
30353
|
-
};
|
|
30354
|
-
OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
|
|
30355
|
-
PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
|
|
30356
|
-
RUNNERS = {
|
|
30357
|
-
// Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
|
|
30358
|
-
ollama: null,
|
|
30359
|
-
// `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
|
|
30360
|
-
claude: { cli: "claude", argv: () => ["-p"] },
|
|
30361
|
-
// `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
|
|
30362
|
-
// when none is given as an argument. It prints its own preamble around the answer, which
|
|
30363
|
-
// is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
|
|
30364
|
-
codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
|
|
30365
|
-
// `agy --print` (alias -p) is Antigravity's print mode, text output by default.
|
|
30366
|
-
agy: { cli: "agy", argv: () => ["-p"] }
|
|
30367
|
-
};
|
|
30368
|
-
RUN_TIMEOUT_MS = 5 * 6e4;
|
|
30369
|
-
}
|
|
30370
|
-
});
|
|
30371
|
-
|
|
30372
30422
|
// src/triage/evidence.ts
|
|
30373
|
-
function assigneeToken(
|
|
30374
|
-
const ref =
|
|
30423
|
+
function assigneeToken(note2) {
|
|
30424
|
+
const ref = note2.assignee ?? "";
|
|
30375
30425
|
return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
|
|
30376
30426
|
}
|
|
30377
30427
|
function words(text) {
|
|
@@ -30399,23 +30449,23 @@ function overlap(a, b) {
|
|
|
30399
30449
|
for (const w of A) if (B.has(w)) shared++;
|
|
30400
30450
|
return shared / (A.size + B.size - shared);
|
|
30401
30451
|
}
|
|
30402
|
-
function supersededBy(
|
|
30403
|
-
const mine = `${
|
|
30404
|
-
const at = Date.parse(
|
|
30452
|
+
function supersededBy(note2, all) {
|
|
30453
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
30454
|
+
const at = Date.parse(note2.createdAt);
|
|
30405
30455
|
let best = null;
|
|
30406
30456
|
for (const other of all) {
|
|
30407
|
-
if (other.id ===
|
|
30457
|
+
if (other.id === note2.id) continue;
|
|
30408
30458
|
if (Date.parse(other.createdAt) <= at) continue;
|
|
30409
30459
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
30410
30460
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
30411
30461
|
}
|
|
30412
30462
|
return best?.note ?? null;
|
|
30413
30463
|
}
|
|
30414
|
-
function doneEcho(
|
|
30415
|
-
const mine = `${
|
|
30464
|
+
function doneEcho(note2, all) {
|
|
30465
|
+
const mine = `${note2.title} ${note2.intent}`;
|
|
30416
30466
|
let best = null;
|
|
30417
30467
|
for (const other of all) {
|
|
30418
|
-
if (other.id ===
|
|
30468
|
+
if (other.id === note2.id || other.status !== "done") continue;
|
|
30419
30469
|
const score = overlap(mine, `${other.title} ${other.intent}`);
|
|
30420
30470
|
if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
|
|
30421
30471
|
}
|
|
@@ -30469,8 +30519,8 @@ function dossier(notes, connections, now) {
|
|
|
30469
30519
|
today: new Date(now).toISOString().slice(0, 10)
|
|
30470
30520
|
};
|
|
30471
30521
|
}
|
|
30472
|
-
function affinityWhy(
|
|
30473
|
-
const shared =
|
|
30522
|
+
function affinityWhy(note2, agent2) {
|
|
30523
|
+
const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
|
|
30474
30524
|
if (shared.length && agent2.recentNotes)
|
|
30475
30525
|
return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
|
|
30476
30526
|
if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
|
|
@@ -30479,12 +30529,12 @@ function affinityWhy(note, agent2) {
|
|
|
30479
30529
|
return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
|
|
30480
30530
|
return null;
|
|
30481
30531
|
}
|
|
30482
|
-
function staleWhy(
|
|
30483
|
-
if (
|
|
30484
|
-
if (
|
|
30485
|
-
if (
|
|
30486
|
-
if (
|
|
30487
|
-
if (
|
|
30532
|
+
function staleWhy(note2) {
|
|
30533
|
+
if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
|
|
30534
|
+
if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
|
|
30535
|
+
if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
|
|
30536
|
+
if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
|
|
30537
|
+
if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
|
|
30488
30538
|
return null;
|
|
30489
30539
|
}
|
|
30490
30540
|
var OPEN_STATUSES, DAY_MS, daysBetween, STOP, SUPERSEDE_AT, RECENT_DAYS;
|
|
@@ -30544,76 +30594,202 @@ var init_evidence = __esm({
|
|
|
30544
30594
|
}
|
|
30545
30595
|
});
|
|
30546
30596
|
|
|
30547
|
-
// src/triage/
|
|
30548
|
-
|
|
30549
|
-
|
|
30550
|
-
|
|
30551
|
-
|
|
30552
|
-
|
|
30553
|
-
|
|
30554
|
-
|
|
30555
|
-
|
|
30556
|
-
|
|
30557
|
-
return null;
|
|
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;
|
|
30558
30608
|
}
|
|
30559
|
-
function
|
|
30560
|
-
const
|
|
30561
|
-
if (!
|
|
30562
|
-
|
|
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) {
|
|
30563
30615
|
try {
|
|
30564
|
-
|
|
30565
|
-
|
|
30566
|
-
return
|
|
30567
|
-
}
|
|
30568
|
-
|
|
30569
|
-
if (!rows) return { ok: false, error: "the JSON was not an array of verdicts", raw };
|
|
30570
|
-
const verdicts = [];
|
|
30571
|
-
const dropped = [];
|
|
30572
|
-
const seen = /* @__PURE__ */ new Set();
|
|
30573
|
-
for (const row of rows) {
|
|
30574
|
-
const r = row;
|
|
30575
|
-
const id = typeof r?.id === "string" ? r.id : "";
|
|
30576
|
-
if (!id) {
|
|
30577
|
-
dropped.push({ row, reason: "no id" });
|
|
30578
|
-
continue;
|
|
30579
|
-
}
|
|
30580
|
-
if (!knownIds.has(id)) {
|
|
30581
|
-
dropped.push({ row, reason: "id is not a note in this queue" });
|
|
30582
|
-
continue;
|
|
30583
|
-
}
|
|
30584
|
-
if (seen.has(id)) {
|
|
30585
|
-
dropped.push({ row, reason: "duplicate verdict for this note" });
|
|
30586
|
-
continue;
|
|
30587
|
-
}
|
|
30588
|
-
const verdict = typeof r?.verdict === "string" ? r.verdict.toLowerCase().trim() : "";
|
|
30589
|
-
if (!VERDICTS.has(verdict)) {
|
|
30590
|
-
dropped.push({ row, reason: `unknown verdict ${JSON.stringify(r?.verdict)}` });
|
|
30591
|
-
continue;
|
|
30592
|
-
}
|
|
30593
|
-
const agent2 = typeof r?.agent === "string" ? r.agent.trim() : null;
|
|
30594
|
-
if (verdict === "assign" && (!agent2 || !knownAgents.has(agent2))) {
|
|
30595
|
-
dropped.push({ row, reason: agent2 ? `no such agent: ${agent2}` : "assign with no agent" });
|
|
30596
|
-
continue;
|
|
30597
|
-
}
|
|
30598
|
-
verdicts.push({
|
|
30599
|
-
id,
|
|
30600
|
-
verdict,
|
|
30601
|
-
agent: verdict === "assign" ? agent2 : null,
|
|
30602
|
-
why: typeof r?.why === "string" ? r.why.trim().slice(0, 200) : null
|
|
30603
|
-
});
|
|
30604
|
-
seen.add(id);
|
|
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;
|
|
30605
30621
|
}
|
|
30606
|
-
return { ok: true, verdicts, dropped };
|
|
30607
30622
|
}
|
|
30608
|
-
function
|
|
30609
|
-
const
|
|
30610
|
-
|
|
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];
|
|
30611
30634
|
}
|
|
30612
|
-
function
|
|
30613
|
-
|
|
30614
|
-
|
|
30615
|
-
"",
|
|
30616
|
-
"
|
|
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
|
+
|
|
30723
|
+
// src/triage/verdict.ts
|
|
30724
|
+
function extractJson(raw) {
|
|
30725
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
30726
|
+
const body = fenced ?? raw;
|
|
30727
|
+
const start = body.indexOf("[");
|
|
30728
|
+
const end = body.lastIndexOf("]");
|
|
30729
|
+
if (start !== -1 && end > start) return body.slice(start, end + 1);
|
|
30730
|
+
const objStart = body.indexOf("{");
|
|
30731
|
+
const objEnd = body.lastIndexOf("}");
|
|
30732
|
+
if (objStart !== -1 && objEnd > objStart) return body.slice(objStart, objEnd + 1);
|
|
30733
|
+
return null;
|
|
30734
|
+
}
|
|
30735
|
+
function parseVerdicts(raw, knownIds, knownAgents) {
|
|
30736
|
+
const json = extractJson(raw);
|
|
30737
|
+
if (!json) return { ok: false, error: "no JSON found in the model's answer", raw };
|
|
30738
|
+
let parsed;
|
|
30739
|
+
try {
|
|
30740
|
+
parsed = JSON.parse(json);
|
|
30741
|
+
} catch (e) {
|
|
30742
|
+
return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
|
|
30743
|
+
}
|
|
30744
|
+
const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.verdicts) ? parsed.verdicts : typeof parsed?.id === "string" ? [parsed] : null;
|
|
30745
|
+
if (!rows) return { ok: false, error: "the JSON was not an array of verdicts", raw };
|
|
30746
|
+
const verdicts = [];
|
|
30747
|
+
const dropped = [];
|
|
30748
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30749
|
+
for (const row of rows) {
|
|
30750
|
+
const r = row;
|
|
30751
|
+
const id = typeof r?.id === "string" ? r.id : "";
|
|
30752
|
+
if (!id) {
|
|
30753
|
+
dropped.push({ row, reason: "no id" });
|
|
30754
|
+
continue;
|
|
30755
|
+
}
|
|
30756
|
+
if (!knownIds.has(id)) {
|
|
30757
|
+
dropped.push({ row, reason: "id is not a note in this queue" });
|
|
30758
|
+
continue;
|
|
30759
|
+
}
|
|
30760
|
+
if (seen.has(id)) {
|
|
30761
|
+
dropped.push({ row, reason: "duplicate verdict for this note" });
|
|
30762
|
+
continue;
|
|
30763
|
+
}
|
|
30764
|
+
const verdict = typeof r?.verdict === "string" ? r.verdict.toLowerCase().trim() : "";
|
|
30765
|
+
if (!VERDICTS.has(verdict)) {
|
|
30766
|
+
dropped.push({ row, reason: `unknown verdict ${JSON.stringify(r?.verdict)}` });
|
|
30767
|
+
continue;
|
|
30768
|
+
}
|
|
30769
|
+
const agent2 = typeof r?.agent === "string" ? r.agent.trim() : null;
|
|
30770
|
+
if (verdict === "assign" && (!agent2 || !knownAgents.has(agent2))) {
|
|
30771
|
+
dropped.push({ row, reason: agent2 ? `no such agent: ${agent2}` : "assign with no agent" });
|
|
30772
|
+
continue;
|
|
30773
|
+
}
|
|
30774
|
+
verdicts.push({
|
|
30775
|
+
id,
|
|
30776
|
+
verdict,
|
|
30777
|
+
agent: verdict === "assign" ? agent2 : null,
|
|
30778
|
+
why: typeof r?.why === "string" ? r.why.trim().slice(0, 200) : null
|
|
30779
|
+
});
|
|
30780
|
+
seen.add(id);
|
|
30781
|
+
}
|
|
30782
|
+
return { ok: true, verdicts, dropped };
|
|
30783
|
+
}
|
|
30784
|
+
function fillMissing(verdicts, notes) {
|
|
30785
|
+
const said = new Set(verdicts.map((v) => v.id));
|
|
30786
|
+
return [...verdicts, ...notes.filter((n) => !said.has(n.id)).map((n) => ({ id: n.id, verdict: "keep", why: null }))];
|
|
30787
|
+
}
|
|
30788
|
+
function buildPrompt(d) {
|
|
30789
|
+
return [
|
|
30790
|
+
"You are triaging a personal task queue. Today is " + d.today + ".",
|
|
30791
|
+
"",
|
|
30792
|
+
"For EVERY note below, decide one of:",
|
|
30617
30793
|
' "keep" \u2014 it is still live work. THE DEFAULT.',
|
|
30618
30794
|
' "stale" \u2014 nothing has moved on it for a long time and nothing says it is finished.',
|
|
30619
30795
|
' "close" \u2014 there is evidence it is already done or no longer wanted.',
|
|
@@ -30659,22 +30835,22 @@ function toProposal(verdicts, d, meta) {
|
|
|
30659
30835
|
const stale = [];
|
|
30660
30836
|
const assignments = /* @__PURE__ */ new Map();
|
|
30661
30837
|
for (const v of verdicts) {
|
|
30662
|
-
const
|
|
30663
|
-
if (!
|
|
30664
|
-
const item = (why) => ({ noteId:
|
|
30838
|
+
const note2 = byId.get(v.id);
|
|
30839
|
+
if (!note2) continue;
|
|
30840
|
+
const item = (why) => ({ noteId: note2.id, title: note2.title, why });
|
|
30665
30841
|
if (v.verdict === "close") {
|
|
30666
|
-
close.push(item(staleWhy(
|
|
30842
|
+
close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
|
|
30667
30843
|
continue;
|
|
30668
30844
|
}
|
|
30669
30845
|
if (v.verdict === "stale") {
|
|
30670
|
-
stale.push(item(staleWhy(
|
|
30846
|
+
stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
|
|
30671
30847
|
continue;
|
|
30672
30848
|
}
|
|
30673
30849
|
if (v.verdict !== "assign" || !v.agent) continue;
|
|
30674
30850
|
const agent2 = agentById.get(v.agent);
|
|
30675
30851
|
if (!agent2) continue;
|
|
30676
30852
|
const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
|
|
30677
|
-
group.notes.push(item(affinityWhy(
|
|
30853
|
+
group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
|
|
30678
30854
|
assignments.set(agent2.token, group);
|
|
30679
30855
|
}
|
|
30680
30856
|
return {
|
|
@@ -30897,6 +31073,15 @@ function parseArgs(argv) {
|
|
|
30897
31073
|
return { error: "--yes-close/--yes-stale/--yes-assign only apply with --apply" };
|
|
30898
31074
|
return args;
|
|
30899
31075
|
}
|
|
31076
|
+
var MCP_WIRES = [
|
|
31077
|
+
{ cli: "claude", name: "Claude Code", wire: "claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
|
|
31078
|
+
// Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
|
|
31079
|
+
// a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
|
|
31080
|
+
// written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
|
|
31081
|
+
// all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
|
|
31082
|
+
{ cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
|
|
31083
|
+
{ cli: "agy", name: "Antigravity", wire: "agy mcp list | grep -q '^paigy ' || agy mcp add paigy -- npx -y @paigy/mcp@latest" }
|
|
31084
|
+
];
|
|
30900
31085
|
var ENABLE_TOOLS_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
|
|
30901
31086
|
function onEnter(input, onLine) {
|
|
30902
31087
|
if (!input.isTTY) return () => {
|
|
@@ -30916,10 +31101,10 @@ function onEnter(input, onLine) {
|
|
|
30916
31101
|
init_dist();
|
|
30917
31102
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
30918
31103
|
import { hostname } from "os";
|
|
30919
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
31104
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
30920
31105
|
import { execFile as execFile2, execSync } from "child_process";
|
|
30921
|
-
import { homedir as
|
|
30922
|
-
import { join as
|
|
31106
|
+
import { homedir as homedir10 } from "os";
|
|
31107
|
+
import { join as join10 } from "path";
|
|
30923
31108
|
|
|
30924
31109
|
// src/run.ts
|
|
30925
31110
|
init_dist();
|
|
@@ -30927,7 +31112,7 @@ init_dist();
|
|
|
30927
31112
|
// src/harness/session.ts
|
|
30928
31113
|
import { spawn } from "child_process";
|
|
30929
31114
|
import { existsSync as existsSync3 } from "fs";
|
|
30930
|
-
import { homedir as
|
|
31115
|
+
import { homedir as homedir4 } from "os";
|
|
30931
31116
|
import { resolve, delimiter as delimiter2 } from "path";
|
|
30932
31117
|
|
|
30933
31118
|
// src/harness/acp.ts
|
|
@@ -30938,19 +31123,23 @@ function optionFor(options, decision) {
|
|
|
30938
31123
|
return options.find((o) => o.kind === want)?.optionId ?? null;
|
|
30939
31124
|
}
|
|
30940
31125
|
function createAcpDriver(opts) {
|
|
30941
|
-
return new AcpDriver(opts.cwd, opts.mode, opts.resume);
|
|
31126
|
+
return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
|
|
30942
31127
|
}
|
|
30943
31128
|
var AcpDriver = class {
|
|
30944
31129
|
/** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
|
|
30945
|
-
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
30946
|
-
|
|
31130
|
+
* the session a bound identity belongs to, so the process this drives IS its holder.
|
|
31131
|
+
* `meta`: the adapter's own `_meta` on the frame that opens the session — where a switch only
|
|
31132
|
+
* one adapter understands rides (`session.ts`, a Claude session's tools). */
|
|
31133
|
+
constructor(cwd, mode, resume, meta) {
|
|
30947
31134
|
this.cwd = cwd;
|
|
30948
31135
|
this.mode = mode;
|
|
30949
31136
|
this.resume = resume;
|
|
31137
|
+
this.meta = meta;
|
|
30950
31138
|
}
|
|
30951
31139
|
cwd;
|
|
30952
31140
|
mode;
|
|
30953
31141
|
resume;
|
|
31142
|
+
meta;
|
|
30954
31143
|
nextId = 1;
|
|
30955
31144
|
initId;
|
|
30956
31145
|
sessionNewId;
|
|
@@ -30970,6 +31159,11 @@ var AcpDriver = class {
|
|
|
30970
31159
|
/** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
|
|
30971
31160
|
text = "";
|
|
30972
31161
|
tools = [];
|
|
31162
|
+
/** Tool calls opened and not yet reported `completed` or `failed`, by `toolCallId` — whoever
|
|
31163
|
+
* started them. A running tool is the one long silence that is still a working turn (`quiet`). */
|
|
31164
|
+
running = /* @__PURE__ */ new Set();
|
|
31165
|
+
/** The prompt in flight was already cancelled for silence: once is the whole remedy. */
|
|
31166
|
+
cancelled = false;
|
|
30973
31167
|
/** The opening frame. Everything after is driven by responses in `handleLine`. */
|
|
30974
31168
|
open() {
|
|
30975
31169
|
this.initId = this.nextId++;
|
|
@@ -31001,6 +31195,29 @@ var AcpDriver = class {
|
|
|
31001
31195
|
optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
|
|
31002
31196
|
];
|
|
31003
31197
|
}
|
|
31198
|
+
/**
|
|
31199
|
+
* THE ANSWER THAT NEVER COMES (2026-09-26, session "mute"). A turn ends when the adapter answers
|
|
31200
|
+
* its prompt, and nothing else ends one. claude-agent-acp can fold a prompt into a turn the agent
|
|
31201
|
+
* started on its own and never answer it (agentclientprotocol/claude-agent-acp#1145): the prompt
|
|
31202
|
+
* stayed in flight, no `idle` came, the pump never saw the agent rest, and the session was deaf
|
|
31203
|
+
* for twelve hours with its heartbeat alive.
|
|
31204
|
+
*
|
|
31205
|
+
* The caller's clock says nothing has crossed the pipe for `ms`; this says whether that silence
|
|
31206
|
+
* is still a turn. A running tool is (a ten-minute build streams nothing), and so is a permission
|
|
31207
|
+
* waiting on the person. Anything else, with a prompt in flight, is a turn that is over without
|
|
31208
|
+
* its answer, and the remedy is the protocol's own settle: `session/cancel`, which the adapter
|
|
31209
|
+
* answers `stopReason: "cancelled"` through the ordinary response path above — a turn that
|
|
31210
|
+
* ended, not one that failed. Once per prompt, and said as a crumb: a silent recovery is
|
|
31211
|
+
* indistinguishable from a working turn.
|
|
31212
|
+
*/
|
|
31213
|
+
quiet(ms) {
|
|
31214
|
+
if (this.promptId === void 0 || this.cancelled || this.running.size || this.pending.size) return none;
|
|
31215
|
+
this.cancelled = true;
|
|
31216
|
+
return {
|
|
31217
|
+
events: [{ kind: "error", message: `turn settled: no reply after ${Math.round(ms / 1e3)}s quiet with no tool running \u2014 sent session/cancel` }],
|
|
31218
|
+
writes: [frame2({ method: "session/cancel", params: { sessionId: this.sessionId } })]
|
|
31219
|
+
};
|
|
31220
|
+
}
|
|
31004
31221
|
/** Cancel anything still blocked — called on stop so the process can exit cleanly. */
|
|
31005
31222
|
close() {
|
|
31006
31223
|
const writes = [...this.pending.values()].map(
|
|
@@ -31032,10 +31249,7 @@ var AcpDriver = class {
|
|
|
31032
31249
|
return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
|
|
31033
31250
|
}
|
|
31034
31251
|
this.sessionNewId = this.nextId++;
|
|
31035
|
-
return {
|
|
31036
|
-
events: [],
|
|
31037
|
-
writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : this.sessionNew(this.sessionNewId))]
|
|
31038
|
-
};
|
|
31252
|
+
return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
|
|
31039
31253
|
}
|
|
31040
31254
|
if (msg.id === this.sessionNewId) {
|
|
31041
31255
|
this.sessionNewId = void 0;
|
|
@@ -31045,7 +31259,7 @@ var AcpDriver = class {
|
|
|
31045
31259
|
this.sessionNewId = this.nextId++;
|
|
31046
31260
|
return {
|
|
31047
31261
|
events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
|
|
31048
|
-
writes: [frame2(this.
|
|
31262
|
+
writes: [frame2(this.opening(this.sessionNewId))]
|
|
31049
31263
|
};
|
|
31050
31264
|
}
|
|
31051
31265
|
const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
|
|
@@ -31070,6 +31284,8 @@ var AcpDriver = class {
|
|
|
31070
31284
|
}
|
|
31071
31285
|
if (msg.id === this.promptId) {
|
|
31072
31286
|
this.promptId = void 0;
|
|
31287
|
+
this.running.clear();
|
|
31288
|
+
this.cancelled = false;
|
|
31073
31289
|
const events = [];
|
|
31074
31290
|
const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
|
|
31075
31291
|
if (report || this.tools.length) {
|
|
@@ -31101,6 +31317,10 @@ var AcpDriver = class {
|
|
|
31101
31317
|
handleNotification(msg) {
|
|
31102
31318
|
if (msg.method !== "session/update") return none;
|
|
31103
31319
|
const update = msg.params?.update;
|
|
31320
|
+
if (update?.toolCallId) {
|
|
31321
|
+
if (update.status === "completed" || update.status === "failed") this.running.delete(update.toolCallId);
|
|
31322
|
+
else if (update.sessionUpdate === "tool_call") this.running.add(update.toolCallId);
|
|
31323
|
+
}
|
|
31104
31324
|
switch (update?.sessionUpdate) {
|
|
31105
31325
|
case "agent_message_chunk":
|
|
31106
31326
|
this.text += update.content?.text ?? "";
|
|
@@ -31108,9 +31328,9 @@ var AcpDriver = class {
|
|
|
31108
31328
|
case "tool_call": {
|
|
31109
31329
|
const title = update.title?.trim() || update.kind || "tool";
|
|
31110
31330
|
this.tools.push(title);
|
|
31111
|
-
const
|
|
31331
|
+
const note2 = this.text.slice(this.reportFrom).trim();
|
|
31112
31332
|
this.reportFrom = this.text.length;
|
|
31113
|
-
return { events: [{ kind: "work", tool: title, ...
|
|
31333
|
+
return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
|
|
31114
31334
|
}
|
|
31115
31335
|
default:
|
|
31116
31336
|
return none;
|
|
@@ -31144,8 +31364,11 @@ var AcpDriver = class {
|
|
|
31144
31364
|
writes: []
|
|
31145
31365
|
};
|
|
31146
31366
|
}
|
|
31147
|
-
|
|
31148
|
-
|
|
31367
|
+
/** The frame that opens the conversation: a resume of `resume` while there is one to try,
|
|
31368
|
+
* else a new session. */
|
|
31369
|
+
opening(id) {
|
|
31370
|
+
const params = { cwd: this.cwd, mcpServers: [], ...this.meta ? { _meta: this.meta } : {} };
|
|
31371
|
+
return this.resume ? { id, method: "session/resume", params: { sessionId: this.resume, ...params } } : { id, method: "session/new", params };
|
|
31149
31372
|
}
|
|
31150
31373
|
/** Send the queued prompts as one turn, if the agent can take one right now. */
|
|
31151
31374
|
flush() {
|
|
@@ -31166,11 +31389,93 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
|
|
|
31166
31389
|
|
|
31167
31390
|
// src/harness/session.ts
|
|
31168
31391
|
init_catalog();
|
|
31392
|
+
|
|
31393
|
+
// src/paigy/inbox.ts
|
|
31394
|
+
import { mkdirSync as mkdirSync2, readdirSync, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
31395
|
+
import { join as join4 } from "path";
|
|
31396
|
+
import { homedir as homedir3 } from "os";
|
|
31397
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
31398
|
+
var MIDTURN_HOOK = `d="$PAIGY_INBOX"; [ -n "$d" ] || { cat >/dev/null; exit 0; }; set -- "$d"/*.note; [ -e "$1" ] || { cat >/dev/null; exit 0; }; grep '"agent_id"' >/dev/null && exit 0; mkdir -p "$d/taken"; o=""; for f in "$@"; do n=\${f##*/}; mv "$f" "$d/taken/$n" 2>/dev/null || continue; touch "$d/taken/$n"; o="$o\${o:+\\n\\n}$(cat "$d/taken/$n")"; done; [ -n "$o" ] || exit 0; printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}' "$o"`;
|
|
31399
|
+
var missing = (error) => error.code === "ENOENT";
|
|
31400
|
+
function openInbox(dir) {
|
|
31401
|
+
mkdirSync2(dir, { recursive: true });
|
|
31402
|
+
let written = 0;
|
|
31403
|
+
const receipt = (name) => join4(dir, "taken", `${name}.note`);
|
|
31404
|
+
const taken = (name) => {
|
|
31405
|
+
try {
|
|
31406
|
+
return statSync2(receipt(name)).mtimeMs;
|
|
31407
|
+
} catch (error) {
|
|
31408
|
+
if (missing(error)) return null;
|
|
31409
|
+
throw error;
|
|
31410
|
+
}
|
|
31411
|
+
};
|
|
31412
|
+
return {
|
|
31413
|
+
dir,
|
|
31414
|
+
put(text) {
|
|
31415
|
+
const name = `${Date.now()}-${String(++written).padStart(6, "0")}`;
|
|
31416
|
+
const tmp = join4(dir, `${name}.tmp`);
|
|
31417
|
+
writeFileSync2(tmp, JSON.stringify(text).slice(1, -1));
|
|
31418
|
+
renameSync(tmp, join4(dir, `${name}.note`));
|
|
31419
|
+
return name;
|
|
31420
|
+
},
|
|
31421
|
+
taken,
|
|
31422
|
+
settle(name) {
|
|
31423
|
+
try {
|
|
31424
|
+
renameSync(join4(dir, `${name}.note`), join4(dir, `${name}.back`));
|
|
31425
|
+
rmSync2(join4(dir, `${name}.back`), { force: true });
|
|
31426
|
+
return "retracted";
|
|
31427
|
+
} catch (error) {
|
|
31428
|
+
if (!missing(error)) throw error;
|
|
31429
|
+
}
|
|
31430
|
+
const seen = taken(name) !== null;
|
|
31431
|
+
rmSync2(receipt(name), { force: true });
|
|
31432
|
+
return seen ? "taken" : "retracted";
|
|
31433
|
+
},
|
|
31434
|
+
close() {
|
|
31435
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
31436
|
+
}
|
|
31437
|
+
};
|
|
31438
|
+
}
|
|
31439
|
+
var INBOX_ROOT = join4(homedir3(), ".paigy", "inbox");
|
|
31440
|
+
var alive = (pid) => {
|
|
31441
|
+
try {
|
|
31442
|
+
process.kill(pid, 0);
|
|
31443
|
+
return true;
|
|
31444
|
+
} catch (error) {
|
|
31445
|
+
return error.code === "EPERM";
|
|
31446
|
+
}
|
|
31447
|
+
};
|
|
31448
|
+
function inboxFor(root = INBOX_ROOT) {
|
|
31449
|
+
try {
|
|
31450
|
+
for (const name of readdirSync(root)) {
|
|
31451
|
+
const pid = Number(name.split("-")[0]);
|
|
31452
|
+
if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join4(root, name), { recursive: true, force: true });
|
|
31453
|
+
}
|
|
31454
|
+
} catch (error) {
|
|
31455
|
+
if (!missing(error)) throw error;
|
|
31456
|
+
}
|
|
31457
|
+
return openInbox(join4(root, `${process.pid}-${randomUUID4().slice(0, 8)}`));
|
|
31458
|
+
}
|
|
31459
|
+
|
|
31460
|
+
// src/harness/session.ts
|
|
31169
31461
|
var ADAPTER_BIN = {
|
|
31170
31462
|
claude: "claude-agent-acp",
|
|
31171
31463
|
codex: "codex-acp",
|
|
31172
31464
|
agy: "paigy-agy-acp"
|
|
31173
31465
|
};
|
|
31466
|
+
var CLAUDE_ONE_WAY_IN = {
|
|
31467
|
+
env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" },
|
|
31468
|
+
disallowedTools: ["Monitor", "ScheduleWakeup", "CronCreate", "Workflow"]
|
|
31469
|
+
};
|
|
31470
|
+
var CLAUDE_META = {
|
|
31471
|
+
claudeCode: {
|
|
31472
|
+
options: {
|
|
31473
|
+
disallowedTools: CLAUDE_ONE_WAY_IN.disallowedTools,
|
|
31474
|
+
settings: { hooks: { PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: MIDTURN_HOOK }] }] } }
|
|
31475
|
+
}
|
|
31476
|
+
}
|
|
31477
|
+
};
|
|
31478
|
+
var QUIET_MS = 15 * 6e4;
|
|
31174
31479
|
function splitLines(buffer, chunk) {
|
|
31175
31480
|
const combined = buffer + chunk;
|
|
31176
31481
|
const parts = combined.split("\n");
|
|
@@ -31178,7 +31483,7 @@ function splitLines(buffer, chunk) {
|
|
|
31178
31483
|
return { lines: parts.filter((l) => l.trim()), rest };
|
|
31179
31484
|
}
|
|
31180
31485
|
function startSession(opts) {
|
|
31181
|
-
const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/,
|
|
31486
|
+
const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
|
|
31182
31487
|
if (!existsSync3(cwd)) {
|
|
31183
31488
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
31184
31489
|
}
|
|
@@ -31198,7 +31503,9 @@ function startSession(opts) {
|
|
|
31198
31503
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
31199
31504
|
// fresh spawn has none, which is how a fresh run read as "not harness-started" to the
|
|
31200
31505
|
// session-start hook and the `listen` tool (they now test either mark; listening.ts).
|
|
31201
|
-
PAIGY_HARNESS: "1"
|
|
31506
|
+
PAIGY_HARNESS: "1",
|
|
31507
|
+
...opts.harness === "claude" ? CLAUDE_ONE_WAY_IN.env : {},
|
|
31508
|
+
...opts.harness === "claude" && opts.inbox ? { PAIGY_INBOX: opts.inbox } : {}
|
|
31202
31509
|
},
|
|
31203
31510
|
stdio: ["pipe", "pipe", "pipe"]
|
|
31204
31511
|
});
|
|
@@ -31209,6 +31516,16 @@ function startSession(opts) {
|
|
|
31209
31516
|
if (ended2) return;
|
|
31210
31517
|
for (const f of frames) child.stdin?.write(`${f}
|
|
31211
31518
|
`);
|
|
31519
|
+
if (frames.length) listen();
|
|
31520
|
+
};
|
|
31521
|
+
const apply = ({ events, writes }) => {
|
|
31522
|
+
write(writes);
|
|
31523
|
+
for (const event of events) opts.onEvent(event);
|
|
31524
|
+
};
|
|
31525
|
+
let hush;
|
|
31526
|
+
const listen = () => {
|
|
31527
|
+
clearTimeout(hush);
|
|
31528
|
+
if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
|
|
31212
31529
|
};
|
|
31213
31530
|
child.stderr?.on("data", (chunk) => {
|
|
31214
31531
|
const text = String(chunk).trim();
|
|
@@ -31216,6 +31533,7 @@ function startSession(opts) {
|
|
|
31216
31533
|
});
|
|
31217
31534
|
child.on("exit", (code) => {
|
|
31218
31535
|
ended2 = true;
|
|
31536
|
+
clearTimeout(hush);
|
|
31219
31537
|
opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
|
|
31220
31538
|
opts.onExit?.();
|
|
31221
31539
|
});
|
|
@@ -31223,16 +31541,18 @@ function startSession(opts) {
|
|
|
31223
31541
|
opts.onEvent({ kind: "error", message: e.message });
|
|
31224
31542
|
opts.onExit?.();
|
|
31225
31543
|
});
|
|
31226
|
-
const driver = createAcpDriver({
|
|
31544
|
+
const driver = createAcpDriver({
|
|
31545
|
+
cwd,
|
|
31546
|
+
mode: opts.mode,
|
|
31547
|
+
...opts.resume ? { resume: opts.resume } : {},
|
|
31548
|
+
...opts.harness === "claude" ? { meta: CLAUDE_META } : {}
|
|
31549
|
+
});
|
|
31227
31550
|
let buffer = "";
|
|
31228
31551
|
child.stdout?.on("data", (chunk) => {
|
|
31552
|
+
listen();
|
|
31229
31553
|
const { lines: lines2, rest } = splitLines(buffer, String(chunk));
|
|
31230
31554
|
buffer = rest;
|
|
31231
|
-
for (const line of lines2)
|
|
31232
|
-
const { events, writes } = driver.handleLine(line);
|
|
31233
|
-
write(writes);
|
|
31234
|
-
for (const event of events) opts.onEvent(event);
|
|
31235
|
-
}
|
|
31555
|
+
for (const line of lines2) apply(driver.handleLine(line));
|
|
31236
31556
|
});
|
|
31237
31557
|
write(driver.open());
|
|
31238
31558
|
return {
|
|
@@ -31245,6 +31565,7 @@ function startSession(opts) {
|
|
|
31245
31565
|
stop() {
|
|
31246
31566
|
write(driver.close());
|
|
31247
31567
|
ended2 = true;
|
|
31568
|
+
clearTimeout(hush);
|
|
31248
31569
|
child.kill();
|
|
31249
31570
|
}
|
|
31250
31571
|
};
|
|
@@ -32601,18 +32922,18 @@ var AskInputSchema2 = external_exports.object({
|
|
|
32601
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."),
|
|
32602
32923
|
repo: external_exports.string().optional().describe("Optional repository context."),
|
|
32603
32924
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
32604
|
-
"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."
|
|
32605
32926
|
),
|
|
32606
|
-
options: external_exports.array(OptionInputSchema2).min(
|
|
32927
|
+
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
|
|
32607
32928
|
}).strict();
|
|
32608
32929
|
var StartContactSchema2 = external_exports.object({
|
|
32609
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."),
|
|
32610
|
-
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."),
|
|
32611
32932
|
channel: external_exports.enum(["notification", "call"]).default("notification")
|
|
32612
32933
|
}).strict();
|
|
32613
32934
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
32614
32935
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
32615
|
-
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.";
|
|
32616
32937
|
var CreateGoalSchema2 = external_exports.object({
|
|
32617
32938
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
32618
32939
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -32650,16 +32971,29 @@ var UpdateGoalSchema2 = external_exports.object({
|
|
|
32650
32971
|
state: external_exports.enum(["active", "done", "cancelled"]).optional(),
|
|
32651
32972
|
progress: external_exports.string().trim().min(1).max(1e4).optional(),
|
|
32652
32973
|
reviewed: external_exports.literal(true).optional(),
|
|
32653
|
-
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()
|
|
32654
32979
|
}).strict().refine((v) => Object.keys(v).length > 0),
|
|
32655
32980
|
reason: external_exports.string().trim().min(1).max(2e3),
|
|
32656
32981
|
operationId: external_exports.string().uuid().optional()
|
|
32657
32982
|
}).strict();
|
|
32658
32983
|
var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
|
|
32659
32984
|
var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
|
|
32660
|
-
var GetGoalSchema2 = external_exports.object({
|
|
32661
|
-
|
|
32662
|
-
|
|
32985
|
+
var GetGoalSchema2 = external_exports.object({
|
|
32986
|
+
goalId: external_exports.string().uuid(),
|
|
32987
|
+
/** Every entry in full. Without it the read carries the person's words, open questions and your
|
|
32988
|
+
* newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
|
|
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()
|
|
32994
|
+
}).strict();
|
|
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.`;
|
|
32663
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.";
|
|
32664
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.";
|
|
32665
32999
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
@@ -33278,18 +33612,20 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33278
33612
|
* clobber guard as voiceMode: absent = leave unchanged on write. */
|
|
33279
33613
|
broker: BrokerTuningSchema2.optional()
|
|
33280
33614
|
});
|
|
33281
|
-
var
|
|
33615
|
+
var HistoryWorkSchema2 = external_exports.object({
|
|
33282
33616
|
id: external_exports.string(),
|
|
33283
|
-
/** 'user' = a request you sent; 'agent' = a notification an agent sent you. */
|
|
33284
|
-
initiator: external_exports.enum(["user", "agent"]),
|
|
33285
33617
|
title: external_exports.string(),
|
|
33286
|
-
|
|
33287
|
-
|
|
33288
|
-
|
|
33289
|
-
|
|
33290
|
-
|
|
33291
|
-
|
|
33292
|
-
|
|
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()
|
|
33293
33629
|
});
|
|
33294
33630
|
var ACTIVITY_LINES2 = 2;
|
|
33295
33631
|
var ACTIVITY_LINE_MAX2 = 80;
|
|
@@ -33473,6 +33809,9 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33473
33809
|
progressLine: external_exports.string().nullable().optional(),
|
|
33474
33810
|
reviewPending: external_exports.boolean().default(false),
|
|
33475
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(),
|
|
33476
33815
|
/** The Goal this one was opened under; null at the root. */
|
|
33477
33816
|
parentGoalId: external_exports.string().nullable().default(null),
|
|
33478
33817
|
/** Goals opened under this one — only those the same list holds. */
|
|
@@ -33481,8 +33820,17 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33481
33820
|
dependencyGoalIds: external_exports.array(external_exports.string()).default([]),
|
|
33482
33821
|
/** True while any gate is on a Goal that is not done — the walk draws it dashed. */
|
|
33483
33822
|
blocked: external_exports.boolean().default(false),
|
|
33484
|
-
/**
|
|
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. */
|
|
33485
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(),
|
|
33486
33834
|
/** Every note on it the person replied to (`QueueReplySchema`) — the page decides which to show.
|
|
33487
33835
|
* Optional, not defaulted: absent is none, and every hand-built queue (fixtures, the demo) need
|
|
33488
33836
|
* not spell an empty list. */
|
|
@@ -33494,7 +33842,19 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33494
33842
|
/** When its owner last SAID something about it (`goals.last_progress_at`, written by every
|
|
33495
33843
|
* `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
|
|
33496
33844
|
* state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
|
|
33497
|
-
lastProgressAt: external_exports.string().nullable().optional()
|
|
33845
|
+
lastProgressAt: external_exports.string().nullable().optional(),
|
|
33846
|
+
/** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
|
|
33847
|
+
* kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
|
|
33848
|
+
* note. A progress note is an Entry, so this is already the newer of the two: the person's note
|
|
33849
|
+
* shows the moment it is written, and the agent's reply or next note replaces it by being newer.
|
|
33850
|
+
* `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
|
|
33851
|
+
* Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
|
|
33852
|
+
latest: external_exports.object({
|
|
33853
|
+
from: external_exports.enum(["person", "agent"]),
|
|
33854
|
+
said: external_exports.string(),
|
|
33855
|
+
at: external_exports.string(),
|
|
33856
|
+
entryId: external_exports.string()
|
|
33857
|
+
}).nullable().optional()
|
|
33498
33858
|
});
|
|
33499
33859
|
var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
|
|
33500
33860
|
var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
|
|
@@ -33605,6 +33965,19 @@ var DeliveryConfigSchema2 = external_exports.object({
|
|
|
33605
33965
|
* carries credentials; only a `poll` registration can come back with null here. */
|
|
33606
33966
|
realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
|
|
33607
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
|
+
});
|
|
33608
33981
|
var WakeNudgeSchema2 = external_exports.object({
|
|
33609
33982
|
kind: external_exports.enum(["reply", "request", "callback"]),
|
|
33610
33983
|
notificationId: external_exports.string().optional(),
|
|
@@ -33649,6 +34022,9 @@ var DeviceTokenSchema2 = external_exports.object({
|
|
|
33649
34022
|
* landed in the FIRST granted workspace and the agent rediscovered its own repo from
|
|
33650
34023
|
* the thread each time (host.ts, live catch 2026-08-06 — prompt-papered until now). */
|
|
33651
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(),
|
|
33652
34028
|
uik_pub: external_exports.string().nullable().optional()
|
|
33653
34029
|
});
|
|
33654
34030
|
var SupportRequestSchema2 = external_exports.object({
|
|
@@ -33670,6 +34046,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
|
|
|
33670
34046
|
"other"
|
|
33671
34047
|
// anything else — the note carries it.
|
|
33672
34048
|
]);
|
|
34049
|
+
var SlimOptionSchema2 = OptionSchema2.omit({ html: true });
|
|
34050
|
+
var QuestionRowSchema2 = external_exports.object({
|
|
34051
|
+
/** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
|
|
34052
|
+
id: external_exports.string(),
|
|
34053
|
+
deliveryId: external_exports.string(),
|
|
34054
|
+
entryId: external_exports.string(),
|
|
34055
|
+
/** The decision it waits on; null for an update, which asks nothing. */
|
|
34056
|
+
needId: external_exports.string().nullable(),
|
|
34057
|
+
goalIds: external_exports.array(external_exports.string()),
|
|
34058
|
+
/** The name of the work it is about, when the read could word it. */
|
|
34059
|
+
goalTitle: external_exports.string().optional(),
|
|
34060
|
+
tokenId: external_exports.string().optional(),
|
|
34061
|
+
name: external_exports.string(),
|
|
34062
|
+
title: external_exports.string(),
|
|
34063
|
+
body: external_exports.string(),
|
|
34064
|
+
select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
|
|
34065
|
+
options: external_exports.array(SlimOptionSchema2),
|
|
34066
|
+
hasPreview: external_exports.boolean(),
|
|
34067
|
+
blocking: external_exports.boolean(),
|
|
34068
|
+
askedAt: external_exports.string().datetime(),
|
|
34069
|
+
ring: InboxItemSchema2.shape.ring,
|
|
34070
|
+
onCall: external_exports.literal(true).optional(),
|
|
34071
|
+
sealed: external_exports.boolean()
|
|
34072
|
+
});
|
|
34073
|
+
var WorkStateSchema2 = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
|
|
34074
|
+
var WorkRowSchema2 = external_exports.object({
|
|
34075
|
+
id: external_exports.string(),
|
|
34076
|
+
parentId: external_exports.string().nullable(),
|
|
34077
|
+
title: external_exports.string(),
|
|
34078
|
+
/** Straight off the Goal. */
|
|
34079
|
+
state: WorkStateSchema2,
|
|
34080
|
+
owner: external_exports.string().nullable(),
|
|
34081
|
+
revision: external_exports.number().int(),
|
|
34082
|
+
/** Open questions on it, counted to 100. */
|
|
34083
|
+
waiting: external_exports.number().int(),
|
|
34084
|
+
/** Held by a gate on work that is not done. */
|
|
34085
|
+
blocked: external_exports.boolean(),
|
|
34086
|
+
lastProgressAt: external_exports.string().datetime().nullable(),
|
|
34087
|
+
/** The line written for its newest progress note, else that note's first words. */
|
|
34088
|
+
line: external_exports.string().nullable(),
|
|
34089
|
+
/** Work directly under it, counted to 100; the list carries up to 12 of them. */
|
|
34090
|
+
children: external_exports.number().int(),
|
|
34091
|
+
createdAt: external_exports.string().datetime(),
|
|
34092
|
+
updatedAt: external_exports.string().datetime(),
|
|
34093
|
+
/** When anything at or under it last moved — the order the list is in. */
|
|
34094
|
+
activeAt: external_exports.string().datetime(),
|
|
34095
|
+
/** A sealed outcome has no title here; the work's page opens it. */
|
|
34096
|
+
sealed: external_exports.boolean()
|
|
34097
|
+
});
|
|
34098
|
+
var ComputerRowSchema2 = ConnectionSummarySchema2.omit({ activity: true });
|
|
34099
|
+
var AgentRowSchema2 = ComputerRowSchema2.extend({
|
|
34100
|
+
/** Open questions it is asking the person, over every open card; null when that read failed. */
|
|
34101
|
+
asking: external_exports.number().int().nullable(),
|
|
34102
|
+
oldestAskAt: external_exports.string().datetime().nullable(),
|
|
34103
|
+
/** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
|
|
34104
|
+
* many in all among the account's 200 most recently active agent-held live Goals
|
|
34105
|
+
* (`agent_holds`); null when that read failed. */
|
|
34106
|
+
holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
|
|
34107
|
+
held: external_exports.number().int().nullable(),
|
|
34108
|
+
/** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
|
|
34109
|
+
* while none has, or when that read failed. */
|
|
34110
|
+
cold: external_exports.string().datetime().nullable(),
|
|
34111
|
+
/** The newest line of its working log, and when the harness saw it. */
|
|
34112
|
+
line: external_exports.string().nullable(),
|
|
34113
|
+
lineAt: external_exports.string().datetime().nullable()
|
|
34114
|
+
});
|
|
34115
|
+
var SnapshotSchema2 = external_exports.object({
|
|
34116
|
+
/** The API's clock, taken before the first read: what a later delta will start from. */
|
|
34117
|
+
at: external_exports.string().datetime(),
|
|
34118
|
+
questions: external_exports.object({
|
|
34119
|
+
/** The newest 30 open cards, questions before updates. */
|
|
34120
|
+
items: external_exports.array(QuestionRowSchema2),
|
|
34121
|
+
/** Every open question, and apart from them every update, and what was put off. */
|
|
34122
|
+
total: external_exports.number().int(),
|
|
34123
|
+
updates: external_exports.number().int(),
|
|
34124
|
+
putOff: external_exports.number().int()
|
|
34125
|
+
}).nullable(),
|
|
34126
|
+
agents: external_exports.object({
|
|
34127
|
+
/** Up to 60, most recently seen first. */
|
|
34128
|
+
items: external_exports.array(AgentRowSchema2),
|
|
34129
|
+
more: external_exports.boolean()
|
|
34130
|
+
}).nullable(),
|
|
34131
|
+
work: external_exports.object({
|
|
34132
|
+
/** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
|
|
34133
|
+
items: external_exports.array(WorkRowSchema2),
|
|
34134
|
+
/** How much work is behind each of the Work tab's four filters, each counted to 100, read with
|
|
34135
|
+
* the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
|
|
34136
|
+
* `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
|
|
34137
|
+
* is `ready`; Done is `done` or `cancelled`. */
|
|
34138
|
+
counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
|
|
34139
|
+
}).nullable(),
|
|
34140
|
+
you: external_exports.object({
|
|
34141
|
+
settings: UserSettingsSchema2,
|
|
34142
|
+
callable: external_exports.boolean(),
|
|
34143
|
+
/** Up to 20 paired computers; null when the roster read failed. */
|
|
34144
|
+
computers: external_exports.array(ComputerRowSchema2).nullable()
|
|
34145
|
+
}).nullable()
|
|
34146
|
+
});
|
|
33673
34147
|
|
|
33674
34148
|
// src/paigy/activity.ts
|
|
33675
34149
|
function shortenPaths(s) {
|
|
@@ -33679,8 +34153,8 @@ function shortenPaths(s) {
|
|
|
33679
34153
|
});
|
|
33680
34154
|
}
|
|
33681
34155
|
function workLine(event) {
|
|
33682
|
-
const
|
|
33683
|
-
const line = shortenPaths([event.tool.trim(),
|
|
34156
|
+
const note2 = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
|
|
34157
|
+
const line = shortenPaths([event.tool.trim(), note2.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
|
|
33684
34158
|
return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
|
|
33685
34159
|
}
|
|
33686
34160
|
function pushWork(lines2, line) {
|
|
@@ -33694,127 +34168,141 @@ function sameTail(a, b) {
|
|
|
33694
34168
|
// src/paigy/bridge.ts
|
|
33695
34169
|
init_dist();
|
|
33696
34170
|
|
|
33697
|
-
// src/paigy/
|
|
33698
|
-
|
|
33699
|
-
|
|
33700
|
-
|
|
33701
|
-
|
|
33702
|
-
|
|
33703
|
-
|
|
33704
|
-
|
|
33705
|
-
|
|
33706
|
-
|
|
33707
|
-
|
|
33708
|
-
|
|
33709
|
-
|
|
33710
|
-
|
|
33711
|
-
|
|
33712
|
-
|
|
33713
|
-
|
|
33714
|
-
|
|
33715
|
-
|
|
33716
|
-
|
|
33717
|
-
|
|
33718
|
-
|
|
33719
|
-
|
|
33720
|
-
|
|
33721
|
-
|
|
33722
|
-
/\b(how would you like|how should we|what would you like|what should we)\b/,
|
|
33723
|
-
/\b(please choose|choose one|select one|how do you want)\b/
|
|
33724
|
-
].some((re) => re.test(tail));
|
|
34171
|
+
// src/paigy/limit.ts
|
|
34172
|
+
var LIMIT_PREFIXES = [
|
|
34173
|
+
"You've hit your",
|
|
34174
|
+
"You've reached your",
|
|
34175
|
+
"You're out of usage credits",
|
|
34176
|
+
"Your org is out of usage",
|
|
34177
|
+
"Your seat type doesn't include usage",
|
|
34178
|
+
"Your usage allocation has been disabled by your admin",
|
|
34179
|
+
"Your group's usage limit is set to $0",
|
|
34180
|
+
"requires usage credits",
|
|
34181
|
+
"You're out of extra usage",
|
|
34182
|
+
"Your seat type doesn't include extra usage"
|
|
34183
|
+
];
|
|
34184
|
+
var RETRY_MS2 = 15 * 6e4;
|
|
34185
|
+
var MARGIN_MS = 6e4;
|
|
34186
|
+
var FARTHEST_MS = 8 * 24 * 60 * 6e4;
|
|
34187
|
+
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
34188
|
+
function limitIn(text, now) {
|
|
34189
|
+
const line = (text ?? "").split("\n").map((l) => l.trim()).reverse().find((l) => LIMIT_PREFIXES.some((p) => l.startsWith(p) || p === "requires usage credits" && l.includes(p)));
|
|
34190
|
+
if (!line) return null;
|
|
34191
|
+
const reset = resetOf(line, now);
|
|
34192
|
+
if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
|
|
34193
|
+
return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock3(reset) };
|
|
34194
|
+
}
|
|
34195
|
+
return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
|
|
33725
34196
|
}
|
|
33726
|
-
|
|
33727
|
-
|
|
33728
|
-
|
|
33729
|
-
|
|
33730
|
-
|
|
33731
|
-
|
|
33732
|
-
|
|
33733
|
-
|
|
33734
|
-
|
|
33735
|
-
|
|
33736
|
-
|
|
33737
|
-
|
|
34197
|
+
function resetOf(line, now) {
|
|
34198
|
+
const m = /resets\s+(?:at\s+)?(?:([a-z]{3})[a-z]*\s+(\d{1,2}),?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b(?:\s*\(([^)]+)\))?/i.exec(line);
|
|
34199
|
+
if (!m) return null;
|
|
34200
|
+
const zone = zoneOr(m[6]);
|
|
34201
|
+
let hour = Number(m[3]) % 12;
|
|
34202
|
+
if (m[5].toLowerCase() === "pm") hour += 12;
|
|
34203
|
+
const minute = Number(m[4] ?? 0);
|
|
34204
|
+
const today = partsAt(now, zone);
|
|
34205
|
+
if (m[1]) {
|
|
34206
|
+
const month = MONTHS.indexOf(m[1].toLowerCase());
|
|
34207
|
+
if (month < 0) return null;
|
|
34208
|
+
const at2 = wallToEpoch(today.year, month, Number(m[2]), hour, minute, zone);
|
|
34209
|
+
return at2 > now ? at2 : wallToEpoch(today.year + 1, month, Number(m[2]), hour, minute, zone);
|
|
34210
|
+
}
|
|
34211
|
+
const at = wallToEpoch(today.year, today.month, today.day, hour, minute, zone);
|
|
34212
|
+
return at > now ? at : wallToEpoch(today.year, today.month, today.day + 1, hour, minute, zone);
|
|
34213
|
+
}
|
|
34214
|
+
function zoneOr(zone) {
|
|
34215
|
+
const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
34216
|
+
if (!zone) return local;
|
|
34217
|
+
try {
|
|
34218
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone.trim() });
|
|
34219
|
+
return zone.trim();
|
|
34220
|
+
} catch {
|
|
34221
|
+
return local;
|
|
33738
34222
|
}
|
|
33739
|
-
return words2;
|
|
33740
34223
|
}
|
|
33741
|
-
|
|
33742
|
-
|
|
33743
|
-
|
|
33744
|
-
|
|
33745
|
-
|
|
33746
|
-
if (opts?.initialPrompt?.trim()) {
|
|
33747
|
-
const prompt = opts.initialPrompt.trim();
|
|
33748
|
-
opts.initialPrompt = void 0;
|
|
33749
|
-
const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID4() });
|
|
33750
|
-
brief2 = await rail.claim();
|
|
33751
|
-
if (!brief2.goalId || brief2.revision === void 0) {
|
|
33752
|
-
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
33753
|
-
if (checkCallOnDone(prompt)) state.callOnDone = true;
|
|
33754
|
-
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
33755
|
-
return;
|
|
33756
|
-
}
|
|
33757
|
-
} else {
|
|
33758
|
-
return;
|
|
33759
|
-
}
|
|
33760
|
-
}
|
|
33761
|
-
unheard(brief2, state);
|
|
33762
|
-
if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
|
|
33763
|
-
else state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
33764
|
-
const outcome = brief2.outcome?.trim() ?? "";
|
|
33765
|
-
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
|
|
33766
|
-
const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
|
|
33767
|
-
if (checkCallOnDone(fullText)) state.callOnDone = true;
|
|
33768
|
-
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
33769
|
-
return;
|
|
33770
|
-
}
|
|
33771
|
-
const brief = await rail.read(state.goal.id);
|
|
33772
|
-
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
33773
|
-
for (const answer of brief.answers ?? []) {
|
|
33774
|
-
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
33775
|
-
if (!resolve3) continue;
|
|
33776
|
-
state.asks.delete(answer.requestEntryId);
|
|
33777
|
-
resolve3(answer.result);
|
|
33778
|
-
}
|
|
33779
|
-
if (ended(brief.state)) {
|
|
33780
|
-
state.goal = void 0;
|
|
33781
|
-
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
33782
|
-
state.asks?.clear();
|
|
33783
|
-
return;
|
|
33784
|
-
}
|
|
33785
|
-
if (state.asks?.size || !state.resting) return;
|
|
33786
|
-
const words2 = unheard(brief, state);
|
|
33787
|
-
if (words2.length) {
|
|
33788
|
-
const text = words2.join("\n\n");
|
|
33789
|
-
if (checkCallOnDone(text)) state.callOnDone = true;
|
|
33790
|
-
send2(session, state, text);
|
|
33791
|
-
}
|
|
34224
|
+
function partsAt(t, zone) {
|
|
34225
|
+
const p = Object.fromEntries(
|
|
34226
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" }).formatToParts(new Date(t)).map((x) => [x.type, x.value])
|
|
34227
|
+
);
|
|
34228
|
+
return { year: Number(p.year), month: Number(p.month) - 1, day: Number(p.day), hour: Number(p.hour), minute: Number(p.minute), second: Number(p.second) };
|
|
33792
34229
|
}
|
|
33793
|
-
function
|
|
33794
|
-
|
|
33795
|
-
|
|
34230
|
+
function wallToEpoch(year, month, day, hour, minute, zone) {
|
|
34231
|
+
const wall = Date.UTC(year, month, day, hour, minute);
|
|
34232
|
+
const offset = (t) => {
|
|
34233
|
+
const p = partsAt(t, zone);
|
|
34234
|
+
return Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - Math.floor(t / 1e3) * 1e3;
|
|
34235
|
+
};
|
|
34236
|
+
const first = wall - offset(wall);
|
|
34237
|
+
return wall - offset(first);
|
|
33796
34238
|
}
|
|
33797
|
-
|
|
33798
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
34239
|
+
function clock3(t) {
|
|
34240
|
+
return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
|
|
34241
|
+
}
|
|
34242
|
+
var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
|
|
34243
|
+
var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on where you stopped.";
|
|
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:";
|
|
34245
|
+
var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
|
|
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
|
+
}
|
|
34255
|
+
function episodes(opts = {}) {
|
|
34256
|
+
const gather2 = opts.gatherMs ?? GATHER_MS;
|
|
34257
|
+
const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
|
|
34258
|
+
let open = null;
|
|
34259
|
+
return {
|
|
34260
|
+
paused(name, now) {
|
|
34261
|
+
if (!open || open.called) open = { since: now, paused: [], running: [], called: false, scheduled: false };
|
|
34262
|
+
if (!open.paused.includes(name)) open.paused.push(name);
|
|
34263
|
+
open.running = open.running.filter((n) => n !== name);
|
|
34264
|
+
},
|
|
34265
|
+
running(name, call, log) {
|
|
34266
|
+
const episode = open;
|
|
34267
|
+
if (!episode || !episode.paused.includes(name)) return;
|
|
34268
|
+
if (episode.called) {
|
|
34269
|
+
log?.(`\u25B6 ${name} is running again after the limit (the call already went out)`);
|
|
34270
|
+
return;
|
|
34271
|
+
}
|
|
34272
|
+
if (!episode.running.includes(name)) episode.running.push(name);
|
|
34273
|
+
if (episode.scheduled) return;
|
|
34274
|
+
episode.scheduled = true;
|
|
34275
|
+
later(() => {
|
|
34276
|
+
if (!episode.running.length) {
|
|
34277
|
+
episode.scheduled = false;
|
|
34278
|
+
return;
|
|
34279
|
+
}
|
|
34280
|
+
episode.called = true;
|
|
34281
|
+
const rest = episode.paused.filter((n) => !episode.running.includes(n));
|
|
34282
|
+
const text = [
|
|
34283
|
+
`Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
|
|
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.`
|
|
34285
|
+
].join(" ");
|
|
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
|
+
);
|
|
34290
|
+
}, gather2);
|
|
33803
34291
|
}
|
|
33804
|
-
|
|
33805
|
-
await (opts.pause ?? wait)();
|
|
33806
|
-
}
|
|
34292
|
+
};
|
|
33807
34293
|
}
|
|
34294
|
+
var LIMITS = episodes();
|
|
33808
34295
|
|
|
33809
34296
|
// src/paigy/bridge.ts
|
|
33810
34297
|
function railFor(opts) {
|
|
33811
34298
|
return {
|
|
33812
|
-
claim: () => claimGoal(
|
|
34299
|
+
claim: (goalId) => claimGoal(goalId, opts),
|
|
33813
34300
|
read: (goalId) => getGoal(goalId, opts),
|
|
33814
34301
|
update: (goalId, input) => updateGoal(goalId, input, opts),
|
|
33815
34302
|
// One read and back: the pump owns the waiting, so a Call must not hold a window here.
|
|
33816
34303
|
contact: (input) => contact(input, { ...opts, waits: false }),
|
|
33817
|
-
create: (input) => createGoal(input, opts)
|
|
34304
|
+
create: (input) => createGoal(input, opts),
|
|
34305
|
+
waiting: () => checkReplies(opts)
|
|
33818
34306
|
};
|
|
33819
34307
|
}
|
|
33820
34308
|
function cancelAsks(state) {
|
|
@@ -33833,27 +34321,13 @@ function decisionFrom(result) {
|
|
|
33833
34321
|
}
|
|
33834
34322
|
return { allow: false, reason: "No approval given" };
|
|
33835
34323
|
}
|
|
33836
|
-
function wordsOf(result) {
|
|
33837
|
-
if (!result || !("kind" in result)) return null;
|
|
33838
|
-
switch (result.kind) {
|
|
33839
|
-
case "text":
|
|
33840
|
-
return result.text.trim() || null;
|
|
33841
|
-
case "confirm":
|
|
33842
|
-
return result.approved ? "yes" : "no";
|
|
33843
|
-
case "one":
|
|
33844
|
-
return result.option.label;
|
|
33845
|
-
case "many":
|
|
33846
|
-
case "rank":
|
|
33847
|
-
return result.options.map((o) => o.label).join(", ");
|
|
33848
|
-
}
|
|
33849
|
-
}
|
|
33850
34324
|
async function ask(state, rail, input) {
|
|
33851
34325
|
if (!state.goal) return null;
|
|
33852
34326
|
try {
|
|
33853
34327
|
const sent = await rail.contact({
|
|
33854
|
-
asks: [{ ask: input.ask, parentId: state.goal.id,
|
|
34328
|
+
asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
|
|
33855
34329
|
waiting: "hard",
|
|
33856
|
-
channel:
|
|
34330
|
+
channel: "call"
|
|
33857
34331
|
});
|
|
33858
34332
|
if ("recorded" in sent) return null;
|
|
33859
34333
|
const entryId = sent.requestEntryIds[0];
|
|
@@ -33864,106 +34338,416 @@ async function ask(state, rail, input) {
|
|
|
33864
34338
|
return null;
|
|
33865
34339
|
}
|
|
33866
34340
|
}
|
|
33867
|
-
function checkCallOnDone(text) {
|
|
33868
|
-
if (!text) return false;
|
|
33869
|
-
return /\b(call|ring|phone)\b.*\b(done|finish(ed)?|complete(d)?)\b/i.test(text) || /\b(when|if|once)\b.*\b(done|finish(ed)?|complete(d)?)\b.*\b(call|ring|phone)\b/i.test(text) || /\bcall\s+(me|us)\b/i.test(text);
|
|
33870
|
-
}
|
|
33871
|
-
function parseQuestionOptions(text) {
|
|
33872
|
-
if (!text) return { question: "" };
|
|
33873
|
-
const trimmed = text.trim();
|
|
33874
|
-
const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
33875
|
-
const optionLines = [];
|
|
33876
|
-
const questionLines = [];
|
|
33877
|
-
for (const line of lines2) {
|
|
33878
|
-
const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
|
|
33879
|
-
if (bulletMatch) {
|
|
33880
|
-
optionLines.push(bulletMatch[1].trim());
|
|
33881
|
-
} else {
|
|
33882
|
-
questionLines.push(line);
|
|
33883
|
-
}
|
|
33884
|
-
}
|
|
33885
|
-
if (optionLines.length >= 2 && optionLines.length <= 6) {
|
|
33886
|
-
return {
|
|
33887
|
-
question: questionLines.join("\n\n").trim() || trimmed,
|
|
33888
|
-
options: optionLines.map((raw) => {
|
|
33889
|
-
const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
|
|
33890
|
-
const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
|
|
33891
|
-
if (colonSplit && colonSplit[1] && colonSplit[2]) {
|
|
33892
|
-
return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
|
|
33893
|
-
}
|
|
33894
|
-
if (parenSplit && parenSplit[1] && parenSplit[2]) {
|
|
33895
|
-
return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
|
|
33896
|
-
}
|
|
33897
|
-
return { label: raw.slice(0, 1e3) };
|
|
33898
|
-
})
|
|
33899
|
-
};
|
|
33900
|
-
}
|
|
33901
|
-
return { question: trimmed };
|
|
33902
|
-
}
|
|
33903
34341
|
async function askPermission(event, state, rail) {
|
|
33904
34342
|
return decisionFrom(await ask(state, rail, {
|
|
33905
34343
|
ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
|
|
33906
34344
|
options: [
|
|
33907
34345
|
{ label: ALLOW, hint: "Permits the agent to run this command" },
|
|
33908
34346
|
{ label: "Deny", hint: "Blocks execution of this command" }
|
|
33909
|
-
]
|
|
33910
|
-
call: true
|
|
33911
|
-
}));
|
|
33912
|
-
}
|
|
33913
|
-
async function askQuestion(question, state, rail, opts) {
|
|
33914
|
-
return wordsOf(await ask(state, rail, {
|
|
33915
|
-
ask: question,
|
|
33916
|
-
call: opts?.call ?? true,
|
|
33917
|
-
options: opts?.options
|
|
34347
|
+
]
|
|
33918
34348
|
}));
|
|
33919
34349
|
}
|
|
33920
34350
|
async function finish(result, state, rail, opts) {
|
|
33921
34351
|
const goal = state.goal;
|
|
33922
34352
|
if (!goal) return;
|
|
33923
|
-
|
|
33924
|
-
|
|
33925
|
-
const channel = opts?.call || state.callOnDone ? "call" : "notification";
|
|
34353
|
+
const progress = result?.trim().slice(0, 1e4) || void 0;
|
|
34354
|
+
let revision = goal.revision;
|
|
33926
34355
|
try {
|
|
33927
|
-
await rail.
|
|
33928
|
-
|
|
33929
|
-
|
|
34356
|
+
const held = await rail.claim(goal.id);
|
|
34357
|
+
revision = held.revision ?? revision;
|
|
34358
|
+
const owed = state.noted?.some((b) => b.goals.includes(goal.id));
|
|
34359
|
+
if (!held.openDecisionNeedIds?.length && !owed) {
|
|
34360
|
+
await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
|
|
34361
|
+
state.goal = void 0;
|
|
34362
|
+
return;
|
|
34363
|
+
}
|
|
33930
34364
|
} catch (error) {
|
|
33931
|
-
opts?.log?.(`\u26A0
|
|
34365
|
+
opts?.log?.(`\u26A0 the Goal did not close, still holding it: ${error.message}`);
|
|
34366
|
+
}
|
|
34367
|
+
if (!progress) return;
|
|
34368
|
+
try {
|
|
34369
|
+
await rail.update(goal.id, { revision, changes: { progress }, reason: "The agent's turn ended; its Goal is not closed yet." });
|
|
34370
|
+
} catch (error) {
|
|
34371
|
+
opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
|
|
33932
34372
|
}
|
|
33933
34373
|
}
|
|
33934
|
-
async function
|
|
34374
|
+
async function cannotWork(report, state, rail, opts) {
|
|
34375
|
+
state.resting = true;
|
|
33935
34376
|
try {
|
|
33936
|
-
|
|
33937
|
-
|
|
33938
|
-
|
|
33939
|
-
|
|
33940
|
-
|
|
33941
|
-
|
|
33942
|
-
|
|
33943
|
-
${reason}`, parentId: goalId }],
|
|
33944
|
-
waiting: "none",
|
|
33945
|
-
channel: "notification"
|
|
33946
|
-
});
|
|
34377
|
+
if (!state.goal) {
|
|
34378
|
+
const claimed = await rail.claim();
|
|
34379
|
+
if (!claimed.goalId || claimed.revision === void 0) return;
|
|
34380
|
+
state.goal = { id: claimed.goalId, revision: claimed.revision };
|
|
34381
|
+
}
|
|
34382
|
+
await rail.contact({ asks: [{ ask: report, parentId: state.goal.id }], waiting: "none", channel: "notification" });
|
|
33947
34383
|
} catch (error) {
|
|
33948
|
-
opts?.log?.(`\u26A0 could not
|
|
34384
|
+
opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
|
|
33949
34385
|
}
|
|
33950
34386
|
}
|
|
33951
34387
|
async function afterTurn(event, state, rail, opts) {
|
|
33952
|
-
if (
|
|
33953
|
-
|
|
33954
|
-
|
|
33955
|
-
|
|
34388
|
+
if (event.failed) {
|
|
34389
|
+
const limit = limitIn(event.result, (opts?.now ?? Date.now)());
|
|
34390
|
+
if (limit) {
|
|
34391
|
+
state.paused = limit;
|
|
34392
|
+
if (state.typed) state.owed = state.typed;
|
|
34393
|
+
state.typed = void 0;
|
|
34394
|
+
state.carriedOn = false;
|
|
34395
|
+
state.resting = true;
|
|
34396
|
+
opts?.log?.(`\u23F8 paused by the usage limit until ${limit.resets}: nothing is typed in until then, and it carries on by itself`);
|
|
34397
|
+
return limit;
|
|
34398
|
+
}
|
|
34399
|
+
await cannotWork(`The agent stopped without finishing.
|
|
34400
|
+
|
|
34401
|
+
${event.result?.trim() || "No result reported."}`, state, rail, opts);
|
|
34402
|
+
return void 0;
|
|
33956
34403
|
}
|
|
33957
|
-
|
|
34404
|
+
await finish(event.result, state, rail, opts);
|
|
34405
|
+
state.typed = void 0;
|
|
34406
|
+
state.resting = true;
|
|
34407
|
+
return void 0;
|
|
34408
|
+
}
|
|
33958
34409
|
|
|
33959
|
-
|
|
33960
|
-
|
|
33961
|
-
|
|
33962
|
-
|
|
33963
|
-
|
|
34410
|
+
// src/paigy/conversation.ts
|
|
34411
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
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).";
|
|
34413
|
+
var SAFETY_MS = 6e4;
|
|
34414
|
+
var ASK_BEAT_MS = 5e3;
|
|
34415
|
+
function beat() {
|
|
34416
|
+
let pending = null;
|
|
34417
|
+
let wakeUp = null;
|
|
34418
|
+
return {
|
|
34419
|
+
nudge(cause) {
|
|
34420
|
+
if (wakeUp) {
|
|
34421
|
+
const w = wakeUp;
|
|
34422
|
+
wakeUp = null;
|
|
34423
|
+
w(cause);
|
|
34424
|
+
} else pending ??= cause;
|
|
34425
|
+
},
|
|
34426
|
+
next(ms) {
|
|
34427
|
+
if (pending) {
|
|
34428
|
+
const cause = pending;
|
|
34429
|
+
pending = null;
|
|
34430
|
+
return Promise.resolve(cause);
|
|
34431
|
+
}
|
|
34432
|
+
return new Promise((resolve3) => {
|
|
34433
|
+
const timer = setTimeout(() => {
|
|
34434
|
+
wakeUp = null;
|
|
34435
|
+
resolve3("safety");
|
|
34436
|
+
}, ms);
|
|
34437
|
+
wakeUp = (cause) => {
|
|
34438
|
+
clearTimeout(timer);
|
|
34439
|
+
resolve3(cause);
|
|
34440
|
+
};
|
|
34441
|
+
});
|
|
34442
|
+
}
|
|
34443
|
+
};
|
|
34444
|
+
}
|
|
34445
|
+
var BEAT_REPORT_MS = 10 * 6e4;
|
|
34446
|
+
var ended = (state) => state === "done" || state === "cancelled";
|
|
34447
|
+
function unheard(brief, state, since = null, after = -1) {
|
|
34448
|
+
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
34449
|
+
const words2 = [];
|
|
34450
|
+
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
34451
|
+
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
34452
|
+
if (i <= after) {
|
|
34453
|
+
seen.add(e.entryId);
|
|
34454
|
+
continue;
|
|
34455
|
+
}
|
|
34456
|
+
if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
|
|
34457
|
+
seen.add(e.entryId);
|
|
34458
|
+
if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
|
|
34459
|
+
const text = entryWords(e).trim();
|
|
34460
|
+
if (text) words2.push(text);
|
|
34461
|
+
}
|
|
34462
|
+
return words2;
|
|
34463
|
+
}
|
|
34464
|
+
function sinceTheWork(brief) {
|
|
34465
|
+
const entries = brief.entries ?? [];
|
|
34466
|
+
let after = -1;
|
|
34467
|
+
for (const [i, e] of entries.entries()) if (!e.authorParticipant.startsWith("human:")) after = i;
|
|
34468
|
+
const last = entries[after];
|
|
34469
|
+
const handed = last && brief.ownerParticipant && last.authorParticipant !== brief.ownerParticipant;
|
|
34470
|
+
const note2 = handed ? entryWords(last).trim() : "";
|
|
34471
|
+
return { after, ...note2 ? { note: note2 } : {} };
|
|
34472
|
+
}
|
|
34473
|
+
var baseline = (goal, subtree) => ({
|
|
34474
|
+
goal,
|
|
34475
|
+
goals: new Map((subtree ?? []).map((g) => [g.goalId, { since: g.updatedAt ?? null, at: g.updatedAt ?? null }]))
|
|
34476
|
+
});
|
|
34477
|
+
function movedBelow(held, state) {
|
|
34478
|
+
const goals = state.below.goals;
|
|
34479
|
+
return (held.subtree ?? []).flatMap((g) => {
|
|
34480
|
+
const known = goals.get(g.goalId);
|
|
34481
|
+
const stamp = g.updatedAt ?? null;
|
|
34482
|
+
if (!known) return [{ goal: g, since: null }];
|
|
34483
|
+
if (known.at === null) {
|
|
34484
|
+
known.since = known.at = stamp;
|
|
34485
|
+
return [];
|
|
34486
|
+
}
|
|
34487
|
+
return stamp !== null && stamp !== known.at ? [{ goal: g, since: known.since }] : [];
|
|
33964
34488
|
});
|
|
33965
|
-
|
|
33966
|
-
|
|
34489
|
+
}
|
|
34490
|
+
function named(goalId, title, brief) {
|
|
34491
|
+
const line = (title ?? brief.outcome ?? "").trim().split("\n")[0].trim();
|
|
34492
|
+
const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
|
|
34493
|
+
return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
|
|
34494
|
+
}
|
|
34495
|
+
async function gather(state, rail, held) {
|
|
34496
|
+
const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
|
|
34497
|
+
const look2 = (goalId, via, title, since) => {
|
|
34498
|
+
if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
|
|
34499
|
+
};
|
|
34500
|
+
const waiting = await rail.waiting().catch(() => null);
|
|
34501
|
+
const listed = /* @__PURE__ */ new Set();
|
|
34502
|
+
const announced = state.announced ??= /* @__PURE__ */ new Set();
|
|
34503
|
+
if (waiting) {
|
|
34504
|
+
for (const id of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id)) announced.delete(id);
|
|
34505
|
+
}
|
|
34506
|
+
for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
|
|
34507
|
+
for (const g of waiting?.review ?? []) {
|
|
34508
|
+
listed.add(g.goalId);
|
|
34509
|
+
look2(g.goalId, "waiting work", g.title);
|
|
34510
|
+
}
|
|
34511
|
+
for (const d of waiting?.deliveries ?? []) for (const id of d.goalIds) {
|
|
34512
|
+
listed.add(id);
|
|
34513
|
+
look2(id, "waiting work");
|
|
34514
|
+
}
|
|
34515
|
+
const moved = movedBelow(held, state);
|
|
34516
|
+
for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
|
|
34517
|
+
const heard = [];
|
|
34518
|
+
const read = /* @__PURE__ */ new Set();
|
|
34519
|
+
for (const [goalId, { since, ...found }] of looks) {
|
|
34520
|
+
let brief = found.brief;
|
|
34521
|
+
if (!brief) {
|
|
34522
|
+
try {
|
|
34523
|
+
brief = await rail.read(goalId);
|
|
34524
|
+
} catch (error) {
|
|
34525
|
+
if (error.status === 404) read.add(goalId);
|
|
34526
|
+
continue;
|
|
34527
|
+
}
|
|
34528
|
+
}
|
|
34529
|
+
read.add(goalId);
|
|
34530
|
+
const fresh = found.via === "waiting work" || found.via === "assigned" ? sinceTheWork(brief) : { after: -1 };
|
|
34531
|
+
if (found.via === "assigned") announced.add(goalId);
|
|
34532
|
+
heard.push({ goalId, ...found, brief, words: unheard(brief, state, since ?? null, fresh.after), ...fresh.note ? { note: fresh.note } : {} });
|
|
34533
|
+
}
|
|
34534
|
+
for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
|
|
34535
|
+
return { heard, listed };
|
|
34536
|
+
}
|
|
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.";
|
|
34538
|
+
var says = (h) => h.words.length > 0 || h.via === "assigned";
|
|
34539
|
+
function whatItIs(h) {
|
|
34540
|
+
const outcome2 = h.brief.outcome?.trim() ?? "";
|
|
34541
|
+
const name = (h.title ?? outcome2).trim().split("\n")[0].trim();
|
|
34542
|
+
return outcome2 && outcome2 !== name ? [outcome2] : [];
|
|
34543
|
+
}
|
|
34544
|
+
var block = (h) => [
|
|
34545
|
+
...h.via === "assigned" ? [GIVEN] : [],
|
|
34546
|
+
named(h.goalId, h.title, h.brief),
|
|
34547
|
+
...h.via === "assigned" ? whatItIs(h) : [],
|
|
34548
|
+
...h.note ? [`The last note on it, from the agent before you: ${h.note}`] : [],
|
|
34549
|
+
...h.words
|
|
34550
|
+
].join("\n\n");
|
|
34551
|
+
var short = (ids) => ids.map((id) => id.slice(0, 8)).join(", ");
|
|
34552
|
+
var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
|
|
34553
|
+
async function hear(session, state, rail, held, opts) {
|
|
34554
|
+
const log = opts?.log;
|
|
34555
|
+
const { heard, listed } = await gather(state, rail, held);
|
|
34556
|
+
const said = heard.filter(says);
|
|
34557
|
+
if (said.length) {
|
|
34558
|
+
send2(session, state, said.map(block).join("\n\n"));
|
|
34559
|
+
const worded = said.filter((h) => h.words.length);
|
|
34560
|
+
if (worded.length) log?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
|
|
34561
|
+
const goals = given(said);
|
|
34562
|
+
if (goals.length) {
|
|
34563
|
+
log?.(`\u25B8 announced assigned ${short(goals)} (via rest)`);
|
|
34564
|
+
if (opts?.tally) opts.tally.announced += goals.length;
|
|
34565
|
+
}
|
|
34566
|
+
}
|
|
34567
|
+
for (const h of heard) if (listed.has(h.goalId)) await acknowledge(h.goalId, h.brief, state, rail, log);
|
|
34568
|
+
}
|
|
34569
|
+
var NOTED = "New from the person while you work. Carry on unless it changes what you're doing: it comes back to you as your next turn when this one ends.";
|
|
34570
|
+
var SEEN_MID_TURN = "While you worked, the person said this, and you saw it mid-task. Act on it now, unless you already have.";
|
|
34571
|
+
async function note(state, rail, held, inbox, how) {
|
|
34572
|
+
const { heard } = await gather(state, rail, held);
|
|
34573
|
+
const said = heard.filter(says);
|
|
34574
|
+
if (!said.length) return 0;
|
|
34575
|
+
const words2 = said.map(block).join("\n\n");
|
|
34576
|
+
const goals = said.map((h) => h.goalId);
|
|
34577
|
+
const assigned = given(said);
|
|
34578
|
+
if (how.tally) how.tally.announced += assigned.length;
|
|
34579
|
+
let name = "";
|
|
34580
|
+
try {
|
|
34581
|
+
name = inbox.put(`${NOTED}
|
|
34582
|
+
|
|
34583
|
+
${words2}`);
|
|
34584
|
+
how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
|
|
34585
|
+
if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
|
|
34586
|
+
} catch (error) {
|
|
34587
|
+
how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
|
|
34588
|
+
}
|
|
34589
|
+
(state.noted ??= []).push({ name, goals, words: words2, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
|
|
34590
|
+
return 1;
|
|
34591
|
+
}
|
|
34592
|
+
function receipts(state, inbox, log) {
|
|
34593
|
+
let seen = 0;
|
|
34594
|
+
for (const b of state.noted ?? []) {
|
|
34595
|
+
if (b.seen !== void 0 || !b.name) continue;
|
|
34596
|
+
const at = inbox.taken(b.name);
|
|
34597
|
+
if (at === null) continue;
|
|
34598
|
+
b.seen = at;
|
|
34599
|
+
seen++;
|
|
34600
|
+
log?.(`\u2713 seen mid-turn: ${short(b.goals)} ${Math.max(0, Math.round((at - b.at) / 1e3))}s after the ${b.cause}`);
|
|
34601
|
+
}
|
|
34602
|
+
return seen;
|
|
34603
|
+
}
|
|
34604
|
+
function settle(session, state, inbox, log) {
|
|
34605
|
+
const batches = state.noted ?? [];
|
|
34606
|
+
state.noted = [];
|
|
34607
|
+
const seen = [];
|
|
34608
|
+
const unseen = [];
|
|
34609
|
+
for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
|
|
34610
|
+
send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
|
|
34611
|
+
log?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
|
|
34612
|
+
}
|
|
34613
|
+
async function acknowledge(goalId, brief, state, rail, log) {
|
|
34614
|
+
const acked = state.acked ??= /* @__PURE__ */ new Map();
|
|
34615
|
+
if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
|
|
34616
|
+
acked.set(goalId, brief.revision);
|
|
34617
|
+
try {
|
|
34618
|
+
const claimed = await rail.claim(goalId);
|
|
34619
|
+
const done = await rail.update(goalId, { revision: claimed.revision ?? brief.revision ?? 1, changes: { reviewed: true }, reason: "Read by the harness session; the person's words on it are typed in." });
|
|
34620
|
+
acked.set(goalId, done.revision);
|
|
34621
|
+
if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
|
|
34622
|
+
} catch (error) {
|
|
34623
|
+
log?.(`\u26A0 could not acknowledge ${goalId.slice(0, 8)}: ${error.message} \u2014 its words are typed in; it stays listed until acknowledged`);
|
|
34624
|
+
}
|
|
34625
|
+
}
|
|
34626
|
+
async function step(session, state, rail, opts) {
|
|
34627
|
+
if (state.paused) {
|
|
34628
|
+
if ((opts?.now ?? Date.now)() < state.paused.until) {
|
|
34629
|
+
if (opts?.tally) opts.tally.paused = (opts.tally.paused ?? 0) + 1;
|
|
34630
|
+
return;
|
|
34631
|
+
}
|
|
34632
|
+
opts?.log?.(`\u25B6 resumed after the limit (it reset at ${state.paused.resets}): told to carry on, with what arrived meanwhile`);
|
|
34633
|
+
state.paused = void 0;
|
|
34634
|
+
state.carryOn = true;
|
|
34635
|
+
}
|
|
34636
|
+
await look(session, state, rail, opts);
|
|
34637
|
+
if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
|
|
34638
|
+
}
|
|
34639
|
+
async function look(session, state, rail, opts) {
|
|
34640
|
+
if (opts?.inbox) {
|
|
34641
|
+
const seen = receipts(state, opts.inbox, opts.log);
|
|
34642
|
+
if (opts.tally) opts.tally.seen += seen;
|
|
34643
|
+
}
|
|
34644
|
+
if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
|
|
34645
|
+
if (!state.goal) {
|
|
34646
|
+
if (!state.resting) return;
|
|
34647
|
+
let brief2 = await rail.claim();
|
|
34648
|
+
if (!brief2.goalId || brief2.revision === void 0) {
|
|
34649
|
+
if (opts?.initialPrompt?.trim()) {
|
|
34650
|
+
const prompt = opts.initialPrompt.trim();
|
|
34651
|
+
opts.initialPrompt = void 0;
|
|
34652
|
+
const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID5() });
|
|
34653
|
+
brief2 = await rail.claim();
|
|
34654
|
+
if (!brief2.goalId || brief2.revision === void 0) {
|
|
34655
|
+
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
34656
|
+
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
34657
|
+
return;
|
|
34658
|
+
}
|
|
34659
|
+
} else {
|
|
34660
|
+
return;
|
|
34661
|
+
}
|
|
34662
|
+
}
|
|
34663
|
+
unheard(brief2, state);
|
|
34664
|
+
if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
|
|
34665
|
+
else {
|
|
34666
|
+
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
34667
|
+
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
34668
|
+
}
|
|
34669
|
+
const outcome2 = brief2.outcome?.trim() ?? "";
|
|
34670
|
+
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
34671
|
+
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
34672
|
+
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
|
|
34673
|
+
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
34674
|
+
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
34675
|
+
return;
|
|
34676
|
+
}
|
|
34677
|
+
const brief = await rail.read(state.goal.id);
|
|
34678
|
+
state.goal.revision = brief.revision ?? state.goal.revision;
|
|
34679
|
+
for (const answer of brief.answers ?? []) {
|
|
34680
|
+
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
34681
|
+
if (!resolve3) continue;
|
|
34682
|
+
state.asks.delete(answer.requestEntryId);
|
|
34683
|
+
resolve3(answer.result);
|
|
34684
|
+
}
|
|
34685
|
+
if (ended(brief.state)) {
|
|
34686
|
+
state.goal = void 0;
|
|
34687
|
+
for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
|
|
34688
|
+
state.asks?.clear();
|
|
34689
|
+
return;
|
|
34690
|
+
}
|
|
34691
|
+
if (state.below?.goal !== state.goal.id) state.below = baseline(state.goal.id, brief.subtree);
|
|
34692
|
+
if (state.asks?.size) return;
|
|
34693
|
+
if (!state.resting) {
|
|
34694
|
+
if (!opts?.inbox) return;
|
|
34695
|
+
const noted = await note(state, rail, brief, opts.inbox, { cause: opts.cause ?? "safety", now: opts.now ?? Date.now, ...opts.log ? { log: opts.log } : {}, ...opts.tally ? { tally: opts.tally } : {} });
|
|
34696
|
+
if (opts.tally) opts.tally.noted += noted;
|
|
34697
|
+
return;
|
|
34698
|
+
}
|
|
34699
|
+
await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
|
|
34700
|
+
}
|
|
34701
|
+
function send2(session, state, text) {
|
|
34702
|
+
state.resting = false;
|
|
34703
|
+
let words2 = text;
|
|
34704
|
+
if (state.carryOn) {
|
|
34705
|
+
state.carryOn = false;
|
|
34706
|
+
state.carriedOn = true;
|
|
34707
|
+
const owed = state.owed;
|
|
34708
|
+
state.owed = void 0;
|
|
34709
|
+
words2 = [owed, text].filter(Boolean).join("\n\n");
|
|
34710
|
+
text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
|
|
34711
|
+
}
|
|
34712
|
+
state.typed = words2 || void 0;
|
|
34713
|
+
session.send(text);
|
|
34714
|
+
}
|
|
34715
|
+
async function pump(session, state, rail, opts) {
|
|
34716
|
+
const clock4 = opts.beat ?? beat();
|
|
34717
|
+
const now = opts.now ?? Date.now;
|
|
34718
|
+
const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
|
|
34719
|
+
const tally = { noted: 0, seen: 0, announced: 0 };
|
|
34720
|
+
const stepOpts = {
|
|
34721
|
+
...opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : {},
|
|
34722
|
+
...opts.log ? { log: opts.log } : {},
|
|
34723
|
+
...opts.inbox ? { inbox: opts.inbox } : {},
|
|
34724
|
+
now,
|
|
34725
|
+
tally
|
|
34726
|
+
};
|
|
34727
|
+
let since = now();
|
|
34728
|
+
let cause = "start";
|
|
34729
|
+
for (; ; ) {
|
|
34730
|
+
if (cause === "wake" && !state.resting && !state.asks?.size && !opts.inbox) counts.skipped++;
|
|
34731
|
+
else {
|
|
34732
|
+
if (cause !== "start") counts[cause]++;
|
|
34733
|
+
try {
|
|
34734
|
+
stepOpts.cause = cause;
|
|
34735
|
+
await step(session, state, rail, stepOpts);
|
|
34736
|
+
} catch {
|
|
34737
|
+
}
|
|
34738
|
+
}
|
|
34739
|
+
if (!await opts.onIdle()) return;
|
|
34740
|
+
if (now() - since >= BEAT_REPORT_MS) {
|
|
34741
|
+
const read = counts.wake + counts.rest + counts.safety;
|
|
34742
|
+
if (read || counts.skipped || tally.noted || tally.seen || tally.announced) {
|
|
34743
|
+
opts.log?.(`\u25B9 beats: ${read} read (${counts.wake} wake, ${counts.rest} rest, ${counts.safety} safety), ${counts.skipped} wake mid-turn skipped, ${tally.noted} noted mid-turn, ${tally.seen} seen by the hook, ${tally.announced} assigned announced${tally.paused ? `, ${tally.paused} held by the usage limit` : ""}`);
|
|
34744
|
+
}
|
|
34745
|
+
counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
|
|
34746
|
+
since = now();
|
|
34747
|
+
}
|
|
34748
|
+
cause = await clock4.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
|
|
34749
|
+
if (!await opts.onIdle()) return;
|
|
34750
|
+
}
|
|
33967
34751
|
}
|
|
33968
34752
|
|
|
33969
34753
|
// src/run.ts
|
|
@@ -33971,15 +34755,32 @@ function runHarness(opts) {
|
|
|
33971
34755
|
let running = true;
|
|
33972
34756
|
let tail = [];
|
|
33973
34757
|
const state = { resting: true };
|
|
34758
|
+
const clock4 = beat();
|
|
33974
34759
|
let session = null;
|
|
33975
34760
|
const speaksAs = opts.resume ?? opts.session;
|
|
33976
34761
|
const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
|
|
34762
|
+
let inbox;
|
|
34763
|
+
if (opts.harness === "claude") {
|
|
34764
|
+
try {
|
|
34765
|
+
inbox = inboxFor();
|
|
34766
|
+
} catch (error) {
|
|
34767
|
+
opts.log(`\u26A0 no inbox (${error.message}): what the person says mid-turn waits for the turn to end`);
|
|
34768
|
+
}
|
|
34769
|
+
}
|
|
34770
|
+
const name = opts.name ?? "The agent";
|
|
34771
|
+
function runningAgain() {
|
|
34772
|
+
state.carriedOn = false;
|
|
34773
|
+
opts.log("\u25B6 running again after the limit");
|
|
34774
|
+
LIMITS.running(name, (text) => rail.contact({ asks: [{ ask: text, ...state.goal ? { parentId: state.goal.id } : {} }], waiting: "none", channel: "call" }), opts.log);
|
|
34775
|
+
}
|
|
33977
34776
|
async function handle(event) {
|
|
33978
34777
|
if (event.kind === "error") {
|
|
33979
34778
|
opts.log(`\u26A0 ${event.message}`);
|
|
33980
34779
|
if (event.fatal && running) {
|
|
33981
34780
|
opts.log(`\u2716 could not start: ${event.message}`);
|
|
33982
|
-
await
|
|
34781
|
+
await cannotWork(`The session could not start, so nothing is working on this yet.
|
|
34782
|
+
|
|
34783
|
+
${event.message}`, state, rail, { log: opts.log });
|
|
33983
34784
|
stop2();
|
|
33984
34785
|
}
|
|
33985
34786
|
return;
|
|
@@ -33998,6 +34799,7 @@ function runHarness(opts) {
|
|
|
33998
34799
|
return;
|
|
33999
34800
|
}
|
|
34000
34801
|
if (event.kind === "work") {
|
|
34802
|
+
if (state.carriedOn) runningAgain();
|
|
34001
34803
|
opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
|
|
34002
34804
|
tail = pushWork(tail, workLine(event));
|
|
34003
34805
|
return;
|
|
@@ -34005,20 +34807,10 @@ function runHarness(opts) {
|
|
|
34005
34807
|
if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
|
|
34006
34808
|
if (event.kind === "idle") {
|
|
34007
34809
|
tail = [];
|
|
34008
|
-
|
|
34009
|
-
|
|
34010
|
-
|
|
34011
|
-
|
|
34012
|
-
state.resting = false;
|
|
34013
|
-
session.send(local);
|
|
34014
|
-
return;
|
|
34015
|
-
}
|
|
34016
|
-
}
|
|
34017
|
-
const reply = await afterTurn(event, state, rail, { log: opts.log });
|
|
34018
|
-
if (reply && session && running) {
|
|
34019
|
-
opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
|
|
34020
|
-
session.send(reply);
|
|
34021
|
-
}
|
|
34810
|
+
const carried = state.carriedOn && !event.failed;
|
|
34811
|
+
if (await afterTurn(event, state, rail, { log: opts.log })) LIMITS.paused(name, Date.now());
|
|
34812
|
+
else if (carried) runningAgain();
|
|
34813
|
+
clock4.nudge("rest");
|
|
34022
34814
|
}
|
|
34023
34815
|
}
|
|
34024
34816
|
session = startSession({
|
|
@@ -34029,19 +34821,25 @@ function runHarness(opts) {
|
|
|
34029
34821
|
...opts.resume ? { resume: opts.resume } : {},
|
|
34030
34822
|
...opts.session ? { session: opts.session } : {},
|
|
34031
34823
|
...opts.bin ? { bin: opts.bin } : {},
|
|
34032
|
-
...
|
|
34824
|
+
...inbox ? { inbox: inbox.dir } : {},
|
|
34825
|
+
onExit: () => {
|
|
34826
|
+
inbox?.close();
|
|
34827
|
+
opts.onExit?.();
|
|
34828
|
+
},
|
|
34033
34829
|
onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
|
|
34034
34830
|
});
|
|
34035
|
-
void pump(session, state, rail, { onIdle: () => running, initialPrompt: opts.prompt }).catch(
|
|
34831
|
+
void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
|
|
34036
34832
|
(e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
|
|
34037
34833
|
);
|
|
34038
34834
|
opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
|
|
34039
34835
|
function stop2() {
|
|
34040
34836
|
running = false;
|
|
34837
|
+
clock4.nudge("wake");
|
|
34041
34838
|
tail = [];
|
|
34042
34839
|
cancelAsks(state);
|
|
34043
34840
|
session?.stop();
|
|
34044
34841
|
session = null;
|
|
34842
|
+
inbox?.close();
|
|
34045
34843
|
opts.log("\u25A0 stopped");
|
|
34046
34844
|
}
|
|
34047
34845
|
return {
|
|
@@ -34049,24 +34847,42 @@ function runHarness(opts) {
|
|
|
34049
34847
|
session?.send(text);
|
|
34050
34848
|
},
|
|
34051
34849
|
working: () => running && state.resting !== true,
|
|
34052
|
-
|
|
34850
|
+
// Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
|
|
34851
|
+
tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
|
|
34852
|
+
nudge: () => clock4.nudge("wake"),
|
|
34053
34853
|
stop: stop2
|
|
34054
34854
|
};
|
|
34055
34855
|
}
|
|
34056
34856
|
|
|
34057
34857
|
// src/host.ts
|
|
34058
34858
|
init_dist();
|
|
34859
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
34860
|
+
import { homedir as homedir9 } from "os";
|
|
34861
|
+
import { join as join9 } from "path";
|
|
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
|
|
34059
34878
|
init_catalog();
|
|
34060
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
34061
|
-
import { homedir as homedir6 } from "os";
|
|
34062
|
-
import { join as join7 } from "path";
|
|
34063
34879
|
|
|
34064
34880
|
// src/workspaces.ts
|
|
34065
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
34066
|
-
import { dirname as dirname2, join as
|
|
34067
|
-
import { homedir as
|
|
34881
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
34882
|
+
import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
|
|
34883
|
+
import { homedir as homedir6 } from "os";
|
|
34068
34884
|
function workspacesFile() {
|
|
34069
|
-
return
|
|
34885
|
+
return join5(homedir6(), ".paigy", "workspaces.json");
|
|
34070
34886
|
}
|
|
34071
34887
|
function listWorkspaces(deps) {
|
|
34072
34888
|
try {
|
|
@@ -34077,31 +34893,94 @@ function listWorkspaces(deps) {
|
|
|
34077
34893
|
}
|
|
34078
34894
|
}
|
|
34079
34895
|
function addWorkspace(dir, deps) {
|
|
34080
|
-
const next = resolve2(dir.replace(/^~(?=$|\/)/,
|
|
34896
|
+
const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir6()));
|
|
34081
34897
|
const all = listWorkspaces(deps);
|
|
34082
34898
|
if (!all.includes(next)) {
|
|
34083
34899
|
all.push(next);
|
|
34084
|
-
|
|
34085
|
-
|
|
34900
|
+
mkdirSync3(dirname2(deps.file), { recursive: true });
|
|
34901
|
+
writeFileSync3(deps.file, JSON.stringify(all, null, 2));
|
|
34086
34902
|
}
|
|
34087
34903
|
return all;
|
|
34088
34904
|
}
|
|
34089
34905
|
function allowed(cwd, deps) {
|
|
34090
|
-
const target = resolve2(cwd.replace(/^~(?=$|\/)/,
|
|
34906
|
+
const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir6()));
|
|
34091
34907
|
return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
|
|
34092
34908
|
}
|
|
34093
34909
|
function resolveWakeDir(pinned, deps) {
|
|
34094
34910
|
if (pinned && allowed(pinned, deps)) {
|
|
34095
|
-
const dir = resolve2(pinned.replace(/^~(?=$|\/)/,
|
|
34911
|
+
const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir6()));
|
|
34096
34912
|
if ((deps.exists ?? existsSync4)(dir)) return dir;
|
|
34097
34913
|
}
|
|
34098
34914
|
return listWorkspaces(deps)[0];
|
|
34099
34915
|
}
|
|
34100
34916
|
|
|
34917
|
+
// src/holder.ts
|
|
34918
|
+
init_dist();
|
|
34919
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
34920
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
34921
|
+
import { homedir as homedir7 } from "os";
|
|
34922
|
+
import { join as join6 } from "path";
|
|
34923
|
+
function isAlive(pid) {
|
|
34924
|
+
try {
|
|
34925
|
+
process.kill(pid, 0);
|
|
34926
|
+
return true;
|
|
34927
|
+
} catch (e) {
|
|
34928
|
+
return e.code === "EPERM";
|
|
34929
|
+
}
|
|
34930
|
+
}
|
|
34931
|
+
function psStart(pid, zone) {
|
|
34932
|
+
try {
|
|
34933
|
+
const env = zone === "UTC" ? { ...process.env, TZ: "UTC", LC_ALL: "C" } : { ...process.env, LC_ALL: "C" };
|
|
34934
|
+
return execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], { env, encoding: "utf8", timeout: 2e3 }).trim() || null;
|
|
34935
|
+
} catch {
|
|
34936
|
+
return null;
|
|
34937
|
+
}
|
|
34938
|
+
}
|
|
34939
|
+
var squash = (s) => s.trim().replace(/\s+/g, " ");
|
|
34940
|
+
function holds(pid, procStart, deps) {
|
|
34941
|
+
if (!deps.alive(pid)) return false;
|
|
34942
|
+
if (typeof procStart !== "string" || !procStart.trim()) return true;
|
|
34943
|
+
const want = squash(procStart);
|
|
34944
|
+
for (const zone of ["UTC", "local"]) {
|
|
34945
|
+
const got = deps.started(pid, zone);
|
|
34946
|
+
if (got === null) return true;
|
|
34947
|
+
if (squash(got) === want) return true;
|
|
34948
|
+
}
|
|
34949
|
+
return false;
|
|
34950
|
+
}
|
|
34951
|
+
function localHolders(deps = {}) {
|
|
34952
|
+
const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
|
|
34953
|
+
const list = deps.list ?? ((d) => readdirSync2(d));
|
|
34954
|
+
const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
|
|
34955
|
+
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
34956
|
+
const held = /* @__PURE__ */ new Map();
|
|
34957
|
+
let files;
|
|
34958
|
+
try {
|
|
34959
|
+
files = list(dir);
|
|
34960
|
+
} catch {
|
|
34961
|
+
return held;
|
|
34962
|
+
}
|
|
34963
|
+
for (const file of files) {
|
|
34964
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
34965
|
+
let row;
|
|
34966
|
+
try {
|
|
34967
|
+
row = JSON.parse(read(join6(dir, file)));
|
|
34968
|
+
} catch {
|
|
34969
|
+
continue;
|
|
34970
|
+
}
|
|
34971
|
+
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
34972
|
+
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
34973
|
+
const slot = sessionSlot(row.sessionId);
|
|
34974
|
+
if (held.has(slot)) continue;
|
|
34975
|
+
if (holds(pid, row.procStart, check)) held.set(slot, { pid });
|
|
34976
|
+
}
|
|
34977
|
+
return held;
|
|
34978
|
+
}
|
|
34979
|
+
|
|
34101
34980
|
// src/owner.ts
|
|
34102
|
-
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
|
|
34103
|
-
import { homedir as
|
|
34104
|
-
import { join as
|
|
34981
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
|
|
34982
|
+
import { homedir as homedir8 } from "os";
|
|
34983
|
+
import { join as join7 } from "path";
|
|
34105
34984
|
var HEAD_BYTES = 64e3;
|
|
34106
34985
|
function readHead(file) {
|
|
34107
34986
|
const fd = openSync2(file, "r");
|
|
@@ -34117,8 +34996,8 @@ function ownerSession(slot, deps = {}) {
|
|
|
34117
34996
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
34118
34997
|
if (!m) return null;
|
|
34119
34998
|
const prefix = m[1];
|
|
34120
|
-
const root = deps.projectsDir ??
|
|
34121
|
-
const list = deps.list ?? ((dir) =>
|
|
34999
|
+
const root = deps.projectsDir ?? join7(homedir8(), ".claude", "projects");
|
|
35000
|
+
const list = deps.list ?? ((dir) => readdirSync3(dir));
|
|
34122
35001
|
const head = deps.head ?? readHead;
|
|
34123
35002
|
let folders;
|
|
34124
35003
|
try {
|
|
@@ -34129,7 +35008,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34129
35008
|
for (const folder of folders) {
|
|
34130
35009
|
let files;
|
|
34131
35010
|
try {
|
|
34132
|
-
files = list(
|
|
35011
|
+
files = list(join7(root, folder));
|
|
34133
35012
|
} catch {
|
|
34134
35013
|
continue;
|
|
34135
35014
|
}
|
|
@@ -34137,7 +35016,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34137
35016
|
if (!file) continue;
|
|
34138
35017
|
let text;
|
|
34139
35018
|
try {
|
|
34140
|
-
text = head(
|
|
35019
|
+
text = head(join7(root, folder, file));
|
|
34141
35020
|
} catch {
|
|
34142
35021
|
continue;
|
|
34143
35022
|
}
|
|
@@ -34157,7 +35036,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
34157
35036
|
// src/update.ts
|
|
34158
35037
|
import { execFile } from "child_process";
|
|
34159
35038
|
import { createRequire } from "module";
|
|
34160
|
-
import { dirname as dirname3, join as
|
|
35039
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
34161
35040
|
import { promisify } from "util";
|
|
34162
35041
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
34163
35042
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -34184,7 +35063,7 @@ async function latestVersion() {
|
|
|
34184
35063
|
}
|
|
34185
35064
|
async function install(version) {
|
|
34186
35065
|
const bin = dirname3(process.execPath);
|
|
34187
|
-
await promisify(execFile)(
|
|
35066
|
+
await promisify(execFile)(join8(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
34188
35067
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
34189
35068
|
timeout: 5 * 6e4
|
|
34190
35069
|
});
|
|
@@ -34235,28 +35114,99 @@ function selfUpdater(deps) {
|
|
|
34235
35114
|
}
|
|
34236
35115
|
|
|
34237
35116
|
// src/host.ts
|
|
34238
|
-
var HOST_FILE =
|
|
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
|
+
}
|
|
34239
35127
|
var runKey = (slot) => `slot:${slot}`;
|
|
34240
35128
|
var claimedRun = (sessionId2) => {
|
|
34241
35129
|
const slot = sessionSlot(sessionId2);
|
|
34242
35130
|
return { slot, key: runKey(slot) };
|
|
34243
35131
|
};
|
|
34244
35132
|
var launchRun = (spec) => ({ session: spec.sessionId });
|
|
34245
|
-
|
|
34246
|
-
|
|
35133
|
+
var NOTHING_WAITING = "has nothing waiting";
|
|
35134
|
+
async function waitingWork(token, session) {
|
|
35135
|
+
let found;
|
|
35136
|
+
try {
|
|
35137
|
+
found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
|
|
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
|
+
}
|
|
35143
|
+
return { skip: `could not read what is waiting (${error.message})` };
|
|
35144
|
+
}
|
|
35145
|
+
const next = found.claimable;
|
|
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 };
|
|
35150
|
+
}
|
|
35151
|
+
function evict(runs, key, run) {
|
|
35152
|
+
if (runs.get(key)?.run === run) runs.delete(key);
|
|
34247
35153
|
}
|
|
34248
35154
|
var PRESENCE_FRESH_MS = 3 * 6e4;
|
|
34249
|
-
|
|
35155
|
+
var IDLE_HOLDER_MS = 30 * 6e4;
|
|
35156
|
+
function wakeAction(live, now, serverSeenAt, here = {}) {
|
|
34250
35157
|
if (live) return { action: "skip", why: "run" };
|
|
34251
|
-
if (
|
|
35158
|
+
if (here.holder) return { action: "skip", why: "holder" };
|
|
35159
|
+
if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
|
|
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" };
|
|
35162
|
+
return { action: "skip", why: "presence" };
|
|
35163
|
+
}
|
|
34252
35164
|
return { action: "spawn", why: "nobody" };
|
|
34253
35165
|
}
|
|
34254
35166
|
function startHost(opts) {
|
|
34255
35167
|
const runs = /* @__PURE__ */ new Map();
|
|
34256
35168
|
const asHost = { token: opts.token };
|
|
35169
|
+
const goneAt = /* @__PURE__ */ new Map();
|
|
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
|
+
}
|
|
34257
35207
|
const refreshIdentities = async () => {
|
|
34258
35208
|
for (const slot of listSlots()) {
|
|
34259
|
-
if (slot === "Desktop") continue;
|
|
35209
|
+
if (slot === "Desktop" || isRevoked(slot)) continue;
|
|
34260
35210
|
const token = readToken(slot);
|
|
34261
35211
|
if (!token) continue;
|
|
34262
35212
|
const me = await whoAmI({ token }).catch(() => null);
|
|
@@ -34273,7 +35223,7 @@ function startHost(opts) {
|
|
|
34273
35223
|
working: () => [...runs.values()].filter((r) => r.run.working()).length,
|
|
34274
35224
|
log: opts.log
|
|
34275
35225
|
});
|
|
34276
|
-
const
|
|
35226
|
+
const beat2 = () => {
|
|
34277
35227
|
void refreshIdentities();
|
|
34278
35228
|
void update().catch(() => {
|
|
34279
35229
|
});
|
|
@@ -34294,6 +35244,19 @@ function startHost(opts) {
|
|
|
34294
35244
|
const label = spec.name ?? spec.harness;
|
|
34295
35245
|
const { slot, key } = claimedRun(spec.sessionId);
|
|
34296
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
|
+
}
|
|
34297
35260
|
const run = runHarness({
|
|
34298
35261
|
harness: spec.harness,
|
|
34299
35262
|
cwd: spec.workspace,
|
|
@@ -34302,23 +35265,20 @@ function startHost(opts) {
|
|
|
34302
35265
|
token: spec.token,
|
|
34303
35266
|
...launchRun(spec),
|
|
34304
35267
|
onExit: () => {
|
|
34305
|
-
runs
|
|
35268
|
+
evict(runs, key, run);
|
|
35269
|
+
goneAt.set(slot, Date.now());
|
|
34306
35270
|
opts.log(`\u2716 ${label} ended`);
|
|
34307
35271
|
},
|
|
35272
|
+
name: label,
|
|
34308
35273
|
log
|
|
34309
35274
|
});
|
|
34310
35275
|
runs.set(key, { run, label, token: spec.token });
|
|
34311
35276
|
void heartbeat(void 0, { token: spec.token }).catch(() => {
|
|
34312
35277
|
});
|
|
34313
35278
|
void refreshIdentities();
|
|
34314
|
-
try {
|
|
34315
|
-
saveToken({ access_token: spec.token, name: label, device: null, workspace: spec.workspace }, slot);
|
|
34316
|
-
} catch {
|
|
34317
|
-
}
|
|
34318
35279
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
34319
35280
|
}
|
|
34320
35281
|
}
|
|
34321
|
-
const SLOT_HARNESS = { "mcp-agent": "claude", codex: "codex", antigravity: "agy" };
|
|
34322
35282
|
async function presenceMap() {
|
|
34323
35283
|
const presence = /* @__PURE__ */ new Map();
|
|
34324
35284
|
try {
|
|
@@ -34331,27 +35291,87 @@ function startHost(opts) {
|
|
|
34331
35291
|
return presence;
|
|
34332
35292
|
}
|
|
34333
35293
|
let lastPresence = /* @__PURE__ */ new Map();
|
|
34334
|
-
async function
|
|
35294
|
+
async function evidence() {
|
|
35295
|
+
const presence = await presenceMap();
|
|
35296
|
+
lastHolders = localHolders();
|
|
35297
|
+
return { presence, holders: lastHolders };
|
|
35298
|
+
}
|
|
35299
|
+
let lastHolders = /* @__PURE__ */ new Map();
|
|
35300
|
+
async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
|
|
34335
35301
|
{
|
|
34336
|
-
const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
|
|
34337
35302
|
const key = runKey(slot);
|
|
34338
|
-
if (
|
|
35303
|
+
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
34339
35304
|
const live = runs.get(key);
|
|
34340
35305
|
const tokenId = slotIdentity(slot).tokenId;
|
|
34341
35306
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
34342
|
-
const
|
|
34343
|
-
if (
|
|
34344
|
-
|
|
35307
|
+
const holder = holders.get(slot);
|
|
35308
|
+
if (holder) heldHere.add(slot);
|
|
35309
|
+
else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
|
|
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") {
|
|
35314
|
+
if (decided.why === "run" && cause === "wake") live?.run.nudge();
|
|
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`);
|
|
34345
35316
|
return;
|
|
34346
35317
|
}
|
|
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
|
+
};
|
|
34347
35324
|
const token = readToken(slot);
|
|
34348
|
-
if (!token)
|
|
34349
|
-
|
|
34350
|
-
|
|
35325
|
+
if (!token) {
|
|
35326
|
+
skip("has no token on this machine");
|
|
35327
|
+
return;
|
|
35328
|
+
}
|
|
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
|
+
}
|
|
35360
|
+
if (owner && !allowed(owner.cwd, opts.wsDeps)) {
|
|
35361
|
+
skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
|
|
35362
|
+
return;
|
|
35363
|
+
}
|
|
34351
35364
|
const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
|
|
34352
|
-
if (!workspace)
|
|
34353
|
-
|
|
34354
|
-
|
|
35365
|
+
if (!workspace) {
|
|
35366
|
+
skip("has no granted folder to wake in");
|
|
35367
|
+
return;
|
|
35368
|
+
}
|
|
35369
|
+
found ??= await read() ?? void 0;
|
|
35370
|
+
if (!found) return;
|
|
35371
|
+
if (runs.has(key)) {
|
|
35372
|
+
skip("was started by the wake that read it first");
|
|
35373
|
+
return;
|
|
35374
|
+
}
|
|
34355
35375
|
const log = (line) => opts.log(`[${label}] ${line}`);
|
|
34356
35376
|
const run = runHarness({
|
|
34357
35377
|
harness,
|
|
@@ -34359,24 +35379,30 @@ function startHost(opts) {
|
|
|
34359
35379
|
mode: "bypass",
|
|
34360
35380
|
prompt: "",
|
|
34361
35381
|
token,
|
|
35382
|
+
...identity.sessionId ? { session: identity.sessionId } : {},
|
|
34362
35383
|
...owner ? { resume: owner.sessionId } : {},
|
|
34363
|
-
// A dead run must not squat the slot — evict so the next wake can respawn.
|
|
35384
|
+
// A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
|
|
35385
|
+
// a run that is no longer the slot's must not take the live one's place away with it.
|
|
34364
35386
|
onExit: () => {
|
|
34365
|
-
runs
|
|
35387
|
+
evict(runs, key, run);
|
|
35388
|
+
goneAt.set(slot, Date.now());
|
|
34366
35389
|
opts.log(`\u2716 ${label} ended`);
|
|
34367
35390
|
},
|
|
35391
|
+
name: label,
|
|
34368
35392
|
log
|
|
34369
35393
|
});
|
|
34370
35394
|
runs.set(key, { run, label, token });
|
|
34371
35395
|
void heartbeat(void 0, { token }).catch(() => {
|
|
34372
35396
|
});
|
|
34373
|
-
|
|
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);
|
|
34374
35400
|
}
|
|
34375
35401
|
}
|
|
34376
35402
|
const listening = /* @__PURE__ */ new Map();
|
|
34377
35403
|
const joining = /* @__PURE__ */ new Set();
|
|
34378
35404
|
async function listenSlots() {
|
|
34379
|
-
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)));
|
|
34380
35406
|
for (const [slot, sub] of [...listening]) {
|
|
34381
35407
|
if (want.has(slot)) continue;
|
|
34382
35408
|
listening.delete(slot);
|
|
@@ -34390,13 +35416,17 @@ function startHost(opts) {
|
|
|
34390
35416
|
joining.add(slot);
|
|
34391
35417
|
try {
|
|
34392
35418
|
const sub = await subscribeWake(
|
|
34393
|
-
() => void
|
|
35419
|
+
() => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
|
|
34394
35420
|
}),
|
|
34395
35421
|
{ token }
|
|
34396
35422
|
);
|
|
34397
35423
|
listening.set(slot, sub);
|
|
34398
35424
|
opts.log(`\u25C9 listening for ${slotName(slot) ?? slot}`);
|
|
34399
35425
|
} catch (e) {
|
|
35426
|
+
if (e instanceof RevokedError) {
|
|
35427
|
+
drop(slot, token);
|
|
35428
|
+
continue;
|
|
35429
|
+
}
|
|
34400
35430
|
opts.log(`\u25CC not listening for ${slotName(slot) ?? slot} \u2014 ${e.message}`);
|
|
34401
35431
|
} finally {
|
|
34402
35432
|
joining.delete(slot);
|
|
@@ -34404,8 +35434,8 @@ function startHost(opts) {
|
|
|
34404
35434
|
}
|
|
34405
35435
|
}
|
|
34406
35436
|
async function sweepSlots() {
|
|
34407
|
-
const
|
|
34408
|
-
for (const slot of listSlots()) await wakeSlot(slot,
|
|
35437
|
+
const seen = await evidence();
|
|
35438
|
+
for (const slot of listSlots()) await wakeSlot(slot, seen);
|
|
34409
35439
|
}
|
|
34410
35440
|
const ACTIVITY_MS = 2e3;
|
|
34411
35441
|
const published = /* @__PURE__ */ new Map();
|
|
@@ -34430,12 +35460,12 @@ function startHost(opts) {
|
|
|
34430
35460
|
};
|
|
34431
35461
|
const publish = () => {
|
|
34432
35462
|
try {
|
|
34433
|
-
|
|
35463
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
|
|
34434
35464
|
} catch {
|
|
34435
35465
|
}
|
|
34436
35466
|
};
|
|
34437
|
-
|
|
34438
|
-
const pulse = setInterval(
|
|
35467
|
+
beat2();
|
|
35468
|
+
const pulse = setInterval(beat2, 6e4);
|
|
34439
35469
|
const spawnPoll = setInterval(() => {
|
|
34440
35470
|
void claimAndSpawn();
|
|
34441
35471
|
publish();
|
|
@@ -34461,10 +35491,10 @@ function startHost(opts) {
|
|
|
34461
35491
|
// them live, and a run mid-turn makes it working.
|
|
34462
35492
|
roster: () => {
|
|
34463
35493
|
const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
|
|
34464
|
-
const
|
|
34465
|
-
for (const key of listSlots()) if (key !== "Desktop")
|
|
34466
|
-
for (const label of live.keys()) if (!
|
|
34467
|
-
return [...
|
|
35494
|
+
const names2 = /* @__PURE__ */ new Map();
|
|
35495
|
+
for (const key of listSlots()) if (key !== "Desktop" && !isRevoked(key)) names2.set(slotName(key) ?? key, key);
|
|
35496
|
+
for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
|
|
35497
|
+
return [...names2.entries()].map(([name, slot]) => {
|
|
34468
35498
|
const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
|
|
34469
35499
|
return {
|
|
34470
35500
|
name,
|
|
@@ -34481,7 +35511,7 @@ function startHost(opts) {
|
|
|
34481
35511
|
// sessions we started ourselves. The server already knows: `last_seen_at` on the
|
|
34482
35512
|
// agent's own token, the same evidence `wakeAction` stands back for. So: a run we
|
|
34483
35513
|
// hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
|
|
34484
|
-
running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
|
|
35514
|
+
running: live.has(name) || !!slot && lastHolders.has(slot) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
|
|
34485
35515
|
// `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
|
|
34486
35516
|
// thing a tail can tell us. A process we did not spawn has no tail here to read.
|
|
34487
35517
|
working: live.get(name)?.working() ?? false
|
|
@@ -34497,7 +35527,7 @@ function startHost(opts) {
|
|
|
34497
35527
|
streamActivity();
|
|
34498
35528
|
clearInterval(activityTick);
|
|
34499
35529
|
try {
|
|
34500
|
-
|
|
35530
|
+
writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
|
|
34501
35531
|
} catch {
|
|
34502
35532
|
}
|
|
34503
35533
|
}
|
|
@@ -34530,17 +35560,17 @@ function startBridgeInstalls(bridges, deps = {}) {
|
|
|
34530
35560
|
const sleep3 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
34531
35561
|
const running = bridges.map((b) => {
|
|
34532
35562
|
let proc = null;
|
|
34533
|
-
let
|
|
35563
|
+
let outcome2;
|
|
34534
35564
|
try {
|
|
34535
35565
|
proc = spawn2(b.install);
|
|
34536
|
-
|
|
35566
|
+
outcome2 = proc.exited.then(
|
|
34537
35567
|
(code) => code === 0 ? "installed" : "failed",
|
|
34538
35568
|
() => "failed"
|
|
34539
35569
|
);
|
|
34540
35570
|
} catch {
|
|
34541
|
-
|
|
35571
|
+
outcome2 = Promise.resolve("failed");
|
|
34542
35572
|
}
|
|
34543
|
-
return { ...b, proc, outcome };
|
|
35573
|
+
return { ...b, proc, outcome: outcome2 };
|
|
34544
35574
|
});
|
|
34545
35575
|
return {
|
|
34546
35576
|
started: running.length,
|
|
@@ -34602,18 +35632,18 @@ async function pairThisMachine() {
|
|
|
34602
35632
|
}
|
|
34603
35633
|
}
|
|
34604
35634
|
function forwardCodexThreadId() {
|
|
34605
|
-
const cfg =
|
|
35635
|
+
const cfg = join10(process.env.CODEX_HOME || join10(homedir10(), ".codex"), "config.toml");
|
|
34606
35636
|
if (!existsSync5(cfg)) return;
|
|
34607
|
-
const text =
|
|
35637
|
+
const text = readFileSync6(cfg, "utf8");
|
|
34608
35638
|
const head = "[mcp_servers.paigy]\n";
|
|
34609
35639
|
const at = text.indexOf(head);
|
|
34610
35640
|
if (at < 0) return;
|
|
34611
|
-
const
|
|
35641
|
+
const block2 = text.slice(at + head.length, (() => {
|
|
34612
35642
|
const n = text.indexOf("\n[", at + head.length);
|
|
34613
35643
|
return n < 0 ? text.length : n;
|
|
34614
35644
|
})());
|
|
34615
|
-
if (/^\s*env_vars\s*=/m.test(
|
|
34616
|
-
|
|
35645
|
+
if (/^\s*env_vars\s*=/m.test(block2)) return;
|
|
35646
|
+
writeFileSync5(cfg, text.slice(0, at + head.length) + 'env_vars = ["CODEX_THREAD_ID", "CODEX_SESSION_ID"]\n' + text.slice(at + head.length));
|
|
34617
35647
|
}
|
|
34618
35648
|
async function main() {
|
|
34619
35649
|
const parsed = parseArgs(process.argv.slice(2));
|
|
@@ -34704,7 +35734,7 @@ async function main() {
|
|
|
34704
35734
|
const wsDeps = { file: workspacesFile() };
|
|
34705
35735
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
34706
35736
|
if (!listWorkspaces(wsDeps).length) {
|
|
34707
|
-
const guess =
|
|
35737
|
+
const guess = join10(homedir10(), "projects");
|
|
34708
35738
|
if (existsSync5(guess)) {
|
|
34709
35739
|
addWorkspace(guess, wsDeps);
|
|
34710
35740
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -34717,23 +35747,14 @@ async function main() {
|
|
|
34717
35747
|
console.log("service install failed \u2014 run `paigy-harness host` to host in the foreground");
|
|
34718
35748
|
}
|
|
34719
35749
|
} else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
|
|
34720
|
-
const
|
|
34721
|
-
|
|
34722
|
-
// Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
|
|
34723
|
-
// a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
|
|
34724
|
-
// written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
|
|
34725
|
-
// all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
|
|
34726
|
-
{ cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
|
|
34727
|
-
{ cli: "agy", name: "Antigravity" }
|
|
34728
|
-
];
|
|
34729
|
-
for (const t of TERMINAL_AGENTS) {
|
|
34730
|
-
if (!which(t.cli) || !t.wire) continue;
|
|
35750
|
+
for (const t of MCP_WIRES) {
|
|
35751
|
+
if (!which(t.cli)) continue;
|
|
34731
35752
|
try {
|
|
34732
35753
|
execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
|
|
34733
35754
|
if (t.cli === "codex") forwardCodexThreadId();
|
|
34734
35755
|
console.log(`\u2713 paigy wired for ${t.cli} \u2014 each session hatches its own identity (restart open sessions)`);
|
|
34735
35756
|
} catch (e) {
|
|
34736
|
-
console.log(`\u26A0 could not wire ${t.cli} (${e.message.split("\n")[0]}) \u2014
|
|
35757
|
+
console.log(`\u26A0 could not wire ${t.cli} (${e.message.split("\n")[0]}) \u2014 its agents cannot reach you until it is: ${t.wire}`);
|
|
34737
35758
|
}
|
|
34738
35759
|
}
|
|
34739
35760
|
if (which("claude")) {
|
|
@@ -34745,12 +35766,12 @@ async function main() {
|
|
|
34745
35766
|
}
|
|
34746
35767
|
}
|
|
34747
35768
|
try {
|
|
34748
|
-
const settings =
|
|
35769
|
+
const settings = join10(homedir10(), ".claude", "settings.json");
|
|
34749
35770
|
if (which("claude") && existsSync5(settings)) {
|
|
34750
|
-
const cfg = JSON.parse(
|
|
35771
|
+
const cfg = JSON.parse(readFileSync6(settings, "utf8"));
|
|
34751
35772
|
if (!cfg["statusLine"]) {
|
|
34752
35773
|
cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
|
|
34753
|
-
|
|
35774
|
+
writeFileSync5(settings, JSON.stringify(cfg, null, 2));
|
|
34754
35775
|
console.log("\u2713 statusline wired");
|
|
34755
35776
|
}
|
|
34756
35777
|
}
|
|
@@ -34803,14 +35824,14 @@ async function main() {
|
|
|
34803
35824
|
<key>SoftResourceLimits</key><dict>
|
|
34804
35825
|
<key>NumberOfFiles</key><integer>65536</integer>
|
|
34805
35826
|
</dict>
|
|
34806
|
-
<key>StandardOutPath</key><string>${
|
|
34807
|
-
<key>StandardErrorPath</key><string>${
|
|
35827
|
+
<key>StandardOutPath</key><string>${join10(homedir10(), ".paigy", "host.log")}</string>
|
|
35828
|
+
<key>StandardErrorPath</key><string>${join10(homedir10(), ".paigy", "host.log")}</string>
|
|
34808
35829
|
</dict></plist>
|
|
34809
35830
|
`;
|
|
34810
|
-
const dir =
|
|
34811
|
-
|
|
34812
|
-
const path =
|
|
34813
|
-
|
|
35831
|
+
const dir = join10(homedir10(), "Library", "LaunchAgents");
|
|
35832
|
+
mkdirSync4(dir, { recursive: true });
|
|
35833
|
+
const path = join10(dir, `${SERVICE_LABEL}.plist`);
|
|
35834
|
+
writeFileSync5(path, plist);
|
|
34814
35835
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
34815
35836
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
34816
35837
|
console.log(" file limit raised to 65536 for the host and every session it spawns");
|
|
@@ -34827,12 +35848,12 @@ async function main() {
|
|
|
34827
35848
|
process.exit(1);
|
|
34828
35849
|
}
|
|
34829
35850
|
const cwd = process.cwd();
|
|
34830
|
-
if (!updateSlot(slot, { workspace: cwd })) {
|
|
35851
|
+
if (!updateSlot(slot, { workspace: cwd, session_id: sessionId() })) {
|
|
34831
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.`);
|
|
34832
35853
|
process.exit(1);
|
|
34833
35854
|
}
|
|
34834
35855
|
const name = slotName(slot) ?? slot;
|
|
34835
|
-
console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(
|
|
35856
|
+
console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir10(), "~")}`);
|
|
34836
35857
|
if (!allowed(cwd, { file: workspacesFile() })) {
|
|
34837
35858
|
console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
|
|
34838
35859
|
console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
|
|
@@ -34924,12 +35945,11 @@ async function main() {
|
|
|
34924
35945
|
const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);
|
|
34925
35946
|
if (!said) return null;
|
|
34926
35947
|
return /^(y|yes|ok|allow|approve)/i.test(said) ? { allow: true } : { allow: false, reason: said };
|
|
34927
|
-
}
|
|
34928
|
-
question: (q) => askTerminal(`\u2753 ${q}`)
|
|
35948
|
+
}
|
|
34929
35949
|
},
|
|
34930
35950
|
log: (line) => console.log(line)
|
|
34931
35951
|
});
|
|
34932
|
-
const
|
|
35952
|
+
const beat2 = async () => {
|
|
34933
35953
|
try {
|
|
34934
35954
|
await heartbeat();
|
|
34935
35955
|
signedOut = false;
|
|
@@ -34942,8 +35962,8 @@ async function main() {
|
|
|
34942
35962
|
console.log(" paigy-harness pair");
|
|
34943
35963
|
}
|
|
34944
35964
|
};
|
|
34945
|
-
const pulse = setInterval(() => void
|
|
34946
|
-
void
|
|
35965
|
+
const pulse = setInterval(() => void beat2(), 6e4);
|
|
35966
|
+
void beat2();
|
|
34947
35967
|
pulse.unref?.();
|
|
34948
35968
|
process.on("SIGINT", () => {
|
|
34949
35969
|
run.stop();
|