@mgiles/perk 1.0.1

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 (98) hide show
  1. package/README.md +105 -0
  2. package/extension/adapters/planAdapterPlannotator.ts +269 -0
  3. package/extension/adapters/planAdapterTombell.ts +147 -0
  4. package/extension/adapters/todoAdapterJuicesharp.ts +105 -0
  5. package/extension/checkpoints/checkpoints.ts +542 -0
  6. package/extension/checkpoints/planSteps.ts +108 -0
  7. package/extension/doors/address.ts +360 -0
  8. package/extension/doors/askUser.ts +194 -0
  9. package/extension/doors/ciExecutor.ts +583 -0
  10. package/extension/doors/land.ts +222 -0
  11. package/extension/doors/learn.ts +235 -0
  12. package/extension/doors/learnDocs.ts +99 -0
  13. package/extension/doors/lifecycleGates.ts +171 -0
  14. package/extension/doors/prReview.ts +339 -0
  15. package/extension/doors/ready.ts +86 -0
  16. package/extension/doors/selfcheck.ts +155 -0
  17. package/extension/doors/submit.ts +253 -0
  18. package/extension/factories/objective.ts +240 -0
  19. package/extension/factories/objectiveAuthor.ts +114 -0
  20. package/extension/factories/objectiveDraft.ts +343 -0
  21. package/extension/factories/objectivePlan.ts +838 -0
  22. package/extension/factories/objectiveSave.ts +285 -0
  23. package/extension/factories/planDraft.ts +140 -0
  24. package/extension/factories/planMode.ts +214 -0
  25. package/extension/factories/planReview.ts +644 -0
  26. package/extension/factories/planSave.ts +589 -0
  27. package/extension/factories/planTitle.ts +123 -0
  28. package/extension/index.ts +459 -0
  29. package/extension/substrate/bindingDelivery.ts +199 -0
  30. package/extension/substrate/bindings.ts +180 -0
  31. package/extension/substrate/cache.ts +163 -0
  32. package/extension/substrate/coldDoor.ts +226 -0
  33. package/extension/substrate/config.ts +339 -0
  34. package/extension/substrate/miniYaml.ts +262 -0
  35. package/extension/substrate/prompts.ts +35 -0
  36. package/extension/substrate/providers.ts +177 -0
  37. package/extension/substrate/registry.ts +62 -0
  38. package/extension/substrate/resources.ts +41 -0
  39. package/extension/substrate/result.ts +72 -0
  40. package/extension/substrate/runId.ts +49 -0
  41. package/extension/substrate/sessionData.ts +229 -0
  42. package/extension/substrate/structuredOutput.ts +141 -0
  43. package/extension/substrate/toolGating.ts +400 -0
  44. package/extension/substrate/toolParams.ts +106 -0
  45. package/extension/substrate/workflowState.ts +233 -0
  46. package/extension/surfaces/footerProvider.ts +43 -0
  47. package/extension/surfaces/report.ts +34 -0
  48. package/extension/surfaces/surfaces.ts +460 -0
  49. package/extension/vendor/btw/btw.ts +964 -0
  50. package/extension/vendor/btw/core.ts +153 -0
  51. package/extension/vendor/whimsical/whimsical.ts +485 -0
  52. package/extension/worker/readOnlySession.ts +282 -0
  53. package/extension/worker/worker.ts +765 -0
  54. package/extension/workerMain.ts +150 -0
  55. package/package.json +55 -0
  56. package/prompts/README.md +15 -0
  57. package/prompts/_fixtures/cases.yaml +140 -0
  58. package/prompts/_fixtures/golden/address-action-model.txt +10 -0
  59. package/prompts/_fixtures/golden/address-action.txt +10 -0
  60. package/prompts/_fixtures/golden/address-preview-model.txt +6 -0
  61. package/prompts/_fixtures/golden/address-preview.txt +6 -0
  62. package/prompts/_fixtures/golden/hello.txt +1 -0
  63. package/prompts/_fixtures/golden/implement-github.txt +8 -0
  64. package/prompts/_fixtures/golden/learn-docs.txt +8 -0
  65. package/prompts/_fixtures/golden/learn-github.txt +11 -0
  66. package/prompts/_fixtures/golden/learn-linear.txt +11 -0
  67. package/prompts/_fixtures/golden/learn-no-ref.txt +8 -0
  68. package/prompts/_fixtures/golden/learn-other.txt +8 -0
  69. package/prompts/_fixtures/golden/objective-plan-guidance-linear.txt +8 -0
  70. package/prompts/_fixtures/golden/objective-plan-guidance.txt +8 -0
  71. package/prompts/_fixtures/golden/objective-plan-seed-linear.txt +20 -0
  72. package/prompts/_fixtures/golden/objective-plan-seed.txt +15 -0
  73. package/prompts/_fixtures/golden/objective-read-linear-nourl.txt +1 -0
  74. package/prompts/_fixtures/golden/objective-read-linear.txt +1 -0
  75. package/prompts/_fixtures/golden/plan-read-github.txt +1 -0
  76. package/prompts/_fixtures/golden/plan-read-linear.txt +1 -0
  77. package/prompts/_fixtures/golden/plan-read-other.txt +1 -0
  78. package/prompts/_fixtures/golden/with_include.txt +4 -0
  79. package/prompts/_fixtures/templates/_greeting.md +1 -0
  80. package/prompts/_fixtures/templates/hello.md +1 -0
  81. package/prompts/_fixtures/templates/with_include.md +4 -0
  82. package/prompts/common/objective-read/linear.md +1 -0
  83. package/prompts/common/plan-read/github.md +1 -0
  84. package/prompts/common/plan-read/linear.md +1 -0
  85. package/prompts/common/plan-read/other.md +1 -0
  86. package/prompts/stages/address/action.md +10 -0
  87. package/prompts/stages/address/preview.md +6 -0
  88. package/prompts/stages/implement.md +8 -0
  89. package/prompts/stages/learn-docs.md +8 -0
  90. package/prompts/stages/learn.md +21 -0
  91. package/prompts/stages/objective-plan/guidance.md +12 -0
  92. package/prompts/stages/objective-plan/seed.md +20 -0
  93. package/shared/README.md +29 -0
  94. package/shared/bindings.yaml +64 -0
  95. package/shared/contracts-history.md +403 -0
  96. package/shared/contracts.md +4172 -0
  97. package/shared/providers.yaml +221 -0
  98. package/shared/registry.yaml +199 -0
@@ -0,0 +1,221 @@
1
+ # perk provider-selection supported set — the THIRD parsed cross-plane contract
2
+ # (sibling of registry.yaml and bindings.yaml).
3
+ #
4
+ # Authored once here, bundled into each build artifact (wheel -> perk/_shared/,
5
+ # npm tarball -> shared/), and parsed directly by both planes at runtime: the
6
+ # Python reader is perk/substrate/providers.py, the TS reader is extension/substrate/providers.ts. No
7
+ # codegen step — both planes read this same file.
8
+ #
9
+ # This is the SUPPORTED SET (the full catalog of providers perk knows how to wire),
10
+ # distinct from the per-repo SELECTION (the flat `[providers]` table in .pi/perk.toml,
11
+ # which is just a pointer into this catalog). Because both planes read this with their
12
+ # full YAML readers, it can carry nested structure — notably the Pi `package_filter`
13
+ # object — that the narrow-TOML config reader cannot.
14
+ #
15
+ # Vocabulary — each provider entry:
16
+ # id — the stable provider id. NOT the cache.plan-ref `provider` string:
17
+ # that field is the ISSUE BACKEND (the stamped `backend_id`, e.g.
18
+ # "github"), not the seam id — see contracts.md §8.10 / §8.21.
19
+ # seam — `plan` | `todo` | `askuser` | `footer` | `web` (the five seams pluggability-taxonomy.md scope-fenced).
20
+ # package — the foreign Pi package spec added to .pi/settings.json `packages`
21
+ # (e.g. "npm:@tombell/pi-plan"); `null` for perk's own bundled reference
22
+ # provider (nothing to add — perk produces the contract natively). NOT
23
+ # universal: the `web` seam's reference provider (`pi-web-access`) is itself
24
+ # a FOREIGN package because perk owns no native web implementation — its
25
+ # `default: true` entry carries a non-null `package` (the documented exception).
26
+ # adapter — the perk-owned shim module that bridges the foreign surface to the
27
+ # artifact boundary (cache.plan-ref / perk:checkpoint); `null` for the
28
+ # reference provider.
29
+ # default — exactly ONE `true` per seam: the behavior-preserving no-config pick.
30
+ # package_filter — optional Pi object-form filter (extensions/skills/… arrays)
31
+ # merged into the foreign package's `packages` entry to enable only the
32
+ # conflicting surface. perk's own package is NEVER filtered (it defers at
33
+ # runtime instead — adapter-architecture.md Invariant 2).
34
+ #
35
+ # Validation split (mirrors bindings.yaml): the shape-only loaders (perk/substrate/providers.py,
36
+ # extension/substrate/providers.ts) check schema_version, id uniqueness, seam membership, and
37
+ # exactly-one-default-per-seam. Cross-checks — does the repo's `[providers]` selection
38
+ # name a provider that exists / has the right seam? — are `doctor`'s job, NOT the
39
+ # loaders. The Python plane is the authoritative validator.
40
+ #
41
+ # Status (Node 2.1 → 3.2): this file ships the substrate (the three reference entries +
42
+ # foreign entries per seam), and ALL THREE seams now have a REAL foreign provider. The PLAN
43
+ # adapter landed at Node 2.3: `tombell-plan` is a REAL, selectable plan provider —
44
+ # `planAdapterTombell` bridges `@tombell/pi-plan`'s free-form prose surface to perk's
45
+ # canonical `plan_save` → `cache.plan-ref` contract, and perk VACATES the plan surface at
46
+ # REGISTRATION time (not just handler-time) under a foreign plan selection so the foreign
47
+ # `/plan`/`Ctrl+Alt+P`/`--plan` are the sole registrants (Pi suffixes duplicate command
48
+ # names). The TODO adapter landed at Node 3.2: `juicesharp-todo` is a REAL, selectable todo
49
+ # provider — `todoAdapterJuicesharp` carries perk's implement-progress discipline onto
50
+ # `@juicesharp/rpiv-todo`'s checklist overlay (injection-only, active-workflow-gated), and
51
+ # perk's own checkpoints simply DEFER at runtime (Node 3.1) — NO registration-time vacating,
52
+ # because the todo seam has no command-name collision (`/checkpoints` vs the overlay's own
53
+ # command). The DEFAULT path (the reference providers) is the hard guarantee.
54
+ #
55
+ # The ASKUSER seam is an INTERFACE seam (no durable artifact to bridge): its stable contract is
56
+ # the tool NAME `ask_user_question` plus its non-terminating-answer semantics. `juicesharp-ask-user`
57
+ # (`@juicesharp/rpiv-ask-user-question`) registers a tool with the IDENTICAL name, and tools
58
+ # (unlike commands) do NOT get `:N` suffixes — a same-named tool replaces/warns by extension load
59
+ # order. So the adapter is VACATE-ONLY (`adapter: null`, no shim, no injected context): under a
60
+ # foreign askuser selection perk's `registerAskUser` registers NOTHING at REGISTRATION time,
61
+ # leaving exactly one `ask_user_question` standing. Under the default (`perk-ask-user`) the foreign
62
+ # package is never installed, so perk's tool is the sole registrant.
63
+ #
64
+ # The FOOTER seam is the SECOND INTERFACE seam (no durable artifact to bridge), mirroring askuser
65
+ # but with INSTALL-SITE (runtime) vacating rather than registration-time vacating: perk installs
66
+ # its own footer (`installPerkFooter`) inside the `session_start` event handler, so under a foreign
67
+ # `[providers] footer` selection perk simply does NOT call `installPerkFooter` (a runtime guard at
68
+ # that single install site, keyed off `ctx.cwd`), leaving the foreign footer (`pi-powerline-footer`,
69
+ # `pi-bar`, or `@tombell/pi-status`) as the sole footer surface. `adapter: null`, no shim: for
70
+ # `pi-powerline-footer`/`pi-bar` perk's objective/checkpoints progress reaches the foreign footer
71
+ # automatically via the already-publishing composed `perk` `setStatus` slot (both render extension
72
+ # statuses), so the bridge is automatic; `@tombell/pi-status` is the exception (it does NOT render
73
+ # extension statuses — see the `pi-status-footer` note below).
74
+ # Fail-safe: any config-read error resolves to the reference id → perk installs its footer (the
75
+ # default path is the hard zero-change guarantee).
76
+ #
77
+ # Two more footer outcomes round out the catalog so the footer is governed EXCLUSIVELY by
78
+ # `[providers] footer` (no footer outcome ever needs a manual `packages` edit):
79
+ # - `pi-status-footer` (`npm:@tombell/pi-status`) is vacate-only like the two above, BUT — unlike
80
+ # `powerline-footer`/`pi-bar-footer` — pi-status does NOT render extension statuses, so perk's
81
+ # objective/checkpoints progress is NOT shown in the footer when it is selected. This is an
82
+ # accepted limitation (it matches what pi-status already does today); perk does not build a
83
+ # status-bridge adapter for it (it is a genuine interface seam).
84
+ # - `pi-default` (`package: null`) is the "install nothing / leave pi's stock built-in footer"
85
+ # option: perk vacates (gate false), convergence adds no package, so pi's own footer stands.
86
+ #
87
+ # The WEB seam is the THIRD INTERFACE seam (no durable artifact to bridge), but with a genuine
88
+ # novelty: it is the FIRST seam whose `default: true` provider has a NON-NULL `package`. perk owns
89
+ # no native web-research implementation, so the behavior-preserving reference (`pi-web-access`,
90
+ # zero-config Exa search + content fetch + the bundled `librarian` skill) is itself a foreign npm
91
+ # package. Selection simply SWAPS which web package the provider-convergence installs — `adapter:
92
+ # null`, no shim, and there is NO perk surface to vacate at registration time because perk registers
93
+ # NO web tools of its own. The read-only allowlist statically carries the UNION of all known web
94
+ # tool names (inert when absent); perk does NOT normalize tool names across providers. `librarian`
95
+ # is pi-web-access-specific (it depends on `fetch_content`'s GitHub-clone path) and is accepted as
96
+ # lost under a foreign web selection. Fail-safe: any config-read error resolves to `pi-web-access`,
97
+ # keeping today's behavior exactly.
98
+
99
+ schema_version: 1
100
+
101
+ providers:
102
+ - id: perk-plan # perk's own planMode/planSave over toolGating
103
+ seam: plan
104
+ package: null # null ⇒ perk's bundled reference provider (no foreign package)
105
+ adapter: null # null ⇒ no shim needed; perk produces the contract natively
106
+ default: true # the behavior-preserving no-config pick for the plan seam
107
+ - id: perk-checkpoints # perk's own checkpoints
108
+ seam: todo
109
+ package: null
110
+ adapter: null
111
+ default: true # the behavior-preserving no-config pick for the todo seam
112
+ - id: perk-ask-user # perk's own ask_user_question tool (extension/doors/askUser.ts)
113
+ seam: askuser
114
+ package: null
115
+ adapter: null
116
+ default: true # the behavior-preserving no-config pick for the askuser seam
117
+
118
+ # --- foreign entries ---
119
+ - id: tombell-plan # REAL plan provider (Node 2.3) — planAdapterTombell bridges it
120
+ seam: plan
121
+ package: "npm:@tombell/pi-plan" # the foreign Pi package to add to `packages`
122
+ adapter: "planAdapterTombell" # the perk-owned shim bridging surface → cache.plan-ref
123
+ default: false
124
+ # No `package_filter`: `@tombell/pi-plan`'s sole extension is its root `index.ts` (not under
125
+ # `extensions/`), so omitting the filter ("load all of that type") loads exactly that one
126
+ # extension. The `package_filter` field stays in the vocabulary for future providers.
127
+ - id: plannotator-plan # REAL plan provider — planAdapterPlannotator bridges its browser review
128
+ seam: plan
129
+ package: "npm:@plannotator/pi-extension" # the foreign Pi package to add to `packages`
130
+ adapter: "planAdapterPlannotator" # the perk-owned shim bridging plan_review → its event API
131
+ default: false
132
+ # AUGMENT posture (contrast with tombell-plan's REPLACE posture): plannotator does not replace
133
+ # perk's plan surface — perk's `/plan` command, authoring injection, and read-only gate STAY
134
+ # registered; perk skips only the two real registration collisions (`--plan` flag and the
135
+ # `Ctrl+Alt+P` shortcut, both of which plannotator also registers — duplicate flag/shortcut
136
+ # registration is the known potentially-fatal Pi behavior). The bridge is the model-callable
137
+ # `plan_review` tool (planAdapterPlannotator) → plannotator's `plannotator:request` plan-review
138
+ # event flow (its browser review UI). Saving stays the human-run `/plan-save`.
139
+ # No `package_filter`: `@plannotator/pi-extension`'s `pi.extensions` is `["./"]` (the sole
140
+ # extension is the package root), so omitting the filter loads exactly that one extension.
141
+ - id: juicesharp-todo # REAL todo provider (Node 3.2) — todoAdapterJuicesharp bridges it
142
+ seam: todo
143
+ package: "npm:@juicesharp/rpiv-todo" # the foreign Pi package to add to `packages`
144
+ adapter: "todoAdapterJuicesharp" # the perk-owned shim carrying progress discipline onto it
145
+ default: false
146
+ # No `package_filter`: `@juicesharp/rpiv-todo` is a single-concern checklist overlay, so omitting
147
+ # the filter ("load all of that type") loads its bundled extension. The smoke's manual half is the
148
+ # gate that no perk surface collides; a targeted `extensions` filter is a follow-up if one ever does.
149
+ - id: juicesharp-ask-user # REAL askuser provider — VACATE-ONLY (interface seam, no adapter)
150
+ seam: askuser
151
+ package: "npm:@juicesharp/rpiv-ask-user-question" # the foreign Pi package to add to `packages`
152
+ adapter: null # VACATE-ONLY: the foreign tool shares the exact name `ask_user_question`,
153
+ # so perk's `registerAskUser` registers NOTHING under this selection —
154
+ # there is no durable artifact to bridge (interface seam), and the
155
+ # foreign tool self-documents via its own `promptGuidelines`.
156
+ default: false
157
+ # No `package_filter`: the verified manifest is `{"extensions": ["./index.ts"]}`, so omitting the
158
+ # filter ("load all of that type") loads exactly that one extension — the same reasoning as the
159
+ # tombell-plan / juicesharp-todo entries.
160
+ - id: perk-footer # perk's own footer (extension/surfaces/surfaces.ts installPerkFooter)
161
+ seam: footer
162
+ package: null
163
+ adapter: null
164
+ default: true # the behavior-preserving no-config pick for the footer seam
165
+ - id: powerline-footer # REAL footer provider — VACATE-ONLY (interface seam, no adapter)
166
+ seam: footer
167
+ package: "npm:pi-powerline-footer" # the foreign Pi package to add to `packages`
168
+ adapter: null # VACATE-ONLY: the footer produces no durable artifact (interface seam),
169
+ # and pi-powerline-footer already renders perk's `perk` setStatus slot in
170
+ # its extension_statuses segment, so the bridge is automatic (no shim).
171
+ default: false
172
+ # No `package_filter`: load the package's sole footer extension ("load all of that type").
173
+ - id: pi-bar-footer # REAL footer provider — VACATE-ONLY (interface seam, no adapter)
174
+ seam: footer
175
+ package: "npm:pi-bar" # the foreign Pi package to add to `packages`
176
+ adapter: null # VACATE-ONLY: pi-bar replaces pi's footer and "keeps extension statuses
177
+ # visible", so perk's `perk` setStatus slot appears automatically — no shim.
178
+ default: false
179
+ # No `package_filter`: load the package's sole footer extension ("load all of that type").
180
+ - id: pi-status-footer # REAL footer provider — VACATE-ONLY (interface seam, no adapter)
181
+ seam: footer
182
+ package: "npm:@tombell/pi-status" # the foreign Pi package to add to `packages`
183
+ adapter: null # VACATE-ONLY: the footer produces no durable artifact (interface seam).
184
+ # UNLIKE powerline/pi-bar, pi-status does NOT render extension statuses,
185
+ # so perk's objective/checkpoints progress is NOT shown in the footer
186
+ # under this selection (an accepted limitation — matches pi-status today;
187
+ # no status-bridge adapter is built).
188
+ default: false
189
+ # No `package_filter`: load the package's sole footer extension ("load all of that type").
190
+ - id: pi-default # "install nothing" footer provider — leaves pi's stock built-in footer
191
+ seam: footer
192
+ package: null # null ⇒ perk adds NO package; pi's own built-in footer stands.
193
+ adapter: null # VACATE-ONLY: perk's footer-install gate is false (resolved id ≠
194
+ # perk-footer), so perk skips installPerkFooter; nothing else to add.
195
+ default: false
196
+ - id: pi-web-access # web seam DEFAULT — the FOREIGN reference provider (perk owns no native web impl)
197
+ seam: web
198
+ package: "npm:pi-web-access" # NON-NULL default package: the novelty — perk has no native web provider
199
+ adapter: null # VACATE-ONLY interface seam: no durable artifact, and perk registers NO web
200
+ # tools, so there is no perk surface to vacate. Selection just swaps the package.
201
+ default: true # the behavior-preserving no-config pick (zero-config Exa search + `librarian`)
202
+ # No `package_filter`: `pi-web-access`'s sole extension is its root `./index.ts` (verified via
203
+ # `npm view pi-web-access pi`), so omitting the filter loads exactly that one extension.
204
+ - id: ollama-web-search # REAL web provider — VACATE-ONLY (interface seam, no adapter)
205
+ seam: web
206
+ package: "npm:@ollama/pi-web-search" # the foreign Pi package to add to `packages`
207
+ adapter: null # VACATE-ONLY: web is an interface seam (no artifact); perk registers no web
208
+ # tools, so nothing to vacate. Tools: `ollama_web_search`/`ollama_web_fetch`
209
+ # (needs a local Ollama daemon). Divergent names — perk allowlists the union.
210
+ default: false
211
+ # No `package_filter`: verified manifest is `{"extensions": ["./index.ts"]}`, so omitting the
212
+ # filter loads exactly that one extension.
213
+ - id: juicesharp-web-tools # REAL web provider — VACATE-ONLY (interface seam, no adapter)
214
+ seam: web
215
+ package: "npm:@juicesharp/rpiv-web-tools" # the foreign Pi package to add to `packages`
216
+ adapter: null # VACATE-ONLY: interface seam, no artifact; perk registers no web tools.
217
+ # Tools: `web_search`/`web_fetch` (needs an API key; default provider Brave;
218
+ # registers a `/web-tools` command — no perk collision).
219
+ default: false
220
+ # No `package_filter`: verified manifest is `{"extensions": ["./index.ts"]}`, so omitting the
221
+ # filter loads exactly that one extension (the juicesharp single-extension convention).
@@ -0,0 +1,199 @@
1
+ # perk stage registry — the language-neutral contract both planes read (Q4/Q6).
2
+ #
3
+ # Authored once here, bundled into each build artifact (wheel -> perk/_shared/,
4
+ # npm tarball -> shared/), and parsed directly by both planes at runtime: the
5
+ # Python CLI generates its subcommands from it (T4); the TS extension drives its
6
+ # in-session transitions from it (Phase 1+). No codegen step (Q6).
7
+ #
8
+ # T2 locks the descriptor *shape*, the stage *graph*, and the state-key
9
+ # *vocabulary*. The drift-prone per-stage state I/O (`requires`/`reads`/`writes`)
10
+ # is left empty and filled as each handler lands (Phase 1+) — locking the shape,
11
+ # not authoring fiction about unbuilt stages.
12
+
13
+ schema_version: 1
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # State-key vocabulary (Q4). `requires`/`reads`/`writes` draw ONLY from these,
17
+ # written as dotted "<tier>.<key>" strings (e.g. "github.plan"). The self-check
18
+ # flattens this map to a set for membership validation.
19
+ # ---------------------------------------------------------------------------
20
+ state_keys:
21
+ # Tier 1 · GitHub — the canonical source of truth.
22
+ github:
23
+ - plan # the plan issue (header + body)
24
+ - objective # the objective issue (storage + mechanics live in P2.T9; its stage in T10)
25
+ - pr # the pull request
26
+ - labels # repo labels (e.g. perk:plan)
27
+ - comments # discussion comments
28
+ - review-threads # review threads (distinct API from comments)
29
+ - learn # the perk:learn knowledge-capture issue (P2.T8b)
30
+ # Tier 2 · .pi/workflow/ — the local cache (see contracts.md §8.1).
31
+ cache:
32
+ - plan # materialized plan body cache
33
+ - plan-ref # provider-agnostic plan->branch ref (see contracts.md §8.4)
34
+ - scratch # per-run inter-process workflow files
35
+ - handoff # pre-session CLI->extension cold-door state
36
+ - markers # existence-based friction semaphores (e.g. pending-learn)
37
+ - session-data # the run-scoped session data dir artifacts (scratch/runs/<run_id>/data/; #339 Node 2.1)
38
+ # Tier 3 · session entries — transient in-session state.
39
+ session:
40
+ - workflow-state # the single `perk:workflow-state` record (contracts.md §8.3)
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Stages — the loop (Q5): objective-author -> objective-save -> objective-plan -> plan ->
44
+ # save -> implement -> submit -> address -> land -> learn.
45
+ #
46
+ # Every field is locked EXCEPT `requires`/`reads`/`writes` (empty until handlers
47
+ # land). `run_id` is uniform AND shape-locked by the validator: `perk/substrate/registry.py`
48
+ # `_check_doors_and_run_id` enforces `warm == keep` and `cold_local`/`cold_remote == mint`
49
+ # for every stage (Q2). It is a shape-locked map, NOT free per-stage data — the field is kept
50
+ # because the shape is the contract, not because stages vary.
51
+ # `command` is the human/contract LABEL for the stage's command, not the routing table: the
52
+ # actual dedicated cold-door routing lives in code via `perk/cli/stages.py` `DEDICATED_STAGES`
53
+ # (the generic launcher generator skips those ids). The field documents intent; it does not route.
54
+ # `doors.cold_remote` records which stages a remote runner may run (cli-vs-pi §4.5) —
55
+ # a real registry-recorded constraint, not merely "not built yet". P2.T8c flips it
56
+ # `true` on `implement` + `address` (the agentic, headless-runnable stages a Phase-3 CI
57
+ # worker drives); `plan`/`save` need interactive local exploration and
58
+ # `submit`/`land`/`learn` are quick deterministic ops, so they stay `false`. Phase 2
59
+ # *builds + resolves* the target (the resolver + this door + the --json descriptor);
60
+ # Phase 3 *drives* it. Revisable in Phase 3.
61
+ # ---------------------------------------------------------------------------
62
+ stages:
63
+ - id: objective-author
64
+ summary: Draft a new objective + roadmap (read-only authoring).
65
+ mode: read-only
66
+ worktree: none # interactive authoring, like plan — no branch, no objective yet
67
+ doors: { warm: true, cold_local: true, cold_remote: false } # interactive local authoring
68
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
69
+ command: objective author # P3.T2: dedicated seeded cold door (perk objective author)
70
+ requires: [] # nothing exists yet — this stage CREATES the objective
71
+ reads: []
72
+ writes: [session.workflow-state, cache.session-data] # the read-only `mode` append (the gate toggle); #352 Node 2.1: objective_draft writes the objective-draft artifact + its session_artifacts pointer
73
+ predecessors: [] # the new single initial (authoring precedes everything)
74
+ successors: [objective-save]
75
+
76
+ - id: objective-save
77
+ summary: Persist the drafted objective to GitHub; the read-only -> read-write boundary.
78
+ mode: read-write
79
+ worktree: none # still no branch; save writes the objective issue
80
+ doors: { warm: true, cold_local: true, cold_remote: false }
81
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
82
+ command: objective save # P3.T2: the objective_save terminating tool + /objective-save command
83
+ requires: []
84
+ reads: []
85
+ writes: [github.objective, session.workflow-state] # create the objective issue; link active_objective
86
+ predecessors: [objective-author]
87
+ successors: [objective-plan]
88
+
89
+ - id: objective-plan
90
+ summary: Select the next objective node and author a bounded plan (read-only).
91
+ mode: read-only
92
+ worktree: none # interactive planning, like plan — no branch yet
93
+ doors: { warm: true, cold_local: true, cold_remote: false } # interactive local exploration
94
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
95
+ command: objective plan # P2.T10: dedicated cold door (perk objective plan) + /objective-plan
96
+ requires: [github.objective] # an objective issue must exist
97
+ reads: [github.objective] # roadmap nodes + next-node selection
98
+ writes: [github.objective, session.workflow-state, cache.session-data] # mark node planning; set active_objective; #339 Node 2.1: plan_draft writes the working-plan artifact + its session_artifacts pointer
99
+ predecessors: [objective-save] # P3.T2: objective-save now precedes planning
100
+ successors: [plan]
101
+
102
+ - id: plan
103
+ summary: Explore the codebase and draft a plan (read-only).
104
+ mode: read-only
105
+ worktree: none # no implementation branch exists yet
106
+ doors: { warm: true, cold_local: true, cold_remote: false }
107
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
108
+ command: plan # slash = /plan (perk-owned P2.T2a; toggles the read-only tool gate)
109
+ requires: []
110
+ reads: []
111
+ writes: [session.workflow-state, cache.session-data] # P2.T2a: /plan enter/exit appends `mode`; #339 Node 2.1: plan_draft writes the working-plan artifact + its session_artifacts pointer
112
+ predecessors: [objective-plan] # P2.T10: objective-plan is the new single initial
113
+ successors: [save]
114
+
115
+ - id: save
116
+ summary: Persist the plan to GitHub; the read-only -> read-write boundary.
117
+ mode: read-write
118
+ worktree: none # still no branch; save writes the plan issue + ref
119
+ doors: { warm: true, cold_local: true, cold_remote: false }
120
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
121
+ command: plan save # cold: the merged `perk plan save` door (Node 3.2); warm twin: the plan_save tool + /plan-save command
122
+ requires: []
123
+ reads: []
124
+ writes: [github.plan, cache.plan-ref, session.workflow-state] # T2a: plan issue; T2b: plan-ref pointer; T3: warm door appends active_plan_ref
125
+ predecessors: [plan]
126
+ successors: [implement]
127
+
128
+ - id: implement
129
+ summary: Do the work on a branch (requires fresh context; cold-only).
130
+ mode: read-write
131
+ worktree: create # first stage that needs a branch/worktree (folds in erk's `prepare`)
132
+ doors: { warm: false, cold_local: true, cold_remote: true } # P2.T8c: a Phase-3 CI worker can run it
133
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
134
+ command: implement
135
+ requires: [cache.plan-ref] # T4: cold door derives plan-<pr_id> from the active ref
136
+ reads: [cache.plan-ref] # T4: positions the worktree from the ref (agent reads github.plan)
137
+ writes: [session.workflow-state] # T4: the worktree session links active_plan_ref on session_start
138
+ predecessors: [save]
139
+ successors: [submit]
140
+
141
+ - id: submit
142
+ summary: Push the branch and open a draft PR.
143
+ mode: read-write
144
+ worktree: reuse
145
+ doors: { warm: true, cold_local: true, cold_remote: false }
146
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
147
+ command: pr submit
148
+ requires: [cache.plan-ref] # T5a: reads the active ref to derive the branch
149
+ reads: [cache.plan-ref, github.plan] # T5a: ref -> branch; plan issue title for the PR
150
+ writes: [github.pr, github.plan] # T5a: open the draft PR; populate staged branch/pr header
151
+ predecessors: [implement]
152
+ successors: [address]
153
+
154
+ - id: address
155
+ summary: Classify PR review feedback (isolated child) and resolve the threads.
156
+ mode: read-write
157
+ worktree: reuse
158
+ doors: { warm: true, cold_local: true, cold_remote: true } # P2.T8c: a Phase-3 CI worker can run it
159
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
160
+ command: pr address
161
+ # P2.T7: a spawned read-only child (borrowed pi-subagents) fetches + classifies feedback;
162
+ # the parent fixes only actionable items and batch-resolves the threads (`resolve_review_threads`).
163
+ requires: [github.pr] # the PR whose feedback we address
164
+ reads: [cache.plan-ref, github.pr, github.review-threads, github.comments]
165
+ writes: [github.review-threads, github.comments, github.pr, session.workflow-state]
166
+ predecessors: [submit]
167
+ successors: [land]
168
+
169
+ - id: land
170
+ summary: Merge the ready/approved PR and reconcile; sets the pending-learn marker.
171
+ mode: read-write
172
+ worktree: reuse
173
+ doors: { warm: true, cold_local: true, cold_remote: false }
174
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
175
+ command: pr land
176
+ requires: [github.pr] # T5b: the PR to merge must exist
177
+ reads: [cache.plan-ref, github.pr, github.objective] # T5b ref->branch->PR; T11a node lookup
178
+ writes: [github.pr, cache.markers, github.objective] # T5b merge+marker; T11a mechanical node-done
179
+ predecessors: [address]
180
+ successors: [learn]
181
+
182
+ - id: learn
183
+ summary: Capture learnings from the landed run; clears the pending-learn marker.
184
+ mode: read-write
185
+ worktree: reuse # pending-learn blocks worktree deletion until this runs (Q5)
186
+ doors: { warm: true, cold_local: true, cold_remote: false }
187
+ run_id: { warm: keep, cold_local: mint, cold_remote: mint }
188
+ command: learn
189
+ # P2.T8b: deepened from a thin marker-clear into a real knowledge-capture pass — with a
190
+ # summary, create a perk:learn issue (idempotent via the perk:learn-scoped find_learn_issue) +
191
+ # back-link comment, then clear pending-learn; without one, the thin marker-clear (no issue).
192
+ # P2.T17: now launches PRIMED (launch.py _learn_prompt) — the cold session opens investigating
193
+ # the landed change; bare warm /learn injects perk-learn guidance (interactive). No new I/O
194
+ # keys (no new gateway op) — the existing learn tool / learn capture worker still persist.
195
+ requires: [cache.markers] # pending-learn gates the worktree
196
+ reads: [cache.markers, cache.plan-ref] # was it set? + the plan ref -> issue number/run_id
197
+ writes: [cache.markers, github.learn, github.comments] # clear marker; create learn issue + back-link
198
+ predecessors: [land]
199
+ successors: []