@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/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/issues.js
CHANGED
|
@@ -9,6 +9,7 @@ const shared = require('./shared');
|
|
|
9
9
|
|
|
10
10
|
const DEFAULT_REPO = 'yemi33/minions';
|
|
11
11
|
const DEFAULT_LABELS = ['bug'];
|
|
12
|
+
const WRITABLE_REPO_PERMISSIONS = new Set(['WRITE', 'MAINTAIN', 'ADMIN']);
|
|
12
13
|
|
|
13
14
|
class GitHubIssueError extends Error {
|
|
14
15
|
constructor(message, statusCode = 500) {
|
|
@@ -67,16 +68,105 @@ function extractIssueUrl(output) {
|
|
|
67
68
|
return match ? match[0] : null;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function runGh(execFileSync, args, timeout) {
|
|
71
|
-
|
|
71
|
+
function runGh(execFileSync, args, timeout, { env } = {}) {
|
|
72
|
+
const opts = {
|
|
72
73
|
encoding: 'utf8',
|
|
73
74
|
timeout,
|
|
74
75
|
windowsHide: true,
|
|
75
|
-
}
|
|
76
|
+
};
|
|
77
|
+
if (env) opts.env = { ...process.env, ...env };
|
|
78
|
+
return execFileSync('gh', args, opts);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isWritableRepoPermission(permission) {
|
|
82
|
+
return WRITABLE_REPO_PERMISSIONS.has(String(permission || '').trim().toUpperCase());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function repoViewerPermission({ repo, execFileSync, ghEnv }) {
|
|
86
|
+
const output = runGh(execFileSync, ['repo', 'view', repo, '--json', 'viewerPermission'], 10000, { env: ghEnv });
|
|
87
|
+
const parsed = JSON.parse(output || '{}');
|
|
88
|
+
return String(parsed.viewerPermission || '').trim().toUpperCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseGhAuthUsers(output) {
|
|
92
|
+
const users = [];
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
95
|
+
const match = line.match(/\baccount\s+([^\s()]+)/i) || line.match(/\bas\s+([^\s()]+)/i);
|
|
96
|
+
if (!match) continue;
|
|
97
|
+
const user = match[1].trim();
|
|
98
|
+
const key = user.toLowerCase();
|
|
99
|
+
if (!user || seen.has(key)) continue;
|
|
100
|
+
seen.add(key);
|
|
101
|
+
users.push(user);
|
|
102
|
+
}
|
|
103
|
+
return users;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listAuthenticatedGhUsers({ execFileSync }) {
|
|
107
|
+
const output = runGh(execFileSync, ['auth', 'status', '--hostname', 'github.com'], 10000);
|
|
108
|
+
return parseGhAuthUsers(output);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ghTokenForUser({ user, execFileSync }) {
|
|
112
|
+
return String(runGh(execFileSync, ['auth', 'token', '--hostname', 'github.com', '--user', user], 10000) || '').trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveWritableGitHubEnv({ repo, execFileSync }) {
|
|
116
|
+
let activePermission = '';
|
|
117
|
+
let activeError = null;
|
|
118
|
+
try {
|
|
119
|
+
activePermission = repoViewerPermission({ repo, execFileSync });
|
|
120
|
+
if (isWritableRepoPermission(activePermission)) return undefined;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
activeError = e;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let users = [];
|
|
126
|
+
let accountListError = null;
|
|
127
|
+
try {
|
|
128
|
+
users = listAuthenticatedGhUsers({ execFileSync });
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (activeError && isAuthError(activeError) && isAuthError(e)) {
|
|
131
|
+
throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
|
|
132
|
+
}
|
|
133
|
+
accountListError = e;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const candidateErrors = [];
|
|
137
|
+
for (const user of users) {
|
|
138
|
+
try {
|
|
139
|
+
const token = ghTokenForUser({ user, execFileSync });
|
|
140
|
+
if (!token) continue;
|
|
141
|
+
const ghEnv = { GH_TOKEN: token };
|
|
142
|
+
const permission = repoViewerPermission({ repo, execFileSync, ghEnv });
|
|
143
|
+
if (isWritableRepoPermission(permission)) return ghEnv;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
candidateErrors.push(`${user}: ${conciseGhMessage(e)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const activeDetail = activePermission
|
|
150
|
+
? ` Active account permission is ${activePermission}.`
|
|
151
|
+
: activeError
|
|
152
|
+
? ` Active account permission could not be checked: ${conciseGhMessage(activeError)}.`
|
|
153
|
+
: '';
|
|
154
|
+
const authStatusDetail = accountListError
|
|
155
|
+
? ` Authenticated accounts could not be listed: ${conciseGhMessage(accountListError)}.`
|
|
156
|
+
: '';
|
|
157
|
+
const candidateDetail = candidateErrors.length
|
|
158
|
+
? ` Checked ${candidateErrors.length} authenticated account(s) without finding write access.`
|
|
159
|
+
: '';
|
|
160
|
+
throw new GitHubIssueError(
|
|
161
|
+
`No authenticated GitHub account with write access to ${repo}.${activeDetail} ` +
|
|
162
|
+
`${authStatusDetail}${candidateDetail} ` +
|
|
163
|
+
`Run: gh auth login --hostname github.com with a profile that can write to ${repo}, or set GH_TOKEN for that account.`,
|
|
164
|
+
403
|
|
165
|
+
);
|
|
76
166
|
}
|
|
77
167
|
|
|
78
|
-
function listRepoLabels({ repo, execFileSync }) {
|
|
79
|
-
const output = runGh(execFileSync, ['label', 'list', '--repo', repo, '--json', 'name', '--limit', '1000'], 15000);
|
|
168
|
+
function listRepoLabels({ repo, execFileSync, ghEnv }) {
|
|
169
|
+
const output = runGh(execFileSync, ['label', 'list', '--repo', repo, '--json', 'name', '--limit', '1000'], 15000, { env: ghEnv });
|
|
80
170
|
const parsed = JSON.parse(output || '[]');
|
|
81
171
|
if (!Array.isArray(parsed)) {
|
|
82
172
|
throw new GitHubIssueError('GitHub label list returned an unexpected response shape');
|
|
@@ -89,14 +179,14 @@ function listRepoLabels({ repo, execFileSync }) {
|
|
|
89
179
|
return labelsByLower;
|
|
90
180
|
}
|
|
91
181
|
|
|
92
|
-
function resolveLabels({ labels, repo, execFileSync }) {
|
|
182
|
+
function resolveLabels({ labels, repo, execFileSync, ghEnv }) {
|
|
93
183
|
const requested = normalizeLabels(labels);
|
|
94
184
|
if (requested.length === 0) {
|
|
95
185
|
return { requested, labelsToApply: [], labelsSkipped: [], validationUnavailable: false };
|
|
96
186
|
}
|
|
97
187
|
|
|
98
188
|
try {
|
|
99
|
-
const available = listRepoLabels({ repo, execFileSync });
|
|
189
|
+
const available = listRepoLabels({ repo, execFileSync, ghEnv });
|
|
100
190
|
const labelsToApply = [];
|
|
101
191
|
const labelsSkipped = [];
|
|
102
192
|
for (const label of requested) {
|
|
@@ -175,10 +265,10 @@ function _redactIssueContent(value, { repo, projects } = {}) {
|
|
|
175
265
|
});
|
|
176
266
|
}
|
|
177
267
|
|
|
178
|
-
function createIssueWithLabels({ title, bodyFile, repo, labels, execFileSync }) {
|
|
268
|
+
function createIssueWithLabels({ title, bodyFile, repo, labels, execFileSync, ghEnv }) {
|
|
179
269
|
const args = ['issue', 'create', '--repo', repo, '--title', title, '--body-file', bodyFile];
|
|
180
270
|
if (labels.length > 0) args.push('--label', labels.join(','));
|
|
181
|
-
const output = runGh(execFileSync, args, 30000);
|
|
271
|
+
const output = runGh(execFileSync, args, 30000, { env: ghEnv });
|
|
182
272
|
const url = extractIssueUrl(output);
|
|
183
273
|
if (!url) {
|
|
184
274
|
throw new GitHubIssueError(`Issue may not have been created: ${conciseGhMessage(output)}`);
|
|
@@ -202,6 +292,7 @@ function createGitHubIssue({
|
|
|
202
292
|
} catch (e) {
|
|
203
293
|
throw new GitHubIssueError('gh CLI not found. Install from https://cli.github.com/');
|
|
204
294
|
}
|
|
295
|
+
const ghEnv = resolveWritableGitHubEnv({ repo, execFileSync });
|
|
205
296
|
|
|
206
297
|
const redactionProjects = projects || shared.getProjects();
|
|
207
298
|
const safeTitle = _redactIssueContent(title, { repo, projects: redactionProjects });
|
|
@@ -214,13 +305,14 @@ function createGitHubIssue({
|
|
|
214
305
|
|
|
215
306
|
let resolved;
|
|
216
307
|
try {
|
|
217
|
-
resolved = resolveLabels({ labels, repo, execFileSync });
|
|
308
|
+
resolved = resolveLabels({ labels, repo, execFileSync, ghEnv });
|
|
218
309
|
const created = createIssueWithLabels({
|
|
219
310
|
title: safeTitle,
|
|
220
311
|
bodyFile,
|
|
221
312
|
repo,
|
|
222
313
|
labels: resolved.labelsToApply,
|
|
223
314
|
execFileSync,
|
|
315
|
+
ghEnv,
|
|
224
316
|
});
|
|
225
317
|
const filedWithoutLabels = resolved.requested.length > 0 && resolved.labelsToApply.length === 0;
|
|
226
318
|
return {
|
|
@@ -237,7 +329,7 @@ function createGitHubIssue({
|
|
|
237
329
|
if (isAuthError(e)) throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
|
|
238
330
|
if (resolved && resolved.labelsToApply.length > 0 && isLabelUnavailableError(e)) {
|
|
239
331
|
try {
|
|
240
|
-
const created = createIssueWithLabels({ title: safeTitle, bodyFile, repo, labels: [], execFileSync });
|
|
332
|
+
const created = createIssueWithLabels({ title: safeTitle, bodyFile, repo, labels: [], execFileSync, ghEnv });
|
|
241
333
|
const skipped = normalizeLabels([...resolved.labelsSkipped, ...resolved.labelsToApply], []);
|
|
242
334
|
return {
|
|
243
335
|
ok: true,
|
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
|
@@ -205,19 +205,26 @@ function _pipelineProjectValues(...values) {
|
|
|
205
205
|
for (const value of values) {
|
|
206
206
|
if (value === undefined) continue;
|
|
207
207
|
if (Array.isArray(value)) out.push(...value);
|
|
208
|
+
else if (value && typeof value === 'object' && value.project !== undefined) out.push(value.project);
|
|
209
|
+
else if (value && typeof value === 'object' && value._project !== undefined) out.push(value._project);
|
|
208
210
|
else out.push(value);
|
|
209
211
|
}
|
|
210
212
|
return out;
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
function
|
|
215
|
+
function _pipelineProjectValuePresent(value) {
|
|
216
|
+
if (value === undefined) return false;
|
|
217
|
+
if (value === null) return true;
|
|
218
|
+
if (typeof value === 'string') return value.trim() !== '';
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _resolvePipelineProjectValues(rawValues, config) {
|
|
214
223
|
const projects = shared.getProjects(config);
|
|
215
|
-
const rawValues = _pipelineProjectValues(...values);
|
|
216
|
-
if (rawValues.length === 0) return { projects: [null] };
|
|
217
224
|
const resolved = [];
|
|
218
225
|
const seen = new Set();
|
|
219
226
|
for (const raw of rawValues) {
|
|
220
|
-
if (raw
|
|
227
|
+
if (!_pipelineProjectValuePresent(raw)) continue;
|
|
221
228
|
if (raw === null || String(raw).trim().toLowerCase() === 'central') {
|
|
222
229
|
if (!seen.has('central')) { seen.add('central'); resolved.push(null); }
|
|
223
230
|
continue;
|
|
@@ -230,6 +237,16 @@ function _resolvePipelineProjects(values, config) {
|
|
|
230
237
|
return { projects: resolved.length ? resolved : [null] };
|
|
231
238
|
}
|
|
232
239
|
|
|
240
|
+
function _resolvePipelineProjects(scopes, config) {
|
|
241
|
+
for (const scope of (Array.isArray(scopes) ? scopes : [scopes])) {
|
|
242
|
+
const values = _pipelineProjectValues(...(Array.isArray(scope) ? scope : [scope]))
|
|
243
|
+
.filter(_pipelineProjectValuePresent);
|
|
244
|
+
if (values.length === 0) continue;
|
|
245
|
+
return _resolvePipelineProjectValues(values, config);
|
|
246
|
+
}
|
|
247
|
+
return { projects: [null] };
|
|
248
|
+
}
|
|
249
|
+
|
|
233
250
|
function _pipelineProjectSlug(project) {
|
|
234
251
|
return project ? shared.safeSlugComponent(project.name || 'project', 32).toLowerCase() : 'central';
|
|
235
252
|
}
|
|
@@ -339,12 +356,9 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
339
356
|
for (let i = 0; i < count; i++) {
|
|
340
357
|
const item = items[i % items.length];
|
|
341
358
|
const projectResolution = _resolvePipelineProjects([
|
|
342
|
-
item.projects,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
stage.project,
|
|
346
|
-
pipeline.projects,
|
|
347
|
-
pipeline.project,
|
|
359
|
+
[item.projects, item.project],
|
|
360
|
+
[stage.projects, stage.project],
|
|
361
|
+
[pipeline.projects, pipeline.project],
|
|
348
362
|
], config);
|
|
349
363
|
if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
|
|
350
364
|
for (const project of projectResolution.projects) {
|
|
@@ -376,7 +390,10 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
376
390
|
touchedProjects.push(project?.name || null);
|
|
377
391
|
}
|
|
378
392
|
}
|
|
379
|
-
|
|
393
|
+
const projects = [...new Set(touchedProjects)];
|
|
394
|
+
const artifacts = { workItems: [...new Set(createdIds)] };
|
|
395
|
+
if (projects.some(Boolean)) artifacts.projects = projects;
|
|
396
|
+
return { status: PIPELINE_STATUS.RUNNING, artifacts };
|
|
380
397
|
}
|
|
381
398
|
|
|
382
399
|
function executeTaskStageLegacy(stage, stageState, run, config) {
|
|
@@ -488,7 +505,10 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
488
505
|
if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
|
|
489
506
|
|
|
490
507
|
const slug = slugify(stage.title || 'pipeline-plan');
|
|
491
|
-
const projectResolution = _resolvePipelineProjects([
|
|
508
|
+
const projectResolution = _resolvePipelineProjects([
|
|
509
|
+
[stage.projects, stage.project],
|
|
510
|
+
[pipeline.projects, pipeline.project],
|
|
511
|
+
], config);
|
|
492
512
|
if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
|
|
493
513
|
const targetProjects = projectResolution.projects;
|
|
494
514
|
const wiIdForProject = (project) => `PL-${run.runId.slice(4, 12)}-${stage.id}-prd${targetProjects.length > 1 || project ? '-' + _pipelineProjectSlug(project) : ''}`;
|
|
@@ -1093,6 +1113,7 @@ module.exports = {
|
|
|
1093
1113
|
getPipelineRuns, getActiveRun, startRun, updateRunStage, completeRun,
|
|
1094
1114
|
discoverPipelineWork,
|
|
1095
1115
|
evaluateCondition, // exported for testing
|
|
1096
|
-
executeTaskStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1116
|
+
executeTaskStage, executePlanStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1117
|
+
_resolvePipelineProjects, // exported for testing
|
|
1097
1118
|
_findMeetingsInRun, _findExistingPlanForMeeting, _findExistingPrdForPlan, // exported for testing
|
|
1098
1119
|
};
|
package/engine/projects.js
CHANGED
|
@@ -66,7 +66,7 @@ function removeProject(target, options = {}) {
|
|
|
66
66
|
// 1. Cancel pending/queued work items linked to this project (project-local
|
|
67
67
|
// file + central). Done items are preserved as history.
|
|
68
68
|
summary.cancelledItems += dispatch.cancelPendingWorkItems(
|
|
69
|
-
|
|
69
|
+
shared.projectWorkItemsPath(project),
|
|
70
70
|
() => true,
|
|
71
71
|
'project-removed',
|
|
72
72
|
);
|
|
@@ -103,7 +103,7 @@ function removeProject(target, options = {}) {
|
|
|
103
103
|
// specifically. Don't touch schedules with project='any' or unset.
|
|
104
104
|
if (Array.isArray(config.schedules)) {
|
|
105
105
|
for (const s of config.schedules) {
|
|
106
|
-
if (s.project
|
|
106
|
+
if (shared.resolveProjectSource(s.project, [project], { allowCentral: false }).project && s.enabled !== false) {
|
|
107
107
|
s.enabled = false;
|
|
108
108
|
summary.disabledSchedules++;
|
|
109
109
|
}
|
|
@@ -164,20 +164,20 @@ function removeProject(target, options = {}) {
|
|
|
164
164
|
...(p.monitoredResources || []),
|
|
165
165
|
...((p.stages || []).flatMap(s => s.monitoredResources || [])),
|
|
166
166
|
];
|
|
167
|
-
if (refs.some(r => r && (r.project
|
|
167
|
+
if (refs.some(r => r && shared.resolveProjectSource(r.project || r._project, [project], { allowCentral: false }).project)) {
|
|
168
168
|
summary.pipelineRefs.push(p.id);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
} catch { /* pipelines optional */ }
|
|
172
172
|
|
|
173
173
|
// 7. Remove from config.json (and persist any schedule disables)
|
|
174
|
-
config.projects = (config.projects || []).filter(p => p
|
|
174
|
+
config.projects = (config.projects || []).filter(p => !shared.resolveProjectSource(p, [project], { allowCentral: false }).project);
|
|
175
175
|
try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); }
|
|
176
176
|
catch (e) { return { ...summary, error: 'Failed to write config: ' + e.message }; }
|
|
177
177
|
|
|
178
178
|
// 8. Move (or purge) projects/<name>/ — preserves PR/work-item history by
|
|
179
179
|
// default so a re-add can pick up where it left off.
|
|
180
|
-
const dataDir =
|
|
180
|
+
const dataDir = shared.projectStateDir(project);
|
|
181
181
|
if (fs.existsSync(dataDir)) {
|
|
182
182
|
if (dataMode === 'purge') {
|
|
183
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));
|
package/engine/shared.js
CHANGED
|
@@ -1725,7 +1725,7 @@ function findProjectByName(projects, projectName) {
|
|
|
1725
1725
|
function findProjectByNameOrPath(projects, target) {
|
|
1726
1726
|
const value = String(target || '').trim();
|
|
1727
1727
|
if (!value) return null;
|
|
1728
|
-
return
|
|
1728
|
+
return resolveProjectSource(value, projects, { allowCentral: false }).project || null;
|
|
1729
1729
|
}
|
|
1730
1730
|
|
|
1731
1731
|
function resolveConfiguredProject(projectName, projectsOrConfig, options = {}) {
|
|
@@ -1746,6 +1746,117 @@ function resolveConfiguredProject(projectName, projectsOrConfig, options = {}) {
|
|
|
1746
1746
|
return { project: null, explicit: false, value: '' };
|
|
1747
1747
|
}
|
|
1748
1748
|
|
|
1749
|
+
function centralWorkItemsPath(minionsDir = MINIONS_DIR) {
|
|
1750
|
+
return path.join(minionsDir, 'work-items.json');
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function centralPullRequestsPath(minionsDir = MINIONS_DIR) {
|
|
1754
|
+
return path.join(minionsDir, 'pull-requests.json');
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function _projectSourceRawValue(source) {
|
|
1758
|
+
if (source && typeof source === 'object') {
|
|
1759
|
+
return source.name ?? source.project ?? source._project ?? source.source ?? source._source ??
|
|
1760
|
+
source.wiPath ?? source.workItemsPath ?? source.prPath ?? source.pullRequestsPath ??
|
|
1761
|
+
source.stateDir ?? source.path ?? source.localPath ?? '';
|
|
1762
|
+
}
|
|
1763
|
+
return source;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function _sameSourcePath(value, targetPath, minionsDir = MINIONS_DIR) {
|
|
1767
|
+
if (!value || !targetPath) return false;
|
|
1768
|
+
if (sameResolvedPath(value, targetPath)) return true;
|
|
1769
|
+
if (!path.isAbsolute(value) && sameResolvedPath(path.resolve(minionsDir, value), targetPath)) return true;
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function _projectSourceDescriptor(project, value, explicit, minionsDir = MINIONS_DIR) {
|
|
1774
|
+
const wiPath = project ? projectWorkItemsPath(project) : centralWorkItemsPath(minionsDir);
|
|
1775
|
+
const prPath = project ? projectPrPath(project) : centralPullRequestsPath(minionsDir);
|
|
1776
|
+
const stateDir = project ? projectStateDir(project) : minionsDir;
|
|
1777
|
+
return {
|
|
1778
|
+
project: project || null,
|
|
1779
|
+
explicit: !!explicit,
|
|
1780
|
+
value: value || '',
|
|
1781
|
+
sourceName: project?.name || 'central',
|
|
1782
|
+
isCentral: !project,
|
|
1783
|
+
stateDir,
|
|
1784
|
+
wiPath,
|
|
1785
|
+
prPath,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
1790
|
+
const projects = Array.isArray(projectsOrConfig) ? projectsOrConfig : getProjects(projectsOrConfig);
|
|
1791
|
+
const minionsDir = options.minionsDir ? path.resolve(options.minionsDir) : MINIONS_DIR;
|
|
1792
|
+
const allowCentral = options.allowCentral !== false;
|
|
1793
|
+
const raw = _projectSourceRawValue(source);
|
|
1794
|
+
const value = String(raw || '').trim();
|
|
1795
|
+
const explicit = !!value;
|
|
1796
|
+
|
|
1797
|
+
if (!value) {
|
|
1798
|
+
if (options.defaultWhenSingle && projects.length === 1) {
|
|
1799
|
+
return _projectSourceDescriptor(projects[0], '', false, minionsDir);
|
|
1800
|
+
}
|
|
1801
|
+
if (allowCentral) return _projectSourceDescriptor(null, '', false, minionsDir);
|
|
1802
|
+
return { project: null, explicit: false, value: '', sourceName: '', isCentral: false, wiPath: null, prPath: null, stateDir: null };
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const centralWorkPath = centralWorkItemsPath(minionsDir);
|
|
1806
|
+
const centralPrPath = centralPullRequestsPath(minionsDir);
|
|
1807
|
+
const centralNames = new Set(['central', 'root']);
|
|
1808
|
+
const lowerValue = value.toLowerCase();
|
|
1809
|
+
const isCentral = centralNames.has(lowerValue) ||
|
|
1810
|
+
_sameSourcePath(value, minionsDir, minionsDir) ||
|
|
1811
|
+
_sameSourcePath(value, centralWorkPath, minionsDir) ||
|
|
1812
|
+
_sameSourcePath(value, centralPrPath, minionsDir);
|
|
1813
|
+
if (isCentral) {
|
|
1814
|
+
if (allowCentral) return _projectSourceDescriptor(null, value, true, minionsDir);
|
|
1815
|
+
return {
|
|
1816
|
+
project: null,
|
|
1817
|
+
explicit: true,
|
|
1818
|
+
value,
|
|
1819
|
+
sourceName: '',
|
|
1820
|
+
isCentral: false,
|
|
1821
|
+
wiPath: null,
|
|
1822
|
+
prPath: null,
|
|
1823
|
+
stateDir: null,
|
|
1824
|
+
error: 'central source is not allowed here',
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
for (const project of projects || []) {
|
|
1829
|
+
if (!project) continue;
|
|
1830
|
+
if (String(project.name || '').toLowerCase() === lowerValue) {
|
|
1831
|
+
return _projectSourceDescriptor(project, value, explicit, minionsDir);
|
|
1832
|
+
}
|
|
1833
|
+
const candidates = [
|
|
1834
|
+
project.localPath,
|
|
1835
|
+
projectStateDir(project),
|
|
1836
|
+
projectWorkItemsPath(project),
|
|
1837
|
+
projectPrPath(project),
|
|
1838
|
+
legacyProjectStateDir(project),
|
|
1839
|
+
legacyProjectStatePath(project, 'work-items.json'),
|
|
1840
|
+
legacyProjectStatePath(project, 'pull-requests.json'),
|
|
1841
|
+
].filter(Boolean);
|
|
1842
|
+
if (candidates.some(candidate => _sameSourcePath(value, candidate, minionsDir))) {
|
|
1843
|
+
return _projectSourceDescriptor(project, value, explicit, minionsDir);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
return {
|
|
1848
|
+
project: null,
|
|
1849
|
+
explicit: true,
|
|
1850
|
+
value,
|
|
1851
|
+
sourceName: '',
|
|
1852
|
+
isCentral: false,
|
|
1853
|
+
wiPath: null,
|
|
1854
|
+
prPath: null,
|
|
1855
|
+
stateDir: null,
|
|
1856
|
+
error: formatUnknownProjectError(value, projects),
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1749
1860
|
function projectRoot(project) {
|
|
1750
1861
|
return path.resolve(project.localPath);
|
|
1751
1862
|
}
|
|
@@ -1812,9 +1923,13 @@ function mergeProjectStateArrays(current, legacy) {
|
|
|
1812
1923
|
|
|
1813
1924
|
function sameResolvedPath(a, b) {
|
|
1814
1925
|
if (!a || !b) return false;
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1926
|
+
try {
|
|
1927
|
+
const left = path.resolve(a);
|
|
1928
|
+
const right = path.resolve(b);
|
|
1929
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
1930
|
+
} catch {
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1818
1933
|
}
|
|
1819
1934
|
|
|
1820
1935
|
function removeLegacyProjectStateDir(project) {
|
|
@@ -3323,7 +3438,10 @@ module.exports = {
|
|
|
3323
3438
|
findProjectByName,
|
|
3324
3439
|
findProjectByNameOrPath,
|
|
3325
3440
|
resolveConfiguredProject,
|
|
3441
|
+
resolveProjectSource,
|
|
3326
3442
|
projectRoot,
|
|
3443
|
+
centralWorkItemsPath,
|
|
3444
|
+
centralPullRequestsPath,
|
|
3327
3445
|
projectStateDir,
|
|
3328
3446
|
projectStateDirEnsure,
|
|
3329
3447
|
projectWorkItemsPath,
|
|
@@ -3331,6 +3449,7 @@ module.exports = {
|
|
|
3331
3449
|
legacyProjectStateDir,
|
|
3332
3450
|
legacyProjectStatePath,
|
|
3333
3451
|
ensureProjectStateFiles,
|
|
3452
|
+
sameResolvedPath,
|
|
3334
3453
|
resolveProjectForPrPath, // exported for testing
|
|
3335
3454
|
getPrLinks,
|
|
3336
3455
|
addPrLink,
|