@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,571 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const apiValidation = require('./api-validation');
|
|
4
|
+
const steeringConstraints = require('./steering-constraints');
|
|
5
|
+
const steeringStore = require('./steering-store');
|
|
6
|
+
|
|
7
|
+
const AGENT_ID_MAX_LENGTH = 128;
|
|
8
|
+
const DISPATCH_ID_MAX_LENGTH = 128;
|
|
9
|
+
const AGENT_CHARTER_MAX_LENGTH = 256 * 1024;
|
|
10
|
+
const STEERING_MESSAGE_MAX_LENGTH = 64 * 1024;
|
|
11
|
+
const CANCEL_TASK_MAX_LENGTH = 512;
|
|
12
|
+
const LIVE_TAIL_MAX_BYTES = 64 * 1024;
|
|
13
|
+
const LIVE_RANGE_MAX_BYTES = 512 * 1024;
|
|
14
|
+
const STEERING_LIST_MAX_LIMIT = 200;
|
|
15
|
+
const OUTPUT_FILE_MAX_LENGTH = 512;
|
|
16
|
+
|
|
17
|
+
const AGENT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
18
|
+
const DISPATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/;
|
|
19
|
+
const STATIC_OUTPUT_FILENAMES = new Set([
|
|
20
|
+
'output.log',
|
|
21
|
+
'live-output.log',
|
|
22
|
+
'live-output-prev.log',
|
|
23
|
+
]);
|
|
24
|
+
const OUTPUT_FILENAMES = Object.freeze([
|
|
25
|
+
...STATIC_OUTPUT_FILENAMES,
|
|
26
|
+
'output-<dispatch-id>.log',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function inputError(message, options) {
|
|
30
|
+
throw new apiValidation.ApiInputError(message, options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateAgentId(value, options = {}) {
|
|
34
|
+
return apiValidation.validateIdentifier(value, {
|
|
35
|
+
field: options.field || 'id',
|
|
36
|
+
path: options.path || options.field || 'id',
|
|
37
|
+
pattern: AGENT_ID_PATTERN,
|
|
38
|
+
maxLength: AGENT_ID_MAX_LENGTH,
|
|
39
|
+
code: options.code || 'invalid-agent-id',
|
|
40
|
+
trim: options.trim === true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateDispatchId(value, options = {}) {
|
|
45
|
+
return apiValidation.validateIdentifier(value, {
|
|
46
|
+
field: options.field || 'id',
|
|
47
|
+
path: options.path || options.field || 'id',
|
|
48
|
+
pattern: DISPATCH_ID_PATTERN,
|
|
49
|
+
maxLength: DISPATCH_ID_MAX_LENGTH,
|
|
50
|
+
code: options.code || 'invalid-dispatch-id',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateSteeringId(value, options = {}) {
|
|
55
|
+
return apiValidation.validateIdentifier(value, {
|
|
56
|
+
field: options.field || 'id',
|
|
57
|
+
path: options.path || options.field || 'id',
|
|
58
|
+
pattern: steeringConstraints.STEER_ID_PATTERN,
|
|
59
|
+
maxLength: 16,
|
|
60
|
+
code: options.code || 'invalid-steering-id',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function knownAgentIds({ config = {}, dispatch = {}, defaultAgents = {} } = {}) {
|
|
65
|
+
const configured = config.agents && Object.keys(config.agents).length > 0
|
|
66
|
+
? config.agents
|
|
67
|
+
: defaultAgents;
|
|
68
|
+
const ids = new Set(Object.keys(configured || {}));
|
|
69
|
+
for (const entry of Array.isArray(dispatch.active) ? dispatch.active : []) {
|
|
70
|
+
if (typeof entry?.agent === 'string'
|
|
71
|
+
&& entry.agent.startsWith('temp-')
|
|
72
|
+
&& AGENT_ID_PATTERN.test(entry.agent)
|
|
73
|
+
&& entry.agent.length <= AGENT_ID_MAX_LENGTH) {
|
|
74
|
+
ids.add(entry.agent);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return ids;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateKnownAgent(value, context, options = {}) {
|
|
81
|
+
const field = options.field || 'agent';
|
|
82
|
+
const path = options.path || field;
|
|
83
|
+
const agentId = validateAgentId(value, {
|
|
84
|
+
field,
|
|
85
|
+
path,
|
|
86
|
+
trim: options.trim !== false,
|
|
87
|
+
});
|
|
88
|
+
if (!knownAgentIds(context).has(agentId)) {
|
|
89
|
+
inputError(`Unknown agent: ${agentId}`, {
|
|
90
|
+
statusCode: 404,
|
|
91
|
+
code: 'unknown-agent',
|
|
92
|
+
field,
|
|
93
|
+
path,
|
|
94
|
+
rejectedValue: agentId,
|
|
95
|
+
expected: 'configured agent or active temp agent',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return agentId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read-only historical-agent set. Mutating routes (charter/steer/kill) are
|
|
102
|
+
// limited to configured or active agents via knownAgentIds, but output/detail
|
|
103
|
+
// reads must remain available for agents whose dispatch has already completed —
|
|
104
|
+
// e.g. a temp-* agent that errored: the completed-dispatch table still offers an
|
|
105
|
+
// "error details" button that reads its persisted output.log. Restricting reads
|
|
106
|
+
// to the active set would 404 that historical output the moment the dispatch
|
|
107
|
+
// moves active -> completed. Includes any well-formed agent id observed in the
|
|
108
|
+
// completed-dispatch history so those persisted logs stay reachable.
|
|
109
|
+
function readableAgentIds(context = {}) {
|
|
110
|
+
const ids = knownAgentIds(context);
|
|
111
|
+
const dispatch = context.dispatch || {};
|
|
112
|
+
for (const entry of Array.isArray(dispatch.completed) ? dispatch.completed : []) {
|
|
113
|
+
if (typeof entry?.agent === 'string'
|
|
114
|
+
&& AGENT_ID_PATTERN.test(entry.agent)
|
|
115
|
+
&& entry.agent.length <= AGENT_ID_MAX_LENGTH) {
|
|
116
|
+
ids.add(entry.agent);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return ids;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Validation seam for read-only output/detail access. Accepts a safe, well-formed
|
|
123
|
+
// agent identity that is configured, currently active, present in completed
|
|
124
|
+
// dispatch history, OR whose output artifacts still persist on disk (context may
|
|
125
|
+
// supply an `isPersistedAgent` predicate so pruned completed entries whose
|
|
126
|
+
// agents/<id>/ directory survives remain readable). The agent id is always
|
|
127
|
+
// shape-validated first (no traversal), so a rejected value never reaches the
|
|
128
|
+
// filesystem. Mutations must keep using validateKnownAgent.
|
|
129
|
+
function validateReadableAgent(value, context, options = {}) {
|
|
130
|
+
const field = options.field || 'agent';
|
|
131
|
+
const path = options.path || field;
|
|
132
|
+
const agentId = validateAgentId(value, {
|
|
133
|
+
field,
|
|
134
|
+
path,
|
|
135
|
+
trim: options.trim !== false,
|
|
136
|
+
});
|
|
137
|
+
if (readableAgentIds(context).has(agentId)) return agentId;
|
|
138
|
+
if (typeof context?.isPersistedAgent === 'function'
|
|
139
|
+
&& context.isPersistedAgent(agentId) === true) {
|
|
140
|
+
return agentId;
|
|
141
|
+
}
|
|
142
|
+
inputError(`Unknown agent: ${agentId}`, {
|
|
143
|
+
statusCode: 404,
|
|
144
|
+
code: 'unknown-agent',
|
|
145
|
+
field,
|
|
146
|
+
path,
|
|
147
|
+
rejectedValue: agentId,
|
|
148
|
+
expected: 'configured, active, or historical agent with persisted output',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateCharterRequest(body, context) {
|
|
153
|
+
apiValidation.validatePlainObject(body, { field: 'body', path: 'body' });
|
|
154
|
+
const agent = validateKnownAgent(body.agent, context, {
|
|
155
|
+
field: 'agent',
|
|
156
|
+
path: 'body.agent',
|
|
157
|
+
});
|
|
158
|
+
const content = apiValidation.validateString(body.content, {
|
|
159
|
+
field: 'content',
|
|
160
|
+
path: 'body.content',
|
|
161
|
+
maxLength: AGENT_CHARTER_MAX_LENGTH,
|
|
162
|
+
code: 'invalid-charter-content',
|
|
163
|
+
});
|
|
164
|
+
return { agent, content };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateSteeringRequest(body, context) {
|
|
168
|
+
apiValidation.validatePlainObject(body, { field: 'body', path: 'body' });
|
|
169
|
+
const agent = validateKnownAgent(body.agent, context, {
|
|
170
|
+
field: 'agent',
|
|
171
|
+
path: 'body.agent',
|
|
172
|
+
});
|
|
173
|
+
const message = apiValidation.validateString(body.message, {
|
|
174
|
+
field: 'message',
|
|
175
|
+
path: 'body.message',
|
|
176
|
+
trim: true,
|
|
177
|
+
allowEmpty: false,
|
|
178
|
+
maxLength: STEERING_MESSAGE_MAX_LENGTH,
|
|
179
|
+
code: 'invalid-steering-message',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
let scope = steeringConstraints.SCOPE.AGENT;
|
|
183
|
+
if (body.scope !== undefined) {
|
|
184
|
+
scope = apiValidation.validateString(body.scope, {
|
|
185
|
+
field: 'scope',
|
|
186
|
+
path: 'body.scope',
|
|
187
|
+
trim: true,
|
|
188
|
+
allowEmpty: false,
|
|
189
|
+
maxLength: 32,
|
|
190
|
+
code: 'invalid-steering-scope',
|
|
191
|
+
}).toLowerCase();
|
|
192
|
+
apiValidation.validateEnum(scope, {
|
|
193
|
+
field: 'scope',
|
|
194
|
+
path: 'body.scope',
|
|
195
|
+
values: Object.values(steeringConstraints.SCOPE),
|
|
196
|
+
code: 'invalid-steering-scope',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let supersede = '';
|
|
201
|
+
if (body.supersede !== undefined && body.supersede !== null && body.supersede !== '') {
|
|
202
|
+
supersede = apiValidation.validateString(body.supersede, {
|
|
203
|
+
field: 'supersede',
|
|
204
|
+
path: 'body.supersede',
|
|
205
|
+
trim: true,
|
|
206
|
+
allowEmpty: false,
|
|
207
|
+
maxLength: 32,
|
|
208
|
+
code: 'invalid-steering-supersede',
|
|
209
|
+
});
|
|
210
|
+
if (!steeringConstraints.isSupersedeValue(supersede)) {
|
|
211
|
+
inputError('supersede must be all, unacked, or a steering id', {
|
|
212
|
+
code: 'invalid-steering-supersede',
|
|
213
|
+
field: 'supersede',
|
|
214
|
+
path: 'body.supersede',
|
|
215
|
+
rejectedValue: supersede,
|
|
216
|
+
expected: 'all | unacked | steer-<10 hex characters>',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { agent, message, scope, supersede };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function presentAliasValues(body, names) {
|
|
225
|
+
return names
|
|
226
|
+
.filter(name => Object.prototype.hasOwnProperty.call(body, name)
|
|
227
|
+
&& body[name] !== undefined
|
|
228
|
+
&& body[name] !== null)
|
|
229
|
+
.map(name => ({ name, value: body[name] }));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function conflictingAliases(entries, normalizedValues, canonicalField) {
|
|
233
|
+
if (new Set(normalizedValues).size <= 1) return;
|
|
234
|
+
const conflicting = entries[entries.length - 1];
|
|
235
|
+
inputError(`Conflicting aliases for ${canonicalField}`, {
|
|
236
|
+
code: 'conflicting-aliases',
|
|
237
|
+
field: conflicting.name,
|
|
238
|
+
path: `body.${conflicting.name}`,
|
|
239
|
+
rejectedValue: conflicting.value,
|
|
240
|
+
expected: `same value as body.${entries[0].name}`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function validateCancelRequest(body) {
|
|
245
|
+
apiValidation.validatePlainObject(body, { field: 'body', path: 'body' });
|
|
246
|
+
const agentEntries = presentAliasValues(body, ['agent', 'agentId']);
|
|
247
|
+
const taskEntries = presentAliasValues(body, ['task', 'cancelTask']);
|
|
248
|
+
|
|
249
|
+
if (agentEntries.length === 0 && taskEntries.length === 0) {
|
|
250
|
+
inputError('Exactly one agent or task target is required', {
|
|
251
|
+
code: 'missing-target',
|
|
252
|
+
details: [
|
|
253
|
+
{
|
|
254
|
+
code: 'missing-target',
|
|
255
|
+
field: 'agent',
|
|
256
|
+
path: 'body.agent',
|
|
257
|
+
message: 'agent or task is required',
|
|
258
|
+
expected: 'agent identifier or task substring',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
code: 'missing-target',
|
|
262
|
+
field: 'task',
|
|
263
|
+
path: 'body.task',
|
|
264
|
+
message: 'agent or task is required',
|
|
265
|
+
expected: 'agent identifier or task substring',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (agentEntries.length > 0 && taskEntries.length > 0) {
|
|
271
|
+
inputError('Provide exactly one agent or task target', {
|
|
272
|
+
code: 'ambiguous-target',
|
|
273
|
+
details: [
|
|
274
|
+
{
|
|
275
|
+
code: 'ambiguous-target',
|
|
276
|
+
field: 'agent',
|
|
277
|
+
path: `body.${agentEntries[0].name}`,
|
|
278
|
+
message: 'agent and task cannot both be supplied',
|
|
279
|
+
rejectedValue: agentEntries[0].value,
|
|
280
|
+
expected: 'exactly one target selector',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
code: 'ambiguous-target',
|
|
284
|
+
field: 'task',
|
|
285
|
+
path: `body.${taskEntries[0].name}`,
|
|
286
|
+
message: 'agent and task cannot both be supplied',
|
|
287
|
+
rejectedValue: taskEntries[0].value,
|
|
288
|
+
expected: 'exactly one target selector',
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (agentEntries.length > 0) {
|
|
295
|
+
const normalized = agentEntries.map(entry => validateAgentId(entry.value, {
|
|
296
|
+
field: entry.name,
|
|
297
|
+
path: `body.${entry.name}`,
|
|
298
|
+
trim: true,
|
|
299
|
+
}));
|
|
300
|
+
conflictingAliases(agentEntries, normalized, 'agent');
|
|
301
|
+
return { kind: 'agent', field: agentEntries[0].name, value: normalized[0] };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const normalized = taskEntries.map(entry => apiValidation.validateString(entry.value, {
|
|
305
|
+
field: entry.name,
|
|
306
|
+
path: `body.${entry.name}`,
|
|
307
|
+
trim: true,
|
|
308
|
+
allowEmpty: false,
|
|
309
|
+
maxLength: CANCEL_TASK_MAX_LENGTH,
|
|
310
|
+
code: 'invalid-task-selector',
|
|
311
|
+
}));
|
|
312
|
+
conflictingAliases(taskEntries, normalized, 'task');
|
|
313
|
+
return { kind: 'task', field: taskEntries[0].name, value: normalized[0] };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function resolveSingleCancelTarget(selector, activeEntries) {
|
|
317
|
+
const active = Array.isArray(activeEntries) ? activeEntries : [];
|
|
318
|
+
const matches = selector.kind === 'agent'
|
|
319
|
+
? active.filter(entry => entry.agent === selector.value)
|
|
320
|
+
: active.filter(entry => String(entry.task || '').toLowerCase()
|
|
321
|
+
.includes(selector.value.toLowerCase()));
|
|
322
|
+
const path = `body.${selector.field}`;
|
|
323
|
+
if (matches.length === 0) {
|
|
324
|
+
inputError(`No active dispatch matches ${selector.kind} target`, {
|
|
325
|
+
statusCode: 404,
|
|
326
|
+
code: 'target-not-found',
|
|
327
|
+
field: selector.field,
|
|
328
|
+
path,
|
|
329
|
+
rejectedValue: selector.value,
|
|
330
|
+
expected: `one active dispatch matched by ${selector.kind}`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (matches.length > 1) {
|
|
334
|
+
inputError(`${selector.kind} target matches multiple active dispatches`, {
|
|
335
|
+
statusCode: 409,
|
|
336
|
+
code: 'ambiguous-target',
|
|
337
|
+
field: selector.field,
|
|
338
|
+
path,
|
|
339
|
+
rejectedValue: selector.value,
|
|
340
|
+
expected: 'selector matching exactly one active dispatch',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return matches[0];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function queryValues(params, name) {
|
|
347
|
+
const values = params.getAll(name);
|
|
348
|
+
if (values.length > 1) {
|
|
349
|
+
inputError(`${name} may be supplied only once`, {
|
|
350
|
+
code: 'invalid-query',
|
|
351
|
+
field: name,
|
|
352
|
+
path: `query.${name}`,
|
|
353
|
+
rejectedValue: values,
|
|
354
|
+
expected: 'single query parameter',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return values.length === 1 ? values[0] : undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function queryInteger(params, name, options = {}) {
|
|
361
|
+
const raw = queryValues(params, name);
|
|
362
|
+
if (raw === undefined) return options.defaultValue;
|
|
363
|
+
if (!/^(?:0|[1-9]\d*)$/.test(raw)) {
|
|
364
|
+
inputError(`${name} must be a non-negative integer`, {
|
|
365
|
+
code: options.code || 'invalid-query',
|
|
366
|
+
field: name,
|
|
367
|
+
path: `query.${name}`,
|
|
368
|
+
rejectedValue: raw,
|
|
369
|
+
expected: 'non-negative integer',
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const value = Number(raw);
|
|
373
|
+
if (!Number.isSafeInteger(value)
|
|
374
|
+
|| (Number.isFinite(options.min) && value < options.min)
|
|
375
|
+
|| (Number.isFinite(options.max) && value > options.max)) {
|
|
376
|
+
inputError(`${name} is outside the allowed range`, {
|
|
377
|
+
code: options.code || 'invalid-query',
|
|
378
|
+
field: name,
|
|
379
|
+
path: `query.${name}`,
|
|
380
|
+
rejectedValue: raw,
|
|
381
|
+
expected: 'integer in range',
|
|
382
|
+
...(Number.isFinite(options.min) ? { min: options.min } : {}),
|
|
383
|
+
...(Number.isFinite(options.max) ? { max: options.max } : {}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function validateLiveStreamQuery(url) {
|
|
390
|
+
const params = new URL(url, 'http://localhost').searchParams;
|
|
391
|
+
return {
|
|
392
|
+
tail: queryInteger(params, 'tail', {
|
|
393
|
+
defaultValue: LIVE_TAIL_MAX_BYTES,
|
|
394
|
+
min: 0,
|
|
395
|
+
max: LIVE_TAIL_MAX_BYTES,
|
|
396
|
+
}),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function validateLiveOutputQuery(url) {
|
|
401
|
+
const params = new URL(url, 'http://localhost').searchParams;
|
|
402
|
+
const rawTail = queryValues(params, 'tail');
|
|
403
|
+
const rawFrom = queryValues(params, 'from');
|
|
404
|
+
const rawTo = queryValues(params, 'to');
|
|
405
|
+
const rawPrev = queryValues(params, 'prev');
|
|
406
|
+
const hasRange = rawFrom !== undefined || rawTo !== undefined;
|
|
407
|
+
|
|
408
|
+
if (hasRange && rawTail !== undefined) {
|
|
409
|
+
inputError('tail cannot be combined with from/to', {
|
|
410
|
+
code: 'ambiguous-query',
|
|
411
|
+
field: 'tail',
|
|
412
|
+
path: 'query.tail',
|
|
413
|
+
rejectedValue: rawTail,
|
|
414
|
+
expected: 'tail mode or range mode, not both',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (hasRange && (rawFrom === undefined || rawTo === undefined)) {
|
|
418
|
+
const field = rawFrom === undefined ? 'from' : 'to';
|
|
419
|
+
inputError('from and to must both be provided for a range read', {
|
|
420
|
+
code: 'missing-range-bound',
|
|
421
|
+
field,
|
|
422
|
+
path: `query.${field}`,
|
|
423
|
+
expected: 'paired from and to values',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
if (rawPrev !== undefined && rawPrev !== '0' && rawPrev !== '1') {
|
|
427
|
+
inputError('prev must be 0 or 1', {
|
|
428
|
+
code: 'invalid-query',
|
|
429
|
+
field: 'prev',
|
|
430
|
+
path: 'query.prev',
|
|
431
|
+
rejectedValue: rawPrev,
|
|
432
|
+
allowedValues: ['0', '1'],
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (!hasRange && rawPrev === '1') {
|
|
436
|
+
inputError('prev=1 requires a from/to range', {
|
|
437
|
+
code: 'ambiguous-query',
|
|
438
|
+
field: 'prev',
|
|
439
|
+
path: 'query.prev',
|
|
440
|
+
rejectedValue: rawPrev,
|
|
441
|
+
expected: 'prev is only valid in range mode',
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!hasRange) {
|
|
446
|
+
return {
|
|
447
|
+
mode: 'tail',
|
|
448
|
+
tail: queryInteger(params, 'tail', {
|
|
449
|
+
defaultValue: 8192,
|
|
450
|
+
min: 1,
|
|
451
|
+
max: LIVE_TAIL_MAX_BYTES,
|
|
452
|
+
}),
|
|
453
|
+
prev: false,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const from = queryInteger(params, 'from', { min: 0 });
|
|
458
|
+
const to = queryInteger(params, 'to', { min: 1 });
|
|
459
|
+
if (from >= to) {
|
|
460
|
+
inputError('from must be less than to', {
|
|
461
|
+
code: 'invalid-range',
|
|
462
|
+
field: 'from',
|
|
463
|
+
path: 'query.from',
|
|
464
|
+
rejectedValue: rawFrom,
|
|
465
|
+
expected: 'integer less than query.to',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (to - from > LIVE_RANGE_MAX_BYTES) {
|
|
469
|
+
inputError(`range exceeds maximum of ${LIVE_RANGE_MAX_BYTES} bytes`, {
|
|
470
|
+
code: 'range-too-large',
|
|
471
|
+
field: 'to',
|
|
472
|
+
path: 'query.to',
|
|
473
|
+
rejectedValue: rawTo,
|
|
474
|
+
expected: `range no larger than ${LIVE_RANGE_MAX_BYTES} bytes`,
|
|
475
|
+
max: LIVE_RANGE_MAX_BYTES,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
mode: 'range',
|
|
480
|
+
from,
|
|
481
|
+
to,
|
|
482
|
+
prev: rawPrev === '1',
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function validateSteeringListQuery(url) {
|
|
487
|
+
const params = new URL(url, 'http://localhost').searchParams;
|
|
488
|
+
const limit = queryInteger(params, 'limit', {
|
|
489
|
+
defaultValue: 50,
|
|
490
|
+
min: 1,
|
|
491
|
+
max: STEERING_LIST_MAX_LIMIT,
|
|
492
|
+
});
|
|
493
|
+
const status = queryValues(params, 'status');
|
|
494
|
+
if (status !== undefined && !steeringStore.VALID_STATUSES.has(status)) {
|
|
495
|
+
inputError('status must be a valid steering delivery status', {
|
|
496
|
+
code: 'invalid-steering-status',
|
|
497
|
+
field: 'status',
|
|
498
|
+
path: 'query.status',
|
|
499
|
+
rejectedValue: status,
|
|
500
|
+
allowedValues: [...steeringStore.VALID_STATUSES],
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return { limit, status };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function validateOutputFile(value, context) {
|
|
507
|
+
const file = apiValidation.validateString(value, {
|
|
508
|
+
field: 'file',
|
|
509
|
+
path: 'query.file',
|
|
510
|
+
trim: true,
|
|
511
|
+
allowEmpty: false,
|
|
512
|
+
maxLength: OUTPUT_FILE_MAX_LENGTH,
|
|
513
|
+
code: 'invalid-output-file',
|
|
514
|
+
});
|
|
515
|
+
const parts = file.replace(/\\/g, '/').split('/');
|
|
516
|
+
const archiveMatch = /^output-(.+)\.log$/.exec(parts[2] || '');
|
|
517
|
+
const supportedFilename = STATIC_OUTPUT_FILENAMES.has(parts[2])
|
|
518
|
+
|| (archiveMatch
|
|
519
|
+
&& archiveMatch[1].length <= DISPATCH_ID_MAX_LENGTH
|
|
520
|
+
&& DISPATCH_ID_PATTERN.test(archiveMatch[1]));
|
|
521
|
+
if (parts.length !== 3
|
|
522
|
+
|| parts[0] !== 'agents'
|
|
523
|
+
|| !AGENT_ID_PATTERN.test(parts[1])
|
|
524
|
+
|| parts[1].length > AGENT_ID_MAX_LENGTH
|
|
525
|
+
|| !supportedFilename) {
|
|
526
|
+
inputError('file must name a supported agent output file', {
|
|
527
|
+
code: 'invalid-output-file',
|
|
528
|
+
field: 'file',
|
|
529
|
+
path: 'query.file',
|
|
530
|
+
rejectedValue: file,
|
|
531
|
+
expected: 'agents/<agent-id>/<supported-output-filename>',
|
|
532
|
+
allowedValues: OUTPUT_FILENAMES,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (context) {
|
|
536
|
+
validateReadableAgent(parts[1], context, {
|
|
537
|
+
field: 'file',
|
|
538
|
+
path: 'query.file',
|
|
539
|
+
trim: false,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return file;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
module.exports = {
|
|
546
|
+
AGENT_ID_MAX_LENGTH,
|
|
547
|
+
DISPATCH_ID_MAX_LENGTH,
|
|
548
|
+
AGENT_CHARTER_MAX_LENGTH,
|
|
549
|
+
STEERING_MESSAGE_MAX_LENGTH,
|
|
550
|
+
CANCEL_TASK_MAX_LENGTH,
|
|
551
|
+
LIVE_TAIL_MAX_BYTES,
|
|
552
|
+
LIVE_RANGE_MAX_BYTES,
|
|
553
|
+
STEERING_LIST_MAX_LIMIT,
|
|
554
|
+
OUTPUT_FILE_MAX_LENGTH,
|
|
555
|
+
OUTPUT_FILENAMES,
|
|
556
|
+
validateAgentId,
|
|
557
|
+
validateDispatchId,
|
|
558
|
+
validateSteeringId,
|
|
559
|
+
knownAgentIds,
|
|
560
|
+
validateKnownAgent,
|
|
561
|
+
readableAgentIds,
|
|
562
|
+
validateReadableAgent,
|
|
563
|
+
validateCharterRequest,
|
|
564
|
+
validateSteeringRequest,
|
|
565
|
+
validateCancelRequest,
|
|
566
|
+
resolveSingleCancelTarget,
|
|
567
|
+
validateLiveStreamQuery,
|
|
568
|
+
validateLiveOutputQuery,
|
|
569
|
+
validateSteeringListQuery,
|
|
570
|
+
validateOutputFile,
|
|
571
|
+
};
|
|
@@ -26,19 +26,20 @@
|
|
|
26
26
|
* // it died during the lease)
|
|
27
27
|
* }
|
|
28
28
|
* pool.setPoolSize(n) / getPoolSize()
|
|
29
|
+
* pool.setEnabled(bool) // close idle workers when disabled
|
|
29
30
|
* pool.shutdown() // close every worker, reject the queue
|
|
30
31
|
*
|
|
31
32
|
* Assignment strategy (acquireWorker), per the P-7a1c2e4f spike's findings:
|
|
32
33
|
*
|
|
33
|
-
* 1. A FREE worker whose MCP
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* 1. A FREE worker whose MCP and process-launch context already match the
|
|
35
|
+
* request wins first. Persisted sessions prefer their current owning
|
|
36
|
+
* worker and continue in place; otherwise the worker uses `session/load`
|
|
37
|
+
* or a fresh `session/new`.
|
|
37
38
|
* 2. Otherwise any OTHER free worker is evicted (closed) and a replacement
|
|
38
39
|
* is spawned fresh with the requested mcpServers/cwd — MCP servers are
|
|
39
40
|
* resolved at `copilot --acp` process boot (via $COPILOT_HOME), so a
|
|
40
|
-
* changed local/stdio server set
|
|
41
|
-
* not just a new session (spike (c)).
|
|
41
|
+
* changed local/stdio server set, process cwd, or process-scoped ACP CLI
|
|
42
|
+
* policy requires a full respawn, not just a new session (spike (c)).
|
|
42
43
|
* 3. Otherwise, if the pool has room (fewer than `poolSize` workers
|
|
43
44
|
* allocated across free+busy+in-flight-spawn), spawn a brand new
|
|
44
45
|
* worker.
|