@pi-unipi/background-tasks 2.6.1

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 (116) hide show
  1. package/README.md +87 -0
  2. package/extensions/anthropic-attribution.ts +1 -0
  3. package/extensions/delegate-child.ts +1 -0
  4. package/extensions/fusion-child.ts +1 -0
  5. package/package.json +40 -0
  6. package/src/__tests__/anthropic-attribution.test.ts +195 -0
  7. package/src/__tests__/config.test.ts +137 -0
  8. package/src/__tests__/core.test.ts +493 -0
  9. package/src/__tests__/delegate-artifacts.test.ts +528 -0
  10. package/src/__tests__/delegate-budget.test.ts +456 -0
  11. package/src/__tests__/delegate-launch.test.ts +676 -0
  12. package/src/__tests__/delegate-result-package.test.ts +350 -0
  13. package/src/__tests__/delegate-seed.test.ts +392 -0
  14. package/src/__tests__/durable-fs.test.ts +559 -0
  15. package/src/__tests__/extension-api.test.ts +579 -0
  16. package/src/__tests__/fusion-artifacts.test.ts +1039 -0
  17. package/src/__tests__/fusion-budget.test.ts +1356 -0
  18. package/src/__tests__/fusion-claude-cache.test.ts +320 -0
  19. package/src/__tests__/fusion-config.test.ts +335 -0
  20. package/src/__tests__/fusion-context-prompts.test.ts +670 -0
  21. package/src/__tests__/fusion-evaluation.test.ts +315 -0
  22. package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
  23. package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
  24. package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
  25. package/src/__tests__/fusion-model-selector.test.ts +205 -0
  26. package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
  27. package/src/__tests__/fusion-rpc.test.ts +369 -0
  28. package/src/__tests__/fusion-sdk.test.ts +1226 -0
  29. package/src/__tests__/fusion-v5-core.test.ts +219 -0
  30. package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
  31. package/src/__tests__/fusion-web-fetch.test.ts +485 -0
  32. package/src/__tests__/fusion-workflows.test.ts +59 -0
  33. package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
  34. package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
  35. package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
  36. package/src/__tests__/helpers/fusion-canonical.ts +140 -0
  37. package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
  38. package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
  39. package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
  40. package/src/__tests__/helpers/normalize.ts +22 -0
  41. package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
  42. package/src/__tests__/pi-launch.test.ts +202 -0
  43. package/src/__tests__/registry.test.ts +1580 -0
  44. package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
  45. package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
  46. package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
  47. package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
  48. package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
  49. package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
  50. package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
  51. package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
  52. package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
  53. package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
  54. package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
  55. package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
  56. package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
  57. package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
  58. package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
  59. package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
  60. package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
  61. package/src/__tests__/task-manager.test.ts +479 -0
  62. package/src/__tests__/windows-taskkill.test.ts +161 -0
  63. package/src/anthropic-attribution-path.ts +21 -0
  64. package/src/anthropic-attribution.ts +1983 -0
  65. package/src/attested-pi-run.ts +612 -0
  66. package/src/child-process.ts +55 -0
  67. package/src/common.ts +8 -0
  68. package/src/config.ts +292 -0
  69. package/src/context-parent-snapshot.ts +142 -0
  70. package/src/context-token-budget.ts +903 -0
  71. package/src/context-visible-conversation-v2.ts +551 -0
  72. package/src/delegate/artifacts.ts +487 -0
  73. package/src/delegate/budget.ts +415 -0
  74. package/src/delegate/hook-contract-evidence.json +18 -0
  75. package/src/delegate/hook-contract.ts +154 -0
  76. package/src/delegate/launch.ts +497 -0
  77. package/src/delegate/result-package.ts +459 -0
  78. package/src/delegate/runner.ts +449 -0
  79. package/src/delegate/seed.ts +423 -0
  80. package/src/delegate/types.ts +323 -0
  81. package/src/delegate-child-extension.ts +978 -0
  82. package/src/delegate-extension.ts +806 -0
  83. package/src/durable-fs.ts +386 -0
  84. package/src/extension-api.ts +548 -0
  85. package/src/fixtures/delegate-context-incident.json +17 -0
  86. package/src/fixtures/fusion-golden-bytes.json +310 -0
  87. package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
  88. package/src/fusion/artifacts.ts +967 -0
  89. package/src/fusion/budget.ts +1162 -0
  90. package/src/fusion/child-protocol.ts +305 -0
  91. package/src/fusion/claude-cache.ts +207 -0
  92. package/src/fusion/clean-context.ts +91 -0
  93. package/src/fusion/config.ts +449 -0
  94. package/src/fusion/context.ts +265 -0
  95. package/src/fusion/evaluation.ts +800 -0
  96. package/src/fusion/orchestrator.ts +1288 -0
  97. package/src/fusion/output-contract.ts +34 -0
  98. package/src/fusion/pi-child.ts +2373 -0
  99. package/src/fusion/prompts.ts +345 -0
  100. package/src/fusion/result-package.ts +959 -0
  101. package/src/fusion/source-policy.ts +257 -0
  102. package/src/fusion/types.ts +1139 -0
  103. package/src/fusion/web-fetch.ts +1060 -0
  104. package/src/fusion/workflows.ts +184 -0
  105. package/src/fusion-child-extension.ts +1052 -0
  106. package/src/fusion-extension.ts +1293 -0
  107. package/src/index.ts +295 -0
  108. package/src/pi-launch.ts +225 -0
  109. package/src/registry.ts +2424 -0
  110. package/src/settings-overlay.ts +208 -0
  111. package/src/task-manager.ts +774 -0
  112. package/src/tools.ts +530 -0
  113. package/src/turndown.d.ts +15 -0
  114. package/src/types.ts +963 -0
  115. package/src/ui/fusion-model-selector.ts +322 -0
  116. package/src/windows-taskkill.ts +250 -0
@@ -0,0 +1,140 @@
1
+ import { SessionManager } from '@earendil-works/pi-coding-agent';
2
+ import type { AssistantMessage, ToolResultMessage, UserMessage } from '@earendil-works/pi-ai';
3
+ import {
4
+ buildFusionCanonicalInput,
5
+ expandFusionProjectionEntry,
6
+ type BuildFusionCanonicalInputOptions,
7
+ type BuiltFusionCanonicalInput,
8
+ } from '../../fusion/context.js';
9
+ import type {
10
+ ProjectionEntry,
11
+ ProjectionOmissionEntry,
12
+ ProjectionTextEntry,
13
+ } from '../../context-visible-conversation-v2.js';
14
+ import type {
15
+ FusionCanonicalInputV3,
16
+ FusionContextOmissionLedgerV2,
17
+ FusionUsage,
18
+ } from '../../fusion/types.js';
19
+
20
+ export function testUsage(): FusionUsage {
21
+ return {
22
+ input: 1,
23
+ output: 1,
24
+ cacheRead: 0,
25
+ cacheWrite: 0,
26
+ totalTokens: 2,
27
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
28
+ };
29
+ }
30
+
31
+ export function userMessage(content: UserMessage['content'], timestamp = 1): UserMessage {
32
+ return { role: 'user', content, timestamp };
33
+ }
34
+
35
+ export function assistantMessage(
36
+ content: AssistantMessage['content'],
37
+ timestamp = 2,
38
+ ): AssistantMessage {
39
+ return {
40
+ role: 'assistant',
41
+ api: 'openai-codex-responses',
42
+ provider: 'openai-codex',
43
+ model: 'gpt-5.5',
44
+ usage: testUsage(),
45
+ stopReason: 'toolUse',
46
+ content,
47
+ timestamp,
48
+ };
49
+ }
50
+
51
+ export function toolResultMessage(
52
+ toolCallId: string,
53
+ toolName: string,
54
+ content: ToolResultMessage['content'],
55
+ timestamp = 3,
56
+ ): ToolResultMessage {
57
+ return {
58
+ role: 'toolResult',
59
+ toolCallId,
60
+ toolName,
61
+ content,
62
+ details: { ok: true },
63
+ isError: false,
64
+ timestamp,
65
+ };
66
+ }
67
+
68
+ export function sessionWith(messages: readonly (UserMessage | AssistantMessage | ToolResultMessage)[]) {
69
+ const session = SessionManager.inMemory('/tmp/project');
70
+ for (const message of messages) session.appendMessage(message);
71
+ return session;
72
+ }
73
+
74
+ export function buildFrom(
75
+ messages: readonly (UserMessage | AssistantMessage | ToolResultMessage)[],
76
+ options: BuildFusionCanonicalInputOptions,
77
+ systemPrompt = 'system',
78
+ ): BuiltFusionCanonicalInput {
79
+ const session = sessionWith(messages);
80
+ return buildFusionCanonicalInput(
81
+ { cwd: '/tmp/project', sessionManager: session, getSystemPrompt: () => systemPrompt },
82
+ options,
83
+ );
84
+ }
85
+
86
+ export type ExpandedFusionTextEntry = ProjectionTextEntry;
87
+ export type ExpandedFusionOmissionEntry = ProjectionOmissionEntry;
88
+ export type ExpandedFusionEntry = ProjectionEntry;
89
+
90
+ function assertProjection<T>(projection: T | undefined): asserts projection is T {
91
+ if (projection === undefined) throw new Error('expected session projection input');
92
+ }
93
+
94
+ export function expandedEntries(input: FusionCanonicalInputV3): readonly ExpandedFusionEntry[] {
95
+ const projection = input.context?.kind === 'session_projection'
96
+ ? input.context.conversation_projection
97
+ : 'conversation_projection' in input
98
+ ? input.conversation_projection
99
+ : undefined;
100
+ assertProjection(projection);
101
+ return projection.entries.map(expandFusionProjectionEntry);
102
+ }
103
+
104
+ export function textEntries(
105
+ input: FusionCanonicalInputV3,
106
+ ): readonly ExpandedFusionTextEntry[] {
107
+ return expandedEntries(input).filter(
108
+ (entry): entry is ExpandedFusionTextEntry => entry.kind === 'text',
109
+ );
110
+ }
111
+
112
+ export function omissionEntries(
113
+ input: FusionCanonicalInputV3,
114
+ ): readonly ExpandedFusionOmissionEntry[] {
115
+ return expandedEntries(input).filter(
116
+ (entry): entry is ExpandedFusionOmissionEntry => entry.kind === 'omitted_activity',
117
+ );
118
+ }
119
+
120
+ export function projectedText(input: FusionCanonicalInputV3): string {
121
+ return textEntries(input)
122
+ .map((entry) => entry.text)
123
+ .join('\n');
124
+ }
125
+
126
+ export function entryKinds(input: FusionCanonicalInputV3): readonly ExpandedFusionEntry['kind'][] {
127
+ return expandedEntries(input).map((entry) => entry.kind);
128
+ }
129
+
130
+ /** Minimal ledger for orchestrator tests that supply a hand-built canonical input. */
131
+ export function emptyLedger(policyId: string): FusionContextOmissionLedgerV2 {
132
+ return {
133
+ schema_version: 'pi-background-tasks.fusion-context-ledger.v2',
134
+ policy_id: policyId,
135
+ transform: 'visible-conversation-ledger-v2',
136
+ entries: [],
137
+ projection_map: [],
138
+ root_sha256: 'a'.repeat(64),
139
+ };
140
+ }
@@ -0,0 +1,279 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmod, mkdir, writeFile } from 'node:fs/promises';
3
+ import { delimiter, join } from 'node:path';
4
+
5
+ export interface FusionFakePiInstallOptions {
6
+ delegatePi?: string | undefined;
7
+ mergedText?: string | undefined;
8
+ delayMs?: number | undefined;
9
+ invalidFirstEvaluation?: boolean | undefined;
10
+ failStage?: 'candidate' | 'evaluation' | 'evaluation-repair' | 'merge' | undefined;
11
+ }
12
+
13
+ export interface FusionFakePiInstallResult {
14
+ binDir: string;
15
+ executable: string;
16
+ packageRoot: string;
17
+ packageJsonPath: string;
18
+ packageCliPath: string;
19
+ resolvePackageJson: (specifier: string) => string;
20
+ logPath: string;
21
+ env: NodeJS.ProcessEnv;
22
+ }
23
+
24
+ const PI_PACKAGE_NAME = '@earendil-works/pi-coding-agent';
25
+ const PI_PACKAGE_MANIFEST = `${PI_PACKAGE_NAME}/package.json`;
26
+
27
+ export function resolveRealPiCli(): string | undefined {
28
+ const result = spawnSync('bash', ['-lc', 'command -v pi'], { encoding: 'utf8' });
29
+ return result.status === 0 ? result.stdout.trim() || undefined : undefined;
30
+ }
31
+
32
+ function scriptBody(options: FusionFakePiInstallOptions, logPath: string): string {
33
+ const delegate = options.delegatePi ?? '';
34
+ const merged = options.mergedText ?? 'Fused fake answer.';
35
+ const delayMs = options.delayMs ?? 0;
36
+ const invalidFirstEvaluation = options.invalidFirstEvaluation ?? false;
37
+ const failStage = options.failStage ?? '';
38
+ return `const { appendFileSync, readFileSync, writeFileSync } = require('node:fs');
39
+ const { spawnSync } = require('node:child_process');
40
+ const { createHash } = require('node:crypto');
41
+ const args = process.argv.slice(2);
42
+ const logPath = ${JSON.stringify(logPath)};
43
+ const delegate = ${JSON.stringify(delegate)};
44
+ const mergedText = ${JSON.stringify(merged)};
45
+ const delayMs = ${JSON.stringify(delayMs)};
46
+ const invalidFirstEvaluation = ${JSON.stringify(invalidFirstEvaluation)};
47
+ const failStage = ${JSON.stringify(failStage)};
48
+ function argValue(name) {
49
+ const index = args.indexOf(name);
50
+ return index >= 0 ? args[index + 1] : undefined;
51
+ }
52
+ function hasFusionToolPolicy() {
53
+ // Reasoning-only children pass --no-tools. Tool-enabled children (inspect and each
54
+ // later capability) replace it with an explicit allowlist. Both are legitimate
55
+ // fusion children, so recognising only --no-tools would make a tool-enabled child
56
+ // fall through to the delegate path and fail as a non-fusion invocation.
57
+ if (args.includes('--no-tools')) return true;
58
+ return args.includes('--no-builtin-tools') && typeof argValue('--tools') === 'string';
59
+ }
60
+ function isFusionChild() {
61
+ const extension = argValue('--extension');
62
+ return argValue('--mode') === 'text' && hasFusionToolPolicy() && args.includes('--no-extensions') && args.includes('--no-context-files') && typeof extension === 'string' && /fusion-child\\.(?:ts|js)$/.test(extension);
63
+ }
64
+ if (!isFusionChild()) {
65
+ if (!delegate) {
66
+ console.error('fusion fake pi received a non-fusion invocation without a delegate');
67
+ process.exit(99);
68
+ }
69
+ const result = spawnSync(delegate, args, { stdio: 'inherit', env: process.env, cwd: process.cwd() });
70
+ if (result.error) {
71
+ console.error(result.error.message);
72
+ process.exit(98);
73
+ }
74
+ process.exit(result.status === null ? 97 : result.status);
75
+ }
76
+ // The real fusion child extension creates its tool-call log before tools can run,
77
+ // so an ABSENT file means the audit trail was never established rather than "zero tool
78
+ // calls". The parent enforces that distinction, so the fake must establish the file too
79
+ // or every tool-enabled child fails as a missing audit trail.
80
+ const toolCallLogPath = process.env.PI_FUSION_TOOL_CALL_LOG_PATH;
81
+ if (toolCallLogPath) appendFileSync(toolCallLogPath, '');
82
+ const provider = argValue('--provider') || 'fake-provider';
83
+ const model = argValue('--model') || 'fake-model';
84
+ const systemPrompt = argValue('--system-prompt') || '';
85
+ let stdin = '';
86
+ try {
87
+ stdin = readFileSync(0, 'utf8');
88
+ } catch (error) {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ console.error('fusion fake pi failed to read stdin: ' + message);
91
+ process.exit(96);
92
+ }
93
+ let stage = 'candidate';
94
+ if (systemPrompt.includes('invalid blind-evaluation JSON response')) stage = 'evaluation-repair';
95
+ else if (systemPrompt.includes('strict blind evaluator')) stage = 'evaluation';
96
+ else if (systemPrompt.includes('final synthesis process')) stage = 'merge';
97
+ const workflow = systemPrompt.includes('validation report') || systemPrompt.includes('validation reports') ? 'validate' : 'reason';
98
+ appendFileSync(logPath, JSON.stringify({ stage, workflow, provider, model, args, stdin, systemPrompt, cwd: process.cwd(), env: { PI_SESSION_ID: process.env.PI_SESSION_ID || null, PI_PROVIDER: process.env.PI_PROVIDER || null, PI_MODEL: process.env.PI_MODEL || null, PI_SKIP_VERSION_CHECK: process.env.PI_SKIP_VERSION_CHECK || null } }) + '\\n');
99
+ if (failStage && stage === failStage) {
100
+ console.error('fusion fake pi failing requested stage ' + stage);
101
+ process.exit(42);
102
+ }
103
+ function sourceFindingsFromStdin() {
104
+ try {
105
+ const parsed = JSON.parse(stdin);
106
+ if (!Array.isArray(parsed.candidates)) return [];
107
+ return parsed.candidates.flatMap((candidate) => {
108
+ const report = JSON.parse(candidate.response);
109
+ return (report.findings || []).map((finding, index) => ({ id: candidate.candidate_id + '-F' + String(index + 1).padStart(3, '0'), candidate_id: candidate.candidate_id, ...finding }));
110
+ });
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
115
+ function evaluationText() {
116
+ const sourceFindings = workflow === 'validate' ? sourceFindingsFromStdin() : [];
117
+ const base = {
118
+ schema_version: 'pi-background-tasks.fusion-evaluation.v1',
119
+ candidate_assessments: [
120
+ { candidate_id: 'A', summary: 'A summary', strengths: ['A strength'], limitations: ['A limitation'], useful_contributions: ['A contribution'], risks: ['A risk'] },
121
+ { candidate_id: 'B', summary: 'B summary', strengths: ['B strength'], limitations: ['B limitation'], useful_contributions: ['B contribution'], risks: ['B risk'] },
122
+ { candidate_id: 'C', summary: 'C summary', strengths: ['C strength'], limitations: ['C limitation'], useful_contributions: ['C contribution'], risks: ['C risk'] }
123
+ ],
124
+ agreements: ['All address the request'],
125
+ conflicts: [{ topic: 'detail', positions: [{ candidate_id: 'A', position: 'A position' }, { candidate_id: 'B', position: 'B position' }], resolution: 'Combine the useful detail' }],
126
+ synthesis_plan: { must_include: [{ candidate_id: 'A', contribution: 'A contribution' }], must_resolve: ['detail'], must_avoid: ['unsupported claims'] }
127
+ };
128
+ if (workflow === 'validate') {
129
+ base.validation_accounting = {
130
+ findings: sourceFindings,
131
+ decisions: sourceFindings.map((finding, index) => ({ source_id: finding.id, disposition: 'include', rationale: 'preserved by fake evaluator', group_id: 'G' + String(index + 1).padStart(3, '0') })),
132
+ groups: sourceFindings.map((finding, index) => ({
133
+ group_id: 'G' + String(index + 1).padStart(3, '0'),
134
+ source_ids: [finding.id],
135
+ severity: finding.severity,
136
+ location: finding.location,
137
+ evidence: finding.evidence,
138
+ impact: finding.impact,
139
+ summary: finding.summary,
140
+ rationale: 'resolved by fake evaluator'
141
+ }))
142
+ };
143
+ }
144
+ return JSON.stringify(base);
145
+ }
146
+ function validationCandidateText() {
147
+ return JSON.stringify({
148
+ schema_version: 'pi-background-tasks.fusion-validation-candidate.v1',
149
+ findings: [{ severity: 'minor', location: 'README.md:1', evidence: 'fake evidence', impact: 'fake impact', summary: 'fake finding' }],
150
+ verified: ['fake verification'],
151
+ limitations: ['fake limitation']
152
+ });
153
+ }
154
+ function responseText() {
155
+ if (invalidFirstEvaluation && stage === 'evaluation') return JSON.stringify({ schema_version: 'pi-background-tasks.fusion-evaluation.v1', bad: true });
156
+ if (stage === 'evaluation' || stage === 'evaluation-repair') return evaluationText();
157
+ if (stage === 'merge') return mergedText;
158
+ if (workflow === 'validate') return validationCandidateText();
159
+ return 'Candidate fake answer from ' + provider + '/' + model + '.';
160
+ }
161
+ function emit() {
162
+ if (toolCallLogPath) {
163
+ const logBytes = readFileSync(toolCallLogPath);
164
+ const seal = {
165
+ schema_version: 'pi-background-tasks.fusion-tool-call-seal.v1',
166
+ status: 'complete',
167
+ record_count: 0,
168
+ total_result_bytes: 0,
169
+ log_sha256: createHash('sha256').update(logBytes).digest('hex')
170
+ };
171
+ writeFileSync(toolCallLogPath + '.seal.json', JSON.stringify(seal) + '\\n', { flag: 'wx', mode: 0o600 });
172
+ }
173
+ const text = responseText();
174
+ const digest = createHash('sha256').update(text, 'utf8').digest('hex');
175
+ const cacheObservation = provider === 'anthropic'
176
+ ? {
177
+ schema_version: 'pi-background-tasks.fusion-claude-cache-observation.v1',
178
+ applicability: 'anthropic',
179
+ source: 'default',
180
+ requested_retention: 'long',
181
+ effective_retention: 'long',
182
+ breakpoint_count: 3,
183
+ request_ordinal: 1
184
+ }
185
+ : {
186
+ schema_version: 'pi-background-tasks.fusion-claude-cache-observation.v1',
187
+ applicability: 'not_applicable',
188
+ source: 'not_applicable',
189
+ requested_retention: null,
190
+ effective_retention: null,
191
+ breakpoint_count: 0,
192
+ request_ordinal: 1
193
+ };
194
+ const metadata = {
195
+ schema_version: 'pi-background-tasks.fusion-child-result.v4',
196
+ provider,
197
+ model,
198
+ stop_reason: 'stop',
199
+ text_blocks: [{ utf8_bytes: Buffer.byteLength(text, 'utf8'), sha256: digest }],
200
+ text_sha256: digest,
201
+ usage: { input: 11, output: 7, cacheRead: 2, cacheWrite: 3, totalTokens: 23, cost: { input: 0.001, output: 0.002, cacheRead: 0.003, cacheWrite: 0.004, total: 0.01 } },
202
+ cache_observation: cacheObservation,
203
+ output_contract: {
204
+ json_rendered_bytes: Buffer.byteLength(JSON.stringify(text), 'utf8'),
205
+ candidate_limit_bytes: stage === 'candidate' ? 49152 : null,
206
+ recovery_role: 'none'
207
+ }
208
+ };
209
+ const eventBytes = Buffer.from(JSON.stringify(metadata) + '\\n', 'utf8');
210
+ const settlement = {
211
+ schema_version: 'pi-background-tasks.fusion-child-settlement.v3',
212
+ status: 'complete',
213
+ record_count: 1,
214
+ records_sha256: createHash('sha256').update(eventBytes).digest('hex'),
215
+ final_record_index: 0,
216
+ final_text_sha256: digest,
217
+ recovered_error_ordinals: [],
218
+ recovered_output_cap_ordinals: [],
219
+ failure_reason: null
220
+ };
221
+ process.stderr.write('\\x1ePI_FUSION_CHILD_RESULT ' + JSON.stringify(metadata) + '\\n');
222
+ process.stderr.write('\\x1ePI_FUSION_CHILD_SETTLEMENT ' + JSON.stringify(settlement) + '\\n');
223
+ process.stdout.write(text + '\\n');
224
+ }
225
+ if (delayMs > 0) setTimeout(emit, delayMs);
226
+ else emit();
227
+ `;
228
+ }
229
+
230
+ function executableScript(options: FusionFakePiInstallOptions, logPath: string): string {
231
+ return `#!/usr/bin/env node\n${scriptBody(options, logPath)}`;
232
+ }
233
+
234
+ export async function installFusionFakePi(
235
+ root: string,
236
+ options: FusionFakePiInstallOptions = {},
237
+ ): Promise<FusionFakePiInstallResult> {
238
+ const binDir = join(root, 'bin');
239
+ const packageRoot = join(root, 'fake-pi-pkg');
240
+ const packageDist = join(packageRoot, 'dist');
241
+ await mkdir(binDir, { recursive: true });
242
+ await mkdir(packageDist, { recursive: true });
243
+ const executable = join(binDir, 'pi');
244
+ const packageJsonPath = join(packageRoot, 'package.json');
245
+ const packageCliPath = join(packageDist, 'cli.cjs');
246
+ const logPath = join(root, 'fusion-fake-pi.jsonl');
247
+ const body = scriptBody(options, logPath);
248
+ await writeFile(executable, executableScript(options, logPath), 'utf8');
249
+ await chmod(executable, 0o755);
250
+ await writeFile(
251
+ packageJsonPath,
252
+ `${JSON.stringify(
253
+ { name: PI_PACKAGE_NAME, version: '0.0.0-test', bin: { pi: 'dist/cli.cjs' } },
254
+ null,
255
+ 2,
256
+ )}\n`,
257
+ 'utf8',
258
+ );
259
+ await writeFile(packageCliPath, body, 'utf8');
260
+ await chmod(packageCliPath, 0o755);
261
+ return {
262
+ binDir,
263
+ executable,
264
+ packageRoot,
265
+ packageJsonPath,
266
+ packageCliPath,
267
+ resolvePackageJson: (specifier: string) => {
268
+ if (specifier !== PI_PACKAGE_MANIFEST) {
269
+ throw new Error(`unexpected package manifest specifier: ${specifier}`);
270
+ }
271
+ return packageJsonPath;
272
+ },
273
+ logPath,
274
+ env: {
275
+ ...process.env,
276
+ PATH: `${binDir}${delimiter}${process.env['PATH'] ?? ''}`,
277
+ },
278
+ };
279
+ }