@yemi33/squad 0.1.19 → 0.1.21

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/dashboard.js CHANGED
@@ -151,9 +151,32 @@ function getAgents() {
151
151
  }
152
152
 
153
153
  function getPrdInfo() {
154
- // Squad-level PRD single file at ~/.squad/prd.json
155
- const prdPath = path.join(SQUAD_DIR, 'prd.json');
156
- if (!fs.existsSync(prdPath)) return { progress: null, status: null };
154
+ // All PRD items come from plans/*.json (both manual /prd entries and agent-generated plans)
155
+ const plansDir = path.join(SQUAD_DIR, 'plans');
156
+ let allPrdItems = [];
157
+ let latestStat = null;
158
+
159
+ try {
160
+ const planFiles = fs.readdirSync(plansDir).filter(f => f.endsWith('.json'));
161
+ for (const pf of planFiles) {
162
+ try {
163
+ const plan = JSON.parse(fs.readFileSync(path.join(plansDir, pf), 'utf8'));
164
+ if (!plan.missing_features) continue;
165
+ const stat = fs.statSync(path.join(plansDir, pf));
166
+ if (!latestStat || stat.mtimeMs > latestStat.mtimeMs) latestStat = stat;
167
+ (plan.missing_features || []).forEach(f => allPrdItems.push({
168
+ ...f, _source: pf, _planStatus: plan.status || 'active',
169
+ _planSummary: plan.plan_summary || pf,
170
+ }));
171
+ } catch {}
172
+ }
173
+ } catch {}
174
+
175
+ // Legacy prd.json removed — all PRD items now in plans/*.json
176
+
177
+ if (allPrdItems.length === 0) return { progress: null, status: null };
178
+
179
+ const items = allPrdItems;
157
180
 
158
181
  try {
159
182
  const stat = fs.statSync(prdPath);
@@ -197,12 +220,12 @@ function getPrdInfo() {
197
220
 
198
221
  const status = {
199
222
  exists: true,
200
- age: timeSince(stat.mtimeMs),
201
- existing: data.existing_features?.length || 0,
202
- missing: data.missing_features?.length || 0,
203
- questions: data.open_questions?.length || 0,
204
- summary: data.summary || '',
205
- missingList: (data.missing_features || []).map(f => ({ id: f.id, name: f.name || f.title, priority: f.priority, complexity: f.estimated_complexity || f.size })),
223
+ age: latestStat ? timeSince(latestStat.mtimeMs) : 'unknown',
224
+ existing: 0,
225
+ missing: items.filter(i => i.status === 'missing').length,
226
+ questions: 0,
227
+ summary: '',
228
+ missingList: items.filter(i => i.status === 'missing').map(f => ({ id: f.id, name: f.name || f.title, priority: f.priority, complexity: f.estimated_complexity || f.size })),
206
229
  };
207
230
 
208
231
  return { progress, status };
@@ -262,29 +285,8 @@ function getPullRequests() {
262
285
  }
263
286
 
264
287
  function getArchivedPrds() {
265
- const firstProject = PROJECTS[0];
266
- const root = path.resolve(firstProject.localPath || path.resolve(SQUAD_DIR, '..'));
267
- const prdSrc = firstProject.workSources?.prd || CONFIG.workSources?.prd || {};
268
- const prdDir = path.dirname(path.resolve(root, prdSrc.path || 'docs/prd-gaps.json'));
269
- const archiveDir = path.join(prdDir, 'archive');
270
- return safeReadDir(archiveDir)
271
- .filter(f => f.startsWith('prd-gaps') && f.endsWith('.json'))
272
- .map(f => {
273
- try {
274
- const data = JSON.parse(fs.readFileSync(path.join(archiveDir, f), 'utf8'));
275
- const items = data.missing_features || [];
276
- return {
277
- file: f,
278
- version: data.version || f.replace('prd-gaps-', '').replace('.json', ''),
279
- summary: data.summary || '',
280
- total: items.length,
281
- existing_features: data.existing_features || [],
282
- missing_features: items,
283
- open_questions: data.open_questions || [],
284
- };
285
- } catch { return null; }
286
- })
287
- .filter(Boolean);
288
+ // All PRD items now in plans/*.json — no separate archive needed
289
+ return [];
288
290
  }
289
291
 
290
292
  function getEngineState() {
@@ -788,24 +790,35 @@ const server = http.createServer(async (req, res) => {
788
790
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
789
791
  }
790
792
 
791
- // POST /api/prd-items — squad-level PRD
793
+ // POST /api/prd-items — create a PRD item as a plan file in plans/ (auto-approved)
792
794
  if (req.method === 'POST' && req.url === '/api/prd-items') {
793
795
  try {
794
796
  const body = await readBody(req);
795
797
  if (!body.name || !body.name.trim()) return jsonReply(res, 400, { error: 'name is required' });
796
- const prdPath = path.join(SQUAD_DIR, 'prd.json');
797
- let data = { missing_features: [], existing_features: [], open_questions: [] };
798
- const existing = safeRead(prdPath);
799
- if (existing) { try { data = JSON.parse(existing); } catch {} }
800
- if (!data.missing_features) data.missing_features = [];
801
- data.missing_features.push({
802
- id: body.id, name: body.name, description: body.description || '',
803
- priority: body.priority || 'medium', estimated_complexity: body.estimated_complexity || 'medium',
804
- rationale: body.rationale || '', status: 'missing', affected_areas: [],
805
- projects: body.projects || [],
806
- });
807
- safeWrite(prdPath, data);
808
- return jsonReply(res, 200, { ok: true, id: body.id });
798
+
799
+ const plansDir = path.join(SQUAD_DIR, 'plans');
800
+ if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
801
+
802
+ const id = body.id || ('M' + String(Date.now()).slice(-4));
803
+ const planFile = 'manual-' + Date.now() + '.json';
804
+ const plan = {
805
+ version: 'manual-' + new Date().toISOString().slice(0, 10),
806
+ project: body.project || (PROJECTS[0]?.name || 'Unknown'),
807
+ generated_by: 'dashboard',
808
+ generated_at: new Date().toISOString().slice(0, 10),
809
+ plan_summary: body.name,
810
+ status: 'awaiting-approval',
811
+ requires_approval: true,
812
+ branch_strategy: 'parallel',
813
+ missing_features: [{
814
+ id, name: body.name, description: body.description || '',
815
+ priority: body.priority || 'medium', estimated_complexity: body.estimated_complexity || 'medium',
816
+ status: 'missing', depends_on: [], acceptance_criteria: [],
817
+ }],
818
+ open_questions: [],
819
+ };
820
+ safeWrite(path.join(plansDir, planFile), plan);
821
+ return jsonReply(res, 200, { ok: true, id, file: planFile });
809
822
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
810
823
  }
811
824
 
@@ -909,37 +922,63 @@ const server = http.createServer(async (req, res) => {
909
922
  return;
910
923
  }
911
924
 
912
- // GET /api/plans — list all plan files with status
925
+ // GET /api/plans — list all plan files (.json PRDs + .md raw plans)
913
926
  if (req.method === 'GET' && req.url === '/api/plans') {
914
927
  const plansDir = path.join(SQUAD_DIR, 'plans');
915
- const files = safeReadDir(plansDir).filter(f => f.endsWith('.json'));
916
- const plans = files.map(f => {
917
- const plan = JSON.parse(safeRead(path.join(plansDir, f)) || '{}');
918
- return {
919
- file: f,
920
- project: plan.project || '',
921
- summary: plan.plan_summary || '',
922
- status: plan.status || 'active',
923
- branchStrategy: plan.branch_strategy || 'parallel',
924
- featureBranch: plan.feature_branch || '',
925
- itemCount: (plan.missing_features || []).length,
926
- generatedBy: plan.generated_by || '',
927
- generatedAt: plan.generated_at || '',
928
- requiresApproval: plan.requires_approval || false,
929
- revisionFeedback: plan.revision_feedback || null,
930
- };
931
- }).sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
928
+ const allFiles = safeReadDir(plansDir).filter(f => f.endsWith('.json') || f.endsWith('.md'));
929
+ const plans = allFiles.map(f => {
930
+ const content = safeRead(path.join(plansDir, f)) || '';
931
+ const isJson = f.endsWith('.json');
932
+ if (isJson) {
933
+ try {
934
+ const plan = JSON.parse(content);
935
+ return {
936
+ file: f, format: 'prd',
937
+ project: plan.project || '',
938
+ summary: plan.plan_summary || '',
939
+ status: plan.status || 'active',
940
+ branchStrategy: plan.branch_strategy || 'parallel',
941
+ featureBranch: plan.feature_branch || '',
942
+ itemCount: (plan.missing_features || []).length,
943
+ generatedBy: plan.generated_by || '',
944
+ generatedAt: plan.generated_at || '',
945
+ requiresApproval: plan.requires_approval || false,
946
+ revisionFeedback: plan.revision_feedback || null,
947
+ };
948
+ } catch { return null; }
949
+ } else {
950
+ // .md raw plan — extract metadata from markdown
951
+ const titleMatch = content.match(/^#\s+(?:Plan:\s*)?(.+)/m);
952
+ const projectMatch = content.match(/\*\*Project:\*\*\s*(.+)/m);
953
+ const authorMatch = content.match(/\*\*Author:\*\*\s*(.+)/m);
954
+ const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/m);
955
+ return {
956
+ file: f, format: 'draft',
957
+ project: projectMatch ? projectMatch[1].trim() : '',
958
+ summary: titleMatch ? titleMatch[1].trim() : f.replace('.md', ''),
959
+ status: 'draft',
960
+ branchStrategy: '',
961
+ featureBranch: '',
962
+ itemCount: (content.match(/^\d+\.\s+\*\*/gm) || []).length,
963
+ generatedBy: authorMatch ? authorMatch[1].trim() : '',
964
+ generatedAt: dateMatch ? dateMatch[1].trim() : '',
965
+ requiresApproval: false,
966
+ revisionFeedback: null,
967
+ };
968
+ }
969
+ }).filter(Boolean).sort((a, b) => (b.generatedAt || '').localeCompare(a.generatedAt || ''));
932
970
  return jsonReply(res, 200, plans);
933
971
  }
934
972
 
935
- // GET /api/plans/:file — read full plan JSON
973
+ // GET /api/plans/:file — read full plan (JSON or markdown)
936
974
  const planFileMatch = req.url.match(/^\/api\/plans\/([^?]+)$/);
937
975
  if (planFileMatch && req.method === 'GET') {
938
976
  const file = decodeURIComponent(planFileMatch[1]);
939
977
  if (file.includes('..') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
940
978
  const content = safeRead(path.join(SQUAD_DIR, 'plans', file));
941
979
  if (!content) return jsonReply(res, 404, { error: 'not found' });
942
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
980
+ const contentType = file.endsWith('.json') ? 'application/json' : 'text/plain';
981
+ res.setHeader('Content-Type', contentType + '; charset=utf-8');
943
982
  res.setHeader('Access-Control-Allow-Origin', '*');
944
983
  res.end(content);
945
984
  return;
@@ -1107,6 +1146,250 @@ What would you like to discuss or change? When you're happy, say "approve" and I
1107
1146
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1108
1147
  }
1109
1148
 
1149
+ // POST /api/doc-chat — unified Q&A + steering: Haiku decides whether to answer or edit
1150
+ if (req.method === 'POST' && req.url === '/api/doc-chat') {
1151
+ try {
1152
+ const body = await readBody(req);
1153
+ if (!body.message) return jsonReply(res, 400, { error: 'message required' });
1154
+ if (!body.document) return jsonReply(res, 400, { error: 'document required' });
1155
+
1156
+ const canEdit = !!body.filePath;
1157
+ const isJson = body.filePath?.endsWith('.json');
1158
+
1159
+ // Read current content from disk if editable (authoritative source)
1160
+ let currentContent = body.document;
1161
+ let fullPath = null;
1162
+ if (canEdit) {
1163
+ fullPath = path.resolve(SQUAD_DIR, body.filePath);
1164
+ if (!fullPath.startsWith(path.resolve(SQUAD_DIR))) return jsonReply(res, 400, { error: 'path must be under squad directory' });
1165
+ const diskContent = safeRead(fullPath);
1166
+ if (diskContent !== null) currentContent = diskContent;
1167
+ }
1168
+
1169
+ const prompt = `You are a document assistant for a software engineering squad. You can answer questions AND edit documents.
1170
+
1171
+ ## Document
1172
+ ${body.title ? '**Title:** ' + body.title + '\n' : ''}File: ${body.filePath || '(read-only)'}
1173
+ ${isJson ? 'Format: JSON' : 'Format: Markdown'}
1174
+
1175
+ \`\`\`
1176
+ ${currentContent.slice(0, 20000)}
1177
+ \`\`\`
1178
+
1179
+ ${body.selection ? '## Highlighted Selection\n> ' + body.selection.slice(0, 1500) + '\n' : ''}
1180
+ ${(body.history && body.history.length > 0) ? '## Conversation So Far\n\n' + body.history.map(h => (h.role === 'user' ? 'User: ' : 'Assistant: ') + h.text).join('\n\n') + '\n' : ''}
1181
+ ## User Message
1182
+
1183
+ ${body.message}
1184
+
1185
+ ## Instructions
1186
+
1187
+ Determine what the user wants:
1188
+
1189
+ **If they're asking a question** (e.g., "what does this mean", "why is this", "explain"):
1190
+ - Answer concisely with source citations
1191
+ - Do NOT include the ---DOCUMENT--- delimiter
1192
+
1193
+ **If they want to edit the document** (e.g., "change", "remove", "add", "update", "rename", "fix", "reword"):
1194
+ - Briefly explain what you changed (1-2 sentences)
1195
+ - Then on a new line write exactly: ---DOCUMENT---
1196
+ - Then the COMPLETE updated document (full file, not a diff)
1197
+ ${isJson ? '- The document MUST be valid JSON. No markdown code fences.' : ''}
1198
+ ${!canEdit ? '\nNote: This document is READ-ONLY. Answer questions only — do not output ---DOCUMENT---.' : ''}
1199
+
1200
+ Be concise. No preamble.`;
1201
+
1202
+ const sysPrompt = 'You are a precise document assistant. Determine intent (question vs edit) from the user message. Follow the output format exactly.';
1203
+
1204
+ const id = Date.now();
1205
+ const promptPath = path.join(SQUAD_DIR, 'engine', 'chat-prompt-' + id + '.md');
1206
+ const sysPath = path.join(SQUAD_DIR, 'engine', 'chat-sys-' + id + '.md');
1207
+ safeWrite(promptPath, prompt);
1208
+ safeWrite(sysPath, sysPrompt);
1209
+
1210
+ const spawnScript = path.join(SQUAD_DIR, 'engine', 'spawn-agent.js');
1211
+ const childEnv = { ...process.env };
1212
+ for (const key of Object.keys(childEnv)) {
1213
+ if (key === 'CLAUDECODE' || key.startsWith('CLAUDE_CODE') || key.startsWith('CLAUDECODE_')) delete childEnv[key];
1214
+ }
1215
+
1216
+ const { spawn: cpSpawn } = require('child_process');
1217
+ const proc = cpSpawn(process.execPath, [
1218
+ spawnScript, promptPath, sysPath,
1219
+ '--output-format', 'text', '--max-turns', '1', '--model', 'haiku',
1220
+ '--permission-mode', 'bypassPermissions', '--verbose',
1221
+ ], { cwd: SQUAD_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
1222
+
1223
+ let stdout = '';
1224
+ let stderr = '';
1225
+ proc.stdout.on('data', d => { stdout += d.toString(); });
1226
+ proc.stderr.on('data', d => { stderr += d.toString(); });
1227
+
1228
+ const timeout = setTimeout(() => { try { proc.kill('SIGTERM'); } catch {} }, 90000);
1229
+
1230
+ proc.on('close', (code) => {
1231
+ clearTimeout(timeout);
1232
+ try { fs.unlinkSync(promptPath); } catch {}
1233
+ try { fs.unlinkSync(sysPath); } catch {}
1234
+
1235
+ if (code === 0 && stdout.trim()) {
1236
+ const output = stdout.trim();
1237
+ const delimIdx = output.indexOf('---DOCUMENT---');
1238
+
1239
+ if (delimIdx < 0) {
1240
+ // Q&A response — no edit
1241
+ return jsonReply(res, 200, { ok: true, answer: output, edited: false });
1242
+ }
1243
+
1244
+ // Edit response
1245
+ const explanation = output.slice(0, delimIdx).trim();
1246
+ let newContent = output.slice(delimIdx + '---DOCUMENT---'.length).trim();
1247
+ newContent = newContent.replace(/^```\w*\n?/, '').replace(/\n?```$/, '').trim();
1248
+
1249
+ if (isJson) {
1250
+ try { JSON.parse(newContent); } catch (e) {
1251
+ return jsonReply(res, 200, { ok: true, answer: explanation + '\n\n(JSON invalid — not saved: ' + e.message + ')', edited: false });
1252
+ }
1253
+ }
1254
+
1255
+ if (canEdit && fullPath) {
1256
+ safeWrite(fullPath, newContent);
1257
+ return jsonReply(res, 200, { ok: true, answer: explanation, edited: true, content: newContent });
1258
+ } else {
1259
+ return jsonReply(res, 200, { ok: true, answer: explanation + '\n\n(Read-only — changes not saved)', edited: false });
1260
+ }
1261
+ } else {
1262
+ return jsonReply(res, 500, { error: 'Failed (code=' + code + ')', stderr: stderr.slice(0, 200) });
1263
+ }
1264
+ });
1265
+
1266
+ proc.on('error', (err) => {
1267
+ clearTimeout(timeout);
1268
+ try { fs.unlinkSync(promptPath); } catch {}
1269
+ try { fs.unlinkSync(sysPath); } catch {}
1270
+ return jsonReply(res, 500, { error: err.message });
1271
+ });
1272
+ return;
1273
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1274
+ }
1275
+
1276
+ // POST /api/steer-document — modify a document via natural language instruction (Haiku)
1277
+ if (req.method === 'POST' && req.url === '/api/steer-document') {
1278
+ try {
1279
+ const body = await readBody(req);
1280
+ if (!body.instruction) return jsonReply(res, 400, { error: 'instruction required' });
1281
+ if (!body.filePath) return jsonReply(res, 400, { error: 'filePath required' });
1282
+
1283
+ // Read current document from disk (authoritative source)
1284
+ const fullPath = path.resolve(SQUAD_DIR, body.filePath);
1285
+ // Security: must be under SQUAD_DIR
1286
+ if (!fullPath.startsWith(path.resolve(SQUAD_DIR))) return jsonReply(res, 400, { error: 'path must be under squad directory' });
1287
+ const currentContent = safeRead(fullPath);
1288
+ if (currentContent === null) return jsonReply(res, 404, { error: 'file not found' });
1289
+
1290
+ const isJson = body.filePath.endsWith('.json');
1291
+ const prompt = `You are editing a document based on a user instruction.
1292
+
1293
+ ## Current Document
1294
+ File: ${body.filePath}
1295
+ ${isJson ? 'Format: JSON (you MUST output valid JSON)' : 'Format: Markdown'}
1296
+
1297
+ \`\`\`
1298
+ ${currentContent.slice(0, 20000)}
1299
+ \`\`\`
1300
+
1301
+ ${body.selection ? '## Context: User highlighted this section\n> ' + body.selection.slice(0, 1000) + '\n' : ''}
1302
+
1303
+ ## Instruction
1304
+
1305
+ ${body.instruction}
1306
+
1307
+ ## Output Format
1308
+
1309
+ Respond with EXACTLY two sections separated by the delimiter \`---DOCUMENT---\`:
1310
+
1311
+ 1. First: A brief explanation of what you changed (1-3 sentences)
1312
+ 2. Then the delimiter on its own line: \`---DOCUMENT---\`
1313
+ 3. Then the COMPLETE updated document content (not a diff — the full file)
1314
+
1315
+ Example:
1316
+ Removed item P003 and reordered remaining items.
1317
+ ---DOCUMENT---
1318
+ {full updated file content here}
1319
+
1320
+ ${isJson ? 'CRITICAL: The document section must be valid JSON. Do not add markdown code fences around it.' : ''}`;
1321
+
1322
+ const sysPrompt = 'You are a precise document editor. Follow the output format exactly. No code fences around the document output.';
1323
+
1324
+ const id = Date.now();
1325
+ const promptPath = path.join(SQUAD_DIR, 'engine', 'steer-prompt-' + id + '.md');
1326
+ const sysPath = path.join(SQUAD_DIR, 'engine', 'steer-sys-' + id + '.md');
1327
+ safeWrite(promptPath, prompt);
1328
+ safeWrite(sysPath, sysPrompt);
1329
+
1330
+ const spawnScript = path.join(SQUAD_DIR, 'engine', 'spawn-agent.js');
1331
+ const childEnv = { ...process.env };
1332
+ for (const key of Object.keys(childEnv)) {
1333
+ if (key === 'CLAUDECODE' || key.startsWith('CLAUDE_CODE') || key.startsWith('CLAUDECODE_')) delete childEnv[key];
1334
+ }
1335
+
1336
+ const { spawn: cpSpawn } = require('child_process');
1337
+ const proc = cpSpawn(process.execPath, [
1338
+ spawnScript, promptPath, sysPath,
1339
+ '--output-format', 'text', '--max-turns', '1', '--model', 'haiku',
1340
+ '--permission-mode', 'bypassPermissions', '--verbose',
1341
+ ], { cwd: SQUAD_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
1342
+
1343
+ let stdout = '';
1344
+ let stderr = '';
1345
+ proc.stdout.on('data', d => { stdout += d.toString(); });
1346
+ proc.stderr.on('data', d => { stderr += d.toString(); });
1347
+
1348
+ const timeout = setTimeout(() => { try { proc.kill('SIGTERM'); } catch {} }, 90000);
1349
+
1350
+ proc.on('close', (code) => {
1351
+ clearTimeout(timeout);
1352
+ try { fs.unlinkSync(promptPath); } catch {}
1353
+ try { fs.unlinkSync(sysPath); } catch {}
1354
+
1355
+ if (code === 0 && stdout.trim()) {
1356
+ const output = stdout.trim();
1357
+ const delimIdx = output.indexOf('---DOCUMENT---');
1358
+ if (delimIdx < 0) {
1359
+ return jsonReply(res, 200, { ok: true, answer: output, updated: false, reason: 'No document delimiter found — treated as Q&A response' });
1360
+ }
1361
+
1362
+ const explanation = output.slice(0, delimIdx).trim();
1363
+ let newContent = output.slice(delimIdx + '---DOCUMENT---'.length).trim();
1364
+
1365
+ // Strip code fences if model added them
1366
+ newContent = newContent.replace(/^```\w*\n?/, '').replace(/\n?```$/, '').trim();
1367
+
1368
+ // Validate JSON if applicable
1369
+ if (isJson) {
1370
+ try { JSON.parse(newContent); } catch (e) {
1371
+ return jsonReply(res, 200, { ok: true, answer: explanation + '\n\n(Warning: JSON validation failed — document not saved: ' + e.message + ')', updated: false });
1372
+ }
1373
+ }
1374
+
1375
+ // Write back
1376
+ safeWrite(fullPath, newContent);
1377
+ return jsonReply(res, 200, { ok: true, answer: explanation, updated: true, content: newContent });
1378
+ } else {
1379
+ return jsonReply(res, 500, { error: 'Steer failed (code=' + code + ')', stderr: stderr.slice(0, 200) });
1380
+ }
1381
+ });
1382
+
1383
+ proc.on('error', (err) => {
1384
+ clearTimeout(timeout);
1385
+ try { fs.unlinkSync(promptPath); } catch {}
1386
+ try { fs.unlinkSync(sysPath); } catch {}
1387
+ return jsonReply(res, 500, { error: err.message });
1388
+ });
1389
+ return;
1390
+ } catch (e) { return jsonReply(res, 400, { error: e.message }); }
1391
+ }
1392
+
1110
1393
  // POST /api/ask-about — ask a question about a document with context, answered by Haiku
1111
1394
  if (req.method === 'POST' && req.url === '/api/ask-about') {
1112
1395
  try {
@@ -25,7 +25,7 @@ Agent completes task
25
25
  Agent finishes task
26
26
  → writes notes/inbox/<agent>-<date>.md
27
27
  → engine checks inbox on next tick (~60s)
28
- → consolidateInbox() fires (threshold: 1 file)
28
+ → consolidateInbox() fires (threshold: 3 files)
29
29
  → spawns Claude Haiku for LLM-powered summarization
30
30
  → Haiku reads all inbox notes + existing notes.md
31
31
  → produces deduplicated, categorized digest
@@ -323,7 +323,7 @@ When a git merge or rebase produces conflicts in yarn.lock.
323
323
 
324
324
  | Setting | Default | What it controls |
325
325
  |---------|---------|-----------------|
326
- | `engine.inboxConsolidateThreshold` | 1 | Files needed before consolidation triggers |
326
+ | `engine.inboxConsolidateThreshold` | 3 | Files needed before consolidation triggers |
327
327
  | Consolidation model | Haiku | LLM used for summarization (fast, cheap) |
328
328
  | Consolidation timeout | 3 min | Max time for LLM call before fallback |
329
329
  | notes.md max size | 50KB | Auto-prunes old sections above this |