@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,1320 @@
|
|
|
1
|
+
// The v1 Pi installer for the objective-node refinement feature (contracts.md §8.67):
|
|
2
|
+
// `installObjectiveRefinementBindings` owns every refinement registration — the ONE model-facing
|
|
3
|
+
// `objective_refinement_draft` tool, the warm `/objective-refine` entry, the human failsafe
|
|
4
|
+
// `/objective-refinement-save`, and the refinement context hook pair; `runRefinementReviewV1` is
|
|
5
|
+
// the `plan_review` refinement arm the stage dispatcher routes to; `importRefinementContextOnClaim`
|
|
6
|
+
// is the cold claim's one-time context import. The feature logic lives in
|
|
7
|
+
// `authoring/refinement/`; this module decodes at the tool/command boundary, builds the
|
|
8
|
+
// cold-door backend / worker / gate adapters, constructs the warm-door Result envelopes, and
|
|
9
|
+
// places the feature-owned prose units in Pi fields.
|
|
10
|
+
//
|
|
11
|
+
// Isolation posture: a refinement session never creates a plan, claims a node, links a plan-ref,
|
|
12
|
+
// changes node/objective state or provisions a checkout. There is NO `objective_refinement_save`
|
|
13
|
+
// tool — the two persistence paths are the APPROVED `plan_review` arm and the human command,
|
|
14
|
+
// both through the shared save seam over the `perk objective refinement-save` worker.
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import {
|
|
20
|
+
importColdRefinementContext,
|
|
21
|
+
importRefinementContext,
|
|
22
|
+
REFINE_STAGE,
|
|
23
|
+
REFINEMENT_CONTEXT_ARTIFACT,
|
|
24
|
+
type RefinementContextRead,
|
|
25
|
+
validateContextTransfer,
|
|
26
|
+
} from "../../authoring/refinement/context.ts";
|
|
27
|
+
import {
|
|
28
|
+
REFINEMENT_DRAFT_ARTIFACT,
|
|
29
|
+
renderRefinementDraft,
|
|
30
|
+
resumeRefinementDraft,
|
|
31
|
+
reviseRefinementDraft,
|
|
32
|
+
} from "../../authoring/refinement/draft.ts";
|
|
33
|
+
import {
|
|
34
|
+
REFINEMENT_CONTEXT_TYPE,
|
|
35
|
+
REFINEMENT_DRAFT_TOOL_GUIDELINES,
|
|
36
|
+
REFINEMENT_MARKER,
|
|
37
|
+
refinementContextContent,
|
|
38
|
+
refinementGuidance,
|
|
39
|
+
refinementStageRefusal,
|
|
40
|
+
} from "../../authoring/refinement/prose.ts";
|
|
41
|
+
import {
|
|
42
|
+
completeRefinementReview,
|
|
43
|
+
type RefinementReviewOutcome,
|
|
44
|
+
type ReviewRefinementResult,
|
|
45
|
+
} from "../../authoring/refinement/review.ts";
|
|
46
|
+
import {
|
|
47
|
+
type RefinementApprovalSaveOutcome,
|
|
48
|
+
type RefinementBackend,
|
|
49
|
+
type RefinementBackendSaveResult,
|
|
50
|
+
type ReviewedRefinementPair,
|
|
51
|
+
refinementApprovalSave,
|
|
52
|
+
reviewedPairOf,
|
|
53
|
+
} from "../../authoring/refinement/save.ts";
|
|
54
|
+
import type { ApprovalGate } from "../../authoring/review/approvalGate.ts";
|
|
55
|
+
import { openBranchWorkflowSession } from "../../session/branchWorkflowSession.ts";
|
|
56
|
+
import type { WorkflowSession } from "../../session/workflowSession.ts";
|
|
57
|
+
import { bindingSuffix } from "../../substrate/bindingDelivery.ts";
|
|
58
|
+
import {
|
|
59
|
+
type Handoff,
|
|
60
|
+
handoffPath,
|
|
61
|
+
isSafeRunId,
|
|
62
|
+
readHandoff,
|
|
63
|
+
runScratchDir,
|
|
64
|
+
} from "../../substrate/cache.ts";
|
|
65
|
+
import {
|
|
66
|
+
booleanField,
|
|
67
|
+
type ColdJson,
|
|
68
|
+
runColdDoor,
|
|
69
|
+
stringField,
|
|
70
|
+
stringListField,
|
|
71
|
+
} from "../../substrate/coldDoor.ts";
|
|
72
|
+
import { registerPerkCommand } from "../../substrate/command.ts";
|
|
73
|
+
import { loadPerkConfig } from "../../substrate/config.ts";
|
|
74
|
+
import { failFor, ok, type Result } from "../../substrate/result.ts";
|
|
75
|
+
import type { ToolGating } from "../../substrate/toolGating.ts";
|
|
76
|
+
import { paramsOf, stringParam } from "../../substrate/toolParams.ts";
|
|
77
|
+
import {
|
|
78
|
+
type BranchEntry,
|
|
79
|
+
branchOf,
|
|
80
|
+
rebuildWorkflowState,
|
|
81
|
+
type WorkflowState,
|
|
82
|
+
} from "../../substrate/workflowState.ts";
|
|
83
|
+
import { report } from "../../surfaces/report.ts";
|
|
84
|
+
import { installInjectedContext } from "./contextInjection.ts";
|
|
85
|
+
import {
|
|
86
|
+
checkDraftReviewDecision,
|
|
87
|
+
type DecisionCheck,
|
|
88
|
+
type DraftReviewSlot,
|
|
89
|
+
destinationChangedResult,
|
|
90
|
+
type OpenDraftReview,
|
|
91
|
+
openRefusedResult,
|
|
92
|
+
recordSaveOutcome,
|
|
93
|
+
saveUnconfirmedResult,
|
|
94
|
+
staleApprovalResult,
|
|
95
|
+
supersededReviewResult,
|
|
96
|
+
withDraftChangedNote,
|
|
97
|
+
} from "./draftReview.ts";
|
|
98
|
+
import { hasDirectEditsHeading } from "./providers/plannotator.ts";
|
|
99
|
+
import { isPlannotatorPlanSelected } from "./providers/selection.ts";
|
|
100
|
+
import {
|
|
101
|
+
approvedSubjectSaveResult,
|
|
102
|
+
type DraftReviewBridge,
|
|
103
|
+
type ReviewOutcome,
|
|
104
|
+
type ReviewSubject,
|
|
105
|
+
runFirstPartyReview,
|
|
106
|
+
skipResult,
|
|
107
|
+
subjectReviewOutcomeResult,
|
|
108
|
+
type ToolResult,
|
|
109
|
+
verdictsFor,
|
|
110
|
+
} from "./review.ts";
|
|
111
|
+
|
|
112
|
+
const SCOPE_REFINE = "objective-refine";
|
|
113
|
+
const SCOPE_SAVE = "objective-refinement-save";
|
|
114
|
+
|
|
115
|
+
// ------------------------------------------------------------------- the tool-boundary decode
|
|
116
|
+
|
|
117
|
+
/** Decode the `objective_refinement_draft` params: exactly `{ markdown: string }` (strict). */
|
|
118
|
+
export function decodeRefinementDraftParams(params: unknown): { markdown: string } | null {
|
|
119
|
+
const p = paramsOf(params);
|
|
120
|
+
if (p === null) return null;
|
|
121
|
+
const markdown = stringParam(p, "markdown");
|
|
122
|
+
if (markdown === null || markdown === undefined) return null;
|
|
123
|
+
return { markdown };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ----------------------------------------------------------------- the command-args decode
|
|
127
|
+
|
|
128
|
+
export type RefineCommandArgs =
|
|
129
|
+
| { ok: true; objective: string | null; node: string | null }
|
|
130
|
+
| { ok: false; problem: string };
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse `/objective-refine [objective] [--node ID | --node=ID]`: at most one positional (an
|
|
134
|
+
* opaque objective id, `#` tolerated), at most one `--node` with a nonblank value, nothing else.
|
|
135
|
+
* Every extra/duplicate/missing-value shape refuses (never a silent default). Pure.
|
|
136
|
+
*/
|
|
137
|
+
export function parseRefineCommandArgs(args: string): RefineCommandArgs {
|
|
138
|
+
const tokens = args
|
|
139
|
+
.trim()
|
|
140
|
+
.split(/\s+/)
|
|
141
|
+
.filter((t) => t.length > 0);
|
|
142
|
+
let objective: string | null = null;
|
|
143
|
+
let node: string | null = null;
|
|
144
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
145
|
+
const token = tokens[i] as string;
|
|
146
|
+
if (token === "--node" || token.startsWith("--node=")) {
|
|
147
|
+
if (node !== null) return { ok: false, problem: "--node may be given at most once" };
|
|
148
|
+
const value = token === "--node" ? tokens[++i] : token.slice("--node=".length);
|
|
149
|
+
if (value === undefined || value.trim() === "" || value.startsWith("--"))
|
|
150
|
+
return { ok: false, problem: "--node needs a node id (e.g. --node 2.3)" };
|
|
151
|
+
node = value;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (token.startsWith("-")) return { ok: false, problem: `unknown option ${token}` };
|
|
155
|
+
if (objective !== null) return { ok: false, problem: `unexpected extra argument ${token}` };
|
|
156
|
+
const id = token.replace(/^#/, "");
|
|
157
|
+
if (id === "") return { ok: false, problem: "the objective id must not be blank" };
|
|
158
|
+
objective = id;
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, objective, node };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ------------------------------------------------------------------- the warm admission
|
|
164
|
+
|
|
165
|
+
/** The plan-graph stages a launch handoff marks a session as plan-bearing with. */
|
|
166
|
+
const PLAN_BEARING_STAGES: readonly string[] = [
|
|
167
|
+
"plan",
|
|
168
|
+
"objective-plan",
|
|
169
|
+
"save",
|
|
170
|
+
"implement",
|
|
171
|
+
"submit",
|
|
172
|
+
"address",
|
|
173
|
+
"land",
|
|
174
|
+
"learn",
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
export type WarmRefinementAdmission =
|
|
178
|
+
| { ok: true; runId: string; activeObjective: string | null; alreadyRefining: boolean }
|
|
179
|
+
| { ok: false; code: "bad_state" | "bound_session"; problem: string };
|
|
180
|
+
|
|
181
|
+
function isNonblankString(value: unknown): value is string {
|
|
182
|
+
return typeof value === "string" && value.trim() !== "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Decide whether an existing session may enter refinement (contracts.md §8.67's warm
|
|
187
|
+
* admission), read STRICTLY over the rebuilt state + the run's launch handoff:
|
|
188
|
+
* - an unsafe/missing run identity, a malformed claim / plan-ref / stage, or an unreadable
|
|
189
|
+
* handoff → `bad_state` (never treated as absence);
|
|
190
|
+
* - a sound active plan-ref, a planning claim, or a plan-bearing launch handoff (a plan-graph
|
|
191
|
+
* stage, a planning link or an adoption source) → `bound_session` — nothing is cleared,
|
|
192
|
+
* suspended or restored;
|
|
193
|
+
* - otherwise admitted; `alreadyRefining` marks an explicit re-entry (a new grounding pass).
|
|
194
|
+
* The cached root plan-ref selector is never consulted. Pure.
|
|
195
|
+
*/
|
|
196
|
+
export function decideWarmRefinementAdmission(
|
|
197
|
+
state: WorkflowState,
|
|
198
|
+
handoff:
|
|
199
|
+
| { status: "absent" }
|
|
200
|
+
| { status: "present"; handoff: Handoff }
|
|
201
|
+
| { status: "unreadable" },
|
|
202
|
+
): WarmRefinementAdmission {
|
|
203
|
+
const bad = (problem: string): WarmRefinementAdmission => ({
|
|
204
|
+
ok: false,
|
|
205
|
+
code: "bad_state",
|
|
206
|
+
problem,
|
|
207
|
+
});
|
|
208
|
+
const bound = (problem: string): WarmRefinementAdmission => ({
|
|
209
|
+
ok: false,
|
|
210
|
+
code: "bound_session",
|
|
211
|
+
problem,
|
|
212
|
+
});
|
|
213
|
+
const runId = state.run_id;
|
|
214
|
+
if (typeof runId !== "string" || !isSafeRunId(runId)) return bad("no safe session run identity");
|
|
215
|
+
const claim = state.objective_node_claim;
|
|
216
|
+
if (claim !== undefined && claim !== null) {
|
|
217
|
+
if (
|
|
218
|
+
typeof claim !== "object" ||
|
|
219
|
+
Array.isArray(claim) ||
|
|
220
|
+
!isNonblankString((claim as { objective?: unknown }).objective) ||
|
|
221
|
+
!isNonblankString((claim as { node?: unknown }).node)
|
|
222
|
+
)
|
|
223
|
+
return bad("the session's objective_node_claim is malformed");
|
|
224
|
+
return bound(
|
|
225
|
+
`this session carries a planning claim (objective ${(claim as { objective: string }).objective} node ${(claim as { node: string }).node})`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const ref = state.active_plan_ref;
|
|
229
|
+
if (ref !== undefined && ref !== null) {
|
|
230
|
+
if (
|
|
231
|
+
typeof ref !== "object" ||
|
|
232
|
+
Array.isArray(ref) ||
|
|
233
|
+
!isNonblankString((ref as { provider?: unknown }).provider) ||
|
|
234
|
+
!isNonblankString((ref as { pr_id?: unknown }).pr_id)
|
|
235
|
+
)
|
|
236
|
+
return bad("the session's active_plan_ref is malformed");
|
|
237
|
+
return bound(
|
|
238
|
+
`this session is bound to plan ${(ref as { provider: string }).provider}:${(ref as { pr_id: string }).pr_id}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const stage = state.stage;
|
|
242
|
+
if (stage !== undefined && stage !== null && typeof stage !== "string")
|
|
243
|
+
return bad("the session's stage is malformed");
|
|
244
|
+
if (handoff.status === "unreadable") return bad("the run's launch handoff is unreadable");
|
|
245
|
+
if (handoff.status === "present") {
|
|
246
|
+
const h = handoff.handoff;
|
|
247
|
+
if (h.run_id !== runId) return bad("the run's launch handoff names another run");
|
|
248
|
+
if (isNonblankString(h.objective_id) || isNonblankString(h.node_id))
|
|
249
|
+
return bound("this session was launched with a planning link");
|
|
250
|
+
if (isNonblankString(h.adopt_from))
|
|
251
|
+
return bound("this session was launched as a plan adoption");
|
|
252
|
+
if (typeof h.stage === "string" && PLAN_BEARING_STAGES.includes(h.stage))
|
|
253
|
+
return bound(`this session was launched as a ${h.stage} session`);
|
|
254
|
+
}
|
|
255
|
+
if (typeof stage === "string" && PLAN_BEARING_STAGES.includes(stage))
|
|
256
|
+
return bound(`this session is a ${stage} session`);
|
|
257
|
+
const activeObjective = state.active_objective;
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
runId,
|
|
261
|
+
activeObjective: isNonblankString(activeObjective) ? activeObjective : null,
|
|
262
|
+
alreadyRefining: stage === REFINE_STAGE,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Read the run's handoff strictly: ENOENT is `absent`; any other failure is `unreadable`. */
|
|
267
|
+
function readHandoffStrict(
|
|
268
|
+
cwd: string,
|
|
269
|
+
runId: string,
|
|
270
|
+
): { status: "absent" } | { status: "present"; handoff: Handoff } | { status: "unreadable" } {
|
|
271
|
+
let raw: string;
|
|
272
|
+
try {
|
|
273
|
+
raw = readFileSync(handoffPath(cwd, runId), "utf8");
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
|
|
276
|
+
return { status: "absent" };
|
|
277
|
+
return { status: "unreadable" };
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const parsed: unknown = JSON.parse(raw);
|
|
281
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
282
|
+
return { status: "unreadable" };
|
|
283
|
+
const h = parsed as Record<string, unknown>;
|
|
284
|
+
if (typeof h.run_id !== "string") return { status: "unreadable" };
|
|
285
|
+
return { status: "present", handoff: h as unknown as Handoff };
|
|
286
|
+
} catch {
|
|
287
|
+
return { status: "unreadable" };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------- the cold-door adapters
|
|
292
|
+
|
|
293
|
+
/** The decoded `perk objective refine-context --json` envelope slice. */
|
|
294
|
+
interface RefineContextPayload {
|
|
295
|
+
contextJson: string;
|
|
296
|
+
contextDigest: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function decodeRefineContext(payload: ColdJson): RefineContextPayload | null {
|
|
300
|
+
const contextJson = stringField(payload, "context_json");
|
|
301
|
+
const contextDigest = stringField(payload, "context_digest");
|
|
302
|
+
if (contextJson === undefined || contextDigest === undefined) return null;
|
|
303
|
+
return { contextJson, contextDigest };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The warm context worker: `perk objective refine-context <objective> [--node ID] --run-id RID
|
|
308
|
+
* --json` — the context is a JSON STRING inside the envelope (never re-encoded here).
|
|
309
|
+
*/
|
|
310
|
+
async function fetchRefinementContext(
|
|
311
|
+
pi: ExtensionAPI,
|
|
312
|
+
ctx: ExtensionContext,
|
|
313
|
+
req: { objective: string; node: string | null; runId: string },
|
|
314
|
+
): Promise<
|
|
315
|
+
| { ok: true; contextJson: string; contextDigest: string }
|
|
316
|
+
| { ok: false; message: string; errorType: string }
|
|
317
|
+
> {
|
|
318
|
+
const args = ["objective", "refine-context", req.objective];
|
|
319
|
+
if (req.node !== null) args.push("--node", req.node);
|
|
320
|
+
args.push("--run-id", req.runId, "--json");
|
|
321
|
+
const r = await runColdDoor<RefineContextPayload>(pi, ctx, args, {
|
|
322
|
+
label: "perk objective refine-context",
|
|
323
|
+
decode: decodeRefineContext,
|
|
324
|
+
});
|
|
325
|
+
if (!r.ok) return { ok: false, message: r.message, errorType: r.errorType };
|
|
326
|
+
return { ok: true, contextJson: r.data.contextJson, contextDigest: r.data.contextDigest };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The decoded `perk objective refinement-save --json` success payload. */
|
|
330
|
+
interface RefinementSavePayload {
|
|
331
|
+
commentId: string;
|
|
332
|
+
carrierUrl: string;
|
|
333
|
+
carrierIdentifier: string;
|
|
334
|
+
objectiveId: string;
|
|
335
|
+
objectiveRunId: string;
|
|
336
|
+
nodeId: string;
|
|
337
|
+
bodyDigest: string;
|
|
338
|
+
savedAt: string;
|
|
339
|
+
authoredAt: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function decodeRefinementSave(payload: ColdJson): RefinementSavePayload | null {
|
|
343
|
+
const fields = [
|
|
344
|
+
"comment_id",
|
|
345
|
+
"carrier_url",
|
|
346
|
+
"carrier_identifier",
|
|
347
|
+
"objective_id",
|
|
348
|
+
"objective_run_id",
|
|
349
|
+
"node_id",
|
|
350
|
+
"body_digest",
|
|
351
|
+
"saved_at",
|
|
352
|
+
"authored_at",
|
|
353
|
+
] as const;
|
|
354
|
+
const values: Record<string, string> = {};
|
|
355
|
+
for (const key of fields) {
|
|
356
|
+
const value = stringField(payload, key);
|
|
357
|
+
if (value === undefined) return null;
|
|
358
|
+
values[key] = value;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
commentId: values.comment_id as string,
|
|
362
|
+
carrierUrl: values.carrier_url as string,
|
|
363
|
+
carrierIdentifier: values.carrier_identifier as string,
|
|
364
|
+
objectiveId: values.objective_id as string,
|
|
365
|
+
objectiveRunId: values.objective_run_id as string,
|
|
366
|
+
nodeId: values.node_id as string,
|
|
367
|
+
bodyDigest: values.body_digest as string,
|
|
368
|
+
savedAt: values.saved_at as string,
|
|
369
|
+
authoredAt: values.authored_at as string,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* The production `RefinementBackend` over the Python worker (`perk objective refinement-save
|
|
375
|
+
* --run-id RID --json` via the shared cold-door client; the EXACT draft bytes ride the
|
|
376
|
+
* run-scratch `--draft-file` channel). The worker reads the bound context from the run's
|
|
377
|
+
* session data itself — no caller-supplied context path, target or expectation. Failures keep
|
|
378
|
+
* the worker's typed diagnostics (`write_attempted`, `comment_ids`) beside the message.
|
|
379
|
+
*/
|
|
380
|
+
export function coldDoorRefinementBackend(
|
|
381
|
+
pi: ExtensionAPI,
|
|
382
|
+
ctx: ExtensionContext,
|
|
383
|
+
): RefinementBackend {
|
|
384
|
+
return {
|
|
385
|
+
async save(req) {
|
|
386
|
+
const r = await runColdDoor<RefinementSavePayload>(
|
|
387
|
+
pi,
|
|
388
|
+
ctx,
|
|
389
|
+
["objective", "refinement-save", "--run-id", req.runId, "--json"],
|
|
390
|
+
{
|
|
391
|
+
label: "perk objective refinement-save",
|
|
392
|
+
decode: decodeRefinementSave,
|
|
393
|
+
stdin: {
|
|
394
|
+
flag: "--draft-file",
|
|
395
|
+
content: req.draftRaw,
|
|
396
|
+
filename: REFINEMENT_DRAFT_ARTIFACT,
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
if (!r.ok) {
|
|
401
|
+
const attempted =
|
|
402
|
+
r.payload === undefined ? undefined : booleanField(r.payload, "write_attempted");
|
|
403
|
+
return {
|
|
404
|
+
status: "failed",
|
|
405
|
+
message: r.message,
|
|
406
|
+
errorType: r.errorType,
|
|
407
|
+
writeAttempted: attempted ?? null,
|
|
408
|
+
commentIds: r.payload === undefined ? [] : stringListField(r.payload, "comment_ids"),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const d = r.data;
|
|
412
|
+
return {
|
|
413
|
+
status: "saved",
|
|
414
|
+
commentId: d.commentId,
|
|
415
|
+
carrierUrl: d.carrierUrl,
|
|
416
|
+
carrierIdentifier: d.carrierIdentifier,
|
|
417
|
+
objectiveId: d.objectiveId,
|
|
418
|
+
objectiveRunId: d.objectiveRunId,
|
|
419
|
+
nodeId: d.nodeId,
|
|
420
|
+
bodyDigest: d.bodyDigest,
|
|
421
|
+
savedAt: d.savedAt,
|
|
422
|
+
authoredAt: d.authoredAt,
|
|
423
|
+
};
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ------------------------------------------------------------------------- result envelopes
|
|
429
|
+
|
|
430
|
+
/** The ok-arm fields of a verified refinement save (the structured `details` surface). */
|
|
431
|
+
export interface RefinementSaveOk {
|
|
432
|
+
refinement: {
|
|
433
|
+
comment_id: string;
|
|
434
|
+
carrier_url: string;
|
|
435
|
+
carrier_identifier: string;
|
|
436
|
+
objective_id: string;
|
|
437
|
+
objective_run_id: string;
|
|
438
|
+
node_id: string;
|
|
439
|
+
body_digest: string;
|
|
440
|
+
saved_at: string;
|
|
441
|
+
authored_at: string;
|
|
442
|
+
};
|
|
443
|
+
advisory: true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export type RefinementSaveResult = Result<
|
|
447
|
+
RefinementSaveOk,
|
|
448
|
+
{ write_attempted: boolean | null; comment_ids: string[] }
|
|
449
|
+
>;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Render a backend save outcome as the warm-door Result envelope — the ONE result-construction
|
|
453
|
+
* site for every refinement save surface (review arm + human command). A verified save says
|
|
454
|
+
* ADVISORY content — not an executable plan — was saved and terminates; a failure reports
|
|
455
|
+
* through `failFor` with the worker's diagnostics retained (never "nothing saved", never a
|
|
456
|
+
* blind-retry prescription).
|
|
457
|
+
*/
|
|
458
|
+
export function refinementSaveResultOf(
|
|
459
|
+
ctx: ExtensionContext,
|
|
460
|
+
save: RefinementBackendSaveResult,
|
|
461
|
+
): RefinementSaveResult {
|
|
462
|
+
if (save.status === "failed") {
|
|
463
|
+
return failFor<{ write_attempted: boolean | null; comment_ids: string[] }>(ctx, SCOPE_SAVE)(
|
|
464
|
+
save.message,
|
|
465
|
+
save.errorType,
|
|
466
|
+
{
|
|
467
|
+
write_attempted: save.writeAttempted,
|
|
468
|
+
comment_ids: save.commentIds,
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return ok(
|
|
473
|
+
`Saved refinement for objective ${save.objectiveId} node ${save.nodeId} → comment ` +
|
|
474
|
+
`${save.commentId} on ${save.carrierIdentifier} (${save.carrierUrl}); body digest ` +
|
|
475
|
+
`${save.bodyDigest}; saved at ${save.savedAt}.\n` +
|
|
476
|
+
"ADVISORY content — not an executable plan — was saved: no plan was created, no node was " +
|
|
477
|
+
"claimed, and no node/objective state changed.",
|
|
478
|
+
{
|
|
479
|
+
refinement: {
|
|
480
|
+
comment_id: save.commentId,
|
|
481
|
+
carrier_url: save.carrierUrl,
|
|
482
|
+
carrier_identifier: save.carrierIdentifier,
|
|
483
|
+
objective_id: save.objectiveId,
|
|
484
|
+
objective_run_id: save.objectiveRunId,
|
|
485
|
+
node_id: save.nodeId,
|
|
486
|
+
body_digest: save.bodyDigest,
|
|
487
|
+
saved_at: save.savedAt,
|
|
488
|
+
authored_at: save.authoredAt,
|
|
489
|
+
},
|
|
490
|
+
advisory: true,
|
|
491
|
+
},
|
|
492
|
+
{ terminate: true },
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ------------------------------------------------------------------------- adapter plumbing
|
|
497
|
+
|
|
498
|
+
function openSession(pi: ExtensionAPI, ctx: ExtensionContext): WorkflowSession {
|
|
499
|
+
return openBranchWorkflowSession(pi, ctx);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function gateFor(gating: ToolGating, ctx: ExtensionContext): ApprovalGate {
|
|
503
|
+
return { isActive: () => gating.isActive(), exit: () => gating.exit(ctx) };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Whether the branch is a refinement session (stage match; the gate is checked separately). */
|
|
507
|
+
export function isRefinementSession(branch: readonly BranchEntry[]): boolean {
|
|
508
|
+
return rebuildWorkflowState(branch).stage === REFINE_STAGE;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function inRefinementStage(ctx: ExtensionContext): boolean {
|
|
512
|
+
try {
|
|
513
|
+
return isRefinementSession(branchOf(ctx));
|
|
514
|
+
} catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The cold claim's one-time context import (called from `session_start` right after a
|
|
521
|
+
* successful claim): reads the consumed handoff back for its namespaced block, then imports the
|
|
522
|
+
* fixed run-scratch transfer. A refusal is reported loudly; the session stays gated and no
|
|
523
|
+
* draft can be written until a grounding pass supplies a valid context. Never throws.
|
|
524
|
+
*/
|
|
525
|
+
export function importRefinementContextOnClaim(
|
|
526
|
+
pi: ExtensionAPI,
|
|
527
|
+
ctx: ExtensionContext,
|
|
528
|
+
claim: { runId: string; stage: string | undefined },
|
|
529
|
+
): void {
|
|
530
|
+
if (claim.stage !== REFINE_STAGE) return;
|
|
531
|
+
try {
|
|
532
|
+
const handoff = readHandoff(ctx.cwd, claim.runId);
|
|
533
|
+
if (handoff === null) {
|
|
534
|
+
report(
|
|
535
|
+
ctx,
|
|
536
|
+
SCOPE_REFINE,
|
|
537
|
+
"error",
|
|
538
|
+
"context import refused — the run's handoff is unreadable",
|
|
539
|
+
{
|
|
540
|
+
alsoLog: true,
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const transferPath = join(runScratchDir(ctx.cwd, claim.runId), REFINEMENT_CONTEXT_ARTIFACT);
|
|
546
|
+
const imported = importColdRefinementContext(
|
|
547
|
+
openSession(pi, ctx),
|
|
548
|
+
{ runId: claim.runId, stage: claim.stage, handoff },
|
|
549
|
+
{
|
|
550
|
+
readTransfer() {
|
|
551
|
+
try {
|
|
552
|
+
// Fatal decoding: malformed UTF-8 is a refusal, never a silent U+FFFD substitution.
|
|
553
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(transferPath));
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (
|
|
556
|
+
typeof error === "object" &&
|
|
557
|
+
error !== null &&
|
|
558
|
+
"code" in error &&
|
|
559
|
+
error.code === "ENOENT"
|
|
560
|
+
)
|
|
561
|
+
return null;
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
);
|
|
567
|
+
if (imported.status === "refused") {
|
|
568
|
+
report(ctx, SCOPE_REFINE, "error", `context import refused — ${imported.problem}`, {
|
|
569
|
+
alsoLog: true,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
report(ctx, SCOPE_REFINE, "error", `context import failed — ${String(error)}`, {
|
|
574
|
+
alsoLog: true,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ------------------------------------------------------------------------------ the installer
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Install every refinement Pi binding: the refinement context hook pair, the
|
|
583
|
+
* `objective_refinement_draft` tool, the warm `/objective-refine` entry and the human
|
|
584
|
+
* `/objective-refinement-save` failsafe. Inert outside refinement sessions; never throws.
|
|
585
|
+
*/
|
|
586
|
+
export function installObjectiveRefinementBindings(
|
|
587
|
+
pi: ExtensionAPI,
|
|
588
|
+
gating: ToolGating,
|
|
589
|
+
reviews: DraftReviewSlot,
|
|
590
|
+
runnerChild: () => boolean,
|
|
591
|
+
): void {
|
|
592
|
+
// The refinement context is keyed off (the gate active AND the refinement stage on the FULL
|
|
593
|
+
// branch — the module's own `isRefinementSession`); once the approved save exits the gate the
|
|
594
|
+
// selection turns null and the shared helper retires the owned copy. The runner fence lives in
|
|
595
|
+
// the shared helper.
|
|
596
|
+
installInjectedContext(
|
|
597
|
+
pi,
|
|
598
|
+
{
|
|
599
|
+
customType: REFINEMENT_CONTEXT_TYPE,
|
|
600
|
+
flavors: {
|
|
601
|
+
[REFINEMENT_MARKER]: (ctx) =>
|
|
602
|
+
refinementContextContent(loadPerkConfig(ctx.cwd).planAuthoring),
|
|
603
|
+
},
|
|
604
|
+
select: (_ctx, branch) =>
|
|
605
|
+
gating.isActive() && isRefinementSession(branch) ? REFINEMENT_MARKER : null,
|
|
606
|
+
},
|
|
607
|
+
runnerChild,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
pi.registerTool({
|
|
611
|
+
name: "objective_refinement_draft",
|
|
612
|
+
label: "Refinement draft",
|
|
613
|
+
description:
|
|
614
|
+
"Write (or overwrite) the working objective-node refinement — the advisory Markdown — to " +
|
|
615
|
+
"the session data dir, bound to this session's grounding context. The only sanctioned " +
|
|
616
|
+
"write surface in a refinement session. NOT a save: an APPROVED plan_review, or the " +
|
|
617
|
+
"human's /objective-refinement-save, persists the node's refinement comment.",
|
|
618
|
+
promptSnippet:
|
|
619
|
+
"Persist the working refinement Markdown to the session data dir (full rewrite, context-bound)",
|
|
620
|
+
promptGuidelines: REFINEMENT_DRAFT_TOOL_GUIDELINES,
|
|
621
|
+
executionMode: "sequential",
|
|
622
|
+
parameters: {
|
|
623
|
+
type: "object",
|
|
624
|
+
additionalProperties: false,
|
|
625
|
+
required: ["markdown"],
|
|
626
|
+
properties: {
|
|
627
|
+
markdown: {
|
|
628
|
+
type: "string",
|
|
629
|
+
description:
|
|
630
|
+
"The full refinement Markdown: what the node must deliver, the prerequisites that " +
|
|
631
|
+
"do not exist yet, the code seams as observed at capture time, the risks, and the " +
|
|
632
|
+
"assumptions a later real plan must re-verify.",
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
637
|
+
const fail = failFor(ctx, "objective-refinement-draft", "objective_refinement_draft");
|
|
638
|
+
const decoded = decodeRefinementDraftParams(params);
|
|
639
|
+
if (decoded === null)
|
|
640
|
+
return fail("objective_refinement_draft needs { markdown: string }", "bad_input");
|
|
641
|
+
// Independent of tool visibility: the draft writer only exists inside a refinement session.
|
|
642
|
+
if (!inRefinementStage(ctx))
|
|
643
|
+
return fail(
|
|
644
|
+
"objective_refinement_draft is only available in an objective-refine session " +
|
|
645
|
+
"(enter one with perk objective refine <objective> or /objective-refine)",
|
|
646
|
+
"wrong_stage",
|
|
647
|
+
);
|
|
648
|
+
const revised = reviseRefinementDraft(decoded, openSession(pi, ctx));
|
|
649
|
+
switch (revised.status) {
|
|
650
|
+
case "revised":
|
|
651
|
+
case "unchanged": {
|
|
652
|
+
const c = revised.context.context;
|
|
653
|
+
return ok(
|
|
654
|
+
`Refinement draft written → ${revised.receipt.path} (${revised.receipt.digest}); ` +
|
|
655
|
+
`bound to objective ${c.objective.id} node ${c.target.identity.node_id} ` +
|
|
656
|
+
`(context ${revised.context.digest})`,
|
|
657
|
+
{
|
|
658
|
+
name: REFINEMENT_DRAFT_ARTIFACT,
|
|
659
|
+
path: revised.receipt.path,
|
|
660
|
+
digest: revised.receipt.digest,
|
|
661
|
+
bytes: revised.bytes,
|
|
662
|
+
run_id: revised.receipt.runId,
|
|
663
|
+
context_digest: revised.context.digest,
|
|
664
|
+
objective_id: c.objective.id,
|
|
665
|
+
node_id: c.target.identity.node_id,
|
|
666
|
+
unchanged: revised.status === "unchanged",
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
case "rejected":
|
|
671
|
+
return fail(
|
|
672
|
+
revised.problem,
|
|
673
|
+
revised.reason === "blank_markdown"
|
|
674
|
+
? "invalid_input"
|
|
675
|
+
: revised.reason === "no_identity"
|
|
676
|
+
? "no_run_id"
|
|
677
|
+
: revised.reason === "no_context"
|
|
678
|
+
? "refinement_context_missing"
|
|
679
|
+
: revised.reason === "context_refused"
|
|
680
|
+
? "refinement_context_invalid"
|
|
681
|
+
: "write_failed",
|
|
682
|
+
);
|
|
683
|
+
case "unverified":
|
|
684
|
+
return fail(revised.problem, "write_failed");
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
registerPerkCommand(pi, "objective-refine", {
|
|
690
|
+
description:
|
|
691
|
+
"Enter an objective-node refinement pass in this session: [objective] [--node ID] " +
|
|
692
|
+
"(else the active objective; the first refinable future node). Read-only.",
|
|
693
|
+
handler: async (args, ctx) => {
|
|
694
|
+
const warn = (message: string) => report(ctx, SCOPE_REFINE, "warning", message);
|
|
695
|
+
const parsed = parseRefineCommandArgs(args ?? "");
|
|
696
|
+
if (!parsed.ok) {
|
|
697
|
+
warn(`${parsed.problem}. Usage: /objective-refine [objective] [--node ID]`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (!ctx.isIdle()) {
|
|
701
|
+
warn("the model is running — wait for the turn to finish, then re-run /objective-refine");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const state = rebuildWorkflowState(branchOf(ctx));
|
|
705
|
+
const runIdForHandoff = typeof state.run_id === "string" ? state.run_id : "";
|
|
706
|
+
const admission = decideWarmRefinementAdmission(
|
|
707
|
+
state,
|
|
708
|
+
runIdForHandoff && isSafeRunId(runIdForHandoff)
|
|
709
|
+
? readHandoffStrict(ctx.cwd, runIdForHandoff)
|
|
710
|
+
: { status: "absent" },
|
|
711
|
+
);
|
|
712
|
+
const objectiveHint = parsed.objective ?? "<objective>";
|
|
713
|
+
if (!admission.ok) {
|
|
714
|
+
warn(
|
|
715
|
+
`${admission.problem} — refusing (${admission.code}). Start a fresh session with ` +
|
|
716
|
+
`\`perk objective refine ${objectiveHint}${parsed.node ? ` --node ${parsed.node}` : ""}\`.`,
|
|
717
|
+
);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const objective = parsed.objective ?? admission.activeObjective;
|
|
721
|
+
if (objective === null) {
|
|
722
|
+
warn(
|
|
723
|
+
"no objective given and none active (objective_required). Use " +
|
|
724
|
+
"`/objective-refine <objective> [--node ID]`.",
|
|
725
|
+
);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
report(
|
|
729
|
+
ctx,
|
|
730
|
+
SCOPE_REFINE,
|
|
731
|
+
"info",
|
|
732
|
+
`${objective}${parsed.node ? ` node ${parsed.node}` : ""}${admission.alreadyRefining ? " (new grounding pass)" : ""}`,
|
|
733
|
+
);
|
|
734
|
+
const fetched = await fetchRefinementContext(pi, ctx, {
|
|
735
|
+
objective,
|
|
736
|
+
node: parsed.node,
|
|
737
|
+
runId: admission.runId,
|
|
738
|
+
});
|
|
739
|
+
if (!fetched.ok) {
|
|
740
|
+
report(ctx, SCOPE_REFINE, "error", `${fetched.message} (${fetched.errorType})`, {
|
|
741
|
+
alsoLog: true,
|
|
742
|
+
});
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const validated = validateContextTransfer(fetched.contextJson, {
|
|
746
|
+
runId: admission.runId,
|
|
747
|
+
expectedDigest: fetched.contextDigest,
|
|
748
|
+
});
|
|
749
|
+
if (!validated.ok) {
|
|
750
|
+
report(
|
|
751
|
+
ctx,
|
|
752
|
+
SCOPE_REFINE,
|
|
753
|
+
"error",
|
|
754
|
+
`the prepared context is invalid — ${validated.problem} (refinement_context_invalid)`,
|
|
755
|
+
{ alsoLog: true },
|
|
756
|
+
);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
// Recheck the same run/admission against LIVE state, persist the exact raw context, then
|
|
760
|
+
// the stage-only entry. A new grounding pass rebinds the reviewed context digest, so any
|
|
761
|
+
// open review's approval is refused by the reviewed-bytes guard (`draftReview.ts`).
|
|
762
|
+
type Entered =
|
|
763
|
+
| { status: "entered" | "unchanged"; path: string; read: RefinementContextRead }
|
|
764
|
+
| { status: "refused"; problem: string };
|
|
765
|
+
const enter = (session: WorkflowSession): Entered => {
|
|
766
|
+
const live = rebuildWorkflowState(branchOf(ctx));
|
|
767
|
+
const runId = typeof live.run_id === "string" ? live.run_id : "";
|
|
768
|
+
const again = decideWarmRefinementAdmission(
|
|
769
|
+
live,
|
|
770
|
+
runId && isSafeRunId(runId) ? readHandoffStrict(ctx.cwd, runId) : { status: "absent" },
|
|
771
|
+
);
|
|
772
|
+
if (!again.ok) return { status: "refused", problem: `${again.problem} (${again.code})` };
|
|
773
|
+
if (again.runId !== admission.runId)
|
|
774
|
+
return { status: "refused", problem: "the session identity changed (bad_state)" };
|
|
775
|
+
const imported = importRefinementContext(session, validated.read);
|
|
776
|
+
if (imported.status === "rejected" || imported.status === "unverified")
|
|
777
|
+
return {
|
|
778
|
+
status: "refused",
|
|
779
|
+
problem: `could not persist the context (${imported.problem})`,
|
|
780
|
+
};
|
|
781
|
+
const staged = session.apply({ kind: "enter-refinement-stage" });
|
|
782
|
+
if (staged.status === "rejected" || staged.status === "unverified")
|
|
783
|
+
return {
|
|
784
|
+
status: "refused",
|
|
785
|
+
problem:
|
|
786
|
+
`the context was persisted but the stage entry failed (${staged.problem}) — the ` +
|
|
787
|
+
"session is not in refinement; re-run /objective-refine",
|
|
788
|
+
};
|
|
789
|
+
return {
|
|
790
|
+
status:
|
|
791
|
+
imported.status === "unchanged" && staged.status === "unchanged"
|
|
792
|
+
? "unchanged"
|
|
793
|
+
: "entered",
|
|
794
|
+
path: imported.receipt.path,
|
|
795
|
+
read: imported.read,
|
|
796
|
+
};
|
|
797
|
+
};
|
|
798
|
+
const entered = enter(openSession(pi, ctx));
|
|
799
|
+
if (entered.status === "refused") {
|
|
800
|
+
report(ctx, SCOPE_REFINE, "error", entered.problem, { alsoLog: true });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
// Enter/re-scope the gate for the refinement stage (its own allowlist + mode flavor), then
|
|
804
|
+
// drive the shared flow guidance. Failed setup above never reaches this point.
|
|
805
|
+
if (!gating.isActive()) gating.enter(ctx);
|
|
806
|
+
gating.syncFromState("read-only", REFINE_STAGE);
|
|
807
|
+
const c = entered.read.context;
|
|
808
|
+
report(
|
|
809
|
+
ctx,
|
|
810
|
+
SCOPE_REFINE,
|
|
811
|
+
"info",
|
|
812
|
+
`${entered.status === "unchanged" ? "context unchanged" : "context prepared"} for objective ` +
|
|
813
|
+
`${c.objective.id} node ${c.target.identity.node_id} (${c.target.status}` +
|
|
814
|
+
`${c.prior !== null ? "; re-refining a prior refinement" : ""}) → ${entered.path}; ` +
|
|
815
|
+
"read-only ON — author with objective_refinement_draft, review with plan_review.",
|
|
816
|
+
);
|
|
817
|
+
pi.sendUserMessage(
|
|
818
|
+
refinementGuidance(c, entered.path) + bindingSuffix(ctx.cwd, `stage:${REFINE_STAGE}`),
|
|
819
|
+
);
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
registerPerkCommand(pi, "objective-refinement-save", {
|
|
824
|
+
description:
|
|
825
|
+
"Save the current validated refinement draft as the node's refinement comment — the " +
|
|
826
|
+
"human's explicit save gesture (no arguments; artifact-first; refinement sessions only).",
|
|
827
|
+
handler: async (args, ctx) => {
|
|
828
|
+
const warn = (message: string) => report(ctx, SCOPE_SAVE, "warning", message);
|
|
829
|
+
if ((args ?? "").trim() !== "") {
|
|
830
|
+
warn("/objective-refinement-save takes no arguments (invalid_input)");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (!inRefinementStage(ctx)) {
|
|
834
|
+
warn(
|
|
835
|
+
"not an objective-refine session (wrong_stage) — nothing saved. Enter one with " +
|
|
836
|
+
"`perk objective refine <objective>` or /objective-refine.",
|
|
837
|
+
);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (!ctx.isIdle()) {
|
|
841
|
+
warn("the model is running (session_busy) — wait for the turn to finish, then re-run");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
// The command IS the human authorization — the deliberate retry the unconfirmed-save latch
|
|
845
|
+
// points at, so it never consults the latch (but reports into it). The shared seam
|
|
846
|
+
// strict-resumes both artifacts and saves the EXACT draft bytes; the gate exits only on a
|
|
847
|
+
// verified save.
|
|
848
|
+
const outcome = await refinementApprovalSave({
|
|
849
|
+
session: openSession(pi, ctx),
|
|
850
|
+
backend: coldDoorRefinementBackend(pi, ctx),
|
|
851
|
+
gate: gateFor(gating, ctx),
|
|
852
|
+
});
|
|
853
|
+
recordRefinementSaveOutcome(reviews, outcome);
|
|
854
|
+
switch (outcome.status) {
|
|
855
|
+
case "no-draft":
|
|
856
|
+
report(
|
|
857
|
+
ctx,
|
|
858
|
+
SCOPE_SAVE,
|
|
859
|
+
"error",
|
|
860
|
+
"no working refinement draft — nothing saved. Write it with objective_refinement_draft " +
|
|
861
|
+
"(the model's tool), then re-run /objective-refinement-save.",
|
|
862
|
+
);
|
|
863
|
+
return;
|
|
864
|
+
case "no-context":
|
|
865
|
+
report(
|
|
866
|
+
ctx,
|
|
867
|
+
SCOPE_SAVE,
|
|
868
|
+
"error",
|
|
869
|
+
"no refinement context in this session — nothing saved. Re-enter a grounding pass " +
|
|
870
|
+
"with /objective-refine, rewrite the draft, then re-run.",
|
|
871
|
+
);
|
|
872
|
+
return;
|
|
873
|
+
case "refused-draft":
|
|
874
|
+
report(
|
|
875
|
+
ctx,
|
|
876
|
+
SCOPE_SAVE,
|
|
877
|
+
"error",
|
|
878
|
+
`the working refinement draft is invalid: ${outcome.problem} — nothing saved. ` +
|
|
879
|
+
"Rewrite it with objective_refinement_draft, then re-run /objective-refinement-save.",
|
|
880
|
+
);
|
|
881
|
+
return;
|
|
882
|
+
case "source-changed":
|
|
883
|
+
// Unreachable without a `reviewed` pair (the manual save reviews nothing) — kept total.
|
|
884
|
+
report(ctx, SCOPE_SAVE, "error", "the refinement pair changed — nothing saved; re-run");
|
|
885
|
+
return;
|
|
886
|
+
case "saved": {
|
|
887
|
+
const result = refinementSaveResultOf(ctx, outcome.save);
|
|
888
|
+
// One headline line: the verified facts AND the advisory statement stay visible.
|
|
889
|
+
report(
|
|
890
|
+
ctx,
|
|
891
|
+
SCOPE_SAVE,
|
|
892
|
+
"info",
|
|
893
|
+
`manual human save (not a reviewer approval): ${(result.content[0]?.text ?? "saved").replace(/\n/g, " ")}`,
|
|
894
|
+
);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
case "save-failed": {
|
|
898
|
+
const s = outcome.save;
|
|
899
|
+
const diagnostics =
|
|
900
|
+
`write_attempted=${s.writeAttempted === null ? "unknown" : String(s.writeAttempted)}` +
|
|
901
|
+
(s.commentIds.length > 0 ? `; comment_ids=${s.commentIds.join(",")}` : "");
|
|
902
|
+
report(
|
|
903
|
+
ctx,
|
|
904
|
+
SCOPE_SAVE,
|
|
905
|
+
"error",
|
|
906
|
+
`manual save FAILED (${s.errorType}): ${s.message} [${diagnostics}] — the session ` +
|
|
907
|
+
"stays read-only; read the node's comments back and reconcile before another attempt.",
|
|
908
|
+
);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Report a refinement approval-save outcome into the unconfirmed-save latch. */
|
|
917
|
+
function recordRefinementSaveOutcome(
|
|
918
|
+
slot: DraftReviewSlot,
|
|
919
|
+
outcome: RefinementApprovalSaveOutcome,
|
|
920
|
+
): void {
|
|
921
|
+
switch (outcome.status) {
|
|
922
|
+
case "saved":
|
|
923
|
+
recordSaveOutcome(slot, "refinement", { confirmed: true });
|
|
924
|
+
return;
|
|
925
|
+
case "save-failed":
|
|
926
|
+
recordSaveOutcome(slot, "refinement", {
|
|
927
|
+
confirmed: false,
|
|
928
|
+
detail: `${outcome.save.message} (write attempted: ${
|
|
929
|
+
outcome.save.writeAttempted === null ? "unknown" : String(outcome.save.writeAttempted)
|
|
930
|
+
})`,
|
|
931
|
+
});
|
|
932
|
+
return;
|
|
933
|
+
case "no-draft":
|
|
934
|
+
case "no-context":
|
|
935
|
+
case "refused-draft":
|
|
936
|
+
case "source-changed":
|
|
937
|
+
// Nothing reached the backend — no attempt to confirm.
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ------------------------------------------------------------------------ the review arm
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* The refinement review subject. It carries no static `saveDestination`: the first-party approve
|
|
946
|
+
* label names the bound context's carrier per backend (`refinementSaveDestination`), so every
|
|
947
|
+
* `verdictsFor` call for this subject spreads the derived destination in. The outcome texts read
|
|
948
|
+
* the other fields only.
|
|
949
|
+
*/
|
|
950
|
+
const REFINEMENT_SUBJECT: ReviewSubject = {
|
|
951
|
+
noun: "refinement",
|
|
952
|
+
present: "the complete refinement to the user",
|
|
953
|
+
presentUnavailable: "the complete refinement to the user",
|
|
954
|
+
implementHereWhere: "on the refinement path",
|
|
955
|
+
draftTool: "objective_refinement_draft",
|
|
956
|
+
failsafeCmd: "/objective-refinement-save",
|
|
957
|
+
detailsExtra: { subject: "refinement" },
|
|
958
|
+
noSourceError: "no refinement draft resolved",
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* The save destination the first-party approve verdict names, derived from the bound context's
|
|
963
|
+
* `identity.backend` (the carrier the save is bound to — never the checkout's `[issues]`
|
|
964
|
+
* config, which the service fences separately). Pure; never throws: an unrecognized backend id
|
|
965
|
+
* renders verbatim so the label never lies about where the save lands.
|
|
966
|
+
*/
|
|
967
|
+
export function refinementSaveDestination(backend: string): string {
|
|
968
|
+
switch (backend) {
|
|
969
|
+
case "linear":
|
|
970
|
+
return "Linear (the node's refinement comment)";
|
|
971
|
+
case "github":
|
|
972
|
+
return "GitHub (the objective issue's refinement comment)";
|
|
973
|
+
default:
|
|
974
|
+
return `${backend} (the node's refinement comment)`;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const REFINEMENT_REVIEW_EDITOR_TITLE =
|
|
979
|
+
"Refinement review (view only — edits are not saved) — Enter: continue to verdict · Esc: " +
|
|
980
|
+
"skip · Ctrl+G: $EDITOR";
|
|
981
|
+
|
|
982
|
+
function noRefinementDraftResult(): ToolResult {
|
|
983
|
+
return {
|
|
984
|
+
content: [
|
|
985
|
+
{
|
|
986
|
+
type: "text",
|
|
987
|
+
text:
|
|
988
|
+
"no refinement draft to review — write the working refinement with " +
|
|
989
|
+
"objective_refinement_draft, then call plan_review again.",
|
|
990
|
+
},
|
|
991
|
+
],
|
|
992
|
+
details: {
|
|
993
|
+
ok: false,
|
|
994
|
+
error: "no refinement draft to review — write it with objective_refinement_draft first",
|
|
995
|
+
error_type: "no_refinement_draft",
|
|
996
|
+
status: "skipped",
|
|
997
|
+
reason: "no_refinement_draft",
|
|
998
|
+
subject: "refinement",
|
|
999
|
+
},
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function noRefinementContextResult(): ToolResult {
|
|
1004
|
+
return {
|
|
1005
|
+
content: [
|
|
1006
|
+
{
|
|
1007
|
+
type: "text",
|
|
1008
|
+
text:
|
|
1009
|
+
"no refinement context in this session — the working draft cannot be reviewed. " +
|
|
1010
|
+
"Ask the human to re-enter a grounding pass with /objective-refine, then rewrite the " +
|
|
1011
|
+
"draft with objective_refinement_draft and call plan_review again.",
|
|
1012
|
+
},
|
|
1013
|
+
],
|
|
1014
|
+
details: {
|
|
1015
|
+
ok: false,
|
|
1016
|
+
error: "no refinement context — a grounding pass is required",
|
|
1017
|
+
error_type: "refinement_context_missing",
|
|
1018
|
+
status: "skipped",
|
|
1019
|
+
reason: "no_refinement_context",
|
|
1020
|
+
subject: "refinement",
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** Translate a review-door outcome into the feature's `RefinementReviewOutcome`. */
|
|
1026
|
+
function refinementOutcomeOf(outcome: ReviewOutcome): RefinementReviewOutcome {
|
|
1027
|
+
switch (outcome.status) {
|
|
1028
|
+
case "completed": {
|
|
1029
|
+
const carried = {
|
|
1030
|
+
...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
|
|
1031
|
+
reviewId: outcome.reviewId,
|
|
1032
|
+
};
|
|
1033
|
+
if (outcome.approved) {
|
|
1034
|
+
if (outcome.feedback !== undefined && hasDirectEditsHeading(outcome.feedback))
|
|
1035
|
+
return { status: "approvedDirectEdits", feedback: outcome.feedback, ...carried };
|
|
1036
|
+
return { status: "approved", ...carried };
|
|
1037
|
+
}
|
|
1038
|
+
return { status: "denied", ...carried };
|
|
1039
|
+
}
|
|
1040
|
+
case "unavailable":
|
|
1041
|
+
return { status: "unavailable", warning: outcome.warning };
|
|
1042
|
+
case "aborted":
|
|
1043
|
+
return { status: "aborted" };
|
|
1044
|
+
case "dismissed":
|
|
1045
|
+
case "implement-here":
|
|
1046
|
+
return { status: "dismissed" };
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* The first-party reviewer: the in-TUI editor review, VIEW-ONLY (3 verdicts). Returns the door
|
|
1052
|
+
* vocabulary (`ReviewOutcome`) so both arms share one ladder + completion path. The approve
|
|
1053
|
+
* verdict names the bound context's carrier for `backend` (`refinementSaveDestination`), so the
|
|
1054
|
+
* human approves a save to the place it actually lands.
|
|
1055
|
+
*/
|
|
1056
|
+
async function firstPartyRefinementReview(
|
|
1057
|
+
ctx: ExtensionContext,
|
|
1058
|
+
rendered: string,
|
|
1059
|
+
signal: AbortSignal | undefined,
|
|
1060
|
+
backend: string,
|
|
1061
|
+
): Promise<ReviewOutcome> {
|
|
1062
|
+
const fp = await runFirstPartyReview({
|
|
1063
|
+
ui: ctx.ui,
|
|
1064
|
+
plan: rendered,
|
|
1065
|
+
writeDraft: () => true,
|
|
1066
|
+
signal,
|
|
1067
|
+
editorTitle: REFINEMENT_REVIEW_EDITOR_TITLE,
|
|
1068
|
+
verdicts: verdictsFor({
|
|
1069
|
+
...REFINEMENT_SUBJECT,
|
|
1070
|
+
saveDestination: refinementSaveDestination(backend),
|
|
1071
|
+
}),
|
|
1072
|
+
viewOnly: true,
|
|
1073
|
+
});
|
|
1074
|
+
return fp.outcome;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** Whether a completed bridge outcome's effect is a save (an approval without Direct Edits). */
|
|
1078
|
+
function refinementEffectOf(
|
|
1079
|
+
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
1080
|
+
): "save" | "revision" {
|
|
1081
|
+
return outcome.approved &&
|
|
1082
|
+
!(outcome.feedback !== undefined && hasDirectEditsHeading(outcome.feedback))
|
|
1083
|
+
? "save"
|
|
1084
|
+
: "revision";
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** Render a non-`proceed` ladder verdict as the refinement arm's tool result (nothing saved). */
|
|
1088
|
+
function refinementGuardResult(
|
|
1089
|
+
check: Exclude<DecisionCheck, { kind: "proceed" }>,
|
|
1090
|
+
review: OpenDraftReview,
|
|
1091
|
+
feedback: string | undefined,
|
|
1092
|
+
): ToolResult {
|
|
1093
|
+
switch (check.kind) {
|
|
1094
|
+
case "superseded":
|
|
1095
|
+
return supersededReviewResult("refinement");
|
|
1096
|
+
case "save-unconfirmed":
|
|
1097
|
+
return saveUnconfirmedResult("refinement", review.runId, check.detail, feedback);
|
|
1098
|
+
case "stale-approval":
|
|
1099
|
+
return staleApprovalResult("refinement", check.reviewedDigest, feedback);
|
|
1100
|
+
case "destination-changed":
|
|
1101
|
+
return destinationChangedResult("refinement", check.changed, feedback);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function completedOutcome(
|
|
1106
|
+
approved: boolean,
|
|
1107
|
+
carried: { feedback?: string; reviewId?: string },
|
|
1108
|
+
): Extract<ReviewOutcome, { status: "completed" }> {
|
|
1109
|
+
return {
|
|
1110
|
+
status: "completed",
|
|
1111
|
+
approved,
|
|
1112
|
+
...(carried.feedback !== undefined ? { feedback: carried.feedback } : {}),
|
|
1113
|
+
reviewId: carried.reviewId ?? "",
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* The `plan_review` refinement arm: headless soft-skip; the validated (draft, context) PAIR as
|
|
1119
|
+
* the sole review source (a well-typed `plan` param is ignored upstream); the slot open with the
|
|
1120
|
+
* exact pair (`raw` = the draft bytes, `contextDigest` = the grounding context's digest — the
|
|
1121
|
+
* reviewed-bytes guard compares BOTH, so a re-prepared context also refuses the approval);
|
|
1122
|
+
* reviewer dispatch (plannotator bridge or the first-party view-only editor); the decision
|
|
1123
|
+
* ladder; the shared completion. An approval carrying Direct Edits returns the NON-terminating
|
|
1124
|
+
* revise round with nothing saved; a plain approval re-resumes the pair through
|
|
1125
|
+
* `refinementApprovalSave` with the reviewed pair as its content guard (gate released only after
|
|
1126
|
+
* the verified save).
|
|
1127
|
+
*/
|
|
1128
|
+
export async function runRefinementReviewV1(
|
|
1129
|
+
pi: ExtensionAPI,
|
|
1130
|
+
ctx: ExtensionContext,
|
|
1131
|
+
gating: ToolGating,
|
|
1132
|
+
bridge: DraftReviewBridge,
|
|
1133
|
+
slot: DraftReviewSlot,
|
|
1134
|
+
signal?: AbortSignal,
|
|
1135
|
+
): Promise<ToolResult> {
|
|
1136
|
+
if (!ctx.hasUI) return skipResult();
|
|
1137
|
+
const sig = signal ?? ctx.signal;
|
|
1138
|
+
if (sig?.aborted) return subjectReviewOutcomeResult(REFINEMENT_SUBJECT, { status: "aborted" });
|
|
1139
|
+
const session = openSession(pi, ctx);
|
|
1140
|
+
const resumed = resumeRefinementDraft(session);
|
|
1141
|
+
if (resumed.kind === "absent") return noRefinementDraftResult();
|
|
1142
|
+
if (resumed.kind === "no-context") return noRefinementContextResult();
|
|
1143
|
+
if (resumed.kind === "refused" || resumed.kind === "mismatch")
|
|
1144
|
+
return renderRefinementReviewResult(ctx, { status: "refusedDraft", problem: resumed.problem });
|
|
1145
|
+
const reviewed = reviewedPairOf(resumed.pair);
|
|
1146
|
+
const rendered = renderRefinementDraft(resumed.pair);
|
|
1147
|
+
const plannotator = isPlannotatorPlanSelected(ctx.cwd);
|
|
1148
|
+
const opened = slot.open(ctx, {
|
|
1149
|
+
subject: "refinement",
|
|
1150
|
+
source: plannotator ? "artifact" : "editor",
|
|
1151
|
+
raw: reviewed.draftRaw,
|
|
1152
|
+
markdown: rendered,
|
|
1153
|
+
contextDigest: reviewed.contextDigest,
|
|
1154
|
+
});
|
|
1155
|
+
if (!opened.ok) return openRefusedResult(opened);
|
|
1156
|
+
const review = opened.review;
|
|
1157
|
+
const outcome = plannotator
|
|
1158
|
+
? await bridge.review(rendered, sig)
|
|
1159
|
+
: await firstPartyRefinementReview(
|
|
1160
|
+
ctx,
|
|
1161
|
+
rendered,
|
|
1162
|
+
sig,
|
|
1163
|
+
resumed.pair.context.context.target.identity.backend,
|
|
1164
|
+
);
|
|
1165
|
+
if (sig?.aborted) return subjectReviewOutcomeResult(REFINEMENT_SUBJECT, { status: "aborted" });
|
|
1166
|
+
if (outcome.status !== "completed")
|
|
1167
|
+
return subjectReviewOutcomeResult(REFINEMENT_SUBJECT, outcome);
|
|
1168
|
+
const check = checkDraftReviewDecision(slot, ctx, review, refinementEffectOf(outcome));
|
|
1169
|
+
if (check.kind !== "proceed") return refinementGuardResult(check, review, outcome.feedback);
|
|
1170
|
+
return withDraftChangedNote(
|
|
1171
|
+
await completeRefinementReviewV1(pi, ctx, gating, slot, outcome, reviewed),
|
|
1172
|
+
check.draftChanged,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export function renderRefinementReviewResult(
|
|
1177
|
+
ctx: ExtensionContext,
|
|
1178
|
+
result: ReviewRefinementResult,
|
|
1179
|
+
): ToolResult {
|
|
1180
|
+
switch (result.status) {
|
|
1181
|
+
case "noDraft":
|
|
1182
|
+
return noRefinementDraftResult();
|
|
1183
|
+
case "noContext":
|
|
1184
|
+
return noRefinementContextResult();
|
|
1185
|
+
case "refusedDraft":
|
|
1186
|
+
return {
|
|
1187
|
+
content: [
|
|
1188
|
+
{
|
|
1189
|
+
type: "text",
|
|
1190
|
+
text:
|
|
1191
|
+
`the working refinement draft is invalid: ${result.problem} — rewrite it with ` +
|
|
1192
|
+
"objective_refinement_draft, then call plan_review again.",
|
|
1193
|
+
},
|
|
1194
|
+
],
|
|
1195
|
+
details: {
|
|
1196
|
+
ok: false,
|
|
1197
|
+
error: result.problem,
|
|
1198
|
+
error_type: "bad_state",
|
|
1199
|
+
status: "skipped",
|
|
1200
|
+
reason: "refinement_draft_refused",
|
|
1201
|
+
subject: "refinement",
|
|
1202
|
+
},
|
|
1203
|
+
};
|
|
1204
|
+
case "directEditsRevise":
|
|
1205
|
+
return {
|
|
1206
|
+
content: [
|
|
1207
|
+
{
|
|
1208
|
+
type: "text",
|
|
1209
|
+
text:
|
|
1210
|
+
"refinement APPROVED with direct browser edits — nothing was saved. Fold the " +
|
|
1211
|
+
"Markdown hunks below into one objective_refinement_draft rewrite; hunks against " +
|
|
1212
|
+
"the header (objective, node, carrier, pass time, checkout observation) are bound " +
|
|
1213
|
+
"metadata — they need a new grounding pass (/objective-refine), never fabricated " +
|
|
1214
|
+
`values. Then call plan_review again to confirm.\n\nReviewer feedback:\n${result.feedback}`,
|
|
1215
|
+
},
|
|
1216
|
+
],
|
|
1217
|
+
details: {
|
|
1218
|
+
ok: true,
|
|
1219
|
+
status: "revise",
|
|
1220
|
+
reason: "direct_edits",
|
|
1221
|
+
approved: true,
|
|
1222
|
+
feedback: result.feedback,
|
|
1223
|
+
reviewId: result.reviewId,
|
|
1224
|
+
subject: "refinement",
|
|
1225
|
+
},
|
|
1226
|
+
};
|
|
1227
|
+
case "approvedSaved":
|
|
1228
|
+
return approvedSubjectSaveResult(REFINEMENT_SUBJECT, completedOutcome(true, result), {
|
|
1229
|
+
status: "saved",
|
|
1230
|
+
result: refinementSaveResultOf(ctx, result.save.save),
|
|
1231
|
+
gateExited: result.save.gateExited,
|
|
1232
|
+
});
|
|
1233
|
+
case "approvedSaveFailed":
|
|
1234
|
+
return approvedSubjectSaveResult(REFINEMENT_SUBJECT, completedOutcome(true, result), {
|
|
1235
|
+
status: "save-failed",
|
|
1236
|
+
result: refinementSaveResultOf(ctx, result.save.save),
|
|
1237
|
+
gateExited: false,
|
|
1238
|
+
});
|
|
1239
|
+
case "approvedNoDraft":
|
|
1240
|
+
return approvedSubjectSaveResult(REFINEMENT_SUBJECT, completedOutcome(true, result), {
|
|
1241
|
+
status: "no-source",
|
|
1242
|
+
});
|
|
1243
|
+
case "approvedRefusedDraft":
|
|
1244
|
+
return approvedSubjectSaveResult(REFINEMENT_SUBJECT, completedOutcome(true, result), {
|
|
1245
|
+
status: "refused-draft",
|
|
1246
|
+
problem: result.problem,
|
|
1247
|
+
});
|
|
1248
|
+
case "approvedSourceChanged": {
|
|
1249
|
+
const what =
|
|
1250
|
+
result.changed === "context"
|
|
1251
|
+
? "grounding context was re-prepared"
|
|
1252
|
+
: "working draft was rewritten";
|
|
1253
|
+
return {
|
|
1254
|
+
content: [
|
|
1255
|
+
{
|
|
1256
|
+
type: "text",
|
|
1257
|
+
text:
|
|
1258
|
+
`refinement APPROVED by reviewer, but the ${what} after the reviewed version was ` +
|
|
1259
|
+
"shown — nothing was saved and the session stays read-only. An approval never " +
|
|
1260
|
+
"transfers to a replacement: call plan_review again over the current draft.",
|
|
1261
|
+
},
|
|
1262
|
+
],
|
|
1263
|
+
details: {
|
|
1264
|
+
ok: false,
|
|
1265
|
+
status: "stale",
|
|
1266
|
+
reason: "source_changed",
|
|
1267
|
+
changed: result.changed,
|
|
1268
|
+
approved: true,
|
|
1269
|
+
...(result.feedback !== undefined ? { feedback: result.feedback } : {}),
|
|
1270
|
+
...(result.reviewId !== undefined ? { reviewId: result.reviewId } : {}),
|
|
1271
|
+
subject: "refinement",
|
|
1272
|
+
},
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
case "denied":
|
|
1276
|
+
return subjectReviewOutcomeResult(REFINEMENT_SUBJECT, completedOutcome(false, result));
|
|
1277
|
+
case "dismissed":
|
|
1278
|
+
return subjectReviewOutcomeResult(REFINEMENT_SUBJECT, { status: "dismissed" });
|
|
1279
|
+
case "aborted":
|
|
1280
|
+
return subjectReviewOutcomeResult(REFINEMENT_SUBJECT, { status: "aborted" });
|
|
1281
|
+
case "unavailable":
|
|
1282
|
+
return subjectReviewOutcomeResult(REFINEMENT_SUBJECT, {
|
|
1283
|
+
status: "unavailable",
|
|
1284
|
+
warning: result.warning,
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* The refinement completion shared by the tool arm and any door: a completed outcome → the
|
|
1291
|
+
* feature completion (an approval re-resumes the pair through `refinementApprovalSave`, the
|
|
1292
|
+
* `reviewed` pair its content guard — a replacement written during the wait is the
|
|
1293
|
+
* `approvedSourceChanged` arm; Direct Edits is the no-save revise round) → the latch record →
|
|
1294
|
+
* the rendered tool result. A failed worker surfaces as the feature's own `approvedSaveFailed`
|
|
1295
|
+
* (its typed diagnostics ride the rendered save message).
|
|
1296
|
+
*/
|
|
1297
|
+
export async function completeRefinementReviewV1(
|
|
1298
|
+
pi: ExtensionAPI,
|
|
1299
|
+
ctx: ExtensionContext,
|
|
1300
|
+
gating: ToolGating,
|
|
1301
|
+
slot: DraftReviewSlot,
|
|
1302
|
+
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
1303
|
+
reviewed: ReviewedRefinementPair,
|
|
1304
|
+
): Promise<ToolResult> {
|
|
1305
|
+
const result = await completeRefinementReview(refinementOutcomeOf(outcome), () =>
|
|
1306
|
+
refinementApprovalSave({
|
|
1307
|
+
session: openSession(pi, ctx),
|
|
1308
|
+
backend: coldDoorRefinementBackend(pi, ctx),
|
|
1309
|
+
gate: gateFor(gating, ctx),
|
|
1310
|
+
reviewed,
|
|
1311
|
+
}),
|
|
1312
|
+
);
|
|
1313
|
+
// Denials, Direct Edits and the no-draft arm never reach the backend — nothing to confirm.
|
|
1314
|
+
if (result.status === "approvedSaved" || result.status === "approvedSaveFailed")
|
|
1315
|
+
recordRefinementSaveOutcome(slot, result.save);
|
|
1316
|
+
return renderRefinementReviewResult(ctx, result);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** The refusal text the plan-graph surfaces render inside a refinement session. */
|
|
1320
|
+
export { refinementStageRefusal };
|