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

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 (53) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/prompts/master.md +10 -30
  4. package/prompts/worker.md +8 -12
  5. package/src/desk/ipc.ts +4 -3
  6. package/src/execution/agent-runner.ts +16 -129
  7. package/src/execution/delegation.ts +10 -32
  8. package/src/execution/live-run.ts +3 -14
  9. package/src/execution/pi-runner.ts +27 -162
  10. package/src/index.ts +3 -0
  11. package/src/lobby/feed.ts +1 -1
  12. package/src/lobby/host.ts +2 -0
  13. package/src/lobby/planner.ts +2 -2
  14. package/src/lobby/pr-review.ts +3 -3
  15. package/src/lobby/quickfix.ts +4 -3
  16. package/src/lobby/recovery.ts +2 -10
  17. package/src/lobby/runtime.ts +1 -1
  18. package/src/lobby/service.ts +3 -1
  19. package/src/master/goal-check.ts +1 -6
  20. package/src/master/master.ts +10 -22
  21. package/src/master/research.ts +3 -6
  22. package/src/pi/commands.ts +5 -45
  23. package/src/pi/delegation.ts +4 -4
  24. package/src/pi/events.ts +6 -19
  25. package/src/pi/grants.ts +89 -0
  26. package/src/pi/model-settings.ts +2 -4
  27. package/src/pi/model-support.ts +6 -6
  28. package/src/pi/route.ts +1 -1
  29. package/src/pi/run-summary.ts +3 -7
  30. package/src/pi/start-flags.ts +3 -3
  31. package/src/pi/start-task.ts +2 -8
  32. package/src/pi/tools.ts +8 -11
  33. package/src/roles/worker.ts +0 -18
  34. package/src/schemas/configuration.ts +47 -39
  35. package/src/schemas/findings.ts +0 -12
  36. package/src/schemas/task.ts +0 -1
  37. package/src/state/metrics.ts +1 -1
  38. package/src/state/work-clock.ts +140 -0
  39. package/src/state/work-time.ts +4 -7
  40. package/src/webui/api/index.ts +2 -1
  41. package/src/webui/api/settings.ts +6 -18
  42. package/src/webui/dev/fake-service.ts +14 -0
  43. package/src/webui/protocol.ts +23 -0
  44. package/src/workflow/workflow.ts +23 -318
  45. package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-CIfjkb5H.js} +1 -1
  46. package/webui/dist/assets/index-BkXhtB6A.css +2 -0
  47. package/webui/dist/assets/index-DIM2-FUQ.js +114 -0
  48. package/webui/dist/build.json +10 -6
  49. package/webui/dist/index.html +2 -2
  50. package/src/state/budget.ts +0 -301
  51. package/src/state/step-limits.ts +0 -41
  52. package/webui/dist/assets/index-Cca5uEVs.js +0 -115
  53. package/webui/dist/assets/index-DsVdXmh5.css +0 -2
@@ -8,6 +8,7 @@ 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";
@@ -39,7 +40,8 @@ export interface PiRunOptions {
39
40
  tools?: readonly string[];
40
41
  model?: string;
41
42
  thinking?: string;
42
- timeoutMs: number;
43
+ /** Names of the skills this agent may load; absent keeps pi's own discovery. */
44
+ skills?: string[];
43
45
  signal?: AbortSignal;
44
46
  /** Reports the current one-word tool activity while the agent works. */
45
47
  onActivity?: (activity: string) => void;
@@ -53,52 +55,21 @@ export interface PiRunOptions {
53
55
  stallTimeoutMs?: number;
54
56
  /** Silence allowed while one tool call is in flight (tests, builds); 0 disables. */
55
57
  toolStallTimeoutMs?: number;
56
- /** Ask the agent to wrap up and report at this elapsed time; 0 disables. */
57
- wrapUpAtMs?: number;
58
58
  /** Receives the steering handle once the process is running. */
59
59
  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
60
  /** Answers the agent's relayed questions; without it they are cancelled like any other dialog. */
63
61
  onAsk?: RelayAsk;
64
62
  }
65
63
 
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;
84
- }
85
-
86
64
  export interface ProcessOutcome {
87
65
  exitCode: number;
88
66
  stdout: string;
89
67
  stderr: string;
90
68
  killed: boolean;
91
- timedOut: boolean;
92
69
  /** Killed by the watchdog after going silent. */
93
70
  stalled?: boolean;
94
- /** The agent was asked to wrap up before its deadline. */
95
- wrappedUp?: boolean;
96
71
  /** The RPC host rejected the prompt itself. */
97
72
  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
73
  }
103
74
 
104
75
  /** Streaming agent events forwarded as they arrive on stdout. */
@@ -113,14 +84,9 @@ export type PiStreamEvent =
113
84
  | { type: "compaction" }
114
85
  | { type: "compaction_end"; error?: string }
115
86
  | { 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
87
  /** One finished thinking block, bounded to `MAX_THOUGHT_CHARS`. */
122
88
  | { type: "thought"; text: string }
123
- /** It asked the user (relayed); its clocks wait until the answers are back. */
89
+ /** It asked the user (relayed); the stall watchdog waits until the answers are back. */
124
90
  | { type: "asking"; questions: number }
125
91
  /** The answers went back after `waitedMs` of waiting on the user. */
126
92
  | { type: "answered"; waitedMs: number }
@@ -132,7 +98,6 @@ export const MAX_THOUGHT_CHARS = 1500;
132
98
  export interface ProcessRunOptions {
133
99
  cwd: string;
134
100
  signal?: AbortSignal;
135
- timeoutMs: number;
136
101
  /** The task message sent to the agent over RPC stdin. */
137
102
  prompt?: string;
138
103
  /** Optional sink for streaming agent events. */
@@ -140,12 +105,7 @@ export interface ProcessRunOptions {
140
105
  env?: Record<string, string>;
141
106
  stallTimeoutMs?: number;
142
107
  toolStallTimeoutMs?: number;
143
- wrapUpAtMs?: number;
144
- wrapUpMessage?: string;
145
108
  onStart?: (handle: RunHandle) => void;
146
- /** Grace between the deadline abort and the hard kill. */
147
- graceMs?: number;
148
- time?: TimeLimit;
149
109
  onAsk?: RelayAsk;
150
110
  }
151
111
 
@@ -162,9 +122,6 @@ export interface PiRunResult {
162
122
  usage: { input: number; output: number; cost: number; turns: number };
163
123
  model?: string;
164
124
  stalled?: boolean;
165
- wrappedUp?: boolean;
166
- timeUp?: boolean;
167
- extendedMs?: number;
168
125
  }
169
126
 
170
127
  export interface ParsedStream {
@@ -179,17 +136,8 @@ const MAX_STREAM_CHARS = 5_000_000;
179
136
  const HEARTBEAT_MS = 2000;
180
137
  const SETTLE_FALLBACK_MS = 3000;
181
138
  const EXIT_DRAIN_MS = 1500;
182
- const DEADLINE_GRACE_MS = 5000;
183
139
  const DIALOG_METHODS = new Set(["select", "confirm", "input", "editor"]);
184
140
 
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
141
  /** RPC protocol lines the process host reacts to (never forwarded to the caller). */
194
142
  export interface ControlEvent {
195
143
  type: string;
@@ -368,10 +316,15 @@ export function finishedThought(line: string): string | undefined {
368
316
  export const MCP_RESOURCE_TOOLS: readonly string[] = ["list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource"];
369
317
 
370
318
  /** 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[] {
319
+ export function buildPiArgs(options: Omit<PiRunOptions, "task"> & { systemPromptFile?: string; skillFiles?: string[] }): string[] {
372
320
  const args = ["--mode", "rpc", "--no-session", "--no-prompt-templates", "--no-themes", "--extension", fileURLToPath(new URL("../index.ts", import.meta.url))];
373
321
  if (options.model && options.model !== "inherit") args.push("--model", options.model);
374
322
  if (options.thinking) args.push("--thinking", options.thinking);
323
+ // A skill grant replaces pi's own discovery with exactly the granted skills.
324
+ if (options.skillFiles) {
325
+ args.push("--no-skills");
326
+ for (const file of options.skillFiles) args.push("--skill", file);
327
+ }
375
328
  if (options.tools && options.tools.length > 0) {
376
329
  // pi keeps every MCP tool under --tools unless an entry names one, so name the granted ones (and the resource
377
330
  // tools their servers need), or run without MCP at all when nothing is granted.
@@ -504,19 +457,11 @@ function rpcInput(proc: ChildProcess): { send(command: object): void; close(): v
504
457
 
505
458
  interface RunState {
506
459
  killed: boolean;
507
- timedOut: boolean;
508
460
  stalled: boolean;
509
- wrappedUp: boolean;
510
461
  settled: boolean;
511
462
  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. */
463
+ /** Relayed questions waiting on the user: the watchdog waits. */
518
464
  asking: number;
519
- extendedMs: number;
520
465
  }
521
466
 
522
467
  /** Smallest positive limit, used to pick a watchdog cadence that can see it. */
@@ -528,11 +473,11 @@ function watchInterval(...limits: Array<number | undefined>): number {
528
473
 
529
474
  /**
530
475
  * 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.
476
+ * The child leads its own process group so a stall or cancel takes its
477
+ * grandchildren (dev servers, watch-mode tests) with it, and the run ends on
478
+ * process exit rather than waiting for a pipe a grandchild may hold open.
479
+ * There is no time limit: a run ends when the agent finishes, is cancelled,
480
+ * or goes silent for so long it is killed as stalled.
536
481
  */
537
482
  export function spawnPiProcess(args: string[], options: ProcessRunOptions): Promise<ProcessOutcome> {
538
483
  return new Promise((resolve) => {
@@ -547,25 +492,16 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
547
492
  windowsHide: true,
548
493
  stdio: ["pipe", "pipe", "pipe"],
549
494
  });
550
- const state: RunState = { killed: false, timedOut: false, stalled: false, wrappedUp: false, settled: false, timeUp: false, headsUp: false, deciding: false, asking: 0, extendedMs: 0 };
495
+ const state: RunState = { killed: false, stalled: false, settled: false, asking: 0 };
551
496
  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;
497
+ let lastOutput = Date.now();
559
498
  let quietGraceUntil = 0;
560
499
  let toolsInFlight = 0;
561
500
  let stderr = "";
562
501
  let exitCode: number | undefined;
563
502
  let settleTimer: ReturnType<typeof setTimeout> | undefined;
564
503
  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
504
  let askStarted = 0;
568
- let pausedMs = 0;
569
505
  const asks = new AbortController();
570
506
 
571
507
  const onEvent = (event: PiStreamEvent) => {
@@ -574,51 +510,18 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
574
510
  if (event.type === "retry") quietGraceUntil = Date.now() + event.delayMs;
575
511
  options.onEvent?.(event);
576
512
  };
577
- // A turn that ended after the time ran out waits on the decision; any other ends the run.
513
+ // The run ends once its turn is over.
578
514
  let turnDone = false;
579
515
  const turnOver = () => {
580
516
  if (turnDone) return;
581
517
  turnDone = true;
582
518
  clearTimeout(settleTimer);
583
519
  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
- });
520
+ input.close();
615
521
  };
616
522
  const relay = (id: string | undefined, questions: AskQuestion[], ask: RelayAsk) => {
617
523
  state.asking += 1;
618
- if (state.asking === 1) {
619
- askStarted = Date.now();
620
- clearTimeout(deadline);
621
- }
524
+ if (state.asking === 1) askStarted = Date.now();
622
525
  options.onEvent?.({ type: "asking", questions: questions.length });
623
526
  ask(questions, asks.signal)
624
527
  .catch((): AskResult => ({ answers: [], cancelled: true }))
@@ -628,11 +531,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
628
531
  if (state.asking === 0) {
629
532
  const now = Date.now();
630
533
  const waited = now - askStarted;
631
- pausedMs += waited;
632
- deadlineAt += waited;
633
- clockStart += waited;
634
534
  lastOutput = now;
635
- if (!state.deciding) deadline = setTimeout(onDeadline, Math.max(0, deadlineAt - now));
636
535
  options.onEvent?.({ type: "answered", waitedMs: waited });
637
536
  }
638
537
  input.send({ type: "extension_ui_response", id, value: JSON.stringify(result) });
@@ -675,7 +574,6 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
675
574
  const finish = (code: number) => {
676
575
  if (done) return;
677
576
  done = true;
678
- clearTimeout(deadline);
679
577
  clearTimeout(settleTimer);
680
578
  clearInterval(watch);
681
579
  // Questions still open when the run ends are put away.
@@ -684,9 +582,8 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
684
582
  input.close();
685
583
  // Reap anything the agent left running in its group.
686
584
  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 } : {}) });
585
+ const { killed, stalled, protocolError } = state;
586
+ resolve({ exitCode: code, stdout: collector.finish(), stderr, killed, stalled, protocolError });
690
587
  };
691
588
  const onAbort = () => {
692
589
  state.killed = true;
@@ -694,31 +591,9 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
694
591
  input.close();
695
592
  terminate(proc, 3000);
696
593
  };
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
594
  const watch = setInterval(() => {
706
- if (state.killed || done || state.deciding || state.asking > 0) return;
595
+ if (state.killed || done || state.asking > 0) return;
707
596
  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
597
  const limit = toolsInFlight > 0 ? options.toolStallTimeoutMs : options.stallTimeoutMs;
723
598
  if (limit && limit > 0 && now - Math.max(lastOutput, quietGraceUntil) >= limit) {
724
599
  state.killed = true;
@@ -726,7 +601,7 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
726
601
  input.close();
727
602
  terminate(proc, 3000);
728
603
  }
729
- }, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs, options.wrapUpAtMs, time?.upAtMs, time?.headsUpAtMs));
604
+ }, watchInterval(options.stallTimeoutMs, options.toolStallTimeoutMs));
730
605
  watch.unref();
731
606
 
732
607
  proc.on("exit", (code, signal) => {
@@ -747,15 +622,11 @@ export function spawnPiProcess(args: string[], options: ProcessRunOptions): Prom
747
622
  }
748
623
 
749
624
  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 } : {}) };
625
+ const base = { output: parsed.text, usage: parsed.usage, model: parsed.model };
751
626
  if (outcome.stalled) {
752
627
  const quiet = shortDuration(options.stallTimeoutMs ?? 0);
753
628
  return { ...base, status: "timeout", stalled: true, error: `stalled: no output for ${quiet}` };
754
629
  }
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
630
  if (outcome.killed || aborted || parsed.stopReason === "aborted") {
760
631
  return { ...base, status: "cancelled", error: "agent was cancelled" };
761
632
  }
@@ -786,23 +657,17 @@ export async function runPiAgent(given: PiRunOptions, run: ProcessRunner = spawn
786
657
  try {
787
658
  prompt = writePromptFile([options.systemPrompt, host.note].filter(Boolean).join("\n\n"));
788
659
  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 });
660
+ const args = buildPiArgs({ ...options, tools: host.tools, mcpTools: host.mcpTools, systemPromptFile: prompt.file, ...(options.skills ? { skillFiles: skillFiles(options.skills, options.cwd) } : {}) });
790
661
  const outcome = await run(args, {
791
662
  cwd: options.cwd,
792
663
  signal: options.signal,
793
- timeoutMs: options.timeoutMs,
794
664
  prompt: `Task: ${options.task}`,
795
665
  env: host.env,
796
666
  stallTimeoutMs: options.stallTimeoutMs,
797
667
  toolStallTimeoutMs: options.toolStallTimeoutMs,
798
- wrapUpAtMs: options.wrapUpAtMs,
799
- ...(options.time ? { time: options.time } : {}),
800
668
  ...(options.onAsk ? { onAsk: options.onAsk } : {}),
801
669
  onStart: host.onStart,
802
670
  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
671
  if (event.type === "tool_execution_start") options.onActivity?.(activityWord(event.toolName));
807
672
  options.onEvent?.(event);
808
673
  },
package/src/index.ts CHANGED
@@ -21,11 +21,14 @@ import { registerFreshContext } from "./pi/fresh-context.ts";
21
21
  import { registerAskTool } from "./ask/tool.ts";
22
22
  import { registerWebTools } from "./web/tools.ts";
23
23
  import { registerToolBudget } from "./pi/tool-budget.ts";
24
+ import { bindGrants } from "./pi/grants.ts";
24
25
 
25
26
  export default function (pi: ExtensionAPI): void {
26
27
  registerAgentPermissions(pi);
27
28
  registerDelegation(pi);
28
29
  registerAutoCompaction(pi);
30
+ // Settings lists this session's MCP tools and skills to grant, and agents find their granted skills through it.
31
+ if (!isSubagentProcess()) bindGrants(pi);
29
32
  let root = detectProjectRoot(process.cwd(), CONFIG_DIR_NAME);
30
33
  pi.on("session_start", (_event, ctx) => { root = detectProjectRoot(ctx.cwd, CONFIG_DIR_NAME); });
31
34
  // First, so every session_start handler below finds the classifier bound to this session's keys.
package/src/lobby/feed.ts CHANGED
@@ -266,7 +266,7 @@ export class LobbyFeed {
266
266
  if (run.status !== "running") this.settleThought(key);
267
267
  if (run.status !== "running" && before !== run.status) {
268
268
  this.settle(undefined, key);
269
- const kind: ActivityKind = run.status === "success" ? (run.wrappedUp ? "warning" : "success") : run.status === "cancelled" ? "warning" : "error";
269
+ const kind: ActivityKind = run.status === "success" ? "success" : run.status === "cancelled" ? "warning" : "error";
270
270
  this.log(source, `${run.role} ${run.status === "success" ? "finished" : run.status}${run.error ? ` — ${run.error.split("\n")[0]}` : ""}`, kind, at);
271
271
  }
272
272
  }
package/src/lobby/host.ts CHANGED
@@ -148,6 +148,8 @@ export interface LobbyService {
148
148
  configChanged?(): void;
149
149
  /** The models Pi offers, for the settings page's picker. */
150
150
  models?(): ModelChoice[];
151
+ /** The MCP tools and skills pi offers to grant agents, for the settings page. */
152
+ grants?(): { mcpTools: Array<{ name: string; server: string; description: string; readOnly: boolean }>; skills: Array<{ name: string; description: string; path: string }> };
151
153
  /** The classifier models pi can call with its logins, for the classifier's picker. */
152
154
  classifierModels?(): Array<{ id: string; label: string }>;
153
155
  /** The model this window's session runs on (`provider/id`), when it has one. */
@@ -139,7 +139,7 @@ export interface PlannerDeps {
139
139
  configDir: string;
140
140
  /** The oracle chairing the panel (the Planner settings entry). */
141
141
  profile: () => QuickFixProfile;
142
- /** A seat's model, thinking and time limit; the oracle's profile when absent. */
142
+ /** A seat's model, thinking and grants; the oracle's profile when absent. */
143
143
  memberProfile?: (member: PanelMember) => QuickFixProfile;
144
144
  /** Seats on the panel when the session starts; every seat when absent. */
145
145
  panel?: readonly PanelMember[];
@@ -1015,7 +1015,7 @@ export class PlanningSession {
1015
1015
  if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
1016
1016
  this.deps.onChange?.();
1017
1017
  },
1018
- timeoutMs: profile.timeoutMs,
1018
+ ...(profile.skills ? { skills: profile.skills } : {}),
1019
1019
  signal,
1020
1020
  stallTimeoutMs: this.deps.stallTimeoutMs,
1021
1021
  toolStallTimeoutMs: this.deps.toolStallTimeoutMs,
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  /**
3
3
  * Reviewing pull requests from the Git tab. A read-only agent (on QA's model,
4
- * thinking and time limit) gets the pull request's description, changed files
4
+ * thinking and grants) gets the pull request's description, changed files
5
5
  * and diff from `gh`, may read the repository for context, and writes a
6
6
  * review: verdict, summary, findings, tests, questions. Jev can read a pull
7
7
  * request first, in a moment, and say whether a full review is worth its
@@ -74,7 +74,7 @@ export interface PullReviewDeps {
74
74
  root: string;
75
75
  configDir: string;
76
76
  exec: Exec;
77
- /** QA's model, thinking and time limit, read as a review starts. */
77
+ /** QA's model, thinking and grants, read as a review starts. */
78
78
  profile: () => QuickFixProfile;
79
79
  stallTimeoutMs?: number;
80
80
  toolStallTimeoutMs?: number;
@@ -285,7 +285,7 @@ export class PullReviews {
285
285
  if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
286
286
  this.deps.onChange?.();
287
287
  },
288
- timeoutMs: profile.timeoutMs,
288
+ ...(profile.skills ? { skills: profile.skills } : {}),
289
289
  signal,
290
290
  stallTimeoutMs: this.deps.stallTimeoutMs,
291
291
  toolStallTimeoutMs: this.deps.toolStallTimeoutMs,
@@ -89,9 +89,10 @@ export interface SubmitOptions {
89
89
  export interface QuickFixProfile {
90
90
  model?: string;
91
91
  thinking: string;
92
- timeoutMs: number;
93
92
  instructions?: string;
94
93
  mcpTools?: string[];
94
+ /** Skill names it may load; absent keeps every skill pi finds. */
95
+ skills?: string[];
95
96
  /** False when its settings turn the agent's auto-compaction off. */
96
97
  autoCompact?: boolean;
97
98
  /** Where the job goes when its model runs out of usage or is unavailable. */
@@ -101,7 +102,7 @@ export interface QuickFixDeps {
101
102
  cwd: string;
102
103
  root: string;
103
104
  configDir: string;
104
- /** Model, thinking and time limit for the next job, read from settings at start time. */
105
+ /** Model, thinking and grants for the next job, read from settings at start time. */
105
106
  profile: () => QuickFixProfile;
106
107
  stallTimeoutMs?: number;
107
108
  toolStallTimeoutMs?: number;
@@ -355,7 +356,7 @@ export class QuickFixQueue {
355
356
  if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
356
357
  this.changed();
357
358
  },
358
- timeoutMs: profile.timeoutMs,
359
+ ...(profile.skills ? { skills: profile.skills } : {}),
359
360
  signal: controller.signal,
360
361
  stallTimeoutMs: this.deps.stallTimeoutMs,
361
362
  toolStallTimeoutMs: this.deps.toolStallTimeoutMs,
@@ -16,7 +16,6 @@
16
16
  import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
17
17
  import { loadTask, ownedTask, saveTask } from "../state/persistence.ts";
18
18
  import { resolvePhaseTiming } from "../state/phase-timing.ts";
19
- import { updateBudget } from "../state/budget.ts";
20
19
  import { clearStaleRuns } from "../state/work-time.ts";
21
20
  import { livePresence } from "../state/presence.ts";
22
21
  import {
@@ -59,8 +58,8 @@ const INTERRUPTED_DECISION = "pi stopped unexpectedly; the task carried on in th
59
58
 
60
59
  /**
61
60
  * Clear what the stopped process left marked as under way on a task: the
62
- * questions it was waiting on (they no longer wait), the time given to the
63
- * steps it was running, and the runs it noted as active. A decision records
61
+ * questions it was waiting on (they no longer wait) and the runs it noted as
62
+ * active. A decision records
64
63
  * the stop, so the task's history says what happened.
65
64
  */
66
65
  export function settleInterrupted(root: string, configDir: string, taskId: string, now = new Date(), note = INTERRUPTED_DECISION): void {
@@ -71,13 +70,6 @@ export function settleInterrupted(root: string, configDir: string, taskId: strin
71
70
  task.decisions.push({ domain: "master", text: note, createdAt: at });
72
71
  task.updatedAt = at;
73
72
  saveTask(root, configDir, task);
74
- updateBudget(root, configDir, taskId, (budget) => {
75
- for (const allotment of budget.allotments) {
76
- if (allotment.endedAt) continue;
77
- allotment.endedAt = at;
78
- allotment.outcome = "stopped";
79
- }
80
- });
81
73
  clearStaleRuns(root, configDir, taskId);
82
74
  }
83
75
 
@@ -126,7 +126,7 @@ function handToQuickFix(state: Runtime, request: string, builder: Domain | undef
126
126
  const job = state.quickfix.submit(request, Date.now(), {
127
127
  force: true,
128
128
  ...(profile ? { profile } : {}),
129
- note: `The oracle sent your request here: ${reason.replace(/[.\s]+$/, "")}.${builder ? ` A quick feature: it runs on ${builder === "designer" ? "DESIGN" : "DEV"}'s model, thinking and time limit.` : ""}`,
129
+ note: `The oracle sent your request here: ${reason.replace(/[.\s]+$/, "")}.${builder ? ` A quick feature: it runs on ${builder === "designer" ? "DESIGN" : "DEV"}'s model, thinking and grants.` : ""}`,
130
130
  });
131
131
  lobbyTopics.bump("quickfix");
132
132
  pushNotice("The oracle sent your request to the quick-fix agent.", "info");
@@ -47,6 +47,7 @@ import { settleInterrupted, type Relaunch } from "./recovery.ts";
47
47
  import { carryOnMessage, resumeRoute, RESUMED_DECISION, type Drivers } from "./resume.ts";
48
48
  import { archivePlan as archivePlanOnDisk, deletePlan as deletePlanOnDisk, isPlanId, previousPlans, readPlan, type PlanRecord, type PlanSummary } from "../state/plan-history.ts";
49
49
  import { applyMasterModel } from "../pi/model-settings.ts";
50
+ import { grantableSkills, grantableTools } from "../pi/grants.ts";
50
51
  import { lobbyTopics } from "./topics.ts";
51
52
  import type { LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
52
53
  import type { LiveSession, LobbyService, SwitchTarget } from "./host.ts";
@@ -719,7 +720,7 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
719
720
  return `deleted ${taskId} for good`;
720
721
  }
721
722
 
722
- /** What a pull request review runs on: QA's model, thinking and time limit. */
723
+ /** What a pull request review runs on: QA's model, thinking and grants. */
723
724
  export function reviewProfile(state: Runtime) {
724
725
  return resolveReviewProfile(loadConfig(), {
725
726
  lookup: modelLookup(state.ctx),
@@ -988,6 +989,7 @@ export function createLobbyService(state: Runtime): LobbyService {
988
989
  }
989
990
  },
990
991
  classifierModels: () => availableClassifiers().map((model) => ({ id: model.id, label: model.label })),
992
+ grants: () => ({ mcpTools: grantableTools(state.pi), skills: grantableSkills(state.root, state.pi) }),
991
993
  issuesEnabled: () => loadConfig().lobby.issues,
992
994
  recovered: () => state.recovered,
993
995
  seatLabel: (member) => {
@@ -8,7 +8,7 @@
8
8
  import type { AgentRun } from "../schemas/findings.ts";
9
9
  import type { BotLobbyConfig, ProfileResolver } from "../schemas/configuration.ts";
10
10
  import { profileFor } from "../schemas/configuration.ts";
11
- import { runAgent, watchdogOptions, type AgentTime } from "../execution/agent-runner.ts";
11
+ import { runAgent, watchdogOptions } from "../execution/agent-runner.ts";
12
12
  import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
13
13
  import { loadPrompt } from "../prompts/loader.ts";
14
14
  import { truncate } from "../text.ts";
@@ -33,11 +33,8 @@ export interface GoalCheckRequest {
33
33
  profile?: ProfileResolver;
34
34
  signal?: AbortSignal;
35
35
  onUpdate?: (run: AgentRun) => void;
36
- time?: AgentTime;
37
36
  }
38
37
 
39
- /** At most this long: it reads, it does not test. */
40
- export const GOAL_CHECK_MS = 6 * 60 * 1000;
41
38
  /** Most of the change it reads; a longer diff is cut, and the check reads the files it needs. */
42
39
  export const GOAL_DIFF_CHARS = 60_000;
43
40
 
@@ -106,8 +103,6 @@ export async function runGoalCheck(request: GoalCheckRequest, run: ProcessRunner
106
103
  systemPrompt: goalCheckPrompt(profile.instructions),
107
104
  ...(profile.model ? { model: profile.model } : {}),
108
105
  thinking: profile.thinking,
109
- timeoutMs: Math.min(GOAL_CHECK_MS, profile.timeoutMs),
110
- ...(request.time ? { time: request.time } : {}),
111
106
  cwd: request.cwd,
112
107
  ...(request.signal ? { signal: request.signal } : {}),
113
108
  ...(request.onUpdate ? { onUpdate: (update: AgentRun) => request.onUpdate!({ ...update, note: update.note ?? "goal check" }) } : {}),