@mgiles/perk 2.2.0 → 3.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 (129) hide show
  1. package/README.md +1 -1
  2. package/extension/adapters/planAdapterPlannotator.ts +132 -91
  3. package/extension/adapters/planAdapterTombell.ts +5 -3
  4. package/extension/doors/address.ts +235 -37
  5. package/extension/doors/annotationPush.ts +924 -0
  6. package/extension/doors/auditWaveTools.ts +352 -0
  7. package/extension/doors/ciExecutor.ts +220 -55
  8. package/extension/doors/draftReviewWaveTools.ts +384 -0
  9. package/extension/doors/harvestWaveTools.ts +279 -0
  10. package/extension/doors/land.ts +1 -0
  11. package/extension/doors/learn.ts +238 -24
  12. package/extension/doors/learnFactory.ts +33 -7
  13. package/extension/doors/lifecycleGates.ts +1 -1
  14. package/extension/doors/objectiveReviewBrowser.ts +514 -0
  15. package/extension/doors/objectiveStack.ts +1143 -0
  16. package/extension/doors/planReviewBrowser.ts +453 -0
  17. package/extension/doors/plannotatorHandoff.ts +120 -49
  18. package/extension/doors/prReview.ts +203 -22
  19. package/extension/doors/prReviewBrowser.ts +31 -23
  20. package/extension/doors/prReviewDynamic.ts +256 -0
  21. package/extension/doors/prReviewTerminal.ts +6 -11
  22. package/extension/doors/reviewWaveTools.ts +374 -0
  23. package/extension/doors/selfcheck.ts +1 -1
  24. package/extension/doors/submit.ts +86 -10
  25. package/extension/factories/gistAuthor.ts +4 -4
  26. package/extension/factories/gistDraft.ts +1 -1
  27. package/extension/factories/gistSave.ts +2 -2
  28. package/extension/factories/objective.ts +7 -7
  29. package/extension/factories/objectiveAuthor.ts +5 -4
  30. package/extension/factories/objectiveDraft.ts +61 -6
  31. package/extension/factories/objectivePlan.ts +163 -8
  32. package/extension/factories/objectiveSave.ts +16 -1
  33. package/extension/factories/planMode.ts +5 -3
  34. package/extension/factories/planReview.ts +131 -36
  35. package/extension/factories/planTitle.ts +27 -9
  36. package/extension/hunkFeedback/inbox.ts +389 -0
  37. package/extension/hunkFeedback/perkFeedback.ts +364 -0
  38. package/extension/hunkFeedback/receiver.ts +296 -0
  39. package/extension/hunkFeedback/store.ts +533 -0
  40. package/extension/index.ts +116 -44
  41. package/extension/substrate/cache.ts +75 -22
  42. package/extension/substrate/coldDoor.ts +2 -3
  43. package/extension/substrate/config.ts +42 -14
  44. package/extension/substrate/git.ts +10 -0
  45. package/extension/substrate/providers.ts +4 -33
  46. package/extension/substrate/resources.ts +11 -0
  47. package/extension/substrate/result.ts +13 -5
  48. package/extension/substrate/sessionData.ts +3 -3
  49. package/extension/substrate/sessionPointers.ts +3 -3
  50. package/extension/substrate/structuredOutput.ts +78 -19
  51. package/extension/substrate/terminalLaunch.ts +1 -1
  52. package/extension/substrate/toolGating.ts +206 -26
  53. package/extension/substrate/workflowState.ts +6 -0
  54. package/extension/surfaces/footerProvider.ts +6 -6
  55. package/extension/surfaces/surfaces.ts +98 -282
  56. package/extension/vendor/btw/btw.ts +57 -12
  57. package/extension/waves/adversarialReviewWave.ts +164 -0
  58. package/extension/waves/auditWave.ts +312 -0
  59. package/extension/waves/draftReviewWave.ts +189 -0
  60. package/extension/waves/harvestWave.ts +399 -0
  61. package/extension/waves/learnWave.ts +155 -0
  62. package/extension/waves/memoryAdapter.ts +139 -0
  63. package/extension/waves/objectiveExplorerWave.ts +120 -0
  64. package/extension/waves/prReviewDynamicWave.ts +696 -0
  65. package/extension/waves/prReviewWave.ts +274 -0
  66. package/extension/waves/reportWave.ts +749 -0
  67. package/extension/waves/reviewClassifierWave.ts +109 -0
  68. package/extension/waves/rpcAdapter.ts +264 -0
  69. package/extension/worker/readOnlySession.ts +3 -3
  70. package/extension/worker/worker.ts +71 -93
  71. package/extension/workerMain.ts +4 -6
  72. package/package.json +20 -5
  73. package/prompts/README.md +19 -0
  74. package/prompts/_fixtures/live.yaml +71 -72
  75. package/prompts/common/resume-advisory.md +1 -0
  76. package/prompts/contexts/adapters/plannotator-gist.md +13 -0
  77. package/prompts/contexts/adapters/plannotator-objective.md +1 -2
  78. package/prompts/contexts/adapters/plannotator-plan.md +1 -2
  79. package/prompts/contexts/gist-authoring.md +6 -20
  80. package/prompts/contexts/objective-authoring.md +6 -18
  81. package/prompts/stages/address/action.md +4 -4
  82. package/prompts/stages/address/preview.md +3 -3
  83. package/prompts/stages/audit.md +18 -0
  84. package/prompts/stages/conflict-resolution.md +1 -1
  85. package/prompts/stages/gist-author/seed.md +4 -4
  86. package/prompts/stages/gist-save.md +1 -1
  87. package/prompts/stages/implement.md +3 -1
  88. package/prompts/stages/learn-code.md +1 -1
  89. package/prompts/stages/learn-docs.md +2 -2
  90. package/prompts/stages/learn-harvest.md +15 -0
  91. package/prompts/stages/learn-orchestrate.md +7 -5
  92. package/prompts/stages/objective-author/adopt.md +5 -3
  93. package/prompts/stages/objective-author/file.md +5 -4
  94. package/prompts/stages/objective-author/seed.md +5 -4
  95. package/prompts/stages/objective-land.md +7 -0
  96. package/prompts/stages/objective-plan/guidance.md +2 -2
  97. package/prompts/stages/objective-plan/seed.md +5 -1
  98. package/prompts/stages/objective-recover.md +8 -0
  99. package/prompts/stages/objective-replan.md +4 -2
  100. package/prompts/stages/objective-review-browser.md +9 -0
  101. package/prompts/stages/objective-sync.md +6 -0
  102. package/prompts/stages/plan-from/adopt.md +2 -2
  103. package/prompts/stages/plan-from/file.md +2 -2
  104. package/prompts/stages/plan-review-browser.md +9 -0
  105. package/prompts/stages/pr-review-browser/active.md +6 -6
  106. package/prompts/stages/pr-review-browser/foreign.md +6 -6
  107. package/prompts/stages/pr-review-dynamic.md +7 -0
  108. package/prompts/stages/pr-review-terminal/active.md +7 -7
  109. package/prompts/stages/pr-review-terminal/foreign.md +7 -7
  110. package/prompts/stages/pr-review.md +7 -6
  111. package/prompts/stages/replan.md +3 -3
  112. package/shared/README.md +2 -2
  113. package/shared/bindings.yaml +15 -0
  114. package/shared/contracts-history.md +11 -0
  115. package/shared/contracts.md +3713 -227
  116. package/shared/providers.yaml +27 -60
  117. package/shared/registry.yaml +32 -8
  118. package/shared/schemas/outputs/objective-doctor.schema.json +449 -0
  119. package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
  120. package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
  121. package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
  122. package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
  123. package/shared/schemas/outputs/plan-save.schema.json +12 -0
  124. package/shared/schemas/outputs/pr-submit.schema.json +177 -1
  125. package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
  126. package/extension/checkpoints/checkpoints.ts +0 -550
  127. package/extension/checkpoints/planSteps.ts +0 -108
  128. package/extension/doors/askUser.ts +0 -198
  129. package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
package/README.md CHANGED
@@ -65,7 +65,7 @@ For the guided first run, follow
65
65
 
66
66
  ## Documentation
67
67
 
68
- Operator docs live under [`docs/user-docs/`](docs/user-docs/index.md):
68
+ Operator docs live under [`docs/user-docs/`](docs/user-docs/index.mdx):
69
69
 
70
70
  - **[Tutorials](docs/user-docs/tutorials/index.md)** — learn by doing; start with
71
71
  [Get started with perk](docs/user-docs/tutorials/get-started.md).
@@ -7,12 +7,16 @@
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, each once-only: branch-scan
11
- // dedup'd on the flavor's marker: the plan bridge context, or the
12
- // objective flavor when the stage is `objective-author`) and (2) the pure event-bus bridge
13
- // (`createPlannotatorBridge`) that planReview.ts dispatches to when plannotator is the
14
- // selected plan provider. The bridge speaks plannotator's published `plannotator:request` event
15
- // API (in-process `pi.events` bus).
10
+ // plannotator is selected — THREE content flavors, one customType, each once-only: branch-scan
11
+ // dedup'd on the flavor's marker: the plan bridge context, the objective flavor when the stage
12
+ // is `objective-author`, or the gist flavor when the stage is `gist-author`) and (2) the pure
13
+ // event-bus bridge
14
+ // (`requestPlannotatorPlanReview`; `createPlannotatorBridge` is its thin structural wrapper)
15
+ // that planReview.ts dispatches to when plannotator is the selected plan provider and the
16
+ // plan-review browser open (plannotatorHandoff.ts) launches. The bridge speaks plannotator's
17
+ // published `plannotator:request` event API (in-process `pi.events` bus); the decision wait is
18
+ // a per-review `plannotator:review-result` listener disposed via the unsubscribe pi's
19
+ // `EventBus.on` returns.
16
20
  //
17
21
  // INERT BY DEFAULT. The shim is ALWAYS registered in index.ts but the injection fires only when
18
22
  // the resolved `[providers] plan` selection is `plannotator-plan` (read fresh per-event, same
@@ -46,6 +50,7 @@
46
50
 
47
51
  import { randomUUID } from "node:crypto";
48
52
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
53
+ import { GIST_AUTHOR_STAGE } from "../factories/gistAuthor.ts";
49
54
  import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
50
55
  import { resolvedPlanProviderId } from "../factories/planMode.ts";
51
56
  // Type-only (erased at runtime — no cycle): the outcome vocabulary lives with the review door.
@@ -58,6 +63,7 @@ import { branchCarries, branchOf, rebuildWorkflowState } from "../substrate/work
58
63
  export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE = "perk:plan-adapter-plannotator";
59
64
  const PLAN_ADAPTER_PLANNOTATOR_MARKER = "[PLAN ADAPTER: PLANNOTATOR]";
60
65
  const OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER = "[OBJECTIVE ADAPTER: PLANNOTATOR]";
66
+ const GIST_ADAPTER_PLANNOTATOR_MARKER = "[GIST ADAPTER: PLANNOTATOR]";
61
67
 
62
68
  /**
63
69
  * The handshake timeout for plannotator's immediate `respond` callback (mirrors plannotator's own
@@ -90,6 +96,16 @@ export const OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT = render(
90
96
  { marker: OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER },
91
97
  );
92
98
 
99
+ /**
100
+ * The gist flavor of the bridge prompt, injected in a `gist-author` session instead of the
101
+ * plan/objective flavors. The review surface renders the gist draft (title + scope line +
102
+ * prose); an approval carrying `# Direct Edits` does NOT auto-save — the model folds the diff
103
+ * into the matching `gist_draft` fields and re-reviews (contracts.md §8.23's gist arm).
104
+ */
105
+ export const GIST_ADAPTER_PLANNOTATOR_CONTEXT = render("contexts/adapters/plannotator-gist.md", {
106
+ marker: GIST_ADAPTER_PLANNOTATOR_MARKER,
107
+ });
108
+
93
109
  /** Whether the foreign `plannotator-plan` provider is the selected plan provider for `cwd`. */
94
110
  export function isPlannotatorPlanSelected(cwd: string): boolean {
95
111
  return resolvedPlanProviderId(cwd) === PLANNOTATOR_PLAN_PROVIDER_ID;
@@ -97,10 +113,10 @@ export function isPlannotatorPlanSelected(cwd: string): boolean {
97
113
 
98
114
  // ------------------------------------------------------------------ the event-bus bridge core
99
115
 
100
- /** The minimal `pi.events` surface the bridge needs (mirrors pi's EventBus). */
116
+ /** The minimal `pi.events` surface the bridge needs (mirrors pi's EventBus, whose `on` returns an unsubscribe function). */
101
117
  export interface PlannotatorBus {
102
118
  emit(channel: string, data: unknown): void;
103
- on(channel: string, handler: (data: unknown) => void): void;
119
+ on(channel: string, handler: (data: unknown) => void): () => void;
104
120
  }
105
121
 
106
122
  /** Plannotator's immediate `respond` handshake payload (pinned envelope, see header). */
@@ -117,84 +133,96 @@ interface ReviewDecision {
117
133
  }
118
134
 
119
135
  /**
120
- * Create the plannotator bridge over an event bus: ONE persistent `plannotator:review-result`
121
- * listener registered up front, resolving pending reviews from a Map keyed by reviewId (no
122
- * dependence on an undocumented `pi.events.off`). Pure over the bus → unit-testable offline with a
123
- * fake plannotator listener.
136
+ * The pure, offline-testable plan-review bridge (the ergonomic mirror of
137
+ * `requestPlannotatorCodeReview` in plannotatorHandoff.ts): emit ONE `plannotator:request` with
138
+ * `action: "plan-review"`, await the bounded `respond` handshake, then await the human decision
139
+ * on a PER-REVIEW `plannotator:review-result` listener — filtered on the handshake's `reviewId`
140
+ * and disposed via the unsubscribe `bus.on` returns when the decision arrives or the turn
141
+ * aborts. Pure over the bus → unit-testable offline with a fake plannotator listener.
124
142
  */
125
- export function createPlannotatorBridge(bus: PlannotatorBus): {
126
- review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome>;
127
- } {
128
- const pending = new Map<string, (decision: ReviewDecision) => void>();
143
+ export async function requestPlannotatorPlanReview(
144
+ bus: PlannotatorBus,
145
+ plan: string,
146
+ signal?: AbortSignal,
147
+ ): Promise<ReviewOutcome> {
148
+ if (signal?.aborted) return { status: "aborted" };
129
149
 
130
- bus.on("plannotator:review-result", (data) => {
131
- const d = data as { reviewId?: unknown; approved?: unknown; feedback?: unknown };
132
- if (typeof d?.reviewId !== "string") return;
133
- const resolve = pending.get(d.reviewId);
134
- if (resolve === undefined) return;
135
- pending.delete(d.reviewId);
136
- resolve({
137
- approved: d.approved === true,
138
- feedback: typeof d.feedback === "string" && d.feedback.trim() ? d.feedback : undefined,
139
- });
150
+ // 1. Emit the request and await the immediate `respond` handshake (bounded fail-open).
151
+ const requestId = randomUUID();
152
+ let respondResolve: (response: HandshakeResponse) => void = () => {};
153
+ const handshake = new Promise<HandshakeResponse | "timeout">((resolve) => {
154
+ respondResolve = resolve;
140
155
  });
156
+ const timer = setTimeout(() => respondResolve("timeout" as never), handshakeTimeoutMs());
157
+ bus.emit("plannotator:request", {
158
+ requestId,
159
+ action: "plan-review",
160
+ payload: { planContent: plan, origin: "perk" },
161
+ respond: (response: unknown) => respondResolve(response as HandshakeResponse),
162
+ });
163
+ const response = await handshake;
164
+ clearTimeout(timer);
141
165
 
142
- async function review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> {
143
- if (signal?.aborted) return { status: "aborted" };
144
-
145
- // 1. Emit the request and await the immediate `respond` handshake (bounded — fail-open).
146
- const requestId = randomUUID();
147
- let respondResolve: (response: HandshakeResponse) => void = () => {};
148
- const handshake = new Promise<HandshakeResponse | "timeout">((resolve) => {
149
- respondResolve = resolve;
150
- });
151
- const timer = setTimeout(() => respondResolve("timeout" as never), handshakeTimeoutMs());
152
- bus.emit("plannotator:request", {
153
- requestId,
154
- action: "plan-review",
155
- payload: { planContent: plan, origin: "perk" },
156
- respond: (response: unknown) => respondResolve(response as HandshakeResponse),
157
- });
158
- const response = await handshake;
159
- clearTimeout(timer);
166
+ if (response === "timeout") {
167
+ return {
168
+ status: "unavailable",
169
+ warning: "plannotator did not respond to the review request (handshake timeout)",
170
+ };
171
+ }
172
+ if (response?.status !== "handled") {
173
+ const detail = response?.error ? `: ${response.error}` : "";
174
+ return {
175
+ status: "unavailable",
176
+ warning: `plannotator reported ${response?.status ?? "an invalid response"}${detail}`,
177
+ };
178
+ }
179
+ const reviewId = response.result?.reviewId;
180
+ if (response.result?.status !== "pending" || typeof reviewId !== "string") {
181
+ return {
182
+ status: "unavailable",
183
+ warning: "plannotator handshake returned no pending reviewId",
184
+ };
185
+ }
160
186
 
161
- if (response === "timeout") {
162
- return {
163
- status: "unavailable",
164
- warning: "plannotator did not respond to the review request (handshake timeout)",
165
- };
166
- }
167
- if (response?.status !== "handled") {
168
- const detail = response?.error ? `: ${response.error}` : "";
169
- return {
170
- status: "unavailable",
171
- warning: `plannotator reported ${response?.status ?? "an invalid response"}${detail}`,
172
- };
173
- }
174
- const reviewId = response.result?.reviewId;
175
- if (response.result?.status !== "pending" || typeof reviewId !== "string") {
176
- return {
177
- status: "unavailable",
178
- warning: "plannotator handshake returned no pending reviewId",
179
- };
180
- }
187
+ // A turn aborted DURING the handshake wait must not wedge: `addEventListener("abort", …)` on
188
+ // an already-aborted signal never fires, so re-check before registering the decision wait.
189
+ if (signal?.aborted) return { status: "aborted" };
181
190
 
182
- // 2. Await the human decision (no timeout — the reviewer takes as long as they take), but
183
- // honor a turn abort so an interrupted session never leaks a wedged promise.
184
- return await new Promise<ReviewOutcome>((resolve) => {
185
- const onAbort = (): void => {
186
- pending.delete(reviewId);
187
- resolve({ status: "aborted" });
191
+ // 2. Await the human decision (no timeout — the reviewer takes as long as they take), but
192
+ // honor a turn abort so an interrupted session never leaks a wedged promise. Either exit
193
+ // disposes the result listener via the unsubscribe.
194
+ return await new Promise<ReviewOutcome>((resolve) => {
195
+ let settled = false;
196
+ const finish = (outcome: ReviewOutcome): void => {
197
+ if (settled) return;
198
+ settled = true;
199
+ unsubscribe();
200
+ signal?.removeEventListener("abort", onAbort);
201
+ resolve(outcome);
202
+ };
203
+ const onAbort = (): void => finish({ status: "aborted" });
204
+ const unsubscribe = bus.on("plannotator:review-result", (data) => {
205
+ const d = data as { reviewId?: unknown; approved?: unknown; feedback?: unknown };
206
+ if (d?.reviewId !== reviewId) return;
207
+ const decision: ReviewDecision = {
208
+ approved: d.approved === true,
209
+ feedback: typeof d.feedback === "string" && d.feedback.trim() ? d.feedback : undefined,
188
210
  };
189
- pending.set(reviewId, (decision) => {
190
- signal?.removeEventListener("abort", onAbort);
191
- resolve({ status: "completed", reviewId, ...decision });
192
- });
193
- signal?.addEventListener("abort", onAbort, { once: true });
211
+ finish({ status: "completed", reviewId, ...decision });
194
212
  });
195
- }
213
+ signal?.addEventListener("abort", onAbort, { once: true });
214
+ });
215
+ }
196
216
 
197
- return { review };
217
+ /**
218
+ * Create the plannotator bridge over an event bus — the thin structural slice
219
+ * (`{ review(plan, signal) }`) that `registerPlanReview` injects into the review door; the body
220
+ * lives in `requestPlannotatorPlanReview`.
221
+ */
222
+ export function createPlannotatorBridge(bus: PlannotatorBus): {
223
+ review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome>;
224
+ } {
225
+ return { review: (plan, signal) => requestPlannotatorPlanReview(bus, plan, signal) };
198
226
  }
199
227
 
200
228
  // ------------------------------------------------------------------ Direct Edits extraction
@@ -260,26 +288,38 @@ export function extractDirectEdits(feedback: string): { diff: string; remainder?
260
288
  */
261
289
  export function registerPlanAdapterPlannotator(pi: ExtensionAPI): void {
262
290
  // Inject the bridge context while the read-only gate is active AND plannotator is selected.
263
- // Two content flavors, one customType: an objective-author session (also read-only) gets the
264
- // objective flavor (the review surface renders the objective draft); any other
265
- // gated stage gets the plan flavor. The gate-active check reads the persisted
266
- // `perk:workflow-state.mode` (the gate's state twin) — never the gate itself.
291
+ // Three content flavors, one customType: an objective-author session (also read-only) gets
292
+ // the objective flavor (the review surface renders the objective draft), a gist-author
293
+ // session gets the gist flavor (the rendered gist draft); any other gated stage gets the plan
294
+ // flavor. The gate-active check reads the persisted `perk:workflow-state.mode` (the gate's
295
+ // state twin) — never the gate itself.
267
296
  pi.on("before_agent_start", async (_event, ctx) => {
268
297
  if (!isPlannotatorPlanSelected(ctx.cwd)) return;
269
298
  const branch = branchOf(ctx);
270
299
  const state = rebuildWorkflowState(branch);
271
300
  if (state.mode !== "read-only") return;
272
- const objectiveFlavor = state.stage === OBJECTIVE_AUTHOR_STAGE;
273
- const content = objectiveFlavor
274
- ? OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT
275
- : PLAN_ADAPTER_PLANNOTATOR_CONTEXT;
301
+ const flavor =
302
+ state.stage === OBJECTIVE_AUTHOR_STAGE
303
+ ? "objective"
304
+ : state.stage === GIST_AUTHOR_STAGE
305
+ ? "gist"
306
+ : "plan";
307
+ const content =
308
+ flavor === "objective"
309
+ ? OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT
310
+ : flavor === "gist"
311
+ ? GIST_ADAPTER_PLANNOTATOR_CONTEXT
312
+ : PLAN_ADAPTER_PLANNOTATOR_CONTEXT;
276
313
  // Once-only PER FLAVOR: the dedup key is the flavor's marker (not the shared customType), so
277
- // a stage change still delivers the missing flavor while a prior copy of the other flavor
314
+ // a stage change still delivers the missing flavor while a prior copy of another flavor
278
315
  // sits on the branch. Injected customs persist, so a live copy suppresses re-injection;
279
316
  // compaction dropping it makes the scan come up clean and the next turn re-injects.
280
- const marker = objectiveFlavor
281
- ? OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER
282
- : PLAN_ADAPTER_PLANNOTATOR_MARKER;
317
+ const marker =
318
+ flavor === "objective"
319
+ ? OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER
320
+ : flavor === "gist"
321
+ ? GIST_ADAPTER_PLANNOTATOR_MARKER
322
+ : PLAN_ADAPTER_PLANNOTATOR_MARKER;
283
323
  if (branchCarries(branch, marker)) return;
284
324
  return {
285
325
  message: {
@@ -290,11 +330,12 @@ export function registerPlanAdapterPlannotator(pi: ExtensionAPI): void {
290
330
  };
291
331
  });
292
332
 
293
- // Strip the stale bridge markers (BOTH flavors) from context when plannotator-plan is no
333
+ // Strip the stale bridge markers (ALL THREE flavors) from context when plannotator-plan is no
294
334
  // longer selected (same hygiene as the tombell shim), so they never linger across a deselect.
295
335
  const hasMarker = (text: string): boolean =>
296
336
  text.includes(PLAN_ADAPTER_PLANNOTATOR_MARKER) ||
297
- text.includes(OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER);
337
+ text.includes(OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER) ||
338
+ text.includes(GIST_ADAPTER_PLANNOTATOR_MARKER);
298
339
  pi.on("context", async (event, ctx) => {
299
340
  if (isPlannotatorPlanSelected(ctx.cwd)) return;
300
341
  return {
@@ -35,6 +35,7 @@
35
35
  // stages bind only to the provider-agnostic plan-ref and are unchanged.
36
36
 
37
37
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
38
+ import { GIST_AUTHOR_STAGE } from "../factories/gistAuthor.ts";
38
39
  import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
39
40
  import { resolvedPlanProviderId } from "../factories/planMode.ts";
40
41
  import { render } from "../substrate/prompts.ts";
@@ -89,13 +90,14 @@ export function registerPlanAdapterTombell(pi: ExtensionAPI): void {
89
90
  // Inject the bridge context while the foreign tombell-plan provider is selected AND a plan
90
91
  // authoring mode is on — perk's read-only gate (per the persisted `perk:workflow-state.mode`,
91
92
  // the gate's state twin — never the gate object) OR tombell's own persisted `plan-mode-state`
92
- // entry (the ad-hoc interactive `/plan` arm). Objective-author sessions are excepted
93
- // (objectiveAuthor owns that session; mirrors the plannotator adapter's recipe).
93
+ // entry (the ad-hoc interactive `/plan` arm). Objective-author and gist-author sessions are
94
+ // excepted (objectiveAuthor/gistAuthor own those sessions; mirrors the plannotator adapter's
95
+ // recipe — the tombell REPLACE posture covers the plan surface only).
94
96
  pi.on("before_agent_start", async (_event, ctx) => {
95
97
  if (!isTombellPlanSelected(ctx.cwd)) return;
96
98
  const branch = branchOf(ctx);
97
99
  const state = rebuildWorkflowState(branch);
98
- if (state.stage === OBJECTIVE_AUTHOR_STAGE) return;
100
+ if (state.stage === OBJECTIVE_AUTHOR_STAGE || state.stage === GIST_AUTHOR_STAGE) return;
99
101
  if (state.mode !== "read-only" && !isTombellPlanModeEnabled(branch)) return;
100
102
  // Once-only: injected customs persist to the branch, so a live copy suppresses re-injection;
101
103
  // compaction dropping it makes the scan come up clean and the next turn re-injects.