@paigy/harness 0.3.30 → 0.3.32
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 +10 -4
- package/dist/cli.js +380 -112
- package/dist/main.js +315 -68
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -22793,14 +22793,14 @@ function sessionId() {
|
|
|
22793
22793
|
async function reach(url, init) {
|
|
22794
22794
|
return send(url, init, sessionId());
|
|
22795
22795
|
}
|
|
22796
|
-
function reachAs(session) {
|
|
22797
|
-
return (url, init) => send(url, init, session);
|
|
22796
|
+
function reachAs(session, instance) {
|
|
22797
|
+
return (url, init) => send(url, init, session, instance);
|
|
22798
22798
|
}
|
|
22799
|
-
async function send(url, init, session) {
|
|
22799
|
+
async function send(url, init, session, instance = envSession("PAIGY_INSTANCE_ID") ?? INSTANCE_ID) {
|
|
22800
22800
|
try {
|
|
22801
22801
|
const headers = {
|
|
22802
22802
|
...init?.headers,
|
|
22803
|
-
"x-paigy-instance":
|
|
22803
|
+
"x-paigy-instance": instance,
|
|
22804
22804
|
"x-paigy-session": session
|
|
22805
22805
|
};
|
|
22806
22806
|
return await fetch(url, { ...init, headers, dispatcher: await proxy() });
|
|
@@ -23749,6 +23749,9 @@ function readToken(agent2 = agentName()) {
|
|
|
23749
23749
|
if (process.env.PAIGY_TOKEN) return process.env.PAIGY_TOKEN;
|
|
23750
23750
|
return readTokenFile()[agent2]?.access_token ?? "";
|
|
23751
23751
|
}
|
|
23752
|
+
function readSlot(agent2) {
|
|
23753
|
+
return readTokenFile()[agent2]?.access_token ?? "";
|
|
23754
|
+
}
|
|
23752
23755
|
function listSlots() {
|
|
23753
23756
|
return Object.keys(readTokenFile());
|
|
23754
23757
|
}
|
|
@@ -23831,11 +23834,16 @@ async function updateGoal(goalId, input, opts = {}) {
|
|
|
23831
23834
|
if (!res.ok) await fail("update_goal", res);
|
|
23832
23835
|
return await res.json();
|
|
23833
23836
|
}
|
|
23834
|
-
async function hatch(name, voice = null) {
|
|
23835
|
-
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/hatch`, {
|
|
23837
|
+
async function hatch(name, voice = null, opts = {}) {
|
|
23838
|
+
const res = await ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/hatch`, {
|
|
23836
23839
|
method: "POST",
|
|
23837
23840
|
headers: { "content-type": "application/json", authorization: `Bearer ${authToken()}` },
|
|
23838
|
-
body: JSON.stringify({
|
|
23841
|
+
body: JSON.stringify({
|
|
23842
|
+
name,
|
|
23843
|
+
voice,
|
|
23844
|
+
...opts.brief ? { brief: opts.brief } : {},
|
|
23845
|
+
...opts.parentGoalId ? { parentGoalId: opts.parentGoalId } : {}
|
|
23846
|
+
})
|
|
23839
23847
|
}));
|
|
23840
23848
|
if (!res.ok) throw new Error(`hatch failed: ${res.status} ${await res.text()}`);
|
|
23841
23849
|
return await res.json();
|
|
@@ -23858,6 +23866,22 @@ async function claimSessions(opts = {}) {
|
|
|
23858
23866
|
if (!res.ok) throw new Error(`claim_sessions failed: ${res.status}`);
|
|
23859
23867
|
return (await res.json()).sessions;
|
|
23860
23868
|
}
|
|
23869
|
+
async function claimSessionEnds(opts = {}) {
|
|
23870
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/claim`, {
|
|
23871
|
+
headers: { authorization: `Bearer ${authToken(opts.token)}` }
|
|
23872
|
+
// the HOST's identity
|
|
23873
|
+
}));
|
|
23874
|
+
if (!res.ok) throw new Error(`claim_session_ends failed: ${res.status}`);
|
|
23875
|
+
return (await res.json()).ends ?? [];
|
|
23876
|
+
}
|
|
23877
|
+
async function finishSessionEnd(endId, result, opts = {}) {
|
|
23878
|
+
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/sessions/ends/${encodeURIComponent(endId)}`, {
|
|
23879
|
+
method: "POST",
|
|
23880
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
|
|
23881
|
+
body: JSON.stringify({ result })
|
|
23882
|
+
}));
|
|
23883
|
+
if (!res.ok) await fail("finish_session_end", res);
|
|
23884
|
+
}
|
|
23861
23885
|
async function recordDecision(decision, opts = {}) {
|
|
23862
23886
|
const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/host/decisions`, {
|
|
23863
23887
|
method: "POST",
|
|
@@ -23964,7 +23988,9 @@ async function contact(input, opts = {}) {
|
|
|
23964
23988
|
// this cannot see.
|
|
23965
23989
|
repo: repoFromRemote(a.repo) ?? a.repo ?? currentRepo() ?? void 0,
|
|
23966
23990
|
ask: a.ask,
|
|
23967
|
-
options: a.options ?? []
|
|
23991
|
+
options: a.options ?? [],
|
|
23992
|
+
...a.select ? { select: a.select } : {},
|
|
23993
|
+
...a.answers ? { answers: a.answers } : {}
|
|
23968
23994
|
}));
|
|
23969
23995
|
const res = await ensureAuthed(await send22(`${BACKEND_URL}/api/goals/contact`, {
|
|
23970
23996
|
method: "POST",
|
|
@@ -24186,7 +24212,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
24186
24212
|
}
|
|
24187
24213
|
};
|
|
24188
24214
|
}
|
|
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, WORKING_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, CallRecapSchema, 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;
|
|
24215
|
+
var import_undici, require2, CODEX_ENV, 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, WORKING_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, CallRecapSchema, 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;
|
|
24190
24216
|
var init_dist = __esm({
|
|
24191
24217
|
"../../packages/sdk/dist/index.js"() {
|
|
24192
24218
|
"use strict";
|
|
@@ -24199,6 +24225,7 @@ var init_dist = __esm({
|
|
|
24199
24225
|
init_zod();
|
|
24200
24226
|
import_undici = __toESM(require_undici(), 1);
|
|
24201
24227
|
require2 = __sdkCreateRequire(import.meta.url);
|
|
24228
|
+
CODEX_ENV = ["CODEX_THREAD_ID", "CODEX_SESSION_ID", "PAIGY_TOKEN", "PAIGY_SESSION_ID", "PAIGY_HARNESS", "PAIGY_INSTANCE_ID"];
|
|
24202
24229
|
BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
24203
24230
|
NETWORK_MSG = `Can't reach ${BACKEND_URL} \u2014 the connection was blocked or dropped before an HTTP response. If this agent runs in a sandboxed environment with a network allowlist (e.g. Claude Code on the web, CI), ask the user to add the Paigy domain (paigy.ai) to the environment's allowed domains, then retry.`;
|
|
24204
24231
|
PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
@@ -24601,7 +24628,18 @@ var init_dist = __esm({
|
|
|
24601
24628
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
24602
24629
|
"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."
|
|
24603
24630
|
),
|
|
24604
|
-
options: external_exports.array(OptionInputSchema).min(1).max(6).optional()
|
|
24631
|
+
options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
|
|
24632
|
+
// YOUR VIEW OF ONE OR MANY, A HINT TO PAIGY'S READ (owner, 2026-10-02: "one and many makes sense...
|
|
24633
|
+
// make sure the brain that formats is aware of how it should work"). Three independent fixes went
|
|
24634
|
+
// out as "All three / 1 and 3 only / Just log it" because every card was pick-one. #2943 made this
|
|
24635
|
+
// field override the read; no agent sent it, so nothing changed. The read now decides each card's
|
|
24636
|
+
// shape (`goal/intake.ts` `answerOf`), and this reaches it as the agent's view, weighed, not obeyed.
|
|
24637
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
24638
|
+
`Your view of how the options are answered: "one" to pick one, "many" to tick any number of them. A hint: Paigy's read decides each card's shape, a checklist when the options can all be wanted at once, a pick when choosing one rules out the others. The person can always answer in their own words as well. Ignored without options.`
|
|
24639
|
+
),
|
|
24640
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
24641
|
+
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
24642
|
+
)
|
|
24605
24643
|
}).strict();
|
|
24606
24644
|
StartContactSchema = external_exports.object({
|
|
24607
24645
|
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."),
|
|
@@ -24610,7 +24648,7 @@ var init_dist = __esm({
|
|
|
24610
24648
|
}).strict();
|
|
24611
24649
|
ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
24612
24650
|
CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
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.";
|
|
24651
|
+
CONTACT_DESCRIPTION = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', '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. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). Whether a card is a checklist or a pick is decided by Paigy's read: independent options they may want several of become a checklist, alternatives a pick. select:'many' or select:'one' tells it your view, a hint it weighs. The person can always answer in their own words, so never add an 'Other' option. 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.";
|
|
24614
24652
|
CreateGoalSchema = external_exports.object({
|
|
24615
24653
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
24616
24654
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -24633,7 +24671,7 @@ var init_dist = __esm({
|
|
|
24633
24671
|
CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
24634
24672
|
idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
24635
24673
|
}).strict();
|
|
24636
|
-
CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only:
|
|
24674
|
+
CREATE_GOAL_DESCRIPTION = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
|
|
24637
24675
|
UpdateGoalSchema = external_exports.object({
|
|
24638
24676
|
revision: external_exports.number().int().positive(),
|
|
24639
24677
|
changes: external_exports.object({
|
|
@@ -24669,12 +24707,13 @@ var init_dist = __esm({
|
|
|
24669
24707
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
24670
24708
|
diagnose: external_exports.boolean().optional()
|
|
24671
24709
|
}).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.
|
|
24673
|
-
UPDATE_GOAL_DESCRIPTION = `Update
|
|
24674
|
-
CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own
|
|
24710
|
+
GET_GOAL_DESCRIPTION = "Read one Goal without claiming it, including others (the ten most recent other contributors, with names, latest entry headlines and times): 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. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. 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. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals 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.";
|
|
24711
|
+
UPDATE_GOAL_DESCRIPTION = `Update a Goal you are on at an exact revision. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. 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.`;
|
|
24712
|
+
CLAIM_GOAL_DESCRIPTION = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change its state; use update_goal with ownerParticipant when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
24675
24713
|
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.";
|
|
24676
24714
|
CheckRepliesSchema = external_exports.object({}).strict();
|
|
24677
24715
|
AGENT_TOOLS = [
|
|
24716
|
+
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
24678
24717
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
24679
24718
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
24680
24719
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
@@ -24684,7 +24723,7 @@ var init_dist = __esm({
|
|
|
24684
24723
|
];
|
|
24685
24724
|
AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
24686
24725
|
LIVE_MS = 3 * 6e4;
|
|
24687
|
-
WORKING_MS =
|
|
24726
|
+
WORKING_MS = 60 * 6e4;
|
|
24688
24727
|
ContextSchema = external_exports.object({
|
|
24689
24728
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
24690
24729
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -25036,7 +25075,13 @@ var init_dist = __esm({
|
|
|
25036
25075
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
25037
25076
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
25038
25077
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
25039
|
-
blocking: external_exports.boolean().optional()
|
|
25078
|
+
blocking: external_exports.boolean().optional(),
|
|
25079
|
+
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
25080
|
+
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
25081
|
+
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
25082
|
+
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
25083
|
+
* speak, it is dropped and their words are taken like any reply. */
|
|
25084
|
+
ifSilent: external_exports.boolean().optional()
|
|
25040
25085
|
});
|
|
25041
25086
|
CLAIM_STALE_MS = 30 * 6e4;
|
|
25042
25087
|
InboxItemSchema = external_exports.object({
|
|
@@ -25237,12 +25282,13 @@ var init_dist = __esm({
|
|
|
25237
25282
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
25238
25283
|
* what people read (owner, 2026-09-30). */
|
|
25239
25284
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
25240
|
-
/**
|
|
25241
|
-
*
|
|
25242
|
-
*
|
|
25243
|
-
* wanting
|
|
25244
|
-
*
|
|
25245
|
-
|
|
25285
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
25286
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
25287
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
25288
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
25289
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
25290
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
25291
|
+
* next dead field for the same reason. */
|
|
25246
25292
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
25247
25293
|
improveConsent: external_exports.boolean(),
|
|
25248
25294
|
missedCall: MissedCallSchema.default("backoff_standard"),
|
|
@@ -25376,7 +25422,8 @@ var init_dist = __esm({
|
|
|
25376
25422
|
});
|
|
25377
25423
|
LedgerItemSchema = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
25378
25424
|
AgentLedgerSchema = external_exports.object({
|
|
25379
|
-
agent
|
|
25425
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
25426
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
25380
25427
|
/** Its own questions you have not answered. */
|
|
25381
25428
|
asks: external_exports.array(LedgerItemSchema),
|
|
25382
25429
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -25543,7 +25590,7 @@ var init_dist = __esm({
|
|
|
25543
25590
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
25544
25591
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
25545
25592
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
25546
|
-
*
|
|
25593
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
25547
25594
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
25548
25595
|
*
|
|
25549
25596
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -25860,7 +25907,9 @@ var init_dist = __esm({
|
|
|
25860
25907
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
25861
25908
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
25862
25909
|
* anything that is not an answer. */
|
|
25863
|
-
|
|
25910
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
25911
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
25912
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
|
|
25864
25913
|
})),
|
|
25865
25914
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
25866
25915
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -31041,9 +31090,9 @@ init_catalog();
|
|
|
31041
31090
|
|
|
31042
31091
|
// src/cli-args.ts
|
|
31043
31092
|
var TRIAGE_VIA = ["ollama", "claude", "codex", "agy"];
|
|
31044
|
-
var USAGE = "usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | enable-tools [--scope user|project] | hatch NAME [--voice KEY] [--slot SLOT] | handoff | triage [--via ollama|claude|codex|agy] [--json] [--apply [--yes-close] [--yes-stale] [--yes-assign]] | [--harness claude|codex|agy] [--mode bypass|ask] [--cwd DIR] [--
|
|
31093
|
+
var USAGE = "usage: paigy-harness setup | pair | host [--grant DIR]\u2026 | service | enable-tools [--scope user|project] | hatch NAME [--voice KEY] [--slot SLOT] | launch NAME [--under GOAL] [--voice KEY] [--harness \u2026] [--mode \u2026] [--cwd DIR] BRIEF\u2026 | handoff | triage [--via ollama|claude|codex|agy] [--json] [--apply [--yes-close] [--yes-stale] [--yes-assign]] | [--harness claude|codex|agy] [--mode bypass|ask] [--cwd DIR] [--grace SECONDS] [--doctor] PROMPT\u2026";
|
|
31045
31094
|
function parseArgs(argv) {
|
|
31046
|
-
const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, help: false, host: false, pair: false, service: false, setup: false, enableTools: false, scope: "user", grant: [], hatch: null, voice: null, slot: null,
|
|
31095
|
+
const args = { harness: "claude", mode: "bypass", cwd: process.cwd(), doctor: false, help: false, host: false, pair: false, service: false, setup: false, enableTools: false, scope: "user", grant: [], hatch: null, voice: null, slot: null, launch: null, under: null, graceSeconds: 90, prompt: "", handoff: false, triage: false, via: null, json: false, apply: false, yesClose: false, yesStale: false, yesAssign: false };
|
|
31047
31096
|
const words2 = [];
|
|
31048
31097
|
for (let i = 0; i < argv.length; i++) {
|
|
31049
31098
|
const arg = argv[i];
|
|
@@ -31114,9 +31163,13 @@ function parseArgs(argv) {
|
|
|
31114
31163
|
} else if (arg === "--slot") {
|
|
31115
31164
|
args.slot = next() ?? null;
|
|
31116
31165
|
} else if (arg === "--identity") {
|
|
31166
|
+
return { error: '--identity is gone: start a helper with `paigy-harness launch NAME "brief"`, which gives it its own session' };
|
|
31167
|
+
} else if (arg === "launch") {
|
|
31117
31168
|
const v = next();
|
|
31118
|
-
if (!v) return { error:
|
|
31119
|
-
args.
|
|
31169
|
+
if (!v) return { error: 'launch needs a name: paigy-harness launch "Deploy check" "brief"' };
|
|
31170
|
+
args.launch = v;
|
|
31171
|
+
} else if (arg === "--under") {
|
|
31172
|
+
args.under = next() ?? null;
|
|
31120
31173
|
} else if (arg?.startsWith("--")) {
|
|
31121
31174
|
return { error: `unknown flag ${arg}` };
|
|
31122
31175
|
} else if (arg) {
|
|
@@ -31125,6 +31178,7 @@ function parseArgs(argv) {
|
|
|
31125
31178
|
}
|
|
31126
31179
|
args.prompt = words2.join(" ");
|
|
31127
31180
|
if (args.help) return args;
|
|
31181
|
+
if (args.launch && !args.prompt) return { error: 'launch needs a brief: paigy-harness launch "Deploy check" "brief"' };
|
|
31128
31182
|
if (!args.doctor && !args.handoff && !args.hatch && !args.host && !args.pair && !args.service && !args.setup && !args.enableTools && !args.triage && !args.prompt) return { error: "a prompt is required (or setup / --doctor / pair / host / service / enable-tools / hatch NAME / handoff / triage)" };
|
|
31129
31183
|
if ((args.yesClose || args.yesStale || args.yesAssign) && !args.apply)
|
|
31130
31184
|
return { error: "--yes-close/--yes-stale/--yes-assign only apply with --apply" };
|
|
@@ -31157,20 +31211,23 @@ function onEnter(input, onLine) {
|
|
|
31157
31211
|
// src/cli.ts
|
|
31158
31212
|
init_dist();
|
|
31159
31213
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
31214
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
31160
31215
|
import { hostname } from "os";
|
|
31161
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as
|
|
31216
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
31162
31217
|
import { execFile as execFile2, execSync } from "child_process";
|
|
31163
31218
|
import { homedir as homedir10 } from "os";
|
|
31164
|
-
import { join as
|
|
31219
|
+
import { join as join11 } from "path";
|
|
31165
31220
|
|
|
31166
31221
|
// src/run.ts
|
|
31167
31222
|
init_dist();
|
|
31223
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
31168
31224
|
|
|
31169
31225
|
// src/harness/session.ts
|
|
31226
|
+
init_dist();
|
|
31170
31227
|
import { spawn } from "child_process";
|
|
31171
|
-
import { existsSync as existsSync3 } from "fs";
|
|
31228
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
31172
31229
|
import { homedir as homedir4 } from "os";
|
|
31173
|
-
import { resolve, delimiter as delimiter2 } from "path";
|
|
31230
|
+
import { resolve, delimiter as delimiter2, join as join5 } from "path";
|
|
31174
31231
|
|
|
31175
31232
|
// src/harness/acp.ts
|
|
31176
31233
|
var none = { events: [], writes: [] };
|
|
@@ -31544,6 +31601,21 @@ function startSession(opts) {
|
|
|
31544
31601
|
if (!existsSync3(cwd)) {
|
|
31545
31602
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
31546
31603
|
}
|
|
31604
|
+
const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
|
|
31605
|
+
if (codexConfig) {
|
|
31606
|
+
const key = "mcp_servers.paigy.env_vars";
|
|
31607
|
+
const configPath = join5(process.env.CODEX_HOME || join5(homedir4(), ".codex"), "config.toml");
|
|
31608
|
+
const saved = existsSync3(configPath) ? readFileSync3(configPath, "utf8") : "";
|
|
31609
|
+
const registered = /\[mcp_servers\.paigy\][\s\S]*?(?=\n\[|$)/.exec(saved)?.[0];
|
|
31610
|
+
const configured = codexConfig["mcp_servers.paigy.command"] || codexConfig["mcp_servers.paigy.url"] || codexConfig.mcp_servers?.paigy?.command || codexConfig.mcp_servers?.paigy?.url;
|
|
31611
|
+
if (!configured && !registered) {
|
|
31612
|
+
codexConfig["mcp_servers.paigy.command"] = "npx";
|
|
31613
|
+
codexConfig["mcp_servers.paigy.args"] = ["-y", "@paigy/mcp@latest"];
|
|
31614
|
+
}
|
|
31615
|
+
const savedVars = registered?.match(/env_vars\s*=\s*\[([\s\S]*?)\]/)?.[1] ?? "";
|
|
31616
|
+
const inherited = [...savedVars.matchAll(/["']([A-Za-z_][A-Za-z0-9_]*)["']/g)].map((match) => match[1]);
|
|
31617
|
+
codexConfig[key] = [.../* @__PURE__ */ new Set([...inherited, ...codexConfig[key] ?? codexConfig.mcp_servers?.paigy?.env_vars ?? [], ...CODEX_ENV])];
|
|
31618
|
+
}
|
|
31547
31619
|
const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
|
|
31548
31620
|
cwd,
|
|
31549
31621
|
// The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
|
|
@@ -31554,7 +31626,9 @@ function startSession(opts) {
|
|
|
31554
31626
|
...process.env,
|
|
31555
31627
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
31556
31628
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
31557
|
-
...opts.
|
|
31629
|
+
...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
|
|
31630
|
+
...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
|
|
31631
|
+
...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
|
|
31558
31632
|
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
31559
31633
|
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
31560
31634
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
@@ -32981,7 +33055,18 @@ var AskInputSchema2 = external_exports.object({
|
|
|
32981
33055
|
ask: external_exports.string().trim().min(1).max(1e4).describe(
|
|
32982
33056
|
"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."
|
|
32983
33057
|
),
|
|
32984
|
-
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional()
|
|
33058
|
+
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
|
|
33059
|
+
// YOUR VIEW OF ONE OR MANY, A HINT TO PAIGY'S READ (owner, 2026-10-02: "one and many makes sense...
|
|
33060
|
+
// make sure the brain that formats is aware of how it should work"). Three independent fixes went
|
|
33061
|
+
// out as "All three / 1 and 3 only / Just log it" because every card was pick-one. #2943 made this
|
|
33062
|
+
// field override the read; no agent sent it, so nothing changed. The read now decides each card's
|
|
33063
|
+
// shape (`goal/intake.ts` `answerOf`), and this reaches it as the agent's view, weighed, not obeyed.
|
|
33064
|
+
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
33065
|
+
`Your view of how the options are answered: "one" to pick one, "many" to tick any number of them. A hint: Paigy's read decides each card's shape, a checklist when the options can all be wanted at once, a pick when choosing one rules out the others. The person can always answer in their own words as well. Ignored without options.`
|
|
33066
|
+
),
|
|
33067
|
+
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
33068
|
+
"Your reply answers a question the person asked you: the id the conversation shows for it (8 characters or whole). Their question closes with this reply as its answer, and any decision of yours it was holding back goes back to them. Needs parentId: the Goal the question is on. The reply is sent to them as it is; to also ask something new, send that as its own ask."
|
|
33069
|
+
)
|
|
32985
33070
|
}).strict();
|
|
32986
33071
|
var StartContactSchema2 = external_exports.object({
|
|
32987
33072
|
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."),
|
|
@@ -32990,7 +33075,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
32990
33075
|
}).strict();
|
|
32991
33076
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
32992
33077
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
32993
|
-
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.";
|
|
33078
|
+
var CONTACT_DESCRIPTION2 = "Contact the user with one or more asks/questions. Pass an array of asks (each with 'ask', optional 'options', 'select', '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. Each option's label must stand on its own \u2014 the person may see only the labels \u2014 so never a label that points into your text ('All three', 'Option 2', '1 and 3 only'). Whether a card is a checklist or a pick is decided by Paigy's read: independent options they may want several of become a checklist, alternatives a pick. select:'many' or select:'one' tells it your view, a hint it weighs. The person can always answer in their own words, so never add an 'Other' option. 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.";
|
|
32994
33079
|
var CreateGoalSchema2 = external_exports.object({
|
|
32995
33080
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
32996
33081
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -33013,7 +33098,7 @@ var CreateGoalSchema2 = external_exports.object({
|
|
|
33013
33098
|
var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
|
|
33014
33099
|
idempotencyKey: CreateGoalSchema2.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
|
|
33015
33100
|
}).strict();
|
|
33016
|
-
var CREATE_GOAL_DESCRIPTION2 = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only:
|
|
33101
|
+
var CREATE_GOAL_DESCRIPTION2 = 'Create a durable Goal for an outcome. Without parentGoalId it is placed against your open Goals: if one already IS this work, that Goal comes back (existing: true) and nothing new is created \u2014 continue it; if the work belongs under one, it is created there (parentGoalId in the receipt); otherwise it is a root. Pass parentGoalId yourself to put it under a specific Goal. Pass repo to anchor the work to a specific repository ("owner/repo" or repo name); delegated children inherit it. Pass title to name it in one to five words, as a person would refer to it out loud ("the night rings") \u2014 it heads every list and is spoken on a call; without one the brain writes it. Admission only: claim it before doing work, then update it as it advances. Returns an admission receipt with goalId, current state, revision, ownerParticipant, and the next step; no Goal content or execution lease.';
|
|
33017
33102
|
var UpdateGoalSchema2 = external_exports.object({
|
|
33018
33103
|
revision: external_exports.number().int().positive(),
|
|
33019
33104
|
changes: external_exports.object({
|
|
@@ -33049,12 +33134,13 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
33049
33134
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
33050
33135
|
diagnose: external_exports.boolean().optional()
|
|
33051
33136
|
}).strict();
|
|
33052
|
-
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.
|
|
33053
|
-
var UPDATE_GOAL_DESCRIPTION2 = `Update
|
|
33054
|
-
var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own
|
|
33137
|
+
var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it, including others (the ten most recent other contributors, with names, latest entry headlines and times): 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. A question in that conversation reads `open` (nothing yet), `answered` (a choice was made, and `answer` carries it), `replied` (they said something and the read settled the question on their words \u2014 NO option of yours was chosen, and the words are the reply line beside it, so read that before you act), or `closed`. 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. Every Goal of your person is readable, whichever of their agents owns it; another account's Goals 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.";
|
|
33138
|
+
var UPDATE_GOAL_DESCRIPTION2 = `Update a Goal you are on at an exact revision. You are on a Goal you own, or one you have written on (an update with progress, or a contact on it); any of your person's agents may write on any of their Goals, and one that never touched a Goal is refused (409 goal_not_joined). State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. A CONTACT ON THIS GOAL MOVES ITS REVISION: a question filed on a Goal is a change to it, so an update prepared before a contact and sent after it is refused as stale (409 goal_revision_conflict) \u2014 re-read the Goal, then write. 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.`;
|
|
33139
|
+
var CLAIM_GOAL_DESCRIPTION2 = "Claim a pending answer to your question or the oldest runnable or review-pending Goal you own. Pass goalId to join any Goal of your person; its assignment stays unchanged. Read others before overlapping another agent\u2019s work. Joining lets you contribute and change its state; use update_goal with ownerParticipant when the assignment itself should change. Returns the Goal as get_goal reads it, and marks you as on it, which never shuts another agent out: other agents of your person may write on it and change it too, and two changes at once are told apart by revision (409 goal_revision_conflict).";
|
|
33055
33140
|
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.";
|
|
33056
33141
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
33057
33142
|
var AGENT_TOOLS2 = [
|
|
33143
|
+
{ name: "who_is_working", description: "Read your person's agents that contributed in the last 24 hours, newest first: at most 30 agents and their five most recently touched Goals, with their latest entry headline and time. Use get_goal to read a Goal and its other contributors before starting overlapping work. Read-only; no arguments.", inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
33058
33144
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
33059
33145
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
33060
33146
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
|
|
@@ -33079,7 +33165,7 @@ function entryWords(entry) {
|
|
|
33079
33165
|
return entry.sources.map((source) => source.text).join("\n");
|
|
33080
33166
|
}
|
|
33081
33167
|
var LIVE_MS2 = 3 * 6e4;
|
|
33082
|
-
var WORKING_MS2 =
|
|
33168
|
+
var WORKING_MS2 = 60 * 6e4;
|
|
33083
33169
|
var ContextSchema2 = external_exports.object({
|
|
33084
33170
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
33085
33171
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -33431,7 +33517,13 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
33431
33517
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
33432
33518
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
33433
33519
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
33434
|
-
blocking: external_exports.boolean().optional()
|
|
33520
|
+
blocking: external_exports.boolean().optional(),
|
|
33521
|
+
/** SPOKEN ONLY IF THEY SAY NOTHING (owner, 2026-10-01, call 812de935: "you're gonna re-ask, but it
|
|
33522
|
+
* shouldn't be the same words … more like, hey, are you still there, or are you able to answer, or
|
|
33523
|
+
* would you need more information"). The walk holds this turn out of its queue; at the queue's end it
|
|
33524
|
+
* listens for the last word, and only if that listen is silent is this turn said and asked. If they
|
|
33525
|
+
* speak, it is dropped and their words are taken like any reply. */
|
|
33526
|
+
ifSilent: external_exports.boolean().optional()
|
|
33435
33527
|
});
|
|
33436
33528
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
33437
33529
|
var InboxItemSchema2 = external_exports.object({
|
|
@@ -33632,12 +33724,13 @@ var UserSettingsSchema2 = external_exports.object({
|
|
|
33632
33724
|
* they are an enum on a live column across every account, and the rename is a rename of
|
|
33633
33725
|
* what people read (owner, 2026-09-30). */
|
|
33634
33726
|
sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
|
|
33635
|
-
/**
|
|
33636
|
-
*
|
|
33637
|
-
*
|
|
33638
|
-
* wanting
|
|
33639
|
-
*
|
|
33640
|
-
silentPush
|
|
33727
|
+
/** `silentPush` lived here until #2813 and is now GONE, field and column both. It was kept as an
|
|
33728
|
+
* optional long after DateNight stopped reading it, on the theory that a phone on an older
|
|
33729
|
+
* bundle PATCHing the whole settings object would be REFUSED for sending a key we had stopped
|
|
33730
|
+
* wanting. That theory was wrong about this schema: these are plain `z.object`s with no
|
|
33731
|
+
* `.strict()` anywhere in this file, and zod STRIPS unknown keys rather than rejecting them, so
|
|
33732
|
+
* an old bundle's `silentPush` is accepted and ignored. Worth remembering before keeping the
|
|
33733
|
+
* next dead field for the same reason. */
|
|
33641
33734
|
/** Opt-in (default false) to using your content to improve Paigy and train models. */
|
|
33642
33735
|
improveConsent: external_exports.boolean(),
|
|
33643
33736
|
missedCall: MissedCallSchema2.default("backoff_standard"),
|
|
@@ -33771,7 +33864,8 @@ var ConnectionSummarySchema2 = external_exports.object({
|
|
|
33771
33864
|
});
|
|
33772
33865
|
var LedgerItemSchema2 = external_exports.object({ id: external_exports.string(), parentId: external_exports.string(), title: external_exports.string(), createdAt: external_exports.string() });
|
|
33773
33866
|
var AgentLedgerSchema2 = external_exports.object({
|
|
33774
|
-
agent
|
|
33867
|
+
/** Null when the agent has not named itself yet — never a placeholder (owner, 2026-10-01). */
|
|
33868
|
+
agent: external_exports.object({ id: external_exports.string(), name: external_exports.string().nullable(), revokedAt: external_exports.string().nullable() }),
|
|
33775
33869
|
/** Its own questions you have not answered. */
|
|
33776
33870
|
asks: external_exports.array(LedgerItemSchema2),
|
|
33777
33871
|
/** Its questions you answered that nobody acted on — still owed to somebody. */
|
|
@@ -33938,7 +34032,7 @@ var QueueItemSchema2 = external_exports.object({
|
|
|
33938
34032
|
* the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
|
|
33939
34033
|
* answer to a question, a voice note filed as work. Null when the only hands on it have been its
|
|
33940
34034
|
* agent's; optional, so a hand-built queue (fixtures, the demo) and a door older than
|
|
33941
|
-
*
|
|
34035
|
+
* 20261001030911 need not spell it — `work/list.ts`'s `yoursAt` keeps its other sources, so the
|
|
33942
34036
|
* Work tab orders as it did before the migration rather than throwing on the missing key.
|
|
33943
34037
|
*
|
|
33944
34038
|
* It is a FACT, not a reconstruction: `latest` holds one Entry, so an agent's progress note a
|
|
@@ -34253,7 +34347,9 @@ var CallRecapSchema2 = external_exports.object({
|
|
|
34253
34347
|
* summarized like a pre-made option is"), so the row scans like a chosen option and their
|
|
34254
34348
|
* own words stay under it. Absent on every line stored before the read wrote them, and on
|
|
34255
34349
|
* anything that is not an answer. */
|
|
34256
|
-
|
|
34350
|
+
/** `about` is the request the line answered (its question), null for words that answered none —
|
|
34351
|
+
* the key the screen groups on, so one question is one row however many times it was answered. */
|
|
34352
|
+
lines: external_exports.array(external_exports.object({ entryId: external_exports.string().uuid(), words: external_exports.string(), headline: external_exports.string().optional(), about: external_exports.string().nullable().optional() }))
|
|
34257
34353
|
})),
|
|
34258
34354
|
unfiled: external_exports.array(external_exports.object({ lineId: external_exports.string().uuid(), words: external_exports.string(), atMs: external_exports.number() })),
|
|
34259
34355
|
more: external_exports.object({ lines: external_exports.number(), entries: external_exports.number(), topics: external_exports.number() })
|
|
@@ -34467,9 +34563,13 @@ async function finish(result, state, rail, opts) {
|
|
|
34467
34563
|
const progress = result?.trim().slice(0, 1e4) || void 0;
|
|
34468
34564
|
let revision = goal.revision;
|
|
34469
34565
|
try {
|
|
34470
|
-
const held = await rail.
|
|
34566
|
+
const held = await rail.read(goal.id);
|
|
34471
34567
|
revision = held.revision ?? revision;
|
|
34472
34568
|
const owed = state.noted?.some((b) => b.goals.includes(goal.id));
|
|
34569
|
+
if (held.state === "done" || held.state === "cancelled") {
|
|
34570
|
+
if (!owed) state.goal = void 0;
|
|
34571
|
+
return;
|
|
34572
|
+
}
|
|
34473
34573
|
if (!held.openDecisionNeedIds?.length && !owed) {
|
|
34474
34574
|
await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
|
|
34475
34575
|
state.goal = void 0;
|
|
@@ -34558,11 +34658,16 @@ function beat() {
|
|
|
34558
34658
|
}
|
|
34559
34659
|
var BEAT_REPORT_MS = 10 * 6e4;
|
|
34560
34660
|
var ended = (state) => state === "done" || state === "cancelled";
|
|
34561
|
-
function unheard(brief, state, since = null, after = -1) {
|
|
34661
|
+
function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
34562
34662
|
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
34563
34663
|
const words2 = [];
|
|
34664
|
+
const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
|
|
34564
34665
|
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
34565
34666
|
if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
|
|
34667
|
+
if (pendingAt !== null && Date.parse(e.createdAt) < pendingAt) {
|
|
34668
|
+
seen.add(e.entryId);
|
|
34669
|
+
continue;
|
|
34670
|
+
}
|
|
34566
34671
|
if (i <= after) {
|
|
34567
34672
|
seen.add(e.entryId);
|
|
34568
34673
|
continue;
|
|
@@ -34573,6 +34678,12 @@ function unheard(brief, state, since = null, after = -1) {
|
|
|
34573
34678
|
const text = entryWords(e).trim();
|
|
34574
34679
|
if (text) words2.push(text);
|
|
34575
34680
|
}
|
|
34681
|
+
if (pendingAt !== null) for (const answer of brief.answers ?? []) {
|
|
34682
|
+
const key = `answer:${answer.decisionNeedId}`;
|
|
34683
|
+
if (seen.has(key) || Date.parse(answer.acceptedAt) < pendingAt) continue;
|
|
34684
|
+
seen.add(key);
|
|
34685
|
+
words2.push(`Answer to question ${answer.decisionNeedId}: ${JSON.stringify(answer.result)}`);
|
|
34686
|
+
}
|
|
34576
34687
|
return words2;
|
|
34577
34688
|
}
|
|
34578
34689
|
function sinceTheWork(brief) {
|
|
@@ -34641,9 +34752,9 @@ async function gather(state, rail, held) {
|
|
|
34641
34752
|
}
|
|
34642
34753
|
}
|
|
34643
34754
|
read.add(goalId);
|
|
34644
|
-
const fresh = found.via === "waiting work" || found.via === "assigned" ? sinceTheWork(brief) : { after: -1 };
|
|
34755
|
+
const fresh = !brief.reviewSince && (found.via === "waiting work" || found.via === "assigned") ? sinceTheWork(brief) : { after: -1 };
|
|
34645
34756
|
if (found.via === "assigned") announced.add(goalId);
|
|
34646
|
-
heard.push({ goalId, ...found, brief, words: unheard(brief, state, since ?? null, fresh.after), ...fresh.note ? { note: fresh.note } : {} });
|
|
34757
|
+
heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince), ...fresh.note ? { note: fresh.note } : {} });
|
|
34647
34758
|
}
|
|
34648
34759
|
for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
|
|
34649
34760
|
return { heard, listed };
|
|
@@ -34729,8 +34840,8 @@ async function acknowledge(goalId, brief, state, rail, log) {
|
|
|
34729
34840
|
if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
|
|
34730
34841
|
acked.set(goalId, brief.revision);
|
|
34731
34842
|
try {
|
|
34732
|
-
|
|
34733
|
-
const done = await rail.update(goalId, { revision:
|
|
34843
|
+
if (brief.revision === void 0) throw new Error("review_revision_missing");
|
|
34844
|
+
const done = await rail.update(goalId, { revision: brief.revision, changes: { reviewed: true }, reason: "Read by the harness session; the person's words on it are typed in." });
|
|
34734
34845
|
acked.set(goalId, done.revision);
|
|
34735
34846
|
if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
|
|
34736
34847
|
} catch (error) {
|
|
@@ -34767,25 +34878,26 @@ async function look(session, state, rail, opts) {
|
|
|
34767
34878
|
brief2 = await rail.claim();
|
|
34768
34879
|
if (!brief2.goalId || brief2.revision === void 0) {
|
|
34769
34880
|
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
34770
|
-
send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
|
|
34881
|
+
send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
|
|
34771
34882
|
return;
|
|
34772
34883
|
}
|
|
34773
34884
|
} else {
|
|
34774
34885
|
return;
|
|
34775
34886
|
}
|
|
34776
34887
|
}
|
|
34777
|
-
unheard(brief2, state);
|
|
34778
|
-
if (
|
|
34779
|
-
|
|
34888
|
+
const pendingWords = brief2.reviewSince ? unheard(brief2, state, null, -1, brief2.reviewSince) : null;
|
|
34889
|
+
if (!pendingWords) unheard(brief2, state);
|
|
34890
|
+
if (!ended(brief2.state)) {
|
|
34780
34891
|
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
34781
34892
|
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
34782
34893
|
}
|
|
34783
34894
|
const outcome2 = brief2.outcome?.trim() ?? "";
|
|
34784
|
-
const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
34895
|
+
const said = (pendingWords ?? (brief2.entries ?? []).map((e) => entryWords(e).trim())).filter((w) => w && w.toLowerCase() !== outcome2.toLowerCase());
|
|
34785
34896
|
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
34786
34897
|
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
|
|
34787
34898
|
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
34788
|
-
send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
|
|
34899
|
+
send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
|
|
34900
|
+
if (ended(brief2.state)) await acknowledge(brief2.goalId, brief2, state, rail, opts?.log);
|
|
34789
34901
|
return;
|
|
34790
34902
|
}
|
|
34791
34903
|
const brief = await rail.read(state.goal.id);
|
|
@@ -34794,6 +34906,7 @@ async function look(session, state, rail, opts) {
|
|
|
34794
34906
|
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
34795
34907
|
if (!resolve3) continue;
|
|
34796
34908
|
state.asks.delete(answer.requestEntryId);
|
|
34909
|
+
(state.seen ??= /* @__PURE__ */ new Set()).add(`answer:${answer.decisionNeedId}`);
|
|
34797
34910
|
resolve3(answer.result);
|
|
34798
34911
|
}
|
|
34799
34912
|
if (ended(brief.state)) {
|
|
@@ -34871,8 +34984,9 @@ function runHarness(opts) {
|
|
|
34871
34984
|
const state = { resting: true };
|
|
34872
34985
|
const clock4 = beat();
|
|
34873
34986
|
let session = null;
|
|
34874
|
-
const speaksAs = opts.
|
|
34875
|
-
const
|
|
34987
|
+
const speaksAs = opts.session ?? opts.resume ?? sessionId();
|
|
34988
|
+
const instance = randomUUID6();
|
|
34989
|
+
const rail = railFor({ token: opts.token, reach: reachAs(speaksAs, instance) });
|
|
34876
34990
|
let inbox;
|
|
34877
34991
|
if (opts.harness === "claude") {
|
|
34878
34992
|
try {
|
|
@@ -34928,6 +35042,7 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
34928
35042
|
}
|
|
34929
35043
|
}
|
|
34930
35044
|
session = startSession({
|
|
35045
|
+
instance,
|
|
34931
35046
|
harness: opts.harness,
|
|
34932
35047
|
cwd: opts.cwd,
|
|
34933
35048
|
mode: opts.mode,
|
|
@@ -34970,9 +35085,9 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
34970
35085
|
|
|
34971
35086
|
// src/host.ts
|
|
34972
35087
|
init_dist();
|
|
34973
|
-
import { readFileSync as
|
|
35088
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
34974
35089
|
import { homedir as homedir9 } from "os";
|
|
34975
|
-
import { join as
|
|
35090
|
+
import { join as join10 } from "path";
|
|
34976
35091
|
|
|
34977
35092
|
// src/broker.ts
|
|
34978
35093
|
init_evidence();
|
|
@@ -34992,15 +35107,15 @@ function ago(iso, now) {
|
|
|
34992
35107
|
init_catalog();
|
|
34993
35108
|
|
|
34994
35109
|
// src/workspaces.ts
|
|
34995
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as
|
|
34996
|
-
import { dirname as dirname2, join as
|
|
35110
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
35111
|
+
import { dirname as dirname2, join as join6, resolve as resolve2 } from "path";
|
|
34997
35112
|
import { homedir as homedir6 } from "os";
|
|
34998
35113
|
function workspacesFile() {
|
|
34999
|
-
return
|
|
35114
|
+
return join6(homedir6(), ".paigy", "workspaces.json");
|
|
35000
35115
|
}
|
|
35001
35116
|
function listWorkspaces(deps) {
|
|
35002
35117
|
try {
|
|
35003
|
-
const parsed = JSON.parse(
|
|
35118
|
+
const parsed = JSON.parse(readFileSync4(deps.file, "utf8"));
|
|
35004
35119
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
35005
35120
|
} catch {
|
|
35006
35121
|
return [];
|
|
@@ -35029,8 +35144,8 @@ function resolveWakeDir(pinned, deps) {
|
|
|
35029
35144
|
}
|
|
35030
35145
|
function originOf(dir) {
|
|
35031
35146
|
try {
|
|
35032
|
-
if (!statSync3(
|
|
35033
|
-
const config =
|
|
35147
|
+
if (!statSync3(join6(dir, ".git")).isDirectory()) return null;
|
|
35148
|
+
const config = readFileSync4(join6(dir, ".git", "config"), "utf8");
|
|
35034
35149
|
const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
|
|
35035
35150
|
return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
|
|
35036
35151
|
} catch {
|
|
@@ -35051,7 +35166,7 @@ function listRepos(deps) {
|
|
|
35051
35166
|
}
|
|
35052
35167
|
for (const name of children) {
|
|
35053
35168
|
if (repos.length >= MAX_REPOS) return repos;
|
|
35054
|
-
const path =
|
|
35169
|
+
const path = join6(root, name);
|
|
35055
35170
|
const remote = originOf(path);
|
|
35056
35171
|
if (remote) repos.push({ path, remote });
|
|
35057
35172
|
}
|
|
@@ -35062,9 +35177,9 @@ function listRepos(deps) {
|
|
|
35062
35177
|
// src/holder.ts
|
|
35063
35178
|
init_dist();
|
|
35064
35179
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
35065
|
-
import { readFileSync as
|
|
35180
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync3 } from "fs";
|
|
35066
35181
|
import { homedir as homedir7 } from "os";
|
|
35067
|
-
import { join as
|
|
35182
|
+
import { join as join7 } from "path";
|
|
35068
35183
|
function isAlive(pid) {
|
|
35069
35184
|
try {
|
|
35070
35185
|
process.kill(pid, 0);
|
|
@@ -35093,10 +35208,63 @@ function holds(pid, procStart, deps) {
|
|
|
35093
35208
|
}
|
|
35094
35209
|
return false;
|
|
35095
35210
|
}
|
|
35211
|
+
function proven(pid, procStart, deps) {
|
|
35212
|
+
if (!deps.alive(pid) || typeof procStart !== "string" || !procStart.trim()) return false;
|
|
35213
|
+
const want = squash(procStart);
|
|
35214
|
+
return ["UTC", "local"].some((zone) => {
|
|
35215
|
+
const got = deps.started(pid, zone);
|
|
35216
|
+
return got !== null && squash(got) === want;
|
|
35217
|
+
});
|
|
35218
|
+
}
|
|
35219
|
+
function localHolderPids(deps = {}) {
|
|
35220
|
+
const dir = deps.dir ?? join7(process.env.CLAUDE_CONFIG_DIR?.trim() || join7(homedir7(), ".claude"), "sessions");
|
|
35221
|
+
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
35222
|
+
const read = deps.read ?? ((f) => readFileSync5(f, "utf8"));
|
|
35223
|
+
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
35224
|
+
const held = /* @__PURE__ */ new Map();
|
|
35225
|
+
let files;
|
|
35226
|
+
try {
|
|
35227
|
+
files = list(dir);
|
|
35228
|
+
} catch {
|
|
35229
|
+
return held;
|
|
35230
|
+
}
|
|
35231
|
+
for (const file of files) {
|
|
35232
|
+
if (!/^\d+\.json$/.test(file)) continue;
|
|
35233
|
+
let row;
|
|
35234
|
+
try {
|
|
35235
|
+
row = JSON.parse(read(join7(dir, file)));
|
|
35236
|
+
} catch {
|
|
35237
|
+
continue;
|
|
35238
|
+
}
|
|
35239
|
+
const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
|
|
35240
|
+
if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
|
|
35241
|
+
if (!holds(pid, row.procStart, check)) continue;
|
|
35242
|
+
const slot = sessionSlot(row.sessionId);
|
|
35243
|
+
held.set(slot, [...held.get(slot) ?? [], { pid, proven: proven(pid, row.procStart, check) }]);
|
|
35244
|
+
}
|
|
35245
|
+
return held;
|
|
35246
|
+
}
|
|
35247
|
+
function psParent(pid) {
|
|
35248
|
+
try {
|
|
35249
|
+
const out = execFileSync3("ps", ["-o", "ppid=", "-p", String(pid)], { encoding: "utf8", timeout: 2e3 }).trim();
|
|
35250
|
+
const n = Number(out);
|
|
35251
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
35252
|
+
} catch {
|
|
35253
|
+
return null;
|
|
35254
|
+
}
|
|
35255
|
+
}
|
|
35256
|
+
function descendsFrom(pid, ancestor, parent = psParent) {
|
|
35257
|
+
let at = pid;
|
|
35258
|
+
for (let i = 0; i < 32 && at !== null && at > 1; i += 1) {
|
|
35259
|
+
if (at === ancestor) return true;
|
|
35260
|
+
at = parent(at);
|
|
35261
|
+
}
|
|
35262
|
+
return false;
|
|
35263
|
+
}
|
|
35096
35264
|
function localHolders(deps = {}) {
|
|
35097
|
-
const dir = deps.dir ??
|
|
35265
|
+
const dir = deps.dir ?? join7(process.env.CLAUDE_CONFIG_DIR?.trim() || join7(homedir7(), ".claude"), "sessions");
|
|
35098
35266
|
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
35099
|
-
const read = deps.read ?? ((f) =>
|
|
35267
|
+
const read = deps.read ?? ((f) => readFileSync5(f, "utf8"));
|
|
35100
35268
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
35101
35269
|
const held = /* @__PURE__ */ new Map();
|
|
35102
35270
|
let files;
|
|
@@ -35109,7 +35277,7 @@ function localHolders(deps = {}) {
|
|
|
35109
35277
|
if (!/^\d+\.json$/.test(file)) continue;
|
|
35110
35278
|
let row;
|
|
35111
35279
|
try {
|
|
35112
|
-
row = JSON.parse(read(
|
|
35280
|
+
row = JSON.parse(read(join7(dir, file)));
|
|
35113
35281
|
} catch {
|
|
35114
35282
|
continue;
|
|
35115
35283
|
}
|
|
@@ -35122,10 +35290,69 @@ function localHolders(deps = {}) {
|
|
|
35122
35290
|
return held;
|
|
35123
35291
|
}
|
|
35124
35292
|
|
|
35293
|
+
// src/end.ts
|
|
35294
|
+
var GRACE_MS = 5e3;
|
|
35295
|
+
async function endTerminal(pid, slot, deps) {
|
|
35296
|
+
try {
|
|
35297
|
+
deps.kill(pid, "SIGTERM");
|
|
35298
|
+
} catch (e) {
|
|
35299
|
+
return { ok: false, kind: "terminal", pid, why: `SIGTERM refused: ${e.message}`.slice(0, 200) };
|
|
35300
|
+
}
|
|
35301
|
+
const grace = deps.graceMs ?? GRACE_MS;
|
|
35302
|
+
for (let waited = 0; waited < grace; waited += 250) {
|
|
35303
|
+
if (!deps.alive(pid)) {
|
|
35304
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGTERM" })}`);
|
|
35305
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGTERM" };
|
|
35306
|
+
}
|
|
35307
|
+
await deps.sleep(250);
|
|
35308
|
+
}
|
|
35309
|
+
try {
|
|
35310
|
+
deps.kill(pid, "SIGKILL");
|
|
35311
|
+
} catch (e) {
|
|
35312
|
+
return { ok: false, kind: "terminal", pid, why: `SIGKILL refused: ${e.message}`.slice(0, 200) };
|
|
35313
|
+
}
|
|
35314
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, pid, kind: "terminal", signal: "SIGKILL" })}`);
|
|
35315
|
+
return { ok: true, kind: "terminal", pid, signal: "SIGKILL" };
|
|
35316
|
+
}
|
|
35317
|
+
async function endSession(end, deps) {
|
|
35318
|
+
const slots = deps.slotsFor(end.agent);
|
|
35319
|
+
if (!slots.length) return { ok: false, why: "not_on_this_host" };
|
|
35320
|
+
let ended2 = null;
|
|
35321
|
+
if (end.which === "harness" || end.which === "any") {
|
|
35322
|
+
for (const slot of slots) {
|
|
35323
|
+
const run = deps.ownRun(slot);
|
|
35324
|
+
if (!run) continue;
|
|
35325
|
+
run.stop();
|
|
35326
|
+
deps.log(`session_ended ${JSON.stringify({ sessionId: slot, kind: "harness", signal: "stop" })}`);
|
|
35327
|
+
ended2 = { ok: true, kind: "harness", signal: "stop" };
|
|
35328
|
+
}
|
|
35329
|
+
if (end.which === "harness" && !ended2) return { ok: false, kind: "harness", why: "no_harness_copy_here" };
|
|
35330
|
+
}
|
|
35331
|
+
if (end.which === "terminal" || end.which === "any") {
|
|
35332
|
+
let terminal = null;
|
|
35333
|
+
let unproven = null;
|
|
35334
|
+
for (const slot of slots) {
|
|
35335
|
+
for (const { pid, proven: proven2 } of deps.pidsFor(slot).filter((p) => !deps.isOwn(p.pid))) {
|
|
35336
|
+
if (!proven2) {
|
|
35337
|
+
deps.log(`session_end_refused ${JSON.stringify({ sessionId: slot, pid, why: "no_proc_start" })}`);
|
|
35338
|
+
unproven = { ok: false, kind: "terminal", pid, why: "no_proc_start" };
|
|
35339
|
+
continue;
|
|
35340
|
+
}
|
|
35341
|
+
terminal = await endTerminal(pid, slot, deps);
|
|
35342
|
+
if (!terminal.ok) return terminal;
|
|
35343
|
+
}
|
|
35344
|
+
}
|
|
35345
|
+
if (terminal) return terminal;
|
|
35346
|
+
if (unproven) return unproven;
|
|
35347
|
+
if (end.which === "terminal") return { ok: false, kind: "terminal", why: "no_terminal_copy_here" };
|
|
35348
|
+
}
|
|
35349
|
+
return ended2 ?? { ok: false, why: "nothing_running_here" };
|
|
35350
|
+
}
|
|
35351
|
+
|
|
35125
35352
|
// src/owner.ts
|
|
35126
35353
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync4 } from "fs";
|
|
35127
35354
|
import { homedir as homedir8 } from "os";
|
|
35128
|
-
import { join as
|
|
35355
|
+
import { join as join8 } from "path";
|
|
35129
35356
|
var HEAD_BYTES = 64e3;
|
|
35130
35357
|
function readHead(file) {
|
|
35131
35358
|
const fd = openSync2(file, "r");
|
|
@@ -35141,7 +35368,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35141
35368
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
35142
35369
|
if (!m) return null;
|
|
35143
35370
|
const prefix = m[1];
|
|
35144
|
-
const root = deps.projectsDir ??
|
|
35371
|
+
const root = deps.projectsDir ?? join8(homedir8(), ".claude", "projects");
|
|
35145
35372
|
const list = deps.list ?? ((dir) => readdirSync4(dir));
|
|
35146
35373
|
const head = deps.head ?? readHead;
|
|
35147
35374
|
let folders;
|
|
@@ -35153,7 +35380,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35153
35380
|
for (const folder of folders) {
|
|
35154
35381
|
let files;
|
|
35155
35382
|
try {
|
|
35156
|
-
files = list(
|
|
35383
|
+
files = list(join8(root, folder));
|
|
35157
35384
|
} catch {
|
|
35158
35385
|
continue;
|
|
35159
35386
|
}
|
|
@@ -35161,7 +35388,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35161
35388
|
if (!file) continue;
|
|
35162
35389
|
let text;
|
|
35163
35390
|
try {
|
|
35164
|
-
text = head(
|
|
35391
|
+
text = head(join8(root, folder, file));
|
|
35165
35392
|
} catch {
|
|
35166
35393
|
continue;
|
|
35167
35394
|
}
|
|
@@ -35181,7 +35408,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35181
35408
|
// src/update.ts
|
|
35182
35409
|
import { execFile } from "child_process";
|
|
35183
35410
|
import { createRequire } from "module";
|
|
35184
|
-
import { dirname as dirname3, join as
|
|
35411
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
35185
35412
|
import { promisify } from "util";
|
|
35186
35413
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
35187
35414
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -35208,7 +35435,7 @@ async function latestVersion() {
|
|
|
35208
35435
|
}
|
|
35209
35436
|
async function install(version) {
|
|
35210
35437
|
const bin = dirname3(process.execPath);
|
|
35211
|
-
await promisify(execFile)(
|
|
35438
|
+
await promisify(execFile)(join9(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
35212
35439
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
35213
35440
|
timeout: 5 * 6e4
|
|
35214
35441
|
});
|
|
@@ -35259,7 +35486,7 @@ function selfUpdater(deps) {
|
|
|
35259
35486
|
}
|
|
35260
35487
|
|
|
35261
35488
|
// src/host.ts
|
|
35262
|
-
var HOST_FILE =
|
|
35489
|
+
var HOST_FILE = join10(homedir9(), ".paigy", "host.json");
|
|
35263
35490
|
function wakeHarness(slot, saved, claudeTranscript) {
|
|
35264
35491
|
if (slot === "Desktop") return void 0;
|
|
35265
35492
|
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
@@ -35425,6 +35652,31 @@ function startHost(opts) {
|
|
|
35425
35652
|
void refreshIdentities();
|
|
35426
35653
|
opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
|
|
35427
35654
|
}
|
|
35655
|
+
const ends = await claimSessionEnds(asHost).catch(() => []);
|
|
35656
|
+
for (const end of ends) {
|
|
35657
|
+
const result = await endSession(end, {
|
|
35658
|
+
slotsFor: (agent2) => listSlots().filter((slot) => slot !== "Desktop" && slotIdentity(slot).tokenId === agent2),
|
|
35659
|
+
pidsFor: (slot) => localHolderPids().get(slot) ?? [],
|
|
35660
|
+
ownRun: (slot) => runs.get(runKey(slot))?.run ?? null,
|
|
35661
|
+
isOwn: (pid) => descendsFrom(pid, process.pid),
|
|
35662
|
+
kill: (pid, signal) => {
|
|
35663
|
+
process.kill(pid, signal);
|
|
35664
|
+
},
|
|
35665
|
+
alive: (pid) => {
|
|
35666
|
+
try {
|
|
35667
|
+
process.kill(pid, 0);
|
|
35668
|
+
return true;
|
|
35669
|
+
} catch (e) {
|
|
35670
|
+
return e.code === "EPERM";
|
|
35671
|
+
}
|
|
35672
|
+
},
|
|
35673
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
35674
|
+
log: opts.log
|
|
35675
|
+
}).catch((e) => ({ ok: false, why: `end_threw: ${e.message}`.slice(0, 200) }));
|
|
35676
|
+
opts.log(result.ok ? `\u25A0 ended ${end.which} copy of ${end.agent.slice(0, 8)}` : `\u25CC end of ${end.agent.slice(0, 8)} not done here: ${result.why}`);
|
|
35677
|
+
await finishSessionEnd(end.endId, result, asHost).catch(() => {
|
|
35678
|
+
});
|
|
35679
|
+
}
|
|
35428
35680
|
}
|
|
35429
35681
|
async function presenceMap() {
|
|
35430
35682
|
const presence = /* @__PURE__ */ new Map();
|
|
@@ -35449,6 +35701,14 @@ function startHost(opts) {
|
|
|
35449
35701
|
const key = runKey(slot);
|
|
35450
35702
|
if (slot === "Desktop" || isRevoked(slot)) return;
|
|
35451
35703
|
const live = runs.get(key);
|
|
35704
|
+
if (live) {
|
|
35705
|
+
const theirs = (localHolderPids().get(slot) ?? []).map((p) => p.pid).filter((pid) => !descendsFrom(pid, process.pid));
|
|
35706
|
+
if (theirs.length) {
|
|
35707
|
+
opts.log(`session_stood_back ${JSON.stringify({ sessionId: slot, livePid: theirs[0] })} \u2014 a terminal runs this conversation; stopping this host's copy`);
|
|
35708
|
+
live.run.stop();
|
|
35709
|
+
return;
|
|
35710
|
+
}
|
|
35711
|
+
}
|
|
35452
35712
|
const tokenId = slotIdentity(slot).tokenId;
|
|
35453
35713
|
const seenByServer = tokenId ? presence.get(tokenId) : void 0;
|
|
35454
35714
|
const holder = holders.get(slot);
|
|
@@ -35779,9 +36039,9 @@ async function pairThisMachine() {
|
|
|
35779
36039
|
}
|
|
35780
36040
|
}
|
|
35781
36041
|
function forwardCodexThreadId() {
|
|
35782
|
-
const cfg =
|
|
36042
|
+
const cfg = join11(process.env.CODEX_HOME || join11(homedir10(), ".codex"), "config.toml");
|
|
35783
36043
|
if (!existsSync5(cfg)) return;
|
|
35784
|
-
const text =
|
|
36044
|
+
const text = readFileSync7(cfg, "utf8");
|
|
35785
36045
|
const head = "[mcp_servers.paigy]\n";
|
|
35786
36046
|
const at = text.indexOf(head);
|
|
35787
36047
|
if (at < 0) return;
|
|
@@ -35790,7 +36050,8 @@ function forwardCodexThreadId() {
|
|
|
35790
36050
|
return n < 0 ? text.length : n;
|
|
35791
36051
|
})());
|
|
35792
36052
|
if (/^\s*env_vars\s*=/m.test(block2)) return;
|
|
35793
|
-
writeFileSync5(cfg, text.slice(0, at + head.length) +
|
|
36053
|
+
writeFileSync5(cfg, text.slice(0, at + head.length) + `env_vars = [${CODEX_ENV.map((name) => JSON.stringify(name)).join(", ")}]
|
|
36054
|
+
` + text.slice(at + head.length));
|
|
35794
36055
|
}
|
|
35795
36056
|
async function main() {
|
|
35796
36057
|
const parsed = parseArgs(process.argv.slice(2));
|
|
@@ -35808,6 +36069,7 @@ async function main() {
|
|
|
35808
36069
|
console.log(" service install the host as a login service (macOS)");
|
|
35809
36070
|
console.log(" enable-tools allowlist Paigy's tools in Claude Code so they don't prompt each time");
|
|
35810
36071
|
console.log(" hatch NAME mint a sibling identity from this machine's credential");
|
|
36072
|
+
console.log(' launch NAME "brief" start a helper session with its own identity and its brief as its first Goal [--under GOAL]');
|
|
35811
36073
|
console.log(" handoff pin this terminal's identity here so your phone can resume it");
|
|
35812
36074
|
console.log(" triage sweep your whole note queue with a model on THIS machine and");
|
|
35813
36075
|
console.log(" send grouped suggestions to your phone (close / stale / assign)");
|
|
@@ -35881,7 +36143,7 @@ async function main() {
|
|
|
35881
36143
|
const wsDeps = { file: workspacesFile() };
|
|
35882
36144
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
35883
36145
|
if (!listWorkspaces(wsDeps).length) {
|
|
35884
|
-
const guess =
|
|
36146
|
+
const guess = join11(homedir10(), "projects");
|
|
35885
36147
|
if (existsSync5(guess)) {
|
|
35886
36148
|
addWorkspace(guess, wsDeps);
|
|
35887
36149
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -35913,9 +36175,9 @@ async function main() {
|
|
|
35913
36175
|
}
|
|
35914
36176
|
}
|
|
35915
36177
|
try {
|
|
35916
|
-
const settings =
|
|
36178
|
+
const settings = join11(homedir10(), ".claude", "settings.json");
|
|
35917
36179
|
if (which("claude") && existsSync5(settings)) {
|
|
35918
|
-
const cfg = JSON.parse(
|
|
36180
|
+
const cfg = JSON.parse(readFileSync7(settings, "utf8"));
|
|
35919
36181
|
if (!cfg["statusLine"]) {
|
|
35920
36182
|
cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
|
|
35921
36183
|
writeFileSync5(settings, JSON.stringify(cfg, null, 2));
|
|
@@ -35971,13 +36233,13 @@ async function main() {
|
|
|
35971
36233
|
<key>SoftResourceLimits</key><dict>
|
|
35972
36234
|
<key>NumberOfFiles</key><integer>65536</integer>
|
|
35973
36235
|
</dict>
|
|
35974
|
-
<key>StandardOutPath</key><string>${
|
|
35975
|
-
<key>StandardErrorPath</key><string>${
|
|
36236
|
+
<key>StandardOutPath</key><string>${join11(homedir10(), ".paigy", "host.log")}</string>
|
|
36237
|
+
<key>StandardErrorPath</key><string>${join11(homedir10(), ".paigy", "host.log")}</string>
|
|
35976
36238
|
</dict></plist>
|
|
35977
36239
|
`;
|
|
35978
|
-
const dir =
|
|
36240
|
+
const dir = join11(homedir10(), "Library", "LaunchAgents");
|
|
35979
36241
|
mkdirSync4(dir, { recursive: true });
|
|
35980
|
-
const path =
|
|
36242
|
+
const path = join11(dir, `${SERVICE_LABEL}.plist`);
|
|
35981
36243
|
writeFileSync5(path, plist);
|
|
35982
36244
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
35983
36245
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
@@ -36022,7 +36284,7 @@ async function main() {
|
|
|
36022
36284
|
return;
|
|
36023
36285
|
}
|
|
36024
36286
|
if (parsed.hatch) {
|
|
36025
|
-
overrideToken(
|
|
36287
|
+
overrideToken(readSlot("Desktop") || null);
|
|
36026
36288
|
const minted = await hatch(parsed.hatch, parsed.voice ?? null);
|
|
36027
36289
|
saveToken({ access_token: minted.token, name: minted.name ?? parsed.hatch, device: null }, parsed.slot ?? minted.name ?? parsed.hatch);
|
|
36028
36290
|
if (!minted.name) {
|
|
@@ -36030,8 +36292,7 @@ async function main() {
|
|
|
36030
36292
|
return;
|
|
36031
36293
|
}
|
|
36032
36294
|
console.log(`\u2713 hatched "${minted.name}" \u2014 it's on your phone's Agents screen now.`);
|
|
36033
|
-
console.log(`
|
|
36034
|
-
console.log(` run with it: paigy-harness --identity "${minted.name}" --harness codex "\u2026"`);
|
|
36295
|
+
console.log(` start a helper with its own session instead: paigy-harness launch "<name>" "<brief>"`);
|
|
36035
36296
|
return;
|
|
36036
36297
|
}
|
|
36037
36298
|
if (parsed.host) {
|
|
@@ -36049,21 +36310,27 @@ async function main() {
|
|
|
36049
36310
|
});
|
|
36050
36311
|
return;
|
|
36051
36312
|
}
|
|
36052
|
-
let identityToken;
|
|
36053
|
-
if (parsed.identity) {
|
|
36054
|
-
const slot = readToken(parsed.identity);
|
|
36055
|
-
if (!slot) {
|
|
36056
|
-
console.error(`No slot for "${parsed.identity}" \u2014 hatch it first: paigy-harness hatch "${parsed.identity}"`);
|
|
36057
|
-
process.exit(1);
|
|
36058
|
-
}
|
|
36059
|
-
identityToken = slot;
|
|
36060
|
-
}
|
|
36061
36313
|
const state = detectAll().find((a) => a.name === parsed.harness);
|
|
36062
36314
|
if (state && state.status !== "ready") {
|
|
36063
36315
|
const hint = state.hint ?? "setup incomplete";
|
|
36064
36316
|
console.error(`\u270B ${state.label}: ${hint}`);
|
|
36065
36317
|
process.exit(1);
|
|
36066
36318
|
}
|
|
36319
|
+
let launched;
|
|
36320
|
+
if (parsed.launch) {
|
|
36321
|
+
overrideToken(readSlot("Desktop") || null);
|
|
36322
|
+
const session = randomUUID7();
|
|
36323
|
+
const minted = await hatch(parsed.launch, parsed.voice ?? null, {
|
|
36324
|
+
reach: reachAs(session),
|
|
36325
|
+
brief: parsed.prompt,
|
|
36326
|
+
...parsed.under ? { parentGoalId: parsed.under } : {}
|
|
36327
|
+
}).catch((e) => {
|
|
36328
|
+
console.error(`\u270B the helper was not launched: ${e.message}`);
|
|
36329
|
+
process.exit(1);
|
|
36330
|
+
}).finally(() => overrideToken(null));
|
|
36331
|
+
launched = { token: minted.token, session, ...minted.goalId ? { goalId: minted.goalId } : {} };
|
|
36332
|
+
console.log(`\u2713 launched "${minted.name ?? parsed.launch}"${minted.name ? "" : " (unnamed: you already have an agent with that name)"}, on its own session; its Goal is ${minted.goalId ?? "missing"}`);
|
|
36333
|
+
}
|
|
36067
36334
|
const askTerminal = async (promptText) => {
|
|
36068
36335
|
if (parsed.graceSeconds <= 0 || !process.stdin.isTTY) return null;
|
|
36069
36336
|
const readline = await import("readline/promises");
|
|
@@ -36084,9 +36351,10 @@ async function main() {
|
|
|
36084
36351
|
harness: parsed.harness,
|
|
36085
36352
|
cwd: parsed.cwd,
|
|
36086
36353
|
mode: parsed.mode,
|
|
36087
|
-
|
|
36354
|
+
// hatch already admitted this brief; the pump must not admit it again after completion.
|
|
36355
|
+
prompt: launched ? "" : parsed.prompt,
|
|
36088
36356
|
...state?.bin ? { bin: state.bin } : {},
|
|
36089
|
-
...
|
|
36357
|
+
...launched ? { token: launched.token, session: launched.session } : {},
|
|
36090
36358
|
localAsk: {
|
|
36091
36359
|
permission: async (summary) => {
|
|
36092
36360
|
const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);
|
|
@@ -36098,7 +36366,7 @@ async function main() {
|
|
|
36098
36366
|
});
|
|
36099
36367
|
const beat2 = async () => {
|
|
36100
36368
|
try {
|
|
36101
|
-
await heartbeat();
|
|
36369
|
+
await heartbeat(void 0, launched ? { token: launched.token } : {});
|
|
36102
36370
|
signedOut = false;
|
|
36103
36371
|
} catch (e) {
|
|
36104
36372
|
if (!isUnauthorized(e) || signedOut) return;
|