@yemi33/squad 0.1.5 → 0.1.6
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.html +117 -11
- package/dashboard.js +95 -10
- package/docs/self-improvement.md +25 -1
- package/engine.js +535 -43
- package/package.json +1 -1
- package/playbooks/ask.md +49 -0
- package/playbooks/implement-shared.md +68 -0
- package/playbooks/implement.md +13 -0
- package/playbooks/plan-to-prd.md +22 -1
- package/playbooks/plan.md +99 -0
- package/routing.md +2 -0
package/engine.js
CHANGED
|
@@ -600,16 +600,29 @@ function spawnAgent(dispatchItem, config) {
|
|
|
600
600
|
worktreePath = path.resolve(rootDir, engineConfig.worktreeRoot || '../worktrees', branchName);
|
|
601
601
|
try {
|
|
602
602
|
if (!fs.existsSync(worktreePath)) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
603
|
+
const isSharedBranch = meta?.branchStrategy === 'shared-branch' || meta?.useExistingBranch;
|
|
604
|
+
if (isSharedBranch) {
|
|
605
|
+
// Shared branch: fetch and checkout existing branch (no -b)
|
|
606
|
+
log('info', `Creating worktree for shared branch: ${worktreePath} on ${branchName}`);
|
|
607
|
+
try { execSync(`git fetch origin "${branchName}"`, { cwd: rootDir, stdio: 'pipe' }); } catch {}
|
|
608
|
+
execSync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: rootDir, stdio: 'pipe' });
|
|
609
|
+
} else {
|
|
610
|
+
// Parallel: create new branch
|
|
611
|
+
log('info', `Creating worktree: ${worktreePath} on branch ${branchName}`);
|
|
612
|
+
execSync(`git worktree add "${worktreePath}" -b "${branchName}" ${sanitizeBranch(project.mainBranch || 'main')}`, {
|
|
613
|
+
cwd: rootDir, stdio: 'pipe'
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
} else if (meta?.branchStrategy === 'shared-branch') {
|
|
617
|
+
// Worktree exists — pull latest from prior plan item
|
|
618
|
+
log('info', `Pulling latest on shared branch ${branchName}`);
|
|
619
|
+
try { execSync(`git pull origin "${branchName}"`, { cwd: worktreePath, stdio: 'pipe' }); } catch {}
|
|
607
620
|
}
|
|
608
621
|
cwd = worktreePath;
|
|
609
622
|
} catch (err) {
|
|
610
623
|
log('error', `Failed to create worktree for ${branchName}: ${err.message}`);
|
|
611
624
|
// Fall back to main directory for non-writing tasks
|
|
612
|
-
if (type === 'review' || type === 'analyze' || type === 'plan-to-prd') {
|
|
625
|
+
if (type === 'review' || type === 'analyze' || type === 'plan-to-prd' || type === 'plan') {
|
|
613
626
|
cwd = rootDir;
|
|
614
627
|
} else {
|
|
615
628
|
completeDispatch(id, 'error', 'Worktree creation failed');
|
|
@@ -730,6 +743,16 @@ function spawnAgent(dispatchItem, config) {
|
|
|
730
743
|
updateWorkItemStatus(meta, 'done', '');
|
|
731
744
|
}
|
|
732
745
|
|
|
746
|
+
// Post-completion: chain plan → plan-to-prd if this was a plan task
|
|
747
|
+
if (code === 0 && type === 'plan' && meta?.item?.chain === 'plan-to-prd') {
|
|
748
|
+
chainPlanToPrd(dispatchItem, meta, config);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Post-completion: check shared-branch plan completion
|
|
752
|
+
if (code === 0 && meta?.item?.branchStrategy === 'shared-branch') {
|
|
753
|
+
checkPlanCompletion(meta, config);
|
|
754
|
+
}
|
|
755
|
+
|
|
733
756
|
// Post-completion: scan output for PRs and sync to pull-requests.json
|
|
734
757
|
if (code === 0) {
|
|
735
758
|
syncPrsFromOutput(stdout, agentId, meta, config);
|
|
@@ -872,6 +895,223 @@ function completeDispatch(id, result = 'success', reason = '') {
|
|
|
872
895
|
}
|
|
873
896
|
}
|
|
874
897
|
|
|
898
|
+
// ─── Dependency Gate ─────────────────────────────────────────────────────────
|
|
899
|
+
function areDependenciesMet(item, config) {
|
|
900
|
+
const deps = item.depends_on;
|
|
901
|
+
if (!deps || deps.length === 0) return true;
|
|
902
|
+
const sourcePlan = item.sourcePlan;
|
|
903
|
+
if (!sourcePlan) return true;
|
|
904
|
+
const projectName = item.project;
|
|
905
|
+
const projects = getProjects(config);
|
|
906
|
+
const project = projectName
|
|
907
|
+
? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase())
|
|
908
|
+
: projects[0];
|
|
909
|
+
if (!project) return true;
|
|
910
|
+
const wiPath = projectWorkItemsPath(project);
|
|
911
|
+
const workItems = safeJson(wiPath) || [];
|
|
912
|
+
for (const depId of deps) {
|
|
913
|
+
const depItem = workItems.find(w => w.sourcePlan === sourcePlan && w.planItemId === depId);
|
|
914
|
+
if (!depItem || depItem.status !== 'done') return false;
|
|
915
|
+
}
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function detectDependencyCycles(items) {
|
|
920
|
+
const graph = new Map();
|
|
921
|
+
for (const item of items) graph.set(item.id, item.depends_on || []);
|
|
922
|
+
const visited = new Set(), inStack = new Set(), cycleIds = new Set();
|
|
923
|
+
function dfs(id) {
|
|
924
|
+
if (inStack.has(id)) { cycleIds.add(id); return true; }
|
|
925
|
+
if (visited.has(id)) return false;
|
|
926
|
+
visited.add(id); inStack.add(id);
|
|
927
|
+
for (const dep of (graph.get(id) || [])) { if (dfs(dep)) cycleIds.add(id); }
|
|
928
|
+
inStack.delete(id); return false;
|
|
929
|
+
}
|
|
930
|
+
for (const id of graph.keys()) dfs(id);
|
|
931
|
+
return [...cycleIds];
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ─── Plan Completion Detection ───────────────────────────────────────────────
|
|
935
|
+
function checkPlanCompletion(meta, config) {
|
|
936
|
+
const planFile = meta.item?.sourcePlan;
|
|
937
|
+
if (!planFile) return;
|
|
938
|
+
const plan = safeJson(path.join(PLANS_DIR, planFile));
|
|
939
|
+
if (!plan?.missing_features || plan.branch_strategy !== 'shared-branch') return;
|
|
940
|
+
if (plan.status === 'completed') return;
|
|
941
|
+
const projectName = plan.project;
|
|
942
|
+
const projects = getProjects(config);
|
|
943
|
+
const project = projectName
|
|
944
|
+
? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
|
|
945
|
+
if (!project) return;
|
|
946
|
+
const wiPath = projectWorkItemsPath(project);
|
|
947
|
+
const workItems = safeJson(wiPath) || [];
|
|
948
|
+
const planItems = workItems.filter(w => w.sourcePlan === planFile && w.planItemId !== 'PR');
|
|
949
|
+
if (planItems.length === 0) return;
|
|
950
|
+
if (!planItems.every(w => w.status === 'done')) return;
|
|
951
|
+
log('info', `All ${planItems.length} items in plan ${planFile} completed — creating PR work item`);
|
|
952
|
+
const maxNum = workItems.reduce((max, i) => {
|
|
953
|
+
const m = (i.id || '').match(/(\d+)$/);
|
|
954
|
+
return m ? Math.max(max, parseInt(m[1])) : max;
|
|
955
|
+
}, 0);
|
|
956
|
+
const id = 'PL-W' + String(maxNum + 1).padStart(3, '0');
|
|
957
|
+
const featureBranch = plan.feature_branch;
|
|
958
|
+
const mainBranch = project.mainBranch || 'main';
|
|
959
|
+
const itemSummary = planItems.map(w => '- ' + w.planItemId + ': ' + w.title.replace('Implement: ', '')).join('\n');
|
|
960
|
+
workItems.push({
|
|
961
|
+
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
962
|
+
type: 'implement', priority: 'high',
|
|
963
|
+
description: `All plan items from \`${planFile}\` are complete on branch \`${featureBranch}\`.
|
|
964
|
+
Create a pull request and clean up the worktree.
|
|
965
|
+
|
|
966
|
+
**Branch:** \`${featureBranch}\`
|
|
967
|
+
**Target:** \`${mainBranch}\`
|
|
968
|
+
|
|
969
|
+
## Completed Items
|
|
970
|
+
${itemSummary}
|
|
971
|
+
|
|
972
|
+
## Instructions
|
|
973
|
+
1. Rebase onto ${mainBranch} if needed (abort if conflicts — PR as-is is fine)
|
|
974
|
+
2. Push: git push origin ${featureBranch} --force-with-lease
|
|
975
|
+
3. Create the PR targeting ${mainBranch}
|
|
976
|
+
4. Cleanup: git worktree remove ../worktrees/${featureBranch} --force`,
|
|
977
|
+
status: 'pending', created: ts(), createdBy: 'engine:plan-completion',
|
|
978
|
+
sourcePlan: planFile, planItemId: 'PR',
|
|
979
|
+
branch: featureBranch, branchStrategy: 'shared-branch', project: projectName,
|
|
980
|
+
});
|
|
981
|
+
safeWrite(wiPath, workItems);
|
|
982
|
+
plan.status = 'completed'; plan.completedAt = ts();
|
|
983
|
+
safeWrite(path.join(PLANS_DIR, planFile), plan);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ─── Plan → PRD Chaining ─────────────────────────────────────────────────────
|
|
987
|
+
// When a 'plan' type task completes, find the plan file it wrote and dispatch
|
|
988
|
+
// a plan-to-prd task so Lambert converts it to structured PRD items.
|
|
989
|
+
function chainPlanToPrd(dispatchItem, meta, config) {
|
|
990
|
+
const planDir = path.join(SQUAD_DIR, 'plans');
|
|
991
|
+
if (!fs.existsSync(planDir)) {
|
|
992
|
+
log('warn', `Plan chaining: no plans/ directory found after plan task ${dispatchItem.id}`);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Find the plan file — look for recently created .md files in plans/
|
|
997
|
+
const planFiles = fs.readdirSync(planDir)
|
|
998
|
+
.filter(f => f.endsWith('.md'))
|
|
999
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(planDir, f)).mtimeMs }))
|
|
1000
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
1001
|
+
|
|
1002
|
+
// Use the most recently modified plan file (the one the plan agent just wrote)
|
|
1003
|
+
const planFile = planFiles[0];
|
|
1004
|
+
if (!planFile) {
|
|
1005
|
+
log('warn', `Plan chaining: no .md plan files found in plans/ after task ${dispatchItem.id}`);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const planPath = path.join(planDir, planFile.name);
|
|
1010
|
+
let planContent;
|
|
1011
|
+
try { planContent = fs.readFileSync(planPath, 'utf8'); } catch (e) {
|
|
1012
|
+
log('error', `Plan chaining: failed to read plan file ${planFile.name}: ${e.message}`);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Resolve target project
|
|
1017
|
+
const projectName = meta?.item?.project || meta?.project?.name;
|
|
1018
|
+
const projects = getProjects(config);
|
|
1019
|
+
const targetProject = projectName
|
|
1020
|
+
? projects.find(p => p.name === projectName) || projects[0]
|
|
1021
|
+
: projects[0];
|
|
1022
|
+
|
|
1023
|
+
if (!targetProject) {
|
|
1024
|
+
log('error', 'Plan chaining: no target project available');
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Resolve agent for plan-to-prd (typically Lambert)
|
|
1029
|
+
const agentId = resolveAgent('plan-to-prd', config);
|
|
1030
|
+
if (!agentId) {
|
|
1031
|
+
// No agent available now — create a pending work item so engine picks it up on next tick
|
|
1032
|
+
log('info', `Plan chaining: no agent available now, queuing plan-to-prd for next tick`);
|
|
1033
|
+
const wiPath = path.join(SQUAD_DIR, 'work-items.json');
|
|
1034
|
+
let items = [];
|
|
1035
|
+
try { items = JSON.parse(fs.readFileSync(wiPath, 'utf8')); } catch {}
|
|
1036
|
+
const maxNum = items.reduce((max, i) => {
|
|
1037
|
+
const m = (i.id || '').match(/(\d+)$/);
|
|
1038
|
+
return m ? Math.max(max, parseInt(m[1])) : max;
|
|
1039
|
+
}, 0);
|
|
1040
|
+
items.push({
|
|
1041
|
+
id: 'W' + String(maxNum + 1).padStart(3, '0'),
|
|
1042
|
+
title: `Convert plan to PRD: ${meta?.item?.title || planFile.name}`,
|
|
1043
|
+
type: 'plan-to-prd',
|
|
1044
|
+
priority: meta?.item?.priority || 'high',
|
|
1045
|
+
description: `Plan file: plans/${planFile.name}\nChained from plan task ${dispatchItem.id}`,
|
|
1046
|
+
status: 'pending',
|
|
1047
|
+
created: ts(),
|
|
1048
|
+
createdBy: 'engine:chain',
|
|
1049
|
+
project: targetProject.name,
|
|
1050
|
+
planFile: planFile.name,
|
|
1051
|
+
});
|
|
1052
|
+
safeWrite(wiPath, items);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Build plan-to-prd vars and dispatch immediately
|
|
1057
|
+
const vars = {
|
|
1058
|
+
agent_id: agentId,
|
|
1059
|
+
agent_name: config.agents[agentId]?.name || agentId,
|
|
1060
|
+
agent_role: config.agents[agentId]?.role || '',
|
|
1061
|
+
project_name: targetProject.name || 'Unknown',
|
|
1062
|
+
project_path: targetProject.localPath || '',
|
|
1063
|
+
main_branch: targetProject.mainBranch || 'main',
|
|
1064
|
+
ado_org: targetProject.adoOrg || config.project?.adoOrg || 'Unknown',
|
|
1065
|
+
ado_project: targetProject.adoProject || config.project?.adoProject || 'Unknown',
|
|
1066
|
+
repo_name: targetProject.repoName || config.project?.repoName || 'Unknown',
|
|
1067
|
+
team_root: SQUAD_DIR,
|
|
1068
|
+
date: dateStamp(),
|
|
1069
|
+
plan_content: planContent,
|
|
1070
|
+
plan_summary: (meta?.item?.title || planFile.name).substring(0, 80),
|
|
1071
|
+
project_name_lower: (targetProject.name || 'project').toLowerCase(),
|
|
1072
|
+
branch_strategy_hint: meta?.item?.branchStrategy
|
|
1073
|
+
? `The user requested **${meta.item.branchStrategy}** strategy. Use this unless the analysis strongly suggests otherwise.`
|
|
1074
|
+
: 'Choose the best strategy based on your analysis of item dependencies.',
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true });
|
|
1078
|
+
|
|
1079
|
+
const prompt = renderPlaybook('plan-to-prd', vars);
|
|
1080
|
+
if (!prompt) {
|
|
1081
|
+
log('error', 'Plan chaining: could not render plan-to-prd playbook');
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const id = addToDispatch({
|
|
1086
|
+
type: 'plan-to-prd',
|
|
1087
|
+
agent: agentId,
|
|
1088
|
+
agentName: config.agents[agentId]?.name,
|
|
1089
|
+
agentRole: config.agents[agentId]?.role,
|
|
1090
|
+
task: `[${targetProject.name}] Convert plan to PRD: ${vars.plan_summary}`,
|
|
1091
|
+
prompt,
|
|
1092
|
+
meta: {
|
|
1093
|
+
source: 'chain',
|
|
1094
|
+
chainedFrom: dispatchItem.id,
|
|
1095
|
+
project: { name: targetProject.name, localPath: targetProject.localPath },
|
|
1096
|
+
planFile: planFile.name,
|
|
1097
|
+
planSummary: vars.plan_summary,
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
log('info', `Plan chaining: dispatched plan-to-prd ${id} → ${config.agents[agentId]?.name} (chained from ${dispatchItem.id})`);
|
|
1102
|
+
|
|
1103
|
+
// Spawn immediately if engine is running
|
|
1104
|
+
const control = getControl();
|
|
1105
|
+
if (control.state === 'running') {
|
|
1106
|
+
const dispatch = getDispatch();
|
|
1107
|
+
const item = dispatch.pending.find(d => d.id === id);
|
|
1108
|
+
if (item) {
|
|
1109
|
+
spawnAgent(item, config);
|
|
1110
|
+
log('info', `Plan chaining: agent spawned immediately for ${id}`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
875
1115
|
function updateWorkItemStatus(meta, status, reason) {
|
|
876
1116
|
const itemId = meta.item?.id;
|
|
877
1117
|
if (!itemId) return;
|
|
@@ -1334,6 +1574,114 @@ async function pollPrStatus(config) {
|
|
|
1334
1574
|
}
|
|
1335
1575
|
}
|
|
1336
1576
|
|
|
1577
|
+
// ─── Poll Human Comments on PRs ──────────────────────────────────────────────
|
|
1578
|
+
|
|
1579
|
+
async function pollPrHumanComments(config) {
|
|
1580
|
+
const token = getAdoToken();
|
|
1581
|
+
if (!token) return;
|
|
1582
|
+
|
|
1583
|
+
const projects = getProjects(config);
|
|
1584
|
+
let totalUpdated = 0;
|
|
1585
|
+
|
|
1586
|
+
for (const project of projects) {
|
|
1587
|
+
if (!project.adoOrg || !project.adoProject || !project.repositoryId) continue;
|
|
1588
|
+
|
|
1589
|
+
const prs = getPrs(project);
|
|
1590
|
+
const activePrs = prs.filter(pr => pr.status === 'active');
|
|
1591
|
+
if (activePrs.length === 0) continue;
|
|
1592
|
+
|
|
1593
|
+
let projectUpdated = 0;
|
|
1594
|
+
|
|
1595
|
+
for (const pr of activePrs) {
|
|
1596
|
+
const prNum = (pr.id || '').replace('PR-', '');
|
|
1597
|
+
if (!prNum) continue;
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
let orgBase;
|
|
1601
|
+
if (project.prUrlBase) {
|
|
1602
|
+
const m = project.prUrlBase.match(/^(https?:\/\/[^/]+(?:\/DefaultCollection)?)/);
|
|
1603
|
+
if (m) orgBase = m[1];
|
|
1604
|
+
}
|
|
1605
|
+
if (!orgBase) {
|
|
1606
|
+
orgBase = project.adoOrg.includes('.')
|
|
1607
|
+
? `https://${project.adoOrg}`
|
|
1608
|
+
: `https://dev.azure.com/${project.adoOrg}`;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const threadsUrl = `${orgBase}/${project.adoProject}/_apis/git/repositories/${project.repositoryId}/pullrequests/${prNum}/threads?api-version=7.1`;
|
|
1612
|
+
const threadsData = await adoFetch(threadsUrl, token);
|
|
1613
|
+
const threads = threadsData.value || [];
|
|
1614
|
+
|
|
1615
|
+
// First pass: count all unique human commenters across the entire PR
|
|
1616
|
+
const allHumanAuthors = new Set();
|
|
1617
|
+
for (const thread of threads) {
|
|
1618
|
+
for (const comment of (thread.comments || [])) {
|
|
1619
|
+
if (!comment.content || comment.commentType === 'system') continue;
|
|
1620
|
+
if (/\bSquad\s*\(/i.test(comment.content)) continue;
|
|
1621
|
+
allHumanAuthors.add(comment.author?.uniqueName || comment.author?.displayName || '');
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
const soloReviewer = allHumanAuthors.size <= 1;
|
|
1625
|
+
|
|
1626
|
+
// Second pass: collect new human comments after cutoff
|
|
1627
|
+
const cutoff = pr.humanFeedback?.lastProcessedCommentDate || pr.created || '1970-01-01';
|
|
1628
|
+
const newHumanComments = [];
|
|
1629
|
+
|
|
1630
|
+
for (const thread of threads) {
|
|
1631
|
+
for (const comment of (thread.comments || [])) {
|
|
1632
|
+
if (!comment.content || comment.commentType === 'system') continue;
|
|
1633
|
+
if (/\bSquad\s*\(/i.test(comment.content)) continue;
|
|
1634
|
+
if (!(comment.publishedDate && comment.publishedDate > cutoff)) continue;
|
|
1635
|
+
|
|
1636
|
+
// Solo reviewer: all comments are actionable. Multiple humans: require @squad.
|
|
1637
|
+
if (!soloReviewer && !/@squad\b/i.test(comment.content)) continue;
|
|
1638
|
+
|
|
1639
|
+
newHumanComments.push({
|
|
1640
|
+
threadId: thread.id,
|
|
1641
|
+
commentId: comment.id,
|
|
1642
|
+
author: comment.author?.displayName || 'Human',
|
|
1643
|
+
content: comment.content,
|
|
1644
|
+
date: comment.publishedDate
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
if (newHumanComments.length === 0) continue;
|
|
1650
|
+
|
|
1651
|
+
// Sort by date, concatenate feedback
|
|
1652
|
+
newHumanComments.sort((a, b) => a.date.localeCompare(b.date));
|
|
1653
|
+
const feedbackContent = newHumanComments
|
|
1654
|
+
.map(c => `**${c.author}** (${c.date}):\n${c.content.replace(/@squad\s*/gi, '').trim()}`)
|
|
1655
|
+
.join('\n\n---\n\n');
|
|
1656
|
+
const latestDate = newHumanComments[newHumanComments.length - 1].date;
|
|
1657
|
+
|
|
1658
|
+
pr.humanFeedback = {
|
|
1659
|
+
lastProcessedCommentDate: latestDate,
|
|
1660
|
+
pendingFix: true,
|
|
1661
|
+
feedbackContent
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
log('info', `PR ${pr.id}: found ${newHumanComments.length} new human comment(s)${soloReviewer ? '' : ' (via @squad)'}`);
|
|
1665
|
+
projectUpdated++;
|
|
1666
|
+
} catch (e) {
|
|
1667
|
+
log('warn', `Failed to poll comments for ${pr.id}: ${e.message}`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
if (projectUpdated > 0) {
|
|
1672
|
+
const root = path.resolve(project.localPath);
|
|
1673
|
+
const prSrc = project.workSources?.pullRequests || {};
|
|
1674
|
+
const prPath = path.resolve(root, prSrc.path || '.squad/pull-requests.json');
|
|
1675
|
+
safeWrite(prPath, prs);
|
|
1676
|
+
totalUpdated += projectUpdated;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (totalUpdated > 0) {
|
|
1681
|
+
log('info', `PR comment poll: found human feedback on ${totalUpdated} PR(s)`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1337
1685
|
// ─── Post-Merge / Post-Close Hooks ───────────────────────────────────────────
|
|
1338
1686
|
|
|
1339
1687
|
async function handlePostMerge(pr, project, config, newStatus) {
|
|
@@ -1358,11 +1706,9 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1358
1706
|
// Only run remaining hooks for merged PRs (not abandoned)
|
|
1359
1707
|
if (newStatus !== 'merged') return;
|
|
1360
1708
|
|
|
1361
|
-
// 2. Update PRD item status to 'implemented'
|
|
1709
|
+
// 2. Update PRD item status to 'implemented' (squad-level PRD)
|
|
1362
1710
|
if (pr.prdItems?.length > 0) {
|
|
1363
|
-
const
|
|
1364
|
-
const prdSrc = project.workSources?.prd || {};
|
|
1365
|
-
const prdPath = path.resolve(root, prdSrc.path || 'docs/prd-gaps.json');
|
|
1711
|
+
const prdPath = path.join(SQUAD_DIR, 'prd.json');
|
|
1366
1712
|
const prd = safeJson(prdPath);
|
|
1367
1713
|
if (prd?.missing_features) {
|
|
1368
1714
|
let updated = 0;
|
|
@@ -2191,33 +2537,49 @@ function isAlreadyDispatched(key) {
|
|
|
2191
2537
|
}
|
|
2192
2538
|
|
|
2193
2539
|
/**
|
|
2194
|
-
* Scan PRD (
|
|
2540
|
+
* Scan squad-level PRD (~/.squad/prd.json) for missing/planned items → queue implement tasks.
|
|
2541
|
+
* Items can span multiple projects via the `projects` array field.
|
|
2195
2542
|
*/
|
|
2196
|
-
function discoverFromPrd(config
|
|
2197
|
-
const
|
|
2198
|
-
if (!src?.enabled) return [];
|
|
2199
|
-
|
|
2200
|
-
const root = project?.localPath ? path.resolve(project.localPath) : path.resolve(SQUAD_DIR, '..');
|
|
2201
|
-
const prdPath = path.resolve(root, src.path);
|
|
2543
|
+
function discoverFromPrd(config) {
|
|
2544
|
+
const prdPath = path.join(SQUAD_DIR, 'prd.json');
|
|
2202
2545
|
const prd = safeJson(prdPath);
|
|
2203
2546
|
if (!prd) return [];
|
|
2204
2547
|
|
|
2205
|
-
const cooldownMs = (
|
|
2206
|
-
const statusFilter =
|
|
2548
|
+
const cooldownMs = (config.workSources?.prd?.cooldownMinutes || 30) * 60 * 1000;
|
|
2549
|
+
const statusFilter = config.workSources?.prd?.itemFilter?.status || ['missing', 'planned'];
|
|
2207
2550
|
const items = (prd.missing_features || []).filter(f => statusFilter.includes(f.status));
|
|
2208
2551
|
const newWork = [];
|
|
2209
|
-
const skipped = { dispatched: 0, cooldown: 0, noAgent: 0 };
|
|
2552
|
+
const skipped = { dispatched: 0, cooldown: 0, noAgent: 0, noProject: 0 };
|
|
2553
|
+
const allProjects = config.projects || [];
|
|
2210
2554
|
|
|
2211
2555
|
for (const item of items) {
|
|
2212
|
-
const key = `prd-${
|
|
2556
|
+
const key = `prd-squad-${item.id}`;
|
|
2213
2557
|
if (isAlreadyDispatched(key)) { skipped.dispatched++; continue; }
|
|
2214
2558
|
if (isOnCooldown(key, cooldownMs)) { skipped.cooldown++; continue; }
|
|
2215
2559
|
|
|
2560
|
+
// Resolve target projects from item.projects array
|
|
2561
|
+
const targetProjects = (item.projects || [])
|
|
2562
|
+
.map(name => allProjects.find(p => p.name.toLowerCase() === name.toLowerCase()))
|
|
2563
|
+
.filter(Boolean);
|
|
2564
|
+
if (targetProjects.length === 0) { skipped.noProject++; continue; }
|
|
2565
|
+
|
|
2216
2566
|
const workType = item.estimated_complexity === 'large' ? 'implement:large' : 'implement';
|
|
2217
2567
|
const agentId = resolveAgent(workType, config);
|
|
2218
2568
|
if (!agentId) { skipped.noAgent++; continue; }
|
|
2219
2569
|
|
|
2570
|
+
// Primary project = first in the list (used for worktree, branch, PR)
|
|
2571
|
+
const primary = targetProjects[0];
|
|
2572
|
+
const root = path.resolve(primary.localPath);
|
|
2220
2573
|
const branchName = `feature/${item.id.toLowerCase()}-${item.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40)}`;
|
|
2574
|
+
|
|
2575
|
+
// Build related projects context for multi-project items
|
|
2576
|
+
let relatedProjects = '';
|
|
2577
|
+
if (targetProjects.length > 1) {
|
|
2578
|
+
relatedProjects = targetProjects.map(p =>
|
|
2579
|
+
`- **${p.name}** — ${path.resolve(p.localPath)} (${p.adoOrg}/${p.adoProject}/${p.repoName}, branch: ${p.mainBranch || 'main'})`
|
|
2580
|
+
).join('\n');
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2221
2583
|
const vars = {
|
|
2222
2584
|
agent_id: agentId,
|
|
2223
2585
|
agent_name: config.agents[agentId]?.name || agentId,
|
|
@@ -2229,15 +2591,16 @@ function discoverFromPrd(config, project) {
|
|
|
2229
2591
|
item_description: item.description || '',
|
|
2230
2592
|
branch_name: branchName,
|
|
2231
2593
|
project_path: root,
|
|
2232
|
-
main_branch:
|
|
2594
|
+
main_branch: primary.mainBranch || 'main',
|
|
2233
2595
|
worktree_path: path.resolve(root, config.engine?.worktreeRoot || '../worktrees', branchName),
|
|
2234
2596
|
commit_message: `feat(${item.id.toLowerCase()}): ${item.name}`,
|
|
2235
2597
|
team_root: SQUAD_DIR,
|
|
2236
|
-
repo_id:
|
|
2237
|
-
project_name:
|
|
2238
|
-
ado_org:
|
|
2239
|
-
ado_project:
|
|
2240
|
-
repo_name:
|
|
2598
|
+
repo_id: primary.repositoryId || '',
|
|
2599
|
+
project_name: targetProjects.map(p => p.name).join(', '),
|
|
2600
|
+
ado_org: primary.adoOrg || 'Unknown',
|
|
2601
|
+
ado_project: primary.adoProject || 'Unknown',
|
|
2602
|
+
repo_name: primary.repoName || 'Unknown',
|
|
2603
|
+
related_projects: relatedProjects,
|
|
2241
2604
|
};
|
|
2242
2605
|
|
|
2243
2606
|
const prompt = renderPlaybook('implement', vars);
|
|
@@ -2248,17 +2611,17 @@ function discoverFromPrd(config, project) {
|
|
|
2248
2611
|
agent: agentId,
|
|
2249
2612
|
agentName: config.agents[agentId]?.name,
|
|
2250
2613
|
agentRole: config.agents[agentId]?.role,
|
|
2251
|
-
task: `[${
|
|
2614
|
+
task: `[${vars.project_name}] Implement ${item.id}: ${item.name}`,
|
|
2252
2615
|
prompt,
|
|
2253
|
-
meta: { dispatchKey: key, source: 'prd', branch: branchName, item, project: { name:
|
|
2616
|
+
meta: { dispatchKey: key, source: 'prd', branch: branchName, item, project: { name: primary.name, localPath: primary.localPath } }
|
|
2254
2617
|
});
|
|
2255
2618
|
|
|
2256
2619
|
setCooldown(key);
|
|
2257
2620
|
}
|
|
2258
2621
|
|
|
2259
|
-
const skipTotal = skipped.dispatched + skipped.cooldown + skipped.noAgent;
|
|
2622
|
+
const skipTotal = skipped.dispatched + skipped.cooldown + skipped.noAgent + skipped.noProject;
|
|
2260
2623
|
if (skipTotal > 0) {
|
|
2261
|
-
log('debug', `PRD discovery
|
|
2624
|
+
log('debug', `PRD discovery: skipped ${skipTotal} items (${skipped.dispatched} dispatched, ${skipped.cooldown} cooldown, ${skipped.noAgent} no agent, ${skipped.noProject} no project)`);
|
|
2262
2625
|
}
|
|
2263
2626
|
|
|
2264
2627
|
return newWork;
|
|
@@ -2316,7 +2679,12 @@ function materializePlansAsWorkItems(config) {
|
|
|
2316
2679
|
created: ts(),
|
|
2317
2680
|
createdBy: 'engine:plan-discovery',
|
|
2318
2681
|
sourcePlan: file,
|
|
2319
|
-
sourcePlanItem: item.id
|
|
2682
|
+
sourcePlanItem: item.id,
|
|
2683
|
+
planItemId: item.id,
|
|
2684
|
+
depends_on: item.depends_on || [],
|
|
2685
|
+
branchStrategy: plan.branch_strategy || 'parallel',
|
|
2686
|
+
featureBranch: plan.feature_branch || null,
|
|
2687
|
+
project: plan.project || null,
|
|
2320
2688
|
});
|
|
2321
2689
|
created++;
|
|
2322
2690
|
}
|
|
@@ -2324,6 +2692,36 @@ function materializePlansAsWorkItems(config) {
|
|
|
2324
2692
|
if (created > 0) {
|
|
2325
2693
|
safeWrite(wiPath, existingItems);
|
|
2326
2694
|
log('info', `Plan discovery: created ${created} work item(s) from ${file} → ${project.name}`);
|
|
2695
|
+
|
|
2696
|
+
// Pre-create shared feature branch if branch_strategy is shared-branch
|
|
2697
|
+
if (plan.branch_strategy === 'shared-branch' && plan.feature_branch) {
|
|
2698
|
+
try {
|
|
2699
|
+
const root = path.resolve(project.localPath);
|
|
2700
|
+
const mainBranch = project.mainBranch || 'main';
|
|
2701
|
+
const branch = sanitizeBranch(plan.feature_branch);
|
|
2702
|
+
// Create branch from main (idempotent — ignores if exists)
|
|
2703
|
+
execSync(`git branch "${branch}" "${mainBranch}" 2>/dev/null || true`, { cwd: root, stdio: 'pipe' });
|
|
2704
|
+
execSync(`git push -u origin "${branch}" 2>/dev/null || true`, { cwd: root, stdio: 'pipe' });
|
|
2705
|
+
log('info', `Shared branch pre-created: ${branch} for plan ${file}`);
|
|
2706
|
+
} catch (err) {
|
|
2707
|
+
log('warn', `Failed to pre-create shared branch for ${file}: ${err.message}`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
// Cycle detection for plan items
|
|
2712
|
+
const planDerivedItems = plan.missing_features.filter(f => f.depends_on && f.depends_on.length > 0);
|
|
2713
|
+
if (planDerivedItems.length > 0) {
|
|
2714
|
+
const cycles = detectDependencyCycles(plan.missing_features);
|
|
2715
|
+
if (cycles.length > 0) {
|
|
2716
|
+
log('warn', `Dependency cycle detected in plan ${file}: ${cycles.join(', ')} — clearing deps for cycling items`);
|
|
2717
|
+
for (const wi of existingItems) {
|
|
2718
|
+
if (wi.sourcePlan === file && cycles.includes(wi.planItemId)) {
|
|
2719
|
+
wi.depends_on = [];
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
safeWrite(wiPath, existingItems);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2327
2725
|
}
|
|
2328
2726
|
}
|
|
2329
2727
|
}
|
|
@@ -2429,6 +2827,51 @@ function discoverFromPrs(config, project) {
|
|
|
2429
2827
|
setCooldown(key);
|
|
2430
2828
|
}
|
|
2431
2829
|
|
|
2830
|
+
// PRs with pending human feedback (@squad comments) → route to author for fix
|
|
2831
|
+
if (pr.humanFeedback?.pendingFix) {
|
|
2832
|
+
const key = `human-fix-${project?.name || 'default'}-${pr.id}-${pr.humanFeedback.lastProcessedCommentDate}`;
|
|
2833
|
+
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) continue;
|
|
2834
|
+
|
|
2835
|
+
const agentId = resolveAgent('fix', config, pr.agent);
|
|
2836
|
+
if (!agentId) continue;
|
|
2837
|
+
|
|
2838
|
+
const prNumber = (pr.id || '').replace(/^PR-/, '');
|
|
2839
|
+
const vars = {
|
|
2840
|
+
agent_id: agentId,
|
|
2841
|
+
agent_name: config.agents[agentId]?.name || agentId,
|
|
2842
|
+
agent_role: config.agents[agentId]?.role || 'Agent',
|
|
2843
|
+
pr_id: pr.id,
|
|
2844
|
+
pr_number: prNumber,
|
|
2845
|
+
pr_title: pr.title || '',
|
|
2846
|
+
pr_branch: pr.branch || '',
|
|
2847
|
+
reviewer: 'Human Reviewer',
|
|
2848
|
+
review_note: pr.humanFeedback.feedbackContent || 'See PR thread comments',
|
|
2849
|
+
team_root: SQUAD_DIR,
|
|
2850
|
+
repo_id: project?.repositoryId || config.project?.repositoryId || '',
|
|
2851
|
+
project_name: project?.name || 'Unknown Project',
|
|
2852
|
+
ado_org: project?.adoOrg || 'Unknown',
|
|
2853
|
+
ado_project: project?.adoProject || 'Unknown',
|
|
2854
|
+
repo_name: project?.repoName || 'Unknown'
|
|
2855
|
+
};
|
|
2856
|
+
|
|
2857
|
+
const prompt = renderPlaybook('fix', vars);
|
|
2858
|
+
if (!prompt) continue;
|
|
2859
|
+
|
|
2860
|
+
newWork.push({
|
|
2861
|
+
type: 'fix',
|
|
2862
|
+
agent: agentId,
|
|
2863
|
+
agentName: config.agents[agentId]?.name,
|
|
2864
|
+
agentRole: config.agents[agentId]?.role,
|
|
2865
|
+
task: `[${project?.name || 'project'}] Fix PR ${pr.id} — human feedback`,
|
|
2866
|
+
prompt,
|
|
2867
|
+
meta: { dispatchKey: key, source: 'pr-human-feedback', pr, branch: pr.branch, project: { name: project?.name, localPath: project?.localPath } }
|
|
2868
|
+
});
|
|
2869
|
+
|
|
2870
|
+
// Clear pendingFix so it doesn't re-dispatch next tick
|
|
2871
|
+
pr.humanFeedback.pendingFix = false;
|
|
2872
|
+
setCooldown(key);
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2432
2875
|
// PRs with build failures → route to any idle agent
|
|
2433
2876
|
if (pr.status === 'active' && pr.buildStatus === 'failing') {
|
|
2434
2877
|
const key = `build-fix-${project?.name || 'default'}-${pr.id}`;
|
|
@@ -2545,6 +2988,9 @@ function discoverFromWorkItems(config, project) {
|
|
|
2545
2988
|
for (const item of items) {
|
|
2546
2989
|
if (item.status !== 'queued' && item.status !== 'pending') continue;
|
|
2547
2990
|
|
|
2991
|
+
// Dependency gate: skip items whose depends_on are not yet met
|
|
2992
|
+
if (item.depends_on && item.depends_on.length > 0 && !areDependenciesMet(item, config)) continue;
|
|
2993
|
+
|
|
2548
2994
|
const key = `work-${project?.name || 'default'}-${item.id}`;
|
|
2549
2995
|
if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) { skipped.gated++; continue; }
|
|
2550
2996
|
|
|
@@ -2555,7 +3001,8 @@ function discoverFromWorkItems(config, project) {
|
|
|
2555
3001
|
const agentId = item.agent || resolveAgent(workType, config);
|
|
2556
3002
|
if (!agentId) { skipped.noAgent++; continue; }
|
|
2557
3003
|
|
|
2558
|
-
const
|
|
3004
|
+
const isShared = item.branchStrategy === 'shared-branch' && item.featureBranch;
|
|
3005
|
+
const branchName = isShared ? item.featureBranch : (item.branch || `work/${item.id}`);
|
|
2559
3006
|
const vars = {
|
|
2560
3007
|
agent_id: agentId,
|
|
2561
3008
|
agent_name: config.agents[agentId]?.name || agentId,
|
|
@@ -2581,9 +3028,22 @@ function discoverFromWorkItems(config, project) {
|
|
|
2581
3028
|
date: dateStamp()
|
|
2582
3029
|
};
|
|
2583
3030
|
|
|
2584
|
-
//
|
|
2585
|
-
|
|
2586
|
-
|
|
3031
|
+
// Inject ask-specific variables for the ask playbook
|
|
3032
|
+
if (workType === 'ask') {
|
|
3033
|
+
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3034
|
+
vars.task_id = item.id;
|
|
3035
|
+
vars.notes_content = '';
|
|
3036
|
+
try { vars.notes_content = fs.readFileSync(path.join(SQUAD_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// Select playbook: shared-branch items use implement-shared, others use type-specific or work-item
|
|
3040
|
+
let playbookName;
|
|
3041
|
+
if (item.branchStrategy === 'shared-branch' && (workType === 'implement' || workType === 'implement:large')) {
|
|
3042
|
+
playbookName = 'implement-shared';
|
|
3043
|
+
} else {
|
|
3044
|
+
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask'];
|
|
3045
|
+
playbookName = typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
|
|
3046
|
+
}
|
|
2587
3047
|
const prompt = item.prompt || renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars) || item.description;
|
|
2588
3048
|
if (!prompt) {
|
|
2589
3049
|
log('warn', `No playbook rendered for ${item.id} (type: ${workType}, playbook: ${playbookName}) — skipping`);
|
|
@@ -2602,7 +3062,7 @@ function discoverFromWorkItems(config, project) {
|
|
|
2602
3062
|
agentRole: config.agents[agentId]?.role,
|
|
2603
3063
|
task: `[${project?.name || 'project'}] ${item.title || item.description?.slice(0, 80) || item.id}`,
|
|
2604
3064
|
prompt,
|
|
2605
|
-
meta: { dispatchKey: key, source: 'work-item', branch: branchName, item, project: { name: project?.name, localPath: project?.localPath } }
|
|
3065
|
+
meta: { dispatchKey: key, source: 'work-item', branch: branchName, branchStrategy: item.branchStrategy || 'parallel', useExistingBranch: !!(item.branchStrategy === 'shared-branch' && item.featureBranch), item, project: { name: project?.name, localPath: project?.localPath } }
|
|
2606
3066
|
});
|
|
2607
3067
|
|
|
2608
3068
|
setCooldown(key);
|
|
@@ -2859,7 +3319,15 @@ function discoverCentralWorkItems(config) {
|
|
|
2859
3319
|
date: dateStamp()
|
|
2860
3320
|
};
|
|
2861
3321
|
|
|
2862
|
-
|
|
3322
|
+
// Inject ask-specific variables for the ask playbook
|
|
3323
|
+
if (workType === 'ask') {
|
|
3324
|
+
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3325
|
+
vars.task_id = item.id;
|
|
3326
|
+
vars.notes_content = '';
|
|
3327
|
+
try { vars.notes_content = fs.readFileSync(path.join(SQUAD_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask'];
|
|
2863
3331
|
const playbookName = typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
|
|
2864
3332
|
const prompt = renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars);
|
|
2865
3333
|
if (!prompt) continue;
|
|
@@ -2917,7 +3385,26 @@ function discoverCentralWorkItems(config) {
|
|
|
2917
3385
|
date: dateStamp()
|
|
2918
3386
|
};
|
|
2919
3387
|
|
|
2920
|
-
|
|
3388
|
+
// Inject plan-specific variables for the plan playbook
|
|
3389
|
+
if (workType === 'plan') {
|
|
3390
|
+
const planFileName = `plan-${item.id.toLowerCase()}-${dateStamp()}.md`;
|
|
3391
|
+
vars.plan_content = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3392
|
+
vars.plan_title = item.title;
|
|
3393
|
+
vars.plan_file = planFileName;
|
|
3394
|
+
vars.task_description = item.title;
|
|
3395
|
+
vars.notes_content = '';
|
|
3396
|
+
try { vars.notes_content = fs.readFileSync(path.join(SQUAD_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// Inject ask-specific variables for the ask playbook
|
|
3400
|
+
if (workType === 'ask') {
|
|
3401
|
+
vars.question = item.title + (item.description ? '\n\n' + item.description : '');
|
|
3402
|
+
vars.task_id = item.id;
|
|
3403
|
+
vars.notes_content = '';
|
|
3404
|
+
try { vars.notes_content = fs.readFileSync(path.join(SQUAD_DIR, 'notes.md'), 'utf8'); } catch {}
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask'];
|
|
2921
3408
|
const playbookName = typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
|
|
2922
3409
|
const prompt = renderPlaybook(playbookName, vars) || renderPlaybook('work-item', vars);
|
|
2923
3410
|
if (!prompt) continue;
|
|
@@ -2946,7 +3433,7 @@ function discoverCentralWorkItems(config) {
|
|
|
2946
3433
|
|
|
2947
3434
|
/**
|
|
2948
3435
|
* Run all work discovery sources and queue new items
|
|
2949
|
-
* Priority: fix (0) > review (1) > implement (2) > work-items (3) > central (4)
|
|
3436
|
+
* Priority: fix (0) > ask (1) > review (1) > implement (2) > work-items (3) > central (4)
|
|
2950
3437
|
*/
|
|
2951
3438
|
function discoverWork(config) {
|
|
2952
3439
|
const projects = getProjects(config);
|
|
@@ -2966,9 +3453,6 @@ function discoverWork(config) {
|
|
|
2966
3453
|
allReviews.push(...prWork.filter(w => w.type === 'review'));
|
|
2967
3454
|
allWorkItems.push(...prWork.filter(w => w.type === 'test'));
|
|
2968
3455
|
|
|
2969
|
-
// Source 2: PRD gaps → implements
|
|
2970
|
-
allImplements.push(...discoverFromPrd(config, project));
|
|
2971
|
-
|
|
2972
3456
|
// Side-effect: specs → work items (picked up below)
|
|
2973
3457
|
materializeSpecsAsWorkItems(config, project);
|
|
2974
3458
|
|
|
@@ -2976,6 +3460,9 @@ function discoverWork(config) {
|
|
|
2976
3460
|
allWorkItems.push(...discoverFromWorkItems(config, project));
|
|
2977
3461
|
}
|
|
2978
3462
|
|
|
3463
|
+
// Source 2: Squad-level PRD → implements (multi-project, called once outside project loop)
|
|
3464
|
+
allImplements.push(...discoverFromPrd(config));
|
|
3465
|
+
|
|
2979
3466
|
// Central work items (project-agnostic — agent decides where to work)
|
|
2980
3467
|
const centralWork = discoverCentralWorkItems(config);
|
|
2981
3468
|
|
|
@@ -3038,6 +3525,11 @@ async function tickInner() {
|
|
|
3038
3525
|
try { await pollPrStatus(config); } catch (e) { log('warn', `PR status poll error: ${e.message}`); }
|
|
3039
3526
|
}
|
|
3040
3527
|
|
|
3528
|
+
// 2.7. Poll PR threads for human @squad comments (every 12 ticks = ~6 minutes)
|
|
3529
|
+
if (tickCount % 12 === 0) {
|
|
3530
|
+
try { await pollPrHumanComments(config); } catch (e) { log('warn', `PR comment poll error: ${e.message}`); }
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3041
3533
|
// 3. Discover new work from sources
|
|
3042
3534
|
discoverWork(config);
|
|
3043
3535
|
|
|
@@ -3057,7 +3549,7 @@ async function tickInner() {
|
|
|
3057
3549
|
const slotsAvailable = maxConcurrent - activeCount;
|
|
3058
3550
|
|
|
3059
3551
|
// Priority dispatch: fixes > reviews > plan-to-prd > implement > other
|
|
3060
|
-
const typePriority = { fix: 0, review: 1, test: 2, 'plan-to-prd': 3, 'implement:large': 4, implement: 5 };
|
|
3552
|
+
const typePriority = { fix: 0, ask: 1, review: 1, test: 2, plan: 3, 'plan-to-prd': 3, 'implement:large': 4, implement: 5 };
|
|
3061
3553
|
const itemPriority = { high: 0, medium: 1, low: 2 };
|
|
3062
3554
|
dispatch.pending.sort((a, b) => {
|
|
3063
3555
|
const ta = typePriority[a.type] ?? 5, tb = typePriority[b.type] ?? 5;
|