chapterhouse 0.3.13 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -69
- package/dist/api/server.js +8 -155
- package/dist/api/server.test.js +1 -1
- package/dist/cli.js +0 -30
- package/dist/config.js +0 -3
- package/dist/copilot/agent-event-bus.js +41 -0
- package/dist/copilot/agent-event-bus.test.js +23 -0
- package/dist/copilot/agents.js +4 -59
- package/dist/copilot/orchestrator.js +20 -39
- package/dist/copilot/orchestrator.test.js +73 -158
- package/dist/copilot/task-event-log.js +5 -5
- package/dist/copilot/task-event-log.test.js +68 -142
- package/dist/copilot/tools.js +9 -85
- package/dist/daemon.js +0 -22
- package/dist/store/db.js +2 -50
- package/dist/store/db.test.js +0 -45
- package/package.json +1 -3
- package/web/dist/assets/index-BlIWCM11.js +217 -0
- package/web/dist/assets/index-BlIWCM11.js.map +1 -0
- package/web/dist/assets/{index-BtAcw3EP.css → index-lvHFM_ut.css} +1 -1
- package/web/dist/index.html +2 -2
- package/dist/api/ralph.js +0 -153
- package/dist/api/ralph.test.js +0 -101
- package/dist/copilot/agents.squad.test.js +0 -72
- package/dist/copilot/hooks.js +0 -157
- package/dist/copilot/hooks.test.js +0 -315
- package/dist/copilot/squad-event-bus.js +0 -27
- package/dist/copilot/tools.squad.test.js +0 -168
- package/dist/squad/charter.js +0 -125
- package/dist/squad/charter.test.js +0 -89
- package/dist/squad/context.js +0 -48
- package/dist/squad/context.test.js +0 -59
- package/dist/squad/discovery.js +0 -268
- package/dist/squad/discovery.test.js +0 -154
- package/dist/squad/index.js +0 -9
- package/dist/squad/init-cli.js +0 -109
- package/dist/squad/init.js +0 -395
- package/dist/squad/init.test.js +0 -351
- package/dist/squad/mirror.js +0 -83
- package/dist/squad/mirror.scheduler.js +0 -80
- package/dist/squad/mirror.scheduler.test.js +0 -197
- package/dist/squad/mirror.test.js +0 -172
- package/dist/squad/registry.js +0 -162
- package/dist/squad/registry.test.js +0 -31
- package/dist/squad/squad-coordinator-system-message.test.js +0 -190
- package/dist/squad/squad-session-routing.test.js +0 -260
- package/dist/squad/types.js +0 -4
- package/dist/squad/worktree.js +0 -295
- package/dist/squad/worktree.test.js +0 -189
- package/dist/store/squad-sessions.test.js +0 -341
- package/web/dist/assets/index-IgSOXx_a.js +0 -219
- package/web/dist/assets/index-IgSOXx_a.js.map +0 -1
package/dist/squad/init.test.js
DELETED
|
@@ -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
|
package/dist/squad/mirror.js
DELETED
|
@@ -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
|