@paigy/harness 0.3.41 → 0.3.43

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 (3) hide show
  1. package/dist/cli.js +307 -161
  2. package/dist/main.js +302 -156
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -23101,7 +23101,8 @@ var CATALOG = [
23101
23101
  authProbe: ["claude", "auth", "status"],
23102
23102
  loginHint: "Run `claude` once in a terminal and sign in.",
23103
23103
  installCli: "curl -fsSL https://claude.ai/install.sh | bash",
23104
- installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp"
23104
+ installAdapter: "npm install -g @agentclientprotocol/claude-agent-acp",
23105
+ models: ["claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"]
23105
23106
  },
23106
23107
  {
23107
23108
  name: "codex",
@@ -23111,7 +23112,8 @@ var CATALOG = [
23111
23112
  authProbe: ["codex", "login", "status"],
23112
23113
  loginHint: "Run `codex login` to authenticate.",
23113
23114
  installCli: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
23114
- installAdapter: "npm install -g @agentclientprotocol/codex-acp"
23115
+ installAdapter: "npm install -g @agentclientprotocol/codex-acp",
23116
+ models: ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini"]
23115
23117
  },
23116
23118
  {
23117
23119
  name: "agy",
@@ -23121,7 +23123,8 @@ var CATALOG = [
23121
23123
  adapter: "paigy-agy-acp",
23122
23124
  authProbe: ["agy", "help"],
23123
23125
  loginHint: "Install and set up Antigravity (`agy`).",
23124
- installCli: "curl -fsSL https://antigravity.dev/install.sh | bash"
23126
+ installCli: "curl -fsSL https://antigravity.dev/install.sh | bash",
23127
+ models: ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.0-pro-exp"]
23125
23128
  }
23126
23129
  ];
23127
23130
  function probeDirs(home = homedir()) {
@@ -23157,7 +23160,7 @@ function runProbe(argv) {
23157
23160
  function detect(entry, deps = {}) {
23158
23161
  const find = deps.which ?? which;
23159
23162
  const probe = deps.probe ?? runProbe;
23160
- const base = { name: entry.name, label: entry.label };
23163
+ const base = { name: entry.name, label: entry.label, models: entry.models };
23161
23164
  const cliPath = find(entry.cli);
23162
23165
  if (!cliPath) return { ...base, status: "missing", hint: `${entry.label} is not installed.`, install: entry.installCli };
23163
23166
  const adapterPath = entry.adapter ? find(entry.adapter) : null;
@@ -27326,7 +27329,7 @@ function onEnter(input, onLine) {
27326
27329
  };
27327
27330
  }
27328
27331
 
27329
- // ../../packages/sdk/dist/index.js
27332
+ // ../../../../../../private/tmp/paigy-draft-reaches/packages/sdk/dist/index.js
27330
27333
  import { createRequire as __sdkCreateRequire } from "module";
27331
27334
  import { execFileSync } from "child_process";
27332
27335
  import { createHash, randomUUID } from "crypto";
@@ -29745,6 +29748,31 @@ var AgendaTurnSchema = external_exports.object({
29745
29748
  ifSilent: external_exports.boolean().optional()
29746
29749
  });
29747
29750
  var CLAIM_STALE_MS = 30 * 6e4;
29751
+ var CallQuestionSchema = external_exports.object({
29752
+ id: external_exports.string(),
29753
+ entryId: external_exports.string(),
29754
+ title: external_exports.string(),
29755
+ state: external_exports.enum(["open", "answered"]),
29756
+ answer: external_exports.string().nullable(),
29757
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
29758
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
29759
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
29760
+ * author is not an agent this account holds (unpaired since, or a person). */
29761
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
29762
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
29763
+ * its need offers, exactly as its own card carries them, present only when one of them has a
29764
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
29765
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
29766
+ options: external_exports.array(OptionSchema).optional(),
29767
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
29768
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
29769
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
29770
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
29771
+ * preview layout for any `options`, and must see exactly what it did. */
29772
+ choices: external_exports.array(OptionSchema).optional(),
29773
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
29774
+ select: external_exports.enum(["one", "many", "rank"]).optional()
29775
+ });
29748
29776
  var InboxItemSchema = external_exports.object({
29749
29777
  id: external_exports.string(),
29750
29778
  tokenId: external_exports.string().optional(),
@@ -29786,31 +29814,10 @@ var InboxItemSchema = external_exports.object({
29786
29814
  * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
29787
29815
  * the card's own concise heading; `answer` the accepted answer in words, null while open. It
29788
29816
  * REPLACED `agenda` (turns), which nothing ever filled. */
29789
- questions: external_exports.array(external_exports.object({
29790
- id: external_exports.string(),
29791
- entryId: external_exports.string(),
29792
- title: external_exports.string(),
29793
- state: external_exports.enum(["open", "answered"]),
29794
- answer: external_exports.string().nullable(),
29795
- /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
29796
- * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
29797
- * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
29798
- * author is not an agent this account holds (unpaired since, or a person). */
29799
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema.optional() }).optional(),
29800
- /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
29801
- * its need offers, exactly as its own card carries them, present only when one of them has a
29802
- * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
29803
- * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
29804
- options: external_exports.array(OptionSchema).optional(),
29805
- /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
29806
- * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
29807
- * numbered chips above the call's controls, so a checklist is never only something read aloud. A
29808
- * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
29809
- * preview layout for any `options`, and must see exactly what it did. */
29810
- choices: external_exports.array(OptionSchema).optional(),
29811
- /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
29812
- select: external_exports.enum(["one", "many", "rank"]).optional()
29813
- })).optional(),
29817
+ questions: external_exports.array(CallQuestionSchema).optional(),
29818
+ /** The same call carrying order, including request Entries with no DecisionNeed as reports.
29819
+ * Additive: questions stays decision-only for every other surface and older bundles. */
29820
+ callItems: external_exports.array(CallQuestionSchema.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
29814
29821
  visuals: external_exports.array(VisualSchema).optional(),
29815
29822
  /** The connected agent's name (the single pairing name — user-typed, or the
29816
29823
  * agent's suggestion, or a default silly name). */
@@ -30009,12 +30016,29 @@ var UserSettingsSchema = external_exports.object({
30009
30016
  * clobber guard as voiceMode: absent = leave unchanged on write. */
30010
30017
  broker: BrokerTuningSchema.optional()
30011
30018
  });
30019
+ var GoalLatestSchema = external_exports.object({
30020
+ from: external_exports.enum(["person", "agent"]),
30021
+ said: external_exports.string(),
30022
+ at: external_exports.string(),
30023
+ entryId: external_exports.string(),
30024
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
30025
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
30026
+ * none was. The Work row reads it before `said`. */
30027
+ summary: external_exports.string().optional(),
30028
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
30029
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
30030
+ titles: external_exports.array(external_exports.string()).optional(),
30031
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
30032
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
30033
+ line: external_exports.string().optional()
30034
+ });
30012
30035
  var HistoryWorkSchema = external_exports.object({
30013
30036
  id: external_exports.string(),
30014
30037
  title: external_exports.string(),
30015
30038
  state: external_exports.enum(["done", "cancelled"]),
30016
30039
  /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
30017
- assignee: external_exports.string()
30040
+ assignee: external_exports.string(),
30041
+ latest: GoalLatestSchema.nullable().optional()
30018
30042
  });
30019
30043
  var HistoryEntrySchema = external_exports.union([
30020
30044
  external_exports.object({ at: external_exports.string(), card: InboxItemSchema }),
@@ -30082,7 +30106,7 @@ var ConnectionSummarySchema = external_exports.object({
30082
30106
  /** The @paigy/harness this host is running — a machine the self-update has not reached
30083
30107
  * shows its age here (`apps/desktop/src/update.ts`). */
30084
30108
  version: external_exports.string().optional(),
30085
- harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
30109
+ 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(),
30086
30110
  workspaces: external_exports.array(external_exports.string()).optional(),
30087
30111
  /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
30088
30112
  * repo, and each repo directly inside one, with its `origin` remote. A session started for
@@ -30242,22 +30266,7 @@ var QueueItemSchema = external_exports.object({
30242
30266
  * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
30243
30267
  * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
30244
30268
  * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
30245
- latest: external_exports.object({
30246
- from: external_exports.enum(["person", "agent"]),
30247
- said: external_exports.string(),
30248
- at: external_exports.string(),
30249
- entryId: external_exports.string(),
30250
- /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
30251
- * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
30252
- * none was. The Work row reads it before `said`. */
30253
- summary: external_exports.string().optional(),
30254
- /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
30255
- * of each unit"): each at most 50 characters, in order; absent when it has none. */
30256
- titles: external_exports.array(external_exports.string()).optional(),
30257
- /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
30258
- * update's own line, before its units; the row caps it at 200. Absent on anything else. */
30259
- line: external_exports.string().optional()
30260
- }).nullable().optional(),
30269
+ latest: GoalLatestSchema.nullable().optional(),
30261
30270
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
30262
30271
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
30263
30272
  * answer to a question, a voice note filed as work. Null when the only hands on it have been its
@@ -31130,7 +31139,7 @@ var import_qrcode = __toESM(require_lib(), 1);
31130
31139
  import { randomUUID as randomUUID5 } from "crypto";
31131
31140
  import { hostname } from "os";
31132
31141
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
31133
- import { execFile as execFile2, execSync } from "child_process";
31142
+ import { execFile as execFile2, execSync as execSync2 } from "child_process";
31134
31143
  import { homedir as homedir9 } from "os";
31135
31144
  import { join as join11 } from "path";
31136
31145
 
@@ -31512,7 +31521,8 @@ function startSession(opts) {
31512
31521
  if (!existsSync3(cwd)) {
31513
31522
  queueMicrotask(() => opts.onEvent({ kind: "error", message: `workspace does not exist: ${cwd}` }));
31514
31523
  }
31515
- const codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
31524
+ let codexConfig = opts.harness === "codex" ? JSON.parse(process.env.CODEX_CONFIG || "{}") : null;
31525
+ if (opts.model && opts.harness === "codex") codexConfig = { ...codexConfig, model: opts.model };
31516
31526
  if (codexConfig) {
31517
31527
  const key3 = "mcp_servers.paigy.env_vars";
31518
31528
  const configPath = join5(process.env.CODEX_HOME || join5(homedir4(), ".codex"), "config.toml");
@@ -31538,6 +31548,8 @@ function startSession(opts) {
31538
31548
  PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
31539
31549
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {},
31540
31550
  ...opts.instance ? { PAIGY_INSTANCE_ID: opts.instance } : {},
31551
+ ...opts.model && opts.harness === "claude" ? { CLAUDE_MODEL_CONFIG: JSON.stringify({ provider: "anthropic", model: opts.model }) } : {},
31552
+ ...opts.model && opts.harness === "agy" ? { AGY_MODEL_CONFIG: JSON.stringify({ model: opts.model }) } : {},
31541
31553
  ...codexConfig ? { CODEX_CONFIG: JSON.stringify(codexConfig) } : {},
31542
31554
  ...opts.session ?? opts.resume ? { PAIGY_SESSION_ID: opts.session ?? opts.resume } : {},
31543
31555
  // A PUMP DRIVES THIS PROCESS (2026-09-22, #2265): every run `runHarness` starts has the
@@ -31552,19 +31564,56 @@ function startSession(opts) {
31552
31564
  stdio: ["pipe", "pipe", "pipe"]
31553
31565
  });
31554
31566
  let ended2 = false;
31555
- child.stdin?.on("error", () => {
31556
- });
31567
+ let failure;
31568
+ let hush;
31569
+ const pending = /* @__PURE__ */ new Set();
31570
+ const end = (error) => {
31571
+ ended2 = true;
31572
+ failure ??= error;
31573
+ clearTimeout(hush);
31574
+ for (const receipt of pending) receipt.reject(failure);
31575
+ pending.clear();
31576
+ };
31577
+ const broken = (error) => {
31578
+ if (ended2) return;
31579
+ end(new Error(`session_write_failed: ${error.message}`));
31580
+ opts.onEvent({ kind: "error", message: failure.message });
31581
+ child.kill();
31582
+ };
31583
+ child.stdin?.on("error", broken);
31557
31584
  const write = (frames) => {
31558
31585
  if (ended2) return;
31559
- for (const f of frames) child.stdin?.write(`${f}
31560
- `);
31561
- if (frames.length) listen();
31586
+ for (const f of frames) {
31587
+ if (ended2) break;
31588
+ if (!child.stdin || child.stdin.destroyed || !child.stdin.writable) {
31589
+ broken(new Error("stdin unavailable"));
31590
+ break;
31591
+ }
31592
+ const frame3 = JSON.parse(f);
31593
+ const receipts2 = frame3.method === "session/prompt" ? [...pending].filter((receipt) => !receipt.writing).slice(0, frame3.params.prompt.length) : [];
31594
+ for (const receipt of receipts2) receipt.writing = true;
31595
+ try {
31596
+ child.stdin.write(`${f}
31597
+ `, (error) => {
31598
+ if (error) {
31599
+ broken(error);
31600
+ return;
31601
+ }
31602
+ if (ended2) return;
31603
+ for (const receipt of receipts2) if (pending.delete(receipt)) receipt.resolve();
31604
+ });
31605
+ } catch (error) {
31606
+ broken(error instanceof Error ? error : new Error(String(error)));
31607
+ }
31608
+ }
31609
+ if (frames.length && !ended2) listen();
31562
31610
  };
31563
31611
  const apply = ({ events, writes }) => {
31612
+ const fatal = events.find((event) => event.kind === "error" && event.fatal);
31613
+ if (fatal?.kind === "error") end(new Error(fatal.message));
31564
31614
  write(writes);
31565
31615
  for (const event of events) opts.onEvent(event);
31566
31616
  };
31567
- let hush;
31568
31617
  const listen = () => {
31569
31618
  clearTimeout(hush);
31570
31619
  if (!ended2) hush = setTimeout(() => apply(driver.quiet(QUIET_MS)), QUIET_MS);
@@ -31574,12 +31623,12 @@ function startSession(opts) {
31574
31623
  if (text) opts.onEvent({ kind: "error", message: text });
31575
31624
  });
31576
31625
  child.on("exit", (code) => {
31577
- ended2 = true;
31578
- clearTimeout(hush);
31626
+ end(new Error(`session_ended: ${code ?? "signal"}`));
31579
31627
  opts.onEvent({ kind: "idle", failed: code !== 0, ...code ? { result: `exited ${code}` } : {} });
31580
31628
  opts.onExit?.();
31581
31629
  });
31582
31630
  child.on("error", (e) => {
31631
+ end(new Error(`session_start_failed: ${e.message}`));
31583
31632
  opts.onEvent({ kind: "error", message: e.message });
31584
31633
  opts.onExit?.();
31585
31634
  });
@@ -31599,15 +31648,24 @@ function startSession(opts) {
31599
31648
  write(driver.open());
31600
31649
  return {
31601
31650
  send(text) {
31602
- write(driver.send(text));
31651
+ const sent = new Promise((resolve3, reject) => {
31652
+ if (ended2) {
31653
+ reject(failure ?? new Error("session_ended"));
31654
+ return;
31655
+ }
31656
+ pending.add({ resolve: resolve3, reject, writing: false });
31657
+ write(driver.send(text));
31658
+ });
31659
+ void sent.catch(() => {
31660
+ });
31661
+ return sent;
31603
31662
  },
31604
31663
  respond(id3, decision) {
31605
31664
  write(driver.respond(id3, decision));
31606
31665
  },
31607
31666
  stop() {
31608
31667
  write(driver.close());
31609
- ended2 = true;
31610
- clearTimeout(hush);
31668
+ end(new Error("session_stopped"));
31611
31669
  child.kill();
31612
31670
  }
31613
31671
  };
@@ -32897,7 +32955,7 @@ var zodToJsonSchema2 = (schema, options) => {
32897
32955
  return combined;
32898
32956
  };
32899
32957
 
32900
- // ../../packages/schema/dist/index.js
32958
+ // ../../../../../../private/tmp/paigy-draft-reaches/packages/schema/dist/index.js
32901
32959
  var OPTIONS_MIN2 = 2;
32902
32960
  var OPTIONS_MAX2 = 6;
32903
32961
  var OPTION_LABEL_MAX2 = 50;
@@ -33992,6 +34050,31 @@ var AgendaTurnSchema2 = external_exports.object({
33992
34050
  ifSilent: external_exports.boolean().optional()
33993
34051
  });
33994
34052
  var CLAIM_STALE_MS2 = 30 * 6e4;
34053
+ var CallQuestionSchema2 = external_exports.object({
34054
+ id: external_exports.string(),
34055
+ entryId: external_exports.string(),
34056
+ title: external_exports.string(),
34057
+ state: external_exports.enum(["open", "answered"]),
34058
+ answer: external_exports.string().nullable(),
34059
+ /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
34060
+ * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
34061
+ * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
34062
+ * author is not an agent this account holds (unpaired since, or a person). */
34063
+ agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
34064
+ /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
34065
+ * its need offers, exactly as its own card carries them, present only when one of them has a
34066
+ * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
34067
+ * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
34068
+ options: external_exports.array(OptionSchema2).optional(),
34069
+ /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
34070
+ * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
34071
+ * numbered chips above the call's controls, so a checklist is never only something read aloud. A
34072
+ * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
34073
+ * preview layout for any `options`, and must see exactly what it did. */
34074
+ choices: external_exports.array(OptionSchema2).optional(),
34075
+ /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
34076
+ select: external_exports.enum(["one", "many", "rank"]).optional()
34077
+ });
33995
34078
  var InboxItemSchema2 = external_exports.object({
33996
34079
  id: external_exports.string(),
33997
34080
  tokenId: external_exports.string().optional(),
@@ -34033,31 +34116,10 @@ var InboxItemSchema2 = external_exports.object({
34033
34116
  * `turn` topic (`asking`, `settled`), because the bot never sees a DecisionNeed id. `title` is
34034
34117
  * the card's own concise heading; `answer` the accepted answer in words, null while open. It
34035
34118
  * REPLACED `agenda` (turns), which nothing ever filled. */
34036
- questions: external_exports.array(external_exports.object({
34037
- id: external_exports.string(),
34038
- entryId: external_exports.string(),
34039
- title: external_exports.string(),
34040
- state: external_exports.enum(["open", "answered"]),
34041
- answer: external_exports.string().nullable(),
34042
- /** WHO ASKED IT (owner, 2026-09-23, Goal a345e906: each agenda row wears its agent's face) — the
34043
- * request Entry's author, as the same three facts the item's own `tokenId`/`name`/`voice`
34044
- * carry for the call's one agent, so the phone draws it with the same seed. Absent when the
34045
- * author is not an agent this account holds (unpaired since, or a person). */
34046
- agent: external_exports.object({ tokenId: external_exports.string(), name: external_exports.string(), voice: VoiceKeySchema2.optional() }).optional(),
34047
- /** ITS OPTIONS, WHEN THERE IS SOMETHING TO SEE (owner, 2026-09-25: "Yes, add it"): the options
34048
- * its need offers, exactly as its own card carries them, present only when one of them has a
34049
- * preview (`html` or `image`). The call screen opens them from the agenda row, so a preview is
34050
- * never re-sent as a second card to be seen mid-call. Words-only options are `choices`. */
34051
- options: external_exports.array(OptionSchema2).optional(),
34052
- /** ITS OPTIONS WHEN THEY ARE WORDS (owner, 2026-10-02, on a call: "whenever there are pre-made
34053
- * options … now for a checklist, we should also show them on the screen"; design C, 2026-10-08):
34054
- * numbered chips above the call's controls, so a checklist is never only something read aloud. A
34055
- * field of its own so `options` keeps meaning "previews": a phone on an older bundle opens the
34056
- * preview layout for any `options`, and must see exactly what it did. */
34057
- choices: external_exports.array(OptionSchema2).optional(),
34058
- /** HOW MANY MAY BE PICKED, beside its `choices`: one, any (`many`) or an order (`rank`). */
34059
- select: external_exports.enum(["one", "many", "rank"]).optional()
34060
- })).optional(),
34119
+ questions: external_exports.array(CallQuestionSchema2).optional(),
34120
+ /** The same call carrying order, including request Entries with no DecisionNeed as reports.
34121
+ * Additive: questions stays decision-only for every other surface and older bundles. */
34122
+ callItems: external_exports.array(CallQuestionSchema2.extend({ state: external_exports.enum(["open", "answered", "report"]) })).optional(),
34061
34123
  visuals: external_exports.array(VisualSchema2).optional(),
34062
34124
  /** The connected agent's name (the single pairing name — user-typed, or the
34063
34125
  * agent's suggestion, or a default silly name). */
@@ -34256,12 +34318,29 @@ var UserSettingsSchema2 = external_exports.object({
34256
34318
  * clobber guard as voiceMode: absent = leave unchanged on write. */
34257
34319
  broker: BrokerTuningSchema2.optional()
34258
34320
  });
34321
+ var GoalLatestSchema2 = external_exports.object({
34322
+ from: external_exports.enum(["person", "agent"]),
34323
+ said: external_exports.string(),
34324
+ at: external_exports.string(),
34325
+ entryId: external_exports.string(),
34326
+ /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
34327
+ * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
34328
+ * none was. The Work row reads it before `said`. */
34329
+ summary: external_exports.string().optional(),
34330
+ /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
34331
+ * of each unit"): each at most 50 characters, in order; absent when it has none. */
34332
+ titles: external_exports.array(external_exports.string()).optional(),
34333
+ /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
34334
+ * update's own line, before its units; the row caps it at 200. Absent on anything else. */
34335
+ line: external_exports.string().optional()
34336
+ });
34259
34337
  var HistoryWorkSchema2 = external_exports.object({
34260
34338
  id: external_exports.string(),
34261
34339
  title: external_exports.string(),
34262
34340
  state: external_exports.enum(["done", "cancelled"]),
34263
34341
  /** Who held it (`agent:<tokenId>` or `human:<userId>`). */
34264
- assignee: external_exports.string()
34342
+ assignee: external_exports.string(),
34343
+ latest: GoalLatestSchema2.nullable().optional()
34265
34344
  });
34266
34345
  var HistoryEntrySchema2 = external_exports.union([
34267
34346
  external_exports.object({ at: external_exports.string(), card: InboxItemSchema2 }),
@@ -34329,7 +34408,7 @@ var ConnectionSummarySchema2 = external_exports.object({
34329
34408
  /** The @paigy/harness this host is running — a machine the self-update has not reached
34330
34409
  * shows its age here (`apps/desktop/src/update.ts`). */
34331
34410
  version: external_exports.string().optional(),
34332
- harnesses: external_exports.array(external_exports.object({ name: external_exports.string(), label: external_exports.string(), status: external_exports.string() })).optional(),
34411
+ 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(),
34333
34412
  workspaces: external_exports.array(external_exports.string()).optional(),
34334
34413
  /** THE GIT REPOS IN THOSE FOLDERS (2026-10-01, Goal 26982211): each granted folder that is a
34335
34414
  * repo, and each repo directly inside one, with its `origin` remote. A session started for
@@ -34489,22 +34568,7 @@ var QueueItemSchema2 = external_exports.object({
34489
34568
  * shows the moment it is written, and the agent's reply or next note replaces it by being newer.
34490
34569
  * `said` is bounded to 280 characters server-side (a line, not the conversation). Null when the
34491
34570
  * Goal carries no readable Entry; optional, so hand-built queues need not spell it. */
34492
- latest: external_exports.object({
34493
- from: external_exports.enum(["person", "agent"]),
34494
- said: external_exports.string(),
34495
- at: external_exports.string(),
34496
- entryId: external_exports.string(),
34497
- /** ITS SUMMARY IN AT MOST TEN WORDS (owner, 2026-10-08: "the same switch to the more summarized sources
34498
- * at the work tab top level screen too"): what the talker or filer wrote on the Entry; absent when
34499
- * none was. The Work row reads it before `said`. */
34500
- summary: external_exports.string().optional(),
34501
- /** AN AGENT'S UPDATE OR QUESTION AS ITS UNITS' TITLES (owner, 2026-10-08: "send the 50 chars from the title
34502
- * of each unit"): each at most 50 characters, in order; absent when it has none. */
34503
- titles: external_exports.array(external_exports.string()).optional(),
34504
- /** A PROGRESS NOTE'S ONE POINT ("An agent's progress note -> maybe let's cap it at 200 chars"): the
34505
- * update's own line, before its units; the row caps it at 200. Absent on anything else. */
34506
- line: external_exports.string().optional()
34507
- }).nullable().optional(),
34571
+ latest: GoalLatestSchema2.nullable().optional(),
34508
34572
  /** WHEN THIS PERSON LAST PUT A HAND ON IT THEMSELVES (owner, Paigy Goal 16d18f51, 2026-09-30):
34509
34573
  * the newest Entry on the Goal they wrote, of any kind — a line they added, a reply to a note, an
34510
34574
  * answer to a question, a voice note filed as work. Null when the only hands on it have been its
@@ -35004,12 +35068,13 @@ var LIMITS = episodes();
35004
35068
  // src/paigy/bridge.ts
35005
35069
  function railFor(opts) {
35006
35070
  return {
35007
- next: async () => {
35008
- const waiting = await receive(opts);
35071
+ next: async (alreadyRead) => {
35072
+ const waiting = alreadyRead ?? await receive(opts);
35009
35073
  const goalId = waiting.claimable?.goalId ?? waiting.review?.[0]?.goalId;
35010
35074
  return goalId ? getGoals([goalId], opts).then(([goal]) => goal) : { state: "idle", message: "" };
35011
35075
  },
35012
35076
  read: (goalId) => getGoals([goalId], opts).then(([goal]) => goal),
35077
+ delivery: (deliveryId) => readDelivery(deliveryId, opts),
35013
35078
  complete: async (goalId, revision) => {
35014
35079
  const done = await manageGoals({ changes: [{ kind: "state", goalId, state: "completed", ...revision !== void 0 ? { revision } : {} }] }, opts);
35015
35080
  const result = done.results[0];
@@ -35190,7 +35255,7 @@ function beat() {
35190
35255
  }
35191
35256
  var BEAT_REPORT_MS = 10 * 6e4;
35192
35257
  var ended = (state) => state === "done" || state === "cancelled";
35193
- function unheard(brief, state, since = null, after = -1, reviewSince) {
35258
+ function unheard(brief, state, since = null, after = -1, reviewSince, gathered) {
35194
35259
  const seen = state.seen ??= /* @__PURE__ */ new Set();
35195
35260
  const words = [];
35196
35261
  const pendingAt = reviewSince ? Date.parse(reviewSince) : null;
@@ -35205,10 +35270,13 @@ function unheard(brief, state, since = null, after = -1, reviewSince) {
35205
35270
  continue;
35206
35271
  }
35207
35272
  if (since !== null && Date.parse(e.createdAt) <= Date.parse(since)) continue;
35208
- seen.add(e.entryId);
35209
35273
  if (e.aboutId && seen.has(e.aboutId) && brief.answers?.some((a) => a.requestEntryId === e.aboutId)) continue;
35274
+ seen.add(e.entryId);
35210
35275
  const text = entryWords(e).trim();
35211
- if (text) words.push(text);
35276
+ if (text) {
35277
+ words.push(text);
35278
+ gathered?.add(e.entryId);
35279
+ }
35212
35280
  }
35213
35281
  if (pendingAt !== null) for (const answer of brief.answers ?? []) {
35214
35282
  const key3 = `answer:${answer.decisionNeedId}`;
@@ -35249,12 +35317,28 @@ function named(goalId, title, brief) {
35249
35317
  const name = line.length > 80 ? `${line.slice(0, 79)}\u2026` : line;
35250
35318
  return name ? `On Goal "${name}" (${goalId}):` : `On Goal ${goalId}:`;
35251
35319
  }
35252
- async function gather(state, rail, held) {
35253
- const looks = /* @__PURE__ */ new Map([[state.goal.id, { via: "held", brief: held }]]);
35320
+ async function gather(state, rail, held, alreadyRead, log) {
35321
+ const looks = /* @__PURE__ */ new Map();
35322
+ if (held && state.goal) looks.set(state.goal.id, { via: "held", brief: held });
35254
35323
  const look2 = (goalId, via, title, since) => {
35255
35324
  if (!looks.has(goalId)) looks.set(goalId, { via, ...title ? { title } : {}, ...since !== void 0 ? { since } : {} });
35256
35325
  };
35257
- const waiting = await rail.waiting().catch(() => null);
35326
+ const waiting = alreadyRead ?? await rail.waiting().catch(() => null);
35327
+ const direct = [];
35328
+ const staged = new Set(state.seen);
35329
+ const deliveries = /* @__PURE__ */ new Set();
35330
+ for (const d of waiting?.deliveries ?? []) {
35331
+ if (d.goalIds.length || deliveries.has(d.deliveryId) || state.acked?.get(`delivery:${d.deliveryId}`) === "handled") continue;
35332
+ deliveries.add(d.deliveryId);
35333
+ try {
35334
+ const full = await rail.delivery(d.deliveryId);
35335
+ const entries = full.entries.map((e) => ({ id: e.entryId, words: entryWords(e) }));
35336
+ if (!entries.length || entries.some((e) => !e.words.trim())) continue;
35337
+ direct.push({ eventId: d.deliveryId, entries, entryIds: entries.map((e) => e.id), words: [] });
35338
+ } catch (error) {
35339
+ log?.(`\u26A0 could not read instruction ${d.deliveryId.slice(0, 8)}: ${error.message}`);
35340
+ }
35341
+ }
35258
35342
  const listed = /* @__PURE__ */ new Map();
35259
35343
  const list = (goalId, eventId) => {
35260
35344
  const ids = listed.get(goalId) ?? [];
@@ -35262,19 +35346,21 @@ async function gather(state, rail, held) {
35262
35346
  listed.set(goalId, ids);
35263
35347
  };
35264
35348
  const announced = state.announced ??= /* @__PURE__ */ new Set();
35265
- if (waiting) {
35266
- for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
35267
- }
35268
- for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
35269
- for (const g of waiting?.review ?? []) {
35270
- list(g.goalId, g.eventId);
35271
- look2(g.goalId, "waiting work", g.title);
35272
- }
35273
- for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
35274
- list(id3, d.deliveryId);
35275
- look2(id3, "waiting work");
35349
+ if (held) {
35350
+ if (waiting) {
35351
+ for (const id3 of announced) if (!(waiting.assigned ?? []).some((g) => g.goalId === id3)) announced.delete(id3);
35352
+ }
35353
+ for (const g of waiting?.assigned ?? []) if (g.goalId !== state.goal.id && !announced.has(g.goalId)) look2(g.goalId, "assigned", g.title);
35354
+ for (const g of waiting?.review ?? []) {
35355
+ list(g.goalId, g.eventId);
35356
+ look2(g.goalId, "waiting work", g.title);
35357
+ }
35358
+ for (const d of waiting?.deliveries ?? []) for (const id3 of d.goalIds) {
35359
+ list(id3, d.deliveryId);
35360
+ look2(id3, "waiting work");
35361
+ }
35276
35362
  }
35277
- const moved = movedBelow(held, state);
35363
+ const moved = held ? movedBelow(held, state) : [];
35278
35364
  for (const { goal, since } of moved) look2(goal.goalId, "subtree", goal.title, since);
35279
35365
  const heard = [];
35280
35366
  const read3 = /* @__PURE__ */ new Set();
@@ -35291,10 +35377,15 @@ async function gather(state, rail, held) {
35291
35377
  read3.add(goalId);
35292
35378
  const fresh = !brief.reviewSince && (found.via === "waiting work" || found.via === "assigned") ? sinceTheWork(brief) : { after: -1 };
35293
35379
  if (found.via === "assigned") announced.add(goalId);
35294
- heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince), ...fresh.note ? { note: fresh.note } : {} });
35380
+ heard.push({ goalId, ...found, brief, words: unheard(brief, state, brief.reviewSince ? null : since ?? null, fresh.after, brief.reviewSince, staged), ...fresh.note ? { note: fresh.note } : {} });
35295
35381
  }
35382
+ for (const d of direct) d.words = d.entries.filter((e) => {
35383
+ if (staged.has(e.id)) return false;
35384
+ staged.add(e.id);
35385
+ return true;
35386
+ }).map((e) => e.words);
35296
35387
  for (const { goal, since } of moved) if (read3.has(goal.goalId)) state.below.goals.set(goal.goalId, { since, at: goal.updatedAt ?? null });
35297
- return { heard, listed };
35388
+ return { heard, listed, direct };
35298
35389
  }
35299
35390
  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.";
35300
35391
  var says = (h) => h.words.length > 0 || h.via === "assigned";
@@ -35312,12 +35403,21 @@ var block = (h) => [
35312
35403
  ].join("\n\n");
35313
35404
  var short = (ids) => ids.map((id3) => id3.slice(0, 8)).join(", ");
35314
35405
  var given = (heard) => heard.filter((h) => h.via === "assigned").map((h) => h.goalId);
35315
- async function hear(session, state, rail, held, opts) {
35406
+ async function hear(session, state, rail, held, opts, waiting) {
35316
35407
  const log = opts?.log;
35317
- const { heard, listed } = await gather(state, rail, held);
35408
+ const { heard, listed, direct } = await gather(state, rail, held, waiting, log);
35318
35409
  const said = heard.filter(says);
35319
- if (said.length) {
35320
- send2(session, state, said.map(block).join("\n\n"));
35410
+ const words = [...direct.flatMap((d) => d.words), ...said.map(block)];
35411
+ if (words.length) {
35412
+ const text = words.join("\n\n");
35413
+ try {
35414
+ await send2(session, state, text);
35415
+ } catch (error) {
35416
+ (state.noted ??= []).push({ name: "", goals: said.map((h) => h.goalId), words: text, eventIds: direct.map((d) => d.eventId), at: Date.now(), cause: "read" });
35417
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
35418
+ throw error;
35419
+ }
35420
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
35321
35421
  const worded = said.filter((h) => h.words.length);
35322
35422
  if (worded.length) log?.(`\u25B8 typed the person's words from ${worded.map((h) => `${h.goalId.slice(0, 8)} (via ${h.via})`).join(", ")}`);
35323
35423
  const goals = given(said);
@@ -35326,18 +35426,21 @@ async function hear(session, state, rail, held, opts) {
35326
35426
  if (opts?.tally) opts.tally.announced += goals.length;
35327
35427
  }
35328
35428
  }
35429
+ await acknowledgeDirect(direct.map((d) => d.eventId), state, rail, log);
35329
35430
  for (const h of heard) {
35330
35431
  const events = listed.get(h.goalId);
35331
35432
  if (events) await acknowledge2(h.goalId, h.brief, events, state, rail, log);
35332
35433
  }
35434
+ return direct.length > 0 || said.length > 0;
35333
35435
  }
35334
35436
  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.";
35335
35437
  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.";
35336
- async function note(state, rail, held, inbox, how) {
35337
- const { heard } = await gather(state, rail, held);
35438
+ async function note(state, rail, held, inbox, how, waiting) {
35439
+ const { heard, direct } = await gather(state, rail, held, waiting, how.log);
35338
35440
  const said = heard.filter(says);
35339
- if (!said.length) return 0;
35340
- const words = said.map(block).join("\n\n");
35441
+ const parts = [...direct.flatMap((d) => d.words), ...said.map(block)];
35442
+ if (!parts.length) return 0;
35443
+ const words = parts.join("\n\n");
35341
35444
  const goals = said.map((h) => h.goalId);
35342
35445
  const assigned = given(said);
35343
35446
  if (how.tally) how.tally.announced += assigned.length;
@@ -35351,7 +35454,8 @@ ${words}`);
35351
35454
  } catch (error) {
35352
35455
  how.log?.(`\u26A0 could not note mid-turn (${error.message}): ${short(goals)} wait for the turn's end`);
35353
35456
  }
35354
- (state.noted ??= []).push({ name, goals, words, at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
35457
+ (state.noted ??= []).push({ name, goals, words, eventIds: direct.map((d) => d.eventId), at: how.now(), cause: how.cause === "wake" ? "wake" : "read" });
35458
+ for (const d of direct) for (const id3 of d.entryIds) (state.seen ??= /* @__PURE__ */ new Set()).add(id3);
35355
35459
  return 1;
35356
35460
  }
35357
35461
  function receipts(state, inbox, log) {
@@ -35366,15 +35470,33 @@ function receipts(state, inbox, log) {
35366
35470
  }
35367
35471
  return seen;
35368
35472
  }
35369
- function settle(session, state, inbox, log) {
35473
+ async function settle(session, state, rail, inbox, log) {
35370
35474
  const batches = state.noted ?? [];
35371
- state.noted = [];
35372
35475
  const seen = [];
35373
35476
  const unseen = [];
35374
35477
  for (const b of batches) (b.name && inbox?.settle(b.name) === "taken" ? seen : unseen).push(b);
35375
- send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
35478
+ await send2(session, state, [...seen.length ? [SEEN_MID_TURN, ...seen.map((b) => b.words)] : [], ...unseen.map((b) => b.words)].join("\n\n"));
35479
+ state.noted = [];
35480
+ await acknowledgeDirect(batches.flatMap((b) => b.eventIds ?? []), state, rail, log);
35376
35481
  log?.(`\u25B8 typed the person's words from ${short([...new Set(batches.flatMap((b) => b.goals))])} (noted mid-turn: ${seen.length} seen by the hook, ${unseen.length} not)`);
35377
35482
  }
35483
+ async function acknowledgeDirect(events, state, rail, log) {
35484
+ const acked = state.acked ??= /* @__PURE__ */ new Map();
35485
+ const ids = [...new Set(events)].filter((id3) => acked.get(`delivery:${id3}`) !== "handled");
35486
+ for (let at = 0; at < ids.length; at += 100) {
35487
+ const batch = ids.slice(at, at + 100);
35488
+ try {
35489
+ const results = await rail.ack(batch);
35490
+ for (const id3 of batch) {
35491
+ const result = results.find((r) => r.eventId === id3);
35492
+ if (result?.acknowledged) acked.set(`delivery:${id3}`, "handled");
35493
+ else log?.(`\u26A0 could not acknowledge ${id3.slice(0, 8)}: ${result?.refused ?? "no result"} \u2014 its words are typed in; it stays listed until acknowledged`);
35494
+ }
35495
+ } catch (error) {
35496
+ log?.(`\u26A0 could not acknowledge instructions: ${error.message} \u2014 their words are typed in; they stay listed until acknowledged`);
35497
+ }
35498
+ }
35499
+ }
35378
35500
  async function acknowledge2(goalId, brief, events, state, rail, log) {
35379
35501
  const ids = [.../* @__PURE__ */ new Set([...brief.reviewEventId ? [brief.reviewEventId] : [], ...events])];
35380
35502
  if (!ids.length) return;
@@ -35400,17 +35522,23 @@ async function step(session, state, rail, opts) {
35400
35522
  state.carryOn = true;
35401
35523
  }
35402
35524
  await look(session, state, rail, opts);
35403
- if (state.carryOn && state.resting && !state.asks?.size) send2(session, state, "");
35525
+ if (state.carryOn && state.resting && !state.asks?.size) await send2(session, state, "");
35404
35526
  }
35405
35527
  async function look(session, state, rail, opts) {
35406
35528
  if (opts?.inbox) {
35407
35529
  const seen = receipts(state, opts.inbox, opts.log);
35408
35530
  if (opts.tally) opts.tally.seen += seen;
35409
35531
  }
35410
- if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, opts?.inbox, opts?.log);
35532
+ if (state.resting && state.noted?.length && !state.asks?.size) return settle(session, state, rail, opts?.inbox, opts?.log);
35411
35533
  if (!state.goal) {
35534
+ if (state.asks?.size || !state.resting && !opts?.inbox) return;
35535
+ const waiting = await rail.waiting();
35536
+ if (waiting.deliveries.some((d) => !d.goalIds.length && state.acked?.get(`delivery:${d.deliveryId}`) !== "handled")) {
35537
+ 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);
35538
+ if (handled) return;
35539
+ }
35412
35540
  if (!state.resting) return;
35413
- let brief2 = await rail.next();
35541
+ let brief2 = await rail.next(waiting);
35414
35542
  if (!brief2.goalId || brief2.revision === void 0) {
35415
35543
  if (opts?.initialPrompt?.trim()) {
35416
35544
  const prompt = opts.initialPrompt.trim();
@@ -35419,7 +35547,7 @@ async function look(session, state, rail, opts) {
35419
35547
  brief2 = await rail.next();
35420
35548
  if (!brief2.goalId || brief2.revision === void 0) {
35421
35549
  state.goal = { id: admitted.goalId, revision: admitted.revision };
35422
- send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
35550
+ await send2(session, state, [HARNESS_INSTRUCTION, named(admitted.goalId, void 0, { outcome: prompt }), prompt].join("\n\n"));
35423
35551
  return;
35424
35552
  }
35425
35553
  } else {
@@ -35437,7 +35565,7 @@ async function look(session, state, rail, opts) {
35437
35565
  const left = brief2.earlier ? brief2.earlier.omitted + brief2.earlier.shortened : 0;
35438
35566
  const earlier = left ? `(${left} earlier entries on this Goal are left out or shortened; get_goals with history: true reads them in full.)` : "";
35439
35567
  const fullText = [outcome2, earlier, ...said].filter(Boolean).join("\n\n");
35440
- send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
35568
+ await send2(session, state, [HARNESS_INSTRUCTION, named(brief2.goalId, void 0, brief2), fullText].filter(Boolean).join("\n\n"));
35441
35569
  if (ended(brief2.state)) await acknowledge2(brief2.goalId, brief2, [], state, rail, opts?.log);
35442
35570
  return;
35443
35571
  }
@@ -35466,19 +35594,27 @@ async function look(session, state, rail, opts) {
35466
35594
  }
35467
35595
  await hear(session, state, rail, brief, { ...opts?.log ? { log: opts.log } : {}, ...opts?.tally ? { tally: opts.tally } : {} });
35468
35596
  }
35469
- function send2(session, state, text) {
35470
- state.resting = false;
35597
+ async function send2(session, state, text) {
35471
35598
  let words = text;
35472
35599
  if (state.carryOn) {
35473
- state.carryOn = false;
35474
- state.carriedOn = true;
35475
35600
  const owed = state.owed;
35476
- state.owed = void 0;
35477
35601
  words = [owed, text].filter(Boolean).join("\n\n");
35478
35602
  text = [CARRY_ON, ...owed ? [OWED, owed] : [], text].filter(Boolean).join("\n\n");
35479
35603
  }
35604
+ const resting = state.resting;
35605
+ state.resting = false;
35606
+ try {
35607
+ await session.send(text);
35608
+ } catch (error) {
35609
+ state.resting = resting;
35610
+ throw error;
35611
+ }
35480
35612
  state.typed = words || void 0;
35481
- session.send(text);
35613
+ if (state.carryOn) {
35614
+ state.carryOn = false;
35615
+ state.carriedOn = true;
35616
+ state.owed = void 0;
35617
+ }
35482
35618
  }
35483
35619
  async function pump(session, state, rail, opts) {
35484
35620
  const clock5 = opts.beat ?? beat();
@@ -35618,9 +35754,6 @@ ${event.message}`, state, rail, { log: opts.log });
35618
35754
  opts.log("\u25A0 stopped");
35619
35755
  }
35620
35756
  return {
35621
- send(text) {
35622
- session?.send(text);
35623
- },
35624
35757
  working: () => running && state.resting !== true,
35625
35758
  // Paused by a usage limit, the activity line says so: the quiet place the pause is shown.
35626
35759
  tail: () => state.paused ? [pausedLine(state.paused)] : [...tail],
@@ -35631,6 +35764,8 @@ ${event.message}`, state, rail, { log: opts.log });
35631
35764
 
35632
35765
  // src/host.ts
35633
35766
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
35767
+ import { execSync } from "child_process";
35768
+ import { randomBytes } from "crypto";
35634
35769
  import { homedir as homedir8 } from "os";
35635
35770
  import { join as join10 } from "path";
35636
35771
 
@@ -36048,7 +36183,7 @@ var claimedRun = (sessionId2) => {
36048
36183
  const slot = sessionSlot(sessionId2);
36049
36184
  return { slot, key: runKey(slot) };
36050
36185
  };
36051
- var launchRun = (spec) => ({ session: spec.sessionId });
36186
+ var launchRun = (spec) => ({ session: spec.sessionId, model: spec.model });
36052
36187
  var BOUND_ELSEWHERE_MS = 10 * 6e4;
36053
36188
  var boundElsewhere = /* @__PURE__ */ new Map();
36054
36189
  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";
@@ -36156,7 +36291,7 @@ function startHost(opts) {
36156
36291
  });
36157
36292
  void heartbeat({
36158
36293
  version: VERSION,
36159
- harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status })),
36294
+ harnesses: detectAll().map((a) => ({ name: a.name, label: a.label, status: a.status, models: a.models })),
36160
36295
  workspaces: listWorkspaces(opts.wsDeps),
36161
36296
  // The repos inside them, so a session started for work on one opens IN it (Goal 26982211).
36162
36297
  repos: listRepos(opts.wsDeps)
@@ -36169,6 +36304,17 @@ function startHost(opts) {
36169
36304
  const specs = await claimSessions(asHost).catch(() => []);
36170
36305
  for (const spec of specs) {
36171
36306
  const label = spec.name ?? spec.harness;
36307
+ let workspace = spec.workspace;
36308
+ if (spec.worktree) {
36309
+ const suffix = randomBytes(4).toString("hex");
36310
+ const newWt = `/tmp/paigy-${spec.harness}-${suffix}`;
36311
+ try {
36312
+ execSync(`git worktree add ${newWt} -b paigy-${suffix}`, { cwd: workspace, stdio: "ignore" });
36313
+ workspace = newWt;
36314
+ } catch (e) {
36315
+ opts.log(`[${label}] Failed to create worktree, falling back to ${workspace}`);
36316
+ }
36317
+ }
36172
36318
  const { slot, key: key3 } = claimedRun(spec.sessionId);
36173
36319
  const log = (line) => opts.log(`[${label}] ${line}`);
36174
36320
  try {
@@ -36186,7 +36332,7 @@ function startHost(opts) {
36186
36332
  }
36187
36333
  const run = runHarness({
36188
36334
  harness: spec.harness,
36189
- cwd: spec.workspace,
36335
+ cwd: workspace,
36190
36336
  mode: "bypass",
36191
36337
  prompt: "",
36192
36338
  token: spec.token,
@@ -36641,7 +36787,7 @@ async function main() {
36641
36787
  }
36642
36788
  if (parsed.enableTools) {
36643
36789
  try {
36644
- execSync(`${ENABLE_TOOLS_COMMAND} --scope ${parsed.scope}`, { stdio: "inherit", shell: "/bin/sh" });
36790
+ execSync2(`${ENABLE_TOOLS_COMMAND} --scope ${parsed.scope}`, { stdio: "inherit", shell: "/bin/sh" });
36645
36791
  } catch {
36646
36792
  console.error(`Couldn't run it \u2014 try directly: ${ENABLE_TOOLS_COMMAND} --scope ${parsed.scope}`);
36647
36793
  process.exit(1);
@@ -36701,7 +36847,7 @@ async function main() {
36701
36847
  }
36702
36848
  if (process.platform === "darwin") {
36703
36849
  try {
36704
- execSync(`${JSON.stringify(process.execPath)} ${JSON.stringify(process.argv[1])} service`, { stdio: "inherit", shell: "/bin/sh" });
36850
+ execSync2(`${JSON.stringify(process.execPath)} ${JSON.stringify(process.argv[1])} service`, { stdio: "inherit", shell: "/bin/sh" });
36705
36851
  } catch {
36706
36852
  console.log("service install failed \u2014 run `paigy-harness host` to host in the foreground");
36707
36853
  }
@@ -36709,7 +36855,7 @@ async function main() {
36709
36855
  for (const t of MCP_WIRES) {
36710
36856
  if (!which(t.cli)) continue;
36711
36857
  try {
36712
- execSync(t.wire, { stdio: "ignore", shell: "/bin/sh" });
36858
+ execSync2(t.wire, { stdio: "ignore", shell: "/bin/sh" });
36713
36859
  if (t.cli === "codex") forwardCodexThreadId();
36714
36860
  console.log(`\u2713 paigy wired for ${t.cli} \u2014 each session hatches its own identity (restart open sessions)`);
36715
36861
  } catch (e) {
@@ -36718,7 +36864,7 @@ async function main() {
36718
36864
  }
36719
36865
  if (which("claude")) {
36720
36866
  try {
36721
- execSync(ENABLE_TOOLS_COMMAND, { stdio: "ignore", shell: "/bin/sh" });
36867
+ execSync2(ENABLE_TOOLS_COMMAND, { stdio: "ignore", shell: "/bin/sh" });
36722
36868
  console.log("\u2713 Paigy's tools allowlisted in Claude Code (they won't prompt each time)");
36723
36869
  } catch {
36724
36870
  console.log(` allowlist it later with: paigy-harness enable-tools`);
@@ -36791,7 +36937,7 @@ async function main() {
36791
36937
  mkdirSync4(dir, { recursive: true });
36792
36938
  const path = join11(dir, `${SERVICE_LABEL}.plist`);
36793
36939
  writeFileSync5(path, plist);
36794
- execSync(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
36940
+ execSync2(`launchctl unload ${path} 2>/dev/null; launchctl load ${path}`, { shell: "/bin/sh" });
36795
36941
  console.log(`\u2713 host installed as a login service (${path}) \u2014 logs at ~/.paigy/host.log`);
36796
36942
  console.log(" file limit raised to 65536 for the host and every session it spawns");
36797
36943
  return;