metame-cli 1.4.34 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +136 -94
  2. package/index.js +312 -57
  3. package/package.json +8 -4
  4. package/scripts/agent-layer.js +320 -0
  5. package/scripts/daemon-admin-commands.js +328 -28
  6. package/scripts/daemon-agent-commands.js +145 -6
  7. package/scripts/daemon-agent-tools.js +163 -7
  8. package/scripts/daemon-bridges.js +110 -20
  9. package/scripts/daemon-checkpoints.js +36 -7
  10. package/scripts/daemon-claude-engine.js +849 -358
  11. package/scripts/daemon-command-router.js +31 -10
  12. package/scripts/daemon-default.yaml +28 -4
  13. package/scripts/daemon-engine-runtime.js +328 -0
  14. package/scripts/daemon-exec-commands.js +15 -7
  15. package/scripts/daemon-notify.js +37 -1
  16. package/scripts/daemon-ops-commands.js +8 -6
  17. package/scripts/daemon-runtime-lifecycle.js +129 -5
  18. package/scripts/daemon-session-commands.js +60 -25
  19. package/scripts/daemon-session-store.js +121 -13
  20. package/scripts/daemon-task-scheduler.js +129 -49
  21. package/scripts/daemon-user-acl.js +35 -9
  22. package/scripts/daemon.js +268 -33
  23. package/scripts/distill.js +327 -18
  24. package/scripts/docs/agent-guide.md +12 -0
  25. package/scripts/docs/maintenance-manual.md +155 -0
  26. package/scripts/docs/pointer-map.md +110 -0
  27. package/scripts/feishu-adapter.js +42 -13
  28. package/scripts/hooks/stop-session-capture.js +243 -0
  29. package/scripts/memory-extract.js +105 -6
  30. package/scripts/memory-nightly-reflect.js +199 -11
  31. package/scripts/memory.js +134 -3
  32. package/scripts/mentor-engine.js +405 -0
  33. package/scripts/platform.js +24 -0
  34. package/scripts/providers.js +182 -22
  35. package/scripts/schema.js +12 -0
  36. package/scripts/session-analytics.js +245 -12
  37. package/scripts/skill-changelog.js +245 -0
  38. package/scripts/skill-evolution.js +288 -5
  39. package/scripts/telegram-adapter.js +12 -8
  40. package/scripts/usage-classifier.js +1 -1
  41. package/scripts/daemon-admin-commands.test.js +0 -333
  42. package/scripts/daemon-task-envelope.test.js +0 -59
  43. package/scripts/daemon-task-scheduler.test.js +0 -106
  44. package/scripts/reliability-core.test.js +0 -280
  45. package/scripts/skill-evolution.test.js +0 -113
  46. package/scripts/task-board.test.js +0 -83
  47. package/scripts/test_daemon.js +0 -1407
  48. package/scripts/utils.test.js +0 -192
@@ -1,280 +0,0 @@
1
- const test = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { execFileSync, spawn } = require('child_process');
7
-
8
- const ROOT = path.resolve(__dirname, '..');
9
-
10
- function mkHome(prefix = 'metame-reliability-') {
11
- const home = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
12
- fs.mkdirSync(path.join(home, '.metame'), { recursive: true });
13
- return home;
14
- }
15
-
16
- function homeEnv(home) {
17
- // On Windows, os.homedir() reads USERPROFILE, not HOME
18
- return process.platform === 'win32'
19
- ? { HOME: home, USERPROFILE: home }
20
- : { HOME: home };
21
- }
22
-
23
- function runNode(home, code, extraEnv = {}) {
24
- return execFileSync(process.execPath, ['-e', code], {
25
- cwd: ROOT,
26
- env: { ...process.env, ...homeEnv(home), ...extraEnv },
27
- encoding: 'utf8',
28
- timeout: 30000,
29
- });
30
- }
31
-
32
- function installFakeClaude(home, body) {
33
- const bin = path.join(home, 'bin');
34
- fs.mkdirSync(bin, { recursive: true });
35
- if (process.platform === 'win32') {
36
- const cli = path.join(bin, 'claude.cmd');
37
- fs.writeFileSync(cli, `@echo off\n${body}\n`, 'utf8');
38
- return { ...homeEnv(home), PATH: `${bin};${process.env.PATH}` };
39
- }
40
- const cli = path.join(bin, 'claude');
41
- fs.writeFileSync(cli, `#!/bin/sh\n${body}\n`, 'utf8');
42
- fs.chmodSync(cli, 0o755);
43
- return { ...homeEnv(home), PATH: `${bin}:${process.env.PATH}` };
44
- }
45
-
46
- function sendSignal(home, prompt, extraEnv = {}) {
47
- return new Promise((resolve, reject) => {
48
- const child = spawn(process.execPath, [path.join(ROOT, 'scripts', 'signal-capture.js')], {
49
- cwd: ROOT,
50
- env: { ...process.env, ...homeEnv(home), ...extraEnv },
51
- stdio: ['pipe', 'ignore', 'ignore'],
52
- });
53
- const timer = setTimeout(() => {
54
- try { child.kill('SIGKILL'); } catch {}
55
- reject(new Error('signal-capture timed out'));
56
- }, 10000);
57
- child.on('error', reject);
58
- child.on('close', (code) => {
59
- clearTimeout(timer);
60
- if (code === 0) resolve();
61
- else reject(new Error(`signal-capture exited with ${code}`));
62
- });
63
- child.stdin.end(JSON.stringify({
64
- prompt,
65
- session_id: `s-${Math.random().toString(36).slice(2, 10)}`,
66
- cwd: '/tmp',
67
- }));
68
- });
69
- }
70
-
71
- test('signal-capture preserves all entries under concurrent writes', async () => {
72
- const home = mkHome();
73
- const count = 40;
74
- await Promise.all(
75
- Array.from({ length: count }, (_, i) => sendSignal(home, `请记住以后规则${i}`))
76
- );
77
-
78
- const buffer = path.join(home, '.metame', 'raw_signals.jsonl');
79
- const lines = fs.readFileSync(buffer, 'utf8').split('\n').filter(Boolean);
80
- assert.equal(lines.length, count);
81
- const prompts = new Set(lines.map((l) => JSON.parse(l).prompt));
82
- assert.equal(prompts.size, count);
83
- });
84
-
85
- test('distill keeps raw_signals when model returns malformed output', () => {
86
- const home = mkHome();
87
- const env = installFakeClaude(home, 'echo "MALFORMED_OUTPUT"');
88
- const buffer = path.join(home, '.metame', 'raw_signals.jsonl');
89
-
90
- fs.writeFileSync(
91
- buffer,
92
- JSON.stringify({
93
- ts: new Date().toISOString(),
94
- prompt: '请记住以后都用中文并保持简洁',
95
- confidence: 'high',
96
- type: 'directive',
97
- session: 'sess-1',
98
- cwd: '/tmp',
99
- }) + '\n',
100
- 'utf8'
101
- );
102
-
103
- runNode(home, `
104
- const { distill } = require('./scripts/distill');
105
- (async () => {
106
- const r = await distill();
107
- console.log(JSON.stringify(r));
108
- })().then(() => process.exit(0)).catch(() => process.exit(1));
109
- `, env);
110
-
111
- const lines = fs.readFileSync(buffer, 'utf8').split('\n').filter(Boolean);
112
- assert.equal(lines.length, 1);
113
- assert.match(JSON.parse(lines[0]).prompt, /以后都用中文/);
114
- });
115
-
116
- test('memory-extract does not mark session extracted when extraction fails', () => {
117
- const home = mkHome();
118
- const env = installFakeClaude(home, 'echo "downstream failure" 1>&2; exit 1');
119
- const projDir = path.join(home, '.claude', 'projects', 'demo');
120
- fs.mkdirSync(projDir, { recursive: true });
121
- const sessionId = 'session-retry-me';
122
- const sessionPath = path.join(projDir, `${sessionId}.jsonl`);
123
-
124
- const rows = [];
125
- for (let i = 0; i < 24; i++) {
126
- rows.push(JSON.stringify({
127
- type: 'user',
128
- timestamp: new Date(Date.now() + i * 1000).toISOString(),
129
- cwd: '/tmp/demo',
130
- message: { content: `这是一条用于memory extract回归测试的用户消息 ${i},需要保留重试能力。` },
131
- }));
132
- rows.push(JSON.stringify({
133
- type: 'assistant',
134
- timestamp: new Date(Date.now() + i * 1000 + 500).toISOString(),
135
- message: { content: [{ type: 'text', text: 'ack' }] },
136
- }));
137
- }
138
- fs.writeFileSync(sessionPath, rows.join('\n') + '\n', 'utf8');
139
-
140
- runNode(home, `
141
- const me = require('./scripts/memory-extract');
142
- (async () => {
143
- await me.run();
144
- const sa = require('./scripts/session-analytics');
145
- const remain = sa.findAllUnextractedSessions(50).map(s => s.session_id);
146
- console.log(JSON.stringify(remain));
147
- })().then(() => process.exit(0)).catch(() => process.exit(1));
148
- `, env);
149
-
150
- const remain = JSON.parse(
151
- runNode(home, `
152
- const sa = require('./scripts/session-analytics');
153
- console.log(JSON.stringify(sa.findAllUnextractedSessions(50).map(s => s.session_id)));
154
- `).trim()
155
- );
156
- assert.ok(remain.includes(sessionId));
157
- });
158
-
159
- test('skill-evolution keeps signals when haiku output is malformed', () => {
160
- const home = mkHome();
161
- const env = installFakeClaude(home, 'echo "NOT_JSON_BLOCK"');
162
- const skillDir = path.join(home, '.claude', 'skills', 'demo-skill');
163
- fs.mkdirSync(skillDir, { recursive: true });
164
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '# Demo Skill\n\nA sample skill.', 'utf8');
165
-
166
- const sigFile = path.join(home, '.metame', 'skill_signals.jsonl');
167
- const signals = [
168
- { ts: new Date().toISOString(), prompt: '请求1', outcome: 'error', skills_invoked: ['demo-skill'], has_tool_failure: true, error: 'x' },
169
- { ts: new Date().toISOString(), prompt: '请求2', outcome: 'error', skills_invoked: ['demo-skill'], has_tool_failure: true, error: 'y' },
170
- { ts: new Date().toISOString(), prompt: '请求3', outcome: 'error', skills_invoked: ['demo-skill'], has_tool_failure: true, error: 'z' },
171
- ];
172
- fs.writeFileSync(sigFile, signals.map(s => JSON.stringify(s)).join('\n') + '\n', 'utf8');
173
-
174
- runNode(home, `
175
- const se = require('./scripts/skill-evolution');
176
- (async () => {
177
- await se.distillSkills();
178
- })().then(() => process.exit(0)).catch(() => process.exit(1));
179
- `, env);
180
-
181
- const lines = fs.readFileSync(sigFile, 'utf8').split('\n').filter(Boolean);
182
- assert.equal(lines.length, 3);
183
- });
184
-
185
- test('signal-capture: overflow entries are drained and merged on next lock acquisition', async () => {
186
- const home = mkHome();
187
- const bufferFile = path.join(home, '.metame', 'raw_signals.jsonl');
188
- const overflowFile = path.join(home, '.metame', 'raw_signals.overflow.jsonl');
189
-
190
- const makeEntry = (i) => JSON.stringify({
191
- ts: new Date().toISOString(), prompt: `以后记住规则${i}`, confidence: 'high',
192
- type: 'directive', session: null, cwd: '/tmp',
193
- });
194
-
195
- // Pre-populate main buffer (3 entries) and overflow (2 entries)
196
- fs.writeFileSync(bufferFile, [0, 1, 2].map(makeEntry).join('\n') + '\n', 'utf8');
197
- fs.writeFileSync(overflowFile, [100, 101].map(makeEntry).join('\n') + '\n', 'utf8');
198
-
199
- // One normal signal — should drain overflow inside the lock
200
- await sendSignal(home, '以后回复请保持简洁风格');
201
-
202
- const lines = fs.readFileSync(bufferFile, 'utf8').split('\n').filter(Boolean);
203
- // 3 existing + 2 overflow + 1 new = 6
204
- assert.equal(lines.length, 6);
205
- assert.equal(fs.existsSync(overflowFile), false, 'overflow file should be removed after drain');
206
- });
207
-
208
- test('signal-capture: overflow drain respects MAX_BUFFER_LINES cap', async () => {
209
- const home = mkHome();
210
- const MAX = 300;
211
- const bufferFile = path.join(home, '.metame', 'raw_signals.jsonl');
212
- const overflowFile = path.join(home, '.metame', 'raw_signals.overflow.jsonl');
213
-
214
- const makeEntry = (i) => JSON.stringify({
215
- ts: new Date().toISOString(), prompt: `以后偏好配置${i}`, confidence: 'normal',
216
- type: 'implicit', session: null, cwd: '/tmp',
217
- });
218
-
219
- // Fill main buffer to 297, overflow to 5 — combined 302 + 1 new → must cap to 300
220
- fs.writeFileSync(bufferFile, Array.from({ length: 297 }, (_, i) => makeEntry(i)).join('\n') + '\n', 'utf8');
221
- fs.writeFileSync(overflowFile, Array.from({ length: 5 }, (_, i) => makeEntry(i + 1000)).join('\n') + '\n', 'utf8');
222
-
223
- await sendSignal(home, '以后总是用英文写注释');
224
-
225
- const lines = fs.readFileSync(bufferFile, 'utf8').split('\n').filter(Boolean);
226
- assert.ok(lines.length <= MAX, `buffer must not exceed MAX_BUFFER_LINES (got ${lines.length})`);
227
- assert.equal(lines.length, MAX);
228
- assert.equal(fs.existsSync(overflowFile), false, 'overflow file should be removed after drain');
229
- });
230
-
231
- test('skill-evolution: overflow entries are drained and merged on next appendSkillSignal', () => {
232
- const home = mkHome();
233
- const sigFile = path.join(home, '.metame', 'skill_signals.jsonl');
234
- const overflowFile = path.join(home, '.metame', 'skill_signals.overflow.jsonl');
235
-
236
- const makeSignal = (i) => JSON.stringify({
237
- ts: new Date().toISOString(), prompt: `prompt-${i}`, outcome: 'success',
238
- skills_invoked: ['demo-skill'], has_tool_failure: false, error: null,
239
- output_excerpt: '', tools_used: [], files_modified: [], cwd: '/tmp',
240
- });
241
-
242
- fs.writeFileSync(sigFile, [0, 1, 2].map(makeSignal).join('\n') + '\n', 'utf8');
243
- fs.writeFileSync(overflowFile, [100, 101].map(makeSignal).join('\n') + '\n', 'utf8');
244
-
245
- runNode(home, `
246
- const se = require('./scripts/skill-evolution');
247
- const sig = {
248
- ts: new Date().toISOString(), prompt: 'new-signal', outcome: 'success',
249
- skills_invoked: ['demo-skill'], has_tool_failure: false, error: null,
250
- output_excerpt: '', tools_used: [], files_modified: [], cwd: '/tmp',
251
- };
252
- se.appendSkillSignal(sig);
253
- process.exit(0);
254
- `);
255
-
256
- const lines = fs.readFileSync(sigFile, 'utf8').split('\n').filter(Boolean);
257
- // 3 existing + 2 overflow + 1 new = 6
258
- assert.equal(lines.length, 6);
259
- assert.equal(fs.existsSync(overflowFile), false, 'overflow file should be removed after drain');
260
- });
261
-
262
- test('writeBrainFileSafe throws when lock cannot be acquired', () => {
263
- const home = mkHome();
264
- fs.writeFileSync(path.join(home, '.metame', 'brain.lock'), '99999', 'utf8');
265
-
266
- const out = runNode(home, `
267
- const { writeBrainFileSafe } = require('./scripts/utils');
268
- (async () => {
269
- try {
270
- await writeBrainFileSafe('x: 1\\n', process.env.HOME + '/profile.yaml');
271
- console.log('WROTE');
272
- } catch (e) {
273
- console.log('THREW');
274
- }
275
- })().then(() => process.exit(0)).catch(() => process.exit(1));
276
- `).trim();
277
-
278
- assert.equal(out, 'THREW');
279
- assert.equal(fs.existsSync(path.join(home, 'profile.yaml')), false);
280
- });
@@ -1,113 +0,0 @@
1
- const test = require('node:test');
2
- const assert = require('node:assert/strict');
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
- const { execFileSync } = require('child_process');
7
- const yaml = require('js-yaml');
8
-
9
- const ROOT = path.resolve(__dirname, '..');
10
-
11
- function mkHome() {
12
- return fs.mkdtempSync(path.join(os.tmpdir(), 'metame-skill-evo-'));
13
- }
14
-
15
- function homeEnv(home) {
16
- return process.platform === 'win32'
17
- ? { HOME: home, USERPROFILE: home }
18
- : { HOME: home };
19
- }
20
-
21
- function runWithHome(home, code) {
22
- return execFileSync(process.execPath, ['-e', code], {
23
- cwd: ROOT,
24
- env: { ...process.env, ...homeEnv(home) },
25
- encoding: 'utf8',
26
- });
27
- }
28
-
29
- test('captures missing-skill failures into skill_gap queue', () => {
30
- const home = mkHome();
31
- runWithHome(home, `
32
- const se = require('./scripts/skill-evolution');
33
- const s = se.extractSkillSignal('帮我做封面图', 'Error: skill not found: nano-banana', null, [], process.cwd(), []);
34
- se.appendSkillSignal(s);
35
- se.checkHotEvolution(s);
36
- `);
37
-
38
- const queuePath = path.join(home, '.metame', 'evolution_queue.yaml');
39
- const queue = yaml.load(fs.readFileSync(queuePath, 'utf8'));
40
- assert.equal(Array.isArray(queue.items), true);
41
- assert.equal(queue.items.length, 1);
42
- assert.equal(queue.items[0].type, 'skill_gap');
43
- assert.equal(queue.items[0].status, 'pending');
44
- assert.ok(queue.items[0].id);
45
- });
46
-
47
- test('resolves queue item by id', () => {
48
- const home = mkHome();
49
- runWithHome(home, `
50
- const se = require('./scripts/skill-evolution');
51
- const s = se.extractSkillSignal('帮我做封面图', 'Error: skill not found: nano-banana', null, [], process.cwd(), []);
52
- se.appendSkillSignal(s);
53
- se.checkHotEvolution(s);
54
- const items = se.listQueueItems({ status: 'pending', limit: 5 });
55
- if (!items[0]) throw new Error('no pending queue item');
56
- const ok = se.resolveQueueItemById(items[0].id, 'installed');
57
- if (!ok) throw new Error('resolveQueueItemById returned false');
58
- `);
59
-
60
- const queuePath = path.join(home, '.metame', 'evolution_queue.yaml');
61
- const queue = yaml.load(fs.readFileSync(queuePath, 'utf8'));
62
- assert.equal(queue.items.length, 1);
63
- assert.equal(queue.items[0].status, 'installed');
64
- });
65
-
66
- test('smartStitch preserves sections after evolution block', () => {
67
- const home = mkHome();
68
- runWithHome(home, `
69
- const fs = require('fs');
70
- const path = require('path');
71
- const se = require('./scripts/skill-evolution');
72
- const dir = path.join(process.env.HOME, 'sample-skill');
73
- fs.mkdirSync(dir, { recursive: true });
74
- fs.writeFileSync(path.join(dir, 'SKILL.md'), '# Demo\\n\\nBody\\n\\n## User-Learned Best Practices & Constraints\\nOld\\n\\n## KeepMe\\nKeep this section\\n');
75
- fs.writeFileSync(path.join(dir, 'evolution.json'), JSON.stringify({ preferences: ['prefer concise output'] }, null, 2));
76
- se.smartStitch(dir);
77
- `);
78
-
79
- const content = fs.readFileSync(path.join(home, 'sample-skill', 'SKILL.md'), 'utf8');
80
- assert.match(content, /METAME-EVOLUTION:START/);
81
- assert.match(content, /prefer concise output/);
82
- assert.match(content, /## KeepMe/);
83
- assert.match(content, /Keep this section/);
84
- });
85
-
86
- test('trackInsightOutcome updates only matched insights', () => {
87
- const home = mkHome();
88
- runWithHome(home, `
89
- const fs = require('fs');
90
- const path = require('path');
91
- const se = require('./scripts/skill-evolution');
92
- const dir = path.join(process.env.HOME, '.claude', 'skills', 'demo-skill');
93
- fs.mkdirSync(dir, { recursive: true });
94
- fs.writeFileSync(path.join(dir, 'SKILL.md'), '# Demo');
95
- fs.writeFileSync(path.join(dir, 'evolution.json'), JSON.stringify({
96
- preferences: ['alpha_mode'],
97
- fixes: ['beta_mode']
98
- }, null, 2));
99
- se.trackInsightOutcome(dir, true, {
100
- prompt: 'please use alpha_mode',
101
- error: '',
102
- output_excerpt: '',
103
- tools_used: [],
104
- files_modified: []
105
- });
106
- `);
107
-
108
- const evo = JSON.parse(fs.readFileSync(path.join(home, '.claude', 'skills', 'demo-skill', 'evolution.json'), 'utf8'));
109
- assert.equal(typeof evo.insights_stats, 'object');
110
- assert.ok(evo.insights_stats.alpha_mode);
111
- assert.equal(evo.insights_stats.alpha_mode.success_count, 1);
112
- assert.equal(evo.insights_stats.beta_mode, undefined);
113
- });
@@ -1,83 +0,0 @@
1
- 'use strict';
2
-
3
- const test = require('node:test');
4
- const assert = require('node:assert/strict');
5
- const fs = require('fs');
6
- const os = require('os');
7
- const path = require('path');
8
-
9
- const { createTaskBoard } = require('./task-board');
10
-
11
- function newTmpDbPath() {
12
- const rand = Math.random().toString(36).slice(2, 8);
13
- return path.join(os.tmpdir(), `metame-task-board-${Date.now()}-${rand}.db`);
14
- }
15
-
16
- test('task board upsert/get/list/status flow', () => {
17
- const dbPath = newTmpDbPath();
18
- const board = createTaskBoard({ dbPath });
19
- const taskId = 't_test_001';
20
-
21
- const up = board.upsertTask({
22
- task_id: taskId,
23
- scope_id: 'scope_alpha',
24
- from_agent: 'assistant',
25
- to_agent: 'coder',
26
- goal: 'run tests',
27
- task_kind: 'team',
28
- participants: ['assistant', 'coder'],
29
- definition_of_done: ['all tests pass'],
30
- inputs: { cwd: '/tmp/project' },
31
- priority: 'high',
32
- status: 'queued',
33
- created_at: '2026-02-25T00:00:00.000Z',
34
- updated_at: '2026-02-25T00:00:00.000Z',
35
- });
36
- assert.equal(up.ok, true);
37
-
38
- const got = board.getTask(taskId);
39
- assert.ok(got);
40
- assert.equal(got.task_kind, 'team');
41
- assert.equal(got.scope_id, 'scope_alpha');
42
- assert.equal(got.goal, 'run tests');
43
- assert.deepEqual(got.definition_of_done, ['all tests pass']);
44
- assert.deepEqual(got.participants, ['assistant', 'coder']);
45
-
46
- const ev = board.appendTaskEvent(taskId, 'dispatch_enqueued', 'assistant', { x: 1 });
47
- assert.equal(ev.ok, true);
48
- const events = board.listTaskEvents(taskId, 5);
49
- assert.equal(events.length, 1);
50
- assert.equal(events[0].event_type, 'dispatch_enqueued');
51
-
52
- const st = board.markTaskStatus(taskId, 'done', { summary: 'ok', artifacts: ['/tmp/log.txt'] });
53
- assert.equal(st.ok, true);
54
- const done = board.getTask(taskId);
55
- assert.equal(done.status, 'done');
56
- assert.equal(done.summary, 'ok');
57
- assert.deepEqual(done.artifacts, ['/tmp/log.txt']);
58
-
59
- const recent = board.listRecentTasks(5, null, 'team');
60
- assert.ok(recent.some(t => t.task_id === taskId));
61
-
62
- const up2 = board.upsertTask({
63
- task_id: 't_test_002',
64
- scope_id: 'scope_alpha',
65
- from_agent: 'coder',
66
- to_agent: 'reviewer',
67
- goal: 'review test results',
68
- task_kind: 'team',
69
- participants: ['coder', 'reviewer'],
70
- status: 'queued',
71
- priority: 'normal',
72
- created_at: '2026-02-25T00:01:00.000Z',
73
- updated_at: '2026-02-25T00:01:00.000Z',
74
- });
75
- assert.equal(up2.ok, true);
76
- const scoped = board.listScopeTasks('scope_alpha', 10);
77
- assert.equal(scoped.length >= 2, true);
78
- const participants = board.listScopeParticipants('scope_alpha');
79
- assert.deepEqual(participants.sort(), ['assistant', 'coder', 'reviewer'].sort());
80
-
81
- board.close();
82
- try { fs.unlinkSync(dbPath); } catch {}
83
- });