@mgiles/perk 1.0.1 → 2.0.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 (157) hide show
  1. package/README.md +67 -57
  2. package/extension/adapters/planAdapterPlannotator.ts +27 -41
  3. package/extension/adapters/planAdapterTombell.ts +15 -28
  4. package/extension/adapters/todoAdapterJuicesharp.ts +10 -13
  5. package/extension/checkpoints/checkpoints.ts +21 -13
  6. package/extension/doors/address.ts +6 -5
  7. package/extension/doors/askUser.ts +12 -8
  8. package/extension/doors/ciExecutor.ts +23 -15
  9. package/extension/doors/hunkHandoff.ts +202 -0
  10. package/extension/doors/land.ts +33 -10
  11. package/extension/doors/learn.ts +240 -34
  12. package/extension/doors/learnFactory.ts +144 -0
  13. package/extension/doors/lifecycleGates.ts +2 -1
  14. package/extension/doors/plannotatorHandoff.ts +509 -0
  15. package/extension/doors/prReview.ts +22 -37
  16. package/extension/doors/prReviewBrowser.ts +341 -0
  17. package/extension/doors/prReviewTerminal.ts +267 -0
  18. package/extension/doors/ready.ts +2 -1
  19. package/extension/doors/selfcheck.ts +240 -6
  20. package/extension/doors/submit.ts +37 -19
  21. package/extension/doors/submitPrReview.ts +408 -0
  22. package/extension/factories/implementHere.ts +116 -0
  23. package/extension/factories/objective.ts +17 -6
  24. package/extension/factories/objectiveAuthor.ts +15 -32
  25. package/extension/factories/objectiveDraft.ts +1 -1
  26. package/extension/factories/objectivePlan.ts +16 -34
  27. package/extension/factories/objectiveSave.ts +7 -17
  28. package/extension/factories/planMode.ts +24 -38
  29. package/extension/factories/planReview.ts +291 -177
  30. package/extension/factories/planSave.ts +23 -8
  31. package/extension/index.ts +117 -20
  32. package/extension/substrate/bindingDelivery.ts +32 -10
  33. package/extension/substrate/bindings.ts +4 -2
  34. package/extension/substrate/cache.ts +36 -9
  35. package/extension/substrate/clipboard.ts +81 -0
  36. package/extension/substrate/command.ts +25 -0
  37. package/extension/substrate/config.ts +98 -76
  38. package/extension/substrate/consoleCapture.ts +90 -0
  39. package/extension/substrate/git.ts +77 -0
  40. package/extension/substrate/miniJinja.ts +480 -0
  41. package/extension/substrate/paths.ts +38 -0
  42. package/extension/substrate/prompts.ts +15 -24
  43. package/extension/substrate/providers.ts +62 -8
  44. package/extension/substrate/sessionData.ts +1 -1
  45. package/extension/substrate/sessionPointers.ts +184 -0
  46. package/extension/substrate/structuredOutput.ts +3 -1
  47. package/extension/substrate/terminalLaunch.ts +178 -0
  48. package/extension/substrate/toolGating.ts +332 -73
  49. package/extension/substrate/toolParams.ts +7 -0
  50. package/extension/substrate/workflowState.ts +54 -2
  51. package/extension/surfaces/footerProvider.ts +8 -4
  52. package/extension/surfaces/surfaces.ts +338 -13
  53. package/extension/vendor/btw/btw.ts +10 -0
  54. package/extension/worker/readOnlySession.ts +19 -6
  55. package/extension/worker/worker.ts +187 -31
  56. package/extension/workerMain.ts +14 -13
  57. package/package.json +3 -7
  58. package/prompts/README.md +56 -5
  59. package/prompts/_fixtures/cases.yaml +52 -131
  60. package/prompts/_fixtures/golden/cond_elif-a.txt +3 -0
  61. package/prompts/_fixtures/golden/cond_elif-b.txt +3 -0
  62. package/prompts/_fixtures/golden/cond_elif-c.txt +3 -0
  63. package/prompts/_fixtures/golden/cond_if-false.txt +3 -0
  64. package/prompts/_fixtures/golden/cond_if-true.txt +3 -0
  65. package/prompts/_fixtures/golden/cond_ops-1.txt +3 -0
  66. package/prompts/_fixtures/golden/cond_ops-2.txt +3 -0
  67. package/prompts/_fixtures/golden/no_trailing_nl.txt +2 -0
  68. package/prompts/_fixtures/golden/trailing_nl.txt +2 -0
  69. package/prompts/_fixtures/golden/trim_block.txt +3 -0
  70. package/prompts/_fixtures/golden/trim_inline.txt +1 -0
  71. package/prompts/_fixtures/live.yaml +396 -0
  72. package/prompts/_fixtures/templates/cond_elif.md +9 -0
  73. package/prompts/_fixtures/templates/cond_if.md +7 -0
  74. package/prompts/_fixtures/templates/cond_ops.md +3 -0
  75. package/prompts/_fixtures/templates/no_trailing_nl.md +2 -0
  76. package/prompts/_fixtures/templates/trailing_nl.md +2 -0
  77. package/prompts/_fixtures/templates/trim_block.md +5 -0
  78. package/prompts/_fixtures/templates/trim_inline.md +1 -0
  79. package/prompts/contexts/adapters/juicesharp-todo.md +7 -0
  80. package/prompts/contexts/adapters/plannotator-objective.md +7 -0
  81. package/prompts/contexts/adapters/plannotator-plan.md +6 -0
  82. package/prompts/contexts/adapters/tombell-plan.md +17 -0
  83. package/prompts/contexts/objective-authoring.md +20 -0
  84. package/prompts/contexts/plan-authoring.md +24 -0
  85. package/prompts/contexts/read-only.md +10 -0
  86. package/prompts/stages/conflict-resolution.md +4 -0
  87. package/prompts/stages/learn-code.md +8 -0
  88. package/prompts/stages/learn-docs.md +7 -6
  89. package/prompts/stages/learn-orchestrate.md +6 -0
  90. package/prompts/stages/learn.md +1 -1
  91. package/prompts/stages/objective-author/adopt.md +12 -0
  92. package/prompts/stages/objective-author/file.md +9 -0
  93. package/prompts/stages/objective-author/seed.md +9 -0
  94. package/prompts/stages/objective-plan/guidance.md +1 -1
  95. package/prompts/stages/objective-plan/seed.md +3 -2
  96. package/prompts/stages/objective-reconcile.md +7 -0
  97. package/prompts/stages/objective-replan.md +14 -0
  98. package/prompts/stages/objective-save.md +9 -0
  99. package/prompts/stages/plan-from/adopt.md +10 -0
  100. package/prompts/stages/plan-from/file.md +9 -0
  101. package/prompts/stages/pr-review-browser/active.md +11 -0
  102. package/prompts/stages/pr-review-browser/foreign.md +11 -0
  103. package/prompts/stages/pr-review-terminal/active.md +12 -0
  104. package/prompts/stages/pr-review-terminal/foreign.md +13 -0
  105. package/prompts/stages/pr-review-terminal/local.md +4 -0
  106. package/prompts/stages/pr-review.md +6 -0
  107. package/prompts/stages/replan.md +13 -0
  108. package/prompts/stages/skills/create-from.md +15 -0
  109. package/prompts/stages/skills/create.md +9 -0
  110. package/prompts/stages/skills/refine.md +9 -0
  111. package/shared/README.md +23 -13
  112. package/shared/bindings.yaml +22 -2
  113. package/shared/contracts-history.md +191 -0
  114. package/shared/contracts.md +2258 -1638
  115. package/shared/providers.yaml +8 -1
  116. package/shared/registry.yaml +10 -11
  117. package/shared/schemas/contracts/bindings.schema.json +38 -0
  118. package/shared/schemas/contracts/providers.schema.json +89 -0
  119. package/shared/schemas/contracts/registry.schema.json +98 -0
  120. package/shared/schemas/inputs/handoff-arg.schema.json +6 -0
  121. package/shared/schemas/inputs/resolve-threads-batch.schema.json +37 -0
  122. package/shared/schemas/inputs/review-post-batch.schema.json +84 -0
  123. package/shared/schemas/inputs/review-submit-batch.schema.json +66 -0
  124. package/shared/schemas/inputs/structured-roadmap-node.schema.json +102 -0
  125. package/shared/schemas/outputs/doctor-report.schema.json +236 -0
  126. package/shared/schemas/outputs/init-report.schema.json +419 -0
  127. package/shared/schemas/outputs/learn-capture.schema.json +90 -0
  128. package/shared/schemas/outputs/learn-skip.schema.json +59 -0
  129. package/shared/schemas/outputs/plan-save.schema.json +209 -0
  130. package/shared/schemas/outputs/pr-feedback.schema.json +334 -0
  131. package/shared/schemas/outputs/pr-land.schema.json +187 -0
  132. package/shared/schemas/outputs/pr-ready.schema.json +75 -0
  133. package/shared/schemas/outputs/pr-review-checkout.schema.json +69 -0
  134. package/shared/schemas/outputs/pr-review-cleanup.schema.json +54 -0
  135. package/shared/schemas/outputs/pr-review-context.schema.json +86 -0
  136. package/shared/schemas/outputs/pr-review-submit.schema.json +64 -0
  137. package/shared/schemas/outputs/pr-submit.schema.json +147 -0
  138. package/extension/doors/learnDocs.ts +0 -99
  139. package/prompts/_fixtures/golden/address-action-model.txt +0 -10
  140. package/prompts/_fixtures/golden/address-action.txt +0 -10
  141. package/prompts/_fixtures/golden/address-preview-model.txt +0 -6
  142. package/prompts/_fixtures/golden/address-preview.txt +0 -6
  143. package/prompts/_fixtures/golden/implement-github.txt +0 -8
  144. package/prompts/_fixtures/golden/learn-docs.txt +0 -8
  145. package/prompts/_fixtures/golden/learn-github.txt +0 -11
  146. package/prompts/_fixtures/golden/learn-linear.txt +0 -11
  147. package/prompts/_fixtures/golden/learn-no-ref.txt +0 -8
  148. package/prompts/_fixtures/golden/learn-other.txt +0 -8
  149. package/prompts/_fixtures/golden/objective-plan-guidance-linear.txt +0 -8
  150. package/prompts/_fixtures/golden/objective-plan-guidance.txt +0 -8
  151. package/prompts/_fixtures/golden/objective-plan-seed-linear.txt +0 -20
  152. package/prompts/_fixtures/golden/objective-plan-seed.txt +0 -15
  153. package/prompts/_fixtures/golden/objective-read-linear-nourl.txt +0 -1
  154. package/prompts/_fixtures/golden/objective-read-linear.txt +0 -1
  155. package/prompts/_fixtures/golden/plan-read-github.txt +0 -1
  156. package/prompts/_fixtures/golden/plan-read-linear.txt +0 -1
  157. package/prompts/_fixtures/golden/plan-read-other.txt +0 -1
@@ -13,12 +13,26 @@
13
13
  // the converged content actually *reached* the prompt. That closes perk's two-plane blind spot —
14
14
  // doctor checks disk, selfcheck checks the prompt.
15
15
  //
16
+ // The same report additionally carries a per-surface payload CENSUS — derived counts/chars for
17
+ // `appendSystemPrompt`, `contextFiles`, the skills catalog section, the active tool definitions,
18
+ // and perk-injected `custom_message` branch context. Report-only: the census never affects the
19
+ // ok/level verdict (contracts.md §8.7).
20
+ //
16
21
  // Sensitivity: `getSystemPromptOptions()` exposes the full system-prompt construction inputs. This
17
- // module logs ONLY derived booleans/counts (never the raw prompt text). See docs/learned/pi/.
22
+ // module logs ONLY derived output identifiers (paths, skill/tool names, source strings,
23
+ // customTypes) and counts/chars; never prompt/content text. See docs/learned/pi/.
18
24
 
19
25
  import { existsSync, readFileSync } from "node:fs";
20
26
  import { join } from "node:path";
21
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
27
+ import {
28
+ type ExtensionAPI,
29
+ formatSkillsForPrompt,
30
+ type Skill,
31
+ type ToolInfo,
32
+ } from "@earendil-works/pi-coding-agent";
33
+ import { BINDING_HEADER } from "../substrate/bindingDelivery.ts";
34
+ import { registerPerkCommand } from "../substrate/command.ts";
35
+ import { branchOf } from "../substrate/workflowState.ts";
22
36
  import { report as reportTo } from "../surfaces/report.ts";
23
37
 
24
38
  /** Project-scoped ambient routing index, relative to the repo root. */
@@ -27,10 +41,15 @@ export const AMBIENT_INDEX_REL_PATH = join(".pi", "APPEND_SYSTEM.md");
27
41
  /** The managed `AGENTS.md` block marker (cross-plane: written by `perk init`, read here). */
28
42
  export const MANAGED_AGENTS_MARKER = "<!-- BEGIN perk managed -->";
29
43
 
30
- /** The narrow slice of `BuildSystemPromptOptions` the verifier probes. */
44
+ /** The slice of `BuildSystemPromptOptions` the verifier + census probe (all optional). */
31
45
  export interface SystemPromptProbeInput {
46
+ customPrompt?: string;
32
47
  appendSystemPrompt?: string;
33
48
  contextFiles?: { path: string; content: string }[];
49
+ skills?: Skill[];
50
+ selectedTools?: string[];
51
+ toolSnippets?: Record<string, string>;
52
+ promptGuidelines?: string[];
34
53
  }
35
54
 
36
55
  /** Read the on-disk ambient routing index (`.pi/APPEND_SYSTEM.md`), or `null` if absent. */
@@ -129,6 +148,214 @@ export function buildSelfcheckReport(input: {
129
148
  return { version, sharedOk, ambient, agents, ok, summary, level: ok ? "info" : "warning" };
130
149
  }
131
150
 
151
+ // ---------------------------------------------------------------------------
152
+ // The per-surface payload census (contracts.md §8.7). Pure builders over the same
153
+ // `getSystemPromptOptions()` slice plus the tool registry and the session branch. All counts are
154
+ // `string.length` char counts (rendered with a `c` suffix). Identifiers (paths, skill/tool names,
155
+ // source strings, customTypes) and counts/chars only — never prompt/content text.
156
+ // ---------------------------------------------------------------------------
157
+
158
+ /** Derived per-surface counts for the system-prompt construction inputs. */
159
+ export interface PromptCensus {
160
+ /** `customPrompt` length; `null` = pi's built-in default is in use (not measurable here). */
161
+ basePromptChars: number | null;
162
+ appendChars: number;
163
+ contextFiles: { count: number; totalChars: number; files: { path: string; chars: number }[] };
164
+ /** `hidden` = skills with `disableModelInvocation`; `promptSectionChars` is the exact
165
+ * system-prompt contribution (pi's own exported `formatSkillsForPrompt`). */
166
+ skills: { visible: number; hidden: number; promptSectionChars: number };
167
+ toolGuidelineChars: number;
168
+ toolSnippetChars: number;
169
+ }
170
+
171
+ /** Census the system-prompt surfaces from the probed options (undefined → zeros). */
172
+ export function promptCensus(options: SystemPromptProbeInput | undefined): PromptCensus {
173
+ const files = (options?.contextFiles ?? []).map((f) => ({
174
+ path: f.path,
175
+ chars: f.content.length,
176
+ }));
177
+ const skills = options?.skills ?? [];
178
+ const hidden = skills.filter((s) => s.disableModelInvocation).length;
179
+ let guidelineChars = 0;
180
+ for (const g of options?.promptGuidelines ?? []) guidelineChars += g.length;
181
+ let snippetChars = 0;
182
+ for (const s of Object.values(options?.toolSnippets ?? {})) snippetChars += s.length;
183
+ return {
184
+ basePromptChars: options?.customPrompt === undefined ? null : options.customPrompt.length,
185
+ appendChars: (options?.appendSystemPrompt ?? "").length,
186
+ contextFiles: {
187
+ count: files.length,
188
+ totalChars: files.reduce((sum, f) => sum + f.chars, 0),
189
+ files,
190
+ },
191
+ skills: {
192
+ visible: skills.length - hidden,
193
+ hidden,
194
+ // formatSkillsForPrompt filters disableModelInvocation skills itself, so passing the full
195
+ // list measures exactly the visible skills' prompt-section contribution.
196
+ promptSectionChars: formatSkillsForPrompt(skills).length,
197
+ },
198
+ toolGuidelineChars: guidelineChars,
199
+ toolSnippetChars: snippetChars,
200
+ };
201
+ }
202
+
203
+ /** Derived counts for the tool-definitions payload (active tools = the per-request payload). */
204
+ export interface ToolsCensus {
205
+ active: number;
206
+ all: number;
207
+ /** Sum of `JSON.stringify({name, description, parameters})` over ACTIVE tools. */
208
+ schemaChars: number;
209
+ /** Active tools grouped by `sourceInfo.source`, sorted by source string (stable output). */
210
+ bySource: { source: string; active: number; schemaChars: number }[];
211
+ }
212
+
213
+ /** Census the tool registry: active vs registered counts + per-source schema chars. */
214
+ export function toolsCensus(allTools: ToolInfo[], activeNames: string[]): ToolsCensus {
215
+ const activeSet = new Set(activeNames);
216
+ const bySource = new Map<string, { active: number; schemaChars: number }>();
217
+ let schemaChars = 0;
218
+ for (const tool of allTools) {
219
+ if (!activeSet.has(tool.name)) continue;
220
+ const chars = JSON.stringify({
221
+ name: tool.name,
222
+ description: tool.description,
223
+ parameters: tool.parameters,
224
+ }).length;
225
+ schemaChars += chars;
226
+ const row = bySource.get(tool.sourceInfo.source) ?? { active: 0, schemaChars: 0 };
227
+ row.active += 1;
228
+ row.schemaChars += chars;
229
+ bySource.set(tool.sourceInfo.source, row);
230
+ }
231
+ return {
232
+ active: activeNames.length,
233
+ all: allTools.length,
234
+ schemaChars,
235
+ bySource: [...bySource.entries()]
236
+ .map(([source, row]) => ({ source, ...row }))
237
+ .sort((a, b) => (a.source < b.source ? -1 : a.source > b.source ? 1 : 0)),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * The structural slice of a session entry the branch census reads. Injected contexts persist as
243
+ * `type: "custom_message"` entries carrying `content` — distinct from `type: "custom"` state
244
+ * entries (workflow state, checkpoints), which the census must NOT count as context.
245
+ */
246
+ export interface CensusBranchEntry {
247
+ type: string;
248
+ customType?: string;
249
+ content?: unknown;
250
+ }
251
+
252
+ /** Derived counts for perk-injected branch context (perk's customTypes share the `perk:` prefix). */
253
+ export interface BranchContextCensus {
254
+ entries: number;
255
+ /** `custom_message` entries whose customType starts with `perk:`, sorted by customType. */
256
+ perkContexts: { customType: string; copies: number; totalChars: number }[];
257
+ /** `custom_message` entries without the `perk:` prefix (borrowed packages). */
258
+ otherCustomMessages: { copies: number; totalChars: number };
259
+ /** Entries (any type) whose JSON form carries `BINDING_HEADER` — the cold↔warm dedup marker. */
260
+ bindingHeaderCopies: number;
261
+ }
262
+
263
+ /** Char count of a `custom_message` entry's content — string, `text`-part array, or JSON size. */
264
+ function contentChars(content: unknown): number {
265
+ if (content === undefined) return 0;
266
+ if (typeof content === "string") return content.length;
267
+ if (Array.isArray(content)) {
268
+ let sum = 0;
269
+ for (const part of content) {
270
+ if (typeof part !== "object" || part === null || Array.isArray(part)) continue;
271
+ const text = (part as { text?: unknown }).text;
272
+ if (typeof text === "string") sum += text.length;
273
+ }
274
+ return sum;
275
+ }
276
+ return JSON.stringify(content).length;
277
+ }
278
+
279
+ /** Census the session branch's injected context (counts/chars only — never message content). */
280
+ export function branchContextCensus(entries: readonly CensusBranchEntry[]): BranchContextCensus {
281
+ const perk = new Map<string, { copies: number; totalChars: number }>();
282
+ const other = { copies: 0, totalChars: 0 };
283
+ let bindingHeaderCopies = 0;
284
+ for (const entry of entries) {
285
+ if (JSON.stringify(entry).includes(BINDING_HEADER)) bindingHeaderCopies += 1;
286
+ if (entry.type !== "custom_message") continue;
287
+ const chars = contentChars(entry.content);
288
+ const customType = entry.customType ?? "";
289
+ if (customType.startsWith("perk:")) {
290
+ const row = perk.get(customType) ?? { copies: 0, totalChars: 0 };
291
+ row.copies += 1;
292
+ row.totalChars += chars;
293
+ perk.set(customType, row);
294
+ } else {
295
+ other.copies += 1;
296
+ other.totalChars += chars;
297
+ }
298
+ }
299
+ return {
300
+ entries: entries.length,
301
+ perkContexts: [...perk.entries()]
302
+ .map(([customType, row]) => ({ customType, ...row }))
303
+ .sort((a, b) => (a.customType < b.customType ? -1 : a.customType > b.customType ? 1 : 0)),
304
+ otherCustomMessages: other,
305
+ bindingHeaderCopies,
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Render the census as a fixed multi-line block. The line grammar (the `census:` /
311
+ * `append-system-prompt:` / `context-files:` / `skills:` / `tools:` / `per source:` / `branch:` /
312
+ * `perk contexts:` keys) is stable — the closing audit diffs against these exact keys.
313
+ */
314
+ export function renderCensus(
315
+ prompt: PromptCensus,
316
+ tools: ToolsCensus,
317
+ branch: BranchContextCensus,
318
+ ): string {
319
+ const lines: string[] = ["census:"];
320
+ lines.push(
321
+ prompt.basePromptChars === null
322
+ ? " base-prompt: pi-default (not measured)"
323
+ : ` base-prompt: custom ${prompt.basePromptChars}c`,
324
+ );
325
+ lines.push(` append-system-prompt: ${prompt.appendChars}c`);
326
+ const fileList = prompt.contextFiles.files.map((f) => `${f.path}=${f.chars}c`).join(", ");
327
+ lines.push(
328
+ ` context-files: ${prompt.contextFiles.count} file(s), ${prompt.contextFiles.totalChars}c` +
329
+ (fileList.length > 0 ? ` — ${fileList}` : ""),
330
+ );
331
+ lines.push(
332
+ ` skills: ${prompt.skills.visible} visible + ${prompt.skills.hidden} hidden; ` +
333
+ `prompt-section=${prompt.skills.promptSectionChars}c`,
334
+ );
335
+ lines.push(
336
+ ` tools: ${tools.active} active / ${tools.all} registered; schemas=${tools.schemaChars}c; ` +
337
+ `guidelines=${prompt.toolGuidelineChars}c; snippets=${prompt.toolSnippetChars}c`,
338
+ );
339
+ if (tools.bySource.length > 0) {
340
+ const rows = tools.bySource
341
+ .map((r) => `${r.source}=${r.active} (${r.schemaChars}c)`)
342
+ .join("; ");
343
+ lines.push(` per source: ${rows}`);
344
+ }
345
+ lines.push(
346
+ ` branch: ${branch.entries} entries; binding-header-copies=${branch.bindingHeaderCopies}`,
347
+ );
348
+ const perkRows = branch.perkContexts.map(
349
+ (r) => `${r.customType} ×${r.copies} (${r.totalChars}c)`,
350
+ );
351
+ const perkSegment = perkRows.length > 0 ? perkRows.join("; ") : "none";
352
+ lines.push(
353
+ ` perk contexts: ${perkSegment}; other custom_message ` +
354
+ `×${branch.otherCustomMessages.copies} (${branch.otherCustomMessages.totalChars}c)`,
355
+ );
356
+ return lines.join("\n");
357
+ }
358
+
132
359
  /**
133
360
  * Register `/perk-selfcheck`: the session-wiring verifier. Runs on a command context (the only
134
361
  * context exposing `getSystemPromptOptions()`), so it sees the live splice — not just disk.
@@ -137,7 +364,7 @@ export function registerSelfcheck(
137
364
  pi: ExtensionAPI,
138
365
  opts: { version: string; sharedOk: boolean },
139
366
  ): void {
140
- pi.registerCommand("perk-selfcheck", {
367
+ registerPerkCommand(pi, "perk-selfcheck", {
141
368
  description:
142
369
  "Verify perk's session wiring: the ambient index + managed AGENTS block reached the prompt.",
143
370
  handler: async (_args, ctx) => {
@@ -148,8 +375,15 @@ export function registerSelfcheck(
148
375
  onDiskIndex: readAmbientIndex(ctx.cwd),
149
376
  options,
150
377
  });
151
- // Headless-safe: report() surfaces the derived booleans/counts (never raw prompt content).
152
- reportTo(ctx, "selfcheck", report.level, report.summary);
378
+ // The census is report-only: it rides the same report but never affects ok/level. A command
379
+ // doesn't fire `before_agent_start`, so the branch reads as of the last completed turn.
380
+ const census = renderCensus(
381
+ promptCensus(options),
382
+ toolsCensus(pi.getAllTools(), pi.getActiveTools()),
383
+ branchContextCensus(branchOf(ctx)),
384
+ );
385
+ // Headless-safe: report() surfaces the derived counts/identifiers (never raw prompt content).
386
+ reportTo(ctx, "selfcheck", report.level, `${report.summary}\n${census}`);
153
387
  },
154
388
  });
155
389
  }
@@ -21,8 +21,11 @@ import {
21
21
  runColdDoor,
22
22
  stringField,
23
23
  } from "../substrate/coldDoor.ts";
24
+ import { registerPerkCommand } from "../substrate/command.ts";
24
25
  import { loadPerkConfig } from "../substrate/config.ts";
26
+ import { render } from "../substrate/prompts.ts";
25
27
  import { failFor, type OkDetails, ok, type Result } from "../substrate/result.ts";
28
+ import { captureSessionPointer } from "../substrate/sessionPointers.ts";
26
29
  import { appendWorkflowState, branchOf, rebuildWorkflowState } from "../substrate/workflowState.ts";
27
30
  import { report } from "../surfaces/report.ts";
28
31
 
@@ -104,12 +107,38 @@ function isUnmergeable(details: SubmitDetails): details is OkDetails<SubmitOk> {
104
107
  export async function submitPr(pi: ExtensionAPI, ctx: ExtensionContext): Promise<SubmitResult> {
105
108
  const fail = failFor(ctx, "submit");
106
109
 
107
- const r = await runColdDoor<SubmitOk>(pi, ctx, ["pr", "submit", "--json"], {
110
+ // Stamp this implement run id into the plan-header `impl_run_ids` linkage (contracts.md
111
+ // §8.35) so a later/other session can resolve the implement session pointers cross-run.
112
+ // Mirrors planSave's `--run-id` thread; covers the interactive /submit AND the headless worker
113
+ // drive (both flow through this warm door). Absent run_id ⇒ omit (bare-stamp untouched).
114
+ const runId = rebuildWorkflowState(branchOf(ctx)).run_id ?? "";
115
+ const args = ["pr", "submit", "--json"];
116
+ if (runId) args.push("--run-id", runId);
117
+ const r = await runColdDoor<SubmitOk>(pi, ctx, args, {
108
118
  label: "perk pr submit",
109
119
  decode: decodeSubmit,
110
120
  });
111
121
  if (!r.ok) return fail(r.message, r.errorType);
112
122
 
123
+ // Capture `implementation/main` at the moment the run id enters `impl_run_ids` (contracts.md
124
+ // §8.35): any run id stamped into the linkage gets its pointer captured in the same gesture.
125
+ // This covers address/warm sessions that submit — which the stage-gated `session_start` capture
126
+ // never sees — so a submitted run resolves `found` instead of `missing`. For the implement
127
+ // session's own /submit it is an idempotent same-session refresh; `preserveForeign` guarantees
128
+ // it can never clobber a different session's pointer. Best-effort + non-fatal like every
129
+ // capture site (a successful submit must stand).
130
+ if (runId) {
131
+ captureSessionPointer({
132
+ cwd: ctx.cwd,
133
+ runId,
134
+ klass: "implementation",
135
+ site: "main",
136
+ // Optional-chained: best-effort, and some side-session fakes have no getSessionFile.
137
+ sessionFile: ctx.sessionManager.getSessionFile?.(),
138
+ preserveForeign: true,
139
+ });
140
+ }
141
+
113
142
  const verb = r.data.pr.existed ? "Found existing" : "Opened draft";
114
143
  const conflicted = r.data.mergeable === false && (r.data.conflicts?.length ?? 0) > 0;
115
144
  // Reset the counter on every clean (or undetermined) submit — idempotent; keeps a later
@@ -147,23 +176,12 @@ export function conflictResolutionGuidance(
147
176
  cap: number,
148
177
  model?: string,
149
178
  ): string {
150
- const modelClause = model
151
- ? `, and pass \`model: "${model}"\` on that call (the configured [subagents] conflict-resolver model)`
152
- : " (no model override — the agent's default model is used)";
153
- return [
154
- `perk /submit your PR has merge conflicts against \`${base}\`; resolve them before the work ` +
155
- "is submitted for review. This is attempt " +
156
- `${attempt} of ${cap}.`,
157
- `1. Spawn the \`perk.conflict-resolver\` agent via the \`subagent\` tool with \`context: "fresh"\`${modelClause}. ` +
158
- "A fresh context keeps this implementation session's history from biasing the resolution.",
159
- `2. Tell it: rebase the PR branch onto \`${base}\` and **carefully** resolve all merge ` +
160
- "conflicts so the resulting diff is **clean** (no stray markers, no unrelated churn) and " +
161
- "**correct** (preserve the change's intent on both sides). The child reads its own plan + PR " +
162
- "diff context first (it runs `perk pr review-context`) so it resolves with the change's " +
163
- "intent in hand, verifies, and force-pushes — the raw diff never enters this session.",
164
- "3. After the child reports success, call `/submit` again to re-verify mergeability. Do NOT " +
165
- "edit or resolve conflicts yourself here — the child owns the rebase/resolve/push.",
166
- ].join("\n");
179
+ return render("stages/conflict-resolution.md", {
180
+ base,
181
+ attempt: String(attempt),
182
+ cap: String(cap),
183
+ model: model ?? "",
184
+ });
167
185
  }
168
186
 
169
187
  /**
@@ -239,7 +257,7 @@ export function registerSubmit(pi: ExtensionAPI): void {
239
257
  },
240
258
  });
241
259
 
242
- pi.registerCommand("submit", {
260
+ registerPerkCommand(pi, "submit", {
243
261
  description: "Push the branch and open a draft PR for the active plan (implement → submit).",
244
262
  handler: async (_args, ctx) => {
245
263
  const result = await submitPr(pi, ctx);