@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
|
@@ -1,1237 +0,0 @@
|
|
|
1
|
-
// The backend-neutral `plan_review` review door. perk's UNIVERSAL
|
|
2
|
-
// plan-review surface: the model calls ONE tool; this module dispatches to the configured review
|
|
3
|
-
// backend. Plannotator-selected → the event-bus bridge (`createPlannotatorBridge`,
|
|
4
|
-
// planAdapterPlannotator.ts — the AUGMENT-posture path, byte-stable); ANY other
|
|
5
|
-
// selection (perk-plan, tombell, unknown ids) → the FIRST-PARTY in-TUI editor review
|
|
6
|
-
// (`runFirstPartyReview`): display the draft in pi's built-in `ctx.ui.editor` dialog (scrollable,
|
|
7
|
-
// Ctrl+G opens the user's external $EDITOR), write optional human edits back to the draft via
|
|
8
|
-
// `writePlanDraft` BEFORE the verdict (reviewed bytes == artifact bytes == saved bytes — a failed
|
|
9
|
-
// write-back ABORTS the review fail-open, nothing saved), then an approve/deny/skip
|
|
10
|
-
// `ctx.ui.select` verdict — on the plan arm with a 4th "Implement here — no issue saved" option
|
|
11
|
-
// (§8.23; suppressed in objective-node planning sessions) — with deny feedback via a second
|
|
12
|
-
// editor dialog.
|
|
13
|
-
//
|
|
14
|
-
// REVIEW SEMANTICS (file-first, approval auto-saves): the review runs while the session is still
|
|
15
|
-
// read-only (the tool is in READ_ONLY_TOOLS — review happens before the gate ever comes off).
|
|
16
|
-
// The reviewed plan resolves FILE-FIRST via `resolvePlanSource` (the validated `plan-draft.md`
|
|
17
|
-
// artifact wins; the `plan` param is the fallback; the transcript scrape is NEVER reviewed — an
|
|
18
|
-
// approval would auto-save scraped conversation bytes, so no draft + no param soft-skips with a
|
|
19
|
-
// `plan_draft` redirect). An APPROVED outcome (either backend) wires into the shared
|
|
20
|
-
// `approvalSave` seam (planSave.ts): auto-save → D1a gate exit → terminating result,
|
|
21
|
-
// node link recovered from the `objective_node_claim` carrier inside `savePlan`. A DENY returns
|
|
22
|
-
// feedback and directs a `plan_draft` rewrite + re-review. Plannotator's browser "Direct Edits"
|
|
23
|
-
// (a `# Direct Edits` unified diff opening the feedback) are handled asymmetrically per arm: the
|
|
24
|
-
// PLAN arm mechanically applies an approved diff (strict apply → draft write-back → save the
|
|
25
|
-
// edited bytes; any failure falls open to the verbatim save + a loud warning); the OBJECTIVE arm
|
|
26
|
-
// cannot fold rendered-markdown edits into the structured draft, so an approve-with-edits SKIPS
|
|
27
|
-
// the save and returns one model-mediated revise round; DENY stays model-mediated on both arms. Strict on deny, FAIL-OPEN everywhere
|
|
28
|
-
// else: headless / dismissed (Esc anywhere = skip, mirroring ask_user_question's dismissal — deny
|
|
29
|
-
// is always explicit) / backend-unavailable all soft-skip so plan authoring never wedges — those
|
|
30
|
-
// arms keep the present-the-plan + human-`/plan-save` discipline (the manual failsafe).
|
|
31
|
-
//
|
|
32
|
-
// THE LAUNCH CHOOSER (plannotator arms only, §8.23): on an eligible round (injected `WaveLaunch`
|
|
33
|
-
// deps present, plannotator loaded, the review source a validated draft artifact) the tool first
|
|
34
|
-
// asks the human — "Browser review + reviewer wave" vs "Browser review only" — before anything
|
|
35
|
-
// launches. The wave choice collects an optional trimmed custom angle, delegates to the door's
|
|
36
|
-
// guidance-returning open core (openPlanReviewSurface / openObjectiveReviewSurface, injected —
|
|
37
|
-
// never imported: doors value-import this module), and returns the NON-terminating
|
|
38
|
-
// `wave_launched` result carrying the door guidance verbatim; the browser decision then routes
|
|
39
|
-
// through the door's background decision task. Esc ⇒ the plain flavor (never a cancel); abort
|
|
40
|
-
// outranks every dialog result AND the awaited opener (an interrupted turn never reports a
|
|
41
|
-
// launched wave); a null opener return (synchronous port-pick failure, loudly reported in the
|
|
42
|
-
// core) falls open to the plain blocking review in the same call.
|
|
43
|
-
//
|
|
44
|
-
// `ctx.ui.editor` takes NO AbortSignal (unlike select/confirm/input) — `signal?.aborted` is
|
|
45
|
-
// checked between dialogs; an in-flight editor dialog survives a turn abort and its result is
|
|
46
|
-
// discarded (the aborted arm wins). Enter submits in the editor dialog (Shift+Enter = newline),
|
|
47
|
-
// so the dialog titles carry the key hints — pi renders no other affordance.
|
|
48
|
-
//
|
|
49
|
-
// THE OBJECTIVE ARM: an objective-authoring session (read-only, stage `objective-author` or
|
|
50
|
-
// `objective-save` — the two stages whose working draft IS the objective draft; neither carries
|
|
51
|
-
// `plan_draft`) routes through `executeObjectiveReview` instead of the plan path — the
|
|
52
|
-
// reviewed bytes are the RENDERED objective draft (`readObjectiveDraft` + `renderObjectiveDraft`,
|
|
53
|
-
// objectiveDraft.ts — never raw JSON, never the `plan` param, never the transcript; no draft
|
|
54
|
-
// soft-skips with `reason: "no_objective_draft"`). Dispatch mirrors the plan path (plannotator
|
|
55
|
-
// bridge or the first-party editor, VIEW-ONLY — edits are never written back; deny+feedback is
|
|
56
|
-
// the change channel). An APPROVED outcome wires into the `objectiveApprovalSave` seam
|
|
57
|
-
// (objectiveSave.ts): re-read the STRUCTURED artifact → `saveObjective` → D1a gate
|
|
58
|
-
// exit → a TERMINATING result; a failed save is non-terminating, leaves the gate read-only, and
|
|
59
|
-
// directs the human `/objective-save` failsafe.
|
|
60
|
-
//
|
|
61
|
-
// THE GIST ARM: a gist-author session (read-only, stage `gist-author`) routes through
|
|
62
|
-
// `executeGistReview` the same way — the reviewed bytes are the RENDERED gist draft
|
|
63
|
-
// (`readGistDraft` + `renderGistDraft`, gistDraft.ts), first-party VIEW-ONLY, implement-here
|
|
64
|
-
// never offered, APPROVED → the `gistApprovalSave` seam (gistSave.ts), no draft soft-skips with
|
|
65
|
-
// `reason: "no_gist_draft"`. Plannotator approve-with-Direct-Edits mirrors the objective arm:
|
|
66
|
-
// the save seam re-reads the STRUCTURED gist artifact, so rendered edits cannot be folded back
|
|
67
|
-
// mechanically — the arm SKIPS the save and returns one model-mediated revise round (fold each
|
|
68
|
-
// hunk into the matching `gist_draft` field — title heading → `title`, `Scope:` line → `scope`,
|
|
69
|
-
// prose → `prose` — then re-review to confirm).
|
|
70
|
-
//
|
|
71
|
-
// INVARIANTS HELD: never calls `setActiveTools`, never registers a `tool_call` handler, never
|
|
72
|
-
// restamps `cache.plan-ref.provider`. The door composes the gate AND the save EXCLUSIVELY
|
|
73
|
-
// through the `approvalSave` seam (Invariant 1: composes, never owns).
|
|
74
|
-
|
|
75
|
-
import { randomUUID } from "node:crypto";
|
|
76
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
77
|
-
import {
|
|
78
|
-
createPlannotatorBridge,
|
|
79
|
-
extractDirectEdits,
|
|
80
|
-
hasDirectEditsHeading,
|
|
81
|
-
isPlannotatorPlanSelected,
|
|
82
|
-
} from "../adapters/planAdapterPlannotator.ts";
|
|
83
|
-
import type { Result } from "../substrate/result.ts";
|
|
84
|
-
import { readSessionArtifact } from "../substrate/sessionData.ts";
|
|
85
|
-
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
86
|
-
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
87
|
-
import { applyUnifiedDiff } from "../substrate/unifiedDiff.ts";
|
|
88
|
-
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
89
|
-
import { GIST_AUTHOR_STAGE } from "./gistAuthor.ts";
|
|
90
|
-
import { readGistDraft, renderGistDraft } from "./gistDraft.ts";
|
|
91
|
-
import { type GistApprovalSaveOutcome, gistApprovalSave } from "./gistSave.ts";
|
|
92
|
-
import { implementHereExit, implementHereGuidance } from "./implementHere.ts";
|
|
93
|
-
import { OBJECTIVE_AUTHOR_STAGE } from "./objectiveAuthor.ts";
|
|
94
|
-
import {
|
|
95
|
-
OBJECTIVE_DRAFT_ARTIFACT,
|
|
96
|
-
readObjectiveDraft,
|
|
97
|
-
renderObjectiveDraft,
|
|
98
|
-
} from "./objectiveDraft.ts";
|
|
99
|
-
import { readNodeClaim } from "./objectivePlan.ts";
|
|
100
|
-
import {
|
|
101
|
-
OBJECTIVE_SAVE_STAGE,
|
|
102
|
-
type ObjectiveApprovalSaveOutcome,
|
|
103
|
-
objectiveApprovalSave,
|
|
104
|
-
} from "./objectiveSave.ts";
|
|
105
|
-
import { writePlanDraft } from "./planDraft.ts";
|
|
106
|
-
import { type ApprovalSaveOutcome, approvalSave, resolvePlanSource } from "./planSave.ts";
|
|
107
|
-
|
|
108
|
-
// ----------------------------------------------------------------------------- review outcomes
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* The review outcome a backend produces, mapped into a tool result below (also `details.status`).
|
|
112
|
-
* The `dismissed` arm is FIRST-PARTY ONLY (Esc anywhere = fail-open skip; the plannotator bridge
|
|
113
|
-
* never produces it). The `implement-here` arm is first-party PLAN-arm only (the human chose the
|
|
114
|
-
* no-save exit — contracts.md §8.23); the plannotator bridge never produces it (its browser
|
|
115
|
-
* envelope returns only approve/deny) and the objective arm never offers it.
|
|
116
|
-
*/
|
|
117
|
-
export type ReviewOutcome =
|
|
118
|
-
| { status: "unavailable"; warning: string }
|
|
119
|
-
| { status: "aborted" }
|
|
120
|
-
| { status: "dismissed" }
|
|
121
|
-
| { status: "implement-here"; reviewId: string }
|
|
122
|
-
| { status: "completed"; approved: boolean; feedback?: string; reviewId: string };
|
|
123
|
-
|
|
124
|
-
interface ToolResult {
|
|
125
|
-
content: { type: "text"; text: string }[];
|
|
126
|
-
details: Record<string, unknown>;
|
|
127
|
-
terminate?: boolean;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The subject descriptor parameterizing the shared renderer cores below — the plan and objective
|
|
132
|
-
* review arms render the same outcome shapes, differing only in these fields. Module-private on
|
|
133
|
-
* purpose: nothing outside this module needs it (both execute arms live here), and a third review
|
|
134
|
-
* subject would land here too, constructing its own descriptor and reusing the cores.
|
|
135
|
-
*/
|
|
136
|
-
interface ReviewSubject {
|
|
137
|
-
/** The display noun in every rendered text ("plan" / "objective"). */
|
|
138
|
-
noun: string;
|
|
139
|
-
/** The lowercase present-the-work phrase (dismissed / implement-here arms). */
|
|
140
|
-
present: string;
|
|
141
|
-
/** The unavailable-arm phrase (the plan flavor appends "in your next message"). */
|
|
142
|
-
presentUnavailable: string;
|
|
143
|
-
/** Where an implement-here verdict "cannot" have come from (the defensive arm's text). */
|
|
144
|
-
implementHereWhere: string;
|
|
145
|
-
/** The draft-rewrite tool the DENIED text redirects to. */
|
|
146
|
-
draftTool: string;
|
|
147
|
-
/** The manual-failsafe slash command. */
|
|
148
|
-
failsafeCmd: string;
|
|
149
|
-
/** Extra keys merged into every details object ({} on the plan arm). */
|
|
150
|
-
detailsExtra: Record<string, unknown>;
|
|
151
|
-
/** The defensively-unreachable no-source save arm's error string. */
|
|
152
|
-
noSourceError: string;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const PLAN_SUBJECT: ReviewSubject = {
|
|
156
|
-
noun: "plan",
|
|
157
|
-
present: "the complete plan to the user",
|
|
158
|
-
presentUnavailable: "the complete plan to the user in your next message",
|
|
159
|
-
implementHereWhere: "outside the execute path",
|
|
160
|
-
draftTool: "plan_draft",
|
|
161
|
-
failsafeCmd: "/plan-save",
|
|
162
|
-
detailsExtra: {},
|
|
163
|
-
noSourceError: "no plan source resolved",
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const OBJECTIVE_SUBJECT: ReviewSubject = {
|
|
167
|
-
noun: "objective",
|
|
168
|
-
present: "the complete objective + structured roadmap to the user",
|
|
169
|
-
presentUnavailable: "the complete objective + structured roadmap to the user",
|
|
170
|
-
implementHereWhere: "on the objective path",
|
|
171
|
-
draftTool: "objective_draft",
|
|
172
|
-
failsafeCmd: "/objective-save",
|
|
173
|
-
detailsExtra: { subject: "objective" },
|
|
174
|
-
noSourceError: "no objective draft resolved",
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const GIST_SUBJECT: ReviewSubject = {
|
|
178
|
-
noun: "gist",
|
|
179
|
-
present: "the complete gist to the user",
|
|
180
|
-
presentUnavailable: "the complete gist to the user",
|
|
181
|
-
implementHereWhere: "on the gist path",
|
|
182
|
-
draftTool: "gist_draft",
|
|
183
|
-
failsafeCmd: "/gist-save",
|
|
184
|
-
detailsExtra: { subject: "gist" },
|
|
185
|
-
noSourceError: "no gist draft resolved",
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const SKIP_TEXT =
|
|
189
|
-
"no interactive review surface available — present the complete plan to the user in your next message.";
|
|
190
|
-
|
|
191
|
-
function skipResult(): ToolResult {
|
|
192
|
-
return { content: [{ type: "text", text: SKIP_TEXT }], details: { ok: true, status: "skipped" } };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* The shared outcome-mapper core: map a non-approved review outcome into the model-facing tool
|
|
197
|
-
* result for `subject`. The `completed` case renders the DENIED text — both execute paths route
|
|
198
|
-
* approved outcomes to their approved-save mapper FIRST, so callers only reach `completed` here
|
|
199
|
-
* with `approved: false` (kept total for safety; the `approved: outcome.approved` passthrough is
|
|
200
|
-
* deliberately behavior-preserving — never hardcode `false`). The `dismissed` arm renders as a
|
|
201
|
-
* skip — the human declined to decide, so the present-the-work + manual-failsafe discipline
|
|
202
|
-
* applies.
|
|
203
|
-
*/
|
|
204
|
-
function subjectReviewOutcomeResult(subject: ReviewSubject, outcome: ReviewOutcome): ToolResult {
|
|
205
|
-
switch (outcome.status) {
|
|
206
|
-
case "unavailable":
|
|
207
|
-
return {
|
|
208
|
-
content: [
|
|
209
|
-
{
|
|
210
|
-
type: "text",
|
|
211
|
-
text:
|
|
212
|
-
`WARNING: ${outcome.warning} — no review performed. ` +
|
|
213
|
-
`Present ${subject.presentUnavailable} instead.`,
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
details: {
|
|
217
|
-
ok: false,
|
|
218
|
-
error: outcome.warning,
|
|
219
|
-
error_type: "unavailable",
|
|
220
|
-
status: "unavailable",
|
|
221
|
-
...subject.detailsExtra,
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
case "aborted":
|
|
225
|
-
return {
|
|
226
|
-
content: [{ type: "text", text: `${subject.noun} review aborted (turn interrupted).` }],
|
|
227
|
-
details: { ok: true, status: "aborted", ...subject.detailsExtra },
|
|
228
|
-
};
|
|
229
|
-
case "dismissed":
|
|
230
|
-
return {
|
|
231
|
-
content: [
|
|
232
|
-
{
|
|
233
|
-
type: "text",
|
|
234
|
-
text:
|
|
235
|
-
`${subject.noun} review dismissed — present ${subject.present}; the human runs ` +
|
|
236
|
-
`${subject.failsafeCmd} (the manual failsafe).`,
|
|
237
|
-
},
|
|
238
|
-
],
|
|
239
|
-
details: { ok: true, status: "skipped", reason: "dismissed", ...subject.detailsExtra },
|
|
240
|
-
};
|
|
241
|
-
case "implement-here":
|
|
242
|
-
// Defensively unreachable: the plan execute path routes implement-here to
|
|
243
|
-
// implementHereResult FIRST (mirror the approved-first routing), and the objective arm
|
|
244
|
-
// never offers the verdict. Map to a skip shape rather than throwing.
|
|
245
|
-
return {
|
|
246
|
-
content: [
|
|
247
|
-
{
|
|
248
|
-
type: "text",
|
|
249
|
-
text:
|
|
250
|
-
`implement-here verdict received ${subject.implementHereWhere} — nothing saved; ` +
|
|
251
|
-
`present ${subject.present}.`,
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
details: { ok: true, status: "skipped", reason: "implement-here", ...subject.detailsExtra },
|
|
255
|
-
};
|
|
256
|
-
case "completed": {
|
|
257
|
-
const feedback = outcome.feedback ? `\n\nReviewer feedback:\n${outcome.feedback}` : "";
|
|
258
|
-
const text =
|
|
259
|
-
`${subject.noun} DENIED — revise per this feedback, rewrite the working draft with ` +
|
|
260
|
-
`${subject.draftTool}, then call plan_review again.${feedback}`;
|
|
261
|
-
return {
|
|
262
|
-
content: [{ type: "text", text }],
|
|
263
|
-
details: {
|
|
264
|
-
ok: true,
|
|
265
|
-
status: "completed",
|
|
266
|
-
approved: outcome.approved,
|
|
267
|
-
feedback: outcome.feedback ?? null,
|
|
268
|
-
reviewId: outcome.reviewId,
|
|
269
|
-
...subject.detailsExtra,
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Map a non-approved review outcome into the model-facing tool result (exported for the offline
|
|
278
|
-
* tests) — the plan flavor of `subjectReviewOutcomeResult`. The execute path routes approved
|
|
279
|
-
* outcomes to `approvedSaveResult` first, so `completed` renders DENIED here.
|
|
280
|
-
*/
|
|
281
|
-
export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
282
|
-
return subjectReviewOutcomeResult(PLAN_SUBJECT, outcome);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* The normalized approval-save outcome the shared core consumes — each delegator maps its
|
|
287
|
-
* subject-specific no-source discriminant (`no-plan` / `no-draft`) onto `no-source`. `result`
|
|
288
|
-
* widens to `Result<object>`: the core reads only `content[0]?.text` and the `details.ok`
|
|
289
|
-
* discriminant (+ `details.error` on the fail arm), passing `details` through opaquely.
|
|
290
|
-
*/
|
|
291
|
-
type SubjectSaveOutcome =
|
|
292
|
-
| { status: "no-source" }
|
|
293
|
-
| { status: "saved" | "save-failed"; result: Result<object>; gateExited: boolean };
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* The shared approved-save mapper core: map an APPROVED review outcome + the approval-save
|
|
297
|
-
* outcome into the model-facing tool result for `subject`. A successful save TERMINATES the turn
|
|
298
|
-
* (propagating the seam's `terminate: true` intent); a failed save is non-terminating, leaves
|
|
299
|
-
* the gate read-only, and directs the human manual failsafe. Reviewer feedback is surfaced
|
|
300
|
-
* loudly as implementation guidance — the approved bytes were saved verbatim, never post-edited.
|
|
301
|
-
* The `paramMismatch`/`edited`/`directEditsFailed` opts are plan-arm-only (their literals name
|
|
302
|
-
* "plan"/"draft"): the objective delegator never passes opts, so the suffixes render empty and
|
|
303
|
-
* `edited` never reaches its details. `directEditsFailed` (plannotator-only) flags that a Direct
|
|
304
|
-
* Edits section was seen but could not be honored — the saved arm gains a loud warning that the
|
|
305
|
-
* plan was saved WITHOUT the reviewer's edits, and details carry `direct_edits_applied: false`.
|
|
306
|
-
* The `no-source` arm is defensively unreachable (the reviewed source is always
|
|
307
|
-
* non-blank) but maps to the save-failed shape rather than throwing.
|
|
308
|
-
*/
|
|
309
|
-
function approvedSubjectSaveResult(
|
|
310
|
-
subject: ReviewSubject,
|
|
311
|
-
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
312
|
-
save: SubjectSaveOutcome,
|
|
313
|
-
opts?: { paramMismatch?: boolean; edited?: boolean; directEditsFailed?: boolean },
|
|
314
|
-
): ToolResult {
|
|
315
|
-
const feedback = outcome.feedback
|
|
316
|
-
? `\n\nReviewer feedback (implementation guidance — the approved ${subject.noun} was saved ` +
|
|
317
|
-
`verbatim):\n${outcome.feedback}`
|
|
318
|
-
: "";
|
|
319
|
-
const base = {
|
|
320
|
-
status: "completed",
|
|
321
|
-
approved: true,
|
|
322
|
-
reviewId: outcome.reviewId,
|
|
323
|
-
feedback: outcome.feedback ?? null,
|
|
324
|
-
...subject.detailsExtra,
|
|
325
|
-
...(opts?.edited === true ? { edited: true } : {}),
|
|
326
|
-
...(opts?.directEditsFailed === true ? { direct_edits_applied: false } : {}),
|
|
327
|
-
};
|
|
328
|
-
if (save.status === "saved") {
|
|
329
|
-
const saveText = save.result.content[0]?.text ?? "";
|
|
330
|
-
const edited =
|
|
331
|
-
opts?.edited === true ? " · human edits were written back to the draft and saved" : "";
|
|
332
|
-
const mismatch =
|
|
333
|
-
opts?.paramMismatch === true
|
|
334
|
-
? "\n\n⚠ differing plan param ignored — the validated draft was reviewed and saved."
|
|
335
|
-
: "";
|
|
336
|
-
const editsWarning =
|
|
337
|
-
opts?.directEditsFailed === true
|
|
338
|
-
? "\n\n⚠ WARNING: the reviewer's Direct Edits could NOT be auto-applied — the plan was " +
|
|
339
|
-
"saved WITHOUT them. The diff remains in the reviewer feedback above; apply it to the " +
|
|
340
|
-
"plan issue manually or via a follow-up."
|
|
341
|
-
: "";
|
|
342
|
-
return {
|
|
343
|
-
content: [
|
|
344
|
-
{
|
|
345
|
-
type: "text",
|
|
346
|
-
text: `${subject.noun} APPROVED by reviewer.${feedback}\n\n${saveText}${edited}${mismatch}${editsWarning}`,
|
|
347
|
-
},
|
|
348
|
-
],
|
|
349
|
-
// `ok` sits per-branch, NOT in `base` — `base` is spread into the fail branch too.
|
|
350
|
-
details: {
|
|
351
|
-
ok: true,
|
|
352
|
-
...base,
|
|
353
|
-
saved: true,
|
|
354
|
-
gateExited: save.gateExited,
|
|
355
|
-
save: save.result.details,
|
|
356
|
-
},
|
|
357
|
-
terminate: true,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
const error =
|
|
361
|
-
save.status === "no-source"
|
|
362
|
-
? subject.noSourceError
|
|
363
|
-
: save.result.details.ok
|
|
364
|
-
? "unknown save failure"
|
|
365
|
-
: save.result.details.error;
|
|
366
|
-
return {
|
|
367
|
-
content: [
|
|
368
|
-
{
|
|
369
|
-
type: "text",
|
|
370
|
-
text:
|
|
371
|
-
`${subject.noun} APPROVED by reviewer, but the auto-save FAILED (${error}) — the ` +
|
|
372
|
-
`session stays read-only. Ask the user to run ${subject.failsafeCmd} (the manual ` +
|
|
373
|
-
`failsafe) to retry.${feedback}`,
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
details: {
|
|
377
|
-
ok: false,
|
|
378
|
-
error,
|
|
379
|
-
error_type: "save_failed",
|
|
380
|
-
...base,
|
|
381
|
-
saved: false,
|
|
382
|
-
save: save.status === "no-source" ? null : save.result.details,
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Map an APPROVED review outcome + the `approvalSave` outcome into the model-facing tool result
|
|
389
|
-
* (exported for the offline tests) — the plan flavor of `approvedSubjectSaveResult`. `edited`
|
|
390
|
-
* flags that human edits were written back to the draft pre-verdict (the first-party editor, or
|
|
391
|
-
* the plannotator Direct Edits auto-apply), so the saved bytes carry them. `directEditsFailed`
|
|
392
|
-
* (plannotator-only, optional — absent keeps every existing call site byte-stable) flags a
|
|
393
|
-
* Direct Edits section that could not be honored: the plan saved verbatim, a loud warning added.
|
|
394
|
-
*/
|
|
395
|
-
export function approvedSaveResult(
|
|
396
|
-
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
397
|
-
save: ApprovalSaveOutcome,
|
|
398
|
-
opts: { paramMismatch: boolean; edited?: boolean; directEditsFailed?: boolean },
|
|
399
|
-
): ToolResult {
|
|
400
|
-
return approvedSubjectSaveResult(
|
|
401
|
-
PLAN_SUBJECT,
|
|
402
|
-
outcome,
|
|
403
|
-
save.status === "no-plan" ? { status: "no-source" } : save,
|
|
404
|
-
opts,
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Map an IMPLEMENT-HERE review outcome + the `implementHereExit` outcome into the model-facing
|
|
410
|
-
* tool result (exported for the offline tests). NON-terminating on purpose — the model continues
|
|
411
|
-
* the turn and implements immediately. The text is the implement-here guidance; when the human
|
|
412
|
-
* edited the plan during review (`edited`), the final reviewed bytes are inlined so the model
|
|
413
|
-
* implements THOSE, not its stale in-context version (the draft write-back already happened
|
|
414
|
-
* pre-verdict). Nothing is saved: no issue, no plan-ref, the draft artifact intact (§8.23).
|
|
415
|
-
*/
|
|
416
|
-
export function implementHereResult(
|
|
417
|
-
outcome: Extract<ReviewOutcome, { status: "implement-here" }>,
|
|
418
|
-
exit: { gateExited: boolean },
|
|
419
|
-
opts: { cwd: string; plan: string; edited: boolean },
|
|
420
|
-
): ToolResult {
|
|
421
|
-
return {
|
|
422
|
-
content: [
|
|
423
|
-
{
|
|
424
|
-
type: "text",
|
|
425
|
-
text: implementHereGuidance(opts.cwd, { editedPlan: opts.edited ? opts.plan : undefined }),
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
details: {
|
|
429
|
-
ok: true,
|
|
430
|
-
status: "implement-here",
|
|
431
|
-
saved: false,
|
|
432
|
-
gateExited: exit.gateExited,
|
|
433
|
-
reviewId: outcome.reviewId,
|
|
434
|
-
...(opts.edited ? { edited: true } : {}),
|
|
435
|
-
},
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// ------------------------------------------------------ the plannotator Direct-Edits apply
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* The shared plannotator APPROVE mechanical-apply path (contracts.md §8.23): inspect an
|
|
443
|
-
* APPROVED outcome's feedback for a `# Direct Edits` section and mechanically apply the
|
|
444
|
-
* reviewer's diff to the exact bytes reviewed (`basePlan`), writing the patched bytes back to
|
|
445
|
-
* the draft (reviewed bytes == artifact bytes == saved bytes). Consumed by `executePlanReview`'s
|
|
446
|
-
* plannotator arm AND the `/plan-review-browser` door — one apply path, byte-identical
|
|
447
|
-
* semantics:
|
|
448
|
-
*
|
|
449
|
-
* - only an `approved` outcome WITH feedback is inspected (anything else passes through
|
|
450
|
-
* verbatim);
|
|
451
|
-
* - a clean extract + apply + write-back swaps `reviewedPlan` to the patched bytes, sets
|
|
452
|
-
* `edited: true`, and strips the applied section from the returned outcome's feedback (only
|
|
453
|
-
* the annotation remainder survives — the applied diff must never render as "apply these
|
|
454
|
-
* exact changes" guidance);
|
|
455
|
-
* - a seen-but-unhonorable heading (or a failed apply / write-back) sets
|
|
456
|
-
* `directEditsFailed: true` with the plan left verbatim (the caller renders the loud warning;
|
|
457
|
-
* the diff stays in the surfaced feedback for a manual follow-up).
|
|
458
|
-
*/
|
|
459
|
-
export function applyPlannotatorDirectEdits(
|
|
460
|
-
pi: ExtensionAPI,
|
|
461
|
-
ctx: ExtensionContext,
|
|
462
|
-
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
463
|
-
basePlan: string,
|
|
464
|
-
): {
|
|
465
|
-
outcome: Extract<ReviewOutcome, { status: "completed" }>;
|
|
466
|
-
reviewedPlan: string;
|
|
467
|
-
edited: boolean;
|
|
468
|
-
directEditsFailed: boolean;
|
|
469
|
-
} {
|
|
470
|
-
if (!outcome.approved || outcome.feedback === undefined) {
|
|
471
|
-
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: false };
|
|
472
|
-
}
|
|
473
|
-
const section = extractDirectEdits(outcome.feedback);
|
|
474
|
-
if (section !== null) {
|
|
475
|
-
const patched = applyUnifiedDiff(basePlan, section.diff);
|
|
476
|
-
if (patched !== null && writePlanDraft(pi, ctx, patched).details.ok) {
|
|
477
|
-
return {
|
|
478
|
-
outcome: { ...outcome, feedback: section.remainder },
|
|
479
|
-
reviewedPlan: patched,
|
|
480
|
-
edited: true,
|
|
481
|
-
directEditsFailed: false,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: true };
|
|
485
|
-
}
|
|
486
|
-
if (hasDirectEditsHeading(outcome.feedback)) {
|
|
487
|
-
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: true };
|
|
488
|
-
}
|
|
489
|
-
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: false };
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ------------------------------------------------------ the launch chooser (the wave arm)
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* The injected wave-launch deps (composed in index.ts from the door exports — structural on
|
|
496
|
-
* purpose: this module imports NOTHING from door modules, avoiding the value-import cycle;
|
|
497
|
-
* `planReviewBrowser.ts` already value-imports this module). `present` is the plannotator
|
|
498
|
-
* presence probe (`plannotatorPresent(pi)` at the call site); `plan`/`objective` are the
|
|
499
|
-
* guidance-returning door open cores (`openPlanReviewSurface` / `openObjectiveReviewSurface`) —
|
|
500
|
-
* one open path, byte-identical door semantics (contracts.md §8.23). `null` from an opener is
|
|
501
|
-
* the synchronous port-pick failure (already loudly reported inside the core) — the caller
|
|
502
|
-
* falls open to the plain blocking review.
|
|
503
|
-
*/
|
|
504
|
-
export interface WaveLaunch {
|
|
505
|
-
present(): boolean;
|
|
506
|
-
plan(ctx: ExtensionContext, opts: { draft: string; custom?: string }): Promise<string | null>;
|
|
507
|
-
objective(
|
|
508
|
-
ctx: ExtensionContext,
|
|
509
|
-
opts: { rendered: string; artifactRaw: string; custom?: string },
|
|
510
|
-
): Promise<string | null>;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/** The minimal structural `ctx.ui` subset the launch chooser needs (both dialogs signal-aware). */
|
|
514
|
-
export interface ReviewLaunchUI {
|
|
515
|
-
select(
|
|
516
|
-
title: string,
|
|
517
|
-
options: string[],
|
|
518
|
-
opts?: { signal?: AbortSignal },
|
|
519
|
-
): Promise<string | undefined>;
|
|
520
|
-
input(
|
|
521
|
-
title: string,
|
|
522
|
-
placeholder?: string,
|
|
523
|
-
opts?: { signal?: AbortSignal },
|
|
524
|
-
): Promise<string | undefined>;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** The launch chooser's outcome: the review flavor (never a cancel), or the aborted turn. */
|
|
528
|
-
export type ReviewLaunchChoice =
|
|
529
|
-
| { launch: "plain" }
|
|
530
|
-
| { launch: "wave"; custom?: string }
|
|
531
|
-
| { launch: "aborted" };
|
|
532
|
-
|
|
533
|
-
const LAUNCH_WAVE = "Browser review + reviewer wave";
|
|
534
|
-
const LAUNCH_PLAIN = "Browser review only";
|
|
535
|
-
const CUSTOM_ANGLE_TITLE = "Custom review angle (optional — Enter to skip)";
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* The launch chooser (pure over the injected ui slice — offline-testable): every eligible
|
|
539
|
-
* plannotator round asks the human whether the browser review launches WITH the streamed
|
|
540
|
-
* reviewer wave; the wave choice then asks for an optional custom review angle. Esc/dismiss
|
|
541
|
-
* anywhere selects a FLAVOR, never cancels the review (Esc at the chooser ⇒ plain; Esc/blank at
|
|
542
|
-
* the angle input ⇒ wave with no custom lane — the input is `.trim()`'d before blank detection,
|
|
543
|
-
* the door handlers' exact discipline). ABORT OUTRANKS EVERYTHING: `signal?.aborted` is checked
|
|
544
|
-
* at entry and re-checked immediately after each awaited dialog, BEFORE interpreting its result
|
|
545
|
-
* (the `runFirstPartyReview` discipline) — a conforming caller can never launch a browser or
|
|
546
|
-
* enter a blocking review after the turn was interrupted. No other return paths exist.
|
|
547
|
-
*/
|
|
548
|
-
export async function chooseReviewLaunch(
|
|
549
|
-
ui: ReviewLaunchUI,
|
|
550
|
-
subjectNoun: string,
|
|
551
|
-
signal?: AbortSignal,
|
|
552
|
-
): Promise<ReviewLaunchChoice> {
|
|
553
|
-
if (signal?.aborted) return { launch: "aborted" };
|
|
554
|
-
const picked = await ui.select(`${subjectNoun} review launch`, [LAUNCH_WAVE, LAUNCH_PLAIN], {
|
|
555
|
-
signal,
|
|
556
|
-
});
|
|
557
|
-
if (signal?.aborted) return { launch: "aborted" }; // abort outranks Esc AND any selection
|
|
558
|
-
if (picked !== LAUNCH_WAVE) return { launch: "plain" }; // Esc/dismiss = the plain flavor
|
|
559
|
-
const raw = await ui.input(CUSTOM_ANGLE_TITLE, undefined, { signal });
|
|
560
|
-
if (signal?.aborted) return { launch: "aborted" }; // abort outranks the input result too
|
|
561
|
-
const custom = (raw ?? "").trim();
|
|
562
|
-
return custom.length > 0 ? { launch: "wave", custom } : { launch: "wave" };
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* The NON-terminating wave-launched result: the door core's guidance rides back verbatim as the
|
|
567
|
-
* tool text (same templates, same binding suffix — the model behaves identically whether the
|
|
568
|
-
* human summoned the door or chose the wave inside `plan_review`), and the human's browser
|
|
569
|
-
* decision routes through the door's background decision task — never through this call.
|
|
570
|
-
*/
|
|
571
|
-
function waveLaunchedResult(subject: ReviewSubject, guidance: string): ToolResult {
|
|
572
|
-
return {
|
|
573
|
-
content: [{ type: "text", text: guidance }],
|
|
574
|
-
details: { ok: true, status: "wave_launched", ...subject.detailsExtra },
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// ----------------------------------------------------------------- the first-party review core
|
|
579
|
-
|
|
580
|
-
/** The minimal structural `ctx.ui` subset the first-party review needs (the ciExecutor.ts pure-core + injected-fakes recipe). */
|
|
581
|
-
export interface PlanReviewUI {
|
|
582
|
-
editor(title: string, prefill?: string): Promise<string | undefined>;
|
|
583
|
-
select(
|
|
584
|
-
title: string,
|
|
585
|
-
options: string[],
|
|
586
|
-
opts?: { signal?: AbortSignal },
|
|
587
|
-
): Promise<string | undefined>;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Derive a subject's verdict options (plain text — charter D3: no emoji outside the footer);
|
|
592
|
-
* only the skip label's manual-failsafe command varies by subject. `VERDICT_IMPLEMENT_HERE`
|
|
593
|
-
* stays a standalone constant on purpose — the no-save exit is plan-arm-only by contract
|
|
594
|
-
* (§8.23), never part of the descriptor.
|
|
595
|
-
*/
|
|
596
|
-
function verdictsFor(subject: ReviewSubject): { approve: string; deny: string; skip: string } {
|
|
597
|
-
return {
|
|
598
|
-
approve: "Approve — auto-save to GitHub",
|
|
599
|
-
deny: "Deny — send feedback for revision",
|
|
600
|
-
skip: `Skip — decide later (manual ${subject.failsafeCmd})`,
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/** The optional 4th verdict (plan arm only): the no-save implement-here exit (§8.23). */
|
|
605
|
-
const VERDICT_IMPLEMENT_HERE = "Implement here — no issue saved";
|
|
606
|
-
|
|
607
|
-
const REVIEW_EDITOR_TITLE =
|
|
608
|
-
"Plan review — Enter: continue to verdict · Esc: skip · Ctrl+G: $EDITOR";
|
|
609
|
-
const DENY_FEEDBACK_TITLE = "Deny feedback (optional) — Enter to send";
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* The first-party in-TUI review core, pure over injected seams (the ciExecutor.ts pure-core + injected-fakes recipe) — fully
|
|
613
|
-
* offline-testable. Flow: (1) display the plan in the editor dialog (Esc = dismissed; the human
|
|
614
|
-
* may edit, incl. via Ctrl+G/$EDITOR); (2) a non-blank edit differing from the displayed plan is
|
|
615
|
-
* written back to the draft BEFORE the verdict (reviewed bytes == artifact bytes == saved bytes;
|
|
616
|
-
* a failed write-back aborts the review fail-open — never let an approval save a stale artifact;
|
|
617
|
-
* a blank edit result is treated as no-edit); (3) the 3-option verdict select (Esc/Skip =
|
|
618
|
-
* dismissed); (4) on deny, optional feedback via a second editor dialog. `ctx.ui.editor` takes no
|
|
619
|
-
* AbortSignal — `signal?.aborted` is checked before each dialog (the aborted arm wins).
|
|
620
|
-
*
|
|
621
|
-
* Presentation options (defaults preserve the plan-path behavior byte-for-byte):
|
|
622
|
-
* `editorTitle`/`verdicts` swap the displayed strings; `verdicts.implementHere`, when present,
|
|
623
|
-
* makes the verdict select 4 options — approve, implement-here, deny, skip (implement-here sits
|
|
624
|
-
* adjacent to approve: both are "accept the plan" outcomes) — and selecting it returns the
|
|
625
|
-
* `implement-here` outcome arm; `viewOnly: true` skips the write-back
|
|
626
|
-
* branch entirely — the editor output is used only for Esc/dismissed detection, `plan` is
|
|
627
|
-
* returned unchanged and `edited` stays false (deny+feedback is the change channel).
|
|
628
|
-
*/
|
|
629
|
-
export async function runFirstPartyReview(args: {
|
|
630
|
-
ui: PlanReviewUI;
|
|
631
|
-
plan: string;
|
|
632
|
-
/** Bound to `writePlanDraft(pi, ctx, …).details.ok` by the execute path. */
|
|
633
|
-
writeDraft(plan: string): boolean;
|
|
634
|
-
signal?: AbortSignal;
|
|
635
|
-
editorTitle?: string;
|
|
636
|
-
verdicts?: { approve: string; deny: string; skip: string; implementHere?: string };
|
|
637
|
-
viewOnly?: boolean;
|
|
638
|
-
}): Promise<{ outcome: ReviewOutcome; plan: string; edited: boolean }> {
|
|
639
|
-
const { ui, writeDraft, signal } = args;
|
|
640
|
-
const editorTitle = args.editorTitle ?? REVIEW_EDITOR_TITLE;
|
|
641
|
-
const verdicts: { approve: string; deny: string; skip: string; implementHere?: string } =
|
|
642
|
-
args.verdicts ?? verdictsFor(PLAN_SUBJECT);
|
|
643
|
-
let plan = args.plan;
|
|
644
|
-
let edited = false;
|
|
645
|
-
const result = (
|
|
646
|
-
outcome: ReviewOutcome,
|
|
647
|
-
): { outcome: ReviewOutcome; plan: string; edited: boolean } => ({
|
|
648
|
-
outcome,
|
|
649
|
-
plan,
|
|
650
|
-
edited,
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
if (signal?.aborted) return result({ status: "aborted" });
|
|
654
|
-
const reviewed = await ui.editor(editorTitle, plan);
|
|
655
|
-
if (signal?.aborted) return result({ status: "aborted" });
|
|
656
|
-
if (reviewed === undefined) return result({ status: "dismissed" });
|
|
657
|
-
|
|
658
|
-
// Write human edits back to the draft BEFORE the verdict (blank = no-edit, review the original
|
|
659
|
-
// bytes). A failed write-back aborts the review fail-open — nothing saved. View-only reviews
|
|
660
|
-
// skip the branch entirely (the editor is display-only; deny+feedback is the change channel).
|
|
661
|
-
if (args.viewOnly !== true && reviewed !== plan && reviewed.trim().length > 0) {
|
|
662
|
-
if (!writeDraft(reviewed)) {
|
|
663
|
-
return result({
|
|
664
|
-
status: "unavailable",
|
|
665
|
-
warning:
|
|
666
|
-
"could not write the edited draft back to the session data dir — review aborted, " +
|
|
667
|
-
"nothing saved",
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
plan = reviewed;
|
|
671
|
-
edited = true;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (signal?.aborted) return result({ status: "aborted" });
|
|
675
|
-
const options =
|
|
676
|
-
verdicts.implementHere === undefined
|
|
677
|
-
? [verdicts.approve, verdicts.deny, verdicts.skip]
|
|
678
|
-
: [verdicts.approve, verdicts.implementHere, verdicts.deny, verdicts.skip];
|
|
679
|
-
const verdict = await ui.select("Plan review verdict", options, { signal });
|
|
680
|
-
if (signal?.aborted) return result({ status: "aborted" });
|
|
681
|
-
if (verdict === verdicts.approve) {
|
|
682
|
-
return result({ status: "completed", approved: true, reviewId: randomUUID() });
|
|
683
|
-
}
|
|
684
|
-
if (verdicts.implementHere !== undefined && verdict === verdicts.implementHere) {
|
|
685
|
-
return result({ status: "implement-here", reviewId: randomUUID() });
|
|
686
|
-
}
|
|
687
|
-
if (verdict === verdicts.deny) {
|
|
688
|
-
const feedback = await ui.editor(DENY_FEEDBACK_TITLE, "");
|
|
689
|
-
if (signal?.aborted) return result({ status: "aborted" });
|
|
690
|
-
return result({
|
|
691
|
-
status: "completed",
|
|
692
|
-
approved: false,
|
|
693
|
-
feedback: feedback?.trim() ? feedback : undefined,
|
|
694
|
-
reviewId: randomUUID(),
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
// Skip option, or the select dismissed (Esc) — fail-open skip.
|
|
698
|
-
return result({ status: "dismissed" });
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// ------------------------------------------------------------------- the objective review arm
|
|
702
|
-
|
|
703
|
-
const OBJECTIVE_REVIEW_EDITOR_TITLE =
|
|
704
|
-
"Objective review (view only — edits are not saved) — Enter: continue to verdict · Esc: skip · " +
|
|
705
|
-
"Ctrl+G: $EDITOR";
|
|
706
|
-
|
|
707
|
-
/**
|
|
708
|
-
* Map a non-approved objective review outcome into the model-facing tool result (exported for
|
|
709
|
-
* the offline tests) — the objective-flavored sibling of `reviewOutcomeResult`, delegating to
|
|
710
|
-
* `subjectReviewOutcomeResult` with `OBJECTIVE_SUBJECT`. Every arm carries
|
|
711
|
-
* `details.subject: "objective"`; the texts redirect to `objective_draft` / `/objective-save`.
|
|
712
|
-
* The execute path routes approved outcomes to `approvedObjectiveSaveResult` first, so
|
|
713
|
-
* `completed` renders DENIED here.
|
|
714
|
-
*/
|
|
715
|
-
export function objectiveReviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
716
|
-
return subjectReviewOutcomeResult(OBJECTIVE_SUBJECT, outcome);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Map an APPROVED objective review outcome + the `objectiveApprovalSave` outcome into the
|
|
721
|
-
* model-facing tool result (exported for the offline tests) — the objective sibling of
|
|
722
|
-
* `approvedSaveResult`, delegating to `approvedSubjectSaveResult` with `OBJECTIVE_SUBJECT` and
|
|
723
|
-
* no opts (the objective path reviews only the rendered draft, view-only — no
|
|
724
|
-
* `paramMismatch`/`edited`).
|
|
725
|
-
*/
|
|
726
|
-
export function approvedObjectiveSaveResult(
|
|
727
|
-
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
728
|
-
save: ObjectiveApprovalSaveOutcome,
|
|
729
|
-
): ToolResult {
|
|
730
|
-
return approvedSubjectSaveResult(
|
|
731
|
-
OBJECTIVE_SUBJECT,
|
|
732
|
-
outcome,
|
|
733
|
-
save.status === "no-draft" ? { status: "no-source" } : save,
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* The objective review arm, mirroring `executePlanReview`'s shape but
|
|
739
|
-
* with the rendered objective draft as the SOLE review source (never the `plan` param, never
|
|
740
|
-
* the transcript). First-party reviews run VIEW-ONLY (edits are never written back;
|
|
741
|
-
* deny+feedback is the change channel). An APPROVED outcome wires into the
|
|
742
|
-
* `objectiveApprovalSave` seam (re-read the STRUCTURED artifact → `saveObjective` → D1a gate
|
|
743
|
-
* exit → terminating); every other outcome maps via `objectiveReviewOutcomeResult`. ONE
|
|
744
|
-
* carve-out (plannotator only): an approval whose feedback opens a Direct Edits section SKIPS
|
|
745
|
-
* the save — rendered-markdown edits cannot be folded back into the structured draft
|
|
746
|
-
* mechanically — and returns a NON-terminating revise round with the gate untouched (fold the
|
|
747
|
-
* diff in via `objective_draft`, re-review to confirm); perk never saves an objective the
|
|
748
|
-
* reviewer explicitly edited away from.
|
|
749
|
-
*/
|
|
750
|
-
export async function executeObjectiveReview(
|
|
751
|
-
pi: ExtensionAPI,
|
|
752
|
-
ctx: ExtensionContext,
|
|
753
|
-
gating: ToolGating,
|
|
754
|
-
bridge: { review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> },
|
|
755
|
-
signal?: AbortSignal,
|
|
756
|
-
wave?: WaveLaunch,
|
|
757
|
-
): Promise<ToolResult> {
|
|
758
|
-
// 1. Headless → soft skip (fail-open; never wedges CI/supervisor runs on an interactive UI).
|
|
759
|
-
if (!ctx.hasUI) return skipResult();
|
|
760
|
-
// 2. The wave arm's stale-guard baseline is captured BEFORE the validated read below (the
|
|
761
|
-
// objective door's fail-closed ordering): the rendered bytes always derive from a read at
|
|
762
|
-
// or after this baseline, so a concurrent objective_draft write between the two reads makes
|
|
763
|
-
// the browsed render NEWER than the baseline and routeObjectiveReviewDecision's existing
|
|
764
|
-
// guard refuses the approval — the reverse order would fail open (approve unreviewed
|
|
765
|
-
// bytes). Raw artifact bytes on purpose: the save-authoritative surface catches
|
|
766
|
-
// render-invisible changes.
|
|
767
|
-
const baseline = readSessionArtifact(ctx, OBJECTIVE_DRAFT_ARTIFACT);
|
|
768
|
-
// 3. The draft artifact is the sole review source — no draft → soft skip with the
|
|
769
|
-
// objective_draft redirect.
|
|
770
|
-
const draft = readObjectiveDraft(ctx);
|
|
771
|
-
if (draft === null) {
|
|
772
|
-
return {
|
|
773
|
-
content: [
|
|
774
|
-
{
|
|
775
|
-
type: "text",
|
|
776
|
-
text:
|
|
777
|
-
"no objective draft to review — write the working objective with objective_draft " +
|
|
778
|
-
"(prose + the structured roadmap), then call plan_review again.",
|
|
779
|
-
},
|
|
780
|
-
],
|
|
781
|
-
details: {
|
|
782
|
-
ok: false,
|
|
783
|
-
error: "no objective draft to review — write it with objective_draft first",
|
|
784
|
-
error_type: "no_objective_draft",
|
|
785
|
-
status: "skipped",
|
|
786
|
-
reason: "no_objective_draft",
|
|
787
|
-
},
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
// 4. The reviewed bytes are the RENDERED markdown (prose + roadmap table) — never raw JSON.
|
|
791
|
-
const rendered = renderObjectiveDraft(draft);
|
|
792
|
-
// 5. Backend dispatch (mirrors the plan path): plannotator-selected → the bridge; ANY other
|
|
793
|
-
// selection → the first-party editor, view-only.
|
|
794
|
-
const sig = signal ?? ctx.signal;
|
|
795
|
-
let outcome: ReviewOutcome;
|
|
796
|
-
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
797
|
-
// The launch chooser (contracts.md §8.23): every eligible round the human picks with/without
|
|
798
|
-
// the streamed reviewer wave BEFORE anything launches. Eligibility is drafts-only — the wave
|
|
799
|
-
// door stale-guards the raw artifact baseline, so a null baseline keeps the plain path
|
|
800
|
-
// (silently: there is no forced mode to warn about).
|
|
801
|
-
if (wave?.present() && baseline !== null) {
|
|
802
|
-
const choice = await chooseReviewLaunch(ctx.ui, "Objective", sig);
|
|
803
|
-
if (choice.launch === "aborted") return objectiveReviewOutcomeResult({ status: "aborted" });
|
|
804
|
-
if (choice.launch === "wave") {
|
|
805
|
-
const guidance = await wave.objective(ctx, {
|
|
806
|
-
rendered,
|
|
807
|
-
artifactRaw: baseline.content,
|
|
808
|
-
...(choice.custom !== undefined ? { custom: choice.custom } : {}),
|
|
809
|
-
});
|
|
810
|
-
// Abort outranks the opener result too: a turn interrupted during the awaited open must
|
|
811
|
-
// never report a successful launch (the door's own bridge abort handling settles the
|
|
812
|
-
// background tasks and clears the primed surfaces).
|
|
813
|
-
if (sig?.aborted) return objectiveReviewOutcomeResult({ status: "aborted" });
|
|
814
|
-
if (guidance !== null) return waveLaunchedResult(OBJECTIVE_SUBJECT, guidance);
|
|
815
|
-
// null = the synchronous port-pick failure (already loudly reported inside the core) —
|
|
816
|
-
// fall open to the plain blocking review in the same call: the review never wedges.
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
outcome = await bridge.review(rendered, sig);
|
|
820
|
-
// APPROVE + Direct Edits (browser edits of the RENDERED markdown), checked BEFORE the
|
|
821
|
-
// approved-save routing (the approved-first discipline): the save seam re-reads the
|
|
822
|
-
// STRUCTURED artifact, so rendered-markdown edits — roadmap-table rows included — cannot be
|
|
823
|
-
// folded back without model judgment. Skip the save, keep the gate read-only, and route ONE
|
|
824
|
-
// revise round: the model folds the diff into `objective_draft`, then re-reviews to confirm.
|
|
825
|
-
// The heading check suffices (extraction success is irrelevant here — the diff goes to the
|
|
826
|
-
// model verbatim either way).
|
|
827
|
-
if (
|
|
828
|
-
outcome.status === "completed" &&
|
|
829
|
-
outcome.approved &&
|
|
830
|
-
outcome.feedback !== undefined &&
|
|
831
|
-
hasDirectEditsHeading(outcome.feedback)
|
|
832
|
-
) {
|
|
833
|
-
return {
|
|
834
|
-
content: [
|
|
835
|
-
{
|
|
836
|
-
type: "text",
|
|
837
|
-
text:
|
|
838
|
-
"objective APPROVED with direct browser edits — these cannot be auto-applied to " +
|
|
839
|
-
"the structured draft, so nothing was saved. Fold the Direct Edits diff below into " +
|
|
840
|
-
"the working draft with objective_draft (prose hunks → the prose; roadmap-table " +
|
|
841
|
-
"hunks → the matching node fields), then call plan_review again to confirm.\n\n" +
|
|
842
|
-
`Reviewer feedback:\n${outcome.feedback}`,
|
|
843
|
-
},
|
|
844
|
-
],
|
|
845
|
-
details: {
|
|
846
|
-
ok: true,
|
|
847
|
-
status: "revise",
|
|
848
|
-
reason: "direct_edits",
|
|
849
|
-
approved: true,
|
|
850
|
-
feedback: outcome.feedback,
|
|
851
|
-
reviewId: outcome.reviewId,
|
|
852
|
-
subject: "objective",
|
|
853
|
-
},
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
} else {
|
|
857
|
-
const fp = await runFirstPartyReview({
|
|
858
|
-
ui: ctx.ui,
|
|
859
|
-
plan: rendered,
|
|
860
|
-
writeDraft: () => true, // unreachable under viewOnly — the branch is skipped
|
|
861
|
-
signal: sig,
|
|
862
|
-
editorTitle: OBJECTIVE_REVIEW_EDITOR_TITLE,
|
|
863
|
-
verdicts: verdictsFor(OBJECTIVE_SUBJECT),
|
|
864
|
-
viewOnly: true,
|
|
865
|
-
});
|
|
866
|
-
outcome = fp.outcome;
|
|
867
|
-
}
|
|
868
|
-
// 6. An APPROVED decision (either backend) wires into the objectiveApprovalSave seam (the
|
|
869
|
-
// STRUCTURED artifact is re-read at save time — never the rendered bytes; auto-save → D1a
|
|
870
|
-
// gate exit → terminating result); everything else maps via objectiveReviewOutcomeResult.
|
|
871
|
-
// Approved-first routing: objectiveReviewOutcomeResult's completed case renders DENIED.
|
|
872
|
-
if (outcome.status === "completed" && outcome.approved) {
|
|
873
|
-
const save = await objectiveApprovalSave(pi, ctx, gating);
|
|
874
|
-
return approvedObjectiveSaveResult(outcome, save);
|
|
875
|
-
}
|
|
876
|
-
return objectiveReviewOutcomeResult(outcome);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// ------------------------------------------------------------------------ the gist review arm
|
|
880
|
-
|
|
881
|
-
const GIST_REVIEW_EDITOR_TITLE =
|
|
882
|
-
"Gist review (view only — edits are not saved) — Enter: continue to verdict · Esc: skip · " +
|
|
883
|
-
"Ctrl+G: $EDITOR";
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* Map a non-approved gist review outcome into the model-facing tool result (exported for the
|
|
887
|
-
* offline tests) — the gist-flavored sibling of `objectiveReviewOutcomeResult`, delegating to
|
|
888
|
-
* `subjectReviewOutcomeResult` with `GIST_SUBJECT`. Every arm carries `details.subject: "gist"`;
|
|
889
|
-
* the texts redirect to `gist_draft` / `/gist-save`. The execute path routes approved outcomes
|
|
890
|
-
* to `approvedGistSaveResult` first, so `completed` renders DENIED here.
|
|
891
|
-
*/
|
|
892
|
-
export function gistReviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
893
|
-
return subjectReviewOutcomeResult(GIST_SUBJECT, outcome);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Map an APPROVED gist review outcome + the `gistApprovalSave` outcome into the model-facing
|
|
898
|
-
* tool result (exported for the offline tests) — the gist sibling of
|
|
899
|
-
* `approvedObjectiveSaveResult`, delegating to `approvedSubjectSaveResult` with `GIST_SUBJECT`
|
|
900
|
-
* and no opts (the gist path reviews only the rendered draft, view-only — no
|
|
901
|
-
* `paramMismatch`/`edited`).
|
|
902
|
-
*/
|
|
903
|
-
export function approvedGistSaveResult(
|
|
904
|
-
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
905
|
-
save: GistApprovalSaveOutcome,
|
|
906
|
-
): ToolResult {
|
|
907
|
-
return approvedSubjectSaveResult(
|
|
908
|
-
GIST_SUBJECT,
|
|
909
|
-
outcome,
|
|
910
|
-
save.status === "no-draft" ? { status: "no-source" } : save,
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* The gist review arm, mirroring `executeObjectiveReview`'s shape with the rendered gist draft
|
|
916
|
-
* as the SOLE review source (never the `plan` param, never the transcript). First-party reviews
|
|
917
|
-
* run VIEW-ONLY (edits are never written back; deny+feedback is the change channel); the
|
|
918
|
-
* implement-here verdict is never offered (a gist is not implementable — it has no strategy).
|
|
919
|
-
* An APPROVED outcome wires into the `gistApprovalSave` seam (re-read the artifact → `saveGist`
|
|
920
|
-
* → D1a gate exit → terminating); every other outcome maps via `gistReviewOutcomeResult`. ONE
|
|
921
|
-
* carve-out (plannotator only, mirroring the objective arm): an approval whose feedback opens a
|
|
922
|
-
* Direct Edits section SKIPS the save — rendered edits cannot be folded back into the
|
|
923
|
-
* structured draft mechanically — and returns a NON-terminating revise round with the gate
|
|
924
|
-
* untouched (fold each hunk into the matching `gist_draft` field, re-review to confirm); perk
|
|
925
|
-
* never saves a gist the reviewer explicitly edited away from.
|
|
926
|
-
*/
|
|
927
|
-
export async function executeGistReview(
|
|
928
|
-
pi: ExtensionAPI,
|
|
929
|
-
ctx: ExtensionContext,
|
|
930
|
-
gating: ToolGating,
|
|
931
|
-
bridge: { review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> },
|
|
932
|
-
signal?: AbortSignal,
|
|
933
|
-
): Promise<ToolResult> {
|
|
934
|
-
// 1. Headless → soft skip (fail-open; never wedges CI/supervisor runs on an interactive UI).
|
|
935
|
-
if (!ctx.hasUI) return skipResult();
|
|
936
|
-
// 2. The draft artifact is the sole review source — no draft → soft skip with the gist_draft
|
|
937
|
-
// redirect.
|
|
938
|
-
const draft = readGistDraft(ctx);
|
|
939
|
-
if (draft === null) {
|
|
940
|
-
return {
|
|
941
|
-
content: [
|
|
942
|
-
{
|
|
943
|
-
type: "text",
|
|
944
|
-
text:
|
|
945
|
-
"no gist draft to review — write the working gist with gist_draft (the " +
|
|
946
|
-
"statement-of-intent prose), then call plan_review again.",
|
|
947
|
-
},
|
|
948
|
-
],
|
|
949
|
-
details: {
|
|
950
|
-
ok: false,
|
|
951
|
-
error: "no gist draft to review — write it with gist_draft first",
|
|
952
|
-
error_type: "no_gist_draft",
|
|
953
|
-
status: "skipped",
|
|
954
|
-
reason: "no_gist_draft",
|
|
955
|
-
},
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
// 3. The reviewed bytes are the RENDERED markdown (title + scope + prose) — never raw JSON.
|
|
959
|
-
const rendered = renderGistDraft(draft);
|
|
960
|
-
// 4. Backend dispatch (mirrors the objective path): plannotator-selected → the bridge; ANY
|
|
961
|
-
// other selection → the first-party editor, view-only.
|
|
962
|
-
const sig = signal ?? ctx.signal;
|
|
963
|
-
let outcome: ReviewOutcome;
|
|
964
|
-
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
965
|
-
outcome = await bridge.review(rendered, sig);
|
|
966
|
-
// APPROVE + Direct Edits (browser edits of the RENDERED markdown), checked BEFORE the
|
|
967
|
-
// approved-save routing (the approved-first discipline; mirrors the objective arm): the
|
|
968
|
-
// save seam re-reads the STRUCTURED artifact, so rendered edits cannot be folded back
|
|
969
|
-
// without model judgment. Skip the save, keep the gate read-only, and route ONE revise
|
|
970
|
-
// round: the model folds each hunk into the matching `gist_draft` field, then re-reviews
|
|
971
|
-
// to confirm. The heading check suffices (extraction success is irrelevant here — the diff
|
|
972
|
-
// goes to the model verbatim either way).
|
|
973
|
-
if (
|
|
974
|
-
outcome.status === "completed" &&
|
|
975
|
-
outcome.approved &&
|
|
976
|
-
outcome.feedback !== undefined &&
|
|
977
|
-
hasDirectEditsHeading(outcome.feedback)
|
|
978
|
-
) {
|
|
979
|
-
return {
|
|
980
|
-
content: [
|
|
981
|
-
{
|
|
982
|
-
type: "text",
|
|
983
|
-
text:
|
|
984
|
-
"gist APPROVED with direct browser edits — these cannot be auto-applied to the " +
|
|
985
|
-
"structured draft, so nothing was saved. Fold each Direct Edits hunk below into " +
|
|
986
|
-
"the matching gist_draft field (a `# <title>` heading hunk → title, a `Scope:` " +
|
|
987
|
-
"line hunk → scope, prose hunks → prose), then call plan_review again to " +
|
|
988
|
-
`confirm.\n\nReviewer feedback:\n${outcome.feedback}`,
|
|
989
|
-
},
|
|
990
|
-
],
|
|
991
|
-
details: {
|
|
992
|
-
ok: true,
|
|
993
|
-
status: "revise",
|
|
994
|
-
reason: "direct_edits",
|
|
995
|
-
approved: true,
|
|
996
|
-
feedback: outcome.feedback,
|
|
997
|
-
reviewId: outcome.reviewId,
|
|
998
|
-
subject: "gist",
|
|
999
|
-
},
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
} else {
|
|
1003
|
-
const fp = await runFirstPartyReview({
|
|
1004
|
-
ui: ctx.ui,
|
|
1005
|
-
plan: rendered,
|
|
1006
|
-
writeDraft: () => true, // unreachable under viewOnly — the branch is skipped
|
|
1007
|
-
signal: sig,
|
|
1008
|
-
editorTitle: GIST_REVIEW_EDITOR_TITLE,
|
|
1009
|
-
verdicts: verdictsFor(GIST_SUBJECT),
|
|
1010
|
-
viewOnly: true,
|
|
1011
|
-
});
|
|
1012
|
-
outcome = fp.outcome;
|
|
1013
|
-
}
|
|
1014
|
-
// 5. An APPROVED decision (either backend) wires into the gistApprovalSave seam (the artifact
|
|
1015
|
-
// is re-read at save time — never the rendered bytes; auto-save → D1a gate exit →
|
|
1016
|
-
// terminating result); everything else maps via gistReviewOutcomeResult. Approved-first
|
|
1017
|
-
// routing: gistReviewOutcomeResult's completed case renders DENIED.
|
|
1018
|
-
if (outcome.status === "completed" && outcome.approved) {
|
|
1019
|
-
const save = await gistApprovalSave(pi, ctx, gating);
|
|
1020
|
-
return approvedGistSaveResult(outcome, save);
|
|
1021
|
-
}
|
|
1022
|
-
return gistReviewOutcomeResult(outcome);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// ------------------------------------------------------------------------- the execute core
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* The `plan_review` execute core, extracted pure-over-its-seams (the bridge, the gating, the
|
|
1029
|
-
* ctx) so the resolution + dispatch + approved-save paths are unit-testable offline. Arm order:
|
|
1030
|
-
* param decode → the objective arm (`executeObjectiveReview` — the rendered objective draft is
|
|
1031
|
-
* the review subject in BOTH objective-authoring stages) → headless skip → file-first
|
|
1032
|
-
* resolution → backend
|
|
1033
|
-
* dispatch (plannotator-selected → the event-bus bridge; ANY other selection → the first-party
|
|
1034
|
-
* in-TUI editor review) → approved → `approvalSave`.
|
|
1035
|
-
*/
|
|
1036
|
-
export async function executePlanReview(
|
|
1037
|
-
pi: ExtensionAPI,
|
|
1038
|
-
ctx: ExtensionContext,
|
|
1039
|
-
gating: ToolGating,
|
|
1040
|
-
bridge: { review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> },
|
|
1041
|
-
params: unknown,
|
|
1042
|
-
signal?: AbortSignal,
|
|
1043
|
-
wave?: WaveLaunch,
|
|
1044
|
-
): Promise<ToolResult> {
|
|
1045
|
-
// Tool-boundary decode, in this tool's native fail-open vocabulary: a MISTYPED
|
|
1046
|
-
// `plan` (or non-object params) skip-shapes (`reason: "bad_input"`) without reviewing; an
|
|
1047
|
-
// ABSENT `plan` proceeds — the validated draft artifact is the preferred source.
|
|
1048
|
-
const p = paramsOf(params);
|
|
1049
|
-
const plan = p === null ? null : stringParam(p, "plan");
|
|
1050
|
-
if (plan === null) {
|
|
1051
|
-
return {
|
|
1052
|
-
content: [
|
|
1053
|
-
{
|
|
1054
|
-
type: "text",
|
|
1055
|
-
text: "plan_review takes { plan?: string } — omit it (the plan-draft artifact is preferred) or pass a string.",
|
|
1056
|
-
},
|
|
1057
|
-
],
|
|
1058
|
-
details: {
|
|
1059
|
-
ok: false,
|
|
1060
|
-
error: "plan must be a string",
|
|
1061
|
-
error_type: "bad_input",
|
|
1062
|
-
status: "skipped",
|
|
1063
|
-
reason: "bad_input",
|
|
1064
|
-
},
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
// 1. An objective-authoring session (objective-author OR objective-save — both stages'
|
|
1068
|
-
// working draft is the objective draft, and neither carries `plan_draft`) → the objective
|
|
1069
|
-
// review arm: the rendered objective draft is the sole review source; a well-typed `plan`
|
|
1070
|
-
// param is ignored here — the plan-arm fallthrough could otherwise review/save an
|
|
1071
|
-
// unrelated plan param from an objective session. A gist-author session likewise routes to
|
|
1072
|
-
// the gist arm (the rendered gist draft).
|
|
1073
|
-
const launchedStage = rebuildWorkflowState(branchOf(ctx)).stage;
|
|
1074
|
-
if (launchedStage === OBJECTIVE_AUTHOR_STAGE || launchedStage === OBJECTIVE_SAVE_STAGE) {
|
|
1075
|
-
return executeObjectiveReview(pi, ctx, gating, bridge, signal ?? ctx.signal, wave);
|
|
1076
|
-
}
|
|
1077
|
-
if (launchedStage === GIST_AUTHOR_STAGE) {
|
|
1078
|
-
return executeGistReview(pi, ctx, gating, bridge, signal ?? ctx.signal);
|
|
1079
|
-
}
|
|
1080
|
-
// 2. Headless → soft skip (fail-open; never wedges CI/supervisor runs on an interactive UI).
|
|
1081
|
-
if (!ctx.hasUI) return skipResult();
|
|
1082
|
-
// 3. File-first resolution: artifact → param, NEVER transcript — an approval
|
|
1083
|
-
// auto-saves the reviewed bytes, and scraped conversation bytes must never be those.
|
|
1084
|
-
const src = resolvePlanSource(ctx, plan);
|
|
1085
|
-
if (src === null || src.source === "transcript") {
|
|
1086
|
-
return {
|
|
1087
|
-
content: [
|
|
1088
|
-
{
|
|
1089
|
-
type: "text",
|
|
1090
|
-
text:
|
|
1091
|
-
"no plan to review — write the working draft with plan_draft (or pass the plan " +
|
|
1092
|
-
"param), then call plan_review again.",
|
|
1093
|
-
},
|
|
1094
|
-
],
|
|
1095
|
-
details: {
|
|
1096
|
-
ok: false,
|
|
1097
|
-
error: "no plan to review — write the draft with plan_draft first",
|
|
1098
|
-
error_type: "no_plan",
|
|
1099
|
-
status: "skipped",
|
|
1100
|
-
reason: "no_plan",
|
|
1101
|
-
},
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
|
-
// 4. Backend dispatch: plannotator-selected → the event-bus bridge; ANY other selection
|
|
1105
|
-
// (perk-plan, tombell, unknown ids) → the first-party in-TUI editor review.
|
|
1106
|
-
const sig = signal ?? ctx.signal;
|
|
1107
|
-
let outcome: ReviewOutcome;
|
|
1108
|
-
let reviewedPlan = src.plan;
|
|
1109
|
-
let edited = false;
|
|
1110
|
-
let directEditsFailed = false;
|
|
1111
|
-
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
1112
|
-
// The launch chooser (contracts.md §8.23): every eligible round the human picks with/without
|
|
1113
|
-
// the streamed reviewer wave BEFORE anything launches. Eligibility is drafts-only — the wave
|
|
1114
|
-
// door reviews and stale-guards the validated artifact, so a param-tier source keeps the
|
|
1115
|
-
// plain path (silently: there is no forced mode to warn about; the `wave === undefined` arm
|
|
1116
|
-
// is defensive/test-only and behaves identically).
|
|
1117
|
-
if (wave?.present() && src.source === "plan-draft") {
|
|
1118
|
-
const choice = await chooseReviewLaunch(ctx.ui, "Plan", sig);
|
|
1119
|
-
if (choice.launch === "aborted") return reviewOutcomeResult({ status: "aborted" });
|
|
1120
|
-
if (choice.launch === "wave") {
|
|
1121
|
-
const guidance = await wave.plan(ctx, {
|
|
1122
|
-
draft: src.plan,
|
|
1123
|
-
...(choice.custom !== undefined ? { custom: choice.custom } : {}),
|
|
1124
|
-
});
|
|
1125
|
-
// Abort outranks the opener result too: a turn interrupted during the awaited open must
|
|
1126
|
-
// never report a successful launch (the door's own bridge abort handling settles the
|
|
1127
|
-
// background tasks and clears the primed surfaces).
|
|
1128
|
-
if (sig?.aborted) return reviewOutcomeResult({ status: "aborted" });
|
|
1129
|
-
if (guidance !== null) return waveLaunchedResult(PLAN_SUBJECT, guidance);
|
|
1130
|
-
// null = the synchronous port-pick failure (already loudly reported inside the core) —
|
|
1131
|
-
// fall open to the plain blocking review in the same call: the review never wedges.
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
outcome = await bridge.review(src.plan, sig);
|
|
1135
|
-
// APPROVE + Direct Edits (browser plan edits, contracts.md §8.23): mechanically apply the
|
|
1136
|
-
// reviewer's diff via the shared helper (the first-party pre-verdict write-back, replayed
|
|
1137
|
-
// here post-verdict because the bridge only reports the diff), and save the EDITED bytes.
|
|
1138
|
-
// Every rung fails open to the verbatim path (never save bytes the artifact doesn't carry).
|
|
1139
|
-
// DENY stays model-mediated — the feedback (diff included) passes through for the
|
|
1140
|
-
// plan_draft rewrite.
|
|
1141
|
-
if (outcome.status === "completed") {
|
|
1142
|
-
const applied = applyPlannotatorDirectEdits(pi, ctx, outcome, src.plan);
|
|
1143
|
-
outcome = applied.outcome;
|
|
1144
|
-
reviewedPlan = applied.reviewedPlan;
|
|
1145
|
-
edited = applied.edited;
|
|
1146
|
-
directEditsFailed = applied.directEditsFailed;
|
|
1147
|
-
}
|
|
1148
|
-
} else {
|
|
1149
|
-
// The 4th verdict (implement-here, the no-save exit) is offered UNLESS this is an
|
|
1150
|
-
// objective-node planning session — a node-linked plan must save (the node advance and
|
|
1151
|
-
// backlink depend on it), so the claim suppresses it back to the 3-option select.
|
|
1152
|
-
const fp = await runFirstPartyReview({
|
|
1153
|
-
ui: ctx.ui,
|
|
1154
|
-
plan: src.plan,
|
|
1155
|
-
writeDraft: (text) => writePlanDraft(pi, ctx, text).details.ok,
|
|
1156
|
-
signal: sig,
|
|
1157
|
-
verdicts:
|
|
1158
|
-
readNodeClaim(ctx) === null
|
|
1159
|
-
? { ...verdictsFor(PLAN_SUBJECT), implementHere: VERDICT_IMPLEMENT_HERE }
|
|
1160
|
-
: undefined,
|
|
1161
|
-
});
|
|
1162
|
-
outcome = fp.outcome;
|
|
1163
|
-
reviewedPlan = fp.plan;
|
|
1164
|
-
edited = fp.edited;
|
|
1165
|
-
}
|
|
1166
|
-
// 5. IMPLEMENT-HERE (first-party only) routes before the generic mapper (mirror the
|
|
1167
|
-
// approved-first split): gate exit WITHOUT save through the implementHereExit seam → a
|
|
1168
|
-
// NON-terminating result carrying the implement-now guidance.
|
|
1169
|
-
if (outcome.status === "implement-here") {
|
|
1170
|
-
const exit = implementHereExit(ctx, gating);
|
|
1171
|
-
return implementHereResult(outcome, exit, { cwd: ctx.cwd, plan: reviewedPlan, edited });
|
|
1172
|
-
}
|
|
1173
|
-
// 6. An APPROVED decision (either backend) wires into the approvalSave seam (auto-save → D1a
|
|
1174
|
-
// gate exit → terminating result); everything else maps via reviewOutcomeResult.
|
|
1175
|
-
if (outcome.status === "completed" && outcome.approved) {
|
|
1176
|
-
const save = await approvalSave(pi, ctx, gating, { reviewedPlan });
|
|
1177
|
-
return approvedSaveResult(outcome, save, {
|
|
1178
|
-
paramMismatch: src.paramMismatch,
|
|
1179
|
-
edited,
|
|
1180
|
-
directEditsFailed,
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
return reviewOutcomeResult(outcome);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// ----------------------------------------------------------------------------- registration
|
|
1187
|
-
|
|
1188
|
-
/**
|
|
1189
|
-
* Register `plan_review` — perk's universal review door. In READ_ONLY_TOOLS so it is callable
|
|
1190
|
-
* INSIDE plan mode (the whole point — review happens before the gate ever comes off). Fail-open
|
|
1191
|
-
* everywhere: headless / dismissed / backend-unavailable all soft-skip so authoring never wedges.
|
|
1192
|
-
* `wave` is the injected wave-launch deps (index.ts composes them from the door open cores);
|
|
1193
|
-
* absent ⇒ the chooser never appears and every path is byte-stable.
|
|
1194
|
-
*/
|
|
1195
|
-
export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating, wave?: WaveLaunch): void {
|
|
1196
|
-
const bridge = createPlannotatorBridge(pi.events);
|
|
1197
|
-
|
|
1198
|
-
pi.registerTool({
|
|
1199
|
-
name: "plan_review",
|
|
1200
|
-
label: "Plan review",
|
|
1201
|
-
description:
|
|
1202
|
-
"Present the plan to the configured review surface — the Plannotator browser UI when " +
|
|
1203
|
-
"selected, otherwise perk's in-TUI editor review — and wait for the human decision. " +
|
|
1204
|
-
"Reviews the validated plan-draft artifact (keep it current with plan_draft); on approval " +
|
|
1205
|
-
"the plan is auto-saved and the turn terminates. On deny, revise per the returned " +
|
|
1206
|
-
"feedback, rewrite the draft with plan_draft, and call again. On the Plannotator surface " +
|
|
1207
|
-
"the human may first opt into a streamed reviewer wave — the call then returns immediately " +
|
|
1208
|
-
'with wave guidance (status "wave_launched") to follow in the same turn, and the browser ' +
|
|
1209
|
-
"decision routes back automatically. No-op skip when the session is headless or the " +
|
|
1210
|
-
"review is dismissed.",
|
|
1211
|
-
promptSnippet: "Request a human review of the working plan draft",
|
|
1212
|
-
promptGuidelines: [
|
|
1213
|
-
"Keep the working draft current with plan_draft — the validated plan-draft artifact is what plan_review reviews AND auto-saves; the plan param is only a fallback when no draft exists.",
|
|
1214
|
-
"Call plan_review only when the plan is decision-complete.",
|
|
1215
|
-
"On a DENIED review, revise per the feedback, rewrite the draft with plan_draft, then call plan_review again.",
|
|
1216
|
-
"On an APPROVED plan_review, the plan is auto-saved and the turn ends — never re-dump the plan as a final message and never tell the user to run /plan-save; relay the save outcome instead.",
|
|
1217
|
-
"On a wave_launched result (the human opted into the reviewer wave), follow the returned guidance in the same turn — launch the wave and relay its findings; the human's browser decision routes back automatically, so never re-call plan_review while that browser review is open.",
|
|
1218
|
-
"If plan_review reports it was skipped or unavailable (headless, dismissed), fall back to presenting the complete plan; the human runs /plan-save (the manual failsafe).",
|
|
1219
|
-
],
|
|
1220
|
-
executionMode: "sequential",
|
|
1221
|
-
parameters: {
|
|
1222
|
-
type: "object",
|
|
1223
|
-
additionalProperties: false,
|
|
1224
|
-
properties: {
|
|
1225
|
-
plan: {
|
|
1226
|
-
type: "string",
|
|
1227
|
-
description:
|
|
1228
|
-
"Optional — the validated plan-draft.md artifact is preferred when present; this " +
|
|
1229
|
-
"param is the fallback for sessions that never wrote a draft.",
|
|
1230
|
-
},
|
|
1231
|
-
},
|
|
1232
|
-
},
|
|
1233
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
1234
|
-
return executePlanReview(pi, ctx, gating, bridge, params, signal, wave);
|
|
1235
|
-
},
|
|
1236
|
-
});
|
|
1237
|
-
}
|