@yemi33/squad 0.1.13 → 0.1.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.
- package/README.md +2 -5
- package/dashboard.html +516 -29
- package/dashboard.js +353 -3
- package/docs/blog-first-successful-dispatch.md +1 -1
- package/engine.js +45 -44
- package/package.json +1 -1
- package/playbooks/explore.md +1 -0
- package/playbooks/plan-to-prd.md +2 -0
package/dashboard.js
CHANGED
|
@@ -75,7 +75,7 @@ function getAgentDetail(id) {
|
|
|
75
75
|
const agentDir = path.join(SQUAD_DIR, 'agents', id);
|
|
76
76
|
const charter = safeRead(path.join(agentDir, 'charter.md')) || 'No charter found.';
|
|
77
77
|
const history = safeRead(path.join(agentDir, 'history.md')) || 'No history yet.';
|
|
78
|
-
const outputLog = safeRead(path.join(agentDir, 'output.
|
|
78
|
+
const outputLog = safeRead(path.join(agentDir, 'output.log')) || '';
|
|
79
79
|
|
|
80
80
|
const statusJson = safeRead(path.join(agentDir, 'status.json'));
|
|
81
81
|
let statusData = null;
|
|
@@ -86,7 +86,25 @@ function getAgentDetail(id) {
|
|
|
86
86
|
.filter(f => f.includes(id))
|
|
87
87
|
.map(f => ({ name: f, content: safeRead(path.join(inboxDir, f)) || '' }));
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// Recent completed dispatches for this agent (last 10)
|
|
90
|
+
let recentDispatches = [];
|
|
91
|
+
try {
|
|
92
|
+
const dispatch = JSON.parse(safeRead(path.join(SQUAD_DIR, 'engine', 'dispatch.json')) || '{}');
|
|
93
|
+
recentDispatches = (dispatch.completed || [])
|
|
94
|
+
.filter(d => d.agent === id)
|
|
95
|
+
.slice(-10)
|
|
96
|
+
.reverse()
|
|
97
|
+
.map(d => ({
|
|
98
|
+
id: d.id,
|
|
99
|
+
task: d.task || '',
|
|
100
|
+
type: d.type || '',
|
|
101
|
+
result: d.result || '',
|
|
102
|
+
reason: d.reason || '',
|
|
103
|
+
completed_at: d.completed_at || '',
|
|
104
|
+
}));
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
return { charter, history, statusData, outputLog, inboxContents, recentDispatches };
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
function getAgents() {
|
|
@@ -424,6 +442,20 @@ function getWorkItems() {
|
|
|
424
442
|
return allItems;
|
|
425
443
|
}
|
|
426
444
|
|
|
445
|
+
function getMcpServers() {
|
|
446
|
+
try {
|
|
447
|
+
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
448
|
+
const claudeJsonPath = path.join(home, '.claude.json');
|
|
449
|
+
const data = JSON.parse(safeRead(claudeJsonPath) || '{}');
|
|
450
|
+
const servers = data.mcpServers || {};
|
|
451
|
+
return Object.entries(servers).map(([name, cfg]) => ({
|
|
452
|
+
name,
|
|
453
|
+
command: cfg.command || '',
|
|
454
|
+
args: (cfg.args || []).slice(-1)[0] || '',
|
|
455
|
+
}));
|
|
456
|
+
} catch { return []; }
|
|
457
|
+
}
|
|
458
|
+
|
|
427
459
|
function getStatus() {
|
|
428
460
|
const prdInfo = getPrdInfo();
|
|
429
461
|
return {
|
|
@@ -440,6 +472,7 @@ function getStatus() {
|
|
|
440
472
|
metrics: getMetrics(),
|
|
441
473
|
workItems: getWorkItems(),
|
|
442
474
|
skills: getSkills(),
|
|
475
|
+
mcpServers: getMcpServers(),
|
|
443
476
|
projects: PROJECTS.map(p => ({ name: p.name, path: p.localPath, description: p.description || '' })),
|
|
444
477
|
timestamp: new Date().toISOString(),
|
|
445
478
|
};
|
|
@@ -740,7 +773,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
740
773
|
priority: body.priority || 'high', description: body.description || '',
|
|
741
774
|
status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard',
|
|
742
775
|
chain: 'plan-to-prd',
|
|
743
|
-
branchStrategy: body.branch_strategy || '
|
|
776
|
+
branchStrategy: body.branch_strategy || 'parallel',
|
|
744
777
|
};
|
|
745
778
|
if (body.project) item.project = body.project;
|
|
746
779
|
if (body.agent) item.agent = body.agent;
|
|
@@ -858,6 +891,284 @@ const server = http.createServer(async (req, res) => {
|
|
|
858
891
|
return;
|
|
859
892
|
}
|
|
860
893
|
|
|
894
|
+
// GET /api/plans — list all plan files with status
|
|
895
|
+
if (req.method === 'GET' && req.url === '/api/plans') {
|
|
896
|
+
const plansDir = path.join(SQUAD_DIR, 'plans');
|
|
897
|
+
const files = safeReadDir(plansDir).filter(f => f.endsWith('.json'));
|
|
898
|
+
const plans = files.map(f => {
|
|
899
|
+
const plan = JSON.parse(safeRead(path.join(plansDir, f)) || '{}');
|
|
900
|
+
return {
|
|
901
|
+
file: f,
|
|
902
|
+
project: plan.project || '',
|
|
903
|
+
summary: plan.plan_summary || '',
|
|
904
|
+
status: plan.status || 'active',
|
|
905
|
+
branchStrategy: plan.branch_strategy || 'parallel',
|
|
906
|
+
featureBranch: plan.feature_branch || '',
|
|
907
|
+
itemCount: (plan.missing_features || []).length,
|
|
908
|
+
generatedBy: plan.generated_by || '',
|
|
909
|
+
generatedAt: plan.generated_at || '',
|
|
910
|
+
requiresApproval: plan.requires_approval || false,
|
|
911
|
+
revisionFeedback: plan.revision_feedback || null,
|
|
912
|
+
};
|
|
913
|
+
}).sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
|
|
914
|
+
return jsonReply(res, 200, plans);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// GET /api/plans/:file — read full plan JSON
|
|
918
|
+
const planFileMatch = req.url.match(/^\/api\/plans\/([^?]+)$/);
|
|
919
|
+
if (planFileMatch && req.method === 'GET') {
|
|
920
|
+
const file = decodeURIComponent(planFileMatch[1]);
|
|
921
|
+
if (file.includes('..') || file.includes('/') || file.includes('\\')) return jsonReply(res, 400, { error: 'invalid' });
|
|
922
|
+
const content = safeRead(path.join(SQUAD_DIR, 'plans', file));
|
|
923
|
+
if (!content) return jsonReply(res, 404, { error: 'not found' });
|
|
924
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
925
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
926
|
+
res.end(content);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// POST /api/plans/approve — approve a plan for execution
|
|
931
|
+
if (req.method === 'POST' && req.url === '/api/plans/approve') {
|
|
932
|
+
try {
|
|
933
|
+
const body = await readBody(req);
|
|
934
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
935
|
+
const planPath = path.join(SQUAD_DIR, 'plans', body.file);
|
|
936
|
+
const plan = JSON.parse(safeRead(planPath) || '{}');
|
|
937
|
+
plan.status = 'approved';
|
|
938
|
+
plan.approvedAt = new Date().toISOString();
|
|
939
|
+
plan.approvedBy = body.approvedBy || os.userInfo().username;
|
|
940
|
+
safeWrite(planPath, plan);
|
|
941
|
+
return jsonReply(res, 200, { ok: true, status: 'approved' });
|
|
942
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// POST /api/plans/reject — reject a plan
|
|
946
|
+
if (req.method === 'POST' && req.url === '/api/plans/reject') {
|
|
947
|
+
try {
|
|
948
|
+
const body = await readBody(req);
|
|
949
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
950
|
+
const planPath = path.join(SQUAD_DIR, 'plans', body.file);
|
|
951
|
+
const plan = JSON.parse(safeRead(planPath) || '{}');
|
|
952
|
+
plan.status = 'rejected';
|
|
953
|
+
plan.rejectedAt = new Date().toISOString();
|
|
954
|
+
plan.rejectedBy = body.rejectedBy || os.userInfo().username;
|
|
955
|
+
if (body.reason) plan.rejectionReason = body.reason;
|
|
956
|
+
safeWrite(planPath, plan);
|
|
957
|
+
return jsonReply(res, 200, { ok: true, status: 'rejected' });
|
|
958
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// POST /api/plans/revise — request revision with feedback, dispatches agent to revise
|
|
962
|
+
if (req.method === 'POST' && req.url === '/api/plans/revise') {
|
|
963
|
+
try {
|
|
964
|
+
const body = await readBody(req);
|
|
965
|
+
if (!body.file || !body.feedback) return jsonReply(res, 400, { error: 'file and feedback required' });
|
|
966
|
+
const planPath = path.join(SQUAD_DIR, 'plans', body.file);
|
|
967
|
+
const plan = JSON.parse(safeRead(planPath) || '{}');
|
|
968
|
+
plan.status = 'revision-requested';
|
|
969
|
+
plan.revision_feedback = body.feedback;
|
|
970
|
+
plan.revisionRequestedAt = new Date().toISOString();
|
|
971
|
+
plan.revisionRequestedBy = body.requestedBy || os.userInfo().username;
|
|
972
|
+
safeWrite(planPath, plan);
|
|
973
|
+
|
|
974
|
+
// Create a work item to revise the plan
|
|
975
|
+
const wiPath = path.join(SQUAD_DIR, 'work-items.json');
|
|
976
|
+
let items = [];
|
|
977
|
+
const existing = safeRead(wiPath);
|
|
978
|
+
if (existing) { try { items = JSON.parse(existing); } catch {} }
|
|
979
|
+
const maxNum = items.reduce(function(max, i) {
|
|
980
|
+
const m = (i.id || '').match(/(\d+)$/);
|
|
981
|
+
return m ? Math.max(max, parseInt(m[1])) : max;
|
|
982
|
+
}, 0);
|
|
983
|
+
const id = 'W' + String(maxNum + 1).padStart(3, '0');
|
|
984
|
+
items.push({
|
|
985
|
+
id, title: 'Revise plan: ' + (plan.plan_summary || body.file),
|
|
986
|
+
type: 'plan-to-prd', priority: 'high',
|
|
987
|
+
description: 'Revision requested on plan file: plans/' + body.file + '\n\nFeedback:\n' + body.feedback + '\n\nRevise the plan to address this feedback. Read the existing plan, apply the feedback, and overwrite the file with the updated version. Set status back to "awaiting-approval".',
|
|
988
|
+
status: 'pending', created: new Date().toISOString(), createdBy: 'dashboard:revision',
|
|
989
|
+
project: plan.project || '',
|
|
990
|
+
planFile: body.file,
|
|
991
|
+
});
|
|
992
|
+
safeWrite(wiPath, items);
|
|
993
|
+
return jsonReply(res, 200, { ok: true, status: 'revision-requested', workItemId: id });
|
|
994
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// POST /api/plans/discuss — generate a plan discussion session script
|
|
998
|
+
if (req.method === 'POST' && req.url === '/api/plans/discuss') {
|
|
999
|
+
try {
|
|
1000
|
+
const body = await readBody(req);
|
|
1001
|
+
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
1002
|
+
const planPath = path.join(SQUAD_DIR, 'plans', body.file);
|
|
1003
|
+
const planContent = safeRead(planPath);
|
|
1004
|
+
if (!planContent) return jsonReply(res, 404, { error: 'plan not found' });
|
|
1005
|
+
|
|
1006
|
+
const plan = JSON.parse(planContent);
|
|
1007
|
+
const projectName = plan.project || 'Unknown';
|
|
1008
|
+
|
|
1009
|
+
// Build the session launch script
|
|
1010
|
+
const sessionName = 'plan-review-' + body.file.replace(/\.json$/, '');
|
|
1011
|
+
const sysPrompt = `You are a Plan Advisor helping a human review and refine a feature plan before it gets dispatched to an agent squad.
|
|
1012
|
+
|
|
1013
|
+
## Your Role
|
|
1014
|
+
- Help the user understand, question, and refine the plan
|
|
1015
|
+
- Accept feedback and update the plan accordingly
|
|
1016
|
+
- When the user is satisfied, write the approved plan back to disk
|
|
1017
|
+
|
|
1018
|
+
## The Plan File
|
|
1019
|
+
Path: ${planPath}
|
|
1020
|
+
Project: ${projectName}
|
|
1021
|
+
|
|
1022
|
+
## How This Works
|
|
1023
|
+
1. The user will discuss the plan with you — answer questions, suggest changes
|
|
1024
|
+
2. When they want changes, update the plan items (add/remove/reorder/modify)
|
|
1025
|
+
3. When they say ANY of these (or similar intent):
|
|
1026
|
+
- "approve", "go", "ship it", "looks good", "lgtm"
|
|
1027
|
+
- "clear context and implement", "clear context and go"
|
|
1028
|
+
- "go build it", "start working", "dispatch", "execute"
|
|
1029
|
+
- "do it", "proceed", "let's go", "send it"
|
|
1030
|
+
|
|
1031
|
+
Then:
|
|
1032
|
+
a. Read the current plan file fresh from disk
|
|
1033
|
+
b. Update status to "approved", set approvedAt and approvedBy
|
|
1034
|
+
c. Write it back to ${planPath} using the Write tool
|
|
1035
|
+
d. Print exactly: "Plan approved and saved. The engine will dispatch work on the next tick. You can close this session."
|
|
1036
|
+
e. Then EXIT the session — use /exit or simply stop responding. The user does NOT need to interact further.
|
|
1037
|
+
|
|
1038
|
+
4. If they say "reject" or "cancel":
|
|
1039
|
+
- Update status to "rejected"
|
|
1040
|
+
- Write it back
|
|
1041
|
+
- Confirm and exit.
|
|
1042
|
+
|
|
1043
|
+
## Important
|
|
1044
|
+
- Always read the plan file fresh before writing (another process may have modified it)
|
|
1045
|
+
- Preserve all existing fields when writing back
|
|
1046
|
+
- Use the Write tool to save changes
|
|
1047
|
+
- You have full file access — you can also read the project codebase for context
|
|
1048
|
+
- When the user signals approval, ALWAYS write the file and exit. Do not ask for confirmation — their intent is clear.`;
|
|
1049
|
+
|
|
1050
|
+
const initialPrompt = `Here's the plan awaiting your review:
|
|
1051
|
+
|
|
1052
|
+
**${plan.plan_summary || body.file}**
|
|
1053
|
+
Project: ${projectName}
|
|
1054
|
+
Strategy: ${plan.branch_strategy || 'parallel'}
|
|
1055
|
+
Branch: ${plan.feature_branch || 'per-item'}
|
|
1056
|
+
Items: ${(plan.missing_features || []).length}
|
|
1057
|
+
|
|
1058
|
+
${(plan.missing_features || []).map((f, i) =>
|
|
1059
|
+
`${i + 1}. **${f.id}: ${f.name}** (${f.estimated_complexity}, ${f.priority})${f.depends_on?.length ? ' → depends on: ' + f.depends_on.join(', ') : ''}
|
|
1060
|
+
${f.description || ''}`
|
|
1061
|
+
).join('\n\n')}
|
|
1062
|
+
|
|
1063
|
+
${plan.open_questions?.length ? '\n**Open Questions:**\n' + plan.open_questions.map(q => '- ' + q).join('\n') : ''}
|
|
1064
|
+
|
|
1065
|
+
What would you like to discuss or change? When you're happy, say "approve" and I'll finalize it.`;
|
|
1066
|
+
|
|
1067
|
+
// Write session files
|
|
1068
|
+
const sessionDir = path.join(SQUAD_DIR, 'engine');
|
|
1069
|
+
const sysFile = path.join(sessionDir, `plan-discuss-sys-${Date.now()}.md`);
|
|
1070
|
+
const promptFile = path.join(sessionDir, `plan-discuss-prompt-${Date.now()}.md`);
|
|
1071
|
+
safeWrite(sysFile, sysPrompt);
|
|
1072
|
+
safeWrite(promptFile, initialPrompt);
|
|
1073
|
+
|
|
1074
|
+
// Generate the launch command
|
|
1075
|
+
const cmd = `claude --system-prompt "$(cat '${sysFile.replace(/\\/g, '/')}')" --name "${sessionName}" --add-dir "${SQUAD_DIR.replace(/\\/g, '/')}" < "${promptFile.replace(/\\/g, '/')}"`;
|
|
1076
|
+
|
|
1077
|
+
// Also generate a PowerShell-friendly version
|
|
1078
|
+
const psCmd = `Get-Content "${promptFile}" | claude --system-prompt (Get-Content "${sysFile}" -Raw) --name "${sessionName}" --add-dir "${SQUAD_DIR}"`;
|
|
1079
|
+
|
|
1080
|
+
return jsonReply(res, 200, {
|
|
1081
|
+
ok: true,
|
|
1082
|
+
sessionName,
|
|
1083
|
+
command: cmd,
|
|
1084
|
+
psCommand: psCmd,
|
|
1085
|
+
sysFile,
|
|
1086
|
+
promptFile,
|
|
1087
|
+
planFile: body.file,
|
|
1088
|
+
});
|
|
1089
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// POST /api/ask-about — ask a question about a document with context, answered by Haiku
|
|
1093
|
+
if (req.method === 'POST' && req.url === '/api/ask-about') {
|
|
1094
|
+
try {
|
|
1095
|
+
const body = await readBody(req);
|
|
1096
|
+
if (!body.question) return jsonReply(res, 400, { error: 'question required' });
|
|
1097
|
+
if (!body.document) return jsonReply(res, 400, { error: 'document required' });
|
|
1098
|
+
|
|
1099
|
+
const prompt = `You are answering a question about a document from a software engineering squad's knowledge base.
|
|
1100
|
+
|
|
1101
|
+
## Document
|
|
1102
|
+
${body.title ? '**Title:** ' + body.title + '\n' : ''}
|
|
1103
|
+
${body.document.slice(0, 15000)}
|
|
1104
|
+
|
|
1105
|
+
${body.selection ? '## Highlighted Selection\n\nThe user highlighted this specific part:\n> ' + body.selection.slice(0, 2000) + '\n' : ''}
|
|
1106
|
+
|
|
1107
|
+
## Question
|
|
1108
|
+
|
|
1109
|
+
${body.question}
|
|
1110
|
+
|
|
1111
|
+
## Instructions
|
|
1112
|
+
|
|
1113
|
+
Answer concisely and directly. Follow these rules:
|
|
1114
|
+
1. **Cite sources**: When the document includes file paths, line numbers, PR URLs, or code references — include them in your answer. Format: \`(source: path/to/file.ts:42)\`
|
|
1115
|
+
2. **Quote the document**: Reference the exact text that supports your answer.
|
|
1116
|
+
3. **Flag missing sources**: If the document makes a claim without a source reference (no file path, no PR link, no line number), say: "Note: this claim has no source reference in the document — verify independently."
|
|
1117
|
+
4. **Be honest**: If the document doesn't contain the answer, say so clearly. Don't speculate beyond what's written.
|
|
1118
|
+
5. Use markdown formatting.`;
|
|
1119
|
+
|
|
1120
|
+
const sysPrompt = 'You are a concise technical assistant. Answer based on the document provided. No preamble.';
|
|
1121
|
+
|
|
1122
|
+
// Write temp files
|
|
1123
|
+
const id = Date.now();
|
|
1124
|
+
const promptPath = path.join(SQUAD_DIR, 'engine', 'ask-prompt-' + id + '.md');
|
|
1125
|
+
const sysPath = path.join(SQUAD_DIR, 'engine', 'ask-sys-' + id + '.md');
|
|
1126
|
+
safeWrite(promptPath, prompt);
|
|
1127
|
+
safeWrite(sysPath, sysPrompt);
|
|
1128
|
+
|
|
1129
|
+
// Spawn Haiku
|
|
1130
|
+
const spawnScript = path.join(SQUAD_DIR, 'engine', 'spawn-agent.js');
|
|
1131
|
+
const childEnv = { ...process.env };
|
|
1132
|
+
for (const key of Object.keys(childEnv)) {
|
|
1133
|
+
if (key === 'CLAUDECODE' || key.startsWith('CLAUDE_CODE') || key.startsWith('CLAUDECODE_')) delete childEnv[key];
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const { spawn: cpSpawn } = require('child_process');
|
|
1137
|
+
const proc = cpSpawn(process.execPath, [
|
|
1138
|
+
spawnScript, promptPath, sysPath,
|
|
1139
|
+
'--output-format', 'text', '--max-turns', '1', '--model', 'haiku',
|
|
1140
|
+
'--permission-mode', 'bypassPermissions', '--verbose',
|
|
1141
|
+
], { cwd: SQUAD_DIR, stdio: ['pipe', 'pipe', 'pipe'], env: childEnv });
|
|
1142
|
+
|
|
1143
|
+
let stdout = '';
|
|
1144
|
+
let stderr = '';
|
|
1145
|
+
proc.stdout.on('data', d => { stdout += d.toString(); });
|
|
1146
|
+
proc.stderr.on('data', d => { stderr += d.toString(); });
|
|
1147
|
+
|
|
1148
|
+
// Timeout 60s
|
|
1149
|
+
const timeout = setTimeout(() => { try { proc.kill('SIGTERM'); } catch {} }, 60000);
|
|
1150
|
+
|
|
1151
|
+
proc.on('close', (code) => {
|
|
1152
|
+
clearTimeout(timeout);
|
|
1153
|
+
try { fs.unlinkSync(promptPath); } catch {}
|
|
1154
|
+
try { fs.unlinkSync(sysPath); } catch {}
|
|
1155
|
+
if (code === 0 && stdout.trim()) {
|
|
1156
|
+
return jsonReply(res, 200, { ok: true, answer: stdout.trim() });
|
|
1157
|
+
} else {
|
|
1158
|
+
return jsonReply(res, 500, { error: 'Failed to get answer', stderr: stderr.slice(0, 200) });
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
proc.on('error', (err) => {
|
|
1163
|
+
clearTimeout(timeout);
|
|
1164
|
+
try { fs.unlinkSync(promptPath); } catch {}
|
|
1165
|
+
try { fs.unlinkSync(sysPath); } catch {}
|
|
1166
|
+
return jsonReply(res, 500, { error: err.message });
|
|
1167
|
+
});
|
|
1168
|
+
return; // Don't fall through — response handled in callbacks
|
|
1169
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1170
|
+
}
|
|
1171
|
+
|
|
861
1172
|
// POST /api/inbox/persist — promote an inbox item to team notes
|
|
862
1173
|
if (req.method === 'POST' && req.url === '/api/inbox/persist') {
|
|
863
1174
|
try {
|
|
@@ -898,6 +1209,45 @@ const server = http.createServer(async (req, res) => {
|
|
|
898
1209
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
899
1210
|
}
|
|
900
1211
|
|
|
1212
|
+
// POST /api/inbox/promote-kb — promote an inbox item to the knowledge base
|
|
1213
|
+
if (req.method === 'POST' && req.url === '/api/inbox/promote-kb') {
|
|
1214
|
+
try {
|
|
1215
|
+
const body = await readBody(req);
|
|
1216
|
+
const { name, category } = body;
|
|
1217
|
+
if (!name) return jsonReply(res, 400, { error: 'name required' });
|
|
1218
|
+
const validCategories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
|
|
1219
|
+
if (!category || !validCategories.includes(category)) {
|
|
1220
|
+
return jsonReply(res, 400, { error: 'category required: ' + validCategories.join(', ') });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const inboxPath = path.join(SQUAD_DIR, 'notes', 'inbox', name);
|
|
1224
|
+
const content = safeRead(inboxPath);
|
|
1225
|
+
if (!content) return jsonReply(res, 404, { error: 'inbox item not found' });
|
|
1226
|
+
|
|
1227
|
+
// Add frontmatter if not present
|
|
1228
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1229
|
+
let kbContent = content;
|
|
1230
|
+
if (!content.startsWith('---')) {
|
|
1231
|
+
const titleMatch = content.match(/^#+ (.+)$/m);
|
|
1232
|
+
const title = titleMatch ? titleMatch[1].trim() : name.replace('.md', '');
|
|
1233
|
+
kbContent = `---\ntitle: ${title}\ncategory: ${category}\ndate: ${today}\nsource: inbox/${name}\n---\n\n${content}`;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// Write to knowledge base
|
|
1237
|
+
const kbDir = path.join(SQUAD_DIR, 'knowledge', category);
|
|
1238
|
+
if (!fs.existsSync(kbDir)) fs.mkdirSync(kbDir, { recursive: true });
|
|
1239
|
+
const kbFile = path.join(kbDir, name);
|
|
1240
|
+
safeWrite(kbFile, kbContent);
|
|
1241
|
+
|
|
1242
|
+
// Move inbox item to archive
|
|
1243
|
+
const archiveDir = path.join(SQUAD_DIR, 'notes', 'archive');
|
|
1244
|
+
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
1245
|
+
try { fs.renameSync(inboxPath, path.join(archiveDir, `kb-${category}-${name}`)); } catch {}
|
|
1246
|
+
|
|
1247
|
+
return jsonReply(res, 200, { ok: true, category, file: name });
|
|
1248
|
+
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
1249
|
+
}
|
|
1250
|
+
|
|
901
1251
|
// POST /api/inbox/open — open inbox file in Windows explorer
|
|
902
1252
|
if (req.method === 'POST' && req.url === '/api/inbox/open') {
|
|
903
1253
|
try {
|
|
@@ -87,7 +87,7 @@ When the engine restarts, the in-memory `activeProcesses` Map is lost. Active di
|
|
|
87
87
|
|
|
88
88
|
**Dispatch 1773292681199** — Dallas, central work item, auto-route:
|
|
89
89
|
|
|
90
|
-
1. Engine spawns `node spawn-agent.js prompt.md sysprompt.md --output-format stream-json --verbose --permission-mode bypassPermissions
|
|
90
|
+
1. Engine spawns `node spawn-agent.js prompt.md sysprompt.md --output-format stream-json --verbose --permission-mode bypassPermissions`
|
|
91
91
|
2. spawn-agent.js resolves `cli.js`, spawns `node cli.js -p --system-prompt <content> ...`
|
|
92
92
|
3. Prompt piped via stdin — no shell interpretation
|
|
93
93
|
4. MCP servers connect (azure-ado, azure-kusto, mobile, DevBox)
|
package/engine.js
CHANGED
|
@@ -212,39 +212,6 @@ function getInboxFiles() {
|
|
|
212
212
|
try { return fs.readdirSync(INBOX_DIR).filter(f => f.endsWith('.md')); } catch { return []; }
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
// ─── MCP Server Sync ─────────────────────────────────────────────────────────
|
|
216
|
-
|
|
217
|
-
const MCP_SERVERS_PATH = path.join(SQUAD_DIR, 'mcp-servers.json');
|
|
218
|
-
|
|
219
|
-
function syncMcpServers() {
|
|
220
|
-
// Sync MCP servers from ~/.claude.json into squad's mcp-servers.json
|
|
221
|
-
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
222
|
-
const claudeJsonPath = path.join(home, '.claude.json');
|
|
223
|
-
|
|
224
|
-
if (!fs.existsSync(claudeJsonPath)) {
|
|
225
|
-
console.log(' ~/.claude.json not found — skipping MCP sync');
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
|
|
231
|
-
const servers = claudeJson.mcpServers;
|
|
232
|
-
if (!servers || Object.keys(servers).length === 0) {
|
|
233
|
-
console.log(' No MCP servers found in ~/.claude.json');
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
safeWrite(MCP_SERVERS_PATH, { mcpServers: servers });
|
|
238
|
-
const names = Object.keys(servers);
|
|
239
|
-
console.log(` MCP servers synced (${names.length}): ${names.join(', ')}`);
|
|
240
|
-
log('info', `Synced ${names.length} MCP servers from ~/.claude.json: ${names.join(', ')}`);
|
|
241
|
-
return true;
|
|
242
|
-
} catch (e) {
|
|
243
|
-
console.log(` MCP sync failed: ${e.message}`);
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
215
|
// ─── Skills ──────────────────────────────────────────────────────────────────
|
|
249
216
|
|
|
250
217
|
const SKILLS_DIR = path.join(SQUAD_DIR, 'skills');
|
|
@@ -313,6 +280,32 @@ function getSkillIndex() {
|
|
|
313
280
|
} catch { return ''; }
|
|
314
281
|
}
|
|
315
282
|
|
|
283
|
+
function getKnowledgeBaseIndex() {
|
|
284
|
+
try {
|
|
285
|
+
const kbDir = path.join(SQUAD_DIR, 'knowledge');
|
|
286
|
+
if (!fs.existsSync(kbDir)) return '';
|
|
287
|
+
const categories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
|
|
288
|
+
let entries = [];
|
|
289
|
+
for (const cat of categories) {
|
|
290
|
+
const catDir = path.join(kbDir, cat);
|
|
291
|
+
const files = safeReadDir(catDir).filter(f => f.endsWith('.md'));
|
|
292
|
+
for (const f of files) {
|
|
293
|
+
const content = safeRead(path.join(catDir, f)) || '';
|
|
294
|
+
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
295
|
+
const title = titleMatch ? titleMatch[1].trim() : f.replace(/\.md$/, '');
|
|
296
|
+
entries.push({ cat, file: f, title });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (entries.length === 0) return '';
|
|
300
|
+
let index = '## Knowledge Base Reference\n\n';
|
|
301
|
+
index += 'Deep-reference docs from past work. Read the file if you need detail.\n\n';
|
|
302
|
+
for (const e of entries) {
|
|
303
|
+
index += `- \`knowledge/${e.cat}/${e.file}\` — ${e.title}\n`;
|
|
304
|
+
}
|
|
305
|
+
return index + '\n';
|
|
306
|
+
} catch { return ''; }
|
|
307
|
+
}
|
|
308
|
+
|
|
316
309
|
function getPrs(project) {
|
|
317
310
|
if (project) {
|
|
318
311
|
const prPath = project.workSources?.pullRequests?.path;
|
|
@@ -412,7 +405,8 @@ function renderPlaybook(type, vars) {
|
|
|
412
405
|
content += `- What you learned about the codebase\n`;
|
|
413
406
|
content += `- Patterns you discovered or established\n`;
|
|
414
407
|
content += `- Gotchas or warnings for future agents\n`;
|
|
415
|
-
content += `- Conventions to follow\n
|
|
408
|
+
content += `- Conventions to follow\n`;
|
|
409
|
+
content += `- **SOURCE REFERENCES for every finding** — file paths with line numbers, PR URLs, API endpoints, config keys. Format: \`(source: path/to/file.ts:42)\` or \`(source: PR-12345)\`. Without references, findings cannot be verified.\n\n`;
|
|
416
410
|
content += `### Skill Extraction (IMPORTANT)\n\n`;
|
|
417
411
|
content += `If during this task you discovered a **repeatable workflow** — a multi-step procedure, workaround, build process, or pattern that other agents should follow in similar situations — output it as a fenced skill block. The engine will automatically extract it.\n\n`;
|
|
418
412
|
content += `Format your skill as a fenced code block with the \`skill\` language tag:\n\n`;
|
|
@@ -574,6 +568,12 @@ function buildAgentContext(agentId, config, project) {
|
|
|
574
568
|
context += skillIndex + '\n';
|
|
575
569
|
}
|
|
576
570
|
|
|
571
|
+
// Knowledge base index (paths + titles only — agents can Read if needed)
|
|
572
|
+
const kbIndex = getKnowledgeBaseIndex();
|
|
573
|
+
if (kbIndex) {
|
|
574
|
+
context += kbIndex + '\n';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
577
|
// Team notes (the big one — can be 50KB)
|
|
578
578
|
if (notes) {
|
|
579
579
|
context += `## Team Notes (MUST READ)\n\n${notes}\n\n`;
|
|
@@ -669,11 +669,8 @@ function spawnAgent(dispatchItem, config) {
|
|
|
669
669
|
args.push('--allowedTools', claudeConfig.allowedTools);
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
-
// MCP servers
|
|
673
|
-
|
|
674
|
-
if (fs.existsSync(mcpConfigPath)) {
|
|
675
|
-
args.push('--mcp-config', mcpConfigPath);
|
|
676
|
-
}
|
|
672
|
+
// MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
|
|
673
|
+
// No --mcp-config needed — avoids redundant config and ensures agents always have latest servers.
|
|
677
674
|
|
|
678
675
|
log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
|
|
679
676
|
log('info', `Task type: ${type} | Branch: ${branchName || 'none'}`);
|
|
@@ -3083,6 +3080,13 @@ function materializePlansAsWorkItems(config) {
|
|
|
3083
3080
|
const plan = safeJson(path.join(PLANS_DIR, file));
|
|
3084
3081
|
if (!plan?.missing_features) continue;
|
|
3085
3082
|
|
|
3083
|
+
// Human approval gate: plans start as 'awaiting-approval' and must be approved before work begins
|
|
3084
|
+
// Plans without a status (legacy) or with status 'approved' are allowed through
|
|
3085
|
+
const planStatus = plan.status || (plan.requires_approval ? 'awaiting-approval' : null);
|
|
3086
|
+
if (planStatus === 'awaiting-approval' || planStatus === 'rejected' || planStatus === 'revision-requested') {
|
|
3087
|
+
continue; // Skip — waiting for human approval or revision
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3086
3090
|
const projectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
3087
3091
|
const project = getProjects(config).find(p => p.name?.toLowerCase() === projectName.toLowerCase());
|
|
3088
3092
|
if (!project) continue;
|
|
@@ -3973,7 +3977,7 @@ async function tickInner() {
|
|
|
3973
3977
|
// 2. Consolidate inbox
|
|
3974
3978
|
consolidateInbox(config);
|
|
3975
3979
|
|
|
3976
|
-
// 2.5. Periodic cleanup (every 10 ticks = ~5 minutes)
|
|
3980
|
+
// 2.5. Periodic cleanup + MCP sync (every 10 ticks = ~5 minutes)
|
|
3977
3981
|
if (tickCount % 10 === 0) {
|
|
3978
3982
|
runCleanup(config);
|
|
3979
3983
|
}
|
|
@@ -4058,9 +4062,6 @@ const commands = {
|
|
|
4058
4062
|
const config = getConfig();
|
|
4059
4063
|
const interval = config.engine?.tickInterval || 60000;
|
|
4060
4064
|
|
|
4061
|
-
// Sync MCP servers from Claude Code
|
|
4062
|
-
syncMcpServers();
|
|
4063
|
-
|
|
4064
4065
|
// Validate project paths
|
|
4065
4066
|
const projects = getProjects(config);
|
|
4066
4067
|
for (const p of projects) {
|
|
@@ -4535,7 +4536,7 @@ const commands = {
|
|
|
4535
4536
|
},
|
|
4536
4537
|
|
|
4537
4538
|
'mcp-sync'() {
|
|
4538
|
-
|
|
4539
|
+
console.log('MCP servers are read directly from ~/.claude.json — no sync needed.');
|
|
4539
4540
|
},
|
|
4540
4541
|
|
|
4541
4542
|
discover() {
|
package/package.json
CHANGED
package/playbooks/explore.md
CHANGED
|
@@ -38,6 +38,7 @@ Write your findings to `{{team_root}}/notes/inbox/{{agent_id}}-explore-{{task_id
|
|
|
38
38
|
- **Dependencies**: what depends on what
|
|
39
39
|
- **Gaps**: anything missing, broken, or unclear
|
|
40
40
|
- **Recommendations**: suggestions for the team
|
|
41
|
+
- **Source References**: for EVERY finding, include the source — file paths, line numbers, PR URLs, API endpoints, config keys. Format: `(source: path/to/file.ts:42)` or `(source: PR-12345)`. This is critical — other agents and humans need to verify your findings.
|
|
41
42
|
|
|
42
43
|
### 5. Create Deliverable (if the task asks for one)
|
|
43
44
|
If the task asks you to write a design doc, architecture doc, or any durable artifact:
|
package/playbooks/plan-to-prd.md
CHANGED
|
@@ -33,6 +33,8 @@ This file is NOT checked into the repo. The engine reads it on every tick and di
|
|
|
33
33
|
"generated_by": "{{agent_id}}",
|
|
34
34
|
"generated_at": "{{date}}",
|
|
35
35
|
"plan_summary": "{{plan_summary}}",
|
|
36
|
+
"status": "awaiting-approval",
|
|
37
|
+
"requires_approval": true,
|
|
36
38
|
"branch_strategy": "shared-branch|parallel",
|
|
37
39
|
"feature_branch": "feat/plan-short-name",
|
|
38
40
|
"missing_features": [
|