@a-t-h-i/bot-lobby 0.6.37 → 0.6.38

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
@@ -106,6 +106,18 @@ first, searches the codebase for missing information, and records verified findi
106
106
  agent's knowledge. Answers appear in the side pane; the **Chat / Docs** toggle inside the composer
107
107
  switches back to the selected document. Questions do not create tasks.
108
108
 
109
+ ## Scripts
110
+
111
+ Jobs that come back (seeding data, regenerating fixtures, a set of checks before review) are kept
112
+ as scripts in `.pi/bot-lobby/scripts/`, shared by every agent, so a step done once is run the next
113
+ time instead of being worked out again, which saves tokens and time. Each is named for what it
114
+ does, in snake_case with its language's extension (`seed_public_holidays.py`,
115
+ `run_changed_tests.sh`), and its first lines say what it is for and how to run it. The oracle
116
+ decides when a step is worth a script and saves it (`action=script`); workers only propose one, and
117
+ a replaced script is kept under `archive/`. For each step, Jev picks the scripts that do part of it
118
+ and the agent is told to run them (**Settings → Classifier → Shared scripts**; with Jev off, scripts
119
+ whose names share words with the step are suggested).
120
+
109
121
  ## Keys
110
122
 
111
123
  The lobby is keyboard-first, and every action button shows its key (`Archive E`, `Delete Del`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-t-h-i/bot-lobby",
3
- "version": "0.6.37",
3
+ "version": "0.6.38",
4
4
  "description": "Structured multi-agent software engineering orchestrator for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/prompts/master.md CHANGED
@@ -355,6 +355,31 @@ the relevant knowledge files: omit `taskId` to update project knowledge without
355
355
  creating or advancing a task. Answer from saved knowledge first, cite code
356
356
  evidence for newly learned facts, and never record a guess.
357
357
 
358
+ ## Scripts
359
+
360
+ The project keeps a shared scripts library that every agent can run; your
361
+ prompt lists it under Scripts, and each agent is shown the scripts that fit its
362
+ step. A script saves the tokens and time of working the same job out again, so
363
+ you decide when work becomes one.
364
+
365
+ - Make a script of a job that will come back in much the same form: seeding or
366
+ resetting data, regenerating fixtures or generated files, a migration run per
367
+ environment, a set of checks run before every review, a scaffold. Not of a
368
+ one-off change, and not of a single command anyone would type.
369
+ - When a step is such a job, say so in its brief: have the worker write the
370
+ step as a script, drafted in the task's directory under `scripts/`, and run
371
+ it. Then save it with `action=script`, `name`, `purpose`, `usage` and
372
+ `file` (the draft), or `text` (the script itself). Read it first: it runs
373
+ for every agent from then on.
374
+ - Name it for what it does, in snake_case, with its language's extension:
375
+ `seed_public_holidays.py`, `run_changed_tests.sh`. Prefer the project's own
376
+ language and runtime. Arguments over hard-coded values, so it serves the
377
+ next case too; and it says what it did, and fails loudly.
378
+ - Before you brief a step, look for a script that does it, and name it in the
379
+ brief. To improve one, save it again under the same name; the old version is
380
+ archived.
381
+ - Workers may propose scripts; take one only when it meets the bar above.
382
+
358
383
  ## Review
359
384
 
360
385
  The repository state is the source of truth; do not blindly trust Scout or
package/prompts/worker.md CHANGED
@@ -113,3 +113,14 @@ then add a `## Pushback` block to your output:
113
113
 
114
114
  The engine records the pushback and blocks that domain until the Master
115
115
  resolves it, so be specific and keep working on the rest of the task.
116
+
117
+ ## Shared scripts
118
+
119
+ Your prompt's Scripts section lists the shared scripts that fit your step. Run
120
+ one instead of doing its work by hand, and check its output as you would your
121
+ own; a script that does not do what it says is a finding for `## Notes`. Do not
122
+ edit the library yourself. When your brief asks you to write a step as a
123
+ script, or you wrote one for a job that will clearly come back, draft it in the
124
+ folder your Workflow Context names for script drafts, named for what it does in snake_case
125
+ (`seed_public_holidays.py`), run it, and propose it under `## Script
126
+ Proposals`.
@@ -9,6 +9,8 @@ import { appendMetrics } from "../state/metrics.ts";
9
9
  import { detectProjectRoot, loadConfig } from "../state/project.ts";
10
10
  import { isSubagentProcess } from "../pi/quiet.ts";
11
11
  import { Classifier } from "./classifier.ts";
12
+ import { scriptPicker } from "./scripts.ts";
13
+ import type { ScriptPicker } from "../scripts/pick.ts";
12
14
  import { registerJevProvider, type KeySource, type KeyStatus } from "./hosts.ts";
13
15
  import { fileHinter, type FileHinter, type FileScope } from "./files.ts";
14
16
  import { knowledgePicker, type KnowledgePicker } from "./knowledge.ts";
@@ -46,9 +48,15 @@ export function hintsFor(scope: FileScope): FileHinter {
46
48
  return fileHinter(classifier(), scope, (text) => lobbyFeed.log("CLASSIFIER", text, "info"));
47
49
  }
48
50
 
49
- /** Relevant knowledge for agents, with what Jev kept of each long file logged to the lobby's activity feed. */
51
+ /** Relevant knowledge and shared scripts for agents, with what Jev kept and matched logged to the lobby's activity feed. */
50
52
  export function knowledgeFor(): KnowledgePicker {
51
- return knowledgePicker(classifier(), (text) => lobbyFeed.log("CLASSIFIER", text, "info"));
53
+ const log = (text: string) => lobbyFeed.log("CLASSIFIER", text, "info");
54
+ return { ...knowledgePicker(classifier(), log), scripts: scriptPicker(classifier(), log) };
55
+ }
56
+
57
+ /** Shared scripts for a quick fix, with what Jev matched logged to the lobby's activity feed. */
58
+ export function scriptsPickerFor(): ScriptPicker {
59
+ return scriptPicker(classifier(), (text) => lobbyFeed.log("CLASSIFIER", text, "info"));
52
60
  }
53
61
 
54
62
  /** Triage a request for a task in a tree, logged to the lobby's activity feed; undefined when triage is off or fails. */
@@ -16,6 +16,7 @@ import { score, selectRelevant, splitSections, tokenize, type KnowledgeInput, ty
16
16
  import type { Classifier } from "./classifier.ts";
17
17
  import { noul, yesOf, type SystemOneRequest } from "./client.ts";
18
18
  import { clip } from "./limits.ts";
19
+ import type { ScriptPicker } from "../scripts/pick.ts";
19
20
 
20
21
  /** How many characters of each knowledge file a prompt has room for. */
21
22
  export const KNOWLEDGE_BUDGET_CHARS = 4000;
@@ -27,7 +28,7 @@ export const MAX_SECTIONS = 60;
27
28
  export const KNOWLEDGE_BUDGET_MS = 1500;
28
29
 
29
30
  /** The three files a prompt carries: `knowledge`, `standards` and `decisions`. */
30
- export type KnowledgeSlice = keyof KnowledgeSelection;
31
+ export type KnowledgeSlice = Exclude<keyof KnowledgeSelection, "scripts">;
31
32
  export type KnowledgePaths = Partial<Record<KnowledgeSlice, string>>;
32
33
 
33
34
  export interface SectionCall {
@@ -100,9 +101,11 @@ export function leftOutNote(kept: Kept, name: string, path?: string): string {
100
101
  : `_Kept ${kept.kept} of ${kept.total} sections of ${name} for this step.${where}_`;
101
102
  }
102
103
 
103
- /** Picks what an agent's prompt carries of its knowledge files. */
104
+ /** Picks what an agent's prompt carries of its knowledge files, and of the shared scripts. */
104
105
  export interface KnowledgePicker {
105
106
  select(task: string, files: KnowledgeInput, options?: { signal?: AbortSignal; paths?: KnowledgePaths }): Promise<KnowledgeSelection>;
107
+ /** Which shared scripts fit a step; shared words when absent. */
108
+ scripts?: ScriptPicker;
106
109
  }
107
110
 
108
111
  const SLICES: readonly KnowledgeSlice[] = ["knowledge", "standards", "decisions"];
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared scripts for a step. Jev reads each script's name and purpose against
3
+ * the step (one yes/no per script) and the ones that do part of it are put in
4
+ * the agent's prompt with how to run them, so the agent runs the script rather
5
+ * than working the job out again. A library longer than Jev is asked about is
6
+ * narrowed by shared words first. Off, failed or out of time, shared words
7
+ * decide (`scripts/pick.ts`).
8
+ */
9
+ import type { Classifier } from "./classifier.ts";
10
+ import { noul, yesOf, type SystemOneRequest } from "./client.ts";
11
+ import { clip } from "./limits.ts";
12
+ import { score, tokenize } from "../knowledge/selector.ts";
13
+ import { keywordScripts, MAX_SUGGESTED, type ScriptPicker } from "../scripts/pick.ts";
14
+ import { nameWords, type ScriptEntry } from "../scripts/library.ts";
15
+
16
+ /** Scripts Jev is asked about for one step. */
17
+ export const MAX_CANDIDATES = 40;
18
+ /** The step waits this long for Jev, then goes with shared words. */
19
+ export const SCRIPTS_BUDGET_MS = 1500;
20
+
21
+ export interface ScriptCall {
22
+ request: SystemOneRequest;
23
+ /** The script each question key stands for. */
24
+ keys: Map<string, ScriptEntry>;
25
+ }
26
+
27
+ /** The scripts to ask about: all of them, or the `MAX_CANDIDATES` that share the most words with the step. */
28
+ export function candidateScripts(step: string, entries: readonly ScriptEntry[]): ScriptEntry[] {
29
+ if (entries.length <= MAX_CANDIDATES) return [...entries];
30
+ const tokens = tokenize(step);
31
+ return entries
32
+ .map((entry, index) => ({ entry, index, hits: score(`${nameWords(entry.name)} ${entry.purpose}`, tokens) }))
33
+ .sort((a, b) => b.hits - a.hits || a.index - b.index)
34
+ .slice(0, MAX_CANDIDATES)
35
+ .map((scored) => scored.entry);
36
+ }
37
+
38
+ /** One request that asks, for each script, whether it does part of the step. */
39
+ export function scriptRequest(step: string, entries: readonly ScriptEntry[]): ScriptCall {
40
+ const scripts: Record<string, string> = {};
41
+ const questions: SystemOneRequest["questions"] = {};
42
+ const keys = new Map<string, ScriptEntry>();
43
+ entries.forEach((entry, position) => {
44
+ const key = `k${position + 1}`;
45
+ scripts[key] = clip(`${entry.name}: ${entry.purpose}`, 300);
46
+ questions[key] = noul(`Does the script \`scripts.${key}\` (its file name and purpose) do all or part of \`task\`, so an agent doing the task should run it instead of doing that work by hand? Yes when running it is a step the task needs; no when it is about something else or only shares words with it.`);
47
+ keys.set(key, entry);
48
+ });
49
+ return { request: { state: { task: clip(step, 3000), scripts }, questions }, keys };
50
+ }
51
+
52
+ /**
53
+ * A picker on Jev: the scripts it judges to do part of the step, most likely
54
+ * first. Shared words decide when it is off or cannot judge.
55
+ */
56
+ export function scriptPicker(classifier: Classifier, log?: (text: string) => void): ScriptPicker {
57
+ return {
58
+ async pick(step, entries, options = {}) {
59
+ if (entries.length === 0) return [];
60
+ if (!step.trim() || !classifier.enabled("scripts")) return keywordScripts(step, entries);
61
+ const call = scriptRequest(step, candidateScripts(step, entries));
62
+ const result = await classifier.ask("scripts", call.request, { ...(options.signal ? { signal: options.signal } : {}), timeoutMs: Math.min(classifier.config.timeoutMs, SCRIPTS_BUDGET_MS) });
63
+ if (!result) return keywordScripts(step, entries);
64
+ const judged = [...call.keys].map(([key, entry]) => ({ entry, yes: yesOf(result.answers, key) }));
65
+ // An answer to none of the questions says nothing: shared words decide.
66
+ if (judged.every((entry) => entry.yes === undefined)) return keywordScripts(step, entries);
67
+ const picked = judged
68
+ .filter((entry) => (entry.yes ?? 0) >= classifier.config.thresholds.scriptRelevantAt)
69
+ .sort((a, b) => b.yes! - a.yes!)
70
+ .slice(0, MAX_SUGGESTED)
71
+ .map((entry) => entry.entry);
72
+ log?.(`scripts: ${picked.length > 0 ? picked.map((entry) => entry.name).join(", ") : "none"} fit the step (of ${entries.length}) · ${result.ms} ms`);
73
+ return picked;
74
+ },
75
+ };
76
+ }
@@ -18,6 +18,8 @@ export interface AgentContext {
18
18
  standards?: string;
19
19
  knowledge?: string;
20
20
  decisions?: string;
21
+ /** The shared scripts that fit the step. */
22
+ scripts?: string;
21
23
  /** Per-agent custom instructions from the global config. */
22
24
  instructions?: string;
23
25
  workflowContext?: string;
@@ -9,6 +9,8 @@ export interface KnowledgeSelection {
9
9
  knowledge: string;
10
10
  standards: string;
11
11
  decisions: string;
12
+ /** The shared scripts that fit the step and how to run them (see `scripts/pick.ts`). */
13
+ scripts?: string;
12
14
  }
13
15
 
14
16
  export interface KnowledgeInput {
@@ -23,6 +23,8 @@ import { isAbsolute, join, relative } from "node:path";
23
23
  import type { LintConfig } from "../schemas/configuration.ts";
24
24
  import { repositoryTop } from "../execution/git.ts";
25
25
  import type { LintExec } from "../execution/lint.ts";
26
+ import { scriptsFor, type ScriptPicker } from "../scripts/pick.ts";
27
+ import { dataRoot } from "../state/project.ts";
26
28
  import { lintFeed, runLintGate } from "../workflow/lint.ts";
27
29
 
28
30
  /** A quick fix edits code, so it gets the full coding tool set. */
@@ -105,6 +107,8 @@ export interface QuickFixDeps {
105
107
  notify?: (message: string, level: "info" | "warning" | "error") => void;
106
108
  /** Likely files for the prompt, and the lookup tool, while the classifier's file hints are on. */
107
109
  hints?: FileHinter;
110
+ /** Which shared scripts fit the request; shared words when absent. */
111
+ scripts?: ScriptPicker;
108
112
  /** Holds a request it judges large (a task, not a quick fix) instead of spending a run on it. */
109
113
  classifier?: Classifier;
110
114
  /** Lowers thinking or the model for a request the classifier judges simple or trivial. */
@@ -298,12 +302,16 @@ export class QuickFixQueue {
298
302
  this.deps.feed?.log(QUICK_FIX_SOURCE, `started: ${jobTitle(job)}`, "info", job.startedAt);
299
303
  this.edits.set(job.id, new EditLog(this.deps.cwd));
300
304
  try {
301
- const [likely, route] = await Promise.all([this.likely(job.prompt, controller.signal), this.route(job.prompt, profile, controller.signal)]);
305
+ const [likely, route, scripts] = await Promise.all([
306
+ this.likely(job.prompt, controller.signal),
307
+ this.route(job.prompt, profile, controller.signal),
308
+ scriptsFor(dataRoot(this.deps.root, this.deps.configDir), job.prompt, this.deps.scripts, controller.signal),
309
+ ]);
302
310
  const quota = { used: MAX_CHILDREN };
303
311
  const attempt = (model: string | undefined, thinking: string) => runPiAgent(
304
312
  {
305
313
  cwd: this.deps.cwd,
306
- task: [job.prompt, job.interrupted ? INTERRUPTED_NOTE : "", likely].filter(Boolean).join("\n\n"),
314
+ task: [job.prompt, job.interrupted ? INTERRUPTED_NOTE : "", likely, scripts ? `## Scripts\n\n${scripts}` : ""].filter(Boolean).join("\n\n"),
307
315
  systemPrompt: quickFixPrompt(profile.instructions),
308
316
  tools: [...QUICK_FIX_TOOLS, ...(this.deps.hints?.tools() ?? [])],
309
317
  ...grantOption("quickfix"),
@@ -18,7 +18,7 @@ import { modelLookup } from "../pi/tools.ts";
18
18
  import { setQuickFixHandoff } from "../pi/route.ts";
19
19
  import type { Domain } from "../schemas/agent.ts";
20
20
  import { chatFromEntries, lobbyFeed, narrateEvent, type AgentEventLike } from "./feed.ts";
21
- import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
21
+ import { classifier, effortFor, hintsFor, scriptsPickerFor } from "../classifier/instance.ts";
22
22
  import { checkThinking } from "../pi/model-support.ts";
23
23
  import { QuickFixQueue } from "./quickfix.ts";
24
24
  import type { Asker } from "../ask/types.ts";
@@ -192,6 +192,7 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
192
192
  },
193
193
  notify: (message, level) => lobbyNotify(state, message, level),
194
194
  hints: hintsFor({ cwd: ctx.cwd, root, configDir }),
195
+ scripts: scriptsPickerFor(),
195
196
  classifier: classifier(),
196
197
  effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
197
198
  lint: () => loadConfig().lint,
@@ -7,7 +7,9 @@ import { domainSpec } from "../agents/registry.ts";
7
7
  import { boundedAgentTime, runAgent, runParallel, watchdogOptions, type AgentRequest, type AgentTime } from "../execution/agent-runner.ts";
8
8
  import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/pi-runner.ts";
9
9
  import { knowledgeFilePath, readAgentKnowledge, writeFileEnsured } from "../knowledge/store.ts";
10
+ import { taskDir } from "../knowledge/paths.ts";
10
11
  import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.ts";
12
+ import { scriptsFor } from "../scripts/pick.ts";
11
13
  import { summarizeOutcomes } from "./synthesis.ts";
12
14
  import { parseWorkerResult, validateWorkerResult } from "../roles/worker.ts";
13
15
  import { parseReviewResult, validateReviewResult } from "../roles/reviewer.ts";
@@ -73,9 +75,14 @@ function knowledgePaths(dataRoots: readonly string[], agent: Domain): KnowledgeP
73
75
  /**
74
76
  * What an agent's prompt carries of its knowledge for a step: Jev's picks from
75
77
  * the files that are too long when a picker is given, the keyword selection
76
- * otherwise and whenever the picker fails.
78
+ * otherwise and whenever the picker fails; and the shared scripts that fit it.
77
79
  */
78
80
  async function pickKnowledge(request: { knowledge?: KnowledgePicker; dataRoots: readonly string[]; signal?: AbortSignal }, agent: Domain, query: string): Promise<KnowledgeSelection> {
81
+ const [selected, scripts] = await Promise.all([pickFiles(request, agent, query), scriptsFor(request.dataRoots[0], query, request.knowledge?.scripts, request.signal)]);
82
+ return scripts ? { ...selected, scripts } : selected;
83
+ }
84
+
85
+ async function pickFiles(request: { knowledge?: KnowledgePicker; dataRoots: readonly string[]; signal?: AbortSignal }, agent: Domain, query: string): Promise<KnowledgeSelection> {
79
86
  const slices = readAgentKnowledge(request.dataRoots, agent);
80
87
  if (!request.knowledge) return selectKnowledge(query, slices);
81
88
  try {
@@ -271,6 +278,7 @@ function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
271
278
  request.risk ?? "",
272
279
  request.time?.note ?? "",
273
280
  request.ask ? askGuidance(request.ask.previews) : "",
281
+ request.dataRoots[0] ? `Scripts you draft for the shared library go in \`${join(taskDir(request.dataRoots[0], request.taskId), "scripts")}\`.` : "",
274
282
  ].filter(Boolean).join("\n\n");
275
283
  }
276
284
 
@@ -120,6 +120,8 @@ function orchestrateStep(args: unknown): string {
120
120
  return "recording knowledge";
121
121
  case "compact":
122
122
  return `compacting ${text("file") ?? "knowledge"}`;
123
+ case "script":
124
+ return `saving the shared script ${text("name") ?? ""}`.trimEnd();
123
125
  case "resolve_approval":
124
126
  return `${text("decision") === "rejected" ? "rejecting" : "approving"} ${text("approvalId") ?? "a request"}`;
125
127
  case "complete":
package/src/pi/events.ts CHANGED
@@ -4,6 +4,7 @@ import { detectProjectRoot, loadConfig, readDataRoots } from "../state/project.t
4
4
  import { compilePrompt } from "../prompts/compiler.ts";
5
5
  import { readAgentKnowledge } from "../knowledge/store.ts";
6
6
  import { selectKnowledge } from "../knowledge/selector.ts";
7
+ import { libraryBlock } from "../scripts/pick.ts";
7
8
  import { cancelAllRuns } from "../execution/agent-runner.ts";
8
9
  import { describeTask } from "../workflow/workflow.ts";
9
10
  import { truncate } from "../text.ts";
@@ -137,7 +138,8 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
137
138
  else delete event.systemPromptOptions.sections["bot-lobby"];
138
139
  return;
139
140
  }
140
- const slices = readAgentKnowledge(readDataRoots(root, configDir), "master");
141
+ const dataRoots = readDataRoots(root, configDir);
142
+ const slices = readAgentKnowledge(dataRoots, "master");
141
143
  const selected = selectKnowledge(`${taskRequest(task)} ${task.proposal ?? ""}`, slices);
142
144
  event.systemPromptOptions.sections["bot-lobby"] = compilePrompt({
143
145
  domain: "master",
@@ -145,6 +147,7 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
145
147
  standards: selected.standards,
146
148
  knowledge: selected.knowledge,
147
149
  decisions: selected.decisions,
150
+ scripts: dataRoots[0] ? libraryBlock(dataRoots[0]) : "",
148
151
  workflowContext: masterWorkflowContext(task, budgetContext(root, configDir, task)),
149
152
  instructions: loadConfig().master.instructions,
150
153
  });
package/src/pi/tools.ts CHANGED
@@ -59,9 +59,11 @@ const OrchestrateSchema = Type.Object({
59
59
  StringEnum(["fast", "full"] as const, { description: "track: fast (straight to the roster's agents, no scouts, proposal or plan; QA only for tests) or full (the whole workflow)" }),
60
60
  ),
61
61
  roster: Type.Optional(Type.Array(Type.String(), { description: "track: who takes part — any of designer, backend, qa, researcher" })),
62
- text: Type.Optional(Type.String({ description: "decide/complete/knowledge: the decision, completion summary, or knowledge text" })),
63
- file: Type.Optional(Type.String({ description: "compact: the knowledge file to rewrite, e.g. knowledge.md" })),
64
- name: Type.Optional(Type.String({ description: "whiteboard: name for the new Excalidraw session" })),
62
+ text: Type.Optional(Type.String({ description: "decide/complete/knowledge/script: the decision, completion summary, knowledge text, or the script itself" })),
63
+ file: Type.Optional(Type.String({ description: "compact: the knowledge file to rewrite, e.g. knowledge.md; script: a script a worker drafted, to save instead of text" })),
64
+ name: Type.Optional(Type.String({ description: "whiteboard: name for the new Excalidraw session; script: the file name, what it does in snake_case with its language's extension (seed_public_holidays.py, run_changed_tests.sh)" })),
65
+ purpose: Type.Optional(Type.String({ description: "script: one line on what the script does" })),
66
+ usage: Type.Optional(Type.String({ description: "script: the command that runs it, with its arguments" })),
65
67
  kind: Type.Optional(
66
68
  StringEnum(["knowledge", "standard", "decision", "completed"] as const, {
67
69
  description: "knowledge: which persistent file the text belongs to",
@@ -76,7 +78,7 @@ const DESCRIPTION = [
76
78
  "dependency), propose (record the proposal and request approval), plan (record the internal",
77
79
  "plan, or amend it later with the full revised plan), implement (delegate a step to a domain worker, or several domains in parallel with assignments), qa (final quality gate and the only",
78
80
  "review), knowledge (record approved project knowledge or a decision; works without a task),",
79
- "compact (replace a knowledge file with a rewritten version, archiving the old one; works without a task; disperse domain-relevant facts to action=knowledge (domain=designer|backend|qa) first), whiteboard (create your own Excalidraw session and assign it to yourself),",
81
+ "compact (replace a knowledge file with a rewritten version, archiving the old one; works without a task; disperse domain-relevant facts to action=knowledge (domain=designer|backend|qa) first), script (save a script to the shared scripts library, or replace one, for a job that comes back; works without a task), whiteboard (create your own Excalidraw session and assign it to yourself),",
80
82
  "resolve_approval (approve or reject a request), complete (declare the task done after the gates",
81
83
  "pass, then ask the user to confirm merging and pushing main and removing its worktree), block/resume (escalate or continue), budget (under a time budget: where it stands, or ask the",
82
84
  "user for more minutes with a reason), track (the task's path and who takes part: show it, or correct it with",
@@ -15,6 +15,8 @@ export interface CompileInput {
15
15
  standards?: string;
16
16
  knowledge?: string;
17
17
  decisions?: string;
18
+ /** The shared scripts that fit the step, or the library for the oracle. */
19
+ scripts?: string;
18
20
  workflowContext?: string;
19
21
  }
20
22
 
@@ -29,7 +31,7 @@ function section(title: string, body?: string): string {
29
31
 
30
32
  /**
31
33
  * Compose the layered prompt: global + domain + role + task context +
32
- * standards + knowledge + decisions + workflow context + output contract.
34
+ * standards + knowledge + decisions + scripts + workflow context + output contract.
33
35
  * Empty layers are dropped so agents never receive empty headings.
34
36
  */
35
37
  export function compilePrompt(input: CompileInput): string {
@@ -43,6 +45,7 @@ export function compilePrompt(input: CompileInput): string {
43
45
  section("Standards", input.standards),
44
46
  section("Knowledge", input.knowledge),
45
47
  section("Decisions", input.decisions),
48
+ section("Scripts", input.scripts),
46
49
  section("Workflow Context", input.workflowContext),
47
50
  spec?.contract ?? "",
48
51
  ];
@@ -1,6 +1,6 @@
1
1
  import type { Domain, RoleSpec } from "../schemas/agent.ts";
2
2
  import type { Blocker } from "../schemas/task.ts";
3
- import type { FileChange, KnowledgeProposal, WorkerResult } from "../schemas/findings.ts";
3
+ import type { FileChange, KnowledgeProposal, ScriptProposal, WorkerResult } from "../schemas/findings.ts";
4
4
  import { bullets, fieldValue, findSection, parseFileBullet, parsePushback, parseSections } from "./markdown.ts";
5
5
 
6
6
  /**
@@ -18,6 +18,7 @@ export const workerSpec: RoleSpec = {
18
18
  "Respond with exactly these sections and nothing else: `## Completed`, `## Files Changed`,",
19
19
  "`## Verification`, `## Brief Check` (one `- criterion — met|not met — evidence` line per \"Done when\" item of your brief), `## Notes`, `## Blockers`, `## Dependencies Needed`, `## Architecture Changes`,",
20
20
  "`## Knowledge Proposals` (`- knowledge: ...`, `- standard: ...`, or `- decision: ...`).",
21
+ "An optional `## Script Proposals` (`- \\`name.ext\\` — what it does — draft: \\`path\\``) proposes a script you wrote for a job that will come back, for the shared library.",
21
22
  "`## Files Changed` entries are `- \\`path\\` — change`.",
22
23
  "`## Verification` entries are `- command — result`.",
23
24
  "Blocked work uses `**Blocker:**`, `**Tried:**`, `**Need:**` under `## Blockers`.",
@@ -39,6 +40,18 @@ export function parseKnowledgeProposals(domain: Domain, text: string | undefined
39
40
  });
40
41
  }
41
42
 
43
+ /** Parse `` - `name.ext` — purpose — draft: `path` `` proposals; one without a name is dropped. */
44
+ export function parseScriptProposals(domain: Domain, text: string | undefined): ScriptProposal[] {
45
+ return bullets(text).flatMap((entry): ScriptProposal[] => {
46
+ const draftMatch = /\bdraft\s*:\s*`?([^`\s]+)`?/i.exec(entry);
47
+ const rest = (draftMatch ? entry.slice(0, draftMatch.index) : entry).replace(/[\s—–(-]+$/, "");
48
+ const named = /^`?([A-Za-z0-9_.-]+\.[A-Za-z0-9]+)`?\s*(?:[—–:-]+\s*)?(.*)$/.exec(rest.trim());
49
+ if (!named) return [];
50
+ const purpose = named[2]!.replace(/^[—–:-]+\s*/, "").trim();
51
+ return [{ domain, name: named[1]!, purpose, ...(draftMatch ? { draft: draftMatch[1]! } : {}) }];
52
+ });
53
+ }
54
+
42
55
  function splitList(text: string | undefined): string[] {
43
56
  return (text ?? "")
44
57
  .split(/[;\n]|\s+then\s+/)
@@ -100,6 +113,7 @@ export function parseWorkerResult(domain: Domain, raw: string, now = new Date().
100
113
  notes: findSection(sections, "notes") ?? "",
101
114
  blockers: parseBlockers(sections, domain, now),
102
115
  knowledgeProposals: parseKnowledgeProposals(domain, findSection(sections, "knowledge proposals")),
116
+ scriptProposals: parseScriptProposals(domain, findSection(sections, "script proposals")),
103
117
  pushback: parsePushback(sections),
104
118
  dependencyNeeds: realAsks(bullets(findSection(sections, "dependencies needed"))),
105
119
  architectureChanges: realAsks(bullets(findSection(sections, "architecture changes"))),
@@ -173,7 +173,7 @@ export interface LobbyConfig {
173
173
  }
174
174
 
175
175
  /** Decisions the classifier can make, each switched on or off on its own. */
176
- export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort", "review", "knowledge", "qa"] as const;
176
+ export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort", "review", "knowledge", "scripts", "qa"] as const;
177
177
  export type ClassifierFeature = (typeof CLASSIFIER_FEATURES)[number];
178
178
 
179
179
  /** Hosts that serve Jev behind the same System One API; `auto` takes OpenCode's free Jev when pi holds an OpenCode key, else TypeSafe. */
@@ -194,6 +194,8 @@ export interface ClassifierThresholds {
194
194
  fileRelevantAt: number;
195
195
  /** A section of a knowledge file that is over the prompt budget stays in at this relevance. */
196
196
  knowledgeRelevantAt: number;
197
+ /** A shared script is suggested for a step at this probability that it does part of it. */
198
+ scriptRelevantAt: number;
197
199
  /** A step scored simple at this confidence runs one thinking level lower. */
198
200
  simpleAt: number;
199
201
  /** A step scored trivial at this confidence runs on the cheaper model. */
@@ -294,7 +296,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
294
296
  model: "",
295
297
  baseUrl: "",
296
298
  timeoutMs: 4000,
297
- features: { seats: true, answers: true, files: true, triage: true, effort: true, review: true, knowledge: true, qa: true },
299
+ features: { seats: true, answers: true, files: true, triage: true, effort: true, review: true, knowledge: true, scripts: true, qa: true },
298
300
  thresholds: {
299
301
  seatAt: 0.35,
300
302
  reseatReadyAt: 0.6,
@@ -302,6 +304,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
302
304
  autoAnswerMargin: 0.5,
303
305
  fileRelevantAt: 0.5,
304
306
  knowledgeRelevantAt: 0.4,
307
+ scriptRelevantAt: 0.5,
305
308
  simpleAt: 0.7,
306
309
  trivialAt: 0.8,
307
310
  quickFixAt: 0.7,
@@ -24,6 +24,16 @@ export interface KnowledgeProposal {
24
24
  content: string;
25
25
  }
26
26
 
27
+ /** A script a worker thinks the shared library should keep, for the oracle to save or ignore. */
28
+ export interface ScriptProposal {
29
+ domain: Domain;
30
+ /** The file name it proposes: `seed_public_holidays.py`. */
31
+ name: string;
32
+ purpose: string;
33
+ /** Where the worker drafted it, when it did. */
34
+ draft?: string;
35
+ }
36
+
27
37
  export interface ScoutResult {
28
38
  domain: Domain;
29
39
  role: "scout";
@@ -51,6 +61,8 @@ export interface WorkerResult {
51
61
  notes: string;
52
62
  blockers: Blocker[];
53
63
  knowledgeProposals: KnowledgeProposal[];
64
+ /** Scripts the worker proposes for the shared library. */
65
+ scriptProposals?: ScriptProposal[];
54
66
  dependencyNeeds: string[];
55
67
  architectureChanges: string[];
56
68
  pushback?: Pushback;