@mgiles/perk 3.0.0 → 3.2.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/extension/adapters/planAdapterPlannotator.ts +12 -9
- package/extension/doors/address.ts +11 -0
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +43 -15
- package/extension/doors/dreamWaveTools.ts +489 -0
- package/extension/doors/land.ts +6 -0
- package/extension/doors/learn.ts +16 -3
- package/extension/doors/lifecycleGates.ts +36 -1
- package/extension/doors/objectiveReviewBrowser.ts +36 -13
- package/extension/doors/objectiveStack.ts +424 -24
- package/extension/doors/planReviewBrowser.ts +30 -8
- package/extension/doors/plannotatorHandoff.ts +80 -8
- package/extension/doors/prReview.ts +158 -50
- package/extension/doors/prReviewBrowser.ts +75 -27
- package/extension/doors/prReviewDynamic.ts +33 -13
- package/extension/doors/ready.ts +209 -17
- package/extension/doors/reviewWaveTools.ts +61 -17
- package/extension/doors/stackReviewBrowser.ts +573 -0
- package/extension/doors/submit.ts +36 -10
- package/extension/doors/submitPrReview.ts +116 -19
- package/extension/factories/objectiveDraft.ts +95 -27
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectivePlan.ts +12 -6
- package/extension/factories/objectiveSave.ts +77 -1
- package/extension/factories/planReview.ts +173 -10
- package/extension/index.ts +88 -16
- package/extension/substrate/agentScratch.ts +171 -0
- package/extension/substrate/bindingDelivery.ts +9 -11
- package/extension/substrate/cache.ts +92 -2
- package/extension/substrate/command.ts +9 -6
- package/extension/substrate/config.ts +10 -3
- package/extension/substrate/git.ts +85 -2
- package/extension/substrate/paths.ts +2 -7
- package/extension/substrate/resolverLease.ts +363 -0
- package/extension/substrate/result.ts +3 -2
- package/extension/substrate/sessionData.ts +6 -4
- package/extension/substrate/sessionPointers.ts +3 -4
- package/extension/substrate/toolGating.ts +25 -0
- package/extension/substrate/workflowState.ts +57 -5
- package/extension/surfaces/report.ts +38 -12
- package/extension/surfaces/surfaces.ts +129 -7
- package/extension/vendor/btw/btw.ts +38 -6
- package/extension/waves/adversarialReviewWave.ts +34 -3
- package/extension/waves/draftReviewWave.ts +17 -1
- 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 +1 -1
- package/extension/waves/ponytail.ts +104 -0
- package/extension/waves/prReviewDynamicWave.ts +115 -34
- package/extension/waves/prReviewWave.ts +122 -17
- package/extension/waves/reportWave.ts +103 -7
- package/extension/worker/readOnlySession.ts +2 -3
- package/package.json +6 -3
- package/prompts/_fixtures/live.yaml +112 -0
- package/prompts/commit-and-compact-continuation.md +13 -0
- package/prompts/contexts/adapters/plannotator-objective.md +7 -1
- package/prompts/contexts/adapters/plannotator-plan.md +7 -1
- package/prompts/contexts/adapters/tombell-plan.md +4 -0
- package/prompts/contexts/plan-authoring.md +6 -5
- package/prompts/stages/conflict-resolution-continuation.md +6 -0
- package/prompts/stages/conflict-resolution.md +2 -2
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/objective-author/adopt.md +1 -1
- package/prompts/stages/objective-author/file.md +1 -1
- package/prompts/stages/objective-author/seed.md +1 -1
- package/prompts/stages/objective-reconcile-ready.md +7 -0
- package/prompts/stages/objective-review-browser.md +1 -1
- package/prompts/stages/objective-sync.md +1 -1
- package/prompts/stages/plan-review-browser.md +1 -1
- package/prompts/stages/pr-review-browser/active.md +1 -1
- package/prompts/stages/pr-review-browser/foreign.md +1 -1
- package/prompts/stages/pr-review-dynamic.md +5 -5
- package/prompts/stages/pr-review-terminal/active.md +1 -1
- package/prompts/stages/pr-review-terminal/foreign.md +1 -1
- package/prompts/stages/pr-review-terminal/local.md +1 -1
- package/prompts/stages/pr-review.md +5 -5
- package/prompts/stages/stack-review/cold.md +1 -0
- package/prompts/stages/stack-review-browser/stack.md +23 -0
- package/shared/README.md +0 -3
- package/shared/bindings.yaml +6 -0
- package/shared/contracts.md +3784 -1851
- package/shared/registry.yaml +28 -13
- package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
- package/shared/schemas/outputs/objective-doctor.schema.json +39 -1
- package/shared/schemas/outputs/objective-stack-status.schema.json +172 -1
- package/shared/schemas/outputs/pr-land.schema.json +3 -3
- package/shared/schemas/outputs/pr-ready.schema.json +110 -2
- package/shared/contracts-history.md +0 -605
|
@@ -29,6 +29,18 @@
|
|
|
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),
|
|
@@ -69,6 +81,7 @@ import {
|
|
|
69
81
|
isPlannotatorPlanSelected,
|
|
70
82
|
} from "../adapters/planAdapterPlannotator.ts";
|
|
71
83
|
import type { Result } from "../substrate/result.ts";
|
|
84
|
+
import { readSessionArtifact } from "../substrate/sessionData.ts";
|
|
72
85
|
import type { ToolGating } from "../substrate/toolGating.ts";
|
|
73
86
|
import { paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
74
87
|
import { applyUnifiedDiff } from "../substrate/unifiedDiff.ts";
|
|
@@ -78,7 +91,11 @@ import { readGistDraft, renderGistDraft } from "./gistDraft.ts";
|
|
|
78
91
|
import { type GistApprovalSaveOutcome, gistApprovalSave } from "./gistSave.ts";
|
|
79
92
|
import { implementHereExit, implementHereGuidance } from "./implementHere.ts";
|
|
80
93
|
import { OBJECTIVE_AUTHOR_STAGE } from "./objectiveAuthor.ts";
|
|
81
|
-
import {
|
|
94
|
+
import {
|
|
95
|
+
OBJECTIVE_DRAFT_ARTIFACT,
|
|
96
|
+
readObjectiveDraft,
|
|
97
|
+
renderObjectiveDraft,
|
|
98
|
+
} from "./objectiveDraft.ts";
|
|
82
99
|
import { readNodeClaim } from "./objectivePlan.ts";
|
|
83
100
|
import {
|
|
84
101
|
OBJECTIVE_SAVE_STAGE,
|
|
@@ -472,6 +489,92 @@ export function applyPlannotatorDirectEdits(
|
|
|
472
489
|
return { outcome, reviewedPlan: basePlan, edited: false, directEditsFailed: false };
|
|
473
490
|
}
|
|
474
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
|
+
|
|
475
578
|
// ----------------------------------------------------------------- the first-party review core
|
|
476
579
|
|
|
477
580
|
/** The minimal structural `ctx.ui` subset the first-party review needs (the ciExecutor.ts pure-core + injected-fakes recipe). */
|
|
@@ -650,10 +753,19 @@ export async function executeObjectiveReview(
|
|
|
650
753
|
gating: ToolGating,
|
|
651
754
|
bridge: { review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> },
|
|
652
755
|
signal?: AbortSignal,
|
|
756
|
+
wave?: WaveLaunch,
|
|
653
757
|
): Promise<ToolResult> {
|
|
654
758
|
// 1. Headless → soft skip (fail-open; never wedges CI/supervisor runs on an interactive UI).
|
|
655
759
|
if (!ctx.hasUI) return skipResult();
|
|
656
|
-
// 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
|
|
657
769
|
// objective_draft redirect.
|
|
658
770
|
const draft = readObjectiveDraft(ctx);
|
|
659
771
|
if (draft === null) {
|
|
@@ -675,13 +787,35 @@ export async function executeObjectiveReview(
|
|
|
675
787
|
},
|
|
676
788
|
};
|
|
677
789
|
}
|
|
678
|
-
//
|
|
790
|
+
// 4. The reviewed bytes are the RENDERED markdown (prose + roadmap table) — never raw JSON.
|
|
679
791
|
const rendered = renderObjectiveDraft(draft);
|
|
680
|
-
//
|
|
792
|
+
// 5. Backend dispatch (mirrors the plan path): plannotator-selected → the bridge; ANY other
|
|
681
793
|
// selection → the first-party editor, view-only.
|
|
682
794
|
const sig = signal ?? ctx.signal;
|
|
683
795
|
let outcome: ReviewOutcome;
|
|
684
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
|
+
}
|
|
685
819
|
outcome = await bridge.review(rendered, sig);
|
|
686
820
|
// APPROVE + Direct Edits (browser edits of the RENDERED markdown), checked BEFORE the
|
|
687
821
|
// approved-save routing (the approved-first discipline): the save seam re-reads the
|
|
@@ -731,7 +865,7 @@ export async function executeObjectiveReview(
|
|
|
731
865
|
});
|
|
732
866
|
outcome = fp.outcome;
|
|
733
867
|
}
|
|
734
|
-
//
|
|
868
|
+
// 6. An APPROVED decision (either backend) wires into the objectiveApprovalSave seam (the
|
|
735
869
|
// STRUCTURED artifact is re-read at save time — never the rendered bytes; auto-save → D1a
|
|
736
870
|
// gate exit → terminating result); everything else maps via objectiveReviewOutcomeResult.
|
|
737
871
|
// Approved-first routing: objectiveReviewOutcomeResult's completed case renders DENIED.
|
|
@@ -906,6 +1040,7 @@ export async function executePlanReview(
|
|
|
906
1040
|
bridge: { review(plan: string, signal?: AbortSignal): Promise<ReviewOutcome> },
|
|
907
1041
|
params: unknown,
|
|
908
1042
|
signal?: AbortSignal,
|
|
1043
|
+
wave?: WaveLaunch,
|
|
909
1044
|
): Promise<ToolResult> {
|
|
910
1045
|
// Tool-boundary decode, in this tool's native fail-open vocabulary: a MISTYPED
|
|
911
1046
|
// `plan` (or non-object params) skip-shapes (`reason: "bad_input"`) without reviewing; an
|
|
@@ -937,7 +1072,7 @@ export async function executePlanReview(
|
|
|
937
1072
|
// the gist arm (the rendered gist draft).
|
|
938
1073
|
const launchedStage = rebuildWorkflowState(branchOf(ctx)).stage;
|
|
939
1074
|
if (launchedStage === OBJECTIVE_AUTHOR_STAGE || launchedStage === OBJECTIVE_SAVE_STAGE) {
|
|
940
|
-
return executeObjectiveReview(pi, ctx, gating, bridge, signal ?? ctx.signal);
|
|
1075
|
+
return executeObjectiveReview(pi, ctx, gating, bridge, signal ?? ctx.signal, wave);
|
|
941
1076
|
}
|
|
942
1077
|
if (launchedStage === GIST_AUTHOR_STAGE) {
|
|
943
1078
|
return executeGistReview(pi, ctx, gating, bridge, signal ?? ctx.signal);
|
|
@@ -974,6 +1109,28 @@ export async function executePlanReview(
|
|
|
974
1109
|
let edited = false;
|
|
975
1110
|
let directEditsFailed = false;
|
|
976
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
|
+
}
|
|
977
1134
|
outcome = await bridge.review(src.plan, sig);
|
|
978
1135
|
// APPROVE + Direct Edits (browser plan edits, contracts.md §8.23): mechanically apply the
|
|
979
1136
|
// reviewer's diff via the shared helper (the first-party pre-verdict write-back, replayed
|
|
@@ -1032,8 +1189,10 @@ export async function executePlanReview(
|
|
|
1032
1189
|
* Register `plan_review` — perk's universal review door. In READ_ONLY_TOOLS so it is callable
|
|
1033
1190
|
* INSIDE plan mode (the whole point — review happens before the gate ever comes off). Fail-open
|
|
1034
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.
|
|
1035
1194
|
*/
|
|
1036
|
-
export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
1195
|
+
export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating, wave?: WaveLaunch): void {
|
|
1037
1196
|
const bridge = createPlannotatorBridge(pi.events);
|
|
1038
1197
|
|
|
1039
1198
|
pi.registerTool({
|
|
@@ -1044,14 +1203,18 @@ export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
|
1044
1203
|
"selected, otherwise perk's in-TUI editor review — and wait for the human decision. " +
|
|
1045
1204
|
"Reviews the validated plan-draft artifact (keep it current with plan_draft); on approval " +
|
|
1046
1205
|
"the plan is auto-saved and the turn terminates. On deny, revise per the returned " +
|
|
1047
|
-
"feedback, rewrite the draft with plan_draft, and call again.
|
|
1048
|
-
"
|
|
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.",
|
|
1049
1211
|
promptSnippet: "Request a human review of the working plan draft",
|
|
1050
1212
|
promptGuidelines: [
|
|
1051
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.",
|
|
1052
1214
|
"Call plan_review only when the plan is decision-complete.",
|
|
1053
1215
|
"On a DENIED review, revise per the feedback, rewrite the draft with plan_draft, then call plan_review again.",
|
|
1054
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.",
|
|
1055
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).",
|
|
1056
1219
|
],
|
|
1057
1220
|
executionMode: "sequential",
|
|
@@ -1068,7 +1231,7 @@ export function registerPlanReview(pi: ExtensionAPI, gating: ToolGating): void {
|
|
|
1068
1231
|
},
|
|
1069
1232
|
},
|
|
1070
1233
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
1071
|
-
return executePlanReview(pi, ctx, gating, bridge, params, signal);
|
|
1234
|
+
return executePlanReview(pi, ctx, gating, bridge, params, signal, wave);
|
|
1072
1235
|
},
|
|
1073
1236
|
});
|
|
1074
1237
|
}
|
package/extension/index.ts
CHANGED
|
@@ -15,14 +15,19 @@ import { registerAuditWave } from "./doors/auditWaveTools.ts";
|
|
|
15
15
|
import { registerCiExecutor } from "./doors/ciExecutor.ts";
|
|
16
16
|
import { registerCommitAndCompact } from "./doors/commitCompact.ts";
|
|
17
17
|
import { registerDraftReviewWaveTools } from "./doors/draftReviewWaveTools.ts";
|
|
18
|
+
import { registerDreamWave } from "./doors/dreamWaveTools.ts";
|
|
18
19
|
import { registerHarvestWave } from "./doors/harvestWaveTools.ts";
|
|
19
20
|
import { registerLand } from "./doors/land.ts";
|
|
20
21
|
import { registerLearn } from "./doors/learn.ts";
|
|
21
22
|
import { CODE_DOOR, DOCS_DOOR, registerLearnFactoryDoor } from "./doors/learnFactory.ts";
|
|
22
23
|
import { registerLifecycleGates } from "./doors/lifecycleGates.ts";
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
openObjectiveReviewSurface,
|
|
26
|
+
registerObjectiveReviewBrowser,
|
|
27
|
+
} from "./doors/objectiveReviewBrowser.ts";
|
|
24
28
|
import { registerObjectiveStack } from "./doors/objectiveStack.ts";
|
|
25
|
-
import {
|
|
29
|
+
import { plannotatorPresent } from "./doors/plannotatorHandoff.ts";
|
|
30
|
+
import { openPlanReviewSurface, registerPlanReviewBrowser } from "./doors/planReviewBrowser.ts";
|
|
26
31
|
import { registerPrReview } from "./doors/prReview.ts";
|
|
27
32
|
import { registerPrReviewBrowser } from "./doors/prReviewBrowser.ts";
|
|
28
33
|
import { registerPrReviewDynamic } from "./doors/prReviewDynamic.ts";
|
|
@@ -30,6 +35,7 @@ import { registerPrReviewTerminal } from "./doors/prReviewTerminal.ts";
|
|
|
30
35
|
import { registerReady } from "./doors/ready.ts";
|
|
31
36
|
import { registerReviewWaveTools } from "./doors/reviewWaveTools.ts";
|
|
32
37
|
import { registerSelfcheck } from "./doors/selfcheck.ts";
|
|
38
|
+
import { registerOpenStackReview, registerStackReviewBrowser } from "./doors/stackReviewBrowser.ts";
|
|
33
39
|
import { registerSubmit } from "./doors/submit.ts";
|
|
34
40
|
import { registerSubmitPrReview } from "./doors/submitPrReview.ts";
|
|
35
41
|
import { registerGistAuthor } from "./factories/gistAuthor.ts";
|
|
@@ -46,6 +52,7 @@ import { registerPlanMode } from "./factories/planMode.ts";
|
|
|
46
52
|
import { registerPlanReview } from "./factories/planReview.ts";
|
|
47
53
|
import { registerPlanSave } from "./factories/planSave.ts";
|
|
48
54
|
import { createHunkFeedbackReceiver } from "./hunkFeedback/receiver.ts";
|
|
55
|
+
import { createAgentScratchProvisioner, registerAgentScratch } from "./substrate/agentScratch.ts";
|
|
49
56
|
import { registerBindingDelivery } from "./substrate/bindingDelivery.ts";
|
|
50
57
|
import {
|
|
51
58
|
atomicWriteFileSync,
|
|
@@ -77,7 +84,9 @@ import {
|
|
|
77
84
|
createPerkStatus,
|
|
78
85
|
installPerkFooter,
|
|
79
86
|
latestCacheHitRate,
|
|
87
|
+
REPORT_DETAIL_TYPE,
|
|
80
88
|
registerTranscriptRenderer,
|
|
89
|
+
reportDetailEntryRenderer,
|
|
81
90
|
workflowStateEntryRenderer,
|
|
82
91
|
} from "./surfaces/surfaces.ts";
|
|
83
92
|
import { registerBtw } from "./vendor/btw/btw.ts";
|
|
@@ -122,12 +131,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
122
131
|
// both session_start AND session_tree below. enter/exit are the surface the gated stages consume.
|
|
123
132
|
const gating = registerToolGating(pi);
|
|
124
133
|
|
|
134
|
+
// Run-owned disposable scratch guidance for every eligible write-capable model turn. One
|
|
135
|
+
// activation-scoped provisioner shares retry/warning suppression with the isolated /btw side
|
|
136
|
+
// session; no model tool or process-global temp environment is introduced.
|
|
137
|
+
const agentScratch = createAgentScratchProvisioner();
|
|
138
|
+
registerAgentScratch(pi, agentScratch);
|
|
139
|
+
|
|
125
140
|
// Vendored `btw`: a `/btw` human-only side-chat popover backed by an isolated in-memory
|
|
126
141
|
// AgentSession. Takes `gating` for the gate-mirror — its side-session toolset + cache key follow
|
|
127
142
|
// perk's read-only gate (`sideSessionTools`), so the isolated session never bypasses the read-only
|
|
128
143
|
// guarantee. Its `ctx.ui.custom` overlay is the ONE sanctioned charter exception (§6 D6): human-
|
|
129
144
|
// invoked only, `hasUI`-gated, no model tool, not a stage/door — never machine-reachable.
|
|
130
|
-
registerBtw(pi, gating);
|
|
145
|
+
registerBtw(pi, gating, agentScratch);
|
|
131
146
|
|
|
132
147
|
// Vendored `whimsical`: flavors pi's default working-message label with a random phrase per
|
|
133
148
|
// turn, via the headless-no-op `setWorkingMessage` surfaces seam. Always on, no config toggle.
|
|
@@ -156,8 +171,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
171
|
// `plan_review`, perk's UNIVERSAL review door: plannotator-selected → the event-bus
|
|
157
172
|
// bridge; ANY other selection → the first-party in-TUI editor review. It takes `gating` only to
|
|
158
173
|
// COMPOSE the approvalSave seam on an APPROVED review (auto-save → D1a gate exit) — Invariant 1
|
|
159
|
-
// holds: the door composes the gate through the seam, never owns it.
|
|
160
|
-
|
|
174
|
+
// holds: the door composes the gate through the seam, never owns it. The injected wave-launch
|
|
175
|
+
// deps power the plannotator launch chooser (§8.23): the presence probe + the two door open
|
|
176
|
+
// cores are composed HERE so planReview.ts imports nothing from door modules (the value-import
|
|
177
|
+
// cycle break — planReviewBrowser.ts already value-imports planReview.ts).
|
|
178
|
+
registerPlanReview(pi, gating, {
|
|
179
|
+
present: () => plannotatorPresent(pi),
|
|
180
|
+
plan: (ctx, opts) => openPlanReviewSurface(pi, ctx, gating, opts),
|
|
181
|
+
objective: (ctx, opts) => openObjectiveReviewSurface(pi, ctx, gating, opts),
|
|
182
|
+
});
|
|
161
183
|
|
|
162
184
|
// Objective-author context injection (the objective mirror of plan mode's authoring
|
|
163
185
|
// half). Keyed off (read-only gate AND stage === objective-author); planMode defers to it.
|
|
@@ -190,9 +212,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
190
212
|
// publisher below; the footer reads it back via get/subscribe.
|
|
191
213
|
const perkStatus = createPerkStatus();
|
|
192
214
|
|
|
193
|
-
//
|
|
194
|
-
// surfaces.ts
|
|
195
|
-
//
|
|
215
|
+
// The generic full report-detail entry and the `perk:workflow-state` transition marker. Renderer
|
|
216
|
+
// bodies live in surfaces.ts; registration is wiring through the pre-0.80.4-safe seam. The report
|
|
217
|
+
// family is appended by command-attached sinks; one workflow registration covers every appender.
|
|
218
|
+
registerTranscriptRenderer(pi, REPORT_DETAIL_TYPE, reportDetailEntryRenderer);
|
|
196
219
|
registerTranscriptRenderer(pi, WORKFLOW_STATE_TYPE, workflowStateEntryRenderer);
|
|
197
220
|
|
|
198
221
|
// The hunk watch feedback receiver controller (contracts §8.58) — factory-scoped (no module
|
|
@@ -209,7 +232,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
232
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
210
233
|
const currentSessionId = sessionFile ? basename(sessionFile) : null;
|
|
211
234
|
|
|
212
|
-
//
|
|
235
|
+
// Terminal-safe linkage failure: managed headline when headful, complete stderr when headless
|
|
236
|
+
// (plus the explicit RPC mirror); non-fatal and leaves the run unclaimed.
|
|
213
237
|
const reportError = (message: string) => {
|
|
214
238
|
report(ctx, "workflow-state linkage error", "error", message, { alsoLog: true });
|
|
215
239
|
};
|
|
@@ -237,6 +261,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
237
261
|
if (handoff === null || handoff.run_id !== decision.runId) {
|
|
238
262
|
reportError(`handoff missing or mismatched for run ${decision.runId}`);
|
|
239
263
|
} else {
|
|
264
|
+
// The objective-plan cold door's handoff_extra carries the node link
|
|
265
|
+
// (objective_id/node_id): persist it as the objective_node_claim so the implement-here
|
|
266
|
+
// exits are structurally suppressed in COLD objective-plan sessions too (the warm
|
|
267
|
+
// `objective_node` tool records the claim; a cold factory session never calls it — the
|
|
268
|
+
// door marked the node before launch). Blank/absent ids persist nothing; the claim
|
|
269
|
+
// clears on a successful node-linked save exactly as the warm-recorded one does.
|
|
270
|
+
const handoffObjective = handoff.objective_id;
|
|
271
|
+
const handoffNode = handoff.node_id;
|
|
272
|
+
const nodeClaim =
|
|
273
|
+
typeof handoffObjective === "string" &&
|
|
274
|
+
handoffObjective.trim() !== "" &&
|
|
275
|
+
typeof handoffNode === "string" &&
|
|
276
|
+
handoffNode.trim() !== ""
|
|
277
|
+
? { objective: handoffObjective, node: handoffNode }
|
|
278
|
+
: undefined;
|
|
240
279
|
const data: WorkflowState = {
|
|
241
280
|
run_id: decision.runId,
|
|
242
281
|
pi_session_id: currentSessionId ?? undefined,
|
|
@@ -245,6 +284,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
245
284
|
// Record the launched stage so the interior can tell e.g. objective-author from plan
|
|
246
285
|
// (both are read-only) and inject the right authoring context (planMode vs objectiveAuthor).
|
|
247
286
|
stage: handoff.stage,
|
|
287
|
+
...(nodeClaim !== undefined ? { objective_node_claim: nodeClaim } : {}),
|
|
248
288
|
};
|
|
249
289
|
const okAppend = appendWorkflowState(pi, ctx, {
|
|
250
290
|
data,
|
|
@@ -263,8 +303,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
263
303
|
}
|
|
264
304
|
}
|
|
265
305
|
} else if (decision.action === "fork") {
|
|
266
|
-
// Inherited a run_id from a different session file → isolate the child's scratch.
|
|
267
|
-
|
|
306
|
+
// Inherited a run_id from a different session file → isolate the child's scratch. A static
|
|
307
|
+
// redirect or filesystem failure is loud but does not prevent the derived workflow identity
|
|
308
|
+
// from settling; later eligible turns retry through the agent-scratch resolver.
|
|
309
|
+
try {
|
|
310
|
+
ensureRunScratch(ctx.cwd, decision.childRunId);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
report(
|
|
313
|
+
ctx,
|
|
314
|
+
"run scratch",
|
|
315
|
+
"warning",
|
|
316
|
+
`could not create fork run root for ${decision.childRunId}: ${String(error)}`,
|
|
317
|
+
{ alsoLog: true },
|
|
318
|
+
);
|
|
319
|
+
}
|
|
268
320
|
const data: WorkflowState = {
|
|
269
321
|
run_id: decision.childRunId,
|
|
270
322
|
pi_session_id: currentSessionId ?? undefined,
|
|
@@ -281,7 +333,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
281
333
|
// the launched session: never re-consume the handoff (its pi_session_id keeps the true
|
|
282
334
|
// claimer), no `stage` (no stage impersonation / stage-binding injection), and no
|
|
283
335
|
// implementation/main pointer capture (resolveRunStage stays null for adopt).
|
|
284
|
-
|
|
336
|
+
try {
|
|
337
|
+
ensureRunScratch(ctx.cwd, decision.childRunId);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
report(
|
|
340
|
+
ctx,
|
|
341
|
+
"run scratch",
|
|
342
|
+
"warning",
|
|
343
|
+
`could not create adopted run root for ${decision.childRunId}: ${String(error)}`,
|
|
344
|
+
{ alsoLog: true },
|
|
345
|
+
);
|
|
346
|
+
}
|
|
285
347
|
const data: WorkflowState = {
|
|
286
348
|
run_id: decision.childRunId,
|
|
287
349
|
pi_session_id: currentSessionId ?? undefined,
|
|
@@ -527,7 +589,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
527
589
|
registerSubmit(pi);
|
|
528
590
|
|
|
529
591
|
// The warm `ready` door: the deliberate draft→ready review gate (submit keeps draft).
|
|
530
|
-
|
|
592
|
+
// Takes `gating`: the warm ready→reconcile continuation refuses (loudly) to drive the
|
|
593
|
+
// ready-time pass into a read-only session (contracts.md §8.66).
|
|
594
|
+
registerReady(pi, gating);
|
|
531
595
|
|
|
532
596
|
// Warm doors: `land` merges + sets pending-learn; `learn` clears it (TS-only).
|
|
533
597
|
registerLand(pi);
|
|
@@ -564,6 +628,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
564
628
|
registerReviewWaveTools(pi);
|
|
565
629
|
registerAuditWave(pi);
|
|
566
630
|
registerHarvestWave(pi);
|
|
631
|
+
registerDreamWave(pi);
|
|
567
632
|
|
|
568
633
|
// The flow-scoped draft-review-wave pair (`start_draft_review_wave`/
|
|
569
634
|
// `collect_draft_review_wave`) the draft-review door drives: non-blocking draft-review
|
|
@@ -584,6 +649,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
584
649
|
// human's own platform-post from the UI, with `submit_pr_review` for request-changes only.
|
|
585
650
|
registerPrReviewBrowser(pi);
|
|
586
651
|
|
|
652
|
+
// The warm `/stack-review-browser` door + its cold-launch twin (`open_stack_review`): the
|
|
653
|
+
// stacked-PR browser review over the combined base→top diff — one reviewer wave with
|
|
654
|
+
// `stack: true`, then judgment-routed per-PR posting through `submit_pr_review`.
|
|
655
|
+
registerStackReviewBrowser(pi);
|
|
656
|
+
registerOpenStackReview(pi);
|
|
657
|
+
|
|
587
658
|
// The warm `/plan-review-browser` door: the summonable streaming draft review — the
|
|
588
659
|
// plannotator plan-review browser on the working plan draft, draft reviewers streaming
|
|
589
660
|
// phrase-anchored findings in; APPROVE auto-saves via the approvalSave seam, DENY returns a
|
|
@@ -605,9 +676,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
605
676
|
// (The deterministic objective mechanics live in the Python plane: `perk objective …`.)
|
|
606
677
|
registerObjective(pi, perkStatus);
|
|
607
678
|
|
|
608
|
-
// The warm `/commit-and-compact` utility door: drive a commit of the work so far,
|
|
609
|
-
//
|
|
610
|
-
// immediately; no commit → compaction
|
|
679
|
+
// The warm `/commit-and-compact` utility door: drive a commit of the work so far, compact once
|
|
680
|
+
// a successful outcome is known, then completion-gate an automatic evidence-first continuation
|
|
681
|
+
// (clean/read-only trees compact immediately; no commit → no compaction or continuation).
|
|
682
|
+
// Human-only — no tool twin.
|
|
611
683
|
registerCommitAndCompact(pi, gating);
|
|
612
684
|
|
|
613
685
|
// The warm `objective_save` door: the `objective_save` tool + `/objective-save` command
|