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,200 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ import { CONFIG } from '../config.mjs';
6
+ import { redactObject } from '../format.mjs';
7
+
8
+ export const RC_POLICY_SCHEMA_VERSION = 1;
9
+
10
+ export const DEFAULT_REQUIRED_CHECKS = Object.freeze([
11
+ 'git-state',
12
+ 'npm-run-check',
13
+ 'npm-test',
14
+ 'schema-check',
15
+ 'provider-health',
16
+ 'engine-config',
17
+ 'budget-config',
18
+ 'safety-config',
19
+ 'credential-audit',
20
+ 'lock-observation',
21
+ 'fallback-chain-state',
22
+ 'temp-files'
23
+ ]);
24
+
25
+ export const STRICT_EXTRA_CHECKS = Object.freeze(['readiness', 'doctor']);
26
+
27
+ export const DEFAULT_RC_POLICY = Object.freeze({
28
+ schemaVersion: RC_POLICY_SCHEMA_VERSION,
29
+ requiredHealthTargets: [],
30
+ optionalHealthTargets: [],
31
+ canaryRequired: false,
32
+ allowInfraWarning: true,
33
+ requiredChecks: DEFAULT_REQUIRED_CHECKS,
34
+ skippedChecks: [],
35
+ timeouts: {},
36
+ reportOutput: {
37
+ enabled: false,
38
+ path: null
39
+ },
40
+ strictGitState: false,
41
+ knownDirtyPaths: [],
42
+ strictExtraChecks: []
43
+ });
44
+
45
+ function sha256(text) {
46
+ return crypto.createHash('sha256').update(text).digest('hex');
47
+ }
48
+
49
+ function arrayOfStrings(value, name, errors) {
50
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
51
+ errors.push(name + ' must be an array of strings');
52
+ return [];
53
+ }
54
+ return value;
55
+ }
56
+
57
+ function booleanField(value, name, errors) {
58
+ if (typeof value !== 'boolean') {
59
+ errors.push(name + ' must be boolean');
60
+ return false;
61
+ }
62
+ return value;
63
+ }
64
+
65
+ function normalizeTimeouts(value, errors) {
66
+ if (value === undefined) return {};
67
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
68
+ errors.push('timeouts must be an object');
69
+ return {};
70
+ }
71
+ const timeouts = {};
72
+ for (const [key, raw] of Object.entries(value)) {
73
+ if (!Number.isInteger(raw) || raw < 1) {
74
+ errors.push('timeouts.' + key + ' must be a positive integer');
75
+ } else {
76
+ timeouts[key] = raw;
77
+ }
78
+ }
79
+ return timeouts;
80
+ }
81
+
82
+ function normalizeReportOutput(value, errors) {
83
+ if (value === undefined) return { ...DEFAULT_RC_POLICY.reportOutput };
84
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
85
+ errors.push('reportOutput must be an object');
86
+ return { ...DEFAULT_RC_POLICY.reportOutput };
87
+ }
88
+ const enabled = value.enabled === undefined ? false : booleanField(value.enabled, 'reportOutput.enabled', errors);
89
+ const reportPath = value.path === undefined || value.path === null ? null : value.path;
90
+ if (reportPath !== null && typeof reportPath !== 'string') {
91
+ errors.push('reportOutput.path must be null or string');
92
+ }
93
+ return { enabled, path: typeof reportPath === 'string' ? reportPath : null };
94
+ }
95
+
96
+ function normalizePolicy(raw) {
97
+ const errors = [];
98
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
99
+ return { policy: { ...DEFAULT_RC_POLICY }, errors: ['policy must be an object'] };
100
+ }
101
+ if (raw.schemaVersion !== RC_POLICY_SCHEMA_VERSION) {
102
+ errors.push('schemaVersion must be 1');
103
+ }
104
+
105
+ const requiredHealthTargets = raw.requiredHealthTargets === undefined
106
+ ? []
107
+ : arrayOfStrings(raw.requiredHealthTargets, 'requiredHealthTargets', errors);
108
+ const optionalHealthTargets = raw.optionalHealthTargets === undefined
109
+ ? []
110
+ : arrayOfStrings(raw.optionalHealthTargets, 'optionalHealthTargets', errors);
111
+ const requiredChecks = raw.requiredChecks === undefined
112
+ ? [...DEFAULT_REQUIRED_CHECKS]
113
+ : arrayOfStrings(raw.requiredChecks, 'requiredChecks', errors);
114
+ const skippedChecks = raw.skippedChecks === undefined
115
+ ? []
116
+ : arrayOfStrings(raw.skippedChecks, 'skippedChecks', errors);
117
+ const knownDirtyPaths = raw.knownDirtyPaths === undefined
118
+ ? []
119
+ : arrayOfStrings(raw.knownDirtyPaths, 'knownDirtyPaths', errors);
120
+ const strictExtraChecks = raw.strictExtraChecks === undefined
121
+ ? []
122
+ : arrayOfStrings(raw.strictExtraChecks, 'strictExtraChecks', errors);
123
+
124
+ for (const checkId of strictExtraChecks) {
125
+ if (!STRICT_EXTRA_CHECKS.includes(checkId)) {
126
+ errors.push('strictExtraChecks contains unsupported check: ' + checkId);
127
+ }
128
+ }
129
+
130
+ const requiredSet = new Set(requiredChecks);
131
+ for (const checkId of DEFAULT_REQUIRED_CHECKS) {
132
+ if (!requiredSet.has(checkId) && !skippedChecks.includes(checkId)) {
133
+ errors.push('required check omitted without explicit skippedChecks: ' + checkId);
134
+ }
135
+ }
136
+
137
+ const policy = {
138
+ schemaVersion: RC_POLICY_SCHEMA_VERSION,
139
+ requiredHealthTargets,
140
+ optionalHealthTargets,
141
+ canaryRequired: raw.canaryRequired === undefined ? false : booleanField(raw.canaryRequired, 'canaryRequired', errors),
142
+ allowInfraWarning: raw.allowInfraWarning === undefined ? true : booleanField(raw.allowInfraWarning, 'allowInfraWarning', errors),
143
+ requiredChecks,
144
+ skippedChecks,
145
+ timeouts: normalizeTimeouts(raw.timeouts, errors),
146
+ reportOutput: normalizeReportOutput(raw.reportOutput, errors),
147
+ strictGitState: raw.strictGitState === undefined ? false : booleanField(raw.strictGitState, 'strictGitState', errors),
148
+ knownDirtyPaths,
149
+ strictExtraChecks
150
+ };
151
+
152
+ return { policy, errors };
153
+ }
154
+
155
+ export async function loadRcPolicy(options = {}) {
156
+ const projectRoot = options.projectRoot || process.cwd();
157
+ const policyPath = options.policyPath || path.join(projectRoot, CONFIG.maestroPath, 'rc-policy.json');
158
+ try {
159
+ const rawText = await fs.readFile(policyPath, 'utf-8');
160
+ const parsed = JSON.parse(rawText);
161
+ const normalized = normalizePolicy(parsed);
162
+ const valid = normalized.errors.length === 0;
163
+ return redactObject({
164
+ source: 'file',
165
+ path: policyPath,
166
+ valid,
167
+ hash: sha256(rawText),
168
+ defaultsApplied: false,
169
+ policy: normalized.policy,
170
+ errors: normalized.errors
171
+ });
172
+ } catch (error) {
173
+ if (error.code === 'ENOENT') {
174
+ return {
175
+ source: 'defaults',
176
+ path: null,
177
+ valid: true,
178
+ hash: null,
179
+ defaultsApplied: true,
180
+ policy: JSON.parse(JSON.stringify(DEFAULT_RC_POLICY)),
181
+ errors: []
182
+ };
183
+ }
184
+ const errors = [error instanceof SyntaxError ? 'policy JSON is invalid' : (error.message || String(error))];
185
+ return redactObject({
186
+ source: 'file',
187
+ path: policyPath,
188
+ valid: false,
189
+ hash: null,
190
+ defaultsApplied: false,
191
+ policy: JSON.parse(JSON.stringify(DEFAULT_RC_POLICY)),
192
+ errors
193
+ });
194
+ }
195
+ }
196
+
197
+ export function hashPolicyInfo(policyInfo) {
198
+ if (policyInfo && policyInfo.hash) return policyInfo.hash;
199
+ return sha256(JSON.stringify((policyInfo && policyInfo.policy) || DEFAULT_RC_POLICY));
200
+ }
@@ -0,0 +1,146 @@
1
+ // v0.5.1 "Enforce Orchestra Runtime": the single choke point every real
2
+ // worker execution must pass through. Everything here is read-only /
3
+ // decision-only — this module never calls an engine, never mutates a task,
4
+ // and never writes to RUNS.jsonl itself. Callers (src/task-commands.mjs)
5
+ // own all side effects; this module only answers "what should happen".
6
+ import { classifyTask } from './task-classifier.mjs';
7
+ import { listEngineRecords, getEngineRecord } from './capability-registry.mjs';
8
+ import { runPreflight } from './preflight.mjs';
9
+ import { selectEngine } from './engine-selector.mjs';
10
+ import { planSectorWork } from './sector-managers.mjs';
11
+ import { loadBudget, evaluateBudget } from './budget-manager.mjs';
12
+ import { checkEnginePolicy } from './engine-policy.mjs';
13
+ import { classifyFailure, planFallback } from './fallback-graph.mjs';
14
+ import { getRecentRuns } from '../runner.mjs';
15
+
16
+ export const GATE_DECISIONS = ['EXECUTE', 'BLOCK', 'NEEDS_HUMAN', 'DELEGATE', 'FALLBACK'];
17
+
18
+ // The only preflight outcomes that can ever become a NEEDS_HUMAN or FALLBACK
19
+ // gate decision; everything else in this set is a hard BLOCK (or FALLBACK
20
+ // when an alternative engine already exists) that --force can never lift.
21
+ const HARD_PREFLIGHT_BLOCKS = new Set(['UNAVAILABLE', 'DECLINE', 'NEEDS_CAPABILITY', 'NEEDS_CONTEXT']);
22
+
23
+ export async function countRunsForTask(taskId) {
24
+ const runs = await getRecentRuns(Infinity);
25
+ return runs.filter(run => String(run.taskId) === String(taskId)).length;
26
+ }
27
+
28
+ // Among selectEngine()'s rejected alternatives, one that lost only on score
29
+ // (not a hard filter) is a legitimate fallback candidate — it already
30
+ // passed tools/cost/context/health checks on its own.
31
+ function findAlternativeCandidateId(selection) {
32
+ const runnerUp = (selection.alternatives || []).find(alt => /^lower score/.test(alt.rejectedBecause || ''));
33
+ return runnerUp ? runnerUp.engine : null;
34
+ }
35
+
36
+ // Deterministic, ficha-only decision. No model is ever consulted to decide
37
+ // this (project principle: "Claude pensa somente quando necessário").
38
+ function computeDecision({ selection, preflight, budgetResult, policyResult, managerPlan }) {
39
+ if (!selection.selectedEngine) {
40
+ return {
41
+ decision: 'BLOCK',
42
+ blockCategory: 'no-candidate',
43
+ reasons: ['No engine satisfies this task\'s hard requirements: ' + selection.reason.join('; ')]
44
+ };
45
+ }
46
+ if (!budgetResult.allowed) {
47
+ return { decision: 'BLOCK', blockCategory: 'budget', reasons: ['Budget: ' + budgetResult.reason] };
48
+ }
49
+ if (!policyResult.ok) {
50
+ return { decision: 'BLOCK', blockCategory: 'policy', reasons: policyResult.violations };
51
+ }
52
+ if (HARD_PREFLIGHT_BLOCKS.has(preflight.decision)) {
53
+ const alternativeId = findAlternativeCandidateId(selection);
54
+ return {
55
+ decision: alternativeId ? 'FALLBACK' : 'BLOCK',
56
+ blockCategory: alternativeId ? null : 'preflight',
57
+ alternativeEngineId: alternativeId,
58
+ reasons: [preflight.reason]
59
+ };
60
+ }
61
+ if (preflight.decision === 'NEEDS_HUMAN') {
62
+ return { decision: 'NEEDS_HUMAN', blockCategory: null, reasons: [preflight.reason] };
63
+ }
64
+ if (managerPlan.decision === 'decompose') {
65
+ return { decision: 'DELEGATE', blockCategory: null, reasons: [managerPlan.reason] };
66
+ }
67
+ return { decision: 'EXECUTE', blockCategory: null, reasons: ['Classification, engine selection, preflight, budget, and policy all passed.'] };
68
+ }
69
+
70
+ /**
71
+ * The mandatory pre-execution pipeline: classify -> select manager ->
72
+ * select engine -> preflight -> budget -> policy -> decide.
73
+ *
74
+ * @param {object} params
75
+ * @param {object} params.task - the task record (id, title, description, files, acceptance, requestKind, engine, ...)
76
+ * @param {string} [params.projectRoot] - informational only; all sub-modules already resolve paths via CONFIG.maestroPath
77
+ * @param {'real'|'dry-run'} [params.mode]
78
+ * @param {boolean} [params.force] - may only ever promote a NEEDS_HUMAN decision to EXECUTE; never touches BLOCK/DELEGATE/FALLBACK
79
+ * @param {number} [params.runsSoFar] - defaults to counting .maestro/RUNS.jsonl rows for this task
80
+ * @param {boolean} [params.allowCritical] - if true, a critical-risk task does not trigger NEEDS_HUMAN by itself (still subject to every other gate). Defaults to false; --force is the intended way to move past NEEDS_HUMAN, not this flag.
81
+ */
82
+ export async function evaluateTaskRun({ task, projectRoot = process.cwd(), mode = 'real', force = false, runsSoFar, allowCritical = false } = {}) {
83
+ const classification = classifyTask(task);
84
+ const managerPlan = planSectorWork(task, classification);
85
+ const engines = await listEngineRecords();
86
+ const selection = await selectEngine(classification, { engines });
87
+ const engineRecord = selection.selectedEngine ? await getEngineRecord(selection.selectedEngine) : null;
88
+
89
+ // skipDelegationCheck: the gate derives DELEGATE from the sector-manager
90
+ // plan (managerPlan.decision === 'decompose'), not from preflight's own
91
+ // simplified complexity>=3 heuristic — see runtime-gate design notes in
92
+ // .maestro/AUTONOMOUS_ORCHESTRA.md.
93
+ const preflight = engineRecord
94
+ ? runPreflight(task, engineRecord, classification, { skipDelegationCheck: true, allowCritical })
95
+ : { decision: 'UNAVAILABLE', confidence: 0.9, reason: 'No engine selected.', missingCapabilities: [], recommendedRole: null, recommendedSuccessor: null, suggestedSubtasks: [] };
96
+
97
+ const resolvedRunsSoFar = typeof runsSoFar === 'number' ? runsSoFar : await countRunsForTask(task.id);
98
+ const budgetPolicy = await loadBudget();
99
+ const budgetResult = await evaluateBudget({ runsSoFar: resolvedRunsSoFar, engineRecord });
100
+ const policyResult = checkEnginePolicy(engineRecord, { role: 'worker' });
101
+
102
+ const outcome = computeDecision({ selection, preflight, budgetResult, policyResult, managerPlan });
103
+
104
+ let decision = outcome.decision;
105
+ let forced = false;
106
+ const reasons = [...outcome.reasons];
107
+ if (force === true && decision === 'NEEDS_HUMAN') {
108
+ decision = 'EXECUTE';
109
+ forced = true;
110
+ reasons.push('Forced by user (--force): NEEDS_HUMAN sign-off bypassed. Budget, policy, and capability gates were still enforced and all passed.');
111
+ }
112
+
113
+ let fallback = null;
114
+ if (decision === 'FALLBACK' || (decision === 'BLOCK' && outcome.blockCategory === 'preflight')) {
115
+ const category = classifyFailure({ preflightDecision: preflight.decision, missingCapabilities: preflight.missingCapabilities });
116
+ fallback = await planFallback({ category });
117
+ }
118
+
119
+ const warnings = [...(policyResult.warnings || [])];
120
+ if (mode === 'dry-run') {
121
+ warnings.push('dry-run: this evaluation was computed but no engine/worker will be called.');
122
+ }
123
+
124
+ return {
125
+ decision,
126
+ blockCategory: outcome.blockCategory,
127
+ mode,
128
+ forced,
129
+ projectRoot,
130
+ taskClassification: classification,
131
+ selectedManager: managerPlan,
132
+ selectedEngine: {
133
+ id: selection.selectedEngine,
134
+ score: selection.score,
135
+ reason: selection.reason,
136
+ alternatives: selection.alternatives,
137
+ record: engineRecord
138
+ },
139
+ preflight,
140
+ budget: { policy: budgetPolicy, evaluation: budgetResult },
141
+ policy: policyResult,
142
+ fallback,
143
+ warnings,
144
+ reason: reasons
145
+ };
146
+ }
@@ -0,0 +1,215 @@
1
+ // A "sector manager" is a role the Maestro assigns for a task, not a fixed
2
+ // engine or a separate running process. It exists only long enough to
3
+ // decide whether a task should be executed directly or decomposed into a
4
+ // short list of subtasks handed to the delegation manager (Phase 9). All
5
+ // decomposition here is deterministic/heuristic, matching this project's
6
+ // existing planner style (src/smart-planner.mjs) — no LLM call is made to
7
+ // "decide" anything, per the project principle "Claude pensa somente quando
8
+ // necessário."
9
+
10
+ export const SECTOR_MANAGERS = [
11
+ {
12
+ id: "architecture-manager",
13
+ displayName: "Architecture Manager",
14
+ handles: ["architecture"],
15
+ canDelegate: true,
16
+ maxSubtasks: 4,
17
+ requiresHumanFor: [],
18
+ preferredVerifier: "testing-manager"
19
+ },
20
+ {
21
+ id: "security-manager",
22
+ displayName: "Security Manager",
23
+ handles: ["security-review"],
24
+ canDelegate: true,
25
+ maxSubtasks: 4,
26
+ requiresHumanFor: ["critical-security"],
27
+ preferredVerifier: "testing-manager"
28
+ },
29
+ {
30
+ id: "backend-manager",
31
+ displayName: "Backend Manager",
32
+ handles: ["backend", "database"],
33
+ canDelegate: true,
34
+ maxSubtasks: 4,
35
+ requiresHumanFor: [],
36
+ preferredVerifier: "testing-manager"
37
+ },
38
+ {
39
+ id: "frontend-manager",
40
+ displayName: "Frontend Manager",
41
+ handles: ["frontend", "ui-app"],
42
+ canDelegate: true,
43
+ maxSubtasks: 4,
44
+ requiresHumanFor: [],
45
+ preferredVerifier: "testing-manager"
46
+ },
47
+ {
48
+ id: "testing-manager",
49
+ displayName: "Testing Manager",
50
+ handles: ["testing", "debugging"],
51
+ canDelegate: true,
52
+ maxSubtasks: 4,
53
+ requiresHumanFor: [],
54
+ preferredVerifier: "testing-manager"
55
+ },
56
+ {
57
+ id: "documentation-manager",
58
+ displayName: "Documentation Manager",
59
+ handles: ["documentation", "memory-update"],
60
+ canDelegate: true,
61
+ maxSubtasks: 4,
62
+ requiresHumanFor: [],
63
+ preferredVerifier: "testing-manager"
64
+ },
65
+ {
66
+ id: "research-manager",
67
+ displayName: "Research Manager",
68
+ handles: ["research", "file-inspection", "code-review"],
69
+ canDelegate: true,
70
+ maxSubtasks: 4,
71
+ requiresHumanFor: [],
72
+ preferredVerifier: "testing-manager"
73
+ }
74
+ ];
75
+
76
+ const MINIMUM_COMPLEXITY_FOR_MANAGER = 3;
77
+
78
+ function findSectorForTaskKind(taskKind) {
79
+ for (const manager of SECTOR_MANAGERS) {
80
+ if (manager.handles.includes(taskKind)) {
81
+ return manager;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ // Managers only get involved when there is real benefit: complexity high
88
+ // enough that decomposition helps (spec rule: complexity >= 3), and a
89
+ // recognized sector actually claims this taskKind. A trivial task or an
90
+ // unmapped taskKind (e.g. 'simple-edit', 'project-management',
91
+ // 'deployment', 'data-transformation', 'image-analysis') stays with the
92
+ // Maestro/worker directly — coordination overhead would exceed the gain.
93
+ export function resolveSectorManager(classification) {
94
+ if (!classification || classification.complexity < MINIMUM_COMPLEXITY_FOR_MANAGER) {
95
+ return null;
96
+ }
97
+ return findSectorForTaskKind(classification.taskKind);
98
+ }
99
+
100
+ const SUBTASK_TEMPLATES = {
101
+ 'architecture-manager': [
102
+ { suffix: 'Mapear estado atual e restrições', taskKind: 'file-inspection' },
103
+ { suffix: 'Definir mudança proposta e riscos', taskKind: 'architecture' },
104
+ { suffix: 'Validar decisão com verificação independente', taskKind: 'code-review' }
105
+ ],
106
+ 'security-manager': [
107
+ { suffix: 'Levantar superfície de risco e arquivos afetados', taskKind: 'file-inspection' },
108
+ { suffix: 'Aplicar correção de segurança', taskKind: 'security-review' },
109
+ { suffix: 'Verificar correção com checagem independente', taskKind: 'code-review' }
110
+ ],
111
+ 'backend-manager': [
112
+ { suffix: 'Mapear rotas/serviços/schema envolvidos', taskKind: 'file-inspection' },
113
+ { suffix: 'Implementar mudança de backend', taskKind: 'backend' },
114
+ { suffix: 'Rodar testes relacionados', taskKind: 'testing' }
115
+ ],
116
+ 'frontend-manager': [
117
+ { suffix: 'Mapear componentes/telas envolvidas', taskKind: 'file-inspection' },
118
+ { suffix: 'Implementar mudança de frontend', taskKind: 'frontend' },
119
+ { suffix: 'Validar fluxo manualmente ou via teste', taskKind: 'testing' }
120
+ ],
121
+ 'testing-manager': [
122
+ { suffix: 'Reproduzir ou isolar o problema', taskKind: 'debugging' },
123
+ { suffix: 'Corrigir causa raiz', taskKind: 'debugging' },
124
+ { suffix: 'Adicionar/rodar testes de regressão', taskKind: 'testing' }
125
+ ],
126
+ 'documentation-manager': [
127
+ { suffix: 'Levantar o que mudou e precisa de documentação', taskKind: 'file-inspection' },
128
+ { suffix: 'Atualizar documentação/memória', taskKind: 'documentation' }
129
+ ],
130
+ 'research-manager': [
131
+ { suffix: 'Levantar fontes e opções', taskKind: 'research' },
132
+ { suffix: 'Consolidar achados e recomendação', taskKind: 'documentation' }
133
+ ]
134
+ };
135
+
136
+ const MAX_SUBTASKS_PER_SECTOR = 4;
137
+
138
+ export function generateSubtasksForSector(sector, task) {
139
+ if (!sector) {
140
+ return [];
141
+ }
142
+ const templates = SUBTASK_TEMPLATES[sector.id] || [];
143
+ const baseTitle = task.title || task.description || 'tarefa';
144
+ return templates.slice(0, MAX_SUBTASKS_PER_SECTOR).map((template, index) => ({
145
+ title: baseTitle + ' — ' + template.suffix,
146
+ description: template.suffix + ' (subtarefa ' + (index + 1) + '/' + templates.length + ' de "' + baseTitle + '", gerenciada por ' + sector.displayName + ').',
147
+ taskKind: template.taskKind,
148
+ files: [],
149
+ sector: sector.id,
150
+ parentTaskId: task.id ?? null
151
+ }));
152
+ }
153
+
154
+ // Top-level entry point: given a task + its classification, decide whether
155
+ // a sector manager should handle it, and if so, what it would do
156
+ // (execute directly vs. decompose). This does not perform any I/O and does
157
+ // not itself enforce the delegation-manager's depth/count limits — see
158
+ // src/orchestration/delegation-manager.mjs for that.
159
+ export function planSectorWork(task, classification) {
160
+ const sector = resolveSectorManager(classification);
161
+ if (!sector) {
162
+ return {
163
+ sector: null,
164
+ decision: 'no-manager',
165
+ reason: classification && classification.complexity < MINIMUM_COMPLEXITY_FOR_MANAGER
166
+ ? 'complexity ' + (classification ? classification.complexity : 'unknown') + ' below manager threshold (' + MINIMUM_COMPLEXITY_FOR_MANAGER + ')'
167
+ : 'no sector manager maps to taskKind "' + (classification ? classification.taskKind : 'unknown') + '"',
168
+ subtasks: []
169
+ };
170
+ }
171
+
172
+ // Complexity 3 is handled directly by the manager acting as a strong
173
+ // worker (coordination overhead of decomposing a merely-medium task isn't
174
+ // worth it); complexity >= 4 decomposes into the sector's subtask
175
+ // template. This mirrors the spec's own examples (a complexity-3 frontend
176
+ // task in the spec's own metadata example is still a single unit of work).
177
+ if (classification.complexity < 4) {
178
+ return {
179
+ sector: sector.id,
180
+ decision: 'execute-directly',
181
+ reason: sector.displayName + ' handles this task directly (complexity ' + classification.complexity + ').',
182
+ subtasks: []
183
+ };
184
+ }
185
+
186
+ const subtasks = generateSubtasksForSector(sector, task);
187
+ return {
188
+ sector: sector.id,
189
+ decision: 'decompose',
190
+ reason: sector.displayName + ' decomposes this task (complexity ' + classification.complexity + ') into ' + subtasks.length + ' subtasks.',
191
+ subtasks
192
+ };
193
+ }
194
+ /**
195
+ * Seleciona um gerente de setor baseado na classificação.
196
+ * Alias para resolveSectorManager para compatibilidade com delegation-executor.
197
+ */
198
+ export function selectSectorManager(classification) {
199
+ return resolveSectorManager(classification);
200
+ }
201
+
202
+ /**
203
+ * Determina se um gerente de setor deve ser usado para a tarefa.
204
+ */
205
+ export function shouldUseManager(classification) {
206
+ if (!classification) {
207
+ return false;
208
+ }
209
+ // Manager é útil quando complexidade >= 3 e existe um sector aplicável
210
+ if (classification.complexity < MINIMUM_COMPLEXITY_FOR_MANAGER) {
211
+ return false;
212
+ }
213
+ const manager = resolveSectorManager(classification);
214
+ return manager !== null;
215
+ }