@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,363 +0,0 @@
1
- // The warm `objective_save` door, the objective mirror of planSave.ts. The in-session twin
2
- // of the Python cold door (`perk objective create`): a deterministic, terminating tool + command
3
- // that WRAP the existing storage — they do NOT reimplement the GitHub write. `saveObjective()`
4
- // passes the STRUCTURED roadmap as `--roadmap <json>` (the agent never hand-writes roadmap YAML)
5
- // and delegates to `perk objective create --json` via the shared cold-door client (`runColdDoor`,
6
- // the prose rides the run-scratch stdin channel), then links the live session: `active_objective` + a fresh `perk:objective-budget` activation marker
7
- // (mirrors the `/objective <id>` activation in objective.ts). Failures are loud-but-non-fatal.
8
- //
9
- // APPROVAL→SAVE ORCHESTRATION (mirroring planSave.ts's `approvalSave`). The
10
- // exported `objectiveApprovalSave` seam is the shared APPROVED-review → save flow: re-read the
11
- // STRUCTURED artifact (`readObjectiveDraft` — never the rendered markdown, never the transcript)
12
- // → `saveObjective` → D1a gate exit on a successful save (snapshot `gating.isActive()` BEFORE the
13
- // save; a failed save leaves the gate ON). `plan_review`'s objective arm (planReview.ts) wires
14
- // its APPROVED outcome into it; the `/objective-save` command is the artifact-first MANUAL
15
- // FAILSAFE invocation of the same seam, keeping the legacy drive-the-session behavior as the
16
- // no-draft fallback (objectives have no transcript scrape by design).
17
-
18
- import { join } from "node:path";
19
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
20
- import { bindingSuffix } from "../substrate/bindingDelivery.ts";
21
- import { atomicWriteFileSync, ensureRunScratch } from "../substrate/cache.ts";
22
- import {
23
- booleanField,
24
- type ColdJson,
25
- objectField,
26
- runColdDoor,
27
- stringField,
28
- } from "../substrate/coldDoor.ts";
29
- import { registerPerkCommand } from "../substrate/command.ts";
30
- import { render } from "../substrate/prompts.ts";
31
- import { failFor, ok, type Result } from "../substrate/result.ts";
32
- import type { ToolGating } from "../substrate/toolGating.ts";
33
- import { appendWorkflowState, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
34
- import { report, type Severity } from "../surfaces/report.ts";
35
- import { OBJECTIVE_BUDGET_TYPE } from "./objective.ts";
36
- import {
37
- DELIVERY_PARAM_SCHEMA,
38
- type DeliveryChoice,
39
- DREAM_REPORT_PARAM_SCHEMA,
40
- decodeObjectiveSaveParams,
41
- ROADMAP_PARAM_SCHEMA,
42
- readObjectiveDraft,
43
- } from "./objectiveDraft.ts";
44
- import { resolveDreamReportGate } from "./objectiveDreamReport.ts";
45
-
46
- /** The `objective-save` registry stage id (the objectiveAuthor.ts constant's sibling). */
47
- export const OBJECTIVE_SAVE_STAGE = "objective-save";
48
-
49
- /**
50
- * The run-scoped dream-report transfer filename (contracts §8.64) — the extension→door handoff
51
- * carrying the reviewed CANONICAL parts. The Python mirror is
52
- * `perk.learn.dream_companion.DREAM_REPORT_TRANSFER_FILENAME` (parity-pinned by test), beside
53
- * the existing `DREAM_MANIFEST_FILENAME` mirror pair.
54
- */
55
- export const DREAM_REPORT_TRANSFER_FILENAME = "dream-report-transfer.json";
56
-
57
- /** The ok-arm fields — the structured `details` surface doubles as branch-safe persisted state. */
58
- export interface ObjectiveSaveOk {
59
- /** `id` is the opaque string objective id (GitHub "7", Linear "ENG-7") — §8.21. */
60
- objective: { id: string; url: string };
61
- existed: boolean | null;
62
- }
63
-
64
- export type ObjectiveSaveResult = Result<ObjectiveSaveOk>;
65
-
66
- /** The decoded `perk objective create --json` payload slice the warm door consumes. */
67
- interface ObjectiveCreatePayload {
68
- objective: { id: string; url: string; existed: boolean | undefined };
69
- }
70
-
71
- /** Narrow the `perk objective create --json` success payload; strict on `objective`. */
72
- function decodeObjectiveCreate(payload: ColdJson): ObjectiveCreatePayload | null {
73
- const objective = objectField(payload, "objective");
74
- if (objective === undefined) return null;
75
- const id = stringField(objective, "id");
76
- const url = stringField(objective, "url");
77
- if (id === undefined || url === undefined) return null;
78
- return { objective: { id, url, existed: booleanField(objective, "existed") } };
79
- }
80
-
81
- /**
82
- * The single save implementation both surfaces call. Delegates the GitHub write to the Python cold
83
- * door, then links the live session (`active_objective` + budget marker). Returns a soft result
84
- * (never throws); failures set `details.ok = false` and append no linkage.
85
- *
86
- * `dream_report` is ONE carrier with two sources (§8.63): the direct tool path supplies
87
- * `{input}` and the save stamps `generated_at`; the approval path passes the artifact block
88
- * through with its stored stamp AND stored parts — the stored parts are byte-compared against
89
- * the freshly re-rendered ones, so run-scratch drift or artifact tamper between draft-write
90
- * and save refuses `bad_state` (nothing saved, the gate stays on). The parts do NOT cross to
91
- * the Python plane yet — companion persistence is deferred (explicit in §8.63, not silent).
92
- */
93
- export async function saveObjective(
94
- pi: ExtensionAPI,
95
- ctx: ExtensionContext,
96
- opts: {
97
- prose: string;
98
- title?: string;
99
- roadmap?: unknown[];
100
- base?: string;
101
- delivery?: DeliveryChoice;
102
- dream_report?: { input: unknown; generated_at?: string; parts?: string[] };
103
- },
104
- ): Promise<ObjectiveSaveResult> {
105
- const fail = failFor(ctx, "objective-save");
106
-
107
- const prose = opts.prose.trim();
108
- if (!prose)
109
- return fail("no objective prose to save (draft the objective first)", "invalid_input");
110
- if (opts.roadmap !== undefined && !Array.isArray(opts.roadmap)) {
111
- return fail("roadmap must be a JSON array of nodes", "invalid_input");
112
- }
113
-
114
- // The §8.63 fail-closed re-validation, before anything reaches the cold door. Presence is
115
- // the `opts.dream_report === undefined` boundary — an `{input: undefined}` carrier is never
116
- // constructed (the execute wraps only a present decoded value; the approval path passes the
117
- // validated artifact block).
118
- const generatedAt = opts.dream_report?.generated_at ?? new Date().toISOString();
119
- const gate =
120
- opts.dream_report === undefined
121
- ? resolveDreamReportGate(ctx, undefined, generatedAt)
122
- : resolveDreamReportGate(ctx, opts.dream_report.input, generatedAt);
123
- if (gate.kind === "refuse") {
124
- return fail(gate.detail, gate.errorType);
125
- }
126
- if (gate.kind === "block" && opts.dream_report?.parts !== undefined) {
127
- // The approval path: the reviewed (stored) parts must byte-match the re-render against
128
- // freshly recovered context — the same stored `generated_at` stamp keeps the comparison
129
- // deterministic.
130
- if (JSON.stringify(gate.block.parts) !== JSON.stringify(opts.dream_report.parts)) {
131
- return fail(
132
- "the reviewed report no longer matches the wave state — re-draft and re-review",
133
- "bad_state",
134
- );
135
- }
136
- }
137
-
138
- const branch = () => branchOf(ctx);
139
- const runId = rebuildWorkflowState(branch()).run_id ?? "";
140
-
141
- if (gate.kind === "block") {
142
- // The §8.64 transfer write (the dream arm only): the reviewed CANONICAL parts cross to the
143
- // Python save door through the run-scoped scratch handoff — written atomically BEFORE the
144
- // cold door is invoked. A write failure is the soft `scratch_failed` failure (the
145
- // runColdDoor stdin-staging precedent): the cold door is NOT invoked, nothing activates,
146
- // and the read-only gate stays on. Non-dream saves write nothing (byte-identical).
147
- try {
148
- const dir = ensureRunScratch(ctx.cwd, runId);
149
- const content = `${JSON.stringify(
150
- { schema_version: "1", run_id: runId, parts: gate.block.parts },
151
- null,
152
- 2,
153
- )}\n`;
154
- atomicWriteFileSync(join(dir, DREAM_REPORT_TRANSFER_FILENAME), content);
155
- } catch (err) {
156
- return fail(`could not stage the dream-report transfer: ${String(err)}`, "scratch_failed");
157
- }
158
- }
159
-
160
- const args = ["objective", "create", "--json"];
161
- if (opts.title) args.push("--title", opts.title);
162
- if (opts.base) args.push("--base", opts.base);
163
- // The reviewed delivery choice rides verbatim; the cold door owns validation + preflight.
164
- if (opts.delivery) args.push("--delivery", opts.delivery);
165
- if (runId) args.push("--run-id", runId);
166
- if (opts.roadmap && opts.roadmap.length > 0) {
167
- args.push("--roadmap", JSON.stringify(opts.roadmap));
168
- }
169
- const r = await runColdDoor<ObjectiveCreatePayload>(pi, ctx, args, {
170
- label: "perk objective create",
171
- decode: decodeObjectiveCreate,
172
- stdin: { flag: "--body", content: prose, filename: "objective.md" },
173
- });
174
- if (!r.ok) return fail(r.message, r.errorType);
175
-
176
- // Link the live session: set active_objective (LWW) + seed a fresh budget activation marker
177
- // (mirrors objective.ts's `/objective <id>` activation), so budget tracking starts immediately.
178
- // The envelope id is already the opaque string id (§8.21) — no coercion needed.
179
- const objective = r.data.objective;
180
- const objectiveId = objective.id;
181
- const linked = rebuildWorkflowState(branch()).active_objective ?? null;
182
- if (linked !== objectiveId) {
183
- appendWorkflowState(pi, ctx, {
184
- data: { active_objective: objectiveId },
185
- field: "active_objective",
186
- expected: objectiveId,
187
- scope: "objective-save",
188
- failure: `active_objective read-back failed for #${objectiveId}`,
189
- });
190
- pi.appendEntry(OBJECTIVE_BUDGET_TYPE, {
191
- objective_id: objectiveId,
192
- activated_at: new Date().toISOString(),
193
- });
194
- }
195
-
196
- const verb = objective.existed ? "Found existing" : "Saved";
197
- return ok(
198
- `${verb} objective #${objective.id} → ${objective.url}`,
199
- {
200
- objective: { id: objective.id, url: objective.url },
201
- existed: objective.existed ?? null,
202
- },
203
- { terminate: true },
204
- );
205
- }
206
-
207
- /** The approval→save orchestration outcome (the objective `ApprovalSaveOutcome`). */
208
- export type ObjectiveApprovalSaveOutcome =
209
- | { status: "no-draft" }
210
- | { status: "saved" | "save-failed"; result: ObjectiveSaveResult; gateExited: boolean };
211
-
212
- /**
213
- * The shared approval→save orchestration seam (the objective sibling of
214
- * planSave.ts's `approvalSave`): an APPROVED objective review (`plan_review`'s objective arm)
215
- * and the manual `/objective-save` failsafe both run THIS. Flow: re-read the STRUCTURED draft
216
- * artifact at save time (`readObjectiveDraft` — never the rendered markdown, never in-hand
217
- * bytes) → `saveObjective` → gate exit on a successful save while read-only (the D1a pattern:
218
- * snapshot `gating.isActive()` before the save; a failed save leaves the gate ON). No draft →
219
- * `no-draft` (nothing saved, the gate untouched); callers render their own fallback. Title
220
- * precedence: an explicit `opts.title` wins; else the draft's `title`; else the cold door
221
- * derives from the prose heading. The returned result keeps `saveObjective`'s `terminate: true`
222
- * for tool-path callers.
223
- */
224
- export async function objectiveApprovalSave(
225
- pi: ExtensionAPI,
226
- ctx: ExtensionContext,
227
- gating: ToolGating,
228
- opts: { title?: string } = {},
229
- ): Promise<ObjectiveApprovalSaveOutcome> {
230
- const draft = readObjectiveDraft(ctx);
231
- if (draft === null) return { status: "no-draft" };
232
- // D1a: snapshot the gate BEFORE the save; on success, exit it so save marks the read-only →
233
- // read-write boundary in one gesture. A failed save leaves the gate on.
234
- const wasReadOnly = gating.isActive();
235
- const result = await saveObjective(pi, ctx, {
236
- prose: draft.prose,
237
- title: opts.title ?? draft.title,
238
- roadmap: draft.roadmap,
239
- base: draft.base,
240
- delivery: draft.delivery,
241
- // The approval path passes the artifact block through whole — stored stamp + stored parts
242
- // (the save re-validates and byte-compares, §8.63).
243
- ...(draft.dream_report !== undefined ? { dream_report: draft.dream_report } : {}),
244
- });
245
- let gateExited = false;
246
- if (result.details.ok && wasReadOnly) {
247
- gating.exit(ctx);
248
- gateExited = true;
249
- }
250
- return { status: result.details.ok ? "saved" : "save-failed", result, gateExited };
251
- }
252
-
253
- const TOOL_GUIDELINES = [
254
- "Use objective_save only after the objective + roadmap are decision-complete; it creates the canonical perk:objective issue, activates it, and ends the turn.",
255
- "Pass objective_save the objective PROSE in `prose` and the STRUCTURED roadmap in `roadmap` (a JSON array of nodes) — never hand-write roadmap YAML.",
256
- 'Each objective_save roadmap node needs a stable `id` (e.g. "1.1") and a `description`; `status` defaults to pending. Use `depends_on` for explicit ordering.',
257
- ];
258
-
259
- /**
260
- * The seed guidance the warm `/objective-save` injects to drive the structured save (the
261
- * perk-objective-author skill pointer rides the skill-binding suffix — not hardcoded
262
- * here). Pure + exported for offline tests.
263
- */
264
- export function objectiveSaveGuidance(title?: string): string {
265
- const named = title?.trim() || "";
266
- return render("stages/objective-save.md", { title: named });
267
- }
268
-
269
- /** Register the warm door: the `objective_save` tool (canonical) + the `/objective-save` twin. */
270
- export function registerObjectiveSave(pi: ExtensionAPI, gating: ToolGating): void {
271
- pi.registerTool({
272
- name: "objective_save",
273
- label: "Save objective",
274
- description:
275
- "Persist a drafted objective + structured roadmap to GitHub as a perk:objective issue, " +
276
- "activate it, and start budget tracking. Terminating: ends the turn on save. Call only when " +
277
- "the objective and roadmap are decision-complete.",
278
- promptSnippet: "Save the decision-complete objective + roadmap to GitHub (terminates the turn)",
279
- promptGuidelines: TOOL_GUIDELINES,
280
- executionMode: "sequential",
281
- parameters: {
282
- type: "object",
283
- additionalProperties: false,
284
- required: ["prose"],
285
- properties: {
286
- prose: {
287
- type: "string",
288
- description: "The objective prose (the why, the design, the boundaries/non-goals).",
289
- },
290
- title: {
291
- type: "string",
292
- description: "Optional objective title (defaults to the prose's first heading).",
293
- },
294
- base: {
295
- type: "string",
296
- description:
297
- "Optional target branch for this objective's plans (omit to use the repo default).",
298
- },
299
- delivery: DELIVERY_PARAM_SCHEMA,
300
- dream_report: DREAM_REPORT_PARAM_SCHEMA,
301
- roadmap: {
302
- type: "array",
303
- description:
304
- "The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
305
- items: ROADMAP_PARAM_SCHEMA,
306
- },
307
- },
308
- },
309
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
310
- const decoded = decodeObjectiveSaveParams(params);
311
- if (decoded === null) {
312
- return failFor(
313
- ctx,
314
- "objective-save",
315
- "objective_save",
316
- )(
317
- "objective_save needs { prose: string, roadmap?: array } per the tool schema",
318
- "bad_input",
319
- );
320
- }
321
- // The direct tool path wraps ONLY a present decoded value as the `{input}` carrier (the
322
- // save stamps generated_at); no stored parts, so no byte-compare on this path.
323
- const { dream_report, ...rest } = decoded;
324
- return saveObjective(pi, ctx, {
325
- ...rest,
326
- ...(dream_report !== undefined ? { dream_report: { input: dream_report } } : {}),
327
- });
328
- },
329
- });
330
-
331
- registerPerkCommand(pi, "objective-save", {
332
- description:
333
- "Save the working objective draft to GitHub — the manual failsafe for the approval→save " +
334
- "flow (artifact-first; drives the structured save only when no draft exists).",
335
- handler: async (args, ctx) => {
336
- const title = args.trim() || undefined;
337
- // The artifact-first manual-failsafe invocation of the shared approval→save
338
- // seam (the D1a gate exit lives in the seam). The legacy drive-the-session behavior is kept
339
- // as the NO-DRAFT fallback — objectives have no transcript scrape by design, so a draftless
340
- // session still needs a working save path.
341
- const outcome = await objectiveApprovalSave(pi, ctx, gating, { title });
342
- if (outcome.status === "no-draft") {
343
- // Exit the read-only gate so the objective_save tool (excluded from READ_ONLY_TOOLS)
344
- // becomes reachable on the driven turn, then drive the turn (mirrors /address and
345
- // /objective-plan).
346
- if (gating.isActive()) gating.exit(ctx);
347
- report(ctx, "objective-save", "info", "handing the structured save to the session");
348
- // The perk-objective-author pointer rides the skill-binding suffix (D5) since a
349
- // warm /objective-save outside a stage:objective-author session gets none from Mechanism A.
350
- pi.sendUserMessage(
351
- objectiveSaveGuidance(title) + bindingSuffix(ctx.cwd, "stage:objective-author"),
352
- );
353
- return;
354
- }
355
- // Saved or save-failed: relay the save message. No node-link sub-step on the objective path,
356
- // so the severity ladder is simpler than /plan-save's (no warning tier).
357
- const result = outcome.result;
358
- const message = result.content[0]?.text ?? "objective-save done";
359
- const severity: Severity = result.details.ok ? "info" : "error";
360
- report(ctx, "objective-save", severity, message);
361
- },
362
- });
363
- }
@@ -1,140 +0,0 @@
1
- // The `plan_draft` file tool: the first session-data PRODUCER and the
2
- // narrow structural read-only-gate carve-out (session data dir only).
3
- //
4
- // Carve-out doctrine: the tool takes NO path/name parameter — the artifact name is the fixed
5
- // constant `PLAN_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-plan artifact in the current run's data dir (gitignored scratch). Allowlisting
8
- // its name in `READ_ONLY_TOOLS` (toolGating.ts) is therefore safe: the read-only invariant (the
9
- // worktree stays untouched) holds, and the gate's `tool_call` edit/write/bash blocking logic is
10
- // UNCHANGED. Full rewrite per call, non-terminating; NOT a save — `plan_save`/`/plan-save` still
11
- // persist to GitHub. Consumers read the draft only via `readSessionArtifact` (digest-validated,
12
- // fail-open); the artifact is consumed by `resolvePlanSource` (planSave.ts) — both save
13
- // surfaces prefer it over an explicit param and over the transcript scrape.
14
- //
15
- // Imports stay node builtins + sibling seams (sessionData.ts, toolParams.ts, result.ts) so the
16
- // module loads under `node --test`; no manual `scratch`/`runs` path segments (cacheGuard.test.ts).
17
-
18
- import { relative } from "node:path";
19
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
- import { failFor, ok, type Result } from "../substrate/result.ts";
21
- import {
22
- activeSessionRunId,
23
- digestSessionData,
24
- type SessionDataCtx,
25
- writeSessionArtifact,
26
- } from "../substrate/sessionData.ts";
27
- import { paramsOf, stringParam } from "../substrate/toolParams.ts";
28
- import type { EntrySink } from "../substrate/workflowState.ts";
29
- import type { ReportTarget } from "../surfaces/report.ts";
30
-
31
- /** The fixed working-plan artifact name (NOT `plan.md` — `cache.plan` is a different file). */
32
- export const PLAN_DRAFT_ARTIFACT = "plan-draft.md";
33
-
34
- /**
35
- * Decode unknown `plan_draft` tool-call params (the tool-boundary seam). `plan` absent decodes to
36
- * `""` (so the core's `invalid_input` arm owns the empty-plan message); present-but-mistyped →
37
- * null (strict-fail `bad_input`). Decode-before-side-effect.
38
- */
39
- export function decodePlanDraftParams(params: unknown): { plan: string } | null {
40
- const p = paramsOf(params);
41
- if (p === null) return null;
42
- const plan = stringParam(p, "plan");
43
- if (plan === null) return null;
44
- return { plan: plan ?? "" };
45
- }
46
-
47
- /** The ok-arm details — provenance-consistent with the recorded `session_artifacts` pointer. */
48
- export interface PlanDraftOk {
49
- name: string;
50
- path: string;
51
- digest: string;
52
- bytes: number;
53
- run_id: string;
54
- }
55
-
56
- export type PlanDraftResult = Result<PlanDraftOk>;
57
-
58
- /**
59
- * The core both the tool handler and tests call: write the working-plan artifact through the
60
- * accessor seam (file + `session_artifacts` provenance pointer). Soft result, never throws —
61
- * failure taxonomy: empty plan → `invalid_input`; no session run_id → `no_run_id`; file-or-pointer
62
- * write failure → `write_failed` (the seam already warned on stderr).
63
- */
64
- export function writePlanDraft(
65
- sink: EntrySink,
66
- ctx: SessionDataCtx & ReportTarget,
67
- plan: string,
68
- ): PlanDraftResult {
69
- const fail = failFor(ctx, "plan-draft");
70
-
71
- if (!plan.trim()) {
72
- return fail("no plan markdown to write (pass the full working draft)", "invalid_input");
73
- }
74
-
75
- const runId = activeSessionRunId(ctx);
76
- if (runId === null) {
77
- return fail("session has no run_id — cannot write the plan-draft artifact", "no_run_id");
78
- }
79
-
80
- const written = writeSessionArtifact(sink, ctx, PLAN_DRAFT_ARTIFACT, plan);
81
- if (written === null) {
82
- return fail(
83
- `could not write the ${PLAN_DRAFT_ARTIFACT} artifact (see warnings)`,
84
- "write_failed",
85
- );
86
- }
87
-
88
- // Derive digest/relative path consistently with the pointer the seam recorded.
89
- const digest = digestSessionData(plan);
90
- const relPath = relative(ctx.cwd, written);
91
- return ok(`Plan draft written → ${relPath} (${digest})`, {
92
- name: PLAN_DRAFT_ARTIFACT,
93
- path: relPath,
94
- digest,
95
- bytes: Buffer.byteLength(plan, "utf8"),
96
- run_id: runId,
97
- });
98
- }
99
-
100
- const TOOL_GUIDELINES = [
101
- "Call plan_draft to persist the current working draft as you author or revise the plan; pass the FULL plan markdown each time (it rewrites the whole draft).",
102
- "plan_draft never saves to GitHub and never ends the turn — plan_save//plan-save remain the canonical save surface.",
103
- ];
104
-
105
- /** Register the `plan_draft` tool (the carve-out producer; interior-only). */
106
- export function registerPlanDraft(pi: ExtensionAPI): void {
107
- pi.registerTool({
108
- name: "plan_draft",
109
- label: "Plan draft",
110
- description:
111
- "Write (or overwrite) the working plan draft to the session data dir and record its " +
112
- "provenance pointer. The only sanctioned write surface while read-only. NOT a save — " +
113
- "plan_save//plan-save still persist the plan to GitHub.",
114
- promptSnippet: "Persist the working plan draft to the session data dir (full rewrite)",
115
- promptGuidelines: TOOL_GUIDELINES,
116
- executionMode: "sequential",
117
- parameters: {
118
- type: "object",
119
- additionalProperties: false,
120
- required: ["plan"],
121
- properties: {
122
- plan: {
123
- type: "string",
124
- description: "The full working-plan markdown (rewrites the whole draft).",
125
- },
126
- },
127
- },
128
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
129
- const decoded = decodePlanDraftParams(params);
130
- if (decoded === null) {
131
- return failFor(
132
- ctx,
133
- "plan-draft",
134
- "plan_draft",
135
- )("plan_draft needs { plan: string } per the tool schema", "bad_input");
136
- }
137
- return writePlanDraft(pi, ctx, decoded.plan);
138
- },
139
- });
140
- }