@arnilo/prism-coding-agent 0.0.7 → 0.0.10
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/CHANGELOG.md +28 -6
- package/README.md +11 -5
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +35 -0
- package/dist/checks.d.ts +26 -0
- package/dist/checks.js +249 -0
- package/dist/coding-checkpoint.d.ts +159 -0
- package/dist/coding-checkpoint.js +576 -0
- package/dist/git-exec.d.ts +62 -0
- package/dist/git-exec.js +257 -0
- package/dist/git-status.d.ts +30 -0
- package/dist/git-status.js +146 -0
- package/dist/git-tools.d.ts +34 -0
- package/dist/git-tools.js +502 -0
- package/dist/git.d.ts +139 -0
- package/dist/git.js +495 -0
- package/dist/index.d.ts +34 -4
- package/dist/index.js +39 -5
- package/dist/limits.d.ts +76 -0
- package/dist/limits.js +81 -0
- package/dist/list.d.ts +14 -0
- package/dist/list.js +144 -0
- package/dist/repository.d.ts +119 -0
- package/dist/repository.js +633 -0
- package/dist/search.d.ts +14 -0
- package/dist/search.js +166 -0
- package/package.json +6 -5
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded durable coding-task checkpoint metadata.
|
|
3
|
+
*
|
|
4
|
+
* This is not a second runtime. Hosts persist plan/todo Markdown in the workspace
|
|
5
|
+
* and store only references/hashes/summaries in workflow checkpoint state. Resume
|
|
6
|
+
* revalidates fingerprints and artifact integrity before import/execution.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
11
|
+
import { DEFAULT_MAX_CHECK_SUMMARY_BYTES, DEFAULT_MAX_CODING_ARTIFACT_BYTES, DEFAULT_MAX_CODING_ARTIFACTS, DEFAULT_MAX_CODING_CHECKPOINT_BYTES, DEFAULT_MAX_PLAN_BYTES, DEFAULT_MAX_TODO_TEXT_BYTES, DEFAULT_MAX_TODOS, HARD_MAX_CHECK_SUMMARY_BYTES, HARD_MAX_CODING_ARTIFACT_BYTES, HARD_MAX_CODING_ARTIFACTS, HARD_MAX_CODING_CHECKPOINT_BYTES, HARD_MAX_PLAN_BYTES, HARD_MAX_TODO_TEXT_BYTES, HARD_MAX_TODOS, validateCodingLimit, } from "./limits.js";
|
|
12
|
+
import { sha256Hex } from "./artifacts.js";
|
|
13
|
+
export const CODING_CHECKPOINT_SCHEMA_VERSION = 1;
|
|
14
|
+
/** Workflow shared-state key that holds coding checkpoint metadata. */
|
|
15
|
+
export const CODING_STATE_KEY = "coding";
|
|
16
|
+
const SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
17
|
+
const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/;
|
|
18
|
+
const BRANCH = /^[^\s]{1,255}$/;
|
|
19
|
+
const TODO_LINE = /^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/;
|
|
20
|
+
const FORBIDDEN_METADATA_KEYS = new Set([
|
|
21
|
+
"credentials",
|
|
22
|
+
"credential",
|
|
23
|
+
"secret",
|
|
24
|
+
"secrets",
|
|
25
|
+
"token",
|
|
26
|
+
"tokens",
|
|
27
|
+
"password",
|
|
28
|
+
"cookie",
|
|
29
|
+
"cookies",
|
|
30
|
+
"storageState",
|
|
31
|
+
"storage_state",
|
|
32
|
+
"authorization",
|
|
33
|
+
"env",
|
|
34
|
+
"processEnv",
|
|
35
|
+
"commandOutput",
|
|
36
|
+
"rawOutput",
|
|
37
|
+
"stdout",
|
|
38
|
+
"stderr",
|
|
39
|
+
]);
|
|
40
|
+
export class CodingCheckpointError extends Error {
|
|
41
|
+
code = "ERR_PRISM_CODING_CHECKPOINT";
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "CodingCheckpointError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function resolveCodingCheckpointLimits(options) {
|
|
48
|
+
return {
|
|
49
|
+
maxPlanBytes: validateCodingLimit("maxPlanBytes", options?.maxPlanBytes ?? DEFAULT_MAX_PLAN_BYTES, HARD_MAX_PLAN_BYTES),
|
|
50
|
+
maxTodos: validateCodingLimit("maxTodos", options?.maxTodos ?? DEFAULT_MAX_TODOS, HARD_MAX_TODOS),
|
|
51
|
+
maxTodoTextBytes: validateCodingLimit("maxTodoTextBytes", options?.maxTodoTextBytes ?? DEFAULT_MAX_TODO_TEXT_BYTES, HARD_MAX_TODO_TEXT_BYTES),
|
|
52
|
+
maxArtifacts: validateCodingLimit("maxArtifacts", options?.maxArtifacts ?? DEFAULT_MAX_CODING_ARTIFACTS, HARD_MAX_CODING_ARTIFACTS),
|
|
53
|
+
maxArtifactBytes: validateCodingLimit("maxArtifactBytes", options?.maxArtifactBytes ?? DEFAULT_MAX_CODING_ARTIFACT_BYTES, HARD_MAX_CODING_ARTIFACT_BYTES),
|
|
54
|
+
maxCheckSummaryBytes: validateCodingLimit("maxCheckSummaryBytes", options?.maxCheckSummaryBytes ?? DEFAULT_MAX_CHECK_SUMMARY_BYTES, HARD_MAX_CHECK_SUMMARY_BYTES),
|
|
55
|
+
maxCheckpointBytes: validateCodingLimit("maxCheckpointBytes", options?.maxCheckpointBytes ?? DEFAULT_MAX_CODING_CHECKPOINT_BYTES, HARD_MAX_CODING_CHECKPOINT_BYTES),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Deterministic SHA-256 fingerprint over a JSON-stable encoding. */
|
|
59
|
+
export function fingerprintJson(value) {
|
|
60
|
+
return sha256Hex(Buffer.from(stableStringify(value), "utf8"));
|
|
61
|
+
}
|
|
62
|
+
export function createCodingArtifactRef(input) {
|
|
63
|
+
const maxBytes = input.maxBytes ?? DEFAULT_MAX_CODING_ARTIFACT_BYTES;
|
|
64
|
+
if (input.bytes.length > maxBytes) {
|
|
65
|
+
throw new CodingCheckpointError(`Artifact exceeds ${maxBytes} byte limit`);
|
|
66
|
+
}
|
|
67
|
+
if (!isNonEmptyString(input.uri) || input.uri.length > 2_048) {
|
|
68
|
+
throw new CodingCheckpointError("Artifact URI must be a non-empty string at most 2048 characters");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
kind: input.kind,
|
|
72
|
+
uri: input.uri,
|
|
73
|
+
sha256: sha256Hex(input.bytes),
|
|
74
|
+
bytes: input.bytes.length,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function verifyCodingArtifactBytes(ref, bytes, limits) {
|
|
78
|
+
const resolved = resolveCodingCheckpointLimits(limits);
|
|
79
|
+
if (ref.bytes > resolved.maxArtifactBytes || bytes.length > resolved.maxArtifactBytes) {
|
|
80
|
+
throw new CodingCheckpointError(`Artifact exceeds ${resolved.maxArtifactBytes} byte limit`);
|
|
81
|
+
}
|
|
82
|
+
if (bytes.length !== ref.bytes) {
|
|
83
|
+
throw new CodingCheckpointError(`Artifact byte count mismatch: expected ${ref.bytes}, got ${bytes.length}`);
|
|
84
|
+
}
|
|
85
|
+
const digest = sha256Hex(bytes);
|
|
86
|
+
if (digest !== ref.sha256) {
|
|
87
|
+
throw new CodingCheckpointError("Artifact SHA-256 mismatch");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function createCodingPlanMarkdown(input) {
|
|
91
|
+
const limits = resolveCodingCheckpointLimits(input.limits);
|
|
92
|
+
if (input.todos.length > limits.maxTodos) {
|
|
93
|
+
throw new CodingCheckpointError(`Plan exceeds ${limits.maxTodos} todo limit`);
|
|
94
|
+
}
|
|
95
|
+
const lines = [
|
|
96
|
+
`# ${input.title.trim() || "Coding task"}`,
|
|
97
|
+
"",
|
|
98
|
+
`- Task ID: \`${input.taskId}\``,
|
|
99
|
+
`- Status: \`${input.status ?? "planned"}\``,
|
|
100
|
+
"",
|
|
101
|
+
"## Todos",
|
|
102
|
+
"",
|
|
103
|
+
];
|
|
104
|
+
for (const todo of input.todos) {
|
|
105
|
+
const text = todo.text.trim();
|
|
106
|
+
assertTodoText(text, limits.maxTodoTextBytes);
|
|
107
|
+
const mark = todo.done ? "x" : " ";
|
|
108
|
+
const idPrefix = todo.id ? `[${todo.id}] ` : "";
|
|
109
|
+
lines.push(`- [${mark}] ${idPrefix}${text}`);
|
|
110
|
+
}
|
|
111
|
+
if (input.notes?.trim()) {
|
|
112
|
+
lines.push("", "## Notes", "", input.notes.trim(), "");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
lines.push("");
|
|
116
|
+
}
|
|
117
|
+
const markdown = lines.join("\n");
|
|
118
|
+
assertByteLimit("plan", markdown, limits.maxPlanBytes);
|
|
119
|
+
return markdown;
|
|
120
|
+
}
|
|
121
|
+
export function parseCodingPlanTodos(markdown, limits) {
|
|
122
|
+
const resolved = resolveCodingCheckpointLimits(limits);
|
|
123
|
+
assertByteLimit("plan", markdown, resolved.maxPlanBytes);
|
|
124
|
+
const todos = [];
|
|
125
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
126
|
+
const match = TODO_LINE.exec(line);
|
|
127
|
+
if (!match)
|
|
128
|
+
continue;
|
|
129
|
+
const done = match[1].toLowerCase() === "x";
|
|
130
|
+
const raw = match[2].trim();
|
|
131
|
+
assertTodoText(raw, resolved.maxTodoTextBytes);
|
|
132
|
+
const idMatch = /^\[([A-Za-z0-9._-]{1,64})\]\s+(.+)$/.exec(raw);
|
|
133
|
+
const id = idMatch?.[1] ?? `todo-${todos.length + 1}`;
|
|
134
|
+
const text = idMatch?.[2] ?? raw;
|
|
135
|
+
assertTodoText(text, resolved.maxTodoTextBytes);
|
|
136
|
+
todos.push({ id, text, done });
|
|
137
|
+
if (todos.length > resolved.maxTodos) {
|
|
138
|
+
throw new CodingCheckpointError(`Plan exceeds ${resolved.maxTodos} todo limit`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return todos;
|
|
142
|
+
}
|
|
143
|
+
export async function writeCodingPlanFile(input) {
|
|
144
|
+
const limits = resolveCodingCheckpointLimits(input.limits);
|
|
145
|
+
assertByteLimit("plan", input.markdown, limits.maxPlanBytes);
|
|
146
|
+
const absolute = resolveUnderWorkspace(input.workspaceRoot, input.planPath);
|
|
147
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
148
|
+
const bytes = Buffer.from(input.markdown, "utf8");
|
|
149
|
+
await writeFile(absolute, bytes, { mode: 0o600 });
|
|
150
|
+
return createCodingArtifactRef({
|
|
151
|
+
kind: "plan",
|
|
152
|
+
uri: `file://${absolute}`,
|
|
153
|
+
bytes,
|
|
154
|
+
maxBytes: limits.maxPlanBytes,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
export async function readCodingPlanFile(input) {
|
|
158
|
+
const limits = resolveCodingCheckpointLimits(input.limits);
|
|
159
|
+
const absolute = resolveUnderWorkspace(input.workspaceRoot, input.planPath);
|
|
160
|
+
const bytes = await readFile(absolute);
|
|
161
|
+
if (bytes.length > limits.maxPlanBytes) {
|
|
162
|
+
throw new CodingCheckpointError(`Plan exceeds ${limits.maxPlanBytes} byte limit`);
|
|
163
|
+
}
|
|
164
|
+
const artifact = createCodingArtifactRef({
|
|
165
|
+
kind: "plan",
|
|
166
|
+
uri: `file://${absolute}`,
|
|
167
|
+
bytes,
|
|
168
|
+
maxBytes: limits.maxPlanBytes,
|
|
169
|
+
});
|
|
170
|
+
if (input.expected) {
|
|
171
|
+
verifyCodingArtifactBytes(input.expected, bytes, limits);
|
|
172
|
+
}
|
|
173
|
+
const markdown = bytes.toString("utf8");
|
|
174
|
+
return {
|
|
175
|
+
markdown,
|
|
176
|
+
artifact,
|
|
177
|
+
todos: parseCodingPlanTodos(markdown, limits),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export function buildCodingCheckpointMetadata(input) {
|
|
181
|
+
const metadata = {
|
|
182
|
+
schemaVersion: CODING_CHECKPOINT_SCHEMA_VERSION,
|
|
183
|
+
taskId: input.taskId,
|
|
184
|
+
workspaceRoot: input.workspaceRoot,
|
|
185
|
+
baseBranch: input.baseBranch,
|
|
186
|
+
branch: input.branch,
|
|
187
|
+
worktreePath: input.worktreePath,
|
|
188
|
+
planPath: input.planPath,
|
|
189
|
+
plan: input.plan,
|
|
190
|
+
workspaceExport: input.workspaceExport,
|
|
191
|
+
artifacts: input.artifacts ?? [],
|
|
192
|
+
checks: input.checks ?? [],
|
|
193
|
+
handoff: input.handoff,
|
|
194
|
+
status: input.status ?? "planned",
|
|
195
|
+
fingerprints: input.fingerprints,
|
|
196
|
+
todos: input.todos ?? [],
|
|
197
|
+
updatedAt: input.updatedAt ?? new Date().toISOString(),
|
|
198
|
+
};
|
|
199
|
+
return validateCodingCheckpointMetadata(metadata, input.limits);
|
|
200
|
+
}
|
|
201
|
+
export function validateCodingCheckpointMetadata(value, limits) {
|
|
202
|
+
const resolved = resolveCodingCheckpointLimits(limits);
|
|
203
|
+
if (!isPlainObject(value)) {
|
|
204
|
+
throw new CodingCheckpointError("Coding checkpoint metadata must be an object");
|
|
205
|
+
}
|
|
206
|
+
assertNoForbiddenKeys(value);
|
|
207
|
+
if (value.schemaVersion !== CODING_CHECKPOINT_SCHEMA_VERSION) {
|
|
208
|
+
throw new CodingCheckpointError(`Unsupported coding checkpoint schemaVersion: ${String(value.schemaVersion)}`);
|
|
209
|
+
}
|
|
210
|
+
const taskId = requireString(value.taskId, "taskId");
|
|
211
|
+
if (!TASK_ID.test(taskId)) {
|
|
212
|
+
throw new CodingCheckpointError("taskId has invalid format");
|
|
213
|
+
}
|
|
214
|
+
const workspaceRoot = requireAbsolutePath(value.workspaceRoot, "workspaceRoot");
|
|
215
|
+
const baseBranch = requireBranch(value.baseBranch, "baseBranch");
|
|
216
|
+
const branch = requireBranch(value.branch, "branch");
|
|
217
|
+
const planPath = requireRelativePath(value.planPath, "planPath");
|
|
218
|
+
const plan = validateArtifactRef(value.plan, resolved, { requireKind: "plan" });
|
|
219
|
+
const worktreePath = value.worktreePath === undefined ? undefined : requireAbsolutePath(value.worktreePath, "worktreePath");
|
|
220
|
+
const workspaceExport = value.workspaceExport === undefined
|
|
221
|
+
? undefined
|
|
222
|
+
: validateArtifactRef(value.workspaceExport, resolved);
|
|
223
|
+
const artifacts = requireArray(value.artifacts, "artifacts").map((item, index) => validateArtifactRef(item, resolved, { label: `artifacts[${index}]` }));
|
|
224
|
+
if (artifacts.length > resolved.maxArtifacts) {
|
|
225
|
+
throw new CodingCheckpointError(`Coding checkpoint exceeds ${resolved.maxArtifacts} artifact references`);
|
|
226
|
+
}
|
|
227
|
+
const checks = requireArray(value.checks, "checks").map((item, index) => validateCheckSummary(item, resolved, `checks[${index}]`));
|
|
228
|
+
if (checks.length > HARD_MAX_CODING_ARTIFACTS) {
|
|
229
|
+
throw new CodingCheckpointError("Too many check summaries");
|
|
230
|
+
}
|
|
231
|
+
const todos = requireArray(value.todos, "todos").map((item, index) => validateTodo(item, resolved, `todos[${index}]`));
|
|
232
|
+
if (todos.length > resolved.maxTodos) {
|
|
233
|
+
throw new CodingCheckpointError(`Coding checkpoint exceeds ${resolved.maxTodos} todos`);
|
|
234
|
+
}
|
|
235
|
+
const fingerprints = validateFingerprints(value.fingerprints);
|
|
236
|
+
const status = requireStatus(value.status);
|
|
237
|
+
const updatedAt = requireString(value.updatedAt, "updatedAt");
|
|
238
|
+
if (Number.isNaN(Date.parse(updatedAt))) {
|
|
239
|
+
throw new CodingCheckpointError("updatedAt must be an ISO-8601 timestamp");
|
|
240
|
+
}
|
|
241
|
+
const handoff = value.handoff === undefined ? undefined : validateHandoffSummary(value.handoff, resolved);
|
|
242
|
+
const metadata = {
|
|
243
|
+
schemaVersion: CODING_CHECKPOINT_SCHEMA_VERSION,
|
|
244
|
+
taskId,
|
|
245
|
+
workspaceRoot,
|
|
246
|
+
baseBranch,
|
|
247
|
+
branch,
|
|
248
|
+
worktreePath,
|
|
249
|
+
planPath,
|
|
250
|
+
plan,
|
|
251
|
+
workspaceExport,
|
|
252
|
+
artifacts,
|
|
253
|
+
checks,
|
|
254
|
+
handoff,
|
|
255
|
+
status,
|
|
256
|
+
fingerprints,
|
|
257
|
+
todos,
|
|
258
|
+
updatedAt,
|
|
259
|
+
};
|
|
260
|
+
const encoded = Buffer.byteLength(JSON.stringify(metadata), "utf8");
|
|
261
|
+
if (encoded > resolved.maxCheckpointBytes) {
|
|
262
|
+
throw new CodingCheckpointError(`Coding checkpoint metadata exceeds ${resolved.maxCheckpointBytes} byte limit`);
|
|
263
|
+
}
|
|
264
|
+
return metadata;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Fail closed before resume/import when ownership-equivalent fingerprints diverge
|
|
268
|
+
* or artifact references cannot be verified.
|
|
269
|
+
*/
|
|
270
|
+
export function assertCodingResumeAllowed(input) {
|
|
271
|
+
const metadata = validateCodingCheckpointMetadata(input.metadata, input.limits);
|
|
272
|
+
assertFingerprintsMatch(metadata.fingerprints, input.expected);
|
|
273
|
+
if (input.expectedWorkspaceRoot !== undefined &&
|
|
274
|
+
resolve(input.expectedWorkspaceRoot) !== resolve(metadata.workspaceRoot)) {
|
|
275
|
+
throw new CodingCheckpointError("Workspace root mismatch on coding resume");
|
|
276
|
+
}
|
|
277
|
+
if (input.expectedBaseBranch !== undefined && input.expectedBaseBranch !== metadata.baseBranch) {
|
|
278
|
+
throw new CodingCheckpointError("Base branch mismatch on coding resume");
|
|
279
|
+
}
|
|
280
|
+
if (input.planBytes) {
|
|
281
|
+
verifyCodingArtifactBytes(metadata.plan, input.planBytes, input.limits);
|
|
282
|
+
}
|
|
283
|
+
if (input.workspaceExportBytes) {
|
|
284
|
+
if (!metadata.workspaceExport) {
|
|
285
|
+
throw new CodingCheckpointError("Workspace export bytes provided without metadata reference");
|
|
286
|
+
}
|
|
287
|
+
verifyCodingArtifactBytes(metadata.workspaceExport, input.workspaceExportBytes, input.limits);
|
|
288
|
+
}
|
|
289
|
+
return metadata;
|
|
290
|
+
}
|
|
291
|
+
/** Extract and validate `state.coding` when present. */
|
|
292
|
+
export function readCodingCheckpointFromState(state, limits) {
|
|
293
|
+
if (!(CODING_STATE_KEY in state))
|
|
294
|
+
return undefined;
|
|
295
|
+
return validateCodingCheckpointMetadata(state[CODING_STATE_KEY], limits);
|
|
296
|
+
}
|
|
297
|
+
export function codingCheckpointStatePatch(metadata) {
|
|
298
|
+
return { [CODING_STATE_KEY]: validateCodingCheckpointMetadata(metadata) };
|
|
299
|
+
}
|
|
300
|
+
function assertFingerprintsMatch(actual, expected) {
|
|
301
|
+
if (actual.workflowRevision !== expected.workflowRevision) {
|
|
302
|
+
throw new CodingCheckpointError("Workflow revision fingerprint mismatch on coding resume");
|
|
303
|
+
}
|
|
304
|
+
if (actual.toolFingerprint !== expected.toolFingerprint) {
|
|
305
|
+
throw new CodingCheckpointError("Tool fingerprint mismatch on coding resume");
|
|
306
|
+
}
|
|
307
|
+
if (actual.policyFingerprint !== expected.policyFingerprint) {
|
|
308
|
+
throw new CodingCheckpointError("Policy fingerprint mismatch on coding resume");
|
|
309
|
+
}
|
|
310
|
+
if (expected.definitionHash !== undefined) {
|
|
311
|
+
if (actual.definitionHash !== expected.definitionHash) {
|
|
312
|
+
throw new CodingCheckpointError("Definition hash mismatch on coding resume");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (expected.imageDigest !== undefined) {
|
|
316
|
+
if (actual.imageDigest !== expected.imageDigest) {
|
|
317
|
+
throw new CodingCheckpointError("Image digest mismatch on coding resume");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function validateFingerprints(value) {
|
|
322
|
+
if (!isPlainObject(value)) {
|
|
323
|
+
throw new CodingCheckpointError("fingerprints must be an object");
|
|
324
|
+
}
|
|
325
|
+
assertNoForbiddenKeys(value);
|
|
326
|
+
const workflowRevision = requireString(value.workflowRevision, "fingerprints.workflowRevision");
|
|
327
|
+
const toolFingerprint = requireFingerprint(value.toolFingerprint, "fingerprints.toolFingerprint");
|
|
328
|
+
const policyFingerprint = requireFingerprint(value.policyFingerprint, "fingerprints.policyFingerprint");
|
|
329
|
+
const definitionHash = value.definitionHash === undefined
|
|
330
|
+
? undefined
|
|
331
|
+
: requireFingerprint(value.definitionHash, "fingerprints.definitionHash");
|
|
332
|
+
const imageDigest = value.imageDigest === undefined
|
|
333
|
+
? undefined
|
|
334
|
+
: requireString(value.imageDigest, "fingerprints.imageDigest");
|
|
335
|
+
if (imageDigest !== undefined && !/sha256:[a-f0-9]{64}/.test(imageDigest) && !SHA256_HEX.test(imageDigest)) {
|
|
336
|
+
// Allow either raw hex or docker digest form.
|
|
337
|
+
if (!imageDigest.includes("@sha256:") && !imageDigest.startsWith("sha256:")) {
|
|
338
|
+
throw new CodingCheckpointError("fingerprints.imageDigest must be a digest string");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
workflowRevision,
|
|
343
|
+
definitionHash,
|
|
344
|
+
imageDigest,
|
|
345
|
+
toolFingerprint,
|
|
346
|
+
policyFingerprint,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function validateArtifactRef(value, limits, options) {
|
|
350
|
+
const label = options?.label ?? "artifact";
|
|
351
|
+
if (!isPlainObject(value)) {
|
|
352
|
+
throw new CodingCheckpointError(`${label} must be an object`);
|
|
353
|
+
}
|
|
354
|
+
assertNoForbiddenKeys(value);
|
|
355
|
+
const kind = requireString(value.kind, `${label}.kind`);
|
|
356
|
+
if (!["plan", "workspace", "patch", "bundle", "diff", "other"].includes(kind)) {
|
|
357
|
+
throw new CodingCheckpointError(`${label}.kind is unsupported`);
|
|
358
|
+
}
|
|
359
|
+
if (options?.requireKind && kind !== options.requireKind) {
|
|
360
|
+
throw new CodingCheckpointError(`${label}.kind must be ${options.requireKind}`);
|
|
361
|
+
}
|
|
362
|
+
const uri = requireString(value.uri, `${label}.uri`);
|
|
363
|
+
if (uri.length > 2_048) {
|
|
364
|
+
throw new CodingCheckpointError(`${label}.uri exceeds 2048 characters`);
|
|
365
|
+
}
|
|
366
|
+
const sha256 = requireString(value.sha256, `${label}.sha256`).toLowerCase();
|
|
367
|
+
if (!SHA256_HEX.test(sha256)) {
|
|
368
|
+
throw new CodingCheckpointError(`${label}.sha256 must be a 64-char hex digest`);
|
|
369
|
+
}
|
|
370
|
+
const bytes = requireSafeInt(value.bytes, `${label}.bytes`);
|
|
371
|
+
if (bytes < 0 || bytes > limits.maxArtifactBytes) {
|
|
372
|
+
throw new CodingCheckpointError(`${label}.bytes out of range`);
|
|
373
|
+
}
|
|
374
|
+
return { kind, uri, sha256, bytes };
|
|
375
|
+
}
|
|
376
|
+
function validateCheckSummary(value, limits, label) {
|
|
377
|
+
if (!isPlainObject(value)) {
|
|
378
|
+
throw new CodingCheckpointError(`${label} must be an object`);
|
|
379
|
+
}
|
|
380
|
+
assertNoForbiddenKeys(value);
|
|
381
|
+
const name = requireString(value.name, `${label}.name`);
|
|
382
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(name)) {
|
|
383
|
+
throw new CodingCheckpointError(`${label}.name has invalid format`);
|
|
384
|
+
}
|
|
385
|
+
const exitCode = requireSafeInt(value.exitCode, `${label}.exitCode`);
|
|
386
|
+
if (exitCode < 0 || exitCode > 255) {
|
|
387
|
+
throw new CodingCheckpointError(`${label}.exitCode out of range`);
|
|
388
|
+
}
|
|
389
|
+
const summary = requireString(value.summary, `${label}.summary`);
|
|
390
|
+
assertByteLimit(`${label}.summary`, summary, limits.maxCheckSummaryBytes);
|
|
391
|
+
if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(summary)) {
|
|
392
|
+
throw new CodingCheckpointError(`${label}.summary contains control characters`);
|
|
393
|
+
}
|
|
394
|
+
return { name, exitCode, summary };
|
|
395
|
+
}
|
|
396
|
+
function validateTodo(value, limits, label) {
|
|
397
|
+
if (!isPlainObject(value)) {
|
|
398
|
+
throw new CodingCheckpointError(`${label} must be an object`);
|
|
399
|
+
}
|
|
400
|
+
assertNoForbiddenKeys(value);
|
|
401
|
+
const id = requireString(value.id, `${label}.id`);
|
|
402
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(id)) {
|
|
403
|
+
throw new CodingCheckpointError(`${label}.id has invalid format`);
|
|
404
|
+
}
|
|
405
|
+
const text = requireString(value.text, `${label}.text`);
|
|
406
|
+
assertTodoText(text, limits.maxTodoTextBytes);
|
|
407
|
+
if (typeof value.done !== "boolean") {
|
|
408
|
+
throw new CodingCheckpointError(`${label}.done must be a boolean`);
|
|
409
|
+
}
|
|
410
|
+
return { id, text, done: value.done };
|
|
411
|
+
}
|
|
412
|
+
function validateHandoffSummary(value, limits) {
|
|
413
|
+
if (!isPlainObject(value)) {
|
|
414
|
+
throw new CodingCheckpointError("handoff must be an object");
|
|
415
|
+
}
|
|
416
|
+
assertNoForbiddenKeys(value);
|
|
417
|
+
const base = requireString(value.base, "handoff.base");
|
|
418
|
+
const head = requireString(value.head, "handoff.head");
|
|
419
|
+
const changedPathCount = requireSafeInt(value.changedPathCount, "handoff.changedPathCount");
|
|
420
|
+
const checkCount = requireSafeInt(value.checkCount, "handoff.checkCount");
|
|
421
|
+
if (changedPathCount < 0 || checkCount < 0) {
|
|
422
|
+
throw new CodingCheckpointError("handoff counts must be non-negative");
|
|
423
|
+
}
|
|
424
|
+
const artifact = value.artifact === undefined
|
|
425
|
+
? undefined
|
|
426
|
+
: validateArtifactRef(value.artifact, limits, { label: "handoff.artifact" });
|
|
427
|
+
return { base, head, changedPathCount, checkCount, artifact };
|
|
428
|
+
}
|
|
429
|
+
function requireStatus(value) {
|
|
430
|
+
const status = requireString(value, "status");
|
|
431
|
+
const allowed = [
|
|
432
|
+
"planned",
|
|
433
|
+
"editing",
|
|
434
|
+
"checking",
|
|
435
|
+
"awaiting_approval",
|
|
436
|
+
"ready_for_handoff",
|
|
437
|
+
"completed",
|
|
438
|
+
"failed",
|
|
439
|
+
"cancelled",
|
|
440
|
+
];
|
|
441
|
+
if (!allowed.includes(status)) {
|
|
442
|
+
throw new CodingCheckpointError(`Unsupported coding task status: ${status}`);
|
|
443
|
+
}
|
|
444
|
+
return status;
|
|
445
|
+
}
|
|
446
|
+
function resolveUnderWorkspace(workspaceRoot, relativePath) {
|
|
447
|
+
const root = requireAbsolutePath(workspaceRoot, "workspaceRoot");
|
|
448
|
+
const rel = requireRelativePath(relativePath, "planPath");
|
|
449
|
+
const candidate = resolve(root, rel);
|
|
450
|
+
const relToRoot = relative(root, candidate);
|
|
451
|
+
if (relToRoot.startsWith("..") || isAbsolute(relToRoot)) {
|
|
452
|
+
throw new CodingCheckpointError("Plan path escapes workspace root");
|
|
453
|
+
}
|
|
454
|
+
return candidate;
|
|
455
|
+
}
|
|
456
|
+
function requireAbsolutePath(value, label) {
|
|
457
|
+
const path = requireString(value, label);
|
|
458
|
+
if (!isAbsolute(path)) {
|
|
459
|
+
throw new CodingCheckpointError(`${label} must be an absolute path`);
|
|
460
|
+
}
|
|
461
|
+
const normalized = normalize(path);
|
|
462
|
+
if (normalized.includes(`..${sep}`) || normalized.endsWith(`${sep}..`)) {
|
|
463
|
+
throw new CodingCheckpointError(`${label} must not contain parent segments`);
|
|
464
|
+
}
|
|
465
|
+
return normalized;
|
|
466
|
+
}
|
|
467
|
+
function requireRelativePath(value, label) {
|
|
468
|
+
const path = requireString(value, label);
|
|
469
|
+
if (isAbsolute(path) || path.split(/[\\/]/).includes("..")) {
|
|
470
|
+
throw new CodingCheckpointError(`${label} must be a relative path without parent segments`);
|
|
471
|
+
}
|
|
472
|
+
if (!path || path === ".") {
|
|
473
|
+
throw new CodingCheckpointError(`${label} must be a non-empty relative path`);
|
|
474
|
+
}
|
|
475
|
+
return path.replace(/\\/g, "/");
|
|
476
|
+
}
|
|
477
|
+
function requireBranch(value, label) {
|
|
478
|
+
const branch = requireString(value, label);
|
|
479
|
+
if (!BRANCH.test(branch) || branch.includes("..")) {
|
|
480
|
+
throw new CodingCheckpointError(`${label} has invalid format`);
|
|
481
|
+
}
|
|
482
|
+
return branch;
|
|
483
|
+
}
|
|
484
|
+
function requireFingerprint(value, label) {
|
|
485
|
+
const digest = requireString(value, label).toLowerCase();
|
|
486
|
+
if (!SHA256_HEX.test(digest)) {
|
|
487
|
+
throw new CodingCheckpointError(`${label} must be a 64-char hex digest`);
|
|
488
|
+
}
|
|
489
|
+
return digest;
|
|
490
|
+
}
|
|
491
|
+
function assertTodoText(text, maxBytes) {
|
|
492
|
+
if (!text) {
|
|
493
|
+
throw new CodingCheckpointError("Todo text must be non-empty");
|
|
494
|
+
}
|
|
495
|
+
assertByteLimit("todo text", text, maxBytes);
|
|
496
|
+
if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/.test(text)) {
|
|
497
|
+
throw new CodingCheckpointError("Todo text contains control characters");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function assertByteLimit(label, text, maxBytes) {
|
|
501
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
502
|
+
if (bytes > maxBytes) {
|
|
503
|
+
throw new CodingCheckpointError(`${label} exceeds ${maxBytes} byte limit`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function assertNoForbiddenKeys(value, path = "") {
|
|
507
|
+
for (const [key, child] of Object.entries(value)) {
|
|
508
|
+
const lower = key.toLowerCase();
|
|
509
|
+
if (FORBIDDEN_METADATA_KEYS.has(key) || FORBIDDEN_METADATA_KEYS.has(lower)) {
|
|
510
|
+
throw new CodingCheckpointError(`Forbidden coding checkpoint field: ${path}${key}`);
|
|
511
|
+
}
|
|
512
|
+
if (isPlainObject(child)) {
|
|
513
|
+
assertNoForbiddenKeys(child, `${path}${key}.`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function requireString(value, label) {
|
|
518
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
519
|
+
throw new CodingCheckpointError(`${label} must be a non-empty string`);
|
|
520
|
+
}
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
function requireSafeInt(value, label) {
|
|
524
|
+
if (!Number.isSafeInteger(value)) {
|
|
525
|
+
throw new CodingCheckpointError(`${label} must be a safe integer`);
|
|
526
|
+
}
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
function requireArray(value, label) {
|
|
530
|
+
if (!Array.isArray(value)) {
|
|
531
|
+
throw new CodingCheckpointError(`${label} must be an array`);
|
|
532
|
+
}
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
function isPlainObject(value) {
|
|
536
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
537
|
+
}
|
|
538
|
+
function isNonEmptyString(value) {
|
|
539
|
+
return typeof value === "string" && value.length > 0;
|
|
540
|
+
}
|
|
541
|
+
function stableStringify(value) {
|
|
542
|
+
return JSON.stringify(sortValue(value));
|
|
543
|
+
}
|
|
544
|
+
function sortValue(value) {
|
|
545
|
+
if (Array.isArray(value)) {
|
|
546
|
+
return value.map(sortValue);
|
|
547
|
+
}
|
|
548
|
+
if (isPlainObject(value)) {
|
|
549
|
+
const out = {};
|
|
550
|
+
for (const key of Object.keys(value).sort()) {
|
|
551
|
+
out[key] = sortValue(value[key]);
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
556
|
+
throw new CodingCheckpointError("Fingerprint input must not contain non-finite numbers");
|
|
557
|
+
}
|
|
558
|
+
if (typeof value === "bigint" || typeof value === "function" || typeof value === "symbol") {
|
|
559
|
+
throw new CodingCheckpointError("Fingerprint input contains unsupported values");
|
|
560
|
+
}
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
563
|
+
/** Exported for tests that need a quick digest helper without importing crypto. */
|
|
564
|
+
export function codingSha256Hex(data) {
|
|
565
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
566
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
567
|
+
}
|
|
568
|
+
// Keep path join available for hosts constructing plan paths.
|
|
569
|
+
export function codingPlanPathForTask(taskId) {
|
|
570
|
+
if (!TASK_ID.test(taskId)) {
|
|
571
|
+
throw new CodingCheckpointError("taskId has invalid format");
|
|
572
|
+
}
|
|
573
|
+
const safe = taskId.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
574
|
+
return join("plans", `${safe}.md`).replace(/\\/g, "/");
|
|
575
|
+
}
|
|
576
|
+
//# sourceMappingURL=coding-checkpoint.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare class GitError extends Error {
|
|
2
|
+
readonly code = "ERR_PRISM_GIT";
|
|
3
|
+
readonly exitCode: number | null;
|
|
4
|
+
constructor(message: string, exitCode?: number | null);
|
|
5
|
+
}
|
|
6
|
+
export interface GitExecRequest {
|
|
7
|
+
readonly args: readonly string[];
|
|
8
|
+
readonly cwd: string;
|
|
9
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly stdin?: Buffer | string;
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
12
|
+
readonly timeoutMs?: number;
|
|
13
|
+
readonly maxOutputBytes?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface GitExecResult {
|
|
16
|
+
readonly exitCode: number | null;
|
|
17
|
+
readonly stdout: Buffer;
|
|
18
|
+
readonly stderr: Buffer;
|
|
19
|
+
readonly timedOut: boolean;
|
|
20
|
+
readonly aborted: boolean;
|
|
21
|
+
readonly outputBytes: number;
|
|
22
|
+
}
|
|
23
|
+
export type GitRunner = (request: GitExecRequest & {
|
|
24
|
+
gitPath: string;
|
|
25
|
+
}) => Promise<GitExecResult>;
|
|
26
|
+
/** Noninteractive, pager-safe, credential-prompt-free baseline for Git child processes. */
|
|
27
|
+
export declare const SAFE_GIT_ENV: Readonly<Record<string, string>>;
|
|
28
|
+
/** Config flags prepended to every git invocation to disable hooks/external helpers. */
|
|
29
|
+
export declare const SAFE_GIT_CONFIG_ARGS: readonly ["-c", "core.hooksPath=/dev/null", "-c", "core.pager=cat", "-c", "sequence.editor=true", "-c", "credential.helper=", "-c", "advice.detachedHead=false"];
|
|
30
|
+
export declare function assertAbsoluteGit(path: string): Promise<string>;
|
|
31
|
+
/** Local spawn-based Git runner. Never invokes a shell. */
|
|
32
|
+
export declare function runGitCli(request: GitExecRequest & {
|
|
33
|
+
gitPath: string;
|
|
34
|
+
}): Promise<GitExecResult>;
|
|
35
|
+
export interface CreateGitRunnerOptions {
|
|
36
|
+
readonly gitPath?: string;
|
|
37
|
+
readonly runner?: GitRunner;
|
|
38
|
+
/** Optional sandbox-style execFile adapter. Collects streamed onData into stdout. */
|
|
39
|
+
readonly execFile?: (request: {
|
|
40
|
+
file: string;
|
|
41
|
+
args: readonly string[];
|
|
42
|
+
cwd?: string;
|
|
43
|
+
env?: Readonly<Record<string, string>>;
|
|
44
|
+
onData?: (data: Buffer) => void;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}) => Promise<{
|
|
48
|
+
exitCode: number | null;
|
|
49
|
+
}>;
|
|
50
|
+
readonly maxOutputBytes?: number;
|
|
51
|
+
readonly timeoutMs?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface BoundGitRunner {
|
|
54
|
+
readonly gitPath: string;
|
|
55
|
+
exec(request: GitExecRequest): Promise<GitExecResult>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a bound Git runner from an absolute git path, custom runner, or sandbox execFile.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createBoundGitRunner(options?: CreateGitRunnerOptions): Promise<BoundGitRunner>;
|
|
61
|
+
export declare function gitText(result: GitExecResult, stream?: "stdout" | "stderr"): string;
|
|
62
|
+
export declare function gitRequireOk(runner: BoundGitRunner, request: GitExecRequest, label: string): Promise<GitExecResult>;
|