@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.
Files changed (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/integrations/skills/pappra/SKILL.md +68 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +30 -0
  6. package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
  7. package/assets/integrations/skills/pappra/references/operations.md +14 -0
  8. package/assets/manifest.json +4 -0
  9. package/dist/app/collaboration.d.ts +170 -0
  10. package/dist/app/collaboration.js +1650 -0
  11. package/dist/app/doctor.d.ts +9 -0
  12. package/dist/app/doctor.js +1030 -0
  13. package/dist/app/init.d.ts +143 -0
  14. package/dist/app/init.js +1111 -0
  15. package/dist/cli/commands/code.d.ts +16 -0
  16. package/dist/cli/commands/code.js +127 -0
  17. package/dist/cli/commands/collaboration.d.ts +13 -0
  18. package/dist/cli/commands/collaboration.js +186 -0
  19. package/dist/cli/commands/continuity.d.ts +29 -0
  20. package/dist/cli/commands/continuity.js +133 -0
  21. package/dist/cli/commands/docs.d.ts +11 -0
  22. package/dist/cli/commands/docs.js +133 -0
  23. package/dist/cli/commands/doctor.d.ts +14 -0
  24. package/dist/cli/commands/doctor.js +57 -0
  25. package/dist/cli/commands/host.d.ts +13 -0
  26. package/dist/cli/commands/host.js +125 -0
  27. package/dist/cli/commands/init.d.ts +51 -0
  28. package/dist/cli/commands/init.js +668 -0
  29. package/dist/cli/main.d.ts +2 -0
  30. package/dist/cli/main.js +10 -0
  31. package/dist/cli/output.d.ts +4 -0
  32. package/dist/cli/output.js +1 -0
  33. package/dist/cli/run.d.ts +2 -0
  34. package/dist/cli/run.js +597 -0
  35. package/dist/code/budget.d.ts +14 -0
  36. package/dist/code/budget.js +39 -0
  37. package/dist/code/exec.d.ts +23 -0
  38. package/dist/code/exec.js +133 -0
  39. package/dist/code/identity.d.ts +13 -0
  40. package/dist/code/identity.js +214 -0
  41. package/dist/code/index.d.ts +6 -0
  42. package/dist/code/index.js +3 -0
  43. package/dist/code/providers/codegraph.d.ts +59 -0
  44. package/dist/code/providers/codegraph.js +567 -0
  45. package/dist/code/registration.d.ts +9 -0
  46. package/dist/code/registration.js +293 -0
  47. package/dist/code/service.d.ts +53 -0
  48. package/dist/code/service.js +633 -0
  49. package/dist/code/text.d.ts +3 -0
  50. package/dist/code/text.js +202 -0
  51. package/dist/collab/admission.d.ts +450 -0
  52. package/dist/collab/admission.js +797 -0
  53. package/dist/collab/canonical.d.ts +26 -0
  54. package/dist/collab/canonical.js +136 -0
  55. package/dist/collab/contracts.d.ts +73 -0
  56. package/dist/collab/contracts.js +76 -0
  57. package/dist/collab/envelope.d.ts +52 -0
  58. package/dist/collab/envelope.js +241 -0
  59. package/dist/collab/fingerprint.d.ts +7 -0
  60. package/dist/collab/fingerprint.js +58 -0
  61. package/dist/collab/handoff.d.ts +350 -0
  62. package/dist/collab/handoff.js +513 -0
  63. package/dist/collab/index.d.ts +9 -0
  64. package/dist/collab/index.js +8 -0
  65. package/dist/collab/lifecycle.d.ts +449 -0
  66. package/dist/collab/lifecycle.js +534 -0
  67. package/dist/collab/messages.d.ts +148 -0
  68. package/dist/collab/messages.js +583 -0
  69. package/dist/collab/notify.d.ts +47 -0
  70. package/dist/collab/notify.js +140 -0
  71. package/dist/collab/preflight.d.ts +31 -0
  72. package/dist/collab/preflight.js +271 -0
  73. package/dist/collab/resources.d.ts +119 -0
  74. package/dist/collab/resources.js +451 -0
  75. package/dist/collab/store.d.ts +143 -0
  76. package/dist/collab/store.js +606 -0
  77. package/dist/collab/types.d.ts +354 -0
  78. package/dist/collab/types.js +1 -0
  79. package/dist/collab/validation.d.ts +10 -0
  80. package/dist/collab/validation.js +303 -0
  81. package/dist/continuity/context.d.ts +24 -0
  82. package/dist/continuity/context.js +179 -0
  83. package/dist/continuity/index.d.ts +3 -0
  84. package/dist/continuity/index.js +1 -0
  85. package/dist/continuity/paths.d.ts +37 -0
  86. package/dist/continuity/paths.js +258 -0
  87. package/dist/continuity/service.d.ts +59 -0
  88. package/dist/continuity/service.js +1258 -0
  89. package/dist/hosts/claude/native.d.ts +79 -0
  90. package/dist/hosts/claude/native.js +853 -0
  91. package/dist/hosts/codex/native.d.ts +345 -0
  92. package/dist/hosts/codex/native.js +1528 -0
  93. package/dist/hosts/codex/protocol.d.ts +51 -0
  94. package/dist/hosts/codex/protocol.js +42 -0
  95. package/dist/hosts/detect.d.ts +28 -0
  96. package/dist/hosts/detect.js +73 -0
  97. package/dist/hosts/index.d.ts +10 -0
  98. package/dist/hosts/index.js +10 -0
  99. package/dist/hosts/integration/bundle.d.ts +16 -0
  100. package/dist/hosts/integration/bundle.js +81 -0
  101. package/dist/hosts/integration/hook-entry.d.ts +1 -0
  102. package/dist/hosts/integration/hook-entry.js +68 -0
  103. package/dist/hosts/integration/hook.d.ts +32 -0
  104. package/dist/hosts/integration/hook.js +781 -0
  105. package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
  106. package/dist/hosts/integration/pi-extension-runtime.js +89 -0
  107. package/dist/hosts/integration/pi-template.d.ts +3 -0
  108. package/dist/hosts/integration/pi-template.js +5 -0
  109. package/dist/hosts/integration/service.d.ts +41 -0
  110. package/dist/hosts/integration/service.js +1381 -0
  111. package/dist/hosts/integration/support.d.ts +36 -0
  112. package/dist/hosts/integration/support.js +41 -0
  113. package/dist/hosts/integration/templates.d.ts +22 -0
  114. package/dist/hosts/integration/templates.js +70 -0
  115. package/dist/hosts/pi/protocol.d.ts +22 -0
  116. package/dist/hosts/pi/protocol.js +28 -0
  117. package/dist/hosts/pi/query-result.d.ts +22 -0
  118. package/dist/hosts/pi/query-result.js +710 -0
  119. package/dist/hosts/pi/rpc.d.ts +937 -0
  120. package/dist/hosts/pi/rpc.js +1621 -0
  121. package/dist/hosts/pilot.d.ts +485 -0
  122. package/dist/hosts/pilot.js +593 -0
  123. package/dist/index.d.ts +5 -0
  124. package/dist/index.js +4 -0
  125. package/dist/infra/fs.d.ts +27 -0
  126. package/dist/infra/fs.js +130 -0
  127. package/dist/infra/git.d.ts +60 -0
  128. package/dist/infra/git.js +358 -0
  129. package/dist/infra/owned-process-group.d.ts +173 -0
  130. package/dist/infra/owned-process-group.js +571 -0
  131. package/dist/install/apply.d.ts +65 -0
  132. package/dist/install/apply.js +663 -0
  133. package/dist/install/index.d.ts +5 -0
  134. package/dist/install/index.js +5 -0
  135. package/dist/install/ownership.d.ts +85 -0
  136. package/dist/install/ownership.js +430 -0
  137. package/dist/install/plan.d.ts +39 -0
  138. package/dist/install/plan.js +97 -0
  139. package/dist/install/rollback.d.ts +18 -0
  140. package/dist/install/rollback.js +544 -0
  141. package/dist/install/runtime.d.ts +76 -0
  142. package/dist/install/runtime.js +1145 -0
  143. package/dist/knowledge/catalog.d.ts +47 -0
  144. package/dist/knowledge/catalog.js +308 -0
  145. package/dist/knowledge/context.d.ts +28 -0
  146. package/dist/knowledge/context.js +61 -0
  147. package/dist/knowledge/index.d.ts +3 -0
  148. package/dist/knowledge/index.js +1 -0
  149. package/dist/knowledge/journal.d.ts +16 -0
  150. package/dist/knowledge/journal.js +109 -0
  151. package/dist/knowledge/lifecycle.d.ts +8 -0
  152. package/dist/knowledge/lifecycle.js +1001 -0
  153. package/dist/knowledge/paths.d.ts +37 -0
  154. package/dist/knowledge/paths.js +236 -0
  155. package/dist/knowledge/refs.d.ts +36 -0
  156. package/dist/knowledge/refs.js +267 -0
  157. package/dist/knowledge/service.d.ts +21 -0
  158. package/dist/knowledge/service.js +393 -0
  159. package/dist/knowledge/types.d.ts +171 -0
  160. package/dist/knowledge/types.js +1 -0
  161. package/dist/knowledge/write.d.ts +8 -0
  162. package/dist/knowledge/write.js +1223 -0
  163. package/dist/package-info.d.ts +6 -0
  164. package/dist/package-info.js +19 -0
  165. package/dist/schemas/code.d.ts +377 -0
  166. package/dist/schemas/code.js +145 -0
  167. package/dist/schemas/collaboration.d.ts +115 -0
  168. package/dist/schemas/collaboration.js +106 -0
  169. package/dist/schemas/continuity.d.ts +315 -0
  170. package/dist/schemas/continuity.js +199 -0
  171. package/dist/schemas/diagnostics.d.ts +28 -0
  172. package/dist/schemas/diagnostics.js +27 -0
  173. package/dist/schemas/doctor.d.ts +157 -0
  174. package/dist/schemas/doctor.js +75 -0
  175. package/dist/schemas/document.d.ts +644 -0
  176. package/dist/schemas/document.js +296 -0
  177. package/dist/schemas/errors.d.ts +177 -0
  178. package/dist/schemas/errors.js +176 -0
  179. package/dist/schemas/evidence.d.ts +24 -0
  180. package/dist/schemas/evidence.js +16 -0
  181. package/dist/schemas/host.d.ts +436 -0
  182. package/dist/schemas/host.js +201 -0
  183. package/dist/schemas/index.d.ts +15 -0
  184. package/dist/schemas/index.js +15 -0
  185. package/dist/schemas/init.d.ts +121 -0
  186. package/dist/schemas/init.js +94 -0
  187. package/dist/schemas/install.d.ts +213 -0
  188. package/dist/schemas/install.js +139 -0
  189. package/dist/schemas/machine.d.ts +43 -0
  190. package/dist/schemas/machine.js +96 -0
  191. package/dist/schemas/outcome.d.ts +23 -0
  192. package/dist/schemas/outcome.js +17 -0
  193. package/dist/schemas/paths.d.ts +19 -0
  194. package/dist/schemas/paths.js +63 -0
  195. package/dist/schemas/workspace.d.ts +120 -0
  196. package/dist/schemas/workspace.js +201 -0
  197. package/dist/workspace/config-file.d.ts +36 -0
  198. package/dist/workspace/config-file.js +237 -0
  199. package/dist/workspace/config.d.ts +28 -0
  200. package/dist/workspace/config.js +79 -0
  201. package/dist/workspace/discover.d.ts +34 -0
  202. package/dist/workspace/discover.js +195 -0
  203. package/dist/workspace/documents.d.ts +21 -0
  204. package/dist/workspace/documents.js +395 -0
  205. package/dist/workspace/ignore.d.ts +10 -0
  206. package/dist/workspace/ignore.js +48 -0
  207. package/dist/workspace/index.d.ts +7 -0
  208. package/dist/workspace/index.js +3 -0
  209. package/dist/workspace/repositories.d.ts +24 -0
  210. package/dist/workspace/repositories.js +411 -0
  211. package/dist/workspace/resolve.d.ts +24 -0
  212. package/dist/workspace/resolve.js +268 -0
  213. package/dist/workspace/types.d.ts +71 -0
  214. package/dist/workspace/types.js +1 -0
  215. package/dist/workspace/validation.d.ts +18 -0
  216. package/dist/workspace/validation.js +197 -0
  217. package/package.json +105 -4
  218. package/skills/pappra/SKILL.md +68 -0
  219. package/skills/pappra/references/hosts.md +30 -0
  220. package/skills/pappra/references/lifecycle.md +37 -0
  221. package/skills/pappra/references/operations.md +14 -0
@@ -0,0 +1,593 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { spawn } from 'node:child_process';
6
+ import { admitLaunch, reserveLaunch, settleLaunch, classifyIdentity, TOOL_PROFILES, } from '../collab/admission.js';
7
+ import { createNonce, nowIso } from '../collab/store.js';
8
+ import { sha256File as digestFile } from '../collab/envelope.js';
9
+ import { object, parseJson } from '../collab/validation.js';
10
+ import { isAssignment } from '../collab/contracts.js';
11
+ const PHASES = new Set([
12
+ 'discovery',
13
+ 'implementation',
14
+ 'review',
15
+ 'handoff',
16
+ 'fix',
17
+ ]);
18
+ function buildArgv({ harness, profile, cwd, prompt, sessionId, resume, maxBudgetUsd, sessionDir, trustProject = false, extraDirs = [], }) {
19
+ if (harness === 'pi') {
20
+ // `--session <id>` continues an existing project session; `--session-id` creates one.
21
+ const argv = [
22
+ '--print',
23
+ '--no-mcp',
24
+ '--session-dir',
25
+ sessionDir,
26
+ '--tools',
27
+ TOOL_PROFILES.pi[profile],
28
+ ...(trustProject ? ['--approve'] : []),
29
+ resume ? '--session' : '--session-id',
30
+ (resume ?? sessionId),
31
+ prompt,
32
+ ];
33
+ return { command: 'pi', argv, session_selector: resume ?? sessionId };
34
+ }
35
+ if (harness === 'codex') {
36
+ const sandbox = profile === 'reviewer' ? 'read-only' : 'workspace-write';
37
+ // `codex exec resume` accepts neither -C nor -s: it continues the recorded cwd, so the
38
+ // launcher must still be started in that cwd, and the sandbox is re-asserted as a
39
+ // per-run config override (a sandbox setting, never a model or effort setting).
40
+ const base = resume
41
+ ? [
42
+ 'exec',
43
+ 'resume',
44
+ resume,
45
+ '--json',
46
+ '--skip-git-repo-check',
47
+ '-c',
48
+ `sandbox_mode="${sandbox}"`,
49
+ prompt,
50
+ ]
51
+ : [
52
+ 'exec',
53
+ '--json',
54
+ '-C',
55
+ cwd,
56
+ '-s',
57
+ sandbox,
58
+ '--skip-git-repo-check',
59
+ prompt,
60
+ ];
61
+ return { command: 'codex', argv: base, session_selector: resume ?? null };
62
+ }
63
+ if (harness === 'claude') {
64
+ // `--permission-mode acceptEdits` is required in --print mode: without it an edit tool
65
+ // call has nobody to prompt and is denied. The reviewer profile keeps the default mode
66
+ // so a review cannot silently write.
67
+ const argv = [
68
+ '--print',
69
+ '--output-format',
70
+ 'stream-json',
71
+ '--verbose',
72
+ ...(profile === 'writer' ? ['--permission-mode', 'acceptEdits'] : []),
73
+ // `user,project` keeps the user settings and also lets project-scoped skills resolve;
74
+ // the isolated fixture ships no project settings file.
75
+ '--setting-sources',
76
+ 'user,project',
77
+ ...extraDirs.flatMap((directory) => ['--add-dir', directory]),
78
+ '--add-dir',
79
+ cwd,
80
+ '--tools',
81
+ TOOL_PROFILES.claude[profile],
82
+ '--mcp-config',
83
+ '{"mcpServers":{}}',
84
+ '--strict-mcp-config',
85
+ '--settings',
86
+ '{"disableAllHooks":true}',
87
+ '--max-budget-usd',
88
+ String(maxBudgetUsd),
89
+ resume ? '--resume' : '--session-id',
90
+ (resume ?? sessionId),
91
+ prompt,
92
+ ];
93
+ return { command: 'claude', argv, session_selector: resume ?? sessionId };
94
+ }
95
+ throw new Error(`Unsupported harness: ${harness}`);
96
+ }
97
+ function runProcess({ command, argv, cwd, timeoutMs, }) {
98
+ return new Promise((resolve) => {
99
+ const child = spawn(command, argv, {
100
+ cwd,
101
+ stdio: ['ignore', 'pipe', 'pipe'],
102
+ env: process.env,
103
+ });
104
+ const stdout = [];
105
+ const stderr = [];
106
+ let timedOut = false;
107
+ const timer = setTimeout(() => {
108
+ timedOut = true;
109
+ child.kill('SIGTERM');
110
+ setTimeout(() => child.kill('SIGKILL'), 5000);
111
+ }, timeoutMs);
112
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
113
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
114
+ child.on('error', (error) => {
115
+ clearTimeout(timer);
116
+ resolve({
117
+ code: null,
118
+ error: error instanceof Error ? error.message : String(error),
119
+ spawned: false,
120
+ stdout: Buffer.concat(stdout),
121
+ stderr: Buffer.concat(stderr),
122
+ timedOut,
123
+ });
124
+ });
125
+ child.on('close', (code) => {
126
+ clearTimeout(timer);
127
+ resolve({
128
+ code,
129
+ error: null,
130
+ spawned: true,
131
+ stdout: Buffer.concat(stdout),
132
+ stderr: Buffer.concat(stderr),
133
+ timedOut,
134
+ });
135
+ });
136
+ });
137
+ }
138
+ /** Read the native session id from a saved Claude stream (system/init). */
139
+ export function readClaudeSessionId(file) {
140
+ try {
141
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
142
+ const trimmed = line.trim();
143
+ if (!trimmed)
144
+ continue;
145
+ const row = parseJson(trimmed);
146
+ if (object(row) &&
147
+ row.type === 'system' &&
148
+ row.subtype === 'init' &&
149
+ typeof row.session_id === 'string')
150
+ return row.session_id;
151
+ }
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ return null;
157
+ }
158
+ /** Read the native session id from a saved Pi session file. */
159
+ export function readPiSessionId(file) {
160
+ try {
161
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
162
+ const trimmed = line.trim();
163
+ if (!trimmed)
164
+ continue;
165
+ const row = parseJson(trimmed);
166
+ if (object(row) && row.type === 'session' && typeof row.id === 'string')
167
+ return row.id;
168
+ }
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ return null;
174
+ }
175
+ /**
176
+ * Record the size/mtime of every Pi session file before a run starts, so a later lookup can
177
+ * tell which files this invocation actually created or changed.
178
+ */
179
+ function snapshotPiSessions(sessionDir) {
180
+ const snapshot = new Map();
181
+ let names = [];
182
+ try {
183
+ names = fs
184
+ .readdirSync(sessionDir)
185
+ .filter((name) => name.endsWith('.jsonl'));
186
+ }
187
+ catch {
188
+ return snapshot;
189
+ }
190
+ for (const name of names) {
191
+ try {
192
+ const stat = fs.statSync(path.join(sessionDir, name));
193
+ snapshot.set(name, { size: stat.size, mtimeMs: stat.mtimeMs });
194
+ }
195
+ catch {
196
+ // A file that disappears during the snapshot is simply absent from the baseline.
197
+ }
198
+ }
199
+ return snapshot;
200
+ }
201
+ /**
202
+ * Find the native session file this invocation produced.
203
+ *
204
+ * Only a file that was created or changed after the baseline may count, and its header must
205
+ * carry the requested id. An unrelated pre-existing file (or an old file this failed
206
+ * invocation never touched) is never attributed to the run; a wrong attribution would be
207
+ * worse than reporting no observed identity.
208
+ */
209
+ function findPiSession(sessionDir, sessionId, baseline = new Map()) {
210
+ let names = [];
211
+ try {
212
+ names = fs
213
+ .readdirSync(sessionDir)
214
+ .filter((name) => name.endsWith('.jsonl'));
215
+ }
216
+ catch {
217
+ return null;
218
+ }
219
+ const candidates = [];
220
+ for (const name of names) {
221
+ const file = path.join(sessionDir, name);
222
+ let stat;
223
+ try {
224
+ stat = fs.statSync(file);
225
+ }
226
+ catch {
227
+ continue;
228
+ }
229
+ const before = baseline.get(name);
230
+ if (before && before.size === stat.size && before.mtimeMs === stat.mtimeMs)
231
+ continue;
232
+ if (readPiSessionId(file) !== sessionId)
233
+ continue;
234
+ candidates.push({ file, mtimeMs: stat.mtimeMs });
235
+ }
236
+ if (!candidates.length)
237
+ return null;
238
+ candidates.sort((left, right) => left.mtimeMs - right.mtimeMs);
239
+ return candidates.at(-1).file;
240
+ }
241
+ function readCodexThread(eventsFile) {
242
+ try {
243
+ for (const line of fs.readFileSync(eventsFile, 'utf8').split('\n')) {
244
+ const trimmed = line.trim();
245
+ if (!trimmed)
246
+ continue;
247
+ const row = parseJson(trimmed);
248
+ if (object(row) &&
249
+ row.type === 'thread.started' &&
250
+ typeof row.thread_id === 'string')
251
+ return row.thread_id;
252
+ }
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ return null;
258
+ }
259
+ export async function runNativePilot({ harness, profile, cwd, promptFile, evidenceDir, sessionId = null, resume = null, timeoutMs = 300000, maxBudgetUsd = 1, outputName = null, phase = 'discovery', trustProject = false, extraDirs = [], clock = Date.now(), assignmentFile, capabilitiesFile, taskRoot, maxAgeMinutes = 30, launchKind = 'first', discovery = false, }) {
260
+ if (!['pi', 'codex', 'claude'].includes(harness))
261
+ throw new Error('Unsupported harness');
262
+ if (!['writer', 'reviewer'].includes(profile))
263
+ throw new Error('Unsupported profile');
264
+ if (!path.isAbsolute(cwd ?? '') || !fs.statSync(cwd).isDirectory())
265
+ throw new Error('cwd must be an existing absolute directory');
266
+ if (!path.isAbsolute(promptFile ?? '') || !fs.statSync(promptFile).isFile())
267
+ throw new Error('prompt-file must be an absolute existing file');
268
+ if (!path.isAbsolute(evidenceDir ?? ''))
269
+ throw new Error('evidence-dir must be an absolute path');
270
+ if (!PHASES.has(phase))
271
+ throw new Error('Unsupported phase');
272
+ const id = sessionId ??
273
+ resume ??
274
+ (harness === 'claude' || harness === 'pi' ? crypto.randomUUID() : null);
275
+ if (harness !== 'codex' && !id)
276
+ throw new Error('pi and claude need a session id');
277
+ if (!path.isAbsolute(assignmentFile ?? '') ||
278
+ !path.isAbsolute(capabilitiesFile ?? '')) {
279
+ throw new Error('The launcher requires --assignment and --capabilities: a native process is never started without a capability preflight');
280
+ }
281
+ const assignmentForLaunch = parseJson(fs.readFileSync(assignmentFile, 'utf8'));
282
+ if (!isAssignment(assignmentForLaunch))
283
+ throw new Error('Invalid assignment structure');
284
+ const effectiveLaunchKind = resume ? 'resume' : launchKind;
285
+ if (effectiveLaunchKind === 'resume' && !resume)
286
+ throw new Error('A resume launch needs the recorded native session via --resume');
287
+ if (effectiveLaunchKind !== 'resume' && resume)
288
+ throw new Error('A first launch must not carry a resume selector');
289
+ // Everything that can fail is validated BEFORE the reservation exists, so a validation error
290
+ // never leaves a dangling reservation behind.
291
+ const prompt = fs.readFileSync(promptFile, 'utf8');
292
+ if (prompt.length > 20000)
293
+ throw new Error('Prompt is too large for a bounded pilot run');
294
+ // Frozen before the process starts: a child that deletes or rewrites these files must not be
295
+ // able to break the post-run bookkeeping.
296
+ const frozenPrompt = { bytes: prompt, digest: digestFile(promptFile) };
297
+ const frozenCapabilities = {
298
+ file: capabilitiesFile,
299
+ digest: digestFile(capabilitiesFile),
300
+ };
301
+ for (const directory of extraDirs) {
302
+ if (!path.isAbsolute(directory) || !fs.statSync(directory).isDirectory())
303
+ throw new Error(`extra --add-dir path must be an existing absolute directory: ${directory}`);
304
+ }
305
+ fs.mkdirSync(evidenceDir, { recursive: true, mode: 0o700 });
306
+ const name = outputName ?? `${harness}-${profile}-${resume ? 'resume' : 'new'}`;
307
+ const eventsFile = path.join(evidenceDir, `${name}.events.jsonl`);
308
+ const stdoutFile = path.join(evidenceDir, `${name}.stdout.log`);
309
+ const stderrFile = path.join(evidenceDir, `${name}.stderr.log`);
310
+ const executionFile = path.join(evidenceDir, `${name}.execution.json`);
311
+ for (const file of [eventsFile, stdoutFile, stderrFile, executionFile]) {
312
+ if (fs.existsSync(file))
313
+ throw new Error(`Evidence file already exists: ${file}`);
314
+ }
315
+ // A resumed Pi session is looked up inside this directory. Changing the evidence directory
316
+ // is not native session migration: a resume caller must preserve the original session
317
+ // directory, otherwise Pi reports no matching session and no new evidence is attributable.
318
+ const piSessionDir = path.join(evidenceDir, 'pi-sessions');
319
+ fs.mkdirSync(piSessionDir, { recursive: true, mode: 0o700 });
320
+ const built = buildArgv({
321
+ harness,
322
+ profile,
323
+ cwd,
324
+ prompt,
325
+ sessionId: id,
326
+ resume,
327
+ maxBudgetUsd,
328
+ sessionDir: piSessionDir,
329
+ trustProject,
330
+ extraDirs,
331
+ });
332
+ const admission = admitLaunch({
333
+ assignmentFile,
334
+ capabilitiesFile,
335
+ taskRoot,
336
+ cwd,
337
+ sessionId: resume ?? id,
338
+ harness,
339
+ profile,
340
+ launchKind: effectiveLaunchKind,
341
+ resumeSelector: resume,
342
+ discovery,
343
+ now: clock,
344
+ maxAgeMinutes,
345
+ });
346
+ if (!admission.ok)
347
+ return { refused: true, admission, reason: admission.reason };
348
+ const reservation = reserveLaunch(taskRoot, {
349
+ expectRevision: admission.state_revision,
350
+ clock,
351
+ reservation: {
352
+ reservation_id: `launch-${createNonce()}`,
353
+ subtask_id: assignmentForLaunch.assignment_id,
354
+ attempt: assignmentForLaunch.attempt,
355
+ kind: effectiveLaunchKind,
356
+ mode: admission.mode,
357
+ phase,
358
+ holder: assignmentForLaunch.assignee,
359
+ pid: process.pid,
360
+ hostname: os.hostname(),
361
+ requested_session_id: resume ?? id,
362
+ resume_selector: resume ?? null,
363
+ },
364
+ });
365
+ if (!reservation.ok)
366
+ return {
367
+ refused: true,
368
+ admission,
369
+ reason: reservation.reason,
370
+ reservation,
371
+ };
372
+ const settle = (outcome, extra = {}) => settleLaunch(taskRoot, {
373
+ reservationId: reservation.reservation_id,
374
+ outcome,
375
+ runner: assignmentForLaunch.runner?.name ?? null,
376
+ detail: extra.detail ?? null,
377
+ ...extra,
378
+ });
379
+ // Frozen before the process starts, so only evidence this invocation created or changed can
380
+ // later be attributed to it.
381
+ const sessionBaseline = harness === 'pi' ? snapshotPiSessions(piSessionDir) : new Map();
382
+ let result;
383
+ let startedAt;
384
+ let wallMs;
385
+ try {
386
+ startedAt = nowIso(clock);
387
+ const started = Date.now();
388
+ result = await runProcess({
389
+ command: built.command,
390
+ argv: built.argv,
391
+ cwd,
392
+ timeoutMs,
393
+ });
394
+ startedAt = result.startedAt ?? startedAt;
395
+ wallMs = Date.now() - started;
396
+ }
397
+ catch (error) {
398
+ // The process never spawned: release the reservation and leave the attempt untouched.
399
+ settle('not_started', {
400
+ detail: error instanceof Error ? error.message : String(error),
401
+ });
402
+ throw error;
403
+ }
404
+ const finishedAt = nowIso();
405
+ const isStream = harness !== 'pi';
406
+ let nativeEvidence = null;
407
+ let nativeSessionId = null;
408
+ try {
409
+ fs.writeFileSync(isStream ? eventsFile : stdoutFile, result.stdout, {
410
+ flag: 'wx',
411
+ mode: 0o600,
412
+ });
413
+ fs.writeFileSync(stderrFile, result.stderr, { flag: 'wx', mode: 0o600 });
414
+ if (harness === 'codex') {
415
+ nativeEvidence = eventsFile;
416
+ nativeSessionId = readCodexThread(eventsFile);
417
+ }
418
+ else if (harness === 'pi') {
419
+ // Only a session file this invocation created or changed may be observed. A continued Pi
420
+ // session appends to its original file, which makes that file attributable; an unrelated
421
+ // or untouched file is never used to confirm the launch.
422
+ nativeEvidence = findPiSession(piSessionDir, (resume ?? id), sessionBaseline);
423
+ nativeSessionId = nativeEvidence ? readPiSessionId(nativeEvidence) : null;
424
+ }
425
+ else {
426
+ nativeEvidence = eventsFile;
427
+ nativeSessionId = readClaudeSessionId(eventsFile);
428
+ }
429
+ }
430
+ catch (error) {
431
+ const settlement = settle('evidence_write_failed', {
432
+ exitCode: result.code ?? null,
433
+ sessionId: null,
434
+ runId: `${name}:${startedAt}`,
435
+ evidenceRef: null,
436
+ expectedIdentity: admission.expected_identity,
437
+ capabilityEvidence: admission.capability_evidence,
438
+ detail: error instanceof Error ? error.message : String(error),
439
+ });
440
+ // No execution record was written, but the process did run: keep its metadata so the caller
441
+ // can still see what actually happened.
442
+ return {
443
+ execution: null,
444
+ evidenceError: error instanceof Error ? error.message : String(error),
445
+ settlement,
446
+ process: {
447
+ status: result.error
448
+ ? 'spawn_failed'
449
+ : result.timedOut
450
+ ? 'timed_out'
451
+ : 'exited',
452
+ exit_code: result.code ?? null,
453
+ spawned: result.spawned === true,
454
+ timed_out: result.timedOut === true,
455
+ started_at: startedAt,
456
+ finished_at: finishedAt,
457
+ wall_ms: wallMs,
458
+ },
459
+ };
460
+ }
461
+ const settledOutcome = result.error
462
+ ? 'spawn_failed'
463
+ : result.timedOut
464
+ ? 'timed_out'
465
+ : result.code === 0
466
+ ? 'exited'
467
+ : 'exited_nonzero';
468
+ let execution = null;
469
+ try {
470
+ const identity = classifyIdentity({
471
+ ran: settledOutcome === 'exited' || settledOutcome === 'exited_nonzero',
472
+ observed: nativeSessionId,
473
+ expected: admission.expected_identity,
474
+ capabilityEvidence: admission.capability_evidence,
475
+ });
476
+ execution = {
477
+ schema: 'harness-collab/native-execution/1',
478
+ harness,
479
+ profile,
480
+ phase,
481
+ status: result.error
482
+ ? 'spawn_failed'
483
+ : result.timedOut
484
+ ? 'timed_out'
485
+ : 'exited',
486
+ exit_code: result.code,
487
+ spawn_error: result.error,
488
+ timed_out: result.timedOut,
489
+ started_at: startedAt,
490
+ finished_at: finishedAt,
491
+ wall_ms: wallMs,
492
+ cwd,
493
+ // The identity actually observed in native evidence; `requested_session_id` is what the
494
+ // launcher asked for and is never treated as an observation.
495
+ session_id: nativeSessionId,
496
+ requested_session_id: id,
497
+ resumed_from: resume,
498
+ identity_status: identity.status,
499
+ // A nonzero exit still records the run and its identity, but the observation is not
500
+ // confirmed: only a clean exit can back a runtime capability claim.
501
+ identity_confirmed: identity.confirmed && settledOutcome === 'exited',
502
+ argv_summary: built.argv.map((token) => token.length > 120 ? `${token.slice(0, 117)}...` : token),
503
+ tool_allowlist: harness === 'pi'
504
+ ? TOOL_PROFILES.pi[profile]
505
+ : harness === 'claude'
506
+ ? TOOL_PROFILES.claude[profile]
507
+ : null,
508
+ sandbox: harness === 'codex'
509
+ ? profile === 'reviewer'
510
+ ? 'read-only'
511
+ : 'workspace-write'
512
+ : null,
513
+ limits: {
514
+ timeout_ms: timeoutMs,
515
+ max_budget_usd: harness === 'claude' ? maxBudgetUsd : null,
516
+ model_override: null,
517
+ effort_override: null,
518
+ },
519
+ project_trust: harness === 'pi'
520
+ ? trustProject
521
+ ? 'approved-for-this-run'
522
+ : 'not-approved'
523
+ : 'host-default',
524
+ extra_dirs: extraDirs,
525
+ native_evidence: nativeEvidence,
526
+ events_file: isStream ? eventsFile : null,
527
+ stdout_file: isStream ? null : stdoutFile,
528
+ stderr_file: stderrFile,
529
+ prompt_file: promptFile,
530
+ prompt_digest: frozenPrompt.digest,
531
+ admission: {
532
+ assignment_file: assignmentFile,
533
+ assignment_digest: admission.assignment_digest,
534
+ capabilities_file: frozenCapabilities.file,
535
+ capabilities_digest: frozenCapabilities.digest,
536
+ task_root: taskRoot,
537
+ max_age_minutes: maxAgeMinutes,
538
+ mode: admission.mode,
539
+ launch_kind: admission.launch_kind,
540
+ resume_selector: admission.resume_selector,
541
+ resources: admission.resources,
542
+ reservation_id: reservation.reservation_id,
543
+ checks: admission.checks,
544
+ },
545
+ // The authoritative outcome of the launch lives in the task record's launch_reservations;
546
+ // this file is the evidence it points at.
547
+ planned_outcome: settledOutcome,
548
+ };
549
+ // The record must exist before the settlement points at it.
550
+ fs.writeFileSync(executionFile, `${JSON.stringify(execution, null, 2)}\n`, {
551
+ flag: 'wx',
552
+ mode: 0o600,
553
+ });
554
+ }
555
+ catch (error) {
556
+ // Anything that fails after the process ran — reading the identity back, writing the native
557
+ // streams, or building and writing this record — shares one settlement, so the fact that it
558
+ // ran is on record as an uncertain execution instead of a silently stuck reservation.
559
+ const settlement = settle('evidence_write_failed', {
560
+ exitCode: result.code ?? null,
561
+ sessionId: nativeSessionId,
562
+ runId: `${name}:${startedAt}`,
563
+ evidenceRef: null,
564
+ expectedIdentity: admission.expected_identity,
565
+ capabilityEvidence: admission.capability_evidence,
566
+ detail: error instanceof Error ? error.message : String(error),
567
+ });
568
+ return {
569
+ execution,
570
+ evidenceError: error instanceof Error ? error.message : String(error),
571
+ settlement,
572
+ };
573
+ }
574
+ const settlement = settle(settledOutcome, {
575
+ exitCode: result.code,
576
+ sessionId: nativeSessionId,
577
+ runId: `${name}:${startedAt}`,
578
+ evidenceRef: executionFile,
579
+ expectedIdentity: admission.expected_identity,
580
+ capabilityEvidence: admission.capability_evidence,
581
+ detail: result.error ?? null,
582
+ });
583
+ return {
584
+ execution,
585
+ executionFile,
586
+ eventsFile,
587
+ stdoutFile,
588
+ stderrFile,
589
+ nativeEvidence,
590
+ reservation_id: reservation.reservation_id,
591
+ settlement,
592
+ };
593
+ }
@@ -0,0 +1,5 @@
1
+ export { getPackageInfo } from './package-info.js';
2
+ export type { PackageInfo } from './package-info.js';
3
+ export { HostAdapter, HostIntegrationService, type HostIntegrationOptions, type HostTargetOptions, } from './hosts/integration/service.js';
4
+ export { mapNativeHookEvent, runHostHook, type HostHookOptions, type NativeHookMapping, } from './hosts/integration/hook.js';
5
+ export * from './app/collaboration.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { getPackageInfo } from './package-info.js';
2
+ export { HostAdapter, HostIntegrationService, } from './hosts/integration/service.js';
3
+ export { mapNativeHookEvent, runHostHook, } from './hosts/integration/hook.js';
4
+ export * from './app/collaboration.js';
@@ -0,0 +1,27 @@
1
+ import type { Stats } from 'node:fs';
2
+ import type { Outcome } from '../schemas/outcome.js';
3
+ export declare const DEFAULT_CONFIG_MAX_BYTES: number;
4
+ export interface BoundedFile {
5
+ readonly bytes: Buffer;
6
+ readonly digest: string;
7
+ readonly size: number;
8
+ }
9
+ export interface BoundedTextFile extends BoundedFile {
10
+ readonly text: string;
11
+ }
12
+ export declare function sha256Digest(bytes: Uint8Array | string): string;
13
+ /**
14
+ * Reads at most `maxBytes + 1` bytes from a regular file. The implementation
15
+ * never loads a larger file into memory just to report that it is too large.
16
+ */
17
+ export declare function readFileBounded(path: string, maxBytes?: number): Promise<Outcome<BoundedFile>>;
18
+ export declare function readTextFileBounded(path: string, maxBytes?: number): Promise<Outcome<BoundedTextFile>>;
19
+ export type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';
20
+ export interface PathObservation {
21
+ readonly kind: PathKind;
22
+ readonly path: string;
23
+ readonly stats: Stats | null;
24
+ }
25
+ /** Observes one path segment without following a final symlink. */
26
+ export declare function observePath(path: string): Promise<PathObservation>;
27
+ export declare function resolveRealpath(path: string): Promise<string | null>;