@abelxiaoxing/cadence 1.0.1 → 1.0.3
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/agents/contract-reviewer.md +2 -2
- package/agents/diagnosis-worker.md +3 -2
- package/agents/implementation-worker.md +3 -2
- package/package.json +1 -1
- package/prompts/abel-design.md +13 -3
- package/prompts/abel-implement.md +53 -13
- package/skills/abel-workflow/SKILL.md +60 -22
- package/src/candidate-preflight.ts +1468 -0
- package/src/child-session.ts +136 -30
- package/src/contracts.ts +1343 -63
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +99 -28
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +166 -21
- package/src/patch.ts +331 -33
- package/src/result-store.ts +282 -7
- package/src/runtime.ts +1584 -154
- package/src/scheduler.ts +60 -16
- package/src/scoped-tools.ts +27 -6
- package/src/subagent-activity.ts +56 -25
- package/src/submit-tool.ts +15 -3
- package/src/worker.ts +114 -85
package/src/patch.ts
CHANGED
|
@@ -1,113 +1,411 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { preflightCandidate } from "./candidate-preflight.ts";
|
|
13
|
+
import {
|
|
14
|
+
type AgentsCheckpointRequest,
|
|
15
|
+
type ApplyCandidateResult,
|
|
16
|
+
type CandidateFailure,
|
|
17
|
+
diffWritePaths,
|
|
18
|
+
validateAgentsCheckpointRequest,
|
|
19
|
+
} from "./contracts.ts";
|
|
20
|
+
import { type Bound, isCurrent } from "./file-snapshot.ts";
|
|
4
21
|
import type { ResultStore } from "./result-store.ts";
|
|
5
22
|
|
|
6
23
|
interface CommandResult {
|
|
7
24
|
code: number;
|
|
8
25
|
stdout: Buffer;
|
|
9
26
|
stderr: Buffer;
|
|
27
|
+
cancelled: boolean;
|
|
10
28
|
}
|
|
11
29
|
|
|
12
30
|
async function git(
|
|
13
31
|
root: string,
|
|
14
32
|
args: string[],
|
|
15
33
|
input: Buffer,
|
|
34
|
+
signal?: AbortSignal,
|
|
16
35
|
): Promise<CommandResult> {
|
|
17
36
|
return new Promise((resolve, reject) => {
|
|
18
37
|
const child = spawn("git", args, {
|
|
19
38
|
cwd: root,
|
|
20
39
|
shell: false,
|
|
21
40
|
stdio: ["pipe", "pipe", "pipe"],
|
|
41
|
+
...(signal ? { signal } : {}),
|
|
22
42
|
});
|
|
23
43
|
const stdout: Buffer[] = [];
|
|
24
44
|
const stderr: Buffer[] = [];
|
|
45
|
+
let settled = false;
|
|
46
|
+
const finish = (result: CommandResult) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
resolve(result);
|
|
50
|
+
};
|
|
25
51
|
child.stdout.on("data", (b: Buffer) => stdout.push(b));
|
|
26
52
|
child.stderr.on("data", (b: Buffer) => stderr.push(b));
|
|
27
|
-
child.on("error",
|
|
53
|
+
child.on("error", (error) => {
|
|
54
|
+
if (signal?.aborted || error.name === "AbortError") {
|
|
55
|
+
finish({
|
|
56
|
+
code: 1,
|
|
57
|
+
stdout: Buffer.concat(stdout),
|
|
58
|
+
stderr: Buffer.concat(stderr),
|
|
59
|
+
cancelled: true,
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
reject(error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
28
65
|
child.on("close", (code) =>
|
|
29
|
-
|
|
66
|
+
finish({
|
|
30
67
|
code: code ?? 1,
|
|
31
68
|
stdout: Buffer.concat(stdout),
|
|
32
69
|
stderr: Buffer.concat(stderr),
|
|
70
|
+
cancelled: signal?.aborted ?? false,
|
|
33
71
|
}),
|
|
34
72
|
);
|
|
35
73
|
child.stdin.end(input);
|
|
36
74
|
});
|
|
37
75
|
}
|
|
38
76
|
|
|
77
|
+
function failure(value: CandidateFailure): {
|
|
78
|
+
ok: false;
|
|
79
|
+
failure: CandidateFailure;
|
|
80
|
+
} {
|
|
81
|
+
return { ok: false, failure: value };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cancellationFailure(): {
|
|
85
|
+
ok: false;
|
|
86
|
+
failure: CandidateFailure;
|
|
87
|
+
} {
|
|
88
|
+
return failure({ kind: "cancelled", code: "cancelled" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type AgentsCheckpointResult =
|
|
92
|
+
| {
|
|
93
|
+
ok: true;
|
|
94
|
+
target: string;
|
|
95
|
+
agentsImpact: AgentsCheckpointRequest["agentsImpact"];
|
|
96
|
+
checkExitCode: 0;
|
|
97
|
+
applyExitCode: 0;
|
|
98
|
+
}
|
|
99
|
+
| {
|
|
100
|
+
ok: false;
|
|
101
|
+
failure: CandidateFailure;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const AGENTS_START = "<!-- ABEL:AGENTS-INDEX:START -->";
|
|
105
|
+
const AGENTS_END = "<!-- ABEL:AGENTS-INDEX:END -->";
|
|
106
|
+
const NONREGULAR_AGENTS_TARGET = Symbol("nonregular AGENTS target");
|
|
107
|
+
|
|
108
|
+
function managedParts(text: string): { prefix: string; suffix: string } | null {
|
|
109
|
+
const lines = text.split("\n");
|
|
110
|
+
const starts: Array<{ line: number; start: number; end: number }> = [];
|
|
111
|
+
const ends: Array<{ line: number; start: number; end: number }> = [];
|
|
112
|
+
let offset = 0;
|
|
113
|
+
for (const [line, value] of lines.entries()) {
|
|
114
|
+
const range = { line, start: offset, end: offset + value.length };
|
|
115
|
+
if (value === AGENTS_START) starts.push(range);
|
|
116
|
+
if (value === AGENTS_END) ends.push(range);
|
|
117
|
+
offset = range.end + 1;
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
starts.length !== 1 ||
|
|
121
|
+
ends.length !== 1 ||
|
|
122
|
+
starts[0].line >= ends[0].line
|
|
123
|
+
) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
prefix: text.slice(0, starts[0].start),
|
|
128
|
+
suffix: text.slice(ends[0].end),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function checkpointContentIsScoped(
|
|
133
|
+
impact: AgentsCheckpointRequest["agentsImpact"],
|
|
134
|
+
before: string | null,
|
|
135
|
+
after: string | null,
|
|
136
|
+
): boolean {
|
|
137
|
+
const beforeParts = before === null ? null : managedParts(before);
|
|
138
|
+
const afterParts = after === null ? null : managedParts(after);
|
|
139
|
+
if (impact === "update-existing") {
|
|
140
|
+
return (
|
|
141
|
+
beforeParts !== null &&
|
|
142
|
+
afterParts !== null &&
|
|
143
|
+
beforeParts.prefix === afterParts.prefix &&
|
|
144
|
+
beforeParts.suffix === afterParts.suffix
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (impact === "create-index") {
|
|
148
|
+
return (
|
|
149
|
+
before === null &&
|
|
150
|
+
afterParts !== null &&
|
|
151
|
+
afterParts.prefix.trim() === "" &&
|
|
152
|
+
afterParts.suffix.trim() === ""
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (beforeParts === null) return false;
|
|
156
|
+
if (after === null) {
|
|
157
|
+
return beforeParts.prefix.trim() === "" && beforeParts.suffix.trim() === "";
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
managedParts(after) === null &&
|
|
161
|
+
after === `${beforeParts.prefix}${beforeParts.suffix}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function regularText(
|
|
166
|
+
root: string,
|
|
167
|
+
target: string,
|
|
168
|
+
): string | null | typeof NONREGULAR_AGENTS_TARGET {
|
|
169
|
+
const resolvedRoot = path.resolve(root);
|
|
170
|
+
const absolute = path.resolve(resolvedRoot, target);
|
|
171
|
+
if (
|
|
172
|
+
absolute === resolvedRoot ||
|
|
173
|
+
!absolute.startsWith(`${resolvedRoot}${path.sep}`)
|
|
174
|
+
) {
|
|
175
|
+
throw new Error("AGENTS target escapes the root");
|
|
176
|
+
}
|
|
177
|
+
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
178
|
+
if (!stat) return null;
|
|
179
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
180
|
+
return NONREGULAR_AGENTS_TARGET;
|
|
181
|
+
}
|
|
182
|
+
return readFileSync(absolute, "utf8");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function applyAgentsCheckpoint(
|
|
186
|
+
root: string,
|
|
187
|
+
value: unknown,
|
|
188
|
+
signal?: AbortSignal,
|
|
189
|
+
): Promise<AgentsCheckpointResult> {
|
|
190
|
+
const validation = validateAgentsCheckpointRequest(value);
|
|
191
|
+
if (!validation.ok) {
|
|
192
|
+
return failure({ kind: "artifact", code: "invalid-checkpoint-contract" });
|
|
193
|
+
}
|
|
194
|
+
const request = validation.value;
|
|
195
|
+
if (signal?.aborted) {
|
|
196
|
+
return cancellationFailure();
|
|
197
|
+
}
|
|
198
|
+
const before = regularText(root, request.agentsTarget);
|
|
199
|
+
if (before === NONREGULAR_AGENTS_TARGET) {
|
|
200
|
+
return failure({ kind: "artifact", code: "nonregular-mode" });
|
|
201
|
+
}
|
|
202
|
+
if (!isCurrent(root, request.snapshot as Bound)) {
|
|
203
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
204
|
+
}
|
|
205
|
+
let targets: string[];
|
|
206
|
+
try {
|
|
207
|
+
targets = diffWritePaths(request.diff).paths;
|
|
208
|
+
} catch {
|
|
209
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
210
|
+
}
|
|
211
|
+
if (targets.length !== 1 || targets[0] !== request.agentsTarget) {
|
|
212
|
+
return failure({ kind: "artifact", code: "agents-target-mismatch" });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let temp: string | undefined;
|
|
216
|
+
try {
|
|
217
|
+
if (
|
|
218
|
+
(request.agentsImpact === "create-index" && before !== null) ||
|
|
219
|
+
(request.agentsImpact !== "create-index" && before === null)
|
|
220
|
+
) {
|
|
221
|
+
return failure({ kind: "artifact", code: "agents-impact-mismatch" });
|
|
222
|
+
}
|
|
223
|
+
temp = mkdtempSync(path.join(tmpdir(), "cadence-agents-checkpoint-"));
|
|
224
|
+
if (before !== null) {
|
|
225
|
+
const temporaryTarget = path.join(temp, request.agentsTarget);
|
|
226
|
+
mkdirSync(path.dirname(temporaryTarget), { recursive: true });
|
|
227
|
+
copyFileSync(path.join(root, request.agentsTarget), temporaryTarget);
|
|
228
|
+
}
|
|
229
|
+
const bytes = Buffer.from(request.diff, "utf8");
|
|
230
|
+
const candidateCheck = await git(
|
|
231
|
+
temp,
|
|
232
|
+
["apply", "--check", "--recount", "--whitespace=nowarn", "-"],
|
|
233
|
+
bytes,
|
|
234
|
+
signal,
|
|
235
|
+
);
|
|
236
|
+
if (candidateCheck.cancelled || signal?.aborted) {
|
|
237
|
+
return cancellationFailure();
|
|
238
|
+
}
|
|
239
|
+
if (candidateCheck.code !== 0) {
|
|
240
|
+
return failure({ kind: "artifact", code: "git-apply-check-failed" });
|
|
241
|
+
}
|
|
242
|
+
const candidateApply = await git(
|
|
243
|
+
temp,
|
|
244
|
+
["apply", "--recount", "--whitespace=nowarn", "-"],
|
|
245
|
+
bytes,
|
|
246
|
+
signal,
|
|
247
|
+
);
|
|
248
|
+
if (candidateApply.cancelled || signal?.aborted) {
|
|
249
|
+
return cancellationFailure();
|
|
250
|
+
}
|
|
251
|
+
if (candidateApply.code !== 0) {
|
|
252
|
+
return failure({ kind: "artifact", code: "git-apply-failed" });
|
|
253
|
+
}
|
|
254
|
+
const after = regularText(temp, request.agentsTarget);
|
|
255
|
+
if (after === NONREGULAR_AGENTS_TARGET) {
|
|
256
|
+
return failure({ kind: "artifact", code: "nonregular-mode" });
|
|
257
|
+
}
|
|
258
|
+
if (!checkpointContentIsScoped(request.agentsImpact, before, after)) {
|
|
259
|
+
return failure({ kind: "artifact", code: "outside-managed-region" });
|
|
260
|
+
}
|
|
261
|
+
if (!isCurrent(root, request.snapshot as Bound)) {
|
|
262
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
263
|
+
}
|
|
264
|
+
const check = await git(
|
|
265
|
+
root,
|
|
266
|
+
["apply", "--check", "--recount", "--whitespace=nowarn", "-"],
|
|
267
|
+
bytes,
|
|
268
|
+
signal,
|
|
269
|
+
);
|
|
270
|
+
if (check.cancelled || signal?.aborted) {
|
|
271
|
+
return cancellationFailure();
|
|
272
|
+
}
|
|
273
|
+
if (check.code !== 0) {
|
|
274
|
+
return failure({ kind: "stale", code: "git-apply-check-failed" });
|
|
275
|
+
}
|
|
276
|
+
const apply = await git(
|
|
277
|
+
root,
|
|
278
|
+
["apply", "--recount", "--whitespace=nowarn", "-"],
|
|
279
|
+
bytes,
|
|
280
|
+
);
|
|
281
|
+
if (apply.code !== 0) {
|
|
282
|
+
return failure({ kind: "environment", code: "git-apply-failed" });
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
target: request.agentsTarget,
|
|
287
|
+
agentsImpact: request.agentsImpact,
|
|
288
|
+
checkExitCode: 0,
|
|
289
|
+
applyExitCode: 0,
|
|
290
|
+
};
|
|
291
|
+
} finally {
|
|
292
|
+
if (temp) rmSync(temp, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
39
296
|
export async function applyRetainedPatch(input: {
|
|
40
297
|
root: string;
|
|
41
298
|
id: string;
|
|
42
299
|
store: ResultStore;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const retained = input.store.get(input.id);
|
|
48
|
-
if (!retained) return { ok: false, error: "retained result not found" };
|
|
300
|
+
signal?: AbortSignal;
|
|
301
|
+
}): Promise<ApplyCandidateResult> {
|
|
302
|
+
if (input.signal?.aborted) return cancellationFailure();
|
|
303
|
+
const retained = input.store.resolveForApply(input.id);
|
|
49
304
|
if (retained.root !== input.root)
|
|
50
|
-
|
|
51
|
-
if (!isCurrent(input.root, retained.snapshot))
|
|
52
|
-
return {
|
|
305
|
+
throw new Error("retained result root mismatch");
|
|
306
|
+
if (!retained.verification && !isCurrent(input.root, retained.snapshot))
|
|
307
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
53
308
|
|
|
54
309
|
let targets: string[];
|
|
55
310
|
try {
|
|
56
311
|
targets = diffWritePaths(retained.diff.toString("utf8")).paths;
|
|
57
|
-
} catch
|
|
58
|
-
return {
|
|
312
|
+
} catch {
|
|
313
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
59
314
|
}
|
|
60
315
|
if (targets.some((path) => !retained.writeSet.includes(path))) {
|
|
61
|
-
return {
|
|
62
|
-
ok: false,
|
|
63
|
-
error: "patch target is outside the declared write set",
|
|
64
|
-
};
|
|
316
|
+
return failure({ kind: "artifact", code: "write-set-mismatch" });
|
|
65
317
|
}
|
|
66
318
|
|
|
319
|
+
if (retained.verification) {
|
|
320
|
+
if (
|
|
321
|
+
!retained.packageManifest ||
|
|
322
|
+
!retained.lockfile ||
|
|
323
|
+
!retained.dependencyTarget
|
|
324
|
+
) {
|
|
325
|
+
throw new Error("retained candidate preflight inputs are incomplete");
|
|
326
|
+
}
|
|
327
|
+
const preflight = await preflightCandidate({
|
|
328
|
+
root: input.root,
|
|
329
|
+
diff: retained.diff,
|
|
330
|
+
writeSet: retained.writeSet,
|
|
331
|
+
approvedDependencies: retained.approvedDependencies,
|
|
332
|
+
snapshot: retained.snapshot,
|
|
333
|
+
baseline: retained.baseline,
|
|
334
|
+
verification: retained.verification,
|
|
335
|
+
packageManifest: retained.packageManifest,
|
|
336
|
+
lockfile: retained.lockfile,
|
|
337
|
+
dependencyTarget: retained.dependencyTarget,
|
|
338
|
+
signal: input.signal,
|
|
339
|
+
});
|
|
340
|
+
if (!preflight.ok) {
|
|
341
|
+
switch (preflight.kind) {
|
|
342
|
+
case "artifact":
|
|
343
|
+
return failure({
|
|
344
|
+
kind: preflight.kind,
|
|
345
|
+
code: preflight.code,
|
|
346
|
+
...(preflight.excerpt ? { evidence: [preflight.excerpt] } : {}),
|
|
347
|
+
});
|
|
348
|
+
case "stale":
|
|
349
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
350
|
+
case "environment":
|
|
351
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
352
|
+
case "approval-boundary":
|
|
353
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
354
|
+
case "cancelled":
|
|
355
|
+
return failure({ kind: preflight.kind, code: preflight.code });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (!isCurrent(input.root, retained.snapshot)) {
|
|
359
|
+
return failure({ kind: "stale", code: "stale-snapshot" });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (input.signal?.aborted) return cancellationFailure();
|
|
363
|
+
|
|
67
364
|
const numstat = await git(
|
|
68
365
|
input.root,
|
|
69
366
|
["apply", "--numstat", "-z", "--recount", "-"],
|
|
70
367
|
retained.diff,
|
|
368
|
+
input.signal,
|
|
71
369
|
);
|
|
370
|
+
if (numstat.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
72
371
|
if (numstat.code !== 0 || numstat.stdout.includes(Buffer.from("-\t-\t"))) {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
error: `git numstat screening failed: ${numstat.stderr.toString("utf8")}`,
|
|
76
|
-
};
|
|
372
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
77
373
|
}
|
|
78
374
|
const summary = await git(
|
|
79
375
|
input.root,
|
|
80
376
|
["apply", "--summary", "--recount", "-"],
|
|
81
377
|
retained.diff,
|
|
378
|
+
input.signal,
|
|
82
379
|
);
|
|
380
|
+
if (summary.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
83
381
|
if (
|
|
84
382
|
summary.code !== 0 ||
|
|
85
383
|
/rename|copy|mode change|create mode 160000/i.test(
|
|
86
384
|
summary.stdout.toString("utf8"),
|
|
87
385
|
)
|
|
88
386
|
) {
|
|
89
|
-
return {
|
|
387
|
+
return failure({ kind: "artifact", code: "invalid-diff" });
|
|
90
388
|
}
|
|
91
389
|
const check = await git(
|
|
92
390
|
input.root,
|
|
93
391
|
["apply", "--check", "--recount", "--whitespace=nowarn", "-"],
|
|
94
392
|
retained.diff,
|
|
393
|
+
input.signal,
|
|
95
394
|
);
|
|
395
|
+
if (check.cancelled || input.signal?.aborted) return cancellationFailure();
|
|
96
396
|
if (check.code !== 0)
|
|
97
|
-
return {
|
|
98
|
-
|
|
99
|
-
error: `git apply --check failed: ${check.stderr.toString("utf8")}`,
|
|
100
|
-
};
|
|
397
|
+
return failure({ kind: "artifact", code: "git-apply-check-failed" });
|
|
398
|
+
if (input.signal?.aborted) return cancellationFailure();
|
|
101
399
|
const apply = await git(
|
|
102
400
|
input.root,
|
|
103
401
|
["apply", "--recount", "--whitespace=nowarn", "-"],
|
|
104
402
|
retained.diff,
|
|
105
403
|
);
|
|
106
404
|
if (apply.code !== 0)
|
|
107
|
-
return {
|
|
108
|
-
ok: false,
|
|
109
|
-
error: `git apply failed: ${apply.stderr.toString("utf8")}`,
|
|
110
|
-
};
|
|
405
|
+
return failure({ kind: "environment", code: "git-apply-failed" });
|
|
111
406
|
input.store.discard(input.id);
|
|
112
|
-
return {
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
result: { targets, checkExitCode: 0, applyExitCode: 0 },
|
|
410
|
+
};
|
|
113
411
|
}
|