@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,606 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const api = require('./api-validation');
5
+ const shared = require('./shared');
6
+ const memoryStore = require('./memory-store');
7
+
8
+ const LIMITS = Object.freeze({
9
+ title: 200,
10
+ content: 512 * 1024,
11
+ author: 100,
12
+ reason: 64 * 1024,
13
+ basename: 255,
14
+ pinCount: 500,
15
+ pinKey: 512,
16
+ memoryQuery: 500,
17
+ memoryLimit: 100,
18
+ skillDir: 2048,
19
+ skillSource: 200,
20
+ });
21
+ const PIN_LEVELS = Object.freeze(['info', 'warning', 'critical']);
22
+ const DEFAULT_MEMORY_SEARCH_LIMIT = 20;
23
+ const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
24
+ const WINDOWS_INVALID_FILENAME_PATTERN = /[<>:"|?*\x00-\x1f]/;
25
+ const WINDOWS_RESERVED_FILENAME_PATTERN =
26
+ /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9]|conin\$|conout\$)(?:\..*)?$/i;
27
+
28
+ function hasOwn(value, key) {
29
+ return Object.prototype.hasOwnProperty.call(value, key);
30
+ }
31
+
32
+ function reject(message, options = {}) {
33
+ throw new api.ApiInputError(message, {
34
+ code: options.code || 'invalid-input',
35
+ field: options.field,
36
+ path: options.path || (options.field ? ['body', options.field] : undefined),
37
+ expected: options.expected,
38
+ rejectedValue: options.rejectedValue,
39
+ allowedValues: options.allowedValues,
40
+ min: options.min,
41
+ max: options.max,
42
+ minLength: options.minLength,
43
+ maxLength: options.maxLength,
44
+ });
45
+ }
46
+
47
+ function validateBody(body) {
48
+ return api.validatePlainObject(body, { field: 'body', path: 'body' });
49
+ }
50
+
51
+ function validateSingleLine(value, options = {}) {
52
+ const normalized = api.validateString(value, {
53
+ field: options.field,
54
+ path: options.path,
55
+ trim: options.trim !== false,
56
+ allowEmpty: options.allowEmpty === true,
57
+ minLength: options.allowEmpty === true ? 0 : 1,
58
+ maxLength: options.maxLength,
59
+ });
60
+ if (/[\0\r\n]/.test(normalized)) {
61
+ reject(`${options.field || 'value'} must be a single line`, {
62
+ code: 'invalid-string',
63
+ field: options.field,
64
+ path: options.path,
65
+ expected: 'single-line string',
66
+ rejectedValue: value,
67
+ maxLength: options.maxLength,
68
+ });
69
+ }
70
+ return normalized;
71
+ }
72
+
73
+ function validateTitle(value, options = {}) {
74
+ const title = validateSingleLine(value, {
75
+ field: options.field || 'title',
76
+ path: options.path,
77
+ maxLength: LIMITS.title,
78
+ });
79
+ if (/(?:^|[\\/])\.\.(?:$|[\\/])/.test(title)) {
80
+ reject(`${options.field || 'title'} contains an unsafe path segment`, {
81
+ code: 'invalid-title',
82
+ field: options.field || 'title',
83
+ path: options.path,
84
+ expected: 'single-line title without traversal segments',
85
+ rejectedValue: value,
86
+ maxLength: LIMITS.title,
87
+ });
88
+ }
89
+ if (options.requireSlug && !shared.slugify(title, 60)) {
90
+ reject(`${options.field || 'title'} must produce a safe file name`, {
91
+ code: 'invalid-title',
92
+ field: options.field || 'title',
93
+ path: options.path,
94
+ expected: 'title containing letters or numbers',
95
+ rejectedValue: value,
96
+ maxLength: LIMITS.title,
97
+ });
98
+ }
99
+ return title;
100
+ }
101
+
102
+ function validateFreeForm(value, options = {}) {
103
+ if (value === undefined) {
104
+ if (options.required) {
105
+ reject(`${options.field || 'content'} is required`, {
106
+ code: 'invalid-string',
107
+ field: options.field,
108
+ path: options.path,
109
+ expected: 'string',
110
+ rejectedValue: value,
111
+ minLength: 1,
112
+ maxLength: options.maxLength || LIMITS.content,
113
+ });
114
+ }
115
+ return options.defaultValue === undefined ? '' : options.defaultValue;
116
+ }
117
+ const text = api.validateString(value, {
118
+ field: options.field,
119
+ path: options.path,
120
+ maxLength: options.maxLength || LIMITS.content,
121
+ });
122
+ if (options.required && !text.trim()) {
123
+ reject(`${options.field || 'content'} is required`, {
124
+ code: 'invalid-string',
125
+ field: options.field,
126
+ path: options.path,
127
+ expected: 'non-blank string',
128
+ rejectedValue: value,
129
+ minLength: 1,
130
+ maxLength: options.maxLength || LIMITS.content,
131
+ });
132
+ }
133
+ return text;
134
+ }
135
+
136
+ function decodePathValue(value, options = {}) {
137
+ const raw = api.validateString(value, {
138
+ field: options.field,
139
+ path: options.path,
140
+ trim: false,
141
+ allowEmpty: false,
142
+ minLength: 1,
143
+ maxLength: options.maxLength || LIMITS.basename,
144
+ });
145
+ let decoded;
146
+ try {
147
+ decoded = decodeURIComponent(raw);
148
+ } catch {
149
+ reject(`${options.field || 'path'} has invalid percent encoding`, {
150
+ code: 'invalid-path-encoding',
151
+ field: options.field,
152
+ path: options.path,
153
+ expected: 'valid percent-encoded path component',
154
+ rejectedValue: raw,
155
+ maxLength: options.maxLength || LIMITS.basename,
156
+ });
157
+ }
158
+ if (!decoded.trim() || decoded.trim() !== decoded || /[\0\r\n]/.test(decoded)) {
159
+ reject(`${options.field || 'path'} is not a valid path component`, {
160
+ code: 'invalid-path',
161
+ field: options.field,
162
+ path: options.path,
163
+ expected: 'non-blank path component without control characters',
164
+ rejectedValue: raw,
165
+ maxLength: options.maxLength || LIMITS.basename,
166
+ });
167
+ }
168
+ return decoded;
169
+ }
170
+
171
+ function validateSafeBasename(value, options = {}) {
172
+ const field = options.field || 'name';
173
+ const decoded = decodePathValue(value, {
174
+ field,
175
+ path: options.path,
176
+ maxLength: options.maxLength || LIMITS.basename,
177
+ });
178
+ const unsafe = decoded === '.'
179
+ || decoded === '..'
180
+ || decoded.includes('\0')
181
+ || decoded.includes('/')
182
+ || decoded.includes('\\')
183
+ || path.isAbsolute(decoded)
184
+ || path.win32.isAbsolute(decoded)
185
+ || path.basename(decoded) !== decoded
186
+ || path.win32.basename(decoded) !== decoded
187
+ || WINDOWS_INVALID_FILENAME_PATTERN.test(decoded)
188
+ || WINDOWS_RESERVED_FILENAME_PATTERN.test(decoded)
189
+ || /[. ]$/.test(decoded);
190
+ if (unsafe) {
191
+ reject(`${field} must be a safe file name`, {
192
+ code: 'invalid-file-name',
193
+ field,
194
+ path: options.path,
195
+ expected: options.extension ? `safe *${options.extension} basename` : 'safe basename',
196
+ rejectedValue: value,
197
+ maxLength: options.maxLength || LIMITS.basename,
198
+ });
199
+ }
200
+ if (options.extension && !decoded.toLowerCase().endsWith(options.extension.toLowerCase())) {
201
+ reject(`${field} must end with ${options.extension}`, {
202
+ code: 'invalid-file-name',
203
+ field,
204
+ path: options.path,
205
+ expected: `safe *${options.extension} basename`,
206
+ rejectedValue: value,
207
+ maxLength: options.maxLength || LIMITS.basename,
208
+ });
209
+ }
210
+ if (options.baseDir) {
211
+ try {
212
+ const resolved = shared.sanitizePath(decoded, options.baseDir);
213
+ if (path.dirname(resolved) !== path.resolve(options.baseDir)) throw new Error('nested path');
214
+ } catch {
215
+ reject(`${field} must be a safe file name`, {
216
+ code: 'invalid-file-name',
217
+ field,
218
+ path: options.path,
219
+ expected: options.extension ? `safe *${options.extension} basename` : 'safe basename',
220
+ rejectedValue: value,
221
+ maxLength: options.maxLength || LIMITS.basename,
222
+ });
223
+ }
224
+ }
225
+ return decoded;
226
+ }
227
+
228
+ function validateOptionalSingleLine(value, options = {}) {
229
+ if (value === undefined || value === '') return options.defaultValue;
230
+ return validateSingleLine(value, options);
231
+ }
232
+
233
+ function validateNotesCreate(body) {
234
+ validateBody(body);
235
+ const title = validateTitle(body.title);
236
+ const noteContent = {};
237
+ for (const field of ['what', 'content', 'description']) {
238
+ if (hasOwn(body, field)) {
239
+ noteContent[field] = validateFreeForm(body[field], {
240
+ field,
241
+ maxLength: LIMITS.content,
242
+ });
243
+ }
244
+ }
245
+ let what = '';
246
+ for (const field of ['what', 'content', 'description']) {
247
+ if (!hasOwn(noteContent, field)) continue;
248
+ what = noteContent[field];
249
+ break;
250
+ }
251
+ const why = validateFreeForm(body.why, {
252
+ field: 'why',
253
+ maxLength: LIMITS.reason,
254
+ defaultValue: '',
255
+ });
256
+ const author = validateOptionalSingleLine(body.author, {
257
+ field: 'author',
258
+ maxLength: LIMITS.author,
259
+ defaultValue: '',
260
+ });
261
+ return { title, what, why, author };
262
+ }
263
+
264
+ function validateNotesSave(body) {
265
+ validateBody(body);
266
+ if (!hasOwn(body, 'content')) {
267
+ reject('content is required', {
268
+ code: 'invalid-string',
269
+ field: 'content',
270
+ expected: 'string',
271
+ rejectedValue: undefined,
272
+ maxLength: LIMITS.content,
273
+ });
274
+ }
275
+ const content = validateFreeForm(body.content, {
276
+ field: 'content',
277
+ maxLength: LIMITS.content,
278
+ });
279
+ const file = body.file === undefined || body.file === ''
280
+ ? 'notes.md'
281
+ : validateSingleLine(body.file, { field: 'file', maxLength: 32 });
282
+ api.validateEnum(file, {
283
+ field: 'file',
284
+ path: ['body', 'file'],
285
+ values: ['notes.md'],
286
+ });
287
+ return { content, file };
288
+ }
289
+
290
+ function normalizePinLevel(value) {
291
+ if (value === undefined || value === '') return 'info';
292
+ return api.validateEnum(value, {
293
+ field: 'level',
294
+ path: ['body', 'level'],
295
+ values: PIN_LEVELS,
296
+ });
297
+ }
298
+
299
+ function validatePinnedAdd(body) {
300
+ validateBody(body);
301
+ return {
302
+ title: validateTitle(body.title),
303
+ content: validateFreeForm(body.content, {
304
+ field: 'content',
305
+ required: true,
306
+ maxLength: LIMITS.content,
307
+ }),
308
+ level: normalizePinLevel(body.level),
309
+ };
310
+ }
311
+
312
+ function validatePinnedRemove(body) {
313
+ validateBody(body);
314
+ return { title: validateTitle(body.title) };
315
+ }
316
+
317
+ function validatePinnedUpdate(body) {
318
+ validateBody(body);
319
+ return {
320
+ originalTitle: validateTitle(body.originalTitle, { field: 'originalTitle' }),
321
+ title: validateTitle(body.title),
322
+ content: validateFreeForm(body.content, {
323
+ field: 'content',
324
+ required: true,
325
+ maxLength: LIMITS.content,
326
+ }),
327
+ level: normalizePinLevel(body.level),
328
+ };
329
+ }
330
+
331
+ function validatePinKey(value, pathValue) {
332
+ return validateSingleLine(value, {
333
+ field: 'key',
334
+ path: pathValue,
335
+ maxLength: LIMITS.pinKey,
336
+ });
337
+ }
338
+
339
+ function validateKbPinsSet(body) {
340
+ validateBody(body);
341
+ api.validateArray(body.pins, {
342
+ field: 'pins',
343
+ path: ['body', 'pins'],
344
+ maxLength: LIMITS.pinCount,
345
+ });
346
+ const pins = body.pins.map((value, index) =>
347
+ validatePinKey(value, ['body', 'pins', index]));
348
+ return { pins };
349
+ }
350
+
351
+ function validateKbPinToggle(body) {
352
+ validateBody(body);
353
+ return { key: validatePinKey(body.key, ['body', 'key']) };
354
+ }
355
+
356
+ function validateKnowledgeCategory(value, options = {}) {
357
+ const category = options.decode
358
+ ? decodePathValue(value, {
359
+ field: options.field || 'category',
360
+ path: options.path,
361
+ maxLength: 64,
362
+ })
363
+ : api.validateString(value, {
364
+ field: options.field || 'category',
365
+ path: options.path,
366
+ trim: false,
367
+ allowEmpty: false,
368
+ minLength: 1,
369
+ maxLength: 64,
370
+ });
371
+ const values = options.readable ? shared.KB_READABLE_CATEGORIES : shared.KB_CATEGORIES;
372
+ if (!values.includes(category)) {
373
+ reject('invalid category', {
374
+ code: 'invalid-enum',
375
+ field: options.field || 'category',
376
+ path: options.path || ['body', options.field || 'category'],
377
+ expected: 'enum',
378
+ rejectedValue: category,
379
+ allowedValues: values,
380
+ });
381
+ }
382
+ return category;
383
+ }
384
+
385
+ function validateKnowledgeCreate(body) {
386
+ validateBody(body);
387
+ return {
388
+ category: validateKnowledgeCategory(body.category),
389
+ title: validateTitle(body.title, { requireSlug: true }),
390
+ content: validateFreeForm(body.content, {
391
+ field: 'content',
392
+ required: true,
393
+ maxLength: LIMITS.content,
394
+ }),
395
+ };
396
+ }
397
+
398
+ function validateKnowledgeRead(category, file, minionsDir) {
399
+ const decodedCategory = validateKnowledgeCategory(category, {
400
+ decode: true,
401
+ readable: true,
402
+ path: ['path', 'category'],
403
+ });
404
+ const baseDir = path.join(minionsDir, 'knowledge', decodedCategory);
405
+ return {
406
+ category: decodedCategory,
407
+ file: validateSafeBasename(file, {
408
+ field: 'file',
409
+ path: ['path', 'file'],
410
+ extension: '.md',
411
+ baseDir,
412
+ }),
413
+ };
414
+ }
415
+
416
+ function validateInboxAction(body, minionsDir, options = {}) {
417
+ validateBody(body);
418
+ const result = {
419
+ name: validateSafeBasename(body.name, {
420
+ field: 'name',
421
+ path: ['body', 'name'],
422
+ extension: '.md',
423
+ baseDir: path.join(minionsDir, 'notes', 'inbox'),
424
+ }),
425
+ };
426
+ if (options.category) result.category = validateKnowledgeCategory(body.category);
427
+ return result;
428
+ }
429
+
430
+ function validateKnowledgePinKey(value, index, minionsDir) {
431
+ const key = validatePinKey(value, ['body', 'pinnedKeys', index]);
432
+ const parts = key.split('/');
433
+ if (parts.length !== 3 || parts[0] !== 'knowledge') {
434
+ reject('pinnedKeys entries must identify knowledge/<category>/<file>', {
435
+ code: 'invalid-pin-key',
436
+ field: 'pinnedKeys',
437
+ path: ['body', 'pinnedKeys', index],
438
+ expected: 'knowledge/<category>/<file>.md',
439
+ rejectedValue: value,
440
+ maxLength: LIMITS.pinKey,
441
+ });
442
+ }
443
+ const category = validateKnowledgeCategory(parts[1], {
444
+ decode: true,
445
+ readable: true,
446
+ field: 'pinnedKeys',
447
+ path: ['body', 'pinnedKeys', index],
448
+ });
449
+ const file = validateSafeBasename(parts[2], {
450
+ field: 'pinnedKeys',
451
+ path: ['body', 'pinnedKeys', index],
452
+ extension: '.md',
453
+ baseDir: path.join(minionsDir, 'knowledge', category),
454
+ });
455
+ return `knowledge/${category}/${file}`;
456
+ }
457
+
458
+ function validateKnowledgeSweep(body, minionsDir) {
459
+ validateBody(body);
460
+ let pinnedKeys;
461
+ if (body.pinnedKeys !== undefined) {
462
+ api.validateArray(body.pinnedKeys, {
463
+ field: 'pinnedKeys',
464
+ path: ['body', 'pinnedKeys'],
465
+ maxLength: LIMITS.pinCount,
466
+ });
467
+ pinnedKeys = body.pinnedKeys.map((value, index) =>
468
+ validateKnowledgePinKey(value, index, minionsDir));
469
+ }
470
+ const dryRun = body.dryRun === undefined
471
+ ? undefined
472
+ : api.validateBoolean(body.dryRun, { field: 'dryRun', path: ['body', 'dryRun'] });
473
+ return { pinnedKeys, dryRun };
474
+ }
475
+
476
+ function validateMemorySearch(searchParams) {
477
+ const query = validateFreeForm(searchParams.get('q'), {
478
+ field: 'q',
479
+ path: ['query', 'q'],
480
+ required: true,
481
+ maxLength: LIMITS.memoryQuery,
482
+ }).trim();
483
+ if (!query) {
484
+ reject('q is required', {
485
+ code: 'invalid-string',
486
+ field: 'q',
487
+ path: ['query', 'q'],
488
+ expected: 'non-blank string',
489
+ rejectedValue: searchParams.get('q'),
490
+ minLength: 1,
491
+ maxLength: LIMITS.memoryQuery,
492
+ });
493
+ }
494
+ const statusValue = searchParams.get('status') || 'active';
495
+ const status = api.validateEnum(statusValue, {
496
+ field: 'status',
497
+ path: ['query', 'status'],
498
+ values: [...memoryStore.STATUSES],
499
+ });
500
+ const rawLimit = searchParams.get('limit');
501
+ let limit = DEFAULT_MEMORY_SEARCH_LIMIT;
502
+ if (rawLimit !== null && rawLimit !== '') {
503
+ if (!/^\d+$/.test(rawLimit)) {
504
+ reject('limit must be an integer', {
505
+ code: 'invalid-integer',
506
+ field: 'limit',
507
+ path: ['query', 'limit'],
508
+ expected: 'integer',
509
+ rejectedValue: rawLimit,
510
+ min: 1,
511
+ max: LIMITS.memoryLimit,
512
+ });
513
+ }
514
+ limit = api.validateInteger(Number(rawLimit), {
515
+ field: 'limit',
516
+ path: ['query', 'limit'],
517
+ });
518
+ api.validateRange(limit, {
519
+ field: 'limit',
520
+ path: ['query', 'limit'],
521
+ min: 1,
522
+ max: LIMITS.memoryLimit,
523
+ });
524
+ }
525
+ return { query, status, limit };
526
+ }
527
+
528
+ function validateMemoryAction(idValue, body) {
529
+ const decodedId = decodePathValue(idValue, {
530
+ field: 'id',
531
+ path: ['path', 'id'],
532
+ maxLength: 128,
533
+ });
534
+ const id = api.validateIdentifier(decodedId, {
535
+ field: 'id',
536
+ path: ['path', 'id'],
537
+ maxLength: 128,
538
+ pattern: IDENTIFIER_PATTERN,
539
+ });
540
+ validateBody(body);
541
+ const action = api.validateEnum(body.action, {
542
+ field: 'action',
543
+ path: ['body', 'action'],
544
+ values: [...memoryStore.ACTIONS],
545
+ });
546
+ return { id, action };
547
+ }
548
+
549
+ function validateSkillQuery(searchParams) {
550
+ const file = validateSafeBasename(searchParams.get('file'), {
551
+ field: 'file',
552
+ path: ['query', 'file'],
553
+ });
554
+ const dirValue = searchParams.get('dir');
555
+ const sourceValue = searchParams.get('source');
556
+ const dir = dirValue === null || dirValue === ''
557
+ ? null
558
+ : validateSkillDir(dirValue);
559
+ const source = sourceValue === null || sourceValue === ''
560
+ ? ''
561
+ : validateSingleLine(sourceValue, {
562
+ field: 'source',
563
+ path: ['query', 'source'],
564
+ maxLength: LIMITS.skillSource,
565
+ });
566
+ return { file, dir, source };
567
+ }
568
+
569
+ function validateSkillDir(value) {
570
+ const decoded = decodePathValue(value, {
571
+ field: 'dir',
572
+ path: ['query', 'dir'],
573
+ maxLength: LIMITS.skillDir,
574
+ });
575
+ const segments = decoded.replace(/\\/g, '/').split('/');
576
+ if (segments.includes('..')) {
577
+ reject('dir must not contain traversal segments', {
578
+ code: 'invalid-path',
579
+ field: 'dir',
580
+ path: ['query', 'dir'],
581
+ expected: 'path without traversal segments',
582
+ rejectedValue: value,
583
+ maxLength: LIMITS.skillDir,
584
+ });
585
+ }
586
+ return decoded;
587
+ }
588
+
589
+ module.exports = {
590
+ LIMITS,
591
+ PIN_LEVELS,
592
+ validateNotesCreate,
593
+ validateNotesSave,
594
+ validatePinnedAdd,
595
+ validatePinnedRemove,
596
+ validatePinnedUpdate,
597
+ validateKbPinsSet,
598
+ validateKbPinToggle,
599
+ validateKnowledgeCreate,
600
+ validateKnowledgeRead,
601
+ validateInboxAction,
602
+ validateKnowledgeSweep,
603
+ validateMemorySearch,
604
+ validateMemoryAction,
605
+ validateSkillQuery,
606
+ };
@@ -157,9 +157,12 @@ async function prepareCreatePrWorktree({
157
157
  * touch a path that doesn't carry the minions ownership marker (so a bad/forged
158
158
  * path can never delete an arbitrary directory).
159
159
  */
160
- async function cleanupCreatePrWorktree({ worktreePath, project, projects, _git, _fs } = {}) {
160
+ async function cleanupCreatePrWorktree({
161
+ worktreePath, project, projects, _git, _fs, _isWorktreePathLive,
162
+ } = {}) {
161
163
  const git = _git || ((args, opts) => shared.shellSafeGit(args, opts));
162
164
  const fsm = _fs || fs;
165
+ const isWorktreePathLive = _isWorktreePathLive || shared.isWorktreePathLive;
163
166
  if (!worktreePath || typeof worktreePath !== 'string') {
164
167
  throw new Error('cleanupCreatePrWorktree: worktreePath required');
165
168
  }
@@ -178,6 +181,24 @@ async function cleanupCreatePrWorktree({ worktreePath, project, projects, _git,
178
181
  } catch {
179
182
  return { ok: false, reason: 'protected-project-overlap' };
180
183
  }
184
+ let marker;
185
+ try {
186
+ marker = JSON.parse(fsm.readFileSync(
187
+ path.join(path.resolve(worktreePath), shared.WORKTREE_OWNER_MARKER),
188
+ 'utf8',
189
+ ));
190
+ } catch {
191
+ return { ok: false, reason: 'invalid-ownership-marker' };
192
+ }
193
+ if (!marker || marker.engine !== true || marker.source !== 'cc-create-pr') {
194
+ return { ok: false, reason: 'not-create-pr-worktree' };
195
+ }
196
+ if (project?.name && marker.project !== project.name) {
197
+ return { ok: false, reason: 'project-ownership-mismatch' };
198
+ }
199
+ if (isWorktreePathLive(worktreePath)) {
200
+ return { ok: false, reason: 'worktree-live' };
201
+ }
181
202
  const fromDir = (project && project.localPath) || worktreePath;
182
203
  try {
183
204
  await git(['-C', fromDir, 'worktree', 'remove', '--force', worktreePath]);