@paigy/harness 0.3.10 → 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.
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(),
@@ -12234,10 +12246,10 @@ var UpdateGoalSchema = external_exports.object({
12234
12246
  var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
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
- var GET_GOAL_DESCRIPTION = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. 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 new revision and a prose summary.";
12239
- var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
12240
- 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, the newest words in brief, accepted decisions and open decision needs, without the Entries. 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.";
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.";
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.";
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.";
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();
12242
12254
  var GetThreadSchema = external_exports.object({
12243
12255
  parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
@@ -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(),
@@ -13233,25 +13258,6 @@ var DeliveryConfigSchema = external_exports.object({
13233
13258
  * carries credentials; only a `poll` registration can come back with null here. */
13234
13259
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
13235
13260
  });
13236
- var StatusSchema = external_exports.object({
13237
- name: external_exports.string(),
13238
- sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
13239
- /** A phone is registered for push/ring (any push token on the account). */
13240
- phone: external_exports.boolean(),
13241
- /** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
13242
- * it never picked up. THE SAME NUMBER the harness's wake gate reads
13243
- * (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
13244
- * zero while the sweep sees one is two ideas of "waiting".
13245
- *
13246
- * Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
13247
- * that nobody spawned, so a terminal session only learns of work by asking. The harness
13248
- * used to paper over that by spawning a SECOND process on the identity; now it stands
13249
- * back, correctly, and the person sitting at the terminal is the one who can act. A
13250
- * coffee-beans request sat unread for three days.
13251
- *
13252
- * Optional: an older API sends no field, and the statusline then renders exactly as before. */
13253
- waiting: external_exports.number().int().nonnegative().optional()
13254
- });
13255
13261
  var EnvelopeRecipientSchema = external_exports.object({
13256
13262
  keyId: external_exports.string(),
13257
13263
  epk: external_exports.string(),
@@ -13593,7 +13599,7 @@ async function listNotes(opts = {}) {
13593
13599
  }
13594
13600
  async function listConnections(opts = {}) {
13595
13601
  const res = ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
13596
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
13602
+ headers: { authorization: `Bearer ${authToken(opts.token)}`, "x-paigy-model": "goal-entry-v1" }
13597
13603
  }));
13598
13604
  if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
13599
13605
  return await res.json();
@@ -13634,14 +13640,14 @@ async function dismissTriage(proposalId, opts = {}) {
13634
13640
  async function contact(input, opts = {}) {
13635
13641
  const parsed = ContactSchema.parse(input);
13636
13642
  opts.signal?.throwIfAborted();
13637
- const send2 = opts.reach ?? reach;
13643
+ const send22 = opts.reach ?? reach;
13638
13644
  const headers = { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token) ?? ""}`, "x-paigy-model": "goal-entry-v1" };
13639
13645
  let deliveryId;
13640
13646
  if ("deliveryId" in parsed) deliveryId = parsed.deliveryId;
13641
13647
  else {
13642
13648
  if (!opts.reach && readKeyFile()?.e2ee) throw new Error("Goal contact does not support E2EE yet; refusing to send plaintext.");
13643
13649
  const shaped = deriveAsk(NotifyRequestSchema.parse({ ask: parsed.ask, waiting: parsed.waiting, options: parsed.options }));
13644
- 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`, {
13645
13651
  method: "POST",
13646
13652
  headers,
13647
13653
  signal: opts.signal,
@@ -13653,11 +13659,9 @@ async function contact(input, opts = {}) {
13653
13659
  if (!deliveryId) throw new Error("Goal contact returned no Delivery identity");
13654
13660
  }
13655
13661
  const read = async (signal2) => {
13656
- 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 }));
13657
13663
  if (!res.ok) await fail("read_delivery", res);
13658
- const delivery = await res.json();
13659
- return delivery.kind === "notification" ? { ...delivery, message: `${delivery.message}
13660
- Inbox delivery is asynchronous; use claim_goal/get_goal to collect durable answers. Do not poll this Notification.` } : delivery;
13664
+ return await res.json();
13661
13665
  };
13662
13666
  const settled = (d) => d.kind === "notification" || d.state === "closed" || d.answers.length > 0 || d.entries.some((e) => e.kind === "contribution");
13663
13667
  if (opts.waits === false) return read(opts.signal);
@@ -13784,7 +13788,7 @@ async function subscribeWake(onNudge, opts = {}) {
13784
13788
 
13785
13789
  // src/main.ts
13786
13790
  var import_qrcode = __toESM(require_lib(), 1);
13787
- import { homedir as homedir7, hostname } from "os";
13791
+ import { homedir as homedir8, hostname } from "os";
13788
13792
 
13789
13793
  // src/harness/catalog.ts
13790
13794
  import { spawnSync } from "child_process";
@@ -13816,6 +13820,8 @@ var CATALOG = [
13816
13820
  name: "agy",
13817
13821
  label: "Antigravity",
13818
13822
  cli: "agy",
13823
+ // `agy` has no ACP mode; its adapter ships inside the harness (`harness/agy-acp.ts`).
13824
+ adapter: "paigy-agy-acp",
13819
13825
  authProbe: ["agy", "help"],
13820
13826
  loginHint: "Install and set up Antigravity (`agy`).",
13821
13827
  installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
@@ -13876,8 +13882,8 @@ function detectAll(deps = {}) {
13876
13882
 
13877
13883
  // src/host.ts
13878
13884
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
13879
- import { homedir as homedir5 } from "os";
13880
- import { join as join5 } from "path";
13885
+ import { homedir as homedir6 } from "os";
13886
+ import { join as join6 } from "path";
13881
13887
 
13882
13888
  // src/workspaces.ts
13883
13889
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -13922,6 +13928,62 @@ function resolveWakeDir(pinned, deps) {
13922
13928
  return listWorkspaces(deps)[0];
13923
13929
  }
13924
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
+
13925
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
13926
13988
  var ignoreOverride2 = /* @__PURE__ */ Symbol("Let zodToJsonSchema decide on which parser to use");
13927
13989
  var defaultOptions2 = {
@@ -15266,6 +15328,9 @@ var CONTACT_SCHEMA2 = { type: "object", ...mcpInputSchema2(ContactSchema2) };
15266
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.";
15267
15329
  var CreateGoalSchema2 = external_exports.object({
15268
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(),
15269
15334
  ownerParticipant: external_exports.string().trim().min(1).optional(),
15270
15335
  idempotencyKey: external_exports.string().trim().min(1).max(200),
15271
15336
  /** A past conversation this Goal should be read against — History's "new session from this"
@@ -15285,11 +15350,14 @@ var CreateGoalSchema2 = external_exports.object({
15285
15350
  var CreateGoalToolSchema2 = CreateGoalSchema2.extend({
15286
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.")
15287
15352
  }).strict();
15288
- 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.';
15289
15354
  var UpdateGoalSchema2 = external_exports.object({
15290
15355
  revision: external_exports.number().int().positive(),
15291
15356
  changes: external_exports.object({
15292
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(),
15293
15361
  ownerParticipant: external_exports.string().trim().min(1).optional(),
15294
15362
  parentGoalId: external_exports.string().uuid().nullable().optional(),
15295
15363
  dependencies: external_exports.array(external_exports.object({ goalId: external_exports.string().uuid(), gate: external_exports.enum(["start", "finish"]) }).strict()).optional(),
@@ -15305,10 +15373,10 @@ var UpdateGoalSchema2 = external_exports.object({
15305
15373
  var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
15306
15374
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
15307
15375
  var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
15308
- var GET_GOAL_DESCRIPTION2 = "Read the current authorized Goal brief: state, owner, blockers, open decisions, progress, and the next operation. Foreign or sibling-owned Goals are not disclosed.";
15309
- 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 new revision and a prose summary.";
15310
- var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Returns a Goal-scoped brief, current revision, blockers, and the next valid operation. Claiming creates or renews the execution lease.";
15311
- 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, the newest words in brief, accepted decisions and open decision needs, without the Entries. 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.";
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.";
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.";
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.";
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.";
15312
15380
  var CheckRepliesSchema2 = external_exports.object({}).strict();
15313
15381
  var GetThreadSchema2 = external_exports.object({
15314
15382
  parentId: external_exports.string().describe("The Thread to read \u2014 the threadId a Delivery returned, or the parentId of a search hit.")
@@ -15344,6 +15412,7 @@ function entryWords(entry) {
15344
15412
  }
15345
15413
  return entry.sources.map((source) => source.text).join("\n");
15346
15414
  }
15415
+ var LIVE_MS2 = 3 * 6e4;
15347
15416
  var ContextSchema2 = external_exports.object({
15348
15417
  title: external_exports.string().min(1).describe("One-line headline of what you need (required, non-empty)."),
15349
15418
  description: external_exports.array(external_exports.string().min(1)).describe(
@@ -16125,7 +16194,19 @@ var QueueQuestionSchema2 = external_exports.object({
16125
16194
  /** The ruling in the person's own words, from the contribution that replied — not the
16126
16195
  * option id, which is not something anyone reads back. Null while it is open, and null
16127
16196
  * for a settled question whose reply carried nothing readable. */
16128
- 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)
16129
16210
  });
16130
16211
  var QueueItemSchema2 = external_exports.object({
16131
16212
  id: external_exports.string(),
@@ -16262,25 +16343,6 @@ var DeliveryConfigSchema2 = external_exports.object({
16262
16343
  * carries credentials; only a `poll` registration can come back with null here. */
16263
16344
  realtime: external_exports.object({ url: external_exports.string(), anonKey: external_exports.string() }).nullable()
16264
16345
  });
16265
- var StatusSchema2 = external_exports.object({
16266
- name: external_exports.string(),
16267
- sessionMode: external_exports.enum(["default", "all_calls", "silent"]),
16268
- /** A phone is registered for push/ring (any push token on the account). */
16269
- phone: external_exports.boolean(),
16270
- /** HOW MANY THINGS ARE WAITING ON THIS IDENTITY — replies it never collected and requests
16271
- * it never picked up. THE SAME NUMBER the harness's wake gate reads
16272
- * (`pendingSummary.unacknowledged`), from the same function, because a statusline saying
16273
- * zero while the sweep sees one is two ideas of "waiting".
16274
- *
16275
- * Why it is here at all (owner, 2026-09-07): nothing can interrupt an idle agent process
16276
- * that nobody spawned, so a terminal session only learns of work by asking. The harness
16277
- * used to paper over that by spawning a SECOND process on the identity; now it stands
16278
- * back, correctly, and the person sitting at the terminal is the one who can act. A
16279
- * coffee-beans request sat unread for three days.
16280
- *
16281
- * Optional: an older API sends no field, and the statusline then renders exactly as before. */
16282
- waiting: external_exports.number().int().nonnegative().optional()
16283
- });
16284
16346
  var EnvelopeRecipientSchema2 = external_exports.object({
16285
16347
  keyId: external_exports.string(),
16286
16348
  epk: external_exports.string(),
@@ -16433,7 +16495,7 @@ function sameTail(a, b) {
16433
16495
  // src/harness/session.ts
16434
16496
  import { spawn } from "child_process";
16435
16497
  import { existsSync as existsSync4 } from "fs";
16436
- import { homedir as homedir4 } from "os";
16498
+ import { homedir as homedir5 } from "os";
16437
16499
  import { resolve as resolve2, delimiter as delimiter2 } from "path";
16438
16500
 
16439
16501
  // src/harness/acp.ts
@@ -16443,15 +16505,19 @@ function optionFor(options, decision) {
16443
16505
  return options.find((o) => o.kind === want)?.optionId ?? null;
16444
16506
  }
16445
16507
  function createAcpDriver(opts) {
16446
- return new AcpDriver(opts.cwd, opts.mode);
16508
+ return new AcpDriver(opts.cwd, opts.mode, opts.resume);
16447
16509
  }
16448
16510
  var AcpDriver = class {
16449
- 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) {
16450
16514
  this.cwd = cwd;
16451
16515
  this.mode = mode;
16516
+ this.resume = resume;
16452
16517
  }
16453
16518
  cwd;
16454
16519
  mode;
16520
+ resume;
16455
16521
  nextId = 1;
16456
16522
  initId;
16457
16523
  sessionNewId;
@@ -16535,15 +16601,15 @@ var AcpDriver = class {
16535
16601
  this.sessionNewId = this.nextId++;
16536
16602
  return {
16537
16603
  events: [],
16538
- 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: [] } })]
16539
16605
  };
16540
16606
  }
16541
16607
  if (msg.id === this.sessionNewId) {
16542
16608
  this.sessionNewId = void 0;
16543
- const sessionId2 = msg.result?.sessionId;
16609
+ const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
16544
16610
  if (!sessionId2) {
16545
16611
  return {
16546
- 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"}` }],
16547
16613
  writes: []
16548
16614
  };
16549
16615
  }
@@ -16657,7 +16723,7 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
16657
16723
  var ADAPTER_BIN = {
16658
16724
  claude: "claude-agent-acp",
16659
16725
  codex: "codex-acp",
16660
- agy: "agy"
16726
+ agy: "paigy-agy-acp"
16661
16727
  };
16662
16728
  function splitLines(buffer, chunk) {
16663
16729
  const combined = buffer + chunk;
@@ -16666,7 +16732,7 @@ function splitLines(buffer, chunk) {
16666
16732
  return { lines: parts.filter((l) => l.trim()), rest };
16667
16733
  }
16668
16734
  function startSession(opts) {
16669
- const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
16735
+ const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir5()));
16670
16736
  if (!existsSync4(cwd)) {
16671
16737
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
16672
16738
  }
@@ -16679,7 +16745,8 @@ function startSession(opts) {
16679
16745
  env: {
16680
16746
  ...process.env,
16681
16747
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
16682
- ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
16748
+ ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
16749
+ ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {}
16683
16750
  },
16684
16751
  stdio: ["pipe", "pipe", "pipe"]
16685
16752
  });
@@ -16699,7 +16766,7 @@ function startSession(opts) {
16699
16766
  opts.onEvent({ kind: "error", message: e.message });
16700
16767
  opts.onExit?.();
16701
16768
  });
16702
- const driver = createAcpDriver({ cwd, mode: opts.mode });
16769
+ const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.resume ? { resume: opts.resume } : {} });
16703
16770
  let buffer = "";
16704
16771
  child.stdout?.on("data", (chunk) => {
16705
16772
  const { lines: lines2, rest } = splitLines(buffer, String(chunk));
@@ -16759,7 +16826,9 @@ async function step(session, state, rail) {
16759
16826
  unheard(brief2, state);
16760
16827
  if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
16761
16828
  else state.goal = { id: brief2.goalId, revision: brief2.revision };
16762
- send(session, state, brief2.message);
16829
+ const outcome = brief2.outcome?.trim() ?? "";
16830
+ const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
16831
+ send2(session, state, [outcome, ...said].filter(Boolean).join("\n\n"));
16763
16832
  return;
16764
16833
  }
16765
16834
  const brief = await rail.read(state.goal.id);
@@ -16778,9 +16847,9 @@ async function step(session, state, rail) {
16778
16847
  }
16779
16848
  if (state.asks?.size || !state.resting) return;
16780
16849
  const words2 = unheard(brief, state);
16781
- if (words2.length) send(session, state, words2.join("\n\n"));
16850
+ if (words2.length) send2(session, state, words2.join("\n\n"));
16782
16851
  }
16783
- function send(session, state, text) {
16852
+ function send2(session, state, text) {
16784
16853
  state.resting = false;
16785
16854
  session.send(text);
16786
16855
  }
@@ -16913,7 +16982,7 @@ function runHarness(opts) {
16913
16982
  let tail = [];
16914
16983
  const state = { resting: true };
16915
16984
  let session = null;
16916
- const rail = railFor({ token: opts.token });
16985
+ const rail = railFor({ token: opts.token, ...opts.resume ? { reach: reachAs(opts.resume) } : {} });
16917
16986
  async function handle(event) {
16918
16987
  if (event.kind === "error") {
16919
16988
  opts.log(`\u26A0 ${event.message}`);
@@ -16961,6 +17030,7 @@ function runHarness(opts) {
16961
17030
  cwd: opts.cwd,
16962
17031
  mode: opts.mode,
16963
17032
  ...opts.token ? { token: opts.token } : {},
17033
+ ...opts.resume ? { resume: opts.resume } : {},
16964
17034
  ...opts.bin ? { bin: opts.bin } : {},
16965
17035
  ...opts.onExit ? { onExit: opts.onExit } : {},
16966
17036
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
@@ -16993,7 +17063,7 @@ function runHarness(opts) {
16993
17063
  // src/update.ts
16994
17064
  import { execFile } from "child_process";
16995
17065
  import { createRequire } from "module";
16996
- import { dirname as dirname3, join as join4 } from "path";
17066
+ import { dirname as dirname3, join as join5 } from "path";
16997
17067
  import { promisify } from "util";
16998
17068
  var SERVICE_LABEL = "ai.paigy.harness";
16999
17069
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -17016,7 +17086,7 @@ async function latestVersion() {
17016
17086
  }
17017
17087
  async function install(version) {
17018
17088
  const bin = dirname3(process.execPath);
17019
- await promisify(execFile)(join4(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
17089
+ await promisify(execFile)(join5(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
17020
17090
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
17021
17091
  timeout: 5 * 6e4
17022
17092
  });
@@ -17050,7 +17120,7 @@ function selfUpdater(deps) {
17050
17120
  }
17051
17121
 
17052
17122
  // src/host.ts
17053
- var HOST_FILE = join5(homedir5(), ".paigy", "host.json");
17123
+ var HOST_FILE = join6(homedir6(), ".paigy", "host.json");
17054
17124
  function readHostState() {
17055
17125
  try {
17056
17126
  const s = JSON.parse(readFileSync3(HOST_FILE, "utf8"));
@@ -17074,8 +17144,8 @@ var claimedRun = (sessionId2) => {
17074
17144
  const slot = sessionSlot(sessionId2);
17075
17145
  return { slot, key: runKey(slot) };
17076
17146
  };
17077
- async function slotHasWaitingWork(token) {
17078
- 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;
17079
17149
  }
17080
17150
  var PRESENCE_FRESH_MS = 3 * 6e4;
17081
17151
  function wakeAction(live, now, serverSeenAt) {
@@ -17088,6 +17158,7 @@ function startHost(opts) {
17088
17158
  const asHost = { token: opts.token };
17089
17159
  const refreshIdentities = async () => {
17090
17160
  for (const slot of listSlots()) {
17161
+ if (slot === "Desktop") continue;
17091
17162
  const token = readToken(slot);
17092
17163
  if (!token) continue;
17093
17164
  const me = await whoAmI({ token }).catch(() => null);
@@ -17167,9 +17238,11 @@ function startHost(opts) {
17167
17238
  if (wakeAction(!!live, Date.now(), seenByServer) === "skip") return;
17168
17239
  const token = readToken(slot);
17169
17240
  if (!token) return;
17170
- 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);
17171
17244
  if (!workspace) return;
17172
- if (!await slotHasWaitingWork(token).catch(() => false)) return;
17245
+ if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
17173
17246
  const label = slotName(slot) ?? slot;
17174
17247
  const log2 = (line) => opts.log(`[${label}] ${line}`);
17175
17248
  const run = runHarness({
@@ -17178,6 +17251,7 @@ function startHost(opts) {
17178
17251
  mode: "bypass",
17179
17252
  prompt: "",
17180
17253
  token,
17254
+ ...owner ? { resume: owner.sessionId } : {},
17181
17255
  // A dead run must not squat the slot — evict so the next wake can respawn.
17182
17256
  onExit: () => {
17183
17257
  runs.delete(key);
@@ -17188,7 +17262,7 @@ function startHost(opts) {
17188
17262
  runs.set(key, { run, label, token });
17189
17263
  void heartbeat(void 0, { token }).catch(() => {
17190
17264
  });
17191
- 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}`);
17192
17266
  }
17193
17267
  }
17194
17268
  const listening = /* @__PURE__ */ new Map();
@@ -17497,7 +17571,7 @@ function staleWhy(note) {
17497
17571
 
17498
17572
  // src/triage/judge.ts
17499
17573
  import { spawnSync as spawnSync2 } from "child_process";
17500
- import { homedir as homedir6 } from "os";
17574
+ import { homedir as homedir7 } from "os";
17501
17575
  import { delimiter as delimiter3 } from "path";
17502
17576
  var PRIVACY = {
17503
17577
  ollama: "never leaves this machine",
@@ -17601,7 +17675,7 @@ async function judgeFor(asked, deps = {}) {
17601
17675
  const out = spawnSync2(bin, runner.argv(null), {
17602
17676
  input: prompt,
17603
17677
  encoding: "utf8",
17604
- cwd: homedir6(),
17678
+ cwd: homedir7(),
17605
17679
  timeout: RUN_TIMEOUT_MS,
17606
17680
  maxBuffer: 32 * 1024 * 1024,
17607
17681
  env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
@@ -17899,7 +17973,7 @@ var win = null;
17899
17973
  function log(message) {
17900
17974
  win?.webContents.send("paigy:log", message);
17901
17975
  }
17902
- var ICON = join6(here, "..", "assets", "icon.png");
17976
+ var ICON = join7(here, "..", "assets", "icon.png");
17903
17977
  function createWindow() {
17904
17978
  win = new BrowserWindow({
17905
17979
  // Sized for the working log, which is now the window's centerpiece; the layout is a
@@ -17911,9 +17985,9 @@ function createWindow() {
17911
17985
  minHeight: 520,
17912
17986
  icon: ICON,
17913
17987
  // Windows/Linux window icon; macOS uses the dock icon below
17914
- webPreferences: { preload: join6(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
17988
+ webPreferences: { preload: join7(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
17915
17989
  });
17916
- void win.loadFile(join6(here, "..", "renderer", "index.html"));
17990
+ void win.loadFile(join7(here, "..", "renderer", "index.html"));
17917
17991
  }
17918
17992
  var host = null;
17919
17993
  app.whenReady().then(() => {
@@ -17982,7 +18056,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
17982
18056
  });
17983
18057
  ipcMain.handle("paigy:agent-log", (_e, name) => {
17984
18058
  try {
17985
- const all = readFileSync4(join6(homedir7(), ".paigy", "host.log"), "utf8").split("\n");
18059
+ const all = readFileSync4(join7(homedir8(), ".paigy", "host.log"), "utf8").split("\n");
17986
18060
  const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
17987
18061
  return mine.slice(-500).join("\n");
17988
18062
  } catch {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.3.10",
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",
7
7
  "main": "dist/main.js",
8
8
  "bin": {
9
9
  "paigy-harness": "dist/cli.js",
10
- "paigy": "dist/cli.js"
10
+ "paigy": "dist/cli.js",
11
+ "paigy-agy-acp": "dist/agy-acp.js"
11
12
  },
12
13
  "scripts": {
13
14
  "build": "tsup",
@@ -32,6 +33,7 @@
32
33
  },
33
34
  "files": [
34
35
  "dist/cli.js",
36
+ "dist/agy-acp.js",
35
37
  "README.md"
36
38
  ],
37
39
  "publishConfig": {