@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
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
const apiValidation = require('./api-validation');
|
|
2
|
+
|
|
3
|
+
const WORK_ITEM_SCOPES = Object.freeze(['fan-out']);
|
|
4
|
+
const WORK_ITEM_FEEDBACK_RATINGS = Object.freeze(['up', 'down']);
|
|
5
|
+
const PR_FOLLOWUP_FIELDS = Object.freeze([
|
|
6
|
+
'parent_pr_url',
|
|
7
|
+
'parent_pr_id',
|
|
8
|
+
'parent_comment_id',
|
|
9
|
+
'parent_comment_author',
|
|
10
|
+
]);
|
|
11
|
+
const PR_FOLLOWUP_MAX_FIELD_LEN = 512;
|
|
12
|
+
const PR_REFERENCE_FIELDS = Object.freeze([
|
|
13
|
+
'targetPr',
|
|
14
|
+
'pr',
|
|
15
|
+
'pr_id',
|
|
16
|
+
'prId',
|
|
17
|
+
'sourcePr',
|
|
18
|
+
'pullRequest',
|
|
19
|
+
'prUrl',
|
|
20
|
+
]);
|
|
21
|
+
const META_BOOLEAN_FIELDS = Object.freeze([
|
|
22
|
+
'keep_processes',
|
|
23
|
+
'keep_processes_skip_workdir_check',
|
|
24
|
+
'managed_spawn',
|
|
25
|
+
]);
|
|
26
|
+
|
|
1
27
|
function _hasText(value) {
|
|
2
28
|
return typeof value === 'string' && value.trim().length > 0;
|
|
3
29
|
}
|
|
@@ -44,9 +70,544 @@ function isMalformedPhantomWorkItem(item) {
|
|
|
44
70
|
&& !hasWorkItemProvenance(item);
|
|
45
71
|
}
|
|
46
72
|
|
|
73
|
+
function _hasOwn(value, key) {
|
|
74
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _inputError(message, options = {}) {
|
|
78
|
+
throw new apiValidation.ApiInputError(message, {
|
|
79
|
+
code: options.code || 'invalid-work-item-input',
|
|
80
|
+
statusCode: options.statusCode,
|
|
81
|
+
field: options.field,
|
|
82
|
+
path: options.path || options.field,
|
|
83
|
+
expected: options.expected,
|
|
84
|
+
rejectedValue: options.rejectedValue,
|
|
85
|
+
allowedValues: options.allowedValues,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _validateBody(body) {
|
|
90
|
+
return apiValidation.validatePlainObject(body, {
|
|
91
|
+
field: 'body',
|
|
92
|
+
path: 'body',
|
|
93
|
+
message: 'work-item request body must be a plain object',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _normalizeIdentifier(value, field, options = {}) {
|
|
98
|
+
if (value === undefined && options.optional) return undefined;
|
|
99
|
+
return apiValidation.validateIdentifier(value, {
|
|
100
|
+
field,
|
|
101
|
+
path: field,
|
|
102
|
+
trim: true,
|
|
103
|
+
maxLength: options.maxLength || 128,
|
|
104
|
+
code: options.code,
|
|
105
|
+
message: `${field} must be a valid identifier`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _normalizeSource(value, field = 'source', options = {}) {
|
|
110
|
+
if (value === undefined && options.optional) return undefined;
|
|
111
|
+
if (value === null && options.nullable) return '';
|
|
112
|
+
return apiValidation.validateString(value, {
|
|
113
|
+
field,
|
|
114
|
+
path: field,
|
|
115
|
+
trim: true,
|
|
116
|
+
allowEmpty: options.allowEmpty !== false,
|
|
117
|
+
message: `${field} must be a string`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _normalizeText(value, field, options = {}) {
|
|
122
|
+
if (value === undefined && options.optional) return undefined;
|
|
123
|
+
if (value === null && options.nullable) return null;
|
|
124
|
+
return apiValidation.validateString(value, {
|
|
125
|
+
field,
|
|
126
|
+
path: field,
|
|
127
|
+
trim: options.trim === true,
|
|
128
|
+
allowEmpty: options.allowEmpty !== false,
|
|
129
|
+
maxLength: options.maxLength,
|
|
130
|
+
message: `${field} must be ${options.allowEmpty === false ? 'a non-empty string' : 'a string'}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _normalizeBoolean(value, field) {
|
|
135
|
+
return apiValidation.validateBoolean(value, {
|
|
136
|
+
field,
|
|
137
|
+
path: field,
|
|
138
|
+
message: `${field} must be a boolean`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _normalizePriority(value, validatePriority) {
|
|
143
|
+
if (typeof validatePriority !== 'function') {
|
|
144
|
+
throw new TypeError('normalize work-item input requires validatePriority');
|
|
145
|
+
}
|
|
146
|
+
const error = validatePriority(value);
|
|
147
|
+
if (!error) return value;
|
|
148
|
+
_inputError(error.error, {
|
|
149
|
+
code: error.code,
|
|
150
|
+
field: 'priority',
|
|
151
|
+
expected: 'canonical work-item priority',
|
|
152
|
+
rejectedValue: error.rejectedValue,
|
|
153
|
+
allowedValues: error.allowedValues,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _normalizeWorkType(value, options = {}) {
|
|
158
|
+
if (value === undefined) return options.defaultValue;
|
|
159
|
+
const raw = _normalizeText(value, 'type', {
|
|
160
|
+
trim: true,
|
|
161
|
+
allowEmpty: false,
|
|
162
|
+
maxLength: 64,
|
|
163
|
+
});
|
|
164
|
+
const normalized = typeof options.normalizeWorkType === 'function'
|
|
165
|
+
? options.normalizeWorkType(raw)
|
|
166
|
+
: raw;
|
|
167
|
+
const allowed = options.allowedWorkTypes instanceof Set
|
|
168
|
+
? [...options.allowedWorkTypes]
|
|
169
|
+
: (Array.isArray(options.allowedWorkTypes) ? options.allowedWorkTypes : []);
|
|
170
|
+
if (!allowed.includes(normalized)) {
|
|
171
|
+
_inputError('type must be a recognized work-item type', {
|
|
172
|
+
code: 'invalid-work-item-type',
|
|
173
|
+
field: 'type',
|
|
174
|
+
rejectedValue: value,
|
|
175
|
+
allowedValues: allowed,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function _normalizeAgent(value, field, options = {}) {
|
|
182
|
+
if (value === undefined && options.optional) return undefined;
|
|
183
|
+
if (value === null && options.nullable) return null;
|
|
184
|
+
const agent = _normalizeText(value, field, {
|
|
185
|
+
trim: true,
|
|
186
|
+
allowEmpty: options.allowEmpty === true,
|
|
187
|
+
maxLength: 128,
|
|
188
|
+
});
|
|
189
|
+
if (!agent && options.allowEmpty) return null;
|
|
190
|
+
_normalizeIdentifier(agent, field, { code: 'invalid-agent' });
|
|
191
|
+
const knownAgents = Array.isArray(options.knownAgents) ? options.knownAgents : [];
|
|
192
|
+
const tempAllowed = options.allowTempAgents === true && /^temp-/.test(agent);
|
|
193
|
+
if (knownAgents.length > 0 && !knownAgents.includes(agent) && !tempAllowed) {
|
|
194
|
+
_inputError(`Unknown agent "${agent}". Valid agents: ${knownAgents.join(', ')}`, {
|
|
195
|
+
code: 'unknown-agent',
|
|
196
|
+
field,
|
|
197
|
+
rejectedValue: value,
|
|
198
|
+
allowedValues: knownAgents,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return agent;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function _normalizeAgents(value, options = {}) {
|
|
205
|
+
if (value === undefined) return undefined;
|
|
206
|
+
const entries = typeof value === 'string'
|
|
207
|
+
? [value]
|
|
208
|
+
: apiValidation.validateArray(value, {
|
|
209
|
+
field: 'agents',
|
|
210
|
+
path: 'agents',
|
|
211
|
+
message: 'agents must be a string or an array of strings',
|
|
212
|
+
});
|
|
213
|
+
const normalized = [];
|
|
214
|
+
entries.forEach((entry, index) => {
|
|
215
|
+
if (typeof entry !== 'string') {
|
|
216
|
+
_inputError('agents entries must be strings', {
|
|
217
|
+
field: 'agents',
|
|
218
|
+
path: ['agents', index],
|
|
219
|
+
rejectedValue: entry,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (!entry.trim()) return;
|
|
223
|
+
normalized.push(_normalizeAgent(entry, 'agents', options));
|
|
224
|
+
});
|
|
225
|
+
return normalized;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function _normalizeStringList(value, field, options = {}) {
|
|
229
|
+
if (value === undefined) return undefined;
|
|
230
|
+
const entries = options.allowStringAlias && typeof value === 'string'
|
|
231
|
+
? value.split(/\r?\n/)
|
|
232
|
+
: apiValidation.validateArray(value, {
|
|
233
|
+
field,
|
|
234
|
+
path: field,
|
|
235
|
+
message: options.allowStringAlias
|
|
236
|
+
? `${field} must be a string or an array of strings`
|
|
237
|
+
: `${field} must be an array of strings`,
|
|
238
|
+
});
|
|
239
|
+
const normalized = [];
|
|
240
|
+
entries.forEach((entry, index) => {
|
|
241
|
+
if (typeof entry !== 'string') {
|
|
242
|
+
_inputError(`${field} entries must be strings`, {
|
|
243
|
+
field,
|
|
244
|
+
path: [field, index],
|
|
245
|
+
rejectedValue: entry,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const trimmed = entry.trim();
|
|
249
|
+
if (!trimmed) return;
|
|
250
|
+
if (options.identifiers) _normalizeIdentifier(trimmed, field);
|
|
251
|
+
normalized.push(trimmed);
|
|
252
|
+
});
|
|
253
|
+
return normalized;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function _normalizeReferences(value) {
|
|
257
|
+
if (value === undefined) return undefined;
|
|
258
|
+
const references = apiValidation.validateArray(value, {
|
|
259
|
+
field: 'references',
|
|
260
|
+
path: 'references',
|
|
261
|
+
message: 'references must be an array of objects',
|
|
262
|
+
});
|
|
263
|
+
return references.map((entry, index) => {
|
|
264
|
+
apiValidation.validatePlainObject(entry, {
|
|
265
|
+
field: 'references',
|
|
266
|
+
path: ['references', index],
|
|
267
|
+
message: 'references entries must be plain objects',
|
|
268
|
+
});
|
|
269
|
+
const url = _normalizeText(entry.url, 'references', {
|
|
270
|
+
trim: true,
|
|
271
|
+
allowEmpty: false,
|
|
272
|
+
});
|
|
273
|
+
const normalized = { ...entry, url };
|
|
274
|
+
for (const field of ['title', 'label', 'type', 'description']) {
|
|
275
|
+
if (_hasOwn(entry, field)) {
|
|
276
|
+
normalized[field] = _normalizeText(entry[field], 'references', {
|
|
277
|
+
trim: true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return normalized;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function _normalizePrFollowup(value) {
|
|
286
|
+
if (value === undefined || value === null) return null;
|
|
287
|
+
apiValidation.validatePlainObject(value, {
|
|
288
|
+
field: 'meta.pr_followup',
|
|
289
|
+
path: 'meta.pr_followup',
|
|
290
|
+
message: 'meta.pr_followup must be a plain object',
|
|
291
|
+
});
|
|
292
|
+
const normalized = {};
|
|
293
|
+
for (const field of PR_FOLLOWUP_FIELDS) {
|
|
294
|
+
normalized[field] = _normalizeText(value[field], `meta.pr_followup.${field}`, {
|
|
295
|
+
trim: true,
|
|
296
|
+
allowEmpty: false,
|
|
297
|
+
maxLength: PR_FOLLOWUP_MAX_FIELD_LEN,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return normalized;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function validatePrFollowupShape(value) {
|
|
304
|
+
try {
|
|
305
|
+
return { valid: true, value: _normalizePrFollowup(value) };
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return { valid: false, error: error.message };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function _normalizeWorkdir(value, field, validateWorkdir) {
|
|
312
|
+
if (typeof validateWorkdir !== 'function') {
|
|
313
|
+
throw new TypeError('normalize work-item input requires validateWorkdir');
|
|
314
|
+
}
|
|
315
|
+
const result = validateWorkdir(value);
|
|
316
|
+
if (!result || result.valid !== true) {
|
|
317
|
+
_inputError(`${field}: ${result?.error || 'invalid workdir'}`, {
|
|
318
|
+
code: 'invalid-workdir',
|
|
319
|
+
field,
|
|
320
|
+
rejectedValue: value,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return result.value;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function _normalizeMeta(value, options = {}) {
|
|
327
|
+
if (value === undefined) return undefined;
|
|
328
|
+
apiValidation.validatePlainObject(value, {
|
|
329
|
+
field: 'meta',
|
|
330
|
+
path: 'meta',
|
|
331
|
+
message: 'meta must be a plain object',
|
|
332
|
+
});
|
|
333
|
+
const normalized = { ...value };
|
|
334
|
+
if (_hasOwn(value, 'pr_followup')) {
|
|
335
|
+
const followup = _normalizePrFollowup(value.pr_followup);
|
|
336
|
+
if (followup) normalized.pr_followup = followup;
|
|
337
|
+
else delete normalized.pr_followup;
|
|
338
|
+
}
|
|
339
|
+
if (_hasOwn(value, 'workdir')) {
|
|
340
|
+
const workdir = _normalizeWorkdir(value.workdir, 'meta.workdir', options.validateWorkdir);
|
|
341
|
+
if (workdir === null) delete normalized.workdir;
|
|
342
|
+
else normalized.workdir = workdir;
|
|
343
|
+
}
|
|
344
|
+
for (const field of META_BOOLEAN_FIELDS) {
|
|
345
|
+
if (_hasOwn(value, field)) {
|
|
346
|
+
normalized[field] = _normalizeBoolean(value[field], `meta.${field}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return normalized;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function _normalizePrFields(body, normalized) {
|
|
353
|
+
for (const field of PR_REFERENCE_FIELDS) {
|
|
354
|
+
if (!_hasOwn(body, field)) continue;
|
|
355
|
+
const value = body[field];
|
|
356
|
+
if ((typeof value !== 'string' || !value.trim())
|
|
357
|
+
&& (!Number.isInteger(value) || value <= 0)) {
|
|
358
|
+
_inputError(`${field} must be a non-empty string or positive integer`, {
|
|
359
|
+
field,
|
|
360
|
+
rejectedValue: value,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
normalized[field] = typeof value === 'string' ? value.trim() : value;
|
|
364
|
+
}
|
|
365
|
+
if (_hasOwn(body, 'prNumber')) {
|
|
366
|
+
const value = body.prNumber;
|
|
367
|
+
const numeric = typeof value === 'string' && /^\d+$/.test(value.trim())
|
|
368
|
+
? Number(value.trim())
|
|
369
|
+
: value;
|
|
370
|
+
if (!Number.isSafeInteger(numeric) || numeric <= 0) {
|
|
371
|
+
_inputError('prNumber must be a positive integer', {
|
|
372
|
+
field: 'prNumber',
|
|
373
|
+
rejectedValue: value,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
normalized.prNumber = numeric;
|
|
377
|
+
}
|
|
378
|
+
for (const field of ['prBranch', 'prTitle']) {
|
|
379
|
+
if (_hasOwn(body, field)) {
|
|
380
|
+
normalized[field] = _normalizeText(body[field], field, {
|
|
381
|
+
trim: true,
|
|
382
|
+
allowEmpty: false,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function normalizeCreateInput(body, options = {}) {
|
|
389
|
+
_validateBody(body);
|
|
390
|
+
const normalized = { ...body };
|
|
391
|
+
normalized.title = _normalizeText(body.title, 'title', {
|
|
392
|
+
trim: true,
|
|
393
|
+
allowEmpty: false,
|
|
394
|
+
});
|
|
395
|
+
normalized.description = body.description === undefined
|
|
396
|
+
? ''
|
|
397
|
+
: _normalizeText(body.description, 'description');
|
|
398
|
+
normalized.type = _normalizeWorkType(body.type, {
|
|
399
|
+
defaultValue: options.defaultType,
|
|
400
|
+
normalizeWorkType: options.normalizeWorkType,
|
|
401
|
+
allowedWorkTypes: options.allowedWorkTypes,
|
|
402
|
+
});
|
|
403
|
+
_normalizePriority(body.priority, options.validatePriority);
|
|
404
|
+
if (_hasOwn(body, 'project')) {
|
|
405
|
+
normalized.project = _normalizeSource(body.project, 'project', {
|
|
406
|
+
nullable: true,
|
|
407
|
+
allowEmpty: true,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
normalized.agent = _normalizeAgent(body.agent, 'agent', {
|
|
411
|
+
optional: true,
|
|
412
|
+
nullable: true,
|
|
413
|
+
allowEmpty: true,
|
|
414
|
+
knownAgents: options.knownAgents,
|
|
415
|
+
allowTempAgents: options.allowTempAgents,
|
|
416
|
+
});
|
|
417
|
+
normalized.agents = _normalizeAgents(body.agents, {
|
|
418
|
+
knownAgents: options.knownAgents,
|
|
419
|
+
allowTempAgents: options.allowTempAgents,
|
|
420
|
+
});
|
|
421
|
+
if (_hasOwn(body, 'scope')) {
|
|
422
|
+
const scope = _normalizeText(body.scope, 'scope', {
|
|
423
|
+
trim: true,
|
|
424
|
+
allowEmpty: false,
|
|
425
|
+
maxLength: 64,
|
|
426
|
+
});
|
|
427
|
+
if (!WORK_ITEM_SCOPES.includes(scope)) {
|
|
428
|
+
_inputError('scope must be a recognized work-item scope', {
|
|
429
|
+
field: 'scope',
|
|
430
|
+
rejectedValue: body.scope,
|
|
431
|
+
allowedValues: WORK_ITEM_SCOPES,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
normalized.scope = scope;
|
|
435
|
+
}
|
|
436
|
+
normalized.references = _normalizeReferences(body.references);
|
|
437
|
+
normalized.acceptanceCriteria = _normalizeStringList(
|
|
438
|
+
body.acceptanceCriteria,
|
|
439
|
+
'acceptanceCriteria',
|
|
440
|
+
{ allowStringAlias: true },
|
|
441
|
+
);
|
|
442
|
+
normalized.depends_on = _normalizeStringList(body.depends_on, 'depends_on', {
|
|
443
|
+
identifiers: true,
|
|
444
|
+
});
|
|
445
|
+
for (const field of ['skipPr', 'oneShot', 'agentLock', 'hardAgent']) {
|
|
446
|
+
if (_hasOwn(body, field)) normalized[field] = _normalizeBoolean(body[field], field);
|
|
447
|
+
}
|
|
448
|
+
normalized.meta = _normalizeMeta(body.meta, options);
|
|
449
|
+
for (const field of ['planFile', 'branch', 'branchStrategy', 'featureBranch']) {
|
|
450
|
+
if (_hasOwn(body, field)) {
|
|
451
|
+
normalized[field] = _normalizeText(body[field], field, {
|
|
452
|
+
trim: true,
|
|
453
|
+
allowEmpty: false,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
_normalizePrFields(body, normalized);
|
|
458
|
+
return normalized;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function normalizeUpdateInput(body, options = {}) {
|
|
462
|
+
_validateBody(body);
|
|
463
|
+
const normalized = { ...body };
|
|
464
|
+
normalized.id = _normalizeIdentifier(body.id, 'id');
|
|
465
|
+
if (_hasOwn(body, 'source')) {
|
|
466
|
+
normalized.source = _normalizeSource(body.source, 'source', {
|
|
467
|
+
nullable: true,
|
|
468
|
+
allowEmpty: true,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
if (_hasOwn(body, 'title')) {
|
|
472
|
+
normalized.title = _normalizeText(body.title, 'title', {
|
|
473
|
+
trim: true,
|
|
474
|
+
allowEmpty: false,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (_hasOwn(body, 'description')) {
|
|
478
|
+
normalized.description = _normalizeText(body.description, 'description');
|
|
479
|
+
}
|
|
480
|
+
if (_hasOwn(body, 'type')) {
|
|
481
|
+
normalized.type = _normalizeWorkType(body.type, {
|
|
482
|
+
normalizeWorkType: options.normalizeWorkType,
|
|
483
|
+
allowedWorkTypes: options.allowedWorkTypes,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
_normalizePriority(body.priority, options.validatePriority);
|
|
487
|
+
if (_hasOwn(body, 'agent')) {
|
|
488
|
+
normalized.agent = _normalizeAgent(body.agent, 'agent', {
|
|
489
|
+
nullable: true,
|
|
490
|
+
allowEmpty: true,
|
|
491
|
+
knownAgents: options.knownAgents,
|
|
492
|
+
allowTempAgents: options.allowTempAgents,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (_hasOwn(body, 'project')) {
|
|
496
|
+
normalized.project = _normalizeSource(body.project, 'project', {
|
|
497
|
+
nullable: true,
|
|
498
|
+
allowEmpty: true,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
normalized.references = _normalizeReferences(body.references);
|
|
502
|
+
normalized.acceptanceCriteria = _normalizeStringList(
|
|
503
|
+
body.acceptanceCriteria,
|
|
504
|
+
'acceptanceCriteria',
|
|
505
|
+
{ allowStringAlias: true },
|
|
506
|
+
);
|
|
507
|
+
normalized.depends_on = _normalizeStringList(body.depends_on, 'depends_on', {
|
|
508
|
+
identifiers: true,
|
|
509
|
+
});
|
|
510
|
+
if (_hasOwn(body, 'workdir')) {
|
|
511
|
+
normalized.workdir = _normalizeWorkdir(body.workdir, 'workdir', options.validateWorkdir);
|
|
512
|
+
}
|
|
513
|
+
normalized.meta = _normalizeMeta(body.meta, options);
|
|
514
|
+
return normalized;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function normalizeActionInput(action, body, options = {}) {
|
|
518
|
+
_validateBody(body);
|
|
519
|
+
const normalized = {
|
|
520
|
+
...body,
|
|
521
|
+
id: _normalizeIdentifier(body.id, 'id'),
|
|
522
|
+
};
|
|
523
|
+
if (_hasOwn(body, 'source')) {
|
|
524
|
+
normalized.source = _normalizeSource(body.source, 'source', {
|
|
525
|
+
nullable: true,
|
|
526
|
+
allowEmpty: true,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (action === 'retry') {
|
|
530
|
+
if (_hasOwn(body, 'prdFile')) {
|
|
531
|
+
const prdFile = _normalizeText(body.prdFile, 'prdFile', {
|
|
532
|
+
trim: true,
|
|
533
|
+
allowEmpty: false,
|
|
534
|
+
});
|
|
535
|
+
if (typeof options.validatePrdFilename !== 'function') {
|
|
536
|
+
throw new TypeError('retry validation requires validatePrdFilename');
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
normalized.prdFile = options.validatePrdFilename(prdFile);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
_inputError(error.message, {
|
|
542
|
+
code: 'invalid-prd-filename',
|
|
543
|
+
field: 'prdFile',
|
|
544
|
+
rejectedValue: body.prdFile,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (_hasOwn(body, 'force')) normalized.force = _normalizeBoolean(body.force, 'force');
|
|
549
|
+
} else if (action === 'cancel') {
|
|
550
|
+
if (_hasOwn(body, 'reason')) {
|
|
551
|
+
normalized.reason = _normalizeText(body.reason, 'reason', {
|
|
552
|
+
trim: true,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
} else if (action === 'reopen') {
|
|
556
|
+
if (_hasOwn(body, 'project')) {
|
|
557
|
+
normalized.project = _normalizeSource(body.project, 'project', {
|
|
558
|
+
nullable: true,
|
|
559
|
+
allowEmpty: true,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (_hasOwn(body, 'description')) {
|
|
563
|
+
normalized.description = _normalizeText(body.description, 'description');
|
|
564
|
+
}
|
|
565
|
+
} else if (action === 'feedback') {
|
|
566
|
+
normalized.rating = apiValidation.validateEnum(body.rating, {
|
|
567
|
+
field: 'rating',
|
|
568
|
+
path: 'rating',
|
|
569
|
+
values: WORK_ITEM_FEEDBACK_RATINGS,
|
|
570
|
+
message: 'rating must be "up" or "down"',
|
|
571
|
+
});
|
|
572
|
+
if (_hasOwn(body, 'comment')) {
|
|
573
|
+
normalized.comment = _normalizeText(body.comment, 'comment');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return normalized;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function normalizeReadInput(id, source) {
|
|
580
|
+
return {
|
|
581
|
+
id: _normalizeIdentifier(id, 'id'),
|
|
582
|
+
source: source === undefined
|
|
583
|
+
? undefined
|
|
584
|
+
: _normalizeSource(source, 'source', { allowEmpty: true }),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function normalizeTraceHeaders(headers = {}) {
|
|
589
|
+
const normalized = {};
|
|
590
|
+
for (const [header, field] of [
|
|
591
|
+
['x-minions-agent', 'X-Minions-Agent'],
|
|
592
|
+
['x-minions-origin-wi', 'X-Minions-Origin-Wi'],
|
|
593
|
+
]) {
|
|
594
|
+
if (!_hasOwn(headers, header)) continue;
|
|
595
|
+
normalized[header] = _normalizeIdentifier(headers[header], field);
|
|
596
|
+
}
|
|
597
|
+
return normalized;
|
|
598
|
+
}
|
|
599
|
+
|
|
47
600
|
module.exports = {
|
|
48
601
|
hasWorkItemSemantics,
|
|
49
602
|
hasWorkItemProvenance,
|
|
50
603
|
validateNewWorkItem,
|
|
51
604
|
isMalformedPhantomWorkItem,
|
|
605
|
+
WORK_ITEM_SCOPES,
|
|
606
|
+
WORK_ITEM_FEEDBACK_RATINGS,
|
|
607
|
+
validatePrFollowupShape,
|
|
608
|
+
normalizeCreateInput,
|
|
609
|
+
normalizeUpdateInput,
|
|
610
|
+
normalizeActionInput,
|
|
611
|
+
normalizeReadInput,
|
|
612
|
+
normalizeTraceHeaders,
|
|
52
613
|
};
|
|
@@ -193,7 +193,7 @@ function readArchivedWorkItemsForScope(scope) {
|
|
|
193
193
|
return out;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function archiveWorkItem(scope, id) {
|
|
196
|
+
function archiveWorkItem(scope, id, opts = {}) {
|
|
197
197
|
const { getDb, withTransaction } = require('./db');
|
|
198
198
|
const db = getDb();
|
|
199
199
|
const archived = withTransaction(db, () => {
|
|
@@ -201,6 +201,17 @@ function archiveWorkItem(scope, id) {
|
|
|
201
201
|
if (!row) return null;
|
|
202
202
|
const item = _parseRow(row);
|
|
203
203
|
if (!item) throw new Error(`work item ${id} has invalid SQL data`);
|
|
204
|
+
const allowedStatuses = opts.allowedStatuses instanceof Set
|
|
205
|
+
? opts.allowedStatuses
|
|
206
|
+
: (Array.isArray(opts.allowedStatuses) ? new Set(opts.allowedStatuses) : null);
|
|
207
|
+
if (allowedStatuses && !allowedStatuses.has(item.status)) {
|
|
208
|
+
const error = new Error(
|
|
209
|
+
`cannot archive item with status: ${item.status || 'unknown'}`,
|
|
210
|
+
);
|
|
211
|
+
error.code = 'invalid-work-item-status';
|
|
212
|
+
error.statusCode = 409;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
204
215
|
item._archived = true;
|
|
205
216
|
item.archivedAt = item.archivedAt || new Date().toISOString();
|
|
206
217
|
db.prepare('UPDATE work_items SET archived=1, data=?, updated_at=? WHERE scope=? AND id=?')
|
|
@@ -287,6 +298,54 @@ function moveWorkItem(sourceScope, targetScope, id, item) {
|
|
|
287
298
|
return moved;
|
|
288
299
|
}
|
|
289
300
|
|
|
301
|
+
function moveWorkItemWithUpdate(sourceScope, targetScope, id, updater) {
|
|
302
|
+
if (typeof updater !== 'function') throw new TypeError('moveWorkItemWithUpdate requires an updater');
|
|
303
|
+
const { getDb, withTransaction } = require('./db');
|
|
304
|
+
const db = getDb();
|
|
305
|
+
const outcome = withTransaction(db, () => {
|
|
306
|
+
const source = db.prepare(
|
|
307
|
+
'SELECT data FROM work_items WHERE scope=? AND id=? AND archived=0',
|
|
308
|
+
).get(sourceScope, String(id));
|
|
309
|
+
if (!source) return { moved: false, reason: 'not-found', item: null };
|
|
310
|
+
const target = db.prepare(
|
|
311
|
+
'SELECT 1 FROM work_items WHERE scope=? AND id=?',
|
|
312
|
+
).get(targetScope, String(id));
|
|
313
|
+
if (target) return { moved: false, reason: 'target-exists', item: null };
|
|
314
|
+
|
|
315
|
+
const current = _parseRow(source);
|
|
316
|
+
if (!current) throw new Error(`work item ${id} has invalid SQL data`);
|
|
317
|
+
const updated = updater(current) || current;
|
|
318
|
+
require('./work-item-validation').validateNewWorkItem(updated);
|
|
319
|
+
const result = db.prepare(`
|
|
320
|
+
UPDATE work_items SET
|
|
321
|
+
scope=?, status=?, type=?, agent=?, parent_id=?, created_at=?,
|
|
322
|
+
completed_at=?, data=?, updated_at=?
|
|
323
|
+
WHERE scope=? AND id=? AND archived=0
|
|
324
|
+
`).run(
|
|
325
|
+
targetScope,
|
|
326
|
+
String(updated.status || 'pending'),
|
|
327
|
+
updated.type || null,
|
|
328
|
+
updated.dispatched_to || updated.agent || null,
|
|
329
|
+
updated.parent_id || null,
|
|
330
|
+
_toMs(updated.created_at || updated.created),
|
|
331
|
+
_toMs(updated.completed_at || updated.completedAt),
|
|
332
|
+
JSON.stringify(updated),
|
|
333
|
+
Date.now(),
|
|
334
|
+
sourceScope,
|
|
335
|
+
String(id),
|
|
336
|
+
);
|
|
337
|
+
if (Number(result.changes) === 0) {
|
|
338
|
+
return { moved: false, reason: 'not-found', item: null };
|
|
339
|
+
}
|
|
340
|
+
return { moved: true, reason: null, item: updated };
|
|
341
|
+
});
|
|
342
|
+
if (outcome.moved) {
|
|
343
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
344
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
345
|
+
}
|
|
346
|
+
return outcome;
|
|
347
|
+
}
|
|
348
|
+
|
|
290
349
|
module.exports = {
|
|
291
350
|
readWorkItemsForScope,
|
|
292
351
|
readAllWorkItems,
|
|
@@ -294,6 +353,7 @@ module.exports = {
|
|
|
294
353
|
archiveWorkItem,
|
|
295
354
|
replaceArchivedWorkItems,
|
|
296
355
|
moveWorkItem,
|
|
356
|
+
moveWorkItemWithUpdate,
|
|
297
357
|
applyWorkItemsMutation,
|
|
298
358
|
dropScope,
|
|
299
359
|
};
|