@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,781 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { link, mkdir, open, rm } from 'node:fs/promises';
3
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { createDiagnostic } from '../../schemas/diagnostics.js';
5
+ import { diagnosticCodes } from '../../schemas/errors.js';
6
+ import { HOST_HOOK_INPUT_SCHEMA, commandHookEventSchema, hostHookInputSchema, hostHookResultSchema, } from '../../schemas/host.js';
7
+ import { observePath, readFileBounded, resolveRealpath, sha256Digest, } from '../../infra/fs.js';
8
+ import { isPathInside } from '../../schemas/paths.js';
9
+ import { resolveWorkspaceRoot } from '../../workspace/resolve.js';
10
+ import { inspectMachineConfig, loadWorkspaceConfig, } from '../../workspace/config.js';
11
+ import { CheckpointService } from '../../continuity/service.js';
12
+ import { ensureStatePathBoundary } from '../../install/ownership.js';
13
+ import { nativeEventFor } from './support.js';
14
+ const DEFAULT_MAX_INPUT_BYTES = 1024 * 1024;
15
+ const DEFAULT_DEADLINE_MS = 1000;
16
+ const DEFAULT_MAX_OUTPUT_BYTES = 8 * 1024;
17
+ function diagnostic(code, message, source, hint) {
18
+ return createDiagnostic({
19
+ code,
20
+ severity: 'error',
21
+ message,
22
+ source,
23
+ hint,
24
+ });
25
+ }
26
+ function result(input) {
27
+ return hostHookResultSchema.parse({
28
+ schema: 'pappra/host-hook-result/1',
29
+ ok: input.ok,
30
+ status: input.status,
31
+ host: input.host,
32
+ event: input.event,
33
+ eventKey: input.eventKey,
34
+ duplicate: input.duplicate,
35
+ context: input.context,
36
+ diagnostics: input.diagnostics,
37
+ });
38
+ }
39
+ export function renderHostHookOutput(host, result) {
40
+ if (result.context === null || result.event === null)
41
+ return null;
42
+ const context = result.context.length > 8000
43
+ ? `${result.context.slice(0, 7997)}...`
44
+ : result.context;
45
+ if (host === 'claude' || host === 'codex') {
46
+ const native = nativeEventFor(host, result.event);
47
+ if (native === null)
48
+ return null;
49
+ return `${JSON.stringify({
50
+ hookSpecificOutput: {
51
+ hookEventName: native,
52
+ additionalContext: context,
53
+ },
54
+ })}\n`;
55
+ }
56
+ return null;
57
+ }
58
+ function cwdInside(root, cwd) {
59
+ return isPathInside(root, cwd);
60
+ }
61
+ async function canonicalEqual(left, right) {
62
+ const [realLeft, realRight] = await Promise.all([
63
+ resolveRealpath(left),
64
+ resolveRealpath(right),
65
+ ]);
66
+ return realLeft !== null && realRight !== null && realLeft === realRight;
67
+ }
68
+ async function privateSegmentsSafe(root, directory) {
69
+ const rel = relative(root, directory);
70
+ if (rel === '')
71
+ return true;
72
+ if (isAbsolute(rel) || rel === '..' || rel.startsWith(`..${sep}`)) {
73
+ return false;
74
+ }
75
+ let current = root;
76
+ for (const segment of rel.split(sep)) {
77
+ current = join(current, segment);
78
+ const observed = await observePath(current);
79
+ if (observed.kind === 'missing')
80
+ return true;
81
+ if (observed.kind !== 'directory')
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+ async function boundedAdvisory(root, event, taskId, workspaceName) {
87
+ const lines = [
88
+ `Pappra workspace "${workspaceName}" is registered for lifecycle event ${event}.`,
89
+ ];
90
+ if (taskId === null) {
91
+ lines.push('No explicit task was selected; run `pappra checkpoint --task <id>` or `pappra resume <id>` for manual continuity.');
92
+ }
93
+ else {
94
+ const service = new CheckpointService(root);
95
+ const resumed = await service.resume({
96
+ taskId,
97
+ currentSession: null,
98
+ });
99
+ if (!resumed.ok || resumed.packet === null) {
100
+ lines.push(`Checkpoint ${taskId} is unavailable or incomplete; manual pappra checkpoint/resume is required.`);
101
+ }
102
+ else {
103
+ lines.push(`Checkpoint ${taskId} is available for manual review.`);
104
+ lines.push(`Objective: ${resumed.packet.boundedContext.objective}`);
105
+ if (resumed.packet.boundedContext.nextActions.length > 0) {
106
+ lines.push(`Next actions: ${resumed.packet.boundedContext.nextActions.join('; ')}`);
107
+ }
108
+ if (!resumed.packet.executionAllowed) {
109
+ lines.push('Context does not authorize execution; inspect blockers before acting.');
110
+ }
111
+ }
112
+ }
113
+ const text = lines.join('\n');
114
+ return text.length <= 8192 ? text : `${text.slice(0, 8189)}...`;
115
+ }
116
+ async function eventNamespaceSafe(namespace) {
117
+ if (!(await privateSegmentsSafe(namespace.root, namespace.directory))) {
118
+ return false;
119
+ }
120
+ const observed = await observePath(namespace.directory);
121
+ if (observed.kind !== 'directory' ||
122
+ observed.stats === null ||
123
+ observed.stats.dev !== namespace.dev ||
124
+ observed.stats.ino !== namespace.ino) {
125
+ return false;
126
+ }
127
+ return (await resolveRealpath(namespace.directory)) === namespace.real;
128
+ }
129
+ async function eventTempStillOwned(path, identity, namespace, requireComplete) {
130
+ if (identity === null || !(await eventNamespaceSafe(namespace)))
131
+ return false;
132
+ const observed = await observePath(path);
133
+ if (observed.kind !== 'file' ||
134
+ observed.stats === null ||
135
+ observed.stats.dev !== identity.dev ||
136
+ observed.stats.ino !== identity.ino) {
137
+ return false;
138
+ }
139
+ if (!requireComplete)
140
+ return true;
141
+ const read = await readFileBounded(path, identity.expected.length + 1);
142
+ return read.ok && identity.expected.equals(read.value.bytes);
143
+ }
144
+ async function removeOwnedEventTemp(path, identity, namespace) {
145
+ if (!(await eventNamespaceSafe(namespace)))
146
+ return path;
147
+ const observed = await observePath(path);
148
+ if (observed.kind === 'missing')
149
+ return null;
150
+ if (identity === null) {
151
+ return observed.kind === 'file' ? path : null;
152
+ }
153
+ if (observed.kind !== 'file' ||
154
+ observed.stats === null ||
155
+ observed.stats.dev !== identity.dev ||
156
+ observed.stats.ino !== identity.ino) {
157
+ return path;
158
+ }
159
+ const read = await readFileBounded(path, identity.expected.length + 1);
160
+ if (!read.ok)
161
+ return path;
162
+ const bytes = read.value.bytes;
163
+ const complete = identity.expected.equals(bytes);
164
+ const prefix = bytes.length < identity.expected.length &&
165
+ identity.expected.subarray(0, bytes.length).equals(bytes);
166
+ const prefixEligible = !identity.writeCompleted && prefix;
167
+ if (!complete && !prefixEligible)
168
+ return path;
169
+ try {
170
+ await rm(path);
171
+ return null;
172
+ }
173
+ catch {
174
+ return path;
175
+ }
176
+ }
177
+ function parseCompleteEventRecord(bytes, expected) {
178
+ let value;
179
+ try {
180
+ value = JSON.parse(bytes.toString('utf8'));
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
186
+ return null;
187
+ }
188
+ const record = value;
189
+ if (record['schema'] !== 'pappra/host-event/1' ||
190
+ record['eventKey'] !== expected.key ||
191
+ record['host'] !== expected.host ||
192
+ record['event'] !== expected.event ||
193
+ record['eventId'] !== expected.eventId ||
194
+ record['sessionId'] !== expected.sessionId ||
195
+ record['cwd'] !== expected.cwd ||
196
+ record['root'] !== expected.root ||
197
+ record['nativeEvent'] !== expected.nativeEvent ||
198
+ record['source'] !== expected.source ||
199
+ typeof record['observedAt'] !== 'string' ||
200
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(record['observedAt'])) {
201
+ return null;
202
+ }
203
+ return record;
204
+ }
205
+ export function mapNativeHookEvent(host, raw, _fallbackEvent, _fallbackCwd) {
206
+ const parsed = commandHookEventSchema.safeParse(raw);
207
+ if (!parsed.success) {
208
+ return {
209
+ ok: false,
210
+ host,
211
+ event: null,
212
+ diagnostic: diagnostic(diagnosticCodes.host_hook_input_invalid, 'Native hook input does not match the observed envelope.', 'host hook stdin', 'Provide the documented host event JSON; no local event was recorded.'),
213
+ };
214
+ }
215
+ const native = parsed.data.hook_event_name;
216
+ let event = null;
217
+ if (native === 'SessionStart') {
218
+ const source = parsed.data.source;
219
+ event =
220
+ source === 'resume'
221
+ ? 'resume'
222
+ : source === undefined || source === 'startup' || source === 'new'
223
+ ? 'startup'
224
+ : null;
225
+ }
226
+ else if (native === 'PostCompact') {
227
+ event = 'compact';
228
+ }
229
+ if (event === null || nativeEventFor(host, event) === null) {
230
+ return {
231
+ ok: false,
232
+ host,
233
+ event,
234
+ diagnostic: diagnostic(diagnosticCodes.host_hook_unsupported, `Host ${host} native event ${native} has no verified Pappra lifecycle mapping.`, 'host hook stdin', 'Keep this event unsupported; do not infer semantics from its name.'),
235
+ };
236
+ }
237
+ const cwd = parsed.data.cwd;
238
+ if (!isAbsolute(cwd)) {
239
+ return {
240
+ ok: false,
241
+ host,
242
+ event,
243
+ diagnostic: diagnostic(diagnosticCodes.host_hook_cwd_invalid, 'Native hook input cwd is not absolute.', 'host hook stdin', 'No workspace was guessed and no event was recorded.'),
244
+ };
245
+ }
246
+ const candidate = hostHookInputSchema.safeParse({
247
+ schema: HOST_HOOK_INPUT_SCHEMA,
248
+ host,
249
+ event,
250
+ nativeEvent: native,
251
+ eventId: parsed.data.event_id ?? null,
252
+ sessionId: parsed.data.session_id ?? null,
253
+ cwd: resolve(cwd),
254
+ root: null,
255
+ taskId: parsed.data.pappra_task_id ?? null,
256
+ source: parsed.data.source ?? null,
257
+ observedAt: new Date().toISOString(),
258
+ });
259
+ if (!candidate.success) {
260
+ return {
261
+ ok: false,
262
+ host,
263
+ event,
264
+ diagnostic: diagnostic(diagnosticCodes.host_hook_input_invalid, 'Normalized host event is invalid.', 'host hook stdin', 'Provide bounded event identifiers and an absolute cwd; no event was recorded.'),
265
+ };
266
+ }
267
+ return { ok: true, input: candidate.data };
268
+ }
269
+ /**
270
+ * Records one bounded local lifecycle observation. It never reads transcripts,
271
+ * starts a host/model/network/index, writes a checkpoint, or continues a run.
272
+ */
273
+ export async function runHostHook(input, options = {}) {
274
+ const parsed = hostHookInputSchema.safeParse(input);
275
+ if (!parsed.success) {
276
+ return result({
277
+ ok: false,
278
+ status: 'invalid',
279
+ host: null,
280
+ event: null,
281
+ eventKey: null,
282
+ duplicate: false,
283
+ context: null,
284
+ diagnostics: [
285
+ diagnostic(diagnosticCodes.host_hook_input_invalid, 'Host hook input failed schema validation.', 'host hook input', 'No local event was recorded.'),
286
+ ],
287
+ });
288
+ }
289
+ const value = parsed.data;
290
+ const deadline = options.deadlineMs ?? DEFAULT_DEADLINE_MS;
291
+ const clock = options.now ?? Date.now;
292
+ const started = clock();
293
+ if (!Number.isSafeInteger(deadline) || deadline < 1) {
294
+ return result({
295
+ ok: false,
296
+ status: 'invalid',
297
+ host: value.host,
298
+ event: value.event,
299
+ eventKey: null,
300
+ duplicate: false,
301
+ context: null,
302
+ diagnostics: [
303
+ diagnostic(diagnosticCodes.host_hook_input_invalid, 'Hook deadline must be a positive integer.', 'host hook input', 'Use the default bounded deadline; no event was recorded.'),
304
+ ],
305
+ });
306
+ }
307
+ if (nativeEventFor(value.host, value.event) === null) {
308
+ return result({
309
+ ok: true,
310
+ status: 'unsupported',
311
+ host: value.host,
312
+ event: value.event,
313
+ eventKey: null,
314
+ duplicate: false,
315
+ context: null,
316
+ diagnostics: [
317
+ diagnostic(diagnosticCodes.host_hook_unsupported, `Host ${value.host} has no verified native event for ${value.event}.`, value.cwd, 'Use manual checkpoint/resume; no event was recorded.'),
318
+ ],
319
+ });
320
+ }
321
+ const resolution = await resolveWorkspaceRoot({ startPath: value.cwd });
322
+ if (resolution.root === null) {
323
+ return result({
324
+ ok: false,
325
+ status: 'unsupported',
326
+ host: value.host,
327
+ event: value.event,
328
+ eventKey: null,
329
+ duplicate: false,
330
+ context: null,
331
+ diagnostics: [
332
+ diagnostic(diagnosticCodes.host_hook_cwd_invalid, `Hook cwd is not inside a registered Pappra workspace: ${value.cwd}`, value.cwd, 'No last workspace was reused; no event was recorded.'),
333
+ ],
334
+ });
335
+ }
336
+ const root = resolution.root;
337
+ const workspaceRead = await loadWorkspaceConfig(root, { missing: 'error' });
338
+ if (!workspaceRead.ok) {
339
+ return result({
340
+ ok: false,
341
+ status: 'invalid',
342
+ host: value.host,
343
+ event: value.event,
344
+ eventKey: null,
345
+ duplicate: false,
346
+ context: null,
347
+ diagnostics: [
348
+ diagnostic(workspaceRead.code, workspaceRead.message, root, workspaceRead.nextAction ??
349
+ 'Repair the workspace configuration; no event was recorded.'),
350
+ ],
351
+ });
352
+ }
353
+ const machineRead = await inspectMachineConfig(root);
354
+ if (machineRead.exists && machineRead.value === null) {
355
+ return result({
356
+ ok: false,
357
+ status: 'invalid',
358
+ host: value.host,
359
+ event: value.event,
360
+ eventKey: null,
361
+ duplicate: false,
362
+ context: null,
363
+ diagnostics: machineRead.diagnostics.length > 0
364
+ ? machineRead.diagnostics
365
+ : [
366
+ diagnostic(diagnosticCodes.machineWorkspaceMismatch, 'Local machine configuration is invalid for this workspace.', root, 'Repair or remove machine.json explicitly; no event was recorded.'),
367
+ ],
368
+ });
369
+ }
370
+ if (value.root !== null && !(await canonicalEqual(value.root, root))) {
371
+ return result({
372
+ ok: false,
373
+ status: 'invalid',
374
+ host: value.host,
375
+ event: value.event,
376
+ eventKey: null,
377
+ duplicate: false,
378
+ context: null,
379
+ diagnostics: [
380
+ diagnostic(diagnosticCodes.host_hook_cwd_invalid, 'Hook root disagrees with the registered workspace resolved from cwd.', value.cwd, 'Use the event cwd/root for the same workspace; no event was recorded.'),
381
+ ],
382
+ });
383
+ }
384
+ const realCwd = await resolveRealpath(value.cwd);
385
+ if (realCwd === null || !cwdInside(root, realCwd)) {
386
+ return result({
387
+ ok: false,
388
+ status: 'invalid',
389
+ host: value.host,
390
+ event: value.event,
391
+ eventKey: null,
392
+ duplicate: false,
393
+ context: null,
394
+ diagnostics: [
395
+ diagnostic(diagnosticCodes.host_hook_cwd_invalid, 'Hook cwd escapes the resolved workspace.', value.cwd, 'No event was recorded.'),
396
+ ],
397
+ });
398
+ }
399
+ if (clock() - started > deadline) {
400
+ return result({
401
+ ok: false,
402
+ status: 'timeout',
403
+ host: value.host,
404
+ event: value.event,
405
+ eventKey: null,
406
+ duplicate: false,
407
+ context: null,
408
+ diagnostics: [
409
+ diagnostic(diagnosticCodes.host_hook_timeout, 'Hook exceeded its bounded local deadline.', value.cwd, 'No event was recorded; retry from the native host or use the manual CLI.'),
410
+ ],
411
+ });
412
+ }
413
+ const eventKey = value.eventId === null
414
+ ? null
415
+ : sha256Digest([value.host, value.event, value.sessionId ?? '', value.eventId].join('\0'));
416
+ const directory = join(root, '.pappra', 'local', 'host-events', value.host);
417
+ if (!(await privateSegmentsSafe(root, directory))) {
418
+ return result({
419
+ ok: false,
420
+ status: 'failed',
421
+ host: value.host,
422
+ event: value.event,
423
+ eventKey,
424
+ duplicate: false,
425
+ context: null,
426
+ diagnostics: [
427
+ diagnostic(diagnosticCodes.host_hook_cwd_invalid, `Private host-event namespace contains a symlink or non-directory ancestor: ${directory}`, directory, 'Inspect or remove the redirected path; no event was recorded.'),
428
+ ],
429
+ });
430
+ }
431
+ const boundary = await ensureStatePathBoundary(root, directory);
432
+ if (!boundary.ok) {
433
+ return result({
434
+ ok: false,
435
+ status: 'failed',
436
+ host: value.host,
437
+ event: value.event,
438
+ eventKey,
439
+ duplicate: false,
440
+ context: null,
441
+ diagnostics: [
442
+ diagnostic(boundary.code, boundary.message, directory, boundary.nextAction ?? 'Inspect the private host-event path.'),
443
+ ],
444
+ });
445
+ }
446
+ const directoryBefore = await observePath(directory);
447
+ if (directoryBefore.kind === 'symlink' ||
448
+ directoryBefore.kind === 'file' ||
449
+ directoryBefore.kind === 'other') {
450
+ return result({
451
+ ok: false,
452
+ status: 'failed',
453
+ host: value.host,
454
+ event: value.event,
455
+ eventKey,
456
+ duplicate: false,
457
+ context: null,
458
+ diagnostics: [
459
+ diagnostic(diagnosticCodes.host_hook_cwd_invalid, `Private host-event path is not a private directory: ${directory}`, directory, 'Inspect or remove the redirected path; no event was recorded.'),
460
+ ],
461
+ });
462
+ }
463
+ await mkdir(directory, { recursive: true, mode: 0o700 });
464
+ const directoryAfter = await observePath(directory);
465
+ const realDirectory = await resolveRealpath(directory);
466
+ const privateAfter = await privateSegmentsSafe(root, directory);
467
+ if (directoryAfter.kind !== 'directory' ||
468
+ directoryAfter.stats === null ||
469
+ realDirectory === null ||
470
+ !privateAfter ||
471
+ !isPathInside(root, realDirectory)) {
472
+ return result({
473
+ ok: false,
474
+ status: 'failed',
475
+ host: value.host,
476
+ event: value.event,
477
+ eventKey,
478
+ duplicate: false,
479
+ context: null,
480
+ diagnostics: [
481
+ diagnostic(diagnosticCodes.host_hook_cwd_invalid, `Private host-event path resolves outside the workspace: ${directory}`, directory, 'Inspect the redirected path; no event was recorded.'),
482
+ ],
483
+ });
484
+ }
485
+ const namespace = {
486
+ root,
487
+ directory,
488
+ real: realDirectory,
489
+ dev: directoryAfter.stats.dev,
490
+ ino: directoryAfter.stats.ino,
491
+ };
492
+ const context = await boundedAdvisory(root, value.event, value.taskId, workspaceRead.value.name);
493
+ if (clock() - started > deadline) {
494
+ return result({
495
+ ok: false,
496
+ status: 'timeout',
497
+ host: value.host,
498
+ event: value.event,
499
+ eventKey,
500
+ duplicate: false,
501
+ context: null,
502
+ diagnostics: [
503
+ diagnostic(diagnosticCodes.host_hook_timeout, 'Hook exceeded its bounded local deadline before publication.', value.cwd, 'No event was recorded; retry from the native host or use the manual CLI.'),
504
+ ],
505
+ });
506
+ }
507
+ const fileName = eventKey === null
508
+ ? `${sha256Digest(`${value.observedAt}\0${randomUUID()}`).slice(7)}.json`
509
+ : `${eventKey.slice(7)}.json`;
510
+ const path = join(directory, fileName);
511
+ const record = Buffer.from(`${JSON.stringify({
512
+ schema: 'pappra/host-event/1',
513
+ host: value.host,
514
+ event: value.event,
515
+ eventKey,
516
+ nativeEvent: value.nativeEvent,
517
+ eventId: value.eventId,
518
+ sessionId: value.sessionId,
519
+ cwd: realCwd,
520
+ root,
521
+ source: value.source,
522
+ observedAt: value.observedAt,
523
+ }, null, 2)}\n`);
524
+ if (record.length > DEFAULT_MAX_OUTPUT_BYTES) {
525
+ return result({
526
+ ok: false,
527
+ status: 'invalid',
528
+ host: value.host,
529
+ event: value.event,
530
+ eventKey,
531
+ duplicate: false,
532
+ context: null,
533
+ diagnostics: [
534
+ diagnostic(diagnosticCodes.host_hook_input_invalid, 'Host hook event record exceeds the private output bound.', path, 'Reduce event identifiers; no event was recorded.'),
535
+ ],
536
+ });
537
+ }
538
+ const temporary = `${path}.tmp-${randomUUID()}`;
539
+ let owned = null;
540
+ try {
541
+ const handle = await open(temporary, 'wx', 0o600);
542
+ try {
543
+ const stats = await handle.stat();
544
+ if (!stats.isFile())
545
+ throw new Error('event temporary is not a file');
546
+ owned = {
547
+ dev: stats.dev,
548
+ ino: stats.ino,
549
+ expected: record,
550
+ writeCompleted: false,
551
+ };
552
+ await handle.writeFile(record);
553
+ owned.writeCompleted = true;
554
+ await handle.sync();
555
+ }
556
+ finally {
557
+ await handle.close().catch(() => undefined);
558
+ }
559
+ if (!(await eventTempStillOwned(temporary, owned, namespace, true))) {
560
+ const residual = await removeOwnedEventTemp(temporary, owned, namespace);
561
+ return result({
562
+ ok: false,
563
+ status: 'failed',
564
+ host: value.host,
565
+ event: value.event,
566
+ eventKey,
567
+ duplicate: false,
568
+ context: null,
569
+ diagnostics: [
570
+ diagnostic(diagnosticCodes.host_hook_failed, `Private host-event namespace, temporary identity, or complete content changed during write; nothing was published: ${temporary}`, residual ?? temporary, residual === null
571
+ ? 'Inspect the workspace-private path; no shared file was adopted.'
572
+ : `Inspect the preserved/unknown temporary at ${residual}; it was not read or deleted through a redirected namespace.`),
573
+ ],
574
+ });
575
+ }
576
+ if (clock() - started > deadline) {
577
+ const residual = await removeOwnedEventTemp(temporary, owned, namespace);
578
+ return result({
579
+ ok: false,
580
+ status: 'timeout',
581
+ host: value.host,
582
+ event: value.event,
583
+ eventKey,
584
+ duplicate: false,
585
+ context: null,
586
+ diagnostics: [
587
+ diagnostic(diagnosticCodes.host_hook_timeout, 'Hook exceeded its bounded local deadline during event write; the record was not published.', residual ?? path, residual === null
588
+ ? 'Retry from the native host or use the manual CLI.'
589
+ : `Inspect the preserved temporary: ${residual}.`),
590
+ ],
591
+ });
592
+ }
593
+ try {
594
+ await link(temporary, path);
595
+ }
596
+ catch (error) {
597
+ const code = typeof error === 'object' &&
598
+ error !== null &&
599
+ 'code' in error &&
600
+ typeof error.code === 'string'
601
+ ? error.code
602
+ : null;
603
+ if (code === 'EEXIST' && eventKey !== null) {
604
+ const existingObserved = await observePath(path);
605
+ const namespaceSafe = await eventNamespaceSafe(namespace);
606
+ const existing = existingObserved.kind === 'file' && namespaceSafe
607
+ ? await readFileBounded(path, 8192)
608
+ : null;
609
+ const complete = existing !== null && existing.ok
610
+ ? parseCompleteEventRecord(existing.value.bytes, {
611
+ key: eventKey,
612
+ host: value.host,
613
+ event: value.event,
614
+ eventId: value.eventId,
615
+ sessionId: value.sessionId,
616
+ cwd: realCwd,
617
+ root,
618
+ nativeEvent: value.nativeEvent,
619
+ source: value.source,
620
+ })
621
+ : null;
622
+ const residual = await removeOwnedEventTemp(temporary, owned, namespace);
623
+ if (complete !== null) {
624
+ return result({
625
+ ok: true,
626
+ status: 'duplicate',
627
+ host: value.host,
628
+ event: value.event,
629
+ eventKey,
630
+ duplicate: true,
631
+ context: null,
632
+ diagnostics: [],
633
+ });
634
+ }
635
+ return result({
636
+ ok: false,
637
+ status: 'failed',
638
+ host: value.host,
639
+ event: value.event,
640
+ eventKey,
641
+ duplicate: false,
642
+ context: null,
643
+ diagnostics: [
644
+ diagnostic(diagnosticCodes.host_hook_failed, `Existing host-event record is incomplete, foreign or invalid; it was preserved: ${path}`, path, residual === null
645
+ ? 'Inspect the existing record; the new temporary was removed safely.'
646
+ : `Inspect both preserved paths: ${path} and ${residual}.`),
647
+ ],
648
+ });
649
+ }
650
+ const residual = await removeOwnedEventTemp(temporary, owned, namespace);
651
+ return result({
652
+ ok: false,
653
+ status: 'failed',
654
+ host: value.host,
655
+ event: value.event,
656
+ eventKey,
657
+ duplicate: false,
658
+ context: null,
659
+ diagnostics: [
660
+ diagnostic(diagnosticCodes.host_hook_failed, `Cannot publish host lifecycle event: ${error instanceof Error ? error.message : String(error)}`, residual ?? path, residual === null
661
+ ? 'Inspect the private host-event directory; no complete event was claimed.'
662
+ : `Inspect the preserved temporary: ${residual}.`),
663
+ ],
664
+ });
665
+ }
666
+ if (clock() - started > deadline) {
667
+ const residual = await removeOwnedEventTemp(temporary, owned, namespace);
668
+ return result({
669
+ ok: true,
670
+ status: 'processed',
671
+ host: value.host,
672
+ event: value.event,
673
+ eventKey,
674
+ duplicate: false,
675
+ context,
676
+ diagnostics: [
677
+ diagnostic(diagnosticCodes.host_hook_timeout, 'Hook exceeded its bounded local deadline during publication; the complete record is committed.', residual ?? path, residual === null
678
+ ? 'The native host received the advisory after the local deadline; no temporary remains.'
679
+ : `Inspect the preserved temporary: ${residual}.`),
680
+ ],
681
+ });
682
+ }
683
+ const residual = await removeOwnedEventTemp(temporary, owned, namespace);
684
+ if (residual !== null) {
685
+ return result({
686
+ ok: true,
687
+ status: 'processed',
688
+ host: value.host,
689
+ event: value.event,
690
+ eventKey,
691
+ duplicate: false,
692
+ context,
693
+ diagnostics: [
694
+ diagnostic(diagnosticCodes.host_hook_failed, `Host-event record was published but its temporary remains: ${residual}`, residual, 'Inspect the preserved temporary; the complete record is already present.'),
695
+ ],
696
+ });
697
+ }
698
+ return result({
699
+ ok: true,
700
+ status: 'processed',
701
+ host: value.host,
702
+ event: value.event,
703
+ eventKey,
704
+ duplicate: false,
705
+ context,
706
+ diagnostics: [],
707
+ });
708
+ }
709
+ catch (error) {
710
+ const namespaceSafe = await eventNamespaceSafe(namespace);
711
+ const observedTemporary = namespaceSafe
712
+ ? await observePath(temporary)
713
+ : null;
714
+ const residual = namespaceSafe && observedTemporary?.kind === 'missing' ? null : temporary;
715
+ return result({
716
+ ok: false,
717
+ status: 'failed',
718
+ host: value.host,
719
+ event: value.event,
720
+ eventKey,
721
+ duplicate: false,
722
+ context: null,
723
+ diagnostics: [
724
+ diagnostic(diagnosticCodes.host_hook_failed, `Cannot write host lifecycle event: ${error instanceof Error ? error.message : String(error)}`, residual ?? path, residual === null
725
+ ? 'Inspect the private host-event directory; no complete event was claimed.'
726
+ : `Inspect the preserved temporary: ${residual}.`),
727
+ ],
728
+ });
729
+ }
730
+ }
731
+ export async function readHookInput(stream, maxBytes = DEFAULT_MAX_INPUT_BYTES, deadlineMs = DEFAULT_DEADLINE_MS) {
732
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs < 1) {
733
+ return {
734
+ ok: false,
735
+ diagnostic: diagnostic(diagnosticCodes.host_hook_input_invalid, 'Hook input deadline must be a positive integer.', 'host hook stdin', 'No local event was recorded.'),
736
+ };
737
+ }
738
+ const chunks = [];
739
+ let total = 0;
740
+ const iterator = stream[Symbol.asyncIterator]();
741
+ const timeout = Symbol('hook-input-timeout');
742
+ let timer;
743
+ const deadline = new Promise((resolve) => {
744
+ timer = setTimeout(() => resolve(timeout), deadlineMs);
745
+ });
746
+ try {
747
+ for (;;) {
748
+ const next = await Promise.race([iterator.next(), deadline]);
749
+ if (next === timeout) {
750
+ void iterator.return?.().catch(() => undefined);
751
+ return {
752
+ ok: false,
753
+ diagnostic: diagnostic(diagnosticCodes.host_hook_timeout, `Host hook input did not finish within ${deadlineMs} ms.`, 'host hook stdin', 'No local event was recorded; the host may report its own timeout.'),
754
+ };
755
+ }
756
+ if (next.done)
757
+ break;
758
+ const bytes = typeof next.value === 'string'
759
+ ? Buffer.from(next.value)
760
+ : Buffer.from(next.value);
761
+ total += bytes.length;
762
+ if (total > maxBytes) {
763
+ void iterator.return?.().catch(() => undefined);
764
+ return {
765
+ ok: false,
766
+ diagnostic: diagnostic(diagnosticCodes.host_hook_input_invalid, `Host hook input exceeds ${maxBytes} bytes.`, 'host hook stdin', 'No local event was recorded.'),
767
+ };
768
+ }
769
+ chunks.push(bytes);
770
+ }
771
+ }
772
+ finally {
773
+ if (timer !== undefined)
774
+ clearTimeout(timer);
775
+ }
776
+ return { ok: true, bytes: Buffer.concat(chunks) };
777
+ }
778
+ export async function readBoundedHookFile(path) {
779
+ const read = await readFileBounded(path, DEFAULT_MAX_INPUT_BYTES);
780
+ return read.ok ? read.value.bytes : null;
781
+ }