@mgiles/perk 3.1.0 → 3.3.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 (211) hide show
  1. package/README.md +5 -0
  2. package/extension/authoring/gist/draft.ts +198 -0
  3. package/extension/authoring/gist/prose.ts +46 -0
  4. package/extension/authoring/gist/review.ts +133 -0
  5. package/extension/authoring/gist/save.ts +118 -0
  6. package/extension/authoring/objective/draft.ts +345 -0
  7. package/extension/{factories/objectiveDreamReport.ts → authoring/objective/dreamReportGate.ts} +74 -131
  8. package/extension/authoring/objective/planning.ts +124 -0
  9. package/extension/authoring/objective/prose.ts +103 -0
  10. package/extension/authoring/objective/review.ts +128 -0
  11. package/extension/authoring/objective/save.ts +224 -0
  12. package/extension/authoring/plan/draft.ts +84 -0
  13. package/extension/authoring/plan/prose.ts +41 -0
  14. package/extension/authoring/plan/review.ts +269 -0
  15. package/extension/authoring/plan/save.ts +256 -0
  16. package/extension/authoring/plan/source.ts +82 -0
  17. package/extension/authoring/refinement/context.ts +468 -0
  18. package/extension/authoring/refinement/draft.ts +261 -0
  19. package/extension/authoring/refinement/prose.ts +79 -0
  20. package/extension/authoring/refinement/review.ts +111 -0
  21. package/extension/authoring/refinement/save.ts +119 -0
  22. package/extension/authoring/review/approvalGate.ts +34 -0
  23. package/extension/authoring/review/draftContext.ts +68 -0
  24. package/extension/codeReview/automated.ts +352 -0
  25. package/extension/codeReview/submission.ts +229 -0
  26. package/extension/delivery/address.ts +295 -0
  27. package/extension/delivery/ci.ts +355 -0
  28. package/extension/delivery/commitCompact.ts +93 -0
  29. package/extension/delivery/conflictResolution.ts +247 -0
  30. package/extension/delivery/ready.ts +193 -0
  31. package/extension/delivery/stackConflict.ts +361 -0
  32. package/extension/delivery/stackObjective.ts +16 -0
  33. package/extension/delivery/stackReconcile.ts +165 -0
  34. package/extension/delivery/submit.ts +171 -0
  35. package/extension/index.ts +369 -359
  36. package/extension/learning/analystWave.ts +324 -0
  37. package/extension/learning/audit.ts +667 -0
  38. package/extension/learning/capture.ts +92 -0
  39. package/extension/learning/containment.ts +104 -0
  40. package/extension/{waves/dreamWave.ts → learning/dream.ts} +112 -94
  41. package/extension/learning/dreamAnalysis.ts +435 -0
  42. package/extension/{waves/dreamReducerWave.ts → learning/dreamReducer.ts} +46 -41
  43. package/extension/{waves → learning}/dreamReport.ts +35 -31
  44. package/extension/learning/harvest.ts +491 -0
  45. package/extension/learning/prose.ts +66 -0
  46. package/extension/learning/routing.ts +79 -0
  47. package/extension/pi/v1/bashScanTimeout.ts +64 -0
  48. package/extension/{doors/prReview.ts → pi/v1/codeReview/automated.ts} +215 -310
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +125 -57
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +169 -116
  52. package/extension/pi/v1/codeReview/stack.ts +606 -0
  53. package/extension/pi/v1/codeReview/submit.ts +354 -0
  54. package/extension/{doors/prReviewTerminal.ts → pi/v1/codeReview/terminal.ts} +32 -27
  55. package/extension/pi/v1/contextEvidence.ts +80 -0
  56. package/extension/pi/v1/contextInjection.ts +207 -0
  57. package/extension/{doors → pi/v1/delivery}/address.ts +163 -265
  58. package/extension/pi/v1/delivery/ci.ts +570 -0
  59. package/extension/pi/v1/delivery/commitCompact.ts +201 -0
  60. package/extension/pi/v1/delivery/conflictResolverEngine.ts +425 -0
  61. package/extension/{doors → pi/v1/delivery}/land.ts +128 -60
  62. package/extension/pi/v1/delivery/ready.ts +322 -0
  63. package/extension/pi/v1/delivery/stackConflictResolver.ts +172 -0
  64. package/extension/pi/v1/delivery/stackDrive.ts +120 -0
  65. package/extension/pi/v1/delivery/stackLand.ts +223 -0
  66. package/extension/pi/v1/delivery/stackRecover.ts +265 -0
  67. package/extension/pi/v1/delivery/stackStatus.ts +237 -0
  68. package/extension/pi/v1/delivery/stackSync.ts +658 -0
  69. package/extension/pi/v1/delivery/submit.ts +389 -0
  70. package/extension/pi/v1/delivery/submitConflict.ts +186 -0
  71. package/extension/pi/v1/draftReview.ts +431 -0
  72. package/extension/{doors → pi/v1}/draftReviewWaveTools.ts +141 -151
  73. package/extension/pi/v1/gist.ts +794 -0
  74. package/extension/pi/v1/learning/audit.ts +186 -0
  75. package/extension/pi/v1/learning/dream.ts +207 -0
  76. package/extension/{doors/learnFactory.ts → pi/v1/learning/factory.ts} +18 -65
  77. package/extension/{doors/harvestWaveTools.ts → pi/v1/learning/harvest.ts} +46 -100
  78. package/extension/pi/v1/learning/learn.ts +585 -0
  79. package/extension/{doors → pi/v1}/lifecycleGates.ts +14 -59
  80. package/extension/{factories → pi/v1}/objective.ts +53 -33
  81. package/extension/pi/v1/objectiveAuthoring.ts +672 -0
  82. package/extension/pi/v1/objectiveDreamGate.ts +160 -0
  83. package/extension/pi/v1/objectivePlanning.ts +776 -0
  84. package/extension/pi/v1/objectiveRefinement.ts +1320 -0
  85. package/extension/pi/v1/objectiveReview.ts +451 -0
  86. package/extension/{doors → pi/v1}/objectiveReviewBrowser.ts +259 -172
  87. package/extension/pi/v1/plan.ts +812 -0
  88. package/extension/pi/v1/planReview.ts +820 -0
  89. package/extension/{doors → pi/v1}/planReviewBrowser.ts +228 -152
  90. package/extension/{doors/annotationPush.ts → pi/v1/providers/annotations.ts} +158 -89
  91. package/extension/pi/v1/providers/plannotator.ts +487 -0
  92. package/extension/{doors → pi/v1/providers}/plannotatorHandoff.ts +152 -34
  93. package/extension/pi/v1/providers/selection.ts +43 -0
  94. package/extension/{adapters/planAdapterTombell.ts → pi/v1/providers/tombell.ts} +43 -72
  95. package/extension/pi/v1/review.ts +538 -0
  96. package/extension/pi/v1/reviewOutcome.ts +9 -0
  97. package/extension/pi/v1/scoutWave.ts +318 -0
  98. package/extension/{doors → pi/v1}/selfcheck.ts +4 -4
  99. package/extension/session/branchWorkflowSession.ts +60 -0
  100. package/extension/session/lifecycle.ts +644 -0
  101. package/extension/session/lifecycleGates.ts +64 -0
  102. package/extension/session/saveDestination.ts +87 -0
  103. package/extension/session/workflowSession.ts +971 -0
  104. package/extension/substrate/agentScratch.ts +27 -54
  105. package/extension/substrate/bashScanTimeout.ts +181 -0
  106. package/extension/substrate/bindingDelivery.ts +38 -30
  107. package/extension/substrate/bindings.ts +4 -5
  108. package/extension/substrate/cache.ts +64 -12
  109. package/extension/substrate/childRestrictions.ts +39 -0
  110. package/extension/substrate/coldDoor.ts +17 -1
  111. package/extension/substrate/config.ts +157 -19
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/paths.ts +2 -7
  115. package/extension/substrate/prompts.ts +22 -0
  116. package/extension/substrate/registry.ts +2 -0
  117. package/extension/substrate/resolverLease.ts +364 -0
  118. package/extension/substrate/sessionData.ts +85 -152
  119. package/extension/substrate/toolGating.ts +279 -84
  120. package/extension/substrate/unifiedDiff.ts +1 -1
  121. package/extension/substrate/workflowState.ts +191 -166
  122. package/extension/substrate/worktreeResolverLock.ts +261 -0
  123. package/extension/surfaces/surfaces.ts +79 -27
  124. package/extension/waves/adversarialReviewWave.ts +103 -48
  125. package/extension/waves/blockedReports.ts +59 -0
  126. package/extension/waves/draftReviewWave.ts +42 -42
  127. package/extension/waves/laneIdentity.ts +77 -0
  128. package/extension/waves/objectiveExplorerWave.ts +24 -24
  129. package/extension/waves/prReviewWave.ts +89 -77
  130. package/extension/waves/reportWave.ts +438 -578
  131. package/extension/waves/reviewClassifierWave.ts +22 -22
  132. package/extension/waves/rpcAdapter.ts +100 -15
  133. package/extension/waves/scoutWave.ts +192 -0
  134. package/extension/waves/transport.ts +480 -0
  135. package/extension/worker/sdkAdapter.ts +494 -0
  136. package/extension/worker/stageExecution.ts +679 -0
  137. package/extension/workerMain.ts +18 -19
  138. package/package.json +6 -4
  139. package/prompts/_fixtures/live.yaml +98 -10
  140. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  142. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  143. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  144. package/prompts/contexts/adapters/tombell-plan.md +4 -0
  145. package/prompts/contexts/objective-refinement.md +17 -0
  146. package/prompts/contexts/plan-authoring.md +6 -5
  147. package/prompts/contexts/read-only.md +1 -1
  148. package/prompts/stages/conflict-resolution-continuation.md +9 -0
  149. package/prompts/stages/conflict-resolution.md +4 -4
  150. package/prompts/stages/objective-author/adopt.md +1 -1
  151. package/prompts/stages/objective-author/file.md +1 -1
  152. package/prompts/stages/objective-author/seed.md +1 -1
  153. package/prompts/stages/objective-plan/guidance.md +2 -2
  154. package/prompts/stages/objective-plan/seed.md +9 -1
  155. package/prompts/stages/objective-reconcile-ready.md +7 -0
  156. package/prompts/stages/objective-reconcile.md +1 -1
  157. package/prompts/stages/objective-refine/seed.md +18 -0
  158. package/prompts/stages/objective-review-browser.md +4 -4
  159. package/prompts/stages/objective-sync.md +1 -1
  160. package/prompts/stages/plan-review-browser.md +4 -4
  161. package/prompts/stages/pr-review-browser/active.md +3 -4
  162. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  163. package/prompts/stages/pr-review-terminal/active.md +3 -3
  164. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  165. package/prompts/stages/pr-review.md +3 -3
  166. package/prompts/stages/stack-review/cold.md +1 -0
  167. package/prompts/stages/stack-review-browser/stack.md +22 -0
  168. package/shared/README.md +8 -3
  169. package/shared/bindings.yaml +6 -3
  170. package/shared/contracts.md +4499 -2147
  171. package/shared/fixtures/issues-table.json +130 -0
  172. package/shared/registry.yaml +29 -1
  173. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  174. package/shared/schemas/outputs/objective-stack-status.schema.json +178 -2
  175. package/shared/schemas/outputs/pr-ready.schema.json +110 -2
  176. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  177. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  178. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  179. package/extension/doors/auditWaveTools.ts +0 -352
  180. package/extension/doors/ciExecutor.ts +0 -756
  181. package/extension/doors/commitCompact.ts +0 -251
  182. package/extension/doors/dreamWaveTools.ts +0 -475
  183. package/extension/doors/learn.ts +0 -655
  184. package/extension/doors/objectiveStack.ts +0 -1143
  185. package/extension/doors/prReviewDynamic.ts +0 -276
  186. package/extension/doors/ready.ts +0 -87
  187. package/extension/doors/submit.ts +0 -347
  188. package/extension/doors/submitPrReview.ts +0 -408
  189. package/extension/factories/gistAuthor.ts +0 -94
  190. package/extension/factories/gistDraft.ts +0 -265
  191. package/extension/factories/gistSave.ts +0 -251
  192. package/extension/factories/implementHere.ts +0 -116
  193. package/extension/factories/objectiveAuthor.ts +0 -98
  194. package/extension/factories/objectiveDraft.ts +0 -466
  195. package/extension/factories/objectivePlan.ts +0 -975
  196. package/extension/factories/objectiveSave.ts +0 -363
  197. package/extension/factories/planDraft.ts +0 -140
  198. package/extension/factories/planMode.ts +0 -205
  199. package/extension/factories/planReview.ts +0 -1237
  200. package/extension/factories/planSave.ts +0 -604
  201. package/extension/factories/planTitle.ts +0 -141
  202. package/extension/substrate/structuredOutput.ts +0 -202
  203. package/extension/waves/auditWave.ts +0 -312
  204. package/extension/waves/harvestWave.ts +0 -399
  205. package/extension/waves/learnWave.ts +0 -155
  206. package/extension/waves/memoryAdapter.ts +0 -139
  207. package/extension/waves/prReviewDynamicWave.ts +0 -777
  208. package/extension/worker/readOnlySession.ts +0 -294
  209. package/extension/worker/worker.ts +0 -899
  210. package/prompts/stages/pr-review-dynamic.md +0 -7
  211. package/shared/contracts-history.md +0 -605
@@ -1,98 +0,0 @@
1
- // Objective-authoring context injection (the objective mirror of planMode's plan-authoring
2
- // half). A `perk objective author` cold launch opens a READ-ONLY session whose handoff `stage` is
3
- // `objective-author`; this module injects the objective-authoring contract under its own
4
- // `perk:objective-author-context` customType (once-only: branch-scan dedup'd on the marker),
5
- // keyed off (read-only gate AND stage === objective-author), optionally extended by the same `[workflow] plan_authoring` addendum the
6
- // plan-authoring injection consumes (verbatim reuse, read per-event via loadPerkConfig). planMode.ts defers when the stage is objective-author, so exactly one
7
- // authoring context is injected — the coupling break: plan-authoring is no longer keyed off
8
- // the bare read-only gate.
9
- //
10
- // The `objective_save` warm door (the tool + `/objective-save` command) lives in objectiveSave.ts,
11
- // the mirror of planSave.ts.
12
-
13
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
- import { loadPerkConfig } from "../substrate/config.ts";
15
- import { render } from "../substrate/prompts.ts";
16
- import type { ToolGating } from "../substrate/toolGating.ts";
17
- import {
18
- type BranchEntry,
19
- branchCarries,
20
- branchOf,
21
- rebuildWorkflowState,
22
- } from "../substrate/workflowState.ts";
23
-
24
- /** The registry stage id of the objective-authoring session (shared with planMode's defer check). */
25
- export const OBJECTIVE_AUTHOR_STAGE = "objective-author";
26
-
27
- /** The objective-authoring context customType (distinct from planMode's `perk:plan-context`). */
28
- export const OBJECTIVE_AUTHOR_CONTEXT_TYPE = "perk:objective-author-context";
29
- const OBJECTIVE_AUTHOR_MARKER = "[OBJECTIVE AUTHORING]";
30
-
31
- /**
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).
37
- */
38
- export const OBJECTIVE_AUTHORING_CONTEXT = render("contexts/objective-authoring.md", {
39
- marker: OBJECTIVE_AUTHOR_MARKER,
40
- });
41
-
42
- /** Build the full objective-authoring injection, appending the project config addendum when present. */
43
- export function objectiveAuthoringContextContent(cwd: string): string {
44
- const addendum = loadPerkConfig(cwd).planAuthoring;
45
- return addendum
46
- ? `${OBJECTIVE_AUTHORING_CONTEXT}\n\n${addendum.trim()}`
47
- : OBJECTIVE_AUTHORING_CONTEXT;
48
- }
49
-
50
- /** Whether the current branch is an objective-author session (read-only gate AND stage match). */
51
- function isObjectiveAuthoring(gating: ToolGating, branch: readonly BranchEntry[]): boolean {
52
- return gating.isActive() && rebuildWorkflowState(branch).stage === OBJECTIVE_AUTHOR_STAGE;
53
- }
54
-
55
- /**
56
- * Register the objective-authoring context injection (display:false), the mirror of planMode's
57
- * injection half. Inert outside an objective-author session; never throws.
58
- */
59
- export function registerObjectiveAuthor(pi: ExtensionAPI, gating: ToolGating): void {
60
- pi.on("before_agent_start", async (_event, ctx) => {
61
- const branch = branchOf(ctx);
62
- if (!isObjectiveAuthoring(gating, branch)) return;
63
- // Once-only: injected customs persist to the branch, so a live copy suppresses re-injection;
64
- // compaction dropping it makes the scan come up clean and the next turn re-injects.
65
- if (branchCarries(branch, OBJECTIVE_AUTHOR_MARKER)) return;
66
- return {
67
- message: {
68
- customType: OBJECTIVE_AUTHOR_CONTEXT_TYPE,
69
- content: objectiveAuthoringContextContent(ctx.cwd),
70
- display: false,
71
- },
72
- };
73
- });
74
-
75
- // Strip the stale objective-authoring marker from context once the session is no longer authoring
76
- // (gate off, or the stage moved on) so it never lingers — the same hygiene planMode applies.
77
- pi.on("context", async (event, ctx) => {
78
- const branch = branchOf(ctx);
79
- if (isObjectiveAuthoring(gating, branch)) return;
80
- return {
81
- messages: event.messages.filter((m) => {
82
- const msg = m as { customType?: string; role?: string; content?: unknown };
83
- if (msg.customType === OBJECTIVE_AUTHOR_CONTEXT_TYPE) return false;
84
- if (msg.role !== "user") return true;
85
- const content = msg.content;
86
- if (typeof content === "string") return !content.includes(OBJECTIVE_AUTHOR_MARKER);
87
- if (Array.isArray(content)) {
88
- return !content.some(
89
- (c) =>
90
- (c as { type?: string; text?: string }).type === "text" &&
91
- ((c as { text?: string }).text ?? "").includes(OBJECTIVE_AUTHOR_MARKER),
92
- );
93
- }
94
- return true;
95
- }),
96
- };
97
- });
98
- }
@@ -1,466 +0,0 @@
1
- // The `objective_draft` file tool: the objective-flavored twin of the
2
- // `plan_draft` carve-out (planDraft.ts).
3
- //
4
- // Carve-out doctrine: the tool takes NO path/name parameter — the artifact name is the fixed
5
- // constant `OBJECTIVE_DRAFT_ARTIFACT` and the path is derived exclusively through the session-data
6
- // accessor seam (`writeSessionArtifact`, sessionData.ts), so the only bytes it can ever write are
7
- // the one working-objective artifact in the current run's data dir (gitignored scratch).
8
- // Allowlisting its name in `READ_ONLY_TOOLS` (toolGating.ts) is therefore safe: the read-only
9
- // invariant (the worktree stays untouched) holds, and the gate's `tool_call` edit/write/bash
10
- // blocking logic is UNCHANGED. Full rewrite per call, non-terminating; NOT a save —
11
- // `objective_save`/`/objective-save` still persist the objective to GitHub.
12
- //
13
- // Format doctrine: JSON is the storage/transport format, NEVER the human review surface. 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
17
- // `parse_structured_roadmap` path). The review surface reads the draft via
18
- // `readObjectiveDraft` (over `readSessionArtifact` — digest-validated, fail-open) and renders
19
- // markdown via `renderObjectiveDraft` (the prose + a roadmap table) — never raw JSON; the
20
- // approval→`objective_save` orchestration feeds the recovered roadmap
21
- // back as structured JSON.
22
- //
23
- // Vocabulary ownership: this module owns the shared draft/save param vocabulary
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.
28
- //
29
- // Imports stay node builtins + sibling seams (sessionData.ts, result.ts) so the module loads
30
- // under `node --test`; no manual `scratch`/`runs` path segments (cacheGuard.test.ts).
31
-
32
- import { relative } from "node:path";
33
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
34
- import { failFor, ok, type Result } from "../substrate/result.ts";
35
- import {
36
- activeSessionRunId,
37
- digestSessionData,
38
- readSessionArtifact,
39
- type SessionDataCtx,
40
- writeSessionArtifact,
41
- } from "../substrate/sessionData.ts";
42
- import { arrayParam, objectParam, paramsOf, stringParam } from "../substrate/toolParams.ts";
43
- import type { EntrySink } from "../substrate/workflowState.ts";
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";
54
-
55
- /** The decoded `objective_save` tool params (shared with `objective_draft`). */
56
- export interface ObjectiveSaveParams {
57
- prose: string;
58
- title?: string;
59
- roadmap?: unknown[];
60
- // The objective's target branch; omitted to use the repo default.
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;
67
- }
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
-
97
- /**
98
- * The roadmap-node items JSON schema, shared between `objective_save` and `objective_draft`
99
- * so the two tools' roadmap contracts cannot drift.
100
- */
101
- export const ROADMAP_PARAM_SCHEMA = {
102
- type: "object",
103
- additionalProperties: false,
104
- required: ["id", "description"],
105
- properties: {
106
- id: { type: "string", description: 'A stable node id, e.g. "1.1".' },
107
- description: { type: "string", description: "What this node delivers." },
108
- status: {
109
- type: "string",
110
- enum: ["pending", "planning", "in_progress", "done", "blocked", "skipped"],
111
- description: "Optional initial status (defaults to pending).",
112
- },
113
- slug: { type: "string", description: "Optional short slug." },
114
- pr: { type: "string", description: 'Optional plan/PR backlink, e.g. "#42".' },
115
- depends_on: {
116
- type: "array",
117
- items: { type: "string" },
118
- description: "Optional explicit dependency node ids.",
119
- },
120
- comment: { type: "string", description: "Optional note." },
121
- adopt_issue: {
122
- type: "string",
123
- description:
124
- "Optional: the id/identifier of a pre-existing source issue this node adopts in place " +
125
- "(objective author --from, Linear only).",
126
- },
127
- },
128
- } as const;
129
-
130
- /**
131
- * Decode unknown `objective_save` tool-call params (the tool-boundary seam). `prose`
132
- * absent decodes to `""` (so `saveObjective`'s "no objective prose to save" `invalid_input` arm
133
- * keeps owning that message) but present-but-mistyped → null (strict-fail). `roadmap` stays
134
- * `unknown[]` — the Python cold door owns node-shape validation.
135
- */
136
- export function decodeObjectiveSaveParams(params: unknown): ObjectiveSaveParams | null {
137
- const p = paramsOf(params);
138
- if (p === null) return null;
139
- const prose = stringParam(p, "prose");
140
- const title = stringParam(p, "title");
141
- const roadmap = arrayParam(p, "roadmap");
142
- const base = stringParam(p, "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 };
160
- }
161
-
162
- /** The fixed working-objective artifact name (one JSON file: prose + the structured roadmap). */
163
- export const OBJECTIVE_DRAFT_ARTIFACT = "objective-draft.json";
164
-
165
- /** The ok-arm details — provenance-consistent with the recorded `session_artifacts` pointer. */
166
- export interface ObjectiveDraftOk {
167
- name: string;
168
- path: string;
169
- digest: string;
170
- bytes: number;
171
- run_id: string;
172
- roadmap_nodes: number;
173
- }
174
-
175
- export type ObjectiveDraftResult = Result<ObjectiveDraftOk>;
176
-
177
- /**
178
- * The core both the tool handler and tests call: serialize the working objective (prose + the
179
- * structured roadmap, verbatim — the draft never validates node shapes) as one JSON artifact and
180
- * write it through the accessor seam (file + `session_artifacts` provenance pointer). Soft
181
- * result, never throws — failure taxonomy: empty prose → `invalid_input`; no session 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).
185
- */
186
- export function writeObjectiveDraft(
187
- sink: EntrySink,
188
- ctx: SessionDataCtx & ReportTarget,
189
- opts: {
190
- prose: string;
191
- title?: string;
192
- roadmap?: unknown[];
193
- base?: string;
194
- delivery?: DeliveryChoice;
195
- dream_report?: unknown;
196
- },
197
- ): ObjectiveDraftResult {
198
- const fail = failFor(ctx, "objective-draft");
199
-
200
- if (!opts.prose.trim()) {
201
- return fail("no objective prose to write (pass the full working draft)", "invalid_input");
202
- }
203
-
204
- const runId = activeSessionRunId(ctx);
205
- if (runId === null) {
206
- return fail("session has no run_id — cannot write the objective-draft artifact", "no_run_id");
207
- }
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
-
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).
218
- const title = opts.title?.trim();
219
- const base = opts.base?.trim();
220
- const delivery = opts.delivery;
221
- const roadmap = opts.roadmap ?? [];
222
- const payload = {
223
- schema_version: 1,
224
- ...(title ? { title } : {}),
225
- ...(base ? { base } : {}),
226
- ...(delivery ? { delivery } : {}),
227
- ...(gate.kind === "block" ? { dream_report: gate.block } : {}),
228
- prose: opts.prose,
229
- roadmap,
230
- };
231
- const content = `${JSON.stringify(payload, null, 2)}\n`;
232
-
233
- const written = writeSessionArtifact(sink, ctx, OBJECTIVE_DRAFT_ARTIFACT, content);
234
- if (written === null) {
235
- return fail(
236
- `could not write the ${OBJECTIVE_DRAFT_ARTIFACT} artifact (see warnings)`,
237
- "write_failed",
238
- );
239
- }
240
-
241
- // Derive digest/relative path consistently with the pointer the seam recorded.
242
- const digest = digestSessionData(content);
243
- const relPath = relative(ctx.cwd, written);
244
- return ok(`Objective draft written → ${relPath} (${digest}; ${roadmap.length} roadmap nodes)`, {
245
- name: OBJECTIVE_DRAFT_ARTIFACT,
246
- path: relPath,
247
- digest,
248
- bytes: Buffer.byteLength(content, "utf8"),
249
- run_id: runId,
250
- roadmap_nodes: roadmap.length,
251
- });
252
- }
253
-
254
- // ------------------------------------------------------------------- the reader + the renderer
255
-
256
- /** The validated working-objective draft shape consumers receive from `readObjectiveDraft`. */
257
- export interface ObjectiveDraft {
258
- title?: string;
259
- prose: string;
260
- roadmap: unknown[];
261
- // The objective's target branch; kept only when a non-blank string in the artifact.
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;
267
- }
268
-
269
- /**
270
- * Read + validate the working-objective draft artifact. Fail-open `null` everywhere (mirroring
271
- * `readSessionArtifact`'s loud tier): no pointer/file/digest → `null` (the seam already spoke);
272
- * malformed JSON, a non-object payload, an unsupported `schema_version`, or blank prose → a
273
- * stderr warning + `null`. `roadmap` defaults to `[]` when absent/non-array; `title` is kept
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.
278
- */
279
- export function readObjectiveDraft(ctx: SessionDataCtx): ObjectiveDraft | null {
280
- const artifact = readSessionArtifact(ctx, OBJECTIVE_DRAFT_ARTIFACT);
281
- if (artifact === null) return null;
282
-
283
- const refuse = (why: string): null => {
284
- console.error(`perk: warning: ${OBJECTIVE_DRAFT_ARTIFACT} ${why} — refusing the draft`);
285
- return null;
286
- };
287
- let parsed: unknown;
288
- try {
289
- parsed = JSON.parse(artifact.content);
290
- } catch {
291
- return refuse("is not valid JSON");
292
- }
293
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
294
- return refuse("is not a JSON object");
295
- }
296
- const payload = parsed as Record<string, unknown>;
297
- if (payload.schema_version !== 1) {
298
- return refuse(`has an unsupported schema_version (${JSON.stringify(payload.schema_version)})`);
299
- }
300
- const prose = payload.prose;
301
- if (typeof prose !== "string" || !prose.trim()) {
302
- return refuse("has no prose");
303
- }
304
- const roadmap = Array.isArray(payload.roadmap) ? payload.roadmap : [];
305
- const title =
306
- typeof payload.title === "string" && payload.title.trim() ? payload.title : undefined;
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
- }
320
- return {
321
- ...(title !== undefined ? { title } : {}),
322
- ...(base !== undefined ? { base } : {}),
323
- ...(delivery !== undefined ? { delivery } : {}),
324
- ...(dreamReport !== undefined ? { dream_report: dreamReport } : {}),
325
- prose,
326
- roadmap,
327
- };
328
- }
329
-
330
- /** Sanitize a table cell: `|` escaped, newlines collapsed to a single space. */
331
- function tableCell(value: string): string {
332
- return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
333
- }
334
-
335
- /** Read a string field off an unknown-shaped roadmap node (`""` when absent/mistyped). */
336
- function nodeString(node: unknown, key: string): string {
337
- if (typeof node !== "object" || node === null) return "";
338
- const value = (node as Record<string, unknown>)[key];
339
- return typeof value === "string" ? value : "";
340
- }
341
-
342
- /** Render a node's `depends_on` as a `", "`-join of its string members; `-` when empty/absent. */
343
- function nodeDependsOn(node: unknown): string {
344
- if (typeof node !== "object" || node === null) return "-";
345
- const value = (node as Record<string, unknown>).depends_on;
346
- if (!Array.isArray(value)) return "-";
347
- const deps = value.filter((d): d is string => typeof d === "string");
348
- return deps.length > 0 ? deps.join(", ") : "-";
349
- }
350
-
351
- /**
352
- * Render the draft as the markdown review surface (JSON is storage/transport only — contracts
353
- * §8.1): the optional `# title` heading, the prose verbatim, and (when the roadmap is non-empty)
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.
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
-
373
- export function renderObjectiveDraft(draft: ObjectiveDraft): string {
374
- let out = "";
375
- if (draft.title) out += `# ${draft.title}\n\n`;
376
- out += `${deliveryLine(draft)}\n\n`;
377
- out += draft.prose;
378
-
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
- }
396
-
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`;
400
- }
401
-
402
- const TOOL_GUIDELINES = [
403
- "Call objective_draft to persist the current working objective as you author or revise it; pass the FULL prose and the FULL structured roadmap each time (it rewrites the whole draft).",
404
- "objective_draft never saves to GitHub and never ends the turn — objective_save//objective-save remain the canonical save surface. Never hand-write roadmap YAML — hand the structured roadmap to the tool.",
405
- "Pass objective_draft's `base` only to target a non-default branch; omit it to use the repo default.",
406
- ];
407
-
408
- /** Register the `objective_draft` tool (the carve-out producer; interior-only). */
409
- export function registerObjectiveDraft(pi: ExtensionAPI): void {
410
- pi.registerTool({
411
- name: "objective_draft",
412
- label: "Objective draft",
413
- description:
414
- "Write (or overwrite) the working objective draft — prose + the structured roadmap — to " +
415
- "the session data dir and record its provenance pointer. The only sanctioned write surface " +
416
- "while read-only. NOT a save — objective_save//objective-save still persist the objective " +
417
- "to GitHub.",
418
- promptSnippet:
419
- "Persist the working objective draft (prose + structured roadmap) to the session data dir (full rewrite)",
420
- promptGuidelines: TOOL_GUIDELINES,
421
- executionMode: "sequential",
422
- parameters: {
423
- type: "object",
424
- additionalProperties: false,
425
- required: ["prose"],
426
- properties: {
427
- prose: {
428
- type: "string",
429
- description: "The objective prose (the why, the design, the boundaries/non-goals).",
430
- },
431
- title: {
432
- type: "string",
433
- description: "Optional objective title (defaults to the prose's first heading).",
434
- },
435
- base: {
436
- type: "string",
437
- description:
438
- "Optional target branch for this objective's plans (omit to use the repo default).",
439
- },
440
- delivery: DELIVERY_PARAM_SCHEMA,
441
- dream_report: DREAM_REPORT_PARAM_SCHEMA,
442
- roadmap: {
443
- type: "array",
444
- description:
445
- "The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
446
- items: ROADMAP_PARAM_SCHEMA,
447
- },
448
- },
449
- },
450
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
451
- // The shared param contract: the same decode as `objective_save`, so the two cannot drift.
452
- const decoded = decodeObjectiveSaveParams(params);
453
- if (decoded === null) {
454
- return failFor(
455
- ctx,
456
- "objective-draft",
457
- "objective_draft",
458
- )(
459
- "objective_draft needs { prose: string, roadmap?: array } per the tool schema",
460
- "bad_input",
461
- );
462
- }
463
- return writeObjectiveDraft(pi, ctx, decoded);
464
- },
465
- });
466
- }