@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
@@ -0,0 +1,124 @@
1
+ // The objective planning feature: the roadmap-node → plan-generation policy — the bounded
2
+ // node-transition operation (with the completion-audit gate and the warm node-claim carrier
3
+ // maintenance). The reconcile/add-node writes carry no feature policy beyond their decoders,
4
+ // so they live adapter-tier in pi/v1/objectivePlanning.ts (direct cold-door calls — no port).
5
+ //
6
+ // The completion-audit gate is a property of the MODEL-FACING boundary only — NOT an invariant
7
+ // on the node-`done` state: the canonical `perk objective node --status done` (human/CI cold
8
+ // CLI) has no audit gate, and the auto-on-merge node-done deliberately sets `done` without one.
9
+ // Both are intentional non-audited paths; the structural refusal protects the model's path
10
+ // only. The "are we done?" judgment text lives in the perk-objective-plan skill.
11
+ //
12
+ // Decode-once-at-the-edge: inputs arrive TYPED (the pi/v1 decoders own the tool-boundary
13
+ // shape), so the ops re-validate nothing. Canonical mutations stay in the Python plane — the
14
+ // backends delegate; the objective's canonical state is the issue (re-read on demand), and the
15
+ // only session state these ops maintain is the warm `objective_node_claim` carrier, THROUGH the
16
+ // session seam.
17
+
18
+ import type { WorkflowChangeResult, WorkflowSession } from "../../session/workflowSession.ts";
19
+
20
+ /** The valid node statuses (mirrors the Python `objective.NodeStatus` StrEnum). */
21
+ export const NODE_STATUSES = [
22
+ "pending",
23
+ "planning",
24
+ "in_progress",
25
+ "done",
26
+ "blocked",
27
+ "skipped",
28
+ ] as const;
29
+ export type NodeStatus = (typeof NODE_STATUSES)[number];
30
+
31
+ /** The minimum trimmed length of a non-trivial completion `audit` (the pinnable predicate). */
32
+ export const MIN_AUDIT_LENGTH = 40;
33
+
34
+ /** A non-trivial audit iff it is a string whose value after `.trim()` is ≥ MIN_AUDIT_LENGTH. */
35
+ export function isNonTrivialAudit(audit: unknown): boolean {
36
+ return typeof audit === "string" && audit.trim().length >= MIN_AUDIT_LENGTH;
37
+ }
38
+
39
+ /** The typed `objective_node` input (`objective` is the opaque §8.21 string id). */
40
+ export interface ObjectiveNodeInput {
41
+ objective: string;
42
+ node: string;
43
+ status?: NodeStatus;
44
+ pr?: string;
45
+ description?: string;
46
+ audit?: string;
47
+ }
48
+
49
+ /** The narrow exterior port the node transition writes through (the cold door in pi/v1). */
50
+ export interface ObjectiveNodeBackend {
51
+ transition(req: {
52
+ objective: string;
53
+ node: string;
54
+ status?: NodeStatus;
55
+ pr?: string;
56
+ description?: string;
57
+ }): Promise<
58
+ | { status: "ok"; commentUpdated: boolean }
59
+ | { status: "failed"; message: string; errorType: string }
60
+ >;
61
+ }
62
+
63
+ /**
64
+ * The transition outcome. `claimChange` is the session seam's own `WorkflowChangeResult`
65
+ * VERBATIM (`null` = not attempted: a pr/description-only call leaves the carrier untouched);
66
+ * the adapter's rendering ignores it (the seam's report() stays the loudness channel — a failed
67
+ * claim append never fails the tool result).
68
+ */
69
+ export type TransitionObjectiveNodeOutcome =
70
+ | { status: "ok"; commentUpdated: boolean; claimChange: WorkflowChangeResult | null }
71
+ | { status: "failed"; message: string; errorType: string };
72
+
73
+ /**
74
+ * The bounded `objective_node` transition: the completion-audit gate (`status:"done"` requires
75
+ * a non-trivial `audit` — model-path-only) → the no-change refusal (neither status nor pr nor
76
+ * description) → `backend.transition` → on success, maintain the warm node-link carrier
77
+ * THROUGH THE SEAM: `planning` records the claim (the exact moment the warm factory learns the
78
+ * node id; an idempotent re-claim short-circuits `unchanged`); any other explicit status clears
79
+ * it (the seam's both-field match — an unrelated claim is never clobbered); no status change
80
+ * leaves it untouched. Never throws.
81
+ */
82
+ export async function transitionObjectiveNode(
83
+ input: ObjectiveNodeInput,
84
+ deps: { backend: ObjectiveNodeBackend; session: WorkflowSession },
85
+ ): Promise<TransitionObjectiveNodeOutcome> {
86
+ // The completion-audit gate (model-path-only): `status:"done"` requires a non-trivial `audit`.
87
+ if (input.status === "done" && !isNonTrivialAudit(input.audit)) {
88
+ return {
89
+ status: "failed",
90
+ message:
91
+ `setting a node to "done" requires a completion audit (a requirement→evidence mapping of ` +
92
+ `at least ${MIN_AUDIT_LENGTH} characters) — confirm the work actually landed first.`,
93
+ errorType: "audit_required",
94
+ };
95
+ }
96
+ if (input.status === undefined && input.pr === undefined && input.description === undefined) {
97
+ return {
98
+ status: "failed",
99
+ message: "objective_node needs a `status`, a `pr`, or a `description` to change",
100
+ errorType: "bad_input",
101
+ };
102
+ }
103
+
104
+ const transitioned = await deps.backend.transition({
105
+ objective: input.objective,
106
+ node: input.node,
107
+ ...(input.status !== undefined ? { status: input.status } : {}),
108
+ ...(input.pr !== undefined ? { pr: input.pr } : {}),
109
+ ...(input.description !== undefined ? { description: input.description } : {}),
110
+ });
111
+ if (transitioned.status === "failed") return transitioned;
112
+
113
+ // Maintain the warm node-link carrier off the successful transition (best-effort: a failed
114
+ // append is loud via the seam's report() but never fails the tool result).
115
+ let claimChange: WorkflowChangeResult | null = null;
116
+ if (input.status !== undefined) {
117
+ const claim = { objective: input.objective, node: input.node };
118
+ claimChange =
119
+ input.status === "planning"
120
+ ? deps.session.apply({ kind: "record-node-claim", claim })
121
+ : deps.session.apply({ kind: "clear-node-claim", claim });
122
+ }
123
+ return { status: "ok", commentUpdated: transitioned.commentUpdated, claimChange };
124
+ }
@@ -0,0 +1,103 @@
1
+ // Feature-owned prose + identity constants for the objective flows — the stage ids the
2
+ // providers/doors key off, the objective-authoring context (contracts.md §8.57: live state +
3
+ // pointers only; the flow is stated by the launch statement, the detail by the bound skill),
4
+ // and the seed guidance the warm doors inject (`/objective-plan`, `/objective-reconcile`,
5
+ // `/objective-save`). Everything renders through the shared template seam
6
+ // (`substrate/prompts.ts` — a mechanism import, §8.31); branching stays in code.
7
+ //
8
+ // Pi-free (guard Rule D covers `authoring/` by prefix). Tool-guideline arrays deliberately do
9
+ // NOT live here: they stay inline at the `pi/v1` registration sites (the prose-review
10
+ // workbench's source-shape constraint).
11
+
12
+ import { render } from "../../substrate/prompts.ts";
13
+
14
+ /** The registry stage id of the objective-authoring session (shared with planMode's defer check). */
15
+ export const OBJECTIVE_AUTHOR_STAGE = "objective-author";
16
+
17
+ /** The registry stage id of the structured-save session (`perk objective save`). */
18
+ export const OBJECTIVE_SAVE_STAGE = "objective-save";
19
+
20
+ /** The objective-authoring context customType (distinct from planMode's `perk:plan-context`). */
21
+ export const OBJECTIVE_AUTHOR_CONTEXT_TYPE = "perk:objective-author-context";
22
+
23
+ /** The dedup/strip marker carried by the injected objective-authoring context. */
24
+ export const OBJECTIVE_AUTHOR_MARKER = "[OBJECTIVE AUTHORING]";
25
+
26
+ /**
27
+ * The objective-authoring session context: live state + pointers only (contracts.md §8.57 — the
28
+ * flow is stated by the launch statement, the detail by the `perk-objective-author` skill). It
29
+ * names the working-draft artifact (`objective_draft`), the review tool (`plan_review`), and
30
+ * the bound skill; it never restates the flow. Prompting, NOT enforcement (the tool gate is the
31
+ * enforcement).
32
+ */
33
+ export const OBJECTIVE_AUTHORING_CONTEXT = render("contexts/objective-authoring.md", {
34
+ marker: OBJECTIVE_AUTHOR_MARKER,
35
+ });
36
+
37
+ /**
38
+ * Build the full objective-authoring injection, appending the project-config addendum when
39
+ * present. Pure: the adapter passes `loadPerkConfig(cwd).planAuthoring` (the same `[workflow]
40
+ * plan_authoring` addendum the plan-authoring injection consumes — verbatim reuse).
41
+ */
42
+ export function objectiveAuthoringContextContent(addendum: string | undefined): string {
43
+ return addendum
44
+ ? `${OBJECTIVE_AUTHORING_CONTEXT}\n\n${addendum.trim()}`
45
+ : OBJECTIVE_AUTHORING_CONTEXT;
46
+ }
47
+
48
+ /**
49
+ * Backend-aware supplemental clause for the objective-read step of the factory prompts.
50
+ * The wording lives in `prompts/common/objective-read/linear.md`, rendered identically by both
51
+ * planes via the shared render seam (contracts.md §8.31); branching stays in code. github (and any
52
+ * non-linear) → "" (the `perk objective show` step already covers it); linear → the Project URL +
53
+ * the linear_get_issue/linear_list_comments tools (an `open <url>` fallback when the url is known).
54
+ */
55
+ export function objectiveReadInstruction(
56
+ backend: string,
57
+ objectiveId: string,
58
+ url: string,
59
+ ): string {
60
+ if (backend !== "linear") return "";
61
+ const where = url ? `(${url})` : `(run \`perk objective show ${objectiveId}\` for its URL)`;
62
+ const fallback = url ? `; if the linear tools are unavailable, open ${url}` : "";
63
+ return render("common/objective-read/linear.md", { where, fallback });
64
+ }
65
+
66
+ /** The seed guidance the warm `/objective-plan` injects to start the factory loop (the
67
+ * perk-objective-plan skill pointer rides the skill-binding suffix — not hardcoded).
68
+ * The loop is file-first (`plan_draft` → `plan_review` → approval-driven save); the node link
69
+ * rides the `objective_node_claim` carrier recorded by the unconditional `planning` mark.
70
+ * The OPTIONAL explore step is ONE `explore_objective_node` call — the tool owns the wave
71
+ * mechanics, the report schema, and reads the configured `[models.subagents] objective-explorer`
72
+ * model at execute time. */
73
+ export function factoryGuidance(
74
+ objective: string,
75
+ node: string | null,
76
+ backend = "github",
77
+ url = "",
78
+ ): string {
79
+ const readClause = objectiveReadInstruction(backend, objective, url);
80
+ return render("stages/objective-plan/guidance.md", {
81
+ objective,
82
+ node: node ?? "",
83
+ read_clause: readClause,
84
+ });
85
+ }
86
+
87
+ /** The seed guidance the warm `/objective-reconcile` injects to start the reconcile pass (the
88
+ * perk-objective-reconcile skill pointer rides the skill-binding suffix — not
89
+ * hardcoded). */
90
+ export function reconcileGuidance(objective: string, backend = "github", url = ""): string {
91
+ const readClause = objectiveReadInstruction(backend, objective, url);
92
+ return render("stages/objective-reconcile.md", { objective, read_clause: readClause });
93
+ }
94
+
95
+ /**
96
+ * The seed guidance the warm `/objective-save` injects to drive the structured save (the
97
+ * perk-objective-author skill pointer rides the skill-binding suffix — not hardcoded
98
+ * here). Pure + exported for offline tests.
99
+ */
100
+ export function objectiveSaveGuidance(title?: string): string {
101
+ const named = title?.trim() || "";
102
+ return render("stages/objective-save.md", { title: named });
103
+ }
@@ -0,0 +1,128 @@
1
+ // The objective review feature: the `ObjectiveDraftReviewer` role (one production adapter per
2
+ // review backend — plannotator bridge or first-party editor, both built in pi/v1 — plus a
3
+ // scripted fake in the tests) and the one-entry `reviewObjectiveDraft` operation.
4
+ //
5
+ // Ordering is the review door's exact discipline: resume FIRST (the validated artifact only —
6
+ // objectives have no param/transcript tier; none ⇒ `noDraft`) → render (the reviewed bytes are
7
+ // ALWAYS the rendered markdown — the review-surface law: JSON is storage/transport, never the
8
+ // human surface) → review → the abort checkpoint → route the verdict. Provider vocabulary
9
+ // (plannotator's `# Direct Edits` heading) is translated INTO `ObjectiveReviewOutcome` at the
10
+ // adapter — the feature sees typed variants only.
11
+ //
12
+ // No implement-here arm (the no-save exit is plan-arm-only — contracts §8.23) and no
13
+ // edited-bytes channel: objective reviews are VIEW-ONLY (rendered-markdown edits cannot fold
14
+ // back into the structured draft mechanically), so `approvedDirectEdits` is the revise-round
15
+ // refusal arm — NOTHING saved, the gate untouched; deny+feedback is the change channel.
16
+ //
17
+ // Cancellation ownership: `signal?.aborted` is checked at entry AND re-checked immediately
18
+ // after `reviewer.review` resolves, BEFORE any effect (save, gate exit) — abort ⇒ the `aborted`
19
+ // arm with nothing saved and the gate untouched.
20
+
21
+ import type { WorkflowSession } from "../../session/workflowSession.ts";
22
+ import { renderObjectiveDraft, resumeObjectiveDraft } from "./draft.ts";
23
+
24
+ /**
25
+ * The reviewer's verdict on the rendered objective bytes. `approvedDirectEdits` carries the
26
+ * FULL raw feedback (the adapter renders the revise-round guidance with it); the heading check
27
+ * that produces it stays adapter-side (provider vocabulary).
28
+ */
29
+ export type ObjectiveReviewOutcome =
30
+ | { status: "approved"; feedback?: string; reviewId?: string }
31
+ | { status: "approvedDirectEdits"; rawFeedback: string; reviewId?: string }
32
+ | { status: "denied"; feedback?: string; reviewId?: string }
33
+ | { status: "dismissed" }
34
+ | { status: "aborted" }
35
+ | { status: "unavailable"; warning: string };
36
+
37
+ /** The reviewer role: judge the rendered objective markdown (view-only). */
38
+ export interface ObjectiveDraftReviewer {
39
+ review(rendered: string, signal?: AbortSignal): Promise<ObjectiveReviewOutcome>;
40
+ }
41
+
42
+ /**
43
+ * The structural approval-save shape the review op routes over. The adapter binds a RICHER twin
44
+ * (`objectiveApprovalSaveV1` — arms carrying rendered results + `gateExited`); the arms flow
45
+ * through VERBATIM via the generic, so no information is erased and no cast is needed.
46
+ */
47
+ export interface ObjectiveApprovalSaveShape {
48
+ status: "no-draft" | "refused-draft" | "saved" | "save-failed";
49
+ }
50
+
51
+ /** The one-entry review outcome — each arm carries exactly what its caller renders.
52
+ * `refusedDraft` is the pre-review fail-closed stop for an invalid artifact (the approval-time
53
+ * race arm rides the save shape verbatim through `approvedSave`). */
54
+ export type ReviewObjectiveDraftResult<A extends ObjectiveApprovalSaveShape> =
55
+ | { status: "noDraft" }
56
+ | { status: "refusedDraft"; problem: string }
57
+ | { status: "approvedSave"; save: A; feedback?: string; reviewId?: string }
58
+ | { status: "approvedDirectEdits"; rawFeedback: string; reviewId?: string }
59
+ | { status: "denied"; feedback?: string; reviewId?: string }
60
+ | { status: "dismissed" }
61
+ | { status: "aborted" }
62
+ | { status: "unavailable"; warning: string };
63
+
64
+ /**
65
+ * Review the working objective draft end-to-end: resume (none ⇒ `noDraft` — the adapter renders
66
+ * the `no_objective_draft` skip) → render → review → the abort checkpoint → route. Routing:
67
+ * `approved` ⇒ `approvalSave()` (the injected orchestration re-resumes the draft itself — the
68
+ * artifact is the save source, never the rendered bytes; one extra validated read, no
69
+ * observable delta) · `approvedDirectEdits` ⇒ the revise-round refusal arm — NOTHING saved, the
70
+ * gate untouched · `denied`/`dismissed`/`aborted`/`unavailable` pass through. Never throws.
71
+ */
72
+ export async function reviewObjectiveDraft<A extends ObjectiveApprovalSaveShape>(
73
+ deps: {
74
+ session: WorkflowSession;
75
+ reviewer: ObjectiveDraftReviewer;
76
+ approvalSave: () => Promise<A>;
77
+ },
78
+ signal?: AbortSignal,
79
+ ): Promise<ReviewObjectiveDraftResult<A>> {
80
+ if (signal?.aborted) return { status: "aborted" };
81
+ const resumed = resumeObjectiveDraft(deps.session);
82
+ if (resumed.kind === "absent") return { status: "noDraft" };
83
+ if (resumed.kind === "refused") return { status: "refusedDraft", problem: resumed.problem };
84
+
85
+ const outcome = await deps.reviewer.review(renderObjectiveDraft(resumed.draft), signal);
86
+ // The abort checkpoint: a turn interrupted while the reviewer ran must produce NO effect —
87
+ // no save, no gate exit (the aborted arm wins over any verdict).
88
+ if (signal?.aborted) return { status: "aborted" };
89
+
90
+ return completeObjectiveReview(outcome, deps.approvalSave);
91
+ }
92
+
93
+ /** Subject policy only: callers authorize effects before entering this completion seam. */
94
+ export async function completeObjectiveReview<A extends ObjectiveApprovalSaveShape>(
95
+ outcome: ObjectiveReviewOutcome,
96
+ approvalSave: () => Promise<A>,
97
+ ): Promise<ReviewObjectiveDraftResult<A>> {
98
+ switch (outcome.status) {
99
+ case "approved":
100
+ return {
101
+ status: "approvedSave",
102
+ save: await approvalSave(),
103
+ ...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
104
+ ...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
105
+ };
106
+ case "approvedDirectEdits":
107
+ // The revise-round refusal: rendered-markdown edits cannot fold back into the structured
108
+ // draft mechanically — nothing saved, the gate untouched; the adapter renders the
109
+ // revise guidance with the FULL feedback.
110
+ return {
111
+ status: "approvedDirectEdits",
112
+ rawFeedback: outcome.rawFeedback,
113
+ ...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
114
+ };
115
+ case "denied":
116
+ return {
117
+ status: "denied",
118
+ ...(outcome.feedback !== undefined ? { feedback: outcome.feedback } : {}),
119
+ ...(outcome.reviewId !== undefined ? { reviewId: outcome.reviewId } : {}),
120
+ };
121
+ case "dismissed":
122
+ return { status: "dismissed" };
123
+ case "aborted":
124
+ return { status: "aborted" };
125
+ case "unavailable":
126
+ return { status: "unavailable", warning: outcome.warning };
127
+ }
128
+ }
@@ -0,0 +1,224 @@
1
+ // The objective save feature: the narrow exterior `ObjectiveBackend` port (one production
2
+ // adapter — the `perk objective create` cold door in pi/v1 — plus one deterministic fake in the
3
+ // tests: the port admission rule), the `saveObjective` operation, and the shared
4
+ // APPROVED-review → save orchestration `objectiveApprovalSave` (the `gistApprovalSave`/
5
+ // `planApprovalSave` mirror — three sibling flows, deliberately unshared shapes).
6
+ //
7
+ // Identity-less saves stay legal: `session.runId === null` ⇒ the backend sees `runId: null`
8
+ // (the adapter omits `--run-id`) and the LINKAGE op still runs — workflow-state appends are
9
+ // branch-backed and identity-independent (today's behavior, preserved). Backend saves are not
10
+ // abortable mid-flight (stated, not changed).
11
+ //
12
+ // The §8.63 dream gate arrives INJECTED (ctx-bound by the adapter); the §8.64 transfer staging
13
+ // (the reviewed CANONICAL parts crossing to the Python plane) is the ADAPTER's — the port's
14
+ // `dreamParts` carries the gate-proven parts and the adapter stages the run-scoped
15
+ // `dream-report-transfer.json` handoff atomically before the cold door.
16
+
17
+ import type { WorkflowChangeResult, WorkflowSession } from "../../session/workflowSession.ts";
18
+ import { type ApprovalGate, saveThroughApprovalGate } from "../review/approvalGate.ts";
19
+ import type { DeliveryChoice } from "./draft.ts";
20
+ import { resumeObjectiveDraft } from "./draft.ts";
21
+ import type { DreamReportGateOutcome, ObjectiveDreamReportBlock } from "./dreamReportGate.ts";
22
+
23
+ /** The backend save facts (`id` is the opaque string objective id — contracts §8.21). */
24
+ export type ObjectiveBackendSaveResult =
25
+ | { status: "saved"; id: string; url: string; existed: boolean | null }
26
+ | { status: "failed"; message: string; errorType: string };
27
+
28
+ /**
29
+ * The narrow exterior port the save operation writes through. `runId: null` means the caller
30
+ * has no session identity — the adapter omits its run linkage (an identity-less save keeps
31
+ * working). `dreamParts` (the §8.63 gate-proven CANONICAL parts) makes the adapter stage the
32
+ * §8.64 transfer handoff before invoking the door.
33
+ */
34
+ export interface ObjectiveBackend {
35
+ create(req: {
36
+ prose: string;
37
+ title?: string;
38
+ base?: string;
39
+ delivery?: DeliveryChoice;
40
+ roadmap?: unknown[];
41
+ runId: string | null;
42
+ dreamParts?: string[];
43
+ }): Promise<ObjectiveBackendSaveResult>;
44
+ }
45
+
46
+ /**
47
+ * The §8.63 dream-report carrier's two proven sources, as a discriminated union so a partial
48
+ * "reviewed" bag cannot compile: the direct tool path supplies only the raw `input` (the save
49
+ * stamps `generated_at` fresh); the approval path passes the reviewed artifact block through
50
+ * WHOLE — stored stamp AND stored parts (byte-compared against the fresh re-render).
51
+ */
52
+ export type DreamReportCarrier =
53
+ | { source: "direct"; input: unknown }
54
+ | { source: "reviewed"; block: ObjectiveDreamReportBlock };
55
+
56
+ /** The typed save input — decode owns the shape at the tool boundary (pi/v1). */
57
+ export interface SaveObjectiveInput {
58
+ prose: string;
59
+ title?: string;
60
+ roadmap?: unknown[];
61
+ base?: string;
62
+ delivery?: DeliveryChoice;
63
+ dream_report?: DreamReportCarrier;
64
+ }
65
+
66
+ /**
67
+ * The save outcome. The saved arm carries the backend facts plus the session seam's own
68
+ * `WorkflowChangeResult` linkage VERBATIM (the `SavePlanOutcome` precedent; `null` = not
69
+ * attempted — a failed save never touches the session). The adapter renders the terminating
70
+ * "Saved/Found existing objective #id → url" twin and appends the budget activation marker iff
71
+ * the linkage was attempted and not `unchanged` (byte-equivalent to the historical
72
+ * `linked !== objectiveId` guard — applied/unverified/rejected all imply "differed", and the
73
+ * marker never keyed off the append's read-back result).
74
+ */
75
+ export type SaveObjectiveOutcome =
76
+ | {
77
+ status: "saved";
78
+ id: string;
79
+ url: string;
80
+ existed: boolean | null;
81
+ linkage: WorkflowChangeResult | null;
82
+ }
83
+ | { status: "failed"; message: string; errorType: string };
84
+
85
+ /** The dependency bag the save operation runs over (the adapter composes production values). */
86
+ export interface ObjectiveSaveDeps {
87
+ session: WorkflowSession;
88
+ backend: ObjectiveBackend;
89
+ /** The §8.63 gate, adapter-bound (`resolveDreamReportGate` over the production recovery capability). */
90
+ resolveDreamGate: (input: unknown, generatedAt: string) => DreamReportGateOutcome;
91
+ }
92
+
93
+ /**
94
+ * The single save operation every objective-save surface calls. Ordering preserved exactly:
95
+ * validate the non-blank prose → the §8.63 fail-closed gate re-validation (presence is the
96
+ * `input.dream_report === undefined` boundary — an `{input: undefined}` carrier is never
97
+ * constructed) → the approval path's stored-parts byte-compare (the same stored `generated_at`
98
+ * stamp keeps the comparison deterministic; drift/tamper between draft-write and save refuses
99
+ * `bad_state`, nothing saved) → `backend.create` with `runId: session.runId` → on success,
100
+ * link the live session (`apply({kind: "link-objective"})` — the seam appends iff the rebuilt
101
+ * `active_objective` differs, strict read-back). Whitespace-only `title`/`base` normalize to
102
+ * absent (trim-or-omit, matching the draft path — a blank `--title` can no longer reach the
103
+ * cold door). Never throws.
104
+ */
105
+ export async function saveObjective(
106
+ input: SaveObjectiveInput,
107
+ deps: ObjectiveSaveDeps,
108
+ ): Promise<SaveObjectiveOutcome> {
109
+ const prose = input.prose.trim();
110
+ if (!prose) {
111
+ return {
112
+ status: "failed",
113
+ message: "no objective prose to save (draft the objective first)",
114
+ errorType: "invalid_input",
115
+ };
116
+ }
117
+ const title = input.title?.trim() || undefined;
118
+ const base = input.base?.trim() || undefined;
119
+
120
+ const carrier = input.dream_report;
121
+ const generatedAt =
122
+ carrier?.source === "reviewed" ? carrier.block.generated_at : new Date().toISOString();
123
+ const gate =
124
+ carrier === undefined
125
+ ? deps.resolveDreamGate(undefined, generatedAt)
126
+ : deps.resolveDreamGate(
127
+ carrier.source === "reviewed" ? carrier.block.input : carrier.input,
128
+ generatedAt,
129
+ );
130
+ if (gate.kind === "refuse") {
131
+ return { status: "failed", message: gate.detail, errorType: gate.errorType };
132
+ }
133
+ if (gate.kind === "block" && carrier?.source === "reviewed") {
134
+ // The approval path: the reviewed (stored) parts must byte-match the re-render against
135
+ // freshly recovered context.
136
+ if (JSON.stringify(gate.block.parts) !== JSON.stringify(carrier.block.parts)) {
137
+ return {
138
+ status: "failed",
139
+ message: "the reviewed report no longer matches the wave state — re-draft and re-review",
140
+ errorType: "bad_state",
141
+ };
142
+ }
143
+ }
144
+
145
+ const saved = await deps.backend.create({
146
+ prose,
147
+ ...(title !== undefined ? { title } : {}),
148
+ ...(base !== undefined ? { base } : {}),
149
+ ...(input.delivery !== undefined ? { delivery: input.delivery } : {}),
150
+ ...(input.roadmap !== undefined ? { roadmap: input.roadmap } : {}),
151
+ runId: deps.session.runId,
152
+ ...(gate.kind === "block" ? { dreamParts: gate.block.parts } : {}),
153
+ });
154
+ if (saved.status === "failed") return saved;
155
+
156
+ // Link the live session: the seam appends iff the rebuilt `active_objective` differs, with a
157
+ // strict read-back — verbatim into the outcome (the seam's report() stays the loudness
158
+ // channel; a linkage failure is loud-but-non-fatal, the save stands).
159
+ const linkage = deps.session.apply({ kind: "link-objective", objective: saved.id });
160
+ return { status: "saved", id: saved.id, url: saved.url, existed: saved.existed, linkage };
161
+ }
162
+
163
+ /** The approval→save dependency bag: the save deps + the gate. */
164
+ export interface ObjectiveApprovalSaveDeps extends ObjectiveSaveDeps {
165
+ gate: ApprovalGate;
166
+ }
167
+
168
+ /** The approval→save orchestration outcome (the objective `ApprovalSaveOutcome`).
169
+ * `refused-draft` is the fail-closed stop for an invalid artifact: nothing saved, the gate
170
+ * never touched — distinct from `no-draft` (the genuine draft-less fallback arm). */
171
+ export type ObjectiveApprovalSaveOutcome =
172
+ | { status: "no-draft" }
173
+ | { status: "refused-draft"; problem: string }
174
+ | {
175
+ status: "saved";
176
+ result: Extract<SaveObjectiveOutcome, { status: "saved" }>;
177
+ gateExited: boolean;
178
+ }
179
+ | {
180
+ status: "save-failed";
181
+ result: Extract<SaveObjectiveOutcome, { status: "failed" }>;
182
+ gateExited: false;
183
+ };
184
+
185
+ /**
186
+ * The shared APPROVED-review → save orchestration (the objective sibling of `planApprovalSave`):
187
+ * an APPROVED objective review (`plan_review`'s objective arm) and the manual `/objective-save`
188
+ * failsafe both run THIS. Flow: re-read the STRUCTURED draft artifact at save time
189
+ * (`resumeObjectiveDraft` — never the rendered markdown, never in-hand bytes; the artifact's
190
+ * `dream_report` block passes through whole: stored stamp + stored parts) → `saveObjective`
191
+ * through `saveThroughApprovalGate` (the D1a invariant: snapshot before the save; exit only
192
+ * after a successful save while read-only; a failed save leaves the gate ON). No draft → `no-draft`
193
+ * (nothing saved, the gate untouched); a REFUSED draft → `refused-draft` before the gate
194
+ * snapshot (fail-closed stop — `gateExited` semantics never arise). Title precedence: an
195
+ * explicit `opts.title` wins; else the draft's `title`; else the cold door derives from the
196
+ * prose heading.
197
+ */
198
+ export async function objectiveApprovalSave(
199
+ deps: ObjectiveApprovalSaveDeps,
200
+ opts: { title?: string } = {},
201
+ ): Promise<ObjectiveApprovalSaveOutcome> {
202
+ const resumed = resumeObjectiveDraft(deps.session);
203
+ if (resumed.kind === "absent") return { status: "no-draft" };
204
+ if (resumed.kind === "refused") return { status: "refused-draft", problem: resumed.problem };
205
+ const draft = resumed.draft;
206
+ const title = opts.title ?? draft.title;
207
+ const { outcome: result, gateExited } = await saveThroughApprovalGate(deps.gate, () =>
208
+ saveObjective(
209
+ {
210
+ prose: draft.prose,
211
+ ...(title !== undefined ? { title } : {}),
212
+ roadmap: draft.roadmap,
213
+ ...(draft.base !== undefined ? { base: draft.base } : {}),
214
+ ...(draft.delivery !== undefined ? { delivery: draft.delivery } : {}),
215
+ ...(draft.dream_report !== undefined
216
+ ? { dream_report: { source: "reviewed" as const, block: draft.dream_report } }
217
+ : {}),
218
+ },
219
+ deps,
220
+ ),
221
+ );
222
+ if (result.status === "failed") return { status: "save-failed", result, gateExited: false };
223
+ return { status: "saved", result, gateExited };
224
+ }
@@ -0,0 +1,84 @@
1
+ // The plan working-draft feature: the fixed artifact constant and the two draft operations over
2
+ // the WorkflowSession seam. Unlike the gist sibling there is NO encode/decode layer — the
3
+ // artifact is raw plan markdown (no JSON envelope), so a revision is the bytes themselves.
4
+ //
5
+ // Carve-out doctrine (mirrors `authoring/gist/`): the artifact name is the fixed constant
6
+ // `PLAN_DRAFT_ARTIFACT` and every byte flows through the session seam (file + verified
7
+ // `session_artifacts` pointer), so the only thing the draft ops can ever touch is the one
8
+ // working-plan artifact in the current run's data dir (gitignored scratch). A revision is a
9
+ // WHOLE-VALUE replacement — full rewrite per call, never a save (`plan_save`/`/plan-save` still
10
+ // persist to GitHub).
11
+
12
+ import type { SessionArtifactReceipt, WorkflowSession } from "../../session/workflowSession.ts";
13
+
14
+ /** The fixed working-plan artifact name (NOT `plan.md` — `cache.plan` is a different file). */
15
+ export const PLAN_DRAFT_ARTIFACT = "plan-draft.md";
16
+
17
+ /**
18
+ * The revise outcome. `rejected` splits by `reason` so the adapter renders the exact failure
19
+ * taxonomy it always had: `blank_plan` (input refused), `no_identity` (an identity-less
20
+ * session), and `write_refused` (the seam refused before any effect); `unverified` means an
21
+ * effect may have landed but the read-back proof failed. `problem` carries the caller-facing
22
+ * message bytes.
23
+ */
24
+ export type RevisePlanDraftResult =
25
+ | { status: "revised"; receipt: SessionArtifactReceipt; bytes: number }
26
+ | { status: "unchanged"; receipt: SessionArtifactReceipt; bytes: number }
27
+ | { status: "rejected"; reason: "blank_plan" | "no_identity" | "write_refused"; problem: string }
28
+ | { status: "unverified"; problem: string };
29
+
30
+ /**
31
+ * Rewrite the working plan draft (a whole-value replacement) through the session seam.
32
+ * Diagnostic precedence preserved: a blank plan is refused FIRST, missing identity second (the
33
+ * identity-optional session classifies `runId: null` — an identity-less caller still opens),
34
+ * then the verified artifact write. A byte-identical rewrite short-circuits `unchanged` (the
35
+ * session engine owns the probe). Never throws.
36
+ */
37
+ export function revisePlanDraft(
38
+ input: { plan: string },
39
+ session: WorkflowSession,
40
+ ): RevisePlanDraftResult {
41
+ if (!input.plan.trim()) {
42
+ return {
43
+ status: "rejected",
44
+ reason: "blank_plan",
45
+ problem: "no plan markdown to write (pass the full working draft)",
46
+ };
47
+ }
48
+ if (session.runId === null) {
49
+ return {
50
+ status: "rejected",
51
+ reason: "no_identity",
52
+ problem: "session has no run_id — cannot write the plan-draft artifact",
53
+ };
54
+ }
55
+ const bytes = Buffer.byteLength(input.plan, "utf8");
56
+ const written = session.writeArtifact(PLAN_DRAFT_ARTIFACT, input.plan);
57
+ switch (written.status) {
58
+ case "applied":
59
+ return { status: "revised", receipt: written.receipt, bytes };
60
+ case "unchanged":
61
+ return { status: "unchanged", receipt: written.receipt, bytes };
62
+ case "rejected":
63
+ return {
64
+ status: "rejected",
65
+ reason: "write_refused",
66
+ problem: `could not write the ${PLAN_DRAFT_ARTIFACT} artifact (see warnings)`,
67
+ };
68
+ case "unverified":
69
+ return {
70
+ status: "unverified",
71
+ problem: `could not write the ${PLAN_DRAFT_ARTIFACT} artifact (see warnings)`,
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Resume the working plan draft from the session. Fail-open `null` everywhere: `absent` is the
78
+ * silent no-draft arm; `invalid` was already warned by the seam. Raw markdown — blankness is
79
+ * the RESOLVER's concern (`resolvePlanSource` treats a blank draft as no draft). Never throws.
80
+ */
81
+ export function resumePlanDraft(session: WorkflowSession): string | null {
82
+ const read = session.readArtifact(PLAN_DRAFT_ARTIFACT);
83
+ return read.status === "found" ? read.content : null;
84
+ }