@mgiles/perk 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +369 -359
- 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 -310
- package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
- package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
- package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
- package/extension/pi/v1/codeReview/stack.ts +606 -0
- 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 +163 -265
- 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 +128 -60
- 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/{doors → pi/v1}/lifecycleGates.ts +14 -59
- 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/pi/v1/objectivePlanning.ts +776 -0
- 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 +152 -34
- 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 -19
- package/extension/substrate/git.ts +88 -6
- package/extension/substrate/modelVisible.ts +53 -0
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/prompts.ts +22 -0
- package/extension/substrate/registry.ts +2 -0
- package/extension/substrate/resolverLease.ts +364 -0
- package/extension/substrate/sessionData.ts +85 -152
- package/extension/substrate/toolGating.ts +279 -84
- package/extension/substrate/unifiedDiff.ts +1 -1
- package/extension/substrate/workflowState.ts +191 -166
- package/extension/substrate/worktreeResolverLock.ts +261 -0
- package/extension/surfaces/surfaces.ts +79 -27
- package/extension/waves/adversarialReviewWave.ts +103 -48
- 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 +98 -10
- 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/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/objective-refinement.md +17 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/contexts/read-only.md +1 -1
- package/prompts/stages/conflict-resolution-continuation.md +9 -0
- package/prompts/stages/conflict-resolution.md +4 -4
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- 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 +7 -0
- 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/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +22 -0
- package/shared/README.md +8 -3
- package/shared/bindings.yaml +6 -3
- package/shared/contracts.md +4499 -2147
- package/shared/fixtures/issues-table.json +130 -0
- package/shared/registry.yaml +29 -1
- package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- 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 -475
- package/extension/doors/learn.ts +0 -655
- package/extension/doors/objectiveStack.ts +0 -1143
- package/extension/doors/prReviewDynamic.ts +0 -276
- package/extension/doors/ready.ts +0 -87
- package/extension/doors/submit.ts +0 -347
- package/extension/doors/submitPrReview.ts +0 -408
- 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/objectivePlan.ts +0 -975
- package/extension/factories/objectiveSave.ts +0 -363
- 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
- package/shared/contracts-history.md +0 -605
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// The draft-review guards (contracts.md §8.23 "Draft-review guards"): one in-memory slot per
|
|
2
|
+
// activation shared by EVERY review surface (the blocking `plan_review` tool's Plannotator and
|
|
3
|
+
// first-party arms, both browser doors), plus the decision ladder a completed review runs
|
|
4
|
+
// through before its verdict has any effect.
|
|
5
|
+
//
|
|
6
|
+
// Four guards, nothing persisted:
|
|
7
|
+
// 1. reviewed-bytes — at decision time the live draft must equal the bytes the human saw
|
|
8
|
+
// (artifact-sourced reviews only); an APPROVE over a moved draft saves nothing, a DENY
|
|
9
|
+
// proceeds with a one-line note.
|
|
10
|
+
// 2. destination fence — at APPROVE the save destination (`session/saveDestination.ts`) must
|
|
11
|
+
// equal what it was at open; a change or an unverifiable capture saves nothing and asks for
|
|
12
|
+
// a fresh human approval.
|
|
13
|
+
// 3. current-review slot — opening a review on ANY surface supersedes the previous one; a
|
|
14
|
+
// decision from a superseded review is ignored loudly (a TUI warning, never an injection).
|
|
15
|
+
// 4. unconfirmed-save latch — after a save attempt that returned no typed receipt, automatic
|
|
16
|
+
// (approval-driven) saves pause for the rest of the activation; the manual save command is
|
|
17
|
+
// the deliberate human retry. Linear's create→marker crash window is why: a blind retry can
|
|
18
|
+
// duplicate an issue the retry cannot find.
|
|
19
|
+
//
|
|
20
|
+
// A browser decision does not survive a Pi restart — the human re-runs the door. No lock, no
|
|
21
|
+
// state file, no reconciliation procedure.
|
|
22
|
+
|
|
23
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { GIST_DRAFT_ARTIFACT } from "../../authoring/gist/draft.ts";
|
|
25
|
+
import { OBJECTIVE_DRAFT_ARTIFACT } from "../../authoring/objective/draft.ts";
|
|
26
|
+
import { PLAN_DRAFT_ARTIFACT } from "../../authoring/plan/draft.ts";
|
|
27
|
+
import { REFINEMENT_DRAFT_ARTIFACT } from "../../authoring/refinement/draft.ts";
|
|
28
|
+
import { openBranchWorkflowSession } from "../../session/branchWorkflowSession.ts";
|
|
29
|
+
import {
|
|
30
|
+
captureSaveDestination,
|
|
31
|
+
changedDestinationComponents,
|
|
32
|
+
type DestinationComponent,
|
|
33
|
+
type SaveDestination,
|
|
34
|
+
} from "../../session/saveDestination.ts";
|
|
35
|
+
import {
|
|
36
|
+
digestSessionData,
|
|
37
|
+
REFINEMENT_CONTEXT_ARTIFACT,
|
|
38
|
+
type WorkflowSession,
|
|
39
|
+
} from "../../session/workflowSession.ts";
|
|
40
|
+
import { type ToolResult, untrustedReviewFeedback } from "./review.ts";
|
|
41
|
+
|
|
42
|
+
// ------------------------------------------------------------------------------ the vocabulary
|
|
43
|
+
|
|
44
|
+
/** The reviewable draft kinds — the stage-derived subject `WorkflowSession.draftReviewContext` reports. */
|
|
45
|
+
export type DraftReviewSubject = "plan" | "objective" | "gist" | "refinement";
|
|
46
|
+
|
|
47
|
+
/** Subject → the session artifact whose raw bytes the reviewed-bytes guard compares. */
|
|
48
|
+
export const REVIEW_SUBJECT_ARTIFACTS: Readonly<Record<DraftReviewSubject, string>> = {
|
|
49
|
+
plan: PLAN_DRAFT_ARTIFACT,
|
|
50
|
+
objective: OBJECTIVE_DRAFT_ARTIFACT,
|
|
51
|
+
gist: GIST_DRAFT_ARTIFACT,
|
|
52
|
+
refinement: REFINEMENT_DRAFT_ARTIFACT,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Subject → the manual save command (the deliberate retry once the latch is set). */
|
|
56
|
+
export const MANUAL_SAVE_COMMANDS: Readonly<Record<DraftReviewSubject, string>> = {
|
|
57
|
+
plan: "/plan-save",
|
|
58
|
+
objective: "/objective-save",
|
|
59
|
+
gist: "/gist-save",
|
|
60
|
+
refinement: "/objective-refinement-save",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Where the reviewed bytes came from: `artifact` — the validated draft artifact (the doors and
|
|
65
|
+
* the Plannotator tool arm; the only source the byte compare applies to); `parameter` — the
|
|
66
|
+
* blocking tool's `plan` param (no artifact to compare against); `editor` — a first-party
|
|
67
|
+
* in-TUI review, where the human's own edit write-back legitimately changes the artifact before
|
|
68
|
+
* the verdict.
|
|
69
|
+
*/
|
|
70
|
+
export type ReviewSource = "artifact" | "parameter" | "editor";
|
|
71
|
+
|
|
72
|
+
/** The token every review arm holds from `open` to decision. */
|
|
73
|
+
export interface OpenDraftReview {
|
|
74
|
+
readonly subject: DraftReviewSubject;
|
|
75
|
+
readonly runId: string;
|
|
76
|
+
readonly source: ReviewSource;
|
|
77
|
+
/** The exact reviewed bytes (artifact bytes / parameter text / editor text) — never the rendering. */
|
|
78
|
+
readonly raw: string;
|
|
79
|
+
/** What the reviewer was shown (equal to `raw` for markdown drafts). */
|
|
80
|
+
readonly markdown: string;
|
|
81
|
+
/** `digestSessionData(raw)`. */
|
|
82
|
+
readonly reviewedDigest: string;
|
|
83
|
+
/** Refinement only: the strict grounding-context artifact digest at open; null otherwise. */
|
|
84
|
+
readonly contextDigest: string | null;
|
|
85
|
+
readonly destination: SaveDestination;
|
|
86
|
+
/** Whether this review is still the activation's current one (no later `open`/`supersede`). */
|
|
87
|
+
isCurrent(): boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type OpenDraftReviewRefusal =
|
|
91
|
+
| "no-identity"
|
|
92
|
+
| "invalid-state"
|
|
93
|
+
| "subject-mismatch"
|
|
94
|
+
| "destination-unavailable";
|
|
95
|
+
|
|
96
|
+
export type OpenDraftReviewResult =
|
|
97
|
+
| { ok: true; review: OpenDraftReview }
|
|
98
|
+
| { ok: false; reason: OpenDraftReviewRefusal; detail: string };
|
|
99
|
+
|
|
100
|
+
export interface DraftReviewSnapshot {
|
|
101
|
+
subject: DraftReviewSubject;
|
|
102
|
+
source: ReviewSource;
|
|
103
|
+
raw: string;
|
|
104
|
+
markdown: string;
|
|
105
|
+
contextDigest?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The activation-scoped slot: the current review token plus the unconfirmed-save latch. */
|
|
109
|
+
export interface DraftReviewSlot {
|
|
110
|
+
/** The Pi API the slot's session reads ride (the ladder re-reads through the same seams). */
|
|
111
|
+
readonly pi: ExtensionAPI;
|
|
112
|
+
readonly ports: DraftReviewPorts;
|
|
113
|
+
/**
|
|
114
|
+
* Open a review: verify the session identity and stage-derived subject, capture the save
|
|
115
|
+
* destination, and make this the activation's current review (superseding any other).
|
|
116
|
+
*/
|
|
117
|
+
open(ctx: ExtensionContext, snapshot: DraftReviewSnapshot): OpenDraftReviewResult;
|
|
118
|
+
/** Clear the current review (e.g. `/implement-here` exiting plan mode without saving). */
|
|
119
|
+
supersede(): void;
|
|
120
|
+
/** Latch: a save attempt returned no typed receipt. First writer wins; nothing clears it. */
|
|
121
|
+
markUnconfirmed(subject: DraftReviewSubject, detail: string): void;
|
|
122
|
+
unconfirmed(): { subject: DraftReviewSubject; detail: string } | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const DESTINATION_UNAVAILABLE_DETAIL =
|
|
126
|
+
"could not read the git remotes to determine the save destination";
|
|
127
|
+
|
|
128
|
+
/** Injectable seams (tests fence destinations without a real repo). */
|
|
129
|
+
export interface DraftReviewPorts {
|
|
130
|
+
session(pi: ExtensionAPI, ctx: ExtensionContext): WorkflowSession;
|
|
131
|
+
destination(
|
|
132
|
+
cwd: string,
|
|
133
|
+
nodeClaim: { objective: string; node: string } | null,
|
|
134
|
+
): SaveDestination | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const PRODUCTION_PORTS: DraftReviewPorts = {
|
|
138
|
+
session: openBranchWorkflowSession,
|
|
139
|
+
destination: captureSaveDestination,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export function createDraftReviewSlot(
|
|
143
|
+
pi: ExtensionAPI,
|
|
144
|
+
ports: DraftReviewPorts = PRODUCTION_PORTS,
|
|
145
|
+
): DraftReviewSlot {
|
|
146
|
+
let current: symbol | null = null;
|
|
147
|
+
let latch: { subject: DraftReviewSubject; detail: string } | null = null;
|
|
148
|
+
return {
|
|
149
|
+
pi,
|
|
150
|
+
ports,
|
|
151
|
+
open(ctx, snapshot) {
|
|
152
|
+
const context = ports.session(pi, ctx).draftReviewContext();
|
|
153
|
+
if (!context.ok) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
reason: context.reason,
|
|
157
|
+
detail:
|
|
158
|
+
context.reason === "no-identity"
|
|
159
|
+
? "this session has no run identity (run_id) to review under"
|
|
160
|
+
: "the session's workflow state could not be read",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (context.subject !== snapshot.subject) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: "subject-mismatch",
|
|
167
|
+
detail: `this is a ${context.subject} session, not a ${snapshot.subject} session`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const destination = ports.destination(ctx.cwd, context.warmNodeClaim);
|
|
171
|
+
if (destination === null) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
reason: "destination-unavailable",
|
|
175
|
+
detail: DESTINATION_UNAVAILABLE_DETAIL,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const token = Symbol("draft-review");
|
|
179
|
+
current = token;
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
review: {
|
|
183
|
+
subject: snapshot.subject,
|
|
184
|
+
runId: context.runId,
|
|
185
|
+
source: snapshot.source,
|
|
186
|
+
raw: snapshot.raw,
|
|
187
|
+
markdown: snapshot.markdown,
|
|
188
|
+
reviewedDigest: digestSessionData(snapshot.raw),
|
|
189
|
+
contextDigest: snapshot.contextDigest ?? null,
|
|
190
|
+
destination,
|
|
191
|
+
isCurrent: () => current === token,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
supersede() {
|
|
196
|
+
current = null;
|
|
197
|
+
},
|
|
198
|
+
markUnconfirmed(subject, detail) {
|
|
199
|
+
if (latch === null) latch = { subject, detail };
|
|
200
|
+
},
|
|
201
|
+
unconfirmed() {
|
|
202
|
+
return latch;
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------- the decision ladder
|
|
208
|
+
|
|
209
|
+
export type DecisionCheck =
|
|
210
|
+
| { kind: "superseded" }
|
|
211
|
+
| { kind: "save-unconfirmed"; subject: DraftReviewSubject; detail: string }
|
|
212
|
+
| { kind: "stale-approval"; reviewedDigest: string }
|
|
213
|
+
| { kind: "destination-changed"; changed: DestinationComponent[] | "unverifiable" }
|
|
214
|
+
| { kind: "proceed"; draftChanged: boolean };
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Run a completed review's decision through the guards, in this order: superseded (the slot
|
|
218
|
+
* moved on, or the live run id / subject differ); the latch (`save` only); the reviewed-bytes
|
|
219
|
+
* compare (`artifact` source only — refinement also compares its grounding-context digest;
|
|
220
|
+
* `save` + changed → `stale-approval`, `revision` + changed → proceed with the note flag); the
|
|
221
|
+
* destination fence (`save` only, EVERY source — first-party included). The slot the review
|
|
222
|
+
* came from supplies the latch and the ports.
|
|
223
|
+
*/
|
|
224
|
+
export function checkDraftReviewDecision(
|
|
225
|
+
slot: DraftReviewSlot,
|
|
226
|
+
ctx: ExtensionContext,
|
|
227
|
+
review: OpenDraftReview,
|
|
228
|
+
effect: "save" | "revision",
|
|
229
|
+
): DecisionCheck {
|
|
230
|
+
if (!review.isCurrent()) return { kind: "superseded" };
|
|
231
|
+
const session = slot.ports.session(slot.pi, ctx);
|
|
232
|
+
const live = session.draftReviewContext();
|
|
233
|
+
if (!live.ok || live.runId !== review.runId || live.subject !== review.subject)
|
|
234
|
+
return { kind: "superseded" };
|
|
235
|
+
|
|
236
|
+
if (effect === "save") {
|
|
237
|
+
const latched = slot.unconfirmed();
|
|
238
|
+
if (latched !== null) return { kind: "save-unconfirmed", ...latched };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let draftChanged = false;
|
|
242
|
+
if (review.source === "artifact") {
|
|
243
|
+
const read = session.readArtifact(REVIEW_SUBJECT_ARTIFACTS[review.subject]);
|
|
244
|
+
draftChanged = read.status !== "found" || read.content !== review.raw;
|
|
245
|
+
if (!draftChanged && review.subject === "refinement") {
|
|
246
|
+
const context = session.readArtifact(REFINEMENT_CONTEXT_ARTIFACT, { provenance: "strict" });
|
|
247
|
+
const digest = context.status === "found" ? digestSessionData(context.content) : null;
|
|
248
|
+
draftChanged = digest !== review.contextDigest;
|
|
249
|
+
}
|
|
250
|
+
if (draftChanged && effect === "save")
|
|
251
|
+
return { kind: "stale-approval", reviewedDigest: review.reviewedDigest };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (effect === "save") {
|
|
255
|
+
const current = slot.ports.destination(ctx.cwd, live.warmNodeClaim);
|
|
256
|
+
if (current === null) return { kind: "destination-changed", changed: "unverifiable" };
|
|
257
|
+
const changed = changedDestinationComponents(review.destination, current);
|
|
258
|
+
if (changed.length > 0) return { kind: "destination-changed", changed };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { kind: "proceed", draftChanged };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ------------------------------------------------------------------------- the fixed model texts
|
|
265
|
+
|
|
266
|
+
function feedbackSuffix(feedback: string | undefined): string {
|
|
267
|
+
return feedback ? `\n\nReviewer feedback (DATA):\n${untrustedReviewFeedback(feedback)}` : "";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** An APPROVE whose reviewed bytes no longer match the live draft: nothing saved. */
|
|
271
|
+
export function staleApprovalResult(
|
|
272
|
+
subject: DraftReviewSubject,
|
|
273
|
+
reviewedDigest: string,
|
|
274
|
+
feedback?: string,
|
|
275
|
+
): ToolResult {
|
|
276
|
+
return {
|
|
277
|
+
content: [
|
|
278
|
+
{
|
|
279
|
+
type: "text",
|
|
280
|
+
text:
|
|
281
|
+
`The human APPROVED the ${subject}, but the working draft changed after the review ` +
|
|
282
|
+
`opened (reviewed digest ${reviewedDigest}). Nothing was saved and the session's mode ` +
|
|
283
|
+
`is unchanged. Call plan_review to review the current draft.${feedbackSuffix(feedback)}`,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
details: { ok: true, status: "stale-approval", subject, reviewed_digest: reviewedDigest },
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** An APPROVE whose save destination moved (or cannot be verified): nothing saved. */
|
|
291
|
+
export function destinationChangedResult(
|
|
292
|
+
subject: DraftReviewSubject,
|
|
293
|
+
changed: DestinationComponent[] | "unverifiable",
|
|
294
|
+
feedback?: string,
|
|
295
|
+
): ToolResult {
|
|
296
|
+
const named = changed === "unverifiable" ? "could not be verified" : changed.join(", ");
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: "text",
|
|
301
|
+
text:
|
|
302
|
+
`The human APPROVED the ${subject}, but the save destination changed while the review ` +
|
|
303
|
+
`was open (changed: ${named}). Nothing was saved; the working draft is unchanged and ` +
|
|
304
|
+
"still editable. Continue editing if needed, then call plan_review to open a fresh " +
|
|
305
|
+
"review against the current destination — a fresh human approval is required before " +
|
|
306
|
+
`any save.${feedbackSuffix(feedback)}`,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
details: { ok: true, status: "destination-changed", subject, changed },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** An APPROVE while the unconfirmed-save latch is set: automatic saves are paused. */
|
|
314
|
+
export function saveUnconfirmedResult(
|
|
315
|
+
subject: DraftReviewSubject,
|
|
316
|
+
runId: string,
|
|
317
|
+
detail: string,
|
|
318
|
+
feedback?: string,
|
|
319
|
+
): ToolResult {
|
|
320
|
+
return {
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: "text",
|
|
324
|
+
text:
|
|
325
|
+
`The human APPROVED the ${subject}, but an earlier save attempt in this session did ` +
|
|
326
|
+
`not confirm (${detail}), so automatic saves are paused for this session. Nothing new ` +
|
|
327
|
+
`was saved. Ask the human to check the issue backend for an existing ${subject} ` +
|
|
328
|
+
`carrying run id ${runId} before retrying — on Linear a partially completed create can ` +
|
|
329
|
+
`leave an issue the retry cannot find — then run ${MANUAL_SAVE_COMMANDS[subject]} (the ` +
|
|
330
|
+
`deliberate retry) or continue in the existing saved object.${feedbackSuffix(feedback)}`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
details: { ok: false, error_type: "save_unconfirmed", status: "refused", subject },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* A blocking review whose verdict arrived after a newer review superseded it (another surface
|
|
339
|
+
* opened, or `/implement-here` retired it): ignored; nothing saved.
|
|
340
|
+
*/
|
|
341
|
+
export function supersededReviewResult(subject: DraftReviewSubject): ToolResult {
|
|
342
|
+
return {
|
|
343
|
+
content: [
|
|
344
|
+
{
|
|
345
|
+
type: "text",
|
|
346
|
+
text:
|
|
347
|
+
`This ${subject} review was superseded by a newer review before its decision arrived — ` +
|
|
348
|
+
"the decision was ignored and nothing was saved. Call plan_review again if a review is " +
|
|
349
|
+
"still wanted.",
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
details: { ok: false, error_type: "review_superseded", status: "superseded", subject },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Prepended to a revision result's first text block when the draft moved during the review. */
|
|
357
|
+
export const DRAFT_CHANGED_NOTE =
|
|
358
|
+
"Note: the working draft changed after this review opened — weigh the feedback against the current draft.";
|
|
359
|
+
|
|
360
|
+
/** TUI `report()` only — never injected into the model's context. */
|
|
361
|
+
export const SUPERSEDED_DECISION_WARNING =
|
|
362
|
+
"a browser decision arrived for a superseded review — ignored; nothing was saved";
|
|
363
|
+
|
|
364
|
+
/** A slot `open` refusal rendered as the blocking tool's non-terminating result. */
|
|
365
|
+
export function openRefusedResult(
|
|
366
|
+
refusal: Extract<OpenDraftReviewResult, { ok: false }>,
|
|
367
|
+
): ToolResult {
|
|
368
|
+
return {
|
|
369
|
+
content: [
|
|
370
|
+
{
|
|
371
|
+
type: "text",
|
|
372
|
+
text: `cannot open the review: ${refusal.detail} — fix the cause and call plan_review again`,
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
details: {
|
|
376
|
+
ok: false,
|
|
377
|
+
error_type: "review_open_refused",
|
|
378
|
+
status: "refused",
|
|
379
|
+
reason: refusal.reason,
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Prefix `DRAFT_CHANGED_NOTE` onto a result's first text block (a new object; the input is untouched). */
|
|
385
|
+
export function withDraftChangedNote(result: ToolResult, draftChanged: boolean): ToolResult {
|
|
386
|
+
if (!draftChanged) return result;
|
|
387
|
+
const [first, ...rest] = result.content;
|
|
388
|
+
if (first === undefined || first.type !== "text")
|
|
389
|
+
return { ...result, content: [{ type: "text", text: DRAFT_CHANGED_NOTE }, ...result.content] };
|
|
390
|
+
return {
|
|
391
|
+
...result,
|
|
392
|
+
content: [{ ...first, text: `${DRAFT_CHANGED_NOTE}\n\n${first.text}` }, ...rest],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ------------------------------------------------------------------------------- the effects
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Deliver a browser decision's rendered result to the model: the text blocks joined with `\n`,
|
|
400
|
+
* as a fresh user message when the agent is idle, else queued as a follow-up.
|
|
401
|
+
*/
|
|
402
|
+
export function injectDraftReviewResult(
|
|
403
|
+
pi: ExtensionAPI,
|
|
404
|
+
ctx: ExtensionContext,
|
|
405
|
+
result: ToolResult,
|
|
406
|
+
): void {
|
|
407
|
+
const text = result.content.map((block) => block.text).join("\n");
|
|
408
|
+
if (ctx.isIdle()) pi.sendUserMessage(text);
|
|
409
|
+
else pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The one place a completed save reports into the latch. `confirmed` is the subject's typed
|
|
414
|
+
* saved/approvedSaved arm; anything else (`save-failed`, a thrown backend call, an
|
|
415
|
+
* `unavailable` port) latches with the outcome's message. Called by every subject completion
|
|
416
|
+
* AND by the manual save tools/commands — a manual save that fails latches too (though manual
|
|
417
|
+
* saves never consult the latch: they ARE the deliberate retry).
|
|
418
|
+
*/
|
|
419
|
+
export function recordSaveOutcome(
|
|
420
|
+
slot: DraftReviewSlot,
|
|
421
|
+
subject: DraftReviewSubject,
|
|
422
|
+
outcome: { confirmed: boolean; detail?: string },
|
|
423
|
+
): void {
|
|
424
|
+
if (outcome.confirmed) return;
|
|
425
|
+
slot.markUnconfirmed(subject, outcome.detail?.trim() || "backend call did not return a receipt");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Read the first text block of a tool result (the save message a failed save carries). */
|
|
429
|
+
export function firstText(result: ToolResult): string | undefined {
|
|
430
|
+
return result.content[0]?.text;
|
|
431
|
+
}
|