@a-t-h-i/bot-lobby 0.6.54 → 0.6.56

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 (66) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +12 -1
  3. package/package.json +4 -1
  4. package/prompts/master.md +10 -30
  5. package/prompts/worker.md +8 -12
  6. package/src/desk/ipc.ts +4 -3
  7. package/src/execution/agent-runner.ts +20 -129
  8. package/src/execution/delegation.ts +10 -32
  9. package/src/execution/live-run.ts +3 -14
  10. package/src/execution/pi-runner.ts +36 -166
  11. package/src/harness/acp.ts +272 -0
  12. package/src/harness/catalog.ts +257 -0
  13. package/src/harness/claude-sdk.ts +169 -0
  14. package/src/harness/claude.ts +188 -0
  15. package/src/harness/cli-args.ts +62 -0
  16. package/src/harness/codex.ts +155 -0
  17. package/src/harness/instructions.ts +45 -0
  18. package/src/harness/model-manifest.json +829 -0
  19. package/src/harness/process.ts +175 -0
  20. package/src/harness/runner.ts +188 -0
  21. package/src/harness/status.ts +115 -0
  22. package/src/index.ts +3 -0
  23. package/src/lobby/feed.ts +1 -1
  24. package/src/lobby/host.ts +4 -0
  25. package/src/lobby/planner.ts +4 -3
  26. package/src/lobby/pr-review.ts +4 -3
  27. package/src/lobby/quickfix.ts +8 -4
  28. package/src/lobby/recovery.ts +2 -10
  29. package/src/lobby/runtime.ts +1 -1
  30. package/src/lobby/service.ts +3 -1
  31. package/src/master/goal-check.ts +1 -6
  32. package/src/master/master.ts +12 -24
  33. package/src/master/research.ts +4 -6
  34. package/src/pi/commands.ts +5 -45
  35. package/src/pi/delegation.ts +4 -4
  36. package/src/pi/events.ts +6 -19
  37. package/src/pi/grants.ts +89 -0
  38. package/src/pi/model-settings.ts +2 -4
  39. package/src/pi/model-support.ts +10 -6
  40. package/src/pi/route.ts +1 -1
  41. package/src/pi/run-summary.ts +3 -7
  42. package/src/pi/start-flags.ts +3 -3
  43. package/src/pi/start-task.ts +2 -8
  44. package/src/pi/tools.ts +8 -11
  45. package/src/roles/worker.ts +0 -18
  46. package/src/schemas/configuration.ts +73 -45
  47. package/src/schemas/findings.ts +0 -12
  48. package/src/schemas/harness.ts +122 -0
  49. package/src/schemas/task.ts +0 -1
  50. package/src/state/metrics.ts +1 -1
  51. package/src/state/work-clock.ts +140 -0
  52. package/src/state/work-time.ts +4 -7
  53. package/src/webui/api/index.ts +3 -1
  54. package/src/webui/api/settings.ts +12 -18
  55. package/src/webui/dev/fake-service.ts +27 -0
  56. package/src/webui/protocol.ts +32 -0
  57. package/src/workflow/workflow.ts +23 -318
  58. package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-BVehQ7yB.js} +1 -1
  59. package/webui/dist/assets/index-BKiKxUUG.js +114 -0
  60. package/webui/dist/assets/index-DOLQ4FDd.css +2 -0
  61. package/webui/dist/build.json +20 -12
  62. package/webui/dist/index.html +2 -2
  63. package/src/state/budget.ts +0 -301
  64. package/src/state/step-limits.ts +0 -41
  65. package/webui/dist/assets/index-Cca5uEVs.js +0 -115
  66. package/webui/dist/assets/index-DsVdXmh5.css +0 -2
@@ -8,10 +8,13 @@ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
8
  import { tmpdir } from "node:os";
9
9
  import { basename, join } from "node:path";
10
10
  import { activityWord } from "../pi/activity.ts";
11
+ import { skillFiles } from "../pi/grants.ts";
11
12
  import { shortDuration } from "../text.ts";
12
13
  import { readRelayRequest } from "../ask/relay.ts";
13
14
  import type { AskQuestion, AskResult } from "../ask/types.ts";
14
15
  import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
16
+ import type { HarnessRun } from "../schemas/harness.ts";
17
+ import { runHarnessAgent } from "../harness/runner.ts";
15
18
 
16
19
  /** Live control over one running subagent: queue a steering message for its next turn. */
17
20
  export interface RunHandle {
@@ -39,7 +42,8 @@ export interface PiRunOptions {
39
42
  tools?: readonly string[];
40
43
  model?: string;
41
44
  thinking?: string;
42
- timeoutMs: number;
45
+ /** Names of the skills this agent may load; absent keeps pi's own discovery. */
46
+ skills?: string[];
43
47
  signal?: AbortSignal;
44
48
  /** Reports the current one-word tool activity while the agent works. */
45
49
  onActivity?: (activity: string) => void;
@@ -53,34 +57,12 @@ export interface PiRunOptions {
53
57
  stallTimeoutMs?: number;
54
58
  /** Silence allowed while one tool call is in flight (tests, builds); 0 disables. */
55
59
  toolStallTimeoutMs?: number;
56
- /** Ask the agent to wrap up and report at this elapsed time; 0 disables. */
57
- wrapUpAtMs?: number;
58
60
  /** Receives the steering handle once the process is running. */
59
61
  onStart?: (handle: RunHandle) => void;
60
- /** Under a task time budget: when the agent is warned, when its time is up, and who decides on more (see ProcessRunOptions). */
61
- time?: TimeLimit;
62
62
  /** Answers the agent's relayed questions; without it they are cancelled like any other dialog. */
63
63
  onAsk?: RelayAsk;
64
- }
65
-
66
- /**
67
- * A run's allotted time under a task budget. At `headsUpAtMs` the agent is
68
- * told how much is left; at `upAtMs` it is steered to stop and report where it
69
- * left off. Once that report is in (its turn ended), `onTimeUp` decides: more
70
- * ms keeps the same process going with its context (a follow-up prompt), 0
71
- * ends it. The hard deadline (`timeoutMs`) and the stall watchdog wait while
72
- * the decision is pending, and move out with any time granted.
73
- */
74
- export interface TimeLimit {
75
- /** Reporting time is included in the allowance, including extensions. */
76
- hardLimit?: boolean;
77
- upAtMs: number;
78
- headsUpAtMs?: number;
79
- headsUpMessage?: string;
80
- upMessage: string;
81
- onTimeUp?: (report: string) => Promise<{ extraMs: number; message?: string }>;
82
- /** Time the agent gets to write its report after `upAtMs` before it is stopped. */
83
- graceMs: number;
64
+ /** Run on this third-party harness instead of pi (`model` is then ignored for the harness's own). */
65
+ harness?: HarnessRun;
84
66
  }
85
67
 
86
68
  export interface ProcessOutcome {
@@ -88,17 +70,10 @@ export interface ProcessOutcome {
88
70
  stdout: string;
89
71
  stderr: string;
90
72
  killed: boolean;
91
- timedOut: boolean;
92
73
  /** Killed by the watchdog after going silent. */
93
74
  stalled?: boolean;
94
- /** The agent was asked to wrap up before its deadline. */
95
- wrappedUp?: boolean;
96
75
  /** The RPC host rejected the prompt itself. */
97
76
  protocolError?: string;
98
- /** Stopped at its allotted time and not given more: its report says where it left off. */
99
- timeUp?: boolean;
100
- /** Time granted past its allotment, in ms. */
101
- extendedMs?: number;
102
77
  }
103
78
 
104
79
  /** Streaming agent events forwarded as they arrive on stdout. */
@@ -113,14 +88,9 @@ export type PiStreamEvent =
113
88
  | { type: "compaction" }
114
89
  | { type: "compaction_end"; error?: string }
115
90
  | { type: "usage"; input: number; output: number; cost: number; model?: string }
116
- | { type: "wrap_up" }
117
- /** Its allotted time is up: it was asked to stop and report where it left off. */
118
- | { type: "time_up" }
119
- /** It was given `ms` more and carries on. */
120
- | { type: "extended"; ms: number }
121
91
  /** One finished thinking block, bounded to `MAX_THOUGHT_CHARS`. */
122
92
  | { type: "thought"; text: string }
123
- /** It asked the user (relayed); its clocks wait until the answers are back. */
93
+ /** It asked the user (relayed); the stall watchdog waits until the answers are back. */
124
94
  | { type: "asking"; questions: number }
125
95
  /** The answers went back after `waitedMs` of waiting on the user. */
126
96
  | { type: "answered"; waitedMs: number }
@@ -132,7 +102,6 @@ export const MAX_THOUGHT_CHARS = 1500;
132
102
  export interface ProcessRunOptions {
133
103
  cwd: string;
134
104
  signal?: AbortSignal;
135
- timeoutMs: number;
136
105
  /** The task message sent to the agent over RPC stdin. */
137
106
  prompt?: string;
138
107
  /** Optional sink for streaming agent events. */
@@ -140,12 +109,7 @@ export interface ProcessRunOptions {
140
109
  env?: Record<string, string>;
141
110
  stallTimeoutMs?: number;
142
111
  toolStallTimeoutMs?: number;
143
- wrapUpAtMs?: number;
144
- wrapUpMessage?: string;
145
112
  onStart?: (handle: RunHandle) => void;
146
- /** Grace between the deadline abort and the hard kill. */
147
- graceMs?: number;
148
- time?: TimeLimit;
149
113
  onAsk?: RelayAsk;
150
114
  }
151
115
 
@@ -162,9 +126,6 @@ export interface PiRunResult {
162
126
  usage: { input: number; output: number; cost: number; turns: number };
163
127
  model?: string;
164
128
  stalled?: boolean;
165
- wrappedUp?: boolean;
166
- timeUp?: boolean;
167
- extendedMs?: number;
168
129
  }
169
130
 
170
131
  export interface ParsedStream {
@@ -179,17 +140,8 @@ const MAX_STREAM_CHARS = 5_000_000;
179
140
  const HEARTBEAT_MS = 2000;
180
141
  const SETTLE_FALLBACK_MS = 3000;
181
142
  const EXIT_DRAIN_MS = 1500;
182
- const DEADLINE_GRACE_MS = 5000;
183
143
  const DIALOG_METHODS = new Set(["select", "confirm", "input", "editor"]);
184
144
 
185
- /** The steering message sent at the wrap-up point. */
186
- export const WRAP_UP_MESSAGE = [
187
- "bot-lobby: you are close to your time limit.",
188
- "Stop exploring now. Finish or revert any half-done edit so every file is consistent,",
189
- "then write your final report in the required output format.",
190
- "List anything unfinished under Blockers or Notes.",
191
- ].join(" ");
192
-
193
145
  /** RPC protocol lines the process host reacts to (never forwarded to the caller). */
194
146
  export interface ControlEvent {
195
147
  type: string;
@@ -368,10 +320,15 @@ export function finishedThought(line: string): string | undefined {
368
320
  export const MCP_RESOURCE_TOOLS: readonly string[] = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
369
321
 
370
322
  /** Build the `pi` argv for one isolated, headless RPC agent run; the task goes over stdin. */
371
- export function buildPiArgs(options: Omit<PiRunOptions, "task"> & { systemPromptFile?: string }): string[] {
323
+ export function buildPiArgs(options: Omit<PiRunOptions, "task"> & { systemPromptFile?: string; skillFiles?: string[] }): string[] {
372
324
  const args = ["--mode", "rpc", "--no-session", "--no-prompt-templates", "--no-themes", "--extension", fileURLToPath(new URL("../index.ts", import.meta.url))];
373
325
  if (options.model && options.model !== "inherit") args.push("--model", options.model);
374
326
  if (options.thinking) args.push("--thinking", options.thinking);
327
+ // A skill grant replaces pi's own discovery with exactly the granted skills.
328
+ if (options.skillFiles) {
329
+ args.push("--no-skills");
330
+ for (const file of options.skillFiles) args.push("--skill", file);
331
+ }
375
332
  if (options.tools && options.tools.length > 0) {
376
333
  // pi keeps every MCP tool under --tools unless an entry names one, so name the granted ones (and the resource
377
334
  // tools their servers need), or run without MCP at all when nothing is granted.
@@ -449,10 +406,10 @@ export function isPiLauncher(script: string): boolean {
449
406
  return base === "pi" || base === "pi.js" || script.includes("pi-coding-agent");
450
407
  }
451
408
 
452
- const POSIX = process.platform !== "win32";
409
+ export const POSIX = process.platform !== "win32";
453
410
 
454
411
  /** Signal the child's whole process group (POSIX), so grandchildren go too. */
455
- function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
412
+ export function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
456
413
  if (proc.pid === undefined) return;
457
414
  if (POSIX) {
458
415
  try {
@@ -469,7 +426,7 @@ function signalGroup(proc: ChildProcess, signal: NodeJS.Signals): void {
469
426
  }
470
427
  }
471
428
 
472
- function terminate(proc: ChildProcess, graceMs = 5000): void {
429
+ export function terminate(proc: ChildProcess, graceMs = 5000): void {
473
430
  signalGroup(proc, "SIGTERM");
474
431
  const escalation = setTimeout(() => signalGroup(proc, "SIGKILL"), graceMs);
475
432
  escalation.unref();
@@ -504,23 +461,15 @@ function rpcInput(proc: ChildProcess): { send(command: object): void; close(): v
504
461
 
505
462
  interface RunState {
506
463
  killed: boolean;
507
- timedOut: boolean;
508
464
  stalled: boolean;
509
- wrappedUp: boolean;
510
465
  settled: boolean;
511
466
  protocolError?: string;
512
- /** Told its time is up; its next turn end waits on the decision. */
513
- timeUp: boolean;
514
- headsUp: boolean;
515
- /** Waiting on the decision: the agent is idle, so the deadline and the stall watchdog wait too. */
516
- deciding: boolean;
517
- /** Relayed questions waiting on the user: every clock and the watchdog wait. */
467
+ /** Relayed questions waiting on the user: the watchdog waits. */
518
468
  asking: number;
519
- extendedMs: number;
520
469
  }
521
470
 
522
471
  /** Smallest positive limit, used to pick a watchdog cadence that can see it. */
523
- function watchInterval(...limits: Array<number | undefined>): number {
472
+ export function watchInterval(...limits: Array<number | undefined>): number {
524
473
  const positive = limits.filter((limit): limit is number => typeof limit === "number" && limit > 0);
525
474
  const smallest = positive.length > 0 ? Math.min(...positive) : 4000;
526
475
  return Math.max(25, Math.min(1000, Math.floor(smallest / 4)));
@@ -528,11 +477,11 @@ function watchInterval(...limits: Array<number | undefined>): number {
528
477
 
529
478
  /**
530
479
  * Default runner: spawn pi in RPC mode, send the task over stdin, and watch it.
531
- * The child leads its own process group so a stall, deadline or cancel takes
532
- * its grandchildren (dev servers, watch-mode tests) with it, and the run ends
533
- * on process exit rather than waiting for a pipe a grandchild may hold open.
534
- * Near the deadline the agent is steered to wrap up and report; at the
535
- * deadline it is aborted; after prolonged silence it is killed as stalled.
480
+ * The child leads its own process group so a stall or cancel takes its
481
+ * grandchildren (dev servers, watch-mode tests) with it, and the run ends on
482
+ * process exit rather than waiting for a pipe a grandchild may hold open.
483
+ * There is no time limit: a run ends when the agent finishes, is cancelled,
484
+ * or goes silent for so long it is killed as stalled.
536
485
  */
537
486
  export function spawnPiProcess(args: string[], options: ProcessRunOptions): Promise<ProcessOutcome> {
538
487
  return new Promise((resolve) => {
@@ -547,25 +496,16 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
547
496
  windowsHide: true,
548
497
  stdio: ["pipe", "pipe", "pipe"],
549
498
  });
550
- const state: RunState = { killed: false, timedOut: false, stalled: false, wrappedUp: false, settled: false, timeUp: false, headsUp: false, deciding: false, asking: 0, extendedMs: 0 };
499
+ const state: RunState = { killed: false, stalled: false, settled: false, asking: 0 };
551
500
  const input = rpcInput(proc);
552
- const started = Date.now();
553
- const time = options.time;
554
- // The allotted time counts from here; a grant restarts it.
555
- let clockStart = started;
556
- let upAtMs = time?.upAtMs ?? 0;
557
- let headsUpAtMs = time?.headsUpAtMs ?? 0;
558
- let lastOutput = started;
501
+ let lastOutput = Date.now();
559
502
  let quietGraceUntil = 0;
560
503
  let toolsInFlight = 0;
561
504
  let stderr = "";
562
505
  let exitCode: number | undefined;
563
506
  let settleTimer: ReturnType<typeof setTimeout> | undefined;
564
507
  let done = false;
565
- // Time spent waiting on the user's answers is not the agent's: its limits move out by it.
566
- let deadlineAt = started + options.timeoutMs;
567
508
  let askStarted = 0;
568
- let pausedMs = 0;
569
509
  const asks = new AbortController();
570
510
 
571
511
  const onEvent = (event: PiStreamEvent) => {
@@ -574,51 +514,18 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
574
514
  if (event.type === "retry") quietGraceUntil = Date.now() + event.delayMs;
575
515
  options.onEvent?.(event);
576
516
  };
577
- // A turn that ended after the time ran out waits on the decision; any other ends the run.
517
+ // The run ends once its turn is over.
578
518
  let turnDone = false;
579
519
  const turnOver = () => {
580
520
  if (turnDone) return;
581
521
  turnDone = true;
582
522
  clearTimeout(settleTimer);
583
523
  settleTimer = undefined;
584
- if (state.timeUp && time?.onTimeUp && !done && !state.killed) decide(time.onTimeUp);
585
- else input.close();
586
- };
587
- const decide = (onTimeUp: NonNullable<TimeLimit["onTimeUp"]>) => {
588
- state.deciding = true;
589
- clearTimeout(deadline);
590
- const report = parsePiStream(collector.peek()).text;
591
- onTimeUp(report).then(({ extraMs, message }) => {
592
- if (done || state.killed) return;
593
- state.deciding = false;
594
- if (extraMs <= 0) return input.close();
595
- // Granted: the same agent carries on where it stopped, its context intact.
596
- state.timeUp = false;
597
- state.headsUp = false;
598
- state.settled = false;
599
- state.extendedMs += extraMs;
600
- turnDone = false;
601
- clockStart = Date.now();
602
- lastOutput = clockStart;
603
- upAtMs = time?.hardLimit ? Math.max(1, extraMs - Math.min(time.graceMs, Math.floor(extraMs / 4))) : extraMs;
604
- // The user just named the time it has: no heads-up quoting the old allotment.
605
- headsUpAtMs = 0;
606
- const nextLimit = extraMs + (time?.hardLimit ? 0 : time?.graceMs ?? 0);
607
- deadlineAt = Date.now() + nextLimit;
608
- deadline = setTimeout(onDeadline, nextLimit);
609
- input.send({ type: "prompt", message: message ?? "You have more time: carry on from where you left off and finish." });
610
- options.onEvent?.({ type: "extended", ms: extraMs });
611
- }, () => {
612
- state.deciding = false;
613
- input.close();
614
- });
524
+ input.close();
615
525
  };
616
526
  const relay = (id: string | undefined, questions: AskQuestion[], ask: RelayAsk) => {
617
527
  state.asking += 1;
618
- if (state.asking === 1) {
619
- askStarted = Date.now();
620
- clearTimeout(deadline);
621
- }
528
+ if (state.asking === 1) askStarted = Date.now();
622
529
  options.onEvent?.({ type: "asking", questions: questions.length });
623
530
  ask(questions, asks.signal)
624
531
  .catch((): AskResult => ({ answers: [], cancelled: true }))
@@ -628,11 +535,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
628
535
  if (state.asking === 0) {
629
536
  const now = Date.now();
630
537
  const waited = now - askStarted;
631
- pausedMs += waited;
632
- deadlineAt += waited;
633
- clockStart += waited;
634
538
  lastOutput = now;
635
- if (!state.deciding) deadline = setTimeout(onDeadline, Math.max(0, deadlineAt - now));
636
539
  options.onEvent?.({ type: "answered", waitedMs: waited });
637
540
  }
638
541
  input.send({ type: "extension_ui_response", id, value: JSON.stringify(result) });
@@ -675,7 +578,6 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
675
578
  const finish = (code: number) => {
676
579
  if (done) return;
677
580
  done = true;
678
- clearTimeout(deadline);
679
581
  clearTimeout(settleTimer);
680
582
  clearInterval(watch);
681
583
  // Questions still open when the run ends are put away.
@@ -684,9 +586,8 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
684
586
  input.close();
685
587
  // Reap anything the agent left running in its group.
686
588
  if (POSIX) signalGroup(proc, "SIGTERM");
687
- const { killed, timedOut, stalled, wrappedUp, protocolError } = state;
688
- const timeUp = state.timeUp && !killed;
689
- resolve({ exitCode: code, stdout: collector.finish(), stderr, killed, timedOut, stalled, wrappedUp, protocolError, ...(timeUp ? { timeUp } : {}), ...(state.extendedMs > 0 ? { extendedMs: state.extendedMs } : {}) });
589
+ const { killed, stalled, protocolError } = state;
590
+ resolve({ exitCode: code, stdout: collector.finish(), stderr, killed, stalled, protocolError });
690
591
  };
691
592
  const onAbort = () => {
692
593
  state.killed = true;
@@ -694,31 +595,9 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
694
595
  input.close();
695
596
  terminate(proc, 3000);
696
597
  };
697
- const onDeadline = () => {
698
- state.killed = true;
699
- state.timedOut = true;
700
- input.send({ type: "abort" });
701
- input.close();
702
- setTimeout(() => terminate(proc), options.graceMs ?? DEADLINE_GRACE_MS).unref();
703
- };
704
- let deadline = setTimeout(onDeadline, options.timeoutMs);
705
598
  const watch = setInterval(() => {
706
- if (state.killed || done || state.deciding || state.asking > 0) return;
599
+ if (state.killed || done || state.asking > 0) return;
707
600
  const now = Date.now();
708
- if (!state.wrappedUp && !state.settled && options.wrapUpAtMs && options.wrapUpAtMs > 0 && now - started - pausedMs >= options.wrapUpAtMs) {
709
- state.wrappedUp = true;
710
- input.send({ type: "steer", message: options.wrapUpMessage ?? WRAP_UP_MESSAGE });
711
- options.onEvent?.({ type: "wrap_up" });
712
- }
713
- if (time && !state.headsUp && !state.timeUp && !turnDone && headsUpAtMs > 0 && now - clockStart >= headsUpAtMs) {
714
- state.headsUp = true;
715
- if (time.headsUpMessage) input.send({ type: "steer", message: time.headsUpMessage });
716
- }
717
- if (time && !state.timeUp && !turnDone && upAtMs > 0 && now - clockStart >= upAtMs) {
718
- state.timeUp = true;
719
- input.send({ type: "steer", message: time.upMessage });
720
- options.onEvent?.({ type: "time_up" });
721
- }
722
601
  const limit = toolsInFlight > 0 ? options.toolStallTimeoutMs : options.stallTimeoutMs;
723
602
  if (limit && limit > 0 && now - Math.max(lastOutput, quietGraceUntil) >= limit) {
724
603
  state.killed = true;
@@ -726,7 +605,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
726
605
  input.close();
727
606
  terminate(proc, 3000);
728
607
  }
729
- }, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs, options.wrapUpAtMs, time?.upAtMs, time?.headsUpAtMs));
608
+ }, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs));
730
609
  watch.unref();
731
610
 
732
611
  proc.on("exit", (code, signal) => {
@@ -747,15 +626,11 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
747
626
  }
748
627
 
749
628
  function toResult(parsed: ParsedStream, outcome: ProcessOutcome, aborted: boolean, options: PiRunOptions): PiRunResult {
750
- const base = { output: parsed.text, usage: parsed.usage, model: parsed.model, wrappedUp: outcome.wrappedUp === true, ...(outcome.timeUp ? { timeUp: true } : {}), ...(outcome.extendedMs ? { extendedMs: outcome.extendedMs } : {}) };
629
+ const base = { output: parsed.text, usage: parsed.usage, model: parsed.model };
751
630
  if (outcome.stalled) {
752
631
  const quiet = shortDuration(options.stallTimeoutMs ?? 0);
753
632
  return { ...base, status: "timeout", stalled: true, error: `stalled: no output for ${quiet}` };
754
633
  }
755
- if (outcome.timedOut) {
756
- const note = outcome.wrappedUp ? " after a wrap-up request" : "";
757
- return { ...base, status: "timeout", error: `hit the ${shortDuration(options.timeoutMs)} time limit${note}` };
758
- }
759
634
  if (outcome.killed || aborted || parsed.stopReason === "aborted") {
760
635
  return { ...base, status: "cancelled", error: "agent was cancelled" };
761
636
  }
@@ -780,29 +655,24 @@ function writePromptFile(prompt: string): { file: string; cleanup: () => void }
780
655
 
781
656
  /** Run one isolated subagent and normalise its outcome. */
782
657
  export async function runPiAgent(given: PiRunOptions, run: ProcessRunner = spawnPiProcess): Promise<PiRunResult> {
658
+ if (given.harness) return runHarnessAgent({ ...given, harness: given.harness });
783
659
  const options = given.autoCompact === false ? { ...given, env: { ...given.env, [AUTO_COMPACT_ENV]: "0" } } : given;
784
660
  const host = await delegationHost(options, run);
785
661
  let prompt: ReturnType<typeof writePromptFile> | undefined;
786
662
  try {
787
663
  prompt = writePromptFile([options.systemPrompt, host.note].filter(Boolean).join("\n\n"));
788
664
  if (options.signal?.aborted) return { status: "cancelled", output: "", usage: { input: 0, output: 0, cost: 0, turns: 0 } };
789
- const args = buildPiArgs({ ...options, tools: host.tools, mcpTools: host.mcpTools, systemPromptFile: prompt.file });
665
+ const args = buildPiArgs({ ...options, tools: host.tools, mcpTools: host.mcpTools, systemPromptFile: prompt.file, ...(options.skills ? { skillFiles: skillFiles(options.skills, options.cwd) } : {}) });
790
666
  const outcome = await run(args, {
791
667
  cwd: options.cwd,
792
668
  signal: options.signal,
793
- timeoutMs: options.timeoutMs,
794
669
  prompt: `Task: ${options.task}`,
795
670
  env: host.env,
796
671
  stallTimeoutMs: options.stallTimeoutMs,
797
672
  toolStallTimeoutMs: options.toolStallTimeoutMs,
798
- wrapUpAtMs: options.wrapUpAtMs,
799
- ...(options.time ? { time: options.time } : {}),
800
673
  ...(options.onAsk ? { onAsk: options.onAsk } : {}),
801
674
  onStart: host.onStart,
802
675
  onEvent: (event) => {
803
- if (event.type === "asking") host.pause?.();
804
- if (event.type === "answered") host.extend?.(event.waitedMs);
805
- if (event.type === "extended") host.extend?.(event.ms, true);
806
676
  if (event.type === "tool_execution_start") options.onActivity?.(activityWord(event.toolName));
807
677
  options.onEvent?.(event);
808
678
  },