@paigy/harness 0.3.17 → 0.3.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +15 -14
  2. package/dist/cli.js +1162 -365
  3. package/dist/main.js +1118 -323
  4. package/package.json +1 -1
package/dist/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.";
@@ -25461,7 +25466,19 @@ var init_dist = __esm({
25461
25466
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
25462
25467
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
25463
25468
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
25464
- 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()
25465
25482
  });
25466
25483
  COLD_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
25467
25484
  NoteSourceSchema = external_exports.enum(["app", "call"]);
@@ -25639,6 +25656,104 @@ var init_dist = __esm({
25639
25656
  "other"
25640
25657
  // anything else — the note carries it.
25641
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
+ });
25642
25757
  TOKEN_PATH = join22(homedir22(), ".paigy", "token.json");
25643
25758
  KEY_PATH = join22(homedir22(), ".paigy", "key.json");
25644
25759
  sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -30245,7 +30360,7 @@ var require_lib = __commonJS({
30245
30360
 
30246
30361
  // src/triage/judge.ts
30247
30362
  import { spawnSync as spawnSync2 } from "child_process";
30248
- import { homedir as homedir7 } from "os";
30363
+ import { homedir as homedir9 } from "os";
30249
30364
  import { delimiter as delimiter3 } from "path";
30250
30365
  function pickOllamaModel(tags) {
30251
30366
  if (tags.length === 0) return null;
@@ -30328,7 +30443,7 @@ async function judgeFor(asked2, deps = {}) {
30328
30443
  const out = spawnSync2(bin, runner.argv(null), {
30329
30444
  input: prompt,
30330
30445
  encoding: "utf8",
30331
- cwd: homedir7(),
30446
+ cwd: homedir9(),
30332
30447
  timeout: RUN_TIMEOUT_MS,
30333
30448
  maxBuffer: 32 * 1024 * 1024,
30334
30449
  env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter3) }
@@ -30370,8 +30485,8 @@ var init_judge = __esm({
30370
30485
  });
30371
30486
 
30372
30487
  // src/triage/evidence.ts
30373
- function assigneeToken(note) {
30374
- const ref = note.assignee ?? "";
30488
+ function assigneeToken(note2) {
30489
+ const ref = note2.assignee ?? "";
30375
30490
  return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
30376
30491
  }
30377
30492
  function words(text) {
@@ -30399,23 +30514,23 @@ function overlap(a, b) {
30399
30514
  for (const w of A) if (B.has(w)) shared++;
30400
30515
  return shared / (A.size + B.size - shared);
30401
30516
  }
30402
- function supersededBy(note, all) {
30403
- const mine = `${note.title} ${note.intent}`;
30404
- 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);
30405
30520
  let best = null;
30406
30521
  for (const other of all) {
30407
- if (other.id === note.id) continue;
30522
+ if (other.id === note2.id) continue;
30408
30523
  if (Date.parse(other.createdAt) <= at) continue;
30409
30524
  const score = overlap(mine, `${other.title} ${other.intent}`);
30410
30525
  if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
30411
30526
  }
30412
30527
  return best?.note ?? null;
30413
30528
  }
30414
- function doneEcho(note, all) {
30415
- const mine = `${note.title} ${note.intent}`;
30529
+ function doneEcho(note2, all) {
30530
+ const mine = `${note2.title} ${note2.intent}`;
30416
30531
  let best = null;
30417
30532
  for (const other of all) {
30418
- if (other.id === note.id || other.status !== "done") continue;
30533
+ if (other.id === note2.id || other.status !== "done") continue;
30419
30534
  const score = overlap(mine, `${other.title} ${other.intent}`);
30420
30535
  if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
30421
30536
  }
@@ -30469,8 +30584,8 @@ function dossier(notes, connections, now) {
30469
30584
  today: new Date(now).toISOString().slice(0, 10)
30470
30585
  };
30471
30586
  }
30472
- function affinityWhy(note, agent2) {
30473
- 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));
30474
30589
  if (shared.length && agent2.recentNotes)
30475
30590
  return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
30476
30591
  if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
@@ -30479,12 +30594,12 @@ function affinityWhy(note, agent2) {
30479
30594
  return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
30480
30595
  return null;
30481
30596
  }
30482
- function staleWhy(note) {
30483
- if (note.doneEcho) return `a later note says this shipped: "${note.doneEcho.title}"`;
30484
- if (note.supersededBy) return `superseded by "${note.supersededBy.title}"`;
30485
- if (note.rides >= 3) return `carried on ${note.rides} calls and still open`;
30486
- if (note.idleDays >= 30) return `no movement in ${note.idleDays} days`;
30487
- 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`;
30488
30603
  return null;
30489
30604
  }
30490
30605
  var OPEN_STATUSES, DAY_MS, daysBetween, STOP, SUPERSEDE_AT, RECENT_DAYS;
@@ -30659,22 +30774,22 @@ function toProposal(verdicts, d, meta) {
30659
30774
  const stale = [];
30660
30775
  const assignments = /* @__PURE__ */ new Map();
30661
30776
  for (const v of verdicts) {
30662
- const note = byId.get(v.id);
30663
- if (!note) continue;
30664
- 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 });
30665
30780
  if (v.verdict === "close") {
30666
- close.push(item(staleWhy(note) ?? v.why ?? "looks finished"));
30781
+ close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
30667
30782
  continue;
30668
30783
  }
30669
30784
  if (v.verdict === "stale") {
30670
- 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`));
30671
30786
  continue;
30672
30787
  }
30673
30788
  if (v.verdict !== "assign" || !v.agent) continue;
30674
30789
  const agent2 = agentById.get(v.agent);
30675
30790
  if (!agent2) continue;
30676
30791
  const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
30677
- 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"));
30678
30793
  assignments.set(agent2.token, group);
30679
30794
  }
30680
30795
  return {
@@ -30897,6 +31012,15 @@ function parseArgs(argv) {
30897
31012
  return { error: "--yes-close/--yes-stale/--yes-assign only apply with --apply" };
30898
31013
  return args;
30899
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
+ ];
30900
31024
  var ENABLE_TOOLS_COMMAND = "npx -y -p @paigy/mcp@latest paigy-enable-tools";
30901
31025
  function onEnter(input, onLine) {
30902
31026
  if (!input.isTTY) return () => {
@@ -30916,10 +31040,10 @@ function onEnter(input, onLine) {
30916
31040
  init_dist();
30917
31041
  var import_qrcode = __toESM(require_lib(), 1);
30918
31042
  import { hostname } from "os";
30919
- 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";
30920
31044
  import { execFile as execFile2, execSync } from "child_process";
30921
- import { homedir as homedir8 } from "os";
30922
- import { join as join8 } from "path";
31045
+ import { homedir as homedir10 } from "os";
31046
+ import { join as join10 } from "path";
30923
31047
 
30924
31048
  // src/run.ts
30925
31049
  init_dist();
@@ -30927,7 +31051,7 @@ init_dist();
30927
31051
  // src/harness/session.ts
30928
31052
  import { spawn } from "child_process";
30929
31053
  import { existsSync as existsSync3 } from "fs";
30930
- import { homedir as homedir3 } from "os";
31054
+ import { homedir as homedir4 } from "os";
30931
31055
  import { resolve, delimiter as delimiter2 } from "path";
30932
31056
 
30933
31057
  // src/harness/acp.ts
@@ -30938,19 +31062,23 @@ function optionFor(options, decision) {
30938
31062
  return options.find((o) => o.kind === want)?.optionId ?? null;
30939
31063
  }
30940
31064
  function createAcpDriver(opts) {
30941
- return new AcpDriver(opts.cwd, opts.mode, opts.resume);
31065
+ return new AcpDriver(opts.cwd, opts.mode, opts.resume, opts.meta);
30942
31066
  }
30943
31067
  var AcpDriver = class {
30944
31068
  /** `resume`: the id of an existing session to continue rather than open a new one (#1637) —
30945
- * the session a bound identity belongs to, so the process this drives IS its holder. */
30946
- 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) {
30947
31073
  this.cwd = cwd;
30948
31074
  this.mode = mode;
30949
31075
  this.resume = resume;
31076
+ this.meta = meta;
30950
31077
  }
30951
31078
  cwd;
30952
31079
  mode;
30953
31080
  resume;
31081
+ meta;
30954
31082
  nextId = 1;
30955
31083
  initId;
30956
31084
  sessionNewId;
@@ -30970,6 +31098,11 @@ var AcpDriver = class {
30970
31098
  /** The turn being streamed: text accumulates, tools append, both flush on stopReason. */
30971
31099
  text = "";
30972
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;
30973
31106
  /** The opening frame. Everything after is driven by responses in `handleLine`. */
30974
31107
  open() {
30975
31108
  this.initId = this.nextId++;
@@ -31001,6 +31134,29 @@ var AcpDriver = class {
31001
31134
  optionId ? frame2({ id: request.id, result: { outcome: { outcome: "selected", optionId } } }) : frame2({ id: request.id, result: { outcome: { outcome: "cancelled" } } })
31002
31135
  ];
31003
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
+ }
31004
31160
  /** Cancel anything still blocked — called on stop so the process can exit cleanly. */
31005
31161
  close() {
31006
31162
  const writes = [...this.pending.values()].map(
@@ -31032,10 +31188,7 @@ var AcpDriver = class {
31032
31188
  return { events: [{ kind: "error", message: `initialize failed: ${msg.error.message}`, fatal: true }], writes: [] };
31033
31189
  }
31034
31190
  this.sessionNewId = this.nextId++;
31035
- return {
31036
- events: [],
31037
- writes: [frame2(this.resume ? { id: this.sessionNewId, method: "session/resume", params: { sessionId: this.resume, cwd: this.cwd, mcpServers: [] } } : this.sessionNew(this.sessionNewId))]
31038
- };
31191
+ return { events: [], writes: [frame2(this.opening(this.sessionNewId))] };
31039
31192
  }
31040
31193
  if (msg.id === this.sessionNewId) {
31041
31194
  this.sessionNewId = void 0;
@@ -31045,7 +31198,7 @@ var AcpDriver = class {
31045
31198
  this.sessionNewId = this.nextId++;
31046
31199
  return {
31047
31200
  events: [{ kind: "error", message: `session/resume found nothing to resume (${msg.error.message}) \u2014 opening a new conversation` }],
31048
- writes: [frame2(this.sessionNew(this.sessionNewId))]
31201
+ writes: [frame2(this.opening(this.sessionNewId))]
31049
31202
  };
31050
31203
  }
31051
31204
  const sessionId2 = msg.error ? void 0 : msg.result?.sessionId ?? this.resume;
@@ -31070,6 +31223,8 @@ var AcpDriver = class {
31070
31223
  }
31071
31224
  if (msg.id === this.promptId) {
31072
31225
  this.promptId = void 0;
31226
+ this.running.clear();
31227
+ this.cancelled = false;
31073
31228
  const events = [];
31074
31229
  const report = this.text.slice(this.reportFrom).trim() || this.text.trim();
31075
31230
  if (report || this.tools.length) {
@@ -31101,6 +31256,10 @@ var AcpDriver = class {
31101
31256
  handleNotification(msg) {
31102
31257
  if (msg.method !== "session/update") return none;
31103
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
+ }
31104
31263
  switch (update?.sessionUpdate) {
31105
31264
  case "agent_message_chunk":
31106
31265
  this.text += update.content?.text ?? "";
@@ -31108,9 +31267,9 @@ var AcpDriver = class {
31108
31267
  case "tool_call": {
31109
31268
  const title = update.title?.trim() || update.kind || "tool";
31110
31269
  this.tools.push(title);
31111
- const note = this.text.slice(this.reportFrom).trim();
31270
+ const note2 = this.text.slice(this.reportFrom).trim();
31112
31271
  this.reportFrom = this.text.length;
31113
- return { events: [{ kind: "work", tool: title, ...note ? { note } : {} }], writes: [] };
31272
+ return { events: [{ kind: "work", tool: title, ...note2 ? { note: note2 } : {} }], writes: [] };
31114
31273
  }
31115
31274
  default:
31116
31275
  return none;
@@ -31144,8 +31303,11 @@ var AcpDriver = class {
31144
31303
  writes: []
31145
31304
  };
31146
31305
  }
31147
- sessionNew(id) {
31148
- return { id, method: "session/new", params: { cwd: this.cwd, mcpServers: [] } };
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 };
31149
31311
  }
31150
31312
  /** Send the queued prompts as one turn, if the agent can take one right now. */
31151
31313
  flush() {
@@ -31166,11 +31328,93 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
31166
31328
 
31167
31329
  // src/harness/session.ts
31168
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
31169
31400
  var ADAPTER_BIN = {
31170
31401
  claude: "claude-agent-acp",
31171
31402
  codex: "codex-acp",
31172
31403
  agy: "paigy-agy-acp"
31173
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;
31174
31418
  function splitLines(buffer, chunk) {
31175
31419
  const combined = buffer + chunk;
31176
31420
  const parts = combined.split("\n");
@@ -31178,7 +31422,7 @@ function splitLines(buffer, chunk) {
31178
31422
  return { lines: parts.filter((l) => l.trim()), rest };
31179
31423
  }
31180
31424
  function startSession(opts) {
31181
- const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir3()));
31425
+ const cwd = resolve(opts.cwd.replace(/^~(?=$|\/)/, homedir4()));
31182
31426
  if (!existsSync3(cwd)) {
31183
31427
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
31184
31428
  }
@@ -31198,7 +31442,9 @@ function startSession(opts) {
31198
31442
  // Exported on EVERY spawn — `PAIGY_SESSION_ID` above is the conversation to resume, and a
31199
31443
  // fresh spawn has none, which is how a fresh run read as "not harness-started" to the
31200
31444
  // session-start hook and the `listen` tool (they now test either mark; listening.ts).
31201
- 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 } : {}
31202
31448
  },
31203
31449
  stdio: ["pipe", "pipe", "pipe"]
31204
31450
  });
@@ -31209,6 +31455,16 @@ function startSession(opts) {
31209
31455
  if (ended2) return;
31210
31456
  for (const f of frames) child.stdin?.write(`${f}
31211
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);
31212
31468
  };
31213
31469
  child.stderr?.on("data", (chunk) => {
31214
31470
  const text = String(chunk).trim();
@@ -31216,6 +31472,7 @@ function startSession(opts) {
31216
31472
  });
31217
31473
  child.on("exit", (code) => {
31218
31474
  ended2 = true;
31475
+ clearTimeout(hush);
31219
31476
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
31220
31477
  opts.onExit?.();
31221
31478
  });
@@ -31223,16 +31480,18 @@ function startSession(opts) {
31223
31480
  opts.onEvent({ kind: "error", message: e.message });
31224
31481
  opts.onExit?.();
31225
31482
  });
31226
- 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
+ });
31227
31489
  let buffer = "";
31228
31490
  child.stdout?.on("data", (chunk) => {
31491
+ listen();
31229
31492
  const { lines: lines2, rest } = splitLines(buffer, String(chunk));
31230
31493
  buffer = rest;
31231
- for (const line of lines2) {
31232
- const { events, writes } = driver.handleLine(line);
31233
- write(writes);
31234
- for (const event of events) opts.onEvent(event);
31235
- }
31494
+ for (const line of lines2) apply(driver.handleLine(line));
31236
31495
  });
31237
31496
  write(driver.open());
31238
31497
  return {
@@ -31245,6 +31504,7 @@ function startSession(opts) {
31245
31504
  stop() {
31246
31505
  write(driver.close());
31247
31506
  ended2 = true;
31507
+ clearTimeout(hush);
31248
31508
  child.kill();
31249
31509
  }
31250
31510
  };
@@ -32657,8 +32917,13 @@ var UpdateGoalSchema2 = external_exports.object({
32657
32917
  }).strict();
32658
32918
  var UpdateGoalToolSchema2 = UpdateGoalSchema2.omit({ operationId: true }).extend({ goalId: external_exports.string().uuid() }).strict();
32659
32919
  var ClaimGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid().optional() }).strict();
32660
- var GetGoalSchema2 = external_exports.object({ goalId: external_exports.string().uuid() }).strict();
32661
- 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.";
32662
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.`;
32663
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.";
32664
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.";
@@ -32672,6 +32937,7 @@ var AGENT_TOOLS2 = [
32672
32937
  { name: "update_goal", description: UPDATE_GOAL_DESCRIPTION2, inputSchema: mcpInputSchema2(UpdateGoalToolSchema2) }
32673
32938
  ];
32674
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.";
32675
32941
  function entryWords(entry) {
32676
32942
  const content = entry.content;
32677
32943
  if (content && "sealed" in content) return "";
@@ -33494,7 +33760,19 @@ var QueueItemSchema2 = external_exports.object({
33494
33760
  /** When its owner last SAID something about it (`goals.last_progress_at`, written by every
33495
33761
  * `update_goal` that changes `progress`). `updatedAt` moves for reasons nobody chose — a
33496
33762
  * state recomputed, a review flag — so it cannot tell work in hand from work gone quiet. */
33497
- 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()
33498
33776
  });
33499
33777
  var COLD_AFTER_MS2 = 3 * 24 * 60 * 60 * 1e3;
33500
33778
  var NoteSourceSchema2 = external_exports.enum(["app", "call"]);
@@ -33670,6 +33948,104 @@ var NotificationFeedbackKindSchema2 = external_exports.enum([
33670
33948
  "other"
33671
33949
  // anything else — the note carries it.
33672
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
+ });
33673
34049
 
33674
34050
  // src/paigy/activity.ts
33675
34051
  function shortenPaths(s) {
@@ -33679,8 +34055,8 @@ function shortenPaths(s) {
33679
34055
  });
33680
34056
  }
33681
34057
  function workLine(event) {
33682
- const note = (event.note ?? "").split("\n").find((l) => l.trim()) ?? "";
33683
- 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, " "));
33684
34060
  return line.length > ACTIVITY_LINE_MAX2 ? `${line.slice(0, ACTIVITY_LINE_MAX2 - 1)}\u2026` : line;
33685
34061
  }
33686
34062
  function pushWork(lines2, line) {
@@ -33694,127 +34070,131 @@ function sameTail(a, b) {
33694
34070
  // src/paigy/bridge.ts
33695
34071
  init_dist();
33696
34072
 
33697
- // src/paigy/conversation.ts
33698
- import { randomUUID as randomUUID4 } from "crypto";
33699
- 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.";
33700
- function endsWithQuestion(text) {
33701
- if (!text) return false;
33702
- const trimmed = text.trim();
33703
- if (!trimmed) return false;
33704
- if (trimmed.endsWith("?")) return true;
33705
- const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
33706
- if (lines2.length === 0) return false;
33707
- let lastNonListIdx = lines2.length - 1;
33708
- while (lastNonListIdx >= 0) {
33709
- const line = lines2[lastNonListIdx];
33710
- if (line && /^[-*•\d.)]\s+/.test(line)) {
33711
- lastNonListIdx--;
33712
- } else {
33713
- break;
33714
- }
33715
- }
33716
- const promptLine = lastNonListIdx >= 0 ? lines2[lastNonListIdx] : lines2[lines2.length - 1];
33717
- if (promptLine?.endsWith("?")) return true;
33718
- const tail = lines2.slice(-3).join(" ").toLowerCase();
33719
- return [
33720
- /\b(let me know|which (one|would|do you)|do you want|should i|shall i)\b/,
33721
- /\b(want me to|prefer|your call|up to you|confirm whether)\b/,
33722
- /\b(how would you like|how should we|what would you like|what should we)\b/,
33723
- /\b(please choose|choose one|select one|how do you want)\b/
33724
- ].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) };
33725
34098
  }
33726
- var POLL_MS = 5e3;
33727
- var wait = () => new Promise((r) => setTimeout(r, POLL_MS));
33728
- var ended = (state) => state === "done" || state === "cancelled";
33729
- function unheard(brief, state) {
33730
- const seen = state.seen ??= /* @__PURE__ */ new Set();
33731
- const words2 = [];
33732
- for (const e of brief.entries ?? []) {
33733
- if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
33734
- seen.add(e.entryId);
33735
- if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
33736
- const text = entryWords(e).trim();
33737
- if (text) words2.push(text);
33738
- }
33739
- 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);
33740
34115
  }
33741
- async function step(session, state, rail, opts) {
33742
- if (!state.goal) {
33743
- if (!state.resting) return;
33744
- let brief2 = await rail.claim();
33745
- if (!brief2.goalId || brief2.revision === void 0) {
33746
- if (opts?.initialPrompt?.trim()) {
33747
- const prompt = opts.initialPrompt.trim();
33748
- opts.initialPrompt = void 0;
33749
- const admitted = await rail.create({ outcome: prompt, idempotencyKey: randomUUID4() });
33750
- brief2 = await rail.claim();
33751
- if (!brief2.goalId || brief2.revision === void 0) {
33752
- state.goal = { id: admitted.goalId, revision: admitted.revision };
33753
- if (checkCallOnDone(prompt)) state.callOnDone = true;
33754
- send2(session, state, [HARNESS_INSTRUCTION, prompt].join("\n\n"));
33755
- return;
33756
- }
33757
- } else {
33758
- return;
33759
- }
33760
- }
33761
- unheard(brief2, state);
33762
- if (ended(brief2.state)) await rail.update(brief2.goalId, { revision: brief2.revision, changes: { reviewed: true }, reason: "Typed into the session." });
33763
- else state.goal = { id: brief2.goalId, revision: brief2.revision };
33764
- const outcome = brief2.outcome?.trim() ?? "";
33765
- const said = (brief2.entries ?? []).map((e) => entryWords(e).trim()).filter((w) => w && w.toLowerCase() !== outcome.toLowerCase());
33766
- const fullText = [outcome, ...said].filter(Boolean).join("\n\n");
33767
- if (checkCallOnDone(fullText)) state.callOnDone = true;
33768
- send2(session, state, [HARNESS_INSTRUCTION, fullText].filter(Boolean).join("\n\n"));
33769
- return;
33770
- }
33771
- const brief = await rail.read(state.goal.id);
33772
- state.goal.revision = brief.revision ?? state.goal.revision;
33773
- for (const answer of brief.answers ?? []) {
33774
- const resolve3 = state.asks?.get(answer.requestEntryId);
33775
- if (!resolve3) continue;
33776
- state.asks.delete(answer.requestEntryId);
33777
- resolve3(answer.result);
33778
- }
33779
- if (ended(brief.state)) {
33780
- state.goal = void 0;
33781
- for (const resolve3 of state.asks?.values() ?? []) resolve3(null);
33782
- state.asks?.clear();
33783
- return;
33784
- }
33785
- if (state.asks?.size || !state.resting) return;
33786
- const words2 = unheard(brief, state);
33787
- if (words2.length) {
33788
- const text = words2.join("\n\n");
33789
- if (checkCallOnDone(text)) state.callOnDone = true;
33790
- 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;
33791
34124
  }
33792
34125
  }
33793
- function send2(session, state, text) {
33794
- state.resting = false;
33795
- 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) };
33796
34131
  }
33797
- async function pump(session, state, rail, opts) {
33798
- const promptRef = opts.initialPrompt ? { initialPrompt: opts.initialPrompt } : void 0;
33799
- for (; ; ) {
33800
- try {
33801
- await step(session, state, rail, promptRef);
33802
- } 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);
33803
34183
  }
33804
- if (!await opts.onIdle()) return;
33805
- await (opts.pause ?? wait)();
33806
- }
34184
+ };
33807
34185
  }
34186
+ var LIMITS = episodes();
33808
34187
 
33809
34188
  // src/paigy/bridge.ts
33810
34189
  function railFor(opts) {
33811
34190
  return {
33812
- claim: () => claimGoal(void 0, opts),
34191
+ claim: (goalId) => claimGoal(goalId, opts),
33813
34192
  read: (goalId) => getGoal(goalId, opts),
33814
34193
  update: (goalId, input) => updateGoal(goalId, input, opts),
33815
34194
  // One read and back: the pump owns the waiting, so a Call must not hold a window here.
33816
34195
  contact: (input) => contact(input, { ...opts, waits: false }),
33817
- create: (input) => createGoal(input, opts)
34196
+ create: (input) => createGoal(input, opts),
34197
+ waiting: () => checkReplies(opts)
33818
34198
  };
33819
34199
  }
33820
34200
  function cancelAsks(state) {
@@ -33833,27 +34213,13 @@ function decisionFrom(result) {
33833
34213
  }
33834
34214
  return { allow: false, reason: "No approval given" };
33835
34215
  }
33836
- function wordsOf(result) {
33837
- if (!result || !("kind" in result)) return null;
33838
- switch (result.kind) {
33839
- case "text":
33840
- return result.text.trim() || null;
33841
- case "confirm":
33842
- return result.approved ? "yes" : "no";
33843
- case "one":
33844
- return result.option.label;
33845
- case "many":
33846
- case "rank":
33847
- return result.options.map((o) => o.label).join(", ");
33848
- }
33849
- }
33850
34216
  async function ask(state, rail, input) {
33851
34217
  if (!state.goal) return null;
33852
34218
  try {
33853
34219
  const sent = await rail.contact({
33854
- 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 }],
33855
34221
  waiting: "hard",
33856
- channel: input.call ? "call" : "notification"
34222
+ channel: "call"
33857
34223
  });
33858
34224
  if ("recorded" in sent) return null;
33859
34225
  const entryId = sent.requestEntryIds[0];
@@ -33864,106 +34230,416 @@ async function ask(state, rail, input) {
33864
34230
  return null;
33865
34231
  }
33866
34232
  }
33867
- function checkCallOnDone(text) {
33868
- if (!text) return false;
33869
- 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);
33870
- }
33871
- function parseQuestionOptions(text) {
33872
- if (!text) return { question: "" };
33873
- const trimmed = text.trim();
33874
- const lines2 = trimmed.split("\n").map((l) => l.trim()).filter(Boolean);
33875
- const optionLines = [];
33876
- const questionLines = [];
33877
- for (const line of lines2) {
33878
- const bulletMatch = line.match(/^[-*•]\s+(.*)$/) ?? line.match(/^\d+[.)]\s+(.*)$/);
33879
- if (bulletMatch) {
33880
- optionLines.push(bulletMatch[1].trim());
33881
- } else {
33882
- questionLines.push(line);
33883
- }
33884
- }
33885
- if (optionLines.length >= 2 && optionLines.length <= 6) {
33886
- return {
33887
- question: questionLines.join("\n\n").trim() || trimmed,
33888
- options: optionLines.map((raw) => {
33889
- const colonSplit = raw.match(/^([^:—–-]+)\s*[:—–-]\s*(.+)$/);
33890
- const parenSplit = raw.match(/^([^(]+)\s*\(([^)]+)\)$/);
33891
- if (colonSplit && colonSplit[1] && colonSplit[2]) {
33892
- return { label: colonSplit[1].trim().slice(0, 100), hint: colonSplit[2].trim().slice(0, 250) };
33893
- }
33894
- if (parenSplit && parenSplit[1] && parenSplit[2]) {
33895
- return { label: parenSplit[1].trim().slice(0, 100), hint: parenSplit[2].trim().slice(0, 250) };
33896
- }
33897
- return { label: raw.slice(0, 1e3) };
33898
- })
33899
- };
33900
- }
33901
- return { question: trimmed };
33902
- }
33903
34233
  async function askPermission(event, state, rail) {
33904
34234
  return decisionFrom(await ask(state, rail, {
33905
34235
  ask: [`The agent is blocked waiting to run: ${event.summary}`, ...event.reason ? [event.reason] : []].join("\n\n"),
33906
34236
  options: [
33907
34237
  { label: ALLOW, hint: "Permits the agent to run this command" },
33908
34238
  { label: "Deny", hint: "Blocks execution of this command" }
33909
- ],
33910
- call: true
33911
- }));
33912
- }
33913
- async function askQuestion(question, state, rail, opts) {
33914
- return wordsOf(await ask(state, rail, {
33915
- ask: question,
33916
- call: opts?.call ?? true,
33917
- options: opts?.options
34239
+ ]
33918
34240
  }));
33919
34241
  }
33920
34242
  async function finish(result, state, rail, opts) {
33921
34243
  const goal = state.goal;
33922
34244
  if (!goal) return;
33923
- state.goal = void 0;
33924
- const report = result?.trim() || "Done.";
33925
- 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;
33926
34260
  try {
33927
- await rail.contact({ asks: [{ ask: report, parentId: goal.id }], waiting: "none", channel });
33928
- const { revision } = await rail.read(goal.id);
33929
- 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." });
33930
34262
  } catch (error) {
33931
- opts?.log?.(`\u26A0 finish contact failed: ${error.message}`);
34263
+ opts?.log?.(`\u26A0 could not keep the turn as progress: ${error.message}`);
33932
34264
  }
33933
34265
  }
33934
- async function failedToStart(reason, state, rail, opts) {
34266
+ async function cannotWork(report, state, rail, opts) {
34267
+ state.resting = true;
33935
34268
  try {
33936
- const goalId = state.goal?.id ?? (await rail.claim()).goalId;
33937
- state.goal = void 0;
33938
- state.resting = true;
33939
- if (!goalId) return;
33940
- await rail.contact({
33941
- asks: [{ ask: `The session could not start, so nothing is working on this yet.
33942
-
33943
- ${reason}`, parentId: goalId }],
33944
- waiting: "none",
33945
- channel: "notification"
33946
- });
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" });
33947
34275
  } catch (error) {
33948
- opts?.log?.(`\u26A0 could not report the failed start: ${error.message}`);
34276
+ opts?.log?.(`\u26A0 could not say the agent cannot work: ${error.message}`);
33949
34277
  }
33950
34278
  }
33951
34279
  async function afterTurn(event, state, rail, opts) {
33952
- if (!event.failed && !endsWithQuestion(event.result)) {
33953
- await finish(event.result, state, rail, opts);
33954
- state.resting = true;
33955
- 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;
33956
34295
  }
33957
- 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
+ }
33958
34301
 
33959
- ${event.result?.trim() || "No result reported."}` : event.result ?? "";
33960
- const parsed = parseQuestionOptions(rawText);
33961
- const reply = await askQuestion(parsed.question, state, rail, {
33962
- call: true,
33963
- 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 }] : [];
33964
34380
  });
33965
- if (!reply) state.resting = true;
33966
- 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
+ }
33967
34643
  }
33968
34644
 
33969
34645
  // src/run.ts
@@ -33971,15 +34647,32 @@ function runHarness(opts) {
33971
34647
  let running = true;
33972
34648
  let tail = [];
33973
34649
  const state = { resting: true };
34650
+ const clock4 = beat();
33974
34651
  let session = null;
33975
34652
  const speaksAs = opts.resume ?? opts.session;
33976
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
+ }
33977
34668
  async function handle(event) {
33978
34669
  if (event.kind === "error") {
33979
34670
  opts.log(`\u26A0 ${event.message}`);
33980
34671
  if (event.fatal && running) {
33981
34672
  opts.log(`\u2716 could not start: ${event.message}`);
33982
- await failedToStart(event.message, state, rail, { log: opts.log });
34673
+ await cannotWork(`The session could not start, so nothing is working on this yet.
34674
+
34675
+ ${event.message}`, state, rail, { log: opts.log });
33983
34676
  stop2();
33984
34677
  }
33985
34678
  return;
@@ -33998,6 +34691,7 @@ function runHarness(opts) {
33998
34691
  return;
33999
34692
  }
34000
34693
  if (event.kind === "work") {
34694
+ if (state.carriedOn) runningAgain();
34001
34695
  opts.log(`\u2699 ${event.tool}${event.note ? ` \u2014 ${event.note.split("\n")[0] ?? ""}` : ""}`);
34002
34696
  tail = pushWork(tail, workLine(event));
34003
34697
  return;
@@ -34005,20 +34699,10 @@ function runHarness(opts) {
34005
34699
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
34006
34700
  if (event.kind === "idle") {
34007
34701
  tail = [];
34008
- if (endsWithQuestion(event.result)) {
34009
- const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
34010
- if (local?.trim() && session && running) {
34011
- opts.log(`you (here): ${local.split("\n")[0] ?? ""}`);
34012
- state.resting = false;
34013
- session.send(local);
34014
- return;
34015
- }
34016
- }
34017
- const reply = await afterTurn(event, state, rail, { log: opts.log });
34018
- if (reply && session && running) {
34019
- opts.log(`you: ${reply.split("\n")[0] ?? ""}`);
34020
- session.send(reply);
34021
- }
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");
34022
34706
  }
34023
34707
  }
34024
34708
  session = startSession({
@@ -34029,19 +34713,25 @@ function runHarness(opts) {
34029
34713
  ...opts.resume ? { resume: opts.resume } : {},
34030
34714
  ...opts.session ? { session: opts.session } : {},
34031
34715
  ...opts.bin ? { bin: opts.bin } : {},
34032
- ...opts.onExit ? { onExit: opts.onExit } : {},
34716
+ ...inbox ? { inbox: inbox.dir } : {},
34717
+ onExit: () => {
34718
+ inbox?.close();
34719
+ opts.onExit?.();
34720
+ },
34033
34721
  onEvent: (event) => void handle(event).catch((err) => opts.log(`\u26A0 ${err.message}`))
34034
34722
  });
34035
- 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(
34036
34724
  (e) => opts.log(`\u26A0 conversation loop stopped: ${e.message}`)
34037
34725
  );
34038
34726
  opts.log(`\u25B6 started ${opts.harness} in ${opts.cwd} (${opts.mode} mode)`);
34039
34727
  function stop2() {
34040
34728
  running = false;
34729
+ clock4.nudge("wake");
34041
34730
  tail = [];
34042
34731
  cancelAsks(state);
34043
34732
  session?.stop();
34044
34733
  session = null;
34734
+ inbox?.close();
34045
34735
  opts.log("\u25A0 stopped");
34046
34736
  }
34047
34737
  return {
@@ -34049,7 +34739,9 @@ function runHarness(opts) {
34049
34739
  session?.send(text);
34050
34740
  },
34051
34741
  working: () => running && state.resting !== true,
34052
- tail: () => [...tail],
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"),
34053
34745
  stop: stop2
34054
34746
  };
34055
34747
  }
@@ -34057,16 +34749,16 @@ function runHarness(opts) {
34057
34749
  // src/host.ts
34058
34750
  init_dist();
34059
34751
  init_catalog();
34060
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
34061
- import { homedir as homedir6 } from "os";
34062
- 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";
34063
34755
 
34064
34756
  // src/workspaces.ts
34065
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
34066
- import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
34067
- 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";
34068
34760
  function workspacesFile() {
34069
- return join4(homedir4(), ".paigy", "workspaces.json");
34761
+ return join5(homedir5(), ".paigy", "workspaces.json");
34070
34762
  }
34071
34763
  function listWorkspaces(deps) {
34072
34764
  try {
@@ -34077,31 +34769,94 @@ function listWorkspaces(deps) {
34077
34769
  }
34078
34770
  }
34079
34771
  function addWorkspace(dir, deps) {
34080
- const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir4()));
34772
+ const next = resolve2(dir.replace(/^~(?=$|\/)/, homedir5()));
34081
34773
  const all = listWorkspaces(deps);
34082
34774
  if (!all.includes(next)) {
34083
34775
  all.push(next);
34084
- mkdirSync2(dirname2(deps.file), { recursive: true });
34085
- writeFileSync2(deps.file, JSON.stringify(all, null, 2));
34776
+ mkdirSync3(dirname2(deps.file), { recursive: true });
34777
+ writeFileSync3(deps.file, JSON.stringify(all, null, 2));
34086
34778
  }
34087
34779
  return all;
34088
34780
  }
34089
34781
  function allowed(cwd, deps) {
34090
- const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir4()));
34782
+ const target = resolve2(cwd.replace(/^~(?=$|\/)/, homedir5()));
34091
34783
  return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
34092
34784
  }
34093
34785
  function resolveWakeDir(pinned, deps) {
34094
34786
  if (pinned && allowed(pinned, deps)) {
34095
- const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir4()));
34787
+ const dir = resolve2(pinned.replace(/^~(?=$|\/)/, homedir5()));
34096
34788
  if ((deps.exists ?? existsSync4)(dir)) return dir;
34097
34789
  }
34098
34790
  return listWorkspaces(deps)[0];
34099
34791
  }
34100
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
+
34101
34856
  // src/owner.ts
34102
- import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync } from "fs";
34103
- import { homedir as homedir5 } from "os";
34104
- 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";
34105
34860
  var HEAD_BYTES = 64e3;
34106
34861
  function readHead(file) {
34107
34862
  const fd = openSync2(file, "r");
@@ -34117,8 +34872,8 @@ function ownerSession(slot, deps = {}) {
34117
34872
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
34118
34873
  if (!m) return null;
34119
34874
  const prefix = m[1];
34120
- const root = deps.projectsDir ?? join5(homedir5(), ".claude", "projects");
34121
- const list = deps.list ?? ((dir) => readdirSync(dir));
34875
+ const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
34876
+ const list = deps.list ?? ((dir) => readdirSync3(dir));
34122
34877
  const head = deps.head ?? readHead;
34123
34878
  let folders;
34124
34879
  try {
@@ -34129,7 +34884,7 @@ function ownerSession(slot, deps = {}) {
34129
34884
  for (const folder of folders) {
34130
34885
  let files;
34131
34886
  try {
34132
- files = list(join5(root, folder));
34887
+ files = list(join7(root, folder));
34133
34888
  } catch {
34134
34889
  continue;
34135
34890
  }
@@ -34137,7 +34892,7 @@ function ownerSession(slot, deps = {}) {
34137
34892
  if (!file) continue;
34138
34893
  let text;
34139
34894
  try {
34140
- text = head(join5(root, folder, file));
34895
+ text = head(join7(root, folder, file));
34141
34896
  } catch {
34142
34897
  continue;
34143
34898
  }
@@ -34157,7 +34912,7 @@ function ownerSession(slot, deps = {}) {
34157
34912
  // src/update.ts
34158
34913
  import { execFile } from "child_process";
34159
34914
  import { createRequire } from "module";
34160
- import { dirname as dirname3, join as join6 } from "path";
34915
+ import { dirname as dirname3, join as join8 } from "path";
34161
34916
  import { promisify } from "util";
34162
34917
  var SERVICE_LABEL = "ai.paigy.harness";
34163
34918
  var VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -34184,7 +34939,7 @@ async function latestVersion() {
34184
34939
  }
34185
34940
  async function install(version) {
34186
34941
  const bin = dirname3(process.execPath);
34187
- await promisify(execFile)(join6(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34942
+ await promisify(execFile)(join8(bin, "npm"), ["install", "-g", `@paigy/harness@${version}`], {
34188
34943
  env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}`, npm_config_update_notifier: "false" },
34189
34944
  timeout: 5 * 6e4
34190
34945
  });
@@ -34235,25 +34990,43 @@ function selfUpdater(deps) {
34235
34990
  }
34236
34991
 
34237
34992
  // src/host.ts
34238
- var HOST_FILE = join7(homedir6(), ".paigy", "host.json");
34993
+ var HOST_FILE = join9(homedir8(), ".paigy", "host.json");
34239
34994
  var runKey = (slot) => `slot:${slot}`;
34240
34995
  var claimedRun = (sessionId2) => {
34241
34996
  const slot = sessionSlot(sessionId2);
34242
34997
  return { slot, key: runKey(slot) };
34243
34998
  };
34244
34999
  var launchRun = (spec) => ({ session: spec.sessionId });
34245
- async function slotHasWaitingWork(token, session) {
34246
- return (await checkReplies({ token, ...session ? { reach: reachAs(session) } : {} })).deliveries.length > 0;
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);
34247
35014
  }
34248
35015
  var PRESENCE_FRESH_MS = 3 * 6e4;
34249
- function wakeAction(live, now, serverSeenAt) {
35016
+ function wakeAction(live, now, serverSeenAt, here = {}) {
34250
35017
  if (live) return { action: "skip", why: "run" };
34251
- 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
+ }
34252
35023
  return { action: "spawn", why: "nobody" };
34253
35024
  }
34254
35025
  function startHost(opts) {
34255
35026
  const runs = /* @__PURE__ */ new Map();
34256
35027
  const asHost = { token: opts.token };
35028
+ const goneAt = /* @__PURE__ */ new Map();
35029
+ const heldHere = /* @__PURE__ */ new Set();
34257
35030
  const refreshIdentities = async () => {
34258
35031
  for (const slot of listSlots()) {
34259
35032
  if (slot === "Desktop") continue;
@@ -34273,7 +35046,7 @@ function startHost(opts) {
34273
35046
  working: () => [...runs.values()].filter((r) => r.run.working()).length,
34274
35047
  log: opts.log
34275
35048
  });
34276
- const beat = () => {
35049
+ const beat2 = () => {
34277
35050
  void refreshIdentities();
34278
35051
  void update().catch(() => {
34279
35052
  });
@@ -34302,9 +35075,11 @@ function startHost(opts) {
34302
35075
  token: spec.token,
34303
35076
  ...launchRun(spec),
34304
35077
  onExit: () => {
34305
- runs.delete(key);
35078
+ evict(runs, key, run);
35079
+ goneAt.set(slot, Date.now());
34306
35080
  opts.log(`\u2716 ${label} ended`);
34307
35081
  },
35082
+ name: label,
34308
35083
  log
34309
35084
  });
34310
35085
  runs.set(key, { run, label, token: spec.token });
@@ -34331,7 +35106,13 @@ function startHost(opts) {
34331
35106
  return presence;
34332
35107
  }
34333
35108
  let lastPresence = /* @__PURE__ */ new Map();
34334
- 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") {
34335
35116
  {
34336
35117
  const harness = SLOT_HARNESS[slot] ?? (slot === "Desktop" ? void 0 : "claude");
34337
35118
  const key = runKey(slot);
@@ -34339,19 +35120,42 @@ function startHost(opts) {
34339
35120
  const live = runs.get(key);
34340
35121
  const tokenId = slotIdentity(slot).tokenId;
34341
35122
  const seenByServer = tokenId ? presence.get(tokenId) : void 0;
34342
- 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) });
34343
35127
  if (decided.action === "skip") {
34344
- 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`);
34345
35131
  return;
34346
35132
  }
35133
+ const label = slotName(slot) ?? slot;
35134
+ const skip = (why) => opts.log(`\u25CC ${label} ${why} \u2014 not spawning`);
34347
35135
  const token = readToken(slot);
34348
- if (!token) return;
35136
+ if (!token) {
35137
+ skip("has no token on this machine");
35138
+ return;
35139
+ }
34349
35140
  const owner = ownerSession(slot);
34350
- 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
+ }
34351
35145
  const workspace = owner?.cwd ?? resolveWakeDir(slotIdentity(slot).workspace, opts.wsDeps);
34352
- if (!workspace) return;
34353
- if (!await slotHasWaitingWork(token, owner?.sessionId).catch(() => false)) return;
34354
- 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
+ }
34355
35159
  const log = (line) => opts.log(`[${label}] ${line}`);
34356
35160
  const run = runHarness({
34357
35161
  harness,
@@ -34360,17 +35164,20 @@ function startHost(opts) {
34360
35164
  prompt: "",
34361
35165
  token,
34362
35166
  ...owner ? { resume: owner.sessionId } : {},
34363
- // 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.
34364
35169
  onExit: () => {
34365
- runs.delete(key);
35170
+ evict(runs, key, run);
35171
+ goneAt.set(slot, Date.now());
34366
35172
  opts.log(`\u2716 ${label} ended`);
34367
35173
  },
35174
+ name: label,
34368
35175
  log
34369
35176
  });
34370
35177
  runs.set(key, { run, label, token });
34371
35178
  void heartbeat(void 0, { token }).catch(() => {
34372
35179
  });
34373
- 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)" : ""}`);
34374
35181
  }
34375
35182
  }
34376
35183
  const listening = /* @__PURE__ */ new Map();
@@ -34390,7 +35197,7 @@ function startHost(opts) {
34390
35197
  joining.add(slot);
34391
35198
  try {
34392
35199
  const sub = await subscribeWake(
34393
- () => void presenceMap().then((presence) => wakeSlot(slot, presence)).catch(() => {
35200
+ () => void evidence().then((seen) => wakeSlot(slot, seen, "wake")).catch(() => {
34394
35201
  }),
34395
35202
  { token }
34396
35203
  );
@@ -34404,8 +35211,8 @@ function startHost(opts) {
34404
35211
  }
34405
35212
  }
34406
35213
  async function sweepSlots() {
34407
- const presence = await presenceMap();
34408
- for (const slot of listSlots()) await wakeSlot(slot, presence);
35214
+ const seen = await evidence();
35215
+ for (const slot of listSlots()) await wakeSlot(slot, seen);
34409
35216
  }
34410
35217
  const ACTIVITY_MS = 2e3;
34411
35218
  const published = /* @__PURE__ */ new Map();
@@ -34430,12 +35237,12 @@ function startHost(opts) {
34430
35237
  };
34431
35238
  const publish = () => {
34432
35239
  try {
34433
- 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() }));
34434
35241
  } catch {
34435
35242
  }
34436
35243
  };
34437
- beat();
34438
- const pulse = setInterval(beat, 6e4);
35244
+ beat2();
35245
+ const pulse = setInterval(beat2, 6e4);
34439
35246
  const spawnPoll = setInterval(() => {
34440
35247
  void claimAndSpawn();
34441
35248
  publish();
@@ -34461,10 +35268,10 @@ function startHost(opts) {
34461
35268
  // them live, and a run mid-turn makes it working.
34462
35269
  roster: () => {
34463
35270
  const live = new Map([...runs.values()].map((r) => [r.label, r.run]));
34464
- const names = /* @__PURE__ */ new Map();
34465
- for (const key of listSlots()) if (key !== "Desktop") names.set(slotName(key) ?? key, key);
34466
- for (const label of live.keys()) if (!names.has(label)) names.set(label, null);
34467
- 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]) => {
34468
35275
  const id = slot ? slotIdentity(slot) : { voice: null, tokenId: null };
34469
35276
  return {
34470
35277
  name,
@@ -34481,7 +35288,7 @@ function startHost(opts) {
34481
35288
  // sessions we started ourselves. The server already knows: `last_seen_at` on the
34482
35289
  // agent's own token, the same evidence `wakeAction` stands back for. So: a run we
34483
35290
  // hold, OR a process the server heard from inside `PRESENCE_FRESH_MS`.
34484
- 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,
34485
35292
  // `working` stays ours alone — it means a run WE hold is mid-turn, which is the only
34486
35293
  // thing a tail can tell us. A process we did not spawn has no tail here to read.
34487
35294
  working: live.get(name)?.working() ?? false
@@ -34497,7 +35304,7 @@ function startHost(opts) {
34497
35304
  streamActivity();
34498
35305
  clearInterval(activityTick);
34499
35306
  try {
34500
- writeFileSync3(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
35307
+ writeFileSync4(HOST_FILE, JSON.stringify({ pid: process.pid, at: 0, roster: [] }));
34501
35308
  } catch {
34502
35309
  }
34503
35310
  }
@@ -34602,18 +35409,18 @@ async function pairThisMachine() {
34602
35409
  }
34603
35410
  }
34604
35411
  function forwardCodexThreadId() {
34605
- 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");
34606
35413
  if (!existsSync5(cfg)) return;
34607
- const text = readFileSync5(cfg, "utf8");
35414
+ const text = readFileSync6(cfg, "utf8");
34608
35415
  const head = "[mcp_servers.paigy]\n";
34609
35416
  const at = text.indexOf(head);
34610
35417
  if (at < 0) return;
34611
- const block = text.slice(at + head.length, (() => {
35418
+ const block2 = text.slice(at + head.length, (() => {
34612
35419
  const n = text.indexOf("\n[", at + head.length);
34613
35420
  return n < 0 ? text.length : n;
34614
35421
  })());
34615
- if (/^\s*env_vars\s*=/m.test(block)) return;
34616
- 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));
34617
35424
  }
34618
35425
  async function main() {
34619
35426
  const parsed = parseArgs(process.argv.slice(2));
@@ -34704,7 +35511,7 @@ async function main() {
34704
35511
  const wsDeps = { file: workspacesFile() };
34705
35512
  for (const dir of parsed.grant) addWorkspace(dir, wsDeps);
34706
35513
  if (!listWorkspaces(wsDeps).length) {
34707
- const guess = join8(homedir8(), "projects");
35514
+ const guess = join10(homedir10(), "projects");
34708
35515
  if (existsSync5(guess)) {
34709
35516
  addWorkspace(guess, wsDeps);
34710
35517
  console.log(`\u2713 workspace granted: ${guess} (change with --grant DIR)`);
@@ -34717,23 +35524,14 @@ async function main() {
34717
35524
  console.log("service install failed \u2014 run `paigy-harness host` to host in the foreground");
34718
35525
  }
34719
35526
  } else console.log("start hosting with: paigy-harness host (keep it running under your init system)");
34720
- const TERMINAL_AGENTS = [
34721
- { cli: "claude", name: "Claude Code", wire: "claude plugin marketplace add paigy-ai/mcp; claude plugin install paigy@paigy" },
34722
- // Plain add only. Forwarding CODEX_THREAD_ID is a SECOND step (forwardCodexThreadId):
34723
- // a `-c mcp_servers.paigy.env_vars=…` override on `mcp add` lands before the block is
34724
- // written, codex sees a half-table and rejects it ("invalid transport"), and nothing at
34725
- // all gets wired — which the old silent catch below hid entirely (found by QA 2026-09-06).
34726
- { cli: "codex", name: "Codex", wire: "codex mcp add paigy -- npx -y @paigy/mcp@latest" },
34727
- { cli: "agy", name: "Antigravity" }
34728
- ];
34729
- for (const t of TERMINAL_AGENTS) {
34730
- if (!which(t.cli) || !t.wire) continue;
35527
+ for (const t of MCP_WIRES) {
35528
+ if (!which(t.cli)) continue;
34731
35529
  try {
34732
35530
  execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
34733
35531
  if (t.cli === "codex") forwardCodexThreadId();
34734
35532
  console.log(`\u2713 paigy wired for ${t.cli} \u2014 each session hatches its own identity (restart open sessions)`);
34735
35533
  } catch (e) {
34736
- 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}`);
34737
35535
  }
34738
35536
  }
34739
35537
  if (which("claude")) {
@@ -34745,12 +35543,12 @@ async function main() {
34745
35543
  }
34746
35544
  }
34747
35545
  try {
34748
- const settings = join8(homedir8(), ".claude", "settings.json");
35546
+ const settings = join10(homedir10(), ".claude", "settings.json");
34749
35547
  if (which("claude") && existsSync5(settings)) {
34750
- const cfg = JSON.parse(readFileSync5(settings, "utf8"));
35548
+ const cfg = JSON.parse(readFileSync6(settings, "utf8"));
34751
35549
  if (!cfg["statusLine"]) {
34752
35550
  cfg["statusLine"] = { type: "command", command: "npx -y -p @paigy/mcp@latest paigy-statusline" };
34753
- writeFileSync4(settings, JSON.stringify(cfg, null, 2));
35551
+ writeFileSync5(settings, JSON.stringify(cfg, null, 2));
34754
35552
  console.log("\u2713 statusline wired");
34755
35553
  }
34756
35554
  }
@@ -34803,14 +35601,14 @@ async function main() {
34803
35601
  <key>SoftResourceLimits</key><dict>
34804
35602
  <key>NumberOfFiles</key><integer>65536</integer>
34805
35603
  </dict>
34806
- <key>StandardOutPath</key><string>${join8(homedir8(), ".paigy", "host.log")}</string>
34807
- <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>
34808
35606
  </dict></plist>
34809
35607
  `;
34810
- const dir = join8(homedir8(), "Library", "LaunchAgents");
34811
- mkdirSync3(dir, { recursive: true });
34812
- const path = join8(dir, `${SERVICE_LABEL}.plist`);
34813
- 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);
34814
35612
  execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
34815
35613
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
34816
35614
  console.log(" file limit raised to 65536 for the host and every session it spawns");
@@ -34832,7 +35630,7 @@ async function main() {
34832
35630
  process.exit(1);
34833
35631
  }
34834
35632
  const name = slotName(slot) ?? slot;
34835
- 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(), "~")}`);
34836
35634
  if (!allowed(cwd, { file: workspacesFile() })) {
34837
35635
  console.log(` \u26A0 that folder isn't on the allow-list, so wakes will land in the first granted`);
34838
35636
  console.log(` workspace instead \u2014 add it in the desktop app, or: paigy-harness host --grant ${cwd}`);
@@ -34924,12 +35722,11 @@ async function main() {
34924
35722
  const said = await askTerminal(`\u23F8 blocked: ${summary} \u2014 allow? [y/n]`);
34925
35723
  if (!said) return null;
34926
35724
  return /^(y|yes|ok|allow|approve)/i.test(said) ? { allow: true } : { allow: false, reason: said };
34927
- },
34928
- question: (q) => askTerminal(`\u2753 ${q}`)
35725
+ }
34929
35726
  },
34930
35727
  log: (line) => console.log(line)
34931
35728
  });
34932
- const beat = async () => {
35729
+ const beat2 = async () => {
34933
35730
  try {
34934
35731
  await heartbeat();
34935
35732
  signedOut = false;
@@ -34942,8 +35739,8 @@ async function main() {
34942
35739
  console.log(" paigy-harness pair");
34943
35740
  }
34944
35741
  };
34945
- const pulse = setInterval(() => void beat(), 6e4);
34946
- void beat();
35742
+ const pulse = setInterval(() => void beat2(), 6e4);
35743
+ void beat2();
34947
35744
  pulse.unref?.();
34948
35745
  process.on("SIGINT", () => {
34949
35746
  run.stop();