ytdb-slate 0.1.0

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.
@@ -0,0 +1,172 @@
1
+ # Slate review rules
2
+
3
+ How the orchestrator reviews work produced by worker threads. Review
4
+ threads are created dynamically like any other thread; there is no fixed
5
+ roster. These rules govern how reviews are composed, not who performs
6
+ them. Worker threads do not automatically load this document. When
7
+ dispatching a reviewer, either embed in the task text the perspective's
8
+ charter (§3), the reasoning obligations (§7), and the output contract
9
+ (§4: the perspective's ID prefix, the severity vocabulary
10
+ blocker/should-fix/suggestion, and a closing compact findings block —
11
+ one `ID` | `severity` | `location` | `one-line summary` |
12
+ `counterexample gist` line per finding, see §5), or direct the
13
+ reviewer to read the relevant sections of this file itself (worker
14
+ threads can read files; they just do not receive this one
15
+ automatically).
16
+
17
+ ## 1. When to review
18
+
19
+ - Every non-trivial change gets reviewed before it is declared done.
20
+ - Rigor scales with risk of the change, never with effort already spent.
21
+ When in doubt, review more.
22
+ - For long multi-step work streams, review high-risk steps in isolation
23
+ right after they land — defects localized to one step get buried once
24
+ the diff folds into the cumulative result — and always review the
25
+ cumulative diff at the end regardless of per-step reviews.
26
+
27
+ ## 2. Reviewers are fresh threads
28
+
29
+ - A review perspective is always a NEW thread. Never reuse the
30
+ implementing thread and never review substantial work yourself: fresh
31
+ eyes are the point.
32
+ - Do not pass implementer episodes or implementer reasoning to
33
+ reviewers. Give a reviewer the artifact/diff, the intent of the
34
+ change, its in-scope files, and its charter plus reasoning/output
35
+ obligations (see the intro).
36
+ - Independent perspectives run in PARALLEL on the same artifact.
37
+
38
+ ## 3. Choose perspectives by what the change touches
39
+
40
+ There is no fixed roster — compose perspectives per change. Each
41
+ bullet below is that perspective's charter:
42
+
43
+ - **Code baseline** (any production/test code change): correctness &
44
+ bugs (logic, null safety, resource leaks, lifecycle); code quality
45
+ (readability, duplication, error handling); test quality (do tests
46
+ verify behavior, cover the change, and would they fail on
47
+ regression?).
48
+ - **Specialists**, added when the change touches their domain:
49
+ - concurrency — any defect requiring reasoning about thread
50
+ interleavings (locks, shared state, atomicity);
51
+ - security — public API surface, authentication, user input,
52
+ network, (de)serialization;
53
+ - performance — hot paths, lock contention, caching, algorithmic
54
+ complexity, large data structures;
55
+ - test structure — complex fixtures, shared state between tests,
56
+ isolation and lifecycle concerns.
57
+ - **Project-supplied perspectives**: a consuming project may add
58
+ review perspectives via the `reviewPerspectivesPath` key in its
59
+ `slate.json` — a markdown file of additional charters (typically
60
+ domain specialists). Each charter in that file MUST declare its own
61
+ stable finding-ID prefix (uniqueness rules in §4). Compose them per
62
+ change exactly like the built-in perspectives above.
63
+ - **Non-code artifacts** (docs, prompts, process rules, extension
64
+ guidance like this file) get their own perspectives INSTEAD of the
65
+ code baseline: internal consistency & cross-references; instruction
66
+ completeness (can a reader execute this without guessing?); context
67
+ budget (what becomes always-loaded vs on-demand); writing style;
68
+ and safety of any scripts/hooks/config touched.
69
+ - **Mixed diffs**: scope each reviewer to its in-scope files. Code
70
+ reviewers never review process/docs files, and vice versa.
71
+ - Selection is a judgment call, not a rigid filter — when in doubt,
72
+ include the perspective. Risk/complexity changes how deep iteration
73
+ goes (§6), never which perspectives run.
74
+
75
+ ## 4. Findings
76
+
77
+ - Each reviewer emits findings with stable IDs prefixed by perspective
78
+ (BG = bugs, CQ = code quality, TQ = test quality, CN = concurrency,
79
+ SE = security, PF = performance, TS = test structure,
80
+ WC = consistency, WI = completeness, WB = context budget,
81
+ WS = writing style, WH = script/hook/config safety, RG = regression
82
+ (filed by gate threads)). IDs are cumulative across iterations and
83
+ are never renumbered — they are the sole addressing key for fixes
84
+ and verification.
85
+ - Project-supplied perspectives (§3) emit findings under the prefix
86
+ their charter declares; those prefixes join the orchestrator's
87
+ cumulative ledger exactly like the built-in ones. Declared prefixes
88
+ MUST NOT collide with the built-in roster above (BG, CQ, TQ, CN, SE,
89
+ PF, TS, WC, WI, WB, WS, WH, RG) nor with each other. On collision
90
+ the built-in roster wins: the orchestrator assigns the colliding
91
+ charter a fresh non-colliding prefix at dispatch, records the
92
+ substitution in the ledger, and uses the substitute everywhere — the
93
+ declared prefix is never used. When two project charters collide
94
+ with each other, the charter appearing first in the perspectives
95
+ file keeps its declared prefix; later ones are reassigned.
96
+ - The orchestrator owns the cumulative finding-ID ledger: when
97
+ spawning a later-iteration reviewer or gate thread, tell it the next
98
+ free number for its prefix.
99
+ - Severity: **blocker** (must fix before done), **should-fix** (fix
100
+ unless explicitly justified and reported), **suggestion** (optional).
101
+ - Synthesize across reviewers: deduplicate by location/root cause,
102
+ keep all contributing IDs on the merged finding, and take the
103
+ highest severity when duplicates disagree (upgrade-only — never
104
+ downgrade a severity during synthesis).
105
+
106
+ ## 5. Reviewers find, implementers fix
107
+
108
+ - Review threads never edit files; fix work is dispatched to an
109
+ implementation thread (usually the one that produced the change, or
110
+ a fresh one if it is gone or compromised — context-poisoned or
111
+ repeatedly failing). The orchestrator never edits files itself.
112
+ - Episodes are compressed summaries (~300–800 words), so full finding
113
+ prose does not reliably survive them. Reviewers must therefore end
114
+ their final response with a self-contained compact findings block —
115
+ one `ID` | `severity` | `location` | `one-line summary` |
116
+ `counterexample gist` line per finding — sized to survive episode
117
+ compression. Route fixes by passing the reviewer's episode reference
118
+ (`context`) plus the compact finding index (the orchestrator's
119
+ synthesized list of all open findings, one `ID` | `severity` |
120
+ `location` | `one-line summary` line each); fixers re-read the
121
+ affected code themselves instead of relying on reviewer prose.
122
+
123
+ ## 6. Iteration and termination
124
+
125
+ - After fixes, re-verify each addressed finding — in a gate thread,
126
+ not by trusting the fixer's claim. A gate thread is a fresh thread
127
+ whose sole job is verdicts; it receives the compact finding index
128
+ (§5) and the fix diff, but not the fixer's or implementer's
129
+ episodes. Verdict per finding:
130
+ - **VERIFIED** — the fix resolves the finding;
131
+ - **REJECTED** — the change claimed as the fix does not address the
132
+ finding;
133
+ - **STILL OPEN** — the finding was not addressed, or the attempted
134
+ fix leaves the defect in place;
135
+ - **MOOT** — the finding is obsoleted by other changes;
136
+ - **REGRESSION** — the fix broke something else; file the breakage
137
+ as a new blocker (RG prefix).
138
+ - Blockers loop until clear. Should-fix findings get up to 3
139
+ iterations for normal changes; go deeper only for high-risk changes.
140
+ - No-progress rule: if an iteration clears nothing and surfaces no new
141
+ fixable finding, stop and escalate to the user instead of spinning.
142
+ - Done = no blockers remain, addressed findings VERIFIED, and any
143
+ remaining should-fix/suggestions explicitly reported to the user.
144
+
145
+ ## 7. Semi-formal reasoning (how reviewers argue)
146
+
147
+ Reviewers use semi-formal reasoning — structured argument between
148
+ free-form chain-of-thought and formal proof (Ugare & Chandra, "Agentic
149
+ Code Reasoning", arXiv:2603.01896). Embed these obligations in every
150
+ review dispatch:
151
+
152
+ - State the decision criteria (definitions) and numbered premises
153
+ before reaching any verdict.
154
+ - Trace, don't guess: cite evidence from code actually read (file:line
155
+ or diff hunk) for every claim about behavior. Read third-party /
156
+ library code instead of assuming its semantics.
157
+ - Enumerate cases exhaustively along the changed execution paths
158
+ (branches, error paths, boundary values); mark each case as checked
159
+ or explicitly out of scope.
160
+ - Back every defect claim (blocker or should-fix) with a concrete
161
+ counterexample: the input, state, or interleaving that triggers the
162
+ defect, traced through the code.
163
+ - Back every correctness claim ("no issue here") with a justification
164
+ for why no counterexample exists — not merely the absence of one
165
+ found.
166
+ - Before finalizing, run an alternative-hypothesis check: "if the
167
+ opposite verdict were true, what evidence would exist?" — then look
168
+ for that evidence.
169
+ - While exploring, log hypotheses explicitly (hypothesis → evidence
170
+ sought → confirmed / refuted / refined) instead of wandering.
171
+ - Derive the final verdict explicitly from the stated definitions and
172
+ claims, not from overall impression.
@@ -0,0 +1,310 @@
1
+ # Track-based development workflow
2
+
3
+ The mandatory flow for every change delivered under this workflow:
4
+
5
+ Research → (lazy) research log → user design review → adversarial
6
+ review → user approves the track split → per-track loop (implement →
7
+ track code review → fixes → mandatory user review → marker commit) →
8
+ delivery.
9
+
10
+ Publishing is conditional on `workflow.draftPRs` in the project's
11
+ `slate.json` (default: false). When it is ENABLED, an umbrella draft
12
+ PR is created after both reviews pass and before implementation
13
+ starts — see pr-publishing.md in this directory for all PR mechanics.
14
+ When it is DISABLED, the workflow log is the change's durable record;
15
+ its lifecycle is defined in § Research log below. Marker commits work
16
+ identically in both modes.
17
+
18
+ The flow scales with change size:
19
+
20
+ | Change size | What applies |
21
+ | --- | --- |
22
+ | Multi-track change | Full flow as described above. |
23
+ | Single-track change | No track split, no marker commits. Everything else applies. |
24
+ | Trivial change (typo, doc-only, mechanical rename, obvious one-file fix) | No split. The planned-changes statement is a 2–3-sentence paragraph; the design review collapses into user consent to it. Micro adversarial review, or skip it with explicit user consent. |
25
+
26
+ The mandatory user review gate applies at EVERY tier. For single-track
27
+ and trivial changes the whole branch diff is the track, so the user
28
+ review sits after the agent code review and before delivery.
29
+
30
+ Throughout this document, the "planned-changes statement" is the
31
+ high-level design summary of the change. With publishing enabled it
32
+ becomes the Planned-changes section of the PR description (see
33
+ pr-publishing.md); with publishing disabled it lives in the workflow
34
+ log.
35
+
36
+ ## Research phase (lightweight)
37
+
38
+ Research is interactive exploration before any implementation: read
39
+ real source code, trace call chains, and clarify aims and constraints
40
+ with the user. There is no design document, no implementation plan,
41
+ and no mandatory artifacts. The phase ends when the initial design of
42
+ the change is understood and has passed the user design review (see
43
+ below).
44
+
45
+ ## Research log (lazy-triggered)
46
+
47
+ Start research WITHOUT a log. Open one the moment any trigger below
48
+ fires, then backfill the decisions already made — backfilling is cheap
49
+ while they are still in context.
50
+
51
+ | # | Trigger |
52
+ | --- | --- |
53
+ | 1 | Second non-trivial decision (a choice where a plausible alternative was rejected for a reason). |
54
+ | 2 | First surprise — the codebase behaves differently than assumed. |
55
+ | 3 | First risky invariant identified — concurrency, durability or recovery, transactional semantics, a public API or behavioral change. |
56
+ | 4 | Research will cross a session boundary. |
57
+ | 5 | The change is multi-track. |
58
+ | 6 | The user requests it. |
59
+
60
+ ### Log format
61
+
62
+ Four sections, opened with the log:
63
+
64
+ - **Initial request** — verbatim, written once.
65
+ - **Decision Log** — append-only. Each entry is at most 4 lines: the
66
+ decision, why, and the alternatives rejected. Each entry MUST be
67
+ self-sufficient in one sentence; optional evidence citations go in
68
+ as deep links.
69
+ - **Surprises & Discoveries**
70
+ - **Open Questions**
71
+
72
+ Two more named sections join as the corresponding phase begins:
73
+
74
+ - **Planned changes** — the planned-changes statement, added when the
75
+ design converges; the design review and adversarial review verdict
76
+ lines are appended at its end. (With publishing enabled this section
77
+ feeds the PR description at umbrella-PR creation.)
78
+ - **Track table** — publishing disabled only: added when the user
79
+ approves the track split (constraints in § Track table). With
80
+ publishing enabled the table lives in the PR description from
81
+ creation instead.
82
+
83
+ ### Persistence
84
+
85
+ During research the log lives as an untracked file `research-log.md`
86
+ at the repo root. What happens next depends on `workflow.draftPRs`:
87
+
88
+ - **Enabled** — the log's life ends at umbrella-PR creation: its
89
+ content folds into the PR description and the file is deleted
90
+ (mechanics owned by pr-publishing.md § Creation).
91
+ - **Disabled (default)** — the log is RETAINED at the repo root as the
92
+ change's workflow log until the change is delivered (§ Delivery
93
+ below). The planned-changes statement and both verdict lines anchor
94
+ in its Planned changes section, the track table in its Track table
95
+ section (§ Log format), and post-design decisions keep appending to
96
+ the Decision Log.
97
+
98
+ ### Delivery (publishing disabled)
99
+
100
+ A change is DELIVERED when its final squashed commit lands on the
101
+ repository's default development branch, or when the user explicitly
102
+ abandons it. At delivery the agent folds the log into the delivery
103
+ commit's message body — the motivation, the planned-changes statement,
104
+ and both verdict lines: the same content that would have become the PR
105
+ description — strips the track table (track numbers are ephemeral
106
+ branch-life identifiers and would dangle in history), resolves or
107
+ explicitly hands any remaining Open Questions to the user, then
108
+ deletes the log file. On abandonment the agent deletes the log file
109
+ after offering its content to the user for archiving.
110
+
111
+ ### Under-trigger guardrail
112
+
113
+ When shaping the planned-changes statement without a log, state this
114
+ explicitly (e.g. "no log kept — one trivial decision, no surprises")
115
+ so the user can override and request one.
116
+
117
+ ## User design review (mandatory, pre-adversarial)
118
+
119
+ When research converges, the agent presents the design to the user:
120
+ the proposed approach, key decisions with the alternatives rejected,
121
+ risks, and open questions. The presentation input is keyed on log
122
+ existence, the same way as the adversarial review's: log exists →
123
+ present from the log; no log → present the draft planned-changes
124
+ statement. The agent then loops on user feedback — revising the design
125
+ (and log) — until the user explicitly approves. Only after that
126
+ approval does the adversarial review run.
127
+
128
+ Rationale: the user owns the design direction. Reviewing with the user
129
+ first means the adversarial review attacks a stabilized, user-endorsed
130
+ design instead of one the user may still redirect — adversarial rounds
131
+ are not spent on designs that would change anyway. The loop mechanics
132
+ mirror the track loop's mandatory user review (present → feedback →
133
+ explicit approval); the position relative to machine review is
134
+ deliberately inverted — here the user reviews first.
135
+
136
+ Durable record — append a verdict line to the log:
137
+
138
+ ```
139
+ Design review: user-approved — YYYY-MM-DD
140
+ ```
141
+
142
+ With no log, append the line to the draft planned-changes statement
143
+ instead. With publishing enabled it travels into the PR description at
144
+ umbrella-PR creation (pr-publishing.md § Creation); with publishing
145
+ disabled the workflow log's Planned changes section is the verdict
146
+ line's only durable home — writing the first verdict line opens the
147
+ log if none exists, seeded with the planned-changes statement. (Crossing a session boundary before the change is published
148
+ or delivered is research-log trigger #4.) The verdict line is written
149
+ at every tier; at the trivial tier it is appended after the
150
+ planned-changes paragraph, which stands in for the Risks & accepted
151
+ trade-offs subsection.
152
+
153
+ Tier scaling: at the trivial tier the design review collapses into the
154
+ user's consent to the 2–3-sentence planned-changes paragraph. With
155
+ publishing enabled, the ask for trivial and single-track changes may
156
+ be batched: the agent presents the design together with the draft PR
157
+ description in ONE pre-adversarial ask, and the user's single approval
158
+ covers both. Umbrella-PR creation still follows the adversarial
159
+ review; the description is re-presented only if adversarial triage
160
+ changed it.
161
+
162
+ ## Adversarial review (mandatory, pre-implementation)
163
+
164
+ Adversarial review runs after the user approves the design in the user
165
+ design review, BEFORE the planned-changes statement is finalized and
166
+ implementation starts, for EVERY change. The rationale, briefly:
167
+ critique activates latent knowledge that constructive planning does
168
+ not (generator/critic asymmetry), and a fresh-context reviewer has no
169
+ anchoring on the author's rationale.
170
+
171
+ The reviewer must be a fresh context (sub-agent or fresh session) that
172
+ did not author the decisions.
173
+
174
+ Input is keyed on log existence:
175
+
176
+ - Log exists → the adversary attacks the log (plus its cited
177
+ evidence).
178
+ - No log → the adversary attacks the draft planned-changes statement.
179
+ - Trivial tier → micro-review with one bounded question ("what breaks
180
+ / what am I not seeing?"), or skipped with explicit user consent.
181
+
182
+ Charter scaling: for single-track changes, limit the mandate to
183
+ correctness, hidden coupling, and missed alternatives — style and
184
+ speculative scope creep are out of bounds.
185
+
186
+ Licensed null verdict: "no substantive findings" is an acceptable,
187
+ respected outcome — the review prompt must say so.
188
+
189
+ Triage each finding with the user. Three outcomes:
190
+
191
+ - **Strengthen** — enrich the alternatives-rejected rationale of the
192
+ attacked decision.
193
+ - **Reverse** — change the decision now, while it is still cheap.
194
+ - **Accept-as-risk** — record it in Open Questions / Risks.
195
+
196
+ Triage runs with the user, so a Reverse outcome is itself
197
+ user-endorsed; after any reversal, refresh the design-review verdict
198
+ line (new date) before any further adversarial round (a round-2
199
+ reversal refreshes the line before the change is declared reviewed).
200
+
201
+ One round by default; run a second round only if any decision was
202
+ actually reversed. Append a verdict line to the log (or to the draft
203
+ planned-changes statement if there is no log — same homing rules as
204
+ the design-review verdict line):
205
+
206
+ ```
207
+ Adversarial review: passed, N accepted risks — YYYY-MM-DD
208
+ ```
209
+
210
+ ## Track loop
211
+
212
+ A track is one unit in a stacked series: it builds on the tracks
213
+ before it, stands alone as an independently reviewable diff, and
214
+ carries as much of the change as one reviewable diff holds.
215
+
216
+ Sizing (soft bounds): a track of ≤~12 in-scope files folds into a
217
+ neighbor; >~20–25 in-scope files is a split candidate.
218
+
219
+ The user approves the proposed track split before implementation
220
+ starts. Mid-flight changes to the split are re-presented to the user.
221
+
222
+ All development is linear on the single working branch; each track is
223
+ a contiguous commit range. Track numbering is append-only: completed
224
+ tracks never renumber, a replanned remainder gets new numbers, and
225
+ abandoned planned tracks are struck through in the track table — their
226
+ numbers are never reused.
227
+
228
+ Per-track sequence:
229
+
230
+ 1. Implement the track, following the project's own engineering
231
+ guidelines for commit/test/push discipline.
232
+ 2. MANDATORY agent code review of the cumulative track diff
233
+ `git diff <prev-marker>..HEAD` — correctness, test coverage, style,
234
+ API surface, documentation sync — composed per review-rules.md in
235
+ this directory.
236
+ 3. Fix findings as normal commits.
237
+ 4. MANDATORY user review: present the track summary and the track diff
238
+ to the user, then loop on user feedback — landing fixes as normal
239
+ commits — until the user explicitly approves. The agent waits for
240
+ that approval; the marker commit certifies a fully user-reviewed
241
+ track.
242
+ 5. Land the marker commit.
243
+ 6. Update the track's row in the track table (status, scope drift);
244
+ revise the planned-changes statement only if reality diverged from
245
+ it.
246
+
247
+ ### Track table
248
+
249
+ This section owns the track-table constraints. The table is a
250
+ display-only index of the split: track names, one-line scopes,
251
+ statuses — never commit SHAs, and never the source of truth for track
252
+ boundaries (marker commits are, see § Marker commits). It lives in the
253
+ umbrella PR description when publishing is enabled and in the workflow
254
+ log's Track table section when it is disabled.
255
+
256
+ ## Marker commits (source of truth for track boundaries)
257
+
258
+ Format — an empty commit with a zero-padded two-digit track number:
259
+
260
+ ```bash
261
+ git commit --allow-empty -m "Track NN complete: <short name>"
262
+ # e.g. Track 03 complete: cache-refactor
263
+ ```
264
+
265
+ List all boundaries offline:
266
+
267
+ ```bash
268
+ git log --oneline --grep '^Track [0-9]* complete:'
269
+ ```
270
+
271
+ Track N's diff is `marker(N-1)..marker(N)`; track 01's base is the
272
+ merge-base with the repository's default development branch.
273
+
274
+ Properties:
275
+
276
+ - **Rebase-resilient** — markers are in the history being rebased, so
277
+ they move with it.
278
+ - **Zero cleanup** — delivering the change as a single squashed unit
279
+ erases them.
280
+ - The track table is never an alternative source of truth for
281
+ boundaries (constraints owned by § Track table).
282
+
283
+ Single-track changes land no markers — the whole branch diff is the
284
+ track.
285
+
286
+ ## Rebase note
287
+
288
+ After any rebase of the working branch the markers move with the
289
+ rebased history — nothing to fix in this baseline. Any layered process
290
+ that pins refs to marker commits must re-pin them after a rebase.
291
+
292
+ ## Peer review (project-layered)
293
+
294
+ This baseline contains no peer-review process. Projects may layer one
295
+ — e.g. per-track satellite review PRs for separate peer reviewers —
296
+ via a doctrine extension (the `doctrineExtraPath` key in the project's
297
+ `slate.json`). A layered peer review supplements, never replaces, the
298
+ mandatory per-track user review.
299
+
300
+ ## Layering richer workflows on top
301
+
302
+ Richer internal planning and execution machinery — whatever agent
303
+ tooling is in use — may be layered on top of this baseline, provided
304
+ it satisfies the mandatory gates: a user design review before
305
+ adversarial review, pre-implementation adversarial review, an agent
306
+ code review per track, a mandatory user review per track, marker
307
+ commits at track boundaries, and — when draft-PR publishing is
308
+ enabled — the obligations of pr-publishing.md (umbrella draft PR
309
+ before coding starts, user-performed merge). This document defines the
310
+ baseline that applies regardless of the tooling.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Episode compression (ExecPlan D5, D6, D8).
3
+ *
4
+ * An episode is the compressed, structured record of ONE completed thread
5
+ * action. It is produced by a single LLM call over the messages generated
6
+ * during that action, stored at <config dir>/slate/episodes/<id>.md, and returned to
7
+ * the orchestrator as the tool result — it IS the synchronization mechanism.
8
+ *
9
+ * Compressor model resolution (D5): config episodeModel → newest available
10
+ * Anthropic Sonnet → the worker's own model.
11
+ */
12
+
13
+ import { mkdirSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { complete } from "@earendil-works/pi-ai/compat";
16
+ import {
17
+ CONFIG_DIR_NAME,
18
+ convertToLlm,
19
+ serializeConversation,
20
+ type ExtensionContext,
21
+ } from "@earendil-works/pi-coding-agent";
22
+
23
+ const MAX_TRANSCRIPT_CHARS = 300_000;
24
+ const COMPRESSOR_MAX_TOKENS = 4096;
25
+
26
+ const EPISODE_SECTIONS = [
27
+ "## Intent",
28
+ "## Actions Taken",
29
+ "## Key Findings",
30
+ "## Artifacts Changed",
31
+ "## Open Issues",
32
+ "## Handoff Notes",
33
+ ];
34
+
35
+ function compressorPrompt(task: string, transcript: string): string {
36
+ return `You are compressing one completed action of a worker thread into an episode:
37
+ a durable, structured record another agent will rely on WITHOUT seeing the
38
+ raw transcript. Retain decisions, discoveries, exact identifiers (paths,
39
+ symbols, commands, versions, error messages) and outcomes. Drop tactical
40
+ noise (retries, scrolling, dead ends — unless a dead end is itself a finding).
41
+ Note: the transcript covers only THIS action; the thread may legitimately use
42
+ context from its earlier actions that you cannot see — do not flag that as
43
+ fabrication.
44
+ Target 300-800 words. Output ONLY markdown with EXACTLY these sections:
45
+
46
+ ${EPISODE_SECTIONS.join("\n")}
47
+
48
+ The action's task was:
49
+ ${task}
50
+
51
+ Transcript:
52
+ ${transcript}`;
53
+ }
54
+
55
+ async function resolveCompressorModel(
56
+ ctx: ExtensionContext,
57
+ configured: string | undefined,
58
+ workerModel: { provider: string; id: string } | undefined,
59
+ ) {
60
+ if (configured) {
61
+ const slash = configured.indexOf("/");
62
+ if (slash > 0) {
63
+ const m = ctx.modelRegistry.find(configured.slice(0, slash), configured.slice(slash + 1));
64
+ if (m) return m;
65
+ }
66
+ }
67
+ try {
68
+ const available = await ctx.modelRegistry.getAvailable();
69
+ const sonnets = available
70
+ .filter((m: { provider: string; id: string }) => m.provider === "anthropic" && m.id.includes("sonnet"))
71
+ .sort((a: { id: string }, b: { id: string }) => b.id.localeCompare(a.id));
72
+ if (sonnets.length > 0) return sonnets[0];
73
+ } catch {
74
+ /* fall through */
75
+ }
76
+ if (workerModel) return ctx.modelRegistry.find(workerModel.provider, workerModel.id) ?? undefined;
77
+ return undefined;
78
+ }
79
+
80
+ export interface CompressEpisodeOptions {
81
+ ctx: ExtensionContext;
82
+ episodeId: string;
83
+ threadId: string;
84
+ threadName: string;
85
+ task: string;
86
+ status: "ok" | "failed";
87
+ diagnostics?: string; // failure diagnostics (D6)
88
+ messages: unknown[]; // AgentMessages produced during this action
89
+ workerModel?: { provider: string; id: string };
90
+ configuredModel?: string;
91
+ signal?: AbortSignal;
92
+ }
93
+
94
+ export interface CompressedEpisode {
95
+ text: string; // full episode markdown (header + body) as returned to the orchestrator
96
+ file: string;
97
+ compressor: string; // model used, or "(uncompressed fallback)"
98
+ costUsd: number; // USD cost of the compression LLM call (0 on fallback)
99
+ }
100
+
101
+ function lastAssistantText(messages: unknown[]): string {
102
+ for (let i = messages.length - 1; i >= 0; i--) {
103
+ const m = messages[i] as { role?: string; content?: Array<{ type: string; text?: string }> };
104
+ if (m.role === "assistant" && Array.isArray(m.content)) {
105
+ const text = m.content
106
+ .filter((c) => c.type === "text")
107
+ .map((c) => c.text ?? "")
108
+ .join("\n")
109
+ .trim();
110
+ if (text) return text;
111
+ }
112
+ }
113
+ return "(no output)";
114
+ }
115
+
116
+ export async function compressEpisode(opts: CompressEpisodeOptions): Promise<CompressedEpisode> {
117
+ const { ctx } = opts;
118
+ const dir = join(ctx.cwd, CONFIG_DIR_NAME, "slate", "episodes");
119
+ mkdirSync(dir, { recursive: true });
120
+ const file = join(dir, `${opts.episodeId}.md`);
121
+
122
+ let body: string | undefined;
123
+ let compressor = "(uncompressed fallback)";
124
+ let costUsd = 0;
125
+
126
+ try {
127
+ const model = await resolveCompressorModel(ctx, opts.configuredModel, opts.workerModel);
128
+ if (model) {
129
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
130
+ if (auth.ok && auth.apiKey) {
131
+ let transcript = serializeConversation(convertToLlm(opts.messages as never));
132
+ if (transcript.length > MAX_TRANSCRIPT_CHARS) {
133
+ transcript = `[transcript head truncated]\n...${transcript.slice(-MAX_TRANSCRIPT_CHARS)}`;
134
+ }
135
+ if (opts.diagnostics) {
136
+ transcript += `\n\n[dispatch diagnostics: ${opts.diagnostics}]`;
137
+ }
138
+ const response = await complete(
139
+ model,
140
+ {
141
+ messages: [
142
+ {
143
+ role: "user" as const,
144
+ content: [{ type: "text" as const, text: compressorPrompt(opts.task, transcript) }],
145
+ timestamp: Date.now(),
146
+ },
147
+ ],
148
+ },
149
+ {
150
+ apiKey: auth.apiKey,
151
+ headers: auth.headers,
152
+ env: auth.env,
153
+ maxTokens: COMPRESSOR_MAX_TOKENS,
154
+ signal: opts.signal,
155
+ },
156
+ );
157
+ costUsd = response.usage?.cost?.total ?? 0;
158
+ const text = response.content
159
+ .filter((c: { type: string }): c is { type: "text"; text: string } => c.type === "text")
160
+ .map((c: { text: string }) => c.text)
161
+ .join("\n")
162
+ .trim();
163
+ if (text) {
164
+ body = text;
165
+ compressor = `${model.provider}/${model.id}`;
166
+ }
167
+ }
168
+ }
169
+ } catch {
170
+ /* fall back below */
171
+ }
172
+
173
+ if (!body) {
174
+ body = [
175
+ "## Intent",
176
+ opts.task,
177
+ "",
178
+ "## Key Findings",
179
+ "(episode compression unavailable — raw final worker output follows)",
180
+ "",
181
+ lastAssistantText(opts.messages).slice(0, 8000),
182
+ ...(opts.diagnostics ? ["", "## Open Issues", opts.diagnostics] : []),
183
+ ].join("\n");
184
+ }
185
+
186
+ const statusLabel = opts.status === "ok" ? "OK" : "FAILED";
187
+ const header = [
188
+ `# Episode ${opts.episodeId} — thread ${opts.threadId} (${opts.threadName}) — STATUS: ${statusLabel}`,
189
+ "",
190
+ `> task: ${opts.task.replace(/\s+/g, " ").slice(0, 200)}`,
191
+ `> date: ${new Date().toISOString()} | compressor: ${compressor}`,
192
+ ...(opts.status === "failed" && opts.diagnostics ? [`> failure: ${opts.diagnostics.slice(0, 300)}`] : []),
193
+ "",
194
+ "",
195
+ ].join("\n");
196
+
197
+ const text = `${header}${body}\n`;
198
+ writeFileSync(file, text, "utf8");
199
+ return { text, file, compressor, costUsd };
200
+ }