@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2
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.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import { object, parseJson, errorCode } from '../../collab/validation.js';
|
|
2
|
+
import { isPiRecord } from './protocol.js';
|
|
3
|
+
function isPiMessage(value) {
|
|
4
|
+
if (!object(value))
|
|
5
|
+
return false;
|
|
6
|
+
for (const key of ['role', 'stopReason', 'toolCallId'])
|
|
7
|
+
if (value[key] !== undefined && typeof value[key] !== 'string')
|
|
8
|
+
return false;
|
|
9
|
+
if (value['isError'] !== undefined && typeof value['isError'] !== 'boolean')
|
|
10
|
+
return false;
|
|
11
|
+
const content = value['content'];
|
|
12
|
+
return (content === undefined ||
|
|
13
|
+
typeof content === 'string' ||
|
|
14
|
+
(Array.isArray(content) &&
|
|
15
|
+
content.every((part) => object(part) &&
|
|
16
|
+
['type', 'text', 'id'].every((key) => part[key] === undefined || typeof part[key] === 'string'))));
|
|
17
|
+
}
|
|
18
|
+
function isReceipt(value) {
|
|
19
|
+
return (object(value) &&
|
|
20
|
+
['schema', 'task_id', 'assignment_id', 'input_digest', 'status'].every((key) => typeof value[key] === 'string') &&
|
|
21
|
+
typeof value['attempt'] === 'number' &&
|
|
22
|
+
Array.isArray(value['outputs']) &&
|
|
23
|
+
value['outputs'].every((item) => object(item) &&
|
|
24
|
+
typeof item['ref'] === 'string' &&
|
|
25
|
+
typeof item['digest'] === 'string'));
|
|
26
|
+
}
|
|
27
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
28
|
+
import { spawn } from 'node:child_process';
|
|
29
|
+
import fs from 'node:fs';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { canonicalJson } from '../../collab/envelope.js';
|
|
32
|
+
// Read-only recovery of a completed native Pi execution whose REPORT notification was lost.
|
|
33
|
+
//
|
|
34
|
+
// The exact original session file is reopened through `pi --mode rpc` and only
|
|
35
|
+
// get_state, get_messages and get_session_stats are sent. No command in this file can
|
|
36
|
+
// start model work, run a tool, or change the session. This is NOT a query against an
|
|
37
|
+
// already-live Pi process and NOT a read of the shared task record; see docs/a09-native-query.md.
|
|
38
|
+
//
|
|
39
|
+
// Protocol facts used (Pi 1.3.0 docs/rpc.md): strict JSONL framed on LF only, every
|
|
40
|
+
// command carries an `id` that its `response` repeats, stdout carries events that are not
|
|
41
|
+
// responses, and closing stdin requests an orderly shutdown.
|
|
42
|
+
const SCHEMA = 'harness-collab/pi-native-query/1';
|
|
43
|
+
const RECEIPT_SCHEMA = 'harness-collab/native-result/1';
|
|
44
|
+
const RECEIPT_KEYS = [
|
|
45
|
+
'assignment_id',
|
|
46
|
+
'attempt',
|
|
47
|
+
'input_digest',
|
|
48
|
+
'outputs',
|
|
49
|
+
'schema',
|
|
50
|
+
'status',
|
|
51
|
+
'task_id',
|
|
52
|
+
];
|
|
53
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
54
|
+
const MAX_TIMEOUT_MS = 30000;
|
|
55
|
+
const DEFAULT_GRACE_MS = 2000;
|
|
56
|
+
const STDERR_KEEP = 64 * 1024;
|
|
57
|
+
const SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
58
|
+
class Refusal extends Error {
|
|
59
|
+
status;
|
|
60
|
+
code;
|
|
61
|
+
constructor(status, code) {
|
|
62
|
+
super(code);
|
|
63
|
+
this.status = status;
|
|
64
|
+
this.code = code;
|
|
65
|
+
this.status = status;
|
|
66
|
+
this.code = code;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function refuse(code, status = 'invalid') {
|
|
70
|
+
throw new Refusal(status, code);
|
|
71
|
+
}
|
|
72
|
+
const sha256 = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
73
|
+
const isObject = object;
|
|
74
|
+
const realpath = (file) => {
|
|
75
|
+
try {
|
|
76
|
+
return fs.realpathSync(file);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const samePath = (left, right) => {
|
|
83
|
+
const a = realpath(left);
|
|
84
|
+
return a !== null && a === realpath(right);
|
|
85
|
+
};
|
|
86
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
87
|
+
function normalize(options) {
|
|
88
|
+
const absolute = (value, code) => {
|
|
89
|
+
if (typeof value !== 'string' ||
|
|
90
|
+
!path.isAbsolute(value) ||
|
|
91
|
+
value.includes('\0'))
|
|
92
|
+
refuse(code);
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
const text = (value, code) => {
|
|
96
|
+
if (typeof value !== 'string' || !value.trim())
|
|
97
|
+
refuse(code);
|
|
98
|
+
return value;
|
|
99
|
+
};
|
|
100
|
+
const timeoutMs = options.timeoutMs ?? MAX_TIMEOUT_MS;
|
|
101
|
+
if (!Number.isSafeInteger(timeoutMs) ||
|
|
102
|
+
timeoutMs < 1 ||
|
|
103
|
+
timeoutMs > MAX_TIMEOUT_MS)
|
|
104
|
+
refuse('timeout_out_of_range');
|
|
105
|
+
if (!Number.isSafeInteger(options.attempt) || options.attempt < 1)
|
|
106
|
+
refuse('invalid_attempt');
|
|
107
|
+
if (typeof options.inputDigest !== 'string' ||
|
|
108
|
+
!SHA256.test(options.inputDigest))
|
|
109
|
+
refuse('invalid_input_digest');
|
|
110
|
+
return {
|
|
111
|
+
session: absolute(options.session, 'session_must_be_absolute'),
|
|
112
|
+
cwd: absolute(options.cwd, 'cwd_must_be_absolute'),
|
|
113
|
+
expectedSession: text(options.expectedSession, 'expected_session_required'),
|
|
114
|
+
taskId: text(options.taskId, 'task_id_required'),
|
|
115
|
+
assignmentId: text(options.assignmentId, 'assignment_id_required'),
|
|
116
|
+
attempt: options.attempt,
|
|
117
|
+
inputDigest: options.inputDigest,
|
|
118
|
+
output: options.output === undefined
|
|
119
|
+
? null
|
|
120
|
+
: absolute(options.output, 'output_must_be_absolute'),
|
|
121
|
+
evidenceDir: options.evidenceDir === undefined
|
|
122
|
+
? null
|
|
123
|
+
: absolute(options.evidenceDir, 'evidence_dir_must_be_absolute'),
|
|
124
|
+
piBin: options.piBin ?? 'pi',
|
|
125
|
+
timeoutMs,
|
|
126
|
+
graceMs: options.graceMs ?? DEFAULT_GRACE_MS,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function hashFile(file) {
|
|
130
|
+
const hash = createHash('sha256');
|
|
131
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
132
|
+
const fd = fs.openSync(file, 'r');
|
|
133
|
+
let bytes = 0;
|
|
134
|
+
try {
|
|
135
|
+
for (;;) {
|
|
136
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
137
|
+
if (!count)
|
|
138
|
+
break;
|
|
139
|
+
hash.update(buffer.subarray(0, count));
|
|
140
|
+
bytes += count;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
fs.closeSync(fd);
|
|
145
|
+
}
|
|
146
|
+
return { digest: `sha256:${hash.digest('hex')}`, bytes };
|
|
147
|
+
}
|
|
148
|
+
function readHeader(file) {
|
|
149
|
+
let line;
|
|
150
|
+
try {
|
|
151
|
+
if (!fs.statSync(file).isFile())
|
|
152
|
+
refuse('session_not_a_file');
|
|
153
|
+
const fd = fs.openSync(file, 'r');
|
|
154
|
+
try {
|
|
155
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
156
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
157
|
+
const end = buffer.subarray(0, count).indexOf(10);
|
|
158
|
+
line = buffer.subarray(0, end === -1 ? count : end).toString('utf8');
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
fs.closeSync(fd);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof Refusal)
|
|
166
|
+
throw error;
|
|
167
|
+
refuse('session_unreadable');
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const header = parseJson(line);
|
|
171
|
+
if (isObject(header) &&
|
|
172
|
+
header.type === 'session' &&
|
|
173
|
+
typeof header.id === 'string' &&
|
|
174
|
+
typeof header.cwd === 'string')
|
|
175
|
+
return {
|
|
176
|
+
...header,
|
|
177
|
+
id: header.id,
|
|
178
|
+
cwd: header.cwd,
|
|
179
|
+
version: header['version'],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* falls through to the refusal below */
|
|
184
|
+
}
|
|
185
|
+
return refuse('session_header_invalid');
|
|
186
|
+
}
|
|
187
|
+
// One child process, strictly sequential requests, bounded by one deadline and one byte cap.
|
|
188
|
+
function startRpc(opts, captured) {
|
|
189
|
+
const args = [
|
|
190
|
+
'--mode',
|
|
191
|
+
'rpc',
|
|
192
|
+
'--session',
|
|
193
|
+
opts.session,
|
|
194
|
+
'--session-dir',
|
|
195
|
+
path.dirname(opts.session),
|
|
196
|
+
'--no-mcp',
|
|
197
|
+
'--no-tools',
|
|
198
|
+
];
|
|
199
|
+
const nonce = randomBytes(6).toString('hex');
|
|
200
|
+
const child = spawn(opts.piBin, args, {
|
|
201
|
+
cwd: opts.cwd,
|
|
202
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
203
|
+
});
|
|
204
|
+
const state = {
|
|
205
|
+
lines: [],
|
|
206
|
+
partial: [],
|
|
207
|
+
total: 0,
|
|
208
|
+
failure: null,
|
|
209
|
+
exit: null,
|
|
210
|
+
spawnFailed: false,
|
|
211
|
+
waiter: null,
|
|
212
|
+
};
|
|
213
|
+
let sequence = 0;
|
|
214
|
+
let killedAfterEof = false;
|
|
215
|
+
const wake = () => {
|
|
216
|
+
const waiter = state.waiter;
|
|
217
|
+
state.waiter = null;
|
|
218
|
+
waiter?.();
|
|
219
|
+
};
|
|
220
|
+
const fail = (status, code) => {
|
|
221
|
+
state.failure ??= new Refusal(status, code);
|
|
222
|
+
wake();
|
|
223
|
+
};
|
|
224
|
+
const closed = new Promise((resolve) => {
|
|
225
|
+
child.once('close', (code, signal) => {
|
|
226
|
+
state.exit = { code, signal };
|
|
227
|
+
resolve();
|
|
228
|
+
wake();
|
|
229
|
+
});
|
|
230
|
+
child.once('error', () => {
|
|
231
|
+
state.spawnFailed = true;
|
|
232
|
+
fail('needs_input', 'spawn_failed');
|
|
233
|
+
resolve();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
// A broken pipe means the child is gone; the exit path below reports why.
|
|
237
|
+
child.stdin.on('error', () => { });
|
|
238
|
+
child.stdout.on('data', (chunk) => {
|
|
239
|
+
state.total += chunk.length;
|
|
240
|
+
if (state.total > MAX_BYTES) {
|
|
241
|
+
fail('invalid', 'response_too_large');
|
|
242
|
+
child.kill('SIGKILL');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Split on LF bytes only; U+2028/U+2029 are legal inside JSON strings.
|
|
246
|
+
let start = 0;
|
|
247
|
+
for (let index = chunk.indexOf(10, start); index !== -1; index = chunk.indexOf(10, start)) {
|
|
248
|
+
state.partial.push(chunk.subarray(start, index));
|
|
249
|
+
state.lines.push(Buffer.concat(state.partial));
|
|
250
|
+
state.partial = [];
|
|
251
|
+
start = index + 1;
|
|
252
|
+
}
|
|
253
|
+
if (start < chunk.length)
|
|
254
|
+
state.partial.push(chunk.subarray(start));
|
|
255
|
+
wake();
|
|
256
|
+
});
|
|
257
|
+
const stderr = [];
|
|
258
|
+
let stderrBytes = 0;
|
|
259
|
+
child.stderr.on('data', (chunk) => {
|
|
260
|
+
if (stderrBytes < STDERR_KEEP)
|
|
261
|
+
stderr.push(chunk.subarray(0, STDERR_KEEP - stderrBytes));
|
|
262
|
+
stderrBytes += chunk.length;
|
|
263
|
+
});
|
|
264
|
+
const timer = setTimeout(() => {
|
|
265
|
+
fail('needs_input', 'timeout');
|
|
266
|
+
child.kill('SIGKILL');
|
|
267
|
+
}, opts.timeoutMs);
|
|
268
|
+
async function request(type) {
|
|
269
|
+
const id = `pi-query-${nonce}-${sequence++}`;
|
|
270
|
+
captured.commands.push(type);
|
|
271
|
+
if (!state.failure && state.exit === null)
|
|
272
|
+
child.stdin.write(`${JSON.stringify({ id, type })}\n`);
|
|
273
|
+
for (;;) {
|
|
274
|
+
if (state.failure)
|
|
275
|
+
throw state.failure;
|
|
276
|
+
const line = state.lines.shift();
|
|
277
|
+
if (line) {
|
|
278
|
+
const response = parseLine(line, id, type);
|
|
279
|
+
if (response) {
|
|
280
|
+
captured.raw.push(line);
|
|
281
|
+
return response;
|
|
282
|
+
}
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (state.exit !== null) {
|
|
286
|
+
if (state.partial.length)
|
|
287
|
+
refuse('incomplete_framing');
|
|
288
|
+
refuse('rpc_exited_early', 'needs_input');
|
|
289
|
+
}
|
|
290
|
+
await new Promise((resolve) => {
|
|
291
|
+
state.waiter = resolve;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function close() {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
if (state.exit === null && !state.spawnFailed) {
|
|
298
|
+
try {
|
|
299
|
+
child.stdin.end();
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
/* already closed */
|
|
303
|
+
}
|
|
304
|
+
const exited = await Promise.race([
|
|
305
|
+
closed.then(() => true),
|
|
306
|
+
delay(opts.graceMs).then(() => false),
|
|
307
|
+
]);
|
|
308
|
+
if (!exited) {
|
|
309
|
+
killedAfterEof = true;
|
|
310
|
+
child.kill('SIGKILL');
|
|
311
|
+
await closed;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
captured.stderr = Buffer.concat(stderr);
|
|
315
|
+
return {
|
|
316
|
+
exit_code: state.exit?.code ?? null,
|
|
317
|
+
signal: state.exit?.signal ?? null,
|
|
318
|
+
killed_after_eof_timeout: killedAfterEof,
|
|
319
|
+
stderr_bytes: stderrBytes,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return { request, close };
|
|
323
|
+
}
|
|
324
|
+
// Returns the matching response, or null for an event record that is not a response.
|
|
325
|
+
function parseLine(line, id, type) {
|
|
326
|
+
const text = line.toString('utf8').replace(/\r$/, '');
|
|
327
|
+
if (!text.length)
|
|
328
|
+
return null;
|
|
329
|
+
let record;
|
|
330
|
+
try {
|
|
331
|
+
record = parseJson(text);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return refuse('malformed_record');
|
|
335
|
+
}
|
|
336
|
+
if (!isPiRecord(record))
|
|
337
|
+
refuse('malformed_record');
|
|
338
|
+
if (record.type !== 'response')
|
|
339
|
+
return null;
|
|
340
|
+
if (record.id !== id)
|
|
341
|
+
refuse(record.command === 'parse' ? 'rpc_parse_error' : 'unexpected_response_id');
|
|
342
|
+
if (record.command !== type)
|
|
343
|
+
refuse('response_command_mismatch');
|
|
344
|
+
if (record.success !== true)
|
|
345
|
+
refuse('native_command_failed');
|
|
346
|
+
return record;
|
|
347
|
+
}
|
|
348
|
+
function checkState(response, opts) {
|
|
349
|
+
const data = response.data;
|
|
350
|
+
if (!isObject(data))
|
|
351
|
+
refuse('state_malformed');
|
|
352
|
+
if (data.sessionId !== opts.expectedSession)
|
|
353
|
+
refuse('session_id_mismatch');
|
|
354
|
+
if (typeof data.sessionFile !== 'string' ||
|
|
355
|
+
!samePath(data.sessionFile, opts.session))
|
|
356
|
+
refuse('session_file_mismatch');
|
|
357
|
+
if (typeof data.isStreaming !== 'boolean' ||
|
|
358
|
+
typeof data.isCompacting !== 'boolean' ||
|
|
359
|
+
!Number.isSafeInteger(data.pendingMessageCount))
|
|
360
|
+
refuse('state_incomplete');
|
|
361
|
+
if (data.isStreaming || data.isCompacting || data.pendingMessageCount !== 0)
|
|
362
|
+
refuse('native_not_settled', 'needs_input');
|
|
363
|
+
return {
|
|
364
|
+
is_streaming: data.isStreaming,
|
|
365
|
+
is_compacting: data.isCompacting,
|
|
366
|
+
pending_message_count: data.pendingMessageCount,
|
|
367
|
+
message_count: Number.isSafeInteger(data.messageCount)
|
|
368
|
+
? data.messageCount
|
|
369
|
+
: null,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function checkStats(response, opts) {
|
|
373
|
+
const data = response.data;
|
|
374
|
+
if (!isObject(data))
|
|
375
|
+
refuse('stats_malformed');
|
|
376
|
+
if (data.sessionId !== opts.expectedSession)
|
|
377
|
+
refuse('session_id_mismatch');
|
|
378
|
+
if (typeof data.sessionFile !== 'string' ||
|
|
379
|
+
!samePath(data.sessionFile, opts.session))
|
|
380
|
+
refuse('session_file_mismatch');
|
|
381
|
+
const count = (key) => Number.isSafeInteger(data[key]) ? data[key] : null;
|
|
382
|
+
return {
|
|
383
|
+
user_messages: count('userMessages'),
|
|
384
|
+
assistant_messages: count('assistantMessages'),
|
|
385
|
+
tool_calls: count('toolCalls'),
|
|
386
|
+
tool_results: count('toolResults'),
|
|
387
|
+
total_messages: count('totalMessages'),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function assistantText(message) {
|
|
391
|
+
const parts = typeof message.content === 'string'
|
|
392
|
+
? [{ type: 'text', text: message.content }]
|
|
393
|
+
: message.content;
|
|
394
|
+
if (!Array.isArray(parts))
|
|
395
|
+
return null;
|
|
396
|
+
let text = '';
|
|
397
|
+
for (const part of parts) {
|
|
398
|
+
if (part?.type === 'thinking')
|
|
399
|
+
continue;
|
|
400
|
+
if (part?.type !== 'text' || typeof part.text !== 'string')
|
|
401
|
+
return null;
|
|
402
|
+
text += part.text;
|
|
403
|
+
}
|
|
404
|
+
return text.trim();
|
|
405
|
+
}
|
|
406
|
+
// Finds the one terminal assistant receipt that matches the exact assignment.
|
|
407
|
+
function findReceipt(messages, opts) {
|
|
408
|
+
if (!Array.isArray(messages) || !messages.every(isPiMessage))
|
|
409
|
+
refuse('messages_malformed');
|
|
410
|
+
// Every tool call in the session must have a successful result: a failed or missing
|
|
411
|
+
// tool result means the run's own record cannot be trusted as a completed execution.
|
|
412
|
+
const calls = new Set();
|
|
413
|
+
const results = new Map();
|
|
414
|
+
for (const message of messages) {
|
|
415
|
+
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
|
416
|
+
for (const part of message.content) {
|
|
417
|
+
if (part?.type !== 'toolCall')
|
|
418
|
+
continue;
|
|
419
|
+
if (typeof part.id !== 'string' || calls.has(part.id))
|
|
420
|
+
refuse('tool_call_malformed');
|
|
421
|
+
calls.add(part.id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else if (message.role === 'toolResult') {
|
|
425
|
+
if (typeof message.toolCallId !== 'string' ||
|
|
426
|
+
typeof message.isError !== 'boolean' ||
|
|
427
|
+
results.has(message.toolCallId))
|
|
428
|
+
refuse('tool_result_malformed');
|
|
429
|
+
results.set(message.toolCallId, message.isError);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const id of calls)
|
|
433
|
+
if (results.get(id) !== false)
|
|
434
|
+
refuse('tool_call_unsuccessful');
|
|
435
|
+
let terminal = -1;
|
|
436
|
+
messages.forEach((message, index) => {
|
|
437
|
+
if (message.role === 'assistant')
|
|
438
|
+
terminal = index;
|
|
439
|
+
});
|
|
440
|
+
if (terminal < 0)
|
|
441
|
+
refuse('no_assistant_message', 'needs_input');
|
|
442
|
+
const stop = messages[terminal].stopReason;
|
|
443
|
+
if (stop === 'error' || stop === 'aborted')
|
|
444
|
+
refuse('terminal_turn_failed');
|
|
445
|
+
if (stop !== 'stop')
|
|
446
|
+
refuse('terminal_turn_not_complete', 'needs_input');
|
|
447
|
+
if (terminal !== messages.length - 1)
|
|
448
|
+
refuse('receipt_not_terminal');
|
|
449
|
+
// Only assistant messages that stopped normally can carry a receipt; user, tool and
|
|
450
|
+
// failed-assistant text that merely looks like one is ignored.
|
|
451
|
+
const related = [];
|
|
452
|
+
messages.forEach((message, index) => {
|
|
453
|
+
if (message.role !== 'assistant' || message.stopReason !== 'stop')
|
|
454
|
+
return;
|
|
455
|
+
const text = assistantText(message);
|
|
456
|
+
if (!text?.startsWith('{'))
|
|
457
|
+
return;
|
|
458
|
+
let receipt;
|
|
459
|
+
try {
|
|
460
|
+
receipt = parseJson(text);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (!isObject(receipt) || receipt.schema !== RECEIPT_SCHEMA)
|
|
466
|
+
return;
|
|
467
|
+
if (receipt.task_id === opts.taskId &&
|
|
468
|
+
receipt.assignment_id === opts.assignmentId &&
|
|
469
|
+
receipt.attempt === opts.attempt) {
|
|
470
|
+
related.push({ index, receipt, timestamp: message.timestamp ?? null });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
if (related.some((item) => item.receipt.input_digest !== opts.inputDigest))
|
|
474
|
+
refuse('input_digest_mismatch');
|
|
475
|
+
if (!related.length)
|
|
476
|
+
refuse('no_matching_receipt', 'needs_input');
|
|
477
|
+
if (related.length > 1)
|
|
478
|
+
refuse('ambiguous_receipt');
|
|
479
|
+
const match = related[0];
|
|
480
|
+
if (match.index !== terminal)
|
|
481
|
+
refuse('receipt_not_terminal');
|
|
482
|
+
const { receipt } = match;
|
|
483
|
+
if (Object.keys(receipt).sort().join() !== RECEIPT_KEYS.join() ||
|
|
484
|
+
receipt.status !== 'completed' ||
|
|
485
|
+
!Array.isArray(receipt.outputs) ||
|
|
486
|
+
receipt.outputs.some((item) => !isObject(item) ||
|
|
487
|
+
Object.keys(item).sort().join() !== 'digest,ref' ||
|
|
488
|
+
typeof item.ref !== 'string' ||
|
|
489
|
+
typeof item.digest !== 'string' ||
|
|
490
|
+
!SHA256.test(item.digest)))
|
|
491
|
+
refuse('receipt_shape_invalid');
|
|
492
|
+
if (!isReceipt(receipt))
|
|
493
|
+
refuse('receipt_shape_invalid');
|
|
494
|
+
return { ...match, receipt, receipt_digest: sha256(canonicalJson(receipt)) };
|
|
495
|
+
}
|
|
496
|
+
// Output files are only hashed. They are never opened for execution or interpreted.
|
|
497
|
+
function verifyOutputs(outputs, cwd) {
|
|
498
|
+
const root = realpath(cwd) ?? refuse('cwd_unavailable');
|
|
499
|
+
const seen = new Set();
|
|
500
|
+
return outputs.map(({ ref, digest }) => {
|
|
501
|
+
if (!path.isAbsolute(ref) || ref.includes('\0') || seen.has(ref))
|
|
502
|
+
refuse('output_ref_invalid');
|
|
503
|
+
seen.add(ref);
|
|
504
|
+
const real = realpath(ref);
|
|
505
|
+
if (real === null)
|
|
506
|
+
return refuse('output_missing');
|
|
507
|
+
const relative = path.relative(root, real);
|
|
508
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
509
|
+
refuse('output_escapes_cwd');
|
|
510
|
+
let hashed;
|
|
511
|
+
try {
|
|
512
|
+
if (!fs.statSync(real).isFile())
|
|
513
|
+
refuse('output_not_a_file');
|
|
514
|
+
hashed = hashFile(real);
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (error instanceof Refusal)
|
|
518
|
+
throw error;
|
|
519
|
+
return refuse('output_unreadable');
|
|
520
|
+
}
|
|
521
|
+
if (hashed.digest !== digest)
|
|
522
|
+
refuse('output_digest_mismatch');
|
|
523
|
+
return { ref, digest, bytes: hashed.bytes };
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Result and evidence targets must be new: a repeated query never overwrites an earlier one.
|
|
527
|
+
function precheckTargets(opts) {
|
|
528
|
+
for (const target of [opts.output, opts.evidenceDir]) {
|
|
529
|
+
if (target === null)
|
|
530
|
+
continue;
|
|
531
|
+
if (fs.lstatSync(target, { throwIfNoEntry: false }))
|
|
532
|
+
refuse('output_exists');
|
|
533
|
+
if (!fs
|
|
534
|
+
.statSync(path.dirname(target), { throwIfNoEntry: false })
|
|
535
|
+
?.isDirectory())
|
|
536
|
+
refuse('output_parent_missing');
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async function query(opts, provenance, captured) {
|
|
540
|
+
const header = readHeader(opts.session);
|
|
541
|
+
if (header.id !== opts.expectedSession)
|
|
542
|
+
refuse('session_id_mismatch');
|
|
543
|
+
if (!samePath(header.cwd, opts.cwd))
|
|
544
|
+
refuse('native_cwd_mismatch');
|
|
545
|
+
const before = hashFile(opts.session);
|
|
546
|
+
provenance.session = {
|
|
547
|
+
...provenance.session,
|
|
548
|
+
id: header.id,
|
|
549
|
+
cwd: header.cwd,
|
|
550
|
+
pi_version: header.version ?? null,
|
|
551
|
+
sha256_before: before.digest,
|
|
552
|
+
bytes_before: before.bytes,
|
|
553
|
+
};
|
|
554
|
+
const rpc = startRpc(opts, captured);
|
|
555
|
+
let refusal = null;
|
|
556
|
+
let messages = null;
|
|
557
|
+
try {
|
|
558
|
+
provenance.state = checkState(await rpc.request('get_state'), opts);
|
|
559
|
+
const response = await rpc.request('get_messages');
|
|
560
|
+
messages = response.data?.messages;
|
|
561
|
+
provenance.stats = checkStats(await rpc.request('get_session_stats'), opts);
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
refusal = error;
|
|
565
|
+
}
|
|
566
|
+
finally {
|
|
567
|
+
provenance.cleanup = await rpc.close();
|
|
568
|
+
}
|
|
569
|
+
const after = hashFile(opts.session);
|
|
570
|
+
provenance.session.sha256_after = after.digest;
|
|
571
|
+
if (after.digest !== before.digest)
|
|
572
|
+
refuse('native_session_mutated');
|
|
573
|
+
if (refusal)
|
|
574
|
+
throw refusal;
|
|
575
|
+
const match = findReceipt(messages, opts);
|
|
576
|
+
const outputs = verifyOutputs(match.receipt.outputs, opts.cwd);
|
|
577
|
+
if (!Array.isArray(messages))
|
|
578
|
+
refuse('messages_malformed');
|
|
579
|
+
provenance.message_count = messages.length;
|
|
580
|
+
provenance.receipt_message_index = match.index;
|
|
581
|
+
provenance.receipt_message_timestamp = match.timestamp;
|
|
582
|
+
const messageId = `msg-pirecover-${createHash('sha256')
|
|
583
|
+
.update(canonicalJson({
|
|
584
|
+
task_id: opts.taskId,
|
|
585
|
+
assignment_id: opts.assignmentId,
|
|
586
|
+
attempt: opts.attempt,
|
|
587
|
+
receipt_digest: match.receipt_digest,
|
|
588
|
+
}))
|
|
589
|
+
.digest('hex')
|
|
590
|
+
.slice(0, 40)}`;
|
|
591
|
+
return {
|
|
592
|
+
status: 'recovered',
|
|
593
|
+
blockers: [],
|
|
594
|
+
recovered: {
|
|
595
|
+
message_id: messageId,
|
|
596
|
+
receipt_digest: match.receipt_digest,
|
|
597
|
+
outputs,
|
|
598
|
+
// Body for a REPORT sent through submitMessage; sender, recipient and subtask are the coordinator's to set.
|
|
599
|
+
report_body: {
|
|
600
|
+
status: 'completed',
|
|
601
|
+
input_revision: opts.inputDigest,
|
|
602
|
+
outputs: match.receipt.outputs.map(({ ref, digest }) => ({
|
|
603
|
+
ref,
|
|
604
|
+
digest,
|
|
605
|
+
})),
|
|
606
|
+
native_ref: {
|
|
607
|
+
runner: 'pi',
|
|
608
|
+
session_id: header.id,
|
|
609
|
+
session_file: opts.session,
|
|
610
|
+
session_sha256: before.digest,
|
|
611
|
+
message_index: match.index,
|
|
612
|
+
receipt_digest: match.receipt_digest,
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function persist(opts, result, captured) {
|
|
619
|
+
try {
|
|
620
|
+
if (opts.evidenceDir) {
|
|
621
|
+
fs.mkdirSync(opts.evidenceDir, { mode: 0o700 });
|
|
622
|
+
const files = [];
|
|
623
|
+
const save = (name, bytes) => {
|
|
624
|
+
const file = path.join(opts.evidenceDir, name);
|
|
625
|
+
fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
|
|
626
|
+
files.push({ ref: file, digest: sha256(bytes) });
|
|
627
|
+
};
|
|
628
|
+
if (captured.raw.length)
|
|
629
|
+
save('rpc-responses.jsonl', Buffer.concat(captured.raw.flatMap((line) => [line, Buffer.from('\n')])));
|
|
630
|
+
if (captured.stderr?.length)
|
|
631
|
+
save('rpc-stderr.log', captured.stderr);
|
|
632
|
+
result.provenance.evidence = files;
|
|
633
|
+
}
|
|
634
|
+
if (opts.output)
|
|
635
|
+
fs.writeFileSync(opts.output, `${JSON.stringify(result, null, 2)}\n`, {
|
|
636
|
+
flag: 'wx',
|
|
637
|
+
mode: 0o600,
|
|
638
|
+
});
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
const { recovered: _recovered, ...rest } = result;
|
|
643
|
+
return {
|
|
644
|
+
...rest,
|
|
645
|
+
status: 'invalid',
|
|
646
|
+
blockers: ['cannot_write_evidence_or_output'],
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
export async function queryPiResult(options) {
|
|
651
|
+
let opts;
|
|
652
|
+
try {
|
|
653
|
+
opts = normalize(options);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
return {
|
|
657
|
+
schema: SCHEMA,
|
|
658
|
+
status: 'invalid',
|
|
659
|
+
blockers: [errorCode(error) ?? 'invalid_options'],
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const identity = {
|
|
663
|
+
task_id: opts.taskId,
|
|
664
|
+
assignment_id: opts.assignmentId,
|
|
665
|
+
attempt: opts.attempt,
|
|
666
|
+
input_digest: opts.inputDigest,
|
|
667
|
+
};
|
|
668
|
+
try {
|
|
669
|
+
precheckTargets(opts);
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
// A rejected destination is reported as is: nothing is spawned, created or persisted.
|
|
673
|
+
if (!(error instanceof Refusal))
|
|
674
|
+
throw error;
|
|
675
|
+
return {
|
|
676
|
+
schema: SCHEMA,
|
|
677
|
+
status: error.status,
|
|
678
|
+
blockers: [error.code],
|
|
679
|
+
...identity,
|
|
680
|
+
provenance: { source: 'pi-rpc-session-reopen', commands_sent: [] },
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const provenance = {
|
|
684
|
+
source: 'pi-rpc-session-reopen',
|
|
685
|
+
commands_sent: [],
|
|
686
|
+
session: { file: opts.session, expected_id: opts.expectedSession },
|
|
687
|
+
};
|
|
688
|
+
const captured = {
|
|
689
|
+
commands: provenance.commands_sent,
|
|
690
|
+
raw: [],
|
|
691
|
+
stderr: null,
|
|
692
|
+
};
|
|
693
|
+
let outcome;
|
|
694
|
+
try {
|
|
695
|
+
outcome = await query(opts, provenance, captured);
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
outcome =
|
|
699
|
+
error instanceof Refusal
|
|
700
|
+
? { status: error.status, blockers: [error.code] }
|
|
701
|
+
: { status: 'invalid', blockers: ['internal_error'] };
|
|
702
|
+
}
|
|
703
|
+
const result = {
|
|
704
|
+
schema: SCHEMA,
|
|
705
|
+
...outcome,
|
|
706
|
+
...identity,
|
|
707
|
+
provenance,
|
|
708
|
+
};
|
|
709
|
+
return persist(opts, result, captured);
|
|
710
|
+
}
|