@mgiles/perk 2.3.0 → 3.1.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 (145) hide show
  1. package/README.md +1 -1
  2. package/extension/adapters/planAdapterPlannotator.ts +135 -91
  3. package/extension/adapters/planAdapterTombell.ts +5 -3
  4. package/extension/doors/address.ts +235 -37
  5. package/extension/doors/annotationPush.ts +924 -0
  6. package/extension/doors/auditWaveTools.ts +352 -0
  7. package/extension/doors/ciExecutor.ts +220 -55
  8. package/extension/doors/commitCompact.ts +98 -10
  9. package/extension/doors/draftReviewWaveTools.ts +412 -0
  10. package/extension/doors/dreamWaveTools.ts +475 -0
  11. package/extension/doors/harvestWaveTools.ts +279 -0
  12. package/extension/doors/land.ts +1 -0
  13. package/extension/doors/learn.ts +24 -6
  14. package/extension/doors/learnFactory.ts +33 -7
  15. package/extension/doors/lifecycleGates.ts +1 -1
  16. package/extension/doors/objectiveReviewBrowser.ts +537 -0
  17. package/extension/doors/objectiveStack.ts +1143 -0
  18. package/extension/doors/planReviewBrowser.ts +475 -0
  19. package/extension/doors/plannotatorHandoff.ts +120 -49
  20. package/extension/doors/prReview.ts +180 -63
  21. package/extension/doors/prReviewBrowser.ts +31 -23
  22. package/extension/doors/prReviewDynamic.ts +57 -30
  23. package/extension/doors/prReviewTerminal.ts +6 -11
  24. package/extension/doors/reviewWaveTools.ts +397 -0
  25. package/extension/doors/selfcheck.ts +1 -1
  26. package/extension/doors/submit.ts +82 -7
  27. package/extension/factories/gistAuthor.ts +4 -4
  28. package/extension/factories/gistDraft.ts +1 -1
  29. package/extension/factories/gistSave.ts +2 -2
  30. package/extension/factories/objective.ts +7 -7
  31. package/extension/factories/objectiveAuthor.ts +5 -4
  32. package/extension/factories/objectiveDraft.ts +153 -30
  33. package/extension/factories/objectiveDreamReport.ts +347 -0
  34. package/extension/factories/objectivePlan.ts +163 -9
  35. package/extension/factories/objectiveSave.ts +90 -2
  36. package/extension/factories/planMode.ts +5 -3
  37. package/extension/factories/planReview.ts +304 -46
  38. package/extension/factories/planTitle.ts +27 -9
  39. package/extension/hunkFeedback/inbox.ts +389 -0
  40. package/extension/hunkFeedback/perkFeedback.ts +364 -0
  41. package/extension/hunkFeedback/receiver.ts +296 -0
  42. package/extension/hunkFeedback/store.ts +533 -0
  43. package/extension/index.ts +169 -57
  44. package/extension/substrate/agentScratch.ts +171 -0
  45. package/extension/substrate/bindingDelivery.ts +9 -11
  46. package/extension/substrate/cache.ts +166 -23
  47. package/extension/substrate/coldDoor.ts +2 -3
  48. package/extension/substrate/command.ts +9 -6
  49. package/extension/substrate/config.ts +41 -12
  50. package/extension/substrate/git.ts +95 -2
  51. package/extension/substrate/providers.ts +4 -33
  52. package/extension/substrate/resources.ts +11 -0
  53. package/extension/substrate/result.ts +16 -7
  54. package/extension/substrate/sessionData.ts +8 -6
  55. package/extension/substrate/sessionPointers.ts +4 -5
  56. package/extension/substrate/structuredOutput.ts +78 -19
  57. package/extension/substrate/toolGating.ts +179 -23
  58. package/extension/substrate/workflowState.ts +50 -2
  59. package/extension/surfaces/footerProvider.ts +6 -6
  60. package/extension/surfaces/report.ts +38 -12
  61. package/extension/surfaces/surfaces.ts +213 -275
  62. package/extension/vendor/btw/btw.ts +94 -17
  63. package/extension/waves/adversarialReviewWave.ts +181 -0
  64. package/extension/waves/auditWave.ts +312 -0
  65. package/extension/waves/draftReviewWave.ts +205 -0
  66. package/extension/waves/dreamReducerWave.ts +700 -0
  67. package/extension/waves/dreamReport.ts +1494 -0
  68. package/extension/waves/dreamWave.ts +927 -0
  69. package/extension/waves/harvestWave.ts +399 -0
  70. package/extension/waves/memoryAdapter.ts +14 -1
  71. package/extension/waves/objectiveExplorerWave.ts +120 -0
  72. package/extension/waves/ponytail.ts +104 -0
  73. package/extension/waves/prReviewDynamicWave.ts +383 -72
  74. package/extension/waves/prReviewWave.ts +175 -25
  75. package/extension/waves/reportWave.ts +503 -107
  76. package/extension/waves/reviewClassifierWave.ts +109 -0
  77. package/extension/waves/rpcAdapter.ts +65 -2
  78. package/extension/worker/readOnlySession.ts +4 -5
  79. package/extension/worker/worker.ts +71 -93
  80. package/extension/workerMain.ts +4 -6
  81. package/package.json +18 -5
  82. package/prompts/README.md +19 -0
  83. package/prompts/_fixtures/live.yaml +115 -65
  84. package/prompts/commit-and-compact-continuation.md +13 -0
  85. package/prompts/common/resume-advisory.md +1 -0
  86. package/prompts/contexts/adapters/plannotator-gist.md +13 -0
  87. package/prompts/contexts/adapters/plannotator-objective.md +8 -3
  88. package/prompts/contexts/adapters/plannotator-plan.md +8 -3
  89. package/prompts/contexts/gist-authoring.md +6 -20
  90. package/prompts/contexts/objective-authoring.md +6 -18
  91. package/prompts/stages/address/action.md +3 -14
  92. package/prompts/stages/address/preview.md +2 -13
  93. package/prompts/stages/audit.md +18 -0
  94. package/prompts/stages/conflict-resolution.md +1 -1
  95. package/prompts/stages/gist-author/seed.md +4 -4
  96. package/prompts/stages/gist-save.md +1 -1
  97. package/prompts/stages/implement.md +3 -1
  98. package/prompts/stages/learn-code.md +1 -1
  99. package/prompts/stages/learn-docs.md +2 -2
  100. package/prompts/stages/learn-dream.md +10 -0
  101. package/prompts/stages/learn-harvest.md +15 -0
  102. package/prompts/stages/learn-orchestrate.md +1 -1
  103. package/prompts/stages/objective-author/adopt.md +5 -3
  104. package/prompts/stages/objective-author/file.md +5 -4
  105. package/prompts/stages/objective-author/seed.md +5 -4
  106. package/prompts/stages/objective-land.md +7 -0
  107. package/prompts/stages/objective-plan/guidance.md +2 -13
  108. package/prompts/stages/objective-plan/seed.md +5 -12
  109. package/prompts/stages/objective-recover.md +8 -0
  110. package/prompts/stages/objective-replan.md +4 -2
  111. package/prompts/stages/objective-review-browser.md +9 -0
  112. package/prompts/stages/objective-sync.md +6 -0
  113. package/prompts/stages/plan-from/adopt.md +2 -2
  114. package/prompts/stages/plan-from/file.md +2 -2
  115. package/prompts/stages/plan-review-browser.md +9 -0
  116. package/prompts/stages/pr-review-browser/active.md +6 -14
  117. package/prompts/stages/pr-review-browser/foreign.md +6 -14
  118. package/prompts/stages/pr-review-dynamic.md +6 -6
  119. package/prompts/stages/pr-review-terminal/active.md +7 -15
  120. package/prompts/stages/pr-review-terminal/foreign.md +7 -15
  121. package/prompts/stages/pr-review-terminal/local.md +1 -1
  122. package/prompts/stages/pr-review.md +5 -5
  123. package/prompts/stages/replan.md +3 -3
  124. package/shared/README.md +2 -2
  125. package/shared/bindings.yaml +15 -0
  126. package/shared/contracts-history.md +11 -0
  127. package/shared/contracts.md +5373 -281
  128. package/shared/providers.yaml +27 -60
  129. package/shared/registry.yaml +42 -18
  130. package/shared/schemas/inputs/review-post-batch.schema.json +14 -1
  131. package/shared/schemas/outputs/objective-doctor.schema.json +487 -0
  132. package/shared/schemas/outputs/objective-stack-land.schema.json +732 -0
  133. package/shared/schemas/outputs/objective-stack-recover.schema.json +397 -0
  134. package/shared/schemas/outputs/objective-stack-status.schema.json +577 -0
  135. package/shared/schemas/outputs/objective-stack-sync.schema.json +184 -0
  136. package/shared/schemas/outputs/plan-save.schema.json +12 -0
  137. package/shared/schemas/outputs/pr-land.schema.json +3 -3
  138. package/shared/schemas/outputs/pr-submit.schema.json +177 -1
  139. package/extension/adapters/todoAdapterJuicesharp.ts +0 -102
  140. package/extension/checkpoints/checkpoints.ts +0 -550
  141. package/extension/checkpoints/planSteps.ts +0 -108
  142. package/extension/doors/askUser.ts +0 -198
  143. package/prompts/common/output-schemas/objective-explorer.md +0 -36
  144. package/prompts/common/output-schemas/review-classifier.md +0 -47
  145. package/prompts/contexts/adapters/juicesharp-todo.md +0 -7
@@ -53,20 +53,29 @@ export function ok<D extends object>(
53
53
  /**
54
54
  * Bind the module's fail constructor once: `const fail = failFor(ctx, scope)` (or
55
55
  * `failFor(ctx, scope, label)` when the content label differs from the report scope). Each call
56
- * reports loudly (`report(ctx, scope, "error", message, { alsoLog: true })`) and returns the
57
- * canonical soft failure: content `"<label> failed: <message>"`, details
58
- * `{ ok: false, error: message, error_type: errorType }`, no `terminate`.
56
+ * reports through the terminal-safe report seam (`alsoLog` mirrors complete diagnostics only in
57
+ * headless/RPC contexts) and returns the canonical soft failure: content
58
+ * `"<label> failed: <message>"`, details
59
+ * `{ ok: false, error: message, error_type: errorType }`, no `terminate`. `X` is the
60
+ * `FailDetails` extras hook: `failFor<X>(…)` lets a call attach module-specific fail details
61
+ * (spread AFTER `error`/`error_type`) — e.g. the learn wave's attempt receipts. Extras stay
62
+ * optional, so existing extras-free call sites are unchanged.
59
63
  */
60
- export function failFor(
64
+ export function failFor<X extends object = Record<never, never>>(
61
65
  target: ReportTarget,
62
66
  scope: string,
63
67
  label: string = scope,
64
- ): (message: string, errorType: string) => FailResult {
65
- return (message, errorType) => {
68
+ ): (message: string, errorType: string, extras?: X) => FailResult<X> {
69
+ return (message, errorType, extras) => {
66
70
  report(target, scope, "error", message, { alsoLog: true });
67
71
  return {
68
72
  content: [{ type: "text", text: `${label} failed: ${message}` }],
69
- details: { ok: false, error: message, error_type: errorType },
73
+ details: {
74
+ ok: false,
75
+ error: message,
76
+ error_type: errorType,
77
+ ...(extras ?? {}),
78
+ } as FailDetails<X>,
70
79
  };
71
80
  };
72
81
  }
@@ -33,10 +33,10 @@
33
33
  // loadable under `node --test`; accepts a minimal structural ctx (`BranchSource & { cwd }`).
34
34
 
35
35
  import { createHash } from "node:crypto";
36
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
36
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
37
37
  import { join, relative } from "node:path";
38
38
  import type { ReportTarget } from "../surfaces/report.ts";
39
- import { sessionDataDir } from "./cache.ts";
39
+ import { atomicWriteFileSync, ensureRunScratch, sessionDataDir } from "./cache.ts";
40
40
  import {
41
41
  appendWorkflowState,
42
42
  type BranchSource,
@@ -72,11 +72,13 @@ export function activeSessionDataDir(ctx: SessionDataCtx): string | null {
72
72
  return sessionDataDir(ctx.cwd, runId);
73
73
  }
74
74
 
75
- /** Ensure (mkdir -p) the current session's data dir; `null` + a warning on failure. */
75
+ /** Ensure the validated run root, then its data dir; `null` + a warning on failure. */
76
76
  export function ensureSessionDataDir(ctx: SessionDataCtx): string | null {
77
- const dir = activeSessionDataDir(ctx);
78
- if (dir === null) return null;
77
+ const runId = activeSessionRunId(ctx);
78
+ if (runId === null) return null;
79
+ const dir = sessionDataDir(ctx.cwd, runId);
79
80
  try {
81
+ ensureRunScratch(ctx.cwd, runId);
80
82
  mkdirSync(dir, { recursive: true });
81
83
  } catch (error) {
82
84
  console.error(`perk: warning: could not create session data dir ${dir}: ${error}`);
@@ -115,7 +117,7 @@ export function writeSessionData(
115
117
  if (dir === null) return null;
116
118
  const path = join(dir, name);
117
119
  try {
118
- writeFileSync(path, content, "utf8");
120
+ atomicWriteFileSync(path, content);
119
121
  } catch (error) {
120
122
  console.error(`perk: warning: could not write session data ${path}: ${error}`);
121
123
  return null;
@@ -13,9 +13,9 @@
13
13
  // wedge the save/launch/drive it rides on. Node builtins + cache.ts + git.ts only (loads under
14
14
  // `node --test`).
15
15
 
16
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
+ import { existsSync, readFileSync } from "node:fs";
17
17
  import { basename, join } from "node:path";
18
- import { runScratchDir } from "./cache.ts";
18
+ import { atomicWriteFileSync, ensureRunScratch, runScratchDir } from "./cache.ts";
19
19
  import { mainCheckoutRoot } from "./git.ts";
20
20
 
21
21
  export const SESSION_POINTERS_FILE = "session-pointers.json";
@@ -119,9 +119,8 @@ export function recordSessionPointer(
119
119
  // The run id is authoritative — a record read from disk keeps its own; a fresh one is minted
120
120
  // with `runId`. (A mismatched on-disk run_id is left as-is; self-keying guarantees a match.)
121
121
  record[klass][site] = pointer;
122
- const dir = runScratchDir(root, runId);
123
- mkdirSync(dir, { recursive: true });
124
- writeFileSync(sessionPointersPath(root, runId), serialize(record), "utf8");
122
+ ensureRunScratch(root, runId);
123
+ atomicWriteFileSync(sessionPointersPath(root, runId), serialize(record));
125
124
  return true;
126
125
  } catch (error) {
127
126
  console.error(`perk: warning: could not record session pointer (${klass}.${site}): ${error}`);
@@ -2,16 +2,19 @@
2
2
  //
3
3
  // pi-ai has no dedicated JSON-mode; structured output is done via tool calling. This module wraps
4
4
  // that idiom into two pure, dependency-light, NEVER-throwing helpers:
5
- // - `resolveModelAuth(ctx)` reuses the session's configured + authenticated model (the sanctioned
6
- // `ModelRegistry.getApiKeyAndHeaders` path), and
7
- // - `completeStructured(opts)` builds a single-tool `Context`, calls `complete`, and validates the
8
- // returned tool-call arguments against a TypeBox schema.
5
+ // - `resolveModelAuth(ctx)` reuses the session's configured + authenticated model (the compat
6
+ // `ModelRegistry.getApiKeyAndHeaders` path — the fallback for hosts without registry
7
+ // dispatch), and
8
+ // - `completeStructured(opts)` builds a single-tool `Context`, completes it (via the injected
9
+ // registry `dispatch` when the host provides one, else the compat `complete`), and validates
10
+ // the returned tool-call arguments against a TypeBox schema.
9
11
  // Both report failure via a soft `{ ok:false, error }` outcome — no throws ever reach the caller, so
10
12
  // every consumer can stay fail-safe with a deterministic fallback. The first consumer is
11
13
  // `extension/factories/planTitle.ts` (LLM-generated plan-issue titles).
12
14
 
13
15
  import {
14
16
  type Api,
17
+ type AssistantMessage,
15
18
  type Context,
16
19
  type Model,
17
20
  type Static,
@@ -24,21 +27,54 @@ import {
24
27
  // keeps the types. Pi's extension loader aliases both the root and /compat to the compat entry.
25
28
  import { complete } from "@earendil-works/pi-ai/compat";
26
29
 
27
- /** Structurally-minimal slice of `ExtensionContext` needed to reuse the session's model + auth. */
30
+ /**
31
+ * Registry dispatch (mirrors pi ≥ 0.84's `ModelRegistry.complete`): pi owns final request
32
+ * assembly — resolved auth, nullable headers, credential-resolved `baseUrl`, provider `env` —
33
+ * end to end. Feature-detect it (`typeof … === "function"`); absent on older hosts.
34
+ */
35
+ export type ModelDispatch = (
36
+ model: Model<Api>,
37
+ context: Context,
38
+ options: { signal?: AbortSignal; timeoutMs?: number },
39
+ ) => Promise<AssistantMessage>;
40
+
41
+ /**
42
+ * Structurally-minimal slice of `ExtensionContext` needed to reuse the session's model + auth.
43
+ * Header values mirror pi-ai's `ProviderHeaders` (`string | null` — null is a header-deletion
44
+ * marker); `baseUrl`/`env` mirror pi 0.84's `ResolvedRequestAuth`.
45
+ */
28
46
  export interface ModelAuthContext {
29
47
  model: Model<Api> | undefined;
30
48
  modelRegistry: {
31
- getApiKeyAndHeaders(
32
- model: Model<Api>,
33
- ): Promise<
34
- { ok: true; apiKey?: string; headers?: Record<string, string> } | { ok: false; error: string }
49
+ getApiKeyAndHeaders(model: Model<Api>): Promise<
50
+ | {
51
+ ok: true;
52
+ apiKey?: string;
53
+ headers?: Record<string, string | null>;
54
+ baseUrl?: string;
55
+ env?: Record<string, string>;
56
+ }
57
+ | { ok: false; error: string }
35
58
  >;
59
+ /** pi ≥ 0.84 registry dispatch; absent on older hosts (feature-detected, never assumed). */
60
+ complete?(
61
+ model: Model<Api>,
62
+ context: Context,
63
+ options?: { signal?: AbortSignal; timeoutMs?: number },
64
+ ): Promise<AssistantMessage>;
36
65
  };
37
66
  }
38
67
 
39
68
  /** Resolved model + auth, or a soft failure (no model / unresolved auth). */
40
69
  export type ResolvedModelAuth =
41
- | { ok: true; model: Model<Api>; apiKey?: string; headers?: Record<string, string> }
70
+ | {
71
+ ok: true;
72
+ model: Model<Api>;
73
+ apiKey?: string;
74
+ headers?: Record<string, string | null>;
75
+ baseUrl?: string;
76
+ env?: Record<string, string>;
77
+ }
42
78
  | { ok: false; error: string };
43
79
 
44
80
  /** The generic structured-output outcome — soft success/failure, never a throw. */
@@ -60,7 +96,14 @@ export async function resolveModelAuth(ctx: ModelAuthContext): Promise<ResolvedM
60
96
  try {
61
97
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
62
98
  if (!auth.ok) return { ok: false, error: auth.error };
63
- return { ok: true, model, apiKey: auth.apiKey, headers: auth.headers };
99
+ return {
100
+ ok: true,
101
+ model,
102
+ apiKey: auth.apiKey,
103
+ headers: auth.headers,
104
+ baseUrl: auth.baseUrl,
105
+ env: auth.env,
106
+ };
64
107
  } catch (err) {
65
108
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
66
109
  }
@@ -79,8 +122,17 @@ export interface CompleteStructuredOptions<S extends TSchema> {
79
122
  instruction: string;
80
123
  /** The payload (e.g. the document to summarize/classify). */
81
124
  input: string;
125
+ /**
126
+ * Registry dispatch (pi ≥ 0.84). When present it REPLACES the compat `complete` +
127
+ * apiKey/headers assembly below — pi owns auth end to end, including the credential-resolved
128
+ * `baseUrl` the fallback path cannot carry.
129
+ */
130
+ dispatch?: ModelDispatch;
82
131
  apiKey?: string;
83
- headers?: Record<string, string>;
132
+ /** `string | null` mirrors pi-ai's `ProviderHeaders` — a null value deletes a default header. */
133
+ headers?: Record<string, string | null>;
134
+ /** Provider-scoped environment values (pi 0.84 `ResolvedRequestAuth.env`), fallback path only. */
135
+ env?: Record<string, string>;
84
136
  signal?: AbortSignal;
85
137
  timeoutMs?: number;
86
138
  }
@@ -114,14 +166,21 @@ export async function completeStructured<S extends TSchema>(
114
166
  tools: [tool],
115
167
  };
116
168
 
117
- let msg: Awaited<ReturnType<typeof complete>>;
169
+ // Primary: registry dispatch (pi ≥ 0.84 owns auth/headers/baseUrl/env). Fallback: the compat
170
+ // `complete` with caller-resolved auth — it forwards apiKey/headers/env but has NO `baseUrl`
171
+ // option, so a credential-resolved endpoint is silently dropped on old hosts (the fallback's
172
+ // named, pre-existing limitation; the dispatch path is the fix).
173
+ let msg: AssistantMessage;
118
174
  try {
119
- msg = await complete(opts.model, context, {
120
- apiKey: opts.apiKey,
121
- headers: opts.headers,
122
- signal: opts.signal,
123
- timeoutMs: opts.timeoutMs,
124
- });
175
+ msg = opts.dispatch
176
+ ? await opts.dispatch(opts.model, context, { signal: opts.signal, timeoutMs: opts.timeoutMs })
177
+ : await complete(opts.model, context, {
178
+ apiKey: opts.apiKey,
179
+ headers: opts.headers,
180
+ env: opts.env,
181
+ signal: opts.signal,
182
+ timeoutMs: opts.timeoutMs,
183
+ });
125
184
  } catch (err) {
126
185
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
127
186
  }
@@ -89,8 +89,9 @@ export const LINEAR_MUTATING_TOOLS: readonly string[] = [
89
89
  * (≈830 schema chars — accepted, documented, test-pinned). A later `session_tree` re-apply
90
90
  * filters over the original snapshot (which lacks the late names), so a tree navigation drops
91
91
  * them — the pre-existing snapshot behavior, unchanged. Child-side tools (`contact_supervisor`,
92
- * `structured_output`) are out of scope: spawned children are unscoped by design (§8.40
93
- * adopt-never-impersonates).
92
+ * `structured_output`) are out of scope for the STAGE census spawned children stay
93
+ * stage-unscoped by design (§8.40 adopt-never-impersonates) — but they DO ride READ_ONLY_TOOLS,
94
+ * because the read-only gate IS inherited by adopted children (see SUBAGENT_CHILD_TOOLS).
94
95
  */
95
96
  export const SUBAGENT_TOOLS: readonly string[] = [
96
97
  "subagent",
@@ -99,6 +100,31 @@ export const SUBAGENT_TOOLS: readonly string[] = [
99
100
  "intercom",
100
101
  ];
101
102
 
103
+ /**
104
+ * pi-subagents' CHILD-side engine tools. `structured_output` and `contact_supervisor` register
105
+ * only inside spawned child sessions (the env-keyed prompt-runtime registration — never in
106
+ * parents, so those names are inert in every gated parent session; `setActiveTools` ignores
107
+ * unknown names). `subagent_wait` is ALSO registered by the top-level parent extension
108
+ * (pi-subagents 0.45.1's `registerWaitTool`) — the accepted widening: a wait-only,
109
+ * non-repo-mutating tool active in gated parents. A gated ADOPTED child (mode inherited via the
110
+ * `adopt` arm, contracts.md §8.3) must keep them active:
111
+ * - `structured_output` is the engine-REQUIRED completion call when the launch carries an
112
+ * `outputSchema` — stripping it makes the child physically unable to finish and fails the
113
+ * run with `structuredOutputFailed`;
114
+ * - `contact_supervisor` is the child→parent intercom door;
115
+ * - `subagent_wait` is the fanout-child wait door.
116
+ * None mutates the repo (`structured_output` writes only the engine's capture file under
117
+ * `.pi-subagents/` scratch). Census decision, recorded: these names deliberately join NEITHER
118
+ * PERK_TOOLS nor BORROWED_TOOLS — the stage-filter universe never sees them because children
119
+ * are stage-unscoped by design (adopt never impersonates a stage), so gate membership is their
120
+ * only governance surface.
121
+ */
122
+ export const SUBAGENT_CHILD_TOOLS: readonly string[] = [
123
+ "structured_output",
124
+ "contact_supervisor",
125
+ "subagent_wait",
126
+ ];
127
+
102
128
  /**
103
129
  * @ff-labs/pi-fff's search tools. BOTH mode name-sets are enumerated (static names, inert
104
130
  * when absent — the code_search version-tolerance precedent): warm sessions run pi-fff's
@@ -131,9 +157,12 @@ export const FFF_SEARCH_TOOLS: readonly string[] = [
131
157
  * stage set over a foreign restriction. Pre-existing interplay, recorded, not re-engineered.
132
158
  * - Zero-tool packages: @tombell/pi-diff (commands only), the footer providers, and the hunk
133
159
  * review CLI (not a Pi package) register nothing — nothing to enumerate.
134
- * - Single-governance rule: `ask_user_question` must stay OUT of this census — the
135
- * @juicesharp/rpiv-ask-user-question provider registers the IDENTICAL name perk does, so the
136
- * name-keyed PERK_TOOLS entry already governs both registrations (hygiene-tested).
160
+ * - Single-governance rule: a name is governed ONCE it lives in exactly one census. perk
161
+ * registers no same-named `ask_user_question` anymore (the first-party tool is deleted), so
162
+ * the name lives HERE, in the borrowed census, not in PERK_TOOLS (hygiene-tested).
163
+ * Registration timing nuance: @juicesharp/rpiv-ask-user-question registers the tool at load
164
+ * time, then a `hasUI`-keyed reconcile strips/restores it — headless sessions carry no
165
+ * `ask_user_question` schema at all.
137
166
  * - @ff-labs/pi-fff (FFF_SEARCH_TOOLS): registration timing load-time (both modes); no
138
167
  * `setFooter` (only a keyed optional-chained `setStatus`); zero bundled skills.
139
168
  */
@@ -143,7 +172,12 @@ export const BORROWED_TOOLS: readonly string[] = [
143
172
  ...LINEAR_MUTATING_TOOLS,
144
173
  ...SUBAGENT_TOOLS,
145
174
  ...FFF_SEARCH_TOOLS,
146
- "todo", // @juicesharp/rpiv-todo (the juicesharp-todo provider) — load-time
175
+ // @juicesharp/rpiv-todo (required borrow) — registers at load; its checklist overlay is
176
+ // `hasUI`-gated (headless-safe).
177
+ "todo",
178
+ // @juicesharp/rpiv-ask-user-question (required borrow) — registers at load; strips itself
179
+ // headlessly (!hasUI reconcile).
180
+ "ask_user_question",
147
181
  // @plannotator/pi-extension: perk never drives its plan phases (the adapter bridges
148
182
  // `plan_review` to its event API), so the submit tool is dead weight in stage sessions.
149
183
  "plannotator_submit_plan",
@@ -189,23 +223,60 @@ export const READ_ONLY_TOOLS = [
189
223
  // FFF local search belongs in read-only exploration (the override names find/grep are
190
224
  // already present above; these are the additive tools-and-ui names + multi_grep).
191
225
  ...FFF_SEARCH_TOOLS,
192
- // The delegation carve-in: the gated objective-plan seed/guidance names the
193
- // `perk.objective-explorer` spawn, so `subagent`/`wait` (+ the parent supervisor pair, which
194
- // already leaks active into cold-door gated sessions via late registration — keeping
195
- // warm-entered gates consistent, and letting the parent answer child `contact_supervisor`
196
- // asks) must be reachable while gated. ACCEPTED LENIENCY, deliberately documented: spawned
197
- // children are unscoped by design (§8.40 adopt-never-impersonates). The explorer's agent def
198
- // is structurally write-blocked (`tools: read, grep, find, ls, bash` frontmatter in
199
- // agents/objective-explorer.md), but the `subagent` tool itself can spawn ad-hoc read-write
226
+ // The delegation carve-in: `subagent`/`wait` (+ the parent supervisor pair, which already
227
+ // leaks active into cold-door gated sessions via late registration keeping warm-entered
228
+ // gates consistent, and letting the parent answer child `contact_supervisor` asks) stay
229
+ // reachable while gated for the other delegation flows (the gated objective-plan guidance now
230
+ // names the `explore_objective_node` tool below, not a direct spawn). ACCEPTED LENIENCY,
231
+ // deliberately documented: spawned children are unscoped by design (§8.40
232
+ // adopt-never-impersonates), and the `subagent` tool itself can spawn ad-hoc read-write
200
233
  // children — a posture choice with NO agent-allowlist backstop, consistent with the arg-blind
201
234
  // `curl`/`agent-browser` precedents (contracts.md §8.3).
202
235
  ...SUBAGENT_TOOLS,
236
+ // The explorer-wave carve-in: the gated objective-plan session's OPTIONAL explore step is the
237
+ // `explore_objective_node` tool — it spawns the read-only `perk.objective-explorer` child over
238
+ // the already-carved-in SUBAGENT tools (the draft-review-wave precedent) and writes nothing to
239
+ // the worktree.
240
+ "explore_objective_node",
241
+ // The child-side carve-in: gated adopt-children must keep the engine's injected tools — see
242
+ // SUBAGENT_CHILD_TOOLS.
243
+ ...SUBAGENT_CHILD_TOOLS,
244
+ // The draft-review-door carve-in: plan-authoring sessions run GATED, so the
245
+ // /plan-review-browser companions must be reachable while read-only. `push_annotations` only
246
+ // POSTs findings to the door-primed local plannotator server (no worktree writes — the
247
+ // fetch_content cache-write precedent class); the wave pair spawns the read-only
248
+ // `perk.draft-reviewer` over the already-carved-in SUBAGENT_TOOLS/SUBAGENT_CHILD_TOOLS.
249
+ "push_annotations",
250
+ "start_draft_review_wave",
251
+ "collect_draft_review_wave",
252
+ // The audit-wave carve-in: the seeded `perk-dev audit judge` session runs GATED (the `audit`
253
+ // stage is read-only), so `run_audit_wave` must be reachable while read-only. Its one write
254
+ // (`<bundle>/verdicts.json`) is structurally bound to the cold door's workflow-state
255
+ // `audit_bundle_dir` — the tool takes NO parameters, so no caller-supplied path exists and a
256
+ // gated session cannot aim the writer anywhere (contracts.md §8.50).
257
+ "run_audit_wave",
258
+ // The harvest-wave carve-in: the seeded learn-harvest session runs GATED (the read-only
259
+ // objective-author borrow), so `run_harvest_wave` must be reachable while read-only. Its
260
+ // manifest read is structurally bound to the session's claimed run-scoped scratch path (the
261
+ // `manifest_path` param is verified against it and any other path refused — the
262
+ // `run_audit_wave` no-aimable-writer posture, read-side), it spawns the read-only
263
+ // `perk.harvest-analyst` over the already-carved-in SUBAGENT_TOOLS/SUBAGENT_CHILD_TOOLS, and
264
+ // it writes nothing to the worktree (contracts.md §8.48).
265
+ "run_harvest_wave",
266
+ // The dream-wave carve-in: the seeded `perk learn dream` session runs GATED (the read-only
267
+ // objective-author borrow), so `run_dream_wave` must be reachable while read-only. The tool
268
+ // takes NO parameters: its manifest read AND its one write (the fixed-name run-scratch
269
+ // bundle beside that manifest) are both derived from the claimed run's manifest path — no
270
+ // caller-supplied path exists (the `run_audit_wave` no-aimable-writer posture, BOTH sides),
271
+ // and it spawns only the read-only `perk.dream-analyst`/`perk.dream-reducer` over the
272
+ // already-carved-in delegation family (contracts.md §8.61).
273
+ "run_dream_wave",
203
274
  ];
204
275
 
205
276
  /**
206
277
  * Every tool perk itself registers (contracts.md §8.40). Name-keyed: `setActiveTools` ignores
207
- * unknown names, so a vacated registration (e.g. `ask_user_question` under a foreign
208
- * `[providers] askuser` selection registers the IDENTICAL name) or an absent tool is inert.
278
+ * unknown names, so an absent tool is inert (e.g. a borrowed census name whose package stripped
279
+ * or never registered it `ask_user_question` in a headless session has nothing to enable).
209
280
  * Stage scoping filters the scoped universe `PERK_TOOLS ∪ BORROWED_TOOLS` — builtins and
210
281
  * un-enumerated foreign names pass through untouched (fail-open).
211
282
  */
@@ -222,16 +293,34 @@ export const PERK_TOOLS: readonly string[] = [
222
293
  "gist_save",
223
294
  "learn",
224
295
  "run_learn_wave",
225
- "ask_user_question",
296
+ "run_audit_wave",
297
+ "run_harvest_wave",
298
+ "run_dream_wave",
226
299
  "land",
227
300
  "post_pr_review",
228
301
  "ready",
229
- "resolve_review_threads",
302
+ "classify_review_feedback",
303
+ "finalize_address",
304
+ "explore_objective_node",
230
305
  "run_pr_review_wave",
231
306
  "run_pr_review_dynamic_wave",
232
307
  "submit_pr_review",
308
+ "start_review_wave",
309
+ "collect_review_wave",
310
+ "push_annotations",
311
+ "start_draft_review_wave",
312
+ "collect_draft_review_wave",
233
313
  "run_ci",
234
314
  "submit",
315
+ // The stacked-delivery warm surface (contracts.md §8.51/§8.56): read + control tools over
316
+ // the cold `objective stack` workers. Never in READ_ONLY_TOOLS — sync/adopt/recover/land
317
+ // mutate published branches and PRs; the gated posture is the driving commands' soft
318
+ // refusal.
319
+ "objective_stack_status",
320
+ "objective_stack_sync",
321
+ "objective_stack_adopt",
322
+ "objective_stack_recover",
323
+ "objective_stack_land",
235
324
  ];
236
325
 
237
326
  /**
@@ -253,7 +342,7 @@ const RESEARCH_TOOLS: readonly string[] = [
253
342
  * the post-land reconcile drive — `/land` auto-drives `/objective-reconcile` in-session, whose
254
343
  * guidance names the reconcile trio, so the trio must be active in every worktree stage). The
255
344
  * headless worker also REQUIRES the model-invoked `submit` (implement) /
256
- * `resolve_review_threads` (address) to reach its completion bar. Borrowed additions: delegation
345
+ * `finalize_address` (address) to reach its completion bar. Borrowed additions: delegation
257
346
  * (SUBAGENT_TOOLS — the `/pr-review`/`/address`/`/submit`-conflict/`/learn` orchestration flows)
258
347
  * and `todo` (the foreign checklist overlay the implement-progress discipline rides) are
259
348
  * worktree-family only.
@@ -266,17 +355,34 @@ const WORKTREE_STAGE_TOOLS: readonly string[] = [
266
355
  "land",
267
356
  "learn",
268
357
  "run_learn_wave",
269
- "resolve_review_threads",
358
+ "classify_review_feedback",
359
+ "finalize_address",
270
360
  "post_pr_review",
271
361
  "run_pr_review_wave",
272
362
  "run_pr_review_dynamic_wave",
273
363
  "submit_pr_review",
364
+ // The human review doors' companion tools (/pr-review-terminal, /pr-review-browser): the
365
+ // review-wave pair + the door-primed annotation push. The plan-stage widening landed via the
366
+ // draft-review door (/plan-review-browser): the plan-family stage lists carry the draft-wave
367
+ // pair + push_annotations.
368
+ "start_review_wave",
369
+ "collect_review_wave",
370
+ "push_annotations",
274
371
  // The reconcile trio: `/land` auto-drives the objective-reconcile pass inside the CURRENT
275
372
  // worktree session (driveReconcileAfterLand), and the manual `/objective-reconcile` gesture is
276
373
  // registered globally — both inject guidance naming these three tools.
277
374
  "reconcile_objective",
278
375
  "add_objective_node",
279
376
  "objective_node",
377
+ // The stacked-delivery quintet: `/objective-sync`/`/objective-recover`/`/objective-land`
378
+ // drive worktree sessions (post-amend sync from implement/address; recovery and the atomic
379
+ // landing from anywhere in the PR loop), so their guidance-named tools must be active
380
+ // across the whole family.
381
+ "objective_stack_status",
382
+ "objective_stack_sync",
383
+ "objective_stack_adopt",
384
+ "objective_stack_recover",
385
+ "objective_stack_land",
280
386
  ...RESEARCH_TOOLS,
281
387
  ...SUBAGENT_TOOLS,
282
388
  "todo",
@@ -286,7 +392,8 @@ const WORKTREE_STAGE_TOOLS: readonly string[] = [
286
392
  * Per-stage active perk tools for gate-OFF sessions (contracts.md §8.40). Keys = the registry
287
393
  * stage ids; an unknown/absent stage id is fail-open (no filtering — version-skew safety).
288
394
  * Rationale pins:
289
- * - `ask_user_question` is universal (every stage list carries it).
395
+ * - `ask_user_question` is universal (every stage list carries it); the name is BORROWED now
396
+ * (the @juicesharp questionnaire, via BORROWED_TOOLS — headless sessions carry no schema).
290
397
  * - `plan`/`save` cover the plan-family stage borrowers (`plan from`/`plan replan`/
291
398
  * `learn docs`/`learn code` borrow `plan`; `skills create/refine` borrow `save`).
292
399
  * - `objective-author`/`objective-save` cover `objective replan` + `objective author --from`.
@@ -296,6 +403,12 @@ const WORKTREE_STAGE_TOOLS: readonly string[] = [
296
403
  * three objective stages (the post-save `/objective-reconcile` gesture) AND the worktree
297
404
  * family (the post-land `driveReconcileAfterLand` drive + the manual `/objective-reconcile`
298
405
  * gesture — its guidance names all three).
406
+ * - the draft-review companions (`start_draft_review_wave`/`collect_draft_review_wave`/
407
+ * `push_annotations`) also ride the two objective stages (§8.23's
408
+ * `/objective-review-browser` — gate-OFF coverage: after `objectiveApprovalSave` exits the
409
+ * gate mid-flow, late collects/pushes must not dead-end), and `plan_review` rides them
410
+ * because the door guidance names it (in both objective stages it routes to the objective
411
+ * review arm; the drive-coverage guard forces both the moment the guidance names them).
299
412
  */
300
413
  export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
301
414
  "gist-author": ["ask_user_question", "gist_draft", "gist_save", ...RESEARCH_TOOLS],
@@ -307,6 +420,13 @@ export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
307
420
  "reconcile_objective",
308
421
  "add_objective_node",
309
422
  "objective_node",
423
+ // The /objective-review-browser companions (gate-OFF coverage: after objectiveApprovalSave
424
+ // exits the gate mid-flow, late collects/pushes must not dead-end) + plan_review (the door
425
+ // guidance names it; it routes to the objective review arm here).
426
+ "start_draft_review_wave",
427
+ "collect_draft_review_wave",
428
+ "push_annotations",
429
+ "plan_review",
310
430
  ...RESEARCH_TOOLS,
311
431
  ],
312
432
  "objective-save": [
@@ -316,6 +436,11 @@ export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
316
436
  "reconcile_objective",
317
437
  "add_objective_node",
318
438
  "objective_node",
439
+ // The /objective-review-browser companions + plan_review (see the objective-author note).
440
+ "start_draft_review_wave",
441
+ "collect_draft_review_wave",
442
+ "push_annotations",
443
+ "plan_review",
319
444
  ...RESEARCH_TOOLS,
320
445
  ],
321
446
  "objective-plan": [
@@ -324,17 +449,48 @@ export const STAGE_TOOLS: Readonly<Record<string, readonly string[]>> = {
324
449
  "plan_review",
325
450
  "plan_save",
326
451
  "objective_node",
452
+ "explore_objective_node",
327
453
  "reconcile_objective",
328
454
  "add_objective_node",
455
+ // The /plan-review-browser companions (gate-OFF coverage: after approvalSave exits the gate
456
+ // mid-flow, late collects/pushes must not dead-end — the drive-coverage guard forces this
457
+ // the moment the guidance names them).
458
+ "start_draft_review_wave",
459
+ "collect_draft_review_wave",
460
+ "push_annotations",
461
+ ...RESEARCH_TOOLS,
462
+ ],
463
+ plan: [
464
+ "ask_user_question",
465
+ "plan_draft",
466
+ "plan_review",
467
+ "plan_save",
468
+ // The /plan-review-browser companions (see the objective-plan note).
469
+ "start_draft_review_wave",
470
+ "collect_draft_review_wave",
471
+ "push_annotations",
472
+ ...RESEARCH_TOOLS,
473
+ ],
474
+ save: [
475
+ "ask_user_question",
476
+ "plan_draft",
477
+ "plan_review",
478
+ "plan_save",
479
+ // The /plan-review-browser companions (see the objective-plan note).
480
+ "start_draft_review_wave",
481
+ "collect_draft_review_wave",
482
+ "push_annotations",
329
483
  ...RESEARCH_TOOLS,
330
484
  ],
331
- plan: ["ask_user_question", "plan_draft", "plan_review", "plan_save", ...RESEARCH_TOOLS],
332
- save: ["ask_user_question", "plan_draft", "plan_review", "plan_save", ...RESEARCH_TOOLS],
333
485
  implement: WORKTREE_STAGE_TOOLS,
334
486
  submit: WORKTREE_STAGE_TOOLS,
335
487
  address: WORKTREE_STAGE_TOOLS,
336
488
  land: WORKTREE_STAGE_TOOLS,
337
489
  learn: WORKTREE_STAGE_TOOLS,
490
+ // The dev-only session-audit orchestrator (`perk-dev audit judge`). The session runs GATED
491
+ // (read-only mode), where this list is inert; it exists for the keys≡registry pin and the
492
+ // defensive gate-off arm.
493
+ audit: ["ask_user_question", "run_audit_wave", ...RESEARCH_TOOLS],
338
494
  };
339
495
 
340
496
  /** The read-only marker / custom-message type injected into context while active. */