@wrongstack/tools 0.313.0 → 0.313.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 +10 -4055
- package/dist/bash.js +9 -2308
- package/dist/batch-tool-use.js +2 -187
- package/dist/browser/index.js +25 -1339
- package/dist/builtin.js +69 -33949
- package/dist/chunk-2CTB3UK5.js +197 -0
- package/dist/chunk-2ME7ZWDD.js +1272 -0
- package/dist/chunk-2TFQDEPU.js +113 -0
- package/dist/chunk-2ZCJNKFE.js +315 -0
- package/dist/chunk-3PVYQFRQ.js +472 -0
- package/dist/chunk-3QMQBO5Z.js +89 -0
- package/dist/chunk-42AKKX7O.js +385 -0
- package/dist/chunk-42SKEETB.js +113 -0
- package/dist/chunk-4BHUSMNL.js +104 -0
- package/dist/chunk-4I4LWFOO.js +431 -0
- package/dist/chunk-553IQUMO.js +478 -0
- package/dist/chunk-55KF77OK.js +1944 -0
- package/dist/chunk-6HLZK37X.js +359 -0
- package/dist/chunk-6RNIFLG4.js +305 -0
- package/dist/chunk-A6BRWIXN.js +317 -0
- package/dist/chunk-AHHDWNXC.js +200 -0
- package/dist/chunk-B2CIOKWP.js +899 -0
- package/dist/chunk-BTSFTGIL.js +7 -0
- package/dist/chunk-D33S4RQG.js +222 -0
- package/dist/chunk-DIJKHSUO.js +134 -0
- package/dist/chunk-DPL47QGS.js +3515 -0
- package/dist/chunk-EMRGN4KM.js +170 -0
- package/dist/chunk-FGOABE4Z.js +955 -0
- package/dist/chunk-FW65TP4G.js +520 -0
- package/dist/chunk-GOIZNNOA.js +221 -0
- package/dist/chunk-H5MJUAA6.js +295 -0
- package/dist/chunk-H5OHH6F4.js +223 -0
- package/dist/chunk-HC5RM4WY.js +199 -0
- package/dist/chunk-HEQFMLZ7.js +190 -0
- package/dist/chunk-HFXSNKCV.js +196 -0
- package/dist/chunk-HXSBQ2SE.js +1723 -0
- package/dist/chunk-ILESNGNM.js +163 -0
- package/dist/chunk-INF5CGAO.js +137 -0
- package/dist/chunk-IWDFZ6XR.js +337 -0
- package/dist/chunk-IXMNTNW5.js +238 -0
- package/dist/chunk-J4SRJKBW.js +428 -0
- package/dist/chunk-J5SKD6LN.js +235 -0
- package/dist/chunk-JRJJX3L5.js +424 -0
- package/dist/chunk-KXNDHMUB.js +366 -0
- package/dist/chunk-LIJQMYYX.js +59 -0
- package/dist/chunk-MAN26PZ5.js +260 -0
- package/dist/chunk-MKRQMLZA.js +474 -0
- package/dist/chunk-NBDZMMG2.js +169 -0
- package/dist/chunk-NHA6BEIH.js +101 -0
- package/dist/chunk-OA6YJIWL.js +4412 -0
- package/dist/chunk-OZC255IC.js +2830 -0
- package/dist/chunk-PUR6WU2B.js +154 -0
- package/dist/chunk-Q5JPPCZC.js +12 -0
- package/dist/chunk-QBV725YG.js +128 -0
- package/dist/chunk-QHVW4AAM.js +892 -0
- package/dist/chunk-QNJ7AQ4B.js +1000 -0
- package/dist/chunk-RGISBZBD.js +231 -0
- package/dist/chunk-RJZQWEMS.js +84 -0
- package/dist/chunk-RP2ZA47V.js +15 -0
- package/dist/chunk-RXISBFR5.js +315 -0
- package/dist/chunk-S5CCLZL3.js +99 -0
- package/dist/chunk-TVTCSBJF.js +122 -0
- package/dist/chunk-U4D5S3VQ.js +21 -0
- package/dist/chunk-U4VAATKP.js +193 -0
- package/dist/chunk-UKU2ZX4C.js +203 -0
- package/dist/chunk-UNSKG6DC.js +159 -0
- package/dist/chunk-URFKEIHM.js +1091 -0
- package/dist/chunk-UTV7YAW7.js +178 -0
- package/dist/chunk-V43EQ6VR.js +68 -0
- package/dist/chunk-VB3MJGCV.js +610 -0
- package/dist/chunk-WHIIIREL.js +223 -0
- package/dist/chunk-XMIFYKUT.js +1371 -0
- package/dist/chunk-YF2RSV34.js +54 -0
- package/dist/chunk-Z6DAKQ3U.js +1 -0
- package/dist/chunk-ZIGAZFWE.js +522 -0
- package/dist/circuit-breaker.js +3 -191
- package/dist/codebase-index/index.js +84 -13688
- package/dist/codebase-index/parser-worker-script.js +8 -3995
- package/dist/codebase-index/project-server.js +60 -8548
- package/dist/codebase-index/worker.js +20 -8416
- package/dist/design.js +2 -299
- package/dist/diff.js +4 -256
- package/dist/document.js +4 -222
- package/dist/e2e.js +5 -476
- package/dist/edit.js +14 -10200
- package/dist/exec.js +16 -3131
- package/dist/fetch.js +8 -398
- package/dist/format.js +10 -3750
- package/dist/generic-parser-VUXDIAOZ.js +13 -0
- package/dist/git.js +4 -380
- package/dist/glob.js +6 -280
- package/dist/go-parser-6CA77WZ4.js +469 -0
- package/dist/grep.js +7 -604
- package/dist/index.js +318 -35387
- package/dist/install.js +10 -4091
- package/dist/json-parser-7YMDJSBJ.js +213 -0
- package/dist/json.js +4 -665
- package/dist/kanban.js +3 -2096
- package/dist/languages/index.js +23 -4664
- package/dist/legacy-bridge-7UKZT46G.js +18 -0
- package/dist/lint.js +10 -3706
- package/dist/logs.js +4 -287
- package/dist/memory.js +6 -215
- package/dist/mode.js +3 -99
- package/dist/outdated.js +4 -5170
- package/dist/pack.js +65 -33897
- package/dist/parser-dispatch-VPOFDO2U.js +14 -0
- package/dist/patch.js +13 -9935
- package/dist/plan.js +5 -3286
- package/dist/process-registry.js +8 -651
- package/dist/ps-slash.js +12 -1439
- package/dist/py-parser-GPM5VYRG.js +16 -0
- package/dist/read.js +13 -9816
- package/dist/replace.js +15 -10014
- package/dist/rs-parser-WVQ4KYCX.js +81 -0
- package/dist/scaffold.js +4 -253
- package/dist/search.js +5 -585
- package/dist/session-kanban.js +30 -972
- package/dist/skill.js +2 -215
- package/dist/task.js +5 -3410
- package/dist/test.js +10 -3773
- package/dist/todo.js +4 -2891
- package/dist/tool-help.js +3 -123
- package/dist/tool-search.js +3 -79
- package/dist/tool-tier.js +68 -33984
- package/dist/tool-use.js +2 -93
- package/dist/tree-sitter-parser-YCILHGZD.js +17 -0
- package/dist/tree.js +4 -288
- package/dist/ts-parser-YH4ZMWMH.js +232 -0
- package/dist/typecheck.js +10 -3753
- package/dist/win32.js +6 -60
- package/dist/write.js +14 -9882
- package/dist/yaml-parser-GFAOKV53.js +157 -0
- package/package.json +5 -5
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import {
|
|
2
|
+
todoTool
|
|
3
|
+
} from "./chunk-J4SRJKBW.js";
|
|
4
|
+
import {
|
|
5
|
+
projectSessionPlanToKanban
|
|
6
|
+
} from "./chunk-QNJ7AQ4B.js";
|
|
7
|
+
|
|
8
|
+
// src/plan.ts
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
addPlanItem,
|
|
12
|
+
clearPlan,
|
|
13
|
+
deriveTodosFromPlanItem,
|
|
14
|
+
formatPlan,
|
|
15
|
+
getPlanTemplate,
|
|
16
|
+
mutatePlan,
|
|
17
|
+
mutateTasks,
|
|
18
|
+
removePlanItem,
|
|
19
|
+
setPlanItemStatus
|
|
20
|
+
} from "@wrongstack/core/storage";
|
|
21
|
+
import { formatTaskList } from "@wrongstack/core/utils";
|
|
22
|
+
var planTool = {
|
|
23
|
+
name: "plan",
|
|
24
|
+
category: "Session",
|
|
25
|
+
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.',
|
|
26
|
+
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 `action: "status"` with `open`, `in_progress`, or `done` for exact status changes.\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- Unfinished plan items cannot be removed or cleared; complete them first.\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.',
|
|
27
|
+
permission: "confirm",
|
|
28
|
+
// WS-046: gives permission decisions something to key on.
|
|
29
|
+
// The action performed; plan has no single file or path subject.
|
|
30
|
+
subjectKey: "action",
|
|
31
|
+
mutating: true,
|
|
32
|
+
capabilities: ["fs.write"],
|
|
33
|
+
icon: "plan",
|
|
34
|
+
timeoutMs: 3e4,
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
action: {
|
|
39
|
+
type: "string",
|
|
40
|
+
enum: [
|
|
41
|
+
"show",
|
|
42
|
+
"add",
|
|
43
|
+
"status",
|
|
44
|
+
"start",
|
|
45
|
+
"done",
|
|
46
|
+
"remove",
|
|
47
|
+
"promote",
|
|
48
|
+
"template_use",
|
|
49
|
+
"clear",
|
|
50
|
+
"taskify"
|
|
51
|
+
],
|
|
52
|
+
description: "The operation to perform on the plan board."
|
|
53
|
+
},
|
|
54
|
+
title: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Title of the plan item. Required for action=add."
|
|
57
|
+
},
|
|
58
|
+
details: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Additional details or description for a new plan item (action=add)."
|
|
61
|
+
},
|
|
62
|
+
target: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Identifier for the target plan item (id, 1-based index, or partial title). Required for most actions except add/show/clear."
|
|
65
|
+
},
|
|
66
|
+
status: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["open", "in_progress", "done"],
|
|
69
|
+
description: "Exact plan item status for action=status."
|
|
70
|
+
},
|
|
71
|
+
subtasks: {
|
|
72
|
+
type: "array",
|
|
73
|
+
items: { type: "string" },
|
|
74
|
+
description: "List of subtask titles. Used with promote to break a plan item into multiple todos."
|
|
75
|
+
},
|
|
76
|
+
template: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Template identifier when using action=template_use. Common values: new-feature, bug-fix, refactor, release, security-audit."
|
|
79
|
+
},
|
|
80
|
+
scope: {
|
|
81
|
+
type: "string",
|
|
82
|
+
enum: ["session", "project"],
|
|
83
|
+
description: 'Storage scope: "session" (default, isolated to this session) or "project" (shared across all sessions for this project).'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
required: ["action"]
|
|
87
|
+
},
|
|
88
|
+
async execute(input, ctx) {
|
|
89
|
+
const sessionPlanPath = ctx.meta["plan.path"];
|
|
90
|
+
let planPath;
|
|
91
|
+
if (input.scope === "project") {
|
|
92
|
+
if (typeof sessionPlanPath === "string") {
|
|
93
|
+
const lastSep = Math.max(
|
|
94
|
+
sessionPlanPath.lastIndexOf("/"),
|
|
95
|
+
sessionPlanPath.lastIndexOf("\\")
|
|
96
|
+
);
|
|
97
|
+
if (lastSep < 0) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
message: `Cannot derive the project-scoped plan path: session plan path "${sessionPlanPath}" has no directory component.`,
|
|
101
|
+
plan: "",
|
|
102
|
+
count: 0,
|
|
103
|
+
open: 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
planPath = sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json";
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
planPath = sessionPlanPath;
|
|
110
|
+
}
|
|
111
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
message: "Plan storage path is not configured for this session.",
|
|
115
|
+
plan: "",
|
|
116
|
+
count: 0,
|
|
117
|
+
open: 0
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const sessionId = ctx.session?.id ?? "unknown";
|
|
121
|
+
let early = null;
|
|
122
|
+
const taskifyMeta = { title: "", details: "" };
|
|
123
|
+
let didTaskify = false;
|
|
124
|
+
let todosToReplace = null;
|
|
125
|
+
let plan;
|
|
126
|
+
try {
|
|
127
|
+
plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
128
|
+
switch (input.action) {
|
|
129
|
+
case "show":
|
|
130
|
+
break;
|
|
131
|
+
case "add": {
|
|
132
|
+
const title = input.title?.trim();
|
|
133
|
+
if (!title) {
|
|
134
|
+
early = mkResult(p, false, "add requires `title`.");
|
|
135
|
+
return p;
|
|
136
|
+
}
|
|
137
|
+
const { plan: updated } = addPlanItem(p, title, input.details?.trim() || void 0);
|
|
138
|
+
return updated;
|
|
139
|
+
}
|
|
140
|
+
case "start":
|
|
141
|
+
case "done": {
|
|
142
|
+
if (!input.target) {
|
|
143
|
+
early = mkResult(
|
|
144
|
+
p,
|
|
145
|
+
false,
|
|
146
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
147
|
+
);
|
|
148
|
+
return p;
|
|
149
|
+
}
|
|
150
|
+
const next = setPlanItemStatus(
|
|
151
|
+
p,
|
|
152
|
+
input.target,
|
|
153
|
+
input.action === "start" ? "in_progress" : "done"
|
|
154
|
+
);
|
|
155
|
+
if (next === p) {
|
|
156
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
157
|
+
return p;
|
|
158
|
+
}
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
case "status": {
|
|
162
|
+
if (!input.target || !input.status) {
|
|
163
|
+
early = mkResult(
|
|
164
|
+
p,
|
|
165
|
+
false,
|
|
166
|
+
"status requires `target` (id|index|substring) and `status`."
|
|
167
|
+
);
|
|
168
|
+
return p;
|
|
169
|
+
}
|
|
170
|
+
const next = setPlanItemStatus(p, input.target, input.status);
|
|
171
|
+
if (next === p) {
|
|
172
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
173
|
+
return p;
|
|
174
|
+
}
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
case "remove": {
|
|
178
|
+
if (!input.target) {
|
|
179
|
+
early = mkResult(p, false, "remove requires `target` (id|index|substring).");
|
|
180
|
+
return p;
|
|
181
|
+
}
|
|
182
|
+
const next = removePlanItem(p, input.target);
|
|
183
|
+
if (next === p) {
|
|
184
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
185
|
+
return p;
|
|
186
|
+
}
|
|
187
|
+
const nextIds = new Set(next.items.map((item) => item.id));
|
|
188
|
+
const removed = p.items.find((item) => !nextIds.has(item.id));
|
|
189
|
+
if (removed?.status !== "done") {
|
|
190
|
+
early = mkResult(
|
|
191
|
+
p,
|
|
192
|
+
false,
|
|
193
|
+
`Plan item "${removed?.title ?? input.target}" is not done and cannot be removed. Complete it first.`
|
|
194
|
+
);
|
|
195
|
+
return p;
|
|
196
|
+
}
|
|
197
|
+
return next;
|
|
198
|
+
}
|
|
199
|
+
case "promote": {
|
|
200
|
+
if (!input.target) {
|
|
201
|
+
early = mkResult(
|
|
202
|
+
p,
|
|
203
|
+
false,
|
|
204
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
205
|
+
);
|
|
206
|
+
return p;
|
|
207
|
+
}
|
|
208
|
+
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
209
|
+
if (!derived) {
|
|
210
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
211
|
+
return p;
|
|
212
|
+
}
|
|
213
|
+
todosToReplace = derived.todos;
|
|
214
|
+
early = mkResult(
|
|
215
|
+
derived.plan,
|
|
216
|
+
true,
|
|
217
|
+
`${input.action} ok \u2014 ${derived.todos.length} todo(s) created.`,
|
|
218
|
+
derived.todos
|
|
219
|
+
);
|
|
220
|
+
return derived.plan;
|
|
221
|
+
}
|
|
222
|
+
case "template_use": {
|
|
223
|
+
const templateName = input.template?.trim();
|
|
224
|
+
if (!templateName) {
|
|
225
|
+
early = mkResult(p, false, "template_use requires `template` name.");
|
|
226
|
+
return p;
|
|
227
|
+
}
|
|
228
|
+
const template = getPlanTemplate(templateName);
|
|
229
|
+
if (!template) {
|
|
230
|
+
early = mkResult(p, false, `Unknown template "${templateName}".`);
|
|
231
|
+
return p;
|
|
232
|
+
}
|
|
233
|
+
let updated = p;
|
|
234
|
+
for (const item of template.items) {
|
|
235
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
236
|
+
}
|
|
237
|
+
early = mkResult(
|
|
238
|
+
updated,
|
|
239
|
+
true,
|
|
240
|
+
`Applied template "${template.name}" \u2014 ${template.items.length} items added.`
|
|
241
|
+
);
|
|
242
|
+
return updated;
|
|
243
|
+
}
|
|
244
|
+
case "clear":
|
|
245
|
+
if (p.items.some((item) => item.status !== "done")) {
|
|
246
|
+
early = mkResult(
|
|
247
|
+
p,
|
|
248
|
+
false,
|
|
249
|
+
"Plan contains unfinished items and cannot be cleared. Complete them first."
|
|
250
|
+
);
|
|
251
|
+
return p;
|
|
252
|
+
}
|
|
253
|
+
return clearPlan(p);
|
|
254
|
+
case "taskify": {
|
|
255
|
+
if (!input.target) {
|
|
256
|
+
early = mkResult(
|
|
257
|
+
p,
|
|
258
|
+
false,
|
|
259
|
+
"taskify requires `target` (plan item id|index|substring)."
|
|
260
|
+
);
|
|
261
|
+
return p;
|
|
262
|
+
}
|
|
263
|
+
let itemIdx = -1;
|
|
264
|
+
const asNum = Number.parseInt(input.target, 10);
|
|
265
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= p.items.length) {
|
|
266
|
+
itemIdx = asNum - 1;
|
|
267
|
+
} else {
|
|
268
|
+
itemIdx = p.items.findIndex((it) => it.id === input.target);
|
|
269
|
+
if (itemIdx === -1) {
|
|
270
|
+
const lower = input.target.toLowerCase();
|
|
271
|
+
itemIdx = p.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (itemIdx === -1 || !p.items[itemIdx]) {
|
|
275
|
+
early = mkResult(p, false, `No plan item matched "${input.target}".`);
|
|
276
|
+
return p;
|
|
277
|
+
}
|
|
278
|
+
const item = p.items[itemIdx];
|
|
279
|
+
taskifyMeta.title = item.title;
|
|
280
|
+
taskifyMeta.details = item.details ?? "";
|
|
281
|
+
didTaskify = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
early = mkResult(p, false, `Unknown action "${input.action}".`);
|
|
286
|
+
return p;
|
|
287
|
+
}
|
|
288
|
+
return p;
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
message: `Plan change not saved \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
294
|
+
plan: "",
|
|
295
|
+
count: 0,
|
|
296
|
+
open: 0
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
ctx.meta["plan.path.resolved"] = planPath;
|
|
300
|
+
await projectSessionPlanToKanban(ctx.projectRoot, plan.items, sessionId);
|
|
301
|
+
if (todosToReplace) {
|
|
302
|
+
await todoTool.execute({ todos: todosToReplace }, ctx, {
|
|
303
|
+
signal: AbortSignal.timeout(3e4)
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (early) return early;
|
|
307
|
+
if (didTaskify) {
|
|
308
|
+
const taskPathRaw = ctx.meta["task.path"];
|
|
309
|
+
if (typeof taskPathRaw !== "string" || !taskPathRaw) {
|
|
310
|
+
return mkResult(plan, false, "Task storage path not configured \u2014 cannot taskify.");
|
|
311
|
+
}
|
|
312
|
+
let taskPath = taskPathRaw;
|
|
313
|
+
if (input.scope === "project") {
|
|
314
|
+
const lastSep = Math.max(taskPath.lastIndexOf("/"), taskPath.lastIndexOf("\\"));
|
|
315
|
+
taskPath = lastSep >= 0 ? taskPath.slice(0, lastSep + 1) + "backlog.tasks.json" : "backlog.tasks.json";
|
|
316
|
+
}
|
|
317
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
318
|
+
try {
|
|
319
|
+
const taskFile = await mutateTasks(taskPath, sessionId, (f) => {
|
|
320
|
+
f.tasks.push({
|
|
321
|
+
id: `task_${randomUUID()}`,
|
|
322
|
+
title: taskifyMeta.title,
|
|
323
|
+
description: taskifyMeta.details || void 0,
|
|
324
|
+
type: "feature",
|
|
325
|
+
priority: "medium",
|
|
326
|
+
status: "pending",
|
|
327
|
+
createdAt: now,
|
|
328
|
+
updatedAt: now
|
|
329
|
+
});
|
|
330
|
+
return f;
|
|
331
|
+
});
|
|
332
|
+
ctx.meta["task.path.resolved"] = taskPath;
|
|
333
|
+
return mkResult(
|
|
334
|
+
plan,
|
|
335
|
+
true,
|
|
336
|
+
`taskify ok \u2014 added "${taskifyMeta.title}" to tasks.
|
|
337
|
+
${formatTaskList(taskFile.tasks)}`
|
|
338
|
+
);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return mkResult(
|
|
341
|
+
plan,
|
|
342
|
+
false,
|
|
343
|
+
`taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function mkResult(plan, ok, message, todos) {
|
|
351
|
+
const open = plan.items.filter((i) => i.status !== "done").length;
|
|
352
|
+
const result = {
|
|
353
|
+
ok,
|
|
354
|
+
message,
|
|
355
|
+
plan: formatPlan(plan),
|
|
356
|
+
count: plan.items.length,
|
|
357
|
+
open
|
|
358
|
+
};
|
|
359
|
+
if (todos !== void 0) result.todos = todos;
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
planTool
|
|
365
|
+
};
|
|
366
|
+
//# sourceMappingURL=chunk-KXNDHMUB.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/codebase-index/gitignore.ts
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { compileGlob } from "@wrongstack/core/utils";
|
|
5
|
+
function globBody(glob) {
|
|
6
|
+
return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
|
|
7
|
+
}
|
|
8
|
+
function compileGitignore(lines) {
|
|
9
|
+
const rules = [];
|
|
10
|
+
for (const raw of lines) {
|
|
11
|
+
let line = raw.replace(/\r$/, "");
|
|
12
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
13
|
+
line = line.trim();
|
|
14
|
+
let negated = false;
|
|
15
|
+
if (line.startsWith("!")) {
|
|
16
|
+
negated = true;
|
|
17
|
+
line = line.slice(1);
|
|
18
|
+
}
|
|
19
|
+
let dirOnly = false;
|
|
20
|
+
if (line.endsWith("/")) {
|
|
21
|
+
dirOnly = true;
|
|
22
|
+
line = line.slice(0, -1);
|
|
23
|
+
}
|
|
24
|
+
if (!line) continue;
|
|
25
|
+
const anchored = line.startsWith("/") || line.includes("/");
|
|
26
|
+
if (line.startsWith("/")) line = line.slice(1);
|
|
27
|
+
const body = globBody(line);
|
|
28
|
+
const prefix = anchored ? "^" : "(?:^|.*/)";
|
|
29
|
+
rules.push({
|
|
30
|
+
eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
|
|
31
|
+
under: new RegExp(`${prefix}${body}/.*$`),
|
|
32
|
+
negated,
|
|
33
|
+
dirOnly
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return (relPath, isDir) => {
|
|
37
|
+
const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
38
|
+
let ignored = false;
|
|
39
|
+
for (const r of rules) {
|
|
40
|
+
const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
|
|
41
|
+
if (re.test(p)) ignored = !r.negated;
|
|
42
|
+
}
|
|
43
|
+
return ignored;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function loadGitignoreMatcher(projectRoot) {
|
|
47
|
+
let lines = [];
|
|
48
|
+
try {
|
|
49
|
+
const raw = await fs.readFile(path.join(projectRoot, ".gitignore"), "utf8");
|
|
50
|
+
lines = raw.split("\n");
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
return compileGitignore(lines);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export {
|
|
57
|
+
loadGitignoreMatcher
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=chunk-LIJQMYYX.js.map
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createOutputSpool,
|
|
3
|
+
spoolNote
|
|
4
|
+
} from "./chunk-2TFQDEPU.js";
|
|
5
|
+
import {
|
|
6
|
+
getProcessRegistry,
|
|
7
|
+
redactCommand
|
|
8
|
+
} from "./chunk-3PVYQFRQ.js";
|
|
9
|
+
import {
|
|
10
|
+
buildWin32CmdShimInvocation,
|
|
11
|
+
resolveWin32Command
|
|
12
|
+
} from "./chunk-V43EQ6VR.js";
|
|
13
|
+
|
|
14
|
+
// src/_spawn-stream.ts
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import {
|
|
17
|
+
emitProcessCompleted,
|
|
18
|
+
emitProcessOutput,
|
|
19
|
+
emitProcessStarted
|
|
20
|
+
} from "@wrongstack/core/observability";
|
|
21
|
+
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
22
|
+
var isWin = process.platform === "win32";
|
|
23
|
+
async function* spawnStream(opts) {
|
|
24
|
+
const max = opts.maxBytes ?? 999999999;
|
|
25
|
+
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
26
|
+
const maxQueue = opts.maxQueueSize ?? 500;
|
|
27
|
+
const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
|
|
28
|
+
let stdout = "";
|
|
29
|
+
let stderr = "";
|
|
30
|
+
let stdoutRetainedBytes = 0;
|
|
31
|
+
let stderrRetainedBytes = 0;
|
|
32
|
+
let pending = "";
|
|
33
|
+
let error;
|
|
34
|
+
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
35
|
+
const resolved = resolveWin32Command(opts.cmd);
|
|
36
|
+
const needsShell = isWin && (resolved.endsWith(".cmd") || resolved.endsWith(".bat"));
|
|
37
|
+
const shim = needsShell ? buildWin32CmdShimInvocation(resolved, opts.args) : null;
|
|
38
|
+
const cmd = shim?.command ?? resolved;
|
|
39
|
+
const args = shim?.args ?? opts.args;
|
|
40
|
+
const child = spawn(cmd, args, {
|
|
41
|
+
cwd: opts.cwd,
|
|
42
|
+
env: buildChildEnv(),
|
|
43
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
...isWin ? {} : { signal: opts.signal },
|
|
46
|
+
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
47
|
+
});
|
|
48
|
+
const registry = getProcessRegistry();
|
|
49
|
+
const pid = child.pid;
|
|
50
|
+
const processStartedAt = Date.now();
|
|
51
|
+
let stdoutBytes = 0;
|
|
52
|
+
let stderrBytes = 0;
|
|
53
|
+
let telemetryCompleted = false;
|
|
54
|
+
emitProcessStarted({
|
|
55
|
+
...pid !== void 0 ? { pid } : {},
|
|
56
|
+
parentPid: process.pid,
|
|
57
|
+
command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
|
|
58
|
+
args: redactCommand(opts.args.join(" ")).split(" ").filter(Boolean),
|
|
59
|
+
cwd: opts.cwd,
|
|
60
|
+
background: false,
|
|
61
|
+
startedAt: new Date(processStartedAt).toISOString()
|
|
62
|
+
});
|
|
63
|
+
if (typeof pid === "number") {
|
|
64
|
+
registry.register({
|
|
65
|
+
pid,
|
|
66
|
+
name: opts.cmd,
|
|
67
|
+
command: redactCommand(`${opts.cmd} ${opts.args.join(" ")}`),
|
|
68
|
+
startedAt: Date.now(),
|
|
69
|
+
child
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const queue = [];
|
|
73
|
+
let queuedBytes = 0;
|
|
74
|
+
let waiter;
|
|
75
|
+
let paused = false;
|
|
76
|
+
const wake = () => {
|
|
77
|
+
if (waiter) {
|
|
78
|
+
const w = waiter;
|
|
79
|
+
waiter = void 0;
|
|
80
|
+
w();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const resume = () => {
|
|
84
|
+
if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
|
|
85
|
+
paused = false;
|
|
86
|
+
child.stdout?.resume();
|
|
87
|
+
child.stderr?.resume();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const onOut = (c) => {
|
|
91
|
+
const s = c.toString();
|
|
92
|
+
stdoutBytes += c.byteLength;
|
|
93
|
+
emitProcessOutput({ pid, stream: "stdout", chunk: c });
|
|
94
|
+
if (stdoutRetainedBytes < max) {
|
|
95
|
+
const retained = utf8Prefix(s, max - stdoutRetainedBytes);
|
|
96
|
+
stdout += retained;
|
|
97
|
+
stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
98
|
+
}
|
|
99
|
+
spool.write(s);
|
|
100
|
+
queue.push({ kind: "out", data: s, bytes: c.byteLength });
|
|
101
|
+
queuedBytes += c.byteLength;
|
|
102
|
+
wake();
|
|
103
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
104
|
+
paused = true;
|
|
105
|
+
child.stdout?.pause();
|
|
106
|
+
child.stderr?.pause();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const onErr = (c) => {
|
|
110
|
+
const s = c.toString();
|
|
111
|
+
stderrBytes += c.byteLength;
|
|
112
|
+
emitProcessOutput({ pid, stream: "stderr", chunk: c });
|
|
113
|
+
if (stderrRetainedBytes < max) {
|
|
114
|
+
const retained = utf8Prefix(s, max - stderrRetainedBytes);
|
|
115
|
+
stderr += retained;
|
|
116
|
+
stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
117
|
+
}
|
|
118
|
+
spool.write(s);
|
|
119
|
+
queue.push({ kind: "err", data: s, bytes: c.byteLength });
|
|
120
|
+
queuedBytes += c.byteLength;
|
|
121
|
+
wake();
|
|
122
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
123
|
+
paused = true;
|
|
124
|
+
child.stdout?.pause();
|
|
125
|
+
child.stderr?.pause();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
child.stdout?.on("data", onOut);
|
|
129
|
+
child.stderr?.on("data", onErr);
|
|
130
|
+
child.on("error", (e) => {
|
|
131
|
+
error = e.message;
|
|
132
|
+
const bytes = Buffer.byteLength(e.message, "utf8");
|
|
133
|
+
queue.push({ kind: "error", data: e.message, bytes });
|
|
134
|
+
queuedBytes += bytes;
|
|
135
|
+
wake();
|
|
136
|
+
});
|
|
137
|
+
const completeTelemetry = (code, signal, timedOut = false) => {
|
|
138
|
+
if (telemetryCompleted) return;
|
|
139
|
+
telemetryCompleted = true;
|
|
140
|
+
emitProcessCompleted({
|
|
141
|
+
...pid !== void 0 ? { pid } : {},
|
|
142
|
+
exitCode: code,
|
|
143
|
+
...signal ? { signal } : {},
|
|
144
|
+
durationMs: Date.now() - processStartedAt,
|
|
145
|
+
stdoutBytes,
|
|
146
|
+
stderrBytes,
|
|
147
|
+
timedOut,
|
|
148
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
child.on("close", (code, signal) => {
|
|
152
|
+
if (typeof pid === "number") registry.unregister(pid);
|
|
153
|
+
const exitCode2 = code ?? (signal ? 1 : 0);
|
|
154
|
+
completeTelemetry(exitCode2, signal ?? void 0);
|
|
155
|
+
queue.push({
|
|
156
|
+
kind: "close",
|
|
157
|
+
data: "",
|
|
158
|
+
bytes: 0,
|
|
159
|
+
code: exitCode2,
|
|
160
|
+
...signal ? { signal } : {}
|
|
161
|
+
});
|
|
162
|
+
wake();
|
|
163
|
+
});
|
|
164
|
+
const onAbort = () => {
|
|
165
|
+
if (typeof pid === "number") {
|
|
166
|
+
registry.kill(pid, { force: true });
|
|
167
|
+
} else {
|
|
168
|
+
try {
|
|
169
|
+
child.kill("SIGKILL");
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
|
|
174
|
+
completeTelemetry(124, "SIGKILL", true);
|
|
175
|
+
wake();
|
|
176
|
+
};
|
|
177
|
+
if (isWin) {
|
|
178
|
+
if (opts.signal.aborted) onAbort();
|
|
179
|
+
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
180
|
+
}
|
|
181
|
+
let exitCode = 0;
|
|
182
|
+
let spawnFailed = false;
|
|
183
|
+
try {
|
|
184
|
+
for (; ; ) {
|
|
185
|
+
while (queue.length === 0) {
|
|
186
|
+
await new Promise((resolve) => {
|
|
187
|
+
waiter = resolve;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const chunk = queue.shift();
|
|
191
|
+
queuedBytes -= chunk.bytes;
|
|
192
|
+
resume();
|
|
193
|
+
if (chunk.kind === "close") {
|
|
194
|
+
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (chunk.kind === "error") {
|
|
198
|
+
spawnFailed = true;
|
|
199
|
+
exitCode = 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
pending += chunk.data;
|
|
203
|
+
if (pending.length >= flushAt) {
|
|
204
|
+
yield { type: "partial_output", text: pending };
|
|
205
|
+
pending = "";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (pending.length > 0) {
|
|
209
|
+
yield { type: "partial_output", text: pending };
|
|
210
|
+
}
|
|
211
|
+
const spooled = spool.finalize();
|
|
212
|
+
return {
|
|
213
|
+
// The marker rides on stdout's tail so every consumer's head+tail
|
|
214
|
+
// normalization keeps it without per-tool changes.
|
|
215
|
+
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
216
|
+
stderr,
|
|
217
|
+
exitCode,
|
|
218
|
+
truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
|
|
219
|
+
error,
|
|
220
|
+
spoolPath: spooled?.path,
|
|
221
|
+
spoolBytes: spooled?.bytes
|
|
222
|
+
};
|
|
223
|
+
} finally {
|
|
224
|
+
spool.finalize();
|
|
225
|
+
if (isWin) opts.signal.removeEventListener("abort", onAbort);
|
|
226
|
+
child.stdout?.off("data", onOut);
|
|
227
|
+
child.stderr?.off("data", onErr);
|
|
228
|
+
child.stdout?.destroy();
|
|
229
|
+
child.stderr?.destroy();
|
|
230
|
+
if (child.exitCode === null && !child.killed) {
|
|
231
|
+
if (typeof pid === "number") {
|
|
232
|
+
registry.kill(pid, { force: true });
|
|
233
|
+
} else {
|
|
234
|
+
try {
|
|
235
|
+
child.kill("SIGKILL");
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function utf8Prefix(text, maxBytes) {
|
|
243
|
+
if (maxBytes <= 0) return "";
|
|
244
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
245
|
+
let low = 0;
|
|
246
|
+
let high = text.length;
|
|
247
|
+
while (low < high) {
|
|
248
|
+
const mid = Math.ceil((low + high) / 2);
|
|
249
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
|
|
250
|
+
else high = mid - 1;
|
|
251
|
+
}
|
|
252
|
+
let end = low;
|
|
253
|
+
if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
|
|
254
|
+
return text.slice(0, end);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export {
|
|
258
|
+
spawnStream
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=chunk-MAN26PZ5.js.map
|