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.
- package/LICENSE +201 -0
- package/README.md +72 -0
- package/docs/design-principles.md +183 -0
- package/docs/pr-publishing.md +123 -0
- package/docs/review-rules.md +172 -0
- package/docs/track-workflow.md +310 -0
- package/extension/episodes.ts +200 -0
- package/extension/handoff.ts +271 -0
- package/extension/index.ts +84 -0
- package/extension/mode.ts +283 -0
- package/extension/paths.ts +19 -0
- package/extension/prompt-docs.ts +49 -0
- package/extension/render.ts +138 -0
- package/extension/state.ts +191 -0
- package/extension/threads.ts +311 -0
- package/extension/tools.ts +134 -0
- package/extension/worker.ts +119 -0
- package/package.json +26 -0
|
@@ -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
|
+
}
|