@mgiles/perk 1.1.0 → 2.0.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 (93) hide show
  1. package/README.md +68 -44
  2. package/extension/adapters/planAdapterPlannotator.ts +27 -41
  3. package/extension/adapters/planAdapterTombell.ts +15 -28
  4. package/extension/adapters/todoAdapterJuicesharp.ts +10 -13
  5. package/extension/checkpoints/checkpoints.ts +19 -12
  6. package/extension/doors/address.ts +4 -4
  7. package/extension/doors/askUser.ts +12 -8
  8. package/extension/doors/ciExecutor.ts +21 -14
  9. package/extension/doors/hunkHandoff.ts +202 -0
  10. package/extension/doors/land.ts +31 -9
  11. package/extension/doors/learn.ts +2 -2
  12. package/extension/doors/learnFactory.ts +144 -0
  13. package/extension/doors/plannotatorHandoff.ts +509 -0
  14. package/extension/doors/prReview.ts +4 -4
  15. package/extension/doors/prReviewBrowser.ts +341 -0
  16. package/extension/doors/prReviewTerminal.ts +267 -0
  17. package/extension/doors/selfcheck.ts +238 -5
  18. package/extension/doors/submit.ts +20 -0
  19. package/extension/doors/submitPrReview.ts +408 -0
  20. package/extension/factories/objective.ts +15 -5
  21. package/extension/factories/objectiveAuthor.ts +15 -32
  22. package/extension/factories/objectiveDraft.ts +1 -1
  23. package/extension/factories/objectivePlan.ts +12 -10
  24. package/extension/factories/objectiveSave.ts +2 -2
  25. package/extension/factories/planMode.ts +22 -40
  26. package/extension/factories/planReview.ts +213 -191
  27. package/extension/factories/planSave.ts +7 -7
  28. package/extension/index.ts +83 -25
  29. package/extension/substrate/bindingDelivery.ts +32 -10
  30. package/extension/substrate/bindings.ts +4 -2
  31. package/extension/substrate/cache.ts +34 -7
  32. package/extension/substrate/clipboard.ts +81 -0
  33. package/extension/substrate/config.ts +88 -65
  34. package/extension/substrate/git.ts +43 -0
  35. package/extension/substrate/paths.ts +1 -1
  36. package/extension/substrate/prompts.ts +2 -2
  37. package/extension/substrate/providers.ts +62 -8
  38. package/extension/substrate/sessionPointers.ts +35 -6
  39. package/extension/substrate/structuredOutput.ts +3 -1
  40. package/extension/substrate/terminalLaunch.ts +178 -0
  41. package/extension/substrate/toolGating.ts +330 -79
  42. package/extension/substrate/toolParams.ts +7 -0
  43. package/extension/substrate/workflowState.ts +54 -2
  44. package/extension/surfaces/footerProvider.ts +8 -4
  45. package/extension/surfaces/surfaces.ts +330 -12
  46. package/extension/vendor/btw/btw.ts +10 -0
  47. package/extension/worker/readOnlySession.ts +19 -6
  48. package/extension/worker/worker.ts +77 -7
  49. package/extension/workerMain.ts +12 -13
  50. package/package.json +3 -3
  51. package/prompts/_fixtures/live.yaml +117 -2
  52. package/prompts/contexts/adapters/juicesharp-todo.md +7 -0
  53. package/prompts/contexts/adapters/plannotator-objective.md +7 -0
  54. package/prompts/contexts/adapters/plannotator-plan.md +6 -0
  55. package/prompts/contexts/adapters/tombell-plan.md +17 -0
  56. package/prompts/contexts/objective-authoring.md +20 -0
  57. package/prompts/contexts/plan-authoring.md +24 -0
  58. package/prompts/contexts/read-only.md +10 -0
  59. package/prompts/stages/conflict-resolution.md +1 -1
  60. package/prompts/stages/learn-code.md +1 -1
  61. package/prompts/stages/learn-docs.md +2 -2
  62. package/prompts/stages/learn-orchestrate.md +1 -1
  63. package/prompts/stages/objective-author/adopt.md +1 -1
  64. package/prompts/stages/objective-author/file.md +1 -1
  65. package/prompts/stages/objective-plan/guidance.md +1 -1
  66. package/prompts/stages/objective-plan/seed.md +1 -1
  67. package/prompts/stages/objective-reconcile.md +1 -1
  68. package/prompts/stages/objective-replan.md +1 -1
  69. package/prompts/stages/plan-from/adopt.md +2 -2
  70. package/prompts/stages/plan-from/file.md +2 -2
  71. package/prompts/stages/pr-review-browser/active.md +11 -0
  72. package/prompts/stages/pr-review-browser/foreign.md +11 -0
  73. package/prompts/stages/pr-review-terminal/active.md +12 -0
  74. package/prompts/stages/pr-review-terminal/foreign.md +13 -0
  75. package/prompts/stages/pr-review-terminal/local.md +4 -0
  76. package/prompts/stages/pr-review.md +1 -1
  77. package/prompts/stages/replan.md +2 -2
  78. package/prompts/stages/skills/create-from.md +1 -1
  79. package/prompts/stages/skills/create.md +1 -1
  80. package/prompts/stages/skills/refine.md +1 -1
  81. package/shared/README.md +22 -18
  82. package/shared/bindings.yaml +10 -2
  83. package/shared/contracts-history.md +24 -0
  84. package/shared/contracts.md +1442 -1787
  85. package/shared/providers.yaml +8 -1
  86. package/shared/registry.yaml +7 -8
  87. package/shared/schemas/inputs/review-submit-batch.schema.json +66 -0
  88. package/shared/schemas/outputs/pr-review-checkout.schema.json +69 -0
  89. package/shared/schemas/outputs/pr-review-cleanup.schema.json +54 -0
  90. package/shared/schemas/outputs/pr-review-submit.schema.json +64 -0
  91. package/extension/doors/learnCode.ts +0 -100
  92. package/extension/doors/learnDocs.ts +0 -100
  93. package/extension/doors/prReviewLocal.ts +0 -229
package/README.md CHANGED
@@ -1,64 +1,89 @@
1
1
  # perk
2
2
 
3
- A Pi-native, plan-oriented engineering workflow: a Python `perk` CLI (the session
4
- *exterior*) plus a TypeScript Pi extension (the session *interior*).
3
+ perk is a [Pi](https://pi.dev)-native workflow for plan-driven software engineering.
5
4
 
6
- > Inspired by `erk`, formely maintained by the team at [dagster](https://github.com/dagster-io/dagster)
5
+ It combines a Python CLI that manages the **session exterior**, a TypeScript Pi extension
6
+ that governs the **session interior**, and an **issue backend** for durable workflow state,
7
+ GitHub by default. The workflow is organized around _planning_, _implementation_,
8
+ _code review_, and _learning_.
7
9
 
8
- perk was built with perk.
10
+ Workflow stages and surfaces can use perk’s built-in defaults or pluggable third-party
11
+ extensions, such as [plannonator](https://github.com/backnotprop/plannotator) for plan
12
+ review.
13
+
14
+ perk is built for work that may be interrupted, resumed elsewhere, superseded, or moved
15
+ across machines. Resumability, human feedback, and remote execution are not special cases;
16
+ they are part of the core model.
17
+
18
+ `perk` is inspired by `erk`, formerly maintained by the team at
19
+ [Dagster](https://github.com/dagster-io/dagster).
9
20
 
10
21
  ## What perk is
11
22
 
12
- perk implements a plan-oriented engineering workflow (plan implement submit review
13
- address land learn) for [Pi](https://github.com/earendil-works), split across **two planes**:
23
+ perk's unit of work is a **plan**: a reviewed, durable description of a change,
24
+ written before files are edited and saved as canonical workflow state. The core spine is
25
+ `plan -> save -> implement -> submit -> address -> land -> learn`; `address` only happens
26
+ when review feedback needs a response. Longer-running **objectives** can emit bounded plans
27
+ into the same spine.
28
+
29
+ The implementation is split across two planes:
14
30
 
15
- - the **exterior** — a Python `perk` CLI that scaffolds repos, positions worktrees, mints
16
- run ids, and launches primed `pi` sessions (everything that happens *outside* a session);
17
- - the **interior** — a TypeScript Pi extension that drives stage transitions and state
18
- *inside* a running session.
31
+ - **exterior** — the Python `perk` CLI, which scaffolds repos, manages worktrees,
32
+ mints run ids, and launches primed `pi` sessions;
33
+ - **interior** — a TypeScript Pi extension, which owns in-session stage transitions,
34
+ tool gates, context, and warm `/...` commands.
19
35
 
20
- A language-neutral [`shared/`](shared/) contract (the stage registry + cross-plane specs)
21
- is the single source both planes read, hopefully staying in lockstep without codegen.
36
+ Both planes read the language-neutral [`shared/`](shared/) registry and contracts directly.
37
+ There is no codegen layer to drift.
38
+
39
+ The operating model is deliberately simple: canonical state lives in GitHub by default
40
+ (Linear is supported), `.perk/workflow/` is a local cache, and session state is throwaway.
41
+ Cold doors launch fresh context from a shell (`perk implement`, `perk plan resume`); warm
42
+ doors keep context inside a session (`/submit`, `/land`, `/learn`). `implement` and
43
+ `address` can dispatch to CI; review, merge, and learning stay local.
22
44
 
23
45
  ## Quickstart
24
46
 
25
- perk targets any git repo. From the repo you want to wire:
47
+ From the git repo you want to wire:
26
48
 
27
49
  ```bash
28
- uv tool install perk # (or run from source — see Develop)
29
- perk init # scaffold/converge Pi wiring (idempotent; safe to re-run)
30
- perk doctor # report health; perk doctor --fix repairs drift
50
+ uv tool install perk
51
+ perk init
52
+ perk doctor
53
+ perk plan
31
54
  ```
32
55
 
33
- `perk init` requires a git repo + `git`, `gh`, `node 22`, `ast-grep`, and `pi` on PATH. GitHub auth
34
- is verified but never required (it is reported, never fatal).
56
+ `perk init` is idempotent and safe to re-run. `perk doctor` reports health;
57
+ `perk doctor --fix` repairs managed drift.
58
+
59
+ You need a git repo plus `git`, `gh`, `node >= 22`, `pi`, and `uv`. `ast-grep` is preferred
60
+ for structural search and reported when absent, but it is advisory. GitHub auth is verified
61
+ and reported by init; it becomes required when you drive real plans, PRs, and merges.
35
62
 
36
63
  For the guided first run, follow
37
64
  [Get started with perk](docs/user-docs/tutorials/get-started.md).
38
65
 
39
66
  ## Documentation
40
67
 
41
- The operator-facing docs live under [`docs/user-docs/`](docs/user-docs/index.md), organized as:
68
+ Operator docs live under [`docs/user-docs/`](docs/user-docs/index.md):
42
69
 
43
- - **[Tutorials](docs/user-docs/tutorials/index.md)** — learning-oriented lessons; start with
70
+ - **[Tutorials](docs/user-docs/tutorials/index.md)** — learn by doing; start with
44
71
  [Get started with perk](docs/user-docs/tutorials/get-started.md).
45
- - **[How-to guides](docs/user-docs/how-to/index.md)** — goal-oriented recipes (resume a plan,
46
- address review feedback, switch to Linear, attach a skill, …).
47
- - **[Reference](docs/user-docs/reference/index.md)** — the CLI surface, in-session commands &
48
- tools, the objective roadmap model, configuration, and providers & backends.
49
- - **[Explanation](docs/user-docs/explanation/index.md)** — how perk thinks; headless/remote
50
- maturity.
72
+ - **[How-to guides](docs/user-docs/how-to/index.md)** — recipes for specific jobs:
73
+ resume a plan, address review feedback, switch to Linear, attach a skill, dispatch to CI.
74
+ - **[Reference](docs/user-docs/reference/index.md)** — CLI commands, in-session commands
75
+ and tools, objectives, configuration, providers, backends, and schemas.
76
+ - **[Explanation](docs/user-docs/explanation/index.md)** — the mental model: plans,
77
+ planes, state tiers, doors, and remote maturity.
51
78
 
52
- perk's internal research and planning record lives under [`docs/`](docs/index.md) and is for
53
- perk's own developers.
79
+ Internal research, design notes, and durable learnings live under [`docs/`](docs/index.md).
54
80
 
55
81
  ## Layout
56
82
 
57
- - `perk/` — the Python CLI (the session exterior).
58
- - `extension/` — the TypeScript Pi extension (the session interior).
59
- - `shared/` — cross-plane contracts (the stage registry + specs), bundled into both build
60
- artifacts.
61
- - `docs/` — research inputs, design notes, and durable learnings.
83
+ - `perk/` — Python CLI, the session exterior.
84
+ - `extension/` — TypeScript Pi extension, the session interior.
85
+ - `shared/` — stage registry and cross-plane contracts.
86
+ - `docs/` — operator docs, research, design notes, and learnings.
62
87
 
63
88
  ## Develop
64
89
 
@@ -72,19 +97,18 @@ Two pinned toolchains:
72
97
  With [`just`](https://github.com/casey/just):
73
98
 
74
99
  ```bash
75
- just setup # uv sync + npm install + git hooks + install-cli (the `perk` CLI on PATH)
76
- just install-cli # just the `perk` CLI on PATH (editable: tracks this clone)
77
- just fmt # ruff format + biome format
78
- just lint # ruff check + biome check
79
- just typecheck # ty + tsc
80
- just test # pytest + node:test (the regression gate)
81
- just ci # setup + lint + typecheck + test
100
+ just setup
101
+ just install-cli
102
+ just fmt
103
+ just lint
104
+ just typecheck
105
+ just test
106
+ just ci
82
107
  ```
83
108
 
84
- `just setup` also runs `just hooks` (`prek install`), wiring a [prek](https://prek.j178.dev)
85
- pre-commit hook that runs `ruff check` on staged Python (config in `prek.toml`; the ruff
86
- env is built by prek from the remote ruff-pre-commit repo, so it never depends on a
87
- system/`.venv` ruff). Re-run `just hooks` after a fresh clone.
109
+ `just setup` runs `uv sync`, `npm install`, `just hooks`, and `just install-cli`. The hook is
110
+ managed by [prek](https://prek.j178.dev) and runs `ruff check` on staged Python. Re-run
111
+ `just hooks` after a fresh clone if hooks are missing.
88
112
 
89
113
  Without `just`: `uv run …` for Python (`uv run perk init`, `uv run pytest`,
90
114
  `uv run ruff check perk tests`, `uv run ty check`) and `npm run …` for TypeScript
@@ -7,7 +7,8 @@
7
7
  // INJECTION + BRIDGE ONLY: the `plan_review` TOOL lives in `extension/factories/planReview.ts`
8
8
  // (perk's backend-neutral review door); this module is the injection-only adapter shape. It owns
9
9
  // (1) the plannotator review-step authoring context (injected while the gate is active AND
10
- // plannotator is selected — TWO content flavors, one customType: the plan bridge context, or the
10
+ // plannotator is selected — TWO content flavors, one customType, each once-only: branch-scan
11
+ // dedup'd on the flavor's marker: the plan bridge context, or the
11
12
  // objective flavor when the stage is `objective-author`) and (2) the pure event-bus bridge
12
13
  // (`createPlannotatorBridge`) that planReview.ts dispatches to when plannotator is the
13
14
  // selected plan provider. The bridge speaks plannotator's published `plannotator:request` event
@@ -24,7 +25,8 @@
24
25
  // gate exit all live behind planReview.ts's seams; the injection's gate-active check reads the
25
26
  // persisted `perk:workflow-state.mode`, the gate's own state twin.
26
27
  //
27
- // EVENT ENVELOPE (pinned against `@plannotator/pi-extension@0.20.0`, `plannotator-events.ts`):
28
+ // EVENT ENVELOPE (pinned against `@plannotator/pi-extension@0.20.0`, `plannotator-events.ts`
29
+ // verified unchanged through 0.22.0):
28
30
  // request — pi.events.emit("plannotator:request", { requestId, action: "plan-review",
29
31
  // payload: { planContent, origin? }, respond }) // respond = in-payload callback
30
32
  // handshake — respond({ status: "handled", result: { status: "pending", reviewId } })
@@ -37,8 +39,9 @@ import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
37
39
  import { resolvedPlanProviderId } from "../factories/planMode.ts";
38
40
  // Type-only (erased at runtime — no cycle): the outcome vocabulary lives with the review door.
39
41
  import type { ReviewOutcome } from "../factories/planReview.ts";
42
+ import { render } from "../substrate/prompts.ts";
40
43
  import { PLANNOTATOR_PLAN_PROVIDER_ID } from "../substrate/providers.ts";
41
- import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
44
+ import { branchCarries, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
42
45
 
43
46
  /** The plannotator plan-adapter bridge customType (distinct from planMode's `perk:plan-context`). */
44
47
  export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE = "perk:plan-adapter-plannotator";
@@ -61,21 +64,9 @@ function handshakeTimeoutMs(): number {
61
64
  * step. Prompting, NOT enforcement (perk's own gate is the read-only authority). Durable anchors
62
65
  * only — mirrors PLAN_AUTHORING_CONTEXT, which is also injected (perk's plan mode stays).
63
66
  */
64
- export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT = `${PLAN_ADAPTER_PLANNOTATOR_MARKER}
65
- A Plannotator browser review surface is configured for plan authoring in this repo. Author the plan
66
- read-only exactly as the plan-authoring contract describes; then add one review step:
67
-
68
- - Keep the working draft current with plan_draft — the validated plan-draft artifact is what gets
69
- reviewed AND auto-saved; the plan param is only a fallback for sessions that never wrote a draft.
70
- - When the plan is decision-complete, call the plan_review tool. The Plannotator browser UI opens
71
- for the human reviewer.
72
- - If the review is DENIED: revise per the returned annotations/feedback, rewrite the working draft
73
- with plan_draft, then call plan_review again.
74
- - If the review is APPROVED: the plan is auto-saved to GitHub and the session leaves read-only.
75
- Do NOT re-dump the plan as a final message and do NOT tell the user to run /plan-save — relay
76
- the save outcome (and any reviewer feedback) instead.
77
- - If plan_review reports it was skipped or no review surface is available: fall back to presenting
78
- the complete plan to the user; the human runs /plan-save (the manual failsafe).`;
67
+ export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT = render("contexts/adapters/plannotator-plan.md", {
68
+ marker: PLAN_ADAPTER_PLANNOTATOR_MARKER,
69
+ });
79
70
 
80
71
  /**
81
72
  * The objective flavor of the bridge prompt, injected in an
@@ -83,24 +74,10 @@ read-only exactly as the plan-authoring contract describes; then add one review
83
74
  * objective via the `objectiveApprovalSave` seam; `/objective-save` is the manual failsafe on
84
75
  * the skipped/unavailable arms.
85
76
  */
86
- export const OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT = `${OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER}
87
- A Plannotator browser review surface is configured for objective authoring in this repo. Author
88
- the objective read-only exactly as the objective-authoring contract describes; then add one
89
- review step:
90
-
91
- - Keep the working objective current with objective_draft — pass the FULL prose and the FULL
92
- structured roadmap each call (it rewrites the whole draft); never hand-write roadmap YAML.
93
- - When the objective + roadmap are decision-complete, call the plan_review tool. The Plannotator
94
- browser UI shows the RENDERED objective (the prose + a roadmap table) derived from the draft
95
- artifact — never raw JSON.
96
- - If the review is DENIED: revise per the returned annotations/feedback, rewrite the working
97
- draft with objective_draft, then call plan_review again.
98
- - If the review is APPROVED: the objective is auto-saved to GitHub and the session leaves
99
- read-only — do NOT re-dump the objective as a final message and do NOT tell the user to run
100
- /objective-save; relay the save outcome (and any reviewer feedback) instead.
101
- - If plan_review reports it was skipped or no review surface is available: present the complete
102
- objective + structured roadmap to the user; the human runs /objective-save (the manual
103
- failsafe).`;
77
+ export const OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT = render(
78
+ "contexts/adapters/plannotator-objective.md",
79
+ { marker: OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER },
80
+ );
104
81
 
105
82
  /** Whether the foreign `plannotator-plan` provider is the selected plan provider for `cwd`. */
106
83
  export function isPlannotatorPlanSelected(cwd: string): boolean {
@@ -226,12 +203,21 @@ export function registerPlanAdapterPlannotator(pi: ExtensionAPI): void {
226
203
  // `perk:workflow-state.mode` (the gate's state twin) — never the gate itself.
227
204
  pi.on("before_agent_start", async (_event, ctx) => {
228
205
  if (!isPlannotatorPlanSelected(ctx.cwd)) return;
229
- const state = rebuildWorkflowState(branchOf(ctx));
206
+ const branch = branchOf(ctx);
207
+ const state = rebuildWorkflowState(branch);
230
208
  if (state.mode !== "read-only") return;
231
- const content =
232
- state.stage === OBJECTIVE_AUTHOR_STAGE
233
- ? OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT
234
- : PLAN_ADAPTER_PLANNOTATOR_CONTEXT;
209
+ const objectiveFlavor = state.stage === OBJECTIVE_AUTHOR_STAGE;
210
+ const content = objectiveFlavor
211
+ ? OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT
212
+ : PLAN_ADAPTER_PLANNOTATOR_CONTEXT;
213
+ // Once-only PER FLAVOR: the dedup key is the flavor's marker (not the shared customType), so
214
+ // a stage change still delivers the missing flavor while a prior copy of the other flavor
215
+ // sits on the branch. Injected customs persist, so a live copy suppresses re-injection;
216
+ // compaction dropping it makes the scan come up clean and the next turn re-injects.
217
+ const marker = objectiveFlavor
218
+ ? OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER
219
+ : PLAN_ADAPTER_PLANNOTATOR_MARKER;
220
+ if (branchCarries(branch, marker)) return;
235
221
  return {
236
222
  message: {
237
223
  customType: PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE,
@@ -8,7 +8,8 @@
8
8
  // change on the default path.
9
9
  //
10
10
  // WHAT IT DOES (and does NOT do):
11
- // - It injects a hidden (`display:false`) `perk:plan-adapter-tombell` context that tells the model
11
+ // - It injects a hidden (`display:false`, once-only: branch-scan dedup'd on the marker)
12
+ // `perk:plan-adapter-tombell` context that tells the model
12
13
  // the foreign `/plan` surface authors a FREE-FORM PROSE plan, and directs it through perk's
13
14
  // review-first discipline: keep the draft current with `plan_draft`, then call
14
15
  // `plan_review` — which (for any non-plannotator selection, tombell included) runs the
@@ -36,8 +37,14 @@
36
37
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
37
38
  import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
38
39
  import { resolvedPlanProviderId } from "../factories/planMode.ts";
40
+ import { render } from "../substrate/prompts.ts";
39
41
  import { TOMBELL_PLAN_PROVIDER_ID } from "../substrate/providers.ts";
40
- import { type BranchEntry, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
42
+ import {
43
+ type BranchEntry,
44
+ branchCarries,
45
+ branchOf,
46
+ rebuildWorkflowState,
47
+ } from "../substrate/workflowState.ts";
41
48
 
42
49
  /** The tombell plan-adapter bridge customType (distinct from planMode's `perk:plan-context`). */
43
50
  export const PLAN_ADAPTER_TOMBELL_CONTEXT_TYPE = "perk:plan-adapter-tombell";
@@ -50,32 +57,9 @@ const PLAN_ADAPTER_TOMBELL_MARKER = "[PLAN ADAPTER: TOMBELL]";
50
57
  * engaged by the cold-door launch, is the read-only authority). Durable anchors only, no line
51
58
  * numbers.
52
59
  */
53
- export const PLAN_ADAPTER_TOMBELL_CONTEXT = `${PLAN_ADAPTER_TOMBELL_MARKER}
54
- You are authoring a plan through the @tombell/pi-plan \`/plan\` surface — a read-only exploration mode
55
- that produces a FREE-FORM PROSE plan (it emits no structured plan and no save tool of its own).
56
-
57
- Gather before you plan, then write the plan so an executor with zero prior context can implement it
58
- without guessing: anchor every change durably — function/class names, behavioral descriptions,
59
- structural locations — never line numbers, and resolve every open choice before you save.
60
-
61
- perk persists the plan to the provider-agnostic plan reference (cache.plan-ref); the objective/node
62
- linkage and any consumed-learn numbers are recovered automatically from the launch handoff — never
63
- try to write the plan reference yourself.
64
-
65
- - Keep the working draft current with the plan_draft tool — the validated plan-draft artifact is
66
- what gets reviewed AND auto-saved.
67
- - When the plan is decision-complete, call the plan_review tool — the human reviews the draft in
68
- perk's in-TUI editor review.
69
- - If the review is DENIED: revise per the feedback, rewrite the draft with plan_draft, then call
70
- plan_review again.
71
- - If the review is APPROVED: the plan is auto-saved and the session leaves read-only. Relay the
72
- save outcome — do NOT re-dump the plan as a final message and do NOT tell the user to run
73
- /plan-save.
74
- - If plan_review reports it was skipped or unavailable, OR the plan_draft/plan_review tools are not
75
- in your tool set (this plan surface restricts tools): write the COMPLETE final plan as your last
76
- message and present it to the user — the human runs the /plan-save command when satisfied (it
77
- prefers the validated draft artifact and falls back to scraping your latest message, so that
78
- final message must be the clean, complete plan and nothing else).`;
60
+ export const PLAN_ADAPTER_TOMBELL_CONTEXT = render("contexts/adapters/tombell-plan.md", {
61
+ marker: PLAN_ADAPTER_TOMBELL_MARKER,
62
+ });
79
63
 
80
64
  /** Whether the foreign `tombell-plan` provider is the selected plan provider for `cwd`. */
81
65
  export function isTombellPlanSelected(cwd: string): boolean {
@@ -113,6 +97,9 @@ export function registerPlanAdapterTombell(pi: ExtensionAPI): void {
113
97
  const state = rebuildWorkflowState(branch);
114
98
  if (state.stage === OBJECTIVE_AUTHOR_STAGE) return;
115
99
  if (state.mode !== "read-only" && !isTombellPlanModeEnabled(branch)) return;
100
+ // Once-only: injected customs persist to the branch, so a live copy suppresses re-injection;
101
+ // compaction dropping it makes the scan come up clean and the next turn re-injects.
102
+ if (branchCarries(branch, PLAN_ADAPTER_TOMBELL_MARKER)) return;
116
103
  return {
117
104
  message: {
118
105
  customType: PLAN_ADAPTER_TOMBELL_CONTEXT_TYPE,
@@ -11,7 +11,8 @@
11
11
  // behavior change on the default path.
12
12
  //
13
13
  // WHAT IT DOES (and does NOT do):
14
- // - It injects a hidden (`display:false`) `perk:todo-adapter-juicesharp` context that tells the
14
+ // - It injects a hidden (`display:false`, once-only: branch-scan dedup'd on the marker)
15
+ // `perk:todo-adapter-juicesharp` context that tells the
15
16
  // model the foreign `@juicesharp/rpiv-todo` checklist overlay is the sole progress surface here
16
17
  // (perk's own checkpoints stepped aside) and directs it to carry perk's
17
18
  // implement-progress DISCIPLINE onto that overlay: seed it from the plan body's `## Steps` and
@@ -29,8 +30,9 @@
29
30
 
30
31
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
31
32
  import { resolvedTodoProviderId } from "../checkpoints/checkpoints.ts";
33
+ import { render } from "../substrate/prompts.ts";
32
34
  import { JUICESHARP_TODO_PROVIDER_ID } from "../substrate/providers.ts";
33
- import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
35
+ import { branchCarries, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
34
36
 
35
37
  /** The juicesharp todo-adapter bridge customType (distinct from checkpoints' `perk:checkpoint`). */
36
38
  export const TODO_ADAPTER_JUICESHARP_CONTEXT_TYPE = "perk:todo-adapter-juicesharp";
@@ -41,17 +43,9 @@ const TODO_ADAPTER_JUICESHARP_MARKER = "[TODO ADAPTER: JUICESHARP]";
41
43
  * overlay. Prompting, NOT enforcement (perk's checkpoint scanner is deferred under this selection).
42
44
  * Durable anchors only, no line numbers.
43
45
  */
44
- export const TODO_ADAPTER_JUICESHARP_CONTEXT = `${TODO_ADAPTER_JUICESHARP_MARKER}
45
- This implement session tracks progress through the \`@juicesharp/rpiv-todo\` checklist overlay — the
46
- selected todo provider (\`[providers] todo = "juicesharp-todo"\`). perk's own checkpoint surface has
47
- stepped aside (Node 3.1), so the foreign overlay is the sole progress surface here.
48
-
49
- Carry perk's implement-progress discipline onto that overlay: seed it from the plan body's
50
- \`## Steps\` numbered list — one checklist item per step, in order — then mark each item complete as
51
- you finish the corresponding step, the same gather-then-advance flow perk's checkpoints embody. Use
52
- the overlay's own controls to add and complete items; you do not need perk's \`[WIP:n]\`/\`[DONE:n]\`
53
- markers here (perk's checkpoint scanner is deferred under this selection). If the plan has no
54
- \`## Steps\` list, there is nothing to seed — let the overlay behave as its defaults suggest.`;
46
+ export const TODO_ADAPTER_JUICESHARP_CONTEXT = render("contexts/adapters/juicesharp-todo.md", {
47
+ marker: TODO_ADAPTER_JUICESHARP_MARKER,
48
+ });
55
49
 
56
50
  /** Whether the foreign `juicesharp-todo` provider is the selected todo provider for `cwd`. */
57
51
  export function isJuicesharpTodoSelected(cwd: string): boolean {
@@ -71,6 +65,9 @@ export function registerTodoAdapterJuicesharp(pi: ExtensionAPI): void {
71
65
  if (!isJuicesharpTodoSelected(ctx.cwd)) return;
72
66
  const branch = branchOf(ctx);
73
67
  if (rebuildWorkflowState(branch).active_plan_ref == null) return;
68
+ // Once-only: injected customs persist to the branch, so a live copy suppresses re-injection;
69
+ // compaction dropping it makes the scan come up clean and the next turn re-injects.
70
+ if (branchCarries(branch, TODO_ADAPTER_JUICESHARP_MARKER)) return;
74
71
  return {
75
72
  message: {
76
73
  customType: TODO_ADAPTER_JUICESHARP_CONTEXT_TYPE,
@@ -27,7 +27,6 @@
27
27
  // config-read error → treated as the reference → zero change on the default selection.
28
28
 
29
29
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
30
- import { truncateToWidth } from "@earendil-works/pi-tui";
31
30
  import { readHandoff, readPlanBody } from "../substrate/cache.ts";
32
31
  import { registerPerkCommand } from "../substrate/command.ts";
33
32
  import { loadPerkConfig } from "../substrate/config.ts";
@@ -42,11 +41,14 @@ import {
42
41
  writeSessionArtifact,
43
42
  } from "../substrate/sessionData.ts";
44
43
  import type { BranchEntry } from "../substrate/workflowState.ts";
45
- import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
44
+ import { branchCarries, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
46
45
  import {
46
+ checkpointEntryRenderer,
47
47
  MARK_CHECKPOINTS,
48
48
  type PerkStatusHandle,
49
49
  progressLine,
50
+ registerTranscriptRenderer,
51
+ renderCoarsePlanLines,
50
52
  renderProgressLines,
51
53
  report,
52
54
  setStandingWidget,
@@ -67,13 +69,17 @@ export const STEPS_ARTIFACT_NAME = "plan-steps.json";
67
69
  /**
68
70
  * The resolved `[providers] todo` selection id for `cwd`, read fresh per-event (no static state —
69
71
  * the same per-event-read shape `resolvedPlanProviderId` uses in planMode.ts). Fail-safe to the
70
- * perk-checkpoints reference: any load/resolution failure (corrupt bundled set, etc.) returns the
71
- * reference id so perk's own checkpoints keep working — the default path is the hard guarantee.
72
+ * perk-checkpoints reference: any load/resolution failure returns the reference id so perk's own
73
+ * checkpoints keep working — the default path is the hard guarantee. The catch narrows to genuine
74
+ * file-read/parse failures (the resolver is per-seam fail-open) and is logged, never silent.
72
75
  */
73
76
  export function resolvedTodoProviderId(cwd: string): string {
74
77
  try {
75
78
  return resolveProviders(loadPerkConfig(cwd).providers, loadProviders()).todo.id;
76
- } catch {
79
+ } catch (error) {
80
+ console.error(
81
+ `perk: todo provider resolution failed — falling back to ${PERK_CHECKPOINTS_PROVIDER_ID}: ${error}`,
82
+ );
77
83
  return PERK_CHECKPOINTS_PROVIDER_ID;
78
84
  }
79
85
  }
@@ -339,12 +345,7 @@ function renderStatus(
339
345
  ctx,
340
346
  WIDGET_SLOT_CHECKPOINTS,
341
347
  (_tui: unknown, theme: ThemeLike) => ({
342
- render: (width: number) => [
343
- truncateToWidth(
344
- theme.fg("dim", `Plan #${coarse.planId}: prose plan — no \`## Steps\` checklist`),
345
- width,
346
- ),
347
- ],
348
+ render: (width: number) => renderCoarsePlanLines(coarse.planId, theme, width),
348
349
  invalidate: () => {},
349
350
  }),
350
351
  { placement: "belowEditor" },
@@ -376,6 +377,12 @@ function renderStatus(
376
377
  * `session_start` AND `session_tree`; advances on `turn_end`; lists via `/checkpoints`.
377
378
  */
378
379
  export function registerCheckpoints(pi: ExtensionAPI, status: PerkStatusHandle): void {
380
+ // Transcript marker for `perk:checkpoint` snapshots (audit §2.3): renderer body in surfaces.ts,
381
+ // registration = wiring, feature-detect inside the seam (pre-0.80.4 hosts stay inert). No
382
+ // todo-provider deferral here: entries exist only when perk's checkpoints appended them, so
383
+ // rendering history stays correct under any later provider selection.
384
+ registerTranscriptRenderer(pi, CHECKPOINT_TYPE, checkpointEntryRenderer);
385
+
379
386
  pi.on("session_start", async (_event, ctx) => {
380
387
  try {
381
388
  // Todo-provider deferral: when a foreign `[providers] todo` is selected, step the progress
@@ -502,7 +509,7 @@ export function registerCheckpoints(pi: ExtensionAPI, status: PerkStatusHandle):
502
509
  try {
503
510
  if (!isPerkCheckpointsReferenceSelected(ctx.cwd)) return;
504
511
  const branch = branchOf(ctx);
505
- if (branch.some((entry) => JSON.stringify(entry).includes(STEPS_CONTEXT_TYPE))) return;
512
+ if (branchCarries(branch, STEPS_CONTEXT_TYPE)) return;
506
513
  const state = rebuildCheckpoint(branch);
507
514
  if (isInert(state) || !isGeneratedState(ctx.cwd, state)) return;
508
515
  return {
@@ -227,8 +227,8 @@ export async function resolveReviewThreads(
227
227
 
228
228
  const TOOL_GUIDELINES = [
229
229
  "Call resolve_review_threads only AFTER you have applied (and committed) fixes for the actionable items — it replies-then-resolves the threads you pass.",
230
- "Pass threads as [{thread_id, comment?}] using the thread_id values from the perk.review-classifier child's structured output; the optional comment is posted as a reply before resolving.",
231
- "Judgment and edits stay with you (the parent) — never delegate the fix; the spawned classifier is read-only and classification-only.",
230
+ "Pass resolve_review_threads `threads` as [{thread_id, comment?}] using the thread_id values from the perk.review-classifier child's structured output; the optional comment is posted as a reply before resolving.",
231
+ "Judgment and edits stay with you (the parent) — never delegate the fix; the classifier child that feeds resolve_review_threads is read-only and classification-only.",
232
232
  ];
233
233
 
234
234
  /** Resolve the active plan-ref (worktree first, then the rebuilt workflow-state). The converged
@@ -248,7 +248,7 @@ function activePlanRef(ctx: ExtensionContext): PlanRef | null {
248
248
 
249
249
  /** Inject the address-workflow guidance the model follows (the perk-address skill pointer is
250
250
  * delivered by the skill-binding suffix — not hardcoded here). When `model` is set, the
251
- * `perk.review-classifier` spawn carries an inline `model` override ([subagents] review-classifier);
251
+ * `perk.review-classifier` spawn carries an inline `model` override ([models.subagents] review-classifier);
252
252
  * otherwise the agent's frontmatter default is used.
253
253
  *
254
254
  * The wording lives in the shared canonical templates `prompts/stages/address/*` rendered via the
@@ -258,7 +258,7 @@ function activePlanRef(ctx: ExtensionContext): PlanRef | null {
258
258
  * `model_clause` render var. */
259
259
  export function addressGuidance(ref: PlanRef, preview: boolean, model?: string): string {
260
260
  const modelClause = model
261
- ? `, passing \`model: "${model}"\` on that call (the configured [subagents] review-classifier model)`
261
+ ? `, passing \`model: "${model}"\` on that call (the configured [models.subagents] review-classifier model)`
262
262
  : "";
263
263
  const variables = {
264
264
  provider: ref.provider,
@@ -120,22 +120,26 @@ export function decodeAskUserParams(params: unknown): { question: string; option
120
120
  }
121
121
 
122
122
  const TOOL_GUIDELINES = [
123
- "Prefer this during planning to resolve genuine ambiguity rather than guessing.",
124
- "Ask ONE focused question at a time, and wait for the answer before the next.",
125
- "Provide `options` when the answer is a choice — a free-text escape is always added.",
126
- "Explore the codebase first: if a question is answerable from the code, read it instead of asking.",
123
+ "Prefer ask_user_question during planning to resolve genuine ambiguity rather than guessing.",
124
+ "Ask ONE focused question per ask_user_question call, and wait for the answer before the next.",
125
+ "Provide ask_user_question `options` when the answer is a choice — a free-text escape is always added.",
126
+ "Explore the codebase before calling ask_user_question: if a question is answerable from the code, read it instead of asking.",
127
127
  ];
128
128
 
129
129
  /**
130
130
  * The resolved `[providers] askuser` selection id for `cwd`. Fail-safe to the perk-ask-user
131
- * reference: any load/resolution failure (corrupt bundled set, etc.) returns the reference id so
132
- * perk's own tool keeps registering — the default path is the hard guarantee. Mirror of
133
- * `resolvedPlanProviderId`.
131
+ * reference: any load/resolution failure returns the reference id so perk's own tool keeps
132
+ * registering — the default path is the hard guarantee. Mirror of `resolvedPlanProviderId`: the
133
+ * catch narrows to genuine file-read/parse failures (the resolver is per-seam fail-open) and is
134
+ * logged, never silent.
134
135
  */
135
136
  export function resolvedAskUserProviderId(cwd: string): string {
136
137
  try {
137
138
  return resolveProviders(loadPerkConfig(cwd).providers, loadProviders()).askuser.id;
138
- } catch {
139
+ } catch (error) {
140
+ console.error(
141
+ `perk: askuser provider resolution failed — falling back to ${PERK_ASK_USER_PROVIDER_ID}: ${error}`,
142
+ );
139
143
  return PERK_ASK_USER_PROVIDER_ID;
140
144
  }
141
145
  }