@abelxiaoxing/cadence 1.0.1 → 1.0.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/package.json +1 -1
- package/prompts/abel-implement.md +13 -2
- package/skills/abel-workflow/SKILL.md +7 -1
- package/src/candidate-preflight.ts +1163 -0
- package/src/child-session.ts +46 -5
- package/src/contracts.ts +379 -48
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +55 -10
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +91 -12
- package/src/patch.ts +73 -3
- package/src/result-store.ts +40 -3
- package/src/runtime.ts +775 -66
- package/src/scheduler.ts +1 -0
- package/src/scoped-tools.ts +27 -6
- package/src/submit-tool.ts +3 -1
- package/src/worker.ts +136 -26
package/src/scheduler.ts
CHANGED
package/src/scoped-tools.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface ScopedToolDef {
|
|
|
31
31
|
|
|
32
32
|
interface Options {
|
|
33
33
|
roots: string[];
|
|
34
|
+
allowedPaths?: string[];
|
|
34
35
|
observer?: (obs: Observation) => void;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -38,7 +39,11 @@ type PathResult =
|
|
|
38
39
|
| { ok: true; abs: string; rel: string }
|
|
39
40
|
| { ok: false; error: string };
|
|
40
41
|
|
|
41
|
-
function resolveScoped(
|
|
42
|
+
function resolveScoped(
|
|
43
|
+
roots: string[],
|
|
44
|
+
input: unknown,
|
|
45
|
+
allowedPaths?: string[],
|
|
46
|
+
): PathResult {
|
|
42
47
|
if (typeof input !== "string" || input.length === 0) {
|
|
43
48
|
return { ok: false, error: "missing path" };
|
|
44
49
|
}
|
|
@@ -54,11 +59,21 @@ function resolveScoped(roots: string[], input: unknown): PathResult {
|
|
|
54
59
|
if (input.startsWith("./") || input.includes("//") || input.endsWith("/.")) {
|
|
55
60
|
return { ok: false, error: "noncanonical path rejected" };
|
|
56
61
|
}
|
|
62
|
+
let insideRoot = false;
|
|
57
63
|
for (const root of roots) {
|
|
58
64
|
const abs = resolve(root, input);
|
|
59
65
|
const inside =
|
|
60
66
|
abs === root || abs.startsWith(root.endsWith("/") ? root : `${root}/`);
|
|
61
67
|
if (!inside) continue;
|
|
68
|
+
insideRoot = true;
|
|
69
|
+
if (
|
|
70
|
+
allowedPaths &&
|
|
71
|
+
!allowedPaths.some(
|
|
72
|
+
(allowed) => abs === allowed || abs.startsWith(`${allowed}/`),
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
62
77
|
// Hidden directories are rejected unless the root itself is the hidden scope.
|
|
63
78
|
const hiddenRoot =
|
|
64
79
|
basename(realpathSync(root)) === ".git" ||
|
|
@@ -89,7 +104,12 @@ function resolveScoped(roots: string[], input: unknown): PathResult {
|
|
|
89
104
|
}
|
|
90
105
|
return { ok: true, abs: abs, rel: relative(root, abs) || "." };
|
|
91
106
|
}
|
|
92
|
-
return {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: insideRoot
|
|
110
|
+
? "path is outside the declared read/write scope"
|
|
111
|
+
: "path is outside every approved root",
|
|
112
|
+
};
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
function existsSync(p: string): boolean {
|
|
@@ -113,6 +133,7 @@ function utf8Text(abs: string): string | null {
|
|
|
113
133
|
|
|
114
134
|
export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
115
135
|
const roots = opts.roots.map((r) => resolve(r));
|
|
136
|
+
const allowedPaths = opts.allowedPaths?.map((p) => resolve(p));
|
|
116
137
|
const observe = opts.observer ?? (() => {});
|
|
117
138
|
|
|
118
139
|
const readTool: ScopedToolDef = {
|
|
@@ -120,7 +141,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
120
141
|
description:
|
|
121
142
|
"Read a regular UTF-8 text file within the approved scope (max 2,000 lines / 50 KiB).",
|
|
122
143
|
async execute(params) {
|
|
123
|
-
const resolved = resolveScoped(roots, params.path);
|
|
144
|
+
const resolved = resolveScoped(roots, params.path, allowedPaths);
|
|
124
145
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
125
146
|
let st: Stats | undefined;
|
|
126
147
|
try {
|
|
@@ -167,7 +188,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
167
188
|
error: `pattern exceeds ${TOOL_LIMITS.maxGrepPattern} characters`,
|
|
168
189
|
};
|
|
169
190
|
}
|
|
170
|
-
const resolved = resolveScoped(roots, params.path ?? ".");
|
|
191
|
+
const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
|
|
171
192
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
172
193
|
let regex: RegExp;
|
|
173
194
|
try {
|
|
@@ -207,7 +228,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
207
228
|
description:
|
|
208
229
|
"List a directory within the approved scope (max 20,000 entries, stable code-unit order).",
|
|
209
230
|
async execute(params) {
|
|
210
|
-
const resolved = resolveScoped(roots, params.path ?? ".");
|
|
231
|
+
const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
|
|
211
232
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
212
233
|
let st: Stats | undefined;
|
|
213
234
|
try {
|
|
@@ -236,7 +257,7 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
236
257
|
description:
|
|
237
258
|
"Recursively list paths within the approved scope (max 20,000 entries, stable code-unit order).",
|
|
238
259
|
async execute(params) {
|
|
239
|
-
const resolved = resolveScoped(roots, params.path ?? ".");
|
|
260
|
+
const resolved = resolveScoped(roots, params.path ?? ".", allowedPaths);
|
|
240
261
|
if (!resolved.ok) return { ok: false, error: resolved.error };
|
|
241
262
|
let st: Stats | undefined;
|
|
242
263
|
try {
|
package/src/submit-tool.ts
CHANGED
|
@@ -63,6 +63,7 @@ export interface SubmitClassification {
|
|
|
63
63
|
|
|
64
64
|
export function createSubmitTool(input: {
|
|
65
65
|
requestId: string;
|
|
66
|
+
taskId?: string;
|
|
66
67
|
role: string;
|
|
67
68
|
phase: string;
|
|
68
69
|
output: "evidence" | "diff";
|
|
@@ -98,7 +99,8 @@ export function createSubmitTool(input: {
|
|
|
98
99
|
if (value.id !== input.requestId) identity.request = false;
|
|
99
100
|
if (value.role !== input.role) identity.role = false;
|
|
100
101
|
if (input.output === "diff") {
|
|
101
|
-
if (value.taskId !== input.requestId)
|
|
102
|
+
if (value.taskId !== (input.taskId ?? input.requestId))
|
|
103
|
+
identity.task = false;
|
|
102
104
|
if (value.phase !== input.phase) identity.phase = false;
|
|
103
105
|
}
|
|
104
106
|
const validation =
|
package/src/worker.ts
CHANGED
|
@@ -1,77 +1,149 @@
|
|
|
1
1
|
// Process-local logical Worker registry for private orchestration. A Worker
|
|
2
|
-
// is
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// phase may be mechanically redispatched exactly once when its canonical
|
|
7
|
-
// request is identical apart from refreshed snapshot hashes; a second failure
|
|
8
|
-
// blocks that branch. No credential, transcript, or retry ledger is retained.
|
|
2
|
+
// is keyed by stable task identity. Its immutable task contract is separate
|
|
3
|
+
// from the current phase request, snapshot, verification, and launch budget.
|
|
4
|
+
// No credential, transcript, provider diagnostic, or unbounded retry ledger
|
|
5
|
+
// is retained.
|
|
9
6
|
|
|
10
|
-
import type { RequestEnvelope } from "./contracts.ts";
|
|
7
|
+
import type { RequestEnvelope, VerificationContract } from "./contracts.ts";
|
|
11
8
|
|
|
12
9
|
export interface WorkerContract {
|
|
13
10
|
stage: string;
|
|
14
11
|
role: string;
|
|
15
|
-
|
|
12
|
+
taskId: string;
|
|
16
13
|
objective: string;
|
|
17
14
|
roots: string[];
|
|
18
15
|
agents: string;
|
|
19
16
|
contractText: string;
|
|
20
17
|
read: string[];
|
|
21
|
-
write: string[];
|
|
22
18
|
conflicts: string[];
|
|
23
19
|
resources: string[];
|
|
24
|
-
verificationLock?: string;
|
|
25
20
|
output: string;
|
|
21
|
+
currentPhase: WorkerPhaseContract;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorkerTaskContract {
|
|
25
|
+
stage: string;
|
|
26
|
+
role: string;
|
|
27
|
+
taskId: string;
|
|
28
|
+
objective: string;
|
|
29
|
+
roots: string[];
|
|
30
|
+
agents: string;
|
|
31
|
+
contractText: string;
|
|
32
|
+
read: string[];
|
|
33
|
+
conflicts: string[];
|
|
34
|
+
resources: string[];
|
|
35
|
+
output: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface WorkerPhaseContract {
|
|
39
|
+
requestId: string;
|
|
40
|
+
phase: string;
|
|
41
|
+
readSet: string[];
|
|
42
|
+
writeSet: string[];
|
|
43
|
+
conflicts: string[];
|
|
44
|
+
resources: string[];
|
|
45
|
+
verificationLock?: string;
|
|
46
|
+
snapshot?: unknown;
|
|
47
|
+
verification?: VerificationContract;
|
|
48
|
+
correctionIndex: 0 | 1;
|
|
26
49
|
}
|
|
27
50
|
|
|
28
51
|
export interface LogicalWorker {
|
|
29
52
|
identity: string;
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
taskContract: WorkerTaskContract;
|
|
54
|
+
currentPhase: WorkerPhaseContract;
|
|
55
|
+
state: WorkerState;
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
|
|
35
|
-
|
|
58
|
+
export type WorkerState =
|
|
59
|
+
| { kind: "ready" }
|
|
60
|
+
| { kind: "candidate-pending" }
|
|
61
|
+
| { kind: "phase-applied" }
|
|
62
|
+
| { kind: "artifact-correction-pending"; rejection: string }
|
|
63
|
+
| { kind: "stale-redispatch-pending" }
|
|
64
|
+
| { kind: "blocked"; reason: "artifact" | "mechanical" };
|
|
65
|
+
|
|
36
66
|
export function contractOf(envelope: RequestEnvelope): WorkerContract {
|
|
37
67
|
return {
|
|
38
68
|
stage: envelope.stage,
|
|
39
69
|
role: envelope.role,
|
|
40
|
-
|
|
70
|
+
taskId: envelope.taskId ?? envelope.id,
|
|
41
71
|
objective: envelope.objective,
|
|
42
72
|
roots: [...envelope.roots],
|
|
43
73
|
agents: envelope.context.agents,
|
|
44
74
|
contractText: envelope.context.contract,
|
|
45
75
|
read: [...envelope.declared.read],
|
|
46
|
-
write: [...envelope.declared.write],
|
|
47
76
|
conflicts: [...envelope.declared.conflicts],
|
|
48
77
|
resources: [...envelope.declared.resources],
|
|
49
|
-
verificationLock: envelope.declared.verificationLock,
|
|
50
78
|
output: envelope.output,
|
|
79
|
+
currentPhase: phaseOf(envelope, 0),
|
|
51
80
|
};
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
export function sameContract(
|
|
83
|
+
export function sameContract(
|
|
84
|
+
a: WorkerTaskContract,
|
|
85
|
+
b: WorkerTaskContract,
|
|
86
|
+
): boolean {
|
|
55
87
|
if (
|
|
56
88
|
a.stage !== b.stage ||
|
|
57
89
|
a.role !== b.role ||
|
|
58
|
-
a.
|
|
90
|
+
a.taskId !== b.taskId ||
|
|
59
91
|
a.objective !== b.objective ||
|
|
60
92
|
a.agents !== b.agents ||
|
|
61
93
|
a.contractText !== b.contractText ||
|
|
62
|
-
a.output !== b.output
|
|
63
|
-
a.verificationLock !== b.verificationLock
|
|
94
|
+
a.output !== b.output
|
|
64
95
|
)
|
|
65
96
|
return false;
|
|
66
97
|
return (
|
|
67
98
|
arrayEqual(a.roots, b.roots) &&
|
|
68
99
|
arrayEqual(a.read, b.read) &&
|
|
69
|
-
arrayEqual(a.write, b.write) &&
|
|
70
100
|
arrayEqual(a.conflicts, b.conflicts) &&
|
|
71
101
|
arrayEqual(a.resources, b.resources)
|
|
72
102
|
);
|
|
73
103
|
}
|
|
74
104
|
|
|
105
|
+
export function samePhaseContract(
|
|
106
|
+
a: WorkerPhaseContract,
|
|
107
|
+
b: WorkerPhaseContract,
|
|
108
|
+
): boolean {
|
|
109
|
+
return (
|
|
110
|
+
a.phase === b.phase &&
|
|
111
|
+
arrayEqual(a.readSet, b.readSet) &&
|
|
112
|
+
arrayEqual(a.writeSet, b.writeSet) &&
|
|
113
|
+
arrayEqual(a.conflicts, b.conflicts) &&
|
|
114
|
+
arrayEqual(a.resources, b.resources) &&
|
|
115
|
+
a.verificationLock === b.verificationLock &&
|
|
116
|
+
JSON.stringify(a.verification) === JSON.stringify(b.verification)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function phaseOf(
|
|
121
|
+
envelope: RequestEnvelope,
|
|
122
|
+
correctionIndex: 0 | 1,
|
|
123
|
+
): WorkerPhaseContract {
|
|
124
|
+
return {
|
|
125
|
+
requestId: envelope.id,
|
|
126
|
+
phase: envelope.phase,
|
|
127
|
+
readSet: [...envelope.declared.read],
|
|
128
|
+
writeSet: [...envelope.declared.write],
|
|
129
|
+
conflicts: [...envelope.declared.conflicts],
|
|
130
|
+
resources: [...envelope.declared.resources],
|
|
131
|
+
verificationLock: envelope.declared.verificationLock,
|
|
132
|
+
snapshot:
|
|
133
|
+
envelope.snapshot === undefined
|
|
134
|
+
? undefined
|
|
135
|
+
: structuredClone(envelope.snapshot),
|
|
136
|
+
verification:
|
|
137
|
+
envelope.verification === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: {
|
|
140
|
+
...envelope.verification,
|
|
141
|
+
argv: [...envelope.verification.argv],
|
|
142
|
+
},
|
|
143
|
+
correctionIndex,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
75
147
|
function arrayEqual(a: string[], b: string[]): boolean {
|
|
76
148
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
77
149
|
}
|
|
@@ -104,12 +176,30 @@ export class WorkerRegistry {
|
|
|
104
176
|
}
|
|
105
177
|
|
|
106
178
|
pin(contract: WorkerContract, identity: string): LogicalWorker {
|
|
179
|
+
const currentPhase = clonePhase(contract.currentPhase);
|
|
107
180
|
const worker: LogicalWorker = {
|
|
108
181
|
identity,
|
|
109
|
-
contract,
|
|
110
|
-
|
|
182
|
+
taskContract: cloneTask(contract),
|
|
183
|
+
currentPhase,
|
|
184
|
+
state: { kind: "ready" },
|
|
111
185
|
};
|
|
112
|
-
this.workers.set(contract.
|
|
186
|
+
this.workers.set(contract.taskId, worker);
|
|
187
|
+
return worker;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setCurrentPhase(
|
|
191
|
+
taskId: string,
|
|
192
|
+
contract: WorkerContract,
|
|
193
|
+
correctionIndex: 0 | 1,
|
|
194
|
+
): LogicalWorker | undefined {
|
|
195
|
+
const worker = this.workers.get(taskId);
|
|
196
|
+
if (!worker) return undefined;
|
|
197
|
+
const currentPhase = {
|
|
198
|
+
...clonePhase(contract.currentPhase),
|
|
199
|
+
correctionIndex,
|
|
200
|
+
};
|
|
201
|
+
worker.currentPhase = currentPhase;
|
|
202
|
+
worker.state = { kind: "ready" };
|
|
113
203
|
return worker;
|
|
114
204
|
}
|
|
115
205
|
|
|
@@ -117,3 +207,23 @@ export class WorkerRegistry {
|
|
|
117
207
|
this.workers.clear();
|
|
118
208
|
}
|
|
119
209
|
}
|
|
210
|
+
|
|
211
|
+
function cloneTask(contract: WorkerContract): WorkerTaskContract {
|
|
212
|
+
return {
|
|
213
|
+
stage: contract.stage,
|
|
214
|
+
role: contract.role,
|
|
215
|
+
taskId: contract.taskId,
|
|
216
|
+
objective: contract.objective,
|
|
217
|
+
roots: [...contract.roots],
|
|
218
|
+
agents: contract.agents,
|
|
219
|
+
contractText: contract.contractText,
|
|
220
|
+
read: [...contract.read],
|
|
221
|
+
conflicts: [...contract.conflicts],
|
|
222
|
+
resources: [...contract.resources],
|
|
223
|
+
output: contract.output,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function clonePhase(phase: WorkerPhaseContract): WorkerPhaseContract {
|
|
228
|
+
return structuredClone(phase);
|
|
229
|
+
}
|