@veewo/claw-core 0.1.25 → 0.1.26

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.
@@ -1,1687 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import fs from "node:fs";
4
- import os from "node:os";
5
- import path from "node:path";
6
- import { DatabaseSync } from "node:sqlite";
7
- import { buildMemoryIndex, ensureProjectProtocol, ensureUtf8Bom, editPlan, enforceTaskRetention, getMemory, ingestTruth, initProject, resolveContext, searchMemory, showPlan, switchTask, writePlan, } from "../src/index.js";
8
- import { readTextFile } from "../src/io.js";
9
- import { buildProjectKeywordSearchPlan, buildProjectQueryIntent, extractProjectKeywordTerms, } from "../src/memory-query.js";
10
- function createFixture(name) {
11
- const root = fs.mkdtempSync(path.join(os.tmpdir(), `claw-kit-${name}-`));
12
- fs.mkdirSync(path.join(root, ".claw", "truth"), { recursive: true });
13
- return root;
14
- }
15
- function createEmptyFixture(name) {
16
- return fs.mkdtempSync(path.join(os.tmpdir(), `claw-kit-${name}-`));
17
- }
18
- test("context resolves nested cwd to project .claw", () => {
19
- const root = createFixture("context");
20
- fs.mkdirSync(path.join(root, "src", "nested"), { recursive: true });
21
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({ id: "demo-project" }, null, 2));
22
- const result = resolveContext(path.join(root, "src", "nested"));
23
- assert.equal(result.project.projectRoot, root);
24
- assert.equal(result.project.projectId, "demo-project");
25
- });
26
- test("initProject creates a minimal .claw project scaffold", () => {
27
- const root = createEmptyFixture("init");
28
- const result = initProject({
29
- cwd: root,
30
- projectName: "Demo Project",
31
- maxTasksToKeep: 20,
32
- externalTruthSkill: "external-truth-writer",
33
- externalAdrSkill: "external-adr-writer",
34
- contextPaths: ["docs/project-guide.md"],
35
- externalDocPaths: ["docs/", "README.md"],
36
- gitnexusEnabled: true,
37
- });
38
- const projectConfig = JSON.parse(fs.readFileSync(path.join(root, ".claw", "project.json"), "utf-8"));
39
- assert.equal(result.projectId, "demo-project");
40
- assert.ok(fs.existsSync(path.join(root, ".claw", "project.json")));
41
- assert.ok(fs.existsSync(path.join(root, ".claw", "memory.md")));
42
- assert.ok(fs.existsSync(path.join(root, ".claw", "truth", "SUMMARY.md")));
43
- assert.ok(fs.existsSync(path.join(root, ".claw", "tasks")));
44
- assert.deepEqual(projectConfig, {
45
- id: "demo-project",
46
- name: "Demo Project",
47
- maxTasksToKeep: 20,
48
- externalTruthSkill: "external-truth-writer",
49
- externalAdrSkill: "external-adr-writer",
50
- contextPaths: ["docs/project-guide.md"],
51
- memory: {
52
- externalDocPaths: ["docs/", "README.md"],
53
- embedding: {
54
- provider: "local",
55
- model: "Snowflake/snowflake-arctic-embed-xs",
56
- local: {
57
- modelCacheDir: ".claw/models",
58
- },
59
- store: {
60
- vector: {
61
- enabled: true,
62
- },
63
- },
64
- },
65
- },
66
- gitnexus: {
67
- enabled: true,
68
- },
69
- });
70
- });
71
- test("plan write creates task-bound plan and updates activePlan", async () => {
72
- const root = createFixture("plan-write");
73
- const result = await writePlan({
74
- cwd: root,
75
- taskName: "demo-task",
76
- title: "Demo task",
77
- goalText: "Ship the first plan",
78
- });
79
- const meta = JSON.parse(fs.readFileSync(result.metaPath, "utf-8"));
80
- assert.equal(result.planFile, "plan.json");
81
- assert.equal(meta.activePlan, "plan.json");
82
- assert.equal(meta.rootPlan, "plan.json");
83
- assert.ok(fs.existsSync(result.planPath));
84
- assert.equal(result.workflowGuidance.stage, "requirements");
85
- assert.equal(result.workflowGuidance.delegateSubagents, undefined);
86
- assert.equal(result.workflowGuidance.goalMode, undefined);
87
- assert.ok(result.workflowGuidance.summary.includes("Fill the remaining plan fields"));
88
- assert.ok(result.workflowGuidance.nextStep.includes("Review whether requirements are clear enough to execute"));
89
- assert.ok(result.workflowGuidance.nextStep.includes("Fill the `requirements` section"));
90
- assert.ok(result.workflowGuidance.nextStep.includes("move into `process.active`"));
91
- assert.equal(result.workflowGuidance.askUser, undefined);
92
- assert.deepEqual(result.planSchema.references[0], {
93
- path: "<string>",
94
- why: "<string>",
95
- });
96
- assert.equal(result.planView.collapsedSummary, "Demo task");
97
- assert.equal(result.planView.goal.defaultCollapsed, true);
98
- assert.equal(result.planView.renderHints.defaultCollapsed, true);
99
- assert.equal(result.planView.expanded.sections[0]?.id, "goal");
100
- assert.equal(result.planView.expanded.sections[0]?.defaultExpanded, false);
101
- assert.equal(result.planView.expanded.sections[1]?.id, "tasks");
102
- });
103
- test("plan write guidance leaves requirement judgment to the agent", async () => {
104
- const root = createFixture("plan-write-clear-requirements");
105
- const result = await writePlan({
106
- cwd: root,
107
- taskName: "demo-task",
108
- title: "Demo task",
109
- goalText: "Ship the first plan",
110
- content: {
111
- title: "Demo task",
112
- status: "prepare.requirements",
113
- goal: { text: "Ship the first plan" },
114
- tasks: [{ id: 1, title: "Implement work", status: "pending" }],
115
- },
116
- });
117
- assert.equal(result.workflowGuidance.askUser, undefined);
118
- assert.ok(result.workflowGuidance.nextStep.includes("Fill the `requirements` section"));
119
- assert.ok(result.workflowGuidance.nextStep.includes("If requirements are clear, move into `process.active`"));
120
- assert.ok(result.workflowGuidance.nextStep.includes("If requirements are not clear, ask the user to clarify the missing scope first"));
121
- assert.deepEqual(result.workflowGuidance.notes, [
122
- "Do not start implementation while the plan is still in `prepare.requirements`.",
123
- "If requirements are already complete after editing the plan, switch the status to `process.active` immediately.",
124
- ]);
125
- });
126
- test("plan write without goal tells the agent to fill goal first", async () => {
127
- const root = createFixture("plan-write-no-goal");
128
- const result = await writePlan({
129
- cwd: root,
130
- title: "Goal later task",
131
- });
132
- assert.equal(result.planStatus, "prepare.requirements");
133
- assert.equal(result.workflowGuidance.goalMode, undefined);
134
- assert.ok(result.workflowGuidance.summary.includes("Add the goal first"));
135
- assert.ok(result.workflowGuidance.nextStep.includes("Fill `goal.text`"));
136
- assert.deepEqual(result.workflowGuidance.recommendedCommands?.slice(0, 2), [
137
- "claw plan edit --task Goal-later-task --plan-status process.active",
138
- "claw plan edit --task Goal-later-task --patch <updated-plan.json>",
139
- ]);
140
- });
141
- test("plan write auto-assigns stable integer task ids when omitted", async () => {
142
- const root = createFixture("plan-write-auto-task-ids");
143
- const result = await writePlan({
144
- cwd: root,
145
- taskName: "demo-task",
146
- title: "Demo task",
147
- goalText: "Ship the first plan",
148
- content: {
149
- title: "Demo task",
150
- status: "prepare.requirements",
151
- goal: { text: "Ship the first plan" },
152
- tasks: [
153
- { title: "First task", status: "pending" },
154
- { title: "Second task", status: "pending" },
155
- ],
156
- },
157
- });
158
- assert.deepEqual(result.planView.tasks.items.map((task) => ({ id: task.id, title: task.title })), [
159
- { id: 1, title: "First task" },
160
- { id: 2, title: "Second task" },
161
- ]);
162
- assert.equal(result.workflowGuidance.askUser, undefined);
163
- });
164
- test("plan write updates existing task and supports subplan under plans without switching task scope", async () => {
165
- const root = createFixture("subplan-write");
166
- await writePlan({
167
- cwd: root,
168
- taskName: "demo-task",
169
- title: "Demo task",
170
- goalText: "Ship the parent plan",
171
- content: {
172
- title: "Demo task",
173
- status: "process.active",
174
- goal: { text: "Ship the parent plan" },
175
- tasks: [
176
- {
177
- id: 1,
178
- title: "Implement child work",
179
- status: "pending",
180
- },
181
- ],
182
- },
183
- });
184
- const result = await writePlan({
185
- cwd: root,
186
- taskName: "demo-task",
187
- filePath: "child-plan.json",
188
- parentTaskId: 1,
189
- title: "Child plan",
190
- goalText: "Handle a subplan",
191
- content: {
192
- title: "Child plan",
193
- status: "prepare.requirements",
194
- goal: { text: "Handle a subplan" },
195
- tasks: [],
196
- },
197
- });
198
- const meta = JSON.parse(fs.readFileSync(path.join(root, ".claw", "tasks", "demo-task", "meta.json"), "utf-8"));
199
- const parentPlan = JSON.parse(fs.readFileSync(path.join(root, ".claw", "tasks", "demo-task", "plan.json"), "utf-8"));
200
- assert.equal(result.planFile, "plans/child-plan.json");
201
- assert.equal(meta.activePlan, "plans/child-plan.json");
202
- assert.equal(parentPlan.tasks[0]?.execution?.type, "subplan");
203
- assert.equal(parentPlan.tasks[0]?.execution?.subplan, "plans/child-plan.json");
204
- });
205
- test("subplan completion resumes the parent plan and marks the parent task done", async () => {
206
- const root = createFixture("subplan-complete-resume-parent");
207
- await writePlan({
208
- cwd: root,
209
- taskName: "demo-task",
210
- title: "Demo task",
211
- goalText: "Ship the parent plan",
212
- content: {
213
- title: "Demo task",
214
- status: "process.active",
215
- goal: { text: "Ship the parent plan" },
216
- tasks: [
217
- {
218
- id: 1,
219
- title: "Implement child work",
220
- status: "pending",
221
- },
222
- {
223
- id: 2,
224
- title: "Resume parent work",
225
- status: "pending",
226
- },
227
- ],
228
- },
229
- });
230
- await writePlan({
231
- cwd: root,
232
- taskName: "demo-task",
233
- filePath: "child-plan.json",
234
- parentTaskId: 1,
235
- title: "Child plan",
236
- goalText: "Handle a subplan",
237
- content: {
238
- title: "Child plan",
239
- status: "process.active",
240
- goal: { text: "Handle a subplan" },
241
- tasks: [{ id: 1, title: "Finish child", status: "done" }],
242
- retrospective: { summary: "Child complete." },
243
- },
244
- });
245
- const result = await editPlan({
246
- cwd: root,
247
- taskName: "demo-task",
248
- planFile: "plans/child-plan.json",
249
- planStatus: "end.completed",
250
- patch: {
251
- retrospective: { summary: "Child complete." },
252
- },
253
- });
254
- const meta = JSON.parse(fs.readFileSync(path.join(root, ".claw", "tasks", "demo-task", "meta.json"), "utf-8"));
255
- const parentPlan = JSON.parse(fs.readFileSync(path.join(root, ".claw", "tasks", "demo-task", "plan.json"), "utf-8"));
256
- const childPlan = JSON.parse(fs.readFileSync(path.join(root, ".claw", "tasks", "demo-task", "plans", "child-plan.json"), "utf-8"));
257
- assert.equal(result.planFile, "plan.json");
258
- assert.equal(result.planStatus, "process.active");
259
- assert.equal(result.workflowGuidance.stage, "execution");
260
- assert.equal(result.workflowGuidance.nextTask?.id, 2);
261
- assert.equal(result.workflowGuidance.nextTask?.title, "Resume parent work");
262
- assert.equal(result.workflowGuidance.delegateSubagents, undefined);
263
- assert.equal(meta.activePlan, "plan.json");
264
- assert.equal(meta.status, "active");
265
- assert.equal(parentPlan.status, "process.active");
266
- assert.equal(parentPlan.tasks[0]?.status, "done");
267
- assert.equal(parentPlan.tasks[1]?.status, "pending");
268
- assert.equal(childPlan.status, "end.completed");
269
- });
270
- test("plan write no longer runs a separate review gate before execution", async () => {
271
- const root = createFixture("plan-write-review");
272
- const result = await writePlan({
273
- cwd: root,
274
- taskName: "demo-task",
275
- title: "Demo task",
276
- goalText: "Ship work directly",
277
- planStatus: "process.active",
278
- });
279
- assert.equal(result.planStatus, "process.active");
280
- assert.equal(result.planReview, undefined);
281
- });
282
- test("plan edit enforces two-part transition rules", async () => {
283
- const root = createFixture("plan-edit-transition");
284
- await writePlan({
285
- cwd: root,
286
- taskName: "demo-task",
287
- title: "Demo task",
288
- goalText: "Ship the first plan",
289
- content: {
290
- title: "Demo task",
291
- status: "process.active",
292
- goal: { text: "Ship the first plan" },
293
- tasks: [],
294
- },
295
- });
296
- await assert.rejects(() => editPlan({
297
- cwd: root,
298
- taskName: "demo-task",
299
- planStatus: "prepare.requirements",
300
- }), /Cannot move from process\.\* back to prepare\.\*/);
301
- });
302
- test("plan edit requires retrospective summary before end.completed", async () => {
303
- const root = createFixture("plan-edit-retrospective");
304
- await writePlan({
305
- cwd: root,
306
- taskName: "demo-task",
307
- title: "Demo task",
308
- goalText: "Ship the first plan",
309
- });
310
- await assert.rejects(() => editPlan({
311
- cwd: root,
312
- taskName: "demo-task",
313
- planStatus: "end.completed",
314
- }), /retrospective\.summary/);
315
- });
316
- test("plan edit can move from requirements to process.active without a separate review gate", async () => {
317
- const root = createFixture("plan-edit-review");
318
- await writePlan({
319
- cwd: root,
320
- taskName: "demo-task",
321
- title: "Demo task",
322
- goalText: "Ship the first plan",
323
- content: {
324
- title: "Demo task",
325
- status: "prepare.requirements",
326
- goal: { text: "Ship the first plan" },
327
- tasks: [{ id: 1, title: "Implement work", status: "pending" }],
328
- },
329
- });
330
- const activated = await editPlan({
331
- cwd: root,
332
- taskName: "demo-task",
333
- planStatus: "process.active",
334
- });
335
- assert.equal(activated.planStatus, "process.active");
336
- assert.equal(activated.workflowGuidance.stage, "execution");
337
- assert.equal(activated.workflowGuidance.nextTask?.id, 1);
338
- assert.equal(activated.workflowGuidance.nextTask?.title, "Implement work");
339
- assert.equal(activated.workflowGuidance.goalMode?.recommendedObjective, "\u6309\u7167 claw \u6d41\u7a0b\uff0c\u63a8\u8fdb\u4efb\u52a1\uff0c\u66f4\u65b0plan\uff0c\u5b8c\u6210\uff1aShip the first plan");
340
- assert.equal(activated.workflowGuidance.goalMode?.setWhen, "on_enter_process_active");
341
- assert.ok(activated.workflowGuidance.nextStep.includes("Sync the thread progress with our tasks."));
342
- assert.ok(activated.workflowGuidance.nextStep.includes("task #1"));
343
- const result = await editPlan({
344
- cwd: root,
345
- taskName: "demo-task",
346
- taskId: 1,
347
- taskStatus: "done",
348
- });
349
- assert.equal(result.planStatus, "process.active");
350
- assert.equal(result.planView.counts.completed, 1);
351
- assert.deepEqual(result.planView.tasks.items.map((task) => ({ id: task.id, status: task.status })), [{ id: 1, status: "done" }]);
352
- assert.equal(result.workflowGuidance.stage, "done");
353
- assert.ok(result.workflowGuidance.recommendedCommands?.some((command) => command.includes("claw plan done")));
354
- const truthDelegate = result.workflowGuidance.delegateSubagents?.[0];
355
- assert.ok(truthDelegate);
356
- assert.equal(truthDelegate.name, "truth-writer");
357
- assert.equal(truthDelegate.skill, "claw-kit:truth-writer");
358
- assert.equal(truthDelegate.model, "gpt-5.4-mini");
359
- assert.equal(truthDelegate.fork_context, false);
360
- assert.equal(truthDelegate.waitForCompletion, false);
361
- assert.equal(truthDelegate.preferReuseSameTypeInThread, true);
362
- assert.equal(truthDelegate.closePolicy, "keep_open_for_reuse");
363
- assert.equal(truthDelegate.inputContract, "curated completed subtask report with valuable findings for truth deposition");
364
- assert.ok(result.workflowGuidance.nextStep.includes("truth-writer"));
365
- assert.ok(result.workflowGuidance.nextStep.includes("retrospective"));
366
- });
367
- test("plan edit rejects entering process.active without goal text", async () => {
368
- const root = createFixture("plan-edit-active-requires-goal");
369
- await writePlan({
370
- cwd: root,
371
- title: "Goal later task",
372
- });
373
- await assert.rejects(() => editPlan({
374
- cwd: root,
375
- taskName: "Goal-later-task",
376
- planStatus: "process.active",
377
- }), /goal\.text is required before the plan can leave prepare\.requirements/);
378
- });
379
- test("process entry returns the first task and task completion returns truth-writer contract before plan completion", async () => {
380
- const root = createFixture("process-entry-and-truth-contract");
381
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
382
- id: "process-entry-and-truth-contract",
383
- name: "Process Entry And Truth Contract",
384
- maxTasksToKeep: 99,
385
- externalTruthSkill: "external-truth-writer",
386
- externalAdrSkill: null,
387
- contextPaths: [],
388
- memory: {
389
- externalDocPaths: [],
390
- embedding: {
391
- provider: "local",
392
- model: "Snowflake/snowflake-arctic-embed-xs",
393
- local: {
394
- modelCacheDir: ".claw/models",
395
- },
396
- store: {
397
- vector: {
398
- enabled: true,
399
- },
400
- },
401
- },
402
- },
403
- gitnexus: { enabled: false },
404
- }, null, 2), "utf-8");
405
- await writePlan({
406
- cwd: root,
407
- taskName: "demo-task",
408
- title: "Demo task",
409
- goalText: "Verify process entry and task completion semantics",
410
- content: {
411
- title: "Demo task",
412
- status: "prepare.requirements",
413
- goal: { text: "Verify process entry and task completion semantics" },
414
- tasks: [
415
- { id: 1, title: "First task", status: "pending" },
416
- { id: 2, title: "Second task", status: "pending" },
417
- ],
418
- },
419
- });
420
- const activated = await editPlan({
421
- cwd: root,
422
- taskName: "demo-task",
423
- planStatus: "process.active",
424
- });
425
- assert.equal(activated.workflowGuidance.nextTask?.id, 1);
426
- assert.equal(activated.workflowGuidance.delegateSubagents, undefined);
427
- const taskDone = await editPlan({
428
- cwd: root,
429
- taskName: "demo-task",
430
- taskId: 1,
431
- taskStatus: "done",
432
- });
433
- assert.equal(taskDone.workflowGuidance.stage, "execution");
434
- assert.equal(taskDone.workflowGuidance.nextTask?.id, 2);
435
- assert.equal(taskDone.workflowGuidance.nextStep, "1. Sync the thread progress with our tasks. 2. Curate the valuable findings from the completed task into a completed subtask report, then dispatch `truth-writer` with that report. 3. Continue with task #2.");
436
- assert.equal(taskDone.workflowGuidance.delegateSubagents?.[0]?.skill, "external-truth-writer");
437
- assert.equal(taskDone.workflowGuidance.delegateSubagents?.[0]?.fork_context, false);
438
- });
439
- test("plan edit appendTasks auto-assigns ids when omitted", async () => {
440
- const root = createFixture("plan-edit-auto-task-ids");
441
- await writePlan({
442
- cwd: root,
443
- taskName: "demo-task",
444
- title: "Demo task",
445
- goalText: "Ship the first plan",
446
- content: {
447
- title: "Demo task",
448
- status: "process.active",
449
- goal: { text: "Ship the first plan" },
450
- tasks: [{ id: 3, title: "Existing task", status: "pending" }],
451
- },
452
- });
453
- const result = await editPlan({
454
- cwd: root,
455
- taskName: "demo-task",
456
- appendTasks: [
457
- { title: "Auto id task", status: "pending" },
458
- ],
459
- });
460
- assert.deepEqual(result.planView.tasks.items.map((task) => ({ id: task.id, title: task.title })), [
461
- { id: 3, title: "Existing task" },
462
- { id: 4, title: "Auto id task" },
463
- ]);
464
- });
465
- test("plan edit changing a task back to pending does not advertise nextTask", async () => {
466
- const root = createFixture("plan-edit-pending-no-next-task");
467
- await writePlan({
468
- cwd: root,
469
- taskName: "demo-task",
470
- title: "Demo task",
471
- goalText: "Verify pending edits stay lightweight",
472
- content: {
473
- title: "Demo task",
474
- status: "process.active",
475
- goal: { text: "Verify pending edits stay lightweight" },
476
- tasks: [
477
- { id: 1, title: "Current task", status: "in_progress" },
478
- { id: 2, title: "Later task", status: "pending" },
479
- ],
480
- },
481
- });
482
- const result = await editPlan({
483
- cwd: root,
484
- taskName: "demo-task",
485
- taskId: 1,
486
- taskStatus: "pending",
487
- });
488
- assert.equal(result.workflowGuidance.nextStep, "Continue with task #1.");
489
- assert.equal(result.workflowGuidance.nextTask, undefined);
490
- assert.deepEqual(result.workflowGuidance.recommendedCommands, [
491
- "claw plan edit --task demo-task --task-id <id> --task-status done",
492
- ]);
493
- });
494
- test("plan view orders unfinished tasks before done tasks while preserving stable order", async () => {
495
- const root = createFixture("plan-view-order");
496
- await writePlan({
497
- cwd: root,
498
- taskName: "demo-task",
499
- title: "Ordered task",
500
- goalText: "Check plan view ordering",
501
- content: {
502
- title: "Ordered task",
503
- status: "process.active",
504
- goal: { text: "Check plan view ordering" },
505
- tasks: [
506
- { id: 1, title: "Done first", status: "done" },
507
- { id: 2, title: "Pending second", status: "pending" },
508
- { id: 3, title: "Blocked third", status: "blocked" },
509
- { id: 4, title: "Done fourth", status: "done" },
510
- ],
511
- },
512
- });
513
- const result = await editPlan({
514
- cwd: root,
515
- taskName: "demo-task",
516
- patch: { summary: "No-op patch to inspect plan view" },
517
- });
518
- assert.equal(result.planView.collapsedSummary, "2/4 Ordered task");
519
- assert.deepEqual(result.planView.tasks.items.map((task) => task.id), [2, 3, 1, 4]);
520
- });
521
- test("plan show returns canonical plan plus collapsed and expanded plan view data", async () => {
522
- const root = createFixture("plan-show");
523
- await writePlan({
524
- cwd: root,
525
- taskName: "demo-task",
526
- title: "Shown task",
527
- goalText: "Render the current plan",
528
- content: {
529
- title: "Shown task",
530
- status: "process.active",
531
- goal: { text: "Render the current plan" },
532
- tasks: [
533
- { id: 1, title: "First task", status: "done" },
534
- { id: 2, title: "Second task", status: "in_progress" },
535
- ],
536
- },
537
- });
538
- const result = showPlan({
539
- cwd: root,
540
- taskName: "demo-task",
541
- });
542
- assert.equal(result.plan.title, "Shown task");
543
- assert.equal(result.planView.collapsedSummary, "1/2 Shown task");
544
- assert.equal(result.planView.goal.text, "Render the current plan");
545
- assert.equal(result.planView.renderHints.defaultCollapsed, true);
546
- assert.deepEqual(result.planView.expanded.sections.map((section) => section.id), ["goal", "tasks"]);
547
- assert.equal(result.planView.expanded.sections[0]?.type, "disclosure");
548
- assert.equal(result.planView.expanded.sections[1]?.type, "list");
549
- assert.deepEqual(result.planView.tasks.items.map((task) => task.id), [2, 1]);
550
- assert.deepEqual(result.planView.expanded.sections[1]?.items.map((task) => task.id), [2, 1]);
551
- });
552
- test("plan show falls back to archived tasks when the active task no longer exists", async () => {
553
- const root = createFixture("plan-show-archived");
554
- initProject({
555
- cwd: root,
556
- projectName: "Archived Show",
557
- maxTasksToKeep: 99,
558
- force: true,
559
- });
560
- await writePlan({
561
- cwd: root,
562
- taskName: "archived-task",
563
- title: "Archived task",
564
- goalText: "Show archived plan",
565
- content: {
566
- title: "Archived task",
567
- status: "end.completed",
568
- goal: { text: "Show archived plan" },
569
- tasks: [{ id: 1, title: "Done task", status: "done" }],
570
- retrospective: { summary: "Archived." },
571
- },
572
- });
573
- const project = resolveContext(root).project;
574
- enforceTaskRetention(project, "archived-task");
575
- const result = showPlan({
576
- cwd: root,
577
- taskName: "archived-task",
578
- });
579
- assert.equal(result.archived, true);
580
- assert.match(result.planPath, /archive[\\/]tasks[\\/]archived-task[\\/].*plan\.json$/);
581
- assert.equal(result.plan.title, "Archived task");
582
- assert.equal(result.planView.collapsedSummary, "1/1 Archived task");
583
- });
584
- test("switch-task writes lineage metadata without session runtime", async () => {
585
- const root = createFixture("switch-task");
586
- await writePlan({ cwd: root, taskName: "source-task", title: "Source", goalText: "Source goal" });
587
- await writePlan({ cwd: root, taskName: "target-task", title: "Target", goalText: "Target goal" });
588
- const result = switchTask({
589
- cwd: root,
590
- fromTask: "source-task",
591
- toTask: "target-task",
592
- });
593
- const sourceMeta = JSON.parse(fs.readFileSync(result.sourceMetaPath, "utf-8"));
594
- const targetMeta = JSON.parse(fs.readFileSync(result.targetMetaPath, "utf-8"));
595
- assert.equal(sourceMeta.leaveState.toTask, "target-task");
596
- assert.equal(targetMeta.previousTask.task, "source-task");
597
- assert.equal(targetMeta.inheritedFrom.task, "source-task");
598
- });
599
- test("memory search defaults to project scope and task scope prioritizes active plan structured memory", async () => {
600
- const root = createFixture("memory-search");
601
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
602
- id: "memory-search",
603
- memory: {
604
- externalDocPaths: ["docs/"],
605
- embedding: {
606
- provider: "local",
607
- model: "Snowflake/snowflake-arctic-embed-xs",
608
- local: {
609
- modelCacheDir: path.join(root, ".model-cache"),
610
- },
611
- },
612
- },
613
- }, null, 2), "utf-8");
614
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
615
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project alpha memory\n", "utf-8");
616
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared beta truth\n", "utf-8");
617
- fs.writeFileSync(path.join(root, "docs", "guide.md"), "zeta external doc\n", "utf-8");
618
- fs.writeFileSync(path.join(root, "docs", "notes.txt"), "legacy txt doc\n", "utf-8");
619
- await writePlan({
620
- cwd: root,
621
- taskName: "demo-task",
622
- title: "Task title",
623
- goalText: "Task goal",
624
- content: {
625
- title: "Task title",
626
- status: "process.active",
627
- goal: { text: "Task goal" },
628
- tasks: [],
629
- rules: ["gamma rule"],
630
- references: [{ why: "delta proof", path: "src/index.ts" }],
631
- },
632
- });
633
- fs.writeFileSync(path.join(root, ".claw", "tasks", "demo-task", "memory.md"), "legacy epsilon task memory\n", "utf-8");
634
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
635
- process.env.CLAW_EMBEDDING_MOCK = "1";
636
- try {
637
- const projectIndex = buildMemoryIndex({ cwd: root });
638
- const taskIndex = buildMemoryIndex({ cwd: root, scope: "task", taskName: "demo-task" });
639
- const projectSearch = searchMemory({ cwd: root, query: "alpha" });
640
- const externalSearch = searchMemory({ cwd: root, query: "zeta" });
641
- const txtSearch = searchMemory({ cwd: root, query: "legacy" });
642
- const taskSearch = searchMemory({ cwd: root, scope: "task", taskName: "demo-task", query: "gamma" });
643
- const taskMemory = getMemory({ cwd: root, scope: "task", taskName: "demo-task" });
644
- assert.equal(projectIndex.scope, "project");
645
- assert.equal(taskIndex.scope, "task");
646
- assert.deepEqual(projectIndex.embedding, {
647
- provider: "local",
648
- model: "Snowflake/snowflake-arctic-embed-xs",
649
- local: {
650
- modelCacheDir: path.join(root, ".model-cache"),
651
- },
652
- store: {
653
- vector: {
654
- enabled: true,
655
- },
656
- },
657
- });
658
- assert.ok(projectSearch.results.some((item) => item.sourcePath.endsWith(path.join(".claw", "memory.md"))));
659
- assert.ok(projectIndex.sources.some((item) => item.endsWith(path.join("docs", "guide.md"))));
660
- assert.equal(projectIndex.sources.some((item) => item.endsWith(path.join("docs", "notes.txt"))), false);
661
- assert.ok(externalSearch.results.some((item) => item.sourcePath.endsWith(path.join("docs", "guide.md"))));
662
- assert.equal(txtSearch.results.some((item) => item.sourcePath.endsWith(path.join("docs", "notes.txt"))), false);
663
- assert.ok(taskSearch.results.some((item) => item.kind === "active_plan"));
664
- assert.equal(taskMemory.sources[0]?.kind, "active_plan");
665
- }
666
- finally {
667
- if (previousMockEnv === undefined) {
668
- delete process.env.CLAW_EMBEDDING_MOCK;
669
- }
670
- else {
671
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
672
- }
673
- }
674
- });
675
- test("project search rejects queries when no vector index is available", () => {
676
- const root = createFixture("memory-search-no-vectors");
677
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
678
- id: "memory-search-no-vectors",
679
- memory: {
680
- externalDocPaths: [],
681
- embedding: {
682
- provider: "local",
683
- model: "Snowflake/snowflake-arctic-embed-xs",
684
- local: {
685
- modelCacheDir: ".claw/models",
686
- },
687
- store: {
688
- vector: {
689
- enabled: false,
690
- },
691
- },
692
- },
693
- },
694
- }, null, 2), "utf-8");
695
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project alpha memory\n", "utf-8");
696
- assert.throws(() => searchMemory({ cwd: root, query: "alpha" }), /requires memory\.embedding|vector index/i);
697
- });
698
- /* test("project keyword search plan keeps exact multi-term query and per-term Chinese fallbacks", () => {
699
- assert.deepEqual(
700
- buildProjectKeywordSearchPlan("搜打撤 哈基宝"),
701
- [
702
- { query: "\"搜打撤\" AND \"哈基宝\"", matchedTerms: ["搜打撤", "哈基宝"] },
703
- { query: "\"搜打撤\"", matchedTerms: ["搜打撤"] },
704
- { query: "\"哈基宝\"", matchedTerms: ["哈基宝"] },
705
- ],
706
- );
707
- });
708
-
709
- test("project search keeps recall for multi-term Chinese queries across different markdown docs", { concurrency: false }, () => {
710
- const root = createFixture("memory-search-chinese-multi-term");
711
- fs.writeFileSync(
712
- path.join(root, ".claw", "project.json"),
713
- JSON.stringify(
714
- {
715
- id: "memory-search-chinese-multi-term",
716
- memory: {
717
- externalDocPaths: ["docs/"],
718
- embedding: {
719
- provider: "local",
720
- model: "Snowflake/snowflake-arctic-embed-xs",
721
- local: {
722
- modelCacheDir: path.join(root, ".model-cache"),
723
- },
724
- },
725
- },
726
- },
727
- null,
728
- 2,
729
- ),
730
- "utf-8",
731
- );
732
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
733
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "项目检索记忆\n", "utf-8");
734
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "共享中文 truth\n", "utf-8");
735
- fs.writeFileSync(path.join(root, "docs", "sdtz.md"), "这里记录搜打撤模式的说明\n", "utf-8");
736
- fs.writeFileSync(path.join(root, "docs", "hjb.md"), "这里记录哈基宝的说明\n", "utf-8");
737
-
738
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
739
- process.env.CLAW_EMBEDDING_MOCK = "1";
740
-
741
- try {
742
- buildMemoryIndex({ cwd: root });
743
-
744
- const firstTerm = searchMemory({ cwd: root, query: "搜打撤" });
745
- const secondTerm = searchMemory({ cwd: root, query: "哈基宝" });
746
- const multiTerm = searchMemory({ cwd: root, query: "搜打撤 哈基宝", limit: 5 });
747
-
748
- assert.ok(firstTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "sdtz.md"))));
749
- assert.ok(secondTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "hjb.md"))));
750
- assert.ok(multiTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "sdtz.md"))));
751
- assert.ok(multiTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "hjb.md"))));
752
- } finally {
753
- if (previousMockEnv === undefined) {
754
- delete process.env.CLAW_EMBEDDING_MOCK;
755
- } else {
756
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
757
- }
758
- }
759
- });
760
-
761
- */
762
- test("project keyword search plan keeps exact multi-term query and per-term Chinese fallbacks", () => {
763
- assert.deepEqual(buildProjectKeywordSearchPlan("\u641c\u6253\u64a4 \u54c8\u57fa\u5b9d"), [
764
- {
765
- query: "\"\u641c\u6253\u64a4\" AND \"\u54c8\u57fa\u5b9d\"",
766
- matchedTerms: ["\u641c\u6253\u64a4", "\u54c8\u57fa\u5b9d"],
767
- substringTerms: [],
768
- },
769
- {
770
- query: "\"\u641c\u6253\u64a4\"",
771
- matchedTerms: ["\u641c\u6253\u64a4"],
772
- substringTerms: [],
773
- },
774
- {
775
- query: "\"\u54c8\u57fa\u5b9d\"",
776
- matchedTerms: ["\u54c8\u57fa\u5b9d"],
777
- substringTerms: [],
778
- },
779
- ]);
780
- });
781
- test("project keyword search plan uses substring fallback for short Chinese terms", () => {
782
- assert.deepEqual(buildProjectKeywordSearchPlan("\u641c\u6253 \u54c8\u57fa"), [
783
- {
784
- query: null,
785
- matchedTerms: ["\u641c\u6253", "\u54c8\u57fa"],
786
- substringTerms: ["\u641c\u6253", "\u54c8\u57fa"],
787
- },
788
- {
789
- query: null,
790
- matchedTerms: ["\u641c\u6253"],
791
- substringTerms: ["\u641c\u6253"],
792
- },
793
- {
794
- query: null,
795
- matchedTerms: ["\u54c8\u57fa"],
796
- substringTerms: ["\u54c8\u57fa"],
797
- },
798
- ]);
799
- });
800
- test("project keyword extraction keeps meaningful Chinese terms from conversational queries", () => {
801
- assert.deepEqual(extractProjectKeywordTerms("\u4e4b\u524d\u8ba8\u8bba\u7684\u90a3\u4e2a\u641c\u6253\u64a4\u65b9\u6848"), ["\u8ba8\u8bba", "\u641c\u6253\u64a4", "\u65b9\u6848"]);
802
- });
803
- test("project keyword extraction drops OpenClaw-style English stop words", () => {
804
- assert.deepEqual(extractProjectKeywordTerms("please show me that thing we discussed about the extraction API"), ["discussed", "extraction", "api"]);
805
- });
806
- test("project keyword search plan skips weak standalone fallback terms from conversational Chinese queries", () => {
807
- assert.deepEqual(buildProjectKeywordSearchPlan("\u4e4b\u524d\u8ba8\u8bba\u7684\u90a3\u4e2a\u641c\u6253\u64a4\u65b9\u6848"), [
808
- {
809
- query: "\"\u641c\u6253\u64a4\"",
810
- matchedTerms: ["\u8ba8\u8bba", "\u641c\u6253\u64a4", "\u65b9\u6848"],
811
- substringTerms: ["\u8ba8\u8bba", "\u65b9\u6848"],
812
- },
813
- {
814
- query: "\"\u641c\u6253\u64a4\"",
815
- matchedTerms: ["\u641c\u6253\u64a4", "\u65b9\u6848"],
816
- substringTerms: ["\u65b9\u6848"],
817
- },
818
- {
819
- query: "\"\u641c\u6253\u64a4\"",
820
- matchedTerms: ["\u641c\u6253\u64a4"],
821
- substringTerms: [],
822
- },
823
- ]);
824
- });
825
- test("project query intent uses strong terms for conversational Chinese embedding text", () => {
826
- assert.deepEqual(buildProjectQueryIntent("\u4e4b\u524d\u8ba8\u8bba\u7684\u90a3\u4e2a\u641c\u6253\u64a4\u65b9\u6848"), {
827
- terms: ["\u8ba8\u8bba", "\u641c\u6253\u64a4", "\u65b9\u6848"],
828
- strongTerms: ["\u641c\u6253\u64a4"],
829
- weakTerms: ["\u8ba8\u8bba", "\u65b9\u6848"],
830
- embeddingText: "\u641c\u6253\u64a4",
831
- });
832
- });
833
- test("project search prioritizes strong-term Chinese docs for conversational queries over generic plan docs", { concurrency: false }, () => {
834
- const root = createFixture("memory-search-conversational-chinese-ranking");
835
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
836
- id: "memory-search-conversational-chinese-ranking",
837
- memory: {
838
- externalDocPaths: ["docs/"],
839
- embedding: {
840
- provider: "local",
841
- model: "Snowflake/snowflake-arctic-embed-xs",
842
- local: {
843
- modelCacheDir: path.join(root, ".model-cache"),
844
- },
845
- },
846
- },
847
- }, null, 2), "utf-8");
848
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
849
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project memory notes\n", "utf-8");
850
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared truth summary\n", "utf-8");
851
- fs.writeFileSync(path.join(root, "docs", "sdtz-guide.md"), "\u641c\u6253\u64a4\u6a21\u5f0f\u8bf4\u660e\n", "utf-8");
852
- fs.writeFileSync(path.join(root, "docs", "generic-plan-a.md"), "\u5185\u5b58\u4f18\u5316\u65b9\u6848\n", "utf-8");
853
- fs.writeFileSync(path.join(root, "docs", "generic-plan-b.md"), "\u7f51\u7edc\u91cd\u6784\u65b9\u6848\n", "utf-8");
854
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
855
- process.env.CLAW_EMBEDDING_MOCK = "1";
856
- try {
857
- buildMemoryIndex({ cwd: root });
858
- const result = searchMemory({ cwd: root, query: "\u4e4b\u524d\u8ba8\u8bba\u7684\u90a3\u4e2a\u641c\u6253\u64a4\u65b9\u6848", limit: 5 });
859
- assert.equal(path.basename(result.results[0]?.sourcePath ?? ""), "sdtz-guide.md");
860
- }
861
- finally {
862
- if (previousMockEnv === undefined) {
863
- delete process.env.CLAW_EMBEDDING_MOCK;
864
- }
865
- else {
866
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
867
- }
868
- }
869
- });
870
- test("project search demotes index-like docs when a focused Chinese doc matches the same topic", { concurrency: false }, () => {
871
- const root = createFixture("memory-search-index-doc-penalty");
872
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
873
- id: "memory-search-index-doc-penalty",
874
- memory: {
875
- externalDocPaths: ["docs/"],
876
- embedding: {
877
- provider: "local",
878
- model: "Snowflake/snowflake-arctic-embed-xs",
879
- local: {
880
- modelCacheDir: path.join(root, ".model-cache"),
881
- },
882
- },
883
- },
884
- }, null, 2), "utf-8");
885
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
886
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project memory notes\n", "utf-8");
887
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared truth summary\n", "utf-8");
888
- fs.writeFileSync(path.join(root, "docs", "contents.md"), "\u8fd9\u91cc\u5217\u51fa\u641c\u6253\u64a4\u3001\u6b66\u5668\u3001\u9053\u5177\u3001\u7cfb\u7edf\u3001\u6280\u672f\u5b9e\u73b0\u7d22\u5f15\u3002\n", "utf-8");
889
- fs.writeFileSync(path.join(root, "docs", "sdtz-guide.md"), "\u641c\u6253\u64a4\u6a21\u5f0f\u8bf4\u660e\n", "utf-8");
890
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
891
- process.env.CLAW_EMBEDDING_MOCK = "1";
892
- try {
893
- buildMemoryIndex({ cwd: root });
894
- const result = searchMemory({ cwd: root, query: "\u641c\u6253\u64a4", limit: 5 });
895
- assert.equal(path.basename(result.results[0]?.sourcePath ?? ""), "sdtz-guide.md");
896
- }
897
- finally {
898
- if (previousMockEnv === undefined) {
899
- delete process.env.CLAW_EMBEDDING_MOCK;
900
- }
901
- else {
902
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
903
- }
904
- }
905
- });
906
- test("project search keeps recall for multi-term Chinese queries across different markdown docs", () => {
907
- const root = createFixture("memory-search-chinese-multi-term");
908
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
909
- id: "memory-search-chinese-multi-term",
910
- memory: {
911
- externalDocPaths: ["docs/"],
912
- embedding: {
913
- provider: "local",
914
- model: "Snowflake/snowflake-arctic-embed-xs",
915
- local: {
916
- modelCacheDir: path.join(root, ".model-cache"),
917
- },
918
- },
919
- },
920
- }, null, 2), "utf-8");
921
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
922
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "\u9879\u76ee\u68c0\u7d22\u8bb0\u5fc6\n", "utf-8");
923
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "\u5171\u4eab\u4e2d\u6587 truth\n", "utf-8");
924
- fs.writeFileSync(path.join(root, "docs", "sdtz.md"), "\u8fd9\u91cc\u8bb0\u5f55\u641c\u6253\u64a4\u6a21\u5f0f\u7684\u8bf4\u660e\n", "utf-8");
925
- fs.writeFileSync(path.join(root, "docs", "hjb.md"), "\u8fd9\u91cc\u8bb0\u5f55\u54c8\u57fa\u5b9d\u7684\u8bf4\u660e\n", "utf-8");
926
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
927
- process.env.CLAW_EMBEDDING_MOCK = "1";
928
- try {
929
- buildMemoryIndex({ cwd: root });
930
- const firstTerm = searchMemory({ cwd: root, query: "\u641c\u6253\u64a4" });
931
- const secondTerm = searchMemory({ cwd: root, query: "\u54c8\u57fa\u5b9d" });
932
- const multiTerm = searchMemory({ cwd: root, query: "\u641c\u6253\u64a4 \u54c8\u57fa\u5b9d", limit: 5 });
933
- assert.ok(firstTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "sdtz.md"))));
934
- assert.ok(secondTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "hjb.md"))));
935
- assert.ok(multiTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "sdtz.md"))));
936
- assert.ok(multiTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "hjb.md"))));
937
- }
938
- finally {
939
- if (previousMockEnv === undefined) {
940
- delete process.env.CLAW_EMBEDDING_MOCK;
941
- }
942
- else {
943
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
944
- }
945
- }
946
- });
947
- test("project search reranks multi-term Chinese queries to cover distinct strong terms near the top", { concurrency: false }, () => {
948
- const root = createFixture("memory-search-chinese-multi-term-coverage");
949
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
950
- id: "memory-search-chinese-multi-term-coverage",
951
- memory: {
952
- externalDocPaths: ["docs/"],
953
- embedding: {
954
- provider: "local",
955
- model: "Snowflake/snowflake-arctic-embed-xs",
956
- local: {
957
- modelCacheDir: path.join(root, ".model-cache"),
958
- },
959
- },
960
- },
961
- }, null, 2), "utf-8");
962
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
963
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project memory notes\n", "utf-8");
964
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared truth summary\n", "utf-8");
965
- fs.writeFileSync(path.join(root, "docs", "sdtz-guide.md"), "\u641c\u6253\u64a4\u6a21\u5f0f\u8bf4\u660e\n", "utf-8");
966
- fs.writeFileSync(path.join(root, "docs", "hjb-guide.md"), "\u54c8\u57fa\u5b9d\u7cfb\u7edf\u8bf4\u660e\n", "utf-8");
967
- fs.writeFileSync(path.join(root, "docs", "noise.md"), "\u641c\u6253\u64a4\u6280\u672f\u5b9e\u73b0 \u641c\u6253\u64a4\u4ea4\u4e92\u89c4\u5219 \u641c\u6253\u64a4\u754c\u9762\u8bf4\u660e\n", "utf-8");
968
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
969
- process.env.CLAW_EMBEDDING_MOCK = "1";
970
- try {
971
- buildMemoryIndex({ cwd: root });
972
- const result = searchMemory({ cwd: root, query: "\u641c\u6253\u64a4 \u54c8\u57fa\u5b9d", limit: 3 });
973
- const topBasenames = result.results.slice(0, 2).map((item) => path.basename(item.sourcePath));
974
- assert.ok(topBasenames.includes("sdtz-guide.md"));
975
- assert.ok(topBasenames.includes("hjb-guide.md"));
976
- }
977
- finally {
978
- if (previousMockEnv === undefined) {
979
- delete process.env.CLAW_EMBEDDING_MOCK;
980
- }
981
- else {
982
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
983
- }
984
- }
985
- });
986
- test("project search uses substring fallback for short Chinese multi-term queries", { concurrency: false }, () => {
987
- const root = createFixture("memory-search-short-chinese-multi-term");
988
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
989
- id: "memory-search-short-chinese-multi-term",
990
- memory: {
991
- externalDocPaths: ["docs/"],
992
- embedding: {
993
- provider: "local",
994
- model: "Snowflake/snowflake-arctic-embed-xs",
995
- local: {
996
- modelCacheDir: path.join(root, ".model-cache"),
997
- },
998
- },
999
- },
1000
- }, null, 2), "utf-8");
1001
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
1002
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "\u77ed\u8bcd\u4e2d\u6587\u68c0\u7d22\n", "utf-8");
1003
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "\u77ed\u8bcd truth\n", "utf-8");
1004
- fs.writeFileSync(path.join(root, "docs", "sdtz.md"), "\u8fd9\u91cc\u8bb0\u5f55\u641c\u6253\u64a4\u6a21\u5f0f\u7684\u8bf4\u660e\n", "utf-8");
1005
- fs.writeFileSync(path.join(root, "docs", "hjb.md"), "\u8fd9\u91cc\u8bb0\u5f55\u54c8\u57fa\u5b9d\u89d2\u8272\u7684\u8bf4\u660e\n", "utf-8");
1006
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
1007
- process.env.CLAW_EMBEDDING_MOCK = "1";
1008
- try {
1009
- buildMemoryIndex({ cwd: root });
1010
- const shortFirst = searchMemory({ cwd: root, query: "\u641c\u6253" });
1011
- const shortSecond = searchMemory({ cwd: root, query: "\u54c8\u57fa" });
1012
- const multiTerm = searchMemory({ cwd: root, query: "\u641c\u6253 \u54c8\u57fa", limit: 5 });
1013
- assert.ok(shortFirst.results.some((item) => item.sourcePath.endsWith(path.join("docs", "sdtz.md"))));
1014
- assert.ok(shortSecond.results.some((item) => item.sourcePath.endsWith(path.join("docs", "hjb.md"))));
1015
- assert.ok(multiTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "sdtz.md"))));
1016
- assert.ok(multiTerm.results.some((item) => item.sourcePath.endsWith(path.join("docs", "hjb.md"))));
1017
- }
1018
- finally {
1019
- if (previousMockEnv === undefined) {
1020
- delete process.env.CLAW_EMBEDDING_MOCK;
1021
- }
1022
- else {
1023
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
1024
- }
1025
- }
1026
- });
1027
- test("project search can rescue filename-aligned Chinese plan docs through candidate reranking", { concurrency: false }, () => {
1028
- const root = createFixture("memory-search-filename-rescue");
1029
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1030
- id: "memory-search-filename-rescue",
1031
- memory: {
1032
- externalDocPaths: ["docs/"],
1033
- embedding: {
1034
- provider: "local",
1035
- model: "Snowflake/snowflake-arctic-embed-xs",
1036
- local: {
1037
- modelCacheDir: path.join(root, ".model-cache"),
1038
- },
1039
- },
1040
- },
1041
- }, null, 2), "utf-8");
1042
- fs.mkdirSync(path.join(root, "docs", "plans"), { recursive: true });
1043
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project memory notes\n", "utf-8");
1044
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared truth summary\n", "utf-8");
1045
- fs.writeFileSync(path.join(root, "docs", "plans", "\u641c\u6253\u64a4\u65b9\u6848.md"), "\u7cfb\u7edf\u8bbe\u8ba1\u6458\u8981\n", "utf-8");
1046
- fs.writeFileSync(path.join(root, "docs", "sdtz-test-log.md"), "\u641c\u6253\u64a4\u6d4b\u8bd5\u8bb0\u5f55 \u641c\u6253\u64a4\u6d4b\u8bd5\u8bb0\u5f55 \u641c\u6253\u64a4\u6d4b\u8bd5\u8bb0\u5f55\n", "utf-8");
1047
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
1048
- process.env.CLAW_EMBEDDING_MOCK = "1";
1049
- try {
1050
- buildMemoryIndex({ cwd: root });
1051
- const result = searchMemory({ cwd: root, query: "\u4e4b\u524d\u8ba8\u8bba\u7684\u90a3\u4e2a\u641c\u6253\u64a4\u65b9\u6848", limit: 3 });
1052
- assert.equal(path.basename(result.results[0]?.sourcePath ?? ""), "\u641c\u6253\u64a4\u65b9\u6848.md");
1053
- }
1054
- finally {
1055
- if (previousMockEnv === undefined) {
1056
- delete process.env.CLAW_EMBEDDING_MOCK;
1057
- }
1058
- else {
1059
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
1060
- }
1061
- }
1062
- });
1063
- test("project search prioritizes exact Chinese document hits over weaker project-memory matches", { concurrency: false }, () => {
1064
- const root = createFixture("memory-search-chinese-ranking");
1065
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1066
- id: "memory-search-chinese-ranking",
1067
- memory: {
1068
- externalDocPaths: ["docs/"],
1069
- embedding: {
1070
- provider: "local",
1071
- model: "Snowflake/snowflake-arctic-embed-xs",
1072
- local: {
1073
- modelCacheDir: path.join(root, ".model-cache"),
1074
- },
1075
- },
1076
- },
1077
- }, null, 2), "utf-8");
1078
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
1079
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project memory notes\n", "utf-8");
1080
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared truth summary\n", "utf-8");
1081
- fs.writeFileSync(path.join(root, "docs", "sdtz-guide.md"), "\u641c\u6253\u64a4\u6a21\u5f0f\u8bf4\u660e\n", "utf-8");
1082
- fs.writeFileSync(path.join(root, "docs", "hjb-guide.md"), "\u54c8\u57fa\u5b9d\u89d2\u8272\u8bf4\u660e\n", "utf-8");
1083
- fs.writeFileSync(path.join(root, "docs", "noise-a.md"), "\u641c\u6253\u64a4\u76f8\u5173\u6218\u672f\u4e0e\u8d5b\u5b63\u5e73\u8861\u3001\u5c40\u5185\u8def\u7ebf\u3001\u641c\u6253\u64a4\u6280\u5de7\u3001\u591a\u4eba\u641c\u6253\u64a4\u7ecf\u9a8c\u3001\u88c5\u5907\u3001\u64a4\u79bb\u3002\n", "utf-8");
1084
- fs.writeFileSync(path.join(root, "docs", "noise-b.md"), "\u54c8\u57fa\u5b9d\u517b\u6210\u3001\u54c8\u57fa\u5b9d\u642d\u914d\u3001\u54c8\u57fa\u5b9d\u7ecf\u9a8c\u3002\n", "utf-8");
1085
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
1086
- process.env.CLAW_EMBEDDING_MOCK = "1";
1087
- try {
1088
- buildMemoryIndex({ cwd: root });
1089
- const multiTerm = searchMemory({ cwd: root, query: "\u641c\u6253\u64a4 \u54c8\u57fa\u5b9d", limit: 5 });
1090
- const topTwo = multiTerm.results.slice(0, 2).map((item) => path.basename(item.sourcePath)).sort();
1091
- assert.deepEqual(topTwo, ["hjb-guide.md", "sdtz-guide.md"]);
1092
- }
1093
- finally {
1094
- if (previousMockEnv === undefined) {
1095
- delete process.env.CLAW_EMBEDDING_MOCK;
1096
- }
1097
- else {
1098
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
1099
- }
1100
- }
1101
- });
1102
- test("project memory refresh generates local embedding metadata and vector rows for markdown sources", { concurrency: false }, () => {
1103
- const root = createFixture("memory-local-embeddings");
1104
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
1105
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1106
- id: "memory-local-embeddings",
1107
- name: "Memory Local Embeddings",
1108
- maxTasksToKeep: 99,
1109
- externalTruthSkill: null,
1110
- externalAdrSkill: null,
1111
- contextPaths: [],
1112
- memory: {
1113
- externalDocPaths: ["docs/"],
1114
- embedding: {
1115
- provider: "local",
1116
- model: "Snowflake/snowflake-arctic-embed-xs",
1117
- local: {
1118
- modelCacheDir: path.join(root, ".model-cache"),
1119
- },
1120
- },
1121
- },
1122
- gitnexus: {
1123
- enabled: false,
1124
- },
1125
- }, null, 2), "utf-8");
1126
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project alpha memory\n", "utf-8");
1127
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared beta truth\n", "utf-8");
1128
- fs.writeFileSync(path.join(root, "docs", "guide.md"), "gamma markdown doc\n", "utf-8");
1129
- fs.writeFileSync(path.join(root, "docs", "notes.txt"), "should stay unindexed\n", "utf-8");
1130
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
1131
- process.env.CLAW_EMBEDDING_MOCK = "1";
1132
- try {
1133
- const result = buildMemoryIndex({ cwd: root });
1134
- assert.deepEqual(result.embedding, {
1135
- provider: "local",
1136
- model: "Snowflake/snowflake-arctic-embed-xs",
1137
- local: {
1138
- modelCacheDir: path.join(root, ".model-cache"),
1139
- },
1140
- store: {
1141
- vector: {
1142
- enabled: true,
1143
- },
1144
- },
1145
- });
1146
- assert.deepEqual(result.vectorIndex, {
1147
- enabled: true,
1148
- provider: "local",
1149
- model: "Snowflake/snowflake-arctic-embed-xs",
1150
- dimensions: 384,
1151
- chunkCount: 3,
1152
- });
1153
- assert.equal(result.sources.some((item) => item.endsWith(path.join("docs", "notes.txt"))), false);
1154
- const db = new DatabaseSync(result.storePath);
1155
- try {
1156
- const metadata = db
1157
- .prepare("SELECT value FROM index_metadata WHERE key = ?")
1158
- .get("vector_index");
1159
- const vectors = db
1160
- .prepare("SELECT COUNT(*) AS count FROM doc_embeddings")
1161
- .get();
1162
- assert.ok(metadata);
1163
- assert.deepEqual(JSON.parse(metadata.value), result.vectorIndex);
1164
- assert.equal(vectors.count, 3);
1165
- }
1166
- finally {
1167
- db.close();
1168
- }
1169
- }
1170
- finally {
1171
- if (previousMockEnv === undefined) {
1172
- delete process.env.CLAW_EMBEDDING_MOCK;
1173
- }
1174
- else {
1175
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
1176
- }
1177
- }
1178
- });
1179
- test("project memory refresh incrementally reuses unchanged docs and syncs changed or deleted markdown docs", { concurrency: false }, () => {
1180
- const root = createFixture("memory-incremental-refresh");
1181
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
1182
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1183
- id: "memory-incremental-refresh",
1184
- name: "Memory Incremental Refresh",
1185
- maxTasksToKeep: 99,
1186
- externalTruthSkill: null,
1187
- externalAdrSkill: null,
1188
- contextPaths: [],
1189
- memory: {
1190
- externalDocPaths: ["docs/"],
1191
- embedding: {
1192
- provider: "local",
1193
- model: "Snowflake/snowflake-arctic-embed-xs",
1194
- local: {
1195
- modelCacheDir: path.join(root, ".model-cache"),
1196
- },
1197
- },
1198
- },
1199
- gitnexus: {
1200
- enabled: false,
1201
- },
1202
- }, null, 2), "utf-8");
1203
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project alpha memory\n", "utf-8");
1204
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared beta truth\n", "utf-8");
1205
- fs.writeFileSync(path.join(root, "docs", "stable.md"), "stable doc stays the same\n", "utf-8");
1206
- fs.writeFileSync(path.join(root, "docs", "change.md"), "first version paragraph\n\nsecond paragraph\n", "utf-8");
1207
- fs.writeFileSync(path.join(root, "docs", "remove.md"), "remove me later\n", "utf-8");
1208
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
1209
- process.env.CLAW_EMBEDDING_MOCK = "1";
1210
- try {
1211
- const firstIndex = buildMemoryIndex({ cwd: root });
1212
- const firstDb = new DatabaseSync(firstIndex.storePath);
1213
- let stableBefore;
1214
- let changedBefore;
1215
- let removedBefore;
1216
- let stableEmbeddingBefore;
1217
- try {
1218
- stableBefore = firstDb
1219
- .prepare("SELECT id FROM docs WHERE source_path = ?")
1220
- .get(path.join(root, "docs", "stable.md"));
1221
- changedBefore = firstDb
1222
- .prepare("SELECT id FROM docs WHERE source_path = ?")
1223
- .get(path.join(root, "docs", "change.md"));
1224
- removedBefore = firstDb
1225
- .prepare("SELECT id FROM docs WHERE source_path = ?")
1226
- .get(path.join(root, "docs", "remove.md"));
1227
- stableEmbeddingBefore = firstDb
1228
- .prepare("SELECT embedding_json FROM doc_embeddings WHERE doc_id = ? AND chunk_index = 0")
1229
- .get(stableBefore?.id ?? -1);
1230
- }
1231
- finally {
1232
- firstDb.close();
1233
- }
1234
- fs.writeFileSync(path.join(root, "docs", "change.md"), "updated version paragraph only\n", "utf-8");
1235
- fs.unlinkSync(path.join(root, "docs", "remove.md"));
1236
- const secondIndex = buildMemoryIndex({ cwd: root });
1237
- const secondDb = new DatabaseSync(secondIndex.storePath);
1238
- try {
1239
- const stableAfter = secondDb
1240
- .prepare("SELECT id FROM docs WHERE source_path = ?")
1241
- .get(path.join(root, "docs", "stable.md"));
1242
- const changedAfter = secondDb
1243
- .prepare("SELECT id, content FROM docs WHERE source_path = ?")
1244
- .get(path.join(root, "docs", "change.md"));
1245
- const removedAfter = secondDb
1246
- .prepare("SELECT id FROM docs WHERE source_path = ?")
1247
- .get(path.join(root, "docs", "remove.md"));
1248
- const stableEmbeddingAfter = secondDb
1249
- .prepare("SELECT embedding_json FROM doc_embeddings WHERE doc_id = ? AND chunk_index = 0")
1250
- .get(stableAfter?.id ?? -1);
1251
- const changedEmbeddings = secondDb
1252
- .prepare("SELECT COUNT(*) AS count FROM doc_embeddings WHERE doc_id = ?")
1253
- .get(changedAfter?.id ?? -1);
1254
- const removedEmbeddings = secondDb
1255
- .prepare("SELECT COUNT(*) AS count FROM doc_embeddings WHERE source_path = ?")
1256
- .get(path.join(root, "docs", "remove.md"));
1257
- assert.ok(stableBefore);
1258
- assert.ok(changedBefore);
1259
- assert.ok(removedBefore);
1260
- assert.ok(stableAfter);
1261
- assert.ok(changedAfter);
1262
- assert.equal(stableAfter.id, stableBefore.id);
1263
- assert.notEqual(changedAfter.id, changedBefore.id);
1264
- assert.equal(changedAfter.content, "updated version paragraph only\n");
1265
- assert.equal(removedAfter, undefined);
1266
- assert.equal(stableEmbeddingAfter?.embedding_json, stableEmbeddingBefore?.embedding_json);
1267
- assert.equal(changedEmbeddings.count, 1);
1268
- assert.equal(removedEmbeddings.count, 0);
1269
- }
1270
- finally {
1271
- secondDb.close();
1272
- }
1273
- }
1274
- finally {
1275
- if (previousMockEnv === undefined) {
1276
- delete process.env.CLAW_EMBEDDING_MOCK;
1277
- }
1278
- else {
1279
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
1280
- }
1281
- }
1282
- });
1283
- test("project memory refresh backfills vectors for existing docs when embeddings are missing", { concurrency: false }, () => {
1284
- const root = createFixture("memory-backfill-missing-embeddings");
1285
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
1286
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1287
- id: "memory-backfill-missing-embeddings",
1288
- name: "Memory Backfill Missing Embeddings",
1289
- maxTasksToKeep: 99,
1290
- externalTruthSkill: null,
1291
- externalAdrSkill: null,
1292
- contextPaths: [],
1293
- memory: {
1294
- externalDocPaths: ["docs/"],
1295
- embedding: {
1296
- provider: "local",
1297
- model: "Snowflake/snowflake-arctic-embed-xs",
1298
- local: {
1299
- modelCacheDir: path.join(root, ".model-cache"),
1300
- },
1301
- },
1302
- },
1303
- gitnexus: {
1304
- enabled: false,
1305
- },
1306
- }, null, 2), "utf-8");
1307
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project alpha memory\n", "utf-8");
1308
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared beta truth\n", "utf-8");
1309
- fs.writeFileSync(path.join(root, "docs", "guide.md"), "gamma markdown doc\n", "utf-8");
1310
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
1311
- process.env.CLAW_EMBEDDING_MOCK = "1";
1312
- try {
1313
- const firstIndex = buildMemoryIndex({ cwd: root });
1314
- assert.deepEqual(firstIndex.vectorIndex, {
1315
- enabled: true,
1316
- provider: "local",
1317
- model: "Snowflake/snowflake-arctic-embed-xs",
1318
- dimensions: 384,
1319
- chunkCount: 3,
1320
- });
1321
- const db = new DatabaseSync(firstIndex.storePath);
1322
- try {
1323
- db.exec("DELETE FROM doc_embeddings;");
1324
- db.prepare("DELETE FROM index_metadata WHERE key = ?").run("vector_index");
1325
- }
1326
- finally {
1327
- db.close();
1328
- }
1329
- const repairedIndex = buildMemoryIndex({ cwd: root });
1330
- assert.deepEqual(repairedIndex.vectorIndex, {
1331
- enabled: true,
1332
- provider: "local",
1333
- model: "Snowflake/snowflake-arctic-embed-xs",
1334
- dimensions: 384,
1335
- chunkCount: 3,
1336
- });
1337
- assert.equal(repairedIndex.processedFileCount, 0);
1338
- const repairedDb = new DatabaseSync(repairedIndex.storePath);
1339
- try {
1340
- const vectors = repairedDb
1341
- .prepare("SELECT COUNT(*) AS count FROM doc_embeddings")
1342
- .get();
1343
- const metadata = repairedDb
1344
- .prepare("SELECT value FROM index_metadata WHERE key = ?")
1345
- .get("vector_index");
1346
- assert.equal(vectors.count, 3);
1347
- assert.ok(metadata);
1348
- assert.deepEqual(JSON.parse(metadata.value), repairedIndex.vectorIndex);
1349
- }
1350
- finally {
1351
- repairedDb.close();
1352
- }
1353
- }
1354
- finally {
1355
- if (previousMockEnv === undefined) {
1356
- delete process.env.CLAW_EMBEDDING_MOCK;
1357
- }
1358
- else {
1359
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
1360
- }
1361
- }
1362
- });
1363
- test("project memory refresh defaults to processing changed files in 100-file batches", { concurrency: false }, () => {
1364
- const root = createFixture("memory-default-file-batches");
1365
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
1366
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1367
- id: "memory-default-file-batches",
1368
- name: "Memory Default File Batches",
1369
- maxTasksToKeep: 99,
1370
- externalTruthSkill: null,
1371
- externalAdrSkill: null,
1372
- contextPaths: [],
1373
- memory: {
1374
- externalDocPaths: ["docs/"],
1375
- embedding: {
1376
- provider: "local",
1377
- model: "Snowflake/snowflake-arctic-embed-xs",
1378
- local: {
1379
- modelCacheDir: path.join(root, ".model-cache"),
1380
- },
1381
- },
1382
- },
1383
- gitnexus: {
1384
- enabled: false,
1385
- },
1386
- }, null, 2), "utf-8");
1387
- fs.writeFileSync(path.join(root, ".claw", "memory.md"), "project alpha memory\n", "utf-8");
1388
- fs.writeFileSync(path.join(root, ".claw", "truth", "SUMMARY.md"), "shared beta truth\n", "utf-8");
1389
- for (let index = 0; index < 101; index += 1) {
1390
- fs.writeFileSync(path.join(root, "docs", `doc-${index.toString().padStart(3, "0")}.md`), `doc ${index}\n`, "utf-8");
1391
- }
1392
- const previousMockEnv = process.env.CLAW_EMBEDDING_MOCK;
1393
- process.env.CLAW_EMBEDDING_MOCK = "1";
1394
- try {
1395
- const firstIndex = buildMemoryIndex({ cwd: root });
1396
- const secondIndex = buildMemoryIndex({ cwd: root });
1397
- const db = new DatabaseSync(secondIndex.storePath);
1398
- try {
1399
- const docs = db.prepare("SELECT COUNT(*) AS count FROM docs").get();
1400
- assert.equal(firstIndex.indexedCount, 103);
1401
- assert.equal(firstIndex.processedFileCount, 100);
1402
- assert.equal(firstIndex.pendingFileCount, 3);
1403
- assert.equal(secondIndex.processedFileCount, 3);
1404
- assert.equal(secondIndex.pendingFileCount, 0);
1405
- assert.equal(docs.count, 103);
1406
- }
1407
- finally {
1408
- db.close();
1409
- }
1410
- }
1411
- finally {
1412
- if (previousMockEnv === undefined) {
1413
- delete process.env.CLAW_EMBEDDING_MOCK;
1414
- }
1415
- else {
1416
- process.env.CLAW_EMBEDDING_MOCK = previousMockEnv;
1417
- }
1418
- }
1419
- });
1420
- test("workflow guidance uses external writer skills from project config", async () => {
1421
- const root = createFixture("external-writer-skill-guidance");
1422
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1423
- id: "external-writer-skill-guidance",
1424
- name: "External Writer Guidance",
1425
- maxTasksToKeep: 99,
1426
- externalTruthSkill: "external-truth-writer",
1427
- externalAdrSkill: "external-adr-writer",
1428
- contextPaths: [],
1429
- memory: {
1430
- externalDocPaths: [],
1431
- embedding: {
1432
- provider: "local",
1433
- model: "Snowflake/snowflake-arctic-embed-xs",
1434
- local: {
1435
- modelCacheDir: ".claw/models",
1436
- },
1437
- store: {
1438
- vector: {
1439
- enabled: true,
1440
- },
1441
- },
1442
- },
1443
- },
1444
- gitnexus: { enabled: false },
1445
- }, null, 2), "utf-8");
1446
- await writePlan({
1447
- cwd: root,
1448
- taskName: "demo-task",
1449
- title: "Demo task",
1450
- goalText: "Verify external writer routing",
1451
- content: {
1452
- title: "Demo task",
1453
- status: "process.active",
1454
- goal: { text: "Verify external writer routing" },
1455
- tasks: [{ id: 1, title: "Complete the task", status: "pending" }],
1456
- },
1457
- });
1458
- const taskDone = await editPlan({
1459
- cwd: root,
1460
- taskName: "demo-task",
1461
- taskId: 1,
1462
- taskStatus: "done",
1463
- });
1464
- assert.equal(taskDone.workflowGuidance.delegateSubagents?.[0]?.skill, "external-truth-writer");
1465
- assert.equal(taskDone.workflowGuidance.delegateSubagents?.[0]?.model, "gpt-5.4-mini");
1466
- assert.equal(taskDone.workflowGuidance.delegateSubagents?.[0]?.fork_context, false);
1467
- const completed = await editPlan({
1468
- cwd: root,
1469
- taskName: "demo-task",
1470
- planStatus: "end.completed",
1471
- patch: { retrospective: { summary: "Done." } },
1472
- });
1473
- assert.equal(completed.workflowGuidance.delegateSubagents?.[0]?.skill, "external-adr-writer");
1474
- assert.equal(completed.workflowGuidance.delegateSubagents?.[0]?.model, "gpt-5.4-mini");
1475
- assert.equal(completed.workflowGuidance.delegateSubagents?.[0]?.fork_context, false);
1476
- });
1477
- test("truth ingest writes only under .claw/truth", () => {
1478
- const root = createFixture("truth-ingest");
1479
- const result = ingestTruth({
1480
- cwd: root,
1481
- target: "features/test-feature.md",
1482
- content: "# Feature\n\nCanonical truth.\n",
1483
- });
1484
- assert.ok(result.targetPath.startsWith(path.join(root, ".claw", "truth")));
1485
- assert.equal(readTextFile(result.targetPath), "# Feature\n\nCanonical truth.\n");
1486
- });
1487
- test("existing .claw project without project.json still works", async () => {
1488
- const root = createFixture("legacy");
1489
- await writePlan({
1490
- cwd: root,
1491
- taskName: "legacy-task",
1492
- title: "Legacy task",
1493
- goalText: "Remain compatible",
1494
- });
1495
- const result = resolveContext(root, "legacy-task");
1496
- assert.equal(result.project.projectId, path.basename(root));
1497
- assert.equal(result.task?.taskName, "legacy-task");
1498
- });
1499
- test("ensureProjectProtocol rewrites project.json into explicit canonical protocol fields", () => {
1500
- const root = createFixture("project-check-fix");
1501
- fs.writeFileSync(path.join(root, ".claw", "project.json"), JSON.stringify({
1502
- id: "Fix Me",
1503
- name: "Fix Me",
1504
- maxTasksToKeep: 0,
1505
- memory: {
1506
- externalDocPaths: ["docs/", 123],
1507
- embedding: {
1508
- provider: "openai",
1509
- model: "text-embedding-3-small",
1510
- remote: {
1511
- apiKeyEnvVar: "OPENAI_API_KEY",
1512
- },
1513
- },
1514
- },
1515
- }, null, 2), "utf-8");
1516
- const result = ensureProjectProtocol(root);
1517
- const projectConfig = JSON.parse(fs.readFileSync(result.projectJsonPath, "utf-8"));
1518
- assert.equal(result.ok, true);
1519
- assert.equal(result.changed, true);
1520
- assert.ok(result.issueCountBefore > 0);
1521
- assert.equal(projectConfig.id, "fix-me");
1522
- assert.equal(projectConfig.name, "Fix Me");
1523
- assert.equal(projectConfig.maxTasksToKeep, 99);
1524
- assert.equal(projectConfig.externalTruthSkill, null);
1525
- assert.equal(projectConfig.externalAdrSkill, null);
1526
- assert.deepEqual(projectConfig.contextPaths, []);
1527
- assert.deepEqual(projectConfig.memory.externalDocPaths, ["docs/"]);
1528
- assert.deepEqual(projectConfig.memory.embedding, {
1529
- provider: "openai",
1530
- model: "text-embedding-3-small",
1531
- remote: {
1532
- apiKeyEnvVar: "OPENAI_API_KEY",
1533
- },
1534
- store: {
1535
- vector: {
1536
- enabled: true,
1537
- },
1538
- },
1539
- });
1540
- assert.equal(projectConfig.gitnexus.enabled, false);
1541
- });
1542
- test("enforceTaskRetention archives completed task and prunes archive by updatedAt", async () => {
1543
- const root = createFixture("task-retention");
1544
- initProject({
1545
- cwd: root,
1546
- projectName: "Retention Project",
1547
- maxTasksToKeep: 1,
1548
- force: true,
1549
- });
1550
- await writePlan({
1551
- cwd: root,
1552
- taskName: "older-task",
1553
- title: "Older task",
1554
- goalText: "Archive older task",
1555
- content: {
1556
- title: "Older task",
1557
- status: "end.completed",
1558
- goal: { text: "Archive older task" },
1559
- tasks: [],
1560
- retrospective: { summary: "Older complete." },
1561
- },
1562
- });
1563
- await writePlan({
1564
- cwd: root,
1565
- taskName: "newer-task",
1566
- title: "Newer task",
1567
- goalText: "Archive newer task",
1568
- content: {
1569
- title: "Newer task",
1570
- status: "end.completed",
1571
- goal: { text: "Archive newer task" },
1572
- tasks: [],
1573
- retrospective: { summary: "Newer complete." },
1574
- },
1575
- });
1576
- const olderMetaPath = path.join(root, ".claw", "tasks", "older-task", "meta.json");
1577
- const olderMeta = JSON.parse(fs.readFileSync(olderMetaPath, "utf-8"));
1578
- olderMeta.updatedAt = "2026-01-01T00:00:00.000Z";
1579
- fs.writeFileSync(olderMetaPath, `${JSON.stringify(olderMeta, null, 2)}\n`, "utf-8");
1580
- const newerMetaPath = path.join(root, ".claw", "tasks", "newer-task", "meta.json");
1581
- const newerMeta = JSON.parse(fs.readFileSync(newerMetaPath, "utf-8"));
1582
- newerMeta.updatedAt = "2026-02-01T00:00:00.000Z";
1583
- fs.writeFileSync(newerMetaPath, `${JSON.stringify(newerMeta, null, 2)}\n`, "utf-8");
1584
- const project = resolveContext(root).project;
1585
- const first = enforceTaskRetention(project, "older-task");
1586
- assert.equal(first.archivedCurrentTask?.taskName, "older-task");
1587
- assert.equal(fs.existsSync(path.join(root, ".claw", "tasks", "older-task")), false);
1588
- assert.equal(first.prunedArchivedTasks[0]?.taskName, "older-task");
1589
- assert.equal(fs.existsSync(first.archivedCurrentTask?.archivedTaskDir ?? ""), false);
1590
- const second = enforceTaskRetention(project, "newer-task");
1591
- assert.equal(second.archivedCurrentTask, undefined);
1592
- assert.deepEqual(second.prunedArchivedTasks, []);
1593
- assert.equal(fs.existsSync(path.join(root, ".claw", "archive", "tasks", "newer-task")), true);
1594
- });
1595
- test("enforceTaskRetention also archives legacy completed tasks still left in active tasks", async () => {
1596
- const root = createFixture("task-retention-legacy-completed");
1597
- initProject({
1598
- cwd: root,
1599
- projectName: "Retention Sweep Project",
1600
- maxTasksToKeep: 99,
1601
- force: true,
1602
- });
1603
- await writePlan({
1604
- cwd: root,
1605
- taskName: "legacy-completed",
1606
- title: "Legacy completed",
1607
- goalText: "Archive legacy completed task",
1608
- content: {
1609
- title: "Legacy completed",
1610
- status: "end.completed",
1611
- goal: { text: "Archive legacy completed task" },
1612
- tasks: [],
1613
- retrospective: { summary: "Legacy complete." },
1614
- },
1615
- });
1616
- await writePlan({
1617
- cwd: root,
1618
- taskName: "current-completed",
1619
- title: "Current completed",
1620
- goalText: "Archive current completed task",
1621
- content: {
1622
- title: "Current completed",
1623
- status: "end.completed",
1624
- goal: { text: "Archive current completed task" },
1625
- tasks: [],
1626
- retrospective: { summary: "Current complete." },
1627
- },
1628
- });
1629
- const project = resolveContext(root).project;
1630
- const result = enforceTaskRetention(project, "current-completed");
1631
- assert.equal(result.archivedCurrentTask?.taskName, "current-completed");
1632
- assert.equal(fs.existsSync(path.join(root, ".claw", "tasks", "legacy-completed")), false);
1633
- assert.equal(fs.existsSync(path.join(root, ".claw", "tasks", "current-completed")), false);
1634
- assert.equal(fs.existsSync(path.join(root, ".claw", "archive", "tasks", "legacy-completed")), true);
1635
- assert.equal(fs.existsSync(path.join(root, ".claw", "archive", "tasks", "current-completed")), true);
1636
- });
1637
- test("concurrent plan writes fail fast with PLAN_WRITE_CONFLICT", async () => {
1638
- const root = createFixture("plan-write-conflict");
1639
- await writePlan({
1640
- cwd: root,
1641
- taskName: "demo-task",
1642
- title: "Demo task",
1643
- goalText: "Protect canonical writes",
1644
- content: {
1645
- title: "Demo task",
1646
- status: "process.active",
1647
- goal: { text: "Protect canonical writes" },
1648
- tasks: [{ id: 1, title: "Only task", status: "pending" }],
1649
- },
1650
- });
1651
- const taskDir = path.join(root, ".claw", "tasks", "demo-task");
1652
- const planPath = path.join(taskDir, "plan.json");
1653
- fs.writeFileSync(`${planPath}.lock`, "", "utf-8");
1654
- await assert.rejects(editPlan({
1655
- cwd: root,
1656
- taskName: "demo-task",
1657
- taskId: 1,
1658
- taskStatus: "done",
1659
- }), (error) => {
1660
- const candidate = error;
1661
- return (typeof candidate === "object" &&
1662
- candidate !== null &&
1663
- candidate.code === "PLAN_WRITE_CONFLICT" &&
1664
- String(candidate.message).includes("Concurrent write detected"));
1665
- });
1666
- fs.unlinkSync(`${planPath}.lock`);
1667
- });
1668
- test("ensureUtf8Bom prefixes markdown text exactly once", () => {
1669
- const original = "# ADR\n\n中文正文。\n";
1670
- const once = ensureUtf8Bom(original);
1671
- const twice = ensureUtf8Bom(once);
1672
- assert.equal(once.charCodeAt(0), 0xfeff);
1673
- assert.equal(twice, once);
1674
- });
1675
- test("truth ingest writes markdown with UTF-8 BOM for Windows PowerShell compatibility", () => {
1676
- const root = createFixture("truth-bom");
1677
- const result = ingestTruth({
1678
- cwd: root,
1679
- target: "adr/test.md",
1680
- content: "# 标题\n\n中文正文。\n",
1681
- });
1682
- const raw = fs.readFileSync(result.targetPath);
1683
- assert.equal(raw[0], 0xef);
1684
- assert.equal(raw[1], 0xbb);
1685
- assert.equal(raw[2], 0xbf);
1686
- });
1687
- //# sourceMappingURL=core.test.js.map