@paigy/harness 0.3.11 → 0.3.13

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 (3) hide show
  1. package/dist/cli.js +186 -48
  2. package/dist/main.js +180 -45
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -22759,11 +22759,17 @@ function sessionId() {
22759
22759
  return SESSION_ID;
22760
22760
  }
22761
22761
  async function reach(url, init) {
22762
+ return send(url, init, SESSION_ID);
22763
+ }
22764
+ function reachAs(session) {
22765
+ return (url, init) => send(url, init, session);
22766
+ }
22767
+ async function send(url, init, session) {
22762
22768
  try {
22763
22769
  const headers = {
22764
22770
  ...init?.headers,
22765
22771
  "x-paigy-instance": INSTANCE_ID,
22766
- "x-paigy-session": SESSION_ID
22772
+ "x-paigy-session": session
22767
22773
  };
22768
22774
  return await fetch(url, { ...init, headers, dispatcher: await proxy() });
22769
22775
  } catch (e) {
@@ -23921,14 +23927,14 @@ async function acceptTriage(proposalId, body, opts = {}) {
23921
23927
  async function contact(input, opts = {}) {
23922
23928
  const parsed = ContactSchema.parse(input);
23923
23929
  opts.signal?.throwIfAborted();
23924
- const send2 = opts.reach ?? reach;
23930
+ const send22 = opts.reach ?? reach;
23925
23931
  const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
23926
23932
  let deliveryId;
23927
23933
  if ("deliveryId" in parsed) deliveryId = parsed.deliveryId;
23928
23934
  else {
23929
23935
  if (!opts.reach && readKeyFile()?.e2ee) throw new Error("Goal contact does not support E2EE yet; refusing to send plaintext.");
23930
23936
  const shaped = deriveAsk(NotifyRequestSchema.parse({ ask: parsed.ask, waiting: parsed.waiting, options: parsed.options }));
23931
- const res = ensureAuthed(await send2(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
23937
+ const res = ensureAuthed(await send22(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
23932
23938
  method: "POST",
23933
23939
  headers,
23934
23940
  signal: opts.signal,
@@ -23940,7 +23946,7 @@ async function contact(input, opts = {}) {
23940
23946
  if (!deliveryId) throw new Error("Goal contact returned no Delivery identity");
23941
23947
  }
23942
23948
  const read = async (signal2) => {
23943
- const res = ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23949
+ const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
23944
23950
  if (!res.ok) await fail("read_delivery", res);
23945
23951
  return await res.json();
23946
23952
  };
@@ -24062,7 +24068,7 @@ async function subscribeWake(onNudge, opts = {}) {
24062
24068
  }
24063
24069
  };
24064
24070
  }
24065
- var import_undici, require2, __create2, __defProp2, __getOwnPropDesc2, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __require2, __commonJS2, __copyProps2, __toESM2, require_nacl_fast, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, SESSION_ID, 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, 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, GetThreadSchema, GET_THREAD_DESCRIPTION, SearchThreadsSchema, SEARCH_THREADS_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, ContextSchema, ParticipantSchema, TransformSchema, OptionSchema, 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, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueItemSchema, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, EnvelopeRecipientSchema, EnvelopeHeaderSchema, EnvelopeSchema, SealedAnswerSchema, DeviceCredentialSchema, DeviceRosterSchema, WakeNudgeSchema, PairingStatusSchema, PairingRevealSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, FeedbackResolutionSchema, FeedbackOutcomeSchema, import_tweetnacl, import_tweetnacl2, import_tweetnacl3, import_tweetnacl4, import_tweetnacl5, AGENT_NAME, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, AWAIT_WINDOW_MS, tokenOverride, authToken, HEARTBEAT_MS, RETRY_MS, JOIN, lines;
24071
+ var import_undici, require2, __create2, __defProp2, __getOwnPropDesc2, __getOwnPropNames2, __getProtoOf2, __hasOwnProp2, __require2, __commonJS2, __copyProps2, __toESM2, require_nacl_fast, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, SESSION_ID, 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, 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, GetThreadSchema, GET_THREAD_DESCRIPTION, SearchThreadsSchema, SEARCH_THREADS_DESCRIPTION, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, OptionSchema, 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, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueItemSchema, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, EnvelopeRecipientSchema, EnvelopeHeaderSchema, EnvelopeSchema, SealedAnswerSchema, DeviceCredentialSchema, DeviceRosterSchema, WakeNudgeSchema, PairingStatusSchema, PairingRevealSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, FeedbackResolutionSchema, FeedbackOutcomeSchema, import_tweetnacl, import_tweetnacl2, import_tweetnacl3, import_tweetnacl4, import_tweetnacl5, AGENT_NAME, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, AWAIT_WINDOW_MS, tokenOverride, authToken, HEARTBEAT_MS, RETRY_MS, JOIN, lines;
24066
24072
  var init_dist = __esm({
24067
24073
  "../../packages/sdk/dist/index.js"() {
24068
24074
  "use strict";
@@ -26735,6 +26741,9 @@ var init_dist = __esm({
26735
26741
  CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
26736
26742
  CreateGoalSchema = external_exports.object({
26737
26743
  outcome: external_exports.string().trim().min(1).max(1e4),
26744
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
26745
+ * rings"). Omit it and the brain writes one at admission from the outcome. */
26746
+ title: external_exports.string().trim().min(1).max(80).optional(),
26738
26747
  ownerParticipant: external_exports.string().trim().min(1).optional(),
26739
26748
  idempotencyKey: external_exports.string().trim().min(1).max(200),
26740
26749
  /** A past conversation this Goal should be read against — History's "new session from this"
@@ -26754,11 +26763,14 @@ var init_dist = __esm({
26754
26763
  CreateGoalToolSchema = CreateGoalSchema.extend({
26755
26764
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
26756
26765
  }).strict();
26757
- 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. 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.";
26766
+ 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 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.';
26758
26767
  UpdateGoalSchema = external_exports.object({
26759
26768
  revision: external_exports.number().int().positive(),
26760
26769
  changes: external_exports.object({
26761
26770
  outcome: external_exports.string().trim().min(1).max(1e4).optional(),
26771
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
26772
+ * brain writes one at admission; this is the owner saying it better. Null clears it. */
26773
+ title: external_exports.string().trim().min(1).max(80).nullable().optional(),
26762
26774
  ownerParticipant: external_exports.string().trim().min(1).optional(),
26763
26775
  parentGoalId: external_exports.string().uuid().nullable().optional(),
26764
26776
  dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
@@ -26775,7 +26787,7 @@ var init_dist = __esm({
26775
26787
  ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
26776
26788
  GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
26777
26789
  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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
26778
- UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. 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. Returns the Goal as get_goal reads it, at its new revision.";
26790
+ UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
26779
26791
  CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
26780
26792
  CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
26781
26793
  CheckRepliesSchema = external_exports.object({}).strict();
@@ -26798,6 +26810,7 @@ var init_dist = __esm({
26798
26810
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
26799
26811
  ];
26800
26812
  AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
26813
+ LIVE_MS = 3 * 6e4;
26801
26814
  ContextSchema = external_exports.object({
26802
26815
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
26803
26816
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -27579,7 +27592,19 @@ var init_dist = __esm({
27579
27592
  /** The ruling in the person's own words, from the contribution that replied — not the
27580
27593
  * option id, which is not something anyone reads back. Null while it is open, and null
27581
27594
  * for a settled question whose reply carried nothing readable. */
27582
- answer: external_exports.string().nullable().default(null)
27595
+ answer: external_exports.string().nullable().default(null),
27596
+ /** The Goal this question belongs to — a step knows its Goal on its own, not only through
27597
+ * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
27598
+ * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
27599
+ * order, its route, home's trees and the list of steps all take a step's Goal from here, so
27600
+ * this is the field they agree through rather than each re-deriving it from the row it
27601
+ * arrived under. Required because the API projects it on every need it sends. */
27602
+ goalId: external_exports.string(),
27603
+ /** True only while an unmet START gate holds the Goal — a Goal that merely waits to
27604
+ * *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
27605
+ * the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
27606
+ * gate at all. */
27607
+ blocked: external_exports.boolean().default(false)
27583
27608
  });
27584
27609
  QueueItemSchema = external_exports.object({
27585
27610
  id: external_exports.string(),
@@ -32456,7 +32481,7 @@ var require_lib = __commonJS({
32456
32481
 
32457
32482
  // src/triage/judge.ts
32458
32483
  import { spawnSync as spawnSync2 } from "child_process";
32459
- import { homedir as homedir6 } from "os";
32484
+ import { homedir as homedir7 } from "os";
32460
32485
  import { delimiter as delimiter3 } from "path";
32461
32486
  function pickOllamaModel(tags) {
32462
32487
  if (tags.length === 0) return null;
@@ -32539,7 +32564,7 @@ async function judgeFor(asked, deps = {}) {
32539
32564
  const out = spawnSync2(bin, runner.argv(null), {
32540
32565
  input: prompt,
32541
32566
  encoding: "utf8",
32542
- cwd: homedir6(),
32567
+ cwd: homedir7(),
32543
32568
  timeout: RUN_TIMEOUT_MS,
32544
32569
  maxBuffer: 32 * 1024 * 1024,
32545
32570
  env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
@@ -33129,8 +33154,11 @@ var import_qrcode = __toESM(require_lib(), 1);
33129
33154
  import { hostname } from "os";
33130
33155
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
33131
33156
  import { execFile as execFile2, execSync } from "child_process";
33132
- import { homedir as homedir7 } from "os";
33133
- import { join as join6 } from "path";
33157
+ import { homedir as homedir8 } from "os";
33158
+ import { join as join7 } from "path";
33159
+
33160
+ // src/run.ts
33161
+ init_dist();
33134
33162
 
33135
33163
  // src/harness/session.ts
33136
33164
  import { spawn } from "child_process";
@@ -33145,15 +33173,19 @@ function optionFor(options, decision) {
33145
33173
  return options.find((o) => o.kind === want)?.optionId ?? null;
33146
33174
  }
33147
33175
  function createAcpDriver(opts) {
33148
- return new AcpDriver(opts.cwd, opts.mode);
33176
+ return new AcpDriver(opts.cwd, opts.mode, opts.resume);
33149
33177
  }
33150
33178
  var AcpDriver = class {
33151
- constructor(cwd, mode) {
33179
+ /** `resume`: the id of an existing session to continue rather than open a new one (#1637)
33180
+ * the session a bound identity belongs to, so the process this drives IS its holder. */
33181
+ constructor(cwd, mode, resume) {
33152
33182
  this.cwd = cwd;
33153
33183
  this.mode = mode;
33184
+ this.resume = resume;
33154
33185
  }
33155
33186
  cwd;
33156
33187
  mode;
33188
+ resume;
33157
33189
  nextId = 1;
33158
33190
  initId;
33159
33191
  sessionNewId;
@@ -33237,15 +33269,15 @@ var AcpDriver = class {
33237
33269
  this.sessionNewId = this.nextId++;
33238
33270
  return {
33239
33271
  events: [],
33240
- writes: [frame2({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
33272
+ writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : { id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
33241
33273
  };
33242
33274
  }
33243
33275
  if (msg.id === this.sessionNewId) {
33244
33276
  this.sessionNewId = void 0;
33245
- const sessionId2 = msg.result?.sessionId;
33277
+ const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
33246
33278
  if (!sessionId2) {
33247
33279
  return {
33248
- events: [{ kind: "error", message: `session/new failed: ${msg.error?.message ?? "no sessionId"}` }],
33280
+ events: [{ kind: "error", message: `${this.resume ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}` }],
33249
33281
  writes: []
33250
33282
  };
33251
33283
  }
@@ -33382,7 +33414,8 @@ function startSession(opts) {
33382
33414
  env: {
33383
33415
  ...process.env,
33384
33416
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
33385
- ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
33417
+ ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
33418
+ ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
33386
33419
  },
33387
33420
  stdio: ["pipe", "pipe", "pipe"]
33388
33421
  });
@@ -33402,7 +33435,7 @@ function startSession(opts) {
33402
33435
  opts.onEvent({ kind: "error", message: e.message });
33403
33436
  opts.onExit?.();
33404
33437
  });
33405
- const driver = createAcpDriver({ cwd, mode: opts.mode });
33438
+ const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.resume ? { resume: opts.resume } : {} });
33406
33439
  let buffer = "";
33407
33440
  child.stdout?.on("data", (chunk) => {
33408
33441
  const { lines: lines2, rest } = splitLines(buffer, String(chunk));
@@ -34784,6 +34817,9 @@ var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
34784
34817
  var CONTACT_DESCRIPTION2 = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
34785
34818
  var CreateGoalSchema2 = external_exports.object({
34786
34819
  outcome: external_exports.string().trim().min(1).max(1e4),
34820
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
34821
+ * rings"). Omit it and the brain writes one at admission from the outcome. */
34822
+ title: external_exports.string().trim().min(1).max(80).optional(),
34787
34823
  ownerParticipant: external_exports.string().trim().min(1).optional(),
34788
34824
  idempotencyKey: external_exports.string().trim().min(1).max(200),
34789
34825
  /** A past conversation this Goal should be read against — History's "new session from this"
@@ -34803,11 +34839,14 @@ var CreateGoalSchema2 = external_exports.object({
34803
34839
  var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
34804
34840
  idempotencyKey: CreateGoalSchema2.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
34805
34841
  }).strict();
34806
- 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. 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.";
34842
+ 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 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.';
34807
34843
  var UpdateGoalSchema2 = external_exports.object({
34808
34844
  revision: external_exports.number().int().positive(),
34809
34845
  changes: external_exports.object({
34810
34846
  outcome: external_exports.string().trim().min(1).max(1e4).optional(),
34847
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
34848
+ * brain writes one at admission; this is the owner saying it better. Null clears it. */
34849
+ title: external_exports.string().trim().min(1).max(80).nullable().optional(),
34811
34850
  ownerParticipant: external_exports.string().trim().min(1).optional(),
34812
34851
  parentGoalId: external_exports.string().uuid().nullable().optional(),
34813
34852
  dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
@@ -34824,7 +34863,7 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
34824
34863
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
34825
34864
  var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
34826
34865
  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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
34827
- var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. 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. Returns the Goal as get_goal reads it, at its new revision.";
34866
+ var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
34828
34867
  var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
34829
34868
  var CHECK_REPLIES_DESCRIPTION2 = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
34830
34869
  var CheckRepliesSchema2 = external_exports.object({}).strict();
@@ -34862,6 +34901,7 @@ function entryWords(entry) {
34862
34901
  }
34863
34902
  return entry.sources.map((source) => source.text).join("\n");
34864
34903
  }
34904
+ var LIVE_MS2 = 3 * 6e4;
34865
34905
  var ContextSchema2 = external_exports.object({
34866
34906
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
34867
34907
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -35643,7 +35683,19 @@ var QueueQuestionSchema2 = external_exports.object({
35643
35683
  /** The ruling in the person's own words, from the contribution that replied — not the
35644
35684
  * option id, which is not something anyone reads back. Null while it is open, and null
35645
35685
  * for a settled question whose reply carried nothing readable. */
35646
- answer: external_exports.string().nullable().default(null)
35686
+ answer: external_exports.string().nullable().default(null),
35687
+ /** The Goal this question belongs to — a step knows its Goal on its own, not only through
35688
+ * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
35689
+ * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
35690
+ * order, its route, home's trees and the list of steps all take a step's Goal from here, so
35691
+ * this is the field they agree through rather than each re-deriving it from the row it
35692
+ * arrived under. Required because the API projects it on every need it sends. */
35693
+ goalId: external_exports.string(),
35694
+ /** True only while an unmet START gate holds the Goal — a Goal that merely waits to
35695
+ * *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
35696
+ * the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
35697
+ * gate at all. */
35698
+ blocked: external_exports.boolean().default(false)
35647
35699
  });
35648
35700
  var QueueItemSchema2 = external_exports.object({
35649
35701
  id: external_exports.string(),
@@ -35968,7 +36020,7 @@ async function step(session, state, rail) {
35968
36020
  else state.goal = { id: brief2.goalId, revision: brief2.revision };
35969
36021
  const outcome = brief2.outcome?.trim() ?? "";
35970
36022
  const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
35971
- send(session, state, [outcome, ...said].filter(Boolean).join("\n\n"));
36023
+ send2(session, state, [outcome, ...said].filter(Boolean).join("\n\n"));
35972
36024
  return;
35973
36025
  }
35974
36026
  const brief = await rail.read(state.goal.id);
@@ -35987,9 +36039,9 @@ async function step(session, state, rail) {
35987
36039
  }
35988
36040
  if (state.asks?.size || !state.resting) return;
35989
36041
  const words2 = unheard(brief, state);
35990
- if (words2.length) send(session, state, words2.join("\n\n"));
36042
+ if (words2.length) send2(session, state, words2.join("\n\n"));
35991
36043
  }
35992
- function send(session, state, text) {
36044
+ function send2(session, state, text) {
35993
36045
  state.resting = false;
35994
36046
  session.send(text);
35995
36047
  }
@@ -36122,7 +36174,7 @@ function runHarness(opts) {
36122
36174
  let tail = [];
36123
36175
  const state = { resting: true };
36124
36176
  let session = null;
36125
- const rail = railFor({ token: opts.token });
36177
+ const rail = railFor({ token: opts.token, ...opts.resume ? { reach: reachAs(opts.resume) } : {} });
36126
36178
  async function handle(event) {
36127
36179
  if (event.kind === "error") {
36128
36180
  opts.log(`\u26A0 ${event.message}`);
@@ -36170,6 +36222,7 @@ function runHarness(opts) {
36170
36222
  cwd: opts.cwd,
36171
36223
  mode: opts.mode,
36172
36224
  ...opts.token ? { token: opts.token } : {},
36225
+ ...opts.resume ? { resume: opts.resume } : {},
36173
36226
  ...opts.bin ? { bin: opts.bin } : {},
36174
36227
  ...opts.onExit ? { onExit: opts.onExit } : {},
36175
36228
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
@@ -36203,8 +36256,8 @@ function runHarness(opts) {
36203
36256
  init_dist();
36204
36257
  init_catalog();
36205
36258
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
36206
- import { homedir as homedir5 } from "os";
36207
- import { join as join5 } from "path";
36259
+ import { homedir as homedir6 } from "os";
36260
+ import { join as join6 } from "path";
36208
36261
 
36209
36262
  // src/workspaces.ts
36210
36263
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -36243,10 +36296,66 @@ function resolveWakeDir(pinned, deps) {
36243
36296
  return listWorkspaces(deps)[0];
36244
36297
  }
36245
36298
 
36299
+ // src/owner.ts
36300
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
36301
+ import { homedir as homedir5 } from "os";
36302
+ import { join as join4 } from "path";
36303
+ var HEAD_BYTES = 64e3;
36304
+ function readHead(file) {
36305
+ const fd = openSync2(file, "r");
36306
+ try {
36307
+ const buf = Buffer.alloc(HEAD_BYTES);
36308
+ const n = readSync(fd, buf, 0, HEAD_BYTES, 0);
36309
+ return buf.subarray(0, n).toString("utf8");
36310
+ } finally {
36311
+ closeSync2(fd);
36312
+ }
36313
+ }
36314
+ function ownerSession(slot, deps = {}) {
36315
+ const m = /^session:([0-9a-f]{8})$/.exec(slot);
36316
+ if (!m) return null;
36317
+ const prefix = m[1];
36318
+ const root = deps.projectsDir ?? join4(homedir5(), ".claude", "projects");
36319
+ const list = deps.list ?? ((dir) => readdirSync(dir));
36320
+ const head = deps.head ?? readHead;
36321
+ let folders;
36322
+ try {
36323
+ folders = list(root);
36324
+ } catch {
36325
+ return null;
36326
+ }
36327
+ for (const folder of folders) {
36328
+ let files;
36329
+ try {
36330
+ files = list(join4(root, folder));
36331
+ } catch {
36332
+ continue;
36333
+ }
36334
+ const file = files.find((f) => f.startsWith(prefix) && f.endsWith(".jsonl"));
36335
+ if (!file) continue;
36336
+ let text;
36337
+ try {
36338
+ text = head(join4(root, folder, file));
36339
+ } catch {
36340
+ continue;
36341
+ }
36342
+ for (const line of text.split("\n")) {
36343
+ try {
36344
+ const row = JSON.parse(line);
36345
+ if (typeof row.cwd === "string" && typeof row.sessionId === "string" && row.sessionId.startsWith(prefix)) {
36346
+ return { sessionId: row.sessionId, cwd: row.cwd };
36347
+ }
36348
+ } catch {
36349
+ }
36350
+ }
36351
+ }
36352
+ return null;
36353
+ }
36354
+
36246
36355
  // src/update.ts
36247
36356
  import { execFile } from "child_process";
36248
36357
  import { createRequire } from "module";
36249
- import { dirname as dirname3, join as join4 } from "path";
36358
+ import { dirname as dirname3, join as join5 } from "path";
36250
36359
  import { promisify } from "util";
36251
36360
  var SERVICE_LABEL = "ai.paigy.harness";
36252
36361
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -36259,9 +36368,13 @@ function newer(latest, current) {
36259
36368
  for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
36260
36369
  return false;
36261
36370
  }
36371
+ var WAIT_CAP_MS = 6 * 36e5;
36262
36372
  function updateAction(o) {
36263
36373
  if (!o.service || !o.latest || o.latest === o.failed || !newer(o.latest, o.current)) return "none";
36264
- return o.sessions > 0 ? "wait" : "install";
36374
+ if (o.sessions <= 0) return "install";
36375
+ const working = o.working ?? o.sessions;
36376
+ if (working > 0) return "wait";
36377
+ return (o.waited ?? 0) >= WAIT_CAP_MS ? "install" : "wait";
36265
36378
  }
36266
36379
  async function latestVersion() {
36267
36380
  const res = await fetch(REGISTRY, { signal: AbortSignal.timeout(1e4) });
@@ -36269,7 +36382,7 @@ async function latestVersion() {
36269
36382
  }
36270
36383
  async function install(version) {
36271
36384
  const bin = dirname3(process.execPath);
36272
- await promisify(execFile)(join4(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
36385
+ await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
36273
36386
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
36274
36387
  timeout: 5 * 6e4
36275
36388
  });
@@ -36280,6 +36393,7 @@ function selfUpdater(deps) {
36280
36393
  let checkedAt = -Infinity;
36281
36394
  let failed;
36282
36395
  let waiting = false;
36396
+ let waitingSince = null;
36283
36397
  return async () => {
36284
36398
  if (!service) return;
36285
36399
  const now = (deps.now ?? Date.now)();
@@ -36287,9 +36401,25 @@ function selfUpdater(deps) {
36287
36401
  checkedAt = now;
36288
36402
  latest = await (deps.latest ?? latestVersion)().catch(() => latest);
36289
36403
  }
36290
- const action = updateAction({ current: VERSION, latest, service, sessions: deps.sessions(), failed });
36291
- if (action === "wait" && !waiting) deps.log(`\u21E1 @paigy/harness ${latest} is out \u2014 updating once no session is running`);
36404
+ const sessions = deps.sessions();
36405
+ const working = deps.working?.();
36406
+ if (latest && newer(latest, VERSION) && latest !== failed && waitingSince === null) waitingSince = now;
36407
+ const action = updateAction({
36408
+ current: VERSION,
36409
+ latest,
36410
+ service,
36411
+ sessions,
36412
+ failed,
36413
+ ...working === void 0 ? {} : { working },
36414
+ waited: waitingSince === null ? 0 : now - waitingSince
36415
+ });
36416
+ if (action === "wait" && !waiting) {
36417
+ deps.log(working ? `\u21E1 @paigy/harness ${latest} is out \u2014 updating once the turn in flight finishes` : `\u21E1 @paigy/harness ${latest} is out \u2014 updating once no session is running (at most ${Math.round(WAIT_CAP_MS / 36e5)}h)`);
36418
+ }
36292
36419
  waiting = action === "wait";
36420
+ if (action === "install" && sessions > 0) {
36421
+ deps.log(`\u21E1 @paigy/harness ${latest} is out and ${sessions} resting session(s) have held it for ${Math.round((now - (waitingSince ?? now)) / 36e5)}h \u2014 restarting over them`);
36422
+ }
36293
36423
  if (action !== "install" || !latest) return;
36294
36424
  try {
36295
36425
  await (deps.install ?? install)(latest);
@@ -36303,14 +36433,14 @@ function selfUpdater(deps) {
36303
36433
  }
36304
36434
 
36305
36435
  // src/host.ts
36306
- var HOST_FILE = join5(homedir5(), ".paigy", "host.json");
36436
+ var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
36307
36437
  var runKey = (slot) => `slot:${slot}`;
36308
36438
  var claimedRun = (sessionId2) => {
36309
36439
  const slot = sessionSlot(sessionId2);
36310
36440
  return { slot, key: runKey(slot) };
36311
36441
  };
36312
- async function slotHasWaitingWork(token) {
36313
- return (await checkReplies({ token })).deliveries.length > 0;
36442
+ async function slotHasWaitingWork(token, session) {
36443
+ return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
36314
36444
  }
36315
36445
  var PRESENCE_FRESH_MS = 3 * 6e4;
36316
36446
  function wakeAction(live, now, serverSeenAt) {
@@ -36334,7 +36464,12 @@ function startHost(opts) {
36334
36464
  }
36335
36465
  }
36336
36466
  };
36337
- const update = selfUpdater({ sessions: () => runs.size, log: opts.log });
36467
+ const update = selfUpdater({
36468
+ sessions: () => runs.size,
36469
+ // A turn in flight blocks the update; a session merely standing by does not hold it forever.
36470
+ working: () => [...runs.values()].filter((r) => r.run.working()).length,
36471
+ log: opts.log
36472
+ });
36338
36473
  const beat = () => {
36339
36474
  void refreshIdentities();
36340
36475
  void update().catch(() => {
@@ -36403,9 +36538,11 @@ function startHost(opts) {
36403
36538
  if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
36404
36539
  const token = readToken(slot);
36405
36540
  if (!token) return;
36406
- const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
36541
+ const owner = ownerSession(slot);
36542
+ if (owner && !allowed(owner.cwd, opts.wsDeps)) return;
36543
+ const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
36407
36544
  if (!workspace) return;
36408
- if (!await slotHasWaitingWork(token).catch(() => false)) return;
36545
+ if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
36409
36546
  const label = slotName(slot) ?? slot;
36410
36547
  const log = (line) => opts.log(`[${label}] ${line}`);
36411
36548
  const run = runHarness({
@@ -36414,6 +36551,7 @@ function startHost(opts) {
36414
36551
  mode: "bypass",
36415
36552
  prompt: "",
36416
36553
  token,
36554
+ ...owner ? { resume: owner.sessionId } : {},
36417
36555
  // A dead run must not squat the slot — evict so the next wake can respawn.
36418
36556
  onExit: () => {
36419
36557
  runs.delete(key);
@@ -36424,7 +36562,7 @@ function startHost(opts) {
36424
36562
  runs.set(key, { run, label, token });
36425
36563
  void heartbeat(void 0, { token }).catch(() => {
36426
36564
  });
36427
- opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
36565
+ opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
36428
36566
  }
36429
36567
  }
36430
36568
  const listening = /* @__PURE__ */ new Map();
@@ -36656,7 +36794,7 @@ async function pairThisMachine() {
36656
36794
  }
36657
36795
  }
36658
36796
  function forwardCodexThreadId() {
36659
- const cfg = join6(process.env.CODEX_HOME || join6(homedir7(), ".codex"), "config.toml");
36797
+ const cfg = join7(process.env.CODEX_HOME || join7(homedir8(), ".codex"), "config.toml");
36660
36798
  if (!existsSync5(cfg)) return;
36661
36799
  const text = readFileSync4(cfg, "utf8");
36662
36800
  const head = "[mcp_servers.paigy]\n";
@@ -36758,7 +36896,7 @@ async function main() {
36758
36896
  const wsDeps = { file: workspacesFile() };
36759
36897
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
36760
36898
  if (!listWorkspaces(wsDeps).length) {
36761
- const guess = join6(homedir7(), "projects");
36899
+ const guess = join7(homedir8(), "projects");
36762
36900
  if (existsSync5(guess)) {
36763
36901
  addWorkspace(guess, wsDeps);
36764
36902
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -36799,7 +36937,7 @@ async function main() {
36799
36937
  }
36800
36938
  }
36801
36939
  try {
36802
- const settings = join6(homedir7(), ".claude", "settings.json");
36940
+ const settings = join7(homedir8(), ".claude", "settings.json");
36803
36941
  if (which("claude") && existsSync5(settings)) {
36804
36942
  const cfg = JSON.parse(readFileSync4(settings, "utf8"));
36805
36943
  if (!cfg["statusLine"]) {
@@ -36857,13 +36995,13 @@ async function main() {
36857
36995
  <key>SoftResourceLimits</key><dict>
36858
36996
  <key>NumberOfFiles</key><integer>65536</integer>
36859
36997
  </dict>
36860
- <key>StandardOutPath</key><string>${join6(homedir7(), ".paigy", "host.log")}</string>
36861
- <key>StandardErrorPath</key><string>${join6(homedir7(), ".paigy", "host.log")}</string>
36998
+ <key>StandardOutPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
36999
+ <key>StandardErrorPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
36862
37000
  </dict></plist>
36863
37001
  `;
36864
- const dir = join6(homedir7(), "Library", "LaunchAgents");
37002
+ const dir = join7(homedir8(), "Library", "LaunchAgents");
36865
37003
  mkdirSync3(dir, { recursive: true });
36866
- const path = join6(dir, `${SERVICE_LABEL}.plist`);
37004
+ const path = join7(dir, `${SERVICE_LABEL}.plist`);
36867
37005
  writeFileSync4(path, plist);
36868
37006
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
36869
37007
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
@@ -36886,7 +37024,7 @@ async function main() {
36886
37024
  process.exit(1);
36887
37025
  }
36888
37026
  const name = slotName(slot) ?? slot;
36889
- console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir7(), "~")}`);
37027
+ console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir8(), "~")}`);
36890
37028
  if (!allowed(cwd, { file: workspacesFile() })) {
36891
37029
  console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
36892
37030
  console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
package/dist/main.js CHANGED
@@ -4580,7 +4580,7 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron";
4580
4580
  import { spawn as spawn2 } from "child_process";
4581
4581
  import { readFileSync as readFileSync4 } from "fs";
4582
4582
  import { fileURLToPath } from "url";
4583
- import { dirname as dirname4, join as join6 } from "path";
4583
+ import { dirname as dirname4, join as join7 } from "path";
4584
4584
 
4585
4585
  // ../../packages/sdk/dist/index.js
4586
4586
  import { createRequire as __sdkCreateRequire } from "module";
@@ -10915,11 +10915,17 @@ function sessionId() {
10915
10915
  return SESSION_ID;
10916
10916
  }
10917
10917
  async function reach(url, init) {
10918
+ return send(url, init, SESSION_ID);
10919
+ }
10920
+ function reachAs(session) {
10921
+ return (url, init) => send(url, init, session);
10922
+ }
10923
+ async function send(url, init, session) {
10918
10924
  try {
10919
10925
  const headers = {
10920
10926
  ...init?.headers,
10921
10927
  "x-paigy-instance": INSTANCE_ID,
10922
- "x-paigy-session": SESSION_ID
10928
+ "x-paigy-session": session
10923
10929
  };
10924
10930
  return await fetch(url, { ...init, headers, dispatcher: await proxy() });
10925
10931
  } catch (e) {
@@ -12195,6 +12201,9 @@ var CONTACT_SCHEMA = { type: "object", ...mcpInputSchema(ContactSchema) };
12195
12201
  var CONTACT_DESCRIPTION = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
12196
12202
  var CreateGoalSchema = external_exports.object({
12197
12203
  outcome: external_exports.string().trim().min(1).max(1e4),
12204
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
12205
+ * rings"). Omit it and the brain writes one at admission from the outcome. */
12206
+ title: external_exports.string().trim().min(1).max(80).optional(),
12198
12207
  ownerParticipant: external_exports.string().trim().min(1).optional(),
12199
12208
  idempotencyKey: external_exports.string().trim().min(1).max(200),
12200
12209
  /** A past conversation this Goal should be read against — History's "new session from this"
@@ -12214,11 +12223,14 @@ var CreateGoalSchema = external_exports.object({
12214
12223
  var CreateGoalToolSchema = CreateGoalSchema.extend({
12215
12224
  idempotencyKey: CreateGoalSchema.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
12216
12225
  }).strict();
12217
- var 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. 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.";
12226
+ var 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 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.';
12218
12227
  var UpdateGoalSchema = external_exports.object({
12219
12228
  revision: external_exports.number().int().positive(),
12220
12229
  changes: external_exports.object({
12221
12230
  outcome: external_exports.string().trim().min(1).max(1e4).optional(),
12231
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
12232
+ * brain writes one at admission; this is the owner saying it better. Null clears it. */
12233
+ title: external_exports.string().trim().min(1).max(80).nullable().optional(),
12222
12234
  ownerParticipant: external_exports.string().trim().min(1).optional(),
12223
12235
  parentGoalId: external_exports.string().uuid().nullable().optional(),
12224
12236
  dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
@@ -12235,7 +12247,7 @@ var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({
12235
12247
  var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
12236
12248
  var GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
12237
12249
  var 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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
12238
- var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. 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. Returns the Goal as get_goal reads it, at its new revision.";
12250
+ var UPDATE_GOAL_DESCRIPTION = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
12239
12251
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
12240
12252
  var CHECK_REPLIES_DESCRIPTION = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
12241
12253
  var CheckRepliesSchema = external_exports.object({}).strict();
@@ -12258,6 +12270,7 @@ var AGENT_TOOLS = [
12258
12270
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION, inputSchema: mcpInputSchema(UpdateGoalToolSchema) }
12259
12271
  ];
12260
12272
  var AGENT_TOOL_NAMES = AGENT_TOOLS.map((t) => t.name);
12273
+ var LIVE_MS = 3 * 6e4;
12261
12274
  var ContextSchema = external_exports.object({
12262
12275
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
12263
12276
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -13094,7 +13107,19 @@ var QueueQuestionSchema = external_exports.object({
13094
13107
  /** The ruling in the person's own words, from the contribution that replied — not the
13095
13108
  * option id, which is not something anyone reads back. Null while it is open, and null
13096
13109
  * for a settled question whose reply carried nothing readable. */
13097
- answer: external_exports.string().nullable().default(null)
13110
+ answer: external_exports.string().nullable().default(null),
13111
+ /** The Goal this question belongs to — a step knows its Goal on its own, not only through
13112
+ * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
13113
+ * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
13114
+ * order, its route, home's trees and the list of steps all take a step's Goal from here, so
13115
+ * this is the field they agree through rather than each re-deriving it from the row it
13116
+ * arrived under. Required because the API projects it on every need it sends. */
13117
+ goalId: external_exports.string(),
13118
+ /** True only while an unmet START gate holds the Goal — a Goal that merely waits to
13119
+ * *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
13120
+ * the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
13121
+ * gate at all. */
13122
+ blocked: external_exports.boolean().default(false)
13098
13123
  });
13099
13124
  var QueueItemSchema = external_exports.object({
13100
13125
  id: external_exports.string(),
@@ -13615,14 +13640,14 @@ async function dismissTriage(proposalId, opts = {}) {
13615
13640
  async function contact(input, opts = {}) {
13616
13641
  const parsed = ContactSchema.parse(input);
13617
13642
  opts.signal?.throwIfAborted();
13618
- const send2 = opts.reach ?? reach;
13643
+ const send22 = opts.reach ?? reach;
13619
13644
  const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
13620
13645
  let deliveryId;
13621
13646
  if ("deliveryId" in parsed) deliveryId = parsed.deliveryId;
13622
13647
  else {
13623
13648
  if (!opts.reach && readKeyFile()?.e2ee) throw new Error("Goal contact does not support E2EE yet; refusing to send plaintext.");
13624
13649
  const shaped = deriveAsk(NotifyRequestSchema.parse({ ask: parsed.ask, waiting: parsed.waiting, options: parsed.options }));
13625
- const res = ensureAuthed(await send2(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
13650
+ const res = ensureAuthed(await send22(`${BACKEND_URL}/api/goals/${parsed.goalIds[0]}/contact`, {
13626
13651
  method: "POST",
13627
13652
  headers,
13628
13653
  signal: opts.signal,
@@ -13634,7 +13659,7 @@ async function contact(input, opts = {}) {
13634
13659
  if (!deliveryId) throw new Error("Goal contact returned no Delivery identity");
13635
13660
  }
13636
13661
  const read = async (signal2) => {
13637
- const res = ensureAuthed(await send2(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
13662
+ const res = ensureAuthed(await send22(`${BACKEND_URL}/api/deliveries/${encodeURIComponent(deliveryId)}`, { headers, signal: signal2 }));
13638
13663
  if (!res.ok) await fail("read_delivery", res);
13639
13664
  return await res.json();
13640
13665
  };
@@ -13763,7 +13788,7 @@ async function subscribeWake(onNudge, opts = {}) {
13763
13788
 
13764
13789
  // src/main.ts
13765
13790
  var import_qrcode = __toESM(require_lib(), 1);
13766
- import { homedir as homedir7, hostname } from "os";
13791
+ import { homedir as homedir8, hostname } from "os";
13767
13792
 
13768
13793
  // src/harness/catalog.ts
13769
13794
  import { spawnSync } from "child_process";
@@ -13857,8 +13882,8 @@ function detectAll(deps = {}) {
13857
13882
 
13858
13883
  // src/host.ts
13859
13884
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
13860
- import { homedir as homedir5 } from "os";
13861
- import { join as join5 } from "path";
13885
+ import { homedir as homedir6 } from "os";
13886
+ import { join as join6 } from "path";
13862
13887
 
13863
13888
  // src/workspaces.ts
13864
13889
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -13903,6 +13928,62 @@ function resolveWakeDir(pinned, deps) {
13903
13928
  return listWorkspaces(deps)[0];
13904
13929
  }
13905
13930
 
13931
+ // src/owner.ts
13932
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
13933
+ import { homedir as homedir4 } from "os";
13934
+ import { join as join4 } from "path";
13935
+ var HEAD_BYTES = 64e3;
13936
+ function readHead(file) {
13937
+ const fd = openSync2(file, "r");
13938
+ try {
13939
+ const buf = Buffer.alloc(HEAD_BYTES);
13940
+ const n = readSync(fd, buf, 0, HEAD_BYTES, 0);
13941
+ return buf.subarray(0, n).toString("utf8");
13942
+ } finally {
13943
+ closeSync2(fd);
13944
+ }
13945
+ }
13946
+ function ownerSession(slot, deps = {}) {
13947
+ const m = /^session:([0-9a-f]{8})$/.exec(slot);
13948
+ if (!m) return null;
13949
+ const prefix = m[1];
13950
+ const root = deps.projectsDir ?? join4(homedir4(), ".claude", "projects");
13951
+ const list = deps.list ?? ((dir) => readdirSync(dir));
13952
+ const head = deps.head ?? readHead;
13953
+ let folders;
13954
+ try {
13955
+ folders = list(root);
13956
+ } catch {
13957
+ return null;
13958
+ }
13959
+ for (const folder of folders) {
13960
+ let files;
13961
+ try {
13962
+ files = list(join4(root, folder));
13963
+ } catch {
13964
+ continue;
13965
+ }
13966
+ const file = files.find((f) => f.startsWith(prefix) && f.endsWith(".jsonl"));
13967
+ if (!file) continue;
13968
+ let text;
13969
+ try {
13970
+ text = head(join4(root, folder, file));
13971
+ } catch {
13972
+ continue;
13973
+ }
13974
+ for (const line of text.split("\n")) {
13975
+ try {
13976
+ const row = JSON.parse(line);
13977
+ if (typeof row.cwd === "string" && typeof row.sessionId === "string" && row.sessionId.startsWith(prefix)) {
13978
+ return { sessionId: row.sessionId, cwd: row.cwd };
13979
+ }
13980
+ } catch {
13981
+ }
13982
+ }
13983
+ }
13984
+ return null;
13985
+ }
13986
+
13906
13987
  // ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
13907
13988
  var ignoreOverride2 = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
13908
13989
  var defaultOptions2 = {
@@ -15247,6 +15328,9 @@ var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
15247
15328
  var CONTACT_DESCRIPTION2 = "Contact the user about exactly one existing Goal: pass goalIds:[goalId], ask, channel:'notification'|'call', and waiting:'none'|'hard'. Options supply choices. Notification returns immediately; collect durable answers with claim_goal/get_goal. On stdio, a Call holds one cancellable ~45s window; continue with ONLY {deliveryId}. Continuation sends nothing and rereads the same durable evidence, including previously read answers. Entries retain authorship and provenance; accepted decisions are separate from quoted speech. Call state open does not mean ringing. Unsupported: soft waiting, multiple Goals/questions, outcome admission, and re-presentation of an existing request. Create a Goal explicitly first; never resend a pending ask to continue waiting.";
15248
15329
  var CreateGoalSchema2 = external_exports.object({
15249
15330
  outcome: external_exports.string().trim().min(1).max(1e4),
15331
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud ("the night
15332
+ * rings"). Omit it and the brain writes one at admission from the outcome. */
15333
+ title: external_exports.string().trim().min(1).max(80).optional(),
15250
15334
  ownerParticipant: external_exports.string().trim().min(1).optional(),
15251
15335
  idempotencyKey: external_exports.string().trim().min(1).max(200),
15252
15336
  /** A past conversation this Goal should be read against — History's "new session from this"
@@ -15266,11 +15350,14 @@ var CreateGoalSchema2 = external_exports.object({
15266
15350
  var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
15267
15351
  idempotencyKey: CreateGoalSchema2.shape.idempotencyKey.optional().describe("Optional. One is minted per call; pass your own only so a retry lands on the same Goal.")
15268
15352
  }).strict();
15269
- 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. 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.";
15353
+ 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 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.';
15270
15354
  var UpdateGoalSchema2 = external_exports.object({
15271
15355
  revision: external_exports.number().int().positive(),
15272
15356
  changes: external_exports.object({
15273
15357
  outcome: external_exports.string().trim().min(1).max(1e4).optional(),
15358
+ /** The work's NAME (#2115) — one to five words, how a person refers to it out loud. The
15359
+ * brain writes one at admission; this is the owner saying it better. Null clears it. */
15360
+ title: external_exports.string().trim().min(1).max(80).nullable().optional(),
15274
15361
  ownerParticipant: external_exports.string().trim().min(1).optional(),
15275
15362
  parentGoalId: external_exports.string().uuid().nullable().optional(),
15276
15363
  dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
@@ -15287,7 +15374,7 @@ var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend
15287
15374
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
15288
15375
  var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
15289
15376
  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), and `next`, the one step to take. Foreign or sibling-owned Goals are not disclosed.";
15290
- var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, and review acknowledgement are explicit; stale revisions are rejected. 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. Returns the Goal as get_goal reads it, at its new revision.";
15377
+ var UPDATE_GOAL_DESCRIPTION2 = "Update an owned Goal at an exact revision. State, ownership, dependencies, children, progress, title, and review acknowledgement are explicit; stale revisions are rejected. title is the work's name in one to five words, as a person would refer to it out loud (it is spoken on a call and heads every list); null clears it. reviewed: true acknowledges new evidence and closes the Deliveries addressed to you on that Goal, never over an open decision. dueAt (an ISO instant, or null) makes the Goal wait until then; when it passes you are woken for it \u2014 use it for a promise to follow up later. Returns the Goal as get_goal reads it, at its new revision.";
15291
15378
  var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
15292
15379
  var CHECK_REPLIES_DESCRIPTION2 = "Your open Deliveries: every Notification or Call currently addressed to you \u2014 a request the user started toward you, an answer relayed to something you asked, a handoff \u2014 one row each: its Goals, how many decisions are still open, and the newest words in brief. 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 it in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true closes the Deliveries addressed to you on that Goal. Your runnable and review-pending Goals come from claim_goal, not from here.";
15293
15380
  var CheckRepliesSchema2 = external_exports.object({}).strict();
@@ -15325,6 +15412,7 @@ function entryWords(entry) {
15325
15412
  }
15326
15413
  return entry.sources.map((source) => source.text).join("\n");
15327
15414
  }
15415
+ var LIVE_MS2 = 3 * 6e4;
15328
15416
  var ContextSchema2 = external_exports.object({
15329
15417
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
15330
15418
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -16106,7 +16194,19 @@ var QueueQuestionSchema2 = external_exports.object({
16106
16194
  /** The ruling in the person's own words, from the contribution that replied — not the
16107
16195
  * option id, which is not something anyone reads back. Null while it is open, and null
16108
16196
  * for a settled question whose reply carried nothing readable. */
16109
- answer: external_exports.string().nullable().default(null)
16197
+ answer: external_exports.string().nullable().default(null),
16198
+ /** The Goal this question belongs to — a step knows its Goal on its own, not only through
16199
+ * an `InboxItem`'s `communication.goalIds[0]` (walk/design.md §12 item 3).
16200
+ * READ BY `apps/client/src/walk/order.ts`, which stamps it onto every `WalkStep`: the walk's
16201
+ * order, its route, home's trees and the list of steps all take a step's Goal from here, so
16202
+ * this is the field they agree through rather than each re-deriving it from the row it
16203
+ * arrived under. Required because the API projects it on every need it sends. */
16204
+ goalId: external_exports.string(),
16205
+ /** True only while an unmet START gate holds the Goal — a Goal that merely waits to
16206
+ * *finish* does not stop a person from answering (owner, 2026-09-16: "per need gate from
16207
+ * the API"; §4's dashed node). Not the same fact as `QueueItem.blocked`, which counts any
16208
+ * gate at all. */
16209
+ blocked: external_exports.boolean().default(false)
16110
16210
  });
16111
16211
  var QueueItemSchema2 = external_exports.object({
16112
16212
  id: external_exports.string(),
@@ -16395,7 +16495,7 @@ function sameTail(a, b) {
16395
16495
  // src/harness/session.ts
16396
16496
  import { spawn } from "child_process";
16397
16497
  import { existsSync as existsSync4 } from "fs";
16398
- import { homedir as homedir4 } from "os";
16498
+ import { homedir as homedir5 } from "os";
16399
16499
  import { resolve as resolve2, delimiter as delimiter2 } from "path";
16400
16500
 
16401
16501
  // src/harness/acp.ts
@@ -16405,15 +16505,19 @@ function optionFor(options, decision) {
16405
16505
  return options.find((o) => o.kind === want)?.optionId ?? null;
16406
16506
  }
16407
16507
  function createAcpDriver(opts) {
16408
- return new AcpDriver(opts.cwd, opts.mode);
16508
+ return new AcpDriver(opts.cwd, opts.mode, opts.resume);
16409
16509
  }
16410
16510
  var AcpDriver = class {
16411
- constructor(cwd, mode) {
16511
+ /** `resume`: the id of an existing session to continue rather than open a new one (#1637)
16512
+ * the session a bound identity belongs to, so the process this drives IS its holder. */
16513
+ constructor(cwd, mode, resume) {
16412
16514
  this.cwd = cwd;
16413
16515
  this.mode = mode;
16516
+ this.resume = resume;
16414
16517
  }
16415
16518
  cwd;
16416
16519
  mode;
16520
+ resume;
16417
16521
  nextId = 1;
16418
16522
  initId;
16419
16523
  sessionNewId;
@@ -16497,15 +16601,15 @@ var AcpDriver = class {
16497
16601
  this.sessionNewId = this.nextId++;
16498
16602
  return {
16499
16603
  events: [],
16500
- writes: [frame2({ id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
16604
+ writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : { id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
16501
16605
  };
16502
16606
  }
16503
16607
  if (msg.id === this.sessionNewId) {
16504
16608
  this.sessionNewId = void 0;
16505
- const sessionId2 = msg.result?.sessionId;
16609
+ const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
16506
16610
  if (!sessionId2) {
16507
16611
  return {
16508
- events: [{ kind: "error", message: `session/new failed: ${msg.error?.message ?? "no sessionId"}` }],
16612
+ events: [{ kind: "error", message: `${this.resume ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}` }],
16509
16613
  writes: []
16510
16614
  };
16511
16615
  }
@@ -16628,7 +16732,7 @@ function splitLines(buffer, chunk) {
16628
16732
  return { lines: parts.filter((l) => l.trim()), rest };
16629
16733
  }
16630
16734
  function startSession(opts) {
16631
- const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
16735
+ const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir5()));
16632
16736
  if (!existsSync4(cwd)) {
16633
16737
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
16634
16738
  }
@@ -16641,7 +16745,8 @@ function startSession(opts) {
16641
16745
  env: {
16642
16746
  ...process.env,
16643
16747
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
16644
- ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
16748
+ ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
16749
+ ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
16645
16750
  },
16646
16751
  stdio: ["pipe", "pipe", "pipe"]
16647
16752
  });
@@ -16661,7 +16766,7 @@ function startSession(opts) {
16661
16766
  opts.onEvent({ kind: "error", message: e.message });
16662
16767
  opts.onExit?.();
16663
16768
  });
16664
- const driver = createAcpDriver({ cwd, mode: opts.mode });
16769
+ const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.resume ? { resume: opts.resume } : {} });
16665
16770
  let buffer = "";
16666
16771
  child.stdout?.on("data", (chunk) => {
16667
16772
  const { lines: lines2, rest } = splitLines(buffer, String(chunk));
@@ -16723,7 +16828,7 @@ async function step(session, state, rail) {
16723
16828
  else state.goal = { id: brief2.goalId, revision: brief2.revision };
16724
16829
  const outcome = brief2.outcome?.trim() ?? "";
16725
16830
  const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
16726
- send(session, state, [outcome, ...said].filter(Boolean).join("\n\n"));
16831
+ send2(session, state, [outcome, ...said].filter(Boolean).join("\n\n"));
16727
16832
  return;
16728
16833
  }
16729
16834
  const brief = await rail.read(state.goal.id);
@@ -16742,9 +16847,9 @@ async function step(session, state, rail) {
16742
16847
  }
16743
16848
  if (state.asks?.size || !state.resting) return;
16744
16849
  const words2 = unheard(brief, state);
16745
- if (words2.length) send(session, state, words2.join("\n\n"));
16850
+ if (words2.length) send2(session, state, words2.join("\n\n"));
16746
16851
  }
16747
- function send(session, state, text) {
16852
+ function send2(session, state, text) {
16748
16853
  state.resting = false;
16749
16854
  session.send(text);
16750
16855
  }
@@ -16877,7 +16982,7 @@ function runHarness(opts) {
16877
16982
  let tail = [];
16878
16983
  const state = { resting: true };
16879
16984
  let session = null;
16880
- const rail = railFor({ token: opts.token });
16985
+ const rail = railFor({ token: opts.token, ...opts.resume ? { reach: reachAs(opts.resume) } : {} });
16881
16986
  async function handle(event) {
16882
16987
  if (event.kind === "error") {
16883
16988
  opts.log(`\u26A0 ${event.message}`);
@@ -16925,6 +17030,7 @@ function runHarness(opts) {
16925
17030
  cwd: opts.cwd,
16926
17031
  mode: opts.mode,
16927
17032
  ...opts.token ? { token: opts.token } : {},
17033
+ ...opts.resume ? { resume: opts.resume } : {},
16928
17034
  ...opts.bin ? { bin: opts.bin } : {},
16929
17035
  ...opts.onExit ? { onExit: opts.onExit } : {},
16930
17036
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
@@ -16957,7 +17063,7 @@ function runHarness(opts) {
16957
17063
  // src/update.ts
16958
17064
  import { execFile } from "child_process";
16959
17065
  import { createRequire } from "module";
16960
- import { dirname as dirname3, join as join4 } from "path";
17066
+ import { dirname as dirname3, join as join5 } from "path";
16961
17067
  import { promisify } from "util";
16962
17068
  var SERVICE_LABEL = "ai.paigy.harness";
16963
17069
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -16970,9 +17076,13 @@ function newer(latest, current) {
16970
17076
  for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
16971
17077
  return false;
16972
17078
  }
17079
+ var WAIT_CAP_MS = 6 * 36e5;
16973
17080
  function updateAction(o) {
16974
17081
  if (!o.service || !o.latest || o.latest === o.failed || !newer(o.latest, o.current)) return "none";
16975
- return o.sessions > 0 ? "wait" : "install";
17082
+ if (o.sessions <= 0) return "install";
17083
+ const working = o.working ?? o.sessions;
17084
+ if (working > 0) return "wait";
17085
+ return (o.waited ?? 0) >= WAIT_CAP_MS ? "install" : "wait";
16976
17086
  }
16977
17087
  async function latestVersion() {
16978
17088
  const res = await fetch(REGISTRY, { signal: AbortSignal.timeout(1e4) });
@@ -16980,7 +17090,7 @@ async function latestVersion() {
16980
17090
  }
16981
17091
  async function install(version) {
16982
17092
  const bin = dirname3(process.execPath);
16983
- await promisify(execFile)(join4(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
17093
+ await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
16984
17094
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
16985
17095
  timeout: 5 * 6e4
16986
17096
  });
@@ -16991,6 +17101,7 @@ function selfUpdater(deps) {
16991
17101
  let checkedAt = -Infinity;
16992
17102
  let failed;
16993
17103
  let waiting = false;
17104
+ let waitingSince = null;
16994
17105
  return async () => {
16995
17106
  if (!service) return;
16996
17107
  const now = (deps.now ?? Date.now)();
@@ -16998,9 +17109,25 @@ function selfUpdater(deps) {
16998
17109
  checkedAt = now;
16999
17110
  latest = await (deps.latest ?? latestVersion)().catch(() => latest);
17000
17111
  }
17001
- const action = updateAction({ current: VERSION, latest, service, sessions: deps.sessions(), failed });
17002
- if (action === "wait" && !waiting) deps.log(`\u21E1 @paigy/harness ${latest} is out \u2014 updating once no session is running`);
17112
+ const sessions = deps.sessions();
17113
+ const working = deps.working?.();
17114
+ if (latest && newer(latest, VERSION) && latest !== failed && waitingSince === null) waitingSince = now;
17115
+ const action = updateAction({
17116
+ current: VERSION,
17117
+ latest,
17118
+ service,
17119
+ sessions,
17120
+ failed,
17121
+ ...working === void 0 ? {} : { working },
17122
+ waited: waitingSince === null ? 0 : now - waitingSince
17123
+ });
17124
+ if (action === "wait" && !waiting) {
17125
+ deps.log(working ? `\u21E1 @paigy/harness ${latest} is out \u2014 updating once the turn in flight finishes` : `\u21E1 @paigy/harness ${latest} is out \u2014 updating once no session is running (at most ${Math.round(WAIT_CAP_MS / 36e5)}h)`);
17126
+ }
17003
17127
  waiting = action === "wait";
17128
+ if (action === "install" && sessions > 0) {
17129
+ deps.log(`\u21E1 @paigy/harness ${latest} is out and ${sessions} resting session(s) have held it for ${Math.round((now - (waitingSince ?? now)) / 36e5)}h \u2014 restarting over them`);
17130
+ }
17004
17131
  if (action !== "install" || !latest) return;
17005
17132
  try {
17006
17133
  await (deps.install ?? install)(latest);
@@ -17014,7 +17141,7 @@ function selfUpdater(deps) {
17014
17141
  }
17015
17142
 
17016
17143
  // src/host.ts
17017
- var HOST_FILE = join5(homedir5(), ".paigy", "host.json");
17144
+ var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
17018
17145
  function readHostState() {
17019
17146
  try {
17020
17147
  const s = JSON.parse(readFileSync3(HOST_FILE, "utf8"));
@@ -17038,8 +17165,8 @@ var claimedRun = (sessionId2) => {
17038
17165
  const slot = sessionSlot(sessionId2);
17039
17166
  return { slot, key: runKey(slot) };
17040
17167
  };
17041
- async function slotHasWaitingWork(token) {
17042
- return (await checkReplies({ token })).deliveries.length > 0;
17168
+ async function slotHasWaitingWork(token, session) {
17169
+ return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
17043
17170
  }
17044
17171
  var PRESENCE_FRESH_MS = 3 * 6e4;
17045
17172
  function wakeAction(live, now, serverSeenAt) {
@@ -17063,7 +17190,12 @@ function startHost(opts) {
17063
17190
  }
17064
17191
  }
17065
17192
  };
17066
- const update = selfUpdater({ sessions: () => runs.size, log: opts.log });
17193
+ const update = selfUpdater({
17194
+ sessions: () => runs.size,
17195
+ // A turn in flight blocks the update; a session merely standing by does not hold it forever.
17196
+ working: () => [...runs.values()].filter((r) => r.run.working()).length,
17197
+ log: opts.log
17198
+ });
17067
17199
  const beat = () => {
17068
17200
  void refreshIdentities();
17069
17201
  void update().catch(() => {
@@ -17132,9 +17264,11 @@ function startHost(opts) {
17132
17264
  if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
17133
17265
  const token = readToken(slot);
17134
17266
  if (!token) return;
17135
- const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
17267
+ const owner = ownerSession(slot);
17268
+ if (owner && !allowed(owner.cwd, opts.wsDeps)) return;
17269
+ const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
17136
17270
  if (!workspace) return;
17137
- if (!await slotHasWaitingWork(token).catch(() => false)) return;
17271
+ if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
17138
17272
  const label = slotName(slot) ?? slot;
17139
17273
  const log2 = (line) => opts.log(`[${label}] ${line}`);
17140
17274
  const run = runHarness({
@@ -17143,6 +17277,7 @@ function startHost(opts) {
17143
17277
  mode: "bypass",
17144
17278
  prompt: "",
17145
17279
  token,
17280
+ ...owner ? { resume: owner.sessionId } : {},
17146
17281
  // A dead run must not squat the slot — evict so the next wake can respawn.
17147
17282
  onExit: () => {
17148
17283
  runs.delete(key);
@@ -17153,7 +17288,7 @@ function startHost(opts) {
17153
17288
  runs.set(key, { run, label, token });
17154
17289
  void heartbeat(void 0, { token }).catch(() => {
17155
17290
  });
17156
- opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
17291
+ opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
17157
17292
  }
17158
17293
  }
17159
17294
  const listening = /* @__PURE__ */ new Map();
@@ -17462,7 +17597,7 @@ function staleWhy(note) {
17462
17597
 
17463
17598
  // src/triage/judge.ts
17464
17599
  import { spawnSync as spawnSync2 } from "child_process";
17465
- import { homedir as homedir6 } from "os";
17600
+ import { homedir as homedir7 } from "os";
17466
17601
  import { delimiter as delimiter3 } from "path";
17467
17602
  var PRIVACY = {
17468
17603
  ollama: "never leaves this machine",
@@ -17566,7 +17701,7 @@ async function judgeFor(asked, deps = {}) {
17566
17701
  const out = spawnSync2(bin, runner.argv(null), {
17567
17702
  input: prompt,
17568
17703
  encoding: "utf8",
17569
- cwd: homedir6(),
17704
+ cwd: homedir7(),
17570
17705
  timeout: RUN_TIMEOUT_MS,
17571
17706
  maxBuffer: 32 * 1024 * 1024,
17572
17707
  env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
@@ -17864,7 +17999,7 @@ var win = null;
17864
17999
  function log(message) {
17865
18000
  win?.webContents.send("paigy:log", message);
17866
18001
  }
17867
- var ICON = join6(here, "..", "assets", "icon.png");
18002
+ var ICON = join7(here, "..", "assets", "icon.png");
17868
18003
  function createWindow() {
17869
18004
  win = new BrowserWindow({
17870
18005
  // Sized for the working log, which is now the window's centerpiece; the layout is a
@@ -17876,9 +18011,9 @@ function createWindow() {
17876
18011
  minHeight: 520,
17877
18012
  icon: ICON,
17878
18013
  // Windows/Linux window icon; macOS uses the dock icon below
17879
- webPreferences: { preload: join6(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
18014
+ webPreferences: { preload: join7(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
17880
18015
  });
17881
- void win.loadFile(join6(here, "..", "renderer", "index.html"));
18016
+ void win.loadFile(join7(here, "..", "renderer", "index.html"));
17882
18017
  }
17883
18018
  var host = null;
17884
18019
  app.whenReady().then(() => {
@@ -17947,7 +18082,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
17947
18082
  });
17948
18083
  ipcMain.handle("paigy:agent-log", (_e, name) => {
17949
18084
  try {
17950
- const all = readFileSync4(join6(homedir7(), ".paigy", "host.log"), "utf8").split("\n");
18085
+ const all = readFileSync4(join7(homedir8(), ".paigy", "host.log"), "utf8").split("\n");
17951
18086
  const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
17952
18087
  return mine.slice(-500).join("\n");
17953
18088
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "Run Claude Code / Codex on this machine, bridged to Paigy — launchable from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",