@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
@@ -1,80 +1,101 @@
1
- // perk Pi extension — the session *interior*.
1
+ // perk Pi extension — the session *interior*: the composition root.
2
2
  //
3
- // The tier-3 session-state mechanics (contracts.md §8.2/§8.3): claim PERK_RUN_ID on
3
+ // The tier-3 session-state mechanics (contracts.md §8.2/§8.3) claim PERK_RUN_ID on
4
4
  // `session_start` (verified-linkage), rebuild `perk:workflow-state` on `session_start` AND
5
- // `session_tree` (per-field LWW), and derive a child run_id on fork.
5
+ // `session_tree` (per-field LWW), derive a child run_id on fork, reconcile the stage-gated plan
6
+ // linkage — are OWNED by `session/lifecycle.ts` (identity arms + the two-phase startup facts +
7
+ // the navigation facts). This file binds the production ports and keeps the Pi effects visibly
8
+ // ORDERED: gate sync from the pure scope slice → claimed-only refinement import → the post-gate
9
+ // facts → implementation pointer capture → feedback receiver sync → presentation/probe tail.
6
10
 
7
11
  import { existsSync, mkdirSync } from "node:fs";
8
12
  import { basename, join } from "node:path";
9
13
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
- import { registerPlanAdapterPlannotator } from "./adapters/planAdapterPlannotator.ts";
11
- import { registerPlanAdapterTombell } from "./adapters/planAdapterTombell.ts";
12
- import { registerAddress } from "./doors/address.ts";
13
- import { registerAnnotationPushTool } from "./doors/annotationPush.ts";
14
- import { registerAuditWave } from "./doors/auditWaveTools.ts";
15
- import { registerCiExecutor } from "./doors/ciExecutor.ts";
16
- import { registerCommitAndCompact } from "./doors/commitCompact.ts";
17
- import { registerDraftReviewWaveTools } from "./doors/draftReviewWaveTools.ts";
18
- import { registerDreamWave } from "./doors/dreamWaveTools.ts";
19
- import { registerHarvestWave } from "./doors/harvestWaveTools.ts";
20
- import { registerLand } from "./doors/land.ts";
21
- import { registerLearn } from "./doors/learn.ts";
22
- import { CODE_DOOR, DOCS_DOOR, registerLearnFactoryDoor } from "./doors/learnFactory.ts";
23
- import { registerLifecycleGates } from "./doors/lifecycleGates.ts";
14
+ import { createDraftReviewWaveState } from "./authoring/review/draftContext.ts";
15
+ import { createHunkFeedbackReceiver, type HunkFeedbackReceiver } from "./hunkFeedback/receiver.ts";
16
+ import { registerBashScanTimeout } from "./pi/v1/bashScanTimeout.ts";
17
+ import { installAutomatedReviewBindings } from "./pi/v1/codeReview/automated.ts";
18
+ import { installPrReviewBrowserBindings } from "./pi/v1/codeReview/browser.ts";
19
+ import { installReviewWaveBindings } from "./pi/v1/codeReview/reviewWave.ts";
20
+ import { installStackReviewBindings } from "./pi/v1/codeReview/stack.ts";
21
+ import { installCuratedSubmissionBindings } from "./pi/v1/codeReview/submit.ts";
22
+ import { installPrReviewTerminalBindings } from "./pi/v1/codeReview/terminal.ts";
23
+ import { installAddressBindings } from "./pi/v1/delivery/address.ts";
24
+ import { installCiBindings } from "./pi/v1/delivery/ci.ts";
25
+ import { installCommitCompactBindings } from "./pi/v1/delivery/commitCompact.ts";
26
+ import {
27
+ type ConflictResolverEngineOptions,
28
+ createConflictResolverEngine,
29
+ } from "./pi/v1/delivery/conflictResolverEngine.ts";
30
+ import { installLandBindings } from "./pi/v1/delivery/land.ts";
31
+ import { installReadyBindings } from "./pi/v1/delivery/ready.ts";
32
+ import { createStackConflictResolver } from "./pi/v1/delivery/stackConflictResolver.ts";
33
+ import { installStackLandBindings } from "./pi/v1/delivery/stackLand.ts";
34
+ import { installStackRecoverBindings } from "./pi/v1/delivery/stackRecover.ts";
35
+ import { installStackStatusBindings } from "./pi/v1/delivery/stackStatus.ts";
36
+ import {
37
+ installStackSyncBindings,
38
+ type StackResolutionDelivery,
39
+ } from "./pi/v1/delivery/stackSync.ts";
40
+ import { installSubmitBindings } from "./pi/v1/delivery/submit.ts";
41
+ import { installSubmitConflictBindings } from "./pi/v1/delivery/submitConflict.ts";
42
+ import { createDraftReviewSlot } from "./pi/v1/draftReview.ts";
43
+ import { registerDraftReviewWaveTools } from "./pi/v1/draftReviewWaveTools.ts";
44
+ import { installGistBindings } from "./pi/v1/gist.ts";
45
+ import { installAuditBindings } from "./pi/v1/learning/audit.ts";
46
+ import { installDreamBindings } from "./pi/v1/learning/dream.ts";
47
+ import { installLearnFactoryBindings } from "./pi/v1/learning/factory.ts";
48
+ import { installHarvestBindings } from "./pi/v1/learning/harvest.ts";
49
+ import { installLearnBindings } from "./pi/v1/learning/learn.ts";
50
+ import { registerLifecycleGates } from "./pi/v1/lifecycleGates.ts";
51
+ import { installObjectiveBindings } from "./pi/v1/objective.ts";
52
+ import { installObjectiveAuthoringBindings } from "./pi/v1/objectiveAuthoring.ts";
53
+ import { installObjectivePlanningBindings } from "./pi/v1/objectivePlanning.ts";
54
+ import {
55
+ importRefinementContextOnClaim,
56
+ installObjectiveRefinementBindings,
57
+ } from "./pi/v1/objectiveRefinement.ts";
24
58
  import {
25
59
  openObjectiveReviewSurface,
26
60
  registerObjectiveReviewBrowser,
27
- } from "./doors/objectiveReviewBrowser.ts";
28
- import { registerObjectiveStack } from "./doors/objectiveStack.ts";
29
- import { plannotatorPresent } from "./doors/plannotatorHandoff.ts";
30
- import { openPlanReviewSurface, registerPlanReviewBrowser } from "./doors/planReviewBrowser.ts";
31
- import { registerPrReview } from "./doors/prReview.ts";
32
- import { registerPrReviewBrowser } from "./doors/prReviewBrowser.ts";
33
- import { registerPrReviewDynamic } from "./doors/prReviewDynamic.ts";
34
- import { registerPrReviewTerminal } from "./doors/prReviewTerminal.ts";
35
- import { registerReady } from "./doors/ready.ts";
36
- import { registerReviewWaveTools } from "./doors/reviewWaveTools.ts";
37
- import { registerSelfcheck } from "./doors/selfcheck.ts";
38
- import { registerOpenStackReview, registerStackReviewBrowser } from "./doors/stackReviewBrowser.ts";
39
- import { registerSubmit } from "./doors/submit.ts";
40
- import { registerSubmitPrReview } from "./doors/submitPrReview.ts";
41
- import { registerGistAuthor } from "./factories/gistAuthor.ts";
42
- import { registerGistDraft } from "./factories/gistDraft.ts";
43
- import { registerGistSave } from "./factories/gistSave.ts";
44
- import { registerImplementHere } from "./factories/implementHere.ts";
45
- import { registerObjective } from "./factories/objective.ts";
46
- import { registerObjectiveAuthor } from "./factories/objectiveAuthor.ts";
47
- import { registerObjectiveDraft } from "./factories/objectiveDraft.ts";
48
- import { registerObjectivePlan } from "./factories/objectivePlan.ts";
49
- import { registerObjectiveSave } from "./factories/objectiveSave.ts";
50
- import { registerPlanDraft } from "./factories/planDraft.ts";
51
- import { registerPlanMode } from "./factories/planMode.ts";
52
- import { registerPlanReview } from "./factories/planReview.ts";
53
- import { registerPlanSave } from "./factories/planSave.ts";
54
- import { createHunkFeedbackReceiver } from "./hunkFeedback/receiver.ts";
61
+ } from "./pi/v1/objectiveReviewBrowser.ts";
62
+ import { installPlanBindings } from "./pi/v1/plan.ts";
63
+ import { openPlanReviewSurface, registerPlanReviewBrowser } from "./pi/v1/planReviewBrowser.ts";
64
+ import { createAnnotationState, installAnnotationBindings } from "./pi/v1/providers/annotations.ts";
65
+ import { installPlannotatorPlanAdapter } from "./pi/v1/providers/plannotator.ts";
66
+ import { plannotatorPresent } from "./pi/v1/providers/plannotatorHandoff.ts";
67
+ import { installTombellPlanAdapter } from "./pi/v1/providers/tombell.ts";
68
+ import { installScoutWaveBindings } from "./pi/v1/scoutWave.ts";
69
+ import { registerSelfcheck } from "./pi/v1/selfcheck.ts";
70
+ import {
71
+ branchSessionStateStore,
72
+ establishSessionIdentity,
73
+ reflectSessionReadOnlyFloor,
74
+ resolveSessionStartFacts,
75
+ sessionStartToolScope,
76
+ sessionTreeFacts,
77
+ } from "./session/lifecycle.ts";
55
78
  import { createAgentScratchProvisioner, registerAgentScratch } from "./substrate/agentScratch.ts";
56
79
  import { registerBindingDelivery } from "./substrate/bindingDelivery.ts";
57
80
  import {
58
81
  atomicWriteFileSync,
59
82
  ensureRunScratch,
83
+ listRunIds,
60
84
  markHandoffConsumed,
61
85
  readHandoff,
62
86
  readPlanRef,
63
87
  setMarker,
64
88
  workflowDir,
65
89
  } from "./substrate/cache.ts";
66
- import { loadRegistry, type Registry, stageConsumesPlanRef } from "./substrate/registry.ts";
90
+ import { decodeReadOnlyFloor, isRunnerChild } from "./substrate/childRestrictions.ts";
91
+ import { loadRegistry, type Registry } from "./substrate/registry.ts";
67
92
  import { perkVersion, sharedDir, versionStamp } from "./substrate/resources.ts";
68
93
  import { mintRunId } from "./substrate/runId.ts";
69
94
  import { captureSessionPointer } from "./substrate/sessionPointers.ts";
70
95
  import { registerToolGating } from "./substrate/toolGating.ts";
71
96
  import {
72
- appendWorkflowState,
73
97
  branchOf,
74
- decideClaim,
75
- planRefsEqual,
76
98
  rebuildWorkflowState,
77
- resolveRunStage,
78
99
  WORKFLOW_STATE_TYPE,
79
100
  type WorkflowState,
80
101
  } from "./substrate/workflowState.ts";
@@ -91,6 +112,7 @@ import {
91
112
  } from "./surfaces/surfaces.ts";
92
113
  import { registerBtw } from "./vendor/btw/btw.ts";
93
114
  import { registerWhimsical } from "./vendor/whimsical/whimsical.ts";
115
+ import { createReportWave } from "./waves/reportWave.ts";
94
116
 
95
117
  // Cross-plane proof marker (TS writes via cache.ts; the Python helper reads it — gate check 3).
96
118
  const T3_MARKER = "t3-extension-cache-write";
@@ -124,18 +146,43 @@ function writeT3Sentinel(
124
146
  }
125
147
  }
126
148
 
127
- export default function (pi: ExtensionAPI) {
149
+ export default function perk(
150
+ pi: ExtensionAPI,
151
+ options: {
152
+ resolverEngine?: Pick<ConflictResolverEngineOptions, "configPath" | "acquire">;
153
+ stackResolutionDelivery?: StackResolutionDelivery;
154
+ /**
155
+ * Construction-only: the hunk feedback receiver factory (default `createHunkFeedbackReceiver`).
156
+ * Constructed ONCE per activation exactly like production; the registered-path suites bind a
157
+ * recording receiver to observe the startup/navigation sync order and inputs.
158
+ */
159
+ feedbackReceiverFactory?: (pi: ExtensionAPI) => HunkFeedbackReceiver;
160
+ } = {},
161
+ ) {
128
162
  const version = perkVersion();
129
163
 
130
164
  // The read-only tool-gating primitive. Attaches to perk:workflow-state.mode; synced on
131
165
  // both session_start AND session_tree below. enter/exit are the surface the gated stages consume.
132
- const gating = registerToolGating(pi);
166
+ // The two native-child booleans (§8.3), re-read at every session_start: the runner bit (also the
167
+ // `() => runnerChild` closure every injected authoring/adapter context takes as its fence — the
168
+ // REAL closure, never a constant), and the read-only floor a runner child derives from the
169
+ // report restriction packet — latched for the activation (`||=`) so no later session_start,
170
+ // gate exit, or tree navigation can clear it.
171
+ let runnerChild = false;
172
+ let readOnlyFloor = false;
173
+ const gating = registerToolGating(pi, () => readOnlyFloor);
174
+
175
+ // The bash scan-timeout guard: always on in every perk session — gated or not, runner children
176
+ // included (the slow gitignore-blind scans were observed in read-write sessions too). The gate's
177
+ // `tool_call` hook runs first only because it registers first; a gate block short-circuits
178
+ // before injection matters.
179
+ registerBashScanTimeout(pi);
133
180
 
134
181
  // Run-owned disposable scratch guidance for every eligible write-capable model turn. One
135
182
  // activation-scoped provisioner shares retry/warning suppression with the isolated /btw side
136
183
  // session; no model tool or process-global temp environment is introduced.
137
184
  const agentScratch = createAgentScratchProvisioner();
138
- registerAgentScratch(pi, agentScratch);
185
+ registerAgentScratch(pi, agentScratch, () => !gating.isActive() && !runnerChild);
139
186
 
140
187
  // Vendored `btw`: a `/btw` human-only side-chat popover backed by an isolated in-memory
141
188
  // AgentSession. Takes `gating` for the gate-mirror — its side-session toolset + cache key follow
@@ -148,46 +195,123 @@ export default function (pi: ExtensionAPI) {
148
195
  // turn, via the headless-no-op `setWorkingMessage` surfaces seam. Always on, no config toggle.
149
196
  registerWhimsical(pi);
150
197
 
151
- // perk-owned plan mode: the `/plan` + Ctrl+Alt+P + `--plan` toggle surface over the
152
- // read-only gate, plus the plan-authoring context injection. perk owns plan mode end-to-end now (the
153
- // borrowed `@tombell/pi-plan` is retired).
154
- registerPlanMode(pi, gating);
198
+ // The draft-review pair's per-activation state: ONE instance serves the `plan_review` wave
199
+ // arm (via the closures below), the two draft-review browser doors, and the
200
+ // `start_draft_review_wave`/`collect_draft_review_wave` tool pair — per activation, never per
201
+ // process (two bound sessions in one process share nothing). Plain object construction, so
202
+ // creating it before any registration is order-safe.
203
+ const draftReviewWave = createDraftReviewWaveState();
204
+
205
+ // The annotation-push per-activation state: ONE instance serves the `push_annotations`
206
+ // installer and every priming door — the PR/stack review doors (review mode) and the
207
+ // plan/objective review doors (plan mode) — per activation, never per process (the
208
+ // `draftReviewWave` threading pattern).
209
+ const annotations = createAnnotationState();
210
+
211
+ // The composition root's ONE per-activation report-wave instance (the `draftReviewWave`
212
+ // threading pattern): the wave owns adapter selection (a fresh rpc adapter per launch over
213
+ // pi's event bus) and pending execution (instance-owned refs), and is threaded into every
214
+ // wave-consuming installer — no installer touches the transport tier. Plain construction, no
215
+ // Pi registration, order-safe.
216
+ const reportWave = createReportWave(pi.events);
217
+
218
+ const conflictResolver = createConflictResolverEngine({
219
+ events: pi.events,
220
+ enginePresent: () => pi.getAllTools().some((tool) => tool.name === "subagent"),
221
+ readOnly: () => gating.isActive(),
222
+ authorized: (request) =>
223
+ request.mode === "pr-rebase"
224
+ ? submitConflict.authorized(request)
225
+ : stackConflict.authorized(request),
226
+ ...options.resolverEngine,
227
+ });
228
+ const submitConflict = installSubmitConflictBindings(pi, conflictResolver, () =>
229
+ gating.isActive(),
230
+ );
231
+ const stackConflict = createStackConflictResolver(conflictResolver, () => gating.isActive());
232
+
233
+ // The v1 plan installer: perk-owned plan mode (the `/plan` + Ctrl+Alt+P + `--plan` toggle
234
+ // surface over the read-only gate, plus the plan-authoring context injection — this call
235
+ // sits at the frozen hooks-ordering slot the mode surface always held), the
236
+ // `plan_draft`/`plan_save` tools, the `/plan-save` + `/implement-here` commands, and
237
+ // `plan_review` — perk's UNIVERSAL review door (plannotator-selected → the event-bus bridge;
238
+ // ANY other selection → the first-party in-TUI editor review). Takes `gating` to toggle plan
239
+ // mode and to COMPOSE the approval→save seam (auto-save → D1a gate exit) — Invariant 1 holds:
240
+ // the surfaces compose the gate through the seams, never own it. The injected wave-launch
241
+ // deps power the plannotator launch chooser (§8.23): the presence probe + the two door open
242
+ // cores are composed HERE so plan.ts/planReview.ts import nothing from the browser modules
243
+ // (planReviewBrowser.ts/objectiveReviewBrowser.ts — the value-import cycle break:
244
+ // planReviewBrowser.ts value-imports the review arms). `draftReviews` is the ONE
245
+ // per-activation current-review slot + unconfirmed-save latch every review surface shares
246
+ // (§8.23 "Draft-review guards") — in-memory, nothing persisted.
247
+ const draftReviews = createDraftReviewSlot(pi);
248
+
249
+ // The composed `perk` status handle (charter D2): one slot carrying the objective segment +
250
+ // the ref-counted browser-wait activity. Created once here (no hidden module state) and
251
+ // threaded into the objective publisher, the plan installer (the warm plannotator arm) and
252
+ // the browser doors; the footer reads it back via get/subscribe.
253
+ const perkStatus = createPerkStatus();
254
+
255
+ installPlanBindings(pi, gating, draftReviews, () => runnerChild, perkStatus, {
256
+ present: () => plannotatorPresent(pi),
257
+ plan: (ctx, opts) =>
258
+ openPlanReviewSurface(
259
+ pi,
260
+ ctx,
261
+ gating,
262
+ opts,
263
+ draftReviewWave,
264
+ annotations,
265
+ draftReviews,
266
+ perkStatus,
267
+ ),
268
+ objective: (ctx, opts) =>
269
+ openObjectiveReviewSurface(
270
+ pi,
271
+ ctx,
272
+ gating,
273
+ opts,
274
+ draftReviewWave,
275
+ annotations,
276
+ draftReviews,
277
+ perkStatus,
278
+ ),
279
+ });
155
280
 
156
281
  // The first 3rd-party plan adapter: a perk-owned, injection-only bridge that re-enables
157
282
  // `@tombell/pi-plan` as a real plan provider. Always registered, but INERT unless
158
283
  // `[providers] plan = "tombell-plan"`; it directs the foreign free-form prose `/plan` surface into
159
284
  // perk's canonical `plan_save` → `cache.plan-ref` contract. It needs no `gating` (Invariant 1: the
160
285
  // read-only gate stays perk's, engaged by the cold-door launch — the shim never arbitrates tools).
161
- registerPlanAdapterTombell(pi);
286
+ installTombellPlanAdapter(pi, () => runnerChild);
162
287
 
163
288
  // The second 3rd-party plan adapter — AUGMENT posture: `@plannotator/pi-extension` contributes
164
- // its browser plan-review UI while perk's plan surface + gate stay (planMode skips only
165
- // `--plan`/`Ctrl+Alt+P` under this selection). Always registered, but INERT unless
166
- // `[providers] plan = "plannotator-plan"`. Injection-only — the `plan_review`
167
- // tool moved to planReview.ts (below), which dispatches to this adapter's event-bus bridge
168
- // when plannotator is selected.
169
- registerPlanAdapterPlannotator(pi);
170
-
171
- // `plan_review`, perk's UNIVERSAL review door: plannotator-selected the event-bus
172
- // bridge; ANY other selection the first-party in-TUI editor review. It takes `gating` only to
173
- // COMPOSE the approvalSave seam on an APPROVED review (auto-save D1a gate exit) Invariant 1
174
- // holds: the door composes the gate through the seam, never owns it. The injected wave-launch
175
- // deps power the plannotator launch chooser (§8.23): the presence probe + the two door open
176
- // cores are composed HERE so planReview.ts imports nothing from door modules (the value-import
177
- // cycle break — planReviewBrowser.ts already value-imports planReview.ts).
178
- registerPlanReview(pi, gating, {
179
- present: () => plannotatorPresent(pi),
180
- plan: (ctx, opts) => openPlanReviewSurface(pi, ctx, gating, opts),
181
- objective: (ctx, opts) => openObjectiveReviewSurface(pi, ctx, gating, opts),
182
- });
183
-
184
- // Objective-author context injection (the objective mirror of plan mode's authoring
185
- // half). Keyed off (read-only gate AND stage === objective-author); planMode defers to it.
186
- registerObjectiveAuthor(pi, gating);
187
-
188
- // Gist-author context injection (the gist mirror). Keyed off (read-only gate AND
189
- // stage === gist-author); planMode defers to it too.
190
- registerGistAuthor(pi, gating);
289
+ // its browser plan-review UI while perk's plan surface + gate stay (the plan installer skips
290
+ // only `--plan`/`Ctrl+Alt+P` under this selection). Always registered, but INERT unless
291
+ // `[providers] plan = "plannotator-plan"`. Injection-only — the `plan_review` tool lives in
292
+ // the plan installer (above), which dispatches to this adapter's event-bus bridge when
293
+ // plannotator is selected.
294
+ installPlannotatorPlanAdapter(pi, () => runnerChild);
295
+
296
+ // The v1 objective-authoring installer: the objective-author context hook pair (this call
297
+ // sits at the frozen hooks-ordering slot the injection always held keyed off (read-only
298
+ // gate AND stage === objective-author); planMode defers to it), plus the
299
+ // `objective_draft`/`objective_save` tools and the `/objective-save` command (registration is
300
+ // name-keyed only the hooks ordering is frozen).
301
+ installObjectiveAuthoringBindings(pi, gating, draftReviews, () => runnerChild);
302
+
303
+ // The v1 gist installer: the gist-authoring context hook pair (this call sits at the frozen
304
+ // hooks-ordering slot the injection always held; planMode defers to it too), plus the
305
+ // `gist_draft`/`gist_save` tools and the `/gist-save` command (registration is name-keyed
306
+ // only the hooks ordering is frozen).
307
+ installGistBindings(pi, gating, draftReviews, () => runnerChild);
308
+
309
+ // The v1 objective-refinement installer (contracts.md §8.67/§8.68): the refinement context hook
310
+ // pair (keyed off (read-only gate AND stage === objective-refine); plan mode and the provider
311
+ // adapters defer to it), the ONE model-facing `objective_refinement_draft` tool, the warm
312
+ // `/objective-refine` entry and the human `/objective-refinement-save` failsafe. Registered
313
+ // before the tool snapshots.
314
+ installObjectiveRefinementBindings(pi, gating, draftReviews, () => runnerChild);
191
315
  let sharedOk = false;
192
316
  try {
193
317
  sharedDir();
@@ -207,11 +331,6 @@ export default function (pi: ExtensionAPI) {
207
331
  }
208
332
  const registryOk = registryStages > 0;
209
333
 
210
- // The single-value `perk` status handle (charter D2): one slot carrying the objective
211
- // segment. Created once here (no hidden module state) and threaded into the objective
212
- // publisher below; the footer reads it back via get/subscribe.
213
- const perkStatus = createPerkStatus();
214
-
215
334
  // The generic full report-detail entry and the `perk:workflow-state` transition marker. Renderer
216
335
  // bodies live in surfaces.ts; registration is wiring through the pre-0.80.4-safe seam. The report
217
336
  // family is appended by command-attached sinks; one workflow registration covers every appender.
@@ -222,13 +341,23 @@ export default function (pi: ExtensionAPI) {
222
341
  // globals). Synced from session_start/session_tree below; closed on session_shutdown so the
223
342
  // consumer lease releases with the session. A stale /reload predecessor instance is retired
224
343
  // by the lease fencing (fresh token per same-identity reacquire + verify-before-inject).
225
- const feedbackReceiver = createHunkFeedbackReceiver(pi);
226
- pi.on("session_shutdown", async () => {
344
+ const feedbackReceiver = (options.feedbackReceiverFactory ?? createHunkFeedbackReceiver)(pi);
345
+ pi.on("session_shutdown", async (_event, ctx) => {
346
+ submitConflict.shutdown();
347
+ stackConflict.shutdown();
348
+ await conflictResolver.shutdown();
227
349
  feedbackReceiver.close();
350
+ // A browser-wait activity cannot outlive the session.
351
+ perkStatus.clearActivity(ctx);
228
352
  });
229
353
 
230
354
  pi.on("session_start", async (_event, ctx) => {
231
- const branchEntries = () => branchOf(ctx);
355
+ // Read the two native-child booleans and latch the floor before lifecycle work or tool rebuilds.
356
+ runnerChild = isRunnerChild(process.env);
357
+ readOnlyFloor ||= decodeReadOnlyFloor(runnerChild, process.env.PI_SUBAGENT_EXTENSION_BINDINGS);
358
+
359
+ submitConflict.setContext(ctx);
360
+ stackConflict.setContext(ctx);
232
361
  const sessionFile = ctx.sessionManager.getSessionFile();
233
362
  const currentSessionId = sessionFile ? basename(sessionFile) : null;
234
363
 
@@ -238,225 +367,108 @@ export default function (pi: ExtensionAPI) {
238
367
  report(ctx, "workflow-state linkage error", "error", message, { alsoLog: true });
239
368
  };
240
369
 
241
- const decision = decideClaim({
242
- state: rebuildWorkflowState(branchEntries()),
243
- currentSessionId,
244
- envRunId: process.env.PERK_RUN_ID ?? null,
245
- cwd: ctx.cwd,
246
- });
247
-
248
- // The session-audit exact-vintage stamp (§8.3), recorded by every run-identity arm below
370
+ // The session-audit exact-vintage stamp (§8.3), recorded by every run-identity arm
249
371
  // (claim/fork/adopt/mint); undefined on the perkVersion() failure sentinel, which drops the
250
372
  // key on serialize and leaves the session on the timestamp-estimate arm.
251
373
  const stamp = versionStamp(version);
252
374
 
253
- // `claim`/`adopt` carry no prior branch state (adopt's is written by its arm below).
254
- let resolved: WorkflowState =
255
- decision.action === "claim" || decision.action === "adopt" ? {} : decision.state;
256
- let minted = false;
257
-
258
- if (decision.action === "claim") {
259
- // Cold claim — establish before consume (strict).
260
- const handoff = readHandoff(ctx.cwd, decision.runId);
261
- if (handoff === null || handoff.run_id !== decision.runId) {
262
- reportError(`handoff missing or mismatched for run ${decision.runId}`);
263
- } else {
264
- // The objective-plan cold door's handoff_extra carries the node link
265
- // (objective_id/node_id): persist it as the objective_node_claim so the implement-here
266
- // exits are structurally suppressed in COLD objective-plan sessions too (the warm
267
- // `objective_node` tool records the claim; a cold factory session never calls it — the
268
- // door marked the node before launch). Blank/absent ids persist nothing; the claim
269
- // clears on a successful node-linked save exactly as the warm-recorded one does.
270
- const handoffObjective = handoff.objective_id;
271
- const handoffNode = handoff.node_id;
272
- const nodeClaim =
273
- typeof handoffObjective === "string" &&
274
- handoffObjective.trim() !== "" &&
275
- typeof handoffNode === "string" &&
276
- handoffNode.trim() !== ""
277
- ? { objective: handoffObjective, node: handoffNode }
278
- : undefined;
279
- const data: WorkflowState = {
280
- run_id: decision.runId,
281
- pi_session_id: currentSessionId ?? undefined,
282
- mode: handoff.mode,
283
- perk_version: stamp,
284
- // Record the launched stage so the interior can tell e.g. objective-author from plan
285
- // (both are read-only) and inject the right authoring context (planMode vs objectiveAuthor).
286
- stage: handoff.stage,
287
- ...(nodeClaim !== undefined ? { objective_node_claim: nodeClaim } : {}),
288
- };
289
- const okAppend = appendWorkflowState(pi, ctx, {
290
- data,
291
- field: "run_id",
292
- expected: decision.runId,
293
- scope: "workflow-state linkage error",
294
- failure: `read-back failed for run ${decision.runId}`,
295
- });
296
- if (!okAppend) {
297
- // do NOT consume
298
- } else {
299
- markHandoffConsumed(ctx.cwd, decision.runId, {
300
- piSessionId: currentSessionId ?? undefined,
301
- });
302
- resolved = data;
303
- }
304
- }
305
- } else if (decision.action === "fork") {
306
- // Inherited a run_id from a different session file → isolate the child's scratch. A static
307
- // redirect or filesystem failure is loud but does not prevent the derived workflow identity
308
- // from settling; later eligible turns retry through the agent-scratch resolver.
309
- try {
310
- ensureRunScratch(ctx.cwd, decision.childRunId);
311
- } catch (error) {
312
- report(
313
- ctx,
314
- "run scratch",
315
- "warning",
316
- `could not create fork run root for ${decision.childRunId}: ${String(error)}`,
317
- { alsoLog: true },
318
- );
319
- }
320
- const data: WorkflowState = {
321
- run_id: decision.childRunId,
322
- pi_session_id: currentSessionId ?? undefined,
323
- predecessor: decision.parentRunId,
324
- mode: decision.state.mode,
325
- perk_version: stamp,
326
- };
327
- pi.appendEntry(WORKFLOW_STATE_TYPE, data);
328
- resolved = data;
329
- } else if (decision.action === "adopt") {
330
- // An env-inherited run id whose handoff was already consumed by a different session: a
331
- // spawned child (contracts §8.2). Mirror the fork arm — derived child identity, isolated
332
- // scratch, inherited mode (read-only gating survives) — minus everything that belongs to
333
- // the launched session: never re-consume the handoff (its pi_session_id keeps the true
334
- // claimer), no `stage` (no stage impersonation / stage-binding injection), and no
335
- // implementation/main pointer capture (resolveRunStage stays null for adopt).
336
- try {
337
- ensureRunScratch(ctx.cwd, decision.childRunId);
338
- } catch (error) {
375
+ // The identity lifecycle (claim / fork / adopt / mint / keep) is the named session
376
+ // operation (session/lifecycle.ts owns the arms); this handler binds the production ports
377
+ // and renders the outcome's per-arm problems/warnings with the exact report scopes the
378
+ // arms always used. The strict appends keep reporting read-back failures through the
379
+ // strict-append seam's own loudness channel. The same cwd-bound handoff reader serves the
380
+ // post-gate facts below (ONE handoff authority).
381
+ const identityPorts = {
382
+ readHandoff: (runId: string) => readHandoff(ctx.cwd, runId),
383
+ listRunIds: () => listRunIds(ctx.cwd),
384
+ markHandoffConsumed: (runId: string, opts: { piSessionId?: string }) =>
385
+ markHandoffConsumed(ctx.cwd, runId, opts),
386
+ ensureRunScratch: (runId: string) => {
387
+ ensureRunScratch(ctx.cwd, runId);
388
+ },
389
+ mintRunId,
390
+ versionStamp: stamp,
391
+ };
392
+ const stateStore = branchSessionStateStore(pi, ctx);
393
+ let identity = establishSessionIdentity(stateStore, identityPorts, {
394
+ currentSessionId,
395
+ envRunId: process.env.PERK_RUN_ID ?? null,
396
+ });
397
+ if (readOnlyFloor) {
398
+ const reflected = reflectSessionReadOnlyFloor(stateStore, identity);
399
+ identity = reflected.outcome;
400
+ if (reflected.unexpectedFailure) {
339
401
  report(
340
402
  ctx,
341
- "run scratch",
342
- "warning",
343
- `could not create adopted run root for ${decision.childRunId}: ${String(error)}`,
403
+ "child restriction",
404
+ "error",
405
+ "could not persist child read-only restriction; in-memory restriction remains active",
344
406
  { alsoLog: true },
345
407
  );
346
408
  }
347
- const data: WorkflowState = {
348
- run_id: decision.childRunId,
349
- pi_session_id: currentSessionId ?? undefined,
350
- predecessor: decision.parentRunId,
351
- mode: decision.mode,
352
- perk_version: stamp,
353
- };
354
- pi.appendEntry(WORKFLOW_STATE_TYPE, data);
355
- resolved = data;
356
- } else if (decision.action === "none") {
357
- // A warm session with no identity mints its own run_id so
358
- // per-run state (the session data dir) can key off it. No disk artifacts —
359
- // dirs are the accessor's job; provenance is recorded separately. A failed cold claim above never
360
- // falls here (claim stays a loud unclaimed error).
361
- const runId = mintRunId();
362
- const data: WorkflowState = {
363
- run_id: runId,
364
- pi_session_id: currentSessionId ?? undefined,
365
- perk_version: stamp,
366
- };
367
- const okAppend = appendWorkflowState(pi, ctx, {
368
- data,
369
- field: "run_id",
370
- expected: runId,
371
- scope: "workflow-state linkage error",
372
- failure: `read-back failed for minted run ${runId}`,
373
- });
374
- if (okAppend) {
375
- resolved = { ...decision.state, ...data };
376
- minted = true;
377
- }
378
409
  }
379
-
380
- // Reapply the read-only allowlist + stage scoping from the resolved mode/stage — FIRST,
381
- // before the plan-ref/stage reconciliation below. `resolved.mode` is final once the
382
- // claim/fork/none arms settle (the later blocks only touch `active_plan_ref` / capture
383
- // pointers), and ordering the sync ahead of them guarantees no cache read or reconciliation
384
- // failure can leave the gate unsynced (defense in depth on top of the total cache readers).
385
- // The scope stage is the workflow-state `stage` key (§8.40): claim → the handoff-recorded
386
- // stage just appended; keep/none the branch-LWW stage; fork INHERITS the parent's stage (a
387
- // forked implement session is an implement session); adopt NEVER impersonates (subagent
388
- // children stay unscoped their fresh branch carries no stage, so session_tree agrees). A
389
- // failed claim leaves `resolved` empty no stage unscoped (stage scoping is fail-open).
390
- // Fail-closed on the gate: if the sync throws, leave it as-is (a failed sync never opens it).
391
- const scopeStage =
392
- decision.action === "adopt"
393
- ? undefined
394
- : (resolved.stage ?? (decision.action === "fork" ? decision.state.stage : undefined));
410
+ for (const problem of identity.problems) reportError(problem);
411
+ for (const warning of identity.warnings) {
412
+ report(ctx, "run scratch", "warning", warning, { alsoLog: true });
413
+ }
414
+ const decision = identity.decision;
415
+ const minted = identity.arm === "minted";
416
+
417
+ // PHASE 1 reapply the read-only allowlist + stage scoping from the established identity
418
+ // FIRST, before the fallible post-gate facts below. The scope derivation is pure (no store,
419
+ // handoff, registry, or checkout read), so no read failure can leave the gate unsynced
420
+ // (defense in depth on top of the total cache readers); `resolved.mode` is final once the
421
+ // arms settle. Fail-closed on the gate: if the sync throws, leave it as-is (a failed sync
422
+ // never opens it).
423
+ const toolScope = sessionStartToolScope(identity);
395
424
  try {
396
- gating.syncFromState(resolved.mode, scopeStage);
425
+ gating.syncFromState(toolScope.mode, toolScope.stage);
397
426
  } catch (error) {
398
427
  console.error(`perk: tool-gating sync failed on session_start — ${error}`);
399
428
  }
400
429
 
401
- // Plan-ref linkage (stage-gated): reconcile the cache.plan-ref file into
402
- // active_plan_ref but ONLY when the launched stage *consumes* the ref (its registry
403
- // `requires`/`reads` list `cache.plan-ref`). That is the worktree binding stages
404
- // (implement/submit/address/land/learn); the root `worktree: none` stages
405
- // (plan/objective-plan/save) must NOT inherit the root *selector* into a fresh planning
406
- // session. Idempotent by (provider, pr_id), strict read-back, headless-safe. Runs after the
407
- // run_id claim so the run is settled first; the two append independent LWW fields.
408
- // Reload/fork/tree (no launched stage) rely on the LWW rebuild — never re-read the file.
409
- const linked = rebuildWorkflowState(branchEntries()).active_plan_ref ?? null;
410
- const runStage = resolveRunStage(decision, ctx.cwd);
411
- // Registry-missing is permissive when a stage is present, to preserve implement linkage.
412
- const consumesPlanRef =
413
- runStage !== null && (registry === null || stageConsumesPlanRef(registry, runStage));
414
- if (consumesPlanRef) {
415
- const cachedRef = readPlanRef(ctx.cwd);
416
- if (cachedRef !== null) {
417
- if (planRefsEqual(linked, cachedRef)) {
418
- resolved = { ...resolved, active_plan_ref: linked };
419
- } else {
420
- if (
421
- appendWorkflowState(pi, ctx, {
422
- data: { active_plan_ref: cachedRef },
423
- field: "active_plan_ref",
424
- expected: cachedRef,
425
- scope: "workflow-state linkage error",
426
- failure: `plan-ref read-back failed for ${cachedRef.provider}:${cachedRef.pr_id}`,
427
- equals: planRefsEqual,
428
- })
429
- ) {
430
- resolved = { ...resolved, active_plan_ref: cachedRef };
431
- }
432
- }
433
- } else if (linked !== null) {
434
- resolved = { ...resolved, active_plan_ref: linked };
435
- }
436
- } else if (linked !== null) {
437
- // Non-consuming stage (or no launched stage): preserve any already-linked ref via LWW,
438
- // but NEVER read the cache file — the root selector must not leak in.
439
- resolved = { ...resolved, active_plan_ref: linked };
430
+ // The refinement cold claim's ONE-TIME context import (contracts.md §8.67): only on the
431
+ // actual cold claim of an `objective-refine` handoff (never keep/fork/adopt/mint), after the
432
+ // identity settled and the gate synced. A refusal is loud and leaves the session gated
433
+ // without a usable context (no orphan repair, no reimport on reload).
434
+ if (identity.arm === "claimed" && typeof identity.resolved.run_id === "string") {
435
+ importRefinementContextOnClaim(pi, ctx, {
436
+ runId: identity.resolved.run_id,
437
+ stage: identity.resolved.stage,
438
+ });
440
439
  }
441
440
 
441
+ // PHASE 2 — the post-gate facts (session/lifecycle.ts owns the decision tree): the lazy,
442
+ // stage-gated `cache.plan-ref` → `active_plan_ref` reconciliation (only a launched stage
443
+ // that *consumes* the ref reads the checkout; claim/keep read the handoff, fork/adopt/none
444
+ // never do; one strict verified append, idempotent by (provider, pr_id)), plus the derived
445
+ // implementation-capture and receiver inputs. Called HERE — after the gate and the
446
+ // refinement import — never while constructing gate inputs. A throwing branch/handoff read
447
+ // propagates to Pi's hook error boundary with the gate already synced: unreadability is
448
+ // never turned into confirmed absence, and no later effect runs from guessed facts.
449
+ const facts = resolveSessionStartFacts(
450
+ stateStore,
451
+ { readHandoff: identityPorts.readHandoff, readPlanRef: () => readPlanRef(ctx.cwd) },
452
+ { identity, registry, currentSessionId },
453
+ );
454
+ const resolved: WorkflowState = facts.resolved;
455
+
442
456
  // Implementation session pointer (contracts.md §8.35): an implement session self-keys its own
443
457
  // session file into the shared main checkout so a later/other session resolves it cross-run.
444
- // The headless worker's inner session lands here too (.main); driveStage records the matching
458
+ // The headless worker's inner session lands here too (.main); runStage records the matching
445
459
  // .worker. A forked implement session inherits the parent's launched stage + threads the
446
460
  // inherited parent session id as fork provenance. Best-effort + non-fatal (carrier warns).
447
461
  // First-write-wins (`preserveForeign`): this is the corroborated shadowing defect site — the
448
462
  // claimer's original capture stays authoritative, and any future shadow vector warns loudly
449
463
  // instead of silently corrupting /learn evidence.
450
- const implStage =
451
- runStage ?? (decision.action === "fork" ? (decision.state.stage ?? null) : null);
452
- if (resolved.run_id && implStage === "implement") {
464
+ if (facts.implementationCapture !== null) {
453
465
  captureSessionPointer({
454
466
  cwd: ctx.cwd,
455
- runId: resolved.run_id,
467
+ runId: facts.implementationCapture.runId,
456
468
  klass: "implementation",
457
469
  site: "main",
458
470
  sessionFile,
459
- parentSessionId: decision.action === "fork" ? (decision.state.pi_session_id ?? null) : null,
471
+ parentSessionId: facts.implementationCapture.parentSessionId,
460
472
  preserveForeign: true,
461
473
  });
462
474
  }
@@ -466,14 +478,7 @@ export default function (pi: ExtensionAPI) {
466
478
  // outbox. Eligibility (interactive TUI + implement stage + non-adopted + settled identity +
467
479
  // plan-ref match against one fresh cache read) is evaluated inside sync; every ineligible
468
480
  // shape closes any open inbox. Never throws (the controller contains its own failures).
469
- feedbackReceiver.sync(ctx, {
470
- stage: implStage,
471
- adopted: decision.action === "adopt",
472
- runId: resolved.run_id ?? null,
473
- piSessionId: currentSessionId,
474
- activePlanRef: resolved.active_plan_ref ?? null,
475
- mode: ctx.mode ?? null,
476
- });
481
+ feedbackReceiver.sync(ctx, { ...facts.feedback, mode: ctx.mode ?? null });
477
482
 
478
483
  // Soft version-parity drift signal: pi can lazy-install / load a stale `npm:@mgiles/perk`, so the
479
484
  // extension actually running may differ from the `perk` CLI that launched it. The local launch
@@ -538,173 +543,153 @@ export default function (pi: ExtensionAPI) {
538
543
 
539
544
  // Non-negotiable: rebuild on branch navigation too, or state goes stale after /tree (§8.3).
540
545
  pi.on("session_tree", async (_event, ctx) => {
546
+ stackConflict.setContext(ctx);
547
+ // ONE fresh full-branch rebuild; the navigation facts derive purely from it (no handoff/
548
+ // checkout read, claim, linkage, or capture on navigation — session/lifecycle.ts owns the
549
+ // asymmetry with startup).
541
550
  const state = rebuildWorkflowState(branchOf(ctx));
551
+ const facts = sessionTreeFacts(state);
542
552
  // Non-negotiable: re-sync the gate + stage scoping on tree navigation too (mode and stage are
543
553
  // per-field LWW — the branch-rebuilt stage is the §8.40 key). Fail-closed on the gate.
544
554
  try {
545
- gating.syncFromState(state.mode, state.stage);
555
+ gating.syncFromState(facts.toolScope.mode, facts.toolScope.stage);
546
556
  } catch (error) {
547
557
  console.error(`perk: tool-gating sync failed on session_tree — ${error}`);
548
558
  }
549
- // Re-sync the feedback receiver from the LWW-rebuilt state (§8.58). `adopted: false` is
550
- // right here: an env-adopted child's fresh branch carries no stage, so the stage gate
551
- // alone keeps it inert on tree navigation.
552
- feedbackReceiver.sync(ctx, {
553
- stage: state.stage ?? null,
554
- adopted: false,
555
- runId: state.run_id ?? null,
556
- piSessionId: state.pi_session_id ?? null,
557
- activePlanRef: state.active_plan_ref ?? null,
558
- mode: ctx.mode ?? null,
559
- });
559
+ // Re-sync the feedback receiver from the LWW-rebuilt state (§8.58) gate first, then
560
+ // receiver; `adopted: false` is right here: an env-adopted child's fresh branch carries no
561
+ // stage, so the stage gate alone keeps it inert on tree navigation.
562
+ feedbackReceiver.sync(ctx, { ...facts.feedback, mode: ctx.mode ?? null });
560
563
  if (process.env.PERK_SELFCHECK) {
561
564
  writeT3Sentinel(ctx.cwd, "tree", state, ctx.mode ?? null);
562
565
  }
563
566
  });
564
567
 
565
- // Warm door: the `plan_save` tool + `/plan-save` command. Takes `gating`:
566
- // a successful command-path save exits read-only mode (the read-only → read-write boundary).
567
- registerPlanSave(pi, gating);
568
-
569
- // The `/implement-here` command: the human-only no-save exit from plan mode (§8.23) —
570
- // implement the reviewed draft in-session, no issue created. Composes the gate through the
571
- // implementHereExit seam; no model tool is registered (machine-unreachable by construction).
572
- registerImplementHere(pi, gating);
573
-
574
- // The `plan_draft` working-draft file tool. Registered in the factory so it
575
- // exists before the gate snapshots tools; its name is in READ_ONLY_TOOLS (the structural
576
- // session-data carve-out), so it survives plan mode.
577
- registerPlanDraft(pi);
578
-
579
- // The `objective_draft` working-objective file tool (the plan_draft twin).
580
- registerObjectiveDraft(pi);
581
-
582
- // The `gist_draft` working-gist file tool (the third draft carve-out).
583
- registerGistDraft(pi);
584
-
585
568
  // Lifecycle gates: the dirty-repo switch/fork guard + the guard-only `/implement`.
586
569
  registerLifecycleGates(pi);
587
570
 
588
571
  // Warm door: the `submit` tool + `/submit` command.
589
- registerSubmit(pi);
590
-
591
- // The warm `ready` door: the deliberate draft→ready review gate (submit keeps draft).
592
- // Takes `gating`: the warm ready→reconcile continuation refuses (loudly) to drive the
593
- // ready-time pass into a read-only session (contracts.md §8.66).
594
- registerReady(pi, gating);
595
-
596
- // Warm doors: `land` merges + sets pending-learn; `learn` clears it (TS-only).
597
- registerLand(pi);
598
- registerLearn(pi);
599
-
600
- // The warm stacked-delivery surface (§8.51): `/objective-stack` (read) +
601
- // `/objective-sync`/`/objective-recover` (drives) + the four typed stack tools. Takes
602
- // `gating` for the driving commands' gate-on soft refusal (stack sync/recovery mutates
603
- // published branches; the stack tools never join READ_ONLY_TOOLS).
604
- registerObjectiveStack(pi, gating);
572
+ installSubmitBindings(pi, submitConflict);
573
+
574
+ // The warm ready + handoff bindings: the deliberate draft→ready review gate (submit keeps
575
+ // draft). Takes `gating`: the warm ready→reconcile continuation refuses (loudly) to drive
576
+ // the ready-time pass into a read-only session (contracts.md §8.66).
577
+ installReadyBindings(pi, gating);
578
+
579
+ // Warm bindings: `land` merges + sets pending-learn; the v1 learn installer (the `learn` +
580
+ // `run_learn_wave` tools and the `/learn` command over the `learning/` feature ops) clears it.
581
+ installLandBindings(pi);
582
+ installLearnBindings(pi, reportWave);
583
+
584
+ // The warm stacked-delivery mutating surface (§8.51): the `/objective-sync`/
585
+ // `/objective-recover`/`/objective-land` drives + the four typed stack tools over the
586
+ // `delivery/stackConflict.ts` + `delivery/stackReconcile.ts` feature ops. Takes `gating` for
587
+ // the driving commands' gate-on soft refusal (stack sync/recovery mutates published
588
+ // branches; the stack tools never join READ_ONLY_TOOLS).
589
+ installStackSyncBindings(pi, gating, stackConflict, options.stackResolutionDelivery);
590
+ installStackRecoverBindings(pi, gating);
591
+ installStackLandBindings(pi, gating);
592
+
593
+ // The stacked-delivery status read: the `objective_stack_status` tool + the `/objective-stack`
594
+ // command (read-only end to end — the command works gate-on; the tool stays gate-off).
595
+ installStackStatusBindings(pi);
605
596
 
606
597
  // The warm `/address` review loop: the submit-then-resolve `finalize_address` tool + `/address`
607
598
  // command. Classify-then-act (the verbose feedback fetch + classification runs in an isolated
608
599
  // spawned child; the parent fixes actionable items and finalizes the committed repairs).
609
- registerAddress(pi);
600
+ installAddressBindings(pi, reportWave, submitConflict);
610
601
 
611
602
  // The warm `/pr-review` door: automated code review in a FRESH, isolated subagent that
612
603
  // POSTS its review to the PR (the deliberate departure from /address's read-only-child rule).
613
- registerPrReview(pi);
614
-
615
- // The EXPERIMENTAL warm `/pr-review-dynamic` door: the selector-driven sibling — angle
616
- // selection delegated to a fresh perk.review-angle-selector lane, normalized in
617
- // module-rendered code; posting shares /pr-review's post_pr_review + clean guard. The
618
- // baseline /pr-review stays canonical; promotion/retire is a later dogfood's call.
619
- registerPrReviewDynamic(pi);
604
+ installAutomatedReviewBindings(pi, reportWave);
620
605
 
621
606
  // The warm `submit_pr_review` tool: the human-gated curated-posting surface both review
622
607
  // doors ride (contracts §8.4) — neither door registers tools of its own.
623
- registerSubmitPrReview(pi);
608
+ installCuratedSubmissionBindings(pi);
624
609
 
625
610
  // The flow-scoped review-wave pair (`start_review_wave`/`collect_review_wave`) both human
626
611
  // review doors drive: non-blocking adversarial-review launch + the typed collect, flow-scoped
627
612
  // via the session's pending-wave guard.
628
- registerReviewWaveTools(pi);
629
- registerAuditWave(pi);
630
- registerHarvestWave(pi);
631
- registerDreamWave(pi);
613
+ installReviewWaveBindings(pi, reportWave);
614
+ installAuditBindings(pi, reportWave);
615
+ installHarvestBindings(pi, reportWave);
616
+ installDreamBindings(pi, reportWave);
617
+
618
+ // The scout launcher (`run_scout_wave`, contracts.md §8.70): the authoring sessions' blocking
619
+ // fan-out of self-contained read-only briefs onto fresh `perk.scout` lanes — one attempt,
620
+ // no retry, reachable in every gated stage except refinement.
621
+ installScoutWaveBindings(pi, reportWave);
632
622
 
633
623
  // The flow-scoped draft-review-wave pair (`start_draft_review_wave`/
634
624
  // `collect_draft_review_wave`) the draft-review door drives: non-blocking draft-review
635
625
  // launch over the door-primed context + the typed collect.
636
- registerDraftReviewWaveTools(pi);
626
+ registerDraftReviewWaveTools(pi, draftReviewWave, reportWave);
637
627
 
638
628
  // The door-primed browser annotation tool (`push_annotations`): the browser door primes the
639
629
  // surface handle on open and clears it on settle/degrade — the tool refuses outside a
640
630
  // door-opened flow.
641
- registerAnnotationPushTool(pi);
631
+ installAnnotationBindings(pi, annotations);
642
632
 
643
633
  // The warm `/pr-review-terminal` door: the terminal review entry — hunk always, no provider
644
634
  // dispatch (the command IS the selection); posting rides `submit_pr_review` above.
645
- registerPrReviewTerminal(pi);
635
+ installPrReviewTerminalBindings(pi);
646
636
 
647
637
  // The warm `/pr-review-browser` door: the browser review entry — plannotator always, opened
648
638
  // in the background (pre-PR it absorbs the since-base local browser review); posting is the
649
639
  // human's own platform-post from the UI, with `submit_pr_review` for request-changes only.
650
- registerPrReviewBrowser(pi);
640
+ installPrReviewBrowserBindings(pi, annotations, perkStatus);
651
641
 
652
642
  // The warm `/stack-review-browser` door + its cold-launch twin (`open_stack_review`): the
653
643
  // stacked-PR browser review over the combined base→top diff — one reviewer wave with
654
644
  // `stack: true`, then judgment-routed per-PR posting through `submit_pr_review`.
655
- registerStackReviewBrowser(pi);
656
- registerOpenStackReview(pi);
645
+ installStackReviewBindings(pi, annotations, perkStatus);
657
646
 
658
647
  // The warm `/plan-review-browser` door: the summonable streaming draft review — the
659
648
  // plannotator plan-review browser on the working plan draft, draft reviewers streaming
660
649
  // phrase-anchored findings in; APPROVE auto-saves via the approvalSave seam, DENY returns a
661
650
  // model-mediated revision round.
662
- registerPlanReviewBrowser(pi, gating);
651
+ registerPlanReviewBrowser(pi, gating, draftReviewWave, annotations, draftReviews, perkStatus);
663
652
 
664
653
  // The warm `/objective-review-browser` door: the summonable streaming objective-draft review
665
654
  // — the plannotator plan-review browser on the RENDERED working objective draft, draft
666
655
  // reviewers streaming phrase-anchored findings in; APPROVE auto-saves via the
667
656
  // objectiveApprovalSave seam, Direct Edits = a model-mediated revise round (never auto-saved).
668
- registerObjectiveReviewBrowser(pi, gating);
657
+ registerObjectiveReviewBrowser(
658
+ pi,
659
+ gating,
660
+ draftReviewWave,
661
+ annotations,
662
+ draftReviews,
663
+ perkStatus,
664
+ );
669
665
 
670
666
  // The read-only CI executor: the `run_ci` tool + `/ci` command + `--allow-project-ci`
671
667
  // flag. Runs the project's `[ci]` named checks deterministically and reports (never fixes/loops).
672
- registerCiExecutor(pi);
668
+ installCiBindings(pi);
673
669
 
674
670
  // The objective substrate: `/objective` set/clear, budget accounting, threshold
675
671
  // compaction, all keyed off the now-live `active_objective`. Inert when no objective is active.
676
672
  // (The deterministic objective mechanics live in the Python plane: `perk objective …`.)
677
- registerObjective(pi, perkStatus);
673
+ installObjectiveBindings(pi, perkStatus);
678
674
 
679
675
  // The warm `/commit-and-compact` utility door: drive a commit of the work so far, compact once
680
676
  // a successful outcome is known, then completion-gate an automatic evidence-first continuation
681
677
  // (clean/read-only trees compact immediately; no commit → no compaction or continuation).
682
678
  // Human-only — no tool twin.
683
- registerCommitAndCompact(pi, gating);
684
-
685
- // The warm `objective_save` door: the `objective_save` tool + `/objective-save` command
686
- // (the objective mirror of plan-save). Takes `gating` for the read-only → read-write boundary.
687
- registerObjectiveSave(pi, gating);
688
-
689
- // The warm `gist_save` door: the `gist_save` tool + `/gist-save` command (the gist mirror).
690
- registerGistSave(pi, gating);
679
+ installCommitCompactBindings(pi, gating);
691
680
 
692
681
  // The objective plan factory's warm transition surface: the `objective_node` bounded
693
682
  // tool (delegates to the Python cold door; `status:"done"` requires a completion audit) + the
694
683
  // `/objective-plan` command (select the next node and author a bounded plan). The command now
695
684
  // enters the read-only gate on invocation (parity with the cold door's `mode: read-only`
696
685
  // handoff; exit stays with plan_save / `/plan` off) — hence `gating`.
697
- registerObjectivePlan(pi, gating);
698
-
699
- // The learned-docs plan factory's warm surface: the `/learn-docs` command gathers open
700
- // perk:learn issues into an inbox (via the `perk learn docs --gather` cold door) and injects the
701
- // factory guidance so the model authors a docs/learned consolidation plan (no model tool).
702
- registerLearnFactoryDoor(pi, DOCS_DOOR);
686
+ installObjectivePlanningBindings(pi, gating, reportWave);
703
687
 
704
- // The learn-code plan factory's warm surface: the `/learn-code` command gathers pre-stamped
705
- // SHOULD_BE_CODE perk:learn issues into an inbox (via the `perk learn code --gather` cold door)
706
- // and injects the factory guidance so the model authors a code-routing plan (no model tool).
707
- registerLearnFactoryDoor(pi, CODE_DOOR);
688
+ // The two learn plan factories' warm surfaces: `/learn-docs` gathers open perk:learn issues
689
+ // (via the `perk learn docs --gather` cold door) toward a docs/learned consolidation plan;
690
+ // `/learn-code` gathers pre-stamped SHOULD_BE_CODE issues (via `perk learn code --gather`)
691
+ // toward a code-routing plan. Guidance-injection only (no model tool).
692
+ installLearnFactoryBindings(pi);
708
693
 
709
694
  // Warm-door skill-binding delivery: Mechanism A's `before_agent_start` injection of
710
695
  // the launched stage's user-originated bindings (+ the stale-context strip). Mechanism B (the