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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +15 -0
- package/README.md +727 -0
- package/bin/maestro.mjs +246 -0
- package/package.json +29 -0
- package/src/checkpoint.mjs +102 -0
- package/src/codex-diagnostics.mjs +682 -0
- package/src/codex-home-inspect.mjs +252 -0
- package/src/codex-home.mjs +258 -0
- package/src/commands.mjs +809 -0
- package/src/config.mjs +11 -0
- package/src/debug.mjs +164 -0
- package/src/encoding-guard.mjs +125 -0
- package/src/engines/ai-router-engine.mjs +37 -0
- package/src/engines/codex-engine.mjs +21 -0
- package/src/engines/engine.mjs +21 -0
- package/src/engines/registry.mjs +29 -0
- package/src/files.mjs +65 -0
- package/src/format.mjs +132 -0
- package/src/lock.mjs +439 -0
- package/src/memory-log.mjs +18 -0
- package/src/memory.mjs +1 -0
- package/src/orchestration/attempt-chain-runtime.mjs +627 -0
- package/src/orchestration/budget-manager.mjs +121 -0
- package/src/orchestration/capability-registry.mjs +108 -0
- package/src/orchestration/consolidator.mjs +772 -0
- package/src/orchestration/delegation-executor.mjs +262 -0
- package/src/orchestration/delegation-manager.mjs +116 -0
- package/src/orchestration/deployment-intent.mjs +36 -0
- package/src/orchestration/engine-history.mjs +110 -0
- package/src/orchestration/engine-policy.mjs +73 -0
- package/src/orchestration/engine-selector.mjs +187 -0
- package/src/orchestration/execution-context.mjs +45 -0
- package/src/orchestration/failure-classifier.mjs +136 -0
- package/src/orchestration/failure-evidence.mjs +237 -0
- package/src/orchestration/fallback-chain-lock.mjs +217 -0
- package/src/orchestration/fallback-chain-trail.mjs +218 -0
- package/src/orchestration/fallback-executor.mjs +146 -0
- package/src/orchestration/fallback-graph.mjs +106 -0
- package/src/orchestration/fallback-recommendation.mjs +73 -0
- package/src/orchestration/file-conflict-detector.mjs +126 -0
- package/src/orchestration/host-validation.mjs +241 -0
- package/src/orchestration/orchestration-loop.mjs +1971 -0
- package/src/orchestration/orchestration-runtime.mjs +1019 -0
- package/src/orchestration/orchestration-scheduler.mjs +135 -0
- package/src/orchestration/orchestration-trail.mjs +192 -0
- package/src/orchestration/preflight.mjs +212 -0
- package/src/orchestration/provider-health.mjs +566 -0
- package/src/orchestration/provider-router.mjs +352 -0
- package/src/orchestration/rc-check-adapters.mjs +817 -0
- package/src/orchestration/rc-check.mjs +544 -0
- package/src/orchestration/rc-policy.mjs +200 -0
- package/src/orchestration/runtime-gate.mjs +146 -0
- package/src/orchestration/sector-managers.mjs +215 -0
- package/src/orchestration/self-hosting-canary.mjs +231 -0
- package/src/orchestration/self-hosting-readiness.mjs +244 -0
- package/src/orchestration/spec-planner.mjs +877 -0
- package/src/orchestration/subtask-planner.mjs +176 -0
- package/src/orchestration/task-classifier.mjs +241 -0
- package/src/orchestration/verifier.mjs +1608 -0
- package/src/orchestration-commands.mjs +279 -0
- package/src/planner.mjs +38 -0
- package/src/project.mjs +1 -0
- package/src/run-diagnostics.mjs +641 -0
- package/src/runner.mjs +243 -0
- package/src/safety.mjs +116 -0
- package/src/schema.mjs +371 -0
- package/src/session-commands.mjs +521 -0
- package/src/shell.mjs +93 -0
- package/src/smart-planner.mjs +249 -0
- package/src/task-commands.mjs +1182 -0
- package/src/tasks.mjs +134 -0
- package/src/ui/commands.mjs +76 -0
- package/src/ui/events.mjs +45 -0
- package/src/ui/public/app.js +600 -0
- package/src/ui/public/index.html +88 -0
- package/src/ui/public/style.css +460 -0
- package/src/ui/server.mjs +112 -0
- package/src/ui/state.mjs +504 -0
- package/src/usage.mjs +178 -0
- 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
|
+
}
|