@wrongstack/core 0.303.0 → 0.305.0
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/coordination/agents/index.js +530 -130
- package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
- package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
- package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
- package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
- package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
- package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
- package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
- package/dist/coordination/agents/types.d.ts +10 -2
- package/dist/coordination/director-prompts.d.ts +19 -6
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/fleet.d.ts +0 -6
- package/dist/coordination/index.d.ts +1 -1
- package/dist/coordination/index.js +1579 -955
- package/dist/core/agent-types.d.ts +4 -2
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/context.d.ts +15 -0
- package/dist/core/conversation-state.d.ts +14 -0
- package/dist/core/fallback-profile-manager.d.ts +70 -2
- package/dist/core/index.js +308 -108
- package/dist/core/system-prompt-blocks.d.ts +1 -1
- package/dist/core/system-prompt-builder.d.ts +13 -1
- package/dist/core/system-prompt-glossary.d.ts +73 -0
- package/dist/core/system-prompt-memory-skills.d.ts +2 -2
- package/dist/defaults/index.js +910 -693
- package/dist/execution/council-orchestrator.d.ts +3 -13
- package/dist/execution/index.js +211 -75
- package/dist/execution/one-shot-llm.d.ts +5 -0
- package/dist/hq/index.js +17 -7
- package/dist/hq/protocol/kanban.d.ts +21 -0
- package/dist/hq/protocol.js +5 -1
- package/dist/hq/redaction.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3482 -2538
- package/dist/infrastructure/index.js +247 -122
- package/dist/plugin/index.js +101 -3
- package/dist/registry/index.js +11 -0
- package/dist/registry/tool-registry.d.ts +8 -0
- package/dist/replay/hash.d.ts +9 -0
- package/dist/replay/index.js +14 -4
- package/dist/replay/replay-provider-runner.d.ts +31 -1
- package/dist/security/index.js +25 -20
- package/dist/security/secret-vault.d.ts +2 -0
- package/dist/session-catalog/index.js +62 -8
- package/dist/session-catalog/project-server.js +73 -13
- package/dist/session-catalog/protocol.d.ts +11 -4
- package/dist/session-catalog/store.d.ts +2 -2
- package/dist/storage/index.js +224 -67
- package/dist/storage/memory-consolidator.d.ts +4 -2
- package/dist/storage/session-resume-validation.d.ts +24 -0
- package/dist/storage/session-store/directory-scan.d.ts +5 -1
- package/dist/storage/session-store/fork-session.d.ts +13 -1
- package/dist/storage/session-store/load-cache.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +5 -0
- package/dist/storage/session-store.d.ts +18 -0
- package/dist/tools/index.js +174 -74
- package/dist/types/config/mcp-features.d.ts +31 -1
- package/dist/types/config/root.d.ts +12 -0
- package/dist/types/config/tools.d.ts +22 -0
- package/dist/types/default-config.d.ts +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/session.d.ts +9 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +214 -76
- package/dist/utils/project-state-guard.d.ts +21 -0
- package/dist/utils/session-scoped-path.d.ts +17 -0
- package/dist/utils/todos-format.d.ts +20 -0
- package/instructions/leader-after-task.md +3 -4
- package/instructions/system-lite.md +10 -13
- package/instructions/system-pro.md +18 -25
- package/instructions/system.md +18 -23
- package/package.json +3 -3
- package/skills/wrongstack-kanban/SKILL.md +95 -124
package/dist/utils/index.js
CHANGED
|
@@ -724,17 +724,17 @@ async function updateJsonObjectFile(filePath, mutator) {
|
|
|
724
724
|
function isForbiddenSegment(segment) {
|
|
725
725
|
return typeof segment === "string" && FORBIDDEN_PROTO_KEYS.has(segment);
|
|
726
726
|
}
|
|
727
|
-
function assertSafePath(
|
|
728
|
-
for (const segment of
|
|
727
|
+
function assertSafePath(path12) {
|
|
728
|
+
for (const segment of path12) {
|
|
729
729
|
if (isForbiddenSegment(segment)) {
|
|
730
730
|
throw new Error(`Refusing to use reserved key "${String(segment)}" in a JSON path`);
|
|
731
731
|
}
|
|
732
732
|
}
|
|
733
733
|
}
|
|
734
|
-
function getJsonPath(root,
|
|
735
|
-
if (
|
|
734
|
+
function getJsonPath(root, path12) {
|
|
735
|
+
if (path12.some(isForbiddenSegment)) return void 0;
|
|
736
736
|
let current = root;
|
|
737
|
-
for (const segment of
|
|
737
|
+
for (const segment of path12) {
|
|
738
738
|
if (typeof segment === "number") {
|
|
739
739
|
if (!Array.isArray(current)) return void 0;
|
|
740
740
|
current = current[segment];
|
|
@@ -745,14 +745,14 @@ function getJsonPath(root, path11) {
|
|
|
745
745
|
}
|
|
746
746
|
return current;
|
|
747
747
|
}
|
|
748
|
-
function setJsonPath(root,
|
|
749
|
-
assertSafePath(
|
|
750
|
-
if (
|
|
748
|
+
function setJsonPath(root, path12, value) {
|
|
749
|
+
assertSafePath(path12);
|
|
750
|
+
if (path12.length === 0) {
|
|
751
751
|
if (!isJsonObject(value)) throw new Error("Root config value must be an object");
|
|
752
752
|
return value;
|
|
753
753
|
}
|
|
754
|
-
const parent = ensureJsonParent(root,
|
|
755
|
-
const leaf = lastPathSegment(
|
|
754
|
+
const parent = ensureJsonParent(root, path12);
|
|
755
|
+
const leaf = lastPathSegment(path12);
|
|
756
756
|
if (typeof leaf === "number") {
|
|
757
757
|
if (!Array.isArray(parent))
|
|
758
758
|
throw new Error(`Cannot set numeric segment ${leaf} on non-array parent`);
|
|
@@ -763,11 +763,11 @@ function setJsonPath(root, path11, value) {
|
|
|
763
763
|
}
|
|
764
764
|
return root;
|
|
765
765
|
}
|
|
766
|
-
function removeJsonPath(root,
|
|
767
|
-
if (
|
|
768
|
-
if (
|
|
769
|
-
const parent = getJsonPath(root,
|
|
770
|
-
const leaf = lastPathSegment(
|
|
766
|
+
function removeJsonPath(root, path12) {
|
|
767
|
+
if (path12.some(isForbiddenSegment)) return false;
|
|
768
|
+
if (path12.length === 0) return false;
|
|
769
|
+
const parent = getJsonPath(root, path12.slice(0, -1));
|
|
770
|
+
const leaf = lastPathSegment(path12);
|
|
771
771
|
if (typeof leaf === "number") {
|
|
772
772
|
if (!Array.isArray(parent) || leaf < 0 || leaf >= parent.length) return false;
|
|
773
773
|
parent.splice(leaf, 1);
|
|
@@ -777,28 +777,28 @@ function removeJsonPath(root, path11) {
|
|
|
777
777
|
delete parent[leaf];
|
|
778
778
|
return true;
|
|
779
779
|
}
|
|
780
|
-
async function setJsonPathInFile(filePath,
|
|
781
|
-
return updateJsonObjectFile(filePath, (config) => setJsonPath(config,
|
|
780
|
+
async function setJsonPathInFile(filePath, path12, value) {
|
|
781
|
+
return updateJsonObjectFile(filePath, (config) => setJsonPath(config, path12, value));
|
|
782
782
|
}
|
|
783
|
-
async function removeJsonPathInFile(filePath,
|
|
783
|
+
async function removeJsonPathInFile(filePath, path12) {
|
|
784
784
|
return updateJsonObjectFile(filePath, (config) => {
|
|
785
|
-
removeJsonPath(config,
|
|
785
|
+
removeJsonPath(config, path12);
|
|
786
786
|
});
|
|
787
787
|
}
|
|
788
788
|
function isJsonObject(value) {
|
|
789
789
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
790
790
|
}
|
|
791
|
-
function lastPathSegment(
|
|
792
|
-
const segment =
|
|
791
|
+
function lastPathSegment(path12) {
|
|
792
|
+
const segment = path12[path12.length - 1];
|
|
793
793
|
if (segment === void 0) throw new Error("Invalid empty JSON path");
|
|
794
794
|
return segment;
|
|
795
795
|
}
|
|
796
|
-
function ensureJsonParent(root,
|
|
797
|
-
assertSafePath(
|
|
796
|
+
function ensureJsonParent(root, path12) {
|
|
797
|
+
assertSafePath(path12);
|
|
798
798
|
let current = root;
|
|
799
|
-
for (let i = 0; i <
|
|
800
|
-
const segment =
|
|
801
|
-
const nextSegment =
|
|
799
|
+
for (let i = 0; i < path12.length - 1; i += 1) {
|
|
800
|
+
const segment = path12[i];
|
|
801
|
+
const nextSegment = path12[i + 1];
|
|
802
802
|
if (segment === void 0) throw new Error("Invalid empty JSON path segment");
|
|
803
803
|
const nextContainer = typeof nextSegment === "number" ? [] : {};
|
|
804
804
|
if (typeof segment === "number") {
|
|
@@ -1361,6 +1361,37 @@ function isEmptyMessage(msg) {
|
|
|
1361
1361
|
return !hasMeaningfulContent(msg.content);
|
|
1362
1362
|
}
|
|
1363
1363
|
|
|
1364
|
+
// src/utils/todos-format.ts
|
|
1365
|
+
function formatTodosList(todos) {
|
|
1366
|
+
if (todos.length === 0) return "No todos.";
|
|
1367
|
+
const lines = [];
|
|
1368
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
1369
|
+
lines.push(color.dim(`Todos (${done}/${todos.length} done):`));
|
|
1370
|
+
todos.forEach((t, i) => {
|
|
1371
|
+
const mark = t.status === "completed" ? color.green("[x]") : t.status === "in_progress" ? color.yellow("[~]") : color.dim("[ ]");
|
|
1372
|
+
const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
|
|
1373
|
+
const label = t.status === "completed" ? color.dim(text) : text;
|
|
1374
|
+
lines.push(` ${color.dim(String(i + 1).padStart(2))}. ${mark} ${label}`);
|
|
1375
|
+
});
|
|
1376
|
+
return lines.join("\n");
|
|
1377
|
+
}
|
|
1378
|
+
function formatTodoForModel(todo) {
|
|
1379
|
+
const binding = todo.kanbanBoardId && todo.kanbanTaskId ? ` <kanban ${todo.kanbanBoardId}/${todo.kanbanTaskId}>` : "";
|
|
1380
|
+
const blocked = todo.blockedBy?.length ? ` [blocked by: ${todo.blockedBy.join("; ")}]` : "";
|
|
1381
|
+
return `- [${todo.status}]${blocked} ${todo.content} (${todo.id})${binding}`;
|
|
1382
|
+
}
|
|
1383
|
+
function formatTodosForModel(todos, emptyLine = "- No active todos remain.") {
|
|
1384
|
+
return todos.length ? todos.map(formatTodoForModel).join("\n") : emptyLine;
|
|
1385
|
+
}
|
|
1386
|
+
function hasKanbanBoundTodos(todos) {
|
|
1387
|
+
if (!Array.isArray(todos)) return false;
|
|
1388
|
+
return todos.some((todo) => Boolean(todo.kanbanBoardId && todo.kanbanTaskId));
|
|
1389
|
+
}
|
|
1390
|
+
function hasOpenTodos(todos) {
|
|
1391
|
+
if (!Array.isArray(todos) || todos.length === 0) return false;
|
|
1392
|
+
return todos.some((t) => t.status === "pending" || t.status === "in_progress");
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1364
1395
|
// src/utils/tool-wire-compact.ts
|
|
1365
1396
|
var TOOL_DESCRIPTION_MAX_CHARS = 400;
|
|
1366
1397
|
var SCHEMA_DESCRIPTION_MAX_CHARS = 120;
|
|
@@ -1675,25 +1706,6 @@ function resetCalibration(calibrationKey) {
|
|
|
1675
1706
|
_cals.delete(calibrationKey);
|
|
1676
1707
|
}
|
|
1677
1708
|
|
|
1678
|
-
// src/utils/todos-format.ts
|
|
1679
|
-
function formatTodosList(todos) {
|
|
1680
|
-
if (todos.length === 0) return "No todos.";
|
|
1681
|
-
const lines = [];
|
|
1682
|
-
const done = todos.filter((t) => t.status === "completed").length;
|
|
1683
|
-
lines.push(color.dim(`Todos (${done}/${todos.length} done):`));
|
|
1684
|
-
todos.forEach((t, i) => {
|
|
1685
|
-
const mark = t.status === "completed" ? color.green("[x]") : t.status === "in_progress" ? color.yellow("[~]") : color.dim("[ ]");
|
|
1686
|
-
const text = t.status === "in_progress" && t.activeForm ? t.activeForm : t.content;
|
|
1687
|
-
const label = t.status === "completed" ? color.dim(text) : text;
|
|
1688
|
-
lines.push(` ${color.dim(String(i + 1).padStart(2))}. ${mark} ${label}`);
|
|
1689
|
-
});
|
|
1690
|
-
return lines.join("\n");
|
|
1691
|
-
}
|
|
1692
|
-
function hasOpenTodos(todos) {
|
|
1693
|
-
if (!Array.isArray(todos) || todos.length === 0) return false;
|
|
1694
|
-
return todos.some((t) => t.status === "pending" || t.status === "in_progress");
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
1709
|
// src/core/agent-response.ts
|
|
1698
1710
|
var MAX_TODO_SNAPSHOT_ITEMS = 10;
|
|
1699
1711
|
var MAX_TODO_SNAPSHOT_CONTENT = 180;
|
|
@@ -1725,12 +1737,15 @@ function buildLiveNextStepsGateBlock(ctx) {
|
|
|
1725
1737
|
const todoSnapshot = openTodos.slice(0, MAX_TODO_SNAPSHOT_ITEMS).map((todo) => {
|
|
1726
1738
|
const normalized = todo.content.replace(/\s+/g, " ").trim();
|
|
1727
1739
|
const content = normalized.length > MAX_TODO_SNAPSHOT_CONTENT ? `${normalized.slice(0, MAX_TODO_SNAPSHOT_CONTENT - 1)}\u2026` : normalized;
|
|
1728
|
-
return
|
|
1740
|
+
return formatTodoForModel({ ...todo, content });
|
|
1729
1741
|
});
|
|
1730
1742
|
const omitted = openTodos.length - todoSnapshot.length;
|
|
1731
1743
|
if (omitted > 0) todoSnapshot.push(`- \u2026and ${omitted} more open todo(s)`);
|
|
1732
1744
|
const todoReconciliation = ctx.tools?.some((tool) => tool.name === "todo") ? [
|
|
1733
|
-
"Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state."
|
|
1745
|
+
"Before ending the turn, you MUST call the `todo` tool with the complete current list to reconcile actual progress: finished items completed, exactly one actively worked item in_progress, and untouched items pending. A prose claim that work is done does not update the Todo/Kanban state.",
|
|
1746
|
+
...hasKanbanBoundTodos(openTodos) ? [
|
|
1747
|
+
"Rows below carry a <kanban board/task> binding. Pass those exact ids back as `kanbanBoardId`/`kanbanTaskId` on every row you resend; a row that arrives without its binding is not applied to its card."
|
|
1748
|
+
] : []
|
|
1734
1749
|
] : [];
|
|
1735
1750
|
return {
|
|
1736
1751
|
type: "text",
|
|
@@ -2222,8 +2237,8 @@ async function expandGlob(pattern) {
|
|
|
2222
2237
|
for (const e of entries) {
|
|
2223
2238
|
const full = `${dir}${SEP}${e}`;
|
|
2224
2239
|
try {
|
|
2225
|
-
const
|
|
2226
|
-
if (
|
|
2240
|
+
const stat4 = await fsp.stat(full);
|
|
2241
|
+
if (stat4.isDirectory()) await walk2(full, rest);
|
|
2227
2242
|
} catch {
|
|
2228
2243
|
}
|
|
2229
2244
|
}
|
|
@@ -2240,8 +2255,8 @@ async function expandGlob(pattern) {
|
|
|
2240
2255
|
if (entries.includes(seg)) {
|
|
2241
2256
|
const full = `${dir}${SEP}${seg}`;
|
|
2242
2257
|
try {
|
|
2243
|
-
const
|
|
2244
|
-
if (
|
|
2258
|
+
const stat4 = await fsp.stat(full);
|
|
2259
|
+
if (stat4.isDirectory()) await walk2(full, rest);
|
|
2245
2260
|
} catch {
|
|
2246
2261
|
}
|
|
2247
2262
|
}
|
|
@@ -2664,8 +2679,8 @@ async function pruneOldSessions(root, currentDir) {
|
|
|
2664
2679
|
const sessions = await Promise.all(
|
|
2665
2680
|
entries.filter((entry) => entry.isDirectory() && entry.name !== path4.basename(currentDir)).map(async (entry) => {
|
|
2666
2681
|
const fullPath = path4.join(root, entry.name);
|
|
2667
|
-
const
|
|
2668
|
-
return { fullPath, mtimeMs:
|
|
2682
|
+
const stat4 = await fsp2.stat(fullPath);
|
|
2683
|
+
return { fullPath, mtimeMs: stat4.mtimeMs };
|
|
2669
2684
|
})
|
|
2670
2685
|
);
|
|
2671
2686
|
sessions.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -3509,10 +3524,10 @@ function validateAgainstSchema(value, schema) {
|
|
|
3509
3524
|
return { ok: errors.length === 0, errors };
|
|
3510
3525
|
}
|
|
3511
3526
|
var MAX_SCHEMA_DEPTH = 64;
|
|
3512
|
-
function walk(value, schema,
|
|
3527
|
+
function walk(value, schema, path12, errors, depth) {
|
|
3513
3528
|
if (depth > MAX_SCHEMA_DEPTH) {
|
|
3514
3529
|
errors.push({
|
|
3515
|
-
path:
|
|
3530
|
+
path: path12 || "<root>",
|
|
3516
3531
|
message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
|
|
3517
3532
|
});
|
|
3518
3533
|
return;
|
|
@@ -3520,7 +3535,7 @@ function walk(value, schema, path11, errors, depth) {
|
|
|
3520
3535
|
if (schema.enum !== void 0) {
|
|
3521
3536
|
if (!enumIncludes(schema.enum, value)) {
|
|
3522
3537
|
errors.push({
|
|
3523
|
-
path:
|
|
3538
|
+
path: path12 || "<root>",
|
|
3524
3539
|
message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
|
|
3525
3540
|
});
|
|
3526
3541
|
return;
|
|
@@ -3529,7 +3544,7 @@ function walk(value, schema, path11, errors, depth) {
|
|
|
3529
3544
|
if (typeof schema.type === "string") {
|
|
3530
3545
|
if (!checkType(value, schema.type)) {
|
|
3531
3546
|
errors.push({
|
|
3532
|
-
path:
|
|
3547
|
+
path: path12 || "<root>",
|
|
3533
3548
|
message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
|
|
3534
3549
|
});
|
|
3535
3550
|
return;
|
|
@@ -3541,7 +3556,7 @@ function walk(value, schema, path11, errors, depth) {
|
|
|
3541
3556
|
if (!(req in obj)) {
|
|
3542
3557
|
const expected = schema.properties?.[req]?.type;
|
|
3543
3558
|
errors.push({
|
|
3544
|
-
path: joinPath(
|
|
3559
|
+
path: joinPath(path12, req),
|
|
3545
3560
|
message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
|
|
3546
3561
|
});
|
|
3547
3562
|
}
|
|
@@ -3549,14 +3564,14 @@ function walk(value, schema, path11, errors, depth) {
|
|
|
3549
3564
|
if (schema.properties) {
|
|
3550
3565
|
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
3551
3566
|
if (key in obj) {
|
|
3552
|
-
walk(obj[key], subSchema, joinPath(
|
|
3567
|
+
walk(obj[key], subSchema, joinPath(path12, key), errors, depth + 1);
|
|
3553
3568
|
}
|
|
3554
3569
|
}
|
|
3555
3570
|
}
|
|
3556
3571
|
}
|
|
3557
3572
|
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
3558
3573
|
for (let i = 0; i < value.length; i++) {
|
|
3559
|
-
walk(value[i], schema.items, `${
|
|
3574
|
+
walk(value[i], schema.items, `${path12}[${i}]`, errors, depth + 1);
|
|
3560
3575
|
}
|
|
3561
3576
|
}
|
|
3562
3577
|
}
|
|
@@ -4061,24 +4076,133 @@ function isAlreadyExistsError(error) {
|
|
|
4061
4076
|
return error?.code === "EEXIST";
|
|
4062
4077
|
}
|
|
4063
4078
|
|
|
4064
|
-
// src/utils/project-
|
|
4079
|
+
// src/utils/project-state-guard.ts
|
|
4065
4080
|
import * as fs6 from "node:fs";
|
|
4081
|
+
import * as fsp4 from "node:fs/promises";
|
|
4066
4082
|
import * as path9 from "node:path";
|
|
4067
|
-
var
|
|
4083
|
+
var PROJECT_STATE_DIRECTORY = ".wrongstack";
|
|
4084
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
4085
|
+
var REPAIR_DEBOUNCE_MS = 25;
|
|
4086
|
+
var ACTIVE_GUARD_KEY = /* @__PURE__ */ Symbol.for("wrongstack.activeProjectStateGuard");
|
|
4068
4087
|
var globalScope = globalThis;
|
|
4069
|
-
|
|
4070
|
-
var
|
|
4071
|
-
function
|
|
4088
|
+
globalScope[ACTIVE_GUARD_KEY] ??= {};
|
|
4089
|
+
var activeGuard = globalScope[ACTIVE_GUARD_KEY];
|
|
4090
|
+
function normalizedRoot(root) {
|
|
4072
4091
|
const resolved = path9.resolve(root);
|
|
4073
4092
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
4074
4093
|
}
|
|
4094
|
+
function concernsProjectState(filename) {
|
|
4095
|
+
if (filename === null) return true;
|
|
4096
|
+
const firstSegment = String(filename).replaceAll("\\", "/").split("/")[0];
|
|
4097
|
+
return firstSegment?.toLowerCase() === PROJECT_STATE_DIRECTORY;
|
|
4098
|
+
}
|
|
4099
|
+
async function startProjectStateGuard(projectRoot, options = {}) {
|
|
4100
|
+
const root = path9.resolve(projectRoot);
|
|
4101
|
+
const directory = path9.join(root, PROJECT_STATE_DIRECTORY);
|
|
4102
|
+
const pollIntervalMs = Math.max(25, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
4103
|
+
let closed = false;
|
|
4104
|
+
let repairPending = false;
|
|
4105
|
+
let repairInFlight;
|
|
4106
|
+
let repairTimer;
|
|
4107
|
+
const ensureDirectory = async () => {
|
|
4108
|
+
await fsp4.mkdir(directory, { recursive: true });
|
|
4109
|
+
};
|
|
4110
|
+
const ensureInvariant = async () => {
|
|
4111
|
+
await ensureDirectory();
|
|
4112
|
+
try {
|
|
4113
|
+
await ensureProjectGitignore(root);
|
|
4114
|
+
} catch (error) {
|
|
4115
|
+
options.onError?.(error);
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
const scheduleRepair = () => {
|
|
4119
|
+
if (closed) return;
|
|
4120
|
+
repairPending = true;
|
|
4121
|
+
if (repairInFlight || repairTimer) return;
|
|
4122
|
+
repairTimer = setTimeout(() => {
|
|
4123
|
+
repairTimer = void 0;
|
|
4124
|
+
repairInFlight = (async () => {
|
|
4125
|
+
while (!closed && repairPending) {
|
|
4126
|
+
repairPending = false;
|
|
4127
|
+
try {
|
|
4128
|
+
const rootState = await fsp4.stat(root).catch(() => void 0);
|
|
4129
|
+
if (!rootState?.isDirectory()) return;
|
|
4130
|
+
await ensureInvariant();
|
|
4131
|
+
} catch (error) {
|
|
4132
|
+
options.onError?.(error);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
})().finally(() => {
|
|
4136
|
+
repairInFlight = void 0;
|
|
4137
|
+
if (repairPending && !closed) scheduleRepair();
|
|
4138
|
+
});
|
|
4139
|
+
}, REPAIR_DEBOUNCE_MS);
|
|
4140
|
+
repairTimer.unref?.();
|
|
4141
|
+
};
|
|
4142
|
+
await ensureDirectory();
|
|
4143
|
+
await ensureProjectGitignore(root).catch((error) => options.onError?.(error));
|
|
4144
|
+
let watcher;
|
|
4145
|
+
try {
|
|
4146
|
+
watcher = fs6.watch(root, { persistent: false }, (_eventType, filename) => {
|
|
4147
|
+
if (concernsProjectState(filename) || String(filename).toLowerCase() === ".gitignore") {
|
|
4148
|
+
scheduleRepair();
|
|
4149
|
+
}
|
|
4150
|
+
});
|
|
4151
|
+
watcher.on("error", (error) => {
|
|
4152
|
+
options.onError?.(error);
|
|
4153
|
+
watcher = void 0;
|
|
4154
|
+
scheduleRepair();
|
|
4155
|
+
});
|
|
4156
|
+
} catch (error) {
|
|
4157
|
+
options.onError?.(error);
|
|
4158
|
+
}
|
|
4159
|
+
const poll = setInterval(scheduleRepair, pollIntervalMs);
|
|
4160
|
+
poll.unref?.();
|
|
4161
|
+
return {
|
|
4162
|
+
directory,
|
|
4163
|
+
close() {
|
|
4164
|
+
if (closed) return;
|
|
4165
|
+
closed = true;
|
|
4166
|
+
clearInterval(poll);
|
|
4167
|
+
if (repairTimer) clearTimeout(repairTimer);
|
|
4168
|
+
repairTimer = void 0;
|
|
4169
|
+
try {
|
|
4170
|
+
watcher?.close();
|
|
4171
|
+
} catch {
|
|
4172
|
+
}
|
|
4173
|
+
watcher = void 0;
|
|
4174
|
+
}
|
|
4175
|
+
};
|
|
4176
|
+
}
|
|
4177
|
+
async function activateProjectStateGuard(projectRoot, options = {}) {
|
|
4178
|
+
const root = normalizedRoot(projectRoot);
|
|
4179
|
+
if (activeGuard.root === root && activeGuard.guard) return activeGuard.guard;
|
|
4180
|
+
const next = await startProjectStateGuard(projectRoot, options);
|
|
4181
|
+
const previous = activeGuard.guard;
|
|
4182
|
+
activeGuard.root = root;
|
|
4183
|
+
activeGuard.guard = next;
|
|
4184
|
+
previous?.close();
|
|
4185
|
+
return next;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
// src/utils/project-watch.ts
|
|
4189
|
+
import * as fs7 from "node:fs";
|
|
4190
|
+
import * as path10 from "node:path";
|
|
4191
|
+
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("wrongstack.projectWatchRegistry");
|
|
4192
|
+
var globalScope2 = globalThis;
|
|
4193
|
+
if (!globalScope2[REGISTRY_KEY]) globalScope2[REGISTRY_KEY] = /* @__PURE__ */ new Map();
|
|
4194
|
+
var registry = globalScope2[REGISTRY_KEY];
|
|
4195
|
+
function registryKey(root) {
|
|
4196
|
+
const resolved = path10.resolve(root);
|
|
4197
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
4198
|
+
}
|
|
4075
4199
|
function watchProjectTree(root, listener, opts) {
|
|
4076
4200
|
const key = registryKey(root);
|
|
4077
4201
|
let entry = registry.get(key);
|
|
4078
4202
|
if (!entry || entry.dead) {
|
|
4079
4203
|
const subscribers = /* @__PURE__ */ new Set();
|
|
4080
|
-
const watcher =
|
|
4081
|
-
|
|
4204
|
+
const watcher = fs7.watch(
|
|
4205
|
+
path10.resolve(root),
|
|
4082
4206
|
{ recursive: true, persistent: false },
|
|
4083
4207
|
(eventType, filename) => {
|
|
4084
4208
|
const event = {
|
|
@@ -4394,14 +4518,14 @@ function stripJsonComments(s) {
|
|
|
4394
4518
|
}
|
|
4395
4519
|
|
|
4396
4520
|
// src/utils/session-scoped-path.ts
|
|
4397
|
-
import * as
|
|
4521
|
+
import * as path11 from "node:path";
|
|
4398
4522
|
function sessionScopedPath(dir, sessionId, suffix) {
|
|
4399
4523
|
if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
4400
4524
|
throw invalid(sessionId);
|
|
4401
4525
|
}
|
|
4402
|
-
const resolved =
|
|
4403
|
-
const rel =
|
|
4404
|
-
if (rel.startsWith("..") ||
|
|
4526
|
+
const resolved = path11.resolve(dir, `${sessionId}${suffix}`);
|
|
4527
|
+
const rel = path11.relative(path11.resolve(dir), resolved);
|
|
4528
|
+
if (rel.startsWith("..") || path11.isAbsolute(rel)) {
|
|
4405
4529
|
throw invalid(sessionId);
|
|
4406
4530
|
}
|
|
4407
4531
|
return resolved;
|
|
@@ -4417,7 +4541,7 @@ function invalid(sessionId) {
|
|
|
4417
4541
|
|
|
4418
4542
|
// src/utils/sleep.ts
|
|
4419
4543
|
function sleep(ms) {
|
|
4420
|
-
return new Promise((
|
|
4544
|
+
return new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
4421
4545
|
}
|
|
4422
4546
|
|
|
4423
4547
|
// src/utils/slug.ts
|
|
@@ -4678,20 +4802,29 @@ function findWordBoundary(text, limit) {
|
|
|
4678
4802
|
}
|
|
4679
4803
|
|
|
4680
4804
|
// src/utils/tool-name.ts
|
|
4805
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4681
4806
|
var WIRE_TOOL_NAME_MAX_LENGTH = 128;
|
|
4682
4807
|
var WIRE_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
4808
|
+
var MCP_SERVER_SEGMENT_MAX_LENGTH = 48;
|
|
4809
|
+
var MCP_IDENTITY_HASH_LENGTH = 10;
|
|
4683
4810
|
function sanitizeWireToolName(name) {
|
|
4684
4811
|
const replaced = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4685
4812
|
const clamped = replaced.slice(0, WIRE_TOOL_NAME_MAX_LENGTH);
|
|
4686
4813
|
return clamped.length > 0 ? clamped : "tool";
|
|
4687
4814
|
}
|
|
4815
|
+
function collisionSafeMcpSegment(value, maxLength) {
|
|
4816
|
+
const replaced = value.replace(/[^a-zA-Z0-9_-]/g, "_") || "tool";
|
|
4817
|
+
if (replaced === value && replaced.length <= maxLength) return replaced;
|
|
4818
|
+
const suffix = `_${createHash3("sha256").update(value, "utf8").digest("hex").slice(0, MCP_IDENTITY_HASH_LENGTH)}`;
|
|
4819
|
+
return `${replaced.slice(0, Math.max(1, maxLength - suffix.length))}${suffix}`;
|
|
4820
|
+
}
|
|
4688
4821
|
function mcpServerToolPrefix(serverName) {
|
|
4689
|
-
return `mcp__${
|
|
4822
|
+
return `mcp__${collisionSafeMcpSegment(serverName, MCP_SERVER_SEGMENT_MAX_LENGTH)}__`;
|
|
4690
4823
|
}
|
|
4691
4824
|
function mcpQualifiedToolName(serverName, toolName) {
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4825
|
+
const prefix = mcpServerToolPrefix(serverName);
|
|
4826
|
+
const toolSegment = collisionSafeMcpSegment(toolName, WIRE_TOOL_NAME_MAX_LENGTH - prefix.length);
|
|
4827
|
+
return `${prefix}${toolSegment}`;
|
|
4695
4828
|
}
|
|
4696
4829
|
|
|
4697
4830
|
// src/utils/tool-output-serializer.ts
|
|
@@ -5599,6 +5732,7 @@ export {
|
|
|
5599
5732
|
TerminalLifecycle,
|
|
5600
5733
|
WIRE_TOOL_NAME_MAX_LENGTH,
|
|
5601
5734
|
WIRE_TOOL_NAME_PATTERN,
|
|
5735
|
+
activateProjectStateGuard,
|
|
5602
5736
|
applyToolDescriptionModeToTool,
|
|
5603
5737
|
applyToolDescriptionModes,
|
|
5604
5738
|
applyToolResultRenderModes,
|
|
@@ -5664,6 +5798,8 @@ export {
|
|
|
5664
5798
|
formatCompletedWorkLedger,
|
|
5665
5799
|
formatTaskList,
|
|
5666
5800
|
formatTaskProgress,
|
|
5801
|
+
formatTodoForModel,
|
|
5802
|
+
formatTodosForModel,
|
|
5667
5803
|
formatTodosList,
|
|
5668
5804
|
getCalibrationState,
|
|
5669
5805
|
getChildEnvGitIdentity,
|
|
@@ -5672,6 +5808,7 @@ export {
|
|
|
5672
5808
|
getPerfProfile,
|
|
5673
5809
|
getToolDescriptionMode,
|
|
5674
5810
|
getToolResultRenderMode,
|
|
5811
|
+
hasKanbanBoundTodos,
|
|
5675
5812
|
hasMeaningfulContent,
|
|
5676
5813
|
hasOpenTodos,
|
|
5677
5814
|
indexParallelBatchSize,
|
|
@@ -5750,6 +5887,7 @@ export {
|
|
|
5750
5887
|
splitSageOutputBlock,
|
|
5751
5888
|
sqliteCachePragmas,
|
|
5752
5889
|
startHeapWatchdog,
|
|
5890
|
+
startProjectStateGuard,
|
|
5753
5891
|
startSharedHeapWatchdog,
|
|
5754
5892
|
stripAnsi,
|
|
5755
5893
|
stripCodeFences,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ProjectStateGuard {
|
|
2
|
+
readonly directory: string;
|
|
3
|
+
close(): void;
|
|
4
|
+
}
|
|
5
|
+
export interface ProjectStateGuardOptions {
|
|
6
|
+
/** Fallback for platforms/filesystems that drop watch events. */
|
|
7
|
+
pollIntervalMs?: number | undefined;
|
|
8
|
+
onError?: ((error: unknown) => void) | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Keep the active project's state directory present for the lifetime of a
|
|
12
|
+
* runtime. The root watcher repairs direct deletion immediately; an unref'ed
|
|
13
|
+
* poll covers filesystems where watch events are lossy or unsupported.
|
|
14
|
+
*/
|
|
15
|
+
export declare function startProjectStateGuard(projectRoot: string, options?: ProjectStateGuardOptions): Promise<ProjectStateGuard>;
|
|
16
|
+
/**
|
|
17
|
+
* Activate the process-wide guard for one project. Switching projects closes
|
|
18
|
+
* the previous guard only after the next directory has been established.
|
|
19
|
+
*/
|
|
20
|
+
export declare function activateProjectStateGuard(projectRoot: string, options?: ProjectStateGuardOptions): Promise<ProjectStateGuard>;
|
|
21
|
+
//# sourceMappingURL=project-state-guard.d.ts.map
|
|
@@ -10,4 +10,21 @@
|
|
|
10
10
|
* several stores ended up throwing on every modern session id.
|
|
11
11
|
*/
|
|
12
12
|
export declare function sessionScopedPath(dir: string, sessionId: string, suffix: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* True when a file name in the sessions directory is a session transcript.
|
|
15
|
+
*
|
|
16
|
+
* Every directory scan needs this, and each one used to answer it for itself:
|
|
17
|
+
* `SessionRecovery` filtered sidecars, the store's `collectSessionIds` /
|
|
18
|
+
* `collectSessionFiles` did not, and the catalog daemon's `rebuildCatalog`
|
|
19
|
+
* excluded `_index.jsonl` alone. The scans that did not filter turned every
|
|
20
|
+
* sidecar into a phantom session — `sess_x.replay` and `_mailbox` were listed
|
|
21
|
+
* next to `sess_x` in the picker, written into `_index.jsonl` by
|
|
22
|
+
* `rebuildIndex()`, and counted as candidates by prefix resolution, so a
|
|
23
|
+
* session id that had been unique became an ambiguous prefix as soon as the
|
|
24
|
+
* session recorded a replay log.
|
|
25
|
+
*
|
|
26
|
+
* One predicate, so a new sidecar suffix cannot be added to one scanner's
|
|
27
|
+
* blocklist and forgotten in the others.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isSessionTranscriptFileName(name: string): boolean;
|
|
13
30
|
//# sourceMappingURL=session-scoped-path.d.ts.map
|
|
@@ -13,6 +13,26 @@ import type { TodoItem } from '../core/context.js';
|
|
|
13
13
|
* straight to a history dispatcher or stdout.
|
|
14
14
|
*/
|
|
15
15
|
export declare function formatTodosList(todos: TodoItem[]): string;
|
|
16
|
+
/**
|
|
17
|
+
* Render one todo row for the model, including its Kanban binding.
|
|
18
|
+
*
|
|
19
|
+
* The binding is the whole contract: with Kanban active a row is not an
|
|
20
|
+
* independent note, it is a projection of a real card, and the model is
|
|
21
|
+
* required to echo `kanbanBoardId`/`kanbanTaskId` back on the next `todo`
|
|
22
|
+
* call or the card cannot be advanced. Every surface that replayed live todo
|
|
23
|
+
* state to the model used to strip these ids, so the only place they appeared
|
|
24
|
+
* was the `todo` tool's own return value — which ages out of context. Once it
|
|
25
|
+
* did, the next update fell back to fuzzy title matching, and a row whose
|
|
26
|
+
* title had drifted silently stopped applying to its card.
|
|
27
|
+
*/
|
|
28
|
+
export declare function formatTodoForModel(todo: TodoItem): string;
|
|
29
|
+
/** Multi-line `formatTodoForModel` rendering, or a stable empty-state line. */
|
|
30
|
+
export declare function formatTodosForModel(todos: readonly TodoItem[], emptyLine?: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* True when at least one row is bound to a real Kanban card, meaning the list
|
|
33
|
+
* is a managed projection rather than free-standing session state.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hasKanbanBoundTodos(todos: readonly TodoItem[] | undefined | null): boolean;
|
|
16
36
|
/**
|
|
17
37
|
* True when the todos list still has at least one unfinished item — either
|
|
18
38
|
* `pending` (not started) or `in_progress` (underway). The REPL and other
|
|
@@ -27,13 +27,12 @@ Format — one numbered line per item, ordered by priority:
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Rules:
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
30
|
+
- **Only prompt-message sentences belong inside `<nextsteps>`.** Every item must be a complete sentence that can be fed verbatim back to the agent as its next prompt — "Run the test suite and fix any failures" is valid; "tests" or "fix bugs" is not. It should ask the agent to perform useful work; it does not need to be a shell command.
|
|
31
|
+
- **Never address the user inside `<nextsteps>`.** Items like "Open DevTools and check the console yourself" or "Click the settings gear to verify" are forbidden — they leave work to the human. Write agent-directed prompts; if the agent has suitable tools, instruct it to use them and act on the result. Human-only actions may appear as informational prose outside the tag, but never inside it.
|
|
32
|
+
- **If there are genuinely no useful follow-on actions, omit the tag entirely.** Do not pad with generic filler, repeat completed work, or invent work merely to satisfy the format. Use the explicit no-further-steps prose branch (decision tree point 3) instead.
|
|
33
33
|
- The opening tag must be exactly `<nextsteps>` with no attributes. Never emit `<nextsteps auto="true">` or attach any other metadata to the tag.
|
|
34
34
|
- At most one item may have ` auto="true"`, and it must be item 1. Add it only when that first prompt is safe to run unattended (YOLO+auto mode executes it verbatim); the prompt must be complete and copy-paste-ready.
|
|
35
35
|
- **Omit the tag entirely while the live `ctx.todos` list has any `pending` or `in_progress` item.** Finishing the in-flight todo list takes priority, and the runtime discards `<nextsteps>` in that state anyway. Emit it again on the turn the last todo flips to `completed`.
|
|
36
|
-
- Do not pad the block with generic filler, repeat completed work, or invent work merely to satisfy the format. Use the explicit no-further-steps branch when appropriate.
|
|
37
36
|
|
|
38
37
|
<!--ws:if tool=remember-->
|
|
39
38
|
**After a significant task, when `remember` is live, remember durable key findings** — established conventions, confirmed decisions, or stable facts likely to help a future session. Pick the most specific `kind`, set `importance`, add tags, and `anchor` to the relevant file/symbol when applicable.
|
|
@@ -44,13 +44,13 @@ If verification fails twice for unclear reasons, stop and re-read the source ins
|
|
|
44
44
|
<!--ws:if tool=kanban-->
|
|
45
45
|
## Work planning with Kanban
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
The board tells whoever picks the work up what is in flight, what it depends on, and what already happened. It is a record, not a checkpoint. Put substantial or multi-step work on it; a trivial edit or a question does not need a card. Resume the existing card for the same request.
|
|
48
48
|
|
|
49
49
|
If multiple boards are active or card identity is unclear, read the bounded Kanban `workbench` first. Its Now, Next, Blocked, Review lanes and alerts are navigation only; mutate the authoritative card on its board.
|
|
50
50
|
|
|
51
|
-
Use one
|
|
51
|
+
Use one childless leaf card for atomic work, and a parent with dependency-ordered children only for genuinely composite work; never invent subtasks for process theatre. **The board follows the work, the work does not wait on the board.** If persistence fails, say so and keep working rather than stalling.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
A useful card usually carries:
|
|
54
54
|
- **Description** — what needs to be done
|
|
55
55
|
- **Verification** — how success is measured
|
|
56
56
|
- **Risk level** — low / medium / high
|
|
@@ -60,17 +60,14 @@ Scale the number of cards to the work, never the existence of tracking.
|
|
|
60
60
|
|
|
61
61
|
## Kanban Agent hard conditions
|
|
62
62
|
|
|
63
|
-
These
|
|
63
|
+
These apply to what you write on the board, not to whether you may work; none is a reason to stall:
|
|
64
64
|
|
|
65
|
-
1. **Never abandon or misrepresent work.** Do not
|
|
66
|
-
2. **
|
|
67
|
-
3. **
|
|
68
|
-
4. **
|
|
69
|
-
5. **
|
|
70
|
-
6. **Keep Contract Map off the critical path.** Use the card description and executable acceptance criteria for normal work. Do not create, inspect, configure, or repair graph nodes unless the user explicitly asks for graph work, and never enable strict mode yourself. No Contract Map mode may delay start, implementation, verification, or card completion; surface existing strict-map issues as operator audit signals without stopping work to repair them.
|
|
71
|
-
7. **Never shrink tracked scope by omission.** Todo, task, and plan rows carry Kanban requirement identity. Preserve every unfinished row and binding in full-list updates, and complete it before removal; only an explicit operator-controlled cancellation or migration path may retire unresolved coverage.
|
|
65
|
+
1. **Never abandon or misrepresent work.** Do not claim success while work remains or call a task done with incomplete acceptance criteria. If blocked, keep the card out of Done and record the blocker on it.
|
|
66
|
+
2. **Describe a card well enough to be picked up by someone else.** Fill the description, owner, acceptance criteria and dependencies you actually know; a thin card beats untracked work. Only composite parents (`atomic: true`) need persisted `childTaskIds`; an executable leaf card stays childless.
|
|
67
|
+
3. **Keep the board current as you go.** Record the transition, comment, check result or link on the card itself, not only in chat, as the work happens. Do not leave finished work sitting in Running. Updating the card follows the action; it does not authorize it.
|
|
68
|
+
4. **Managed boards have a fixed column order.** Cards move `Backlog → Todo → Running → Review → Done`, one step at a time. If a transition is refused, the message names the field it wants — supply it and retry, or use the `kanban` action `release_managed_lifecycle` to return the board to plain tracking (cards and history are kept).
|
|
69
|
+
5. **Never shrink tracked scope by omission.** Todo, task, and plan rows carry Kanban requirement identity. Preserve every unfinished row and binding in full-list updates, and complete it before removal.
|
|
72
70
|
|
|
73
|
-
If a managed transition is rejected, repair the card details or evidence and retry the same transition. Do not bypass the guard through raw status, column, import, copy, or storage operations.
|
|
74
71
|
<!--ws:end-->
|
|
75
72
|
|
|
76
73
|
## Filesystem and code discovery
|
|
@@ -208,7 +205,7 @@ Use `plan` for work that spans turns.
|
|
|
208
205
|
Use `task` for structured cross-session work.
|
|
209
206
|
<!--ws:end-->
|
|
210
207
|
<!--ws:if tool=kanban-->
|
|
211
|
-
Use `kanban`
|
|
208
|
+
Use `kanban` to record substantial work on the durable board so it survives the session.
|
|
212
209
|
For managed Kanban cards, follow the board lifecycle exactly and persist truthful progress.
|
|
213
210
|
<!--ws:end-->
|
|
214
211
|
<!--ws:if tool=mail_inbox,mailbox-->
|