@mgiles/perk 3.2.0 → 3.3.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 (202) hide show
  1. package/README.md +5 -0
  2. package/extension/authoring/gist/draft.ts +198 -0
  3. package/extension/authoring/gist/prose.ts +46 -0
  4. package/extension/authoring/gist/review.ts +133 -0
  5. package/extension/authoring/gist/save.ts +118 -0
  6. package/extension/authoring/objective/draft.ts +345 -0
  7. package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
  8. package/extension/authoring/objective/planning.ts +124 -0
  9. package/extension/authoring/objective/prose.ts +103 -0
  10. package/extension/authoring/objective/review.ts +128 -0
  11. package/extension/authoring/objective/save.ts +224 -0
  12. package/extension/authoring/plan/draft.ts +84 -0
  13. package/extension/authoring/plan/prose.ts +41 -0
  14. package/extension/authoring/plan/review.ts +269 -0
  15. package/extension/authoring/plan/save.ts +256 -0
  16. package/extension/authoring/plan/source.ts +82 -0
  17. package/extension/authoring/refinement/context.ts +468 -0
  18. package/extension/authoring/refinement/draft.ts +261 -0
  19. package/extension/authoring/refinement/prose.ts +79 -0
  20. package/extension/authoring/refinement/review.ts +111 -0
  21. package/extension/authoring/refinement/save.ts +119 -0
  22. package/extension/authoring/review/approvalGate.ts +34 -0
  23. package/extension/authoring/review/draftContext.ts +68 -0
  24. package/extension/codeReview/automated.ts +352 -0
  25. package/extension/codeReview/submission.ts +229 -0
  26. package/extension/delivery/address.ts +295 -0
  27. package/extension/delivery/ci.ts +355 -0
  28. package/extension/delivery/commitCompact.ts +93 -0
  29. package/extension/delivery/conflictResolution.ts +247 -0
  30. package/extension/delivery/ready.ts +193 -0
  31. package/extension/delivery/stackConflict.ts +361 -0
  32. package/extension/delivery/stackObjective.ts +16 -0
  33. package/extension/delivery/stackReconcile.ts +165 -0
  34. package/extension/delivery/submit.ts +171 -0
  35. package/extension/index.ts +365 -380
  36. package/extension/learning/analystWave.ts +324 -0
  37. package/extension/learning/audit.ts +667 -0
  38. package/extension/learning/capture.ts +92 -0
  39. package/extension/learning/containment.ts +104 -0
  40. package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
  41. package/extension/learning/dreamAnalysis.ts +435 -0
  42. package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
  43. package/extension/{waves → learning}/dreamReport.ts +35 -31
  44. package/extension/learning/harvest.ts +491 -0
  45. package/extension/learning/prose.ts +66 -0
  46. package/extension/learning/routing.ts +79 -0
  47. package/extension/pi/v1/bashScanTimeout.ts +64 -0
  48. package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -311
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
  52. package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
  53. package/extension/pi/v1/codeReview/submit.ts +354 -0
  54. package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
  55. package/extension/pi/v1/contextEvidence.ts +80 -0
  56. package/extension/pi/v1/contextInjection.ts +207 -0
  57. package/extension/{doors → pi/v1/delivery}/address.ts +154 -267
  58. package/extension/pi/v1/delivery/ci.ts +570 -0
  59. package/extension/pi/v1/delivery/commitCompact.ts +201 -0
  60. package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
  61. package/extension/{doors → pi/v1/delivery}/land.ts +123 -61
  62. package/extension/pi/v1/delivery/ready.ts +322 -0
  63. package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
  64. package/extension/pi/v1/delivery/stackDrive.ts +120 -0
  65. package/extension/pi/v1/delivery/stackLand.ts +223 -0
  66. package/extension/pi/v1/delivery/stackRecover.ts +265 -0
  67. package/extension/pi/v1/delivery/stackStatus.ts +237 -0
  68. package/extension/pi/v1/delivery/stackSync.ts +658 -0
  69. package/extension/pi/v1/delivery/submit.ts +389 -0
  70. package/extension/pi/v1/delivery/submitConflict.ts +186 -0
  71. package/extension/pi/v1/draftReview.ts +431 -0
  72. package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
  73. package/extension/pi/v1/gist.ts +794 -0
  74. package/extension/pi/v1/learning/audit.ts +186 -0
  75. package/extension/pi/v1/learning/dream.ts +207 -0
  76. package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
  77. package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
  78. package/extension/pi/v1/learning/learn.ts +585 -0
  79. package/extension/pi/v1/lifecycleGates.ts +127 -0
  80. package/extension/{factories → pi/v1}/objective.ts +53 -33
  81. package/extension/pi/v1/objectiveAuthoring.ts +672 -0
  82. package/extension/pi/v1/objectiveDreamGate.ts +160 -0
  83. package/extension/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
  84. package/extension/pi/v1/objectiveRefinement.ts +1320 -0
  85. package/extension/pi/v1/objectiveReview.ts +451 -0
  86. package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
  87. package/extension/pi/v1/plan.ts +812 -0
  88. package/extension/pi/v1/planReview.ts +820 -0
  89. package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
  90. package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
  91. package/extension/pi/v1/providers/plannotator.ts +487 -0
  92. package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +73 -27
  93. package/extension/pi/v1/providers/selection.ts +43 -0
  94. package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
  95. package/extension/pi/v1/review.ts +538 -0
  96. package/extension/pi/v1/reviewOutcome.ts +9 -0
  97. package/extension/pi/v1/scoutWave.ts +318 -0
  98. package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
  99. package/extension/session/branchWorkflowSession.ts +60 -0
  100. package/extension/session/lifecycle.ts +644 -0
  101. package/extension/session/lifecycleGates.ts +64 -0
  102. package/extension/session/saveDestination.ts +87 -0
  103. package/extension/session/workflowSession.ts +971 -0
  104. package/extension/substrate/agentScratch.ts +27 -54
  105. package/extension/substrate/bashScanTimeout.ts +181 -0
  106. package/extension/substrate/bindingDelivery.ts +38 -30
  107. package/extension/substrate/bindings.ts +4 -5
  108. package/extension/substrate/cache.ts +64 -12
  109. package/extension/substrate/childRestrictions.ts +39 -0
  110. package/extension/substrate/coldDoor.ts +17 -1
  111. package/extension/substrate/config.ts +157 -21
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/prompts.ts +22 -0
  115. package/extension/substrate/registry.ts +2 -0
  116. package/extension/substrate/resolverLease.ts +5 -4
  117. package/extension/substrate/sessionData.ts +85 -152
  118. package/extension/substrate/toolGating.ts +263 -84
  119. package/extension/substrate/unifiedDiff.ts +1 -1
  120. package/extension/substrate/workflowState.ts +178 -163
  121. package/extension/substrate/worktreeResolverLock.ts +261 -0
  122. package/extension/surfaces/surfaces.ts +79 -27
  123. package/extension/waves/adversarialReviewWave.ts +87 -46
  124. package/extension/waves/blockedReports.ts +59 -0
  125. package/extension/waves/draftReviewWave.ts +42 -42
  126. package/extension/waves/laneIdentity.ts +77 -0
  127. package/extension/waves/objectiveExplorerWave.ts +24 -24
  128. package/extension/waves/prReviewWave.ts +89 -77
  129. package/extension/waves/reportWave.ts +438 -578
  130. package/extension/waves/reviewClassifierWave.ts +22 -22
  131. package/extension/waves/rpcAdapter.ts +100 -15
  132. package/extension/waves/scoutWave.ts +192 -0
  133. package/extension/waves/transport.ts +480 -0
  134. package/extension/worker/sdkAdapter.ts +494 -0
  135. package/extension/worker/stageExecution.ts +679 -0
  136. package/extension/workerMain.ts +18 -19
  137. package/package.json +6 -4
  138. package/prompts/_fixtures/live.yaml +43 -18
  139. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  140. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  142. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  143. package/prompts/contexts/objective-refinement.md +17 -0
  144. package/prompts/contexts/read-only.md +1 -1
  145. package/prompts/stages/conflict-resolution-continuation.md +9 -6
  146. package/prompts/stages/conflict-resolution.md +4 -4
  147. package/prompts/stages/objective-plan/guidance.md +2 -2
  148. package/prompts/stages/objective-plan/seed.md +9 -1
  149. package/prompts/stages/objective-reconcile-ready.md +1 -1
  150. package/prompts/stages/objective-reconcile.md +1 -1
  151. package/prompts/stages/objective-refine/seed.md +18 -0
  152. package/prompts/stages/objective-review-browser.md +4 -4
  153. package/prompts/stages/objective-sync.md +1 -1
  154. package/prompts/stages/plan-review-browser.md +4 -4
  155. package/prompts/stages/pr-review-browser/active.md +3 -4
  156. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  157. package/prompts/stages/pr-review-terminal/active.md +3 -3
  158. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  159. package/prompts/stages/pr-review.md +3 -3
  160. package/prompts/stages/stack-review-browser/stack.md +5 -6
  161. package/shared/README.md +8 -0
  162. package/shared/bindings.yaml +3 -3
  163. package/shared/contracts.md +2601 -506
  164. package/shared/fixtures/issues-table.json +130 -0
  165. package/shared/registry.yaml +13 -0
  166. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  167. package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
  168. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  169. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  170. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  171. package/extension/doors/auditWaveTools.ts +0 -352
  172. package/extension/doors/ciExecutor.ts +0 -756
  173. package/extension/doors/commitCompact.ts +0 -251
  174. package/extension/doors/dreamWaveTools.ts +0 -489
  175. package/extension/doors/learn.ts +0 -668
  176. package/extension/doors/lifecycleGates.ts +0 -207
  177. package/extension/doors/objectiveStack.ts +0 -1543
  178. package/extension/doors/prReviewDynamic.ts +0 -276
  179. package/extension/doors/ready.ts +0 -279
  180. package/extension/doors/submit.ts +0 -373
  181. package/extension/doors/submitPrReview.ts +0 -505
  182. package/extension/factories/gistAuthor.ts +0 -94
  183. package/extension/factories/gistDraft.ts +0 -265
  184. package/extension/factories/gistSave.ts +0 -251
  185. package/extension/factories/implementHere.ts +0 -116
  186. package/extension/factories/objectiveAuthor.ts +0 -98
  187. package/extension/factories/objectiveDraft.ts +0 -466
  188. package/extension/factories/objectiveSave.ts +0 -366
  189. package/extension/factories/planDraft.ts +0 -140
  190. package/extension/factories/planMode.ts +0 -205
  191. package/extension/factories/planReview.ts +0 -1237
  192. package/extension/factories/planSave.ts +0 -604
  193. package/extension/factories/planTitle.ts +0 -141
  194. package/extension/substrate/structuredOutput.ts +0 -202
  195. package/extension/waves/auditWave.ts +0 -312
  196. package/extension/waves/harvestWave.ts +0 -399
  197. package/extension/waves/learnWave.ts +0 -155
  198. package/extension/waves/memoryAdapter.ts +0 -139
  199. package/extension/waves/prReviewDynamicWave.ts +0 -777
  200. package/extension/worker/readOnlySession.ts +0 -294
  201. package/extension/worker/worker.ts +0 -899
  202. package/prompts/stages/pr-review-dynamic.md +0 -7
@@ -0,0 +1,196 @@
1
+ {
2
+ "$defs": {
3
+ "DiffSource": {
4
+ "enum": [
5
+ "github",
6
+ "local-git"
7
+ ],
8
+ "type": "string"
9
+ },
10
+ "StackContextMemberOut": {
11
+ "additionalProperties": false,
12
+ "description": "One ``stack[]`` per-member section (bottom\u2192top order); text fields are file references\nunder ``context_dir/stack/<pr>/``.",
13
+ "properties": {
14
+ "pr": {
15
+ "title": "Pr",
16
+ "type": "integer"
17
+ },
18
+ "base_ref": {
19
+ "title": "Base Ref",
20
+ "type": "string"
21
+ },
22
+ "head_ref": {
23
+ "title": "Head Ref",
24
+ "type": "string"
25
+ },
26
+ "title": {
27
+ "title": "Title",
28
+ "type": "string"
29
+ },
30
+ "body": {
31
+ "$ref": "#/$defs/TextFileRefOut"
32
+ },
33
+ "diff": {
34
+ "$ref": "#/$defs/TextFileRefOut"
35
+ },
36
+ "plan_body": {
37
+ "anyOf": [
38
+ {
39
+ "$ref": "#/$defs/TextFileRefOut"
40
+ },
41
+ {
42
+ "type": "null"
43
+ }
44
+ ]
45
+ },
46
+ "diff_source": {
47
+ "$ref": "#/$defs/DiffSource"
48
+ }
49
+ },
50
+ "required": [
51
+ "pr",
52
+ "base_ref",
53
+ "head_ref",
54
+ "title",
55
+ "body",
56
+ "diff",
57
+ "plan_body",
58
+ "diff_source"
59
+ ],
60
+ "title": "StackContextMemberOut",
61
+ "type": "object"
62
+ },
63
+ "TextFileRefOut": {
64
+ "additionalProperties": false,
65
+ "description": "One materialized text section: its absolute ``path`` plus the sizes a reviewer pages\nagainst \u2014 ``max_line_bytes`` is the longest line's UTF-8 length (compared against Pi's\nper-line ``read`` bound to pick the byte-slice fallback).",
66
+ "properties": {
67
+ "path": {
68
+ "title": "Path",
69
+ "type": "string"
70
+ },
71
+ "bytes": {
72
+ "title": "Bytes",
73
+ "type": "integer"
74
+ },
75
+ "lines": {
76
+ "title": "Lines",
77
+ "type": "integer"
78
+ },
79
+ "max_line_bytes": {
80
+ "title": "Max Line Bytes",
81
+ "type": "integer"
82
+ }
83
+ },
84
+ "required": [
85
+ "path",
86
+ "bytes",
87
+ "lines",
88
+ "max_line_bytes"
89
+ ],
90
+ "title": "TextFileRefOut",
91
+ "type": "object"
92
+ }
93
+ },
94
+ "additionalProperties": false,
95
+ "description": "The ``--stack`` envelope: the single-PR fields (describing the top PR) plus the\nadditive per-member sections and the combined-diff reference. A separate model so\nnon-stack calls stay byte-identical (no null stack keys).",
96
+ "properties": {
97
+ "success": {
98
+ "title": "Success",
99
+ "type": "boolean"
100
+ },
101
+ "error_type": {
102
+ "anyOf": [
103
+ {
104
+ "type": "string"
105
+ },
106
+ {
107
+ "type": "null"
108
+ }
109
+ ],
110
+ "title": "Error Type"
111
+ },
112
+ "message": {
113
+ "anyOf": [
114
+ {
115
+ "type": "string"
116
+ },
117
+ {
118
+ "type": "null"
119
+ }
120
+ ],
121
+ "title": "Message"
122
+ },
123
+ "branch": {
124
+ "title": "Branch",
125
+ "type": "string"
126
+ },
127
+ "pr": {
128
+ "title": "Pr",
129
+ "type": "integer"
130
+ },
131
+ "base_ref": {
132
+ "title": "Base Ref",
133
+ "type": "string"
134
+ },
135
+ "head_ref": {
136
+ "title": "Head Ref",
137
+ "type": "string"
138
+ },
139
+ "title": {
140
+ "title": "Title",
141
+ "type": "string"
142
+ },
143
+ "context_dir": {
144
+ "title": "Context Dir",
145
+ "type": "string"
146
+ },
147
+ "body": {
148
+ "$ref": "#/$defs/TextFileRefOut"
149
+ },
150
+ "diff": {
151
+ "$ref": "#/$defs/TextFileRefOut"
152
+ },
153
+ "plan_body": {
154
+ "anyOf": [
155
+ {
156
+ "$ref": "#/$defs/TextFileRefOut"
157
+ },
158
+ {
159
+ "type": "null"
160
+ }
161
+ ]
162
+ },
163
+ "diff_source": {
164
+ "$ref": "#/$defs/DiffSource"
165
+ },
166
+ "stack": {
167
+ "items": {
168
+ "$ref": "#/$defs/StackContextMemberOut"
169
+ },
170
+ "title": "Stack",
171
+ "type": "array"
172
+ },
173
+ "combined_diff": {
174
+ "$ref": "#/$defs/TextFileRefOut"
175
+ }
176
+ },
177
+ "required": [
178
+ "success",
179
+ "error_type",
180
+ "message",
181
+ "branch",
182
+ "pr",
183
+ "base_ref",
184
+ "head_ref",
185
+ "title",
186
+ "context_dir",
187
+ "body",
188
+ "diff",
189
+ "plan_body",
190
+ "diff_source",
191
+ "stack",
192
+ "combined_diff"
193
+ ],
194
+ "title": "PrReviewStackContextOut",
195
+ "type": "object"
196
+ }
@@ -1,362 +0,0 @@
1
- // The SECOND 3rd-party plan adapter — and the first with the AUGMENT posture. A perk-owned shim
2
- // that enables `@plannotator/pi-extension` as a REAL, selectable plan provider: unlike the tombell
3
- // adapter (REPLACE posture — perk's plan surface fully vacates), plannotator AUGMENTS perk's plan
4
- // flow. perk's `/plan` mode, authoring injection, and read-only gate STAY (planMode skips only the
5
- // `--plan` flag + `Ctrl+Alt+P` shortcut — the two real registration collisions).
6
- //
7
- // INJECTION + BRIDGE ONLY: the `plan_review` TOOL lives in `extension/factories/planReview.ts`
8
- // (perk's backend-neutral review door); this module is the injection-only adapter shape. It owns
9
- // (1) the plannotator review-step authoring context (injected while the gate is active AND
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 `objective-save` (both objective stages route to the objective
13
- // review arm), or the gist flavor when the stage is `gist-author`) and (2) the pure
14
- // event-bus bridge
15
- // (`requestPlannotatorPlanReview`; `createPlannotatorBridge` is its thin structural wrapper)
16
- // that planReview.ts dispatches to when plannotator is the selected plan provider and the
17
- // plan-review browser open (plannotatorHandoff.ts) launches. The bridge speaks plannotator's
18
- // published `plannotator:request` event API (in-process `pi.events` bus); the decision wait is
19
- // a per-review `plannotator:review-result` listener disposed via the unsubscribe pi's
20
- // `EventBus.on` returns.
21
- //
22
- // INERT BY DEFAULT. The shim is ALWAYS registered in index.ts but the injection fires only when
23
- // the resolved `[providers] plan` selection is `plannotator-plan` (read fresh per-event, same
24
- // shape as planMode/planAdapterTombell). On any other selection the context handler only strips
25
- // its own stale marker — zero behavior change on the default path.
26
- //
27
- // INVARIANTS HELD: never calls `setActiveTools`, never registers a `tool_call` handler, never
28
- // restamps `cache.plan-ref.provider` (stays `"github"`). The adapter is INJECTION-ONLY again
29
- // (Invariant 1: composes, never owns) — the review tool, the `approvalSave` composition, and the
30
- // gate exit all live behind planReview.ts's seams; the injection's gate-active check reads the
31
- // persisted `perk:workflow-state.mode`, the gate's own state twin.
32
- //
33
- // EVENT ENVELOPE (pinned against `@plannotator/pi-extension@0.20.0`, `plannotator-events.ts` —
34
- // verified unchanged through 0.26.1):
35
- // request — pi.events.emit("plannotator:request", { requestId, action: "plan-review",
36
- // payload: { planContent, origin? }, respond }) // respond = in-payload callback
37
- // handshake — respond({ status: "handled", result: { status: "pending", reviewId } })
38
- // | respond({ status: "unavailable", error? }) | respond({ status: "error", error })
39
- // decision — pi.events.on("plannotator:review-result", { reviewId, approved, feedback?, ... })
40
- //
41
- // DIRECT EDITS FEEDBACK FORMAT (pinned against plannotator `packages/editor/directEdits.ts`,
42
- // `buildDirectEditsSection` / `composeFeedbackWithDirectEdits`, at v0.26.1). The browser's
43
- // direct-edit mode arrives as PROSE inside the existing `feedback` string, never a new envelope
44
- // field: `# Direct Edits\n` + blank line + a one-sentence preamble (two wording variants — never
45
- // couple to it) + blank line + a ```diff fence containing
46
- // `createTwoFilesPatch('plan.md (original)', 'plan.md (edited)', base, edited, undefined,
47
- // undefined, { context: 3 }).trimEnd()` against the exact bytes perk submitted. The section is
48
- // composed FIRST; non-sentinel annotation feedback follows after `\n\n---\n\n`; edits-only
49
- // feedback is just the section. `extractDirectEdits` below parses it strictly (fail-open — a
50
- // null degrades to today's verbatim behavior).
51
-
52
- import { randomUUID } from "node:crypto";
53
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
54
- import { GIST_AUTHOR_STAGE } from "../factories/gistAuthor.ts";
55
- import { OBJECTIVE_AUTHOR_STAGE } from "../factories/objectiveAuthor.ts";
56
- import { OBJECTIVE_SAVE_STAGE } from "../factories/objectiveSave.ts";
57
- import { resolvedPlanProviderId } from "../factories/planMode.ts";
58
- // Type-only (erased at runtime — no cycle): the outcome vocabulary lives with the review door.
59
- import type { ReviewOutcome } from "../factories/planReview.ts";
60
- import { render } from "../substrate/prompts.ts";
61
- import { PLANNOTATOR_PLAN_PROVIDER_ID } from "../substrate/providers.ts";
62
- import { branchCarries, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
63
-
64
- /** The plannotator plan-adapter bridge customType (distinct from planMode's `perk:plan-context`). */
65
- export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE = "perk:plan-adapter-plannotator";
66
- const PLAN_ADAPTER_PLANNOTATOR_MARKER = "[PLAN ADAPTER: PLANNOTATOR]";
67
- const OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER = "[OBJECTIVE ADAPTER: PLANNOTATOR]";
68
- const GIST_ADAPTER_PLANNOTATOR_MARKER = "[GIST ADAPTER: PLANNOTATOR]";
69
-
70
- /**
71
- * The handshake timeout for plannotator's immediate `respond` callback (mirrors plannotator's own
72
- * `PLANNOTATOR_TIMEOUT_MS = 5_000`). Overridable for tests via PERK_PLANNOTATOR_HANDSHAKE_MS.
73
- */
74
- export const PLANNOTATOR_HANDSHAKE_TIMEOUT_MS = 5_000;
75
-
76
- function handshakeTimeoutMs(): number {
77
- const raw = Number(process.env.PERK_PLANNOTATOR_HANDSHAKE_MS ?? "");
78
- return Number.isFinite(raw) && raw > 0 ? raw : PLANNOTATOR_HANDSHAKE_TIMEOUT_MS;
79
- }
80
-
81
- /**
82
- * The augment-posture bridge prompt: perk's plan-authoring discipline plus the plannotator review
83
- * step. Prompting, NOT enforcement (perk's own gate is the read-only authority). Durable anchors
84
- * only — mirrors PLAN_AUTHORING_CONTEXT, which is also injected (perk's plan mode stays).
85
- */
86
- export const PLAN_ADAPTER_PLANNOTATOR_CONTEXT = render("contexts/adapters/plannotator-plan.md", {
87
- marker: PLAN_ADAPTER_PLANNOTATOR_MARKER,
88
- });
89
-
90
- /**
91
- * The objective flavor of the bridge prompt, injected in an objective-authoring session
92
- * (stage `objective-author` or `objective-save`) instead of the plan flavor. An APPROVED review auto-saves the
93
- * objective via the `objectiveApprovalSave` seam; `/objective-save` is the manual failsafe on
94
- * the skipped/unavailable arms.
95
- */
96
- export const OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT = render(
97
- "contexts/adapters/plannotator-objective.md",
98
- { marker: OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER },
99
- );
100
-
101
- /**
102
- * The gist flavor of the bridge prompt, injected in a `gist-author` session instead of the
103
- * plan/objective flavors. The review surface renders the gist draft (title + scope line +
104
- * prose); an approval carrying `# Direct Edits` does NOT auto-save — the model folds the diff
105
- * into the matching `gist_draft` fields and re-reviews (contracts.md §8.23's gist arm).
106
- */
107
- export const GIST_ADAPTER_PLANNOTATOR_CONTEXT = render("contexts/adapters/plannotator-gist.md", {
108
- marker: GIST_ADAPTER_PLANNOTATOR_MARKER,
109
- });
110
-
111
- /** Whether the foreign `plannotator-plan` provider is the selected plan provider for `cwd`. */
112
- export function isPlannotatorPlanSelected(cwd: string): boolean {
113
- return resolvedPlanProviderId(cwd) === PLANNOTATOR_PLAN_PROVIDER_ID;
114
- }
115
-
116
- // ------------------------------------------------------------------ the event-bus bridge core
117
-
118
- /** The minimal `pi.events` surface the bridge needs (mirrors pi's EventBus, whose `on` returns an unsubscribe function). */
119
- export interface PlannotatorBus {
120
- emit(channel: string, data: unknown): void;
121
- on(channel: string, handler: (data: unknown) => void): () => void;
122
- }
123
-
124
- /** Plannotator's immediate `respond` handshake payload (pinned envelope, see header). */
125
- interface HandshakeResponse {
126
- status?: string;
127
- error?: string;
128
- result?: { status?: string; reviewId?: string };
129
- }
130
-
131
- /** The human decision arriving on `plannotator:review-result`. */
132
- interface ReviewDecision {
133
- approved: boolean;
134
- feedback?: string;
135
- }
136
-
137
- /**
138
- * The pure, offline-testable plan-review bridge (the ergonomic mirror of
139
- * `requestPlannotatorCodeReview` in plannotatorHandoff.ts): emit ONE `plannotator:request` with
140
- * `action: "plan-review"`, await the bounded `respond` handshake, then await the human decision
141
- * on a PER-REVIEW `plannotator:review-result` listener — filtered on the handshake's `reviewId`
142
- * and disposed via the unsubscribe `bus.on` returns when the decision arrives or the turn
143
- * aborts. Pure over the bus → unit-testable offline with a fake plannotator listener.
144
- */
145
- export async function requestPlannotatorPlanReview(
146
- bus: PlannotatorBus,
147
- plan: string,
148
- signal?: AbortSignal,
149
- ): Promise<ReviewOutcome> {
150
- if (signal?.aborted) return { status: "aborted" };
151
-
152
- // 1. Emit the request and await the immediate `respond` handshake (bounded — fail-open).
153
- const requestId = randomUUID();
154
- let respondResolve: (response: HandshakeResponse) => void = () => {};
155
- const handshake = new Promise<HandshakeResponse | "timeout">((resolve) => {
156
- respondResolve = resolve;
157
- });
158
- const timer = setTimeout(() => respondResolve("timeout" as never), handshakeTimeoutMs());
159
- bus.emit("plannotator:request", {
160
- requestId,
161
- action: "plan-review",
162
- payload: { planContent: plan, origin: "perk" },
163
- respond: (response: unknown) => respondResolve(response as HandshakeResponse),
164
- });
165
- const response = await handshake;
166
- clearTimeout(timer);
167
-
168
- if (response === "timeout") {
169
- return {
170
- status: "unavailable",
171
- warning: "plannotator did not respond to the review request (handshake timeout)",
172
- };
173
- }
174
- if (response?.status !== "handled") {
175
- const detail = response?.error ? `: ${response.error}` : "";
176
- return {
177
- status: "unavailable",
178
- warning: `plannotator reported ${response?.status ?? "an invalid response"}${detail}`,
179
- };
180
- }
181
- const reviewId = response.result?.reviewId;
182
- if (response.result?.status !== "pending" || typeof reviewId !== "string") {
183
- return {
184
- status: "unavailable",
185
- warning: "plannotator handshake returned no pending reviewId",
186
- };
187
- }
188
-
189
- // A turn aborted DURING the handshake wait must not wedge: `addEventListener("abort", …)` on
190
- // an already-aborted signal never fires, so re-check before registering the decision wait.
191
- if (signal?.aborted) return { status: "aborted" };
192
-
193
- // 2. Await the human decision (no timeout — the reviewer takes as long as they take), but
194
- // honor a turn abort so an interrupted session never leaks a wedged promise. Either exit
195
- // disposes the result listener via the unsubscribe.
196
- return await new Promise<ReviewOutcome>((resolve) => {
197
- let settled = false;
198
- const finish = (outcome: ReviewOutcome): void => {
199
- if (settled) return;
200
- settled = true;
201
- unsubscribe();
202
- signal?.removeEventListener("abort", onAbort);
203
- resolve(outcome);
204
- };
205
- const onAbort = (): void => finish({ status: "aborted" });
206
- const unsubscribe = bus.on("plannotator:review-result", (data) => {
207
- const d = data as { reviewId?: unknown; approved?: unknown; feedback?: unknown };
208
- if (d?.reviewId !== reviewId) return;
209
- const decision: ReviewDecision = {
210
- approved: d.approved === true,
211
- feedback: typeof d.feedback === "string" && d.feedback.trim() ? d.feedback : undefined,
212
- };
213
- finish({ status: "completed", reviewId, ...decision });
214
- });
215
- signal?.addEventListener("abort", onAbort, { once: true });
216
- });
217
- }
218
-
219
- /**
220
- * Create the plannotator bridge over an event bus — the thin structural slice
221
- * (`{ review(plan, signal) }`) that `registerPlanReview` injects into the review door; the body
222
- * lives in `requestPlannotatorPlanReview`.
223
- */
224
- export function createPlannotatorBridge(bus: PlannotatorBus): {
225
- review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome>;
226
- } {
227
- return { review: (plan, signal) => requestPlannotatorPlanReview(bus, plan, signal) };
228
- }
229
-
230
- // ------------------------------------------------------------------ Direct Edits extraction
231
-
232
- const DIRECT_EDITS_HEADING = "# Direct Edits";
233
- const DIFF_FENCE_OPEN = "```diff\n";
234
- const REMAINDER_SEPARATOR = "\n\n---\n\n";
235
-
236
- /**
237
- * Whether `feedback` OPENS with the Direct Edits heading (plan-review feedback composes the
238
- * section first — a heading anywhere else is quoted prose, not a section). Callers pair this
239
- * with `extractDirectEdits`: heading present but extraction null means the section was seen but
240
- * could not be honored (the fail-open ladder's loud-warning arm).
241
- */
242
- export function hasDirectEditsHeading(feedback: string): boolean {
243
- return feedback === DIRECT_EDITS_HEADING || feedback.startsWith(`${DIRECT_EDITS_HEADING}\n`);
244
- }
245
-
246
- /**
247
- * Strictly extract the Direct Edits unified diff from a plannotator review-result `feedback`
248
- * string (the format pin lives in the module header). Returns the fence body as `diff` plus the
249
- * annotation `remainder` after the section (one leading `\n\n---\n\n` separator stripped;
250
- * `undefined` when blank). Null means "no extractable Direct Edits section" — both the
251
- * no-section case AND a present-heading-but-unparseable body (callers distinguish the two via
252
- * `hasDirectEditsHeading`). The preamble prose between the heading and the fence is skipped
253
- * without inspecting its wording (plannotator ships two variants).
254
- */
255
- export function extractDirectEdits(feedback: string): { diff: string; remainder?: string } | null {
256
- if (!hasDirectEditsHeading(feedback)) return null;
257
- const openIdx = feedback.indexOf(`\n${DIFF_FENCE_OPEN}`, DIRECT_EDITS_HEADING.length);
258
- if (openIdx === -1) return null;
259
- const bodyStart = openIdx + 1 + DIFF_FENCE_OPEN.length;
260
- // The closing fence is the first line that is exactly ``` — unambiguous inside the body,
261
- // because every diff body line carries a prefix char (` `/`-`/`+`/`\`/`@`), so no body line
262
- // can start with a backtick.
263
- let close = -1;
264
- let searchFrom = bodyStart;
265
- while (close === -1) {
266
- const idx = feedback.indexOf("\n```", searchFrom);
267
- if (idx === -1) return null;
268
- const after = feedback[idx + 4];
269
- if (after === undefined || after === "\n") {
270
- close = idx;
271
- } else {
272
- searchFrom = idx + 4;
273
- }
274
- }
275
- const diff = feedback.slice(bodyStart, close);
276
- if (diff.trim() === "") return null;
277
- let rest = feedback.slice(close + 4);
278
- if (rest.startsWith(REMAINDER_SEPARATOR)) rest = rest.slice(REMAINDER_SEPARATOR.length);
279
- return { diff, remainder: rest.trim() === "" ? undefined : rest };
280
- }
281
-
282
- // ----------------------------------------------------------------------------- registration
283
-
284
- /**
285
- * Register the plannotator plan adapter: the augment-posture authoring-context injection, inert
286
- * unless `[providers] plan = "plannotator-plan"`. INJECTION-ONLY (Invariant 1: composes, never
287
- * owns) — the `plan_review` tool lives in planReview.ts (the backend-neutral review door), which
288
- * dispatches to this module's bridge when plannotator is selected; the adapter itself never
289
- * arbitrates tools and needs no gating.
290
- */
291
- export function registerPlanAdapterPlannotator(pi: ExtensionAPI): void {
292
- // Inject the bridge context while the read-only gate is active AND plannotator is selected.
293
- // Three content flavors, one customType: an objective-authoring session (also read-only —
294
- // BOTH objective stages: `plan_review` routes objective-author AND objective-save to the
295
- // objective review arm) gets the objective flavor (the review surface renders the objective
296
- // draft), a gist-author session gets the gist flavor (the rendered gist draft); any other
297
- // gated stage gets the plan flavor. The gate-active check reads the persisted
298
- // `perk:workflow-state.mode` (the gate's state twin) — never the gate itself.
299
- pi.on("before_agent_start", async (_event, ctx) => {
300
- if (!isPlannotatorPlanSelected(ctx.cwd)) return;
301
- const branch = branchOf(ctx);
302
- const state = rebuildWorkflowState(branch);
303
- if (state.mode !== "read-only") return;
304
- const flavor =
305
- state.stage === OBJECTIVE_AUTHOR_STAGE || state.stage === OBJECTIVE_SAVE_STAGE
306
- ? "objective"
307
- : state.stage === GIST_AUTHOR_STAGE
308
- ? "gist"
309
- : "plan";
310
- const content =
311
- flavor === "objective"
312
- ? OBJECTIVE_ADAPTER_PLANNOTATOR_CONTEXT
313
- : flavor === "gist"
314
- ? GIST_ADAPTER_PLANNOTATOR_CONTEXT
315
- : PLAN_ADAPTER_PLANNOTATOR_CONTEXT;
316
- // Once-only PER FLAVOR: the dedup key is the flavor's marker (not the shared customType), so
317
- // a stage change still delivers the missing flavor while a prior copy of another flavor
318
- // sits on the branch. Injected customs persist, so a live copy suppresses re-injection;
319
- // compaction dropping it makes the scan come up clean and the next turn re-injects.
320
- const marker =
321
- flavor === "objective"
322
- ? OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER
323
- : flavor === "gist"
324
- ? GIST_ADAPTER_PLANNOTATOR_MARKER
325
- : PLAN_ADAPTER_PLANNOTATOR_MARKER;
326
- if (branchCarries(branch, marker)) return;
327
- return {
328
- message: {
329
- customType: PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE,
330
- content,
331
- display: false,
332
- },
333
- };
334
- });
335
-
336
- // Strip the stale bridge markers (ALL THREE flavors) from context when plannotator-plan is no
337
- // longer selected (same hygiene as the tombell shim), so they never linger across a deselect.
338
- const hasMarker = (text: string): boolean =>
339
- text.includes(PLAN_ADAPTER_PLANNOTATOR_MARKER) ||
340
- text.includes(OBJECTIVE_ADAPTER_PLANNOTATOR_MARKER) ||
341
- text.includes(GIST_ADAPTER_PLANNOTATOR_MARKER);
342
- pi.on("context", async (event, ctx) => {
343
- if (isPlannotatorPlanSelected(ctx.cwd)) return;
344
- return {
345
- messages: event.messages.filter((m) => {
346
- const msg = m as { customType?: string; role?: string; content?: unknown };
347
- if (msg.customType === PLAN_ADAPTER_PLANNOTATOR_CONTEXT_TYPE) return false;
348
- if (msg.role !== "user") return true;
349
- const content = msg.content;
350
- if (typeof content === "string") return !hasMarker(content);
351
- if (Array.isArray(content)) {
352
- return !content.some(
353
- (c) =>
354
- (c as { type?: string; text?: string }).type === "text" &&
355
- hasMarker((c as { text?: string }).text ?? ""),
356
- );
357
- }
358
- return true;
359
- }),
360
- };
361
- });
362
- }