@wrongstack/tools 0.260.0 → 0.265.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit.js +154 -10
- package/dist/audit.js.map +1 -1
- package/dist/bash.js +138 -2
- package/dist/bash.js.map +1 -1
- package/dist/batch-tool-use.js +1 -0
- package/dist/batch-tool-use.js.map +1 -1
- package/dist/builtin.d.ts +20 -1
- package/dist/builtin.js +674 -333
- package/dist/builtin.js.map +1 -1
- package/dist/circuit-breaker.d.ts +20 -0
- package/dist/circuit-breaker.js +40 -2
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/codebase-index/index.d.ts +16 -0
- package/dist/codebase-index/index.js +62 -27
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +59 -27
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/diff.js +14 -6
- package/dist/diff.js.map +1 -1
- package/dist/document.js +18 -11
- package/dist/document.js.map +1 -1
- package/dist/edit.js +22 -14
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +140 -3
- package/dist/exec.js.map +1 -1
- package/dist/fetch.d.ts +19 -1
- package/dist/fetch.js +2 -1
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +153 -10
- package/dist/format.js.map +1 -1
- package/dist/git.js +1 -0
- package/dist/git.js.map +1 -1
- package/dist/glob.js +14 -6
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +14 -6
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +55 -3
- package/dist/index.js +833 -336
- package/dist/index.js.map +1 -1
- package/dist/install.js +154 -10
- package/dist/install.js.map +1 -1
- package/dist/json.js +2 -0
- package/dist/json.js.map +1 -1
- package/dist/lint.js +153 -10
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +14 -6
- package/dist/logs.js.map +1 -1
- package/dist/memory.js +1 -0
- package/dist/memory.js.map +1 -1
- package/dist/mode.js +1 -0
- package/dist/mode.js.map +1 -1
- package/dist/outdated.js +16 -7
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +643 -332
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +14 -6
- package/dist/patch.js.map +1 -1
- package/dist/process-registry.d.ts +56 -2
- package/dist/process-registry.js +138 -3
- package/dist/process-registry.js.map +1 -1
- package/dist/read.js +24 -18
- package/dist/read.js.map +1 -1
- package/dist/replace.js +14 -6
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +14 -6
- package/dist/scaffold.js.map +1 -1
- package/dist/search.js +3 -3
- package/dist/search.js.map +1 -1
- package/dist/test.js +153 -10
- package/dist/test.js.map +1 -1
- package/dist/todo.js +1 -0
- package/dist/todo.js.map +1 -1
- package/dist/tool-help.js +1 -0
- package/dist/tool-help.js.map +1 -1
- package/dist/tool-icons.d.ts +20 -0
- package/dist/tool-icons.js +130 -0
- package/dist/tool-icons.js.map +1 -0
- package/dist/tool-search.js +1 -0
- package/dist/tool-search.js.map +1 -1
- package/dist/tool-use.js +1 -0
- package/dist/tool-use.js.map +1 -1
- package/dist/tree.js +14 -6
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +153 -10
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +22 -14
- package/dist/write.js.map +1 -1
- package/package.json +6 -2
package/dist/pack.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn, execFileSync, spawnSync } from 'node:child_process';
|
|
2
2
|
import * as Core from '@wrongstack/core';
|
|
3
|
-
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus,
|
|
3
|
+
import { buildChildEnv, detectNewlineStyle, normalizeToLf, toStyle, atomicWrite, unifiedDiff, isPrivateIPv4, isPrivateIPv6, compileGlob, expectDefined, recordPackageAction, detectPackageEcosystem, mutatePlan, clearPlan, getPlanTemplate, addPlanItem, deriveTodosFromPlanItem, removePlanItem, setPlanItemStatus, mutateTasks, formatTaskList, formatPlan, computeTaskItemProgress, loadPlan, savePlan, loadTasks, saveTasks, wstackGlobalRoot, resolveWstackPaths, truncate } from '@wrongstack/core';
|
|
4
4
|
import * as fs from 'node:fs';
|
|
5
5
|
import { statSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
|
|
6
6
|
import * as fs14 from 'node:fs/promises';
|
|
7
7
|
import * as path3 from 'node:path';
|
|
8
8
|
import { resolve, sep, dirname, join } from 'node:path';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
|
+
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
10
11
|
import { createRequire } from 'node:module';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { Worker } from 'node:worker_threads';
|
|
@@ -141,6 +142,23 @@ var CircuitBreaker = class {
|
|
|
141
142
|
lastSlowAt = null;
|
|
142
143
|
/** Timestamp when the breaker was opened (for cooldown calculation). */
|
|
143
144
|
openedAt = null;
|
|
145
|
+
/**
|
|
146
|
+
* Master enable flag. When false the breaker is bypassed: `beforeCall`
|
|
147
|
+
* always returns true and `afterCall` records nothing. The class itself
|
|
148
|
+
* defaults to enabled (so the standalone unit tests exercise tripping); the
|
|
149
|
+
* ProcessRegistry flips this off until the user opts in via `/settings`.
|
|
150
|
+
*/
|
|
151
|
+
enabled = true;
|
|
152
|
+
/**
|
|
153
|
+
* Fired (best-effort) when the breaker transitions into the `open` state.
|
|
154
|
+
* The registry uses this to arm its auto kill/reset countdown.
|
|
155
|
+
*/
|
|
156
|
+
onTrip;
|
|
157
|
+
/**
|
|
158
|
+
* Fired (best-effort) when the breaker returns to `closed` after having been
|
|
159
|
+
* open/half-open. The registry uses this to cancel a pending kill/reset.
|
|
160
|
+
*/
|
|
161
|
+
onReset;
|
|
144
162
|
constructor(config = {}) {
|
|
145
163
|
this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
146
164
|
this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
|
|
@@ -149,12 +167,22 @@ var CircuitBreaker = class {
|
|
|
149
167
|
this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
|
|
150
168
|
this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
151
169
|
}
|
|
170
|
+
/** Toggle the master enable. Disabling resets to a clean `closed` state. */
|
|
171
|
+
setEnabled(enabled) {
|
|
172
|
+
if (this.enabled === enabled) return;
|
|
173
|
+
this.enabled = enabled;
|
|
174
|
+
if (!enabled) this._reset();
|
|
175
|
+
}
|
|
176
|
+
get isEnabled() {
|
|
177
|
+
return this.enabled;
|
|
178
|
+
}
|
|
152
179
|
/**
|
|
153
180
|
* Returns true if the circuit allows a new call to proceed.
|
|
154
181
|
* When false, callers should abort the tool call and return a
|
|
155
182
|
* circuit-breaker error instead of spawning a process.
|
|
156
183
|
*/
|
|
157
184
|
get canProceed() {
|
|
185
|
+
if (!this.enabled) return true;
|
|
158
186
|
this._checkStateTransition();
|
|
159
187
|
return this.state !== "open";
|
|
160
188
|
}
|
|
@@ -190,7 +218,7 @@ var CircuitBreaker = class {
|
|
|
190
218
|
* not affect breaker state.
|
|
191
219
|
*/
|
|
192
220
|
beforeCall(bypass = false) {
|
|
193
|
-
if (bypass) return true;
|
|
221
|
+
if (bypass || !this.enabled) return true;
|
|
194
222
|
this._checkStateTransition();
|
|
195
223
|
if (this.state === "open") return false;
|
|
196
224
|
return true;
|
|
@@ -205,7 +233,7 @@ var CircuitBreaker = class {
|
|
|
205
233
|
* Use for background/fire-and-forget processes.
|
|
206
234
|
*/
|
|
207
235
|
afterCall(durationMs, failed, bypass = false) {
|
|
208
|
-
if (bypass) return;
|
|
236
|
+
if (bypass || !this.enabled) return;
|
|
209
237
|
const now = Date.now();
|
|
210
238
|
if (this.state === "half-open") {
|
|
211
239
|
if (failed) {
|
|
@@ -251,12 +279,23 @@ var CircuitBreaker = class {
|
|
|
251
279
|
if (this.state === "open") return;
|
|
252
280
|
this.state = "open";
|
|
253
281
|
this.openedAt = Date.now();
|
|
282
|
+
try {
|
|
283
|
+
this.onTrip?.();
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
254
286
|
}
|
|
255
287
|
_reset() {
|
|
288
|
+
const wasRecovering = this.state !== "closed";
|
|
256
289
|
this.state = "closed";
|
|
257
290
|
this.consecutiveFailures = 0;
|
|
258
291
|
this.window = [];
|
|
259
292
|
this.openedAt = null;
|
|
293
|
+
if (wasRecovering) {
|
|
294
|
+
try {
|
|
295
|
+
this.onReset?.();
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
260
299
|
}
|
|
261
300
|
/** Transition from open → half-open when cooldown elapses. */
|
|
262
301
|
_checkStateTransition() {
|
|
@@ -318,8 +357,21 @@ function killWin32Tree(pid) {
|
|
|
318
357
|
var ProcessRegistryImpl = class {
|
|
319
358
|
processes = /* @__PURE__ */ new Map();
|
|
320
359
|
breaker;
|
|
360
|
+
/**
|
|
361
|
+
* Auto kill/reset config. When the breaker trips and `autoKillResetMs > 0`,
|
|
362
|
+
* a countdown is armed; on expiry all tracked processes are killed and the
|
|
363
|
+
* breaker is reset to closed (forced recovery). Zero means manual recovery
|
|
364
|
+
* only (`/kill reset`).
|
|
365
|
+
*/
|
|
366
|
+
autoKillResetMs = 0;
|
|
367
|
+
autoKillTimer = null;
|
|
368
|
+
autoKillArmedAt = null;
|
|
369
|
+
breakerCountdownListeners = [];
|
|
321
370
|
constructor(breakerConfig) {
|
|
322
371
|
this.breaker = new CircuitBreaker(breakerConfig);
|
|
372
|
+
this.breaker.onTrip = () => this._armAutoKillReset();
|
|
373
|
+
this.breaker.onReset = () => this._cancelAutoKillReset();
|
|
374
|
+
this.breaker.setEnabled(false);
|
|
323
375
|
}
|
|
324
376
|
register(info) {
|
|
325
377
|
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
@@ -395,6 +447,90 @@ var ProcessRegistryImpl = class {
|
|
|
395
447
|
forceBreakerReset() {
|
|
396
448
|
this.breaker.forceReset();
|
|
397
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Configure circuit-breaker protection at runtime. Called from `/settings`
|
|
452
|
+
* (instant, all modes) and on TUI mount (applies persisted config).
|
|
453
|
+
*
|
|
454
|
+
* - `enabled` toggles whether the breaker gates `bash`/`exec`.
|
|
455
|
+
* - `autoKillResetMs` arms the auto kill/reset countdown when the breaker
|
|
456
|
+
* trips (0 = manual recovery only).
|
|
457
|
+
*
|
|
458
|
+
* Re-applies cleanly on every call: cancels a pending countdown when the
|
|
459
|
+
* timeout is cleared or protection disabled, and re-arms if the breaker is
|
|
460
|
+
* currently open under the new settings.
|
|
461
|
+
*/
|
|
462
|
+
setBreakerConfig(cfg) {
|
|
463
|
+
if (cfg.enabled !== void 0) this.breaker.setEnabled(cfg.enabled);
|
|
464
|
+
if (cfg.autoKillResetMs !== void 0) this.autoKillResetMs = Math.max(0, cfg.autoKillResetMs);
|
|
465
|
+
if (this.autoKillResetMs <= 0) {
|
|
466
|
+
this._cancelAutoKillReset();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (this.breaker.isEnabled && this.breaker.snapshot().state === "open") {
|
|
470
|
+
this._armAutoKillReset();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Live countdown to the next auto kill/reset, or null when nothing is armed.
|
|
475
|
+
* The TUI polls this on a 1s tick while armed so the statusline decrements.
|
|
476
|
+
*/
|
|
477
|
+
getBreakerCountdown() {
|
|
478
|
+
if (this.autoKillArmedAt === null || this.autoKillResetMs <= 0) return null;
|
|
479
|
+
const elapsed = Date.now() - this.autoKillArmedAt;
|
|
480
|
+
return { remainingMs: Math.max(0, this.autoKillResetMs - elapsed), totalMs: this.autoKillResetMs };
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Subscribe to countdown arm/cancel events. Returns an unsubscribe function.
|
|
484
|
+
* Use {@link getBreakerCountdown} for the live ticking value between events.
|
|
485
|
+
*/
|
|
486
|
+
onBreakerCountdownChange(listener) {
|
|
487
|
+
this.breakerCountdownListeners.push(listener);
|
|
488
|
+
return () => {
|
|
489
|
+
this.breakerCountdownListeners = this.breakerCountdownListeners.filter((l) => l !== listener);
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
_emitBreakerCountdown() {
|
|
493
|
+
const snap = this.getBreakerCountdown();
|
|
494
|
+
for (const l of this.breakerCountdownListeners) {
|
|
495
|
+
try {
|
|
496
|
+
l(snap);
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Arm the auto kill/reset countdown. Idempotent: re-arming resets the window
|
|
503
|
+
* (a fresh trip after a failed half-open probe restarts the clock). No-op
|
|
504
|
+
* when protection is off or no timeout is configured.
|
|
505
|
+
*/
|
|
506
|
+
_armAutoKillReset() {
|
|
507
|
+
if (this.autoKillResetMs <= 0 || !this.breaker.isEnabled) return;
|
|
508
|
+
this._clearAutoKillTimer();
|
|
509
|
+
this.autoKillArmedAt = Date.now();
|
|
510
|
+
this.autoKillTimer = setTimeout(() => {
|
|
511
|
+
this.autoKillTimer = null;
|
|
512
|
+
this.autoKillArmedAt = null;
|
|
513
|
+
this.killAll({ force: false });
|
|
514
|
+
this.breaker.forceReset();
|
|
515
|
+
this._emitBreakerCountdown();
|
|
516
|
+
}, this.autoKillResetMs);
|
|
517
|
+
this.autoKillTimer.unref?.();
|
|
518
|
+
this._emitBreakerCountdown();
|
|
519
|
+
}
|
|
520
|
+
_cancelAutoKillReset() {
|
|
521
|
+
const wasArmed = this.autoKillArmedAt !== null;
|
|
522
|
+
this._clearAutoKillTimer();
|
|
523
|
+
if (wasArmed) {
|
|
524
|
+
this.autoKillArmedAt = null;
|
|
525
|
+
this._emitBreakerCountdown();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
_clearAutoKillTimer() {
|
|
529
|
+
if (this.autoKillTimer !== null) {
|
|
530
|
+
clearTimeout(this.autoKillTimer);
|
|
531
|
+
this.autoKillTimer = null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
398
534
|
/** Kill a single process by PID.
|
|
399
535
|
*
|
|
400
536
|
* On POSIX: sends SIGTERM to the *process group* (-pid) so that
|
|
@@ -533,8 +669,9 @@ async function* spawnStream(opts) {
|
|
|
533
669
|
let pending2 = "";
|
|
534
670
|
let error;
|
|
535
671
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
536
|
-
const
|
|
537
|
-
const needsShell = isWin && (
|
|
672
|
+
const resolved = resolveWin32Command(opts.cmd);
|
|
673
|
+
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
674
|
+
const cmd = needsShell ? opts.cmd : resolved;
|
|
538
675
|
const child = spawn(cmd, opts.args, {
|
|
539
676
|
cwd: opts.cwd,
|
|
540
677
|
env: buildChildEnv(),
|
|
@@ -698,20 +835,29 @@ async function detectPackageManager(cwd) {
|
|
|
698
835
|
function resolvePath(input, ctx) {
|
|
699
836
|
return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
700
837
|
}
|
|
838
|
+
function allowedRoots(ctx) {
|
|
839
|
+
return [path3.resolve(ctx.projectRoot), path3.resolve(Core.wstackGlobalRoot())];
|
|
840
|
+
}
|
|
841
|
+
function isInsideAny(target, roots) {
|
|
842
|
+
return roots.some((root) => {
|
|
843
|
+
const rel = path3.relative(root, target);
|
|
844
|
+
return rel === "" || !rel.startsWith("..") && !path3.isAbsolute(rel);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
701
847
|
function ensureInsideRoot(absPath, ctx) {
|
|
702
|
-
const root = path3.resolve(ctx.projectRoot);
|
|
703
848
|
const target = path3.resolve(absPath);
|
|
704
|
-
|
|
705
|
-
if (
|
|
706
|
-
|
|
707
|
-
}
|
|
708
|
-
return target;
|
|
849
|
+
if (ctx.allowOutsideProjectRoot) return target;
|
|
850
|
+
if (isInsideAny(target, allowedRoots(ctx))) return target;
|
|
851
|
+
throw new Error(`Path "${absPath}" is outside project root "${path3.resolve(ctx.projectRoot)}"`);
|
|
709
852
|
}
|
|
710
853
|
function safeResolve(input, ctx) {
|
|
711
854
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
712
855
|
}
|
|
713
856
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
714
|
-
|
|
857
|
+
if (ctx.allowOutsideProjectRoot) return;
|
|
858
|
+
const realRoots = await Promise.all(
|
|
859
|
+
allowedRoots(ctx).map((r) => fs14.realpath(r).catch(() => path3.resolve(r)))
|
|
860
|
+
);
|
|
715
861
|
let probe = absPath;
|
|
716
862
|
for (; ; ) {
|
|
717
863
|
let real;
|
|
@@ -726,13 +872,10 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
726
872
|
}
|
|
727
873
|
throw err;
|
|
728
874
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
);
|
|
734
|
-
}
|
|
735
|
-
return;
|
|
875
|
+
if (isInsideAny(real, realRoots)) return;
|
|
876
|
+
throw new Error(
|
|
877
|
+
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
878
|
+
);
|
|
736
879
|
}
|
|
737
880
|
}
|
|
738
881
|
async function safeResolveReal(input, ctx) {
|
|
@@ -834,6 +977,7 @@ var auditTool = {
|
|
|
834
977
|
permission: "confirm",
|
|
835
978
|
mutating: false,
|
|
836
979
|
capabilities: ["shell.restricted"],
|
|
980
|
+
icon: "package",
|
|
837
981
|
timeoutMs: 6e4,
|
|
838
982
|
inputSchema: {
|
|
839
983
|
type: "object",
|
|
@@ -938,6 +1082,7 @@ var bashTool = {
|
|
|
938
1082
|
permission: "confirm",
|
|
939
1083
|
mutating: true,
|
|
940
1084
|
riskTier: "destructive",
|
|
1085
|
+
icon: "terminal",
|
|
941
1086
|
// Trust rules match on the literal `command` string. Without subjectKey
|
|
942
1087
|
// the policy heuristic would have done the same here, but declaring it
|
|
943
1088
|
// explicitly removes the implicit cross-tool aliasing.
|
|
@@ -1278,6 +1423,7 @@ var batchToolUseTool = {
|
|
|
1278
1423
|
mutating: true,
|
|
1279
1424
|
timeoutMs: 12e4,
|
|
1280
1425
|
capabilities: ["tool.mutate.any"],
|
|
1426
|
+
icon: "meta",
|
|
1281
1427
|
inputSchema: {
|
|
1282
1428
|
type: "object",
|
|
1283
1429
|
properties: {
|
|
@@ -1607,7 +1753,7 @@ function loadDatabaseSync() {
|
|
|
1607
1753
|
DatabaseSyncCtor = req("node:sqlite").DatabaseSync;
|
|
1608
1754
|
} catch (err) {
|
|
1609
1755
|
throw new Error(
|
|
1610
|
-
`The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${
|
|
1756
|
+
`The codebase index needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${toErrorMessage(err)}`
|
|
1611
1757
|
);
|
|
1612
1758
|
}
|
|
1613
1759
|
return DatabaseSyncCtor;
|
|
@@ -2053,39 +2199,57 @@ var IndexStore = class {
|
|
|
2053
2199
|
}
|
|
2054
2200
|
});
|
|
2055
2201
|
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Bulk-insert refs for many source symbols in a single transaction.
|
|
2204
|
+
*
|
|
2205
|
+
* Unlike {@link insertRefs} this does NOT delete per source id — the caller
|
|
2206
|
+
* (the indexer) has already cleared stale refs for the file via
|
|
2207
|
+
* {@link deleteRefsForFile}, so the per-source DELETE would be redundant work
|
|
2208
|
+
* repeated once per symbol. One transaction for the whole file instead of one
|
|
2209
|
+
* per symbol turns an O(symbols) transaction count into O(1).
|
|
2210
|
+
*
|
|
2211
|
+
* Each ref's own {@link Ref.fromId} is used; pass an empty array to no-op.
|
|
2212
|
+
*/
|
|
2213
|
+
insertRefsBatch(refs) {
|
|
2214
|
+
if (refs.length === 0) return;
|
|
2215
|
+
this.runWithRetry(() => {
|
|
2216
|
+
const stmt = this.db.prepare(
|
|
2217
|
+
`INSERT INTO refs(from_id, to_name, to_id, call_type, line)
|
|
2218
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
2219
|
+
);
|
|
2220
|
+
for (const ref of refs) {
|
|
2221
|
+
stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2056
2225
|
/**
|
|
2057
2226
|
* Delete all refs whose source symbols are in a given file.
|
|
2058
2227
|
* Used when re-indexing a file to clear stale refs.
|
|
2059
2228
|
*/
|
|
2060
2229
|
deleteRefsForFile(file) {
|
|
2061
2230
|
this.runWithRetry(() => {
|
|
2062
|
-
|
|
2063
|
-
"SELECT id FROM symbols WHERE file = ?"
|
|
2064
|
-
).
|
|
2065
|
-
if (!ids.length) return;
|
|
2066
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
2067
|
-
this.db.prepare(`DELETE FROM refs WHERE from_id IN (${placeholders})`).run(...ids.map((r) => r.id));
|
|
2231
|
+
this.db.prepare(
|
|
2232
|
+
"DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)"
|
|
2233
|
+
).run(file);
|
|
2068
2234
|
});
|
|
2069
2235
|
}
|
|
2070
2236
|
/**
|
|
2071
2237
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
2072
2238
|
* Call this after all symbols have been inserted to fill in cross-references.
|
|
2239
|
+
*
|
|
2240
|
+
* Single statement: the `to_name IN (SELECT name FROM symbols)` guard restricts
|
|
2241
|
+
* the UPDATE to refs that will actually resolve, so `.changes` counts only refs
|
|
2242
|
+
* that found a target — matching the previous per-row loop's return value.
|
|
2073
2243
|
*/
|
|
2074
2244
|
resolveRefs() {
|
|
2075
2245
|
return this.runWithRetry(() => {
|
|
2076
|
-
const
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
if (first) {
|
|
2084
|
-
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
2085
|
-
resolved++;
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
return resolved;
|
|
2246
|
+
const result = this.db.prepare(
|
|
2247
|
+
`UPDATE refs SET to_id = (
|
|
2248
|
+
SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
|
|
2249
|
+
) WHERE to_id IS NULL AND to_name IS NOT NULL
|
|
2250
|
+
AND to_name IN (SELECT name FROM symbols)`
|
|
2251
|
+
).run();
|
|
2252
|
+
return result.changes ?? 0;
|
|
2089
2253
|
});
|
|
2090
2254
|
}
|
|
2091
2255
|
/**
|
|
@@ -2192,7 +2356,7 @@ function getSignature(node, sourceFile) {
|
|
|
2192
2356
|
}
|
|
2193
2357
|
function getJsDoc(node, sourceFile) {
|
|
2194
2358
|
const fullText = sourceFile.getFullText();
|
|
2195
|
-
const nodePos = node.
|
|
2359
|
+
const nodePos = node.getFullStart();
|
|
2196
2360
|
const comments = ts.getLeadingCommentRanges(fullText, nodePos);
|
|
2197
2361
|
if (!comments) return "";
|
|
2198
2362
|
for (const range of comments) {
|
|
@@ -3560,14 +3724,27 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3560
3724
|
symbolsIndexed += count;
|
|
3561
3725
|
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
3562
3726
|
if (parsed.refs && parsed.refs.length > 0) {
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
if (
|
|
3567
|
-
|
|
3568
|
-
|
|
3727
|
+
const refsByLine = /* @__PURE__ */ new Map();
|
|
3728
|
+
for (const r of parsed.refs) {
|
|
3729
|
+
let arr = refsByLine.get(r.line);
|
|
3730
|
+
if (!arr) {
|
|
3731
|
+
arr = [];
|
|
3732
|
+
refsByLine.set(r.line, arr);
|
|
3733
|
+
}
|
|
3734
|
+
arr.push(r);
|
|
3735
|
+
}
|
|
3736
|
+
const batch = [];
|
|
3737
|
+
for (const sym of symbolsWithIds) {
|
|
3738
|
+
const symRefs = refsByLine.get(sym.line);
|
|
3739
|
+
if (symRefs) {
|
|
3740
|
+
for (const r of symRefs) {
|
|
3741
|
+
batch.push({ ...r, fromId: sym.id });
|
|
3742
|
+
}
|
|
3569
3743
|
}
|
|
3570
3744
|
}
|
|
3745
|
+
if (batch.length > 0) {
|
|
3746
|
+
store.insertRefsBatch(batch);
|
|
3747
|
+
}
|
|
3571
3748
|
}
|
|
3572
3749
|
store.upsertFile({
|
|
3573
3750
|
file,
|
|
@@ -3900,6 +4077,7 @@ async function codebaseIndexStats(args, opts = {}) {
|
|
|
3900
4077
|
var codebaseIndexTool = {
|
|
3901
4078
|
name: "codebase-index",
|
|
3902
4079
|
category: "Project",
|
|
4080
|
+
icon: "index",
|
|
3903
4081
|
description: "Build or incrementally update the project-wide symbol index. This powers fast codebase search and understanding. By default it only processes files that have changed since the last indexing run.",
|
|
3904
4082
|
usageHint: "IMPORTANT FOR LARGE CODEBASES:\n\n- First run (or after major changes): consider `force: true` for a clean rebuild.\n- Normal usage: call without arguments for fast incremental updates.\n- Use `langs` to restrict to specific languages if you only care about certain parts of the project.\nThis tool is relatively expensive \u2014 do not call it on every turn. Use it when the index is stale or before heavy codebase-search sessions.",
|
|
3905
4083
|
permission: "confirm",
|
|
@@ -3956,6 +4134,7 @@ var codebaseIndexTool = {
|
|
|
3956
4134
|
var codebaseSearchTool = {
|
|
3957
4135
|
name: "codebase-search",
|
|
3958
4136
|
category: "Project",
|
|
4137
|
+
icon: "index",
|
|
3959
4138
|
description: "Semantic/keyword search over the indexed codebase symbols (functions, classes, interfaces, etc.). Uses BM25 ranking. Much more powerful and structured than raw `grep` for finding code by name or concept.",
|
|
3960
4139
|
usageHint: "PREFERRED FOR CODE UNDERSTANDING:\n\n- Use when you need to find where something is defined or used by name.\n- `kind` filter is very useful (e.g. only functions or only interfaces).\n- Combine with `file` filter to scope to a specific directory or module.\nThis is generally better than `grep` when you are looking for symbols rather than arbitrary text patterns.",
|
|
3961
4140
|
permission: "auto",
|
|
@@ -4044,6 +4223,7 @@ var codebaseSearchTool = {
|
|
|
4044
4223
|
var codebaseStatsTool = {
|
|
4045
4224
|
name: "codebase-stats",
|
|
4046
4225
|
category: "Project",
|
|
4226
|
+
icon: "index",
|
|
4047
4227
|
description: "Return health and statistics about the current symbol index (total symbols, files, language/kind breakdown, size, last update). Useful to decide whether to re-index.",
|
|
4048
4228
|
usageHint: "CALL BEFORE HEAVY CODEBASE-SEARCH WORK:\n\n- Use to see if the index is up-to-date or needs a refresh.\n- No arguments required.\n- Helps avoid wasting tokens on searches against a stale index.\nLightweight and safe to call frequently.",
|
|
4049
4229
|
permission: "auto",
|
|
@@ -4104,6 +4284,7 @@ var diffTool = {
|
|
|
4104
4284
|
permission: "auto",
|
|
4105
4285
|
mutating: false,
|
|
4106
4286
|
capabilities: ["fs.read"],
|
|
4287
|
+
icon: "diff",
|
|
4107
4288
|
timeoutMs: 1e4,
|
|
4108
4289
|
inputSchema: {
|
|
4109
4290
|
type: "object",
|
|
@@ -4250,6 +4431,7 @@ var documentTool = {
|
|
|
4250
4431
|
mutating: false,
|
|
4251
4432
|
timeoutMs: 3e4,
|
|
4252
4433
|
capabilities: ["fs.read"],
|
|
4434
|
+
icon: "document",
|
|
4253
4435
|
inputSchema: {
|
|
4254
4436
|
type: "object",
|
|
4255
4437
|
properties: {
|
|
@@ -4328,10 +4510,10 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
4328
4510
|
}
|
|
4329
4511
|
function processFile(content, absPath, _style, _overwrite, target) {
|
|
4330
4512
|
const results = [];
|
|
4331
|
-
const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)
|
|
4332
|
-
const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s
|
|
4333
|
-
const classRegex = /class\s+(\w+)
|
|
4334
|
-
const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]
|
|
4513
|
+
const functionRegex = /(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
|
|
4514
|
+
const arrowRegex = /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/g;
|
|
4515
|
+
const classRegex = /class\s+(\w+)/g;
|
|
4516
|
+
const typeRegex = /(?:type|interface)\s+(\w+)\s*[=<]/g;
|
|
4335
4517
|
const allMatches = [];
|
|
4336
4518
|
if (target === "all" || target === "function") {
|
|
4337
4519
|
for (const m of content.matchAll(functionRegex)) {
|
|
@@ -4394,6 +4576,7 @@ var editTool = {
|
|
|
4394
4576
|
permission: "confirm",
|
|
4395
4577
|
mutating: true,
|
|
4396
4578
|
capabilities: ["fs.write"],
|
|
4579
|
+
icon: "edit",
|
|
4397
4580
|
timeoutMs: 5e3,
|
|
4398
4581
|
inputSchema: {
|
|
4399
4582
|
type: "object",
|
|
@@ -4616,6 +4799,7 @@ var execTool = {
|
|
|
4616
4799
|
riskTier: "standard",
|
|
4617
4800
|
timeoutMs: DEFAULT_TIMEOUT_MS2,
|
|
4618
4801
|
capabilities: ["shell.restricted"],
|
|
4802
|
+
icon: "terminal",
|
|
4619
4803
|
inputSchema: {
|
|
4620
4804
|
type: "object",
|
|
4621
4805
|
properties: {
|
|
@@ -4715,7 +4899,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4715
4899
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
4716
4900
|
const resolved = resolveWin32Command(cmd);
|
|
4717
4901
|
const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4718
|
-
const
|
|
4902
|
+
const spawnCmd = needsShell ? cmd : resolved;
|
|
4903
|
+
const child = spawn(spawnCmd, args, {
|
|
4719
4904
|
cwd,
|
|
4720
4905
|
env: buildChildEnv(sessionId),
|
|
4721
4906
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4902,6 +5087,7 @@ var fetchTool = {
|
|
|
4902
5087
|
permission: "confirm",
|
|
4903
5088
|
mutating: false,
|
|
4904
5089
|
capabilities: ["net.outbound"],
|
|
5090
|
+
icon: "web",
|
|
4905
5091
|
// Trust rules for fetch match on the literal URL — declare it explicitly
|
|
4906
5092
|
// so a user can trust `https://api.example.com/*` without accidentally
|
|
4907
5093
|
// matching that pattern on any other tool that happens to have a `url`
|
|
@@ -5051,6 +5237,7 @@ var formatTool = {
|
|
|
5051
5237
|
permission: "confirm",
|
|
5052
5238
|
mutating: true,
|
|
5053
5239
|
capabilities: ["fs.write", "shell.exec"],
|
|
5240
|
+
icon: "code",
|
|
5054
5241
|
timeoutMs: 6e4,
|
|
5055
5242
|
inputSchema: {
|
|
5056
5243
|
type: "object",
|
|
@@ -5151,6 +5338,7 @@ var gitTool = {
|
|
|
5151
5338
|
description: "Safe wrapper around common git operations. Supports status, log, diff, commit, branch, checkout, stash, push, pull, fetch, reset, worktree, etc. This is the preferred way to interact with git instead of using the raw `bash` or `exec` tools.",
|
|
5152
5339
|
usageHint: "ALWAYS prefer this tool over raw shell git commands.\n\nKey fields:\n- `command`: one of the supported subcommands (status, log, diff, commit, etc.)\n- Use `message` only for commit operations.\n- Use `files` array for operations that take paths (status, diff, add, etc.).\n- Non-mutating commands (status, log, diff, branch, fetch) are still permission:confirm for safety.\nNever pass raw git flags through `args` for dangerous operations \u2014 use the structured fields.",
|
|
5153
5340
|
permission: "confirm",
|
|
5341
|
+
icon: "git",
|
|
5154
5342
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
5155
5343
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
5156
5344
|
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
@@ -5410,6 +5598,7 @@ var globTool = {
|
|
|
5410
5598
|
permission: "auto",
|
|
5411
5599
|
mutating: false,
|
|
5412
5600
|
capabilities: ["fs.read"],
|
|
5601
|
+
icon: "folder",
|
|
5413
5602
|
maxOutputBytes: 65536,
|
|
5414
5603
|
timeoutMs: 5e3,
|
|
5415
5604
|
inputSchema: {
|
|
@@ -5542,6 +5731,7 @@ var grepTool = {
|
|
|
5542
5731
|
permission: "auto",
|
|
5543
5732
|
mutating: false,
|
|
5544
5733
|
capabilities: ["fs.read"],
|
|
5734
|
+
icon: "search",
|
|
5545
5735
|
maxOutputBytes: 131072,
|
|
5546
5736
|
timeoutMs: 1e4,
|
|
5547
5737
|
inputSchema: {
|
|
@@ -5828,6 +6018,7 @@ var installTool = {
|
|
|
5828
6018
|
permission: "confirm",
|
|
5829
6019
|
mutating: true,
|
|
5830
6020
|
riskTier: "standard",
|
|
6021
|
+
icon: "package",
|
|
5831
6022
|
timeoutMs: 12e4,
|
|
5832
6023
|
capabilities: ["package.install", "shell.restricted"],
|
|
5833
6024
|
inputSchema: {
|
|
@@ -5952,6 +6143,7 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
5952
6143
|
case "yarn":
|
|
5953
6144
|
case "npm":
|
|
5954
6145
|
return join(cwd, "package.json");
|
|
6146
|
+
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
5955
6147
|
default:
|
|
5956
6148
|
return join(cwd, "package.json");
|
|
5957
6149
|
}
|
|
@@ -5965,6 +6157,7 @@ var jsonTool = {
|
|
|
5965
6157
|
mutating: false,
|
|
5966
6158
|
timeoutMs: 5e3,
|
|
5967
6159
|
capabilities: ["fs.read"],
|
|
6160
|
+
icon: "json",
|
|
5968
6161
|
inputSchema: {
|
|
5969
6162
|
type: "object",
|
|
5970
6163
|
properties: {
|
|
@@ -6007,6 +6200,7 @@ var jsonTool = {
|
|
|
6007
6200
|
data: null,
|
|
6008
6201
|
formatted: "",
|
|
6009
6202
|
type: "unknown",
|
|
6203
|
+
/* v8 ignore next -- JSON.parse only throws SyntaxError (an Error); the String(e) side is defensive. */
|
|
6010
6204
|
error: `Parse failed: ${e instanceof Error ? e.message : String(e)}`
|
|
6011
6205
|
};
|
|
6012
6206
|
}
|
|
@@ -6089,6 +6283,7 @@ var lintTool = {
|
|
|
6089
6283
|
mutating: false,
|
|
6090
6284
|
timeoutMs: 6e4,
|
|
6091
6285
|
capabilities: ["shell.restricted"],
|
|
6286
|
+
icon: "code",
|
|
6092
6287
|
inputSchema: {
|
|
6093
6288
|
type: "object",
|
|
6094
6289
|
properties: {
|
|
@@ -6182,6 +6377,7 @@ var logsTool = {
|
|
|
6182
6377
|
mutating: false,
|
|
6183
6378
|
timeoutMs: 3e4,
|
|
6184
6379
|
capabilities: ["shell.restricted"],
|
|
6380
|
+
icon: "logs",
|
|
6185
6381
|
inputSchema: {
|
|
6186
6382
|
type: "object",
|
|
6187
6383
|
properties: {
|
|
@@ -6383,6 +6579,7 @@ var outdatedTool = {
|
|
|
6383
6579
|
description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
|
|
6384
6580
|
usageHint: "MAINTENANCE & SECURITY TOOL:\n\n- Run periodically or before dependency-related work.\n- Helps surface packages that may need updates for security or features.\n- Hits the package registry over HTTP, so it is NOT purely local \u2014 flagged as mutating for the confirmation gate.\nUse the output to decide on upgrades. Prefer this over manual shell commands for dependency hygiene.",
|
|
6385
6581
|
permission: "confirm",
|
|
6582
|
+
icon: "package",
|
|
6386
6583
|
// Network side-effecting (registry HTTP). Pairs with `mutating: true`
|
|
6387
6584
|
// so the H7 invariant test (`no auto-permission tool declares
|
|
6388
6585
|
// mutating: true`) passes — a tool claiming `'auto'` must be purely
|
|
@@ -6434,7 +6631,8 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
6434
6631
|
const MAX = 1e5;
|
|
6435
6632
|
const resolved = resolveWin32Command(manager);
|
|
6436
6633
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
6437
|
-
const
|
|
6634
|
+
const spawnCmd = needsShell ? manager : resolved;
|
|
6635
|
+
const child = spawn(spawnCmd, args, { cwd, signal, env: buildChildEnv(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true, ...needsShell ? { shell: true, windowsVerbatimArguments: true } : {} });
|
|
6438
6636
|
child.stdout?.on("data", (c) => {
|
|
6439
6637
|
if (stdout.length < MAX) stdout += c.toString();
|
|
6440
6638
|
});
|
|
@@ -6498,6 +6696,7 @@ var patchTool = {
|
|
|
6498
6696
|
permission: "confirm",
|
|
6499
6697
|
mutating: true,
|
|
6500
6698
|
capabilities: ["fs.write"],
|
|
6699
|
+
icon: "edit",
|
|
6501
6700
|
timeoutMs: 3e4,
|
|
6502
6701
|
inputSchema: {
|
|
6503
6702
|
type: "object",
|
|
@@ -6605,11 +6804,12 @@ function extractPatchedFiles(output) {
|
|
|
6605
6804
|
var planTool = {
|
|
6606
6805
|
name: "plan",
|
|
6607
6806
|
category: "Session",
|
|
6608
|
-
description:
|
|
6609
|
-
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns
|
|
6807
|
+
description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
|
|
6808
|
+
usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
|
|
6610
6809
|
permission: "confirm",
|
|
6611
6810
|
mutating: true,
|
|
6612
6811
|
capabilities: ["fs.write"],
|
|
6812
|
+
icon: "plan",
|
|
6613
6813
|
timeoutMs: 2e3,
|
|
6614
6814
|
inputSchema: {
|
|
6615
6815
|
type: "object",
|
|
@@ -6649,12 +6849,26 @@ var planTool = {
|
|
|
6649
6849
|
template: {
|
|
6650
6850
|
type: "string",
|
|
6651
6851
|
description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
|
|
6852
|
+
},
|
|
6853
|
+
scope: {
|
|
6854
|
+
type: "string",
|
|
6855
|
+
enum: ["session", "project"],
|
|
6856
|
+
description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
|
|
6652
6857
|
}
|
|
6653
6858
|
},
|
|
6654
6859
|
required: ["action"]
|
|
6655
6860
|
},
|
|
6656
6861
|
async execute(input, ctx) {
|
|
6657
|
-
const
|
|
6862
|
+
const sessionPlanPath = ctx.meta["plan.path"];
|
|
6863
|
+
let planPath;
|
|
6864
|
+
if (input.scope === "project") {
|
|
6865
|
+
if (typeof sessionPlanPath === "string") {
|
|
6866
|
+
const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
|
|
6867
|
+
planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
6868
|
+
}
|
|
6869
|
+
} else {
|
|
6870
|
+
planPath = sessionPlanPath;
|
|
6871
|
+
}
|
|
6658
6872
|
if (typeof planPath !== "string" || !planPath) {
|
|
6659
6873
|
return {
|
|
6660
6874
|
ok: false,
|
|
@@ -6668,148 +6882,169 @@ var planTool = {
|
|
|
6668
6882
|
let early = null;
|
|
6669
6883
|
const taskifyMeta = { title: "", details: "" };
|
|
6670
6884
|
let didTaskify = false;
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
case "done": {
|
|
6686
|
-
if (!input.target) {
|
|
6687
|
-
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6688
|
-
return p;
|
|
6689
|
-
}
|
|
6690
|
-
const next = setPlanItemStatus(
|
|
6691
|
-
p,
|
|
6692
|
-
input.target,
|
|
6693
|
-
input.action === "start" ? "in_progress" : "done"
|
|
6694
|
-
);
|
|
6695
|
-
if (next === p) {
|
|
6696
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6697
|
-
return p;
|
|
6698
|
-
}
|
|
6699
|
-
return next;
|
|
6700
|
-
}
|
|
6701
|
-
case "remove": {
|
|
6702
|
-
if (!input.target) {
|
|
6703
|
-
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
6704
|
-
return p;
|
|
6705
|
-
}
|
|
6706
|
-
const next = removePlanItem(p, input.target);
|
|
6707
|
-
if (next === p) {
|
|
6708
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6709
|
-
return p;
|
|
6710
|
-
}
|
|
6711
|
-
return next;
|
|
6712
|
-
}
|
|
6713
|
-
case "promote": {
|
|
6714
|
-
if (!input.target) {
|
|
6715
|
-
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6716
|
-
return p;
|
|
6717
|
-
}
|
|
6718
|
-
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
6719
|
-
if (!derived) {
|
|
6720
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6721
|
-
return p;
|
|
6885
|
+
let plan;
|
|
6886
|
+
try {
|
|
6887
|
+
plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
6888
|
+
switch (input.action) {
|
|
6889
|
+
case "show":
|
|
6890
|
+
break;
|
|
6891
|
+
case "add": {
|
|
6892
|
+
const title = input.title?.trim();
|
|
6893
|
+
if (!title) {
|
|
6894
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
6895
|
+
return p;
|
|
6896
|
+
}
|
|
6897
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
6898
|
+
return updated;
|
|
6722
6899
|
}
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6900
|
+
case "start":
|
|
6901
|
+
case "done": {
|
|
6902
|
+
if (!input.target) {
|
|
6903
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6904
|
+
return p;
|
|
6905
|
+
}
|
|
6906
|
+
const next = setPlanItemStatus(
|
|
6907
|
+
p,
|
|
6908
|
+
input.target,
|
|
6909
|
+
input.action === "start" ? "in_progress" : "done"
|
|
6910
|
+
);
|
|
6911
|
+
if (next === p) {
|
|
6912
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6913
|
+
return p;
|
|
6914
|
+
}
|
|
6915
|
+
return next;
|
|
6737
6916
|
}
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6917
|
+
case "remove": {
|
|
6918
|
+
if (!input.target) {
|
|
6919
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
6920
|
+
return p;
|
|
6921
|
+
}
|
|
6922
|
+
const next = removePlanItem(p, input.target);
|
|
6923
|
+
if (next === p) {
|
|
6924
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6925
|
+
return p;
|
|
6926
|
+
}
|
|
6927
|
+
return next;
|
|
6742
6928
|
}
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6929
|
+
case "promote": {
|
|
6930
|
+
if (!input.target) {
|
|
6931
|
+
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6932
|
+
return p;
|
|
6933
|
+
}
|
|
6934
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
6935
|
+
if (!derived) {
|
|
6936
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6937
|
+
return p;
|
|
6938
|
+
}
|
|
6939
|
+
ctx.state.replaceTodos(derived.todos);
|
|
6940
|
+
early = mkResult(
|
|
6941
|
+
derived.plan,
|
|
6942
|
+
true,
|
|
6943
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
6944
|
+
derived.todos
|
|
6945
|
+
);
|
|
6946
|
+
return derived.plan;
|
|
6746
6947
|
}
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6948
|
+
case "template_use": {
|
|
6949
|
+
const templateName = input.template?.trim();
|
|
6950
|
+
if (!templateName) {
|
|
6951
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
6952
|
+
return p;
|
|
6953
|
+
}
|
|
6954
|
+
const template = getPlanTemplate(templateName);
|
|
6955
|
+
if (!template) {
|
|
6956
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
6957
|
+
return p;
|
|
6958
|
+
}
|
|
6959
|
+
let updated = p;
|
|
6960
|
+
for (const item of template.items) {
|
|
6961
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
6962
|
+
}
|
|
6963
|
+
early = mkResult(
|
|
6964
|
+
updated,
|
|
6965
|
+
true,
|
|
6966
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
6967
|
+
);
|
|
6968
|
+
return updated;
|
|
6760
6969
|
}
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
if (itemIdx === -1) {
|
|
6768
|
-
const lower = input.target.toLowerCase();
|
|
6769
|
-
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
6970
|
+
case "clear":
|
|
6971
|
+
return clearPlan(p);
|
|
6972
|
+
case "taskify": {
|
|
6973
|
+
if (!input.target) {
|
|
6974
|
+
early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
|
|
6975
|
+
return p;
|
|
6770
6976
|
}
|
|
6977
|
+
let itemIdx = -1;
|
|
6978
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
6979
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
6980
|
+
itemIdx = asNum - 1;
|
|
6981
|
+
} else {
|
|
6982
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
6983
|
+
if (itemIdx === -1) {
|
|
6984
|
+
const lower = input.target.toLowerCase();
|
|
6985
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
6986
|
+
}
|
|
6987
|
+
}
|
|
6988
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
6989
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6990
|
+
return p;
|
|
6991
|
+
}
|
|
6992
|
+
const item = p.items[itemIdx];
|
|
6993
|
+
taskifyMeta.title = item.title;
|
|
6994
|
+
taskifyMeta.details = item.details ?? "";
|
|
6995
|
+
didTaskify = true;
|
|
6996
|
+
break;
|
|
6771
6997
|
}
|
|
6772
|
-
|
|
6773
|
-
early = mkResult(p, false, `
|
|
6998
|
+
default:
|
|
6999
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
6774
7000
|
return p;
|
|
6775
|
-
}
|
|
6776
|
-
const item = p.items[itemIdx];
|
|
6777
|
-
taskifyMeta.title = item.title;
|
|
6778
|
-
taskifyMeta.details = item.details ?? "";
|
|
6779
|
-
didTaskify = true;
|
|
6780
|
-
break;
|
|
6781
7001
|
}
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
7002
|
+
return p;
|
|
7003
|
+
});
|
|
7004
|
+
} catch (err) {
|
|
7005
|
+
return {
|
|
7006
|
+
ok: false,
|
|
7007
|
+
message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
7008
|
+
plan: "",
|
|
7009
|
+
count: 0,
|
|
7010
|
+
open: 0
|
|
7011
|
+
};
|
|
7012
|
+
}
|
|
6788
7013
|
if (early) return early;
|
|
6789
7014
|
if (didTaskify) {
|
|
6790
|
-
const
|
|
6791
|
-
if (typeof
|
|
7015
|
+
const taskPathRaw = ctx.meta["task.path"];
|
|
7016
|
+
if (typeof taskPathRaw !== "string" || !taskPathRaw) {
|
|
6792
7017
|
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
6793
7018
|
}
|
|
6794
|
-
|
|
7019
|
+
let taskPath = taskPathRaw;
|
|
7020
|
+
if (input.scope === "project") {
|
|
7021
|
+
const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
|
|
7022
|
+
taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
7023
|
+
}
|
|
6795
7024
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
7025
|
+
try {
|
|
7026
|
+
const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
|
|
7027
|
+
f.tasks.push({
|
|
7028
|
+
id: `task_${randomUUID()}`,
|
|
7029
|
+
title: taskifyMeta.title,
|
|
7030
|
+
description: taskifyMeta.details || void 0,
|
|
7031
|
+
type: "feature",
|
|
7032
|
+
priority: "medium",
|
|
7033
|
+
status: "pending",
|
|
7034
|
+
createdAt: now,
|
|
7035
|
+
updatedAt: now
|
|
7036
|
+
});
|
|
7037
|
+
return f;
|
|
7038
|
+
});
|
|
7039
|
+
return mkResult(
|
|
7040
|
+
plan,
|
|
7041
|
+
true,
|
|
7042
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
6811
7043
|
${formatTaskList(taskFile.tasks)}`
|
|
6812
|
-
|
|
7044
|
+
);
|
|
7045
|
+
} catch (err) {
|
|
7046
|
+
return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
7047
|
+
}
|
|
6813
7048
|
}
|
|
6814
7049
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
6815
7050
|
}
|
|
@@ -6835,6 +7070,7 @@ var readTool = {
|
|
|
6835
7070
|
permission: "auto",
|
|
6836
7071
|
mutating: false,
|
|
6837
7072
|
capabilities: ["fs.read"],
|
|
7073
|
+
icon: "file",
|
|
6838
7074
|
maxOutputBytes: 262144,
|
|
6839
7075
|
timeoutMs: 5e3,
|
|
6840
7076
|
inputSchema: {
|
|
@@ -6865,7 +7101,7 @@ var readTool = {
|
|
|
6865
7101
|
const code = err.code;
|
|
6866
7102
|
if (code === "ENOENT") throw new Error(`read: file not found "${input.path}"`);
|
|
6867
7103
|
throw new Error(
|
|
6868
|
-
`read: failed to stat "${input.path}": ${
|
|
7104
|
+
`read: failed to stat "${input.path}": ${toErrorMessage(err)}`
|
|
6869
7105
|
);
|
|
6870
7106
|
}
|
|
6871
7107
|
if (!stat11.isFile()) throw new Error(`read: "${input.path}" is not a regular file`);
|
|
@@ -6907,6 +7143,7 @@ var replaceTool = {
|
|
|
6907
7143
|
permission: "confirm",
|
|
6908
7144
|
mutating: true,
|
|
6909
7145
|
capabilities: ["fs.write"],
|
|
7146
|
+
icon: "edit",
|
|
6910
7147
|
timeoutMs: 3e4,
|
|
6911
7148
|
inputSchema: {
|
|
6912
7149
|
type: "object",
|
|
@@ -7198,6 +7435,7 @@ var scaffoldTool = {
|
|
|
7198
7435
|
permission: "confirm",
|
|
7199
7436
|
mutating: true,
|
|
7200
7437
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
7438
|
+
icon: "scaffold",
|
|
7201
7439
|
timeoutMs: 3e4,
|
|
7202
7440
|
inputSchema: {
|
|
7203
7441
|
type: "object",
|
|
@@ -7293,6 +7531,7 @@ var searchTool = {
|
|
|
7293
7531
|
permission: "confirm",
|
|
7294
7532
|
mutating: false,
|
|
7295
7533
|
capabilities: ["net.outbound"],
|
|
7534
|
+
icon: "search",
|
|
7296
7535
|
timeoutMs: TIMEOUT_MS3,
|
|
7297
7536
|
inputSchema: {
|
|
7298
7537
|
type: "object",
|
|
@@ -7367,7 +7606,7 @@ async function duckduckgoSearch(query2, num, signal) {
|
|
|
7367
7606
|
truncated: results.length >= num
|
|
7368
7607
|
};
|
|
7369
7608
|
} catch (err) {
|
|
7370
|
-
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error:
|
|
7609
|
+
console.log(JSON.stringify({ level: "debug", event: "search_failed", query: query2, error: toErrorMessage(err) }));
|
|
7371
7610
|
return {
|
|
7372
7611
|
query: query2,
|
|
7373
7612
|
results: [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }],
|
|
@@ -7506,6 +7745,7 @@ var setWorkingDirTool = {
|
|
|
7506
7745
|
permission: "confirm",
|
|
7507
7746
|
mutating: true,
|
|
7508
7747
|
capabilities: ["fs.read"],
|
|
7748
|
+
icon: "settings",
|
|
7509
7749
|
timeoutMs: 5e3,
|
|
7510
7750
|
inputSchema: {
|
|
7511
7751
|
type: "object",
|
|
@@ -7530,7 +7770,7 @@ var setWorkingDirTool = {
|
|
|
7530
7770
|
} catch (err) {
|
|
7531
7771
|
return {
|
|
7532
7772
|
current: ctx.workingDir,
|
|
7533
|
-
error:
|
|
7773
|
+
error: toErrorMessage(err)
|
|
7534
7774
|
};
|
|
7535
7775
|
}
|
|
7536
7776
|
try {
|
|
@@ -7566,11 +7806,12 @@ function findTaskIndex(tasks, query2) {
|
|
|
7566
7806
|
var taskTool = {
|
|
7567
7807
|
name: "task",
|
|
7568
7808
|
category: "Session",
|
|
7569
|
-
description:
|
|
7570
|
-
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate',
|
|
7809
|
+
description: 'Manage session-persistent structured work items with dependencies, types, and priorities. Unlike `todo` (flat, tactical), `task` supports typed work (feature/bugfix/refactor/etc.), dependencies between items, priority ranking, and agent assignment. Tasks are written to disk and survive session resumes. By default they are isolated to this session; use `scope: "project"` to store tasks in a shared project-level file visible to all sessions.',
|
|
7810
|
+
usageHint: 'USE FOR STRUCTURED WORK:\n- `action: "replace"` \u2014 set the complete task list (tasks ordered by priority)\n- `action: "add"` \u2014 append a single task\n- `action: "status"` \u2014 update a task\'s status (e.g. pending\u2192in_progress, in_progress\u2192completed)\n- `action: "show"` \u2014 view current tasks without changing them\n- `action: "promote"` \u2014 convert a task into actionable todo items via `target` (id|index|substring)\n- `action: "planify"` \u2014 promote a task to a plan item (strategic level) via `target` (id|index|substring)\n\nTask fields:\n- `dependsOn`: list of task IDs this one waits for\n- `type`: "feature" | "bugfix" | "refactor" | "docs" | "test" | "chore"\n- `priority`: "critical" | "high" | "medium" | "low"\n- `assignee`: agent/subagent name (e.g. "bug-hunter", "refactor-planner")\n- `estimateHours`: rough time estimate\n- `scope`: "session" (default, isolated) or "project" (shared across sessions)',
|
|
7571
7811
|
permission: "confirm",
|
|
7572
7812
|
mutating: true,
|
|
7573
7813
|
capabilities: ["fs.write"],
|
|
7814
|
+
icon: "task",
|
|
7574
7815
|
timeoutMs: 2e3,
|
|
7575
7816
|
inputSchema: {
|
|
7576
7817
|
type: "object",
|
|
@@ -7636,12 +7877,26 @@ var taskTool = {
|
|
|
7636
7877
|
type: "array",
|
|
7637
7878
|
items: { type: "string" },
|
|
7638
7879
|
description: "Optional subtask titles for action=promote. Each becomes a pending todo."
|
|
7880
|
+
},
|
|
7881
|
+
scope: {
|
|
7882
|
+
type: "string",
|
|
7883
|
+
enum: ["session", "project"],
|
|
7884
|
+
description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
|
|
7639
7885
|
}
|
|
7640
7886
|
},
|
|
7641
7887
|
required: ["action"]
|
|
7642
7888
|
},
|
|
7643
7889
|
async execute(input, ctx) {
|
|
7644
|
-
const
|
|
7890
|
+
const sessionTaskPath = ctx.meta["task.path"];
|
|
7891
|
+
let taskPath;
|
|
7892
|
+
if (input.scope === "project") {
|
|
7893
|
+
if (typeof sessionTaskPath === "string") {
|
|
7894
|
+
const lastSep = Math.max(sessionTaskPath.lastIndexOf("/"), sessionTaskPath.lastIndexOf("\\"));
|
|
7895
|
+
taskPath = lastSep >= 0 ? sessionTaskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
7896
|
+
}
|
|
7897
|
+
} else {
|
|
7898
|
+
taskPath = sessionTaskPath;
|
|
7899
|
+
}
|
|
7645
7900
|
if (typeof taskPath !== "string" || !taskPath) {
|
|
7646
7901
|
return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
|
|
7647
7902
|
}
|
|
@@ -7651,23 +7906,66 @@ var taskTool = {
|
|
|
7651
7906
|
const planifyMeta = { title: "", details: "" };
|
|
7652
7907
|
let didPlanify = false;
|
|
7653
7908
|
let todosToReplace = null;
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7909
|
+
let file;
|
|
7910
|
+
try {
|
|
7911
|
+
file = await mutateTasks(taskPath, sessionId, async (f) => {
|
|
7912
|
+
switch (input.action) {
|
|
7913
|
+
case "show":
|
|
7914
|
+
break;
|
|
7915
|
+
case "replace": {
|
|
7916
|
+
if (!Array.isArray(input.tasks)) {
|
|
7917
|
+
early = { ok: false, message: "action=replace requires `tasks` array.", count: 0, completed: 0, inProgress: 0 };
|
|
7918
|
+
return f;
|
|
7919
|
+
}
|
|
7920
|
+
const newIds = new Set(input.tasks.map((t) => t.id));
|
|
7921
|
+
if (newIds.size !== input.tasks.length) {
|
|
7922
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7923
|
+
const dupes = [...new Set(input.tasks.map((t) => t.id).filter((id) => seen.has(id) ? true : (seen.add(id), false)))];
|
|
7924
|
+
early = {
|
|
7925
|
+
ok: false,
|
|
7926
|
+
message: `action=replace has duplicate task IDs: ${dupes.join(", ")}. Each task id must be unique.`,
|
|
7927
|
+
count: 0,
|
|
7928
|
+
completed: 0,
|
|
7929
|
+
inProgress: 0
|
|
7930
|
+
};
|
|
7931
|
+
return f;
|
|
7932
|
+
}
|
|
7933
|
+
for (const t of input.tasks) {
|
|
7934
|
+
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7935
|
+
const missing = t.dependsOn.filter((d) => !newIds.has(d));
|
|
7936
|
+
if (missing.length > 0) {
|
|
7937
|
+
early = {
|
|
7938
|
+
ok: false,
|
|
7939
|
+
message: `dependsOn validation failed: task "${t.id}" references unknown IDs: ${missing.join(", ")}`,
|
|
7940
|
+
count: 0,
|
|
7941
|
+
completed: 0,
|
|
7942
|
+
inProgress: 0
|
|
7943
|
+
};
|
|
7944
|
+
return f;
|
|
7945
|
+
}
|
|
7946
|
+
}
|
|
7947
|
+
}
|
|
7948
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7949
|
+
f.tasks = input.tasks.map((t) => ({
|
|
7950
|
+
...t,
|
|
7951
|
+
createdAt: t.createdAt || now,
|
|
7952
|
+
updatedAt: now
|
|
7953
|
+
}));
|
|
7954
|
+
break;
|
|
7662
7955
|
}
|
|
7663
|
-
|
|
7664
|
-
|
|
7956
|
+
case "add": {
|
|
7957
|
+
const t = input.task;
|
|
7958
|
+
if (!t || !t.title) {
|
|
7959
|
+
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
7960
|
+
return f;
|
|
7961
|
+
}
|
|
7665
7962
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7666
|
-
const
|
|
7963
|
+
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
7964
|
+
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
7667
7965
|
if (missing.length > 0) {
|
|
7668
7966
|
early = {
|
|
7669
7967
|
ok: false,
|
|
7670
|
-
message: `dependsOn validation failed: task
|
|
7968
|
+
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
7671
7969
|
count: 0,
|
|
7672
7970
|
completed: 0,
|
|
7673
7971
|
inProgress: 0
|
|
@@ -7675,165 +7973,170 @@ var taskTool = {
|
|
|
7675
7973
|
return f;
|
|
7676
7974
|
}
|
|
7677
7975
|
}
|
|
7976
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7977
|
+
const newTask = {
|
|
7978
|
+
id: `task_${Date.now()}_${randomUUID().slice(0, 8)}`,
|
|
7979
|
+
title: t.title,
|
|
7980
|
+
description: t.description,
|
|
7981
|
+
type: t.type || "feature",
|
|
7982
|
+
priority: t.priority || "medium",
|
|
7983
|
+
status: t.status || "pending",
|
|
7984
|
+
dependsOn: t.dependsOn,
|
|
7985
|
+
assignee: t.assignee,
|
|
7986
|
+
estimateHours: t.estimateHours,
|
|
7987
|
+
tags: t.tags,
|
|
7988
|
+
createdAt: now,
|
|
7989
|
+
updatedAt: now
|
|
7990
|
+
};
|
|
7991
|
+
f.tasks.push(newTask);
|
|
7992
|
+
break;
|
|
7678
7993
|
}
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
createdAt: t.createdAt || now,
|
|
7683
|
-
updatedAt: now
|
|
7684
|
-
}));
|
|
7685
|
-
break;
|
|
7686
|
-
}
|
|
7687
|
-
case "add": {
|
|
7688
|
-
const t = input.task;
|
|
7689
|
-
if (!t || !t.title) {
|
|
7690
|
-
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
7691
|
-
return f;
|
|
7692
|
-
}
|
|
7693
|
-
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7694
|
-
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
7695
|
-
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
7696
|
-
if (missing.length > 0) {
|
|
7697
|
-
early = {
|
|
7698
|
-
ok: false,
|
|
7699
|
-
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
7700
|
-
count: 0,
|
|
7701
|
-
completed: 0,
|
|
7702
|
-
inProgress: 0
|
|
7703
|
-
};
|
|
7994
|
+
case "status": {
|
|
7995
|
+
if (!input.id || !input.status) {
|
|
7996
|
+
early = { ok: false, message: "action=status requires `id` and `status`.", count: 0, completed: 0, inProgress: 0 };
|
|
7704
7997
|
return f;
|
|
7705
7998
|
}
|
|
7999
|
+
const task = f.tasks.find((t) => t.id === input.id);
|
|
8000
|
+
if (!task) {
|
|
8001
|
+
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
8002
|
+
return f;
|
|
8003
|
+
}
|
|
8004
|
+
task.status = input.status;
|
|
8005
|
+
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8006
|
+
break;
|
|
7706
8007
|
}
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
}
|
|
7730
|
-
const task = f.tasks.find((t) => t.id === input.id);
|
|
7731
|
-
if (!task) {
|
|
7732
|
-
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
7733
|
-
return f;
|
|
7734
|
-
}
|
|
7735
|
-
task.status = input.status;
|
|
7736
|
-
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7737
|
-
break;
|
|
7738
|
-
}
|
|
7739
|
-
case "promote": {
|
|
7740
|
-
const target = input.target?.trim();
|
|
7741
|
-
if (!target) {
|
|
7742
|
-
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
7743
|
-
return f;
|
|
7744
|
-
}
|
|
7745
|
-
const idx = findTaskIndex(f.tasks, target);
|
|
7746
|
-
if (idx === -1) {
|
|
7747
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7748
|
-
return f;
|
|
7749
|
-
}
|
|
7750
|
-
const match = f.tasks[idx];
|
|
7751
|
-
if (!match) {
|
|
7752
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7753
|
-
return f;
|
|
7754
|
-
}
|
|
7755
|
-
if (match.status !== "completed" && match.status !== "failed") {
|
|
7756
|
-
match.status = "in_progress";
|
|
7757
|
-
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7758
|
-
}
|
|
7759
|
-
const todos = [];
|
|
7760
|
-
const ts2 = Date.now();
|
|
7761
|
-
todos.push({
|
|
7762
|
-
id: `todo_${ts2}_task`,
|
|
7763
|
-
content: match.title,
|
|
7764
|
-
status: "in_progress",
|
|
7765
|
-
activeForm: match.title,
|
|
7766
|
-
promotedFromTask: match.id
|
|
7767
|
-
});
|
|
7768
|
-
if (match.description) {
|
|
8008
|
+
case "promote": {
|
|
8009
|
+
const target = input.target?.trim();
|
|
8010
|
+
if (!target) {
|
|
8011
|
+
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
8012
|
+
return f;
|
|
8013
|
+
}
|
|
8014
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
8015
|
+
if (idx === -1) {
|
|
8016
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8017
|
+
return f;
|
|
8018
|
+
}
|
|
8019
|
+
const match = f.tasks[idx];
|
|
8020
|
+
if (!match) {
|
|
8021
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8022
|
+
return f;
|
|
8023
|
+
}
|
|
8024
|
+
if (match.status !== "completed" && match.status !== "failed") {
|
|
8025
|
+
match.status = "in_progress";
|
|
8026
|
+
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8027
|
+
}
|
|
8028
|
+
const todos = [];
|
|
8029
|
+
const ts2 = Date.now();
|
|
7769
8030
|
todos.push({
|
|
7770
|
-
id: `todo_${ts2}
|
|
7771
|
-
content: match.
|
|
7772
|
-
status: "
|
|
8031
|
+
id: `todo_${ts2}_task`,
|
|
8032
|
+
content: match.title,
|
|
8033
|
+
status: "in_progress",
|
|
8034
|
+
activeForm: match.title,
|
|
7773
8035
|
promotedFromTask: match.id
|
|
7774
8036
|
});
|
|
7775
|
-
|
|
7776
|
-
if (input.subtasks && input.subtasks.length > 0) {
|
|
7777
|
-
for (const st of input.subtasks) {
|
|
8037
|
+
if (match.description) {
|
|
7778
8038
|
todos.push({
|
|
7779
8039
|
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
7780
|
-
content:
|
|
8040
|
+
content: match.description.slice(0, 200),
|
|
7781
8041
|
status: "pending",
|
|
7782
8042
|
promotedFromTask: match.id
|
|
7783
8043
|
});
|
|
7784
8044
|
}
|
|
8045
|
+
if (input.subtasks && input.subtasks.length > 0) {
|
|
8046
|
+
for (const st of input.subtasks) {
|
|
8047
|
+
todos.push({
|
|
8048
|
+
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
8049
|
+
content: st,
|
|
8050
|
+
status: "pending",
|
|
8051
|
+
promotedFromTask: match.id
|
|
8052
|
+
});
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
todosToReplace = todos;
|
|
8056
|
+
promoteMeta.count = todos.length;
|
|
8057
|
+
promoteMeta.title = match.title;
|
|
8058
|
+
break;
|
|
7785
8059
|
}
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
8060
|
+
case "planify": {
|
|
8061
|
+
const target = input.target?.trim();
|
|
8062
|
+
if (!target) {
|
|
8063
|
+
early = { ok: false, message: "action=planify requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
8064
|
+
return f;
|
|
8065
|
+
}
|
|
8066
|
+
const idx = findTaskIndex(f.tasks, target);
|
|
8067
|
+
if (idx === -1) {
|
|
8068
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8069
|
+
return f;
|
|
8070
|
+
}
|
|
8071
|
+
const match = f.tasks[idx];
|
|
8072
|
+
if (!match) {
|
|
8073
|
+
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8074
|
+
return f;
|
|
8075
|
+
}
|
|
8076
|
+
planifyMeta.title = match.title;
|
|
8077
|
+
planifyMeta.details = match.description ?? "";
|
|
8078
|
+
didPlanify = true;
|
|
8079
|
+
break;
|
|
7801
8080
|
}
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
8081
|
+
default:
|
|
8082
|
+
early = { ok: false, message: `Unknown action "${input.action}". Use replace | add | status | show | promote | planify.`, count: 0, completed: 0, inProgress: 0 };
|
|
7805
8083
|
return f;
|
|
7806
|
-
}
|
|
7807
|
-
planifyMeta.title = match.title;
|
|
7808
|
-
planifyMeta.details = match.description ?? "";
|
|
7809
|
-
didPlanify = true;
|
|
7810
|
-
break;
|
|
7811
8084
|
}
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
8085
|
+
return f;
|
|
8086
|
+
});
|
|
8087
|
+
} catch (err) {
|
|
8088
|
+
return {
|
|
8089
|
+
ok: false,
|
|
8090
|
+
message: `Task change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
8091
|
+
count: 0,
|
|
8092
|
+
completed: 0,
|
|
8093
|
+
inProgress: 0
|
|
8094
|
+
};
|
|
8095
|
+
}
|
|
7818
8096
|
if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
|
|
7819
8097
|
if (early) return early;
|
|
7820
8098
|
if (didPlanify) {
|
|
7821
8099
|
const { title, details } = planifyMeta;
|
|
7822
|
-
const
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
8100
|
+
const planPathRaw = ctx.meta["plan.path"];
|
|
8101
|
+
const prog = computeTaskItemProgress(file.tasks);
|
|
8102
|
+
if (typeof planPathRaw === "string" && planPathRaw) {
|
|
8103
|
+
let planPath = planPathRaw;
|
|
8104
|
+
if (input.scope === "project") {
|
|
8105
|
+
const lastSep = Math.max(planPath.lastIndexOf("/"), planPath.lastIndexOf("\\"));
|
|
8106
|
+
planPath = lastSep >= 0 ? planPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
8107
|
+
}
|
|
8108
|
+
let formatted = "";
|
|
8109
|
+
try {
|
|
8110
|
+
await mutatePlan(planPath, sessionId, (pf) => {
|
|
8111
|
+
const { plan: updated } = addPlanItem(pf, title, details || void 0);
|
|
8112
|
+
formatted = formatPlan(updated);
|
|
8113
|
+
return updated;
|
|
8114
|
+
});
|
|
8115
|
+
} catch (err) {
|
|
8116
|
+
return {
|
|
8117
|
+
ok: false,
|
|
8118
|
+
message: `planify: plan not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
8119
|
+
count: file.tasks.length,
|
|
8120
|
+
completed: prog.completed,
|
|
8121
|
+
inProgress: prog.inProgress
|
|
8122
|
+
};
|
|
8123
|
+
}
|
|
7827
8124
|
return {
|
|
7828
8125
|
ok: true,
|
|
7829
8126
|
message: `planify ok \u2014 added "${title}" to plan.
|
|
7830
|
-
${
|
|
8127
|
+
${formatted}`,
|
|
7831
8128
|
count: file.tasks.length,
|
|
7832
|
-
completed:
|
|
7833
|
-
inProgress:
|
|
8129
|
+
completed: prog.completed,
|
|
8130
|
+
inProgress: prog.inProgress
|
|
7834
8131
|
};
|
|
7835
8132
|
}
|
|
7836
|
-
return {
|
|
8133
|
+
return {
|
|
8134
|
+
ok: false,
|
|
8135
|
+
message: "Plan storage path not configured \u2014 cannot planify.",
|
|
8136
|
+
count: file.tasks.length,
|
|
8137
|
+
completed: prog.completed,
|
|
8138
|
+
inProgress: prog.inProgress
|
|
8139
|
+
};
|
|
7837
8140
|
}
|
|
7838
8141
|
const p = computeTaskItemProgress(file.tasks);
|
|
7839
8142
|
const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
|
|
@@ -7854,6 +8157,7 @@ var testTool = {
|
|
|
7854
8157
|
usageHint: "ESSENTIAL BEFORE CONSIDERING WORK DONE:\n\n- Use `files` or `grep` to run only relevant tests during development.\n- `coverage: true` is useful when working on critical paths.\nRun tests frequently. A clean test run is usually required before the task can be considered complete.",
|
|
7855
8158
|
permission: "confirm",
|
|
7856
8159
|
mutating: false,
|
|
8160
|
+
icon: "test",
|
|
7857
8161
|
timeoutMs: 12e4,
|
|
7858
8162
|
capabilities: ["shell.restricted"],
|
|
7859
8163
|
inputSchema: {
|
|
@@ -8013,6 +8317,7 @@ var todoTool = {
|
|
|
8013
8317
|
// mutates only conversation state (ctx.todos), not external state — no confirmation needed
|
|
8014
8318
|
timeoutMs: 1e3,
|
|
8015
8319
|
capabilities: ["session.todo"],
|
|
8320
|
+
icon: "todo",
|
|
8016
8321
|
inputSchema: {
|
|
8017
8322
|
type: "object",
|
|
8018
8323
|
properties: {
|
|
@@ -8121,6 +8426,7 @@ var toolHelpTool = {
|
|
|
8121
8426
|
mutating: false,
|
|
8122
8427
|
timeoutMs: 5e3,
|
|
8123
8428
|
capabilities: ["tool.meta"],
|
|
8429
|
+
icon: "meta",
|
|
8124
8430
|
inputSchema: {
|
|
8125
8431
|
type: "object",
|
|
8126
8432
|
properties: {
|
|
@@ -8244,6 +8550,7 @@ var toolSearchTool = {
|
|
|
8244
8550
|
mutating: false,
|
|
8245
8551
|
timeoutMs: 1e3,
|
|
8246
8552
|
capabilities: ["tool.meta"],
|
|
8553
|
+
icon: "meta",
|
|
8247
8554
|
inputSchema: {
|
|
8248
8555
|
type: "object",
|
|
8249
8556
|
properties: {
|
|
@@ -8323,6 +8630,7 @@ var toolUseTool = {
|
|
|
8323
8630
|
mutating: true,
|
|
8324
8631
|
timeoutMs: 6e4,
|
|
8325
8632
|
capabilities: ["tool.mutate.any"],
|
|
8633
|
+
icon: "meta",
|
|
8326
8634
|
inputSchema: {
|
|
8327
8635
|
type: "object",
|
|
8328
8636
|
properties: {
|
|
@@ -8403,6 +8711,7 @@ var treeTool = {
|
|
|
8403
8711
|
permission: "auto",
|
|
8404
8712
|
mutating: false,
|
|
8405
8713
|
capabilities: ["fs.read"],
|
|
8714
|
+
icon: "tree",
|
|
8406
8715
|
timeoutMs: 15e3,
|
|
8407
8716
|
inputSchema: {
|
|
8408
8717
|
type: "object",
|
|
@@ -8568,6 +8877,7 @@ var typecheckTool = {
|
|
|
8568
8877
|
mutating: false,
|
|
8569
8878
|
timeoutMs: 12e4,
|
|
8570
8879
|
capabilities: ["shell.restricted"],
|
|
8880
|
+
icon: "code",
|
|
8571
8881
|
inputSchema: {
|
|
8572
8882
|
type: "object",
|
|
8573
8883
|
properties: {
|
|
@@ -8656,6 +8966,7 @@ var writeTool = {
|
|
|
8656
8966
|
mutating: true,
|
|
8657
8967
|
timeoutMs: 5e3,
|
|
8658
8968
|
capabilities: ["fs.write"],
|
|
8969
|
+
icon: "file",
|
|
8659
8970
|
inputSchema: {
|
|
8660
8971
|
type: "object",
|
|
8661
8972
|
properties: {
|