@yemi33/minions 0.1.2424 → 0.1.2426
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install-internal-minions.js +1209 -0
- package/bin/minions.js +75 -9
- package/dashboard/docs/typography.md +27 -12
- package/dashboard/js/command-center.js +13 -11
- package/dashboard/js/live-stream.js +1 -1
- package/dashboard/js/memory-search.js +388 -43
- package/dashboard/js/modal-qa.js +5 -5
- package/dashboard/js/qa.js +39 -26
- package/dashboard/js/refresh.js +37 -18
- package/dashboard/js/render-agents.js +14 -18
- package/dashboard/js/render-dispatch.js +14 -74
- package/dashboard/js/render-inbox.js +3 -3
- package/dashboard/js/render-meetings.js +8 -8
- package/dashboard/js/render-other.js +45 -20
- package/dashboard/js/render-pipelines.js +6 -6
- package/dashboard/js/render-plans.js +14 -14
- package/dashboard/js/render-prd.js +46 -46
- package/dashboard/js/render-prs.js +9 -65
- package/dashboard/js/render-schedules.js +5 -5
- package/dashboard/js/render-skills.js +6 -6
- package/dashboard/js/render-utils.js +4 -4
- package/dashboard/js/render-watches.js +4 -16
- package/dashboard/js/render-work-items.js +95 -107
- package/dashboard/js/settings.js +175 -74
- package/dashboard/layout.html +3 -2
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/work.html +1 -1
- package/dashboard/shared/model-display.js +9 -0
- package/dashboard/shared/watches-source.js +43 -0
- package/dashboard/shared/welcome-popup.js +259 -0
- package/dashboard/slim/body.html +6 -6
- package/dashboard/slim/js/chat.js +4 -2
- package/dashboard/slim/js/history.js +10 -3
- package/dashboard/slim/js/modals-tiles.js +85 -16
- package/dashboard/slim/js/status.js +12 -16
- package/dashboard/slim/styles.css +24 -4
- package/dashboard/styles.css +62 -27
- package/dashboard-build.js +8 -2
- package/dashboard.js +3854 -2514
- package/docs/README.md +7 -5
- package/docs/api-errors.md +144 -0
- package/docs/auto-discovery.md +83 -61
- package/docs/capture-demos.js +173 -30
- package/docs/command-center.md +5 -1
- package/docs/completion-reports.md +127 -11
- package/docs/constants.md +15 -3
- package/docs/constellation-style-telemetry.md +2 -2
- package/docs/copilot-cli-schema.md +32 -22
- package/docs/cross-repo-plans.md +30 -30
- package/docs/demo/memory-system.html +1 -1
- package/docs/deprecated.json +132 -18
- package/docs/diagnostics-memory.md +8 -4
- package/docs/human-vs-automated.md +2 -2
- package/docs/index.html +9 -2
- package/docs/internal-install.md +212 -0
- package/docs/kb-pr3223-cascade-archiving.md +16 -0
- package/docs/kb-pr696-merge-conflict-docs.md +23 -0
- package/docs/kb-sweep.md +2 -2
- package/docs/keep-processes.md +8 -1
- package/docs/live-checkout-mode.md +1 -1
- package/docs/managed-spawn.md +21 -3
- package/docs/named-agents.md +3 -2
- package/docs/onboarding.md +26 -0
- package/docs/plan-lifecycle.md +32 -32
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/qa-runbook-lifecycle.md +67 -51
- package/docs/qa-runbooks.md +2 -2
- package/docs/runtime-adapters.md +101 -50
- package/docs/security.md +28 -0
- package/docs/self-improvement.md +47 -13
- package/docs/skills.md +29 -0
- package/docs/slim-ux/concepts.md +2 -2
- package/docs/specs/agent-configurability.md +43 -41
- package/docs/specs/agent-rename.md +13 -13
- package/docs/team-memory.md +283 -14
- package/docs/tutorials/01-install-and-connect.md +23 -0
- package/docs/tutorials/03-plan-driven-feature.md +3 -3
- package/docs/tutorials/04-runtimes-and-harness.md +18 -21
- package/docs/tutorials/08-operations-and-recovery.md +3 -2
- package/docs/tutorials/README.md +4 -3
- package/docs/visual-evidence-ci.md +103 -0
- package/docs/watches.md +35 -30
- package/docs/worktree-lifecycle.md +151 -13
- package/engine/acp-transport.js +1 -0
- package/engine/ado.js +80 -33
- package/engine/agent-api-validation.js +571 -0
- package/engine/agent-worker-pool.js +7 -6
- package/engine/api-contracts/agent-content.js +761 -0
- package/engine/api-contracts/cc-ops.js +219 -0
- package/engine/api-contracts/config-runtime.js +419 -0
- package/engine/api-contracts/core.js +138 -0
- package/engine/api-contracts/index.js +506 -0
- package/engine/api-contracts/orchestration.js +602 -0
- package/engine/api-contracts/pull-requests.js +323 -0
- package/engine/api-contracts/qa-process.js +764 -0
- package/engine/api-contracts/work-plan-prd.js +494 -0
- package/engine/api-validation.js +543 -0
- package/engine/cc-api-validation.js +586 -0
- package/engine/cc-worker-pool.js +9 -1
- package/engine/cleanup.js +393 -271
- package/engine/cli.js +65 -7
- package/engine/comment-classifier.js +70 -5
- package/engine/consolidation.js +123 -5
- package/engine/content-api-validation.js +606 -0
- package/engine/create-pr-worktree.js +22 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +162 -0
- package/engine/dispatch.js +33 -0
- package/engine/distribution.js +189 -0
- package/engine/execution-model.js +59 -0
- package/engine/features.js +12 -0
- package/engine/github.js +67 -25
- package/engine/harness-context.js +313 -0
- package/engine/inbox-store.js +1 -1
- package/engine/kb-sweep.js +161 -38
- package/engine/keep-process-sweep.js +43 -3
- package/engine/lifecycle.js +1066 -165
- package/engine/llm.js +4 -0
- package/engine/managed-spawn.js +463 -48
- package/engine/meeting.js +197 -34
- package/engine/memory-retrieval.js +280 -0
- package/engine/memory-store.js +821 -38
- package/engine/model-discovery.js +17 -3
- package/engine/pipeline.js +802 -14
- package/engine/plan-prd-validation.js +745 -0
- package/engine/playbook.js +206 -17
- package/engine/pooled-agent-process.js +6 -1
- package/engine/pr-action.js +1 -1
- package/engine/pr-issue-validation.js +653 -0
- package/engine/pr-resolve.js +75 -13
- package/engine/prd-store.js +39 -6
- package/engine/preflight.js +7 -2
- package/engine/process-utils.js +79 -31
- package/engine/projects.js +198 -12
- package/engine/promotion.js +371 -0
- package/engine/qa-from-prd.js +3 -0
- package/engine/qa-process-validation.js +572 -0
- package/engine/qa-runbooks.js +41 -37
- package/engine/qa-runners/maestro.js +11 -7
- package/engine/qa-runners/playwright.js +37 -14
- package/engine/qa-runners.js +9 -10
- package/engine/qa-runs.js +124 -11
- package/engine/qa-sessions.js +139 -40
- package/engine/quarantine-refs.js +81 -11
- package/engine/queries.js +163 -9
- package/engine/review-learning-backfill.js +482 -0
- package/engine/review-learning.js +1236 -0
- package/engine/runtimes/claude.js +60 -2
- package/engine/runtimes/codex.js +57 -16
- package/engine/runtimes/contract.js +21 -0
- package/engine/runtimes/copilot.js +51 -3
- package/engine/runtimes/index.js +1 -0
- package/engine/scheduler.js +201 -32
- package/engine/settings-validation.js +909 -0
- package/engine/shared.js +705 -208
- package/engine/spawn-agent.js +48 -21
- package/engine/steering-constraints.js +31 -0
- package/engine/steering-store.js +10 -2
- package/engine/steering.js +14 -2
- package/engine/timeout.js +404 -111
- package/engine/untrusted-fence.js +24 -10
- package/engine/watch-actions.js +7 -2
- package/engine/watches.js +353 -51
- package/engine/work-item-validation.js +561 -0
- package/engine/work-items-store.js +61 -1
- package/engine/worktree-gc.js +656 -37
- package/engine/worktree-preflight.js +154 -0
- package/engine.js +2068 -342
- package/package.json +1 -1
- package/playbooks/_pr-description-audit.md +88 -41
- package/playbooks/build-fix-complex.md +3 -3
- package/playbooks/docs.md +2 -2
- package/playbooks/fix.md +18 -18
- package/playbooks/implement.md +18 -18
- package/playbooks/qa-session-draft.md +10 -12
- package/playbooks/qa-session-execute.md +16 -12
- package/playbooks/qa-validate.md +8 -7
- package/playbooks/shared-rules.md +45 -0
- package/playbooks/test.md +4 -0
- package/prompts/cc-system.md +1 -1
- package/skills/check-self-authored-review-comment/SKILL.md +2 -2
|
@@ -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({
|
|
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]);
|