@mgiles/perk 3.1.0 → 3.2.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.
Files changed (43) hide show
  1. package/extension/doors/address.ts +11 -0
  2. package/extension/doors/dreamWaveTools.ts +29 -15
  3. package/extension/doors/land.ts +6 -0
  4. package/extension/doors/learn.ts +16 -3
  5. package/extension/doors/lifecycleGates.ts +36 -1
  6. package/extension/doors/objectiveStack.ts +423 -23
  7. package/extension/doors/plannotatorHandoff.ts +80 -8
  8. package/extension/doors/prReview.ts +2 -1
  9. package/extension/doors/prReviewBrowser.ts +75 -27
  10. package/extension/doors/ready.ts +209 -17
  11. package/extension/doors/reviewWaveTools.ts +24 -3
  12. package/extension/doors/stackReviewBrowser.ts +573 -0
  13. package/extension/doors/submit.ts +36 -10
  14. package/extension/doors/submitPrReview.ts +116 -19
  15. package/extension/factories/objectivePlan.ts +12 -6
  16. package/extension/factories/objectiveSave.ts +5 -2
  17. package/extension/index.ts +26 -1
  18. package/extension/substrate/config.ts +4 -2
  19. package/extension/substrate/paths.ts +2 -7
  20. package/extension/substrate/resolverLease.ts +363 -0
  21. package/extension/substrate/toolGating.ts +16 -0
  22. package/extension/substrate/workflowState.ts +13 -3
  23. package/extension/waves/adversarialReviewWave.ts +16 -2
  24. package/package.json +1 -1
  25. package/prompts/_fixtures/live.yaml +63 -0
  26. package/prompts/contexts/adapters/tombell-plan.md +4 -0
  27. package/prompts/contexts/plan-authoring.md +6 -5
  28. package/prompts/stages/conflict-resolution-continuation.md +6 -0
  29. package/prompts/stages/conflict-resolution.md +1 -1
  30. package/prompts/stages/objective-author/adopt.md +1 -1
  31. package/prompts/stages/objective-author/file.md +1 -1
  32. package/prompts/stages/objective-author/seed.md +1 -1
  33. package/prompts/stages/objective-reconcile-ready.md +7 -0
  34. package/prompts/stages/objective-sync.md +1 -1
  35. package/prompts/stages/stack-review/cold.md +1 -0
  36. package/prompts/stages/stack-review-browser/stack.md +23 -0
  37. package/shared/README.md +0 -3
  38. package/shared/bindings.yaml +3 -0
  39. package/shared/contracts.md +2010 -1753
  40. package/shared/registry.yaml +16 -1
  41. package/shared/schemas/outputs/objective-stack-status.schema.json +172 -1
  42. package/shared/schemas/outputs/pr-ready.schema.json +110 -2
  43. package/shared/contracts-history.md +0 -605
@@ -1,605 +0,0 @@
1
- # perk cross-plane contracts — history
2
-
3
- The changelog sibling of [`contracts.md`](./contracts.md). It carries the relocated chronological
4
- `Status (…)` history so the spec file stays a compact current-spec document — the durable `## §N.M`
5
- contract bodies live in `contracts.md`; the present-tense-of-a-past-node landing notes live here.
6
- This file ships in **both** build artifacts alongside `contracts.md` (the whole `shared/` dir is
7
- bundled — the Python wheel as package data `perk/_shared/`, the npm package under `shared/`).
8
-
9
- ## Entry convention
10
-
11
- - Entries are **grouped by the originating `§N.M` anchor**, in `contracts.md`'s section order.
12
- - **Chronological within** each group (oldest landing first).
13
- - Each entry is the original `Status (…)` blockquote **verbatim** — keep-and-annotate, never
14
- reword, never "fix" a now-stale claim (the relocation is mechanical; reconciliation judgment
15
- stays out).
16
- - Each group's `§N.M` heading **is** the cross-reference anchor.
17
- - **Exception:** document-opening statuses not bound to a single section live under the leading
18
- **"General / opening"** group below.
19
-
20
- ## General / opening
21
-
22
- > **Status (T2):** specs locked. Implementations land later — state helpers in **T3**, the
23
- > launch/`PERK_RUN_ID` emit in **T4**, the gateway verification ops in **T5** (Python) /
24
- > Phase 1 (TS). Gateway *mutation* ops are named here but **not authored** (payloads land in
25
- > Phase 1, when `/plan-save` knows their shape — `Q7`/`Q9`).
26
- >
27
- > **Status (T5):** the §8.4 **verification ops are implemented in the Python plane**
28
- > (`perk/github/auth.py` — `check_auth` / `check_repo_access`, verification-only, never mutating);
29
- > the TS plane authors the same shapes in Phase 1. The §8.5 init machine-surface contract is
30
- > live (`perk init --json`).
31
- >
32
- > **Status (P1.T2a):** the §8.4 **plan-write mutations are implemented in the Python plane**
33
- > (`perk/github/plans.py` `create_label` / `create_plan_issue` / `add_issue_comment` /
34
- > `find_plan_issue` + `perk/plan.py` storage) — the **cold/worker** save door
35
- > (`perk plan-save`). The warm in-session twin (the TS `/plan-save` tool) is T3. Both planes
36
- > use **REST `gh api`** (never porcelain — porcelain's GraphQL has a separate, often-exhausted
37
- > rate-limit quota) and pass large bodies via `-F body=@file`.
38
-
39
- ## §8.4 · The GitHub gateway contract (Q9/Q10)
40
-
41
- > **Status (P1.T2b):** the plan-ref is **materialized**. T2a emits it (`--json`); T2b persists
42
- > it as the `cache.plan-ref` file (`.pi/workflow/plan-ref.json`, written by the cold door,
43
- > read by both planes) and reconciles it into the `active_plan_ref` session field on
44
- > `session_start` (§8.3).
45
- >
46
- > **Status (P1.T3):** the **warm door** is built. The in-session `plan_save` tool + `/plan-save`
47
- > command **wrap** this cold `--json` write (via process launch + the §3.2 machine-JSON surface —
48
- > **not** a TS reimplementation): they delegate to `perk plan-save --json`, then append
49
- > `active_plan_ref` to link the live session. This is the read-only → read-write boundary; the
50
- > plan→implement transition is the **cold door** (T4, fresh context). `save.writes` is now
51
- > `[github.plan, cache.plan-ref, session.workflow-state]`.
52
- >
53
- > **Status (P1.T4a):** the **cold door** consumes the plan-ref. `perk implement` (no positional —
54
- > the *active* ref; arbitrary `#N` is `perk resume`, T5c) reads `cache.plan-ref` from the repo root,
55
- > **derives a deterministic worktree/branch name `plan-<pr_id>`** (`pr_id` stays a string), creates
56
- > the worktree **idempotently** (an existing one is reused — resume), and **materializes the
57
- > handoff + plan-ref into the worktree** so the launched `pi` (cwd = worktree) reconciles
58
- > `active_plan_ref` on `session_start` (§8.3) with no extension change. The plan-header's `branch`
59
- > field stays `null` until it is recorded at **submit** (T5a). `implement` reads `cache.plan-ref`
60
- > and writes `session.workflow-state` (the worktree link).
61
- >
62
- > **Status (origin-aware create base).** On **create** (not reuse), `perk implement` does a
63
- > **best-effort `git fetch origin`** and bases the new `plan-<pr_id>` branch on **`origin/<trunk>`**
64
- > (trunk via `git symbolic-ref refs/remotes/origin/HEAD`, fallback `main`/`master`, final `main`) —
65
- > so work starts on up-to-date trunk, not stale local HEAD. If the plan's branch already exists on
66
- > the remote it bases off **`origin/<branch>`** (tracking the resumed/remote branch). A
67
- > **`--base <ref>` override wins verbatim** (deliberate stacking on an unlanded branch, even a
68
- > non-origin ref). An **offline fetch failure is non-fatal but warns loudly** and falls back to the
69
- > last-known origin ref (or local HEAD when there is no remote — `base: null`). The
70
- > **reuse/resume** path (an existing worktree) never fetches or re-bases (D4). `--dry-run`/`--json`
71
- > surfaces the resolved start-point as a `base` field (resolved from local refs, no fetch). No
72
- > registry I/O change.
73
- >
74
- > **Status (P1.T4c) — implement gains a plan arg + session priming.** The Phase-1 dogfood run
75
- > surfaced two cold-door gaps and corrected them forward (T4a's no-positional D2 was the deviation
76
- > from phase-1-plan §P1.T4's `perk implement <plan>`): (1) **`perk implement [PLAN]`** is now a
77
- > *dedicated* command — an optional issue number (`perk implement 42`) resolves the plan via
78
- > `github.get_plan`, writes it as the active `cache.plan-ref` (mirroring `perk resume`), then
79
- > launches; omitting it uses the active ref (the T4a behavior). (2) The launcher **primes the
80
- > implement session** — `launch_stage` passes an initial prompt to `pi` (read the plan via
81
- > `gh issue view <n> --comments`, implement on the branch, `/submit` when committed) so the session
82
- > starts working instead of opening idle. Only the `implement` stage is primed; `plan` stays
83
- > user-driven. No registry I/O change (still `reads:[cache.plan-ref]`, `writes:[session.workflow-state]`).
84
- >
85
- > **Status (P1.T5a) + the delegation decision.** The §8.4 opening's "one contract, implemented
86
- > **once per plane**" (a Python gateway *and* a TS gateway, same shapes) was a Phase-0 hypothesis.
87
- > **T3 deviated** (the warm `/plan-save` delegates to `perk plan-save` via `pi.exec`), and T5
88
- > **confirms delegation as the standing pattern for GitHub mutations**: the **Python gateway is
89
- > canonical**; the TS warm doors (`/submit`, and `/land` in T5b) **delegate** to thin Python workers
90
- > (`perk pr submit`/`perk pr land --json`) over the §3.2 machine-JSON channel — they do **not**
91
- > reimplement the writes. (Cache/session tiers keep their per-plane I/O — `cache.ts`/`cache.py` —
92
- > because those are *files*, not GitHub.) The "two gh gateways" idea is retired; there is **one
93
- > canonical Python GitHub gateway**. So **T5a** opens a **draft** PR (`Closes #<issue>` so the
94
- > squash-merge closes the plan), then `update_plan_header` populates the staged `branch=plan-<pr_id>`,
95
- > `pr=<number>`, `lifecycle_stage=impl`. `submit` reads `cache.plan-ref` + `github.plan` and writes
96
- > `github.pr` + `github.plan`.
97
- >
98
- > **Status (P1.T5b):** the **land path** is built. `land` (warm `/land` + cold `perk pr land`)
99
- > marks the PR ready (if draft), **squash-merges** it (idempotent — `already merged` ⇒ success), and
100
- > sets the **`pending-learn`** marker; `learn` (warm `/learn`, TS-only) clears it. The cold worker
101
- > sets the marker on its real run; the warm door also sets it post-delegate (idempotent existence
102
- > file), so each plane's path is independently correct. `land` reads `cache.plan-ref` + `github.pr`
103
- > and writes `github.pr` + `cache.markers`; `learn` reads/writes `cache.markers`. Reconciliation
104
- > typing + the review/`address` loop + deep learn tooling stay Phase 2.
105
- >
106
- > **Status (P1.T5c):** `perk resume <plan>` is built — the cross-stage verb. It reads the plan via
107
- > `get_plan`, **reconstructs `cache.plan-ref`** from the GitHub state, derives the **current
108
- > actionable stage** (no PR → `implement`; PR open → `submit`; PR merged + `pending-learn` →
109
- > `learn`; merged + learned → nothing), then reuses T4a's `launch_stage` (idempotent worktree +
110
- > materialize + `exec pi`). `--dry-run`/`--json` resolve + print without launching (no ref write).
111
- > The resolution is a **pure, unit-tested** function (`perk/run/resume.py`). For `reuse` stages
112
- > (`submit`/`land`/`learn`) it assumes a **local** worktree; recreating one from a remote branch on
113
- > a fresh clone is Phase 2. This closes the spine: `plan → save → implement → submit → land →
114
- > learn`, resumable at any stage.
115
- >
116
- > **Status (P1.T6 — the Phase-1 gate; + T4c/T3b corrections).** The spine is **closed end-to-end and
117
- > dogfooded** — perk shipped a real change (`prek` + a ruff hook) through its own loop on its own
118
- > repo (plan #1 → PR #2 merged → learned; `perk resume 1` reports "nothing to resume"). The gate run
119
- > is recorded in [`phase-1-gate.md`](../docs/planning/phase-1-gate.md). Two dogfood-surfaced fixes
120
- > converged forward: **T4c** — `perk implement [PLAN]` takes a plan arg and `launch_stage` **primes**
121
- > the implement session (it launched bare/idle before); **T3b** — `save` fails fast while plan mode
122
- > is active and the `plan_save` tool (explicit `plan` param) is the canonical save (the borrowed
123
- > `pi-plan` emits no structured plan, so the `<proposed_plan>` scrape was dropped). Neither changed
124
- > any stage's state-I/O. The registry per-stage `requires`/`reads`/`writes` + `doors` are filled for
125
- > all six spine stages.
126
- >
127
- > **Status (P2.T8a):** the **submit body is deepened + the issue-numbered-footer bug is fixed**.
128
- > `perk pr submit` composes an HTML-enhanced GitHub PR body (best-effort verbatim-plan `<details>`
129
- > embed via `get_plan_body`) and appends the checkout footer via **create-then-update**
130
- > (`update_pr_body`) carrying the **PR** number, then runs `validate_pr_body` as a post-write
131
- > self-check (`pr_check_failed` on failure). A thin `perk pr check --json` is the supervisor surface.
132
- > Submit keeps the PR **draft**; the new `perk pr ready` (warm `/ready`) is the deliberate review
133
- > gate. The two-target split is explicit: HTML in the GitHub body, plain text in the squash commit
134
- > (deepened at T8b). `submit`'s registry I/O is unchanged.
135
- >
136
- > **Status (P2.T8b):** `/land` + `/learn` are **deepened**. Land's squash commit message is now
137
- > plain `"<plan title>\n\nCloses #N"` (fallback on empty title) — the second of the two PR targets.
138
- > `/learn` graduates to a real knowledge-capture pass: with a `summary` it creates a `perk:learn`
139
- > issue (idempotent via the **`perk:learn`-scoped `find_learn_issue`** — label + `learn-header`
140
- > block, so it never matches the plan issue) + a back-link comment, then clears `pending-learn`;
141
- > without one it stays the thin marker-clear. `learn` reads `[cache.markers, cache.plan-ref]` and
142
- > writes `[cache.markers, github.learn, github.comments]` (the new `github.learn` key). The
143
- > reconciliation-typing vocabulary (Mechanical/Reconcilable/Immutable) is established; only the
144
- > deterministic **Mechanical** type is applied this turn (Reconcilable + objective reconciliation are
145
- > **implemented in P2.T11** — see the P2.T11 subsection of §8.4).
146
- >
147
- > **Status (P2.T8c — the CLI plumbing slice).** The `--remote` stub graduates to a real **target
148
- > resolver** (`launch.resolve_target(stage, remote) -> Target`, pure + unit-tested): `None` → local
149
- > (unchanged); a `cold_remote:false` stage → `UserFacingCliError`/`remote_blocked`; a
150
- > `cold_remote:true` stage → a `RemoteTarget` descriptor (runner ref + run_id→plan linkage) surfaced
151
- > in `--dry-run`/`--json`, then a stable `UserFacingCliError`/`remote_not_driven` exit (it does **not**
152
- > persist intent or trigger a runner — the Phase-3 consumer is not built, cli-vs-pi §4.5). The
153
- > registry now records `doors.cold_remote: true` on **`implement` + `address`** (the agentic,
154
- > headless-runnable stages a Phase-3 CI worker drives) and `false` on the other five — the reused
155
- > seam = resolver + validated registry doors + the `--json` target descriptor. **Phase 2 builds and
156
- > resolves the target; Phase 3 drives it.** The `--remote` help text on the three launchers is
157
- > reconciled from "Phase 3; currently blocked" to "Local (default) or a remote runner; remote
158
- > dispatch is driven by the Phase-3 worker."
159
-
160
- ## §8.9 · Skill bindings (the trigger→skill delivery contract)
161
-
162
- > **Status (Node 2.3):** cold-door (Python) **and** warm-door (TS) delivery landed, **and** perk's own
163
- > hardcoded "Follow the … skill" strings are migrated onto the mechanism + deleted (Node 2.3) — the
164
- > skill-binding mechanism is now the single delivery path for perk's own nudges. The render header
165
- > was neutralized to `"The following skill binding(s) apply here:"` (the `.pi/perk.toml` parenthetical
166
- > was false for the delivered perk defaults). Known residual (out of scope, documented): in a cold
167
- > `learn-docs` session, after compaction Mechanism A re-renders the borrowed `stage:plan` and injects
168
- > `perk-plan` rather than `perk-learn-docs` — benign (learn-docs *is* a planning factory); a
169
- > pre-existing stage-vs-command `binding_trigger` quirk. Deferred: `doctor` target-existence
170
- > validation → **Node 3.1**; `init` `[[bindings]]` template + user docs → **Node 3.2**.
171
- >
172
- > **Status (Node 3.1):** `doctor` target-existence/skill-presence validation landed (the non-fatal
173
- > `bindings` check), plus the injection-time skill-presence mirror (the `nudge` path warns;
174
- > `bindingSuffix` now logs its warnings). Deferred: `init` `[[bindings]]` commented template + user
175
- > docs → **Node 3.2**.
176
- >
177
- > **Status (Node 3.2):** the `init` `[[bindings]]` commented template + user docs landed, resolving
178
- > the deferral above. `PERK_TOML_TEMPLATE` now seeds a comment-only `[[bindings]]` block documenting
179
- > `trigger` / `skill` / `mode` and the nudge-vs-transclude choice; `PERK_LOCAL_TOML_TEMPLATE` records
180
- > the whole-array-replace override rule. README gains a `## Skill bindings` user section. The seeded
181
- > block is inert (comment-only) — a fresh repo still resolves to zero user bindings and `doctor`
182
- > stays exit-0 (pinned by a `tests/test_config.py` regression).
183
-
184
- ## §8.10 · Provider selection (the supported-set registry + the `[providers]` selection)
185
-
186
- > **Status (Node 2.1):** ships the selection **substrate** only — `shared/providers.yaml`, the two
187
- > shape-only loaders + the pure resolver, the `[providers]` config-reading in both planes, the
188
- > two-directional `init` wiring, and the `doctor` selection cross-check. The concrete adapter shims
189
- > (`planAdapterTombell`, `todoAdapterJuicesharp`) are **Nodes 2.3 / 3.2**; the read-only tool-gate
190
- > (`extension/substrate/toolGating.ts`, Invariant 1) is untouched.
191
- >
192
- > **Status (Node 2.2):** lands the TS resolver (`resolveProviders`) and the **plan-seam runtime
193
- > deferral** — perk's `planMode` authoring surface (`/plan`, `Ctrl+Alt+P`, `--plan`, the
194
- > `perk:plan-context` injection) steps aside when the resolved `[providers] plan` ≠ `perk-plan`
195
- > (fail-safe to the reference). `savePlan`/`plan_save`/`/plan-save`/the read-only gate are
196
- > seam-shared substrate — always-registered, the produced-contract landing the Node 2.3 adapter
197
- > bridges to — and do **not** defer. The **todo**-seam deferral (`checkpoints`) is still **Node 3.1**.
198
- >
199
- > **Status (Node 2.3):** the **first 3rd-party plan adapter** lands `tombell-plan` as a real,
200
- > selectable plan provider. (1) The shipped entry drops `package_filter` (the illustrative
201
- > `extensions/*.ts` matched nothing — `@tombell/pi-plan`'s sole extension is its root `index.ts`,
202
- > so omitting the filter loads exactly that one extension); the `package_filter` field stays in the
203
- > vocabulary for future providers. (2) perk's plan surface now **vacates at REGISTRATION time** (not
204
- > just handler-time): `registerPlanMode` resolves the plan provider once at factory time and, under a
205
- > foreign selection, registers NONE of `/plan` / `Ctrl+Alt+P` / `--plan` / the injection — so the
206
- > foreign surfaces are the sole registrants (Pi suffixes duplicate command names, so handler-time
207
- > deferral alone is insufficient once the foreign package is loaded). Fail-safe to the reference
208
- > registers everything. (3) The new `extension/adapters/planAdapterTombell.ts` shim is an **injection-only**
209
- > bridge — always registered, inert unless `[providers] plan = "tombell-plan"`, injecting a hidden
210
- > `perk:plan-adapter-tombell` context that directs the foreign free-form prose `/plan` output into
211
- > perk's canonical save. The prose→plan-ref bridge **reuses the existing** `/plan-save`
212
- > `extractPlanMarkdown` scrape (planSave.ts); no new save machinery. The shim **never** owns or
213
- > duplicates the read-only gate and **never** calls `setActiveTools` (Invariant 1 — the gate stays
214
- > perk's, engaged by the cold-door launch; the foreign package self-enforces ad-hoc). (4) The adapter
215
- > does **NOT** restamp `cache.plan-ref.provider` — a tombell-authored prose plan lands with
216
- > `provider="github"` exactly like a perk-authored plan; the authoring-provider id lives only in the
217
- > `[providers] plan` selection, and all downstream stages bind only to the provider-agnostic
218
- > plan-ref (unchanged).
219
- >
220
- > **Status (Node 3.1):** lands the **todo-seam runtime deferral** — perk's `checkpoints` reference
221
- > surface (`session_start`/`session_tree`/`turn_end` render + the `/checkpoints` command) steps
222
- > aside when the resolved `[providers] todo` ≠ `perk-checkpoints` (`resolvedTodoProviderId` /
223
- > `isPerkCheckpointsReferenceSelected`, fail-safe to the reference) — the exact todo-seam mirror of
224
- > the Node 2.2 plan-seam deferral: silent early-returns on the event handlers, an announced deferral
225
- > on `/checkpoints`. The pure checkpoint helpers + the `perk:checkpoint` entry + `## Steps` seeding
226
- > are seam-shared substrate (untouched). **Runtime** deferral only — the concrete
227
- > `@juicesharp/rpiv-todo` adapter is **Node 3.2** (which, per Correction 1 below, adds **no**
228
- > registration-time vacating: the todo seam has no command-name collision, so runtime deferral is
229
- > already sufficient — the forward-assumption here that registration-time vacating would be needed
230
- > turned out not to transfer from the plan seam).
231
- >
232
- > **Status (Node 3.2):** the **first 3rd-party todo adapter** lands `juicesharp-todo` as a real,
233
- > selectable todo provider (no longer illustrative); the todo seam is **behavior-complete**. (1) The
234
- > shipped entry carries no `package_filter` (single-concern checklist overlay — mirrors the tombell
235
- > case). (2) **NO registration-time vacating** (an explicit deviation from the Node 3.1
236
- > forward-assumption): the plan seam needed it only because perk and `@tombell/pi-plan` both register
237
- > `/plan` (Pi suffixes duplicate names); the todo seam has **no command-name collision** — perk
238
- > registers `/checkpoints`, the foreign overlay registers its own differently-named command(s) — so
239
- > Node 3.1's runtime deferral is already sufficient. (3) The new
240
- > `extension/adapters/todoAdapterJuicesharp.ts` shim is an **injection-only**, **active-workflow-gated**
241
- > (`active_plan_ref != null`) bridge — always registered, inert unless `[providers] todo =
242
- > "juicesharp-todo"`, injecting a hidden `perk:todo-adapter-juicesharp` context that carries perk's
243
- > implement-progress **discipline** (seed from `## Steps`, mark each item complete in order) onto the
244
- > foreign overlay. (4) It does **NOT** write `perk:checkpoint` or revive the deferred marker scanner
245
- > (Correction 2): that entry is a transient TS-only overlay nothing downstream consumes, and perk's
246
- > render + scanner are already deferred (Node 3.1), so re-populating it would be dead duplication —
247
- > the lighter bridge the todo seam's lack of a downstream consumer permits. The shim **never** owns
248
- > the read-only gate, **never** `setActiveTools`, and **never** restamps any provider field.
249
- > Validation record: `docs/design/provider-smoke-juicesharp-todo.md`.
250
- >
251
- > **Status (plannotator-plan):** the **second 3rd-party plan adapter** lands `plannotator-plan`
252
- > (→ `npm:@plannotator/pi-extension`, `adapter: planAdapterPlannotator`) — the first provider with
253
- > the **AUGMENT posture** (contrast tombell's REPLACE posture). (1) Plannotator does **not** replace
254
- > perk's plan surface: perk's `/plan` command, the `perk:plan-context` authoring injection, and the
255
- > read-only gate **stay registered**; `registerPlanMode` is now a **three-tier** branch — full
256
- > registration for `perk-plan` (and the fail-safe error path), a **partial vacate** under
257
- > `plannotator-plan` (skip only the `--plan` flag + the `Ctrl+Alt+P` shortcut + the `--plan`
258
- > session_start handler — the two real registration collisions; duplicate flag/shortcut
259
- > registration is the potentially-fatal Pi behavior), and the full vacate for any other foreign id
260
- > (tombell, unchanged). (2) **`plan_review` is the backend-neutral review door** (Node 2.5,
261
- > `extension/factories/planReview.ts`): the `plan` param is **optional/fallback** — the reviewed plan
262
- > resolves **file-first** via `resolvePlanSource` (the validated `plan-draft.md` artifact → the
263
- > param; the **transcript tier is explicitly excluded from review** — no draft + no param
264
- > soft-skips with `reason: "no_plan"` and a `plan_draft` redirect, since an approval would
265
- > otherwise auto-save scraped conversation bytes). **Dispatch:** when `plannotator-plan` is
266
- > selected the door runs the **event-bus bridge** (`createPlannotatorBridge`, kept in
267
- > `extension/adapters/planAdapterPlannotator.ts`): it emits plannotator's published `plannotator:request`
268
- > plan-review envelope on the in-process `pi.events` bus (pinned against
269
- > `@plannotator/pi-extension@0.20.0`), awaits the in-payload `respond` handshake bounded at 5s,
270
- > then awaits the human decision on `plannotator:review-result` (no decision timeout; honors the
271
- > turn-abort signal). On **ANY other selection** (perk-plan, tombell, unknown ids) the door runs
272
- > the **first-party in-TUI editor review** (`runFirstPartyReview`): the plan is displayed in pi's
273
- > built-in `ctx.ui.editor` dialog (scrollable; Ctrl+G opens the user's external `$EDITOR`); a
274
- > non-blank human edit differing from the displayed plan is **written back to the draft via
275
- > `writePlanDraft` BEFORE the verdict** (reviewed bytes == artifact bytes == saved bytes — a
276
- > failed write-back **aborts the review fail-open** with a loud `unavailable` warning, nothing
277
- > saved); then a 3-option approve/deny/skip `ctx.ui.select` verdict, with optional deny feedback
278
- > via a second editor dialog. **Esc anywhere = fail-open skip** (`reason: "dismissed"`,
279
- > mirroring `ask_user_question`'s dismissal — deny is always explicit); `ctx.ui.editor` takes no
280
- > AbortSignal, so `signal?.aborted` is checked between dialogs (the aborted arm wins over an
281
- > in-flight dialog's result). An **APPROVED** decision (either backend) wires into the
282
- > **`approvalSave` seam** (auto-save → D1a gate exit on success → a **terminating** result; on
283
- > the first-party path the saved bytes carry any write-back edits and the result flags
284
- > `edited: true`; the objective node link is recovered from the `objective_node_claim` carrier
285
- > inside `savePlan`; a failed save is non-terminating, leaves the gate read-only, and directs
286
- > the human `/plan-save` failsafe). A human **DENY** is strict: feedback returned with a
287
- > directive to rewrite the working draft via `plan_draft` + re-review. **The objective-author
288
- > arm (#352 Node 2.2):** in an objective-author session the door routes to
289
- > `executeObjectiveReview` — the review subject is the **rendered objective draft** (§8.1's
290
- > `readObjectiveDraft` + `renderObjectiveDraft`; the `plan` param is decoded first — a mistyped
291
- > param still `bad_input` — but never a source), dispatched to the same backends; the
292
- > first-party editor runs **view-only** (edits are never written back — deny+feedback is the
293
- > change channel) with objective verdict labels. An **APPROVED** outcome (#352 Node 2.3) wires
294
- > into the **`objectiveApprovalSave` seam** (the structured artifact is re-read at save time —
295
- > never the rendered bytes → `saveObjective` → D1a gate exit on success): a successful save is a
296
- > **terminating** result (`details.subject: "objective"`, `saved: true`, `gateExited`,
297
- > `terminate: true`); a failed save is non-terminating, leaves the gate read-only, and directs
298
- > the human `/objective-save` failsafe.
299
- > **Fail-open semantics:** headless (`!ctx.hasUI`) / dismissed / handshake-timeout /
300
- > `unavailable` / `error` all **soft-skip** with a result instructing the model to present the
301
- > plan to the user directly — plan authoring never wedges. `plan_review` is in
302
- > `READ_ONLY_TOOLS` so review happens **inside** plan mode, before the gate ever comes off.
303
- > (3) The plannotator adapter shim is **injection-only again** (Node 2.5): it owns the hidden
304
- > `perk:plan-adapter-plannotator` context (gate-active AND selected; **two content flavors, one
305
- > customType** — the plan bridge context, or `OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT` in an
306
- > objective-author session, whose marker `[OBJECTIVE ADAPTER: PLANNOTATOR]` the strip handler
307
- > also covers)
308
- > plus the bridge core, and otherwise keeps the standard adapter hygiene: never
309
- > `setActiveTools`, never a `tool_call` handler, never restamps `cache.plan-ref.provider` (stays
310
- > `"github"`); the door composes the gate and the save **only** through the `approvalSave` seam
311
- > (never owns the gate, never writes GitHub itself). The catalog entry carries no
312
- > `package_filter` (`pi.extensions: ["./"]` — the sole extension is the package root).
313
- >
314
- > **Status (Node 2.6):** the tombell bridge context is **re-aimed to review-first**
315
- > (`plan_draft` → `plan_review` → the first-party in-TUI review → `approvalSave` auto-save), with
316
- > the present + `/plan-save` flow as its explicit fail-open arm (see §8.10's interactive save
317
- > discipline). The injection is now **conditioned** — it fires only when perk's gate is read-only
318
- > (per the persisted `perk:workflow-state.mode`) **or** tombell's own persisted `plan-mode-state`
319
- > entry has `enabled: true` (latest wins), and never in an objective-author session — replacing
320
- > Node 2.3's unconditional-on-selection injection.
321
- >
322
- > **Status (askuser — the third seam, an INTERFACE seam):** a third seam, **`askuser`**, lets a repo
323
- > swap perk's first-party `ask_user_question` tool (`extension/doors/askUser.ts`) for the foreign
324
- > `@juicesharp/rpiv-ask-user-question` extension, which registers a tool with the **identical name**
325
- > `ask_user_question` (a richer multi-question dialog). (1) **Interface seam, not artifact seam:**
326
- > ask-user produces **no** durable state key or session-entry vocabulary (no `cache.plan-ref` /
327
- > `perk:checkpoint` analogue); its stable contract is the **tool name `ask_user_question` + its
328
- > non-terminating-answer semantics**. (2) **Vacate-only adapter** (`adapter: null` in
329
- > `providers.yaml`, **no shim module**, no injected context): the foreign tool self-documents via
330
- > its own `promptGuidelines`, so there is nothing to bridge. (3) **Registration-time vacating** in
331
- > `registerAskUser` (mirroring the plan seam's `registerPlanMode`): because the foreign tool shares
332
- > the **exact** name `ask_user_question` and tools — unlike commands — are **not** `:N`-suffixed
333
- > (they replace/warn by extension load order, non-deterministically), `registerAskUser` resolves the
334
- > provider id once at factory time (`resolvedAskUserProviderId(process.cwd())`, fail-safe to
335
- > `perk-ask-user`) and **early-returns before `pi.registerTool`** under any foreign selection,
336
- > leaving exactly one `ask_user_question` standing. The default/fail-safe path registers exactly as
337
- > before (zero behavior change). (4) The foreign package is **two-directionally** wired by
338
- > `_converge_provider_packages` (installed only when selected, removed on deselect), so under the
339
- > default the foreign package is never loaded and perk's tool is the sole registrant. (5) **No
340
- > `READ_ONLY_TOOLS` / `SDK_READ_ONLY_TOOLS` change:** `ask_user_question` is already in
341
- > `READ_ONLY_TOOLS` (`extension/substrate/toolGating.ts`), so the foreign same-named tool is
342
- > allowlisted in read-only/plan mode automatically (the shared-name allowlist precedent); the
343
- > read-only notice interpolates `READ_ONLY_TOOLS` so it self-updates. `SDK_READ_ONLY_TOOLS`
344
- > (`extension/worker/readOnlySession.ts`) intentionally does **not** include `ask_user_question`
345
- > (headless children never prompt a human) — unchanged. Catalog entry carries no `package_filter`
346
- > (verified manifest `{"extensions": ["./index.ts"]}`). Validation record:
347
- > `docs/design/provider-smoke-juicesharp-ask-user.md`.
348
- >
349
- > **Status (footer — the fourth seam, a SECOND INTERFACE seam):** a fourth seam, **`footer`**, lets a
350
- > repo swap perk's own footer (`installPerkFooter`, `extension/surfaces/surfaces.ts`) for a foreign
351
- > footer package — either `powerline-footer` (→ `npm:pi-powerline-footer`) or `pi-bar-footer`
352
- > (→ `npm:pi-bar`). (1) **Interface seam, not artifact seam** (mirrors askuser): the footer produces
353
- > **no** durable state key or session-entry vocabulary; its “contract” is purely the rendered footer
354
- > surface. (2) **Vacate-only adapter** (`adapter: null` for **both** foreign entries, **no shim
355
- > module**, no injected context): both foreign footers already **render extension statuses**, so
356
- > perk's composed `perk` `setStatus` slot (the objective + checkpoints segments, published
357
- > unconditionally by `createPerkStatus`/`checkpoints.ts` independent of footer ownership) appears in
358
- > the foreign footer automatically — the bridge is automatic, there is nothing to shim. (3)
359
- > **Install-site (runtime) vacating, NOT registration-time vacating** (the key divergence from
360
- > askuser/plan): perk installs its footer inside the `session_start` event handler (not at
361
- > factory-bind), so the natural mechanism is a **runtime guard at that single install site, keyed off
362
- > `ctx.cwd`** — `index.ts` calls `installPerkFooter` only when
363
- > `isPerkFooterReferenceSelected(ctx.cwd)` (`extension/surfaces/footerProvider.ts`,
364
- > `resolvedFooterProviderId` fail-safe to `perk-footer`). The easier tier: `ctx.cwd` flows through the
365
- > event, so tests need no `process.chdir`. (4) The foreign package is **two-directionally** wired by
366
- > `_converge_provider_packages` (installed only when selected, removed on deselect), so under the
367
- > default (`perk-footer`) the foreign package is never loaded and perk owns the footer exactly as
368
- > before (zero behavior change — the hard guarantee). (5) **No `surfaces.ts` change:** `perkFooter` /
369
- > `installPerkFooter` stay the reference footer; the only change is whether `index.ts` calls it.
370
- > Catalog entries carry no `package_filter` (each package ships a single footer extension).
371
- >
372
- > **Status (web — the fifth seam, a THIRD INTERFACE seam with a NOVEL foreign default):** a fifth
373
- > seam, **`web`**, lets a repo swap its web-research provider among three packages: `pi-web-access`
374
- > (the **default** — zero-config Exa search + content fetch + the bundled `librarian` skill,
375
- > exactly today's behavior), `ollama-web-search` (→ `npm:@ollama/pi-web-search`, needs a local
376
- > Ollama daemon) and `juicesharp-web-tools` (→ `npm:@juicesharp/rpiv-web-tools`, needs an API key;
377
- > default provider Brave; registers a `/web-tools` command — no perk collision). (1) **Interface
378
- > seam, not artifact seam** (mirrors askuser/footer): web produces **no** durable state key or
379
- > session-entry vocabulary; its “contract” is the loose “web search + fetch capability is
380
- > available”. (2) **The NOVEL property — the first non-null-package default:** perk owns **no**
381
- > native web implementation, so the behavior-preserving reference (`pi-web-access`) is itself a
382
- > **foreign npm package** — its `default: true` entry carries a **non-null `package`** (every prior
383
- > seam's default was `package: null`). This needs **no** substrate change: `_converge_provider_packages`
384
- > already builds `desired` from every resolved provider's truthy `package` and the managed-identity
385
- > set from every non-null `package`, and `validate()` enforces only exactly-one-default-per-seam
386
- > (it never required a default to be `package: null`). (3) **Vacate-only adapter** (`adapter: null`
387
- > for **all three** entries, **no shim module**, no injected context) with **no surface to vacate**
388
- > at all: perk registers **no** web tools of its own, so unlike askuser (registration-time vacating)
389
- > or footer (install-site vacating) there is **nothing** to step aside — selection simply **swaps**
390
- > which web package `_converge_provider_packages` installs. The entire seam is Python convergence +
391
- > the census widening + the read-only allowlist. (4) **Static union allowlist, no normalization:**
392
- > the three packages expose **divergent** tool names (`web_search`/`code_search`/`fetch_content`/
393
- > `get_search_content` vs `ollama_web_search`/`ollama_web_fetch` vs `web_search`/`web_fetch`), and
394
- > perk does **not** normalize them — `READ_ONLY_TOOLS` (`extension/substrate/toolGating.ts`) carries
395
- > the **union** of all known web tool names, inert when a package is absent (the shared-name
396
- > allowlist precedent). `SDK_READ_ONLY_TOOLS` (`extension/worker/readOnlySession.ts`) intentionally
397
- > omits them (headless children) — unchanged. (5) The foreign package is **two-directionally** wired
398
- > by `_converge_provider_packages` (installed only when selected, removed on deselect); under the
399
- > default the committed `npm:pi-web-access` entry stays installed (now **provider-managed**, no
400
- > longer in `BORROWED_PACKAGES`). (6) **`librarian` is accepted as lost under a foreign web
401
- > selection** — it is pi-web-access-specific (it depends on `fetch_content`'s GitHub-clone path),
402
- > documented and not re-homed. Catalog entries carry no `package_filter` (each package's sole
403
- > extension is its root `./index.ts`, verified via `npm view <pkg> pi`).
404
- >
405
- > **Status (review — the sixth seam, the first DISPATCH seam; RETIRED 2026-07, Objective #1261
406
- > Node 4.1):** a sixth seam, **`review`**, existed from the toolbox objective's nodes 1.x through
407
- > 4.1 and is now **retired**. Its shape while live: the first **DISPATCH posture** — no adapter
408
- > (the seam produces no durable artifact) and nothing to vacate (perk owned no prior guest-review
409
- > surface); the selection drove **protocol dispatch** inside a dispatching `/review` door — which
410
- > review surface the door drove (`hunk`, the default: the hunk session CLI handshake, with perk's
411
- > `submit_pr_review` tool as the only posting path; or `plannotator-review`: the `code-review`
412
- > `pi.events` bridge behind an `open_plannotator_review` tool plus the agent-driven
413
- > external-annotations HTTP stream, with the UI's native platform-posting as the GitHub path) and
414
- > which posting path was primary. The default `hunk` was the first `default: true` entry whose
415
- > substrate is an **external CLI** (npm `hunkdiff`) rather than a Pi package;
416
- > `plannotator-review` shared `npm:@plannotator/pi-extension` with `plannotator-plan`, the
417
- > cross-seam instance of the desired-**union** package convergence. **The retirement (Node 4.1):**
418
- > once the surface-named doors (`/pr-review-terminal`, `/pr-review-browser`) absorbed both arms
419
- > (nodes 2.1–3.1), the dispatch had no job left — the command IS the selection. `/review`, its
420
- > dispatch, `open_plannotator_review`, and the seam rows were deleted; `SEAMS`/`PROVIDER_SEAMS`
421
- > shrank back to five; the retired `[providers] review` key got the legacy-tripwire treatment
422
- > (hard-fail on the Python plane, silent-ignore on the TS plane); the hunk-CLI init/doctor
423
- > handling was already unconditional (a review *surface*, not a selection consequence) and is
424
- > unchanged; the plannotator package is desired via `plannotator-plan` alone (the union mechanism
425
- > stays generic, its only cross-seam instance retired). `submit_pr_review` re-homed to
426
- > `extension/doors/submitPrReview.ts` with an unchanged contract; the `perk-review` skill split
427
- > into `perk-pr-review-terminal` + `perk-pr-review-browser`.
428
- >
429
- > **Status (askuser + todo seams — RETIRED 2026-08, Objective #1416):** both seams are retired to
430
- > **required borrows** (`npm:@juicesharp/rpiv-ask-user-question`, `npm:@juicesharp/rpiv-todo` in
431
- > `BORROWED_PACKAGES`) after the first-party deletions left each seam exactly one selectable
432
- > provider (the borrow-vs-seam criterion: a seam with a single implementation is a borrow, not a
433
- > seam); the census narrowed to `plan`/`footer`/`web`. The first-party `ask_user_question` tool
434
- > and the checkpoint substrate (`extension/checkpoints/`, the `[WIP:n]`/`[DONE:n]` grammar,
435
- > `/checkpoints`, `setStandingWidget`) are deleted; `RETIRED_PROVIDER_KEYS` grew `askuser` +
436
- > `todo` (hard-fail with removal guidance on the Python plane; TS ignores); `step_marker` is
437
- > deprecated/never-emitted (§8.12); the per-worktree `plan.md` snapshot is repurposed to review
438
- > fidelity (§8.1). Nodes 1.1/2.1/2.2/2.3/2.4.
439
-
440
- ## §8.31 · The prompt render seam + golden parity (Objective #791, Node 1.2)
441
-
442
- **First prompt moved onto the seam — the plan-read instruction (Node 2.1).** The cross-plane
443
- plan-read instruction (the "how do I read the saved plan" SSOT) is the first real (non-fixture)
444
- consumer of the render seam. Its three arm templates live at
445
- `prompts/common/plan-read/{github,linear,other}.md` — one file per provider arm, no
446
- conditionals/loops in the frozen subset. **Branching stays in code**: `perk/run/launch/prompts.py::
447
- _plan_read_instruction` and `extension/doors/lifecycleGates.ts::planReadInstruction` keep their
448
- `(provider, pr_id/prId, url)` signature and the same if/elif/else, each arm now a `render(...)` call
449
- selecting its arm template (passing `{pr_id, url}`; jinja2/nunjucks ignore unused vars). The helpers
450
- still branch on `cache.plan-ref.provider` — only the **wording source** moved.
451
-
452
- The arm templates (and their golden files) carry **no trailing newline** — the helper returns
453
- single-line strings embedded mid-prompt, so the render output must equal the prior literal exactly
454
- (a deliberate departure from the fixture convention of trailing newlines). The three `plan-read-*`
455
- golden cases in `cases.yaml` prove cross-plane byte-identity for each arm; a thin per-arm selection
456
- test in each plane (`tests/test_worker_prompt_parity.py`, `extension/doors/lifecycleGates.test.ts`)
457
- proves the code picks the right arm and `render()` is wired. This golden-fixture parity (plus the
458
- selection tests) **replaces the prior dedicated substring parity** for plan-read; the
459
- implement/learn prompt parity suites are untouched (they embed the byte-identical helper output, so
460
- they keep passing — the downstream prompts move in nodes 2.2/2.4).
461
-
462
- **Second prompt moved — the implement primer (Node 2.2).** The implement-stage primer wording lives
463
- at `prompts/stages/implement.md`, the second real consumer of the render seam. All three sites that
464
- used to hand-duplicate it — cold `perk/run/launch/prompts.py::_implement_prompt`, worker
465
- `extension/worker/worker.ts::initialPromptFor` (implement arm), and warm
466
- `extension/doors/lifecycleGates.ts::implementHandoffPrompt` — are now thin `render("stages/
467
- implement.md", {provider, pr_id, url, read_cmd})` calls. The prior warm/cold variance (the warm
468
- handoff omitting the "Progress markers:" tail) is reconciled by **unifying**: all three render the
469
- one template with the same vars, so they are **byte-identical** and the warm handoff now carries the
470
- progress markers too. `read_cmd` is the provider-selected plan-read instruction computed in code via
471
- the Node-2.1 helper — branching stays in code, no `{% if %}`/second template. The template and its
472
- golden (`implement-github`) carry **no trailing newline** (matching the prior cold/worker literal).
473
- One golden case proves the template renders identically in both planes; thin per-plane composition
474
- tests (start-with / contains read_cmd / ends-with the progress tail) prove each helper wires the
475
- right template + vars — together these **replace `IMPLEMENT_SUBSTRINGS`**. The pre-objective audit
476
- `docs/design/prompt-language-audit.md` (still describing warm as a shorter near-copy) is left as a
477
- frozen snapshot; this paragraph is the authoritative current-state note.
478
-
479
- **The address prompt moved onto the seam — converging three consumers (Node 2.3).** The
480
- address-stage wording lives in two canonical templates `prompts/stages/address/{action,preview}.md`
481
- (each a complete body, no template logic; vars `{{ provider }}`, `{{ pr_id }}`, `{{ url }}`,
482
- `{{ model_clause }}`), rendered identically by **all three** address consumers via the shared
483
- render seam: the cold `perk/run/launch/prompts.py::_address_prompt`, the worker
484
- `extension/worker/worker.ts::initialPromptFor("address")`, and the warm
485
- `extension/doors/address.ts::addressGuidance`. Before this node the warm `/address` loop used a
486
- *different* wording; the three were **converged** onto one canonical body — the cold/worker
487
- structure (the PR-identity header a fresh headless worker needs) **plus** warm's Plan File Mode
488
- step, which now upgrades the cold/worker path too; warm loses its divergent framing. This is a
489
- deliberate wording change to all three surfaces; the *command/flag/config* surface of `/address`
490
- and `perk pr address` is unchanged.
491
-
492
- **Branching stays in code** (the frozen subset has no conditionals): preview vs action is a
493
- template *selection* (`preview.md` for `--preview`, which omits the action steps including Plan
494
- File Mode; `action.md` otherwise), and the classifier present/absent split builds the
495
- `model_clause` render var in code (empty string when no `[subagents] review-classifier` model) —
496
- the clause's own wording is deferred to node 3.3. The worker has **no preview path** (preview is a
497
- warm/cold flag), so it always renders `action.md`.
498
-
499
- **The warm door is now ref-aware and null-guarded.** The converged body carries the PR identity, so
500
- `addressGuidance` takes the active `PlanRef`; the `/address` handler resolves it via the same
501
- helper `doors/learn.ts` uses (`readPlanRef(ctx.cwd)` → fallback
502
- `rebuildWorkflowState(branchOf(ctx)).active_plan_ref`). A null ref reports a `warning` (mirroring
503
- the `/implement` guard) and sends no guidance — a strict improvement, since `/address` cannot
504
- function without a plan-ref regardless (the classifier child's `perk pr feedback` hard-errors
505
- `no_plan_ref`).
506
-
507
- The two address templates (and their golden files) carry **no trailing newline** (the builders
508
- return mid-prompt strings). Four `address-*` golden cases in `cases.yaml` (action/preview × model
509
- present/absent) prove cross-plane byte-identity; thin per-plane selection tests prove each caller
510
- picks the right template and injects/omits the model clause, and the warm null-ref guard is
511
- covered. This golden-fixture parity **replaces the prior `ADDRESS_SUBSTRINGS` substring parity**.
512
-
513
- **The learn primer moved onto the seam (Node 2.4).** The learn-stage primer wording moved off its
514
- two hand-concatenated twins onto the render seam — one canonical `prompts/stages/learn.md` rendered
515
- byte-identical by cold `perk/run/launch/prompts.py::_learn_prompt` and warm
516
- `extension/doors/learn.ts::learnGuidance` (learn has **no worker twin** — only cold + warm). Cold
517
- and warm are **unified onto the cold body**: warm `/learn` wording changed from its prior numbered
518
- "perk /learn —" style to the cold bullet "You are in the learn step…" body, the `other` arm
519
- collapsed to a single "Open the plan and its merged change" line (warm **lost** its prior `other`
520
- merged-PR derivation — an accepted change for the effectively-unreachable provider arm), and warm's
521
- no-plan-ref fallback folded into the same template. This node is the **first template to use
522
- conditionals**: the `{% if pr_id %}` header split and the no-ref / github+linear / other structure
523
- selection are the template's conditional on `provider` (+ `pr_id` presence); the provider read-line
524
- text is supplied as the `read_cmd` var from the node-2.1 plan-read helper (`_plan_read_instruction`
525
- / `planReadInstruction`), `read_cmd` passed always (empty string when absent) so it is defined. The
526
- template keeps each `{% if %}`/`{% elif %}`/`{% else %}`/`{% endif %}` tag on its **own line** (off
527
- the content lines) — enabled by the `trim_blocks` env flip above, which swallows the single newline
528
- after each block tag so the indented bullet content renders intact (whitespace-control `{%- -%}`
529
- markers alone could not — they also strip the bullets' leading indentation). The
530
- template and all four golden files carry **no trailing newline** (matching the cold literal). Four
531
- `learn-*` golden cases in `cases.yaml` (`learn-github`, `learn-linear`, `learn-other`,
532
- `learn-no-ref`) prove cross-plane byte-identity and **replace the dedicated learn substring parity**;
533
- thin per-plane selection/composition tests remain. nunjucks stays the TS engine — the golden suite
534
- is the byte-parity proof that jinja2 and nunjucks render the conditional template identically (the
535
- tag-hugging whitespace discipline keeps them equal with `trim_blocks`/`lstrip_blocks` off).
536
-
537
- **The objective-read instruction moved onto the seam (Node 2.5).** The cross-plane objective-read
538
- clause (the supplemental wording telling the model how to inspect a Linear-Project-backed
539
- objective's node-issues) moved off its two hand-duplicated twins onto the render seam, mirroring the
540
- plan-read move. The wording lives in a subdirectory at `prompts/common/objective-read/linear.md` —
541
- one arm file for the **linear** arm only (github and any non-linear backend return `""` directly in
542
- code without rendering, since `perk objective show` already covers them). **Branching stays in
543
- code**: `perk/cli/commands/objective/shared.py::objective_read_instruction` and
544
- `extension/factories/objectivePlan.ts::objectiveReadInstruction` keep their `(backend,
545
- objective_id/objectiveId, url)` signature and the `backend != "linear" → ""` early return; the
546
- linear arm computes the two **url-presence** render vars `where`/`fallback` in code (the frozen
547
- subset has no conditionals — mirroring the `model_clause` precedent) and renders the one template.
548
-
549
- The template (and its golden files) carry **no trailing newline** — the helper returns a single-line
550
- string embedded mid-prompt, so the render output must equal the prior literal exactly (the
551
- `_seed_prompt`/`factoryGuidance`/`reconcileGuidance` composition tests embed it and keep passing).
552
- Two `objective-read-*` golden cases in `cases.yaml` (the linear arm, both url sub-variants) prove
553
- cross-plane byte-identity; the empty github/other arm stays code-only (no render → no golden) and is
554
- covered by the per-plane selection tests. Per-plane selection tests in each plane
555
- (`tests/test_objective_prompt_parity.py`, `extension/factories/objectivePlan.test.ts`) prove the
556
- code picks the right arm + computes where/fallback. This golden-fixture parity **replaces the prior
557
- `OBJECTIVE_LINEAR_SUBSTRINGS` substring lockstep** (which remains only as a local constant for the
558
- per-plane + seed-composition tests, no longer a cross-plane invariant). The `_seed_prompt` /
559
- `factoryGuidance` / `reconcileGuidance` body moves are deferred to Node 2.6.
560
-
561
- **The objective-plan factory seed + warm guidance moved onto the seam (Node 2.6).** The two
562
- hand-built objective-plan-factory prompt bodies — the **cold** seed
563
- (`perk/cli/commands/objective/plan_cmd.py::_seed_prompt`) and the **warm** guidance
564
- (`extension/factories/objectivePlan.ts::factoryGuidance`) — moved onto the render seam as the sixth
565
- real consumer. Unlike the implement (2.2) / learn (2.4) moves, they are **NOT unified**: the cold
566
- seed launches a *fresh* read-only session, so it **injects** the objective title + node description
567
- (the `<untrusted_objective>` block) and the pre-planning node-engagement block as DATA, and its node
568
- is already marked `planning` by the cold door; the warm guidance runs *in-session*, so it
569
- **instructs** the model to fetch the objective + node engagement and to mark the node `planning`
570
- itself. This **cold-injects / warm-instructs** asymmetry makes them genuinely different bodies, so
571
- they become **two arm files in a subdirectory** — `prompts/stages/objective-plan/{seed,guidance}.md`
572
- (filenames mirror the function names) — like 2.1/2.3/2.5 landed despite singular node titles. The
573
- **branching moved INTO the templates** as `{% if %}` conditionals (the learn-2.4 pattern, enabled by
574
- `trim_blocks`): block-level tags on their own lines (the cold engagement block, the warm
575
- node-selection line) and inline tags mid-line (the read clause, the explorer/model clause). The
576
- helpers now pass **raw** vars — `node_engagement` (the rendered block, `""` when absent),
577
- `read_clause` (the rendered linear clause, `""` for github/other), `model` (`""` when unset), and
578
- (warm) `node` (`""` → select-next) — while the in-code arm SELECTION
579
- (`objective_read_instruction` / `objectiveReadInstruction` backend logic) is unchanged. Both
580
- templates and their golden files carry **no trailing newline** (the prior literals had none). Four
581
- `objective-plan-*` golden cases in `cases.yaml` (seed/guidance × github/linear) prove cross-plane
582
- byte-parity across both arms of every conditional. The per-plane composition tests are **retained**
583
- (`OBJECTIVE_LINEAR_SUBSTRINGS` survives as a local constant feeding the per-plane selection +
584
- seed-composition tests); no cross-plane substring lockstep existed between the two different prompts,
585
- so none is removed.
586
-
587
- **The learned-docs factory seed + warm guidance moved onto the seam (Node 2.7).** The two
588
- hand-built learned-docs-factory prompt bodies — the **cold** seed
589
- (`perk/cli/commands/learn/docs_cmd.py::_seed_prompt`) and the **warm** guidance
590
- (`extension/doors/learnDocs.ts::learnDocsGuidance`) — moved onto the render seam as the seventh real
591
- consumer. **Unlike 2.6 they are UNIFIED** (the implement-2.2 / learn-2.4 pattern): the cold/warm
592
- differences were all **superficial factory house-style** — header wording, a header blank line,
593
- step-number indentation, a cold-only "from this read-only session" qualifier, and the
594
- closing-paragraph phrasing — none load-bearing, so they were **converged away** onto the **cold-seed
595
- orientation form** rather than preserved behind conditionals. The warm guidance gained the "You are
596
- running…" header + the standalone closing paragraph ("Judgment, user interaction, and durable writes
597
- stay with you — never delegate them."), and the cold seed lost the `" "` step indent + the "from
598
- this read-only session" qualifier (the warm session is not read-only, so the qualifier was
599
- cold-only-accurate anyway; the bare "NEVER write the docs directly" is correct in both planes). The
600
- result is a single **flat** template `prompts/stages/learn-docs.md` with **zero `{% if %}`
601
- conditionals**; both planes pass the same two vars (`inbox_path`, `num_list`). The template and its
602
- golden carry **no trailing newline**. One `learn-docs` golden case in `cases.yaml` proves cross-plane
603
- byte-parity. No cross-plane substring lockstep existed between cold and warm, so none is removed; the
604
- per-plane composition tests are retained (one warm header assertion updated from `"perk /learn-docs"`
605
- to `"learned-docs plan factory"`).