@wrongstack/core 0.1.0

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.
@@ -0,0 +1,4549 @@
1
+ import * as fs4 from 'fs';
2
+ import * as path2 from 'path';
3
+ import * as fsp from 'fs/promises';
4
+ import { randomBytes, createCipheriv, createDecipheriv, randomUUID } from 'crypto';
5
+ import * as os from 'os';
6
+ import { EventEmitter } from 'events';
7
+
8
+ // src/defaults/logger.ts
9
+
10
+ // src/utils/color.ts
11
+ var isColorTty = () => {
12
+ if (process.env.NO_COLOR) return false;
13
+ if (process.env.FORCE_COLOR) return true;
14
+ return Boolean(process.stdout?.isTTY);
15
+ };
16
+ var COLOR = isColorTty();
17
+ var wrap = (open3, close) => (s) => COLOR ? `\x1B[${open3}m${s}\x1B[${close}m` : s;
18
+ var color = {
19
+ reset: wrap("0", "0"),
20
+ bold: wrap("1", "22"),
21
+ dim: wrap("2", "22"),
22
+ italic: wrap("3", "23"),
23
+ underline: wrap("4", "24"),
24
+ red: wrap("31", "39"),
25
+ green: wrap("32", "39"),
26
+ yellow: wrap("33", "39"),
27
+ blue: wrap("34", "39"),
28
+ magenta: wrap("35", "39"),
29
+ cyan: wrap("36", "39"),
30
+ gray: wrap("90", "39"),
31
+ amber: wrap("38;5;214", "39"),
32
+ pink: wrap("38;5;205", "39"),
33
+ bgRed: wrap("41", "49"),
34
+ bgGreen: wrap("42", "49")
35
+ };
36
+
37
+ // src/defaults/logger.ts
38
+ var LEVEL_RANK = {
39
+ error: 0,
40
+ warn: 1,
41
+ info: 2,
42
+ debug: 3,
43
+ trace: 4
44
+ };
45
+ var COLORS = {
46
+ error: color.red,
47
+ warn: color.yellow,
48
+ info: color.cyan,
49
+ debug: color.gray,
50
+ trace: color.dim
51
+ };
52
+ var DefaultLogger = class _DefaultLogger {
53
+ level;
54
+ file;
55
+ bindings;
56
+ pretty;
57
+ constructor(opts = {}) {
58
+ this.level = opts.level ?? process.env.WRONGSTACK_LOG_LEVEL ?? "info";
59
+ this.file = opts.file;
60
+ this.bindings = opts.bindings ?? {};
61
+ this.pretty = opts.pretty ?? true;
62
+ if (this.file) {
63
+ try {
64
+ fs4.mkdirSync(path2.dirname(this.file), { recursive: true });
65
+ } catch {
66
+ }
67
+ }
68
+ }
69
+ error(msg, ctx) {
70
+ this.log("error", msg, ctx);
71
+ }
72
+ warn(msg, ctx) {
73
+ this.log("warn", msg, ctx);
74
+ }
75
+ info(msg, ctx) {
76
+ this.log("info", msg, ctx);
77
+ }
78
+ debug(msg, ctx) {
79
+ this.log("debug", msg, ctx);
80
+ }
81
+ trace(msg, ctx) {
82
+ this.log("trace", msg, ctx);
83
+ }
84
+ child(bindings) {
85
+ return new _DefaultLogger({
86
+ level: this.level,
87
+ file: this.file,
88
+ pretty: this.pretty,
89
+ bindings: { ...this.bindings, ...bindings }
90
+ });
91
+ }
92
+ log(level, msg, ctx) {
93
+ const r = LEVEL_RANK[level];
94
+ const allowed = LEVEL_RANK[this.level];
95
+ if (r > allowed) return;
96
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
97
+ const entry = { ts, level, msg, ...this.bindings };
98
+ if (ctx !== void 0) {
99
+ entry.ctx = ctx instanceof Error ? { message: ctx.message, stack: ctx.stack } : ctx;
100
+ }
101
+ if (this.file) {
102
+ try {
103
+ fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
104
+ `);
105
+ } catch {
106
+ }
107
+ }
108
+ if (r <= LEVEL_RANK.warn || this.level === "debug" || this.level === "trace") {
109
+ const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
110
+ if (ctx !== void 0) {
111
+ process.stderr.write(`${head} ${formatCtx(ctx)}
112
+ `);
113
+ } else {
114
+ process.stderr.write(`${head}
115
+ `);
116
+ }
117
+ }
118
+ }
119
+ };
120
+ function formatCtx(ctx) {
121
+ if (ctx instanceof Error) return color.dim(ctx.message);
122
+ if (typeof ctx === "string") return color.dim(ctx);
123
+ try {
124
+ return color.dim(JSON.stringify(ctx));
125
+ } catch {
126
+ return color.dim(String(ctx));
127
+ }
128
+ }
129
+ var PROJECT_MARKERS = [
130
+ ".git",
131
+ "package.json",
132
+ "pnpm-workspace.yaml",
133
+ "go.mod",
134
+ "Cargo.toml",
135
+ "pyproject.toml",
136
+ ".wrongstack"
137
+ ];
138
+ var DefaultPathResolver = class {
139
+ projectRoot;
140
+ cwd;
141
+ constructor(cwd = process.cwd()) {
142
+ this.cwd = path2.resolve(cwd);
143
+ this.projectRoot = this.detectProjectRoot(this.cwd);
144
+ }
145
+ detectProjectRoot(start) {
146
+ let dir = path2.resolve(start);
147
+ const root = path2.parse(dir).root;
148
+ while (dir !== root) {
149
+ for (const marker of PROJECT_MARKERS) {
150
+ try {
151
+ fs4.accessSync(path2.join(dir, marker));
152
+ return dir;
153
+ } catch {
154
+ }
155
+ }
156
+ const parent = path2.dirname(dir);
157
+ if (parent === dir) break;
158
+ dir = parent;
159
+ }
160
+ return path2.resolve(start);
161
+ }
162
+ resolve(input) {
163
+ const abs = path2.isAbsolute(input) ? input : path2.resolve(this.cwd, input);
164
+ let real;
165
+ try {
166
+ real = fs4.realpathSync(abs);
167
+ } catch {
168
+ real = path2.normalize(abs);
169
+ }
170
+ return real;
171
+ }
172
+ isInsideRoot(absPath) {
173
+ const normalized = path2.normalize(absPath);
174
+ const root = path2.normalize(this.projectRoot);
175
+ if (normalized === root) return true;
176
+ const rel = path2.relative(root, normalized);
177
+ return !rel.startsWith("..") && !path2.isAbsolute(rel);
178
+ }
179
+ ensureInsideRoot(absPath) {
180
+ const resolved = this.resolve(absPath);
181
+ if (!this.isInsideRoot(resolved)) {
182
+ throw new Error(
183
+ `Path "${absPath}" resolves outside the project root (${this.projectRoot})`
184
+ );
185
+ }
186
+ return resolved;
187
+ }
188
+ };
189
+
190
+ // src/defaults/secret-scrubber.ts
191
+ var PATTERNS = [
192
+ // Anchored at the start where possible so partial matches inside larger
193
+ // strings don't trigger false positives.
194
+ { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
195
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
196
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
197
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
198
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
199
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
200
+ { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
201
+ { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
202
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
203
+ {
204
+ type: "jwt",
205
+ // Anchored: look for literal "eyJ" which is unambiguous for JWT header
206
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
207
+ },
208
+ {
209
+ type: "private_key",
210
+ // Anchored: start must be BEGIN, end must be END with no extra dashes after END
211
+ regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
212
+ },
213
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
214
+ { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
215
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
216
+ { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
217
+ { type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
218
+ {
219
+ type: "high_entropy_env",
220
+ // Value-side word boundary + length gate to avoid matching short random strings
221
+ regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
222
+ }
223
+ ];
224
+ var DefaultSecretScrubber = class {
225
+ scrub(text) {
226
+ if (!text) return text;
227
+ let out = text;
228
+ for (const p of PATTERNS) {
229
+ out = out.replace(p.regex, (_match, group1, group2) => {
230
+ if (p.type === "high_entropy_env" && group1 && group2) {
231
+ return `${group1}=[REDACTED:${p.type}]`;
232
+ }
233
+ return `[REDACTED:${p.type}]`;
234
+ });
235
+ }
236
+ return out;
237
+ }
238
+ scrubObject(obj) {
239
+ const seen = /* @__PURE__ */ new WeakSet();
240
+ const visit = (v) => {
241
+ if (typeof v === "string") return this.scrub(v);
242
+ if (v === null || typeof v !== "object") return v;
243
+ if (seen.has(v)) return v;
244
+ seen.add(v);
245
+ if (Array.isArray(v)) return v.map(visit);
246
+ const out = {};
247
+ for (const [k, val] of Object.entries(v)) {
248
+ out[k] = visit(val);
249
+ }
250
+ return out;
251
+ };
252
+ return visit(obj);
253
+ }
254
+ };
255
+
256
+ // src/types/provider.ts
257
+ var ProviderError = class extends Error {
258
+ status;
259
+ retryable;
260
+ providerId;
261
+ body;
262
+ cause;
263
+ constructor(message, status, retryable, providerId, opts = {}) {
264
+ super(message);
265
+ this.name = "ProviderError";
266
+ this.status = status;
267
+ this.retryable = retryable;
268
+ this.providerId = providerId;
269
+ this.body = opts.body;
270
+ this.cause = opts.cause;
271
+ }
272
+ /**
273
+ * Render a one-line, user-facing description. Designed for the CLI/TUI
274
+ * status line and the agent's retry warning. Avoids dumping raw JSON
275
+ * (which is what users see today when a 529 lands and the log message
276
+ * includes the full `{"type":"error",...}` body).
277
+ *
278
+ * Examples:
279
+ * "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
280
+ * "openai rate limited (429): Retry after 12s"
281
+ * "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
282
+ * "groq HTTP 500 (server error)"
283
+ */
284
+ describe() {
285
+ const kind = describeStatus(this.status, this.body?.type);
286
+ const head = `${this.providerId} ${kind}`;
287
+ const detail = this.body?.message?.trim();
288
+ const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
289
+ if (detail && detail.length > 0) {
290
+ return `${head}: ${truncate(detail, 240)}${reqId}`;
291
+ }
292
+ return `${head}${reqId}`;
293
+ }
294
+ };
295
+ function describeStatus(status, type) {
296
+ if (status === 0) return "network error";
297
+ if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
298
+ if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
299
+ if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
300
+ if (type === "permission_error" || status === 403) return `forbidden (${status})`;
301
+ if (type === "not_found_error" || status === 404) return `not found (${status})`;
302
+ if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
303
+ if (status === 408) return `timeout (${status})`;
304
+ if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
305
+ if (type) return `${type} (${status})`;
306
+ return `HTTP ${status}`;
307
+ }
308
+ function truncate(s, n) {
309
+ return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
310
+ }
311
+
312
+ // src/defaults/retry-policy.ts
313
+ var DefaultRetryPolicy = class {
314
+ shouldRetry(err, attempt) {
315
+ if (err instanceof ProviderError) {
316
+ if (!err.retryable) return false;
317
+ return attempt < this.maxAttempts(err);
318
+ }
319
+ const msg = err.message ?? "";
320
+ const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
321
+ if (isNetwork) return attempt < 2;
322
+ return false;
323
+ }
324
+ maxAttempts(err) {
325
+ if (err instanceof ProviderError) {
326
+ if (err.status === 429) return 5;
327
+ if (err.status === 529) return 3;
328
+ if (err.status >= 500) return 3;
329
+ return 0;
330
+ }
331
+ return 2;
332
+ }
333
+ delayMs(attempt) {
334
+ const base = 1e3;
335
+ const exp = base * 2 ** attempt;
336
+ const jitter = Math.random() * base;
337
+ return Math.min(3e4, exp + jitter);
338
+ }
339
+ };
340
+
341
+ // src/defaults/error-handler.ts
342
+ function buildRecoveryStrategies(opts) {
343
+ return [
344
+ {
345
+ label: "context_overflow_reduce",
346
+ compactor: opts?.compactor,
347
+ async attempt(err, ctx) {
348
+ if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
349
+ if (this.compactor) {
350
+ try {
351
+ const report = await this.compactor.compact(ctx, { aggressive: true });
352
+ if (report.after < report.before) {
353
+ return {
354
+ content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
355
+ stopReason: "end_turn",
356
+ usage: { input: 0, output: 0 },
357
+ model: ctx.model
358
+ };
359
+ }
360
+ } catch {
361
+ }
362
+ }
363
+ return null;
364
+ }
365
+ return null;
366
+ }
367
+ },
368
+ {
369
+ label: "rate_limit_backoff",
370
+ async attempt(err, ctx) {
371
+ if (err instanceof ProviderError && err.status === 429) {
372
+ const delayMs = err.body?.retryAfterMs ?? 5e3;
373
+ const delay = Math.max(1e3, Math.min(delayMs, 6e4));
374
+ await new Promise((r) => setTimeout(r, delay));
375
+ return {
376
+ content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
377
+ stopReason: "end_turn",
378
+ usage: { input: 0, output: 0 },
379
+ model: ctx.model
380
+ };
381
+ }
382
+ return null;
383
+ }
384
+ },
385
+ {
386
+ label: "downgrade_model",
387
+ async attempt(err, ctx) {
388
+ if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
389
+ return null;
390
+ }
391
+ return null;
392
+ }
393
+ }
394
+ ];
395
+ }
396
+ var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
397
+ var DefaultErrorHandler = class {
398
+ strategies;
399
+ constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
400
+ this.strategies = strategies;
401
+ }
402
+ classify(err) {
403
+ if (err instanceof DOMException && err.name === "AbortError") {
404
+ return { kind: "abort", retryable: false };
405
+ }
406
+ if (err instanceof Error && err.name === "AbortError") {
407
+ return { kind: "abort", retryable: false };
408
+ }
409
+ if (err instanceof ProviderError) {
410
+ if (err.status === 429) return { kind: "rate_limit", retryable: true };
411
+ if (err.status === 529) return { kind: "overloaded", retryable: true };
412
+ if (err.status >= 500) return { kind: "server", retryable: true };
413
+ if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
414
+ return { kind: "context_overflow", retryable: false };
415
+ }
416
+ if (err.status >= 400) return { kind: "client", retryable: false };
417
+ }
418
+ if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
419
+ return { kind: "network", retryable: true };
420
+ }
421
+ return { kind: "unknown", retryable: false };
422
+ }
423
+ async recover(err, ctx) {
424
+ for (const strategy of this.strategies) {
425
+ const result = await strategy.attempt(err, ctx);
426
+ if (result !== null) return result;
427
+ }
428
+ return null;
429
+ }
430
+ };
431
+
432
+ // src/defaults/token-counter.ts
433
+ var DefaultTokenCounter = class {
434
+ input = 0;
435
+ output = 0;
436
+ cacheRead = 0;
437
+ cacheWrite = 0;
438
+ costInput = 0;
439
+ costOutput = 0;
440
+ registry;
441
+ providerId;
442
+ events;
443
+ priceCache = /* @__PURE__ */ new Map();
444
+ constructor(opts = {}) {
445
+ this.registry = opts.registry;
446
+ this.providerId = opts.providerId;
447
+ this.events = opts.events;
448
+ }
449
+ account(usage, model) {
450
+ this.input += usage.input;
451
+ this.output += usage.output;
452
+ this.cacheRead += usage.cacheRead ?? 0;
453
+ this.cacheWrite += usage.cacheWrite ?? 0;
454
+ const price = model ? this.priceCache.get(model) : void 0;
455
+ if (price) {
456
+ this.applyPrice(usage, price);
457
+ } else if (this.registry && this.providerId && model) {
458
+ void this.registry.getModel(this.providerId, model).then((m) => {
459
+ if (m) {
460
+ const p = priceFromModel(m);
461
+ this.priceCache.set(model, p);
462
+ this.applyPrice(usage, p);
463
+ }
464
+ }).catch(() => {
465
+ this.events?.emit("token.cost_estimate_unavailable", { model: model ?? "<unknown>" });
466
+ return void 0;
467
+ });
468
+ }
469
+ }
470
+ /** Synchronous variant for code paths that have already resolved the model. */
471
+ accountWithModel(usage, resolved) {
472
+ this.input += usage.input;
473
+ this.output += usage.output;
474
+ this.cacheRead += usage.cacheRead ?? 0;
475
+ this.cacheWrite += usage.cacheWrite ?? 0;
476
+ const price = priceFromModel(resolved);
477
+ this.priceCache.set(resolved.modelId, price);
478
+ this.applyPrice(usage, price);
479
+ }
480
+ total() {
481
+ return {
482
+ input: this.input,
483
+ output: this.output,
484
+ cacheRead: this.cacheRead,
485
+ cacheWrite: this.cacheWrite
486
+ };
487
+ }
488
+ estimateCost() {
489
+ return {
490
+ input: round4(this.costInput),
491
+ output: round4(this.costOutput),
492
+ total: round4(this.costInput + this.costOutput),
493
+ currency: "USD"
494
+ };
495
+ }
496
+ cacheStats() {
497
+ const denom = this.cacheRead + this.input;
498
+ return {
499
+ readTokens: this.cacheRead,
500
+ writeTokens: this.cacheWrite,
501
+ hitRatio: denom === 0 ? 0 : this.cacheRead / denom
502
+ };
503
+ }
504
+ /** Invalidate cached prices so the next account() call fetches fresh data. */
505
+ invalidateCache() {
506
+ this.priceCache.clear();
507
+ }
508
+ reset() {
509
+ this.input = 0;
510
+ this.output = 0;
511
+ this.cacheRead = 0;
512
+ this.cacheWrite = 0;
513
+ this.costInput = 0;
514
+ this.costOutput = 0;
515
+ }
516
+ applyPrice(usage, price) {
517
+ if (price.input) this.costInput += usage.input / 1e6 * price.input;
518
+ if (price.output) this.costOutput += usage.output / 1e6 * price.output;
519
+ if (usage.cacheRead && price.cacheRead) {
520
+ this.costInput += usage.cacheRead / 1e6 * price.cacheRead;
521
+ }
522
+ if (usage.cacheWrite && price.cacheWrite) {
523
+ this.costInput += usage.cacheWrite / 1e6 * price.cacheWrite;
524
+ }
525
+ }
526
+ };
527
+ function priceFromModel(m) {
528
+ return {
529
+ input: m.cost?.input,
530
+ output: m.cost?.output,
531
+ cacheRead: m.cost?.cache_read,
532
+ cacheWrite: m.cost?.cache_write
533
+ };
534
+ }
535
+ function round4(n) {
536
+ return Math.round(n * 1e4) / 1e4;
537
+ }
538
+ async function atomicWrite(targetPath, content, opts = {}) {
539
+ const dir = path2.dirname(targetPath);
540
+ await fsp.mkdir(dir, { recursive: true });
541
+ const tmp = path2.join(dir, `.${path2.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
542
+ try {
543
+ if (typeof content === "string") {
544
+ await fsp.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
545
+ } else {
546
+ await fsp.writeFile(tmp, content, { flag: "wx" });
547
+ }
548
+ try {
549
+ const fh = await fsp.open(tmp, "r+");
550
+ await fh.sync();
551
+ await fh.close();
552
+ } catch {
553
+ }
554
+ let mode;
555
+ try {
556
+ const stat4 = await fsp.stat(targetPath);
557
+ mode = stat4.mode & 511;
558
+ } catch {
559
+ }
560
+ if (mode !== void 0) {
561
+ await fsp.chmod(tmp, mode);
562
+ }
563
+ await fsp.rename(tmp, targetPath);
564
+ } catch (err) {
565
+ try {
566
+ await fsp.unlink(tmp);
567
+ } catch {
568
+ }
569
+ throw err;
570
+ }
571
+ }
572
+ async function ensureDir(dir) {
573
+ await fsp.mkdir(dir, { recursive: true });
574
+ }
575
+
576
+ // src/defaults/session-store.ts
577
+ var DefaultSessionStore = class {
578
+ dir;
579
+ events;
580
+ constructor(opts) {
581
+ this.dir = opts.dir;
582
+ this.events = opts.events;
583
+ }
584
+ async create(meta) {
585
+ await ensureDir(this.dir);
586
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
587
+ const id = meta.id ?? `${startedAt.replace(/[:.]/g, "-")}-${randomBytes(2).toString("hex")}`;
588
+ const file = path2.join(this.dir, `${id}.jsonl`);
589
+ let handle;
590
+ try {
591
+ handle = await fsp.open(file, "a", 384);
592
+ } catch (err) {
593
+ throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`);
594
+ }
595
+ try {
596
+ return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
597
+ } catch (err) {
598
+ await handle.close().catch(() => {
599
+ });
600
+ throw err;
601
+ }
602
+ }
603
+ async resume(id) {
604
+ const data = await this.load(id);
605
+ const file = path2.join(this.dir, `${id}.jsonl`);
606
+ let handle;
607
+ try {
608
+ handle = await fsp.open(file, "a", 384);
609
+ } catch (err) {
610
+ throw new Error(
611
+ `Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`
612
+ );
613
+ }
614
+ const writer = new FileSessionWriter(
615
+ id,
616
+ handle,
617
+ (/* @__PURE__ */ new Date()).toISOString(),
618
+ {
619
+ id,
620
+ model: data.metadata.model,
621
+ provider: data.metadata.provider
622
+ },
623
+ { resumed: true, dir: this.dir, filePath: file }
624
+ );
625
+ return { writer, data };
626
+ }
627
+ async load(id) {
628
+ const file = path2.join(this.dir, `${id}.jsonl`);
629
+ const raw = await fsp.readFile(file, "utf8");
630
+ const lines = raw.split("\n").filter((l) => l.trim());
631
+ const events = [];
632
+ for (const line of lines) {
633
+ try {
634
+ events.push(JSON.parse(line));
635
+ } catch {
636
+ }
637
+ }
638
+ const meta = this.metaFromEvents(id, events);
639
+ const { messages, usage } = this.replay(events, id);
640
+ return { metadata: meta, events, messages, usage };
641
+ }
642
+ async list(limit = 20) {
643
+ try {
644
+ await ensureDir(this.dir);
645
+ const files = await fsp.readdir(this.dir);
646
+ const ids = files.filter((f) => f.endsWith(".jsonl")).map((f) => f.replace(/\.jsonl$/, ""));
647
+ const sessions = await Promise.all(
648
+ ids.map((id) => this.summaryFor(id).catch(() => null))
649
+ );
650
+ const out = sessions.filter((s) => s !== null);
651
+ out.sort((a, b) => a.startedAt < b.startedAt ? 1 : -1);
652
+ return out.slice(0, limit);
653
+ } catch {
654
+ return [];
655
+ }
656
+ }
657
+ async summaryFor(id) {
658
+ const manifest = path2.join(this.dir, `${id}.summary.json`);
659
+ try {
660
+ const raw = await fsp.readFile(manifest, "utf8");
661
+ return JSON.parse(raw);
662
+ } catch {
663
+ const full = path2.join(this.dir, `${id}.jsonl`);
664
+ const stat4 = await fsp.stat(full);
665
+ const summary = await this.summarize(id, stat4.mtime.toISOString());
666
+ await fsp.writeFile(manifest, JSON.stringify(summary), { mode: 384 }).catch(() => void 0);
667
+ return summary;
668
+ }
669
+ }
670
+ async delete(id) {
671
+ await fsp.unlink(path2.join(this.dir, `${id}.jsonl`));
672
+ await fsp.unlink(path2.join(this.dir, `${id}.summary.json`)).catch(() => void 0);
673
+ }
674
+ async summarize(id, mtime) {
675
+ try {
676
+ const data = await this.load(id);
677
+ const firstUser = data.events.find((e) => e.type === "user_input");
678
+ const title = firstUser && firstUser.type === "user_input" ? userInputTitle(firstUser.content) : "(empty session)";
679
+ return {
680
+ id,
681
+ title,
682
+ startedAt: data.metadata.startedAt,
683
+ model: data.metadata.model ?? "unknown",
684
+ provider: data.metadata.provider ?? "unknown",
685
+ tokenTotal: data.usage.input + data.usage.output
686
+ };
687
+ } catch {
688
+ return {
689
+ id,
690
+ title: "(damaged)",
691
+ startedAt: mtime,
692
+ model: "unknown",
693
+ provider: "unknown",
694
+ tokenTotal: 0
695
+ };
696
+ }
697
+ }
698
+ metaFromEvents(id, events) {
699
+ const start = events.find((e) => e.type === "session_start");
700
+ const end = events.find((e) => e.type === "session_end");
701
+ return {
702
+ id,
703
+ startedAt: start?.ts ?? (/* @__PURE__ */ new Date(0)).toISOString(),
704
+ endedAt: end?.ts,
705
+ model: start?.model,
706
+ provider: start?.provider
707
+ };
708
+ }
709
+ replay(events, sessionId = "unknown") {
710
+ const messages = [];
711
+ let usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
712
+ const openToolUses = /* @__PURE__ */ new Set();
713
+ for (const e of events) {
714
+ if (e.type === "user_input") {
715
+ openToolUses.clear();
716
+ messages.push({ role: "user", content: e.content });
717
+ } else if (e.type === "llm_response") {
718
+ messages.push({ role: "assistant", content: e.content });
719
+ for (const b of e.content) {
720
+ if (b.type === "tool_use") openToolUses.add(b.id);
721
+ }
722
+ usage = {
723
+ input: usage.input + (e.usage.input ?? 0),
724
+ output: usage.output + (e.usage.output ?? 0),
725
+ cacheRead: (usage.cacheRead ?? 0) + (e.usage.cacheRead ?? 0),
726
+ cacheWrite: (usage.cacheWrite ?? 0) + (e.usage.cacheWrite ?? 0)
727
+ };
728
+ } else if (e.type === "tool_result") {
729
+ if (!openToolUses.has(e.id)) {
730
+ this.events?.emit("session.damaged", {
731
+ sessionId,
732
+ detail: `Orphan tool_result "${e.id}" has no matching tool_use`
733
+ });
734
+ continue;
735
+ }
736
+ openToolUses.delete(e.id);
737
+ const content = [
738
+ {
739
+ type: "tool_result",
740
+ tool_use_id: e.id,
741
+ content: typeof e.content === "string" ? e.content : JSON.stringify(e.content),
742
+ is_error: e.isError
743
+ }
744
+ ];
745
+ const last = messages[messages.length - 1];
746
+ if (last && last.role === "user") {
747
+ if (Array.isArray(last.content)) {
748
+ last.content.push(...content);
749
+ } else if (typeof last.content === "string") {
750
+ last.content = [{ type: "text", text: last.content }, ...content];
751
+ } else {
752
+ messages.push({ role: "user", content });
753
+ }
754
+ } else {
755
+ messages.push({ role: "user", content });
756
+ }
757
+ }
758
+ }
759
+ if (openToolUses.size > 0) {
760
+ throw new Error(
761
+ `Session damaged: ${openToolUses.size} tool_use blocks without matching results`
762
+ );
763
+ }
764
+ return { messages, usage };
765
+ }
766
+ };
767
+ var FileSessionWriter = class {
768
+ constructor(id, handle, startedAt, meta, opts = {}) {
769
+ this.id = id;
770
+ this.handle = handle;
771
+ this.startedAt = startedAt;
772
+ this.meta = meta;
773
+ this.resumed = opts.resumed ?? false;
774
+ this.manifestFile = opts.dir ? path2.join(opts.dir, `${id}.summary.json`) : "";
775
+ this.filePath = opts.filePath ?? "";
776
+ this.summary = {
777
+ id,
778
+ title: "(empty session)",
779
+ startedAt,
780
+ model: meta.model ?? "unknown",
781
+ provider: meta.provider ?? "unknown",
782
+ tokenTotal: 0
783
+ };
784
+ }
785
+ id;
786
+ handle;
787
+ startedAt;
788
+ meta;
789
+ closed = false;
790
+ manifestFile;
791
+ summary;
792
+ tokenIn = 0;
793
+ tokenOut = 0;
794
+ filePath;
795
+ initDone = false;
796
+ resumed;
797
+ async writeSessionStart() {
798
+ if (this.initDone || this.closed) return;
799
+ this.initDone = true;
800
+ const record = `${JSON.stringify({
801
+ type: this.resumed ? "session_resumed" : "session_start",
802
+ ts: this.startedAt,
803
+ id: this.id,
804
+ model: this.meta.model ?? "unknown",
805
+ provider: this.meta.provider ?? "unknown"
806
+ })}
807
+ `;
808
+ try {
809
+ if (this.filePath) {
810
+ await fsp.writeFile(this.filePath, record, { flag: "a", mode: 384 });
811
+ }
812
+ } catch {
813
+ }
814
+ }
815
+ async append(event) {
816
+ if (this.closed) return;
817
+ if (!this.initDone) {
818
+ await this.writeSessionStart();
819
+ }
820
+ this.observeForSummary(event);
821
+ try {
822
+ await this.handle.appendFile(`${JSON.stringify(event)}
823
+ `, "utf8");
824
+ } catch (err) {
825
+ console.warn("[session] append failed:", err instanceof Error ? err.message : String(err));
826
+ }
827
+ }
828
+ /**
829
+ * Watch events as they're appended and keep the summary state hot, so
830
+ * `close()` can flush a `<id>.summary.json` manifest without re-reading
831
+ * the JSONL. `list()` reads only manifests, turning a per-session full
832
+ * parse into a single stat+read.
833
+ */
834
+ observeForSummary(event) {
835
+ if (event.type === "user_input" && this.summary.title === "(empty session)") {
836
+ this.summary = { ...this.summary, title: userInputTitle(event.content) };
837
+ } else if (event.type === "llm_response") {
838
+ this.tokenIn += event.usage.input;
839
+ this.tokenOut += event.usage.output;
840
+ this.summary = { ...this.summary, tokenTotal: this.tokenIn + this.tokenOut };
841
+ } else if (event.type === "session_end") {
842
+ const total = event.usage.input + event.usage.output;
843
+ if (total > 0) this.summary = { ...this.summary, tokenTotal: total };
844
+ }
845
+ }
846
+ async close() {
847
+ if (this.closed) return;
848
+ this.closed = true;
849
+ if (this.manifestFile) {
850
+ try {
851
+ await fsp.writeFile(this.manifestFile, JSON.stringify(this.summary), { mode: 384 });
852
+ } catch {
853
+ }
854
+ }
855
+ try {
856
+ await this.handle.close();
857
+ } catch {
858
+ }
859
+ }
860
+ };
861
+ function userInputTitle(content) {
862
+ if (typeof content === "string") return content.slice(0, 60);
863
+ const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
864
+ return (text || "(non-text input)").slice(0, 60);
865
+ }
866
+ var LOCK_FILE = "active.json";
867
+ var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
868
+ var RecoveryLock = class {
869
+ file;
870
+ pid;
871
+ hostname;
872
+ maxAgeMs;
873
+ sessionStore;
874
+ probe;
875
+ constructor(opts) {
876
+ this.file = path2.join(opts.dir, LOCK_FILE);
877
+ this.pid = opts.pid ?? process.pid;
878
+ this.hostname = opts.hostname ?? os.hostname();
879
+ this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
880
+ this.sessionStore = opts.sessionStore;
881
+ this.probe = opts.isPidAlive ?? defaultIsPidAlive;
882
+ }
883
+ /**
884
+ * Examine the lockfile and decide whether it represents an abandoned
885
+ * session. Returns `null` if the file is missing, points to a live
886
+ * instance, references a clean-closed session, is too old, or is
887
+ * malformed. Otherwise returns enough detail to prompt the user.
888
+ *
889
+ * Important: this is a read-only check. We never delete an active
890
+ * lock from here — if another wstack instance is alive, the caller
891
+ * should bail or run with a fresh session instead.
892
+ */
893
+ async checkAbandoned() {
894
+ const lock = await this.readLock();
895
+ if (!lock) return null;
896
+ const ageMs = Date.now() - new Date(lock.startedAt).getTime();
897
+ if (Number.isNaN(ageMs) || ageMs < 0) {
898
+ return null;
899
+ }
900
+ if (ageMs > this.maxAgeMs) return null;
901
+ if (lock.hostname === this.hostname && this.probe(lock.pid)) {
902
+ return null;
903
+ }
904
+ let messageCount = 0;
905
+ if (this.sessionStore) {
906
+ try {
907
+ const data = await this.sessionStore.load(lock.sessionId);
908
+ const closed = data.events.some((e) => e.type === "session_end");
909
+ if (closed) return null;
910
+ messageCount = data.messages.length;
911
+ } catch {
912
+ return null;
913
+ }
914
+ }
915
+ return {
916
+ sessionId: lock.sessionId,
917
+ pid: lock.pid,
918
+ startedAt: lock.startedAt,
919
+ ageMs,
920
+ messageCount
921
+ };
922
+ }
923
+ /**
924
+ * Claim the lock for the given session. Overwrites any existing lock
925
+ * — the caller should have already handled abandonment (via
926
+ * `checkAbandoned`) before calling this.
927
+ */
928
+ async write(sessionId) {
929
+ await ensureDir(path2.dirname(this.file));
930
+ const lock = {
931
+ v: 1,
932
+ sessionId,
933
+ pid: this.pid,
934
+ hostname: this.hostname,
935
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
936
+ };
937
+ const tmp = `${this.file}.tmp`;
938
+ await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
939
+ await fsp.rename(tmp, this.file);
940
+ }
941
+ /**
942
+ * Release the lock. Idempotent — silently succeeds if the file is
943
+ * already gone (e.g. someone else cleared it, or the directory was
944
+ * wiped).
945
+ */
946
+ async clear() {
947
+ try {
948
+ await fsp.unlink(this.file);
949
+ } catch (err) {
950
+ const code = err.code;
951
+ if (code === "ENOENT") return;
952
+ throw err;
953
+ }
954
+ }
955
+ async readLock() {
956
+ let raw;
957
+ try {
958
+ raw = await fsp.readFile(this.file, "utf8");
959
+ } catch (err) {
960
+ const code = err.code;
961
+ if (code === "ENOENT") return null;
962
+ return null;
963
+ }
964
+ try {
965
+ const parsed = JSON.parse(raw);
966
+ if (!isLockFile(parsed)) return null;
967
+ return parsed;
968
+ } catch {
969
+ return null;
970
+ }
971
+ }
972
+ };
973
+ function isLockFile(v) {
974
+ if (typeof v !== "object" || v === null) return false;
975
+ const o = v;
976
+ return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
977
+ }
978
+ function defaultIsPidAlive(pid) {
979
+ if (!Number.isInteger(pid) || pid <= 0) return false;
980
+ try {
981
+ process.kill(pid, 0);
982
+ return true;
983
+ } catch (err) {
984
+ const code = err.code;
985
+ if (code === "EPERM") return true;
986
+ return false;
987
+ }
988
+ }
989
+ var QueueStore = class {
990
+ file;
991
+ constructor(opts) {
992
+ this.file = path2.join(opts.dir, "queue.json");
993
+ }
994
+ async write(items) {
995
+ if (items.length === 0) {
996
+ await this.clear();
997
+ return;
998
+ }
999
+ await atomicWrite(this.file, JSON.stringify(items), { mode: 384 });
1000
+ }
1001
+ async read() {
1002
+ let raw;
1003
+ try {
1004
+ raw = await fsp.readFile(this.file, "utf8");
1005
+ } catch (err) {
1006
+ const code = err.code;
1007
+ if (code === "ENOENT") return [];
1008
+ return [];
1009
+ }
1010
+ let parsed;
1011
+ try {
1012
+ parsed = JSON.parse(raw);
1013
+ } catch {
1014
+ return [];
1015
+ }
1016
+ if (!Array.isArray(parsed)) return [];
1017
+ const out = [];
1018
+ for (const v of parsed) {
1019
+ if (isPersistedQueueItem(v)) out.push(v);
1020
+ }
1021
+ return out;
1022
+ }
1023
+ async clear() {
1024
+ try {
1025
+ await fsp.unlink(this.file);
1026
+ } catch (err) {
1027
+ const code = err.code;
1028
+ if (code === "ENOENT") return;
1029
+ console.warn(`QueueStore.clear() failed for ${this.file}: ${err.message}`);
1030
+ }
1031
+ }
1032
+ };
1033
+ function isPersistedQueueItem(v) {
1034
+ if (typeof v !== "object" || v === null) return false;
1035
+ const o = v;
1036
+ return typeof o["displayText"] === "string" && Array.isArray(o["blocks"]);
1037
+ }
1038
+ var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
1039
+ var PLACEHOLDER_RE = /\[(pasted|image|file) #(\d+)\]/g;
1040
+ var DefaultAttachmentStore = class {
1041
+ items = /* @__PURE__ */ new Map();
1042
+ refs = [];
1043
+ nextSeq = { text: 0, image: 0, file: 0 };
1044
+ spoolDir;
1045
+ spoolThreshold;
1046
+ constructor(opts = {}) {
1047
+ this.spoolDir = opts.spoolDir;
1048
+ this.spoolThreshold = opts.spoolThresholdBytes ?? DEFAULT_SPOOL_THRESHOLD;
1049
+ }
1050
+ async add(input) {
1051
+ const seq = ++this.nextSeq[input.kind];
1052
+ const id = `${kindPrefix(input.kind)}-${seq}-${randomBytes(3).toString("hex")}`;
1053
+ const bytes = Buffer.byteLength(input.data, input.kind === "image" ? "base64" : "utf8");
1054
+ let spooledPath;
1055
+ let data = input.data;
1056
+ if (this.spoolDir && bytes >= this.spoolThreshold) {
1057
+ await fsp.mkdir(this.spoolDir, { recursive: true });
1058
+ spooledPath = path2.join(this.spoolDir, `${id}.bin`);
1059
+ await fsp.writeFile(spooledPath, input.data, input.kind === "image" ? "base64" : "utf8");
1060
+ data = void 0;
1061
+ }
1062
+ const att = {
1063
+ id,
1064
+ kind: input.kind,
1065
+ meta: input.meta ?? {},
1066
+ data,
1067
+ path: spooledPath,
1068
+ bytes,
1069
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1070
+ };
1071
+ this.items.set(id, att);
1072
+ const ref = { id, kind: input.kind, seq, meta: att.meta };
1073
+ this.refs.push(ref);
1074
+ return ref;
1075
+ }
1076
+ async get(id) {
1077
+ return this.items.get(id);
1078
+ }
1079
+ list() {
1080
+ return [...this.refs];
1081
+ }
1082
+ async expand(text) {
1083
+ const matches = [...text.matchAll(PLACEHOLDER_RE)];
1084
+ if (matches.length === 0) return text ? [{ type: "text", text }] : [];
1085
+ const blocks = [];
1086
+ let lastIndex = 0;
1087
+ for (const m of matches) {
1088
+ const idx = m.index ?? 0;
1089
+ const before = text.slice(lastIndex, idx);
1090
+ if (before) blocks.push({ type: "text", text: before });
1091
+ const kind = prefixToKind(m[1]);
1092
+ const seq = Number(m[2]);
1093
+ const ref = this.refs.find((r) => r.kind === kind && r.seq === seq);
1094
+ const att = ref ? this.items.get(ref.id) : void 0;
1095
+ if (!att) {
1096
+ blocks.push({ type: "text", text: m[0] });
1097
+ } else {
1098
+ blocks.push(await this.toBlock(att));
1099
+ }
1100
+ lastIndex = idx + m[0].length;
1101
+ }
1102
+ const tail = text.slice(lastIndex);
1103
+ if (tail) blocks.push({ type: "text", text: tail });
1104
+ return mergeAdjacentText(blocks);
1105
+ }
1106
+ async clear() {
1107
+ this.items.clear();
1108
+ this.refs.length = 0;
1109
+ this.nextSeq = { text: 0, image: 0, file: 0 };
1110
+ }
1111
+ async toBlock(att) {
1112
+ if (att.kind === "image") {
1113
+ const data = att.data ?? (att.path ? await fsp.readFile(att.path, { encoding: "base64" }) : "");
1114
+ return {
1115
+ type: "image",
1116
+ source: {
1117
+ type: "base64",
1118
+ media_type: att.meta.mediaType ?? "image/png",
1119
+ data
1120
+ }
1121
+ };
1122
+ }
1123
+ const raw = att.data ?? (att.path ? await fsp.readFile(att.path, "utf8") : "");
1124
+ const label = att.meta.filename ? `<file path="${att.meta.filename}">` : "<pasted>";
1125
+ const close = att.meta.filename ? "</file>" : "</pasted>";
1126
+ return { type: "text", text: `${label}
1127
+ ${raw}
1128
+ ${close}` };
1129
+ }
1130
+ };
1131
+ function kindPrefix(kind) {
1132
+ return kind === "text" ? "pasted" : kind;
1133
+ }
1134
+ function prefixToKind(prefix) {
1135
+ if (prefix === "pasted") return "text";
1136
+ if (prefix === "image") return "image";
1137
+ return "file";
1138
+ }
1139
+ function mergeAdjacentText(blocks) {
1140
+ const out = [];
1141
+ for (const b of blocks) {
1142
+ const prev = out[out.length - 1];
1143
+ if (b.type === "text" && prev && prev.type === "text") {
1144
+ prev.text += b.text;
1145
+ } else {
1146
+ out.push(b);
1147
+ }
1148
+ }
1149
+ return out;
1150
+ }
1151
+
1152
+ // src/types/secret-vault.ts
1153
+ var ENCRYPTED_PREFIX = "enc:v1:";
1154
+
1155
+ // src/defaults/secret-vault.ts
1156
+ var KEY_BYTES = 32;
1157
+ var IV_BYTES = 12;
1158
+ var TAG_BYTES = 16;
1159
+ var ALGO = "aes-256-gcm";
1160
+ var DefaultSecretVault = class {
1161
+ keyFile;
1162
+ key;
1163
+ constructor(opts) {
1164
+ this.keyFile = opts.keyFile;
1165
+ }
1166
+ isEncrypted(value) {
1167
+ return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
1168
+ }
1169
+ encrypt(plaintext) {
1170
+ if (this.isEncrypted(plaintext)) return plaintext;
1171
+ const key = this.loadOrCreateKey();
1172
+ const iv = randomBytes(IV_BYTES);
1173
+ const cipher = createCipheriv(ALGO, key, iv);
1174
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
1175
+ const tag = cipher.getAuthTag();
1176
+ return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
1177
+ }
1178
+ decrypt(value) {
1179
+ if (!this.isEncrypted(value)) return value;
1180
+ const rest = value.slice(ENCRYPTED_PREFIX.length);
1181
+ const parts = rest.split(":");
1182
+ if (parts.length !== 3) {
1183
+ throw new Error("SecretVault: malformed encrypted value");
1184
+ }
1185
+ const [ivB64, tagB64, ctB64] = parts;
1186
+ const iv = Buffer.from(ivB64, "base64");
1187
+ const tag = Buffer.from(tagB64, "base64");
1188
+ const ct = Buffer.from(ctB64, "base64");
1189
+ if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
1190
+ if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
1191
+ const key = this.loadOrCreateKey();
1192
+ const decipher = createDecipheriv(ALGO, key, iv);
1193
+ decipher.setAuthTag(tag);
1194
+ const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
1195
+ return pt.toString("utf8");
1196
+ }
1197
+ loadOrCreateKey() {
1198
+ if (this.key) return this.key;
1199
+ try {
1200
+ const buf = fs4.readFileSync(this.keyFile);
1201
+ if (buf.length !== KEY_BYTES) {
1202
+ throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
1203
+ }
1204
+ this.key = buf;
1205
+ return this.key;
1206
+ } catch (err) {
1207
+ if (err.code !== "ENOENT") throw err;
1208
+ }
1209
+ fs4.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
1210
+ const key = randomBytes(KEY_BYTES);
1211
+ try {
1212
+ fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
1213
+ } catch (err) {
1214
+ if (err.code !== "EEXIST") throw err;
1215
+ const buf = fs4.readFileSync(this.keyFile);
1216
+ if (buf.length !== KEY_BYTES) {
1217
+ throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
1218
+ }
1219
+ this.key = buf;
1220
+ return this.key;
1221
+ }
1222
+ this.key = key;
1223
+ return key;
1224
+ }
1225
+ };
1226
+ function decryptConfigSecrets(cfg, vault) {
1227
+ return walk(cfg, vault, (v) => vault.decrypt(v));
1228
+ }
1229
+ function encryptConfigSecrets(cfg, vault) {
1230
+ return walk(cfg, vault, (v) => vault.encrypt(v));
1231
+ }
1232
+ function walk(node, vault, transform) {
1233
+ if (node === null || node === void 0) return node;
1234
+ if (typeof node !== "object") return node;
1235
+ if (Array.isArray(node)) {
1236
+ return node.map((item) => walk(item, vault, transform));
1237
+ }
1238
+ const out = {};
1239
+ for (const [k, v] of Object.entries(node)) {
1240
+ if (typeof v === "string" && isSecretField(k)) {
1241
+ out[k] = transform(v);
1242
+ } else if (typeof v === "object" && v !== null) {
1243
+ out[k] = walk(v, vault, transform);
1244
+ } else {
1245
+ out[k] = v;
1246
+ }
1247
+ }
1248
+ return out;
1249
+ }
1250
+ var SECRET_KEY_PATTERN = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key)/i;
1251
+ var NON_SECRET_OVERRIDES = /* @__PURE__ */ new Set(["publickey", "public_key"]);
1252
+ function isSecretField(name) {
1253
+ const lc = name.toLowerCase();
1254
+ if (NON_SECRET_OVERRIDES.has(lc)) return false;
1255
+ return SECRET_KEY_PATTERN.test(lc);
1256
+ }
1257
+ async function rewriteConfigEncrypted(configPath, vault, patch) {
1258
+ let current = {};
1259
+ try {
1260
+ const raw = await fsp.readFile(configPath, "utf8");
1261
+ current = JSON.parse(raw);
1262
+ } catch {
1263
+ }
1264
+ const merged = deepMerge(current, patch ?? {});
1265
+ const encrypted = encryptConfigSecrets(merged, vault);
1266
+ await fsp.mkdir(path2.dirname(configPath), { recursive: true });
1267
+ await fsp.writeFile(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1268
+ try {
1269
+ await fsp.chmod(configPath, 384);
1270
+ } catch {
1271
+ }
1272
+ }
1273
+ async function migratePlaintextSecrets(configPath, vault) {
1274
+ let raw;
1275
+ try {
1276
+ raw = await fsp.readFile(configPath, "utf8");
1277
+ } catch {
1278
+ return { migrated: 0, file: configPath };
1279
+ }
1280
+ let parsed;
1281
+ try {
1282
+ parsed = JSON.parse(raw);
1283
+ } catch {
1284
+ return { migrated: 0, file: configPath };
1285
+ }
1286
+ const counter = { n: 0 };
1287
+ const migrated = walkCount(parsed, vault, counter);
1288
+ if (counter.n === 0) return { migrated: 0, file: configPath };
1289
+ await fsp.writeFile(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
1290
+ try {
1291
+ await fsp.chmod(configPath, 384);
1292
+ } catch {
1293
+ }
1294
+ return { migrated: counter.n, file: configPath };
1295
+ }
1296
+ function walkCount(node, vault, counter) {
1297
+ if (node === null || node === void 0) return node;
1298
+ if (typeof node !== "object") return node;
1299
+ if (Array.isArray(node)) {
1300
+ return node.map((item) => walkCount(item, vault, counter));
1301
+ }
1302
+ const out = {};
1303
+ for (const [k, v] of Object.entries(node)) {
1304
+ if (typeof v === "string" && isSecretField(k) && !vault.isEncrypted(v) && v.length > 0) {
1305
+ out[k] = vault.encrypt(v);
1306
+ counter.n++;
1307
+ } else if (typeof v === "object" && v !== null) {
1308
+ out[k] = walkCount(v, vault, counter);
1309
+ } else {
1310
+ out[k] = v;
1311
+ }
1312
+ }
1313
+ return out;
1314
+ }
1315
+ function deepMerge(a, b) {
1316
+ const out = { ...a };
1317
+ for (const [k, v] of Object.entries(b)) {
1318
+ const existing = out[k];
1319
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
1320
+ out[k] = deepMerge(existing, v);
1321
+ } else {
1322
+ out[k] = v;
1323
+ }
1324
+ }
1325
+ return out;
1326
+ }
1327
+ var MAX_BYTES_TOTAL = 32e3;
1328
+ var DefaultMemoryStore = class {
1329
+ files;
1330
+ constructor(opts) {
1331
+ this.files = {
1332
+ "project-agents": opts.paths.inProjectAgentsFile,
1333
+ "project-memory": opts.paths.projectMemory,
1334
+ "user-memory": opts.paths.globalMemory
1335
+ };
1336
+ }
1337
+ async readAll() {
1338
+ const parts = [];
1339
+ for (const scope of ["project-agents", "project-memory", "user-memory"]) {
1340
+ const body = await this.read(scope);
1341
+ if (body.trim()) parts.push(`## ${labelOf(scope)}
1342
+
1343
+ ${body.trim()}`);
1344
+ }
1345
+ return parts.join("\n\n");
1346
+ }
1347
+ async read(scope) {
1348
+ try {
1349
+ return await fsp.readFile(this.files[scope], "utf8");
1350
+ } catch {
1351
+ return "";
1352
+ }
1353
+ }
1354
+ async remember(text, scope = "project-memory") {
1355
+ const file = this.files[scope];
1356
+ await ensureDir(path2.dirname(file));
1357
+ let existing = "";
1358
+ try {
1359
+ existing = await fsp.readFile(file, "utf8");
1360
+ } catch {
1361
+ }
1362
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1363
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
1364
+ const entry = `
1365
+ - [${ts}] ${id} ${text.replace(/\n/g, " ")}
1366
+ `;
1367
+ const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
1368
+ ${entry}`;
1369
+ await atomicWrite(file, next);
1370
+ const buf = Buffer.byteLength(next, "utf8");
1371
+ if (buf > MAX_BYTES_TOTAL) {
1372
+ await this.consolidate(scope);
1373
+ }
1374
+ }
1375
+ async forget(query, scope = "project-memory") {
1376
+ const file = this.files[scope];
1377
+ let existing;
1378
+ try {
1379
+ existing = await fsp.readFile(file, "utf8");
1380
+ } catch {
1381
+ return 0;
1382
+ }
1383
+ const needle = query.toLowerCase();
1384
+ const idMatcher = /mem_\d+_\w+/;
1385
+ let removed = 0;
1386
+ const lines = existing.split("\n").filter((line) => {
1387
+ const trimmed = line.trim();
1388
+ if (!trimmed.startsWith("- ")) return true;
1389
+ if (idMatcher.test(query)) {
1390
+ const afterBracket = trimmed.indexOf("] ");
1391
+ if (afterBracket !== -1) {
1392
+ const afterTs = trimmed.slice(afterBracket + 2);
1393
+ const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
1394
+ if (entryIdMatch && entryIdMatch[0] === query) {
1395
+ removed++;
1396
+ return false;
1397
+ }
1398
+ }
1399
+ }
1400
+ if (trimmed.toLowerCase().includes(needle)) {
1401
+ removed++;
1402
+ return false;
1403
+ }
1404
+ return true;
1405
+ });
1406
+ if (removed > 0) {
1407
+ await atomicWrite(file, lines.join("\n"));
1408
+ }
1409
+ return removed;
1410
+ }
1411
+ async consolidate(scope) {
1412
+ const file = this.files[scope];
1413
+ let existing;
1414
+ try {
1415
+ existing = await fsp.readFile(file, "utf8");
1416
+ } catch {
1417
+ return;
1418
+ }
1419
+ const seen = /* @__PURE__ */ new Set();
1420
+ const lines = existing.split("\n").filter((line) => {
1421
+ const trimmed = line.trim();
1422
+ if (!trimmed.startsWith("- ")) return true;
1423
+ const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
1424
+ if (seen.has(norm)) return false;
1425
+ seen.add(norm);
1426
+ return true;
1427
+ });
1428
+ const next = lines.join("\n");
1429
+ try {
1430
+ await atomicWrite(file, next);
1431
+ } catch {
1432
+ return;
1433
+ }
1434
+ const backup = `${file}.bak.${Date.now()}`;
1435
+ try {
1436
+ await fsp.copyFile(file, backup);
1437
+ } catch {
1438
+ }
1439
+ }
1440
+ async clear(scope) {
1441
+ if (scope) {
1442
+ await atomicWrite(this.files[scope], "");
1443
+ } else {
1444
+ for (const s of ["project-agents", "project-memory", "user-memory"]) {
1445
+ await atomicWrite(this.files[s], "");
1446
+ }
1447
+ }
1448
+ }
1449
+ };
1450
+ function labelOf(scope) {
1451
+ switch (scope) {
1452
+ case "project-agents":
1453
+ return "Project AGENTS.md";
1454
+ case "project-memory":
1455
+ return "Project memory";
1456
+ case "user-memory":
1457
+ return "User memory";
1458
+ }
1459
+ }
1460
+
1461
+ // src/utils/glob-match.ts
1462
+ function escapeRegex(s) {
1463
+ return s.replace(/[.+^${}()|\\/]/g, "\\$&");
1464
+ }
1465
+ function compileGlob(pattern) {
1466
+ let i = 0;
1467
+ let re = "^";
1468
+ while (i < pattern.length) {
1469
+ const c = pattern[i];
1470
+ if (c === "*") {
1471
+ if (pattern[i + 1] === "*") {
1472
+ re += ".*";
1473
+ i += 2;
1474
+ if (pattern[i] === "/") i++;
1475
+ } else {
1476
+ re += "[^/]*";
1477
+ i++;
1478
+ }
1479
+ } else if (c === "?") {
1480
+ re += "[^/]";
1481
+ i++;
1482
+ } else if (c === "[") {
1483
+ let cls = "[";
1484
+ i++;
1485
+ if (pattern[i] === "!" || pattern[i] === "^") {
1486
+ cls += "^";
1487
+ i++;
1488
+ }
1489
+ while (i < pattern.length && pattern[i] !== "]") {
1490
+ const ch = pattern[i] ?? "";
1491
+ if (ch === "\\") {
1492
+ cls += "\\\\";
1493
+ } else if (ch === "]" || ch === "^") {
1494
+ cls += `\\${ch}`;
1495
+ } else {
1496
+ cls += ch;
1497
+ }
1498
+ i++;
1499
+ }
1500
+ cls += "]";
1501
+ re += cls;
1502
+ i++;
1503
+ } else {
1504
+ re += escapeRegex(c ?? "");
1505
+ i++;
1506
+ }
1507
+ }
1508
+ re += "$";
1509
+ return new RegExp(re);
1510
+ }
1511
+ function matchGlob(pattern, input) {
1512
+ return compileGlob(pattern).test(input);
1513
+ }
1514
+ function matchAny(patterns, input) {
1515
+ return patterns.some((p) => matchGlob(p, input));
1516
+ }
1517
+
1518
+ // src/utils/safe-json.ts
1519
+ function safeParse(input, maxBytes = 5e6) {
1520
+ if (input.length > maxBytes) {
1521
+ return { ok: false, error: `Input exceeds limit (${maxBytes} bytes)` };
1522
+ }
1523
+ try {
1524
+ return { ok: true, value: JSON.parse(input) };
1525
+ } catch (err) {
1526
+ return {
1527
+ ok: false,
1528
+ error: err instanceof Error ? err.message : String(err)
1529
+ };
1530
+ }
1531
+ }
1532
+
1533
+ // src/defaults/permission-policy.ts
1534
+ var DefaultPermissionPolicy = class {
1535
+ policy = {};
1536
+ loaded = false;
1537
+ trustFile;
1538
+ yolo;
1539
+ promptDelegate;
1540
+ constructor(opts) {
1541
+ this.trustFile = opts.trustFile;
1542
+ this.yolo = opts.yolo ?? false;
1543
+ this.promptDelegate = opts.promptDelegate;
1544
+ }
1545
+ async reload() {
1546
+ try {
1547
+ const raw = await fsp.readFile(this.trustFile, "utf8");
1548
+ const parsed = safeParse(raw);
1549
+ if (parsed.ok && parsed.value) this.policy = parsed.value;
1550
+ } catch {
1551
+ this.policy = {};
1552
+ }
1553
+ this.loaded = true;
1554
+ }
1555
+ async evaluate(tool, input, _ctx) {
1556
+ if (!this.loaded) await this.reload();
1557
+ const namespaceEntry = this.findNamespaceEntry(tool.name);
1558
+ const entry = this.policy[tool.name] ?? namespaceEntry;
1559
+ const subject = this.subjectFor(tool.name, input);
1560
+ if (entry?.deny && subject && matchAny(entry.deny, subject)) {
1561
+ return { permission: "deny", source: "deny", reason: "matched deny pattern" };
1562
+ }
1563
+ if (tool.permission === "deny") {
1564
+ return { permission: "deny", source: "default", reason: "tool default deny" };
1565
+ }
1566
+ if (entry?.allow && subject && matchAny(entry.allow, subject)) {
1567
+ return { permission: "auto", source: "trust", reason: "matched allow pattern" };
1568
+ }
1569
+ if (entry?.auto) {
1570
+ return { permission: "auto", source: "trust" };
1571
+ }
1572
+ if (this.yolo) {
1573
+ return { permission: "auto", source: "yolo" };
1574
+ }
1575
+ if (tool.permission === "auto") {
1576
+ return { permission: "auto", source: "default" };
1577
+ }
1578
+ if (this.promptDelegate) {
1579
+ const decision = await this.promptDelegate(tool, input, subject ?? tool.name);
1580
+ if (decision === "always") {
1581
+ await this.trust({ tool: tool.name, pattern: subject ?? tool.name });
1582
+ return { permission: "auto", source: "user", reason: "user always-allowed" };
1583
+ }
1584
+ if (decision === "deny") {
1585
+ return { permission: "deny", source: "user", reason: "user denied" };
1586
+ }
1587
+ return { permission: decision === "yes" ? "auto" : "deny", source: "user" };
1588
+ }
1589
+ return { permission: "confirm", source: "default" };
1590
+ }
1591
+ async trust(rule) {
1592
+ if (!this.loaded) await this.reload();
1593
+ const entry = this.policy[rule.tool] ?? {};
1594
+ entry.allow = Array.from(/* @__PURE__ */ new Set([...entry.allow ?? [], rule.pattern]));
1595
+ this.policy[rule.tool] = entry;
1596
+ try {
1597
+ await atomicWrite(this.trustFile, JSON.stringify(this.policy, null, 2));
1598
+ } catch (err) {
1599
+ const existing = this.policy[rule.tool];
1600
+ if (existing?.allow) {
1601
+ const idx = existing.allow.indexOf(rule.pattern);
1602
+ if (idx !== -1) existing.allow.splice(idx, 1);
1603
+ }
1604
+ throw err;
1605
+ }
1606
+ }
1607
+ subjectFor(toolName, input) {
1608
+ if (!input || typeof input !== "object") return void 0;
1609
+ const obj = input;
1610
+ const globChars = /[*?\[\]]/g;
1611
+ const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
1612
+ if (toolName === "bash" && typeof obj.command === "string") {
1613
+ return escapeGlob(obj.command);
1614
+ }
1615
+ if (typeof obj.path === "string") {
1616
+ return escapeGlob(obj.path.replace(/\\/g, "/"));
1617
+ }
1618
+ if (typeof obj.url === "string") {
1619
+ return escapeGlob(obj.url);
1620
+ }
1621
+ if (typeof obj.name === "string") {
1622
+ return escapeGlob(obj.name);
1623
+ }
1624
+ return void 0;
1625
+ }
1626
+ findNamespaceEntry(toolName) {
1627
+ for (const key of Object.keys(this.policy)) {
1628
+ if (key.includes("*") && matchGlob(key, toolName)) {
1629
+ return this.policy[key];
1630
+ }
1631
+ }
1632
+ return void 0;
1633
+ }
1634
+ };
1635
+ var DefaultSkillLoader = class {
1636
+ dirs;
1637
+ cache;
1638
+ constructor(opts) {
1639
+ this.dirs = [
1640
+ { dir: opts.paths.inProjectSkills, source: "project" },
1641
+ { dir: opts.paths.globalSkills, source: "user" }
1642
+ ];
1643
+ if (opts.bundledDir) {
1644
+ this.dirs.push({ dir: opts.bundledDir, source: "bundled" });
1645
+ }
1646
+ }
1647
+ async list() {
1648
+ if (this.cache) return this.cache;
1649
+ const found = [];
1650
+ const seen = /* @__PURE__ */ new Set();
1651
+ for (const { dir, source } of this.dirs) {
1652
+ try {
1653
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
1654
+ for (const e of entries) {
1655
+ if (!e.isDirectory()) continue;
1656
+ const skillFile = path2.join(dir, e.name, "SKILL.md");
1657
+ try {
1658
+ const raw = await fsp.readFile(skillFile, "utf8");
1659
+ const meta = parseFrontmatter(raw);
1660
+ if (!meta.name || !meta.description) continue;
1661
+ if (seen.has(meta.name)) continue;
1662
+ seen.add(meta.name);
1663
+ found.push({
1664
+ name: meta.name,
1665
+ description: meta.description,
1666
+ version: meta.version,
1667
+ path: skillFile,
1668
+ source
1669
+ });
1670
+ } catch {
1671
+ }
1672
+ }
1673
+ } catch {
1674
+ }
1675
+ }
1676
+ this.cache = found;
1677
+ return found;
1678
+ }
1679
+ async find(name) {
1680
+ const all = await this.list();
1681
+ return all.find((s) => s.name === name);
1682
+ }
1683
+ async manifestText() {
1684
+ const skills = await this.list();
1685
+ if (skills.length === 0) return "";
1686
+ const lines = ["## Available skills"];
1687
+ for (const s of skills) {
1688
+ lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
1689
+ lines.push(` Path: ${s.path}`);
1690
+ }
1691
+ return lines.join("\n");
1692
+ }
1693
+ async readBody(name) {
1694
+ const m = await this.find(name);
1695
+ if (!m) throw new Error(`Skill "${name}" not found`);
1696
+ return fsp.readFile(m.path, "utf8");
1697
+ }
1698
+ };
1699
+ function parseFrontmatter(raw) {
1700
+ if (!raw.startsWith("---")) return {};
1701
+ const end = raw.indexOf("\n---", 4);
1702
+ if (end === -1) return {};
1703
+ const block = raw.slice(4, end);
1704
+ const out = {};
1705
+ let key = null;
1706
+ let value = [];
1707
+ const flush = () => {
1708
+ if (key) {
1709
+ out[key] = value.join("\n").trim();
1710
+ }
1711
+ key = null;
1712
+ value = [];
1713
+ };
1714
+ for (const line of block.split("\n")) {
1715
+ const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
1716
+ if (m) {
1717
+ flush();
1718
+ key = m[1] ?? "";
1719
+ const pipe = m[2];
1720
+ const rest = m[3] ?? "";
1721
+ if (pipe === "|") {
1722
+ value = [];
1723
+ } else if (rest) {
1724
+ value = [rest];
1725
+ } else {
1726
+ value = [];
1727
+ }
1728
+ } else if (key) {
1729
+ value.push(line.replace(/^\s+/, ""));
1730
+ }
1731
+ }
1732
+ flush();
1733
+ return out;
1734
+ }
1735
+ var BEHAVIOR_DEFAULTS = {
1736
+ version: 1,
1737
+ context: {
1738
+ warnThreshold: 0.6,
1739
+ softThreshold: 0.75,
1740
+ hardThreshold: 0.9,
1741
+ autoCompact: true,
1742
+ preserveK: 10,
1743
+ eliseThreshold: 2e3
1744
+ },
1745
+ tools: {
1746
+ defaultExecutionStrategy: "smart",
1747
+ maxIterations: 100,
1748
+ iterationTimeoutMs: 3e5,
1749
+ sessionTimeoutMs: 18e5,
1750
+ perIterationOutputCapBytes: 1e5,
1751
+ autoExtendLimit: true
1752
+ },
1753
+ log: { level: "info" },
1754
+ features: {
1755
+ mcp: true,
1756
+ plugins: true,
1757
+ memory: true,
1758
+ modelsRegistry: true,
1759
+ skills: true
1760
+ }
1761
+ };
1762
+ var ENV_MAP = {
1763
+ WRONGSTACK_PROVIDER: (c, v) => {
1764
+ c.provider = v;
1765
+ },
1766
+ WRONGSTACK_MODEL: (c, v) => {
1767
+ c.model = v;
1768
+ },
1769
+ WRONGSTACK_API_KEY: (c, v) => {
1770
+ c.apiKey = v;
1771
+ },
1772
+ WRONGSTACK_BASE_URL: (c, v) => {
1773
+ c.baseUrl = v;
1774
+ },
1775
+ WRONGSTACK_LOG_LEVEL: (c, v) => {
1776
+ if (!c.log) c.log = { level: "info" };
1777
+ c.log.level = v;
1778
+ }
1779
+ };
1780
+ function isPrimitiveArray(a) {
1781
+ return a.every((v) => v === null || typeof v !== "object");
1782
+ }
1783
+ function deepMerge2(base, patch) {
1784
+ if (typeof base !== "object" || base === null) return patch ?? base;
1785
+ if (typeof patch !== "object" || patch === null) return base;
1786
+ const out = { ...base };
1787
+ for (const [k, v] of Object.entries(patch)) {
1788
+ const existing = out[k];
1789
+ if (Array.isArray(v)) {
1790
+ if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
1791
+ out[k] = [.../* @__PURE__ */ new Set([...existing, ...v])];
1792
+ } else {
1793
+ out[k] = v;
1794
+ if (process.env.WRONGSTACK_DEBUG_CONFIG) {
1795
+ console.warn(
1796
+ `[config] Non-primitive array for "${k}" replaced (global + local config merge). Global entries: ${existing?.length ?? 0}, local entries: ${v.length}.`
1797
+ );
1798
+ }
1799
+ }
1800
+ } else if (typeof v === "object" && v !== null && typeof existing === "object" && existing !== null) {
1801
+ out[k] = deepMerge2(existing, v);
1802
+ } else if (v !== void 0) {
1803
+ out[k] = v;
1804
+ }
1805
+ }
1806
+ return out;
1807
+ }
1808
+ var DefaultConfigLoader = class {
1809
+ paths;
1810
+ strict;
1811
+ vault;
1812
+ extraSources;
1813
+ constructor(opts) {
1814
+ this.paths = opts.paths;
1815
+ this.strict = opts.strict ?? false;
1816
+ this.vault = opts.vault;
1817
+ this.extraSources = opts.sources ?? [];
1818
+ }
1819
+ async load(opts = {}) {
1820
+ let cfg = { ...BEHAVIOR_DEFAULTS };
1821
+ const [global, local] = await Promise.all([
1822
+ this.readJson(this.paths.globalConfig),
1823
+ this.readJson(this.paths.projectLocalConfig)
1824
+ ]);
1825
+ cfg = deepMerge2(cfg, global);
1826
+ cfg = deepMerge2(cfg, local);
1827
+ for (const [key, fn] of Object.entries(ENV_MAP)) {
1828
+ const v = process.env[key];
1829
+ if (v) fn(cfg, v);
1830
+ }
1831
+ const sorted = [...this.extraSources].sort((a, b) => {
1832
+ const pd = (a.priority ?? 50) - (b.priority ?? 50);
1833
+ if (pd !== 0) return pd;
1834
+ return a.name.localeCompare(b.name);
1835
+ });
1836
+ for (const src of sorted) {
1837
+ try {
1838
+ const patch = await src.read();
1839
+ if (patch && Object.keys(patch).length > 0) {
1840
+ cfg = deepMerge2(cfg, patch);
1841
+ }
1842
+ } catch (err) {
1843
+ console.warn(`Config source "${src.name}" failed`, err);
1844
+ }
1845
+ }
1846
+ if (opts.cliFlags) {
1847
+ cfg = deepMerge2(cfg, opts.cliFlags);
1848
+ }
1849
+ if (this.vault) {
1850
+ cfg = decryptConfigSecrets(cfg, this.vault);
1851
+ }
1852
+ this.validateBehavior(cfg);
1853
+ if (this.strict) this.validateIdentity(cfg);
1854
+ return Object.freeze(cfg);
1855
+ }
1856
+ async readJson(file) {
1857
+ try {
1858
+ const raw = await fsp.readFile(file, "utf8");
1859
+ const parsed = safeParse(raw);
1860
+ if (parsed.ok && parsed.value) return parsed.value;
1861
+ } catch {
1862
+ }
1863
+ return {};
1864
+ }
1865
+ validateBehavior(cfg) {
1866
+ if (cfg.version === void 0) throw new Error("Config: missing version field");
1867
+ if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
1868
+ const c = cfg.context;
1869
+ if (!c) throw new Error("Config: missing context section");
1870
+ if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
1871
+ throw new Error("Config: context thresholds must satisfy warn < soft < hard");
1872
+ }
1873
+ }
1874
+ validateIdentity(cfg) {
1875
+ if (!cfg.provider) {
1876
+ throw new Error(
1877
+ "Config: no provider configured. Run `wstack init` or set WRONGSTACK_PROVIDER."
1878
+ );
1879
+ }
1880
+ if (!cfg.model) {
1881
+ throw new Error(
1882
+ "Config: no model configured. Run `wstack init` or set WRONGSTACK_MODEL."
1883
+ );
1884
+ }
1885
+ }
1886
+ };
1887
+
1888
+ // src/defaults/compactor.ts
1889
+ var HybridCompactor = class {
1890
+ preserveK;
1891
+ eliseThreshold;
1892
+ estimator;
1893
+ constructor(opts = {}) {
1894
+ this.preserveK = opts.preserveK ?? 10;
1895
+ this.eliseThreshold = opts.eliseThreshold ?? 2e3;
1896
+ this.estimator = opts.estimator ?? roughTokenEstimate;
1897
+ }
1898
+ async compact(ctx, opts = {}) {
1899
+ const beforeTokens = this.estimateMessages(ctx.messages);
1900
+ const reductions = [];
1901
+ const phase1Saved = this.eliseOldToolResults(ctx);
1902
+ if (phase1Saved > 0) reductions.push({ phase: "elision", saved: phase1Saved });
1903
+ if (opts.aggressive) {
1904
+ const phase2Saved = this.collapseAncientTurns(ctx);
1905
+ if (phase2Saved > 0) reductions.push({ phase: "summary", saved: phase2Saved });
1906
+ }
1907
+ const afterTokens = this.estimateMessages(ctx.messages);
1908
+ return { before: beforeTokens, after: afterTokens, reductions };
1909
+ }
1910
+ eliseOldToolResults(ctx) {
1911
+ const messages = ctx.messages;
1912
+ let pairCount = 0;
1913
+ let preserveStart = messages.length;
1914
+ for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
1915
+ const m = messages[i];
1916
+ if (!m) continue;
1917
+ if (m.role === "user" || m.role === "assistant") {
1918
+ pairCount++;
1919
+ preserveStart = i;
1920
+ }
1921
+ }
1922
+ let saved = 0;
1923
+ for (let i = 0; i < preserveStart; i++) {
1924
+ const msg = messages[i];
1925
+ if (!msg || !Array.isArray(msg.content)) continue;
1926
+ const newContent = msg.content.map((b) => {
1927
+ if (b.type !== "tool_result") return b;
1928
+ const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
1929
+ const tokens = this.estimator(text);
1930
+ if (tokens < this.eliseThreshold) return b;
1931
+ saved += tokens;
1932
+ const elided = {
1933
+ type: "tool_result",
1934
+ tool_use_id: b.tool_use_id,
1935
+ content: `[elided: ~${tokens} tokens removed. Call the tool again if needed.]`,
1936
+ is_error: b.is_error
1937
+ };
1938
+ return elided;
1939
+ });
1940
+ messages[i] = { ...msg, content: newContent };
1941
+ }
1942
+ return saved;
1943
+ }
1944
+ collapseAncientTurns(ctx) {
1945
+ const messages = ctx.messages;
1946
+ const cutTarget = Math.max(0, messages.length - this.preserveK * 2);
1947
+ if (cutTarget <= 0) return 0;
1948
+ let boundary = -1;
1949
+ for (let i = cutTarget; i < messages.length; i++) {
1950
+ const m = messages[i];
1951
+ if (!m) continue;
1952
+ if (m.role === "user" && hasTextContent(m)) {
1953
+ boundary = i;
1954
+ break;
1955
+ }
1956
+ }
1957
+ if (boundary <= 0) return 0;
1958
+ const removed = messages.slice(0, boundary);
1959
+ const removedTokens = this.estimateMessages(removed);
1960
+ const summary = [
1961
+ {
1962
+ role: "user",
1963
+ content: `[previous_session_summary: ${removed.length} earlier turns compacted. Todo state preserved in context.]`
1964
+ },
1965
+ { role: "assistant", content: "Continuing from compacted context." }
1966
+ ];
1967
+ ctx.messages.splice(0, boundary, ...summary);
1968
+ return Math.max(0, removedTokens - this.estimateMessages(summary));
1969
+ }
1970
+ estimateMessages(messages) {
1971
+ let total = 0;
1972
+ for (const m of messages) {
1973
+ if (typeof m.content === "string") {
1974
+ total += this.estimator(m.content);
1975
+ } else {
1976
+ for (const b of m.content) {
1977
+ if (b.type === "text") total += this.estimator(b.text);
1978
+ else if (b.type === "tool_use") total += this.estimator(JSON.stringify(b.input));
1979
+ else if (b.type === "tool_result") {
1980
+ total += this.estimator(
1981
+ typeof b.content === "string" ? b.content : JSON.stringify(b.content)
1982
+ );
1983
+ }
1984
+ }
1985
+ }
1986
+ }
1987
+ return total;
1988
+ }
1989
+ };
1990
+ function hasTextContent(m) {
1991
+ if (typeof m.content === "string") return m.content.trim().length > 0;
1992
+ return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
1993
+ }
1994
+ function roughTokenEstimate(text) {
1995
+ return Math.max(1, Math.ceil(text.length / 4));
1996
+ }
1997
+
1998
+ // src/types/blocks.ts
1999
+ function isTextBlock(b) {
2000
+ return b.type === "text";
2001
+ }
2002
+
2003
+ // src/defaults/intelligent-compactor.ts
2004
+ var IntelligentCompactor = class {
2005
+ provider;
2006
+ warnThreshold;
2007
+ softThreshold;
2008
+ hardThreshold;
2009
+ maxContext;
2010
+ preserveK;
2011
+ eliseThreshold;
2012
+ summarizerPrompt;
2013
+ summarizerModel;
2014
+ constructor(opts) {
2015
+ this.provider = opts.provider;
2016
+ this.warnThreshold = opts.warnThreshold ?? 0.6;
2017
+ this.softThreshold = opts.softThreshold ?? 0.75;
2018
+ this.hardThreshold = opts.hardThreshold ?? 0.9;
2019
+ this.maxContext = opts.maxContext ?? 128e3;
2020
+ this.preserveK = opts.preserveK ?? 4;
2021
+ this.eliseThreshold = opts.eliseThreshold ?? 500;
2022
+ this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of conversation messages, produce a concise but complete summary that preserves all factual information, decisions made, and any state changes (e.g. file edits, todo updates). Do not add commentary. Output only the summary.";
2023
+ this.summarizerModel = opts.summarizerModel;
2024
+ }
2025
+ async compact(ctx, opts = {}) {
2026
+ const beforeTokens = this.estimateTokens(ctx.messages);
2027
+ const reductions = [];
2028
+ const load = beforeTokens / this.maxContext;
2029
+ const aggressive = opts.aggressive ?? load >= this.softThreshold;
2030
+ const saved1 = this.eliseOldToolResults(ctx);
2031
+ if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
2032
+ if (aggressive) {
2033
+ const saved2 = await this.summarizeAncientTurns(ctx);
2034
+ if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
2035
+ } else if (load >= this.warnThreshold) {
2036
+ const saved2 = this.lightweightCompact(ctx);
2037
+ if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
2038
+ }
2039
+ const afterTokens = this.estimateTokens(ctx.messages);
2040
+ return { before: beforeTokens, after: afterTokens, reductions };
2041
+ }
2042
+ async summarizeAncientTurns(ctx) {
2043
+ const messages = ctx.messages;
2044
+ const cutoff = Math.max(0, messages.length - this.preserveK * 2);
2045
+ if (cutoff <= 2) return 0;
2046
+ const boundary = this.findSafeBoundary(messages, 0, cutoff);
2047
+ if (boundary <= 1) return 0;
2048
+ const toSummarize = messages.slice(0, boundary);
2049
+ const removedTokens = this.estimateTokens(toSummarize);
2050
+ let summaryText;
2051
+ try {
2052
+ summaryText = await this.callSummarizer(toSummarize, ctx);
2053
+ } catch {
2054
+ summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
2055
+ }
2056
+ const summaryMsg = {
2057
+ role: "system",
2058
+ content: `[prior_turns_summary: ${summaryText}]`
2059
+ };
2060
+ const summaryTokens = this.estimateTokens([summaryMsg]);
2061
+ ctx.messages.splice(0, boundary, summaryMsg);
2062
+ return Math.max(0, removedTokens - summaryTokens);
2063
+ }
2064
+ findSafeBoundary(messages, from, to) {
2065
+ for (let i = to; i >= from; i--) {
2066
+ const m = messages[i];
2067
+ if (!m) continue;
2068
+ if (m.role === "user" && this.hasTextContent(m)) {
2069
+ return this.findExchangeStart(messages, i);
2070
+ }
2071
+ }
2072
+ return -1;
2073
+ }
2074
+ findExchangeStart(messages, userIndex) {
2075
+ for (let i = userIndex - 1; i >= 0; i--) {
2076
+ const m = messages[i];
2077
+ if (!m) continue;
2078
+ if (m.role === "assistant") {
2079
+ const hasToolUse = Array.isArray(m.content) ? m.content.some((b) => b.type === "tool_use") : false;
2080
+ if (!hasToolUse) {
2081
+ return i + 1;
2082
+ }
2083
+ } else if (m.role !== "user") ; else {
2084
+ return i;
2085
+ }
2086
+ }
2087
+ return 0;
2088
+ }
2089
+ async callSummarizer(messages, ctx) {
2090
+ const prompt = [
2091
+ { type: "text", text: this.summarizerPrompt },
2092
+ { type: "text", text: "\n\nConversation to summarize:\n" },
2093
+ ...this.messagesToText(messages)
2094
+ ];
2095
+ const req = {
2096
+ model: this.summarizerModel ?? ctx.model,
2097
+ system: prompt,
2098
+ messages: [],
2099
+ maxTokens: 1024
2100
+ };
2101
+ const signal = ctx.signal ?? new AbortController().signal;
2102
+ const res = await this.provider.complete(req, { signal });
2103
+ const textBlocks = res.content.filter(isTextBlock);
2104
+ return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
2105
+ }
2106
+ messagesToText(messages) {
2107
+ const lines = [];
2108
+ for (const m of messages) {
2109
+ const role = m.role.padEnd(10, " ");
2110
+ if (typeof m.content === "string") {
2111
+ lines.push(`[${role}]: ${m.content.slice(0, 500)}`);
2112
+ } else if (Array.isArray(m.content)) {
2113
+ const textParts = m.content.filter(isTextBlock).map((b) => b.text);
2114
+ if (textParts.length > 0) {
2115
+ lines.push(`[${role}]: ${textParts.join(" ").slice(0, 500)}`);
2116
+ }
2117
+ }
2118
+ }
2119
+ return [{ type: "text", text: lines.join("\n") }];
2120
+ }
2121
+ lightweightCompact(ctx) {
2122
+ return this.eliseOldToolResults(ctx);
2123
+ }
2124
+ eliseOldToolResults(ctx) {
2125
+ const messages = ctx.messages;
2126
+ let pairCount = 0;
2127
+ let preserveStart = messages.length;
2128
+ for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
2129
+ const m = messages[i];
2130
+ if (!m) continue;
2131
+ if (m.role === "user" || m.role === "assistant") {
2132
+ pairCount++;
2133
+ preserveStart = i;
2134
+ }
2135
+ }
2136
+ let saved = 0;
2137
+ for (let i = 0; i < preserveStart; i++) {
2138
+ const msg = messages[i];
2139
+ if (!msg || !Array.isArray(msg.content)) continue;
2140
+ const newContent = msg.content.map((b) => {
2141
+ if (b.type !== "tool_result") return b;
2142
+ const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
2143
+ const tokens = this.roughTokenEstimate(text);
2144
+ if (tokens < this.eliseThreshold) return b;
2145
+ saved += tokens;
2146
+ return {
2147
+ type: "tool_result",
2148
+ tool_use_id: b.tool_use_id,
2149
+ content: `[elided: ~${tokens} tokens]`,
2150
+ is_error: b.is_error
2151
+ };
2152
+ });
2153
+ messages[i] = { ...msg, content: newContent };
2154
+ }
2155
+ return saved;
2156
+ }
2157
+ hasTextContent(m) {
2158
+ if (typeof m.content === "string") return m.content.trim().length > 0;
2159
+ return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
2160
+ }
2161
+ estimateTokens(messages) {
2162
+ let total = 0;
2163
+ for (const m of messages) {
2164
+ if (typeof m.content === "string") {
2165
+ total += this.roughTokenEstimate(m.content);
2166
+ } else {
2167
+ for (const b of m.content) {
2168
+ if (b.type === "text") total += this.roughTokenEstimate(b.text);
2169
+ else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
2170
+ else if (b.type === "tool_result") {
2171
+ total += this.roughTokenEstimate(
2172
+ typeof b.content === "string" ? b.content : JSON.stringify(b.content)
2173
+ );
2174
+ }
2175
+ }
2176
+ }
2177
+ }
2178
+ return total;
2179
+ }
2180
+ roughTokenEstimate(text) {
2181
+ return Math.max(1, Math.ceil(text.length / 4));
2182
+ }
2183
+ };
2184
+
2185
+ // src/defaults/llm-selector.ts
2186
+ var DEFAULT_SYSTEM_PROMPT = `You are a context pruning assistant. Given a conversation history and a token budget, decide which message ranges are worth keeping verbatim and which should be collapsed into summaries.
2187
+
2188
+ Output a JSON object with this structure:
2189
+ {
2190
+ "kept": [{"from": 0, "to": 5, "importance": "critical"}],
2191
+ "collapsed": [{"from": 6, "to": 20, "summary": "optional summary"}],
2192
+ "reasoning": "brief explanation of decisions"
2193
+ }
2194
+
2195
+ Importance tiers:
2196
+ - "critical": decisions, file edits, tool results that affect state, final answers
2197
+ - "high": substantive tool use, complex reasoning, non-obvious observations
2198
+ - "medium": routine exchanges, confirmations, straightforward Q&A
2199
+
2200
+ Rules:
2201
+ - Always keep the most recent K pairs (preserve recency)
2202
+ - Never collapse the final 2 user/assistant pairs (working memory)
2203
+ - Preserve tool results that modified files or had external effects
2204
+ - Collapse old, low-information exchanges (greetings, acknowledgements, etc.)
2205
+ - If unsure, keep rather than collapse (errors are more costly than waste)
2206
+
2207
+ Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
2208
+ function estimateTokens(messages) {
2209
+ let total = 0;
2210
+ for (const m of messages) {
2211
+ if (typeof m.content === "string") {
2212
+ total += Math.ceil(m.content.length / 4);
2213
+ } else if (Array.isArray(m.content)) {
2214
+ for (const b of m.content) {
2215
+ if (b.type === "text") total += Math.ceil(b.text.length / 4);
2216
+ else total += Math.ceil(JSON.stringify(b).length / 4);
2217
+ }
2218
+ }
2219
+ }
2220
+ return total;
2221
+ }
2222
+ function formatMessages(messages, maxChars = 8e3) {
2223
+ const lines = [];
2224
+ let used = 0;
2225
+ for (let i = 0; i < messages.length; i++) {
2226
+ const m = messages[i];
2227
+ const role = m.role.padEnd(10, " ");
2228
+ let text;
2229
+ if (typeof m.content === "string") {
2230
+ text = m.content.slice(0, 500);
2231
+ } else {
2232
+ const content = m.content;
2233
+ text = content.filter(isTextBlock).map((b) => b.text).join(" ");
2234
+ const toolUses = content.filter((b) => b.type === "tool_use");
2235
+ if (toolUses.length > 0) {
2236
+ text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
2237
+ }
2238
+ }
2239
+ const line = `[${i}][${role}]: ${text}`;
2240
+ if (used + line.length > maxChars) break;
2241
+ lines.push(line);
2242
+ used += line.length;
2243
+ }
2244
+ return lines.join("\n");
2245
+ }
2246
+ var LLMSelector = class {
2247
+ provider;
2248
+ model;
2249
+ maxContextTokens;
2250
+ systemPrompt;
2251
+ constructor(opts) {
2252
+ this.provider = opts.provider;
2253
+ this.model = opts.model ?? "unknown";
2254
+ this.maxContextTokens = opts.maxContextTokens ?? 4e4;
2255
+ this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
2256
+ }
2257
+ async select(messages, maxToKeep) {
2258
+ const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
2259
+ const historyText = formatMessages(messages);
2260
+ const totalTokens = estimateTokens(messages);
2261
+ const systemText = `${this.systemPrompt}
2262
+
2263
+ Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
2264
+ `;
2265
+ const budgetInstruction = totalTokens > effectiveBudget ? `
2266
+
2267
+ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
2268
+ const req = {
2269
+ model: this.model,
2270
+ system: [{ type: "text", text: systemText + budgetInstruction }],
2271
+ messages: [{ role: "user", content: historyText }],
2272
+ maxTokens: 1024
2273
+ };
2274
+ let raw;
2275
+ try {
2276
+ const res = await this.provider.complete(req, { signal: new AbortController().signal });
2277
+ const textBlocks = res.content.filter(isTextBlock);
2278
+ raw = textBlocks.map((b) => b.text).join("\n").trim();
2279
+ } catch (err) {
2280
+ return this.fallbackSelect(messages, effectiveBudget);
2281
+ }
2282
+ return this.parseSelectorOutput(raw, messages.length);
2283
+ }
2284
+ fallbackSelect(messages, budget) {
2285
+ const toKeep = [];
2286
+ const toCollapse = [];
2287
+ let tokenCount = 0;
2288
+ let startIdx = 0;
2289
+ for (let i = messages.length - 1; i >= 0; i--) {
2290
+ const m = messages[i];
2291
+ const cost = typeof m.content === "string" ? Math.ceil(m.content.length / 4) : m.content.reduce((acc, b) => acc + (b.type === "text" ? Math.ceil(b.text.length / 4) : Math.ceil(JSON.stringify(b).length / 4)), 0);
2292
+ if (tokenCount + cost <= budget) {
2293
+ tokenCount += cost;
2294
+ } else {
2295
+ startIdx = i + 1;
2296
+ break;
2297
+ }
2298
+ }
2299
+ if (startIdx > 0) {
2300
+ toCollapse.push({ from: 0, to: startIdx - 1 });
2301
+ }
2302
+ toKeep.push({ from: startIdx, to: messages.length - 1, importance: "high" });
2303
+ return {
2304
+ kept: toKeep,
2305
+ collapsed: toCollapse,
2306
+ reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
2307
+ };
2308
+ }
2309
+ parseSelectorOutput(raw, messageCount) {
2310
+ const jsonStart = raw.indexOf("{");
2311
+ const jsonEnd = raw.lastIndexOf("}");
2312
+ if (jsonStart === -1 || jsonEnd === -1) {
2313
+ return this.fallbackSelect(
2314
+ Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
2315
+ this.maxContextTokens
2316
+ );
2317
+ }
2318
+ let parsed;
2319
+ try {
2320
+ parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
2321
+ } catch {
2322
+ return this.fallbackSelect(
2323
+ Array.from({ length: messageCount }, (_, i) => ({ role: "user", content: "" })),
2324
+ this.maxContextTokens
2325
+ );
2326
+ }
2327
+ const obj = parsed;
2328
+ const kept = obj.kept ?? [];
2329
+ const collapsed = obj.collapsed ?? [];
2330
+ return {
2331
+ kept: kept.map((k) => ({ from: k.from, to: k.to, importance: k.importance ?? "medium" })),
2332
+ collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
2333
+ reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
2334
+ };
2335
+ }
2336
+ };
2337
+
2338
+ // src/defaults/selective-compactor.ts
2339
+ var SelectiveCompactor = class {
2340
+ provider;
2341
+ selector;
2342
+ warnThreshold;
2343
+ softThreshold;
2344
+ hardThreshold;
2345
+ maxContext;
2346
+ preserveK;
2347
+ eliseThreshold;
2348
+ summarizerModel;
2349
+ summarizerPrompt;
2350
+ constructor(opts) {
2351
+ this.provider = opts.provider;
2352
+ this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
2353
+ this.warnThreshold = opts.warnThreshold ?? 0.6;
2354
+ this.softThreshold = opts.softThreshold ?? 0.75;
2355
+ this.hardThreshold = opts.hardThreshold ?? 0.9;
2356
+ this.maxContext = opts.maxContext ?? 128e3;
2357
+ this.preserveK = opts.preserveK ?? 4;
2358
+ this.eliseThreshold = opts.eliseThreshold ?? 500;
2359
+ this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
2360
+ this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
2361
+ }
2362
+ async compact(ctx, opts = {}) {
2363
+ const beforeTokens = this.estimateTokens(ctx.messages);
2364
+ const reductions = [];
2365
+ const load = beforeTokens / this.maxContext;
2366
+ const shouldCompact = load >= this.warnThreshold || opts.aggressive;
2367
+ if (!shouldCompact) {
2368
+ const saved = this.eliseOldToolResults(ctx);
2369
+ if (saved > 0) reductions.push({ phase: "elision", saved });
2370
+ const afterTokens2 = this.estimateTokens(ctx.messages);
2371
+ return { before: beforeTokens, after: afterTokens2, reductions };
2372
+ }
2373
+ const savedElision = this.eliseOldToolResults(ctx);
2374
+ if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
2375
+ const afterPhase1 = this.estimateTokens(ctx.messages);
2376
+ const targetBudget = this.computeTargetBudget(load, opts.aggressive ?? false);
2377
+ if (afterPhase1 > targetBudget) {
2378
+ const savedSelective = await this.runSelector(ctx, targetBudget);
2379
+ if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
2380
+ }
2381
+ const afterTokens = this.estimateTokens(ctx.messages);
2382
+ return { before: beforeTokens, after: afterTokens, reductions };
2383
+ }
2384
+ /**
2385
+ * Run the LLM selector to decide what to keep vs collapse.
2386
+ * Returns the token savings achieved.
2387
+ */
2388
+ async runSelector(ctx, targetBudget) {
2389
+ const before = this.estimateTokens(ctx.messages);
2390
+ let result;
2391
+ try {
2392
+ result = await this.selector.select(ctx.messages, targetBudget);
2393
+ } catch {
2394
+ return this.aggressiveRecencyTrim(ctx, targetBudget);
2395
+ }
2396
+ await this.executePlan(ctx, result);
2397
+ const after = this.estimateTokens(ctx.messages);
2398
+ return Math.max(0, before - after);
2399
+ }
2400
+ /**
2401
+ * Execute a SelectorResult plan: collapse/remove ranges and
2402
+ * insert summaries where the selector provided them.
2403
+ */
2404
+ async executePlan(ctx, plan) {
2405
+ const messages = ctx.messages;
2406
+ if (messages.length === 0) return;
2407
+ const sortedCollapsed = [...plan.collapsed].sort((a, b) => b.from - a.from);
2408
+ for (const range of sortedCollapsed) {
2409
+ if (range.from < 0 || range.to >= messages.length || range.from > range.to) continue;
2410
+ let summary = range.summary;
2411
+ if (!summary) {
2412
+ const toSummarize = messages.slice(range.from, range.to + 1);
2413
+ summary = await this.summarizeRange(toSummarize, ctx);
2414
+ }
2415
+ const summaryMsg = {
2416
+ role: "system",
2417
+ content: `[prior_turns_${range.from}-${range.to}: ${summary}]`
2418
+ };
2419
+ messages.splice(range.from, range.to - range.from + 1, summaryMsg);
2420
+ }
2421
+ }
2422
+ async summarizeRange(messages, ctx) {
2423
+ const systemText = `${this.summarizerPrompt}
2424
+
2425
+ Summarize the following message range:`;
2426
+ const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
2427
+ const req = {
2428
+ model: this.summarizerModel,
2429
+ system: [{ type: "text", text: systemText }],
2430
+ messages: [{ role: "user", content: body }],
2431
+ maxTokens: 512
2432
+ };
2433
+ try {
2434
+ const res = await this.provider.complete(req, { signal: ctx.signal ?? new AbortController().signal });
2435
+ return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
2436
+ } catch {
2437
+ return `[${messages.length} earlier turns omitted]`;
2438
+ }
2439
+ }
2440
+ messagePreview(m) {
2441
+ if (typeof m.content === "string") return m.content.slice(0, 300);
2442
+ return m.content.filter(isTextBlock).map((b) => b.text).join(" ").slice(0, 300);
2443
+ }
2444
+ /**
2445
+ * Fallback when selector fails: aggressively trim from the oldest end
2446
+ * until we hit targetBudget.
2447
+ */
2448
+ aggressiveRecencyTrim(ctx, targetBudget) {
2449
+ const messages = ctx.messages;
2450
+ this.estimateTokens(messages);
2451
+ const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
2452
+ if (preserveIdx <= 0) return 0;
2453
+ let boundary = preserveIdx;
2454
+ for (let i = preserveIdx; i < messages.length && i < preserveIdx + 6; i++) {
2455
+ const m = messages[i];
2456
+ if (m.role === "user" && this.hasTextContent(m)) {
2457
+ boundary = i;
2458
+ break;
2459
+ }
2460
+ }
2461
+ const removed = messages.slice(0, boundary);
2462
+ const removedTokens = this.estimateTokens(removed);
2463
+ const summaryMsg = {
2464
+ role: "system",
2465
+ content: `[${removed.length} earlier turns trimmed \u2014 see session log for details]`
2466
+ };
2467
+ messages.splice(0, boundary, summaryMsg);
2468
+ return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
2469
+ }
2470
+ computeTargetBudget(load, aggressive) {
2471
+ if (load >= this.hardThreshold) {
2472
+ return Math.floor(this.maxContext * 0.5);
2473
+ }
2474
+ if (load >= this.softThreshold) {
2475
+ return Math.floor(this.maxContext * 0.65);
2476
+ }
2477
+ return Math.floor(this.maxContext * 0.75);
2478
+ }
2479
+ eliseOldToolResults(ctx) {
2480
+ const messages = ctx.messages;
2481
+ let pairCount = 0;
2482
+ let preserveStart = messages.length;
2483
+ for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
2484
+ const m = messages[i];
2485
+ if (!m) continue;
2486
+ if (m.role === "user" || m.role === "assistant") {
2487
+ pairCount++;
2488
+ preserveStart = i;
2489
+ }
2490
+ }
2491
+ let saved = 0;
2492
+ for (let i = 0; i < preserveStart; i++) {
2493
+ const msg = messages[i];
2494
+ if (!msg || !Array.isArray(msg.content)) continue;
2495
+ const newContent = msg.content.map((b) => {
2496
+ if (b.type !== "tool_result") return b;
2497
+ const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
2498
+ const tokens = this.roughTokenEstimate(text);
2499
+ if (tokens < this.eliseThreshold) return b;
2500
+ saved += tokens;
2501
+ return {
2502
+ type: "tool_result",
2503
+ tool_use_id: b.tool_use_id,
2504
+ content: `[elided: ~${tokens} tokens]`,
2505
+ is_error: b.is_error
2506
+ };
2507
+ });
2508
+ messages[i] = { ...msg, content: newContent };
2509
+ }
2510
+ return saved;
2511
+ }
2512
+ hasTextContent(m) {
2513
+ if (typeof m.content === "string") return m.content.trim().length > 0;
2514
+ return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
2515
+ }
2516
+ estimateTokens(messages) {
2517
+ let total = 0;
2518
+ for (const m of messages) {
2519
+ if (typeof m.content === "string") {
2520
+ total += this.roughTokenEstimate(m.content);
2521
+ } else {
2522
+ for (const b of m.content) {
2523
+ if (b.type === "text") total += this.roughTokenEstimate(b.text);
2524
+ else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
2525
+ else if (b.type === "tool_result") {
2526
+ total += this.roughTokenEstimate(
2527
+ typeof b.content === "string" ? b.content : JSON.stringify(b.content)
2528
+ );
2529
+ }
2530
+ }
2531
+ }
2532
+ }
2533
+ return total;
2534
+ }
2535
+ roughTokenEstimate(text) {
2536
+ return Math.max(1, Math.ceil(text.length / 4));
2537
+ }
2538
+ };
2539
+
2540
+ // src/defaults/auto-compaction-middleware.ts
2541
+ var AutoCompactionMiddleware = class {
2542
+ name = "AutoCompaction";
2543
+ compactor;
2544
+ warnThreshold;
2545
+ // fraction of maxContext (0-1)
2546
+ softThreshold;
2547
+ hardThreshold;
2548
+ maxContext;
2549
+ estimator;
2550
+ aggressiveOn;
2551
+ /**
2552
+ * @param compactor Compactor to use for compaction
2553
+ * @param maxContext Provider's max context window in tokens
2554
+ * @param estimator Token estimation function (ctx → token count)
2555
+ * @param thresholds Threshold fractions (0-1) of maxContext
2556
+ * @param aggressiveOn Which threshold triggers aggressive (full LLM summarization)
2557
+ */
2558
+ constructor(compactor, maxContext, estimator, thresholds, aggressiveOn = "soft") {
2559
+ this.compactor = compactor;
2560
+ this.maxContext = maxContext;
2561
+ this.estimator = estimator;
2562
+ this.warnThreshold = thresholds.warn;
2563
+ this.softThreshold = thresholds.soft;
2564
+ this.hardThreshold = thresholds.hard;
2565
+ this.aggressiveOn = aggressiveOn;
2566
+ }
2567
+ handler() {
2568
+ return async (ctx, next) => {
2569
+ const tokens = this.estimator(ctx);
2570
+ const load = tokens / this.maxContext;
2571
+ if (load >= this.hardThreshold) {
2572
+ await this.compact(ctx, true);
2573
+ } else if (load >= this.softThreshold) {
2574
+ await this.compact(ctx, this.aggressiveOn !== "hard");
2575
+ } else if (load >= this.warnThreshold) {
2576
+ await this.compact(ctx, false);
2577
+ }
2578
+ return next(ctx);
2579
+ };
2580
+ }
2581
+ async compact(ctx, aggressive) {
2582
+ try {
2583
+ await this.compactor.compact(ctx, { aggressive });
2584
+ } catch {
2585
+ }
2586
+ }
2587
+ };
2588
+ var DEFAULT_URL = "https://models.dev/api.json";
2589
+ var DEFAULT_TTL_SECONDS = 24 * 3600;
2590
+ var FAMILY_BY_NPM = {
2591
+ "@ai-sdk/anthropic": "anthropic",
2592
+ "@ai-sdk/google-vertex/anthropic": "anthropic",
2593
+ "@ai-sdk/openai": "openai",
2594
+ "@ai-sdk/openai-compatible": "openai-compatible",
2595
+ "@ai-sdk/groq": "openai-compatible",
2596
+ "@ai-sdk/xai": "openai-compatible",
2597
+ "@ai-sdk/cerebras": "openai-compatible",
2598
+ "@ai-sdk/togetherai": "openai-compatible",
2599
+ "@ai-sdk/perplexity": "openai-compatible",
2600
+ "@ai-sdk/deepinfra": "openai-compatible",
2601
+ "@openrouter/ai-sdk-provider": "openai-compatible",
2602
+ "ai-gateway-provider": "openai-compatible",
2603
+ "@ai-sdk/vercel": "openai-compatible",
2604
+ "@ai-sdk/gateway": "openai-compatible",
2605
+ "@aihubmix/ai-sdk-provider": "openai-compatible",
2606
+ "venice-ai-sdk-provider": "openai-compatible",
2607
+ "@ai-sdk/google": "google"
2608
+ };
2609
+ function classifyFamily(npm) {
2610
+ if (!npm) return "unsupported";
2611
+ return FAMILY_BY_NPM[npm] ?? "unsupported";
2612
+ }
2613
+ var DefaultModelsRegistry = class {
2614
+ payload;
2615
+ fetchedAt;
2616
+ cacheFile;
2617
+ url;
2618
+ ttlMs;
2619
+ fetchImpl;
2620
+ seed;
2621
+ maxStaleAgeMs;
2622
+ constructor(opts) {
2623
+ this.cacheFile = opts.cacheFile;
2624
+ this.url = opts.url ?? DEFAULT_URL;
2625
+ this.ttlMs = (opts.ttlSeconds ?? DEFAULT_TTL_SECONDS) * 1e3;
2626
+ this.fetchImpl = opts.fetchImpl ?? fetch;
2627
+ this.seed = opts.seed;
2628
+ const maxStaleSeconds = opts.maxStaleAgeSeconds ?? 7 * 24 * 3600;
2629
+ this.maxStaleAgeMs = maxStaleSeconds * 1e3;
2630
+ }
2631
+ async load(opts = {}) {
2632
+ if (this.payload && !opts.force) return this.payload;
2633
+ if (this.seed) {
2634
+ this.payload = this.seed;
2635
+ this.fetchedAt = /* @__PURE__ */ new Date();
2636
+ return this.payload;
2637
+ }
2638
+ if (!opts.force) {
2639
+ const cached = await this.readCache();
2640
+ if (cached && this.isFresh(cached.fetchedAt)) {
2641
+ this.payload = cached.payload;
2642
+ this.fetchedAt = new Date(cached.fetchedAt);
2643
+ return cached.payload;
2644
+ }
2645
+ }
2646
+ try {
2647
+ return await this.refresh();
2648
+ } catch (err) {
2649
+ const cached = await this.readCache();
2650
+ if (cached && this.isWithinMaxStaleAge(cached.fetchedAt)) {
2651
+ this.payload = cached.payload;
2652
+ this.fetchedAt = new Date(cached.fetchedAt);
2653
+ return cached.payload;
2654
+ }
2655
+ throw err;
2656
+ }
2657
+ }
2658
+ async refresh() {
2659
+ const res = await this.fetchImpl(this.url, {
2660
+ method: "GET",
2661
+ headers: { accept: "application/json" }
2662
+ });
2663
+ if (!res.ok) {
2664
+ throw new Error(`ModelsRegistry: HTTP ${res.status} fetching ${this.url}`);
2665
+ }
2666
+ const json = await res.json();
2667
+ this.payload = json;
2668
+ this.fetchedAt = /* @__PURE__ */ new Date();
2669
+ const envelope = {
2670
+ fetchedAt: this.fetchedAt.toISOString(),
2671
+ url: this.url,
2672
+ payload: json
2673
+ };
2674
+ await atomicWrite(this.cacheFile, JSON.stringify(envelope));
2675
+ return json;
2676
+ }
2677
+ async listProviders() {
2678
+ const payload = await this.load();
2679
+ return Object.values(payload).map((p) => this.resolveProvider(p));
2680
+ }
2681
+ async getProvider(id) {
2682
+ const payload = await this.load();
2683
+ const p = payload[id];
2684
+ return p ? this.resolveProvider(p) : void 0;
2685
+ }
2686
+ async getModel(providerId, modelId) {
2687
+ const provider = await this.getProvider(providerId);
2688
+ if (!provider) return void 0;
2689
+ const model = provider.models.find((m) => m.id === modelId);
2690
+ if (!model) return void 0;
2691
+ return {
2692
+ providerId,
2693
+ modelId,
2694
+ capabilities: {
2695
+ tools: model.tool_call ?? false,
2696
+ vision: Boolean(model.modalities?.input?.includes("image")),
2697
+ reasoning: model.reasoning ?? false,
2698
+ maxContext: model.limit?.context ?? 0,
2699
+ maxOutput: model.limit?.output,
2700
+ knowledge: model.knowledge
2701
+ },
2702
+ cost: model.cost
2703
+ };
2704
+ }
2705
+ async suggestModel(providerId) {
2706
+ const provider = await this.getProvider(providerId);
2707
+ if (!provider || provider.models.length === 0) return void 0;
2708
+ const ranked = [...provider.models].sort((a, b) => {
2709
+ const at = a.release_date ?? a.last_updated ?? "";
2710
+ const bt = b.release_date ?? b.last_updated ?? "";
2711
+ return bt.localeCompare(at);
2712
+ });
2713
+ return ranked[0]?.id;
2714
+ }
2715
+ async ageSeconds() {
2716
+ if (!this.fetchedAt) {
2717
+ const cached = await this.readCache();
2718
+ if (!cached) return Number.POSITIVE_INFINITY;
2719
+ return (Date.now() - new Date(cached.fetchedAt).getTime()) / 1e3;
2720
+ }
2721
+ return (Date.now() - this.fetchedAt.getTime()) / 1e3;
2722
+ }
2723
+ resolveProvider(p) {
2724
+ return {
2725
+ id: p.id,
2726
+ name: p.name,
2727
+ family: classifyFamily(p.npm),
2728
+ apiBase: p.api,
2729
+ envVars: p.env ?? [],
2730
+ doc: p.doc,
2731
+ models: Object.values(p.models ?? {}),
2732
+ npm: p.npm
2733
+ };
2734
+ }
2735
+ isFresh(fetchedAtIso) {
2736
+ return Date.now() - new Date(fetchedAtIso).getTime() < this.ttlMs;
2737
+ }
2738
+ isWithinMaxStaleAge(fetchedAtIso) {
2739
+ return Date.now() - new Date(fetchedAtIso).getTime() < this.maxStaleAgeMs;
2740
+ }
2741
+ async readCache() {
2742
+ try {
2743
+ const raw = await fsp.readFile(this.cacheFile, "utf8");
2744
+ return JSON.parse(raw);
2745
+ } catch {
2746
+ return void 0;
2747
+ }
2748
+ }
2749
+ /** Used by `wstack models refresh` to expose where the cache lives. */
2750
+ cacheLocation() {
2751
+ return path2.resolve(this.cacheFile);
2752
+ }
2753
+ };
2754
+
2755
+ // src/types/mode.ts
2756
+ var DEFAULT_MODES = [
2757
+ {
2758
+ id: "default",
2759
+ name: "Default",
2760
+ description: "General-purpose coding assistant",
2761
+ prompt: "",
2762
+ tags: ["general"]
2763
+ },
2764
+ {
2765
+ id: "code-reviewer",
2766
+ name: "Code Reviewer",
2767
+ description: "Focus on code quality, best practices, and potential bugs",
2768
+ prompt: `## Code Reviewer Mode
2769
+
2770
+ When reviewing code:
2771
+ - Look for potential bugs, race conditions, and edge cases
2772
+ - Check for security vulnerabilities (SQL injection, XSS, CSRF, etc.)
2773
+ - Evaluate error handling completeness
2774
+ - Assess code readability and maintainability
2775
+ - Check for performance anti-patterns
2776
+ - Verify test coverage for critical paths
2777
+ - Ensure naming conventions are followed`,
2778
+ tags: ["review", "quality", "security"],
2779
+ toolPreferences: ["read", "grep", "git", "diff", "test"]
2780
+ },
2781
+ {
2782
+ id: "code-auditor",
2783
+ name: "Code Auditor",
2784
+ description: "Security-focused code analysis",
2785
+ prompt: `## Code Auditor Mode
2786
+
2787
+ When auditing code for security:
2788
+ - Identify injection vulnerabilities (SQL, Command, XSS, LDAP)
2789
+ - Check authentication and authorization patterns
2790
+ - Look for sensitive data exposure (secrets, PII in logs)
2791
+ - Verify cryptographic implementations
2792
+ - Check for insecure dependencies or configurations
2793
+ - Assess input validation and output encoding
2794
+ - Look for timing attacks and information leakage`,
2795
+ tags: ["security", "audit", "compliance"],
2796
+ toolPreferences: ["grep", "read", "audit", "bash"]
2797
+ },
2798
+ {
2799
+ id: "architect",
2800
+ name: "Software Architect",
2801
+ description: "Design patterns, scalability, and system design",
2802
+ prompt: `## Architect Mode
2803
+
2804
+ When designing or reviewing architecture:
2805
+ - Evaluate scalability and future growth
2806
+ - Check for appropriate design patterns
2807
+ - Assess coupling and cohesion
2808
+ - Look forSOLID principle violations
2809
+ - Evaluate data modeling decisions
2810
+ - Check for eventual consistency issues
2811
+ - Assess API design and contract stability
2812
+ - Consider operational aspects (monitoring, logging, deployment)`,
2813
+ tags: ["architecture", "design", "scalability"],
2814
+ toolPreferences: ["read", "glob", "tree", "diff"]
2815
+ },
2816
+ {
2817
+ id: "debugger",
2818
+ name: "Debugger",
2819
+ description: "Root cause analysis and error investigation",
2820
+ prompt: `## Debugger Mode
2821
+
2822
+ When investigating bugs:
2823
+ - Reproduce the issue with minimal steps
2824
+ - Check error messages and stack traces thoroughly
2825
+ - Look for related logs and historical context
2826
+ - Verify assumptions about data flow
2827
+ - Check for race conditions in async code
2828
+ - Validate environment and configuration
2829
+ - Use binary search to isolate the root cause
2830
+ - Verify fixes with tests before considering done`,
2831
+ tags: ["debug", "investigation", "error-resolution"],
2832
+ toolPreferences: ["read", "grep", "bash", "logs", "test"]
2833
+ },
2834
+ {
2835
+ id: "tester",
2836
+ name: "QA Engineer",
2837
+ description: "Test coverage, edge cases, and quality assurance",
2838
+ prompt: `## Tester Mode
2839
+
2840
+ When testing or writing tests:
2841
+ - Cover happy path and error paths equally
2842
+ - Think about edge cases and boundary conditions
2843
+ - Check for missing null/undefined handling tests
2844
+ - Verify error messages are tested
2845
+ - Look for race condition tests in async code
2846
+ - Assess mutation testing opportunities
2847
+ - Check for integration test gaps
2848
+ - Verify test isolation and cleanup`,
2849
+ tags: ["testing", "qa", "quality"],
2850
+ toolPreferences: ["read", "grep", "test", "bash"]
2851
+ },
2852
+ {
2853
+ id: "devops",
2854
+ name: "DevOps Engineer",
2855
+ description: "Infrastructure, deployment, and operations",
2856
+ prompt: `## DevOps Mode
2857
+
2858
+ When working on infrastructure:
2859
+ - Check for containerization and deployment readiness
2860
+ - Verify CI/CD pipeline configurations
2861
+ - Assess monitoring and alerting setup
2862
+ - Look for health check endpoints
2863
+ - Check for graceful shutdown handling
2864
+ - Verify backup and disaster recovery plans
2865
+ - Assess secrets management
2866
+ - Check for resource limits and quotas`,
2867
+ tags: ["devops", "infrastructure", "operations"],
2868
+ toolPreferences: ["read", "bash", "grep", "logs", "git"]
2869
+ },
2870
+ {
2871
+ id: "refactorer",
2872
+ name: "Refactorer",
2873
+ description: "Code improvement and modernization",
2874
+ prompt: `## Refactorer Mode
2875
+
2876
+ When refactoring code:
2877
+ - Maintain existing behavior \u2014 tests must pass before and after
2878
+ - Make one change at a time, verify after each
2879
+ - Prefer small, focused commits
2880
+ - Preserve API contracts unless explicitly changing
2881
+ - Remove dead code and comments
2882
+ - Improve naming as you go
2883
+ - Don't mix formatting changes with logic changes
2884
+ - Keep performance in mind \u2014 don't regress`,
2885
+ tags: ["refactor", "modernization", "improvement"],
2886
+ toolPreferences: ["read", "edit", "test", "git", "grep"]
2887
+ }
2888
+ ];
2889
+
2890
+ // src/defaults/mode-store.ts
2891
+ var DefaultModeStore = class {
2892
+ activeModeId = null;
2893
+ modes;
2894
+ configDir;
2895
+ constructor(config) {
2896
+ this.configDir = config.directory;
2897
+ this.modes = [...DEFAULT_MODES];
2898
+ }
2899
+ async getActiveMode() {
2900
+ if (!this.activeModeId) {
2901
+ await this.loadActiveMode();
2902
+ }
2903
+ if (!this.activeModeId) return null;
2904
+ return this.modes.find((m) => m.id === this.activeModeId) ?? null;
2905
+ }
2906
+ async setActiveMode(modeId) {
2907
+ this.activeModeId = modeId;
2908
+ await this.saveActiveMode();
2909
+ }
2910
+ async listModes() {
2911
+ return [...this.modes];
2912
+ }
2913
+ async getMode(modeId) {
2914
+ return this.modes.find((m) => m.id === modeId) ?? null;
2915
+ }
2916
+ async addMode(mode) {
2917
+ const idx = this.modes.findIndex((m) => m.id === mode.id);
2918
+ if (idx >= 0) {
2919
+ this.modes[idx] = mode;
2920
+ } else {
2921
+ this.modes.push(mode);
2922
+ }
2923
+ }
2924
+ async removeMode(modeId) {
2925
+ const builtIn = DEFAULT_MODES.find((m) => m.id === modeId);
2926
+ if (builtIn) {
2927
+ throw new Error(`Cannot remove built-in mode "${modeId}"`);
2928
+ }
2929
+ this.modes = this.modes.filter((m) => m.id !== modeId);
2930
+ if (this.activeModeId === modeId) {
2931
+ this.activeModeId = null;
2932
+ await this.saveActiveMode();
2933
+ }
2934
+ }
2935
+ async loadActiveMode() {
2936
+ try {
2937
+ const configPath = path2.join(this.configDir, "mode.json");
2938
+ const content = await fsp.readFile(configPath, "utf8");
2939
+ const data = JSON.parse(content);
2940
+ this.activeModeId = data.activeMode ?? null;
2941
+ } catch {
2942
+ this.activeModeId = "default";
2943
+ }
2944
+ }
2945
+ async saveActiveMode() {
2946
+ try {
2947
+ await fsp.mkdir(this.configDir, { recursive: true });
2948
+ const configPath = path2.join(this.configDir, "mode.json");
2949
+ await fsp.writeFile(
2950
+ configPath,
2951
+ JSON.stringify({ activeMode: this.activeModeId }, null, 2),
2952
+ "utf8"
2953
+ );
2954
+ } catch {
2955
+ }
2956
+ }
2957
+ };
2958
+ async function loadProjectModes(modesDir) {
2959
+ const modes = [];
2960
+ try {
2961
+ const entries = await fsp.readdir(modesDir);
2962
+ for (const entry of entries) {
2963
+ if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
2964
+ const filePath = path2.join(modesDir, entry);
2965
+ const stat4 = await fsp.stat(filePath);
2966
+ if (!stat4.isFile()) continue;
2967
+ const content = await fsp.readFile(filePath, "utf8");
2968
+ const id = path2.basename(entry, path2.extname(entry));
2969
+ modes.push({
2970
+ id,
2971
+ name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
2972
+ description: content.split("\n")[0] ?? id,
2973
+ prompt: content,
2974
+ tags: ["project"]
2975
+ });
2976
+ }
2977
+ } catch {
2978
+ }
2979
+ return modes;
2980
+ }
2981
+ async function loadUserModes(modesDir) {
2982
+ const modes = [];
2983
+ try {
2984
+ const manifestPath = path2.join(modesDir, "modes.json");
2985
+ const content = await fsp.readFile(manifestPath, "utf8");
2986
+ const manifest = JSON.parse(content);
2987
+ for (const mode of manifest.modes) {
2988
+ modes.push(mode);
2989
+ }
2990
+ } catch {
2991
+ }
2992
+ return modes;
2993
+ }
2994
+ var DefaultMultiAgentCoordinator = class extends EventEmitter {
2995
+ coordinatorId;
2996
+ config;
2997
+ subagents = /* @__PURE__ */ new Map();
2998
+ pendingTasks = [];
2999
+ completedResults = [];
3000
+ totalIterations = 0;
3001
+ constructor(config) {
3002
+ super();
3003
+ this.coordinatorId = config.coordinatorId;
3004
+ this.config = config;
3005
+ }
3006
+ async spawn(subagent) {
3007
+ const id = subagent.id || randomUUID();
3008
+ const context = {
3009
+ subagentId: id,
3010
+ tasks: [],
3011
+ parentBridge: null,
3012
+ doneCondition: this.config.doneCondition,
3013
+ maxConcurrent: this.config.maxConcurrent ?? 4
3014
+ };
3015
+ this.subagents.set(id, {
3016
+ config: subagent,
3017
+ context,
3018
+ status: "idle"
3019
+ });
3020
+ this.emit("subagent.started", { subagent: { ...subagent, id } });
3021
+ return {
3022
+ subagentId: id,
3023
+ agentId: id
3024
+ };
3025
+ }
3026
+ async assign(task) {
3027
+ this.pendingTasks.push(task);
3028
+ const available = this.getAvailableSubagent();
3029
+ if (available) {
3030
+ await this.dispatch(available, task);
3031
+ }
3032
+ }
3033
+ async delegate(to, msg) {
3034
+ const subagent = this.subagents.get(to);
3035
+ if (!subagent) throw new Error(`Subagent "${to}" not found`);
3036
+ if (!subagent.context.parentBridge) {
3037
+ throw new Error(`Subagent "${to}" has no parentBridge \u2014 call setSubagentBridge() first`);
3038
+ }
3039
+ await subagent.context.parentBridge.send(msg);
3040
+ }
3041
+ /**
3042
+ * Wire up the communication bridge for a subagent. Call this after `spawn()`
3043
+ * once the caller has created the bidirectional bridge connection.
3044
+ */
3045
+ setSubagentBridge(subagentId, bridge) {
3046
+ const subagent = this.subagents.get(subagentId);
3047
+ if (!subagent) throw new Error(`Subagent "${subagentId}" not found`);
3048
+ subagent.context.parentBridge = bridge;
3049
+ }
3050
+ async stop(subagentId) {
3051
+ const subagent = this.subagents.get(subagentId);
3052
+ if (!subagent) return;
3053
+ subagent.status = "stopped";
3054
+ subagent.currentTask = void 0;
3055
+ subagent.context.parentBridge = null;
3056
+ this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
3057
+ }
3058
+ async stopAll() {
3059
+ for (const id of this.subagents.keys()) {
3060
+ await this.stop(id);
3061
+ }
3062
+ }
3063
+ getStatus() {
3064
+ return {
3065
+ coordinatorId: this.coordinatorId,
3066
+ subagents: Array.from(this.subagents.entries()).map(([id, s]) => ({
3067
+ id,
3068
+ name: s.config.name,
3069
+ status: s.status,
3070
+ currentTask: s.currentTask
3071
+ })),
3072
+ pendingTasks: this.pendingTasks.length,
3073
+ completedTasks: this.completedResults.length,
3074
+ totalIterations: this.totalIterations,
3075
+ done: this.isDone()
3076
+ };
3077
+ }
3078
+ getAvailableSubagent() {
3079
+ for (const [id, s] of this.subagents) {
3080
+ if (s.status === "idle") return id;
3081
+ }
3082
+ return null;
3083
+ }
3084
+ async dispatch(subagentId, task) {
3085
+ const subagent = this.subagents.get(subagentId);
3086
+ if (!subagent) return;
3087
+ subagent.status = "running";
3088
+ subagent.currentTask = task.id;
3089
+ task.subagentId = subagentId;
3090
+ subagent.context.tasks.push(task);
3091
+ if (!subagent.context.parentBridge) {
3092
+ this.emit("task.assigned", { task, subagentId });
3093
+ return;
3094
+ }
3095
+ await subagent.context.parentBridge.send({
3096
+ id: randomUUID(),
3097
+ type: "task",
3098
+ from: this.coordinatorId,
3099
+ to: subagentId,
3100
+ payload: task,
3101
+ timestamp: Date.now()
3102
+ });
3103
+ this.emit("task.assigned", { task, subagentId });
3104
+ }
3105
+ isDone() {
3106
+ if (this.config.doneCondition.type === "all_tasks_done") {
3107
+ return this.pendingTasks.length === 0 && this.completedResults.every((r) => r.status === "success");
3108
+ }
3109
+ if (this.config.doneCondition.maxIterations && this.totalIterations >= this.config.doneCondition.maxIterations) {
3110
+ return true;
3111
+ }
3112
+ return false;
3113
+ }
3114
+ completeTask(result) {
3115
+ this.completedResults.push(result);
3116
+ this.totalIterations += result.iterations;
3117
+ const subagent = this.subagents.get(result.subagentId);
3118
+ if (subagent) {
3119
+ subagent.status = "idle";
3120
+ subagent.currentTask = void 0;
3121
+ }
3122
+ this.emit("task.completed", {
3123
+ task: this.pendingTasks.shift(),
3124
+ result
3125
+ });
3126
+ if (this.pendingTasks.length > 0) {
3127
+ const available = this.getAvailableSubagent();
3128
+ if (available) {
3129
+ const nextTask = this.pendingTasks.shift();
3130
+ this.dispatch(available, nextTask);
3131
+ }
3132
+ } else if (this.isDone()) {
3133
+ this.emit("done", {
3134
+ results: this.completedResults,
3135
+ totalIterations: this.totalIterations
3136
+ });
3137
+ }
3138
+ }
3139
+ };
3140
+ var InMemoryBridgeTransport = class {
3141
+ subs = /* @__PURE__ */ new Map();
3142
+ send(msg, to) {
3143
+ const handlers = this.subs.get(to);
3144
+ if (handlers) {
3145
+ for (const h of handlers) {
3146
+ try {
3147
+ h(msg);
3148
+ } catch {
3149
+ }
3150
+ }
3151
+ }
3152
+ return Promise.resolve();
3153
+ }
3154
+ subscribe(agentId, handler) {
3155
+ if (!this.subs.has(agentId)) this.subs.set(agentId, /* @__PURE__ */ new Set());
3156
+ this.subs.get(agentId).add(handler);
3157
+ return () => this.subs.get(agentId)?.delete(handler);
3158
+ }
3159
+ close(agentId) {
3160
+ this.subs.delete(agentId);
3161
+ return Promise.resolve();
3162
+ }
3163
+ };
3164
+ var InMemoryAgentBridge = class {
3165
+ agentId;
3166
+ coordinatorId;
3167
+ transport;
3168
+ subscriptions = /* @__PURE__ */ new Set();
3169
+ pendingRequests = /* @__PURE__ */ new Map();
3170
+ stopped = false;
3171
+ timeoutMs;
3172
+ constructor(config, transport) {
3173
+ this.agentId = config.agentId;
3174
+ this.coordinatorId = config.coordinatorId;
3175
+ this.transport = transport;
3176
+ this.timeoutMs = config.timeoutMs ?? 3e4;
3177
+ this.transport.subscribe(this.agentId, (msg) => {
3178
+ if (msg.type === "heartbeat") return;
3179
+ const pending = this.pendingRequests.get(msg.id);
3180
+ if (pending) {
3181
+ clearTimeout(pending.timer);
3182
+ this.pendingRequests.delete(msg.id);
3183
+ pending.resolve(msg);
3184
+ return;
3185
+ }
3186
+ for (const h of this.subscriptions) {
3187
+ try {
3188
+ h(msg);
3189
+ } catch {
3190
+ }
3191
+ }
3192
+ });
3193
+ }
3194
+ async send(msg) {
3195
+ msg.timestamp = Date.now();
3196
+ await this.transport.send(msg, msg.to ?? this.coordinatorId);
3197
+ }
3198
+ async broadcast(msg) {
3199
+ msg.timestamp = Date.now();
3200
+ msg.to = "*";
3201
+ await this.transport.send(msg, "*");
3202
+ }
3203
+ subscribe(handler) {
3204
+ this.subscriptions.add(handler);
3205
+ return () => this.subscriptions.delete(handler);
3206
+ }
3207
+ async request(msg, timeoutMs) {
3208
+ if (this.stopped) throw new Error("Bridge is stopped");
3209
+ const timeout = timeoutMs ?? this.timeoutMs;
3210
+ const correlationId = msg.id;
3211
+ return new Promise((resolve3, reject) => {
3212
+ const timer = setTimeout(() => {
3213
+ this.pendingRequests.delete(correlationId);
3214
+ reject(new Error(`Request ${correlationId} timed out after ${timeout}ms`));
3215
+ }, timeout);
3216
+ this.pendingRequests.set(correlationId, { resolve: resolve3, reject, timer });
3217
+ msg.timestamp = Date.now();
3218
+ this.transport.send(msg, msg.to ?? this.coordinatorId).catch((e) => {
3219
+ clearTimeout(timer);
3220
+ this.pendingRequests.delete(correlationId);
3221
+ reject(e);
3222
+ });
3223
+ });
3224
+ }
3225
+ async stop() {
3226
+ this.stopped = true;
3227
+ for (const [, p] of this.pendingRequests) {
3228
+ clearTimeout(p.timer);
3229
+ }
3230
+ this.pendingRequests.clear();
3231
+ this.subscriptions.clear();
3232
+ await this.transport.close(this.agentId);
3233
+ }
3234
+ };
3235
+ function createMessage(type, from, payload, to) {
3236
+ return {
3237
+ id: randomUUID(),
3238
+ type,
3239
+ from,
3240
+ to,
3241
+ payload,
3242
+ timestamp: Date.now(),
3243
+ priority: "normal"
3244
+ };
3245
+ }
3246
+
3247
+ // src/defaults/autonomous-runner.ts
3248
+ var DoneConditionChecker = class {
3249
+ constructor(condition) {
3250
+ this.condition = condition;
3251
+ }
3252
+ condition;
3253
+ check(state) {
3254
+ switch (this.condition.type) {
3255
+ case "iterations":
3256
+ if (this.condition.maxIterations && state.iterations >= this.condition.maxIterations) {
3257
+ return { done: true, reason: `max iterations (${this.condition.maxIterations}) reached`, ...state };
3258
+ }
3259
+ break;
3260
+ case "tool_calls":
3261
+ if (this.condition.maxToolCalls && state.toolCalls >= this.condition.maxToolCalls) {
3262
+ return { done: true, reason: `max tool calls (${this.condition.maxToolCalls}) reached`, ...state };
3263
+ }
3264
+ break;
3265
+ case "output_match":
3266
+ if (this.condition.pattern && state.lastOutput) {
3267
+ const regex = new RegExp(this.condition.pattern);
3268
+ if (regex.test(state.lastOutput)) {
3269
+ return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
3270
+ }
3271
+ }
3272
+ break;
3273
+ }
3274
+ return { done: false, iterations: state.iterations, toolCalls: state.toolCalls };
3275
+ }
3276
+ };
3277
+ var AutonomousRunner = class {
3278
+ constructor(opts) {
3279
+ this.opts = opts;
3280
+ this.doneChecker = new DoneConditionChecker(opts.doneCondition);
3281
+ }
3282
+ opts;
3283
+ iterations = 0;
3284
+ toolCalls = 0;
3285
+ lastOutput;
3286
+ stopped = false;
3287
+ doneChecker;
3288
+ async run() {
3289
+ while (!this.stopped) {
3290
+ const check = this.doneChecker.check({
3291
+ iterations: this.iterations,
3292
+ toolCalls: this.toolCalls,
3293
+ lastOutput: this.lastOutput
3294
+ });
3295
+ if (check.done) {
3296
+ const result = {
3297
+ status: "done",
3298
+ iterations: this.iterations,
3299
+ toolCalls: this.toolCalls,
3300
+ reason: check.reason
3301
+ };
3302
+ this.opts.onDone?.(result);
3303
+ return result;
3304
+ }
3305
+ this.opts.onIteration?.({ iteration: this.iterations, toolCalls: this.toolCalls });
3306
+ const ctrl = new AbortController();
3307
+ const timeout = setTimeout(() => ctrl.abort(), this.opts.iterationTimeoutMs ?? 3e4);
3308
+ try {
3309
+ const result = await this.opts.agent.run(
3310
+ "",
3311
+ { signal: ctrl.signal, maxIterations: 1, executionStrategy: "sequential" }
3312
+ );
3313
+ this.iterations++;
3314
+ this.lastOutput = result.finalText;
3315
+ this.toolCalls++;
3316
+ if (result.status === "failed" || result.status === "aborted") {
3317
+ const failedResult = {
3318
+ status: result.status,
3319
+ error: result.error,
3320
+ iterations: this.iterations,
3321
+ toolCalls: this.toolCalls
3322
+ };
3323
+ this.opts.onDone?.(failedResult);
3324
+ return failedResult;
3325
+ }
3326
+ } catch (e) {
3327
+ if (e.message.includes("timeout")) {
3328
+ const timeoutResult = {
3329
+ status: "failed",
3330
+ error: e,
3331
+ iterations: this.iterations,
3332
+ toolCalls: this.toolCalls,
3333
+ reason: "iteration timeout"
3334
+ };
3335
+ this.opts.onDone?.(timeoutResult);
3336
+ return timeoutResult;
3337
+ }
3338
+ } finally {
3339
+ clearTimeout(timeout);
3340
+ }
3341
+ }
3342
+ return {
3343
+ status: "aborted",
3344
+ iterations: this.iterations,
3345
+ toolCalls: this.toolCalls,
3346
+ reason: "stopped externally"
3347
+ };
3348
+ }
3349
+ stop() {
3350
+ this.stopped = true;
3351
+ }
3352
+ };
3353
+
3354
+ // src/defaults/spec-parser.ts
3355
+ var SpecParser = class {
3356
+ constructor(opts = {}) {
3357
+ this.opts = opts;
3358
+ }
3359
+ opts;
3360
+ parse(content) {
3361
+ const lines = content.split("\n");
3362
+ const sections = this.extractSections(lines);
3363
+ const requirements = this.extractRequirements(lines);
3364
+ return {
3365
+ id: crypto.randomUUID(),
3366
+ title: this.extractTitle(lines),
3367
+ version: this.extractVersion(lines),
3368
+ status: "draft",
3369
+ overview: this.extractOverview(lines),
3370
+ sections,
3371
+ requirements,
3372
+ createdAt: Date.now(),
3373
+ updatedAt: Date.now()
3374
+ };
3375
+ }
3376
+ extractTitle(lines) {
3377
+ for (const line of lines) {
3378
+ const m = /^#\s+(.+)/.exec(line.trim());
3379
+ if (m?.[1]) return m[1];
3380
+ }
3381
+ return "Untitled Specification";
3382
+ }
3383
+ extractVersion(lines) {
3384
+ for (const line of lines) {
3385
+ const m = /version[:\s]+(\d+\.\d+\.\d+)/i.exec(line.trim());
3386
+ if (m?.[1]) return m[1];
3387
+ }
3388
+ return "0.0.1";
3389
+ }
3390
+ extractOverview(lines) {
3391
+ const overviewLines = [];
3392
+ let inOverview = false;
3393
+ let foundHeading = false;
3394
+ for (const line of lines) {
3395
+ if (/^##\s+Overview/i.test(line.trim())) {
3396
+ inOverview = true;
3397
+ foundHeading = true;
3398
+ continue;
3399
+ }
3400
+ if (foundHeading && /^##\s+/.test(line.trim())) break;
3401
+ if (inOverview) overviewLines.push(line);
3402
+ }
3403
+ return overviewLines.join("\n").trim() || "No overview provided";
3404
+ }
3405
+ extractSections(lines) {
3406
+ const sections = [];
3407
+ let currentSection = null;
3408
+ let currentLines = [];
3409
+ let depth = 1;
3410
+ for (const line of lines) {
3411
+ const h2 = /^##\s+(.+)/.exec(line.trim());
3412
+ const h3 = /^###\s+(.+)/.exec(line.trim());
3413
+ if (h2) {
3414
+ if (currentSection && currentLines.length > 0) {
3415
+ sections.push({
3416
+ type: this.mapSectionType(currentSection.title ?? "unknown"),
3417
+ title: currentSection.title ?? "Unknown",
3418
+ level: depth,
3419
+ content: currentLines.join("\n").trim()
3420
+ });
3421
+ }
3422
+ currentSection = { title: h2[1] ?? "Unknown" };
3423
+ currentLines = [];
3424
+ depth = 2;
3425
+ continue;
3426
+ }
3427
+ if (h3) {
3428
+ currentLines.push(line);
3429
+ continue;
3430
+ }
3431
+ if (currentSection) {
3432
+ currentLines.push(line);
3433
+ }
3434
+ }
3435
+ if (currentSection && currentLines.length > 0) {
3436
+ sections.push({
3437
+ type: this.mapSectionType(currentSection.title ?? "unknown"),
3438
+ title: currentSection.title ?? "Unknown",
3439
+ level: depth,
3440
+ content: currentLines.join("\n").trim()
3441
+ });
3442
+ }
3443
+ return sections;
3444
+ }
3445
+ extractRequirements(lines) {
3446
+ const requirements = [];
3447
+ let inRequirements = false;
3448
+ let idCounter = 0;
3449
+ for (const line of lines) {
3450
+ if (/^##\s+Requirements/i.test(line.trim())) {
3451
+ inRequirements = true;
3452
+ continue;
3453
+ }
3454
+ if (inRequirements && /^##\s+/.test(line.trim())) break;
3455
+ if (inRequirements) {
3456
+ const req = this.parseRequirementLine(line, `REQ-${++idCounter}`);
3457
+ if (req) requirements.push(req);
3458
+ }
3459
+ }
3460
+ return requirements;
3461
+ }
3462
+ parseRequirementLine(line, id) {
3463
+ const trimmed = line.trim();
3464
+ if (!trimmed || trimmed.startsWith("#")) return null;
3465
+ const typeMap = {
3466
+ "functional": "functional",
3467
+ "non-functional": "non-functional",
3468
+ "security": "security",
3469
+ "performance": "performance",
3470
+ "ux": "ux"
3471
+ };
3472
+ let type = "functional";
3473
+ let priority = "medium";
3474
+ for (const [key, val] of Object.entries(typeMap)) {
3475
+ if (trimmed.toLowerCase().includes(`[${key}]`)) {
3476
+ type = val;
3477
+ }
3478
+ }
3479
+ if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
3480
+ priority = "critical";
3481
+ } else if (trimmed.includes("[high]")) {
3482
+ priority = "high";
3483
+ } else if (trimmed.includes("[low]")) {
3484
+ priority = "low";
3485
+ }
3486
+ return {
3487
+ id,
3488
+ type,
3489
+ priority,
3490
+ description: trimmed.replace(/\[[^\]]+\]/g, "").trim(),
3491
+ acceptanceCriteria: []
3492
+ };
3493
+ }
3494
+ mapSectionType(title) {
3495
+ const t = title.toLowerCase();
3496
+ if (t.includes("overview")) return "overview";
3497
+ if (t.includes("requirement")) return "requirements";
3498
+ if (t.includes("architect")) return "architecture";
3499
+ if (t.includes("api")) return "api";
3500
+ if (t.includes("data")) return "data";
3501
+ if (t.includes("security")) return "security";
3502
+ if (t.includes("acceptance")) return "acceptance";
3503
+ return "overview";
3504
+ }
3505
+ analyze(spec) {
3506
+ const gaps = [];
3507
+ const suggestions = [];
3508
+ const risks = [];
3509
+ const hasOverview = spec.sections.some((s) => s.type === "overview");
3510
+ const hasRequirements = spec.sections.some((s) => s.type === "requirements");
3511
+ const hasAcceptance = spec.sections.some((s) => s.type === "acceptance");
3512
+ if (!hasOverview) gaps.push("Missing Overview section");
3513
+ if (!hasRequirements) gaps.push("Missing Requirements section");
3514
+ if (!hasAcceptance) gaps.push("Missing Acceptance Criteria section");
3515
+ if (spec.requirements.length === 0) {
3516
+ gaps.push("No requirements defined");
3517
+ suggestions.push("Add specific functional and non-functional requirements");
3518
+ }
3519
+ const unverifiedReqs = spec.requirements.filter((r) => r.acceptanceCriteria.length === 0);
3520
+ if (unverifiedReqs.length > 0) {
3521
+ gaps.push(`${unverifiedReqs.length} requirements without acceptance criteria`);
3522
+ suggestions.push("Define clear acceptance criteria for each requirement");
3523
+ }
3524
+ const criticalUnresolved = spec.requirements.filter(
3525
+ (r) => r.priority === "critical" && r.blockedBy && r.blockedBy.length > 0
3526
+ );
3527
+ for (const req of criticalUnresolved) {
3528
+ risks.push({
3529
+ requirement: req.id,
3530
+ risk: `Critical requirement blocked by ${req.blockedBy?.length} other requirements`,
3531
+ severity: "high"
3532
+ });
3533
+ }
3534
+ const completeness = Math.round(
3535
+ ((hasOverview ? 1 : 0) + (hasRequirements ? 1 : 0) + (hasAcceptance ? 1 : 0) + (spec.requirements.length > 0 ? 1 : 0) + (spec.sections.length > 3 ? 1 : 0)) / 5 * 100
3536
+ );
3537
+ return {
3538
+ specId: spec.id,
3539
+ completeness,
3540
+ coverage: {
3541
+ requirements: spec.requirements.length,
3542
+ apiEndpoints: spec.apiEndpoints?.length ?? 0,
3543
+ edgeCases: 0,
3544
+ errorHandling: 0
3545
+ },
3546
+ gaps,
3547
+ risks,
3548
+ suggestions
3549
+ };
3550
+ }
3551
+ validate(spec) {
3552
+ const errors = [];
3553
+ const warnings = [];
3554
+ if (!spec.title.trim()) {
3555
+ errors.push({ path: "title", message: "Title is required" });
3556
+ }
3557
+ if (!spec.version.trim()) {
3558
+ errors.push({ path: "version", message: "Version is required" });
3559
+ }
3560
+ for (const req of spec.requirements) {
3561
+ if (!req.description.trim()) {
3562
+ errors.push({ path: `requirement.${req.id}`, message: "Requirement description is empty" });
3563
+ }
3564
+ if (req.acceptanceCriteria.length === 0) {
3565
+ warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
3566
+ }
3567
+ }
3568
+ const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
3569
+ for (const id of blockedByIds) {
3570
+ if (!spec.requirements.find((r) => r.id === id)) {
3571
+ errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
3572
+ }
3573
+ }
3574
+ return {
3575
+ valid: errors.length === 0,
3576
+ errors,
3577
+ warnings
3578
+ };
3579
+ }
3580
+ };
3581
+
3582
+ // src/defaults/task-generator.ts
3583
+ var TaskGenerator = class {
3584
+ constructor(opts) {
3585
+ this.opts = opts;
3586
+ }
3587
+ opts;
3588
+ async generateFromSpec(spec) {
3589
+ const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
3590
+ const overview = spec.sections.find((s) => s.type === "overview");
3591
+ if (overview) {
3592
+ this.opts.taskTracker.addNode({
3593
+ title: `Implement ${spec.title}`,
3594
+ description: overview.content,
3595
+ type: "feature",
3596
+ priority: "high",
3597
+ status: "pending"
3598
+ });
3599
+ }
3600
+ const criticalReqs = spec.requirements.filter((r) => r.priority === "critical");
3601
+ const highReqs = spec.requirements.filter((r) => r.priority === "high");
3602
+ const mediumReqs = spec.requirements.filter((r) => r.priority === "medium");
3603
+ const lowReqs = spec.requirements.filter((r) => r.priority === "low");
3604
+ for (const req of criticalReqs) {
3605
+ const task = this.createTaskFromRequirement(req, spec.title);
3606
+ this.opts.taskTracker.addNode(task);
3607
+ }
3608
+ for (const req of highReqs) {
3609
+ const task = this.createTaskFromRequirement(req, spec.title);
3610
+ this.opts.taskTracker.addNode(task);
3611
+ }
3612
+ for (const req of mediumReqs) {
3613
+ const task = this.createTaskFromRequirement(req, spec.title);
3614
+ this.opts.taskTracker.addNode(task);
3615
+ }
3616
+ for (const req of lowReqs) {
3617
+ const task = this.createTaskFromRequirement(req, spec.title);
3618
+ this.opts.taskTracker.addNode(task);
3619
+ }
3620
+ if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
3621
+ const apiParent = this.opts.taskTracker.addNode({
3622
+ title: "API Implementation",
3623
+ description: `Implement ${spec.apiEndpoints.length} API endpoints`,
3624
+ type: "feature",
3625
+ priority: "high",
3626
+ status: "pending"
3627
+ });
3628
+ for (const endpoint of spec.apiEndpoints) {
3629
+ const task = this.createTaskFromEndpoint(endpoint);
3630
+ this.opts.taskTracker.addNode({
3631
+ ...task,
3632
+ parentId: apiParent.id
3633
+ });
3634
+ }
3635
+ }
3636
+ this.opts.taskTracker.addNode({
3637
+ title: "Write Tests",
3638
+ description: "Comprehensive test coverage for all features",
3639
+ type: "test",
3640
+ priority: "high",
3641
+ status: "pending"
3642
+ });
3643
+ this.opts.taskTracker.addNode({
3644
+ title: "Update Documentation",
3645
+ description: "Update docs for new features",
3646
+ type: "docs",
3647
+ priority: "medium",
3648
+ status: "pending"
3649
+ });
3650
+ return graph;
3651
+ }
3652
+ createTaskFromRequirement(req, specTitle) {
3653
+ const type = this.mapRequirementType(req.type);
3654
+ const tags = [req.type, req.priority];
3655
+ return {
3656
+ title: req.description,
3657
+ description: this.buildDescription(req, specTitle),
3658
+ type,
3659
+ priority: this.mapPriority(req.priority),
3660
+ status: "pending",
3661
+ specRequirementId: req.id,
3662
+ tags,
3663
+ estimateHours: this.estimateHours(req)
3664
+ };
3665
+ }
3666
+ createTaskFromEndpoint(endpoint) {
3667
+ return {
3668
+ title: `${endpoint.method} ${endpoint.path}`,
3669
+ description: endpoint.description,
3670
+ type: "feature",
3671
+ priority: "high",
3672
+ status: "pending",
3673
+ tags: [endpoint.method],
3674
+ estimateHours: this.estimateForEndpoint(endpoint)
3675
+ };
3676
+ }
3677
+ buildDescription(req, specTitle) {
3678
+ const lines = [
3679
+ req.description,
3680
+ "",
3681
+ "**Type:** " + req.type,
3682
+ "**Priority:** " + req.priority
3683
+ ];
3684
+ if (req.acceptanceCriteria.length > 0) {
3685
+ lines.push("", "**Acceptance Criteria:**");
3686
+ for (const criterion of req.acceptanceCriteria) {
3687
+ lines.push(`- ${criterion}`);
3688
+ }
3689
+ }
3690
+ if (req.blockedBy && req.blockedBy.length > 0) {
3691
+ lines.push("", `**Blocked by:** ${req.blockedBy.join(", ")}`);
3692
+ }
3693
+ return lines.join("\n");
3694
+ }
3695
+ mapRequirementType(type) {
3696
+ switch (type) {
3697
+ case "functional":
3698
+ return "feature";
3699
+ case "non-functional":
3700
+ return "feature";
3701
+ case "security":
3702
+ return "feature";
3703
+ case "performance":
3704
+ return "feature";
3705
+ case "ux":
3706
+ return "feature";
3707
+ default:
3708
+ return "feature";
3709
+ }
3710
+ }
3711
+ mapPriority(priority) {
3712
+ switch (priority) {
3713
+ case "critical":
3714
+ return "critical";
3715
+ case "high":
3716
+ return "high";
3717
+ case "medium":
3718
+ return "medium";
3719
+ case "low":
3720
+ return "low";
3721
+ default:
3722
+ return "medium";
3723
+ }
3724
+ }
3725
+ estimateHours(req) {
3726
+ switch (req.priority) {
3727
+ case "critical":
3728
+ return 8;
3729
+ case "high":
3730
+ return 4;
3731
+ case "medium":
3732
+ return 2;
3733
+ case "low":
3734
+ return 1;
3735
+ default:
3736
+ return 2;
3737
+ }
3738
+ }
3739
+ estimateForEndpoint(endpoint) {
3740
+ let hours = 2;
3741
+ if (endpoint.auth) hours += 1;
3742
+ if (endpoint.request) hours += 1;
3743
+ return hours;
3744
+ }
3745
+ async generateSubtasks(parentTaskId, spec) {
3746
+ const reqId = this.opts.taskTracker.getNode(parentTaskId)?.specRequirementId;
3747
+ if (!reqId) return;
3748
+ const req = spec.requirements.find((r) => r.id === reqId);
3749
+ if (!req) return;
3750
+ if (req.acceptanceCriteria.length > 0) {
3751
+ for (const criterion of req.acceptanceCriteria) {
3752
+ this.opts.taskTracker.addNode({
3753
+ title: criterion,
3754
+ description: `Verify: ${criterion}`,
3755
+ type: "test",
3756
+ priority: "medium",
3757
+ status: "pending",
3758
+ parentId: parentTaskId
3759
+ });
3760
+ }
3761
+ }
3762
+ }
3763
+ };
3764
+ var DefaultTaskStore = class {
3765
+ graphs = /* @__PURE__ */ new Map();
3766
+ async saveGraph(graph) {
3767
+ this.graphs.set(graph.id, this.cloneGraph(graph));
3768
+ }
3769
+ async loadGraph(id) {
3770
+ const g = this.graphs.get(id);
3771
+ return g ? this.cloneGraph(g) : null;
3772
+ }
3773
+ async listGraphs() {
3774
+ return Array.from(this.graphs.values()).map((g) => ({
3775
+ id: g.id,
3776
+ title: g.title,
3777
+ updatedAt: g.updatedAt
3778
+ }));
3779
+ }
3780
+ async deleteGraph(id) {
3781
+ this.graphs.delete(id);
3782
+ }
3783
+ cloneGraph(g) {
3784
+ return {
3785
+ ...g,
3786
+ nodes: new Map(g.nodes),
3787
+ edges: [...g.edges],
3788
+ rootNodes: [...g.rootNodes]
3789
+ };
3790
+ }
3791
+ };
3792
+
3793
+ // src/types/task-graph.ts
3794
+ function computeTaskProgress(graph) {
3795
+ const nodes = Array.from(graph.nodes.values());
3796
+ const total = nodes.length;
3797
+ const completed = nodes.filter((n) => n.status === "completed").length;
3798
+ const pending = nodes.filter((n) => n.status === "pending").length;
3799
+ const inProgress = nodes.filter((n) => n.status === "in_progress").length;
3800
+ const blocked = nodes.filter((n) => n.status === "blocked").length;
3801
+ const failed = nodes.filter((n) => n.status === "failed").length;
3802
+ const review = nodes.filter((n) => n.status === "review").length;
3803
+ const estimatedHours = nodes.reduce((sum, n) => sum + (n.estimateHours ?? 0), 0);
3804
+ const actualHours = nodes.reduce((sum, n) => sum + (n.actualHours ?? 0), 0);
3805
+ return {
3806
+ total,
3807
+ pending,
3808
+ inProgress,
3809
+ blocked,
3810
+ failed,
3811
+ review,
3812
+ completed,
3813
+ percentComplete: total > 0 ? Math.round(completed / total * 100) : 0,
3814
+ estimatedHours,
3815
+ actualHours
3816
+ };
3817
+ }
3818
+
3819
+ // src/defaults/task-tracker.ts
3820
+ var TaskTracker = class {
3821
+ constructor(opts) {
3822
+ this.opts = opts;
3823
+ }
3824
+ opts;
3825
+ graph = null;
3826
+ transitions = [];
3827
+ async createGraph(specId, title) {
3828
+ this.graph = {
3829
+ id: crypto.randomUUID(),
3830
+ specId,
3831
+ title,
3832
+ nodes: /* @__PURE__ */ new Map(),
3833
+ edges: [],
3834
+ rootNodes: [],
3835
+ createdAt: Date.now(),
3836
+ updatedAt: Date.now()
3837
+ };
3838
+ await this.opts.store.saveGraph(this.graph);
3839
+ return this.graph;
3840
+ }
3841
+ async loadGraph(id) {
3842
+ this.graph = await this.opts.store.loadGraph(id);
3843
+ return this.graph;
3844
+ }
3845
+ addNode(node) {
3846
+ if (!this.graph) throw new Error("No graph loaded");
3847
+ const now = Date.now();
3848
+ const newNode = {
3849
+ ...node,
3850
+ id: crypto.randomUUID(),
3851
+ status: node.status ?? "pending",
3852
+ createdAt: now,
3853
+ updatedAt: now
3854
+ };
3855
+ this.graph.nodes.set(newNode.id, newNode);
3856
+ if (!node.parentId) {
3857
+ this.graph.rootNodes.push(newNode.id);
3858
+ }
3859
+ this.graph.updatedAt = now;
3860
+ this.opts.store.saveGraph(this.graph);
3861
+ return newNode;
3862
+ }
3863
+ addEdge(from, to, type = "depends_on") {
3864
+ if (!this.graph) throw new Error("No graph loaded");
3865
+ const edge = {
3866
+ id: crypto.randomUUID(),
3867
+ from,
3868
+ to,
3869
+ type
3870
+ };
3871
+ this.graph.edges.push(edge);
3872
+ this.graph.updatedAt = Date.now();
3873
+ this.opts.store.saveGraph(this.graph);
3874
+ }
3875
+ updateNodeStatus(id, status, reason) {
3876
+ if (!this.graph) throw new Error("No graph loaded");
3877
+ const node = this.graph.nodes.get(id);
3878
+ if (!node) throw new Error(`Node ${id} not found`);
3879
+ const from = node.status;
3880
+ node.status = status;
3881
+ node.updatedAt = Date.now();
3882
+ if (status === "completed") {
3883
+ node.completedAt = Date.now();
3884
+ }
3885
+ this.transitions.push({ from, to: status, timestamp: Date.now(), reason });
3886
+ if (status === "completed") {
3887
+ this.unblockDependents(id);
3888
+ }
3889
+ if (status === "in_progress") {
3890
+ this.checkAndBlockIfNeeded(id);
3891
+ }
3892
+ this.graph.updatedAt = Date.now();
3893
+ this.opts.store.saveGraph(this.graph);
3894
+ }
3895
+ getNode(id) {
3896
+ return this.graph?.nodes.get(id);
3897
+ }
3898
+ getAllNodes(filter, sort) {
3899
+ if (!this.graph) return [];
3900
+ let nodes = Array.from(this.graph.nodes.values());
3901
+ if (filter) {
3902
+ nodes = nodes.filter((n) => {
3903
+ if (filter.status?.length && !filter.status.includes(n.status)) return false;
3904
+ if (filter.priority?.length && !filter.priority.includes(n.priority)) return false;
3905
+ if (filter.type?.length && !filter.type.includes(n.type)) return false;
3906
+ if (filter.assignee?.length && n.assignee && !filter.assignee.includes(n.assignee)) return false;
3907
+ if (filter.tags?.length && n.tags && !n.tags.some((t) => filter.tags.includes(t))) return false;
3908
+ if (filter.specRequirementId && n.specRequirementId !== filter.specRequirementId) return false;
3909
+ return true;
3910
+ });
3911
+ }
3912
+ if (sort) {
3913
+ nodes.sort((a, b) => {
3914
+ const aVal = a[sort.field] ?? "";
3915
+ const bVal = b[sort.field] ?? "";
3916
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
3917
+ return sort.direction === "asc" ? cmp : -cmp;
3918
+ });
3919
+ }
3920
+ return nodes;
3921
+ }
3922
+ getChildren(parentId) {
3923
+ if (!this.graph) return [];
3924
+ return Array.from(this.graph.nodes.values()).filter((n) => n.parentId === parentId);
3925
+ }
3926
+ getDependents(taskId) {
3927
+ if (!this.graph) return [];
3928
+ return this.graph.edges.filter((e) => e.from === taskId && e.type === "depends_on").map((e) => e.to);
3929
+ }
3930
+ getBlockers(taskId) {
3931
+ if (!this.graph) return [];
3932
+ return this.graph.edges.filter((e) => e.to === taskId && e.type === "depends_on").map((e) => e.from);
3933
+ }
3934
+ canStart(taskId) {
3935
+ const blockers = this.getBlockers(taskId);
3936
+ return blockers.every((id) => {
3937
+ const node = this.graph?.nodes.get(id);
3938
+ return node?.status === "completed";
3939
+ });
3940
+ }
3941
+ getProgress() {
3942
+ if (!this.graph) {
3943
+ return {
3944
+ total: 0,
3945
+ pending: 0,
3946
+ inProgress: 0,
3947
+ blocked: 0,
3948
+ failed: 0,
3949
+ review: 0,
3950
+ completed: 0,
3951
+ percentComplete: 0,
3952
+ estimatedHours: 0,
3953
+ actualHours: 0
3954
+ };
3955
+ }
3956
+ return computeTaskProgress(this.graph);
3957
+ }
3958
+ getTransitions(taskId) {
3959
+ if (!taskId) return [...this.transitions];
3960
+ return [...this.transitions];
3961
+ }
3962
+ unblockDependents(completedId) {
3963
+ if (!this.graph) return;
3964
+ const dependents = this.getDependents(completedId);
3965
+ for (const depId of dependents) {
3966
+ const dep = this.graph.nodes.get(depId);
3967
+ if (dep?.status === "blocked") {
3968
+ const remainingBlockers = this.getBlockers(depId);
3969
+ const allUnblocked = remainingBlockers.every((id) => {
3970
+ const blocker = this.graph?.nodes.get(id);
3971
+ return blocker?.status === "completed";
3972
+ });
3973
+ if (allUnblocked) {
3974
+ dep.status = "pending";
3975
+ dep.updatedAt = Date.now();
3976
+ }
3977
+ }
3978
+ }
3979
+ }
3980
+ checkAndBlockIfNeeded(taskId) {
3981
+ if (!this.graph) return;
3982
+ const blockers = this.getBlockers(taskId);
3983
+ const someBlocked = blockers.some((id) => {
3984
+ const blocker = this.graph?.nodes.get(id);
3985
+ return blocker?.status !== "completed";
3986
+ });
3987
+ if (someBlocked) {
3988
+ const node = this.graph.nodes.get(taskId);
3989
+ if (node) {
3990
+ node.status = "blocked";
3991
+ node.updatedAt = Date.now();
3992
+ }
3993
+ }
3994
+ }
3995
+ };
3996
+
3997
+ // src/defaults/task-flow.ts
3998
+ var TaskFlow = class {
3999
+ constructor(opts) {
4000
+ this.opts = opts;
4001
+ this.setPhase("idle");
4002
+ }
4003
+ opts;
4004
+ phase = "idle";
4005
+ spec = null;
4006
+ graph = null;
4007
+ stopped = false;
4008
+ emit(event, payload) {
4009
+ this.opts.events.emit(event, payload);
4010
+ }
4011
+ async fromSpec(specContent) {
4012
+ this.setPhase("parsing");
4013
+ const parser = new SpecParser();
4014
+ this.spec = parser.parse(specContent);
4015
+ this.setPhase("analyzing");
4016
+ const analysis = parser.analyze(this.spec);
4017
+ this.emit("spec.analyzed", { analysis });
4018
+ if (analysis.completeness < 50) {
4019
+ this.emit("error", {
4020
+ phase: "analyzing",
4021
+ error: new Error(`Spec completeness too low: ${analysis.completeness}%`)
4022
+ });
4023
+ this.setPhase("failed");
4024
+ throw new Error("Spec too incomplete");
4025
+ }
4026
+ this.setPhase("generating");
4027
+ const generator = new TaskGenerator({ taskTracker: this.opts.tracker });
4028
+ this.graph = await generator.generateFromSpec(this.spec);
4029
+ return this.graph;
4030
+ }
4031
+ async execute(ctx) {
4032
+ if (!this.graph) throw new Error("No graph loaded. Call fromSpec first.");
4033
+ this.setPhase("executing");
4034
+ this.stopped = false;
4035
+ const pendingTasks = this.getExecutableTasks();
4036
+ const maxConcurrent = this.opts.maxConcurrent ?? 2;
4037
+ while (pendingTasks.length > 0 && !this.stopped) {
4038
+ const batch = pendingTasks.splice(0, maxConcurrent);
4039
+ const results = await Promise.allSettled(
4040
+ batch.map((task) => this.executeSingleTask(task, ctx))
4041
+ );
4042
+ for (let i = 0; i < results.length; i++) {
4043
+ const result = results[i];
4044
+ const task = batch[i];
4045
+ if (!result || !task) continue;
4046
+ if (result.status === "rejected") {
4047
+ this.opts.tracker.updateNodeStatus(task.id, "failed", result.reason?.message);
4048
+ this.emit("task.failed", { taskId: task.id, error: result.reason?.message ?? "unknown" });
4049
+ ctx.onTaskFail?.(task, result.reason);
4050
+ } else {
4051
+ this.opts.tracker.updateNodeStatus(task.id, "completed");
4052
+ this.emit("task.completed", { taskId: task.id, result: result.value });
4053
+ ctx.onTaskComplete?.(task, result.value);
4054
+ }
4055
+ this.emitProgress();
4056
+ }
4057
+ const stillPending = this.getExecutableTasks();
4058
+ pendingTasks.length = 0;
4059
+ pendingTasks.push(...stillPending);
4060
+ if (this.checkDoneCondition()) {
4061
+ break;
4062
+ }
4063
+ }
4064
+ this.setPhase("completing");
4065
+ this.emit("done", { graph: this.graph });
4066
+ this.setPhase("done");
4067
+ return this.graph;
4068
+ }
4069
+ async reviewTask(taskId, approved, comment) {
4070
+ const task = this.opts.tracker.getNode(taskId);
4071
+ if (!task) throw new Error(`Task ${taskId} not found`);
4072
+ if (approved) {
4073
+ this.opts.tracker.updateNodeStatus(taskId, "completed", comment);
4074
+ this.emit("task.completed", { taskId });
4075
+ } else {
4076
+ this.opts.tracker.updateNodeStatus(taskId, "in_progress", comment ?? "Needs revision");
4077
+ this.emit("task.review", { taskId });
4078
+ }
4079
+ }
4080
+ stop() {
4081
+ this.stopped = true;
4082
+ }
4083
+ getPhase() {
4084
+ return this.phase;
4085
+ }
4086
+ getGraph() {
4087
+ return this.graph;
4088
+ }
4089
+ getSpec() {
4090
+ return this.spec;
4091
+ }
4092
+ setPhase(phase) {
4093
+ const from = this.phase;
4094
+ this.phase = phase;
4095
+ this.emit("phase.change", { from, to: phase });
4096
+ }
4097
+ getExecutableTasks() {
4098
+ return this.opts.tracker.getAllNodes({ status: ["pending", "blocked"] }).filter((n) => n.status === "pending" && this.opts.tracker.canStart(n.id)).sort((a, b) => {
4099
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
4100
+ return (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4);
4101
+ });
4102
+ }
4103
+ async executeSingleTask(task, ctx) {
4104
+ this.opts.tracker.updateNodeStatus(task.id, "in_progress");
4105
+ this.emit("task.started", { taskId: task.id });
4106
+ return ctx.executeTask(task);
4107
+ }
4108
+ checkDoneCondition() {
4109
+ const condition = this.opts.doneCondition;
4110
+ if (!condition) {
4111
+ const progress = this.opts.tracker.getProgress();
4112
+ return progress.percentComplete === 100;
4113
+ }
4114
+ switch (condition.type) {
4115
+ case "all_tasks_done": {
4116
+ const progress = this.opts.tracker.getProgress();
4117
+ return progress.pending === 0 && progress.inProgress === 0;
4118
+ }
4119
+ case "iterations":
4120
+ return false;
4121
+ // Not tracked here
4122
+ case "tool_calls":
4123
+ return false;
4124
+ default:
4125
+ return false;
4126
+ }
4127
+ }
4128
+ emitProgress() {
4129
+ const progress = this.opts.tracker.getProgress();
4130
+ this.emit("progress", {
4131
+ percent: progress.percentComplete,
4132
+ message: `${progress.completed}/${progress.total} tasks completed`
4133
+ });
4134
+ }
4135
+ };
4136
+ var SpecDrivenDev = class {
4137
+ store;
4138
+ tracker;
4139
+ events;
4140
+ flows = /* @__PURE__ */ new Map();
4141
+ constructor(opts) {
4142
+ this.store = new DefaultTaskStore();
4143
+ this.tracker = new TaskTracker({ store: this.store });
4144
+ this.events = opts.events;
4145
+ }
4146
+ async createFlow(specContent, options) {
4147
+ const flow = new TaskFlow({
4148
+ tracker: this.tracker,
4149
+ events: this.events,
4150
+ ...options
4151
+ });
4152
+ const graph = await flow.fromSpec(specContent);
4153
+ this.flows.set(graph.id, flow);
4154
+ return flow;
4155
+ }
4156
+ getTracker() {
4157
+ return this.tracker;
4158
+ }
4159
+ getFlow(graphId) {
4160
+ return this.flows.get(graphId);
4161
+ }
4162
+ listFlows() {
4163
+ return Array.from(this.flows.entries()).map(([id, flow]) => ({
4164
+ id,
4165
+ title: flow.getGraph()?.title ?? "Untitled",
4166
+ phase: flow.getPhase()
4167
+ }));
4168
+ }
4169
+ };
4170
+
4171
+ // src/utils/tool-output-serializer.ts
4172
+ function createToolOutputSerializer(opts = {}) {
4173
+ const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
4174
+ function serialize(value) {
4175
+ if (typeof value === "string") return value;
4176
+ if (value === null || value === void 0) return "";
4177
+ if (typeof value === "object") {
4178
+ if (Array.isArray(value)) return value.map(serialize).join("\n");
4179
+ if ("text" in value) {
4180
+ const t = value.text;
4181
+ return typeof t === "string" ? t : JSON.stringify(value, null, 2);
4182
+ }
4183
+ try {
4184
+ return JSON.stringify(value, null, 2);
4185
+ } catch {
4186
+ return String(value);
4187
+ }
4188
+ }
4189
+ return String(value);
4190
+ }
4191
+ function enforceCap(text, remainingBudget) {
4192
+ if (remainingBudget <= 0) {
4193
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
4194
+ }
4195
+ const textBytes = Buffer.byteLength(text, "utf8");
4196
+ if (textBytes <= remainingBudget) {
4197
+ return { text, newBudget: remainingBudget - textBytes };
4198
+ }
4199
+ const marker = `
4200
+ \u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
4201
+ `;
4202
+ const markerBytes = Buffer.byteLength(marker, "utf8");
4203
+ const available = remainingBudget - markerBytes;
4204
+ if (available <= 0) {
4205
+ return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
4206
+ }
4207
+ const half = Math.floor(available / 2);
4208
+ const first = text.slice(0, half);
4209
+ Buffer.byteLength(first, "utf8");
4210
+ const second = text.slice(text.length - half);
4211
+ return { text: `${first}${marker}${second}`, newBudget: 0 };
4212
+ }
4213
+ return { serialize, enforceCap, capBytes };
4214
+ }
4215
+
4216
+ // src/defaults/tool-executor.ts
4217
+ var ToolExecutor = class {
4218
+ constructor(registry, opts) {
4219
+ this.registry = registry;
4220
+ this.opts = opts;
4221
+ this.iterationTimeoutMs = opts.iterationTimeoutMs ?? 3e5;
4222
+ this.serializer = createToolOutputSerializer({
4223
+ perIterationOutputCapBytes: opts.perIterationOutputCapBytes ?? 1e5
4224
+ });
4225
+ }
4226
+ registry;
4227
+ opts;
4228
+ serializer;
4229
+ iterationTimeoutMs;
4230
+ /**
4231
+ * Execute a batch of tool uses using the configured strategy.
4232
+ * Returns the execution results and the remaining output budget.
4233
+ */
4234
+ async executeBatch(toolUses, ctx, strategy) {
4235
+ let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
4236
+ const runOne = async (use, index) => {
4237
+ const start = Date.now();
4238
+ const tool = this.registry.get(use.name);
4239
+ let result;
4240
+ if (!tool) {
4241
+ result = this.unknownToolResult(use, () => this.registry.list().map((t) => t.name));
4242
+ } else {
4243
+ const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
4244
+ if (decision.permission === "deny") {
4245
+ result = this.deniedResult(use, decision.reason);
4246
+ } else if (decision.permission === "confirm") {
4247
+ result = this.confirmResult(use);
4248
+ } else {
4249
+ try {
4250
+ result = await this.executeTool(tool, use, ctx, budget);
4251
+ } catch (err) {
4252
+ const msg = err instanceof Error ? err.message : String(err);
4253
+ const scrubbed = this.opts.secretScrubber.scrub(msg);
4254
+ this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
4255
+ result = {
4256
+ type: "tool_result",
4257
+ tool_use_id: use.id,
4258
+ content: `Tool "${use.name}" threw: ${scrubbed}`,
4259
+ is_error: true
4260
+ };
4261
+ }
4262
+ }
4263
+ }
4264
+ const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
4265
+ budget = Math.max(0, budget - contentBytes);
4266
+ return { result, tool, durationMs: Date.now() - start };
4267
+ };
4268
+ if (strategy === "sequential") {
4269
+ const outputs = [];
4270
+ for (let i = 0; i < toolUses.length; i++) {
4271
+ const use = toolUses[i];
4272
+ if (use) outputs.push(await runOne(use));
4273
+ }
4274
+ return { outputs, remainingBudget: budget };
4275
+ }
4276
+ if (strategy === "parallel") {
4277
+ const outputs = await Promise.all(toolUses.map((use, i) => runOne(use)));
4278
+ return { outputs, remainingBudget: budget };
4279
+ }
4280
+ const nonMutating = [];
4281
+ const mutating = [];
4282
+ for (let i = 0; i < toolUses.length; i++) {
4283
+ const use = toolUses[i];
4284
+ if (!use) continue;
4285
+ const tool = this.registry.get(use.name);
4286
+ if (tool?.mutating) mutating.push({ use, index: i });
4287
+ else nonMutating.push({ use, index: i });
4288
+ }
4289
+ const firstPass = await Promise.all(nonMutating.map(({ use, index }) => runOne(use)));
4290
+ const secondPass = [];
4291
+ for (const { use, index } of mutating) {
4292
+ secondPass.push(await runOne(use));
4293
+ }
4294
+ return {
4295
+ outputs: [...firstPass, ...secondPass],
4296
+ remainingBudget: budget
4297
+ };
4298
+ }
4299
+ /**
4300
+ * Execute a single tool with timeout, permission check, and output capping.
4301
+ * Emits `tool.started` via the injected EventBus (if any) right before
4302
+ * invoking the tool — closes the observability gap between "model decided
4303
+ * to call a tool" and "tool.executed".
4304
+ */
4305
+ async executeTool(tool, use, ctx, budget) {
4306
+ this.opts.events?.emit("tool.started", {
4307
+ name: tool.name,
4308
+ id: use.id,
4309
+ input: use.input
4310
+ });
4311
+ this.opts.renderer?.writeToolCall(tool.name, use.input);
4312
+ const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx);
4313
+ const text = this.serializer.serialize(output);
4314
+ const scrubbed = this.opts.secretScrubber.scrub(text);
4315
+ const { text: capped } = this.serializer.enforceCap(scrubbed, budget);
4316
+ this.opts.renderer?.writeToolResult(tool.name, capped, false);
4317
+ return {
4318
+ type: "tool_result",
4319
+ tool_use_id: use.id,
4320
+ name: tool.name,
4321
+ content: capped,
4322
+ is_error: false
4323
+ };
4324
+ }
4325
+ async runWithTimeout(tool, input, parentSignal, ctx) {
4326
+ if (parentSignal.aborted) {
4327
+ throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
4328
+ }
4329
+ const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
4330
+ const ctrl = new AbortController();
4331
+ const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
4332
+ const combined = anySignal([parentSignal, ctrl.signal]);
4333
+ try {
4334
+ return await tool.execute(input, ctx, { signal: combined });
4335
+ } finally {
4336
+ clearTimeout(timer);
4337
+ }
4338
+ }
4339
+ unknownToolResult(use, listFns) {
4340
+ return {
4341
+ type: "tool_result",
4342
+ tool_use_id: use.id,
4343
+ content: `Tool "${use.name}" is not registered. Available tools: ${listFns().join(", ")}`,
4344
+ is_error: true
4345
+ };
4346
+ }
4347
+ deniedResult(use, reason) {
4348
+ return {
4349
+ type: "tool_result",
4350
+ tool_use_id: use.id,
4351
+ content: `Tool "${use.name}" denied: ${reason ?? "policy"}`,
4352
+ is_error: true
4353
+ };
4354
+ }
4355
+ confirmResult(use) {
4356
+ return {
4357
+ type: "tool_result",
4358
+ tool_use_id: use.id,
4359
+ content: `Tool "${use.name}" requires user confirmation but no prompt handler was available.`,
4360
+ is_error: true
4361
+ };
4362
+ }
4363
+ };
4364
+ function anySignal(signals) {
4365
+ if (typeof AbortSignal.any === "function") {
4366
+ return AbortSignal.any(signals);
4367
+ }
4368
+ const ctrl = new AbortController();
4369
+ const abortSources = [];
4370
+ for (const s of signals) {
4371
+ if (s.aborted) {
4372
+ ctrl.abort(s.reason);
4373
+ return ctrl.signal;
4374
+ }
4375
+ abortSources.push(s);
4376
+ }
4377
+ for (const s of abortSources) {
4378
+ s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
4379
+ }
4380
+ return ctrl.signal;
4381
+ }
4382
+
4383
+ // src/defaults/context-manager.ts
4384
+ var CONTEXT_MANAGER_TOOL_NAME = "context_manager";
4385
+ function roughEstimate(messages) {
4386
+ let total = 0;
4387
+ for (const m of messages) {
4388
+ if (typeof m.content === "string") {
4389
+ total += Math.ceil(m.content.length / 4);
4390
+ } else if (Array.isArray(m.content)) {
4391
+ for (const b of m.content) {
4392
+ if (b.type === "text") total += Math.ceil(b.text.length / 4);
4393
+ else if (b.type === "tool_use" || b.type === "tool_result") {
4394
+ total += Math.ceil(JSON.stringify(b).length / 4);
4395
+ }
4396
+ }
4397
+ }
4398
+ }
4399
+ return total;
4400
+ }
4401
+ function createContextManagerTool(opts = {}) {
4402
+ return {
4403
+ name: CONTEXT_MANAGER_TOOL_NAME,
4404
+ description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction.',
4405
+ inputSchema: {
4406
+ type: "object",
4407
+ properties: {
4408
+ action: {
4409
+ type: "string",
4410
+ enum: ["check", "summary", "prune", "add_note", "compact"],
4411
+ description: "The context operation to perform."
4412
+ },
4413
+ from: {
4414
+ type: "number",
4415
+ description: "Start index (inclusive) for summary/prune operations."
4416
+ },
4417
+ to: {
4418
+ type: "number",
4419
+ description: "End index (inclusive) for summary/prune operations."
4420
+ },
4421
+ text: {
4422
+ type: "string",
4423
+ description: 'Summary or note text. For "summary": the model-provided summary of the removed range. For "add_note": the note to inject.'
4424
+ },
4425
+ afterIndex: {
4426
+ type: "number",
4427
+ description: "Insert after this index (for add_note). Defaults to prepend (0)."
4428
+ }
4429
+ },
4430
+ required: ["action"]
4431
+ },
4432
+ permission: "auto",
4433
+ mutating: true,
4434
+ async execute(input, ctx) {
4435
+ const messages = ctx.messages;
4436
+ const beforeTokens = roughEstimate(messages);
4437
+ switch (input.action) {
4438
+ case "check": {
4439
+ return {
4440
+ action: "check",
4441
+ beforeTokens,
4442
+ messageCount: messages.length,
4443
+ notes: JSON.stringify({
4444
+ messages: messages.length,
4445
+ tokens: beforeTokens,
4446
+ readFiles: ctx.readFiles.size,
4447
+ todos: ctx.todos.length,
4448
+ inProgress: ctx.todos.filter((t) => t.status === "in_progress").length
4449
+ })
4450
+ };
4451
+ }
4452
+ case "compact": {
4453
+ if (!opts.compactor) {
4454
+ return {
4455
+ action: "compact",
4456
+ beforeTokens,
4457
+ messageCount: messages.length,
4458
+ notes: "No compactor registered. Use /compact aggressive via slash command instead."
4459
+ };
4460
+ }
4461
+ const report = await opts.compactor.compact(ctx);
4462
+ return {
4463
+ action: "compact",
4464
+ beforeTokens,
4465
+ afterTokens: report.after,
4466
+ messageCount: messages.length
4467
+ };
4468
+ }
4469
+ case "prune": {
4470
+ const from = input.from ?? 0;
4471
+ const to = input.to ?? messages.length - 1;
4472
+ if (from < 0 || to >= messages.length || from > to) {
4473
+ return {
4474
+ action: "prune",
4475
+ beforeTokens,
4476
+ messageCount: messages.length,
4477
+ notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
4478
+ };
4479
+ }
4480
+ const removed = messages.splice(from, to - from + 1);
4481
+ const afterTokens = roughEstimate(messages);
4482
+ return {
4483
+ action: "prune",
4484
+ beforeTokens,
4485
+ afterTokens,
4486
+ messageCount: messages.length,
4487
+ removedCount: removed.length
4488
+ };
4489
+ }
4490
+ case "add_note": {
4491
+ const noteText = input.text ?? "(no summary)";
4492
+ const afterIdx = Math.min(input.afterIndex ?? 0, messages.length);
4493
+ const noteMsg = {
4494
+ role: "system",
4495
+ content: `[note: ${noteText}]`
4496
+ };
4497
+ messages.splice(afterIdx, 0, noteMsg);
4498
+ const afterTokens = roughEstimate(messages);
4499
+ return {
4500
+ action: "add_note",
4501
+ beforeTokens,
4502
+ afterTokens,
4503
+ messageCount: messages.length,
4504
+ summary: noteText
4505
+ };
4506
+ }
4507
+ case "summary": {
4508
+ const from = input.from ?? 0;
4509
+ const to = input.to ?? messages.length - 1;
4510
+ if (from < 0 || to >= messages.length || from > to) {
4511
+ return {
4512
+ action: "summary",
4513
+ beforeTokens,
4514
+ messageCount: messages.length,
4515
+ notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
4516
+ };
4517
+ }
4518
+ messages.slice(from, to + 1);
4519
+ const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
4520
+ const summaryMsg = {
4521
+ role: "system",
4522
+ content: `[summary of messages ${from}\u2013${to}]: ${summaryText}`
4523
+ };
4524
+ messages.splice(from, to - from + 1, summaryMsg);
4525
+ const afterTokens = roughEstimate(messages);
4526
+ return {
4527
+ action: "summary",
4528
+ beforeTokens,
4529
+ afterTokens,
4530
+ messageCount: messages.length,
4531
+ summary: summaryText
4532
+ };
4533
+ }
4534
+ default:
4535
+ return {
4536
+ action: input.action,
4537
+ beforeTokens,
4538
+ messageCount: messages.length,
4539
+ notes: `Unknown action: ${input.action}`
4540
+ };
4541
+ }
4542
+ }
4543
+ };
4544
+ }
4545
+ var contextManagerTool = createContextManagerTool();
4546
+
4547
+ export { AutoCompactionMiddleware, AutonomousRunner, DefaultAttachmentStore, DefaultConfigLoader, DefaultErrorHandler, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, IntelligentCompactor, LLMSelector, QueueStore, RecoveryLock, SelectiveCompactor, SpecDrivenDev, SpecParser, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, classifyFamily, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, loadProjectModes, loadUserModes, migratePlaintextSecrets, rewriteConfigEncrypted };
4548
+ //# sourceMappingURL=index.js.map
4549
+ //# sourceMappingURL=index.js.map