@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.
Files changed (73) hide show
  1. package/dist/coordination/agents/index.js +530 -130
  2. package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
  3. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  4. package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
  5. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  6. package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
  7. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  8. package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
  9. package/dist/coordination/agents/types.d.ts +10 -2
  10. package/dist/coordination/director-prompts.d.ts +19 -6
  11. package/dist/coordination/director-tools.d.ts +2 -2
  12. package/dist/coordination/fleet.d.ts +0 -6
  13. package/dist/coordination/index.d.ts +1 -1
  14. package/dist/coordination/index.js +1579 -955
  15. package/dist/core/agent-types.d.ts +4 -2
  16. package/dist/core/agent.d.ts +1 -0
  17. package/dist/core/context.d.ts +15 -0
  18. package/dist/core/conversation-state.d.ts +14 -0
  19. package/dist/core/fallback-profile-manager.d.ts +70 -2
  20. package/dist/core/index.js +308 -108
  21. package/dist/core/system-prompt-blocks.d.ts +1 -1
  22. package/dist/core/system-prompt-builder.d.ts +13 -1
  23. package/dist/core/system-prompt-glossary.d.ts +73 -0
  24. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  25. package/dist/defaults/index.js +910 -693
  26. package/dist/execution/council-orchestrator.d.ts +3 -13
  27. package/dist/execution/index.js +211 -75
  28. package/dist/execution/one-shot-llm.d.ts +5 -0
  29. package/dist/hq/index.js +17 -7
  30. package/dist/hq/protocol/kanban.d.ts +21 -0
  31. package/dist/hq/protocol.js +5 -1
  32. package/dist/hq/redaction.d.ts +14 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +3482 -2538
  35. package/dist/infrastructure/index.js +247 -122
  36. package/dist/plugin/index.js +101 -3
  37. package/dist/registry/index.js +11 -0
  38. package/dist/registry/tool-registry.d.ts +8 -0
  39. package/dist/replay/hash.d.ts +9 -0
  40. package/dist/replay/index.js +14 -4
  41. package/dist/replay/replay-provider-runner.d.ts +31 -1
  42. package/dist/security/index.js +25 -20
  43. package/dist/security/secret-vault.d.ts +2 -0
  44. package/dist/session-catalog/index.js +62 -8
  45. package/dist/session-catalog/project-server.js +73 -13
  46. package/dist/session-catalog/protocol.d.ts +11 -4
  47. package/dist/session-catalog/store.d.ts +2 -2
  48. package/dist/storage/index.js +224 -67
  49. package/dist/storage/memory-consolidator.d.ts +4 -2
  50. package/dist/storage/session-resume-validation.d.ts +24 -0
  51. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  52. package/dist/storage/session-store/fork-session.d.ts +13 -1
  53. package/dist/storage/session-store/load-cache.d.ts +11 -0
  54. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  55. package/dist/storage/session-store.d.ts +18 -0
  56. package/dist/tools/index.js +174 -74
  57. package/dist/types/config/mcp-features.d.ts +31 -1
  58. package/dist/types/config/root.d.ts +12 -0
  59. package/dist/types/config/tools.d.ts +22 -0
  60. package/dist/types/default-config.d.ts +1 -0
  61. package/dist/types/index.js +3 -0
  62. package/dist/types/session.d.ts +9 -1
  63. package/dist/utils/index.d.ts +1 -0
  64. package/dist/utils/index.js +214 -76
  65. package/dist/utils/project-state-guard.d.ts +21 -0
  66. package/dist/utils/session-scoped-path.d.ts +17 -0
  67. package/dist/utils/todos-format.d.ts +20 -0
  68. package/instructions/leader-after-task.md +3 -4
  69. package/instructions/system-lite.md +10 -13
  70. package/instructions/system-pro.md +18 -25
  71. package/instructions/system.md +18 -23
  72. package/package.json +3 -3
  73. package/skills/wrongstack-kanban/SKILL.md +95 -124
@@ -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(path11) {
728
- for (const segment of path11) {
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, path11) {
735
- if (path11.some(isForbiddenSegment)) return void 0;
734
+ function getJsonPath(root, path12) {
735
+ if (path12.some(isForbiddenSegment)) return void 0;
736
736
  let current = root;
737
- for (const segment of path11) {
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, path11, value) {
749
- assertSafePath(path11);
750
- if (path11.length === 0) {
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, path11);
755
- const leaf = lastPathSegment(path11);
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, path11) {
767
- if (path11.some(isForbiddenSegment)) return false;
768
- if (path11.length === 0) return false;
769
- const parent = getJsonPath(root, path11.slice(0, -1));
770
- const leaf = lastPathSegment(path11);
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, path11, value) {
781
- return updateJsonObjectFile(filePath, (config) => setJsonPath(config, path11, value));
780
+ async function setJsonPathInFile(filePath, path12, value) {
781
+ return updateJsonObjectFile(filePath, (config) => setJsonPath(config, path12, value));
782
782
  }
783
- async function removeJsonPathInFile(filePath, path11) {
783
+ async function removeJsonPathInFile(filePath, path12) {
784
784
  return updateJsonObjectFile(filePath, (config) => {
785
- removeJsonPath(config, path11);
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(path11) {
792
- const segment = path11[path11.length - 1];
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, path11) {
797
- assertSafePath(path11);
796
+ function ensureJsonParent(root, path12) {
797
+ assertSafePath(path12);
798
798
  let current = root;
799
- for (let i = 0; i < path11.length - 1; i += 1) {
800
- const segment = path11[i];
801
- const nextSegment = path11[i + 1];
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 `- [${todo.status}] ${content}`;
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 stat3 = await fsp.stat(full);
2226
- if (stat3.isDirectory()) await walk2(full, rest);
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 stat3 = await fsp.stat(full);
2244
- if (stat3.isDirectory()) await walk2(full, rest);
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 stat3 = await fsp2.stat(fullPath);
2668
- return { fullPath, mtimeMs: stat3.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, path11, errors, depth) {
3527
+ function walk(value, schema, path12, errors, depth) {
3513
3528
  if (depth > MAX_SCHEMA_DEPTH) {
3514
3529
  errors.push({
3515
- path: path11 || "<root>",
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: path11 || "<root>",
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: path11 || "<root>",
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(path11, req),
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(path11, key), errors, depth + 1);
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, `${path11}[${i}]`, errors, depth + 1);
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-watch.ts
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 REGISTRY_KEY = /* @__PURE__ */ Symbol.for("wrongstack.projectWatchRegistry");
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
- if (!globalScope[REGISTRY_KEY]) globalScope[REGISTRY_KEY] = /* @__PURE__ */ new Map();
4070
- var registry = globalScope[REGISTRY_KEY];
4071
- function registryKey(root) {
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 = fs6.watch(
4081
- path9.resolve(root),
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 path10 from "node:path";
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 = path10.resolve(dir, `${sessionId}${suffix}`);
4403
- const rel = path10.relative(path10.resolve(dir), resolved);
4404
- if (rel.startsWith("..") || path10.isAbsolute(rel)) {
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((resolve9) => setTimeout(resolve9, ms));
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__${sanitizeWireToolName(serverName)}__`;
4822
+ return `mcp__${collisionSafeMcpSegment(serverName, MCP_SERVER_SEGMENT_MAX_LENGTH)}__`;
4690
4823
  }
4691
4824
  function mcpQualifiedToolName(serverName, toolName) {
4692
- return sanitizeWireToolName(
4693
- `${mcpServerToolPrefix(serverName)}${sanitizeWireToolName(toolName)}`
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
- - Each item is the **exact natural-language prompt message** to submit through the current TUI or WebUI input. It should ask the agent to perform useful work; it does not need to be a shell command.
31
- - Write agent-directed prompts such as "Run the test suite and fix any failures." Never write a checklist item that leaves the work to the user, such as "Open DevTools and check the console yourself." If the agent has suitable browser tools, ask it to use them and act on the result instead.
32
- - Human-only actions that the agent cannot perform may be mentioned outside the tag as informational text, but they are never valid `<nextsteps>` items.
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
- **Every actionable work request MUST be represented by a Kanban card before the first project action**. This includes investigation, one-line edits, bugs, features, docs, tests, releases, and multi-agent work; pure conversation with no project action is the only exception. Resume the existing card for the same request. Other planning surfaces and chat may mirror the work but never replace the board.
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 fully detailed childless leaf card for genuinely atomic work. Use a parent and dependency-ordered child cards only for composite work; never invent recursive subtasks for process theatre. The required handshake is **Kanban first, project action second**. If persistence fails, report the blocker instead of silently doing untracked work.
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
- Before creating a card, identify these prerequisites as a minimum starting point (the full "MUST" specification is governed by the Kanban Agent hard conditions below):
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 conditions apply to every actionable work request while Kanban is available. They are not suggestions and cannot be overridden for convenience:
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 leave an accepted card unfinished, claim success while work remains, or describe a task as done when its acceptance criteria and verification are incomplete. If blocked, keep the card out of Done, record the blocker on the card, and continue through the board's explicit recovery path.
66
- 2. **Fully specify every card before advancing it.** Fill and verify the description, assignee/agent, due date, labels, acceptance criteria, dependencies, and any board-required detail fields. Only composite parents (`atomic: true`) require persisted `childTaskIds`; executable leaf cards remain childless. An under-filled card must remain in Backlog.
67
- 3. **Persist every completed action immediately.** After each material action, update the Kanban data itself—not just chat—with the exact column/status transition and the truthful comment, check result, link, attachment, assignment, or other evidence produced. Never fake, batch away, or skip intermediate updates.
68
- 4. **Follow the lifecycle exactly.** Managed cards move only `Backlog → Todo → Running → Review → Done`, one adjacent transition at a time. Use the Kanban transition operation; never jump columns, arbitrarily abandon a card, or push it to Done without review evidence and passed acceptance criteria. Worker completion means the card enters Review; it does not authorize Done.
69
- 5. **Close and advance immediately.** Persist the accepted card in Running before work. Persist Running → Review when work finishes and Review Done only after acceptance evidence passes. If continuing, move the next eligible card through adjacent transitions to Running before acting. Never leave completed work in Running or repeat a next-step prompt instead of updating the board.
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` for every actionable project request; all such work belongs on the durable board.
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-->