@yemi33/minions 0.1.2424 → 0.1.2426
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/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +7 -5
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/index.html +9 -2
- package/docs/internal-install.md +212 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/onboarding.md +26 -0
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/01-install-and-connect.md +23 -0
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const api = require('./api-validation');
|
|
5
|
+
const shared = require('./shared');
|
|
6
|
+
|
|
7
|
+
const COMPLEXITY_VALUES = Object.freeze(['small', 'medium', 'large']);
|
|
8
|
+
const BRANCH_STRATEGY_VALUES = Object.freeze(['parallel', 'shared-branch']);
|
|
9
|
+
const PLAN_STATUS_VALUES = Object.freeze(Object.values(shared.PLAN_STATUS));
|
|
10
|
+
const PRD_ITEM_STATUS_VALUES = Object.freeze(Object.values(shared.PRD_ITEM_STATUS));
|
|
11
|
+
const WORK_TYPE_VALUES = Object.freeze([...shared.VALID_WORK_TYPES]);
|
|
12
|
+
|
|
13
|
+
const PLAN_ACTION_STATUSES = Object.freeze({
|
|
14
|
+
approve: new Set([
|
|
15
|
+
shared.PLAN_STATUS.ACTIVE,
|
|
16
|
+
shared.PLAN_STATUS.AWAITING_APPROVAL,
|
|
17
|
+
shared.PLAN_STATUS.APPROVED,
|
|
18
|
+
shared.PLAN_STATUS.PAUSED,
|
|
19
|
+
shared.PLAN_STATUS.COMPLETED,
|
|
20
|
+
]),
|
|
21
|
+
pause: new Set([shared.PLAN_STATUS.ACTIVE, shared.PLAN_STATUS.APPROVED]),
|
|
22
|
+
reject: new Set([
|
|
23
|
+
shared.PLAN_STATUS.ACTIVE,
|
|
24
|
+
shared.PLAN_STATUS.AWAITING_APPROVAL,
|
|
25
|
+
shared.PLAN_STATUS.APPROVED,
|
|
26
|
+
shared.PLAN_STATUS.PAUSED,
|
|
27
|
+
shared.PLAN_STATUS.REVISION_REQUESTED,
|
|
28
|
+
]),
|
|
29
|
+
regenerate: new Set([
|
|
30
|
+
shared.PLAN_STATUS.ACTIVE,
|
|
31
|
+
shared.PLAN_STATUS.APPROVED,
|
|
32
|
+
shared.PLAN_STATUS.PAUSED,
|
|
33
|
+
shared.PLAN_STATUS.COMPLETED,
|
|
34
|
+
]),
|
|
35
|
+
revise: new Set([
|
|
36
|
+
shared.PLAN_STATUS.ACTIVE,
|
|
37
|
+
shared.PLAN_STATUS.AWAITING_APPROVAL,
|
|
38
|
+
shared.PLAN_STATUS.APPROVED,
|
|
39
|
+
shared.PLAN_STATUS.PAUSED,
|
|
40
|
+
]),
|
|
41
|
+
discuss: new Set([
|
|
42
|
+
shared.PLAN_STATUS.AWAITING_APPROVAL,
|
|
43
|
+
shared.PLAN_STATUS.REVISION_REQUESTED,
|
|
44
|
+
]),
|
|
45
|
+
'trigger-verify': new Set([shared.PLAN_STATUS.COMPLETED]),
|
|
46
|
+
'item-mutate': new Set([
|
|
47
|
+
shared.PLAN_STATUS.ACTIVE,
|
|
48
|
+
shared.PLAN_STATUS.AWAITING_APPROVAL,
|
|
49
|
+
shared.PLAN_STATUS.APPROVED,
|
|
50
|
+
shared.PLAN_STATUS.PAUSED,
|
|
51
|
+
shared.PLAN_STATUS.COMPLETED,
|
|
52
|
+
shared.PLAN_STATUS.REVISION_REQUESTED,
|
|
53
|
+
]),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function inputError(message, options = {}) {
|
|
57
|
+
throw new api.ApiInputError(message, options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateBody(body) {
|
|
61
|
+
return api.validatePlainObject(body, {
|
|
62
|
+
field: 'body',
|
|
63
|
+
path: 'body',
|
|
64
|
+
message: 'Request body must be a JSON object',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateNoQuery(req) {
|
|
69
|
+
const parsed = new URL(req?.url || '/', 'http://localhost');
|
|
70
|
+
const names = [...new Set(parsed.searchParams.keys())];
|
|
71
|
+
if (names.length > 0) {
|
|
72
|
+
inputError(`This endpoint does not accept query parameters: ${names.join(', ')}`, {
|
|
73
|
+
code: 'unexpected-query',
|
|
74
|
+
field: names[0],
|
|
75
|
+
path: `query.${names[0]}`,
|
|
76
|
+
rejectedValue: parsed.search,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fullyDecode(value, field) {
|
|
82
|
+
let decoded = value;
|
|
83
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
84
|
+
let next;
|
|
85
|
+
try {
|
|
86
|
+
next = decodeURIComponent(decoded);
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
inputError(`${field} contains malformed percent encoding`, {
|
|
89
|
+
code: 'invalid-artifact-name',
|
|
90
|
+
field,
|
|
91
|
+
path: field,
|
|
92
|
+
rejectedValue: value,
|
|
93
|
+
cause,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (next === decoded) return decoded;
|
|
97
|
+
decoded = next;
|
|
98
|
+
}
|
|
99
|
+
inputError(`${field} contains excessive nested percent encoding`, {
|
|
100
|
+
code: 'invalid-artifact-name',
|
|
101
|
+
field,
|
|
102
|
+
path: field,
|
|
103
|
+
rejectedValue: value,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function unsafeBasename(value) {
|
|
108
|
+
return value === '.' || value === '..'
|
|
109
|
+
|| value.includes('..')
|
|
110
|
+
|| value.includes('/')
|
|
111
|
+
|| value.includes('\\')
|
|
112
|
+
|| value.includes('\0')
|
|
113
|
+
|| value.includes(':')
|
|
114
|
+
|| path.basename(value) !== value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateArtifactName(value, options = {}) {
|
|
118
|
+
const field = options.field || 'file';
|
|
119
|
+
const raw = api.validateString(value, {
|
|
120
|
+
field,
|
|
121
|
+
path: field,
|
|
122
|
+
allowEmpty: false,
|
|
123
|
+
maxLength: 255,
|
|
124
|
+
message: `${field} must be a non-empty artifact basename`,
|
|
125
|
+
});
|
|
126
|
+
if (raw !== raw.trim()) {
|
|
127
|
+
inputError(`${field} must not contain leading or trailing whitespace`, {
|
|
128
|
+
code: 'invalid-artifact-name',
|
|
129
|
+
field,
|
|
130
|
+
path: field,
|
|
131
|
+
rejectedValue: value,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const decoded = fullyDecode(raw, field);
|
|
135
|
+
if (unsafeBasename(decoded) || unsafeBasename(raw)) {
|
|
136
|
+
inputError(`${field} must be a safe basename without traversal or path separators`, {
|
|
137
|
+
code: 'invalid-artifact-name',
|
|
138
|
+
field,
|
|
139
|
+
path: field,
|
|
140
|
+
rejectedValue: value,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const result = options.decode === true ? decoded : raw;
|
|
144
|
+
const extensions = Array.isArray(options.extensions) ? options.extensions : [];
|
|
145
|
+
if (extensions.length > 0 && !extensions.some(extension => result.endsWith(extension))) {
|
|
146
|
+
inputError(`${field} must end with ${extensions.join(' or ')}`, {
|
|
147
|
+
code: 'invalid-artifact-extension',
|
|
148
|
+
field,
|
|
149
|
+
path: field,
|
|
150
|
+
rejectedValue: value,
|
|
151
|
+
allowedValues: extensions,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function validatePrdFilename(value, options = {}) {
|
|
158
|
+
return validateArtifactName(value, {
|
|
159
|
+
...options,
|
|
160
|
+
extensions: ['.json'],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function validatePlanFilename(value, options = {}) {
|
|
165
|
+
return validateArtifactName(value, {
|
|
166
|
+
...options,
|
|
167
|
+
extensions: ['.md'],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function validatePlanArtifactFilename(value, options = {}) {
|
|
172
|
+
return validateArtifactName(value, {
|
|
173
|
+
...options,
|
|
174
|
+
extensions: ['.md', '.json'],
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateItemIdentifier(value, options = {}) {
|
|
179
|
+
const field = options.field || 'itemId';
|
|
180
|
+
const normalized = api.validateIdentifier(value, {
|
|
181
|
+
field,
|
|
182
|
+
path: field,
|
|
183
|
+
trim: true,
|
|
184
|
+
maxLength: 128,
|
|
185
|
+
pattern: /^[A-Za-z0-9][A-Za-z0-9._:-]*$/,
|
|
186
|
+
optional: options.optional === true,
|
|
187
|
+
message: `${field} must be a safe item identifier`,
|
|
188
|
+
});
|
|
189
|
+
if (normalized !== undefined && normalized.includes('..')) {
|
|
190
|
+
inputError(`${field} must not contain traversal segments`, {
|
|
191
|
+
code: 'invalid-identifier',
|
|
192
|
+
field,
|
|
193
|
+
path: field,
|
|
194
|
+
rejectedValue: value,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return normalized;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validateOptionalString(value, field, options = {}) {
|
|
201
|
+
return api.validateString(value, {
|
|
202
|
+
field,
|
|
203
|
+
path: field,
|
|
204
|
+
optional: true,
|
|
205
|
+
trim: options.trim === true,
|
|
206
|
+
allowEmpty: options.allowEmpty !== false,
|
|
207
|
+
maxLength: options.maxLength || 100000,
|
|
208
|
+
message: `${field} must be a string`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function validateRequiredString(value, field, options = {}) {
|
|
213
|
+
return api.validateString(value, {
|
|
214
|
+
field,
|
|
215
|
+
path: field,
|
|
216
|
+
trim: options.trim !== false,
|
|
217
|
+
allowEmpty: false,
|
|
218
|
+
maxLength: options.maxLength || 100000,
|
|
219
|
+
message: `${field} is required and must be a non-empty string`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateOptionalBoolean(value, field) {
|
|
224
|
+
return api.validateBoolean(value, {
|
|
225
|
+
field,
|
|
226
|
+
path: field,
|
|
227
|
+
optional: true,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function validatePriority(value) {
|
|
232
|
+
const error = shared.validateWorkItemPriority(value);
|
|
233
|
+
if (!error) return value;
|
|
234
|
+
inputError(error.error, {
|
|
235
|
+
code: error.code,
|
|
236
|
+
field: 'priority',
|
|
237
|
+
path: 'priority',
|
|
238
|
+
rejectedValue: error.rejectedValue,
|
|
239
|
+
allowedValues: error.allowedValues,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function resolveProject(value, projects, options = {}) {
|
|
244
|
+
const field = options.field || 'project';
|
|
245
|
+
const projectValue = validateRequiredString(value, field, { maxLength: 512 });
|
|
246
|
+
const resolved = shared.resolveProjectSource(projectValue, projects, {
|
|
247
|
+
allowCentral: false,
|
|
248
|
+
minionsDir: options.minionsDir,
|
|
249
|
+
});
|
|
250
|
+
if (!resolved.project || resolved.error) {
|
|
251
|
+
inputError(resolved.error || `Project "${projectValue}" is not configured`, {
|
|
252
|
+
code: 'invalid-project',
|
|
253
|
+
field,
|
|
254
|
+
path: field,
|
|
255
|
+
rejectedValue: value,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return resolved.project.name;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeProjectSelection(value, projects, options = {}) {
|
|
262
|
+
const field = options.field || 'project';
|
|
263
|
+
if (value === undefined || value === null || value === '') return [];
|
|
264
|
+
const rawValues = Array.isArray(value) ? value : [value];
|
|
265
|
+
if (Array.isArray(value) && options.allowArray === false) {
|
|
266
|
+
inputError(`${field} must be a project string`, {
|
|
267
|
+
code: 'invalid-project',
|
|
268
|
+
field,
|
|
269
|
+
path: field,
|
|
270
|
+
rejectedValue: value,
|
|
271
|
+
expected: 'string',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const names = [];
|
|
275
|
+
rawValues.forEach((entry, index) => {
|
|
276
|
+
if (typeof entry !== 'string' || !entry.trim()) {
|
|
277
|
+
inputError(`${field}[${index}] must be a non-empty project string`, {
|
|
278
|
+
code: 'invalid-project',
|
|
279
|
+
field,
|
|
280
|
+
path: [field, index],
|
|
281
|
+
rejectedValue: entry,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const name = resolveProject(entry, projects, {
|
|
285
|
+
field: Array.isArray(value) ? `${field}[${index}]` : field,
|
|
286
|
+
minionsDir: options.minionsDir,
|
|
287
|
+
});
|
|
288
|
+
if (!names.includes(name)) names.push(name);
|
|
289
|
+
});
|
|
290
|
+
return names;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function parseProjectList(raw, label, projects, options = {}) {
|
|
294
|
+
const parts = String(raw).split(/[,|;]/);
|
|
295
|
+
if (parts.length < 2 || parts.some(part => !part.trim())) {
|
|
296
|
+
inputError(`${label} must declare at least two non-empty projects`, {
|
|
297
|
+
code: 'invalid-project-declaration',
|
|
298
|
+
field: label,
|
|
299
|
+
path: label,
|
|
300
|
+
rejectedValue: raw,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const names = normalizeProjectSelection(
|
|
304
|
+
parts.map(part => part.trim().replace(/^["'`]+|["'`]+$/g, '').trim()),
|
|
305
|
+
projects,
|
|
306
|
+
{ field: label, allowArray: true, minionsDir: options.minionsDir },
|
|
307
|
+
);
|
|
308
|
+
if (names.length < 2) {
|
|
309
|
+
inputError(`${label} must resolve to at least two distinct projects`, {
|
|
310
|
+
code: 'invalid-project-declaration',
|
|
311
|
+
field: label,
|
|
312
|
+
path: label,
|
|
313
|
+
rejectedValue: raw,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return names;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function sameList(left, right) {
|
|
320
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function validatePlanProjectDeclarations(content, projects, options = {}) {
|
|
324
|
+
const text = api.validateString(content, {
|
|
325
|
+
field: options.field || 'content',
|
|
326
|
+
path: options.field || 'content',
|
|
327
|
+
allowEmpty: false,
|
|
328
|
+
maxLength: 1000000,
|
|
329
|
+
});
|
|
330
|
+
const markerTokens = text.match(/minions:targetProjects/gi) || [];
|
|
331
|
+
const markerMatches = [...text.matchAll(/<!--\s*minions:targetProjects\s*=\s*([^>]*?)\s*-->/gi)];
|
|
332
|
+
if (markerTokens.length !== markerMatches.length || markerMatches.length > 1) {
|
|
333
|
+
inputError('Plan contains a malformed or duplicate minions:targetProjects declaration', {
|
|
334
|
+
code: 'invalid-project-declaration',
|
|
335
|
+
field: 'targetProjects',
|
|
336
|
+
path: 'content.targetProjects',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const headerLines = text.split(/\r?\n/).slice(0, 80).map(line => (
|
|
341
|
+
line.replace(/^\s*[-*]\s+/, '').replace(/\*\*/g, '').trim()
|
|
342
|
+
));
|
|
343
|
+
const pluralMatches = headerLines
|
|
344
|
+
.map(line => line.match(/^Projects\s*:\s*(.*)$/i))
|
|
345
|
+
.filter(Boolean);
|
|
346
|
+
const singularMatches = headerLines
|
|
347
|
+
.map(line => line.match(/^Project\s*:\s*(.*)$/i))
|
|
348
|
+
.filter(Boolean);
|
|
349
|
+
if (pluralMatches.length > 1 || singularMatches.length > 1) {
|
|
350
|
+
inputError('Plan contains duplicate project declarations', {
|
|
351
|
+
code: 'invalid-project-declaration',
|
|
352
|
+
field: 'project',
|
|
353
|
+
path: 'content.project',
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const markerProjects = markerMatches.length
|
|
358
|
+
? parseProjectList(markerMatches[0][1], 'targetProjects', projects, options)
|
|
359
|
+
: [];
|
|
360
|
+
const pluralProjects = pluralMatches.length
|
|
361
|
+
? parseProjectList(pluralMatches[0][1].replace(/\s+#.*$/, '').trim(), 'projects', projects, options)
|
|
362
|
+
: [];
|
|
363
|
+
if (markerProjects.length && pluralProjects.length && !sameList(markerProjects, pluralProjects)) {
|
|
364
|
+
inputError('Plan targetProjects marker and Projects header must declare the same projects in the same order', {
|
|
365
|
+
code: 'conflicting-project-declaration',
|
|
366
|
+
field: 'project',
|
|
367
|
+
path: 'content.project',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const targetProjects = markerProjects.length ? markerProjects : pluralProjects;
|
|
371
|
+
|
|
372
|
+
let declaredProject = '';
|
|
373
|
+
if (singularMatches.length) {
|
|
374
|
+
const raw = singularMatches[0][1].replace(/\s+#.*$/, '').trim()
|
|
375
|
+
.replace(/^["'`]+|["'`]+$/g, '').trim();
|
|
376
|
+
if (!raw) {
|
|
377
|
+
inputError('Project declaration must name a configured project', {
|
|
378
|
+
code: 'invalid-project-declaration',
|
|
379
|
+
field: 'project',
|
|
380
|
+
path: 'content.project',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
declaredProject = resolveProject(raw, projects, {
|
|
384
|
+
field: 'project',
|
|
385
|
+
minionsDir: options.minionsDir,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (declaredProject && targetProjects.length) {
|
|
389
|
+
inputError('Plan cannot contain both singular Project and cross-repo Projects declarations', {
|
|
390
|
+
code: 'conflicting-project-declaration',
|
|
391
|
+
field: 'project',
|
|
392
|
+
path: 'content.project',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return { declaredProject, targetProjects };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function validateProjectSelectionAgreement(selection, declarations) {
|
|
399
|
+
if (!selection.length) return;
|
|
400
|
+
const declared = declarations.targetProjects.length
|
|
401
|
+
? declarations.targetProjects
|
|
402
|
+
: (declarations.declaredProject ? [declarations.declaredProject] : []);
|
|
403
|
+
if (declared.length && !sameList(selection, declared)) {
|
|
404
|
+
inputError('Request project selection conflicts with the project declaration in plan content', {
|
|
405
|
+
code: 'conflicting-project-declaration',
|
|
406
|
+
field: 'project',
|
|
407
|
+
path: 'project',
|
|
408
|
+
rejectedValue: selection,
|
|
409
|
+
allowedValues: declared,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validateAgent(value, agents) {
|
|
415
|
+
if (value === undefined || value === null || value === '') return '';
|
|
416
|
+
const agent = validateRequiredString(value, 'agent', { maxLength: 128 });
|
|
417
|
+
if (!agents || !Object.prototype.hasOwnProperty.call(agents, agent)) {
|
|
418
|
+
inputError(`Unknown agent "${agent}"`, {
|
|
419
|
+
code: 'invalid-agent',
|
|
420
|
+
field: 'agent',
|
|
421
|
+
path: 'agent',
|
|
422
|
+
rejectedValue: value,
|
|
423
|
+
allowedValues: Object.keys(agents || {}),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return agent;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function validateBranchStrategy(body) {
|
|
430
|
+
const snake = body.branch_strategy;
|
|
431
|
+
const camel = body.branchStrategy;
|
|
432
|
+
if (snake !== undefined && camel !== undefined && snake !== camel) {
|
|
433
|
+
inputError('branch_strategy and branchStrategy must match when both are supplied', {
|
|
434
|
+
code: 'conflicting-aliases',
|
|
435
|
+
field: 'branchStrategy',
|
|
436
|
+
path: 'branchStrategy',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const value = snake !== undefined ? snake : camel;
|
|
440
|
+
if (value === undefined) return undefined;
|
|
441
|
+
return api.validateEnum(value, {
|
|
442
|
+
field: 'branchStrategy',
|
|
443
|
+
path: snake !== undefined ? 'branch_strategy' : 'branchStrategy',
|
|
444
|
+
values: [...BRANCH_STRATEGY_VALUES],
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function validatePlanWorkItemRequest(body, options = {}) {
|
|
449
|
+
validateBody(body);
|
|
450
|
+
const title = validateRequiredString(body.title, 'title', { maxLength: 500 });
|
|
451
|
+
const description = validateOptionalString(body.description, 'description');
|
|
452
|
+
validatePriority(body.priority);
|
|
453
|
+
const projectNames = normalizeProjectSelection(body.project, options.projects || [], {
|
|
454
|
+
field: 'project',
|
|
455
|
+
minionsDir: options.minionsDir,
|
|
456
|
+
});
|
|
457
|
+
const agent = validateAgent(body.agent, options.agents || {});
|
|
458
|
+
const branchStrategy = validateBranchStrategy(body);
|
|
459
|
+
return {
|
|
460
|
+
...body,
|
|
461
|
+
title,
|
|
462
|
+
...(description !== undefined ? { description } : {}),
|
|
463
|
+
project: Array.isArray(body.project) ? projectNames : (projectNames[0] || ''),
|
|
464
|
+
...(agent ? { agent } : {}),
|
|
465
|
+
...(branchStrategy ? { branchStrategy, branch_strategy: branchStrategy } : {}),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function validatePlanFileCreateRequest(body, options = {}) {
|
|
470
|
+
validateBody(body);
|
|
471
|
+
const title = validateRequiredString(body.title, 'title', { maxLength: 500 });
|
|
472
|
+
const content = validateRequiredString(body.content, 'content', {
|
|
473
|
+
trim: false,
|
|
474
|
+
maxLength: 1000000,
|
|
475
|
+
});
|
|
476
|
+
const meetingId = validateItemIdentifier(body.meetingId, {
|
|
477
|
+
field: 'meetingId',
|
|
478
|
+
optional: true,
|
|
479
|
+
});
|
|
480
|
+
const projectNames = normalizeProjectSelection(body.project, options.projects || [], {
|
|
481
|
+
field: 'project',
|
|
482
|
+
minionsDir: options.minionsDir,
|
|
483
|
+
});
|
|
484
|
+
const declarations = validatePlanProjectDeclarations(content, options.projects || [], options);
|
|
485
|
+
validateProjectSelectionAgreement(projectNames, declarations);
|
|
486
|
+
return {
|
|
487
|
+
...body,
|
|
488
|
+
title,
|
|
489
|
+
content,
|
|
490
|
+
projectNames,
|
|
491
|
+
declarations,
|
|
492
|
+
...(meetingId ? { meetingId } : {}),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function validatePlanExecuteRequest(body, options = {}) {
|
|
497
|
+
validateBody(body);
|
|
498
|
+
const file = validatePlanFilename(body.file);
|
|
499
|
+
const feedback = validateOptionalString(body.feedback, 'feedback', {
|
|
500
|
+
trim: true,
|
|
501
|
+
maxLength: 100000,
|
|
502
|
+
});
|
|
503
|
+
const projectNames = normalizeProjectSelection(body.project, options.projects || [], {
|
|
504
|
+
field: 'project',
|
|
505
|
+
allowArray: false,
|
|
506
|
+
minionsDir: options.minionsDir,
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
...body,
|
|
510
|
+
file,
|
|
511
|
+
feedback: feedback || '',
|
|
512
|
+
project: projectNames[0] || '',
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function validatePlanActionRequest(body, options = {}) {
|
|
517
|
+
validateBody(body);
|
|
518
|
+
const field = options.field || 'file';
|
|
519
|
+
let file;
|
|
520
|
+
if (options.extension === '.md') {
|
|
521
|
+
file = validatePlanFilename(body[field], { field });
|
|
522
|
+
} else if (options.extension === '.json') {
|
|
523
|
+
file = validatePrdFilename(body[field], { field });
|
|
524
|
+
} else {
|
|
525
|
+
file = validatePlanArtifactFilename(body[field], { field });
|
|
526
|
+
}
|
|
527
|
+
const normalized = { ...body, [field]: file };
|
|
528
|
+
for (const stringField of options.optionalStrings || []) {
|
|
529
|
+
const value = validateOptionalString(body[stringField], stringField, {
|
|
530
|
+
trim: options.trimStrings?.includes(stringField),
|
|
531
|
+
maxLength: 100000,
|
|
532
|
+
});
|
|
533
|
+
if (value !== undefined) normalized[stringField] = value;
|
|
534
|
+
}
|
|
535
|
+
for (const booleanField of options.optionalBooleans || []) {
|
|
536
|
+
const value = validateOptionalBoolean(body[booleanField], booleanField);
|
|
537
|
+
if (value !== undefined) normalized[booleanField] = value;
|
|
538
|
+
}
|
|
539
|
+
if (normalized.forceRegen === true && normalized.skipRegen === true) {
|
|
540
|
+
inputError('forceRegen and skipRegen cannot both be true', {
|
|
541
|
+
code: 'incompatible-options',
|
|
542
|
+
field: 'forceRegen',
|
|
543
|
+
path: 'forceRegen',
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return normalized;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function explicitWorkType(body) {
|
|
550
|
+
const fields = ['type', 'workType', 'work_type'].filter(field => body[field] !== undefined);
|
|
551
|
+
if (fields.length === 0) return undefined;
|
|
552
|
+
const values = fields.map(field => validateRequiredString(body[field], field, { maxLength: 64 }));
|
|
553
|
+
if (!values.every(value => value === values[0])) {
|
|
554
|
+
inputError('type, workType, and work_type must match when multiple aliases are supplied', {
|
|
555
|
+
code: 'conflicting-aliases',
|
|
556
|
+
field: 'type',
|
|
557
|
+
path: 'type',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
const candidate = values[0];
|
|
561
|
+
const resolved = shared.resolveWorkItemTypeFromPrdItem({ type: candidate });
|
|
562
|
+
if (!shared.VALID_WORK_TYPES.has(candidate) || resolved !== candidate) {
|
|
563
|
+
inputError(`type must be one of: ${WORK_TYPE_VALUES.join(', ')}`, {
|
|
564
|
+
code: 'invalid-work-type',
|
|
565
|
+
field: 'type',
|
|
566
|
+
path: fields[0],
|
|
567
|
+
rejectedValue: candidate,
|
|
568
|
+
allowedValues: [...WORK_TYPE_VALUES],
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return resolved;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function validatePrdItemRequest(body, options = {}) {
|
|
575
|
+
validateBody(body);
|
|
576
|
+
const mode = options.mode || 'create';
|
|
577
|
+
const normalized = { ...body };
|
|
578
|
+
if (mode === 'create') {
|
|
579
|
+
normalized.name = validateRequiredString(body.name, 'name', { maxLength: 500 });
|
|
580
|
+
normalized.id = validateItemIdentifier(body.id, { field: 'id', optional: true });
|
|
581
|
+
if (body.source !== undefined) normalized.source = validatePrdFilename(body.source, { field: 'source' });
|
|
582
|
+
} else {
|
|
583
|
+
normalized.source = validatePrdFilename(body.source, { field: 'source' });
|
|
584
|
+
normalized.itemId = validateItemIdentifier(body.itemId, { field: 'itemId' });
|
|
585
|
+
if (mode === 'update' && body.name !== undefined) {
|
|
586
|
+
normalized.name = validateRequiredString(body.name, 'name', { maxLength: 500 });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (body.description !== undefined) {
|
|
590
|
+
normalized.description = validateOptionalString(body.description, 'description');
|
|
591
|
+
}
|
|
592
|
+
validatePriority(body.priority);
|
|
593
|
+
if (body.estimated_complexity !== undefined) {
|
|
594
|
+
normalized.estimated_complexity = api.validateEnum(body.estimated_complexity, {
|
|
595
|
+
field: 'estimated_complexity',
|
|
596
|
+
path: 'estimated_complexity',
|
|
597
|
+
values: [...COMPLEXITY_VALUES],
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (body.status !== undefined) {
|
|
601
|
+
normalized.status = api.validateEnum(body.status, {
|
|
602
|
+
field: 'status',
|
|
603
|
+
path: 'status',
|
|
604
|
+
values: [...PRD_ITEM_STATUS_VALUES],
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const type = explicitWorkType(body);
|
|
608
|
+
if (type !== undefined) normalized.type = type;
|
|
609
|
+
|
|
610
|
+
if (body.project !== undefined) {
|
|
611
|
+
const names = normalizeProjectSelection(body.project, options.projects || [], {
|
|
612
|
+
field: 'project',
|
|
613
|
+
allowArray: false,
|
|
614
|
+
minionsDir: options.minionsDir,
|
|
615
|
+
});
|
|
616
|
+
normalized.project = names[0] || '';
|
|
617
|
+
}
|
|
618
|
+
const itemProjectValue = body.item_project !== undefined ? body.item_project : body.itemProject;
|
|
619
|
+
if (mode !== 'create' && itemProjectValue !== undefined) {
|
|
620
|
+
inputError('item_project is only supported when creating a PRD item', {
|
|
621
|
+
code: 'unsupported-field',
|
|
622
|
+
field: body.item_project !== undefined ? 'item_project' : 'itemProject',
|
|
623
|
+
path: body.item_project !== undefined ? 'item_project' : 'itemProject',
|
|
624
|
+
rejectedValue: itemProjectValue,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
if (body.item_project !== undefined && body.itemProject !== undefined
|
|
628
|
+
&& body.item_project !== body.itemProject) {
|
|
629
|
+
inputError('item_project and itemProject must match when both are supplied', {
|
|
630
|
+
code: 'conflicting-aliases',
|
|
631
|
+
field: 'itemProject',
|
|
632
|
+
path: 'itemProject',
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (itemProjectValue !== undefined) {
|
|
636
|
+
const names = normalizeProjectSelection(itemProjectValue, options.projects || [], {
|
|
637
|
+
field: 'item_project',
|
|
638
|
+
allowArray: false,
|
|
639
|
+
minionsDir: options.minionsDir,
|
|
640
|
+
});
|
|
641
|
+
normalized.item_project = names[0] || '';
|
|
642
|
+
normalized.itemProject = names[0] || '';
|
|
643
|
+
}
|
|
644
|
+
return normalized;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function effectivePlanStatus(plan, options = {}) {
|
|
648
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
649
|
+
inputError('PRD record is malformed', {
|
|
650
|
+
code: 'invalid-plan-state',
|
|
651
|
+
statusCode: 409,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
if (options.physicallyArchived || shared.isPrdArchived(plan)) return 'archived';
|
|
655
|
+
const status = plan.status || (
|
|
656
|
+
plan.requires_approval && !plan.approvedAt
|
|
657
|
+
? shared.PLAN_STATUS.AWAITING_APPROVAL
|
|
658
|
+
: shared.PLAN_STATUS.ACTIVE
|
|
659
|
+
);
|
|
660
|
+
if (!PLAN_STATUS_VALUES.includes(status)) {
|
|
661
|
+
inputError(`PRD has invalid status "${status}"`, {
|
|
662
|
+
code: 'invalid-plan-state',
|
|
663
|
+
statusCode: 409,
|
|
664
|
+
field: 'status',
|
|
665
|
+
path: 'status',
|
|
666
|
+
rejectedValue: status,
|
|
667
|
+
allowedValues: [...PLAN_STATUS_VALUES],
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return status;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function assertPlanAction(plan, action, options = {}) {
|
|
674
|
+
const status = effectivePlanStatus(plan, options);
|
|
675
|
+
if (action === 'archive') {
|
|
676
|
+
if (status !== 'archived') return status;
|
|
677
|
+
} else if (action === 'unarchive') {
|
|
678
|
+
if (status === 'archived') return status;
|
|
679
|
+
} else {
|
|
680
|
+
const allowed = PLAN_ACTION_STATUSES[action];
|
|
681
|
+
if (!allowed) throw new TypeError(`Unknown plan action: ${action}`);
|
|
682
|
+
if (allowed.has(status)) return status;
|
|
683
|
+
}
|
|
684
|
+
inputError(`Cannot ${action} a PRD in "${status}" state`, {
|
|
685
|
+
code: 'invalid-plan-transition',
|
|
686
|
+
statusCode: 409,
|
|
687
|
+
field: 'status',
|
|
688
|
+
path: 'status',
|
|
689
|
+
rejectedValue: status,
|
|
690
|
+
allowedValues: action === 'unarchive' ? ['archived'] : [...(PLAN_ACTION_STATUSES[action] || [])],
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function validatePrdProjects(plan, projects, options = {}) {
|
|
695
|
+
const references = [];
|
|
696
|
+
if (plan?.project) references.push({ value: plan.project, path: 'project' });
|
|
697
|
+
for (let index = 0; index < (Array.isArray(plan?.missing_features) ? plan.missing_features.length : 0); index += 1) {
|
|
698
|
+
const project = plan.missing_features[index]?.project;
|
|
699
|
+
if (project) references.push({ value: project, path: `missing_features[${index}].project` });
|
|
700
|
+
}
|
|
701
|
+
const names = [];
|
|
702
|
+
for (const reference of references) {
|
|
703
|
+
const name = resolveProject(reference.value, projects, {
|
|
704
|
+
field: reference.path,
|
|
705
|
+
minionsDir: options.minionsDir,
|
|
706
|
+
});
|
|
707
|
+
if (!names.includes(name)) names.push(name);
|
|
708
|
+
}
|
|
709
|
+
return names;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function validateSourcePlanName(plan) {
|
|
713
|
+
if (!plan?.source_plan) return '';
|
|
714
|
+
return validatePlanFilename(shared.sourcePlanKey(plan.source_plan), {
|
|
715
|
+
field: 'source_plan',
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
module.exports = {
|
|
720
|
+
COMPLEXITY_VALUES,
|
|
721
|
+
BRANCH_STRATEGY_VALUES,
|
|
722
|
+
PLAN_STATUS_VALUES,
|
|
723
|
+
PRD_ITEM_STATUS_VALUES,
|
|
724
|
+
WORK_TYPE_VALUES,
|
|
725
|
+
validateBody,
|
|
726
|
+
validateNoQuery,
|
|
727
|
+
validateArtifactName,
|
|
728
|
+
validatePrdFilename,
|
|
729
|
+
validatePlanFilename,
|
|
730
|
+
validatePlanArtifactFilename,
|
|
731
|
+
validateItemIdentifier,
|
|
732
|
+
validatePriority,
|
|
733
|
+
normalizeProjectSelection,
|
|
734
|
+
validatePlanProjectDeclarations,
|
|
735
|
+
validateProjectSelectionAgreement,
|
|
736
|
+
validatePlanWorkItemRequest,
|
|
737
|
+
validatePlanFileCreateRequest,
|
|
738
|
+
validatePlanExecuteRequest,
|
|
739
|
+
validatePlanActionRequest,
|
|
740
|
+
validatePrdItemRequest,
|
|
741
|
+
effectivePlanStatus,
|
|
742
|
+
assertPlanAction,
|
|
743
|
+
validatePrdProjects,
|
|
744
|
+
validateSourcePlanName,
|
|
745
|
+
};
|