@yemi33/minions 0.1.2423 → 0.1.2425

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 (184) 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 +6 -4
  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/internal-install.md +190 -0
  55. package/docs/kb-pr3223-cascade-archiving.md +16 -0
  56. package/docs/kb-pr696-merge-conflict-docs.md +23 -0
  57. package/docs/kb-sweep.md +2 -2
  58. package/docs/keep-processes.md +8 -1
  59. package/docs/live-checkout-mode.md +12 -7
  60. package/docs/managed-spawn.md +21 -3
  61. package/docs/named-agents.md +3 -2
  62. package/docs/plan-lifecycle.md +32 -32
  63. package/docs/pr-auto-fix-dispatch.md +3 -3
  64. package/docs/pr-review-fix-loop.md +6 -6
  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/03-plan-driven-feature.md +3 -3
  82. package/docs/tutorials/04-runtimes-and-harness.md +18 -21
  83. package/docs/tutorials/08-operations-and-recovery.md +3 -2
  84. package/docs/tutorials/README.md +4 -3
  85. package/docs/visual-evidence-ci.md +103 -0
  86. package/docs/watches.md +35 -30
  87. package/docs/worktree-lifecycle.md +151 -13
  88. package/engine/acp-transport.js +1 -0
  89. package/engine/ado.js +80 -33
  90. package/engine/agent-api-validation.js +571 -0
  91. package/engine/agent-worker-pool.js +7 -6
  92. package/engine/api-contracts/agent-content.js +761 -0
  93. package/engine/api-contracts/cc-ops.js +219 -0
  94. package/engine/api-contracts/config-runtime.js +419 -0
  95. package/engine/api-contracts/core.js +138 -0
  96. package/engine/api-contracts/index.js +506 -0
  97. package/engine/api-contracts/orchestration.js +602 -0
  98. package/engine/api-contracts/pull-requests.js +323 -0
  99. package/engine/api-contracts/qa-process.js +764 -0
  100. package/engine/api-contracts/work-plan-prd.js +494 -0
  101. package/engine/api-validation.js +543 -0
  102. package/engine/cc-api-validation.js +586 -0
  103. package/engine/cc-worker-pool.js +9 -1
  104. package/engine/cleanup.js +393 -271
  105. package/engine/cli.js +65 -7
  106. package/engine/comment-classifier.js +70 -5
  107. package/engine/consolidation.js +123 -5
  108. package/engine/content-api-validation.js +606 -0
  109. package/engine/create-pr-worktree.js +22 -1
  110. package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
  111. package/engine/dispatch.js +33 -0
  112. package/engine/distribution.js +189 -0
  113. package/engine/execution-model.js +59 -0
  114. package/engine/features.js +12 -0
  115. package/engine/github.js +67 -25
  116. package/engine/harness-context.js +313 -0
  117. package/engine/inbox-store.js +1 -1
  118. package/engine/kb-sweep.js +161 -38
  119. package/engine/keep-process-sweep.js +43 -3
  120. package/engine/lifecycle.js +1066 -165
  121. package/engine/llm.js +4 -0
  122. package/engine/managed-spawn.js +463 -48
  123. package/engine/meeting.js +197 -34
  124. package/engine/memory-retrieval.js +280 -0
  125. package/engine/memory-store.js +821 -38
  126. package/engine/model-discovery.js +17 -3
  127. package/engine/pipeline.js +802 -14
  128. package/engine/plan-prd-validation.js +745 -0
  129. package/engine/playbook.js +206 -17
  130. package/engine/pooled-agent-process.js +6 -1
  131. package/engine/pr-action.js +1 -1
  132. package/engine/pr-issue-validation.js +653 -0
  133. package/engine/pr-resolve.js +75 -13
  134. package/engine/prd-store.js +39 -6
  135. package/engine/preflight.js +7 -2
  136. package/engine/process-utils.js +79 -31
  137. package/engine/projects.js +198 -12
  138. package/engine/promotion.js +371 -0
  139. package/engine/qa-from-prd.js +3 -0
  140. package/engine/qa-process-validation.js +572 -0
  141. package/engine/qa-runbooks.js +41 -37
  142. package/engine/qa-runners/maestro.js +11 -7
  143. package/engine/qa-runners/playwright.js +37 -14
  144. package/engine/qa-runners.js +9 -10
  145. package/engine/qa-runs.js +124 -11
  146. package/engine/qa-sessions.js +139 -40
  147. package/engine/quarantine-refs.js +81 -11
  148. package/engine/queries.js +163 -9
  149. package/engine/review-learning-backfill.js +482 -0
  150. package/engine/review-learning.js +1236 -0
  151. package/engine/runtimes/claude.js +60 -2
  152. package/engine/runtimes/codex.js +57 -16
  153. package/engine/runtimes/contract.js +21 -0
  154. package/engine/runtimes/copilot.js +51 -3
  155. package/engine/runtimes/index.js +1 -0
  156. package/engine/scheduler.js +201 -32
  157. package/engine/settings-validation.js +909 -0
  158. package/engine/shared.js +705 -208
  159. package/engine/spawn-agent.js +48 -21
  160. package/engine/steering-constraints.js +31 -0
  161. package/engine/steering-store.js +10 -2
  162. package/engine/steering.js +14 -2
  163. package/engine/timeout.js +404 -111
  164. package/engine/untrusted-fence.js +24 -10
  165. package/engine/watch-actions.js +7 -2
  166. package/engine/watches.js +353 -51
  167. package/engine/work-item-validation.js +561 -0
  168. package/engine/work-items-store.js +61 -1
  169. package/engine/worktree-gc.js +656 -37
  170. package/engine/worktree-preflight.js +154 -0
  171. package/engine.js +2068 -342
  172. package/package.json +1 -1
  173. package/playbooks/_pr-description-audit.md +88 -41
  174. package/playbooks/build-fix-complex.md +3 -3
  175. package/playbooks/docs.md +2 -2
  176. package/playbooks/fix.md +18 -18
  177. package/playbooks/implement.md +18 -18
  178. package/playbooks/qa-session-draft.md +10 -12
  179. package/playbooks/qa-session-execute.md +16 -12
  180. package/playbooks/qa-validate.md +8 -7
  181. package/playbooks/shared-rules.md +45 -0
  182. package/playbooks/test.md +4 -0
  183. package/prompts/cc-system.md +1 -1
  184. package/skills/check-self-authored-review-comment/SKILL.md +2 -2
@@ -0,0 +1,653 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const apiValidation = require('./api-validation');
5
+ const issues = require('./issues');
6
+ const prAction = require('./pr-action');
7
+ const prFixTarget = require('./pr-fix-target');
8
+ const prResolve = require('./pr-resolve');
9
+ const shared = require('./shared');
10
+
11
+ const MAX_PR_REFERENCE_LENGTH = 2048;
12
+ const MAX_CONTEXT_LENGTH = 16 * 1024;
13
+ const MAX_ISSUE_DESCRIPTION_LENGTH = 64 * 1024;
14
+ const MAX_ISSUE_LABELS = 20;
15
+ const MAX_ISSUE_LABEL_LENGTH = 50;
16
+
17
+ function hasOwn(value, key) {
18
+ return Object.prototype.hasOwnProperty.call(value, key);
19
+ }
20
+
21
+ function inputError(message, options = {}) {
22
+ return new apiValidation.ApiInputError(message, {
23
+ statusCode: options.statusCode,
24
+ code: options.code || 'invalid-input',
25
+ field: options.field,
26
+ path: options.path || options.field,
27
+ expected: options.expected,
28
+ rejectedValue: options.rejectedValue,
29
+ allowedValues: options.allowedValues,
30
+ secret: options.secret === true,
31
+ cause: options.cause,
32
+ });
33
+ }
34
+
35
+ function validateBody(body) {
36
+ return apiValidation.validatePlainObject(body, {
37
+ field: 'body',
38
+ message: 'request body must be a JSON object',
39
+ });
40
+ }
41
+
42
+ function requiredString(value, field, options = {}) {
43
+ return apiValidation.validateString(value, {
44
+ field,
45
+ trim: options.trim !== false,
46
+ allowEmpty: false,
47
+ maxLength: options.maxLength,
48
+ secret: options.secret === true,
49
+ message: `${field} is required and must be a string`,
50
+ });
51
+ }
52
+
53
+ function optionalString(body, field, options = {}) {
54
+ if (!hasOwn(body, field) || body[field] === undefined || body[field] === null) {
55
+ return options.defaultValue;
56
+ }
57
+ return apiValidation.validateString(body[field], {
58
+ field,
59
+ trim: options.trim === true,
60
+ allowEmpty: options.allowEmpty !== false,
61
+ maxLength: options.maxLength,
62
+ message: `${field} must be a string`,
63
+ });
64
+ }
65
+
66
+ function optionalBoolean(body, field, defaultValue = false) {
67
+ if (!hasOwn(body, field) || body[field] === undefined) return defaultValue;
68
+ return apiValidation.validateBoolean(body[field], {
69
+ field,
70
+ message: `${field} must be a boolean`,
71
+ });
72
+ }
73
+
74
+ function positiveInteger(value, field) {
75
+ let candidate = value;
76
+ if (typeof candidate === 'string' && /^[0-9]+$/.test(candidate.trim())) {
77
+ candidate = Number(candidate.trim());
78
+ }
79
+ try {
80
+ return shared.validatePrNum(candidate);
81
+ } catch {
82
+ throw inputError(`${field} must be a positive safe integer`, {
83
+ field,
84
+ code: 'invalid-integer',
85
+ expected: 'positive safe integer',
86
+ rejectedValue: value,
87
+ });
88
+ }
89
+ }
90
+
91
+ function validatePrReference(value, field = 'url') {
92
+ const raw = requiredString(value, field, {
93
+ maxLength: MAX_PR_REFERENCE_LENGTH,
94
+ secret: true,
95
+ });
96
+ try {
97
+ const ref = prResolve.resolvePrActionRef({ url: raw }, prAction.PrActionError);
98
+ return { raw, ref };
99
+ } catch (cause) {
100
+ if (!(cause instanceof prAction.PrActionError)) throw cause;
101
+ throw inputError(`${field} must be a valid GitHub/ADO PR reference`, {
102
+ field,
103
+ code: 'invalid-pr-reference',
104
+ expected: 'GitHub/ADO PR URL or canonical PR id',
105
+ rejectedValue: raw,
106
+ secret: true,
107
+ cause,
108
+ });
109
+ }
110
+ }
111
+
112
+ function validateCanonicalPrId(value, field = 'prId') {
113
+ const raw = requiredString(value, field, {
114
+ maxLength: MAX_PR_REFERENCE_LENGTH,
115
+ secret: true,
116
+ });
117
+ if (!/^(github|ado):/i.test(raw)) {
118
+ throw inputError(`${field} must be a canonical GitHub/ADO PR id`, {
119
+ field,
120
+ code: 'invalid-pr-reference',
121
+ expected: 'github:owner/repo#number or ado:org/project/repo#number',
122
+ rejectedValue: raw,
123
+ secret: true,
124
+ });
125
+ }
126
+ try {
127
+ const { ref } = validatePrReference(raw, field);
128
+ return ref.id;
129
+ } catch (cause) {
130
+ if (!apiValidation.isApiInputError(cause)) throw cause;
131
+ throw inputError(`${field} must be a canonical GitHub/ADO PR id with a positive number`, {
132
+ field,
133
+ code: 'invalid-pr-reference',
134
+ expected: 'github:owner/repo#number or ado:org/project/repo#number',
135
+ rejectedValue: raw,
136
+ secret: true,
137
+ cause,
138
+ });
139
+ }
140
+ }
141
+
142
+ function validatePrDetailId(value) {
143
+ const raw = requiredString(value, 'id', {
144
+ maxLength: MAX_PR_REFERENCE_LENGTH,
145
+ secret: true,
146
+ });
147
+ if (/^[0-9]+$/.test(raw)) {
148
+ const number = positiveInteger(raw, 'id');
149
+ return { lookupId: String(number), canonicalId: null, number };
150
+ }
151
+ const canonicalId = validateCanonicalPrId(raw, 'id');
152
+ const ref = prResolve.normalizePrRef(canonicalId);
153
+ return { lookupId: canonicalId, canonicalId, number: ref.number };
154
+ }
155
+
156
+ function validatePrActionRequest(body) {
157
+ validateBody(body);
158
+ const { raw: url } = validatePrReference(body.url);
159
+ const execute = optionalBoolean(body, 'execute');
160
+ const draftOnly = optionalBoolean(body, 'draftOnly');
161
+ const resolved = prAction.resolvePrActionRequest({
162
+ url,
163
+ action: body.action,
164
+ });
165
+ if (draftOnly && resolved.action !== 'comment') {
166
+ throw inputError('draftOnly is only valid for the comment action', {
167
+ field: 'draftOnly',
168
+ code: 'invalid-combination',
169
+ rejectedValue: draftOnly,
170
+ });
171
+ }
172
+ return {
173
+ url,
174
+ action: resolved.action,
175
+ ref: resolved.ref,
176
+ execute,
177
+ draftOnly,
178
+ };
179
+ }
180
+
181
+ function validatePrFixRequest(body) {
182
+ validateBody(body);
183
+ const { raw: url, ref } = validatePrReference(body.url);
184
+ const remember = optionalBoolean(body, 'remember');
185
+ const reprompt = optionalBoolean(body, 'reprompt');
186
+ let target;
187
+ if (hasOwn(body, 'target')) {
188
+ target = requiredString(body.target, 'target', { maxLength: 64 }).toLowerCase();
189
+ if (!prFixTarget.EXECUTION_TARGET_IDS.includes(target)) {
190
+ throw inputError(`unknown execution target: ${JSON.stringify(body.target)}. Valid targets: ${prFixTarget.EXECUTION_TARGET_IDS.join(', ')}`, {
191
+ field: 'target',
192
+ code: 'invalid-enum',
193
+ rejectedValue: body.target,
194
+ allowedValues: prFixTarget.EXECUTION_TARGET_IDS,
195
+ });
196
+ }
197
+ }
198
+ if (remember && !target) {
199
+ throw inputError('remember requires an explicit target', {
200
+ field: 'remember',
201
+ code: 'invalid-combination',
202
+ rejectedValue: remember,
203
+ });
204
+ }
205
+ if (reprompt && target) {
206
+ throw inputError('reprompt cannot be combined with an explicit target', {
207
+ field: 'reprompt',
208
+ code: 'invalid-combination',
209
+ rejectedValue: reprompt,
210
+ });
211
+ }
212
+ return { url, ref, target, remember, reprompt };
213
+ }
214
+
215
+ function validatePrUrlRequest(body) {
216
+ validateBody(body);
217
+ const { raw: url, ref } = validatePrReference(body.url);
218
+ return { url, ref };
219
+ }
220
+
221
+ function resolveProject(projectName, projects) {
222
+ const name = requiredString(projectName, 'project', { maxLength: 128 });
223
+ const project = shared.findProjectByName(Array.isArray(projects) ? projects : [], name);
224
+ if (!project) {
225
+ throw inputError(`unknown project: ${name}`, {
226
+ statusCode: 404,
227
+ code: 'project-not-found',
228
+ field: 'project',
229
+ rejectedValue: name,
230
+ });
231
+ }
232
+ return { name: project.name, project };
233
+ }
234
+
235
+ function extractLinkedWorkItemId(context, workItemId) {
236
+ if (typeof workItemId === 'string' && workItemId.trim()) return workItemId.trim();
237
+ if (!context) return null;
238
+ if (typeof context === 'object' && !Array.isArray(context)
239
+ && typeof context.workItemId === 'string' && context.workItemId.trim()) {
240
+ return context.workItemId.trim();
241
+ }
242
+ if (typeof context === 'string') {
243
+ const match = context.match(/\b(P-[a-z0-9]{6,}|W-[a-z0-9]{6,}|PL-[a-z0-9]{6,})\b/i);
244
+ return match ? match[1] : null;
245
+ }
246
+ return null;
247
+ }
248
+
249
+ function validateLinkContext(context) {
250
+ if (context === undefined || context === null) return context;
251
+ if (typeof context === 'string') {
252
+ return apiValidation.validateString(context, {
253
+ field: 'context',
254
+ maxLength: MAX_CONTEXT_LENGTH,
255
+ message: 'context must be a string or plain object',
256
+ });
257
+ }
258
+ apiValidation.validatePlainObject(context, {
259
+ field: 'context',
260
+ message: 'context must be a string or plain object',
261
+ });
262
+ let encoded;
263
+ try { encoded = JSON.stringify(context); } catch {
264
+ throw inputError('context must be JSON serializable', {
265
+ field: 'context',
266
+ rejectedValue: context,
267
+ });
268
+ }
269
+ if (encoded.length > MAX_CONTEXT_LENGTH) {
270
+ throw inputError('context is too large', {
271
+ field: 'context',
272
+ code: 'invalid-string',
273
+ rejectedValue: context,
274
+ });
275
+ }
276
+ if (context && typeof context === 'object' && hasOwn(context, 'workItemId')) {
277
+ requiredString(context.workItemId, 'context.workItemId', { maxLength: 128 });
278
+ }
279
+ return context;
280
+ }
281
+
282
+ function validateWorkItemLink(workItemId, projectName, workItems) {
283
+ if (!workItemId) return;
284
+ const matches = (Array.isArray(workItems) ? workItems : [])
285
+ .filter(item => item && item.id === workItemId);
286
+ if (matches.length === 0) {
287
+ throw inputError(`work item ${workItemId} not found`, {
288
+ statusCode: 404,
289
+ code: 'work-item-not-found',
290
+ field: 'workItemId',
291
+ rejectedValue: workItemId,
292
+ });
293
+ }
294
+ if (!projectName && matches.length > 1) {
295
+ throw inputError(`work item ${workItemId} is ambiguous; specify project`, {
296
+ statusCode: 409,
297
+ code: 'work-item-link-conflict',
298
+ field: 'workItemId',
299
+ rejectedValue: workItemId,
300
+ });
301
+ }
302
+ if (projectName) {
303
+ const matchesProject = matches.some(item => {
304
+ const itemProject = String(item.project || item._source || '').trim();
305
+ return !itemProject || itemProject === 'central' || itemProject === projectName;
306
+ });
307
+ if (!matchesProject) {
308
+ throw inputError(`work item ${workItemId} does not belong to project ${projectName}`, {
309
+ statusCode: 409,
310
+ code: 'work-item-link-conflict',
311
+ field: 'workItemId',
312
+ rejectedValue: workItemId,
313
+ });
314
+ }
315
+ }
316
+ }
317
+
318
+ function inferProjectNameForRef(ref, projects) {
319
+ const matches = (Array.isArray(projects) ? projects : []).filter(project => {
320
+ if (shared.getProjectAllPrScopes(project).includes(ref.scope)) return true;
321
+ return ref.host === 'ado' && shared.isAdoPrScopeCompatible(ref.scope, project);
322
+ });
323
+ return matches.length === 1 ? matches[0].name : null;
324
+ }
325
+
326
+ function validatePrLinkRequest(body, options = {}) {
327
+ validateBody(body);
328
+ const { raw: url, ref } = validatePrReference(body.url);
329
+ const context = validateLinkContext(body.context);
330
+ const explicitWorkItemId = hasOwn(body, 'workItemId')
331
+ ? requiredString(body.workItemId, 'workItemId', { maxLength: 128 })
332
+ : null;
333
+ const nestedWorkItemId = extractLinkedWorkItemId(context, null);
334
+ if (explicitWorkItemId && nestedWorkItemId && explicitWorkItemId !== nestedWorkItemId) {
335
+ throw inputError('workItemId conflicts with context.workItemId', {
336
+ statusCode: 409,
337
+ code: 'work-item-link-conflict',
338
+ field: 'workItemId',
339
+ rejectedValue: explicitWorkItemId,
340
+ });
341
+ }
342
+ let project;
343
+ if (hasOwn(body, 'project') && body.project !== undefined && body.project !== null && body.project !== '') {
344
+ const resolved = resolveProject(body.project, options.projects);
345
+ project = resolved.name;
346
+ if (!shared.isPrCompatibleWithProject(resolved.project, ref)) {
347
+ throw inputError(`PR ${ref.id} does not belong to project ${project}`, {
348
+ statusCode: 409,
349
+ code: 'project-link-conflict',
350
+ field: 'project',
351
+ rejectedValue: project,
352
+ });
353
+ }
354
+ }
355
+ const workItemId = explicitWorkItemId || nestedWorkItemId || null;
356
+ const effectiveProject = project
357
+ || inferProjectNameForRef(ref, options.projects)
358
+ || 'central';
359
+ validateWorkItemLink(workItemId, effectiveProject, options.workItems);
360
+ const contextOnly = hasOwn(body, 'contextOnly')
361
+ ? apiValidation.validateBoolean(body.contextOnly, {
362
+ field: 'contextOnly',
363
+ message: 'contextOnly must be a boolean',
364
+ })
365
+ : undefined;
366
+ const title = optionalString(body, 'title', {
367
+ trim: true,
368
+ maxLength: 120,
369
+ defaultValue: undefined,
370
+ });
371
+ return {
372
+ ...body,
373
+ url,
374
+ ref,
375
+ ...(project ? { project } : {}),
376
+ ...(title !== undefined ? { title } : {}),
377
+ ...(contextOnly !== undefined ? { contextOnly } : {}),
378
+ ...(workItemId ? { workItemId } : {}),
379
+ ...(context !== undefined ? { context } : {}),
380
+ };
381
+ }
382
+
383
+ function validateGitBranch(value) {
384
+ const branch = requiredString(value, 'branch', { maxLength: 256 });
385
+ if (branch === 'HEAD') {
386
+ throw inputError('branch must be a valid non-reserved git branch name', {
387
+ field: 'branch',
388
+ code: 'invalid-git-ref',
389
+ rejectedValue: value,
390
+ });
391
+ }
392
+ try {
393
+ return shared.validateGitRef(branch);
394
+ } catch {
395
+ throw inputError('branch must be a valid git ref', {
396
+ field: 'branch',
397
+ code: 'invalid-git-ref',
398
+ rejectedValue: value,
399
+ });
400
+ }
401
+ }
402
+
403
+ function validateCreatePrRequest(body, options = {}) {
404
+ validateBody(body);
405
+ const kind = options.kind;
406
+ if (!['offer', 'prepare', 'cleanup'].includes(kind)) {
407
+ throw new TypeError(`unknown create-PR request kind: ${kind}`);
408
+ }
409
+ const { name: projectName, project } = resolveProject(body.project, options.projects);
410
+ const result = { projectName, project };
411
+ if (kind === 'offer') {
412
+ result.contextOnly = optionalBoolean(body, 'contextOnly');
413
+ }
414
+ if ((kind === 'offer' || kind === 'prepare') && hasOwn(body, 'branch')) {
415
+ result.branch = validateGitBranch(body.branch);
416
+ }
417
+ if (kind === 'cleanup') {
418
+ const worktreePath = requiredString(body.worktreePath, 'worktreePath', { maxLength: 4096 });
419
+ if (/[\u0000-\u001f\u007f]/.test(worktreePath)) {
420
+ throw inputError('worktreePath must not contain control characters', {
421
+ field: 'worktreePath',
422
+ code: 'invalid-path',
423
+ rejectedValue: worktreePath,
424
+ });
425
+ }
426
+ if (/^(?:\\\\|\/\/)/.test(worktreePath)) {
427
+ throw inputError('worktreePath must not be a network or device path', {
428
+ field: 'worktreePath',
429
+ code: 'invalid-path',
430
+ rejectedValue: worktreePath,
431
+ });
432
+ }
433
+ if (!path.isAbsolute(worktreePath)) {
434
+ throw inputError('worktreePath must be an absolute path', {
435
+ field: 'worktreePath',
436
+ code: 'invalid-path',
437
+ rejectedValue: worktreePath,
438
+ });
439
+ }
440
+ const resolvedPath = path.resolve(worktreePath);
441
+ const projectRoot = shared.resolveProjectRootDir(project.localPath, options.minionsDir);
442
+ const worktreeRoot = path.resolve(
443
+ projectRoot,
444
+ options.engine?.worktreeRoot || shared.ENGINE_DEFAULTS.worktreeRoot,
445
+ );
446
+ if (!shared.isPathInside(resolvedPath, worktreeRoot)) {
447
+ throw inputError('worktreePath must be inside the configured worktree root', {
448
+ field: 'worktreePath',
449
+ code: 'invalid-path',
450
+ rejectedValue: worktreePath,
451
+ });
452
+ }
453
+ result.worktreePath = resolvedPath;
454
+ }
455
+ return result;
456
+ }
457
+
458
+ function validatePrObserveRequest(body) {
459
+ validateBody(body);
460
+ const host = requiredString(body.host, 'host', { maxLength: 16 }).toLowerCase();
461
+ apiValidation.validateEnum(host, {
462
+ field: 'host',
463
+ values: ['github', 'ado'],
464
+ message: 'host must be "github" or "ado"',
465
+ });
466
+ const slug = requiredString(body.slug, 'slug', { maxLength: 512 });
467
+ const number = positiveInteger(body.number, 'number');
468
+ const ref = prResolve.normalizePrRef(`${host}:${slug}#${number}`);
469
+ if (!ref || ref.host !== host) {
470
+ throw inputError(`slug must match the canonical ${host} repository shape`, {
471
+ field: 'slug',
472
+ code: 'invalid-pr-scope',
473
+ rejectedValue: slug,
474
+ });
475
+ }
476
+ const hasContextOnly = hasOwn(body, 'contextOnly') && body.contextOnly !== undefined;
477
+ const hasObserve = hasOwn(body, 'observe') && body.observe !== undefined;
478
+ const contextOnly = hasContextOnly
479
+ ? apiValidation.validateBoolean(body.contextOnly, {
480
+ field: 'contextOnly',
481
+ message: 'contextOnly must be a boolean',
482
+ })
483
+ : undefined;
484
+ const observe = hasObserve
485
+ ? apiValidation.validateBoolean(body.observe, {
486
+ field: 'observe',
487
+ message: 'observe must be a boolean',
488
+ })
489
+ : undefined;
490
+ if (!hasContextOnly && !hasObserve) {
491
+ throw inputError('contextOnly (or legacy observe) must be a boolean', {
492
+ field: 'contextOnly',
493
+ code: 'missing-field',
494
+ });
495
+ }
496
+ return {
497
+ host: ref.host,
498
+ slug: ref.slug,
499
+ number: ref.number,
500
+ contextOnly: hasContextOnly ? contextOnly : !observe,
501
+ };
502
+ }
503
+
504
+ function validatePrDeleteRequest(body, options = {}) {
505
+ validateBody(body);
506
+ const rawId = requiredString(body.id, 'id', { maxLength: MAX_PR_REFERENCE_LENGTH });
507
+ let id;
508
+ if (/^(github|ado):/i.test(rawId)) {
509
+ id = validateCanonicalPrId(rawId, 'id');
510
+ } else {
511
+ const match = rawId.match(/^PR-([0-9]+)$/i) || rawId.match(/^([0-9]+)$/);
512
+ if (!match) {
513
+ throw inputError('id must be a canonical PR id, PR-number alias, or bare number', {
514
+ field: 'id',
515
+ code: 'invalid-pr-reference',
516
+ rejectedValue: rawId,
517
+ });
518
+ }
519
+ id = `PR-${positiveInteger(match[1], 'id')}`;
520
+ }
521
+ let project;
522
+ if (hasOwn(body, 'project') && body.project !== undefined && body.project !== null && body.project !== '') {
523
+ const projectName = requiredString(body.project, 'project', { maxLength: 128 });
524
+ const configured = shared.findProjectByName(
525
+ Array.isArray(options.projects) ? options.projects : [],
526
+ projectName,
527
+ );
528
+ if (!configured && !(options.trackedScopes || []).includes(projectName)) {
529
+ throw inputError(`unknown project: ${projectName}`, {
530
+ statusCode: 404,
531
+ code: 'project-not-found',
532
+ field: 'project',
533
+ rejectedValue: projectName,
534
+ });
535
+ }
536
+ project = configured?.name || projectName;
537
+ }
538
+ return { id, ...(project ? { project } : {}) };
539
+ }
540
+
541
+ function validatePauseClearRequest(body, options = {}) {
542
+ validateBody(body);
543
+ const kind = options.kind;
544
+ const result = { prId: validateCanonicalPrId(body.prId, 'prId') };
545
+ if (kind === 'cause') {
546
+ const cause = requiredString(body.cause, 'cause', { maxLength: 64 });
547
+ const allowed = Object.values(shared.PR_FIX_CAUSE);
548
+ apiValidation.validateEnum(cause, {
549
+ field: 'cause',
550
+ values: allowed,
551
+ message: `cause must be one of: ${allowed.join(', ')}`,
552
+ });
553
+ result.cause = cause;
554
+ } else if (kind === 'build-fix-ineffective') {
555
+ const headSha = optionalString(body, 'headSha', {
556
+ trim: true,
557
+ maxLength: 64,
558
+ defaultValue: '',
559
+ });
560
+ if (headSha && !/^[0-9a-f]{7,64}$/i.test(headSha)) {
561
+ throw inputError('headSha must be a 7-64 character hexadecimal commit id', {
562
+ field: 'headSha',
563
+ code: 'invalid-identifier',
564
+ rejectedValue: headSha,
565
+ });
566
+ }
567
+ result.headSha = headSha;
568
+ } else if (kind !== 'worktree-held') {
569
+ throw new TypeError(`unknown pause-clear request kind: ${kind}`);
570
+ }
571
+ return result;
572
+ }
573
+
574
+ function validateNoInputRequest(body, label = 'request') {
575
+ validateBody(body);
576
+ if (Object.keys(body).length > 0) {
577
+ throw inputError(`${label} does not accept request body fields`, {
578
+ field: 'body',
579
+ code: 'unexpected-input',
580
+ rejectedValue: body,
581
+ });
582
+ }
583
+ return {};
584
+ }
585
+
586
+ function validateIssueLabels(labels) {
587
+ if (labels === undefined || labels === null) return undefined;
588
+ if (typeof labels !== 'string' && !Array.isArray(labels)) {
589
+ throw inputError('labels must be a comma-separated string or an array of strings', {
590
+ field: 'labels',
591
+ code: 'invalid-array',
592
+ rejectedValue: labels,
593
+ });
594
+ }
595
+ const raw = typeof labels === 'string' ? labels.split(',') : labels;
596
+ apiValidation.validateArray(raw, {
597
+ field: 'labels',
598
+ maxLength: MAX_ISSUE_LABELS,
599
+ itemValidator: (entry, context) => {
600
+ const label = apiValidation.validateString(entry, {
601
+ field: 'labels',
602
+ path: context.path,
603
+ trim: true,
604
+ allowEmpty: false,
605
+ maxLength: MAX_ISSUE_LABEL_LENGTH,
606
+ message: 'labels entries must be non-empty strings',
607
+ });
608
+ if (/[\u0000-\u001f\u007f]/.test(label)) {
609
+ throw inputError('labels entries must not contain control characters', {
610
+ field: 'labels',
611
+ path: context.path,
612
+ rejectedValue: label,
613
+ });
614
+ }
615
+ },
616
+ });
617
+ const normalized = issues.normalizeLabels(raw, []);
618
+ return normalized;
619
+ }
620
+
621
+ function validateIssueCreateRequest(body) {
622
+ validateBody(body);
623
+ const title = requiredString(body.title, 'title', { maxLength: 256 });
624
+ if (/[\u0000-\u001f\u007f]/.test(title)) {
625
+ throw inputError('title must not contain control characters', {
626
+ field: 'title',
627
+ code: 'invalid-string',
628
+ rejectedValue: title,
629
+ });
630
+ }
631
+ const description = optionalString(body, 'description', {
632
+ maxLength: MAX_ISSUE_DESCRIPTION_LENGTH,
633
+ defaultValue: '',
634
+ });
635
+ const labels = validateIssueLabels(body.labels);
636
+ return { title, description, labels };
637
+ }
638
+
639
+ module.exports = {
640
+ MAX_ISSUE_LABELS,
641
+ validatePrDetailId,
642
+ validatePrActionRequest,
643
+ validatePrFixRequest,
644
+ validatePrUrlRequest,
645
+ validatePrLinkRequest,
646
+ validateCreatePrRequest,
647
+ validatePrObserveRequest,
648
+ validatePrDeleteRequest,
649
+ validatePauseClearRequest,
650
+ validateNoInputRequest,
651
+ validateIssueCreateRequest,
652
+ extractLinkedWorkItemId,
653
+ };