@paigy/harness 0.2.4 → 0.2.8

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.
package/README.md CHANGED
@@ -168,6 +168,15 @@ macOS launchd agent so hosting survives reboots — the host loop IS the service
168
168
  launchd just keeps it alive (the old "no daemon" note was about the WAKE channel,
169
169
  which still belongs to paigy-listen).
170
170
 
171
+ ## The window is not distributable (yet)
172
+
173
+ npm is the ONLY shipping channel: `@paigy/harness` installs the CLI and the whole
174
+ bridge, and updates with `npm i -g @paigy/harness@latest`. The Electron window runs
175
+ from source (`electron .`) — there is no packaging, no Developer ID signing or
176
+ notarization, and therefore no auto-update (macOS auto-update requires the signing).
177
+ A downloaded unsigned app would hit Gatekeeper's "damaged" dialog, which is worse
178
+ than no download button. Steps for when it's time: #854.
179
+
171
180
  ## Run it
172
181
 
173
182
  ```sh
package/dist/cli.js CHANGED
@@ -11631,10 +11631,10 @@ var require_headers = __commonJS({
11631
11631
  const lowercaseName = isLowerCase ? name : name.toLowerCase();
11632
11632
  const exists = this[kHeadersMap].get(lowercaseName);
11633
11633
  if (exists) {
11634
- const delimiter2 = lowercaseName === "cookie" ? "; " : ", ";
11634
+ const delimiter3 = lowercaseName === "cookie" ? "; " : ", ";
11635
11635
  this[kHeadersMap].set(lowercaseName, {
11636
11636
  name: exists.name,
11637
- value: `${exists.value}${delimiter2}${value}`
11637
+ value: `${exists.value}${delimiter3}${value}`
11638
11638
  });
11639
11639
  } else {
11640
11640
  this[kHeadersMap].set(lowercaseName, { name, value });
@@ -23091,7 +23091,7 @@ var require_lib = __commonJS({
23091
23091
  import { spawnSync } from "child_process";
23092
23092
  import { existsSync } from "fs";
23093
23093
  import { homedir } from "os";
23094
- import { join } from "path";
23094
+ import { delimiter, dirname, join } from "path";
23095
23095
  var CATALOG = [
23096
23096
  {
23097
23097
  name: "claude",
@@ -23115,7 +23115,13 @@ var CATALOG = [
23115
23115
  }
23116
23116
  ];
23117
23117
  function probeDirs(home = homedir()) {
23118
- return [join(home, ".local", "bin"), "/usr/local/bin", "/opt/homebrew/bin", join(home, ".npm-global", "bin")];
23118
+ return [
23119
+ dirname(process.execPath),
23120
+ join(home, ".local", "bin"),
23121
+ "/usr/local/bin",
23122
+ "/opt/homebrew/bin",
23123
+ join(home, ".npm-global", "bin")
23124
+ ];
23119
23125
  }
23120
23126
  function which(bin) {
23121
23127
  const hit = spawnSync("which", [bin], { encoding: "utf8" });
@@ -23132,7 +23138,11 @@ function runProbe(argv) {
23132
23138
  if (!cmd) return false;
23133
23139
  const resolved = which(cmd);
23134
23140
  if (!resolved) return false;
23135
- return spawnSync(resolved, args, { timeout: 1e4, stdio: "ignore" }).status === 0;
23141
+ return spawnSync(resolved, args, {
23142
+ timeout: 1e4,
23143
+ stdio: "ignore",
23144
+ env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) }
23145
+ }).status === 0;
23136
23146
  }
23137
23147
  function detect(entry, deps = {}) {
23138
23148
  const find = deps.which ?? which;
@@ -29735,7 +29745,12 @@ var UserAnswerSchema = external_exports.discriminatedUnion("kind", [
29735
29745
  external_exports.object({ kind: external_exports.literal("ranked"), optionIds: external_exports.array(external_exports.string()), labels: external_exports.array(external_exports.string()).optional() }),
29736
29746
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
29737
29747
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
29738
- external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) })
29748
+ external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
29749
+ /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
29750
+ * delivered through the same settle/await path as a human answer, carrying the judge's
29751
+ * derivation and the precedent ids it grew from. Always paired with a visible trail
29752
+ * card the user can reply to — the broker never overrides the user. */
29753
+ external_exports.object({ kind: external_exports.literal("precedent"), answer: external_exports.string(), derivation: external_exports.string(), sources: external_exports.array(external_exports.string()).min(1) })
29739
29754
  ]);
29740
29755
  var IntentSchema = external_exports.object({
29741
29756
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
@@ -29748,7 +29763,12 @@ var IntentSchema = external_exports.object({
29748
29763
  * "after lunch") against the agent machine's clock — the user's — and attaches
29749
29764
  * the seconds, ready to pass straight to schedule_callback. Absent when the
29750
29765
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
29751
- dueInSeconds: external_exports.number().int().positive().optional()
29766
+ dueInSeconds: external_exports.number().int().positive().optional(),
29767
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
29768
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
29769
+ * 'other' on every row. A table that records that something was wrong and nothing
29770
+ * about what cannot answer "is the bot looping less this week?". */
29771
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
29752
29772
  });
29753
29773
  var AwaitItemSchema = external_exports.discriminatedUnion("type", [
29754
29774
  external_exports.object({
@@ -29918,6 +29938,11 @@ var AgendaTurnSchema = external_exports.object({
29918
29938
  claimId: external_exports.string().optional(),
29919
29939
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
29920
29940
  voice: external_exports.string().optional(),
29941
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
29942
+ * whose request this is: an item that folded in from another agent arrived as a bare
29943
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
29944
+ * and the owner answered "What?". The bot names the agent before its first turn. */
29945
+ agent: external_exports.string().optional(),
29921
29946
  select: SelectShapeSchema.optional(),
29922
29947
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
29923
29948
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -31084,7 +31109,12 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
31084
31109
  external_exports.object({ kind: external_exports.literal("ranked"), optionIds: external_exports.array(external_exports.string()), labels: external_exports.array(external_exports.string()).optional() }),
31085
31110
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
31086
31111
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
31087
- external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) })
31112
+ external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
31113
+ /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
31114
+ * delivered through the same settle/await path as a human answer, carrying the judge's
31115
+ * derivation and the precedent ids it grew from. Always paired with a visible trail
31116
+ * card the user can reply to — the broker never overrides the user. */
31117
+ external_exports.object({ kind: external_exports.literal("precedent"), answer: external_exports.string(), derivation: external_exports.string(), sources: external_exports.array(external_exports.string()).min(1) })
31088
31118
  ]);
31089
31119
  var IntentSchema2 = external_exports.object({
31090
31120
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
@@ -31097,7 +31127,12 @@ var IntentSchema2 = external_exports.object({
31097
31127
  * "after lunch") against the agent machine's clock — the user's — and attaches
31098
31128
  * the seconds, ready to pass straight to schedule_callback. Absent when the
31099
31129
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
31100
- dueInSeconds: external_exports.number().int().positive().optional()
31130
+ dueInSeconds: external_exports.number().int().positive().optional(),
31131
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
31132
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
31133
+ * 'other' on every row. A table that records that something was wrong and nothing
31134
+ * about what cannot answer "is the bot looping less this week?". */
31135
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
31101
31136
  });
31102
31137
  var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
31103
31138
  external_exports.object({
@@ -31267,6 +31302,11 @@ var AgendaTurnSchema2 = external_exports.object({
31267
31302
  claimId: external_exports.string().optional(),
31268
31303
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
31269
31304
  voice: external_exports.string().optional(),
31305
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
31306
+ * whose request this is: an item that folded in from another agent arrived as a bare
31307
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
31308
+ * and the owner answered "What?". The bot names the agent before its first turn. */
31309
+ agent: external_exports.string().optional(),
31270
31310
  select: SelectShapeSchema2.optional(),
31271
31311
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
31272
31312
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -31735,7 +31775,8 @@ async function drainInput(state, deps = {}) {
31735
31775
  return [];
31736
31776
  }
31737
31777
  const ours = (parentId) => state.exclusive || parentId === state.parentId;
31738
- const claimable = (notificationId) => !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
31778
+ const opening = !!state.exclusive && !state.parentId;
31779
+ const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
31739
31780
  const mine = [
31740
31781
  ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
31741
31782
  ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
@@ -31768,7 +31809,10 @@ async function pump(session, state, deps = {}) {
31768
31809
  continue;
31769
31810
  }
31770
31811
  const words = spokenText(item.answer);
31771
- if (words) session.send(words);
31812
+ if (words) {
31813
+ state.resting = false;
31814
+ session.send(words);
31815
+ }
31772
31816
  }
31773
31817
  const keepGoing = await deps.onIdle?.() ?? false;
31774
31818
  if (!keepGoing) return;
@@ -31956,6 +32000,9 @@ function spokenText(answer) {
31956
32000
  // A call log: the user's replies are the instruction, the bot's prompts are not.
31957
32001
  case "turns":
31958
32002
  return answer.turns.map((t) => t.reply).join(" ").trim() || null;
32003
+ // An auto-answer derived from the user's past decisions — reads like a fast human.
32004
+ case "precedent":
32005
+ return answer.answer;
31959
32006
  case "ignored":
31960
32007
  return null;
31961
32008
  }
@@ -31967,7 +32014,7 @@ var clip = (s) => (s.length > 120 ? `${s.slice(0, 117)}\u2026` : s) || "(no text
31967
32014
  import { spawn } from "child_process";
31968
32015
  import { existsSync as existsSync3 } from "fs";
31969
32016
  import { homedir as homedir3 } from "os";
31970
- import { dirname, resolve, delimiter } from "path";
32017
+ import { resolve, delimiter as delimiter2 } from "path";
31971
32018
 
31972
32019
  // src/harness/acp.ts
31973
32020
  var none = { events: [], writes: [] };
@@ -32200,7 +32247,7 @@ function startSession(opts) {
32200
32247
  // PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
32201
32248
  env: {
32202
32249
  ...process.env,
32203
- PATH: [process.env.PATH, dirname(process.execPath), ...probeDirs()].filter(Boolean).join(delimiter),
32250
+ PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
32204
32251
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
32205
32252
  },
32206
32253
  stdio: ["pipe", "pipe", "pipe"]
@@ -32281,6 +32328,7 @@ function runHarness(opts) {
32281
32328
  state.parentId ??= parentId;
32282
32329
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
32283
32330
  if (event.kind === "idle") {
32331
+ state.resting = true;
32284
32332
  if (endsWithQuestion(event.result)) {
32285
32333
  const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
32286
32334
  if (local?.trim() && session && running) {
@@ -32329,7 +32377,7 @@ function runHarness(opts) {
32329
32377
 
32330
32378
  // src/workspaces.ts
32331
32379
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
32332
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
32380
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
32333
32381
  import { homedir as homedir4 } from "os";
32334
32382
  function workspacesFile() {
32335
32383
  return join3(homedir4(), ".paigy", "workspaces.json");
@@ -32347,7 +32395,7 @@ function addWorkspace(dir, deps) {
32347
32395
  const all = listWorkspaces(deps);
32348
32396
  if (!all.includes(next)) {
32349
32397
  all.push(next);
32350
- mkdirSync2(dirname2(deps.file), { recursive: true });
32398
+ mkdirSync2(dirname3(deps.file), { recursive: true });
32351
32399
  writeFileSync2(deps.file, JSON.stringify(all, null, 2));
32352
32400
  }
32353
32401
  return all;
@@ -32383,6 +32431,10 @@ function startHost(opts) {
32383
32431
  log
32384
32432
  });
32385
32433
  runs.set(spec.sessionId, { run, label });
32434
+ try {
32435
+ saveToken({ access_token: spec.token, name: label, device: null }, `session:${spec.sessionId.slice(0, 8)}`);
32436
+ } catch {
32437
+ }
32386
32438
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
32387
32439
  }
32388
32440
  }
package/dist/main.js CHANGED
@@ -4577,7 +4577,7 @@ var require_lib = __commonJS({
4577
4577
  import { app, BrowserWindow, dialog, ipcMain } from "electron";
4578
4578
  import { spawn as spawn2 } from "child_process";
4579
4579
  import { fileURLToPath } from "url";
4580
- import { dirname as dirname3, join as join4 } from "path";
4580
+ import { dirname as dirname4, join as join4 } from "path";
4581
4581
 
4582
4582
  // ../../packages/sdk/dist/index.js
4583
4583
  import { createRequire as __sdkCreateRequire } from "module";
@@ -11157,7 +11157,12 @@ var UserAnswerSchema = external_exports.discriminatedUnion("kind", [
11157
11157
  external_exports.object({ kind: external_exports.literal("ranked"), optionIds: external_exports.array(external_exports.string()), labels: external_exports.array(external_exports.string()).optional() }),
11158
11158
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
11159
11159
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
11160
- external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) })
11160
+ external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema).min(1) }),
11161
+ /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
11162
+ * delivered through the same settle/await path as a human answer, carrying the judge's
11163
+ * derivation and the precedent ids it grew from. Always paired with a visible trail
11164
+ * card the user can reply to — the broker never overrides the user. */
11165
+ external_exports.object({ kind: external_exports.literal("precedent"), answer: external_exports.string(), derivation: external_exports.string(), sources: external_exports.array(external_exports.string()).min(1) })
11161
11166
  ]);
11162
11167
  var IntentSchema = external_exports.object({
11163
11168
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
@@ -11170,7 +11175,12 @@ var IntentSchema = external_exports.object({
11170
11175
  * "after lunch") against the agent machine's clock — the user's — and attaches
11171
11176
  * the seconds, ready to pass straight to schedule_callback. Absent when the
11172
11177
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
11173
- dueInSeconds: external_exports.number().int().positive().optional()
11178
+ dueInSeconds: external_exports.number().int().positive().optional(),
11179
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
11180
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
11181
+ * 'other' on every row. A table that records that something was wrong and nothing
11182
+ * about what cannot answer "is the bot looping less this week?". */
11183
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
11174
11184
  });
11175
11185
  var AwaitItemSchema = external_exports.discriminatedUnion("type", [
11176
11186
  external_exports.object({
@@ -11340,6 +11350,11 @@ var AgendaTurnSchema = external_exports.object({
11340
11350
  claimId: external_exports.string().optional(),
11341
11351
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
11342
11352
  voice: external_exports.string().optional(),
11353
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
11354
+ * whose request this is: an item that folded in from another agent arrived as a bare
11355
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
11356
+ * and the owner answered "What?". The bot names the agent before its first turn. */
11357
+ agent: external_exports.string().optional(),
11343
11358
  select: SelectShapeSchema.optional(),
11344
11359
  options: external_exports.array(OptionSchema.omit({ id: true })).optional(),
11345
11360
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -12278,7 +12293,7 @@ import { hostname } from "os";
12278
12293
  import { spawnSync } from "child_process";
12279
12294
  import { existsSync as existsSync2 } from "fs";
12280
12295
  import { homedir as homedir2 } from "os";
12281
- import { join as join2 } from "path";
12296
+ import { delimiter, dirname, join as join2 } from "path";
12282
12297
  var CATALOG = [
12283
12298
  {
12284
12299
  name: "claude",
@@ -12302,7 +12317,13 @@ var CATALOG = [
12302
12317
  }
12303
12318
  ];
12304
12319
  function probeDirs(home = homedir2()) {
12305
- return [join2(home, ".local", "bin"), "/usr/local/bin", "/opt/homebrew/bin", join2(home, ".npm-global", "bin")];
12320
+ return [
12321
+ dirname(process.execPath),
12322
+ join2(home, ".local", "bin"),
12323
+ "/usr/local/bin",
12324
+ "/opt/homebrew/bin",
12325
+ join2(home, ".npm-global", "bin")
12326
+ ];
12306
12327
  }
12307
12328
  function which(bin) {
12308
12329
  const hit = spawnSync("which", [bin], { encoding: "utf8" });
@@ -12319,7 +12340,11 @@ function runProbe(argv) {
12319
12340
  if (!cmd) return false;
12320
12341
  const resolved = which(cmd);
12321
12342
  if (!resolved) return false;
12322
- return spawnSync(resolved, args, { timeout: 1e4, stdio: "ignore" }).status === 0;
12343
+ return spawnSync(resolved, args, {
12344
+ timeout: 1e4,
12345
+ stdio: "ignore",
12346
+ env: { ...process.env, PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter) }
12347
+ }).status === 0;
12323
12348
  }
12324
12349
  function detect(entry, deps = {}) {
12325
12350
  const find = deps.which ?? which;
@@ -12346,7 +12371,7 @@ function detectAll(deps = {}) {
12346
12371
 
12347
12372
  // src/workspaces.ts
12348
12373
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
12349
- import { dirname, join as join3, resolve } from "path";
12374
+ import { dirname as dirname2, join as join3, resolve } from "path";
12350
12375
  import { homedir as homedir3 } from "os";
12351
12376
  function workspacesFile() {
12352
12377
  return join3(homedir3(), ".paigy", "workspaces.json");
@@ -12364,14 +12389,14 @@ function addWorkspace(dir, deps) {
12364
12389
  const all = listWorkspaces(deps);
12365
12390
  if (!all.includes(next)) {
12366
12391
  all.push(next);
12367
- mkdirSync2(dirname(deps.file), { recursive: true });
12392
+ mkdirSync2(dirname2(deps.file), { recursive: true });
12368
12393
  writeFileSync2(deps.file, JSON.stringify(all, null, 2));
12369
12394
  }
12370
12395
  return all;
12371
12396
  }
12372
12397
  function removeWorkspace(dir, deps) {
12373
12398
  const all = listWorkspaces(deps).filter((w) => w !== dir);
12374
- mkdirSync2(dirname(deps.file), { recursive: true });
12399
+ mkdirSync2(dirname2(deps.file), { recursive: true });
12375
12400
  writeFileSync2(deps.file, JSON.stringify(all, null, 2));
12376
12401
  return all;
12377
12402
  }
@@ -12380,7 +12405,7 @@ function removeWorkspace(dir, deps) {
12380
12405
  import { spawn } from "child_process";
12381
12406
  import { existsSync as existsSync4 } from "fs";
12382
12407
  import { homedir as homedir4 } from "os";
12383
- import { dirname as dirname2, resolve as resolve2, delimiter } from "path";
12408
+ import { resolve as resolve2, delimiter as delimiter2 } from "path";
12384
12409
 
12385
12410
  // src/harness/acp.ts
12386
12411
  var none = { events: [], writes: [] };
@@ -12613,7 +12638,7 @@ function startSession(opts) {
12613
12638
  // PATH knows neither — live catch 2026-08-04: the first slot-wake ENOENT'd).
12614
12639
  env: {
12615
12640
  ...process.env,
12616
- PATH: [process.env.PATH, dirname2(process.execPath), ...probeDirs()].filter(Boolean).join(delimiter),
12641
+ PATH: [process.env.PATH, ...probeDirs()].filter(Boolean).join(delimiter2),
12617
12642
  ...opts.token ? { PAIGY_TOKEN: opts.token } : {}
12618
12643
  },
12619
12644
  stdio: ["pipe", "pipe", "pipe"]
@@ -12876,7 +12901,12 @@ var UserAnswerSchema2 = external_exports.discriminatedUnion("kind", [
12876
12901
  external_exports.object({ kind: external_exports.literal("ranked"), optionIds: external_exports.array(external_exports.string()), labels: external_exports.array(external_exports.string()).optional() }),
12877
12902
  external_exports.object({ kind: external_exports.literal("clarify"), chunks: external_exports.array(external_exports.string()).min(1) }),
12878
12903
  external_exports.object({ kind: external_exports.literal("confirm"), approved: external_exports.boolean() }),
12879
- external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) })
12904
+ external_exports.object({ kind: external_exports.literal("turns"), turns: external_exports.array(TurnSchema2).min(1) }),
12905
+ /** An auto-answer derived from the user's PAST decisions (broker/precedent-design.md §2):
12906
+ * delivered through the same settle/await path as a human answer, carrying the judge's
12907
+ * derivation and the precedent ids it grew from. Always paired with a visible trail
12908
+ * card the user can reply to — the broker never overrides the user. */
12909
+ external_exports.object({ kind: external_exports.literal("precedent"), answer: external_exports.string(), derivation: external_exports.string(), sources: external_exports.array(external_exports.string()).min(1) })
12880
12910
  ]);
12881
12911
  var IntentSchema2 = external_exports.object({
12882
12912
  // The full vocabulary the bot's mapper emits (mapper.INTENT_KINDS) — the schema lagged
@@ -12889,7 +12919,12 @@ var IntentSchema2 = external_exports.object({
12889
12919
  * "after lunch") against the agent machine's clock — the user's — and attaches
12890
12920
  * the seconds, ready to pass straight to schedule_callback. Absent when the
12891
12921
  * detail didn't parse (the agent interprets it) or the kind isn't defer. */
12892
- dueInSeconds: external_exports.number().int().positive().optional()
12922
+ dueInSeconds: external_exports.number().int().positive().optional(),
12923
+ /** Feedback only (#812): WHICH failure the complaint names — typed by the mapper that
12924
+ * already read the utterance, so `feedback_from_call.kind` stops defaulting to
12925
+ * 'other' on every row. A table that records that something was wrong and nothing
12926
+ * about what cannot answer "is the bot looping less this week?". */
12927
+ fault: external_exports.enum(["loop", "unanswered", "overridden", "misheard", "slow", "other"]).optional()
12893
12928
  });
12894
12929
  var AwaitItemSchema2 = external_exports.discriminatedUnion("type", [
12895
12930
  external_exports.object({
@@ -13059,6 +13094,11 @@ var AgendaTurnSchema2 = external_exports.object({
13059
13094
  claimId: external_exports.string().optional(),
13060
13095
  /** The claim's voice key (#462) — the OUTBOUND identity, audible who-is-asking. */
13061
13096
  voice: external_exports.string().optional(),
13097
+ /** The claim's AGENT NAME (#838) — the spoken identity. A voice alone doesn't say
13098
+ * whose request this is: an item that folded in from another agent arrived as a bare
13099
+ * non-sequitur ("First real production sign-in is yours to make whenever you want.")
13100
+ * and the owner answered "What?". The bot names the agent before its first turn. */
13101
+ agent: external_exports.string().optional(),
13062
13102
  select: SelectShapeSchema2.optional(),
13063
13103
  options: external_exports.array(OptionSchema2.omit({ id: true })).optional(),
13064
13104
  /** The seat was satisfied by the call's own ACCOUNT (replan-design.md, user_info): the
@@ -13527,7 +13567,8 @@ async function drainInput(state, deps = {}) {
13527
13567
  return [];
13528
13568
  }
13529
13569
  const ours = (parentId) => state.exclusive || parentId === state.parentId;
13530
- const claimable = (notificationId) => !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
13570
+ const opening = !!state.exclusive && !state.parentId;
13571
+ const claimable = (notificationId) => opening || state.resting === true || !state.sharedWithAgent || (state.mine?.has(notificationId) ?? false);
13531
13572
  const mine = [
13532
13573
  ...work.replies.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: r.answer })),
13533
13574
  ...work.requests.filter((r) => ours(r.parentId) && claimable(r.notificationId)).map((r) => ({ notificationId: r.notificationId, parentId: r.parentId, answer: { kind: "text", text: r.text } }))
@@ -13560,7 +13601,10 @@ async function pump(session, state, deps = {}) {
13560
13601
  continue;
13561
13602
  }
13562
13603
  const words = spokenText(item.answer);
13563
- if (words) session.send(words);
13604
+ if (words) {
13605
+ state.resting = false;
13606
+ session.send(words);
13607
+ }
13564
13608
  }
13565
13609
  const keepGoing = await deps.onIdle?.() ?? false;
13566
13610
  if (!keepGoing) return;
@@ -13737,6 +13781,9 @@ function spokenText(answer) {
13737
13781
  // A call log: the user's replies are the instruction, the bot's prompts are not.
13738
13782
  case "turns":
13739
13783
  return answer.turns.map((t) => t.reply).join(" ").trim() || null;
13784
+ // An auto-answer derived from the user's past decisions — reads like a fast human.
13785
+ case "precedent":
13786
+ return answer.answer;
13740
13787
  case "ignored":
13741
13788
  return null;
13742
13789
  }
@@ -13778,6 +13825,7 @@ function runHarness(opts) {
13778
13825
  state.parentId ??= parentId;
13779
13826
  if (event.kind === "turn") opts.log(`${event.role}: ${event.text.split("\n")[0] ?? ""}`);
13780
13827
  if (event.kind === "idle") {
13828
+ state.resting = true;
13781
13829
  if (endsWithQuestion(event.result)) {
13782
13830
  const local = await opts.localAsk?.question?.(event.result ?? "") ?? null;
13783
13831
  if (local?.trim() && session && running) {
@@ -13854,6 +13902,10 @@ function startHost(opts) {
13854
13902
  log: log2
13855
13903
  });
13856
13904
  runs.set(spec.sessionId, { run, label });
13905
+ try {
13906
+ saveToken({ access_token: spec.token, name: label, device: null }, `session:${spec.sessionId.slice(0, 8)}`);
13907
+ } catch {
13908
+ }
13857
13909
  opts.log(`\u25B6 phone-launched ${label} (${spec.harness}) in ${spec.workspace}`);
13858
13910
  }
13859
13911
  }
@@ -13914,7 +13966,7 @@ function startHost(opts) {
13914
13966
  }
13915
13967
 
13916
13968
  // src/main.ts
13917
- var here = dirname3(fileURLToPath(import.meta.url));
13969
+ var here = dirname4(fileURLToPath(import.meta.url));
13918
13970
  var wsDeps = { file: workspacesFile() };
13919
13971
  var DEVICE_SLOT = "Desktop";
13920
13972
  var deviceToken = () => readToken(DEVICE_SLOT) || readToken();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paigy/harness",
3
- "version": "0.2.4",
3
+ "version": "0.2.8",
4
4
  "description": "Run Claude Code / Codex on this machine, bridged to Paigy — launchable from your phone.",
5
5
  "license": "MIT",
6
6
  "type": "module",