chapterhouse 0.3.13 → 0.3.15

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 (60) hide show
  1. package/README.md +2 -69
  2. package/dist/api/server.js +8 -155
  3. package/dist/api/server.test.js +1 -1
  4. package/dist/cli.js +0 -30
  5. package/dist/config.js +11 -3
  6. package/dist/copilot/agent-event-bus.js +41 -0
  7. package/dist/copilot/agent-event-bus.test.js +23 -0
  8. package/dist/copilot/agents.js +4 -59
  9. package/dist/copilot/orchestrator.js +20 -39
  10. package/dist/copilot/orchestrator.test.js +73 -158
  11. package/dist/copilot/system-message.js +7 -0
  12. package/dist/copilot/task-event-log.js +5 -5
  13. package/dist/copilot/task-event-log.test.js +68 -142
  14. package/dist/copilot/tools.js +72 -132
  15. package/dist/daemon.js +6 -22
  16. package/dist/store/db.js +2 -50
  17. package/dist/store/db.test.js +0 -45
  18. package/dist/wiki/fs.js +5 -0
  19. package/dist/wiki/index-manager.js +92 -17
  20. package/dist/wiki/index-manager.test.js +19 -0
  21. package/dist/wiki/migrate-topics.js +132 -0
  22. package/dist/wiki/migrate-topics.test.js +57 -0
  23. package/dist/wiki/topic-structure.js +167 -0
  24. package/dist/wiki/topic-structure.test.js +74 -0
  25. package/package.json +1 -3
  26. package/web/dist/assets/index-BlIWCM11.js +217 -0
  27. package/web/dist/assets/index-BlIWCM11.js.map +1 -0
  28. package/web/dist/assets/{index-BtAcw3EP.css → index-lvHFM_ut.css} +1 -1
  29. package/web/dist/index.html +2 -2
  30. package/dist/api/ralph.js +0 -153
  31. package/dist/api/ralph.test.js +0 -101
  32. package/dist/copilot/agents.squad.test.js +0 -72
  33. package/dist/copilot/hooks.js +0 -157
  34. package/dist/copilot/hooks.test.js +0 -315
  35. package/dist/copilot/squad-event-bus.js +0 -27
  36. package/dist/copilot/tools.squad.test.js +0 -168
  37. package/dist/squad/charter.js +0 -125
  38. package/dist/squad/charter.test.js +0 -89
  39. package/dist/squad/context.js +0 -48
  40. package/dist/squad/context.test.js +0 -59
  41. package/dist/squad/discovery.js +0 -268
  42. package/dist/squad/discovery.test.js +0 -154
  43. package/dist/squad/index.js +0 -9
  44. package/dist/squad/init-cli.js +0 -109
  45. package/dist/squad/init.js +0 -395
  46. package/dist/squad/init.test.js +0 -351
  47. package/dist/squad/mirror.js +0 -83
  48. package/dist/squad/mirror.scheduler.js +0 -80
  49. package/dist/squad/mirror.scheduler.test.js +0 -197
  50. package/dist/squad/mirror.test.js +0 -172
  51. package/dist/squad/registry.js +0 -162
  52. package/dist/squad/registry.test.js +0 -31
  53. package/dist/squad/squad-coordinator-system-message.test.js +0 -190
  54. package/dist/squad/squad-session-routing.test.js +0 -260
  55. package/dist/squad/types.js +0 -4
  56. package/dist/squad/worktree.js +0 -295
  57. package/dist/squad/worktree.test.js +0 -189
  58. package/dist/store/squad-sessions.test.js +0 -341
  59. package/web/dist/assets/index-IgSOXx_a.js +0 -219
  60. package/web/dist/assets/index-IgSOXx_a.js.map +0 -1
@@ -1,351 +0,0 @@
1
- /**
2
- * src/squad/init.test.ts
3
- *
4
- * Tests for the Squad init scaffolding module.
5
- *
6
- * Coverage:
7
- * - scaffoldSquad produces the expected .squad/ layout
8
- * - isSquadInitialized correctly detects initialized/uninitialized dirs
9
- * - Idempotence: scaffoldSquad returns null (does not clobber) on re-run
10
- * - Dune universe character allocation
11
- * - allocateCast uses the selected universe
12
- * - resolveRoles clamps to valid presets
13
- * - force flag overrides idempotence guard
14
- */
15
- import assert from 'node:assert/strict';
16
- import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
17
- import { join } from 'node:path';
18
- import { tmpdir as osTmpdir } from 'node:os';
19
- import test from 'node:test';
20
- import { scaffoldSquad, isSquadInitialized, allocateCast, resolveRoles, DUNE_CHARACTERS, UNIVERSE_CHARACTERS, DEFAULT_UNIVERSE, } from './init.js';
21
- // ---------------------------------------------------------------------------
22
- // Helpers
23
- // ---------------------------------------------------------------------------
24
- function makeTempDir() {
25
- return mkdtempSync(join(osTmpdir(), 'ch-init-test-'));
26
- }
27
- function cleanup(dir) {
28
- rmSync(dir, { recursive: true, force: true });
29
- }
30
- // ---------------------------------------------------------------------------
31
- // Unit tests — pure functions
32
- // ---------------------------------------------------------------------------
33
- test('DUNE_CHARACTERS has at least 5 entries', () => {
34
- assert.ok(DUNE_CHARACTERS.length >= 5, `Expected ≥ 5 Dune characters, got ${DUNE_CHARACTERS.length}`);
35
- });
36
- test('UNIVERSE_CHARACTERS includes Dune', () => {
37
- assert.ok('Dune' in UNIVERSE_CHARACTERS, 'UNIVERSE_CHARACTERS must include "Dune"');
38
- assert.equal(UNIVERSE_CHARACTERS['Dune'], DUNE_CHARACTERS);
39
- });
40
- test('DEFAULT_UNIVERSE is Dune', () => {
41
- assert.equal(DEFAULT_UNIVERSE, 'Dune');
42
- });
43
- test('resolveRoles(4) returns 4 roles', () => {
44
- const roles = resolveRoles(4);
45
- assert.equal(roles.length, 4);
46
- });
47
- test('resolveRoles(3) returns 3 roles', () => {
48
- assert.equal(resolveRoles(3).length, 3);
49
- });
50
- test('resolveRoles(5) returns 5 roles', () => {
51
- assert.equal(resolveRoles(5).length, 5);
52
- });
53
- test('resolveRoles clamps values below 3 to 3', () => {
54
- assert.equal(resolveRoles(1).length, 3);
55
- });
56
- test('resolveRoles clamps values above 5 to 5', () => {
57
- assert.equal(resolveRoles(10).length, 5);
58
- });
59
- test('allocateCast assigns Dune names in order', () => {
60
- const roles = resolveRoles(4);
61
- const cast = allocateCast(roles, 'Dune');
62
- assert.equal(Object.keys(cast).length, 4);
63
- // First role maps to first Dune character
64
- const firstRoleSlug = roles[0].slug;
65
- assert.equal(cast[firstRoleSlug], DUNE_CHARACTERS[0]);
66
- });
67
- test('allocateCast uses provided universe', () => {
68
- const roles = resolveRoles(3);
69
- const cast = allocateCast(roles, 'Firefly');
70
- const firfly = UNIVERSE_CHARACTERS['Firefly'];
71
- const firstRoleSlug = roles[0].slug;
72
- assert.equal(cast[firstRoleSlug], firfly[0]);
73
- });
74
- test('allocateCast falls back to Dune for unknown universe', () => {
75
- const roles = resolveRoles(3);
76
- const cast = allocateCast(roles, 'UnknownUniverse');
77
- const firstRoleSlug = roles[0].slug;
78
- assert.equal(cast[firstRoleSlug], DUNE_CHARACTERS[0]);
79
- });
80
- // ---------------------------------------------------------------------------
81
- // isSquadInitialized
82
- // ---------------------------------------------------------------------------
83
- test('isSquadInitialized returns false for empty dir', () => {
84
- const dir = makeTempDir();
85
- try {
86
- assert.equal(isSquadInitialized(dir), false);
87
- }
88
- finally {
89
- cleanup(dir);
90
- }
91
- });
92
- test('isSquadInitialized returns false when team.md missing', () => {
93
- const dir = makeTempDir();
94
- try {
95
- assert.equal(isSquadInitialized(dir), false);
96
- }
97
- finally {
98
- cleanup(dir);
99
- }
100
- });
101
- // ---------------------------------------------------------------------------
102
- // scaffoldSquad integration
103
- // ---------------------------------------------------------------------------
104
- const BASE_CONFIG = {
105
- projectName: 'TestApp',
106
- stack: 'TypeScript, Node.js',
107
- goal: 'A test application',
108
- teamSize: 4,
109
- universe: 'Dune',
110
- humanName: 'Alice',
111
- };
112
- test('scaffoldSquad returns ScaffoldResult with 4 cast agents', () => {
113
- const dir = makeTempDir();
114
- try {
115
- const result = scaffoldSquad(dir, BASE_CONFIG);
116
- assert.ok(result !== null, 'scaffoldSquad should return a result');
117
- assert.equal(result.agents.length, 4, 'Should cast 4 agents');
118
- assert.equal(result.universe, 'Dune');
119
- }
120
- finally {
121
- cleanup(dir);
122
- }
123
- });
124
- test('scaffoldSquad creates .squad directory', () => {
125
- const dir = makeTempDir();
126
- try {
127
- scaffoldSquad(dir, BASE_CONFIG);
128
- assert.ok(existsSync(join(dir, '.squad')), '.squad/ must exist');
129
- }
130
- finally {
131
- cleanup(dir);
132
- }
133
- });
134
- test('scaffoldSquad creates team.md with ## Members section', () => {
135
- const dir = makeTempDir();
136
- try {
137
- scaffoldSquad(dir, BASE_CONFIG);
138
- const teamMd = readFileSync(join(dir, '.squad', 'team.md'), 'utf-8');
139
- assert.ok(teamMd.includes('## Members'), 'team.md must contain ## Members');
140
- assert.ok(teamMd.includes('Scribe'), 'team.md must list Scribe');
141
- assert.ok(teamMd.includes('Ralph'), 'team.md must list Ralph');
142
- }
143
- finally {
144
- cleanup(dir);
145
- }
146
- });
147
- test('scaffoldSquad creates team.md with project info', () => {
148
- const dir = makeTempDir();
149
- try {
150
- scaffoldSquad(dir, BASE_CONFIG);
151
- const teamMd = readFileSync(join(dir, '.squad', 'team.md'), 'utf-8');
152
- assert.ok(teamMd.includes('TestApp'), 'team.md must contain project name');
153
- assert.ok(teamMd.includes('Dune'), 'team.md must contain universe');
154
- assert.ok(teamMd.includes('Alice'), 'team.md must contain human name');
155
- }
156
- finally {
157
- cleanup(dir);
158
- }
159
- });
160
- test('scaffoldSquad creates routing.md', () => {
161
- const dir = makeTempDir();
162
- try {
163
- scaffoldSquad(dir, BASE_CONFIG);
164
- assert.ok(existsSync(join(dir, '.squad', 'routing.md')));
165
- }
166
- finally {
167
- cleanup(dir);
168
- }
169
- });
170
- test('scaffoldSquad creates ceremonies.md', () => {
171
- const dir = makeTempDir();
172
- try {
173
- scaffoldSquad(dir, BASE_CONFIG);
174
- assert.ok(existsSync(join(dir, '.squad', 'ceremonies.md')));
175
- }
176
- finally {
177
- cleanup(dir);
178
- }
179
- });
180
- test('scaffoldSquad creates decisions.md', () => {
181
- const dir = makeTempDir();
182
- try {
183
- scaffoldSquad(dir, BASE_CONFIG);
184
- assert.ok(existsSync(join(dir, '.squad', 'decisions.md')));
185
- }
186
- finally {
187
- cleanup(dir);
188
- }
189
- });
190
- test('scaffoldSquad creates agent charter.md for each cast agent', () => {
191
- const dir = makeTempDir();
192
- try {
193
- const result = scaffoldSquad(dir, BASE_CONFIG);
194
- for (const agent of result.agents) {
195
- const charterPath = join(dir, '.squad', 'agents', agent.slug, 'charter.md');
196
- assert.ok(existsSync(charterPath), `charter.md missing for ${agent.slug}`);
197
- const content = readFileSync(charterPath, 'utf-8');
198
- assert.ok(content.includes(agent.castName), `charter should mention ${agent.castName}`);
199
- }
200
- }
201
- finally {
202
- cleanup(dir);
203
- }
204
- });
205
- test('scaffoldSquad creates agent history.md for each cast agent', () => {
206
- const dir = makeTempDir();
207
- try {
208
- const result = scaffoldSquad(dir, BASE_CONFIG);
209
- for (const agent of result.agents) {
210
- const histPath = join(dir, '.squad', 'agents', agent.slug, 'history.md');
211
- assert.ok(existsSync(histPath), `history.md missing for ${agent.slug}`);
212
- }
213
- }
214
- finally {
215
- cleanup(dir);
216
- }
217
- });
218
- test('scaffoldSquad creates scribe and ralph agent dirs', () => {
219
- const dir = makeTempDir();
220
- try {
221
- scaffoldSquad(dir, BASE_CONFIG);
222
- assert.ok(existsSync(join(dir, '.squad', 'agents', 'scribe', 'charter.md')));
223
- assert.ok(existsSync(join(dir, '.squad', 'agents', 'ralph', 'charter.md')));
224
- }
225
- finally {
226
- cleanup(dir);
227
- }
228
- });
229
- test('scaffoldSquad creates casting/policy.json', () => {
230
- const dir = makeTempDir();
231
- try {
232
- scaffoldSquad(dir, BASE_CONFIG);
233
- const policyPath = join(dir, '.squad', 'casting', 'policy.json');
234
- assert.ok(existsSync(policyPath));
235
- const policy = JSON.parse(readFileSync(policyPath, 'utf-8'));
236
- assert.ok(Array.isArray(policy.allowlist_universes));
237
- assert.ok(policy.allowlist_universes.includes('Dune'));
238
- }
239
- finally {
240
- cleanup(dir);
241
- }
242
- });
243
- test('scaffoldSquad creates casting/registry.json with all agents', () => {
244
- const dir = makeTempDir();
245
- try {
246
- scaffoldSquad(dir, BASE_CONFIG);
247
- const reg = JSON.parse(readFileSync(join(dir, '.squad', 'casting', 'registry.json'), 'utf-8'));
248
- assert.ok(reg.agents, 'registry must have agents key');
249
- assert.ok('scribe' in reg.agents, 'scribe must be in registry');
250
- assert.ok('ralph' in reg.agents, 'ralph must be in registry');
251
- }
252
- finally {
253
- cleanup(dir);
254
- }
255
- });
256
- test('scaffoldSquad creates casting/history.json with assignment snapshot', () => {
257
- const dir = makeTempDir();
258
- try {
259
- const result = scaffoldSquad(dir, BASE_CONFIG);
260
- const hist = JSON.parse(readFileSync(join(dir, '.squad', 'casting', 'history.json'), 'utf-8'));
261
- assert.ok(Array.isArray(hist.universe_usage_history));
262
- assert.equal(hist.universe_usage_history[0].universe, 'Dune');
263
- assert.ok(result.assignmentId in hist.assignment_cast_snapshots);
264
- }
265
- finally {
266
- cleanup(dir);
267
- }
268
- });
269
- test('scaffoldSquad creates or updates .gitattributes', () => {
270
- const dir = makeTempDir();
271
- try {
272
- scaffoldSquad(dir, BASE_CONFIG);
273
- const ga = readFileSync(join(dir, '.gitattributes'), 'utf-8');
274
- assert.ok(ga.includes('.squad/decisions.md merge=union'), '.gitattributes must include union merge for decisions.md');
275
- assert.ok(ga.includes('.squad/agents/*/history.md merge=union'));
276
- }
277
- finally {
278
- cleanup(dir);
279
- }
280
- });
281
- test('scaffoldSquad is idempotent — returns null on second call', () => {
282
- const dir = makeTempDir();
283
- try {
284
- const first = scaffoldSquad(dir, BASE_CONFIG);
285
- assert.ok(first !== null, 'first call should succeed');
286
- const second = scaffoldSquad(dir, BASE_CONFIG);
287
- assert.equal(second, null, 'second call should return null (idempotent)');
288
- }
289
- finally {
290
- cleanup(dir);
291
- }
292
- });
293
- test('scaffoldSquad with force:true overwrites existing scaffold', () => {
294
- const dir = makeTempDir();
295
- try {
296
- scaffoldSquad(dir, BASE_CONFIG);
297
- const result = scaffoldSquad(dir, { ...BASE_CONFIG, projectName: 'Updated' }, { force: true });
298
- assert.ok(result !== null, 'forced call should succeed');
299
- const teamMd = readFileSync(join(dir, '.squad', 'team.md'), 'utf-8');
300
- assert.ok(teamMd.includes('Updated'), 'team.md should reflect updated project name');
301
- }
302
- finally {
303
- cleanup(dir);
304
- }
305
- });
306
- test('isSquadInitialized returns true after scaffoldSquad', () => {
307
- const dir = makeTempDir();
308
- try {
309
- scaffoldSquad(dir, BASE_CONFIG);
310
- assert.equal(isSquadInitialized(dir), true);
311
- }
312
- finally {
313
- cleanup(dir);
314
- }
315
- });
316
- test('scaffoldSquad uses Dune names from the canonical pool', () => {
317
- const dir = makeTempDir();
318
- try {
319
- const result = scaffoldSquad(dir, BASE_CONFIG);
320
- for (const agent of result.agents) {
321
- assert.ok(DUNE_CHARACTERS.includes(agent.castName), `${agent.castName} is not in the Dune character pool`);
322
- }
323
- }
324
- finally {
325
- cleanup(dir);
326
- }
327
- });
328
- test('scaffoldSquad uses Firefly names when universe is Firefly', () => {
329
- const dir = makeTempDir();
330
- try {
331
- const result = scaffoldSquad(dir, { ...BASE_CONFIG, universe: 'Firefly' });
332
- const fireflyPool = UNIVERSE_CHARACTERS['Firefly'];
333
- for (const agent of result.agents) {
334
- assert.ok(fireflyPool.includes(agent.castName), `${agent.castName} is not in the Firefly pool`);
335
- }
336
- }
337
- finally {
338
- cleanup(dir);
339
- }
340
- });
341
- test('scaffoldSquad creates decisions/inbox directory', () => {
342
- const dir = makeTempDir();
343
- try {
344
- scaffoldSquad(dir, BASE_CONFIG);
345
- assert.ok(existsSync(join(dir, '.squad', 'decisions', 'inbox')));
346
- }
347
- finally {
348
- cleanup(dir);
349
- }
350
- });
351
- //# sourceMappingURL=init.test.js.map
@@ -1,83 +0,0 @@
1
- import { basename, join } from 'path';
2
- import { existsSync, readFileSync, readdirSync } from 'fs';
3
- import { readPage, writePage } from '../wiki/fs.js';
4
- import { ensureWikiStructure } from '../wiki/fs.js';
5
- import { childLogger } from '../util/logger.js';
6
- const log = childLogger('squad.mirror');
7
- /**
8
- * Returns the wiki-relative path for a project's squad decisions page.
9
- * e.g. "pages/projects/chapterhouse/decisions.md"
10
- */
11
- export function projectDecisionWikiPath(projectRoot) {
12
- const slug = basename(projectRoot).toLowerCase().replace(/[^a-z0-9-]/g, '-');
13
- return `pages/projects/${slug}/decisions.md`;
14
- }
15
- /**
16
- * Append a decision entry to the project's wiki decisions page.
17
- * Returns the wiki page path that was written.
18
- *
19
- * Only call this on successful task completion — the caller is responsible for
20
- * not invoking on failure.
21
- */
22
- export async function mirrorDecisionToWiki(link, taskSummary, resultSummary) {
23
- ensureWikiStructure();
24
- const wikiPath = projectDecisionWikiPath(link.projectRoot);
25
- const dateStr = new Date().toISOString().slice(0, 10);
26
- const existing = readPage(wikiPath) ?? `# Squad Decisions — ${basename(link.projectRoot)}\n\n`;
27
- const entry = `## ${dateStr}\n- **Agent:** @${link.squadAgentSlug} | **Task:** ${taskSummary} | **Result:** ${resultSummary}\n`;
28
- // Append entry, ensuring a blank line separator
29
- const separator = existing.endsWith('\n\n') ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
30
- const updated = existing + separator + entry;
31
- writePage(wikiPath, updated);
32
- return wikiPath;
33
- }
34
- /**
35
- * Check the decisions inbox for unmerged decision drops.
36
- * Returns count and filenames — never throws.
37
- */
38
- export async function checkDecisionsInbox(projectRoot) {
39
- const inboxPath = join(projectRoot, '.squad', 'decisions', 'inbox');
40
- try {
41
- if (!existsSync(inboxPath))
42
- return { count: 0, files: [] };
43
- const files = readdirSync(inboxPath).filter(f => f.endsWith('.md'));
44
- return { count: files.length, files };
45
- }
46
- catch {
47
- return { count: 0, files: [] };
48
- }
49
- }
50
- /**
51
- * Sync the content of <projectRoot>/.squad/decisions.md verbatim to the
52
- * project's wiki decisions page. decisions.md is the source of truth — the
53
- * wiki page is overwritten on every call.
54
- *
55
- * Returns null when decisions.md does not exist (project may not be squad-
56
- * initialized); never throws — errors are logged and the caller is unaffected.
57
- */
58
- export async function syncDecisionsFileToWiki(projectRoot) {
59
- const decisionsPath = join(projectRoot, '.squad', 'decisions.md');
60
- try {
61
- if (!existsSync(decisionsPath))
62
- return null;
63
- const source = readFileSync(decisionsPath, 'utf-8');
64
- ensureWikiStructure();
65
- const wikiPath = projectDecisionWikiPath(projectRoot);
66
- const timestamp = new Date().toISOString();
67
- const header = `<!-- Last synced: ${timestamp} -->\n\n`;
68
- writePage(wikiPath, header + source);
69
- // Count entries as second-level headings (## …) in the decisions file
70
- const entriesSynced = (source.match(/^##\s/gm) ?? []).length;
71
- // Visibility check: warn if inbox has unmerged drops
72
- const inbox = await checkDecisionsInbox(projectRoot);
73
- if (inbox.count > 0) {
74
- log.warn({ projectRoot, count: inbox.count, files: inbox.files }, 'unmerged decision drops in inbox — run Scribe to merge');
75
- }
76
- return { entriesSynced, wikiPath };
77
- }
78
- catch (err) {
79
- log.error({ err: err instanceof Error ? err.message : err }, 'syncDecisionsFileToWiki failed (non-fatal)');
80
- return null;
81
- }
82
- }
83
- //# sourceMappingURL=mirror.js.map
@@ -1,80 +0,0 @@
1
- import { getDb } from '../store/db.js';
2
- import { syncDecisionsFileToWiki } from './mirror.js';
3
- import { childLogger } from '../util/logger.js';
4
- const log = childLogger('squad.mirror-scheduler');
5
- export const DEFAULT_DECISIONS_SYNC_INTERVAL_MS = 300_000; // 5 minutes
6
- function readIntervalFromEnv() {
7
- const raw = process.env.CHAPTERHOUSE_DECISIONS_SYNC_INTERVAL_MS;
8
- if (raw === undefined)
9
- return DEFAULT_DECISIONS_SYNC_INTERVAL_MS;
10
- const parsed = Number(raw);
11
- return isNaN(parsed) ? DEFAULT_DECISIONS_SYNC_INTERVAL_MS : parsed;
12
- }
13
- function defaultGetRegisteredProjectRoots() {
14
- try {
15
- const db = getDb();
16
- const rows = db
17
- .prepare(`SELECT project_root FROM project_squads WHERE registered = 1`)
18
- .all();
19
- return rows.map(r => r.project_root);
20
- }
21
- catch (err) {
22
- log.error({ err: err instanceof Error ? err.message : err }, 'failed to query registered projects (non-fatal)');
23
- return [];
24
- }
25
- }
26
- export class DecisionsSyncScheduler {
27
- intervalMs;
28
- getRegisteredProjectRoots;
29
- syncFn;
30
- setIntervalImpl;
31
- clearIntervalImpl;
32
- handle;
33
- constructor(options = {}) {
34
- this.intervalMs = options.intervalMs ?? readIntervalFromEnv();
35
- this.getRegisteredProjectRoots = options.getRegisteredProjectRoots ?? defaultGetRegisteredProjectRoots;
36
- this.syncFn = options.syncFn ?? syncDecisionsFileToWiki;
37
- this.setIntervalImpl = options.setIntervalImpl ?? setInterval;
38
- this.clearIntervalImpl = options.clearIntervalImpl ?? clearInterval;
39
- }
40
- /** Start the periodic sync. No-op if intervalMs <= 0. */
41
- start() {
42
- if (this.intervalMs <= 0) {
43
- log.debug({ intervalMs: this.intervalMs }, 'disabled (intervalMs <= 0)');
44
- return;
45
- }
46
- if (this.handle !== undefined)
47
- return; // already running
48
- log.info({ intervalMs: this.intervalMs }, 'starting');
49
- this.handle = this.setIntervalImpl(() => { void this.tick(); }, this.intervalMs);
50
- // Allow Node.js to exit even if the interval is still active
51
- this.handle?.unref?.();
52
- }
53
- /** Stop the periodic sync and clear the timer. */
54
- stop() {
55
- if (this.handle !== undefined) {
56
- this.clearIntervalImpl(this.handle);
57
- this.handle = undefined;
58
- log.info('stopped');
59
- }
60
- }
61
- async tick() {
62
- const projects = this.getRegisteredProjectRoots();
63
- if (projects.length === 0)
64
- return;
65
- log.debug({ projects: projects.length }, 'syncing');
66
- for (const root of projects) {
67
- try {
68
- const result = await this.syncFn(root);
69
- if (result) {
70
- log.debug({ entriesSynced: result.entriesSynced, root }, 'synced');
71
- }
72
- }
73
- catch (err) {
74
- // syncDecisionsFileToWiki already swallows errors; belt-and-suspenders here
75
- log.error({ root, err: err instanceof Error ? err.message : err }, 'unexpected sync error (non-fatal)');
76
- }
77
- }
78
- }
79
- }
80
- //# sourceMappingURL=mirror.scheduler.js.map