@abelxiaoxing/cadence 1.0.11 → 1.0.12
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/src/child-session.ts +25 -3
- package/src/index.ts +14 -4
- package/src/parent-payload-bridge.ts +31 -7
- package/src/scoped-tools.ts +278 -38
- package/src/subagent-activity.ts +80 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Prompts, skills, and a private orchestration extension exposing the Abel four-stage workflow (/abel-init, /abel-design, /abel-implement, /abel-diagnose) with package-owned professional Agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/child-session.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { Type } from "typebox";
|
|
11
11
|
import type { ChildFailure, DiffResult, EvidenceResult } from "./contracts.ts";
|
|
12
12
|
import { EmptyResourceLoader } from "./empty-resource-loader.ts";
|
|
13
|
-
import { createScopedTools } from "./scoped-tools.ts";
|
|
13
|
+
import { createScopedTools, TOOL_LIMITS } from "./scoped-tools.ts";
|
|
14
14
|
import {
|
|
15
15
|
createSubmitTool,
|
|
16
16
|
type FinalCategory,
|
|
@@ -120,8 +120,30 @@ function wrapScopedTools(
|
|
|
120
120
|
description: scoped.description,
|
|
121
121
|
parameters:
|
|
122
122
|
scoped.name === "grep"
|
|
123
|
-
? Type.Object({
|
|
124
|
-
|
|
123
|
+
? Type.Object({
|
|
124
|
+
path: Type.Optional(Type.String()),
|
|
125
|
+
pattern: Type.String(),
|
|
126
|
+
})
|
|
127
|
+
: scoped.name === "find"
|
|
128
|
+
? Type.Object({
|
|
129
|
+
path: Type.Optional(Type.String()),
|
|
130
|
+
pattern: Type.String(),
|
|
131
|
+
limit: Type.Optional(
|
|
132
|
+
Type.Integer({
|
|
133
|
+
minimum: 1,
|
|
134
|
+
maximum: TOOL_LIMITS.maxEntries,
|
|
135
|
+
}),
|
|
136
|
+
),
|
|
137
|
+
})
|
|
138
|
+
: scoped.name === "read"
|
|
139
|
+
? Type.Object({
|
|
140
|
+
path: Type.String(),
|
|
141
|
+
offset: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
142
|
+
limit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
143
|
+
})
|
|
144
|
+
: Type.Object({
|
|
145
|
+
path: Type.Optional(Type.String()),
|
|
146
|
+
}),
|
|
125
147
|
async execute(_id, params) {
|
|
126
148
|
const result = await scoped.execute(
|
|
127
149
|
params as Record<string, unknown>,
|
package/src/index.ts
CHANGED
|
@@ -232,6 +232,7 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
232
232
|
? "Run exactly one bounded read-only design-explorer packet. When parallel packets are required, emit every sibling abel_dispatch call together in one assistant response before waiting for any result."
|
|
233
233
|
: "Private Abel workflow delegation: run bounded read-only evidence or Worker phase requests, apply or discard retained results, apply a parent-only stable AGENTS checkpoint, cancel work, or finish the stage. Inactive unless an eligible Abel stage verified its invocation.",
|
|
234
234
|
executionMode: "parallel",
|
|
235
|
+
renderShell: "self",
|
|
235
236
|
parameters: {
|
|
236
237
|
type: "object",
|
|
237
238
|
properties: {
|
|
@@ -304,16 +305,25 @@ export default function register(pi: ExtensionAPI): void {
|
|
|
304
305
|
...(usage === undefined ? {} : { usage }),
|
|
305
306
|
};
|
|
306
307
|
},
|
|
307
|
-
renderCall(args: unknown, theme: Theme,
|
|
308
|
-
return renderActivityCall(
|
|
308
|
+
renderCall(args: unknown, theme: Theme, context: unknown) {
|
|
309
|
+
return renderActivityCall(
|
|
310
|
+
args,
|
|
311
|
+
theme,
|
|
312
|
+
context as Parameters<typeof renderActivityCall>[2],
|
|
313
|
+
);
|
|
309
314
|
},
|
|
310
315
|
renderResult(
|
|
311
316
|
result: AgentToolResult<unknown>,
|
|
312
317
|
options: ToolRenderResultOptions,
|
|
313
318
|
theme: Theme,
|
|
314
|
-
|
|
319
|
+
context: unknown,
|
|
315
320
|
) {
|
|
316
|
-
return renderActivityResult(
|
|
321
|
+
return renderActivityResult(
|
|
322
|
+
result,
|
|
323
|
+
options,
|
|
324
|
+
theme,
|
|
325
|
+
context as Parameters<typeof renderActivityResult>[3],
|
|
326
|
+
);
|
|
317
327
|
},
|
|
318
328
|
} as never);
|
|
319
329
|
};
|
|
@@ -46,6 +46,7 @@ export class ParentPayloadBridgeError extends Error {
|
|
|
46
46
|
|
|
47
47
|
export interface ParentProviderRegistry {
|
|
48
48
|
getProvider(provider: string): Provider | undefined;
|
|
49
|
+
getRegisteredNativeProvider?(provider: string): Provider | undefined;
|
|
49
50
|
registerProvider(provider: Provider): void;
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -198,9 +199,12 @@ export class ParentPayloadBridge {
|
|
|
198
199
|
model: Model<string>,
|
|
199
200
|
registry: ParentProviderRegistry,
|
|
200
201
|
): Provider | undefined {
|
|
201
|
-
const current =
|
|
202
|
+
const current = this.nativeCarrier(model.provider, registry);
|
|
202
203
|
if (!current) return undefined;
|
|
203
204
|
const delegate = this.unwrapProvider(current);
|
|
205
|
+
const wrapped = this.isWrappedProvider(current)
|
|
206
|
+
? current
|
|
207
|
+
: this.wrapProvider(delegate);
|
|
204
208
|
const state = this.state;
|
|
205
209
|
if (state?.active) {
|
|
206
210
|
const key = modelKeyId(modelKeyFor(model));
|
|
@@ -210,14 +214,18 @@ export class ParentPayloadBridge {
|
|
|
210
214
|
state.ready.delete(key);
|
|
211
215
|
}
|
|
212
216
|
}
|
|
213
|
-
|
|
214
|
-
|
|
217
|
+
if (wrapped !== current) {
|
|
218
|
+
registry.registerProvider(wrapped);
|
|
219
|
+
}
|
|
215
220
|
return wrapped;
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
capture(
|
|
219
224
|
modelKey: ParentModelKey,
|
|
220
|
-
registry?: Pick<
|
|
225
|
+
registry?: Pick<
|
|
226
|
+
ParentProviderRegistry,
|
|
227
|
+
"getProvider" | "getRegisteredNativeProvider"
|
|
228
|
+
>,
|
|
221
229
|
): ParentPayloadCapture | undefined {
|
|
222
230
|
const state = this.state;
|
|
223
231
|
if (!state?.active) return undefined;
|
|
@@ -226,7 +234,7 @@ export class ParentPayloadBridge {
|
|
|
226
234
|
return undefined;
|
|
227
235
|
}
|
|
228
236
|
if (registry) {
|
|
229
|
-
const current =
|
|
237
|
+
const current = this.nativeCarrier(modelKey.provider, registry);
|
|
230
238
|
if (!current || this.unwrapProvider(current) !== capture.delegate) {
|
|
231
239
|
return undefined;
|
|
232
240
|
}
|
|
@@ -236,11 +244,14 @@ export class ParentPayloadBridge {
|
|
|
236
244
|
|
|
237
245
|
diagnoseCapture(
|
|
238
246
|
modelKey: ParentModelKey,
|
|
239
|
-
registry: Pick<
|
|
247
|
+
registry: Pick<
|
|
248
|
+
ParentProviderRegistry,
|
|
249
|
+
"getProvider" | "getRegisteredNativeProvider"
|
|
250
|
+
>,
|
|
240
251
|
): ParentBridgeFailureCode | undefined {
|
|
241
252
|
const state = this.state;
|
|
242
253
|
if (!state?.active) return "parent-bridge-session-unavailable";
|
|
243
|
-
const current =
|
|
254
|
+
const current = this.nativeCarrier(modelKey.provider, registry);
|
|
244
255
|
if (!current || !this.isWrappedProvider(current)) {
|
|
245
256
|
return "parent-bridge-provider-not-installed";
|
|
246
257
|
}
|
|
@@ -468,6 +479,19 @@ export class ParentPayloadBridge {
|
|
|
468
479
|
return state;
|
|
469
480
|
}
|
|
470
481
|
|
|
482
|
+
private nativeCarrier(
|
|
483
|
+
providerId: string,
|
|
484
|
+
registry: Pick<
|
|
485
|
+
ParentProviderRegistry,
|
|
486
|
+
"getProvider" | "getRegisteredNativeProvider"
|
|
487
|
+
>,
|
|
488
|
+
): Provider | undefined {
|
|
489
|
+
const registered = registry.getRegisteredNativeProvider?.(providerId);
|
|
490
|
+
return registered && this.isWrappedProvider(registered)
|
|
491
|
+
? registered
|
|
492
|
+
: registry.getProvider(providerId);
|
|
493
|
+
}
|
|
494
|
+
|
|
471
495
|
private isWrappedProvider(provider: Provider): boolean {
|
|
472
496
|
return Reflect.get(provider, PROVIDER_WRAPPER) !== undefined;
|
|
473
497
|
}
|
package/src/scoped-tools.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type Stats,
|
|
9
9
|
statSync,
|
|
10
10
|
} from "node:fs";
|
|
11
|
-
import { basename, join, relative, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
12
12
|
|
|
13
13
|
export const TOOL_LIMITS = {
|
|
14
14
|
maxReadBytes: 50 * 1024,
|
|
@@ -19,6 +19,115 @@ export const TOOL_LIMITS = {
|
|
|
19
19
|
maxEntries: 20000,
|
|
20
20
|
} as const;
|
|
21
21
|
|
|
22
|
+
function globToRegExp(pattern: string): RegExp | { error: string } {
|
|
23
|
+
if (pattern.length === 0) return { error: "missing pattern" };
|
|
24
|
+
if (pattern.length > TOOL_LIMITS.maxGrepPattern) {
|
|
25
|
+
return {
|
|
26
|
+
error: `pattern exceeds ${TOOL_LIMITS.maxGrepPattern} characters`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let source = "^";
|
|
30
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
31
|
+
const ch = pattern[i];
|
|
32
|
+
if (ch === "*" && pattern[i + 1] === "*") {
|
|
33
|
+
const next = pattern[i + 2];
|
|
34
|
+
if (next === "/") {
|
|
35
|
+
source += "(?:.*/)?";
|
|
36
|
+
i += 2;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
source += ".*";
|
|
40
|
+
i += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === "*") {
|
|
44
|
+
source += "[^/]*";
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === "?") {
|
|
48
|
+
source += "[^/]";
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === "[") {
|
|
52
|
+
let end = i + 1;
|
|
53
|
+
if (pattern[end] === "!" || pattern[end] === "^") end++;
|
|
54
|
+
while (end < pattern.length && pattern[end] !== "]") end++;
|
|
55
|
+
if (end === pattern.length) {
|
|
56
|
+
source += "\\[";
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const raw = pattern.slice(i + 1, end);
|
|
60
|
+
const negated = raw.startsWith("!") || raw.startsWith("^");
|
|
61
|
+
const body = raw.slice(negated ? 1 : 0);
|
|
62
|
+
if (body.length === 0 || body.includes("/")) {
|
|
63
|
+
return { error: "invalid glob character class" };
|
|
64
|
+
}
|
|
65
|
+
const escaped = [...body]
|
|
66
|
+
.map((value, index) => {
|
|
67
|
+
if (value === "\\" || value === "[" || value === "]") {
|
|
68
|
+
return `\\${value}`;
|
|
69
|
+
}
|
|
70
|
+
return value === "^" && index === 0 ? "\\^" : value;
|
|
71
|
+
})
|
|
72
|
+
.join("");
|
|
73
|
+
source += `[${negated ? "^" : ""}${escaped}]`;
|
|
74
|
+
i = end;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if ("\\^$+()[]{}|.".includes(ch)) source += `\\${ch}`;
|
|
78
|
+
else source += ch;
|
|
79
|
+
}
|
|
80
|
+
source += "$";
|
|
81
|
+
try {
|
|
82
|
+
return new RegExp(source, "u");
|
|
83
|
+
} catch {
|
|
84
|
+
return { error: "invalid glob pattern" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function boundedInteger(value: unknown): number | undefined {
|
|
89
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
90
|
+
? value
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isWithinPath(path: string, parent: string): boolean {
|
|
95
|
+
return path === parent || path.startsWith(`${parent}${sep}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isAllowedContent(path: string, allowedPaths?: string[]): boolean {
|
|
99
|
+
return (
|
|
100
|
+
allowedPaths === undefined ||
|
|
101
|
+
allowedPaths.some((allowed) => isWithinPath(path, allowed))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRelatedToAllowedPath(
|
|
106
|
+
path: string,
|
|
107
|
+
allowedPaths?: string[],
|
|
108
|
+
): boolean {
|
|
109
|
+
return (
|
|
110
|
+
allowedPaths === undefined ||
|
|
111
|
+
allowedPaths.some(
|
|
112
|
+
(allowed) => isWithinPath(path, allowed) || isWithinPath(allowed, path),
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function commonAncestor(paths: string[]): string {
|
|
118
|
+
let candidate = paths[0];
|
|
119
|
+
while (!paths.every((path) => isWithinPath(path, candidate))) {
|
|
120
|
+
const parent = dirname(candidate);
|
|
121
|
+
if (parent === candidate) return candidate;
|
|
122
|
+
candidate = parent;
|
|
123
|
+
}
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function relativePath(base: string, path: string): string {
|
|
128
|
+
return (relative(base, path) || ".").split(sep).join("/");
|
|
129
|
+
}
|
|
130
|
+
|
|
22
131
|
export type Observation =
|
|
23
132
|
| { kind: "file"; path: string }
|
|
24
133
|
| { kind: "dir"; path: string };
|
|
@@ -38,11 +147,13 @@ interface Options {
|
|
|
38
147
|
type PathResult =
|
|
39
148
|
| { ok: true; abs: string; rel: string }
|
|
40
149
|
| { ok: false; error: string };
|
|
150
|
+
type ResolvedPath = Extract<PathResult, { ok: true }>;
|
|
41
151
|
|
|
42
152
|
function resolveScoped(
|
|
43
153
|
roots: string[],
|
|
44
154
|
input: unknown,
|
|
45
155
|
allowedPaths?: string[],
|
|
156
|
+
allowScopedAncestor = false,
|
|
46
157
|
): PathResult {
|
|
47
158
|
if (typeof input !== "string" || input.length === 0) {
|
|
48
159
|
return { ok: false, error: "missing path" };
|
|
@@ -69,7 +180,9 @@ function resolveScoped(
|
|
|
69
180
|
if (
|
|
70
181
|
allowedPaths &&
|
|
71
182
|
!allowedPaths.some(
|
|
72
|
-
(allowed) =>
|
|
183
|
+
(allowed) =>
|
|
184
|
+
isWithinPath(abs, allowed) ||
|
|
185
|
+
(allowScopedAncestor && isWithinPath(allowed, abs)),
|
|
73
186
|
)
|
|
74
187
|
) {
|
|
75
188
|
continue;
|
|
@@ -135,6 +248,37 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
135
248
|
const roots = opts.roots.map((r) => resolve(r));
|
|
136
249
|
const allowedPaths = opts.allowedPaths?.map((p) => resolve(p));
|
|
137
250
|
const observe = opts.observer ?? (() => {});
|
|
251
|
+
const virtualRoot = commonAncestor(roots);
|
|
252
|
+
|
|
253
|
+
function resolveScanTargets(
|
|
254
|
+
input: unknown,
|
|
255
|
+
):
|
|
256
|
+
| { ok: true; targets: ResolvedPath[]; virtual: boolean }
|
|
257
|
+
| { ok: false; error: string } {
|
|
258
|
+
if (input !== undefined) {
|
|
259
|
+
const resolved = resolveScoped(roots, input, allowedPaths, true);
|
|
260
|
+
return resolved.ok
|
|
261
|
+
? { ok: true, targets: [resolved], virtual: false }
|
|
262
|
+
: resolved;
|
|
263
|
+
}
|
|
264
|
+
const targets: ResolvedPath[] = [];
|
|
265
|
+
for (const root of roots) {
|
|
266
|
+
const resolved = resolveScoped([root], ".", allowedPaths, true);
|
|
267
|
+
if (resolved.ok) targets.push(resolved);
|
|
268
|
+
}
|
|
269
|
+
return targets.length > 0
|
|
270
|
+
? { ok: true, targets, virtual: roots.length > 1 }
|
|
271
|
+
: {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: "path is outside the declared read/write scope",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function rootRelativePath(path: string, virtual: boolean): string {
|
|
278
|
+
if (virtual) return relativePath(virtualRoot, path);
|
|
279
|
+
const root = roots.find((candidate) => isWithinPath(path, candidate));
|
|
280
|
+
return relativePath(root ?? roots[0], path);
|
|
281
|
+
}
|
|
138
282
|
|
|
139
283
|
const readTool: ScopedToolDef = {
|
|
140
284
|
name: "read",
|
|
@@ -153,21 +297,39 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
153
297
|
const text = utf8Text(resolved.abs);
|
|
154
298
|
if (text === null) return { ok: false, error: "not valid UTF-8 text" };
|
|
155
299
|
observe({ kind: "file", path: resolved.rel });
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
300
|
+
const allLines = text.split("\n");
|
|
301
|
+
const offset = boundedInteger(params.offset);
|
|
302
|
+
const limit = boundedInteger(params.limit);
|
|
303
|
+
if (offset !== undefined && offset > allLines.length) {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
error: `offset ${offset} exceeds file length (${allLines.length} lines)`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const start = offset === undefined ? 0 : Math.max(0, offset - 1);
|
|
310
|
+
const windowed =
|
|
311
|
+
offset === undefined && limit === undefined
|
|
312
|
+
? allLines
|
|
313
|
+
: allLines.slice(
|
|
314
|
+
start,
|
|
315
|
+
limit === undefined ? undefined : start + limit,
|
|
316
|
+
);
|
|
317
|
+
let content = windowed.join("\n");
|
|
318
|
+
let truncated =
|
|
319
|
+
Buffer.byteLength(content, "utf8") > TOOL_LIMITS.maxReadBytes ||
|
|
320
|
+
windowed.length > TOOL_LIMITS.maxReadLines;
|
|
321
|
+
if (Buffer.byteLength(content, "utf8") > TOOL_LIMITS.maxReadBytes) {
|
|
322
|
+
content = Buffer.from(content, "utf8")
|
|
163
323
|
.subarray(0, TOOL_LIMITS.maxReadBytes)
|
|
164
324
|
.toString("utf8");
|
|
325
|
+
truncated = true;
|
|
165
326
|
}
|
|
166
327
|
if (content.split("\n").length > TOOL_LIMITS.maxReadLines) {
|
|
167
328
|
content = content
|
|
168
329
|
.split("\n")
|
|
169
330
|
.slice(0, TOOL_LIMITS.maxReadLines)
|
|
170
331
|
.join("\n");
|
|
332
|
+
truncated = true;
|
|
171
333
|
}
|
|
172
334
|
return { ok: true, content, truncated };
|
|
173
335
|
},
|
|
@@ -188,8 +350,8 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
188
350
|
error: `pattern exceeds ${TOOL_LIMITS.maxGrepPattern} characters`,
|
|
189
351
|
};
|
|
190
352
|
}
|
|
191
|
-
const
|
|
192
|
-
if (!
|
|
353
|
+
const scan = resolveScanTargets(params.path);
|
|
354
|
+
if (!scan.ok) return { ok: false, error: scan.error };
|
|
193
355
|
let regex: RegExp;
|
|
194
356
|
try {
|
|
195
357
|
regex = new RegExp(pattern, "u");
|
|
@@ -199,20 +361,29 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
199
361
|
const start = performance.now();
|
|
200
362
|
const matches: { path: string; line: number; text: string }[] = [];
|
|
201
363
|
let scanned = 0;
|
|
202
|
-
const files =
|
|
364
|
+
const files: string[] = [];
|
|
365
|
+
for (const target of scan.targets) {
|
|
366
|
+
collectFiles(target.abs, files, TOOL_LIMITS.maxEntries, allowedPaths);
|
|
367
|
+
}
|
|
368
|
+
files.sort((left, right) => {
|
|
369
|
+
const leftPath = rootRelativePath(left, scan.virtual);
|
|
370
|
+
const rightPath = rootRelativePath(right, scan.virtual);
|
|
371
|
+
return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
|
|
372
|
+
});
|
|
203
373
|
for (const file of files) {
|
|
204
374
|
if (scanned >= TOOL_LIMITS.maxGrepFiles) break;
|
|
205
375
|
if (performance.now() - start > TOOL_LIMITS.maxGrepMillis) break;
|
|
206
376
|
scanned++;
|
|
207
377
|
const text = utf8Text(file);
|
|
208
378
|
if (text === null) continue;
|
|
209
|
-
|
|
379
|
+
const path = rootRelativePath(file, scan.virtual);
|
|
380
|
+
observe({ kind: "file", path });
|
|
210
381
|
const lines = text.split("\n");
|
|
211
382
|
for (let i = 0; i < lines.length; i++) {
|
|
212
383
|
regex.lastIndex = 0;
|
|
213
384
|
if (regex.test(lines[i])) {
|
|
214
385
|
matches.push({
|
|
215
|
-
path
|
|
386
|
+
path,
|
|
216
387
|
line: i + 1,
|
|
217
388
|
text: lines[i].slice(0, 500),
|
|
218
389
|
});
|
|
@@ -228,18 +399,41 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
228
399
|
description:
|
|
229
400
|
"List a directory within the approved scope (max 20,000 entries, stable code-unit order).",
|
|
230
401
|
async execute(params) {
|
|
231
|
-
const
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
402
|
+
const scan = resolveScanTargets(params.path);
|
|
403
|
+
if (!scan.ok) return { ok: false, error: scan.error };
|
|
404
|
+
for (const target of scan.targets) {
|
|
405
|
+
let st: Stats | undefined;
|
|
406
|
+
try {
|
|
407
|
+
st = statSync(target.abs);
|
|
408
|
+
} catch {
|
|
409
|
+
return { ok: false, error: "directory does not exist" };
|
|
410
|
+
}
|
|
411
|
+
if (!st.isDirectory()) return { ok: false, error: "not a directory" };
|
|
412
|
+
observe({
|
|
413
|
+
kind: "dir",
|
|
414
|
+
path: rootRelativePath(target.abs, scan.virtual),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (scan.virtual) {
|
|
418
|
+
const names = new Set(
|
|
419
|
+
scan.targets.map(
|
|
420
|
+
(target) => relativePath(virtualRoot, target.abs).split("/")[0],
|
|
421
|
+
),
|
|
422
|
+
);
|
|
423
|
+
return {
|
|
424
|
+
ok: true,
|
|
425
|
+
entries: [...names]
|
|
426
|
+
.sort()
|
|
427
|
+
.slice(0, TOOL_LIMITS.maxEntries)
|
|
428
|
+
.map((name) => ({ name, type: "dir" })),
|
|
429
|
+
};
|
|
238
430
|
}
|
|
239
|
-
|
|
240
|
-
observe({ kind: "dir", path: resolved.rel });
|
|
431
|
+
const resolved = scan.targets[0];
|
|
241
432
|
const entries = readdirSync(resolved.abs, { withFileTypes: true })
|
|
242
433
|
.filter((e) => !isHidden(e.name, roots))
|
|
434
|
+
.filter((e) =>
|
|
435
|
+
isRelatedToAllowedPath(join(resolved.abs, e.name), allowedPaths),
|
|
436
|
+
)
|
|
243
437
|
.slice(0, TOOL_LIMITS.maxEntries)
|
|
244
438
|
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
245
439
|
return {
|
|
@@ -255,33 +449,79 @@ export function createScopedTools(opts: Options): ScopedToolDef[] {
|
|
|
255
449
|
const findTool: ScopedToolDef = {
|
|
256
450
|
name: "find",
|
|
257
451
|
description:
|
|
258
|
-
"
|
|
452
|
+
"Search for files by glob pattern within the approved scope; paths are relative to the search directory (max 20,000 entries).",
|
|
259
453
|
async execute(params) {
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
454
|
+
const pattern = params.pattern;
|
|
455
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
456
|
+
return { ok: false, error: "missing pattern" };
|
|
457
|
+
}
|
|
458
|
+
const limit =
|
|
459
|
+
params.limit === undefined
|
|
460
|
+
? TOOL_LIMITS.maxEntries
|
|
461
|
+
: boundedInteger(params.limit);
|
|
462
|
+
if (limit === undefined) {
|
|
463
|
+
return { ok: false, error: "limit must be a positive integer" };
|
|
267
464
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
465
|
+
const scan = resolveScanTargets(params.path);
|
|
466
|
+
if (!scan.ok) return { ok: false, error: scan.error };
|
|
467
|
+
for (const target of scan.targets) {
|
|
468
|
+
let st: Stats | undefined;
|
|
469
|
+
try {
|
|
470
|
+
st = statSync(target.abs);
|
|
471
|
+
} catch {
|
|
472
|
+
return { ok: false, error: "directory does not exist" };
|
|
473
|
+
}
|
|
474
|
+
if (!st.isDirectory()) return { ok: false, error: "not a directory" };
|
|
475
|
+
observe({
|
|
476
|
+
kind: "dir",
|
|
477
|
+
path: rootRelativePath(target.abs, scan.virtual),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
const matcher = globToRegExp(pattern);
|
|
481
|
+
if ("error" in matcher) {
|
|
482
|
+
return { ok: false, error: matcher.error };
|
|
483
|
+
}
|
|
484
|
+
const files: string[] = [];
|
|
485
|
+
for (const target of scan.targets) {
|
|
486
|
+
collectFiles(target.abs, files, TOOL_LIMITS.maxEntries, allowedPaths);
|
|
487
|
+
}
|
|
488
|
+
const findBase = scan.virtual ? virtualRoot : scan.targets[0].abs;
|
|
489
|
+
const out = files.map((abs) => relativePath(findBase, abs));
|
|
490
|
+
const entries = out.filter(
|
|
491
|
+
(rel) => matcher.test(rel) || matcher.test(basename(rel)),
|
|
492
|
+
);
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
entries: entries
|
|
496
|
+
.sort()
|
|
497
|
+
.slice(0, Math.min(limit, TOOL_LIMITS.maxEntries)),
|
|
498
|
+
};
|
|
272
499
|
},
|
|
273
500
|
};
|
|
274
501
|
|
|
275
|
-
function collectFiles(
|
|
502
|
+
function collectFiles(
|
|
503
|
+
dir: string,
|
|
504
|
+
out: string[],
|
|
505
|
+
limit: number,
|
|
506
|
+
scopedPaths?: string[],
|
|
507
|
+
): string[] {
|
|
276
508
|
if (out.length >= limit) return out;
|
|
509
|
+
const target = statSync(dir);
|
|
510
|
+
if (target.isFile()) {
|
|
511
|
+
if (isAllowedContent(dir, scopedPaths)) out.push(dir);
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
if (!target.isDirectory()) return out;
|
|
277
515
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
278
516
|
for (const e of entries) {
|
|
279
517
|
if (out.length >= limit) break;
|
|
280
518
|
if (isHidden(e.name, roots)) continue;
|
|
281
519
|
const full = join(dir, e.name);
|
|
282
520
|
if (e.isDirectory()) {
|
|
283
|
-
|
|
284
|
-
|
|
521
|
+
if (isRelatedToAllowedPath(full, scopedPaths)) {
|
|
522
|
+
collectFiles(full, out, limit, scopedPaths);
|
|
523
|
+
}
|
|
524
|
+
} else if (e.isFile() && isAllowedContent(full, scopedPaths)) {
|
|
285
525
|
out.push(full);
|
|
286
526
|
}
|
|
287
527
|
}
|
package/src/subagent-activity.ts
CHANGED
|
@@ -87,6 +87,11 @@ type ActivityTheme = {
|
|
|
87
87
|
fg?: (color: ActivityColor, text: string) => string;
|
|
88
88
|
};
|
|
89
89
|
type ActivityUi = Pick<ExtensionUIContext, "setWidget" | "setStatus">;
|
|
90
|
+
type ActivityRenderContext = {
|
|
91
|
+
args?: unknown;
|
|
92
|
+
executionStarted?: boolean;
|
|
93
|
+
isError?: boolean;
|
|
94
|
+
};
|
|
90
95
|
type TimerHandle = ReturnType<typeof setInterval>;
|
|
91
96
|
|
|
92
97
|
export interface ActivityControllerOptions {
|
|
@@ -734,12 +739,41 @@ export function renderActivityResult(
|
|
|
734
739
|
result: unknown,
|
|
735
740
|
options: ToolRenderResultOptions,
|
|
736
741
|
theme?: ActivityTheme,
|
|
742
|
+
context?: ActivityRenderContext,
|
|
737
743
|
): Component {
|
|
738
744
|
const details = asRecord(asRecord(result)?.details);
|
|
739
745
|
const activity = details?.[ACTIVITY_DETAILS_KEY];
|
|
740
|
-
|
|
746
|
+
const failed = context?.isError === true;
|
|
747
|
+
const args = asRecord(context?.args);
|
|
748
|
+
const failedRun = failed && args?.action === "run";
|
|
749
|
+
if (isActivityDisplay(activity) && !failed) {
|
|
741
750
|
return new ActivityInlineComponent(activity, theme, options.expanded);
|
|
742
751
|
}
|
|
752
|
+
if (failedRun) {
|
|
753
|
+
const snapshot = identityFromDispatchArgs(asRecord(context?.args)?.request);
|
|
754
|
+
const display: ActivityDisplay = {
|
|
755
|
+
version: 1,
|
|
756
|
+
kind: "activityDisplay",
|
|
757
|
+
requestId: isActivityDisplay(activity)
|
|
758
|
+
? activity.requestId
|
|
759
|
+
: snapshot.requestId,
|
|
760
|
+
role: isActivityDisplay(activity) ? activity.role : snapshot.role,
|
|
761
|
+
phase: isActivityDisplay(activity) ? activity.phase : snapshot.phase,
|
|
762
|
+
objective: isActivityDisplay(activity)
|
|
763
|
+
? activity.objective
|
|
764
|
+
: snapshot.objective,
|
|
765
|
+
state: "failed",
|
|
766
|
+
tone: "error",
|
|
767
|
+
elapsedMs: isActivityDisplay(activity) ? activity.elapsedMs : 0,
|
|
768
|
+
reason: sanitizeFailureReason(
|
|
769
|
+
isActivityDisplay(activity) ? activity.reason : undefined,
|
|
770
|
+
),
|
|
771
|
+
};
|
|
772
|
+
return new ActivityInlineComponent(display, theme);
|
|
773
|
+
}
|
|
774
|
+
if (failed) {
|
|
775
|
+
return new Text(`${dispatchLabel(args?.action)} failed`, 0, 0);
|
|
776
|
+
}
|
|
743
777
|
const content = asRecord(result)?.content;
|
|
744
778
|
const text = Array.isArray(content)
|
|
745
779
|
? content
|
|
@@ -750,6 +784,11 @@ export function renderActivityResult(
|
|
|
750
784
|
return new Text(text, 0, 0);
|
|
751
785
|
}
|
|
752
786
|
|
|
787
|
+
function dispatchLabel(action: unknown): string {
|
|
788
|
+
const value = sanitizeDisplayText(action ?? "");
|
|
789
|
+
return value.length > 0 ? `Abel Dispatch ${value}` : "Abel Dispatch";
|
|
790
|
+
}
|
|
791
|
+
|
|
753
792
|
function isActivityDisplay(value: unknown): value is ActivityDisplay {
|
|
754
793
|
const record = asRecord(value);
|
|
755
794
|
return (
|
|
@@ -764,26 +803,55 @@ function isActivityDisplay(value: unknown): value is ActivityDisplay {
|
|
|
764
803
|
);
|
|
765
804
|
}
|
|
766
805
|
|
|
806
|
+
function identityFromDispatchArgs(request: unknown): {
|
|
807
|
+
requestId: string;
|
|
808
|
+
role: string;
|
|
809
|
+
phase: string;
|
|
810
|
+
objective: string;
|
|
811
|
+
} {
|
|
812
|
+
const value = asRecord(request);
|
|
813
|
+
const attempt = asRecord(value?.attempt);
|
|
814
|
+
const boundary = asRecord(value?.boundary);
|
|
815
|
+
return {
|
|
816
|
+
requestId: sanitizeDisplayText(
|
|
817
|
+
attempt?.requestId ?? value?.id ?? "unknown",
|
|
818
|
+
128,
|
|
819
|
+
),
|
|
820
|
+
role: sanitizeDisplayText(
|
|
821
|
+
value?.role ??
|
|
822
|
+
(value?.stage === "abel-implement"
|
|
823
|
+
? "implementation-worker"
|
|
824
|
+
: "unknown"),
|
|
825
|
+
80,
|
|
826
|
+
),
|
|
827
|
+
phase: sanitizeDisplayText(attempt?.phase ?? value?.phase ?? "unknown", 40),
|
|
828
|
+
objective: sanitizeDisplayText(
|
|
829
|
+
value?.objective ?? boundary?.objective ?? "",
|
|
830
|
+
240,
|
|
831
|
+
),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
767
835
|
export function renderActivityCall(
|
|
768
836
|
args: unknown,
|
|
769
837
|
theme?: ActivityTheme,
|
|
838
|
+
context?: ActivityRenderContext,
|
|
770
839
|
): Component {
|
|
771
840
|
const value = asRecord(args);
|
|
772
841
|
if (value?.action !== "run") {
|
|
773
|
-
return new Text(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
);
|
|
842
|
+
return new Text(context?.isError ? "" : dispatchLabel(value?.action), 0, 0);
|
|
843
|
+
}
|
|
844
|
+
if (context?.executionStarted || context?.isError) {
|
|
845
|
+
return new Text("");
|
|
778
846
|
}
|
|
779
|
-
|
|
780
|
-
|
|
847
|
+
if (!asRecord(value.request)) return new Text("Subagent", 0, 0);
|
|
848
|
+
const identity = identityFromDispatchArgs(value.request);
|
|
781
849
|
const display: ActivitySnapshot = {
|
|
782
850
|
toolCallId: "call",
|
|
783
|
-
requestId:
|
|
784
|
-
role:
|
|
785
|
-
phase:
|
|
786
|
-
objective:
|
|
851
|
+
requestId: identity.requestId,
|
|
852
|
+
role: identity.role,
|
|
853
|
+
phase: identity.phase,
|
|
854
|
+
objective: identity.objective,
|
|
787
855
|
state: "queued",
|
|
788
856
|
sequence: 0,
|
|
789
857
|
startedAt: Date.now(),
|