@mgiles/perk 2.3.0 → 3.1.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 +1 -1
- package/extension/adapters/planAdapterPlannotator.ts +135 -91
- package/extension/adapters/planAdapterTombell.ts +5 -3
- package/extension/doors/address.ts +235 -37
- package/extension/doors/annotationPush.ts +924 -0
- package/extension/doors/auditWaveTools.ts +352 -0
- package/extension/doors/ciExecutor.ts +220 -55
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +412 -0
- package/extension/doors/dreamWaveTools.ts +475 -0
- package/extension/doors/harvestWaveTools.ts +279 -0
- package/extension/doors/land.ts +1 -0
- package/extension/doors/learn.ts +24 -6
- package/extension/doors/learnFactory.ts +33 -7
- package/extension/doors/lifecycleGates.ts +1 -1
- package/extension/doors/objectiveReviewBrowser.ts +537 -0
- package/extension/doors/objectiveStack.ts +1143 -0
- package/extension/doors/planReviewBrowser.ts +475 -0
- package/extension/doors/plannotatorHandoff.ts +120 -49
- package/extension/doors/prReview.ts +180 -63
- package/extension/doors/prReviewBrowser.ts +31 -23
- package/extension/doors/prReviewDynamic.ts +57 -30
- package/extension/doors/prReviewTerminal.ts +6 -11
- package/extension/doors/reviewWaveTools.ts +397 -0
- package/extension/doors/selfcheck.ts +1 -1
- package/extension/doors/submit.ts +82 -7
- package/extension/factories/gistAuthor.ts +4 -4
- package/extension/factories/gistDraft.ts +1 -1
- package/extension/factories/gistSave.ts +2 -2
- package/extension/factories/objective.ts +7 -7
- package/extension/factories/objectiveAuthor.ts +5 -4
- package/extension/factories/objectiveDraft.ts +153 -30
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +163 -9
- package/extension/factories/objectiveSave.ts +90 -2
- package/extension/factories/planMode.ts +5 -3
- package/extension/factories/planReview.ts +304 -46
- package/extension/factories/planTitle.ts +27 -9
- package/extension/hunkFeedback/inbox.ts +389 -0
- package/extension/hunkFeedback/perkFeedback.ts +364 -0
- package/extension/hunkFeedback/receiver.ts +296 -0
- package/extension/hunkFeedback/store.ts +533 -0
- package/extension/index.ts +169 -57
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +166 -23
- package/extension/substrate/coldDoor.ts +2 -3
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +41 -12
- package/extension/substrate/git.ts +95 -2
- package/extension/substrate/providers.ts +4 -33
- package/extension/substrate/resources.ts +11 -0
- package/extension/substrate/result.ts +16 -7
- package/extension/substrate/sessionData.ts +8 -6
- package/extension/substrate/sessionPointers.ts +4 -5
- package/extension/substrate/structuredOutput.ts +78 -19
- package/extension/substrate/toolGating.ts +179 -23
- package/extension/substrate/workflowState.ts +50 -2
- package/extension/surfaces/footerProvider.ts +6 -6
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +213 -275
- package/extension/vendor/btw/btw.ts +94 -17
- package/extension/waves/adversarialReviewWave.ts +181 -0
- package/extension/waves/auditWave.ts +312 -0
- package/extension/waves/draftReviewWave.ts +205 -0
- package/extension/waves/dreamReducerWave.ts +700 -0
- package/extension/waves/dreamReport.ts +1494 -0
- package/extension/waves/dreamWave.ts +927 -0
- package/extension/waves/harvestWave.ts +399 -0
- package/extension/waves/memoryAdapter.ts +14 -1
- package/extension/waves/objectiveExplorerWave.ts +120 -0
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +383 -72
- package/extension/waves/prReviewWave.ts +175 -25
- package/extension/waves/reportWave.ts +503 -107
- package/extension/waves/reviewClassifierWave.ts +109 -0
- package/extension/waves/rpcAdapter.ts +65 -2
- package/extension/worker/readOnlySession.ts +4 -5
- package/extension/worker/worker.ts +71 -93
- package/extension/workerMain.ts +4 -6
- package/package.json +18 -5
- package/prompts/README.md +19 -0
- package/prompts/_fixtures/live.yaml +115 -65
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/common/resume-advisory.md +1 -0
- package/prompts/contexts/adapters/plannotator-gist.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +8 -3
- package/prompts/contexts/adapters/plannotator-plan.md +8 -3
- package/prompts/contexts/gist-authoring.md +6 -20
- package/prompts/contexts/objective-authoring.md +6 -18
- package/prompts/stages/address/action.md +3 -14
- package/prompts/stages/address/preview.md +2 -13
- package/prompts/stages/audit.md +18 -0
- package/prompts/stages/conflict-resolution.md +1 -1
- package/prompts/stages/gist-author/seed.md +4 -4
- package/prompts/stages/gist-save.md +1 -1
- package/prompts/stages/implement.md +3 -1
- package/prompts/stages/learn-code.md +1 -1
- package/prompts/stages/learn-docs.md +2 -2
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/learn-harvest.md +15 -0
- package/prompts/stages/learn-orchestrate.md +1 -1
- package/prompts/stages/objective-author/adopt.md +5 -3
- package/prompts/stages/objective-author/file.md +5 -4
- package/prompts/stages/objective-author/seed.md +5 -4
- package/prompts/stages/objective-land.md +7 -0
- package/prompts/stages/objective-plan/guidance.md +2 -13
- package/prompts/stages/objective-plan/seed.md +5 -12
- package/prompts/stages/objective-recover.md +8 -0
- package/prompts/stages/objective-replan.md +4 -2
- package/prompts/stages/objective-review-browser.md +9 -0
- package/prompts/stages/objective-sync.md +6 -0
- package/prompts/stages/plan-from/adopt.md +2 -2
- package/prompts/stages/plan-from/file.md +2 -2
- package/prompts/stages/plan-review-browser.md +9 -0
- package/prompts/stages/pr-review-browser/active.md +6 -14
- package/prompts/stages/pr-review-browser/foreign.md +6 -14
- package/prompts/stages/pr-review-dynamic.md +6 -6
- package/prompts/stages/pr-review-terminal/active.md +7 -15
- package/prompts/stages/pr-review-terminal/foreign.md +7 -15
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/replan.md +3 -3
- package/shared/README.md +2 -2
- package/shared/bindings.yaml +15 -0
- package/shared/contracts-history.md +11 -0
- package/shared/contracts.md +5373 -281
- package/shared/providers.yaml +27 -60
- package/shared/registry.yaml +42 -18
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +487 -0
- package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
- package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
- package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
- package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
- package/shared/schemas/outputs/plan-save.schema.json +12 -0
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
- package/shared/schemas/outputs/pr-submit.schema.json +177 -1
- package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
- package/extension/checkpoints/checkpoints.ts +0 -550
- package/extension/checkpoints/planSteps.ts +0 -108
- package/extension/doors/askUser.ts +0 -198
- package/prompts/common/output-schemas/objective-explorer.md +0 -36
- package/prompts/common/output-schemas/review-classifier.md +0 -47
- package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
|
@@ -29,13 +29,26 @@
|
|
|
29
29
|
// is always explicit) / backend-unavailable all soft-skip so plan authoring never wedges — those
|
|
30
30
|
// arms keep the present-the-plan + human-`/plan-save` discipline (the manual failsafe).
|
|
31
31
|
//
|
|
32
|
+
// THE LAUNCH CHOOSER (plannotator arms only, §8.23): on an eligible round (injected `WaveLaunch`
|
|
33
|
+
// deps present, plannotator loaded, the review source a validated draft artifact) the tool first
|
|
34
|
+
// asks the human — "Browser review + reviewer wave" vs "Browser review only" — before anything
|
|
35
|
+
// launches. The wave choice collects an optional trimmed custom angle, delegates to the door's
|
|
36
|
+
// guidance-returning open core (openPlanReviewSurface / openObjectiveReviewSurface, injected —
|
|
37
|
+
// never imported: doors value-import this module), and returns the NON-terminating
|
|
38
|
+
// `wave_launched` result carrying the door guidance verbatim; the browser decision then routes
|
|
39
|
+
// through the door's background decision task. Esc ⇒ the plain flavor (never a cancel); abort
|
|
40
|
+
// outranks every dialog result AND the awaited opener (an interrupted turn never reports a
|
|
41
|
+
// launched wave); a null opener return (synchronous port-pick failure, loudly reported in the
|
|
42
|
+
// core) falls open to the plain blocking review in the same call.
|
|
43
|
+
//
|
|
32
44
|
// `ctx.ui.editor` takes NO AbortSignal (unlike select/confirm/input) — `signal?.aborted` is
|
|
33
45
|
// checked between dialogs; an in-flight editor dialog survives a turn abort and its result is
|
|
34
46
|
// discarded (the aborted arm wins). Enter submits in the editor dialog (Shift+Enter = newline),
|
|
35
47
|
// so the dialog titles carry the key hints — pi renders no other affordance.
|
|
36
48
|
//
|
|
37
|
-
// THE OBJECTIVE ARM: an objective-
|
|
38
|
-
// `objective-
|
|
49
|
+
// THE OBJECTIVE ARM: an objective-authoring session (read-only, stage `objective-author` or
|
|
50
|
+
// `objective-save` — the two stages whose working draft IS the objective draft; neither carries
|
|
51
|
+
// `plan_draft`) routes through `executeObjectiveReview` instead of the plan path — the
|
|
39
52
|
// reviewed bytes are the RENDERED objective draft (`readObjectiveDraft` + `renderObjectiveDraft`,
|
|
40
53
|
// objectiveDraft.ts — never raw JSON, never the `plan` param, never the transcript; no draft
|
|
41
54
|
// soft-skips with `reason: "no_objective_draft"`). Dispatch mirrors the plan path (plannotator
|
|
@@ -49,7 +62,11 @@
|
|
|
49
62
|
// `executeGistReview` the same way — the reviewed bytes are the RENDERED gist draft
|
|
50
63
|
// (`readGistDraft` + `renderGistDraft`, gistDraft.ts), first-party VIEW-ONLY, implement-here
|
|
51
64
|
// never offered, APPROVED → the `gistApprovalSave` seam (gistSave.ts), no draft soft-skips with
|
|
52
|
-
// `reason: "no_gist_draft"`.
|
|
65
|
+
// `reason: "no_gist_draft"`. Plannotator approve-with-Direct-Edits mirrors the objective arm:
|
|
66
|
+
// the save seam re-reads the STRUCTURED gist artifact, so rendered edits cannot be folded back
|
|
67
|
+
// mechanically — the arm SKIPS the save and returns one model-mediated revise round (fold each
|
|
68
|
+
// hunk into the matching `gist_draft` field — title heading → `title`, `Scope:` line → `scope`,
|
|
69
|
+
// prose → `prose` — then re-review to confirm).
|
|
53
70
|
//
|
|
54
71
|
// INVARIANTS HELD: never calls `setActiveTools`, never registers a `tool_call` handler, never
|
|
55
72
|
// restamps `cache.plan-ref.provider`. The door composes the gate AND the save EXCLUSIVELY
|
|
@@ -64,6 +81,7 @@ import {
|
|
|
64
81
|
isPlannotatorPlanSelected,
|
|
65
82
|
} from "../adapters/planAdapterPlannotator.ts";
|
|
66
83
|
import type { Result } from "../substrate/result.ts";
|
|
84
|
+
import { readSessionArtifact } from "../substrate/sessionData.ts";
|
|
67
85
|
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
68
86
|
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
69
87
|
import { applyUnifiedDiff } from "../substrate/unifiedDiff.ts";
|
|
@@ -73,9 +91,17 @@ import { readGistDraft, renderGistDraft } from "./gistDraft.ts";
|
|
|
73
91
|
import { type GistApprovalSaveOutcome, gistApprovalSave } from "./gistSave.ts";
|
|
74
92
|
import { implementHereExit, implementHereGuidance } from "./implementHere.ts";
|
|
75
93
|
import { OBJECTIVE_AUTHOR_STAGE } from "./objectiveAuthor.ts";
|
|
76
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
OBJECTIVE_DRAFT_ARTIFACT,
|
|
96
|
+
readObjectiveDraft,
|
|
97
|
+
renderObjectiveDraft,
|
|
98
|
+
} from "./objectiveDraft.ts";
|
|
77
99
|
import { readNodeClaim } from "./objectivePlan.ts";
|
|
78
|
-
import {
|
|
100
|
+
import {
|
|
101
|
+
OBJECTIVE_SAVE_STAGE,
|
|
102
|
+
type ObjectiveApprovalSaveOutcome,
|
|
103
|
+
objectiveApprovalSave,
|
|
104
|
+
} from "./objectiveSave.ts";
|
|
79
105
|
import { writePlanDraft } from "./planDraft.ts";
|
|
80
106
|
import { type ApprovalSaveOutcome, approvalSave, resolvePlanSource } from "./planSave.ts";
|
|
81
107
|
|
|
@@ -410,9 +436,148 @@ export function implementHereResult(
|
|
|
410
436
|
};
|
|
411
437
|
}
|
|
412
438
|
|
|
439
|
+
// ------------------------------------------------------ the plannotator Direct-Edits apply
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The shared plannotator APPROVE mechanical-apply path (contracts.md §8.23): inspect an
|
|
443
|
+
* APPROVED outcome's feedback for a `# Direct Edits` section and mechanically apply the
|
|
444
|
+
* reviewer's diff to the exact bytes reviewed (`basePlan`), writing the patched bytes back to
|
|
445
|
+
* the draft (reviewed bytes == artifact bytes == saved bytes). Consumed by `executePlanReview`'s
|
|
446
|
+
* plannotator arm AND the `/plan-review-browser` door — one apply path, byte-identical
|
|
447
|
+
* semantics:
|
|
448
|
+
*
|
|
449
|
+
* - only an `approved` outcome WITH feedback is inspected (anything else passes through
|
|
450
|
+
* verbatim);
|
|
451
|
+
* - a clean extract + apply + write-back swaps `reviewedPlan` to the patched bytes, sets
|
|
452
|
+
* `edited: true`, and strips the applied section from the returned outcome's feedback (only
|
|
453
|
+
* the annotation remainder survives — the applied diff must never render as "apply these
|
|
454
|
+
* exact changes" guidance);
|
|
455
|
+
* - a seen-but-unhonorable heading (or a failed apply / write-back) sets
|
|
456
|
+
* `directEditsFailed: true` with the plan left verbatim (the caller renders the loud warning;
|
|
457
|
+
* the diff stays in the surfaced feedback for a manual follow-up).
|
|
458
|
+
*/
|
|
459
|
+
export function applyPlannotatorDirectEdits(
|
|
460
|
+
pi: ExtensionAPI,
|
|
461
|
+
ctx: ExtensionContext,
|
|
462
|
+
outcome: Extract<ReviewOutcome, { status: "completed" }>,
|
|
463
|
+
basePlan: string,
|
|
464
|
+
): {
|
|
465
|
+
outcome: Extract<ReviewOutcome, { status: "completed" }>;
|
|
466
|
+
reviewedPlan: string;
|
|
467
|
+
edited: boolean;
|
|
468
|
+
directEditsFailed: boolean;
|
|
469
|
+
} {
|
|
470
|
+
if (!outcome.approved || outcome.feedback === undefined) {
|
|
471
|
+
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: false };
|
|
472
|
+
}
|
|
473
|
+
const section = extractDirectEdits(outcome.feedback);
|
|
474
|
+
if (section !== null) {
|
|
475
|
+
const patched = applyUnifiedDiff(basePlan, section.diff);
|
|
476
|
+
if (patched !== null && writePlanDraft(pi, ctx, patched).details.ok) {
|
|
477
|
+
return {
|
|
478
|
+
outcome: { ...outcome, feedback: section.remainder },
|
|
479
|
+
reviewedPlan: patched,
|
|
480
|
+
edited: true,
|
|
481
|
+
directEditsFailed: false,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: true };
|
|
485
|
+
}
|
|
486
|
+
if (hasDirectEditsHeading(outcome.feedback)) {
|
|
487
|
+
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: true };
|
|
488
|
+
}
|
|
489
|
+
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: false };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ------------------------------------------------------ the launch chooser (the wave arm)
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The injected wave-launch deps (composed in index.ts from the door exports — structural on
|
|
496
|
+
* purpose: this module imports NOTHING from door modules, avoiding the value-import cycle;
|
|
497
|
+
* `planReviewBrowser.ts` already value-imports this module). `present` is the plannotator
|
|
498
|
+
* presence probe (`plannotatorPresent(pi)` at the call site); `plan`/`objective` are the
|
|
499
|
+
* guidance-returning door open cores (`openPlanReviewSurface` / `openObjectiveReviewSurface`) —
|
|
500
|
+
* one open path, byte-identical door semantics (contracts.md §8.23). `null` from an opener is
|
|
501
|
+
* the synchronous port-pick failure (already loudly reported inside the core) — the caller
|
|
502
|
+
* falls open to the plain blocking review.
|
|
503
|
+
*/
|
|
504
|
+
export interface WaveLaunch {
|
|
505
|
+
present(): boolean;
|
|
506
|
+
plan(ctx: ExtensionContext, opts: { draft: string; custom?: string }): Promise<string | null>;
|
|
507
|
+
objective(
|
|
508
|
+
ctx: ExtensionContext,
|
|
509
|
+
opts: { rendered: string; artifactRaw: string; custom?: string },
|
|
510
|
+
): Promise<string | null>;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** The minimal structural `ctx.ui` subset the launch chooser needs (both dialogs signal-aware). */
|
|
514
|
+
export interface ReviewLaunchUI {
|
|
515
|
+
select(
|
|
516
|
+
title: string,
|
|
517
|
+
options: string[],
|
|
518
|
+
opts?: { signal?: AbortSignal },
|
|
519
|
+
): Promise<string | undefined>;
|
|
520
|
+
input(
|
|
521
|
+
title: string,
|
|
522
|
+
placeholder?: string,
|
|
523
|
+
opts?: { signal?: AbortSignal },
|
|
524
|
+
): Promise<string | undefined>;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The launch chooser's outcome: the review flavor (never a cancel), or the aborted turn. */
|
|
528
|
+
export type ReviewLaunchChoice =
|
|
529
|
+
| { launch: "plain" }
|
|
530
|
+
| { launch: "wave"; custom?: string }
|
|
531
|
+
| { launch: "aborted" };
|
|
532
|
+
|
|
533
|
+
const LAUNCH_WAVE = "Browser review + reviewer wave";
|
|
534
|
+
const LAUNCH_PLAIN = "Browser review only";
|
|
535
|
+
const CUSTOM_ANGLE_TITLE = "Custom review angle (optional — Enter to skip)";
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* The launch chooser (pure over the injected ui slice — offline-testable): every eligible
|
|
539
|
+
* plannotator round asks the human whether the browser review launches WITH the streamed
|
|
540
|
+
* reviewer wave; the wave choice then asks for an optional custom review angle. Esc/dismiss
|
|
541
|
+
* anywhere selects a FLAVOR, never cancels the review (Esc at the chooser ⇒ plain; Esc/blank at
|
|
542
|
+
* the angle input ⇒ wave with no custom lane — the input is `.trim()`'d before blank detection,
|
|
543
|
+
* the door handlers' exact discipline). ABORT OUTRANKS EVERYTHING: `signal?.aborted` is checked
|
|
544
|
+
* at entry and re-checked immediately after each awaited dialog, BEFORE interpreting its result
|
|
545
|
+
* (the `runFirstPartyReview` discipline) — a conforming caller can never launch a browser or
|
|
546
|
+
* enter a blocking review after the turn was interrupted. No other return paths exist.
|
|
547
|
+
*/
|
|
548
|
+
export async function chooseReviewLaunch(
|
|
549
|
+
ui: ReviewLaunchUI,
|
|
550
|
+
subjectNoun: string,
|
|
551
|
+
signal?: AbortSignal,
|
|
552
|
+
): Promise<ReviewLaunchChoice> {
|
|
553
|
+
if (signal?.aborted) return { launch: "aborted" };
|
|
554
|
+
const picked = await ui.select(`${subjectNoun} review launch`, [LAUNCH_WAVE, LAUNCH_PLAIN], {
|
|
555
|
+
signal,
|
|
556
|
+
});
|
|
557
|
+
if (signal?.aborted) return { launch: "aborted" }; // abort outranks Esc AND any selection
|
|
558
|
+
if (picked !== LAUNCH_WAVE) return { launch: "plain" }; // Esc/dismiss = the plain flavor
|
|
559
|
+
const raw = await ui.input(CUSTOM_ANGLE_TITLE, undefined, { signal });
|
|
560
|
+
if (signal?.aborted) return { launch: "aborted" }; // abort outranks the input result too
|
|
561
|
+
const custom = (raw ?? "").trim();
|
|
562
|
+
return custom.length > 0 ? { launch: "wave", custom } : { launch: "wave" };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* The NON-terminating wave-launched result: the door core's guidance rides back verbatim as the
|
|
567
|
+
* tool text (same templates, same binding suffix — the model behaves identically whether the
|
|
568
|
+
* human summoned the door or chose the wave inside `plan_review`), and the human's browser
|
|
569
|
+
* decision routes through the door's background decision task — never through this call.
|
|
570
|
+
*/
|
|
571
|
+
function waveLaunchedResult(subject: ReviewSubject, guidance: string): ToolResult {
|
|
572
|
+
return {
|
|
573
|
+
content: [{ type: "text", text: guidance }],
|
|
574
|
+
details: { ok: true, status: "wave_launched", ...subject.detailsExtra },
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
413
578
|
// ----------------------------------------------------------------- the first-party review core
|
|
414
579
|
|
|
415
|
-
/** The minimal structural `ctx.ui` subset the first-party review needs (the
|
|
580
|
+
/** The minimal structural `ctx.ui` subset the first-party review needs (the ciExecutor.ts pure-core + injected-fakes recipe). */
|
|
416
581
|
export interface PlanReviewUI {
|
|
417
582
|
editor(title: string, prefill?: string): Promise<string | undefined>;
|
|
418
583
|
select(
|
|
@@ -444,7 +609,7 @@ const REVIEW_EDITOR_TITLE =
|
|
|
444
609
|
const DENY_FEEDBACK_TITLE = "Deny feedback (optional) — Enter to send";
|
|
445
610
|
|
|
446
611
|
/**
|
|
447
|
-
* The first-party in-TUI review core, pure over injected seams (the
|
|
612
|
+
* The first-party in-TUI review core, pure over injected seams (the ciExecutor.ts pure-core + injected-fakes recipe) — fully
|
|
448
613
|
* offline-testable. Flow: (1) display the plan in the editor dialog (Esc = dismissed; the human
|
|
449
614
|
* may edit, incl. via Ctrl+G/$EDITOR); (2) a non-blank edit differing from the displayed plan is
|
|
450
615
|
* written back to the draft BEFORE the verdict (reviewed bytes == artifact bytes == saved bytes;
|
|
@@ -588,10 +753,19 @@ export async function executeObjectiveReview(
|
|
|
588
753
|
gating: ToolGating,
|
|
589
754
|
bridge: { review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> },
|
|
590
755
|
signal?: AbortSignal,
|
|
756
|
+
wave?: WaveLaunch,
|
|
591
757
|
): Promise<ToolResult> {
|
|
592
758
|
// 1. Headless → soft skip (fail-open; never wedges CI/supervisor runs on an interactive UI).
|
|
593
759
|
if (!ctx.hasUI) return skipResult();
|
|
594
|
-
// 2. The
|
|
760
|
+
// 2. The wave arm's stale-guard baseline is captured BEFORE the validated read below (the
|
|
761
|
+
// objective door's fail-closed ordering): the rendered bytes always derive from a read at
|
|
762
|
+
// or after this baseline, so a concurrent objective_draft write between the two reads makes
|
|
763
|
+
// the browsed render NEWER than the baseline and routeObjectiveReviewDecision's existing
|
|
764
|
+
// guard refuses the approval — the reverse order would fail open (approve unreviewed
|
|
765
|
+
// bytes). Raw artifact bytes on purpose: the save-authoritative surface catches
|
|
766
|
+
// render-invisible changes.
|
|
767
|
+
const baseline = readSessionArtifact(ctx, OBJECTIVE_DRAFT_ARTIFACT);
|
|
768
|
+
// 3. The draft artifact is the sole review source — no draft → soft skip with the
|
|
595
769
|
// objective_draft redirect.
|
|
596
770
|
const draft = readObjectiveDraft(ctx);
|
|
597
771
|
if (draft === null) {
|
|
@@ -613,13 +787,35 @@ export async function executeObjectiveReview(
|
|
|
613
787
|
},
|
|
614
788
|
};
|
|
615
789
|
}
|
|
616
|
-
//
|
|
790
|
+
// 4. The reviewed bytes are the RENDERED markdown (prose + roadmap table) — never raw JSON.
|
|
617
791
|
const rendered = renderObjectiveDraft(draft);
|
|
618
|
-
//
|
|
792
|
+
// 5. Backend dispatch (mirrors the plan path): plannotator-selected → the bridge; ANY other
|
|
619
793
|
// selection → the first-party editor, view-only.
|
|
620
794
|
const sig = signal ?? ctx.signal;
|
|
621
795
|
let outcome: ReviewOutcome;
|
|
622
796
|
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
797
|
+
// The launch chooser (contracts.md §8.23): every eligible round the human picks with/without
|
|
798
|
+
// the streamed reviewer wave BEFORE anything launches. Eligibility is drafts-only — the wave
|
|
799
|
+
// door stale-guards the raw artifact baseline, so a null baseline keeps the plain path
|
|
800
|
+
// (silently: there is no forced mode to warn about).
|
|
801
|
+
if (wave?.present() && baseline !== null) {
|
|
802
|
+
const choice = await chooseReviewLaunch(ctx.ui, "Objective", sig);
|
|
803
|
+
if (choice.launch === "aborted") return objectiveReviewOutcomeResult({ status: "aborted" });
|
|
804
|
+
if (choice.launch === "wave") {
|
|
805
|
+
const guidance = await wave.objective(ctx, {
|
|
806
|
+
rendered,
|
|
807
|
+
artifactRaw: baseline.content,
|
|
808
|
+
...(choice.custom !== undefined ? { custom: choice.custom } : {}),
|
|
809
|
+
});
|
|
810
|
+
// Abort outranks the opener result too: a turn interrupted during the awaited open must
|
|
811
|
+
// never report a successful launch (the door's own bridge abort handling settles the
|
|
812
|
+
// background tasks and clears the primed surfaces).
|
|
813
|
+
if (sig?.aborted) return objectiveReviewOutcomeResult({ status: "aborted" });
|
|
814
|
+
if (guidance !== null) return waveLaunchedResult(OBJECTIVE_SUBJECT, guidance);
|
|
815
|
+
// null = the synchronous port-pick failure (already loudly reported inside the core) —
|
|
816
|
+
// fall open to the plain blocking review in the same call: the review never wedges.
|
|
817
|
+
}
|
|
818
|
+
}
|
|
623
819
|
outcome = await bridge.review(rendered, sig);
|
|
624
820
|
// APPROVE + Direct Edits (browser edits of the RENDERED markdown), checked BEFORE the
|
|
625
821
|
// approved-save routing (the approved-first discipline): the save seam re-reads the
|
|
@@ -669,7 +865,7 @@ export async function executeObjectiveReview(
|
|
|
669
865
|
});
|
|
670
866
|
outcome = fp.outcome;
|
|
671
867
|
}
|
|
672
|
-
//
|
|
868
|
+
// 6. An APPROVED decision (either backend) wires into the objectiveApprovalSave seam (the
|
|
673
869
|
// STRUCTURED artifact is re-read at save time — never the rendered bytes; auto-save → D1a
|
|
674
870
|
// gate exit → terminating result); everything else maps via objectiveReviewOutcomeResult.
|
|
675
871
|
// Approved-first routing: objectiveReviewOutcomeResult's completed case renders DENIED.
|
|
@@ -721,7 +917,12 @@ export function approvedGistSaveResult(
|
|
|
721
917
|
* run VIEW-ONLY (edits are never written back; deny+feedback is the change channel); the
|
|
722
918
|
* implement-here verdict is never offered (a gist is not implementable — it has no strategy).
|
|
723
919
|
* An APPROVED outcome wires into the `gistApprovalSave` seam (re-read the artifact → `saveGist`
|
|
724
|
-
* → D1a gate exit → terminating); every other outcome maps via `gistReviewOutcomeResult`.
|
|
920
|
+
* → D1a gate exit → terminating); every other outcome maps via `gistReviewOutcomeResult`. ONE
|
|
921
|
+
* carve-out (plannotator only, mirroring the objective arm): an approval whose feedback opens a
|
|
922
|
+
* Direct Edits section SKIPS the save — rendered edits cannot be folded back into the
|
|
923
|
+
* structured draft mechanically — and returns a NON-terminating revise round with the gate
|
|
924
|
+
* untouched (fold each hunk into the matching `gist_draft` field, re-review to confirm); perk
|
|
925
|
+
* never saves a gist the reviewer explicitly edited away from.
|
|
725
926
|
*/
|
|
726
927
|
export async function executeGistReview(
|
|
727
928
|
pi: ExtensionAPI,
|
|
@@ -762,6 +963,42 @@ export async function executeGistReview(
|
|
|
762
963
|
let outcome: ReviewOutcome;
|
|
763
964
|
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
764
965
|
outcome = await bridge.review(rendered, sig);
|
|
966
|
+
// APPROVE + Direct Edits (browser edits of the RENDERED markdown), checked BEFORE the
|
|
967
|
+
// approved-save routing (the approved-first discipline; mirrors the objective arm): the
|
|
968
|
+
// save seam re-reads the STRUCTURED artifact, so rendered edits cannot be folded back
|
|
969
|
+
// without model judgment. Skip the save, keep the gate read-only, and route ONE revise
|
|
970
|
+
// round: the model folds each hunk into the matching `gist_draft` field, then re-reviews
|
|
971
|
+
// to confirm. The heading check suffices (extraction success is irrelevant here — the diff
|
|
972
|
+
// goes to the model verbatim either way).
|
|
973
|
+
if (
|
|
974
|
+
outcome.status === "completed" &&
|
|
975
|
+
outcome.approved &&
|
|
976
|
+
outcome.feedback !== undefined &&
|
|
977
|
+
hasDirectEditsHeading(outcome.feedback)
|
|
978
|
+
) {
|
|
979
|
+
return {
|
|
980
|
+
content: [
|
|
981
|
+
{
|
|
982
|
+
type: "text",
|
|
983
|
+
text:
|
|
984
|
+
"gist APPROVED with direct browser edits — these cannot be auto-applied to the " +
|
|
985
|
+
"structured draft, so nothing was saved. Fold each Direct Edits hunk below into " +
|
|
986
|
+
"the matching gist_draft field (a `# <title>` heading hunk → title, a `Scope:` " +
|
|
987
|
+
"line hunk → scope, prose hunks → prose), then call plan_review again to " +
|
|
988
|
+
`confirm.\n\nReviewer feedback:\n${outcome.feedback}`,
|
|
989
|
+
},
|
|
990
|
+
],
|
|
991
|
+
details: {
|
|
992
|
+
ok: true,
|
|
993
|
+
status: "revise",
|
|
994
|
+
reason: "direct_edits",
|
|
995
|
+
approved: true,
|
|
996
|
+
feedback: outcome.feedback,
|
|
997
|
+
reviewId: outcome.reviewId,
|
|
998
|
+
subject: "gist",
|
|
999
|
+
},
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
765
1002
|
} else {
|
|
766
1003
|
const fp = await runFirstPartyReview({
|
|
767
1004
|
ui: ctx.ui,
|
|
@@ -790,8 +1027,9 @@ export async function executeGistReview(
|
|
|
790
1027
|
/**
|
|
791
1028
|
* The `plan_review` execute core, extracted pure-over-its-seams (the bridge, the gating, the
|
|
792
1029
|
* ctx) so the resolution + dispatch + approved-save paths are unit-testable offline. Arm order:
|
|
793
|
-
* param decode → the objective
|
|
794
|
-
*
|
|
1030
|
+
* param decode → the objective arm (`executeObjectiveReview` — the rendered objective draft is
|
|
1031
|
+
* the review subject in BOTH objective-authoring stages) → headless skip → file-first
|
|
1032
|
+
* resolution → backend
|
|
795
1033
|
* dispatch (plannotator-selected → the event-bus bridge; ANY other selection → the first-party
|
|
796
1034
|
* in-TUI editor review) → approved → `approvalSave`.
|
|
797
1035
|
*/
|
|
@@ -802,6 +1040,7 @@ export async function executePlanReview(
|
|
|
802
1040
|
bridge: { review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> },
|
|
803
1041
|
params: unknown,
|
|
804
1042
|
signal?: AbortSignal,
|
|
1043
|
+
wave?: WaveLaunch,
|
|
805
1044
|
): Promise<ToolResult> {
|
|
806
1045
|
// Tool-boundary decode, in this tool's native fail-open vocabulary: a MISTYPED
|
|
807
1046
|
// `plan` (or non-object params) skip-shapes (`reason: "bad_input"`) without reviewing; an
|
|
@@ -825,12 +1064,15 @@ export async function executePlanReview(
|
|
|
825
1064
|
},
|
|
826
1065
|
};
|
|
827
1066
|
}
|
|
828
|
-
// 1.
|
|
829
|
-
//
|
|
830
|
-
//
|
|
1067
|
+
// 1. An objective-authoring session (objective-author OR objective-save — both stages'
|
|
1068
|
+
// working draft is the objective draft, and neither carries `plan_draft`) → the objective
|
|
1069
|
+
// review arm: the rendered objective draft is the sole review source; a well-typed `plan`
|
|
1070
|
+
// param is ignored here — the plan-arm fallthrough could otherwise review/save an
|
|
1071
|
+
// unrelated plan param from an objective session. A gist-author session likewise routes to
|
|
1072
|
+
// the gist arm (the rendered gist draft).
|
|
831
1073
|
const launchedStage = rebuildWorkflowState(branchOf(ctx)).stage;
|
|
832
|
-
if (launchedStage === OBJECTIVE_AUTHOR_STAGE) {
|
|
833
|
-
return executeObjectiveReview(pi, ctx, gating, bridge, signal ?? ctx.signal);
|
|
1074
|
+
if (launchedStage === OBJECTIVE_AUTHOR_STAGE || launchedStage === OBJECTIVE_SAVE_STAGE) {
|
|
1075
|
+
return executeObjectiveReview(pi, ctx, gating, bridge, signal ?? ctx.signal, wave);
|
|
834
1076
|
}
|
|
835
1077
|
if (launchedStage === GIST_AUTHOR_STAGE) {
|
|
836
1078
|
return executeGistReview(pi, ctx, gating, bridge, signal ?? ctx.signal);
|
|
@@ -867,31 +1109,41 @@ export async function executePlanReview(
|
|
|
867
1109
|
let edited = false;
|
|
868
1110
|
let directEditsFailed = false;
|
|
869
1111
|
if (isPlannotatorPlanSelected(ctx.cwd)) {
|
|
1112
|
+
// The launch chooser (contracts.md §8.23): every eligible round the human picks with/without
|
|
1113
|
+
// the streamed reviewer wave BEFORE anything launches. Eligibility is drafts-only — the wave
|
|
1114
|
+
// door reviews and stale-guards the validated artifact, so a param-tier source keeps the
|
|
1115
|
+
// plain path (silently: there is no forced mode to warn about; the `wave === undefined` arm
|
|
1116
|
+
// is defensive/test-only and behaves identically).
|
|
1117
|
+
if (wave?.present() && src.source === "plan-draft") {
|
|
1118
|
+
const choice = await chooseReviewLaunch(ctx.ui, "Plan", sig);
|
|
1119
|
+
if (choice.launch === "aborted") return reviewOutcomeResult({ status: "aborted" });
|
|
1120
|
+
if (choice.launch === "wave") {
|
|
1121
|
+
const guidance = await wave.plan(ctx, {
|
|
1122
|
+
draft: src.plan,
|
|
1123
|
+
...(choice.custom !== undefined ? { custom: choice.custom } : {}),
|
|
1124
|
+
});
|
|
1125
|
+
// Abort outranks the opener result too: a turn interrupted during the awaited open must
|
|
1126
|
+
// never report a successful launch (the door's own bridge abort handling settles the
|
|
1127
|
+
// background tasks and clears the primed surfaces).
|
|
1128
|
+
if (sig?.aborted) return reviewOutcomeResult({ status: "aborted" });
|
|
1129
|
+
if (guidance !== null) return waveLaunchedResult(PLAN_SUBJECT, guidance);
|
|
1130
|
+
// null = the synchronous port-pick failure (already loudly reported inside the core) —
|
|
1131
|
+
// fall open to the plain blocking review in the same call: the review never wedges.
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
870
1134
|
outcome = await bridge.review(src.plan, sig);
|
|
871
1135
|
// APPROVE + Direct Edits (browser plan edits, contracts.md §8.23): mechanically apply the
|
|
872
|
-
// reviewer's diff
|
|
873
|
-
//
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (patched !== null && writePlanDraft(pi, ctx, patched).details.ok) {
|
|
884
|
-
reviewedPlan = patched;
|
|
885
|
-
edited = true;
|
|
886
|
-
// The applied diff must NOT survive into the result as "apply these exact changes"
|
|
887
|
-
// guidance — only the annotation remainder (when any) stays reviewer feedback.
|
|
888
|
-
outcome = { ...outcome, feedback: section.remainder };
|
|
889
|
-
} else {
|
|
890
|
-
directEditsFailed = true;
|
|
891
|
-
}
|
|
892
|
-
} else if (hasDirectEditsHeading(outcome.feedback)) {
|
|
893
|
-
directEditsFailed = true;
|
|
894
|
-
}
|
|
1136
|
+
// reviewer's diff via the shared helper (the first-party pre-verdict write-back, replayed
|
|
1137
|
+
// here post-verdict because the bridge only reports the diff), and save the EDITED bytes.
|
|
1138
|
+
// Every rung fails open to the verbatim path (never save bytes the artifact doesn't carry).
|
|
1139
|
+
// DENY stays model-mediated — the feedback (diff included) passes through for the
|
|
1140
|
+
// plan_draft rewrite.
|
|
1141
|
+
if (outcome.status === "completed") {
|
|
1142
|
+
const applied = applyPlannotatorDirectEdits(pi, ctx, outcome, src.plan);
|
|
1143
|
+
outcome = applied.outcome;
|
|
1144
|
+
reviewedPlan = applied.reviewedPlan;
|
|
1145
|
+
edited = applied.edited;
|
|
1146
|
+
directEditsFailed = applied.directEditsFailed;
|
|
895
1147
|
}
|
|
896
1148
|
} else {
|
|
897
1149
|
// The 4th verdict (implement-here, the no-save exit) is offered UNLESS this is an
|
|
@@ -937,8 +1189,10 @@ export async function executePlanReview(
|
|
|
937
1189
|
* Register `plan_review` — perk's universal review door. In READ_ONLY_TOOLS so it is callable
|
|
938
1190
|
* INSIDE plan mode (the whole point — review happens before the gate ever comes off). Fail-open
|
|
939
1191
|
* everywhere: headless / dismissed / backend-unavailable all soft-skip so authoring never wedges.
|
|
1192
|
+
* `wave` is the injected wave-launch deps (index.ts composes them from the door open cores);
|
|
1193
|
+
* absent ⇒ the chooser never appears and every path is byte-stable.
|
|
940
1194
|
*/
|
|
941
|
-
export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
1195
|
+
export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating, wave?: WaveLaunch): void {
|
|
942
1196
|
const bridge = createPlannotatorBridge(pi.events);
|
|
943
1197
|
|
|
944
1198
|
pi.registerTool({
|
|
@@ -949,14 +1203,18 @@ export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
|
949
1203
|
"selected, otherwise perk's in-TUI editor review — and wait for the human decision. " +
|
|
950
1204
|
"Reviews the validated plan-draft artifact (keep it current with plan_draft); on approval " +
|
|
951
1205
|
"the plan is auto-saved and the turn terminates. On deny, revise per the returned " +
|
|
952
|
-
"feedback, rewrite the draft with plan_draft, and call again.
|
|
953
|
-
"
|
|
1206
|
+
"feedback, rewrite the draft with plan_draft, and call again. On the Plannotator surface " +
|
|
1207
|
+
"the human may first opt into a streamed reviewer wave — the call then returns immediately " +
|
|
1208
|
+
'with wave guidance (status "wave_launched") to follow in the same turn, and the browser ' +
|
|
1209
|
+
"decision routes back automatically. No-op skip when the session is headless or the " +
|
|
1210
|
+
"review is dismissed.",
|
|
954
1211
|
promptSnippet: "Request a human review of the working plan draft",
|
|
955
1212
|
promptGuidelines: [
|
|
956
1213
|
"Keep the working draft current with plan_draft — the validated plan-draft artifact is what plan_review reviews AND auto-saves; the plan param is only a fallback when no draft exists.",
|
|
957
1214
|
"Call plan_review only when the plan is decision-complete.",
|
|
958
1215
|
"On a DENIED review, revise per the feedback, rewrite the draft with plan_draft, then call plan_review again.",
|
|
959
1216
|
"On an APPROVED plan_review, the plan is auto-saved and the turn ends — never re-dump the plan as a final message and never tell the user to run /plan-save; relay the save outcome instead.",
|
|
1217
|
+
"On a wave_launched result (the human opted into the reviewer wave), follow the returned guidance in the same turn — launch the wave and relay its findings; the human's browser decision routes back automatically, so never re-call plan_review while that browser review is open.",
|
|
960
1218
|
"If plan_review reports it was skipped or unavailable (headless, dismissed), fall back to presenting the complete plan; the human runs /plan-save (the manual failsafe).",
|
|
961
1219
|
],
|
|
962
1220
|
executionMode: "sequential",
|
|
@@ -973,7 +1231,7 @@ export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
|
973
1231
|
},
|
|
974
1232
|
},
|
|
975
1233
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
976
|
-
return executePlanReview(pi, ctx, gating, bridge, params, signal);
|
|
1234
|
+
return executePlanReview(pi, ctx, gating, bridge, params, signal, wave);
|
|
977
1235
|
},
|
|
978
1236
|
});
|
|
979
1237
|
}
|
|
@@ -92,16 +92,35 @@ export async function generatePlanTitle(
|
|
|
92
92
|
): Promise<string | null> {
|
|
93
93
|
if (!llmTitlesEnabled()) return null;
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const model = ctx.model;
|
|
96
|
+
if (!model) return null; // silent: no model configured (fail-safe).
|
|
97
|
+
|
|
98
|
+
// Primary path (pi ≥ 0.84): registry dispatch — pi owns final request assembly (resolved auth,
|
|
99
|
+
// nullable headers, credential-resolved baseUrl, provider env). Feature-detected: the method is
|
|
100
|
+
// absent on older hosts, where the widened resolveModelAuth fallback keeps the old shape.
|
|
101
|
+
const registry = ctx.modelRegistry;
|
|
102
|
+
const registryComplete = registry.complete;
|
|
103
|
+
let authOptions: Pick<
|
|
104
|
+
Parameters<typeof completeStructured>[0],
|
|
105
|
+
"dispatch" | "apiKey" | "headers" | "env"
|
|
106
|
+
>;
|
|
107
|
+
if (typeof registryComplete === "function") {
|
|
108
|
+
authOptions = {
|
|
109
|
+
dispatch: (m, context, options) => registryComplete.call(registry, m, context, options),
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
let auth: Awaited<ReturnType<typeof resolveModelAuth>>;
|
|
113
|
+
try {
|
|
114
|
+
auth = await resolveModelAuth(ctx);
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (!auth.ok) return null; // silent: no auth configured (fail-safe).
|
|
119
|
+
authOptions = { apiKey: auth.apiKey, headers: auth.headers, env: auth.env };
|
|
100
120
|
}
|
|
101
|
-
if (!auth.ok) return null; // silent: no model / no auth configured (fail-safe).
|
|
102
121
|
|
|
103
122
|
const outcome = await completeStructured({
|
|
104
|
-
model
|
|
123
|
+
model,
|
|
105
124
|
schema: PlanTitleSchema,
|
|
106
125
|
toolName: "set_plan_title",
|
|
107
126
|
toolDescription: "Provide the chosen title and category for the plan.",
|
|
@@ -110,8 +129,7 @@ export async function generatePlanTitle(
|
|
|
110
129
|
"Read this implementation plan and choose a title and category. The title must be a concise, " +
|
|
111
130
|
"imperative phrase, at most ~70 characters, with no trailing period and no markdown.",
|
|
112
131
|
input: planMarkdown.slice(0, TITLE_INPUT_CHAR_CAP),
|
|
113
|
-
|
|
114
|
-
headers: auth.headers,
|
|
132
|
+
...authOptions,
|
|
115
133
|
signal,
|
|
116
134
|
});
|
|
117
135
|
if (!outcome.ok || !outcome.value) {
|