@mgiles/perk 3.1.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 (211) 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 +369 -359
  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 -310
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
  52. package/extension/pi/v1/codeReview/stack.ts +606 -0
  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 +163 -265
  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 +128 -60
  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/{doors → pi/v1}/lifecycleGates.ts +14 -59
  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/pi/v1/objectivePlanning.ts +776 -0
  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 +152 -34
  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 -19
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/paths.ts +2 -7
  115. package/extension/substrate/prompts.ts +22 -0
  116. package/extension/substrate/registry.ts +2 -0
  117. package/extension/substrate/resolverLease.ts +364 -0
  118. package/extension/substrate/sessionData.ts +85 -152
  119. package/extension/substrate/toolGating.ts +279 -84
  120. package/extension/substrate/unifiedDiff.ts +1 -1
  121. package/extension/substrate/workflowState.ts +191 -166
  122. package/extension/substrate/worktreeResolverLock.ts +261 -0
  123. package/extension/surfaces/surfaces.ts +79 -27
  124. package/extension/waves/adversarialReviewWave.ts +103 -48
  125. package/extension/waves/blockedReports.ts +59 -0
  126. package/extension/waves/draftReviewWave.ts +42 -42
  127. package/extension/waves/laneIdentity.ts +77 -0
  128. package/extension/waves/objectiveExplorerWave.ts +24 -24
  129. package/extension/waves/prReviewWave.ts +89 -77
  130. package/extension/waves/reportWave.ts +438 -578
  131. package/extension/waves/reviewClassifierWave.ts +22 -22
  132. package/extension/waves/rpcAdapter.ts +100 -15
  133. package/extension/waves/scoutWave.ts +192 -0
  134. package/extension/waves/transport.ts +480 -0
  135. package/extension/worker/sdkAdapter.ts +494 -0
  136. package/extension/worker/stageExecution.ts +679 -0
  137. package/extension/workerMain.ts +18 -19
  138. package/package.json +6 -4
  139. package/prompts/_fixtures/live.yaml +98 -10
  140. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  142. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  143. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  144. package/prompts/contexts/adapters/tombell-plan.md +4 -0
  145. package/prompts/contexts/objective-refinement.md +17 -0
  146. package/prompts/contexts/plan-authoring.md +6 -5
  147. package/prompts/contexts/read-only.md +1 -1
  148. package/prompts/stages/conflict-resolution-continuation.md +9 -0
  149. package/prompts/stages/conflict-resolution.md +4 -4
  150. package/prompts/stages/objective-author/adopt.md +1 -1
  151. package/prompts/stages/objective-author/file.md +1 -1
  152. package/prompts/stages/objective-author/seed.md +1 -1
  153. package/prompts/stages/objective-plan/guidance.md +2 -2
  154. package/prompts/stages/objective-plan/seed.md +9 -1
  155. package/prompts/stages/objective-reconcile-ready.md +7 -0
  156. package/prompts/stages/objective-reconcile.md +1 -1
  157. package/prompts/stages/objective-refine/seed.md +18 -0
  158. package/prompts/stages/objective-review-browser.md +4 -4
  159. package/prompts/stages/objective-sync.md +1 -1
  160. package/prompts/stages/plan-review-browser.md +4 -4
  161. package/prompts/stages/pr-review-browser/active.md +3 -4
  162. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  163. package/prompts/stages/pr-review-terminal/active.md +3 -3
  164. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  165. package/prompts/stages/pr-review.md +3 -3
  166. package/prompts/stages/stack-review/cold.md +1 -0
  167. package/prompts/stages/stack-review-browser/stack.md +22 -0
  168. package/shared/README.md +8 -3
  169. package/shared/bindings.yaml +6 -3
  170. package/shared/contracts.md +4499 -2147
  171. package/shared/fixtures/issues-table.json +130 -0
  172. package/shared/registry.yaml +29 -1
  173. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  174. package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
  175. package/shared/schemas/outputs/pr-ready.schema.json +110 -2
  176. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  177. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  178. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  179. package/extension/doors/auditWaveTools.ts +0 -352
  180. package/extension/doors/ciExecutor.ts +0 -756
  181. package/extension/doors/commitCompact.ts +0 -251
  182. package/extension/doors/dreamWaveTools.ts +0 -475
  183. package/extension/doors/learn.ts +0 -655
  184. package/extension/doors/objectiveStack.ts +0 -1143
  185. package/extension/doors/prReviewDynamic.ts +0 -276
  186. package/extension/doors/ready.ts +0 -87
  187. package/extension/doors/submit.ts +0 -347
  188. package/extension/doors/submitPrReview.ts +0 -408
  189. package/extension/factories/gistAuthor.ts +0 -94
  190. package/extension/factories/gistDraft.ts +0 -265
  191. package/extension/factories/gistSave.ts +0 -251
  192. package/extension/factories/implementHere.ts +0 -116
  193. package/extension/factories/objectiveAuthor.ts +0 -98
  194. package/extension/factories/objectiveDraft.ts +0 -466
  195. package/extension/factories/objectivePlan.ts +0 -975
  196. package/extension/factories/objectiveSave.ts +0 -363
  197. package/extension/factories/planDraft.ts +0 -140
  198. package/extension/factories/planMode.ts +0 -205
  199. package/extension/factories/planReview.ts +0 -1237
  200. package/extension/factories/planSave.ts +0 -604
  201. package/extension/factories/planTitle.ts +0 -141
  202. package/extension/substrate/structuredOutput.ts +0 -202
  203. package/extension/waves/auditWave.ts +0 -312
  204. package/extension/waves/harvestWave.ts +0 -399
  205. package/extension/waves/learnWave.ts +0 -155
  206. package/extension/waves/memoryAdapter.ts +0 -139
  207. package/extension/waves/prReviewDynamicWave.ts +0 -777
  208. package/extension/worker/readOnlySession.ts +0 -294
  209. package/extension/worker/worker.ts +0 -899
  210. package/prompts/stages/pr-review-dynamic.md +0 -7
  211. package/shared/contracts-history.md +0 -605
@@ -1,79 +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 { registerSubmit } from "./doors/submit.ts";
39
- import { registerSubmitPrReview } from "./doors/submitPrReview.ts";
40
- import { registerGistAuthor } from "./factories/gistAuthor.ts";
41
- import { registerGistDraft } from "./factories/gistDraft.ts";
42
- import { registerGistSave } from "./factories/gistSave.ts";
43
- import { registerImplementHere } from "./factories/implementHere.ts";
44
- import { registerObjective } from "./factories/objective.ts";
45
- import { registerObjectiveAuthor } from "./factories/objectiveAuthor.ts";
46
- import { registerObjectiveDraft } from "./factories/objectiveDraft.ts";
47
- import { registerObjectivePlan } from "./factories/objectivePlan.ts";
48
- import { registerObjectiveSave } from "./factories/objectiveSave.ts";
49
- import { registerPlanDraft } from "./factories/planDraft.ts";
50
- import { registerPlanMode } from "./factories/planMode.ts";
51
- import { registerPlanReview } from "./factories/planReview.ts";
52
- import { registerPlanSave } from "./factories/planSave.ts";
53
- 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";
54
78
  import { createAgentScratchProvisioner, registerAgentScratch } from "./substrate/agentScratch.ts";
55
79
  import { registerBindingDelivery } from "./substrate/bindingDelivery.ts";
56
80
  import {
57
81
  atomicWriteFileSync,
58
82
  ensureRunScratch,
83
+ listRunIds,
59
84
  markHandoffConsumed,
60
85
  readHandoff,
61
86
  readPlanRef,
62
87
  setMarker,
63
88
  workflowDir,
64
89
  } from "./substrate/cache.ts";
65
- 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";
66
92
  import { perkVersion, sharedDir, versionStamp } from "./substrate/resources.ts";
67
93
  import { mintRunId } from "./substrate/runId.ts";
68
94
  import { captureSessionPointer } from "./substrate/sessionPointers.ts";
69
95
  import { registerToolGating } from "./substrate/toolGating.ts";
70
96
  import {
71
- appendWorkflowState,
72
97
  branchOf,
73
- decideClaim,
74
- planRefsEqual,
75
98
  rebuildWorkflowState,
76
- resolveRunStage,
77
99
  WORKFLOW_STATE_TYPE,
78
100
  type WorkflowState,
79
101
  } from "./substrate/workflowState.ts";
@@ -90,6 +112,7 @@ import {
90
112
  } from "./surfaces/surfaces.ts";
91
113
  import { registerBtw } from "./vendor/btw/btw.ts";
92
114
  import { registerWhimsical } from "./vendor/whimsical/whimsical.ts";
115
+ import { createReportWave } from "./waves/reportWave.ts";
93
116
 
94
117
  // Cross-plane proof marker (TS writes via cache.ts; the Python helper reads it — gate check 3).
95
118
  const T3_MARKER = "t3-extension-cache-write";
@@ -123,18 +146,43 @@ function writeT3Sentinel(
123
146
  }
124
147
  }
125
148
 
126
- 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
+ ) {
127
162
  const version = perkVersion();
128
163
 
129
164
  // The read-only tool-gating primitive. Attaches to perk:workflow-state.mode; synced on
130
165
  // both session_start AND session_tree below. enter/exit are the surface the gated stages consume.
131
- 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);
132
180
 
133
181
  // Run-owned disposable scratch guidance for every eligible write-capable model turn. One
134
182
  // activation-scoped provisioner shares retry/warning suppression with the isolated /btw side
135
183
  // session; no model tool or process-global temp environment is introduced.
136
184
  const agentScratch = createAgentScratchProvisioner();
137
- registerAgentScratch(pi, agentScratch);
185
+ registerAgentScratch(pi, agentScratch, () => !gating.isActive() && !runnerChild);
138
186
 
139
187
  // Vendored `btw`: a `/btw` human-only side-chat popover backed by an isolated in-memory
140
188
  // AgentSession. Takes `gating` for the gate-mirror — its side-session toolset + cache key follow
@@ -147,46 +195,123 @@ export default function (pi: ExtensionAPI) {
147
195
  // turn, via the headless-no-op `setWorkingMessage` surfaces seam. Always on, no config toggle.
148
196
  registerWhimsical(pi);
149
197
 
150
- // perk-owned plan mode: the `/plan` + Ctrl+Alt+P + `--plan` toggle surface over the
151
- // read-only gate, plus the plan-authoring context injection. perk owns plan mode end-to-end now (the
152
- // borrowed `@tombell/pi-plan` is retired).
153
- 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
+ });
154
280
 
155
281
  // The first 3rd-party plan adapter: a perk-owned, injection-only bridge that re-enables
156
282
  // `@tombell/pi-plan` as a real plan provider. Always registered, but INERT unless
157
283
  // `[providers] plan = "tombell-plan"`; it directs the foreign free-form prose `/plan` surface into
158
284
  // perk's canonical `plan_save` → `cache.plan-ref` contract. It needs no `gating` (Invariant 1: the
159
285
  // read-only gate stays perk's, engaged by the cold-door launch — the shim never arbitrates tools).
160
- registerPlanAdapterTombell(pi);
286
+ installTombellPlanAdapter(pi, () => runnerChild);
161
287
 
162
288
  // The second 3rd-party plan adapter — AUGMENT posture: `@plannotator/pi-extension` contributes
163
- // its browser plan-review UI while perk's plan surface + gate stay (planMode skips only
164
- // `--plan`/`Ctrl+Alt+P` under this selection). Always registered, but INERT unless
165
- // `[providers] plan = "plannotator-plan"`. Injection-only — the `plan_review`
166
- // tool moved to planReview.ts (below), which dispatches to this adapter's event-bus bridge
167
- // when plannotator is selected.
168
- registerPlanAdapterPlannotator(pi);
169
-
170
- // `plan_review`, perk's UNIVERSAL review door: plannotator-selected the event-bus
171
- // bridge; ANY other selection the first-party in-TUI editor review. It takes `gating` only to
172
- // COMPOSE the approvalSave seam on an APPROVED review (auto-save D1a gate exit) Invariant 1
173
- // holds: the door composes the gate through the seam, never owns it. The injected wave-launch
174
- // deps power the plannotator launch chooser (§8.23): the presence probe + the two door open
175
- // cores are composed HERE so planReview.ts imports nothing from door modules (the value-import
176
- // cycle break — planReviewBrowser.ts already value-imports planReview.ts).
177
- registerPlanReview(pi, gating, {
178
- present: () => plannotatorPresent(pi),
179
- plan: (ctx, opts) => openPlanReviewSurface(pi, ctx, gating, opts),
180
- objective: (ctx, opts) => openObjectiveReviewSurface(pi, ctx, gating, opts),
181
- });
182
-
183
- // Objective-author context injection (the objective mirror of plan mode's authoring
184
- // half). Keyed off (read-only gate AND stage === objective-author); planMode defers to it.
185
- registerObjectiveAuthor(pi, gating);
186
-
187
- // Gist-author context injection (the gist mirror). Keyed off (read-only gate AND
188
- // stage === gist-author); planMode defers to it too.
189
- 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);
190
315
  let sharedOk = false;
191
316
  try {
192
317
  sharedDir();
@@ -206,11 +331,6 @@ export default function (pi: ExtensionAPI) {
206
331
  }
207
332
  const registryOk = registryStages > 0;
208
333
 
209
- // The single-value `perk` status handle (charter D2): one slot carrying the objective
210
- // segment. Created once here (no hidden module state) and threaded into the objective
211
- // publisher below; the footer reads it back via get/subscribe.
212
- const perkStatus = createPerkStatus();
213
-
214
334
  // The generic full report-detail entry and the `perk:workflow-state` transition marker. Renderer
215
335
  // bodies live in surfaces.ts; registration is wiring through the pre-0.80.4-safe seam. The report
216
336
  // family is appended by command-attached sinks; one workflow registration covers every appender.
@@ -221,13 +341,23 @@ export default function (pi: ExtensionAPI) {
221
341
  // globals). Synced from session_start/session_tree below; closed on session_shutdown so the
222
342
  // consumer lease releases with the session. A stale /reload predecessor instance is retired
223
343
  // by the lease fencing (fresh token per same-identity reacquire + verify-before-inject).
224
- const feedbackReceiver = createHunkFeedbackReceiver(pi);
225
- 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();
226
349
  feedbackReceiver.close();
350
+ // A browser-wait activity cannot outlive the session.
351
+ perkStatus.clearActivity(ctx);
227
352
  });
228
353
 
229
354
  pi.on("session_start", async (_event, ctx) => {
230
- 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);
231
361
  const sessionFile = ctx.sessionManager.getSessionFile();
232
362
  const currentSessionId = sessionFile ? basename(sessionFile) : null;
233
363
 
@@ -237,209 +367,108 @@ export default function (pi: ExtensionAPI) {
237
367
  report(ctx, "workflow-state linkage error", "error", message, { alsoLog: true });
238
368
  };
239
369
 
240
- const decision = decideClaim({
241
- state: rebuildWorkflowState(branchEntries()),
242
- currentSessionId,
243
- envRunId: process.env.PERK_RUN_ID ?? null,
244
- cwd: ctx.cwd,
245
- });
246
-
247
- // 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
248
371
  // (claim/fork/adopt/mint); undefined on the perkVersion() failure sentinel, which drops the
249
372
  // key on serialize and leaves the session on the timestamp-estimate arm.
250
373
  const stamp = versionStamp(version);
251
374
 
252
- // `claim`/`adopt` carry no prior branch state (adopt's is written by its arm below).
253
- let resolved: WorkflowState =
254
- decision.action === "claim" || decision.action === "adopt" ? {} : decision.state;
255
- let minted = false;
256
-
257
- if (decision.action === "claim") {
258
- // Cold claim — establish before consume (strict).
259
- const handoff = readHandoff(ctx.cwd, decision.runId);
260
- if (handoff === null || handoff.run_id !== decision.runId) {
261
- reportError(`handoff missing or mismatched for run ${decision.runId}`);
262
- } else {
263
- const data: WorkflowState = {
264
- run_id: decision.runId,
265
- pi_session_id: currentSessionId ?? undefined,
266
- mode: handoff.mode,
267
- perk_version: stamp,
268
- // Record the launched stage so the interior can tell e.g. objective-author from plan
269
- // (both are read-only) and inject the right authoring context (planMode vs objectiveAuthor).
270
- stage: handoff.stage,
271
- };
272
- const okAppend = appendWorkflowState(pi, ctx, {
273
- data,
274
- field: "run_id",
275
- expected: decision.runId,
276
- scope: "workflow-state linkage error",
277
- failure: `read-back failed for run ${decision.runId}`,
278
- });
279
- if (!okAppend) {
280
- // do NOT consume
281
- } else {
282
- markHandoffConsumed(ctx.cwd, decision.runId, {
283
- piSessionId: currentSessionId ?? undefined,
284
- });
285
- resolved = data;
286
- }
287
- }
288
- } else if (decision.action === "fork") {
289
- // Inherited a run_id from a different session file → isolate the child's scratch. A static
290
- // redirect or filesystem failure is loud but does not prevent the derived workflow identity
291
- // from settling; later eligible turns retry through the agent-scratch resolver.
292
- try {
293
- ensureRunScratch(ctx.cwd, decision.childRunId);
294
- } catch (error) {
295
- report(
296
- ctx,
297
- "run scratch",
298
- "warning",
299
- `could not create fork run root for ${decision.childRunId}: ${String(error)}`,
300
- { alsoLog: true },
301
- );
302
- }
303
- const data: WorkflowState = {
304
- run_id: decision.childRunId,
305
- pi_session_id: currentSessionId ?? undefined,
306
- predecessor: decision.parentRunId,
307
- mode: decision.state.mode,
308
- perk_version: stamp,
309
- };
310
- pi.appendEntry(WORKFLOW_STATE_TYPE, data);
311
- resolved = data;
312
- } else if (decision.action === "adopt") {
313
- // An env-inherited run id whose handoff was already consumed by a different session: a
314
- // spawned child (contracts §8.2). Mirror the fork arm — derived child identity, isolated
315
- // scratch, inherited mode (read-only gating survives) — minus everything that belongs to
316
- // the launched session: never re-consume the handoff (its pi_session_id keeps the true
317
- // claimer), no `stage` (no stage impersonation / stage-binding injection), and no
318
- // implementation/main pointer capture (resolveRunStage stays null for adopt).
319
- try {
320
- ensureRunScratch(ctx.cwd, decision.childRunId);
321
- } 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) {
322
401
  report(
323
402
  ctx,
324
- "run scratch",
325
- "warning",
326
- `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",
327
406
  { alsoLog: true },
328
407
  );
329
408
  }
330
- const data: WorkflowState = {
331
- run_id: decision.childRunId,
332
- pi_session_id: currentSessionId ?? undefined,
333
- predecessor: decision.parentRunId,
334
- mode: decision.mode,
335
- perk_version: stamp,
336
- };
337
- pi.appendEntry(WORKFLOW_STATE_TYPE, data);
338
- resolved = data;
339
- } else if (decision.action === "none") {
340
- // A warm session with no identity mints its own run_id so
341
- // per-run state (the session data dir) can key off it. No disk artifacts —
342
- // dirs are the accessor's job; provenance is recorded separately. A failed cold claim above never
343
- // falls here (claim stays a loud unclaimed error).
344
- const runId = mintRunId();
345
- const data: WorkflowState = {
346
- run_id: runId,
347
- pi_session_id: currentSessionId ?? undefined,
348
- perk_version: stamp,
349
- };
350
- const okAppend = appendWorkflowState(pi, ctx, {
351
- data,
352
- field: "run_id",
353
- expected: runId,
354
- scope: "workflow-state linkage error",
355
- failure: `read-back failed for minted run ${runId}`,
356
- });
357
- if (okAppend) {
358
- resolved = { ...decision.state, ...data };
359
- minted = true;
360
- }
361
409
  }
362
-
363
- // Reapply the read-only allowlist + stage scoping from the resolved mode/stage — FIRST,
364
- // before the plan-ref/stage reconciliation below. `resolved.mode` is final once the
365
- // claim/fork/none arms settle (the later blocks only touch `active_plan_ref` / capture
366
- // pointers), and ordering the sync ahead of them guarantees no cache read or reconciliation
367
- // failure can leave the gate unsynced (defense in depth on top of the total cache readers).
368
- // The scope stage is the workflow-state `stage` key (§8.40): claim → the handoff-recorded
369
- // stage just appended; keep/none the branch-LWW stage; fork INHERITS the parent's stage (a
370
- // forked implement session is an implement session); adopt NEVER impersonates (subagent
371
- // children stay unscoped their fresh branch carries no stage, so session_tree agrees). A
372
- // failed claim leaves `resolved` empty no stage unscoped (stage scoping is fail-open).
373
- // Fail-closed on the gate: if the sync throws, leave it as-is (a failed sync never opens it).
374
- const scopeStage =
375
- decision.action === "adopt"
376
- ? undefined
377
- : (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);
378
424
  try {
379
- gating.syncFromState(resolved.mode, scopeStage);
425
+ gating.syncFromState(toolScope.mode, toolScope.stage);
380
426
  } catch (error) {
381
427
  console.error(`perk: tool-gating sync failed on session_start — ${error}`);
382
428
  }
383
429
 
384
- // Plan-ref linkage (stage-gated): reconcile the cache.plan-ref file into
385
- // active_plan_ref but ONLY when the launched stage *consumes* the ref (its registry
386
- // `requires`/`reads` list `cache.plan-ref`). That is the worktree binding stages
387
- // (implement/submit/address/land/learn); the root `worktree: none` stages
388
- // (plan/objective-plan/save) must NOT inherit the root *selector* into a fresh planning
389
- // session. Idempotent by (provider, pr_id), strict read-back, headless-safe. Runs after the
390
- // run_id claim so the run is settled first; the two append independent LWW fields.
391
- // Reload/fork/tree (no launched stage) rely on the LWW rebuild — never re-read the file.
392
- const linked = rebuildWorkflowState(branchEntries()).active_plan_ref ?? null;
393
- const runStage = resolveRunStage(decision, ctx.cwd);
394
- // Registry-missing is permissive when a stage is present, to preserve implement linkage.
395
- const consumesPlanRef =
396
- runStage !== null && (registry === null || stageConsumesPlanRef(registry, runStage));
397
- if (consumesPlanRef) {
398
- const cachedRef = readPlanRef(ctx.cwd);
399
- if (cachedRef !== null) {
400
- if (planRefsEqual(linked, cachedRef)) {
401
- resolved = { ...resolved, active_plan_ref: linked };
402
- } else {
403
- if (
404
- appendWorkflowState(pi, ctx, {
405
- data: { active_plan_ref: cachedRef },
406
- field: "active_plan_ref",
407
- expected: cachedRef,
408
- scope: "workflow-state linkage error",
409
- failure: `plan-ref read-back failed for ${cachedRef.provider}:${cachedRef.pr_id}`,
410
- equals: planRefsEqual,
411
- })
412
- ) {
413
- resolved = { ...resolved, active_plan_ref: cachedRef };
414
- }
415
- }
416
- } else if (linked !== null) {
417
- resolved = { ...resolved, active_plan_ref: linked };
418
- }
419
- } else if (linked !== null) {
420
- // Non-consuming stage (or no launched stage): preserve any already-linked ref via LWW,
421
- // but NEVER read the cache file — the root selector must not leak in.
422
- 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
+ });
423
439
  }
424
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
+
425
456
  // Implementation session pointer (contracts.md §8.35): an implement session self-keys its own
426
457
  // session file into the shared main checkout so a later/other session resolves it cross-run.
427
- // 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
428
459
  // .worker. A forked implement session inherits the parent's launched stage + threads the
429
460
  // inherited parent session id as fork provenance. Best-effort + non-fatal (carrier warns).
430
461
  // First-write-wins (`preserveForeign`): this is the corroborated shadowing defect site — the
431
462
  // claimer's original capture stays authoritative, and any future shadow vector warns loudly
432
463
  // instead of silently corrupting /learn evidence.
433
- const implStage =
434
- runStage ?? (decision.action === "fork" ? (decision.state.stage ?? null) : null);
435
- if (resolved.run_id && implStage === "implement") {
464
+ if (facts.implementationCapture !== null) {
436
465
  captureSessionPointer({
437
466
  cwd: ctx.cwd,
438
- runId: resolved.run_id,
467
+ runId: facts.implementationCapture.runId,
439
468
  klass: "implementation",
440
469
  site: "main",
441
470
  sessionFile,
442
- parentSessionId: decision.action === "fork" ? (decision.state.pi_session_id ?? null) : null,
471
+ parentSessionId: facts.implementationCapture.parentSessionId,
443
472
  preserveForeign: true,
444
473
  });
445
474
  }
@@ -449,14 +478,7 @@ export default function (pi: ExtensionAPI) {
449
478
  // outbox. Eligibility (interactive TUI + implement stage + non-adopted + settled identity +
450
479
  // plan-ref match against one fresh cache read) is evaluated inside sync; every ineligible
451
480
  // shape closes any open inbox. Never throws (the controller contains its own failures).
452
- feedbackReceiver.sync(ctx, {
453
- stage: implStage,
454
- adopted: decision.action === "adopt",
455
- runId: resolved.run_id ?? null,
456
- piSessionId: currentSessionId,
457
- activePlanRef: resolved.active_plan_ref ?? null,
458
- mode: ctx.mode ?? null,
459
- });
481
+ feedbackReceiver.sync(ctx, { ...facts.feedback, mode: ctx.mode ?? null });
460
482
 
461
483
  // Soft version-parity drift signal: pi can lazy-install / load a stale `npm:@mgiles/perk`, so the
462
484
  // extension actually running may differ from the `perk` CLI that launched it. The local launch
@@ -521,165 +543,153 @@ export default function (pi: ExtensionAPI) {
521
543
 
522
544
  // Non-negotiable: rebuild on branch navigation too, or state goes stale after /tree (§8.3).
523
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).
524
550
  const state = rebuildWorkflowState(branchOf(ctx));
551
+ const facts = sessionTreeFacts(state);
525
552
  // Non-negotiable: re-sync the gate + stage scoping on tree navigation too (mode and stage are
526
553
  // per-field LWW — the branch-rebuilt stage is the §8.40 key). Fail-closed on the gate.
527
554
  try {
528
- gating.syncFromState(state.mode, state.stage);
555
+ gating.syncFromState(facts.toolScope.mode, facts.toolScope.stage);
529
556
  } catch (error) {
530
557
  console.error(`perk: tool-gating sync failed on session_tree — ${error}`);
531
558
  }
532
- // Re-sync the feedback receiver from the LWW-rebuilt state (§8.58). `adopted: false` is
533
- // right here: an env-adopted child's fresh branch carries no stage, so the stage gate
534
- // alone keeps it inert on tree navigation.
535
- feedbackReceiver.sync(ctx, {
536
- stage: state.stage ?? null,
537
- adopted: false,
538
- runId: state.run_id ?? null,
539
- piSessionId: state.pi_session_id ?? null,
540
- activePlanRef: state.active_plan_ref ?? null,
541
- mode: ctx.mode ?? null,
542
- });
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 });
543
563
  if (process.env.PERK_SELFCHECK) {
544
564
  writeT3Sentinel(ctx.cwd, "tree", state, ctx.mode ?? null);
545
565
  }
546
566
  });
547
567
 
548
- // Warm door: the `plan_save` tool + `/plan-save` command. Takes `gating`:
549
- // a successful command-path save exits read-only mode (the read-only → read-write boundary).
550
- registerPlanSave(pi, gating);
551
-
552
- // The `/implement-here` command: the human-only no-save exit from plan mode (§8.23) —
553
- // implement the reviewed draft in-session, no issue created. Composes the gate through the
554
- // implementHereExit seam; no model tool is registered (machine-unreachable by construction).
555
- registerImplementHere(pi, gating);
556
-
557
- // The `plan_draft` working-draft file tool. Registered in the factory so it
558
- // exists before the gate snapshots tools; its name is in READ_ONLY_TOOLS (the structural
559
- // session-data carve-out), so it survives plan mode.
560
- registerPlanDraft(pi);
561
-
562
- // The `objective_draft` working-objective file tool (the plan_draft twin).
563
- registerObjectiveDraft(pi);
564
-
565
- // The `gist_draft` working-gist file tool (the third draft carve-out).
566
- registerGistDraft(pi);
567
-
568
568
  // Lifecycle gates: the dirty-repo switch/fork guard + the guard-only `/implement`.
569
569
  registerLifecycleGates(pi);
570
570
 
571
571
  // Warm door: the `submit` tool + `/submit` command.
572
- registerSubmit(pi);
573
-
574
- // The warm `ready` door: the deliberate draft→ready review gate (submit keeps draft).
575
- registerReady(pi);
576
-
577
- // Warm doors: `land` merges + sets pending-learn; `learn` clears it (TS-only).
578
- registerLand(pi);
579
- registerLearn(pi);
580
-
581
- // The warm stacked-delivery surface (§8.51): `/objective-stack` (read) +
582
- // `/objective-sync`/`/objective-recover` (drives) + the four typed stack tools. Takes
583
- // `gating` for the driving commands' gate-on soft refusal (stack sync/recovery mutates
584
- // published branches; the stack tools never join READ_ONLY_TOOLS).
585
- 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);
586
596
 
587
597
  // The warm `/address` review loop: the submit-then-resolve `finalize_address` tool + `/address`
588
598
  // command. Classify-then-act (the verbose feedback fetch + classification runs in an isolated
589
599
  // spawned child; the parent fixes actionable items and finalizes the committed repairs).
590
- registerAddress(pi);
600
+ installAddressBindings(pi, reportWave, submitConflict);
591
601
 
592
602
  // The warm `/pr-review` door: automated code review in a FRESH, isolated subagent that
593
603
  // POSTS its review to the PR (the deliberate departure from /address's read-only-child rule).
594
- registerPrReview(pi);
595
-
596
- // The EXPERIMENTAL warm `/pr-review-dynamic` door: the selector-driven sibling — angle
597
- // selection delegated to a fresh perk.review-angle-selector lane, normalized in
598
- // module-rendered code; posting shares /pr-review's post_pr_review + clean guard. The
599
- // baseline /pr-review stays canonical; promotion/retire is a later dogfood's call.
600
- registerPrReviewDynamic(pi);
604
+ installAutomatedReviewBindings(pi, reportWave);
601
605
 
602
606
  // The warm `submit_pr_review` tool: the human-gated curated-posting surface both review
603
607
  // doors ride (contracts §8.4) — neither door registers tools of its own.
604
- registerSubmitPrReview(pi);
608
+ installCuratedSubmissionBindings(pi);
605
609
 
606
610
  // The flow-scoped review-wave pair (`start_review_wave`/`collect_review_wave`) both human
607
611
  // review doors drive: non-blocking adversarial-review launch + the typed collect, flow-scoped
608
612
  // via the session's pending-wave guard.
609
- registerReviewWaveTools(pi);
610
- registerAuditWave(pi);
611
- registerHarvestWave(pi);
612
- 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);
613
622
 
614
623
  // The flow-scoped draft-review-wave pair (`start_draft_review_wave`/
615
624
  // `collect_draft_review_wave`) the draft-review door drives: non-blocking draft-review
616
625
  // launch over the door-primed context + the typed collect.
617
- registerDraftReviewWaveTools(pi);
626
+ registerDraftReviewWaveTools(pi, draftReviewWave, reportWave);
618
627
 
619
628
  // The door-primed browser annotation tool (`push_annotations`): the browser door primes the
620
629
  // surface handle on open and clears it on settle/degrade — the tool refuses outside a
621
630
  // door-opened flow.
622
- registerAnnotationPushTool(pi);
631
+ installAnnotationBindings(pi, annotations);
623
632
 
624
633
  // The warm `/pr-review-terminal` door: the terminal review entry — hunk always, no provider
625
634
  // dispatch (the command IS the selection); posting rides `submit_pr_review` above.
626
- registerPrReviewTerminal(pi);
635
+ installPrReviewTerminalBindings(pi);
627
636
 
628
637
  // The warm `/pr-review-browser` door: the browser review entry — plannotator always, opened
629
638
  // in the background (pre-PR it absorbs the since-base local browser review); posting is the
630
639
  // human's own platform-post from the UI, with `submit_pr_review` for request-changes only.
631
- registerPrReviewBrowser(pi);
640
+ installPrReviewBrowserBindings(pi, annotations, perkStatus);
641
+
642
+ // The warm `/stack-review-browser` door + its cold-launch twin (`open_stack_review`): the
643
+ // stacked-PR browser review over the combined base→top diff — one reviewer wave with
644
+ // `stack: true`, then judgment-routed per-PR posting through `submit_pr_review`.
645
+ installStackReviewBindings(pi, annotations, perkStatus);
632
646
 
633
647
  // The warm `/plan-review-browser` door: the summonable streaming draft review — the
634
648
  // plannotator plan-review browser on the working plan draft, draft reviewers streaming
635
649
  // phrase-anchored findings in; APPROVE auto-saves via the approvalSave seam, DENY returns a
636
650
  // model-mediated revision round.
637
- registerPlanReviewBrowser(pi, gating);
651
+ registerPlanReviewBrowser(pi, gating, draftReviewWave, annotations, draftReviews, perkStatus);
638
652
 
639
653
  // The warm `/objective-review-browser` door: the summonable streaming objective-draft review
640
654
  // — the plannotator plan-review browser on the RENDERED working objective draft, draft
641
655
  // reviewers streaming phrase-anchored findings in; APPROVE auto-saves via the
642
656
  // objectiveApprovalSave seam, Direct Edits = a model-mediated revise round (never auto-saved).
643
- registerObjectiveReviewBrowser(pi, gating);
657
+ registerObjectiveReviewBrowser(
658
+ pi,
659
+ gating,
660
+ draftReviewWave,
661
+ annotations,
662
+ draftReviews,
663
+ perkStatus,
664
+ );
644
665
 
645
666
  // The read-only CI executor: the `run_ci` tool + `/ci` command + `--allow-project-ci`
646
667
  // flag. Runs the project's `[ci]` named checks deterministically and reports (never fixes/loops).
647
- registerCiExecutor(pi);
668
+ installCiBindings(pi);
648
669
 
649
670
  // The objective substrate: `/objective` set/clear, budget accounting, threshold
650
671
  // compaction, all keyed off the now-live `active_objective`. Inert when no objective is active.
651
672
  // (The deterministic objective mechanics live in the Python plane: `perk objective …`.)
652
- registerObjective(pi, perkStatus);
673
+ installObjectiveBindings(pi, perkStatus);
653
674
 
654
675
  // The warm `/commit-and-compact` utility door: drive a commit of the work so far, compact once
655
676
  // a successful outcome is known, then completion-gate an automatic evidence-first continuation
656
677
  // (clean/read-only trees compact immediately; no commit → no compaction or continuation).
657
678
  // Human-only — no tool twin.
658
- registerCommitAndCompact(pi, gating);
659
-
660
- // The warm `objective_save` door: the `objective_save` tool + `/objective-save` command
661
- // (the objective mirror of plan-save). Takes `gating` for the read-only → read-write boundary.
662
- registerObjectiveSave(pi, gating);
663
-
664
- // The warm `gist_save` door: the `gist_save` tool + `/gist-save` command (the gist mirror).
665
- registerGistSave(pi, gating);
679
+ installCommitCompactBindings(pi, gating);
666
680
 
667
681
  // The objective plan factory's warm transition surface: the `objective_node` bounded
668
682
  // tool (delegates to the Python cold door; `status:"done"` requires a completion audit) + the
669
683
  // `/objective-plan` command (select the next node and author a bounded plan). The command now
670
684
  // enters the read-only gate on invocation (parity with the cold door's `mode: read-only`
671
685
  // handoff; exit stays with plan_save / `/plan` off) — hence `gating`.
672
- registerObjectivePlan(pi, gating);
673
-
674
- // The learned-docs plan factory's warm surface: the `/learn-docs` command gathers open
675
- // perk:learn issues into an inbox (via the `perk learn docs --gather` cold door) and injects the
676
- // factory guidance so the model authors a docs/learned consolidation plan (no model tool).
677
- registerLearnFactoryDoor(pi, DOCS_DOOR);
686
+ installObjectivePlanningBindings(pi, gating, reportWave);
678
687
 
679
- // The learn-code plan factory's warm surface: the `/learn-code` command gathers pre-stamped
680
- // SHOULD_BE_CODE perk:learn issues into an inbox (via the `perk learn code --gather` cold door)
681
- // and injects the factory guidance so the model authors a code-routing plan (no model tool).
682
- 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);
683
693
 
684
694
  // Warm-door skill-binding delivery: Mechanism A's `before_agent_start` injection of
685
695
  // the launched stage's user-originated bindings (+ the stale-context strip). Mechanism B (the