@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.
Files changed (4) hide show
  1. package/README.md +10 -4
  2. package/dist/cli.js +181 -110
  3. package/dist/main.js +113 -63
  4. 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": INSTANCE_ID,
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({ name, voice })
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
- // A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
24624
- // checklist mode of answering"). The answer shape was taken off this surface with the wire form
24625
- // (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
24626
- // only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
24627
- // as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
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
- 'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
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'); when the options are independent and they may want several, pass select:'many' and they get a checklist. 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.";
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: the owner must 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.';
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. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
24704
- UPDATE_GOAL_DESCRIPTION = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. 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.`;
24705
- CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
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 = 30 * 6e4;
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] [--identity NAME] [--grace SECONDS] [--doctor] PROMPT\u2026";
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, identity: null, graceSeconds: 90, prompt: "", handoff: false, triage: false, via: null, json: false, apply: false, yesClose: false, yesStale: false, yesAssign: false };
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: "--identity needs a hatched agent's name" };
31154
- args.identity = v;
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 readFileSync6, writeFileSync as writeFileSync5 } from "fs";
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 join10 } from "path";
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.resume ?? opts.session ? { PAIGY_SESSION_ID: opts.resume ?? opts.session } : {},
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
- // A CHECKLIST WHEN THE CHOICES ARE INDEPENDENT (owner, 2026-10-02 01:44 UTC: "bring back the
33021
- // checklist mode of answering"). The answer shape was taken off this surface with the wire form
33022
- // (#575) on the promise that Paigy would derive it, and the Goal-scoped contact that replaced it
33023
- // only ever sends pick-one or words (`goal/intake.ts` `answerOf`): three independent fixes went out
33024
- // as "All three / 1 and 3 only / Just log it", which the person could not read without the text and
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
- 'How the options are answered: "one" (default) to pick one, "many" to tick any number of them. Use "many" when the options are independent things they may want several of \u2014 never combinations of them like "all three" or "1 and 3". Ignored without options.'
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'); when the options are independent and they may want several, pass select:'many' and they get a checklist. 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.";
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: the owner must 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.';
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. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.\n\ndiagnose: true is for when a read has CONFUSED you \u2014 not for the working loop. It adds `diagnosis`, which asks every reader that already answers a question about your work and NAMES the reader behind every value, so you never guess which of two places to look: what state your work is really in (the stored `goals.state` beside the derived `goal_execution_state`, which can differ); whether anything is armed to ring and when (`ladder_candidates`, with the ladder's own last decision); whether a wake fired for you and what it did (durable `wake.*` events, beside when your process was last heard from); and what has reached you (`list_waiting` \u2014 review flags, unstarted work, open questions, the person's newest words). Read `diagnosis.disagreements` FIRST: each one is two readers giving different values for the same fact, with both values and both sources, and it never chooses between them \u2014 that is yours to do, from the Goal's own history. `diagnosis.looked` names every reader asked, including any that could not answer, so an empty answer is never confused with a broken one.";
33101
- var UPDATE_GOAL_DESCRIPTION2 = `Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. 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.`;
33102
- var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
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 = 30 * 6e4;
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.claim(goal.id);
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
- const claimed = await rail.claim(goalId);
34785
- const done = await rail.update(goalId, { revision: claimed.revision ?? brief.revision ?? 1, changes: { reviewed: true }, reason: "Read by the harness session; the person's words on it are typed in." });
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 (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
34831
- else {
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.resume ?? opts.session;
34927
- const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
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 readFileSync5, writeFileSync as writeFileSync4 } from "fs";
35088
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
35026
35089
  import { homedir as homedir9 } from "os";
35027
- import { join as join9 } from "path";
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 readFileSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
35048
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
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 join5(homedir6(), ".paigy", "workspaces.json");
35114
+ return join6(homedir6(), ".paigy", "workspaces.json");
35052
35115
  }
35053
35116
  function listWorkspaces(deps) {
35054
35117
  try {
35055
- const parsed = JSON.parse(readFileSync3(deps.file, "utf8"));
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(join5(dir, ".git")).isDirectory()) return null;
35085
- const config = readFileSync3(join5(dir, ".git", "config"), "utf8");
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 = join5(root, name);
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 readFileSync4, readdirSync as readdirSync3 } from "fs";
35180
+ import { readFileSync as readFileSync5, readdirSync as readdirSync3 } from "fs";
35118
35181
  import { homedir as homedir7 } from "os";
35119
- import { join as join6 } from "path";
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 ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
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) => readFileSync4(f, "utf8"));
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(join6(dir, file)));
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 ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir7(), ".claude"), "sessions");
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) => readFileSync4(f, "utf8"));
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(join6(dir, file)));
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 join7 } from "path";
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 ?? join7(homedir8(), ".claude", "projects");
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(join7(root, folder));
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(join7(root, folder, file));
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 join8 } from "path";
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)(join8(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
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 = join9(homedir9(), ".paigy", "host.json");
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 = join10(process.env.CODEX_HOME || join10(homedir10(), ".codex"), "config.toml");
36042
+ const cfg = join11(process.env.CODEX_HOME || join11(homedir10(), ".codex"), "config.toml");
35980
36043
  if (!existsSync5(cfg)) return;
35981
- const text = readFileSync6(cfg, "utf8");
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) + 'env_vars = ["CODEX_THREAD_ID", "CODEX_SESSION_ID"]\n' + text.slice(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 = join10(homedir10(), "projects");
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 = join10(homedir10(), ".claude", "settings.json");
36178
+ const settings = join11(homedir10(), ".claude", "settings.json");
36114
36179
  if (which("claude") && existsSync5(settings)) {
36115
- const cfg = JSON.parse(readFileSync6(settings, "utf8"));
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>${join10(homedir10(), ".paigy", "host.log")}</string>
36172
- <key>StandardErrorPath</key><string>${join10(homedir10(), ".paigy", "host.log")}</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 = join10(homedir10(), "Library", "LaunchAgents");
36240
+ const dir = join11(homedir10(), "Library", "LaunchAgents");
36176
36241
  mkdirSync4(dir, { recursive: true });
36177
- const path = join10(dir, `${SERVICE_LABEL}.plist`);
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(readToken("Desktop") || null);
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(` speak as it: PAIGY_AGENT="${minted.name}" <any paigy tool>`);
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
- prompt: parsed.prompt,
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
- ...identityToken ? { token: identityToken } : {},
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;