@wrongstack/core 0.302.2 → 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 (97) hide show
  1. package/README.md +1 -1
  2. package/dist/chronicle/index.js +117 -30
  3. package/dist/chronicle/project-server.js +12 -5
  4. package/dist/coordination/agents/index.js +4645 -3448
  5. package/dist/coordination/agents/project-agent-auto-optimize.d.ts +116 -0
  6. package/dist/coordination/agents/project-agent-capture-window.d.ts +29 -0
  7. package/dist/coordination/agents/project-agent-config-io.d.ts +11 -0
  8. package/dist/coordination/agents/project-agent-consolidation.d.ts +34 -2
  9. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  10. package/dist/coordination/agents/project-agent-files.d.ts +12 -3
  11. package/dist/coordination/agents/project-agent-identity-types.d.ts +4 -0
  12. package/dist/coordination/agents/project-agent-identity.d.ts +38 -11
  13. package/dist/coordination/agents/project-agent-learning-entries.d.ts +8 -2
  14. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  15. package/dist/coordination/agents/project-agent-learning-structured.d.ts +72 -1
  16. package/dist/coordination/agents/project-agent-optimizer.d.ts +49 -0
  17. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  18. package/dist/coordination/agents/project-agent-skill-layer.d.ts +146 -0
  19. package/dist/coordination/agents/role-skills.d.ts +11 -1
  20. package/dist/coordination/agents/types.d.ts +10 -2
  21. package/dist/coordination/director-prompts.d.ts +19 -6
  22. package/dist/coordination/director-tools.d.ts +2 -2
  23. package/dist/coordination/fleet.d.ts +0 -6
  24. package/dist/coordination/index.d.ts +1 -1
  25. package/dist/coordination/index.js +5746 -4326
  26. package/dist/coordination/mail-tools.d.ts +1 -1
  27. package/dist/core/agent-types.d.ts +4 -2
  28. package/dist/core/agent.d.ts +1 -0
  29. package/dist/core/context.d.ts +19 -0
  30. package/dist/core/conversation-state.d.ts +14 -0
  31. package/dist/core/fallback-profile-manager.d.ts +70 -2
  32. package/dist/core/index.js +317 -108
  33. package/dist/core/system-prompt-blocks.d.ts +1 -1
  34. package/dist/core/system-prompt-builder.d.ts +13 -1
  35. package/dist/core/system-prompt-glossary.d.ts +73 -0
  36. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  37. package/dist/defaults/index.js +1635 -1120
  38. package/dist/execution/council-orchestrator.d.ts +3 -13
  39. package/dist/execution/index.js +3136 -2706
  40. package/dist/execution/one-shot-llm.d.ts +5 -0
  41. package/dist/goal/index.js +7 -0
  42. package/dist/hq/index.js +17 -7
  43. package/dist/hq/protocol/kanban.d.ts +21 -0
  44. package/dist/hq/protocol.js +5 -1
  45. package/dist/hq/redaction.d.ts +14 -0
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +5502 -3483
  48. package/dist/infrastructure/index.js +247 -122
  49. package/dist/kernel/events/agent-events.d.ts +28 -0
  50. package/dist/plugin/index.js +111 -7
  51. package/dist/registry/index.js +11 -0
  52. package/dist/registry/tool-registry.d.ts +8 -0
  53. package/dist/replay/hash.d.ts +9 -0
  54. package/dist/replay/index.js +14 -4
  55. package/dist/replay/replay-provider-runner.d.ts +31 -1
  56. package/dist/security/index.js +92 -21
  57. package/dist/security/kanban-boundary.d.ts +5 -1
  58. package/dist/security/secret-vault.d.ts +2 -0
  59. package/dist/session-catalog/index.js +86 -10
  60. package/dist/session-catalog/project-server.js +100 -17
  61. package/dist/session-catalog/protocol.d.ts +20 -4
  62. package/dist/session-catalog/store.d.ts +19 -3
  63. package/dist/storage/index.js +305 -69
  64. package/dist/storage/memory-consolidator.d.ts +4 -2
  65. package/dist/storage/plan-store.d.ts +1 -1
  66. package/dist/storage/session-resume-validation.d.ts +24 -0
  67. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  68. package/dist/storage/session-store/fork-session.d.ts +13 -1
  69. package/dist/storage/session-store/load-cache.d.ts +11 -0
  70. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  71. package/dist/storage/session-store.d.ts +18 -0
  72. package/dist/tasking/index.js +5 -0
  73. package/dist/tools/index.js +1151 -831
  74. package/dist/types/config/mcp-features.d.ts +31 -1
  75. package/dist/types/config/root.d.ts +23 -1
  76. package/dist/types/config/skills-fleet-brain.d.ts +34 -0
  77. package/dist/types/config/tools.d.ts +22 -0
  78. package/dist/types/config/ui.d.ts +14 -0
  79. package/dist/types/config.d.ts +1 -0
  80. package/dist/types/default-config.d.ts +1 -0
  81. package/dist/types/index.d.ts +2 -2
  82. package/dist/types/index.js +23 -0
  83. package/dist/types/multi-agent.d.ts +7 -0
  84. package/dist/types/session.d.ts +9 -1
  85. package/dist/types/task-graph.d.ts +2 -0
  86. package/dist/types/tool-executor.d.ts +2 -0
  87. package/dist/utils/index.d.ts +1 -0
  88. package/dist/utils/index.js +217 -76
  89. package/dist/utils/project-state-guard.d.ts +21 -0
  90. package/dist/utils/session-scoped-path.d.ts +17 -0
  91. package/dist/utils/todos-format.d.ts +20 -0
  92. package/instructions/leader-after-task.md +3 -4
  93. package/instructions/system-lite.md +17 -14
  94. package/instructions/system-pro.md +29 -30
  95. package/instructions/system.md +29 -28
  96. package/package.json +3 -3
  97. package/skills/wrongstack-kanban/SKILL.md +95 -93
@@ -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
@@ -5444,6 +5577,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
5444
5577
  const obj = input;
5445
5578
  if (subjectKey) {
5446
5579
  const value = obj[subjectKey];
5580
+ if (Array.isArray(value)) {
5581
+ return escapeGlobSubject(JSON.stringify(value));
5582
+ }
5447
5583
  if (typeof value === "string") {
5448
5584
  if (isPathSubjectKey(subjectKey)) {
5449
5585
  const normalized = normalizePathSubject(value);
@@ -5596,6 +5732,7 @@ export {
5596
5732
  TerminalLifecycle,
5597
5733
  WIRE_TOOL_NAME_MAX_LENGTH,
5598
5734
  WIRE_TOOL_NAME_PATTERN,
5735
+ activateProjectStateGuard,
5599
5736
  applyToolDescriptionModeToTool,
5600
5737
  applyToolDescriptionModes,
5601
5738
  applyToolResultRenderModes,
@@ -5661,6 +5798,8 @@ export {
5661
5798
  formatCompletedWorkLedger,
5662
5799
  formatTaskList,
5663
5800
  formatTaskProgress,
5801
+ formatTodoForModel,
5802
+ formatTodosForModel,
5664
5803
  formatTodosList,
5665
5804
  getCalibrationState,
5666
5805
  getChildEnvGitIdentity,
@@ -5669,6 +5808,7 @@ export {
5669
5808
  getPerfProfile,
5670
5809
  getToolDescriptionMode,
5671
5810
  getToolResultRenderMode,
5811
+ hasKanbanBoundTodos,
5672
5812
  hasMeaningfulContent,
5673
5813
  hasOpenTodos,
5674
5814
  indexParallelBatchSize,
@@ -5747,6 +5887,7 @@ export {
5747
5887
  splitSageOutputBlock,
5748
5888
  sqliteCachePragmas,
5749
5889
  startHeapWatchdog,
5890
+ startProjectStateGuard,
5750
5891
  startSharedHeapWatchdog,
5751
5892
  stripAnsi,
5752
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.