@voybio/ace-swarm 2.4.9 → 2.4.10

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 (185) hide show
  1. package/CHANGELOG.md +52 -14
  2. package/README.md +14 -10
  3. package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
  4. package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
  5. package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
  6. package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
  7. package/assets/agent-state/ACE_WORKFLOW.md +25 -2
  8. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
  9. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
  10. package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
  11. package/assets/eval-battery/battery.json +1 -1
  12. package/assets/instructions/ACE_Coder.instructions.md +9 -13
  13. package/assets/scripts/ace-hook-dispatch.mjs +16 -411
  14. package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
  15. package/assets/vendor/README.md +9 -0
  16. package/assets/vendor/vericify-reader.mjs +272 -0
  17. package/assets/vendor/vericify-reader.provenance.json +10 -0
  18. package/dist/ace-autonomy.d.ts +58 -0
  19. package/dist/ace-autonomy.js +211 -6
  20. package/dist/ace-internal-tools.d.ts +1 -0
  21. package/dist/ace-internal-tools.js +3 -3
  22. package/dist/ace-server-instructions.d.ts +3 -3
  23. package/dist/ace-server-instructions.js +46 -192
  24. package/dist/cli.js +44 -6
  25. package/dist/experience-reflector.d.ts +7 -0
  26. package/dist/experience-reflector.js +39 -0
  27. package/dist/goal-engine.d.ts +64 -7
  28. package/dist/goal-engine.js +197 -41
  29. package/dist/handoff-registry.d.ts +10 -0
  30. package/dist/handoff-registry.js +59 -0
  31. package/dist/helpers/artifacts.d.ts +6 -2
  32. package/dist/helpers/artifacts.js +8 -6
  33. package/dist/helpers/bootstrap.js +13 -20
  34. package/dist/helpers/path-utils.js +2 -0
  35. package/dist/helpers/store-resolution.js +18 -3
  36. package/dist/helpers/workspace-root.d.ts +2 -0
  37. package/dist/helpers/workspace-root.js +9 -0
  38. package/dist/helpers.d.ts +1 -0
  39. package/dist/helpers.js +1 -0
  40. package/dist/hermes/bridge-protocol.d.ts +1 -1
  41. package/dist/hermes/session-manager.d.ts +17 -0
  42. package/dist/hermes/session-manager.js +67 -14
  43. package/dist/host-goal-engine.d.ts +138 -0
  44. package/dist/host-goal-engine.js +793 -0
  45. package/dist/host-goal-status.d.ts +113 -0
  46. package/dist/host-goal-status.js +175 -0
  47. package/dist/host-learning.d.ts +68 -0
  48. package/dist/host-learning.js +115 -0
  49. package/dist/host-observation.d.ts +12 -0
  50. package/dist/host-observation.js +91 -0
  51. package/dist/host-plan-revision.d.ts +14 -0
  52. package/dist/host-plan-revision.js +101 -0
  53. package/dist/host-verification-job.d.ts +12 -0
  54. package/dist/host-verification-job.js +183 -0
  55. package/dist/host-work-context.d.ts +12 -0
  56. package/dist/host-work-context.js +8 -0
  57. package/dist/job-scheduler.d.ts +48 -14
  58. package/dist/job-scheduler.js +169 -23
  59. package/dist/job-worker.d.ts +3 -1
  60. package/dist/job-worker.js +10 -3
  61. package/dist/kanban.d.ts +4 -1
  62. package/dist/kanban.js +44 -16
  63. package/dist/learned-artifacts.d.ts +64 -0
  64. package/dist/learned-artifacts.js +321 -0
  65. package/dist/learning-controls.d.ts +142 -0
  66. package/dist/learning-controls.js +135 -0
  67. package/dist/learning-input-context.d.ts +9 -0
  68. package/dist/learning-input-context.js +62 -0
  69. package/dist/learning-lifecycle.d.ts +10 -0
  70. package/dist/learning-lifecycle.js +11 -0
  71. package/dist/learning-overlays.d.ts +3 -0
  72. package/dist/learning-overlays.js +17 -0
  73. package/dist/learning-policy.d.ts +40 -0
  74. package/dist/learning-policy.js +38 -0
  75. package/dist/learning-records.d.ts +35 -0
  76. package/dist/learning-records.js +17 -0
  77. package/dist/learning-scorecard.d.ts +82 -0
  78. package/dist/learning-scorecard.js +86 -0
  79. package/dist/learning-serving.d.ts +29 -0
  80. package/dist/learning-serving.js +134 -0
  81. package/dist/linked-work.d.ts +9 -0
  82. package/dist/linked-work.js +28 -0
  83. package/dist/local-model-runtime.js +1 -1
  84. package/dist/mcp-command-progress.d.ts +16 -0
  85. package/dist/mcp-command-progress.js +70 -0
  86. package/dist/model-bridge.d.ts +14 -13
  87. package/dist/model-bridge.js +73 -41
  88. package/dist/orchestrator-supervisor.d.ts +108 -6
  89. package/dist/orchestrator-supervisor.js +4 -32
  90. package/dist/plan-proposal.js +3 -3
  91. package/dist/playbook-curator.d.ts +1 -0
  92. package/dist/playbook-curator.js +11 -9
  93. package/dist/process-identity.d.ts +8 -0
  94. package/dist/process-identity.js +38 -0
  95. package/dist/progress-command.d.ts +34 -0
  96. package/dist/progress-command.js +153 -0
  97. package/dist/prompts.js +15 -15
  98. package/dist/resources.js +2 -2
  99. package/dist/runtime-command.d.ts +8 -1
  100. package/dist/runtime-command.js +48 -7
  101. package/dist/runtime-executor.js +33 -11
  102. package/dist/runtime-profile-syntax.d.ts +6 -0
  103. package/dist/runtime-profile-syntax.js +121 -0
  104. package/dist/runtime-profile.d.ts +37 -5
  105. package/dist/runtime-profile.js +36 -129
  106. package/dist/runtime-tool-specs.js +1 -1
  107. package/dist/safe-edit.d.ts +7 -3
  108. package/dist/safe-edit.js +40 -47
  109. package/dist/schemas.js +4 -2
  110. package/dist/server.d.ts +2 -16
  111. package/dist/server.js +14 -20
  112. package/dist/shared.d.ts +2 -0
  113. package/dist/shared.js +2 -0
  114. package/dist/skill-auditor.d.ts +19 -0
  115. package/dist/skill-auditor.js +32 -2
  116. package/dist/skill-catalog.d.ts +4 -1
  117. package/dist/skill-catalog.js +6 -5
  118. package/dist/store/ace-packed-store.d.ts +5 -1
  119. package/dist/store/ace-packed-store.js +33 -1
  120. package/dist/store/bootstrap-store.js +19 -12
  121. package/dist/store/cache-workspace.d.ts +8 -0
  122. package/dist/store/cache-workspace.js +94 -27
  123. package/dist/store/importer.js +11 -20
  124. package/dist/store/knowledge-bake.d.ts +3 -1
  125. package/dist/store/knowledge-bake.js +10 -1
  126. package/dist/store/legacy-vericify-posts.d.ts +8 -0
  127. package/dist/store/legacy-vericify-posts.js +76 -0
  128. package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
  129. package/dist/store/materializers/hook-context-materializer.js +6 -2
  130. package/dist/store/materializers/host-file-materializer.js +2 -2
  131. package/dist/store/materializers/projection-manager.d.ts +4 -1
  132. package/dist/store/materializers/projection-manager.js +10 -3
  133. package/dist/store/materializers/todo-syncer.d.ts +39 -20
  134. package/dist/store/materializers/todo-syncer.js +208 -110
  135. package/dist/store/materializers/vericify-projector.js +15 -26
  136. package/dist/store/repositories/goal-repository.d.ts +51 -2
  137. package/dist/store/repositories/goal-repository.js +114 -48
  138. package/dist/store/repositories/playbook-repository.d.ts +41 -3
  139. package/dist/store/repositories/playbook-repository.js +61 -5
  140. package/dist/store/repositories/todo-repository.d.ts +8 -2
  141. package/dist/store/repositories/todo-repository.js +1 -1
  142. package/dist/store/repositories/vericify-repository.d.ts +4 -0
  143. package/dist/store/repositories/vericify-repository.js +46 -0
  144. package/dist/store/skills-install.d.ts +1 -0
  145. package/dist/store/skills-install.js +20 -16
  146. package/dist/store/source-preservation.d.ts +24 -0
  147. package/dist/store/source-preservation.js +79 -0
  148. package/dist/store/store-artifacts.js +8 -1
  149. package/dist/store/test-write-boundary.d.ts +16 -0
  150. package/dist/store/test-write-boundary.js +175 -0
  151. package/dist/store/types.d.ts +5 -1
  152. package/dist/store/write-coordinator.js +16 -0
  153. package/dist/task-plan-context.d.ts +11 -0
  154. package/dist/task-plan-context.js +59 -0
  155. package/dist/todo-state.d.ts +19 -9
  156. package/dist/todo-state.js +204 -321
  157. package/dist/tools-agent.d.ts +1 -1
  158. package/dist/tools-agent.js +38 -62
  159. package/dist/tools-files.d.ts +0 -3
  160. package/dist/tools-files.js +21 -11
  161. package/dist/tools-framework.js +16 -25
  162. package/dist/tools-goal.d.ts +2 -4
  163. package/dist/tools-goal.js +119 -16
  164. package/dist/tools-handoff.js +28 -4
  165. package/dist/tools-lifecycle.js +1 -1
  166. package/dist/tools-memory.d.ts +0 -3
  167. package/dist/tools-memory.js +51 -6
  168. package/dist/tools-scheduler.js +19 -17
  169. package/dist/tools-todo.js +160 -148
  170. package/dist/vericify-bridge.d.ts +8 -0
  171. package/dist/vericify-bridge.js +18 -0
  172. package/dist/vericify-context.d.ts +4 -0
  173. package/dist/vericify-context.js +26 -5
  174. package/dist/vericify-native.d.ts +101 -0
  175. package/dist/vericify-native.js +221 -0
  176. package/dist/verification-worker.d.ts +2 -0
  177. package/dist/verification-worker.js +31 -0
  178. package/docs/mcp-tool-reference.md +44 -9
  179. package/package.json +19 -6
  180. package/scripts/dev/run-tests-isolated.mjs +165 -0
  181. package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
  182. package/scripts/dev/test-isolation-guard.mjs +20 -0
  183. package/scripts/dev/test-preload.mjs +153 -0
  184. package/scripts/dev/test-source-inventory.mjs +31 -0
  185. package/docs/development.md +0 -46
@@ -1,5 +1,7 @@
1
- import { existsSync } from "node:fs";
2
- import { ACE_TASKS_ROOT_REL, resolveWorkspaceRoot, resolveWritableTaskPath, safeRead, safeWrite, safeWriteAsync, withFileLock, wsPath, } from "./helpers.js";
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { TodoSyncer, parseTodoMd, renderTodoMd } from "./store/materializers/todo-syncer.js";
4
+ import { ACE_TASKS_ROOT_REL, resolveWorkspaceRoot, safeRead, safeWrite, wsPath, } from "./helpers.js";
3
5
  import { openStore } from "./store/ace-packed-store.js";
4
6
  import { ProjectionManager } from "./store/materializers/projection-manager.js";
5
7
  import { TodoRepository } from "./store/repositories/todo-repository.js";
@@ -7,6 +9,7 @@ import { getWorkspaceStorePath, readStoreJsonSync, storeExistsSync } from "./sto
7
9
  import { operationalArtifactVirtualPath } from "./store/store-artifacts.js";
8
10
  import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
9
11
  import { isReadError, slugify } from "./shared.js";
12
+ import { deriveLinkedTodo } from './linked-work.js';
10
13
  const TODO_MARKDOWN_REL = `${ACE_TASKS_ROOT_REL}/todo.md`;
11
14
  const TODO_STATE_REL = "agent-state/todo-state.json";
12
15
  let todoMirrorChain = Promise.resolve();
@@ -14,11 +17,12 @@ function workspaceRoot() {
14
17
  return resolveWorkspaceRoot();
15
18
  }
16
19
  const TRANSITIONS = {
17
- backlog: ["in_progress", "blocked"],
18
- in_progress: ["blocked", "gated", "done"],
19
- blocked: ["backlog", "in_progress"],
20
- gated: ["in_progress", "blocked", "done"],
20
+ backlog: ["in_progress", "blocked", "cancelled"],
21
+ in_progress: ["blocked", "gated", "done", "cancelled"],
22
+ blocked: ["backlog", "in_progress", "cancelled"],
23
+ gated: ["in_progress", "blocked", "done", "cancelled"],
21
24
  done: ["in_progress", "blocked"],
25
+ cancelled: ["backlog", "in_progress"],
22
26
  };
23
27
  function defaultState() {
24
28
  return {
@@ -34,48 +38,7 @@ function isTodoNodeStatus(value) {
34
38
  value === "in_progress" ||
35
39
  value === "blocked" ||
36
40
  value === "gated" ||
37
- value === "done");
38
- }
39
- function parseMeta(rawTitle) {
40
- const metaMatch = rawTitle.match(/<!--\s*ace:(.*?)\s*-->/i);
41
- const config = {};
42
- const applyMetaChunks = (rawMeta) => {
43
- for (const chunk of rawMeta.split(";")) {
44
- const match = chunk.match(/^\s*([A-Za-z0-9_\- ]+)\s*[:=]\s*(.+?)\s*$/);
45
- if (!match)
46
- continue;
47
- const rawKey = match[1]?.trim() ?? "";
48
- const rawValue = match[2]?.trim() ?? "";
49
- if (!rawKey || !rawValue)
50
- continue;
51
- config[rawKey.toLowerCase().replace(/\s+/g, "_")] = rawValue;
52
- }
53
- };
54
- let cleanTitle = rawTitle.trim();
55
- if (metaMatch?.[1]) {
56
- applyMetaChunks(metaMatch[1]);
57
- cleanTitle = cleanTitle.replace(metaMatch[0], "").trim();
58
- }
59
- while (true) {
60
- const inlineMatch = cleanTitle.match(/\s+\(([^()]*[:=][^()]*)\)\s*$/);
61
- if (!inlineMatch?.[1])
62
- break;
63
- const before = cleanTitle.slice(0, cleanTitle.length - inlineMatch[0].length).trimEnd();
64
- const priorConfigSize = Object.keys(config).length;
65
- applyMetaChunks(inlineMatch[1]);
66
- if (Object.keys(config).length === priorConfigSize)
67
- break;
68
- cleanTitle = before;
69
- }
70
- const dependsOn = (config.depends_on ?? config.depends ?? "")
71
- .split(",")
72
- .map((entry) => entry.trim())
73
- .filter((entry) => entry.length > 0);
74
- return {
75
- cleanTitle: cleanTitle.trim(),
76
- id: config.id,
77
- depends_on: dependsOn,
78
- };
41
+ value === "done" || value === "cancelled");
79
42
  }
80
43
  function defaultStatusFor(section, checked) {
81
44
  if (checked)
@@ -87,48 +50,19 @@ function defaultStatusFor(section, checked) {
87
50
  return "in_progress";
88
51
  return "backlog";
89
52
  }
90
- function parseTodoMarkdown(todoRaw) {
91
- const entries = [];
92
- const activeIds = [];
93
- const verificationIndexes = [];
94
- let section = "General";
95
- for (const [idx, line] of todoRaw.split(/\r?\n/).entries()) {
96
- const header = line.match(/^##\s+(.+)$/);
97
- if (header?.[1]) {
98
- section = header[1].trim();
99
- continue;
100
- }
101
- const item = line.match(/^- \[( |x|X)\]\s+(.+)$/);
102
- if (!item?.[2])
103
- continue;
104
- const checked = item[1]?.toLowerCase() === "x";
105
- const rawTitle = item[2].trim();
106
- const meta = parseMeta(rawTitle);
107
- const lineNumber = idx + 1;
108
- const id = (meta.id && meta.id.trim()) || `todo-${lineNumber}-${slugify(meta.cleanTitle)}`;
109
- const row = {
110
- id,
111
- title: meta.cleanTitle || "Untitled task",
112
- section,
113
- line: lineNumber,
114
- checked,
115
- default_status: defaultStatusFor(section, checked),
116
- depends_on: meta.depends_on,
117
- };
118
- if (section.toLowerCase().includes("active work")) {
119
- activeIds.push(id);
120
- }
121
- if (section.toLowerCase().includes("verification") && row.depends_on.length === 0) {
122
- verificationIndexes.push(entries.length);
123
- }
124
- entries.push(row);
125
- }
126
- for (const idx of verificationIndexes) {
127
- const node = entries[idx];
128
- if (!node)
129
- continue;
130
- node.depends_on = [...activeIds];
131
- }
53
+ function parseLegacyTodoEntries(todoRaw) {
54
+ const { items } = parseTodoMd(todoRaw);
55
+ const entries = items.map((item) => ({
56
+ id: item.id ?? `todo-${item.source_line}-${slugify(item.title)}`,
57
+ title: item.title, section: item.section ?? "General", line: item.source_line,
58
+ checked: item.status === "done",
59
+ default_status: item.status === "pending" ? defaultStatusFor(item.section ?? "", false) : mapLegacyStatus(item.status),
60
+ depends_on: item.depends_on ?? [],
61
+ }));
62
+ const active = entries.filter((item) => item.section.toLowerCase().includes("active work")).map((item) => item.id);
63
+ for (const item of entries)
64
+ if (item.section.toLowerCase().includes("verification") && !item.depends_on.length)
65
+ item.depends_on = [...active];
132
66
  return entries;
133
67
  }
134
68
  function parseTodoState(raw) {
@@ -160,6 +94,7 @@ function parseTodoState(raw) {
160
94
  continue;
161
95
  }
162
96
  nodes[key] = {
97
+ ...row,
163
98
  id: row.id,
164
99
  title: row.title,
165
100
  section: row.section,
@@ -183,6 +118,9 @@ function parseTodoState(raw) {
183
118
  }
184
119
  }
185
120
  function readPersistedState() {
121
+ const canonical = readStoreBackedState();
122
+ if (canonical)
123
+ return canonical;
186
124
  const raw = safeRead(TODO_STATE_REL);
187
125
  if (!isReadError(raw)) {
188
126
  const parsed = parseTodoState(raw);
@@ -200,6 +138,9 @@ function mapStoreStatus(status) {
200
138
  case "done":
201
139
  return "done";
202
140
  case "gated":
141
+ return "gated";
142
+ case "cancelled":
143
+ return "cancelled";
203
144
  case "backlog":
204
145
  default:
205
146
  return "pending";
@@ -212,8 +153,11 @@ function mapLegacyStatus(status) {
212
153
  case "blocked":
213
154
  return "blocked";
214
155
  case "done":
215
- case "cancelled":
216
156
  return "done";
157
+ case "gated":
158
+ return "gated";
159
+ case "cancelled":
160
+ return "cancelled";
217
161
  case "pending":
218
162
  default:
219
163
  return "backlog";
@@ -223,41 +167,26 @@ function readStoreBackedState() {
223
167
  const root = workspaceRoot();
224
168
  if (!storeExistsSync(root))
225
169
  return undefined;
226
- const index = readStoreJsonSync(root, "state/todo/index") ?? [];
227
- if (index.length === 0)
170
+ const index = readStoreJsonSync(root, "state/todo/index");
171
+ if (!index)
228
172
  return undefined;
173
+ const records = index.map((id) => readStoreJsonSync(root, `state/todo/${id}`)).filter((record) => !!record).map(record => deriveLinkedTodo(record, root));
174
+ return todoStateFromRecords(records);
175
+ }
176
+ function todoStateFromRecords(records) {
229
177
  const nodes = {};
230
- const order = [];
231
- let updatedAt = Date.now();
232
- for (const id of index) {
233
- const record = readStoreJsonSync(root, `state/todo/${id}`);
234
- if (!record?.id || !record.title || !record.status)
235
- continue;
236
- const noteSection = typeof record.notes === "string" && record.notes.startsWith("section: ")
237
- ? record.notes.slice("section: ".length)
238
- : "";
239
- nodes[id] = {
240
- id: record.id,
241
- title: record.title,
242
- section: noteSection,
243
- source_line: 0,
244
- depends_on: Array.isArray(record.depends_on)
245
- ? record.depends_on.filter((value) => typeof value === "string")
246
- : [],
178
+ let updatedAt = 0;
179
+ for (const record of records) {
180
+ nodes[record.id] = {
181
+ ...record,
182
+ section: record.section ?? (record.notes?.startsWith("section: ") ? record.notes.slice(9) : ""),
183
+ source_line: record.source_line ?? 0,
184
+ depends_on: record.depends_on ?? [],
247
185
  status: mapLegacyStatus(record.status),
248
186
  };
249
- order.push(id);
250
- if (typeof record.updated_at === "number" && Number.isFinite(record.updated_at)) {
251
- updatedAt = Math.max(updatedAt, record.updated_at);
252
- }
187
+ updatedAt = Math.max(updatedAt, record.updated_at ?? 0);
253
188
  }
254
- return {
255
- version: 1,
256
- updated_at: new Date(updatedAt).toISOString(),
257
- source_todo_path: TODO_MARKDOWN_REL,
258
- order,
259
- nodes,
260
- };
189
+ return { version: 1, updated_at: updatedAt ? new Date(updatedAt).toISOString() : "", source_todo_path: TODO_MARKDOWN_REL, order: records.map((record) => record.id), nodes };
261
190
  }
262
191
  async function mirrorTodoStateToStore(root, state) {
263
192
  const storePath = getWorkspaceStorePath(root);
@@ -268,61 +197,29 @@ async function mirrorTodoStateToStore(root, state) {
268
197
  try {
269
198
  const nextIds = state.order.filter((id) => Boolean(state.nodes[id]));
270
199
  const previousIds = (await store.getJSON("state/todo/index")) ?? [];
271
- for (const staleId of previousIds.filter((id) => !nextIds.includes(id))) {
272
- await store.delete(`state/todo/${staleId}`);
273
- }
274
200
  for (const id of nextIds) {
275
201
  const node = state.nodes[id];
276
202
  if (!node)
277
203
  continue;
278
204
  const existing = await store.getJSON(`state/todo/${id}`);
279
205
  await store.setJSON(`state/todo/${id}`, {
206
+ ...existing,
280
207
  id: node.id,
281
208
  title: node.title,
282
209
  status: mapStoreStatus(node.status),
283
210
  depends_on: node.depends_on,
284
211
  created_at: existing?.created_at ?? Date.now(),
285
212
  updated_at: Date.now(),
286
- notes: node.section ? `section: ${node.section}` : undefined,
213
+ section: node.section,
287
214
  });
288
215
  }
289
- await store.setJSON("state/todo/index", nextIds);
290
- await store.commit();
291
- const projections = new ProjectionManager(store, root);
292
- await projections.projectAfterCommit(["todo_state"]);
293
- }
294
- finally {
295
- await store.close();
296
- }
297
- }, { operation_label: "syncTodoStateSafe" });
298
- }
299
- async function writeStateStoreBacked(root, state) {
300
- const storePath = getWorkspaceStorePath(root);
301
- if (!existsSync(storePath)) {
302
- return writeState(state);
303
- }
304
- return withStoreWriteCoordinator(storePath, async () => {
305
- const store = await openStore(storePath);
306
- try {
307
- const repo = new TodoRepository(store);
308
- await repo.replaceAll(state.order
309
- .map((id) => state.nodes[id])
310
- .filter((node) => Boolean(node))
311
- .map((node) => ({
312
- id: node.id,
313
- title: node.title,
314
- status: mapStoreStatus(node.status),
315
- priority: "medium",
316
- depends_on: node.depends_on,
317
- notes: node.section ? `section: ${node.section}` : undefined,
318
- })));
216
+ await store.setJSON("state/todo/index", [...new Set([...previousIds, ...nextIds])]);
319
217
  await store.commit();
320
218
  const projections = new ProjectionManager(store, root);
321
219
  await projections.projectAfterCommit(["todo_state"]);
322
- return operationalArtifactVirtualPath(root, TODO_STATE_REL);
323
220
  }
324
221
  finally {
325
- await store.close();
222
+ await store.close({ commit: false });
326
223
  }
327
224
  }, { operation_label: "syncTodoStateSafe" });
328
225
  }
@@ -344,9 +241,9 @@ function writeState(state) {
344
241
  return path;
345
242
  }
346
243
  function syncStateWithTodo(todoRaw, previous) {
347
- const parsed = parseTodoMarkdown(todoRaw);
348
- const nodes = {};
349
- const order = [];
244
+ const parsed = parseLegacyTodoEntries(todoRaw);
245
+ const nodes = { ...previous.nodes };
246
+ const order = [...previous.order];
350
247
  for (const row of parsed) {
351
248
  const prev = previous.nodes[row.id];
352
249
  let status;
@@ -360,6 +257,7 @@ function syncStateWithTodo(todoRaw, previous) {
360
257
  status = row.default_status;
361
258
  }
362
259
  nodes[row.id] = {
260
+ ...prev,
363
261
  id: row.id,
364
262
  title: row.title,
365
263
  section: row.section,
@@ -367,7 +265,8 @@ function syncStateWithTodo(todoRaw, previous) {
367
265
  depends_on: row.depends_on.filter((dep) => dep !== row.id),
368
266
  status,
369
267
  };
370
- order.push(row.id);
268
+ if (!order.includes(row.id))
269
+ order.push(row.id);
371
270
  }
372
271
  return {
373
272
  version: 1,
@@ -385,38 +284,6 @@ function setTodoCheckbox(todoRaw, line, checked) {
385
284
  lines[line - 1] = target.replace(/^- \[( |x|X)\]/, `- [${checked ? "x" : " "}]`);
386
285
  return lines.join("\n");
387
286
  }
388
- function insertTodoItem(todoRaw, section, title, id, dependsOn) {
389
- const metaParts = [`id=${id}`];
390
- if (dependsOn.length > 0)
391
- metaParts.push(`depends_on=${dependsOn.join(",")}`);
392
- const newLine = `- [ ] ${title} <!-- ace:${metaParts.join("; ")} -->`;
393
- const lines = todoRaw.length > 0 ? todoRaw.split(/\r?\n/) : [];
394
- let sectionStart = -1;
395
- let sectionEnd = lines.length;
396
- for (let i = 0; i < lines.length; i++) {
397
- const header = lines[i]?.match(/^##\s+(.+)$/);
398
- if (!header?.[1])
399
- continue;
400
- if (header[1].trim().toLowerCase() === section.toLowerCase()) {
401
- sectionStart = i;
402
- let j = i + 1;
403
- while (j < lines.length && !/^##\s+/.test(lines[j] ?? ""))
404
- j++;
405
- sectionEnd = j;
406
- break;
407
- }
408
- }
409
- if (sectionStart === -1) {
410
- const trimmed = todoRaw.replace(/\s+$/, "");
411
- const prefix = trimmed.length > 0 ? `${trimmed}\n\n` : "";
412
- return `${prefix}## ${section}\n${newLine}\n`;
413
- }
414
- let insertAt = sectionEnd;
415
- while (insertAt > sectionStart + 1 && (lines[insertAt - 1] ?? "").trim() === "")
416
- insertAt--;
417
- lines.splice(insertAt, 0, newLine);
418
- return lines.join("\n");
419
- }
420
287
  function uniqueTodoId(base, existingIds) {
421
288
  if (!existingIds.has(base))
422
289
  return base;
@@ -435,6 +302,8 @@ export function getTodoStatePath() {
435
302
  return wsPath(TODO_STATE_REL);
436
303
  }
437
304
  export function syncTodoState(todoContent) {
305
+ if (todoContent === undefined && storeExistsSync(workspaceRoot()))
306
+ return { state: readTodoState(), path: operationalArtifactVirtualPath(workspaceRoot(), TODO_STATE_REL) };
438
307
  const todoRaw = typeof todoContent === "string" ? todoContent : safeRead(TODO_MARKDOWN_REL);
439
308
  if (isReadError(todoRaw)) {
440
309
  const path = writeState(defaultState());
@@ -444,28 +313,99 @@ export function syncTodoState(todoContent) {
444
313
  const path = writeState(state);
445
314
  return { state, path };
446
315
  }
447
- // Lock-free core: callers that already hold the TODO_MARKDOWN_REL lock (e.g.
448
- // appendTodoItemSafe) must call this directly instead of syncTodoStateSafe,
449
- // since withFileLock is not reentrant and re-acquiring the same key deadlocks.
450
- async function syncTodoStateLocked(todoContent) {
316
+ /** A single fresh repository transaction, followed by repairable projections. */
317
+ async function mutateTodos(label, action, content) {
451
318
  const root = workspaceRoot();
452
- if (storeExistsSync(root)) {
453
- const todoRaw = typeof todoContent === "string" ? todoContent : safeRead(TODO_MARKDOWN_REL);
454
- if (isReadError(todoRaw)) {
455
- const state = defaultState();
456
- const path = await writeStateStoreBacked(root, state);
457
- return { state, path };
319
+ const storePath = getWorkspaceStorePath(root);
320
+ return withStoreWriteCoordinator(storePath, async () => {
321
+ const store = await openStore(storePath);
322
+ try {
323
+ if ((await store.getJSON("state/todo/index")) === undefined) {
324
+ const legacy = readPersistedState();
325
+ const repo = new TodoRepository(store);
326
+ for (const id of legacy.order) {
327
+ const node = legacy.nodes[id];
328
+ if (node)
329
+ await repo.create({ ...node, status: mapStoreStatus(node.status) });
330
+ }
331
+ }
332
+ const syncer = new TodoSyncer(store, root);
333
+ const md = getTodoPath();
334
+ const original = existsSync(md) ? readFileSync(md, "utf8") : undefined;
335
+ let imported = await syncer.syncFromFile();
336
+ if (typeof content === "string") {
337
+ const submitted = await syncer.syncFromFile(content, false, false);
338
+ imported = { changed: imported.changed || submitted.changed, import_warnings: submitted.import_warnings };
339
+ }
340
+ const repository = new TodoRepository(store);
341
+ for (const todo of await repository.getAll()) {
342
+ const derived = deriveLinkedTodo(todo, root);
343
+ if (derived.status !== todo.status || derived.assigned_to !== todo.assigned_to || JSON.stringify(derived.step_ref) !== JSON.stringify(todo.step_ref))
344
+ await repository.update(todo.id, { status: derived.status, assigned_to: derived.assigned_to, step_ref: derived.step_ref });
345
+ }
346
+ const result = await action(new TodoRepository(store));
347
+ await store.commit();
348
+ const warnings = [...imported.import_warnings];
349
+ let projectionPending = false;
350
+ // Explicit imports/deletes own their supplied text. Never overwrite a newer native edit.
351
+ const replacement = result.ok ? (typeof content === "function" ? content(original ?? "") : content) : undefined;
352
+ try {
353
+ if (replacement !== undefined) {
354
+ if ((existsSync(md) ? readFileSync(md, "utf8") : undefined) !== original) {
355
+ warnings.push("Todo markdown changed during the operation; newer native edit retained.");
356
+ projectionPending = true;
357
+ }
358
+ else {
359
+ mkdirSync(dirname(md), { recursive: true });
360
+ writeFileSync(md, replacement, "utf8");
361
+ if (typeof content === "string")
362
+ await syncer.acknowledgeSubmittedFile(replacement);
363
+ else {
364
+ const checked = await syncer.syncFromFile(undefined, true);
365
+ warnings.splice(0, warnings.length, ...checked.import_warnings);
366
+ }
367
+ await store.commit();
368
+ }
369
+ }
370
+ if ((result.ok && label !== "read_todos") || imported.changed || !existsSync(md) || (!warnings.length && !(await syncer.projectionIsCurrent()))) {
371
+ const projected = await new ProjectionManager(store, root).projectAfterCommit(["todo_state"]);
372
+ warnings.push(...(projected.todo?.import_warnings ?? []));
373
+ projectionPending ||= projected.todo?.written === false;
374
+ }
375
+ }
376
+ catch (error) {
377
+ warnings.push(`Todo state committed; projection refresh pending: ${error instanceof Error ? error.message : String(error)}`);
378
+ projectionPending = true;
379
+ }
380
+ return { ...result, import_warnings: [...new Set(warnings)], projection_pending: projectionPending || warnings.length > 0 };
458
381
  }
459
- const state = syncStateWithTodo(todoRaw, readPersistedState());
460
- const path = await writeStateStoreBacked(root, state);
461
- return { state, path };
462
- }
463
- const result = syncTodoState(todoContent);
464
- await waitForTodoStoreMirror();
465
- return result;
382
+ finally {
383
+ await store.close({ commit: false });
384
+ }
385
+ }, { operation_label: label });
386
+ }
387
+ /** Repair a linked projection after its authoritative plan has been saved. */
388
+ export async function syncLinkedTodoStepSafe(id, ref, step) {
389
+ return mutateTodos('linked_todo_view', async (repo) => {
390
+ const todo = await repo.get(id);
391
+ if (!todo)
392
+ throw new Error(`Linked todo missing: ${id}`);
393
+ if (todo.step_ref && (todo.step_ref.goal_id !== ref.goal_id || todo.step_ref.step_id !== ref.step_id))
394
+ throw new Error(`Todo already linked elsewhere: ${id}`);
395
+ const derived = deriveLinkedTodo({ ...todo, step_ref: ref }, workspaceRoot());
396
+ const notes = step.blocked_reason && !todo.notes?.includes(step.blocked_reason) ? [todo.notes, step.blocked_reason].filter(Boolean).join('\n') : todo.notes;
397
+ const patch = { step_ref: ref, status: derived.status, assigned_to: derived.assigned_to, notes };
398
+ if (Object.entries(patch).some(([key, value]) => JSON.stringify(todo[key]) !== JSON.stringify(value)))
399
+ await repo.update(id, patch);
400
+ return { ok: true };
401
+ });
466
402
  }
467
403
  export async function syncTodoStateSafe(todoContent) {
468
- return withFileLock(TODO_MARKDOWN_REL, () => syncTodoStateLocked(todoContent));
404
+ const result = await mutateTodos(todoContent === undefined ? "read_todos" : "import_todos", async (repo) => {
405
+ const records = await repo.getAll();
406
+ return { ok: true, markdown: renderTodoMd(records), state: todoStateFromRecords(records) };
407
+ }, todoContent);
408
+ return { state: result.state, markdown: result.markdown, path: operationalArtifactVirtualPath(workspaceRoot(), TODO_STATE_REL), import_warnings: result.import_warnings, projection_pending: result.projection_pending };
469
409
  }
470
410
  export function readTodoState() {
471
411
  if (storeExistsSync(workspaceRoot())) {
@@ -544,121 +484,64 @@ export function transitionTodoNode(nodeId, targetStatus) {
544
484
  };
545
485
  }
546
486
  export async function transitionTodoNodeSafe(nodeId, targetStatus) {
547
- return withFileLock(TODO_MARKDOWN_REL, async () => {
548
- const root = workspaceRoot();
549
- if (storeExistsSync(root)) {
550
- const todoRaw = safeRead(TODO_MARKDOWN_REL);
551
- if (isReadError(todoRaw)) {
552
- return {
553
- ok: false,
554
- path: getTodoStatePath(),
555
- todo_path: getTodoPath(),
556
- error: `${TODO_MARKDOWN_REL} not found; cannot apply transition.`,
557
- };
558
- }
559
- const synced = syncStateWithTodo(todoRaw, readPersistedState());
560
- const node = synced.nodes[nodeId];
561
- if (!node) {
562
- return {
563
- ok: false,
564
- path: getTodoStatePath(),
565
- todo_path: getTodoPath(),
566
- error: `Unknown node_id: ${nodeId}`,
567
- };
568
- }
569
- const from = node.status;
570
- if (from !== targetStatus) {
571
- const allowed = TRANSITIONS[from];
572
- if (!allowed.includes(targetStatus)) {
573
- return {
574
- ok: false,
575
- path: getTodoStatePath(),
576
- todo_path: getTodoPath(),
577
- node_id: nodeId,
578
- from,
579
- to: targetStatus,
580
- allowed,
581
- error: `Invalid transition ${from} -> ${targetStatus}`,
582
- };
583
- }
584
- }
585
- if (targetStatus === "in_progress" ||
586
- targetStatus === "gated" ||
587
- targetStatus === "done") {
588
- const unresolved = node.depends_on.filter((depId) => synced.nodes[depId]?.status !== "done");
589
- if (unresolved.length > 0) {
590
- return {
591
- ok: false,
592
- path: getTodoStatePath(),
593
- todo_path: getTodoPath(),
594
- node_id: nodeId,
595
- from,
596
- to: targetStatus,
597
- unresolved_dependencies: unresolved,
598
- error: `Dependencies not satisfied for ${nodeId}`,
599
- };
600
- }
601
- }
602
- node.status = targetStatus;
603
- const statePath = await writeStateStoreBacked(root, synced);
604
- return {
605
- ok: true,
606
- path: statePath,
607
- todo_path: getTodoPath(),
608
- node_id: nodeId,
609
- from,
610
- to: targetStatus,
611
- };
487
+ const current = readStoreJsonSync(workspaceRoot(), `state/todo/${nodeId}`);
488
+ if (current && deriveLinkedTodo(current, workspaceRoot()).step_ref)
489
+ return { ok: false, path: getTodoStatePath(), todo_path: getTodoPath(), error: 'Linked todo status belongs to its goal step; use complete_step.' };
490
+ return mutateTodos("transition_todo", async (repo) => {
491
+ const base = { path: getTodoStatePath(), todo_path: getTodoPath(), node_id: nodeId };
492
+ const node = await repo.get(nodeId);
493
+ if (!node)
494
+ return { ...base, ok: false, error: `Unknown node_id: ${nodeId}` };
495
+ const from = mapLegacyStatus(node.status);
496
+ if (from !== targetStatus && !TRANSITIONS[from]?.includes(targetStatus))
497
+ return { ...base, ok: false, from, to: targetStatus, allowed: TRANSITIONS[from], error: `Invalid transition ${from} -> ${targetStatus}` };
498
+ if (["in_progress", "gated", "done"].includes(targetStatus)) {
499
+ const unresolved = [];
500
+ for (const dep of node.depends_on ?? [])
501
+ if ((await repo.get(dep))?.status !== "done")
502
+ unresolved.push(dep);
503
+ if (unresolved.length)
504
+ return { ...base, ok: false, from, to: targetStatus, unresolved_dependencies: unresolved, error: `Dependencies not satisfied for ${nodeId}` };
612
505
  }
613
- const result = transitionTodoNode(nodeId, targetStatus);
614
- await waitForTodoStoreMirror();
615
- return result;
506
+ if (from !== targetStatus)
507
+ await repo.update(nodeId, { status: mapStoreStatus(targetStatus) });
508
+ return { ...base, ok: true, from, to: targetStatus };
616
509
  });
617
510
  }
618
- /**
619
- * Atomically append a new TODO item under a section, without requiring the
620
- * caller to read the whole board and resubmit it via update_todo(new_content).
621
- * That read-modify-write dance is what let concurrent agents silently clobber
622
- * each other's appended items: the read and write are separate MCP calls, so
623
- * a lock around the write alone cannot close the race. Here the read, insert,
624
- * and write all happen inside one TODO_MARKDOWN_REL lock acquisition.
625
- */
511
+ /** Insert once under the cross-process coordinator; never reconstruct existing records. */
626
512
  export async function appendTodoItemSafe(title, options) {
627
- const trimmedTitle = title.trim();
628
- if (!trimmedTitle) {
629
- return {
630
- ok: false,
631
- path: getTodoStatePath(),
632
- todo_path: getTodoPath(),
633
- error: "Todo item title must not be empty.",
634
- };
635
- }
636
- return withFileLock(TODO_MARKDOWN_REL, async () => {
637
- const existingRaw = safeRead(TODO_MARKDOWN_REL);
638
- const baseRaw = isReadError(existingRaw) ? "# Tasks\n" : existingRaw;
513
+ const trimmed = title.trim();
514
+ const base = { path: getTodoStatePath(), todo_path: getTodoPath() };
515
+ if (!trimmed)
516
+ return { ...base, ok: false, error: "Todo item title must not be empty." };
517
+ return mutateTodos("append_todo", async (repo) => {
518
+ const ids = new Set((await repo.getAll()).map((todo) => todo.id));
519
+ const id = options?.id?.trim() || uniqueTodoId(`todo-${slugify(trimmed)}`, ids);
520
+ if (ids.has(id))
521
+ return { ...base, ok: false, error: `Todo id already exists: ${id}` };
639
522
  const section = options?.section?.trim() || "Active Work";
640
- const dependsOn = (options?.depends_on ?? []).map((dep) => dep.trim()).filter(Boolean);
641
- const existingIds = new Set(parseTodoMarkdown(baseRaw).map((entry) => entry.id));
642
- const requestedId = options?.id?.trim();
643
- if (requestedId && existingIds.has(requestedId)) {
644
- return {
645
- ok: false,
646
- path: getTodoStatePath(),
647
- todo_path: getTodoPath(),
648
- error: `Todo id already exists: ${requestedId}`,
649
- };
650
- }
651
- const id = requestedId || uniqueTodoId(`todo-${slugify(trimmedTitle)}`, existingIds);
652
- const updatedRaw = insertTodoItem(baseRaw, section, trimmedTitle, id, dependsOn);
653
- const synced = await syncTodoStateLocked(updatedRaw);
654
- const mdPath = await safeWriteAsync(resolveWritableTaskPath("todo"), updatedRaw);
655
- return {
656
- ok: true,
657
- path: synced.path,
658
- todo_path: mdPath,
659
- node_id: id,
660
- section,
661
- };
523
+ await repo.create({ id, title: trimmed, status: "pending", section, depends_on: options?.depends_on?.map((dep) => dep.trim()).filter(Boolean) ?? [] });
524
+ return { ...base, ok: true, node_id: id, section };
525
+ });
526
+ }
527
+ export async function deleteTodoItemSafe(nodeId) {
528
+ return mutateTodos("delete_todo", async (repo) => {
529
+ const removed = await repo.remove(nodeId);
530
+ return { ok: removed, node_id: nodeId, path: getTodoStatePath(), todo_path: getTodoPath(), ...(removed ? {} : { error: `Unknown node_id: ${nodeId}` }) };
531
+ }, (raw) => {
532
+ const lines = raw.split(/\r?\n/);
533
+ let removing = false;
534
+ return lines.filter((line) => {
535
+ if (removing && /^\s*>/.test(line))
536
+ return false;
537
+ removing = false;
538
+ const id = line.match(/\(id:\s*([^;)]+)/)?.[1]?.trim() ?? line.match(/<!--\s*ace:.*?id\s*[:=]\s*([^;>]+)/)?.[1]?.trim();
539
+ if (id === nodeId) {
540
+ removing = true;
541
+ return false;
542
+ }
543
+ return true;
544
+ }).join("\n");
662
545
  });
663
546
  }
664
547
  //# sourceMappingURL=todo-state.js.map