@wrongstack/webui 0.272.2 → 0.273.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/assets/index-BGzM4-Zu.css +2 -0
- package/dist/assets/index-D0dNaLPf.js +140 -0
- package/dist/assets/vendor-Doh9e_v3.css +1 -0
- package/dist/assets/{vendor-cIhL9uWi.js → vendor-P9eRrO6V.js} +296 -296
- package/dist/index.html +4 -4
- package/dist/index.js +8175 -4852
- package/dist/index.js.map +1 -1
- package/dist/server/entry.js +1182 -413
- package/dist/server/entry.js.map +1 -1
- package/dist/server/handlers.js +5 -2
- package/dist/server/handlers.js.map +1 -1
- package/dist/server/index.d.ts +165 -2
- package/dist/server/index.js +1187 -414
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +201 -1
- package/package.json +6 -6
- package/dist/assets/index-CTDuGGlX.css +0 -2
- package/dist/assets/index-DSfCNNx4.js +0 -122
- package/dist/assets/vendor-BzqIVtlT.css +0 -1
package/dist/server/index.js
CHANGED
|
@@ -7,7 +7,10 @@ function isRecord(value) {
|
|
|
7
7
|
}
|
|
8
8
|
function validateModelSwitchPayload(payload) {
|
|
9
9
|
if (!isRecord(payload)) {
|
|
10
|
-
return {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
message: "model.switch payload must be an object with string provider and model"
|
|
13
|
+
};
|
|
11
14
|
}
|
|
12
15
|
const provider = payload["provider"];
|
|
13
16
|
const model = payload["model"];
|
|
@@ -29,13 +32,22 @@ function validateMailboxMessagesPayload(payload) {
|
|
|
29
32
|
const agentId = payload["agentId"];
|
|
30
33
|
const unreadOnly = payload["unreadOnly"];
|
|
31
34
|
if (limit !== void 0 && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1)) {
|
|
32
|
-
return {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
message: "mailbox.messages payload.limit must be a positive number when provided"
|
|
38
|
+
};
|
|
33
39
|
}
|
|
34
40
|
if (agentId !== void 0 && typeof agentId !== "string") {
|
|
35
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: "mailbox.messages payload.agentId must be a string when provided"
|
|
44
|
+
};
|
|
36
45
|
}
|
|
37
46
|
if (unreadOnly !== void 0 && typeof unreadOnly !== "boolean") {
|
|
38
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
message: "mailbox.messages payload.unreadOnly must be a boolean when provided"
|
|
50
|
+
};
|
|
39
51
|
}
|
|
40
52
|
return { ok: true, value: { limit, agentId, unreadOnly } };
|
|
41
53
|
}
|
|
@@ -46,7 +58,10 @@ function validateMailboxAgentsPayload(payload) {
|
|
|
46
58
|
}
|
|
47
59
|
const onlineOnly = payload["onlineOnly"];
|
|
48
60
|
if (onlineOnly !== void 0 && typeof onlineOnly !== "boolean") {
|
|
49
|
-
return {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
message: "mailbox.agents payload.onlineOnly must be a boolean when provided"
|
|
64
|
+
};
|
|
50
65
|
}
|
|
51
66
|
return { ok: true, value: { onlineOnly } };
|
|
52
67
|
}
|
|
@@ -58,10 +73,16 @@ function validateMailboxPurgePayload(payload) {
|
|
|
58
73
|
const completedMaxAgeMs = payload["completedMaxAgeMs"];
|
|
59
74
|
const incompleteMaxAgeMs = payload["incompleteMaxAgeMs"];
|
|
60
75
|
if (completedMaxAgeMs !== void 0 && (typeof completedMaxAgeMs !== "number" || !Number.isFinite(completedMaxAgeMs) || completedMaxAgeMs < 0)) {
|
|
61
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided"
|
|
79
|
+
};
|
|
62
80
|
}
|
|
63
81
|
if (incompleteMaxAgeMs !== void 0 && (typeof incompleteMaxAgeMs !== "number" || !Number.isFinite(incompleteMaxAgeMs) || incompleteMaxAgeMs < 0)) {
|
|
64
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided"
|
|
85
|
+
};
|
|
65
86
|
}
|
|
66
87
|
return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };
|
|
67
88
|
}
|
|
@@ -72,7 +93,10 @@ function validateBrainRiskPayload(payload) {
|
|
|
72
93
|
}
|
|
73
94
|
const level = payload["level"];
|
|
74
95
|
if (typeof level !== "string" || !BRAIN_RISK_VALUES.has(level)) {
|
|
75
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
message: "brain.risk payload.level must be one of off, low, medium, high, all"
|
|
99
|
+
};
|
|
76
100
|
}
|
|
77
101
|
return { ok: true, value: { level } };
|
|
78
102
|
}
|
|
@@ -98,7 +122,10 @@ function validateAutonomySwitchPayload(payload) {
|
|
|
98
122
|
}
|
|
99
123
|
function validatePlanTemplateUsePayload(payload) {
|
|
100
124
|
if (!isRecord(payload)) {
|
|
101
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
message: "plan.template_use payload must be an object with string template"
|
|
128
|
+
};
|
|
102
129
|
}
|
|
103
130
|
const template = payload["template"];
|
|
104
131
|
if (typeof template !== "string" || template.trim().length === 0) {
|
|
@@ -113,7 +140,15 @@ var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
|
|
|
113
140
|
var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
|
|
114
141
|
var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
|
|
115
142
|
var REASONING_MODE_VALUES = /* @__PURE__ */ new Set(["auto", "on", "off"]);
|
|
116
|
-
var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
|
|
143
|
+
var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
|
|
144
|
+
"none",
|
|
145
|
+
"minimal",
|
|
146
|
+
"low",
|
|
147
|
+
"medium",
|
|
148
|
+
"high",
|
|
149
|
+
"xhigh",
|
|
150
|
+
"max"
|
|
151
|
+
]);
|
|
117
152
|
var CACHE_TTL_VALUES = /* @__PURE__ */ new Set(["default", "5m", "1h"]);
|
|
118
153
|
var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
119
154
|
"yolo",
|
|
@@ -134,8 +169,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
134
169
|
"tgDelegate",
|
|
135
170
|
"reasoningPreserve",
|
|
136
171
|
"hqEnabled",
|
|
137
|
-
"hqRawContent"
|
|
172
|
+
"hqRawContent",
|
|
173
|
+
"fallbackAuto"
|
|
138
174
|
]);
|
|
175
|
+
var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
|
|
139
176
|
var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
140
177
|
"autonomyDelayMs",
|
|
141
178
|
"autoProceedMaxIterations",
|
|
@@ -167,6 +204,9 @@ function validatePreferenceValue(key, value) {
|
|
|
167
204
|
if (STRING_PREF_KEYS.has(key)) {
|
|
168
205
|
return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
|
|
169
206
|
}
|
|
207
|
+
if (STRING_ARRAY_PREF_KEYS.has(key)) {
|
|
208
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
|
|
209
|
+
}
|
|
170
210
|
const allowed = ENUM_PREF_KEYS[key];
|
|
171
211
|
if (allowed) {
|
|
172
212
|
return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
|
|
@@ -287,16 +327,25 @@ function validateContextModeCreatePayload(payload) {
|
|
|
287
327
|
return { ok: false, message: "context.mode.create payload.description must be a string" };
|
|
288
328
|
}
|
|
289
329
|
if (!isRecord(thresholds)) {
|
|
290
|
-
return {
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers"
|
|
333
|
+
};
|
|
291
334
|
}
|
|
292
335
|
if (!isFiniteNumber(thresholds["warn"]) || !isFiniteNumber(thresholds["soft"]) || !isFiniteNumber(thresholds["hard"])) {
|
|
293
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers"
|
|
339
|
+
};
|
|
294
340
|
}
|
|
295
341
|
if (!isFiniteNumber(preserveK)) {
|
|
296
342
|
return { ok: false, message: "context.mode.create payload.preserveK must be a finite number" };
|
|
297
343
|
}
|
|
298
344
|
if (!isFiniteNumber(eliseThreshold)) {
|
|
299
|
-
return {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
message: "context.mode.create payload.eliseThreshold must be a finite number"
|
|
348
|
+
};
|
|
300
349
|
}
|
|
301
350
|
return {
|
|
302
351
|
ok: true,
|
|
@@ -320,22 +369,34 @@ function validateContextModeUpdatePayload(payload) {
|
|
|
320
369
|
}
|
|
321
370
|
const name2 = payload["name"];
|
|
322
371
|
if (name2 !== void 0 && typeof name2 !== "string") {
|
|
323
|
-
return {
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
message: "context.mode.update payload.name must be a string when provided"
|
|
375
|
+
};
|
|
324
376
|
}
|
|
325
377
|
const description = payload["description"];
|
|
326
378
|
if (description !== void 0 && typeof description !== "string") {
|
|
327
|
-
return {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
message: "context.mode.update payload.description must be a string when provided"
|
|
382
|
+
};
|
|
328
383
|
}
|
|
329
384
|
const thresholds = payload["thresholds"];
|
|
330
385
|
let validatedThresholds;
|
|
331
386
|
if (thresholds !== void 0) {
|
|
332
387
|
if (!isRecord(thresholds)) {
|
|
333
|
-
return {
|
|
388
|
+
return {
|
|
389
|
+
ok: false,
|
|
390
|
+
message: "context.mode.update payload.thresholds must be an object when provided"
|
|
391
|
+
};
|
|
334
392
|
}
|
|
335
393
|
for (const key of ["warn", "soft", "hard"]) {
|
|
336
394
|
const val = thresholds[key];
|
|
337
395
|
if (val !== void 0 && !isFiniteNumber(val)) {
|
|
338
|
-
return {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`
|
|
399
|
+
};
|
|
339
400
|
}
|
|
340
401
|
}
|
|
341
402
|
validatedThresholds = {
|
|
@@ -346,11 +407,17 @@ function validateContextModeUpdatePayload(payload) {
|
|
|
346
407
|
}
|
|
347
408
|
const preserveK = payload["preserveK"];
|
|
348
409
|
if (preserveK !== void 0 && !isFiniteNumber(preserveK)) {
|
|
349
|
-
return {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
message: "context.mode.update payload.preserveK must be a finite number when provided"
|
|
413
|
+
};
|
|
350
414
|
}
|
|
351
415
|
const eliseThreshold = payload["eliseThreshold"];
|
|
352
416
|
if (eliseThreshold !== void 0 && !isFiniteNumber(eliseThreshold)) {
|
|
353
|
-
return {
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
message: "context.mode.update payload.eliseThreshold must be a finite number when provided"
|
|
420
|
+
};
|
|
354
421
|
}
|
|
355
422
|
return {
|
|
356
423
|
ok: true,
|
|
@@ -368,28 +435,31 @@ function validateShellOpenPayload(payload) {
|
|
|
368
435
|
if (!isRecord(payload)) {
|
|
369
436
|
return { ok: false, message: "shell.open payload must be an object with string path" };
|
|
370
437
|
}
|
|
371
|
-
const
|
|
372
|
-
if (typeof
|
|
438
|
+
const path17 = payload["path"];
|
|
439
|
+
if (typeof path17 !== "string" || path17.trim().length === 0) {
|
|
373
440
|
return { ok: false, message: "shell.open payload.path must be a non-empty string" };
|
|
374
441
|
}
|
|
375
442
|
const target = payload["target"];
|
|
376
443
|
if (target !== void 0 && target !== "file" && target !== "terminal") {
|
|
377
|
-
return {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
message: 'shell.open payload.target must be "file" or "terminal" when provided'
|
|
447
|
+
};
|
|
378
448
|
}
|
|
379
|
-
return { ok: true, value: { path:
|
|
449
|
+
return { ok: true, value: { path: path17, target } };
|
|
380
450
|
}
|
|
381
451
|
function validateGitDiffPayload(payload) {
|
|
382
452
|
if (!isRecord(payload)) {
|
|
383
453
|
return { ok: false, message: "git.diff payload must be an object" };
|
|
384
454
|
}
|
|
385
|
-
const
|
|
386
|
-
if (
|
|
455
|
+
const path17 = payload["path"];
|
|
456
|
+
if (path17 === void 0 || path17 === null) {
|
|
387
457
|
return { ok: true, value: { path: "" } };
|
|
388
458
|
}
|
|
389
|
-
if (typeof
|
|
459
|
+
if (typeof path17 !== "string") {
|
|
390
460
|
return { ok: false, message: "git.diff payload.path must be a string when provided" };
|
|
391
461
|
}
|
|
392
|
-
return { ok: true, value: { path:
|
|
462
|
+
return { ok: true, value: { path: path17 } };
|
|
393
463
|
}
|
|
394
464
|
function validateProjectsAddPayload(payload) {
|
|
395
465
|
if (!isRecord(payload)) {
|
|
@@ -569,7 +639,7 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
|
569
639
|
return;
|
|
570
640
|
}
|
|
571
641
|
try {
|
|
572
|
-
const {
|
|
642
|
+
const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
573
643
|
let changed = false;
|
|
574
644
|
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
575
645
|
const before = p.updatedAt;
|
|
@@ -649,7 +719,7 @@ import {
|
|
|
649
719
|
createTieredBrainArbiter
|
|
650
720
|
} from "@wrongstack/core";
|
|
651
721
|
import * as fs13 from "fs/promises";
|
|
652
|
-
import * as
|
|
722
|
+
import * as path16 from "path";
|
|
653
723
|
|
|
654
724
|
// src/server/http-server.ts
|
|
655
725
|
import * as fs from "fs/promises";
|
|
@@ -826,7 +896,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
826
896
|
return;
|
|
827
897
|
}
|
|
828
898
|
try {
|
|
829
|
-
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore:
|
|
899
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
830
900
|
const registry = new SessionRegistry(globalRoot);
|
|
831
901
|
const entry = await registry.get(sessionId);
|
|
832
902
|
if (!entry) {
|
|
@@ -835,7 +905,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
835
905
|
return;
|
|
836
906
|
}
|
|
837
907
|
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
838
|
-
const store = new
|
|
908
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
839
909
|
const reader = new DefaultSessionReader2({ store });
|
|
840
910
|
const rawEntries = [];
|
|
841
911
|
for await (const ev of reader.replay(sessionId)) {
|
|
@@ -1521,8 +1591,8 @@ function isInside(root, target) {
|
|
|
1521
1591
|
}
|
|
1522
1592
|
|
|
1523
1593
|
// src/server/file-handlers.ts
|
|
1524
|
-
import * as
|
|
1525
|
-
import * as
|
|
1594
|
+
import * as fs4 from "fs/promises";
|
|
1595
|
+
import * as path4 from "path";
|
|
1526
1596
|
import { atomicWrite } from "@wrongstack/core";
|
|
1527
1597
|
|
|
1528
1598
|
// src/server/file-picker.ts
|
|
@@ -1573,6 +1643,34 @@ function rankFiles(paths, query, limit) {
|
|
|
1573
1643
|
return scored.slice(0, limit).map((s) => s.path);
|
|
1574
1644
|
}
|
|
1575
1645
|
|
|
1646
|
+
// src/server/path-containment.ts
|
|
1647
|
+
import * as fs3 from "fs/promises";
|
|
1648
|
+
import * as path3 from "path";
|
|
1649
|
+
function isPathInside(root, target) {
|
|
1650
|
+
const relative3 = path3.relative(root, target);
|
|
1651
|
+
return relative3 === "" || !relative3.startsWith("..") && !path3.isAbsolute(relative3);
|
|
1652
|
+
}
|
|
1653
|
+
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
1654
|
+
const resolved = path3.resolve(projectRoot, inputPath);
|
|
1655
|
+
let stat3;
|
|
1656
|
+
try {
|
|
1657
|
+
stat3 = await fs3.stat(resolved);
|
|
1658
|
+
} catch {
|
|
1659
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1660
|
+
}
|
|
1661
|
+
if (!stat3.isDirectory()) {
|
|
1662
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1663
|
+
}
|
|
1664
|
+
const [realProjectRoot, realResolved] = await Promise.all([
|
|
1665
|
+
fs3.realpath(projectRoot),
|
|
1666
|
+
fs3.realpath(resolved)
|
|
1667
|
+
]);
|
|
1668
|
+
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
1669
|
+
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
1670
|
+
}
|
|
1671
|
+
return resolved;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1576
1674
|
// src/server/ws-utils.ts
|
|
1577
1675
|
import { randomBytes } from "crypto";
|
|
1578
1676
|
import { WebSocket } from "ws";
|
|
@@ -1603,23 +1701,73 @@ function generateAuthToken() {
|
|
|
1603
1701
|
}
|
|
1604
1702
|
|
|
1605
1703
|
// src/server/file-handlers.ts
|
|
1704
|
+
async function resolveFileInsideProject(projectRoot, filePath) {
|
|
1705
|
+
const resolved = path4.resolve(projectRoot, filePath);
|
|
1706
|
+
if (!isPathInside(projectRoot, resolved)) {
|
|
1707
|
+
throw new Error("Path outside project root");
|
|
1708
|
+
}
|
|
1709
|
+
const { parent, base } = splitParentAndBase(resolved);
|
|
1710
|
+
const realProjectRoot = await fs4.realpath(projectRoot);
|
|
1711
|
+
const realParent = await realpathAllowMissing(parent);
|
|
1712
|
+
const realFull = path4.join(realParent, base);
|
|
1713
|
+
if (!isPathInside(realProjectRoot, realFull)) {
|
|
1714
|
+
throw new Error("Path outside project root");
|
|
1715
|
+
}
|
|
1716
|
+
return realFull;
|
|
1717
|
+
}
|
|
1718
|
+
function splitParentAndBase(p) {
|
|
1719
|
+
const base = path4.basename(p);
|
|
1720
|
+
const parent = path4.dirname(p);
|
|
1721
|
+
return { parent, base };
|
|
1722
|
+
}
|
|
1723
|
+
async function realpathAllowMissing(p) {
|
|
1724
|
+
try {
|
|
1725
|
+
return await fs4.realpath(p);
|
|
1726
|
+
} catch (err) {
|
|
1727
|
+
if (err.code !== "ENOENT") throw err;
|
|
1728
|
+
}
|
|
1729
|
+
const segments = [];
|
|
1730
|
+
let cursor = p;
|
|
1731
|
+
while (true) {
|
|
1732
|
+
const parent = path4.dirname(cursor);
|
|
1733
|
+
if (parent === cursor) {
|
|
1734
|
+
throw new Error("Path outside project root");
|
|
1735
|
+
}
|
|
1736
|
+
segments.unshift(path4.basename(cursor));
|
|
1737
|
+
try {
|
|
1738
|
+
const realParent = await fs4.realpath(parent);
|
|
1739
|
+
return path4.join(realParent, ...segments);
|
|
1740
|
+
} catch (err) {
|
|
1741
|
+
if (err.code !== "ENOENT") throw err;
|
|
1742
|
+
cursor = parent;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1606
1746
|
async function handleFilesTree(ws, msg, projectRoot) {
|
|
1607
1747
|
const payload = msg.payload;
|
|
1608
1748
|
const rawPath = payload?.path?.trim();
|
|
1609
|
-
|
|
1610
|
-
|
|
1749
|
+
let treeRoot;
|
|
1750
|
+
let realProjectRoot;
|
|
1751
|
+
try {
|
|
1752
|
+
if (rawPath && rawPath !== ".") {
|
|
1753
|
+
treeRoot = await resolveWorkingDirInsideProject(projectRoot, rawPath);
|
|
1754
|
+
} else {
|
|
1755
|
+
treeRoot = projectRoot;
|
|
1756
|
+
}
|
|
1757
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1758
|
+
} catch {
|
|
1611
1759
|
send(ws, {
|
|
1612
1760
|
type: "files.tree",
|
|
1613
1761
|
payload: { root: projectRoot, tree: [], error: "Path outside project root" }
|
|
1614
1762
|
});
|
|
1615
1763
|
return;
|
|
1616
1764
|
}
|
|
1617
|
-
const pathPrefix = treeRoot === projectRoot ? "" : (
|
|
1765
|
+
const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
|
|
1618
1766
|
async function buildTree(dir, rel, depth) {
|
|
1619
1767
|
if (depth > 10) return [];
|
|
1620
1768
|
let entries = [];
|
|
1621
1769
|
try {
|
|
1622
|
-
entries = await
|
|
1770
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1623
1771
|
} catch {
|
|
1624
1772
|
return [];
|
|
1625
1773
|
}
|
|
@@ -1631,11 +1779,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1631
1779
|
for (const e of entries) {
|
|
1632
1780
|
if (isHiddenEntry(e.name)) continue;
|
|
1633
1781
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1634
|
-
const childAbs =
|
|
1782
|
+
const childAbs = path4.join(dir, e.name);
|
|
1635
1783
|
const childPath = pathPrefix + childRel;
|
|
1636
1784
|
if (e.isDirectory()) {
|
|
1637
1785
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1638
|
-
|
|
1786
|
+
let realChild;
|
|
1787
|
+
try {
|
|
1788
|
+
realChild = await fs4.realpath(childAbs);
|
|
1789
|
+
} catch {
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const children = await buildTree(realChild, childRel, depth + 1);
|
|
1639
1796
|
nodes.push({ name: e.name, path: childPath, type: "directory", children });
|
|
1640
1797
|
} else if (e.isFile()) {
|
|
1641
1798
|
nodes.push({ name: e.name, path: childPath, type: "file" });
|
|
@@ -1645,10 +1802,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1645
1802
|
}
|
|
1646
1803
|
try {
|
|
1647
1804
|
const tree = await buildTree(treeRoot, "", 0);
|
|
1648
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1805
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1649
1806
|
send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
|
|
1650
1807
|
} catch (err) {
|
|
1651
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1808
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1652
1809
|
send(ws, {
|
|
1653
1810
|
type: "files.tree",
|
|
1654
1811
|
payload: { root: rootLabel, tree: [], error: errMessage(err) }
|
|
@@ -1657,13 +1814,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1657
1814
|
}
|
|
1658
1815
|
async function handleFilesRead(ws, msg, projectRoot) {
|
|
1659
1816
|
const { filePath } = msg.payload;
|
|
1660
|
-
|
|
1661
|
-
|
|
1817
|
+
let realResolved;
|
|
1818
|
+
try {
|
|
1819
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1820
|
+
} catch {
|
|
1662
1821
|
send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
|
|
1663
1822
|
return;
|
|
1664
1823
|
}
|
|
1665
1824
|
try {
|
|
1666
|
-
const content = await
|
|
1825
|
+
const content = await fs4.readFile(realResolved, "utf8");
|
|
1667
1826
|
send(ws, { type: "files.read", payload: { filePath, content } });
|
|
1668
1827
|
} catch (err) {
|
|
1669
1828
|
send(ws, {
|
|
@@ -1674,16 +1833,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
|
|
|
1674
1833
|
}
|
|
1675
1834
|
async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
1676
1835
|
const { filePath, content } = msg.payload;
|
|
1677
|
-
|
|
1678
|
-
|
|
1836
|
+
let realResolved;
|
|
1837
|
+
try {
|
|
1838
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1839
|
+
} catch {
|
|
1679
1840
|
send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
|
|
1680
1841
|
return;
|
|
1681
1842
|
}
|
|
1682
1843
|
try {
|
|
1683
|
-
await atomicWrite(
|
|
1844
|
+
await atomicWrite(realResolved, content);
|
|
1684
1845
|
send(ws, { type: "files.written", payload: { filePath, success: true } });
|
|
1685
1846
|
if (opts.onWritten) {
|
|
1686
|
-
void Promise.resolve(opts.onWritten(
|
|
1847
|
+
void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
|
|
1687
1848
|
}
|
|
1688
1849
|
} catch (err) {
|
|
1689
1850
|
send(ws, {
|
|
@@ -1695,8 +1856,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
|
1695
1856
|
async function handleFilesList(ws, msg, projectRoot) {
|
|
1696
1857
|
const payload = msg.payload ?? {};
|
|
1697
1858
|
const limit = payload.limit ?? 50;
|
|
1698
|
-
|
|
1699
|
-
|
|
1859
|
+
let listRoot;
|
|
1860
|
+
let realProjectRoot;
|
|
1861
|
+
try {
|
|
1862
|
+
if (payload.path) {
|
|
1863
|
+
listRoot = await resolveWorkingDirInsideProject(projectRoot, payload.path);
|
|
1864
|
+
} else {
|
|
1865
|
+
listRoot = projectRoot;
|
|
1866
|
+
}
|
|
1867
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1868
|
+
} catch {
|
|
1700
1869
|
send(ws, { type: "files.list", payload: { files: [] } });
|
|
1701
1870
|
return;
|
|
1702
1871
|
}
|
|
@@ -1705,7 +1874,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1705
1874
|
if (depth > 8 || results.length >= 600) return;
|
|
1706
1875
|
let entries = [];
|
|
1707
1876
|
try {
|
|
1708
|
-
entries = await
|
|
1877
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1709
1878
|
} catch {
|
|
1710
1879
|
return;
|
|
1711
1880
|
}
|
|
@@ -1715,7 +1884,16 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1715
1884
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1716
1885
|
if (e.isDirectory()) {
|
|
1717
1886
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1718
|
-
|
|
1887
|
+
let realChild;
|
|
1888
|
+
try {
|
|
1889
|
+
realChild = await fs4.realpath(path4.join(dir, e.name));
|
|
1890
|
+
} catch {
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
await walk(realChild, childRel, depth + 1);
|
|
1719
1897
|
} else if (e.isFile()) {
|
|
1720
1898
|
results.push(childRel);
|
|
1721
1899
|
}
|
|
@@ -1729,7 +1907,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1729
1907
|
}
|
|
1730
1908
|
|
|
1731
1909
|
// src/server/completion-handlers.ts
|
|
1732
|
-
import * as
|
|
1910
|
+
import * as path5 from "path";
|
|
1733
1911
|
import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
|
|
1734
1912
|
var MAX_PREFIX_CHARS = 12e3;
|
|
1735
1913
|
var MAX_SUFFIX_CHARS = 4e3;
|
|
@@ -1804,8 +1982,8 @@ async function handleCompletionRequest(ws, msg, opts) {
|
|
|
1804
1982
|
return;
|
|
1805
1983
|
}
|
|
1806
1984
|
const payload = parsed.payload;
|
|
1807
|
-
const projectRoot =
|
|
1808
|
-
const resolved =
|
|
1985
|
+
const projectRoot = path5.resolve(opts.projectRoot);
|
|
1986
|
+
const resolved = path5.resolve(projectRoot, payload.filePath);
|
|
1809
1987
|
if (!isInside2(projectRoot, resolved)) {
|
|
1810
1988
|
send(ws, {
|
|
1811
1989
|
type: "completion.result",
|
|
@@ -2204,7 +2382,7 @@ function buildSearchQuery(linePrefix, filePath) {
|
|
|
2204
2382
|
if (memberMatch?.[1]) return memberMatch[1];
|
|
2205
2383
|
const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
|
|
2206
2384
|
if (token && token.length >= 2) return token;
|
|
2207
|
-
return
|
|
2385
|
+
return path5.basename(filePath, path5.extname(filePath));
|
|
2208
2386
|
}
|
|
2209
2387
|
function currentLinePrefix(prefix) {
|
|
2210
2388
|
const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
|
|
@@ -2234,7 +2412,7 @@ function head(value, max) {
|
|
|
2234
2412
|
return value.length <= max ? value : value.slice(0, max);
|
|
2235
2413
|
}
|
|
2236
2414
|
function isInside2(root, target) {
|
|
2237
|
-
return target === root || target.startsWith(root +
|
|
2415
|
+
return target === root || target.startsWith(root + path5.sep);
|
|
2238
2416
|
}
|
|
2239
2417
|
|
|
2240
2418
|
// src/server/memory-handlers.ts
|
|
@@ -2488,8 +2666,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
2488
2666
|
}
|
|
2489
2667
|
|
|
2490
2668
|
// src/server/skills-handlers.ts
|
|
2491
|
-
import { promises as
|
|
2492
|
-
import
|
|
2669
|
+
import { promises as fs5 } from "fs";
|
|
2670
|
+
import path6 from "path";
|
|
2493
2671
|
import JSZip from "jszip";
|
|
2494
2672
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
2495
2673
|
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
@@ -2560,19 +2738,19 @@ async function handleSkillsContent(ws, ctx, msg) {
|
|
|
2560
2738
|
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
2561
2739
|
return;
|
|
2562
2740
|
}
|
|
2563
|
-
const body = await
|
|
2564
|
-
const skillDir =
|
|
2741
|
+
const body = await fs5.readFile(entry.path, "utf8");
|
|
2742
|
+
const skillDir = path6.dirname(entry.path);
|
|
2565
2743
|
let relatedFiles = [];
|
|
2566
2744
|
try {
|
|
2567
|
-
const files = await
|
|
2568
|
-
relatedFiles = files.filter((f) => f !==
|
|
2745
|
+
const files = await fs5.readdir(skillDir);
|
|
2746
|
+
relatedFiles = files.filter((f) => f !== path6.basename(entry.path)).map((f) => path6.join(skillDir, f));
|
|
2569
2747
|
} catch {
|
|
2570
2748
|
}
|
|
2571
2749
|
const nameLower = name2.toLowerCase();
|
|
2572
2750
|
const refResults = await Promise.all(
|
|
2573
2751
|
entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
|
|
2574
2752
|
try {
|
|
2575
|
-
const content = await
|
|
2753
|
+
const content = await fs5.readFile(e.path, "utf8");
|
|
2576
2754
|
return [e.name, content.toLowerCase().includes(nameLower)];
|
|
2577
2755
|
} catch {
|
|
2578
2756
|
return [e.name, false];
|
|
@@ -2662,14 +2840,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
|
|
|
2662
2840
|
}
|
|
2663
2841
|
const createPayload = parsed.value;
|
|
2664
2842
|
try {
|
|
2665
|
-
const targetDir = createPayload.scope === "global" ?
|
|
2843
|
+
const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
2666
2844
|
try {
|
|
2667
|
-
await
|
|
2845
|
+
await fs5.access(targetDir);
|
|
2668
2846
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
2669
2847
|
return;
|
|
2670
2848
|
} catch {
|
|
2671
2849
|
}
|
|
2672
|
-
await
|
|
2850
|
+
await fs5.mkdir(targetDir, { recursive: true });
|
|
2673
2851
|
const lines = createPayload.description.trim().split("\n");
|
|
2674
2852
|
const firstLine = lines[0].trim();
|
|
2675
2853
|
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
@@ -2717,13 +2895,13 @@ ${trigger}
|
|
|
2717
2895
|
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
2718
2896
|
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
2719
2897
|
].join("\n");
|
|
2720
|
-
await atomicWrite2(
|
|
2898
|
+
await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
|
|
2721
2899
|
send(ws, {
|
|
2722
2900
|
type: "skills.created",
|
|
2723
2901
|
payload: {
|
|
2724
2902
|
success: true,
|
|
2725
2903
|
error: null,
|
|
2726
|
-
skill: { name: createPayload.name.trim(), path:
|
|
2904
|
+
skill: { name: createPayload.name.trim(), path: path6.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
2727
2905
|
}
|
|
2728
2906
|
});
|
|
2729
2907
|
} catch (err) {
|
|
@@ -2787,23 +2965,23 @@ import {
|
|
|
2787
2965
|
Agent,
|
|
2788
2966
|
AutoCompactionMiddleware,
|
|
2789
2967
|
Context,
|
|
2790
|
-
DefaultMemoryStore
|
|
2791
|
-
DefaultModeStore
|
|
2968
|
+
DefaultMemoryStore,
|
|
2969
|
+
DefaultModeStore,
|
|
2792
2970
|
DefaultModelsRegistry,
|
|
2793
2971
|
DefaultSessionReader,
|
|
2794
|
-
DefaultSessionStore as
|
|
2795
|
-
DefaultSkillLoader
|
|
2796
|
-
DefaultSystemPromptBuilder as
|
|
2797
|
-
DefaultTokenCounter
|
|
2972
|
+
DefaultSessionStore as DefaultSessionStore2,
|
|
2973
|
+
DefaultSkillLoader,
|
|
2974
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
|
|
2975
|
+
DefaultTokenCounter,
|
|
2798
2976
|
AnnotationsStore,
|
|
2799
2977
|
CollaborationBus,
|
|
2800
2978
|
collabPauseMiddleware,
|
|
2801
2979
|
collabInjectMiddleware,
|
|
2802
2980
|
estimateRequestTokensCalibrated,
|
|
2803
2981
|
EventBus,
|
|
2804
|
-
createStrategyCompactor
|
|
2982
|
+
createStrategyCompactor,
|
|
2805
2983
|
ProviderRegistry,
|
|
2806
|
-
TOKENS
|
|
2984
|
+
TOKENS,
|
|
2807
2985
|
ToolRegistry,
|
|
2808
2986
|
atomicWrite as atomicWrite6,
|
|
2809
2987
|
createDefaultPipelines,
|
|
@@ -2812,6 +2990,7 @@ import {
|
|
|
2812
2990
|
DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
|
|
2813
2991
|
DEFAULT_SESSION_PRUNE_DAYS,
|
|
2814
2992
|
DEFAULT_TOOLS_CONFIG,
|
|
2993
|
+
applyToolDescriptionModes,
|
|
2815
2994
|
resolveContextWindowPolicy as resolveContextWindowPolicy2,
|
|
2816
2995
|
enhanceUserPrompt,
|
|
2817
2996
|
gatedEnhancerReasoning,
|
|
@@ -2821,109 +3000,10 @@ import {
|
|
|
2821
3000
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
2822
3001
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
2823
3002
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
2824
|
-
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
3003
|
+
import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
2825
3004
|
import { MCPRegistry } from "@wrongstack/mcp";
|
|
2826
3005
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
2827
|
-
|
|
2828
|
-
// ../runtime/src/container.ts
|
|
2829
|
-
import {
|
|
2830
|
-
Container,
|
|
2831
|
-
DefaultConfigStore,
|
|
2832
|
-
DefaultErrorHandler,
|
|
2833
|
-
DefaultMemoryStore,
|
|
2834
|
-
DefaultModeStore,
|
|
2835
|
-
DefaultPermissionPolicy,
|
|
2836
|
-
DefaultRetryPolicy,
|
|
2837
|
-
DefaultSecretScrubber,
|
|
2838
|
-
DefaultSessionStore,
|
|
2839
|
-
DefaultSkillLoader,
|
|
2840
|
-
DefaultSystemPromptBuilder,
|
|
2841
|
-
DefaultTokenCounter,
|
|
2842
|
-
createStrategyCompactor,
|
|
2843
|
-
buildRecoveryStrategies,
|
|
2844
|
-
TOKENS
|
|
2845
|
-
} from "@wrongstack/core";
|
|
2846
|
-
function createDefaultContainer(opts) {
|
|
2847
|
-
const { config, wpaths, logger, modelsRegistry } = opts;
|
|
2848
|
-
const container = new Container();
|
|
2849
|
-
const configStore = new DefaultConfigStore(config);
|
|
2850
|
-
container.bind(TOKENS.ConfigStore, () => configStore);
|
|
2851
|
-
container.bind(TOKENS.Logger, () => logger);
|
|
2852
|
-
container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
|
|
2853
|
-
container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
|
|
2854
|
-
container.bind(
|
|
2855
|
-
TOKENS.ErrorHandler,
|
|
2856
|
-
() => new DefaultErrorHandler(
|
|
2857
|
-
buildRecoveryStrategies({
|
|
2858
|
-
compactor: container.resolve(TOKENS.Compactor),
|
|
2859
|
-
modelsRegistry
|
|
2860
|
-
})
|
|
2861
|
-
)
|
|
2862
|
-
);
|
|
2863
|
-
container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
|
|
2864
|
-
container.bind(
|
|
2865
|
-
TOKENS.TokenCounter,
|
|
2866
|
-
() => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
|
|
2867
|
-
);
|
|
2868
|
-
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
2869
|
-
container.bind(TOKENS.ModeStore, () => modeStore);
|
|
2870
|
-
container.bind(
|
|
2871
|
-
TOKENS.SessionStore,
|
|
2872
|
-
() => new DefaultSessionStore({
|
|
2873
|
-
dir: wpaths.projectSessions,
|
|
2874
|
-
// Scrub secrets out of persisted user/model turns (F-06). Tool output
|
|
2875
|
-
// is already scrubbed by the executor.
|
|
2876
|
-
secretScrubber: container.resolve(TOKENS.SecretScrubber)
|
|
2877
|
-
})
|
|
2878
|
-
);
|
|
2879
|
-
const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
|
|
2880
|
-
container.bind(TOKENS.MemoryStore, () => memoryStore);
|
|
2881
|
-
const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
|
|
2882
|
-
container.bind(TOKENS.SkillLoader, () => skillLoader);
|
|
2883
|
-
if (opts.systemPrompt) {
|
|
2884
|
-
container.bind(
|
|
2885
|
-
TOKENS.SystemPromptBuilder,
|
|
2886
|
-
() => new DefaultSystemPromptBuilder(opts.systemPrompt)
|
|
2887
|
-
);
|
|
2888
|
-
}
|
|
2889
|
-
container.bind(
|
|
2890
|
-
TOKENS.PermissionPolicy,
|
|
2891
|
-
() => {
|
|
2892
|
-
const policyOptions = {
|
|
2893
|
-
trustFile: wpaths.projectTrust,
|
|
2894
|
-
yolo: opts.permission?.yolo ?? false,
|
|
2895
|
-
yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
|
|
2896
|
-
confirmDestructive: opts.permission?.confirmDestructive ?? false
|
|
2897
|
-
};
|
|
2898
|
-
if (opts.permission?.promptDelegate !== void 0) {
|
|
2899
|
-
policyOptions.promptDelegate = opts.permission.promptDelegate;
|
|
2900
|
-
}
|
|
2901
|
-
return new DefaultPermissionPolicy(policyOptions);
|
|
2902
|
-
}
|
|
2903
|
-
);
|
|
2904
|
-
container.bind(
|
|
2905
|
-
TOKENS.Compactor,
|
|
2906
|
-
() => (
|
|
2907
|
-
// Strategy comes from config.context.strategy: 'hybrid' (default, lossless
|
|
2908
|
-
// rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
|
|
2909
|
-
// (LLM-driven selection). The LLM strategies resolve their provider from
|
|
2910
|
-
// ctx at compact()-time, so binding here (before context.provider exists)
|
|
2911
|
-
// is safe. preserveK / eliseThreshold are class-level fallbacks; the active
|
|
2912
|
-
// ContextWindowPolicy in ctx.meta normally overrides both at runtime.
|
|
2913
|
-
// eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
|
|
2914
|
-
// essentially every tool_result (anything > 1 token).
|
|
2915
|
-
createStrategyCompactor({
|
|
2916
|
-
strategy: config.context?.strategy,
|
|
2917
|
-
preserveK: opts.compactor?.preserveK ?? 10,
|
|
2918
|
-
eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
|
|
2919
|
-
smart: true,
|
|
2920
|
-
summarizerModel: config.context?.summarizerModel,
|
|
2921
|
-
llmSelector: config.context?.llmSelector
|
|
2922
|
-
})
|
|
2923
|
-
)
|
|
2924
|
-
);
|
|
2925
|
-
return container;
|
|
2926
|
-
}
|
|
3006
|
+
import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
|
|
2927
3007
|
|
|
2928
3008
|
// src/server/boot.ts
|
|
2929
3009
|
import {
|
|
@@ -2943,6 +3023,7 @@ function patchConfig(config, updates) {
|
|
|
2943
3023
|
import { spawnSync } from "child_process";
|
|
2944
3024
|
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
2945
3025
|
import {
|
|
3026
|
+
assignNickname,
|
|
2946
3027
|
AutoPhasePlanner,
|
|
2947
3028
|
PhaseGraphBuilder,
|
|
2948
3029
|
PhaseOrchestrator,
|
|
@@ -2980,6 +3061,8 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
2980
3061
|
abort = null;
|
|
2981
3062
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
2982
3063
|
worktrees = null;
|
|
3064
|
+
/** Per-run worker identities so the board can show "who is on what". */
|
|
3065
|
+
usedNicknames = /* @__PURE__ */ new Set();
|
|
2983
3066
|
addClient(ws) {
|
|
2984
3067
|
const client = { ws, id: crypto.randomUUID() };
|
|
2985
3068
|
this.clients.add(client);
|
|
@@ -3022,6 +3105,29 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3022
3105
|
await this.handleTaskStatusChange(taskId, status);
|
|
3023
3106
|
break;
|
|
3024
3107
|
}
|
|
3108
|
+
case "autophase.moveTask": {
|
|
3109
|
+
const { taskId, toPhaseId } = msg.payload;
|
|
3110
|
+
if (this.orchestrator?.moveTask(taskId, toPhaseId)) this.afterBoardMutation();
|
|
3111
|
+
break;
|
|
3112
|
+
}
|
|
3113
|
+
case "autophase.assignTask": {
|
|
3114
|
+
const { taskId, agentId, agentName } = msg.payload;
|
|
3115
|
+
if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
|
|
3116
|
+
break;
|
|
3117
|
+
}
|
|
3118
|
+
case "autophase.addTask": {
|
|
3119
|
+
const { phaseId, title, description, type, priority } = msg.payload;
|
|
3120
|
+
if (title?.trim() && this.orchestrator?.addTask(phaseId, { title: title.trim(), description, type, priority })) {
|
|
3121
|
+
this.afterBoardMutation();
|
|
3122
|
+
}
|
|
3123
|
+
break;
|
|
3124
|
+
}
|
|
3125
|
+
case "autophase.retryTask":
|
|
3126
|
+
case "autophase.runTask": {
|
|
3127
|
+
const { taskId } = msg.payload;
|
|
3128
|
+
if (this.orchestrator?.requeueTask(taskId)) this.afterBoardMutation();
|
|
3129
|
+
break;
|
|
3130
|
+
}
|
|
3025
3131
|
case "autophase.toggleAutonomous": {
|
|
3026
3132
|
const autonomous = msg.payload?.autonomous ?? !this.graph?.autonomous;
|
|
3027
3133
|
if (this.graph) {
|
|
@@ -3149,6 +3255,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3149
3255
|
return this.defaultPhases();
|
|
3150
3256
|
}
|
|
3151
3257
|
async executeTaskWithAgent(task, phaseId, env) {
|
|
3258
|
+
if (!task.assignee) {
|
|
3259
|
+
const nick = assignNickname("executor", this.usedNicknames);
|
|
3260
|
+
this.usedNicknames.add(nick.key);
|
|
3261
|
+
task.assignee = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
|
|
3262
|
+
task.updatedAt = Date.now();
|
|
3263
|
+
this.broadcastState();
|
|
3264
|
+
}
|
|
3152
3265
|
const prompt = `Execute task: ${task.title}
|
|
3153
3266
|
|
|
3154
3267
|
Description: ${task.description}
|
|
@@ -3164,6 +3277,11 @@ Type: ${task.type}`;
|
|
|
3164
3277
|
this.context.cwd = prevCwd;
|
|
3165
3278
|
}
|
|
3166
3279
|
}
|
|
3280
|
+
/** Persist + broadcast after an interactive board mutation. */
|
|
3281
|
+
afterBoardMutation() {
|
|
3282
|
+
if (this.graph) void this.store.save(this.graph);
|
|
3283
|
+
this.broadcastState();
|
|
3284
|
+
}
|
|
3167
3285
|
async handleTaskStatusChange(taskId, status) {
|
|
3168
3286
|
if (!this.graph) return;
|
|
3169
3287
|
for (const phase of this.graph.phases.values()) {
|
|
@@ -3207,23 +3325,7 @@ Type: ${task.type}`;
|
|
|
3207
3325
|
(sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
|
|
3208
3326
|
0
|
|
3209
3327
|
);
|
|
3210
|
-
const
|
|
3211
|
-
id: p.id,
|
|
3212
|
-
name: p.name,
|
|
3213
|
-
description: p.description,
|
|
3214
|
-
status: p.status,
|
|
3215
|
-
priority: p.priority,
|
|
3216
|
-
estimateHours: p.estimateHours,
|
|
3217
|
-
actualDurationMs: p.actualDurationMs,
|
|
3218
|
-
startedAt: p.startedAt,
|
|
3219
|
-
completedAt: p.completedAt,
|
|
3220
|
-
progressPercent: p.taskGraph.nodes.size > 0 ? Math.round(Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length / p.taskGraph.nodes.size * 100) : 0,
|
|
3221
|
-
taskCount: p.taskGraph.nodes.size,
|
|
3222
|
-
completedTasks: Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
|
|
3223
|
-
assignedAgents: p.assignedAgents,
|
|
3224
|
-
isActive: p.id === currentActiveId
|
|
3225
|
-
}));
|
|
3226
|
-
const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
|
|
3328
|
+
const mapTask = (t) => ({
|
|
3227
3329
|
id: t.id,
|
|
3228
3330
|
title: t.title,
|
|
3229
3331
|
description: t.description,
|
|
@@ -3236,8 +3338,39 @@ Type: ${task.type}`;
|
|
|
3236
3338
|
tags: t.tags || [],
|
|
3237
3339
|
startedAt: t.startedAt,
|
|
3238
3340
|
completedAt: t.completedAt
|
|
3239
|
-
})
|
|
3341
|
+
});
|
|
3342
|
+
const phaseItems = phases.map((p) => {
|
|
3343
|
+
const nodes = Array.from(p.taskGraph.nodes.values());
|
|
3344
|
+
const done = nodes.filter((t) => t.status === "completed").length;
|
|
3345
|
+
return {
|
|
3346
|
+
id: p.id,
|
|
3347
|
+
name: p.name,
|
|
3348
|
+
description: p.description,
|
|
3349
|
+
status: p.status,
|
|
3350
|
+
priority: p.priority,
|
|
3351
|
+
estimateHours: p.estimateHours,
|
|
3352
|
+
actualDurationMs: p.actualDurationMs,
|
|
3353
|
+
startedAt: p.startedAt,
|
|
3354
|
+
completedAt: p.completedAt,
|
|
3355
|
+
progressPercent: nodes.length > 0 ? Math.round(done / nodes.length * 100) : 0,
|
|
3356
|
+
taskCount: nodes.length,
|
|
3357
|
+
completedTasks: done,
|
|
3358
|
+
assignedAgents: p.assignedAgents,
|
|
3359
|
+
isActive: p.id === currentActiveId,
|
|
3360
|
+
// Every phase carries its full task list so the board can render each
|
|
3361
|
+
// phase as a column (not just the selected one).
|
|
3362
|
+
tasks: nodes.map(mapTask)
|
|
3363
|
+
};
|
|
3364
|
+
});
|
|
3365
|
+
const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
|
|
3240
3366
|
const completedPhases = phases.filter((p) => p.status === "completed").length;
|
|
3367
|
+
const failedPhases = phases.filter((p) => p.status === "failed").length;
|
|
3368
|
+
const failedTasks = phases.reduce(
|
|
3369
|
+
(sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "failed").length,
|
|
3370
|
+
0
|
|
3371
|
+
);
|
|
3372
|
+
const lastFailed = phases.filter((p) => p.status === "failed").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
3373
|
+
const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
|
|
3241
3374
|
return {
|
|
3242
3375
|
title: this.graph.title,
|
|
3243
3376
|
phases: phaseItems,
|
|
@@ -3246,7 +3379,18 @@ Type: ${task.type}`;
|
|
|
3246
3379
|
overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
|
|
3247
3380
|
autonomous: this.graph.autonomous,
|
|
3248
3381
|
totalTasks,
|
|
3249
|
-
completedTasks
|
|
3382
|
+
completedTasks,
|
|
3383
|
+
// Structured progress + lastError consumed by the autophase store (were
|
|
3384
|
+
// defined client-side but never sent, so they stayed null on the board).
|
|
3385
|
+
progress: {
|
|
3386
|
+
totalPhases: phases.length,
|
|
3387
|
+
completed: completedPhases,
|
|
3388
|
+
failed: failedPhases,
|
|
3389
|
+
totalTasks,
|
|
3390
|
+
completedTasks,
|
|
3391
|
+
failedTasks
|
|
3392
|
+
},
|
|
3393
|
+
lastError
|
|
3250
3394
|
};
|
|
3251
3395
|
}
|
|
3252
3396
|
sendState(client) {
|
|
@@ -3269,6 +3413,510 @@ Type: ${task.type}`;
|
|
|
3269
3413
|
}
|
|
3270
3414
|
};
|
|
3271
3415
|
|
|
3416
|
+
// src/server/specs-ws-handler.ts
|
|
3417
|
+
import {
|
|
3418
|
+
computeTaskProgress,
|
|
3419
|
+
SpecStore,
|
|
3420
|
+
TaskGraphStore
|
|
3421
|
+
} from "@wrongstack/core";
|
|
3422
|
+
var SpecsWebSocketHandler = class {
|
|
3423
|
+
specStore;
|
|
3424
|
+
graphStore;
|
|
3425
|
+
clients = /* @__PURE__ */ new Set();
|
|
3426
|
+
constructor(specsDir, taskGraphsDir) {
|
|
3427
|
+
this.specStore = new SpecStore({ baseDir: specsDir });
|
|
3428
|
+
this.graphStore = new TaskGraphStore({ baseDir: taskGraphsDir });
|
|
3429
|
+
}
|
|
3430
|
+
addClient(ws) {
|
|
3431
|
+
const client = { ws, id: crypto.randomUUID() };
|
|
3432
|
+
this.clients.add(client);
|
|
3433
|
+
ws.on("close", () => this.clients.delete(client));
|
|
3434
|
+
ws.on("error", () => this.clients.delete(client));
|
|
3435
|
+
void this.sendList(client);
|
|
3436
|
+
}
|
|
3437
|
+
async handleMessage(msg) {
|
|
3438
|
+
switch (msg.type) {
|
|
3439
|
+
case "specs.list":
|
|
3440
|
+
await this.broadcastList();
|
|
3441
|
+
break;
|
|
3442
|
+
case "specs.get": {
|
|
3443
|
+
const specId = msg.payload?.specId;
|
|
3444
|
+
if (specId) await this.broadcastDetail(specId);
|
|
3445
|
+
break;
|
|
3446
|
+
}
|
|
3447
|
+
case "specs.taskStatus": {
|
|
3448
|
+
const { graphId, taskId, status } = msg.payload;
|
|
3449
|
+
await this.updateTaskStatus(graphId, taskId, status);
|
|
3450
|
+
break;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
// ── List ──────────────────────────────────────────────────────────────────
|
|
3455
|
+
async buildList() {
|
|
3456
|
+
const [specs, graphs] = await Promise.all([this.specStore.list(), this.graphStore.list()]);
|
|
3457
|
+
return specs.map((s, i) => {
|
|
3458
|
+
const graph = graphs.find((g) => g.specId === s.id);
|
|
3459
|
+
return {
|
|
3460
|
+
id: s.id,
|
|
3461
|
+
// FORGE-style display id (spec-001…). The real UUID stays in `id`.
|
|
3462
|
+
displayId: `spec-${String(i + 1).padStart(3, "0")}`,
|
|
3463
|
+
title: s.title,
|
|
3464
|
+
status: s.status,
|
|
3465
|
+
graphId: graph?.id,
|
|
3466
|
+
total: graph?.nodeCount ?? 0,
|
|
3467
|
+
completed: graph?.completedCount ?? 0
|
|
3468
|
+
};
|
|
3469
|
+
});
|
|
3470
|
+
}
|
|
3471
|
+
async broadcastList() {
|
|
3472
|
+
this.broadcast({ type: "specs.list", payload: { specs: await this.buildList() } });
|
|
3473
|
+
}
|
|
3474
|
+
async sendList(client) {
|
|
3475
|
+
this.send(client, { type: "specs.list", payload: { specs: await this.buildList() } });
|
|
3476
|
+
}
|
|
3477
|
+
// ── Detail (dependency board) ───────────────────────────────────────────────
|
|
3478
|
+
async broadcastDetail(specId) {
|
|
3479
|
+
const spec = await this.specStore.load(specId);
|
|
3480
|
+
const graph = await this.findGraphForSpec(specId);
|
|
3481
|
+
if (!spec || !graph) {
|
|
3482
|
+
this.broadcast({ type: "specs.detail", payload: { specId, columns: [], notFound: true } });
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
this.broadcast({ type: "specs.detail", payload: this.buildDetail(spec, graph) });
|
|
3486
|
+
}
|
|
3487
|
+
async findGraphForSpec(specId) {
|
|
3488
|
+
const entry = (await this.graphStore.list()).find((g) => g.specId === specId);
|
|
3489
|
+
if (!entry) return null;
|
|
3490
|
+
return this.graphStore.load(entry.id);
|
|
3491
|
+
}
|
|
3492
|
+
buildDetail(spec, graph) {
|
|
3493
|
+
const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
|
|
3494
|
+
const shortId = /* @__PURE__ */ new Map();
|
|
3495
|
+
nodes.forEach((n, i) => {
|
|
3496
|
+
shortId.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
|
|
3497
|
+
});
|
|
3498
|
+
const blockers = /* @__PURE__ */ new Map();
|
|
3499
|
+
for (const n of nodes) blockers.set(n.id, []);
|
|
3500
|
+
for (const e of graph.edges) {
|
|
3501
|
+
if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
|
|
3502
|
+
}
|
|
3503
|
+
const statusOf = (id) => graph.nodes.get(id)?.status;
|
|
3504
|
+
const depthCache = /* @__PURE__ */ new Map();
|
|
3505
|
+
const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
|
|
3506
|
+
const cached = depthCache.get(id);
|
|
3507
|
+
if (cached !== void 0) return cached;
|
|
3508
|
+
if (seen.has(id)) return 0;
|
|
3509
|
+
seen.add(id);
|
|
3510
|
+
const deps2 = blockers.get(id) ?? [];
|
|
3511
|
+
const d = deps2.length === 0 ? 0 : 1 + Math.max(...deps2.map((b) => depthOf(b, seen)));
|
|
3512
|
+
depthCache.set(id, d);
|
|
3513
|
+
return d;
|
|
3514
|
+
};
|
|
3515
|
+
const toBoardTask = (n) => {
|
|
3516
|
+
const deps2 = blockers.get(n.id) ?? [];
|
|
3517
|
+
const allDepsDone = deps2.every((b) => statusOf(b) === "completed");
|
|
3518
|
+
const displayStatus = n.status === "pending" && deps2.length > 0 && allDepsDone ? "queued" : n.status;
|
|
3519
|
+
return {
|
|
3520
|
+
id: n.id,
|
|
3521
|
+
shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
|
|
3522
|
+
title: n.title,
|
|
3523
|
+
description: n.description,
|
|
3524
|
+
priority: n.priority,
|
|
3525
|
+
type: n.type,
|
|
3526
|
+
status: n.status,
|
|
3527
|
+
displayStatus,
|
|
3528
|
+
deps: deps2.map((b) => shortId.get(b) ?? b.slice(0, 6))
|
|
3529
|
+
};
|
|
3530
|
+
};
|
|
3531
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
3532
|
+
for (const n of nodes) {
|
|
3533
|
+
const d = depthOf(n.id);
|
|
3534
|
+
if (!byDepth.has(d)) byDepth.set(d, []);
|
|
3535
|
+
byDepth.get(d)?.push(toBoardTask(n));
|
|
3536
|
+
}
|
|
3537
|
+
const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, tasks: byDepth.get(d) ?? [] }));
|
|
3538
|
+
const progress = computeTaskProgress(graph);
|
|
3539
|
+
return {
|
|
3540
|
+
specId: spec.id,
|
|
3541
|
+
graphId: graph.id,
|
|
3542
|
+
title: spec.title,
|
|
3543
|
+
overview: spec.overview,
|
|
3544
|
+
status: spec.status,
|
|
3545
|
+
total: progress.total,
|
|
3546
|
+
completed: progress.completed,
|
|
3547
|
+
running: progress.inProgress,
|
|
3548
|
+
pending: progress.pending,
|
|
3549
|
+
columns
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
async updateTaskStatus(graphId, taskId, status) {
|
|
3553
|
+
const graph = await this.graphStore.load(graphId);
|
|
3554
|
+
const node = graph?.nodes.get(taskId);
|
|
3555
|
+
if (!graph || !node) return;
|
|
3556
|
+
node.status = status;
|
|
3557
|
+
node.updatedAt = Date.now();
|
|
3558
|
+
graph.updatedAt = Date.now();
|
|
3559
|
+
await this.graphStore.save(graph);
|
|
3560
|
+
this.broadcastDetail(graph.specId).catch(() => {
|
|
3561
|
+
});
|
|
3562
|
+
await this.broadcastList();
|
|
3563
|
+
}
|
|
3564
|
+
// ── Transport ───────────────────────────────────────────────────────────────
|
|
3565
|
+
broadcast(msg) {
|
|
3566
|
+
const data = JSON.stringify(msg);
|
|
3567
|
+
for (const client of this.clients) {
|
|
3568
|
+
if (client.ws.readyState === 1) client.ws.send(data);
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
send(client, msg) {
|
|
3572
|
+
if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
|
|
3576
|
+
// src/server/sdd-board-ws-handler.ts
|
|
3577
|
+
import { SddBoardStore } from "@wrongstack/core";
|
|
3578
|
+
var CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
3579
|
+
"pause",
|
|
3580
|
+
"resume",
|
|
3581
|
+
"stop",
|
|
3582
|
+
"retry",
|
|
3583
|
+
"retry_all_failed",
|
|
3584
|
+
"reassign",
|
|
3585
|
+
// Per-task model / fallback / verification assignment + stop/delete (drained by start-sdd-run).
|
|
3586
|
+
"set_task_model",
|
|
3587
|
+
"set_task_fallbacks",
|
|
3588
|
+
"set_task_verification",
|
|
3589
|
+
"cancel_task",
|
|
3590
|
+
"delete_task",
|
|
3591
|
+
"split_task",
|
|
3592
|
+
// Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
|
|
3593
|
+
"cleanup_worktrees",
|
|
3594
|
+
"rollback"
|
|
3595
|
+
]);
|
|
3596
|
+
var SddBoardWebSocketHandler = class {
|
|
3597
|
+
store;
|
|
3598
|
+
clients = /* @__PURE__ */ new Set();
|
|
3599
|
+
latest = null;
|
|
3600
|
+
poll = null;
|
|
3601
|
+
unsub = null;
|
|
3602
|
+
constructor(boardsDir, events) {
|
|
3603
|
+
this.store = new SddBoardStore({ baseDir: boardsDir });
|
|
3604
|
+
if (events) {
|
|
3605
|
+
const handler = (e) => {
|
|
3606
|
+
this.latest = e.snapshot;
|
|
3607
|
+
this.broadcast({ type: "sdd.board.snapshot", payload: e.snapshot });
|
|
3608
|
+
};
|
|
3609
|
+
this.unsub = events.on("sdd.board.snapshot", handler);
|
|
3610
|
+
} else {
|
|
3611
|
+
this.poll = setInterval(() => void this.pollLatest(), 1e3);
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
addClient(ws) {
|
|
3615
|
+
const client = { ws, id: crypto.randomUUID() };
|
|
3616
|
+
this.clients.add(client);
|
|
3617
|
+
ws.on("close", () => this.clients.delete(client));
|
|
3618
|
+
ws.on("error", () => this.clients.delete(client));
|
|
3619
|
+
void this.sendCurrent(client);
|
|
3620
|
+
}
|
|
3621
|
+
async handleMessage(msg) {
|
|
3622
|
+
if (msg.type === "sdd.board.get") {
|
|
3623
|
+
await this.broadcastCurrent();
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
if (msg.type === "sdd.board.list") {
|
|
3627
|
+
const boards = await this.store.list();
|
|
3628
|
+
this.broadcast({ type: "sdd.board.list", payload: { boards } });
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
const action = msg.type.replace(/^sdd\.board\./, "");
|
|
3632
|
+
if (CONTROL_TYPES.has(action)) {
|
|
3633
|
+
const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
|
|
3634
|
+
if (runId) {
|
|
3635
|
+
await this.store.appendControl(runId, {
|
|
3636
|
+
ts: Date.now(),
|
|
3637
|
+
type: action,
|
|
3638
|
+
payload: msg.payload
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
dispose() {
|
|
3644
|
+
if (this.poll) clearInterval(this.poll);
|
|
3645
|
+
this.unsub?.();
|
|
3646
|
+
this.poll = null;
|
|
3647
|
+
this.unsub = null;
|
|
3648
|
+
}
|
|
3649
|
+
// ── internal ────────────────────────────────────────────────────────────
|
|
3650
|
+
async pollLatest() {
|
|
3651
|
+
const entry = (await this.store.list())[0];
|
|
3652
|
+
if (!entry) return;
|
|
3653
|
+
if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
const snap = await this.store.load(entry.runId);
|
|
3657
|
+
if (snap) {
|
|
3658
|
+
this.latest = snap;
|
|
3659
|
+
this.broadcast({ type: "sdd.board.snapshot", payload: snap });
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
async sendCurrent(client) {
|
|
3663
|
+
const snap = this.latest ?? await this.loadLatestFromDisk();
|
|
3664
|
+
if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
|
|
3665
|
+
}
|
|
3666
|
+
async broadcastCurrent() {
|
|
3667
|
+
const snap = this.latest ?? await this.loadLatestFromDisk();
|
|
3668
|
+
if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
|
|
3669
|
+
}
|
|
3670
|
+
async loadLatestFromDisk() {
|
|
3671
|
+
const entry = (await this.store.list())[0];
|
|
3672
|
+
return entry ? this.store.load(entry.runId) : null;
|
|
3673
|
+
}
|
|
3674
|
+
broadcast(msg) {
|
|
3675
|
+
const data = JSON.stringify(msg);
|
|
3676
|
+
for (const client of this.clients) {
|
|
3677
|
+
if (client.ws.readyState === 1) client.ws.send(data);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
send(client, msg) {
|
|
3681
|
+
if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
|
|
3682
|
+
}
|
|
3683
|
+
};
|
|
3684
|
+
|
|
3685
|
+
// src/server/sdd-wizard-ws-handler.ts
|
|
3686
|
+
var SddWizardWebSocketHandler = class {
|
|
3687
|
+
constructor(deps2) {
|
|
3688
|
+
this.deps = deps2;
|
|
3689
|
+
}
|
|
3690
|
+
deps;
|
|
3691
|
+
clients = /* @__PURE__ */ new Set();
|
|
3692
|
+
driver = null;
|
|
3693
|
+
/** The agent's most recent question — paired with the next user answer. */
|
|
3694
|
+
lastAgentText = "";
|
|
3695
|
+
/** Guards against overlapping interview turns (one in flight at a time). */
|
|
3696
|
+
busy = false;
|
|
3697
|
+
addClient(ws) {
|
|
3698
|
+
const client = { ws, id: crypto.randomUUID() };
|
|
3699
|
+
this.clients.add(client);
|
|
3700
|
+
ws.on("close", () => this.clients.delete(client));
|
|
3701
|
+
ws.on("error", () => this.clients.delete(client));
|
|
3702
|
+
if (this.driver) this.send(client, this.snapshotMsg());
|
|
3703
|
+
}
|
|
3704
|
+
async handleMessage(msg) {
|
|
3705
|
+
try {
|
|
3706
|
+
switch (msg.type) {
|
|
3707
|
+
case "sdd.spec.start":
|
|
3708
|
+
await this.onStart(String(msg.payload?.goal ?? "").trim());
|
|
3709
|
+
break;
|
|
3710
|
+
case "sdd.spec.message":
|
|
3711
|
+
await this.onMessage(String(msg.payload?.text ?? ""));
|
|
3712
|
+
break;
|
|
3713
|
+
case "sdd.spec.approve":
|
|
3714
|
+
await this.onApprove();
|
|
3715
|
+
break;
|
|
3716
|
+
case "sdd.spec.get":
|
|
3717
|
+
if (this.driver) this.broadcast(this.snapshotMsg());
|
|
3718
|
+
break;
|
|
3719
|
+
case "sdd.run.start":
|
|
3720
|
+
await this.onRunStart({
|
|
3721
|
+
parallelSlots: msg.payload?.parallelSlots,
|
|
3722
|
+
defaultModel: msg.payload?.model,
|
|
3723
|
+
defaultProvider: msg.payload?.provider,
|
|
3724
|
+
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
|
|
3725
|
+
});
|
|
3726
|
+
break;
|
|
3727
|
+
}
|
|
3728
|
+
} catch (err) {
|
|
3729
|
+
this.busy = false;
|
|
3730
|
+
this.broadcast({
|
|
3731
|
+
type: "sdd.spec.error",
|
|
3732
|
+
payload: { message: err instanceof Error ? err.message : String(err) }
|
|
3733
|
+
});
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
// ── message handlers ──────────────────────────────────────────────────────
|
|
3737
|
+
async onStart(goal) {
|
|
3738
|
+
if (!goal) {
|
|
3739
|
+
this.broadcast({ type: "sdd.spec.error", payload: { message: "A goal is required." } });
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
if (this.busy) return;
|
|
3743
|
+
this.driver = this.deps.makeDriver();
|
|
3744
|
+
const prompt = this.driver.start(goal);
|
|
3745
|
+
await this.runTurn(prompt);
|
|
3746
|
+
}
|
|
3747
|
+
async onMessage(text) {
|
|
3748
|
+
if (!this.driver || this.busy) return;
|
|
3749
|
+
if (this.driver.phase() === "questioning" && this.lastAgentText) {
|
|
3750
|
+
this.driver.submitAnswer(this.lastAgentText, text);
|
|
3751
|
+
} else {
|
|
3752
|
+
this.driver.submitAnswer(this.lastAgentText || "(feedback)", text);
|
|
3753
|
+
}
|
|
3754
|
+
await this.runTurn(this.driver.currentPrompt());
|
|
3755
|
+
}
|
|
3756
|
+
async onApprove() {
|
|
3757
|
+
if (!this.driver || this.busy) return;
|
|
3758
|
+
const { phase, prompt } = await this.driver.approve();
|
|
3759
|
+
if (phase === "executing") {
|
|
3760
|
+
this.broadcast(this.snapshotMsg());
|
|
3761
|
+
return;
|
|
3762
|
+
}
|
|
3763
|
+
await this.runTurn(prompt);
|
|
3764
|
+
}
|
|
3765
|
+
async onRunStart(opts) {
|
|
3766
|
+
if (!this.driver) {
|
|
3767
|
+
this.broadcast({ type: "sdd.spec.error", payload: { message: "No active spec session." } });
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
const graph = await this.driver.ensureTaskGraph();
|
|
3771
|
+
if (!graph) {
|
|
3772
|
+
this.broadcast({
|
|
3773
|
+
type: "sdd.spec.error",
|
|
3774
|
+
payload: { message: "No spec yet \u2014 finish the interview before starting a run." }
|
|
3775
|
+
});
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
const { runId } = await this.deps.startRun(this.driver, opts);
|
|
3779
|
+
this.broadcast({ type: "sdd.run.started", payload: { runId } });
|
|
3780
|
+
}
|
|
3781
|
+
// ── internals ───────────────────────────────────────────────────────────
|
|
3782
|
+
/** Run one interview turn against the isolated agent, then ingest + broadcast. */
|
|
3783
|
+
async runTurn(prompt) {
|
|
3784
|
+
this.busy = true;
|
|
3785
|
+
this.broadcast(this.snapshotMsg());
|
|
3786
|
+
try {
|
|
3787
|
+
const text = await this.deps.runInterviewTurn(prompt);
|
|
3788
|
+
this.lastAgentText = text;
|
|
3789
|
+
if (this.driver) await this.driver.ingestAgentOutput(text);
|
|
3790
|
+
this.broadcast({ type: "sdd.spec.agent_text", payload: { text } });
|
|
3791
|
+
} finally {
|
|
3792
|
+
this.busy = false;
|
|
3793
|
+
this.broadcast(this.snapshotMsg());
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
snapshotMsg() {
|
|
3797
|
+
const snap = this.driver?.snapshot();
|
|
3798
|
+
return {
|
|
3799
|
+
type: "sdd.spec.snapshot",
|
|
3800
|
+
payload: { ...snap, busy: this.busy }
|
|
3801
|
+
};
|
|
3802
|
+
}
|
|
3803
|
+
broadcast(msg) {
|
|
3804
|
+
const data = JSON.stringify(msg);
|
|
3805
|
+
for (const client of this.clients) {
|
|
3806
|
+
if (client.ws.readyState === 1) client.ws.send(data);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
send(client, msg) {
|
|
3810
|
+
if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
|
|
3811
|
+
}
|
|
3812
|
+
};
|
|
3813
|
+
|
|
3814
|
+
// src/server/sdd-wizard-wiring.ts
|
|
3815
|
+
import * as path7 from "path";
|
|
3816
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3817
|
+
import {
|
|
3818
|
+
makeCommandVerifier,
|
|
3819
|
+
makeLlmSubtaskGenerator,
|
|
3820
|
+
SddBoardStore as SddBoardStore2,
|
|
3821
|
+
SddInterviewDriver,
|
|
3822
|
+
SddRunRegistry,
|
|
3823
|
+
SddSupervisor,
|
|
3824
|
+
SpecStore as SpecStore2,
|
|
3825
|
+
startSddRun,
|
|
3826
|
+
TaskGraphStore as TaskGraphStore2,
|
|
3827
|
+
WorktreeManager as WorktreeManager2
|
|
3828
|
+
} from "@wrongstack/core";
|
|
3829
|
+
function buildSddWizardDeps(opts) {
|
|
3830
|
+
const registry = new SddRunRegistry();
|
|
3831
|
+
let isolatedSeq = 0;
|
|
3832
|
+
const runIsolatedTurn = async (prompt, name2) => {
|
|
3833
|
+
const result = await opts.subagentFactory({
|
|
3834
|
+
id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
3835
|
+
role: "executor",
|
|
3836
|
+
name: name2,
|
|
3837
|
+
disabledTools: ["delegate"],
|
|
3838
|
+
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
3839
|
+
});
|
|
3840
|
+
try {
|
|
3841
|
+
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
3842
|
+
return res.finalText ?? "";
|
|
3843
|
+
} finally {
|
|
3844
|
+
await result.dispose?.();
|
|
3845
|
+
}
|
|
3846
|
+
};
|
|
3847
|
+
return {
|
|
3848
|
+
makeDriver: () => new SddInterviewDriver({
|
|
3849
|
+
specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
|
|
3850
|
+
graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
|
|
3851
|
+
sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
|
|
3852
|
+
}),
|
|
3853
|
+
runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
|
|
3854
|
+
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
|
|
3855
|
+
const graph = driver.getGraph();
|
|
3856
|
+
const tracker = driver.getTracker();
|
|
3857
|
+
if (!graph || !tracker) {
|
|
3858
|
+
throw new Error("No task graph to run \u2014 finish the interview first.");
|
|
3859
|
+
}
|
|
3860
|
+
let worktrees;
|
|
3861
|
+
if (process.env["WRONGSTACK_SDD_WORKTREES"] !== "0") {
|
|
3862
|
+
const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3863
|
+
cwd: opts.projectRoot,
|
|
3864
|
+
encoding: "utf8",
|
|
3865
|
+
windowsHide: true
|
|
3866
|
+
}).stdout?.trim() === "true";
|
|
3867
|
+
if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
|
|
3868
|
+
}
|
|
3869
|
+
const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
|
|
3870
|
+
const verifyTask = makeCommandVerifier();
|
|
3871
|
+
const superviseFailure = opts.brain ? new SddSupervisor({
|
|
3872
|
+
brain: opts.brain,
|
|
3873
|
+
// The run-level fallback chain (chosen in the wizard) doubles as the
|
|
3874
|
+
// supervisor's reassign options — a `reassign` verdict rotates the
|
|
3875
|
+
// worker model on retry. Empty/undefined → reassign option dropped.
|
|
3876
|
+
reassignModels: fallbackModels,
|
|
3877
|
+
// LLM auto-split: decompose a retry-exhausted task into smaller
|
|
3878
|
+
// sub-tasks on an isolated read-only turn. Heavily validated +
|
|
3879
|
+
// bounded; an empty result degrades the split into a retry.
|
|
3880
|
+
generateSubtasks: makeLlmSubtaskGenerator({
|
|
3881
|
+
run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
|
|
3882
|
+
}),
|
|
3883
|
+
// The standalone brain is a tiered policy→LLM arbiter with NO
|
|
3884
|
+
// human-escalation wrapper (see index.ts), so it never blocks on a
|
|
3885
|
+
// human prompt — an unresolved verdict degrades to a bounded retry.
|
|
3886
|
+
// Safe to let the LLM layer actually pick reassign/split.
|
|
3887
|
+
requestLlmVerdict: true
|
|
3888
|
+
}).superviseFailure : void 0;
|
|
3889
|
+
const handle = startSddRun({
|
|
3890
|
+
tracker,
|
|
3891
|
+
graph,
|
|
3892
|
+
agent: opts.agent,
|
|
3893
|
+
projectRoot: opts.projectRoot,
|
|
3894
|
+
events: opts.events,
|
|
3895
|
+
subagentFactory: opts.subagentFactory,
|
|
3896
|
+
worktrees,
|
|
3897
|
+
boardStore,
|
|
3898
|
+
registry,
|
|
3899
|
+
parallelSlots,
|
|
3900
|
+
defaultModel,
|
|
3901
|
+
defaultProvider,
|
|
3902
|
+
fallbackModels,
|
|
3903
|
+
verifyTask,
|
|
3904
|
+
superviseFailure
|
|
3905
|
+
});
|
|
3906
|
+
void handle.completion.catch(() => {
|
|
3907
|
+
});
|
|
3908
|
+
return { runId: handle.runId };
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
// src/server/sdd-wizard-routes.ts
|
|
3914
|
+
async function handleSddWizardRoute(_ws, msg, handlers) {
|
|
3915
|
+
if (!(msg.type.startsWith("sdd.spec.") || msg.type.startsWith("sdd.run."))) return false;
|
|
3916
|
+
await handlers.handleMessage(msg);
|
|
3917
|
+
return true;
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3272
3920
|
// src/server/collaboration-ws-handler.ts
|
|
3273
3921
|
import { randomUUID } from "crypto";
|
|
3274
3922
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
@@ -3995,16 +4643,16 @@ var CollaborationWebSocketHandler = class {
|
|
|
3995
4643
|
};
|
|
3996
4644
|
|
|
3997
4645
|
// src/server/projects-manifest.ts
|
|
3998
|
-
import * as
|
|
3999
|
-
import * as
|
|
4646
|
+
import * as fs6 from "fs/promises";
|
|
4647
|
+
import * as path8 from "path";
|
|
4000
4648
|
import { projectSlug } from "@wrongstack/core";
|
|
4001
4649
|
function projectsJsonPath(globalConfigPath) {
|
|
4002
|
-
const base =
|
|
4003
|
-
return
|
|
4650
|
+
const base = path8.dirname(globalConfigPath);
|
|
4651
|
+
return path8.join(base, "projects.json");
|
|
4004
4652
|
}
|
|
4005
4653
|
async function loadManifest(globalConfigPath) {
|
|
4006
4654
|
try {
|
|
4007
|
-
const raw = await
|
|
4655
|
+
const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
|
|
4008
4656
|
const parsed = JSON.parse(raw);
|
|
4009
4657
|
return { projects: parsed.projects ?? [] };
|
|
4010
4658
|
} catch {
|
|
@@ -4013,16 +4661,16 @@ async function loadManifest(globalConfigPath) {
|
|
|
4013
4661
|
}
|
|
4014
4662
|
async function saveManifest(manifest, globalConfigPath) {
|
|
4015
4663
|
const file = projectsJsonPath(globalConfigPath);
|
|
4016
|
-
await
|
|
4017
|
-
await
|
|
4664
|
+
await fs6.mkdir(path8.dirname(file), { recursive: true });
|
|
4665
|
+
await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
4018
4666
|
}
|
|
4019
4667
|
function generateProjectSlug(rootPath) {
|
|
4020
4668
|
return projectSlug(rootPath);
|
|
4021
4669
|
}
|
|
4022
4670
|
async function ensureProjectDataDir(slug, globalConfigPath) {
|
|
4023
|
-
const base =
|
|
4024
|
-
const dir =
|
|
4025
|
-
await
|
|
4671
|
+
const base = path8.dirname(globalConfigPath);
|
|
4672
|
+
const dir = path8.join(base, "projects", slug);
|
|
4673
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
4026
4674
|
return dir;
|
|
4027
4675
|
}
|
|
4028
4676
|
|
|
@@ -4448,14 +5096,14 @@ function registerShutdownHandlers(res) {
|
|
|
4448
5096
|
|
|
4449
5097
|
// src/server/instance-registry.ts
|
|
4450
5098
|
import * as os from "os";
|
|
4451
|
-
import * as
|
|
4452
|
-
import * as
|
|
5099
|
+
import * as path9 from "path";
|
|
5100
|
+
import * as fs7 from "fs/promises";
|
|
4453
5101
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
4454
5102
|
function defaultBaseDir() {
|
|
4455
|
-
return
|
|
5103
|
+
return path9.join(os.homedir(), ".wrongstack");
|
|
4456
5104
|
}
|
|
4457
5105
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
4458
|
-
return
|
|
5106
|
+
return path9.join(baseDir, "webui-instances.json");
|
|
4459
5107
|
}
|
|
4460
5108
|
function isPidAlive(pid) {
|
|
4461
5109
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -4468,7 +5116,7 @@ function isPidAlive(pid) {
|
|
|
4468
5116
|
}
|
|
4469
5117
|
async function load(file) {
|
|
4470
5118
|
try {
|
|
4471
|
-
const raw = await
|
|
5119
|
+
const raw = await fs7.readFile(file, "utf8");
|
|
4472
5120
|
const parsed = JSON.parse(raw);
|
|
4473
5121
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
4474
5122
|
return parsed;
|
|
@@ -4577,9 +5225,10 @@ function openBrowser(url, platform = process.platform) {
|
|
|
4577
5225
|
if (child.pid) {
|
|
4578
5226
|
try {
|
|
4579
5227
|
import("@wrongstack/tools").then(({ getProcessRegistry }) => {
|
|
5228
|
+
const pid = child.pid;
|
|
5229
|
+
if (pid === void 0) return;
|
|
4580
5230
|
getProcessRegistry().register({
|
|
4581
|
-
|
|
4582
|
-
pid: child.pid,
|
|
5231
|
+
pid,
|
|
4583
5232
|
name: "browser",
|
|
4584
5233
|
command: `${command} ${args.join(" ")}`,
|
|
4585
5234
|
startedAt: Date.now(),
|
|
@@ -4587,7 +5236,7 @@ function openBrowser(url, platform = process.platform) {
|
|
|
4587
5236
|
protected: true
|
|
4588
5237
|
});
|
|
4589
5238
|
child.on("exit", () => {
|
|
4590
|
-
getProcessRegistry().unregister(
|
|
5239
|
+
getProcessRegistry().unregister(pid);
|
|
4591
5240
|
});
|
|
4592
5241
|
}).catch(() => {
|
|
4593
5242
|
});
|
|
@@ -4612,19 +5261,19 @@ function computeUsageCost(usage, rates) {
|
|
|
4612
5261
|
}
|
|
4613
5262
|
|
|
4614
5263
|
// src/server/provider-handlers.ts
|
|
4615
|
-
import { DefaultSecretScrubber
|
|
5264
|
+
import { DefaultSecretScrubber } from "@wrongstack/core";
|
|
4616
5265
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
4617
5266
|
|
|
4618
5267
|
// src/server/provider-config-io.ts
|
|
4619
|
-
import * as
|
|
4620
|
-
import * as
|
|
5268
|
+
import * as fs8 from "fs/promises";
|
|
5269
|
+
import * as path10 from "path";
|
|
4621
5270
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
4622
5271
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
4623
5272
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
4624
5273
|
async function loadSavedProviders(configPath, vault) {
|
|
4625
5274
|
let raw;
|
|
4626
5275
|
try {
|
|
4627
|
-
raw = await
|
|
5276
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
4628
5277
|
} catch {
|
|
4629
5278
|
return {};
|
|
4630
5279
|
}
|
|
@@ -4641,7 +5290,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
4641
5290
|
let raw;
|
|
4642
5291
|
let fileExists = true;
|
|
4643
5292
|
try {
|
|
4644
|
-
raw = await
|
|
5293
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
4645
5294
|
} catch (err) {
|
|
4646
5295
|
if (err.code !== "ENOENT") {
|
|
4647
5296
|
throw new Error(
|
|
@@ -4669,7 +5318,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
4669
5318
|
await atomicWrite4(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
|
|
4670
5319
|
}
|
|
4671
5320
|
function createProviderConfigIO(configPath) {
|
|
4672
|
-
const keyFile =
|
|
5321
|
+
const keyFile = path10.join(path10.dirname(configPath), ".key");
|
|
4673
5322
|
const vault = new DefaultSecretVault({ keyFile });
|
|
4674
5323
|
return {
|
|
4675
5324
|
load: () => loadSavedProviders(configPath, vault),
|
|
@@ -4798,7 +5447,7 @@ function projectSavedProviders(providers) {
|
|
|
4798
5447
|
return view;
|
|
4799
5448
|
});
|
|
4800
5449
|
}
|
|
4801
|
-
var probeScrubber = new
|
|
5450
|
+
var probeScrubber = new DefaultSecretScrubber();
|
|
4802
5451
|
function createProviderHandlers(deps2) {
|
|
4803
5452
|
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
|
|
4804
5453
|
let configWriteLock = deps2.getConfigWriteLock();
|
|
@@ -4823,7 +5472,10 @@ function createProviderHandlers(deps2) {
|
|
|
4823
5472
|
try {
|
|
4824
5473
|
const providers = await loadConfigProviders();
|
|
4825
5474
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
4826
|
-
if (result.ok)
|
|
5475
|
+
if (result.ok) {
|
|
5476
|
+
await saveConfigProviders(providers);
|
|
5477
|
+
broadcastSaved(providers);
|
|
5478
|
+
}
|
|
4827
5479
|
sendResult2(ws, result.ok, result.message);
|
|
4828
5480
|
} catch (err) {
|
|
4829
5481
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4833,7 +5485,10 @@ function createProviderHandlers(deps2) {
|
|
|
4833
5485
|
try {
|
|
4834
5486
|
const providers = await loadConfigProviders();
|
|
4835
5487
|
const result = deleteKey(providers, providerId, label);
|
|
4836
|
-
if (result.ok)
|
|
5488
|
+
if (result.ok) {
|
|
5489
|
+
await saveConfigProviders(providers);
|
|
5490
|
+
broadcastSaved(providers);
|
|
5491
|
+
}
|
|
4837
5492
|
sendResult2(ws, result.ok, result.message);
|
|
4838
5493
|
} catch (err) {
|
|
4839
5494
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4843,7 +5498,10 @@ function createProviderHandlers(deps2) {
|
|
|
4843
5498
|
try {
|
|
4844
5499
|
const providers = await loadConfigProviders();
|
|
4845
5500
|
const result = setActiveKey(providers, providerId, label);
|
|
4846
|
-
if (result.ok)
|
|
5501
|
+
if (result.ok) {
|
|
5502
|
+
await saveConfigProviders(providers);
|
|
5503
|
+
broadcastSaved(providers);
|
|
5504
|
+
}
|
|
4847
5505
|
sendResult2(ws, result.ok, result.message);
|
|
4848
5506
|
} catch (err) {
|
|
4849
5507
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4853,11 +5511,13 @@ function createProviderHandlers(deps2) {
|
|
|
4853
5511
|
try {
|
|
4854
5512
|
const providers = await loadConfigProviders();
|
|
4855
5513
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
4856
|
-
if (result.ok)
|
|
5514
|
+
if (result.ok) {
|
|
5515
|
+
await saveConfigProviders(providers);
|
|
5516
|
+
broadcastSaved(providers);
|
|
5517
|
+
}
|
|
4857
5518
|
sendResult2(ws, result.ok, result.message);
|
|
4858
5519
|
if (result.ok) {
|
|
4859
5520
|
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
4860
|
-
broadcastSaved(providers);
|
|
4861
5521
|
}
|
|
4862
5522
|
} catch (err) {
|
|
4863
5523
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4867,7 +5527,10 @@ function createProviderHandlers(deps2) {
|
|
|
4867
5527
|
try {
|
|
4868
5528
|
const providers = await loadConfigProviders();
|
|
4869
5529
|
const result = removeProvider(providers, providerId);
|
|
4870
|
-
if (result.ok)
|
|
5530
|
+
if (result.ok) {
|
|
5531
|
+
await saveConfigProviders(providers);
|
|
5532
|
+
broadcastSaved(providers);
|
|
5533
|
+
}
|
|
4871
5534
|
sendResult2(ws, result.ok, result.message);
|
|
4872
5535
|
} catch (err) {
|
|
4873
5536
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4973,7 +5636,7 @@ function createProviderHandlers(deps2) {
|
|
|
4973
5636
|
|
|
4974
5637
|
// src/server/mode-handlers.ts
|
|
4975
5638
|
import {
|
|
4976
|
-
DefaultSystemPromptBuilder
|
|
5639
|
+
DefaultSystemPromptBuilder
|
|
4977
5640
|
} from "@wrongstack/core";
|
|
4978
5641
|
function createModeHandlers(ctx) {
|
|
4979
5642
|
return {
|
|
@@ -5021,7 +5684,7 @@ function createModeHandlers(ctx) {
|
|
|
5021
5684
|
}
|
|
5022
5685
|
ctx.setModeId(id);
|
|
5023
5686
|
const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
|
|
5024
|
-
const freshBuilder = new
|
|
5687
|
+
const freshBuilder = new DefaultSystemPromptBuilder({
|
|
5025
5688
|
memoryStore: ctx.memoryStore,
|
|
5026
5689
|
skillLoader: ctx.skillLoader,
|
|
5027
5690
|
modeStore: ctx.modeStore,
|
|
@@ -5050,42 +5713,12 @@ function createModeHandlers(ctx) {
|
|
|
5050
5713
|
|
|
5051
5714
|
// src/server/project-handlers.ts
|
|
5052
5715
|
import * as fs9 from "fs/promises";
|
|
5053
|
-
import * as
|
|
5716
|
+
import * as path11 from "path";
|
|
5054
5717
|
import {
|
|
5055
|
-
DefaultSessionStore
|
|
5056
|
-
DefaultSystemPromptBuilder as
|
|
5718
|
+
DefaultSessionStore,
|
|
5719
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
|
|
5057
5720
|
getSessionRegistry
|
|
5058
5721
|
} from "@wrongstack/core";
|
|
5059
|
-
|
|
5060
|
-
// src/server/path-containment.ts
|
|
5061
|
-
import * as fs8 from "fs/promises";
|
|
5062
|
-
import * as path9 from "path";
|
|
5063
|
-
function isPathInside(root, target) {
|
|
5064
|
-
const relative3 = path9.relative(root, target);
|
|
5065
|
-
return relative3 === "" || !relative3.startsWith("..") && !path9.isAbsolute(relative3);
|
|
5066
|
-
}
|
|
5067
|
-
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
5068
|
-
const resolved = path9.resolve(projectRoot, inputPath);
|
|
5069
|
-
let stat3;
|
|
5070
|
-
try {
|
|
5071
|
-
stat3 = await fs8.stat(resolved);
|
|
5072
|
-
} catch {
|
|
5073
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5074
|
-
}
|
|
5075
|
-
if (!stat3.isDirectory()) {
|
|
5076
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5077
|
-
}
|
|
5078
|
-
const [realProjectRoot, realResolved] = await Promise.all([
|
|
5079
|
-
fs8.realpath(projectRoot),
|
|
5080
|
-
fs8.realpath(resolved)
|
|
5081
|
-
]);
|
|
5082
|
-
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
5083
|
-
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
5084
|
-
}
|
|
5085
|
-
return resolved;
|
|
5086
|
-
}
|
|
5087
|
-
|
|
5088
|
-
// src/server/project-handlers.ts
|
|
5089
5722
|
function createProjectHandlers(ctx) {
|
|
5090
5723
|
return {
|
|
5091
5724
|
listProjects: async (ws) => {
|
|
@@ -5107,7 +5740,7 @@ function createProjectHandlers(ctx) {
|
|
|
5107
5740
|
}
|
|
5108
5741
|
const { root: addRoot, name: displayName } = parsed.value;
|
|
5109
5742
|
try {
|
|
5110
|
-
const resolved =
|
|
5743
|
+
const resolved = path11.resolve(addRoot);
|
|
5111
5744
|
await fs9.access(resolved);
|
|
5112
5745
|
const stat3 = await fs9.stat(resolved);
|
|
5113
5746
|
if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
@@ -5125,7 +5758,7 @@ function createProjectHandlers(ctx) {
|
|
|
5125
5758
|
});
|
|
5126
5759
|
return;
|
|
5127
5760
|
}
|
|
5128
|
-
const name2 = displayName?.trim() ||
|
|
5761
|
+
const name2 = displayName?.trim() || path11.basename(resolved);
|
|
5129
5762
|
const slug = generateProjectSlug(resolved);
|
|
5130
5763
|
await ensureProjectDataDir(slug, ctx.globalConfigPath);
|
|
5131
5764
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5138,7 +5771,7 @@ function createProjectHandlers(ctx) {
|
|
|
5138
5771
|
} catch (err) {
|
|
5139
5772
|
send(ws, {
|
|
5140
5773
|
type: "projects.added",
|
|
5141
|
-
payload: { name:
|
|
5774
|
+
payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
|
|
5142
5775
|
});
|
|
5143
5776
|
}
|
|
5144
5777
|
},
|
|
@@ -5153,7 +5786,7 @@ function createProjectHandlers(ctx) {
|
|
|
5153
5786
|
}
|
|
5154
5787
|
const { root: selRoot, name: selName } = parsed.value;
|
|
5155
5788
|
try {
|
|
5156
|
-
const resolved =
|
|
5789
|
+
const resolved = path11.resolve(selRoot);
|
|
5157
5790
|
try {
|
|
5158
5791
|
await fs9.access(resolved);
|
|
5159
5792
|
const stat3 = await fs9.stat(resolved);
|
|
@@ -5163,7 +5796,7 @@ function createProjectHandlers(ctx) {
|
|
|
5163
5796
|
type: "projects.selected",
|
|
5164
5797
|
payload: {
|
|
5165
5798
|
root: selRoot,
|
|
5166
|
-
name: selName ||
|
|
5799
|
+
name: selName || path11.basename(selRoot),
|
|
5167
5800
|
message: `Cannot switch: ${errMessage(err)}`
|
|
5168
5801
|
}
|
|
5169
5802
|
});
|
|
@@ -5175,7 +5808,7 @@ function createProjectHandlers(ctx) {
|
|
|
5175
5808
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
5176
5809
|
entry.lastWorkingDir = resolved;
|
|
5177
5810
|
} else {
|
|
5178
|
-
const name2 = selName?.trim() ||
|
|
5811
|
+
const name2 = selName?.trim() || path11.basename(resolved);
|
|
5179
5812
|
const slug = generateProjectSlug(resolved);
|
|
5180
5813
|
manifest.projects.push({
|
|
5181
5814
|
name: name2,
|
|
@@ -5197,7 +5830,7 @@ function createProjectHandlers(ctx) {
|
|
|
5197
5830
|
try {
|
|
5198
5831
|
const modeId = ctx.getModeId();
|
|
5199
5832
|
const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
|
|
5200
|
-
const switchBuilder = new
|
|
5833
|
+
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
5201
5834
|
memoryStore: ctx.memoryStore,
|
|
5202
5835
|
skillLoader: ctx.skillLoader,
|
|
5203
5836
|
modeStore: ctx.modeStore,
|
|
@@ -5214,14 +5847,14 @@ function createProjectHandlers(ctx) {
|
|
|
5214
5847
|
});
|
|
5215
5848
|
} catch {
|
|
5216
5849
|
}
|
|
5217
|
-
const newSessionsDir =
|
|
5218
|
-
|
|
5850
|
+
const newSessionsDir = path11.join(
|
|
5851
|
+
path11.dirname(ctx.globalConfigPath),
|
|
5219
5852
|
"projects",
|
|
5220
5853
|
switchSlug,
|
|
5221
5854
|
"sessions"
|
|
5222
5855
|
);
|
|
5223
5856
|
await fs9.mkdir(newSessionsDir, { recursive: true });
|
|
5224
|
-
const newSessionStore = new
|
|
5857
|
+
const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
|
|
5225
5858
|
const oldSession = ctx.getSession();
|
|
5226
5859
|
const oldSessionId = oldSession.id;
|
|
5227
5860
|
try {
|
|
@@ -5254,7 +5887,7 @@ function createProjectHandlers(ctx) {
|
|
|
5254
5887
|
sessionId: newSession.id,
|
|
5255
5888
|
projectSlug: switchSlug,
|
|
5256
5889
|
projectRoot: resolved,
|
|
5257
|
-
projectName:
|
|
5890
|
+
projectName: path11.basename(resolved),
|
|
5258
5891
|
workingDir: resolved,
|
|
5259
5892
|
clientType: "webui",
|
|
5260
5893
|
pid: process.pid,
|
|
@@ -5266,8 +5899,8 @@ function createProjectHandlers(ctx) {
|
|
|
5266
5899
|
type: "projects.selected",
|
|
5267
5900
|
payload: {
|
|
5268
5901
|
root: resolved,
|
|
5269
|
-
name: selName ||
|
|
5270
|
-
message: `Switched to ${selName ||
|
|
5902
|
+
name: selName || path11.basename(resolved),
|
|
5903
|
+
message: `Switched to ${selName || path11.basename(resolved)}`
|
|
5271
5904
|
}
|
|
5272
5905
|
});
|
|
5273
5906
|
broadcast(ctx.clients, {
|
|
@@ -5287,7 +5920,7 @@ function createProjectHandlers(ctx) {
|
|
|
5287
5920
|
type: "projects.selected",
|
|
5288
5921
|
payload: {
|
|
5289
5922
|
root: selRoot,
|
|
5290
|
-
name: selName ||
|
|
5923
|
+
name: selName || path11.basename(selRoot),
|
|
5291
5924
|
message: errMessage(err)
|
|
5292
5925
|
}
|
|
5293
5926
|
});
|
|
@@ -5318,7 +5951,7 @@ function createProjectHandlers(ctx) {
|
|
|
5318
5951
|
}
|
|
5319
5952
|
|
|
5320
5953
|
// src/server/session-handlers.ts
|
|
5321
|
-
import * as
|
|
5954
|
+
import * as path12 from "path";
|
|
5322
5955
|
import {
|
|
5323
5956
|
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
5324
5957
|
repairToolUseAdjacency,
|
|
@@ -5660,7 +6293,7 @@ function createSessionHandlers(ctx) {
|
|
|
5660
6293
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
5661
6294
|
const projectRoot = ctx.getProjectRoot();
|
|
5662
6295
|
const rewinder = new DefaultSessionRewinder(
|
|
5663
|
-
|
|
6296
|
+
path12.join(projectRoot, ".wrongstack", "sessions"),
|
|
5664
6297
|
projectRoot
|
|
5665
6298
|
);
|
|
5666
6299
|
const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
|
|
@@ -5675,7 +6308,7 @@ function createSessionHandlers(ctx) {
|
|
|
5675
6308
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
5676
6309
|
const projectRoot = ctx.getProjectRoot();
|
|
5677
6310
|
const rewinder = new DefaultSessionRewinder(
|
|
5678
|
-
|
|
6311
|
+
path12.join(projectRoot, ".wrongstack", "sessions"),
|
|
5679
6312
|
projectRoot
|
|
5680
6313
|
);
|
|
5681
6314
|
await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
|
|
@@ -5918,6 +6551,22 @@ async function handleModeRoute(ws, msg, handlers) {
|
|
|
5918
6551
|
}
|
|
5919
6552
|
}
|
|
5920
6553
|
|
|
6554
|
+
// src/server/prefs-routes.ts
|
|
6555
|
+
async function handlePrefsRoute(ws, msg, handlers) {
|
|
6556
|
+
switch (msg.type) {
|
|
6557
|
+
case "prefs.get": {
|
|
6558
|
+
await handlers.getPrefs(ws);
|
|
6559
|
+
return true;
|
|
6560
|
+
}
|
|
6561
|
+
case "prefs.update": {
|
|
6562
|
+
await handlers.updatePrefs(ws, msg.payload ?? {});
|
|
6563
|
+
return true;
|
|
6564
|
+
}
|
|
6565
|
+
default:
|
|
6566
|
+
return false;
|
|
6567
|
+
}
|
|
6568
|
+
}
|
|
6569
|
+
|
|
5921
6570
|
// src/server/shell-git-routes.ts
|
|
5922
6571
|
async function handleShellGitRoute(ws, msg, handlers) {
|
|
5923
6572
|
switch (msg.type) {
|
|
@@ -5958,6 +6607,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
|
|
|
5958
6607
|
}
|
|
5959
6608
|
}
|
|
5960
6609
|
|
|
6610
|
+
// src/server/mcp-routes.ts
|
|
6611
|
+
async function handleMcpRoute(ws, msg, handlers) {
|
|
6612
|
+
switch (msg.type) {
|
|
6613
|
+
case "mcp.list":
|
|
6614
|
+
await handlers.list(ws, msg);
|
|
6615
|
+
return true;
|
|
6616
|
+
case "mcp.add":
|
|
6617
|
+
await handlers.add(ws, msg);
|
|
6618
|
+
return true;
|
|
6619
|
+
case "mcp.update":
|
|
6620
|
+
await handlers.update(ws, msg);
|
|
6621
|
+
return true;
|
|
6622
|
+
case "mcp.remove":
|
|
6623
|
+
await handlers.remove(ws, msg);
|
|
6624
|
+
return true;
|
|
6625
|
+
case "mcp.enable":
|
|
6626
|
+
await handlers.enable(ws, msg);
|
|
6627
|
+
return true;
|
|
6628
|
+
case "mcp.disable":
|
|
6629
|
+
await handlers.disable(ws, msg);
|
|
6630
|
+
return true;
|
|
6631
|
+
case "mcp.sleep":
|
|
6632
|
+
await handlers.sleep(ws, msg);
|
|
6633
|
+
return true;
|
|
6634
|
+
case "mcp.wake":
|
|
6635
|
+
await handlers.wake(ws, msg);
|
|
6636
|
+
return true;
|
|
6637
|
+
case "mcp.restart":
|
|
6638
|
+
await handlers.restart(ws, msg);
|
|
6639
|
+
return true;
|
|
6640
|
+
case "mcp.discover":
|
|
6641
|
+
await handlers.discover(ws, msg);
|
|
6642
|
+
return true;
|
|
6643
|
+
default:
|
|
6644
|
+
return false;
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
|
|
5961
6648
|
// src/server/brain-routes.ts
|
|
5962
6649
|
async function handleBrainRoute(ws, msg, handlers) {
|
|
5963
6650
|
switch (msg.type) {
|
|
@@ -5982,10 +6669,24 @@ async function handleAutoPhaseRoute(_ws, msg, handlers) {
|
|
|
5982
6669
|
return true;
|
|
5983
6670
|
}
|
|
5984
6671
|
|
|
6672
|
+
// src/server/specs-routes.ts
|
|
6673
|
+
async function handleSpecsRoute(_ws, msg, handlers) {
|
|
6674
|
+
if (!msg.type.startsWith("specs.")) return false;
|
|
6675
|
+
await handlers.handleMessage(msg);
|
|
6676
|
+
return true;
|
|
6677
|
+
}
|
|
6678
|
+
|
|
6679
|
+
// src/server/sdd-board-routes.ts
|
|
6680
|
+
async function handleSddBoardRoute(_ws, msg, handlers) {
|
|
6681
|
+
if (!msg.type.startsWith("sdd.board.")) return false;
|
|
6682
|
+
await handlers.handleMessage(msg);
|
|
6683
|
+
return true;
|
|
6684
|
+
}
|
|
6685
|
+
|
|
5985
6686
|
// src/server/setup-events.ts
|
|
5986
6687
|
import * as fs10 from "fs/promises";
|
|
5987
6688
|
import { watch as fsWatch } from "fs";
|
|
5988
|
-
import * as
|
|
6689
|
+
import * as path13 from "path";
|
|
5989
6690
|
function setupEvents(deps2) {
|
|
5990
6691
|
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
|
|
5991
6692
|
const disposers = [];
|
|
@@ -6415,11 +7116,13 @@ function setupEvents(deps2) {
|
|
|
6415
7116
|
events.on("provider.response", (e) => {
|
|
6416
7117
|
if (e.usage?.input != null) {
|
|
6417
7118
|
const maxCtx = context.provider.capabilities.maxContext;
|
|
6418
|
-
const
|
|
7119
|
+
const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
|
|
7120
|
+
const load2 = Math.max(0, Math.min(1, rawLoad));
|
|
6419
7121
|
const costUsd = context.tokenCounter.estimateCost().total;
|
|
6420
7122
|
forwardSubagent("ctx_pct", {
|
|
6421
7123
|
subagentId: "leader",
|
|
6422
|
-
load:
|
|
7124
|
+
load: load2,
|
|
7125
|
+
rawLoad,
|
|
6423
7126
|
tokens: e.usage.input,
|
|
6424
7127
|
maxContext: maxCtx,
|
|
6425
7128
|
costUsd
|
|
@@ -6450,7 +7153,7 @@ function setupEvents(deps2) {
|
|
|
6450
7153
|
if (wpaths?.projectStatus) {
|
|
6451
7154
|
try {
|
|
6452
7155
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
6453
|
-
const dir =
|
|
7156
|
+
const dir = path13.dirname(statusFile);
|
|
6454
7157
|
await fs10.mkdir(dir, { recursive: true });
|
|
6455
7158
|
await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
6456
7159
|
} catch (err) {
|
|
@@ -6459,7 +7162,7 @@ function setupEvents(deps2) {
|
|
|
6459
7162
|
}
|
|
6460
7163
|
});
|
|
6461
7164
|
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
6462
|
-
const projectsDir =
|
|
7165
|
+
const projectsDir = path13.join(wpaths.configDir, "projects");
|
|
6463
7166
|
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
6464
7167
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
6465
7168
|
const DEBOUNCE_MS = 150;
|
|
@@ -6486,7 +7189,7 @@ function setupEvents(deps2) {
|
|
|
6486
7189
|
);
|
|
6487
7190
|
};
|
|
6488
7191
|
const metricsInterval = setInterval(logWatcherMetrics, 6e4);
|
|
6489
|
-
const broadcastStatus = (
|
|
7192
|
+
const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
|
|
6490
7193
|
broadcast2(clients, { type: "client.status_update", payload: statusData });
|
|
6491
7194
|
if (watcherMetrics) {
|
|
6492
7195
|
watcherMetrics.broadcastsSent++;
|
|
@@ -6527,9 +7230,9 @@ function setupEvents(deps2) {
|
|
|
6527
7230
|
if (eventType === "change") {
|
|
6528
7231
|
if (filename == null) return;
|
|
6529
7232
|
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
6530
|
-
const targetFile =
|
|
7233
|
+
const targetFile = path13.join(projectsDir, String(filename));
|
|
6531
7234
|
if (targetFile.endsWith("status.json")) {
|
|
6532
|
-
const projectHash2 =
|
|
7235
|
+
const projectHash2 = path13.basename(path13.dirname(targetFile));
|
|
6533
7236
|
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
6534
7237
|
return;
|
|
6535
7238
|
}
|
|
@@ -6587,7 +7290,7 @@ function setupEvents(deps2) {
|
|
|
6587
7290
|
}
|
|
6588
7291
|
});
|
|
6589
7292
|
}
|
|
6590
|
-
const globalRoot = globalConfigPath ?
|
|
7293
|
+
const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
|
|
6591
7294
|
if (globalRoot) {
|
|
6592
7295
|
const broadcastSessions = async () => {
|
|
6593
7296
|
try {
|
|
@@ -6661,10 +7364,10 @@ function setupEvents(deps2) {
|
|
|
6661
7364
|
// src/server/custom-context-modes.ts
|
|
6662
7365
|
import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
|
|
6663
7366
|
import * as fs11 from "fs/promises";
|
|
6664
|
-
import * as
|
|
7367
|
+
import * as path14 from "path";
|
|
6665
7368
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
6666
7369
|
function storePath(wrongstackDir) {
|
|
6667
|
-
return
|
|
7370
|
+
return path14.join(wrongstackDir, STORE_FILENAME);
|
|
6668
7371
|
}
|
|
6669
7372
|
var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
|
|
6670
7373
|
function createCustomModeStore(wrongstackDir) {
|
|
@@ -6796,12 +7499,12 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
6796
7499
|
|
|
6797
7500
|
// src/server/shell-open.ts
|
|
6798
7501
|
import * as fs12 from "fs/promises";
|
|
6799
|
-
import * as
|
|
7502
|
+
import * as path15 from "path";
|
|
6800
7503
|
import { spawn as spawn2 } from "child_process";
|
|
6801
7504
|
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
6802
7505
|
async function handleShellOpen(req, logger) {
|
|
6803
7506
|
try {
|
|
6804
|
-
const resolved =
|
|
7507
|
+
const resolved = path15.resolve(req.path);
|
|
6805
7508
|
await fs12.access(resolved);
|
|
6806
7509
|
if (METACHAR_REGEX.test(resolved)) {
|
|
6807
7510
|
return { success: false, message: "Path contains unsupported characters." };
|
|
@@ -6911,15 +7614,15 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6911
7614
|
if (!m) continue;
|
|
6912
7615
|
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
6913
7616
|
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
6914
|
-
let
|
|
6915
|
-
if (
|
|
7617
|
+
let path17 = m[3] ?? "";
|
|
7618
|
+
if (path17 === "") {
|
|
6916
7619
|
i += 1;
|
|
6917
|
-
|
|
7620
|
+
path17 = parts[i + 1] ?? parts[i] ?? "";
|
|
6918
7621
|
i += 1;
|
|
6919
7622
|
}
|
|
6920
|
-
if (!
|
|
6921
|
-
const prev = counts.get(
|
|
6922
|
-
counts.set(
|
|
7623
|
+
if (!path17) continue;
|
|
7624
|
+
const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
|
|
7625
|
+
counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
6923
7626
|
}
|
|
6924
7627
|
};
|
|
6925
7628
|
parseNumstat(unstagedNumstat);
|
|
@@ -6931,7 +7634,7 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6931
7634
|
if (!rec || rec.length < 3) continue;
|
|
6932
7635
|
const x = rec[0] ?? " ";
|
|
6933
7636
|
const y = rec[1] ?? " ";
|
|
6934
|
-
const
|
|
7637
|
+
const path17 = rec.slice(3);
|
|
6935
7638
|
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
6936
7639
|
if (isRename) i += 1;
|
|
6937
7640
|
let status;
|
|
@@ -6943,13 +7646,13 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6943
7646
|
else if (x === "D" || y === "D") status = "D";
|
|
6944
7647
|
else status = "M";
|
|
6945
7648
|
const staged = x !== " " && x !== "?";
|
|
6946
|
-
let added = counts.get(
|
|
6947
|
-
let deleted = counts.get(
|
|
7649
|
+
let added = counts.get(path17)?.added ?? 0;
|
|
7650
|
+
let deleted = counts.get(path17)?.deleted ?? 0;
|
|
6948
7651
|
if (status === "?") {
|
|
6949
7652
|
added = 0;
|
|
6950
7653
|
deleted = 0;
|
|
6951
7654
|
}
|
|
6952
|
-
files.push({ path:
|
|
7655
|
+
files.push({ path: path17, status, added, deleted, staged });
|
|
6953
7656
|
}
|
|
6954
7657
|
send(ws, { type: "git.changes", payload: { files } });
|
|
6955
7658
|
} catch (err) {
|
|
@@ -6960,21 +7663,21 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6960
7663
|
}
|
|
6961
7664
|
}
|
|
6962
7665
|
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
6963
|
-
async function handleGitDiff(ws, projectRoot,
|
|
7666
|
+
async function handleGitDiff(ws, projectRoot, path17) {
|
|
6964
7667
|
const cwd = projectRoot || void 0;
|
|
6965
|
-
const reply = (extra) => send(ws, { type: "git.diff", payload: { path:
|
|
6966
|
-
if (!
|
|
7668
|
+
const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
|
|
7669
|
+
if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
|
|
6967
7670
|
reply({ oldText: "", newText: "", error: "invalid path" });
|
|
6968
7671
|
return;
|
|
6969
7672
|
}
|
|
6970
7673
|
try {
|
|
6971
7674
|
const git = makeGit(cwd);
|
|
6972
7675
|
const { readFile: readFile9 } = await import("fs/promises");
|
|
6973
|
-
const { join:
|
|
6974
|
-
const oldText = await git(["show", `HEAD:${
|
|
7676
|
+
const { join: join12 } = await import("path");
|
|
7677
|
+
const oldText = await git(["show", `HEAD:${path17}`]);
|
|
6975
7678
|
let newText = "";
|
|
6976
7679
|
try {
|
|
6977
|
-
const abs = cwd ?
|
|
7680
|
+
const abs = cwd ? join12(cwd, path17) : path17;
|
|
6978
7681
|
const buf = await readFile9(abs);
|
|
6979
7682
|
if (buf.includes(0)) {
|
|
6980
7683
|
reply({ oldText: "", newText: "", binary: true });
|
|
@@ -7070,6 +7773,7 @@ async function handleGoalGet(projectRoot, broadcast2) {
|
|
|
7070
7773
|
|
|
7071
7774
|
// src/server/index.ts
|
|
7072
7775
|
async function startWebUI(opts = {}) {
|
|
7776
|
+
ensureSessionShell();
|
|
7073
7777
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
7074
7778
|
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
7075
7779
|
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
@@ -7151,7 +7855,7 @@ async function startWebUI(opts = {}) {
|
|
|
7151
7855
|
ttlSeconds: 24 * 3600
|
|
7152
7856
|
});
|
|
7153
7857
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
|
|
7154
|
-
const configStore = opts.services?.configStore ?? container.resolve(
|
|
7858
|
+
const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
|
|
7155
7859
|
const providerRegistry = new ProviderRegistry();
|
|
7156
7860
|
try {
|
|
7157
7861
|
const factories = await buildProviderFactoriesFromRegistry({
|
|
@@ -7173,7 +7877,7 @@ async function startWebUI(opts = {}) {
|
|
|
7173
7877
|
r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
7174
7878
|
return r;
|
|
7175
7879
|
})();
|
|
7176
|
-
const memoryStore = new
|
|
7880
|
+
const memoryStore = new DefaultMemoryStore({ paths: wpaths });
|
|
7177
7881
|
if (config.features.memory) {
|
|
7178
7882
|
toolRegistry.register(rememberTool(memoryStore));
|
|
7179
7883
|
toolRegistry.register(forgetTool(memoryStore));
|
|
@@ -7185,6 +7889,8 @@ async function startWebUI(opts = {}) {
|
|
|
7185
7889
|
toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7186
7890
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
7187
7891
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7892
|
+
applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
|
|
7893
|
+
configureExecPolicy(config.tools?.exec ?? {});
|
|
7188
7894
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
7189
7895
|
const mcpRegistry = new MCPRegistry({
|
|
7190
7896
|
toolRegistry,
|
|
@@ -7201,7 +7907,7 @@ async function startWebUI(opts = {}) {
|
|
|
7201
7907
|
});
|
|
7202
7908
|
}
|
|
7203
7909
|
}
|
|
7204
|
-
let sessionStore = opts.services?.session ?? new
|
|
7910
|
+
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
7205
7911
|
if (!opts.services?.session) {
|
|
7206
7912
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
7207
7913
|
if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
|
|
@@ -7228,7 +7934,7 @@ async function startWebUI(opts = {}) {
|
|
|
7228
7934
|
sessionId: session.id,
|
|
7229
7935
|
projectSlug: wpaths.projectSlug,
|
|
7230
7936
|
projectRoot,
|
|
7231
|
-
projectName:
|
|
7937
|
+
projectName: path16.basename(projectRoot),
|
|
7232
7938
|
workingDir,
|
|
7233
7939
|
clientType: "webui",
|
|
7234
7940
|
pid: process.pid,
|
|
@@ -7248,7 +7954,7 @@ async function startWebUI(opts = {}) {
|
|
|
7248
7954
|
const hqTelemetry = createHqPublisherFromEnv({
|
|
7249
7955
|
clientKind: "webui",
|
|
7250
7956
|
projectRoot,
|
|
7251
|
-
projectName:
|
|
7957
|
+
projectName: path16.basename(projectRoot),
|
|
7252
7958
|
appConfig: config,
|
|
7253
7959
|
socketFactory: (url) => new WebSocket2(url)
|
|
7254
7960
|
});
|
|
@@ -7260,7 +7966,7 @@ async function startWebUI(opts = {}) {
|
|
|
7260
7966
|
events,
|
|
7261
7967
|
sessionId: session.id,
|
|
7262
7968
|
projectRoot,
|
|
7263
|
-
projectName:
|
|
7969
|
+
projectName: path16.basename(projectRoot),
|
|
7264
7970
|
globalRoot: wpaths.globalRoot,
|
|
7265
7971
|
initialAgents: statusTracker?.getAgents(),
|
|
7266
7972
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -7289,11 +7995,11 @@ async function startWebUI(opts = {}) {
|
|
|
7289
7995
|
});
|
|
7290
7996
|
} catch {
|
|
7291
7997
|
}
|
|
7292
|
-
const tokenCounter = new
|
|
7998
|
+
const tokenCounter = new DefaultTokenCounter({
|
|
7293
7999
|
registry: modelsRegistry,
|
|
7294
8000
|
providerId: config.provider
|
|
7295
8001
|
});
|
|
7296
|
-
const modeStore = new
|
|
8002
|
+
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
7297
8003
|
const activeMode = await modeStore.getActiveMode();
|
|
7298
8004
|
let modeId = activeMode?.id ?? "default";
|
|
7299
8005
|
const modePrompt = activeMode?.prompt ?? "";
|
|
@@ -7314,15 +8020,15 @@ async function startWebUI(opts = {}) {
|
|
|
7314
8020
|
const modelCapabilitiesRef = {
|
|
7315
8021
|
current: modelCapabilities
|
|
7316
8022
|
};
|
|
7317
|
-
const skillLoader = config.features.skills ? new
|
|
8023
|
+
const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
|
|
7318
8024
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
7319
|
-
manifestPath:
|
|
7320
|
-
projectSkillsDir:
|
|
7321
|
-
globalSkillsDir:
|
|
8025
|
+
manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
8026
|
+
projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
|
|
8027
|
+
globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
|
|
7322
8028
|
projectHash: projectHash(projectRoot),
|
|
7323
8029
|
skillLoader
|
|
7324
8030
|
}) : void 0;
|
|
7325
|
-
const systemPromptBuilder = new
|
|
8031
|
+
const systemPromptBuilder = new DefaultSystemPromptBuilder3({
|
|
7326
8032
|
memoryStore,
|
|
7327
8033
|
skillLoader,
|
|
7328
8034
|
modeStore,
|
|
@@ -7422,6 +8128,8 @@ async function startWebUI(opts = {}) {
|
|
|
7422
8128
|
context.meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
|
|
7423
8129
|
context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
7424
8130
|
context.meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
8131
|
+
context.meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
8132
|
+
context.meta["fallbackAuto"] = config.fallbackAuto !== false;
|
|
7425
8133
|
context.meta["featureMcp"] = config.features.mcp !== false;
|
|
7426
8134
|
context.meta["featurePlugins"] = config.features.plugins !== false;
|
|
7427
8135
|
context.meta["featureMemory"] = config.features.memory !== false;
|
|
@@ -7479,7 +8187,9 @@ async function startWebUI(opts = {}) {
|
|
|
7479
8187
|
"reasoningMode",
|
|
7480
8188
|
"reasoningEffort",
|
|
7481
8189
|
"reasoningPreserve",
|
|
7482
|
-
"cacheTtl"
|
|
8190
|
+
"cacheTtl",
|
|
8191
|
+
"fallbackModels",
|
|
8192
|
+
"fallbackAuto"
|
|
7483
8193
|
];
|
|
7484
8194
|
const prefSnapshot = () => {
|
|
7485
8195
|
const snapshot = {};
|
|
@@ -7510,6 +8220,8 @@ async function startWebUI(opts = {}) {
|
|
|
7510
8220
|
if (typeof payload["enhanceLanguage"] === "string") setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
|
|
7511
8221
|
if (autonomyTouched) decrypted.autonomy = autonomyCfg;
|
|
7512
8222
|
if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
|
|
8223
|
+
if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
|
|
8224
|
+
if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
|
|
7513
8225
|
const FEATURE_MAP = {
|
|
7514
8226
|
featureMcp: "mcp",
|
|
7515
8227
|
featurePlugins: "plugins",
|
|
@@ -7606,7 +8318,7 @@ async function startWebUI(opts = {}) {
|
|
|
7606
8318
|
projectRoot,
|
|
7607
8319
|
logger
|
|
7608
8320
|
});
|
|
7609
|
-
const compactor =
|
|
8321
|
+
const compactor = createStrategyCompactor({
|
|
7610
8322
|
strategy: config.context?.strategy,
|
|
7611
8323
|
preserveK: config.context?.preserveK ?? 10,
|
|
7612
8324
|
eliseThreshold: config.context?.eliseThreshold ?? 2e3,
|
|
@@ -7682,9 +8394,9 @@ async function startWebUI(opts = {}) {
|
|
|
7682
8394
|
maxContext: newMaxContext
|
|
7683
8395
|
});
|
|
7684
8396
|
}
|
|
7685
|
-
const secretScrubber = container.resolve(
|
|
7686
|
-
const renderer = container.has(
|
|
7687
|
-
const permissionPolicy = container.resolve(
|
|
8397
|
+
const secretScrubber = container.resolve(TOKENS.SecretScrubber);
|
|
8398
|
+
const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
|
|
8399
|
+
const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
|
|
7688
8400
|
const toolExecutor = new ToolExecutor(toolRegistry, {
|
|
7689
8401
|
permissionPolicy,
|
|
7690
8402
|
secretScrubber,
|
|
@@ -7727,7 +8439,7 @@ async function startWebUI(opts = {}) {
|
|
|
7727
8439
|
}),
|
|
7728
8440
|
events
|
|
7729
8441
|
);
|
|
7730
|
-
container.bind(
|
|
8442
|
+
container.bind(TOKENS.BrainArbiter, () => brain);
|
|
7731
8443
|
const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
|
|
7732
8444
|
const brainMonitor = new BrainMonitor({
|
|
7733
8445
|
events,
|
|
@@ -7790,6 +8502,29 @@ async function startWebUI(opts = {}) {
|
|
|
7790
8502
|
events,
|
|
7791
8503
|
projectRoot
|
|
7792
8504
|
);
|
|
8505
|
+
const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
|
|
8506
|
+
const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
|
|
8507
|
+
const sddWizardHandler = new SddWizardWebSocketHandler(
|
|
8508
|
+
buildSddWizardDeps({
|
|
8509
|
+
agent,
|
|
8510
|
+
events,
|
|
8511
|
+
projectRoot,
|
|
8512
|
+
brain,
|
|
8513
|
+
subagentFactory: makeLightSubagentFactory({
|
|
8514
|
+
container,
|
|
8515
|
+
providerRegistry,
|
|
8516
|
+
toolRegistry,
|
|
8517
|
+
session,
|
|
8518
|
+
projectRoot
|
|
8519
|
+
}),
|
|
8520
|
+
paths: {
|
|
8521
|
+
projectSpecs: wpaths.projectSpecs,
|
|
8522
|
+
projectTaskGraphs: wpaths.projectTaskGraphs,
|
|
8523
|
+
projectSddBoards: wpaths.projectSddBoards,
|
|
8524
|
+
projectDir: wpaths.projectDir
|
|
8525
|
+
}
|
|
8526
|
+
})
|
|
8527
|
+
);
|
|
7793
8528
|
const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
|
|
7794
8529
|
const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
|
|
7795
8530
|
const collabHandler = new CollaborationWebSocketHandler(
|
|
@@ -7829,7 +8564,7 @@ async function startWebUI(opts = {}) {
|
|
|
7829
8564
|
inputCost,
|
|
7830
8565
|
outputCost,
|
|
7831
8566
|
cacheReadCost,
|
|
7832
|
-
projectName:
|
|
8567
|
+
projectName: path16.basename(projectRoot) || projectRoot,
|
|
7833
8568
|
projectRoot,
|
|
7834
8569
|
cwd: workingDir,
|
|
7835
8570
|
mode: modeId,
|
|
@@ -7921,6 +8656,9 @@ async function startWebUI(opts = {}) {
|
|
|
7921
8656
|
}));
|
|
7922
8657
|
});
|
|
7923
8658
|
autoPhaseHandler.addClient(ws);
|
|
8659
|
+
specsHandler.addClient(ws);
|
|
8660
|
+
sddBoardHandler.addClient(ws);
|
|
8661
|
+
sddWizardHandler.addClient(ws);
|
|
7924
8662
|
worktreeHandler.addClient(ws);
|
|
7925
8663
|
collabHandler.addClient(ws);
|
|
7926
8664
|
terminalHandler.addClient(ws);
|
|
@@ -8045,21 +8783,21 @@ async function startWebUI(opts = {}) {
|
|
|
8045
8783
|
});
|
|
8046
8784
|
}
|
|
8047
8785
|
async function touchProjectEntry(root, workDir) {
|
|
8048
|
-
const resolved =
|
|
8786
|
+
const resolved = path16.resolve(root);
|
|
8049
8787
|
const manifest = await loadManifest(globalConfigPath);
|
|
8050
8788
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8051
|
-
const existing = manifest.projects.find((p) =>
|
|
8789
|
+
const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
|
|
8052
8790
|
if (existing) {
|
|
8053
8791
|
existing.lastSeen = now;
|
|
8054
|
-
if (workDir) existing.lastWorkingDir =
|
|
8792
|
+
if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
|
|
8055
8793
|
} else {
|
|
8056
8794
|
manifest.projects.push({
|
|
8057
|
-
name:
|
|
8795
|
+
name: path16.basename(resolved),
|
|
8058
8796
|
root: resolved,
|
|
8059
8797
|
slug: generateProjectSlug(resolved),
|
|
8060
8798
|
createdAt: now,
|
|
8061
8799
|
lastSeen: now,
|
|
8062
|
-
lastWorkingDir: workDir ?
|
|
8800
|
+
lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
|
|
8063
8801
|
});
|
|
8064
8802
|
}
|
|
8065
8803
|
await saveManifest(manifest, globalConfigPath);
|
|
@@ -8081,19 +8819,29 @@ async function startWebUI(opts = {}) {
|
|
|
8081
8819
|
let sessionRoutes;
|
|
8082
8820
|
let projectRoutes;
|
|
8083
8821
|
let modeRoutes;
|
|
8822
|
+
let prefsRoutes;
|
|
8084
8823
|
let shellGitRoutes;
|
|
8085
8824
|
let mailboxRoutes;
|
|
8825
|
+
let mcpRoutes;
|
|
8086
8826
|
let brainRoutes;
|
|
8087
8827
|
let autoPhaseRoutes;
|
|
8828
|
+
let specsRoutes;
|
|
8829
|
+
let sddBoardRoutes;
|
|
8830
|
+
let sddWizardRoutes;
|
|
8088
8831
|
async function handleMessage(ws, _client, msg) {
|
|
8089
8832
|
if (await handleProviderRoute(ws, msg, providerRoutes)) return;
|
|
8090
8833
|
if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
|
|
8091
8834
|
if (await handleProjectRoute(ws, msg, projectRoutes)) return;
|
|
8092
8835
|
if (await handleModeRoute(ws, msg, modeRoutes)) return;
|
|
8836
|
+
if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
|
|
8093
8837
|
if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
|
|
8094
8838
|
if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
|
|
8839
|
+
if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
|
|
8095
8840
|
if (await handleBrainRoute(ws, msg, brainRoutes)) return;
|
|
8096
8841
|
if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
|
|
8842
|
+
if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
|
|
8843
|
+
if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
|
|
8844
|
+
if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
|
|
8097
8845
|
switch (msg.type) {
|
|
8098
8846
|
// Collaboration messages short-circuit the user/agent flow.
|
|
8099
8847
|
// They don't touch runLock, the agent loop, or the message queue —
|
|
@@ -8199,27 +8947,31 @@ async function startWebUI(opts = {}) {
|
|
|
8199
8947
|
case "memory.forget":
|
|
8200
8948
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
8201
8949
|
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
|
|
8202
|
-
// backed by the live MCPRegistry constructed above.
|
|
8950
|
+
// backed by the live MCPRegistry constructed above. Routed via
|
|
8951
|
+
// handleMcpRoute (see mcpRoutes = { ... } below). These case arms
|
|
8952
|
+
// are unreachable but left as tripwires for any future regression
|
|
8953
|
+
// where the route chain stops claiming 'mcp.*'. If you see one
|
|
8954
|
+
// fire, fix the dispatch order in the handleMessage chain above.
|
|
8203
8955
|
case "mcp.list":
|
|
8204
|
-
|
|
8956
|
+
throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
|
|
8205
8957
|
case "mcp.add":
|
|
8206
|
-
|
|
8207
|
-
case "mcp.remove":
|
|
8208
|
-
return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
|
|
8958
|
+
throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
|
|
8209
8959
|
case "mcp.update":
|
|
8210
|
-
|
|
8211
|
-
case "mcp.
|
|
8212
|
-
|
|
8213
|
-
case "mcp.sleep":
|
|
8214
|
-
return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
|
|
8215
|
-
case "mcp.discover":
|
|
8216
|
-
return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
|
|
8960
|
+
throw new Error("handleMcpRoute did not claim mcp.update \u2014 check chain order");
|
|
8961
|
+
case "mcp.remove":
|
|
8962
|
+
throw new Error("handleMcpRoute did not claim mcp.remove \u2014 check chain order");
|
|
8217
8963
|
case "mcp.enable":
|
|
8218
|
-
|
|
8964
|
+
throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
|
|
8219
8965
|
case "mcp.disable":
|
|
8220
|
-
|
|
8966
|
+
throw new Error("handleMcpRoute did not claim mcp.disable \u2014 check chain order");
|
|
8967
|
+
case "mcp.sleep":
|
|
8968
|
+
throw new Error("handleMcpRoute did not claim mcp.sleep \u2014 check chain order");
|
|
8969
|
+
case "mcp.wake":
|
|
8970
|
+
throw new Error("handleMcpRoute did not claim mcp.wake \u2014 check chain order");
|
|
8221
8971
|
case "mcp.restart":
|
|
8222
|
-
|
|
8972
|
+
throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
|
|
8973
|
+
case "mcp.discover":
|
|
8974
|
+
throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
|
|
8223
8975
|
// Skills — full request→response cycle lives in skills-handlers.ts
|
|
8224
8976
|
// (shared with the CLI's embedded server). skillsCtx is the closed-over
|
|
8225
8977
|
// loader/installer/projectRoot the handlers need.
|
|
@@ -8367,49 +9119,11 @@ async function startWebUI(opts = {}) {
|
|
|
8367
9119
|
break;
|
|
8368
9120
|
}
|
|
8369
9121
|
case "prefs.update": {
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
sendResult2(ws, false, parsed.message);
|
|
8373
|
-
break;
|
|
8374
|
-
}
|
|
8375
|
-
const payload = parsed.value.prefs;
|
|
8376
|
-
for (const [key, val] of Object.entries(payload)) {
|
|
8377
|
-
context.meta[key] = val;
|
|
8378
|
-
}
|
|
8379
|
-
void persistPrefsToConfig(payload);
|
|
8380
|
-
if (typeof payload["yolo"] === "boolean") {
|
|
8381
|
-
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
8382
|
-
}
|
|
8383
|
-
if (typeof payload["featureMcp"] === "boolean")
|
|
8384
|
-
config.features.mcp = payload["featureMcp"];
|
|
8385
|
-
if (typeof payload["featurePlugins"] === "boolean")
|
|
8386
|
-
config.features.plugins = payload["featurePlugins"];
|
|
8387
|
-
if (typeof payload["featureMemory"] === "boolean")
|
|
8388
|
-
config.features.memory = payload["featureMemory"];
|
|
8389
|
-
if (typeof payload["featureSkills"] === "boolean")
|
|
8390
|
-
config.features.skills = payload["featureSkills"];
|
|
8391
|
-
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
8392
|
-
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
8393
|
-
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
8394
|
-
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
8395
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
8396
|
-
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
8397
|
-
} else {
|
|
8398
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
8399
|
-
}
|
|
8400
|
-
}
|
|
8401
|
-
if (typeof payload["logLevel"] === "string") {
|
|
8402
|
-
const valid = ["debug", "info", "warn", "error"];
|
|
8403
|
-
if (valid.includes(payload["logLevel"])) {
|
|
8404
|
-
logger.level = payload["logLevel"];
|
|
8405
|
-
}
|
|
8406
|
-
}
|
|
8407
|
-
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
8408
|
-
break;
|
|
9122
|
+
void ws;
|
|
9123
|
+
throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
|
|
8409
9124
|
}
|
|
8410
9125
|
case "prefs.get": {
|
|
8411
|
-
|
|
8412
|
-
break;
|
|
9126
|
+
throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
|
|
8413
9127
|
}
|
|
8414
9128
|
default:
|
|
8415
9129
|
send(ws, {
|
|
@@ -8452,22 +9166,7 @@ async function startWebUI(opts = {}) {
|
|
|
8452
9166
|
const saved = await providerHandlers.loadConfigProviders();
|
|
8453
9167
|
send(ws, {
|
|
8454
9168
|
type: "providers.saved",
|
|
8455
|
-
payload: {
|
|
8456
|
-
providers: Object.entries(saved).map(([id, cfg]) => {
|
|
8457
|
-
const keys = normalizeKeys(cfg);
|
|
8458
|
-
return {
|
|
8459
|
-
id,
|
|
8460
|
-
family: cfg.family ?? id,
|
|
8461
|
-
baseUrl: cfg.baseUrl,
|
|
8462
|
-
apiKeys: keys.map((k) => ({
|
|
8463
|
-
label: k.label,
|
|
8464
|
-
maskedKey: maskedKey(k.apiKey),
|
|
8465
|
-
isActive: k.label === cfg.activeKey,
|
|
8466
|
-
createdAt: k.createdAt
|
|
8467
|
-
}))
|
|
8468
|
-
};
|
|
8469
|
-
})
|
|
8470
|
-
}
|
|
9169
|
+
payload: { providers: projectSavedProviders(saved) }
|
|
8471
9170
|
});
|
|
8472
9171
|
},
|
|
8473
9172
|
listProviderModels: async (ws, msg) => {
|
|
@@ -8645,6 +9344,55 @@ async function startWebUI(opts = {}) {
|
|
|
8645
9344
|
},
|
|
8646
9345
|
sessionStartPayload
|
|
8647
9346
|
});
|
|
9347
|
+
prefsRoutes = {
|
|
9348
|
+
getPrefs: async (ws) => {
|
|
9349
|
+
send(ws, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9350
|
+
},
|
|
9351
|
+
updatePrefs: async (ws, msgPayload) => {
|
|
9352
|
+
const parsed = validatePrefsUpdatePayload(msgPayload);
|
|
9353
|
+
if (!parsed.ok) {
|
|
9354
|
+
sendResult2(ws, false, parsed.message);
|
|
9355
|
+
return;
|
|
9356
|
+
}
|
|
9357
|
+
const payload = parsed.value.prefs;
|
|
9358
|
+
for (const [key, val] of Object.entries(payload)) {
|
|
9359
|
+
context.meta[key] = val;
|
|
9360
|
+
}
|
|
9361
|
+
void persistPrefsToConfig(payload);
|
|
9362
|
+
if (typeof payload["yolo"] === "boolean") {
|
|
9363
|
+
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
9364
|
+
}
|
|
9365
|
+
if (typeof payload["featureMcp"] === "boolean")
|
|
9366
|
+
config.features.mcp = payload["featureMcp"];
|
|
9367
|
+
if (typeof payload["featurePlugins"] === "boolean")
|
|
9368
|
+
config.features.plugins = payload["featurePlugins"];
|
|
9369
|
+
if (typeof payload["featureMemory"] === "boolean")
|
|
9370
|
+
config.features.memory = payload["featureMemory"];
|
|
9371
|
+
if (typeof payload["featureSkills"] === "boolean")
|
|
9372
|
+
config.features.skills = payload["featureSkills"];
|
|
9373
|
+
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
9374
|
+
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9375
|
+
if (Array.isArray(payload["fallbackModels"]))
|
|
9376
|
+
config.fallbackModels = payload["fallbackModels"];
|
|
9377
|
+
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9378
|
+
config.fallbackAuto = payload["fallbackAuto"];
|
|
9379
|
+
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
9380
|
+
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
9381
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9382
|
+
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
9383
|
+
} else {
|
|
9384
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9385
|
+
}
|
|
9386
|
+
}
|
|
9387
|
+
if (typeof payload["logLevel"] === "string") {
|
|
9388
|
+
const valid = ["debug", "info", "warn", "error"];
|
|
9389
|
+
if (valid.includes(payload["logLevel"])) {
|
|
9390
|
+
logger.level = payload["logLevel"];
|
|
9391
|
+
}
|
|
9392
|
+
}
|
|
9393
|
+
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9394
|
+
}
|
|
9395
|
+
};
|
|
8648
9396
|
shellGitRoutes = {
|
|
8649
9397
|
gitInfo: async (ws) => {
|
|
8650
9398
|
await handleGitInfo(ws, projectRoot);
|
|
@@ -8677,7 +9425,7 @@ async function startWebUI(opts = {}) {
|
|
|
8677
9425
|
sendResult2(ws, false, parsed.message);
|
|
8678
9426
|
return;
|
|
8679
9427
|
}
|
|
8680
|
-
return handleMailboxMessages(ws, { projectRoot, globalRoot:
|
|
9428
|
+
return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
8681
9429
|
},
|
|
8682
9430
|
agents: (ws, msg) => {
|
|
8683
9431
|
const parsed = validateMailboxAgentsPayload(msg.payload);
|
|
@@ -8685,18 +9433,30 @@ async function startWebUI(opts = {}) {
|
|
|
8685
9433
|
sendResult2(ws, false, parsed.message);
|
|
8686
9434
|
return;
|
|
8687
9435
|
}
|
|
8688
|
-
return handleMailboxAgents(ws, { projectRoot, globalRoot:
|
|
9436
|
+
return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
8689
9437
|
},
|
|
8690
|
-
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot:
|
|
9438
|
+
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
|
|
8691
9439
|
purge: (ws, msg) => {
|
|
8692
9440
|
const parsed = validateMailboxPurgePayload(msg.payload);
|
|
8693
9441
|
if (!parsed.ok) {
|
|
8694
9442
|
sendResult2(ws, false, parsed.message);
|
|
8695
9443
|
return;
|
|
8696
9444
|
}
|
|
8697
|
-
return handleMailboxPurge(ws, { projectRoot, globalRoot:
|
|
9445
|
+
return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
8698
9446
|
}
|
|
8699
9447
|
};
|
|
9448
|
+
mcpRoutes = {
|
|
9449
|
+
list: (ws, msg) => handleMcpList(ws, msg, globalConfigPath, mcpRegistry),
|
|
9450
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry),
|
|
9451
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry),
|
|
9452
|
+
remove: (ws, msg) => handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry),
|
|
9453
|
+
enable: (ws, msg) => handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9454
|
+
disable: (ws, msg) => handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9455
|
+
sleep: (ws, msg) => handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry),
|
|
9456
|
+
wake: (ws, msg) => handleMcpWake(ws, msg, globalConfigPath, mcpRegistry),
|
|
9457
|
+
restart: (ws, msg) => handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry),
|
|
9458
|
+
discover: (ws, msg) => handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry)
|
|
9459
|
+
};
|
|
8700
9460
|
brainRoutes = {
|
|
8701
9461
|
status: (ws) => {
|
|
8702
9462
|
send(ws, {
|
|
@@ -8741,6 +9501,15 @@ async function startWebUI(opts = {}) {
|
|
|
8741
9501
|
autoPhaseRoutes = {
|
|
8742
9502
|
handleMessage: (msg) => autoPhaseHandler.handleMessage(msg)
|
|
8743
9503
|
};
|
|
9504
|
+
specsRoutes = {
|
|
9505
|
+
handleMessage: (msg) => specsHandler.handleMessage(msg)
|
|
9506
|
+
};
|
|
9507
|
+
sddBoardRoutes = {
|
|
9508
|
+
handleMessage: (msg) => sddBoardHandler.handleMessage(msg)
|
|
9509
|
+
};
|
|
9510
|
+
sddWizardRoutes = {
|
|
9511
|
+
handleMessage: (msg) => sddWizardHandler.handleMessage(msg)
|
|
9512
|
+
};
|
|
8744
9513
|
const watcherMetrics = {
|
|
8745
9514
|
fileChangesDetected: 0,
|
|
8746
9515
|
filesProcessed: 0,
|
|
@@ -8753,7 +9522,7 @@ async function startWebUI(opts = {}) {
|
|
|
8753
9522
|
};
|
|
8754
9523
|
const httpServer = createHttpServer({
|
|
8755
9524
|
host: wsHost,
|
|
8756
|
-
distDir:
|
|
9525
|
+
distDir: path16.resolve(import.meta.dirname, "../../dist"),
|
|
8757
9526
|
wsPort,
|
|
8758
9527
|
globalRoot: wpaths.globalRoot,
|
|
8759
9528
|
apiToken: wsToken,
|
|
@@ -8762,7 +9531,7 @@ async function startWebUI(opts = {}) {
|
|
|
8762
9531
|
void fleetBroadcast?.();
|
|
8763
9532
|
}
|
|
8764
9533
|
});
|
|
8765
|
-
const registryBaseDir =
|
|
9534
|
+
const registryBaseDir = path16.dirname(globalConfigPath);
|
|
8766
9535
|
httpServer.listen(httpPort, wsHost, () => {
|
|
8767
9536
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
8768
9537
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -8774,7 +9543,7 @@ async function startWebUI(opts = {}) {
|
|
|
8774
9543
|
wsPort,
|
|
8775
9544
|
host: wsHost,
|
|
8776
9545
|
projectRoot,
|
|
8777
|
-
projectName:
|
|
9546
|
+
projectName: path16.basename(projectRoot) || projectRoot,
|
|
8778
9547
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8779
9548
|
url: `http://${wsHost}:${httpPort}`
|
|
8780
9549
|
},
|
|
@@ -8817,11 +9586,15 @@ async function startWebUI(opts = {}) {
|
|
|
8817
9586
|
}
|
|
8818
9587
|
export {
|
|
8819
9588
|
AutoPhaseWebSocketHandler,
|
|
9589
|
+
SddBoardWebSocketHandler,
|
|
9590
|
+
SddWizardWebSocketHandler,
|
|
9591
|
+
SpecsWebSocketHandler,
|
|
8820
9592
|
WorktreeWebSocketHandler,
|
|
8821
9593
|
addProvider,
|
|
8822
9594
|
broadcast,
|
|
8823
9595
|
browserOpenCommand,
|
|
8824
9596
|
buildCspHeader,
|
|
9597
|
+
buildSddWizardDeps,
|
|
8825
9598
|
createCustomModeStore,
|
|
8826
9599
|
createEternalSubscription,
|
|
8827
9600
|
createHttpServer,
|