@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.
Files changed (185) hide show
  1. package/bin/install-internal-minions.js +1209 -0
  2. package/bin/minions.js +75 -9
  3. package/dashboard/docs/typography.md +27 -12
  4. package/dashboard/js/command-center.js +13 -11
  5. package/dashboard/js/live-stream.js +1 -1
  6. package/dashboard/js/memory-search.js +388 -43
  7. package/dashboard/js/modal-qa.js +5 -5
  8. package/dashboard/js/qa.js +39 -26
  9. package/dashboard/js/refresh.js +37 -18
  10. package/dashboard/js/render-agents.js +14 -18
  11. package/dashboard/js/render-dispatch.js +14 -74
  12. package/dashboard/js/render-inbox.js +3 -3
  13. package/dashboard/js/render-meetings.js +8 -8
  14. package/dashboard/js/render-other.js +45 -20
  15. package/dashboard/js/render-pipelines.js +6 -6
  16. package/dashboard/js/render-plans.js +14 -14
  17. package/dashboard/js/render-prd.js +46 -46
  18. package/dashboard/js/render-prs.js +9 -65
  19. package/dashboard/js/render-schedules.js +5 -5
  20. package/dashboard/js/render-skills.js +6 -6
  21. package/dashboard/js/render-utils.js +4 -4
  22. package/dashboard/js/render-watches.js +4 -16
  23. package/dashboard/js/render-work-items.js +95 -107
  24. package/dashboard/js/settings.js +175 -74
  25. package/dashboard/layout.html +3 -2
  26. package/dashboard/pages/inbox.html +1 -1
  27. package/dashboard/pages/work.html +1 -1
  28. package/dashboard/shared/model-display.js +9 -0
  29. package/dashboard/shared/watches-source.js +43 -0
  30. package/dashboard/shared/welcome-popup.js +259 -0
  31. package/dashboard/slim/body.html +6 -6
  32. package/dashboard/slim/js/chat.js +4 -2
  33. package/dashboard/slim/js/history.js +10 -3
  34. package/dashboard/slim/js/modals-tiles.js +85 -16
  35. package/dashboard/slim/js/status.js +12 -16
  36. package/dashboard/slim/styles.css +24 -4
  37. package/dashboard/styles.css +62 -27
  38. package/dashboard-build.js +8 -2
  39. package/dashboard.js +3854 -2514
  40. package/docs/README.md +7 -5
  41. package/docs/api-errors.md +144 -0
  42. package/docs/auto-discovery.md +83 -61
  43. package/docs/capture-demos.js +173 -30
  44. package/docs/command-center.md +5 -1
  45. package/docs/completion-reports.md +127 -11
  46. package/docs/constants.md +15 -3
  47. package/docs/constellation-style-telemetry.md +2 -2
  48. package/docs/copilot-cli-schema.md +32 -22
  49. package/docs/cross-repo-plans.md +30 -30
  50. package/docs/demo/memory-system.html +1 -1
  51. package/docs/deprecated.json +132 -18
  52. package/docs/diagnostics-memory.md +8 -4
  53. package/docs/human-vs-automated.md +2 -2
  54. package/docs/index.html +9 -2
  55. package/docs/internal-install.md +212 -0
  56. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  57. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  58. package/docs/kb-sweep.md +2 -2
  59. package/docs/keep-processes.md +8 -1
  60. package/docs/live-checkout-mode.md +1 -1
  61. package/docs/managed-spawn.md +21 -3
  62. package/docs/named-agents.md +3 -2
  63. package/docs/onboarding.md +26 -0
  64. package/docs/plan-lifecycle.md +32 -32
  65. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
  66. package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
  67. package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
  68. package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
  69. package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
  70. package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
  71. package/docs/qa-runbook-lifecycle.md +67 -51
  72. package/docs/qa-runbooks.md +2 -2
  73. package/docs/runtime-adapters.md +101 -50
  74. package/docs/security.md +28 -0
  75. package/docs/self-improvement.md +47 -13
  76. package/docs/skills.md +29 -0
  77. package/docs/slim-ux/concepts.md +2 -2
  78. package/docs/specs/agent-configurability.md +43 -41
  79. package/docs/specs/agent-rename.md +13 -13
  80. package/docs/team-memory.md +283 -14
  81. package/docs/tutorials/01-install-and-connect.md +23 -0
  82. package/docs/tutorials/03-plan-driven-feature.md +3 -3
  83. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  84. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  85. package/docs/tutorials/README.md +4 -3
  86. package/docs/visual-evidence-ci.md +103 -0
  87. package/docs/watches.md +35 -30
  88. package/docs/worktree-lifecycle.md +151 -13
  89. package/engine/acp-transport.js +1 -0
  90. package/engine/ado.js +80 -33
  91. package/engine/agent-api-validation.js +571 -0
  92. package/engine/agent-worker-pool.js +7 -6
  93. package/engine/api-contracts/agent-content.js +761 -0
  94. package/engine/api-contracts/cc-ops.js +219 -0
  95. package/engine/api-contracts/config-runtime.js +419 -0
  96. package/engine/api-contracts/core.js +138 -0
  97. package/engine/api-contracts/index.js +506 -0
  98. package/engine/api-contracts/orchestration.js +602 -0
  99. package/engine/api-contracts/pull-requests.js +323 -0
  100. package/engine/api-contracts/qa-process.js +764 -0
  101. package/engine/api-contracts/work-plan-prd.js +494 -0
  102. package/engine/api-validation.js +543 -0
  103. package/engine/cc-api-validation.js +586 -0
  104. package/engine/cc-worker-pool.js +9 -1
  105. package/engine/cleanup.js +393 -271
  106. package/engine/cli.js +65 -7
  107. package/engine/comment-classifier.js +70 -5
  108. package/engine/consolidation.js +123 -5
  109. package/engine/content-api-validation.js +606 -0
  110. package/engine/create-pr-worktree.js +22 -1
  111. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  112. package/engine/dispatch.js +33 -0
  113. package/engine/distribution.js +189 -0
  114. package/engine/execution-model.js +59 -0
  115. package/engine/features.js +12 -0
  116. package/engine/github.js +67 -25
  117. package/engine/harness-context.js +313 -0
  118. package/engine/inbox-store.js +1 -1
  119. package/engine/kb-sweep.js +161 -38
  120. package/engine/keep-process-sweep.js +43 -3
  121. package/engine/lifecycle.js +1066 -165
  122. package/engine/llm.js +4 -0
  123. package/engine/managed-spawn.js +463 -48
  124. package/engine/meeting.js +197 -34
  125. package/engine/memory-retrieval.js +280 -0
  126. package/engine/memory-store.js +821 -38
  127. package/engine/model-discovery.js +17 -3
  128. package/engine/pipeline.js +802 -14
  129. package/engine/plan-prd-validation.js +745 -0
  130. package/engine/playbook.js +206 -17
  131. package/engine/pooled-agent-process.js +6 -1
  132. package/engine/pr-action.js +1 -1
  133. package/engine/pr-issue-validation.js +653 -0
  134. package/engine/pr-resolve.js +75 -13
  135. package/engine/prd-store.js +39 -6
  136. package/engine/preflight.js +7 -2
  137. package/engine/process-utils.js +79 -31
  138. package/engine/projects.js +198 -12
  139. package/engine/promotion.js +371 -0
  140. package/engine/qa-from-prd.js +3 -0
  141. package/engine/qa-process-validation.js +572 -0
  142. package/engine/qa-runbooks.js +41 -37
  143. package/engine/qa-runners/maestro.js +11 -7
  144. package/engine/qa-runners/playwright.js +37 -14
  145. package/engine/qa-runners.js +9 -10
  146. package/engine/qa-runs.js +124 -11
  147. package/engine/qa-sessions.js +139 -40
  148. package/engine/quarantine-refs.js +81 -11
  149. package/engine/queries.js +163 -9
  150. package/engine/review-learning-backfill.js +482 -0
  151. package/engine/review-learning.js +1236 -0
  152. package/engine/runtimes/claude.js +60 -2
  153. package/engine/runtimes/codex.js +57 -16
  154. package/engine/runtimes/contract.js +21 -0
  155. package/engine/runtimes/copilot.js +51 -3
  156. package/engine/runtimes/index.js +1 -0
  157. package/engine/scheduler.js +201 -32
  158. package/engine/settings-validation.js +909 -0
  159. package/engine/shared.js +705 -208
  160. package/engine/spawn-agent.js +48 -21
  161. package/engine/steering-constraints.js +31 -0
  162. package/engine/steering-store.js +10 -2
  163. package/engine/steering.js +14 -2
  164. package/engine/timeout.js +404 -111
  165. package/engine/untrusted-fence.js +24 -10
  166. package/engine/watch-actions.js +7 -2
  167. package/engine/watches.js +353 -51
  168. package/engine/work-item-validation.js +561 -0
  169. package/engine/work-items-store.js +61 -1
  170. package/engine/worktree-gc.js +656 -37
  171. package/engine/worktree-preflight.js +154 -0
  172. package/engine.js +2068 -342
  173. package/package.json +1 -1
  174. package/playbooks/_pr-description-audit.md +88 -41
  175. package/playbooks/build-fix-complex.md +3 -3
  176. package/playbooks/docs.md +2 -2
  177. package/playbooks/fix.md +18 -18
  178. package/playbooks/implement.md +18 -18
  179. package/playbooks/qa-session-draft.md +10 -12
  180. package/playbooks/qa-session-execute.md +16 -12
  181. package/playbooks/qa-validate.md +8 -7
  182. package/playbooks/shared-rules.md +45 -0
  183. package/playbooks/test.md +4 -0
  184. package/prompts/cc-system.md +1 -1
  185. 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
  };