@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,127 @@
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`.
8
+ //
9
+ // pi.on("session_before_fork"/"...switch") handlers are fired by extensionRunner.emit({type,...})
10
+ // and their { cancel } result round-trips; the handler's
11
+ // pi.exec("git",["status","--porcelain"],{cwd}) resolves the session cwd.
12
+
13
+ import type {
14
+ ExtensionAPI,
15
+ ExtensionCommandContext,
16
+ ExtensionContext,
17
+ } from "@earendil-works/pi-coding-agent";
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";
23
+
24
+ const DIRTY_MESSAGE = "uncommitted changes — commit or stash before switching/forking this stage.";
25
+ const HANDOFF_DIRTY_MESSAGE =
26
+ "uncommitted changes — commit before a fresh-context /implement handoff (the plan is the " +
27
+ "only artifact that crosses the boundary).";
28
+
29
+ /** True when this session is linked to a plan (a perk workflow is in progress). */
30
+ function workflowActive(ctx: ExtensionContext): boolean {
31
+ return rebuildWorkflowState(branchOf(ctx)).active_plan_ref != null;
32
+ }
33
+
34
+ /**
35
+ * The shared dirty-repo guard for switch/fork. Allows (returns undefined) outside a workflow or on
36
+ * a clean tree; cancels with a loud, fail-safe-headless message on a dirty tree in a workflow. If
37
+ * `git status` itself fails (e.g. not a repo) we allow — this is a hygiene guard, not a validator.
38
+ */
39
+ async function guardTransition(
40
+ pi: ExtensionAPI,
41
+ ctx: ExtensionContext,
42
+ ): Promise<{ cancel: true } | undefined> {
43
+ if (!workflowActive(ctx)) return undefined; // perk does not interfere with non-perk transitions
44
+ const res = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
45
+ const dirty = res.code === 0 && res.stdout.trim().length > 0;
46
+ if (!dirty) return undefined;
47
+ report(ctx, "lifecycle", "warning", DIRTY_MESSAGE); // fail-safe-headless: loud, still cancels
48
+ return { cancel: true };
49
+ }
50
+
51
+ /**
52
+ * The warm `/implement`. It still NEVER performs the cross-worktree plan→implement
53
+ * transition (that is structurally the Python cold door's job — no extension session API can change
54
+ * cwd). Outside an impl context it refuses and points at `perk implement`. INSIDE an active
55
+ * impl worktree (read-write + a linked plan-ref) it offers the in-process twin of the cold door: a
56
+ * lossless `ctx.newSession` fresh-context handoff seeded with the plan-read priming (the plan, and
57
+ * the worktree's materialized plan-ref, are the durable state — model-visible output stays capped).
58
+ * Dirty-tree hygiene is gated manually here (belt-and-suspenders: `newSession` is a session-replace,
59
+ * not a fork/switch, so it may bypass the `session_before_*` gate — we refuse on a dirty tree
60
+ * either way), fail-safe-headless.
61
+ */
62
+ function registerImplementGuard(pi: ExtensionAPI): void {
63
+ registerPerkCommand(pi, "implement", {
64
+ description:
65
+ "Refresh implement context (in-worktree handoff); cross-worktree is `perk implement`.",
66
+ handler: async (_args, ctx) => {
67
+ const state = rebuildWorkflowState(branchOf(ctx));
68
+ const ref = state.active_plan_ref;
69
+ const inImpl = state.mode === "read-write" && ref != null;
70
+ if (!inImpl) {
71
+ report(
72
+ ctx,
73
+ "implement",
74
+ "warning",
75
+ "/implement is cold-only here — run `perk implement` from a shell for fresh context.",
76
+ );
77
+ return;
78
+ }
79
+
80
+ // Dirty-tree gate (manual; see the doc comment). Refuse the handoff with uncommitted work.
81
+ const status = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
82
+ if (status.code === 0 && status.stdout.trim().length > 0) {
83
+ report(ctx, "implement", "warning", HANDOFF_DIRTY_MESSAGE);
84
+ return;
85
+ }
86
+
87
+ const commandCtx = ctx as ExtensionCommandContext;
88
+ if (typeof commandCtx.newSession !== "function") {
89
+ report(
90
+ ctx,
91
+ "implement",
92
+ "warning",
93
+ "a fresh-context /implement handoff needs an interactive session.",
94
+ );
95
+ return;
96
+ }
97
+
98
+ const prompt = implementHandoffPrompt(ref as PlanRef);
99
+ const parentSession = ctx.sessionManager.getSessionFile() ?? undefined;
100
+ const result = await commandCtx.newSession({
101
+ parentSession,
102
+ withSession: async (fresh) => {
103
+ await fresh.sendUserMessage(prompt);
104
+ },
105
+ });
106
+
107
+ // Verify + cap model-visible output (the full state lives in the worktree + the plan issue).
108
+ if (result.cancelled) {
109
+ report(ctx, "implement", "info", "/implement handoff cancelled — staying in this session.");
110
+ return;
111
+ }
112
+ report(
113
+ ctx,
114
+ "implement",
115
+ "info",
116
+ `fresh implement session started for plan #${(ref as PlanRef).pr_id} — the plan is carried forward, not summarized.`,
117
+ );
118
+ },
119
+ });
120
+ }
121
+
122
+ /** Register the dirty-repo lifecycle gate (switch + fork) and the guard-only `/implement`. */
123
+ export function registerLifecycleGates(pi: ExtensionAPI): void {
124
+ pi.on("session_before_fork", async (_event, ctx) => guardTransition(pi, ctx));
125
+ pi.on("session_before_switch", async (_event, ctx) => guardTransition(pi, ctx));
126
+ registerImplementGuard(pi);
127
+ }
@@ -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) {