@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
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
// The warm `submit_pr_review` tool — the agent-driven curated-posting surface shared by the
|
|
2
|
-
// PR-review doors (`/pr-review-terminal`, `/pr-review-browser`).
|
|
1
|
+
// The warm `submit_pr_review` tool — the agent-driven curated-posting surface shared by the
|
|
2
|
+
// PR-review doors (`/pr-review-terminal`, `/pr-review-browser`, `/stack-review-browser`).
|
|
3
3
|
//
|
|
4
4
|
// `submit_pr_review` implements the per-door posting contract (contracts §8.4): nothing perk-
|
|
5
5
|
// driven reaches GitHub before the human triage; ALL perk-side posting flows through this tool
|
|
6
6
|
// on every door (`gh` mutations and direct `perk pr review-submit` calls are forbidden); the
|
|
7
7
|
// verdict lands last, atomically with the comments. On the terminal door this tool is the sole
|
|
8
|
-
// posting path. On the browser door the human platform-posts from the plannotator
|
|
9
|
-
// native posting IS the GitHub path; perk composes nothing by default and posts only
|
|
10
|
-
// human explicitly hands it (typically a request-changes verdict, which the UI cannot
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// to
|
|
14
|
-
//
|
|
15
|
-
//
|
|
8
|
+
// posting path. On the single-PR browser door the human platform-posts from the plannotator
|
|
9
|
+
// UI — that native posting IS the GitHub path; perk composes nothing by default and posts only
|
|
10
|
+
// what the human explicitly hands it (typically a request-changes verdict, which the UI cannot
|
|
11
|
+
// post). On the stack door the local-diff session has NO attached PR, so ALL posting is
|
|
12
|
+
// perk-side after triage: one real call per member PR, bottom→top, each under its own dry-run
|
|
13
|
+
// anchor validation. It delegates to the Python cold door (`perk pr review-submit` — mutations
|
|
14
|
+
// canonical in Python) via `runColdDoor` (the batch rides the run-scratch stdin channel), then
|
|
15
|
+
// appends `last_review` AND the accumulating `review_posts` ledger row to `perk:workflow-state`
|
|
16
|
+
// (the stack flow's resume authority — posted PRs are skipped, never replayed). The human gate
|
|
17
|
+
// splits: explicit conversational go-ahead ALWAYS (pinned in the guidelines/skill); formal
|
|
18
|
+
// events (`approve`/`request-changes`) additionally get the structural gate — headless refuses,
|
|
19
|
+
// interactive raises a blocking `ctx.ui.confirm`.
|
|
16
20
|
// `dry_run` is the anchor-repair loop: no gates, no record, nothing posted.
|
|
17
21
|
|
|
18
22
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -34,7 +38,12 @@ import {
|
|
|
34
38
|
stringParam,
|
|
35
39
|
type ToolParams,
|
|
36
40
|
} from "../substrate/toolParams.ts";
|
|
37
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
appendWorkflowState,
|
|
43
|
+
branchOf,
|
|
44
|
+
type EntrySink,
|
|
45
|
+
rebuildWorkflowState,
|
|
46
|
+
} from "../substrate/workflowState.ts";
|
|
38
47
|
import type { Severity } from "../surfaces/report.ts";
|
|
39
48
|
|
|
40
49
|
// ------------------------------------------------------------------------ params
|
|
@@ -55,6 +64,7 @@ export interface SubmitParams {
|
|
|
55
64
|
body: string;
|
|
56
65
|
comments?: SubmitComment[];
|
|
57
66
|
dry_run?: boolean;
|
|
67
|
+
allow_repost?: boolean;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/** Decode the optional `comments` array; null = present-but-malformed (whole-batch refusal). */
|
|
@@ -87,7 +97,7 @@ function decodeSubmitComments(p: ToolParams): SubmitComment[] | undefined | null
|
|
|
87
97
|
* malformed field ⇒ null (whole-batch refusal). `pr` must be an int; `event` exactly one of the
|
|
88
98
|
* three flag spellings; `body` a string (EMPTY ALLOWED — the cold door owns the event-conditioned
|
|
89
99
|
* body rule and reports `bad_batch`); each `comments` row strict on
|
|
90
|
-
* path/line(int)/side(LEFT|RIGHT)/body; `dry_run`
|
|
100
|
+
* path/line(int)/side(LEFT|RIGHT)/body; `dry_run` and `allow_repost` booleans.
|
|
91
101
|
*/
|
|
92
102
|
export function decodeSubmitParams(params: unknown): SubmitParams | null {
|
|
93
103
|
const p = paramsOf(params);
|
|
@@ -102,9 +112,12 @@ export function decodeSubmitParams(params: unknown): SubmitParams | null {
|
|
|
102
112
|
if (comments === null) return null;
|
|
103
113
|
const dryRun = booleanParam(p, "dry_run");
|
|
104
114
|
if (dryRun === null) return null;
|
|
115
|
+
const allowRepost = booleanParam(p, "allow_repost");
|
|
116
|
+
if (allowRepost === null) return null;
|
|
105
117
|
const result: SubmitParams = { pr, event, body };
|
|
106
118
|
if (comments !== undefined) result.comments = comments;
|
|
107
119
|
if (dryRun !== undefined) result.dry_run = dryRun;
|
|
120
|
+
if (allowRepost !== undefined) result.allow_repost = allowRepost;
|
|
108
121
|
return result;
|
|
109
122
|
}
|
|
110
123
|
|
|
@@ -167,6 +180,42 @@ function decodeInvalidAnchors(payload: ColdJson): InvalidAnchor[] | null {
|
|
|
167
180
|
return rows;
|
|
168
181
|
}
|
|
169
182
|
|
|
183
|
+
// ------------------------------------------------------------------------ the posting ledger
|
|
184
|
+
|
|
185
|
+
/** One `review_posts` ledger row: a REAL submission that reached GitHub. */
|
|
186
|
+
export interface ReviewPostRow {
|
|
187
|
+
pr: number;
|
|
188
|
+
event: string;
|
|
189
|
+
at: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Tolerant re-narrow of the rebuilt `review_posts` list (best-effort tier: a malformed row is
|
|
194
|
+
* dropped, never a refusal — the ledger only ever grows from this tool's own writes).
|
|
195
|
+
*/
|
|
196
|
+
export function reviewPostsOf(raw: unknown): ReviewPostRow[] {
|
|
197
|
+
if (!Array.isArray(raw)) return [];
|
|
198
|
+
const rows: ReviewPostRow[] = [];
|
|
199
|
+
for (const item of raw) {
|
|
200
|
+
const row = paramsOf(item);
|
|
201
|
+
if (row === null) continue;
|
|
202
|
+
if (typeof row.pr !== "number" || !Number.isInteger(row.pr)) continue;
|
|
203
|
+
if (typeof row.event !== "string" || typeof row.at !== "string") continue;
|
|
204
|
+
rows.push({ pr: row.pr, event: row.event, at: row.at });
|
|
205
|
+
}
|
|
206
|
+
return rows;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Ledger equality for the read-back verification (order-sensitive — posting order matters). */
|
|
210
|
+
function reviewPostsEqual(rebuilt: unknown, expected: unknown): boolean {
|
|
211
|
+
const a = reviewPostsOf(rebuilt);
|
|
212
|
+
const b = reviewPostsOf(expected);
|
|
213
|
+
if (a.length !== b.length) return false;
|
|
214
|
+
return a.every(
|
|
215
|
+
(row, i) => row.pr === b[i]?.pr && row.event === b[i]?.event && row.at === b[i]?.at,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
170
219
|
/** Flag spelling → the REST wire spelling shown in the human confirm. */
|
|
171
220
|
const WIRE_EVENT: Record<ReviewEvent, string> = {
|
|
172
221
|
approve: "APPROVE",
|
|
@@ -209,6 +258,27 @@ export async function submitPrReview(
|
|
|
209
258
|
const dryRun = params.dry_run === true;
|
|
210
259
|
const commentCount = params.comments?.length ?? 0;
|
|
211
260
|
|
|
261
|
+
// The enforced resume guard (before the confirm AND the cold-door mutation): a PR that
|
|
262
|
+
// already has a review_posts ledger row in this session is a confirmed success — a repeat
|
|
263
|
+
// real post is refused unless explicitly deliberate. A ledger row can only be MISSING
|
|
264
|
+
// spuriously (best-effort tier), never present spuriously — so the guard refuses on
|
|
265
|
+
// presence and stays silent on absence (a missing row still means: verify posted-vs-pending
|
|
266
|
+
// against GitHub before re-posting).
|
|
267
|
+
if (!dryRun && params.allow_repost !== true) {
|
|
268
|
+
const prior = reviewPostsOf(rebuildWorkflowState(branchOf(ctx)).review_posts).filter(
|
|
269
|
+
(row) => row.pr === params.pr,
|
|
270
|
+
);
|
|
271
|
+
const last = prior.at(-1);
|
|
272
|
+
if (last !== undefined) {
|
|
273
|
+
return fail(
|
|
274
|
+
`a ${last.event} review was already posted to PR #${params.pr} in this session ` +
|
|
275
|
+
`(review_posts row at ${last.at}) — on a stack resume skip this member; pass ` +
|
|
276
|
+
"allow_repost: true only for a deliberate second review of the same PR",
|
|
277
|
+
"already_posted",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
212
282
|
if (!dryRun && params.event !== "comment") {
|
|
213
283
|
if (!ctx.hasUI) {
|
|
214
284
|
return fail(
|
|
@@ -305,6 +375,21 @@ export async function submitPrReview(
|
|
|
305
375
|
failure: "last_review read-back failed",
|
|
306
376
|
});
|
|
307
377
|
|
|
378
|
+
// The accumulating per-PR ledger (read-rebuild-append): ordered rows, one per real success —
|
|
379
|
+
// the stack flow's partial-outcome/resume authority. Same best-effort tier as last_review.
|
|
380
|
+
const posts: ReviewPostRow[] = [
|
|
381
|
+
...reviewPostsOf(rebuildWorkflowState(branchOf(ctx)).review_posts),
|
|
382
|
+
{ pr: record.pr, event: params.event, at: record.at },
|
|
383
|
+
];
|
|
384
|
+
appendWorkflowState(pi, ctx, {
|
|
385
|
+
data: { review_posts: posts },
|
|
386
|
+
field: "review_posts",
|
|
387
|
+
expected: posts,
|
|
388
|
+
scope: "review",
|
|
389
|
+
failure: "review_posts read-back failed",
|
|
390
|
+
equals: reviewPostsEqual,
|
|
391
|
+
});
|
|
392
|
+
|
|
308
393
|
let text =
|
|
309
394
|
`submitted ${params.event} review to PR #${record.pr} ` +
|
|
310
395
|
`(${data.comment_count ?? commentCount} inline comment(s))`;
|
|
@@ -320,10 +405,10 @@ export async function submitPrReview(
|
|
|
320
405
|
|
|
321
406
|
const TOOL_GUIDELINES = [
|
|
322
407
|
"Call submit_pr_review only after the human triage has settled the batch AND the human has explicitly approved posting — nothing reaches GitHub before triage.",
|
|
323
|
-
"Validate first with dry_run: true and repair any reported anchors until validation passes; a dry-run never posts, never gates, and records nothing.",
|
|
324
|
-
"Make ONE real call: comments + body + event land atomically in a single review — the verdict never lands before the comments.",
|
|
408
|
+
"Validate first with dry_run: true and repair any reported anchors until validation passes; a dry-run never posts, never gates, and records nothing. A stack review dry-runs ALL per-PR batches before ANY real post.",
|
|
409
|
+
"Make ONE real call per target PR: comments + body + event land atomically in a single review — the verdict never lands before the comments. A stack review posts one review per member PR, bottom→top; each real success appends a {pr, event, at} row to the review_posts workflow-state ledger. The tool ENFORCES skip-on-resume: a real post to a PR that already has a ledger row is refused (already_posted) unless allow_repost: true — a deliberate second review only. A MISSING row is not proof of no post (the ledger is best-effort): verify posted-vs-pending against GitHub before re-posting.",
|
|
325
410
|
"Formal events (approve / request-changes) additionally raise a blocking in-TUI confirm; headless sessions refuse them (use event: comment or re-run interactively).",
|
|
326
|
-
"All perk-side GitHub posting flows through this tool on
|
|
411
|
+
"All perk-side GitHub posting flows through this tool on every review door — never post via gh or bash (direct perk pr review-submit calls are forbidden). On /pr-review-terminal this tool is the sole posting path. On /pr-review-browser the plannotator UI's native platform-posting is the human's own GitHub path, and perk posts only what the human explicitly hands it (typically a request-changes verdict). On /stack-review-browser the local-diff session has NO attached PR, so ALL posting is perk-side after triage.",
|
|
327
412
|
];
|
|
328
413
|
|
|
329
414
|
// ------------------------------------------------------------------------ registration
|
|
@@ -334,9 +419,12 @@ export function registerSubmitPrReview(pi: ExtensionAPI): void {
|
|
|
334
419
|
name: "submit_pr_review",
|
|
335
420
|
label: "Submit PR review",
|
|
336
421
|
description:
|
|
337
|
-
"Submit the human-curated review-door outcome to the
|
|
338
|
-
"(comments + body + event) via the perk cold door
|
|
339
|
-
"
|
|
422
|
+
"Submit the human-curated review-door outcome to the target PR as ONE atomic review " +
|
|
423
|
+
"(comments + body + event) via the perk cold door — the posting surface of the " +
|
|
424
|
+
"/pr-review-terminal, /pr-review-browser, and /stack-review-browser doors (a stack " +
|
|
425
|
+
"review makes one real call per member PR). dry_run validates the anchors without " +
|
|
426
|
+
"posting (the repair loop); a real submission records last_review and appends the " +
|
|
427
|
+
"review_posts ledger row in workflow-state.",
|
|
340
428
|
promptSnippet: "Submit the curated review batch to the PR",
|
|
341
429
|
promptGuidelines: TOOL_GUIDELINES,
|
|
342
430
|
executionMode: "sequential",
|
|
@@ -363,7 +451,9 @@ export function registerSubmitPrReview(pi: ExtensionAPI): void {
|
|
|
363
451
|
type: "array",
|
|
364
452
|
description:
|
|
365
453
|
"The curated inline comments — human-authored or human-approved only, each anchored " +
|
|
366
|
-
"to a line in the PR diff
|
|
454
|
+
"to a line in the PR diff. Single-PR mode: never re-anchor a child's finding. Stack " +
|
|
455
|
+
"mode: the parent re-anchors combined-diff findings into per-PR coordinates under " +
|
|
456
|
+
"the dry-run loop.",
|
|
367
457
|
items: {
|
|
368
458
|
type: "object",
|
|
369
459
|
additionalProperties: false,
|
|
@@ -386,6 +476,13 @@ export function registerSubmitPrReview(pi: ExtensionAPI): void {
|
|
|
386
476
|
"Validate the batch + anchors without posting (the anchor-repair loop). No gates, " +
|
|
387
477
|
"no last_review record.",
|
|
388
478
|
},
|
|
479
|
+
allow_repost: {
|
|
480
|
+
type: "boolean",
|
|
481
|
+
description:
|
|
482
|
+
"Deliberately post ANOTHER review to a PR that already has a review_posts ledger " +
|
|
483
|
+
"row in this session — the enforced resume guard refuses with already_posted " +
|
|
484
|
+
"otherwise. Never pass it to work around a stack-resume refusal.",
|
|
485
|
+
},
|
|
389
486
|
},
|
|
390
487
|
},
|
|
391
488
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// `objective_save`/`/objective-save` still persist the objective to GitHub.
|
|
12
12
|
//
|
|
13
13
|
// Format doctrine: JSON is the storage/transport format, NEVER the human review surface. The
|
|
14
|
-
// artifact carries `{schema_version, title?, prose, roadmap}`
|
|
15
|
-
//
|
|
14
|
+
// artifact carries `{schema_version, title?, prose, roadmap}` (plus, in a perk learn dream
|
|
15
|
+
// session, the tool-written `dream_report` block — contracts §8.63) — the structured roadmap
|
|
16
|
+
// rides verbatim (node-shape validation stays with the Python plane at save time, the
|
|
16
17
|
// `parse_structured_roadmap` path). The review surface reads the draft via
|
|
17
18
|
// `readObjectiveDraft` (over `readSessionArtifact` — digest-validated, fail-open) and renders
|
|
18
19
|
// markdown via `renderObjectiveDraft` (the prose + a roadmap table) — never raw JSON; the
|
|
@@ -20,9 +21,10 @@
|
|
|
20
21
|
// back as structured JSON.
|
|
21
22
|
//
|
|
22
23
|
// Vocabulary ownership: this module owns the shared draft/save param vocabulary
|
|
23
|
-
// (`ObjectiveSaveParams`, `decodeObjectiveSaveParams`, `ROADMAP_PARAM_SCHEMA
|
|
24
|
-
// the LEAF (mirroring planDraft←planSave's
|
|
25
|
-
// value-import `readObjectiveDraft`
|
|
24
|
+
// (`ObjectiveSaveParams`, `decodeObjectiveSaveParams`, `ROADMAP_PARAM_SCHEMA`,
|
|
25
|
+
// `DREAM_REPORT_PARAM_SCHEMA`) — objectiveDraft is the LEAF (mirroring planDraft←planSave's
|
|
26
|
+
// direction); objectiveSave.ts consumes it, so it may value-import `readObjectiveDraft`
|
|
27
|
+
// cycle-free for the approval→save orchestration.
|
|
26
28
|
//
|
|
27
29
|
// Imports stay node builtins + sibling seams (sessionData.ts, result.ts) so the module loads
|
|
28
30
|
// under `node --test`; no manual `scratch`/`runs` path segments (cacheGuard.test.ts).
|
|
@@ -37,9 +39,15 @@ import {
|
|
|
37
39
|
type SessionDataCtx,
|
|
38
40
|
writeSessionArtifact,
|
|
39
41
|
} from "../substrate/sessionData.ts";
|
|
40
|
-
import { arrayParam, paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
42
|
+
import { arrayParam, objectParam, paramsOf, stringParam } from "../substrate/toolParams.ts";
|
|
41
43
|
import type { EntrySink } from "../substrate/workflowState.ts";
|
|
42
44
|
import type { ReportTarget } from "../surfaces/report.ts";
|
|
45
|
+
import { DREAM_REPORT_INPUT_SCHEMA } from "../waves/dreamReport.ts";
|
|
46
|
+
import {
|
|
47
|
+
decodeDreamReportBlock,
|
|
48
|
+
type ObjectiveDreamReportBlock,
|
|
49
|
+
resolveDreamReportGate,
|
|
50
|
+
} from "./objectiveDreamReport.ts";
|
|
43
51
|
|
|
44
52
|
/** The reviewed objective delivery choice (contracts §8.45). */
|
|
45
53
|
export type DeliveryChoice = "incremental" | "stacked";
|
|
@@ -53,6 +61,9 @@ export interface ObjectiveSaveParams {
|
|
|
53
61
|
base?: string;
|
|
54
62
|
// The reviewed delivery choice; omitted ⇒ incremental (the §8.42 absence rule).
|
|
55
63
|
delivery?: DeliveryChoice;
|
|
64
|
+
// The dream-report input (perk learn dream only — §8.63); deep validation is the gate
|
|
65
|
+
// resolver's, so the decode keeps it opaque beyond the plain-object shape.
|
|
66
|
+
dream_report?: unknown;
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
/**
|
|
@@ -69,6 +80,20 @@ export const DELIVERY_PARAM_SCHEMA = {
|
|
|
69
80
|
"one atomic PR train — capability-checked at save).",
|
|
70
81
|
} as const;
|
|
71
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The `dream_report` property, shared between `objective_save` and `objective_draft` so the
|
|
85
|
+
* two tools' dream contracts cannot drift: the §8.62 `DREAM_REPORT_INPUT_SCHEMA` embedded by
|
|
86
|
+
* identifier (the `DELIVERY_PARAM_SCHEMA`/`ROADMAP_PARAM_SCHEMA` shared-schema pattern) plus
|
|
87
|
+
* the gate description. Structurally reachable only inside a `perk learn dream` session (the
|
|
88
|
+
* resolver refuses it outside one).
|
|
89
|
+
*/
|
|
90
|
+
export const DREAM_REPORT_PARAM_SCHEMA = {
|
|
91
|
+
...DREAM_REPORT_INPUT_SCHEMA,
|
|
92
|
+
description:
|
|
93
|
+
"The perk learn dream session's final report input (the parent's decisions only) — " +
|
|
94
|
+
"required inside a dream session, refused outside one.",
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
72
97
|
/**
|
|
73
98
|
* The roadmap-node items JSON schema, shared between `objective_save` and `objective_draft`
|
|
74
99
|
* so the two tools' roadmap contracts cannot drift.
|
|
@@ -116,12 +141,22 @@ export function decodeObjectiveSaveParams(params: unknown): ObjectiveSaveParams
|
|
|
116
141
|
const roadmap = arrayParam(p, "roadmap");
|
|
117
142
|
const base = stringParam(p, "base");
|
|
118
143
|
const delivery = stringParam(p, "delivery");
|
|
119
|
-
|
|
144
|
+
// `dream_report` must be a plain object when present (absent → undefined); deep validation
|
|
145
|
+
// stays with the gate resolver (resolveDreamReportGate).
|
|
146
|
+
const dreamReport = objectParam(p, "dream_report");
|
|
147
|
+
if (
|
|
148
|
+
prose === null ||
|
|
149
|
+
title === null ||
|
|
150
|
+
roadmap === null ||
|
|
151
|
+
base === null ||
|
|
152
|
+
delivery === null ||
|
|
153
|
+
dreamReport === null
|
|
154
|
+
) {
|
|
120
155
|
return null;
|
|
121
156
|
}
|
|
122
157
|
// The delivery enum is strict beyond `string`: an off-enum value is present-but-mistyped.
|
|
123
158
|
if (delivery !== undefined && delivery !== "incremental" && delivery !== "stacked") return null;
|
|
124
|
-
return { prose: prose ?? "", title, roadmap, base, delivery };
|
|
159
|
+
return { prose: prose ?? "", title, roadmap, base, delivery, dream_report: dreamReport };
|
|
125
160
|
}
|
|
126
161
|
|
|
127
162
|
/** The fixed working-objective artifact name (one JSON file: prose + the structured roadmap). */
|
|
@@ -144,7 +179,9 @@ export type ObjectiveDraftResult = Result<ObjectiveDraftOk>;
|
|
|
144
179
|
* structured roadmap, verbatim — the draft never validates node shapes) as one JSON artifact and
|
|
145
180
|
* write it through the accessor seam (file + `session_artifacts` provenance pointer). Soft
|
|
146
181
|
* result, never throws — failure taxonomy: empty prose → `invalid_input`; no session run_id →
|
|
147
|
-
* `no_run_id`;
|
|
182
|
+
* `no_run_id`; a `dream_report` gate refusal → `invalid_input`/`bad_state` (the §8.63 matrix —
|
|
183
|
+
* validated at write time so a report-less dream bundle can never reach review);
|
|
184
|
+
* file-or-pointer write failure → `write_failed` (the seam already warned).
|
|
148
185
|
*/
|
|
149
186
|
export function writeObjectiveDraft(
|
|
150
187
|
sink: EntrySink,
|
|
@@ -155,6 +192,7 @@ export function writeObjectiveDraft(
|
|
|
155
192
|
roadmap?: unknown[];
|
|
156
193
|
base?: string;
|
|
157
194
|
delivery?: DeliveryChoice;
|
|
195
|
+
dream_report?: unknown;
|
|
158
196
|
},
|
|
159
197
|
): ObjectiveDraftResult {
|
|
160
198
|
const fail = failFor(ctx, "objective-draft");
|
|
@@ -168,6 +206,13 @@ export function writeObjectiveDraft(
|
|
|
168
206
|
return fail("session has no run_id — cannot write the objective-draft artifact", "no_run_id");
|
|
169
207
|
}
|
|
170
208
|
|
|
209
|
+
// The §8.63 gate: validated at draft-write time via buildDreamReport, the ONE stamp stored
|
|
210
|
+
// with the block; `absent` keeps the payload byte-identical (every non-dream path unchanged).
|
|
211
|
+
const gate = resolveDreamReportGate(ctx, opts.dream_report, new Date().toISOString());
|
|
212
|
+
if (gate.kind === "refuse") {
|
|
213
|
+
return fail(gate.detail, gate.errorType);
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
// Deterministic key order via the explicit literal; `title`/`base`/`delivery` are omitted
|
|
172
217
|
// when blank/absent (schema_version stays 1 — an additive optional field, fail-open readers).
|
|
173
218
|
const title = opts.title?.trim();
|
|
@@ -179,6 +224,7 @@ export function writeObjectiveDraft(
|
|
|
179
224
|
...(title ? { title } : {}),
|
|
180
225
|
...(base ? { base } : {}),
|
|
181
226
|
...(delivery ? { delivery } : {}),
|
|
227
|
+
...(gate.kind === "block" ? { dream_report: gate.block } : {}),
|
|
182
228
|
prose: opts.prose,
|
|
183
229
|
roadmap,
|
|
184
230
|
};
|
|
@@ -216,6 +262,8 @@ export interface ObjectiveDraft {
|
|
|
216
262
|
base?: string;
|
|
217
263
|
// The reviewed delivery choice; kept only when exactly the enum (junk → absent, like `base`).
|
|
218
264
|
delivery?: DeliveryChoice;
|
|
265
|
+
// The dream-report block (§8.63); a present-but-malformed block refuses the WHOLE draft.
|
|
266
|
+
dream_report?: ObjectiveDreamReportBlock;
|
|
219
267
|
}
|
|
220
268
|
|
|
221
269
|
/**
|
|
@@ -223,7 +271,10 @@ export interface ObjectiveDraft {
|
|
|
223
271
|
* `readSessionArtifact`'s loud tier): no pointer/file/digest → `null` (the seam already spoke);
|
|
224
272
|
* malformed JSON, a non-object payload, an unsupported `schema_version`, or blank prose → a
|
|
225
273
|
* stderr warning + `null`. `roadmap` defaults to `[]` when absent/non-array; `title` is kept
|
|
226
|
-
* only when a non-blank string.
|
|
274
|
+
* only when a non-blank string. A present-but-malformed `dream_report` block refuses the WHOLE
|
|
275
|
+
* draft (warn + `null`) — deliberately stricter than the lenient junk→absent handling of
|
|
276
|
+
* `base`/`delivery`, because silently dropping a malformed report is exactly what §8.63
|
|
277
|
+
* forbids. Never throws.
|
|
227
278
|
*/
|
|
228
279
|
export function readObjectiveDraft(ctx: SessionDataCtx): ObjectiveDraft | null {
|
|
229
280
|
const artifact = readSessionArtifact(ctx, OBJECTIVE_DRAFT_ARTIFACT);
|
|
@@ -258,10 +309,19 @@ export function readObjectiveDraft(ctx: SessionDataCtx): ObjectiveDraft | null {
|
|
|
258
309
|
payload.delivery === "incremental" || payload.delivery === "stacked"
|
|
259
310
|
? payload.delivery
|
|
260
311
|
: undefined;
|
|
312
|
+
let dreamReport: ObjectiveDreamReportBlock | undefined;
|
|
313
|
+
if ("dream_report" in payload) {
|
|
314
|
+
const block = decodeDreamReportBlock(payload.dream_report);
|
|
315
|
+
if (block === null) {
|
|
316
|
+
return refuse("carries a malformed dream_report block");
|
|
317
|
+
}
|
|
318
|
+
dreamReport = block;
|
|
319
|
+
}
|
|
261
320
|
return {
|
|
262
321
|
...(title !== undefined ? { title } : {}),
|
|
263
322
|
...(base !== undefined ? { base } : {}),
|
|
264
323
|
...(delivery !== undefined ? { delivery } : {}),
|
|
324
|
+
...(dreamReport !== undefined ? { dream_report: dreamReport } : {}),
|
|
265
325
|
prose,
|
|
266
326
|
roadmap,
|
|
267
327
|
};
|
|
@@ -294,7 +354,10 @@ function nodeDependsOn(node: unknown): string {
|
|
|
294
354
|
* a `## Roadmap` section with ONE markdown table. A prominent `**Delivery:**` line renders
|
|
295
355
|
* directly under the title unconditionally (the reviewed choice must be visible either way —
|
|
296
356
|
* contracts §8.45). The `Phase` column appears only when some node carries a non-blank string
|
|
297
|
-
* `phase`.
|
|
357
|
+
* `phase`. When the draft carries a `dream_report` block, the stored CANONICAL parts append as
|
|
358
|
+
* the final section — the review surface IS the approval bundle: the objective and its report
|
|
359
|
+
* review (and are approved or denied) together (§8.63); the parts carry their own
|
|
360
|
+
* `# Dream report — <run_id>` headers. Pure; never throws.
|
|
298
361
|
*/
|
|
299
362
|
/** The always-present prominent `**Delivery:**` review line (contracts §8.45). */
|
|
300
363
|
function deliveryLine(draft: ObjectiveDraft): string {
|
|
@@ -313,23 +376,27 @@ export function renderObjectiveDraft(draft: ObjectiveDraft): string {
|
|
|
313
376
|
out += `${deliveryLine(draft)}\n\n`;
|
|
314
377
|
out += draft.prose;
|
|
315
378
|
|
|
316
|
-
if (draft.roadmap.length
|
|
379
|
+
if (draft.roadmap.length > 0) {
|
|
380
|
+
const withPhase = draft.roadmap.some((node) => nodeString(node, "phase").trim().length > 0);
|
|
381
|
+
const header = withPhase
|
|
382
|
+
? "| Node | Phase | Description | Depends On | Status |\n| --- | --- | --- | --- | --- |"
|
|
383
|
+
: "| Node | Description | Depends On | Status |\n| --- | --- | --- | --- |";
|
|
384
|
+
const rows = draft.roadmap.map((node) => {
|
|
385
|
+
const cells = [
|
|
386
|
+
tableCell(nodeString(node, "id")),
|
|
387
|
+
...(withPhase ? [tableCell(nodeString(node, "phase"))] : []),
|
|
388
|
+
tableCell(nodeString(node, "description")),
|
|
389
|
+
tableCell(nodeDependsOn(node)),
|
|
390
|
+
tableCell(nodeString(node, "status") || "pending"),
|
|
391
|
+
];
|
|
392
|
+
return `| ${cells.join(" | ")} |`;
|
|
393
|
+
});
|
|
394
|
+
out = `${out.trimEnd()}\n\n## Roadmap\n\n${header}\n${rows.join("\n")}\n`;
|
|
395
|
+
}
|
|
317
396
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
: "| Node | Description | Depends On | Status |\n| --- | --- | --- | --- |";
|
|
322
|
-
const rows = draft.roadmap.map((node) => {
|
|
323
|
-
const cells = [
|
|
324
|
-
tableCell(nodeString(node, "id")),
|
|
325
|
-
...(withPhase ? [tableCell(nodeString(node, "phase"))] : []),
|
|
326
|
-
tableCell(nodeString(node, "description")),
|
|
327
|
-
tableCell(nodeDependsOn(node)),
|
|
328
|
-
tableCell(nodeString(node, "status") || "pending"),
|
|
329
|
-
];
|
|
330
|
-
return `| ${cells.join(" | ")} |`;
|
|
331
|
-
});
|
|
332
|
-
return `${out.trimEnd()}\n\n## Roadmap\n\n${header}\n${rows.join("\n")}\n`;
|
|
397
|
+
if (draft.dream_report === undefined) return out;
|
|
398
|
+
// The approval bundle: objective first, then the stored CANONICAL report parts.
|
|
399
|
+
return `${out.trimEnd()}\n\n${draft.dream_report.parts.join("\n\n")}\n`;
|
|
333
400
|
}
|
|
334
401
|
|
|
335
402
|
const TOOL_GUIDELINES = [
|
|
@@ -371,6 +438,7 @@ export function registerObjectiveDraft(pi: ExtensionAPI): void {
|
|
|
371
438
|
"Optional target branch for this objective's plans (omit to use the repo default).",
|
|
372
439
|
},
|
|
373
440
|
delivery: DELIVERY_PARAM_SCHEMA,
|
|
441
|
+
dream_report: DREAM_REPORT_PARAM_SCHEMA,
|
|
374
442
|
roadmap: {
|
|
375
443
|
type: "array",
|
|
376
444
|
description:
|