@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
@@ -1,294 +0,0 @@
1
- // The perk-owned, in-process read-only child session (context-isolation primitive #1).
2
- //
3
- // A deterministic, fully-isolated read-only child spun at the SDK level via `createAgentSession`,
4
- // plus the handoff contract both context-isolation primitives honor (the in-process one here; the
5
- // spawned shape later): cap the model-visible output, keep the FULL result in a verified scratch
6
- // file + a structured block, and return DOUBLE-DELIVERY (compact prose for the human + a structured
7
- // block for the orchestrator). Route-don't-relay is enforced structurally — the raw child output
8
- // never enters the parent; only a path/summary does. This is substrate only; its consumer is the
9
- // read-only CI executor. No registry stage, no door change, no cross-CLI behavior.
10
- //
11
- // Relationship to the in-session gate (extension/substrate/toolGating.ts): its READ_ONLY_TOOLS is the *in-session* allowlist
12
- // and includes a sub-allowlisted `bash`. This SDK-level set is the STRICTER ["read","grep","find",
13
- // "ls"] (no bash) — a separate constant, not a reuse. The CI executor composes its own allowlist when it needs a
14
- // gated test-runner command; that is not authored here.
15
- //
16
- // Isolation comes from the DefaultResourceLoader `no*` flags + the tools allowlist — NOT from
17
- // `extensionFactories: []` (which is already the default and controls only inline factories; it
18
- // does not stop `loader.reload()` from resolving the project's `.pi/settings.json` packages and
19
- // loading perk's own extension into the child). The `no*` flags keep perk's machinery out of the
20
- // child and keep the path offline/deterministic. A custom loader is reloaded by the caller.
21
-
22
- import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
23
- import { tmpdir } from "node:os";
24
- import { join } from "node:path";
25
- import type { Api, Model } from "@earendil-works/pi-ai";
26
- import {
27
- type AgentSession,
28
- createAgentSession,
29
- DefaultResourceLoader,
30
- SessionManager,
31
- SettingsManager,
32
- } from "@earendil-works/pi-coding-agent";
33
- import { atomicWriteFileSync, ensureRunScratch, scratchDir } from "../substrate/cache.ts";
34
-
35
- /**
36
- * The SDK-level read-only allowlist (no `bash`; stricter than the in-session READ_ONLY_TOOLS).
37
- * Mirrors pi's read-only recipe (examples/sdk/05-tools.ts).
38
- */
39
- export const SDK_READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
40
-
41
- /** The model-visible byte cap (matches subagent's PER_TASK_OUTPUT_CAP); overridable per call. */
42
- export const DEFAULT_MODEL_VISIBLE_CAP = 50 * 1024;
43
-
44
- /** Minimal structural shape of a session message (we only read assistant text parts). */
45
- interface MessageLike {
46
- role?: string;
47
- content?: unknown;
48
- }
49
-
50
- /**
51
- * Scan messages newest-first for the last assistant message's first text part; "" if none.
52
- * Pure — mirrors subagent's getFinalOutput.
53
- */
54
- export function extractFinalAssistantText(messages: readonly MessageLike[]): string {
55
- for (let i = messages.length - 1; i >= 0; i--) {
56
- const msg = messages[i];
57
- if (msg?.role !== "assistant") continue;
58
- const content = msg.content;
59
- if (typeof content === "string") return content;
60
- if (Array.isArray(content)) {
61
- for (const part of content) {
62
- const p = part as { type?: string; text?: string };
63
- if (p.type === "text" && typeof p.text === "string") return p.text;
64
- }
65
- }
66
- }
67
- return "";
68
- }
69
-
70
- export interface CapResult {
71
- /** The (possibly truncated) text safe to show the model. */
72
- shown: string;
73
- /** Total UTF-8 byte length of the original text. */
74
- bytesTotal: number;
75
- /** UTF-8 byte length of `shown` (before the truncation notice). */
76
- bytesShown: number;
77
- /** Whether truncation occurred. */
78
- truncated: boolean;
79
- }
80
-
81
- /**
82
- * UTF-8-byte-safe truncation (subagent's byte-trim loop). Under cap ⇒ unchanged, truncated:false.
83
- * When truncated, a notice points at the scratch file holding the full result; the notice sits at
84
- * the cut edge (appended in head mode, prepended in tail mode) so a top-down reader immediately
85
- * knows which side is missing.
86
- *
87
- * `keep` mirrors the SDK's truncateHead/truncateTail guidance: "head" (default) for
88
- * model-authored summaries/handoffs where the beginning matters; "tail" for command/CI logs where
89
- * failure summaries live at the end. Deliberately perk's own byte-only util (not the SDK's
90
- * line-count-aware `truncateTail`): `CapResult`'s byte fields and the scratch-pointing notice are
91
- * load-bearing in `ChildStructured`/`CiCheckResult`. Pure.
92
- */
93
- export function capForModel(
94
- text: string,
95
- cap: number = DEFAULT_MODEL_VISIBLE_CAP,
96
- scratchPath: string | null = null,
97
- keep: "head" | "tail" = "head",
98
- ): CapResult {
99
- const bytesTotal = Buffer.byteLength(text, "utf8");
100
- if (bytesTotal <= cap) {
101
- return { shown: text, bytesTotal, bytesShown: bytesTotal, truncated: false };
102
- }
103
- let trimmed = keep === "head" ? text.slice(0, cap) : text.slice(-cap);
104
- while (Buffer.byteLength(trimmed, "utf8") > cap) {
105
- trimmed = keep === "head" ? trimmed.slice(0, -1) : trimmed.slice(1);
106
- }
107
- const bytesShown = Buffer.byteLength(trimmed, "utf8");
108
- const omitted = bytesTotal - bytesShown;
109
- const where = scratchPath ? ` Full output preserved at ${scratchPath}.` : "";
110
- const shown =
111
- keep === "head"
112
- ? `${trimmed}\n\n[Output truncated: ${omitted} bytes omitted.${where}]`
113
- : `[Output truncated: ${omitted} bytes omitted.${where}]\n\n${trimmed}`;
114
- return { shown, bytesTotal, bytesShown, truncated: true };
115
- }
116
-
117
- /**
118
- * The structured half of the double-delivery handoff — the block the orchestrator parses (and the
119
- * CI executor will place in a tool's forking-safe `details`).
120
- */
121
- export interface ChildStructured {
122
- success: boolean;
123
- summary: string;
124
- scratchPath: string | null;
125
- bytesTotal: number;
126
- bytesShown: number;
127
- truncated: boolean;
128
- error?: string;
129
- }
130
-
131
- /**
132
- * The full double-delivery handoff: `prose` is the capped human-facing summary; `structured` is the
133
- * orchestrator-facing block; `scratchPath` is the verified path to the full result
134
- * (route-don't-relay — the raw output never enters the parent beyond the cap).
135
- */
136
- export interface ChildHandoff {
137
- success: boolean;
138
- prose: string;
139
- structured: ChildStructured;
140
- scratchPath: string | null;
141
- }
142
-
143
- /**
144
- * Create a locked-down, SDK-level read-only child session. Isolation = the DefaultResourceLoader
145
- * `no*` flags + the tools allowlist; a throwaway temp `agentDir` (a locked-down child loads nothing
146
- * from it, and there is no production ctx.agentDir). Creating the session is OFFLINE; only
147
- * `prompt()` requires a key. The caller must `dispose()` the returned session.
148
- */
149
- export async function createReadOnlySession(opts: {
150
- cwd: string;
151
- model?: Model<Api>;
152
- tools?: string[];
153
- }): Promise<{ session: AgentSession }> {
154
- // A throwaway temp agentDir — a locked-down child loads nothing from it (no production
155
- // ctx.agentDir exists), exactly as the offline harness does.
156
- const agentDir = mkdtempSync(join(tmpdir(), "perk-ro-agent-"));
157
- const loader = new DefaultResourceLoader({
158
- cwd: opts.cwd,
159
- agentDir,
160
- noExtensions: true,
161
- noSkills: true,
162
- noPromptTemplates: true,
163
- noThemes: true,
164
- noContextFiles: true,
165
- });
166
- // A custom loader is NOT auto-reloaded by createAgentSession — reload it ourselves.
167
- await loader.reload();
168
- const { session } = await createAgentSession({
169
- cwd: opts.cwd,
170
- model: opts.model,
171
- resourceLoader: loader,
172
- tools: opts.tools ?? SDK_READ_ONLY_TOOLS,
173
- sessionManager: SessionManager.inMemory(opts.cwd),
174
- settingsManager: SettingsManager.inMemory({
175
- compaction: { enabled: false },
176
- retry: { enabled: false },
177
- }),
178
- });
179
- return { session };
180
- }
181
-
182
- export interface RunReadOnlyChildOpts {
183
- cwd: string;
184
- task: string;
185
- model?: Model<Api>;
186
- tools?: string[];
187
- runId?: string;
188
- step?: string;
189
- modelVisibleCap?: number;
190
- signal?: AbortSignal;
191
- }
192
-
193
- export interface RunReadOnlyChildDeps {
194
- createSession?: (opts: {
195
- cwd: string;
196
- model?: Model<Api>;
197
- tools?: string[];
198
- }) => Promise<{ session: AgentSession }>;
199
- runTask?: (session: AgentSession, task: string, signal?: AbortSignal) => Promise<string>;
200
- }
201
-
202
- /** Default production runTask: prompt the child, then extract its final assistant text. */
203
- async function defaultRunTask(
204
- session: AgentSession,
205
- task: string,
206
- _signal?: AbortSignal,
207
- ): Promise<string> {
208
- await session.prompt(task);
209
- return extractFinalAssistantText(session.messages as unknown as MessageLike[]);
210
- }
211
-
212
- /** Resolve the scratch file path for a child's full output (run-scoped when a runId is given). */
213
- function resolveScratchPath(cwd: string, runId?: string, step?: string): string {
214
- if (runId) {
215
- const dir = ensureRunScratch(cwd, runId);
216
- return join(dir, `${step ?? "child"}.md`);
217
- }
218
- const base = scratchDir(cwd);
219
- mkdirSync(base, { recursive: true });
220
- const dir = mkdtempSync(join(base, "child-"));
221
- return join(dir, `${step ?? "child"}.md`);
222
- }
223
-
224
- function failure(prose: string, error: string): ChildHandoff {
225
- return {
226
- success: false,
227
- prose,
228
- scratchPath: null,
229
- structured: {
230
- success: false,
231
- summary: prose,
232
- scratchPath: null,
233
- bytesTotal: 0,
234
- bytesShown: 0,
235
- truncated: false,
236
- error,
237
- },
238
- };
239
- }
240
-
241
- /**
242
- * One-shot read-only child orchestrator implementing the handoff contract: create the read-only
243
- * session, run the task, write the full raw output to a scratch file and VERIFY it
244
- * (write→verify→pass-path), cap the model-visible output into prose, and return
245
- * double-delivery. Fail loud + fail closed: NEVER throws to the parent — on any error it returns
246
- * `{ success:false, scratchPath:null }` with the error in both `prose` and `structured.error`.
247
- */
248
- export async function runReadOnlyChild(
249
- opts: RunReadOnlyChildOpts,
250
- deps: RunReadOnlyChildDeps = {},
251
- ): Promise<ChildHandoff> {
252
- const createSession = deps.createSession ?? createReadOnlySession;
253
- const runTask = deps.runTask ?? defaultRunTask;
254
- const cap = opts.modelVisibleCap ?? DEFAULT_MODEL_VISIBLE_CAP;
255
-
256
- if (opts.signal?.aborted) return failure("read-only child aborted before start.", "aborted");
257
-
258
- let session: AgentSession | null = null;
259
- try {
260
- const created = await createSession({ cwd: opts.cwd, model: opts.model, tools: opts.tools });
261
- session = created.session;
262
-
263
- const output = await runTask(session, opts.task, opts.signal);
264
-
265
- if (opts.signal?.aborted) return failure("read-only child aborted.", "aborted");
266
-
267
- // write → verify → pass-path: persist the full result, then confirm it landed.
268
- const scratchPath = resolveScratchPath(opts.cwd, opts.runId, opts.step);
269
- atomicWriteFileSync(scratchPath, output);
270
- if (!existsSync(scratchPath)) {
271
- return failure("read-only child: scratch write could not be verified.", "scratch-verify");
272
- }
273
-
274
- const capped = capForModel(output, cap, scratchPath);
275
- return {
276
- success: true,
277
- prose: capped.shown,
278
- scratchPath,
279
- structured: {
280
- success: true,
281
- summary: capped.shown,
282
- scratchPath,
283
- bytesTotal: capped.bytesTotal,
284
- bytesShown: capped.bytesShown,
285
- truncated: capped.truncated,
286
- },
287
- };
288
- } catch (err) {
289
- const message = err instanceof Error ? err.message : String(err);
290
- return failure(`read-only child failed: ${message}`, message);
291
- } finally {
292
- session?.dispose();
293
- }
294
- }