@abelxiaoxing/cadence 1.0.0 → 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/README.md +50 -22
- package/package.json +3 -1
- package/prompts/abel-design.md +1 -0
- package/prompts/abel-diagnose.md +1 -0
- package/prompts/abel-implement.md +14 -2
- package/skills/abel-workflow/SKILL.md +7 -1
- package/src/candidate-preflight.ts +1163 -0
- package/src/child-session.ts +98 -10
- package/src/contracts.ts +379 -48
- package/src/file-snapshot.ts +62 -8
- package/src/index.ts +191 -14
- package/src/parent-payload-bridge.ts +442 -0
- package/src/parent-provider.ts +124 -16
- package/src/patch.ts +73 -3
- package/src/result-store.ts +40 -3
- package/src/runtime.ts +1013 -60
- package/src/scheduler.ts +47 -10
- package/src/scoped-tools.ts +27 -6
- package/src/subagent-activity.ts +747 -0
- package/src/submit-tool.ts +3 -1
- package/src/worker.ts +136 -26
package/src/scheduler.ts
CHANGED
|
@@ -43,6 +43,7 @@ interface Entry<T> {
|
|
|
43
43
|
state: EntryState;
|
|
44
44
|
attempts: number;
|
|
45
45
|
cancelRequested: boolean;
|
|
46
|
+
cancelReason: unknown;
|
|
46
47
|
controller: AbortController | null;
|
|
47
48
|
resolve(outcome: ScheduledOutcome<T>): void;
|
|
48
49
|
promise: Promise<ScheduledOutcome<T>>;
|
|
@@ -58,6 +59,7 @@ function conflictEdgeMatches(
|
|
|
58
59
|
): boolean {
|
|
59
60
|
return conflicts.some(
|
|
60
61
|
(edge) =>
|
|
62
|
+
edge === other.taskId ||
|
|
61
63
|
edge === other.id ||
|
|
62
64
|
other.declared.read.includes(edge) ||
|
|
63
65
|
other.declared.write.includes(edge),
|
|
@@ -106,6 +108,7 @@ class Batch<T> {
|
|
|
106
108
|
state: "queued",
|
|
107
109
|
attempts: 0,
|
|
108
110
|
cancelRequested: false,
|
|
111
|
+
cancelReason: undefined,
|
|
109
112
|
controller: null,
|
|
110
113
|
resolve,
|
|
111
114
|
promise,
|
|
@@ -150,6 +153,8 @@ export class Scheduler<T> {
|
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
this.pump();
|
|
156
|
+
const done = Promise.all(batch.entries.map((entry) => entry.promise));
|
|
157
|
+
void done.then(() => this.removeBatch(batch));
|
|
153
158
|
return {
|
|
154
159
|
batchId,
|
|
155
160
|
result: (id: string) => {
|
|
@@ -157,22 +162,54 @@ export class Scheduler<T> {
|
|
|
157
162
|
if (!entry) throw new Error(`unknown scheduled request: ${id}`);
|
|
158
163
|
return entry.promise;
|
|
159
164
|
},
|
|
160
|
-
done
|
|
165
|
+
done,
|
|
161
166
|
};
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
|
|
169
|
+
private removeBatch(batch: Batch<T>): void {
|
|
170
|
+
const index = this.batches.indexOf(batch);
|
|
171
|
+
if (index >= 0) this.batches.splice(index, 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
cancel(
|
|
175
|
+
batchId: string,
|
|
176
|
+
reason: unknown = new Error("batch cancelled"),
|
|
177
|
+
): void {
|
|
165
178
|
const batch = this.batches.find(
|
|
166
179
|
(candidate) => candidate.batchId === batchId,
|
|
167
180
|
);
|
|
168
|
-
if (
|
|
181
|
+
if (batch) this.cancelBatch(batch, reason);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async cancelAll(
|
|
185
|
+
reason: unknown = new Error("batch cancelled"),
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const batches = [...this.batches];
|
|
188
|
+
for (const batch of batches) this.requestCancellation(batch, reason);
|
|
189
|
+
for (const batch of batches) this.settleQueuedCancellation(batch);
|
|
190
|
+
await Promise.all(
|
|
191
|
+
batches.flatMap((batch) => batch.entries.map((entry) => entry.promise)),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private cancelBatch(batch: Batch<T>, reason: unknown): void {
|
|
196
|
+
this.requestCancellation(batch, reason);
|
|
197
|
+
this.settleQueuedCancellation(batch);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private requestCancellation(batch: Batch<T>, reason: unknown): void {
|
|
201
|
+
for (const entry of batch.entries) {
|
|
202
|
+
if (entry.state !== "queued" && entry.state !== "running") continue;
|
|
203
|
+
if (!entry.cancelRequested) entry.cancelReason = reason;
|
|
204
|
+
entry.cancelRequested = true;
|
|
205
|
+
if (entry.state === "running") entry.controller?.abort(reason);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private settleQueuedCancellation(batch: Batch<T>): void {
|
|
169
210
|
for (const entry of batch.entries) {
|
|
170
211
|
if (entry.state === "queued") {
|
|
171
|
-
entry.cancelRequested = true;
|
|
172
212
|
this.settle(batch, entry, this.cancelOutcome(entry));
|
|
173
|
-
} else if (entry.state === "running") {
|
|
174
|
-
entry.cancelRequested = true;
|
|
175
|
-
entry.controller?.abort();
|
|
176
213
|
}
|
|
177
214
|
}
|
|
178
215
|
this.pump();
|
|
@@ -182,7 +219,7 @@ export class Scheduler<T> {
|
|
|
182
219
|
for (const batch of this.batches) {
|
|
183
220
|
for (const entry of batch.entries) {
|
|
184
221
|
if (this.active.size >= this.limit) return;
|
|
185
|
-
if (entry.state !== "queued") continue;
|
|
222
|
+
if (entry.state !== "queued" || entry.cancelRequested) continue;
|
|
186
223
|
if (!this.prerequisitesSucceeded(batch, entry)) continue;
|
|
187
224
|
if (this.hasActiveConflict(entry)) continue;
|
|
188
225
|
this.start(batch, entry);
|
|
@@ -207,7 +244,7 @@ export class Scheduler<T> {
|
|
|
207
244
|
|
|
208
245
|
private start(batch: Batch<T>, entry: Entry<T>): void {
|
|
209
246
|
// Guarded start: a cancelled or settled queued closure never runs later.
|
|
210
|
-
if (entry.state !== "queued") return;
|
|
247
|
+
if (entry.state !== "queued" || entry.cancelRequested) return;
|
|
211
248
|
entry.state = "running";
|
|
212
249
|
entry.attempts += 1;
|
|
213
250
|
entry.controller = new AbortController();
|
|
@@ -262,7 +299,7 @@ export class Scheduler<T> {
|
|
|
262
299
|
id: entry.request.id,
|
|
263
300
|
status: "cancelled",
|
|
264
301
|
attempts: entry.attempts,
|
|
265
|
-
error: "batch cancelled",
|
|
302
|
+
error: errorMessage(entry.cancelReason ?? new Error("batch cancelled")),
|
|
266
303
|
};
|
|
267
304
|
}
|
|
268
305
|
|
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 {
|