principles-disciple 1.7.6 → 1.7.8
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/dist/commands/context.js +5 -15
- package/dist/commands/evolution-status.js +2 -9
- package/dist/commands/export.js +61 -8
- package/dist/commands/nocturnal-review.d.ts +24 -0
- package/dist/commands/nocturnal-review.js +265 -0
- package/dist/commands/nocturnal-rollout.d.ts +27 -0
- package/dist/commands/nocturnal-rollout.js +671 -0
- package/dist/commands/nocturnal-train.d.ts +25 -0
- package/dist/commands/nocturnal-train.js +919 -0
- package/dist/commands/pain.js +8 -21
- package/dist/constants/tools.d.ts +2 -2
- package/dist/constants/tools.js +1 -1
- package/dist/core/adaptive-thresholds.d.ts +186 -0
- package/dist/core/adaptive-thresholds.js +300 -0
- package/dist/core/config.d.ts +2 -38
- package/dist/core/config.js +6 -61
- package/dist/core/event-log.d.ts +1 -2
- package/dist/core/event-log.js +0 -3
- package/dist/core/evolution-engine.js +1 -21
- package/dist/core/evolution-reducer.d.ts +7 -1
- package/dist/core/evolution-reducer.js +56 -4
- package/dist/core/evolution-types.d.ts +61 -9
- package/dist/core/evolution-types.js +31 -9
- package/dist/core/external-training-contract.d.ts +276 -0
- package/dist/core/external-training-contract.js +269 -0
- package/dist/core/local-worker-routing.d.ts +175 -0
- package/dist/core/local-worker-routing.js +525 -0
- package/dist/core/model-deployment-registry.d.ts +218 -0
- package/dist/core/model-deployment-registry.js +503 -0
- package/dist/core/model-training-registry.d.ts +295 -0
- package/dist/core/model-training-registry.js +475 -0
- package/dist/core/nocturnal-arbiter.d.ts +159 -0
- package/dist/core/nocturnal-arbiter.js +534 -0
- package/dist/core/nocturnal-candidate-scoring.d.ts +137 -0
- package/dist/core/nocturnal-candidate-scoring.js +266 -0
- package/dist/core/nocturnal-compliance.d.ts +175 -0
- package/dist/core/nocturnal-compliance.js +824 -0
- package/dist/core/nocturnal-dataset.d.ts +224 -0
- package/dist/core/nocturnal-dataset.js +443 -0
- package/dist/core/nocturnal-executability.d.ts +85 -0
- package/dist/core/nocturnal-executability.js +331 -0
- package/dist/core/nocturnal-export.d.ts +124 -0
- package/dist/core/nocturnal-export.js +275 -0
- package/dist/core/nocturnal-paths.d.ts +124 -0
- package/dist/core/nocturnal-paths.js +214 -0
- package/dist/core/nocturnal-trajectory-extractor.d.ts +242 -0
- package/dist/core/nocturnal-trajectory-extractor.js +307 -0
- package/dist/core/nocturnal-trinity.d.ts +311 -0
- package/dist/core/nocturnal-trinity.js +880 -0
- package/dist/core/paths.d.ts +6 -0
- package/dist/core/paths.js +6 -0
- package/dist/core/principle-training-state.d.ts +121 -0
- package/dist/core/principle-training-state.js +321 -0
- package/dist/core/promotion-gate.d.ts +238 -0
- package/dist/core/promotion-gate.js +529 -0
- package/dist/core/session-tracker.d.ts +10 -0
- package/dist/core/session-tracker.js +14 -0
- package/dist/core/shadow-observation-registry.d.ts +217 -0
- package/dist/core/shadow-observation-registry.js +308 -0
- package/dist/core/training-program.d.ts +233 -0
- package/dist/core/training-program.js +433 -0
- package/dist/core/trajectory.d.ts +95 -1
- package/dist/core/trajectory.js +220 -6
- package/dist/core/workspace-context.d.ts +0 -6
- package/dist/core/workspace-context.js +0 -12
- package/dist/hooks/bash-risk.d.ts +6 -6
- package/dist/hooks/bash-risk.js +8 -8
- package/dist/hooks/gate-block-helper.js +1 -1
- package/dist/hooks/gate.d.ts +1 -1
- package/dist/hooks/gate.js +2 -2
- package/dist/hooks/gfi-gate.d.ts +3 -3
- package/dist/hooks/gfi-gate.js +15 -14
- package/dist/hooks/pain.js +6 -9
- package/dist/hooks/progressive-trust-gate.d.ts +21 -49
- package/dist/hooks/progressive-trust-gate.js +51 -204
- package/dist/hooks/prompt.d.ts +11 -11
- package/dist/hooks/prompt.js +158 -72
- package/dist/hooks/subagent.js +43 -6
- package/dist/i18n/commands.js +8 -8
- package/dist/index.js +129 -28
- package/dist/service/evolution-worker.d.ts +42 -4
- package/dist/service/evolution-worker.js +321 -13
- package/dist/service/nocturnal-runtime.d.ts +183 -0
- package/dist/service/nocturnal-runtime.js +352 -0
- package/dist/service/nocturnal-service.d.ts +163 -0
- package/dist/service/nocturnal-service.js +787 -0
- package/dist/service/nocturnal-target-selector.d.ts +145 -0
- package/dist/service/nocturnal-target-selector.js +315 -0
- package/dist/service/phase3-input-filter.d.ts +2 -23
- package/dist/service/phase3-input-filter.js +3 -27
- package/dist/service/runtime-summary-service.d.ts +0 -10
- package/dist/service/runtime-summary-service.js +1 -54
- package/dist/tools/deep-reflect.js +2 -1
- package/dist/types/event-types.d.ts +2 -10
- package/dist/types/runtime-summary.d.ts +1 -8
- package/dist/types.d.ts +0 -3
- package/dist/types.js +0 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/templates/langs/en/skills/pd-mentor/SKILL.md +5 -5
- package/templates/langs/zh/skills/pd-mentor/SKILL.md +5 -5
- package/templates/pain_settings.json +0 -6
- package/dist/commands/trust.d.ts +0 -4
- package/dist/commands/trust.js +0 -78
- package/dist/core/trust-engine.d.ts +0 -96
- package/dist/core/trust-engine.js +0 -286
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Worker Routing Policy — Task Classification and Routing Decisions
|
|
3
|
+
* ======================================================================
|
|
4
|
+
*
|
|
5
|
+
* PURPOSE: Provide an explainable, testable policy that decides whether a given
|
|
6
|
+
* task can be delegated to a local-worker profile (local-reader or local-editor)
|
|
7
|
+
* or must stay on the main agent.
|
|
8
|
+
*
|
|
9
|
+
* ARCHITECTURE:
|
|
10
|
+
* - This module is POLICY ONLY — it makes routing decisions but does NOT execute them
|
|
11
|
+
* - The main agent (or a delegation hook in a future phase) is responsible for
|
|
12
|
+
* actually routing the task based on the RoutingDecision returned here
|
|
13
|
+
* - All decisions are deterministic and based on structured input fields
|
|
14
|
+
* - No model inference, no learning, no dynamic adaptation
|
|
15
|
+
*
|
|
16
|
+
* TASK CLASSIFICATION TAXONOMY:
|
|
17
|
+
* reader_eligible — clearly suitable for local-reader
|
|
18
|
+
* editor_eligible — clearly suitable for local-editor
|
|
19
|
+
* high_entropy_disallowed — high-complexity tasks that must stay on main agent
|
|
20
|
+
* risk_disallowed — tasks with destructive or high-risk signals
|
|
21
|
+
* ambiguous_scope — tasks that are unclear and need main-agent judgment
|
|
22
|
+
* deployment_unavailable — no enabled deployment exists for the target profile
|
|
23
|
+
*
|
|
24
|
+
* FAIL-CLOSED PRINCIPLE:
|
|
25
|
+
* - When in doubt → stay_main
|
|
26
|
+
* - Unclear intent → stay_main
|
|
27
|
+
* - High complexity → stay_main
|
|
28
|
+
* - Any risk signal → stay_main
|
|
29
|
+
* - No enabled deployment → stay_main
|
|
30
|
+
*
|
|
31
|
+
* DESIGN CONSTRAINTS:
|
|
32
|
+
* - No actual task execution
|
|
33
|
+
* - No automatic learning or route optimization
|
|
34
|
+
* - No Trinity or adaptive threshold logic
|
|
35
|
+
* - Routing decisions are fully explainable (return `reason` + `blockers[]`)
|
|
36
|
+
*/
|
|
37
|
+
import { isRoutingEnabledForProfile, getDeployment, } from './model-deployment-registry.js';
|
|
38
|
+
import { isCheckpointDeployable } from './model-training-registry.js';
|
|
39
|
+
import { getPromotionState } from './promotion-gate.js';
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Keyword Classifiers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
/**
|
|
44
|
+
* Keywords that indicate a task is suitable for `local-reader`.
|
|
45
|
+
* Matched against taskIntent + taskDescription.
|
|
46
|
+
*/
|
|
47
|
+
const READER_KEYWORDS = [
|
|
48
|
+
'read', 'view', 'show', 'get', 'find', 'search', 'grep', 'look',
|
|
49
|
+
'inspect', 'examine', 'list', 'cat', 'head', 'tail', 'diff',
|
|
50
|
+
'summary', 'summarize', 'extract', 'parse', 'review',
|
|
51
|
+
'check', 'verify', 'status', 'describe', 'explain_what',
|
|
52
|
+
'browse', 'fetch', 'show_content', 'file_content', 'code_read',
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Keywords that indicate a task is suitable for `local-editor`.
|
|
56
|
+
* Matched against taskIntent + taskDescription.
|
|
57
|
+
*/
|
|
58
|
+
const EDITOR_KEYWORDS = [
|
|
59
|
+
'edit', 'update', 'modify', 'change', 'fix', 'patch', 'replace',
|
|
60
|
+
'add', 'remove', 'delete', 'insert', 'rewrite', 'refactor',
|
|
61
|
+
'apply', 'execute', 'run', 'transform', 'convert', 'migrate',
|
|
62
|
+
'write', 'create_file', 'append', 'touch', 'rename',
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Keywords that indicate HIGH ENTROPY — tasks that must stay on main agent.
|
|
66
|
+
* These indicate open-ended, multi-step, or ambiguous tasks.
|
|
67
|
+
*/
|
|
68
|
+
const HIGH_ENTROPY_KEYWORDS = [
|
|
69
|
+
'design', 'architect', 'plan', 'strategy', 'roadmap', 'propose',
|
|
70
|
+
'research', 'investigate', 'explore', 'evaluate', 'compare',
|
|
71
|
+
'decide', 'choose', 'recommend', 'suggest', 'analyze_tradeoffs',
|
|
72
|
+
'unclear', 'vague', 'ambiguous', 'open_ended', 'multiple_options',
|
|
73
|
+
'architecture', 'system_design', 'high_level', 'blueprint',
|
|
74
|
+
'thinking', 'reasoning', '思考', '分析', '设计',
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* Keywords that indicate RISK — tasks that must stay on main agent.
|
|
78
|
+
* These indicate destructive, irreversible, or production-sensitive operations.
|
|
79
|
+
*/
|
|
80
|
+
const RISK_KEYWORDS = [
|
|
81
|
+
'rm', 'delete', 'remove', 'drop', 'truncate', 'destroy',
|
|
82
|
+
'force_push', 'force_reset', 'hard_reset', 'rebase_force',
|
|
83
|
+
'exec', 'eval', 'sudo', 'chmod_777', 'shutdown', 'reboot',
|
|
84
|
+
'production', 'prod', 'live', 'deploy', 'release',
|
|
85
|
+
'database', 'db_', 'migration', 'seed',
|
|
86
|
+
'secrets', 'credentials', 'password', 'api_key', 'token',
|
|
87
|
+
'bulk', 'batch', 'all_files', 'entire_', 'recursive_delete',
|
|
88
|
+
];
|
|
89
|
+
/**
|
|
90
|
+
* Risk file patterns — files whose modification indicates high risk.
|
|
91
|
+
* Matched against requestedFiles using simple includes check.
|
|
92
|
+
*/
|
|
93
|
+
const RISK_FILE_PATTERNS = [
|
|
94
|
+
'.git/config',
|
|
95
|
+
'package-lock.json',
|
|
96
|
+
'yarn.lock',
|
|
97
|
+
'pnpm-lock.yaml',
|
|
98
|
+
'node_modules',
|
|
99
|
+
'.env',
|
|
100
|
+
'secrets',
|
|
101
|
+
'credentials',
|
|
102
|
+
'.aws/credentials',
|
|
103
|
+
'~/.ssh/',
|
|
104
|
+
'/etc/',
|
|
105
|
+
'production',
|
|
106
|
+
'.k8s',
|
|
107
|
+
'docker-compose.yml',
|
|
108
|
+
'Dockerfile',
|
|
109
|
+
];
|
|
110
|
+
/**
|
|
111
|
+
* Tools that are inherently risky and indicate a task must stay on main agent.
|
|
112
|
+
*/
|
|
113
|
+
const RISK_TOOLS = [
|
|
114
|
+
'bash', 'shell', 'exec', 'run_command', 'execute',
|
|
115
|
+
'git_push', 'git_force_push', 'git_reset',
|
|
116
|
+
'http_delete', 'DROP', 'DELETE *', 'truncate',
|
|
117
|
+
'sudo', 'chmod', 'chown',
|
|
118
|
+
];
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Classification Helpers
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
/**
|
|
123
|
+
* Simple case-insensitive keyword match.
|
|
124
|
+
*/
|
|
125
|
+
function containsKeyword(text, keywords) {
|
|
126
|
+
if (!text)
|
|
127
|
+
return false;
|
|
128
|
+
const lower = text.toLowerCase();
|
|
129
|
+
return keywords.some((kw) => lower.includes(kw));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if any tool name looks like a risk tool.
|
|
133
|
+
*/
|
|
134
|
+
function hasRiskTool(tools) {
|
|
135
|
+
if (!tools || tools.length === 0)
|
|
136
|
+
return false;
|
|
137
|
+
const combined = tools.join(' ').toLowerCase();
|
|
138
|
+
return RISK_TOOLS.some((rt) => combined.includes(rt));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Check if any requested file matches a risk pattern.
|
|
142
|
+
*/
|
|
143
|
+
function hasRiskFile(files) {
|
|
144
|
+
if (!files || files.length === 0)
|
|
145
|
+
return false;
|
|
146
|
+
return files.some((f) => {
|
|
147
|
+
const lower = f.toLowerCase();
|
|
148
|
+
return RISK_FILE_PATTERNS.some((rp) => lower.includes(rp.toLowerCase()));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Compute a combined text from all input fields for keyword scanning.
|
|
153
|
+
*/
|
|
154
|
+
function computeCombinedText(input) {
|
|
155
|
+
const parts = [];
|
|
156
|
+
if (input.taskIntent)
|
|
157
|
+
parts.push(input.taskIntent);
|
|
158
|
+
if (input.taskDescription)
|
|
159
|
+
parts.push(input.taskDescription);
|
|
160
|
+
if (input.expectedOutputShape)
|
|
161
|
+
parts.push(input.expectedOutputShape);
|
|
162
|
+
if (input.complexityHints)
|
|
163
|
+
parts.push(input.complexityHints.join(' '));
|
|
164
|
+
return parts.join(' ').toLowerCase();
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Core Classification Logic
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* Classify the task based on its input fields.
|
|
171
|
+
* Returns a raw classification category (before deployment check).
|
|
172
|
+
*/
|
|
173
|
+
function classifyTaskKind(input) {
|
|
174
|
+
const text = computeCombinedText(input);
|
|
175
|
+
const { taskIntent, taskDescription, requestedTools, requestedFiles, riskSignals, complexityHints } = input;
|
|
176
|
+
// --- Step 1: Explicit risk signals always block ---
|
|
177
|
+
if (riskSignals && riskSignals.length > 0) {
|
|
178
|
+
return 'risk_disallowed';
|
|
179
|
+
}
|
|
180
|
+
// --- Step 2: High-entropy keyword detection ---
|
|
181
|
+
if (complexityHints?.some((h) => ['multi_step', 'cross_file', 'ambiguous', 'requires_planning', 'open_ended', 'unclear'].includes(h))) {
|
|
182
|
+
return 'high_entropy_disallowed';
|
|
183
|
+
}
|
|
184
|
+
if (containsKeyword(text, HIGH_ENTROPY_KEYWORDS)) {
|
|
185
|
+
return 'high_entropy_disallowed';
|
|
186
|
+
}
|
|
187
|
+
// Architecture design keywords in intent or description
|
|
188
|
+
if (containsKeyword(taskIntent, ['design', 'architect', 'plan', 'propose']) ||
|
|
189
|
+
containsKeyword(taskDescription, ['design', 'architect', 'plan', 'propose'])) {
|
|
190
|
+
return 'high_entropy_disallowed';
|
|
191
|
+
}
|
|
192
|
+
// --- Step 3: Risk keyword detection (intent/description text) ---
|
|
193
|
+
// Check text-based risk signals even without explicit risk tools/files.
|
|
194
|
+
// Must run BEFORE editor eligibility to prevent destructive keywords
|
|
195
|
+
// (delete, remove, drop, etc.) from being misclassified as editor-eligible.
|
|
196
|
+
if (containsKeyword(taskIntent, RISK_KEYWORDS) ||
|
|
197
|
+
containsKeyword(taskDescription, RISK_KEYWORDS)) {
|
|
198
|
+
return 'risk_disallowed';
|
|
199
|
+
}
|
|
200
|
+
// --- Step 4: Risk tool detection ---
|
|
201
|
+
if (hasRiskTool(requestedTools ?? [])) {
|
|
202
|
+
return 'risk_disallowed';
|
|
203
|
+
}
|
|
204
|
+
// --- Step 5: Risk file pattern detection ---
|
|
205
|
+
if (hasRiskFile(requestedFiles ?? [])) {
|
|
206
|
+
return 'risk_disallowed';
|
|
207
|
+
}
|
|
208
|
+
// --- Step 6: Reader eligibility ---
|
|
209
|
+
// Task intent and description both clearly indicate reading/gathering
|
|
210
|
+
const intentIsReader = containsKeyword(taskIntent, READER_KEYWORDS);
|
|
211
|
+
const descIsReader = containsKeyword(taskDescription, READER_KEYWORDS);
|
|
212
|
+
if (intentIsReader && (descIsReader || !taskDescription)) {
|
|
213
|
+
return 'reader_eligible';
|
|
214
|
+
}
|
|
215
|
+
// --- Step 7: Editor eligibility ---
|
|
216
|
+
// Bounded scope: editing a known set of files (1-3) is editor-eligible.
|
|
217
|
+
// Large-scale multi-file editing (4+ distinct files) is inherently high-entropy
|
|
218
|
+
// — requires coordinating changes across a large surface area and carries
|
|
219
|
+
// significant unintended-change risk. Must stay on main agent.
|
|
220
|
+
const uniqueFiles = requestedFiles
|
|
221
|
+
? [...new Set(requestedFiles.filter((f) => f.trim().length > 0))]
|
|
222
|
+
: [];
|
|
223
|
+
const intentIsEditor = containsKeyword(taskIntent, EDITOR_KEYWORDS);
|
|
224
|
+
const descIsEditor = containsKeyword(taskDescription, EDITOR_KEYWORDS);
|
|
225
|
+
if (intentIsEditor && (descIsEditor || !taskDescription)) {
|
|
226
|
+
if (uniqueFiles.length >= 4) {
|
|
227
|
+
return 'high_entropy_disallowed';
|
|
228
|
+
}
|
|
229
|
+
return 'editor_eligible';
|
|
230
|
+
}
|
|
231
|
+
// --- Step 8: Ambiguous scope ---
|
|
232
|
+
// If we have some description but can't classify it clearly
|
|
233
|
+
if (taskDescription && taskDescription.trim().length > 0) {
|
|
234
|
+
const trimmed = taskDescription.trim();
|
|
235
|
+
// Very short or generic descriptions → ambiguous
|
|
236
|
+
if (trimmed.length < 20 || ['todo', 'fix', 'improve', 'change', 'update', 'something'].includes(trimmed.toLowerCase())) {
|
|
237
|
+
return 'ambiguous_scope';
|
|
238
|
+
}
|
|
239
|
+
// Contains question words suggesting open-ended reasoning
|
|
240
|
+
if (/\b(why|how|should|could|would|what if|should we|whether to)\b/i.test(trimmed)) {
|
|
241
|
+
return 'ambiguous_scope';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// --- Step 9: No sufficient information ---
|
|
245
|
+
if (!taskIntent && !taskDescription) {
|
|
246
|
+
return 'ambiguous_scope';
|
|
247
|
+
}
|
|
248
|
+
// Default to ambiguous rather than risky — fail to stay_main
|
|
249
|
+
return 'ambiguous_scope';
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Build the reason string for a given classification.
|
|
253
|
+
*/
|
|
254
|
+
function buildReason(classification, input) {
|
|
255
|
+
const { taskIntent, taskDescription } = input;
|
|
256
|
+
switch (classification) {
|
|
257
|
+
case 'reader_eligible':
|
|
258
|
+
return `Task "${taskIntent || taskDescription || '(unnamed)'}" is classified as reader_eligible. ` +
|
|
259
|
+
`Keywords indicate focused reading, inspection, or information retrieval. ` +
|
|
260
|
+
`No high-entropy or risk signals detected.`;
|
|
261
|
+
case 'editor_eligible':
|
|
262
|
+
return `Task "${taskIntent || taskDescription || '(unnamed)'}" is classified as editor_eligible. ` +
|
|
263
|
+
`Keywords indicate bounded editing, modification, or repair. ` +
|
|
264
|
+
`No high-entropy or risk signals detected.`;
|
|
265
|
+
case 'high_entropy_disallowed': {
|
|
266
|
+
const uniqueFiles = input.requestedFiles
|
|
267
|
+
? [...new Set(input.requestedFiles.filter((f) => f.trim().length > 0))]
|
|
268
|
+
: [];
|
|
269
|
+
const isLargeScaleEdit = uniqueFiles.length >= 4;
|
|
270
|
+
if (isLargeScaleEdit) {
|
|
271
|
+
return `Task "${taskIntent || taskDescription || '(unnamed)'}" is blocked as high_entropy_disallowed. ` +
|
|
272
|
+
`Editing ${uniqueFiles.length} files simultaneously exceeds the bounded-scope limit for local-editor. ` +
|
|
273
|
+
`Large-scale multi-file edits require the main agent's coordination and risk judgment.`;
|
|
274
|
+
}
|
|
275
|
+
return `Task "${taskIntent || taskDescription || '(unnamed)'}" is blocked as high_entropy_disallowed. ` +
|
|
276
|
+
`Keywords indicate open-ended planning, architecture design, or ambiguous multi-step work. ` +
|
|
277
|
+
`These tasks require the main agent's full reasoning capability.`;
|
|
278
|
+
}
|
|
279
|
+
case 'risk_disallowed':
|
|
280
|
+
return `Task "${taskIntent || taskDescription || '(unnamed)'}" is blocked as risk_disallowed. ` +
|
|
281
|
+
`Detected destructive, production, or irreversible operation signals. ` +
|
|
282
|
+
`All risky operations must remain on the main agent with human oversight.`;
|
|
283
|
+
case 'ambiguous_scope':
|
|
284
|
+
return `Task "${taskIntent || taskDescription || '(unnamed)'}" is blocked as ambiguous_scope. ` +
|
|
285
|
+
`The task description is too vague, too short, or contains open-ended question words. ` +
|
|
286
|
+
`Main agent must clarify scope before delegation.`;
|
|
287
|
+
case 'profile_mismatch':
|
|
288
|
+
return `Task profile does not match the requested target profile. ` +
|
|
289
|
+
`The task's natural classification is incompatible with the specified worker profile. ` +
|
|
290
|
+
`Main agent must re-route or choose a compatible profile.`;
|
|
291
|
+
case 'deployment_unavailable':
|
|
292
|
+
return `No enabled deployment available for routing. ` +
|
|
293
|
+
`Either no checkpoint is bound to the profile, or routing has been disabled. ` +
|
|
294
|
+
`Main agent must handle this task.`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Build the blockers list for a given classification.
|
|
299
|
+
*/
|
|
300
|
+
function buildBlockers(classification, input) {
|
|
301
|
+
switch (classification) {
|
|
302
|
+
case 'reader_eligible':
|
|
303
|
+
return [];
|
|
304
|
+
case 'editor_eligible':
|
|
305
|
+
return [];
|
|
306
|
+
case 'high_entropy_disallowed': {
|
|
307
|
+
const uniqueFiles = input.requestedFiles
|
|
308
|
+
? [...new Set(input.requestedFiles.filter((f) => f.trim().length > 0))]
|
|
309
|
+
: [];
|
|
310
|
+
const isLargeScaleEdit = uniqueFiles.length >= 4;
|
|
311
|
+
return [
|
|
312
|
+
isLargeScaleEdit
|
|
313
|
+
? `large-scale multi-file edit detected (${uniqueFiles.length} files): scope too broad for local-editor`
|
|
314
|
+
: 'task contains high-entropy keywords (design/plan/architect/investigate)',
|
|
315
|
+
'complexity hint indicates multi-step or open-ended work',
|
|
316
|
+
'main agent required for full reasoning and judgment',
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
case 'risk_disallowed':
|
|
320
|
+
return [
|
|
321
|
+
'risk tool requested (bash/exec/sudo/DROP/DELETE)',
|
|
322
|
+
'risk file pattern detected (production/secrets/.git/node_modules)',
|
|
323
|
+
'explicit risk signal present in input',
|
|
324
|
+
'main agent must supervise high-risk operations',
|
|
325
|
+
];
|
|
326
|
+
case 'ambiguous_scope':
|
|
327
|
+
return [
|
|
328
|
+
'task description too vague or generic',
|
|
329
|
+
'task intent not provided or unclear',
|
|
330
|
+
'open-ended question words detected',
|
|
331
|
+
'main agent must clarify scope before delegation',
|
|
332
|
+
];
|
|
333
|
+
case 'profile_mismatch':
|
|
334
|
+
return [
|
|
335
|
+
'task natural profile incompatible with requested target profile',
|
|
336
|
+
'main agent must re-route or select a compatible profile',
|
|
337
|
+
];
|
|
338
|
+
case 'deployment_unavailable':
|
|
339
|
+
return [
|
|
340
|
+
'no enabled deployment found for target profile',
|
|
341
|
+
'routing may be disabled in deployment registry',
|
|
342
|
+
'main agent must handle task directly',
|
|
343
|
+
];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Public API
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
/**
|
|
350
|
+
* Classify a task and produce a routing decision.
|
|
351
|
+
*
|
|
352
|
+
* This is the main entry point for routing policy evaluation.
|
|
353
|
+
* It:
|
|
354
|
+
* 1. Classifies the task kind based on keywords and heuristics
|
|
355
|
+
* 2. Checks deployment availability for the target profile
|
|
356
|
+
* 3. Returns a fully explainable RoutingDecision
|
|
357
|
+
*
|
|
358
|
+
* @param input - The routing input describing the task
|
|
359
|
+
* @param stateDir - Workspace state directory (for deployment registry lookup)
|
|
360
|
+
* @returns RoutingDecision with classification, reason, blockers, and routing verdict
|
|
361
|
+
*/
|
|
362
|
+
export function classifyTask(input, stateDir) {
|
|
363
|
+
// --- Determine the raw task classification ---
|
|
364
|
+
const classification = classifyTaskKind(input);
|
|
365
|
+
// --- Determine the target profile ---
|
|
366
|
+
// If input specifies a target, use it. Otherwise, pick based on classification.
|
|
367
|
+
// NOTE: When explicitly specified, we must validate profile-task compatibility below.
|
|
368
|
+
const targetProfile = input.targetProfile ??
|
|
369
|
+
(classification === 'reader_eligible'
|
|
370
|
+
? 'local-reader'
|
|
371
|
+
: classification === 'editor_eligible'
|
|
372
|
+
? 'local-editor'
|
|
373
|
+
: null);
|
|
374
|
+
// --- Profile-task compatibility check ---
|
|
375
|
+
// Only applies when input.targetProfile is EXPLICITLY set.
|
|
376
|
+
// When auto-derived (input.targetProfile is null), compatibility is already
|
|
377
|
+
// guaranteed by the auto-derivation logic above (reader_eligible → local-reader).
|
|
378
|
+
// This check prevents routing a reader task to an editor profile (or vice versa)
|
|
379
|
+
// when the caller explicitly requests the wrong profile.
|
|
380
|
+
const isProfileCompatible = input.targetProfile === undefined
|
|
381
|
+
? true // Auto-derived profile is always compatible by construction
|
|
382
|
+
: targetProfile === 'local-reader'
|
|
383
|
+
? classification === 'reader_eligible'
|
|
384
|
+
: targetProfile === 'local-editor'
|
|
385
|
+
? classification === 'editor_eligible'
|
|
386
|
+
: false;
|
|
387
|
+
// --- Deployment availability check ---
|
|
388
|
+
let deploymentCheck = {
|
|
389
|
+
performed: false,
|
|
390
|
+
profileAvailable: false,
|
|
391
|
+
routingEnabled: false,
|
|
392
|
+
checkpointDeployable: false,
|
|
393
|
+
};
|
|
394
|
+
if (targetProfile) {
|
|
395
|
+
const deployment = getDeployment(stateDir, targetProfile);
|
|
396
|
+
const activeCheckpointId = deployment?.activeCheckpointId ?? null;
|
|
397
|
+
// Re-check deployability on every routing decision — a checkpoint may have been revoked
|
|
398
|
+
const checkpointDeployable = activeCheckpointId
|
|
399
|
+
? isCheckpointDeployable(stateDir, activeCheckpointId)
|
|
400
|
+
: false;
|
|
401
|
+
deploymentCheck = {
|
|
402
|
+
performed: true,
|
|
403
|
+
profileAvailable: deployment !== null,
|
|
404
|
+
routingEnabled: isRoutingEnabledForProfile(stateDir, targetProfile),
|
|
405
|
+
checkpointDeployable,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
// --- Build the decision ---
|
|
409
|
+
const blockers = buildBlockers(classification, input);
|
|
410
|
+
const reason = buildReason(classification, input);
|
|
411
|
+
// FAIL-CLOSED: route_local only if:
|
|
412
|
+
// 1. Classification is eligible (reader_eligible or editor_eligible)
|
|
413
|
+
// 2. A target profile was identified
|
|
414
|
+
// 3. The task's natural profile is compatible with the target profile
|
|
415
|
+
// 4. Deployment is available and routing is enabled
|
|
416
|
+
const isEligibleForRouting = (classification === 'reader_eligible' || classification === 'editor_eligible') &&
|
|
417
|
+
targetProfile !== null &&
|
|
418
|
+
isProfileCompatible &&
|
|
419
|
+
deploymentCheck.routingEnabled;
|
|
420
|
+
const decision = isEligibleForRouting
|
|
421
|
+
? 'route_local'
|
|
422
|
+
: 'stay_main';
|
|
423
|
+
// Derive the final classification — preserves the root cause of stay_main:
|
|
424
|
+
// - profile_mismatch: task would be eligible but wrong profile requested
|
|
425
|
+
// - deployment_unavailable: eligible and compatible but no routing enabled
|
|
426
|
+
// - raw classification: blocked by high_entropy / risk / ambiguous
|
|
427
|
+
const isEligible = classification === 'reader_eligible' || classification === 'editor_eligible';
|
|
428
|
+
const finalClassification = isEligibleForRouting
|
|
429
|
+
? classification
|
|
430
|
+
: isEligible && targetProfile !== null && !isProfileCompatible
|
|
431
|
+
? 'profile_mismatch'
|
|
432
|
+
: isEligible
|
|
433
|
+
? 'deployment_unavailable'
|
|
434
|
+
: classification;
|
|
435
|
+
// Build explainability fields specific to the stay_main reason
|
|
436
|
+
let finalReason = reason;
|
|
437
|
+
let finalBlockers = blockers;
|
|
438
|
+
if (decision === 'stay_main') {
|
|
439
|
+
if (finalClassification === 'profile_mismatch') {
|
|
440
|
+
const wanted = classification === 'reader_eligible' ? 'local-reader' : 'local-editor';
|
|
441
|
+
finalReason = `Task is ${classification} but was explicitly targeted at ${targetProfile}. ` +
|
|
442
|
+
`Routing requires "${wanted}" profile. Ensure the task intent matches the requested profile.`;
|
|
443
|
+
finalBlockers = [
|
|
444
|
+
`profile mismatch: task is ${classification} but targetProfile is ${targetProfile}`,
|
|
445
|
+
`required profile: ${wanted}`,
|
|
446
|
+
];
|
|
447
|
+
}
|
|
448
|
+
else if (finalClassification === 'deployment_unavailable') {
|
|
449
|
+
if (!deploymentCheck.performed) {
|
|
450
|
+
finalReason = reason;
|
|
451
|
+
}
|
|
452
|
+
else if (!deploymentCheck.profileAvailable) {
|
|
453
|
+
finalReason = `Task is ${classification} but no deployment exists for ${targetProfile}. ` +
|
|
454
|
+
`Bind a checkpoint via bindCheckpointToWorkerProfile() and enable routing.`;
|
|
455
|
+
finalBlockers = [`no deployment found for profile: ${targetProfile}`];
|
|
456
|
+
}
|
|
457
|
+
else if (!deploymentCheck.checkpointDeployable) {
|
|
458
|
+
finalReason = `Task is ${classification} but the active checkpoint has been revoked (no longer deployable). ` +
|
|
459
|
+
`Re-bind a passing checkpoint or re-evaluate the current one.`;
|
|
460
|
+
finalBlockers = [
|
|
461
|
+
`active checkpoint is no longer deployable: ${targetProfile}`,
|
|
462
|
+
'revoked checkpoints must not be used for routing',
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
else if (!deploymentCheck.routingEnabled) {
|
|
466
|
+
finalReason = `Task is ${classification} and deployment exists for ${targetProfile} but routing is not enabled. ` +
|
|
467
|
+
`Enable routing via enableRoutingForProfile() in the deployment registry.`;
|
|
468
|
+
finalBlockers = [`routing is disabled for profile: ${targetProfile}`];
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// --- Get active checkpoint ID and state for shadow observation integration ---
|
|
473
|
+
let activeCheckpointId = null;
|
|
474
|
+
let activeCheckpointState = null;
|
|
475
|
+
if (targetProfile && deploymentCheck.performed) {
|
|
476
|
+
const deployment = getDeployment(stateDir, targetProfile);
|
|
477
|
+
activeCheckpointId = deployment?.activeCheckpointId ?? null;
|
|
478
|
+
if (activeCheckpointId) {
|
|
479
|
+
const promotionState = getPromotionState(stateDir, activeCheckpointId);
|
|
480
|
+
if (promotionState === 'shadow_ready' || promotionState === 'promotable' || promotionState === 'candidate_only') {
|
|
481
|
+
activeCheckpointState = promotionState;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
decision,
|
|
487
|
+
targetProfile: decision === 'route_local' ? targetProfile : null,
|
|
488
|
+
classification: finalClassification,
|
|
489
|
+
reason: finalReason,
|
|
490
|
+
blockers: decision === 'stay_main' ? finalBlockers : [],
|
|
491
|
+
deploymentCheck,
|
|
492
|
+
activeCheckpointId,
|
|
493
|
+
activeCheckpointState: activeCheckpointState ?? undefined,
|
|
494
|
+
shadowObservationId: undefined,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Convenience: check if a specific profile can handle a task.
|
|
499
|
+
* Equivalent to calling classifyTask with targetProfile set.
|
|
500
|
+
*/
|
|
501
|
+
export function canRouteToProfile(input, stateDir, profile) {
|
|
502
|
+
const decision = classifyTask({ ...input, targetProfile: profile }, stateDir);
|
|
503
|
+
return decision.decision === 'route_local';
|
|
504
|
+
}
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Read-Only Query Helpers
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
/**
|
|
509
|
+
* Check if any local worker routing is currently enabled for any profile.
|
|
510
|
+
*/
|
|
511
|
+
export function isAnyLocalRoutingEnabled(stateDir) {
|
|
512
|
+
return isRoutingEnabledForProfile(stateDir, 'local-reader') ||
|
|
513
|
+
isRoutingEnabledForProfile(stateDir, 'local-editor');
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* List all profiles that currently have routing enabled.
|
|
517
|
+
*/
|
|
518
|
+
export function listEnabledProfiles(stateDir) {
|
|
519
|
+
const enabled = [];
|
|
520
|
+
if (isRoutingEnabledForProfile(stateDir, 'local-reader'))
|
|
521
|
+
enabled.push('local-reader');
|
|
522
|
+
if (isRoutingEnabledForProfile(stateDir, 'local-editor'))
|
|
523
|
+
enabled.push('local-editor');
|
|
524
|
+
return enabled;
|
|
525
|
+
}
|