@paigy/harness 0.3.40 → 0.3.42

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 +7 -37
  2. package/dist/cli.js +28903 -29641
  3. package/dist/main.js +329 -945
  4. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -11041,6 +11041,31 @@ var AgendaTurnSchema = external_exports.object({
11041
11041
  ifSilent: external_exports.boolean().optional()
11042
11042
  });
11043
11043
  var CLAIM_STALE_MS = 30 * 6e4;
11044
+ var CallQuestionSchema = external_exports.object({
11045
+ id: external_exports.string(),
11046
+ entryId: external_exports.string(),
11047
+ title: external_exports.string(),
11048
+ state: external_exports.enum(["open", "answered"]),
11049
+ answer: external_exports.string().nullable(),
11050
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
11051
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
11052
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
11053
+ * author is not an agent this account holds (unpaired since, or a person). */
11054
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
11055
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
11056
+ * its need offers, exactly as its own card carries them, present only when one of them has a
11057
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
11058
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
11059
+ options: external_exports.array(OptionSchema).optional(),
11060
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
11061
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
11062
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
11063
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
11064
+ * preview layout for any `options`, and must see exactly what it did. */
11065
+ choices: external_exports.array(OptionSchema).optional(),
11066
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
11067
+ select: external_exports.enum(["one", "many", "rank"]).optional()
11068
+ });
11044
11069
  var InboxItemSchema = external_exports.object({
11045
11070
  id: external_exports.string(),
11046
11071
  tokenId: external_exports.string().optional(),
@@ -11082,31 +11107,10 @@ var InboxItemSchema = external_exports.object({
11082
11107
  * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
11083
11108
  * the card's own concise heading; `answer` the accepted answer in words, null while open. It
11084
11109
  * REPLACED `agenda` (turns), which nothing ever filled. */
11085
- questions: external_exports.array(external_exports.object({
11086
- id: external_exports.string(),
11087
- entryId: external_exports.string(),
11088
- title: external_exports.string(),
11089
- state: external_exports.enum(["open", "answered"]),
11090
- answer: external_exports.string().nullable(),
11091
- /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
11092
- * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
11093
- * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
11094
- * author is not an agent this account holds (unpaired since, or a person). */
11095
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
11096
- /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
11097
- * its need offers, exactly as its own card carries them, present only when one of them has a
11098
- * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
11099
- * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
11100
- options: external_exports.array(OptionSchema).optional(),
11101
- /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
11102
- * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
11103
- * numbered chips above the call's controls, so a checklist is never only something read aloud. A
11104
- * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
11105
- * preview layout for any `options`, and must see exactly what it did. */
11106
- choices: external_exports.array(OptionSchema).optional(),
11107
- /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
11108
- select: external_exports.enum(["one", "many", "rank"]).optional()
11109
- })).optional(),
11110
+ questions: external_exports.array(CallQuestionSchema).optional(),
11111
+ /** The same call carrying order, including request Entries with no DecisionNeed as reports.
11112
+ * Additive: questions stays decision-only for every other surface and older bundles. */
11113
+ callItems: external_exports.array(CallQuestionSchema.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
11110
11114
  visuals: external_exports.array(VisualSchema).optional(),
11111
11115
  /** The connected agent's name (the single pairing name — user-typed, or the
11112
11116
  * agent's suggestion, or a default silly name). */
@@ -11305,12 +11309,29 @@ var UserSettingsSchema = external_exports.object({
11305
11309
  * clobber guard as voiceMode: absent = leave unchanged on write. */
11306
11310
  broker: BrokerTuningSchema.optional()
11307
11311
  });
11312
+ var GoalLatestSchema = external_exports.object({
11313
+ from: external_exports.enum(["person", "agent"]),
11314
+ said: external_exports.string(),
11315
+ at: external_exports.string(),
11316
+ entryId: external_exports.string(),
11317
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
11318
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
11319
+ * none was. The Work row reads it before `said`. */
11320
+ summary: external_exports.string().optional(),
11321
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
11322
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
11323
+ titles: external_exports.array(external_exports.string()).optional(),
11324
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
11325
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
11326
+ line: external_exports.string().optional()
11327
+ });
11308
11328
  var HistoryWorkSchema = external_exports.object({
11309
11329
  id: external_exports.string(),
11310
11330
  title: external_exports.string(),
11311
11331
  state: external_exports.enum(["done", "cancelled"]),
11312
11332
  /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
11313
- assignee: external_exports.string()
11333
+ assignee: external_exports.string(),
11334
+ latest: GoalLatestSchema.nullable().optional()
11314
11335
  });
11315
11336
  var HistoryEntrySchema = external_exports.union([
11316
11337
  external_exports.object({ at: external_exports.string(), card: InboxItemSchema }),
@@ -11378,7 +11399,7 @@ var ConnectionSummarySchema = external_exports.object({
11378
11399
  /** The @paigy/harness this host is running — a machine the self-update has not reached
11379
11400
  * shows its age here (`apps/desktop/src/update.ts`). */
11380
11401
  version: external_exports.string().optional(),
11381
- harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
11402
+ harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string(), models: external_exports.array(external_exports.string()).optional() })).optional(),
11382
11403
  workspaces: external_exports.array(external_exports.string()).optional(),
11383
11404
  /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
11384
11405
  * repo, and each repo directly inside one, with its `origin` remote. A session started for
@@ -11538,22 +11559,7 @@ var QueueItemSchema = external_exports.object({
11538
11559
  * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
11539
11560
  * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
11540
11561
  * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
11541
- latest: external_exports.object({
11542
- from: external_exports.enum(["person", "agent"]),
11543
- said: external_exports.string(),
11544
- at: external_exports.string(),
11545
- entryId: external_exports.string(),
11546
- /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
11547
- * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
11548
- * none was. The Work row reads it before `said`. */
11549
- summary: external_exports.string().optional(),
11550
- /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
11551
- * of each unit"): each at most 50 characters, in order; absent when it has none. */
11552
- titles: external_exports.array(external_exports.string()).optional(),
11553
- /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
11554
- * update's own line, before its units; the row caps it at 200. Absent on anything else. */
11555
- line: external_exports.string().optional()
11556
- }).nullable().optional(),
11562
+ latest: GoalLatestSchema.nullable().optional(),
11557
11563
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
11558
11564
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
11559
11565
  * answer to a question, a voice note filed as work. Null when the only hands on it have been its
@@ -11625,58 +11631,6 @@ var NoteSchema = external_exports.object({
11625
11631
  lastRideAt: external_exports.string().nullable().default(null),
11626
11632
  createdAt: external_exports.string()
11627
11633
  });
11628
- var TriageItemSchema = external_exports.object({
11629
- noteId: external_exports.string(),
11630
- /** The note's headline at run time. */
11631
- title: external_exports.string(),
11632
- /** WHY, in one short human line, evidence first — this is read on a phone underneath
11633
- * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
11634
- * Never a model's reasoning transcript, never an id. */
11635
- why: external_exports.string()
11636
- });
11637
- var TriageAssignmentSchema = external_exports.object({
11638
- /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
11639
- agent: external_exports.string(),
11640
- /** Its display name at run time (the name on the hatchling's card). Denormalized for the
11641
- * same reason as `title`: the card must render from the proposal alone. */
11642
- agentName: external_exports.string(),
11643
- notes: external_exports.array(TriageItemSchema)
11644
- });
11645
- var TriageStatusSchema = external_exports.enum(["open", "superseded", "dismissed"]);
11646
- var SubmitTriageSchema = external_exports.object({
11647
- /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
11648
- * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
11649
- * so the phone can say where the content went — an unattributed privacy claim is worth
11650
- * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
11651
- provider: external_exports.string().min(1).max(60),
11652
- /** The concrete model when the provider names one (an ollama tag); null otherwise. */
11653
- model: external_exports.string().max(200).nullable().optional(),
11654
- /** How many open notes the run actually looked at — the denominator on the phone
11655
- * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
11656
- reviewed: external_exports.number().int().min(0).max(1e4).default(0),
11657
- close: external_exports.array(TriageItemSchema).max(200).default([]),
11658
- stale: external_exports.array(TriageItemSchema).max(200).default([]),
11659
- assign: external_exports.array(TriageAssignmentSchema).max(50).default([])
11660
- });
11661
- var TriageProposalSchema = SubmitTriageSchema.extend({
11662
- id: external_exports.string(),
11663
- runAt: external_exports.string(),
11664
- status: TriageStatusSchema,
11665
- model: external_exports.string().nullable().default(null)
11666
- });
11667
- var AcceptTriageSchema = external_exports.discriminatedUnion("group", [
11668
- external_exports.object({ group: external_exports.literal("close"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
11669
- external_exports.object({ group: external_exports.literal("stale"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
11670
- external_exports.object({
11671
- group: external_exports.literal("assign"),
11672
- agent: external_exports.string().min(1),
11673
- noteIds: external_exports.array(external_exports.string()).max(200).optional()
11674
- })
11675
- ]);
11676
- var AcceptTriageResultSchema = external_exports.object({
11677
- accepted: external_exports.array(external_exports.string()),
11678
- failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
11679
- });
11680
11634
  var DeliveryModeSchema = external_exports.enum(["poll", "self_hosted"]);
11681
11635
  var WAKE_EVENT = "wake";
11682
11636
  var wakeChannel = (tokenId) => `wake:${tokenId}`;
@@ -12164,13 +12118,6 @@ async function registerDelivery(mode, opts = {}) {
12164
12118
  if (!res.ok) throw new Error(`register_delivery failed: ${res.status} ${await res.text()}`);
12165
12119
  return await res.json();
12166
12120
  }
12167
- async function listNotes(opts = {}) {
12168
- const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/notes`, {
12169
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
12170
- }));
12171
- if (!res.ok) throw new Error(`list_notes failed: ${res.status} ${await res.text()}`);
12172
- return await res.json();
12173
- }
12174
12121
  async function listConnections(opts = {}) {
12175
12122
  const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/tokens`, {
12176
12123
  headers: { authorization: `Bearer ${authToken(opts.token)}` }
@@ -12178,39 +12125,6 @@ async function listConnections(opts = {}) {
12178
12125
  if (!res.ok) throw new Error(`list_connections failed: ${res.status} ${await res.text()}`);
12179
12126
  return await res.json();
12180
12127
  }
12181
- async function submitTriage(run, opts = {}) {
12182
- const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
12183
- method: "POST",
12184
- headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
12185
- body: JSON.stringify(run)
12186
- }));
12187
- if (!res.ok) throw new Error(`submit_triage failed: ${res.status} ${await res.text()}`);
12188
- return await res.json();
12189
- }
12190
- async function getTriage(opts = {}) {
12191
- const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage`, {
12192
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
12193
- }));
12194
- if (!res.ok) throw new Error(`get_triage failed: ${res.status} ${await res.text()}`);
12195
- return await res.json();
12196
- }
12197
- async function acceptTriage(proposalId, body, opts = {}) {
12198
- const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/accept`, {
12199
- method: "POST",
12200
- headers: { "content-type": "application/json", authorization: `Bearer ${authToken(opts.token)}` },
12201
- body: JSON.stringify(body)
12202
- }));
12203
- if (!res.ok) throw new Error(`accept_triage failed: ${res.status} ${await res.text()}`);
12204
- return await res.json();
12205
- }
12206
- async function dismissTriage(proposalId, opts = {}) {
12207
- const res = await ensureAuthed(await reach(`${BACKEND_URL}/api/triage/${encodeURIComponent(proposalId)}/dismiss`, {
12208
- method: "POST",
12209
- headers: { authorization: `Bearer ${authToken(opts.token)}` }
12210
- }));
12211
- if (!res.ok) throw new Error(`dismiss_triage failed: ${res.status} ${await res.text()}`);
12212
- return await res.json();
12213
- }
12214
12128
  var NOT_SENT = `Recorded as the Goal's progress, not sent: nobody was notified. The person is told only a question, an answer to one they asked you, and what you say once the Goal is done. To close the work, send contact.completions or manage_goals kind:complete with the shared completion report, closing and reporting together. If the person asked you, in so many words, to send them this, send it again with userExplicitlyRequested: "any" on that update.`;
12215
12129
  var APPLIED = "Applied; nothing was sent to the person.";
12216
12130
  var DEMOTED = "Delivered as a card, not a call: the person's settings do not allow a call right now (silent, quiet hours, or a mode). It has reached them; do not send it again.";
@@ -12473,7 +12387,7 @@ async function subscribeWake(onNudge, opts = {}) {
12473
12387
 
12474
12388
  // src/main.ts
12475
12389
  var import_qrcode = __toESM(require_lib(), 1);
12476
- import { homedir as homedir11, hostname } from "os";
12390
+ import { homedir as homedir10, hostname } from "os";
12477
12391
 
12478
12392
  // src/harness/catalog.ts
12479
12393
  import { spawnSync } from "child_process";
@@ -12489,7 +12403,8 @@ var CATALOG = [
12489
12403
  authProbe: ["claude", "auth", "status"],
12490
12404
  loginHint: "Run `claude` once in a terminal and sign in.",
12491
12405
  installCli: "curl -fsSL https://claude.ai/install.sh | bash",
12492
- installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
12406
+ installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp",
12407
+ models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
12493
12408
  },
12494
12409
  {
12495
12410
  name: "codex",
@@ -12499,7 +12414,8 @@ var CATALOG = [
12499
12414
  authProbe: ["codex", "login", "status"],
12500
12415
  loginHint: "Run `codex login` to authenticate.",
12501
12416
  installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
12502
- installAdapter: "npm install -g @agentclientprotocol/codex-acp"
12417
+ installAdapter: "npm install -g @agentclientprotocol/codex-acp",
12418
+ models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"]
12503
12419
  },
12504
12420
  {
12505
12421
  name: "agy",
@@ -12509,7 +12425,8 @@ var CATALOG = [
12509
12425
  adapter: "paigy-agy-acp",
12510
12426
  authProbe: ["agy", "help"],
12511
12427
  loginHint: "Install and set up Antigravity (`agy`).",
12512
- installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
12428
+ installCli: "curl -fsSL https://antigravity.dev/install.sh | bash",
12429
+ models: ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-pro-exp"]
12513
12430
  }
12514
12431
  ];
12515
12432
  function probeDirs(home = homedir3()) {
@@ -12545,7 +12462,7 @@ function runProbe(argv) {
12545
12462
  function detect(entry, deps = {}) {
12546
12463
  const find = deps.which ?? which;
12547
12464
  const probe = deps.probe ?? runProbe;
12548
- const base = { name: entry.name, label: entry.label };
12465
+ const base = { name: entry.name, label: entry.label, models: entry.models };
12549
12466
  const cliPath = find(entry.cli);
12550
12467
  if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
12551
12468
  const adapterPath = entry.adapter ? find(entry.adapter) : null;
@@ -14944,6 +14861,31 @@ var AgendaTurnSchema2 = external_exports.object({
14944
14861
  ifSilent: external_exports.boolean().optional()
14945
14862
  });
14946
14863
  var CLAIM_STALE_MS2 = 30 * 6e4;
14864
+ var CallQuestionSchema2 = external_exports.object({
14865
+ id: external_exports.string(),
14866
+ entryId: external_exports.string(),
14867
+ title: external_exports.string(),
14868
+ state: external_exports.enum(["open", "answered"]),
14869
+ answer: external_exports.string().nullable(),
14870
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
14871
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
14872
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
14873
+ * author is not an agent this account holds (unpaired since, or a person). */
14874
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
14875
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
14876
+ * its need offers, exactly as its own card carries them, present only when one of them has a
14877
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
14878
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
14879
+ options: external_exports.array(OptionSchema2).optional(),
14880
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
14881
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
14882
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
14883
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
14884
+ * preview layout for any `options`, and must see exactly what it did. */
14885
+ choices: external_exports.array(OptionSchema2).optional(),
14886
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
14887
+ select: external_exports.enum(["one", "many", "rank"]).optional()
14888
+ });
14947
14889
  var InboxItemSchema2 = external_exports.object({
14948
14890
  id: external_exports.string(),
14949
14891
  tokenId: external_exports.string().optional(),
@@ -14985,31 +14927,10 @@ var InboxItemSchema2 = external_exports.object({
14985
14927
  * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
14986
14928
  * the card's own concise heading; `answer` the accepted answer in words, null while open. It
14987
14929
  * REPLACED `agenda` (turns), which nothing ever filled. */
14988
- questions: external_exports.array(external_exports.object({
14989
- id: external_exports.string(),
14990
- entryId: external_exports.string(),
14991
- title: external_exports.string(),
14992
- state: external_exports.enum(["open", "answered"]),
14993
- answer: external_exports.string().nullable(),
14994
- /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
14995
- * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
14996
- * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
14997
- * author is not an agent this account holds (unpaired since, or a person). */
14998
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
14999
- /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
15000
- * its need offers, exactly as its own card carries them, present only when one of them has a
15001
- * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
15002
- * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
15003
- options: external_exports.array(OptionSchema2).optional(),
15004
- /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
15005
- * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
15006
- * numbered chips above the call's controls, so a checklist is never only something read aloud. A
15007
- * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
15008
- * preview layout for any `options`, and must see exactly what it did. */
15009
- choices: external_exports.array(OptionSchema2).optional(),
15010
- /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
15011
- select: external_exports.enum(["one", "many", "rank"]).optional()
15012
- })).optional(),
14930
+ questions: external_exports.array(CallQuestionSchema2).optional(),
14931
+ /** The same call carrying order, including request Entries with no DecisionNeed as reports.
14932
+ * Additive: questions stays decision-only for every other surface and older bundles. */
14933
+ callItems: external_exports.array(CallQuestionSchema2.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
15013
14934
  visuals: external_exports.array(VisualSchema2).optional(),
15014
14935
  /** The connected agent's name (the single pairing name — user-typed, or the
15015
14936
  * agent's suggestion, or a default silly name). */
@@ -15208,12 +15129,29 @@ var UserSettingsSchema2 = external_exports.object({
15208
15129
  * clobber guard as voiceMode: absent = leave unchanged on write. */
15209
15130
  broker: BrokerTuningSchema2.optional()
15210
15131
  });
15132
+ var GoalLatestSchema2 = external_exports.object({
15133
+ from: external_exports.enum(["person", "agent"]),
15134
+ said: external_exports.string(),
15135
+ at: external_exports.string(),
15136
+ entryId: external_exports.string(),
15137
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
15138
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
15139
+ * none was. The Work row reads it before `said`. */
15140
+ summary: external_exports.string().optional(),
15141
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
15142
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
15143
+ titles: external_exports.array(external_exports.string()).optional(),
15144
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
15145
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
15146
+ line: external_exports.string().optional()
15147
+ });
15211
15148
  var HistoryWorkSchema2 = external_exports.object({
15212
15149
  id: external_exports.string(),
15213
15150
  title: external_exports.string(),
15214
15151
  state: external_exports.enum(["done", "cancelled"]),
15215
15152
  /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
15216
- assignee: external_exports.string()
15153
+ assignee: external_exports.string(),
15154
+ latest: GoalLatestSchema2.nullable().optional()
15217
15155
  });
15218
15156
  var HistoryEntrySchema2 = external_exports.union([
15219
15157
  external_exports.object({ at: external_exports.string(), card: InboxItemSchema2 }),
@@ -15281,7 +15219,7 @@ var ConnectionSummarySchema2 = external_exports.object({
15281
15219
  /** The @paigy/harness this host is running — a machine the self-update has not reached
15282
15220
  * shows its age here (`apps/desktop/src/update.ts`). */
15283
15221
  version: external_exports.string().optional(),
15284
- harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
15222
+ harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string(), models: external_exports.array(external_exports.string()).optional() })).optional(),
15285
15223
  workspaces: external_exports.array(external_exports.string()).optional(),
15286
15224
  /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
15287
15225
  * repo, and each repo directly inside one, with its `origin` remote. A session started for
@@ -15441,22 +15379,7 @@ var QueueItemSchema2 = external_exports.object({
15441
15379
  * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
15442
15380
  * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
15443
15381
  * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
15444
- latest: external_exports.object({
15445
- from: external_exports.enum(["person", "agent"]),
15446
- said: external_exports.string(),
15447
- at: external_exports.string(),
15448
- entryId: external_exports.string(),
15449
- /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
15450
- * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
15451
- * none was. The Work row reads it before `said`. */
15452
- summary: external_exports.string().optional(),
15453
- /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
15454
- * of each unit"): each at most 50 characters, in order; absent when it has none. */
15455
- titles: external_exports.array(external_exports.string()).optional(),
15456
- /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
15457
- * update's own line, before its units; the row caps it at 200. Absent on anything else. */
15458
- line: external_exports.string().optional()
15459
- }).nullable().optional(),
15382
+ latest: GoalLatestSchema2.nullable().optional(),
15460
15383
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
15461
15384
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
15462
15385
  * answer to a question, a voice note filed as work. Null when the only hands on it have been its
@@ -15528,58 +15451,6 @@ var NoteSchema2 = external_exports.object({
15528
15451
  lastRideAt: external_exports.string().nullable().default(null),
15529
15452
  createdAt: external_exports.string()
15530
15453
  });
15531
- var TriageItemSchema2 = external_exports.object({
15532
- noteId: external_exports.string(),
15533
- /** The note's headline at run time. */
15534
- title: external_exports.string(),
15535
- /** WHY, in one short human line, evidence first — this is read on a phone underneath
15536
- * the note's title: "no movement in 34 days", "worked 3 notes in this repo this week".
15537
- * Never a model's reasoning transcript, never an id. */
15538
- why: external_exports.string()
15539
- });
15540
- var TriageAssignmentSchema2 = external_exports.object({
15541
- /** The agent's token id — what `dispatchNote` resolves and what a request is addressed to. */
15542
- agent: external_exports.string(),
15543
- /** Its display name at run time (the name on the hatchling's card). Denormalized for the
15544
- * same reason as `title`: the card must render from the proposal alone. */
15545
- agentName: external_exports.string(),
15546
- notes: external_exports.array(TriageItemSchema2)
15547
- });
15548
- var TriageStatusSchema2 = external_exports.enum(["open", "superseded", "dismissed"]);
15549
- var SubmitTriageSchema2 = external_exports.object({
15550
- /** Which runtime judged: "ollama" (inference never left the machine) or a harness the
15551
- * user already runs under their own credentials ("claude" / "codex" / "agy"). Recorded
15552
- * so the phone can say where the content went — an unattributed privacy claim is worth
15553
- * nothing, and #1106's promise is precisely "Paigy's servers never see this". */
15554
- provider: external_exports.string().min(1).max(60),
15555
- /** The concrete model when the provider names one (an ollama tag); null otherwise. */
15556
- model: external_exports.string().max(200).nullable().optional(),
15557
- /** How many open notes the run actually looked at — the denominator on the phone
15558
- * ("6 of 50"), and the honest answer to "did it read the whole queue?". */
15559
- reviewed: external_exports.number().int().min(0).max(1e4).default(0),
15560
- close: external_exports.array(TriageItemSchema2).max(200).default([]),
15561
- stale: external_exports.array(TriageItemSchema2).max(200).default([]),
15562
- assign: external_exports.array(TriageAssignmentSchema2).max(50).default([])
15563
- });
15564
- var TriageProposalSchema2 = SubmitTriageSchema2.extend({
15565
- id: external_exports.string(),
15566
- runAt: external_exports.string(),
15567
- status: TriageStatusSchema2,
15568
- model: external_exports.string().nullable().default(null)
15569
- });
15570
- var AcceptTriageSchema2 = external_exports.discriminatedUnion("group", [
15571
- external_exports.object({ group: external_exports.literal("close"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
15572
- external_exports.object({ group: external_exports.literal("stale"), noteIds: external_exports.array(external_exports.string()).max(200).optional() }),
15573
- external_exports.object({
15574
- group: external_exports.literal("assign"),
15575
- agent: external_exports.string().min(1),
15576
- noteIds: external_exports.array(external_exports.string()).max(200).optional()
15577
- })
15578
- ]);
15579
- var AcceptTriageResultSchema2 = external_exports.object({
15580
- accepted: external_exports.array(external_exports.string()),
15581
- failed: external_exports.array(external_exports.object({ noteId: external_exports.string(), reason: external_exports.string() }))
15582
- });
15583
15454
  var DeliveryModeSchema2 = external_exports.enum(["poll", "self_hosted"]);
15584
15455
  var RegisterDeliverySchema2 = external_exports.object({ mode: DeliveryModeSchema2 });
15585
15456
  var OAuthStartSchema2 = external_exports.object({
@@ -15836,579 +15707,29 @@ var CallRecapSchema2 = external_exports.object({
15836
15707
 
15837
15708
  // src/host.ts
15838
15709
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
15839
- import { homedir as homedir10 } from "os";
15710
+ import { execSync } from "child_process";
15711
+ import { randomBytes } from "crypto";
15712
+ import { homedir as homedir9 } from "os";
15840
15713
  import { join as join11 } from "path";
15841
15714
 
15842
- // src/triage/evidence.ts
15843
- var OPEN_STATUSES = ["open", "assigned", "in_progress"];
15844
- var DAY_MS = 864e5;
15845
- var daysBetween = (from, now) => from ? Math.floor((now - Date.parse(from)) / DAY_MS) : null;
15846
- function assigneeToken(note2) {
15847
- const ref = note2.assignee ?? "";
15848
- return ref.startsWith("agent:") ? ref.slice("agent:".length) : null;
15849
- }
15850
- var STOP = /* @__PURE__ */ new Set([
15851
- "the",
15852
- "and",
15853
- "for",
15854
- "with",
15855
- "that",
15856
- "this",
15857
- "from",
15858
- "into",
15859
- "when",
15860
- "what",
15861
- "should",
15862
- "would",
15863
- "could",
15864
- "have",
15865
- "make",
15866
- "made",
15867
- "need",
15868
- "needs",
15869
- "want",
15870
- "wants",
15871
- "please",
15872
- "also",
15873
- "then",
15874
- "than",
15875
- "them",
15876
- "they",
15877
- "there",
15878
- "here",
15879
- "about",
15880
- "some",
15881
- "more",
15882
- "most",
15883
- "just",
15884
- "like",
15885
- "want",
15886
- "will",
15887
- "does",
15888
- "done",
15889
- "your",
15890
- "yours",
15891
- "each",
15892
- "every",
15893
- "onto"
15894
- ]);
15895
- function words(text) {
15896
- return (text.toLowerCase().match(/[a-z0-9][a-z0-9_.-]{2,}/g) ?? []).filter(
15897
- (w) => w.length >= 4 && !STOP.has(w)
15898
- );
15899
- }
15900
- function repoHints(text) {
15901
- const hits = /* @__PURE__ */ new Set();
15902
- for (const m of text.matchAll(/\b([a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*)/gi)) {
15903
- const hit = m[1];
15904
- if (hit) hits.add(hit.toLowerCase());
15905
- }
15906
- for (const m of text.matchAll(/`([^`]{2,60})`/g)) {
15907
- const inner = m[1]?.trim().toLowerCase();
15908
- if (inner && inner.includes("/")) hits.add(inner.split(/\s+/)[0] ?? inner);
15909
- }
15910
- return [...hits].slice(0, 6);
15911
- }
15912
- function overlap(a, b) {
15913
- const A = new Set(words(a));
15914
- const B = new Set(words(b));
15915
- if (A.size === 0 || B.size === 0) return 0;
15916
- let shared = 0;
15917
- for (const w of A) if (B.has(w)) shared++;
15918
- return shared / (A.size + B.size - shared);
15919
- }
15920
- var SUPERSEDE_AT = 0.5;
15921
- function supersededBy(note2, all) {
15922
- const mine = `${note2.title} ${note2.intent}`;
15923
- const at = Date.parse(note2.createdAt);
15924
- let best = null;
15925
- for (const other of all) {
15926
- if (other.id === note2.id) continue;
15927
- if (Date.parse(other.createdAt) <= at) continue;
15928
- const score = overlap(mine, `${other.title} ${other.intent}`);
15929
- if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
15930
- }
15931
- return best?.note ?? null;
15932
- }
15933
- function doneEcho(note2, all) {
15934
- const mine = `${note2.title} ${note2.intent}`;
15935
- let best = null;
15936
- for (const other of all) {
15937
- if (other.id === note2.id || other.status !== "done") continue;
15938
- const score = overlap(mine, `${other.title} ${other.intent}`);
15939
- if (score >= SUPERSEDE_AT && (!best || score > best.score)) best = { note: other, score };
15940
- }
15941
- return best?.note ?? null;
15942
- }
15943
- var RECENT_DAYS = 7;
15944
- function agentSignals(notes, connections, now) {
15945
- return connections.map((c) => {
15946
- const mine = notes.filter((n) => assigneeToken(n) === c.id);
15947
- const recent = mine.filter((n) => (daysBetween(n.createdAt, now) ?? Infinity) <= RECENT_DAYS);
15948
- const repoCount = /* @__PURE__ */ new Map();
15949
- for (const n of recent) for (const r of repoHints(`${n.title} ${n.intent}`)) repoCount.set(r, (repoCount.get(r) ?? 0) + 1);
15950
- return {
15951
- token: c.id,
15952
- name: c.name,
15953
- lastSeenDays: daysBetween(c.lastSeenAt ?? null, now),
15954
- lastContactDays: daysBetween(c.lastContactAt, now),
15955
- openNotes: mine.filter((n) => n.status !== "done").length,
15956
- recentNotes: recent.length,
15957
- repos: [...repoCount.entries()].sort((a, b) => b[1] - a[1]).map(([r]) => r).slice(0, 5),
15958
- recentTitles: recent.slice(0, 5).map((n) => n.title)
15959
- };
15960
- });
15961
- }
15962
- function noteSignals(notes, connections, now) {
15963
- const nameOf = new Map(connections.map((c) => [c.id, c.name]));
15964
- const open = notes.filter((n) => OPEN_STATUSES.includes(n.status));
15965
- return open.map((n) => {
15966
- const superseded = supersededBy(n, notes);
15967
- const echo = doneEcho(n, notes);
15968
- const token = assigneeToken(n);
15969
- return {
15970
- id: n.id,
15971
- title: n.title,
15972
- intent: n.intent,
15973
- status: n.status,
15974
- ageDays: daysBetween(n.createdAt, now) ?? 0,
15975
- idleDays: daysBetween(n.lastRideAt ?? n.createdAt, now) ?? 0,
15976
- rides: n.rides,
15977
- assignee: token,
15978
- assigneeName: token ? nameOf.get(token) ?? null : null,
15979
- repoHints: repoHints(`${n.title} ${n.intent}`),
15980
- supersededBy: superseded ? { id: superseded.id, title: superseded.title } : null,
15981
- doneEcho: echo ? { id: echo.id, title: echo.title } : null
15982
- };
15983
- });
15984
- }
15985
- function dossier(notes, connections, now) {
15986
- return {
15987
- notes: noteSignals(notes, connections, now),
15988
- agents: agentSignals(notes, connections, now),
15989
- today: new Date(now).toISOString().slice(0, 10)
15990
- };
15991
- }
15992
- function affinityWhy(note2, agent2) {
15993
- const shared = note2.repoHints.filter((r) => agent2.repos.includes(r));
15994
- if (shared.length && agent2.recentNotes)
15995
- return `worked ${agent2.recentNotes} note${agent2.recentNotes === 1 ? "" : "s"} in ${shared[0]} this week`;
15996
- if (agent2.recentNotes >= 2) return `took ${agent2.recentNotes} notes this week`;
15997
- if (agent2.lastSeenDays !== null && agent2.lastSeenDays <= 1) return "active on this machine today";
15998
- if (agent2.lastContactDays !== null && agent2.lastContactDays <= RECENT_DAYS)
15999
- return `last spoke ${agent2.lastContactDays === 0 ? "today" : `${agent2.lastContactDays}d ago`}`;
16000
- return null;
16001
- }
16002
- function staleWhy(note2) {
16003
- if (note2.doneEcho) return `a later note says this shipped: "${note2.doneEcho.title}"`;
16004
- if (note2.supersededBy) return `superseded by "${note2.supersededBy.title}"`;
16005
- if (note2.rides >= 3) return `carried on ${note2.rides} calls and still open`;
16006
- if (note2.idleDays >= 30) return `no movement in ${note2.idleDays} days`;
16007
- if (note2.idleDays >= 14 && note2.status === "open") return `unassigned for ${note2.idleDays} days`;
16008
- return null;
16009
- }
16010
-
16011
- // src/triage/judge.ts
16012
- import { spawnSync as spawnSync2 } from "child_process";
16013
- import { homedir as homedir4 } from "os";
16014
- import { delimiter as delimiter2 } from "path";
16015
- var PRIVACY = {
16016
- ollama: "never leaves this machine",
16017
- claude: "this machine + your own Claude subscription; Paigy's servers never see it",
16018
- codex: "this machine + your own Codex subscription; Paigy's servers never see it",
16019
- agy: "this machine + your own Antigravity account; Paigy's servers never see it"
16020
- };
16021
- var OLLAMA_HOST = process.env["OLLAMA_HOST"] ?? "http://localhost:11434";
16022
- var PREFERRED = ["llama3.2", "qwen2.5", "phi", "gemma", "mistral", "llama3.1", "llama3"];
16023
- function pickOllamaModel(tags) {
16024
- if (tags.length === 0) return null;
16025
- const score = (tag) => {
16026
- const i = PREFERRED.findIndex((p) => tag.toLowerCase().startsWith(p));
16027
- return i === -1 ? PREFERRED.length : i;
16028
- };
16029
- return [...tags].sort((a, b) => score(a) - score(b) || a.length - b.length)[0] ?? null;
16030
- }
16031
- function parseOllamaTags(body) {
16032
- const models = body?.models;
16033
- if (!Array.isArray(models)) return [];
16034
- return models.map((m) => m?.name ?? m?.model).filter((n) => typeof n === "string" && n.length > 0);
16035
- }
16036
- async function fetchTags(host2) {
16037
- try {
16038
- const res = await fetch(`${host2}/api/tags`, { signal: AbortSignal.timeout(2e3) });
16039
- if (!res.ok) return null;
16040
- return parseOllamaTags(await res.json());
16041
- } catch {
16042
- return null;
16043
- }
16044
- }
16045
- async function available(deps = {}) {
16046
- const tags = await (deps.tags ?? fetchTags)(OLLAMA_HOST);
16047
- const model = tags ? pickOllamaModel(tags) : null;
16048
- const ollama = tags === null ? { provider: "ollama", ready: false, model: null, hint: `no ollama at ${OLLAMA_HOST} \u2014 install it: https://ollama.com/download` } : model === null ? { provider: "ollama", ready: false, model: null, hint: "ollama is running but has no models \u2014 pull one: ollama pull llama3.2:3b" } : { provider: "ollama", ready: true, model };
16049
- const detected = (deps.detect ?? detectAll)();
16050
- const harnesses = CATALOG.map((entry) => {
16051
- const state = detected.find((d) => d.name === entry.name);
16052
- const usable = state?.status === "ready" || state?.status === "adapter-missing";
16053
- return usable ? { provider: entry.name, ready: true, model: null } : { provider: entry.name, ready: false, model: null, hint: state?.hint ?? `${entry.label} is not installed.` };
16054
- });
16055
- return [ollama, ...harnesses];
16056
- }
16057
- function refusal(options, asked) {
16058
- const lines2 = [
16059
- asked ? `Can't judge with --via ${asked}: ${options.find((o) => o.provider === asked)?.hint ?? "it isn't available on this machine"}` : "No local judge available \u2014 triage refuses rather than sending your queue to a cloud model.",
16060
- "",
16061
- "Triage runs the model where your queue already lives (#1106). Your options:"
16062
- ];
16063
- for (const o of options) {
16064
- lines2.push(` ${o.ready ? "\u2713" : "\u2717"} ${o.provider.padEnd(7)} ${PRIVACY[o.provider]}`);
16065
- if (!o.ready && o.hint) lines2.push(` ${o.hint}`);
16066
- }
16067
- lines2.push("", "Then re-run: paigy-harness triage [--via ollama|claude|codex|agy]");
16068
- return lines2.join("\n");
16069
- }
16070
- var RUNNERS = {
16071
- // Ollama speaks HTTP, not a CLI — handled separately in `judgeFor`.
16072
- ollama: null,
16073
- // `claude -p` is print mode: one prompt, one answer, no session. Verified 2026-08-28.
16074
- claude: { cli: "claude", argv: () => ["-p"] },
16075
- // `codex exec` is Codex's documented non-interactive run; it reads the prompt from stdin
16076
- // when none is given as an argument. It prints its own preamble around the answer, which
16077
- // is exactly why the parser hunts for the JSON rather than trusting the whole of stdout.
16078
- codex: { cli: "codex", argv: () => ["exec", "--skip-git-repo-check", "-"] },
16079
- // `agy --print` (alias -p) is Antigravity's print mode, text output by default.
16080
- agy: { cli: "agy", argv: () => ["-p"] }
16081
- };
16082
- var RUN_TIMEOUT_MS = 5 * 6e4;
16083
- async function judgeFor(asked, deps = {}) {
16084
- const options = await available(deps);
16085
- const chosen = asked ? options.find((o) => o.provider === asked) : options.find((o) => o.ready);
16086
- if (!chosen?.ready) return { error: refusal(options, asked) };
16087
- if (chosen.provider === "ollama") {
16088
- const model = chosen.model;
16089
- return {
16090
- provider: "ollama",
16091
- model,
16092
- run: async (prompt) => {
16093
- const res = await fetch(`${OLLAMA_HOST}/api/generate`, {
16094
- method: "POST",
16095
- headers: { "content-type": "application/json" },
16096
- // Non-streaming: we want the whole answer once, and the parser is the same
16097
- // either way. `temperature: 0` because this is a classification, not prose.
16098
- body: JSON.stringify({ model, prompt, stream: false, options: { temperature: 0 } }),
16099
- signal: AbortSignal.timeout(RUN_TIMEOUT_MS)
16100
- });
16101
- if (!res.ok) throw new Error(`ollama ${res.status}: ${await res.text()}`);
16102
- return (await res.json()).response ?? "";
16103
- }
16104
- };
16105
- }
16106
- const runner = RUNNERS[chosen.provider];
16107
- if (!runner) return { error: refusal(options, asked) };
16108
- const bin = which(runner.cli);
16109
- if (!bin) return { error: refusal(options, asked) };
16110
- return {
16111
- provider: chosen.provider,
16112
- model: null,
16113
- run: async (prompt) => {
16114
- const out = spawnSync2(bin, runner.argv(null), {
16115
- input: prompt,
16116
- encoding: "utf8",
16117
- cwd: homedir4(),
16118
- timeout: RUN_TIMEOUT_MS,
16119
- maxBuffer: 32 * 1024 * 1024,
16120
- env: { ...process.env, PATH: [process.env["PATH"], ...probeDirs()].filter(Boolean).join(delimiter2) }
16121
- });
16122
- if (out.error) throw new Error(`${runner.cli} failed to run: ${out.error.message}`);
16123
- if (out.status !== 0 && !out.stdout?.trim())
16124
- throw new Error(`${runner.cli} exited ${out.status ?? "?"}: ${(out.stderr ?? "").trim().slice(0, 500)}`);
16125
- return out.stdout ?? "";
16126
- }
16127
- };
16128
- }
16129
-
16130
- // src/triage/verdict.ts
16131
- function extractJson(raw) {
16132
- const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
16133
- const body = fenced ?? raw;
16134
- const start = body.indexOf("[");
16135
- const end = body.lastIndexOf("]");
16136
- if (start !== -1 && end > start) return body.slice(start, end + 1);
16137
- const objStart = body.indexOf("{");
16138
- const objEnd = body.lastIndexOf("}");
16139
- if (objStart !== -1 && objEnd > objStart) return body.slice(objStart, objEnd + 1);
16140
- return null;
16141
- }
16142
- var VERDICTS = /* @__PURE__ */ new Set(["keep", "stale", "close", "assign"]);
16143
- function parseVerdicts(raw, knownIds, knownAgents) {
16144
- const json3 = extractJson(raw);
16145
- if (!json3) return { ok: false, error: "no JSON found in the model's answer", raw };
16146
- let parsed;
16147
- try {
16148
- parsed = JSON.parse(json3);
16149
- } catch (e) {
16150
- return { ok: false, error: `JSON.parse failed: ${e.message}`, raw };
16151
- }
16152
- const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.verdicts) ? parsed.verdicts : typeof parsed?.id === "string" ? [parsed] : null;
16153
- if (!rows) return { ok: false, error: "the JSON was not an array of verdicts", raw };
16154
- const verdicts = [];
16155
- const dropped = [];
16156
- const seen = /* @__PURE__ */ new Set();
16157
- for (const row of rows) {
16158
- const r = row;
16159
- const id3 = typeof r?.id === "string" ? r.id : "";
16160
- if (!id3) {
16161
- dropped.push({ row, reason: "no id" });
16162
- continue;
16163
- }
16164
- if (!knownIds.has(id3)) {
16165
- dropped.push({ row, reason: "id is not a note in this queue" });
16166
- continue;
16167
- }
16168
- if (seen.has(id3)) {
16169
- dropped.push({ row, reason: "duplicate verdict for this note" });
16170
- continue;
16171
- }
16172
- const verdict = typeof r?.verdict === "string" ? r.verdict.toLowerCase().trim() : "";
16173
- if (!VERDICTS.has(verdict)) {
16174
- dropped.push({ row, reason: `unknown verdict ${JSON.stringify(r?.verdict)}` });
16175
- continue;
16176
- }
16177
- const agent2 = typeof r?.agent === "string" ? r.agent.trim() : null;
16178
- if (verdict === "assign" && (!agent2 || !knownAgents.has(agent2))) {
16179
- dropped.push({ row, reason: agent2 ? `no such agent: ${agent2}` : "assign with no agent" });
16180
- continue;
16181
- }
16182
- verdicts.push({
16183
- id: id3,
16184
- verdict,
16185
- agent: verdict === "assign" ? agent2 : null,
16186
- why: typeof r?.why === "string" ? r.why.trim().slice(0, 200) : null
16187
- });
16188
- seen.add(id3);
16189
- }
16190
- return { ok: true, verdicts, dropped };
16191
- }
16192
- function fillMissing(verdicts, notes) {
16193
- const said = new Set(verdicts.map((v) => v.id));
16194
- return [...verdicts, ...notes.filter((n) => !said.has(n.id)).map((n) => ({ id: n.id, verdict: "keep", why: null }))];
16195
- }
16196
- function buildPrompt(d) {
16197
- return [
16198
- "You are triaging a personal task queue. Today is " + d.today + ".",
16199
- "",
16200
- "For EVERY note below, decide one of:",
16201
- ' "keep" \u2014 it is still live work. THE DEFAULT.',
16202
- ' "stale" \u2014 nothing has moved on it for a long time and nothing says it is finished.',
16203
- ' "close" \u2014 there is evidence it is already done or no longer wanted.',
16204
- ' "assign" \u2014 it should be handed to one of the agents listed, and you name which.',
16205
- "",
16206
- "The counts and dates below are already computed. Use them; do not recompute them.",
16207
- "",
16208
- "AGENTS (assign only to one of these tokens):",
16209
- JSON.stringify(d.agents, null, 1),
16210
- "",
16211
- "NOTES:",
16212
- JSON.stringify(d.notes, null, 1),
16213
- "",
16214
- "Answer with a JSON array and NOTHING else. One object per note:",
16215
- '[{"id":"<note id>","verdict":"keep|stale|close|assign","agent":"<agent token, only for assign>","why":"<max 12 words>"}]',
16216
- "",
16217
- 'The "why" is read on a phone under the note title. Evidence first, no ids, no preamble.',
16218
- 'Good: "no movement in 34 days" / "worked 3 notes in this repo this week" / "a later note says this shipped".',
16219
- "",
16220
- `When you are not sure, answer "keep". A wrong "keep" costs nothing; a wrong "close" loses the user's work.`
16221
- ].join("\n");
16222
- }
16223
-
16224
- // src/triage/triage.ts
16225
- function toProposal(verdicts, d, meta) {
16226
- const byId = new Map(d.notes.map((n) => [n.id, n]));
16227
- const agentById = new Map(d.agents.map((a) => [a.token, a]));
16228
- const close = [];
16229
- const stale = [];
16230
- const assignments = /* @__PURE__ */ new Map();
16231
- for (const v of verdicts) {
16232
- const note2 = byId.get(v.id);
16233
- if (!note2) continue;
16234
- const item = (why) => ({ noteId: note2.id, title: note2.title, why });
16235
- if (v.verdict === "close") {
16236
- close.push(item(staleWhy(note2) ?? v.why ?? "looks finished"));
16237
- continue;
16238
- }
16239
- if (v.verdict === "stale") {
16240
- stale.push(item(staleWhy(note2) ?? v.why ?? `no movement in ${note2.idleDays} days`));
16241
- continue;
16242
- }
16243
- if (v.verdict !== "assign" || !v.agent) continue;
16244
- const agent2 = agentById.get(v.agent);
16245
- if (!agent2) continue;
16246
- const group = assignments.get(agent2.token) ?? { agent: agent2.token, agentName: agent2.name, notes: [] };
16247
- group.notes.push(item(affinityWhy(note2, agent2) ?? v.why ?? "best match for this work"));
16248
- assignments.set(agent2.token, group);
16249
- }
16250
- return {
16251
- provider: meta.provider,
16252
- model: meta.model,
16253
- reviewed: d.notes.length,
16254
- close,
16255
- stale,
16256
- assign: [...assignments.values()]
16257
- };
16258
- }
15715
+ // src/ago.ts
16259
15716
  var plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
16260
- function suggested(run) {
16261
- return run.close.length + run.stale.length + run.assign.reduce((n, a) => n + a.notes.length, 0);
16262
- }
16263
- function renderTable(run) {
16264
- const lines2 = [];
16265
- const width = Math.max(
16266
- 24,
16267
- ...[...run.close, ...run.stale, ...run.assign.flatMap((a) => a.notes)].map((n) => Math.min(n.title.length, 48))
16268
- );
16269
- const row = (n) => ` ${n.title.slice(0, width).padEnd(width)} ${n.why}`;
16270
- if (run.close.length) {
16271
- lines2.push(`Paigy thinks you should CLOSE ${plural(run.close.length, "note")}:`);
16272
- lines2.push(...run.close.map(row), "");
16273
- }
16274
- if (run.stale.length) {
16275
- lines2.push(`Paigy thinks ${plural(run.stale.length, "note")} ${run.stale.length === 1 ? "has" : "have"} gone STALE:`);
16276
- lines2.push(...run.stale.map(row), "");
16277
- }
16278
- for (const group of run.assign) {
16279
- lines2.push(`Paigy thinks you should ASSIGN ${plural(group.notes.length, "note")} to ${group.agentName}:`);
16280
- lines2.push(...group.notes.map(row), "");
16281
- }
16282
- const acted = suggested(run);
16283
- lines2.push(`Reviewed ${plural(run.reviewed, "open note")} \xB7 ${plural(acted, "suggestion")} \xB7 ${run.reviewed - acted} left alone`);
16284
- return lines2.join("\n");
16285
- }
16286
- async function confirm(question) {
16287
- if (!process.stdin.isTTY) return false;
16288
- const readline = await import("readline/promises");
16289
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
16290
- try {
16291
- return /^(y|yes)$/i.test((await rl.question(`${question} [y/N] `)).trim());
16292
- } finally {
16293
- rl.close();
16294
- }
16295
- }
16296
- async function runTriage(opts, deps = {}) {
16297
- const { log: log2 } = opts;
16298
- const now = (deps.now ?? Date.now)();
16299
- const judge = await (deps.judge ?? judgeFor)(opts.via);
16300
- if ("error" in judge) {
16301
- log2(judge.error);
16302
- return 1;
16303
- }
16304
- const [{ notes }, { items }] = await Promise.all([
16305
- (deps.notes ?? listNotes)(),
16306
- (deps.connections ?? listConnections)()
16307
- ]);
16308
- const d = dossier(notes, items, now);
16309
- if (d.notes.length === 0) {
16310
- log2("Nothing open in the queue \u2014 nothing to triage.");
16311
- return 0;
16312
- }
16313
- if (!opts.json)
16314
- log2(`Reading ${plural(d.notes.length, "open note")} with ${judge.provider}${judge.model ? ` (${judge.model})` : ""} \u2014 ${PRIVACY[judge.provider]}\u2026`);
16315
- const raw = await judge.run(buildPrompt(d));
16316
- const parsed = parseVerdicts(
16317
- raw,
16318
- new Set(d.notes.map((n) => n.id)),
16319
- new Set(d.agents.map((a) => a.token))
16320
- );
16321
- if (!parsed.ok) {
16322
- log2(`The model's answer couldn't be read: ${parsed.error}`);
16323
- log2("--- raw output ---");
16324
- log2(parsed.raw.slice(0, 4e3));
16325
- log2("--- end ---");
16326
- log2(`Try a different judge: paigy-harness triage --via ${judge.provider === "ollama" ? "claude" : "ollama"}`);
16327
- return 1;
16328
- }
16329
- if (parsed.dropped.length && !opts.json)
16330
- log2(`(dropped ${plural(parsed.dropped.length, "unusable row")}: ${[...new Set(parsed.dropped.map((x) => x.reason))].join("; ")})`);
16331
- const run = toProposal(fillMissing(parsed.verdicts, d.notes), d, { provider: judge.provider, model: judge.model });
16332
- if (opts.json) {
16333
- log2(JSON.stringify(run, null, 2));
16334
- return 0;
16335
- }
16336
- log2("");
16337
- log2(renderTable(run));
16338
- log2("");
16339
- const proposal = await (deps.submit ?? submitTriage)(run);
16340
- const acted = suggested(run);
16341
- log2(acted === 0 ? "Nothing to suggest \u2014 sent, so your phone shows the queue as clean." : `Sent to your phone \u2014 accept it there (${plural(acted, "suggestion")}).`);
16342
- if (!opts.apply) return 0;
16343
- const accept = deps.accept ?? ((id3, body) => acceptTriage(id3, body));
16344
- const groups = [
16345
- { body: { group: "close" }, label: `close ${plural(run.close.length, "note")}`, count: run.close.length, preapproved: opts.yes.close },
16346
- { body: { group: "stale" }, label: `close ${plural(run.stale.length, "stale note")}`, count: run.stale.length, preapproved: opts.yes.stale },
16347
- ...run.assign.map((a) => ({
16348
- body: { group: "assign", agent: a.agent },
16349
- label: `assign ${plural(a.notes.length, "note")} to ${a.agentName}`,
16350
- count: a.notes.length,
16351
- preapproved: opts.yes.assign
16352
- }))
16353
- ];
16354
- for (const g of groups) {
16355
- if (g.count === 0) continue;
16356
- if (!g.preapproved && !await confirm(`${g.label}?`)) {
16357
- log2(` skipped: ${g.label}`);
16358
- continue;
16359
- }
16360
- const result = await accept(proposal.id, g.body);
16361
- log2(` \u2713 ${g.label} \u2014 ${result.accepted.length} done${result.failed.length ? `, ${result.failed.length} failed (${result.failed.map((f) => f.reason).join(", ")})` : ""}`);
16362
- }
16363
- return 0;
16364
- }
16365
-
16366
- // src/broker.ts
16367
- var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
16368
15717
  function ago(iso, now) {
16369
15718
  const ms = now - Date.parse(iso);
16370
15719
  if (!Number.isFinite(ms) || ms < 6e4) return "just now";
16371
15720
  const minutes = Math.floor(ms / 6e4);
16372
- if (minutes < 60) return `${plural2(minutes, "minute")} ago`;
15721
+ if (minutes < 60) return `${plural(minutes, "minute")} ago`;
16373
15722
  const hours = Math.floor(minutes / 60);
16374
- if (hours < 24) return `${plural2(hours, "hour")} ago`;
16375
- return `${plural2(Math.floor(hours / 24), "day")} ago`;
16376
- }
16377
- function standing(queue, proposal, now) {
16378
- const head2 = queue.length === 0 ? "Nothing open \u2014 the queue is clear" : `Holding ${plural2(queue.length, "open note")}`;
16379
- if (!proposal) return `${head2} \xB7 no sweep waiting`;
16380
- const n = suggested(proposal);
16381
- return n === 0 ? `${head2} \xB7 swept ${ago(proposal.runAt, now)}, nothing to suggest` : `${head2} \xB7 ${plural2(n, "suggestion")} waiting on you, swept ${ago(proposal.runAt, now)}`;
16382
- }
16383
- async function brokerView(deps) {
16384
- const now = (deps.now ?? Date.now)();
16385
- try {
16386
- const [{ notes }, { items }, { proposal }] = await Promise.all([deps.notes(), deps.connections(), deps.triage()]);
16387
- const queue = noteSignals(notes, items, now).map((n) => ({ ...n, why: staleWhy(n) })).sort((a, b) => b.idleDays - a.idleDays || b.ageDays - a.ageDays);
16388
- return {
16389
- standing: standing(queue, proposal, now),
16390
- queue,
16391
- proposal,
16392
- voices: Object.fromEntries(items.map((c) => [c.id, c.voice ?? null])),
16393
- error: null
16394
- };
16395
- } catch (e) {
16396
- return {
16397
- standing: "Couldn't read the queue just now.",
16398
- queue: [],
16399
- proposal: null,
16400
- voices: {},
16401
- error: e instanceof Error ? e.message : String(e)
16402
- };
16403
- }
15723
+ if (hours < 24) return `${plural(hours, "hour")} ago`;
15724
+ return `${plural(Math.floor(hours / 24), "day")} ago`;
16404
15725
  }
16405
15726
 
16406
15727
  // src/workspaces.ts
16407
15728
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
16408
15729
  import { dirname as dirname2, join as join5, resolve } from "path";
16409
- import { homedir as homedir5 } from "os";
15730
+ import { homedir as homedir4 } from "os";
16410
15731
  function workspacesFile() {
16411
- return join5(homedir5(), ".paigy", "workspaces.json");
15732
+ return join5(homedir4(), ".paigy", "workspaces.json");
16412
15733
  }
16413
15734
  function listWorkspaces(deps) {
16414
15735
  try {
@@ -16419,7 +15740,7 @@ function listWorkspaces(deps) {
16419
15740
  }
16420
15741
  }
16421
15742
  function addWorkspace(dir, deps) {
16422
- const next = resolve(dir.replace(/^~(?=$|\/)/, homedir5()));
15743
+ const next = resolve(dir.replace(/^~(?=$|\/)/, homedir4()));
16423
15744
  const all = listWorkspaces(deps);
16424
15745
  if (!all.includes(next)) {
16425
15746
  all.push(next);
@@ -16435,12 +15756,12 @@ function removeWorkspace(dir, deps) {
16435
15756
  return all;
16436
15757
  }
16437
15758
  function allowed(cwd, deps) {
16438
- const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir5()));
15759
+ const target = resolve(cwd.replace(/^~(?=$|\/)/, homedir4()));
16439
15760
  return listWorkspaces(deps).some((w) => target === w || target.startsWith(`${w}/`));
16440
15761
  }
16441
15762
  function resolveWakeDir(pinned, deps) {
16442
15763
  if (pinned && allowed(pinned, deps)) {
16443
- const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir5()));
15764
+ const dir = resolve(pinned.replace(/^~(?=$|\/)/, homedir4()));
16444
15765
  if ((deps.exists ?? existsSync3)(dir)) return dir;
16445
15766
  }
16446
15767
  return listWorkspaces(deps)[0];
@@ -16480,7 +15801,7 @@ function listRepos(deps) {
16480
15801
  // src/holder.ts
16481
15802
  import { execFileSync as execFileSync2 } from "child_process";
16482
15803
  import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
16483
- import { homedir as homedir6 } from "os";
15804
+ import { homedir as homedir5 } from "os";
16484
15805
  import { join as join6 } from "path";
16485
15806
  function isAlive(pid) {
16486
15807
  try {
@@ -16519,7 +15840,7 @@ function proven(pid, procStart, deps) {
16519
15840
  });
16520
15841
  }
16521
15842
  function localHolderPids(deps = {}) {
16522
- const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
15843
+ const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
16523
15844
  const list = deps.list ?? ((d) => readdirSync2(d));
16524
15845
  const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
16525
15846
  const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
@@ -16568,7 +15889,7 @@ function descendsFrom(pid, ancestor, parent = psParent) {
16568
15889
  return false;
16569
15890
  }
16570
15891
  function localHolders(deps = {}) {
16571
- const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir6(), ".claude"), "sessions");
15892
+ const dir = deps.dir ?? join6(process.env.CLAUDE_CONFIG_DIR?.trim() || join6(homedir5(), ".claude"), "sessions");
16572
15893
  const list = deps.list ?? ((d) => readdirSync2(d));
16573
15894
  const read3 = deps.read ?? ((f) => readFileSync4(f, "utf8"));
16574
15895
  const check = { alive: deps.alive ?? isAlive, started: deps.started ?? psStart };
@@ -16661,7 +15982,7 @@ async function endSession(end, deps) {
16661
15982
 
16662
15983
  // src/owner.ts
16663
15984
  import { closeSync as closeSync2, openSync as openSync2, readSync, readdirSync as readdirSync3 } from "fs";
16664
- import { homedir as homedir7 } from "os";
15985
+ import { homedir as homedir6 } from "os";
16665
15986
  import { join as join7 } from "path";
16666
15987
  var HEAD_BYTES = 64e3;
16667
15988
  function readHead(file) {
@@ -16678,7 +15999,7 @@ function ownerSession(slot, deps = {}) {
16678
15999
  const m = /^session:([0-9a-f]{8})$/.exec(slot);
16679
16000
  if (!m) return null;
16680
16001
  const prefix = m[1];
16681
- const root = deps.projectsDir ?? join7(homedir7(), ".claude", "projects");
16002
+ const root = deps.projectsDir ?? join7(homedir6(), ".claude", "projects");
16682
16003
  const list = deps.list ?? ((dir) => readdirSync3(dir));
16683
16004
  const head2 = deps.head ?? readHead;
16684
16005
  let folders;
@@ -16741,8 +16062,8 @@ import { randomUUID as randomUUID4 } from "crypto";
16741
16062
  // src/harness/session.ts
16742
16063
  import { spawn } from "child_process";
16743
16064
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
16744
- import { homedir as homedir9 } from "os";
16745
- import { resolve as resolve2, delimiter as delimiter3, join as join9 } from "path";
16065
+ import { homedir as homedir8 } from "os";
16066
+ import { resolve as resolve2, delimiter as delimiter2, join as join9 } from "path";
16746
16067
 
16747
16068
  // src/harness/acp.ts
16748
16069
  var none = { events: [], writes: [] };
@@ -17019,7 +16340,7 @@ var frame2 = (body) => JSON.stringify({ jsonrpc: "2.0", ...body });
17019
16340
  // src/paigy/inbox.ts
17020
16341
  import { mkdirSync as mkdirSync3, readdirSync as readdirSync4, renameSync, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
17021
16342
  import { join as join8 } from "path";
17022
- import { homedir as homedir8 } from "os";
16343
+ import { homedir as homedir7 } from "os";
17023
16344
  import { randomUUID as randomUUID3 } from "crypto";
17024
16345
  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"`;
17025
16346
  var missing = (error) => error.code === "ENOENT";
@@ -17062,7 +16383,7 @@ function openInbox(dir) {
17062
16383
  }
17063
16384
  };
17064
16385
  }
17065
- var INBOX_ROOT = join8(homedir8(), ".paigy", "inbox");
16386
+ var INBOX_ROOT = join8(homedir7(), ".paigy", "inbox");
17066
16387
  var alive = (pid) => {
17067
16388
  try {
17068
16389
  process.kill(pid, 0);
@@ -17109,14 +16430,15 @@ function splitLines(buffer, chunk) {
17109
16430
  return { lines: parts.filter((l) => l.trim()), rest };
17110
16431
  }
17111
16432
  function startSession(opts) {
17112
- const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir9()));
16433
+ const cwd = resolve2(opts.cwd.replace(/^~(?=$|\/)/, homedir8()));
17113
16434
  if (!existsSync4(cwd)) {
17114
16435
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
17115
16436
  }
17116
- const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
16437
+ let codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
16438
+ if (opts.model && opts.harness === "codex") codexConfig = { ...codexConfig, model: opts.model };
17117
16439
  if (codexConfig) {
17118
16440
  const key3 = "mcp_servers.paigy.env_vars";
17119
- const configPath = join9(process.env.CODEX_HOME || join9(homedir9(), ".codex"), "config.toml");
16441
+ const configPath = join9(process.env.CODEX_HOME || join9(homedir8(), ".codex"), "config.toml");
17120
16442
  const saved = existsSync4(configPath) ? readFileSync5(configPath, "utf8") : "";
17121
16443
  const registered = /\[mcp_servers\.paigy\][\s\S]*?(?=\n\[|$)/.exec(saved)?.[0];
17122
16444
  const configured = codexConfig["mcp_servers.paigy.command"] || codexConfig["mcp_servers.paigy.url"] || codexConfig.mcp_servers?.paigy?.command || codexConfig.mcp_servers?.paigy?.url;
@@ -17136,9 +16458,11 @@ function startSession(opts) {
17136
16458
  // PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
17137
16459
  env: {
17138
16460
  ...process.env,
17139
- PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter3),
16461
+ PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
17140
16462
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
17141
16463
  ...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
16464
+ ...opts.model && opts.harness === "claude" ? { CLAUDE_MODEL_CONFIG: JSON.stringify({ provider: "anthropic", model: opts.model }) } : {},
16465
+ ...opts.model && opts.harness === "agy" ? { AGY_MODEL_CONFIG: JSON.stringify({ model: opts.model }) } : {},
17142
16466
  ...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
17143
16467
  ...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
17144
16468
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
@@ -17153,19 +16477,56 @@ function startSession(opts) {
17153
16477
  stdio: ["pipe", "pipe", "pipe"]
17154
16478
  });
17155
16479
  let ended2 = false;
17156
- child.stdin?.on("error", () => {
17157
- });
16480
+ let failure;
16481
+ let hush;
16482
+ const pending = /* @__PURE__ */ new Set();
16483
+ const end = (error) => {
16484
+ ended2 = true;
16485
+ failure ??= error;
16486
+ clearTimeout(hush);
16487
+ for (const receipt of pending) receipt.reject(failure);
16488
+ pending.clear();
16489
+ };
16490
+ const broken = (error) => {
16491
+ if (ended2) return;
16492
+ end(new Error(`session_write_failed: ${error.message}`));
16493
+ opts.onEvent({ kind: "error", message: failure.message });
16494
+ child.kill();
16495
+ };
16496
+ child.stdin?.on("error", broken);
17158
16497
  const write = (frames) => {
17159
16498
  if (ended2) return;
17160
- for (const f of frames) child.stdin?.write(`${f}
17161
- `);
17162
- if (frames.length) listen();
16499
+ for (const f of frames) {
16500
+ if (ended2) break;
16501
+ if (!child.stdin || child.stdin.destroyed || !child.stdin.writable) {
16502
+ broken(new Error("stdin unavailable"));
16503
+ break;
16504
+ }
16505
+ const frame3 = JSON.parse(f);
16506
+ const receipts2 = frame3.method === "session/prompt" ? [...pending].filter((receipt) => !receipt.writing).slice(0, frame3.params.prompt.length) : [];
16507
+ for (const receipt of receipts2) receipt.writing = true;
16508
+ try {
16509
+ child.stdin.write(`${f}
16510
+ `, (error) => {
16511
+ if (error) {
16512
+ broken(error);
16513
+ return;
16514
+ }
16515
+ if (ended2) return;
16516
+ for (const receipt of receipts2) if (pending.delete(receipt)) receipt.resolve();
16517
+ });
16518
+ } catch (error) {
16519
+ broken(error instanceof Error ? error : new Error(String(error)));
16520
+ }
16521
+ }
16522
+ if (frames.length && !ended2) listen();
17163
16523
  };
17164
16524
  const apply = ({ events, writes }) => {
16525
+ const fatal = events.find((event) => event.kind === "error" && event.fatal);
16526
+ if (fatal?.kind === "error") end(new Error(fatal.message));
17165
16527
  write(writes);
17166
16528
  for (const event of events) opts.onEvent(event);
17167
16529
  };
17168
- let hush;
17169
16530
  const listen = () => {
17170
16531
  clearTimeout(hush);
17171
16532
  if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
@@ -17175,12 +16536,12 @@ function startSession(opts) {
17175
16536
  if (text) opts.onEvent({ kind: "error", message: text });
17176
16537
  });
17177
16538
  child.on("exit", (code) => {
17178
- ended2 = true;
17179
- clearTimeout(hush);
16539
+ end(new Error(`session_ended: ${code ?? "signal"}`));
17180
16540
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
17181
16541
  opts.onExit?.();
17182
16542
  });
17183
16543
  child.on("error", (e) => {
16544
+ end(new Error(`session_start_failed: ${e.message}`));
17184
16545
  opts.onEvent({ kind: "error", message: e.message });
17185
16546
  opts.onExit?.();
17186
16547
  });
@@ -17200,15 +16561,24 @@ function startSession(opts) {
17200
16561
  write(driver.open());
17201
16562
  return {
17202
16563
  send(text) {
17203
- write(driver.send(text));
16564
+ const sent = new Promise((resolve3, reject) => {
16565
+ if (ended2) {
16566
+ reject(failure ?? new Error("session_ended"));
16567
+ return;
16568
+ }
16569
+ pending.add({ resolve: resolve3, reject, writing: false });
16570
+ write(driver.send(text));
16571
+ });
16572
+ void sent.catch(() => {
16573
+ });
16574
+ return sent;
17204
16575
  },
17205
16576
  respond(id3, decision) {
17206
16577
  write(driver.respond(id3, decision));
17207
16578
  },
17208
16579
  stop() {
17209
16580
  write(driver.close());
17210
- ended2 = true;
17211
- clearTimeout(hush);
16581
+ end(new Error("session_stopped"));
17212
16582
  child.kill();
17213
16583
  }
17214
16584
  };
@@ -17368,12 +16738,13 @@ var LIMITS = episodes();
17368
16738
  // src/paigy/bridge.ts
17369
16739
  function railFor(opts) {
17370
16740
  return {
17371
- next: async () => {
17372
- const waiting = await receive(opts);
16741
+ next: async (alreadyRead) => {
16742
+ const waiting = alreadyRead ?? await receive(opts);
17373
16743
  const goalId = waiting.claimable?.goalId ?? waiting.review?.[0]?.goalId;
17374
16744
  return goalId ? getGoals([goalId], opts).then(([goal]) => goal) : { state: "idle", message: "" };
17375
16745
  },
17376
16746
  read: (goalId) => getGoals([goalId], opts).then(([goal]) => goal),
16747
+ delivery: (deliveryId) => readDelivery(deliveryId, opts),
17377
16748
  complete: async (goalId, revision) => {
17378
16749
  const done = await manageGoals({ changes: [{ kind: "state", goalId, state: "completed", ...revision !== void 0 ? { revision } : {} }] }, opts);
17379
16750
  const result = done.results[0];
@@ -17554,9 +16925,9 @@ function beat() {
17554
16925
  }
17555
16926
  var BEAT_REPORT_MS = 10 * 6e4;
17556
16927
  var ended = (state) => state === "done" || state === "cancelled";
17557
- function unheard(brief, state, since = null, after = -1, reviewSince) {
16928
+ function unheard(brief, state, since = null, after = -1, reviewSince, gathered) {
17558
16929
  const seen = state.seen ??= /* @__PURE__ */ new Set();
17559
- const words2 = [];
16930
+ const words = [];
17560
16931
  const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
17561
16932
  for (const [i, e] of (brief.entries ?? []).entries()) {
17562
16933
  if (seen.has(e.entryId) || !e.authorParticipant.startsWith("human:")) continue;
@@ -17569,18 +16940,21 @@ function unheard(brief, state, since = null, after = -1, reviewSince) {
17569
16940
  continue;
17570
16941
  }
17571
16942
  if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
17572
- seen.add(e.entryId);
17573
16943
  if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
16944
+ seen.add(e.entryId);
17574
16945
  const text = entryWords(e).trim();
17575
- if (text) words2.push(text);
16946
+ if (text) {
16947
+ words.push(text);
16948
+ gathered?.add(e.entryId);
16949
+ }
17576
16950
  }
17577
16951
  if (pendingAt !== null) for (const answer of brief.answers ?? []) {
17578
16952
  const key3 = `answer:${answer.decisionNeedId}`;
17579
16953
  if (seen.has(key3) || Date.parse(answer.acceptedAt) < pendingAt) continue;
17580
16954
  seen.add(key3);
17581
- words2.push(`Answer to question ${answer.decisionNeedId}: ${JSON.stringify(answer.result)}`);
16955
+ words.push(`Answer to question ${answer.decisionNeedId}: ${JSON.stringify(answer.result)}`);
17582
16956
  }
17583
- return words2;
16957
+ return words;
17584
16958
  }
17585
16959
  function sinceTheWork(brief) {
17586
16960
  const entries = brief.entries ?? [];
@@ -17613,12 +16987,28 @@ function named(goalId, title, brief) {
17613
16987
  const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
17614
16988
  return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
17615
16989
  }
17616
- async function gather(state, rail, held) {
17617
- const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
16990
+ async function gather(state, rail, held, alreadyRead, log2) {
16991
+ const looks = /* @__PURE__ */ new Map();
16992
+ if (held && state.goal) looks.set(state.goal.id, { via: "held", brief: held });
17618
16993
  const look2 = (goalId, via, title, since) => {
17619
16994
  if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
17620
16995
  };
17621
- const waiting = await rail.waiting().catch(() => null);
16996
+ const waiting = alreadyRead ?? await rail.waiting().catch(() => null);
16997
+ const direct = [];
16998
+ const staged = new Set(state.seen);
16999
+ const deliveries = /* @__PURE__ */ new Set();
17000
+ for (const d of waiting?.deliveries ?? []) {
17001
+ if (d.goalIds.length || deliveries.has(d.deliveryId) || state.acked?.get(`delivery:${d.deliveryId}`) === "handled") continue;
17002
+ deliveries.add(d.deliveryId);
17003
+ try {
17004
+ const full = await rail.delivery(d.deliveryId);
17005
+ const entries = full.entries.map((e) => ({ id: e.entryId, words: entryWords(e) }));
17006
+ if (!entries.length || entries.some((e) => !e.words.trim())) continue;
17007
+ direct.push({ eventId: d.deliveryId, entries, entryIds: entries.map((e) => e.id), words: [] });
17008
+ } catch (error) {
17009
+ log2?.(`\u26A0 could not read instruction ${d.deliveryId.slice(0, 8)}: ${error.message}`);
17010
+ }
17011
+ }
17622
17012
  const listed = /* @__PURE__ */ new Map();
17623
17013
  const list = (goalId, eventId) => {
17624
17014
  const ids = listed.get(goalId) ?? [];
@@ -17626,19 +17016,21 @@ async function gather(state, rail, held) {
17626
17016
  listed.set(goalId, ids);
17627
17017
  };
17628
17018
  const announced = state.announced ??= /* @__PURE__ */ new Set();
17629
- if (waiting) {
17630
- for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
17631
- }
17632
- for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
17633
- for (const g of waiting?.review ?? []) {
17634
- list(g.goalId, g.eventId);
17635
- look2(g.goalId, "waiting work", g.title);
17636
- }
17637
- for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
17638
- list(id3, d.deliveryId);
17639
- look2(id3, "waiting work");
17019
+ if (held) {
17020
+ if (waiting) {
17021
+ for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
17022
+ }
17023
+ for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
17024
+ for (const g of waiting?.review ?? []) {
17025
+ list(g.goalId, g.eventId);
17026
+ look2(g.goalId, "waiting work", g.title);
17027
+ }
17028
+ for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
17029
+ list(id3, d.deliveryId);
17030
+ look2(id3, "waiting work");
17031
+ }
17640
17032
  }
17641
- const moved = movedBelow(held, state);
17033
+ const moved = held ? movedBelow(held, state) : [];
17642
17034
  for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
17643
17035
  const heard = [];
17644
17036
  const read3 = /* @__PURE__ */ new Set();
@@ -17655,10 +17047,15 @@ async function gather(state, rail, held) {
17655
17047
  read3.add(goalId);
17656
17048
  const fresh = !brief.reviewSince && (found.via === "waiting work" || found.via === "assigned") ? sinceTheWork(brief) : { after: -1 };
17657
17049
  if (found.via === "assigned") announced.add(goalId);
17658
- heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince), ...fresh.note ? { note: fresh.note } : {} });
17050
+ heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince, staged), ...fresh.note ? { note: fresh.note } : {} });
17659
17051
  }
17052
+ for (const d of direct) d.words = d.entries.filter((e) => {
17053
+ if (staged.has(e.id)) return false;
17054
+ staged.add(e.id);
17055
+ return true;
17056
+ }).map((e) => e.words);
17660
17057
  for (const { goal, since } of moved) if (read3.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
17661
- return { heard, listed };
17058
+ return { heard, listed, direct };
17662
17059
  }
17663
17060
  var GIVEN = "This Goal was given to you, and nobody has started it. Carry on with what you're doing unless it changes that; your first write to it (a contact update naming it) starts it when you take it up.";
17664
17061
  var says = (h) => h.words.length > 0 || h.via === "assigned";
@@ -17676,12 +17073,21 @@ var block = (h) => [
17676
17073
  ].join("\n\n");
17677
17074
  var short = (ids) => ids.map((id3) => id3.slice(0, 8)).join(", ");
17678
17075
  var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
17679
- async function hear(session, state, rail, held, opts) {
17076
+ async function hear(session, state, rail, held, opts, waiting) {
17680
17077
  const log2 = opts?.log;
17681
- const { heard, listed } = await gather(state, rail, held);
17078
+ const { heard, listed, direct } = await gather(state, rail, held, waiting, log2);
17682
17079
  const said = heard.filter(says);
17683
- if (said.length) {
17684
- send2(session, state, said.map(block).join("\n\n"));
17080
+ const words = [...direct.flatMap((d) => d.words), ...said.map(block)];
17081
+ if (words.length) {
17082
+ const text = words.join("\n\n");
17083
+ try {
17084
+ await send2(session, state, text);
17085
+ } catch (error) {
17086
+ (state.noted ??= []).push({ name: "", goals: said.map((h) => h.goalId), words: text, eventIds: direct.map((d) => d.eventId), at: Date.now(), cause: "read" });
17087
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
17088
+ throw error;
17089
+ }
17090
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
17685
17091
  const worded = said.filter((h) => h.words.length);
17686
17092
  if (worded.length) log2?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
17687
17093
  const goals = given(said);
@@ -17690,18 +17096,21 @@ async function hear(session, state, rail, held, opts) {
17690
17096
  if (opts?.tally) opts.tally.announced += goals.length;
17691
17097
  }
17692
17098
  }
17099
+ await acknowledgeDirect(direct.map((d) => d.eventId), state, rail, log2);
17693
17100
  for (const h of heard) {
17694
17101
  const events = listed.get(h.goalId);
17695
17102
  if (events) await acknowledge2(h.goalId, h.brief, events, state, rail, log2);
17696
17103
  }
17104
+ return direct.length > 0 || said.length > 0;
17697
17105
  }
17698
17106
  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.";
17699
17107
  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.";
17700
- async function note(state, rail, held, inbox, how) {
17701
- const { heard } = await gather(state, rail, held);
17108
+ async function note(state, rail, held, inbox, how, waiting) {
17109
+ const { heard, direct } = await gather(state, rail, held, waiting, how.log);
17702
17110
  const said = heard.filter(says);
17703
- if (!said.length) return 0;
17704
- const words2 = said.map(block).join("\n\n");
17111
+ const parts = [...direct.flatMap((d) => d.words), ...said.map(block)];
17112
+ if (!parts.length) return 0;
17113
+ const words = parts.join("\n\n");
17705
17114
  const goals = said.map((h) => h.goalId);
17706
17115
  const assigned = given(said);
17707
17116
  if (how.tally) how.tally.announced += assigned.length;
@@ -17709,13 +17118,14 @@ async function note(state, rail, held, inbox, how) {
17709
17118
  try {
17710
17119
  name = inbox.put(`${NOTED}
17711
17120
 
17712
- ${words2}`);
17121
+ ${words}`);
17713
17122
  how.log?.(`\u25B9 noted mid-turn: ${short(goals)}`);
17714
17123
  if (assigned.length) how.log?.(`\u25B8 announced assigned ${short(assigned)} (via hook)`);
17715
17124
  } catch (error) {
17716
17125
  how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
17717
17126
  }
17718
- (state.noted ??= []).push({ name, goals, words: words2, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
17127
+ (state.noted ??= []).push({ name, goals, words, eventIds: direct.map((d) => d.eventId), at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
17128
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
17719
17129
  return 1;
17720
17130
  }
17721
17131
  function receipts(state, inbox, log2) {
@@ -17730,15 +17140,33 @@ function receipts(state, inbox, log2) {
17730
17140
  }
17731
17141
  return seen;
17732
17142
  }
17733
- function settle(session, state, inbox, log2) {
17143
+ async function settle(session, state, rail, inbox, log2) {
17734
17144
  const batches = state.noted ?? [];
17735
- state.noted = [];
17736
17145
  const seen = [];
17737
17146
  const unseen = [];
17738
17147
  for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
17739
- send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
17148
+ await send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
17149
+ state.noted = [];
17150
+ await acknowledgeDirect(batches.flatMap((b) => b.eventIds ?? []), state, rail, log2);
17740
17151
  log2?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
17741
17152
  }
17153
+ async function acknowledgeDirect(events, state, rail, log2) {
17154
+ const acked = state.acked ??= /* @__PURE__ */ new Map();
17155
+ const ids = [...new Set(events)].filter((id3) => acked.get(`delivery:${id3}`) !== "handled");
17156
+ for (let at = 0; at < ids.length; at += 100) {
17157
+ const batch = ids.slice(at, at + 100);
17158
+ try {
17159
+ const results = await rail.ack(batch);
17160
+ for (const id3 of batch) {
17161
+ const result = results.find((r) => r.eventId === id3);
17162
+ if (result?.acknowledged) acked.set(`delivery:${id3}`, "handled");
17163
+ else log2?.(`\u26A0 could not acknowledge ${id3.slice(0, 8)}: ${result?.refused ?? "no result"} \u2014 its words are typed in; it stays listed until acknowledged`);
17164
+ }
17165
+ } catch (error) {
17166
+ log2?.(`\u26A0 could not acknowledge instructions: ${error.message} \u2014 their words are typed in; they stay listed until acknowledged`);
17167
+ }
17168
+ }
17169
+ }
17742
17170
  async function acknowledge2(goalId, brief, events, state, rail, log2) {
17743
17171
  const ids = [.../* @__PURE__ */ new Set([...brief.reviewEventId ? [brief.reviewEventId] : [], ...events])];
17744
17172
  if (!ids.length) return;
@@ -17764,17 +17192,23 @@ async function step(session, state, rail, opts) {
17764
17192
  state.carryOn = true;
17765
17193
  }
17766
17194
  await look(session, state, rail, opts);
17767
- if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
17195
+ if (state.carryOn && state.resting && !state.asks?.size) await send2(session, state, "");
17768
17196
  }
17769
17197
  async function look(session, state, rail, opts) {
17770
17198
  if (opts?.inbox) {
17771
17199
  const seen = receipts(state, opts.inbox, opts.log);
17772
17200
  if (opts.tally) opts.tally.seen += seen;
17773
17201
  }
17774
- if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
17202
+ if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, rail, opts?.inbox, opts?.log);
17775
17203
  if (!state.goal) {
17204
+ if (state.asks?.size || !state.resting && !opts?.inbox) return;
17205
+ const waiting = await rail.waiting();
17206
+ if (waiting.deliveries.some((d) => !d.goalIds.length && state.acked?.get(`delivery:${d.deliveryId}`) !== "handled")) {
17207
+ const handled = state.resting ? await hear(session, state, rail, void 0, opts, waiting) : await note(state, rail, void 0, opts.inbox, { cause: opts?.cause ?? "safety", now: opts?.now ?? Date.now, log: opts?.log, tally: opts?.tally }, waiting);
17208
+ if (handled) return;
17209
+ }
17776
17210
  if (!state.resting) return;
17777
- let brief2 = await rail.next();
17211
+ let brief2 = await rail.next(waiting);
17778
17212
  if (!brief2.goalId || brief2.revision === void 0) {
17779
17213
  if (opts?.initialPrompt?.trim()) {
17780
17214
  const prompt = opts.initialPrompt.trim();
@@ -17783,7 +17217,7 @@ async function look(session, state, rail, opts) {
17783
17217
  brief2 = await rail.next();
17784
17218
  if (!brief2.goalId || brief2.revision === void 0) {
17785
17219
  state.goal = { id: admitted.goalId, revision: admitted.revision };
17786
- send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
17220
+ await send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
17787
17221
  return;
17788
17222
  }
17789
17223
  } else {
@@ -17801,7 +17235,7 @@ async function look(session, state, rail, opts) {
17801
17235
  const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
17802
17236
  const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goals with history: true reads them in full.)` : "";
17803
17237
  const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
17804
- send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
17238
+ await send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
17805
17239
  if (ended(brief2.state)) await acknowledge2(brief2.goalId, brief2, [], state, rail, opts?.log);
17806
17240
  return;
17807
17241
  }
@@ -17830,19 +17264,27 @@ async function look(session, state, rail, opts) {
17830
17264
  }
17831
17265
  await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
17832
17266
  }
17833
- function send2(session, state, text) {
17267
+ async function send2(session, state, text) {
17268
+ let words = text;
17269
+ if (state.carryOn) {
17270
+ const owed = state.owed;
17271
+ words = [owed, text].filter(Boolean).join("\n\n");
17272
+ text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
17273
+ }
17274
+ const resting = state.resting;
17834
17275
  state.resting = false;
17835
- let words2 = text;
17276
+ try {
17277
+ await session.send(text);
17278
+ } catch (error) {
17279
+ state.resting = resting;
17280
+ throw error;
17281
+ }
17282
+ state.typed = words || void 0;
17836
17283
  if (state.carryOn) {
17837
17284
  state.carryOn = false;
17838
17285
  state.carriedOn = true;
17839
- const owed = state.owed;
17840
17286
  state.owed = void 0;
17841
- words2 = [owed, text].filter(Boolean).join("\n\n");
17842
- text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
17843
17287
  }
17844
- state.typed = words2 || void 0;
17845
- session.send(text);
17846
17288
  }
17847
17289
  async function pump(session, state, rail, opts) {
17848
17290
  const clock5 = opts.beat ?? beat();
@@ -17982,9 +17424,6 @@ ${event.message}`, state, rail, { log: opts.log });
17982
17424
  opts.log("\u25A0 stopped");
17983
17425
  }
17984
17426
  return {
17985
- send(text) {
17986
- session?.send(text);
17987
- },
17988
17427
  working: () => running && state.resting !== true,
17989
17428
  // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
17990
17429
  tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
@@ -18074,7 +17513,7 @@ function selfUpdater(deps) {
18074
17513
  }
18075
17514
 
18076
17515
  // src/host.ts
18077
- var HOST_FILE = join11(homedir10(), ".paigy", "host.json");
17516
+ var HOST_FILE = join11(homedir9(), ".paigy", "host.json");
18078
17517
  function wakeHarness(slot, saved, claudeTranscript) {
18079
17518
  if (slot === "Desktop") return void 0;
18080
17519
  if (saved !== null) return saved === "agy" || saved === "codex" || saved === "claude" ? saved : void 0;
@@ -18107,7 +17546,7 @@ var claimedRun = (sessionId2) => {
18107
17546
  const slot = sessionSlot(sessionId2);
18108
17547
  return { slot, key: runKey(slot) };
18109
17548
  };
18110
- var launchRun = (spec) => ({ session: spec.sessionId });
17549
+ var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
18111
17550
  var BOUND_ELSEWHERE_MS = 10 * 6e4;
18112
17551
  var boundElsewhere = /* @__PURE__ */ new Map();
18113
17552
  var BOUND_SKIP = "is bound to the session it was paired in, which this host cannot name \u2014 run `paigy-harness handoff` as it, in its folder, to record it";
@@ -18215,7 +17654,7 @@ function startHost(opts) {
18215
17654
  });
18216
17655
  void heartbeat({
18217
17656
  version: VERSION,
18218
- harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
17657
+ harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
18219
17658
  workspaces: listWorkspaces(opts.wsDeps),
18220
17659
  // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
18221
17660
  repos: listRepos(opts.wsDeps)
@@ -18228,6 +17667,17 @@ function startHost(opts) {
18228
17667
  const specs = await claimSessions(asHost).catch(() => []);
18229
17668
  for (const spec of specs) {
18230
17669
  const label = spec.name ?? spec.harness;
17670
+ let workspace = spec.workspace;
17671
+ if (spec.worktree) {
17672
+ const suffix = randomBytes(4).toString("hex");
17673
+ const newWt = `/tmp/paigy-${spec.harness}-${suffix}`;
17674
+ try {
17675
+ execSync(`git worktree add ${newWt} -b paigy-${suffix}`, { cwd: workspace, stdio: "ignore" });
17676
+ workspace = newWt;
17677
+ } catch (e) {
17678
+ opts.log(`[${label}] Failed to create worktree, falling back to ${workspace}`);
17679
+ }
17680
+ }
18231
17681
  const { slot, key: key3 } = claimedRun(spec.sessionId);
18232
17682
  const log2 = (line) => opts.log(`[${label}] ${line}`);
18233
17683
  try {
@@ -18245,7 +17695,7 @@ function startHost(opts) {
18245
17695
  }
18246
17696
  const run = runHarness({
18247
17697
  harness: spec.harness,
18248
- cwd: spec.workspace,
17698
+ cwd: workspace,
18249
17699
  mode: "bypass",
18250
17700
  prompt: "",
18251
17701
  token: spec.token,
@@ -18666,79 +18116,13 @@ ipcMain.handle("paigy:agent", async (_e, name) => {
18666
18116
  });
18667
18117
  ipcMain.handle("paigy:agent-log", (_e, name) => {
18668
18118
  try {
18669
- const all = readFileSync7(join12(homedir11(), ".paigy", "host.log"), "utf8").split("\n");
18119
+ const all = readFileSync7(join12(homedir10(), ".paigy", "host.log"), "utf8").split("\n");
18670
18120
  const mine = all.filter((l) => l.includes(`[${name}]`) || /[▶✖◉⚠]/.test(l) && l.includes(name));
18671
18121
  return mine.slice(-500).join("\n");
18672
18122
  } catch {
18673
18123
  return "";
18674
18124
  }
18675
18125
  });
18676
- ipcMain.handle("paigy:broker", async () => {
18677
- const token = deviceToken();
18678
- if (!token)
18679
- return {
18680
- standing: "Not paired yet \u2014 scan the code above and Paigy's queue lands here.",
18681
- queue: [],
18682
- proposal: null,
18683
- voices: {},
18684
- error: "unpaired"
18685
- };
18686
- return brokerView({
18687
- notes: () => listNotes({ token }),
18688
- connections: () => listConnections({ token }),
18689
- triage: () => getTriage({ token })
18690
- });
18691
- });
18692
- var sweeping = false;
18693
- ipcMain.handle("paigy:triage-run", async () => {
18694
- const token = deviceToken();
18695
- if (!token) return { code: 1, output: "This Mac isn't paired yet \u2014 scan the pairing code first." };
18696
- if (sweeping) return { code: 1, output: "A sweep is already running." };
18697
- sweeping = true;
18698
- const lines2 = [];
18699
- const say = (line) => {
18700
- for (const l of line.split("\n")) {
18701
- lines2.push(l);
18702
- log(l);
18703
- }
18704
- };
18705
- try {
18706
- const code = await runTriage(
18707
- { via: null, json: false, apply: false, yes: { close: false, stale: false, assign: false }, log: say },
18708
- {
18709
- notes: () => listNotes({ token }),
18710
- connections: () => listConnections({ token }),
18711
- submit: (run) => submitTriage(run, { token })
18712
- }
18713
- );
18714
- return { code, output: lines2.join("\n") };
18715
- } catch (e) {
18716
- const message = e instanceof Error ? e.message : String(e);
18717
- log(`\u26A0 the sweep failed: ${message}`);
18718
- return { code: 1, output: [...lines2, `\u26A0 the sweep failed: ${message}`].join("\n") };
18719
- } finally {
18720
- sweeping = false;
18721
- }
18722
- });
18723
- ipcMain.handle("paigy:triage-accept", async (_e, opts) => {
18724
- const token = deviceToken();
18725
- if (!token) return { ok: false, error: "This Mac isn't paired yet." };
18726
- try {
18727
- return { ok: true, result: await acceptTriage(opts.id, opts.body, { token }) };
18728
- } catch (e) {
18729
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
18730
- }
18731
- });
18732
- ipcMain.handle("paigy:triage-dismiss", async (_e, id3) => {
18733
- const token = deviceToken();
18734
- if (!token) return { ok: false, error: "This Mac isn't paired yet." };
18735
- try {
18736
- await dismissTriage(id3, { token });
18737
- return { ok: true };
18738
- } catch (e) {
18739
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
18740
- }
18741
- });
18742
18126
  ipcMain.handle("paigy:workspaces", () => listWorkspaces(wsDeps));
18743
18127
  ipcMain.handle("paigy:workspace-add", async () => {
18744
18128
  const picked = await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"] });