@abelxiaoxing/cadence 1.0.12 → 1.0.14
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/config/.env.example +1 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +19 -10
- package/prompts/abel-implement.md +19 -6
- package/skills/abel-workflow/SKILL.md +32 -10
- package/src/candidate-preflight.ts +9 -6
- package/src/child-session.ts +75 -25
- package/src/contracts.ts +438 -230
- package/src/file-snapshot.ts +11 -20
- package/src/implement-graph.ts +571 -0
- package/src/patch.ts +103 -21
- package/src/runtime.ts +445 -119
- package/src/safe-path.ts +66 -0
- package/src/submit-tool.ts +26 -13
- package/src/verification-capability.ts +52 -67
- package/src/worker.ts +49 -0
package/src/safe-path.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { isValidRelativePath } from "./contracts.ts";
|
|
5
|
+
|
|
6
|
+
export type SafePathObservation =
|
|
7
|
+
| { kind: "file" }
|
|
8
|
+
| { kind: "directory" }
|
|
9
|
+
| { kind: "absent" }
|
|
10
|
+
| {
|
|
11
|
+
kind: "unsafe";
|
|
12
|
+
reason:
|
|
13
|
+
| "invalid-path"
|
|
14
|
+
| "root-unavailable"
|
|
15
|
+
| "path-unavailable"
|
|
16
|
+
| "symlink"
|
|
17
|
+
| "parent-not-directory"
|
|
18
|
+
| "special-file";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function observeSafePath(
|
|
22
|
+
root: string,
|
|
23
|
+
relative: string,
|
|
24
|
+
): SafePathObservation {
|
|
25
|
+
if (!isValidRelativePath(relative)) {
|
|
26
|
+
return { kind: "unsafe", reason: "invalid-path" };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const resolvedRoot = path.resolve(root);
|
|
30
|
+
const rootStat = lstatSync(resolvedRoot, { throwIfNoEntry: false });
|
|
31
|
+
if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) {
|
|
32
|
+
return { kind: "unsafe", reason: "root-unavailable" };
|
|
33
|
+
}
|
|
34
|
+
if (relative === ".") return { kind: "directory" };
|
|
35
|
+
|
|
36
|
+
let current = resolvedRoot;
|
|
37
|
+
const parts = relative.split("/");
|
|
38
|
+
for (const [index, part] of parts.entries()) {
|
|
39
|
+
current = path.join(current, part);
|
|
40
|
+
if (!current.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
41
|
+
return { kind: "unsafe", reason: "invalid-path" };
|
|
42
|
+
}
|
|
43
|
+
const stat = lstatSync(current, { throwIfNoEntry: false });
|
|
44
|
+
if (!stat) return { kind: "absent" };
|
|
45
|
+
if (stat.isSymbolicLink()) {
|
|
46
|
+
return { kind: "unsafe", reason: "symlink" };
|
|
47
|
+
}
|
|
48
|
+
if (index < parts.length - 1) {
|
|
49
|
+
if (!stat.isDirectory()) {
|
|
50
|
+
return { kind: "unsafe", reason: "parent-not-directory" };
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (stat.isFile()) return { kind: "file" };
|
|
55
|
+
if (stat.isDirectory()) return { kind: "directory" };
|
|
56
|
+
return { kind: "unsafe", reason: "special-file" };
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return { kind: "unsafe", reason: "path-unavailable" };
|
|
60
|
+
}
|
|
61
|
+
return { kind: "unsafe", reason: "invalid-path" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isSafeRegularFile(root: string, relative: string): boolean {
|
|
65
|
+
return observeSafePath(root, relative).kind === "file";
|
|
66
|
+
}
|
package/src/submit-tool.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
type CandidateFailure,
|
|
5
5
|
type DiffResult,
|
|
6
6
|
type EvidenceResult,
|
|
7
|
+
type SubmitFinalCategory,
|
|
8
|
+
type SubmitSchemaState,
|
|
7
9
|
validateDiffResult,
|
|
8
10
|
validateEvidenceResult,
|
|
9
11
|
} from "./contracts.ts";
|
|
@@ -74,12 +76,7 @@ const diffSchema = Type.Object({
|
|
|
74
76
|
contractCompliant: Type.Literal(true),
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
export type FinalCategory =
|
|
78
|
-
| "no-final-assistant"
|
|
79
|
-
| "text-only"
|
|
80
|
-
| "mixed"
|
|
81
|
-
| "multiple-submit"
|
|
82
|
-
| "single-submit-only";
|
|
79
|
+
export type FinalCategory = SubmitFinalCategory;
|
|
83
80
|
|
|
84
81
|
export interface IdentityOutcome {
|
|
85
82
|
request: boolean;
|
|
@@ -91,7 +88,7 @@ export interface IdentityOutcome {
|
|
|
91
88
|
export interface SubmitClassification {
|
|
92
89
|
finalCategory: FinalCategory;
|
|
93
90
|
attempts: number;
|
|
94
|
-
schema:
|
|
91
|
+
schema: SubmitSchemaState;
|
|
95
92
|
identity: IdentityOutcome;
|
|
96
93
|
}
|
|
97
94
|
|
|
@@ -105,7 +102,7 @@ export function createSubmitTool(input: {
|
|
|
105
102
|
let submitted: EvidenceResult | DiffResult | undefined;
|
|
106
103
|
let failure: CandidateFailure | undefined;
|
|
107
104
|
let attempts = 0;
|
|
108
|
-
let schema:
|
|
105
|
+
let schema: SubmitSchemaState = "not-submitted";
|
|
109
106
|
const identity: IdentityOutcome = {
|
|
110
107
|
request: true,
|
|
111
108
|
role: true,
|
|
@@ -134,7 +131,11 @@ export function createSubmitTool(input: {
|
|
|
134
131
|
identity.task = false;
|
|
135
132
|
identity.phase = false;
|
|
136
133
|
schema = "invalid";
|
|
137
|
-
failure = {
|
|
134
|
+
failure = {
|
|
135
|
+
kind: "artifact",
|
|
136
|
+
code: "invalid-structural-result",
|
|
137
|
+
stage: "structural-submit",
|
|
138
|
+
};
|
|
138
139
|
throw new Error("submitted result is not an object");
|
|
139
140
|
}
|
|
140
141
|
if (value.id !== input.requestId) identity.request = false;
|
|
@@ -158,15 +159,27 @@ export function createSubmitTool(input: {
|
|
|
158
159
|
const matches =
|
|
159
160
|
identity.request && identity.role && identity.task && identity.phase;
|
|
160
161
|
if (!validation.ok || !matches) {
|
|
161
|
-
failure =
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
failure = validation.ok
|
|
163
|
+
? {
|
|
164
|
+
kind: "artifact",
|
|
165
|
+
code: "structural-identity-mismatch",
|
|
166
|
+
stage: "structural-submit",
|
|
167
|
+
}
|
|
168
|
+
: (validation.failure ?? {
|
|
169
|
+
kind: "artifact",
|
|
170
|
+
code: "invalid-structural-result",
|
|
171
|
+
stage: "structural-submit",
|
|
172
|
+
});
|
|
164
173
|
throw new Error(
|
|
165
174
|
validation.reason ?? "submitted result identity does not match",
|
|
166
175
|
);
|
|
167
176
|
}
|
|
168
177
|
if (submitted) {
|
|
169
|
-
failure = {
|
|
178
|
+
failure = {
|
|
179
|
+
kind: "artifact",
|
|
180
|
+
code: "invalid-structural-result",
|
|
181
|
+
stage: "structural-submit",
|
|
182
|
+
};
|
|
170
183
|
throw new Error("duplicate structural submission");
|
|
171
184
|
}
|
|
172
185
|
submitted = structuredClone(value) as unknown as
|
|
@@ -2,12 +2,13 @@ import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
type AtomicVerificationContract,
|
|
5
|
-
|
|
5
|
+
type StructuredVerificationContract,
|
|
6
6
|
type VerificationAdapterCode,
|
|
7
7
|
validateVerificationContract,
|
|
8
8
|
verificationInputPaths,
|
|
9
9
|
verificationSteps,
|
|
10
10
|
} from "./contracts.ts";
|
|
11
|
+
import { isSafeRegularFile, observeSafePath } from "./safe-path.ts";
|
|
11
12
|
|
|
12
13
|
export type VerificationCapabilityDiagnostic =
|
|
13
14
|
| {
|
|
@@ -37,11 +38,6 @@ export interface VerificationRunnerBinding {
|
|
|
37
38
|
mountSource?: string;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export interface VerificationReadiness {
|
|
41
|
-
taskContractsExecutable: boolean;
|
|
42
|
-
diagnostics: VerificationCapabilityDiagnostic[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
41
|
const SHELL_OPERATOR = /[;&|`$<>\n\r\0]/u;
|
|
46
42
|
const SIMPLE_TOKEN =
|
|
47
43
|
/^(?:[a-z0-9][a-z0-9._:@/=-]*|--?[a-z0-9][a-z0-9._:@/=-]*)$/iu;
|
|
@@ -88,18 +84,9 @@ function fileStatus(
|
|
|
88
84
|
root: string,
|
|
89
85
|
relative: string,
|
|
90
86
|
): "regular" | "absent" | "unsafe" {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
absolute === resolvedRoot ||
|
|
96
|
-
!absolute.startsWith(`${resolvedRoot}${path.sep}`)
|
|
97
|
-
) {
|
|
98
|
-
return "unsafe";
|
|
99
|
-
}
|
|
100
|
-
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
101
|
-
if (!stat) return "absent";
|
|
102
|
-
return stat.isFile() && !stat.isSymbolicLink() ? "regular" : "unsafe";
|
|
87
|
+
const observation = observeSafePath(root, relative);
|
|
88
|
+
if (observation.kind === "file") return "regular";
|
|
89
|
+
return observation.kind === "absent" ? "absent" : "unsafe";
|
|
103
90
|
}
|
|
104
91
|
|
|
105
92
|
function regularFile(root: string, relative: string): boolean {
|
|
@@ -287,7 +274,7 @@ function validatePackageScript(
|
|
|
287
274
|
verificationId: string,
|
|
288
275
|
packageManager: string,
|
|
289
276
|
script: string,
|
|
290
|
-
approvedCommand
|
|
277
|
+
approvedCommand: string,
|
|
291
278
|
dependencyOwner = root,
|
|
292
279
|
): VerificationCapabilityResult {
|
|
293
280
|
const packageRunner = resolveVerificationRunner(packageManager);
|
|
@@ -311,7 +298,7 @@ function validatePackageScript(
|
|
|
311
298
|
`package.json script ${script} is missing`,
|
|
312
299
|
);
|
|
313
300
|
}
|
|
314
|
-
if (
|
|
301
|
+
if (actual !== approvedCommand) {
|
|
315
302
|
return adapterFailure(
|
|
316
303
|
verificationId,
|
|
317
304
|
"script-command-mismatch",
|
|
@@ -336,21 +323,7 @@ function validateAtomicCapability(
|
|
|
336
323
|
root: string,
|
|
337
324
|
step: AtomicVerificationContract,
|
|
338
325
|
dependencyOwner: string,
|
|
339
|
-
allowedMissingInputs: ReadonlySet<string>,
|
|
340
326
|
): VerificationCapabilityResult {
|
|
341
|
-
for (const input of verificationInputPaths(step)) {
|
|
342
|
-
const status = fileStatus(root, input);
|
|
343
|
-
if (
|
|
344
|
-
status !== "regular" &&
|
|
345
|
-
!(status === "absent" && allowedMissingInputs.has(input))
|
|
346
|
-
) {
|
|
347
|
-
return adapterFailure(
|
|
348
|
-
step.id,
|
|
349
|
-
"input-missing",
|
|
350
|
-
`verification input ${input} is missing or unsafe`,
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
327
|
if (step.kind === "package-script") {
|
|
355
328
|
return validatePackageScript(
|
|
356
329
|
root,
|
|
@@ -419,8 +392,24 @@ export function validateVerificationCapability(
|
|
|
419
392
|
value: unknown,
|
|
420
393
|
options: {
|
|
421
394
|
dependencyOwner?: string;
|
|
422
|
-
allowedMissingInputs?: readonly string[];
|
|
423
395
|
} = {},
|
|
396
|
+
): VerificationCapabilityResult {
|
|
397
|
+
return validateCapability(root, value, options, true);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function validateVerificationAdapterCapability(
|
|
401
|
+
root: string,
|
|
402
|
+
value: unknown,
|
|
403
|
+
options: { dependencyOwner?: string } = {},
|
|
404
|
+
): VerificationCapabilityResult {
|
|
405
|
+
return validateCapability(root, value, options, false);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function validateCapability(
|
|
409
|
+
root: string,
|
|
410
|
+
value: unknown,
|
|
411
|
+
options: { dependencyOwner?: string },
|
|
412
|
+
requireInputAvailability: boolean,
|
|
424
413
|
): VerificationCapabilityResult {
|
|
425
414
|
const validation = validateVerificationContract(value);
|
|
426
415
|
if (!validation.ok) {
|
|
@@ -442,40 +431,36 @@ export function validateVerificationCapability(
|
|
|
442
431
|
};
|
|
443
432
|
}
|
|
444
433
|
const dependencyOwner = options.dependencyOwner ?? root;
|
|
445
|
-
|
|
434
|
+
return validateAcceptedVerificationCapability(
|
|
435
|
+
root,
|
|
436
|
+
validation.value,
|
|
437
|
+
dependencyOwner,
|
|
438
|
+
requireInputAvailability,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function validateAcceptedVerificationCapability(
|
|
443
|
+
root: string,
|
|
444
|
+
verification: StructuredVerificationContract,
|
|
445
|
+
dependencyOwner: string,
|
|
446
|
+
requireInputAvailability: boolean,
|
|
447
|
+
): VerificationCapabilityResult {
|
|
448
|
+
if (requireInputAvailability) {
|
|
449
|
+
for (const input of verificationInputPaths(verification)) {
|
|
450
|
+
if (!isSafeRegularFile(root, input)) {
|
|
451
|
+
return adapterFailure(
|
|
452
|
+
verification.id,
|
|
453
|
+
"input-missing",
|
|
454
|
+
`verification input ${input} is missing or unsafe`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
446
459
|
const runnerBindings: VerificationRunnerBinding[] = [];
|
|
447
|
-
for (const step of verificationSteps(
|
|
448
|
-
const capability = validateAtomicCapability(
|
|
449
|
-
root,
|
|
450
|
-
step,
|
|
451
|
-
dependencyOwner,
|
|
452
|
-
allowedMissingInputs,
|
|
453
|
-
);
|
|
460
|
+
for (const step of verificationSteps(verification)) {
|
|
461
|
+
const capability = validateAtomicCapability(root, step, dependencyOwner);
|
|
454
462
|
if (!capability.ok) return capability;
|
|
455
463
|
runnerBindings.push(...capability.runnerBindings);
|
|
456
464
|
}
|
|
457
|
-
return capabilitySuccess(
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export function assessVerificationReadiness(
|
|
461
|
-
root: string,
|
|
462
|
-
contracts: readonly unknown[],
|
|
463
|
-
options: {
|
|
464
|
-
dependencyOwner?: string;
|
|
465
|
-
allowedMissingInputs?: readonly string[];
|
|
466
|
-
} = {},
|
|
467
|
-
): VerificationReadiness {
|
|
468
|
-
const diagnostics = contracts
|
|
469
|
-
.map((contract) => validateVerificationCapability(root, contract, options))
|
|
470
|
-
.filter(
|
|
471
|
-
(
|
|
472
|
-
result,
|
|
473
|
-
): result is Extract<VerificationCapabilityResult, { ok: false }> =>
|
|
474
|
-
!result.ok,
|
|
475
|
-
)
|
|
476
|
-
.map((result) => result.diagnostic);
|
|
477
|
-
return {
|
|
478
|
-
taskContractsExecutable: diagnostics.length === 0,
|
|
479
|
-
diagnostics,
|
|
480
|
-
};
|
|
465
|
+
return capabilitySuccess(verification.id, runnerBindings);
|
|
481
466
|
}
|
package/src/worker.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type {
|
|
4
4
|
CandidateFailure,
|
|
5
5
|
ImplementationPhase,
|
|
6
|
+
ImplementGraphBoundary,
|
|
6
7
|
PhaseAttempt,
|
|
7
8
|
TaskBoundary,
|
|
8
9
|
TaskFailure,
|
|
@@ -42,6 +43,22 @@ export interface TaskRecord {
|
|
|
42
43
|
state: TaskState;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export interface ImplementGraphRecord {
|
|
47
|
+
key: string;
|
|
48
|
+
workspaceRoot: string;
|
|
49
|
+
graphHash: string;
|
|
50
|
+
boundary: ImplementGraphBoundary;
|
|
51
|
+
completedTasks: Set<string>;
|
|
52
|
+
blockedTasks: Set<string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function graphRecordKey(
|
|
56
|
+
workspaceRoot: string,
|
|
57
|
+
changeId: string,
|
|
58
|
+
): string {
|
|
59
|
+
return `${workspaceRoot}\0${changeId}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
export function taskRecordKey(
|
|
46
63
|
workspaceRoot: string,
|
|
47
64
|
changeId: string,
|
|
@@ -97,6 +114,37 @@ export function workerIdentity(model: {
|
|
|
97
114
|
|
|
98
115
|
export class WorkerRegistry {
|
|
99
116
|
private readonly tasks = new Map<string, TaskRecord>();
|
|
117
|
+
private readonly graphs = new Map<string, ImplementGraphRecord>();
|
|
118
|
+
|
|
119
|
+
admitGraph(
|
|
120
|
+
boundary: ImplementGraphBoundary,
|
|
121
|
+
graphHash: string,
|
|
122
|
+
workspaceRoot: string,
|
|
123
|
+
state: {
|
|
124
|
+
completedTasks: readonly string[];
|
|
125
|
+
blockedTasks: readonly string[];
|
|
126
|
+
},
|
|
127
|
+
): ImplementGraphRecord {
|
|
128
|
+
const key = graphRecordKey(workspaceRoot, boundary.changeId);
|
|
129
|
+
if (this.graphs.has(key)) throw new Error("duplicate graph admission");
|
|
130
|
+
const record: ImplementGraphRecord = {
|
|
131
|
+
key,
|
|
132
|
+
workspaceRoot,
|
|
133
|
+
graphHash,
|
|
134
|
+
boundary: structuredClone(boundary),
|
|
135
|
+
completedTasks: new Set(state.completedTasks),
|
|
136
|
+
blockedTasks: new Set(state.blockedTasks),
|
|
137
|
+
};
|
|
138
|
+
this.graphs.set(key, record);
|
|
139
|
+
return record;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getGraph(
|
|
143
|
+
workspaceRoot: string,
|
|
144
|
+
changeId: string,
|
|
145
|
+
): ImplementGraphRecord | undefined {
|
|
146
|
+
return this.graphs.get(graphRecordKey(workspaceRoot, changeId));
|
|
147
|
+
}
|
|
100
148
|
|
|
101
149
|
has(key: string): boolean {
|
|
102
150
|
return this.tasks.has(key);
|
|
@@ -151,5 +199,6 @@ export class WorkerRegistry {
|
|
|
151
199
|
|
|
152
200
|
clear(): void {
|
|
153
201
|
this.tasks.clear();
|
|
202
|
+
this.graphs.clear();
|
|
154
203
|
}
|
|
155
204
|
}
|