@mgiles/perk 2.3.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +1 -1
  2. package/extension/adapters/planAdapterPlannotator.ts +135 -91
  3. package/extension/adapters/planAdapterTombell.ts +5 -3
  4. package/extension/doors/address.ts +235 -37
  5. package/extension/doors/annotationPush.ts +924 -0
  6. package/extension/doors/auditWaveTools.ts +352 -0
  7. package/extension/doors/ciExecutor.ts +220 -55
  8. package/extension/doors/commitCompact.ts +98 -10
  9. package/extension/doors/draftReviewWaveTools.ts +412 -0
  10. package/extension/doors/dreamWaveTools.ts +475 -0
  11. package/extension/doors/harvestWaveTools.ts +279 -0
  12. package/extension/doors/land.ts +1 -0
  13. package/extension/doors/learn.ts +24 -6
  14. package/extension/doors/learnFactory.ts +33 -7
  15. package/extension/doors/lifecycleGates.ts +1 -1
  16. package/extension/doors/objectiveReviewBrowser.ts +537 -0
  17. package/extension/doors/objectiveStack.ts +1143 -0
  18. package/extension/doors/planReviewBrowser.ts +475 -0
  19. package/extension/doors/plannotatorHandoff.ts +120 -49
  20. package/extension/doors/prReview.ts +180 -63
  21. package/extension/doors/prReviewBrowser.ts +31 -23
  22. package/extension/doors/prReviewDynamic.ts +57 -30
  23. package/extension/doors/prReviewTerminal.ts +6 -11
  24. package/extension/doors/reviewWaveTools.ts +397 -0
  25. package/extension/doors/selfcheck.ts +1 -1
  26. package/extension/doors/submit.ts +82 -7
  27. package/extension/factories/gistAuthor.ts +4 -4
  28. package/extension/factories/gistDraft.ts +1 -1
  29. package/extension/factories/gistSave.ts +2 -2
  30. package/extension/factories/objective.ts +7 -7
  31. package/extension/factories/objectiveAuthor.ts +5 -4
  32. package/extension/factories/objectiveDraft.ts +153 -30
  33. package/extension/factories/objectiveDreamReport.ts +347 -0
  34. package/extension/factories/objectivePlan.ts +163 -9
  35. package/extension/factories/objectiveSave.ts +90 -2
  36. package/extension/factories/planMode.ts +5 -3
  37. package/extension/factories/planReview.ts +304 -46
  38. package/extension/factories/planTitle.ts +27 -9
  39. package/extension/hunkFeedback/inbox.ts +389 -0
  40. package/extension/hunkFeedback/perkFeedback.ts +364 -0
  41. package/extension/hunkFeedback/receiver.ts +296 -0
  42. package/extension/hunkFeedback/store.ts +533 -0
  43. package/extension/index.ts +169 -57
  44. package/extension/substrate/agentScratch.ts +171 -0
  45. package/extension/substrate/bindingDelivery.ts +9 -11
  46. package/extension/substrate/cache.ts +166 -23
  47. package/extension/substrate/coldDoor.ts +2 -3
  48. package/extension/substrate/command.ts +9 -6
  49. package/extension/substrate/config.ts +41 -12
  50. package/extension/substrate/git.ts +95 -2
  51. package/extension/substrate/providers.ts +4 -33
  52. package/extension/substrate/resources.ts +11 -0
  53. package/extension/substrate/result.ts +16 -7
  54. package/extension/substrate/sessionData.ts +8 -6
  55. package/extension/substrate/sessionPointers.ts +4 -5
  56. package/extension/substrate/structuredOutput.ts +78 -19
  57. package/extension/substrate/toolGating.ts +179 -23
  58. package/extension/substrate/workflowState.ts +50 -2
  59. package/extension/surfaces/footerProvider.ts +6 -6
  60. package/extension/surfaces/report.ts +38 -12
  61. package/extension/surfaces/surfaces.ts +213 -275
  62. package/extension/vendor/btw/btw.ts +94 -17
  63. package/extension/waves/adversarialReviewWave.ts +181 -0
  64. package/extension/waves/auditWave.ts +312 -0
  65. package/extension/waves/draftReviewWave.ts +205 -0
  66. package/extension/waves/dreamReducerWave.ts +700 -0
  67. package/extension/waves/dreamReport.ts +1494 -0
  68. package/extension/waves/dreamWave.ts +927 -0
  69. package/extension/waves/harvestWave.ts +399 -0
  70. package/extension/waves/memoryAdapter.ts +14 -1
  71. package/extension/waves/objectiveExplorerWave.ts +120 -0
  72. package/extension/waves/ponytail.ts +104 -0
  73. package/extension/waves/prReviewDynamicWave.ts +383 -72
  74. package/extension/waves/prReviewWave.ts +175 -25
  75. package/extension/waves/reportWave.ts +503 -107
  76. package/extension/waves/reviewClassifierWave.ts +109 -0
  77. package/extension/waves/rpcAdapter.ts +65 -2
  78. package/extension/worker/readOnlySession.ts +4 -5
  79. package/extension/worker/worker.ts +71 -93
  80. package/extension/workerMain.ts +4 -6
  81. package/package.json +18 -5
  82. package/prompts/README.md +19 -0
  83. package/prompts/_fixtures/live.yaml +115 -65
  84. package/prompts/commit-and-compact-continuation.md +13 -0
  85. package/prompts/common/resume-advisory.md +1 -0
  86. package/prompts/contexts/adapters/plannotator-gist.md +13 -0
  87. package/prompts/contexts/adapters/plannotator-objective.md +8 -3
  88. package/prompts/contexts/adapters/plannotator-plan.md +8 -3
  89. package/prompts/contexts/gist-authoring.md +6 -20
  90. package/prompts/contexts/objective-authoring.md +6 -18
  91. package/prompts/stages/address/action.md +3 -14
  92. package/prompts/stages/address/preview.md +2 -13
  93. package/prompts/stages/audit.md +18 -0
  94. package/prompts/stages/conflict-resolution.md +1 -1
  95. package/prompts/stages/gist-author/seed.md +4 -4
  96. package/prompts/stages/gist-save.md +1 -1
  97. package/prompts/stages/implement.md +3 -1
  98. package/prompts/stages/learn-code.md +1 -1
  99. package/prompts/stages/learn-docs.md +2 -2
  100. package/prompts/stages/learn-dream.md +10 -0
  101. package/prompts/stages/learn-harvest.md +15 -0
  102. package/prompts/stages/learn-orchestrate.md +1 -1
  103. package/prompts/stages/objective-author/adopt.md +5 -3
  104. package/prompts/stages/objective-author/file.md +5 -4
  105. package/prompts/stages/objective-author/seed.md +5 -4
  106. package/prompts/stages/objective-land.md +7 -0
  107. package/prompts/stages/objective-plan/guidance.md +2 -13
  108. package/prompts/stages/objective-plan/seed.md +5 -12
  109. package/prompts/stages/objective-recover.md +8 -0
  110. package/prompts/stages/objective-replan.md +4 -2
  111. package/prompts/stages/objective-review-browser.md +9 -0
  112. package/prompts/stages/objective-sync.md +6 -0
  113. package/prompts/stages/plan-from/adopt.md +2 -2
  114. package/prompts/stages/plan-from/file.md +2 -2
  115. package/prompts/stages/plan-review-browser.md +9 -0
  116. package/prompts/stages/pr-review-browser/active.md +6 -14
  117. package/prompts/stages/pr-review-browser/foreign.md +6 -14
  118. package/prompts/stages/pr-review-dynamic.md +6 -6
  119. package/prompts/stages/pr-review-terminal/active.md +7 -15
  120. package/prompts/stages/pr-review-terminal/foreign.md +7 -15
  121. package/prompts/stages/pr-review-terminal/local.md +1 -1
  122. package/prompts/stages/pr-review.md +5 -5
  123. package/prompts/stages/replan.md +3 -3
  124. package/shared/README.md +2 -2
  125. package/shared/bindings.yaml +15 -0
  126. package/shared/contracts-history.md +11 -0
  127. package/shared/contracts.md +5373 -281
  128. package/shared/providers.yaml +27 -60
  129. package/shared/registry.yaml +42 -18
  130. package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
  131. package/shared/schemas/outputs/objective-doctor.schema.json +487 -0
  132. package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
  133. package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
  134. package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
  135. package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
  136. package/shared/schemas/outputs/plan-save.schema.json +12 -0
  137. package/shared/schemas/outputs/pr-land.schema.json +3 -3
  138. package/shared/schemas/outputs/pr-submit.schema.json +177 -1
  139. package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
  140. package/extension/checkpoints/checkpoints.ts +0 -550
  141. package/extension/checkpoints/planSteps.ts +0 -108
  142. package/extension/doors/askUser.ts +0 -198
  143. package/prompts/common/output-schemas/objective-explorer.md +0 -36
  144. package/prompts/common/output-schemas/review-classifier.md +0 -47
  145. package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
@@ -22,7 +22,7 @@ import {
22
22
  stringField,
23
23
  } from "../substrate/coldDoor.ts";
24
24
  import { registerPerkCommand } from "../substrate/command.ts";
25
- import { loadPerkConfig } from "../substrate/config.ts";
25
+ import { subagentModel } from "../substrate/config.ts";
26
26
  import { render } from "../substrate/prompts.ts";
27
27
  import { failFor, type OkDetails, ok, type Result } from "../substrate/result.ts";
28
28
  import { captureSessionPointer } from "../substrate/sessionPointers.ts";
@@ -44,6 +44,18 @@ export interface SubmitOk {
44
44
  mergeable?: boolean | null;
45
45
  /** The conflicted paths when `mergeable === false`; `[]` otherwise (advisory). */
46
46
  conflicts?: string[];
47
+ /** `"stacked"` when the submit routed through the delivery publish operation (§8.47). */
48
+ delivery?: string;
49
+ /** The native-stack facts of a stacked submit (absent for the bottom layer). */
50
+ stack?: { number: number; size: number; position: number };
51
+ /** Lenient summary of an automatic suffix synchronization. */
52
+ operation?: {
53
+ kind: string;
54
+ operation_id: string | null;
55
+ no_op: boolean;
56
+ affected_count: number;
57
+ notes: string[];
58
+ };
47
59
  }
48
60
 
49
61
  export type SubmitResult = Result<SubmitOk>;
@@ -67,6 +79,51 @@ function conflictsField(payload: ColdJson): string[] {
67
79
  return [];
68
80
  }
69
81
 
82
+ /**
83
+ * A lenient read of the advisory stacked `stack` facts: all three numbers or nothing — a
84
+ * malformed value must NOT sink a successful submit decode (it just drops the suffix).
85
+ */
86
+ function stackField(payload: ColdJson): SubmitOk["stack"] {
87
+ const value = objectField(payload, "stack");
88
+ if (value === undefined) return undefined;
89
+ const number = numberField(value, "number");
90
+ const size = numberField(value, "size");
91
+ const position = numberField(value, "position");
92
+ if (number === undefined || size === undefined || position === undefined) return undefined;
93
+ return { number, size, position };
94
+ }
95
+
96
+ /**
97
+ * Lenient all-or-nothing decode of the cascade operation block. The full affected rows remain a
98
+ * Python/CLI detail; the warm door needs only their count plus the recovery notes.
99
+ */
100
+ function operationField(payload: ColdJson): SubmitOk["operation"] {
101
+ const value = objectField(payload, "operation");
102
+ if (value === undefined) return undefined;
103
+ const kind = stringField(value, "kind");
104
+ const operationId = value.operation_id;
105
+ const noOp = booleanField(value, "no_op");
106
+ const affected = value.affected;
107
+ const notes = value.notes;
108
+ if (
109
+ kind === undefined ||
110
+ (typeof operationId !== "string" && operationId !== null) ||
111
+ noOp === undefined ||
112
+ !Array.isArray(affected) ||
113
+ !Array.isArray(notes) ||
114
+ !notes.every((note) => typeof note === "string")
115
+ ) {
116
+ return undefined;
117
+ }
118
+ return {
119
+ kind,
120
+ operation_id: operationId,
121
+ no_op: noOp,
122
+ affected_count: affected.length,
123
+ notes: notes as string[],
124
+ };
125
+ }
126
+
70
127
  /**
71
128
  * Narrow the `perk pr submit --json` success payload; strict on `pr`, lenient on the rest. The
72
129
  * `base`/`mergeable`/`conflicts` mergeability fields are advisory (mirror land.ts's lenient
@@ -90,12 +147,15 @@ function decodeSubmit(payload: ColdJson): SubmitOk | null {
90
147
  base: stringField(payload, "base"),
91
148
  mergeable: mergeableField(payload),
92
149
  conflicts: conflictsField(payload),
150
+ delivery: stringField(payload, "delivery"),
151
+ stack: stackField(payload),
152
+ operation: operationField(payload),
93
153
  };
94
154
  }
95
155
 
96
- /** A definitively-unmergeable submit: the gate fires only on a *definitive* `false` + conflicts. */
156
+ /** A definitively-unmergeable submit; parsed conflict paths are advisory only. */
97
157
  function isUnmergeable(details: SubmitDetails): details is OkDetails<SubmitOk> {
98
- return details.ok && details.mergeable === false && (details.conflicts?.length ?? 0) > 0;
158
+ return details.ok && details.mergeable === false;
99
159
  }
100
160
 
101
161
  /**
@@ -140,15 +200,30 @@ export async function submitPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise
140
200
  }
141
201
 
142
202
  const verb = r.data.pr.existed ? "Found existing" : "Opened draft";
143
- const conflicted = r.data.mergeable === false && (r.data.conflicts?.length ?? 0) > 0;
203
+ const conflicted = r.data.mergeable === false;
144
204
  // Reset the counter on every clean (or undetermined) submit — idempotent; keeps a later
145
205
  // independent conflict bounded fresh.
146
206
  if (r.data.mergeable !== false) resetConflictAttempts(pi, ctx);
207
+ // Automatic-cascade facts supersede the generic stacked suffix. A malformed operation block was
208
+ // dropped by the lenient decoder, so it falls back to the pre-existing stack wording.
209
+ const deliverySuffix =
210
+ r.data.operation?.kind === "sync"
211
+ ? r.data.operation.no_op
212
+ ? " (suffix already in sync)"
213
+ : ` (cascaded ${r.data.operation.affected_count} layer(s))`
214
+ : r.data.stack
215
+ ? ` (stack #${r.data.stack.number}, layer ${r.data.stack.position}/${r.data.stack.size})`
216
+ : r.data.delivery === "stacked"
217
+ ? " (stacked layer)"
218
+ : "";
219
+ for (const note of r.data.operation?.notes ?? []) {
220
+ report(ctx, "submit", "warning", note);
221
+ }
147
222
  const message = conflicted
148
- ? `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} — merge conflicts detected; resolving`
223
+ ? `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} — merge conflicts detected; resolving${deliverySuffix}`
149
224
  : `${verb} PR #${r.data.pr.number} → ${r.data.pr.url} (${
150
225
  r.data.plan_embedded ? "plan embedded" : "no plan embed"
151
- })`;
226
+ })${deliverySuffix}`;
152
227
  return ok(message, r.data, { terminate: true });
153
228
  }
154
229
 
@@ -221,7 +296,7 @@ export function driveConflictResolution(
221
296
  scope: "submit",
222
297
  failure: `conflict_resolution_attempts read-back failed (expected ${next})`,
223
298
  });
224
- const model = loadPerkConfig(ctx.cwd).subagents["conflict-resolver"];
299
+ const model = subagentModel(ctx.cwd, "conflict-resolver");
225
300
  const message =
226
301
  conflictResolutionGuidance(base, next, CONFLICT_RESOLUTION_ATTEMPT_CAP, model) +
227
302
  bindingSuffix(ctx.cwd, "command:submit");
@@ -28,10 +28,10 @@ export const GIST_AUTHOR_CONTEXT_TYPE = "perk:gist-author-context";
28
28
  const GIST_AUTHOR_MARKER = "[GIST AUTHORING]";
29
29
 
30
30
  /**
31
- * The cooperative gather-then-author contract for gists. Prompting, NOT enforcement (the tool
32
- * gate is the enforcement). Mirrors skills/perk-gist-author/SKILL.md: clarify the intent,
33
- * explore lightly, keep the draft current with `gist_draft`, review via `plan_review`, approval
34
- * auto-saves no implementation strategy in the artifact.
31
+ * The gist-authoring session context: live state + pointers only (contracts.md §8.57 — the flow
32
+ * is stated by the launch statement, the detail by the `perk-gist-author` skill). It names the
33
+ * working-draft artifact (`gist_draft`), the review tool (`plan_review`), and the bound skill;
34
+ * it never restates the flow. Prompting, NOT enforcement (the tool gate is the enforcement).
35
35
  */
36
36
  export const GIST_AUTHORING_CONTEXT = render("contexts/gist-authoring.md", {
37
37
  marker: GIST_AUTHOR_MARKER,
@@ -232,7 +232,7 @@ export function registerGistDraft(pi: ExtensionAPI): void {
232
232
  type: "string",
233
233
  description:
234
234
  "The gist prose (the problem-space intent: what we want, why it matters, what " +
235
- "bounds it — no implementation steps).",
235
+ "bounds it, and any high-level solution leanings — no implementation steps).",
236
236
  },
237
237
  title: {
238
238
  type: "string",
@@ -155,7 +155,7 @@ export async function gistApprovalSave(
155
155
 
156
156
  const TOOL_GUIDELINES = [
157
157
  "Use gist_save only after the gist says what it means; it creates the tracked gist in the issue backend and ends the turn.",
158
- "Pass gist_save the statement-of-intent PROSE in `prose` — problem-space only, no implementation steps or roadmap.",
158
+ "Pass gist_save the statement-of-intent PROSE in `prose` — problem-focused, with at most high-level solution leanings; no implementation steps or roadmap.",
159
159
  "Pass gist_save's `scope` only once the consumption tier is settled (plan or objective); omit it to keep the pre-seeded/default scope.",
160
160
  ];
161
161
 
@@ -190,7 +190,7 @@ export function registerGistSave(pi: ExtensionAPI, gating: ToolGating): void {
190
190
  type: "string",
191
191
  description:
192
192
  "The gist prose (the problem-space intent: what we want, why it matters, what " +
193
- "bounds it — no implementation steps).",
193
+ "bounds it, and any high-level solution leanings — no implementation steps).",
194
194
  },
195
195
  title: {
196
196
  type: "string",
@@ -7,8 +7,8 @@
7
7
  // the "fire only when…" tools live elsewhere. This module ships three pieces, all inert when no
8
8
  // objective is active:
9
9
  // 1. `/objective [<id>|clear]` — set/clear `active_objective` (LWW field) + seed a dedicated
10
- // `perk:objective-budget` activation marker (high-churn budget data kept OFF the shared record,
11
- // mirroring checkpoints' dedicated entry).
10
+ // `perk:objective-budget` activation marker (high-churn budget data kept OFF the shared
11
+ // record).
12
12
  // 2. Budget accounting — stateless rebuild (the goal.ts pattern): sum assistant-message tokens
13
13
  // AFTER the latest activation marker; surface via ctx.ui guarded by ctx.hasUI; rebuilt on
14
14
  // session_start AND session_tree AND agent_settled (survives reload/branch/compaction for
@@ -19,7 +19,7 @@
19
19
  // objective is active, compact when context usage crosses a configurable threshold.
20
20
  //
21
21
  // Headless-fail-safe: every UI call is ctx.hasUI-guarded; budget accounting + compaction are
22
- // best-effort and never throw (logged-not-thrown, like checkpoints).
22
+ // best-effort and never throw (logged-not-thrown).
23
23
 
24
24
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
25
25
  import { registerPerkCommand } from "../substrate/command.ts";
@@ -131,8 +131,8 @@ function activeObjective(ctx: ExtensionContext): string | null {
131
131
  }
132
132
 
133
133
  /**
134
- * Surface the budget as the objective segment of the composed `perk` status (the `perk-objective`
135
- * widget is retired; the segment carries id + tokens + elapsed). Headless-safe:
134
+ * Surface the budget as the single-value `perk` status (the `perk-objective`
135
+ * widget is retired; the value carries id + tokens + elapsed). Headless-safe:
136
136
  * the handle no-ops without UI.
137
137
  */
138
138
  function renderStatus(ctx: ExtensionContext, status: PerkStatusHandle): void {
@@ -140,11 +140,11 @@ function renderStatus(ctx: ExtensionContext, status: PerkStatusHandle): void {
140
140
  try {
141
141
  const active = activeObjective(ctx);
142
142
  if (active === null) {
143
- status.set(ctx, "objective", undefined);
143
+ status.set(ctx, undefined);
144
144
  return;
145
145
  }
146
146
  const budget = rebuildBudget(scanBranchOf(ctx), Date.now());
147
- status.set(ctx, "objective", `${MARK_OBJECTIVE} ${active} · ${formatBudgetLine(budget)}`);
147
+ status.set(ctx, `${MARK_OBJECTIVE} ${active} · ${formatBudgetLine(budget)}`);
148
148
  } catch (error) {
149
149
  console.error(`perk: objective status render failed — ${error}`);
150
150
  }
@@ -29,10 +29,11 @@ export const OBJECTIVE_AUTHOR_CONTEXT_TYPE = "perk:objective-author-context";
29
29
  const OBJECTIVE_AUTHOR_MARKER = "[OBJECTIVE AUTHORING]";
30
30
 
31
31
  /**
32
- * The cooperative gather-then-author contract for objectives. Prompting, NOT enforcement (the tool
33
- * gate is the enforcement). Mirrors skills/perk-objective-author/SKILL.md: clarify the goal, explore
34
- * read-only, structure a roadmap, keep the draft current with `objective_draft`, review via
35
- * `plan_review`, approval auto-saves never hand-write roadmap YAML.
32
+ * The objective-authoring session context: live state + pointers only (contracts.md §8.57 — the
33
+ * flow is stated by the launch statement, the detail by the `perk-objective-author` skill). It
34
+ * names the working-draft artifact (`objective_draft`), the review tool (`plan_review`), and
35
+ * the bound skill; it never restates the flow. Prompting, NOT enforcement (the tool gate is the
36
+ * enforcement).
36
37
  */
37
38
  export const OBJECTIVE_AUTHORING_CONTEXT = render("contexts/objective-authoring.md", {
38
39
  marker: OBJECTIVE_AUTHOR_MARKER,
@@ -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}` the structured roadmap rides
15
- // verbatim (node-shape validation stays with the Python plane at save time, the
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`) — objectiveDraft is
24
- // the LEAF (mirroring planDraft←planSave's direction); objectiveSave.ts consumes it, so it may
25
- // value-import `readObjectiveDraft` cycle-free for the approval→save orchestration.
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,18 @@ 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";
51
+
52
+ /** The reviewed objective delivery choice (contracts §8.45). */
53
+ export type DeliveryChoice = "incremental" | "stacked";
43
54
 
44
55
  /** The decoded `objective_save` tool params (shared with `objective_draft`). */
45
56
  export interface ObjectiveSaveParams {
@@ -48,8 +59,41 @@ export interface ObjectiveSaveParams {
48
59
  roadmap?: unknown[];
49
60
  // The objective's target branch; omitted to use the repo default.
50
61
  base?: string;
62
+ // The reviewed delivery choice; omitted ⇒ incremental (the §8.42 absence rule).
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;
51
67
  }
52
68
 
69
+ /**
70
+ * The `delivery` enum property, shared between `objective_save` and `objective_draft` so the
71
+ * two tools' delivery contracts cannot drift. The description bakes in the explicit-human-choice
72
+ * discipline: the agent must ASK, with incremental recommended.
73
+ */
74
+ export const DELIVERY_PARAM_SCHEMA = {
75
+ type: "string",
76
+ enum: ["incremental", "stacked"],
77
+ description:
78
+ "The reviewed delivery choice — ask the human explicitly (incremental is the recommended " +
79
+ "default: each plan lands independently; stacked lands ALL non-skipped roadmap nodes as " +
80
+ "one atomic PR train — capability-checked at save).",
81
+ } as const;
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
+
53
97
  /**
54
98
  * The roadmap-node items JSON schema, shared between `objective_save` and `objective_draft`
55
99
  * so the two tools' roadmap contracts cannot drift.
@@ -96,8 +140,23 @@ export function decodeObjectiveSaveParams(params: unknown): ObjectiveSaveParams
96
140
  const title = stringParam(p, "title");
97
141
  const roadmap = arrayParam(p, "roadmap");
98
142
  const base = stringParam(p, "base");
99
- if (prose === null || title === null || roadmap === null || base === null) return null;
100
- return { prose: prose ?? "", title, roadmap, base };
143
+ const delivery = stringParam(p, "delivery");
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
+ ) {
155
+ return null;
156
+ }
157
+ // The delivery enum is strict beyond `string`: an off-enum value is present-but-mistyped.
158
+ if (delivery !== undefined && delivery !== "incremental" && delivery !== "stacked") return null;
159
+ return { prose: prose ?? "", title, roadmap, base, delivery, dream_report: dreamReport };
101
160
  }
102
161
 
103
162
  /** The fixed working-objective artifact name (one JSON file: prose + the structured roadmap). */
@@ -120,12 +179,21 @@ export type ObjectiveDraftResult = Result<ObjectiveDraftOk>;
120
179
  * structured roadmap, verbatim — the draft never validates node shapes) as one JSON artifact and
121
180
  * write it through the accessor seam (file + `session_artifacts` provenance pointer). Soft
122
181
  * result, never throws — failure taxonomy: empty prose → `invalid_input`; no session run_id →
123
- * `no_run_id`; file-or-pointer write failure → `write_failed` (the seam already warned).
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).
124
185
  */
125
186
  export function writeObjectiveDraft(
126
187
  sink: EntrySink,
127
188
  ctx: SessionDataCtx & ReportTarget,
128
- opts: { prose: string; title?: string; roadmap?: unknown[]; base?: string },
189
+ opts: {
190
+ prose: string;
191
+ title?: string;
192
+ roadmap?: unknown[];
193
+ base?: string;
194
+ delivery?: DeliveryChoice;
195
+ dream_report?: unknown;
196
+ },
129
197
  ): ObjectiveDraftResult {
130
198
  const fail = failFor(ctx, "objective-draft");
131
199
 
@@ -138,14 +206,25 @@ export function writeObjectiveDraft(
138
206
  return fail("session has no run_id — cannot write the objective-draft artifact", "no_run_id");
139
207
  }
140
208
 
141
- // Deterministic key order via the explicit literal; `title`/`base` are omitted when blank.
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
+
216
+ // Deterministic key order via the explicit literal; `title`/`base`/`delivery` are omitted
217
+ // when blank/absent (schema_version stays 1 — an additive optional field, fail-open readers).
142
218
  const title = opts.title?.trim();
143
219
  const base = opts.base?.trim();
220
+ const delivery = opts.delivery;
144
221
  const roadmap = opts.roadmap ?? [];
145
222
  const payload = {
146
223
  schema_version: 1,
147
224
  ...(title ? { title } : {}),
148
225
  ...(base ? { base } : {}),
226
+ ...(delivery ? { delivery } : {}),
227
+ ...(gate.kind === "block" ? { dream_report: gate.block } : {}),
149
228
  prose: opts.prose,
150
229
  roadmap,
151
230
  };
@@ -181,6 +260,10 @@ export interface ObjectiveDraft {
181
260
  roadmap: unknown[];
182
261
  // The objective's target branch; kept only when a non-blank string in the artifact.
183
262
  base?: string;
263
+ // The reviewed delivery choice; kept only when exactly the enum (junk → absent, like `base`).
264
+ delivery?: DeliveryChoice;
265
+ // The dream-report block (§8.63); a present-but-malformed block refuses the WHOLE draft.
266
+ dream_report?: ObjectiveDreamReportBlock;
184
267
  }
185
268
 
186
269
  /**
@@ -188,7 +271,10 @@ export interface ObjectiveDraft {
188
271
  * `readSessionArtifact`'s loud tier): no pointer/file/digest → `null` (the seam already spoke);
189
272
  * malformed JSON, a non-object payload, an unsupported `schema_version`, or blank prose → a
190
273
  * stderr warning + `null`. `roadmap` defaults to `[]` when absent/non-array; `title` is kept
191
- * only when a non-blank string. Never throws.
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.
192
278
  */
193
279
  export function readObjectiveDraft(ctx: SessionDataCtx): ObjectiveDraft | null {
194
280
  const artifact = readSessionArtifact(ctx, OBJECTIVE_DRAFT_ARTIFACT);
@@ -219,9 +305,23 @@ export function readObjectiveDraft(ctx: SessionDataCtx): ObjectiveDraft | null {
219
305
  const title =
220
306
  typeof payload.title === "string" && payload.title.trim() ? payload.title : undefined;
221
307
  const base = typeof payload.base === "string" && payload.base.trim() ? payload.base : undefined;
308
+ const delivery =
309
+ payload.delivery === "incremental" || payload.delivery === "stacked"
310
+ ? payload.delivery
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
+ }
222
320
  return {
223
321
  ...(title !== undefined ? { title } : {}),
224
322
  ...(base !== undefined ? { base } : {}),
323
+ ...(delivery !== undefined ? { delivery } : {}),
324
+ ...(dreamReport !== undefined ? { dream_report: dreamReport } : {}),
225
325
  prose,
226
326
  roadmap,
227
327
  };
@@ -251,31 +351,52 @@ function nodeDependsOn(node: unknown): string {
251
351
  /**
252
352
  * Render the draft as the markdown review surface (JSON is storage/transport only — contracts
253
353
  * §8.1): the optional `# title` heading, the prose verbatim, and (when the roadmap is non-empty)
254
- * a `## Roadmap` section with ONE markdown table. The `Phase` column appears only when some node
255
- * carries a non-blank string `phase`. Pure; never throws.
354
+ * a `## Roadmap` section with ONE markdown table. A prominent `**Delivery:**` line renders
355
+ * directly under the title unconditionally (the reviewed choice must be visible either way —
356
+ * contracts §8.45). The `Phase` column appears only when some node carries a non-blank string
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.
256
361
  */
362
+ /** The always-present prominent `**Delivery:**` review line (contracts §8.45). */
363
+ function deliveryLine(draft: ObjectiveDraft): string {
364
+ if (draft.delivery === "stacked") {
365
+ return (
366
+ "**Delivery: STACKED** — all non-skipped roadmap nodes land as ONE atomic pull-request " +
367
+ "train (capability-checked at save)"
368
+ );
369
+ }
370
+ return "**Delivery: incremental** (the default — each plan lands independently)";
371
+ }
372
+
257
373
  export function renderObjectiveDraft(draft: ObjectiveDraft): string {
258
374
  let out = "";
259
375
  if (draft.title) out += `# ${draft.title}\n\n`;
376
+ out += `${deliveryLine(draft)}\n\n`;
260
377
  out += draft.prose;
261
378
 
262
- if (draft.roadmap.length === 0) return out;
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
+ }
263
396
 
264
- const withPhase = draft.roadmap.some((node) => nodeString(node, "phase").trim().length > 0);
265
- const header = withPhase
266
- ? "| Node | Phase | Description | Depends On | Status |\n| --- | --- | --- | --- | --- |"
267
- : "| Node | Description | Depends On | Status |\n| --- | --- | --- | --- |";
268
- const rows = draft.roadmap.map((node) => {
269
- const cells = [
270
- tableCell(nodeString(node, "id")),
271
- ...(withPhase ? [tableCell(nodeString(node, "phase"))] : []),
272
- tableCell(nodeString(node, "description")),
273
- tableCell(nodeDependsOn(node)),
274
- tableCell(nodeString(node, "status") || "pending"),
275
- ];
276
- return `| ${cells.join(" | ")} |`;
277
- });
278
- 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`;
279
400
  }
280
401
 
281
402
  const TOOL_GUIDELINES = [
@@ -316,6 +437,8 @@ export function registerObjectiveDraft(pi: ExtensionAPI): void {
316
437
  description:
317
438
  "Optional target branch for this objective's plans (omit to use the repo default).",
318
439
  },
440
+ delivery: DELIVERY_PARAM_SCHEMA,
441
+ dream_report: DREAM_REPORT_PARAM_SCHEMA,
319
442
  roadmap: {
320
443
  type: "array",
321
444
  description: