@paigy/harness 0.3.11 → 0.3.12

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 +156 -44
  2. package/dist/main.js +150 -41
  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;
@@ -36269,7 +36378,7 @@ async function latestVersion() {
36269
36378
  }
36270
36379
  async function install(version) {
36271
36380
  const bin = dirname3(process.execPath);
36272
- await promisify(execFile)(join4(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
36381
+ await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
36273
36382
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
36274
36383
  timeout: 5 * 6e4
36275
36384
  });
@@ -36303,14 +36412,14 @@ function selfUpdater(deps) {
36303
36412
  }
36304
36413
 
36305
36414
  // src/host.ts
36306
- var HOST_FILE = join5(homedir5(), ".paigy", "host.json");
36415
+ var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
36307
36416
  var runKey = (slot) => `slot:${slot}`;
36308
36417
  var claimedRun = (sessionId2) => {
36309
36418
  const slot = sessionSlot(sessionId2);
36310
36419
  return { slot, key: runKey(slot) };
36311
36420
  };
36312
- async function slotHasWaitingWork(token) {
36313
- return (await checkReplies({ token })).deliveries.length > 0;
36421
+ async function slotHasWaitingWork(token, session) {
36422
+ return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
36314
36423
  }
36315
36424
  var PRESENCE_FRESH_MS = 3 * 6e4;
36316
36425
  function wakeAction(live, now, serverSeenAt) {
@@ -36403,9 +36512,11 @@ function startHost(opts) {
36403
36512
  if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
36404
36513
  const token = readToken(slot);
36405
36514
  if (!token) return;
36406
- const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
36515
+ const owner = ownerSession(slot);
36516
+ if (owner && !allowed(owner.cwd, opts.wsDeps)) return;
36517
+ const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
36407
36518
  if (!workspace) return;
36408
- if (!await slotHasWaitingWork(token).catch(() => false)) return;
36519
+ if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
36409
36520
  const label = slotName(slot) ?? slot;
36410
36521
  const log = (line) => opts.log(`[${label}] ${line}`);
36411
36522
  const run = runHarness({
@@ -36414,6 +36525,7 @@ function startHost(opts) {
36414
36525
  mode: "bypass",
36415
36526
  prompt: "",
36416
36527
  token,
36528
+ ...owner ? { resume: owner.sessionId } : {},
36417
36529
  // A dead run must not squat the slot — evict so the next wake can respawn.
36418
36530
  onExit: () => {
36419
36531
  runs.delete(key);
@@ -36424,7 +36536,7 @@ function startHost(opts) {
36424
36536
  runs.set(key, { run, label, token });
36425
36537
  void heartbeat(void 0, { token }).catch(() => {
36426
36538
  });
36427
- opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
36539
+ opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
36428
36540
  }
36429
36541
  }
36430
36542
  const listening = /* @__PURE__ */ new Map();
@@ -36656,7 +36768,7 @@ async function pairThisMachine() {
36656
36768
  }
36657
36769
  }
36658
36770
  function forwardCodexThreadId() {
36659
- const cfg = join6(process.env.CODEX_HOME || join6(homedir7(), ".codex"), "config.toml");
36771
+ const cfg = join7(process.env.CODEX_HOME || join7(homedir8(), ".codex"), "config.toml");
36660
36772
  if (!existsSync5(cfg)) return;
36661
36773
  const text = readFileSync4(cfg, "utf8");
36662
36774
  const head = "[mcp_servers.paigy]\n";
@@ -36758,7 +36870,7 @@ async function main() {
36758
36870
  const wsDeps = { file: workspacesFile() };
36759
36871
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
36760
36872
  if (!listWorkspaces(wsDeps).length) {
36761
- const guess = join6(homedir7(), "projects");
36873
+ const guess = join7(homedir8(), "projects");
36762
36874
  if (existsSync5(guess)) {
36763
36875
  addWorkspace(guess, wsDeps);
36764
36876
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -36799,7 +36911,7 @@ async function main() {
36799
36911
  }
36800
36912
  }
36801
36913
  try {
36802
- const settings = join6(homedir7(), ".claude", "settings.json");
36914
+ const settings = join7(homedir8(), ".claude", "settings.json");
36803
36915
  if (which("claude") && existsSync5(settings)) {
36804
36916
  const cfg = JSON.parse(readFileSync4(settings, "utf8"));
36805
36917
  if (!cfg["statusLine"]) {
@@ -36857,13 +36969,13 @@ async function main() {
36857
36969
  <key>SoftResourceLimits</key><dict>
36858
36970
  <key>NumberOfFiles</key><integer>65536</integer>
36859
36971
  </dict>
36860
- <key>StandardOutPath</key><string>${join6(homedir7(), ".paigy", "host.log")}</string>
36861
- <key>StandardErrorPath</key><string>${join6(homedir7(), ".paigy", "host.log")}</string>
36972
+ <key>StandardOutPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
36973
+ <key>StandardErrorPath</key><string>${join7(homedir8(), ".paigy", "host.log")}</string>
36862
36974
  </dict></plist>
36863
36975
  `;
36864
- const dir = join6(homedir7(), "Library", "LaunchAgents");
36976
+ const dir = join7(homedir8(), "Library", "LaunchAgents");
36865
36977
  mkdirSync3(dir, { recursive: true });
36866
- const path = join6(dir, `${SERVICE_LABEL}.plist`);
36978
+ const path = join7(dir, `${SERVICE_LABEL}.plist`);
36867
36979
  writeFileSync4(path, plist);
36868
36980
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
36869
36981
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
@@ -36886,7 +36998,7 @@ async function main() {
36886
36998
  process.exit(1);
36887
36999
  }
36888
37000
  const name = slotName(slot) ?? slot;
36889
- console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir7(), "~")}`);
37001
+ console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir8(), "~")}`);
36890
37002
  if (!allowed(cwd, { file: workspacesFile() })) {
36891
37003
  console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
36892
37004
  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;
@@ -16980,7 +17086,7 @@ async function latestVersion() {
16980
17086
  }
16981
17087
  async function install(version) {
16982
17088
  const bin = dirname3(process.execPath);
16983
- await promisify(execFile)(join4(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
17089
+ await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
16984
17090
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
16985
17091
  timeout: 5 * 6e4
16986
17092
  });
@@ -17014,7 +17120,7 @@ function selfUpdater(deps) {
17014
17120
  }
17015
17121
 
17016
17122
  // src/host.ts
17017
- var HOST_FILE = join5(homedir5(), ".paigy", "host.json");
17123
+ var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
17018
17124
  function readHostState() {
17019
17125
  try {
17020
17126
  const s = JSON.parse(readFileSync3(HOST_FILE, "utf8"));
@@ -17038,8 +17144,8 @@ var claimedRun = (sessionId2) => {
17038
17144
  const slot = sessionSlot(sessionId2);
17039
17145
  return { slot, key: runKey(slot) };
17040
17146
  };
17041
- async function slotHasWaitingWork(token) {
17042
- return (await checkReplies({ token })).deliveries.length > 0;
17147
+ async function slotHasWaitingWork(token, session) {
17148
+ return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
17043
17149
  }
17044
17150
  var PRESENCE_FRESH_MS = 3 * 6e4;
17045
17151
  function wakeAction(live, now, serverSeenAt) {
@@ -17132,9 +17238,11 @@ function startHost(opts) {
17132
17238
  if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
17133
17239
  const token = readToken(slot);
17134
17240
  if (!token) return;
17135
- const workspace = resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
17241
+ const owner = ownerSession(slot);
17242
+ if (owner && !allowed(owner.cwd, opts.wsDeps)) return;
17243
+ const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
17136
17244
  if (!workspace) return;
17137
- if (!await slotHasWaitingWork(token).catch(() => false)) return;
17245
+ if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
17138
17246
  const label = slotName(slot) ?? slot;
17139
17247
  const log2 = (line) => opts.log(`[${label}] ${line}`);
17140
17248
  const run = runHarness({
@@ -17143,6 +17251,7 @@ function startHost(opts) {
17143
17251
  mode: "bypass",
17144
17252
  prompt: "",
17145
17253
  token,
17254
+ ...owner ? { resume: owner.sessionId } : {},
17146
17255
  // A dead run must not squat the slot — evict so the next wake can respawn.
17147
17256
  onExit: () => {
17148
17257
  runs.delete(key);
@@ -17153,7 +17262,7 @@ function startHost(opts) {
17153
17262
  runs.set(key, { run, label, token });
17154
17263
  void heartbeat(void 0, { token }).catch(() => {
17155
17264
  });
17156
- opts.log(`\u25B6 woke ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
17265
+ opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
17157
17266
  }
17158
17267
  }
17159
17268
  const listening = /* @__PURE__ */ new Map();
@@ -17462,7 +17571,7 @@ function staleWhy(note) {
17462
17571
 
17463
17572
  // src/triage/judge.ts
17464
17573
  import { spawnSync as spawnSync2 } from "child_process";
17465
- import { homedir as homedir6 } from "os";
17574
+ import { homedir as homedir7 } from "os";
17466
17575
  import { delimiter as delimiter3 } from "path";
17467
17576
  var PRIVACY = {
17468
17577
  ollama: "never leaves this machine",
@@ -17566,7 +17675,7 @@ async function judgeFor(asked, deps = {}) {
17566
17675
  const out = spawnSync2(bin, runner.argv(null), {
17567
17676
  input: prompt,
17568
17677
  encoding: "utf8",
17569
- cwd: homedir6(),
17678
+ cwd: homedir7(),
17570
17679
  timeout: RUN_TIMEOUT_MS,
17571
17680
  maxBuffer: 32 * 1024 * 1024,
17572
17681
  env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
@@ -17864,7 +17973,7 @@ var win = null;
17864
17973
  function log(message) {
17865
17974
  win?.webContents.send("paigy:log", message);
17866
17975
  }
17867
- var ICON = join6(here, "..", "assets", "icon.png");
17976
+ var ICON = join7(here, "..", "assets", "icon.png");
17868
17977
  function createWindow() {
17869
17978
  win = new BrowserWindow({
17870
17979
  // Sized for the working log, which is now the window's centerpiece; the layout is a
@@ -17876,9 +17985,9 @@ function createWindow() {
17876
17985
  minHeight: 520,
17877
17986
  icon: ICON,
17878
17987
  // Windows/Linux window icon; macOS uses the dock icon below
17879
- webPreferences: { preload: join6(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
17988
+ webPreferences: { preload: join7(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
17880
17989
  });
17881
- void win.loadFile(join6(here, "..", "renderer", "index.html"));
17990
+ void win.loadFile(join7(here, "..", "renderer", "index.html"));
17882
17991
  }
17883
17992
  var host = null;
17884
17993
  app.whenReady().then(() => {
@@ -17947,7 +18056,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
17947
18056
  });
17948
18057
  ipcMain.handle("paigy:agent-log", (_e, name) => {
17949
18058
  try {
17950
- const all = readFileSync4(join6(homedir7(), ".paigy", "host.log"), "utf8").split("\n");
18059
+ const all = readFileSync4(join7(homedir8(), ".paigy", "host.log"), "utf8").split("\n");
17951
18060
  const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
17952
18061
  return mine.slice(-500).join("\n");
17953
18062
  } 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.12",
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",