@yagni-app/code-staging 0.0.0 → 0.1.0-staging.1004.1
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/LICENSE.md +55 -0
- package/README.md +166 -0
- package/dist/branding.d.ts +25 -0
- package/dist/branding.js +27 -0
- package/dist/claudeCompat.d.ts +78 -0
- package/dist/claudeCompat.js +198 -0
- package/dist/cli.d.ts +55 -0
- package/dist/cli.js +326 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +28 -0
- package/dist/credentials.d.ts +41 -0
- package/dist/credentials.js +75 -0
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +94 -0
- package/dist/doctor.js +343 -0
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +188 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +74 -0
- package/dist/extension/branding.d.ts +48 -0
- package/dist/extension/branding.js +119 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +369 -0
- package/dist/extension/config.d.ts +112 -0
- package/dist/extension/config.js +108 -0
- package/dist/extension/costHud.d.ts +73 -0
- package/dist/extension/costHud.js +123 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +116 -0
- package/dist/extension/index.js +388 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +152 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +168 -0
- package/dist/extension/pipeline/eval.js +182 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +128 -0
- package/dist/extension/pipeline/goCommand.js +972 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +48 -0
- package/dist/extension/pipeline/goCompareCommand.js +184 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +636 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +158 -0
- package/dist/extension/pipeline/runState.js +264 -0
- package/dist/extension/pipeline/runner.d.ts +79 -0
- package/dist/extension/pipeline/runner.js +298 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +51 -0
- package/dist/extension/pipeline/stages.js +113 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +436 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +758 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +200 -0
- package/dist/extension/provider.d.ts +11 -0
- package/dist/extension/provider.js +26 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +187 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +135 -0
- package/dist/extension/subagents.js +281 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.js +111 -0
- package/dist/login.d.ts +45 -0
- package/dist/login.js +142 -0
- package/dist/logout.d.ts +14 -0
- package/dist/logout.js +34 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +87 -0
- package/dist/piPackage.d.ts +33 -0
- package/dist/piPackage.js +71 -0
- package/dist/profiles.d.ts +80 -0
- package/dist/profiles.js +222 -0
- package/dist/refresh.d.ts +70 -0
- package/dist/refresh.js +117 -0
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +39 -3
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable write-spool for the judgment-capture tools (spec §4, R4 write half).
|
|
3
|
+
*
|
|
4
|
+
* `record_decision` and `record_engineering_context` are metered writes that
|
|
5
|
+
* must never be blindly retried (invariant §0.8) — but losing a captured
|
|
6
|
+
* decision because the laptop lost Wi-Fi is worse. The backend now dedups on
|
|
7
|
+
* an `idempotencyKey` (§2), so the safe shape is:
|
|
8
|
+
*
|
|
9
|
+
* - every logical write carries a generated idempotencyKey,
|
|
10
|
+
* - a transport failure or 5xx appends `{endpoint, body, idempotencyKey, ts}`
|
|
11
|
+
* to `~/.yagni-code/spool/pending.ndjson` (dir 0700, file 0600,
|
|
12
|
+
* torn-line-tolerant — the same self-healing NDJSON pattern as
|
|
13
|
+
* pipeline/checkpoint.ts),
|
|
14
|
+
* - the spool is flushed at session_start and after any successful write,
|
|
15
|
+
* replaying with the SAME idempotencyKey (the server replays the stored
|
|
16
|
+
* response, so a duplicate flush is harmless),
|
|
17
|
+
* - entries older than 14 days are dropped with an honest notice (stale
|
|
18
|
+
* judgment silently landing weeks later would be worse than losing it).
|
|
19
|
+
*
|
|
20
|
+
* Everything here is FAIL-SOFT: a spool problem must never break a tool call
|
|
21
|
+
* or session start. Eval mode never reaches this module (the capture tools are
|
|
22
|
+
* not registered there).
|
|
23
|
+
*/
|
|
24
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
27
|
+
import { codeStateHome } from "./stateHome.js";
|
|
28
|
+
/** Spooled entries older than this are dropped at flush (with a notice). */
|
|
29
|
+
export const MAX_SPOOL_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
30
|
+
/**
|
|
31
|
+
* Test seam mirroring checkpoint.ts `_setCheckpointHomeForTest`: when set, the
|
|
32
|
+
* spool roots here instead of `~/.yagni-code`. Pass `null` to restore.
|
|
33
|
+
*/
|
|
34
|
+
let homeOverride = null;
|
|
35
|
+
export function _setSpoolHomeForTest(dir) {
|
|
36
|
+
homeOverride = dir;
|
|
37
|
+
}
|
|
38
|
+
function spoolHome() {
|
|
39
|
+
return codeStateHome(homeOverride);
|
|
40
|
+
}
|
|
41
|
+
function spoolDir() {
|
|
42
|
+
return join(spoolHome(), "spool");
|
|
43
|
+
}
|
|
44
|
+
/** The on-disk pending file (exported so tests can inspect it directly). */
|
|
45
|
+
export function spoolFile() {
|
|
46
|
+
return join(spoolDir(), "pending.ndjson");
|
|
47
|
+
}
|
|
48
|
+
/** Append one pending write. NEVER throws (fail-soft: memory loss beats a crash). */
|
|
49
|
+
export function appendToSpool(entry) {
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(spoolDir(), { recursive: true, mode: 0o700 });
|
|
52
|
+
const file = spoolFile();
|
|
53
|
+
// Self-heal a torn previous write (crash mid-append): if the file ends
|
|
54
|
+
// mid-line, prefix a newline so OUR record lands on its own line instead of
|
|
55
|
+
// gluing onto the fragment and losing both (mirrors checkpoint.ts).
|
|
56
|
+
let prefix = "";
|
|
57
|
+
try {
|
|
58
|
+
const existing = readFileSync(file);
|
|
59
|
+
if (existing.length > 0 && existing[existing.length - 1] !== 0x0a)
|
|
60
|
+
prefix = "\n";
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* no file yet — nothing to heal */
|
|
64
|
+
}
|
|
65
|
+
appendFileSync(file, `${prefix}${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* fail-soft: a spool write must never break the tool call */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function isSpoolEntry(value) {
|
|
72
|
+
const e = value;
|
|
73
|
+
return (!!e &&
|
|
74
|
+
typeof e === "object" &&
|
|
75
|
+
typeof e.endpoint === "string" &&
|
|
76
|
+
e.endpoint.startsWith("/") &&
|
|
77
|
+
!!e.body &&
|
|
78
|
+
typeof e.body === "object" &&
|
|
79
|
+
typeof e.idempotencyKey === "string" &&
|
|
80
|
+
typeof e.ts === "number");
|
|
81
|
+
}
|
|
82
|
+
/** Load pending entries, dropping torn/garbage lines. Never throws. */
|
|
83
|
+
export function loadSpool() {
|
|
84
|
+
try {
|
|
85
|
+
const raw = readFileSync(spoolFile(), "utf8");
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const line of raw.split("\n")) {
|
|
88
|
+
if (!line.trim())
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
const obj = JSON.parse(line);
|
|
92
|
+
if (isSpoolEntry(obj))
|
|
93
|
+
out.push(obj);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* unparseable (torn final write) — drop this line, keep the rest */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return []; // missing/unreadable → nothing pending
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Atomically rewrite the pending file with the surviving entries (0600). */
|
|
106
|
+
function rewriteSpool(entries) {
|
|
107
|
+
try {
|
|
108
|
+
const file = spoolFile();
|
|
109
|
+
if (entries.length === 0) {
|
|
110
|
+
rmSync(file, { force: true });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
mkdirSync(spoolDir(), { recursive: true, mode: 0o700 });
|
|
114
|
+
const tmp = join(spoolDir(), `.pending-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.tmp`);
|
|
115
|
+
writeFileSync(tmp, entries.map((e) => JSON.stringify(e)).join("\n") + "\n", { mode: 0o600 });
|
|
116
|
+
renameSync(tmp, file);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* fail-soft: worst case the next flush replays an already-deduped key */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const EMPTY_FLUSH = {
|
|
123
|
+
replayed: 0,
|
|
124
|
+
droppedExpired: 0,
|
|
125
|
+
droppedRejected: 0,
|
|
126
|
+
remaining: 0,
|
|
127
|
+
notices: [],
|
|
128
|
+
};
|
|
129
|
+
function plural(n) {
|
|
130
|
+
return n === 1 ? "" : "s";
|
|
131
|
+
}
|
|
132
|
+
let flushInFlight = null;
|
|
133
|
+
/**
|
|
134
|
+
* Replay every pending write with its ORIGINAL idempotencyKey (server-side
|
|
135
|
+
* dedup makes a double replay a no-op). Serialized: concurrent flushes (a
|
|
136
|
+
* session_start racing a post-write flush) coalesce onto one pass.
|
|
137
|
+
*
|
|
138
|
+
* Per entry: expired (>14d) → dropped with a notice; 2xx → replayed; a
|
|
139
|
+
* permanent 4xx (not auth/rate) → dropped with a notice (the backend will
|
|
140
|
+
* never accept it); transport error, 5xx, 401/403/408/429 → kept, and the
|
|
141
|
+
* rest of the flush stops (the backend is unreachable or the token is bad —
|
|
142
|
+
* hammering the remaining entries would not help). Never throws.
|
|
143
|
+
*/
|
|
144
|
+
export function flushSpool(opts) {
|
|
145
|
+
if (!flushInFlight) {
|
|
146
|
+
flushInFlight = doFlush(opts)
|
|
147
|
+
.catch(() => EMPTY_FLUSH)
|
|
148
|
+
.finally(() => {
|
|
149
|
+
flushInFlight = null;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return flushInFlight;
|
|
153
|
+
}
|
|
154
|
+
/** Statuses that mean "try again later", not "the backend rejected this body". */
|
|
155
|
+
const KEEP_STATUSES = new Set([401, 403, 408, 429]);
|
|
156
|
+
async function doFlush(opts) {
|
|
157
|
+
const entries = loadSpool();
|
|
158
|
+
if (entries.length === 0)
|
|
159
|
+
return EMPTY_FLUSH;
|
|
160
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
161
|
+
// Keys we have RESOLVED this pass (replayed OK, or permanently dropped). We do
|
|
162
|
+
// NOT rewrite from this stale snapshot — instead we re-read the current spool
|
|
163
|
+
// just before writing and drop only these resolved keys, so a write appended
|
|
164
|
+
// mid-flush (a tool that failed while we were replaying) is never clobbered.
|
|
165
|
+
const resolved = new Set();
|
|
166
|
+
let replayed = 0;
|
|
167
|
+
let droppedExpired = 0;
|
|
168
|
+
let droppedRejected = 0;
|
|
169
|
+
let stopped = false;
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (nowMs - entry.ts > MAX_SPOOL_AGE_MS) {
|
|
172
|
+
droppedExpired += 1;
|
|
173
|
+
resolved.add(entry.idempotencyKey);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (stopped)
|
|
177
|
+
continue;
|
|
178
|
+
try {
|
|
179
|
+
const res = await resilientFetch(`${opts.baseUrl}${entry.endpoint}`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
"content-type": "application/json",
|
|
183
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
184
|
+
},
|
|
185
|
+
// Replay verbatim; re-assert the key in case an old entry predates it.
|
|
186
|
+
body: JSON.stringify({ ...entry.body, idempotencyKey: entry.idempotencyKey }),
|
|
187
|
+
}, { fetchImpl: opts.fetchImpl, policy: METERED_POST_FETCH_POLICY });
|
|
188
|
+
if (res.ok) {
|
|
189
|
+
replayed += 1;
|
|
190
|
+
resolved.add(entry.idempotencyKey);
|
|
191
|
+
}
|
|
192
|
+
else if (res.status >= 500 || KEEP_STATUSES.has(res.status)) {
|
|
193
|
+
stopped = true;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
droppedRejected += 1;
|
|
197
|
+
resolved.add(entry.idempotencyKey);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
stopped = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Re-read + filter + rewrite are back-to-back synchronous calls with no await
|
|
205
|
+
// between them, so no append can interleave in that window (single-threaded).
|
|
206
|
+
// Everything not resolved this pass — kept entries AND any mid-flush append —
|
|
207
|
+
// survives. A re-appended duplicate of a replayed key is harmless (the server
|
|
208
|
+
// dedups on idempotencyKey; it just replays next pass).
|
|
209
|
+
const keep = loadSpool().filter((e) => !resolved.has(e.idempotencyKey));
|
|
210
|
+
rewriteSpool(keep);
|
|
211
|
+
const notices = [];
|
|
212
|
+
if (replayed > 0) {
|
|
213
|
+
notices.push(`Replayed ${replayed} locally spooled YAGNI record${plural(replayed)}.`);
|
|
214
|
+
}
|
|
215
|
+
if (droppedExpired > 0) {
|
|
216
|
+
notices.push(`Dropped ${droppedExpired} unsent YAGNI record${plural(droppedExpired)} older than 14 days from the local spool.`);
|
|
217
|
+
}
|
|
218
|
+
if (droppedRejected > 0) {
|
|
219
|
+
notices.push(`Discarded ${droppedRejected} spooled YAGNI record${plural(droppedRejected)} the backend rejected.`);
|
|
220
|
+
}
|
|
221
|
+
return { replayed, droppedExpired, droppedRejected, remaining: keep.length, notices };
|
|
222
|
+
// (keep.length reflects the post-rewrite spool, including any mid-flush append.)
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* POST one judgment write with its idempotencyKey; on transport failure or a
|
|
226
|
+
* 5xx, durably spool it for replay instead of losing it. A successful write
|
|
227
|
+
* also kicks a background flush (fail-soft, fire-and-forget) so earlier
|
|
228
|
+
* stranded entries drain as soon as the backend is healthy again.
|
|
229
|
+
*/
|
|
230
|
+
export async function sendOrSpool(opts, label, endpoint, body, idempotencyKey, signal) {
|
|
231
|
+
const posted = { ...body, idempotencyKey };
|
|
232
|
+
let res;
|
|
233
|
+
try {
|
|
234
|
+
res = await resilientFetch(`${opts.baseUrl}${endpoint}`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
"content-type": "application/json",
|
|
238
|
+
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify(posted),
|
|
241
|
+
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
245
|
+
appendToSpool({ endpoint, body: posted, idempotencyKey, ts: (opts.now ?? Date.now)() });
|
|
246
|
+
return { kind: "spooled", reason };
|
|
247
|
+
}
|
|
248
|
+
if (res.ok) {
|
|
249
|
+
// Success proves the backend is reachable: drain anything stranded earlier.
|
|
250
|
+
void flushSpool(opts).catch(() => { });
|
|
251
|
+
let json = null;
|
|
252
|
+
try {
|
|
253
|
+
json = await res.json();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
/* a 2xx with a non-JSON body still counts as recorded */
|
|
257
|
+
}
|
|
258
|
+
return { kind: "ok", json };
|
|
259
|
+
}
|
|
260
|
+
if (res.status >= 500) {
|
|
261
|
+
appendToSpool({ endpoint, body: posted, idempotencyKey, ts: (opts.now ?? Date.now)() });
|
|
262
|
+
return { kind: "spooled", reason: `the YAGNI backend returned HTTP ${res.status}` };
|
|
263
|
+
}
|
|
264
|
+
return { kind: "rejected", message: await friendlyFetchError(label, res) };
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=spool.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function codeStateHome(override, env = process.env, userHome = homedir()) {
|
|
4
|
+
return override ?? env.YAGNI_CODE_HOME ?? join(userHome, ".yagni-code");
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=stateHome.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The general `subagent` tool — delegate a self-contained task to an agent
|
|
3
|
+
* with its own fresh context window, optionally in parallel.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the /go pipeline's battle-tested child runner (`runStage`: NDJSON
|
|
6
|
+
* stream folding, output caps, abort escalation) through its `personaBody`
|
|
7
|
+
* seam, so a subagent is exactly a grounded pi child whose system prompt is
|
|
8
|
+
* the agent definition's body instead of a /go persona.
|
|
9
|
+
*
|
|
10
|
+
* Agent definitions are the Claude Code format — markdown with
|
|
11
|
+
* name/description/tools/model frontmatter — discovered from the repo's
|
|
12
|
+
* `.claude/agents/` and `.pi/agents/` plus the user's `~/.claude/agents/`
|
|
13
|
+
* (project beats user on a name collision). A built-in `general` agent is
|
|
14
|
+
* always available, so the tool works in a repo with no definitions at all.
|
|
15
|
+
* Claude model names map onto YAGNI tiers (opus→advanced, sonnet→standard,
|
|
16
|
+
* haiku→efficient); unknown models run on standard.
|
|
17
|
+
*/
|
|
18
|
+
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { Type } from "typebox";
|
|
20
|
+
import { runStage } from "./pipeline/runner.js";
|
|
21
|
+
import type { ModelTier, PipelineStage } from "./pipeline/types.js";
|
|
22
|
+
export declare const SUBAGENT_TOOL_NAME = "subagent";
|
|
23
|
+
export declare const GENERAL_AGENT_NAME = "general";
|
|
24
|
+
export declare const MAX_PARALLEL_SUBAGENTS = 4;
|
|
25
|
+
/**
|
|
26
|
+
* The default tool surface a subagent gets when its definition declares none:
|
|
27
|
+
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
28
|
+
* subagent would inherit.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_SUBAGENT_TOOLS: string[];
|
|
31
|
+
export type SubagentSource = "project-claude" | "project-pi" | "user-claude" | "builtin";
|
|
32
|
+
export interface SubagentDef {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
tools?: string[];
|
|
36
|
+
model: ModelTier;
|
|
37
|
+
/** The agent's system prompt (the markdown body). */
|
|
38
|
+
body: string;
|
|
39
|
+
source: SubagentSource;
|
|
40
|
+
}
|
|
41
|
+
/** Claude Code model names → YAGNI tiers; YAGNI tiers pass through. */
|
|
42
|
+
export declare function mapModelTier(model: string | undefined): ModelTier;
|
|
43
|
+
/**
|
|
44
|
+
* Parse one Claude Code-format agent file. Returns null when the file has no
|
|
45
|
+
* description — the one field the tool cannot do without (the model picks
|
|
46
|
+
* agents by it).
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseAgentMarkdown(content: string, fallbackName: string, source: SubagentSource): SubagentDef | null;
|
|
49
|
+
export interface DiscoverDeps {
|
|
50
|
+
cwd: string;
|
|
51
|
+
homeDir?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* All available agents: built-in general, then user `~/.claude/agents`, then
|
|
55
|
+
* the repo's `.pi/agents` and `.claude/agents` — later wins on a name
|
|
56
|
+
* collision, so a project definition beats a user one beats the built-in.
|
|
57
|
+
*/
|
|
58
|
+
export declare function discoverSubagents(deps: DiscoverDeps): SubagentDef[];
|
|
59
|
+
export declare function formatAgentList(agents: SubagentDef[]): string;
|
|
60
|
+
/**
|
|
61
|
+
* A subagent invocation is a synthetic pipeline stage: the task rides in as
|
|
62
|
+
* `{ticket}` verbatim, and the agent's body replaces the /go persona through
|
|
63
|
+
* the runner's personaBody seam.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildSubagentStage(def: SubagentDef, task: string): {
|
|
66
|
+
stage: PipelineStage;
|
|
67
|
+
ctx: {
|
|
68
|
+
ticket: string;
|
|
69
|
+
grounded: boolean;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
type SubagentParams = {
|
|
73
|
+
task?: string;
|
|
74
|
+
agent?: string;
|
|
75
|
+
tasks?: Array<{
|
|
76
|
+
task: string;
|
|
77
|
+
agent?: string;
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
export interface MakeSubagentToolDeps {
|
|
81
|
+
runStageImpl?: typeof runStage;
|
|
82
|
+
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
83
|
+
homeDir?: string;
|
|
84
|
+
}
|
|
85
|
+
export declare function makeSubagentTool(deps?: MakeSubagentToolDeps): {
|
|
86
|
+
name: string;
|
|
87
|
+
label: string;
|
|
88
|
+
description: string;
|
|
89
|
+
promptSnippet: string;
|
|
90
|
+
parameters: Type.TObject<{
|
|
91
|
+
task: Type.TOptional<Type.TString>;
|
|
92
|
+
agent: Type.TOptional<Type.TString>;
|
|
93
|
+
tasks: Type.TOptional<Type.TArray<Type.TObject<{
|
|
94
|
+
task: Type.TString;
|
|
95
|
+
agent: Type.TOptional<Type.TString>;
|
|
96
|
+
}>>>;
|
|
97
|
+
}>;
|
|
98
|
+
execute(_toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: (update: {
|
|
99
|
+
content: Array<{
|
|
100
|
+
type: "text";
|
|
101
|
+
text: string;
|
|
102
|
+
}>;
|
|
103
|
+
details: unknown;
|
|
104
|
+
}) => void, ctx?: ExtensionContext): Promise<{
|
|
105
|
+
content: {
|
|
106
|
+
type: "text";
|
|
107
|
+
text: string;
|
|
108
|
+
}[];
|
|
109
|
+
details: {};
|
|
110
|
+
isError: boolean;
|
|
111
|
+
} | {
|
|
112
|
+
isError?: boolean | undefined;
|
|
113
|
+
content: {
|
|
114
|
+
type: "text";
|
|
115
|
+
text: string;
|
|
116
|
+
}[];
|
|
117
|
+
details: {
|
|
118
|
+
tasks: {
|
|
119
|
+
agent: string;
|
|
120
|
+
task: string;
|
|
121
|
+
exitCode: number;
|
|
122
|
+
usage: import("./pipeline/types.js").StageUsage;
|
|
123
|
+
toolCalls: number;
|
|
124
|
+
}[];
|
|
125
|
+
};
|
|
126
|
+
}>;
|
|
127
|
+
};
|
|
128
|
+
export interface RegisterSubagentsDeps {
|
|
129
|
+
discover?: (deps: DiscoverDeps) => SubagentDef[];
|
|
130
|
+
homeDir?: string;
|
|
131
|
+
}
|
|
132
|
+
/** Wire the subagent tool and the /agents listing command. */
|
|
133
|
+
export declare function registerSubagents(pi: ExtensionAPI, deps?: RegisterSubagentsDeps): void;
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=subagents.d.ts.map
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The general `subagent` tool — delegate a self-contained task to an agent
|
|
3
|
+
* with its own fresh context window, optionally in parallel.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the /go pipeline's battle-tested child runner (`runStage`: NDJSON
|
|
6
|
+
* stream folding, output caps, abort escalation) through its `personaBody`
|
|
7
|
+
* seam, so a subagent is exactly a grounded pi child whose system prompt is
|
|
8
|
+
* the agent definition's body instead of a /go persona.
|
|
9
|
+
*
|
|
10
|
+
* Agent definitions are the Claude Code format — markdown with
|
|
11
|
+
* name/description/tools/model frontmatter — discovered from the repo's
|
|
12
|
+
* `.claude/agents/` and `.pi/agents/` plus the user's `~/.claude/agents/`
|
|
13
|
+
* (project beats user on a name collision). A built-in `general` agent is
|
|
14
|
+
* always available, so the tool works in a repo with no definitions at all.
|
|
15
|
+
* Claude model names map onto YAGNI tiers (opus→advanced, sonnet→standard,
|
|
16
|
+
* haiku→efficient); unknown models run on standard.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { Type } from "typebox";
|
|
23
|
+
import { runStage } from "./pipeline/runner.js";
|
|
24
|
+
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
25
|
+
export const GENERAL_AGENT_NAME = "general";
|
|
26
|
+
export const MAX_PARALLEL_SUBAGENTS = 4;
|
|
27
|
+
/**
|
|
28
|
+
* The default tool surface a subagent gets when its definition declares none:
|
|
29
|
+
* the full working set plus grounded answers, mirroring what a Claude Code
|
|
30
|
+
* subagent would inherit.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_SUBAGENT_TOOLS = [
|
|
33
|
+
"read",
|
|
34
|
+
"grep",
|
|
35
|
+
"find",
|
|
36
|
+
"ls",
|
|
37
|
+
"bash",
|
|
38
|
+
"edit",
|
|
39
|
+
"write",
|
|
40
|
+
"ask_yagni",
|
|
41
|
+
];
|
|
42
|
+
const GENERAL_BODY = `You are a capable software-engineering subagent with a fresh context window. Complete the task you are given end to end, autonomously.
|
|
43
|
+
|
|
44
|
+
You are grounded in how THIS company works: call ask_yagni before inferring a convention, an ownership rule, or anything organization-specific.
|
|
45
|
+
|
|
46
|
+
Your final message is your report back to the driving agent, which has NOT seen what you read or did. Make it compressed and complete: what you did, what you found, exact file paths and key excerpts, and anything the driver must know before continuing.`;
|
|
47
|
+
const GENERAL_AGENT = {
|
|
48
|
+
name: GENERAL_AGENT_NAME,
|
|
49
|
+
description: "General-purpose agent for research, multi-file changes, and self-contained tasks.",
|
|
50
|
+
model: "standard",
|
|
51
|
+
body: GENERAL_BODY,
|
|
52
|
+
source: "builtin",
|
|
53
|
+
};
|
|
54
|
+
const TIER_VALUES = ["peak", "advanced", "standard", "efficient"];
|
|
55
|
+
/** Claude Code model names → YAGNI tiers; YAGNI tiers pass through. */
|
|
56
|
+
export function mapModelTier(model) {
|
|
57
|
+
if (!model)
|
|
58
|
+
return "standard";
|
|
59
|
+
const normalized = model.trim().toLowerCase();
|
|
60
|
+
if (TIER_VALUES.includes(normalized))
|
|
61
|
+
return normalized;
|
|
62
|
+
if (normalized.includes("opus"))
|
|
63
|
+
return "advanced";
|
|
64
|
+
if (normalized.includes("sonnet"))
|
|
65
|
+
return "standard";
|
|
66
|
+
if (normalized.includes("haiku"))
|
|
67
|
+
return "efficient";
|
|
68
|
+
return "standard";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse one Claude Code-format agent file. Returns null when the file has no
|
|
72
|
+
* description — the one field the tool cannot do without (the model picks
|
|
73
|
+
* agents by it).
|
|
74
|
+
*/
|
|
75
|
+
export function parseAgentMarkdown(content, fallbackName, source) {
|
|
76
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
77
|
+
const description = frontmatter.description?.trim();
|
|
78
|
+
if (!description)
|
|
79
|
+
return null;
|
|
80
|
+
const tools = frontmatter.tools
|
|
81
|
+
?.split(",")
|
|
82
|
+
.map((t) => t.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
return {
|
|
85
|
+
name: (frontmatter.name?.trim() || fallbackName).toLowerCase(),
|
|
86
|
+
description,
|
|
87
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
88
|
+
model: mapModelTier(frontmatter.model),
|
|
89
|
+
body: body.trim() || GENERAL_BODY,
|
|
90
|
+
source,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function loadAgentsFromDir(dir, source) {
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const agents = [];
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.name.endsWith(".md"))
|
|
104
|
+
continue;
|
|
105
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
106
|
+
continue;
|
|
107
|
+
try {
|
|
108
|
+
const content = fs.readFileSync(join(dir, entry.name), "utf8");
|
|
109
|
+
const def = parseAgentMarkdown(content, entry.name.replace(/\.md$/, ""), source);
|
|
110
|
+
if (def)
|
|
111
|
+
agents.push(def);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return agents;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* All available agents: built-in general, then user `~/.claude/agents`, then
|
|
121
|
+
* the repo's `.pi/agents` and `.claude/agents` — later wins on a name
|
|
122
|
+
* collision, so a project definition beats a user one beats the built-in.
|
|
123
|
+
*/
|
|
124
|
+
export function discoverSubagents(deps) {
|
|
125
|
+
const home = deps.homeDir ?? homedir();
|
|
126
|
+
const layers = [
|
|
127
|
+
[GENERAL_AGENT],
|
|
128
|
+
loadAgentsFromDir(join(home, ".claude", "agents"), "user-claude"),
|
|
129
|
+
loadAgentsFromDir(join(deps.cwd, ".pi", "agents"), "project-pi"),
|
|
130
|
+
loadAgentsFromDir(join(deps.cwd, ".claude", "agents"), "project-claude"),
|
|
131
|
+
];
|
|
132
|
+
const byName = new Map();
|
|
133
|
+
for (const layer of layers) {
|
|
134
|
+
for (const agent of layer)
|
|
135
|
+
byName.set(agent.name, agent);
|
|
136
|
+
}
|
|
137
|
+
return [...byName.values()];
|
|
138
|
+
}
|
|
139
|
+
const SOURCE_LABEL = {
|
|
140
|
+
builtin: "built-in",
|
|
141
|
+
"user-claude": "~/.claude/agents",
|
|
142
|
+
"project-pi": ".pi/agents",
|
|
143
|
+
"project-claude": ".claude/agents",
|
|
144
|
+
};
|
|
145
|
+
export function formatAgentList(agents) {
|
|
146
|
+
return agents
|
|
147
|
+
.map((a) => `${a.name} (${SOURCE_LABEL[a.source]}): ${a.description}`)
|
|
148
|
+
.join("\n");
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A subagent invocation is a synthetic pipeline stage: the task rides in as
|
|
152
|
+
* `{ticket}` verbatim, and the agent's body replaces the /go persona through
|
|
153
|
+
* the runner's personaBody seam.
|
|
154
|
+
*/
|
|
155
|
+
export function buildSubagentStage(def, task) {
|
|
156
|
+
return {
|
|
157
|
+
stage: {
|
|
158
|
+
id: "implement",
|
|
159
|
+
agent: def.name,
|
|
160
|
+
model: def.model,
|
|
161
|
+
tools: def.tools ?? DEFAULT_SUBAGENT_TOOLS,
|
|
162
|
+
taskTemplate: "{ticket}",
|
|
163
|
+
},
|
|
164
|
+
ctx: { ticket: task, grounded: true },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const parameters = Type.Object({
|
|
168
|
+
task: Type.Optional(Type.String({
|
|
169
|
+
description: "The self-contained task. Include everything needed: the subagent shares none of your context.",
|
|
170
|
+
})),
|
|
171
|
+
agent: Type.Optional(Type.String({ description: "Agent name (see /agents). Defaults to general." })),
|
|
172
|
+
tasks: Type.Optional(Type.Array(Type.Object({
|
|
173
|
+
task: Type.String(),
|
|
174
|
+
agent: Type.Optional(Type.String()),
|
|
175
|
+
}), {
|
|
176
|
+
description: `Run several independent tasks in parallel (max ${MAX_PARALLEL_SUBAGENTS}). Use INSTEAD of task.`,
|
|
177
|
+
})),
|
|
178
|
+
});
|
|
179
|
+
export function makeSubagentTool(deps = {}) {
|
|
180
|
+
const run = deps.runStageImpl ?? runStage;
|
|
181
|
+
const discover = deps.discover ?? discoverSubagents;
|
|
182
|
+
return {
|
|
183
|
+
name: SUBAGENT_TOOL_NAME,
|
|
184
|
+
label: "Subagent",
|
|
185
|
+
description: "Delegate a self-contained task to a subagent with its own fresh context window, and get back " +
|
|
186
|
+
"a compressed report. Use it for context-heavy exploration you don't need blow-by-blow, or to " +
|
|
187
|
+
"run independent tasks in parallel via `tasks`. The subagent shares NONE of your conversation: " +
|
|
188
|
+
"spell out the task completely. Agents come from this repo's .claude/agents and .pi/agents " +
|
|
189
|
+
"(list them with /agents); omit `agent` for the general-purpose one.",
|
|
190
|
+
promptSnippet: "subagent: delegate a self-contained task (or parallel tasks) to a fresh-context agent; returns its report.",
|
|
191
|
+
parameters,
|
|
192
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
193
|
+
const fail = (text) => ({
|
|
194
|
+
content: [{ type: "text", text }],
|
|
195
|
+
details: {},
|
|
196
|
+
isError: true,
|
|
197
|
+
});
|
|
198
|
+
const requested = params.tasks && params.tasks.length > 0
|
|
199
|
+
? params.tasks
|
|
200
|
+
: params.task
|
|
201
|
+
? [{ task: params.task, agent: params.agent }]
|
|
202
|
+
: [];
|
|
203
|
+
if (requested.length === 0) {
|
|
204
|
+
return fail("Error: pass `task` (or a `tasks` array) describing what to do.");
|
|
205
|
+
}
|
|
206
|
+
if (requested.length > MAX_PARALLEL_SUBAGENTS) {
|
|
207
|
+
return fail(`Error: at most ${MAX_PARALLEL_SUBAGENTS} parallel tasks per call.`);
|
|
208
|
+
}
|
|
209
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
210
|
+
const agents = discover({ cwd, homeDir: deps.homeDir });
|
|
211
|
+
const resolved = [];
|
|
212
|
+
for (const req of requested) {
|
|
213
|
+
const name = (req.agent ?? GENERAL_AGENT_NAME).toLowerCase();
|
|
214
|
+
const def = agents.find((a) => a.name === name);
|
|
215
|
+
if (!def) {
|
|
216
|
+
return fail(`Error: unknown agent "${name}". Available agents:\n${formatAgentList(agents)}`);
|
|
217
|
+
}
|
|
218
|
+
resolved.push({ def, task: req.task });
|
|
219
|
+
}
|
|
220
|
+
onUpdate?.({
|
|
221
|
+
content: [
|
|
222
|
+
{
|
|
223
|
+
type: "text",
|
|
224
|
+
text: resolved.length === 1
|
|
225
|
+
? `Running ${resolved[0].def.name} subagent…`
|
|
226
|
+
: `Running ${resolved.length} subagents in parallel…`,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
details: {},
|
|
230
|
+
});
|
|
231
|
+
const outcomes = await Promise.all(resolved.map(async ({ def, task }) => {
|
|
232
|
+
const { stage, ctx: stageCtx } = buildSubagentStage(def, task);
|
|
233
|
+
const result = await run(stage, stageCtx, {
|
|
234
|
+
cwd,
|
|
235
|
+
signal,
|
|
236
|
+
personaBody: () => def.body,
|
|
237
|
+
});
|
|
238
|
+
return { agent: def.name, task, result };
|
|
239
|
+
}));
|
|
240
|
+
const allFailed = outcomes.every((o) => o.result.exitCode !== 0);
|
|
241
|
+
const sections = outcomes.map((o) => {
|
|
242
|
+
const output = o.result.finalOutput.trim();
|
|
243
|
+
const failedNote = o.result.exitCode !== 0
|
|
244
|
+
? `\n\n(subagent failed, exit ${o.result.exitCode}${o.result.stderr.trim() ? `: ${o.result.stderr.trim().slice(-500)}` : ""})`
|
|
245
|
+
: "";
|
|
246
|
+
const bodyText = output || (o.result.exitCode === 0 ? "(no output)" : "");
|
|
247
|
+
return outcomes.length === 1
|
|
248
|
+
? `${bodyText}${failedNote}`
|
|
249
|
+
: `## ${o.agent}: ${o.task}\n\n${bodyText}${failedNote}`;
|
|
250
|
+
});
|
|
251
|
+
return {
|
|
252
|
+
content: [{ type: "text", text: sections.join("\n\n") }],
|
|
253
|
+
details: {
|
|
254
|
+
tasks: outcomes.map((o) => ({
|
|
255
|
+
agent: o.agent,
|
|
256
|
+
task: o.task,
|
|
257
|
+
exitCode: o.result.exitCode,
|
|
258
|
+
usage: o.result.usage,
|
|
259
|
+
toolCalls: o.result.toolCalls,
|
|
260
|
+
})),
|
|
261
|
+
},
|
|
262
|
+
...(allFailed ? { isError: true } : {}),
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/** Wire the subagent tool and the /agents listing command. */
|
|
268
|
+
export function registerSubagents(pi, deps = {}) {
|
|
269
|
+
const discover = deps.discover ?? discoverSubagents;
|
|
270
|
+
pi.registerTool(makeSubagentTool(deps));
|
|
271
|
+
pi.registerCommand("agents", {
|
|
272
|
+
description: "List the subagents available in this repo (.claude/agents, .pi/agents).",
|
|
273
|
+
handler: async (_args, ctx) => {
|
|
274
|
+
if (!ctx.hasUI)
|
|
275
|
+
return;
|
|
276
|
+
const agents = discover({ cwd: ctx.cwd ?? process.cwd(), homeDir: deps.homeDir });
|
|
277
|
+
ctx.ui.notify(`Available subagents:\n${formatAgentList(agents)}`, "info");
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=subagents.js.map
|