@a-t-h-i/bot-lobby 0.6.5 → 0.6.6

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.
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * One-word descriptions of the tool an agent is running: subagents show it as
3
- * `[spinner] word - elapsed`, and the oracle names the master's `orchestrate`
4
- * action. Pure lookup:
5
- * no clock, environment or I/O reads.
3
+ * `[spinner] word - elapsed`. Pure lookup: no clock, environment or I/O reads.
6
4
  */
7
5
  const ACTIVITY_WORDS: Record<string, string> = {
8
6
  read: "reading",
@@ -22,46 +20,16 @@ const ACTIVITY_WORDS: Record<string, string> = {
22
20
 
23
21
  const FALLBACK_WORD = "working";
24
22
 
25
- /** What the oracle says between its own tool calls while a master turn runs. */
26
- export const ORACLE_THINKING = "thinking";
27
-
28
23
  export function activityWord(toolName: string): string {
29
24
  return ACTIVITY_WORDS[toolName.trim().toLowerCase()] ?? FALLBACK_WORD;
30
25
  }
31
26
 
32
- /** Master `orchestrate` actions -> the word the oracle shows instead of the generic "orchestrating". */
33
- const ORACLE_ACTION_WORDS: Record<string, string> = {
34
- clarify: "asking",
35
- scout: "scouting",
36
- research: "researching",
37
- propose: "proposing",
38
- plan: "planning",
39
- implement: "delegating",
40
- qa: "reviewing",
41
- knowledge: "recording",
42
- compact: "recording",
43
- decide: "deciding",
44
- resolve_approval: "deciding",
45
- complete: "wrapping up",
46
- status: "checking",
47
- block: "blocking",
48
- resume: "resuming",
49
- cancel: "cancelling",
50
- };
51
-
52
27
  function orchestrateAction(args: unknown): string | undefined {
53
28
  if (!args || typeof args !== "object" || !("action" in args)) return undefined;
54
29
  const action = (args as { action?: unknown }).action;
55
30
  return typeof action === "string" ? action.trim().toLowerCase() : undefined;
56
31
  }
57
32
 
58
- /** One word for what the oracle (master) is doing; `orchestrate` maps by its action. */
59
- export function oracleActivityWord(toolName: string, args?: unknown): string {
60
- if (toolName.trim().toLowerCase() !== "orchestrate") return activityWord(toolName);
61
- const action = orchestrateAction(args);
62
- return (action && ORACLE_ACTION_WORDS[action]) || FALLBACK_WORD;
63
- }
64
-
65
33
  const DETAIL_CHARS = 28;
66
34
 
67
35
  function clip(text: string): string {
package/src/pi/events.ts CHANGED
@@ -7,8 +7,7 @@ import { selectKnowledge } from "../knowledge/selector.ts";
7
7
  import { cancelAllRuns } from "../execution/agent-runner.ts";
8
8
  import { describeTask } from "../workflow/workflow.ts";
9
9
  import { truncate } from "../text.ts";
10
- import { applyStatus, clearStatus, isMinimized, setMinimized, setOracleActivity } from "./ui.ts";
11
- import { ORACLE_THINKING, oracleActivityWord } from "./activity.ts";
10
+ import { applyStatus, clearStatus, isMinimized, setMinimized } from "./ui.ts";
12
11
  import { isSubagentProcess, webToolsFor } from "./quiet.ts";
13
12
  import { registerQuietTools } from "./tool-renderers.ts";
14
13
  import { taskRequest, type Task, type TaskState } from "../schemas/task.ts";
@@ -76,26 +75,16 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
76
75
  });
77
76
 
78
77
 
79
- // The oracle's speech bubble mirrors the master's own turn: thinking between
80
- // tool calls, the tool's word during one, and silent (your turn) once it ends.
81
- // Subagents report into runs instead.
82
- // Parallel tool calls: the newest still-running call keeps the word.
83
- const inFlight = new Map<string, string>();
84
- const showOracle = () => setOracleActivity([...inFlight.values()].at(-1) ?? ORACLE_THINKING);
85
78
  // A task's time budget counts while the oracle works on it, not while it waits on the user.
86
79
  const asking = new Set<string>();
87
80
  pi.on("agent_start", (_event, ctx) => {
88
81
  if (isSubagentProcess()) return;
89
- inFlight.clear();
90
- showOracle();
91
82
  const root = detectProjectRoot(ctx.cwd, configDir);
92
83
  const task = activeTask(root, configDir, ctx.sessionManager.getSessionId());
93
84
  if (task) startClock(root, configDir, task.id);
94
85
  });
95
86
  pi.on("tool_execution_start", (event) => {
96
87
  if (isSubagentProcess()) return;
97
- inFlight.set(event.toolCallId, oracleActivityWord(event.toolName, event.args));
98
- showOracle();
99
88
  if (ASKING_TOOLS.has(event.toolName) && !asking.has(event.toolCallId)) {
100
89
  asking.add(event.toolCallId);
101
90
  pauseClocks();
@@ -103,14 +92,10 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
103
92
  });
104
93
  pi.on("tool_execution_end", (event) => {
105
94
  if (isSubagentProcess()) return;
106
- inFlight.delete(event.toolCallId);
107
- showOracle();
108
95
  if (asking.delete(event.toolCallId)) resumeClocks();
109
96
  });
110
97
  pi.on("agent_end", () => {
111
98
  if (isSubagentProcess()) return;
112
- inFlight.clear();
113
- setOracleActivity(undefined);
114
99
  for (const _call of asking) resumeClocks();
115
100
  asking.clear();
116
101
  stopClocks();
@@ -0,0 +1,305 @@
1
+ /**
2
+ * The plan as a checklist: its steps parsed from the plan text, and each one
3
+ * done, current or pending from the worker runs so far. Pure.
4
+ */
5
+ import type { AgentRun } from "../schemas/findings.ts";
6
+
7
+ /** Upper bound on parsed plan steps so the checklist stays bounded. */
8
+ export const MAX_PLAN_STEPS = 50;
9
+
10
+ const PREFIX_CHARS = 32;
11
+ const MIN_PREFIX = 8;
12
+ const HEADER_LINE = /^\s*(?:#{1,6}\s+\S.*|\*\*[^*]+\*\*)\s*$/;
13
+ const STEP_SECTION = /sequence|steps|order/i;
14
+ const NUMBERED_STEP_LINE = /^(\s*)(\d+[.)])\s+(.*\S)\s*$/;
15
+ const BULLET_LINE = /^(\s*)([*-])\s+(.*\S)\s*$/;
16
+ const TOP_LEVEL_BULLET = /^()([*-])\s+(.*\S)\s*$/;
17
+ /** `### Step 2: Wire the API`, `**Step 2 — Wire the API**`, `Phase 3) Tests`: one plan step per heading. */
18
+ const STEP_HEADING = /^\s*(?:#{1,6}\s+)?(?:\*\*)?\s*(?:step|phase|stage)\s+#?\d+\s*(?:\*\*)?\s*[:.)\u2014\u2013-]\s*(?:\*\*)?\s*(.*?)\s*(?:\*\*)?\s*$/i;
19
+ const MIN_STEP_HEADINGS = 2;
20
+
21
+ export type PlanStepStatus = "done" | "current" | "pending";
22
+
23
+ export interface PlanStep {
24
+ text: string;
25
+ status: PlanStepStatus;
26
+ }
27
+
28
+ interface ListItem {
29
+ indent: number;
30
+ /** Column where the item's text starts; deeper items are nested under it. */
31
+ content: number;
32
+ text: string;
33
+ }
34
+
35
+ type ItemMatcher = (line: string) => ListItem | undefined;
36
+
37
+ function indentOf(line: string): number {
38
+ return /^\s*/.exec(line)![0].replace(/\t/g, " ").length;
39
+ }
40
+
41
+ function itemMatcher(pattern: RegExp): ItemMatcher {
42
+ return (line) => {
43
+ const match = pattern.exec(line);
44
+ if (!match) return undefined;
45
+ const indent = indentOf(match[1]!);
46
+ return { indent, content: indent + match[2]!.length + 1, text: match[3]! };
47
+ };
48
+ }
49
+
50
+ const numberedItem = itemMatcher(NUMBERED_STEP_LINE);
51
+ const bulletItem = itemMatcher(BULLET_LINE);
52
+ const topBulletItem = itemMatcher(TOP_LEVEL_BULLET);
53
+
54
+ /** Numbered `1.`/`1)` text, or a bullet inside a step section; undefined otherwise. */
55
+ function sectionItem(line: string): ListItem | undefined {
56
+ return numberedItem(line) ?? bulletItem(line);
57
+ }
58
+
59
+ /**
60
+ * Top-level list items only. As in CommonMark, an item indented to its parent's
61
+ * text column is a sub-point of that step, so nested bullets or a nested `1.`
62
+ * list never inflate the checklist. Prose at a shallower indent ends the parent.
63
+ */
64
+ function collectSteps(lines: readonly string[], accept: ItemMatcher): string[] {
65
+ const found: string[] = [];
66
+ let parent: number | undefined;
67
+ for (const line of lines) {
68
+ const item = accept(line);
69
+ if (!item) {
70
+ if (parent !== undefined && line.trim() && indentOf(line) < parent) parent = undefined;
71
+ continue;
72
+ }
73
+ if (parent !== undefined && item.indent >= parent) continue;
74
+ found.push(item.text);
75
+ parent = item.content;
76
+ }
77
+ return found;
78
+ }
79
+
80
+ /** Body of the first `sequence|steps|order` header, up to the next header; undefined when absent. */
81
+ function stepSection(lines: readonly string[]): string[] | undefined {
82
+ const start = lines.findIndex((line) => HEADER_LINE.test(line) && STEP_SECTION.test(line));
83
+ if (start < 0) return undefined;
84
+ const body: string[] = [];
85
+ for (const line of lines.slice(start + 1)) {
86
+ if (HEADER_LINE.test(line)) break;
87
+ body.push(line);
88
+ }
89
+ return body;
90
+ }
91
+
92
+ /** `Step N` headings, when the plan is structured as one heading per step. */
93
+ function headingSteps(lines: readonly string[]): string[] {
94
+ const found: string[] = [];
95
+ for (const line of lines) {
96
+ const match = STEP_HEADING.exec(line);
97
+ if (match) found.push(match[1] || line.replace(/[#*]/g, "").trim());
98
+ }
99
+ return found.length >= MIN_STEP_HEADINGS ? found : [];
100
+ }
101
+
102
+ /**
103
+ * Step texts from a free-form plan, capped. `Step N` headings win; then a
104
+ * `sequence|steps|order` section; then numbered lines; when none exists,
105
+ * top-level bullets are the last resort, so unrelated bullet lists under other
106
+ * headers never leak into the checklist. Only the shallowest items count.
107
+ */
108
+ export function planSteps(plan: string): string[] {
109
+ const lines = plan.split("\n");
110
+ const headings = headingSteps(lines);
111
+ if (headings.length > 0) return headings.slice(0, MAX_PLAN_STEPS);
112
+ const section = stepSection(lines);
113
+ if (section) {
114
+ const sectioned = collectSteps(section, sectionItem);
115
+ if (sectioned.length > 0) return sectioned.slice(0, MAX_PLAN_STEPS);
116
+ }
117
+ const numbered = collectSteps(lines, numberedItem);
118
+ if (numbered.length > 0) return numbered.slice(0, MAX_PLAN_STEPS);
119
+ return collectSteps(lines, topBulletItem).slice(0, MAX_PLAN_STEPS);
120
+ }
121
+
122
+ /* -------------------------------------------------------------------------
123
+ * Step matching. A worker instruction names its step explicitly ("Step 3: ...")
124
+ * or is scored against every step by shared paths, a shared opening phrase and
125
+ * word overlap. Plans reuse file paths across steps, so near-ties go to the
126
+ * earliest step still open instead of the first step that ever mentioned the
127
+ * path -- otherwise every later instruction re-matches step 1 and the tracker
128
+ * never moves.
129
+ * ---------------------------------------------------------------------- */
130
+
131
+ const STEP_REFERENCE = /\bsteps?\s*#?\s*(\d+)(?:\s*(?:-|\u2013|\u2014|to|through|thru|and|&)\s*#?\s*(\d+))?/gi;
132
+ /** Text allowed before a step reference that labels the instruction itself ("Now implement step 3:"). */
133
+ const LEADING_LABEL = /^[\s\W]*(?:(?:now|next|then|please|implement|do|complete|execute|start|begin|finish|work on|continue with|proceed with|plan)\s+)*(?:the\s+)?$/i;
134
+ const MIN_SCORE = 0.5;
135
+ const NEAR_TIE = 0.35;
136
+ const PATH_WEIGHT = 0.75;
137
+ const PREFIX_WEIGHT = 1;
138
+ const WORD = /[a-z0-9_][a-z0-9_./-]*[a-z0-9_]/g;
139
+ const MIN_WORD = 3;
140
+ const STOP_WORDS = new Set([
141
+ "the", "and", "for", "with", "that", "this", "from", "into", "then", "when", "each", "use", "make",
142
+ "sure", "new", "all", "any", "its", "are", "not", "but", "now", "via", "per", "our", "your", "you",
143
+ "has", "have", "will", "should", "must", "also", "only", "step", "steps", "plan", "approved",
144
+ "implement", "please", "add", "update", "change", "changes", "file", "files", "code",
145
+ ]);
146
+
147
+ function normalize(text: string): string {
148
+ return text.replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
149
+ }
150
+
151
+ function significantWords(text: string): Set<string> {
152
+ const words = new Set<string>();
153
+ for (const word of normalize(text).match(WORD) ?? []) {
154
+ if (word.length >= MIN_WORD && !STOP_WORDS.has(word)) words.add(word);
155
+ }
156
+ return words;
157
+ }
158
+
159
+ /** Scoring context for one instruction, built once per run and reused for every step. */
160
+ interface InstructionIndex {
161
+ text: string;
162
+ words: Set<string>;
163
+ }
164
+
165
+ function indexInstruction(instruction: string): InstructionIndex {
166
+ return { text: normalize(instruction), words: significantWords(instruction) };
167
+ }
168
+
169
+ function prefixMatches(step: string, hay: string): boolean {
170
+ const tail = normalize(step.replace(/`[^`]+`/g, " ").replace(/^[\s:;,.\u2014\u2013-]+/, ""));
171
+ return tail.length >= MIN_PREFIX && hay.includes(tail.slice(0, PREFIX_CHARS));
172
+ }
173
+
174
+ function pathShare(step: string, hay: string): number {
175
+ const paths = [...step.matchAll(/`([^`]+)`/g)].map((match) => normalize(match[1]!)).filter((path) => path.length > 0);
176
+ if (paths.length === 0) return 0;
177
+ return paths.filter((path) => hay.includes(path)).length / paths.length;
178
+ }
179
+
180
+ function wordShare(step: string, words: ReadonlySet<string>): number {
181
+ const own = significantWords(step);
182
+ if (own.size === 0) return 0;
183
+ let shared = 0;
184
+ for (const word of own) if (words.has(word)) shared += 1;
185
+ return shared / own.size;
186
+ }
187
+
188
+ /** How strongly an instruction targets one step; 0 when it shares nothing. */
189
+ function stepScore(step: string, instruction: InstructionIndex): number {
190
+ const prefix = prefixMatches(step, instruction.text) ? PREFIX_WEIGHT : 0;
191
+ return prefix + PATH_WEIGHT * pathShare(step, instruction.text) + wordShare(step, instruction.words);
192
+ }
193
+
194
+ /**
195
+ * Highest 0-based step an instruction labels itself with ("Step 3: ...", "steps 2-4"),
196
+ * or -1. A reference counts when it opens the instruction or is the only one
197
+ * named, so "building on step 1, now do step 3" does not jump back to step 1.
198
+ */
199
+ export function explicitStepIndex(instruction: string, count: number): number {
200
+ const refs = [...instruction.matchAll(STEP_REFERENCE)];
201
+ if (refs.length === 0) return -1;
202
+ const first = refs[0]!;
203
+ const leading = LEADING_LABEL.test(instruction.slice(0, first.index ?? 0)) ? first : undefined;
204
+ const distinct = new Set(refs.map((ref) => ref[0].toLowerCase().replace(/\s+/g, "")));
205
+ const chosen = leading ?? (distinct.size === 1 ? refs[0] : undefined);
206
+ if (!chosen) return -1;
207
+ const last = Math.max(Number(chosen[1]), Number(chosen[2] ?? chosen[1]));
208
+ return last >= 1 && last <= count ? last - 1 : -1;
209
+ }
210
+
211
+ /**
212
+ * The step an instruction targets, given the steps already completed; -1 when
213
+ * nothing matches. Among near-tied candidates the earliest open step wins.
214
+ */
215
+ export function targetStep(steps: readonly string[], instruction: string | undefined, completed: ReadonlySet<number> = new Set()): number {
216
+ if (!instruction || steps.length === 0) return -1;
217
+ const explicit = explicitStepIndex(instruction, steps.length);
218
+ if (explicit >= 0) return explicit;
219
+ const index = indexInstruction(instruction);
220
+ const scores = steps.map((step) => stepScore(step, index));
221
+ const best = Math.max(...scores);
222
+ if (best < MIN_SCORE) return -1;
223
+ const near = scores.findIndex((score, at) => score >= best - NEAR_TIE && score >= MIN_SCORE && !completed.has(at));
224
+ return near >= 0 ? near : scores.indexOf(best);
225
+ }
226
+
227
+ /** The latest worker run carrying an instruction; its step is the current one. */
228
+ export function latestWorkerRun(runs: readonly AgentRun[]): AgentRun | undefined {
229
+ let latest: AgentRun | undefined;
230
+ for (const run of runs) {
231
+ if (run.role !== "worker" || !run.instruction) continue;
232
+ if (!latest || Date.parse(run.startedAt) >= Date.parse(latest.startedAt)) latest = run;
233
+ }
234
+ return latest;
235
+ }
236
+
237
+ function stepStatus(index: number, current: number): PlanStepStatus {
238
+ if (current < 0) return index === 0 ? "current" : "pending";
239
+ if (index < current) return "done";
240
+ return index === current ? "current" : "pending";
241
+ }
242
+
243
+ /** Lowest index not yet in `completed`, or -1 when every step is. */
244
+ function nextOpenStep(steps: readonly string[], completed: ReadonlySet<number>): number {
245
+ return steps.findIndex((_text, index) => !completed.has(index));
246
+ }
247
+
248
+ /** Marks every step up to and including `index` as completed. */
249
+ function markThrough(completed: Set<number>, index: number): void {
250
+ for (let step = 0; step <= index; step += 1) completed.add(step);
251
+ }
252
+
253
+ function byStart(runs: readonly AgentRun[]): AgentRun[] {
254
+ const time = (run: AgentRun) => {
255
+ const at = Date.parse(run.startedAt);
256
+ return Number.isFinite(at) ? at : 0;
257
+ };
258
+ return runs
259
+ .map((run, order) => ({ run, order }))
260
+ .sort((a, b) => time(a.run) - time(b.run) || a.order - b.order)
261
+ .map((entry) => entry.run);
262
+ }
263
+
264
+ /**
265
+ * Replays worker runs in start order. A successful run completes everything up
266
+ * to its target step; a run that matches nothing -- the common case for a
267
+ * reworded instruction -- completes the next still-open step, so the count only
268
+ * grows and a failure can never tick one off. The latest worker run's own target
269
+ * is reported separately so a running or failed step reads current.
270
+ */
271
+ function replaySteps(steps: readonly string[], runs: readonly AgentRun[]): { completed: number; latest: number } {
272
+ const completed = new Set<number>();
273
+ const latestRun = latestWorkerRun(runs);
274
+ let latest = -1;
275
+ for (const run of byStart(runs)) {
276
+ if (run.role !== "worker") continue;
277
+ const matched = targetStep(steps, run.instruction, completed);
278
+ if (run === latestRun) latest = matched >= 0 && run.status === "success" ? matched + 1 : matched;
279
+ if (run.status !== "success") continue;
280
+ const target = matched >= 0 ? matched : nextOpenStep(steps, completed);
281
+ if (target >= 0) markThrough(completed, target);
282
+ }
283
+ return { completed: completed.size, latest };
284
+ }
285
+
286
+ /** One-entry memo: the panel asks for the same checklist several times per frame. */
287
+ let checklistMemo: { plan: string; runs: readonly AgentRun[]; steps: PlanStep[] } | undefined;
288
+
289
+ /**
290
+ * Done/current/pending per plan step. A succeeded run completes its step, so the
291
+ * next step becomes current (and the last step reads done); running, failed,
292
+ * cancelled and timeout runs keep it current. Progress is monotonic: steps
293
+ * already completed by successful worker runs stay done, and a later call that
294
+ * names an earlier step can never tick it back. Memoized on the exact plan text
295
+ * and runs array, so callers must not mutate either.
296
+ */
297
+ export function planChecklist(plan: string, runs: readonly AgentRun[]): PlanStep[] {
298
+ if (checklistMemo && checklistMemo.plan === plan && checklistMemo.runs === runs) return checklistMemo.steps;
299
+ const texts = planSteps(plan);
300
+ const replay = replaySteps(texts, runs);
301
+ const current = Math.max(replay.completed, replay.latest);
302
+ const steps = texts.map((text, index) => ({ text, status: stepStatus(index, current) }));
303
+ checklistMemo = { plan, runs, steps };
304
+ return steps;
305
+ }
@@ -6,16 +6,25 @@
6
6
  import type { AgentRun } from "../schemas/findings.ts";
7
7
  import type { RunLogEntry } from "../schemas/task.ts";
8
8
  import { shortDuration } from "../text.ts";
9
- import { SLOT_LABELS, type SlotId } from "./mascot-art.ts";
10
9
 
11
- /** The zen column a run belongs to; a researcher reports under RESEARCH from any domain. */
10
+ export type SlotId = "dev" | "design" | "research" | "qa";
11
+
12
+ /** Uppercase agent names. */
13
+ const SLOT_LABELS: Record<SlotId, string> = {
14
+ dev: "DEV",
15
+ design: "DESIGN",
16
+ research: "RESEARCH",
17
+ qa: "QA",
18
+ };
19
+
20
+ /** The agent slot a run belongs to; a researcher reports under RESEARCH from any domain. */
12
21
  export function slotOf(run: Pick<AgentRun, "domain" | "role">): SlotId {
13
22
  if (run.role === "researcher") return "research";
14
23
  if (run.domain === "backend") return "dev";
15
24
  return run.domain === "designer" ? "design" : "qa";
16
25
  }
17
26
 
18
- /** Upper-case agent name as the scene shows it (DEV, DESIGN, RESEARCH, QA). */
27
+ /** Upper-case agent name (DEV, DESIGN, RESEARCH, QA). */
19
28
  export function agentName(run: Pick<AgentRun, "domain" | "role">): string {
20
29
  return SLOT_LABELS[slotOf(run)];
21
30
  }
@@ -358,7 +358,7 @@ function entryDescription(kind: SettingsKind, view: EntryView): string {
358
358
  /** The lobby's on/off settings as the settings menu lists them; `panel:*` are the Lobby tab's panes. */
359
359
  export type LobbySwitch = "autoOpen" | "autoAsk" | "mouse" | "issues" | `panel:${LobbyPanel}`;
360
360
 
361
- const PANEL_SWITCH_LABELS: Record<LobbyPanel, string> = { animations: "Animations in the lobby", conversation: "Conversation pane", activity: "Activity log pane", thinking: "Thinking pane" };
361
+ const PANEL_SWITCH_LABELS: Record<LobbyPanel, string> = { conversation: "Conversation pane", activity: "Activity log pane", thinking: "Thinking pane" };
362
362
 
363
363
  export const LOBBY_SWITCHES: ReadonlyArray<{ id: LobbySwitch; label: string; help: string }> = [
364
364
  { id: "autoOpen", label: "Open with a task", help: "open the lobby when this session starts or resumes a task" },
@@ -368,7 +368,7 @@ export const LOBBY_SWITCHES: ReadonlyArray<{ id: LobbySwitch; label: string; hel
368
368
  ...LOBBY_PANELS.map((panel) => ({
369
369
  id: `panel:${panel}` as const,
370
370
  label: PANEL_SWITCH_LABELS[panel],
371
- help: panel === "animations" ? "the animated oracle and agents on the Lobby tab; off keeps only the task's status (they still show above pi's editor while the lobby is hidden); alt+z toggles it too" : "shown on the Lobby tab; its key in the lobby toggles it too",
371
+ help: "shown on the Lobby tab; its key in the lobby toggles it too",
372
372
  })),
373
373
  ];
374
374
 
package/src/pi/tools.ts CHANGED
@@ -29,17 +29,17 @@ const OrchestrateSchema = Type.Object({
29
29
  question: Type.Optional(Type.String({ description: "clarify: question for the user" })),
30
30
  options: Type.Optional(Type.Array(Type.String(), { description: "clarify: optional answer choices; put your recommended one first and mark it (Recommended)" })),
31
31
  domains: Type.Optional(Type.Array(Type.String(), { description: "scout: any of designer, backend, qa" })),
32
- instruction: Type.Optional(Type.String({ description: "scout/research: what to investigate (for scout, also used to target-verify a claim)" })),
32
+ instruction: Type.Optional(Type.String({ description: "scout/research: a self-contained brief: the specific questions, where to look, the answer format you want (paths, names, versions, evidence) and what you will do with it. The agent may be a small model: assume nothing" })),
33
33
  proposal: Type.Optional(Type.String({ description: "propose: the user-facing proposal as a short `- ` bullet list, one line per change" })),
34
34
  concerns: Type.Optional(Type.Array(Type.String(), { description: "propose: concerns raised while challenging the request" })),
35
- plan: Type.Optional(Type.String({ description: "plan: the detailed internal plan (while implementing or reviewing, the full revised plan that replaces it)" })),
35
+ plan: Type.Optional(Type.String({ description: "plan: the detailed internal plan (while implementing or reviewing, the full revised plan that replaces it). Every step names its files, its concrete actions and its done criteria, and the contracts between domains are written out; no decision is left to the workers" })),
36
36
  domain: Type.Optional(Type.String({ description: "implement/research: designer, backend, or qa" })),
37
- task: Type.Optional(Type.String({ description: "implement: the concrete step for that domain's worker" })),
37
+ task: Type.Optional(Type.String({ description: "implement: a self-contained brief for that domain's worker (which may be a small, literal model): Goal, exact Files, numbered What to do with names/shapes/values, Contracts, Constraints, Done when (checkable criteria and commands), If stuck. Decide everything yourself; leave nothing to be assumed" })),
38
38
  assignments: Type.Optional(
39
39
  Type.Array(
40
40
  Type.Object({
41
41
  domain: Type.String({ description: "designer, backend, or qa" }),
42
- task: Type.String({ description: "the concrete step(s) for that domain's worker" }),
42
+ task: Type.String({ description: "a self-contained brief for that domain's worker: Goal, exact Files, numbered What to do, Contracts (in full), Constraints, Done when, If stuck. No assumptions" }),
43
43
  minutes: Type.Optional(Type.Number({ description: "under a time budget: minutes for this worker, by its scope" })),
44
44
  }),
45
45
  { description: "implement: run several domains in parallel (distinct domains); workers share files through the file desk" },
@@ -77,6 +77,7 @@ const DESCRIPTION = [
77
77
  "pass), block/resume (escalate or continue), budget (under a time budget: where it stands, or ask the",
78
78
  "user for more minutes with a reason), track (the task's path and who takes part: show it, or correct it with",
79
79
  "track=fast|full, roster and a reason), status, cancel.",
80
+ "Every instruction you give an agent is read by a possibly smaller, cheaper model that cannot infer intent: write each one as a complete, explicit brief with the goal, files, actions, contracts, constraints and done criteria, then check the report against it.",
80
81
  "The engine validates every step against the task state machine, so a rejected action means the workflow is not at that step yet.",
81
82
  ].join(" ");
82
83