@paigy/harness 0.3.16 → 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 +1222 -364
  3. package/dist/main.js +1173 -321
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -16098,13 +16098,13 @@ var require_headers = __commonJS({
16098
16098
  return this.#headersList[kHeadersSortedMap];
16099
16099
  }
16100
16100
  const headers = [];
16101
- const names = this.#headersList.toSortedArray();
16101
+ const names2 = this.#headersList.toSortedArray();
16102
16102
  const cookies = this.#headersList.cookies;
16103
16103
  if (cookies === null || cookies.length === 1) {
16104
- return this.#headersList[kHeadersSortedMap] = names;
16104
+ return this.#headersList[kHeadersSortedMap] = names2;
16105
16105
  }
16106
- for (let i = 0; i < names.length; ++i) {
16107
- const { 0: name, 1: value } = names[i];
16106
+ for (let i = 0; i < names2.length; ++i) {
16107
+ const { 0: name, 1: value } = names2[i];
16108
16108
  if (name === "set-cookie") {
16109
16109
  for (let j = 0; j < cookies.length; ++j) {
16110
16110
  headers.push([name, cookies[j]]);
@@ -23809,9 +23809,9 @@ async function claimGoal(goalId, opts = {}) {
23809
23809
  if (!res.ok) await fail("claim_goal", res);
23810
23810
  return await res.json();
23811
23811
  }
23812
- async function getGoal(goalId, opts = {}) {
23812
+ async function getGoal(goalId, opts = {}, read = {}) {
23813
23813
  const token = authToken(opts.token) ?? "";
23814
- const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}`, {
23814
+ const res = ensureAuthed(await (opts.reach ?? reach)(`${BACKEND_URL}/api/goals/${encodeURIComponent(goalId)}${read.history ? "?history=1" : ""}`, {
23815
23815
  method: "GET",
23816
23816
  headers: { authorization: `Bearer ${token}`, "x-paigy-model": "goal-entry-v1" }
23817
23817
  }));
@@ -24170,7 +24170,7 @@ async function subscribeWake(onNudge, opts = {}) {
24170
24170
  }
24171
24171
  };
24172
24172
  }
24173
- var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24173
+ var import_undici, require2, BACKEND_URL, NETWORK_MSG, PROXY_ENV, agent, INSTANCE_ID, WORKSPACE_ID, envSession, lineage, ignoreOverride, defaultOptions, getDefaultOptions, getRefs, getRelativePath, parseCatchDef, integerDateParser, isJsonSchema7AllOfType, emojiRegex2, zodPatterns, ALPHA_NUMERIC, primitiveMappings, asAnyOf, parseOptionalDef, parsePipelineDef, parseReadonlyDef, selectParser, get$ref, addMeta, zodToJsonSchema, OPTIONS_MIN, OPTIONS_MAX, OptionSchema, OptionInputSchema, NIGHT, AskInputSchema, StartContactSchema, ContactSchema, CONTACT_SCHEMA, CONTACT_DESCRIPTION, CreateGoalSchema, CreateGoalToolSchema, CREATE_GOAL_DESCRIPTION, UpdateGoalSchema, UpdateGoalToolSchema, ClaimGoalSchema, GetGoalSchema, GET_GOAL_DESCRIPTION, UPDATE_GOAL_DESCRIPTION, CLAIM_GOAL_DESCRIPTION, CHECK_REPLIES_DESCRIPTION, CheckRepliesSchema, AGENT_TOOLS, AGENT_TOOL_NAMES, LIVE_MS, ContextSchema, ParticipantSchema, TransformSchema, VisualSchema, NotifyLevelSchema, SelectShapeSchema, ReceiptEventSchema, AttentionSchema, NotifyRequestFields, NotifyRequestSchema, NotifyStatusSchema, AgentStateSchema, TurnSchema, UserAnswerSchema, IntentSchema, RideAlongSchema, AwaitItemSchema, VoiceKeySchema, AgendaTurnSchema, CLAIM_STALE_MS, InboxItemSchema, APNS_TOKEN_RE, PushTokenSchema, MissedCallSchema, clock, QUIET, BrokerTuningSchema, UserSettingsSchema, HistoryItemSchema, ACTIVITY_LINES, ACTIVITY_LINE_MAX, AgentActivitySchema, ConnectionSummarySchema, LedgerItemSchema, AgentLedgerSchema, ReassignResultSchema, MoveRingSchema, MoveSchema, QueueQuestionSchema, QueueReplySchema, QueueItemSchema, COLD_AFTER_MS, NoteSourceSchema, NoteStatusSchema, NoteRepeatSchema, DecisionSchema, NoteSchema, TriageItemSchema, TriageAssignmentSchema, TriageStatusSchema, SubmitTriageSchema, TriageProposalSchema, AcceptTriageSchema, AcceptTriageResultSchema, DeliveryModeSchema, WAKE_EVENT, wakeChannel, RegisterDeliverySchema, OAuthStartSchema, DeliveryConfigSchema, WakeNudgeSchema, PairingStatusSchema, DeviceCodeSchema, DeviceInfoSchema, DeviceTokenSchema, SupportRequestSchema, NotificationFeedbackKindSchema, SlimOptionSchema, QuestionRowSchema, WorkStateSchema, WorkRowSchema, ComputerRowSchema, AgentRowSchema, SnapshotSchema, TOKEN_PATH, KEY_PATH, sleep, UnpairedError, ApiError, tokenOverride, authToken, AWAIT_WINDOW_MS, asked, NOT_SENT, HEARTBEAT_MS, RETRY_MS, JOIN, JOIN_TIMEOUT_MS, lines;
24174
24174
  var init_dist = __esm({
24175
24175
  "../../packages/sdk/dist/index.js"() {
24176
24176
  "use strict";
@@ -24639,8 +24639,13 @@ var init_dist = __esm({
24639
24639
  }).strict();
24640
24640
  UpdateGoalToolSchema = UpdateGoalSchema.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
24641
24641
  ClaimGoalSchema = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
24642
- GetGoalSchema = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
24643
- 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.";
24642
+ GetGoalSchema = external_exports.object({
24643
+ goalId: external_exports.string().uuid(),
24644
+ /** Every entry in full. Without it the read carries the person's words, open questions and your
24645
+ * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
24646
+ history: external_exports.boolean().optional()
24647
+ }).strict();
24648
+ 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.";
24644
24649
  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.`;
24645
24650
  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.";
24646
24651
  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.";
@@ -25059,7 +25064,13 @@ var init_dist = __esm({
25059
25064
  * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
25060
25065
  * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
25061
25066
  * author is not an agent this account holds (unpaired since, or a person). */
25062
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional()
25067
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
25068
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
25069
+ * its need offers, exactly as its own card carries them, present only when one of them has a
25070
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
25071
+ * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
25072
+ * says those, and the list stays small (an `html` is up to 16 KB). */
25073
+ options: external_exports.array(OptionSchema).optional()
25063
25074
  })).optional(),
25064
25075
  visuals: external_exports.array(VisualSchema).optional(),
25065
25076
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -25455,7 +25466,19 @@ var init_dist = __esm({
25455
25466
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
25456
25467
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
25457
25468
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
25458
- lastProgressAt: external_exports.string().nullable().optional()
25469
+ lastProgressAt: external_exports.string().nullable().optional(),
25470
+ /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
25471
+ * kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
25472
+ * note. A progress note is an Entry, so this is already the newer of the two: the person's note
25473
+ * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
25474
+ * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
25475
+ * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
25476
+ latest: external_exports.object({
25477
+ from: external_exports.enum(["person", "agent"]),
25478
+ said: external_exports.string(),
25479
+ at: external_exports.string(),
25480
+ entryId: external_exports.string()
25481
+ }).nullable().optional()
25459
25482
  });
25460
25483
  COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
25461
25484
  NoteSourceSchema = external_exports.enum(["app", "call"]);
@@ -25633,6 +25656,104 @@ var init_dist = __esm({
25633
25656
  "other"
25634
25657
  // anything else — the note carries it.
25635
25658
  ]);
25659
+ SlimOptionSchema = OptionSchema.omit({ html: true });
25660
+ QuestionRowSchema = external_exports.object({
25661
+ /** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
25662
+ id: external_exports.string(),
25663
+ deliveryId: external_exports.string(),
25664
+ entryId: external_exports.string(),
25665
+ /** The decision it waits on; null for an update, which asks nothing. */
25666
+ needId: external_exports.string().nullable(),
25667
+ goalIds: external_exports.array(external_exports.string()),
25668
+ /** The name of the work it is about, when the read could word it. */
25669
+ goalTitle: external_exports.string().optional(),
25670
+ tokenId: external_exports.string().optional(),
25671
+ name: external_exports.string(),
25672
+ title: external_exports.string(),
25673
+ body: external_exports.string(),
25674
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
25675
+ options: external_exports.array(SlimOptionSchema),
25676
+ hasPreview: external_exports.boolean(),
25677
+ blocking: external_exports.boolean(),
25678
+ askedAt: external_exports.string().datetime(),
25679
+ ring: InboxItemSchema.shape.ring,
25680
+ onCall: external_exports.literal(true).optional(),
25681
+ sealed: external_exports.boolean()
25682
+ });
25683
+ WorkStateSchema = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
25684
+ WorkRowSchema = external_exports.object({
25685
+ id: external_exports.string(),
25686
+ parentId: external_exports.string().nullable(),
25687
+ title: external_exports.string(),
25688
+ /** Straight off the Goal. */
25689
+ state: WorkStateSchema,
25690
+ owner: external_exports.string().nullable(),
25691
+ revision: external_exports.number().int(),
25692
+ /** Open questions on it, counted to 100. */
25693
+ waiting: external_exports.number().int(),
25694
+ /** Held by a gate on work that is not done. */
25695
+ blocked: external_exports.boolean(),
25696
+ lastProgressAt: external_exports.string().datetime().nullable(),
25697
+ /** The line written for its newest progress note, else that note's first words. */
25698
+ line: external_exports.string().nullable(),
25699
+ /** Work directly under it, counted to 100; the list carries up to 12 of them. */
25700
+ children: external_exports.number().int(),
25701
+ createdAt: external_exports.string().datetime(),
25702
+ updatedAt: external_exports.string().datetime(),
25703
+ /** When anything at or under it last moved — the order the list is in. */
25704
+ activeAt: external_exports.string().datetime(),
25705
+ /** A sealed outcome has no title here; the work's page opens it. */
25706
+ sealed: external_exports.boolean()
25707
+ });
25708
+ ComputerRowSchema = ConnectionSummarySchema.omit({ activity: true });
25709
+ AgentRowSchema = ComputerRowSchema.extend({
25710
+ /** Open questions it is asking the person, over every open card; null when that read failed. */
25711
+ asking: external_exports.number().int().nullable(),
25712
+ oldestAskAt: external_exports.string().datetime().nullable(),
25713
+ /** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
25714
+ * many in all among the account's 200 most recently active agent-held live Goals
25715
+ * (`agent_holds`); null when that read failed. */
25716
+ holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
25717
+ held: external_exports.number().int().nullable(),
25718
+ /** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
25719
+ * while none has, or when that read failed. */
25720
+ cold: external_exports.string().datetime().nullable(),
25721
+ /** The newest line of its working log, and when the harness saw it. */
25722
+ line: external_exports.string().nullable(),
25723
+ lineAt: external_exports.string().datetime().nullable()
25724
+ });
25725
+ SnapshotSchema = external_exports.object({
25726
+ /** The API's clock, taken before the first read: what a later delta will start from. */
25727
+ at: external_exports.string().datetime(),
25728
+ questions: external_exports.object({
25729
+ /** The newest 30 open cards, questions before updates. */
25730
+ items: external_exports.array(QuestionRowSchema),
25731
+ /** Every open question, and apart from them every update, and what was put off. */
25732
+ total: external_exports.number().int(),
25733
+ updates: external_exports.number().int(),
25734
+ putOff: external_exports.number().int()
25735
+ }).nullable(),
25736
+ agents: external_exports.object({
25737
+ /** Up to 60, most recently seen first. */
25738
+ items: external_exports.array(AgentRowSchema),
25739
+ more: external_exports.boolean()
25740
+ }).nullable(),
25741
+ work: external_exports.object({
25742
+ /** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
25743
+ items: external_exports.array(WorkRowSchema),
25744
+ /** How much work is behind each of the Work tab's four filters, each counted to 100, read with
25745
+ * the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
25746
+ * `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
25747
+ * is `ready`; Done is `done` or `cancelled`. */
25748
+ counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
25749
+ }).nullable(),
25750
+ you: external_exports.object({
25751
+ settings: UserSettingsSchema,
25752
+ callable: external_exports.boolean(),
25753
+ /** Up to 20 paired computers; null when the roster read failed. */
25754
+ computers: external_exports.array(ComputerRowSchema).nullable()
25755
+ }).nullable()
25756
+ });
25636
25757
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
25637
25758
  KEY_PATH = join22(homedir22(), ".paigy", "key.json");
25638
25759
  sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -30239,7 +30360,7 @@ var require_lib = __commonJS({
30239
30360
 
30240
30361
  // src/triage/judge.ts
30241
30362
  import { spawnSync as spawnSync2 } from "child_process";
30242
- import { homedir as homedir7 } from "os";
30363
+ import { homedir as homedir9 } from "os";
30243
30364
  import { delimiter as delimiter3 } from "path";
30244
30365
  function pickOllamaModel(tags) {
30245
30366
  if (tags.length === 0) return null;
@@ -30322,7 +30443,7 @@ async function judgeFor(asked2, deps = {}) {
30322
30443
  const out = spawnSync2(bin, runner.argv(null), {
30323
30444
  input: prompt,
30324
30445
  encoding: "utf8",
30325
- cwd: homedir7(),
30446
+ cwd: homedir9(),
30326
30447
  timeout: RUN_TIMEOUT_MS,
30327
30448
  maxBuffer: 32 * 1024 * 1024,
30328
30449
  env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
@@ -30364,8 +30485,8 @@ var init_judge = __esm({
30364
30485
  });
30365
30486
 
30366
30487
  // src/triage/evidence.ts
30367
- function assigneeToken(note) {
30368
- const ref = note.assignee ?? "";
30488
+ function assigneeToken(note2) {
30489
+ const ref = note2.assignee ?? "";
30369
30490
  return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
30370
30491
  }
30371
30492
  function words(text) {
@@ -30393,23 +30514,23 @@ function overlap(a, b) {
30393
30514
  for (const w of A) if (B.has(w)) shared++;
30394
30515
  return shared / (A.size + B.size - shared);
30395
30516
  }
30396
- function supersededBy(note, all) {
30397
- const mine = `${note.title} ${note.intent}`;
30398
- const at = Date.parse(note.createdAt);
30517
+ function supersededBy(note2, all) {
30518
+ const mine = `${note2.title} ${note2.intent}`;
30519
+ const at = Date.parse(note2.createdAt);
30399
30520
  let best = null;
30400
30521
  for (const other of all) {
30401
- if (other.id === note.id) continue;
30522
+ if (other.id === note2.id) continue;
30402
30523
  if (Date.parse(other.createdAt) <= at) continue;
30403
30524
  const score = overlap(mine, `${other.title} ${other.intent}`);
30404
30525
  if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
30405
30526
  }
30406
30527
  return best?.note ?? null;
30407
30528
  }
30408
- function doneEcho(note, all) {
30409
- const mine = `${note.title} ${note.intent}`;
30529
+ function doneEcho(note2, all) {
30530
+ const mine = `${note2.title} ${note2.intent}`;
30410
30531
  let best = null;
30411
30532
  for (const other of all) {
30412
- if (other.id === note.id || other.status !== "done") continue;
30533
+ if (other.id === note2.id || other.status !== "done") continue;
30413
30534
  const score = overlap(mine, `${other.title} ${other.intent}`);
30414
30535
  if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
30415
30536
  }
@@ -30463,8 +30584,8 @@ function dossier(notes, connections, now) {
30463
30584
  today: new Date(now).toISOString().slice(0, 10)
30464
30585
  };
30465
30586
  }
30466
- function affinityWhy(note, agent2) {
30467
- const shared = note.repoHints.filter((r) => agent2.repos.includes(r));
30587
+ function affinityWhy(note2, agent2) {
30588
+ const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
30468
30589
  if (shared.length && agent2.recentNotes)
30469
30590
  return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
30470
30591
  if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
@@ -30473,12 +30594,12 @@ function affinityWhy(note, agent2) {
30473
30594
  return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
30474
30595
  return null;
30475
30596
  }
30476
- function staleWhy(note) {
30477
- if (note.doneEcho) return `a later note says this shipped: "${note.doneEcho.title}"`;
30478
- if (note.supersededBy) return `superseded by "${note.supersededBy.title}"`;
30479
- if (note.rides >= 3) return `carried on ${note.rides} calls and still open`;
30480
- if (note.idleDays >= 30) return `no movement in ${note.idleDays} days`;
30481
- if (note.idleDays >= 14 && note.status === "open") return `unassigned for ${note.idleDays} days`;
30597
+ function staleWhy(note2) {
30598
+ if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
30599
+ if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
30600
+ if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
30601
+ if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
30602
+ if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
30482
30603
  return null;
30483
30604
  }
30484
30605
  var OPEN_STATUSES, DAY_MS, daysBetween, STOP, SUPERSEDE_AT, RECENT_DAYS;
@@ -30653,22 +30774,22 @@ function toProposal(verdicts, d, meta) {
30653
30774
  const stale = [];
30654
30775
  const assignments = /* @__PURE__ */ new Map();
30655
30776
  for (const v of verdicts) {
30656
- const note = byId.get(v.id);
30657
- if (!note) continue;
30658
- const item = (why) => ({ noteId: note.id, title: note.title, why });
30777
+ const note2 = byId.get(v.id);
30778
+ if (!note2) continue;
30779
+ const item = (why) => ({ noteId: note2.id, title: note2.title, why });
30659
30780
  if (v.verdict === "close") {
30660
- close.push(item(staleWhy(note) ?? v.why ?? "looks finished"));
30781
+ close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
30661
30782
  continue;
30662
30783
  }
30663
30784
  if (v.verdict === "stale") {
30664
- stale.push(item(staleWhy(note) ?? v.why ?? `no movement in ${note.idleDays} days`));
30785
+ stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
30665
30786
  continue;
30666
30787
  }
30667
30788
  if (v.verdict !== "assign" || !v.agent) continue;
30668
30789
  const agent2 = agentById.get(v.agent);
30669
30790
  if (!agent2) continue;
30670
30791
  const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
30671
- group.notes.push(item(affinityWhy(note, agent2) ?? v.why ?? "best match for this work"));
30792
+ group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
30672
30793
  assignments.set(agent2.token, group);
30673
30794
  }
30674
30795
  return {
@@ -30891,6 +31012,15 @@ function parseArgs(argv) {
30891
31012
  return { error: "--yes-close/--yes-stale/--yes-assign only apply with --apply" };
30892
31013
  return args;
30893
31014
  }
31015
+ var MCP_WIRES = [
31016
+ { cli: "claude", name: "Claude Code", wire: "claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
31017
+ // Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
31018
+ // a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
31019
+ // written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
31020
+ // all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
31021
+ { cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
31022
+ { cli: "agy", name: "Antigravity", wire: "agy mcp list | grep -q '^paigy ' || agy mcp add paigy -- npx -y @paigy/mcp@latest" }
31023
+ ];
30894
31024
  var ENABLE_TOOLS_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
30895
31025
  function onEnter(input, onLine) {
30896
31026
  if (!input.isTTY) return () => {
@@ -30910,10 +31040,10 @@ function onEnter(input, onLine) {
30910
31040
  init_dist();
30911
31041
  var import_qrcode = __toESM(require_lib(), 1);
30912
31042
  import { hostname } from "os";
30913
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
31043
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
30914
31044
  import { execFile as execFile2, execSync } from "child_process";
30915
- import { homedir as homedir8 } from "os";
30916
- import { join as join8 } from "path";
31045
+ import { homedir as homedir10 } from "os";
31046
+ import { join as join10 } from "path";
30917
31047
 
30918
31048
  // src/run.ts
30919
31049
  init_dist();
@@ -30921,29 +31051,34 @@ init_dist();
30921
31051
  // src/harness/session.ts
30922
31052
  import { spawn } from "child_process";
30923
31053
  import { existsSync as existsSync3 } from "fs";
30924
- import { homedir as homedir3 } from "os";
31054
+ import { homedir as homedir4 } from "os";
30925
31055
  import { resolve, delimiter as delimiter2 } from "path";
30926
31056
 
30927
31057
  // src/harness/acp.ts
30928
31058
  var none = { events: [], writes: [] };
31059
+ var NOTHING_TO_RESUME = /* @__PURE__ */ new Set([-32002, -32601]);
30929
31060
  function optionFor(options, decision) {
30930
31061
  const want = decision.allow ? "allow_once" : "reject_once";
30931
31062
  return options.find((o) => o.kind === want)?.optionId ?? null;
30932
31063
  }
30933
31064
  function createAcpDriver(opts) {
30934
- return new AcpDriver(opts.cwd, opts.mode, opts.resume);
31065
+ return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
30935
31066
  }
30936
31067
  var AcpDriver = class {
30937
31068
  /** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
30938
- * the session a bound identity belongs to, so the process this drives IS its holder. */
30939
- constructor(cwd, mode, resume) {
31069
+ * the session a bound identity belongs to, so the process this drives IS its holder.
31070
+ * `meta`: the adapter's own `_meta` on the frame that opens the session — where a switch only
31071
+ * one adapter understands rides (`session.ts`, a Claude session's tools). */
31072
+ constructor(cwd, mode, resume, meta) {
30940
31073
  this.cwd = cwd;
30941
31074
  this.mode = mode;
30942
31075
  this.resume = resume;
31076
+ this.meta = meta;
30943
31077
  }
30944
31078
  cwd;
30945
31079
  mode;
30946
31080
  resume;
31081
+ meta;
30947
31082
  nextId = 1;
30948
31083
  initId;
30949
31084
  sessionNewId;
@@ -30963,6 +31098,11 @@ var AcpDriver = class {
30963
31098
  /** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
30964
31099
  text = "";
30965
31100
  tools = [];
31101
+ /** Tool calls opened and not yet reported `completed` or `failed`, by `toolCallId` — whoever
31102
+ * started them. A running tool is the one long silence that is still a working turn (`quiet`). */
31103
+ running = /* @__PURE__ */ new Set();
31104
+ /** The prompt in flight was already cancelled for silence: once is the whole remedy. */
31105
+ cancelled = false;
30966
31106
  /** The opening frame. Everything after is driven by responses in `handleLine`. */
30967
31107
  open() {
30968
31108
  this.initId = this.nextId++;
@@ -30994,6 +31134,29 @@ var AcpDriver = class {
30994
31134
  optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
30995
31135
  ];
30996
31136
  }
31137
+ /**
31138
+ * THE ANSWER THAT NEVER COMES (2026-09-26, session "mute"). A turn ends when the adapter answers
31139
+ * its prompt, and nothing else ends one. claude-agent-acp can fold a prompt into a turn the agent
31140
+ * started on its own and never answer it (agentclientprotocol/claude-agent-acp#1145): the prompt
31141
+ * stayed in flight, no `idle` came, the pump never saw the agent rest, and the session was deaf
31142
+ * for twelve hours with its heartbeat alive.
31143
+ *
31144
+ * The caller's clock says nothing has crossed the pipe for `ms`; this says whether that silence
31145
+ * is still a turn. A running tool is (a ten-minute build streams nothing), and so is a permission
31146
+ * waiting on the person. Anything else, with a prompt in flight, is a turn that is over without
31147
+ * its answer, and the remedy is the protocol's own settle: `session/cancel`, which the adapter
31148
+ * answers `stopReason: "cancelled"` through the ordinary response path above — a turn that
31149
+ * ended, not one that failed. Once per prompt, and said as a crumb: a silent recovery is
31150
+ * indistinguishable from a working turn.
31151
+ */
31152
+ quiet(ms) {
31153
+ if (this.promptId === void 0 || this.cancelled || this.running.size || this.pending.size) return none;
31154
+ this.cancelled = true;
31155
+ return {
31156
+ events: [{ kind: "error", message: `turn settled: no reply after ${Math.round(ms / 1e3)}s quiet with no tool running \u2014 sent session/cancel` }],
31157
+ writes: [frame2({ method: "session/cancel", params: { sessionId: this.sessionId } })]
31158
+ };
31159
+ }
30997
31160
  /** Cancel anything still blocked — called on stop so the process can exit cleanly. */
30998
31161
  close() {
30999
31162
  const writes = [...this.pending.values()].map(
@@ -31022,20 +31185,26 @@ var AcpDriver = class {
31022
31185
  if (msg.id === this.initId) {
31023
31186
  this.initId = void 0;
31024
31187
  if (msg.error) {
31025
- return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}` }], writes: [] };
31188
+ return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
31026
31189
  }
31027
31190
  this.sessionNewId = this.nextId++;
31028
- return {
31029
- events: [],
31030
- writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : { id: this.sessionNewId, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } })]
31031
- };
31191
+ return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
31032
31192
  }
31033
31193
  if (msg.id === this.sessionNewId) {
31034
31194
  this.sessionNewId = void 0;
31195
+ const resuming = this.resume !== void 0;
31196
+ if (resuming && msg.error?.code !== void 0 && NOTHING_TO_RESUME.has(msg.error.code)) {
31197
+ this.resume = void 0;
31198
+ this.sessionNewId = this.nextId++;
31199
+ return {
31200
+ events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
31201
+ writes: [frame2(this.opening(this.sessionNewId))]
31202
+ };
31203
+ }
31035
31204
  const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
31036
31205
  if (!sessionId2) {
31037
31206
  return {
31038
- events: [{ kind: "error", message: `${this.resume ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}` }],
31207
+ events: [{ kind: "error", message: `${resuming ? "session/resume" : "session/new"} failed: ${msg.error?.message ?? "no sessionId"}`, fatal: true }],
31039
31208
  writes: []
31040
31209
  };
31041
31210
  }
@@ -31054,6 +31223,8 @@ var AcpDriver = class {
31054
31223
  }
31055
31224
  if (msg.id === this.promptId) {
31056
31225
  this.promptId = void 0;
31226
+ this.running.clear();
31227
+ this.cancelled = false;
31057
31228
  const events = [];
31058
31229
  const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
31059
31230
  if (report || this.tools.length) {
@@ -31085,6 +31256,10 @@ var AcpDriver = class {
31085
31256
  handleNotification(msg) {
31086
31257
  if (msg.method !== "session/update") return none;
31087
31258
  const update = msg.params?.update;
31259
+ if (update?.toolCallId) {
31260
+ if (update.status === "completed" || update.status === "failed") this.running.delete(update.toolCallId);
31261
+ else if (update.sessionUpdate === "tool_call") this.running.add(update.toolCallId);
31262
+ }
31088
31263
  switch (update?.sessionUpdate) {
31089
31264
  case "agent_message_chunk":
31090
31265
  this.text += update.content?.text ?? "";
@@ -31092,9 +31267,9 @@ var AcpDriver = class {
31092
31267
  case "tool_call": {
31093
31268
  const title = update.title?.trim() || update.kind || "tool";
31094
31269
  this.tools.push(title);
31095
- const note = this.text.slice(this.reportFrom).trim();
31270
+ const note2 = this.text.slice(this.reportFrom).trim();
31096
31271
  this.reportFrom = this.text.length;
31097
- return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
31272
+ return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
31098
31273
  }
31099
31274
  default:
31100
31275
  return none;
@@ -31128,6 +31303,12 @@ var AcpDriver = class {
31128
31303
  writes: []
31129
31304
  };
31130
31305
  }
31306
+ /** The frame that opens the conversation: a resume of `resume` while there is one to try,
31307
+ * else a new session. */
31308
+ opening(id) {
31309
+ const params = { cwd: this.cwd, mcpServers: [], ...this.meta ? { _meta: this.meta } : {} };
31310
+ return this.resume ? { id, method: "session/resume", params: { sessionId: this.resume, ...params } } : { id, method: "session/new", params };
31311
+ }
31131
31312
  /** Send the queued prompts as one turn, if the agent can take one right now. */
31132
31313
  flush() {
31133
31314
  if (!this.sessionId || this.promptId !== void 0 || !this.queued.length) return [];
@@ -31147,11 +31328,93 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
31147
31328
 
31148
31329
  // src/harness/session.ts
31149
31330
  init_catalog();
31331
+
31332
+ // src/paigy/inbox.ts
31333
+ import { mkdirSync as mkdirSync2, readdirSync, renameSync, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
31334
+ import { join as join4 } from "path";
31335
+ import { homedir as homedir3 } from "os";
31336
+ import { randomUUID as randomUUID4 } from "crypto";
31337
+ 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"`;
31338
+ var missing = (error) => error.code === "ENOENT";
31339
+ function openInbox(dir) {
31340
+ mkdirSync2(dir, { recursive: true });
31341
+ let written = 0;
31342
+ const receipt = (name) => join4(dir, "taken", `${name}.note`);
31343
+ const taken = (name) => {
31344
+ try {
31345
+ return statSync2(receipt(name)).mtimeMs;
31346
+ } catch (error) {
31347
+ if (missing(error)) return null;
31348
+ throw error;
31349
+ }
31350
+ };
31351
+ return {
31352
+ dir,
31353
+ put(text) {
31354
+ const name = `${Date.now()}-${String(++written).padStart(6, "0")}`;
31355
+ const tmp = join4(dir, `${name}.tmp`);
31356
+ writeFileSync2(tmp, JSON.stringify(text).slice(1, -1));
31357
+ renameSync(tmp, join4(dir, `${name}.note`));
31358
+ return name;
31359
+ },
31360
+ taken,
31361
+ settle(name) {
31362
+ try {
31363
+ renameSync(join4(dir, `${name}.note`), join4(dir, `${name}.back`));
31364
+ rmSync2(join4(dir, `${name}.back`), { force: true });
31365
+ return "retracted";
31366
+ } catch (error) {
31367
+ if (!missing(error)) throw error;
31368
+ }
31369
+ const seen = taken(name) !== null;
31370
+ rmSync2(receipt(name), { force: true });
31371
+ return seen ? "taken" : "retracted";
31372
+ },
31373
+ close() {
31374
+ rmSync2(dir, { recursive: true, force: true });
31375
+ }
31376
+ };
31377
+ }
31378
+ var INBOX_ROOT = join4(homedir3(), ".paigy", "inbox");
31379
+ var alive = (pid) => {
31380
+ try {
31381
+ process.kill(pid, 0);
31382
+ return true;
31383
+ } catch (error) {
31384
+ return error.code === "EPERM";
31385
+ }
31386
+ };
31387
+ function inboxFor(root = INBOX_ROOT) {
31388
+ try {
31389
+ for (const name of readdirSync(root)) {
31390
+ const pid = Number(name.split("-")[0]);
31391
+ if (!Number.isInteger(pid) || !alive(pid)) rmSync2(join4(root, name), { recursive: true, force: true });
31392
+ }
31393
+ } catch (error) {
31394
+ if (!missing(error)) throw error;
31395
+ }
31396
+ return openInbox(join4(root, `${process.pid}-${randomUUID4().slice(0, 8)}`));
31397
+ }
31398
+
31399
+ // src/harness/session.ts
31150
31400
  var ADAPTER_BIN = {
31151
31401
  claude: "claude-agent-acp",
31152
31402
  codex: "codex-acp",
31153
31403
  agy: "paigy-agy-acp"
31154
31404
  };
31405
+ var CLAUDE_ONE_WAY_IN = {
31406
+ env: { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" },
31407
+ disallowedTools: ["Monitor", "ScheduleWakeup", "CronCreate", "Workflow"]
31408
+ };
31409
+ var CLAUDE_META = {
31410
+ claudeCode: {
31411
+ options: {
31412
+ disallowedTools: CLAUDE_ONE_WAY_IN.disallowedTools,
31413
+ settings: { hooks: { PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: MIDTURN_HOOK }] }] } }
31414
+ }
31415
+ }
31416
+ };
31417
+ var QUIET_MS = 15 * 6e4;
31155
31418
  function splitLines(buffer, chunk) {
31156
31419
  const combined = buffer + chunk;
31157
31420
  const parts = combined.split("\n");
@@ -31159,7 +31422,7 @@ function splitLines(buffer, chunk) {
31159
31422
  return { lines: parts.filter((l) => l.trim()), rest };
31160
31423
  }
31161
31424
  function startSession(opts) {
31162
- const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir3()));
31425
+ const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
31163
31426
  if (!existsSync3(cwd)) {
31164
31427
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
31165
31428
  }
@@ -31173,25 +31436,43 @@ function startSession(opts) {
31173
31436
  ...process.env,
31174
31437
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
31175
31438
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
31176
- ...opts.resume ? { PAIGY_SESSION_ID: opts.resume } : {},
31439
+ ...opts.resume ?? opts.session ? { PAIGY_SESSION_ID: opts.resume ?? opts.session } : {},
31177
31440
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
31178
31441
  // bridge's pump typing answers in, so nothing inside it may start a second delivery path.
31179
31442
  // Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
31180
31443
  // fresh spawn has none, which is how a fresh run read as "not harness-started" to the
31181
31444
  // session-start hook and the `listen` tool (they now test either mark; listening.ts).
31182
- PAIGY_HARNESS: "1"
31445
+ PAIGY_HARNESS: "1",
31446
+ ...opts.harness === "claude" ? CLAUDE_ONE_WAY_IN.env : {},
31447
+ ...opts.harness === "claude" && opts.inbox ? { PAIGY_INBOX: opts.inbox } : {}
31183
31448
  },
31184
31449
  stdio: ["pipe", "pipe", "pipe"]
31185
31450
  });
31451
+ let ended2 = false;
31452
+ child.stdin?.on("error", () => {
31453
+ });
31186
31454
  const write = (frames) => {
31455
+ if (ended2) return;
31187
31456
  for (const f of frames) child.stdin?.write(`${f}
31188
31457
  `);
31458
+ if (frames.length) listen();
31459
+ };
31460
+ const apply = ({ events, writes }) => {
31461
+ write(writes);
31462
+ for (const event of events) opts.onEvent(event);
31463
+ };
31464
+ let hush;
31465
+ const listen = () => {
31466
+ clearTimeout(hush);
31467
+ if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
31189
31468
  };
31190
31469
  child.stderr?.on("data", (chunk) => {
31191
31470
  const text = String(chunk).trim();
31192
31471
  if (text) opts.onEvent({ kind: "error", message: text });
31193
31472
  });
31194
31473
  child.on("exit", (code) => {
31474
+ ended2 = true;
31475
+ clearTimeout(hush);
31195
31476
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
31196
31477
  opts.onExit?.();
31197
31478
  });
@@ -31199,16 +31480,18 @@ function startSession(opts) {
31199
31480
  opts.onEvent({ kind: "error", message: e.message });
31200
31481
  opts.onExit?.();
31201
31482
  });
31202
- const driver = createAcpDriver({ cwd, mode: opts.mode, ...opts.resume ? { resume: opts.resume } : {} });
31483
+ const driver = createAcpDriver({
31484
+ cwd,
31485
+ mode: opts.mode,
31486
+ ...opts.resume ? { resume: opts.resume } : {},
31487
+ ...opts.harness === "claude" ? { meta: CLAUDE_META } : {}
31488
+ });
31203
31489
  let buffer = "";
31204
31490
  child.stdout?.on("data", (chunk) => {
31491
+ listen();
31205
31492
  const { lines: lines2, rest } = splitLines(buffer, String(chunk));
31206
31493
  buffer = rest;
31207
- for (const line of lines2) {
31208
- const { events, writes } = driver.handleLine(line);
31209
- write(writes);
31210
- for (const event of events) opts.onEvent(event);
31211
- }
31494
+ for (const line of lines2) apply(driver.handleLine(line));
31212
31495
  });
31213
31496
  write(driver.open());
31214
31497
  return {
@@ -31220,6 +31503,8 @@ function startSession(opts) {
31220
31503
  },
31221
31504
  stop() {
31222
31505
  write(driver.close());
31506
+ ended2 = true;
31507
+ clearTimeout(hush);
31223
31508
  child.kill();
31224
31509
  }
31225
31510
  };
@@ -32632,8 +32917,13 @@ var UpdateGoalSchema2 = external_exports.object({
32632
32917
  }).strict();
32633
32918
  var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
32634
32919
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
32635
- var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
32636
- 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.";
32920
+ var GetGoalSchema2 = external_exports.object({
32921
+ goalId: external_exports.string().uuid(),
32922
+ /** Every entry in full. Without it the read carries the person's words, open questions and your
32923
+ * newest entry, and counts what it left out (`apps/api/src/goal/collapse.ts`). */
32924
+ history: external_exports.boolean().optional()
32925
+ }).strict();
32926
+ 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.";
32637
32927
  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.`;
32638
32928
  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.";
32639
32929
  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.";
@@ -32647,6 +32937,7 @@ var AGENT_TOOLS2 = [
32647
32937
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
32648
32938
  ];
32649
32939
  var AGENT_TOOL_NAMES2 = AGENT_TOOLS2.map((t) => t.name);
32940
+ 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.";
32650
32941
  function entryWords(entry) {
32651
32942
  const content = entry.content;
32652
32943
  if (content && "sealed" in content) return "";
@@ -33067,7 +33358,13 @@ var InboxItemSchema2 = external_exports.object({
33067
33358
  * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
33068
33359
  * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
33069
33360
  * author is not an agent this account holds (unpaired since, or a person). */
33070
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional()
33361
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
33362
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
33363
+ * its need offers, exactly as its own card carries them, present only when one of them has a
33364
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
33365
+ * never re-sent as a second card to be seen mid-call. Words-only options are absent — the bot
33366
+ * says those, and the list stays small (an `html` is up to 16 KB). */
33367
+ options: external_exports.array(OptionSchema2).optional()
33071
33368
  })).optional(),
33072
33369
  visuals: external_exports.array(VisualSchema2).optional(),
33073
33370
  /** The connected agent's name (the single pairing name — user-typed, or the
@@ -33463,7 +33760,19 @@ var QueueItemSchema2 = external_exports.object({
33463
33760
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
33464
33761
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
33465
33762
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
33466
- lastProgressAt: external_exports.string().nullable().optional()
33763
+ lastProgressAt: external_exports.string().nullable().optional(),
33764
+ /** THE GOAL'S NEWEST WORD, FROM EITHER SIDE (owner, 2026-09-27): the newest Entry on it, of any
33765
+ * kind — what the person added ("Add to this"), their reply, the agent's ask or its progress
33766
+ * note. A progress note is an Entry, so this is already the newer of the two: the person's note
33767
+ * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
33768
+ * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
33769
+ * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
33770
+ latest: external_exports.object({
33771
+ from: external_exports.enum(["person", "agent"]),
33772
+ said: external_exports.string(),
33773
+ at: external_exports.string(),
33774
+ entryId: external_exports.string()
33775
+ }).nullable().optional()
33467
33776
  });
33468
33777
  var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
33469
33778
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
@@ -33639,6 +33948,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
33639
33948
  "other"
33640
33949
  // anything else — the note carries it.
33641
33950
  ]);
33951
+ var SlimOptionSchema2 = OptionSchema2.omit({ html: true });
33952
+ var QuestionRowSchema2 = external_exports.object({
33953
+ /** The card's id (`deliveryId:needId`, or `deliveryId:entryId` for an update), as the inbox mints it. */
33954
+ id: external_exports.string(),
33955
+ deliveryId: external_exports.string(),
33956
+ entryId: external_exports.string(),
33957
+ /** The decision it waits on; null for an update, which asks nothing. */
33958
+ needId: external_exports.string().nullable(),
33959
+ goalIds: external_exports.array(external_exports.string()),
33960
+ /** The name of the work it is about, when the read could word it. */
33961
+ goalTitle: external_exports.string().optional(),
33962
+ tokenId: external_exports.string().optional(),
33963
+ name: external_exports.string(),
33964
+ title: external_exports.string(),
33965
+ body: external_exports.string(),
33966
+ select: external_exports.enum(["one", "many", "rank", "confirm", "text"]),
33967
+ options: external_exports.array(SlimOptionSchema2),
33968
+ hasPreview: external_exports.boolean(),
33969
+ blocking: external_exports.boolean(),
33970
+ askedAt: external_exports.string().datetime(),
33971
+ ring: InboxItemSchema2.shape.ring,
33972
+ onCall: external_exports.literal(true).optional(),
33973
+ sealed: external_exports.boolean()
33974
+ });
33975
+ var WorkStateSchema2 = external_exports.enum(["ready", "active", "waiting", "done", "cancelled"]);
33976
+ var WorkRowSchema2 = external_exports.object({
33977
+ id: external_exports.string(),
33978
+ parentId: external_exports.string().nullable(),
33979
+ title: external_exports.string(),
33980
+ /** Straight off the Goal. */
33981
+ state: WorkStateSchema2,
33982
+ owner: external_exports.string().nullable(),
33983
+ revision: external_exports.number().int(),
33984
+ /** Open questions on it, counted to 100. */
33985
+ waiting: external_exports.number().int(),
33986
+ /** Held by a gate on work that is not done. */
33987
+ blocked: external_exports.boolean(),
33988
+ lastProgressAt: external_exports.string().datetime().nullable(),
33989
+ /** The line written for its newest progress note, else that note's first words. */
33990
+ line: external_exports.string().nullable(),
33991
+ /** Work directly under it, counted to 100; the list carries up to 12 of them. */
33992
+ children: external_exports.number().int(),
33993
+ createdAt: external_exports.string().datetime(),
33994
+ updatedAt: external_exports.string().datetime(),
33995
+ /** When anything at or under it last moved — the order the list is in. */
33996
+ activeAt: external_exports.string().datetime(),
33997
+ /** A sealed outcome has no title here; the work's page opens it. */
33998
+ sealed: external_exports.boolean()
33999
+ });
34000
+ var ComputerRowSchema2 = ConnectionSummarySchema2.omit({ activity: true });
34001
+ var AgentRowSchema2 = ComputerRowSchema2.extend({
34002
+ /** Open questions it is asking the person, over every open card; null when that read failed. */
34003
+ asking: external_exports.number().int().nullable(),
34004
+ oldestAskAt: external_exports.string().datetime().nullable(),
34005
+ /** Up to three of the live Goals it holds, oldest first (the order it picks them up), and how
34006
+ * many in all among the account's 200 most recently active agent-held live Goals
34007
+ * (`agent_holds`); null when that read failed. */
34008
+ holds: external_exports.array(external_exports.object({ id: external_exports.string(), title: external_exports.string() })).nullable(),
34009
+ held: external_exports.number().int().nullable(),
34010
+ /** The earliest instant any Goal it holds went quiet, by the one rule (`coldSince`); null
34011
+ * while none has, or when that read failed. */
34012
+ cold: external_exports.string().datetime().nullable(),
34013
+ /** The newest line of its working log, and when the harness saw it. */
34014
+ line: external_exports.string().nullable(),
34015
+ lineAt: external_exports.string().datetime().nullable()
34016
+ });
34017
+ var SnapshotSchema2 = external_exports.object({
34018
+ /** The API's clock, taken before the first read: what a later delta will start from. */
34019
+ at: external_exports.string().datetime(),
34020
+ questions: external_exports.object({
34021
+ /** The newest 30 open cards, questions before updates. */
34022
+ items: external_exports.array(QuestionRowSchema2),
34023
+ /** Every open question, and apart from them every update, and what was put off. */
34024
+ total: external_exports.number().int(),
34025
+ updates: external_exports.number().int(),
34026
+ putOff: external_exports.number().int()
34027
+ }).nullable(),
34028
+ agents: external_exports.object({
34029
+ /** Up to 60, most recently seen first. */
34030
+ items: external_exports.array(AgentRowSchema2),
34031
+ more: external_exports.boolean()
34032
+ }).nullable(),
34033
+ work: external_exports.object({
34034
+ /** The 60 most recently active roots, each followed by up to 12 children; 240 rows at most. */
34035
+ items: external_exports.array(WorkRowSchema2),
34036
+ /** How much work is behind each of the Work tab's four filters, each counted to 100, read with
34037
+ * the rows. `work_list` (20260928023533) owns the predicates: Live is `ready`, `active` or
34038
+ * `waiting`; Waiting on you is live work with an open question or an unmet gate; Not started
34039
+ * is `ready`; Done is `done` or `cancelled`. */
34040
+ counts: external_exports.object({ live: external_exports.number().int(), waiting: external_exports.number().int(), notStarted: external_exports.number().int(), done: external_exports.number().int() })
34041
+ }).nullable(),
34042
+ you: external_exports.object({
34043
+ settings: UserSettingsSchema2,
34044
+ callable: external_exports.boolean(),
34045
+ /** Up to 20 paired computers; null when the roster read failed. */
34046
+ computers: external_exports.array(ComputerRowSchema2).nullable()
34047
+ }).nullable()
34048
+ });
33642
34049
 
33643
34050
  // src/paigy/activity.ts
33644
34051
  function shortenPaths(s) {
@@ -33648,8 +34055,8 @@ function shortenPaths(s) {
33648
34055
  });
33649
34056
  }
33650
34057
  function workLine(event) {
33651
- const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
33652
- const line = shortenPaths([event.tool.trim(), note.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
34058
+ const note2 = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
34059
+ const line = shortenPaths([event.tool.trim(), note2.trim()].filter(Boolean).join(" \u2014 ").replace(/\s+/g, " "));
33653
34060
  return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
33654
34061
  }
33655
34062
  function pushWork(lines2, line) {
@@ -33663,127 +34070,131 @@ function sameTail(a, b) {
33663
34070
  // src/paigy/bridge.ts
33664
34071
  init_dist();
33665
34072
 
33666
- // src/paigy/conversation.ts
33667
- import { randomUUID as randomUUID4 } from "crypto";
33668
- 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.";
33669
- function endsWithQuestion(text) {
33670
- if (!text) return false;
33671
- const trimmed = text.trim();
33672
- if (!trimmed) return false;
33673
- if (trimmed.endsWith("?")) return true;
33674
- const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
33675
- if (lines2.length === 0) return false;
33676
- let lastNonListIdx = lines2.length - 1;
33677
- while (lastNonListIdx >= 0) {
33678
- const line = lines2[lastNonListIdx];
33679
- if (line && /^[-*•\d.)]\s+/.test(line)) {
33680
- lastNonListIdx--;
33681
- } else {
33682
- break;
33683
- }
33684
- }
33685
- const promptLine = lastNonListIdx >= 0 ? lines2[lastNonListIdx] : lines2[lines2.length - 1];
33686
- if (promptLine?.endsWith("?")) return true;
33687
- const tail = lines2.slice(-3).join(" ").toLowerCase();
33688
- return [
33689
- /\b(let me know|which (one|would|do you)|do you want|should i|shall i)\b/,
33690
- /\b(want me to|prefer|your call|up to you|confirm whether)\b/,
33691
- /\b(how would you like|how should we|what would you like|what should we)\b/,
33692
- /\b(please choose|choose one|select one|how do you want)\b/
33693
- ].some((re) => re.test(tail));
34073
+ // src/paigy/limit.ts
34074
+ var LIMIT_PREFIXES = [
34075
+ "You've hit your",
34076
+ "You've reached your",
34077
+ "You're out of usage credits",
34078
+ "Your org is out of usage",
34079
+ "Your seat type doesn't include usage",
34080
+ "Your usage allocation has been disabled by your admin",
34081
+ "Your group's usage limit is set to $0",
34082
+ "requires usage credits",
34083
+ "You're out of extra usage",
34084
+ "Your seat type doesn't include extra usage"
34085
+ ];
34086
+ var RETRY_MS2 = 15 * 6e4;
34087
+ var MARGIN_MS = 6e4;
34088
+ var FARTHEST_MS = 8 * 24 * 60 * 6e4;
34089
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
34090
+ function limitIn(text, now) {
34091
+ 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)));
34092
+ if (!line) return null;
34093
+ const reset = resetOf(line, now);
34094
+ if (reset !== null && reset > now && reset - now <= FARTHEST_MS) {
34095
+ return { until: reset + MARGIN_MS, resets: /resets\s+(?:at\s+)?(.+?)\s*(?:\(|$)/i.exec(line)?.[1]?.trim() ?? clock3(reset) };
34096
+ }
34097
+ return { until: now + RETRY_MS2, resets: clock3(now + RETRY_MS2) };
33694
34098
  }
33695
- var POLL_MS = 5e3;
33696
- var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
33697
- var ended = (state) => state === "done" || state === "cancelled";
33698
- function unheard(brief, state) {
33699
- const seen = state.seen ??= /* @__PURE__ */ new Set();
33700
- const words2 = [];
33701
- for (const e of brief.entries ?? []) {
33702
- if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
33703
- seen.add(e.entryId);
33704
- if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
33705
- const text = entryWords(e).trim();
33706
- if (text) words2.push(text);
33707
- }
33708
- return words2;
34099
+ function resetOf(line, now) {
34100
+ 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);
34101
+ if (!m) return null;
34102
+ const zone = zoneOr(m[6]);
34103
+ let hour = Number(m[3]) % 12;
34104
+ if (m[5].toLowerCase() === "pm") hour += 12;
34105
+ const minute = Number(m[4] ?? 0);
34106
+ const today = partsAt(now, zone);
34107
+ if (m[1]) {
34108
+ const month = MONTHS.indexOf(m[1].toLowerCase());
34109
+ if (month < 0) return null;
34110
+ const at2 = wallToEpoch(today.year, month, Number(m[2]), hour, minute, zone);
34111
+ return at2 > now ? at2 : wallToEpoch(today.year + 1, month, Number(m[2]), hour, minute, zone);
34112
+ }
34113
+ const at = wallToEpoch(today.year, today.month, today.day, hour, minute, zone);
34114
+ return at > now ? at : wallToEpoch(today.year, today.month, today.day + 1, hour, minute, zone);
33709
34115
  }
33710
- async function step(session, state, rail, opts) {
33711
- if (!state.goal) {
33712
- if (!state.resting) return;
33713
- let brief2 = await rail.claim();
33714
- if (!brief2.goalId || brief2.revision === void 0) {
33715
- if (opts?.initialPrompt?.trim()) {
33716
- const prompt = opts.initialPrompt.trim();
33717
- opts.initialPrompt = void 0;
33718
- const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID4() });
33719
- brief2 = await rail.claim();
33720
- if (!brief2.goalId || brief2.revision === void 0) {
33721
- state.goal = { id: admitted.goalId, revision: admitted.revision };
33722
- if (checkCallOnDone(prompt)) state.callOnDone = true;
33723
- send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
33724
- return;
33725
- }
33726
- } else {
33727
- return;
33728
- }
33729
- }
33730
- unheard(brief2, state);
33731
- if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
33732
- else state.goal = { id: brief2.goalId, revision: brief2.revision };
33733
- const outcome = brief2.outcome?.trim() ?? "";
33734
- const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
33735
- const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
33736
- if (checkCallOnDone(fullText)) state.callOnDone = true;
33737
- send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
33738
- return;
33739
- }
33740
- const brief = await rail.read(state.goal.id);
33741
- state.goal.revision = brief.revision ?? state.goal.revision;
33742
- for (const answer of brief.answers ?? []) {
33743
- const resolve3 = state.asks?.get(answer.requestEntryId);
33744
- if (!resolve3) continue;
33745
- state.asks.delete(answer.requestEntryId);
33746
- resolve3(answer.result);
33747
- }
33748
- if (ended(brief.state)) {
33749
- state.goal = void 0;
33750
- for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
33751
- state.asks?.clear();
33752
- return;
33753
- }
33754
- if (state.asks?.size || !state.resting) return;
33755
- const words2 = unheard(brief, state);
33756
- if (words2.length) {
33757
- const text = words2.join("\n\n");
33758
- if (checkCallOnDone(text)) state.callOnDone = true;
33759
- send2(session, state, text);
34116
+ function zoneOr(zone) {
34117
+ const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
34118
+ if (!zone) return local;
34119
+ try {
34120
+ new Intl.DateTimeFormat("en-US", { timeZone: zone.trim() });
34121
+ return zone.trim();
34122
+ } catch {
34123
+ return local;
33760
34124
  }
33761
34125
  }
33762
- function send2(session, state, text) {
33763
- state.resting = false;
33764
- session.send(text);
34126
+ function partsAt(t, zone) {
34127
+ const p = Object.fromEntries(
34128
+ 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])
34129
+ );
34130
+ 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) };
33765
34131
  }
33766
- async function pump(session, state, rail, opts) {
33767
- const promptRef = opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : void 0;
33768
- for (; ; ) {
33769
- try {
33770
- await step(session, state, rail, promptRef);
33771
- } catch {
34132
+ function wallToEpoch(year, month, day, hour, minute, zone) {
34133
+ const wall = Date.UTC(year, month, day, hour, minute);
34134
+ const offset = (t) => {
34135
+ const p = partsAt(t, zone);
34136
+ return Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - Math.floor(t / 1e3) * 1e3;
34137
+ };
34138
+ const first = wall - offset(wall);
34139
+ return wall - offset(first);
34140
+ }
34141
+ function clock3(t) {
34142
+ return new Date(t).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).replace(/\s?([AP])M$/i, (_, x) => `${x.toLowerCase()}m`);
34143
+ }
34144
+ var pausedLine = (limit) => `Paused by the usage limit until ${limit.resets}`;
34145
+ var CARRY_ON = "The usage limit that stopped your last turn has reset. Carry on where you stopped.";
34146
+ 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:";
34147
+ var names = (list) => list.length <= 1 ? list[0] ?? "" : `${list.slice(0, -1).join(", ")} and ${list[list.length - 1]}`;
34148
+ var GATHER_MS = 9e4;
34149
+ function episodes(opts = {}) {
34150
+ const gather2 = opts.gatherMs ?? GATHER_MS;
34151
+ const later = opts.timer ?? ((fn, ms) => void setTimeout(fn, ms).unref?.());
34152
+ let open = null;
34153
+ return {
34154
+ paused(name, now) {
34155
+ if (!open || open.called) open = { since: now, paused: [], running: [], called: false, scheduled: false };
34156
+ if (!open.paused.includes(name)) open.paused.push(name);
34157
+ open.running = open.running.filter((n) => n !== name);
34158
+ },
34159
+ running(name, call, log) {
34160
+ const episode = open;
34161
+ if (!episode || !episode.paused.includes(name)) return;
34162
+ if (episode.called) {
34163
+ log?.(`\u25B6 ${name} is running again after the limit (the call already went out)`);
34164
+ return;
34165
+ }
34166
+ if (!episode.running.includes(name)) episode.running.push(name);
34167
+ if (episode.scheduled) return;
34168
+ episode.scheduled = true;
34169
+ later(() => {
34170
+ if (!episode.running.length) {
34171
+ episode.scheduled = false;
34172
+ return;
34173
+ }
34174
+ episode.called = true;
34175
+ const rest = episode.paused.filter((n) => !episode.running.includes(n));
34176
+ const text = [
34177
+ `Claude's usage limit paused ${names(episode.paused)} from ${clock3(episode.since)}.`,
34178
+ 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.`
34179
+ ].join(" ");
34180
+ log?.(`\u260E one call for ${episode.paused.length} paused agent${episode.paused.length === 1 ? "" : "s"}: ${names(episode.paused)}`);
34181
+ void call(text).catch((error) => log?.(`\u26A0 could not place the limit call: ${error.message}`));
34182
+ }, gather2);
33772
34183
  }
33773
- if (!await opts.onIdle()) return;
33774
- await (opts.pause ?? wait)();
33775
- }
34184
+ };
33776
34185
  }
34186
+ var LIMITS = episodes();
33777
34187
 
33778
34188
  // src/paigy/bridge.ts
33779
34189
  function railFor(opts) {
33780
34190
  return {
33781
- claim: () => claimGoal(void 0, opts),
34191
+ claim: (goalId) => claimGoal(goalId, opts),
33782
34192
  read: (goalId) => getGoal(goalId, opts),
33783
34193
  update: (goalId, input) => updateGoal(goalId, input, opts),
33784
34194
  // One read and back: the pump owns the waiting, so a Call must not hold a window here.
33785
34195
  contact: (input) => contact(input, { ...opts, waits: false }),
33786
- create: (input) => createGoal(input, opts)
34196
+ create: (input) => createGoal(input, opts),
34197
+ waiting: () => checkReplies(opts)
33787
34198
  };
33788
34199
  }
33789
34200
  function cancelAsks(state) {
@@ -33802,27 +34213,13 @@ function decisionFrom(result) {
33802
34213
  }
33803
34214
  return { allow: false, reason: "No approval given" };
33804
34215
  }
33805
- function wordsOf(result) {
33806
- if (!result || !("kind" in result)) return null;
33807
- switch (result.kind) {
33808
- case "text":
33809
- return result.text.trim() || null;
33810
- case "confirm":
33811
- return result.approved ? "yes" : "no";
33812
- case "one":
33813
- return result.option.label;
33814
- case "many":
33815
- case "rank":
33816
- return result.options.map((o) => o.label).join(", ");
33817
- }
33818
- }
33819
34216
  async function ask(state, rail, input) {
33820
34217
  if (!state.goal) return null;
33821
34218
  try {
33822
34219
  const sent = await rail.contact({
33823
- asks: [{ ask: input.ask, parentId: state.goal.id, ...input.options ? { options: input.options } : {} }],
34220
+ asks: [{ ask: input.ask, parentId: state.goal.id, options: input.options }],
33824
34221
  waiting: "hard",
33825
- channel: input.call ? "call" : "notification"
34222
+ channel: "call"
33826
34223
  });
33827
34224
  if ("recorded" in sent) return null;
33828
34225
  const entryId = sent.requestEntryIds[0];
@@ -33833,89 +34230,416 @@ async function ask(state, rail, input) {
33833
34230
  return null;
33834
34231
  }
33835
34232
  }
33836
- function checkCallOnDone(text) {
33837
- if (!text) return false;
33838
- 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);
33839
- }
33840
- function parseQuestionOptions(text) {
33841
- if (!text) return { question: "" };
33842
- const trimmed = text.trim();
33843
- const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
33844
- const optionLines = [];
33845
- const questionLines = [];
33846
- for (const line of lines2) {
33847
- const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
33848
- if (bulletMatch) {
33849
- optionLines.push(bulletMatch[1].trim());
33850
- } else {
33851
- questionLines.push(line);
33852
- }
33853
- }
33854
- if (optionLines.length >= 2 && optionLines.length <= 6) {
33855
- return {
33856
- question: questionLines.join("\n\n").trim() || trimmed,
33857
- options: optionLines.map((raw) => {
33858
- const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
33859
- const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
33860
- if (colonSplit && colonSplit[1] && colonSplit[2]) {
33861
- return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
33862
- }
33863
- if (parenSplit && parenSplit[1] && parenSplit[2]) {
33864
- return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
33865
- }
33866
- return { label: raw.slice(0, 1e3) };
33867
- })
33868
- };
33869
- }
33870
- return { question: trimmed };
33871
- }
33872
34233
  async function askPermission(event, state, rail) {
33873
34234
  return decisionFrom(await ask(state, rail, {
33874
34235
  ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
33875
34236
  options: [
33876
34237
  { label: ALLOW, hint: "Permits the agent to run this command" },
33877
34238
  { label: "Deny", hint: "Blocks execution of this command" }
33878
- ],
33879
- call: true
33880
- }));
33881
- }
33882
- async function askQuestion(question, state, rail, opts) {
33883
- return wordsOf(await ask(state, rail, {
33884
- ask: question,
33885
- call: opts?.call ?? true,
33886
- options: opts?.options
34239
+ ]
33887
34240
  }));
33888
34241
  }
33889
34242
  async function finish(result, state, rail, opts) {
33890
34243
  const goal = state.goal;
33891
34244
  if (!goal) return;
33892
- state.goal = void 0;
33893
- const report = result?.trim() || "Done.";
33894
- const channel = opts?.call || state.callOnDone ? "call" : "notification";
34245
+ const progress = result?.trim().slice(0, 1e4) || void 0;
34246
+ let revision = goal.revision;
34247
+ try {
34248
+ const held = await rail.claim(goal.id);
34249
+ revision = held.revision ?? revision;
34250
+ const owed = state.noted?.some((b) => b.goals.includes(goal.id));
34251
+ if (!held.openDecisionNeedIds?.length && !owed) {
34252
+ await rail.update(goal.id, { revision, changes: { state: "done", ...progress ? { progress } : {} }, reason: "The agent finished its turn." });
34253
+ state.goal = void 0;
34254
+ return;
34255
+ }
34256
+ } catch (error) {
34257
+ opts?.log?.(`\u26A0 the Goal did not close, still holding it: ${error.message}`);
34258
+ }
34259
+ if (!progress) return;
33895
34260
  try {
33896
- await rail.contact({ asks: [{ ask: report, parentId: goal.id }], waiting: "none", channel });
33897
- const { revision } = await rail.read(goal.id);
33898
- 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." });
34261
+ await rail.update(goal.id, { revision, changes: { progress }, reason: "The agent's turn ended; its Goal is not closed yet." });
33899
34262
  } catch (error) {
33900
- opts?.log?.(`\u26A0 finish contact failed: ${error.message}`);
34263
+ opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
34264
+ }
34265
+ }
34266
+ async function cannotWork(report, state, rail, opts) {
34267
+ state.resting = true;
34268
+ try {
34269
+ if (!state.goal) {
34270
+ const claimed = await rail.claim();
34271
+ if (!claimed.goalId || claimed.revision === void 0) return;
34272
+ state.goal = { id: claimed.goalId, revision: claimed.revision };
34273
+ }
34274
+ await rail.contact({ asks: [{ ask: report, parentId: state.goal.id }], waiting: "none", channel: "notification" });
34275
+ } catch (error) {
34276
+ opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
33901
34277
  }
33902
34278
  }
33903
34279
  async function afterTurn(event, state, rail, opts) {
33904
- if (!event.failed && !endsWithQuestion(event.result)) {
33905
- await finish(event.result, state, rail, opts);
33906
- state.resting = true;
33907
- return null;
34280
+ if (event.failed) {
34281
+ const limit = limitIn(event.result, (opts?.now ?? Date.now)());
34282
+ if (limit) {
34283
+ state.paused = limit;
34284
+ if (state.typed) state.owed = state.typed;
34285
+ state.typed = void 0;
34286
+ state.carriedOn = false;
34287
+ state.resting = true;
34288
+ opts?.log?.(`\u23F8 paused by the usage limit until ${limit.resets}: nothing is typed in until then, and it carries on by itself`);
34289
+ return limit;
34290
+ }
34291
+ await cannotWork(`The agent stopped without finishing.
34292
+
34293
+ ${event.result?.trim() || "No result reported."}`, state, rail, opts);
34294
+ return void 0;
33908
34295
  }
33909
- const rawText = event.failed ? `The agent stopped without finishing.
34296
+ await finish(event.result, state, rail, opts);
34297
+ state.typed = void 0;
34298
+ state.resting = true;
34299
+ return void 0;
34300
+ }
33910
34301
 
33911
- ${event.result?.trim() || "No result reported."}` : event.result ?? "";
33912
- const parsed = parseQuestionOptions(rawText);
33913
- const reply = await askQuestion(parsed.question, state, rail, {
33914
- call: true,
33915
- options: parsed.options
34302
+ // src/paigy/conversation.ts
34303
+ import { randomUUID as randomUUID5 } from "crypto";
34304
+ 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;
34305
+ var SAFETY_MS = 6e4;
34306
+ var ASK_BEAT_MS = 5e3;
34307
+ function beat() {
34308
+ let pending = null;
34309
+ let wakeUp = null;
34310
+ return {
34311
+ nudge(cause) {
34312
+ if (wakeUp) {
34313
+ const w = wakeUp;
34314
+ wakeUp = null;
34315
+ w(cause);
34316
+ } else pending ??= cause;
34317
+ },
34318
+ next(ms) {
34319
+ if (pending) {
34320
+ const cause = pending;
34321
+ pending = null;
34322
+ return Promise.resolve(cause);
34323
+ }
34324
+ return new Promise((resolve3) => {
34325
+ const timer = setTimeout(() => {
34326
+ wakeUp = null;
34327
+ resolve3("safety");
34328
+ }, ms);
34329
+ wakeUp = (cause) => {
34330
+ clearTimeout(timer);
34331
+ resolve3(cause);
34332
+ };
34333
+ });
34334
+ }
34335
+ };
34336
+ }
34337
+ var BEAT_REPORT_MS = 10 * 6e4;
34338
+ var ended = (state) => state === "done" || state === "cancelled";
34339
+ function unheard(brief, state, since = null, after = -1) {
34340
+ const seen = state.seen ??= /* @__PURE__ */ new Set();
34341
+ const words2 = [];
34342
+ for (const [i, e] of (brief.entries ?? []).entries()) {
34343
+ if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
34344
+ if (i <= after) {
34345
+ seen.add(e.entryId);
34346
+ continue;
34347
+ }
34348
+ if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
34349
+ seen.add(e.entryId);
34350
+ if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
34351
+ const text = entryWords(e).trim();
34352
+ if (text) words2.push(text);
34353
+ }
34354
+ return words2;
34355
+ }
34356
+ function sinceTheWork(brief) {
34357
+ const entries = brief.entries ?? [];
34358
+ let after = -1;
34359
+ for (const [i, e] of entries.entries()) if (!e.authorParticipant.startsWith("human:")) after = i;
34360
+ const last = entries[after];
34361
+ const handed = last && brief.ownerParticipant && last.authorParticipant !== brief.ownerParticipant;
34362
+ const note2 = handed ? entryWords(last).trim() : "";
34363
+ return { after, ...note2 ? { note: note2 } : {} };
34364
+ }
34365
+ var baseline = (goal, subtree) => ({
34366
+ goal,
34367
+ goals: new Map((subtree ?? []).map((g) => [g.goalId, { since: g.updatedAt ?? null, at: g.updatedAt ?? null }]))
34368
+ });
34369
+ function movedBelow(held, state) {
34370
+ const goals = state.below.goals;
34371
+ return (held.subtree ?? []).flatMap((g) => {
34372
+ const known = goals.get(g.goalId);
34373
+ const stamp = g.updatedAt ?? null;
34374
+ if (!known) return [{ goal: g, since: null }];
34375
+ if (known.at === null) {
34376
+ known.since = known.at = stamp;
34377
+ return [];
34378
+ }
34379
+ return stamp !== null && stamp !== known.at ? [{ goal: g, since: known.since }] : [];
33916
34380
  });
33917
- if (!reply) state.resting = true;
33918
- return reply;
34381
+ }
34382
+ function named(goalId, title, brief) {
34383
+ const line = (title ?? brief.outcome ?? "").trim().split("\n")[0].trim();
34384
+ const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
34385
+ return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
34386
+ }
34387
+ async function gather(state, rail, held) {
34388
+ const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
34389
+ const look2 = (goalId, via, title, since) => {
34390
+ if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
34391
+ };
34392
+ const waiting = await rail.waiting().catch(() => null);
34393
+ const listed = /* @__PURE__ */ new Set();
34394
+ const announced = state.announced ??= /* @__PURE__ */ new Set();
34395
+ if (waiting) {
34396
+ for (const id of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id)) announced.delete(id);
34397
+ }
34398
+ for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
34399
+ for (const g of waiting?.review ?? []) {
34400
+ listed.add(g.goalId);
34401
+ look2(g.goalId, "waiting work", g.title);
34402
+ }
34403
+ for (const d of waiting?.deliveries ?? []) for (const id of d.goalIds) {
34404
+ listed.add(id);
34405
+ look2(id, "waiting work");
34406
+ }
34407
+ const moved = movedBelow(held, state);
34408
+ for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
34409
+ const heard = [];
34410
+ const read = /* @__PURE__ */ new Set();
34411
+ for (const [goalId, { since, ...found }] of looks) {
34412
+ let brief = found.brief;
34413
+ if (!brief) {
34414
+ try {
34415
+ brief = await rail.read(goalId);
34416
+ } catch (error) {
34417
+ if (error.status === 404) read.add(goalId);
34418
+ continue;
34419
+ }
34420
+ }
34421
+ read.add(goalId);
34422
+ const fresh = found.via === "waiting work" || found.via === "assigned" ? sinceTheWork(brief) : { after: -1 };
34423
+ if (found.via === "assigned") announced.add(goalId);
34424
+ heard.push({ goalId, ...found, brief, words: unheard(brief, state, since ?? null, fresh.after), ...fresh.note ? { note: fresh.note } : {} });
34425
+ }
34426
+ for (const { goal, since } of moved) if (read.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
34427
+ return { heard, listed };
34428
+ }
34429
+ 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.";
34430
+ var says = (h) => h.words.length > 0 || h.via === "assigned";
34431
+ function whatItIs(h) {
34432
+ const outcome = h.brief.outcome?.trim() ?? "";
34433
+ const name = (h.title ?? outcome).trim().split("\n")[0].trim();
34434
+ return outcome && outcome !== name ? [outcome] : [];
34435
+ }
34436
+ var block = (h) => [
34437
+ ...h.via === "assigned" ? [GIVEN] : [],
34438
+ named(h.goalId, h.title, h.brief),
34439
+ ...h.via === "assigned" ? whatItIs(h) : [],
34440
+ ...h.note ? [`The last note on it, from the agent before you: ${h.note}`] : [],
34441
+ ...h.words
34442
+ ].join("\n\n");
34443
+ var short = (ids) => ids.map((id) => id.slice(0, 8)).join(", ");
34444
+ var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
34445
+ async function hear(session, state, rail, held, opts) {
34446
+ const log = opts?.log;
34447
+ const { heard, listed } = await gather(state, rail, held);
34448
+ const said = heard.filter(says);
34449
+ if (said.length) {
34450
+ send2(session, state, said.map(block).join("\n\n"));
34451
+ const worded = said.filter((h) => h.words.length);
34452
+ if (worded.length) log?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
34453
+ const goals = given(said);
34454
+ if (goals.length) {
34455
+ log?.(`\u25B8 announced assigned ${short(goals)} (via rest)`);
34456
+ if (opts?.tally) opts.tally.announced += goals.length;
34457
+ }
34458
+ }
34459
+ for (const h of heard) if (listed.has(h.goalId)) await acknowledge(h.goalId, h.brief, state, rail, log);
34460
+ }
34461
+ 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.";
34462
+ 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.";
34463
+ async function note(state, rail, held, inbox, how) {
34464
+ const { heard } = await gather(state, rail, held);
34465
+ const said = heard.filter(says);
34466
+ if (!said.length) return 0;
34467
+ const words2 = said.map(block).join("\n\n");
34468
+ const goals = said.map((h) => h.goalId);
34469
+ const assigned = given(said);
34470
+ if (how.tally) how.tally.announced += assigned.length;
34471
+ let name = "";
34472
+ try {
34473
+ name = inbox.put(`${NOTED}
34474
+
34475
+ ${words2}`);
34476
+ how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
34477
+ if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
34478
+ } catch (error) {
34479
+ how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
34480
+ }
34481
+ (state.noted ??= []).push({ name, goals, words: words2, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
34482
+ return 1;
34483
+ }
34484
+ function receipts(state, inbox, log) {
34485
+ let seen = 0;
34486
+ for (const b of state.noted ?? []) {
34487
+ if (b.seen !== void 0 || !b.name) continue;
34488
+ const at = inbox.taken(b.name);
34489
+ if (at === null) continue;
34490
+ b.seen = at;
34491
+ seen++;
34492
+ log?.(`\u2713 seen mid-turn: ${short(b.goals)} ${Math.max(0, Math.round((at - b.at) / 1e3))}s after the ${b.cause}`);
34493
+ }
34494
+ return seen;
34495
+ }
34496
+ function settle(session, state, inbox, log) {
34497
+ const batches = state.noted ?? [];
34498
+ state.noted = [];
34499
+ const seen = [];
34500
+ const unseen = [];
34501
+ for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
34502
+ send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
34503
+ log?.(`\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)`);
34504
+ }
34505
+ async function acknowledge(goalId, brief, state, rail, log) {
34506
+ const acked = state.acked ??= /* @__PURE__ */ new Map();
34507
+ if (acked.has(goalId) && acked.get(goalId) === brief.revision) return;
34508
+ acked.set(goalId, brief.revision);
34509
+ try {
34510
+ const claimed = await rail.claim(goalId);
34511
+ 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." });
34512
+ acked.set(goalId, done.revision);
34513
+ if (state.goal?.id === goalId && done.revision !== void 0) state.goal.revision = done.revision;
34514
+ } catch (error) {
34515
+ log?.(`\u26A0 could not acknowledge ${goalId.slice(0, 8)}: ${error.message} \u2014 its words are typed in; it stays listed until acknowledged`);
34516
+ }
34517
+ }
34518
+ async function step(session, state, rail, opts) {
34519
+ if (state.paused) {
34520
+ if ((opts?.now ?? Date.now)() < state.paused.until) {
34521
+ if (opts?.tally) opts.tally.paused = (opts.tally.paused ?? 0) + 1;
34522
+ return;
34523
+ }
34524
+ opts?.log?.(`\u25B6 resumed after the limit (it reset at ${state.paused.resets}): told to carry on, with what arrived meanwhile`);
34525
+ state.paused = void 0;
34526
+ state.carryOn = true;
34527
+ }
34528
+ await look(session, state, rail, opts);
34529
+ if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
34530
+ }
34531
+ async function look(session, state, rail, opts) {
34532
+ if (opts?.inbox) {
34533
+ const seen = receipts(state, opts.inbox, opts.log);
34534
+ if (opts.tally) opts.tally.seen += seen;
34535
+ }
34536
+ if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
34537
+ if (!state.goal) {
34538
+ if (!state.resting) return;
34539
+ let brief2 = await rail.claim();
34540
+ if (!brief2.goalId || brief2.revision === void 0) {
34541
+ if (opts?.initialPrompt?.trim()) {
34542
+ const prompt = opts.initialPrompt.trim();
34543
+ opts.initialPrompt = void 0;
34544
+ const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID5() });
34545
+ brief2 = await rail.claim();
34546
+ if (!brief2.goalId || brief2.revision === void 0) {
34547
+ state.goal = { id: admitted.goalId, revision: admitted.revision };
34548
+ send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
34549
+ return;
34550
+ }
34551
+ } else {
34552
+ return;
34553
+ }
34554
+ }
34555
+ unheard(brief2, state);
34556
+ if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
34557
+ else {
34558
+ state.goal = { id: brief2.goalId, revision: brief2.revision };
34559
+ state.below = baseline(brief2.goalId, brief2.subtree);
34560
+ }
34561
+ const outcome = brief2.outcome?.trim() ?? "";
34562
+ const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
34563
+ const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
34564
+ const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goal with history: true reads them in full.)` : "";
34565
+ const fullText = [outcome, earlier, ...said].filter(Boolean).join("\n\n");
34566
+ send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
34567
+ return;
34568
+ }
34569
+ const brief = await rail.read(state.goal.id);
34570
+ state.goal.revision = brief.revision ?? state.goal.revision;
34571
+ for (const answer of brief.answers ?? []) {
34572
+ const resolve3 = state.asks?.get(answer.requestEntryId);
34573
+ if (!resolve3) continue;
34574
+ state.asks.delete(answer.requestEntryId);
34575
+ resolve3(answer.result);
34576
+ }
34577
+ if (ended(brief.state)) {
34578
+ state.goal = void 0;
34579
+ for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
34580
+ state.asks?.clear();
34581
+ return;
34582
+ }
34583
+ if (state.below?.goal !== state.goal.id) state.below = baseline(state.goal.id, brief.subtree);
34584
+ if (state.asks?.size) return;
34585
+ if (!state.resting) {
34586
+ if (!opts?.inbox) return;
34587
+ 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 } : {} });
34588
+ if (opts.tally) opts.tally.noted += noted;
34589
+ return;
34590
+ }
34591
+ await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
34592
+ }
34593
+ function send2(session, state, text) {
34594
+ state.resting = false;
34595
+ let words2 = text;
34596
+ if (state.carryOn) {
34597
+ state.carryOn = false;
34598
+ state.carriedOn = true;
34599
+ const owed = state.owed;
34600
+ state.owed = void 0;
34601
+ words2 = [owed, text].filter(Boolean).join("\n\n");
34602
+ text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
34603
+ }
34604
+ state.typed = words2 || void 0;
34605
+ session.send(text);
34606
+ }
34607
+ async function pump(session, state, rail, opts) {
34608
+ const clock4 = opts.beat ?? beat();
34609
+ const now = opts.now ?? Date.now;
34610
+ const counts = { wake: 0, rest: 0, safety: 0, skipped: 0 };
34611
+ const tally = { noted: 0, seen: 0, announced: 0 };
34612
+ const stepOpts = {
34613
+ ...opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : {},
34614
+ ...opts.log ? { log: opts.log } : {},
34615
+ ...opts.inbox ? { inbox: opts.inbox } : {},
34616
+ now,
34617
+ tally
34618
+ };
34619
+ let since = now();
34620
+ let cause = "start";
34621
+ for (; ; ) {
34622
+ if (cause === "wake" && !state.resting && !state.asks?.size && !opts.inbox) counts.skipped++;
34623
+ else {
34624
+ if (cause !== "start") counts[cause]++;
34625
+ try {
34626
+ stepOpts.cause = cause;
34627
+ await step(session, state, rail, stepOpts);
34628
+ } catch {
34629
+ }
34630
+ }
34631
+ if (!await opts.onIdle()) return;
34632
+ if (now() - since >= BEAT_REPORT_MS) {
34633
+ const read = counts.wake + counts.rest + counts.safety;
34634
+ if (read || counts.skipped || tally.noted || tally.seen || tally.announced) {
34635
+ 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` : ""}`);
34636
+ }
34637
+ counts.wake = counts.rest = counts.safety = counts.skipped = tally.noted = tally.seen = tally.announced = tally.paused = 0;
34638
+ since = now();
34639
+ }
34640
+ 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);
34641
+ if (!await opts.onIdle()) return;
34642
+ }
33919
34643
  }
33920
34644
 
33921
34645
  // src/run.ts
@@ -33923,11 +34647,34 @@ function runHarness(opts) {
33923
34647
  let running = true;
33924
34648
  let tail = [];
33925
34649
  const state = { resting: true };
34650
+ const clock4 = beat();
33926
34651
  let session = null;
33927
- const rail = railFor({ token: opts.token, ...opts.resume ? { reach: reachAs(opts.resume) } : {} });
34652
+ const speaksAs = opts.resume ?? opts.session;
34653
+ const rail = railFor({ token: opts.token, ...speaksAs ? { reach: reachAs(speaksAs) } : {} });
34654
+ let inbox;
34655
+ if (opts.harness === "claude") {
34656
+ try {
34657
+ inbox = inboxFor();
34658
+ } catch (error) {
34659
+ opts.log(`\u26A0 no inbox (${error.message}): what the person says mid-turn waits for the turn to end`);
34660
+ }
34661
+ }
34662
+ const name = opts.name ?? "The agent";
34663
+ function runningAgain() {
34664
+ state.carriedOn = false;
34665
+ opts.log("\u25B6 running again after the limit");
34666
+ LIMITS.running(name, (text) => rail.contact({ asks: [{ ask: text, ...state.goal ? { parentId: state.goal.id } : {} }], waiting: "none", channel: "call" }), opts.log);
34667
+ }
33928
34668
  async function handle(event) {
33929
34669
  if (event.kind === "error") {
33930
34670
  opts.log(`\u26A0 ${event.message}`);
34671
+ if (event.fatal && running) {
34672
+ opts.log(`\u2716 could not start: ${event.message}`);
34673
+ await cannotWork(`The session could not start, so nothing is working on this yet.
34674
+
34675
+ ${event.message}`, state, rail, { log: opts.log });
34676
+ stop2();
34677
+ }
33931
34678
  return;
33932
34679
  }
33933
34680
  if (event.kind === "permission") {
@@ -33944,6 +34691,7 @@ function runHarness(opts) {
33944
34691
  return;
33945
34692
  }
33946
34693
  if (event.kind === "work") {
34694
+ if (state.carriedOn) runningAgain();
33947
34695
  opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
33948
34696
  tail = pushWork(tail, workLine(event));
33949
34697
  return;
@@ -33951,20 +34699,10 @@ function runHarness(opts) {
33951
34699
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
33952
34700
  if (event.kind === "idle") {
33953
34701
  tail = [];
33954
- if (endsWithQuestion(event.result)) {
33955
- const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
33956
- if (local?.trim() && session && running) {
33957
- opts.log(`you (here): ${local.split("\n")[0] ?? ""}`);
33958
- state.resting = false;
33959
- session.send(local);
33960
- return;
33961
- }
33962
- }
33963
- const reply = await afterTurn(event, state, rail, { log: opts.log });
33964
- if (reply && session && running) {
33965
- opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
33966
- session.send(reply);
33967
- }
34702
+ const carried = state.carriedOn && !event.failed;
34703
+ if (await afterTurn(event, state, rail, { log: opts.log })) LIMITS.paused(name, Date.now());
34704
+ else if (carried) runningAgain();
34705
+ clock4.nudge("rest");
33968
34706
  }
33969
34707
  }
33970
34708
  session = startSession({
@@ -33973,44 +34711,54 @@ function runHarness(opts) {
33973
34711
  mode: opts.mode,
33974
34712
  ...opts.token ? { token: opts.token } : {},
33975
34713
  ...opts.resume ? { resume: opts.resume } : {},
34714
+ ...opts.session ? { session: opts.session } : {},
33976
34715
  ...opts.bin ? { bin: opts.bin } : {},
33977
- ...opts.onExit ? { onExit: opts.onExit } : {},
34716
+ ...inbox ? { inbox: inbox.dir } : {},
34717
+ onExit: () => {
34718
+ inbox?.close();
34719
+ opts.onExit?.();
34720
+ },
33978
34721
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
33979
34722
  });
33980
- void pump(session, state, rail, { onIdle: () => running, initialPrompt: opts.prompt }).catch(
34723
+ void pump(session, state, rail, { onIdle: () => running, beat: clock4, initialPrompt: opts.prompt, log: opts.log, ...inbox ? { inbox } : {} }).catch(
33981
34724
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
33982
34725
  );
33983
34726
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
34727
+ function stop2() {
34728
+ running = false;
34729
+ clock4.nudge("wake");
34730
+ tail = [];
34731
+ cancelAsks(state);
34732
+ session?.stop();
34733
+ session = null;
34734
+ inbox?.close();
34735
+ opts.log("\u25A0 stopped");
34736
+ }
33984
34737
  return {
33985
34738
  send(text) {
33986
34739
  session?.send(text);
33987
34740
  },
33988
34741
  working: () => running && state.resting !== true,
33989
- tail: () => [...tail],
33990
- stop() {
33991
- running = false;
33992
- tail = [];
33993
- cancelAsks(state);
33994
- session?.stop();
33995
- session = null;
33996
- opts.log("\u25A0 stopped");
33997
- }
34742
+ // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
34743
+ tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
34744
+ nudge: () => clock4.nudge("wake"),
34745
+ stop: stop2
33998
34746
  };
33999
34747
  }
34000
34748
 
34001
34749
  // src/host.ts
34002
34750
  init_dist();
34003
34751
  init_catalog();
34004
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
34005
- import { homedir as homedir6 } from "os";
34006
- import { join as join7 } from "path";
34752
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
34753
+ import { homedir as homedir8 } from "os";
34754
+ import { join as join9 } from "path";
34007
34755
 
34008
34756
  // src/workspaces.ts
34009
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
34010
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
34011
- import { homedir as homedir4 } from "os";
34757
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
34758
+ import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
34759
+ import { homedir as homedir5 } from "os";
34012
34760
  function workspacesFile() {
34013
- return join4(homedir4(), ".paigy", "workspaces.json");
34761
+ return join5(homedir5(), ".paigy", "workspaces.json");
34014
34762
  }
34015
34763
  function listWorkspaces(deps) {
34016
34764
  try {
@@ -34021,31 +34769,94 @@ function listWorkspaces(deps) {
34021
34769
  }
34022
34770
  }
34023
34771
  function addWorkspace(dir, deps) {
34024
- const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir4()));
34772
+ const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir5()));
34025
34773
  const all = listWorkspaces(deps);
34026
34774
  if (!all.includes(next)) {
34027
34775
  all.push(next);
34028
- mkdirSync2(dirname2(deps.file), { recursive: true });
34029
- writeFileSync2(deps.file, JSON.stringify(all, null, 2));
34776
+ mkdirSync3(dirname2(deps.file), { recursive: true });
34777
+ writeFileSync3(deps.file, JSON.stringify(all, null, 2));
34030
34778
  }
34031
34779
  return all;
34032
34780
  }
34033
34781
  function allowed(cwd, deps) {
34034
- const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir4()));
34782
+ const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir5()));
34035
34783
  return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
34036
34784
  }
34037
34785
  function resolveWakeDir(pinned, deps) {
34038
34786
  if (pinned && allowed(pinned, deps)) {
34039
- const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir4()));
34787
+ const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir5()));
34040
34788
  if ((deps.exists ?? existsSync4)(dir)) return dir;
34041
34789
  }
34042
34790
  return listWorkspaces(deps)[0];
34043
34791
  }
34044
34792
 
34793
+ // src/holder.ts
34794
+ init_dist();
34795
+ import { execFileSync as execFileSync3 } from "child_process";
34796
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
34797
+ import { homedir as homedir6 } from "os";
34798
+ import { join as join6 } from "path";
34799
+ function isAlive(pid) {
34800
+ try {
34801
+ process.kill(pid, 0);
34802
+ return true;
34803
+ } catch (e) {
34804
+ return e.code === "EPERM";
34805
+ }
34806
+ }
34807
+ function psStart(pid, zone) {
34808
+ try {
34809
+ const env = zone === "UTC" ? { ...process.env, TZ: "UTC", LC_ALL: "C" } : { ...process.env, LC_ALL: "C" };
34810
+ return execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], { env, encoding: "utf8", timeout: 2e3 }).trim() || null;
34811
+ } catch {
34812
+ return null;
34813
+ }
34814
+ }
34815
+ var squash = (s) => s.trim().replace(/\s+/g, " ");
34816
+ function holds(pid, procStart, deps) {
34817
+ if (!deps.alive(pid)) return false;
34818
+ if (typeof procStart !== "string" || !procStart.trim()) return true;
34819
+ const want = squash(procStart);
34820
+ for (const zone of ["UTC", "local"]) {
34821
+ const got = deps.started(pid, zone);
34822
+ if (got === null) return true;
34823
+ if (squash(got) === want) return true;
34824
+ }
34825
+ return false;
34826
+ }
34827
+ function localHolders(deps = {}) {
34828
+ const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
34829
+ const list = deps.list ?? ((d) => readdirSync2(d));
34830
+ const read = deps.read ?? ((f) => readFileSync4(f, "utf8"));
34831
+ const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
34832
+ const held = /* @__PURE__ */ new Map();
34833
+ let files;
34834
+ try {
34835
+ files = list(dir);
34836
+ } catch {
34837
+ return held;
34838
+ }
34839
+ for (const file of files) {
34840
+ if (!/^\d+\.json$/.test(file)) continue;
34841
+ let row;
34842
+ try {
34843
+ row = JSON.parse(read(join6(dir, file)));
34844
+ } catch {
34845
+ continue;
34846
+ }
34847
+ const pid = typeof row.pid === "number" ? row.pid : Number(file.slice(0, -5));
34848
+ if (typeof row.sessionId !== "string" || !row.sessionId.trim() || !Number.isInteger(pid) || pid <= 0) continue;
34849
+ const slot = sessionSlot(row.sessionId);
34850
+ if (held.has(slot)) continue;
34851
+ if (holds(pid, row.procStart, check)) held.set(slot, { pid });
34852
+ }
34853
+ return held;
34854
+ }
34855
+
34045
34856
  // src/owner.ts
34046
- import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
34047
- import { homedir as homedir5 } from "os";
34048
- import { join as join5 } from "path";
34857
+ import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
34858
+ import { homedir as homedir7 } from "os";
34859
+ import { join as join7 } from "path";
34049
34860
  var HEAD_BYTES = 64e3;
34050
34861
  function readHead(file) {
34051
34862
  const fd = openSync2(file, "r");
@@ -34061,8 +34872,8 @@ function ownerSession(slot, deps = {}) {
34061
34872
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
34062
34873
  if (!m) return null;
34063
34874
  const prefix = m[1];
34064
- const root = deps.projectsDir ?? join5(homedir5(), ".claude", "projects");
34065
- const list = deps.list ?? ((dir) => readdirSync(dir));
34875
+ const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
34876
+ const list = deps.list ?? ((dir) => readdirSync3(dir));
34066
34877
  const head = deps.head ?? readHead;
34067
34878
  let folders;
34068
34879
  try {
@@ -34073,7 +34884,7 @@ function ownerSession(slot, deps = {}) {
34073
34884
  for (const folder of folders) {
34074
34885
  let files;
34075
34886
  try {
34076
- files = list(join5(root, folder));
34887
+ files = list(join7(root, folder));
34077
34888
  } catch {
34078
34889
  continue;
34079
34890
  }
@@ -34081,7 +34892,7 @@ function ownerSession(slot, deps = {}) {
34081
34892
  if (!file) continue;
34082
34893
  let text;
34083
34894
  try {
34084
- text = head(join5(root, folder, file));
34895
+ text = head(join7(root, folder, file));
34085
34896
  } catch {
34086
34897
  continue;
34087
34898
  }
@@ -34101,7 +34912,7 @@ function ownerSession(slot, deps = {}) {
34101
34912
  // src/update.ts
34102
34913
  import { execFile } from "child_process";
34103
34914
  import { createRequire } from "module";
34104
- import { dirname as dirname3, join as join6 } from "path";
34915
+ import { dirname as dirname3, join as join8 } from "path";
34105
34916
  import { promisify } from "util";
34106
34917
  var SERVICE_LABEL = "ai.paigy.harness";
34107
34918
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -34128,7 +34939,7 @@ async function latestVersion() {
34128
34939
  }
34129
34940
  async function install(version) {
34130
34941
  const bin = dirname3(process.execPath);
34131
- await promisify(execFile)(join6(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34942
+ await promisify(execFile)(join8(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34132
34943
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
34133
34944
  timeout: 5 * 6e4
34134
34945
  });
@@ -34179,24 +34990,43 @@ function selfUpdater(deps) {
34179
34990
  }
34180
34991
 
34181
34992
  // src/host.ts
34182
- var HOST_FILE = join7(homedir6(), ".paigy", "host.json");
34993
+ var HOST_FILE = join9(homedir8(), ".paigy", "host.json");
34183
34994
  var runKey = (slot) => `slot:${slot}`;
34184
34995
  var claimedRun = (sessionId2) => {
34185
34996
  const slot = sessionSlot(sessionId2);
34186
34997
  return { slot, key: runKey(slot) };
34187
34998
  };
34188
- async function slotHasWaitingWork(token, session) {
34189
- return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
34999
+ var launchRun = (spec) => ({ session: spec.sessionId });
35000
+ async function waitingWork(token, session) {
35001
+ let found;
35002
+ try {
35003
+ found = await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} });
35004
+ } catch (error) {
35005
+ return { skip: `could not read what is waiting (${error.message})` };
35006
+ }
35007
+ const n = found.deliveries.length;
35008
+ if (n) return { waiting: n === 1 ? "a message" : `${n} messages` };
35009
+ const next = found.claimable;
35010
+ return next ? { waiting: `Goal ${next.goalId.slice(0, 8)}${next.title ? ` "${next.title}"` : ""}` } : { skip: "has nothing waiting" };
35011
+ }
35012
+ function evict(runs, key, run) {
35013
+ if (runs.get(key)?.run === run) runs.delete(key);
34190
35014
  }
34191
35015
  var PRESENCE_FRESH_MS = 3 * 6e4;
34192
- function wakeAction(live, now, serverSeenAt) {
35016
+ function wakeAction(live, now, serverSeenAt, here = {}) {
34193
35017
  if (live) return { action: "skip", why: "run" };
34194
- if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) return { action: "skip", why: "presence" };
35018
+ if (here.holder) return { action: "skip", why: "holder" };
35019
+ if (serverSeenAt !== void 0 && now - serverSeenAt < PRESENCE_FRESH_MS) {
35020
+ if (here.goneAt !== void 0 && serverSeenAt <= here.goneAt) return { action: "spawn", why: "gone" };
35021
+ return { action: "skip", why: "presence" };
35022
+ }
34195
35023
  return { action: "spawn", why: "nobody" };
34196
35024
  }
34197
35025
  function startHost(opts) {
34198
35026
  const runs = /* @__PURE__ */ new Map();
34199
35027
  const asHost = { token: opts.token };
35028
+ const goneAt = /* @__PURE__ */ new Map();
35029
+ const heldHere = /* @__PURE__ */ new Set();
34200
35030
  const refreshIdentities = async () => {
34201
35031
  for (const slot of listSlots()) {
34202
35032
  if (slot === "Desktop") continue;
@@ -34216,7 +35046,7 @@ function startHost(opts) {
34216
35046
  working: () => [...runs.values()].filter((r) => r.run.working()).length,
34217
35047
  log: opts.log
34218
35048
  });
34219
- const beat = () => {
35049
+ const beat2 = () => {
34220
35050
  void refreshIdentities();
34221
35051
  void update().catch(() => {
34222
35052
  });
@@ -34243,11 +35073,13 @@ function startHost(opts) {
34243
35073
  mode: "bypass",
34244
35074
  prompt: "",
34245
35075
  token: spec.token,
34246
- resume: spec.sessionId,
35076
+ ...launchRun(spec),
34247
35077
  onExit: () => {
34248
- runs.delete(key);
35078
+ evict(runs, key, run);
35079
+ goneAt.set(slot, Date.now());
34249
35080
  opts.log(`\u2716 ${label} ended`);
34250
35081
  },
35082
+ name: label,
34251
35083
  log
34252
35084
  });
34253
35085
  runs.set(key, { run, label, token: spec.token });
@@ -34274,7 +35106,13 @@ function startHost(opts) {
34274
35106
  return presence;
34275
35107
  }
34276
35108
  let lastPresence = /* @__PURE__ */ new Map();
34277
- async function wakeSlot(slot, presence) {
35109
+ async function evidence() {
35110
+ const presence = await presenceMap();
35111
+ lastHolders = localHolders();
35112
+ return { presence, holders: lastHolders };
35113
+ }
35114
+ let lastHolders = /* @__PURE__ */ new Map();
35115
+ async function wakeSlot(slot, { presence, holders }, cause = "sweep") {
34278
35116
  {
34279
35117
  const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
34280
35118
  const key = runKey(slot);
@@ -34282,19 +35120,42 @@ function startHost(opts) {
34282
35120
  const live = runs.get(key);
34283
35121
  const tokenId = slotIdentity(slot).tokenId;
34284
35122
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
34285
- const decided = wakeAction(!!live, Date.now(), seenByServer);
35123
+ const holder = holders.get(slot);
35124
+ if (holder) heldHere.add(slot);
35125
+ else if (heldHere.delete(slot)) goneAt.set(slot, Date.now());
35126
+ const decided = wakeAction(!!live, Date.now(), seenByServer, { holder, goneAt: goneAt.get(slot) });
34286
35127
  if (decided.action === "skip") {
34287
- 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`);
35128
+ if (decided.why === "run" && cause === "wake") live?.run.nudge();
35129
+ const name = slotName(slot) ?? slot;
35130
+ 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`);
34288
35131
  return;
34289
35132
  }
35133
+ const label = slotName(slot) ?? slot;
35134
+ const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
34290
35135
  const token = readToken(slot);
34291
- if (!token) return;
35136
+ if (!token) {
35137
+ skip("has no token on this machine");
35138
+ return;
35139
+ }
34292
35140
  const owner = ownerSession(slot);
34293
- if (owner && !allowed(owner.cwd, opts.wsDeps)) return;
35141
+ if (owner && !allowed(owner.cwd, opts.wsDeps)) {
35142
+ skip(`ran in ${owner.cwd}, outside the folders this machine granted`);
35143
+ return;
35144
+ }
34294
35145
  const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
34295
- if (!workspace) return;
34296
- if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
34297
- const label = slotName(slot) ?? slot;
35146
+ if (!workspace) {
35147
+ skip("has no granted folder to wake in");
35148
+ return;
35149
+ }
35150
+ const found = await waitingWork(token, owner?.sessionId);
35151
+ if ("skip" in found) {
35152
+ skip(found.skip);
35153
+ return;
35154
+ }
35155
+ if (runs.has(key)) {
35156
+ skip("was started by the wake that read it first");
35157
+ return;
35158
+ }
34298
35159
  const log = (line) => opts.log(`[${label}] ${line}`);
34299
35160
  const run = runHarness({
34300
35161
  harness,
@@ -34303,17 +35164,20 @@ function startHost(opts) {
34303
35164
  prompt: "",
34304
35165
  token,
34305
35166
  ...owner ? { resume: owner.sessionId } : {},
34306
- // A dead run must not squat the slot — evict so the next wake can respawn.
35167
+ // A dead run must not squat the slot — evict so the next wake can respawn. Only ITS entry:
35168
+ // a run that is no longer the slot's must not take the live one's place away with it.
34307
35169
  onExit: () => {
34308
- runs.delete(key);
35170
+ evict(runs, key, run);
35171
+ goneAt.set(slot, Date.now());
34309
35172
  opts.log(`\u2716 ${label} ended`);
34310
35173
  },
35174
+ name: label,
34311
35175
  log
34312
35176
  });
34313
35177
  runs.set(key, { run, label, token });
34314
35178
  void heartbeat(void 0, { token }).catch(() => {
34315
35179
  });
34316
- opts.log(`\u25B6 ${owner ? "resumed" : "woke"} ${label} (${harness}) \u2014 work was waiting in ${workspace}`);
35180
+ 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)" : ""}`);
34317
35181
  }
34318
35182
  }
34319
35183
  const listening = /* @__PURE__ */ new Map();
@@ -34333,7 +35197,7 @@ function startHost(opts) {
34333
35197
  joining.add(slot);
34334
35198
  try {
34335
35199
  const sub = await subscribeWake(
34336
- () => void presenceMap().then((presence) => wakeSlot(slot, presence)).catch(() => {
35200
+ () => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
34337
35201
  }),
34338
35202
  { token }
34339
35203
  );
@@ -34347,8 +35211,8 @@ function startHost(opts) {
34347
35211
  }
34348
35212
  }
34349
35213
  async function sweepSlots() {
34350
- const presence = await presenceMap();
34351
- for (const slot of listSlots()) await wakeSlot(slot, presence);
35214
+ const seen = await evidence();
35215
+ for (const slot of listSlots()) await wakeSlot(slot, seen);
34352
35216
  }
34353
35217
  const ACTIVITY_MS = 2e3;
34354
35218
  const published = /* @__PURE__ */ new Map();
@@ -34373,12 +35237,12 @@ function startHost(opts) {
34373
35237
  };
34374
35238
  const publish = () => {
34375
35239
  try {
34376
- writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
35240
+ writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: Date.now(), roster: api.roster() }));
34377
35241
  } catch {
34378
35242
  }
34379
35243
  };
34380
- beat();
34381
- const pulse = setInterval(beat, 6e4);
35244
+ beat2();
35245
+ const pulse = setInterval(beat2, 6e4);
34382
35246
  const spawnPoll = setInterval(() => {
34383
35247
  void claimAndSpawn();
34384
35248
  publish();
@@ -34404,10 +35268,10 @@ function startHost(opts) {
34404
35268
  // them live, and a run mid-turn makes it working.
34405
35269
  roster: () => {
34406
35270
  const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
34407
- const names = /* @__PURE__ */ new Map();
34408
- for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
34409
- for (const label of live.keys()) if (!names.has(label)) names.set(label, null);
34410
- return [...names.entries()].map(([name, slot]) => {
35271
+ const names2 = /* @__PURE__ */ new Map();
35272
+ for (const key of listSlots()) if (key !== "Desktop") names2.set(slotName(key) ?? key, key);
35273
+ for (const label of live.keys()) if (!names2.has(label)) names2.set(label, null);
35274
+ return [...names2.entries()].map(([name, slot]) => {
34411
35275
  const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
34412
35276
  return {
34413
35277
  name,
@@ -34424,7 +35288,7 @@ function startHost(opts) {
34424
35288
  // sessions we started ourselves. The server already knows: `last_seen_at` on the
34425
35289
  // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
34426
35290
  // hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
34427
- running: live.has(name) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
35291
+ running: live.has(name) || !!slot && lastHolders.has(slot) || !!id.tokenId && Date.now() - (lastPresence.get(id.tokenId) ?? 0) < PRESENCE_FRESH_MS,
34428
35292
  // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
34429
35293
  // thing a tail can tell us. A process we did not spawn has no tail here to read.
34430
35294
  working: live.get(name)?.working() ?? false
@@ -34440,7 +35304,7 @@ function startHost(opts) {
34440
35304
  streamActivity();
34441
35305
  clearInterval(activityTick);
34442
35306
  try {
34443
- writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
35307
+ writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
34444
35308
  } catch {
34445
35309
  }
34446
35310
  }
@@ -34545,18 +35409,18 @@ async function pairThisMachine() {
34545
35409
  }
34546
35410
  }
34547
35411
  function forwardCodexThreadId() {
34548
- const cfg = join8(process.env.CODEX_HOME || join8(homedir8(), ".codex"), "config.toml");
35412
+ const cfg = join10(process.env.CODEX_HOME || join10(homedir10(), ".codex"), "config.toml");
34549
35413
  if (!existsSync5(cfg)) return;
34550
- const text = readFileSync5(cfg, "utf8");
35414
+ const text = readFileSync6(cfg, "utf8");
34551
35415
  const head = "[mcp_servers.paigy]\n";
34552
35416
  const at = text.indexOf(head);
34553
35417
  if (at < 0) return;
34554
- const block = text.slice(at + head.length, (() => {
35418
+ const block2 = text.slice(at + head.length, (() => {
34555
35419
  const n = text.indexOf("\n[", at + head.length);
34556
35420
  return n < 0 ? text.length : n;
34557
35421
  })());
34558
- if (/^\s*env_vars\s*=/m.test(block)) return;
34559
- writeFileSync4(cfg, text.slice(0, at + head.length) + 'env_vars = ["CODEX_THREAD_ID", "CODEX_SESSION_ID"]\n' + text.slice(at + head.length));
35422
+ if (/^\s*env_vars\s*=/m.test(block2)) return;
35423
+ writeFileSync5(cfg, text.slice(0, at + head.length) + 'env_vars = ["CODEX_THREAD_ID", "CODEX_SESSION_ID"]\n' + text.slice(at + head.length));
34560
35424
  }
34561
35425
  async function main() {
34562
35426
  const parsed = parseArgs(process.argv.slice(2));
@@ -34647,7 +35511,7 @@ async function main() {
34647
35511
  const wsDeps = { file: workspacesFile() };
34648
35512
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
34649
35513
  if (!listWorkspaces(wsDeps).length) {
34650
- const guess = join8(homedir8(), "projects");
35514
+ const guess = join10(homedir10(), "projects");
34651
35515
  if (existsSync5(guess)) {
34652
35516
  addWorkspace(guess, wsDeps);
34653
35517
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -34660,23 +35524,14 @@ async function main() {
34660
35524
  console.log("service install failed \u2014 run `paigy-harness host` to host in the foreground");
34661
35525
  }
34662
35526
  } else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
34663
- const TERMINAL_AGENTS = [
34664
- { cli: "claude", name: "Claude Code", wire: "claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
34665
- // Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
34666
- // a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
34667
- // written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
34668
- // all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
34669
- { cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
34670
- { cli: "agy", name: "Antigravity" }
34671
- ];
34672
- for (const t of TERMINAL_AGENTS) {
34673
- if (!which(t.cli) || !t.wire) continue;
35527
+ for (const t of MCP_WIRES) {
35528
+ if (!which(t.cli)) continue;
34674
35529
  try {
34675
35530
  execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
34676
35531
  if (t.cli === "codex") forwardCodexThreadId();
34677
35532
  console.log(`\u2713 paigy wired for ${t.cli} \u2014 each session hatches its own identity (restart open sessions)`);
34678
35533
  } catch (e) {
34679
- console.log(`\u26A0 could not wire ${t.cli} (${e.message.split("\n")[0]}) \u2014 the harness still works; wire it by hand later`);
35534
+ console.log(`\u26A0 could not wire ${t.cli} (${e.message.split("\n")[0]}) \u2014 its agents cannot reach you until it is: ${t.wire}`);
34680
35535
  }
34681
35536
  }
34682
35537
  if (which("claude")) {
@@ -34688,12 +35543,12 @@ async function main() {
34688
35543
  }
34689
35544
  }
34690
35545
  try {
34691
- const settings = join8(homedir8(), ".claude", "settings.json");
35546
+ const settings = join10(homedir10(), ".claude", "settings.json");
34692
35547
  if (which("claude") && existsSync5(settings)) {
34693
- const cfg = JSON.parse(readFileSync5(settings, "utf8"));
35548
+ const cfg = JSON.parse(readFileSync6(settings, "utf8"));
34694
35549
  if (!cfg["statusLine"]) {
34695
35550
  cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
34696
- writeFileSync4(settings, JSON.stringify(cfg, null, 2));
35551
+ writeFileSync5(settings, JSON.stringify(cfg, null, 2));
34697
35552
  console.log("\u2713 statusline wired");
34698
35553
  }
34699
35554
  }
@@ -34746,14 +35601,14 @@ async function main() {
34746
35601
  <key>SoftResourceLimits</key><dict>
34747
35602
  <key>NumberOfFiles</key><integer>65536</integer>
34748
35603
  </dict>
34749
- <key>StandardOutPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34750
- <key>StandardErrorPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
35604
+ <key>StandardOutPath</key><string>${join10(homedir10(), ".paigy", "host.log")}</string>
35605
+ <key>StandardErrorPath</key><string>${join10(homedir10(), ".paigy", "host.log")}</string>
34751
35606
  </dict></plist>
34752
35607
  `;
34753
- const dir = join8(homedir8(), "Library", "LaunchAgents");
34754
- mkdirSync3(dir, { recursive: true });
34755
- const path = join8(dir, `${SERVICE_LABEL}.plist`);
34756
- writeFileSync4(path, plist);
35608
+ const dir = join10(homedir10(), "Library", "LaunchAgents");
35609
+ mkdirSync4(dir, { recursive: true });
35610
+ const path = join10(dir, `${SERVICE_LABEL}.plist`);
35611
+ writeFileSync5(path, plist);
34757
35612
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
34758
35613
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
34759
35614
  console.log(" file limit raised to 65536 for the host and every session it spawns");
@@ -34775,7 +35630,7 @@ async function main() {
34775
35630
  process.exit(1);
34776
35631
  }
34777
35632
  const name = slotName(slot) ?? slot;
34778
- console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir8(), "~")}`);
35633
+ console.log(`\u2713 ${name} is handed off \u2014 wakes in ${cwd.replace(homedir10(), "~")}`);
34779
35634
  if (!allowed(cwd, { file: workspacesFile() })) {
34780
35635
  console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
34781
35636
  console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
@@ -34799,7 +35654,11 @@ async function main() {
34799
35654
  if (parsed.hatch) {
34800
35655
  overrideToken(readToken("Desktop") || null);
34801
35656
  const minted = await hatch(parsed.hatch, parsed.voice ?? null);
34802
- saveToken({ access_token: minted.token, name: minted.name, device: null }, parsed.slot ?? minted.name);
35657
+ saveToken({ access_token: minted.token, name: minted.name ?? parsed.hatch, device: null }, parsed.slot ?? minted.name ?? parsed.hatch);
35658
+ if (!minted.name) {
35659
+ console.log(`\u2713 hatched, unnamed \u2014 you already have an agent called "${parsed.hatch}". Pick another name in the app's Agents screen.`);
35660
+ return;
35661
+ }
34803
35662
  console.log(`\u2713 hatched "${minted.name}" \u2014 it's on your phone's Agents screen now.`);
34804
35663
  console.log(` speak as it: PAIGY_AGENT="${minted.name}" <any paigy tool>`);
34805
35664
  console.log(` run with it: paigy-harness --identity "${minted.name}" --harness codex "\u2026"`);
@@ -34863,12 +35722,11 @@ async function main() {
34863
35722
  const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);
34864
35723
  if (!said) return null;
34865
35724
  return /^(y|yes|ok|allow|approve)/i.test(said) ? { allow: true } : { allow: false, reason: said };
34866
- },
34867
- question: (q) => askTerminal(`\u2753 ${q}`)
35725
+ }
34868
35726
  },
34869
35727
  log: (line) => console.log(line)
34870
35728
  });
34871
- const beat = async () => {
35729
+ const beat2 = async () => {
34872
35730
  try {
34873
35731
  await heartbeat();
34874
35732
  signedOut = false;
@@ -34881,8 +35739,8 @@ async function main() {
34881
35739
  console.log(" paigy-harness pair");
34882
35740
  }
34883
35741
  };
34884
- const pulse = setInterval(() => void beat(), 6e4);
34885
- void beat();
35742
+ const pulse = setInterval(() => void beat2(), 6e4);
35743
+ void beat2();
34886
35744
  pulse.unref?.();
34887
35745
  process.on("SIGINT", () => {
34888
35746
  run.stop();