@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.
- package/CHANGELOG.md +52 -14
- package/README.md +14 -10
- package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
- package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
- package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
- package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
- package/assets/agent-state/ACE_WORKFLOW.md +25 -2
- package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
- package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
- package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
- package/assets/eval-battery/battery.json +1 -1
- package/assets/instructions/ACE_Coder.instructions.md +9 -13
- package/assets/scripts/ace-hook-dispatch.mjs +16 -411
- package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
- package/assets/vendor/README.md +9 -0
- package/assets/vendor/vericify-reader.mjs +272 -0
- package/assets/vendor/vericify-reader.provenance.json +10 -0
- package/dist/ace-autonomy.d.ts +58 -0
- package/dist/ace-autonomy.js +211 -6
- package/dist/ace-internal-tools.d.ts +1 -0
- package/dist/ace-internal-tools.js +3 -3
- package/dist/ace-server-instructions.d.ts +3 -3
- package/dist/ace-server-instructions.js +46 -192
- package/dist/cli.js +44 -6
- package/dist/experience-reflector.d.ts +7 -0
- package/dist/experience-reflector.js +39 -0
- package/dist/goal-engine.d.ts +64 -7
- package/dist/goal-engine.js +197 -41
- package/dist/handoff-registry.d.ts +10 -0
- package/dist/handoff-registry.js +59 -0
- package/dist/helpers/artifacts.d.ts +6 -2
- package/dist/helpers/artifacts.js +8 -6
- package/dist/helpers/bootstrap.js +13 -20
- package/dist/helpers/path-utils.js +2 -0
- package/dist/helpers/store-resolution.js +18 -3
- package/dist/helpers/workspace-root.d.ts +2 -0
- package/dist/helpers/workspace-root.js +9 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +1 -0
- package/dist/hermes/bridge-protocol.d.ts +1 -1
- package/dist/hermes/session-manager.d.ts +17 -0
- package/dist/hermes/session-manager.js +67 -14
- package/dist/host-goal-engine.d.ts +138 -0
- package/dist/host-goal-engine.js +793 -0
- package/dist/host-goal-status.d.ts +113 -0
- package/dist/host-goal-status.js +175 -0
- package/dist/host-learning.d.ts +68 -0
- package/dist/host-learning.js +115 -0
- package/dist/host-observation.d.ts +12 -0
- package/dist/host-observation.js +91 -0
- package/dist/host-plan-revision.d.ts +14 -0
- package/dist/host-plan-revision.js +101 -0
- package/dist/host-verification-job.d.ts +12 -0
- package/dist/host-verification-job.js +183 -0
- package/dist/host-work-context.d.ts +12 -0
- package/dist/host-work-context.js +8 -0
- package/dist/job-scheduler.d.ts +48 -14
- package/dist/job-scheduler.js +169 -23
- package/dist/job-worker.d.ts +3 -1
- package/dist/job-worker.js +10 -3
- package/dist/kanban.d.ts +4 -1
- package/dist/kanban.js +44 -16
- package/dist/learned-artifacts.d.ts +64 -0
- package/dist/learned-artifacts.js +321 -0
- package/dist/learning-controls.d.ts +142 -0
- package/dist/learning-controls.js +135 -0
- package/dist/learning-input-context.d.ts +9 -0
- package/dist/learning-input-context.js +62 -0
- package/dist/learning-lifecycle.d.ts +10 -0
- package/dist/learning-lifecycle.js +11 -0
- package/dist/learning-overlays.d.ts +3 -0
- package/dist/learning-overlays.js +17 -0
- package/dist/learning-policy.d.ts +40 -0
- package/dist/learning-policy.js +38 -0
- package/dist/learning-records.d.ts +35 -0
- package/dist/learning-records.js +17 -0
- package/dist/learning-scorecard.d.ts +82 -0
- package/dist/learning-scorecard.js +86 -0
- package/dist/learning-serving.d.ts +29 -0
- package/dist/learning-serving.js +134 -0
- package/dist/linked-work.d.ts +9 -0
- package/dist/linked-work.js +28 -0
- package/dist/local-model-runtime.js +1 -1
- package/dist/mcp-command-progress.d.ts +16 -0
- package/dist/mcp-command-progress.js +70 -0
- package/dist/model-bridge.d.ts +14 -13
- package/dist/model-bridge.js +73 -41
- package/dist/orchestrator-supervisor.d.ts +108 -6
- package/dist/orchestrator-supervisor.js +4 -32
- package/dist/plan-proposal.js +3 -3
- package/dist/playbook-curator.d.ts +1 -0
- package/dist/playbook-curator.js +11 -9
- package/dist/process-identity.d.ts +8 -0
- package/dist/process-identity.js +38 -0
- package/dist/progress-command.d.ts +34 -0
- package/dist/progress-command.js +153 -0
- package/dist/prompts.js +15 -15
- package/dist/resources.js +2 -2
- package/dist/runtime-command.d.ts +8 -1
- package/dist/runtime-command.js +48 -7
- package/dist/runtime-executor.js +33 -11
- package/dist/runtime-profile-syntax.d.ts +6 -0
- package/dist/runtime-profile-syntax.js +121 -0
- package/dist/runtime-profile.d.ts +37 -5
- package/dist/runtime-profile.js +36 -129
- package/dist/runtime-tool-specs.js +1 -1
- package/dist/safe-edit.d.ts +7 -3
- package/dist/safe-edit.js +40 -47
- package/dist/schemas.js +4 -2
- package/dist/server.d.ts +2 -16
- package/dist/server.js +14 -20
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/dist/skill-auditor.d.ts +19 -0
- package/dist/skill-auditor.js +32 -2
- package/dist/skill-catalog.d.ts +4 -1
- package/dist/skill-catalog.js +6 -5
- package/dist/store/ace-packed-store.d.ts +5 -1
- package/dist/store/ace-packed-store.js +33 -1
- package/dist/store/bootstrap-store.js +19 -12
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +94 -27
- package/dist/store/importer.js +11 -20
- package/dist/store/knowledge-bake.d.ts +3 -1
- package/dist/store/knowledge-bake.js +10 -1
- package/dist/store/legacy-vericify-posts.d.ts +8 -0
- package/dist/store/legacy-vericify-posts.js +76 -0
- package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
- package/dist/store/materializers/hook-context-materializer.js +6 -2
- package/dist/store/materializers/host-file-materializer.js +2 -2
- package/dist/store/materializers/projection-manager.d.ts +4 -1
- package/dist/store/materializers/projection-manager.js +10 -3
- package/dist/store/materializers/todo-syncer.d.ts +39 -20
- package/dist/store/materializers/todo-syncer.js +208 -110
- package/dist/store/materializers/vericify-projector.js +15 -26
- package/dist/store/repositories/goal-repository.d.ts +51 -2
- package/dist/store/repositories/goal-repository.js +114 -48
- package/dist/store/repositories/playbook-repository.d.ts +41 -3
- package/dist/store/repositories/playbook-repository.js +61 -5
- package/dist/store/repositories/todo-repository.d.ts +8 -2
- package/dist/store/repositories/todo-repository.js +1 -1
- package/dist/store/repositories/vericify-repository.d.ts +4 -0
- package/dist/store/repositories/vericify-repository.js +46 -0
- package/dist/store/skills-install.d.ts +1 -0
- package/dist/store/skills-install.js +20 -16
- package/dist/store/source-preservation.d.ts +24 -0
- package/dist/store/source-preservation.js +79 -0
- package/dist/store/store-artifacts.js +8 -1
- package/dist/store/test-write-boundary.d.ts +16 -0
- package/dist/store/test-write-boundary.js +175 -0
- package/dist/store/types.d.ts +5 -1
- package/dist/store/write-coordinator.js +16 -0
- package/dist/task-plan-context.d.ts +11 -0
- package/dist/task-plan-context.js +59 -0
- package/dist/todo-state.d.ts +19 -9
- package/dist/todo-state.js +204 -321
- package/dist/tools-agent.d.ts +1 -1
- package/dist/tools-agent.js +38 -62
- package/dist/tools-files.d.ts +0 -3
- package/dist/tools-files.js +21 -11
- package/dist/tools-framework.js +16 -25
- package/dist/tools-goal.d.ts +2 -4
- package/dist/tools-goal.js +119 -16
- package/dist/tools-handoff.js +28 -4
- package/dist/tools-lifecycle.js +1 -1
- package/dist/tools-memory.d.ts +0 -3
- package/dist/tools-memory.js +51 -6
- package/dist/tools-scheduler.js +19 -17
- package/dist/tools-todo.js +160 -148
- package/dist/vericify-bridge.d.ts +8 -0
- package/dist/vericify-bridge.js +18 -0
- package/dist/vericify-context.d.ts +4 -0
- package/dist/vericify-context.js +26 -5
- package/dist/vericify-native.d.ts +101 -0
- package/dist/vericify-native.js +221 -0
- package/dist/verification-worker.d.ts +2 -0
- package/dist/verification-worker.js +31 -0
- package/docs/mcp-tool-reference.md +44 -9
- package/package.json +19 -6
- package/scripts/dev/run-tests-isolated.mjs +165 -0
- package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
- package/scripts/dev/test-isolation-guard.mjs +20 -0
- package/scripts/dev/test-preload.mjs +153 -0
- package/scripts/dev/test-source-inventory.mjs +31 -0
- package/docs/development.md +0 -46
package/dist/todo-state.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
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
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
const node
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
return
|
|
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
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|