ai-maestro 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +15 -0
  3. package/README.md +727 -0
  4. package/bin/maestro.mjs +246 -0
  5. package/package.json +29 -0
  6. package/src/checkpoint.mjs +102 -0
  7. package/src/codex-diagnostics.mjs +682 -0
  8. package/src/codex-home-inspect.mjs +252 -0
  9. package/src/codex-home.mjs +258 -0
  10. package/src/commands.mjs +809 -0
  11. package/src/config.mjs +11 -0
  12. package/src/debug.mjs +164 -0
  13. package/src/encoding-guard.mjs +125 -0
  14. package/src/engines/ai-router-engine.mjs +37 -0
  15. package/src/engines/codex-engine.mjs +21 -0
  16. package/src/engines/engine.mjs +21 -0
  17. package/src/engines/registry.mjs +29 -0
  18. package/src/files.mjs +65 -0
  19. package/src/format.mjs +132 -0
  20. package/src/lock.mjs +439 -0
  21. package/src/memory-log.mjs +18 -0
  22. package/src/memory.mjs +1 -0
  23. package/src/orchestration/attempt-chain-runtime.mjs +627 -0
  24. package/src/orchestration/budget-manager.mjs +121 -0
  25. package/src/orchestration/capability-registry.mjs +108 -0
  26. package/src/orchestration/consolidator.mjs +772 -0
  27. package/src/orchestration/delegation-executor.mjs +262 -0
  28. package/src/orchestration/delegation-manager.mjs +116 -0
  29. package/src/orchestration/deployment-intent.mjs +36 -0
  30. package/src/orchestration/engine-history.mjs +110 -0
  31. package/src/orchestration/engine-policy.mjs +73 -0
  32. package/src/orchestration/engine-selector.mjs +187 -0
  33. package/src/orchestration/execution-context.mjs +45 -0
  34. package/src/orchestration/failure-classifier.mjs +136 -0
  35. package/src/orchestration/failure-evidence.mjs +237 -0
  36. package/src/orchestration/fallback-chain-lock.mjs +217 -0
  37. package/src/orchestration/fallback-chain-trail.mjs +218 -0
  38. package/src/orchestration/fallback-executor.mjs +146 -0
  39. package/src/orchestration/fallback-graph.mjs +106 -0
  40. package/src/orchestration/fallback-recommendation.mjs +73 -0
  41. package/src/orchestration/file-conflict-detector.mjs +126 -0
  42. package/src/orchestration/host-validation.mjs +241 -0
  43. package/src/orchestration/orchestration-loop.mjs +1971 -0
  44. package/src/orchestration/orchestration-runtime.mjs +1019 -0
  45. package/src/orchestration/orchestration-scheduler.mjs +135 -0
  46. package/src/orchestration/orchestration-trail.mjs +192 -0
  47. package/src/orchestration/preflight.mjs +212 -0
  48. package/src/orchestration/provider-health.mjs +566 -0
  49. package/src/orchestration/provider-router.mjs +352 -0
  50. package/src/orchestration/rc-check-adapters.mjs +817 -0
  51. package/src/orchestration/rc-check.mjs +544 -0
  52. package/src/orchestration/rc-policy.mjs +200 -0
  53. package/src/orchestration/runtime-gate.mjs +146 -0
  54. package/src/orchestration/sector-managers.mjs +215 -0
  55. package/src/orchestration/self-hosting-canary.mjs +231 -0
  56. package/src/orchestration/self-hosting-readiness.mjs +244 -0
  57. package/src/orchestration/spec-planner.mjs +877 -0
  58. package/src/orchestration/subtask-planner.mjs +176 -0
  59. package/src/orchestration/task-classifier.mjs +241 -0
  60. package/src/orchestration/verifier.mjs +1608 -0
  61. package/src/orchestration-commands.mjs +279 -0
  62. package/src/planner.mjs +38 -0
  63. package/src/project.mjs +1 -0
  64. package/src/run-diagnostics.mjs +641 -0
  65. package/src/runner.mjs +243 -0
  66. package/src/safety.mjs +116 -0
  67. package/src/schema.mjs +371 -0
  68. package/src/session-commands.mjs +521 -0
  69. package/src/shell.mjs +93 -0
  70. package/src/smart-planner.mjs +249 -0
  71. package/src/task-commands.mjs +1182 -0
  72. package/src/tasks.mjs +134 -0
  73. package/src/ui/commands.mjs +76 -0
  74. package/src/ui/events.mjs +45 -0
  75. package/src/ui/public/app.js +600 -0
  76. package/src/ui/public/index.html +88 -0
  77. package/src/ui/public/style.css +460 -0
  78. package/src/ui/server.mjs +112 -0
  79. package/src/ui/state.mjs +504 -0
  80. package/src/usage.mjs +178 -0
  81. package/src/workspace-diff.mjs +228 -0
@@ -0,0 +1,176 @@
1
+ // src/orchestration/subtask-planner.mjs
2
+ // Responsabilidade: Gerar subtarefas estruturadas a partir de uma task complexa.
3
+
4
+ import { classifyTask, RISK_LEVELS } from './task-classifier.mjs';
5
+ import { planSectorWork, SECTOR_MANAGERS } from './sector-managers.mjs';
6
+
7
+ /**
8
+ * Gera subtarefas estruturadas a partir de uma task complexa.
9
+ * @param {object} input - Objeto de entrada contendo a task e contexto.
10
+ * @param {object} input.task - A tarefa a ser planejada.
11
+ * @param {object} input.classification - Classificação prévia da tarefa.
12
+ * @param {object} input.manager - O gerente de setor, se aplicável.
13
+ * @param {object} input.projectContext - Contexto geral do projeto.
14
+ * @param {object} input.constraints - Limites para a criação de subtarefas.
15
+ * @param {number} input.constraints.maxSubtasks - Número máximo de subtarefas permitidas.
16
+ * @param {number} input.constraints.maxDepth - Profundidade máxima de delegação.
17
+ * @returns {object} Um plano de subtarefas.
18
+ */
19
+ export function planSubtasks(input) {
20
+ const { task, classification, constraints } = input;
21
+ const maxSubtasks = constraints.maxSubtasks || 8; // Default from spec
22
+ const maxDepth = constraints.maxDepth || 2; // Default from spec
23
+
24
+ if (classification.complexity < 3) {
25
+ return {
26
+ canSplit: false,
27
+ reason: ['Task é muito simples para ser dividida em subtarefas.'],
28
+ subtasks: [],
29
+ warnings: []
30
+ };
31
+ }
32
+
33
+ if (classification.risk === 'critical' && classification.requires.reasoning >= 4) {
34
+ return {
35
+ canSplit: false,
36
+ reason: ['Task de risco crítico e alta complexidade requer intervenção humana.'],
37
+ subtasks: [],
38
+ requiresHuman: true,
39
+ warnings: []
40
+ };
41
+ }
42
+
43
+ const { decision, subtasks: generatedSubtasks, reason: sectorReason } = planSectorWork(task, classification);
44
+
45
+ if (decision === 'no-manager' || decision === 'execute-directly') {
46
+ return {
47
+ canSplit: false,
48
+ reason: [`Nenhum gerente de setor aplicável ou gerente decide executar diretamente: ${sectorReason}`],
49
+ subtasks: [],
50
+ warnings: []
51
+ };
52
+ }
53
+
54
+ const subtasks = generatedSubtasks.slice(0, maxSubtasks).map((subtask, index) => ({
55
+ id: `subtask-${task.id || 't'}-${index + 1}`,
56
+ title: subtask.title,
57
+ taskKind: subtask.taskKind,
58
+ description: subtask.description,
59
+ requires: classifyTask(subtask).requires, // Classify each subtask to get its requirements
60
+ expectedFiles: subtask.files || [],
61
+ blockedBy: [],
62
+ parallelizable: false, // Default per spec for v0.5.2
63
+ risk: classifyTask(subtask).risk, // Classify each subtask to get its risk
64
+ verificationRequired: true
65
+ }));
66
+
67
+ const warnings = [];
68
+ if (generatedSubtasks.length > maxSubtasks) {
69
+ warnings.push(`O número de subtarefas geradas (${generatedSubtasks.length}) excedeu o limite (${maxSubtasks}). Apenas as primeiras ${maxSubtasks} foram incluídas.`);
70
+ }
71
+
72
+ return {
73
+ canSplit: subtasks.length > 0,
74
+ reason: [`Subtarefas geradas com sucesso. ${sectorReason}`],
75
+ subtasks: subtasks,
76
+ warnings: warnings
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Valida um plano de subtarefas gerado.
82
+ * @param {object} plan - O plano de subtarefas a ser validado.
83
+ * @returns {object} Objeto com status de validade e lista de erros/avisos.
84
+ */
85
+ export function validateSubtaskPlan(plan) {
86
+ const errors = [];
87
+ const warnings = [];
88
+
89
+ if (!plan || typeof plan !== 'object') {
90
+ errors.push('Plano inválido: objeto nulo ou não é um objeto.');
91
+ return { isValid: false, errors, warnings };
92
+ }
93
+
94
+ if (typeof plan.canSplit !== 'boolean') {
95
+ errors.push('Campo canSplit é obrigatório e deve ser um booleano.');
96
+ }
97
+
98
+ if (!Array.isArray(plan.reason) || plan.reason.some(r => typeof r !== 'string')) {
99
+ errors.push('Campo reason é obrigatório e deve ser um array de strings.');
100
+ }
101
+
102
+ if (!Array.isArray(plan.subtasks)) {
103
+ errors.push('Campo subtasks é obrigatório e deve ser um array.');
104
+ } else {
105
+ plan.subtasks.forEach((subtask, index) => {
106
+ if (typeof subtask !== 'object' || subtask === null) {
107
+ errors.push(`Subtarefa ${index} inválida: não é um objeto.`);
108
+ return;
109
+ }
110
+
111
+ if (!subtask.id || typeof subtask.id !== 'string') {
112
+ errors.push(`Subtarefa ${index}: ID é obrigatório e deve ser uma string.`);
113
+ }
114
+ if (!subtask.title || typeof subtask.title !== 'string') {
115
+ errors.push(`Subtarefa ${index}: title é obrigatório e deve ser uma string.`);
116
+ }
117
+ if (!subtask.taskKind || typeof subtask.taskKind !== 'string') {
118
+ errors.push(`Subtarefa ${index}: taskKind é obrigatório e deve ser uma string.`);
119
+ }
120
+ if (typeof subtask.description !== 'string') {
121
+ errors.push(`Subtarefa ${index}: description é obrigatório e deve ser uma string.`);
122
+ }
123
+ if (!subtask.requires || typeof subtask.requires !== 'object') {
124
+ errors.push(`Subtarefa ${index}: requires é obrigatório e deve ser um objeto.`);
125
+ }
126
+ if (!Array.isArray(subtask.expectedFiles)) {
127
+ errors.push(`Subtarefa ${index}: expectedFiles é obrigatório e deve ser um array.`);
128
+ }
129
+ if (!Array.isArray(subtask.blockedBy)) {
130
+ errors.push(`Subtarefa ${index}: blockedBy é obrigatório e deve ser um array.`);
131
+ }
132
+ if (typeof subtask.parallelizable !== 'boolean') {
133
+ errors.push(`Subtarefa ${index}: parallelizable é obrigatório e deve ser um booleano.`);
134
+ }
135
+ if (!RISK_LEVELS.includes(subtask.risk)) {
136
+ errors.push(`Subtarefa ${index}: risk inválido. Deve ser um de ${RISK_LEVELS.join(', ')}.`);
137
+ }
138
+ if (typeof subtask.verificationRequired !== 'boolean') {
139
+ errors.push(`Subtarefa ${index}: verificationRequired é obrigatório e deve ser um booleano.`);
140
+ }
141
+ });
142
+ }
143
+
144
+ return { isValid: errors.length === 0, errors, warnings };
145
+ }
146
+
147
+ /**
148
+ * Gera um resumo legível de um plano de subtarefas.
149
+ * @param {object} plan - O plano de subtarefas a ser resumido.
150
+ * @returns {string} Um resumo textual do plano.
151
+ */
152
+ export function summarizeSubtaskPlan(plan) {
153
+ if (!plan || !plan.canSplit) {
154
+ return `Plano de subtarefas: Não é possível dividir a tarefa. Razão: ${plan?.reason?.[0] || 'Desconhecida'}`;
155
+ }
156
+
157
+ let summary = `Plano de Subtarefas:\n`;
158
+ summary += `- Razão: ${plan.reason.join(' ')}\n`;
159
+ summary += `- Total de Subtarefas: ${plan.subtasks.length}\n`;
160
+
161
+ plan.subtasks.forEach((subtask, index) => {
162
+ summary += ` ${index + 1}. [${subtask.taskKind}] ${subtask.title} (Risco: ${subtask.risk}, Verificação: ${subtask.verificationRequired ? 'Sim' : 'Não'})\n`;
163
+ if (subtask.expectedFiles && subtask.expectedFiles.length > 0) {
164
+ summary += ` Arquivos Esperados: ${subtask.expectedFiles.join(', ')}\n`;
165
+ }
166
+ });
167
+
168
+ if (plan.warnings && plan.warnings.length > 0) {
169
+ summary += `\nAvisos:\n`;
170
+ plan.warnings.forEach(warning => {
171
+ summary += `- ${warning}\n`;
172
+ });
173
+ }
174
+
175
+ return summary;
176
+ }
@@ -0,0 +1,241 @@
1
+ import { stripAccents } from '../smart-planner.mjs';
2
+ import { detectAffirmativeDeploymentIntent } from './deployment-intent.mjs';
3
+
4
+ export const TASK_KINDS = [
5
+ 'architecture', 'planning', 'frontend', 'backend', 'database', 'testing',
6
+ 'debugging', 'security-review', 'code-review', 'documentation', 'research',
7
+ 'file-inspection', 'simple-edit', 'data-transformation', 'image-analysis',
8
+ 'deployment', 'memory-update', 'project-management'
9
+ ];
10
+
11
+ export const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
12
+
13
+ // Keyword sets are intentionally bilingual (pt-BR + en) to match this
14
+ // project's existing smart-planner.mjs convention. Each entry is a plain
15
+ // (accent-stripped, lowercased) regex source, not a whole-word requirement,
16
+ // mirroring smart-planner.mjs's own matching style.
17
+ const KIND_PATTERNS = {
18
+ architecture: /\barquitetur|\bdesign do sistema|\bsystem design|\brefactor.*modul|\bcritical architecture/,
19
+ security: /\bseguranc|\bsecurity|\bvulnerab|\bexploit|\binjection|\bxss|\bcsrf|\bauth\b|\bcredencia|\bcredential|\bsecret/,
20
+ codeReview: /\brevis[aã]o de codigo|\bcode review|\bpull request|\brevisar (o )?codigo/,
21
+ review: /\brevis[aã]o|\breview\b|\bauditoria|\baudit\b/,
22
+ // Bare \bschema\b used to fire on "maestro schema-check" (a CLI verb,
23
+ // e.g. in a validation-step acceptance item), misclassifying an ordinary
24
+ // validation task as database/high-risk. The negative lookahead excludes
25
+ // "schema-check"/"schema check" specifically while still matching real
26
+ // database-schema mentions ("revisar o schema do banco", "database
27
+ // schema migration").
28
+ database: /\bbanco de dados|\bdatabase\b|\bsql\b|\bmigra(c|ç)[aã]o|\bmigration\b|\bschema\b(?![\s-]*check)|\bquery\b|\bpostgres|\bmysql|\bsqlite|\bmongo/,
29
+ testing: /\bteste|\btest\b|\bunit test|\bintegration test|\be2e\b|\bcobertura|\bcoverage/,
30
+ debugging: /\bbug|\berro\b|\berror\b|\bfalha|\bquebrado|\btravando|\bnao funciona|\bstack ?trace|\bdebug/,
31
+ frontend: /\bfrontend|\bfront-end|\bui\b|\binterface|\bbotao|\bbotões|\bbutton|\bcomponente|\bcomponent\b|\bcss\b|\bhtml\b|\breact\b|\bvue\b|\btela\b|\bpainel\b/,
32
+ backend: /\bbackend|\bback-end|\bapi\b|\bendpoint|\bservidor|\bserver\b|\brota\b|\broute\b|\bcontroller\b/,
33
+ documentation: /\bdocumenta[cç][aã]o|\bdocumentation\b|\breadme\b|\bhandoff\b|\bchangelog\b/,
34
+ research: /\bpesquisa|\bresearch\b|\binvestigar|\binvestigate\b|\bcomparar|\bcompare\b|\bavaliar op[cç][oõ]es/,
35
+ fileInspection: /\bmapear|\banalisar\b|\bidentificar\b|\blocalizar\b|\bdiagnosticar\b|\blistar\b|\bexplorar estrutura|\bmap the structure/,
36
+ dataTransformation: /\btransforma(c|ç)[aã]o de dados|\bdata transformation|\bconverter\b|\bconvert\b.*(csv|json|xml)|\bparser\b|\betl\b/,
37
+ imageAnalysis: /\bimagem\b|\bimage\b|\bscreenshot|\bvis[aã]o computacional|\bcomputer vision|\bocr\b|\banalisar (a )?foto/,
38
+ // Bare \bdeploy\b (and \bpublicar em produ) used to fire on negated
39
+ // acceptance criteria like "Sem deploy" -- detectAffirmativeDeploymentIntent
40
+ // rejects any deploy/publicar/implantar match that shares a clause with a
41
+ // negation or exclusion cue, in either word order. \brelease\b / ci-cd
42
+ // keywords are left as direct signals since they weren't part of the
43
+ // reported false-positive.
44
+ deployment: { test: text => /\brelease\b|\bci\/cd|\bpipeline de deploy/.test(text) || detectAffirmativeDeploymentIntent(text) },
45
+ memoryUpdate: /\bmemoria\b|\bmemory\b|\bcheckpoint\b|\b\.memory\//,
46
+ projectManagement: /\bplanejamento\b|\bplan\b|\bescopo\b|\bscope\b|\bbacklog\b|\bpriorizar\b|\bprioritize\b|\broadmap\b/,
47
+ simpleEdit: /\bcorrigir typo|\bfix typo|\brenomear\b|\brename\b|\bpequen[oa] ajuste|\bsmall fix|\bone[- ]line/
48
+ };
49
+
50
+ const CATEGORY_ORDER = [
51
+ ['architecture', KIND_PATTERNS.architecture],
52
+ ['security-review', KIND_PATTERNS.security],
53
+ ['code-review', KIND_PATTERNS.codeReview],
54
+ ['database', KIND_PATTERNS.database],
55
+ ['testing', KIND_PATTERNS.testing],
56
+ ['debugging', KIND_PATTERNS.debugging],
57
+ ['deployment', KIND_PATTERNS.deployment],
58
+ ['memory-update', KIND_PATTERNS.memoryUpdate],
59
+ ['documentation', KIND_PATTERNS.documentation],
60
+ ['data-transformation', KIND_PATTERNS.dataTransformation],
61
+ ['image-analysis', KIND_PATTERNS.imageAnalysis],
62
+ ['frontend', KIND_PATTERNS.frontend],
63
+ ['backend', KIND_PATTERNS.backend],
64
+ ['research', KIND_PATTERNS.research],
65
+ ['file-inspection', KIND_PATTERNS.fileInspection],
66
+ ['project-management', KIND_PATTERNS.projectManagement],
67
+ ['simple-edit', KIND_PATTERNS.simpleEdit],
68
+ // generic 'review' is checked last among the review-ish patterns so the
69
+ // more specific 'code-review'/'security-review' matches win first.
70
+ ['code-review', KIND_PATTERNS.review]
71
+ ];
72
+
73
+ export function buildCombinedText(task) {
74
+ const parts = [
75
+ task.title || '',
76
+ task.description || '',
77
+ ...(Array.isArray(task.acceptance) ? task.acceptance : []),
78
+ task.requestKind || ''
79
+ ];
80
+ return stripAccents(parts.join(' ').toLowerCase());
81
+ }
82
+
83
+ function detectImageFiles(files = []) {
84
+ return files.some(file => /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(String(file)));
85
+ }
86
+
87
+ function detectTestFiles(files = []) {
88
+ return files.some(file => /\.(test|spec)\.[a-z]+$/i.test(String(file)) || /(^|\/)tests?\//i.test(String(file)));
89
+ }
90
+
91
+ function detectDbFiles(files = []) {
92
+ return files.some(file => /\.(sql)$/i.test(String(file)) || /migrations?\//i.test(String(file)));
93
+ }
94
+
95
+ function matchesPattern(pattern, text) {
96
+ return typeof pattern.test === 'function' && pattern.test(text);
97
+ }
98
+
99
+ function classifyByTitle(task) {
100
+ const titleText = stripAccents(String(task.title || '').toLowerCase());
101
+ if (!titleText) {
102
+ return null;
103
+ }
104
+ const matches = [];
105
+ for (const [kind, pattern] of CATEGORY_ORDER) {
106
+ if (matchesPattern(pattern, titleText)) {
107
+ matches.push(kind);
108
+ }
109
+ }
110
+ const uniqueMatches = [...new Set(matches)];
111
+ return uniqueMatches.length === 1 ? uniqueMatches[0] : null;
112
+ }
113
+
114
+ export function classifyTaskKind(task) {
115
+ const text = buildCombinedText(task);
116
+ const files = Array.isArray(task.files) ? task.files : [];
117
+
118
+ if (detectImageFiles(files)) {
119
+ return 'image-analysis';
120
+ }
121
+ if (detectDbFiles(files)) {
122
+ return 'database';
123
+ }
124
+ if (detectTestFiles(files)) {
125
+ return 'testing';
126
+ }
127
+
128
+ const titleKind = classifyByTitle(task);
129
+ if (titleKind) {
130
+ return titleKind;
131
+ }
132
+
133
+ for (const [kind, pattern] of CATEGORY_ORDER) {
134
+ if (matchesPattern(pattern, text)) {
135
+ return kind;
136
+ }
137
+ }
138
+
139
+ return 'simple-edit';
140
+ }
141
+
142
+ function estimateComplexity(task, kind) {
143
+ const text = buildCombinedText(task);
144
+ const files = Array.isArray(task.files) ? task.files : [];
145
+ let score = 1;
146
+
147
+ const HIGH_COMPLEXITY_KINDS = new Set(['architecture', 'security-review', 'database', 'deployment']);
148
+ const MEDIUM_COMPLEXITY_KINDS = new Set(['backend', 'frontend', 'testing', 'debugging', 'data-transformation', 'code-review']);
149
+
150
+ if (HIGH_COMPLEXITY_KINDS.has(kind)) {
151
+ score = 4;
152
+ } else if (MEDIUM_COMPLEXITY_KINDS.has(kind)) {
153
+ score = 3;
154
+ } else if (kind === 'file-inspection' || kind === 'research' || kind === 'documentation' || kind === 'memory-update' || kind === 'project-management') {
155
+ score = 2;
156
+ } else {
157
+ score = 1;
158
+ }
159
+
160
+ if (/\bcritic[ao]\b|\bcritical\b|\barquitetura critica|\bcritical architecture|\bfinal review\b/.test(text)) {
161
+ score = 5;
162
+ }
163
+
164
+ if (files.length > 5) {
165
+ score = Math.min(5, score + 1);
166
+ }
167
+
168
+ if (/\bmulti[- ]?arquivo|\bmultiple files\b|\bsistema inteiro\b|\bwhole system\b/.test(text)) {
169
+ score = Math.min(5, score + 1);
170
+ }
171
+
172
+ return Math.max(1, Math.min(5, score));
173
+ }
174
+
175
+ function estimateRisk(task, kind, complexity) {
176
+ const text = buildCombinedText(task);
177
+ const CRITICAL_KINDS = new Set(['security-review', 'deployment']);
178
+ const HIGH_KINDS = new Set(['database', 'architecture']);
179
+
180
+ // \bdeploy\b.*prod used to fire on negated text ("nao fazer deploy em
181
+ // producao" still contains "deploy" followed by "prod"); require an
182
+ // affirmative (non-negated) deploy intent plus a production mention.
183
+ const affirmativeProdDeploy = /\bprod/.test(text) && detectAffirmativeDeploymentIntent(text);
184
+ if (CRITICAL_KINDS.has(kind) || complexity >= 5 || /\bdrop table|\btruncate\b|\bforce push|\breset --hard/.test(text) || affirmativeProdDeploy) {
185
+ return 'critical';
186
+ }
187
+ if (HIGH_KINDS.has(kind) || complexity >= 4) {
188
+ return 'high';
189
+ }
190
+ if (complexity === 3 || kind === 'debugging' || kind === 'backend' || kind === 'code-review') {
191
+ return 'medium';
192
+ }
193
+ return 'low';
194
+ }
195
+
196
+ function estimateRequirements(task, kind, complexity) {
197
+ const files = Array.isArray(task.files) ? task.files : [];
198
+ const analysisOnlyKinds = new Set(['research', 'file-inspection', 'documentation', 'project-management', 'memory-update']);
199
+ const needsFsWrite = !analysisOnlyKinds.has(kind);
200
+ const needsShell = kind !== 'research' && kind !== 'documentation';
201
+ const needsTestExecution = kind === 'testing' || kind === 'debugging' || kind === 'backend' || kind === 'database';
202
+ const needsVision = kind === 'image-analysis' || detectImageFiles(files);
203
+
204
+ return {
205
+ reasoning: Math.min(5, Math.max(1, complexity)),
206
+ coding: analysisOnlyKinds.has(kind) ? 1 : Math.min(5, Math.max(2, complexity)),
207
+ shell: needsShell,
208
+ filesystemWrite: needsFsWrite,
209
+ browser: false,
210
+ vision: needsVision,
211
+ testExecution: needsTestExecution,
212
+ minimumContextTokens: complexity >= 4 ? 32000 : (complexity === 3 ? 16000 : 8000)
213
+ };
214
+ }
215
+
216
+ export function classifyTask(task, context = {}) {
217
+ const kind = classifyTaskKind(task);
218
+ const complexity = estimateComplexity(task, kind);
219
+ const risk = estimateRisk(task, kind, complexity);
220
+ const requires = estimateRequirements(task, kind, complexity);
221
+
222
+ return {
223
+ taskKind: kind,
224
+ complexity,
225
+ risk,
226
+ requires,
227
+ // Delegation to a sector manager only makes sense once there's real
228
+ // decomposition benefit (Phase 8 rule: complexity >= 3).
229
+ canDelegate: complexity >= 3,
230
+ // v0.5.0 deliberately never marks a task parallelizable: the delegation
231
+ // manager (Phase 9) only supports serial subagent execution this
232
+ // release, so nothing downstream should attempt to schedule two tasks'
233
+ // file writes concurrently regardless of what this field says.
234
+ parallelizable: false,
235
+ verificationRequired: risk !== 'low' || complexity >= 3,
236
+ context: {
237
+ stackHint: context.stackHint || null,
238
+ historyHint: context.historyHint || null
239
+ }
240
+ };
241
+ }