@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,1258 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, unlink } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
5
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
|
+
import { checkpointPointerSchema, checkpointSchema, resumePacketSchema, taskContextSchema, } from '../schemas/continuity.js';
|
|
7
|
+
import { errorMessage, } from '../schemas/outcome.js';
|
|
8
|
+
import { isPathInside } from '../schemas/paths.js';
|
|
9
|
+
import { isTaskRecord } from '../collab/validation.js';
|
|
10
|
+
import { observePath, readFileBounded, readTextFileBounded, sha256Digest, } from '../infra/fs.js';
|
|
11
|
+
import { loadContinuityWorkspace, snapshotRepository, } from './context.js';
|
|
12
|
+
import { continuityTaskPaths, ensureContinuityBoundary, ensurePrivateTaskBoundary, readArtifactPath, readContinuityBytes, replaceContinuityBytesIfDigest, writeContinuityBytes, } from './paths.js';
|
|
13
|
+
const MAX_ARTIFACT_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const MAX_RECORD_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
function diag(code, message, source, hint) {
|
|
16
|
+
return createDiagnostic({ code, severity: 'error', message, source, hint });
|
|
17
|
+
}
|
|
18
|
+
function outcomeDiagnostic(failure, source) {
|
|
19
|
+
return createDiagnostic({
|
|
20
|
+
code: failure.code,
|
|
21
|
+
severity: 'error',
|
|
22
|
+
message: failure.message,
|
|
23
|
+
source,
|
|
24
|
+
hint: failure.nextAction,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function parseJsonFile(root, taskId, path, schema) {
|
|
28
|
+
return (async () => {
|
|
29
|
+
const boundary = await ensurePrivateTaskBoundary(root, taskId, path);
|
|
30
|
+
if (!boundary.ok) {
|
|
31
|
+
return {
|
|
32
|
+
exists: true,
|
|
33
|
+
value: null,
|
|
34
|
+
raw: null,
|
|
35
|
+
digest: null,
|
|
36
|
+
diagnostics: [outcomeDiagnostic(boundary, path)],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const read = await readTextFileBounded(path, MAX_RECORD_BYTES);
|
|
40
|
+
if (!read.ok) {
|
|
41
|
+
if (read.code === diagnosticCodes.fileNotFound) {
|
|
42
|
+
return {
|
|
43
|
+
exists: false,
|
|
44
|
+
value: null,
|
|
45
|
+
raw: null,
|
|
46
|
+
digest: null,
|
|
47
|
+
diagnostics: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
exists: true,
|
|
52
|
+
value: null,
|
|
53
|
+
raw: null,
|
|
54
|
+
digest: null,
|
|
55
|
+
diagnostics: [
|
|
56
|
+
diag(read.code, read.message, path, read.nextAction ?? 'Inspect the record.'),
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = JSON.parse(read.value.text);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
exists: true,
|
|
67
|
+
value: null,
|
|
68
|
+
raw: read.value.bytes,
|
|
69
|
+
digest: read.value.digest,
|
|
70
|
+
diagnostics: [
|
|
71
|
+
diag(diagnosticCodes.configInvalidJson, `Invalid JSON: ${errorMessage(error)}`, path, 'Repair the private continuity record; it is not overwritten.'),
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const parsed = schema.safeParse(raw);
|
|
76
|
+
if (!parsed.success) {
|
|
77
|
+
return {
|
|
78
|
+
exists: true,
|
|
79
|
+
value: null,
|
|
80
|
+
raw: read.value.bytes,
|
|
81
|
+
digest: read.value.digest,
|
|
82
|
+
diagnostics: [
|
|
83
|
+
diag(diagnosticCodes.configSchemaInvalid, `Invalid continuity record: ${parsed.error?.issues[0]?.message ?? 'unknown issue'}`, path, 'Inspect the private continuity record; no recovery is guessed.'),
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
exists: true,
|
|
89
|
+
value: parsed.data ?? null,
|
|
90
|
+
raw: read.value.bytes,
|
|
91
|
+
digest: read.value.digest,
|
|
92
|
+
diagnostics: [],
|
|
93
|
+
};
|
|
94
|
+
})();
|
|
95
|
+
}
|
|
96
|
+
async function readTaskContext(root, taskId) {
|
|
97
|
+
const paths = continuityTaskPaths(root, taskId);
|
|
98
|
+
return parseJsonFile(root, taskId, paths.contextPath, taskContextSchema);
|
|
99
|
+
}
|
|
100
|
+
async function readPointer(root, taskId) {
|
|
101
|
+
const paths = continuityTaskPaths(root, taskId);
|
|
102
|
+
return parseJsonFile(root, taskId, paths.latestPath, checkpointPointerSchema);
|
|
103
|
+
}
|
|
104
|
+
function checkpointAbsolutePath(root, taskId, ref) {
|
|
105
|
+
const paths = continuityTaskPaths(root, taskId);
|
|
106
|
+
const candidate = isAbsolute(ref)
|
|
107
|
+
? resolve(ref)
|
|
108
|
+
: resolve(paths.taskDir, ref);
|
|
109
|
+
return isPathInside(paths.taskDir, candidate) ? candidate : null;
|
|
110
|
+
}
|
|
111
|
+
async function readCheckpoint(root, taskId, pointer) {
|
|
112
|
+
const path = checkpointAbsolutePath(root, taskId, pointer.checkpointRef);
|
|
113
|
+
if (path === null) {
|
|
114
|
+
return {
|
|
115
|
+
exists: true,
|
|
116
|
+
value: null,
|
|
117
|
+
raw: null,
|
|
118
|
+
digest: null,
|
|
119
|
+
diagnostics: [
|
|
120
|
+
diag(diagnosticCodes.document_path_unsafe, `Checkpoint pointer escapes the private task directory: ${pointer.checkpointRef}`, pointer.checkpointRef, 'Inspect the latest pointer; it is not followed.'),
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const read = await parseJsonFile(root, taskId, path, checkpointSchema);
|
|
125
|
+
if (read.exists && read.digest !== pointer.checkpointDigest) {
|
|
126
|
+
return {
|
|
127
|
+
...read,
|
|
128
|
+
value: null,
|
|
129
|
+
diagnostics: [
|
|
130
|
+
...read.diagnostics,
|
|
131
|
+
diag(diagnosticCodes.configSchemaInvalid, 'Checkpoint bytes do not match the latest pointer digest.', path, 'Inspect the pointer/checkpoint; Pappra does not guess another task or checkpoint.'),
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return read;
|
|
136
|
+
}
|
|
137
|
+
async function acquireTaskLock(root, taskId) {
|
|
138
|
+
const paths = continuityTaskPaths(root, taskId);
|
|
139
|
+
const boundary = await ensurePrivateTaskBoundary(root, taskId, paths.lockPath);
|
|
140
|
+
if (!boundary.ok) {
|
|
141
|
+
return { ok: false, path: paths.lockPath, message: boundary.message };
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
await mkdir(paths.taskDir, { recursive: true, mode: 0o700 });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
path: paths.lockPath,
|
|
150
|
+
message: `Cannot create private task directory: ${errorMessage(error)}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const token = randomUUID();
|
|
154
|
+
let handle;
|
|
155
|
+
try {
|
|
156
|
+
handle = await open(paths.lockPath, 'wx', 0o600);
|
|
157
|
+
await handle.writeFile(Buffer.from(`${JSON.stringify({ token, pid: process.pid, createdAt: new Date().toISOString() })}\n`));
|
|
158
|
+
await handle.sync();
|
|
159
|
+
await handle.close();
|
|
160
|
+
handle = undefined;
|
|
161
|
+
return { ok: true, path: paths.lockPath, token };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
await handle?.close().catch(() => undefined);
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
path: paths.lockPath,
|
|
168
|
+
message: `Checkpoint writer is already locked or unavailable: ${errorMessage(error)}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function releaseTaskLock(path, token) {
|
|
173
|
+
const read = await readTextFileBounded(path);
|
|
174
|
+
if (!read.ok) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
code: diagnosticCodes.document_locked,
|
|
178
|
+
message: `Cannot verify checkpoint lock: ${read.message}`,
|
|
179
|
+
retryable: false,
|
|
180
|
+
nextAction: 'Inspect the private task lock; it was not removed.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
let value;
|
|
184
|
+
try {
|
|
185
|
+
value = JSON.parse(read.value.text);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
code: diagnosticCodes.document_locked,
|
|
191
|
+
message: 'Checkpoint lock is not valid JSON.',
|
|
192
|
+
retryable: false,
|
|
193
|
+
nextAction: 'Inspect the private task lock; it was not removed.',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (typeof value !== 'object' ||
|
|
197
|
+
value === null ||
|
|
198
|
+
!('token' in value) ||
|
|
199
|
+
value.token !== token) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
code: diagnosticCodes.document_locked,
|
|
203
|
+
message: 'Checkpoint lock ownership changed.',
|
|
204
|
+
retryable: false,
|
|
205
|
+
nextAction: 'Inspect the private task lock; it was not removed.',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
await unlink(path);
|
|
210
|
+
return { ok: true, value: null };
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
code: diagnosticCodes.document_locked,
|
|
216
|
+
message: `Cannot release checkpoint lock: ${errorMessage(error)}`,
|
|
217
|
+
retryable: false,
|
|
218
|
+
nextAction: 'Inspect the private task lock; it remains as evidence.',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function ensureTaskContext(workspace, input, now) {
|
|
223
|
+
const read = await readTaskContext(workspace.root, input.taskId);
|
|
224
|
+
if (read.exists) {
|
|
225
|
+
if (read.value === null) {
|
|
226
|
+
return { ok: false, diagnostics: [...read.diagnostics] };
|
|
227
|
+
}
|
|
228
|
+
if (read.value.taskId !== input.taskId ||
|
|
229
|
+
read.value.workspaceId !== workspace.config.workspaceId) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
diagnostics: [
|
|
233
|
+
diag(diagnosticCodes.document_catalog_conflict, 'Task context identity does not match the requested task/workspace.', continuityTaskPaths(workspace.root, input.taskId).contextPath, 'Use the exact task identity; Pappra does not reuse a context from another task.'),
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (input.objective !== undefined &&
|
|
238
|
+
input.objective !== read.value.objective) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
diagnostics: [
|
|
242
|
+
diag(diagnosticCodes.document_write_conflict, 'Task context objective is immutable for an existing task.', continuityTaskPaths(workspace.root, input.taskId).contextPath, 'Reuse the existing task or choose a new task id.'),
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return { ok: true, context: read.value };
|
|
247
|
+
}
|
|
248
|
+
if (input.objective === undefined || input.acceptance === undefined) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
diagnostics: [
|
|
252
|
+
diag(diagnosticCodes.initInputRequired, 'A new task context requires objective and acceptance.', continuityTaskPaths(workspace.root, input.taskId).contextPath, 'Provide the task goal and acceptance criteria on the first checkpoint.'),
|
|
253
|
+
],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const paths = continuityTaskPaths(workspace.root, input.taskId);
|
|
257
|
+
const context = {
|
|
258
|
+
schema: 'pappra/task-context/1',
|
|
259
|
+
taskId: input.taskId,
|
|
260
|
+
workspaceId: workspace.config.workspaceId,
|
|
261
|
+
objective: input.objective,
|
|
262
|
+
acceptance: [...input.acceptance],
|
|
263
|
+
constraints: [...(input.constraints ?? [])],
|
|
264
|
+
artifacts: [...(input.artifacts ?? [])],
|
|
265
|
+
collaborationRecordRef: input.collaborationRecordRef ?? null,
|
|
266
|
+
createdAt: now,
|
|
267
|
+
updatedAt: now,
|
|
268
|
+
};
|
|
269
|
+
const parsed = taskContextSchema.safeParse(context);
|
|
270
|
+
if (!parsed.success) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
diagnostics: [
|
|
274
|
+
diag(diagnosticCodes.document_record_invalid, `Task context is invalid: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`, paths.contextPath, 'Fix the task context inputs.'),
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const written = await writeContinuityBytes(workspace.root, input.taskId, paths.contextPath, Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`), true);
|
|
279
|
+
if (!written.ok)
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
diagnostics: [outcomeDiagnostic(written, paths.contextPath)],
|
|
283
|
+
};
|
|
284
|
+
return { ok: true, context: parsed.data };
|
|
285
|
+
}
|
|
286
|
+
async function writePrivatePatch(workspace, taskId, bytes, digest) {
|
|
287
|
+
const paths = continuityTaskPaths(workspace.root, taskId);
|
|
288
|
+
const ref = `patches/${digest.slice('sha256:'.length)}.patch`;
|
|
289
|
+
const path = join(paths.taskDir, ref);
|
|
290
|
+
const existing = await observePath(path);
|
|
291
|
+
if (existing.kind === 'file') {
|
|
292
|
+
const read = await readContinuityBytes(workspace.root, taskId, path);
|
|
293
|
+
if (read.ok && sha256Digest(read.value) === digest)
|
|
294
|
+
return { ok: true, value: path };
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
code: diagnosticCodes.document_write_conflict,
|
|
298
|
+
message: `Existing private patch does not match its digest: ${path}`,
|
|
299
|
+
retryable: false,
|
|
300
|
+
nextAction: 'Inspect the private patch; it is not replaced automatically.',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
const written = await writeContinuityBytes(workspace.root, taskId, path, bytes, true);
|
|
304
|
+
return written.ok ? { ok: true, value: path } : written;
|
|
305
|
+
}
|
|
306
|
+
async function collectArtifacts(workspace, context, input) {
|
|
307
|
+
const paths = input.artifacts ?? context.artifacts;
|
|
308
|
+
const artifacts = [];
|
|
309
|
+
const observedAt = new Date().toISOString();
|
|
310
|
+
for (const artifactPath of paths) {
|
|
311
|
+
const resolved = await readArtifactPath(workspace.root, artifactPath);
|
|
312
|
+
if (!resolved.ok)
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
diagnostics: [outcomeDiagnostic(resolved, artifactPath)],
|
|
316
|
+
};
|
|
317
|
+
const read = await readFileBounded(resolved.value, MAX_ARTIFACT_BYTES);
|
|
318
|
+
if (!read.ok)
|
|
319
|
+
return {
|
|
320
|
+
ok: false,
|
|
321
|
+
diagnostics: [outcomeDiagnostic(read, artifactPath)],
|
|
322
|
+
};
|
|
323
|
+
artifacts.push({
|
|
324
|
+
path: artifactPath,
|
|
325
|
+
digest: read.value.digest,
|
|
326
|
+
inputVersion: null,
|
|
327
|
+
observedAt,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return { ok: true, artifacts };
|
|
331
|
+
}
|
|
332
|
+
function nativeRefs(input, now) {
|
|
333
|
+
return (input.nativeRefs ?? []).map((ref) => ({
|
|
334
|
+
harness: ref.harness,
|
|
335
|
+
sessionId: ref.sessionId ?? null,
|
|
336
|
+
turnId: ref.turnId ?? null,
|
|
337
|
+
runId: ref.runId ?? null,
|
|
338
|
+
scope: 'stored-snapshot',
|
|
339
|
+
observedAt: now,
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
async function observeCollaboration(checkpoint, workspaceRoot) {
|
|
343
|
+
const ref = checkpoint.collaborationRecordRef;
|
|
344
|
+
const emptyAuthority = {
|
|
345
|
+
valid: false,
|
|
346
|
+
stateRevision: null,
|
|
347
|
+
ownerEpoch: null,
|
|
348
|
+
dispatchFrozen: null,
|
|
349
|
+
activeAttempts: null,
|
|
350
|
+
unreleasedClaims: null,
|
|
351
|
+
unresolvedUncertainty: null,
|
|
352
|
+
pendingApprovals: null,
|
|
353
|
+
blockerCodes: [],
|
|
354
|
+
};
|
|
355
|
+
if (ref === null) {
|
|
356
|
+
return {
|
|
357
|
+
ref: null,
|
|
358
|
+
status: 'not-referenced',
|
|
359
|
+
digest: null,
|
|
360
|
+
authority: { ...emptyAuthority, blockerCodes: [] },
|
|
361
|
+
scope: 'persisted-history',
|
|
362
|
+
liveStatus: 'unknown',
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
let path;
|
|
366
|
+
if (isAbsolute(ref)) {
|
|
367
|
+
path = ref;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
path = resolve(workspaceRoot, ref);
|
|
371
|
+
const boundary = await ensureContinuityBoundary(workspaceRoot, path);
|
|
372
|
+
if (!boundary.ok) {
|
|
373
|
+
return {
|
|
374
|
+
ref,
|
|
375
|
+
status: 'unreadable',
|
|
376
|
+
digest: null,
|
|
377
|
+
authority: { ...emptyAuthority, blockerCodes: [] },
|
|
378
|
+
scope: 'persisted-history',
|
|
379
|
+
liveStatus: 'unknown',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const observation = await observePath(path).catch(() => null);
|
|
384
|
+
if (observation?.kind === 'directory')
|
|
385
|
+
path = join(path, 'task.json');
|
|
386
|
+
const read = await readTextFileBounded(path, MAX_RECORD_BYTES);
|
|
387
|
+
if (!read.ok) {
|
|
388
|
+
return {
|
|
389
|
+
ref,
|
|
390
|
+
status: read.code === diagnosticCodes.fileNotFound ? 'missing' : 'unreadable',
|
|
391
|
+
digest: null,
|
|
392
|
+
authority: { ...emptyAuthority, blockerCodes: [] },
|
|
393
|
+
scope: 'persisted-history',
|
|
394
|
+
liveStatus: 'unknown',
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
let raw;
|
|
398
|
+
try {
|
|
399
|
+
raw = JSON.parse(read.value.text);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return {
|
|
403
|
+
ref,
|
|
404
|
+
status: 'unreadable',
|
|
405
|
+
digest: read.value.digest,
|
|
406
|
+
authority: { ...emptyAuthority, blockerCodes: [] },
|
|
407
|
+
scope: 'persisted-history',
|
|
408
|
+
liveStatus: 'unknown',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (!isTaskRecord(raw)) {
|
|
412
|
+
return {
|
|
413
|
+
ref,
|
|
414
|
+
status: 'unsupported',
|
|
415
|
+
digest: read.value.digest,
|
|
416
|
+
authority: {
|
|
417
|
+
...emptyAuthority,
|
|
418
|
+
blockerCodes: ['collaboration_authority_invalid'],
|
|
419
|
+
},
|
|
420
|
+
scope: 'persisted-history',
|
|
421
|
+
liveStatus: 'unknown',
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const record = raw;
|
|
425
|
+
const activeAttempts = record.attempts.filter((attempt) => ['assigned', 'accepted', 'running', 'unknown'].includes(attempt.state)).length;
|
|
426
|
+
const pendingApprovals = record.attempts.filter((attempt) => attempt.state === 'needs_input' && attempt.awaiting === 'approval').length;
|
|
427
|
+
const unreleasedClaims = record.claims.filter((claim) => claim.released_at === null).length;
|
|
428
|
+
const unresolvedUncertainty = record.uncertainty.filter((entry) => entry.resolved_at === null).length;
|
|
429
|
+
const blockerCodes = [];
|
|
430
|
+
if (record.dispatch_frozen === true)
|
|
431
|
+
blockerCodes.push('dispatch_frozen');
|
|
432
|
+
if (activeAttempts > 0)
|
|
433
|
+
blockerCodes.push('active_attempt');
|
|
434
|
+
if (pendingApprovals > 0)
|
|
435
|
+
blockerCodes.push('pending_approval');
|
|
436
|
+
if (unreleasedClaims > 0)
|
|
437
|
+
blockerCodes.push('active_claim');
|
|
438
|
+
if (unresolvedUncertainty > 0)
|
|
439
|
+
blockerCodes.push('unresolved_uncertainty');
|
|
440
|
+
return {
|
|
441
|
+
ref,
|
|
442
|
+
status: 'read',
|
|
443
|
+
digest: read.value.digest,
|
|
444
|
+
authority: {
|
|
445
|
+
valid: true,
|
|
446
|
+
stateRevision: record.state_revision,
|
|
447
|
+
ownerEpoch: record.coordinator?.owner_epoch ?? null,
|
|
448
|
+
dispatchFrozen: record.dispatch_frozen ?? null,
|
|
449
|
+
activeAttempts,
|
|
450
|
+
unreleasedClaims,
|
|
451
|
+
unresolvedUncertainty,
|
|
452
|
+
pendingApprovals,
|
|
453
|
+
blockerCodes,
|
|
454
|
+
},
|
|
455
|
+
scope: 'persisted-history',
|
|
456
|
+
liveStatus: 'unknown',
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function boundedArray(values) {
|
|
460
|
+
return values.length > 16
|
|
461
|
+
? { values: values.slice(0, 16), truncated: true }
|
|
462
|
+
: { values: [...values], truncated: false };
|
|
463
|
+
}
|
|
464
|
+
export class CheckpointService {
|
|
465
|
+
root;
|
|
466
|
+
#options;
|
|
467
|
+
constructor(root, options = {}) {
|
|
468
|
+
this.root = root;
|
|
469
|
+
this.#options = options;
|
|
470
|
+
}
|
|
471
|
+
async save(input) {
|
|
472
|
+
const loaded = await loadContinuityWorkspace(this.root);
|
|
473
|
+
if (!loaded.ok || loaded.workspace === null) {
|
|
474
|
+
return {
|
|
475
|
+
ok: false,
|
|
476
|
+
status: 'failed',
|
|
477
|
+
checkpoint: null,
|
|
478
|
+
checkpointPath: null,
|
|
479
|
+
latestPath: null,
|
|
480
|
+
diagnostics: [...loaded.diagnostics],
|
|
481
|
+
residualPaths: [],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const workspace = loaded.workspace;
|
|
485
|
+
let lock;
|
|
486
|
+
try {
|
|
487
|
+
lock = await acquireTaskLock(workspace.root, input.taskId);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
return {
|
|
491
|
+
ok: false,
|
|
492
|
+
status: 'failed',
|
|
493
|
+
checkpoint: null,
|
|
494
|
+
checkpointPath: null,
|
|
495
|
+
latestPath: null,
|
|
496
|
+
diagnostics: [
|
|
497
|
+
diag(diagnosticCodes.document_record_invalid, `Invalid task id: ${errorMessage(error)}`, workspace.root, 'Use a stable task id with letters, digits, dot, underscore, or dash.'),
|
|
498
|
+
],
|
|
499
|
+
residualPaths: [],
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (!lock.ok) {
|
|
503
|
+
return {
|
|
504
|
+
ok: false,
|
|
505
|
+
status: 'conflict',
|
|
506
|
+
checkpoint: null,
|
|
507
|
+
checkpointPath: null,
|
|
508
|
+
latestPath: null,
|
|
509
|
+
diagnostics: [
|
|
510
|
+
diag(diagnosticCodes.document_locked, lock.message, lock.path, 'Retry after the current checkpoint writer finishes.'),
|
|
511
|
+
],
|
|
512
|
+
residualPaths: [],
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const paths = continuityTaskPaths(workspace.root, input.taskId);
|
|
516
|
+
const now = new Date().toISOString();
|
|
517
|
+
let checkpointPath = null;
|
|
518
|
+
let result;
|
|
519
|
+
const saveDiagnostics = [...workspace.diagnostics];
|
|
520
|
+
try {
|
|
521
|
+
const contextResult = await ensureTaskContext(workspace, input, now);
|
|
522
|
+
if (!contextResult.ok) {
|
|
523
|
+
result = {
|
|
524
|
+
ok: false,
|
|
525
|
+
status: 'conflict',
|
|
526
|
+
checkpoint: null,
|
|
527
|
+
checkpointPath: null,
|
|
528
|
+
latestPath: null,
|
|
529
|
+
diagnostics: contextResult.diagnostics,
|
|
530
|
+
residualPaths: [],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
const context = contextResult.context;
|
|
535
|
+
const pointerRead = await readPointer(workspace.root, input.taskId);
|
|
536
|
+
if (pointerRead.diagnostics.length > 0) {
|
|
537
|
+
result = {
|
|
538
|
+
ok: false,
|
|
539
|
+
status: 'failed',
|
|
540
|
+
checkpoint: null,
|
|
541
|
+
checkpointPath: null,
|
|
542
|
+
latestPath: paths.latestPath,
|
|
543
|
+
diagnostics: [...pointerRead.diagnostics],
|
|
544
|
+
residualPaths: [],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const pointer = pointerRead.value;
|
|
549
|
+
let previous = null;
|
|
550
|
+
if (pointer !== null) {
|
|
551
|
+
if (pointer.taskId !== input.taskId ||
|
|
552
|
+
pointer.workspaceId !== workspace.config.workspaceId) {
|
|
553
|
+
result = {
|
|
554
|
+
ok: false,
|
|
555
|
+
status: 'failed',
|
|
556
|
+
checkpoint: null,
|
|
557
|
+
checkpointPath: null,
|
|
558
|
+
latestPath: paths.latestPath,
|
|
559
|
+
diagnostics: [
|
|
560
|
+
diag(diagnosticCodes.document_proposal_stale, 'Latest pointer belongs to another task or workspace.', paths.latestPath, 'Inspect the pointer; Pappra does not reuse another task state.'),
|
|
561
|
+
],
|
|
562
|
+
residualPaths: [],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
const previousRead = await readCheckpoint(workspace.root, input.taskId, pointer);
|
|
567
|
+
if (previousRead.value === null ||
|
|
568
|
+
previousRead.diagnostics.length > 0) {
|
|
569
|
+
result = {
|
|
570
|
+
ok: false,
|
|
571
|
+
status: 'failed',
|
|
572
|
+
checkpoint: null,
|
|
573
|
+
checkpointPath: null,
|
|
574
|
+
latestPath: paths.latestPath,
|
|
575
|
+
diagnostics: [
|
|
576
|
+
...previousRead.diagnostics,
|
|
577
|
+
diag(diagnosticCodes.document_proposal_stale, 'Latest checkpoint is unreadable; Pappra does not guess a previous state.', paths.latestPath, 'Repair or explicitly remove the pointer only after inspection.'),
|
|
578
|
+
],
|
|
579
|
+
residualPaths: [],
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
else if (previousRead.value.taskId !== input.taskId ||
|
|
583
|
+
previousRead.value.workspaceId !==
|
|
584
|
+
workspace.config.workspaceId ||
|
|
585
|
+
previousRead.value.revision !== pointer.revision) {
|
|
586
|
+
result = {
|
|
587
|
+
ok: false,
|
|
588
|
+
status: 'failed',
|
|
589
|
+
checkpoint: null,
|
|
590
|
+
checkpointPath: null,
|
|
591
|
+
latestPath: paths.latestPath,
|
|
592
|
+
diagnostics: [
|
|
593
|
+
diag(diagnosticCodes.document_proposal_stale, 'Latest checkpoint identity or revision does not match its pointer.', paths.latestPath, 'Inspect the private checkpoint/pointer; no new checkpoint was written.'),
|
|
594
|
+
],
|
|
595
|
+
residualPaths: [],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
previous = previousRead.value;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (result === undefined) {
|
|
604
|
+
const observedRepositories = [];
|
|
605
|
+
for (const [index, repository,] of workspace.repositories.entries()) {
|
|
606
|
+
const snapshot = await snapshotRepository(workspace, repository, index);
|
|
607
|
+
if (snapshot.patchBytes !== null &&
|
|
608
|
+
snapshot.record.patchDigest !== null) {
|
|
609
|
+
const patch = await writePrivatePatch(workspace, input.taskId, snapshot.patchBytes, snapshot.record.patchDigest);
|
|
610
|
+
if (!patch.ok) {
|
|
611
|
+
result = {
|
|
612
|
+
ok: false,
|
|
613
|
+
status: 'failed',
|
|
614
|
+
checkpoint: null,
|
|
615
|
+
checkpointPath: null,
|
|
616
|
+
latestPath: paths.latestPath,
|
|
617
|
+
diagnostics: [outcomeDiagnostic(patch, paths.latestPath)],
|
|
618
|
+
residualPaths: [],
|
|
619
|
+
};
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
observedRepositories.push(snapshot.record);
|
|
624
|
+
}
|
|
625
|
+
if (result === undefined) {
|
|
626
|
+
const artifacts = await collectArtifacts(workspace, context, input);
|
|
627
|
+
if (!artifacts.ok) {
|
|
628
|
+
result = {
|
|
629
|
+
ok: false,
|
|
630
|
+
status: 'failed',
|
|
631
|
+
checkpoint: null,
|
|
632
|
+
checkpointPath: null,
|
|
633
|
+
latestPath: paths.latestPath,
|
|
634
|
+
diagnostics: artifacts.diagnostics,
|
|
635
|
+
residualPaths: [],
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
const revision = (previous?.revision ?? 0) + 1;
|
|
640
|
+
const checkpointId = randomUUID();
|
|
641
|
+
const semanticPresent = [
|
|
642
|
+
...(input.decisions ?? []),
|
|
643
|
+
...(input.attempted ?? []),
|
|
644
|
+
...(input.nextActions ?? []),
|
|
645
|
+
].length > 0;
|
|
646
|
+
const contextCompleteness = semanticPresent && input.contextCompleteness !== 'partial'
|
|
647
|
+
? 'complete'
|
|
648
|
+
: 'partial';
|
|
649
|
+
if (!semanticPresent) {
|
|
650
|
+
saveDiagnostics.push(diag(diagnosticCodes.document_write_partial, input.contextCompleteness === 'complete'
|
|
651
|
+
? 'Checkpoint was requested as complete without decisions, attempted work or next actions; stored as partial.'
|
|
652
|
+
: 'Checkpoint has no semantic decisions, attempted work or next actions; stored as partial.', continuityTaskPaths(workspace.root, input.taskId)
|
|
653
|
+
.contextPath, 'Add an explicit semantic summary before claiming complete recovery context.'));
|
|
654
|
+
}
|
|
655
|
+
const checkpoint = {
|
|
656
|
+
schema: 'pappra/checkpoint/1',
|
|
657
|
+
checkpointId,
|
|
658
|
+
taskId: input.taskId,
|
|
659
|
+
workspaceId: workspace.config.workspaceId,
|
|
660
|
+
revision,
|
|
661
|
+
previousDigest: previous === null
|
|
662
|
+
? null
|
|
663
|
+
: (pointerRead.value?.checkpointDigest ?? null),
|
|
664
|
+
createdAt: now,
|
|
665
|
+
authorSession: input.authorSession ?? null,
|
|
666
|
+
objective: context.objective,
|
|
667
|
+
acceptance: [...context.acceptance],
|
|
668
|
+
constraints: [...context.constraints],
|
|
669
|
+
decisions: [...(input.decisions ?? [])],
|
|
670
|
+
attempted: [...(input.attempted ?? [])],
|
|
671
|
+
nextActions: [...(input.nextActions ?? [])],
|
|
672
|
+
limitations: [...(input.limitations ?? [])],
|
|
673
|
+
repositories: observedRepositories,
|
|
674
|
+
artifacts: artifacts.artifacts,
|
|
675
|
+
validations: (input.validations ?? []).map((validation) => ({
|
|
676
|
+
id: validation.id,
|
|
677
|
+
result: validation.result,
|
|
678
|
+
inputVersion: validation.inputVersion ?? null,
|
|
679
|
+
observedAt: now,
|
|
680
|
+
})),
|
|
681
|
+
nativeRefs: nativeRefs(input, now),
|
|
682
|
+
collaborationRecordRef: context.collaborationRecordRef,
|
|
683
|
+
contextCompleteness,
|
|
684
|
+
};
|
|
685
|
+
const parsed = checkpointSchema.safeParse(checkpoint);
|
|
686
|
+
const ref = `checkpoints/${String(revision).padStart(4, '0')}-${checkpointId}.json`;
|
|
687
|
+
checkpointPath = join(paths.taskDir, ref);
|
|
688
|
+
if (!parsed.success) {
|
|
689
|
+
result = {
|
|
690
|
+
ok: false,
|
|
691
|
+
status: 'failed',
|
|
692
|
+
checkpoint: null,
|
|
693
|
+
checkpointPath: null,
|
|
694
|
+
latestPath: paths.latestPath,
|
|
695
|
+
diagnostics: [
|
|
696
|
+
diag(diagnosticCodes.document_record_invalid, `Checkpoint is invalid: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`, paths.contextPath, 'Fix the checkpoint inputs; no pointer was changed.'),
|
|
697
|
+
],
|
|
698
|
+
residualPaths: [],
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
const written = await writeContinuityBytes(workspace.root, input.taskId, checkpointPath, Buffer.from(`${JSON.stringify(parsed.data, null, 2)}\n`), true);
|
|
703
|
+
if (!written.ok) {
|
|
704
|
+
result = {
|
|
705
|
+
ok: false,
|
|
706
|
+
status: 'failed',
|
|
707
|
+
checkpoint: null,
|
|
708
|
+
checkpointPath: null,
|
|
709
|
+
latestPath: paths.latestPath,
|
|
710
|
+
diagnostics: [
|
|
711
|
+
outcomeDiagnostic(written, checkpointPath ?? paths.taskDir),
|
|
712
|
+
],
|
|
713
|
+
residualPaths: [],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
const readBack = await readContinuityBytes(workspace.root, input.taskId, checkpointPath);
|
|
718
|
+
if (!readBack.ok ||
|
|
719
|
+
sha256Digest(readBack.value) !== written.value.digest) {
|
|
720
|
+
result = {
|
|
721
|
+
ok: false,
|
|
722
|
+
status: 'partial',
|
|
723
|
+
checkpoint: null,
|
|
724
|
+
checkpointPath,
|
|
725
|
+
latestPath: paths.latestPath,
|
|
726
|
+
diagnostics: [
|
|
727
|
+
diag(diagnosticCodes.fileUnreadable, 'Checkpoint bytes could not be verified after write.', checkpointPath, 'Inspect the private checkpoint; the latest pointer was not changed.'),
|
|
728
|
+
],
|
|
729
|
+
residualPaths: [checkpointPath],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
const pointerNow = await readPointer(workspace.root, input.taskId);
|
|
734
|
+
if (pointerNow.digest !== pointerRead.digest) {
|
|
735
|
+
result = {
|
|
736
|
+
ok: false,
|
|
737
|
+
status: 'conflict',
|
|
738
|
+
checkpoint: null,
|
|
739
|
+
checkpointPath,
|
|
740
|
+
latestPath: paths.latestPath,
|
|
741
|
+
diagnostics: [
|
|
742
|
+
diag(diagnosticCodes.document_proposal_stale, 'Latest checkpoint changed while this checkpoint was being written.', paths.latestPath, 'The old pointer remains; inspect the residual checkpoint before retrying.'),
|
|
743
|
+
],
|
|
744
|
+
residualPaths: [checkpointPath],
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
try {
|
|
749
|
+
await this.#options.beforeLatestPointerPersist?.();
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
result = {
|
|
753
|
+
ok: false,
|
|
754
|
+
status: 'partial',
|
|
755
|
+
checkpoint: parsed.data,
|
|
756
|
+
checkpointPath,
|
|
757
|
+
latestPath: paths.latestPath,
|
|
758
|
+
diagnostics: [
|
|
759
|
+
diag(diagnosticCodes.fileUnreadable, `Latest pointer write interrupted: ${errorMessage(error)}`, paths.latestPath, 'The previous readable checkpoint remains authoritative; inspect the residual checkpoint.'),
|
|
760
|
+
],
|
|
761
|
+
residualPaths: [checkpointPath],
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
if (result === undefined) {
|
|
765
|
+
const pointerValue = {
|
|
766
|
+
schema: 'pappra/checkpoint-pointer/1',
|
|
767
|
+
taskId: input.taskId,
|
|
768
|
+
workspaceId: workspace.config.workspaceId,
|
|
769
|
+
revision,
|
|
770
|
+
checkpointRef: ref,
|
|
771
|
+
checkpointDigest: written.value.digest,
|
|
772
|
+
updatedAt: new Date().toISOString(),
|
|
773
|
+
};
|
|
774
|
+
const pointerWritten = await replaceContinuityBytesIfDigest(workspace.root, input.taskId, paths.latestPath, Buffer.from(`${JSON.stringify(pointerValue, null, 2)}\n`), pointerRead.digest);
|
|
775
|
+
if (!pointerWritten.ok) {
|
|
776
|
+
result = {
|
|
777
|
+
ok: false,
|
|
778
|
+
status: pointerWritten.code ===
|
|
779
|
+
diagnosticCodes.document_proposal_stale
|
|
780
|
+
? 'conflict'
|
|
781
|
+
: 'partial',
|
|
782
|
+
checkpoint: parsed.data,
|
|
783
|
+
checkpointPath,
|
|
784
|
+
latestPath: paths.latestPath,
|
|
785
|
+
diagnostics: [
|
|
786
|
+
diag(pointerWritten.code, pointerWritten.message, paths.latestPath, 'The previous/concurrent pointer was preserved; inspect the residual checkpoint.'),
|
|
787
|
+
],
|
|
788
|
+
residualPaths: [checkpointPath],
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
result = {
|
|
793
|
+
ok: true,
|
|
794
|
+
status: 'saved',
|
|
795
|
+
checkpoint: parsed.data,
|
|
796
|
+
checkpointPath,
|
|
797
|
+
latestPath: paths.latestPath,
|
|
798
|
+
diagnostics: saveDiagnostics,
|
|
799
|
+
residualPaths: [],
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
result = {
|
|
815
|
+
ok: false,
|
|
816
|
+
status: 'failed',
|
|
817
|
+
checkpoint: null,
|
|
818
|
+
checkpointPath,
|
|
819
|
+
latestPath: paths.latestPath,
|
|
820
|
+
diagnostics: [
|
|
821
|
+
diag(diagnosticCodes.initApplyFailed, `Checkpoint write failed: ${errorMessage(error)}`, paths.taskDir, 'Inspect the private task state; no pointer was intentionally changed.'),
|
|
822
|
+
],
|
|
823
|
+
residualPaths: checkpointPath === null ? [] : [checkpointPath],
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
const release = await releaseTaskLock(lock.path, lock.token);
|
|
827
|
+
if (result === undefined) {
|
|
828
|
+
result = {
|
|
829
|
+
ok: false,
|
|
830
|
+
status: 'failed',
|
|
831
|
+
checkpoint: null,
|
|
832
|
+
checkpointPath,
|
|
833
|
+
latestPath: paths.latestPath,
|
|
834
|
+
diagnostics: [
|
|
835
|
+
diag(diagnosticCodes.initApplyFailed, 'Checkpoint writer ended without a result.', paths.taskDir, 'Inspect the private task state; no pointer was intentionally changed.'),
|
|
836
|
+
],
|
|
837
|
+
residualPaths: checkpointPath === null ? [] : [checkpointPath],
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (!release.ok) {
|
|
841
|
+
return {
|
|
842
|
+
...result,
|
|
843
|
+
ok: false,
|
|
844
|
+
status: result.status === 'saved' ? 'partial' : result.status,
|
|
845
|
+
diagnostics: [
|
|
846
|
+
...result.diagnostics,
|
|
847
|
+
outcomeDiagnostic(release, lock.path),
|
|
848
|
+
],
|
|
849
|
+
residualPaths: [...result.residualPaths, lock.path],
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
async resume(input) {
|
|
855
|
+
const loaded = await loadContinuityWorkspace(this.root);
|
|
856
|
+
if (!loaded.ok || loaded.workspace === null) {
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
status: 'failed',
|
|
860
|
+
packet: null,
|
|
861
|
+
diagnostics: [...loaded.diagnostics],
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const workspace = loaded.workspace;
|
|
865
|
+
try {
|
|
866
|
+
continuityTaskPaths(workspace.root, input.taskId);
|
|
867
|
+
}
|
|
868
|
+
catch (error) {
|
|
869
|
+
return {
|
|
870
|
+
ok: false,
|
|
871
|
+
status: 'invalid',
|
|
872
|
+
packet: null,
|
|
873
|
+
diagnostics: [
|
|
874
|
+
diag(diagnosticCodes.document_record_invalid, `Invalid task id: ${errorMessage(error)}`, workspace.root, 'Use a stable task id with letters, digits, dot, underscore, or dash.'),
|
|
875
|
+
],
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const contextRead = await readTaskContext(workspace.root, input.taskId);
|
|
879
|
+
if (!contextRead.exists) {
|
|
880
|
+
return {
|
|
881
|
+
ok: false,
|
|
882
|
+
status: 'not-found',
|
|
883
|
+
packet: null,
|
|
884
|
+
diagnostics: [
|
|
885
|
+
diag(diagnosticCodes.document_not_found, `Task context not found: ${input.taskId}`, continuityTaskPaths(workspace.root, input.taskId).contextPath, 'Use an exact existing task id; Pappra does not guess the latest task.'),
|
|
886
|
+
],
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
if (contextRead.value === null) {
|
|
890
|
+
return {
|
|
891
|
+
ok: false,
|
|
892
|
+
status: 'invalid',
|
|
893
|
+
packet: null,
|
|
894
|
+
diagnostics: [...contextRead.diagnostics],
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (contextRead.value.taskId !== input.taskId ||
|
|
898
|
+
contextRead.value.workspaceId !== workspace.config.workspaceId) {
|
|
899
|
+
return {
|
|
900
|
+
ok: false,
|
|
901
|
+
status: 'invalid',
|
|
902
|
+
packet: null,
|
|
903
|
+
diagnostics: [
|
|
904
|
+
diag(diagnosticCodes.document_catalog_conflict, 'Task context belongs to another workspace.', workspace.root, 'Use the matching workspace; Pappra does not reuse another workspace task.'),
|
|
905
|
+
],
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const pointerRead = await readPointer(workspace.root, input.taskId);
|
|
909
|
+
if (!pointerRead.exists) {
|
|
910
|
+
return {
|
|
911
|
+
ok: false,
|
|
912
|
+
status: 'not-found',
|
|
913
|
+
packet: null,
|
|
914
|
+
diagnostics: [
|
|
915
|
+
diag(diagnosticCodes.document_not_found, `No checkpoint pointer for task: ${input.taskId}`, continuityTaskPaths(workspace.root, input.taskId).latestPath, 'Pappra does not scan other tasks or infer a latest checkpoint.'),
|
|
916
|
+
],
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
if (pointerRead.value === null) {
|
|
920
|
+
return {
|
|
921
|
+
ok: false,
|
|
922
|
+
status: 'invalid',
|
|
923
|
+
packet: null,
|
|
924
|
+
diagnostics: [...pointerRead.diagnostics],
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
if (pointerRead.value.taskId !== input.taskId ||
|
|
928
|
+
pointerRead.value.workspaceId !== workspace.config.workspaceId) {
|
|
929
|
+
return {
|
|
930
|
+
ok: false,
|
|
931
|
+
status: 'invalid',
|
|
932
|
+
packet: null,
|
|
933
|
+
diagnostics: [
|
|
934
|
+
diag(diagnosticCodes.document_proposal_stale, 'Latest pointer belongs to another task or workspace.', continuityTaskPaths(workspace.root, input.taskId).latestPath, 'Inspect the pointer; Pappra does not reuse another task state.'),
|
|
935
|
+
],
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const checkpointRead = await readCheckpoint(workspace.root, input.taskId, pointerRead.value);
|
|
939
|
+
if (checkpointRead.value === null) {
|
|
940
|
+
return {
|
|
941
|
+
ok: false,
|
|
942
|
+
status: 'invalid',
|
|
943
|
+
packet: null,
|
|
944
|
+
diagnostics: [...checkpointRead.diagnostics],
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
if (checkpointRead.value.taskId !== input.taskId ||
|
|
948
|
+
checkpointRead.value.workspaceId !== workspace.config.workspaceId ||
|
|
949
|
+
checkpointRead.value.revision !== pointerRead.value.revision) {
|
|
950
|
+
return {
|
|
951
|
+
ok: false,
|
|
952
|
+
status: 'invalid',
|
|
953
|
+
packet: null,
|
|
954
|
+
diagnostics: [
|
|
955
|
+
diag(diagnosticCodes.document_proposal_stale, 'Checkpoint identity does not match the requested task, workspace, or pointer revision.', workspace.root, 'Inspect the private checkpoint/pointer; no recovery was guessed.'),
|
|
956
|
+
],
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const checkpoint = checkpointRead.value;
|
|
960
|
+
const diagnostics = [...workspace.diagnostics];
|
|
961
|
+
const identityChecks = [
|
|
962
|
+
{
|
|
963
|
+
name: 'task-id',
|
|
964
|
+
status: checkpoint.taskId === input.taskId ? 'ok' : 'changed',
|
|
965
|
+
detail: checkpoint.taskId,
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
name: 'workspace-id',
|
|
969
|
+
status: checkpoint.workspaceId === workspace.config.workspaceId
|
|
970
|
+
? 'ok'
|
|
971
|
+
: 'changed',
|
|
972
|
+
detail: checkpoint.workspaceId,
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
name: 'pointer-revision',
|
|
976
|
+
status: pointerRead.value.revision === checkpoint.revision ? 'ok' : 'changed',
|
|
977
|
+
detail: `${pointerRead.value.revision}/${checkpoint.revision}`,
|
|
978
|
+
},
|
|
979
|
+
];
|
|
980
|
+
const currentRepositories = new Map();
|
|
981
|
+
for (const [index, repository] of workspace.repositories.entries()) {
|
|
982
|
+
const snapshot = await snapshotRepository(workspace, repository, index);
|
|
983
|
+
currentRepositories.set(snapshot.record.repoId, snapshot.record);
|
|
984
|
+
}
|
|
985
|
+
const repositoryDrift = [];
|
|
986
|
+
for (const stored of checkpoint.repositories) {
|
|
987
|
+
const patchPath = stored.patchDigest === null
|
|
988
|
+
? null
|
|
989
|
+
: join(continuityTaskPaths(workspace.root, input.taskId).patchesDir, `${stored.patchDigest.slice('sha256:'.length)}.patch`);
|
|
990
|
+
let patchStatus = 'not-applicable';
|
|
991
|
+
let patchDetail = null;
|
|
992
|
+
if (patchPath !== null) {
|
|
993
|
+
const patchRead = await readContinuityBytes(workspace.root, input.taskId, patchPath);
|
|
994
|
+
if (!patchRead.ok) {
|
|
995
|
+
patchStatus =
|
|
996
|
+
patchRead.code === diagnosticCodes.fileNotFound
|
|
997
|
+
? 'missing'
|
|
998
|
+
: 'unsafe';
|
|
999
|
+
patchDetail = patchRead.message;
|
|
1000
|
+
}
|
|
1001
|
+
else if (sha256Digest(patchRead.value) !== stored.patchDigest) {
|
|
1002
|
+
patchStatus = 'changed';
|
|
1003
|
+
patchDetail =
|
|
1004
|
+
'Private patch bytes do not match the checkpoint digest.';
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
patchStatus = 'unchanged';
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const current = currentRepositories.get(stored.repoId);
|
|
1011
|
+
if (current === undefined) {
|
|
1012
|
+
repositoryDrift.push({
|
|
1013
|
+
repoId: stored.repoId,
|
|
1014
|
+
status: 'missing',
|
|
1015
|
+
storedHead: stored.head,
|
|
1016
|
+
currentHead: null,
|
|
1017
|
+
storedDirty: stored.dirty,
|
|
1018
|
+
currentDirty: null,
|
|
1019
|
+
storedPatchDigest: stored.patchDigest,
|
|
1020
|
+
currentPatchDigest: null,
|
|
1021
|
+
storedCheckoutId: stored.checkoutId,
|
|
1022
|
+
currentCheckoutId: null,
|
|
1023
|
+
storedCaptureStatus: stored.captureStatus,
|
|
1024
|
+
currentCaptureStatus: 'unknown',
|
|
1025
|
+
patchStatus,
|
|
1026
|
+
patchDetail,
|
|
1027
|
+
detail: `Repository ${stored.repoId} is no longer present.`,
|
|
1028
|
+
});
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const relocated = current.root !== stored.root;
|
|
1032
|
+
const checkoutChanged = current.checkoutId !== stored.checkoutId;
|
|
1033
|
+
const changed = current.head !== stored.head ||
|
|
1034
|
+
current.dirty !== stored.dirty ||
|
|
1035
|
+
current.patchDigest !== stored.patchDigest ||
|
|
1036
|
+
checkoutChanged;
|
|
1037
|
+
const unknownCapture = stored.captureStatus !== 'complete' ||
|
|
1038
|
+
current.captureStatus !== 'complete' ||
|
|
1039
|
+
patchStatus === 'missing' ||
|
|
1040
|
+
patchStatus === 'changed' ||
|
|
1041
|
+
patchStatus === 'unsafe';
|
|
1042
|
+
const detail = relocated
|
|
1043
|
+
? `Repository path moved from ${stored.root} to ${current.root}${stored.root.includes(' ') || current.root.includes(' ') ? ' (space-containing path)' : ''}.`
|
|
1044
|
+
: checkoutChanged
|
|
1045
|
+
? 'Repository checkout identity changed since the checkpoint.'
|
|
1046
|
+
: changed
|
|
1047
|
+
? 'HEAD/dirty/patch state changed since the checkpoint.'
|
|
1048
|
+
: patchDetail;
|
|
1049
|
+
repositoryDrift.push({
|
|
1050
|
+
repoId: stored.repoId,
|
|
1051
|
+
status: relocated
|
|
1052
|
+
? 'relocated'
|
|
1053
|
+
: unknownCapture
|
|
1054
|
+
? 'unknown'
|
|
1055
|
+
: changed
|
|
1056
|
+
? 'changed'
|
|
1057
|
+
: stored.head === null || stored.dirty === null
|
|
1058
|
+
? 'unknown'
|
|
1059
|
+
: 'unchanged',
|
|
1060
|
+
storedHead: stored.head,
|
|
1061
|
+
currentHead: current.head,
|
|
1062
|
+
storedDirty: stored.dirty,
|
|
1063
|
+
currentDirty: current.dirty,
|
|
1064
|
+
storedPatchDigest: stored.patchDigest,
|
|
1065
|
+
currentPatchDigest: current.patchDigest,
|
|
1066
|
+
storedCheckoutId: stored.checkoutId,
|
|
1067
|
+
currentCheckoutId: current.checkoutId,
|
|
1068
|
+
storedCaptureStatus: stored.captureStatus,
|
|
1069
|
+
currentCaptureStatus: current.captureStatus,
|
|
1070
|
+
patchStatus,
|
|
1071
|
+
patchDetail,
|
|
1072
|
+
detail,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
const codeStatus = repositoryDrift.some((entry) => entry.status === 'relocated')
|
|
1076
|
+
? 'relocated'
|
|
1077
|
+
: repositoryDrift.some((entry) => entry.status === 'changed')
|
|
1078
|
+
? 'changed'
|
|
1079
|
+
: repositoryDrift.some((entry) => entry.status === 'missing' || entry.status === 'unknown')
|
|
1080
|
+
? 'unknown'
|
|
1081
|
+
: 'unchanged';
|
|
1082
|
+
const artifactChecks = [];
|
|
1083
|
+
for (const artifact of checkpoint.artifacts) {
|
|
1084
|
+
const resolved = await readArtifactPath(workspace.root, artifact.path);
|
|
1085
|
+
if (!resolved.ok) {
|
|
1086
|
+
artifactChecks.push({
|
|
1087
|
+
path: artifact.path,
|
|
1088
|
+
status: 'unsafe',
|
|
1089
|
+
detail: resolved.message,
|
|
1090
|
+
});
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
const read = await readFileBounded(resolved.value, MAX_ARTIFACT_BYTES);
|
|
1094
|
+
if (!read.ok) {
|
|
1095
|
+
artifactChecks.push({
|
|
1096
|
+
path: artifact.path,
|
|
1097
|
+
status: read.code === diagnosticCodes.fileNotFound ? 'missing' : 'unknown',
|
|
1098
|
+
detail: read.message,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
artifactChecks.push({
|
|
1103
|
+
path: artifact.path,
|
|
1104
|
+
status: read.value.digest === artifact.digest ? 'unchanged' : 'changed',
|
|
1105
|
+
detail: read.value.digest === artifact.digest
|
|
1106
|
+
? null
|
|
1107
|
+
: 'Artifact digest changed since the checkpoint.',
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const collaborationObservation = await observeCollaboration(checkpoint, workspace.root);
|
|
1112
|
+
const blockingReasons = [];
|
|
1113
|
+
const pushBlock = (code, message) => {
|
|
1114
|
+
if (!blockingReasons.some((entry) => entry.code === code)) {
|
|
1115
|
+
blockingReasons.push({ code, message });
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
pushBlock('live_writer_unverified', 'P07 cannot establish live writer/stop ownership from a stored session string; execution remains blocked.');
|
|
1119
|
+
if (collaborationObservation.status === 'unsupported') {
|
|
1120
|
+
pushBlock('collaboration_authority_invalid', 'The referenced collaboration record is not a supported authoritative P01 task record.');
|
|
1121
|
+
}
|
|
1122
|
+
for (const code of collaborationObservation.authority.blockerCodes) {
|
|
1123
|
+
pushBlock(code, `The authoritative collaboration record reports ${code}; execution remains blocked.`);
|
|
1124
|
+
}
|
|
1125
|
+
if (repositoryDrift.some((entry) => entry.storedCheckoutId !== entry.currentCheckoutId)) {
|
|
1126
|
+
pushBlock('checkout_identity_changed', 'Repository checkout identity changed since the checkpoint; execution is blocked.');
|
|
1127
|
+
}
|
|
1128
|
+
if (repositoryDrift.some((entry) => entry.patchStatus !== 'unchanged' &&
|
|
1129
|
+
entry.patchStatus !== 'not-applicable')) {
|
|
1130
|
+
pushBlock('private_patch_unavailable', 'A private patch is missing, changed or unsafe; verification is not current.');
|
|
1131
|
+
}
|
|
1132
|
+
if (repositoryDrift.some((entry) => entry.storedCaptureStatus !== 'complete' ||
|
|
1133
|
+
entry.currentCaptureStatus !== 'complete')) {
|
|
1134
|
+
pushBlock('snapshot_incomplete', 'Repository capture was partial or unknown; current verification is not established.');
|
|
1135
|
+
}
|
|
1136
|
+
if (checkpoint.workspaceId !== workspace.config.workspaceId) {
|
|
1137
|
+
pushBlock('workspace_identity_changed', 'Checkpoint workspace identity differs from the current workspace.');
|
|
1138
|
+
}
|
|
1139
|
+
if (checkpoint.authorSession === null) {
|
|
1140
|
+
pushBlock('unknown_writer', 'Checkpoint author session is unknown; execution is blocked.');
|
|
1141
|
+
}
|
|
1142
|
+
else if (input.currentSession === undefined ||
|
|
1143
|
+
input.currentSession === null) {
|
|
1144
|
+
pushBlock('writer_not_confirmed', 'Current writer session was not supplied; execution is blocked.');
|
|
1145
|
+
}
|
|
1146
|
+
else if (input.currentSession !== checkpoint.authorSession) {
|
|
1147
|
+
pushBlock('writer_mismatch', 'Current writer session differs from the checkpoint author; execution is blocked.');
|
|
1148
|
+
}
|
|
1149
|
+
if (checkpoint.contextCompleteness === 'partial') {
|
|
1150
|
+
pushBlock('context_partial', 'Semantic context is incomplete; execution is blocked.');
|
|
1151
|
+
}
|
|
1152
|
+
if (collaborationObservation.status === 'missing' ||
|
|
1153
|
+
collaborationObservation.status === 'unreadable') {
|
|
1154
|
+
pushBlock('collaboration_record_unavailable', 'The referenced collaboration record could not be read; execution is blocked.');
|
|
1155
|
+
}
|
|
1156
|
+
if (codeStatus === 'relocated' || codeStatus === 'unknown') {
|
|
1157
|
+
pushBlock('code_identity_changed', 'Repository identity/path changed or is unknown; inspect before execution.');
|
|
1158
|
+
}
|
|
1159
|
+
const privatePayloadChanged = repositoryDrift.some((entry) => entry.patchStatus === 'changed');
|
|
1160
|
+
const verificationStatus = codeStatus === 'changed' ||
|
|
1161
|
+
privatePayloadChanged ||
|
|
1162
|
+
artifactChecks.some((entry) => entry.status === 'changed')
|
|
1163
|
+
? 'stale'
|
|
1164
|
+
: codeStatus === 'relocated' ||
|
|
1165
|
+
codeStatus === 'unknown' ||
|
|
1166
|
+
artifactChecks.some((entry) => entry.status !== 'unchanged')
|
|
1167
|
+
? 'unknown'
|
|
1168
|
+
: 'current';
|
|
1169
|
+
const acceptance = boundedArray(checkpoint.acceptance);
|
|
1170
|
+
const constraints = boundedArray(checkpoint.constraints);
|
|
1171
|
+
const decisions = boundedArray(checkpoint.decisions);
|
|
1172
|
+
const attempted = boundedArray(checkpoint.attempted);
|
|
1173
|
+
const nextActions = boundedArray(checkpoint.nextActions);
|
|
1174
|
+
const limitations = boundedArray(checkpoint.limitations);
|
|
1175
|
+
const repositoriesBounded = boundedArray(checkpoint.repositories);
|
|
1176
|
+
const artifactsBounded = boundedArray(checkpoint.artifacts);
|
|
1177
|
+
const truncated = acceptance.truncated ||
|
|
1178
|
+
constraints.truncated ||
|
|
1179
|
+
decisions.truncated ||
|
|
1180
|
+
attempted.truncated ||
|
|
1181
|
+
nextActions.truncated ||
|
|
1182
|
+
limitations.truncated ||
|
|
1183
|
+
repositoriesBounded.truncated ||
|
|
1184
|
+
artifactsBounded.truncated;
|
|
1185
|
+
const packet = {
|
|
1186
|
+
schema: 'pappra/resume-packet/1',
|
|
1187
|
+
generatedAt: new Date().toISOString(),
|
|
1188
|
+
taskId: checkpoint.taskId,
|
|
1189
|
+
workspaceId: checkpoint.workspaceId,
|
|
1190
|
+
checkpoint: {
|
|
1191
|
+
ref: pointerRead.value.checkpointRef,
|
|
1192
|
+
digest: pointerRead.value.checkpointDigest,
|
|
1193
|
+
revision: pointerRead.value.revision,
|
|
1194
|
+
},
|
|
1195
|
+
identityChecks,
|
|
1196
|
+
codeDrift: { status: codeStatus, repositories: repositoryDrift },
|
|
1197
|
+
artifactChecks,
|
|
1198
|
+
executionObservations: checkpoint.nativeRefs.map((ref) => ({
|
|
1199
|
+
harness: ref.harness,
|
|
1200
|
+
sessionId: ref.sessionId,
|
|
1201
|
+
turnId: ref.turnId,
|
|
1202
|
+
runId: ref.runId,
|
|
1203
|
+
scope: 'stored-snapshot',
|
|
1204
|
+
liveStatus: 'unknown',
|
|
1205
|
+
observedAt: ref.observedAt,
|
|
1206
|
+
})),
|
|
1207
|
+
collaborationObservation,
|
|
1208
|
+
blockingReasons,
|
|
1209
|
+
executionAllowed: blockingReasons.length === 0,
|
|
1210
|
+
contextCompleteness: checkpoint.contextCompleteness,
|
|
1211
|
+
verificationStatus,
|
|
1212
|
+
boundedContext: {
|
|
1213
|
+
objective: checkpoint.objective,
|
|
1214
|
+
acceptance: acceptance.values,
|
|
1215
|
+
constraints: constraints.values,
|
|
1216
|
+
decisions: decisions.values,
|
|
1217
|
+
attempted: attempted.values,
|
|
1218
|
+
nextActions: nextActions.values,
|
|
1219
|
+
limitations: limitations.values,
|
|
1220
|
+
repositories: repositoriesBounded.values,
|
|
1221
|
+
artifacts: artifactsBounded.values,
|
|
1222
|
+
truncated,
|
|
1223
|
+
},
|
|
1224
|
+
};
|
|
1225
|
+
const parsed = resumePacketSchema.safeParse(packet);
|
|
1226
|
+
if (!parsed.success) {
|
|
1227
|
+
return {
|
|
1228
|
+
ok: false,
|
|
1229
|
+
status: 'invalid',
|
|
1230
|
+
packet: null,
|
|
1231
|
+
diagnostics: [
|
|
1232
|
+
diag(diagnosticCodes.configSchemaInvalid, `Resume packet is invalid: ${parsed.error.issues[0]?.message ?? 'unknown issue'}`, checkpointPath(workspace.root, input.taskId, pointerRead.value), 'Inspect the private checkpoint; no recovery was guessed.'),
|
|
1233
|
+
],
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
const nowDiagnostics = [...diagnostics];
|
|
1237
|
+
for (const reason of blockingReasons) {
|
|
1238
|
+
nowDiagnostics.push({
|
|
1239
|
+
code: diagnosticCodes.document_proposal_stale,
|
|
1240
|
+
severity: 'warning',
|
|
1241
|
+
message: reason.message,
|
|
1242
|
+
source: continuityTaskPaths(workspace.root, input.taskId).latestPath,
|
|
1243
|
+
pointer: null,
|
|
1244
|
+
hint: 'This is a read-only diagnostic; no execution was started.',
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
return {
|
|
1248
|
+
ok: true,
|
|
1249
|
+
status: blockingReasons.length === 0 ? 'ready' : 'blocked',
|
|
1250
|
+
packet: parsed.data,
|
|
1251
|
+
diagnostics: nowDiagnostics,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function checkpointPath(root, taskId, pointer) {
|
|
1256
|
+
return (checkpointAbsolutePath(root, taskId, pointer.checkpointRef) ??
|
|
1257
|
+
pointer.checkpointRef);
|
|
1258
|
+
}
|