@yemi33/minions 0.1.1809 → 0.1.1811
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/CHANGELOG.md +12 -2
- package/dashboard.js +540 -224
- package/docs/pr-review-fix-loop.md +2 -0
- package/engine/cli.js +15 -9
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +4 -10
- package/engine/lifecycle.js +11 -11
- package/engine/pipeline.js +146 -43
- package/engine/projects.js +15 -16
- package/engine/queries.js +3 -3
- package/engine/shared.js +161 -3
- package/engine.js +63 -34
- package/package.json +1 -1
- package/playbooks/fix.md +17 -6
- package/prompts/cc-system.md +7 -2
- package/prompts/doc-chat-system.md +15 -7
- package/routing.md +1 -0
|
@@ -43,6 +43,7 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
|
|
|
43
43
|
- Coalesces multiple comments arriving during cooldown into single fix
|
|
44
44
|
- Routes to author
|
|
45
45
|
- Not gated by `_evalEscalated` — humans can always force more fixes via PR comments even after the minion review loop escalates.
|
|
46
|
+
- Fix agents must treat human comments as claims to verify, not commands. They inspect or reproduce each claimed issue, make the smallest correct fix only when the claim is valid, and otherwise reply with evidence-backed rationale.
|
|
46
47
|
|
|
47
48
|
### B. Review feedback (`changes-requested`)
|
|
48
49
|
|
|
@@ -51,6 +52,7 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
|
|
|
51
52
|
- `review_note` = reviewer's feedback
|
|
52
53
|
- Sets `fixDispatched = true` — prevents the later conflict fix from also firing this pass
|
|
53
54
|
- **Review-loop escalation**: after `evalMaxIterations` review→fix cycles (default 3), `_evalEscalated` is set on the PR and *only this trigger plus minion review/re-review* stop. Triggers A (human comments), C (build failures), and D (merge conflicts) keep running. The dashboard PR row distinguishes the two states with separate badges (review badge `review-escalated` vs. build badge `build-escalated`).
|
|
55
|
+
- Fix agents validate each requested-change claim before editing. Invalid, stale, already-addressed, out-of-scope, or harmful feedback should get a respectful evidence-backed rebuttal rather than a success-shaped code change.
|
|
54
56
|
|
|
55
57
|
### C. Build failures (`buildStatus === 'failing'`)
|
|
56
58
|
|
package/engine/cli.js
CHANGED
|
@@ -1157,11 +1157,14 @@ const commands = {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
|
|
1159
1159
|
const config = getConfig();
|
|
1160
|
-
const { getProjects, projectWorkItemsPath } = require('./shared');
|
|
1160
|
+
const { getProjects, projectWorkItemsPath, resolveProjectSource } = require('./shared');
|
|
1161
1161
|
const projects = getProjects(config);
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1162
|
+
const target = opts.project ? resolveProjectSource(opts.project, projects, { allowCentral: false }) : null;
|
|
1163
|
+
if (target?.error) {
|
|
1164
|
+
console.log(target.error);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const targetProject = target?.project || projects[0];
|
|
1165
1168
|
const wiPath = projectWorkItemsPath(targetProject);
|
|
1166
1169
|
let item;
|
|
1167
1170
|
mutateWorkItems(wiPath, items => {
|
|
@@ -1202,11 +1205,14 @@ const commands = {
|
|
|
1202
1205
|
}
|
|
1203
1206
|
|
|
1204
1207
|
const config = getConfig();
|
|
1205
|
-
const { getProjects } = require('./shared');
|
|
1208
|
+
const { getProjects, resolveProjectSource } = require('./shared');
|
|
1206
1209
|
const projects = getProjects(config);
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
+
const target = projectName ? resolveProjectSource(projectName, projects, { allowCentral: false }) : null;
|
|
1211
|
+
if (target?.error) {
|
|
1212
|
+
console.log(target.error);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const targetProject = target?.project || projects[0];
|
|
1210
1216
|
|
|
1211
1217
|
if (!targetProject) {
|
|
1212
1218
|
console.log('No projects configured. Run: minions add <dir>');
|
|
@@ -1395,7 +1401,7 @@ const commands = {
|
|
|
1395
1401
|
const wiPath = (item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout')
|
|
1396
1402
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1397
1403
|
: item.meta.project?.localPath
|
|
1398
|
-
? shared.projectWorkItemsPath(
|
|
1404
|
+
? shared.projectWorkItemsPath(shared.resolveProjectSource(item.meta.project, config.projects || [], { allowCentral: false }).project || item.meta.project)
|
|
1399
1405
|
: null;
|
|
1400
1406
|
if (wiPath) {
|
|
1401
1407
|
mutateWorkItems(wiPath, items => {
|
package/engine/dispatch.js
CHANGED
|
@@ -79,7 +79,8 @@ function mutateDispatch(mutator) {
|
|
|
79
79
|
|
|
80
80
|
function getDispatchProjectKey(project) {
|
|
81
81
|
if (!project) return '';
|
|
82
|
-
|
|
82
|
+
if (project.name) return String(project.name).toLowerCase();
|
|
83
|
+
return project.localPath ? path.resolve(project.localPath).toLowerCase() : '';
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function getPrDispatchTargetKey(entry) {
|
|
@@ -176,15 +177,8 @@ function addToDispatch(item) {
|
|
|
176
177
|
function _resolveDispatchProject(projectRef, config) {
|
|
177
178
|
if (!projectRef) return null;
|
|
178
179
|
const projects = getProjects(config);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (byName) return byName;
|
|
182
|
-
}
|
|
183
|
-
if (projectRef.localPath) {
|
|
184
|
-
const refPath = path.resolve(projectRef.localPath);
|
|
185
|
-
const byPath = projects.find(p => p.localPath && path.resolve(p.localPath) === refPath);
|
|
186
|
-
if (byPath) return byPath;
|
|
187
|
-
}
|
|
180
|
+
const resolved = shared.resolveProjectSource(projectRef, projects, { allowCentral: false });
|
|
181
|
+
if (resolved.project) return resolved.project;
|
|
188
182
|
return projectRef;
|
|
189
183
|
}
|
|
190
184
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -160,7 +160,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
160
160
|
// Resolve the primary project for writing new work items (PR, verify)
|
|
161
161
|
const projectName = plan.project;
|
|
162
162
|
const primaryProject = projectName
|
|
163
|
-
?
|
|
163
|
+
? shared.resolveProjectSource(projectName, projects, { allowCentral: false }).project
|
|
164
|
+
: (projects.length === 1 ? projects[0] : null);
|
|
164
165
|
if (!primaryProject) {
|
|
165
166
|
log('warn', `Plan ${planFile}: no primary project found — skipping PR/verify creation`);
|
|
166
167
|
return;
|
|
@@ -196,9 +197,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
196
197
|
log('info', `Plan ${planFile}: verify WI ${existingVerify.id} already ${existingVerify.status} — skipping`);
|
|
197
198
|
} else if (isReopenableVerify(existingVerify) && doneItems.length > 0) {
|
|
198
199
|
const verifyProject = existingVerify.project || projectName;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
);
|
|
200
|
+
const vProject = shared.resolveProjectSource(verifyProject, projects, { allowCentral: false }).project || primaryProject;
|
|
201
|
+
const vWiPath = shared.projectWorkItemsPath(vProject);
|
|
202
202
|
let reopenedVerify = false;
|
|
203
203
|
mutateWorkItems(vWiPath, items => {
|
|
204
204
|
const v = items.find(w => w.id === existingVerify.id);
|
|
@@ -777,7 +777,8 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
777
777
|
|
|
778
778
|
const projects = shared.getProjects(config);
|
|
779
779
|
if (projects.length === 0 && !meta?.project?.name) return 0;
|
|
780
|
-
const defaultProject = (meta?.project?.name &&
|
|
780
|
+
const defaultProject = (meta?.project?.name && shared.resolveProjectSource(meta.project.name, projects, { allowCentral: false }).project) ||
|
|
781
|
+
(projects.length === 1 ? projects[0] : null);
|
|
781
782
|
const useCentral = !defaultProject;
|
|
782
783
|
|
|
783
784
|
// Match each PR to its correct project by finding which repo URL appears near the PR number in output
|
|
@@ -950,16 +951,15 @@ function hasCanonicalPrAttachment(itemId, config) {
|
|
|
950
951
|
function resolvePrFallbackProject(meta, config) {
|
|
951
952
|
const projects = shared.getProjects(config);
|
|
952
953
|
if (meta?.project?.name) {
|
|
953
|
-
const match =
|
|
954
|
+
const match = shared.resolveProjectSource(meta.project.name, projects, { allowCentral: false }).project;
|
|
954
955
|
if (match) return match;
|
|
955
956
|
}
|
|
956
957
|
if (meta?.project?.localPath) {
|
|
957
|
-
const
|
|
958
|
-
const match = projects.find(p => p.localPath && path.resolve(p.localPath) === metaPath);
|
|
958
|
+
const match = shared.resolveProjectSource(meta.project.localPath, projects, { allowCentral: false }).project;
|
|
959
959
|
if (match) return match;
|
|
960
960
|
}
|
|
961
961
|
if (meta?.item?.project) {
|
|
962
|
-
const match =
|
|
962
|
+
const match = shared.resolveProjectSource(meta.item.project, projects, { allowCentral: false }).project;
|
|
963
963
|
if (match) return match;
|
|
964
964
|
}
|
|
965
965
|
return projects.length === 1 ? projects[0] : null;
|
|
@@ -1813,7 +1813,7 @@ async function processPendingRebases(config) {
|
|
|
1813
1813
|
for (const entry of snapshot) {
|
|
1814
1814
|
if (isBranchActive(entry.branch)) { remaining.push(entry); continue; }
|
|
1815
1815
|
|
|
1816
|
-
const project = shared.getProjects(config)
|
|
1816
|
+
const project = shared.resolveProjectSource(entry.projectName, shared.getProjects(config), { allowCentral: false }).project;
|
|
1817
1817
|
if (!project) continue;
|
|
1818
1818
|
|
|
1819
1819
|
const prs = getPrs(project);
|
|
@@ -2037,7 +2037,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
|
|
|
2037
2037
|
if (!m('created')) enrichedBlock = enrichedBlock.replace('---\n', `---\ncreated: ${dateStamp()}\n`);
|
|
2038
2038
|
const skillDirName = name.replace(/[^a-z0-9-]/g, '-');
|
|
2039
2039
|
if (scope === 'project' && project) {
|
|
2040
|
-
const proj = shared.getProjects(config)
|
|
2040
|
+
const proj = shared.resolveProjectSource(project, shared.getProjects(config), { allowCentral: false }).project;
|
|
2041
2041
|
if (proj) {
|
|
2042
2042
|
const projectSkillRoot = skillWriteTargets(effectiveRuntime, proj).project
|
|
2043
2043
|
|| path.resolve(proj.localPath, '.claude', 'skills');
|
package/engine/pipeline.js
CHANGED
|
@@ -200,6 +200,44 @@ function collectPipelinePrRefs(pipeline, run) {
|
|
|
200
200
|
return refs;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
function _pipelineProjectValues(...values) {
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const value of values) {
|
|
206
|
+
if (value === undefined) continue;
|
|
207
|
+
if (Array.isArray(value)) out.push(...value);
|
|
208
|
+
else out.push(value);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function _resolvePipelineProjects(values, config) {
|
|
214
|
+
const projects = shared.getProjects(config);
|
|
215
|
+
const rawValues = _pipelineProjectValues(...values);
|
|
216
|
+
if (rawValues.length === 0) return { projects: [null] };
|
|
217
|
+
const resolved = [];
|
|
218
|
+
const seen = new Set();
|
|
219
|
+
for (const raw of rawValues) {
|
|
220
|
+
if (raw === undefined) continue;
|
|
221
|
+
if (raw === null || String(raw).trim().toLowerCase() === 'central') {
|
|
222
|
+
if (!seen.has('central')) { seen.add('central'); resolved.push(null); }
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const result = shared.resolveConfiguredProject(raw, projects);
|
|
226
|
+
if (result.error) return { error: result.error };
|
|
227
|
+
const key = result.project?.name || 'central';
|
|
228
|
+
if (!seen.has(key)) { seen.add(key); resolved.push(result.project || null); }
|
|
229
|
+
}
|
|
230
|
+
return { projects: resolved.length ? resolved : [null] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _pipelineProjectSlug(project) {
|
|
234
|
+
return project ? shared.safeSlugComponent(project.name || 'project', 32).toLowerCase() : 'central';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function _pipelineWorkItemsPath(project) {
|
|
238
|
+
return project ? shared.projectWorkItemsPath(project) : CENTRAL_WI_PATH;
|
|
239
|
+
}
|
|
240
|
+
|
|
203
241
|
// ── Condition Evaluation ─────────────────────────────────────────────────────
|
|
204
242
|
|
|
205
243
|
/**
|
|
@@ -267,11 +305,11 @@ async function executeStage(stage, run, pipeline, config) {
|
|
|
267
305
|
|
|
268
306
|
switch (resolved.type) {
|
|
269
307
|
case STAGE_TYPE.TASK:
|
|
270
|
-
return executeTaskStage(resolved, stageState, run, config);
|
|
308
|
+
return executeTaskStage(resolved, stageState, run, config, pipeline);
|
|
271
309
|
case STAGE_TYPE.MEETING:
|
|
272
310
|
return executeMeetingStage(resolved, stageState, run, config);
|
|
273
311
|
case STAGE_TYPE.PLAN:
|
|
274
|
-
return executePlanStage(resolved, stageState, run, config);
|
|
312
|
+
return executePlanStage(resolved, stageState, run, config, pipeline);
|
|
275
313
|
case STAGE_TYPE.API:
|
|
276
314
|
return executeApiStage(resolved, stageState, run);
|
|
277
315
|
case STAGE_TYPE.MERGE_PRS:
|
|
@@ -291,8 +329,57 @@ async function executeStage(stage, run, pipeline, config) {
|
|
|
291
329
|
}
|
|
292
330
|
}
|
|
293
331
|
|
|
294
|
-
function executeTaskStage(stage, stageState, run, config) {
|
|
332
|
+
function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
295
333
|
// Create work item(s) for the task
|
|
334
|
+
const items = stage.items || [{ title: stage.title, description: stage.description || '', type: stage.taskType || 'explore', agent: stage.agent }];
|
|
335
|
+
const count = stage.count || items.length;
|
|
336
|
+
const createdIds = [];
|
|
337
|
+
const touchedProjects = [];
|
|
338
|
+
|
|
339
|
+
for (let i = 0; i < count; i++) {
|
|
340
|
+
const item = items[i % items.length];
|
|
341
|
+
const projectResolution = _resolvePipelineProjects([
|
|
342
|
+
item.projects,
|
|
343
|
+
item.project,
|
|
344
|
+
stage.projects,
|
|
345
|
+
stage.project,
|
|
346
|
+
pipeline.projects,
|
|
347
|
+
pipeline.project,
|
|
348
|
+
], config);
|
|
349
|
+
if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
|
|
350
|
+
for (const project of projectResolution.projects) {
|
|
351
|
+
const projectSlug = _pipelineProjectSlug(project);
|
|
352
|
+
const id = `PL-${run.runId.slice(4, 12)}-${stage.id}-${i}${projectResolution.projects.length > 1 || project ? '-' + projectSlug : ''}`;
|
|
353
|
+
const wiPath = _pipelineWorkItemsPath(project);
|
|
354
|
+
mutateWorkItems(wiPath, workItems => {
|
|
355
|
+
if (workItems.some(w => w.id === id)) { createdIds.push(id); return workItems; }
|
|
356
|
+
workItems.push({
|
|
357
|
+
id,
|
|
358
|
+
title: item.title || stage.title,
|
|
359
|
+
description: item.description || stage.description || '',
|
|
360
|
+
type: routing.normalizeWorkType(item.type || stage.taskType, WORK_TYPE.EXPLORE),
|
|
361
|
+
priority: item.priority || stage.priority || 'medium',
|
|
362
|
+
// Agent is a soft routing hint unless agentLock/hardAgent is set.
|
|
363
|
+
...(item.agent || stage.agent ? { agent: item.agent || stage.agent } : {}),
|
|
364
|
+
...(item.agentLock === true || stage.agentLock === true || item.hardAgent === true || stage.hardAgent === true ? { agentLock: true } : {}),
|
|
365
|
+
...(project ? { project: project.name } : {}),
|
|
366
|
+
status: WI_STATUS.PENDING,
|
|
367
|
+
created: ts(),
|
|
368
|
+
createdBy: 'pipeline:' + run.pipelineId,
|
|
369
|
+
branch: `pipeline/${run.pipelineId}/${stage.id}`,
|
|
370
|
+
_pipelineRun: run.runId,
|
|
371
|
+
_pipelineStage: stage.id,
|
|
372
|
+
});
|
|
373
|
+
return workItems;
|
|
374
|
+
});
|
|
375
|
+
createdIds.push(id);
|
|
376
|
+
touchedProjects.push(project?.name || null);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { status: PIPELINE_STATUS.RUNNING, artifacts: { workItems: [...new Set(createdIds)], projects: [...new Set(touchedProjects)] } };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function executeTaskStageLegacy(stage, stageState, run, config) {
|
|
296
383
|
const items = stage.items || [{ title: stage.title, description: stage.description || '', type: stage.taskType || 'explore', agent: stage.agent }];
|
|
297
384
|
const count = stage.count || items.length;
|
|
298
385
|
const wiPath = CENTRAL_WI_PATH;
|
|
@@ -396,13 +483,15 @@ function _findExistingPrdForPlan(planFile, prdDir) {
|
|
|
396
483
|
return null;
|
|
397
484
|
}
|
|
398
485
|
|
|
399
|
-
async function executePlanStage(stage, stageState, run, config) {
|
|
486
|
+
async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
400
487
|
const plansDir = PLANS_DIR;
|
|
401
488
|
if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
|
|
402
489
|
|
|
403
490
|
const slug = slugify(stage.title || 'pipeline-plan');
|
|
404
|
-
const
|
|
405
|
-
|
|
491
|
+
const projectResolution = _resolvePipelineProjects([stage.projects, stage.project, pipeline.projects, pipeline.project], config);
|
|
492
|
+
if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
|
|
493
|
+
const targetProjects = projectResolution.projects;
|
|
494
|
+
const wiIdForProject = (project) => `PL-${run.runId.slice(4, 12)}-${stage.id}-prd${targetProjects.length > 1 || project ? '-' + _pipelineProjectSlug(project) : ''}`;
|
|
406
495
|
|
|
407
496
|
// ── Reconciliation: check if a plan already exists for a meeting in this run ──
|
|
408
497
|
const meetingIds = _findMeetingsInRun(run);
|
|
@@ -422,26 +511,33 @@ async function executePlanStage(stage, stageState, run, config) {
|
|
|
422
511
|
};
|
|
423
512
|
}
|
|
424
513
|
|
|
425
|
-
// Adopt or create plan-to-prd
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
514
|
+
// Adopt or create plan-to-prd WIs atomically under lock for each target project.
|
|
515
|
+
const adoptedWiIds = [];
|
|
516
|
+
for (const project of targetProjects) {
|
|
517
|
+
const wiPath = _pipelineWorkItemsPath(project);
|
|
518
|
+
const wiId = wiIdForProject(project);
|
|
519
|
+
let adoptedWiId = wiId;
|
|
520
|
+
mutateWorkItems(wiPath, workItems => {
|
|
521
|
+
const existing = workItems.find(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === existingPlanFile);
|
|
522
|
+
if (existing) {
|
|
523
|
+
existing._pipelineRun = run.runId;
|
|
524
|
+
existing._pipelineStage = stage.id;
|
|
525
|
+
adoptedWiId = existing.id;
|
|
526
|
+
} else if (!workItems.some(w => w.id === wiId)) {
|
|
527
|
+
workItems.push({
|
|
528
|
+
id: wiId, title: `Convert plan to PRD: ${existingPlanFile}`,
|
|
529
|
+
type: WORK_TYPE.PLAN_TO_PRD, priority: 'high', status: WI_STATUS.PENDING,
|
|
530
|
+
planFile: existingPlanFile, created: ts(), createdBy: 'pipeline:' + run.pipelineId,
|
|
531
|
+
branch: `pipeline/${run.pipelineId}/${stage.id}`, _pipelineRun: run.runId, _pipelineStage: stage.id,
|
|
532
|
+
...(project ? { project: project.name } : {}),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
adoptedWiIds.push(adoptedWiId);
|
|
537
|
+
}
|
|
442
538
|
return {
|
|
443
539
|
status: PIPELINE_STATUS.RUNNING,
|
|
444
|
-
artifacts: { plans: [existingPlanFile], workItems:
|
|
540
|
+
artifacts: { plans: [existingPlanFile], workItems: adoptedWiIds, prds: [], prs: [], projects: targetProjects.map(p => p?.name || null) },
|
|
445
541
|
};
|
|
446
542
|
}
|
|
447
543
|
}
|
|
@@ -511,32 +607,39 @@ async function executePlanStage(stage, stageState, run, config) {
|
|
|
511
607
|
const filePath = shared.uniquePath(path.join(plansDir, filename));
|
|
512
608
|
safeWrite(filePath, content);
|
|
513
609
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
610
|
+
const createdWiIds = [];
|
|
611
|
+
for (const project of targetProjects) {
|
|
612
|
+
const wiPath = _pipelineWorkItemsPath(project);
|
|
613
|
+
const wiId = wiIdForProject(project);
|
|
614
|
+
mutateWorkItems(wiPath, workItems => {
|
|
615
|
+
if (!workItems.some(w => w.id === wiId)) {
|
|
616
|
+
workItems.push({
|
|
617
|
+
id: wiId,
|
|
618
|
+
title: `Convert plan to PRD: ${path.basename(filePath)}`,
|
|
619
|
+
type: WORK_TYPE.PLAN_TO_PRD,
|
|
620
|
+
priority: 'high',
|
|
621
|
+
status: WI_STATUS.PENDING,
|
|
622
|
+
planFile: path.basename(filePath),
|
|
623
|
+
created: ts(),
|
|
624
|
+
createdBy: 'pipeline:' + run.pipelineId,
|
|
625
|
+
branch: `pipeline/${run.pipelineId}/${stage.id}`,
|
|
626
|
+
_pipelineRun: run.runId,
|
|
627
|
+
_pipelineStage: stage.id,
|
|
628
|
+
...(project ? { project: project.name } : {}),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
createdWiIds.push(wiId);
|
|
633
|
+
}
|
|
532
634
|
|
|
533
635
|
return {
|
|
534
636
|
status: PIPELINE_STATUS.RUNNING,
|
|
535
637
|
artifacts: {
|
|
536
638
|
plans: [path.basename(filePath)],
|
|
537
|
-
workItems:
|
|
639
|
+
workItems: createdWiIds,
|
|
538
640
|
prds: [], // discovered later when PRD materializes
|
|
539
641
|
prs: [], // discovered later when agents create PRs
|
|
642
|
+
projects: targetProjects.map(p => p?.name || null),
|
|
540
643
|
}
|
|
541
644
|
};
|
|
542
645
|
}
|
package/engine/projects.js
CHANGED
|
@@ -56,8 +56,7 @@ function removeProject(target, options = {}) {
|
|
|
56
56
|
try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); }
|
|
57
57
|
catch (e) { return { ...summary, error: 'Failed to read config: ' + e.message }; }
|
|
58
58
|
|
|
59
|
-
const project = (config.projects || [])
|
|
60
|
-
p.name === target || path.resolve(p.localPath || '') === path.resolve(target));
|
|
59
|
+
const project = shared.findProjectByNameOrPath(config.projects || [], target);
|
|
61
60
|
if (!project) {
|
|
62
61
|
const available = (config.projects || []).map(p => p.name).join(', ') || '(none)';
|
|
63
62
|
return { ...summary, error: `No project linked matching: ${target}. Available: ${available}` };
|
|
@@ -66,21 +65,21 @@ function removeProject(target, options = {}) {
|
|
|
66
65
|
|
|
67
66
|
// 1. Cancel pending/queued work items linked to this project (project-local
|
|
68
67
|
// file + central). Done items are preserved as history.
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
summary.cancelledItems += dispatch.cancelPendingWorkItems(
|
|
69
|
+
shared.projectWorkItemsPath(project),
|
|
70
|
+
() => true,
|
|
71
|
+
'project-removed',
|
|
72
|
+
);
|
|
73
|
+
summary.cancelledItems += dispatch.cancelPendingWorkItems(
|
|
71
74
|
path.join(MINIONS_DIR, 'work-items.json'),
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
w => !w.project || w.project === project.name,
|
|
76
|
-
'project-removed',
|
|
77
|
-
);
|
|
78
|
-
}
|
|
75
|
+
w => String(w.project || '').toLowerCase() === String(project.name || '').toLowerCase(),
|
|
76
|
+
'project-removed',
|
|
77
|
+
);
|
|
79
78
|
|
|
80
79
|
// 2. Drain dispatch — also kills active agent processes and unlinks pid +
|
|
81
80
|
// prompt sidecars in engine/tmp/, matching what plan delete does.
|
|
82
81
|
summary.drainedDispatches = dispatch.cleanDispatchEntries(
|
|
83
|
-
d => _dispatchProjectName(d) === project.name,
|
|
82
|
+
d => String(_dispatchProjectName(d) || '').toLowerCase() === String(project.name || '').toLowerCase(),
|
|
84
83
|
);
|
|
85
84
|
|
|
86
85
|
// 3. Clean up worktrees under this project's worktree root, honoring
|
|
@@ -104,7 +103,7 @@ function removeProject(target, options = {}) {
|
|
|
104
103
|
// specifically. Don't touch schedules with project='any' or unset.
|
|
105
104
|
if (Array.isArray(config.schedules)) {
|
|
106
105
|
for (const s of config.schedules) {
|
|
107
|
-
if (s.project
|
|
106
|
+
if (shared.resolveProjectSource(s.project, [project], { allowCentral: false }).project && s.enabled !== false) {
|
|
108
107
|
s.enabled = false;
|
|
109
108
|
summary.disabledSchedules++;
|
|
110
109
|
}
|
|
@@ -165,20 +164,20 @@ function removeProject(target, options = {}) {
|
|
|
165
164
|
...(p.monitoredResources || []),
|
|
166
165
|
...((p.stages || []).flatMap(s => s.monitoredResources || [])),
|
|
167
166
|
];
|
|
168
|
-
if (refs.some(r => r && (r.project
|
|
167
|
+
if (refs.some(r => r && shared.resolveProjectSource(r.project || r._project, [project], { allowCentral: false }).project)) {
|
|
169
168
|
summary.pipelineRefs.push(p.id);
|
|
170
169
|
}
|
|
171
170
|
}
|
|
172
171
|
} catch { /* pipelines optional */ }
|
|
173
172
|
|
|
174
173
|
// 7. Remove from config.json (and persist any schedule disables)
|
|
175
|
-
config.projects = (config.projects || []).filter(p => p
|
|
174
|
+
config.projects = (config.projects || []).filter(p => !shared.resolveProjectSource(p, [project], { allowCentral: false }).project);
|
|
176
175
|
try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); }
|
|
177
176
|
catch (e) { return { ...summary, error: 'Failed to write config: ' + e.message }; }
|
|
178
177
|
|
|
179
178
|
// 8. Move (or purge) projects/<name>/ — preserves PR/work-item history by
|
|
180
179
|
// default so a re-add can pick up where it left off.
|
|
181
|
-
const dataDir =
|
|
180
|
+
const dataDir = shared.projectStateDir(project);
|
|
182
181
|
if (fs.existsSync(dataDir)) {
|
|
183
182
|
if (dataMode === 'purge') {
|
|
184
183
|
try { fs.rmSync(dataDir, { recursive: true, force: true }); summary.purgedDataDir = true; }
|
package/engine/queries.js
CHANGED
|
@@ -703,7 +703,7 @@ function buildPrUrlFromId(prId, pr, projects) {
|
|
|
703
703
|
}
|
|
704
704
|
}
|
|
705
705
|
}
|
|
706
|
-
const project = pr?._project ?
|
|
706
|
+
const project = pr?._project ? shared.resolveProjectSource(pr._project, projects, { allowCentral: false }).project : null;
|
|
707
707
|
const prNumber = shared.getPrNumber(pr || prId);
|
|
708
708
|
if (project?.prUrlBase && prNumber != null) return project.prUrlBase + prNumber;
|
|
709
709
|
return '';
|
|
@@ -1179,7 +1179,7 @@ function getWorkItems(config) {
|
|
|
1179
1179
|
const allPrs = getPullRequests(config);
|
|
1180
1180
|
for (const item of allItems) {
|
|
1181
1181
|
if (item._pr && !item._prUrl) {
|
|
1182
|
-
const project =
|
|
1182
|
+
const project = shared.resolveProjectSource(item.project || item._source, projects, { allowCentral: false }).project || null;
|
|
1183
1183
|
const canonicalPrId = shared.getCanonicalPrId(project, item._pr);
|
|
1184
1184
|
const displayPrId = shared.getPrDisplayId(item._pr);
|
|
1185
1185
|
const exactPr = allPrs.find(p => p.id === canonicalPrId);
|
|
@@ -1418,7 +1418,7 @@ function getPrdInfo(config) {
|
|
|
1418
1418
|
// Fallback: work item _pr field for anything still missing
|
|
1419
1419
|
for (const wi of Object.values(wiById)) {
|
|
1420
1420
|
if (!wi._pr || prdToPr[wi.id]?.length) continue;
|
|
1421
|
-
const project =
|
|
1421
|
+
const project = shared.resolveProjectSource(wi.project || wi._source, projects, { allowCentral: false }).project || null;
|
|
1422
1422
|
const canonicalPrId = shared.getCanonicalPrId(project, wi._pr);
|
|
1423
1423
|
const exactPr = prById[canonicalPrId] || null;
|
|
1424
1424
|
const displayMatches = exactPr ? [] : Object.values(prById).filter(candidate => shared.getPrDisplayId(candidate) === shared.getPrDisplayId(wi._pr));
|