@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,8 +1,10 @@
1
- // Session-lifecycle gates (the interior safety primitive). One reusable dirty-repo guard on
2
- // session_before_switch / session_before_fork that returns { cancel: true } when an *active perk
3
- // workflow* has uncommitted changes — so a stage transition never silently orphans work. Plus the
4
- // guard-only `/implement` command that *enforces* the implement stage's `warm: false` legality (the
5
- // plan→implement jump requires fresh context; that is the cold door `perk implement`).
1
+ // Session-lifecycle gates (the interior safety primitive), the Pi registration half. One
2
+ // reusable dirty-repo guard on session_before_switch / session_before_fork that returns
3
+ // { cancel: true } when an *active perk workflow* has uncommitted changes — so a stage
4
+ // transition never silently orphans work. Plus the guard-only `/implement` command that
5
+ // *enforces* the implement stage's `warm: false` legality (the plan→implement jump requires
6
+ // fresh context; that is the cold door `perk implement`). The Pi-free policy these hooks apply
7
+ // (`implementHandoffPrompt`, `planningStageRefusal`) lives in `session/lifecycleGates.ts`.
6
8
  //
7
9
  // pi.on("session_before_fork"/"...switch") handlers are fired by extensionRunner.emit({type,...})
8
10
  // and their { cancel } result round-trips; the handler's
@@ -13,25 +15,17 @@ import type {
13
15
  ExtensionCommandContext,
14
16
  ExtensionContext,
15
17
  } from "@earendil-works/pi-coding-agent";
16
- import type { PlanRef } from "../substrate/cache.ts";
17
- import { registerPerkCommand } from "../substrate/command.ts";
18
- import { render } from "../substrate/prompts.ts";
19
- import { branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
20
- import { report } from "../surfaces/report.ts";
18
+ import { implementHandoffPrompt } from "../../session/lifecycleGates.ts";
19
+ import type { PlanRef } from "../../substrate/cache.ts";
20
+ import { registerPerkCommand } from "../../substrate/command.ts";
21
+ import { branchOf, rebuildWorkflowState } from "../../substrate/workflowState.ts";
22
+ import { report } from "../../surfaces/report.ts";
21
23
 
22
24
  const DIRTY_MESSAGE = "uncommitted changes — commit or stash before switching/forking this stage.";
23
25
  const HANDOFF_DIRTY_MESSAGE =
24
26
  "uncommitted changes — commit before a fresh-context /implement handoff (the plan is the " +
25
27
  "only artifact that crosses the boundary).";
26
28
 
27
- /**
28
- * Pure gate policy: cancel a transition only inside an active perk workflow whose tree is
29
- * dirty. Kept separate from the `pi.exec` effect so the matrix is unit-testable offline.
30
- */
31
- export function gateDecision(inputs: { active: boolean; dirty: boolean }): { cancel: boolean } {
32
- return { cancel: inputs.active && inputs.dirty };
33
- }
34
-
35
29
  /** True when this session is linked to a plan (a perk workflow is in progress). */
36
30
  function workflowActive(ctx: ExtensionContext): boolean {
37
31
  return rebuildWorkflowState(branchOf(ctx)).active_plan_ref != null;
@@ -46,53 +40,14 @@ async function guardTransition(
46
40
  pi: ExtensionAPI,
47
41
  ctx: ExtensionContext,
48
42
  ): Promise<{ cancel: true } | undefined> {
49
- const active = workflowActive(ctx);
50
- if (!active) return undefined; // perk does not interfere with non-perk transitions
43
+ if (!workflowActive(ctx)) return undefined; // perk does not interfere with non-perk transitions
51
44
  const res = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
52
45
  const dirty = res.code === 0 && res.stdout.trim().length > 0;
53
- if (!gateDecision({ active, dirty }).cancel) return undefined;
46
+ if (!dirty) return undefined;
54
47
  report(ctx, "lifecycle", "warning", DIRTY_MESSAGE); // fail-safe-headless: loud, still cancels
55
48
  return { cancel: true };
56
49
  }
57
50
 
58
- /**
59
- * The per-backend plan-read instruction — the prompt SSOT for "how do I read the saved
60
- * plan". Byte-identical to `perk/run/launch.py::_plan_read_instruction` (the Python twin); drift in
61
- * either plane fails the paired parity suites. `github` reads via `gh`; `linear` points at the
62
- * pi-mono-linear tools with an `open <url>` fallback; any other provider falls back to opening
63
- * the url.
64
- *
65
- * The wording now lives in the canonical templates `prompts/common/plan-read/*.md`, rendered
66
- * identically by both planes via the shared render seam (contracts.md §8.31); branching stays in
67
- * code — only the arm chosen and the vars passed differ. Golden-fixture parity (the three
68
- * `plan-read-*` cases) plus a thin per-arm selection test replace the dedicated substring parity.
69
- */
70
- export function planReadInstruction(provider: string, prId: string, url: string): string {
71
- if (provider === "github") return render("common/plan-read/github.md", { pr_id: prId, url });
72
- if (provider === "linear") return render("common/plan-read/linear.md", { pr_id: prId, url });
73
- return render("common/plan-read/other.md", { pr_id: prId, url });
74
- }
75
-
76
- /**
77
- * The plan-read priming seed for a fresh implement session. The in-session twin of
78
- * `perk/run/launch.py`'s `_implement_prompt`: carry the plan FORWARD (read it from its canonical
79
- * source), never summarize it — the plan is the only artifact that crosses the boundary.
80
- *
81
- * The wording lives in the canonical template `prompts/stages/implement.md`, rendered by the shared
82
- * seam (contracts.md §8.31); branching stays in code — only the `read_cmd` var differs. This warm
83
- * handoff is now byte-identical to the cold/worker primer, so it carries the same "Progress
84
- * tracking:" tail (the prior shorter near-copy omission is removed).
85
- */
86
- export function implementHandoffPrompt(ref: PlanRef): string {
87
- const readCmd = planReadInstruction(ref.provider, String(ref.pr_id), ref.url);
88
- return render("stages/implement.md", {
89
- provider: ref.provider,
90
- pr_id: String(ref.pr_id),
91
- url: ref.url,
92
- read_cmd: readCmd,
93
- });
94
- }
95
-
96
51
  /**
97
52
  * The warm `/implement`. It still NEVER performs the cross-worktree plan→implement
98
53
  * transition (that is structurally the Python cold door's job — no extension session API can change
@@ -22,9 +22,11 @@
22
22
  // best-effort and never throw (logged-not-thrown).
23
23
 
24
24
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
25
- import { registerPerkCommand } from "../substrate/command.ts";
26
- import { loadPerkConfig } from "../substrate/config.ts";
27
- import { branchOf, rebuildWorkflowState, WORKFLOW_STATE_TYPE } from "../substrate/workflowState.ts";
25
+ import { openBranchWorkflowSession } from "../../session/branchWorkflowSession.ts";
26
+ import { type ColdJson, objectField, runColdDoor, stringField } from "../../substrate/coldDoor.ts";
27
+ import { registerPerkCommand } from "../../substrate/command.ts";
28
+ import { loadPerkConfig } from "../../substrate/config.ts";
29
+ import { type BranchEntry, branchOf, WORKFLOW_STATE_TYPE } from "../../substrate/workflowState.ts";
28
30
  import {
29
31
  formatBudgetLine,
30
32
  MARK_OBJECTIVE,
@@ -32,7 +34,7 @@ import {
32
34
  type PerkStatusHandle,
33
35
  registerTranscriptRenderer,
34
36
  report,
35
- } from "../surfaces/surfaces.ts";
37
+ } from "../../surfaces/surfaces.ts";
36
38
 
37
39
  /** The dedicated budget/activation session entry type (kept off `perk:workflow-state`). */
38
40
  export const OBJECTIVE_BUDGET_TYPE = "perk:objective-budget";
@@ -41,10 +43,7 @@ export const OBJECTIVE_BUDGET_TYPE = "perk:objective-budget";
41
43
  export const DEFAULT_COMPACT_THRESHOLD = 0.8;
42
44
 
43
45
  /** A branch entry as seen by the budget scan: a custom marker OR an assistant message. */
44
- interface ScanEntry {
45
- type: string;
46
- customType?: string;
47
- data?: { objective_id?: string; activated_at?: string };
46
+ interface ScanEntry extends BranchEntry {
48
47
  message?: { role?: string; usage?: { input?: number; output?: number } };
49
48
  }
50
49
 
@@ -108,7 +107,7 @@ export function rebuildBudget(branch: readonly ScanEntry[], now: number): Budget
108
107
 
109
108
  /** True when context usage has crossed the compaction threshold (pure; tolerant of unknowns). */
110
109
  export function shouldCompact(
111
- usage: { percent: number | null; tokens: number | null } | undefined,
110
+ usage: { percent: number | null } | undefined,
112
111
  threshold: number,
113
112
  ): boolean {
114
113
  if (!usage || usage.percent === null) return false;
@@ -118,16 +117,14 @@ export function shouldCompact(
118
117
  // --- the controller -----------------------------------------------------------------------------
119
118
 
120
119
  function scanBranchOf(ctx: ExtensionContext): ScanEntry[] {
121
- return ctx.sessionManager.getBranch() as unknown as ScanEntry[];
120
+ // `branchOf` owns the typed accessor; the scan only ADDS the optional assistant-message
121
+ // shape, so no round-trip through `unknown` is needed (field presence is checked locally).
122
+ return branchOf(ctx) as ScanEntry[];
122
123
  }
123
124
 
124
- function activeObjective(ctx: ExtensionContext): string | null {
125
- try {
126
- const state = rebuildWorkflowState(branchOf(ctx));
127
- return state.active_objective ?? null;
128
- } catch {
129
- return null;
130
- }
125
+ /** The rebuilt `active_objective`, read through the session seam (fail-open null). */
126
+ function activeObjective(pi: ExtensionAPI, ctx: ExtensionContext): string | null {
127
+ return openBranchWorkflowSession(pi, ctx).activeObjective();
131
128
  }
132
129
 
133
130
  /**
@@ -135,10 +132,10 @@ function activeObjective(ctx: ExtensionContext): string | null {
135
132
  * widget is retired; the value carries id + tokens + elapsed). Headless-safe:
136
133
  * the handle no-ops without UI.
137
134
  */
138
- function renderStatus(ctx: ExtensionContext, status: PerkStatusHandle): void {
135
+ function renderStatus(pi: ExtensionAPI, ctx: ExtensionContext, status: PerkStatusHandle): void {
139
136
  if (!ctx.hasUI) return;
140
137
  try {
141
- const active = activeObjective(ctx);
138
+ const active = activeObjective(pi, ctx);
142
139
  if (active === null) {
143
140
  status.set(ctx, undefined);
144
141
  return;
@@ -164,7 +161,7 @@ function objectiveCommand(
164
161
  const arg = args.trim();
165
162
  try {
166
163
  if (arg === "") {
167
- const active = activeObjective(ctx);
164
+ const active = activeObjective(pi, ctx);
168
165
  const budget = rebuildBudget(scanBranchOf(ctx), Date.now());
169
166
  const message =
170
167
  active === null
@@ -176,18 +173,20 @@ function objectiveCommand(
176
173
 
177
174
  if (arg === "clear") {
178
175
  pi.appendEntry(WORKFLOW_STATE_TYPE, { active_objective: null });
179
- renderStatus(ctx, status);
176
+ renderStatus(pi, ctx, status);
180
177
  report(ctx, "objective", "info", "cleared the active objective.");
181
178
  return;
182
179
  }
183
180
 
184
- // Activate: set the LWW field + seed a fresh budget activation marker.
181
+ // Activate: set the LWW field + seed a fresh budget activation marker. This command path's
182
+ // `active_objective` write is a raw LWW append WITHOUT the session seam's strict read-back
183
+ // (the seam path in `saveObjective` verifies; this one stays best-effort — stated honestly).
185
184
  pi.appendEntry(WORKFLOW_STATE_TYPE, { active_objective: arg });
186
185
  pi.appendEntry(OBJECTIVE_BUDGET_TYPE, {
187
186
  objective_id: arg,
188
187
  activated_at: new Date().toISOString(),
189
188
  });
190
- renderStatus(ctx, status);
189
+ renderStatus(pi, ctx, status);
191
190
  report(ctx, "objective", "info", `activated objective ${arg} (budget tracking started).`);
192
191
  } catch (error) {
193
192
  reportError(ctx, `command failed: ${error}`);
@@ -195,37 +194,58 @@ function objectiveCommand(
195
194
  }
196
195
 
197
196
  /**
198
- * Register the objective substrate: `/objective` command, budget accounting (session_start /
199
- * session_tree / agent_settled), and threshold compaction (turn_end). All inert when no objective
200
- * is active; never throws.
197
+ * Fetch the objective's URL via `perk objective show <id> --json` (reading `objective.url`).
198
+ * Lenient: returns "" on any failure / missing url `runColdDoor` is a soft-result seam and
199
+ * never throws (the seed prompt's step-1 `perk objective show <id>` step surfaces the URL
200
+ * anyway). Only called for the linear backend (github needs no clause → no fetch). Exported for
201
+ * the warm drives that compose the same backend-aware read clause (the ready-time reconcile
202
+ * drive in `pi/v1/delivery/ready.ts`).
203
+ */
204
+ export async function fetchObjectiveUrl(
205
+ pi: ExtensionAPI,
206
+ ctx: ExtensionContext,
207
+ objectiveId: string,
208
+ ): Promise<string> {
209
+ const r = await runColdDoor<string>(pi, ctx, ["objective", "show", objectiveId, "--json"], {
210
+ label: "perk objective show",
211
+ decode: (payload: ColdJson) =>
212
+ stringField(objectField(payload, "objective") ?? {}, "url") ?? "",
213
+ });
214
+ return r.ok ? r.data : "";
215
+ }
216
+
217
+ /**
218
+ * Install the objective substrate bindings: `/objective` command, budget accounting
219
+ * (session_start / session_tree / agent_settled), and threshold compaction (turn_end). All
220
+ * inert when no objective is active; never throws.
201
221
  */
202
- export function registerObjective(pi: ExtensionAPI, status: PerkStatusHandle): void {
222
+ export function installObjectiveBindings(pi: ExtensionAPI, status: PerkStatusHandle): void {
203
223
  // Transcript marker for `perk:objective-budget` activations (audit §2.3): renderer body in
204
224
  // surfaces.ts, registration = wiring, feature-detect inside the seam (pre-0.80.4 hosts stay
205
225
  // inert). Also covers objectiveSave.ts's appends — registration is per entry TYPE.
206
226
  registerTranscriptRenderer(pi, OBJECTIVE_BUDGET_TYPE, objectiveBudgetEntryRenderer);
207
227
 
208
228
  pi.on("session_start", async (_event, ctx) => {
209
- renderStatus(ctx, status);
229
+ renderStatus(pi, ctx, status);
210
230
  });
211
231
 
212
232
  pi.on("session_tree", async (_event, ctx) => {
213
- renderStatus(ctx, status);
233
+ renderStatus(pi, ctx, status);
214
234
  });
215
235
 
216
236
  pi.on("agent_settled", async (_event, ctx) => {
217
237
  // Recompute the budget after each settled run (stateless rebuild from the branch).
218
- renderStatus(ctx, status);
238
+ renderStatus(pi, ctx, status);
219
239
  });
220
240
 
221
241
  pi.on("turn_end", async (_event, ctx) => {
222
242
  try {
223
- if (activeObjective(ctx) === null) return; // inert unless an objective is active
243
+ if (activeObjective(pi, ctx) === null) return; // inert unless an objective is active
224
244
  const threshold =
225
245
  loadPerkConfig(ctx.cwd).objectiveCompactThreshold ?? DEFAULT_COMPACT_THRESHOLD;
226
246
  const usage = ctx.getContextUsage();
227
247
  if (!shouldCompact(usage, threshold)) return;
228
- const active = activeObjective(ctx);
248
+ const active = activeObjective(pi, ctx);
229
249
  ctx.compact({
230
250
  customInstructions:
231
251
  `Preserve the active perk objective (${active}) and its budget context. ` +
@@ -234,7 +254,7 @@ export function registerObjective(pi: ExtensionAPI, status: PerkStatusHandle): v
234
254
  console.error(`perk: objective compaction failed — ${error}`);
235
255
  },
236
256
  onComplete: () => {
237
- renderStatus(ctx, status);
257
+ renderStatus(pi, ctx, status);
238
258
  },
239
259
  });
240
260
  } catch (error) {