@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,586 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const shared = require('./shared');
6
+ const apiValidation = require('./api-validation');
7
+
8
+ const CC_API_LIMITS = Object.freeze({
9
+ messageMaxChars: 100000,
10
+ documentMaxChars: 750000,
11
+ titleMaxChars: 512,
12
+ selectionMaxChars: 65536,
13
+ filePathMaxChars: 1024,
14
+ tabIdMaxChars: 96,
15
+ sessionIdMaxChars: 512,
16
+ transcriptMaxEntries: 20,
17
+ transcriptTextMaxChars: 65536,
18
+ modelMaxChars: 128,
19
+ projectNameMaxChars: 128,
20
+ watchIdMaxChars: 128,
21
+ metadataMaxBytes: 16384,
22
+ metadataMaxKeys: 32,
23
+ contentHashMaxChars: 64,
24
+ presenceUrlMaxChars: 512,
25
+ visibilityMaxChars: 32,
26
+ imageFilenameMaxChars: 255,
27
+ imageMaxCount: 4,
28
+ imageMaxDecodedBytes: 5 * 1024 * 1024,
29
+ });
30
+
31
+ const TRANSCRIPT_ROLES = Object.freeze(['user', 'assistant', 'action', 'system']);
32
+ const TRIAGE_EFFORTS = Object.freeze(['low', 'medium', 'high']);
33
+ const PRESENCE_VISIBILITIES = Object.freeze(['', 'visible', 'hidden', 'prerender', 'unloaded']);
34
+ const CC_IMAGE_MIME_TYPES = Object.freeze(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
35
+ const TAB_ID_PATTERN = /^[A-Za-z0-9._:-]+$/;
36
+ const DOC_CHAT_ALLOWED_EXACT_PATHS = new Set(['notes.md', 'pinned.md', 'routing.md']);
37
+ const DOC_CHAT_ALLOWED_PREFIXES = Object.freeze([
38
+ 'knowledge/',
39
+ 'meetings/',
40
+ 'notes/',
41
+ 'pipelines/',
42
+ 'plans/',
43
+ 'playbooks/',
44
+ 'prd/',
45
+ 'projects/',
46
+ 'runbooks/',
47
+ 'skills/',
48
+ ]);
49
+
50
+ function hasOwn(value, key) {
51
+ return Object.prototype.hasOwnProperty.call(value, key);
52
+ }
53
+
54
+ function throwConstraint(message, {
55
+ field,
56
+ path: detailPath,
57
+ rejectedValue,
58
+ expected,
59
+ code = 'invalid-constraint',
60
+ extra = {},
61
+ } = {}) {
62
+ throw new apiValidation.ApiInputError(message, {
63
+ code,
64
+ field,
65
+ path: detailPath,
66
+ rejectedValue,
67
+ expected,
68
+ ...extra,
69
+ });
70
+ }
71
+
72
+ function validateBody(body) {
73
+ return apiValidation.validatePlainObject(body, {
74
+ field: 'body',
75
+ path: ['body'],
76
+ });
77
+ }
78
+
79
+ function validateNonBlankString(value, {
80
+ field,
81
+ path: detailPath,
82
+ maxLength,
83
+ optional = false,
84
+ nullable = false,
85
+ } = {}) {
86
+ const validated = apiValidation.validateString(value, {
87
+ field,
88
+ path: detailPath,
89
+ minLength: 1,
90
+ maxLength,
91
+ optional,
92
+ nullable,
93
+ });
94
+ if (validated == null) return validated;
95
+ if (!validated.trim()) {
96
+ throwConstraint(`${field} must not be blank`, {
97
+ field,
98
+ path: detailPath,
99
+ rejectedValue: value,
100
+ expected: 'non-blank string',
101
+ });
102
+ }
103
+ return validated;
104
+ }
105
+
106
+ function validateOptionalString(value, {
107
+ field,
108
+ path: detailPath,
109
+ maxLength,
110
+ nullable = true,
111
+ } = {}) {
112
+ return apiValidation.validateString(value, {
113
+ field,
114
+ path: detailPath,
115
+ maxLength,
116
+ optional: true,
117
+ nullable,
118
+ });
119
+ }
120
+
121
+ function validateMessage(value) {
122
+ if (typeof value !== 'string' || !value.trim()) {
123
+ throwConstraint('message required', {
124
+ field: 'message',
125
+ path: ['body', 'message'],
126
+ rejectedValue: value,
127
+ expected: 'non-blank string',
128
+ });
129
+ }
130
+ return validateNonBlankString(value, {
131
+ field: 'message',
132
+ path: ['body', 'message'],
133
+ maxLength: CC_API_LIMITS.messageMaxChars,
134
+ });
135
+ }
136
+
137
+ function validateTabId(value, { required = false, path: detailPath = ['body', 'tabId'] } = {}) {
138
+ if (!required && (value === undefined || value === null)) return value;
139
+ return apiValidation.validateIdentifier(value, {
140
+ field: 'tabId',
141
+ path: detailPath,
142
+ maxLength: CC_API_LIMITS.tabIdMaxChars,
143
+ pattern: TAB_ID_PATTERN,
144
+ });
145
+ }
146
+
147
+ function validateSessionId(value) {
148
+ const sessionId = validateOptionalString(value, {
149
+ field: 'sessionId',
150
+ path: ['body', 'sessionId'],
151
+ maxLength: CC_API_LIMITS.sessionIdMaxChars,
152
+ });
153
+ if (sessionId != null && (!sessionId || /[\u0000-\u001f\u007f]/.test(sessionId))) {
154
+ throwConstraint('sessionId contains invalid characters', {
155
+ field: 'sessionId',
156
+ path: ['body', 'sessionId'],
157
+ rejectedValue: value,
158
+ expected: 'non-empty string without control characters',
159
+ });
160
+ }
161
+ return sessionId;
162
+ }
163
+
164
+ function validateTranscript(transcript) {
165
+ if (transcript === undefined || transcript === null) return transcript;
166
+ apiValidation.validateArray(transcript, {
167
+ field: 'transcript',
168
+ path: ['body', 'transcript'],
169
+ maxLength: CC_API_LIMITS.transcriptMaxEntries,
170
+ });
171
+ transcript.forEach((entry, index) => {
172
+ const entryPath = ['body', 'transcript', index];
173
+ apiValidation.validatePlainObject(entry, {
174
+ field: 'transcript',
175
+ path: entryPath,
176
+ });
177
+ apiValidation.validateEnum(entry.role, {
178
+ field: 'role',
179
+ path: [...entryPath, 'role'],
180
+ values: TRANSCRIPT_ROLES,
181
+ });
182
+ validateNonBlankString(entry.text, {
183
+ field: 'text',
184
+ path: [...entryPath, 'text'],
185
+ maxLength: CC_API_LIMITS.transcriptTextMaxChars,
186
+ });
187
+ });
188
+ return transcript;
189
+ }
190
+
191
+ function validateIntentMetadata(value) {
192
+ if (value === undefined || value === null) return value;
193
+ apiValidation.validatePlainObject(value, {
194
+ field: 'intentMetadata',
195
+ path: ['body', 'intentMetadata'],
196
+ });
197
+ const keys = Object.keys(value);
198
+ let encoded;
199
+ try {
200
+ encoded = JSON.stringify(value);
201
+ } catch {
202
+ throwConstraint('intentMetadata must be JSON-serializable', {
203
+ field: 'intentMetadata',
204
+ path: ['body', 'intentMetadata'],
205
+ rejectedValue: value,
206
+ expected: 'JSON-serializable plain object',
207
+ });
208
+ }
209
+ if (keys.length > CC_API_LIMITS.metadataMaxKeys
210
+ || Buffer.byteLength(encoded || '', 'utf8') > CC_API_LIMITS.metadataMaxBytes) {
211
+ throwConstraint('intentMetadata is too large', {
212
+ field: 'intentMetadata',
213
+ path: ['body', 'intentMetadata'],
214
+ rejectedValue: value,
215
+ expected: `plain object with at most ${CC_API_LIMITS.metadataMaxKeys} keys and ${CC_API_LIMITS.metadataMaxBytes} encoded bytes`,
216
+ extra: { max: CC_API_LIMITS.metadataMaxBytes },
217
+ });
218
+ }
219
+ return value;
220
+ }
221
+
222
+ function validateProjectContext(body, projectNames) {
223
+ const currentProject = validateOptionalString(body.currentProject, {
224
+ field: 'currentProject',
225
+ path: ['body', 'currentProject'],
226
+ maxLength: CC_API_LIMITS.projectNameMaxChars,
227
+ });
228
+ const noProjectSelected = apiValidation.validateBoolean(body.noProjectSelected, {
229
+ field: 'noProjectSelected',
230
+ path: ['body', 'noProjectSelected'],
231
+ optional: true,
232
+ });
233
+ if (currentProject != null) {
234
+ const allowed = new Set(Array.isArray(projectNames) ? projectNames.filter(Boolean) : []);
235
+ if (!allowed.has(currentProject)) {
236
+ throwConstraint('currentProject is not configured', {
237
+ field: 'currentProject',
238
+ path: ['body', 'currentProject'],
239
+ rejectedValue: currentProject,
240
+ expected: 'configured project name',
241
+ extra: { allowedValues: [...allowed] },
242
+ });
243
+ }
244
+ }
245
+ if (currentProject && noProjectSelected === true) {
246
+ throwConstraint('currentProject and noProjectSelected cannot both be set', {
247
+ field: 'noProjectSelected',
248
+ path: ['body', 'noProjectSelected'],
249
+ rejectedValue: noProjectSelected,
250
+ expected: 'false or omitted when currentProject is present',
251
+ });
252
+ }
253
+ }
254
+
255
+ function validateCommandCenterRequest(body, { streaming = false, projectNames = [] } = {}) {
256
+ validateBody(body);
257
+ const reconnect = apiValidation.validateBoolean(body.reconnect, {
258
+ field: 'reconnect',
259
+ path: ['body', 'reconnect'],
260
+ optional: true,
261
+ });
262
+ const tabId = validateTabId(body.tabId);
263
+ const sessionId = validateSessionId(body.sessionId);
264
+
265
+ if (reconnect === true) {
266
+ if (!streaming) {
267
+ throwConstraint('reconnect is only supported by the streaming endpoint', {
268
+ field: 'reconnect',
269
+ path: ['body', 'reconnect'],
270
+ rejectedValue: reconnect,
271
+ expected: 'omitted for non-streaming requests',
272
+ });
273
+ }
274
+ if (!tabId) {
275
+ throwConstraint('reconnect requires tabId', {
276
+ field: 'reconnect',
277
+ path: ['body', 'reconnect'],
278
+ rejectedValue: reconnect,
279
+ expected: 'reconnect with a valid tabId',
280
+ });
281
+ }
282
+ const conflictingFields = [
283
+ 'message',
284
+ 'transcript',
285
+ 'images',
286
+ 'intentMetadata',
287
+ 'currentProject',
288
+ 'noProjectSelected',
289
+ ].filter(field => hasOwn(body, field) && body[field] !== undefined && body[field] !== null);
290
+ if (conflictingFields.length) {
291
+ throwConstraint('reconnect cannot include a new turn payload', {
292
+ field: 'reconnect',
293
+ path: ['body', 'reconnect'],
294
+ rejectedValue: reconnect,
295
+ expected: 'reconnect with only tabId and optional sessionId',
296
+ });
297
+ }
298
+ return body;
299
+ }
300
+
301
+ validateMessage(body.message);
302
+ validateTranscript(body.transcript);
303
+ validateIntentMetadata(body.intentMetadata);
304
+ validateProjectContext(body, projectNames);
305
+ if (sessionId && !tabId) {
306
+ throwConstraint('sessionId requires tabId', {
307
+ field: 'sessionId',
308
+ path: ['body', 'sessionId'],
309
+ rejectedValue: sessionId,
310
+ expected: 'sessionId paired with a valid tabId',
311
+ });
312
+ }
313
+ return body;
314
+ }
315
+
316
+ function validateTriageRequest(body) {
317
+ validateBody(body);
318
+ validateMessage(body.message);
319
+ validateOptionalString(body.watchId, {
320
+ field: 'watchId',
321
+ path: ['body', 'watchId'],
322
+ maxLength: CC_API_LIMITS.watchIdMaxChars,
323
+ });
324
+ apiValidation.validateEnum(body.effort, {
325
+ field: 'effort',
326
+ path: ['body', 'effort'],
327
+ values: TRIAGE_EFFORTS,
328
+ optional: true,
329
+ });
330
+ return body;
331
+ }
332
+
333
+ function validateAbortRequest(body) {
334
+ validateBody(body);
335
+ validateTabId(body.tabId);
336
+ return body;
337
+ }
338
+
339
+ function validateCcWarmRequest(body) {
340
+ validateBody(body);
341
+ validateTabId(body.tabId, { required: true });
342
+ return body;
343
+ }
344
+
345
+ function validateCcSessionPathId(id) {
346
+ return validateTabId(id, { required: true, path: ['path', 'id'] });
347
+ }
348
+
349
+ function realPath(targetPath) {
350
+ return fs.realpathSync.native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath);
351
+ }
352
+
353
+ function resolveRealTarget(fullPath) {
354
+ if (fs.existsSync(fullPath)) return realPath(fullPath);
355
+ let ancestor = path.dirname(fullPath);
356
+ while (!fs.existsSync(ancestor)) {
357
+ const parent = path.dirname(ancestor);
358
+ if (parent === ancestor) break;
359
+ ancestor = parent;
360
+ }
361
+ const realAncestor = realPath(ancestor);
362
+ return path.resolve(realAncestor, path.relative(ancestor, fullPath));
363
+ }
364
+
365
+ function unsafePath(filePath, message) {
366
+ throwConstraint(message || 'filePath is outside the allowed document sandbox', {
367
+ field: 'filePath',
368
+ path: ['body', 'filePath'],
369
+ rejectedValue: filePath,
370
+ expected: 'relative file path under the Minions directory outside protected runtime files',
371
+ code: 'unsafe-path',
372
+ });
373
+ }
374
+
375
+ function rejectSymlinkSegments(fullPath, rootPath, filePath) {
376
+ const root = path.resolve(rootPath);
377
+ let current = fullPath;
378
+ while (current !== root) {
379
+ if (fs.existsSync(current)) {
380
+ let stat;
381
+ try { stat = fs.lstatSync(current); } catch { unsafePath(filePath); }
382
+ if (stat.isSymbolicLink()) {
383
+ unsafePath(filePath, 'filePath cannot traverse symbolic links or junctions');
384
+ }
385
+ }
386
+ const parent = path.dirname(current);
387
+ if (parent === current) unsafePath(filePath);
388
+ current = parent;
389
+ }
390
+ }
391
+
392
+ function resolveDocChatFilePath(filePath, minionsDir) {
393
+ if (filePath === undefined || filePath === null) return null;
394
+ validateNonBlankString(filePath, {
395
+ field: 'filePath',
396
+ path: ['body', 'filePath'],
397
+ maxLength: CC_API_LIMITS.filePathMaxChars,
398
+ });
399
+ let fullPath;
400
+ try {
401
+ fullPath = shared.sanitizePath(filePath, minionsDir);
402
+ } catch {
403
+ unsafePath(filePath);
404
+ }
405
+ const relativePath = path.relative(path.resolve(minionsDir), fullPath).replace(/\\/g, '/');
406
+ if (!DOC_CHAT_ALLOWED_EXACT_PATHS.has(relativePath)
407
+ && !DOC_CHAT_ALLOWED_PREFIXES.some(prefix => relativePath.startsWith(prefix))) {
408
+ unsafePath(filePath, 'filePath is outside an allowed Minions document context');
409
+ }
410
+ rejectSymlinkSegments(fullPath, minionsDir, filePath);
411
+ let realRoot;
412
+ let realTarget;
413
+ try {
414
+ realRoot = realPath(minionsDir);
415
+ realTarget = resolveRealTarget(fullPath);
416
+ } catch {
417
+ unsafePath(filePath);
418
+ }
419
+ if (!shared.isPathInsideOrEqual(realTarget, realRoot)) unsafePath(filePath);
420
+ if (shared.isLiveCommandCenterPath(fullPath, { liveRoot: minionsDir })
421
+ || shared.isLiveCommandCenterPath(realTarget, { liveRoot: realRoot })) {
422
+ unsafePath(filePath, 'filePath targets a protected live Minions runtime file');
423
+ }
424
+ if (fs.existsSync(fullPath)) {
425
+ let stat;
426
+ try { stat = fs.statSync(fullPath); } catch { unsafePath(filePath); }
427
+ if (!stat.isFile()) unsafePath(filePath, 'filePath must identify a file');
428
+ }
429
+ return fullPath;
430
+ }
431
+
432
+ function contentFingerprint(value) {
433
+ if (!value) return '';
434
+ const middle = Math.floor(value.length / 2);
435
+ return `${value.length}:${value.charCodeAt(0)}:${value.charCodeAt(middle)}:${value.charCodeAt(value.length - 1)}`;
436
+ }
437
+
438
+ function validateContentHash(value, document) {
439
+ if (value === undefined || value === null) return value;
440
+ const contentHash = apiValidation.validateString(value, {
441
+ field: 'contentHash',
442
+ path: ['body', 'contentHash'],
443
+ minLength: 1,
444
+ maxLength: CC_API_LIMITS.contentHashMaxChars,
445
+ });
446
+ if (!/^\d+:\d+:\d+:\d+$/.test(contentHash) || contentHash !== contentFingerprint(document)) {
447
+ throwConstraint('contentHash does not match document', {
448
+ field: 'contentHash',
449
+ path: ['body', 'contentHash'],
450
+ rejectedValue: contentHash,
451
+ expected: 'fingerprint of body.document',
452
+ });
453
+ }
454
+ return contentHash;
455
+ }
456
+
457
+ function validateDocChatRequest(body, { minionsDir } = {}) {
458
+ validateBody(body);
459
+ validateMessage(body.message);
460
+ if (typeof body.document !== 'string' || body.document.length === 0) {
461
+ throwConstraint('document required', {
462
+ field: 'document',
463
+ path: ['body', 'document'],
464
+ rejectedValue: body.document,
465
+ expected: 'non-empty string',
466
+ });
467
+ }
468
+ const document = apiValidation.validateString(body.document, {
469
+ field: 'document',
470
+ path: ['body', 'document'],
471
+ minLength: 1,
472
+ maxLength: CC_API_LIMITS.documentMaxChars,
473
+ });
474
+ validateOptionalString(body.title, {
475
+ field: 'title',
476
+ path: ['body', 'title'],
477
+ maxLength: CC_API_LIMITS.titleMaxChars,
478
+ });
479
+ validateOptionalString(body.selection, {
480
+ field: 'selection',
481
+ path: ['body', 'selection'],
482
+ maxLength: CC_API_LIMITS.selectionMaxChars,
483
+ });
484
+ validateOptionalString(body.model, {
485
+ field: 'model',
486
+ path: ['body', 'model'],
487
+ maxLength: CC_API_LIMITS.modelMaxChars,
488
+ });
489
+ apiValidation.validateBoolean(body.freshSession, {
490
+ field: 'freshSession',
491
+ path: ['body', 'freshSession'],
492
+ optional: true,
493
+ });
494
+ validateTranscript(body.transcript);
495
+ validateContentHash(body.contentHash, document);
496
+ if (hasOwn(body, 'images') && body.images != null) {
497
+ throwConstraint('images are not supported by doc-chat', {
498
+ field: 'images',
499
+ path: ['body', 'images'],
500
+ rejectedValue: body.images,
501
+ expected: 'omitted',
502
+ });
503
+ }
504
+ return {
505
+ body,
506
+ fullPath: resolveDocChatFilePath(body.filePath, minionsDir),
507
+ };
508
+ }
509
+
510
+ function validateDocChatWarmRequest(body, { minionsDir } = {}) {
511
+ validateBody(body);
512
+ const title = validateOptionalString(body.title, {
513
+ field: 'title',
514
+ path: ['body', 'title'],
515
+ maxLength: CC_API_LIMITS.titleMaxChars,
516
+ });
517
+ // The read-only doc-chat modal sends `{ filePath: _modalFilePath || '', title }`,
518
+ // so an empty-string filePath means "not provided" and the `title` fallback
519
+ // should warm the session. Treat a blank optional filePath as absent instead
520
+ // of rejecting it as invalid-string.
521
+ const filePathInput = (typeof body.filePath === 'string' && body.filePath.trim() === '')
522
+ ? undefined
523
+ : body.filePath;
524
+ const fullPath = resolveDocChatFilePath(filePathInput, minionsDir);
525
+ if (!fullPath && !title) {
526
+ throwConstraint('filePath or title is required', {
527
+ field: 'filePath',
528
+ path: ['body', 'filePath'],
529
+ rejectedValue: body.filePath,
530
+ expected: 'filePath or title',
531
+ });
532
+ }
533
+ return { body, fullPath };
534
+ }
535
+
536
+ function validateBrowserPresenceRequest(body) {
537
+ validateBody(body);
538
+ validateTabId(body.tabId, { required: true });
539
+ apiValidation.validateBoolean(body.closed, {
540
+ field: 'closed',
541
+ path: ['body', 'closed'],
542
+ optional: true,
543
+ });
544
+ validateOptionalString(body.url, {
545
+ field: 'url',
546
+ path: ['body', 'url'],
547
+ maxLength: CC_API_LIMITS.presenceUrlMaxChars,
548
+ nullable: false,
549
+ });
550
+ apiValidation.validateEnum(body.visibility, {
551
+ field: 'visibility',
552
+ path: ['body', 'visibility'],
553
+ values: PRESENCE_VISIBILITIES,
554
+ optional: true,
555
+ });
556
+ return body;
557
+ }
558
+
559
+ function validateNoRequestBody(body) {
560
+ if (body === undefined) return;
561
+ throwConstraint('This endpoint does not accept a request body', {
562
+ field: 'body',
563
+ path: ['body'],
564
+ rejectedValue: body,
565
+ expected: 'no request body',
566
+ });
567
+ }
568
+
569
+ module.exports = {
570
+ CC_API_LIMITS,
571
+ TRANSCRIPT_ROLES,
572
+ TRIAGE_EFFORTS,
573
+ PRESENCE_VISIBILITIES,
574
+ CC_IMAGE_MIME_TYPES,
575
+ validateCommandCenterRequest,
576
+ validateTriageRequest,
577
+ validateAbortRequest,
578
+ validateCcWarmRequest,
579
+ validateCcSessionPathId,
580
+ validateDocChatRequest,
581
+ validateDocChatWarmRequest,
582
+ validateBrowserPresenceRequest,
583
+ validateNoRequestBody,
584
+ resolveDocChatFilePath,
585
+ contentFingerprint,
586
+ };
@@ -286,6 +286,8 @@ async function getSession({ runtime, tabId, model, effort, mcpServers, systemPro
286
286
  return {
287
287
  sessionId: worker.sessionId,
288
288
  lifecycle,
289
+ runtimeName: worker.runtime.name,
290
+ get currentModel() { return worker.currentModel || null; },
289
291
  stream: (promptText, opts) => worker.stream(promptText, opts),
290
292
  cancel: () => worker.cancel(),
291
293
  close: () => closeTab(tabId),
@@ -328,7 +330,13 @@ async function warmTab(args = {}) {
328
330
  _warmQueue.push(async () => {
329
331
  try {
330
332
  const handle = await getSession(args);
331
- resolve({ tabId: args.tabId, lifecycle: handle.lifecycle, sessionId: handle.sessionId });
333
+ resolve({
334
+ tabId: args.tabId,
335
+ lifecycle: handle.lifecycle,
336
+ sessionId: handle.sessionId,
337
+ runtimeName: handle.runtimeName,
338
+ currentModel: handle.currentModel,
339
+ });
332
340
  } catch (err) {
333
341
  reject(err);
334
342
  }