@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/entry.js
CHANGED
|
@@ -8,7 +8,10 @@ function isRecord(value) {
|
|
|
8
8
|
}
|
|
9
9
|
function validateModelSwitchPayload(payload) {
|
|
10
10
|
if (!isRecord(payload)) {
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
message: "model.switch payload must be an object with string provider and model"
|
|
14
|
+
};
|
|
12
15
|
}
|
|
13
16
|
const provider = payload["provider"];
|
|
14
17
|
const model = payload["model"];
|
|
@@ -30,13 +33,22 @@ function validateMailboxMessagesPayload(payload) {
|
|
|
30
33
|
const agentId = payload["agentId"];
|
|
31
34
|
const unreadOnly = payload["unreadOnly"];
|
|
32
35
|
if (limit !== void 0 && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1)) {
|
|
33
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
message: "mailbox.messages payload.limit must be a positive number when provided"
|
|
39
|
+
};
|
|
34
40
|
}
|
|
35
41
|
if (agentId !== void 0 && typeof agentId !== "string") {
|
|
36
|
-
return {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
message: "mailbox.messages payload.agentId must be a string when provided"
|
|
45
|
+
};
|
|
37
46
|
}
|
|
38
47
|
if (unreadOnly !== void 0 && typeof unreadOnly !== "boolean") {
|
|
39
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
message: "mailbox.messages payload.unreadOnly must be a boolean when provided"
|
|
51
|
+
};
|
|
40
52
|
}
|
|
41
53
|
return { ok: true, value: { limit, agentId, unreadOnly } };
|
|
42
54
|
}
|
|
@@ -47,7 +59,10 @@ function validateMailboxAgentsPayload(payload) {
|
|
|
47
59
|
}
|
|
48
60
|
const onlineOnly = payload["onlineOnly"];
|
|
49
61
|
if (onlineOnly !== void 0 && typeof onlineOnly !== "boolean") {
|
|
50
|
-
return {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
message: "mailbox.agents payload.onlineOnly must be a boolean when provided"
|
|
65
|
+
};
|
|
51
66
|
}
|
|
52
67
|
return { ok: true, value: { onlineOnly } };
|
|
53
68
|
}
|
|
@@ -59,10 +74,16 @@ function validateMailboxPurgePayload(payload) {
|
|
|
59
74
|
const completedMaxAgeMs = payload["completedMaxAgeMs"];
|
|
60
75
|
const incompleteMaxAgeMs = payload["incompleteMaxAgeMs"];
|
|
61
76
|
if (completedMaxAgeMs !== void 0 && (typeof completedMaxAgeMs !== "number" || !Number.isFinite(completedMaxAgeMs) || completedMaxAgeMs < 0)) {
|
|
62
|
-
return {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
message: "mailbox.purge payload.completedMaxAgeMs must be a non-negative number when provided"
|
|
80
|
+
};
|
|
63
81
|
}
|
|
64
82
|
if (incompleteMaxAgeMs !== void 0 && (typeof incompleteMaxAgeMs !== "number" || !Number.isFinite(incompleteMaxAgeMs) || incompleteMaxAgeMs < 0)) {
|
|
65
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
message: "mailbox.purge payload.incompleteMaxAgeMs must be a non-negative number when provided"
|
|
86
|
+
};
|
|
66
87
|
}
|
|
67
88
|
return { ok: true, value: { completedMaxAgeMs, incompleteMaxAgeMs } };
|
|
68
89
|
}
|
|
@@ -73,7 +94,10 @@ function validateBrainRiskPayload(payload) {
|
|
|
73
94
|
}
|
|
74
95
|
const level = payload["level"];
|
|
75
96
|
if (typeof level !== "string" || !BRAIN_RISK_VALUES.has(level)) {
|
|
76
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
message: "brain.risk payload.level must be one of off, low, medium, high, all"
|
|
100
|
+
};
|
|
77
101
|
}
|
|
78
102
|
return { ok: true, value: { level } };
|
|
79
103
|
}
|
|
@@ -99,7 +123,10 @@ function validateAutonomySwitchPayload(payload) {
|
|
|
99
123
|
}
|
|
100
124
|
function validatePlanTemplateUsePayload(payload) {
|
|
101
125
|
if (!isRecord(payload)) {
|
|
102
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
message: "plan.template_use payload must be an object with string template"
|
|
129
|
+
};
|
|
103
130
|
}
|
|
104
131
|
const template = payload["template"];
|
|
105
132
|
if (typeof template !== "string" || template.trim().length === 0) {
|
|
@@ -114,7 +141,15 @@ var ENHANCE_LANGUAGE_VALUES = /* @__PURE__ */ new Set(["original", "english"]);
|
|
|
114
141
|
var LOG_LEVEL_VALUES = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
|
|
115
142
|
var AUDIT_LEVEL_VALUES = /* @__PURE__ */ new Set(["minimal", "standard", "full"]);
|
|
116
143
|
var REASONING_MODE_VALUES = /* @__PURE__ */ new Set(["auto", "on", "off"]);
|
|
117
|
-
var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
|
|
144
|
+
var REASONING_EFFORT_VALUES = /* @__PURE__ */ new Set([
|
|
145
|
+
"none",
|
|
146
|
+
"minimal",
|
|
147
|
+
"low",
|
|
148
|
+
"medium",
|
|
149
|
+
"high",
|
|
150
|
+
"xhigh",
|
|
151
|
+
"max"
|
|
152
|
+
]);
|
|
118
153
|
var CACHE_TTL_VALUES = /* @__PURE__ */ new Set(["default", "5m", "1h"]);
|
|
119
154
|
var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
120
155
|
"yolo",
|
|
@@ -135,8 +170,10 @@ var BOOLEAN_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
|
135
170
|
"tgDelegate",
|
|
136
171
|
"reasoningPreserve",
|
|
137
172
|
"hqEnabled",
|
|
138
|
-
"hqRawContent"
|
|
173
|
+
"hqRawContent",
|
|
174
|
+
"fallbackAuto"
|
|
139
175
|
]);
|
|
176
|
+
var STRING_ARRAY_PREF_KEYS = /* @__PURE__ */ new Set(["fallbackModels"]);
|
|
140
177
|
var NUMBER_PREF_KEYS = /* @__PURE__ */ new Set([
|
|
141
178
|
"autonomyDelayMs",
|
|
142
179
|
"autoProceedMaxIterations",
|
|
@@ -168,6 +205,9 @@ function validatePreferenceValue(key, value) {
|
|
|
168
205
|
if (STRING_PREF_KEYS.has(key)) {
|
|
169
206
|
return typeof value === "string" ? null : `prefs.update payload.${key} must be a string`;
|
|
170
207
|
}
|
|
208
|
+
if (STRING_ARRAY_PREF_KEYS.has(key)) {
|
|
209
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string") ? null : `prefs.update payload.${key} must be an array of strings`;
|
|
210
|
+
}
|
|
171
211
|
const allowed = ENUM_PREF_KEYS[key];
|
|
172
212
|
if (allowed) {
|
|
173
213
|
return typeof value === "string" && allowed.has(value) ? null : `prefs.update payload.${key} must be one of: ${Array.from(allowed).join(", ")}`;
|
|
@@ -288,16 +328,25 @@ function validateContextModeCreatePayload(payload) {
|
|
|
288
328
|
return { ok: false, message: "context.mode.create payload.description must be a string" };
|
|
289
329
|
}
|
|
290
330
|
if (!isRecord(thresholds)) {
|
|
291
|
-
return {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
message: "context.mode.create payload.thresholds must be an object with warn/soft/hard numbers"
|
|
334
|
+
};
|
|
292
335
|
}
|
|
293
336
|
if (!isFiniteNumber(thresholds["warn"]) || !isFiniteNumber(thresholds["soft"]) || !isFiniteNumber(thresholds["hard"])) {
|
|
294
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
message: "context.mode.create payload.thresholds.warn/soft/hard must be finite numbers"
|
|
340
|
+
};
|
|
295
341
|
}
|
|
296
342
|
if (!isFiniteNumber(preserveK)) {
|
|
297
343
|
return { ok: false, message: "context.mode.create payload.preserveK must be a finite number" };
|
|
298
344
|
}
|
|
299
345
|
if (!isFiniteNumber(eliseThreshold)) {
|
|
300
|
-
return {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
message: "context.mode.create payload.eliseThreshold must be a finite number"
|
|
349
|
+
};
|
|
301
350
|
}
|
|
302
351
|
return {
|
|
303
352
|
ok: true,
|
|
@@ -321,22 +370,34 @@ function validateContextModeUpdatePayload(payload) {
|
|
|
321
370
|
}
|
|
322
371
|
const name2 = payload["name"];
|
|
323
372
|
if (name2 !== void 0 && typeof name2 !== "string") {
|
|
324
|
-
return {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
message: "context.mode.update payload.name must be a string when provided"
|
|
376
|
+
};
|
|
325
377
|
}
|
|
326
378
|
const description = payload["description"];
|
|
327
379
|
if (description !== void 0 && typeof description !== "string") {
|
|
328
|
-
return {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
message: "context.mode.update payload.description must be a string when provided"
|
|
383
|
+
};
|
|
329
384
|
}
|
|
330
385
|
const thresholds = payload["thresholds"];
|
|
331
386
|
let validatedThresholds;
|
|
332
387
|
if (thresholds !== void 0) {
|
|
333
388
|
if (!isRecord(thresholds)) {
|
|
334
|
-
return {
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
message: "context.mode.update payload.thresholds must be an object when provided"
|
|
392
|
+
};
|
|
335
393
|
}
|
|
336
394
|
for (const key of ["warn", "soft", "hard"]) {
|
|
337
395
|
const val = thresholds[key];
|
|
338
396
|
if (val !== void 0 && !isFiniteNumber(val)) {
|
|
339
|
-
return {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
message: `context.mode.update payload.thresholds.${key} must be a finite number when provided`
|
|
400
|
+
};
|
|
340
401
|
}
|
|
341
402
|
}
|
|
342
403
|
validatedThresholds = {
|
|
@@ -347,11 +408,17 @@ function validateContextModeUpdatePayload(payload) {
|
|
|
347
408
|
}
|
|
348
409
|
const preserveK = payload["preserveK"];
|
|
349
410
|
if (preserveK !== void 0 && !isFiniteNumber(preserveK)) {
|
|
350
|
-
return {
|
|
411
|
+
return {
|
|
412
|
+
ok: false,
|
|
413
|
+
message: "context.mode.update payload.preserveK must be a finite number when provided"
|
|
414
|
+
};
|
|
351
415
|
}
|
|
352
416
|
const eliseThreshold = payload["eliseThreshold"];
|
|
353
417
|
if (eliseThreshold !== void 0 && !isFiniteNumber(eliseThreshold)) {
|
|
354
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
message: "context.mode.update payload.eliseThreshold must be a finite number when provided"
|
|
421
|
+
};
|
|
355
422
|
}
|
|
356
423
|
return {
|
|
357
424
|
ok: true,
|
|
@@ -369,28 +436,31 @@ function validateShellOpenPayload(payload) {
|
|
|
369
436
|
if (!isRecord(payload)) {
|
|
370
437
|
return { ok: false, message: "shell.open payload must be an object with string path" };
|
|
371
438
|
}
|
|
372
|
-
const
|
|
373
|
-
if (typeof
|
|
439
|
+
const path17 = payload["path"];
|
|
440
|
+
if (typeof path17 !== "string" || path17.trim().length === 0) {
|
|
374
441
|
return { ok: false, message: "shell.open payload.path must be a non-empty string" };
|
|
375
442
|
}
|
|
376
443
|
const target = payload["target"];
|
|
377
444
|
if (target !== void 0 && target !== "file" && target !== "terminal") {
|
|
378
|
-
return {
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
message: 'shell.open payload.target must be "file" or "terminal" when provided'
|
|
448
|
+
};
|
|
379
449
|
}
|
|
380
|
-
return { ok: true, value: { path:
|
|
450
|
+
return { ok: true, value: { path: path17, target } };
|
|
381
451
|
}
|
|
382
452
|
function validateGitDiffPayload(payload) {
|
|
383
453
|
if (!isRecord(payload)) {
|
|
384
454
|
return { ok: false, message: "git.diff payload must be an object" };
|
|
385
455
|
}
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
456
|
+
const path17 = payload["path"];
|
|
457
|
+
if (path17 === void 0 || path17 === null) {
|
|
388
458
|
return { ok: true, value: { path: "" } };
|
|
389
459
|
}
|
|
390
|
-
if (typeof
|
|
460
|
+
if (typeof path17 !== "string") {
|
|
391
461
|
return { ok: false, message: "git.diff payload.path must be a string when provided" };
|
|
392
462
|
}
|
|
393
|
-
return { ok: true, value: { path:
|
|
463
|
+
return { ok: true, value: { path: path17 } };
|
|
394
464
|
}
|
|
395
465
|
function validateProjectsAddPayload(payload) {
|
|
396
466
|
if (!isRecord(payload)) {
|
|
@@ -570,7 +640,7 @@ async function handlePlanItemUpdate(ctx, ws, payload) {
|
|
|
570
640
|
return;
|
|
571
641
|
}
|
|
572
642
|
try {
|
|
573
|
-
const {
|
|
643
|
+
const { mutatePlan, setPlanItemStatus } = await import("@wrongstack/core");
|
|
574
644
|
let changed = false;
|
|
575
645
|
const plan = await mutatePlan(planPath, sessionId, async (p) => {
|
|
576
646
|
const before = p.updatedAt;
|
|
@@ -650,7 +720,7 @@ import {
|
|
|
650
720
|
createTieredBrainArbiter
|
|
651
721
|
} from "@wrongstack/core";
|
|
652
722
|
import * as fs13 from "fs/promises";
|
|
653
|
-
import * as
|
|
723
|
+
import * as path16 from "path";
|
|
654
724
|
|
|
655
725
|
// src/server/http-server.ts
|
|
656
726
|
import * as fs from "fs/promises";
|
|
@@ -827,7 +897,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
827
897
|
return;
|
|
828
898
|
}
|
|
829
899
|
try {
|
|
830
|
-
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore:
|
|
900
|
+
const { SessionRegistry, resolveWstackPaths: resolveWstackPaths2, DefaultSessionStore: DefaultSessionStore3, DefaultSessionReader: DefaultSessionReader2 } = await import("@wrongstack/core");
|
|
831
901
|
const registry = new SessionRegistry(globalRoot);
|
|
832
902
|
const entry = await registry.get(sessionId);
|
|
833
903
|
if (!entry) {
|
|
@@ -836,7 +906,7 @@ async function handleApiSessionEvents(res, globalRoot, sessionId, limit) {
|
|
|
836
906
|
return;
|
|
837
907
|
}
|
|
838
908
|
const paths = resolveWstackPaths2({ projectRoot: entry.projectRoot, globalRoot });
|
|
839
|
-
const store = new
|
|
909
|
+
const store = new DefaultSessionStore3({ dir: paths.projectSessions });
|
|
840
910
|
const reader = new DefaultSessionReader2({ store });
|
|
841
911
|
const rawEntries = [];
|
|
842
912
|
for await (const ev of reader.replay(sessionId)) {
|
|
@@ -1522,8 +1592,8 @@ function isInside(root, target) {
|
|
|
1522
1592
|
}
|
|
1523
1593
|
|
|
1524
1594
|
// src/server/file-handlers.ts
|
|
1525
|
-
import * as
|
|
1526
|
-
import * as
|
|
1595
|
+
import * as fs4 from "fs/promises";
|
|
1596
|
+
import * as path4 from "path";
|
|
1527
1597
|
import { atomicWrite } from "@wrongstack/core";
|
|
1528
1598
|
|
|
1529
1599
|
// src/server/file-picker.ts
|
|
@@ -1574,6 +1644,34 @@ function rankFiles(paths, query, limit) {
|
|
|
1574
1644
|
return scored.slice(0, limit).map((s) => s.path);
|
|
1575
1645
|
}
|
|
1576
1646
|
|
|
1647
|
+
// src/server/path-containment.ts
|
|
1648
|
+
import * as fs3 from "fs/promises";
|
|
1649
|
+
import * as path3 from "path";
|
|
1650
|
+
function isPathInside(root, target) {
|
|
1651
|
+
const relative3 = path3.relative(root, target);
|
|
1652
|
+
return relative3 === "" || !relative3.startsWith("..") && !path3.isAbsolute(relative3);
|
|
1653
|
+
}
|
|
1654
|
+
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
1655
|
+
const resolved = path3.resolve(projectRoot, inputPath);
|
|
1656
|
+
let stat3;
|
|
1657
|
+
try {
|
|
1658
|
+
stat3 = await fs3.stat(resolved);
|
|
1659
|
+
} catch {
|
|
1660
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1661
|
+
}
|
|
1662
|
+
if (!stat3.isDirectory()) {
|
|
1663
|
+
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
1664
|
+
}
|
|
1665
|
+
const [realProjectRoot, realResolved] = await Promise.all([
|
|
1666
|
+
fs3.realpath(projectRoot),
|
|
1667
|
+
fs3.realpath(resolved)
|
|
1668
|
+
]);
|
|
1669
|
+
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
1670
|
+
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
1671
|
+
}
|
|
1672
|
+
return resolved;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1577
1675
|
// src/server/ws-utils.ts
|
|
1578
1676
|
import { randomBytes } from "crypto";
|
|
1579
1677
|
import { WebSocket } from "ws";
|
|
@@ -1604,23 +1702,73 @@ function generateAuthToken() {
|
|
|
1604
1702
|
}
|
|
1605
1703
|
|
|
1606
1704
|
// src/server/file-handlers.ts
|
|
1705
|
+
async function resolveFileInsideProject(projectRoot, filePath) {
|
|
1706
|
+
const resolved = path4.resolve(projectRoot, filePath);
|
|
1707
|
+
if (!isPathInside(projectRoot, resolved)) {
|
|
1708
|
+
throw new Error("Path outside project root");
|
|
1709
|
+
}
|
|
1710
|
+
const { parent, base } = splitParentAndBase(resolved);
|
|
1711
|
+
const realProjectRoot = await fs4.realpath(projectRoot);
|
|
1712
|
+
const realParent = await realpathAllowMissing(parent);
|
|
1713
|
+
const realFull = path4.join(realParent, base);
|
|
1714
|
+
if (!isPathInside(realProjectRoot, realFull)) {
|
|
1715
|
+
throw new Error("Path outside project root");
|
|
1716
|
+
}
|
|
1717
|
+
return realFull;
|
|
1718
|
+
}
|
|
1719
|
+
function splitParentAndBase(p) {
|
|
1720
|
+
const base = path4.basename(p);
|
|
1721
|
+
const parent = path4.dirname(p);
|
|
1722
|
+
return { parent, base };
|
|
1723
|
+
}
|
|
1724
|
+
async function realpathAllowMissing(p) {
|
|
1725
|
+
try {
|
|
1726
|
+
return await fs4.realpath(p);
|
|
1727
|
+
} catch (err) {
|
|
1728
|
+
if (err.code !== "ENOENT") throw err;
|
|
1729
|
+
}
|
|
1730
|
+
const segments = [];
|
|
1731
|
+
let cursor = p;
|
|
1732
|
+
while (true) {
|
|
1733
|
+
const parent = path4.dirname(cursor);
|
|
1734
|
+
if (parent === cursor) {
|
|
1735
|
+
throw new Error("Path outside project root");
|
|
1736
|
+
}
|
|
1737
|
+
segments.unshift(path4.basename(cursor));
|
|
1738
|
+
try {
|
|
1739
|
+
const realParent = await fs4.realpath(parent);
|
|
1740
|
+
return path4.join(realParent, ...segments);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
if (err.code !== "ENOENT") throw err;
|
|
1743
|
+
cursor = parent;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1607
1747
|
async function handleFilesTree(ws, msg, projectRoot) {
|
|
1608
1748
|
const payload = msg.payload;
|
|
1609
1749
|
const rawPath = payload?.path?.trim();
|
|
1610
|
-
|
|
1611
|
-
|
|
1750
|
+
let treeRoot;
|
|
1751
|
+
let realProjectRoot;
|
|
1752
|
+
try {
|
|
1753
|
+
if (rawPath && rawPath !== ".") {
|
|
1754
|
+
treeRoot = await resolveWorkingDirInsideProject(projectRoot, rawPath);
|
|
1755
|
+
} else {
|
|
1756
|
+
treeRoot = projectRoot;
|
|
1757
|
+
}
|
|
1758
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1759
|
+
} catch {
|
|
1612
1760
|
send(ws, {
|
|
1613
1761
|
type: "files.tree",
|
|
1614
1762
|
payload: { root: projectRoot, tree: [], error: "Path outside project root" }
|
|
1615
1763
|
});
|
|
1616
1764
|
return;
|
|
1617
1765
|
}
|
|
1618
|
-
const pathPrefix = treeRoot === projectRoot ? "" : (
|
|
1766
|
+
const pathPrefix = treeRoot === projectRoot ? "" : (path4.relative(projectRoot, treeRoot) + "/").replace(/\\/g, "/");
|
|
1619
1767
|
async function buildTree(dir, rel, depth) {
|
|
1620
1768
|
if (depth > 10) return [];
|
|
1621
1769
|
let entries = [];
|
|
1622
1770
|
try {
|
|
1623
|
-
entries = await
|
|
1771
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1624
1772
|
} catch {
|
|
1625
1773
|
return [];
|
|
1626
1774
|
}
|
|
@@ -1632,11 +1780,20 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1632
1780
|
for (const e of entries) {
|
|
1633
1781
|
if (isHiddenEntry(e.name)) continue;
|
|
1634
1782
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1635
|
-
const childAbs =
|
|
1783
|
+
const childAbs = path4.join(dir, e.name);
|
|
1636
1784
|
const childPath = pathPrefix + childRel;
|
|
1637
1785
|
if (e.isDirectory()) {
|
|
1638
1786
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1639
|
-
|
|
1787
|
+
let realChild;
|
|
1788
|
+
try {
|
|
1789
|
+
realChild = await fs4.realpath(childAbs);
|
|
1790
|
+
} catch {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
const children = await buildTree(realChild, childRel, depth + 1);
|
|
1640
1797
|
nodes.push({ name: e.name, path: childPath, type: "directory", children });
|
|
1641
1798
|
} else if (e.isFile()) {
|
|
1642
1799
|
nodes.push({ name: e.name, path: childPath, type: "file" });
|
|
@@ -1646,10 +1803,10 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1646
1803
|
}
|
|
1647
1804
|
try {
|
|
1648
1805
|
const tree = await buildTree(treeRoot, "", 0);
|
|
1649
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1806
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1650
1807
|
send(ws, { type: "files.tree", payload: { root: rootLabel, tree } });
|
|
1651
1808
|
} catch (err) {
|
|
1652
|
-
const rootLabel = treeRoot === projectRoot ? projectRoot :
|
|
1809
|
+
const rootLabel = treeRoot === projectRoot ? projectRoot : path4.relative(projectRoot, treeRoot) || ".";
|
|
1653
1810
|
send(ws, {
|
|
1654
1811
|
type: "files.tree",
|
|
1655
1812
|
payload: { root: rootLabel, tree: [], error: errMessage(err) }
|
|
@@ -1658,13 +1815,15 @@ async function handleFilesTree(ws, msg, projectRoot) {
|
|
|
1658
1815
|
}
|
|
1659
1816
|
async function handleFilesRead(ws, msg, projectRoot) {
|
|
1660
1817
|
const { filePath } = msg.payload;
|
|
1661
|
-
|
|
1662
|
-
|
|
1818
|
+
let realResolved;
|
|
1819
|
+
try {
|
|
1820
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1821
|
+
} catch {
|
|
1663
1822
|
send(ws, { type: "files.read", payload: { filePath, content: "", error: "Forbidden" } });
|
|
1664
1823
|
return;
|
|
1665
1824
|
}
|
|
1666
1825
|
try {
|
|
1667
|
-
const content = await
|
|
1826
|
+
const content = await fs4.readFile(realResolved, "utf8");
|
|
1668
1827
|
send(ws, { type: "files.read", payload: { filePath, content } });
|
|
1669
1828
|
} catch (err) {
|
|
1670
1829
|
send(ws, {
|
|
@@ -1675,16 +1834,18 @@ async function handleFilesRead(ws, msg, projectRoot) {
|
|
|
1675
1834
|
}
|
|
1676
1835
|
async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
1677
1836
|
const { filePath, content } = msg.payload;
|
|
1678
|
-
|
|
1679
|
-
|
|
1837
|
+
let realResolved;
|
|
1838
|
+
try {
|
|
1839
|
+
realResolved = await resolveFileInsideProject(projectRoot, filePath);
|
|
1840
|
+
} catch {
|
|
1680
1841
|
send(ws, { type: "files.written", payload: { filePath, success: false, error: "Forbidden" } });
|
|
1681
1842
|
return;
|
|
1682
1843
|
}
|
|
1683
1844
|
try {
|
|
1684
|
-
await atomicWrite(
|
|
1845
|
+
await atomicWrite(realResolved, content);
|
|
1685
1846
|
send(ws, { type: "files.written", payload: { filePath, success: true } });
|
|
1686
1847
|
if (opts.onWritten) {
|
|
1687
|
-
void Promise.resolve(opts.onWritten(
|
|
1848
|
+
void Promise.resolve(opts.onWritten(realResolved)).catch(() => void 0);
|
|
1688
1849
|
}
|
|
1689
1850
|
} catch (err) {
|
|
1690
1851
|
send(ws, {
|
|
@@ -1696,8 +1857,16 @@ async function handleFilesWrite(ws, msg, projectRoot, opts = {}) {
|
|
|
1696
1857
|
async function handleFilesList(ws, msg, projectRoot) {
|
|
1697
1858
|
const payload = msg.payload ?? {};
|
|
1698
1859
|
const limit = payload.limit ?? 50;
|
|
1699
|
-
|
|
1700
|
-
|
|
1860
|
+
let listRoot;
|
|
1861
|
+
let realProjectRoot;
|
|
1862
|
+
try {
|
|
1863
|
+
if (payload.path) {
|
|
1864
|
+
listRoot = await resolveWorkingDirInsideProject(projectRoot, payload.path);
|
|
1865
|
+
} else {
|
|
1866
|
+
listRoot = projectRoot;
|
|
1867
|
+
}
|
|
1868
|
+
realProjectRoot = await fs4.realpath(projectRoot);
|
|
1869
|
+
} catch {
|
|
1701
1870
|
send(ws, { type: "files.list", payload: { files: [] } });
|
|
1702
1871
|
return;
|
|
1703
1872
|
}
|
|
@@ -1706,7 +1875,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1706
1875
|
if (depth > 8 || results.length >= 600) return;
|
|
1707
1876
|
let entries = [];
|
|
1708
1877
|
try {
|
|
1709
|
-
entries = await
|
|
1878
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
1710
1879
|
} catch {
|
|
1711
1880
|
return;
|
|
1712
1881
|
}
|
|
@@ -1716,7 +1885,16 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1716
1885
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1717
1886
|
if (e.isDirectory()) {
|
|
1718
1887
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
1719
|
-
|
|
1888
|
+
let realChild;
|
|
1889
|
+
try {
|
|
1890
|
+
realChild = await fs4.realpath(path4.join(dir, e.name));
|
|
1891
|
+
} catch {
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
if (!isPathInside(realProjectRoot, realChild)) {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
await walk(realChild, childRel, depth + 1);
|
|
1720
1898
|
} else if (e.isFile()) {
|
|
1721
1899
|
results.push(childRel);
|
|
1722
1900
|
}
|
|
@@ -1730,7 +1908,7 @@ async function handleFilesList(ws, msg, projectRoot) {
|
|
|
1730
1908
|
}
|
|
1731
1909
|
|
|
1732
1910
|
// src/server/completion-handlers.ts
|
|
1733
|
-
import * as
|
|
1911
|
+
import * as path5 from "path";
|
|
1734
1912
|
import { searchCodebaseIndex } from "@wrongstack/tools/codebase-index/index";
|
|
1735
1913
|
var MAX_PREFIX_CHARS = 12e3;
|
|
1736
1914
|
var MAX_SUFFIX_CHARS = 4e3;
|
|
@@ -1805,8 +1983,8 @@ async function handleCompletionRequest(ws, msg, opts) {
|
|
|
1805
1983
|
return;
|
|
1806
1984
|
}
|
|
1807
1985
|
const payload = parsed.payload;
|
|
1808
|
-
const projectRoot =
|
|
1809
|
-
const resolved =
|
|
1986
|
+
const projectRoot = path5.resolve(opts.projectRoot);
|
|
1987
|
+
const resolved = path5.resolve(projectRoot, payload.filePath);
|
|
1810
1988
|
if (!isInside2(projectRoot, resolved)) {
|
|
1811
1989
|
send(ws, {
|
|
1812
1990
|
type: "completion.result",
|
|
@@ -2205,7 +2383,7 @@ function buildSearchQuery(linePrefix, filePath) {
|
|
|
2205
2383
|
if (memberMatch?.[1]) return memberMatch[1];
|
|
2206
2384
|
const token = linePrefix.match(/([A-Za-z_$][\w$]*)$/)?.[1];
|
|
2207
2385
|
if (token && token.length >= 2) return token;
|
|
2208
|
-
return
|
|
2386
|
+
return path5.basename(filePath, path5.extname(filePath));
|
|
2209
2387
|
}
|
|
2210
2388
|
function currentLinePrefix(prefix) {
|
|
2211
2389
|
const idx = Math.max(prefix.lastIndexOf("\n"), prefix.lastIndexOf("\r"));
|
|
@@ -2235,7 +2413,7 @@ function head(value, max) {
|
|
|
2235
2413
|
return value.length <= max ? value : value.slice(0, max);
|
|
2236
2414
|
}
|
|
2237
2415
|
function isInside2(root, target) {
|
|
2238
|
-
return target === root || target.startsWith(root +
|
|
2416
|
+
return target === root || target.startsWith(root + path5.sep);
|
|
2239
2417
|
}
|
|
2240
2418
|
|
|
2241
2419
|
// src/server/memory-handlers.ts
|
|
@@ -2489,8 +2667,8 @@ async function handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry) {
|
|
|
2489
2667
|
}
|
|
2490
2668
|
|
|
2491
2669
|
// src/server/skills-handlers.ts
|
|
2492
|
-
import { promises as
|
|
2493
|
-
import
|
|
2670
|
+
import { promises as fs5 } from "fs";
|
|
2671
|
+
import path6 from "path";
|
|
2494
2672
|
import JSZip from "jszip";
|
|
2495
2673
|
import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
|
|
2496
2674
|
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
@@ -2561,19 +2739,19 @@ async function handleSkillsContent(ws, ctx, msg) {
|
|
|
2561
2739
|
send(ws, { type: "skills.content", payload: { name: name2, body: "", path: "", source, relatedFiles: [], references: [], error: `Skill "${name2}" not found` } });
|
|
2562
2740
|
return;
|
|
2563
2741
|
}
|
|
2564
|
-
const body = await
|
|
2565
|
-
const skillDir =
|
|
2742
|
+
const body = await fs5.readFile(entry.path, "utf8");
|
|
2743
|
+
const skillDir = path6.dirname(entry.path);
|
|
2566
2744
|
let relatedFiles = [];
|
|
2567
2745
|
try {
|
|
2568
|
-
const files = await
|
|
2569
|
-
relatedFiles = files.filter((f) => f !==
|
|
2746
|
+
const files = await fs5.readdir(skillDir);
|
|
2747
|
+
relatedFiles = files.filter((f) => f !== path6.basename(entry.path)).map((f) => path6.join(skillDir, f));
|
|
2570
2748
|
} catch {
|
|
2571
2749
|
}
|
|
2572
2750
|
const nameLower = name2.toLowerCase();
|
|
2573
2751
|
const refResults = await Promise.all(
|
|
2574
2752
|
entries.filter((e) => e.name.toLowerCase() !== nameLower).map(async (e) => {
|
|
2575
2753
|
try {
|
|
2576
|
-
const content = await
|
|
2754
|
+
const content = await fs5.readFile(e.path, "utf8");
|
|
2577
2755
|
return [e.name, content.toLowerCase().includes(nameLower)];
|
|
2578
2756
|
} catch {
|
|
2579
2757
|
return [e.name, false];
|
|
@@ -2663,14 +2841,14 @@ async function handleSkillsCreate(ws, ctx, msg) {
|
|
|
2663
2841
|
}
|
|
2664
2842
|
const createPayload = parsed.value;
|
|
2665
2843
|
try {
|
|
2666
|
-
const targetDir = createPayload.scope === "global" ?
|
|
2844
|
+
const targetDir = createPayload.scope === "global" ? path6.join(wstackGlobalRoot(), "skills", createPayload.name.trim()) : path6.join(ctx.projectRoot, ".wrongstack", "skills", createPayload.name.trim());
|
|
2667
2845
|
try {
|
|
2668
|
-
await
|
|
2846
|
+
await fs5.access(targetDir);
|
|
2669
2847
|
send(ws, { type: "skills.created", payload: { success: false, error: `Skill "${createPayload.name}" already exists` } });
|
|
2670
2848
|
return;
|
|
2671
2849
|
} catch {
|
|
2672
2850
|
}
|
|
2673
|
-
await
|
|
2851
|
+
await fs5.mkdir(targetDir, { recursive: true });
|
|
2674
2852
|
const lines = createPayload.description.trim().split("\n");
|
|
2675
2853
|
const firstLine = lines[0].trim();
|
|
2676
2854
|
const bodyLines = lines.slice(1).map((l) => l.trim()).filter(Boolean);
|
|
@@ -2718,13 +2896,13 @@ ${trigger}
|
|
|
2718
2896
|
"- `bug-hunter` \u2014 for systematic bug detection patterns",
|
|
2719
2897
|
"- `output-standards` \u2014 for standardized `<next_steps>` formatting"
|
|
2720
2898
|
].join("\n");
|
|
2721
|
-
await atomicWrite2(
|
|
2899
|
+
await atomicWrite2(path6.join(targetDir, "SKILL.md"), skillContent);
|
|
2722
2900
|
send(ws, {
|
|
2723
2901
|
type: "skills.created",
|
|
2724
2902
|
payload: {
|
|
2725
2903
|
success: true,
|
|
2726
2904
|
error: null,
|
|
2727
|
-
skill: { name: createPayload.name.trim(), path:
|
|
2905
|
+
skill: { name: createPayload.name.trim(), path: path6.join(targetDir, "SKILL.md"), scope: createPayload.scope }
|
|
2728
2906
|
}
|
|
2729
2907
|
});
|
|
2730
2908
|
} catch (err) {
|
|
@@ -2788,23 +2966,23 @@ import {
|
|
|
2788
2966
|
Agent,
|
|
2789
2967
|
AutoCompactionMiddleware,
|
|
2790
2968
|
Context,
|
|
2791
|
-
DefaultMemoryStore
|
|
2792
|
-
DefaultModeStore
|
|
2969
|
+
DefaultMemoryStore,
|
|
2970
|
+
DefaultModeStore,
|
|
2793
2971
|
DefaultModelsRegistry,
|
|
2794
2972
|
DefaultSessionReader,
|
|
2795
|
-
DefaultSessionStore as
|
|
2796
|
-
DefaultSkillLoader
|
|
2797
|
-
DefaultSystemPromptBuilder as
|
|
2798
|
-
DefaultTokenCounter
|
|
2973
|
+
DefaultSessionStore as DefaultSessionStore2,
|
|
2974
|
+
DefaultSkillLoader,
|
|
2975
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder3,
|
|
2976
|
+
DefaultTokenCounter,
|
|
2799
2977
|
AnnotationsStore,
|
|
2800
2978
|
CollaborationBus,
|
|
2801
2979
|
collabPauseMiddleware,
|
|
2802
2980
|
collabInjectMiddleware,
|
|
2803
2981
|
estimateRequestTokensCalibrated,
|
|
2804
2982
|
EventBus,
|
|
2805
|
-
createStrategyCompactor
|
|
2983
|
+
createStrategyCompactor,
|
|
2806
2984
|
ProviderRegistry,
|
|
2807
|
-
TOKENS
|
|
2985
|
+
TOKENS,
|
|
2808
2986
|
ToolRegistry,
|
|
2809
2987
|
atomicWrite as atomicWrite6,
|
|
2810
2988
|
createDefaultPipelines,
|
|
@@ -2813,6 +2991,7 @@ import {
|
|
|
2813
2991
|
DEFAULT_CONTEXT_WINDOW_MODE_ID as DEFAULT_CONTEXT_WINDOW_MODE_ID2,
|
|
2814
2992
|
DEFAULT_SESSION_PRUNE_DAYS,
|
|
2815
2993
|
DEFAULT_TOOLS_CONFIG,
|
|
2994
|
+
applyToolDescriptionModes,
|
|
2816
2995
|
resolveContextWindowPolicy as resolveContextWindowPolicy2,
|
|
2817
2996
|
enhanceUserPrompt,
|
|
2818
2997
|
gatedEnhancerReasoning,
|
|
@@ -2822,109 +3001,10 @@ import {
|
|
|
2822
3001
|
import { ToolExecutor } from "@wrongstack/core/execution";
|
|
2823
3002
|
import { decryptConfigSecrets as decryptConfigSecrets2, encryptConfigSecrets as encryptConfigSecrets2 } from "@wrongstack/core/security";
|
|
2824
3003
|
import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
|
|
2825
|
-
import { builtinToolsPack, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
3004
|
+
import { builtinToolsPack, configureExecPolicy, ensureSessionShell, forgetTool, rememberTool, searchMemoryTool, relatedMemoryTool } from "@wrongstack/tools";
|
|
2826
3005
|
import { MCPRegistry } from "@wrongstack/mcp";
|
|
2827
3006
|
import { WebSocket as WebSocket2, WebSocketServer } from "ws";
|
|
2828
|
-
|
|
2829
|
-
// ../runtime/src/container.ts
|
|
2830
|
-
import {
|
|
2831
|
-
Container,
|
|
2832
|
-
DefaultConfigStore,
|
|
2833
|
-
DefaultErrorHandler,
|
|
2834
|
-
DefaultMemoryStore,
|
|
2835
|
-
DefaultModeStore,
|
|
2836
|
-
DefaultPermissionPolicy,
|
|
2837
|
-
DefaultRetryPolicy,
|
|
2838
|
-
DefaultSecretScrubber,
|
|
2839
|
-
DefaultSessionStore,
|
|
2840
|
-
DefaultSkillLoader,
|
|
2841
|
-
DefaultSystemPromptBuilder,
|
|
2842
|
-
DefaultTokenCounter,
|
|
2843
|
-
createStrategyCompactor,
|
|
2844
|
-
buildRecoveryStrategies,
|
|
2845
|
-
TOKENS
|
|
2846
|
-
} from "@wrongstack/core";
|
|
2847
|
-
function createDefaultContainer(opts) {
|
|
2848
|
-
const { config, wpaths, logger, modelsRegistry } = opts;
|
|
2849
|
-
const container = new Container();
|
|
2850
|
-
const configStore = new DefaultConfigStore(config);
|
|
2851
|
-
container.bind(TOKENS.ConfigStore, () => configStore);
|
|
2852
|
-
container.bind(TOKENS.Logger, () => logger);
|
|
2853
|
-
container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
|
|
2854
|
-
container.bind(TOKENS.RetryPolicy, () => new DefaultRetryPolicy());
|
|
2855
|
-
container.bind(
|
|
2856
|
-
TOKENS.ErrorHandler,
|
|
2857
|
-
() => new DefaultErrorHandler(
|
|
2858
|
-
buildRecoveryStrategies({
|
|
2859
|
-
compactor: container.resolve(TOKENS.Compactor),
|
|
2860
|
-
modelsRegistry
|
|
2861
|
-
})
|
|
2862
|
-
)
|
|
2863
|
-
);
|
|
2864
|
-
container.bind(TOKENS.ModelsRegistry, () => modelsRegistry);
|
|
2865
|
-
container.bind(
|
|
2866
|
-
TOKENS.TokenCounter,
|
|
2867
|
-
() => new DefaultTokenCounter({ registry: modelsRegistry, providerId: config.provider })
|
|
2868
|
-
);
|
|
2869
|
-
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
2870
|
-
container.bind(TOKENS.ModeStore, () => modeStore);
|
|
2871
|
-
container.bind(
|
|
2872
|
-
TOKENS.SessionStore,
|
|
2873
|
-
() => new DefaultSessionStore({
|
|
2874
|
-
dir: wpaths.projectSessions,
|
|
2875
|
-
// Scrub secrets out of persisted user/model turns (F-06). Tool output
|
|
2876
|
-
// is already scrubbed by the executor.
|
|
2877
|
-
secretScrubber: container.resolve(TOKENS.SecretScrubber)
|
|
2878
|
-
})
|
|
2879
|
-
);
|
|
2880
|
-
const memoryStore = new DefaultMemoryStore({ paths: wpaths, events: opts.events });
|
|
2881
|
-
container.bind(TOKENS.MemoryStore, () => memoryStore);
|
|
2882
|
-
const skillLoader = new DefaultSkillLoader({ paths: wpaths, bundledDir: opts.bundledSkillsDir });
|
|
2883
|
-
container.bind(TOKENS.SkillLoader, () => skillLoader);
|
|
2884
|
-
if (opts.systemPrompt) {
|
|
2885
|
-
container.bind(
|
|
2886
|
-
TOKENS.SystemPromptBuilder,
|
|
2887
|
-
() => new DefaultSystemPromptBuilder(opts.systemPrompt)
|
|
2888
|
-
);
|
|
2889
|
-
}
|
|
2890
|
-
container.bind(
|
|
2891
|
-
TOKENS.PermissionPolicy,
|
|
2892
|
-
() => {
|
|
2893
|
-
const policyOptions = {
|
|
2894
|
-
trustFile: wpaths.projectTrust,
|
|
2895
|
-
yolo: opts.permission?.yolo ?? false,
|
|
2896
|
-
yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
|
|
2897
|
-
confirmDestructive: opts.permission?.confirmDestructive ?? false
|
|
2898
|
-
};
|
|
2899
|
-
if (opts.permission?.promptDelegate !== void 0) {
|
|
2900
|
-
policyOptions.promptDelegate = opts.permission.promptDelegate;
|
|
2901
|
-
}
|
|
2902
|
-
return new DefaultPermissionPolicy(policyOptions);
|
|
2903
|
-
}
|
|
2904
|
-
);
|
|
2905
|
-
container.bind(
|
|
2906
|
-
TOKENS.Compactor,
|
|
2907
|
-
() => (
|
|
2908
|
-
// Strategy comes from config.context.strategy: 'hybrid' (default, lossless
|
|
2909
|
-
// rules, no LLM), 'intelligent' (LLM summarization), or 'selective'
|
|
2910
|
-
// (LLM-driven selection). The LLM strategies resolve their provider from
|
|
2911
|
-
// ctx at compact()-time, so binding here (before context.provider exists)
|
|
2912
|
-
// is safe. preserveK / eliseThreshold are class-level fallbacks; the active
|
|
2913
|
-
// ContextWindowPolicy in ctx.meta normally overrides both at runtime.
|
|
2914
|
-
// eliseThreshold is a TOKEN COUNT — a previous value of 0.7 elided
|
|
2915
|
-
// essentially every tool_result (anything > 1 token).
|
|
2916
|
-
createStrategyCompactor({
|
|
2917
|
-
strategy: config.context?.strategy,
|
|
2918
|
-
preserveK: opts.compactor?.preserveK ?? 10,
|
|
2919
|
-
eliseThreshold: opts.compactor?.eliseThreshold ?? 2e3,
|
|
2920
|
-
smart: true,
|
|
2921
|
-
summarizerModel: config.context?.summarizerModel,
|
|
2922
|
-
llmSelector: config.context?.llmSelector
|
|
2923
|
-
})
|
|
2924
|
-
)
|
|
2925
|
-
);
|
|
2926
|
-
return container;
|
|
2927
|
-
}
|
|
3007
|
+
import { createDefaultContainer, makeLightSubagentFactory } from "@wrongstack/runtime";
|
|
2928
3008
|
|
|
2929
3009
|
// src/server/boot.ts
|
|
2930
3010
|
import {
|
|
@@ -2944,6 +3024,7 @@ function patchConfig(config, updates) {
|
|
|
2944
3024
|
import { spawnSync } from "child_process";
|
|
2945
3025
|
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
2946
3026
|
import {
|
|
3027
|
+
assignNickname,
|
|
2947
3028
|
AutoPhasePlanner,
|
|
2948
3029
|
PhaseGraphBuilder,
|
|
2949
3030
|
PhaseOrchestrator,
|
|
@@ -2981,6 +3062,8 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
2981
3062
|
abort = null;
|
|
2982
3063
|
/** Optional per-phase git-worktree isolation (lazily created at start). */
|
|
2983
3064
|
worktrees = null;
|
|
3065
|
+
/** Per-run worker identities so the board can show "who is on what". */
|
|
3066
|
+
usedNicknames = /* @__PURE__ */ new Set();
|
|
2984
3067
|
addClient(ws) {
|
|
2985
3068
|
const client = { ws, id: crypto.randomUUID() };
|
|
2986
3069
|
this.clients.add(client);
|
|
@@ -3023,6 +3106,29 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3023
3106
|
await this.handleTaskStatusChange(taskId, status);
|
|
3024
3107
|
break;
|
|
3025
3108
|
}
|
|
3109
|
+
case "autophase.moveTask": {
|
|
3110
|
+
const { taskId, toPhaseId } = msg.payload;
|
|
3111
|
+
if (this.orchestrator?.moveTask(taskId, toPhaseId)) this.afterBoardMutation();
|
|
3112
|
+
break;
|
|
3113
|
+
}
|
|
3114
|
+
case "autophase.assignTask": {
|
|
3115
|
+
const { taskId, agentId, agentName } = msg.payload;
|
|
3116
|
+
if (this.orchestrator?.setTaskAssignee(taskId, agentId, agentName)) this.afterBoardMutation();
|
|
3117
|
+
break;
|
|
3118
|
+
}
|
|
3119
|
+
case "autophase.addTask": {
|
|
3120
|
+
const { phaseId, title, description, type, priority } = msg.payload;
|
|
3121
|
+
if (title?.trim() && this.orchestrator?.addTask(phaseId, { title: title.trim(), description, type, priority })) {
|
|
3122
|
+
this.afterBoardMutation();
|
|
3123
|
+
}
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
3126
|
+
case "autophase.retryTask":
|
|
3127
|
+
case "autophase.runTask": {
|
|
3128
|
+
const { taskId } = msg.payload;
|
|
3129
|
+
if (this.orchestrator?.requeueTask(taskId)) this.afterBoardMutation();
|
|
3130
|
+
break;
|
|
3131
|
+
}
|
|
3026
3132
|
case "autophase.toggleAutonomous": {
|
|
3027
3133
|
const autonomous = msg.payload?.autonomous ?? !this.graph?.autonomous;
|
|
3028
3134
|
if (this.graph) {
|
|
@@ -3150,6 +3256,13 @@ var AutoPhaseWebSocketHandler = class {
|
|
|
3150
3256
|
return this.defaultPhases();
|
|
3151
3257
|
}
|
|
3152
3258
|
async executeTaskWithAgent(task, phaseId, env) {
|
|
3259
|
+
if (!task.assignee) {
|
|
3260
|
+
const nick = assignNickname("executor", this.usedNicknames);
|
|
3261
|
+
this.usedNicknames.add(nick.key);
|
|
3262
|
+
task.assignee = nick.display.replace(/\s*\([^)]*\)\s*$/, "");
|
|
3263
|
+
task.updatedAt = Date.now();
|
|
3264
|
+
this.broadcastState();
|
|
3265
|
+
}
|
|
3153
3266
|
const prompt = `Execute task: ${task.title}
|
|
3154
3267
|
|
|
3155
3268
|
Description: ${task.description}
|
|
@@ -3165,6 +3278,11 @@ Type: ${task.type}`;
|
|
|
3165
3278
|
this.context.cwd = prevCwd;
|
|
3166
3279
|
}
|
|
3167
3280
|
}
|
|
3281
|
+
/** Persist + broadcast after an interactive board mutation. */
|
|
3282
|
+
afterBoardMutation() {
|
|
3283
|
+
if (this.graph) void this.store.save(this.graph);
|
|
3284
|
+
this.broadcastState();
|
|
3285
|
+
}
|
|
3168
3286
|
async handleTaskStatusChange(taskId, status) {
|
|
3169
3287
|
if (!this.graph) return;
|
|
3170
3288
|
for (const phase of this.graph.phases.values()) {
|
|
@@ -3208,23 +3326,7 @@ Type: ${task.type}`;
|
|
|
3208
3326
|
(sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
|
|
3209
3327
|
0
|
|
3210
3328
|
);
|
|
3211
|
-
const
|
|
3212
|
-
id: p.id,
|
|
3213
|
-
name: p.name,
|
|
3214
|
-
description: p.description,
|
|
3215
|
-
status: p.status,
|
|
3216
|
-
priority: p.priority,
|
|
3217
|
-
estimateHours: p.estimateHours,
|
|
3218
|
-
actualDurationMs: p.actualDurationMs,
|
|
3219
|
-
startedAt: p.startedAt,
|
|
3220
|
-
completedAt: p.completedAt,
|
|
3221
|
-
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,
|
|
3222
|
-
taskCount: p.taskGraph.nodes.size,
|
|
3223
|
-
completedTasks: Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "completed").length,
|
|
3224
|
-
assignedAgents: p.assignedAgents,
|
|
3225
|
-
isActive: p.id === currentActiveId
|
|
3226
|
-
}));
|
|
3227
|
-
const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
|
|
3329
|
+
const mapTask = (t) => ({
|
|
3228
3330
|
id: t.id,
|
|
3229
3331
|
title: t.title,
|
|
3230
3332
|
description: t.description,
|
|
@@ -3237,8 +3339,39 @@ Type: ${task.type}`;
|
|
|
3237
3339
|
tags: t.tags || [],
|
|
3238
3340
|
startedAt: t.startedAt,
|
|
3239
3341
|
completedAt: t.completedAt
|
|
3240
|
-
})
|
|
3342
|
+
});
|
|
3343
|
+
const phaseItems = phases.map((p) => {
|
|
3344
|
+
const nodes = Array.from(p.taskGraph.nodes.values());
|
|
3345
|
+
const done = nodes.filter((t) => t.status === "completed").length;
|
|
3346
|
+
return {
|
|
3347
|
+
id: p.id,
|
|
3348
|
+
name: p.name,
|
|
3349
|
+
description: p.description,
|
|
3350
|
+
status: p.status,
|
|
3351
|
+
priority: p.priority,
|
|
3352
|
+
estimateHours: p.estimateHours,
|
|
3353
|
+
actualDurationMs: p.actualDurationMs,
|
|
3354
|
+
startedAt: p.startedAt,
|
|
3355
|
+
completedAt: p.completedAt,
|
|
3356
|
+
progressPercent: nodes.length > 0 ? Math.round(done / nodes.length * 100) : 0,
|
|
3357
|
+
taskCount: nodes.length,
|
|
3358
|
+
completedTasks: done,
|
|
3359
|
+
assignedAgents: p.assignedAgents,
|
|
3360
|
+
isActive: p.id === currentActiveId,
|
|
3361
|
+
// Every phase carries its full task list so the board can render each
|
|
3362
|
+
// phase as a column (not just the selected one).
|
|
3363
|
+
tasks: nodes.map(mapTask)
|
|
3364
|
+
};
|
|
3365
|
+
});
|
|
3366
|
+
const taskItems = activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map(mapTask) : [];
|
|
3241
3367
|
const completedPhases = phases.filter((p) => p.status === "completed").length;
|
|
3368
|
+
const failedPhases = phases.filter((p) => p.status === "failed").length;
|
|
3369
|
+
const failedTasks = phases.reduce(
|
|
3370
|
+
(sum, p) => sum + Array.from(p.taskGraph.nodes.values()).filter((t) => t.status === "failed").length,
|
|
3371
|
+
0
|
|
3372
|
+
);
|
|
3373
|
+
const lastFailed = phases.filter((p) => p.status === "failed").sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0];
|
|
3374
|
+
const lastError = lastFailed ? `${lastFailed.name}: ${lastFailed.metadata?.integrationError ?? "phase failed"}` : null;
|
|
3242
3375
|
return {
|
|
3243
3376
|
title: this.graph.title,
|
|
3244
3377
|
phases: phaseItems,
|
|
@@ -3247,7 +3380,18 @@ Type: ${task.type}`;
|
|
|
3247
3380
|
overallPercent: phases.length > 0 ? Math.round(completedPhases / phases.length * 100) : 0,
|
|
3248
3381
|
autonomous: this.graph.autonomous,
|
|
3249
3382
|
totalTasks,
|
|
3250
|
-
completedTasks
|
|
3383
|
+
completedTasks,
|
|
3384
|
+
// Structured progress + lastError consumed by the autophase store (were
|
|
3385
|
+
// defined client-side but never sent, so they stayed null on the board).
|
|
3386
|
+
progress: {
|
|
3387
|
+
totalPhases: phases.length,
|
|
3388
|
+
completed: completedPhases,
|
|
3389
|
+
failed: failedPhases,
|
|
3390
|
+
totalTasks,
|
|
3391
|
+
completedTasks,
|
|
3392
|
+
failedTasks
|
|
3393
|
+
},
|
|
3394
|
+
lastError
|
|
3251
3395
|
};
|
|
3252
3396
|
}
|
|
3253
3397
|
sendState(client) {
|
|
@@ -3270,6 +3414,510 @@ Type: ${task.type}`;
|
|
|
3270
3414
|
}
|
|
3271
3415
|
};
|
|
3272
3416
|
|
|
3417
|
+
// src/server/specs-ws-handler.ts
|
|
3418
|
+
import {
|
|
3419
|
+
computeTaskProgress,
|
|
3420
|
+
SpecStore,
|
|
3421
|
+
TaskGraphStore
|
|
3422
|
+
} from "@wrongstack/core";
|
|
3423
|
+
var SpecsWebSocketHandler = class {
|
|
3424
|
+
specStore;
|
|
3425
|
+
graphStore;
|
|
3426
|
+
clients = /* @__PURE__ */ new Set();
|
|
3427
|
+
constructor(specsDir, taskGraphsDir) {
|
|
3428
|
+
this.specStore = new SpecStore({ baseDir: specsDir });
|
|
3429
|
+
this.graphStore = new TaskGraphStore({ baseDir: taskGraphsDir });
|
|
3430
|
+
}
|
|
3431
|
+
addClient(ws) {
|
|
3432
|
+
const client = { ws, id: crypto.randomUUID() };
|
|
3433
|
+
this.clients.add(client);
|
|
3434
|
+
ws.on("close", () => this.clients.delete(client));
|
|
3435
|
+
ws.on("error", () => this.clients.delete(client));
|
|
3436
|
+
void this.sendList(client);
|
|
3437
|
+
}
|
|
3438
|
+
async handleMessage(msg) {
|
|
3439
|
+
switch (msg.type) {
|
|
3440
|
+
case "specs.list":
|
|
3441
|
+
await this.broadcastList();
|
|
3442
|
+
break;
|
|
3443
|
+
case "specs.get": {
|
|
3444
|
+
const specId = msg.payload?.specId;
|
|
3445
|
+
if (specId) await this.broadcastDetail(specId);
|
|
3446
|
+
break;
|
|
3447
|
+
}
|
|
3448
|
+
case "specs.taskStatus": {
|
|
3449
|
+
const { graphId, taskId, status } = msg.payload;
|
|
3450
|
+
await this.updateTaskStatus(graphId, taskId, status);
|
|
3451
|
+
break;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
// ── List ──────────────────────────────────────────────────────────────────
|
|
3456
|
+
async buildList() {
|
|
3457
|
+
const [specs, graphs] = await Promise.all([this.specStore.list(), this.graphStore.list()]);
|
|
3458
|
+
return specs.map((s, i) => {
|
|
3459
|
+
const graph = graphs.find((g) => g.specId === s.id);
|
|
3460
|
+
return {
|
|
3461
|
+
id: s.id,
|
|
3462
|
+
// FORGE-style display id (spec-001…). The real UUID stays in `id`.
|
|
3463
|
+
displayId: `spec-${String(i + 1).padStart(3, "0")}`,
|
|
3464
|
+
title: s.title,
|
|
3465
|
+
status: s.status,
|
|
3466
|
+
graphId: graph?.id,
|
|
3467
|
+
total: graph?.nodeCount ?? 0,
|
|
3468
|
+
completed: graph?.completedCount ?? 0
|
|
3469
|
+
};
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
async broadcastList() {
|
|
3473
|
+
this.broadcast({ type: "specs.list", payload: { specs: await this.buildList() } });
|
|
3474
|
+
}
|
|
3475
|
+
async sendList(client) {
|
|
3476
|
+
this.send(client, { type: "specs.list", payload: { specs: await this.buildList() } });
|
|
3477
|
+
}
|
|
3478
|
+
// ── Detail (dependency board) ───────────────────────────────────────────────
|
|
3479
|
+
async broadcastDetail(specId) {
|
|
3480
|
+
const spec = await this.specStore.load(specId);
|
|
3481
|
+
const graph = await this.findGraphForSpec(specId);
|
|
3482
|
+
if (!spec || !graph) {
|
|
3483
|
+
this.broadcast({ type: "specs.detail", payload: { specId, columns: [], notFound: true } });
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
this.broadcast({ type: "specs.detail", payload: this.buildDetail(spec, graph) });
|
|
3487
|
+
}
|
|
3488
|
+
async findGraphForSpec(specId) {
|
|
3489
|
+
const entry = (await this.graphStore.list()).find((g) => g.specId === specId);
|
|
3490
|
+
if (!entry) return null;
|
|
3491
|
+
return this.graphStore.load(entry.id);
|
|
3492
|
+
}
|
|
3493
|
+
buildDetail(spec, graph) {
|
|
3494
|
+
const nodes = Array.from(graph.nodes.values()).sort((a, b) => a.createdAt - b.createdAt);
|
|
3495
|
+
const shortId = /* @__PURE__ */ new Map();
|
|
3496
|
+
nodes.forEach((n, i) => {
|
|
3497
|
+
shortId.set(n.id, `t${String(i + 1).padStart(2, "0")}`);
|
|
3498
|
+
});
|
|
3499
|
+
const blockers = /* @__PURE__ */ new Map();
|
|
3500
|
+
for (const n of nodes) blockers.set(n.id, []);
|
|
3501
|
+
for (const e of graph.edges) {
|
|
3502
|
+
if (e.type === "depends_on") blockers.get(e.to)?.push(e.from);
|
|
3503
|
+
}
|
|
3504
|
+
const statusOf = (id) => graph.nodes.get(id)?.status;
|
|
3505
|
+
const depthCache = /* @__PURE__ */ new Map();
|
|
3506
|
+
const depthOf = (id, seen = /* @__PURE__ */ new Set()) => {
|
|
3507
|
+
const cached = depthCache.get(id);
|
|
3508
|
+
if (cached !== void 0) return cached;
|
|
3509
|
+
if (seen.has(id)) return 0;
|
|
3510
|
+
seen.add(id);
|
|
3511
|
+
const deps2 = blockers.get(id) ?? [];
|
|
3512
|
+
const d = deps2.length === 0 ? 0 : 1 + Math.max(...deps2.map((b) => depthOf(b, seen)));
|
|
3513
|
+
depthCache.set(id, d);
|
|
3514
|
+
return d;
|
|
3515
|
+
};
|
|
3516
|
+
const toBoardTask = (n) => {
|
|
3517
|
+
const deps2 = blockers.get(n.id) ?? [];
|
|
3518
|
+
const allDepsDone = deps2.every((b) => statusOf(b) === "completed");
|
|
3519
|
+
const displayStatus = n.status === "pending" && deps2.length > 0 && allDepsDone ? "queued" : n.status;
|
|
3520
|
+
return {
|
|
3521
|
+
id: n.id,
|
|
3522
|
+
shortId: shortId.get(n.id) ?? n.id.slice(0, 6),
|
|
3523
|
+
title: n.title,
|
|
3524
|
+
description: n.description,
|
|
3525
|
+
priority: n.priority,
|
|
3526
|
+
type: n.type,
|
|
3527
|
+
status: n.status,
|
|
3528
|
+
displayStatus,
|
|
3529
|
+
deps: deps2.map((b) => shortId.get(b) ?? b.slice(0, 6))
|
|
3530
|
+
};
|
|
3531
|
+
};
|
|
3532
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
3533
|
+
for (const n of nodes) {
|
|
3534
|
+
const d = depthOf(n.id);
|
|
3535
|
+
if (!byDepth.has(d)) byDepth.set(d, []);
|
|
3536
|
+
byDepth.get(d)?.push(toBoardTask(n));
|
|
3537
|
+
}
|
|
3538
|
+
const columns = [...byDepth.keys()].sort((a, b) => a - b).map((d) => ({ label: d === 0 ? "Start" : `Phase ${d}`, tasks: byDepth.get(d) ?? [] }));
|
|
3539
|
+
const progress = computeTaskProgress(graph);
|
|
3540
|
+
return {
|
|
3541
|
+
specId: spec.id,
|
|
3542
|
+
graphId: graph.id,
|
|
3543
|
+
title: spec.title,
|
|
3544
|
+
overview: spec.overview,
|
|
3545
|
+
status: spec.status,
|
|
3546
|
+
total: progress.total,
|
|
3547
|
+
completed: progress.completed,
|
|
3548
|
+
running: progress.inProgress,
|
|
3549
|
+
pending: progress.pending,
|
|
3550
|
+
columns
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
async updateTaskStatus(graphId, taskId, status) {
|
|
3554
|
+
const graph = await this.graphStore.load(graphId);
|
|
3555
|
+
const node = graph?.nodes.get(taskId);
|
|
3556
|
+
if (!graph || !node) return;
|
|
3557
|
+
node.status = status;
|
|
3558
|
+
node.updatedAt = Date.now();
|
|
3559
|
+
graph.updatedAt = Date.now();
|
|
3560
|
+
await this.graphStore.save(graph);
|
|
3561
|
+
this.broadcastDetail(graph.specId).catch(() => {
|
|
3562
|
+
});
|
|
3563
|
+
await this.broadcastList();
|
|
3564
|
+
}
|
|
3565
|
+
// ── Transport ───────────────────────────────────────────────────────────────
|
|
3566
|
+
broadcast(msg) {
|
|
3567
|
+
const data = JSON.stringify(msg);
|
|
3568
|
+
for (const client of this.clients) {
|
|
3569
|
+
if (client.ws.readyState === 1) client.ws.send(data);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
send(client, msg) {
|
|
3573
|
+
if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
|
|
3577
|
+
// src/server/sdd-board-ws-handler.ts
|
|
3578
|
+
import { SddBoardStore } from "@wrongstack/core";
|
|
3579
|
+
var CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
3580
|
+
"pause",
|
|
3581
|
+
"resume",
|
|
3582
|
+
"stop",
|
|
3583
|
+
"retry",
|
|
3584
|
+
"retry_all_failed",
|
|
3585
|
+
"reassign",
|
|
3586
|
+
// Per-task model / fallback / verification assignment + stop/delete (drained by start-sdd-run).
|
|
3587
|
+
"set_task_model",
|
|
3588
|
+
"set_task_fallbacks",
|
|
3589
|
+
"set_task_verification",
|
|
3590
|
+
"cancel_task",
|
|
3591
|
+
"delete_task",
|
|
3592
|
+
"split_task",
|
|
3593
|
+
// Lifecycle (pair with a prior `stop`): sweep worktrees / revert merged commits.
|
|
3594
|
+
"cleanup_worktrees",
|
|
3595
|
+
"rollback"
|
|
3596
|
+
]);
|
|
3597
|
+
var SddBoardWebSocketHandler = class {
|
|
3598
|
+
store;
|
|
3599
|
+
clients = /* @__PURE__ */ new Set();
|
|
3600
|
+
latest = null;
|
|
3601
|
+
poll = null;
|
|
3602
|
+
unsub = null;
|
|
3603
|
+
constructor(boardsDir, events) {
|
|
3604
|
+
this.store = new SddBoardStore({ baseDir: boardsDir });
|
|
3605
|
+
if (events) {
|
|
3606
|
+
const handler = (e) => {
|
|
3607
|
+
this.latest = e.snapshot;
|
|
3608
|
+
this.broadcast({ type: "sdd.board.snapshot", payload: e.snapshot });
|
|
3609
|
+
};
|
|
3610
|
+
this.unsub = events.on("sdd.board.snapshot", handler);
|
|
3611
|
+
} else {
|
|
3612
|
+
this.poll = setInterval(() => void this.pollLatest(), 1e3);
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
addClient(ws) {
|
|
3616
|
+
const client = { ws, id: crypto.randomUUID() };
|
|
3617
|
+
this.clients.add(client);
|
|
3618
|
+
ws.on("close", () => this.clients.delete(client));
|
|
3619
|
+
ws.on("error", () => this.clients.delete(client));
|
|
3620
|
+
void this.sendCurrent(client);
|
|
3621
|
+
}
|
|
3622
|
+
async handleMessage(msg) {
|
|
3623
|
+
if (msg.type === "sdd.board.get") {
|
|
3624
|
+
await this.broadcastCurrent();
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3627
|
+
if (msg.type === "sdd.board.list") {
|
|
3628
|
+
const boards = await this.store.list();
|
|
3629
|
+
this.broadcast({ type: "sdd.board.list", payload: { boards } });
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3632
|
+
const action = msg.type.replace(/^sdd\.board\./, "");
|
|
3633
|
+
if (CONTROL_TYPES.has(action)) {
|
|
3634
|
+
const runId = msg.payload?.runId ?? this.latest?.runId ?? (await this.store.list())[0]?.runId;
|
|
3635
|
+
if (runId) {
|
|
3636
|
+
await this.store.appendControl(runId, {
|
|
3637
|
+
ts: Date.now(),
|
|
3638
|
+
type: action,
|
|
3639
|
+
payload: msg.payload
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
dispose() {
|
|
3645
|
+
if (this.poll) clearInterval(this.poll);
|
|
3646
|
+
this.unsub?.();
|
|
3647
|
+
this.poll = null;
|
|
3648
|
+
this.unsub = null;
|
|
3649
|
+
}
|
|
3650
|
+
// ── internal ────────────────────────────────────────────────────────────
|
|
3651
|
+
async pollLatest() {
|
|
3652
|
+
const entry = (await this.store.list())[0];
|
|
3653
|
+
if (!entry) return;
|
|
3654
|
+
if (this.latest && this.latest.updatedAt >= entry.updatedAt && this.latest.runId === entry.runId) {
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
const snap = await this.store.load(entry.runId);
|
|
3658
|
+
if (snap) {
|
|
3659
|
+
this.latest = snap;
|
|
3660
|
+
this.broadcast({ type: "sdd.board.snapshot", payload: snap });
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
async sendCurrent(client) {
|
|
3664
|
+
const snap = this.latest ?? await this.loadLatestFromDisk();
|
|
3665
|
+
if (snap) this.send(client, { type: "sdd.board.snapshot", payload: snap });
|
|
3666
|
+
}
|
|
3667
|
+
async broadcastCurrent() {
|
|
3668
|
+
const snap = this.latest ?? await this.loadLatestFromDisk();
|
|
3669
|
+
if (snap) this.broadcast({ type: "sdd.board.snapshot", payload: snap });
|
|
3670
|
+
}
|
|
3671
|
+
async loadLatestFromDisk() {
|
|
3672
|
+
const entry = (await this.store.list())[0];
|
|
3673
|
+
return entry ? this.store.load(entry.runId) : null;
|
|
3674
|
+
}
|
|
3675
|
+
broadcast(msg) {
|
|
3676
|
+
const data = JSON.stringify(msg);
|
|
3677
|
+
for (const client of this.clients) {
|
|
3678
|
+
if (client.ws.readyState === 1) client.ws.send(data);
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
send(client, msg) {
|
|
3682
|
+
if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
|
|
3683
|
+
}
|
|
3684
|
+
};
|
|
3685
|
+
|
|
3686
|
+
// src/server/sdd-wizard-ws-handler.ts
|
|
3687
|
+
var SddWizardWebSocketHandler = class {
|
|
3688
|
+
constructor(deps2) {
|
|
3689
|
+
this.deps = deps2;
|
|
3690
|
+
}
|
|
3691
|
+
deps;
|
|
3692
|
+
clients = /* @__PURE__ */ new Set();
|
|
3693
|
+
driver = null;
|
|
3694
|
+
/** The agent's most recent question — paired with the next user answer. */
|
|
3695
|
+
lastAgentText = "";
|
|
3696
|
+
/** Guards against overlapping interview turns (one in flight at a time). */
|
|
3697
|
+
busy = false;
|
|
3698
|
+
addClient(ws) {
|
|
3699
|
+
const client = { ws, id: crypto.randomUUID() };
|
|
3700
|
+
this.clients.add(client);
|
|
3701
|
+
ws.on("close", () => this.clients.delete(client));
|
|
3702
|
+
ws.on("error", () => this.clients.delete(client));
|
|
3703
|
+
if (this.driver) this.send(client, this.snapshotMsg());
|
|
3704
|
+
}
|
|
3705
|
+
async handleMessage(msg) {
|
|
3706
|
+
try {
|
|
3707
|
+
switch (msg.type) {
|
|
3708
|
+
case "sdd.spec.start":
|
|
3709
|
+
await this.onStart(String(msg.payload?.goal ?? "").trim());
|
|
3710
|
+
break;
|
|
3711
|
+
case "sdd.spec.message":
|
|
3712
|
+
await this.onMessage(String(msg.payload?.text ?? ""));
|
|
3713
|
+
break;
|
|
3714
|
+
case "sdd.spec.approve":
|
|
3715
|
+
await this.onApprove();
|
|
3716
|
+
break;
|
|
3717
|
+
case "sdd.spec.get":
|
|
3718
|
+
if (this.driver) this.broadcast(this.snapshotMsg());
|
|
3719
|
+
break;
|
|
3720
|
+
case "sdd.run.start":
|
|
3721
|
+
await this.onRunStart({
|
|
3722
|
+
parallelSlots: msg.payload?.parallelSlots,
|
|
3723
|
+
defaultModel: msg.payload?.model,
|
|
3724
|
+
defaultProvider: msg.payload?.provider,
|
|
3725
|
+
fallbackModels: Array.isArray(msg.payload?.fallbackModels) ? msg.payload?.fallbackModels : void 0
|
|
3726
|
+
});
|
|
3727
|
+
break;
|
|
3728
|
+
}
|
|
3729
|
+
} catch (err) {
|
|
3730
|
+
this.busy = false;
|
|
3731
|
+
this.broadcast({
|
|
3732
|
+
type: "sdd.spec.error",
|
|
3733
|
+
payload: { message: err instanceof Error ? err.message : String(err) }
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
// ── message handlers ──────────────────────────────────────────────────────
|
|
3738
|
+
async onStart(goal) {
|
|
3739
|
+
if (!goal) {
|
|
3740
|
+
this.broadcast({ type: "sdd.spec.error", payload: { message: "A goal is required." } });
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
if (this.busy) return;
|
|
3744
|
+
this.driver = this.deps.makeDriver();
|
|
3745
|
+
const prompt = this.driver.start(goal);
|
|
3746
|
+
await this.runTurn(prompt);
|
|
3747
|
+
}
|
|
3748
|
+
async onMessage(text) {
|
|
3749
|
+
if (!this.driver || this.busy) return;
|
|
3750
|
+
if (this.driver.phase() === "questioning" && this.lastAgentText) {
|
|
3751
|
+
this.driver.submitAnswer(this.lastAgentText, text);
|
|
3752
|
+
} else {
|
|
3753
|
+
this.driver.submitAnswer(this.lastAgentText || "(feedback)", text);
|
|
3754
|
+
}
|
|
3755
|
+
await this.runTurn(this.driver.currentPrompt());
|
|
3756
|
+
}
|
|
3757
|
+
async onApprove() {
|
|
3758
|
+
if (!this.driver || this.busy) return;
|
|
3759
|
+
const { phase, prompt } = await this.driver.approve();
|
|
3760
|
+
if (phase === "executing") {
|
|
3761
|
+
this.broadcast(this.snapshotMsg());
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
await this.runTurn(prompt);
|
|
3765
|
+
}
|
|
3766
|
+
async onRunStart(opts) {
|
|
3767
|
+
if (!this.driver) {
|
|
3768
|
+
this.broadcast({ type: "sdd.spec.error", payload: { message: "No active spec session." } });
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
const graph = await this.driver.ensureTaskGraph();
|
|
3772
|
+
if (!graph) {
|
|
3773
|
+
this.broadcast({
|
|
3774
|
+
type: "sdd.spec.error",
|
|
3775
|
+
payload: { message: "No spec yet \u2014 finish the interview before starting a run." }
|
|
3776
|
+
});
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
const { runId } = await this.deps.startRun(this.driver, opts);
|
|
3780
|
+
this.broadcast({ type: "sdd.run.started", payload: { runId } });
|
|
3781
|
+
}
|
|
3782
|
+
// ── internals ───────────────────────────────────────────────────────────
|
|
3783
|
+
/** Run one interview turn against the isolated agent, then ingest + broadcast. */
|
|
3784
|
+
async runTurn(prompt) {
|
|
3785
|
+
this.busy = true;
|
|
3786
|
+
this.broadcast(this.snapshotMsg());
|
|
3787
|
+
try {
|
|
3788
|
+
const text = await this.deps.runInterviewTurn(prompt);
|
|
3789
|
+
this.lastAgentText = text;
|
|
3790
|
+
if (this.driver) await this.driver.ingestAgentOutput(text);
|
|
3791
|
+
this.broadcast({ type: "sdd.spec.agent_text", payload: { text } });
|
|
3792
|
+
} finally {
|
|
3793
|
+
this.busy = false;
|
|
3794
|
+
this.broadcast(this.snapshotMsg());
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
snapshotMsg() {
|
|
3798
|
+
const snap = this.driver?.snapshot();
|
|
3799
|
+
return {
|
|
3800
|
+
type: "sdd.spec.snapshot",
|
|
3801
|
+
payload: { ...snap, busy: this.busy }
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
broadcast(msg) {
|
|
3805
|
+
const data = JSON.stringify(msg);
|
|
3806
|
+
for (const client of this.clients) {
|
|
3807
|
+
if (client.ws.readyState === 1) client.ws.send(data);
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
send(client, msg) {
|
|
3811
|
+
if (client.ws.readyState === 1) client.ws.send(JSON.stringify(msg));
|
|
3812
|
+
}
|
|
3813
|
+
};
|
|
3814
|
+
|
|
3815
|
+
// src/server/sdd-wizard-wiring.ts
|
|
3816
|
+
import * as path7 from "path";
|
|
3817
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3818
|
+
import {
|
|
3819
|
+
makeCommandVerifier,
|
|
3820
|
+
makeLlmSubtaskGenerator,
|
|
3821
|
+
SddBoardStore as SddBoardStore2,
|
|
3822
|
+
SddInterviewDriver,
|
|
3823
|
+
SddRunRegistry,
|
|
3824
|
+
SddSupervisor,
|
|
3825
|
+
SpecStore as SpecStore2,
|
|
3826
|
+
startSddRun,
|
|
3827
|
+
TaskGraphStore as TaskGraphStore2,
|
|
3828
|
+
WorktreeManager as WorktreeManager2
|
|
3829
|
+
} from "@wrongstack/core";
|
|
3830
|
+
function buildSddWizardDeps(opts) {
|
|
3831
|
+
const registry = new SddRunRegistry();
|
|
3832
|
+
let isolatedSeq = 0;
|
|
3833
|
+
const runIsolatedTurn = async (prompt, name2) => {
|
|
3834
|
+
const result = await opts.subagentFactory({
|
|
3835
|
+
id: `sdd-${name2.toLowerCase().replace(/\s+/g, "-")}-${isolatedSeq++}`,
|
|
3836
|
+
role: "executor",
|
|
3837
|
+
name: name2,
|
|
3838
|
+
disabledTools: ["delegate"],
|
|
3839
|
+
allowedCapabilities: ["fs.read", "net.outbound"]
|
|
3840
|
+
});
|
|
3841
|
+
try {
|
|
3842
|
+
const res = await result.agent.run([{ type: "text", text: prompt }]);
|
|
3843
|
+
return res.finalText ?? "";
|
|
3844
|
+
} finally {
|
|
3845
|
+
await result.dispose?.();
|
|
3846
|
+
}
|
|
3847
|
+
};
|
|
3848
|
+
return {
|
|
3849
|
+
makeDriver: () => new SddInterviewDriver({
|
|
3850
|
+
specStore: new SpecStore2({ baseDir: opts.paths.projectSpecs }),
|
|
3851
|
+
graphStore: new TaskGraphStore2({ baseDir: opts.paths.projectTaskGraphs }),
|
|
3852
|
+
sessionPath: path7.join(opts.paths.projectDir, "sdd-wizard-session.json")
|
|
3853
|
+
}),
|
|
3854
|
+
runInterviewTurn: (prompt) => runIsolatedTurn(prompt, "Spec Architect"),
|
|
3855
|
+
startRun: async (driver, { parallelSlots, defaultModel, defaultProvider, fallbackModels }) => {
|
|
3856
|
+
const graph = driver.getGraph();
|
|
3857
|
+
const tracker = driver.getTracker();
|
|
3858
|
+
if (!graph || !tracker) {
|
|
3859
|
+
throw new Error("No task graph to run \u2014 finish the interview first.");
|
|
3860
|
+
}
|
|
3861
|
+
let worktrees;
|
|
3862
|
+
if (process.env["WRONGSTACK_SDD_WORKTREES"] !== "0") {
|
|
3863
|
+
const inGit = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3864
|
+
cwd: opts.projectRoot,
|
|
3865
|
+
encoding: "utf8",
|
|
3866
|
+
windowsHide: true
|
|
3867
|
+
}).stdout?.trim() === "true";
|
|
3868
|
+
if (inGit) worktrees = new WorktreeManager2({ projectRoot: opts.projectRoot, events: opts.events });
|
|
3869
|
+
}
|
|
3870
|
+
const boardStore = new SddBoardStore2({ baseDir: opts.paths.projectSddBoards });
|
|
3871
|
+
const verifyTask = makeCommandVerifier();
|
|
3872
|
+
const superviseFailure = opts.brain ? new SddSupervisor({
|
|
3873
|
+
brain: opts.brain,
|
|
3874
|
+
// The run-level fallback chain (chosen in the wizard) doubles as the
|
|
3875
|
+
// supervisor's reassign options — a `reassign` verdict rotates the
|
|
3876
|
+
// worker model on retry. Empty/undefined → reassign option dropped.
|
|
3877
|
+
reassignModels: fallbackModels,
|
|
3878
|
+
// LLM auto-split: decompose a retry-exhausted task into smaller
|
|
3879
|
+
// sub-tasks on an isolated read-only turn. Heavily validated +
|
|
3880
|
+
// bounded; an empty result degrades the split into a retry.
|
|
3881
|
+
generateSubtasks: makeLlmSubtaskGenerator({
|
|
3882
|
+
run: (prompt) => runIsolatedTurn(prompt, "Task Splitter")
|
|
3883
|
+
}),
|
|
3884
|
+
// The standalone brain is a tiered policy→LLM arbiter with NO
|
|
3885
|
+
// human-escalation wrapper (see index.ts), so it never blocks on a
|
|
3886
|
+
// human prompt — an unresolved verdict degrades to a bounded retry.
|
|
3887
|
+
// Safe to let the LLM layer actually pick reassign/split.
|
|
3888
|
+
requestLlmVerdict: true
|
|
3889
|
+
}).superviseFailure : void 0;
|
|
3890
|
+
const handle = startSddRun({
|
|
3891
|
+
tracker,
|
|
3892
|
+
graph,
|
|
3893
|
+
agent: opts.agent,
|
|
3894
|
+
projectRoot: opts.projectRoot,
|
|
3895
|
+
events: opts.events,
|
|
3896
|
+
subagentFactory: opts.subagentFactory,
|
|
3897
|
+
worktrees,
|
|
3898
|
+
boardStore,
|
|
3899
|
+
registry,
|
|
3900
|
+
parallelSlots,
|
|
3901
|
+
defaultModel,
|
|
3902
|
+
defaultProvider,
|
|
3903
|
+
fallbackModels,
|
|
3904
|
+
verifyTask,
|
|
3905
|
+
superviseFailure
|
|
3906
|
+
});
|
|
3907
|
+
void handle.completion.catch(() => {
|
|
3908
|
+
});
|
|
3909
|
+
return { runId: handle.runId };
|
|
3910
|
+
}
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
// src/server/sdd-wizard-routes.ts
|
|
3915
|
+
async function handleSddWizardRoute(_ws, msg, handlers) {
|
|
3916
|
+
if (!(msg.type.startsWith("sdd.spec.") || msg.type.startsWith("sdd.run."))) return false;
|
|
3917
|
+
await handlers.handleMessage(msg);
|
|
3918
|
+
return true;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3273
3921
|
// src/server/collaboration-ws-handler.ts
|
|
3274
3922
|
import { randomUUID } from "crypto";
|
|
3275
3923
|
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
@@ -3996,16 +4644,16 @@ var CollaborationWebSocketHandler = class {
|
|
|
3996
4644
|
};
|
|
3997
4645
|
|
|
3998
4646
|
// src/server/projects-manifest.ts
|
|
3999
|
-
import * as
|
|
4000
|
-
import * as
|
|
4647
|
+
import * as fs6 from "fs/promises";
|
|
4648
|
+
import * as path8 from "path";
|
|
4001
4649
|
import { projectSlug } from "@wrongstack/core";
|
|
4002
4650
|
function projectsJsonPath(globalConfigPath) {
|
|
4003
|
-
const base =
|
|
4004
|
-
return
|
|
4651
|
+
const base = path8.dirname(globalConfigPath);
|
|
4652
|
+
return path8.join(base, "projects.json");
|
|
4005
4653
|
}
|
|
4006
4654
|
async function loadManifest(globalConfigPath) {
|
|
4007
4655
|
try {
|
|
4008
|
-
const raw = await
|
|
4656
|
+
const raw = await fs6.readFile(projectsJsonPath(globalConfigPath), "utf8");
|
|
4009
4657
|
const parsed = JSON.parse(raw);
|
|
4010
4658
|
return { projects: parsed.projects ?? [] };
|
|
4011
4659
|
} catch {
|
|
@@ -4014,16 +4662,16 @@ async function loadManifest(globalConfigPath) {
|
|
|
4014
4662
|
}
|
|
4015
4663
|
async function saveManifest(manifest, globalConfigPath) {
|
|
4016
4664
|
const file = projectsJsonPath(globalConfigPath);
|
|
4017
|
-
await
|
|
4018
|
-
await
|
|
4665
|
+
await fs6.mkdir(path8.dirname(file), { recursive: true });
|
|
4666
|
+
await fs6.writeFile(file, JSON.stringify(manifest, null, 2), "utf8");
|
|
4019
4667
|
}
|
|
4020
4668
|
function generateProjectSlug(rootPath) {
|
|
4021
4669
|
return projectSlug(rootPath);
|
|
4022
4670
|
}
|
|
4023
4671
|
async function ensureProjectDataDir(slug, globalConfigPath) {
|
|
4024
|
-
const base =
|
|
4025
|
-
const dir =
|
|
4026
|
-
await
|
|
4672
|
+
const base = path8.dirname(globalConfigPath);
|
|
4673
|
+
const dir = path8.join(base, "projects", slug);
|
|
4674
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
4027
4675
|
return dir;
|
|
4028
4676
|
}
|
|
4029
4677
|
|
|
@@ -4449,14 +5097,14 @@ function registerShutdownHandlers(res) {
|
|
|
4449
5097
|
|
|
4450
5098
|
// src/server/instance-registry.ts
|
|
4451
5099
|
import * as os from "os";
|
|
4452
|
-
import * as
|
|
4453
|
-
import * as
|
|
5100
|
+
import * as path9 from "path";
|
|
5101
|
+
import * as fs7 from "fs/promises";
|
|
4454
5102
|
import { atomicWrite as atomicWrite3 } from "@wrongstack/core";
|
|
4455
5103
|
function defaultBaseDir() {
|
|
4456
|
-
return
|
|
5104
|
+
return path9.join(os.homedir(), ".wrongstack");
|
|
4457
5105
|
}
|
|
4458
5106
|
function registryPath(baseDir = defaultBaseDir()) {
|
|
4459
|
-
return
|
|
5107
|
+
return path9.join(baseDir, "webui-instances.json");
|
|
4460
5108
|
}
|
|
4461
5109
|
function isPidAlive(pid) {
|
|
4462
5110
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -4469,7 +5117,7 @@ function isPidAlive(pid) {
|
|
|
4469
5117
|
}
|
|
4470
5118
|
async function load(file) {
|
|
4471
5119
|
try {
|
|
4472
|
-
const raw = await
|
|
5120
|
+
const raw = await fs7.readFile(file, "utf8");
|
|
4473
5121
|
const parsed = JSON.parse(raw);
|
|
4474
5122
|
if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
|
|
4475
5123
|
return parsed;
|
|
@@ -4578,9 +5226,10 @@ function openBrowser(url, platform = process.platform) {
|
|
|
4578
5226
|
if (child.pid) {
|
|
4579
5227
|
try {
|
|
4580
5228
|
import("@wrongstack/tools").then(({ getProcessRegistry }) => {
|
|
5229
|
+
const pid = child.pid;
|
|
5230
|
+
if (pid === void 0) return;
|
|
4581
5231
|
getProcessRegistry().register({
|
|
4582
|
-
|
|
4583
|
-
pid: child.pid,
|
|
5232
|
+
pid,
|
|
4584
5233
|
name: "browser",
|
|
4585
5234
|
command: `${command} ${args.join(" ")}`,
|
|
4586
5235
|
startedAt: Date.now(),
|
|
@@ -4588,7 +5237,7 @@ function openBrowser(url, platform = process.platform) {
|
|
|
4588
5237
|
protected: true
|
|
4589
5238
|
});
|
|
4590
5239
|
child.on("exit", () => {
|
|
4591
|
-
getProcessRegistry().unregister(
|
|
5240
|
+
getProcessRegistry().unregister(pid);
|
|
4592
5241
|
});
|
|
4593
5242
|
}).catch(() => {
|
|
4594
5243
|
});
|
|
@@ -4613,19 +5262,19 @@ function computeUsageCost(usage, rates) {
|
|
|
4613
5262
|
}
|
|
4614
5263
|
|
|
4615
5264
|
// src/server/provider-handlers.ts
|
|
4616
|
-
import { DefaultSecretScrubber
|
|
5265
|
+
import { DefaultSecretScrubber } from "@wrongstack/core";
|
|
4617
5266
|
import { probeLocalLlm } from "@wrongstack/runtime/probe";
|
|
4618
5267
|
|
|
4619
5268
|
// src/server/provider-config-io.ts
|
|
4620
|
-
import * as
|
|
4621
|
-
import * as
|
|
5269
|
+
import * as fs8 from "fs/promises";
|
|
5270
|
+
import * as path10 from "path";
|
|
4622
5271
|
import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
|
|
4623
5272
|
import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
|
|
4624
5273
|
import { DefaultSecretVault } from "@wrongstack/core";
|
|
4625
5274
|
async function loadSavedProviders(configPath, vault) {
|
|
4626
5275
|
let raw;
|
|
4627
5276
|
try {
|
|
4628
|
-
raw = await
|
|
5277
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
4629
5278
|
} catch {
|
|
4630
5279
|
return {};
|
|
4631
5280
|
}
|
|
@@ -4642,7 +5291,7 @@ async function saveProviders(configPath, vault, providers) {
|
|
|
4642
5291
|
let raw;
|
|
4643
5292
|
let fileExists = true;
|
|
4644
5293
|
try {
|
|
4645
|
-
raw = await
|
|
5294
|
+
raw = await fs8.readFile(configPath, "utf8");
|
|
4646
5295
|
} catch (err) {
|
|
4647
5296
|
if (err.code !== "ENOENT") {
|
|
4648
5297
|
throw new Error(
|
|
@@ -4791,7 +5440,7 @@ function projectSavedProviders(providers) {
|
|
|
4791
5440
|
return view;
|
|
4792
5441
|
});
|
|
4793
5442
|
}
|
|
4794
|
-
var probeScrubber = new
|
|
5443
|
+
var probeScrubber = new DefaultSecretScrubber();
|
|
4795
5444
|
function createProviderHandlers(deps2) {
|
|
4796
5445
|
const { globalConfigPath, vault, broadcast: broadcast2, clients } = deps2;
|
|
4797
5446
|
let configWriteLock = deps2.getConfigWriteLock();
|
|
@@ -4816,7 +5465,10 @@ function createProviderHandlers(deps2) {
|
|
|
4816
5465
|
try {
|
|
4817
5466
|
const providers = await loadConfigProviders();
|
|
4818
5467
|
const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
|
|
4819
|
-
if (result.ok)
|
|
5468
|
+
if (result.ok) {
|
|
5469
|
+
await saveConfigProviders(providers);
|
|
5470
|
+
broadcastSaved(providers);
|
|
5471
|
+
}
|
|
4820
5472
|
sendResult2(ws, result.ok, result.message);
|
|
4821
5473
|
} catch (err) {
|
|
4822
5474
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4826,7 +5478,10 @@ function createProviderHandlers(deps2) {
|
|
|
4826
5478
|
try {
|
|
4827
5479
|
const providers = await loadConfigProviders();
|
|
4828
5480
|
const result = deleteKey(providers, providerId, label);
|
|
4829
|
-
if (result.ok)
|
|
5481
|
+
if (result.ok) {
|
|
5482
|
+
await saveConfigProviders(providers);
|
|
5483
|
+
broadcastSaved(providers);
|
|
5484
|
+
}
|
|
4830
5485
|
sendResult2(ws, result.ok, result.message);
|
|
4831
5486
|
} catch (err) {
|
|
4832
5487
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4836,7 +5491,10 @@ function createProviderHandlers(deps2) {
|
|
|
4836
5491
|
try {
|
|
4837
5492
|
const providers = await loadConfigProviders();
|
|
4838
5493
|
const result = setActiveKey(providers, providerId, label);
|
|
4839
|
-
if (result.ok)
|
|
5494
|
+
if (result.ok) {
|
|
5495
|
+
await saveConfigProviders(providers);
|
|
5496
|
+
broadcastSaved(providers);
|
|
5497
|
+
}
|
|
4840
5498
|
sendResult2(ws, result.ok, result.message);
|
|
4841
5499
|
} catch (err) {
|
|
4842
5500
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4846,11 +5504,13 @@ function createProviderHandlers(deps2) {
|
|
|
4846
5504
|
try {
|
|
4847
5505
|
const providers = await loadConfigProviders();
|
|
4848
5506
|
const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
|
|
4849
|
-
if (result.ok)
|
|
5507
|
+
if (result.ok) {
|
|
5508
|
+
await saveConfigProviders(providers);
|
|
5509
|
+
broadcastSaved(providers);
|
|
5510
|
+
}
|
|
4850
5511
|
sendResult2(ws, result.ok, result.message);
|
|
4851
5512
|
if (result.ok) {
|
|
4852
5513
|
console.log(`[WebUI] Provider "${payload.id}" added via provider.add`);
|
|
4853
|
-
broadcastSaved(providers);
|
|
4854
5514
|
}
|
|
4855
5515
|
} catch (err) {
|
|
4856
5516
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4860,7 +5520,10 @@ function createProviderHandlers(deps2) {
|
|
|
4860
5520
|
try {
|
|
4861
5521
|
const providers = await loadConfigProviders();
|
|
4862
5522
|
const result = removeProvider(providers, providerId);
|
|
4863
|
-
if (result.ok)
|
|
5523
|
+
if (result.ok) {
|
|
5524
|
+
await saveConfigProviders(providers);
|
|
5525
|
+
broadcastSaved(providers);
|
|
5526
|
+
}
|
|
4864
5527
|
sendResult2(ws, result.ok, result.message);
|
|
4865
5528
|
} catch (err) {
|
|
4866
5529
|
sendResult2(ws, false, errMessage(err));
|
|
@@ -4966,7 +5629,7 @@ function createProviderHandlers(deps2) {
|
|
|
4966
5629
|
|
|
4967
5630
|
// src/server/mode-handlers.ts
|
|
4968
5631
|
import {
|
|
4969
|
-
DefaultSystemPromptBuilder
|
|
5632
|
+
DefaultSystemPromptBuilder
|
|
4970
5633
|
} from "@wrongstack/core";
|
|
4971
5634
|
function createModeHandlers(ctx) {
|
|
4972
5635
|
return {
|
|
@@ -5014,7 +5677,7 @@ function createModeHandlers(ctx) {
|
|
|
5014
5677
|
}
|
|
5015
5678
|
ctx.setModeId(id);
|
|
5016
5679
|
const modePrompt = id === "default" ? "" : (await ctx.modeStore.getMode(id))?.prompt ?? "";
|
|
5017
|
-
const freshBuilder = new
|
|
5680
|
+
const freshBuilder = new DefaultSystemPromptBuilder({
|
|
5018
5681
|
memoryStore: ctx.memoryStore,
|
|
5019
5682
|
skillLoader: ctx.skillLoader,
|
|
5020
5683
|
modeStore: ctx.modeStore,
|
|
@@ -5043,42 +5706,12 @@ function createModeHandlers(ctx) {
|
|
|
5043
5706
|
|
|
5044
5707
|
// src/server/project-handlers.ts
|
|
5045
5708
|
import * as fs9 from "fs/promises";
|
|
5046
|
-
import * as
|
|
5709
|
+
import * as path11 from "path";
|
|
5047
5710
|
import {
|
|
5048
|
-
DefaultSessionStore
|
|
5049
|
-
DefaultSystemPromptBuilder as
|
|
5711
|
+
DefaultSessionStore,
|
|
5712
|
+
DefaultSystemPromptBuilder as DefaultSystemPromptBuilder2,
|
|
5050
5713
|
getSessionRegistry
|
|
5051
5714
|
} from "@wrongstack/core";
|
|
5052
|
-
|
|
5053
|
-
// src/server/path-containment.ts
|
|
5054
|
-
import * as fs8 from "fs/promises";
|
|
5055
|
-
import * as path9 from "path";
|
|
5056
|
-
function isPathInside(root, target) {
|
|
5057
|
-
const relative3 = path9.relative(root, target);
|
|
5058
|
-
return relative3 === "" || !relative3.startsWith("..") && !path9.isAbsolute(relative3);
|
|
5059
|
-
}
|
|
5060
|
-
async function resolveWorkingDirInsideProject(projectRoot, inputPath) {
|
|
5061
|
-
const resolved = path9.resolve(projectRoot, inputPath);
|
|
5062
|
-
let stat3;
|
|
5063
|
-
try {
|
|
5064
|
-
stat3 = await fs8.stat(resolved);
|
|
5065
|
-
} catch {
|
|
5066
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5067
|
-
}
|
|
5068
|
-
if (!stat3.isDirectory()) {
|
|
5069
|
-
throw new Error(`Directory not found or not accessible: ${resolved}`);
|
|
5070
|
-
}
|
|
5071
|
-
const [realProjectRoot, realResolved] = await Promise.all([
|
|
5072
|
-
fs8.realpath(projectRoot),
|
|
5073
|
-
fs8.realpath(resolved)
|
|
5074
|
-
]);
|
|
5075
|
-
if (!isPathInside(realProjectRoot, realResolved)) {
|
|
5076
|
-
throw new Error(`Path must stay inside the project root: ${projectRoot}`);
|
|
5077
|
-
}
|
|
5078
|
-
return resolved;
|
|
5079
|
-
}
|
|
5080
|
-
|
|
5081
|
-
// src/server/project-handlers.ts
|
|
5082
5715
|
function createProjectHandlers(ctx) {
|
|
5083
5716
|
return {
|
|
5084
5717
|
listProjects: async (ws) => {
|
|
@@ -5100,7 +5733,7 @@ function createProjectHandlers(ctx) {
|
|
|
5100
5733
|
}
|
|
5101
5734
|
const { root: addRoot, name: displayName } = parsed.value;
|
|
5102
5735
|
try {
|
|
5103
|
-
const resolved =
|
|
5736
|
+
const resolved = path11.resolve(addRoot);
|
|
5104
5737
|
await fs9.access(resolved);
|
|
5105
5738
|
const stat3 = await fs9.stat(resolved);
|
|
5106
5739
|
if (!stat3.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
|
|
@@ -5118,7 +5751,7 @@ function createProjectHandlers(ctx) {
|
|
|
5118
5751
|
});
|
|
5119
5752
|
return;
|
|
5120
5753
|
}
|
|
5121
|
-
const name2 = displayName?.trim() ||
|
|
5754
|
+
const name2 = displayName?.trim() || path11.basename(resolved);
|
|
5122
5755
|
const slug = generateProjectSlug(resolved);
|
|
5123
5756
|
await ensureProjectDataDir(slug, ctx.globalConfigPath);
|
|
5124
5757
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5131,7 +5764,7 @@ function createProjectHandlers(ctx) {
|
|
|
5131
5764
|
} catch (err) {
|
|
5132
5765
|
send(ws, {
|
|
5133
5766
|
type: "projects.added",
|
|
5134
|
-
payload: { name:
|
|
5767
|
+
payload: { name: path11.basename(addRoot), root: addRoot, slug: "", message: errMessage(err) }
|
|
5135
5768
|
});
|
|
5136
5769
|
}
|
|
5137
5770
|
},
|
|
@@ -5146,7 +5779,7 @@ function createProjectHandlers(ctx) {
|
|
|
5146
5779
|
}
|
|
5147
5780
|
const { root: selRoot, name: selName } = parsed.value;
|
|
5148
5781
|
try {
|
|
5149
|
-
const resolved =
|
|
5782
|
+
const resolved = path11.resolve(selRoot);
|
|
5150
5783
|
try {
|
|
5151
5784
|
await fs9.access(resolved);
|
|
5152
5785
|
const stat3 = await fs9.stat(resolved);
|
|
@@ -5156,7 +5789,7 @@ function createProjectHandlers(ctx) {
|
|
|
5156
5789
|
type: "projects.selected",
|
|
5157
5790
|
payload: {
|
|
5158
5791
|
root: selRoot,
|
|
5159
|
-
name: selName ||
|
|
5792
|
+
name: selName || path11.basename(selRoot),
|
|
5160
5793
|
message: `Cannot switch: ${errMessage(err)}`
|
|
5161
5794
|
}
|
|
5162
5795
|
});
|
|
@@ -5168,7 +5801,7 @@ function createProjectHandlers(ctx) {
|
|
|
5168
5801
|
entry.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
5169
5802
|
entry.lastWorkingDir = resolved;
|
|
5170
5803
|
} else {
|
|
5171
|
-
const name2 = selName?.trim() ||
|
|
5804
|
+
const name2 = selName?.trim() || path11.basename(resolved);
|
|
5172
5805
|
const slug = generateProjectSlug(resolved);
|
|
5173
5806
|
manifest.projects.push({
|
|
5174
5807
|
name: name2,
|
|
@@ -5190,7 +5823,7 @@ function createProjectHandlers(ctx) {
|
|
|
5190
5823
|
try {
|
|
5191
5824
|
const modeId = ctx.getModeId();
|
|
5192
5825
|
const switchMode = modeId === "default" ? void 0 : await ctx.modeStore.getMode(modeId);
|
|
5193
|
-
const switchBuilder = new
|
|
5826
|
+
const switchBuilder = new DefaultSystemPromptBuilder2({
|
|
5194
5827
|
memoryStore: ctx.memoryStore,
|
|
5195
5828
|
skillLoader: ctx.skillLoader,
|
|
5196
5829
|
modeStore: ctx.modeStore,
|
|
@@ -5207,14 +5840,14 @@ function createProjectHandlers(ctx) {
|
|
|
5207
5840
|
});
|
|
5208
5841
|
} catch {
|
|
5209
5842
|
}
|
|
5210
|
-
const newSessionsDir =
|
|
5211
|
-
|
|
5843
|
+
const newSessionsDir = path11.join(
|
|
5844
|
+
path11.dirname(ctx.globalConfigPath),
|
|
5212
5845
|
"projects",
|
|
5213
5846
|
switchSlug,
|
|
5214
5847
|
"sessions"
|
|
5215
5848
|
);
|
|
5216
5849
|
await fs9.mkdir(newSessionsDir, { recursive: true });
|
|
5217
|
-
const newSessionStore = new
|
|
5850
|
+
const newSessionStore = new DefaultSessionStore({ dir: newSessionsDir });
|
|
5218
5851
|
const oldSession = ctx.getSession();
|
|
5219
5852
|
const oldSessionId = oldSession.id;
|
|
5220
5853
|
try {
|
|
@@ -5247,7 +5880,7 @@ function createProjectHandlers(ctx) {
|
|
|
5247
5880
|
sessionId: newSession.id,
|
|
5248
5881
|
projectSlug: switchSlug,
|
|
5249
5882
|
projectRoot: resolved,
|
|
5250
|
-
projectName:
|
|
5883
|
+
projectName: path11.basename(resolved),
|
|
5251
5884
|
workingDir: resolved,
|
|
5252
5885
|
clientType: "webui",
|
|
5253
5886
|
pid: process.pid,
|
|
@@ -5259,8 +5892,8 @@ function createProjectHandlers(ctx) {
|
|
|
5259
5892
|
type: "projects.selected",
|
|
5260
5893
|
payload: {
|
|
5261
5894
|
root: resolved,
|
|
5262
|
-
name: selName ||
|
|
5263
|
-
message: `Switched to ${selName ||
|
|
5895
|
+
name: selName || path11.basename(resolved),
|
|
5896
|
+
message: `Switched to ${selName || path11.basename(resolved)}`
|
|
5264
5897
|
}
|
|
5265
5898
|
});
|
|
5266
5899
|
broadcast(ctx.clients, {
|
|
@@ -5280,7 +5913,7 @@ function createProjectHandlers(ctx) {
|
|
|
5280
5913
|
type: "projects.selected",
|
|
5281
5914
|
payload: {
|
|
5282
5915
|
root: selRoot,
|
|
5283
|
-
name: selName ||
|
|
5916
|
+
name: selName || path11.basename(selRoot),
|
|
5284
5917
|
message: errMessage(err)
|
|
5285
5918
|
}
|
|
5286
5919
|
});
|
|
@@ -5311,7 +5944,7 @@ function createProjectHandlers(ctx) {
|
|
|
5311
5944
|
}
|
|
5312
5945
|
|
|
5313
5946
|
// src/server/session-handlers.ts
|
|
5314
|
-
import * as
|
|
5947
|
+
import * as path12 from "path";
|
|
5315
5948
|
import {
|
|
5316
5949
|
DEFAULT_CONTEXT_WINDOW_MODE_ID,
|
|
5317
5950
|
repairToolUseAdjacency,
|
|
@@ -5653,7 +6286,7 @@ function createSessionHandlers(ctx) {
|
|
|
5653
6286
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
5654
6287
|
const projectRoot = ctx.getProjectRoot();
|
|
5655
6288
|
const rewinder = new DefaultSessionRewinder(
|
|
5656
|
-
|
|
6289
|
+
path12.join(projectRoot, ".wrongstack", "sessions"),
|
|
5657
6290
|
projectRoot
|
|
5658
6291
|
);
|
|
5659
6292
|
const checkpoints = await rewinder.listCheckpoints(ctx.getSession().id);
|
|
@@ -5668,7 +6301,7 @@ function createSessionHandlers(ctx) {
|
|
|
5668
6301
|
const { DefaultSessionRewinder } = await import("@wrongstack/core");
|
|
5669
6302
|
const projectRoot = ctx.getProjectRoot();
|
|
5670
6303
|
const rewinder = new DefaultSessionRewinder(
|
|
5671
|
-
|
|
6304
|
+
path12.join(projectRoot, ".wrongstack", "sessions"),
|
|
5672
6305
|
projectRoot
|
|
5673
6306
|
);
|
|
5674
6307
|
await rewinder.rewindToCheckpoint(ctx.getSession().id, checkpointIndex);
|
|
@@ -5911,6 +6544,22 @@ async function handleModeRoute(ws, msg, handlers) {
|
|
|
5911
6544
|
}
|
|
5912
6545
|
}
|
|
5913
6546
|
|
|
6547
|
+
// src/server/prefs-routes.ts
|
|
6548
|
+
async function handlePrefsRoute(ws, msg, handlers) {
|
|
6549
|
+
switch (msg.type) {
|
|
6550
|
+
case "prefs.get": {
|
|
6551
|
+
await handlers.getPrefs(ws);
|
|
6552
|
+
return true;
|
|
6553
|
+
}
|
|
6554
|
+
case "prefs.update": {
|
|
6555
|
+
await handlers.updatePrefs(ws, msg.payload ?? {});
|
|
6556
|
+
return true;
|
|
6557
|
+
}
|
|
6558
|
+
default:
|
|
6559
|
+
return false;
|
|
6560
|
+
}
|
|
6561
|
+
}
|
|
6562
|
+
|
|
5914
6563
|
// src/server/shell-git-routes.ts
|
|
5915
6564
|
async function handleShellGitRoute(ws, msg, handlers) {
|
|
5916
6565
|
switch (msg.type) {
|
|
@@ -5951,6 +6600,44 @@ async function handleMailboxRoute(ws, msg, handlers) {
|
|
|
5951
6600
|
}
|
|
5952
6601
|
}
|
|
5953
6602
|
|
|
6603
|
+
// src/server/mcp-routes.ts
|
|
6604
|
+
async function handleMcpRoute(ws, msg, handlers) {
|
|
6605
|
+
switch (msg.type) {
|
|
6606
|
+
case "mcp.list":
|
|
6607
|
+
await handlers.list(ws, msg);
|
|
6608
|
+
return true;
|
|
6609
|
+
case "mcp.add":
|
|
6610
|
+
await handlers.add(ws, msg);
|
|
6611
|
+
return true;
|
|
6612
|
+
case "mcp.update":
|
|
6613
|
+
await handlers.update(ws, msg);
|
|
6614
|
+
return true;
|
|
6615
|
+
case "mcp.remove":
|
|
6616
|
+
await handlers.remove(ws, msg);
|
|
6617
|
+
return true;
|
|
6618
|
+
case "mcp.enable":
|
|
6619
|
+
await handlers.enable(ws, msg);
|
|
6620
|
+
return true;
|
|
6621
|
+
case "mcp.disable":
|
|
6622
|
+
await handlers.disable(ws, msg);
|
|
6623
|
+
return true;
|
|
6624
|
+
case "mcp.sleep":
|
|
6625
|
+
await handlers.sleep(ws, msg);
|
|
6626
|
+
return true;
|
|
6627
|
+
case "mcp.wake":
|
|
6628
|
+
await handlers.wake(ws, msg);
|
|
6629
|
+
return true;
|
|
6630
|
+
case "mcp.restart":
|
|
6631
|
+
await handlers.restart(ws, msg);
|
|
6632
|
+
return true;
|
|
6633
|
+
case "mcp.discover":
|
|
6634
|
+
await handlers.discover(ws, msg);
|
|
6635
|
+
return true;
|
|
6636
|
+
default:
|
|
6637
|
+
return false;
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
|
|
5954
6641
|
// src/server/brain-routes.ts
|
|
5955
6642
|
async function handleBrainRoute(ws, msg, handlers) {
|
|
5956
6643
|
switch (msg.type) {
|
|
@@ -5975,10 +6662,24 @@ async function handleAutoPhaseRoute(_ws, msg, handlers) {
|
|
|
5975
6662
|
return true;
|
|
5976
6663
|
}
|
|
5977
6664
|
|
|
6665
|
+
// src/server/specs-routes.ts
|
|
6666
|
+
async function handleSpecsRoute(_ws, msg, handlers) {
|
|
6667
|
+
if (!msg.type.startsWith("specs.")) return false;
|
|
6668
|
+
await handlers.handleMessage(msg);
|
|
6669
|
+
return true;
|
|
6670
|
+
}
|
|
6671
|
+
|
|
6672
|
+
// src/server/sdd-board-routes.ts
|
|
6673
|
+
async function handleSddBoardRoute(_ws, msg, handlers) {
|
|
6674
|
+
if (!msg.type.startsWith("sdd.board.")) return false;
|
|
6675
|
+
await handlers.handleMessage(msg);
|
|
6676
|
+
return true;
|
|
6677
|
+
}
|
|
6678
|
+
|
|
5978
6679
|
// src/server/setup-events.ts
|
|
5979
6680
|
import * as fs10 from "fs/promises";
|
|
5980
6681
|
import { watch as fsWatch } from "fs";
|
|
5981
|
-
import * as
|
|
6682
|
+
import * as path13 from "path";
|
|
5982
6683
|
function setupEvents(deps2) {
|
|
5983
6684
|
const { events, broadcast: broadcast2, clients, config, context, pendingConfirms, globalConfigPath, sessionBridge, wpaths, watcherMetrics, onFleetBroadcaster } = deps2;
|
|
5984
6685
|
const disposers = [];
|
|
@@ -6408,11 +7109,13 @@ function setupEvents(deps2) {
|
|
|
6408
7109
|
events.on("provider.response", (e) => {
|
|
6409
7110
|
if (e.usage?.input != null) {
|
|
6410
7111
|
const maxCtx = context.provider.capabilities.maxContext;
|
|
6411
|
-
const
|
|
7112
|
+
const rawLoad = maxCtx > 0 ? e.usage.input / maxCtx : 0;
|
|
7113
|
+
const load2 = Math.max(0, Math.min(1, rawLoad));
|
|
6412
7114
|
const costUsd = context.tokenCounter.estimateCost().total;
|
|
6413
7115
|
forwardSubagent("ctx_pct", {
|
|
6414
7116
|
subagentId: "leader",
|
|
6415
|
-
load:
|
|
7117
|
+
load: load2,
|
|
7118
|
+
rawLoad,
|
|
6416
7119
|
tokens: e.usage.input,
|
|
6417
7120
|
maxContext: maxCtx,
|
|
6418
7121
|
costUsd
|
|
@@ -6443,7 +7146,7 @@ function setupEvents(deps2) {
|
|
|
6443
7146
|
if (wpaths?.projectStatus) {
|
|
6444
7147
|
try {
|
|
6445
7148
|
const statusFile = wpaths.projectStatus(e.projectHash);
|
|
6446
|
-
const dir =
|
|
7149
|
+
const dir = path13.dirname(statusFile);
|
|
6447
7150
|
await fs10.mkdir(dir, { recursive: true });
|
|
6448
7151
|
await fs10.writeFile(statusFile, JSON.stringify(e, null, 2), "utf-8");
|
|
6449
7152
|
} catch (err) {
|
|
@@ -6452,7 +7155,7 @@ function setupEvents(deps2) {
|
|
|
6452
7155
|
}
|
|
6453
7156
|
});
|
|
6454
7157
|
if (wpaths?.projectStatus && wpaths.configDir) {
|
|
6455
|
-
const projectsDir =
|
|
7158
|
+
const projectsDir = path13.join(wpaths.configDir, "projects");
|
|
6456
7159
|
const knownProjectHashes = /* @__PURE__ */ new Set();
|
|
6457
7160
|
const debounceTimers = /* @__PURE__ */ new Map();
|
|
6458
7161
|
const DEBOUNCE_MS = 150;
|
|
@@ -6479,7 +7182,7 @@ function setupEvents(deps2) {
|
|
|
6479
7182
|
);
|
|
6480
7183
|
};
|
|
6481
7184
|
const metricsInterval = setInterval(logWatcherMetrics, 6e4);
|
|
6482
|
-
const broadcastStatus = (
|
|
7185
|
+
const broadcastStatus = (_projectHash, statusData, actualDelayMs) => {
|
|
6483
7186
|
broadcast2(clients, { type: "client.status_update", payload: statusData });
|
|
6484
7187
|
if (watcherMetrics) {
|
|
6485
7188
|
watcherMetrics.broadcastsSent++;
|
|
@@ -6520,9 +7223,9 @@ function setupEvents(deps2) {
|
|
|
6520
7223
|
if (eventType === "change") {
|
|
6521
7224
|
if (filename == null) return;
|
|
6522
7225
|
if (watcherMetrics) watcherMetrics.fileChangesDetected++;
|
|
6523
|
-
const targetFile =
|
|
7226
|
+
const targetFile = path13.join(projectsDir, String(filename));
|
|
6524
7227
|
if (targetFile.endsWith("status.json")) {
|
|
6525
|
-
const projectHash2 =
|
|
7228
|
+
const projectHash2 = path13.basename(path13.dirname(targetFile));
|
|
6526
7229
|
if (knownProjectHashes.size > 0 && !knownProjectHashes.has(projectHash2)) {
|
|
6527
7230
|
return;
|
|
6528
7231
|
}
|
|
@@ -6580,7 +7283,7 @@ function setupEvents(deps2) {
|
|
|
6580
7283
|
}
|
|
6581
7284
|
});
|
|
6582
7285
|
}
|
|
6583
|
-
const globalRoot = globalConfigPath ?
|
|
7286
|
+
const globalRoot = globalConfigPath ? path13.dirname(globalConfigPath) : void 0;
|
|
6584
7287
|
if (globalRoot) {
|
|
6585
7288
|
const broadcastSessions = async () => {
|
|
6586
7289
|
try {
|
|
@@ -6654,10 +7357,10 @@ function setupEvents(deps2) {
|
|
|
6654
7357
|
// src/server/custom-context-modes.ts
|
|
6655
7358
|
import { listContextWindowModes, atomicWrite as atomicWrite5 } from "@wrongstack/core";
|
|
6656
7359
|
import * as fs11 from "fs/promises";
|
|
6657
|
-
import * as
|
|
7360
|
+
import * as path14 from "path";
|
|
6658
7361
|
var STORE_FILENAME = "custom-context-modes.json";
|
|
6659
7362
|
function storePath(wrongstackDir) {
|
|
6660
|
-
return
|
|
7363
|
+
return path14.join(wrongstackDir, STORE_FILENAME);
|
|
6661
7364
|
}
|
|
6662
7365
|
var BUILTIN_IDS = /* @__PURE__ */ new Set(["balanced", "frugal", "deep", "archival"]);
|
|
6663
7366
|
function createCustomModeStore(wrongstackDir) {
|
|
@@ -6789,12 +7492,12 @@ function createEternalSubscription(subscribe, broadcast2, clientsRef) {
|
|
|
6789
7492
|
|
|
6790
7493
|
// src/server/shell-open.ts
|
|
6791
7494
|
import * as fs12 from "fs/promises";
|
|
6792
|
-
import * as
|
|
7495
|
+
import * as path15 from "path";
|
|
6793
7496
|
import { spawn as spawn2 } from "child_process";
|
|
6794
7497
|
var METACHAR_REGEX = /[&|<>^"'`\n\r]/;
|
|
6795
7498
|
async function handleShellOpen(req, logger) {
|
|
6796
7499
|
try {
|
|
6797
|
-
const resolved =
|
|
7500
|
+
const resolved = path15.resolve(req.path);
|
|
6798
7501
|
await fs12.access(resolved);
|
|
6799
7502
|
if (METACHAR_REGEX.test(resolved)) {
|
|
6800
7503
|
return { success: false, message: "Path contains unsupported characters." };
|
|
@@ -6904,15 +7607,15 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6904
7607
|
if (!m) continue;
|
|
6905
7608
|
const added = m[1] === "-" ? 0 : Number(m[1]);
|
|
6906
7609
|
const deleted = m[2] === "-" ? 0 : Number(m[2]);
|
|
6907
|
-
let
|
|
6908
|
-
if (
|
|
7610
|
+
let path17 = m[3] ?? "";
|
|
7611
|
+
if (path17 === "") {
|
|
6909
7612
|
i += 1;
|
|
6910
|
-
|
|
7613
|
+
path17 = parts[i + 1] ?? parts[i] ?? "";
|
|
6911
7614
|
i += 1;
|
|
6912
7615
|
}
|
|
6913
|
-
if (!
|
|
6914
|
-
const prev = counts.get(
|
|
6915
|
-
counts.set(
|
|
7616
|
+
if (!path17) continue;
|
|
7617
|
+
const prev = counts.get(path17) ?? { added: 0, deleted: 0 };
|
|
7618
|
+
counts.set(path17, { added: prev.added + added, deleted: prev.deleted + deleted });
|
|
6916
7619
|
}
|
|
6917
7620
|
};
|
|
6918
7621
|
parseNumstat(unstagedNumstat);
|
|
@@ -6924,7 +7627,7 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6924
7627
|
if (!rec || rec.length < 3) continue;
|
|
6925
7628
|
const x = rec[0] ?? " ";
|
|
6926
7629
|
const y = rec[1] ?? " ";
|
|
6927
|
-
const
|
|
7630
|
+
const path17 = rec.slice(3);
|
|
6928
7631
|
const isRename = x === "R" || x === "C" || y === "R" || y === "C";
|
|
6929
7632
|
if (isRename) i += 1;
|
|
6930
7633
|
let status;
|
|
@@ -6936,13 +7639,13 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6936
7639
|
else if (x === "D" || y === "D") status = "D";
|
|
6937
7640
|
else status = "M";
|
|
6938
7641
|
const staged = x !== " " && x !== "?";
|
|
6939
|
-
let added = counts.get(
|
|
6940
|
-
let deleted = counts.get(
|
|
7642
|
+
let added = counts.get(path17)?.added ?? 0;
|
|
7643
|
+
let deleted = counts.get(path17)?.deleted ?? 0;
|
|
6941
7644
|
if (status === "?") {
|
|
6942
7645
|
added = 0;
|
|
6943
7646
|
deleted = 0;
|
|
6944
7647
|
}
|
|
6945
|
-
files.push({ path:
|
|
7648
|
+
files.push({ path: path17, status, added, deleted, staged });
|
|
6946
7649
|
}
|
|
6947
7650
|
send(ws, { type: "git.changes", payload: { files } });
|
|
6948
7651
|
} catch (err) {
|
|
@@ -6953,21 +7656,21 @@ async function handleGitChanges(ws, projectRoot) {
|
|
|
6953
7656
|
}
|
|
6954
7657
|
}
|
|
6955
7658
|
var MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
6956
|
-
async function handleGitDiff(ws, projectRoot,
|
|
7659
|
+
async function handleGitDiff(ws, projectRoot, path17) {
|
|
6957
7660
|
const cwd = projectRoot || void 0;
|
|
6958
|
-
const reply = (extra) => send(ws, { type: "git.diff", payload: { path:
|
|
6959
|
-
if (!
|
|
7661
|
+
const reply = (extra) => send(ws, { type: "git.diff", payload: { path: path17, ...extra } });
|
|
7662
|
+
if (!path17 || path17.includes("\0") || path17.includes("..") || nodePath.isAbsolute(path17)) {
|
|
6960
7663
|
reply({ oldText: "", newText: "", error: "invalid path" });
|
|
6961
7664
|
return;
|
|
6962
7665
|
}
|
|
6963
7666
|
try {
|
|
6964
7667
|
const git = makeGit(cwd);
|
|
6965
7668
|
const { readFile: readFile9 } = await import("fs/promises");
|
|
6966
|
-
const { join:
|
|
6967
|
-
const oldText = await git(["show", `HEAD:${
|
|
7669
|
+
const { join: join12 } = await import("path");
|
|
7670
|
+
const oldText = await git(["show", `HEAD:${path17}`]);
|
|
6968
7671
|
let newText = "";
|
|
6969
7672
|
try {
|
|
6970
|
-
const abs = cwd ?
|
|
7673
|
+
const abs = cwd ? join12(cwd, path17) : path17;
|
|
6971
7674
|
const buf = await readFile9(abs);
|
|
6972
7675
|
if (buf.includes(0)) {
|
|
6973
7676
|
reply({ oldText: "", newText: "", binary: true });
|
|
@@ -7063,6 +7766,7 @@ async function handleGoalGet(projectRoot, broadcast2) {
|
|
|
7063
7766
|
|
|
7064
7767
|
// src/server/index.ts
|
|
7065
7768
|
async function startWebUI(opts = {}) {
|
|
7769
|
+
ensureSessionShell();
|
|
7066
7770
|
const requestedWsPort = opts.wsPort ?? 3457;
|
|
7067
7771
|
const wsHost = opts.wsHost ?? "127.0.0.1";
|
|
7068
7772
|
const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
@@ -7144,7 +7848,7 @@ async function startWebUI(opts = {}) {
|
|
|
7144
7848
|
ttlSeconds: 24 * 3600
|
|
7145
7849
|
});
|
|
7146
7850
|
const container = createDefaultContainer({ config, wpaths, logger, modelsRegistry });
|
|
7147
|
-
const configStore = opts.services?.configStore ?? container.resolve(
|
|
7851
|
+
const configStore = opts.services?.configStore ?? container.resolve(TOKENS.ConfigStore);
|
|
7148
7852
|
const providerRegistry = new ProviderRegistry();
|
|
7149
7853
|
try {
|
|
7150
7854
|
const factories = await buildProviderFactoriesFromRegistry({
|
|
@@ -7166,7 +7870,7 @@ async function startWebUI(opts = {}) {
|
|
|
7166
7870
|
r.registerAllOrThrow([...builtinToolsPack.tools ?? []], builtinToolsPack.name);
|
|
7167
7871
|
return r;
|
|
7168
7872
|
})();
|
|
7169
|
-
const memoryStore = new
|
|
7873
|
+
const memoryStore = new DefaultMemoryStore({ paths: wpaths });
|
|
7170
7874
|
if (config.features.memory) {
|
|
7171
7875
|
toolRegistry.register(rememberTool(memoryStore));
|
|
7172
7876
|
toolRegistry.register(forgetTool(memoryStore));
|
|
@@ -7178,6 +7882,8 @@ async function startWebUI(opts = {}) {
|
|
|
7178
7882
|
toolRegistry.register(makeMailboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7179
7883
|
toolRegistry.register(makeMailSendTool({ projectDir: wpaths.projectDir, events }));
|
|
7180
7884
|
toolRegistry.register(makeMailInboxTool({ projectDir: wpaths.projectDir, events }));
|
|
7885
|
+
applyToolDescriptionModes(toolRegistry, config.tools?.descriptionMode);
|
|
7886
|
+
configureExecPolicy(config.tools?.exec ?? {});
|
|
7181
7887
|
console.log("[WebUI] Tool registry loaded:", toolRegistry.list().length, "tools");
|
|
7182
7888
|
const mcpRegistry = new MCPRegistry({
|
|
7183
7889
|
toolRegistry,
|
|
@@ -7194,7 +7900,7 @@ async function startWebUI(opts = {}) {
|
|
|
7194
7900
|
});
|
|
7195
7901
|
}
|
|
7196
7902
|
}
|
|
7197
|
-
let sessionStore = opts.services?.session ?? new
|
|
7903
|
+
let sessionStore = opts.services?.session ?? new DefaultSessionStore2({ dir: wpaths.projectSessions });
|
|
7198
7904
|
if (!opts.services?.session) {
|
|
7199
7905
|
sessionStore.prune(DEFAULT_SESSION_PRUNE_DAYS).then((count) => {
|
|
7200
7906
|
if (count > 0) logger.info(`Pruned ${count} old session${count === 1 ? "" : "s"}.`);
|
|
@@ -7221,7 +7927,7 @@ async function startWebUI(opts = {}) {
|
|
|
7221
7927
|
sessionId: session.id,
|
|
7222
7928
|
projectSlug: wpaths.projectSlug,
|
|
7223
7929
|
projectRoot,
|
|
7224
|
-
projectName:
|
|
7930
|
+
projectName: path16.basename(projectRoot),
|
|
7225
7931
|
workingDir,
|
|
7226
7932
|
clientType: "webui",
|
|
7227
7933
|
pid: process.pid,
|
|
@@ -7241,7 +7947,7 @@ async function startWebUI(opts = {}) {
|
|
|
7241
7947
|
const hqTelemetry = createHqPublisherFromEnv({
|
|
7242
7948
|
clientKind: "webui",
|
|
7243
7949
|
projectRoot,
|
|
7244
|
-
projectName:
|
|
7950
|
+
projectName: path16.basename(projectRoot),
|
|
7245
7951
|
appConfig: config,
|
|
7246
7952
|
socketFactory: (url) => new WebSocket2(url)
|
|
7247
7953
|
});
|
|
@@ -7253,7 +7959,7 @@ async function startWebUI(opts = {}) {
|
|
|
7253
7959
|
events,
|
|
7254
7960
|
sessionId: session.id,
|
|
7255
7961
|
projectRoot,
|
|
7256
|
-
projectName:
|
|
7962
|
+
projectName: path16.basename(projectRoot),
|
|
7257
7963
|
globalRoot: wpaths.globalRoot,
|
|
7258
7964
|
initialAgents: statusTracker?.getAgents(),
|
|
7259
7965
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -7282,11 +7988,11 @@ async function startWebUI(opts = {}) {
|
|
|
7282
7988
|
});
|
|
7283
7989
|
} catch {
|
|
7284
7990
|
}
|
|
7285
|
-
const tokenCounter = new
|
|
7991
|
+
const tokenCounter = new DefaultTokenCounter({
|
|
7286
7992
|
registry: modelsRegistry,
|
|
7287
7993
|
providerId: config.provider
|
|
7288
7994
|
});
|
|
7289
|
-
const modeStore = new
|
|
7995
|
+
const modeStore = new DefaultModeStore({ directory: wpaths.configDir });
|
|
7290
7996
|
const activeMode = await modeStore.getActiveMode();
|
|
7291
7997
|
let modeId = activeMode?.id ?? "default";
|
|
7292
7998
|
const modePrompt = activeMode?.prompt ?? "";
|
|
@@ -7307,15 +8013,15 @@ async function startWebUI(opts = {}) {
|
|
|
7307
8013
|
const modelCapabilitiesRef = {
|
|
7308
8014
|
current: modelCapabilities
|
|
7309
8015
|
};
|
|
7310
|
-
const skillLoader = config.features.skills ? new
|
|
8016
|
+
const skillLoader = config.features.skills ? new DefaultSkillLoader({ paths: wpaths }) : void 0;
|
|
7311
8017
|
const skillInstaller = config.features.skills ? new SkillInstaller({
|
|
7312
|
-
manifestPath:
|
|
7313
|
-
projectSkillsDir:
|
|
7314
|
-
globalSkillsDir:
|
|
8018
|
+
manifestPath: path16.join(wstackGlobalRoot2(), "installed-skills.json"),
|
|
8019
|
+
projectSkillsDir: path16.join(projectRoot, ".wrongstack", "skills"),
|
|
8020
|
+
globalSkillsDir: path16.join(wstackGlobalRoot2(), "skills"),
|
|
7315
8021
|
projectHash: projectHash(projectRoot),
|
|
7316
8022
|
skillLoader
|
|
7317
8023
|
}) : void 0;
|
|
7318
|
-
const systemPromptBuilder = new
|
|
8024
|
+
const systemPromptBuilder = new DefaultSystemPromptBuilder3({
|
|
7319
8025
|
memoryStore,
|
|
7320
8026
|
skillLoader,
|
|
7321
8027
|
modeStore,
|
|
@@ -7415,6 +8121,8 @@ async function startWebUI(opts = {}) {
|
|
|
7415
8121
|
context.meta["enhanceDelayMs"] = autonomyCfg["enhanceDelayMs"] ?? 6e4;
|
|
7416
8122
|
context.meta["enhanceLanguage"] = autonomyCfg["enhanceLanguage"] ?? "original";
|
|
7417
8123
|
context.meta["nextPrediction"] = config.nextPrediction ?? false;
|
|
8124
|
+
context.meta["fallbackModels"] = config.fallbackModels ?? [];
|
|
8125
|
+
context.meta["fallbackAuto"] = config.fallbackAuto !== false;
|
|
7418
8126
|
context.meta["featureMcp"] = config.features.mcp !== false;
|
|
7419
8127
|
context.meta["featurePlugins"] = config.features.plugins !== false;
|
|
7420
8128
|
context.meta["featureMemory"] = config.features.memory !== false;
|
|
@@ -7472,7 +8180,9 @@ async function startWebUI(opts = {}) {
|
|
|
7472
8180
|
"reasoningMode",
|
|
7473
8181
|
"reasoningEffort",
|
|
7474
8182
|
"reasoningPreserve",
|
|
7475
|
-
"cacheTtl"
|
|
8183
|
+
"cacheTtl",
|
|
8184
|
+
"fallbackModels",
|
|
8185
|
+
"fallbackAuto"
|
|
7476
8186
|
];
|
|
7477
8187
|
const prefSnapshot = () => {
|
|
7478
8188
|
const snapshot = {};
|
|
@@ -7503,6 +8213,8 @@ async function startWebUI(opts = {}) {
|
|
|
7503
8213
|
if (typeof payload["enhanceLanguage"] === "string") setAutonomy("enhanceLanguage", payload["enhanceLanguage"]);
|
|
7504
8214
|
if (autonomyTouched) decrypted.autonomy = autonomyCfg;
|
|
7505
8215
|
if (typeof payload["nextPrediction"] === "boolean") decrypted.nextPrediction = payload["nextPrediction"];
|
|
8216
|
+
if (Array.isArray(payload["fallbackModels"])) decrypted.fallbackModels = payload["fallbackModels"];
|
|
8217
|
+
if (typeof payload["fallbackAuto"] === "boolean") decrypted.fallbackAuto = payload["fallbackAuto"];
|
|
7506
8218
|
const FEATURE_MAP = {
|
|
7507
8219
|
featureMcp: "mcp",
|
|
7508
8220
|
featurePlugins: "plugins",
|
|
@@ -7599,7 +8311,7 @@ async function startWebUI(opts = {}) {
|
|
|
7599
8311
|
projectRoot,
|
|
7600
8312
|
logger
|
|
7601
8313
|
});
|
|
7602
|
-
const compactor =
|
|
8314
|
+
const compactor = createStrategyCompactor({
|
|
7603
8315
|
strategy: config.context?.strategy,
|
|
7604
8316
|
preserveK: config.context?.preserveK ?? 10,
|
|
7605
8317
|
eliseThreshold: config.context?.eliseThreshold ?? 2e3,
|
|
@@ -7675,9 +8387,9 @@ async function startWebUI(opts = {}) {
|
|
|
7675
8387
|
maxContext: newMaxContext
|
|
7676
8388
|
});
|
|
7677
8389
|
}
|
|
7678
|
-
const secretScrubber = container.resolve(
|
|
7679
|
-
const renderer = container.has(
|
|
7680
|
-
const permissionPolicy = container.resolve(
|
|
8390
|
+
const secretScrubber = container.resolve(TOKENS.SecretScrubber);
|
|
8391
|
+
const renderer = container.has(TOKENS.Renderer) ? container.resolve(TOKENS.Renderer) : void 0;
|
|
8392
|
+
const permissionPolicy = container.resolve(TOKENS.PermissionPolicy);
|
|
7681
8393
|
const toolExecutor = new ToolExecutor(toolRegistry, {
|
|
7682
8394
|
permissionPolicy,
|
|
7683
8395
|
secretScrubber,
|
|
@@ -7720,7 +8432,7 @@ async function startWebUI(opts = {}) {
|
|
|
7720
8432
|
}),
|
|
7721
8433
|
events
|
|
7722
8434
|
);
|
|
7723
|
-
container.bind(
|
|
8435
|
+
container.bind(TOKENS.BrainArbiter, () => brain);
|
|
7724
8436
|
const brainMailbox = new GlobalMailbox2(wpaths.projectDir, events);
|
|
7725
8437
|
const brainMonitor = new BrainMonitor({
|
|
7726
8438
|
events,
|
|
@@ -7783,6 +8495,29 @@ async function startWebUI(opts = {}) {
|
|
|
7783
8495
|
events,
|
|
7784
8496
|
projectRoot
|
|
7785
8497
|
);
|
|
8498
|
+
const specsHandler = new SpecsWebSocketHandler(wpaths.projectSpecs, wpaths.projectTaskGraphs);
|
|
8499
|
+
const sddBoardHandler = new SddBoardWebSocketHandler(wpaths.projectSddBoards);
|
|
8500
|
+
const sddWizardHandler = new SddWizardWebSocketHandler(
|
|
8501
|
+
buildSddWizardDeps({
|
|
8502
|
+
agent,
|
|
8503
|
+
events,
|
|
8504
|
+
projectRoot,
|
|
8505
|
+
brain,
|
|
8506
|
+
subagentFactory: makeLightSubagentFactory({
|
|
8507
|
+
container,
|
|
8508
|
+
providerRegistry,
|
|
8509
|
+
toolRegistry,
|
|
8510
|
+
session,
|
|
8511
|
+
projectRoot
|
|
8512
|
+
}),
|
|
8513
|
+
paths: {
|
|
8514
|
+
projectSpecs: wpaths.projectSpecs,
|
|
8515
|
+
projectTaskGraphs: wpaths.projectTaskGraphs,
|
|
8516
|
+
projectSddBoards: wpaths.projectSddBoards,
|
|
8517
|
+
projectDir: wpaths.projectDir
|
|
8518
|
+
}
|
|
8519
|
+
})
|
|
8520
|
+
);
|
|
7786
8521
|
const worktreeHandler = new WorktreeWebSocketHandler(events, logger);
|
|
7787
8522
|
const terminalHandler = new TerminalWebSocketHandler(() => workingDir, logger);
|
|
7788
8523
|
const collabHandler = new CollaborationWebSocketHandler(
|
|
@@ -7822,7 +8557,7 @@ async function startWebUI(opts = {}) {
|
|
|
7822
8557
|
inputCost,
|
|
7823
8558
|
outputCost,
|
|
7824
8559
|
cacheReadCost,
|
|
7825
|
-
projectName:
|
|
8560
|
+
projectName: path16.basename(projectRoot) || projectRoot,
|
|
7826
8561
|
projectRoot,
|
|
7827
8562
|
cwd: workingDir,
|
|
7828
8563
|
mode: modeId,
|
|
@@ -7914,6 +8649,9 @@ async function startWebUI(opts = {}) {
|
|
|
7914
8649
|
}));
|
|
7915
8650
|
});
|
|
7916
8651
|
autoPhaseHandler.addClient(ws);
|
|
8652
|
+
specsHandler.addClient(ws);
|
|
8653
|
+
sddBoardHandler.addClient(ws);
|
|
8654
|
+
sddWizardHandler.addClient(ws);
|
|
7917
8655
|
worktreeHandler.addClient(ws);
|
|
7918
8656
|
collabHandler.addClient(ws);
|
|
7919
8657
|
terminalHandler.addClient(ws);
|
|
@@ -8038,21 +8776,21 @@ async function startWebUI(opts = {}) {
|
|
|
8038
8776
|
});
|
|
8039
8777
|
}
|
|
8040
8778
|
async function touchProjectEntry(root, workDir) {
|
|
8041
|
-
const resolved =
|
|
8779
|
+
const resolved = path16.resolve(root);
|
|
8042
8780
|
const manifest = await loadManifest(globalConfigPath);
|
|
8043
8781
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8044
|
-
const existing = manifest.projects.find((p) =>
|
|
8782
|
+
const existing = manifest.projects.find((p) => path16.resolve(p.root) === resolved);
|
|
8045
8783
|
if (existing) {
|
|
8046
8784
|
existing.lastSeen = now;
|
|
8047
|
-
if (workDir) existing.lastWorkingDir =
|
|
8785
|
+
if (workDir) existing.lastWorkingDir = path16.resolve(workDir);
|
|
8048
8786
|
} else {
|
|
8049
8787
|
manifest.projects.push({
|
|
8050
|
-
name:
|
|
8788
|
+
name: path16.basename(resolved),
|
|
8051
8789
|
root: resolved,
|
|
8052
8790
|
slug: generateProjectSlug(resolved),
|
|
8053
8791
|
createdAt: now,
|
|
8054
8792
|
lastSeen: now,
|
|
8055
|
-
lastWorkingDir: workDir ?
|
|
8793
|
+
lastWorkingDir: workDir ? path16.resolve(workDir) : void 0
|
|
8056
8794
|
});
|
|
8057
8795
|
}
|
|
8058
8796
|
await saveManifest(manifest, globalConfigPath);
|
|
@@ -8074,19 +8812,29 @@ async function startWebUI(opts = {}) {
|
|
|
8074
8812
|
let sessionRoutes;
|
|
8075
8813
|
let projectRoutes;
|
|
8076
8814
|
let modeRoutes;
|
|
8815
|
+
let prefsRoutes;
|
|
8077
8816
|
let shellGitRoutes;
|
|
8078
8817
|
let mailboxRoutes;
|
|
8818
|
+
let mcpRoutes;
|
|
8079
8819
|
let brainRoutes;
|
|
8080
8820
|
let autoPhaseRoutes;
|
|
8821
|
+
let specsRoutes;
|
|
8822
|
+
let sddBoardRoutes;
|
|
8823
|
+
let sddWizardRoutes;
|
|
8081
8824
|
async function handleMessage(ws, _client, msg) {
|
|
8082
8825
|
if (await handleProviderRoute(ws, msg, providerRoutes)) return;
|
|
8083
8826
|
if (await handleSessionRoute(ws, msg, sessionRoutes)) return;
|
|
8084
8827
|
if (await handleProjectRoute(ws, msg, projectRoutes)) return;
|
|
8085
8828
|
if (await handleModeRoute(ws, msg, modeRoutes)) return;
|
|
8829
|
+
if (await handlePrefsRoute(ws, msg, prefsRoutes)) return;
|
|
8086
8830
|
if (await handleShellGitRoute(ws, msg, shellGitRoutes)) return;
|
|
8087
8831
|
if (await handleMailboxRoute(ws, msg, mailboxRoutes)) return;
|
|
8832
|
+
if (await handleMcpRoute(ws, msg, mcpRoutes)) return;
|
|
8088
8833
|
if (await handleBrainRoute(ws, msg, brainRoutes)) return;
|
|
8089
8834
|
if (await handleAutoPhaseRoute(ws, msg, autoPhaseRoutes)) return;
|
|
8835
|
+
if (await handleSpecsRoute(ws, msg, specsRoutes)) return;
|
|
8836
|
+
if (await handleSddBoardRoute(ws, msg, sddBoardRoutes)) return;
|
|
8837
|
+
if (await handleSddWizardRoute(ws, msg, sddWizardRoutes)) return;
|
|
8090
8838
|
switch (msg.type) {
|
|
8091
8839
|
// Collaboration messages short-circuit the user/agent flow.
|
|
8092
8840
|
// They don't touch runLock, the agent loop, or the message queue —
|
|
@@ -8192,27 +8940,31 @@ async function startWebUI(opts = {}) {
|
|
|
8192
8940
|
case "memory.forget":
|
|
8193
8941
|
return handleMemoryForget(ws, msg, memoryStore);
|
|
8194
8942
|
// ── MCP operations — delegated to shared handlers (mcp-handlers.ts),
|
|
8195
|
-
// backed by the live MCPRegistry constructed above.
|
|
8943
|
+
// backed by the live MCPRegistry constructed above. Routed via
|
|
8944
|
+
// handleMcpRoute (see mcpRoutes = { ... } below). These case arms
|
|
8945
|
+
// are unreachable but left as tripwires for any future regression
|
|
8946
|
+
// where the route chain stops claiming 'mcp.*'. If you see one
|
|
8947
|
+
// fire, fix the dispatch order in the handleMessage chain above.
|
|
8196
8948
|
case "mcp.list":
|
|
8197
|
-
|
|
8949
|
+
throw new Error("handleMcpRoute did not claim mcp.list \u2014 check chain order");
|
|
8198
8950
|
case "mcp.add":
|
|
8199
|
-
|
|
8200
|
-
case "mcp.remove":
|
|
8201
|
-
return handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry);
|
|
8951
|
+
throw new Error("handleMcpRoute did not claim mcp.add \u2014 check chain order");
|
|
8202
8952
|
case "mcp.update":
|
|
8203
|
-
|
|
8204
|
-
case "mcp.
|
|
8205
|
-
|
|
8206
|
-
case "mcp.sleep":
|
|
8207
|
-
return handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry);
|
|
8208
|
-
case "mcp.discover":
|
|
8209
|
-
return handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry);
|
|
8953
|
+
throw new Error("handleMcpRoute did not claim mcp.update \u2014 check chain order");
|
|
8954
|
+
case "mcp.remove":
|
|
8955
|
+
throw new Error("handleMcpRoute did not claim mcp.remove \u2014 check chain order");
|
|
8210
8956
|
case "mcp.enable":
|
|
8211
|
-
|
|
8957
|
+
throw new Error("handleMcpRoute did not claim mcp.enable \u2014 check chain order");
|
|
8212
8958
|
case "mcp.disable":
|
|
8213
|
-
|
|
8959
|
+
throw new Error("handleMcpRoute did not claim mcp.disable \u2014 check chain order");
|
|
8960
|
+
case "mcp.sleep":
|
|
8961
|
+
throw new Error("handleMcpRoute did not claim mcp.sleep \u2014 check chain order");
|
|
8962
|
+
case "mcp.wake":
|
|
8963
|
+
throw new Error("handleMcpRoute did not claim mcp.wake \u2014 check chain order");
|
|
8214
8964
|
case "mcp.restart":
|
|
8215
|
-
|
|
8965
|
+
throw new Error("handleMcpRoute did not claim mcp.restart \u2014 check chain order");
|
|
8966
|
+
case "mcp.discover":
|
|
8967
|
+
throw new Error("handleMcpRoute did not claim mcp.discover \u2014 check chain order");
|
|
8216
8968
|
// Skills — full request→response cycle lives in skills-handlers.ts
|
|
8217
8969
|
// (shared with the CLI's embedded server). skillsCtx is the closed-over
|
|
8218
8970
|
// loader/installer/projectRoot the handlers need.
|
|
@@ -8360,49 +9112,11 @@ async function startWebUI(opts = {}) {
|
|
|
8360
9112
|
break;
|
|
8361
9113
|
}
|
|
8362
9114
|
case "prefs.update": {
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
sendResult2(ws, false, parsed.message);
|
|
8366
|
-
break;
|
|
8367
|
-
}
|
|
8368
|
-
const payload = parsed.value.prefs;
|
|
8369
|
-
for (const [key, val] of Object.entries(payload)) {
|
|
8370
|
-
context.meta[key] = val;
|
|
8371
|
-
}
|
|
8372
|
-
void persistPrefsToConfig(payload);
|
|
8373
|
-
if (typeof payload["yolo"] === "boolean") {
|
|
8374
|
-
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
8375
|
-
}
|
|
8376
|
-
if (typeof payload["featureMcp"] === "boolean")
|
|
8377
|
-
config.features.mcp = payload["featureMcp"];
|
|
8378
|
-
if (typeof payload["featurePlugins"] === "boolean")
|
|
8379
|
-
config.features.plugins = payload["featurePlugins"];
|
|
8380
|
-
if (typeof payload["featureMemory"] === "boolean")
|
|
8381
|
-
config.features.memory = payload["featureMemory"];
|
|
8382
|
-
if (typeof payload["featureSkills"] === "boolean")
|
|
8383
|
-
config.features.skills = payload["featureSkills"];
|
|
8384
|
-
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
8385
|
-
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
8386
|
-
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
8387
|
-
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
8388
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
8389
|
-
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
8390
|
-
} else {
|
|
8391
|
-
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
8392
|
-
}
|
|
8393
|
-
}
|
|
8394
|
-
if (typeof payload["logLevel"] === "string") {
|
|
8395
|
-
const valid = ["debug", "info", "warn", "error"];
|
|
8396
|
-
if (valid.includes(payload["logLevel"])) {
|
|
8397
|
-
logger.level = payload["logLevel"];
|
|
8398
|
-
}
|
|
8399
|
-
}
|
|
8400
|
-
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
8401
|
-
break;
|
|
9115
|
+
void ws;
|
|
9116
|
+
throw new Error("handlePrefsRoute did not claim prefs.update \u2014 check chain order");
|
|
8402
9117
|
}
|
|
8403
9118
|
case "prefs.get": {
|
|
8404
|
-
|
|
8405
|
-
break;
|
|
9119
|
+
throw new Error("handlePrefsRoute did not claim prefs.get \u2014 check chain order");
|
|
8406
9120
|
}
|
|
8407
9121
|
default:
|
|
8408
9122
|
send(ws, {
|
|
@@ -8445,22 +9159,7 @@ async function startWebUI(opts = {}) {
|
|
|
8445
9159
|
const saved = await providerHandlers.loadConfigProviders();
|
|
8446
9160
|
send(ws, {
|
|
8447
9161
|
type: "providers.saved",
|
|
8448
|
-
payload: {
|
|
8449
|
-
providers: Object.entries(saved).map(([id, cfg]) => {
|
|
8450
|
-
const keys = normalizeKeys(cfg);
|
|
8451
|
-
return {
|
|
8452
|
-
id,
|
|
8453
|
-
family: cfg.family ?? id,
|
|
8454
|
-
baseUrl: cfg.baseUrl,
|
|
8455
|
-
apiKeys: keys.map((k) => ({
|
|
8456
|
-
label: k.label,
|
|
8457
|
-
maskedKey: maskedKey(k.apiKey),
|
|
8458
|
-
isActive: k.label === cfg.activeKey,
|
|
8459
|
-
createdAt: k.createdAt
|
|
8460
|
-
}))
|
|
8461
|
-
};
|
|
8462
|
-
})
|
|
8463
|
-
}
|
|
9162
|
+
payload: { providers: projectSavedProviders(saved) }
|
|
8464
9163
|
});
|
|
8465
9164
|
},
|
|
8466
9165
|
listProviderModels: async (ws, msg) => {
|
|
@@ -8638,6 +9337,55 @@ async function startWebUI(opts = {}) {
|
|
|
8638
9337
|
},
|
|
8639
9338
|
sessionStartPayload
|
|
8640
9339
|
});
|
|
9340
|
+
prefsRoutes = {
|
|
9341
|
+
getPrefs: async (ws) => {
|
|
9342
|
+
send(ws, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9343
|
+
},
|
|
9344
|
+
updatePrefs: async (ws, msgPayload) => {
|
|
9345
|
+
const parsed = validatePrefsUpdatePayload(msgPayload);
|
|
9346
|
+
if (!parsed.ok) {
|
|
9347
|
+
sendResult2(ws, false, parsed.message);
|
|
9348
|
+
return;
|
|
9349
|
+
}
|
|
9350
|
+
const payload = parsed.value.prefs;
|
|
9351
|
+
for (const [key, val] of Object.entries(payload)) {
|
|
9352
|
+
context.meta[key] = val;
|
|
9353
|
+
}
|
|
9354
|
+
void persistPrefsToConfig(payload);
|
|
9355
|
+
if (typeof payload["yolo"] === "boolean") {
|
|
9356
|
+
permissionPolicy.setYolo?.(payload["yolo"]);
|
|
9357
|
+
}
|
|
9358
|
+
if (typeof payload["featureMcp"] === "boolean")
|
|
9359
|
+
config.features.mcp = payload["featureMcp"];
|
|
9360
|
+
if (typeof payload["featurePlugins"] === "boolean")
|
|
9361
|
+
config.features.plugins = payload["featurePlugins"];
|
|
9362
|
+
if (typeof payload["featureMemory"] === "boolean")
|
|
9363
|
+
config.features.memory = payload["featureMemory"];
|
|
9364
|
+
if (typeof payload["featureSkills"] === "boolean")
|
|
9365
|
+
config.features.skills = payload["featureSkills"];
|
|
9366
|
+
if (typeof payload["featureModelsRegistry"] === "boolean")
|
|
9367
|
+
config.features.modelsRegistry = payload["featureModelsRegistry"];
|
|
9368
|
+
if (Array.isArray(payload["fallbackModels"]))
|
|
9369
|
+
config.fallbackModels = payload["fallbackModels"];
|
|
9370
|
+
if (typeof payload["fallbackAuto"] === "boolean")
|
|
9371
|
+
config.fallbackAuto = payload["fallbackAuto"];
|
|
9372
|
+
if (typeof payload["contextAutoCompact"] === "boolean") {
|
|
9373
|
+
if (payload["contextAutoCompact"] && autoCompactor) {
|
|
9374
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9375
|
+
pipelines.contextWindow.use({ name: "AutoCompaction", handler: autoCompactor.handler() });
|
|
9376
|
+
} else {
|
|
9377
|
+
pipelines.contextWindow.remove("AutoCompaction", { optional: true });
|
|
9378
|
+
}
|
|
9379
|
+
}
|
|
9380
|
+
if (typeof payload["logLevel"] === "string") {
|
|
9381
|
+
const valid = ["debug", "info", "warn", "error"];
|
|
9382
|
+
if (valid.includes(payload["logLevel"])) {
|
|
9383
|
+
logger.level = payload["logLevel"];
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
broadcast(clients, { type: "prefs.updated", payload: prefSnapshot() });
|
|
9387
|
+
}
|
|
9388
|
+
};
|
|
8641
9389
|
shellGitRoutes = {
|
|
8642
9390
|
gitInfo: async (ws) => {
|
|
8643
9391
|
await handleGitInfo(ws, projectRoot);
|
|
@@ -8670,7 +9418,7 @@ async function startWebUI(opts = {}) {
|
|
|
8670
9418
|
sendResult2(ws, false, parsed.message);
|
|
8671
9419
|
return;
|
|
8672
9420
|
}
|
|
8673
|
-
return handleMailboxMessages(ws, { projectRoot, globalRoot:
|
|
9421
|
+
return handleMailboxMessages(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
8674
9422
|
},
|
|
8675
9423
|
agents: (ws, msg) => {
|
|
8676
9424
|
const parsed = validateMailboxAgentsPayload(msg.payload);
|
|
@@ -8678,18 +9426,30 @@ async function startWebUI(opts = {}) {
|
|
|
8678
9426
|
sendResult2(ws, false, parsed.message);
|
|
8679
9427
|
return;
|
|
8680
9428
|
}
|
|
8681
|
-
return handleMailboxAgents(ws, { projectRoot, globalRoot:
|
|
9429
|
+
return handleMailboxAgents(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
8682
9430
|
},
|
|
8683
|
-
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot:
|
|
9431
|
+
clear: (ws) => handleMailboxClear(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }),
|
|
8684
9432
|
purge: (ws, msg) => {
|
|
8685
9433
|
const parsed = validateMailboxPurgePayload(msg.payload);
|
|
8686
9434
|
if (!parsed.ok) {
|
|
8687
9435
|
sendResult2(ws, false, parsed.message);
|
|
8688
9436
|
return;
|
|
8689
9437
|
}
|
|
8690
|
-
return handleMailboxPurge(ws, { projectRoot, globalRoot:
|
|
9438
|
+
return handleMailboxPurge(ws, { projectRoot, globalRoot: path16.dirname(globalConfigPath) }, parsed.value);
|
|
8691
9439
|
}
|
|
8692
9440
|
};
|
|
9441
|
+
mcpRoutes = {
|
|
9442
|
+
list: (ws, msg) => handleMcpList(ws, msg, globalConfigPath, mcpRegistry),
|
|
9443
|
+
add: (ws, msg) => handleMcpAdd(ws, msg, globalConfigPath, mcpRegistry),
|
|
9444
|
+
update: (ws, msg) => handleMcpUpdate(ws, msg, globalConfigPath, mcpRegistry),
|
|
9445
|
+
remove: (ws, msg) => handleMcpRemove(ws, msg, globalConfigPath, mcpRegistry),
|
|
9446
|
+
enable: (ws, msg) => handleMcpEnable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9447
|
+
disable: (ws, msg) => handleMcpDisable(ws, msg, globalConfigPath, mcpRegistry),
|
|
9448
|
+
sleep: (ws, msg) => handleMcpSleep(ws, msg, globalConfigPath, mcpRegistry),
|
|
9449
|
+
wake: (ws, msg) => handleMcpWake(ws, msg, globalConfigPath, mcpRegistry),
|
|
9450
|
+
restart: (ws, msg) => handleMcpRestart(ws, msg, globalConfigPath, mcpRegistry),
|
|
9451
|
+
discover: (ws, msg) => handleMcpDiscover(ws, msg, globalConfigPath, mcpRegistry)
|
|
9452
|
+
};
|
|
8693
9453
|
brainRoutes = {
|
|
8694
9454
|
status: (ws) => {
|
|
8695
9455
|
send(ws, {
|
|
@@ -8734,6 +9494,15 @@ async function startWebUI(opts = {}) {
|
|
|
8734
9494
|
autoPhaseRoutes = {
|
|
8735
9495
|
handleMessage: (msg) => autoPhaseHandler.handleMessage(msg)
|
|
8736
9496
|
};
|
|
9497
|
+
specsRoutes = {
|
|
9498
|
+
handleMessage: (msg) => specsHandler.handleMessage(msg)
|
|
9499
|
+
};
|
|
9500
|
+
sddBoardRoutes = {
|
|
9501
|
+
handleMessage: (msg) => sddBoardHandler.handleMessage(msg)
|
|
9502
|
+
};
|
|
9503
|
+
sddWizardRoutes = {
|
|
9504
|
+
handleMessage: (msg) => sddWizardHandler.handleMessage(msg)
|
|
9505
|
+
};
|
|
8737
9506
|
const watcherMetrics = {
|
|
8738
9507
|
fileChangesDetected: 0,
|
|
8739
9508
|
filesProcessed: 0,
|
|
@@ -8746,7 +9515,7 @@ async function startWebUI(opts = {}) {
|
|
|
8746
9515
|
};
|
|
8747
9516
|
const httpServer = createHttpServer({
|
|
8748
9517
|
host: wsHost,
|
|
8749
|
-
distDir:
|
|
9518
|
+
distDir: path16.resolve(import.meta.dirname, "../../dist"),
|
|
8750
9519
|
wsPort,
|
|
8751
9520
|
globalRoot: wpaths.globalRoot,
|
|
8752
9521
|
apiToken: wsToken,
|
|
@@ -8755,7 +9524,7 @@ async function startWebUI(opts = {}) {
|
|
|
8755
9524
|
void fleetBroadcast?.();
|
|
8756
9525
|
}
|
|
8757
9526
|
});
|
|
8758
|
-
const registryBaseDir =
|
|
9527
|
+
const registryBaseDir = path16.dirname(globalConfigPath);
|
|
8759
9528
|
httpServer.listen(httpPort, wsHost, () => {
|
|
8760
9529
|
const openUrl = `http://${wsHost}:${httpPort}`;
|
|
8761
9530
|
console.log(`[WebUI] HTTP server running on ${openUrl}`);
|
|
@@ -8767,7 +9536,7 @@ async function startWebUI(opts = {}) {
|
|
|
8767
9536
|
wsPort,
|
|
8768
9537
|
host: wsHost,
|
|
8769
9538
|
projectRoot,
|
|
8770
|
-
projectName:
|
|
9539
|
+
projectName: path16.basename(projectRoot) || projectRoot,
|
|
8771
9540
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8772
9541
|
url: `http://${wsHost}:${httpPort}`
|
|
8773
9542
|
},
|