@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/README.md +38 -17
- package/dashboard.html +438 -203
- package/dashboard.js +349 -66
- package/docs/self-improvement.md +2 -2
- package/engine.js +30 -107
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -151,9 +151,32 @@ function getAgents() {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
function getPrdInfo() {
|
|
154
|
-
//
|
|
155
|
-
const
|
|
156
|
-
|
|
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(
|
|
201
|
-
existing:
|
|
202
|
-
missing:
|
|
203
|
-
questions:
|
|
204
|
-
summary:
|
|
205
|
-
missingList: (
|
|
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
|
-
|
|
266
|
-
|
|
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 —
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
|
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
|
|
916
|
-
const plans =
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
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 {
|
package/docs/self-improvement.md
CHANGED
|
@@ -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:
|
|
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` |
|
|
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 |
|