@mgiles/perk 3.2.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 (202) 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 +365 -380
  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 -311
  49. package/extension/{doors/prReviewBrowser.ts → pi/v1/codeReview/browser.ts} +53 -33
  50. package/extension/{doors/hunkHandoff.ts → pi/v1/codeReview/checkout.ts} +12 -8
  51. package/extension/{doors/reviewWaveTools.ts → pi/v1/codeReview/reviewWave.ts} +146 -114
  52. package/extension/{doors/stackReviewBrowser.ts → pi/v1/codeReview/stack.ts} +62 -29
  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 +154 -267
  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 +123 -61
  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/pi/v1/lifecycleGates.ts +127 -0
  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/{factories/objectivePlan.ts → pi/v1/objectivePlanning.ts} +328 -533
  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 +73 -27
  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 -21
  112. package/extension/substrate/git.ts +88 -6
  113. package/extension/substrate/modelVisible.ts +53 -0
  114. package/extension/substrate/prompts.ts +22 -0
  115. package/extension/substrate/registry.ts +2 -0
  116. package/extension/substrate/resolverLease.ts +5 -4
  117. package/extension/substrate/sessionData.ts +85 -152
  118. package/extension/substrate/toolGating.ts +263 -84
  119. package/extension/substrate/unifiedDiff.ts +1 -1
  120. package/extension/substrate/workflowState.ts +178 -163
  121. package/extension/substrate/worktreeResolverLock.ts +261 -0
  122. package/extension/surfaces/surfaces.ts +79 -27
  123. package/extension/waves/adversarialReviewWave.ts +87 -46
  124. package/extension/waves/blockedReports.ts +59 -0
  125. package/extension/waves/draftReviewWave.ts +42 -42
  126. package/extension/waves/laneIdentity.ts +77 -0
  127. package/extension/waves/objectiveExplorerWave.ts +24 -24
  128. package/extension/waves/prReviewWave.ts +89 -77
  129. package/extension/waves/reportWave.ts +438 -578
  130. package/extension/waves/reviewClassifierWave.ts +22 -22
  131. package/extension/waves/rpcAdapter.ts +100 -15
  132. package/extension/waves/scoutWave.ts +192 -0
  133. package/extension/waves/transport.ts +480 -0
  134. package/extension/worker/sdkAdapter.ts +494 -0
  135. package/extension/worker/stageExecution.ts +679 -0
  136. package/extension/workerMain.ts +18 -19
  137. package/package.json +6 -4
  138. package/prompts/_fixtures/live.yaml +43 -18
  139. package/prompts/contexts/adapters/plannotator-gist.md +6 -0
  140. package/prompts/contexts/adapters/plannotator-objective.md +6 -0
  141. package/prompts/contexts/adapters/plannotator-plan.md +8 -1
  142. package/prompts/contexts/adapters/plannotator-refinement.md +22 -0
  143. package/prompts/contexts/objective-refinement.md +17 -0
  144. package/prompts/contexts/read-only.md +1 -1
  145. package/prompts/stages/conflict-resolution-continuation.md +9 -6
  146. package/prompts/stages/conflict-resolution.md +4 -4
  147. package/prompts/stages/objective-plan/guidance.md +2 -2
  148. package/prompts/stages/objective-plan/seed.md +9 -1
  149. package/prompts/stages/objective-reconcile-ready.md +1 -1
  150. package/prompts/stages/objective-reconcile.md +1 -1
  151. package/prompts/stages/objective-refine/seed.md +18 -0
  152. package/prompts/stages/objective-review-browser.md +4 -4
  153. package/prompts/stages/objective-sync.md +1 -1
  154. package/prompts/stages/plan-review-browser.md +4 -4
  155. package/prompts/stages/pr-review-browser/active.md +3 -4
  156. package/prompts/stages/pr-review-browser/foreign.md +3 -4
  157. package/prompts/stages/pr-review-terminal/active.md +3 -3
  158. package/prompts/stages/pr-review-terminal/foreign.md +3 -3
  159. package/prompts/stages/pr-review.md +3 -3
  160. package/prompts/stages/stack-review-browser/stack.md +5 -6
  161. package/shared/README.md +8 -0
  162. package/shared/bindings.yaml +3 -3
  163. package/shared/contracts.md +2601 -506
  164. package/shared/fixtures/issues-table.json +130 -0
  165. package/shared/registry.yaml +13 -0
  166. package/shared/schemas/outputs/objective-node-engagement.schema.json +318 -0
  167. package/shared/schemas/outputs/objective-stack-status.schema.json +6 -1
  168. package/shared/schemas/outputs/pr-review-context.schema.json +54 -9
  169. package/shared/schemas/outputs/pr-review-stack-context.schema.json +196 -0
  170. package/extension/adapters/planAdapterPlannotator.ts +0 -362
  171. package/extension/doors/auditWaveTools.ts +0 -352
  172. package/extension/doors/ciExecutor.ts +0 -756
  173. package/extension/doors/commitCompact.ts +0 -251
  174. package/extension/doors/dreamWaveTools.ts +0 -489
  175. package/extension/doors/learn.ts +0 -668
  176. package/extension/doors/lifecycleGates.ts +0 -207
  177. package/extension/doors/objectiveStack.ts +0 -1543
  178. package/extension/doors/prReviewDynamic.ts +0 -276
  179. package/extension/doors/ready.ts +0 -279
  180. package/extension/doors/submit.ts +0 -373
  181. package/extension/doors/submitPrReview.ts +0 -505
  182. package/extension/factories/gistAuthor.ts +0 -94
  183. package/extension/factories/gistDraft.ts +0 -265
  184. package/extension/factories/gistSave.ts +0 -251
  185. package/extension/factories/implementHere.ts +0 -116
  186. package/extension/factories/objectiveAuthor.ts +0 -98
  187. package/extension/factories/objectiveDraft.ts +0 -466
  188. package/extension/factories/objectiveSave.ts +0 -366
  189. package/extension/factories/planDraft.ts +0 -140
  190. package/extension/factories/planMode.ts +0 -205
  191. package/extension/factories/planReview.ts +0 -1237
  192. package/extension/factories/planSave.ts +0 -604
  193. package/extension/factories/planTitle.ts +0 -141
  194. package/extension/substrate/structuredOutput.ts +0 -202
  195. package/extension/waves/auditWave.ts +0 -312
  196. package/extension/waves/harvestWave.ts +0 -399
  197. package/extension/waves/learnWave.ts +0 -155
  198. package/extension/waves/memoryAdapter.ts +0 -139
  199. package/extension/waves/prReviewDynamicWave.ts +0 -777
  200. package/extension/worker/readOnlySession.ts +0 -294
  201. package/extension/worker/worker.ts +0 -899
  202. package/prompts/stages/pr-review-dynamic.md +0 -7
@@ -1,366 +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). On the dream arm the
91
- * reviewed CANONICAL parts cross to the Python plane through the run-scoped
92
- * `dream-report-transfer.json` handoff (§8.64) — staged atomically before the cold door (a
93
- * write failure is the soft `scratch_failed` refusal; the door is not invoked) — and
94
- * `perk objective create` re-validates the transfer and converges the companion idempotently.
95
- */
96
- export async function saveObjective(
97
- pi: ExtensionAPI,
98
- ctx: ExtensionContext,
99
- opts: {
100
- prose: string;
101
- title?: string;
102
- roadmap?: unknown[];
103
- base?: string;
104
- delivery?: DeliveryChoice;
105
- dream_report?: { input: unknown; generated_at?: string; parts?: string[] };
106
- },
107
- ): Promise<ObjectiveSaveResult> {
108
- const fail = failFor(ctx, "objective-save");
109
-
110
- const prose = opts.prose.trim();
111
- if (!prose)
112
- return fail("no objective prose to save (draft the objective first)", "invalid_input");
113
- if (opts.roadmap !== undefined && !Array.isArray(opts.roadmap)) {
114
- return fail("roadmap must be a JSON array of nodes", "invalid_input");
115
- }
116
-
117
- // The §8.63 fail-closed re-validation, before anything reaches the cold door. Presence is
118
- // the `opts.dream_report === undefined` boundary — an `{input: undefined}` carrier is never
119
- // constructed (the execute wraps only a present decoded value; the approval path passes the
120
- // validated artifact block).
121
- const generatedAt = opts.dream_report?.generated_at ?? new Date().toISOString();
122
- const gate =
123
- opts.dream_report === undefined
124
- ? resolveDreamReportGate(ctx, undefined, generatedAt)
125
- : resolveDreamReportGate(ctx, opts.dream_report.input, generatedAt);
126
- if (gate.kind === "refuse") {
127
- return fail(gate.detail, gate.errorType);
128
- }
129
- if (gate.kind === "block" && opts.dream_report?.parts !== undefined) {
130
- // The approval path: the reviewed (stored) parts must byte-match the re-render against
131
- // freshly recovered context — the same stored `generated_at` stamp keeps the comparison
132
- // deterministic.
133
- if (JSON.stringify(gate.block.parts) !== JSON.stringify(opts.dream_report.parts)) {
134
- return fail(
135
- "the reviewed report no longer matches the wave state — re-draft and re-review",
136
- "bad_state",
137
- );
138
- }
139
- }
140
-
141
- const branch = () => branchOf(ctx);
142
- const runId = rebuildWorkflowState(branch()).run_id ?? "";
143
-
144
- if (gate.kind === "block") {
145
- // The §8.64 transfer write (the dream arm only): the reviewed CANONICAL parts cross to the
146
- // Python save door through the run-scoped scratch handoff — written atomically BEFORE the
147
- // cold door is invoked. A write failure is the soft `scratch_failed` failure (the
148
- // runColdDoor stdin-staging precedent): the cold door is NOT invoked, nothing activates,
149
- // and the read-only gate stays on. Non-dream saves write nothing (byte-identical).
150
- try {
151
- const dir = ensureRunScratch(ctx.cwd, runId);
152
- const content = `${JSON.stringify(
153
- { schema_version: "1", run_id: runId, parts: gate.block.parts },
154
- null,
155
- 2,
156
- )}\n`;
157
- atomicWriteFileSync(join(dir, DREAM_REPORT_TRANSFER_FILENAME), content);
158
- } catch (err) {
159
- return fail(`could not stage the dream-report transfer: ${String(err)}`, "scratch_failed");
160
- }
161
- }
162
-
163
- const args = ["objective", "create", "--json"];
164
- if (opts.title) args.push("--title", opts.title);
165
- if (opts.base) args.push("--base", opts.base);
166
- // The reviewed delivery choice rides verbatim; the cold door owns validation + preflight.
167
- if (opts.delivery) args.push("--delivery", opts.delivery);
168
- if (runId) args.push("--run-id", runId);
169
- if (opts.roadmap && opts.roadmap.length > 0) {
170
- args.push("--roadmap", JSON.stringify(opts.roadmap));
171
- }
172
- const r = await runColdDoor<ObjectiveCreatePayload>(pi, ctx, args, {
173
- label: "perk objective create",
174
- decode: decodeObjectiveCreate,
175
- stdin: { flag: "--body", content: prose, filename: "objective.md" },
176
- });
177
- if (!r.ok) return fail(r.message, r.errorType);
178
-
179
- // Link the live session: set active_objective (LWW) + seed a fresh budget activation marker
180
- // (mirrors objective.ts's `/objective <id>` activation), so budget tracking starts immediately.
181
- // The envelope id is already the opaque string id (§8.21) — no coercion needed.
182
- const objective = r.data.objective;
183
- const objectiveId = objective.id;
184
- const linked = rebuildWorkflowState(branch()).active_objective ?? null;
185
- if (linked !== objectiveId) {
186
- appendWorkflowState(pi, ctx, {
187
- data: { active_objective: objectiveId },
188
- field: "active_objective",
189
- expected: objectiveId,
190
- scope: "objective-save",
191
- failure: `active_objective read-back failed for #${objectiveId}`,
192
- });
193
- pi.appendEntry(OBJECTIVE_BUDGET_TYPE, {
194
- objective_id: objectiveId,
195
- activated_at: new Date().toISOString(),
196
- });
197
- }
198
-
199
- const verb = objective.existed ? "Found existing" : "Saved";
200
- return ok(
201
- `${verb} objective #${objective.id} → ${objective.url}`,
202
- {
203
- objective: { id: objective.id, url: objective.url },
204
- existed: objective.existed ?? null,
205
- },
206
- { terminate: true },
207
- );
208
- }
209
-
210
- /** The approval→save orchestration outcome (the objective `ApprovalSaveOutcome`). */
211
- export type ObjectiveApprovalSaveOutcome =
212
- | { status: "no-draft" }
213
- | { status: "saved" | "save-failed"; result: ObjectiveSaveResult; gateExited: boolean };
214
-
215
- /**
216
- * The shared approval→save orchestration seam (the objective sibling of
217
- * planSave.ts's `approvalSave`): an APPROVED objective review (`plan_review`'s objective arm)
218
- * and the manual `/objective-save` failsafe both run THIS. Flow: re-read the STRUCTURED draft
219
- * artifact at save time (`readObjectiveDraft` — never the rendered markdown, never in-hand
220
- * bytes) → `saveObjective` → gate exit on a successful save while read-only (the D1a pattern:
221
- * snapshot `gating.isActive()` before the save; a failed save leaves the gate ON). No draft →
222
- * `no-draft` (nothing saved, the gate untouched); callers render their own fallback. Title
223
- * precedence: an explicit `opts.title` wins; else the draft's `title`; else the cold door
224
- * derives from the prose heading. The returned result keeps `saveObjective`'s `terminate: true`
225
- * for tool-path callers.
226
- */
227
- export async function objectiveApprovalSave(
228
- pi: ExtensionAPI,
229
- ctx: ExtensionContext,
230
- gating: ToolGating,
231
- opts: { title?: string } = {},
232
- ): Promise<ObjectiveApprovalSaveOutcome> {
233
- const draft = readObjectiveDraft(ctx);
234
- if (draft === null) return { status: "no-draft" };
235
- // D1a: snapshot the gate BEFORE the save; on success, exit it so save marks the read-only →
236
- // read-write boundary in one gesture. A failed save leaves the gate on.
237
- const wasReadOnly = gating.isActive();
238
- const result = await saveObjective(pi, ctx, {
239
- prose: draft.prose,
240
- title: opts.title ?? draft.title,
241
- roadmap: draft.roadmap,
242
- base: draft.base,
243
- delivery: draft.delivery,
244
- // The approval path passes the artifact block through whole — stored stamp + stored parts
245
- // (the save re-validates and byte-compares, §8.63).
246
- ...(draft.dream_report !== undefined ? { dream_report: draft.dream_report } : {}),
247
- });
248
- let gateExited = false;
249
- if (result.details.ok && wasReadOnly) {
250
- gating.exit(ctx);
251
- gateExited = true;
252
- }
253
- return { status: result.details.ok ? "saved" : "save-failed", result, gateExited };
254
- }
255
-
256
- const TOOL_GUIDELINES = [
257
- "Use objective_save only after the objective + roadmap are decision-complete; it creates the canonical perk:objective issue, activates it, and ends the turn.",
258
- "Pass objective_save the objective PROSE in `prose` and the STRUCTURED roadmap in `roadmap` (a JSON array of nodes) — never hand-write roadmap YAML.",
259
- '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.',
260
- ];
261
-
262
- /**
263
- * The seed guidance the warm `/objective-save` injects to drive the structured save (the
264
- * perk-objective-author skill pointer rides the skill-binding suffix — not hardcoded
265
- * here). Pure + exported for offline tests.
266
- */
267
- export function objectiveSaveGuidance(title?: string): string {
268
- const named = title?.trim() || "";
269
- return render("stages/objective-save.md", { title: named });
270
- }
271
-
272
- /** Register the warm door: the `objective_save` tool (canonical) + the `/objective-save` twin. */
273
- export function registerObjectiveSave(pi: ExtensionAPI, gating: ToolGating): void {
274
- pi.registerTool({
275
- name: "objective_save",
276
- label: "Save objective",
277
- description:
278
- "Persist a drafted objective + structured roadmap to GitHub as a perk:objective issue, " +
279
- "activate it, and start budget tracking. Terminating: ends the turn on save. Call only when " +
280
- "the objective and roadmap are decision-complete.",
281
- promptSnippet: "Save the decision-complete objective + roadmap to GitHub (terminates the turn)",
282
- promptGuidelines: TOOL_GUIDELINES,
283
- executionMode: "sequential",
284
- parameters: {
285
- type: "object",
286
- additionalProperties: false,
287
- required: ["prose"],
288
- properties: {
289
- prose: {
290
- type: "string",
291
- description: "The objective prose (the why, the design, the boundaries/non-goals).",
292
- },
293
- title: {
294
- type: "string",
295
- description: "Optional objective title (defaults to the prose's first heading).",
296
- },
297
- base: {
298
- type: "string",
299
- description:
300
- "Optional target branch for this objective's plans (omit to use the repo default).",
301
- },
302
- delivery: DELIVERY_PARAM_SCHEMA,
303
- dream_report: DREAM_REPORT_PARAM_SCHEMA,
304
- roadmap: {
305
- type: "array",
306
- description:
307
- "The structured roadmap: a JSON array of nodes. Never hand-write roadmap YAML.",
308
- items: ROADMAP_PARAM_SCHEMA,
309
- },
310
- },
311
- },
312
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
313
- const decoded = decodeObjectiveSaveParams(params);
314
- if (decoded === null) {
315
- return failFor(
316
- ctx,
317
- "objective-save",
318
- "objective_save",
319
- )(
320
- "objective_save needs { prose: string, roadmap?: array } per the tool schema",
321
- "bad_input",
322
- );
323
- }
324
- // The direct tool path wraps ONLY a present decoded value as the `{input}` carrier (the
325
- // save stamps generated_at); no stored parts, so no byte-compare on this path.
326
- const { dream_report, ...rest } = decoded;
327
- return saveObjective(pi, ctx, {
328
- ...rest,
329
- ...(dream_report !== undefined ? { dream_report: { input: dream_report } } : {}),
330
- });
331
- },
332
- });
333
-
334
- registerPerkCommand(pi, "objective-save", {
335
- description:
336
- "Save the working objective draft to GitHub — the manual failsafe for the approval→save " +
337
- "flow (artifact-first; drives the structured save only when no draft exists).",
338
- handler: async (args, ctx) => {
339
- const title = args.trim() || undefined;
340
- // The artifact-first manual-failsafe invocation of the shared approval→save
341
- // seam (the D1a gate exit lives in the seam). The legacy drive-the-session behavior is kept
342
- // as the NO-DRAFT fallback — objectives have no transcript scrape by design, so a draftless
343
- // session still needs a working save path.
344
- const outcome = await objectiveApprovalSave(pi, ctx, gating, { title });
345
- if (outcome.status === "no-draft") {
346
- // Exit the read-only gate so the objective_save tool (excluded from READ_ONLY_TOOLS)
347
- // becomes reachable on the driven turn, then drive the turn (mirrors /address and
348
- // /objective-plan).
349
- if (gating.isActive()) gating.exit(ctx);
350
- report(ctx, "objective-save", "info", "handing the structured save to the session");
351
- // The perk-objective-author pointer rides the skill-binding suffix (D5) since a
352
- // warm /objective-save outside a stage:objective-author session gets none from Mechanism A.
353
- pi.sendUserMessage(
354
- objectiveSaveGuidance(title) + bindingSuffix(ctx.cwd, "stage:objective-author"),
355
- );
356
- return;
357
- }
358
- // Saved or save-failed: relay the save message. No node-link sub-step on the objective path,
359
- // so the severity ladder is simpler than /plan-save's (no warning tier).
360
- const result = outcome.result;
361
- const message = result.content[0]?.text ?? "objective-save done";
362
- const severity: Severity = result.details.ok ? "info" : "error";
363
- report(ctx, "objective-save", severity, message);
364
- },
365
- });
366
- }
@@ -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
- }