@a-t-h-i/bot-lobby 0.6.46 → 0.6.48
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/THIRD_PARTY_NOTICES.md +15 -0
- package/package.json +17 -1
- package/prompts/designer.md +62 -44
- package/prompts/master.md +4 -3
- package/prompts/qa.md +12 -4
- package/prompts/reviewer.md +7 -1
- package/src/ask/tool.ts +2 -2
- package/src/execution/agent-runner.ts +3 -0
- package/src/execution/pi-runner.ts +5 -1
- package/src/knowledge/oracle-digest.ts +66 -0
- package/src/lobby/planner.ts +2 -0
- package/src/lobby/pr-review.ts +1 -0
- package/src/lobby/quickfix.ts +4 -0
- package/src/master/master.ts +4 -4
- package/src/master/research.ts +1 -0
- package/src/pi/auto-compaction.ts +47 -6
- package/src/pi/quiet.ts +3 -0
- package/src/roles/reviewer.ts +91 -13
- package/src/schemas/configuration.ts +19 -7
- package/src/schemas/findings.ts +2 -0
- package/src/webui/palette-core.ts +278 -1
- package/src/workflow/workflow.ts +3 -1
- package/webui/dist/assets/index-B5AnxwRU.css +2 -0
- package/webui/dist/assets/index-CznnCkjK.js +114 -0
- package/webui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/webui/dist/build.json +48 -28
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-BJ1fEle9.js +0 -110
- package/webui/dist/assets/index-BdtiIB9L.css +0 -2
- package/webui/dist/assets/oracle-CkL__6uz.png +0 -0
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -60,3 +60,18 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
60
60
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
61
61
|
SOFTWARE.
|
|
62
62
|
```
|
|
63
|
+
|
|
64
|
+
## Typefaces
|
|
65
|
+
|
|
66
|
+
The web UI bundles the Latin subset of these variable fonts (from the
|
|
67
|
+
fontsource packages, `@fontsource-variable/*`) as files in `webui/dist/assets`,
|
|
68
|
+
so the Typeface setting works offline. Each is under the SIL Open Font
|
|
69
|
+
License 1.1 (https://openfontlicense.org): Inter (The Inter Project Authors),
|
|
70
|
+
Geist and Geist Mono (Vercel), IBM Plex Sans (IBM Corp.), Manrope (The
|
|
71
|
+
Manrope Project Authors), JetBrains Mono (The JetBrains Mono Project
|
|
72
|
+
Authors), DM Sans (The DM Sans Project Authors), Plus Jakarta Sans (The Plus
|
|
73
|
+
Jakarta Sans Project Authors), Figtree (The Figtree Project Authors), Outfit
|
|
74
|
+
(The Outfit Project Authors), Nunito (The Nunito Project Authors), Space
|
|
75
|
+
Grotesk (The Space Grotesk Project Authors), Source Sans 3 and Source Code Pro
|
|
76
|
+
(Adobe), Fira Code (The Fira Code Project Authors) and Roboto Mono (The Roboto
|
|
77
|
+
Mono Project Authors).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a-t-h-i/bot-lobby",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.48",
|
|
4
4
|
"description": "Structured multi-agent software engineering orchestrator for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -57,6 +57,22 @@
|
|
|
57
57
|
"@earendil-works/pi-ai": "1.0.1",
|
|
58
58
|
"@earendil-works/pi-coding-agent": "1.0.1",
|
|
59
59
|
"@earendil-works/pi-tui": "1.0.1",
|
|
60
|
+
"@fontsource-variable/dm-sans": "^5.3.0",
|
|
61
|
+
"@fontsource-variable/figtree": "^5.3.0",
|
|
62
|
+
"@fontsource-variable/fira-code": "^5.3.0",
|
|
63
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
64
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
65
|
+
"@fontsource-variable/ibm-plex-sans": "^5.3.0",
|
|
66
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
67
|
+
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
|
68
|
+
"@fontsource-variable/manrope": "^5.3.0",
|
|
69
|
+
"@fontsource-variable/nunito": "^5.3.0",
|
|
70
|
+
"@fontsource-variable/outfit": "^5.3.0",
|
|
71
|
+
"@fontsource-variable/plus-jakarta-sans": "^5.3.0",
|
|
72
|
+
"@fontsource-variable/roboto-mono": "^5.3.0",
|
|
73
|
+
"@fontsource-variable/source-code-pro": "^5.3.1",
|
|
74
|
+
"@fontsource-variable/source-sans-3": "^5.3.1",
|
|
75
|
+
"@fontsource-variable/space-grotesk": "^5.3.0",
|
|
60
76
|
"@playwright/test": "1.62.0",
|
|
61
77
|
"@tailwindcss/vite": "4.3.3",
|
|
62
78
|
"@types/node": "^22.10.0",
|
package/prompts/designer.md
CHANGED
|
@@ -6,36 +6,50 @@ accessibility, frontend performance and the design language. Everything that
|
|
|
6
6
|
runs in the browser is yours, including client-side logic and canvas,
|
|
7
7
|
WebGL/three.js graphics.
|
|
8
8
|
|
|
9
|
-
You
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Inspect the existing application before
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
one
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
9
|
+
You bring craft, not a house style. The look of the product is the user's
|
|
10
|
+
call: you never impose an aesthetic of your own, and nothing visual should
|
|
11
|
+
surprise the user once it is built.
|
|
12
|
+
|
|
13
|
+
## The existing design language comes first
|
|
14
|
+
|
|
15
|
+
In a project that already has a UI, follow its design language exactly: its
|
|
16
|
+
components, tokens, spacing, typography, colours, radii, shadows, icons,
|
|
17
|
+
element types and motion. Inspect the existing application before you add
|
|
18
|
+
anything, extend the components it has instead of making visually similar
|
|
19
|
+
new ones, and match how it already does the same kind of thing (if it uses
|
|
20
|
+
pop-overs for quick edits, so do you). Depart from it only when the user
|
|
21
|
+
explicitly asks for a different style, and then only as far as they asked.
|
|
22
|
+
Where no design system exists yet, establish a small one (tokens plus a few
|
|
23
|
+
primitives) from the choices the user makes, rather than scattering one-off
|
|
24
|
+
values.
|
|
25
|
+
|
|
26
|
+
## Ask, don't assume
|
|
27
|
+
|
|
28
|
+
The user makes the design decisions; you make them concrete. Before you build
|
|
29
|
+
anything visual that the existing UI does not already settle, ask the user
|
|
30
|
+
with `ask_user_question` about every choice that is theirs to make, for
|
|
31
|
+
example:
|
|
32
|
+
|
|
33
|
+
- the visual style or direction (when there is no existing one to follow);
|
|
34
|
+
- sizes: buttons and controls, spacing density, type scale;
|
|
35
|
+
- element types: a modal or a pop-over, a drawer or a page, rich text or a
|
|
36
|
+
plain text field, a tooltip or inline help, tabs or a segmented switch,
|
|
37
|
+
a toast or an inline message;
|
|
38
|
+
- motion: whether something animates, how, and how fast;
|
|
39
|
+
- colours, radii, icons and empty, loading and error states where they are
|
|
40
|
+
new.
|
|
41
|
+
|
|
42
|
+
Group related choices into one questionnaire (one question each, with a
|
|
43
|
+
recommendation marked) instead of asking one at a time, and say what the
|
|
44
|
+
existing UI already does when it does. Decide yourself only what the existing
|
|
45
|
+
design language already answers, or what the user has said to leave to you.
|
|
46
|
+
When you report, list each design decision and who made it.
|
|
36
47
|
|
|
37
48
|
## Styling craft
|
|
38
49
|
|
|
50
|
+
Sound practice for whatever style the project has or the user picks; where the
|
|
51
|
+
existing UI does something differently, the existing UI wins.
|
|
52
|
+
|
|
39
53
|
- **Type:** a modular scale (ratio about 1.2–1.25) with at most two families.
|
|
40
54
|
Body text at 16px or larger, line-height about 1.5; headings tighter (about
|
|
41
55
|
1.1–1.25) with slightly reduced letter-spacing at large sizes. Keep line
|
|
@@ -91,8 +105,8 @@ product, and a generic render is a failed one.
|
|
|
91
105
|
|
|
92
106
|
## Interaction and motion
|
|
93
107
|
|
|
94
|
-
|
|
95
|
-
|
|
108
|
+
Motion and feedback follow the existing UI and what the user chose; where
|
|
109
|
+
neither says, ask. As defaults to offer:
|
|
96
110
|
|
|
97
111
|
- Every action gets a response: pressed states, optimistic updates, inline
|
|
98
112
|
confirmation ("Saved"), skeletons or progress for waits over ~300ms, and
|
|
@@ -100,25 +114,29 @@ over-animated.
|
|
|
100
114
|
- Motion explains change: animate only `transform` and `opacity`, 150–250ms,
|
|
101
115
|
ease-out when entering and ease-in when leaving. Never animate layout
|
|
102
116
|
properties, and never delay the user to show an animation.
|
|
103
|
-
- Small playful touches are welcome where they fit the product — a gentle
|
|
104
|
-
check-mark draw, a soft spring on a toggle, a light stagger on a short list —
|
|
105
|
-
but no looping or attention-seeking motion.
|
|
106
117
|
- Always honor `prefers-reduced-motion`: keep the state change, drop the
|
|
107
118
|
movement.
|
|
108
119
|
|
|
109
120
|
## Show the options
|
|
110
121
|
|
|
111
122
|
When a visual decision is the user's to make (a layout, a screen, how a
|
|
112
|
-
component looks, a direction for the whole UI), show it rather than
|
|
113
|
-
it: ask with `ask_user_question` and give every option an
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
placeholder boxes.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
component looks or behaves, a direction for the whole UI), show it rather than
|
|
124
|
+
describe it: ask with `ask_user_question` and give every option an
|
|
125
|
+
`htmlPreview`, a mockup of that option in HTML and CSS, laid out for a page
|
|
126
|
+
about 1200px wide, with the real content, palette and proportions of what you
|
|
127
|
+
would build, in the project's existing design language, never grey
|
|
128
|
+
placeholder boxes. Make every mockup interactive: use real `button`, `input`,
|
|
129
|
+
`select`, `textarea`, `label` and `details`/`summary` elements with their
|
|
130
|
+
hover, focus, active, checked and open states, and CSS transitions and
|
|
131
|
+
`@keyframes` for the motion the option proposes (a pop-over opening, a
|
|
132
|
+
toggle, a list arriving), at the speed you would ship. There are no scripts,
|
|
133
|
+
images, fonts or links (the sandbox strips them), so build the behaviour from
|
|
134
|
+
CSS: `:hover`, `:focus-within`, `:checked` with sibling selectors, and
|
|
135
|
+
`details[open]`. The user sees the focused option's mockup beside the
|
|
136
|
+
question, tries it there, expands it and scrolls through all the options side
|
|
137
|
+
by side, so draw each option at the same fidelity. Ask before you build. In
|
|
138
|
+
the planning panel, draw the same mockups as fenced `mockup` blocks under
|
|
139
|
+
your options.
|
|
122
140
|
|
|
123
141
|
## Accessibility
|
|
124
142
|
|
|
@@ -133,8 +151,8 @@ screen.
|
|
|
133
151
|
|
|
134
152
|
- Mobile-first, responsive from 320px up; test the narrowest and widest
|
|
135
153
|
layouts.
|
|
136
|
-
- Reuse existing components and tokens; keep components
|
|
137
|
-
composable, with state lifted only as far as needed.
|
|
154
|
+
- Reuse the project's existing components, styles and tokens; keep components
|
|
155
|
+
small, typed and composable, with state lifted only as far as needed.
|
|
138
156
|
- Performance is UX: no layout shift (reserve space for media and async
|
|
139
157
|
content), lazy-load below-the-fold assets, size images correctly, keep
|
|
140
158
|
bundles lean and avoid needless re-renders.
|
package/prompts/master.md
CHANGED
|
@@ -142,9 +142,10 @@ and reason about the change inside it:
|
|
|
142
142
|
For anything the user will see (a UI request, a layout, a screen, a visual
|
|
143
143
|
direction), show the choices rather than describe them. When you ask about a
|
|
144
144
|
visual decision, use `ask_user_question` and give each option an
|
|
145
|
-
`htmlPreview` mockup (
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
`htmlPreview` mockup (HTML and CSS of that option in the project's existing
|
|
146
|
+
design language, laid out for a page about 1200px wide, with real controls and
|
|
147
|
+
CSS states and animations so the user can try it); the user expands the
|
|
148
|
+
mockups and scrolls through the options side by side. When the choice needs real design work, delegate it to
|
|
148
149
|
the designer, who asks with its own mockups. In planning, the panel draws
|
|
149
150
|
mockups under its options the same way.
|
|
150
151
|
|
package/prompts/qa.md
CHANGED
|
@@ -55,7 +55,7 @@ you state in the report.
|
|
|
55
55
|
6. Run the smallest useful set of them.
|
|
56
56
|
7. Decide what confidence is still missing.
|
|
57
57
|
8. Only for a meaningful uncovered risk, add a behavioural test.
|
|
58
|
-
9. PASS or
|
|
58
|
+
9. Give the verdict: PASS, CHANGES_REQUIRED or BLOCKED.
|
|
59
59
|
|
|
60
60
|
Never duplicate coverage that exists, and never rewrite existing tests because
|
|
61
61
|
you prefer another style.
|
|
@@ -101,6 +101,12 @@ re-running it. Use the project's own type checker, build and existing tests
|
|
|
101
101
|
before judging by reading, and run the narrowest command that answers the
|
|
102
102
|
question (one test file, one test name, the touched package's typecheck).
|
|
103
103
|
Run a whole suite only at HIGH risk, or when the change touches shared logic.
|
|
104
|
+
A failure is the task's only when the change causes it. A test that fails in
|
|
105
|
+
code the diff does not touch, for a reason the diff cannot explain, was
|
|
106
|
+
already failing: list it as an `info` finding and judge the change on the rest.
|
|
107
|
+
When you cannot tell, run that one test at the task's base commit in a
|
|
108
|
+
throwaway worktree (`git worktree add <tmp dir> <base>`; remove it after) —
|
|
109
|
+
never stash, reset or check out over the working tree.
|
|
104
110
|
Do not re-run a check the worker already ran on the same files with the same
|
|
105
111
|
result; cite it. Always pass a bash `timeout`, and never start watch mode or
|
|
106
112
|
long-running servers.
|
|
@@ -147,9 +153,11 @@ A PASS is a claim backed by evidence, not an absence of complaints.
|
|
|
147
153
|
A PASS without executed checks is downgraded by the engine to
|
|
148
154
|
CHANGES_REQUIRED.
|
|
149
155
|
- Check every acceptance criterion; an unmet or unverifiable one is a finding.
|
|
150
|
-
- If you could not verify something important
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
- If you could not verify something important, say so and never PASS on
|
|
157
|
+
assumption. Verify it another way first (the typecheck, a narrower test,
|
|
158
|
+
reading the code path); if it stays unverifiable, return CHANGES_REQUIRED
|
|
159
|
+
with what is missing. BLOCKED is only for a review that cannot happen at
|
|
160
|
+
all, and it stops the task until the user steps in.
|
|
153
161
|
|
|
154
162
|
## Testing conventions
|
|
155
163
|
|
package/prompts/reviewer.md
CHANGED
|
@@ -108,7 +108,13 @@ If implementation changes are required, report them to the Master.
|
|
|
108
108
|
behaviour, a failing check, an unmet acceptance criterion, a security
|
|
109
109
|
problem. Tag every finding with its severity.
|
|
110
110
|
- **BLOCKED** only when you cannot review at all (it does not build, the
|
|
111
|
-
checks cannot run).
|
|
111
|
+
checks cannot run). It stops the task until the user steps in, so never use
|
|
112
|
+
it for something the workers can fix.
|
|
113
|
+
- **Pre-existing failures are not the task's.** A check that fails in code
|
|
114
|
+
the diff does not touch, for a reason the diff cannot explain, was already
|
|
115
|
+
failing: note it as an `info` finding and judge the change on the rest. When
|
|
116
|
+
unsure, run that check at the task's base commit in a throwaway worktree
|
|
117
|
+
(`git worktree add`), never by stashing or resetting the working tree.
|
|
112
118
|
- **A re-review verifies; it does not start over.** When your context lists
|
|
113
119
|
what the previous round asked for, check each item first and say which are
|
|
114
120
|
addressed. Do not raise the bar between rounds: a new blocking finding must
|
package/src/ask/tool.ts
CHANGED
|
@@ -22,9 +22,9 @@ export { ASK_TOOL };
|
|
|
22
22
|
|
|
23
23
|
const OptionSchema = Type.Object({
|
|
24
24
|
htmlPreview: Type.Optional(Type.Object({
|
|
25
|
-
html: Type.String({ maxLength: MAX_PREVIEW_HTML, description: "
|
|
25
|
+
html: Type.String({ maxLength: MAX_PREVIEW_HTML, description: "HTML of a mockup of this option. Controls (button, input, select, textarea, label, details/summary) stay live and CSS transitions and keyframes run, so the user can try it; scripts, forms, images, fonts and links are stripped." }),
|
|
26
26
|
css: Type.Optional(Type.String({ maxLength: MAX_PREVIEW_CSS })),
|
|
27
|
-
}, { additionalProperties: false, description: "
|
|
27
|
+
}, { additionalProperties: false, description: "An interactive HTML/CSS mockup of this option (no scripts: interaction through CSS states), laid out for a page about 1200px wide: shown as a thumbnail beside the question, which the user expands to scroll through every option's mockup. Give one to every option of a visual question (a layout, a screen, a component's look)." })),
|
|
28
28
|
label: Type.String({ maxLength: MAX_LABEL, description: `The option as the user sees and picks it: 1-5 words, at most ${MAX_LABEL} characters.` }),
|
|
29
29
|
description: Type.Optional(Type.String({ description: "What choosing it means: its trade-offs or consequences. Markdown." })),
|
|
30
30
|
preview: Type.Optional(Type.String({ description: "Markdown shown beside the options while this one is focused: a mockup, a code snippet, a diagram, a config. Only when seeing it helps the user compare." })),
|
|
@@ -46,6 +46,8 @@ export interface AgentRequest {
|
|
|
46
46
|
context: AgentContext;
|
|
47
47
|
model?: string;
|
|
48
48
|
mcpTools?: string[];
|
|
49
|
+
/** False when the agent's settings turn its auto-compaction off. */
|
|
50
|
+
autoCompact?: boolean;
|
|
49
51
|
delegationQuota?: { used: number };
|
|
50
52
|
thinking?: string;
|
|
51
53
|
timeoutMs: number;
|
|
@@ -254,6 +256,7 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
|
|
|
254
256
|
request.onUpdate?.(child);
|
|
255
257
|
},
|
|
256
258
|
mcpTools: request.mcpTools,
|
|
259
|
+
...(request.autoCompact === false ? { autoCompact: false } : {}),
|
|
257
260
|
task: request.instruction,
|
|
258
261
|
systemPrompt,
|
|
259
262
|
tools: toolsFor(request),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
2
2
|
import type { Domain, Role } from "../schemas/agent.ts";
|
|
3
3
|
import { delegationHost } from "./delegation.ts";
|
|
4
|
+
import { AUTO_COMPACT_ENV } from "../pi/quiet.ts";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
6
7
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -44,6 +45,8 @@ export interface PiRunOptions {
|
|
|
44
45
|
onEvent?: (event: PiStreamEvent) => void;
|
|
45
46
|
/** Extra environment for the subagent process. */
|
|
46
47
|
env?: Record<string, string>;
|
|
48
|
+
/** False when the agent's settings turn its auto-compaction off. */
|
|
49
|
+
autoCompact?: boolean;
|
|
47
50
|
/** Kill the agent after this long without any output; 0 disables. */
|
|
48
51
|
stallTimeoutMs?: number;
|
|
49
52
|
/** Silence allowed while one tool call is in flight (tests, builds); 0 disables. */
|
|
@@ -765,7 +768,8 @@ function writePromptFile(prompt: string): { file: string; cleanup: () => void }
|
|
|
765
768
|
}
|
|
766
769
|
|
|
767
770
|
/** Run one isolated subagent and normalise its outcome. */
|
|
768
|
-
export async function runPiAgent(
|
|
771
|
+
export async function runPiAgent(given: PiRunOptions, run: ProcessRunner = spawnPiProcess): Promise<PiRunResult> {
|
|
772
|
+
const options = given.autoCompact === false ? { ...given, env: { ...given.env, [AUTO_COMPACT_ENV]: "0" } } : given;
|
|
769
773
|
const host = await delegationHost(options, run);
|
|
770
774
|
let prompt: ReturnType<typeof writePromptFile> | undefined;
|
|
771
775
|
try {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the oracle learns, shared on its compaction loop. When the oracle's own context is compacted, the checkpoint
|
|
3
|
+
* it writes ends with a "Knowledge to share" section: durable project facts, standards and decisions, each tagged
|
|
4
|
+
* with the agent it belongs to. Those lines go into that agent's knowledge files (through `applyKnowledge`, which
|
|
5
|
+
* drops duplicates), so the oracle keeps its own knowledge current and the domain agents (designer, backend, QA)
|
|
6
|
+
* get what concerns them without the oracle having to remember to write it down.
|
|
7
|
+
*/
|
|
8
|
+
import { applyKnowledge, type KnowledgeKind } from "./store.ts";
|
|
9
|
+
import type { KnowledgeAgent } from "./paths.ts";
|
|
10
|
+
|
|
11
|
+
export const DIGEST_HEADING = "Knowledge to share";
|
|
12
|
+
|
|
13
|
+
/** Added to the oracle's compaction instruction. */
|
|
14
|
+
export const DIGEST_INSTRUCTION = [
|
|
15
|
+
`Finish with a section headed "## ${DIGEST_HEADING}": the durable things this stretch of work established about the project that an agent will need again, one per line, each as \`- [agent:kind] text\`.`,
|
|
16
|
+
"agent is master (you: how the user works, project-wide facts), designer (UI, UX, design language, frontend), backend (APIs, data, services, infrastructure) or qa (testing, verification, known failures).",
|
|
17
|
+
"kind is knowledge (a fact or a useful pattern), standard (a rule the user wants followed) or decision (a choice the user made, with its reason).",
|
|
18
|
+
"Give the user's own words where they set a rule or a preference. Leave out task progress, guesses, and anything only true for this task. Write `- none` when nothing qualifies. At most 12 lines.",
|
|
19
|
+
].join(" ");
|
|
20
|
+
|
|
21
|
+
const AGENTS: readonly KnowledgeAgent[] = ["master", "designer", "backend", "qa"];
|
|
22
|
+
const KINDS: readonly KnowledgeKind[] = ["knowledge", "standard", "decision"];
|
|
23
|
+
const MAX_ITEMS = 12;
|
|
24
|
+
const MAX_CHARS = 400;
|
|
25
|
+
|
|
26
|
+
export interface DigestItem {
|
|
27
|
+
agent: KnowledgeAgent;
|
|
28
|
+
kind: KnowledgeKind;
|
|
29
|
+
text: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The tagged lines under the "Knowledge to share" heading; anything malformed is skipped. */
|
|
33
|
+
export function parseDigest(summary: string): DigestItem[] {
|
|
34
|
+
const lines = summary.split(/\r?\n/);
|
|
35
|
+
const start = lines.findIndex((line) => /^#{1,4}\s*knowledge to share\s*$/i.test(line.trim()));
|
|
36
|
+
if (start < 0) return [];
|
|
37
|
+
const items: DigestItem[] = [];
|
|
38
|
+
for (const line of lines.slice(start + 1)) {
|
|
39
|
+
if (/^#{1,4}\s/.test(line.trim())) break;
|
|
40
|
+
const match = /^\s*[-*]\s*\[\s*([a-z]+)\s*:\s*([a-z]+)\s*\]\s*(.+?)\s*$/i.exec(line);
|
|
41
|
+
if (!match) continue;
|
|
42
|
+
const agent = match[1]!.toLowerCase() as KnowledgeAgent;
|
|
43
|
+
const kind = match[2]!.toLowerCase() as KnowledgeKind;
|
|
44
|
+
const text = match[3]!.replace(/\s+/g, " ").trim();
|
|
45
|
+
if (!AGENTS.includes(agent) || !KINDS.includes(kind) || !text || text.length > MAX_CHARS) continue;
|
|
46
|
+
items.push({ agent, kind, text });
|
|
47
|
+
if (items.length >= MAX_ITEMS) break;
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Write each item into its agent's knowledge; returns how many were new, by agent. */
|
|
53
|
+
export function shareDigest(dataRoot: string, items: readonly DigestItem[], now = new Date()): Partial<Record<KnowledgeAgent, number>> {
|
|
54
|
+
const added: Partial<Record<KnowledgeAgent, number>> = {};
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
const text = item.kind === "decision" ? item.text : `- ${item.text}`;
|
|
57
|
+
if (applyKnowledge({ dataRoot, agent: item.agent, kind: item.kind, text, now }).result === "added") added[item.agent] = (added[item.agent] ?? 0) + 1;
|
|
58
|
+
}
|
|
59
|
+
return added;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** "2 for the designer, 1 for QA" */
|
|
63
|
+
export function describeShared(added: Partial<Record<KnowledgeAgent, number>>): string {
|
|
64
|
+
const names: Record<KnowledgeAgent, string> = { master: "the oracle", designer: "the designer", backend: "backend", qa: "QA" };
|
|
65
|
+
return AGENTS.filter((agent) => added[agent]).map((agent) => `${added[agent]} for ${names[agent]}`).join(", ");
|
|
66
|
+
}
|
package/src/lobby/planner.ts
CHANGED
|
@@ -1008,6 +1008,8 @@ export class PlanningSession {
|
|
|
1008
1008
|
model,
|
|
1009
1009
|
thinking,
|
|
1010
1010
|
mcpTools: profile.mcpTools,
|
|
1011
|
+
...(profile.autoCompact === false ? { autoCompact: false } : {}),
|
|
1012
|
+
...(profile.autoCompact === false ? { autoCompact: false } : {}),
|
|
1011
1013
|
onChildRun: (child) => {
|
|
1012
1014
|
this.deps.feed?.step(label, `child ${child.runId.slice(0, 8)}: ${child.step ?? child.status}`, child.runId);
|
|
1013
1015
|
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
package/src/lobby/pr-review.ts
CHANGED
|
@@ -279,6 +279,7 @@ export class PullReviews {
|
|
|
279
279
|
model,
|
|
280
280
|
thinking,
|
|
281
281
|
mcpTools: profile.mcpTools,
|
|
282
|
+
...(profile.autoCompact === false ? { autoCompact: false } : {}),
|
|
282
283
|
onChildRun: (child) => {
|
|
283
284
|
this.deps.feed?.step(REVIEW_SOURCE, `child ${child.runId.slice(0, 8)}: ${child.step ?? child.status}`, child.runId);
|
|
284
285
|
if (child.status !== "running") this.deps.feed?.end(child.runId, child.status !== "success");
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -92,6 +92,8 @@ export interface QuickFixProfile {
|
|
|
92
92
|
timeoutMs: number;
|
|
93
93
|
instructions?: string;
|
|
94
94
|
mcpTools?: string[];
|
|
95
|
+
/** False when its settings turn the agent's auto-compaction off. */
|
|
96
|
+
autoCompact?: boolean;
|
|
95
97
|
/** Where the job goes when its model runs out of usage or is unavailable. */
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -340,6 +342,8 @@ export class QuickFixQueue {
|
|
|
340
342
|
model,
|
|
341
343
|
thinking,
|
|
342
344
|
mcpTools: profile.mcpTools,
|
|
345
|
+
...(profile.autoCompact === false ? { autoCompact: false } : {}),
|
|
346
|
+
...(profile.autoCompact === false ? { autoCompact: false } : {}),
|
|
343
347
|
delegationQuota: quota,
|
|
344
348
|
agent: { runId: job.id, taskId: job.id, domain: "backend", role: "worker" },
|
|
345
349
|
onChildRun: (child) => {
|
package/src/master/master.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.
|
|
|
12
12
|
import { scriptsFor } from "../scripts/pick.ts";
|
|
13
13
|
import { summarizeOutcomes } from "./synthesis.ts";
|
|
14
14
|
import { parseWorkerResult, validateWorkerResult } from "../roles/worker.ts";
|
|
15
|
-
import { parseReviewResult, validateReviewResult } from "../roles/reviewer.ts";
|
|
15
|
+
import { parseReviewResult, readVerdict, validateReviewResult } from "../roles/reviewer.ts";
|
|
16
16
|
import { truncate } from "../text.ts";
|
|
17
17
|
import { isScoutResultUsable, parseScoutResult, validateScoutResult } from "../roles/scout.ts";
|
|
18
18
|
import { treeState } from "../execution/git.ts";
|
|
@@ -60,7 +60,7 @@ export interface ScoutRequest {
|
|
|
60
60
|
/** Model, thinking and time limit for one run, from settings. */
|
|
61
61
|
function profileFields(config: BotLobbyConfig, resolver: ProfileResolver | undefined, domain: Domain, role: AgentRequest["role"]) {
|
|
62
62
|
const profile = profileFor(config, resolver, domain, role);
|
|
63
|
-
return { model: profile.model, mcpTools: profile.mcpTools, thinking: profile.thinking, timeoutMs: profile.timeoutMs, time: boundedAgentTime(profile.timeoutMs) };
|
|
63
|
+
return { model: profile.model, mcpTools: profile.mcpTools, ...(profile.autoCompact === false ? { autoCompact: false } : {}), thinking: profile.thinking, timeoutMs: profile.timeoutMs, time: boundedAgentTime(profile.timeoutMs) };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function scoutInstruction(request: ScoutRequest, domain: Domain): string {
|
|
@@ -433,9 +433,9 @@ function reviewerContext(request: ReviewerRequest): string {
|
|
|
433
433
|
.join("\n\n");
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
/** True when the output
|
|
436
|
+
/** True when a verdict can be read from the output (the contract's heading, or a line naming it). */
|
|
437
437
|
export function hasReviewVerdict(text: string): boolean {
|
|
438
|
-
return
|
|
438
|
+
return readVerdict(text) !== undefined;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
441
|
/** A success without the Verdict section is off-contract and worth resampling. */
|
package/src/master/research.ts
CHANGED
|
@@ -71,6 +71,7 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
|
|
|
71
71
|
context: researchContext(request),
|
|
72
72
|
model: profile.model,
|
|
73
73
|
mcpTools: profile.mcpTools,
|
|
74
|
+
...(profile.autoCompact === false ? { autoCompact: false } : {}),
|
|
74
75
|
thinking: profile.thinking,
|
|
75
76
|
timeoutMs: profile.timeoutMs,
|
|
76
77
|
...(request.time ? { time: request.time } : {}),
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
import { compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, type ExtensionAPI, type TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { compact, CONFIG_DIR_NAME, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, type ExtensionAPI, type ExtensionContext, type TurnEndEvent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { contextMarks, freshContextOn } from "./fresh-context.ts";
|
|
3
|
-
import { isSubagentProcess } from "./quiet.ts";
|
|
3
|
+
import { AUTO_COMPACT_ENV, isSubagentProcess } from "./quiet.ts";
|
|
4
|
+
import { dataRoot, detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
5
|
+
import { describeShared, DIGEST_INSTRUCTION, parseDigest, shareDigest, type DigestItem } from "../knowledge/oracle-digest.ts";
|
|
6
|
+
|
|
7
|
+
const CHECKPOINT = "Write a concise checkpoint with: Requirements and constraints; Decisions; Files changed; Verification and evidence; Failed approaches (do not repeat); Unfinished work and next action; Outstanding children and their assignments. Preserve concrete file references and distinguish verified facts from guesses. The parent remains responsible for integration and verification.";
|
|
8
|
+
|
|
9
|
+
export interface AutoCompactionOptions {
|
|
10
|
+
/** Whether this agent compacts on its own: a subagent by its environment, the oracle by its settings. */
|
|
11
|
+
enabled?: () => boolean;
|
|
12
|
+
/** Where the oracle's shared knowledge goes; by default the project's knowledge files. */
|
|
13
|
+
share?: (items: DigestItem[], ctx: ExtensionContext) => string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function enabledBySettings(): boolean {
|
|
17
|
+
if (isSubagentProcess()) return process.env[AUTO_COMPACT_ENV] !== "0";
|
|
18
|
+
try {
|
|
19
|
+
return loadConfig().master.autoCompact !== false;
|
|
20
|
+
} catch {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function shareWithAgents(items: DigestItem[], ctx: ExtensionContext): string {
|
|
26
|
+
const root = detectProjectRoot(ctx.cwd, CONFIG_DIR_NAME);
|
|
27
|
+
return describeShared(shareDigest(dataRoot(root, CONFIG_DIR_NAME), items));
|
|
28
|
+
}
|
|
4
29
|
|
|
5
30
|
export const CONTEXT_TARGET = 64_000;
|
|
6
31
|
|
|
@@ -8,13 +33,20 @@ export function compactionThreshold(window: number, reserve = DEFAULT_COMPACTION
|
|
|
8
33
|
return Math.max(1, Math.min(CONTEXT_TARGET, Math.floor(window * 0.6), window - Math.min(reserve, Math.floor(window / 2))));
|
|
9
34
|
}
|
|
10
35
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Compact at a settled tool boundary; ctx.compact() would abort the turn we need to continue. Each agent's settings
|
|
38
|
+
* can turn it off. The oracle's checkpoint also lists what it learned that is worth keeping, tagged by agent, and
|
|
39
|
+
* that goes into the knowledge base: its own, and the designer's, backend's and QA's where it concerns them.
|
|
40
|
+
*/
|
|
41
|
+
export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compact = compact, options: AutoCompactionOptions = {}): void {
|
|
42
|
+
const enabled = options.enabled ?? enabledBySettings;
|
|
43
|
+
const share = options.share ?? shareWithAgents;
|
|
44
|
+
const oracle = !isSubagentProcess();
|
|
13
45
|
let busy = false;
|
|
14
46
|
let lastLeaf: string | null | undefined;
|
|
15
47
|
pi.on("session_start", () => { busy = false; lastLeaf = undefined; });
|
|
16
48
|
pi.on("turn_end", async (event: TurnEndEvent, ctx) => {
|
|
17
|
-
if (busy || !ctx.model) return;
|
|
49
|
+
if (busy || !ctx.model || !enabled()) return;
|
|
18
50
|
const leaf = ctx.sessionManager.getLeafId();
|
|
19
51
|
if (leaf === lastLeaf) return;
|
|
20
52
|
const configured = pi.getSettings().compaction;
|
|
@@ -61,9 +93,18 @@ export function registerAutoCompaction(pi: ExtensionAPI, summarize: typeof compa
|
|
|
61
93
|
const headers = auth.headers ? Object.fromEntries(Object.entries(auth.headers).filter((entry): entry is [string, string] => entry[1] !== null)) : undefined;
|
|
62
94
|
const retry = pi.getSettings().retry;
|
|
63
95
|
const retryPolicy = { enabled: retry?.enabled ?? true, maxRetries: retry?.maxRetries ?? 3, baseDelayMs: retry?.baseDelayMs ?? 2_000, maxAgentDelayMs: retry?.maxAgentDelayMs };
|
|
64
|
-
const result = await summarize({ firstKeptEntryId, messagesToSummarize: messages, turnPrefixMessages: [], isSplitTurn: false, tokensBefore: estimate, previousSummary: previous?.type === "compaction" ? previous.summary : undefined, fileOps, settings: { enabled: true, reserveTokens: reserve, keepRecentTokens } }, auth.baseUrl ? { ...ctx.model, baseUrl: auth.baseUrl } : ctx.model, auth.apiKey, headers,
|
|
96
|
+
const result = await summarize({ firstKeptEntryId, messagesToSummarize: messages, turnPrefixMessages: [], isSplitTurn: false, tokensBefore: estimate, previousSummary: previous?.type === "compaction" ? previous.summary : undefined, fileOps, settings: { enabled: true, reserveTokens: reserve, keepRecentTokens } }, auth.baseUrl ? { ...ctx.model, baseUrl: auth.baseUrl } : ctx.model, auth.apiKey, headers, oracle ? `${CHECKPOINT} ${DIGEST_INSTRUCTION}` : CHECKPOINT, ctx.signal, undefined, undefined, auth.env, retryPolicy);
|
|
65
97
|
ctx.signal?.throwIfAborted();
|
|
66
98
|
ctx.ui.notify("Agent context compacted", "info");
|
|
99
|
+
if (oracle) {
|
|
100
|
+
try {
|
|
101
|
+
const items = parseDigest(result.summary);
|
|
102
|
+
const shared = items.length ? share(items, ctx) : "";
|
|
103
|
+
if (shared) ctx.ui.notify(`Knowledge updated: ${shared}`, "info");
|
|
104
|
+
} catch (error) {
|
|
105
|
+
ctx.ui.notify(`Could not update knowledge: ${(error as Error).message}`, "warning");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
67
108
|
return { entries: [...event.entries, { type: "compaction" as const, summary: result.summary, firstKeptEntryId, usage: result.usage, details: result.details }] };
|
|
68
109
|
} catch (error) {
|
|
69
110
|
ctx.ui.notify(`Context compaction failed: ${(error as Error).message}`, "warning");
|
package/src/pi/quiet.ts
CHANGED
|
@@ -58,6 +58,9 @@ export function webToolsFor(active: string[], taskActive: boolean, hidden: reado
|
|
|
58
58
|
return { active: [...active, ...back], hidden: [] };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Set to "0" in a subagent's environment when its settings turn auto-compaction off. */
|
|
62
|
+
export const AUTO_COMPACT_ENV = "BOT_LOBBY_AUTO_COMPACT";
|
|
63
|
+
|
|
61
64
|
/** True inside a bot-lobby subagent process (marked in `spawnPiProcess`). */
|
|
62
65
|
export function isSubagentProcess(): boolean {
|
|
63
66
|
return process.env.BOT_LOBBY_SUBAGENT === "1";
|