@mgiles/perk 1.1.0 → 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 +68 -44
- 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 +19 -12
- package/extension/doors/address.ts +4 -4
- package/extension/doors/askUser.ts +12 -8
- package/extension/doors/ciExecutor.ts +21 -14
- package/extension/doors/hunkHandoff.ts +202 -0
- package/extension/doors/land.ts +31 -9
- package/extension/doors/learn.ts +2 -2
- package/extension/doors/learnFactory.ts +144 -0
- package/extension/doors/plannotatorHandoff.ts +509 -0
- package/extension/doors/prReview.ts +4 -4
- package/extension/doors/prReviewBrowser.ts +341 -0
- package/extension/doors/prReviewTerminal.ts +267 -0
- package/extension/doors/selfcheck.ts +238 -5
- package/extension/doors/submit.ts +20 -0
- package/extension/doors/submitPrReview.ts +408 -0
- package/extension/factories/objective.ts +15 -5
- package/extension/factories/objectiveAuthor.ts +15 -32
- package/extension/factories/objectiveDraft.ts +1 -1
- package/extension/factories/objectivePlan.ts +12 -10
- package/extension/factories/objectiveSave.ts +2 -2
- package/extension/factories/planMode.ts +22 -40
- package/extension/factories/planReview.ts +213 -191
- package/extension/factories/planSave.ts +7 -7
- package/extension/index.ts +83 -25
- package/extension/substrate/bindingDelivery.ts +32 -10
- package/extension/substrate/bindings.ts +4 -2
- package/extension/substrate/cache.ts +34 -7
- package/extension/substrate/clipboard.ts +81 -0
- package/extension/substrate/config.ts +88 -65
- package/extension/substrate/git.ts +43 -0
- package/extension/substrate/paths.ts +1 -1
- package/extension/substrate/prompts.ts +2 -2
- package/extension/substrate/providers.ts +62 -8
- package/extension/substrate/sessionPointers.ts +35 -6
- package/extension/substrate/structuredOutput.ts +3 -1
- package/extension/substrate/terminalLaunch.ts +178 -0
- package/extension/substrate/toolGating.ts +330 -79
- 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 +330 -12
- package/extension/vendor/btw/btw.ts +10 -0
- package/extension/worker/readOnlySession.ts +19 -6
- package/extension/worker/worker.ts +77 -7
- package/extension/workerMain.ts +12 -13
- package/package.json +3 -3
- package/prompts/_fixtures/live.yaml +117 -2
- 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 +1 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-plan/guidance.md +1 -1
- package/prompts/stages/objective-plan/seed.md +1 -1
- package/prompts/stages/objective-reconcile.md +1 -1
- package/prompts/stages/objective-replan.md +1 -1
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- 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 +1 -1
- package/prompts/stages/replan.md +2 -2
- package/prompts/stages/skills/create-from.md +1 -1
- package/prompts/stages/skills/create.md +1 -1
- package/prompts/stages/skills/refine.md +1 -1
- package/shared/README.md +22 -18
- package/shared/bindings.yaml +10 -2
- package/shared/contracts-history.md +24 -0
- package/shared/contracts.md +1442 -1787
- package/shared/providers.yaml +8 -1
- package/shared/registry.yaml +7 -8
- package/shared/schemas/inputs/review-submit-batch.schema.json +66 -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-submit.schema.json +64 -0
- package/extension/doors/learnCode.ts +0 -100
- package/extension/doors/learnDocs.ts +0 -100
- package/extension/doors/prReviewLocal.ts +0 -229
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
createPlannotatorBridge,
|
|
51
51
|
isPlannotatorPlanSelected,
|
|
52
52
|
} from "../adapters/planAdapterPlannotator.ts";
|
|
53
|
+
import type { Result } from "../substrate/result.ts";
|
|
53
54
|
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
54
55
|
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
55
56
|
import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
|
|
@@ -83,21 +84,70 @@ interface ToolResult {
|
|
|
83
84
|
terminate?: boolean;
|
|
84
85
|
}
|
|
85
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
|
+
|
|
86
134
|
const SKIP_TEXT =
|
|
87
135
|
"no interactive review surface available — present the complete plan to the user in your next message.";
|
|
88
136
|
|
|
89
137
|
function skipResult(): ToolResult {
|
|
90
|
-
return { content: [{ type: "text", text: SKIP_TEXT }], details: { status: "skipped" } };
|
|
138
|
+
return { content: [{ type: "text", text: SKIP_TEXT }], details: { ok: true, status: "skipped" } };
|
|
91
139
|
}
|
|
92
140
|
|
|
93
141
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* outcomes to
|
|
97
|
-
* `approved: false` (kept total for safety
|
|
98
|
-
*
|
|
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.
|
|
99
149
|
*/
|
|
100
|
-
|
|
150
|
+
function subjectReviewOutcomeResult(subject: ReviewSubject, outcome: ReviewOutcome): ToolResult {
|
|
101
151
|
switch (outcome.status) {
|
|
102
152
|
case "unavailable":
|
|
103
153
|
return {
|
|
@@ -106,15 +156,21 @@ export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
|
106
156
|
type: "text",
|
|
107
157
|
text:
|
|
108
158
|
`WARNING: ${outcome.warning} — no review performed. ` +
|
|
109
|
-
|
|
159
|
+
`Present ${subject.presentUnavailable} instead.`,
|
|
110
160
|
},
|
|
111
161
|
],
|
|
112
|
-
details: {
|
|
162
|
+
details: {
|
|
163
|
+
ok: false,
|
|
164
|
+
error: outcome.warning,
|
|
165
|
+
error_type: "unavailable",
|
|
166
|
+
status: "unavailable",
|
|
167
|
+
...subject.detailsExtra,
|
|
168
|
+
},
|
|
113
169
|
};
|
|
114
170
|
case "aborted":
|
|
115
171
|
return {
|
|
116
|
-
content: [{ type: "text", text:
|
|
117
|
-
details: { status: "aborted" },
|
|
172
|
+
content: [{ type: "text", text: `${subject.noun} review aborted (turn interrupted).` }],
|
|
173
|
+
details: { ok: true, status: "aborted", ...subject.detailsExtra },
|
|
118
174
|
};
|
|
119
175
|
case "dismissed":
|
|
120
176
|
return {
|
|
@@ -122,38 +178,41 @@ export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
|
122
178
|
{
|
|
123
179
|
type: "text",
|
|
124
180
|
text:
|
|
125
|
-
|
|
126
|
-
|
|
181
|
+
`${subject.noun} review dismissed — present ${subject.present}; the human runs ` +
|
|
182
|
+
`${subject.failsafeCmd} (the manual failsafe).`,
|
|
127
183
|
},
|
|
128
184
|
],
|
|
129
|
-
details: { status: "skipped", reason: "dismissed" },
|
|
185
|
+
details: { ok: true, status: "skipped", reason: "dismissed", ...subject.detailsExtra },
|
|
130
186
|
};
|
|
131
187
|
case "implement-here":
|
|
132
|
-
// Defensively unreachable: the execute path routes implement-here to
|
|
133
|
-
// FIRST (mirror the approved-first routing)
|
|
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.
|
|
134
191
|
return {
|
|
135
192
|
content: [
|
|
136
193
|
{
|
|
137
194
|
type: "text",
|
|
138
195
|
text:
|
|
139
|
-
|
|
140
|
-
|
|
196
|
+
`implement-here verdict received ${subject.implementHereWhere} — nothing saved; ` +
|
|
197
|
+
`present ${subject.present}.`,
|
|
141
198
|
},
|
|
142
199
|
],
|
|
143
|
-
details: { status: "skipped", reason: "implement-here" },
|
|
200
|
+
details: { ok: true, status: "skipped", reason: "implement-here", ...subject.detailsExtra },
|
|
144
201
|
};
|
|
145
202
|
case "completed": {
|
|
146
203
|
const feedback = outcome.feedback ? `\n\nReviewer feedback:\n${outcome.feedback}` : "";
|
|
147
204
|
const text =
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
`${subject.noun} DENIED — revise per this feedback, rewrite the working draft with ` +
|
|
206
|
+
`${subject.draftTool}, then call plan_review again.${feedback}`;
|
|
150
207
|
return {
|
|
151
208
|
content: [{ type: "text", text }],
|
|
152
209
|
details: {
|
|
210
|
+
ok: true,
|
|
153
211
|
status: "completed",
|
|
154
212
|
approved: outcome.approved,
|
|
155
213
|
feedback: outcome.feedback ?? null,
|
|
156
214
|
reviewId: outcome.reviewId,
|
|
215
|
+
...subject.detailsExtra,
|
|
157
216
|
},
|
|
158
217
|
};
|
|
159
218
|
}
|
|
@@ -161,52 +220,82 @@ export function reviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
|
161
220
|
}
|
|
162
221
|
|
|
163
222
|
/**
|
|
164
|
-
* Map
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* and directs the human `/plan-save` failsafe. Reviewer feedback is surfaced loudly as
|
|
168
|
-
* implementation guidance — the approved bytes were saved verbatim, never post-edited. `edited`
|
|
169
|
-
* (first-party only) flags that human edits were written back to the draft pre-verdict, so the
|
|
170
|
-
* saved bytes carry them. The `no-plan` arm is defensively unreachable (the reviewed plan is
|
|
171
|
-
* 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.
|
|
172
226
|
*/
|
|
173
|
-
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,
|
|
174
254
|
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
175
|
-
save:
|
|
176
|
-
opts
|
|
255
|
+
save: SubjectSaveOutcome,
|
|
256
|
+
opts?: { paramMismatch?: boolean; edited?: boolean },
|
|
177
257
|
): ToolResult {
|
|
178
258
|
const feedback = outcome.feedback
|
|
179
|
-
?
|
|
180
|
-
outcome.feedback
|
|
259
|
+
? `\n\nReviewer feedback (implementation guidance — the approved ${subject.noun} was saved ` +
|
|
260
|
+
`verbatim):\n${outcome.feedback}`
|
|
181
261
|
: "";
|
|
182
262
|
const base = {
|
|
183
263
|
status: "completed",
|
|
184
264
|
approved: true,
|
|
185
265
|
reviewId: outcome.reviewId,
|
|
186
266
|
feedback: outcome.feedback ?? null,
|
|
187
|
-
...
|
|
267
|
+
...subject.detailsExtra,
|
|
268
|
+
...(opts?.edited === true ? { edited: true } : {}),
|
|
188
269
|
};
|
|
189
270
|
if (save.status === "saved") {
|
|
190
271
|
const saveText = save.result.content[0]?.text ?? "";
|
|
191
|
-
const mismatch = opts.paramMismatch
|
|
192
|
-
? "\n\n⚠ differing plan param ignored — the validated draft was reviewed and saved."
|
|
193
|
-
: "";
|
|
194
272
|
const edited =
|
|
195
|
-
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
|
+
: "";
|
|
196
278
|
return {
|
|
197
279
|
content: [
|
|
198
280
|
{
|
|
199
281
|
type: "text",
|
|
200
|
-
text:
|
|
282
|
+
text: `${subject.noun} APPROVED by reviewer.${feedback}\n\n${saveText}${edited}${mismatch}`,
|
|
201
283
|
},
|
|
202
284
|
],
|
|
203
|
-
|
|
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
|
+
},
|
|
204
293
|
terminate: true,
|
|
205
294
|
};
|
|
206
295
|
}
|
|
207
296
|
const error =
|
|
208
|
-
save.status === "no-
|
|
209
|
-
?
|
|
297
|
+
save.status === "no-source"
|
|
298
|
+
? subject.noSourceError
|
|
210
299
|
: save.result.details.ok
|
|
211
300
|
? "unknown save failure"
|
|
212
301
|
: save.result.details.error;
|
|
@@ -215,18 +304,41 @@ export function approvedSaveResult(
|
|
|
215
304
|
{
|
|
216
305
|
type: "text",
|
|
217
306
|
text:
|
|
218
|
-
|
|
219
|
-
`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}`,
|
|
220
310
|
},
|
|
221
311
|
],
|
|
222
312
|
details: {
|
|
313
|
+
ok: false,
|
|
314
|
+
error,
|
|
315
|
+
error_type: "save_failed",
|
|
223
316
|
...base,
|
|
224
317
|
saved: false,
|
|
225
|
-
save: save.status === "no-
|
|
318
|
+
save: save.status === "no-source" ? null : save.result.details,
|
|
226
319
|
},
|
|
227
320
|
};
|
|
228
321
|
}
|
|
229
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
|
+
|
|
230
342
|
/**
|
|
231
343
|
* Map an IMPLEMENT-HERE review outcome + the `implementHereExit` outcome into the model-facing
|
|
232
344
|
* tool result (exported for the offline tests). NON-terminating on purpose — the model continues
|
|
@@ -248,6 +360,7 @@ export function implementHereResult(
|
|
|
248
360
|
},
|
|
249
361
|
],
|
|
250
362
|
details: {
|
|
363
|
+
ok: true,
|
|
251
364
|
status: "implement-here",
|
|
252
365
|
saved: false,
|
|
253
366
|
gateExited: exit.gateExited,
|
|
@@ -269,10 +382,20 @@ export interface PlanReviewUI {
|
|
|
269
382
|
): Promise<string | undefined>;
|
|
270
383
|
}
|
|
271
384
|
|
|
272
|
-
/**
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
|
|
276
399
|
/** The optional 4th verdict (plan arm only): the no-save implement-here exit (§8.23). */
|
|
277
400
|
const VERDICT_IMPLEMENT_HERE = "Implement here — no issue saved";
|
|
278
401
|
|
|
@@ -310,11 +433,8 @@ export async function runFirstPartyReview(args: {
|
|
|
310
433
|
}): Promise<{ outcome: ReviewOutcome; plan: string; edited: boolean }> {
|
|
311
434
|
const { ui, writeDraft, signal } = args;
|
|
312
435
|
const editorTitle = args.editorTitle ?? REVIEW_EDITOR_TITLE;
|
|
313
|
-
const verdicts
|
|
314
|
-
|
|
315
|
-
deny: VERDICT_DENY,
|
|
316
|
-
skip: VERDICT_SKIP,
|
|
317
|
-
};
|
|
436
|
+
const verdicts: { approve: string; deny: string; skip: string; implementHere?: string } =
|
|
437
|
+
args.verdicts ?? verdictsFor(PLAN_SUBJECT);
|
|
318
438
|
let plan = args.plan;
|
|
319
439
|
let edited = false;
|
|
320
440
|
const result = (
|
|
@@ -375,149 +495,38 @@ export async function runFirstPartyReview(args: {
|
|
|
375
495
|
|
|
376
496
|
// ------------------------------------------------------------------- the objective review arm
|
|
377
497
|
|
|
378
|
-
/** The objective-flavored verdict options (approval auto-saves). */
|
|
379
|
-
const OBJECTIVE_VERDICTS = {
|
|
380
|
-
approve: "Approve — auto-save to GitHub",
|
|
381
|
-
deny: "Deny — send feedback for revision",
|
|
382
|
-
skip: "Skip — decide later (manual /objective-save)",
|
|
383
|
-
};
|
|
384
|
-
|
|
385
498
|
const OBJECTIVE_REVIEW_EDITOR_TITLE =
|
|
386
499
|
"Objective review (view only — edits are not saved) — Enter: continue to verdict · Esc: skip · " +
|
|
387
500
|
"Ctrl+G: $EDITOR";
|
|
388
501
|
|
|
389
502
|
/**
|
|
390
503
|
* Map a non-approved objective review outcome into the model-facing tool result (exported for
|
|
391
|
-
* the offline tests) — the objective-flavored sibling of `reviewOutcomeResult
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
* approved outcomes to `approvedObjectiveSaveResult` first, so
|
|
395
|
-
*
|
|
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.
|
|
396
509
|
*/
|
|
397
510
|
export function objectiveReviewOutcomeResult(outcome: ReviewOutcome): ToolResult {
|
|
398
|
-
|
|
399
|
-
case "unavailable":
|
|
400
|
-
return {
|
|
401
|
-
content: [
|
|
402
|
-
{
|
|
403
|
-
type: "text",
|
|
404
|
-
text:
|
|
405
|
-
`WARNING: ${outcome.warning} — no review performed. ` +
|
|
406
|
-
"Present the complete objective + structured roadmap to the user instead.",
|
|
407
|
-
},
|
|
408
|
-
],
|
|
409
|
-
details: { status: "unavailable", subject: "objective" },
|
|
410
|
-
};
|
|
411
|
-
case "aborted":
|
|
412
|
-
return {
|
|
413
|
-
content: [{ type: "text", text: "objective review aborted (turn interrupted)." }],
|
|
414
|
-
details: { status: "aborted", subject: "objective" },
|
|
415
|
-
};
|
|
416
|
-
case "dismissed":
|
|
417
|
-
return {
|
|
418
|
-
content: [
|
|
419
|
-
{
|
|
420
|
-
type: "text",
|
|
421
|
-
text:
|
|
422
|
-
"objective review dismissed — present the complete objective + structured roadmap " +
|
|
423
|
-
"to the user; the human runs /objective-save (the manual failsafe).",
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
details: { status: "skipped", reason: "dismissed", subject: "objective" },
|
|
427
|
-
};
|
|
428
|
-
case "implement-here":
|
|
429
|
-
// Defensively unreachable twice over: the objective arm never offers the verdict, and the
|
|
430
|
-
// execute path routes implement-here first. Map to a skip shape rather than throwing.
|
|
431
|
-
return {
|
|
432
|
-
content: [
|
|
433
|
-
{
|
|
434
|
-
type: "text",
|
|
435
|
-
text:
|
|
436
|
-
"implement-here verdict received on the objective path — nothing saved; present " +
|
|
437
|
-
"the complete objective + structured roadmap to the user.",
|
|
438
|
-
},
|
|
439
|
-
],
|
|
440
|
-
details: { status: "skipped", reason: "implement-here", subject: "objective" },
|
|
441
|
-
};
|
|
442
|
-
case "completed": {
|
|
443
|
-
const feedback = outcome.feedback ? `\n\nReviewer feedback:\n${outcome.feedback}` : "";
|
|
444
|
-
return {
|
|
445
|
-
content: [
|
|
446
|
-
{
|
|
447
|
-
type: "text",
|
|
448
|
-
text:
|
|
449
|
-
"objective DENIED — revise per this feedback, rewrite the working draft with " +
|
|
450
|
-
`objective_draft, then call plan_review again.${feedback}`,
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
details: {
|
|
454
|
-
status: "completed",
|
|
455
|
-
approved: outcome.approved,
|
|
456
|
-
feedback: outcome.feedback ?? null,
|
|
457
|
-
reviewId: outcome.reviewId,
|
|
458
|
-
subject: "objective",
|
|
459
|
-
},
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
}
|
|
511
|
+
return subjectReviewOutcomeResult(OBJECTIVE_SUBJECT, outcome);
|
|
463
512
|
}
|
|
464
513
|
|
|
465
514
|
/**
|
|
466
515
|
* Map an APPROVED objective review outcome + the `objectiveApprovalSave` outcome into the
|
|
467
516
|
* model-facing tool result (exported for the offline tests) — the objective sibling of
|
|
468
|
-
* `approvedSaveResult
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* The `no-draft` arm is defensively unreachable (the review just read the draft) but maps to
|
|
472
|
-
* 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`).
|
|
473
520
|
*/
|
|
474
521
|
export function approvedObjectiveSaveResult(
|
|
475
522
|
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
476
523
|
save: ObjectiveApprovalSaveOutcome,
|
|
477
524
|
): ToolResult {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
: ""
|
|
482
|
-
|
|
483
|
-
status: "completed",
|
|
484
|
-
approved: true,
|
|
485
|
-
reviewId: outcome.reviewId,
|
|
486
|
-
feedback: outcome.feedback ?? null,
|
|
487
|
-
subject: "objective",
|
|
488
|
-
};
|
|
489
|
-
if (save.status === "saved") {
|
|
490
|
-
const saveText = save.result.content[0]?.text ?? "";
|
|
491
|
-
return {
|
|
492
|
-
content: [
|
|
493
|
-
{ type: "text", text: `objective APPROVED by reviewer.${feedback}\n\n${saveText}` },
|
|
494
|
-
],
|
|
495
|
-
details: { ...base, saved: true, gateExited: save.gateExited, save: save.result.details },
|
|
496
|
-
terminate: true,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
const error =
|
|
500
|
-
save.status === "no-draft"
|
|
501
|
-
? "no objective draft resolved"
|
|
502
|
-
: save.result.details.ok
|
|
503
|
-
? "unknown save failure"
|
|
504
|
-
: save.result.details.error;
|
|
505
|
-
return {
|
|
506
|
-
content: [
|
|
507
|
-
{
|
|
508
|
-
type: "text",
|
|
509
|
-
text:
|
|
510
|
-
`objective APPROVED by reviewer, but the auto-save FAILED (${error}) — the session ` +
|
|
511
|
-
"stays read-only. Ask the user to run /objective-save (the manual failsafe) to " +
|
|
512
|
-
`retry.${feedback}`,
|
|
513
|
-
},
|
|
514
|
-
],
|
|
515
|
-
details: {
|
|
516
|
-
...base,
|
|
517
|
-
saved: false,
|
|
518
|
-
save: save.status === "no-draft" ? null : save.result.details,
|
|
519
|
-
},
|
|
520
|
-
};
|
|
525
|
+
return approvedSubjectSaveResult(
|
|
526
|
+
OBJECTIVE_SUBJECT,
|
|
527
|
+
outcome,
|
|
528
|
+
save.status === "no-draft" ? { status: "no-source" } : save,
|
|
529
|
+
);
|
|
521
530
|
}
|
|
522
531
|
|
|
523
532
|
/**
|
|
@@ -550,7 +559,13 @@ export async function executeObjectiveReview(
|
|
|
550
559
|
"(prose + the structured roadmap), then call plan_review again.",
|
|
551
560
|
},
|
|
552
561
|
],
|
|
553
|
-
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
|
+
},
|
|
554
569
|
};
|
|
555
570
|
}
|
|
556
571
|
// 3. The reviewed bytes are the RENDERED markdown (prose + roadmap table) — never raw JSON.
|
|
@@ -568,7 +583,7 @@ export async function executeObjectiveReview(
|
|
|
568
583
|
writeDraft: () => true, // unreachable under viewOnly — the branch is skipped
|
|
569
584
|
signal: sig,
|
|
570
585
|
editorTitle: OBJECTIVE_REVIEW_EDITOR_TITLE,
|
|
571
|
-
verdicts:
|
|
586
|
+
verdicts: verdictsFor(OBJECTIVE_SUBJECT),
|
|
572
587
|
viewOnly: true,
|
|
573
588
|
});
|
|
574
589
|
outcome = fp.outcome;
|
|
@@ -615,7 +630,13 @@ export async function executePlanReview(
|
|
|
615
630
|
text: "plan_review takes { plan?: string } — omit it (the plan-draft artifact is preferred) or pass a string.",
|
|
616
631
|
},
|
|
617
632
|
],
|
|
618
|
-
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
|
+
},
|
|
619
640
|
};
|
|
620
641
|
}
|
|
621
642
|
// 1. Objective-author session → the objective review arm: the rendered
|
|
@@ -638,7 +659,13 @@ export async function executePlanReview(
|
|
|
638
659
|
"param), then call plan_review again.",
|
|
639
660
|
},
|
|
640
661
|
],
|
|
641
|
-
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
|
+
},
|
|
642
669
|
};
|
|
643
670
|
}
|
|
644
671
|
// 4. Backend dispatch: plannotator-selected → the event-bus bridge; ANY other selection
|
|
@@ -660,12 +687,7 @@ export async function executePlanReview(
|
|
|
660
687
|
signal: sig,
|
|
661
688
|
verdicts:
|
|
662
689
|
readNodeClaim(ctx) === null
|
|
663
|
-
? {
|
|
664
|
-
approve: VERDICT_APPROVE,
|
|
665
|
-
deny: VERDICT_DENY,
|
|
666
|
-
skip: VERDICT_SKIP,
|
|
667
|
-
implementHere: VERDICT_IMPLEMENT_HERE,
|
|
668
|
-
}
|
|
690
|
+
? { ...verdictsFor(PLAN_SUBJECT), implementHere: VERDICT_IMPLEMENT_HERE }
|
|
669
691
|
: undefined,
|
|
670
692
|
});
|
|
671
693
|
outcome = fp.outcome;
|
|
@@ -713,7 +735,7 @@ export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
|
713
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.",
|
|
714
736
|
"Call plan_review only when the plan is decision-complete.",
|
|
715
737
|
"On a DENIED review, revise per the feedback, rewrite the draft with plan_draft, then call plan_review again.",
|
|
716
|
-
"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.",
|
|
717
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).",
|
|
718
740
|
],
|
|
719
741
|
executionMode: "sequential",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The warm `/plan-save` door
|
|
1
|
+
// The warm `/plan-save` door. The in-session twin of the Python cold
|
|
2
2
|
// door (`perk plan save`): a deterministic, terminating tool + command that WRAP the existing
|
|
3
3
|
// storage — they do NOT reimplement the GitHub write. `savePlan()` delegates to `perk plan save
|
|
4
4
|
// --json` via the shared cold-door client (`runColdDoor` — the plan markdown rides the run-scratch
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// APPROVAL→SAVE ORCHESTRATION. The exported `approvalSave` seam is the shared
|
|
15
15
|
// APPROVED-review → save flow: artifact-first resolution → `savePlan` (warm node-link recovery
|
|
16
|
-
// inside, from the `objective_node_claim` carrier) → gate exit on a successful save
|
|
16
|
+
// inside, from the `objective_node_claim` carrier) → gate exit on a successful save. The
|
|
17
17
|
// `/plan-save` command is the MANUAL FAILSAFE invocation of the same seam; the review backends
|
|
18
18
|
// (plannotator / first-party / tombell) wire their APPROVED outcome into it.
|
|
19
19
|
//
|
|
@@ -52,7 +52,7 @@ import { nodeClaimsEqual, readNodeClaim } from "./objectivePlan.ts";
|
|
|
52
52
|
import { PLAN_DRAFT_ARTIFACT } from "./planDraft.ts";
|
|
53
53
|
import { generatePlanTitle } from "./planTitle.ts";
|
|
54
54
|
|
|
55
|
-
/** The ok-arm fields
|
|
55
|
+
/** The ok-arm fields — the `details` surface doubles as branch-safe persisted state. */
|
|
56
56
|
export interface PlanSaveOk {
|
|
57
57
|
/** `issue.id` is the opaque string issue id (GitHub "42", Linear "ENG-123") — §8.21. */
|
|
58
58
|
issue: { id: string; url: string };
|
|
@@ -327,7 +327,7 @@ export async function savePlan(
|
|
|
327
327
|
sessionFile: ctx.sessionManager.getSessionFile?.(),
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
-
// Link the live session
|
|
330
|
+
// Link the live session: append iff the rebuilt ref differs, with a strict read-back.
|
|
331
331
|
const ref = r.data.plan_ref;
|
|
332
332
|
if (!planRefsEqual(rebuildWorkflowState(branch()).active_plan_ref ?? null, ref)) {
|
|
333
333
|
appendWorkflowState(pi, ctx, {
|
|
@@ -361,7 +361,7 @@ export async function savePlan(
|
|
|
361
361
|
}
|
|
362
362
|
// Render all THREE node-link outcomes (the silent-partial-failure fix). A failed advance
|
|
363
363
|
// (`linked: false`) is a non-fatal sub-step — the plan genuinely saved — but it must be VISIBLE
|
|
364
|
-
// (the §8.
|
|
364
|
+
// (the §8.3 "surfaced, never swallowed" intent), not swallowed. Both surfaces render content[0].text, so
|
|
365
365
|
// this one site fixes the tool path (the model relays it) and the command path (the user sees the
|
|
366
366
|
// notify) at once.
|
|
367
367
|
let linkSuffix = "";
|
|
@@ -481,8 +481,8 @@ export function decodePlanSaveParams(params: unknown): PlanSaveParams | null {
|
|
|
481
481
|
const TOOL_GUIDELINES = [
|
|
482
482
|
"Use plan_save only after the plan is decision-complete and the user has agreed; it creates the canonical GitHub plan and ends the turn.",
|
|
483
483
|
"Keep the working draft current with plan_draft — the validated plan-draft artifact is what plan_save saves; the `plan` parameter is only a fallback when no draft exists. Never reference line numbers — use durable anchors (function names, behavioral descriptions, structural locations).",
|
|
484
|
-
"Pass consumed_learn (the gathered perk:learn issue ids) only from the learned-docs factory — it links the issues the docs plan consolidates so /land closes + labels them.",
|
|
485
|
-
"When saving an objective-factory plan, pass BOTH objective_id and node_id — this links the node to the plan and advances it planning → in_progress (no separate backlink call).",
|
|
484
|
+
"Pass plan_save's consumed_learn (the gathered perk:learn issue ids) only from the learned-docs factory — it links the issues the docs plan consolidates so /land closes + labels them.",
|
|
485
|
+
"When saving an objective-factory plan, pass plan_save BOTH objective_id and node_id — this links the node to the plan and advances it planning → in_progress (no separate backlink call).",
|
|
486
486
|
];
|
|
487
487
|
|
|
488
488
|
/** Register the warm door: the `plan_save` tool (canonical) + the `/plan-save` command twin. */
|