@paigy/harness 0.3.17 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +15 -14
  2. package/dist/cli.js +1162 -365
  3. package/dist/main.js +1118 -323
  4. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -4578,9 +4578,9 @@ var require_lib = __commonJS({
4578
4578
  // src/main.ts
4579
4579
  import { app, BrowserWindow, dialog, ipcMain } from "electron";
4580
4580
  import { spawn as spawn2 } from "child_process";
4581
- import { readFileSync as readFileSync5 } from "fs";
4581
+ import { readFileSync as readFileSync6 } from "fs";
4582
4582
  import { fileURLToPath } from "url";
4583
- import { dirname as dirname4, join as join9 } from "path";
4583
+ import { dirname as dirname4, join as join11 } from "path";
4584
4584
 
4585
4585
  // ../../packages/sdk/dist/index.js
4586
4586
  import { createRequire as __sdkCreateRequire } from "module";
@@ -10029,8 +10029,13 @@ var UpdateGoalSchema = external_exports.object({
10029
10029
  }).strict();
10030
10030
  var UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
10031
10031
  var ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
10032
- var GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
10033
- 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.";
10032
+ var GetGoalSchema = external_exports.object({
10033
+ goalId: external_exports.string().uuid(),
10034
+ /** Every entry in full. Without it the read carries the person's words, open questions and your
10035
+ * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
10036
+ history: external_exports.boolean().optional()
10037
+ }).strict();
10038
+ 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), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
10034
10039
  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. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
10035
10040
  var CLAIM_GOAL_DESCRIPTION = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
10036
10041
  var CHECK_REPLIES_DESCRIPTION = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
@@ -10851,7 +10856,19 @@ var QueueItemSchema = external_exports.object({
10851
10856
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
10852
10857
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
10853
10858
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
10854
- lastProgressAt: external_exports.string().nullable().optional()
10859
+ lastProgressAt: external_exports.string().nullable().optional(),
10860
+ /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
10861
+ * kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
10862
+ * note. A progress note is an Entry, so this is already the newer of the two: the person's note
10863
+ * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
10864
+ * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
10865
+ * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
10866
+ latest: external_exports.object({
10867
+ from: external_exports.enum(["person", "agent"]),
10868
+ said: external_exports.string(),
10869
+ at: external_exports.string(),
10870
+ entryId: external_exports.string()
10871
+ }).nullable().optional()
10855
10872
  });
10856
10873
  var COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
10857
10874
  var NoteSourceSchema = external_exports.enum(["app", "call"]);
@@ -11029,6 +11046,104 @@ var NotificationFeedbackKindSchema = external_exports.enum([
11029
11046
  "other"
11030
11047
  // anything else — the note carries it.
11031
11048
  ]);
11049
+ var SlimOptionSchema = OptionSchema.omit({ html: true });
11050
+ var QuestionRowSchema = external_exports.object({
11051
+ /** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
11052
+ id: external_exports.string(),
11053
+ deliveryId: external_exports.string(),
11054
+ entryId: external_exports.string(),
11055
+ /** The decision it waits on; null for an update, which asks nothing. */
11056
+ needId: external_exports.string().nullable(),
11057
+ goalIds: external_exports.array(external_exports.string()),
11058
+ /** The name of the work it is about, when the read could word it. */
11059
+ goalTitle: external_exports.string().optional(),
11060
+ tokenId: external_exports.string().optional(),
11061
+ name: external_exports.string(),
11062
+ title: external_exports.string(),
11063
+ body: external_exports.string(),
11064
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
11065
+ options: external_exports.array(SlimOptionSchema),
11066
+ hasPreview: external_exports.boolean(),
11067
+ blocking: external_exports.boolean(),
11068
+ askedAt: external_exports.string().datetime(),
11069
+ ring: InboxItemSchema.shape.ring,
11070
+ onCall: external_exports.literal(true).optional(),
11071
+ sealed: external_exports.boolean()
11072
+ });
11073
+ var WorkStateSchema = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
11074
+ var WorkRowSchema = external_exports.object({
11075
+ id: external_exports.string(),
11076
+ parentId: external_exports.string().nullable(),
11077
+ title: external_exports.string(),
11078
+ /** Straight off the Goal. */
11079
+ state: WorkStateSchema,
11080
+ owner: external_exports.string().nullable(),
11081
+ revision: external_exports.number().int(),
11082
+ /** Open questions on it, counted to 100. */
11083
+ waiting: external_exports.number().int(),
11084
+ /** Held by a gate on work that is not done. */
11085
+ blocked: external_exports.boolean(),
11086
+ lastProgressAt: external_exports.string().datetime().nullable(),
11087
+ /** The line written for its newest progress note, else that note's first words. */
11088
+ line: external_exports.string().nullable(),
11089
+ /** Work directly under it, counted to 100; the list carries up to 12 of them. */
11090
+ children: external_exports.number().int(),
11091
+ createdAt: external_exports.string().datetime(),
11092
+ updatedAt: external_exports.string().datetime(),
11093
+ /** When anything at or under it last moved — the order the list is in. */
11094
+ activeAt: external_exports.string().datetime(),
11095
+ /** A sealed outcome has no title here; the work's page opens it. */
11096
+ sealed: external_exports.boolean()
11097
+ });
11098
+ var ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
11099
+ var AgentRowSchema = ComputerRowSchema.extend({
11100
+ /** Open questions it is asking the person, over every open card; null when that read failed. */
11101
+ asking: external_exports.number().int().nullable(),
11102
+ oldestAskAt: external_exports.string().datetime().nullable(),
11103
+ /** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
11104
+ * many in all among the account's 200 most recently active agent-held live Goals
11105
+ * (`agent_holds`); null when that read failed. */
11106
+ holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
11107
+ held: external_exports.number().int().nullable(),
11108
+ /** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
11109
+ * while none has, or when that read failed. */
11110
+ cold: external_exports.string().datetime().nullable(),
11111
+ /** The newest line of its working log, and when the harness saw it. */
11112
+ line: external_exports.string().nullable(),
11113
+ lineAt: external_exports.string().datetime().nullable()
11114
+ });
11115
+ var SnapshotSchema = external_exports.object({
11116
+ /** The API's clock, taken before the first read: what a later delta will start from. */
11117
+ at: external_exports.string().datetime(),
11118
+ questions: external_exports.object({
11119
+ /** The newest 30 open cards, questions before updates. */
11120
+ items: external_exports.array(QuestionRowSchema),
11121
+ /** Every open question, and apart from them every update, and what was put off. */
11122
+ total: external_exports.number().int(),
11123
+ updates: external_exports.number().int(),
11124
+ putOff: external_exports.number().int()
11125
+ }).nullable(),
11126
+ agents: external_exports.object({
11127
+ /** Up to 60, most recently seen first. */
11128
+ items: external_exports.array(AgentRowSchema),
11129
+ more: external_exports.boolean()
11130
+ }).nullable(),
11131
+ work: external_exports.object({
11132
+ /** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
11133
+ items: external_exports.array(WorkRowSchema),
11134
+ /** How much work is behind each of the Work tab's four filters, each counted to 100, read with
11135
+ * the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
11136
+ * `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
11137
+ * is `ready`; Done is `done` or `cancelled`. */
11138
+ counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
11139
+ }).nullable(),
11140
+ you: external_exports.object({
11141
+ settings: UserSettingsSchema,
11142
+ callable: external_exports.boolean(),
11143
+ /** Up to 20 paired computers; null when the roster read failed. */
11144
+ computers: external_exports.array(ComputerRowSchema).nullable()
11145
+ }).nullable()
11146
+ });
11032
11147
  function sessionSlot(sessionId2) {
11033
11148
  const id = sessionId2 ?? sessionId();
11034
11149
  return `session:${id.slice(0, 8)}`;
@@ -11175,9 +11290,9 @@ async function claimGoal(goalId, opts = {}) {
11175
11290
  if (!res.ok) await fail("claim_goal", res);
11176
11291
  return await res.json();
11177
11292
  }
11178
- async function getGoal(goalId, opts = {}) {
11293
+ async function getGoal(goalId, opts = {}, read = {}) {
11179
11294
  const token = authToken(opts.token) ?? "";
11180
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
11295
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${read.history ? "?history=1" : ""}`, {
11181
11296
  method: "GET",
11182
11297
  headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
11183
11298
  }));
@@ -11553,7 +11668,7 @@ async function subscribeWake(onNudge, opts = {}) {
11553
11668
 
11554
11669
  // src/main.ts
11555
11670
  var import_qrcode = __toESM(require_lib(), 1);
11556
- import { homedir as homedir9, hostname } from "os";
11671
+ import { homedir as homedir11, hostname } from "os";
11557
11672
 
11558
11673
  // src/harness/catalog.ts
11559
11674
  import { spawnSync } from "child_process";
@@ -11646,9 +11761,9 @@ function detectAll(deps = {}) {
11646
11761
  }
11647
11762
 
11648
11763
  // src/host.ts
11649
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
11650
- import { homedir as homedir7 } from "os";
11651
- import { join as join8 } from "path";
11764
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
11765
+ import { homedir as homedir9 } from "os";
11766
+ import { join as join10 } from "path";
11652
11767
 
11653
11768
  // src/workspaces.ts
11654
11769
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
@@ -11693,10 +11808,72 @@ function resolveWakeDir(pinned, deps) {
11693
11808
  return listWorkspaces(deps)[0];
11694
11809
  }
11695
11810
 
11696
- // src/owner.ts
11697
- import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
11811
+ // src/holder.ts
11812
+ import { execFileSync as execFileSync3 } from "child_process";
11813
+ import { readFileSync as readFileSync4, readdirSync } from "fs";
11698
11814
  import { homedir as homedir5 } from "os";
11699
11815
  import { join as join6 } from "path";
11816
+ function isAlive(pid) {
11817
+ try {
11818
+ process.kill(pid, 0);
11819
+ return true;
11820
+ } catch (e) {
11821
+ return e.code === "EPERM";
11822
+ }
11823
+ }
11824
+ function psStart(pid, zone) {
11825
+ try {
11826
+ const env = zone === "UTC" ? { ...process.env, TZ: "UTC", LC_ALL: "C" } : { ...process.env, LC_ALL: "C" };
11827
+ return execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], { env, encoding: "utf8", timeout: 2e3 }).trim() || null;
11828
+ } catch {
11829
+ return null;
11830
+ }
11831
+ }
11832
+ var squash = (s) => s.trim().replace(/\s+/g, " ");
11833
+ function holds(pid, procStart, deps) {
11834
+ if (!deps.alive(pid)) return false;
11835
+ if (typeof procStart !== "string" || !procStart.trim()) return true;
11836
+ const want = squash(procStart);
11837
+ for (const zone of ["UTC", "local"]) {
11838
+ const got = deps.started(pid, zone);
11839
+ if (got === null) return true;
11840
+ if (squash(got) === want) return true;
11841
+ }
11842
+ return false;
11843
+ }
11844
+ function localHolders(deps = {}) {
11845
+ const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
11846
+ const list = deps.list ?? ((d) => readdirSync(d));
11847
+ const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
11848
+ const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
11849
+ const held = /* @__PURE__ */ new Map();
11850
+ let files;
11851
+ try {
11852
+ files = list(dir);
11853
+ } catch {
11854
+ return held;
11855
+ }
11856
+ for (const file of files) {
11857
+ if (!/^\d+\.json$/.test(file)) continue;
11858
+ let row;
11859
+ try {
11860
+ row = JSON.parse(read(join6(dir, file)));
11861
+ } catch {
11862
+ continue;
11863
+ }
11864
+ const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
11865
+ if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
11866
+ const slot = sessionSlot(row.sessionId);
11867
+ if (held.has(slot)) continue;
11868
+ if (holds(pid, row.procStart, check)) held.set(slot, { pid });
11869
+ }
11870
+ return held;
11871
+ }
11872
+
11873
+ // src/owner.ts
11874
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync2 } from "fs";
11875
+ import { homedir as homedir6 } from "os";
11876
+ import { join as join7 } from "path";
11700
11877
  var HEAD_BYTES = 64e3;
11701
11878
  function readHead(file) {
11702
11879
  const fd = openSync2(file, "r");
@@ -11712,8 +11889,8 @@ function ownerSession(slot, deps = {}) {
11712
11889
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
11713
11890
  if (!m) return null;
11714
11891
  const prefix = m[1];
11715
- const root = deps.projectsDir ?? join6(homedir5(), ".claude", "projects");
11716
- const list = deps.list ?? ((dir) => readdirSync(dir));
11892
+ const root = deps.projectsDir ?? join7(homedir6(), ".claude", "projects");
11893
+ const list = deps.list ?? ((dir) => readdirSync2(dir));
11717
11894
  const head = deps.head ?? readHead;
11718
11895
  let folders;
11719
11896
  try {
@@ -11724,7 +11901,7 @@ function ownerSession(slot, deps = {}) {
11724
11901
  for (const folder of folders) {
11725
11902
  let files;
11726
11903
  try {
11727
- files = list(join6(root, folder));
11904
+ files = list(join7(root, folder));
11728
11905
  } catch {
11729
11906
  continue;
11730
11907
  }
@@ -11732,7 +11909,7 @@ function ownerSession(slot, deps = {}) {
11732
11909
  if (!file) continue;
11733
11910
  let text;
11734
11911
  try {
11735
- text = head(join6(root, folder, file));
11912
+ text = head(join7(root, folder, file));
11736
11913
  } catch {
11737
11914
  continue;
11738
11915
  }
@@ -13143,8 +13320,13 @@ var UpdateGoalSchema2 = external_exports.object({
13143
13320
  }).strict();
13144
13321
  var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
13145
13322
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
13146
- var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
13147
- 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.";
13323
+ var GetGoalSchema2 = external_exports.object({
13324
+ goalId: external_exports.string().uuid(),
13325
+ /** Every entry in full. Without it the read carries the person's words, open questions and your
13326
+ * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
13327
+ history: external_exports.boolean().optional()
13328
+ }).strict();
13329
+ var GET_GOAL_DESCRIPTION2 = "Read one Goal without claiming it: its outcome, state, revision, progress, blockers, the conversation on it (each question with its options and what was decided), `next`, the one step to take, and `more`, where to read further. The conversation carries everything the person said, every open question and your newest entry; your older entries are left out and counted \u2014 history: true reads every entry in full. Another account's Goals, and another agent's Goal that is not below one of yours, are not disclosed.";
13148
13330
  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. state: done means the work was accomplished, and it can be reopened: when the person says it is not done ("it's not working"), set state: active on that same Goal instead of starting a new one; its parent reopens with it. cancelled is final. Returns the Goal as get_goal reads it, at its new revision.`;
13149
13331
  var CLAIM_GOAL_DESCRIPTION2 = "Claim the oldest runnable or review-pending Goal you own, or pass goalId to claim that Goal. Another agent's Goal that has gone quiet for 3 days (check_replies lists them as stalledOthers) is taken over when you claim it, and becomes yours to finish or cancel. Returns the Goal as get_goal reads it, and creates or renews the execution lease.";
13150
13332
  var CHECK_REPLIES_DESCRIPTION2 = "What is waiting for you: your open Deliveries (a request the user started toward you, a handoff), one row each with its Goals, how many decisions are still open, and the newest words in brief; `assigned`, your Goals nobody has started yet, however they became yours (claim_goal({goalId}) starts one); and `review`, your Goals with something new on them \u2014 the user's answers and notes are Entries on the Goal, not Deliveries. A pure read with no arguments: nothing is consumed, acknowledged or claimed by reading it, so call it on startup, after a long wait, or whenever you want to know what is outstanding. To act on one, claim its Goal (claim_goal) or reread a Delivery in full with contact({deliveryId}). Once you have acted on what arrived, update_goal with reviewed: true clears the Goal from `review` and closes the Deliveries addressed to you on it.";
@@ -13158,6 +13340,7 @@ var AGENT_TOOLS2 = [
13158
13340
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
13159
13341
  ];
13160
13342
  var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
13343
+ var DONE_MESSAGE = "When the work is done, send one message through contact saying what is done and anything the person needs to do or check.";
13161
13344
  function entryWords(entry) {
13162
13345
  const content = entry.content;
13163
13346
  if (content && "sealed" in content) return "";
@@ -13980,7 +14163,19 @@ var QueueItemSchema2 = external_exports.object({
13980
14163
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
13981
14164
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
13982
14165
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
13983
- lastProgressAt: external_exports.string().nullable().optional()
14166
+ lastProgressAt: external_exports.string().nullable().optional(),
14167
+ /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
14168
+ * kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
14169
+ * note. A progress note is an Entry, so this is already the newer of the two: the person's note
14170
+ * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
14171
+ * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
14172
+ * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
14173
+ latest: external_exports.object({
14174
+ from: external_exports.enum(["person", "agent"]),
14175
+ said: external_exports.string(),
14176
+ at: external_exports.string(),
14177
+ entryId: external_exports.string()
14178
+ }).nullable().optional()
13984
14179
  });
13985
14180
  var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
13986
14181
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
@@ -14156,6 +14351,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
14156
14351
  "other"
14157
14352
  // anything else — the note carries it.
14158
14353
  ]);
14354
+ var SlimOptionSchema2 = OptionSchema2.omit({ html: true });
14355
+ var QuestionRowSchema2 = external_exports.object({
14356
+ /** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
14357
+ id: external_exports.string(),
14358
+ deliveryId: external_exports.string(),
14359
+ entryId: external_exports.string(),
14360
+ /** The decision it waits on; null for an update, which asks nothing. */
14361
+ needId: external_exports.string().nullable(),
14362
+ goalIds: external_exports.array(external_exports.string()),
14363
+ /** The name of the work it is about, when the read could word it. */
14364
+ goalTitle: external_exports.string().optional(),
14365
+ tokenId: external_exports.string().optional(),
14366
+ name: external_exports.string(),
14367
+ title: external_exports.string(),
14368
+ body: external_exports.string(),
14369
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
14370
+ options: external_exports.array(SlimOptionSchema2),
14371
+ hasPreview: external_exports.boolean(),
14372
+ blocking: external_exports.boolean(),
14373
+ askedAt: external_exports.string().datetime(),
14374
+ ring: InboxItemSchema2.shape.ring,
14375
+ onCall: external_exports.literal(true).optional(),
14376
+ sealed: external_exports.boolean()
14377
+ });
14378
+ var WorkStateSchema2 = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
14379
+ var WorkRowSchema2 = external_exports.object({
14380
+ id: external_exports.string(),
14381
+ parentId: external_exports.string().nullable(),
14382
+ title: external_exports.string(),
14383
+ /** Straight off the Goal. */
14384
+ state: WorkStateSchema2,
14385
+ owner: external_exports.string().nullable(),
14386
+ revision: external_exports.number().int(),
14387
+ /** Open questions on it, counted to 100. */
14388
+ waiting: external_exports.number().int(),
14389
+ /** Held by a gate on work that is not done. */
14390
+ blocked: external_exports.boolean(),
14391
+ lastProgressAt: external_exports.string().datetime().nullable(),
14392
+ /** The line written for its newest progress note, else that note's first words. */
14393
+ line: external_exports.string().nullable(),
14394
+ /** Work directly under it, counted to 100; the list carries up to 12 of them. */
14395
+ children: external_exports.number().int(),
14396
+ createdAt: external_exports.string().datetime(),
14397
+ updatedAt: external_exports.string().datetime(),
14398
+ /** When anything at or under it last moved — the order the list is in. */
14399
+ activeAt: external_exports.string().datetime(),
14400
+ /** A sealed outcome has no title here; the work's page opens it. */
14401
+ sealed: external_exports.boolean()
14402
+ });
14403
+ var ComputerRowSchema2 = ConnectionSummarySchema2.omit({ activity: true });
14404
+ var AgentRowSchema2 = ComputerRowSchema2.extend({
14405
+ /** Open questions it is asking the person, over every open card; null when that read failed. */
14406
+ asking: external_exports.number().int().nullable(),
14407
+ oldestAskAt: external_exports.string().datetime().nullable(),
14408
+ /** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
14409
+ * many in all among the account's 200 most recently active agent-held live Goals
14410
+ * (`agent_holds`); null when that read failed. */
14411
+ holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
14412
+ held: external_exports.number().int().nullable(),
14413
+ /** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
14414
+ * while none has, or when that read failed. */
14415
+ cold: external_exports.string().datetime().nullable(),
14416
+ /** The newest line of its working log, and when the harness saw it. */
14417
+ line: external_exports.string().nullable(),
14418
+ lineAt: external_exports.string().datetime().nullable()
14419
+ });
14420
+ var SnapshotSchema2 = external_exports.object({
14421
+ /** The API's clock, taken before the first read: what a later delta will start from. */
14422
+ at: external_exports.string().datetime(),
14423
+ questions: external_exports.object({
14424
+ /** The newest 30 open cards, questions before updates. */
14425
+ items: external_exports.array(QuestionRowSchema2),
14426
+ /** Every open question, and apart from them every update, and what was put off. */
14427
+ total: external_exports.number().int(),
14428
+ updates: external_exports.number().int(),
14429
+ putOff: external_exports.number().int()
14430
+ }).nullable(),
14431
+ agents: external_exports.object({
14432
+ /** Up to 60, most recently seen first. */
14433
+ items: external_exports.array(AgentRowSchema2),
14434
+ more: external_exports.boolean()
14435
+ }).nullable(),
14436
+ work: external_exports.object({
14437
+ /** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
14438
+ items: external_exports.array(WorkRowSchema2),
14439
+ /** How much work is behind each of the Work tab's four filters, each counted to 100, read with
14440
+ * the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
14441
+ * `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
14442
+ * is `ready`; Done is `done` or `cancelled`. */
14443
+ counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
14444
+ }).nullable(),
14445
+ you: external_exports.object({
14446
+ settings: UserSettingsSchema2,
14447
+ callable: external_exports.boolean(),
14448
+ /** Up to 20 paired computers; null when the roster read failed. */
14449
+ computers: external_exports.array(ComputerRowSchema2).nullable()
14450
+ }).nullable()
14451
+ });
14159
14452
 
14160
14453
  // src/paigy/activity.ts
14161
14454
  function shortenPaths(s) {
@@ -14165,8 +14458,8 @@ function shortenPaths(s) {
14165
14458
  });
14166
14459
  }
14167
14460
  function workLine(event) {
14168
- const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
14169
- const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
14461
+ const note2 = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
14462
+ const line = shortenPaths([event.tool.trim(), note2.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
14170
14463
  return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
14171
14464
  }
14172
14465
  function pushWork(lines2, line) {
@@ -14180,7 +14473,7 @@ function sameTail(a, b) {
14180
14473
  // src/harness/session.ts
14181
14474
  import { spawn } from "child_process";
14182
14475
  import { existsSync as existsSync4 } from "fs";
14183
- import { homedir as homedir6 } from "os";
14476
+ import { homedir as homedir8 } from "os";
14184
14477
  import { resolve as resolve2, delimiter as delimiter2 } from "path";
14185
14478
 
14186
14479
  // src/harness/acp.ts
@@ -14191,19 +14484,23 @@ function optionFor(options, decision) {
14191
14484
  return options.find((o) => o.kind === want)?.optionId ?? null;
14192
14485
  }
14193
14486
  function createAcpDriver(opts) {
14194
- return new AcpDriver(opts.cwd, opts.mode, opts.resume);
14487
+ return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
14195
14488
  }
14196
14489
  var AcpDriver = class {
14197
14490
  /** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
14198
- * the session a bound identity belongs to, so the process this drives IS its holder. */
14199
- constructor(cwd, mode, resume) {
14491
+ * the session a bound identity belongs to, so the process this drives IS its holder.
14492
+ * `meta`: the adapter's own `_meta` on the frame that opens the session — where a switch only
14493
+ * one adapter understands rides (`session.ts`, a Claude session's tools). */
14494
+ constructor(cwd, mode, resume, meta) {
14200
14495
  this.cwd = cwd;
14201
14496
  this.mode = mode;
14202
14497
  this.resume = resume;
14498
+ this.meta = meta;
14203
14499
  }
14204
14500
  cwd;
14205
14501
  mode;
14206
14502
  resume;
14503
+ meta;
14207
14504
  nextId = 1;
14208
14505
  initId;
14209
14506
  sessionNewId;
@@ -14223,6 +14520,11 @@ var AcpDriver = class {
14223
14520
  /** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
14224
14521
  text = "";
14225
14522
  tools = [];
14523
+ /** Tool calls opened and not yet reported `completed` or `failed`, by `toolCallId` — whoever
14524
+ * started them. A running tool is the one long silence that is still a working turn (`quiet`). */
14525
+ running = /* @__PURE__ */ new Set();
14526
+ /** The prompt in flight was already cancelled for silence: once is the whole remedy. */
14527
+ cancelled = false;
14226
14528
  /** The opening frame. Everything after is driven by responses in `handleLine`. */
14227
14529
  open() {
14228
14530
  this.initId = this.nextId++;
@@ -14254,6 +14556,29 @@ var AcpDriver = class {
14254
14556
  optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
14255
14557
  ];
14256
14558
  }
14559
+ /**
14560
+ * THE ANSWER THAT NEVER COMES (2026-09-26, session "mute"). A turn ends when the adapter answers
14561
+ * its prompt, and nothing else ends one. claude-agent-acp can fold a prompt into a turn the agent
14562
+ * started on its own and never answer it (agentclientprotocol/claude-agent-acp#1145): the prompt
14563
+ * stayed in flight, no `idle` came, the pump never saw the agent rest, and the session was deaf
14564
+ * for twelve hours with its heartbeat alive.
14565
+ *
14566
+ * The caller's clock says nothing has crossed the pipe for `ms`; this says whether that silence
14567
+ * is still a turn. A running tool is (a ten-minute build streams nothing), and so is a permission
14568
+ * waiting on the person. Anything else, with a prompt in flight, is a turn that is over without
14569
+ * its answer, and the remedy is the protocol's own settle: `session/cancel`, which the adapter
14570
+ * answers `stopReason: "cancelled"` through the ordinary response path above — a turn that
14571
+ * ended, not one that failed. Once per prompt, and said as a crumb: a silent recovery is
14572
+ * indistinguishable from a working turn.
14573
+ */
14574
+ quiet(ms) {
14575
+ if (this.promptId === void 0 || this.cancelled || this.running.size || this.pending.size) return none;
14576
+ this.cancelled = true;
14577
+ return {
14578
+ events: [{ kind: "error", message: `turn settled: no reply after ${Math.round(ms / 1e3)}s quiet with no tool running \u2014 sent session/cancel` }],
14579
+ writes: [frame2({ method: "session/cancel", params: { sessionId: this.sessionId } })]
14580
+ };
14581
+ }
14257
14582
  /** Cancel anything still blocked — called on stop so the process can exit cleanly. */
14258
14583
  close() {
14259
14584
  const writes = [...this.pending.values()].map(
@@ -14285,10 +14610,7 @@ var AcpDriver = class {
14285
14610
  return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
14286
14611
  }
14287
14612
  this.sessionNewId = this.nextId++;
14288
- return {
14289
- events: [],
14290
- writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : this.sessionNew(this.sessionNewId))]
14291
- };
14613
+ return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
14292
14614
  }
14293
14615
  if (msg.id === this.sessionNewId) {
14294
14616
  this.sessionNewId = void 0;
@@ -14298,7 +14620,7 @@ var AcpDriver = class {
14298
14620
  this.sessionNewId = this.nextId++;
14299
14621
  return {
14300
14622
  events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
14301
- writes: [frame2(this.sessionNew(this.sessionNewId))]
14623
+ writes: [frame2(this.opening(this.sessionNewId))]
14302
14624
  };
14303
14625
  }
14304
14626
  const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
@@ -14323,6 +14645,8 @@ var AcpDriver = class {
14323
14645
  }
14324
14646
  if (msg.id === this.promptId) {
14325
14647
  this.promptId = void 0;
14648
+ this.running.clear();
14649
+ this.cancelled = false;
14326
14650
  const events = [];
14327
14651
  const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
14328
14652
  if (report || this.tools.length) {
@@ -14354,6 +14678,10 @@ var AcpDriver = class {
14354
14678
  handleNotification(msg) {
14355
14679
  if (msg.method !== "session/update") return none;
14356
14680
  const update = msg.params?.update;
14681
+ if (update?.toolCallId) {
14682
+ if (update.status === "completed" || update.status === "failed") this.running.delete(update.toolCallId);
14683
+ else if (update.sessionUpdate === "tool_call") this.running.add(update.toolCallId);
14684
+ }
14357
14685
  switch (update?.sessionUpdate) {
14358
14686
  case "agent_message_chunk":
14359
14687
  this.text += update.content?.text ?? "";
@@ -14361,9 +14689,9 @@ var AcpDriver = class {
14361
14689
  case "tool_call": {
14362
14690
  const title = update.title?.trim() || update.kind || "tool";
14363
14691
  this.tools.push(title);
14364
- const note = this.text.slice(this.reportFrom).trim();
14692
+ const note2 = this.text.slice(this.reportFrom).trim();
14365
14693
  this.reportFrom = this.text.length;
14366
- return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
14694
+ return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
14367
14695
  }
14368
14696
  default:
14369
14697
  return none;
@@ -14397,8 +14725,11 @@ var AcpDriver = class {
14397
14725
  writes: []
14398
14726
  };
14399
14727
  }
14400
- sessionNew(id) {
14401
- return { id, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } };
14728
+ /** The frame that opens the conversation: a resume of `resume` while there is one to try,
14729
+ * else a new session. */
14730
+ opening(id) {
14731
+ const params = { cwd: this.cwd, mcpServers: [], ...this.meta ? { _meta: this.meta } : {} };
14732
+ return this.resume ? { id, method: "session/resume", params: { sessionId: this.resume, ...params } } : { id, method: "session/new", params };
14402
14733
  }
14403
14734
  /** Send the queued prompts as one turn, if the agent can take one right now. */
14404
14735
  flush() {
@@ -14417,12 +14748,92 @@ var AcpDriver = class {
14417
14748
  };
14418
14749
  var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
14419
14750
 
14751
+ // src/paigy/inbox.ts
14752
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
14753
+ import { join as join8 } from "path";
14754
+ import { homedir as homedir7 } from "os";
14755
+ import { randomUUID as randomUUID4 } from "crypto";
14756
+ var MIDTURN_HOOK = `d="$PAIGY_INBOX"; [ -n "$d" ] || { cat >/dev/null; exit 0; }; set -- "$d"/*.note; [ -e "$1" ] || { cat >/dev/null; exit 0; }; grep '"agent_id"' >/dev/null && exit 0; mkdir -p "$d/taken"; o=""; for f in "$@"; do n=\${f##*/}; mv "$f" "$d/taken/$n" 2>/dev/null || continue; touch "$d/taken/$n"; o="$o\${o:+\\n\\n}$(cat "$d/taken/$n")"; done; [ -n "$o" ] || exit 0; printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s"}}' "$o"`;
14757
+ var missing = (error) => error.code === "ENOENT";
14758
+ function openInbox(dir) {
14759
+ mkdirSync3(dir, { recursive: true });
14760
+ let written = 0;
14761
+ const receipt = (name) => join8(dir, "taken", `${name}.note`);
14762
+ const taken = (name) => {
14763
+ try {
14764
+ return statSync2(receipt(name)).mtimeMs;
14765
+ } catch (error) {
14766
+ if (missing(error)) return null;
14767
+ throw error;
14768
+ }
14769
+ };
14770
+ return {
14771
+ dir,
14772
+ put(text) {
14773
+ const name = `${Date.now()}-${String(++written).padStart(6, "0")}`;
14774
+ const tmp = join8(dir, `${name}.tmp`);
14775
+ writeFileSync3(tmp, JSON.stringify(text).slice(1, -1));
14776
+ renameSync(tmp, join8(dir, `${name}.note`));
14777
+ return name;
14778
+ },
14779
+ taken,
14780
+ settle(name) {
14781
+ try {
14782
+ renameSync(join8(dir, `${name}.note`), join8(dir, `${name}.back`));
14783
+ rmSync2(join8(dir, `${name}.back`), { force: true });
14784
+ return "retracted";
14785
+ } catch (error) {
14786
+ if (!missing(error)) throw error;
14787
+ }
14788
+ const seen = taken(name) !== null;
14789
+ rmSync2(receipt(name), { force: true });
14790
+ return seen ? "taken" : "retracted";
14791
+ },
14792
+ close() {
14793
+ rmSync2(dir, { recursive: true, force: true });
14794
+ }
14795
+ };
14796
+ }
14797
+ var INBOX_ROOT = join8(homedir7(), ".paigy", "inbox");
14798
+ var alive = (pid) => {
14799
+ try {
14800
+ process.kill(pid, 0);
14801
+ return true;
14802
+ } catch (error) {
14803
+ return error.code === "EPERM";
14804
+ }
14805
+ };
14806
+ function inboxFor(root = INBOX_ROOT) {
14807
+ try {
14808
+ for (const name of readdirSync3(root)) {
14809
+ const pid = Number(name.split("-")[0]);
14810
+ if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join8(root, name), { recursive: true, force: true });
14811
+ }
14812
+ } catch (error) {
14813
+ if (!missing(error)) throw error;
14814
+ }
14815
+ return openInbox(join8(root, `${process.pid}-${randomUUID4().slice(0, 8)}`));
14816
+ }
14817
+
14420
14818
  // src/harness/session.ts
14421
14819
  var ADAPTER_BIN = {
14422
14820
  claude: "claude-agent-acp",
14423
14821
  codex: "codex-acp",
14424
14822
  agy: "paigy-agy-acp"
14425
14823
  };
14824
+ var CLAUDE_ONE_WAY_IN = {
14825
+ env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" },
14826
+ disallowedTools: ["Monitor", "ScheduleWakeup", "CronCreate", "Workflow"]
14827
+ };
14828
+ var CLAUDE_META = {
14829
+ claudeCode: {
14830
+ options: {
14831
+ disallowedTools: CLAUDE_ONE_WAY_IN.disallowedTools,
14832
+ settings: { hooks: { PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: MIDTURN_HOOK }] }] } }
14833
+ }
14834
+ }
14835
+ };
14836
+ var QUIET_MS = 15 * 6e4;
14426
14837
  function splitLines(buffer, chunk) {
14427
14838
  const combined = buffer + chunk;
14428
14839
  const parts = combined.split("\n");
@@ -14430,7 +14841,7 @@ function splitLines(buffer, chunk) {
14430
14841
  return { lines: parts.filter((l) => l.trim()), rest };
14431
14842
  }
14432
14843
  function startSession(opts) {
14433
- const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir6()));
14844
+ const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir8()));
14434
14845
  if (!existsSync4(cwd)) {
14435
14846
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
14436
14847
  }
@@ -14450,7 +14861,9 @@ function startSession(opts) {
14450
14861
  // Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
14451
14862
  // fresh spawn has none, which is how a fresh run read as "not harness-started" to the
14452
14863
  // session-start hook and the `listen` tool (they now test either mark; listening.ts).
14453
- PAIGY_HARNESS: "1"
14864
+ PAIGY_HARNESS: "1",
14865
+ ...opts.harness === "claude" ? CLAUDE_ONE_WAY_IN.env : {},
14866
+ ...opts.harness === "claude" && opts.inbox ? { PAIGY_INBOX: opts.inbox } : {}
14454
14867
  },
14455
14868
  stdio: ["pipe", "pipe", "pipe"]
14456
14869
  });
@@ -14461,6 +14874,16 @@ function startSession(opts) {
14461
14874
  if (ended2) return;
14462
14875
  for (const f of frames) child.stdin?.write(`${f}
14463
14876
  `);
14877
+ if (frames.length) listen();
14878
+ };
14879
+ const apply = ({ events, writes }) => {
14880
+ write(writes);
14881
+ for (const event of events) opts.onEvent(event);
14882
+ };
14883
+ let hush;
14884
+ const listen = () => {
14885
+ clearTimeout(hush);
14886
+ if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
14464
14887
  };
14465
14888
  child.stderr?.on("data", (chunk) => {
14466
14889
  const text = String(chunk).trim();
@@ -14468,6 +14891,7 @@ function startSession(opts) {
14468
14891
  });
14469
14892
  child.on("exit", (code) => {
14470
14893
  ended2 = true;
14894
+ clearTimeout(hush);
14471
14895
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
14472
14896
  opts.onExit?.();
14473
14897
  });
@@ -14475,16 +14899,18 @@ function startSession(opts) {
14475
14899
  opts.onEvent({ kind: "error", message: e.message });
14476
14900
  opts.onExit?.();
14477
14901
  });
14478
- const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.resume ? { resume: opts.resume } : {} });
14902
+ const driver = createAcpDriver({
14903
+ cwd,
14904
+ mode: opts.mode,
14905
+ ...opts.resume ? { resume: opts.resume } : {},
14906
+ ...opts.harness === "claude" ? { meta: CLAUDE_META } : {}
14907
+ });
14479
14908
  let buffer = "";
14480
14909
  child.stdout?.on("data", (chunk) => {
14910
+ listen();
14481
14911
  const { lines: lines2, rest } = splitLines(buffer, String(chunk));
14482
14912
  buffer = rest;
14483
- for (const line of lines2) {
14484
- const { events, writes } = driver.handleLine(line);
14485
- write(writes);
14486
- for (const event of events) opts.onEvent(event);
14487
- }
14913
+ for (const line of lines2) apply(driver.handleLine(line));
14488
14914
  });
14489
14915
  write(driver.open());
14490
14916
  return {
@@ -14497,132 +14923,137 @@ function startSession(opts) {
14497
14923
  stop() {
14498
14924
  write(driver.close());
14499
14925
  ended2 = true;
14926
+ clearTimeout(hush);
14500
14927
  child.kill();
14501
14928
  }
14502
14929
  };
14503
14930
  }
14504
14931
 
14505
- // src/paigy/conversation.ts
14506
- import { randomUUID as randomUUID4 } from "crypto";
14507
- var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent connected to your user through Paigy. The user is NOT at the terminal and cannot see text printed here. To ask questions, request approvals, or make decisions, use Paigy tools or end your turn with a clear question for the bridge to relay. When complete, provide a concise summary of what was accomplished.";
14508
- function endsWithQuestion(text) {
14509
- if (!text) return false;
14510
- const trimmed = text.trim();
14511
- if (!trimmed) return false;
14512
- if (trimmed.endsWith("?")) return true;
14513
- const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
14514
- if (lines2.length === 0) return false;
14515
- let lastNonListIdx = lines2.length - 1;
14516
- while (lastNonListIdx >= 0) {
14517
- const line = lines2[lastNonListIdx];
14518
- if (line && /^[-*•\d.)]\s+/.test(line)) {
14519
- lastNonListIdx--;
14520
- } else {
14521
- break;
14522
- }
14523
- }
14524
- const promptLine = lastNonListIdx >= 0 ? lines2[lastNonListIdx] : lines2[lines2.length - 1];
14525
- if (promptLine?.endsWith("?")) return true;
14526
- const tail = lines2.slice(-3).join(" ").toLowerCase();
14527
- return [
14528
- /\b(let me know|which (one|would|do you)|do you want|should i|shall i)\b/,
14529
- /\b(want me to|prefer|your call|up to you|confirm whether)\b/,
14530
- /\b(how would you like|how should we|what would you like|what should we)\b/,
14531
- /\b(please choose|choose one|select one|how do you want)\b/
14532
- ].some((re) => re.test(tail));
14533
- }
14534
- var POLL_MS = 5e3;
14535
- var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
14536
- var ended = (state) => state === "done" || state === "cancelled";
14537
- function unheard(brief, state) {
14538
- const seen = state.seen ??= /* @__PURE__ */ new Set();
14539
- const words2 = [];
14540
- for (const e of brief.entries ?? []) {
14541
- if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
14542
- seen.add(e.entryId);
14543
- if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
14544
- const text = entryWords(e).trim();
14545
- if (text) words2.push(text);
14546
- }
14547
- return words2;
14932
+ // src/paigy/limit.ts
14933
+ var LIMIT_PREFIXES = [
14934
+ "You've hit your",
14935
+ "You've reached your",
14936
+ "You're out of usage credits",
14937
+ "Your org is out of usage",
14938
+ "Your seat type doesn't include usage",
14939
+ "Your usage allocation has been disabled by your admin",
14940
+ "Your group's usage limit is set to $0",
14941
+ "requires usage credits",
14942
+ "You're out of extra usage",
14943
+ "Your seat type doesn't include extra usage"
14944
+ ];
14945
+ var RETRY_MS2 = 15 * 6e4;
14946
+ var MARGIN_MS = 6e4;
14947
+ var FARTHEST_MS = 8 * 24 * 60 * 6e4;
14948
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
14949
+ function limitIn(text, now) {
14950
+ const line = (text ?? "").split("\n").map((l) => l.trim()).reverse().find((l) => LIMIT_PREFIXES.some((p) => l.startsWith(p) || p === "requires usage credits" && l.includes(p)));
14951
+ if (!line) return null;
14952
+ const reset = resetOf(line, now);
14953
+ if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
14954
+ return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock3(reset) };
14955
+ }
14956
+ return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
14548
14957
  }
14549
- async function step(session, state, rail, opts) {
14550
- if (!state.goal) {
14551
- if (!state.resting) return;
14552
- let brief2 = await rail.claim();
14553
- if (!brief2.goalId || brief2.revision === void 0) {
14554
- if (opts?.initialPrompt?.trim()) {
14555
- const prompt = opts.initialPrompt.trim();
14556
- opts.initialPrompt = void 0;
14557
- const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID4() });
14558
- brief2 = await rail.claim();
14559
- if (!brief2.goalId || brief2.revision === void 0) {
14560
- state.goal = { id: admitted.goalId, revision: admitted.revision };
14561
- if (checkCallOnDone(prompt)) state.callOnDone = true;
14562
- send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
14563
- return;
14564
- }
14565
- } else {
14566
- return;
14567
- }
14568
- }
14569
- unheard(brief2, state);
14570
- if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
14571
- else state.goal = { id: brief2.goalId, revision: brief2.revision };
14572
- const outcome = brief2.outcome?.trim() ?? "";
14573
- const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
14574
- const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
14575
- if (checkCallOnDone(fullText)) state.callOnDone = true;
14576
- send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
14577
- return;
14578
- }
14579
- const brief = await rail.read(state.goal.id);
14580
- state.goal.revision = brief.revision ?? state.goal.revision;
14581
- for (const answer of brief.answers ?? []) {
14582
- const resolve3 = state.asks?.get(answer.requestEntryId);
14583
- if (!resolve3) continue;
14584
- state.asks.delete(answer.requestEntryId);
14585
- resolve3(answer.result);
14586
- }
14587
- if (ended(brief.state)) {
14588
- state.goal = void 0;
14589
- for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
14590
- state.asks?.clear();
14591
- return;
14592
- }
14593
- if (state.asks?.size || !state.resting) return;
14594
- const words2 = unheard(brief, state);
14595
- if (words2.length) {
14596
- const text = words2.join("\n\n");
14597
- if (checkCallOnDone(text)) state.callOnDone = true;
14598
- send2(session, state, text);
14958
+ function resetOf(line, now) {
14959
+ const m = /resets\s+(?:at\s+)?(?:([a-z]{3})[a-z]*\s+(\d{1,2}),?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b(?:\s*\(([^)]+)\))?/i.exec(line);
14960
+ if (!m) return null;
14961
+ const zone = zoneOr(m[6]);
14962
+ let hour = Number(m[3]) % 12;
14963
+ if (m[5].toLowerCase() === "pm") hour += 12;
14964
+ const minute = Number(m[4] ?? 0);
14965
+ const today = partsAt(now, zone);
14966
+ if (m[1]) {
14967
+ const month = MONTHS.indexOf(m[1].toLowerCase());
14968
+ if (month < 0) return null;
14969
+ const at2 = wallToEpoch(today.year, month, Number(m[2]), hour, minute, zone);
14970
+ return at2 > now ? at2 : wallToEpoch(today.year + 1, month, Number(m[2]), hour, minute, zone);
14971
+ }
14972
+ const at = wallToEpoch(today.year, today.month, today.day, hour, minute, zone);
14973
+ return at > now ? at : wallToEpoch(today.year, today.month, today.day + 1, hour, minute, zone);
14974
+ }
14975
+ function zoneOr(zone) {
14976
+ const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
14977
+ if (!zone) return local;
14978
+ try {
14979
+ new Intl.DateTimeFormat("en-US", { timeZone: zone.trim() });
14980
+ return zone.trim();
14981
+ } catch {
14982
+ return local;
14599
14983
  }
14600
14984
  }
14601
- function send2(session, state, text) {
14602
- state.resting = false;
14603
- session.send(text);
14985
+ function partsAt(t, zone) {
14986
+ const p = Object.fromEntries(
14987
+ new Intl.DateTimeFormat("en-US", { timeZone: zone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" }).formatToParts(new Date(t)).map((x) => [x.type, x.value])
14988
+ );
14989
+ return { year: Number(p.year), month: Number(p.month) - 1, day: Number(p.day), hour: Number(p.hour), minute: Number(p.minute), second: Number(p.second) };
14604
14990
  }
14605
- async function pump(session, state, rail, opts) {
14606
- const promptRef = opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : void 0;
14607
- for (; ; ) {
14608
- try {
14609
- await step(session, state, rail, promptRef);
14610
- } catch {
14991
+ function wallToEpoch(year, month, day, hour, minute, zone) {
14992
+ const wall = Date.UTC(year, month, day, hour, minute);
14993
+ const offset = (t) => {
14994
+ const p = partsAt(t, zone);
14995
+ return Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - Math.floor(t / 1e3) * 1e3;
14996
+ };
14997
+ const first = wall - offset(wall);
14998
+ return wall - offset(first);
14999
+ }
15000
+ function clock3(t) {
15001
+ return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
15002
+ }
15003
+ var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
15004
+ var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on where you stopped.";
15005
+ var OWED = "That turn was given this when the limit stopped it, and may not have got to it. Act on it now, unless you already have:";
15006
+ var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
15007
+ var GATHER_MS = 9e4;
15008
+ function episodes(opts = {}) {
15009
+ const gather2 = opts.gatherMs ?? GATHER_MS;
15010
+ const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
15011
+ let open = null;
15012
+ return {
15013
+ paused(name, now) {
15014
+ if (!open || open.called) open = { since: now, paused: [], running: [], called: false, scheduled: false };
15015
+ if (!open.paused.includes(name)) open.paused.push(name);
15016
+ open.running = open.running.filter((n) => n !== name);
15017
+ },
15018
+ running(name, call, log2) {
15019
+ const episode = open;
15020
+ if (!episode || !episode.paused.includes(name)) return;
15021
+ if (episode.called) {
15022
+ log2?.(`\u25B6 ${name} is running again after the limit (the call already went out)`);
15023
+ return;
15024
+ }
15025
+ if (!episode.running.includes(name)) episode.running.push(name);
15026
+ if (episode.scheduled) return;
15027
+ episode.scheduled = true;
15028
+ later(() => {
15029
+ if (!episode.running.length) {
15030
+ episode.scheduled = false;
15031
+ return;
15032
+ }
15033
+ episode.called = true;
15034
+ const rest = episode.paused.filter((n) => !episode.running.includes(n));
15035
+ const text = [
15036
+ `Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
15037
+ rest.length ? `It has reset: ${names(episode.running)} ${episode.running.length > 1 ? "are" : "is"} running again, and ${names(rest)} ${rest.length > 1 ? "haven't" : "hasn't"} started again yet.` : `It has reset, and ${episode.paused.length > 1 ? "they have all" : "it has"} carried on.`
15038
+ ].join(" ");
15039
+ log2?.(`\u260E one call for ${episode.paused.length} paused agent${episode.paused.length === 1 ? "" : "s"}: ${names(episode.paused)}`);
15040
+ void call(text).catch((error) => log2?.(`\u26A0 could not place the limit call: ${error.message}`));
15041
+ }, gather2);
14611
15042
  }
14612
- if (!await opts.onIdle()) return;
14613
- await (opts.pause ?? wait)();
14614
- }
15043
+ };
14615
15044
  }
15045
+ var LIMITS = episodes();
14616
15046
 
14617
15047
  // src/paigy/bridge.ts
14618
15048
  function railFor(opts) {
14619
15049
  return {
14620
- claim: () => claimGoal(void 0, opts),
15050
+ claim: (goalId) => claimGoal(goalId, opts),
14621
15051
  read: (goalId) => getGoal(goalId, opts),
14622
15052
  update: (goalId, input) => updateGoal(goalId, input, opts),
14623
15053
  // One read and back: the pump owns the waiting, so a Call must not hold a window here.
14624
15054
  contact: (input) => contact(input, { ...opts, waits: false }),
14625
- create: (input) => createGoal(input, opts)
15055
+ create: (input) => createGoal(input, opts),
15056
+ waiting: () => checkReplies(opts)
14626
15057
  };
14627
15058
  }
14628
15059
  function cancelAsks(state) {
@@ -14641,27 +15072,13 @@ function decisionFrom(result) {
14641
15072
  }
14642
15073
  return { allow: false, reason: "No approval given" };
14643
15074
  }
14644
- function wordsOf(result) {
14645
- if (!result || !("kind" in result)) return null;
14646
- switch (result.kind) {
14647
- case "text":
14648
- return result.text.trim() || null;
14649
- case "confirm":
14650
- return result.approved ? "yes" : "no";
14651
- case "one":
14652
- return result.option.label;
14653
- case "many":
14654
- case "rank":
14655
- return result.options.map((o) => o.label).join(", ");
14656
- }
14657
- }
14658
15075
  async function ask(state, rail, input) {
14659
15076
  if (!state.goal) return null;
14660
15077
  try {
14661
15078
  const sent = await rail.contact({
14662
- asks: [{ ask: input.ask, parentId: state.goal.id, ...input.options ? { options: input.options } : {} }],
15079
+ asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
14663
15080
  waiting: "hard",
14664
- channel: input.call ? "call" : "notification"
15081
+ channel: "call"
14665
15082
  });
14666
15083
  if ("recorded" in sent) return null;
14667
15084
  const entryId = sent.requestEntryIds[0];
@@ -14672,106 +15089,416 @@ async function ask(state, rail, input) {
14672
15089
  return null;
14673
15090
  }
14674
15091
  }
14675
- function checkCallOnDone(text) {
14676
- if (!text) return false;
14677
- return /\b(call|ring|phone)\b.*\b(done|finish(ed)?|complete(d)?)\b/i.test(text) || /\b(when|if|once)\b.*\b(done|finish(ed)?|complete(d)?)\b.*\b(call|ring|phone)\b/i.test(text) || /\bcall\s+(me|us)\b/i.test(text);
14678
- }
14679
- function parseQuestionOptions(text) {
14680
- if (!text) return { question: "" };
14681
- const trimmed = text.trim();
14682
- const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
14683
- const optionLines = [];
14684
- const questionLines = [];
14685
- for (const line of lines2) {
14686
- const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
14687
- if (bulletMatch) {
14688
- optionLines.push(bulletMatch[1].trim());
14689
- } else {
14690
- questionLines.push(line);
14691
- }
14692
- }
14693
- if (optionLines.length >= 2 && optionLines.length <= 6) {
14694
- return {
14695
- question: questionLines.join("\n\n").trim() || trimmed,
14696
- options: optionLines.map((raw) => {
14697
- const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
14698
- const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
14699
- if (colonSplit && colonSplit[1] && colonSplit[2]) {
14700
- return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
14701
- }
14702
- if (parenSplit && parenSplit[1] && parenSplit[2]) {
14703
- return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
14704
- }
14705
- return { label: raw.slice(0, 1e3) };
14706
- })
14707
- };
14708
- }
14709
- return { question: trimmed };
14710
- }
14711
15092
  async function askPermission(event, state, rail) {
14712
15093
  return decisionFrom(await ask(state, rail, {
14713
15094
  ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
14714
15095
  options: [
14715
15096
  { label: ALLOW, hint: "Permits the agent to run this command" },
14716
15097
  { label: "Deny", hint: "Blocks execution of this command" }
14717
- ],
14718
- call: true
14719
- }));
14720
- }
14721
- async function askQuestion(question, state, rail, opts) {
14722
- return wordsOf(await ask(state, rail, {
14723
- ask: question,
14724
- call: opts?.call ?? true,
14725
- options: opts?.options
15098
+ ]
14726
15099
  }));
14727
15100
  }
14728
15101
  async function finish(result, state, rail, opts) {
14729
15102
  const goal = state.goal;
14730
15103
  if (!goal) return;
14731
- state.goal = void 0;
14732
- const report = result?.trim() || "Done.";
14733
- const channel = opts?.call || state.callOnDone ? "call" : "notification";
15104
+ const progress = result?.trim().slice(0, 1e4) || void 0;
15105
+ let revision = goal.revision;
14734
15106
  try {
14735
- await rail.contact({ asks: [{ ask: report, parentId: goal.id }], waiting: "none", channel });
14736
- const { revision } = await rail.read(goal.id);
14737
- await rail.update(goal.id, { revision: revision ?? goal.revision, changes: { state: "done", progress: report.slice(0, 1e4) }, reason: "The agent finished its turn without asking anything." });
15107
+ const held = await rail.claim(goal.id);
15108
+ revision = held.revision ?? revision;
15109
+ const owed = state.noted?.some((b) => b.goals.includes(goal.id));
15110
+ if (!held.openDecisionNeedIds?.length && !owed) {
15111
+ await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
15112
+ state.goal = void 0;
15113
+ return;
15114
+ }
14738
15115
  } catch (error) {
14739
- opts?.log?.(`\u26A0 finish contact failed: ${error.message}`);
15116
+ opts?.log?.(`\u26A0 the Goal did not close, still holding it: ${error.message}`);
15117
+ }
15118
+ if (!progress) return;
15119
+ try {
15120
+ await rail.update(goal.id, { revision, changes: { progress }, reason: "The agent's turn ended; its Goal is not closed yet." });
15121
+ } catch (error) {
15122
+ opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
14740
15123
  }
14741
15124
  }
14742
- async function failedToStart(reason, state, rail, opts) {
15125
+ async function cannotWork(report, state, rail, opts) {
15126
+ state.resting = true;
14743
15127
  try {
14744
- const goalId = state.goal?.id ?? (await rail.claim()).goalId;
14745
- state.goal = void 0;
14746
- state.resting = true;
14747
- if (!goalId) return;
14748
- await rail.contact({
14749
- asks: [{ ask: `The session could not start, so nothing is working on this yet.
14750
-
14751
- ${reason}`, parentId: goalId }],
14752
- waiting: "none",
14753
- channel: "notification"
14754
- });
15128
+ if (!state.goal) {
15129
+ const claimed = await rail.claim();
15130
+ if (!claimed.goalId || claimed.revision === void 0) return;
15131
+ state.goal = { id: claimed.goalId, revision: claimed.revision };
15132
+ }
15133
+ await rail.contact({ asks: [{ ask: report, parentId: state.goal.id }], waiting: "none", channel: "notification" });
14755
15134
  } catch (error) {
14756
- opts?.log?.(`\u26A0 could not report the failed start: ${error.message}`);
15135
+ opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
14757
15136
  }
14758
15137
  }
14759
15138
  async function afterTurn(event, state, rail, opts) {
14760
- if (!event.failed && !endsWithQuestion(event.result)) {
14761
- await finish(event.result, state, rail, opts);
14762
- state.resting = true;
14763
- return null;
15139
+ if (event.failed) {
15140
+ const limit = limitIn(event.result, (opts?.now ?? Date.now)());
15141
+ if (limit) {
15142
+ state.paused = limit;
15143
+ if (state.typed) state.owed = state.typed;
15144
+ state.typed = void 0;
15145
+ state.carriedOn = false;
15146
+ state.resting = true;
15147
+ opts?.log?.(`\u23F8 paused by the usage limit until ${limit.resets}: nothing is typed in until then, and it carries on by itself`);
15148
+ return limit;
15149
+ }
15150
+ await cannotWork(`The agent stopped without finishing.
15151
+
15152
+ ${event.result?.trim() || "No result reported."}`, state, rail, opts);
15153
+ return void 0;
14764
15154
  }
14765
- const rawText = event.failed ? `The agent stopped without finishing.
15155
+ await finish(event.result, state, rail, opts);
15156
+ state.typed = void 0;
15157
+ state.resting = true;
15158
+ return void 0;
15159
+ }
14766
15160
 
14767
- ${event.result?.trim() || "No result reported."}` : event.result ?? "";
14768
- const parsed = parseQuestionOptions(rawText);
14769
- const reply = await askQuestion(parsed.question, state, rail, {
14770
- call: true,
14771
- options: parsed.options
15161
+ // src/paigy/conversation.ts
15162
+ import { randomUUID as randomUUID5 } from "crypto";
15163
+ var HARNESS_INSTRUCTION = "You are running as an unattended background harness agent; your user reaches you through Paigy and is not at this terminal. Nothing wakes you after your turn ends but the person, so wait on CI or a deploy inside the turn. The person never sees your turn's final message: it is kept as progress on this Goal. Anything the person needs \u2014 a question, a result, a blocker \u2014 you send through Paigy's contact tool, once; the harness sends nothing on your behalf. What the person says later arrives with the Goal it was said on named in front of it; answer them on that Goal (its id as the contact's parentId). " + DONE_MESSAGE;
15164
+ var SAFETY_MS = 6e4;
15165
+ var ASK_BEAT_MS = 5e3;
15166
+ function beat() {
15167
+ let pending = null;
15168
+ let wakeUp = null;
15169
+ return {
15170
+ nudge(cause) {
15171
+ if (wakeUp) {
15172
+ const w = wakeUp;
15173
+ wakeUp = null;
15174
+ w(cause);
15175
+ } else pending ??= cause;
15176
+ },
15177
+ next(ms) {
15178
+ if (pending) {
15179
+ const cause = pending;
15180
+ pending = null;
15181
+ return Promise.resolve(cause);
15182
+ }
15183
+ return new Promise((resolve3) => {
15184
+ const timer = setTimeout(() => {
15185
+ wakeUp = null;
15186
+ resolve3("safety");
15187
+ }, ms);
15188
+ wakeUp = (cause) => {
15189
+ clearTimeout(timer);
15190
+ resolve3(cause);
15191
+ };
15192
+ });
15193
+ }
15194
+ };
15195
+ }
15196
+ var BEAT_REPORT_MS = 10 * 6e4;
15197
+ var ended = (state) => state === "done" || state === "cancelled";
15198
+ function unheard(brief, state, since = null, after = -1) {
15199
+ const seen = state.seen ??= /* @__PURE__ */ new Set();
15200
+ const words2 = [];
15201
+ for (const [i, e] of (brief.entries ?? []).entries()) {
15202
+ if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
15203
+ if (i <= after) {
15204
+ seen.add(e.entryId);
15205
+ continue;
15206
+ }
15207
+ if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
15208
+ seen.add(e.entryId);
15209
+ if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
15210
+ const text = entryWords(e).trim();
15211
+ if (text) words2.push(text);
15212
+ }
15213
+ return words2;
15214
+ }
15215
+ function sinceTheWork(brief) {
15216
+ const entries = brief.entries ?? [];
15217
+ let after = -1;
15218
+ for (const [i, e] of entries.entries()) if (!e.authorParticipant.startsWith("human:")) after = i;
15219
+ const last = entries[after];
15220
+ const handed = last && brief.ownerParticipant && last.authorParticipant !== brief.ownerParticipant;
15221
+ const note2 = handed ? entryWords(last).trim() : "";
15222
+ return { after, ...note2 ? { note: note2 } : {} };
15223
+ }
15224
+ var baseline = (goal, subtree) => ({
15225
+ goal,
15226
+ goals: new Map((subtree ?? []).map((g) => [g.goalId, { since: g.updatedAt ?? null, at: g.updatedAt ?? null }]))
15227
+ });
15228
+ function movedBelow(held, state) {
15229
+ const goals = state.below.goals;
15230
+ return (held.subtree ?? []).flatMap((g) => {
15231
+ const known = goals.get(g.goalId);
15232
+ const stamp = g.updatedAt ?? null;
15233
+ if (!known) return [{ goal: g, since: null }];
15234
+ if (known.at === null) {
15235
+ known.since = known.at = stamp;
15236
+ return [];
15237
+ }
15238
+ return stamp !== null && stamp !== known.at ? [{ goal: g, since: known.since }] : [];
14772
15239
  });
14773
- if (!reply) state.resting = true;
14774
- return reply;
15240
+ }
15241
+ function named(goalId, title, brief) {
15242
+ const line = (title ?? brief.outcome ?? "").trim().split("\n")[0].trim();
15243
+ const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
15244
+ return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
15245
+ }
15246
+ async function gather(state, rail, held) {
15247
+ const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
15248
+ const look2 = (goalId, via, title, since) => {
15249
+ if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
15250
+ };
15251
+ const waiting = await rail.waiting().catch(() => null);
15252
+ const listed = /* @__PURE__ */ new Set();
15253
+ const announced = state.announced ??= /* @__PURE__ */ new Set();
15254
+ if (waiting) {
15255
+ for (const id of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id)) announced.delete(id);
15256
+ }
15257
+ for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
15258
+ for (const g of waiting?.review ?? []) {
15259
+ listed.add(g.goalId);
15260
+ look2(g.goalId, "waiting work", g.title);
15261
+ }
15262
+ for (const d of waiting?.deliveries ?? []) for (const id of d.goalIds) {
15263
+ listed.add(id);
15264
+ look2(id, "waiting work");
15265
+ }
15266
+ const moved = movedBelow(held, state);
15267
+ for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
15268
+ const heard = [];
15269
+ const read = /* @__PURE__ */ new Set();
15270
+ for (const [goalId, { since, ...found }] of looks) {
15271
+ let brief = found.brief;
15272
+ if (!brief) {
15273
+ try {
15274
+ brief = await rail.read(goalId);
15275
+ } catch (error) {
15276
+ if (error.status === 404) read.add(goalId);
15277
+ continue;
15278
+ }
15279
+ }
15280
+ read.add(goalId);
15281
+ const fresh = found.via === "waiting work" || found.via === "assigned" ? sinceTheWork(brief) : { after: -1 };
15282
+ if (found.via === "assigned") announced.add(goalId);
15283
+ heard.push({ goalId, ...found, brief, words: unheard(brief, state, since ?? null, fresh.after), ...fresh.note ? { note: fresh.note } : {} });
15284
+ }
15285
+ for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
15286
+ return { heard, listed };
15287
+ }
15288
+ var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; claim_goal with its id when you take it up.";
15289
+ var says = (h) => h.words.length > 0 || h.via === "assigned";
15290
+ function whatItIs(h) {
15291
+ const outcome = h.brief.outcome?.trim() ?? "";
15292
+ const name = (h.title ?? outcome).trim().split("\n")[0].trim();
15293
+ return outcome && outcome !== name ? [outcome] : [];
15294
+ }
15295
+ var block = (h) => [
15296
+ ...h.via === "assigned" ? [GIVEN] : [],
15297
+ named(h.goalId, h.title, h.brief),
15298
+ ...h.via === "assigned" ? whatItIs(h) : [],
15299
+ ...h.note ? [`The last note on it, from the agent before you: ${h.note}`] : [],
15300
+ ...h.words
15301
+ ].join("\n\n");
15302
+ var short = (ids) => ids.map((id) => id.slice(0, 8)).join(", ");
15303
+ var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
15304
+ async function hear(session, state, rail, held, opts) {
15305
+ const log2 = opts?.log;
15306
+ const { heard, listed } = await gather(state, rail, held);
15307
+ const said = heard.filter(says);
15308
+ if (said.length) {
15309
+ send2(session, state, said.map(block).join("\n\n"));
15310
+ const worded = said.filter((h) => h.words.length);
15311
+ if (worded.length) log2?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
15312
+ const goals = given(said);
15313
+ if (goals.length) {
15314
+ log2?.(`\u25B8 announced assigned ${short(goals)} (via rest)`);
15315
+ if (opts?.tally) opts.tally.announced += goals.length;
15316
+ }
15317
+ }
15318
+ for (const h of heard) if (listed.has(h.goalId)) await acknowledge(h.goalId, h.brief, state, rail, log2);
15319
+ }
15320
+ var NOTED = "New from the person while you work. Carry on unless it changes what you're doing: it comes back to you as your next turn when this one ends.";
15321
+ var SEEN_MID_TURN = "While you worked, the person said this, and you saw it mid-task. Act on it now, unless you already have.";
15322
+ async function note(state, rail, held, inbox, how) {
15323
+ const { heard } = await gather(state, rail, held);
15324
+ const said = heard.filter(says);
15325
+ if (!said.length) return 0;
15326
+ const words2 = said.map(block).join("\n\n");
15327
+ const goals = said.map((h) => h.goalId);
15328
+ const assigned = given(said);
15329
+ if (how.tally) how.tally.announced += assigned.length;
15330
+ let name = "";
15331
+ try {
15332
+ name = inbox.put(`${NOTED}
15333
+
15334
+ ${words2}`);
15335
+ how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
15336
+ if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
15337
+ } catch (error) {
15338
+ how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
15339
+ }
15340
+ (state.noted ??= []).push({ name, goals, words: words2, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
15341
+ return 1;
15342
+ }
15343
+ function receipts(state, inbox, log2) {
15344
+ let seen = 0;
15345
+ for (const b of state.noted ?? []) {
15346
+ if (b.seen !== void 0 || !b.name) continue;
15347
+ const at = inbox.taken(b.name);
15348
+ if (at === null) continue;
15349
+ b.seen = at;
15350
+ seen++;
15351
+ log2?.(`\u2713 seen mid-turn: ${short(b.goals)} ${Math.max(0, Math.round((at - b.at) / 1e3))}s after the ${b.cause}`);
15352
+ }
15353
+ return seen;
15354
+ }
15355
+ function settle(session, state, inbox, log2) {
15356
+ const batches = state.noted ?? [];
15357
+ state.noted = [];
15358
+ const seen = [];
15359
+ const unseen = [];
15360
+ for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
15361
+ send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
15362
+ log2?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
15363
+ }
15364
+ async function acknowledge(goalId, brief, state, rail, log2) {
15365
+ const acked = state.acked ??= /* @__PURE__ */ new Map();
15366
+ if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
15367
+ acked.set(goalId, brief.revision);
15368
+ try {
15369
+ const claimed = await rail.claim(goalId);
15370
+ const done = await rail.update(goalId, { revision: claimed.revision ?? brief.revision ?? 1, changes: { reviewed: true }, reason: "Read by the harness session; the person's words on it are typed in." });
15371
+ acked.set(goalId, done.revision);
15372
+ if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
15373
+ } catch (error) {
15374
+ log2?.(`\u26A0 could not acknowledge ${goalId.slice(0, 8)}: ${error.message} \u2014 its words are typed in; it stays listed until acknowledged`);
15375
+ }
15376
+ }
15377
+ async function step(session, state, rail, opts) {
15378
+ if (state.paused) {
15379
+ if ((opts?.now ?? Date.now)() < state.paused.until) {
15380
+ if (opts?.tally) opts.tally.paused = (opts.tally.paused ?? 0) + 1;
15381
+ return;
15382
+ }
15383
+ opts?.log?.(`\u25B6 resumed after the limit (it reset at ${state.paused.resets}): told to carry on, with what arrived meanwhile`);
15384
+ state.paused = void 0;
15385
+ state.carryOn = true;
15386
+ }
15387
+ await look(session, state, rail, opts);
15388
+ if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
15389
+ }
15390
+ async function look(session, state, rail, opts) {
15391
+ if (opts?.inbox) {
15392
+ const seen = receipts(state, opts.inbox, opts.log);
15393
+ if (opts.tally) opts.tally.seen += seen;
15394
+ }
15395
+ if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
15396
+ if (!state.goal) {
15397
+ if (!state.resting) return;
15398
+ let brief2 = await rail.claim();
15399
+ if (!brief2.goalId || brief2.revision === void 0) {
15400
+ if (opts?.initialPrompt?.trim()) {
15401
+ const prompt = opts.initialPrompt.trim();
15402
+ opts.initialPrompt = void 0;
15403
+ const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID5() });
15404
+ brief2 = await rail.claim();
15405
+ if (!brief2.goalId || brief2.revision === void 0) {
15406
+ state.goal = { id: admitted.goalId, revision: admitted.revision };
15407
+ send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
15408
+ return;
15409
+ }
15410
+ } else {
15411
+ return;
15412
+ }
15413
+ }
15414
+ unheard(brief2, state);
15415
+ if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
15416
+ else {
15417
+ state.goal = { id: brief2.goalId, revision: brief2.revision };
15418
+ state.below = baseline(brief2.goalId, brief2.subtree);
15419
+ }
15420
+ const outcome = brief2.outcome?.trim() ?? "";
15421
+ const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
15422
+ const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
15423
+ const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
15424
+ const fullText = [outcome, earlier, ...said].filter(Boolean).join("\n\n");
15425
+ send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
15426
+ return;
15427
+ }
15428
+ const brief = await rail.read(state.goal.id);
15429
+ state.goal.revision = brief.revision ?? state.goal.revision;
15430
+ for (const answer of brief.answers ?? []) {
15431
+ const resolve3 = state.asks?.get(answer.requestEntryId);
15432
+ if (!resolve3) continue;
15433
+ state.asks.delete(answer.requestEntryId);
15434
+ resolve3(answer.result);
15435
+ }
15436
+ if (ended(brief.state)) {
15437
+ state.goal = void 0;
15438
+ for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
15439
+ state.asks?.clear();
15440
+ return;
15441
+ }
15442
+ if (state.below?.goal !== state.goal.id) state.below = baseline(state.goal.id, brief.subtree);
15443
+ if (state.asks?.size) return;
15444
+ if (!state.resting) {
15445
+ if (!opts?.inbox) return;
15446
+ const noted = await note(state, rail, brief, opts.inbox, { cause: opts.cause ?? "safety", now: opts.now ?? Date.now, ...opts.log ? { log: opts.log } : {}, ...opts.tally ? { tally: opts.tally } : {} });
15447
+ if (opts.tally) opts.tally.noted += noted;
15448
+ return;
15449
+ }
15450
+ await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
15451
+ }
15452
+ function send2(session, state, text) {
15453
+ state.resting = false;
15454
+ let words2 = text;
15455
+ if (state.carryOn) {
15456
+ state.carryOn = false;
15457
+ state.carriedOn = true;
15458
+ const owed = state.owed;
15459
+ state.owed = void 0;
15460
+ words2 = [owed, text].filter(Boolean).join("\n\n");
15461
+ text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
15462
+ }
15463
+ state.typed = words2 || void 0;
15464
+ session.send(text);
15465
+ }
15466
+ async function pump(session, state, rail, opts) {
15467
+ const clock4 = opts.beat ?? beat();
15468
+ const now = opts.now ?? Date.now;
15469
+ const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
15470
+ const tally = { noted: 0, seen: 0, announced: 0 };
15471
+ const stepOpts = {
15472
+ ...opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : {},
15473
+ ...opts.log ? { log: opts.log } : {},
15474
+ ...opts.inbox ? { inbox: opts.inbox } : {},
15475
+ now,
15476
+ tally
15477
+ };
15478
+ let since = now();
15479
+ let cause = "start";
15480
+ for (; ; ) {
15481
+ if (cause === "wake" && !state.resting && !state.asks?.size && !opts.inbox) counts.skipped++;
15482
+ else {
15483
+ if (cause !== "start") counts[cause]++;
15484
+ try {
15485
+ stepOpts.cause = cause;
15486
+ await step(session, state, rail, stepOpts);
15487
+ } catch {
15488
+ }
15489
+ }
15490
+ if (!await opts.onIdle()) return;
15491
+ if (now() - since >= BEAT_REPORT_MS) {
15492
+ const read = counts.wake + counts.rest + counts.safety;
15493
+ if (read || counts.skipped || tally.noted || tally.seen || tally.announced) {
15494
+ opts.log?.(`\u25B9 beats: ${read} read (${counts.wake} wake, ${counts.rest} rest, ${counts.safety} safety), ${counts.skipped} wake mid-turn skipped, ${tally.noted} noted mid-turn, ${tally.seen} seen by the hook, ${tally.announced} assigned announced${tally.paused ? `, ${tally.paused} held by the usage limit` : ""}`);
15495
+ }
15496
+ counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
15497
+ since = now();
15498
+ }
15499
+ cause = await clock4.next(state.asks?.size ? ASK_BEAT_MS : state.paused ? Math.max(1e3, Math.min(SAFETY_MS, state.paused.until - now())) : SAFETY_MS);
15500
+ if (!await opts.onIdle()) return;
15501
+ }
14775
15502
  }
14776
15503
 
14777
15504
  // src/run.ts
@@ -14779,15 +15506,32 @@ function runHarness(opts) {
14779
15506
  let running = true;
14780
15507
  let tail = [];
14781
15508
  const state = { resting: true };
15509
+ const clock4 = beat();
14782
15510
  let session = null;
14783
15511
  const speaksAs = opts.resume ?? opts.session;
14784
15512
  const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
15513
+ let inbox;
15514
+ if (opts.harness === "claude") {
15515
+ try {
15516
+ inbox = inboxFor();
15517
+ } catch (error) {
15518
+ opts.log(`\u26A0 no inbox (${error.message}): what the person says mid-turn waits for the turn to end`);
15519
+ }
15520
+ }
15521
+ const name = opts.name ?? "The agent";
15522
+ function runningAgain() {
15523
+ state.carriedOn = false;
15524
+ opts.log("\u25B6 running again after the limit");
15525
+ LIMITS.running(name, (text) => rail.contact({ asks: [{ ask: text, ...state.goal ? { parentId: state.goal.id } : {} }], waiting: "none", channel: "call" }), opts.log);
15526
+ }
14785
15527
  async function handle(event) {
14786
15528
  if (event.kind === "error") {
14787
15529
  opts.log(`\u26A0 ${event.message}`);
14788
15530
  if (event.fatal && running) {
14789
15531
  opts.log(`\u2716 could not start: ${event.message}`);
14790
- await failedToStart(event.message, state, rail, { log: opts.log });
15532
+ await cannotWork(`The session could not start, so nothing is working on this yet.
15533
+
15534
+ ${event.message}`, state, rail, { log: opts.log });
14791
15535
  stop2();
14792
15536
  }
14793
15537
  return;
@@ -14806,6 +15550,7 @@ function runHarness(opts) {
14806
15550
  return;
14807
15551
  }
14808
15552
  if (event.kind === "work") {
15553
+ if (state.carriedOn) runningAgain();
14809
15554
  opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
14810
15555
  tail = pushWork(tail, workLine(event));
14811
15556
  return;
@@ -14813,20 +15558,10 @@ function runHarness(opts) {
14813
15558
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
14814
15559
  if (event.kind === "idle") {
14815
15560
  tail = [];
14816
- if (endsWithQuestion(event.result)) {
14817
- const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
14818
- if (local?.trim() && session && running) {
14819
- opts.log(`you (here): ${local.split("\n")[0] ?? ""}`);
14820
- state.resting = false;
14821
- session.send(local);
14822
- return;
14823
- }
14824
- }
14825
- const reply = await afterTurn(event, state, rail, { log: opts.log });
14826
- if (reply && session && running) {
14827
- opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
14828
- session.send(reply);
14829
- }
15561
+ const carried = state.carriedOn && !event.failed;
15562
+ if (await afterTurn(event, state, rail, { log: opts.log })) LIMITS.paused(name, Date.now());
15563
+ else if (carried) runningAgain();
15564
+ clock4.nudge("rest");
14830
15565
  }
14831
15566
  }
14832
15567
  session = startSession({
@@ -14837,19 +15572,25 @@ function runHarness(opts) {
14837
15572
  ...opts.resume ? { resume: opts.resume } : {},
14838
15573
  ...opts.session ? { session: opts.session } : {},
14839
15574
  ...opts.bin ? { bin: opts.bin } : {},
14840
- ...opts.onExit ? { onExit: opts.onExit } : {},
15575
+ ...inbox ? { inbox: inbox.dir } : {},
15576
+ onExit: () => {
15577
+ inbox?.close();
15578
+ opts.onExit?.();
15579
+ },
14841
15580
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
14842
15581
  });
14843
- void pump(session, state, rail, { onIdle: () => running, initialPrompt: opts.prompt }).catch(
15582
+ void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
14844
15583
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
14845
15584
  );
14846
15585
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
14847
15586
  function stop2() {
14848
15587
  running = false;
15588
+ clock4.nudge("wake");
14849
15589
  tail = [];
14850
15590
  cancelAsks(state);
14851
15591
  session?.stop();
14852
15592
  session = null;
15593
+ inbox?.close();
14853
15594
  opts.log("\u25A0 stopped");
14854
15595
  }
14855
15596
  return {
@@ -14857,7 +15598,9 @@ function runHarness(opts) {
14857
15598
  session?.send(text);
14858
15599
  },
14859
15600
  working: () => running && state.resting !== true,
14860
- tail: () => [...tail],
15601
+ // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
15602
+ tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
15603
+ nudge: () => clock4.nudge("wake"),
14861
15604
  stop: stop2
14862
15605
  };
14863
15606
  }
@@ -14865,7 +15608,7 @@ function runHarness(opts) {
14865
15608
  // src/update.ts
14866
15609
  import { execFile } from "child_process";
14867
15610
  import { createRequire } from "module";
14868
- import { dirname as dirname3, join as join7 } from "path";
15611
+ import { dirname as dirname3, join as join9 } from "path";
14869
15612
  import { promisify } from "util";
14870
15613
  var SERVICE_LABEL = "ai.paigy.harness";
14871
15614
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -14892,7 +15635,7 @@ async function latestVersion() {
14892
15635
  }
14893
15636
  async function install(version) {
14894
15637
  const bin = dirname3(process.execPath);
14895
- await promisify(execFile)(join7(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
15638
+ await promisify(execFile)(join9(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
14896
15639
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
14897
15640
  timeout: 5 * 6e4
14898
15641
  });
@@ -14943,10 +15686,10 @@ function selfUpdater(deps) {
14943
15686
  }
14944
15687
 
14945
15688
  // src/host.ts
14946
- var HOST_FILE = join8(homedir7(), ".paigy", "host.json");
15689
+ var HOST_FILE = join10(homedir9(), ".paigy", "host.json");
14947
15690
  function readHostState() {
14948
15691
  try {
14949
- const s = JSON.parse(readFileSync4(HOST_FILE, "utf8"));
15692
+ const s = JSON.parse(readFileSync5(HOST_FILE, "utf8"));
14950
15693
  if (Date.now() - s.at > 2e4) return null;
14951
15694
  try {
14952
15695
  process.kill(s.pid, 0);
@@ -14968,18 +15711,36 @@ var claimedRun = (sessionId2) => {
14968
15711
  return { slot, key: runKey(slot) };
14969
15712
  };
14970
15713
  var launchRun = (spec) => ({ session: spec.sessionId });
14971
- async function slotHasWaitingWork(token, session) {
14972
- return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
15714
+ async function waitingWork(token, session) {
15715
+ let found;
15716
+ try {
15717
+ found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
15718
+ } catch (error) {
15719
+ return { skip: `could not read what is waiting (${error.message})` };
15720
+ }
15721
+ const n = found.deliveries.length;
15722
+ if (n) return { waiting: n === 1 ? "a message" : `${n} messages` };
15723
+ const next = found.claimable;
15724
+ return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}` } : { skip: "has nothing waiting" };
15725
+ }
15726
+ function evict(runs, key, run) {
15727
+ if (runs.get(key)?.run === run) runs.delete(key);
14973
15728
  }
14974
15729
  var PRESENCE_FRESH_MS = 3 * 6e4;
14975
- function wakeAction(live, now, serverSeenAt) {
15730
+ function wakeAction(live, now, serverSeenAt, here2 = {}) {
14976
15731
  if (live) return { action: "skip", why: "run" };
14977
- if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
15732
+ if (here2.holder) return { action: "skip", why: "holder" };
15733
+ if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
15734
+ if (here2.goneAt !== void 0 && serverSeenAt <= here2.goneAt) return { action: "spawn", why: "gone" };
15735
+ return { action: "skip", why: "presence" };
15736
+ }
14978
15737
  return { action: "spawn", why: "nobody" };
14979
15738
  }
14980
15739
  function startHost(opts) {
14981
15740
  const runs = /* @__PURE__ */ new Map();
14982
15741
  const asHost = { token: opts.token };
15742
+ const goneAt = /* @__PURE__ */ new Map();
15743
+ const heldHere = /* @__PURE__ */ new Set();
14983
15744
  const refreshIdentities = async () => {
14984
15745
  for (const slot of listSlots()) {
14985
15746
  if (slot === "Desktop") continue;
@@ -14999,7 +15760,7 @@ function startHost(opts) {
14999
15760
  working: () => [...runs.values()].filter((r) => r.run.working()).length,
15000
15761
  log: opts.log
15001
15762
  });
15002
- const beat = () => {
15763
+ const beat2 = () => {
15003
15764
  void refreshIdentities();
15004
15765
  void update().catch(() => {
15005
15766
  });
@@ -15028,9 +15789,11 @@ function startHost(opts) {
15028
15789
  token: spec.token,
15029
15790
  ...launchRun(spec),
15030
15791
  onExit: () => {
15031
- runs.delete(key);
15792
+ evict(runs, key, run);
15793
+ goneAt.set(slot, Date.now());
15032
15794
  opts.log(`\u2716 ${label} ended`);
15033
15795
  },
15796
+ name: label,
15034
15797
  log: log2
15035
15798
  });
15036
15799
  runs.set(key, { run, label, token: spec.token });
@@ -15057,7 +15820,13 @@ function startHost(opts) {
15057
15820
  return presence;
15058
15821
  }
15059
15822
  let lastPresence = /* @__PURE__ */ new Map();
15060
- async function wakeSlot(slot, presence) {
15823
+ async function evidence() {
15824
+ const presence = await presenceMap();
15825
+ lastHolders = localHolders();
15826
+ return { presence, holders: lastHolders };
15827
+ }
15828
+ let lastHolders = /* @__PURE__ */ new Map();
15829
+ async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
15061
15830
  {
15062
15831
  const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
15063
15832
  const key = runKey(slot);
@@ -15065,19 +15834,42 @@ function startHost(opts) {
15065
15834
  const live = runs.get(key);
15066
15835
  const tokenId = slotIdentity(slot).tokenId;
15067
15836
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
15068
- const decided = wakeAction(!!live, Date.now(), seenByServer);
15837
+ const holder = holders.get(slot);
15838
+ if (holder) heldHere.add(slot);
15839
+ else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
15840
+ const decided = wakeAction(!!live, Date.now(), seenByServer, { holder, goneAt: goneAt.get(slot) });
15069
15841
  if (decided.action === "skip") {
15070
- opts.log(decided.why === "run" ? `\u25CC ${slotName(slot) ?? slot} is running here \u2014 its pump types the work in; not spawning` : `\u25CC ${slotName(slot) ?? slot} is working somewhere this host cannot see (heartbeat) \u2014 not spawning`);
15842
+ if (decided.why === "run" && cause === "wake") live?.run.nudge();
15843
+ const name = slotName(slot) ?? slot;
15844
+ opts.log(decided.why === "run" ? `\u25CC ${name} is running here \u2014 ${cause === "wake" ? "woke its pump" : "its pump types the work in"}; not spawning` : decided.why === "holder" ? `\u25B8 ${name} already running (pid ${holder?.pid ?? "?"}) \u2014 not starting a second` : `\u25CC ${name} is working somewhere this host cannot see (heartbeat) \u2014 not spawning`);
15071
15845
  return;
15072
15846
  }
15847
+ const label = slotName(slot) ?? slot;
15848
+ const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
15073
15849
  const token = readToken(slot);
15074
- if (!token) return;
15850
+ if (!token) {
15851
+ skip("has no token on this machine");
15852
+ return;
15853
+ }
15075
15854
  const owner = ownerSession(slot);
15076
- if (owner && !allowed(owner.cwd, opts.wsDeps)) return;
15855
+ if (owner && !allowed(owner.cwd, opts.wsDeps)) {
15856
+ skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
15857
+ return;
15858
+ }
15077
15859
  const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
15078
- if (!workspace) return;
15079
- if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
15080
- const label = slotName(slot) ?? slot;
15860
+ if (!workspace) {
15861
+ skip("has no granted folder to wake in");
15862
+ return;
15863
+ }
15864
+ const found = await waitingWork(token, owner?.sessionId);
15865
+ if ("skip" in found) {
15866
+ skip(found.skip);
15867
+ return;
15868
+ }
15869
+ if (runs.has(key)) {
15870
+ skip("was started by the wake that read it first");
15871
+ return;
15872
+ }
15081
15873
  const log2 = (line) => opts.log(`[${label}] ${line}`);
15082
15874
  const run = runHarness({
15083
15875
  harness,
@@ -15086,17 +15878,20 @@ function startHost(opts) {
15086
15878
  prompt: "",
15087
15879
  token,
15088
15880
  ...owner ? { resume: owner.sessionId } : {},
15089
- // A dead run must not squat the slot — evict so the next wake can respawn.
15881
+ // A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
15882
+ // a run that is no longer the slot's must not take the live one's place away with it.
15090
15883
  onExit: () => {
15091
- runs.delete(key);
15884
+ evict(runs, key, run);
15885
+ goneAt.set(slot, Date.now());
15092
15886
  opts.log(`\u2716 ${label} ended`);
15093
15887
  },
15888
+ name: label,
15094
15889
  log: log2
15095
15890
  });
15096
15891
  runs.set(key, { run, label, token });
15097
15892
  void heartbeat(void 0, { token }).catch(() => {
15098
15893
  });
15099
- opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
15894
+ opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 ${found.waiting} was waiting, in ${workspace}${decided.why === "gone" ? " (its process here ended; its last heartbeat was not waited out)" : ""}`);
15100
15895
  }
15101
15896
  }
15102
15897
  const listening = /* @__PURE__ */ new Map();
@@ -15116,7 +15911,7 @@ function startHost(opts) {
15116
15911
  joining.add(slot);
15117
15912
  try {
15118
15913
  const sub = await subscribeWake(
15119
- () => void presenceMap().then((presence) => wakeSlot(slot, presence)).catch(() => {
15914
+ () => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
15120
15915
  }),
15121
15916
  { token }
15122
15917
  );
@@ -15130,8 +15925,8 @@ function startHost(opts) {
15130
15925
  }
15131
15926
  }
15132
15927
  async function sweepSlots() {
15133
- const presence = await presenceMap();
15134
- for (const slot of listSlots()) await wakeSlot(slot, presence);
15928
+ const seen = await evidence();
15929
+ for (const slot of listSlots()) await wakeSlot(slot, seen);
15135
15930
  }
15136
15931
  const ACTIVITY_MS = 2e3;
15137
15932
  const published = /* @__PURE__ */ new Map();
@@ -15156,12 +15951,12 @@ function startHost(opts) {
15156
15951
  };
15157
15952
  const publish = () => {
15158
15953
  try {
15159
- writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
15954
+ writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
15160
15955
  } catch {
15161
15956
  }
15162
15957
  };
15163
- beat();
15164
- const pulse = setInterval(beat, 6e4);
15958
+ beat2();
15959
+ const pulse = setInterval(beat2, 6e4);
15165
15960
  const spawnPoll = setInterval(() => {
15166
15961
  void claimAndSpawn();
15167
15962
  publish();
@@ -15187,10 +15982,10 @@ function startHost(opts) {
15187
15982
  // them live, and a run mid-turn makes it working.
15188
15983
  roster: () => {
15189
15984
  const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
15190
- const names = /* @__PURE__ */ new Map();
15191
- for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
15192
- for (const label of live.keys()) if (!names.has(label)) names.set(label, null);
15193
- return [...names.entries()].map(([name, slot]) => {
15985
+ const names2 = /* @__PURE__ */ new Map();
15986
+ for (const key of listSlots()) if (key !== "Desktop") names2.set(slotName(key) ?? key, key);
15987
+ for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
15988
+ return [...names2.entries()].map(([name, slot]) => {
15194
15989
  const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
15195
15990
  return {
15196
15991
  name,
@@ -15207,7 +16002,7 @@ function startHost(opts) {
15207
16002
  // sessions we started ourselves. The server already knows: `last_seen_at` on the
15208
16003
  // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
15209
16004
  // hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
15210
- running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
16005
+ running: live.has(name) || !!slot && lastHolders.has(slot) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
15211
16006
  // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
15212
16007
  // thing a tail can tell us. A process we did not spawn has no tail here to read.
15213
16008
  working: live.get(name)?.working() ?? false
@@ -15223,7 +16018,7 @@ function startHost(opts) {
15223
16018
  streamActivity();
15224
16019
  clearInterval(activityTick);
15225
16020
  try {
15226
- writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
16021
+ writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
15227
16022
  } catch {
15228
16023
  }
15229
16024
  }
@@ -15238,8 +16033,8 @@ function startHost(opts) {
15238
16033
  var OPEN_STATUSES = ["open", "assigned", "in_progress"];
15239
16034
  var DAY_MS = 864e5;
15240
16035
  var daysBetween = (from, now) => from ? Math.floor((now - Date.parse(from)) / DAY_MS) : null;
15241
- function assigneeToken(note) {
15242
- const ref = note.assignee ?? "";
16036
+ function assigneeToken(note2) {
16037
+ const ref = note2.assignee ?? "";
15243
16038
  return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
15244
16039
  }
15245
16040
  var STOP = /* @__PURE__ */ new Set([
@@ -15313,23 +16108,23 @@ function overlap(a, b) {
15313
16108
  return shared / (A.size + B.size - shared);
15314
16109
  }
15315
16110
  var SUPERSEDE_AT = 0.5;
15316
- function supersededBy(note, all) {
15317
- const mine = `${note.title} ${note.intent}`;
15318
- const at = Date.parse(note.createdAt);
16111
+ function supersededBy(note2, all) {
16112
+ const mine = `${note2.title} ${note2.intent}`;
16113
+ const at = Date.parse(note2.createdAt);
15319
16114
  let best = null;
15320
16115
  for (const other of all) {
15321
- if (other.id === note.id) continue;
16116
+ if (other.id === note2.id) continue;
15322
16117
  if (Date.parse(other.createdAt) <= at) continue;
15323
16118
  const score = overlap(mine, `${other.title} ${other.intent}`);
15324
16119
  if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
15325
16120
  }
15326
16121
  return best?.note ?? null;
15327
16122
  }
15328
- function doneEcho(note, all) {
15329
- const mine = `${note.title} ${note.intent}`;
16123
+ function doneEcho(note2, all) {
16124
+ const mine = `${note2.title} ${note2.intent}`;
15330
16125
  let best = null;
15331
16126
  for (const other of all) {
15332
- if (other.id === note.id || other.status !== "done") continue;
16127
+ if (other.id === note2.id || other.status !== "done") continue;
15333
16128
  const score = overlap(mine, `${other.title} ${other.intent}`);
15334
16129
  if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
15335
16130
  }
@@ -15384,8 +16179,8 @@ function dossier(notes, connections, now) {
15384
16179
  today: new Date(now).toISOString().slice(0, 10)
15385
16180
  };
15386
16181
  }
15387
- function affinityWhy(note, agent2) {
15388
- const shared = note.repoHints.filter((r) => agent2.repos.includes(r));
16182
+ function affinityWhy(note2, agent2) {
16183
+ const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
15389
16184
  if (shared.length && agent2.recentNotes)
15390
16185
  return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
15391
16186
  if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
@@ -15394,18 +16189,18 @@ function affinityWhy(note, agent2) {
15394
16189
  return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
15395
16190
  return null;
15396
16191
  }
15397
- function staleWhy(note) {
15398
- if (note.doneEcho) return `a later note says this shipped: "${note.doneEcho.title}"`;
15399
- if (note.supersededBy) return `superseded by "${note.supersededBy.title}"`;
15400
- if (note.rides >= 3) return `carried on ${note.rides} calls and still open`;
15401
- if (note.idleDays >= 30) return `no movement in ${note.idleDays} days`;
15402
- if (note.idleDays >= 14 && note.status === "open") return `unassigned for ${note.idleDays} days`;
16192
+ function staleWhy(note2) {
16193
+ if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
16194
+ if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
16195
+ if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
16196
+ if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
16197
+ if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
15403
16198
  return null;
15404
16199
  }
15405
16200
 
15406
16201
  // src/triage/judge.ts
15407
16202
  import { spawnSync as spawnSync2 } from "child_process";
15408
- import { homedir as homedir8 } from "os";
16203
+ import { homedir as homedir10 } from "os";
15409
16204
  import { delimiter as delimiter3 } from "path";
15410
16205
  var PRIVACY = {
15411
16206
  ollama: "never leaves this machine",
@@ -15509,7 +16304,7 @@ async function judgeFor(asked2, deps = {}) {
15509
16304
  const out = spawnSync2(bin, runner.argv(null), {
15510
16305
  input: prompt,
15511
16306
  encoding: "utf8",
15512
- cwd: homedir8(),
16307
+ cwd: homedir10(),
15513
16308
  timeout: RUN_TIMEOUT_MS,
15514
16309
  maxBuffer: 32 * 1024 * 1024,
15515
16310
  env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
@@ -15624,22 +16419,22 @@ function toProposal(verdicts, d, meta) {
15624
16419
  const stale = [];
15625
16420
  const assignments = /* @__PURE__ */ new Map();
15626
16421
  for (const v of verdicts) {
15627
- const note = byId.get(v.id);
15628
- if (!note) continue;
15629
- const item = (why) => ({ noteId: note.id, title: note.title, why });
16422
+ const note2 = byId.get(v.id);
16423
+ if (!note2) continue;
16424
+ const item = (why) => ({ noteId: note2.id, title: note2.title, why });
15630
16425
  if (v.verdict === "close") {
15631
- close.push(item(staleWhy(note) ?? v.why ?? "looks finished"));
16426
+ close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
15632
16427
  continue;
15633
16428
  }
15634
16429
  if (v.verdict === "stale") {
15635
- stale.push(item(staleWhy(note) ?? v.why ?? `no movement in ${note.idleDays} days`));
16430
+ stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
15636
16431
  continue;
15637
16432
  }
15638
16433
  if (v.verdict !== "assign" || !v.agent) continue;
15639
16434
  const agent2 = agentById.get(v.agent);
15640
16435
  if (!agent2) continue;
15641
16436
  const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
15642
- group.notes.push(item(affinityWhy(note, agent2) ?? v.why ?? "best match for this work"));
16437
+ group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
15643
16438
  assignments.set(agent2.token, group);
15644
16439
  }
15645
16440
  return {
@@ -15807,7 +16602,7 @@ var win = null;
15807
16602
  function log(message) {
15808
16603
  win?.webContents.send("paigy:log", message);
15809
16604
  }
15810
- var ICON = join9(here, "..", "assets", "icon.png");
16605
+ var ICON = join11(here, "..", "assets", "icon.png");
15811
16606
  function createWindow() {
15812
16607
  win = new BrowserWindow({
15813
16608
  // Sized for the working log, which is now the window's centerpiece; the layout is a
@@ -15819,9 +16614,9 @@ function createWindow() {
15819
16614
  minHeight: 520,
15820
16615
  icon: ICON,
15821
16616
  // Windows/Linux window icon; macOS uses the dock icon below
15822
- webPreferences: { preload: join9(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
16617
+ webPreferences: { preload: join11(here, "preload.cjs"), contextIsolation: true, nodeIntegration: false }
15823
16618
  });
15824
- void win.loadFile(join9(here, "..", "renderer", "index.html"));
16619
+ void win.loadFile(join11(here, "..", "renderer", "index.html"));
15825
16620
  }
15826
16621
  var host = null;
15827
16622
  app.whenReady().then(() => {
@@ -15890,7 +16685,7 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
15890
16685
  });
15891
16686
  ipcMain.handle("paigy:agent-log", (_e, name) => {
15892
16687
  try {
15893
- const all = readFileSync5(join9(homedir9(), ".paigy", "host.log"), "utf8").split("\n");
16688
+ const all = readFileSync6(join11(homedir11(), ".paigy", "host.log"), "utf8").split("\n");
15894
16689
  const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
15895
16690
  return mine.slice(-500).join("\n");
15896
16691
  } catch {