@mgiles/perk 3.0.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/extension/adapters/planAdapterPlannotator.ts +12 -9
- package/extension/doors/commitCompact.ts +98 -10
- package/extension/doors/draftReviewWaveTools.ts +43 -15
- package/extension/doors/dreamWaveTools.ts +475 -0
- package/extension/doors/objectiveReviewBrowser.ts +36 -13
- package/extension/doors/objectiveStack.ts +1 -1
- package/extension/doors/planReviewBrowser.ts +30 -8
- package/extension/doors/prReview.ts +156 -49
- package/extension/doors/prReviewDynamic.ts +33 -13
- package/extension/doors/reviewWaveTools.ts +37 -14
- package/extension/factories/objectiveDraft.ts +95 -27
- package/extension/factories/objectiveDreamReport.ts +347 -0
- package/extension/factories/objectiveSave.ts +74 -1
- package/extension/factories/planReview.ts +173 -10
- package/extension/index.ts +62 -15
- 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 +6 -1
- package/extension/substrate/git.ts +85 -2
- 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 +9 -0
- package/extension/substrate/workflowState.ts +44 -2
- 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 +19 -2
- 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 +49 -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/stages/conflict-resolution.md +1 -1
- package/prompts/stages/learn-dream.md +10 -0
- package/prompts/stages/objective-review-browser.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/shared/bindings.yaml +3 -0
- package/shared/contracts.md +2176 -500
- package/shared/registry.yaml +12 -12
- 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/pr-land.schema.json +3 -3
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
// FAILSAFE invocation of the same seam, keeping the legacy drive-the-session behavior as the
|
|
16
16
|
// no-draft fallback (objectives have no transcript scrape by design).
|
|
17
17
|
|
|
18
|
+
import { join } from "node:path";
|
|
18
19
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
20
|
import { bindingSuffix } from "../substrate/bindingDelivery.ts";
|
|
21
|
+
import { atomicWriteFileSync, ensureRunScratch } from "../substrate/cache.ts";
|
|
20
22
|
import {
|
|
21
23
|
booleanField,
|
|
22
24
|
type ColdJson,
|
|
@@ -34,14 +36,24 @@ import { OBJECTIVE_BUDGET_TYPE } from "./objective.ts";
|
|
|
34
36
|
import {
|
|
35
37
|
DELIVERY_PARAM_SCHEMA,
|
|
36
38
|
type DeliveryChoice,
|
|
39
|
+
DREAM_REPORT_PARAM_SCHEMA,
|
|
37
40
|
decodeObjectiveSaveParams,
|
|
38
41
|
ROADMAP_PARAM_SCHEMA,
|
|
39
42
|
readObjectiveDraft,
|
|
40
43
|
} from "./objectiveDraft.ts";
|
|
44
|
+
import { resolveDreamReportGate } from "./objectiveDreamReport.ts";
|
|
41
45
|
|
|
42
46
|
/** The `objective-save` registry stage id (the objectiveAuthor.ts constant's sibling). */
|
|
43
47
|
export const OBJECTIVE_SAVE_STAGE = "objective-save";
|
|
44
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The run-scoped dream-report transfer filename (contracts §8.64) — the extension→door handoff
|
|
51
|
+
* carrying the reviewed CANONICAL parts. The Python mirror is
|
|
52
|
+
* `perk.learn.dream_companion.DREAM_REPORT_TRANSFER_FILENAME` (parity-pinned by test), beside
|
|
53
|
+
* the existing `DREAM_MANIFEST_FILENAME` mirror pair.
|
|
54
|
+
*/
|
|
55
|
+
export const DREAM_REPORT_TRANSFER_FILENAME = "dream-report-transfer.json";
|
|
56
|
+
|
|
45
57
|
/** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state. */
|
|
46
58
|
export interface ObjectiveSaveOk {
|
|
47
59
|
/** `id` is the opaque string objective id (GitHub "7", Linear "ENG-7") — §8.21. */
|
|
@@ -70,6 +82,13 @@ function decodeObjectiveCreate(payload: ColdJson): ObjectiveCreatePayload | null
|
|
|
70
82
|
* The single save implementation both surfaces call. Delegates the GitHub write to the Python cold
|
|
71
83
|
* door, then links the live session (`active_objective` + budget marker). Returns a soft result
|
|
72
84
|
* (never throws); failures set `details.ok = false` and append no linkage.
|
|
85
|
+
*
|
|
86
|
+
* `dream_report` is ONE carrier with two sources (§8.63): the direct tool path supplies
|
|
87
|
+
* `{input}` and the save stamps `generated_at`; the approval path passes the artifact block
|
|
88
|
+
* through with its stored stamp AND stored parts — the stored parts are byte-compared against
|
|
89
|
+
* the freshly re-rendered ones, so run-scratch drift or artifact tamper between draft-write
|
|
90
|
+
* and save refuses `bad_state` (nothing saved, the gate stays on). The parts do NOT cross to
|
|
91
|
+
* the Python plane yet — companion persistence is deferred (explicit in §8.63, not silent).
|
|
73
92
|
*/
|
|
74
93
|
export async function saveObjective(
|
|
75
94
|
pi: ExtensionAPI,
|
|
@@ -80,6 +99,7 @@ export async function saveObjective(
|
|
|
80
99
|
roadmap?: unknown[];
|
|
81
100
|
base?: string;
|
|
82
101
|
delivery?: DeliveryChoice;
|
|
102
|
+
dream_report?: { input: unknown; generated_at?: string; parts?: string[] };
|
|
83
103
|
},
|
|
84
104
|
): Promise<ObjectiveSaveResult> {
|
|
85
105
|
const fail = failFor(ctx, "objective-save");
|
|
@@ -91,9 +111,52 @@ export async function saveObjective(
|
|
|
91
111
|
return fail("roadmap must be a JSON array of nodes", "invalid_input");
|
|
92
112
|
}
|
|
93
113
|
|
|
114
|
+
// The §8.63 fail-closed re-validation, before anything reaches the cold door. Presence is
|
|
115
|
+
// the `opts.dream_report === undefined` boundary — an `{input: undefined}` carrier is never
|
|
116
|
+
// constructed (the execute wraps only a present decoded value; the approval path passes the
|
|
117
|
+
// validated artifact block).
|
|
118
|
+
const generatedAt = opts.dream_report?.generated_at ?? new Date().toISOString();
|
|
119
|
+
const gate =
|
|
120
|
+
opts.dream_report === undefined
|
|
121
|
+
? resolveDreamReportGate(ctx, undefined, generatedAt)
|
|
122
|
+
: resolveDreamReportGate(ctx, opts.dream_report.input, generatedAt);
|
|
123
|
+
if (gate.kind === "refuse") {
|
|
124
|
+
return fail(gate.detail, gate.errorType);
|
|
125
|
+
}
|
|
126
|
+
if (gate.kind === "block" && opts.dream_report?.parts !== undefined) {
|
|
127
|
+
// The approval path: the reviewed (stored) parts must byte-match the re-render against
|
|
128
|
+
// freshly recovered context — the same stored `generated_at` stamp keeps the comparison
|
|
129
|
+
// deterministic.
|
|
130
|
+
if (JSON.stringify(gate.block.parts) !== JSON.stringify(opts.dream_report.parts)) {
|
|
131
|
+
return fail(
|
|
132
|
+
"the reviewed report no longer matches the wave state — re-draft and re-review",
|
|
133
|
+
"bad_state",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
94
138
|
const branch = () => branchOf(ctx);
|
|
95
139
|
const runId = rebuildWorkflowState(branch()).run_id ?? "";
|
|
96
140
|
|
|
141
|
+
if (gate.kind === "block") {
|
|
142
|
+
// The §8.64 transfer write (the dream arm only): the reviewed CANONICAL parts cross to the
|
|
143
|
+
// Python save door through the run-scoped scratch handoff — written atomically BEFORE the
|
|
144
|
+
// cold door is invoked. A write failure is the soft `scratch_failed` failure (the
|
|
145
|
+
// runColdDoor stdin-staging precedent): the cold door is NOT invoked, nothing activates,
|
|
146
|
+
// and the read-only gate stays on. Non-dream saves write nothing (byte-identical).
|
|
147
|
+
try {
|
|
148
|
+
const dir = ensureRunScratch(ctx.cwd, runId);
|
|
149
|
+
const content = `${JSON.stringify(
|
|
150
|
+
{ schema_version: "1", run_id: runId, parts: gate.block.parts },
|
|
151
|
+
null,
|
|
152
|
+
2,
|
|
153
|
+
)}\n`;
|
|
154
|
+
atomicWriteFileSync(join(dir, DREAM_REPORT_TRANSFER_FILENAME), content);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return fail(`could not stage the dream-report transfer: ${String(err)}`, "scratch_failed");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
97
160
|
const args = ["objective", "create", "--json"];
|
|
98
161
|
if (opts.title) args.push("--title", opts.title);
|
|
99
162
|
if (opts.base) args.push("--base", opts.base);
|
|
@@ -175,6 +238,9 @@ export async function objectiveApprovalSave(
|
|
|
175
238
|
roadmap: draft.roadmap,
|
|
176
239
|
base: draft.base,
|
|
177
240
|
delivery: draft.delivery,
|
|
241
|
+
// The approval path passes the artifact block through whole — stored stamp + stored parts
|
|
242
|
+
// (the save re-validates and byte-compares, §8.63).
|
|
243
|
+
...(draft.dream_report !== undefined ? { dream_report: draft.dream_report } : {}),
|
|
178
244
|
});
|
|
179
245
|
let gateExited = false;
|
|
180
246
|
if (result.details.ok && wasReadOnly) {
|
|
@@ -231,6 +297,7 @@ export function registerObjectiveSave(pi: ExtensionAPI, gating: ToolGating): voi
|
|
|
231
297
|
"Optional target branch for this objective's plans (omit to use the repo default).",
|
|
232
298
|
},
|
|
233
299
|
delivery: DELIVERY_PARAM_SCHEMA,
|
|
300
|
+
dream_report: DREAM_REPORT_PARAM_SCHEMA,
|
|
234
301
|
roadmap: {
|
|
235
302
|
type: "array",
|
|
236
303
|
description:
|
|
@@ -251,7 +318,13 @@ export function registerObjectiveSave(pi: ExtensionAPI, gating: ToolGating): voi
|
|
|
251
318
|
"bad_input",
|
|
252
319
|
);
|
|
253
320
|
}
|
|
254
|
-
|
|
321
|
+
// The direct tool path wraps ONLY a present decoded value as the `{input}` carrier (the
|
|
322
|
+
// save stamps generated_at); no stored parts, so no byte-compare on this path.
|
|
323
|
+
const { dream_report, ...rest } = decoded;
|
|
324
|
+
return saveObjective(pi, ctx, {
|
|
325
|
+
...rest,
|
|
326
|
+
...(dream_report !== undefined ? { dream_report: { input: dream_report } } : {}),
|
|
327
|
+
});
|
|
255
328
|
},
|
|
256
329
|
});
|
|
257
330
|
|
|
@@ -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";
|
|
@@ -46,6 +51,7 @@ import { registerPlanMode } from "./factories/planMode.ts";
|
|
|
46
51
|
import { registerPlanReview } from "./factories/planReview.ts";
|
|
47
52
|
import { registerPlanSave } from "./factories/planSave.ts";
|
|
48
53
|
import { createHunkFeedbackReceiver } from "./hunkFeedback/receiver.ts";
|
|
54
|
+
import { createAgentScratchProvisioner, registerAgentScratch } from "./substrate/agentScratch.ts";
|
|
49
55
|
import { registerBindingDelivery } from "./substrate/bindingDelivery.ts";
|
|
50
56
|
import {
|
|
51
57
|
atomicWriteFileSync,
|
|
@@ -77,7 +83,9 @@ import {
|
|
|
77
83
|
createPerkStatus,
|
|
78
84
|
installPerkFooter,
|
|
79
85
|
latestCacheHitRate,
|
|
86
|
+
REPORT_DETAIL_TYPE,
|
|
80
87
|
registerTranscriptRenderer,
|
|
88
|
+
reportDetailEntryRenderer,
|
|
81
89
|
workflowStateEntryRenderer,
|
|
82
90
|
} from "./surfaces/surfaces.ts";
|
|
83
91
|
import { registerBtw } from "./vendor/btw/btw.ts";
|
|
@@ -122,12 +130,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
122
130
|
// both session_start AND session_tree below. enter/exit are the surface the gated stages consume.
|
|
123
131
|
const gating = registerToolGating(pi);
|
|
124
132
|
|
|
133
|
+
// Run-owned disposable scratch guidance for every eligible write-capable model turn. One
|
|
134
|
+
// activation-scoped provisioner shares retry/warning suppression with the isolated /btw side
|
|
135
|
+
// session; no model tool or process-global temp environment is introduced.
|
|
136
|
+
const agentScratch = createAgentScratchProvisioner();
|
|
137
|
+
registerAgentScratch(pi, agentScratch);
|
|
138
|
+
|
|
125
139
|
// Vendored `btw`: a `/btw` human-only side-chat popover backed by an isolated in-memory
|
|
126
140
|
// AgentSession. Takes `gating` for the gate-mirror — its side-session toolset + cache key follow
|
|
127
141
|
// perk's read-only gate (`sideSessionTools`), so the isolated session never bypasses the read-only
|
|
128
142
|
// guarantee. Its `ctx.ui.custom` overlay is the ONE sanctioned charter exception (§6 D6): human-
|
|
129
143
|
// invoked only, `hasUI`-gated, no model tool, not a stage/door — never machine-reachable.
|
|
130
|
-
registerBtw(pi, gating);
|
|
144
|
+
registerBtw(pi, gating, agentScratch);
|
|
131
145
|
|
|
132
146
|
// Vendored `whimsical`: flavors pi's default working-message label with a random phrase per
|
|
133
147
|
// turn, via the headless-no-op `setWorkingMessage` surfaces seam. Always on, no config toggle.
|
|
@@ -156,8 +170,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
156
170
|
// `plan_review`, perk's UNIVERSAL review door: plannotator-selected → the event-bus
|
|
157
171
|
// bridge; ANY other selection → the first-party in-TUI editor review. It takes `gating` only to
|
|
158
172
|
// 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
|
-
|
|
173
|
+
// holds: the door composes the gate through the seam, never owns it. The injected wave-launch
|
|
174
|
+
// deps power the plannotator launch chooser (§8.23): the presence probe + the two door open
|
|
175
|
+
// cores are composed HERE so planReview.ts imports nothing from door modules (the value-import
|
|
176
|
+
// cycle break — planReviewBrowser.ts already value-imports planReview.ts).
|
|
177
|
+
registerPlanReview(pi, gating, {
|
|
178
|
+
present: () => plannotatorPresent(pi),
|
|
179
|
+
plan: (ctx, opts) => openPlanReviewSurface(pi, ctx, gating, opts),
|
|
180
|
+
objective: (ctx, opts) => openObjectiveReviewSurface(pi, ctx, gating, opts),
|
|
181
|
+
});
|
|
161
182
|
|
|
162
183
|
// Objective-author context injection (the objective mirror of plan mode's authoring
|
|
163
184
|
// half). Keyed off (read-only gate AND stage === objective-author); planMode defers to it.
|
|
@@ -190,9 +211,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
190
211
|
// publisher below; the footer reads it back via get/subscribe.
|
|
191
212
|
const perkStatus = createPerkStatus();
|
|
192
213
|
|
|
193
|
-
//
|
|
194
|
-
// surfaces.ts
|
|
195
|
-
//
|
|
214
|
+
// The generic full report-detail entry and the `perk:workflow-state` transition marker. Renderer
|
|
215
|
+
// bodies live in surfaces.ts; registration is wiring through the pre-0.80.4-safe seam. The report
|
|
216
|
+
// family is appended by command-attached sinks; one workflow registration covers every appender.
|
|
217
|
+
registerTranscriptRenderer(pi, REPORT_DETAIL_TYPE, reportDetailEntryRenderer);
|
|
196
218
|
registerTranscriptRenderer(pi, WORKFLOW_STATE_TYPE, workflowStateEntryRenderer);
|
|
197
219
|
|
|
198
220
|
// The hunk watch feedback receiver controller (contracts §8.58) — factory-scoped (no module
|
|
@@ -209,7 +231,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
231
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
210
232
|
const currentSessionId = sessionFile ? basename(sessionFile) : null;
|
|
211
233
|
|
|
212
|
-
//
|
|
234
|
+
// Terminal-safe linkage failure: managed headline when headful, complete stderr when headless
|
|
235
|
+
// (plus the explicit RPC mirror); non-fatal and leaves the run unclaimed.
|
|
213
236
|
const reportError = (message: string) => {
|
|
214
237
|
report(ctx, "workflow-state linkage error", "error", message, { alsoLog: true });
|
|
215
238
|
};
|
|
@@ -263,8 +286,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
263
286
|
}
|
|
264
287
|
}
|
|
265
288
|
} else if (decision.action === "fork") {
|
|
266
|
-
// Inherited a run_id from a different session file → isolate the child's scratch.
|
|
267
|
-
|
|
289
|
+
// Inherited a run_id from a different session file → isolate the child's scratch. A static
|
|
290
|
+
// redirect or filesystem failure is loud but does not prevent the derived workflow identity
|
|
291
|
+
// from settling; later eligible turns retry through the agent-scratch resolver.
|
|
292
|
+
try {
|
|
293
|
+
ensureRunScratch(ctx.cwd, decision.childRunId);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
report(
|
|
296
|
+
ctx,
|
|
297
|
+
"run scratch",
|
|
298
|
+
"warning",
|
|
299
|
+
`could not create fork run root for ${decision.childRunId}: ${String(error)}`,
|
|
300
|
+
{ alsoLog: true },
|
|
301
|
+
);
|
|
302
|
+
}
|
|
268
303
|
const data: WorkflowState = {
|
|
269
304
|
run_id: decision.childRunId,
|
|
270
305
|
pi_session_id: currentSessionId ?? undefined,
|
|
@@ -281,7 +316,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
281
316
|
// the launched session: never re-consume the handoff (its pi_session_id keeps the true
|
|
282
317
|
// claimer), no `stage` (no stage impersonation / stage-binding injection), and no
|
|
283
318
|
// implementation/main pointer capture (resolveRunStage stays null for adopt).
|
|
284
|
-
|
|
319
|
+
try {
|
|
320
|
+
ensureRunScratch(ctx.cwd, decision.childRunId);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
report(
|
|
323
|
+
ctx,
|
|
324
|
+
"run scratch",
|
|
325
|
+
"warning",
|
|
326
|
+
`could not create adopted run root for ${decision.childRunId}: ${String(error)}`,
|
|
327
|
+
{ alsoLog: true },
|
|
328
|
+
);
|
|
329
|
+
}
|
|
285
330
|
const data: WorkflowState = {
|
|
286
331
|
run_id: decision.childRunId,
|
|
287
332
|
pi_session_id: currentSessionId ?? undefined,
|
|
@@ -564,6 +609,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
564
609
|
registerReviewWaveTools(pi);
|
|
565
610
|
registerAuditWave(pi);
|
|
566
611
|
registerHarvestWave(pi);
|
|
612
|
+
registerDreamWave(pi);
|
|
567
613
|
|
|
568
614
|
// The flow-scoped draft-review-wave pair (`start_draft_review_wave`/
|
|
569
615
|
// `collect_draft_review_wave`) the draft-review door drives: non-blocking draft-review
|
|
@@ -605,9 +651,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
605
651
|
// (The deterministic objective mechanics live in the Python plane: `perk objective …`.)
|
|
606
652
|
registerObjective(pi, perkStatus);
|
|
607
653
|
|
|
608
|
-
// The warm `/commit-and-compact` utility door: drive a commit of the work so far,
|
|
609
|
-
//
|
|
610
|
-
// immediately; no commit → compaction
|
|
654
|
+
// The warm `/commit-and-compact` utility door: drive a commit of the work so far, compact once
|
|
655
|
+
// a successful outcome is known, then completion-gate an automatic evidence-first continuation
|
|
656
|
+
// (clean/read-only trees compact immediately; no commit → no compaction or continuation).
|
|
657
|
+
// Human-only — no tool twin.
|
|
611
658
|
registerCommitAndCompact(pi, gating);
|
|
612
659
|
|
|
613
660
|
// The warm `objective_save` door: the `objective_save` tool + `/objective-save` command
|