@mgiles/perk 1.0.1 → 2.0.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 +67 -57
- package/extension/adapters/planAdapterPlannotator.ts +27 -41
- package/extension/adapters/planAdapterTombell.ts +15 -28
- package/extension/adapters/todoAdapterJuicesharp.ts +10 -13
- package/extension/checkpoints/checkpoints.ts +21 -13
- package/extension/doors/address.ts +6 -5
- package/extension/doors/askUser.ts +12 -8
- package/extension/doors/ciExecutor.ts +23 -15
- package/extension/doors/hunkHandoff.ts +202 -0
- package/extension/doors/land.ts +33 -10
- package/extension/doors/learn.ts +240 -34
- package/extension/doors/learnFactory.ts +144 -0
- package/extension/doors/lifecycleGates.ts +2 -1
- package/extension/doors/plannotatorHandoff.ts +509 -0
- package/extension/doors/prReview.ts +22 -37
- package/extension/doors/prReviewBrowser.ts +341 -0
- package/extension/doors/prReviewTerminal.ts +267 -0
- package/extension/doors/ready.ts +2 -1
- package/extension/doors/selfcheck.ts +240 -6
- package/extension/doors/submit.ts +37 -19
- package/extension/doors/submitPrReview.ts +408 -0
- package/extension/factories/implementHere.ts +116 -0
- package/extension/factories/objective.ts +17 -6
- package/extension/factories/objectiveAuthor.ts +15 -32
- package/extension/factories/objectiveDraft.ts +1 -1
- package/extension/factories/objectivePlan.ts +16 -34
- package/extension/factories/objectiveSave.ts +7 -17
- package/extension/factories/planMode.ts +24 -38
- package/extension/factories/planReview.ts +291 -177
- package/extension/factories/planSave.ts +23 -8
- package/extension/index.ts +117 -20
- package/extension/substrate/bindingDelivery.ts +32 -10
- package/extension/substrate/bindings.ts +4 -2
- package/extension/substrate/cache.ts +36 -9
- package/extension/substrate/clipboard.ts +81 -0
- package/extension/substrate/command.ts +25 -0
- package/extension/substrate/config.ts +98 -76
- package/extension/substrate/consoleCapture.ts +90 -0
- package/extension/substrate/git.ts +77 -0
- package/extension/substrate/miniJinja.ts +480 -0
- package/extension/substrate/paths.ts +38 -0
- package/extension/substrate/prompts.ts +15 -24
- package/extension/substrate/providers.ts +62 -8
- package/extension/substrate/sessionData.ts +1 -1
- package/extension/substrate/sessionPointers.ts +184 -0
- package/extension/substrate/structuredOutput.ts +3 -1
- package/extension/substrate/terminalLaunch.ts +178 -0
- package/extension/substrate/toolGating.ts +332 -73
- package/extension/substrate/toolParams.ts +7 -0
- package/extension/substrate/workflowState.ts +54 -2
- package/extension/surfaces/footerProvider.ts +8 -4
- package/extension/surfaces/surfaces.ts +338 -13
- package/extension/vendor/btw/btw.ts +10 -0
- package/extension/worker/readOnlySession.ts +19 -6
- package/extension/worker/worker.ts +187 -31
- package/extension/workerMain.ts +14 -13
- package/package.json +3 -7
- package/prompts/README.md +56 -5
- package/prompts/_fixtures/cases.yaml +52 -131
- package/prompts/_fixtures/golden/cond_elif-a.txt +3 -0
- package/prompts/_fixtures/golden/cond_elif-b.txt +3 -0
- package/prompts/_fixtures/golden/cond_elif-c.txt +3 -0
- package/prompts/_fixtures/golden/cond_if-false.txt +3 -0
- package/prompts/_fixtures/golden/cond_if-true.txt +3 -0
- package/prompts/_fixtures/golden/cond_ops-1.txt +3 -0
- package/prompts/_fixtures/golden/cond_ops-2.txt +3 -0
- package/prompts/_fixtures/golden/no_trailing_nl.txt +2 -0
- package/prompts/_fixtures/golden/trailing_nl.txt +2 -0
- package/prompts/_fixtures/golden/trim_block.txt +3 -0
- package/prompts/_fixtures/golden/trim_inline.txt +1 -0
- package/prompts/_fixtures/live.yaml +396 -0
- package/prompts/_fixtures/templates/cond_elif.md +9 -0
- package/prompts/_fixtures/templates/cond_if.md +7 -0
- package/prompts/_fixtures/templates/cond_ops.md +3 -0
- package/prompts/_fixtures/templates/no_trailing_nl.md +2 -0
- package/prompts/_fixtures/templates/trailing_nl.md +2 -0
- package/prompts/_fixtures/templates/trim_block.md +5 -0
- package/prompts/_fixtures/templates/trim_inline.md +1 -0
- package/prompts/contexts/adapters/juicesharp-todo.md +7 -0
- package/prompts/contexts/adapters/plannotator-objective.md +7 -0
- package/prompts/contexts/adapters/plannotator-plan.md +6 -0
- package/prompts/contexts/adapters/tombell-plan.md +17 -0
- package/prompts/contexts/objective-authoring.md +20 -0
- package/prompts/contexts/plan-authoring.md +24 -0
- package/prompts/contexts/read-only.md +10 -0
- package/prompts/stages/conflict-resolution.md +4 -0
- package/prompts/stages/learn-code.md +8 -0
- package/prompts/stages/learn-docs.md +7 -6
- package/prompts/stages/learn-orchestrate.md +6 -0
- package/prompts/stages/learn.md +1 -1
- package/prompts/stages/objective-author/adopt.md +12 -0
- package/prompts/stages/objective-author/file.md +9 -0
- package/prompts/stages/objective-author/seed.md +9 -0
- package/prompts/stages/objective-plan/guidance.md +1 -1
- package/prompts/stages/objective-plan/seed.md +3 -2
- package/prompts/stages/objective-reconcile.md +7 -0
- package/prompts/stages/objective-replan.md +14 -0
- package/prompts/stages/objective-save.md +9 -0
- package/prompts/stages/plan-from/adopt.md +10 -0
- package/prompts/stages/plan-from/file.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +11 -0
- package/prompts/stages/pr-review-browser/foreign.md +11 -0
- package/prompts/stages/pr-review-terminal/active.md +12 -0
- package/prompts/stages/pr-review-terminal/foreign.md +13 -0
- package/prompts/stages/pr-review-terminal/local.md +4 -0
- package/prompts/stages/pr-review.md +6 -0
- package/prompts/stages/replan.md +13 -0
- package/prompts/stages/skills/create-from.md +15 -0
- package/prompts/stages/skills/create.md +9 -0
- package/prompts/stages/skills/refine.md +9 -0
- package/shared/README.md +23 -13
- package/shared/bindings.yaml +22 -2
- package/shared/contracts-history.md +191 -0
- package/shared/contracts.md +2258 -1638
- package/shared/providers.yaml +8 -1
- package/shared/registry.yaml +10 -11
- package/shared/schemas/contracts/bindings.schema.json +38 -0
- package/shared/schemas/contracts/providers.schema.json +89 -0
- package/shared/schemas/contracts/registry.schema.json +98 -0
- package/shared/schemas/inputs/handoff-arg.schema.json +6 -0
- package/shared/schemas/inputs/resolve-threads-batch.schema.json +37 -0
- package/shared/schemas/inputs/review-post-batch.schema.json +84 -0
- package/shared/schemas/inputs/review-submit-batch.schema.json +66 -0
- package/shared/schemas/inputs/structured-roadmap-node.schema.json +102 -0
- package/shared/schemas/outputs/doctor-report.schema.json +236 -0
- package/shared/schemas/outputs/init-report.schema.json +419 -0
- package/shared/schemas/outputs/learn-capture.schema.json +90 -0
- package/shared/schemas/outputs/learn-skip.schema.json +59 -0
- package/shared/schemas/outputs/plan-save.schema.json +209 -0
- package/shared/schemas/outputs/pr-feedback.schema.json +334 -0
- package/shared/schemas/outputs/pr-land.schema.json +187 -0
- package/shared/schemas/outputs/pr-ready.schema.json +75 -0
- package/shared/schemas/outputs/pr-review-checkout.schema.json +69 -0
- package/shared/schemas/outputs/pr-review-cleanup.schema.json +54 -0
- package/shared/schemas/outputs/pr-review-context.schema.json +86 -0
- package/shared/schemas/outputs/pr-review-submit.schema.json +64 -0
- package/shared/schemas/outputs/pr-submit.schema.json +147 -0
- package/extension/doors/learnDocs.ts +0 -99
- package/prompts/_fixtures/golden/address-action-model.txt +0 -10
- package/prompts/_fixtures/golden/address-action.txt +0 -10
- package/prompts/_fixtures/golden/address-preview-model.txt +0 -6
- package/prompts/_fixtures/golden/address-preview.txt +0 -6
- package/prompts/_fixtures/golden/implement-github.txt +0 -8
- package/prompts/_fixtures/golden/learn-docs.txt +0 -8
- package/prompts/_fixtures/golden/learn-github.txt +0 -11
- package/prompts/_fixtures/golden/learn-linear.txt +0 -11
- package/prompts/_fixtures/golden/learn-no-ref.txt +0 -8
- package/prompts/_fixtures/golden/learn-other.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-guidance-linear.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-guidance.txt +0 -8
- package/prompts/_fixtures/golden/objective-plan-seed-linear.txt +0 -20
- package/prompts/_fixtures/golden/objective-plan-seed.txt +0 -15
- package/prompts/_fixtures/golden/objective-read-linear-nourl.txt +0 -1
- package/prompts/_fixtures/golden/objective-read-linear.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-github.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-linear.txt +0 -1
- package/prompts/_fixtures/golden/plan-read-other.txt +0 -1
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// (`runFirstPartyReview`): display the draft in pi's built-in `ctx.ui.editor` dialog (scrollable,
|
|
7
7
|
// Ctrl+G opens the user's external $EDITOR), write optional human edits back to the draft via
|
|
8
8
|
// `writePlanDraft` BEFORE the verdict (reviewed bytes == artifact bytes == saved bytes — a failed
|
|
9
|
-
// write-back ABORTS the review fail-open, nothing saved), then
|
|
10
|
-
// `ctx.ui.select` verdict
|
|
9
|
+
// write-back ABORTS the review fail-open, nothing saved), then an approve/deny/skip
|
|
10
|
+
// `ctx.ui.select` verdict — on the plan arm with a 4th "Implement here — no issue saved" option
|
|
11
|
+
// (§8.23; suppressed in objective-node planning sessions) — with deny feedback via a second
|
|
12
|
+
// editor dialog.
|
|
11
13
|
//
|
|
12
14
|
// REVIEW SEMANTICS (file-first, approval auto-saves): the review runs while the session is still
|
|
13
15
|
// read-only (the tool is in READ_ONLY_TOOLS — review happens before the gate ever comes off).
|
|
@@ -48,11 +50,14 @@ import {
|
|
|
48
50
|
createPlannotatorBridge,
|
|
49
51
|
isPlannotatorPlanSelected,
|
|
50
52
|
} from "../adapters/planAdapterPlannotator.ts";
|
|
53
|
+
import type { Result } from "../substrate/result.ts";
|
|
51
54
|
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
52
55
|
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
53
56
|
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
57
|
+
import { implementHereExit, implementHereGuidance } from "./implementHere.ts";
|
|
54
58
|
import { OBJECTIVE_AUTHOR_STAGE } from "./objectiveAuthor.ts";
|
|
55
59
|
import { readObjectiveDraft, renderObjectiveDraft } from "./objectiveDraft.ts";
|
|
60
|
+
import { readNodeClaim } from "./objectivePlan.ts";
|
|
56
61
|
import { type ObjectiveApprovalSaveOutcome, objectiveApprovalSave } from "./objectiveSave.ts";
|
|
57
62
|
import { writePlanDraft } from "./planDraft.ts";
|
|
58
63
|
import { type ApprovalSaveOutcome, approvalSave, resolvePlanSource } from "./planSave.ts";
|
|
@@ -62,12 +67,15 @@ import { type ApprovalSaveOutcome, approvalSave, resolvePlanSource } from "./pla
|
|
|
62
67
|
/**
|
|
63
68
|
* The review outcome a backend produces, mapped into a tool result below (also `details.status`).
|
|
64
69
|
* The `dismissed` arm is FIRST-PARTY ONLY (Esc anywhere = fail-open skip; the plannotator bridge
|
|
65
|
-
* never produces it).
|
|
70
|
+
* never produces it). The `implement-here` arm is first-party PLAN-arm only (the human chose the
|
|
71
|
+
* no-save exit — contracts.md §8.23); the plannotator bridge never produces it (its browser
|
|
72
|
+
* envelope returns only approve/deny) and the objective arm never offers it.
|
|
66
73
|
*/
|
|
67
74
|
export type ReviewOutcome =
|
|
68
75
|
| { status: "unavailable"; warning: string }
|
|
69
76
|
| { status: "aborted" }
|
|
70
77
|
| { status: "dismissed" }
|
|
78
|
+
| { status: "implement-here"; reviewId: string }
|
|
71
79
|
| { status: "completed"; approved: boolean; feedback?: string; reviewId: string };
|
|
72
80
|
|
|
73
81
|
interface ToolResult {
|
|
@@ -76,21 +84,70 @@ interface ToolResult {
|
|
|
76
84
|
terminate?: boolean;
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The subject descriptor parameterizing the shared renderer cores below — the plan and objective
|
|
89
|
+
* review arms render the same outcome shapes, differing only in these fields. Module-private on
|
|
90
|
+
* purpose: nothing outside this module needs it (both execute arms live here), and a third review
|
|
91
|
+
* subject would land here too, constructing its own descriptor and reusing the cores.
|
|
92
|
+
*/
|
|
93
|
+
interface ReviewSubject {
|
|
94
|
+
/** The display noun in every rendered text ("plan" / "objective"). */
|
|
95
|
+
noun: string;
|
|
96
|
+
/** The lowercase present-the-work phrase (dismissed / implement-here arms). */
|
|
97
|
+
present: string;
|
|
98
|
+
/** The unavailable-arm phrase (the plan flavor appends "in your next message"). */
|
|
99
|
+
presentUnavailable: string;
|
|
100
|
+
/** Where an implement-here verdict "cannot" have come from (the defensive arm's text). */
|
|
101
|
+
implementHereWhere: string;
|
|
102
|
+
/** The draft-rewrite tool the DENIED text redirects to. */
|
|
103
|
+
draftTool: string;
|
|
104
|
+
/** The manual-failsafe slash command. */
|
|
105
|
+
failsafeCmd: string;
|
|
106
|
+
/** Extra keys merged into every details object ({} on the plan arm). */
|
|
107
|
+
detailsExtra: Record<string, unknown>;
|
|
108
|
+
/** The defensively-unreachable no-source save arm's error string. */
|
|
109
|
+
noSourceError: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const PLAN_SUBJECT: ReviewSubject = {
|
|
113
|
+
noun: "plan",
|
|
114
|
+
present: "the complete plan to the user",
|
|
115
|
+
presentUnavailable: "the complete plan to the user in your next message",
|
|
116
|
+
implementHereWhere: "outside the execute path",
|
|
117
|
+
draftTool: "plan_draft",
|
|
118
|
+
failsafeCmd: "/plan-save",
|
|
119
|
+
detailsExtra: {},
|
|
120
|
+
noSourceError: "no plan source resolved",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const OBJECTIVE_SUBJECT: ReviewSubject = {
|
|
124
|
+
noun: "objective",
|
|
125
|
+
present: "the complete objective + structured roadmap to the user",
|
|
126
|
+
presentUnavailable: "the complete objective + structured roadmap to the user",
|
|
127
|
+
implementHereWhere: "on the objective path",
|
|
128
|
+
draftTool: "objective_draft",
|
|
129
|
+
failsafeCmd: "/objective-save",
|
|
130
|
+
detailsExtra: { subject: "objective" },
|
|
131
|
+
noSourceError: "no objective draft resolved",
|
|
132
|
+
};
|
|
133
|
+
|
|
79
134
|
const SKIP_TEXT =
|
|
80
135
|
"no interactive review surface available — present the complete plan to the user in your next message.";
|
|
81
136
|
|
|
82
137
|
function skipResult(): ToolResult {
|
|
83
|
-
return { content: [{ type: "text", text: SKIP_TEXT }], details: { status: "skipped" } };
|
|
138
|
+
return { content: [{ type: "text", text: SKIP_TEXT }], details: { ok: true, status: "skipped" } };
|
|
84
139
|
}
|
|
85
140
|
|
|
86
141
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* outcomes to
|
|
90
|
-
* `approved: false` (kept total for safety
|
|
91
|
-
*
|
|
142
|
+
* The shared outcome-mapper core: map a non-approved review outcome into the model-facing tool
|
|
143
|
+
* result for `subject`. The `completed` case renders the DENIED text — both execute paths route
|
|
144
|
+
* approved outcomes to their approved-save mapper FIRST, so callers only reach `completed` here
|
|
145
|
+
* with `approved: false` (kept total for safety; the `approved: outcome.approved` passthrough is
|
|
146
|
+
* deliberately behavior-preserving — never hardcode `false`). The `dismissed` arm renders as a
|
|
147
|
+
* skip — the human declined to decide, so the present-the-work + manual-failsafe discipline
|
|
148
|
+
* applies.
|
|
92
149
|
*/
|
|
93
|
-
|
|
150
|
+
function subjectReviewOutcomeResult(subject: ReviewSubject, outcome: ReviewOutcome): ToolResult {
|
|
94
151
|
switch (outcome.status) {
|
|
95
152
|
case "unavailable":
|
|
96
153
|
return {
|
|
@@ -99,15 +156,21 @@ export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
|
99
156
|
type: "text",
|
|
100
157
|
text:
|
|
101
158
|
`WARNING: ${outcome.warning} — no review performed. ` +
|
|
102
|
-
|
|
159
|
+
`Present ${subject.presentUnavailable} instead.`,
|
|
103
160
|
},
|
|
104
161
|
],
|
|
105
|
-
details: {
|
|
162
|
+
details: {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: outcome.warning,
|
|
165
|
+
error_type: "unavailable",
|
|
166
|
+
status: "unavailable",
|
|
167
|
+
...subject.detailsExtra,
|
|
168
|
+
},
|
|
106
169
|
};
|
|
107
170
|
case "aborted":
|
|
108
171
|
return {
|
|
109
|
-
content: [{ type: "text", text:
|
|
110
|
-
details: { status: "aborted" },
|
|
172
|
+
content: [{ type: "text", text: `${subject.noun} review aborted (turn interrupted).` }],
|
|
173
|
+
details: { ok: true, status: "aborted", ...subject.detailsExtra },
|
|
111
174
|
};
|
|
112
175
|
case "dismissed":
|
|
113
176
|
return {
|
|
@@ -115,24 +178,41 @@ export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
|
115
178
|
{
|
|
116
179
|
type: "text",
|
|
117
180
|
text:
|
|
118
|
-
|
|
119
|
-
|
|
181
|
+
`${subject.noun} review dismissed — present ${subject.present}; the human runs ` +
|
|
182
|
+
`${subject.failsafeCmd} (the manual failsafe).`,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
details: { ok: true, status: "skipped", reason: "dismissed", ...subject.detailsExtra },
|
|
186
|
+
};
|
|
187
|
+
case "implement-here":
|
|
188
|
+
// Defensively unreachable: the plan execute path routes implement-here to
|
|
189
|
+
// implementHereResult FIRST (mirror the approved-first routing), and the objective arm
|
|
190
|
+
// never offers the verdict. Map to a skip shape rather than throwing.
|
|
191
|
+
return {
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "text",
|
|
195
|
+
text:
|
|
196
|
+
`implement-here verdict received ${subject.implementHereWhere} — nothing saved; ` +
|
|
197
|
+
`present ${subject.present}.`,
|
|
120
198
|
},
|
|
121
199
|
],
|
|
122
|
-
details: { status: "skipped", reason: "
|
|
200
|
+
details: { ok: true, status: "skipped", reason: "implement-here", ...subject.detailsExtra },
|
|
123
201
|
};
|
|
124
202
|
case "completed": {
|
|
125
203
|
const feedback = outcome.feedback ? `\n\nReviewer feedback:\n${outcome.feedback}` : "";
|
|
126
204
|
const text =
|
|
127
|
-
|
|
128
|
-
|
|
205
|
+
`${subject.noun} DENIED — revise per this feedback, rewrite the working draft with ` +
|
|
206
|
+
`${subject.draftTool}, then call plan_review again.${feedback}`;
|
|
129
207
|
return {
|
|
130
208
|
content: [{ type: "text", text }],
|
|
131
209
|
details: {
|
|
210
|
+
ok: true,
|
|
132
211
|
status: "completed",
|
|
133
212
|
approved: outcome.approved,
|
|
134
213
|
feedback: outcome.feedback ?? null,
|
|
135
214
|
reviewId: outcome.reviewId,
|
|
215
|
+
...subject.detailsExtra,
|
|
136
216
|
},
|
|
137
217
|
};
|
|
138
218
|
}
|
|
@@ -140,52 +220,82 @@ export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
|
140
220
|
}
|
|
141
221
|
|
|
142
222
|
/**
|
|
143
|
-
* Map
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* and directs the human `/plan-save` failsafe. Reviewer feedback is surfaced loudly as
|
|
147
|
-
* implementation guidance — the approved bytes were saved verbatim, never post-edited. `edited`
|
|
148
|
-
* (first-party only) flags that human edits were written back to the draft pre-verdict, so the
|
|
149
|
-
* saved bytes carry them. The `no-plan` arm is defensively unreachable (the reviewed plan is
|
|
150
|
-
* always non-blank) but maps to the save-failed shape rather than throwing.
|
|
223
|
+
* Map a non-approved review outcome into the model-facing tool result (exported for the offline
|
|
224
|
+
* tests) — the plan flavor of `subjectReviewOutcomeResult`. The execute path routes approved
|
|
225
|
+
* outcomes to `approvedSaveResult` first, so `completed` renders DENIED here.
|
|
151
226
|
*/
|
|
152
|
-
export function
|
|
227
|
+
export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
228
|
+
return subjectReviewOutcomeResult(PLAN_SUBJECT, outcome);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The normalized approval-save outcome the shared core consumes — each delegator maps its
|
|
233
|
+
* subject-specific no-source discriminant (`no-plan` / `no-draft`) onto `no-source`. `result`
|
|
234
|
+
* widens to `Result<object>`: the core reads only `content[0]?.text` and the `details.ok`
|
|
235
|
+
* discriminant (+ `details.error` on the fail arm), passing `details` through opaquely.
|
|
236
|
+
*/
|
|
237
|
+
type SubjectSaveOutcome =
|
|
238
|
+
| { status: "no-source" }
|
|
239
|
+
| { status: "saved" | "save-failed"; result: Result<object>; gateExited: boolean };
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The shared approved-save mapper core: map an APPROVED review outcome + the approval-save
|
|
243
|
+
* outcome into the model-facing tool result for `subject`. A successful save TERMINATES the turn
|
|
244
|
+
* (propagating the seam's `terminate: true` intent); a failed save is non-terminating, leaves
|
|
245
|
+
* the gate read-only, and directs the human manual failsafe. Reviewer feedback is surfaced
|
|
246
|
+
* loudly as implementation guidance — the approved bytes were saved verbatim, never post-edited.
|
|
247
|
+
* The `paramMismatch`/`edited` opts are plan-arm-only (their literals name "plan"/"draft"): the
|
|
248
|
+
* objective delegator never passes opts, so the suffixes render empty and `edited` never reaches
|
|
249
|
+
* its details. The `no-source` arm is defensively unreachable (the reviewed source is always
|
|
250
|
+
* non-blank) but maps to the save-failed shape rather than throwing.
|
|
251
|
+
*/
|
|
252
|
+
function approvedSubjectSaveResult(
|
|
253
|
+
subject: ReviewSubject,
|
|
153
254
|
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
154
|
-
save:
|
|
155
|
-
opts
|
|
255
|
+
save: SubjectSaveOutcome,
|
|
256
|
+
opts?: { paramMismatch?: boolean; edited?: boolean },
|
|
156
257
|
): ToolResult {
|
|
157
258
|
const feedback = outcome.feedback
|
|
158
|
-
?
|
|
159
|
-
outcome.feedback
|
|
259
|
+
? `\n\nReviewer feedback (implementation guidance — the approved ${subject.noun} was saved ` +
|
|
260
|
+
`verbatim):\n${outcome.feedback}`
|
|
160
261
|
: "";
|
|
161
262
|
const base = {
|
|
162
263
|
status: "completed",
|
|
163
264
|
approved: true,
|
|
164
265
|
reviewId: outcome.reviewId,
|
|
165
266
|
feedback: outcome.feedback ?? null,
|
|
166
|
-
...
|
|
267
|
+
...subject.detailsExtra,
|
|
268
|
+
...(opts?.edited === true ? { edited: true } : {}),
|
|
167
269
|
};
|
|
168
270
|
if (save.status === "saved") {
|
|
169
271
|
const saveText = save.result.content[0]?.text ?? "";
|
|
170
|
-
const mismatch = opts.paramMismatch
|
|
171
|
-
? "\n\n⚠ differing plan param ignored — the validated draft was reviewed and saved."
|
|
172
|
-
: "";
|
|
173
272
|
const edited =
|
|
174
|
-
opts
|
|
273
|
+
opts?.edited === true ? " · human edits were written back to the draft and saved" : "";
|
|
274
|
+
const mismatch =
|
|
275
|
+
opts?.paramMismatch === true
|
|
276
|
+
? "\n\n⚠ differing plan param ignored — the validated draft was reviewed and saved."
|
|
277
|
+
: "";
|
|
175
278
|
return {
|
|
176
279
|
content: [
|
|
177
280
|
{
|
|
178
281
|
type: "text",
|
|
179
|
-
text:
|
|
282
|
+
text: `${subject.noun} APPROVED by reviewer.${feedback}\n\n${saveText}${edited}${mismatch}`,
|
|
180
283
|
},
|
|
181
284
|
],
|
|
182
|
-
|
|
285
|
+
// `ok` sits per-branch, NOT in `base` — `base` is spread into the fail branch too.
|
|
286
|
+
details: {
|
|
287
|
+
ok: true,
|
|
288
|
+
...base,
|
|
289
|
+
saved: true,
|
|
290
|
+
gateExited: save.gateExited,
|
|
291
|
+
save: save.result.details,
|
|
292
|
+
},
|
|
183
293
|
terminate: true,
|
|
184
294
|
};
|
|
185
295
|
}
|
|
186
296
|
const error =
|
|
187
|
-
save.status === "no-
|
|
188
|
-
?
|
|
297
|
+
save.status === "no-source"
|
|
298
|
+
? subject.noSourceError
|
|
189
299
|
: save.result.details.ok
|
|
190
300
|
? "unknown save failure"
|
|
191
301
|
: save.result.details.error;
|
|
@@ -194,14 +304,68 @@ export function approvedSaveResult(
|
|
|
194
304
|
{
|
|
195
305
|
type: "text",
|
|
196
306
|
text:
|
|
197
|
-
|
|
198
|
-
`read-only. Ask the user to run
|
|
307
|
+
`${subject.noun} APPROVED by reviewer, but the auto-save FAILED (${error}) — the ` +
|
|
308
|
+
`session stays read-only. Ask the user to run ${subject.failsafeCmd} (the manual ` +
|
|
309
|
+
`failsafe) to retry.${feedback}`,
|
|
199
310
|
},
|
|
200
311
|
],
|
|
201
312
|
details: {
|
|
313
|
+
ok: false,
|
|
314
|
+
error,
|
|
315
|
+
error_type: "save_failed",
|
|
202
316
|
...base,
|
|
203
317
|
saved: false,
|
|
204
|
-
save: save.status === "no-
|
|
318
|
+
save: save.status === "no-source" ? null : save.result.details,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Map an APPROVED review outcome + the `approvalSave` outcome into the model-facing tool result
|
|
325
|
+
* (exported for the offline tests) — the plan flavor of `approvedSubjectSaveResult`. `edited`
|
|
326
|
+
* (first-party only) flags that human edits were written back to the draft pre-verdict, so the
|
|
327
|
+
* saved bytes carry them.
|
|
328
|
+
*/
|
|
329
|
+
export function approvedSaveResult(
|
|
330
|
+
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
331
|
+
save: ApprovalSaveOutcome,
|
|
332
|
+
opts: { paramMismatch: boolean; edited?: boolean },
|
|
333
|
+
): ToolResult {
|
|
334
|
+
return approvedSubjectSaveResult(
|
|
335
|
+
PLAN_SUBJECT,
|
|
336
|
+
outcome,
|
|
337
|
+
save.status === "no-plan" ? { status: "no-source" } : save,
|
|
338
|
+
opts,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Map an IMPLEMENT-HERE review outcome + the `implementHereExit` outcome into the model-facing
|
|
344
|
+
* tool result (exported for the offline tests). NON-terminating on purpose — the model continues
|
|
345
|
+
* the turn and implements immediately. The text is the implement-here guidance; when the human
|
|
346
|
+
* edited the plan during review (`edited`), the final reviewed bytes are inlined so the model
|
|
347
|
+
* implements THOSE, not its stale in-context version (the draft write-back already happened
|
|
348
|
+
* pre-verdict). Nothing is saved: no issue, no plan-ref, the draft artifact intact (§8.23).
|
|
349
|
+
*/
|
|
350
|
+
export function implementHereResult(
|
|
351
|
+
outcome: Extract<ReviewOutcome, { status: "implement-here" }>,
|
|
352
|
+
exit: { gateExited: boolean },
|
|
353
|
+
opts: { cwd: string; plan: string; edited: boolean },
|
|
354
|
+
): ToolResult {
|
|
355
|
+
return {
|
|
356
|
+
content: [
|
|
357
|
+
{
|
|
358
|
+
type: "text",
|
|
359
|
+
text: implementHereGuidance(opts.cwd, { editedPlan: opts.edited ? opts.plan : undefined }),
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
details: {
|
|
363
|
+
ok: true,
|
|
364
|
+
status: "implement-here",
|
|
365
|
+
saved: false,
|
|
366
|
+
gateExited: exit.gateExited,
|
|
367
|
+
reviewId: outcome.reviewId,
|
|
368
|
+
...(opts.edited ? { edited: true } : {}),
|
|
205
369
|
},
|
|
206
370
|
};
|
|
207
371
|
}
|
|
@@ -218,10 +382,22 @@ export interface PlanReviewUI {
|
|
|
218
382
|
): Promise<string | undefined>;
|
|
219
383
|
}
|
|
220
384
|
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Derive a subject's verdict options (plain text — charter D3: no emoji outside the footer);
|
|
387
|
+
* only the skip label's manual-failsafe command varies by subject. `VERDICT_IMPLEMENT_HERE`
|
|
388
|
+
* stays a standalone constant on purpose — the no-save exit is plan-arm-only by contract
|
|
389
|
+
* (§8.23), never part of the descriptor.
|
|
390
|
+
*/
|
|
391
|
+
function verdictsFor(subject: ReviewSubject): { approve: string; deny: string; skip: string } {
|
|
392
|
+
return {
|
|
393
|
+
approve: "Approve — auto-save to GitHub",
|
|
394
|
+
deny: "Deny — send feedback for revision",
|
|
395
|
+
skip: `Skip — decide later (manual ${subject.failsafeCmd})`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** The optional 4th verdict (plan arm only): the no-save implement-here exit (§8.23). */
|
|
400
|
+
const VERDICT_IMPLEMENT_HERE = "Implement here — no issue saved";
|
|
225
401
|
|
|
226
402
|
const REVIEW_EDITOR_TITLE =
|
|
227
403
|
"Plan review — Enter: continue to verdict · Esc: skip · Ctrl+G: $EDITOR";
|
|
@@ -238,7 +414,10 @@ const DENY_FEEDBACK_TITLE = "Deny feedback (optional) — Enter to send";
|
|
|
238
414
|
* AbortSignal — `signal?.aborted` is checked before each dialog (the aborted arm wins).
|
|
239
415
|
*
|
|
240
416
|
* Presentation options (defaults preserve the plan-path behavior byte-for-byte):
|
|
241
|
-
* `editorTitle`/`verdicts` swap the displayed strings; `
|
|
417
|
+
* `editorTitle`/`verdicts` swap the displayed strings; `verdicts.implementHere`, when present,
|
|
418
|
+
* makes the verdict select 4 options — approve, implement-here, deny, skip (implement-here sits
|
|
419
|
+
* adjacent to approve: both are "accept the plan" outcomes) — and selecting it returns the
|
|
420
|
+
* `implement-here` outcome arm; `viewOnly: true` skips the write-back
|
|
242
421
|
* branch entirely — the editor output is used only for Esc/dismissed detection, `plan` is
|
|
243
422
|
* returned unchanged and `edited` stays false (deny+feedback is the change channel).
|
|
244
423
|
*/
|
|
@@ -249,16 +428,13 @@ export async function runFirstPartyReview(args: {
|
|
|
249
428
|
writeDraft(plan: string): boolean;
|
|
250
429
|
signal?: AbortSignal;
|
|
251
430
|
editorTitle?: string;
|
|
252
|
-
verdicts?: { approve: string; deny: string; skip: string };
|
|
431
|
+
verdicts?: { approve: string; deny: string; skip: string; implementHere?: string };
|
|
253
432
|
viewOnly?: boolean;
|
|
254
433
|
}): Promise<{ outcome: ReviewOutcome; plan: string; edited: boolean }> {
|
|
255
434
|
const { ui, writeDraft, signal } = args;
|
|
256
435
|
const editorTitle = args.editorTitle ?? REVIEW_EDITOR_TITLE;
|
|
257
|
-
const verdicts
|
|
258
|
-
|
|
259
|
-
deny: VERDICT_DENY,
|
|
260
|
-
skip: VERDICT_SKIP,
|
|
261
|
-
};
|
|
436
|
+
const verdicts: { approve: string; deny: string; skip: string; implementHere?: string } =
|
|
437
|
+
args.verdicts ?? verdictsFor(PLAN_SUBJECT);
|
|
262
438
|
let plan = args.plan;
|
|
263
439
|
let edited = false;
|
|
264
440
|
const result = (
|
|
@@ -291,15 +467,18 @@ export async function runFirstPartyReview(args: {
|
|
|
291
467
|
}
|
|
292
468
|
|
|
293
469
|
if (signal?.aborted) return result({ status: "aborted" });
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
);
|
|
470
|
+
const options =
|
|
471
|
+
verdicts.implementHere === undefined
|
|
472
|
+
? [verdicts.approve, verdicts.deny, verdicts.skip]
|
|
473
|
+
: [verdicts.approve, verdicts.implementHere, verdicts.deny, verdicts.skip];
|
|
474
|
+
const verdict = await ui.select("Plan review verdict", options, { signal });
|
|
299
475
|
if (signal?.aborted) return result({ status: "aborted" });
|
|
300
476
|
if (verdict === verdicts.approve) {
|
|
301
477
|
return result({ status: "completed", approved: true, reviewId: randomUUID() });
|
|
302
478
|
}
|
|
479
|
+
if (verdicts.implementHere !== undefined && verdict === verdicts.implementHere) {
|
|
480
|
+
return result({ status: "implement-here", reviewId: randomUUID() });
|
|
481
|
+
}
|
|
303
482
|
if (verdict === verdicts.deny) {
|
|
304
483
|
const feedback = await ui.editor(DENY_FEEDBACK_TITLE, "");
|
|
305
484
|
if (signal?.aborted) return result({ status: "aborted" });
|
|
@@ -316,135 +495,38 @@ export async function runFirstPartyReview(args: {
|
|
|
316
495
|
|
|
317
496
|
// ------------------------------------------------------------------- the objective review arm
|
|
318
497
|
|
|
319
|
-
/** The objective-flavored verdict options (approval auto-saves). */
|
|
320
|
-
const OBJECTIVE_VERDICTS = {
|
|
321
|
-
approve: "Approve — auto-save to GitHub",
|
|
322
|
-
deny: "Deny — send feedback for revision",
|
|
323
|
-
skip: "Skip — decide later (manual /objective-save)",
|
|
324
|
-
};
|
|
325
|
-
|
|
326
498
|
const OBJECTIVE_REVIEW_EDITOR_TITLE =
|
|
327
499
|
"Objective review (view only — edits are not saved) — Enter: continue to verdict · Esc: skip · " +
|
|
328
500
|
"Ctrl+G: $EDITOR";
|
|
329
501
|
|
|
330
502
|
/**
|
|
331
503
|
* Map a non-approved objective review outcome into the model-facing tool result (exported for
|
|
332
|
-
* the offline tests) — the objective-flavored sibling of `reviewOutcomeResult
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* approved outcomes to `approvedObjectiveSaveResult` first, so
|
|
336
|
-
*
|
|
504
|
+
* the offline tests) — the objective-flavored sibling of `reviewOutcomeResult`, delegating to
|
|
505
|
+
* `subjectReviewOutcomeResult` with `OBJECTIVE_SUBJECT`. Every arm carries
|
|
506
|
+
* `details.subject: "objective"`; the texts redirect to `objective_draft` / `/objective-save`.
|
|
507
|
+
* The execute path routes approved outcomes to `approvedObjectiveSaveResult` first, so
|
|
508
|
+
* `completed` renders DENIED here.
|
|
337
509
|
*/
|
|
338
510
|
export function objectiveReviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
339
|
-
|
|
340
|
-
case "unavailable":
|
|
341
|
-
return {
|
|
342
|
-
content: [
|
|
343
|
-
{
|
|
344
|
-
type: "text",
|
|
345
|
-
text:
|
|
346
|
-
`WARNING: ${outcome.warning} — no review performed. ` +
|
|
347
|
-
"Present the complete objective + structured roadmap to the user instead.",
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
details: { status: "unavailable", subject: "objective" },
|
|
351
|
-
};
|
|
352
|
-
case "aborted":
|
|
353
|
-
return {
|
|
354
|
-
content: [{ type: "text", text: "objective review aborted (turn interrupted)." }],
|
|
355
|
-
details: { status: "aborted", subject: "objective" },
|
|
356
|
-
};
|
|
357
|
-
case "dismissed":
|
|
358
|
-
return {
|
|
359
|
-
content: [
|
|
360
|
-
{
|
|
361
|
-
type: "text",
|
|
362
|
-
text:
|
|
363
|
-
"objective review dismissed — present the complete objective + structured roadmap " +
|
|
364
|
-
"to the user; the human runs /objective-save (the manual failsafe).",
|
|
365
|
-
},
|
|
366
|
-
],
|
|
367
|
-
details: { status: "skipped", reason: "dismissed", subject: "objective" },
|
|
368
|
-
};
|
|
369
|
-
case "completed": {
|
|
370
|
-
const feedback = outcome.feedback ? `\n\nReviewer feedback:\n${outcome.feedback}` : "";
|
|
371
|
-
return {
|
|
372
|
-
content: [
|
|
373
|
-
{
|
|
374
|
-
type: "text",
|
|
375
|
-
text:
|
|
376
|
-
"objective DENIED — revise per this feedback, rewrite the working draft with " +
|
|
377
|
-
`objective_draft, then call plan_review again.${feedback}`,
|
|
378
|
-
},
|
|
379
|
-
],
|
|
380
|
-
details: {
|
|
381
|
-
status: "completed",
|
|
382
|
-
approved: outcome.approved,
|
|
383
|
-
feedback: outcome.feedback ?? null,
|
|
384
|
-
reviewId: outcome.reviewId,
|
|
385
|
-
subject: "objective",
|
|
386
|
-
},
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
}
|
|
511
|
+
return subjectReviewOutcomeResult(OBJECTIVE_SUBJECT, outcome);
|
|
390
512
|
}
|
|
391
513
|
|
|
392
514
|
/**
|
|
393
515
|
* Map an APPROVED objective review outcome + the `objectiveApprovalSave` outcome into the
|
|
394
516
|
* model-facing tool result (exported for the offline tests) — the objective sibling of
|
|
395
|
-
* `approvedSaveResult
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* The `no-draft` arm is defensively unreachable (the review just read the draft) but maps to
|
|
399
|
-
* the save-failed shape rather than throwing.
|
|
517
|
+
* `approvedSaveResult`, delegating to `approvedSubjectSaveResult` with `OBJECTIVE_SUBJECT` and
|
|
518
|
+
* no opts (the objective path reviews only the rendered draft, view-only — no
|
|
519
|
+
* `paramMismatch`/`edited`).
|
|
400
520
|
*/
|
|
401
521
|
export function approvedObjectiveSaveResult(
|
|
402
522
|
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
403
523
|
save: ObjectiveApprovalSaveOutcome,
|
|
404
524
|
): ToolResult {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
: ""
|
|
409
|
-
|
|
410
|
-
status: "completed",
|
|
411
|
-
approved: true,
|
|
412
|
-
reviewId: outcome.reviewId,
|
|
413
|
-
feedback: outcome.feedback ?? null,
|
|
414
|
-
subject: "objective",
|
|
415
|
-
};
|
|
416
|
-
if (save.status === "saved") {
|
|
417
|
-
const saveText = save.result.content[0]?.text ?? "";
|
|
418
|
-
return {
|
|
419
|
-
content: [
|
|
420
|
-
{ type: "text", text: `objective APPROVED by reviewer.${feedback}\n\n${saveText}` },
|
|
421
|
-
],
|
|
422
|
-
details: { ...base, saved: true, gateExited: save.gateExited, save: save.result.details },
|
|
423
|
-
terminate: true,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
const error =
|
|
427
|
-
save.status === "no-draft"
|
|
428
|
-
? "no objective draft resolved"
|
|
429
|
-
: save.result.details.ok
|
|
430
|
-
? "unknown save failure"
|
|
431
|
-
: save.result.details.error;
|
|
432
|
-
return {
|
|
433
|
-
content: [
|
|
434
|
-
{
|
|
435
|
-
type: "text",
|
|
436
|
-
text:
|
|
437
|
-
`objective APPROVED by reviewer, but the auto-save FAILED (${error}) — the session ` +
|
|
438
|
-
"stays read-only. Ask the user to run /objective-save (the manual failsafe) to " +
|
|
439
|
-
`retry.${feedback}`,
|
|
440
|
-
},
|
|
441
|
-
],
|
|
442
|
-
details: {
|
|
443
|
-
...base,
|
|
444
|
-
saved: false,
|
|
445
|
-
save: save.status === "no-draft" ? null : save.result.details,
|
|
446
|
-
},
|
|
447
|
-
};
|
|
525
|
+
return approvedSubjectSaveResult(
|
|
526
|
+
OBJECTIVE_SUBJECT,
|
|
527
|
+
outcome,
|
|
528
|
+
save.status === "no-draft" ? { status: "no-source" } : save,
|
|
529
|
+
);
|
|
448
530
|
}
|
|
449
531
|
|
|
450
532
|
/**
|
|
@@ -477,7 +559,13 @@ export async function executeObjectiveReview(
|
|
|
477
559
|
"(prose + the structured roadmap), then call plan_review again.",
|
|
478
560
|
},
|
|
479
561
|
],
|
|
480
|
-
details: {
|
|
562
|
+
details: {
|
|
563
|
+
ok: false,
|
|
564
|
+
error: "no objective draft to review — write it with objective_draft first",
|
|
565
|
+
error_type: "no_objective_draft",
|
|
566
|
+
status: "skipped",
|
|
567
|
+
reason: "no_objective_draft",
|
|
568
|
+
},
|
|
481
569
|
};
|
|
482
570
|
}
|
|
483
571
|
// 3. The reviewed bytes are the RENDERED markdown (prose + roadmap table) — never raw JSON.
|
|
@@ -495,7 +583,7 @@ export async function executeObjectiveReview(
|
|
|
495
583
|
writeDraft: () => true, // unreachable under viewOnly — the branch is skipped
|
|
496
584
|
signal: sig,
|
|
497
585
|
editorTitle: OBJECTIVE_REVIEW_EDITOR_TITLE,
|
|
498
|
-
verdicts:
|
|
586
|
+
verdicts: verdictsFor(OBJECTIVE_SUBJECT),
|
|
499
587
|
viewOnly: true,
|
|
500
588
|
});
|
|
501
589
|
outcome = fp.outcome;
|
|
@@ -542,7 +630,13 @@ export async function executePlanReview(
|
|
|
542
630
|
text: "plan_review takes { plan?: string } — omit it (the plan-draft artifact is preferred) or pass a string.",
|
|
543
631
|
},
|
|
544
632
|
],
|
|
545
|
-
details: {
|
|
633
|
+
details: {
|
|
634
|
+
ok: false,
|
|
635
|
+
error: "plan must be a string",
|
|
636
|
+
error_type: "bad_input",
|
|
637
|
+
status: "skipped",
|
|
638
|
+
reason: "bad_input",
|
|
639
|
+
},
|
|
546
640
|
};
|
|
547
641
|
}
|
|
548
642
|
// 1. Objective-author session → the objective review arm: the rendered
|
|
@@ -565,7 +659,13 @@ export async function executePlanReview(
|
|
|
565
659
|
"param), then call plan_review again.",
|
|
566
660
|
},
|
|
567
661
|
],
|
|
568
|
-
details: {
|
|
662
|
+
details: {
|
|
663
|
+
ok: false,
|
|
664
|
+
error: "no plan to review — write the draft with plan_draft first",
|
|
665
|
+
error_type: "no_plan",
|
|
666
|
+
status: "skipped",
|
|
667
|
+
reason: "no_plan",
|
|
668
|
+
},
|
|
569
669
|
};
|
|
570
670
|
}
|
|
571
671
|
// 4. Backend dispatch: plannotator-selected → the event-bus bridge; ANY other selection
|
|
@@ -577,17 +677,31 @@ export async function executePlanReview(
|
|
|
577
677
|
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
578
678
|
outcome = await bridge.review(src.plan, sig);
|
|
579
679
|
} else {
|
|
680
|
+
// The 4th verdict (implement-here, the no-save exit) is offered UNLESS this is an
|
|
681
|
+
// objective-node planning session — a node-linked plan must save (the node advance and
|
|
682
|
+
// backlink depend on it), so the claim suppresses it back to the 3-option select.
|
|
580
683
|
const fp = await runFirstPartyReview({
|
|
581
684
|
ui: ctx.ui,
|
|
582
685
|
plan: src.plan,
|
|
583
686
|
writeDraft: (text) => writePlanDraft(pi, ctx, text).details.ok,
|
|
584
687
|
signal: sig,
|
|
688
|
+
verdicts:
|
|
689
|
+
readNodeClaim(ctx) === null
|
|
690
|
+
? { ...verdictsFor(PLAN_SUBJECT), implementHere: VERDICT_IMPLEMENT_HERE }
|
|
691
|
+
: undefined,
|
|
585
692
|
});
|
|
586
693
|
outcome = fp.outcome;
|
|
587
694
|
reviewedPlan = fp.plan;
|
|
588
695
|
edited = fp.edited;
|
|
589
696
|
}
|
|
590
|
-
// 5.
|
|
697
|
+
// 5. IMPLEMENT-HERE (first-party only) routes before the generic mapper (mirror the
|
|
698
|
+
// approved-first split): gate exit WITHOUT save through the implementHereExit seam → a
|
|
699
|
+
// NON-terminating result carrying the implement-now guidance.
|
|
700
|
+
if (outcome.status === "implement-here") {
|
|
701
|
+
const exit = implementHereExit(ctx, gating);
|
|
702
|
+
return implementHereResult(outcome, exit, { cwd: ctx.cwd, plan: reviewedPlan, edited });
|
|
703
|
+
}
|
|
704
|
+
// 6. An APPROVED decision (either backend) wires into the approvalSave seam (auto-save → D1a
|
|
591
705
|
// gate exit → terminating result); everything else maps via reviewOutcomeResult.
|
|
592
706
|
if (outcome.status === "completed" && outcome.approved) {
|
|
593
707
|
const save = await approvalSave(pi, ctx, gating, { reviewedPlan });
|
|
@@ -621,7 +735,7 @@ export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
|
621
735
|
"Keep the working draft current with plan_draft — the validated plan-draft artifact is what plan_review reviews AND auto-saves; the plan param is only a fallback when no draft exists.",
|
|
622
736
|
"Call plan_review only when the plan is decision-complete.",
|
|
623
737
|
"On a DENIED review, revise per the feedback, rewrite the draft with plan_draft, then call plan_review again.",
|
|
624
|
-
"On an APPROVED
|
|
738
|
+
"On an APPROVED plan_review, the plan is auto-saved and the turn ends — never re-dump the plan as a final message and never tell the user to run /plan-save; relay the save outcome instead.",
|
|
625
739
|
"If plan_review reports it was skipped or unavailable (headless, dismissed), fall back to presenting the complete plan; the human runs /plan-save (the manual failsafe).",
|
|
626
740
|
],
|
|
627
741
|
executionMode: "sequential",
|