@wrongstack/tools 0.264.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 -11
- 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 +661 -325
- 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 +59 -25
- package/dist/codebase-index/index.js.map +1 -1
- package/dist/codebase-index/worker.js +56 -25
- package/dist/codebase-index/worker.js.map +1 -1
- package/dist/diff.js +14 -7
- package/dist/diff.js.map +1 -1
- package/dist/document.js +14 -8
- package/dist/document.js.map +1 -1
- package/dist/edit.js +21 -15
- package/dist/edit.js.map +1 -1
- package/dist/exec.js +140 -3
- package/dist/exec.js.map +1 -1
- package/dist/fetch.js +1 -0
- package/dist/fetch.js.map +1 -1
- package/dist/format.js +153 -11
- 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 -7
- package/dist/glob.js.map +1 -1
- package/dist/grep.js +14 -7
- package/dist/grep.js.map +1 -1
- package/dist/index.d.ts +55 -3
- package/dist/index.js +819 -325
- package/dist/index.js.map +1 -1
- package/dist/install.js +153 -11
- package/dist/install.js.map +1 -1
- package/dist/json.js +1 -0
- package/dist/json.js.map +1 -1
- package/dist/lint.js +153 -11
- package/dist/lint.js.map +1 -1
- package/dist/logs.js +14 -7
- 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 -8
- package/dist/outdated.js.map +1 -1
- package/dist/pack.js +630 -324
- package/dist/pack.js.map +1 -1
- package/dist/patch.js +14 -7
- 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 +21 -16
- package/dist/read.js.map +1 -1
- package/dist/replace.js +14 -7
- package/dist/replace.js.map +1 -1
- package/dist/scaffold.js +14 -7
- package/dist/scaffold.js.map +1 -1
- package/dist/search.js +1 -0
- package/dist/search.js.map +1 -1
- package/dist/test.js +153 -11
- 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 -7
- package/dist/tree.js.map +1 -1
- package/dist/typecheck.js +153 -11
- package/dist/typecheck.js.map +1 -1
- package/dist/write.js +21 -15
- package/dist/write.js.map +1 -1
- package/package.json +6 -2
package/dist/pack.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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';
|
|
@@ -142,6 +142,23 @@ var CircuitBreaker = class {
|
|
|
142
142
|
lastSlowAt = null;
|
|
143
143
|
/** Timestamp when the breaker was opened (for cooldown calculation). */
|
|
144
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;
|
|
145
162
|
constructor(config = {}) {
|
|
146
163
|
this.maxConsecutiveFailures = config.maxConsecutiveFailures ?? DEFAULT_MAX_CONSECUTIVE_FAILURES;
|
|
147
164
|
this.slowCallThresholdMs = config.slowCallThresholdMs ?? DEFAULT_SLOW_CALL_THRESHOLD_MS;
|
|
@@ -150,12 +167,22 @@ var CircuitBreaker = class {
|
|
|
150
167
|
this.maxCallsPerWindow = config.maxCallsPerWindow ?? DEFAULT_MAX_CALLS_PER_WINDOW;
|
|
151
168
|
this.cooldownMs = config.cooldownMs ?? DEFAULT_COOLDOWN_MS;
|
|
152
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
|
+
}
|
|
153
179
|
/**
|
|
154
180
|
* Returns true if the circuit allows a new call to proceed.
|
|
155
181
|
* When false, callers should abort the tool call and return a
|
|
156
182
|
* circuit-breaker error instead of spawning a process.
|
|
157
183
|
*/
|
|
158
184
|
get canProceed() {
|
|
185
|
+
if (!this.enabled) return true;
|
|
159
186
|
this._checkStateTransition();
|
|
160
187
|
return this.state !== "open";
|
|
161
188
|
}
|
|
@@ -191,7 +218,7 @@ var CircuitBreaker = class {
|
|
|
191
218
|
* not affect breaker state.
|
|
192
219
|
*/
|
|
193
220
|
beforeCall(bypass = false) {
|
|
194
|
-
if (bypass) return true;
|
|
221
|
+
if (bypass || !this.enabled) return true;
|
|
195
222
|
this._checkStateTransition();
|
|
196
223
|
if (this.state === "open") return false;
|
|
197
224
|
return true;
|
|
@@ -206,7 +233,7 @@ var CircuitBreaker = class {
|
|
|
206
233
|
* Use for background/fire-and-forget processes.
|
|
207
234
|
*/
|
|
208
235
|
afterCall(durationMs, failed, bypass = false) {
|
|
209
|
-
if (bypass) return;
|
|
236
|
+
if (bypass || !this.enabled) return;
|
|
210
237
|
const now = Date.now();
|
|
211
238
|
if (this.state === "half-open") {
|
|
212
239
|
if (failed) {
|
|
@@ -252,12 +279,23 @@ var CircuitBreaker = class {
|
|
|
252
279
|
if (this.state === "open") return;
|
|
253
280
|
this.state = "open";
|
|
254
281
|
this.openedAt = Date.now();
|
|
282
|
+
try {
|
|
283
|
+
this.onTrip?.();
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
255
286
|
}
|
|
256
287
|
_reset() {
|
|
288
|
+
const wasRecovering = this.state !== "closed";
|
|
257
289
|
this.state = "closed";
|
|
258
290
|
this.consecutiveFailures = 0;
|
|
259
291
|
this.window = [];
|
|
260
292
|
this.openedAt = null;
|
|
293
|
+
if (wasRecovering) {
|
|
294
|
+
try {
|
|
295
|
+
this.onReset?.();
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
261
299
|
}
|
|
262
300
|
/** Transition from open → half-open when cooldown elapses. */
|
|
263
301
|
_checkStateTransition() {
|
|
@@ -319,8 +357,21 @@ function killWin32Tree(pid) {
|
|
|
319
357
|
var ProcessRegistryImpl = class {
|
|
320
358
|
processes = /* @__PURE__ */ new Map();
|
|
321
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 = [];
|
|
322
370
|
constructor(breakerConfig) {
|
|
323
371
|
this.breaker = new CircuitBreaker(breakerConfig);
|
|
372
|
+
this.breaker.onTrip = () => this._armAutoKillReset();
|
|
373
|
+
this.breaker.onReset = () => this._cancelAutoKillReset();
|
|
374
|
+
this.breaker.setEnabled(false);
|
|
324
375
|
}
|
|
325
376
|
register(info) {
|
|
326
377
|
this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
|
|
@@ -396,6 +447,90 @@ var ProcessRegistryImpl = class {
|
|
|
396
447
|
forceBreakerReset() {
|
|
397
448
|
this.breaker.forceReset();
|
|
398
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
|
+
}
|
|
399
534
|
/** Kill a single process by PID.
|
|
400
535
|
*
|
|
401
536
|
* On POSIX: sends SIGTERM to the *process group* (-pid) so that
|
|
@@ -534,8 +669,9 @@ async function* spawnStream(opts) {
|
|
|
534
669
|
let pending2 = "";
|
|
535
670
|
let error;
|
|
536
671
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
537
|
-
const
|
|
538
|
-
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;
|
|
539
675
|
const child = spawn(cmd, opts.args, {
|
|
540
676
|
cwd: opts.cwd,
|
|
541
677
|
env: buildChildEnv(),
|
|
@@ -699,22 +835,29 @@ async function detectPackageManager(cwd) {
|
|
|
699
835
|
function resolvePath(input, ctx) {
|
|
700
836
|
return path3.isAbsolute(input) ? path3.normalize(input) : path3.resolve(ctx.workingDir ?? ctx.cwd, input);
|
|
701
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
|
+
}
|
|
702
847
|
function ensureInsideRoot(absPath, ctx) {
|
|
703
|
-
if (ctx.allowOutsideProjectRoot) return path3.resolve(absPath);
|
|
704
|
-
const root = path3.resolve(ctx.projectRoot);
|
|
705
848
|
const target = path3.resolve(absPath);
|
|
706
|
-
|
|
707
|
-
if (
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
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)}"`);
|
|
711
852
|
}
|
|
712
853
|
function safeResolve(input, ctx) {
|
|
713
854
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
714
855
|
}
|
|
715
856
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
716
857
|
if (ctx.allowOutsideProjectRoot) return;
|
|
717
|
-
const
|
|
858
|
+
const realRoots = await Promise.all(
|
|
859
|
+
allowedRoots(ctx).map((r) => fs14.realpath(r).catch(() => path3.resolve(r)))
|
|
860
|
+
);
|
|
718
861
|
let probe = absPath;
|
|
719
862
|
for (; ; ) {
|
|
720
863
|
let real;
|
|
@@ -729,13 +872,10 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
729
872
|
}
|
|
730
873
|
throw err;
|
|
731
874
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
return;
|
|
875
|
+
if (isInsideAny(real, realRoots)) return;
|
|
876
|
+
throw new Error(
|
|
877
|
+
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
878
|
+
);
|
|
739
879
|
}
|
|
740
880
|
}
|
|
741
881
|
async function safeResolveReal(input, ctx) {
|
|
@@ -837,6 +977,7 @@ var auditTool = {
|
|
|
837
977
|
permission: "confirm",
|
|
838
978
|
mutating: false,
|
|
839
979
|
capabilities: ["shell.restricted"],
|
|
980
|
+
icon: "package",
|
|
840
981
|
timeoutMs: 6e4,
|
|
841
982
|
inputSchema: {
|
|
842
983
|
type: "object",
|
|
@@ -941,6 +1082,7 @@ var bashTool = {
|
|
|
941
1082
|
permission: "confirm",
|
|
942
1083
|
mutating: true,
|
|
943
1084
|
riskTier: "destructive",
|
|
1085
|
+
icon: "terminal",
|
|
944
1086
|
// Trust rules match on the literal `command` string. Without subjectKey
|
|
945
1087
|
// the policy heuristic would have done the same here, but declaring it
|
|
946
1088
|
// explicitly removes the implicit cross-tool aliasing.
|
|
@@ -1281,6 +1423,7 @@ var batchToolUseTool = {
|
|
|
1281
1423
|
mutating: true,
|
|
1282
1424
|
timeoutMs: 12e4,
|
|
1283
1425
|
capabilities: ["tool.mutate.any"],
|
|
1426
|
+
icon: "meta",
|
|
1284
1427
|
inputSchema: {
|
|
1285
1428
|
type: "object",
|
|
1286
1429
|
properties: {
|
|
@@ -2056,39 +2199,57 @@ var IndexStore = class {
|
|
|
2056
2199
|
}
|
|
2057
2200
|
});
|
|
2058
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
|
+
}
|
|
2059
2225
|
/**
|
|
2060
2226
|
* Delete all refs whose source symbols are in a given file.
|
|
2061
2227
|
* Used when re-indexing a file to clear stale refs.
|
|
2062
2228
|
*/
|
|
2063
2229
|
deleteRefsForFile(file) {
|
|
2064
2230
|
this.runWithRetry(() => {
|
|
2065
|
-
|
|
2066
|
-
"SELECT id FROM symbols WHERE file = ?"
|
|
2067
|
-
).
|
|
2068
|
-
if (!ids.length) return;
|
|
2069
|
-
const placeholders = ids.map(() => "?").join(",");
|
|
2070
|
-
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);
|
|
2071
2234
|
});
|
|
2072
2235
|
}
|
|
2073
2236
|
/**
|
|
2074
2237
|
* Resolve `to_name` → `to_id` for all refs that have a name but no id.
|
|
2075
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.
|
|
2076
2243
|
*/
|
|
2077
2244
|
resolveRefs() {
|
|
2078
2245
|
return this.runWithRetry(() => {
|
|
2079
|
-
const
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
if (first) {
|
|
2087
|
-
this.db.prepare("UPDATE refs SET to_id = ? WHERE id = ?").run(first.id, row.id);
|
|
2088
|
-
resolved++;
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
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;
|
|
2092
2253
|
});
|
|
2093
2254
|
}
|
|
2094
2255
|
/**
|
|
@@ -3563,14 +3724,27 @@ async function runIndexerWithStore(store, opts) {
|
|
|
3563
3724
|
symbolsIndexed += count;
|
|
3564
3725
|
langStats[lang] = (langStats[lang] ?? 0) + count;
|
|
3565
3726
|
if (parsed.refs && parsed.refs.length > 0) {
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
if (
|
|
3570
|
-
|
|
3571
|
-
|
|
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
|
+
}
|
|
3572
3743
|
}
|
|
3573
3744
|
}
|
|
3745
|
+
if (batch.length > 0) {
|
|
3746
|
+
store.insertRefsBatch(batch);
|
|
3747
|
+
}
|
|
3574
3748
|
}
|
|
3575
3749
|
store.upsertFile({
|
|
3576
3750
|
file,
|
|
@@ -3903,6 +4077,7 @@ async function codebaseIndexStats(args, opts = {}) {
|
|
|
3903
4077
|
var codebaseIndexTool = {
|
|
3904
4078
|
name: "codebase-index",
|
|
3905
4079
|
category: "Project",
|
|
4080
|
+
icon: "index",
|
|
3906
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.",
|
|
3907
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.",
|
|
3908
4083
|
permission: "confirm",
|
|
@@ -3959,6 +4134,7 @@ var codebaseIndexTool = {
|
|
|
3959
4134
|
var codebaseSearchTool = {
|
|
3960
4135
|
name: "codebase-search",
|
|
3961
4136
|
category: "Project",
|
|
4137
|
+
icon: "index",
|
|
3962
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.",
|
|
3963
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.",
|
|
3964
4140
|
permission: "auto",
|
|
@@ -4047,6 +4223,7 @@ var codebaseSearchTool = {
|
|
|
4047
4223
|
var codebaseStatsTool = {
|
|
4048
4224
|
name: "codebase-stats",
|
|
4049
4225
|
category: "Project",
|
|
4226
|
+
icon: "index",
|
|
4050
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.",
|
|
4051
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.",
|
|
4052
4229
|
permission: "auto",
|
|
@@ -4107,6 +4284,7 @@ var diffTool = {
|
|
|
4107
4284
|
permission: "auto",
|
|
4108
4285
|
mutating: false,
|
|
4109
4286
|
capabilities: ["fs.read"],
|
|
4287
|
+
icon: "diff",
|
|
4110
4288
|
timeoutMs: 1e4,
|
|
4111
4289
|
inputSchema: {
|
|
4112
4290
|
type: "object",
|
|
@@ -4253,6 +4431,7 @@ var documentTool = {
|
|
|
4253
4431
|
mutating: false,
|
|
4254
4432
|
timeoutMs: 3e4,
|
|
4255
4433
|
capabilities: ["fs.read"],
|
|
4434
|
+
icon: "document",
|
|
4256
4435
|
inputSchema: {
|
|
4257
4436
|
type: "object",
|
|
4258
4437
|
properties: {
|
|
@@ -4397,6 +4576,7 @@ var editTool = {
|
|
|
4397
4576
|
permission: "confirm",
|
|
4398
4577
|
mutating: true,
|
|
4399
4578
|
capabilities: ["fs.write"],
|
|
4579
|
+
icon: "edit",
|
|
4400
4580
|
timeoutMs: 5e3,
|
|
4401
4581
|
inputSchema: {
|
|
4402
4582
|
type: "object",
|
|
@@ -4619,6 +4799,7 @@ var execTool = {
|
|
|
4619
4799
|
riskTier: "standard",
|
|
4620
4800
|
timeoutMs: DEFAULT_TIMEOUT_MS2,
|
|
4621
4801
|
capabilities: ["shell.restricted"],
|
|
4802
|
+
icon: "terminal",
|
|
4622
4803
|
inputSchema: {
|
|
4623
4804
|
type: "object",
|
|
4624
4805
|
properties: {
|
|
@@ -4718,7 +4899,8 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId) {
|
|
|
4718
4899
|
const spool = createOutputSpool({ tool: `exec-${cmd}`, thresholdBytes: MAX_OUTPUT2 });
|
|
4719
4900
|
const resolved = resolveWin32Command(cmd);
|
|
4720
4901
|
const needsShell = isWin2 && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
4721
|
-
const
|
|
4902
|
+
const spawnCmd = needsShell ? cmd : resolved;
|
|
4903
|
+
const child = spawn(spawnCmd, args, {
|
|
4722
4904
|
cwd,
|
|
4723
4905
|
env: buildChildEnv(sessionId),
|
|
4724
4906
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4905,6 +5087,7 @@ var fetchTool = {
|
|
|
4905
5087
|
permission: "confirm",
|
|
4906
5088
|
mutating: false,
|
|
4907
5089
|
capabilities: ["net.outbound"],
|
|
5090
|
+
icon: "web",
|
|
4908
5091
|
// Trust rules for fetch match on the literal URL — declare it explicitly
|
|
4909
5092
|
// so a user can trust `https://api.example.com/*` without accidentally
|
|
4910
5093
|
// matching that pattern on any other tool that happens to have a `url`
|
|
@@ -5054,6 +5237,7 @@ var formatTool = {
|
|
|
5054
5237
|
permission: "confirm",
|
|
5055
5238
|
mutating: true,
|
|
5056
5239
|
capabilities: ["fs.write", "shell.exec"],
|
|
5240
|
+
icon: "code",
|
|
5057
5241
|
timeoutMs: 6e4,
|
|
5058
5242
|
inputSchema: {
|
|
5059
5243
|
type: "object",
|
|
@@ -5154,6 +5338,7 @@ var gitTool = {
|
|
|
5154
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.",
|
|
5155
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.",
|
|
5156
5340
|
permission: "confirm",
|
|
5341
|
+
icon: "git",
|
|
5157
5342
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
5158
5343
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
5159
5344
|
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
@@ -5413,6 +5598,7 @@ var globTool = {
|
|
|
5413
5598
|
permission: "auto",
|
|
5414
5599
|
mutating: false,
|
|
5415
5600
|
capabilities: ["fs.read"],
|
|
5601
|
+
icon: "folder",
|
|
5416
5602
|
maxOutputBytes: 65536,
|
|
5417
5603
|
timeoutMs: 5e3,
|
|
5418
5604
|
inputSchema: {
|
|
@@ -5545,6 +5731,7 @@ var grepTool = {
|
|
|
5545
5731
|
permission: "auto",
|
|
5546
5732
|
mutating: false,
|
|
5547
5733
|
capabilities: ["fs.read"],
|
|
5734
|
+
icon: "search",
|
|
5548
5735
|
maxOutputBytes: 131072,
|
|
5549
5736
|
timeoutMs: 1e4,
|
|
5550
5737
|
inputSchema: {
|
|
@@ -5831,6 +6018,7 @@ var installTool = {
|
|
|
5831
6018
|
permission: "confirm",
|
|
5832
6019
|
mutating: true,
|
|
5833
6020
|
riskTier: "standard",
|
|
6021
|
+
icon: "package",
|
|
5834
6022
|
timeoutMs: 12e4,
|
|
5835
6023
|
capabilities: ["package.install", "shell.restricted"],
|
|
5836
6024
|
inputSchema: {
|
|
@@ -5969,6 +6157,7 @@ var jsonTool = {
|
|
|
5969
6157
|
mutating: false,
|
|
5970
6158
|
timeoutMs: 5e3,
|
|
5971
6159
|
capabilities: ["fs.read"],
|
|
6160
|
+
icon: "json",
|
|
5972
6161
|
inputSchema: {
|
|
5973
6162
|
type: "object",
|
|
5974
6163
|
properties: {
|
|
@@ -6094,6 +6283,7 @@ var lintTool = {
|
|
|
6094
6283
|
mutating: false,
|
|
6095
6284
|
timeoutMs: 6e4,
|
|
6096
6285
|
capabilities: ["shell.restricted"],
|
|
6286
|
+
icon: "code",
|
|
6097
6287
|
inputSchema: {
|
|
6098
6288
|
type: "object",
|
|
6099
6289
|
properties: {
|
|
@@ -6187,6 +6377,7 @@ var logsTool = {
|
|
|
6187
6377
|
mutating: false,
|
|
6188
6378
|
timeoutMs: 3e4,
|
|
6189
6379
|
capabilities: ["shell.restricted"],
|
|
6380
|
+
icon: "logs",
|
|
6190
6381
|
inputSchema: {
|
|
6191
6382
|
type: "object",
|
|
6192
6383
|
properties: {
|
|
@@ -6388,6 +6579,7 @@ var outdatedTool = {
|
|
|
6388
6579
|
description: "Check for outdated dependencies in the project. Reports current, wanted (semver range), and latest versions available.",
|
|
6389
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.",
|
|
6390
6581
|
permission: "confirm",
|
|
6582
|
+
icon: "package",
|
|
6391
6583
|
// Network side-effecting (registry HTTP). Pairs with `mutating: true`
|
|
6392
6584
|
// so the H7 invariant test (`no auto-permission tool declares
|
|
6393
6585
|
// mutating: true`) passes — a tool claiming `'auto'` must be purely
|
|
@@ -6439,7 +6631,8 @@ function runOutdated(manager, args, cwd, signal) {
|
|
|
6439
6631
|
const MAX = 1e5;
|
|
6440
6632
|
const resolved = resolveWin32Command(manager);
|
|
6441
6633
|
const needsShell = process.platform === "win32" && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
6442
|
-
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 } : {} });
|
|
6443
6636
|
child.stdout?.on("data", (c) => {
|
|
6444
6637
|
if (stdout.length < MAX) stdout += c.toString();
|
|
6445
6638
|
});
|
|
@@ -6503,6 +6696,7 @@ var patchTool = {
|
|
|
6503
6696
|
permission: "confirm",
|
|
6504
6697
|
mutating: true,
|
|
6505
6698
|
capabilities: ["fs.write"],
|
|
6699
|
+
icon: "edit",
|
|
6506
6700
|
timeoutMs: 3e4,
|
|
6507
6701
|
inputSchema: {
|
|
6508
6702
|
type: "object",
|
|
@@ -6610,11 +6804,12 @@ function extractPatchedFiles(output) {
|
|
|
6610
6804
|
var planTool = {
|
|
6611
6805
|
name: "plan",
|
|
6612
6806
|
category: "Session",
|
|
6613
|
-
description:
|
|
6614
|
-
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.',
|
|
6615
6809
|
permission: "confirm",
|
|
6616
6810
|
mutating: true,
|
|
6617
6811
|
capabilities: ["fs.write"],
|
|
6812
|
+
icon: "plan",
|
|
6618
6813
|
timeoutMs: 2e3,
|
|
6619
6814
|
inputSchema: {
|
|
6620
6815
|
type: "object",
|
|
@@ -6654,12 +6849,26 @@ var planTool = {
|
|
|
6654
6849
|
template: {
|
|
6655
6850
|
type: "string",
|
|
6656
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).'
|
|
6657
6857
|
}
|
|
6658
6858
|
},
|
|
6659
6859
|
required: ["action"]
|
|
6660
6860
|
},
|
|
6661
6861
|
async execute(input, ctx) {
|
|
6662
|
-
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
|
+
}
|
|
6663
6872
|
if (typeof planPath !== "string" || !planPath) {
|
|
6664
6873
|
return {
|
|
6665
6874
|
ok: false,
|
|
@@ -6673,148 +6882,169 @@ var planTool = {
|
|
|
6673
6882
|
let early = null;
|
|
6674
6883
|
const taskifyMeta = { title: "", details: "" };
|
|
6675
6884
|
let didTaskify = false;
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
case "done": {
|
|
6691
|
-
if (!input.target) {
|
|
6692
|
-
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6693
|
-
return p;
|
|
6694
|
-
}
|
|
6695
|
-
const next = setPlanItemStatus(
|
|
6696
|
-
p,
|
|
6697
|
-
input.target,
|
|
6698
|
-
input.action === "start" ? "in_progress" : "done"
|
|
6699
|
-
);
|
|
6700
|
-
if (next === p) {
|
|
6701
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6702
|
-
return p;
|
|
6703
|
-
}
|
|
6704
|
-
return next;
|
|
6705
|
-
}
|
|
6706
|
-
case "remove": {
|
|
6707
|
-
if (!input.target) {
|
|
6708
|
-
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
6709
|
-
return p;
|
|
6710
|
-
}
|
|
6711
|
-
const next = removePlanItem(p, input.target);
|
|
6712
|
-
if (next === p) {
|
|
6713
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6714
|
-
return p;
|
|
6715
|
-
}
|
|
6716
|
-
return next;
|
|
6717
|
-
}
|
|
6718
|
-
case "promote": {
|
|
6719
|
-
if (!input.target) {
|
|
6720
|
-
early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
|
|
6721
|
-
return p;
|
|
6722
|
-
}
|
|
6723
|
-
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
6724
|
-
if (!derived) {
|
|
6725
|
-
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
6726
|
-
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;
|
|
6727
6899
|
}
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
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;
|
|
6742
6916
|
}
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
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;
|
|
6747
6928
|
}
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
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;
|
|
6751
6947
|
}
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
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;
|
|
6765
6969
|
}
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
if (itemIdx === -1) {
|
|
6773
|
-
const lower = input.target.toLowerCase();
|
|
6774
|
-
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;
|
|
6775
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;
|
|
6776
6997
|
}
|
|
6777
|
-
|
|
6778
|
-
early = mkResult(p, false, `
|
|
6998
|
+
default:
|
|
6999
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
6779
7000
|
return p;
|
|
6780
|
-
}
|
|
6781
|
-
const item = p.items[itemIdx];
|
|
6782
|
-
taskifyMeta.title = item.title;
|
|
6783
|
-
taskifyMeta.details = item.details ?? "";
|
|
6784
|
-
didTaskify = true;
|
|
6785
|
-
break;
|
|
6786
7001
|
}
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
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
|
+
}
|
|
6793
7013
|
if (early) return early;
|
|
6794
7014
|
if (didTaskify) {
|
|
6795
|
-
const
|
|
6796
|
-
if (typeof
|
|
7015
|
+
const taskPathRaw = ctx.meta["task.path"];
|
|
7016
|
+
if (typeof taskPathRaw !== "string" || !taskPathRaw) {
|
|
6797
7017
|
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
6798
7018
|
}
|
|
6799
|
-
|
|
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
|
+
}
|
|
6800
7024
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
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.
|
|
6816
7043
|
${formatTaskList(taskFile.tasks)}`
|
|
6817
|
-
|
|
7044
|
+
);
|
|
7045
|
+
} catch (err) {
|
|
7046
|
+
return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
7047
|
+
}
|
|
6818
7048
|
}
|
|
6819
7049
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
6820
7050
|
}
|
|
@@ -6840,6 +7070,7 @@ var readTool = {
|
|
|
6840
7070
|
permission: "auto",
|
|
6841
7071
|
mutating: false,
|
|
6842
7072
|
capabilities: ["fs.read"],
|
|
7073
|
+
icon: "file",
|
|
6843
7074
|
maxOutputBytes: 262144,
|
|
6844
7075
|
timeoutMs: 5e3,
|
|
6845
7076
|
inputSchema: {
|
|
@@ -6912,6 +7143,7 @@ var replaceTool = {
|
|
|
6912
7143
|
permission: "confirm",
|
|
6913
7144
|
mutating: true,
|
|
6914
7145
|
capabilities: ["fs.write"],
|
|
7146
|
+
icon: "edit",
|
|
6915
7147
|
timeoutMs: 3e4,
|
|
6916
7148
|
inputSchema: {
|
|
6917
7149
|
type: "object",
|
|
@@ -7203,6 +7435,7 @@ var scaffoldTool = {
|
|
|
7203
7435
|
permission: "confirm",
|
|
7204
7436
|
mutating: true,
|
|
7205
7437
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
7438
|
+
icon: "scaffold",
|
|
7206
7439
|
timeoutMs: 3e4,
|
|
7207
7440
|
inputSchema: {
|
|
7208
7441
|
type: "object",
|
|
@@ -7298,6 +7531,7 @@ var searchTool = {
|
|
|
7298
7531
|
permission: "confirm",
|
|
7299
7532
|
mutating: false,
|
|
7300
7533
|
capabilities: ["net.outbound"],
|
|
7534
|
+
icon: "search",
|
|
7301
7535
|
timeoutMs: TIMEOUT_MS3,
|
|
7302
7536
|
inputSchema: {
|
|
7303
7537
|
type: "object",
|
|
@@ -7511,6 +7745,7 @@ var setWorkingDirTool = {
|
|
|
7511
7745
|
permission: "confirm",
|
|
7512
7746
|
mutating: true,
|
|
7513
7747
|
capabilities: ["fs.read"],
|
|
7748
|
+
icon: "settings",
|
|
7514
7749
|
timeoutMs: 5e3,
|
|
7515
7750
|
inputSchema: {
|
|
7516
7751
|
type: "object",
|
|
@@ -7571,11 +7806,12 @@ function findTaskIndex(tasks, query2) {
|
|
|
7571
7806
|
var taskTool = {
|
|
7572
7807
|
name: "task",
|
|
7573
7808
|
category: "Session",
|
|
7574
|
-
description:
|
|
7575
|
-
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)',
|
|
7576
7811
|
permission: "confirm",
|
|
7577
7812
|
mutating: true,
|
|
7578
7813
|
capabilities: ["fs.write"],
|
|
7814
|
+
icon: "task",
|
|
7579
7815
|
timeoutMs: 2e3,
|
|
7580
7816
|
inputSchema: {
|
|
7581
7817
|
type: "object",
|
|
@@ -7641,12 +7877,26 @@ var taskTool = {
|
|
|
7641
7877
|
type: "array",
|
|
7642
7878
|
items: { type: "string" },
|
|
7643
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).'
|
|
7644
7885
|
}
|
|
7645
7886
|
},
|
|
7646
7887
|
required: ["action"]
|
|
7647
7888
|
},
|
|
7648
7889
|
async execute(input, ctx) {
|
|
7649
|
-
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
|
+
}
|
|
7650
7900
|
if (typeof taskPath !== "string" || !taskPath) {
|
|
7651
7901
|
return { ok: false, message: "Task storage path not configured.", count: 0, completed: 0, inProgress: 0 };
|
|
7652
7902
|
}
|
|
@@ -7656,23 +7906,66 @@ var taskTool = {
|
|
|
7656
7906
|
const planifyMeta = { title: "", details: "" };
|
|
7657
7907
|
let didPlanify = false;
|
|
7658
7908
|
let todosToReplace = null;
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
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;
|
|
7667
7955
|
}
|
|
7668
|
-
|
|
7669
|
-
|
|
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
|
+
}
|
|
7670
7962
|
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7671
|
-
const
|
|
7963
|
+
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
7964
|
+
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
7672
7965
|
if (missing.length > 0) {
|
|
7673
7966
|
early = {
|
|
7674
7967
|
ok: false,
|
|
7675
|
-
message: `dependsOn validation failed: task
|
|
7968
|
+
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
7676
7969
|
count: 0,
|
|
7677
7970
|
completed: 0,
|
|
7678
7971
|
inProgress: 0
|
|
@@ -7680,165 +7973,170 @@ var taskTool = {
|
|
|
7680
7973
|
return f;
|
|
7681
7974
|
}
|
|
7682
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;
|
|
7683
7993
|
}
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
createdAt: t.createdAt || now,
|
|
7688
|
-
updatedAt: now
|
|
7689
|
-
}));
|
|
7690
|
-
break;
|
|
7691
|
-
}
|
|
7692
|
-
case "add": {
|
|
7693
|
-
const t = input.task;
|
|
7694
|
-
if (!t || !t.title) {
|
|
7695
|
-
early = { ok: false, message: "action=add requires `task` with at least `title`.", count: 0, completed: 0, inProgress: 0 };
|
|
7696
|
-
return f;
|
|
7697
|
-
}
|
|
7698
|
-
if (t.dependsOn && t.dependsOn.length > 0) {
|
|
7699
|
-
const existingIds = new Set(f.tasks.map((e) => e.id));
|
|
7700
|
-
const missing = t.dependsOn.filter((d) => !existingIds.has(d));
|
|
7701
|
-
if (missing.length > 0) {
|
|
7702
|
-
early = {
|
|
7703
|
-
ok: false,
|
|
7704
|
-
message: `dependsOn validation failed: unknown task IDs: ${missing.join(", ")}`,
|
|
7705
|
-
count: 0,
|
|
7706
|
-
completed: 0,
|
|
7707
|
-
inProgress: 0
|
|
7708
|
-
};
|
|
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 };
|
|
7709
7997
|
return f;
|
|
7710
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;
|
|
7711
8007
|
}
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
}
|
|
7735
|
-
const task = f.tasks.find((t) => t.id === input.id);
|
|
7736
|
-
if (!task) {
|
|
7737
|
-
early = { ok: false, message: `Task "${input.id}" not found.`, count: 0, completed: 0, inProgress: 0 };
|
|
7738
|
-
return f;
|
|
7739
|
-
}
|
|
7740
|
-
task.status = input.status;
|
|
7741
|
-
task.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7742
|
-
break;
|
|
7743
|
-
}
|
|
7744
|
-
case "promote": {
|
|
7745
|
-
const target = input.target?.trim();
|
|
7746
|
-
if (!target) {
|
|
7747
|
-
early = { ok: false, message: "action=promote requires `target` (task id, index, or title substring).", count: 0, completed: 0, inProgress: 0 };
|
|
7748
|
-
return f;
|
|
7749
|
-
}
|
|
7750
|
-
const idx = findTaskIndex(f.tasks, target);
|
|
7751
|
-
if (idx === -1) {
|
|
7752
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7753
|
-
return f;
|
|
7754
|
-
}
|
|
7755
|
-
const match = f.tasks[idx];
|
|
7756
|
-
if (!match) {
|
|
7757
|
-
early = { ok: false, message: `No task matched "${target}".`, count: 0, completed: 0, inProgress: 0 };
|
|
7758
|
-
return f;
|
|
7759
|
-
}
|
|
7760
|
-
if (match.status !== "completed" && match.status !== "failed") {
|
|
7761
|
-
match.status = "in_progress";
|
|
7762
|
-
match.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7763
|
-
}
|
|
7764
|
-
const todos = [];
|
|
7765
|
-
const ts2 = Date.now();
|
|
7766
|
-
todos.push({
|
|
7767
|
-
id: `todo_${ts2}_task`,
|
|
7768
|
-
content: match.title,
|
|
7769
|
-
status: "in_progress",
|
|
7770
|
-
activeForm: match.title,
|
|
7771
|
-
promotedFromTask: match.id
|
|
7772
|
-
});
|
|
7773
|
-
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();
|
|
7774
8030
|
todos.push({
|
|
7775
|
-
id: `todo_${ts2}
|
|
7776
|
-
content: match.
|
|
7777
|
-
status: "
|
|
8031
|
+
id: `todo_${ts2}_task`,
|
|
8032
|
+
content: match.title,
|
|
8033
|
+
status: "in_progress",
|
|
8034
|
+
activeForm: match.title,
|
|
7778
8035
|
promotedFromTask: match.id
|
|
7779
8036
|
});
|
|
7780
|
-
|
|
7781
|
-
if (input.subtasks && input.subtasks.length > 0) {
|
|
7782
|
-
for (const st of input.subtasks) {
|
|
8037
|
+
if (match.description) {
|
|
7783
8038
|
todos.push({
|
|
7784
8039
|
id: `todo_${ts2}_${randomUUID().slice(0, 6)}`,
|
|
7785
|
-
content:
|
|
8040
|
+
content: match.description.slice(0, 200),
|
|
7786
8041
|
status: "pending",
|
|
7787
8042
|
promotedFromTask: match.id
|
|
7788
8043
|
});
|
|
7789
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;
|
|
7790
8059
|
}
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
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;
|
|
7806
8080
|
}
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
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 };
|
|
7810
8083
|
return f;
|
|
7811
|
-
}
|
|
7812
|
-
planifyMeta.title = match.title;
|
|
7813
|
-
planifyMeta.details = match.description ?? "";
|
|
7814
|
-
didPlanify = true;
|
|
7815
|
-
break;
|
|
7816
8084
|
}
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
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
|
+
}
|
|
7823
8096
|
if (todosToReplace) ctx.state.replaceTodos(todosToReplace);
|
|
7824
8097
|
if (early) return early;
|
|
7825
8098
|
if (didPlanify) {
|
|
7826
8099
|
const { title, details } = planifyMeta;
|
|
7827
|
-
const
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
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
|
+
}
|
|
7832
8124
|
return {
|
|
7833
8125
|
ok: true,
|
|
7834
8126
|
message: `planify ok \u2014 added "${title}" to plan.
|
|
7835
|
-
${
|
|
8127
|
+
${formatted}`,
|
|
7836
8128
|
count: file.tasks.length,
|
|
7837
|
-
completed:
|
|
7838
|
-
inProgress:
|
|
8129
|
+
completed: prog.completed,
|
|
8130
|
+
inProgress: prog.inProgress
|
|
7839
8131
|
};
|
|
7840
8132
|
}
|
|
7841
|
-
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
|
+
};
|
|
7842
8140
|
}
|
|
7843
8141
|
const p = computeTaskItemProgress(file.tasks);
|
|
7844
8142
|
const summary = promoteMeta.count > 0 ? `promote ok \u2014 ${promoteMeta.count} todo(s) created from "${promoteMeta.title}".
|
|
@@ -7859,6 +8157,7 @@ var testTool = {
|
|
|
7859
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.",
|
|
7860
8158
|
permission: "confirm",
|
|
7861
8159
|
mutating: false,
|
|
8160
|
+
icon: "test",
|
|
7862
8161
|
timeoutMs: 12e4,
|
|
7863
8162
|
capabilities: ["shell.restricted"],
|
|
7864
8163
|
inputSchema: {
|
|
@@ -8018,6 +8317,7 @@ var todoTool = {
|
|
|
8018
8317
|
// mutates only conversation state (ctx.todos), not external state — no confirmation needed
|
|
8019
8318
|
timeoutMs: 1e3,
|
|
8020
8319
|
capabilities: ["session.todo"],
|
|
8320
|
+
icon: "todo",
|
|
8021
8321
|
inputSchema: {
|
|
8022
8322
|
type: "object",
|
|
8023
8323
|
properties: {
|
|
@@ -8126,6 +8426,7 @@ var toolHelpTool = {
|
|
|
8126
8426
|
mutating: false,
|
|
8127
8427
|
timeoutMs: 5e3,
|
|
8128
8428
|
capabilities: ["tool.meta"],
|
|
8429
|
+
icon: "meta",
|
|
8129
8430
|
inputSchema: {
|
|
8130
8431
|
type: "object",
|
|
8131
8432
|
properties: {
|
|
@@ -8249,6 +8550,7 @@ var toolSearchTool = {
|
|
|
8249
8550
|
mutating: false,
|
|
8250
8551
|
timeoutMs: 1e3,
|
|
8251
8552
|
capabilities: ["tool.meta"],
|
|
8553
|
+
icon: "meta",
|
|
8252
8554
|
inputSchema: {
|
|
8253
8555
|
type: "object",
|
|
8254
8556
|
properties: {
|
|
@@ -8328,6 +8630,7 @@ var toolUseTool = {
|
|
|
8328
8630
|
mutating: true,
|
|
8329
8631
|
timeoutMs: 6e4,
|
|
8330
8632
|
capabilities: ["tool.mutate.any"],
|
|
8633
|
+
icon: "meta",
|
|
8331
8634
|
inputSchema: {
|
|
8332
8635
|
type: "object",
|
|
8333
8636
|
properties: {
|
|
@@ -8408,6 +8711,7 @@ var treeTool = {
|
|
|
8408
8711
|
permission: "auto",
|
|
8409
8712
|
mutating: false,
|
|
8410
8713
|
capabilities: ["fs.read"],
|
|
8714
|
+
icon: "tree",
|
|
8411
8715
|
timeoutMs: 15e3,
|
|
8412
8716
|
inputSchema: {
|
|
8413
8717
|
type: "object",
|
|
@@ -8573,6 +8877,7 @@ var typecheckTool = {
|
|
|
8573
8877
|
mutating: false,
|
|
8574
8878
|
timeoutMs: 12e4,
|
|
8575
8879
|
capabilities: ["shell.restricted"],
|
|
8880
|
+
icon: "code",
|
|
8576
8881
|
inputSchema: {
|
|
8577
8882
|
type: "object",
|
|
8578
8883
|
properties: {
|
|
@@ -8661,6 +8966,7 @@ var writeTool = {
|
|
|
8661
8966
|
mutating: true,
|
|
8662
8967
|
timeoutMs: 5e3,
|
|
8663
8968
|
capabilities: ["fs.write"],
|
|
8969
|
+
icon: "file",
|
|
8664
8970
|
inputSchema: {
|
|
8665
8971
|
type: "object",
|
|
8666
8972
|
properties: {
|