@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.
- package/README.md +5 -0
- package/extension/authoring/gist/draft.ts +198 -0
- package/extension/authoring/gist/prose.ts +46 -0
- package/extension/authoring/gist/review.ts +133 -0
- package/extension/authoring/gist/save.ts +118 -0
- package/extension/authoring/objective/draft.ts +345 -0
- package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
- package/extension/authoring/objective/planning.ts +124 -0
- package/extension/authoring/objective/prose.ts +103 -0
- package/extension/authoring/objective/review.ts +128 -0
- package/extension/authoring/objective/save.ts +224 -0
- package/extension/authoring/plan/draft.ts +84 -0
- package/extension/authoring/plan/prose.ts +41 -0
- package/extension/authoring/plan/review.ts +269 -0
- package/extension/authoring/plan/save.ts +256 -0
- package/extension/authoring/plan/source.ts +82 -0
- package/extension/authoring/refinement/context.ts +468 -0
- package/extension/authoring/refinement/draft.ts +261 -0
- package/extension/authoring/refinement/prose.ts +79 -0
- package/extension/authoring/refinement/review.ts +111 -0
- package/extension/authoring/refinement/save.ts +119 -0
- package/extension/authoring/review/approvalGate.ts +34 -0
- package/extension/authoring/review/draftContext.ts +68 -0
- package/extension/codeReview/automated.ts +352 -0
- package/extension/codeReview/submission.ts +229 -0
- package/extension/delivery/address.ts +295 -0
- package/extension/delivery/ci.ts +355 -0
- package/extension/delivery/commitCompact.ts +93 -0
- package/extension/delivery/conflictResolution.ts +247 -0
- package/extension/delivery/ready.ts +193 -0
- package/extension/delivery/stackConflict.ts +361 -0
- package/extension/delivery/stackObjective.ts +16 -0
- package/extension/delivery/stackReconcile.ts +165 -0
- package/extension/delivery/submit.ts +171 -0
- package/extension/index.ts +365 -380
- package/extension/learning/analystWave.ts +324 -0
- package/extension/learning/audit.ts +667 -0
- package/extension/learning/capture.ts +92 -0
- package/extension/learning/containment.ts +104 -0
- package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
- package/extension/learning/dreamAnalysis.ts +435 -0
- package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
- package/extension/{waves → learning}/dreamReport.ts +35 -31
- package/extension/learning/harvest.ts +491 -0
- package/extension/learning/prose.ts +66 -0
- package/extension/learning/routing.ts +79 -0
- package/extension/pi/v1/bashScanTimeout.ts +64 -0
- package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -311
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
- package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
- package/extension/pi/v1/codeReview/submit.ts +354 -0
- package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
- package/extension/pi/v1/contextEvidence.ts +80 -0
- package/extension/pi/v1/contextInjection.ts +207 -0
- package/extension/{doors → pi/v1/delivery}/address.ts +154 -267
- package/extension/pi/v1/delivery/ci.ts +570 -0
- package/extension/pi/v1/delivery/commitCompact.ts +201 -0
- package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
- package/extension/{doors → pi/v1/delivery}/land.ts +123 -61
- package/extension/pi/v1/delivery/ready.ts +322 -0
- package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
- package/extension/pi/v1/delivery/stackDrive.ts +120 -0
- package/extension/pi/v1/delivery/stackLand.ts +223 -0
- package/extension/pi/v1/delivery/stackRecover.ts +265 -0
- package/extension/pi/v1/delivery/stackStatus.ts +237 -0
- package/extension/pi/v1/delivery/stackSync.ts +658 -0
- package/extension/pi/v1/delivery/submit.ts +389 -0
- package/extension/pi/v1/delivery/submitConflict.ts +186 -0
- package/extension/pi/v1/draftReview.ts +431 -0
- package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
- package/extension/pi/v1/gist.ts +794 -0
- package/extension/pi/v1/learning/audit.ts +186 -0
- package/extension/pi/v1/learning/dream.ts +207 -0
- package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
- package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
- package/extension/pi/v1/learning/learn.ts +585 -0
- package/extension/pi/v1/lifecycleGates.ts +127 -0
- package/extension/{factories → pi/v1}/objective.ts +53 -33
- package/extension/pi/v1/objectiveAuthoring.ts +672 -0
- package/extension/pi/v1/objectiveDreamGate.ts +160 -0
- package/extension/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
- package/extension/pi/v1/objectiveRefinement.ts +1320 -0
- package/extension/pi/v1/objectiveReview.ts +451 -0
- package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
- package/extension/pi/v1/plan.ts +812 -0
- package/extension/pi/v1/planReview.ts +820 -0
- package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
- package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
- package/extension/pi/v1/providers/plannotator.ts +487 -0
- package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +73 -27
- package/extension/pi/v1/providers/selection.ts +43 -0
- package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
- package/extension/pi/v1/review.ts +538 -0
- package/extension/pi/v1/reviewOutcome.ts +9 -0
- package/extension/pi/v1/scoutWave.ts +318 -0
- package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
- package/extension/session/branchWorkflowSession.ts +60 -0
- package/extension/session/lifecycle.ts +644 -0
- package/extension/session/lifecycleGates.ts +64 -0
- package/extension/session/saveDestination.ts +87 -0
- package/extension/session/workflowSession.ts +971 -0
- package/extension/substrate/agentScratch.ts +27 -54
- package/extension/substrate/bashScanTimeout.ts +181 -0
- package/extension/substrate/bindingDelivery.ts +38 -30
- package/extension/substrate/bindings.ts +4 -5
- package/extension/substrate/cache.ts +64 -12
- package/extension/substrate/childRestrictions.ts +39 -0
- package/extension/substrate/coldDoor.ts +17 -1
- package/extension/substrate/config.ts +157 -21
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +5 -4
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +263 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +178 -163
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +87 -46
- package/extension/waves/blockedReports.ts +59 -0
- package/extension/waves/draftReviewWave.ts +42 -42
- package/extension/waves/laneIdentity.ts +77 -0
- package/extension/waves/objectiveExplorerWave.ts +24 -24
- package/extension/waves/prReviewWave.ts +89 -77
- package/extension/waves/reportWave.ts +438 -578
- package/extension/waves/reviewClassifierWave.ts +22 -22
- package/extension/waves/rpcAdapter.ts +100 -15
- package/extension/waves/scoutWave.ts +192 -0
- package/extension/waves/transport.ts +480 -0
- package/extension/worker/sdkAdapter.ts +494 -0
- package/extension/worker/stageExecution.ts +679 -0
- package/extension/workerMain.ts +18 -19
- package/package.json +6 -4
- package/prompts/_fixtures/live.yaml +43 -18
- package/prompts/contexts/adapters/plannotator-gist.md +6 -0
- package/prompts/contexts/adapters/plannotator-objective.md +6 -0
- package/prompts/contexts/adapters/plannotator-plan.md +8 -1
- package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -6
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-plan/guidance.md +2 -2
- package/prompts/stages/objective-plan/seed.md +9 -1
- package/prompts/stages/objective-reconcile-ready.md +1 -1
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-refine/seed.md +18 -0
- package/prompts/stages/objective-review-browser.md +4 -4
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +4 -4
- package/prompts/stages/pr-review-browser/active.md +3 -4
- package/prompts/stages/pr-review-browser/foreign.md +3 -4
- package/prompts/stages/pr-review-terminal/active.md +3 -3
- package/prompts/stages/pr-review-terminal/foreign.md +3 -3
- package/prompts/stages/pr-review.md +3 -3
- package/prompts/stages/stack-review-browser/stack.md +5 -6
- package/shared/README.md +8 -0
- package/shared/bindings.yaml +3 -3
- package/shared/contracts.md +2601 -506
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +13 -0
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
- package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
- package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
- package/extension/adapters/planAdapterPlannotator.ts +0 -362
- package/extension/doors/auditWaveTools.ts +0 -352
- package/extension/doors/ciExecutor.ts +0 -756
- package/extension/doors/commitCompact.ts +0 -251
- package/extension/doors/dreamWaveTools.ts +0 -489
- package/extension/doors/learn.ts +0 -668
- package/extension/doors/lifecycleGates.ts +0 -207
- package/extension/doors/objectiveStack.ts +0 -1543
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -279
- package/extension/doors/submit.ts +0 -373
- package/extension/doors/submitPrReview.ts +0 -505
- package/extension/factories/gistAuthor.ts +0 -94
- package/extension/factories/gistDraft.ts +0 -265
- package/extension/factories/gistSave.ts +0 -251
- package/extension/factories/implementHere.ts +0 -116
- package/extension/factories/objectiveAuthor.ts +0 -98
- package/extension/factories/objectiveDraft.ts +0 -466
- package/extension/factories/objectiveSave.ts +0 -366
- package/extension/factories/planDraft.ts +0 -140
- package/extension/factories/planMode.ts +0 -205
- package/extension/factories/planReview.ts +0 -1237
- package/extension/factories/planSave.ts +0 -604
- package/extension/factories/planTitle.ts +0 -141
- package/extension/substrate/structuredOutput.ts +0 -202
- package/extension/waves/auditWave.ts +0 -312
- package/extension/waves/harvestWave.ts +0 -399
- package/extension/waves/learnWave.ts +0 -155
- package/extension/waves/memoryAdapter.ts +0 -139
- package/extension/waves/prReviewDynamicWave.ts +0 -777
- package/extension/worker/readOnlySession.ts +0 -294
- package/extension/worker/worker.ts +0 -899
- package/prompts/stages/pr-review-dynamic.md +0 -7
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
// The `plan_review` door's execute paths — the backend-neutral review dispatcher and the PLAN
|
|
2
|
+
// arm's adapter over the `authoring/plan/` feature. perk's UNIVERSAL plan-review surface: the
|
|
3
|
+
// model calls ONE tool (registered by `pi/v1/plan.ts`, which injects the production dependency
|
|
4
|
+
// bag built by `planSaveDepsFor` — the ONE composition point, so this module never imports
|
|
5
|
+
// `plan.ts` back); this module dispatches to the configured review backend.
|
|
6
|
+
// Plannotator-selected → the event-bus bridge (`createPlannotatorBridge`,
|
|
7
|
+
// providers/plannotator.ts — the AUGMENT-posture path, byte-stable); ANY other selection
|
|
8
|
+
// (perk-plan, tombell, unknown ids) → the FIRST-PARTY in-TUI editor review
|
|
9
|
+
// (`runFirstPartyReview`, pi/v1/review.ts): display the draft in pi's built-in `ctx.ui.editor`
|
|
10
|
+
// dialog (scrollable, Ctrl+G opens the user's external $EDITOR), write optional human edits
|
|
11
|
+
// back to the draft BEFORE the verdict (a failed write-back aborts the review fail-open), then an
|
|
12
|
+
// approve/deny/skip `ctx.ui.select` verdict — on the plan arm with a 4th "Implement here — no
|
|
13
|
+
// issue saved" option (§8.23; suppressed in objective-node planning sessions) — with deny
|
|
14
|
+
// feedback via a second editor dialog.
|
|
15
|
+
//
|
|
16
|
+
// REVIEW SEMANTICS (file-first, approval auto-saves): the review runs while the session is still
|
|
17
|
+
// read-only (the tool is in READ_ONLY_TOOLS — review happens before the gate ever comes off).
|
|
18
|
+
// The reviewed plan resolves FILE-FIRST via `resolvePlanSource` (the validated `plan-draft.md`
|
|
19
|
+
// artifact wins; the `plan` param is the fallback; the transcript scrape is NEVER reviewed — an
|
|
20
|
+
// approval would auto-save scraped conversation bytes, so no draft + no param soft-skips with a
|
|
21
|
+
// `plan_draft` redirect). An APPROVED outcome (either backend) wires into the shared
|
|
22
|
+
// `planApprovalSave` seam (authoring/plan/save.ts): auto-save → D1a gate exit → terminating
|
|
23
|
+
// result, node link recovered from the `objective_node_claim` carrier inside `savePlan`. A DENY
|
|
24
|
+
// returns feedback and directs a `plan_draft` rewrite + re-review. Plannotator's browser
|
|
25
|
+
// "Direct Edits" (a `# Direct Edits` unified diff opening the feedback) are translated INTO the
|
|
26
|
+
// feature's typed `PlanReviewOutcome` at the reviewer adapter below and handled feature-side:
|
|
27
|
+
// the PLAN arm mechanically applies an approved diff (strict apply → draft write-back → save
|
|
28
|
+
// the edited bytes; any failure falls open to the verbatim save + a loud warning); DENY stays
|
|
29
|
+
// model-mediated. Strict on deny, FAIL-OPEN everywhere else: headless / dismissed (Esc anywhere
|
|
30
|
+
// = skip, mirroring ask_user_question's dismissal — deny is always explicit) /
|
|
31
|
+
// backend-unavailable all soft-skip so plan authoring never wedges — those arms keep the
|
|
32
|
+
// present-the-plan + human-`/plan-save` discipline (the manual failsafe).
|
|
33
|
+
//
|
|
34
|
+
// THE LAUNCH CHOOSER (plannotator arms only, §8.23): on an eligible round (injected `WaveLaunch`
|
|
35
|
+
// deps present, plannotator loaded, the review source a validated draft artifact) the tool first
|
|
36
|
+
// asks the human — "Browser review + reviewer wave" vs "Browser review only" — before anything
|
|
37
|
+
// launches. The wave choice collects an optional trimmed custom angle, delegates to the door's
|
|
38
|
+
// guidance-returning open core (openPlanReviewSurface / openObjectiveReviewSurface, injected —
|
|
39
|
+
// never imported: doors value-import the review arms), and returns the NON-terminating
|
|
40
|
+
// `wave_launched` result carrying the door guidance verbatim; the browser decision then routes
|
|
41
|
+
// through the door's background decision task. Esc ⇒ the plain flavor (never a cancel); abort
|
|
42
|
+
// outranks every dialog result AND the awaited opener (an interrupted turn never reports a
|
|
43
|
+
// launched wave); a null opener return (synchronous port-pick failure, loudly reported in the
|
|
44
|
+
// core) falls open to the plain blocking review in the same call.
|
|
45
|
+
//
|
|
46
|
+
// THE OBJECTIVE ARM routes to `pi/v1/objectiveReview.ts`; THE GIST ARM routes to
|
|
47
|
+
// `pi/v1/gist.ts`'s `runGistReviewV1` — direct sibling imports (the injected-arm indirection
|
|
48
|
+
// died with the factories home; `pi/v1` siblings import directly, and none of them import this
|
|
49
|
+
// module back).
|
|
50
|
+
//
|
|
51
|
+
// THE GUARDS (`draftReview.ts`, §8.23): EVERY arm opens the activation's current-review slot at
|
|
52
|
+
// entry (superseding whatever review any other surface had open) and, on a completed verdict,
|
|
53
|
+
// runs the decision ladder — superseded / unconfirmed-save latch / reviewed-bytes / destination
|
|
54
|
+
// fence — before the feature completion saves anything. Every save result reports into the latch.
|
|
55
|
+
//
|
|
56
|
+
// INVARIANTS HELD: never calls `setActiveTools`, never registers a `tool_call` handler, never
|
|
57
|
+
// restamps `cache.plan-ref.provider`. The door composes the gate AND the save EXCLUSIVELY
|
|
58
|
+
// through the feature seams (Invariant 1: composes, never owns).
|
|
59
|
+
|
|
60
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
61
|
+
import { GIST_AUTHOR_STAGE } from "../../authoring/gist/draft.ts";
|
|
62
|
+
import { OBJECTIVE_AUTHOR_STAGE, OBJECTIVE_SAVE_STAGE } from "../../authoring/objective/prose.ts";
|
|
63
|
+
import { resumePlanDraft, revisePlanDraft } from "../../authoring/plan/draft.ts";
|
|
64
|
+
import {
|
|
65
|
+
completePlanReview,
|
|
66
|
+
type PlanDraftReviewer,
|
|
67
|
+
type PlanReviewOutcome,
|
|
68
|
+
type ReviewPlanDraftResult,
|
|
69
|
+
} from "../../authoring/plan/review.ts";
|
|
70
|
+
import type {
|
|
71
|
+
ObjectiveNodeLink,
|
|
72
|
+
PlanApprovalSaveDeps,
|
|
73
|
+
SavePlanOutcome,
|
|
74
|
+
} from "../../authoring/plan/save.ts";
|
|
75
|
+
import { type PlanSource, resolvePlanSource } from "../../authoring/plan/source.ts";
|
|
76
|
+
import { REFINE_STAGE } from "../../authoring/refinement/context.ts";
|
|
77
|
+
import { openBranchWorkflowSession } from "../../session/branchWorkflowSession.ts";
|
|
78
|
+
import { bindingSuffix } from "../../substrate/bindingDelivery.ts";
|
|
79
|
+
import type { PlanRef } from "../../substrate/cache.ts";
|
|
80
|
+
import type { Result } from "../../substrate/result.ts";
|
|
81
|
+
import type { ToolGating } from "../../substrate/toolGating.ts";
|
|
82
|
+
import { paramsOf, stringParam } from "../../substrate/toolParams.ts";
|
|
83
|
+
import { branchOf, rebuildWorkflowState } from "../../substrate/workflowState.ts";
|
|
84
|
+
import { report } from "../../surfaces/report.ts";
|
|
85
|
+
import {
|
|
86
|
+
checkDraftReviewDecision,
|
|
87
|
+
type DecisionCheck,
|
|
88
|
+
type DraftReviewSlot,
|
|
89
|
+
destinationChangedResult,
|
|
90
|
+
type OpenDraftReview,
|
|
91
|
+
openRefusedResult,
|
|
92
|
+
type ReviewSource,
|
|
93
|
+
recordSaveOutcome,
|
|
94
|
+
saveUnconfirmedResult,
|
|
95
|
+
staleApprovalResult,
|
|
96
|
+
supersededReviewResult,
|
|
97
|
+
withDraftChangedNote,
|
|
98
|
+
} from "./draftReview.ts";
|
|
99
|
+
import { runGistReviewV1 } from "./gist.ts";
|
|
100
|
+
import {
|
|
101
|
+
isRefinementSession,
|
|
102
|
+
refinementStageRefusal,
|
|
103
|
+
runRefinementReviewV1,
|
|
104
|
+
} from "./objectiveRefinement.ts";
|
|
105
|
+
import { executeObjectiveReview } from "./objectiveReview.ts";
|
|
106
|
+
import { extractDirectEdits, hasDirectEditsHeading } from "./providers/plannotator.ts";
|
|
107
|
+
import { isPlannotatorPlanSelected } from "./providers/selection.ts";
|
|
108
|
+
import {
|
|
109
|
+
approvedSubjectSaveResult,
|
|
110
|
+
chooseReviewLaunch,
|
|
111
|
+
type DraftReviewBridge,
|
|
112
|
+
PLAN_SUBJECT,
|
|
113
|
+
type ReviewOutcome,
|
|
114
|
+
runFirstPartyReview,
|
|
115
|
+
skipResult,
|
|
116
|
+
subjectReviewOutcomeResult,
|
|
117
|
+
type ToolResult,
|
|
118
|
+
VERDICT_IMPLEMENT_HERE,
|
|
119
|
+
verdictsFor,
|
|
120
|
+
type WaveLaunch,
|
|
121
|
+
waveLaunchedResult,
|
|
122
|
+
} from "./review.ts";
|
|
123
|
+
|
|
124
|
+
/** The review bridge slice the installer builds over the plannotator event bus. */
|
|
125
|
+
export type PlanReviewBridge = DraftReviewBridge;
|
|
126
|
+
|
|
127
|
+
/** The warm-door ok-arm fields — the `details` surface doubles as branch-safe persisted state. */
|
|
128
|
+
export interface PlanSaveOk {
|
|
129
|
+
/** `issue.id` is the opaque string issue id (GitHub "42", Linear "ENG-123") — §8.21. */
|
|
130
|
+
issue: { id: string; url: string };
|
|
131
|
+
plan_ref: PlanRef;
|
|
132
|
+
cached: boolean;
|
|
133
|
+
existed: boolean | null;
|
|
134
|
+
updated: boolean;
|
|
135
|
+
objective_node: ObjectiveNodeLink | null;
|
|
136
|
+
plan_source: PlanSource | null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The rendered save result every plan-save surface returns (AgentToolResult has no `isError`;
|
|
141
|
+
* failure is signaled via `details.ok`). Declared HERE with the `renderSave` port it types (the
|
|
142
|
+
* concrete shape must stay visible through `approvalSave` — callers narrow `details.ok` and read
|
|
143
|
+
* `objective_node` WITHOUT assertions); the one production renderer lives in `plan.ts`.
|
|
144
|
+
*/
|
|
145
|
+
export type SaveResult = Result<PlanSaveOk>;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The injected production dependency bag (built by `plan.ts`'s `planSaveDepsFor` — the ONE
|
|
149
|
+
* composition point; declared HERE so `plan.ts` imports the port and the graph stays acyclic):
|
|
150
|
+
* the feature approval-save deps plus the installer-owned save rendering (its message assembly
|
|
151
|
+
* lives with the save surfaces in `plan.ts`).
|
|
152
|
+
*/
|
|
153
|
+
export interface PlanReviewV1Deps extends PlanApprovalSaveDeps {
|
|
154
|
+
/** Render a feature `SavePlanOutcome` as the warm-door SaveResult (byte-stable messages). */
|
|
155
|
+
renderSave(save: SavePlanOutcome): SaveResult;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------- plan-flavor mappers
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Map a non-approved review outcome into the model-facing tool result (exported for the offline
|
|
162
|
+
* tests) — the plan flavor of `subjectReviewOutcomeResult`. The execute path routes approved
|
|
163
|
+
* outcomes to `approvedSaveResult` first, so `completed` renders DENIED here.
|
|
164
|
+
*/
|
|
165
|
+
export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
166
|
+
return subjectReviewOutcomeResult(PLAN_SUBJECT, outcome);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The approval→save orchestration outcome as the ADAPTER renders it: the feature's
|
|
171
|
+
* `PlanApprovalSaveOutcome` with the save already rendered as the warm-door Result (the browser
|
|
172
|
+
* door consumes this shape via the `approvalSave` seam in `plan.ts`).
|
|
173
|
+
*/
|
|
174
|
+
export type ApprovalSaveOutcome =
|
|
175
|
+
| { status: "no-plan" }
|
|
176
|
+
| { status: "saved" | "save-failed"; result: SaveResult; gateExited: boolean };
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Map an APPROVED review outcome + the `approvalSave` outcome into the model-facing tool result
|
|
180
|
+
* (exported for the offline tests) — the plan flavor of `approvedSubjectSaveResult`. `edited`
|
|
181
|
+
* flags that human edits were written back to the draft pre-verdict (the first-party editor, or
|
|
182
|
+
* the plannotator Direct Edits auto-apply), so the saved bytes carry them. `directEditsFailed`
|
|
183
|
+
* (plannotator-only, optional — absent keeps every existing call site byte-stable) flags a
|
|
184
|
+
* Direct Edits section that could not be honored: the plan saved verbatim, a loud warning added.
|
|
185
|
+
*/
|
|
186
|
+
export function approvedSaveResult(
|
|
187
|
+
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
188
|
+
save: ApprovalSaveOutcome,
|
|
189
|
+
opts: { paramMismatch: boolean; edited?: boolean; directEditsFailed?: boolean },
|
|
190
|
+
): ToolResult {
|
|
191
|
+
return approvedSubjectSaveResult(
|
|
192
|
+
PLAN_SUBJECT,
|
|
193
|
+
outcome,
|
|
194
|
+
save.status === "no-plan" ? { status: "no-source" } : save,
|
|
195
|
+
opts,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ------------------------------------------------------------------- the implement-here seam
|
|
200
|
+
|
|
201
|
+
/** The core instruction text (exported for the offline content pins). */
|
|
202
|
+
export const IMPLEMENT_HERE_GUIDANCE = `The human chose IMPLEMENT HERE: implement the reviewed plan directly in this session — no plan issue was created and none will be.
|
|
203
|
+
|
|
204
|
+
- The read-only gate is off: make the plan's edits now, in this checkout.
|
|
205
|
+
- Run the checks the plan calls for before declaring done.
|
|
206
|
+
- Do NOT commit, branch, or push unless the user explicitly asks — git gestures stay with the human.
|
|
207
|
+
- perk's lifecycle doors (/submit, /land, /learn) do not apply — there is no plan issue or plan-ref.
|
|
208
|
+
- The plan draft artifact is untouched: /plan-save can still create the canonical issue later.`;
|
|
209
|
+
|
|
210
|
+
/** The one info line every implement-here gate exit reports (the seam + the review arm share it). */
|
|
211
|
+
const IMPLEMENT_HERE_EXIT_NOTICE =
|
|
212
|
+
"plan mode off — implementing here; no issue saved (draft intact; /plan-save can still create it)";
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build the implement-here instruction text. When `editedPlan` is set (review-path human edits
|
|
216
|
+
* were written back to the draft pre-verdict), the final reviewed bytes are inlined so the model
|
|
217
|
+
* implements THOSE, not its stale in-context version. Always appends the Mechanism-B skill-binding
|
|
218
|
+
* suffix for `command:implement-here` (a `[[bindings]]` hook; delivers nothing by default).
|
|
219
|
+
*/
|
|
220
|
+
export function implementHereGuidance(cwd: string, opts: { editedPlan?: string }): string {
|
|
221
|
+
const edited =
|
|
222
|
+
opts.editedPlan === undefined
|
|
223
|
+
? ""
|
|
224
|
+
: `\n\nThe human edited the plan during review; implement THESE final bytes:\n\n${opts.editedPlan}`;
|
|
225
|
+
return `${IMPLEMENT_HERE_GUIDANCE}${edited}${bindingSuffix(cwd, "command:implement-here")}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The gate-exit-WITHOUT-save seam — the no-save sibling of `planApprovalSave`'s D1a arm. If the
|
|
230
|
+
* gate is active, exit it and report one info line; otherwise a no-op. Keeps Invariant 1:
|
|
231
|
+
* callers (the review arm's implement-here verdict, the `/implement-here` command) compose the
|
|
232
|
+
* gate through this seam, never own it.
|
|
233
|
+
*/
|
|
234
|
+
export function implementHereExit(
|
|
235
|
+
ctx: ExtensionContext,
|
|
236
|
+
gating: ToolGating,
|
|
237
|
+
): { gateExited: boolean } {
|
|
238
|
+
if (!gating.isActive()) return { gateExited: false };
|
|
239
|
+
gating.exit(ctx);
|
|
240
|
+
report(ctx, "implement-here", "info", IMPLEMENT_HERE_EXIT_NOTICE);
|
|
241
|
+
return { gateExited: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Map an IMPLEMENT-HERE review outcome + the gate-exit outcome into the model-facing
|
|
246
|
+
* tool result (exported for the offline tests). NON-terminating on purpose — the model continues
|
|
247
|
+
* the turn and implements immediately. The text is the implement-here guidance; when the human
|
|
248
|
+
* edited the plan during review (`edited`), the final reviewed bytes are inlined so the model
|
|
249
|
+
* implements THOSE, not its stale in-context version (the draft write-back already happened
|
|
250
|
+
* pre-verdict). Nothing is saved: no issue, no plan-ref, the draft artifact intact (§8.23).
|
|
251
|
+
*/
|
|
252
|
+
export function implementHereResult(
|
|
253
|
+
outcome: Extract<ReviewOutcome, { status: "implement-here" }>,
|
|
254
|
+
exit: { gateExited: boolean },
|
|
255
|
+
opts: { cwd: string; plan: string; edited: boolean },
|
|
256
|
+
): ToolResult {
|
|
257
|
+
return {
|
|
258
|
+
content: [
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: implementHereGuidance(opts.cwd, { editedPlan: opts.edited ? opts.plan : undefined }),
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
details: {
|
|
265
|
+
ok: true,
|
|
266
|
+
status: "implement-here",
|
|
267
|
+
saved: false,
|
|
268
|
+
gateExited: exit.gateExited,
|
|
269
|
+
reviewId: outcome.reviewId,
|
|
270
|
+
...(opts.edited ? { edited: true } : {}),
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The `/implement-here` command body (the registration stays in `plan.ts`; the handler lives
|
|
277
|
+
* HERE, next to the seam it composes — and so the `sendUserMessage` call sites stay out of the
|
|
278
|
+
* installer file, whose registration prose the prose-review workbench edits through the
|
|
279
|
+
* TypeScript source adapter's whole-file validation). Three arms: (1) an objective-node
|
|
280
|
+
* planning session refuses — an implement-here would strand the node in `planning` (the claim
|
|
281
|
+
* is only cleared by a node-linked save or a non-planning transition); gate untouched, nothing
|
|
282
|
+
* injected. (2) Nothing to exit — the command's meaning is *exiting plan mode without saving*.
|
|
283
|
+
* (3) Gate off → instruct the model. No inlined plan: the model authored the draft in its own
|
|
284
|
+
* context (the review-path edited-bytes inlining is `implementHereResult`'s arm).
|
|
285
|
+
*/
|
|
286
|
+
export async function runImplementHereCommand(
|
|
287
|
+
pi: ExtensionAPI,
|
|
288
|
+
ctx: ExtensionContext,
|
|
289
|
+
gating: ToolGating,
|
|
290
|
+
slot: DraftReviewSlot,
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
// A refinement session has no plan to implement: refuse before any effect — the gate toggle
|
|
293
|
+
// never makes an old plan draft routable.
|
|
294
|
+
if (isRefinementSession(branchOf(ctx))) {
|
|
295
|
+
report(ctx, "implement-here", "warning", refinementStageRefusal("/implement-here"));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// The claim read rides the session seam (the one workflow-state owner) — the command has no
|
|
299
|
+
// injected deps bag, so it opens the branch-backed session the production composition uses.
|
|
300
|
+
if (openBranchWorkflowSession(pi, ctx).nodeClaim() !== null) {
|
|
301
|
+
report(
|
|
302
|
+
ctx,
|
|
303
|
+
"implement-here",
|
|
304
|
+
"warning",
|
|
305
|
+
"this is an objective-node planning session — a node-linked plan must be saved " +
|
|
306
|
+
"(the node advance and backlink depend on it). Use plan_review / /plan-save instead.",
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!gating.isActive()) {
|
|
311
|
+
report(
|
|
312
|
+
ctx,
|
|
313
|
+
"implement-here",
|
|
314
|
+
"warning",
|
|
315
|
+
"not in plan mode — nothing to exit; just ask the model to implement.",
|
|
316
|
+
);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Exiting plan mode without saving retires whatever review is open: a later browser decision
|
|
320
|
+
// for it is ignored (loudly) rather than saving a plan the human chose not to save.
|
|
321
|
+
slot.supersede();
|
|
322
|
+
implementHereExit(ctx, gating);
|
|
323
|
+
const message = implementHereGuidance(ctx.cwd, {});
|
|
324
|
+
if (ctx.isIdle()) {
|
|
325
|
+
pi.sendUserMessage(message);
|
|
326
|
+
} else {
|
|
327
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The defensive refusal arm (the feature's gate-safety invariant surfacing): an implement-here
|
|
333
|
+
* verdict reached the execute path in an objective-node planning session — a node-linked plan
|
|
334
|
+
* must save (the node advance and backlink depend on it). Loud, NON-terminating: nothing saved,
|
|
335
|
+
* the gate untouched.
|
|
336
|
+
*/
|
|
337
|
+
function implementHereRefusedResult(): ToolResult {
|
|
338
|
+
const error =
|
|
339
|
+
"this is an objective-node planning session — a node-linked plan must be saved " +
|
|
340
|
+
"(the node advance and backlink depend on it)";
|
|
341
|
+
return {
|
|
342
|
+
content: [
|
|
343
|
+
{
|
|
344
|
+
type: "text",
|
|
345
|
+
text:
|
|
346
|
+
`WARNING: implement-here refused — ${error}. Nothing was saved and the session stays ` +
|
|
347
|
+
"read-only; approve the plan via plan_review or ask the user to run /plan-save.",
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
details: {
|
|
351
|
+
ok: false,
|
|
352
|
+
error,
|
|
353
|
+
error_type: "implement_here_refused",
|
|
354
|
+
status: "skipped",
|
|
355
|
+
reason: "implement_here_refused",
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// -------------------------------------------------------------------- the reviewer adapters
|
|
361
|
+
|
|
362
|
+
/** Rebuild the door's completed outcome from carried facts (the mappers consume it). */
|
|
363
|
+
function completedOutcome(
|
|
364
|
+
approved: boolean,
|
|
365
|
+
carried: { feedback?: string; reviewId?: string },
|
|
366
|
+
): Extract<ReviewOutcome, { status: "completed" }> {
|
|
367
|
+
return {
|
|
368
|
+
status: "completed",
|
|
369
|
+
approved,
|
|
370
|
+
...(carried.feedback !== undefined ? { feedback: carried.feedback } : {}),
|
|
371
|
+
reviewId: carried.reviewId ?? "",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Translate a bridge outcome (`ReviewOutcome`) into the feature's `PlanReviewOutcome` —
|
|
377
|
+
* provider vocabulary dies HERE: an approval whose feedback opens with `# Direct Edits`
|
|
378
|
+
* becomes `approvedDirectEdits` (extractable diff + remainder) or `approvedEditsUnparseable`
|
|
379
|
+
* (seen heading, unextractable body — the fail-open-but-loud arm); DENY passes the raw
|
|
380
|
+
* feedback (diff included) through for the model-mediated `plan_draft` rewrite.
|
|
381
|
+
*/
|
|
382
|
+
function planOutcomeOf(outcome: ReviewOutcome): PlanReviewOutcome {
|
|
383
|
+
switch (outcome.status) {
|
|
384
|
+
case "completed": {
|
|
385
|
+
const carried = {
|
|
386
|
+
...(outcome.reviewId !== "" ? { reviewId: outcome.reviewId } : {}),
|
|
387
|
+
};
|
|
388
|
+
if (!outcome.approved) {
|
|
389
|
+
return {
|
|
390
|
+
status: "denied",
|
|
391
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
392
|
+
...carried,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (outcome.feedback !== undefined) {
|
|
396
|
+
const section = extractDirectEdits(outcome.feedback);
|
|
397
|
+
if (section !== null) {
|
|
398
|
+
return {
|
|
399
|
+
status: "approvedDirectEdits",
|
|
400
|
+
diff: section.diff,
|
|
401
|
+
...(section.remainder !== undefined ? { remainder: section.remainder } : {}),
|
|
402
|
+
rawFeedback: outcome.feedback,
|
|
403
|
+
...carried,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
if (hasDirectEditsHeading(outcome.feedback)) {
|
|
407
|
+
return { status: "approvedEditsUnparseable", rawFeedback: outcome.feedback, ...carried };
|
|
408
|
+
}
|
|
409
|
+
return { status: "approved", feedback: outcome.feedback, ...carried };
|
|
410
|
+
}
|
|
411
|
+
return { status: "approved", ...carried };
|
|
412
|
+
}
|
|
413
|
+
case "implement-here":
|
|
414
|
+
// Unreachable from the bridge (its browser envelope returns only approve/deny) — kept
|
|
415
|
+
// total; the feature's allowImplementHere routing owns the safety either way.
|
|
416
|
+
return { status: "implementHere", reviewId: outcome.reviewId };
|
|
417
|
+
case "unavailable":
|
|
418
|
+
return { status: "unavailable", warning: outcome.warning };
|
|
419
|
+
case "aborted":
|
|
420
|
+
return { status: "aborted" };
|
|
421
|
+
case "dismissed":
|
|
422
|
+
return { status: "dismissed" };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The first-party reviewer adapter: the in-TUI editor review with the draft write-back bound to
|
|
428
|
+
* the session seam (edits land BEFORE the verdict — a failed write-back is the `unavailable`
|
|
429
|
+
* abort inside the core). The 4th verdict (implement-here, the no-save exit) is offered UNLESS
|
|
430
|
+
* this is an objective-node planning session — a node-linked plan must save (the node advance
|
|
431
|
+
* and backlink depend on it), so the claim suppresses it back to the 3-option select (the UX
|
|
432
|
+
* layer; the feature's `allowImplementHere` refusal is the structural backstop).
|
|
433
|
+
*/
|
|
434
|
+
function firstPartyPlanReviewer(
|
|
435
|
+
ctx: ExtensionContext,
|
|
436
|
+
deps: PlanReviewV1Deps,
|
|
437
|
+
nodeClaimed: boolean,
|
|
438
|
+
): PlanDraftReviewer {
|
|
439
|
+
return {
|
|
440
|
+
async review(plan, signal) {
|
|
441
|
+
const fp = await runFirstPartyReview({
|
|
442
|
+
ui: ctx.ui,
|
|
443
|
+
plan,
|
|
444
|
+
writeDraft: (text) => {
|
|
445
|
+
const written = revisePlanDraft({ plan: text }, deps.session);
|
|
446
|
+
return written.status === "revised" || written.status === "unchanged";
|
|
447
|
+
},
|
|
448
|
+
...(signal !== undefined ? { signal } : {}),
|
|
449
|
+
...(nodeClaimed
|
|
450
|
+
? {}
|
|
451
|
+
: {
|
|
452
|
+
verdicts: { ...verdictsFor(PLAN_SUBJECT), implementHere: VERDICT_IMPLEMENT_HERE },
|
|
453
|
+
}),
|
|
454
|
+
});
|
|
455
|
+
return { outcome: planOutcomeOf(fp.outcome), plan: fp.plan, edited: fp.edited };
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Whether a feature outcome is an approval (the arms whose completion saves). */
|
|
461
|
+
function isApproval(outcome: PlanReviewOutcome): boolean {
|
|
462
|
+
return (
|
|
463
|
+
outcome.status === "approved" ||
|
|
464
|
+
outcome.status === "approvedDirectEdits" ||
|
|
465
|
+
outcome.status === "approvedEditsUnparseable"
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** The reviewer feedback an approval carries (for the no-save results' DATA suffix). */
|
|
470
|
+
function approvalFeedback(outcome: PlanReviewOutcome): string | undefined {
|
|
471
|
+
switch (outcome.status) {
|
|
472
|
+
case "approved":
|
|
473
|
+
case "denied":
|
|
474
|
+
return outcome.feedback;
|
|
475
|
+
case "approvedDirectEdits":
|
|
476
|
+
case "approvedEditsUnparseable":
|
|
477
|
+
return outcome.rawFeedback;
|
|
478
|
+
default:
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Render a non-`proceed` ladder verdict as the plan arm's tool result (nothing saved; the gate
|
|
485
|
+
* untouched). Shared by the Plannotator arm, the first-party arm and the browser door.
|
|
486
|
+
*/
|
|
487
|
+
export function planGuardResult(
|
|
488
|
+
check: Exclude<DecisionCheck, { kind: "proceed" }>,
|
|
489
|
+
review: OpenDraftReview,
|
|
490
|
+
feedback: string | undefined,
|
|
491
|
+
): ToolResult {
|
|
492
|
+
switch (check.kind) {
|
|
493
|
+
case "superseded":
|
|
494
|
+
return supersededReviewResult("plan");
|
|
495
|
+
case "save-unconfirmed":
|
|
496
|
+
return saveUnconfirmedResult("plan", review.runId, check.detail, feedback);
|
|
497
|
+
case "stale-approval":
|
|
498
|
+
return staleApprovalResult("plan", check.reviewedDigest, feedback);
|
|
499
|
+
case "destination-changed":
|
|
500
|
+
return destinationChangedResult("plan", check.changed, feedback);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Report a completed plan review's save result into the unconfirmed-save latch. */
|
|
505
|
+
function recordPlanSaveOutcome(slot: DraftReviewSlot, result: ReviewPlanDraftResult): void {
|
|
506
|
+
switch (result.status) {
|
|
507
|
+
case "approvedSaved":
|
|
508
|
+
recordSaveOutcome(slot, "plan", { confirmed: true });
|
|
509
|
+
return;
|
|
510
|
+
case "approvedSaveFailed":
|
|
511
|
+
recordSaveOutcome(slot, "plan", { confirmed: false, detail: result.save.result.message });
|
|
512
|
+
return;
|
|
513
|
+
default:
|
|
514
|
+
// Denials, Direct Edits and the no-plan arm never reach the backend — no attempt to confirm.
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ------------------------------------------------------------------------- the execute paths
|
|
520
|
+
|
|
521
|
+
/** The plan arm's no-source soft skip (byte-stable redirect). */
|
|
522
|
+
function noPlanResult(): ToolResult {
|
|
523
|
+
return {
|
|
524
|
+
content: [
|
|
525
|
+
{
|
|
526
|
+
type: "text",
|
|
527
|
+
text:
|
|
528
|
+
"no plan to review — write the working draft with plan_draft (or pass the plan " +
|
|
529
|
+
"param), then call plan_review again.",
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
details: {
|
|
533
|
+
ok: false,
|
|
534
|
+
error: "no plan to review — write the draft with plan_draft first",
|
|
535
|
+
error_type: "no_plan",
|
|
536
|
+
status: "skipped",
|
|
537
|
+
reason: "no_plan",
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The plan arm: headless skip → file-first resolution skip (`no_plan`) → the launch chooser
|
|
544
|
+
* (plannotator + drafts-only eligibility, abort-outranks-everything ordering) → the slot open
|
|
545
|
+
* (every arm; the source names how the reviewed bytes were obtained) → reviewer dispatch: the
|
|
546
|
+
* Plannotator bridge judges the reviewed bytes verbatim; the first-party editor review writes
|
|
547
|
+
* human edits back before the verdict → the decision ladder on a completed verdict → the
|
|
548
|
+
* feature completion (`completePlanReview`: the Direct-Edits apply ladder and the D1a approval
|
|
549
|
+
* save) → result rendering, with the save reported into the latch.
|
|
550
|
+
* (No `pi`/`gating` parameters: every effect rides `ctx` or the injected deps bag — the gate
|
|
551
|
+
* is `deps.gate`, composed once in `plan.ts`.)
|
|
552
|
+
*/
|
|
553
|
+
export async function runPlanReviewV1(
|
|
554
|
+
ctx: ExtensionContext,
|
|
555
|
+
bridge: PlanReviewBridge,
|
|
556
|
+
slot: DraftReviewSlot,
|
|
557
|
+
deps: PlanReviewV1Deps,
|
|
558
|
+
plan: string | undefined,
|
|
559
|
+
signal?: AbortSignal,
|
|
560
|
+
wave?: WaveLaunch,
|
|
561
|
+
): Promise<ToolResult> {
|
|
562
|
+
// 1. Headless → soft skip (fail-open; never wedges CI/supervisor runs on an interactive UI).
|
|
563
|
+
if (!ctx.hasUI) return skipResult();
|
|
564
|
+
// 2. File-first resolution: artifact → param, NEVER transcript — an approval
|
|
565
|
+
// auto-saves the reviewed bytes, and scraped conversation bytes must never be those.
|
|
566
|
+
const src = resolvePlanSource(
|
|
567
|
+
{
|
|
568
|
+
draft: resumePlanDraft(deps.session),
|
|
569
|
+
...(plan !== undefined ? { explicit: plan } : {}),
|
|
570
|
+
},
|
|
571
|
+
"review",
|
|
572
|
+
);
|
|
573
|
+
if (src === null) return noPlanResult();
|
|
574
|
+
const sig = signal ?? ctx.signal;
|
|
575
|
+
// The claim read rides the injected session (the seam owns workflow-state reads — another
|
|
576
|
+
// backing must never disagree with a raw branch read here).
|
|
577
|
+
const nodeClaimed = deps.session.nodeClaim() !== null;
|
|
578
|
+
const sourceTier = src.source === "plan-draft" ? "plan-draft" : "param";
|
|
579
|
+
const completion = { source: sourceTier, paramMismatch: src.paramMismatch } as const;
|
|
580
|
+
// 3. Backend dispatch: plannotator-selected → the event-bus bridge; ANY other selection
|
|
581
|
+
// (perk-plan, tombell, unknown ids) → the first-party in-TUI editor review.
|
|
582
|
+
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
583
|
+
// The launch chooser (contracts.md §8.23): every eligible round the human picks with/without
|
|
584
|
+
// the streamed reviewer wave BEFORE anything launches. Eligibility is drafts-only — the wave
|
|
585
|
+
// door reviews and stale-guards the validated artifact, so a param-tier source keeps the
|
|
586
|
+
// plain path (silently: there is no forced mode to warn about; the `wave === undefined` arm
|
|
587
|
+
// is defensive/test-only and behaves identically).
|
|
588
|
+
if (wave?.present() && src.source === "plan-draft") {
|
|
589
|
+
const choice = await chooseReviewLaunch(ctx.ui, "Plan", sig);
|
|
590
|
+
if (choice.launch === "aborted") return reviewOutcomeResult({ status: "aborted" });
|
|
591
|
+
if (choice.launch === "wave") {
|
|
592
|
+
const guidance = await wave.plan(ctx, {
|
|
593
|
+
draft: src.plan,
|
|
594
|
+
...(choice.custom !== undefined ? { custom: choice.custom } : {}),
|
|
595
|
+
});
|
|
596
|
+
// Abort outranks the opener result too: a turn interrupted during the awaited open must
|
|
597
|
+
// never report a successful launch (the door's own bridge abort handling settles the
|
|
598
|
+
// background tasks and clears the primed surfaces).
|
|
599
|
+
if (sig?.aborted) return reviewOutcomeResult({ status: "aborted" });
|
|
600
|
+
if (guidance !== null) return waveLaunchedResult(PLAN_SUBJECT, guidance);
|
|
601
|
+
// null = the synchronous port-pick failure (already loudly reported inside the core) —
|
|
602
|
+
// fall open to the plain blocking review in the same call: the review never wedges.
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (sig?.aborted) return reviewOutcomeResult({ status: "aborted" });
|
|
606
|
+
const source: ReviewSource = src.source === "plan-draft" ? "artifact" : "parameter";
|
|
607
|
+
const opened = slot.open(ctx, { subject: "plan", source, raw: src.plan, markdown: src.plan });
|
|
608
|
+
if (!opened.ok) return openRefusedResult(opened);
|
|
609
|
+
const review = opened.review;
|
|
610
|
+
// Browser edits arrive as the Direct Edits diff ON the outcome (applied feature-side); the
|
|
611
|
+
// reviewed bytes ride through unchanged.
|
|
612
|
+
const outcome = await bridge.review(review.markdown, sig);
|
|
613
|
+
if (sig?.aborted) return reviewOutcomeResult({ status: "aborted" });
|
|
614
|
+
if (outcome.status === "implement-here") return implementHereRefusedResult();
|
|
615
|
+
if (outcome.status !== "completed") return reviewOutcomeResult(outcome);
|
|
616
|
+
const check = checkDraftReviewDecision(
|
|
617
|
+
slot,
|
|
618
|
+
ctx,
|
|
619
|
+
review,
|
|
620
|
+
outcome.approved ? "save" : "revision",
|
|
621
|
+
);
|
|
622
|
+
if (check.kind !== "proceed") return planGuardResult(check, review, outcome.feedback);
|
|
623
|
+
return withDraftChangedNote(
|
|
624
|
+
await completePlanReviewV1(ctx, slot, deps, review.markdown, outcome, completion),
|
|
625
|
+
check.draftChanged,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// The first-party arm: the human's own edit write-back is the one legitimate draft change
|
|
630
|
+
// during a modal review, so the source is `editor` (no byte compare); the destination fence
|
|
631
|
+
// and the latch still apply to an approval.
|
|
632
|
+
const opened = slot.open(ctx, {
|
|
633
|
+
subject: "plan",
|
|
634
|
+
source: "editor",
|
|
635
|
+
raw: src.plan,
|
|
636
|
+
markdown: src.plan,
|
|
637
|
+
});
|
|
638
|
+
if (!opened.ok) return openRefusedResult(opened);
|
|
639
|
+
const review = opened.review;
|
|
640
|
+
if (sig?.aborted) return reviewOutcomeResult({ status: "aborted" });
|
|
641
|
+
const reviewed = await firstPartyPlanReviewer(ctx, deps, nodeClaimed).review(src.plan, sig);
|
|
642
|
+
if (sig?.aborted) return reviewOutcomeResult({ status: "aborted" });
|
|
643
|
+
if (isApproval(reviewed.outcome) || reviewed.outcome.status === "denied") {
|
|
644
|
+
const check = checkDraftReviewDecision(
|
|
645
|
+
slot,
|
|
646
|
+
ctx,
|
|
647
|
+
review,
|
|
648
|
+
isApproval(reviewed.outcome) ? "save" : "revision",
|
|
649
|
+
);
|
|
650
|
+
if (check.kind !== "proceed")
|
|
651
|
+
return planGuardResult(check, review, approvalFeedback(reviewed.outcome));
|
|
652
|
+
}
|
|
653
|
+
const result = await completePlanReview(
|
|
654
|
+
{ ...deps, allowImplementHere: !nodeClaimed },
|
|
655
|
+
reviewed,
|
|
656
|
+
completion,
|
|
657
|
+
);
|
|
658
|
+
recordPlanSaveOutcome(slot, result);
|
|
659
|
+
return renderPlanReviewResult(ctx, deps, result);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Render the feature's review result as the model-facing tool result (byte-stable texts). */
|
|
663
|
+
export function renderPlanReviewResult(
|
|
664
|
+
ctx: ExtensionContext,
|
|
665
|
+
deps: PlanReviewV1Deps,
|
|
666
|
+
result: ReviewPlanDraftResult,
|
|
667
|
+
): ToolResult {
|
|
668
|
+
switch (result.status) {
|
|
669
|
+
case "noPlan":
|
|
670
|
+
return noPlanResult();
|
|
671
|
+
case "approvedSaved":
|
|
672
|
+
return approvedSaveResult(
|
|
673
|
+
completedOutcome(true, result),
|
|
674
|
+
{
|
|
675
|
+
status: "saved",
|
|
676
|
+
result: deps.renderSave(result.save.result),
|
|
677
|
+
gateExited: result.save.gateExited,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
paramMismatch: result.paramMismatch,
|
|
681
|
+
edited: result.edited,
|
|
682
|
+
directEditsFailed: result.directEditsFailed,
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
case "approvedSaveFailed":
|
|
686
|
+
return approvedSaveResult(
|
|
687
|
+
completedOutcome(true, result),
|
|
688
|
+
{
|
|
689
|
+
status: "save-failed",
|
|
690
|
+
result: deps.renderSave(result.save.result),
|
|
691
|
+
gateExited: false,
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
paramMismatch: result.paramMismatch,
|
|
695
|
+
edited: result.edited,
|
|
696
|
+
directEditsFailed: result.directEditsFailed,
|
|
697
|
+
},
|
|
698
|
+
);
|
|
699
|
+
case "approvedNoPlan":
|
|
700
|
+
// Defensively unreachable (the reviewed source is always non-blank) — the save-failed
|
|
701
|
+
// shape with the no-source error, never a throw.
|
|
702
|
+
return approvedSaveResult(
|
|
703
|
+
completedOutcome(true, result),
|
|
704
|
+
{ status: "no-plan" },
|
|
705
|
+
{
|
|
706
|
+
paramMismatch: result.paramMismatch,
|
|
707
|
+
edited: result.edited,
|
|
708
|
+
directEditsFailed: result.directEditsFailed,
|
|
709
|
+
},
|
|
710
|
+
);
|
|
711
|
+
case "implementHere": {
|
|
712
|
+
// The feature exited the gate through the seam; surface the one info line the manual
|
|
713
|
+
// `/implement-here` exit always reported (byte-identical notice, same scope/severity).
|
|
714
|
+
if (result.gateExited) report(ctx, "implement-here", "info", IMPLEMENT_HERE_EXIT_NOTICE);
|
|
715
|
+
return implementHereResult(
|
|
716
|
+
{ status: "implement-here", reviewId: result.reviewId },
|
|
717
|
+
{ gateExited: result.gateExited },
|
|
718
|
+
{ cwd: ctx.cwd, plan: result.plan, edited: result.edited },
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
case "implementHereRefused":
|
|
722
|
+
return implementHereRefusedResult();
|
|
723
|
+
case "denied":
|
|
724
|
+
return reviewOutcomeResult(completedOutcome(false, result));
|
|
725
|
+
case "dismissed":
|
|
726
|
+
return reviewOutcomeResult({ status: "dismissed" });
|
|
727
|
+
case "aborted":
|
|
728
|
+
return reviewOutcomeResult({ status: "aborted" });
|
|
729
|
+
case "unavailable":
|
|
730
|
+
return reviewOutcomeResult({ status: "unavailable", warning: result.warning });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* The `plan_review` execute core — the STAGE DISPATCHER (exported for the offline tests). Arm
|
|
736
|
+
* order: param decode → the objective arm (`executeObjectiveReview` — the rendered objective
|
|
737
|
+
* draft is the review subject in BOTH objective-authoring stages) → the gist arm
|
|
738
|
+
* (`runGistReviewV1` — the rendered gist draft) → the refinement arm (`runRefinementReviewV1`
|
|
739
|
+
* — the rendered (draft, context) pair) → the plan arm (`runPlanReviewV1`).
|
|
740
|
+
*/
|
|
741
|
+
export async function executePlanReview(
|
|
742
|
+
pi: ExtensionAPI,
|
|
743
|
+
ctx: ExtensionContext,
|
|
744
|
+
gating: ToolGating,
|
|
745
|
+
bridge: PlanReviewBridge,
|
|
746
|
+
slot: DraftReviewSlot,
|
|
747
|
+
deps: PlanReviewV1Deps,
|
|
748
|
+
params: unknown,
|
|
749
|
+
signal?: AbortSignal,
|
|
750
|
+
wave?: WaveLaunch,
|
|
751
|
+
): Promise<ToolResult> {
|
|
752
|
+
// Tool-boundary decode, in this tool's native fail-open vocabulary: a MISTYPED
|
|
753
|
+
// `plan` (or non-object params) skip-shapes (`reason: "bad_input"`) without reviewing; an
|
|
754
|
+
// ABSENT `plan` proceeds — the validated draft artifact is the preferred source.
|
|
755
|
+
const p = paramsOf(params);
|
|
756
|
+
const plan = p === null ? null : stringParam(p, "plan");
|
|
757
|
+
if (plan === null) {
|
|
758
|
+
return {
|
|
759
|
+
content: [
|
|
760
|
+
{
|
|
761
|
+
type: "text",
|
|
762
|
+
text: "plan_review takes { plan?: string } — omit it (the plan-draft artifact is preferred) or pass a string.",
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
details: {
|
|
766
|
+
ok: false,
|
|
767
|
+
error: "plan must be a string",
|
|
768
|
+
error_type: "bad_input",
|
|
769
|
+
status: "skipped",
|
|
770
|
+
reason: "bad_input",
|
|
771
|
+
},
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
// An objective-authoring session (objective-author OR objective-save — both stages'
|
|
775
|
+
// working draft is the objective draft, and neither carries `plan_draft`) → the objective
|
|
776
|
+
// review arm: the rendered objective draft is the sole review source; a well-typed `plan`
|
|
777
|
+
// param is ignored here — the plan-arm fallthrough could otherwise review/save an
|
|
778
|
+
// unrelated plan param from an objective session. A gist-author session likewise routes to
|
|
779
|
+
// the gist arm (the rendered gist draft).
|
|
780
|
+
const launchedStage = rebuildWorkflowState(branchOf(ctx)).stage;
|
|
781
|
+
if (launchedStage === OBJECTIVE_AUTHOR_STAGE || launchedStage === OBJECTIVE_SAVE_STAGE) {
|
|
782
|
+
return executeObjectiveReview(pi, ctx, gating, bridge, slot, signal ?? ctx.signal, wave);
|
|
783
|
+
}
|
|
784
|
+
if (launchedStage === GIST_AUTHOR_STAGE) {
|
|
785
|
+
return runGistReviewV1(pi, ctx, gating, bridge, slot, signal ?? ctx.signal);
|
|
786
|
+
}
|
|
787
|
+
// A refinement session routes to the refinement arm BEFORE the plan arm: the validated
|
|
788
|
+
// (draft, context) pair is the sole source; a well-typed `plan` param is ignored (the plan
|
|
789
|
+
// fallthrough could otherwise review/save an unrelated plan from a refinement session).
|
|
790
|
+
if (launchedStage === REFINE_STAGE) {
|
|
791
|
+
return runRefinementReviewV1(pi, ctx, gating, bridge, slot, signal ?? ctx.signal);
|
|
792
|
+
}
|
|
793
|
+
return runPlanReviewV1(ctx, bridge, slot, deps, plan, signal, wave);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* The plan completion shared by the Plannotator tool arm and the browser door: a completed
|
|
798
|
+
* bridge outcome over the reviewed bytes → the feature completion (Direct-Edits apply ladder,
|
|
799
|
+
* D1a approval save; implement-here never allowed here — the bridge cannot produce it) → the
|
|
800
|
+
* latch record → the rendered tool result. The caller has already run the decision ladder.
|
|
801
|
+
*/
|
|
802
|
+
export async function completePlanReviewV1(
|
|
803
|
+
ctx: ExtensionContext,
|
|
804
|
+
slot: DraftReviewSlot,
|
|
805
|
+
deps: PlanReviewV1Deps,
|
|
806
|
+
markdown: string,
|
|
807
|
+
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
808
|
+
completion: { source: "plan-draft" | "param"; paramMismatch: boolean } = {
|
|
809
|
+
source: "plan-draft",
|
|
810
|
+
paramMismatch: false,
|
|
811
|
+
},
|
|
812
|
+
): Promise<ToolResult> {
|
|
813
|
+
const result = await completePlanReview(
|
|
814
|
+
{ ...deps, allowImplementHere: false },
|
|
815
|
+
{ outcome: planOutcomeOf(outcome), plan: markdown, edited: false },
|
|
816
|
+
completion,
|
|
817
|
+
);
|
|
818
|
+
recordPlanSaveOutcome(slot, result);
|
|
819
|
+
return renderPlanReviewResult(ctx, deps, result);
|
|
820
|
+
}
|