@paigy/harness 0.3.31 → 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 +181 -110
- package/dist/main.js +113 -63
- 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();
|
|
@@ -24204,7 +24212,7 @@ async function subscribeWake(onNudge, opts = {}) {
|
|
|
24204
24212
|
}
|
|
24205
24213
|
};
|
|
24206
24214
|
}
|
|
24207
|
-
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;
|
|
24208
24216
|
var init_dist = __esm({
|
|
24209
24217
|
"../../packages/sdk/dist/index.js"() {
|
|
24210
24218
|
"use strict";
|
|
@@ -24217,6 +24225,7 @@ var init_dist = __esm({
|
|
|
24217
24225
|
init_zod();
|
|
24218
24226
|
import_undici = __toESM(require_undici(), 1);
|
|
24219
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"];
|
|
24220
24229
|
BACKEND_URL = process.env.PAIGY_BACKEND_URL ?? "https://paigy.ai";
|
|
24221
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.`;
|
|
24222
24231
|
PROXY_ENV = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
@@ -24620,15 +24629,13 @@ var init_dist = __esm({
|
|
|
24620
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."
|
|
24621
24630
|
),
|
|
24622
24631
|
options: external_exports.array(OptionInputSchema).min(1).max(6).optional(),
|
|
24623
|
-
//
|
|
24624
|
-
//
|
|
24625
|
-
//
|
|
24626
|
-
//
|
|
24627
|
-
//
|
|
24628
|
-
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
24629
|
-
// not: whether its own options exclude each other.
|
|
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.
|
|
24630
24637
|
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
24631
|
-
|
|
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.`
|
|
24632
24639
|
),
|
|
24633
24640
|
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
24634
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."
|
|
@@ -24641,7 +24648,7 @@ var init_dist = __esm({
|
|
|
24641
24648
|
}).strict();
|
|
24642
24649
|
ContactSchema = external_exports.union([StartContactSchema, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
24643
24650
|
CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
|
|
24644
|
-
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')
|
|
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.";
|
|
24645
24652
|
CreateGoalSchema = external_exports.object({
|
|
24646
24653
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
24647
24654
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -24664,7 +24671,7 @@ var init_dist = __esm({
|
|
|
24664
24671
|
CreateGoalToolSchema = CreateGoalSchema.extend({
|
|
24665
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.")
|
|
24666
24673
|
}).strict();
|
|
24667
|
-
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.';
|
|
24668
24675
|
UpdateGoalSchema = external_exports.object({
|
|
24669
24676
|
revision: external_exports.number().int().positive(),
|
|
24670
24677
|
changes: external_exports.object({
|
|
@@ -24700,12 +24707,13 @@ var init_dist = __esm({
|
|
|
24700
24707
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
24701
24708
|
diagnose: external_exports.boolean().optional()
|
|
24702
24709
|
}).strict();
|
|
24703
|
-
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. 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.
|
|
24704
|
-
UPDATE_GOAL_DESCRIPTION = `Update
|
|
24705
|
-
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).";
|
|
24706
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.";
|
|
24707
24714
|
CheckRepliesSchema = external_exports.object({}).strict();
|
|
24708
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) },
|
|
24709
24717
|
{ name: "contact", description: CONTACT_DESCRIPTION, inputSchema: CONTACT_SCHEMA },
|
|
24710
24718
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION, inputSchema: mcpInputSchema(CheckRepliesSchema) },
|
|
24711
24719
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(CreateGoalToolSchema) },
|
|
@@ -24715,7 +24723,7 @@ var init_dist = __esm({
|
|
|
24715
24723
|
];
|
|
24716
24724
|
AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
|
|
24717
24725
|
LIVE_MS = 3 * 6e4;
|
|
24718
|
-
WORKING_MS =
|
|
24726
|
+
WORKING_MS = 60 * 6e4;
|
|
24719
24727
|
ContextSchema = external_exports.object({
|
|
24720
24728
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
24721
24729
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -25067,7 +25075,13 @@ var init_dist = __esm({
|
|
|
25067
25075
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
25068
25076
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
25069
25077
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
25070
|
-
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()
|
|
25071
25085
|
});
|
|
25072
25086
|
CLAIM_STALE_MS = 30 * 6e4;
|
|
25073
25087
|
InboxItemSchema = external_exports.object({
|
|
@@ -31076,9 +31090,9 @@ init_catalog();
|
|
|
31076
31090
|
|
|
31077
31091
|
// src/cli-args.ts
|
|
31078
31092
|
var TRIAGE_VIA = ["ollama", "claude", "codex", "agy"];
|
|
31079
|
-
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";
|
|
31080
31094
|
function parseArgs(argv) {
|
|
31081
|
-
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 };
|
|
31082
31096
|
const words2 = [];
|
|
31083
31097
|
for (let i = 0; i < argv.length; i++) {
|
|
31084
31098
|
const arg = argv[i];
|
|
@@ -31149,9 +31163,13 @@ function parseArgs(argv) {
|
|
|
31149
31163
|
} else if (arg === "--slot") {
|
|
31150
31164
|
args.slot = next() ?? null;
|
|
31151
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") {
|
|
31152
31168
|
const v = next();
|
|
31153
|
-
if (!v) return { error:
|
|
31154
|
-
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;
|
|
31155
31173
|
} else if (arg?.startsWith("--")) {
|
|
31156
31174
|
return { error: `unknown flag ${arg}` };
|
|
31157
31175
|
} else if (arg) {
|
|
@@ -31160,6 +31178,7 @@ function parseArgs(argv) {
|
|
|
31160
31178
|
}
|
|
31161
31179
|
args.prompt = words2.join(" ");
|
|
31162
31180
|
if (args.help) return args;
|
|
31181
|
+
if (args.launch && !args.prompt) return { error: 'launch needs a brief: paigy-harness launch "Deploy check" "brief"' };
|
|
31163
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)" };
|
|
31164
31183
|
if ((args.yesClose || args.yesStale || args.yesAssign) && !args.apply)
|
|
31165
31184
|
return { error: "--yes-close/--yes-stale/--yes-assign only apply with --apply" };
|
|
@@ -31192,20 +31211,23 @@ function onEnter(input, onLine) {
|
|
|
31192
31211
|
// src/cli.ts
|
|
31193
31212
|
init_dist();
|
|
31194
31213
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
31214
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
31195
31215
|
import { hostname } from "os";
|
|
31196
|
-
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";
|
|
31197
31217
|
import { execFile as execFile2, execSync } from "child_process";
|
|
31198
31218
|
import { homedir as homedir10 } from "os";
|
|
31199
|
-
import { join as
|
|
31219
|
+
import { join as join11 } from "path";
|
|
31200
31220
|
|
|
31201
31221
|
// src/run.ts
|
|
31202
31222
|
init_dist();
|
|
31223
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
31203
31224
|
|
|
31204
31225
|
// src/harness/session.ts
|
|
31226
|
+
init_dist();
|
|
31205
31227
|
import { spawn } from "child_process";
|
|
31206
|
-
import { existsSync as existsSync3 } from "fs";
|
|
31228
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
31207
31229
|
import { homedir as homedir4 } from "os";
|
|
31208
|
-
import { resolve, delimiter as delimiter2 } from "path";
|
|
31230
|
+
import { resolve, delimiter as delimiter2, join as join5 } from "path";
|
|
31209
31231
|
|
|
31210
31232
|
// src/harness/acp.ts
|
|
31211
31233
|
var none = { events: [], writes: [] };
|
|
@@ -31579,6 +31601,21 @@ function startSession(opts) {
|
|
|
31579
31601
|
if (!existsSync3(cwd)) {
|
|
31580
31602
|
queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
|
|
31581
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
|
+
}
|
|
31582
31619
|
const child = (opts.spawnFn ?? spawn)(opts.bin ?? ADAPTER_BIN[opts.harness], [], {
|
|
31583
31620
|
cwd,
|
|
31584
31621
|
// The adapter shells out to its vendor CLI (`claude`, `codex`), and a GUI- or
|
|
@@ -31589,7 +31626,9 @@ function startSession(opts) {
|
|
|
31589
31626
|
...process.env,
|
|
31590
31627
|
PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
|
|
31591
31628
|
...opts.token ? { PAIGY_TOKEN: opts.token } : {},
|
|
31592
|
-
...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 } : {},
|
|
31593
31632
|
// A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
|
|
31594
31633
|
// bridge's pump typing answers in, so nothing inside it may start a second delivery path.
|
|
31595
31634
|
// Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
|
|
@@ -33017,15 +33056,13 @@ var AskInputSchema2 = external_exports.object({
|
|
|
33017
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."
|
|
33018
33057
|
),
|
|
33019
33058
|
options: external_exports.array(OptionInputSchema2).min(1).max(6).optional(),
|
|
33020
|
-
//
|
|
33021
|
-
//
|
|
33022
|
-
//
|
|
33023
|
-
//
|
|
33024
|
-
//
|
|
33025
|
-
// could not answer as the ticks he wanted. "many" is the one shape an agent knows and the read does
|
|
33026
|
-
// not: whether its own options exclude each other.
|
|
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.
|
|
33027
33064
|
select: external_exports.enum(["one", "many"]).optional().describe(
|
|
33028
|
-
|
|
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.`
|
|
33029
33066
|
),
|
|
33030
33067
|
answers: external_exports.string().trim().regex(/^[0-9a-fA-F-]{8,36}$/).optional().describe(
|
|
33031
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."
|
|
@@ -33038,7 +33075,7 @@ var StartContactSchema2 = external_exports.object({
|
|
|
33038
33075
|
}).strict();
|
|
33039
33076
|
var ContactSchema2 = external_exports.union([StartContactSchema2, external_exports.object({ deliveryId: external_exports.string().uuid() }).strict()]);
|
|
33040
33077
|
var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
|
|
33041
|
-
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')
|
|
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.";
|
|
33042
33079
|
var CreateGoalSchema2 = external_exports.object({
|
|
33043
33080
|
outcome: external_exports.string().trim().min(1).max(1e4),
|
|
33044
33081
|
/** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
|
|
@@ -33061,7 +33098,7 @@ var CreateGoalSchema2 = external_exports.object({
|
|
|
33061
33098
|
var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
|
|
33062
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.")
|
|
33063
33100
|
}).strict();
|
|
33064
|
-
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.';
|
|
33065
33102
|
var UpdateGoalSchema2 = external_exports.object({
|
|
33066
33103
|
revision: external_exports.number().int().positive(),
|
|
33067
33104
|
changes: external_exports.object({
|
|
@@ -33097,12 +33134,13 @@ var GetGoalSchema2 = external_exports.object({
|
|
|
33097
33134
|
* between two of them named. Off by default; `docs/model/goal/diagnose-design.md`. */
|
|
33098
33135
|
diagnose: external_exports.boolean().optional()
|
|
33099
33136
|
}).strict();
|
|
33100
|
-
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. 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.
|
|
33101
|
-
var UPDATE_GOAL_DESCRIPTION2 = `Update
|
|
33102
|
-
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).";
|
|
33103
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.";
|
|
33104
33141
|
var CheckRepliesSchema2 = external_exports.object({}).strict();
|
|
33105
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) },
|
|
33106
33144
|
{ name: "contact", description: CONTACT_DESCRIPTION2, inputSchema: CONTACT_SCHEMA2 },
|
|
33107
33145
|
{ name: "check_replies", description: CHECK_REPLIES_DESCRIPTION2, inputSchema: mcpInputSchema2(CheckRepliesSchema2) },
|
|
33108
33146
|
{ name: "create_goal", description: CREATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(CreateGoalToolSchema2) },
|
|
@@ -33127,7 +33165,7 @@ function entryWords(entry) {
|
|
|
33127
33165
|
return entry.sources.map((source) => source.text).join("\n");
|
|
33128
33166
|
}
|
|
33129
33167
|
var LIVE_MS2 = 3 * 6e4;
|
|
33130
|
-
var WORKING_MS2 =
|
|
33168
|
+
var WORKING_MS2 = 60 * 6e4;
|
|
33131
33169
|
var ContextSchema2 = external_exports.object({
|
|
33132
33170
|
title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
|
|
33133
33171
|
description: external_exports.array(external_exports.string().min(1)).describe(
|
|
@@ -33479,7 +33517,13 @@ var AgendaTurnSchema2 = external_exports.object({
|
|
|
33479
33517
|
/** Whether the walk WAITS for an answer before moving on. Absent = derived as today
|
|
33480
33518
|
* (a question blocks, context flows). blocking:false on a question = ask and move
|
|
33481
33519
|
* on, the claim stays pending; blocking:true on context = hold for a reply. */
|
|
33482
|
-
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()
|
|
33483
33527
|
});
|
|
33484
33528
|
var CLAIM_STALE_MS2 = 30 * 6e4;
|
|
33485
33529
|
var InboxItemSchema2 = external_exports.object({
|
|
@@ -34519,9 +34563,13 @@ async function finish(result, state, rail, opts) {
|
|
|
34519
34563
|
const progress = result?.trim().slice(0, 1e4) || void 0;
|
|
34520
34564
|
let revision = goal.revision;
|
|
34521
34565
|
try {
|
|
34522
|
-
const held = await rail.
|
|
34566
|
+
const held = await rail.read(goal.id);
|
|
34523
34567
|
revision = held.revision ?? revision;
|
|
34524
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
|
+
}
|
|
34525
34573
|
if (!held.openDecisionNeedIds?.length && !owed) {
|
|
34526
34574
|
await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
|
|
34527
34575
|
state.goal = void 0;
|
|
@@ -34610,11 +34658,16 @@ function beat() {
|
|
|
34610
34658
|
}
|
|
34611
34659
|
var BEAT_REPORT_MS = 10 * 6e4;
|
|
34612
34660
|
var ended = (state) => state === "done" || state === "cancelled";
|
|
34613
|
-
function unheard(brief, state, since = null, after = -1) {
|
|
34661
|
+
function unheard(brief, state, since = null, after = -1, reviewSince) {
|
|
34614
34662
|
const seen = state.seen ??= /* @__PURE__ */ new Set();
|
|
34615
34663
|
const words2 = [];
|
|
34664
|
+
const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
|
|
34616
34665
|
for (const [i, e] of (brief.entries ?? []).entries()) {
|
|
34617
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
|
+
}
|
|
34618
34671
|
if (i <= after) {
|
|
34619
34672
|
seen.add(e.entryId);
|
|
34620
34673
|
continue;
|
|
@@ -34625,6 +34678,12 @@ function unheard(brief, state, since = null, after = -1) {
|
|
|
34625
34678
|
const text = entryWords(e).trim();
|
|
34626
34679
|
if (text) words2.push(text);
|
|
34627
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
|
+
}
|
|
34628
34687
|
return words2;
|
|
34629
34688
|
}
|
|
34630
34689
|
function sinceTheWork(brief) {
|
|
@@ -34693,9 +34752,9 @@ async function gather(state, rail, held) {
|
|
|
34693
34752
|
}
|
|
34694
34753
|
}
|
|
34695
34754
|
read.add(goalId);
|
|
34696
|
-
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 };
|
|
34697
34756
|
if (found.via === "assigned") announced.add(goalId);
|
|
34698
|
-
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 } : {} });
|
|
34699
34758
|
}
|
|
34700
34759
|
for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
|
|
34701
34760
|
return { heard, listed };
|
|
@@ -34781,8 +34840,8 @@ async function acknowledge(goalId, brief, state, rail, log) {
|
|
|
34781
34840
|
if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
|
|
34782
34841
|
acked.set(goalId, brief.revision);
|
|
34783
34842
|
try {
|
|
34784
|
-
|
|
34785
|
-
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." });
|
|
34786
34845
|
acked.set(goalId, done.revision);
|
|
34787
34846
|
if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
|
|
34788
34847
|
} catch (error) {
|
|
@@ -34819,25 +34878,26 @@ async function look(session, state, rail, opts) {
|
|
|
34819
34878
|
brief2 = await rail.claim();
|
|
34820
34879
|
if (!brief2.goalId || brief2.revision === void 0) {
|
|
34821
34880
|
state.goal = { id: admitted.goalId, revision: admitted.revision };
|
|
34822
|
-
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"));
|
|
34823
34882
|
return;
|
|
34824
34883
|
}
|
|
34825
34884
|
} else {
|
|
34826
34885
|
return;
|
|
34827
34886
|
}
|
|
34828
34887
|
}
|
|
34829
|
-
unheard(brief2, state);
|
|
34830
|
-
if (
|
|
34831
|
-
|
|
34888
|
+
const pendingWords = brief2.reviewSince ? unheard(brief2, state, null, -1, brief2.reviewSince) : null;
|
|
34889
|
+
if (!pendingWords) unheard(brief2, state);
|
|
34890
|
+
if (!ended(brief2.state)) {
|
|
34832
34891
|
state.goal = { id: brief2.goalId, revision: brief2.revision };
|
|
34833
34892
|
state.below = baseline(brief2.goalId, brief2.subtree);
|
|
34834
34893
|
}
|
|
34835
34894
|
const outcome2 = brief2.outcome?.trim() ?? "";
|
|
34836
|
-
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());
|
|
34837
34896
|
const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
|
|
34838
34897
|
const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
|
|
34839
34898
|
const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
|
|
34840
|
-
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);
|
|
34841
34901
|
return;
|
|
34842
34902
|
}
|
|
34843
34903
|
const brief = await rail.read(state.goal.id);
|
|
@@ -34846,6 +34906,7 @@ async function look(session, state, rail, opts) {
|
|
|
34846
34906
|
const resolve3 = state.asks?.get(answer.requestEntryId);
|
|
34847
34907
|
if (!resolve3) continue;
|
|
34848
34908
|
state.asks.delete(answer.requestEntryId);
|
|
34909
|
+
(state.seen ??= /* @__PURE__ */ new Set()).add(`answer:${answer.decisionNeedId}`);
|
|
34849
34910
|
resolve3(answer.result);
|
|
34850
34911
|
}
|
|
34851
34912
|
if (ended(brief.state)) {
|
|
@@ -34923,8 +34984,9 @@ function runHarness(opts) {
|
|
|
34923
34984
|
const state = { resting: true };
|
|
34924
34985
|
const clock4 = beat();
|
|
34925
34986
|
let session = null;
|
|
34926
|
-
const speaksAs = opts.
|
|
34927
|
-
const
|
|
34987
|
+
const speaksAs = opts.session ?? opts.resume ?? sessionId();
|
|
34988
|
+
const instance = randomUUID6();
|
|
34989
|
+
const rail = railFor({ token: opts.token, reach: reachAs(speaksAs, instance) });
|
|
34928
34990
|
let inbox;
|
|
34929
34991
|
if (opts.harness === "claude") {
|
|
34930
34992
|
try {
|
|
@@ -34980,6 +35042,7 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
34980
35042
|
}
|
|
34981
35043
|
}
|
|
34982
35044
|
session = startSession({
|
|
35045
|
+
instance,
|
|
34983
35046
|
harness: opts.harness,
|
|
34984
35047
|
cwd: opts.cwd,
|
|
34985
35048
|
mode: opts.mode,
|
|
@@ -35022,9 +35085,9 @@ ${event.message}`, state, rail, { log: opts.log });
|
|
|
35022
35085
|
|
|
35023
35086
|
// src/host.ts
|
|
35024
35087
|
init_dist();
|
|
35025
|
-
import { readFileSync as
|
|
35088
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
35026
35089
|
import { homedir as homedir9 } from "os";
|
|
35027
|
-
import { join as
|
|
35090
|
+
import { join as join10 } from "path";
|
|
35028
35091
|
|
|
35029
35092
|
// src/broker.ts
|
|
35030
35093
|
init_evidence();
|
|
@@ -35044,15 +35107,15 @@ function ago(iso, now) {
|
|
|
35044
35107
|
init_catalog();
|
|
35045
35108
|
|
|
35046
35109
|
// src/workspaces.ts
|
|
35047
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as
|
|
35048
|
-
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";
|
|
35049
35112
|
import { homedir as homedir6 } from "os";
|
|
35050
35113
|
function workspacesFile() {
|
|
35051
|
-
return
|
|
35114
|
+
return join6(homedir6(), ".paigy", "workspaces.json");
|
|
35052
35115
|
}
|
|
35053
35116
|
function listWorkspaces(deps) {
|
|
35054
35117
|
try {
|
|
35055
|
-
const parsed = JSON.parse(
|
|
35118
|
+
const parsed = JSON.parse(readFileSync4(deps.file, "utf8"));
|
|
35056
35119
|
return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
|
|
35057
35120
|
} catch {
|
|
35058
35121
|
return [];
|
|
@@ -35081,8 +35144,8 @@ function resolveWakeDir(pinned, deps) {
|
|
|
35081
35144
|
}
|
|
35082
35145
|
function originOf(dir) {
|
|
35083
35146
|
try {
|
|
35084
|
-
if (!statSync3(
|
|
35085
|
-
const config =
|
|
35147
|
+
if (!statSync3(join6(dir, ".git")).isDirectory()) return null;
|
|
35148
|
+
const config = readFileSync4(join6(dir, ".git", "config"), "utf8");
|
|
35086
35149
|
const section = config.split(/^\[/m).find((s) => /^remote\s+"origin"\]/.test(s));
|
|
35087
35150
|
return section?.match(/^\s*url\s*=\s*(.+)$/m)?.[1]?.trim() ?? null;
|
|
35088
35151
|
} catch {
|
|
@@ -35103,7 +35166,7 @@ function listRepos(deps) {
|
|
|
35103
35166
|
}
|
|
35104
35167
|
for (const name of children) {
|
|
35105
35168
|
if (repos.length >= MAX_REPOS) return repos;
|
|
35106
|
-
const path =
|
|
35169
|
+
const path = join6(root, name);
|
|
35107
35170
|
const remote = originOf(path);
|
|
35108
35171
|
if (remote) repos.push({ path, remote });
|
|
35109
35172
|
}
|
|
@@ -35114,9 +35177,9 @@ function listRepos(deps) {
|
|
|
35114
35177
|
// src/holder.ts
|
|
35115
35178
|
init_dist();
|
|
35116
35179
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
35117
|
-
import { readFileSync as
|
|
35180
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync3 } from "fs";
|
|
35118
35181
|
import { homedir as homedir7 } from "os";
|
|
35119
|
-
import { join as
|
|
35182
|
+
import { join as join7 } from "path";
|
|
35120
35183
|
function isAlive(pid) {
|
|
35121
35184
|
try {
|
|
35122
35185
|
process.kill(pid, 0);
|
|
@@ -35154,9 +35217,9 @@ function proven(pid, procStart, deps) {
|
|
|
35154
35217
|
});
|
|
35155
35218
|
}
|
|
35156
35219
|
function localHolderPids(deps = {}) {
|
|
35157
|
-
const dir = deps.dir ??
|
|
35220
|
+
const dir = deps.dir ?? join7(process.env.CLAUDE_CONFIG_DIR?.trim() || join7(homedir7(), ".claude"), "sessions");
|
|
35158
35221
|
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
35159
|
-
const read = deps.read ?? ((f) =>
|
|
35222
|
+
const read = deps.read ?? ((f) => readFileSync5(f, "utf8"));
|
|
35160
35223
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
35161
35224
|
const held = /* @__PURE__ */ new Map();
|
|
35162
35225
|
let files;
|
|
@@ -35169,7 +35232,7 @@ function localHolderPids(deps = {}) {
|
|
|
35169
35232
|
if (!/^\d+\.json$/.test(file)) continue;
|
|
35170
35233
|
let row;
|
|
35171
35234
|
try {
|
|
35172
|
-
row = JSON.parse(read(
|
|
35235
|
+
row = JSON.parse(read(join7(dir, file)));
|
|
35173
35236
|
} catch {
|
|
35174
35237
|
continue;
|
|
35175
35238
|
}
|
|
@@ -35199,9 +35262,9 @@ function descendsFrom(pid, ancestor, parent = psParent) {
|
|
|
35199
35262
|
return false;
|
|
35200
35263
|
}
|
|
35201
35264
|
function localHolders(deps = {}) {
|
|
35202
|
-
const dir = deps.dir ??
|
|
35265
|
+
const dir = deps.dir ?? join7(process.env.CLAUDE_CONFIG_DIR?.trim() || join7(homedir7(), ".claude"), "sessions");
|
|
35203
35266
|
const list = deps.list ?? ((d) => readdirSync3(d));
|
|
35204
|
-
const read = deps.read ?? ((f) =>
|
|
35267
|
+
const read = deps.read ?? ((f) => readFileSync5(f, "utf8"));
|
|
35205
35268
|
const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
|
|
35206
35269
|
const held = /* @__PURE__ */ new Map();
|
|
35207
35270
|
let files;
|
|
@@ -35214,7 +35277,7 @@ function localHolders(deps = {}) {
|
|
|
35214
35277
|
if (!/^\d+\.json$/.test(file)) continue;
|
|
35215
35278
|
let row;
|
|
35216
35279
|
try {
|
|
35217
|
-
row = JSON.parse(read(
|
|
35280
|
+
row = JSON.parse(read(join7(dir, file)));
|
|
35218
35281
|
} catch {
|
|
35219
35282
|
continue;
|
|
35220
35283
|
}
|
|
@@ -35289,7 +35352,7 @@ async function endSession(end, deps) {
|
|
|
35289
35352
|
// src/owner.ts
|
|
35290
35353
|
import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync4 } from "fs";
|
|
35291
35354
|
import { homedir as homedir8 } from "os";
|
|
35292
|
-
import { join as
|
|
35355
|
+
import { join as join8 } from "path";
|
|
35293
35356
|
var HEAD_BYTES = 64e3;
|
|
35294
35357
|
function readHead(file) {
|
|
35295
35358
|
const fd = openSync2(file, "r");
|
|
@@ -35305,7 +35368,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35305
35368
|
const m = /^session:([0-9a-f]{8})$/.exec(slot);
|
|
35306
35369
|
if (!m) return null;
|
|
35307
35370
|
const prefix = m[1];
|
|
35308
|
-
const root = deps.projectsDir ??
|
|
35371
|
+
const root = deps.projectsDir ?? join8(homedir8(), ".claude", "projects");
|
|
35309
35372
|
const list = deps.list ?? ((dir) => readdirSync4(dir));
|
|
35310
35373
|
const head = deps.head ?? readHead;
|
|
35311
35374
|
let folders;
|
|
@@ -35317,7 +35380,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35317
35380
|
for (const folder of folders) {
|
|
35318
35381
|
let files;
|
|
35319
35382
|
try {
|
|
35320
|
-
files = list(
|
|
35383
|
+
files = list(join8(root, folder));
|
|
35321
35384
|
} catch {
|
|
35322
35385
|
continue;
|
|
35323
35386
|
}
|
|
@@ -35325,7 +35388,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35325
35388
|
if (!file) continue;
|
|
35326
35389
|
let text;
|
|
35327
35390
|
try {
|
|
35328
|
-
text = head(
|
|
35391
|
+
text = head(join8(root, folder, file));
|
|
35329
35392
|
} catch {
|
|
35330
35393
|
continue;
|
|
35331
35394
|
}
|
|
@@ -35345,7 +35408,7 @@ function ownerSession(slot, deps = {}) {
|
|
|
35345
35408
|
// src/update.ts
|
|
35346
35409
|
import { execFile } from "child_process";
|
|
35347
35410
|
import { createRequire } from "module";
|
|
35348
|
-
import { dirname as dirname3, join as
|
|
35411
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
35349
35412
|
import { promisify } from "util";
|
|
35350
35413
|
var SERVICE_LABEL = "ai.paigy.harness";
|
|
35351
35414
|
var VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
@@ -35372,7 +35435,7 @@ async function latestVersion() {
|
|
|
35372
35435
|
}
|
|
35373
35436
|
async function install(version) {
|
|
35374
35437
|
const bin = dirname3(process.execPath);
|
|
35375
|
-
await promisify(execFile)(
|
|
35438
|
+
await promisify(execFile)(join9(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
|
|
35376
35439
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
|
|
35377
35440
|
timeout: 5 * 6e4
|
|
35378
35441
|
});
|
|
@@ -35423,7 +35486,7 @@ function selfUpdater(deps) {
|
|
|
35423
35486
|
}
|
|
35424
35487
|
|
|
35425
35488
|
// src/host.ts
|
|
35426
|
-
var HOST_FILE =
|
|
35489
|
+
var HOST_FILE = join10(homedir9(), ".paigy", "host.json");
|
|
35427
35490
|
function wakeHarness(slot, saved, claudeTranscript) {
|
|
35428
35491
|
if (slot === "Desktop") return void 0;
|
|
35429
35492
|
if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
|
|
@@ -35976,9 +36039,9 @@ async function pairThisMachine() {
|
|
|
35976
36039
|
}
|
|
35977
36040
|
}
|
|
35978
36041
|
function forwardCodexThreadId() {
|
|
35979
|
-
const cfg =
|
|
36042
|
+
const cfg = join11(process.env.CODEX_HOME || join11(homedir10(), ".codex"), "config.toml");
|
|
35980
36043
|
if (!existsSync5(cfg)) return;
|
|
35981
|
-
const text =
|
|
36044
|
+
const text = readFileSync7(cfg, "utf8");
|
|
35982
36045
|
const head = "[mcp_servers.paigy]\n";
|
|
35983
36046
|
const at = text.indexOf(head);
|
|
35984
36047
|
if (at < 0) return;
|
|
@@ -35987,7 +36050,8 @@ function forwardCodexThreadId() {
|
|
|
35987
36050
|
return n < 0 ? text.length : n;
|
|
35988
36051
|
})());
|
|
35989
36052
|
if (/^\s*env_vars\s*=/m.test(block2)) return;
|
|
35990
|
-
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));
|
|
35991
36055
|
}
|
|
35992
36056
|
async function main() {
|
|
35993
36057
|
const parsed = parseArgs(process.argv.slice(2));
|
|
@@ -36005,6 +36069,7 @@ async function main() {
|
|
|
36005
36069
|
console.log(" service install the host as a login service (macOS)");
|
|
36006
36070
|
console.log(" enable-tools allowlist Paigy's tools in Claude Code so they don't prompt each time");
|
|
36007
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]');
|
|
36008
36073
|
console.log(" handoff pin this terminal's identity here so your phone can resume it");
|
|
36009
36074
|
console.log(" triage sweep your whole note queue with a model on THIS machine and");
|
|
36010
36075
|
console.log(" send grouped suggestions to your phone (close / stale / assign)");
|
|
@@ -36078,7 +36143,7 @@ async function main() {
|
|
|
36078
36143
|
const wsDeps = { file: workspacesFile() };
|
|
36079
36144
|
for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
|
|
36080
36145
|
if (!listWorkspaces(wsDeps).length) {
|
|
36081
|
-
const guess =
|
|
36146
|
+
const guess = join11(homedir10(), "projects");
|
|
36082
36147
|
if (existsSync5(guess)) {
|
|
36083
36148
|
addWorkspace(guess, wsDeps);
|
|
36084
36149
|
console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
|
|
@@ -36110,9 +36175,9 @@ async function main() {
|
|
|
36110
36175
|
}
|
|
36111
36176
|
}
|
|
36112
36177
|
try {
|
|
36113
|
-
const settings =
|
|
36178
|
+
const settings = join11(homedir10(), ".claude", "settings.json");
|
|
36114
36179
|
if (which("claude") && existsSync5(settings)) {
|
|
36115
|
-
const cfg = JSON.parse(
|
|
36180
|
+
const cfg = JSON.parse(readFileSync7(settings, "utf8"));
|
|
36116
36181
|
if (!cfg["statusLine"]) {
|
|
36117
36182
|
cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
|
|
36118
36183
|
writeFileSync5(settings, JSON.stringify(cfg, null, 2));
|
|
@@ -36168,13 +36233,13 @@ async function main() {
|
|
|
36168
36233
|
<key>SoftResourceLimits</key><dict>
|
|
36169
36234
|
<key>NumberOfFiles</key><integer>65536</integer>
|
|
36170
36235
|
</dict>
|
|
36171
|
-
<key>StandardOutPath</key><string>${
|
|
36172
|
-
<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>
|
|
36173
36238
|
</dict></plist>
|
|
36174
36239
|
`;
|
|
36175
|
-
const dir =
|
|
36240
|
+
const dir = join11(homedir10(), "Library", "LaunchAgents");
|
|
36176
36241
|
mkdirSync4(dir, { recursive: true });
|
|
36177
|
-
const path =
|
|
36242
|
+
const path = join11(dir, `${SERVICE_LABEL}.plist`);
|
|
36178
36243
|
writeFileSync5(path, plist);
|
|
36179
36244
|
execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
|
|
36180
36245
|
console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
|
|
@@ -36219,7 +36284,7 @@ async function main() {
|
|
|
36219
36284
|
return;
|
|
36220
36285
|
}
|
|
36221
36286
|
if (parsed.hatch) {
|
|
36222
|
-
overrideToken(
|
|
36287
|
+
overrideToken(readSlot("Desktop") || null);
|
|
36223
36288
|
const minted = await hatch(parsed.hatch, parsed.voice ?? null);
|
|
36224
36289
|
saveToken({ access_token: minted.token, name: minted.name ?? parsed.hatch, device: null }, parsed.slot ?? minted.name ?? parsed.hatch);
|
|
36225
36290
|
if (!minted.name) {
|
|
@@ -36227,8 +36292,7 @@ async function main() {
|
|
|
36227
36292
|
return;
|
|
36228
36293
|
}
|
|
36229
36294
|
console.log(`\u2713 hatched "${minted.name}" \u2014 it's on your phone's Agents screen now.`);
|
|
36230
|
-
console.log(`
|
|
36231
|
-
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>"`);
|
|
36232
36296
|
return;
|
|
36233
36297
|
}
|
|
36234
36298
|
if (parsed.host) {
|
|
@@ -36246,21 +36310,27 @@ async function main() {
|
|
|
36246
36310
|
});
|
|
36247
36311
|
return;
|
|
36248
36312
|
}
|
|
36249
|
-
let identityToken;
|
|
36250
|
-
if (parsed.identity) {
|
|
36251
|
-
const slot = readToken(parsed.identity);
|
|
36252
|
-
if (!slot) {
|
|
36253
|
-
console.error(`No slot for "${parsed.identity}" \u2014 hatch it first: paigy-harness hatch "${parsed.identity}"`);
|
|
36254
|
-
process.exit(1);
|
|
36255
|
-
}
|
|
36256
|
-
identityToken = slot;
|
|
36257
|
-
}
|
|
36258
36313
|
const state = detectAll().find((a) => a.name === parsed.harness);
|
|
36259
36314
|
if (state && state.status !== "ready") {
|
|
36260
36315
|
const hint = state.hint ?? "setup incomplete";
|
|
36261
36316
|
console.error(`\u270B ${state.label}: ${hint}`);
|
|
36262
36317
|
process.exit(1);
|
|
36263
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
|
+
}
|
|
36264
36334
|
const askTerminal = async (promptText) => {
|
|
36265
36335
|
if (parsed.graceSeconds <= 0 || !process.stdin.isTTY) return null;
|
|
36266
36336
|
const readline = await import("readline/promises");
|
|
@@ -36281,9 +36351,10 @@ async function main() {
|
|
|
36281
36351
|
harness: parsed.harness,
|
|
36282
36352
|
cwd: parsed.cwd,
|
|
36283
36353
|
mode: parsed.mode,
|
|
36284
|
-
|
|
36354
|
+
// hatch already admitted this brief; the pump must not admit it again after completion.
|
|
36355
|
+
prompt: launched ? "" : parsed.prompt,
|
|
36285
36356
|
...state?.bin ? { bin: state.bin } : {},
|
|
36286
|
-
...
|
|
36357
|
+
...launched ? { token: launched.token, session: launched.session } : {},
|
|
36287
36358
|
localAsk: {
|
|
36288
36359
|
permission: async (summary) => {
|
|
36289
36360
|
const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);
|
|
@@ -36295,7 +36366,7 @@ async function main() {
|
|
|
36295
36366
|
});
|
|
36296
36367
|
const beat2 = async () => {
|
|
36297
36368
|
try {
|
|
36298
|
-
await heartbeat();
|
|
36369
|
+
await heartbeat(void 0, launched ? { token: launched.token } : {});
|
|
36299
36370
|
signedOut = false;
|
|
36300
36371
|
} catch (e) {
|
|
36301
36372
|
if (!isUnauthorized(e) || signedOut) return;
|