@yemi33/minions 0.1.1810 → 0.1.1812
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 +11 -0
- package/dashboard/js/command-center.js +7 -3
- package/dashboard/js/modal-qa.js +3 -2
- package/dashboard.js +142 -142
- package/engine/cli.js +15 -9
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +4 -10
- package/engine/issues.js +103 -11
- package/engine/lifecycle.js +11 -11
- package/engine/pipeline.js +34 -13
- package/engine/projects.js +5 -5
- package/engine/queries.js +3 -3
- package/engine/shared.js +123 -4
- package/engine.js +64 -50
- package/package.json +1 -1
- package/prompts/doc-chat-system.md +13 -1
package/dashboard.js
CHANGED
|
@@ -97,6 +97,13 @@ function reloadConfig() {
|
|
|
97
97
|
}
|
|
98
98
|
ensureConfiguredProjectStateFiles();
|
|
99
99
|
|
|
100
|
+
function resolveScheduleProjectValue(project, projects = PROJECTS) {
|
|
101
|
+
if (project === undefined) return { project: undefined };
|
|
102
|
+
const target = shared.resolveConfiguredProject(project, projects);
|
|
103
|
+
if (target.error) return { project: null, error: target.error };
|
|
104
|
+
return { project: target.project?.name || null };
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
function mutateDashboardConfig(mutator) {
|
|
101
108
|
return mutateJsonFileLocked(CONFIG_PATH, (config) => {
|
|
102
109
|
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
@@ -130,8 +137,8 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
130
137
|
if (body.projects && Array.isArray(body.projects)) {
|
|
131
138
|
if (!Array.isArray(current.projects)) current.projects = [];
|
|
132
139
|
for (const update of body.projects) {
|
|
133
|
-
const candidateProject = (candidate.projects || []
|
|
134
|
-
const currentProject = current.projects
|
|
140
|
+
const candidateProject = shared.findProjectByName(candidate.projects || [], update.name);
|
|
141
|
+
const currentProject = shared.findProjectByName(current.projects, update.name);
|
|
135
142
|
if (!candidateProject || !currentProject) continue;
|
|
136
143
|
currentProject.workSources = candidateProject.workSources;
|
|
137
144
|
}
|
|
@@ -259,12 +266,11 @@ function normalizeWorkItemDedupTitle(value) {
|
|
|
259
266
|
function resolveWorkItemDedupProject(item, wiPath = '') {
|
|
260
267
|
const projectName = normalizeWorkItemDedupText(item?.project || item?._project || item?._source);
|
|
261
268
|
if (projectName) {
|
|
262
|
-
const namedProject =
|
|
263
|
-
if (namedProject) return namedProject;
|
|
269
|
+
const namedProject = shared.resolveProjectSource(projectName, PROJECTS, { allowCentral: false });
|
|
270
|
+
if (namedProject.project) return namedProject.project;
|
|
264
271
|
}
|
|
265
272
|
if (!wiPath) return null;
|
|
266
|
-
|
|
267
|
-
return PROJECTS.find(p => path.resolve(shared.projectWorkItemsPath(p)) === resolvedWiPath) || null;
|
|
273
|
+
return shared.resolveProjectSource(wiPath, PROJECTS, { allowCentral: false }).project || null;
|
|
268
274
|
}
|
|
269
275
|
|
|
270
276
|
function getWorkItemPrRefCandidates(item) {
|
|
@@ -377,18 +383,37 @@ function findProjectByName(projects, projectName) {
|
|
|
377
383
|
}
|
|
378
384
|
|
|
379
385
|
function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
386
|
+
const target = shared.resolveProjectSource(projectName, projects, { defaultWhenSingle: true, minionsDir: MINIONS_DIR });
|
|
387
|
+
if (target.error) return { error: target.error };
|
|
388
|
+
return target;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function resolveProjectSourceTarget(source, projects = PROJECTS, options = {}) {
|
|
392
|
+
return shared.resolveProjectSource(source, projects, { minionsDir: MINIONS_DIR, ...options });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function dispatchPrefixForResolvedSource(target) {
|
|
396
|
+
return target?.project ? `work-${target.project.name}-` : 'central-work-';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function findWorkItemsTargetById(id, source, projects = PROJECTS) {
|
|
400
|
+
const explicitSource = source !== undefined && source !== null && String(source).trim() !== '';
|
|
401
|
+
if (explicitSource) {
|
|
402
|
+
const target = resolveProjectSourceTarget(source, projects);
|
|
403
|
+
if (target.error) return { error: target.error };
|
|
404
|
+
const items = shared.safeJson(target.wiPath) || [];
|
|
405
|
+
return { ...target, found: items.some(i => i.id === id) };
|
|
387
406
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
};
|
|
407
|
+
|
|
408
|
+
const central = resolveProjectSourceTarget('central', projects);
|
|
409
|
+
const centralItems = shared.safeJson(central.wiPath) || [];
|
|
410
|
+
if (centralItems.some(i => i.id === id)) return { ...central, found: true };
|
|
411
|
+
for (const project of projects) {
|
|
412
|
+
const target = resolveProjectSourceTarget(project.name, projects);
|
|
413
|
+
const items = shared.safeJson(target.wiPath) || [];
|
|
414
|
+
if (items.some(i => i.id === id)) return { ...target, found: true };
|
|
415
|
+
}
|
|
416
|
+
return { found: false };
|
|
392
417
|
}
|
|
393
418
|
|
|
394
419
|
function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
@@ -400,6 +425,7 @@ function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
|
400
425
|
if (value.project !== undefined) collect(value.project);
|
|
401
426
|
else if (value._project !== undefined) collect(value._project);
|
|
402
427
|
else if (value.name !== undefined) collect(value.name);
|
|
428
|
+
else if (value.localPath !== undefined) collect(value.localPath);
|
|
403
429
|
return;
|
|
404
430
|
}
|
|
405
431
|
refs.push(String(value));
|
|
@@ -463,9 +489,12 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
463
489
|
}
|
|
464
490
|
const projects = shared.getProjects(config);
|
|
465
491
|
const explicitProjectName = String(projectName || '').trim();
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
492
|
+
const explicitProject = explicitProjectName
|
|
493
|
+
? shared.resolveProjectSource(explicitProjectName, projects, { allowCentral: false, minionsDir: MINIONS_DIR })
|
|
494
|
+
: null;
|
|
495
|
+
let targetProject = explicitProject?.project || null;
|
|
496
|
+
if (explicitProject?.error) {
|
|
497
|
+
const err = new Error(explicitProject.error);
|
|
469
498
|
err.statusCode = 400;
|
|
470
499
|
throw err;
|
|
471
500
|
}
|
|
@@ -474,7 +503,7 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
474
503
|
const matches = prScope ? projects.filter(p => shared.getProjectPrScope(p) === prScope) : [];
|
|
475
504
|
if (matches.length === 1) targetProject = matches[0];
|
|
476
505
|
}
|
|
477
|
-
const prPath = targetProject ? shared.projectPrPath(targetProject) :
|
|
506
|
+
const prPath = targetProject ? shared.projectPrPath(targetProject) : shared.centralPullRequestsPath(MINIONS_DIR);
|
|
478
507
|
|
|
479
508
|
const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
|
|
480
509
|
const prNum = prNumMatch ? (prNumMatch[1] || prNumMatch[2]) : Date.now().toString().slice(-6);
|
|
@@ -2665,6 +2694,8 @@ function normalizePipelineForCompare(pipeline) {
|
|
|
2665
2694
|
stages: Array.isArray(pipeline.stages) ? pipeline.stages : [],
|
|
2666
2695
|
trigger: pipeline.trigger && typeof pipeline.trigger === 'object' ? pipeline.trigger : {},
|
|
2667
2696
|
enabled: pipeline.enabled !== false,
|
|
2697
|
+
project: pipeline.project !== undefined ? pipeline.project : null,
|
|
2698
|
+
projects: Array.isArray(pipeline.projects) ? pipeline.projects : [],
|
|
2668
2699
|
stopWhen: pipeline.stopWhen || null,
|
|
2669
2700
|
monitoredResources: Array.isArray(pipeline.monitoredResources) ? pipeline.monitoredResources : [],
|
|
2670
2701
|
};
|
|
@@ -2678,6 +2709,8 @@ function buildPipelineFromAction(action) {
|
|
|
2678
2709
|
trigger: action.trigger && typeof action.trigger === 'object' ? action.trigger : {},
|
|
2679
2710
|
enabled: action.enabled !== false,
|
|
2680
2711
|
};
|
|
2712
|
+
if (action.project !== undefined) pipeline.project = action.project;
|
|
2713
|
+
if (Array.isArray(action.projects)) pipeline.projects = action.projects;
|
|
2681
2714
|
if (action.stopWhen) pipeline.stopWhen = action.stopWhen;
|
|
2682
2715
|
if (Array.isArray(action.monitoredResources) && action.monitoredResources.length > 0) {
|
|
2683
2716
|
pipeline.monitoredResources = action.monitoredResources;
|
|
@@ -2692,6 +2725,10 @@ function pipelineDefinitionsEqual(a, b) {
|
|
|
2692
2725
|
function createPipelineFromAction(action) {
|
|
2693
2726
|
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
2694
2727
|
const pipeline = buildPipelineFromAction(action);
|
|
2728
|
+
const projectError = validatePipelineProjects(pipeline);
|
|
2729
|
+
if (projectError) {
|
|
2730
|
+
return { type: 'create-pipeline', id: pipeline.id, error: projectError };
|
|
2731
|
+
}
|
|
2695
2732
|
const existing = getPipeline(pipeline.id);
|
|
2696
2733
|
if (existing) {
|
|
2697
2734
|
if (pipelineDefinitionsEqual(existing, pipeline)) {
|
|
@@ -3057,7 +3094,9 @@ async function _ccExecuteLocalApiAction(action) {
|
|
|
3057
3094
|
|
|
3058
3095
|
async function executeCCActions(actions, { source = 'command-center', inferredProject = null } = {}) {
|
|
3059
3096
|
const results = [];
|
|
3060
|
-
|
|
3097
|
+
const dispatchIdsCreatedInThisCall = new Map();
|
|
3098
|
+
for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
|
|
3099
|
+
const rawAction = actions[actionIndex];
|
|
3061
3100
|
const action = normalizeCCAction(rawAction);
|
|
3062
3101
|
if (action?._intentFallbackError) {
|
|
3063
3102
|
results.push({
|
|
@@ -3088,22 +3127,22 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3088
3127
|
// projects → root-level work-items.json (orchestration system standalone use).
|
|
3089
3128
|
let targetProject = null;
|
|
3090
3129
|
if (project) {
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
results.push({ type: action.type, error:
|
|
3130
|
+
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3131
|
+
targetProject = target.project;
|
|
3132
|
+
if (target.error) {
|
|
3133
|
+
results.push({ type: action.type, error: target.error });
|
|
3095
3134
|
break;
|
|
3096
3135
|
}
|
|
3097
3136
|
} else if (prRef) {
|
|
3098
3137
|
const allPrs = getPullRequests().filter(p => !p._ghost);
|
|
3099
3138
|
linkedPr = shared.findPrRecord(allPrs, prRef) || null;
|
|
3100
3139
|
if (linkedPr?._project && linkedPr._project !== 'central') {
|
|
3101
|
-
targetProject =
|
|
3140
|
+
targetProject = resolveProjectSourceTarget(linkedPr._project, PROJECTS, { allowCentral: false }).project || null;
|
|
3102
3141
|
}
|
|
3103
3142
|
} else if (inferredProject) {
|
|
3104
3143
|
// Doc-chat fallback: filePath-derived project when the LLM omits the field. Validated against
|
|
3105
3144
|
// PROJECTS upstream by _inferDocChatProject — a stale lookup would just yield null here.
|
|
3106
|
-
targetProject =
|
|
3145
|
+
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
3107
3146
|
}
|
|
3108
3147
|
if (!targetProject) {
|
|
3109
3148
|
if (PROJECTS.length > 1) {
|
|
@@ -3124,7 +3163,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3124
3163
|
break;
|
|
3125
3164
|
}
|
|
3126
3165
|
|
|
3127
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) :
|
|
3166
|
+
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
3128
3167
|
|
|
3129
3168
|
// Promote `agent` (singular) → `agents` (array). Models emit either shape and the prior code
|
|
3130
3169
|
// only read `action.agents`, silently dropping `agent: "lambert"` style hints.
|
|
@@ -3157,9 +3196,19 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3157
3196
|
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
3158
3197
|
if (!createResult.created) {
|
|
3159
3198
|
const duplicateId = createResult.duplicateOf || createResult.item?.id;
|
|
3199
|
+
if (duplicateId && dispatchIdsCreatedInThisCall.has(duplicateId)) {
|
|
3200
|
+
results.push({
|
|
3201
|
+
type: action.type,
|
|
3202
|
+
id: duplicateId,
|
|
3203
|
+
ok: true,
|
|
3204
|
+
reusedFromAction: dispatchIdsCreatedInThisCall.get(duplicateId),
|
|
3205
|
+
});
|
|
3206
|
+
break;
|
|
3207
|
+
}
|
|
3160
3208
|
results.push({ type: action.type, id: duplicateId, ok: true, duplicate: true, duplicateOf: duplicateId });
|
|
3161
3209
|
break;
|
|
3162
3210
|
}
|
|
3211
|
+
dispatchIdsCreatedInThisCall.set(id, actionIndex);
|
|
3163
3212
|
results.push({ type: action.type, id, ok: true });
|
|
3164
3213
|
|
|
3165
3214
|
// Pre-flight routing check: warn the user if no agent is currently available so the new
|
|
@@ -3187,7 +3236,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3187
3236
|
// unresolved → error so build-and-test can't accidentally run against the wrong repo.
|
|
3188
3237
|
const projectName = action.project || pr._project || null;
|
|
3189
3238
|
const project = projectName
|
|
3190
|
-
?
|
|
3239
|
+
? resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false }).project
|
|
3191
3240
|
: null;
|
|
3192
3241
|
if (!project) {
|
|
3193
3242
|
results.push({ type: 'build-and-test', error: `Project not found for PR ${pr.id}: ${projectName || '(none)'}` });
|
|
@@ -3248,14 +3297,14 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3248
3297
|
const project = action.project || '';
|
|
3249
3298
|
let targetProject = null;
|
|
3250
3299
|
if (project) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
results.push({ type: 'reopen-work-item', id: action.id, error:
|
|
3300
|
+
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3301
|
+
targetProject = target.project;
|
|
3302
|
+
if (target.error) {
|
|
3303
|
+
results.push({ type: 'reopen-work-item', id: action.id, error: target.error });
|
|
3255
3304
|
break;
|
|
3256
3305
|
}
|
|
3257
3306
|
} else if (inferredProject) {
|
|
3258
|
-
targetProject =
|
|
3307
|
+
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
3259
3308
|
}
|
|
3260
3309
|
if (!targetProject) {
|
|
3261
3310
|
if (PROJECTS.length > 1) {
|
|
@@ -3264,7 +3313,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3264
3313
|
}
|
|
3265
3314
|
if (PROJECTS.length === 1) targetProject = PROJECTS[0];
|
|
3266
3315
|
}
|
|
3267
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) :
|
|
3316
|
+
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
3268
3317
|
let reopenResult = null;
|
|
3269
3318
|
mutateJsonFileLocked(wiPath, items => {
|
|
3270
3319
|
if (!Array.isArray(items)) items = [];
|
|
@@ -3383,6 +3432,7 @@ function _buildDocChatActionFeedback(actions, actionResults) {
|
|
|
3383
3432
|
feedback.push({ type, error: String(result.error) });
|
|
3384
3433
|
continue;
|
|
3385
3434
|
}
|
|
3435
|
+
if (result.reusedFromAction !== undefined) continue;
|
|
3386
3436
|
const id = result.id || result.duplicateOf;
|
|
3387
3437
|
if (!result.ok || !id) continue;
|
|
3388
3438
|
const item = { type, id: String(id), ok: true };
|
|
@@ -3844,7 +3894,7 @@ function _inferDocChatProject(filePath) {
|
|
|
3844
3894
|
if (!filePath) return null;
|
|
3845
3895
|
const m = String(filePath).replace(/\\/g, '/').match(/^projects\/([^/]+)\//);
|
|
3846
3896
|
if (!m) return null;
|
|
3847
|
-
const inferred =
|
|
3897
|
+
const inferred = resolveProjectSourceTarget(m[1], PROJECTS, { allowCentral: false }).project;
|
|
3848
3898
|
return inferred?.name || null;
|
|
3849
3899
|
}
|
|
3850
3900
|
|
|
@@ -4531,14 +4581,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
4531
4581
|
}
|
|
4532
4582
|
|
|
4533
4583
|
const config = queries.getConfig();
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4584
|
+
// safeJsonNoRestore — never resurrect an archived PRD's .backup sidecar
|
|
4585
|
+
// during project resolution (W-mouptdh1000h9f39). The active-from-archive
|
|
4586
|
+
// write above (mutateJsonFileLocked) already created activePath when needed.
|
|
4587
|
+
const projectPlan = safeJsonNoRestore(activePath) || safeJsonNoRestore(prdPath);
|
|
4588
|
+
const projectTarget = projectPlan?.project
|
|
4589
|
+
? resolveProjectSourceTarget(projectPlan.project, PROJECTS, { allowCentral: false })
|
|
4590
|
+
: null;
|
|
4591
|
+
const project = projectTarget?.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
|
|
4542
4592
|
|
|
4543
4593
|
// Check for existing verify WI — reset to pending if already done (re-verify)
|
|
4544
4594
|
if (project) {
|
|
@@ -4592,25 +4642,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4592
4642
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4593
4643
|
|
|
4594
4644
|
// Find the right file — check source first, then search all project files
|
|
4595
|
-
let
|
|
4596
|
-
if (
|
|
4597
|
-
|
|
4598
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4599
|
-
}
|
|
4600
|
-
if (!wiPath) {
|
|
4601
|
-
// Search central first, then all projects
|
|
4602
|
-
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4603
|
-
const centralItems = shared.safeJson(centralPath) || [];
|
|
4604
|
-
if (centralItems.some(i => i.id === id)) {
|
|
4605
|
-
wiPath = centralPath;
|
|
4606
|
-
} else {
|
|
4607
|
-
for (const proj of PROJECTS) {
|
|
4608
|
-
const projPath = shared.projectWorkItemsPath(proj);
|
|
4609
|
-
const projItems = shared.safeJson(projPath) || [];
|
|
4610
|
-
if (projItems.some(i => i.id === id)) { wiPath = projPath; break; }
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
4613
|
-
}
|
|
4645
|
+
let resolvedTarget = findWorkItemsTargetById(id, source, PROJECTS);
|
|
4646
|
+
if (resolvedTarget.error) return jsonReply(res, 404, { error: resolvedTarget.error });
|
|
4647
|
+
let wiPath = resolvedTarget.found ? resolvedTarget.wiPath : null;
|
|
4614
4648
|
// If no work item found, attempt to re-materialize from PRD item definition
|
|
4615
4649
|
if (!wiPath) {
|
|
4616
4650
|
const prdFile = body.prdFile;
|
|
@@ -4629,8 +4663,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4629
4663
|
|
|
4630
4664
|
// Determine target work-items file (project from PRD item or plan, fallback to central)
|
|
4631
4665
|
const projName = prdItem.project || plan.project || prdFile.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
4632
|
-
const
|
|
4633
|
-
const
|
|
4666
|
+
const prdTarget = resolveProjectSourceTarget(projName, PROJECTS, { allowCentral: false });
|
|
4667
|
+
const proj = prdTarget.project;
|
|
4668
|
+
const targetWiPath = proj ? shared.projectWorkItemsPath(proj) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
4634
4669
|
|
|
4635
4670
|
// Create new work item from PRD item definition (same logic as materializePlansAsWorkItems)
|
|
4636
4671
|
const complexity = prdItem.estimated_complexity || 'medium';
|
|
@@ -4662,8 +4697,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4662
4697
|
|
|
4663
4698
|
// Clear dispatch history and cooldowns for this item
|
|
4664
4699
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4665
|
-
const
|
|
4666
|
-
const dispatchKey = sourcePrefix + id;
|
|
4700
|
+
const dispatchKey = (proj ? `work-${proj.name}-` : 'central-work-') + id;
|
|
4667
4701
|
try {
|
|
4668
4702
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
4669
4703
|
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
@@ -4708,8 +4742,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4708
4742
|
|
|
4709
4743
|
// Clear completed dispatch entries so the engine doesn't dedup this item
|
|
4710
4744
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4711
|
-
const
|
|
4712
|
-
const dispatchKey = sourcePrefix + id;
|
|
4745
|
+
const dispatchKey = dispatchPrefixForResolvedSource(resolvedTarget) + id;
|
|
4713
4746
|
try {
|
|
4714
4747
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
4715
4748
|
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
@@ -4737,17 +4770,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4737
4770
|
const { id, source } = body;
|
|
4738
4771
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4739
4772
|
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4744
|
-
} else {
|
|
4745
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4746
|
-
if (proj) {
|
|
4747
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
4748
|
-
}
|
|
4749
|
-
}
|
|
4750
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4773
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4774
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4775
|
+
const wiPath = target.wiPath;
|
|
4751
4776
|
|
|
4752
4777
|
let item = null;
|
|
4753
4778
|
let found = false;
|
|
@@ -4797,15 +4822,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4797
4822
|
const { id, source, reason } = body;
|
|
4798
4823
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4799
4824
|
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4804
|
-
} else {
|
|
4805
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4806
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4807
|
-
}
|
|
4808
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4825
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4826
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4827
|
+
const wiPath = target.wiPath;
|
|
4809
4828
|
|
|
4810
4829
|
let result = null;
|
|
4811
4830
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4853,16 +4872,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4853
4872
|
const { id, source } = body;
|
|
4854
4873
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4855
4874
|
|
|
4856
|
-
|
|
4857
|
-
if (
|
|
4858
|
-
|
|
4859
|
-
} else {
|
|
4860
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4861
|
-
if (proj) {
|
|
4862
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
4863
|
-
}
|
|
4864
|
-
}
|
|
4865
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4875
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4876
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4877
|
+
const wiPath = target.wiPath;
|
|
4866
4878
|
|
|
4867
4879
|
let archivedItem = null;
|
|
4868
4880
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4884,7 +4896,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4884
4896
|
}, { defaultValue: [] });
|
|
4885
4897
|
|
|
4886
4898
|
// Clean dispatch entries for archived item
|
|
4887
|
-
const sourcePrefix = (
|
|
4899
|
+
const sourcePrefix = dispatchPrefixForResolvedSource(target);
|
|
4888
4900
|
cleanDispatchEntries(d =>
|
|
4889
4901
|
d.meta?.dispatchKey === sourcePrefix + id ||
|
|
4890
4902
|
d.meta?.item?.id === id
|
|
@@ -4910,15 +4922,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4910
4922
|
const project = body.project || body.source;
|
|
4911
4923
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4912
4924
|
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4917
|
-
} else {
|
|
4918
|
-
const proj = PROJECTS.find(p => p.name === project);
|
|
4919
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4920
|
-
}
|
|
4921
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'project not found' });
|
|
4925
|
+
const target = resolveProjectSourceTarget(project, PROJECTS);
|
|
4926
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4927
|
+
const wiPath = target.wiPath;
|
|
4922
4928
|
|
|
4923
4929
|
let result = null;
|
|
4924
4930
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4938,8 +4944,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4938
4944
|
if (result.code !== 200) return jsonReply(res, result.code, result.body);
|
|
4939
4945
|
|
|
4940
4946
|
// Clear dispatch history and cooldowns outside lock
|
|
4941
|
-
const
|
|
4942
|
-
const dispatchKey = sourcePrefix + id;
|
|
4947
|
+
const dispatchKey = dispatchPrefixForResolvedSource(target) + id;
|
|
4943
4948
|
try {
|
|
4944
4949
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4945
4950
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
@@ -5005,16 +5010,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
5005
5010
|
const { id, source, title, description, type, priority, agent } = body;
|
|
5006
5011
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
5007
5012
|
|
|
5008
|
-
|
|
5009
|
-
if (
|
|
5010
|
-
|
|
5011
|
-
} else {
|
|
5012
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
5013
|
-
if (proj) {
|
|
5014
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
5015
|
-
}
|
|
5016
|
-
}
|
|
5017
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
5013
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
5014
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
5015
|
+
const wiPath = target.wiPath;
|
|
5018
5016
|
|
|
5019
5017
|
let result = null;
|
|
5020
5018
|
let agentChanged = false;
|
|
@@ -5097,7 +5095,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5097
5095
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
5098
5096
|
const plan = {
|
|
5099
5097
|
version: 'manual-' + new Date().toISOString().slice(0, 10),
|
|
5100
|
-
project: target.project?.name ||
|
|
5098
|
+
project: target.project?.name || 'Unknown',
|
|
5101
5099
|
generated_by: 'dashboard',
|
|
5102
5100
|
generated_at: new Date().toISOString().slice(0, 10),
|
|
5103
5101
|
plan_summary: body.name,
|
|
@@ -5844,7 +5842,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
5844
5842
|
}).join('\n');
|
|
5845
5843
|
|
|
5846
5844
|
const projectName = plan.project || body.file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
5847
|
-
const
|
|
5845
|
+
const projectTarget = resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false });
|
|
5846
|
+
const targetProject = projectTarget.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
|
|
5848
5847
|
if (targetProject) {
|
|
5849
5848
|
diffAwareQueued = shared.queuePlanToPrd({
|
|
5850
5849
|
planFile: plan.source_plan, prdFile: body.file,
|
|
@@ -6808,7 +6807,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6808
6807
|
let alreadyLinked = false;
|
|
6809
6808
|
mutateDashboardConfig(config => {
|
|
6810
6809
|
if (!Array.isArray(config.projects)) config.projects = [];
|
|
6811
|
-
alreadyLinked = config.projects.some(p =>
|
|
6810
|
+
alreadyLinked = config.projects.some(p => shared.sameResolvedPath(p.localPath, target));
|
|
6812
6811
|
return config;
|
|
6813
6812
|
});
|
|
6814
6813
|
if (alreadyLinked) {
|
|
@@ -6853,7 +6852,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6853
6852
|
let duplicate = false;
|
|
6854
6853
|
mutateDashboardConfig(config => {
|
|
6855
6854
|
if (!Array.isArray(config.projects)) config.projects = [];
|
|
6856
|
-
if (config.projects.some(p =>
|
|
6855
|
+
if (config.projects.some(p => shared.sameResolvedPath(p.localPath, target))) {
|
|
6857
6856
|
duplicate = true;
|
|
6858
6857
|
return config;
|
|
6859
6858
|
}
|
|
@@ -7444,9 +7443,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7444
7443
|
const body = await readBody(req);
|
|
7445
7444
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
7446
7445
|
if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
|
|
7447
|
-
|
|
7446
|
+
reloadConfig();
|
|
7447
|
+
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
7448
7448
|
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7449
|
-
project = projectTarget.project
|
|
7449
|
+
project = projectTarget.project || null;
|
|
7450
7450
|
|
|
7451
7451
|
// Auto-generate ID from title if not provided
|
|
7452
7452
|
if (!id) {
|
|
@@ -7481,10 +7481,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7481
7481
|
const body = await readBody(req);
|
|
7482
7482
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
7483
7483
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
7484
|
+
reloadConfig();
|
|
7484
7485
|
if (project !== undefined) {
|
|
7485
|
-
const projectTarget =
|
|
7486
|
+
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
7486
7487
|
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7487
|
-
project = projectTarget.project
|
|
7488
|
+
project = projectTarget.project || null;
|
|
7488
7489
|
}
|
|
7489
7490
|
|
|
7490
7491
|
let missingSchedules = false;
|
|
@@ -7546,16 +7547,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7546
7547
|
reloadConfig();
|
|
7547
7548
|
const sched = (CONFIG.schedules || []).find(s => s.id === id);
|
|
7548
7549
|
if (!sched) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
sched.project = projectTarget.project.name;
|
|
7553
|
-
}
|
|
7550
|
+
const projectTarget = resolveScheduleProjectValue(sched.project, PROJECTS);
|
|
7551
|
+
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7552
|
+
const schedForRun = { ...sched, project: projectTarget.project || null };
|
|
7554
7553
|
|
|
7555
7554
|
const schedulerMod = require('./engine/scheduler');
|
|
7556
7555
|
let item;
|
|
7557
7556
|
try {
|
|
7558
|
-
item = schedulerMod.createScheduledWorkItem(
|
|
7557
|
+
item = schedulerMod.createScheduledWorkItem(schedForRun);
|
|
7559
7558
|
} catch (e) {
|
|
7560
7559
|
return jsonReply(res, 400, { error: e.message });
|
|
7561
7560
|
}
|
|
@@ -7566,7 +7565,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7566
7565
|
meeting = createMeeting({
|
|
7567
7566
|
title: item.title,
|
|
7568
7567
|
agenda: item.description,
|
|
7569
|
-
participants: Array.isArray(
|
|
7568
|
+
participants: Array.isArray(schedForRun.participants) ? schedForRun.participants : [],
|
|
7570
7569
|
});
|
|
7571
7570
|
} else {
|
|
7572
7571
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
@@ -7585,22 +7584,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7585
7584
|
return jsonReply(res, 409, {
|
|
7586
7585
|
error: 'Schedule already has an active work item',
|
|
7587
7586
|
id: duplicate.id,
|
|
7588
|
-
scheduleId:
|
|
7587
|
+
scheduleId: schedForRun.id,
|
|
7589
7588
|
});
|
|
7590
7589
|
}
|
|
7591
7590
|
}
|
|
7592
7591
|
|
|
7593
|
-
const runEntry = schedulerMod.recordScheduleRun(
|
|
7592
|
+
const runEntry = schedulerMod.recordScheduleRun(schedForRun.id, item.id);
|
|
7594
7593
|
try {
|
|
7595
7594
|
shared.mutateControl(control => ({ ...control, _wakeupAt: Date.now() }));
|
|
7596
7595
|
} catch (e) {
|
|
7597
|
-
shared.log('warn', `Schedule run-now wakeup failed for ${
|
|
7596
|
+
shared.log('warn', `Schedule run-now wakeup failed for ${schedForRun.id}: ${e.message}`);
|
|
7598
7597
|
}
|
|
7599
7598
|
invalidateStatusCache();
|
|
7600
7599
|
return jsonReply(res, 200, {
|
|
7601
7600
|
ok: true,
|
|
7602
7601
|
id: item.id,
|
|
7603
|
-
scheduleId:
|
|
7602
|
+
scheduleId: schedForRun.id,
|
|
7604
7603
|
lastRun: runEntry?.lastRun || null,
|
|
7605
7604
|
...(meeting ? { meetingId: meeting.id } : {}),
|
|
7606
7605
|
});
|
|
@@ -8008,7 +8007,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8008
8007
|
if (body.projects && Array.isArray(body.projects)) {
|
|
8009
8008
|
if (!config.projects) config.projects = [];
|
|
8010
8009
|
for (const update of body.projects) {
|
|
8011
|
-
const proj = config.projects
|
|
8010
|
+
const proj = shared.findProjectByName(config.projects, update.name);
|
|
8012
8011
|
if (!proj) continue;
|
|
8013
8012
|
if (!proj.workSources) proj.workSources = {};
|
|
8014
8013
|
if (update.workSources?.pullRequests !== undefined) {
|
|
@@ -9042,6 +9041,7 @@ module.exports = {
|
|
|
9042
9041
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
9043
9042
|
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
9044
9043
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
9044
|
+
_resolveScheduleProjectValue: resolveScheduleProjectValue,
|
|
9045
9045
|
_collectArchivedWorkItems: collectArchivedWorkItems,
|
|
9046
9046
|
_createPipelineFromAction: createPipelineFromAction,
|
|
9047
9047
|
_setCcLocalApiInvokerForTest,
|