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,877 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { CONFIG } from '../config.mjs';
4
+ import { inferEngine } from '../smart-planner.mjs';
5
+
6
+ export const SPEC_PLANNER_VERSION = 'spec-file-v1';
7
+ const MAX_SPEC_BYTES = 128 * 1024;
8
+
9
+ function projectRoot() {
10
+ return path.resolve(CONFIG.projectRoot || process.cwd());
11
+ }
12
+
13
+ function normalizeRelativePath(filePath) {
14
+ return filePath.split(path.sep).join('/');
15
+ }
16
+
17
+ function isInsideProject(root, resolvedPath) {
18
+ const relative = path.relative(root, resolvedPath);
19
+ return Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
20
+ }
21
+
22
+ function cleanPathCandidate(value) {
23
+ return String(value || '')
24
+ .trim()
25
+ .replace(/^[\x60"'(<]+/, '')
26
+ .replace(/[\x60,.;:!?)>"']+$/, '');
27
+ }
28
+
29
+ function containsEnvPath(candidate) {
30
+ return /(^|[\\/])\.env($|[\\/.])/i.test(candidate) || /\.env/i.test(candidate);
31
+ }
32
+
33
+ function isAllowedSpecPath(relativePath) {
34
+ const normalized = relativePath.replace(/\\/g, '/').toLowerCase();
35
+ return normalized.startsWith('.maestro/') && normalized.endsWith('.md') && !normalized.includes('/../') && !containsEnvPath(normalized);
36
+ }
37
+
38
+ function findMarkdownCandidates(request) {
39
+ const text = String(request || '');
40
+ const candidates = [];
41
+ const pattern = /[\x60"']?((?:[A-Za-z]:[\\/]|\.\.?[\\/]|\.maestro[\\/])[^\s\x60"')]+?\.md)[\x60"']?/gi;
42
+ let match;
43
+ while ((match = pattern.exec(text)) !== null) {
44
+ candidates.push({ rawMatch: cleanPathCandidate(match[1]), index: match.index });
45
+ }
46
+ return candidates;
47
+ }
48
+
49
+ export async function detectSpecificationReference(request) {
50
+ const root = projectRoot();
51
+ for (const candidate of findMarkdownCandidates(request)) {
52
+ const rawMatch = cleanPathCandidate(candidate.rawMatch);
53
+ if (!rawMatch || rawMatch.includes('\0') || containsEnvPath(rawMatch)) {
54
+ continue;
55
+ }
56
+ const resolved = path.resolve(root, rawMatch);
57
+ if (!isInsideProject(root, resolved)) {
58
+ continue;
59
+ }
60
+ const specPath = normalizeRelativePath(path.relative(root, resolved));
61
+ if (!isAllowedSpecPath(specPath)) {
62
+ continue;
63
+ }
64
+ try {
65
+ const stat = await fs.stat(resolved);
66
+ if (!stat.isFile()) {
67
+ continue;
68
+ }
69
+ return { hasSpecFile: true, specPath, rawMatch };
70
+ } catch (error) {
71
+ continue;
72
+ }
73
+ }
74
+ return { hasSpecFile: false, specPath: null, rawMatch: null };
75
+ }
76
+
77
+ function normalizeText(value) {
78
+ return String(value || '')
79
+ .normalize('NFD')
80
+ .replace(/[\u0300-\u036f]/g, '')
81
+ .toLowerCase();
82
+ }
83
+
84
+ function normalizeHeading(title) {
85
+ return normalizeText(title)
86
+ .replace(/`/g, '')
87
+ .replace(/^\s*\d+(?:\.\d+)*\.?\s*/, '')
88
+ .replace(/[()[\]:;,_-]+/g, ' ')
89
+ .replace(/\s+/g, ' ')
90
+ .trim();
91
+ }
92
+
93
+ function markdownSections(content) {
94
+ const headings = [];
95
+ const pattern = /^(#{2,6})\s+(.+)$/gm;
96
+ let match;
97
+ while ((match = pattern.exec(content)) !== null) {
98
+ headings.push({ level: match[1].length, title: match[2].trim(), headingStart: match.index, bodyStart: pattern.lastIndex });
99
+ }
100
+ return headings.map((heading, index) => {
101
+ const end = index + 1 < headings.length ? headings[index + 1].headingStart : content.length;
102
+ return {
103
+ level: heading.level,
104
+ title: heading.title,
105
+ start: heading.bodyStart,
106
+ end,
107
+ normalizedTitle: normalizeHeading(heading.title),
108
+ body: content.slice(heading.bodyStart, end).trim()
109
+ };
110
+ });
111
+ }
112
+
113
+ function sectionByTitle(sections, titlePart) {
114
+ const normalized = normalizeHeading(titlePart);
115
+ return sections.find(section => section.normalizedTitle.includes(normalized)) || null;
116
+ }
117
+
118
+ function bulletItems(text) {
119
+ const items = [];
120
+ const pattern = /^\s*-\s+(.+)$/gm;
121
+ let match;
122
+ while ((match = pattern.exec(text || '')) !== null) {
123
+ items.push(match[1].trim().replace(/;$/, ''));
124
+ }
125
+ return items;
126
+ }
127
+
128
+ function numberedItems(text) {
129
+ const items = [];
130
+ const pattern = /^\s*\d+\.\s+(.+)$/gm;
131
+ let match;
132
+ while ((match = pattern.exec(text || '')) !== null) {
133
+ items.push(match[1].trim().replace(/;$/, ''));
134
+ }
135
+ return items;
136
+ }
137
+
138
+ function fencedBlocks(text) {
139
+ const blocks = [];
140
+ const pattern = /\x60\x60\x60[a-zA-Z0-9_-]*\r?\n([\s\S]*?)\x60\x60\x60/g;
141
+ let match;
142
+ while ((match = pattern.exec(text || '')) !== null) {
143
+ blocks.push(match[1].trim());
144
+ }
145
+ return blocks;
146
+ }
147
+
148
+ function firstNonEmptyLine(text) {
149
+ return String(text || '').split(/\r?\n/).map(line => line.trim()).find(Boolean) || null;
150
+ }
151
+
152
+ function extractTargetFile(title, body) {
153
+ const combined = [title, body].join('\n');
154
+ const explicit = combined.match(/\x60([^\x60]*src\/orchestration\/[^\x60]+?\.mjs)\x60/i);
155
+ if (explicit) {
156
+ return explicit[1];
157
+ }
158
+ const loose = combined.match(/src\/orchestration\/[A-Za-z0-9_-]+\.mjs/i);
159
+ return loose ? loose[0] : null;
160
+ }
161
+
162
+ function extractExports(body) {
163
+ const exports = new Set();
164
+ for (const block of fencedBlocks(body)) {
165
+ for (const line of block.split(/\r?\n/)) {
166
+ const match = line.trim().match(/^([A-Za-z_$][\w$]*)\s*\(/);
167
+ if (match) {
168
+ exports.add(match[1]);
169
+ }
170
+ }
171
+ }
172
+ if (/detectFileConflicts/.test(body)) {
173
+ exports.add('detectFileConflicts');
174
+ }
175
+ return [...exports];
176
+ }
177
+
178
+ function sectionRules(body) {
179
+ const marker = body.match(/Regras:\s*([\s\S]*)/i);
180
+ return marker ? bulletItems(marker[1]) : [];
181
+ }
182
+
183
+ function docsToUpdateItems(body) {
184
+ const docsOnly = String(body || '').split(/^Explicar:/im)[0];
185
+ return bulletItems(docsOnly).map(item => item.replace(/\x60/g, '').replace(/\.$/, ''));
186
+ }
187
+
188
+ const INFORMATIONAL_HEADING_TERMS = [
189
+ 'contexto',
190
+ 'estado atual',
191
+ 'estado confirmado',
192
+ 'escopo',
193
+ 'responsabilidade',
194
+ 'nao escopo',
195
+ 'fora de escopo',
196
+ 'arquivos consultados',
197
+ 'arquivos existentes',
198
+ 'arquivos permitidos',
199
+ 'arquivos proibidos',
200
+ 'apis',
201
+ 'contratos publicos',
202
+ 'estados',
203
+ 'estados separados',
204
+ 'eventos',
205
+ 'transicoes',
206
+ 'estados terminais',
207
+ 'runtime gate',
208
+ 'dependencias',
209
+ 'adapters',
210
+ 'delegacao',
211
+ 'subtarefas',
212
+ 'conflito',
213
+ 'conflitos',
214
+ 'execucao simulada',
215
+ 'consolidator',
216
+ 'verifier',
217
+ 'limites',
218
+ 'deteccao de ciclos',
219
+ 'trace',
220
+ 'resultado final',
221
+ 'recommended next action',
222
+ 'exceptions',
223
+ 'pureza',
224
+ 'determinismo',
225
+ 'nao mutacao',
226
+ 'compatibilidade futura',
227
+ 'cli',
228
+ 'allowlist',
229
+ 'acceptance criteria',
230
+ 'itens nao confirmados',
231
+ 'riscos',
232
+ 'resultado esperado',
233
+ 'commit esperado'
234
+ ];
235
+
236
+ const NEGATIVE_SECTION_TERMS = [
237
+ 'nao escopo',
238
+ 'fora de escopo',
239
+ 'arquivos proibidos',
240
+ 'itens nao confirmados',
241
+ 'riscos',
242
+ 'adiado',
243
+ 'futuro',
244
+ 'opcional posterior',
245
+ 'nao implementar ainda',
246
+ 'proibidos na implementacao',
247
+ 'nao alterar'
248
+ ];
249
+
250
+ const NEGATIVE_INTENT_PATTERN = /\b(nao implementar|nao alterar|nao criar|nao executar|fora do escopo|adiado|futuro|somente posteriormente|nao deve|nao pode)\b/;
251
+ const POSITIVE_ACTION_PATTERN = /\b(criar|implementar|adicionar|atualizar|modificar|evoluir|alterar)\b/;
252
+
253
+ function titleHasTerm(normalizedTitle, term) {
254
+ return normalizedTitle === term || normalizedTitle.includes(term);
255
+ }
256
+
257
+ function isNegativeSection(section) {
258
+ const title = section.normalizedTitle;
259
+ return NEGATIVE_SECTION_TERMS.some(term => titleHasTerm(title, term));
260
+ }
261
+
262
+ function isInformationalSection(section) {
263
+ const title = section.normalizedTitle;
264
+ return INFORMATIONAL_HEADING_TERMS.some(term => titleHasTerm(title, term));
265
+ }
266
+
267
+ function hasNegativeIntent(text) {
268
+ return NEGATIVE_INTENT_PATTERN.test(normalizeHeading(text));
269
+ }
270
+
271
+ function positiveLines(text) {
272
+ return String(text || '')
273
+ .split(/\r?\n/)
274
+ .map(line => line.trim())
275
+ .filter(line => line && !hasNegativeIntent(line));
276
+ }
277
+
278
+ function uniqueStrings(values) {
279
+ return [...new Set((values || []).map(value => String(value || '').trim()).filter(Boolean))];
280
+ }
281
+
282
+ function normalizeTaskFile(filePath) {
283
+ return cleanPathCandidate(filePath).replace(/\\/g, '/');
284
+ }
285
+
286
+ function isSafeTaskFile(filePath) {
287
+ const normalized = normalizeTaskFile(filePath);
288
+ return Boolean(normalized) &&
289
+ !path.isAbsolute(normalized) &&
290
+ !normalized.split('/').includes('..') &&
291
+ !containsEnvPath(normalized);
292
+ }
293
+
294
+ function extractFileCandidates(text) {
295
+ const candidates = [];
296
+ const patterns = [
297
+ /\x60([^`\r\n]+?)\x60/g,
298
+ /(?:^|[\s(["'])((?:src|tests|bin)\/[A-Za-z0-9_./*-]+|package\.json|README\.md|\.maestro\/[A-Za-z0-9_./*-]+|\.memory\/[A-Za-z0-9_./*-]+)(?=$|[\s)"',.;:])/g
299
+ ];
300
+ for (const pattern of patterns) {
301
+ let match;
302
+ while ((match = pattern.exec(text || '')) !== null) {
303
+ const value = normalizeTaskFile(match[1]);
304
+ if (/^(src|tests|bin)\/|^package\.json$|^README\.md$|^\.maestro\/|^\.memory\//i.test(value)) {
305
+ candidates.push(value);
306
+ }
307
+ }
308
+ }
309
+ return uniqueStrings(candidates);
310
+ }
311
+
312
+ function extractConstraintFiles(sections) {
313
+ const allowed = [];
314
+ const prohibited = [];
315
+ for (const section of sections) {
316
+ let mode = null;
317
+ const title = section.normalizedTitle;
318
+ if (title.includes('arquivos permitidos') || title.includes('permitidos para alteracao')) {
319
+ mode = 'allowed';
320
+ }
321
+ if (title.includes('arquivos proibidos') || title.includes('proibidos para alteracao')) {
322
+ mode = 'prohibited';
323
+ }
324
+ if (title.includes('allowlist futura')) {
325
+ mode = null;
326
+ }
327
+ for (const rawLine of String(section.body || '').split(/\r?\n/)) {
328
+ const line = rawLine.trim();
329
+ const normalized = normalizeHeading(line);
330
+ if (!normalized) {
331
+ continue;
332
+ }
333
+ if ((normalized.includes('permitidos') || normalized.includes('allowlist')) && !normalized.includes('proibidos')) {
334
+ mode = 'allowed';
335
+ continue;
336
+ }
337
+ if (normalized.includes('proibidos') || normalized.startsWith('nao alterar')) {
338
+ mode = 'prohibited';
339
+ continue;
340
+ }
341
+ const files = extractFileCandidates(line);
342
+ if (mode === 'allowed') {
343
+ allowed.push(...files);
344
+ } else if (mode === 'prohibited') {
345
+ prohibited.push(...files);
346
+ }
347
+ }
348
+ }
349
+ return {
350
+ allowedFiles: uniqueStrings(allowed),
351
+ prohibitedFiles: uniqueStrings(prohibited)
352
+ };
353
+ }
354
+
355
+ function extractAcceptanceCriteria(sections) {
356
+ const criteria = [];
357
+ for (const section of sections) {
358
+ const title = section.normalizedTitle;
359
+ if (title.includes('acceptance criteria') || title.includes('criterios de aceite')) {
360
+ criteria.push(...bulletItems(section.body));
361
+ }
362
+ }
363
+ return uniqueStrings(criteria);
364
+ }
365
+
366
+ function extractValidationCommandsFromSections(sections, fallbackNeedsTest) {
367
+ const commands = [];
368
+ const commandPattern = /\b(npm run check|npm test|maestro schema-check)\b/g;
369
+ for (const section of sections) {
370
+ const title = section.normalizedTitle;
371
+ if (!title.includes('validacoes') && !title.includes('validation') && !title.includes('acceptance criteria') && !title.includes('criterios de aceite')) {
372
+ continue;
373
+ }
374
+ for (const block of fencedBlocks(section.body)) {
375
+ commands.push(...block.split(/\r?\n/).map(line => line.trim()).filter(Boolean));
376
+ }
377
+ let match;
378
+ while ((match = commandPattern.exec(section.body || '')) !== null) {
379
+ commands.push(match[1]);
380
+ }
381
+ }
382
+ if (fallbackNeedsTest && commands.includes('npm run check') && !commands.includes('npm test')) {
383
+ commands.splice(commands.indexOf('npm run check') + 1, 0, 'npm test');
384
+ }
385
+ return uniqueStrings(commands);
386
+ }
387
+
388
+ function deliverableKind(title) {
389
+ const normalized = normalizeHeading(title);
390
+ if (/\bcriar\b/.test(normalized)) return 'create-module';
391
+ if (/\bevoluir\b/.test(normalized)) return 'evolve-module';
392
+ if (/\batualizar\b/.test(normalized)) return 'update-file';
393
+ return 'other';
394
+ }
395
+
396
+ function targetFileKind(filePath) {
397
+ const normalized = String(filePath || '').toLowerCase();
398
+ if (normalized.startsWith('tests/') || /\.(test|spec)\.[a-z]+$/i.test(normalized)) return 'tests';
399
+ if (normalized === 'package.json') return 'check-script';
400
+ if (normalized.startsWith('src/') && /\.mjs$/i.test(normalized)) return 'module';
401
+ return 'file';
402
+ }
403
+
404
+ function explicitDeliverableEvidence(section) {
405
+ const titleText = normalizeHeading(section.title);
406
+ const bodyText = normalizeHeading(positiveLines(section.body).join('\n'));
407
+ const titleFiles = extractFileCandidates(section.title);
408
+ const targetFiles = extractFileCandidates(section.title + '\n' + positiveLines(section.body).join('\n'));
409
+ const hasAction = POSITIVE_ACTION_PATTERN.test(titleText) || /\b(criar funcao|adicionar funcao|exportar|devera ser adicionado|deve ser adicionado)\b/.test(bodyText);
410
+ const hasModuleTarget = titleFiles.some(file => targetFileKind(file) === 'module');
411
+ const hasTestTarget = targetFiles.some(file => targetFileKind(file) === 'tests');
412
+ const hasPackageCheck = targetFiles.includes('package.json') && /\b(check|script)\b/.test(bodyText);
413
+ return { hasAction, hasModuleTarget, hasTestTarget, hasPackageCheck, targetFiles };
414
+ }
415
+
416
+ function parseDeliverables(sections) {
417
+ const deliverables = [];
418
+ for (const section of sections) {
419
+ const title = section.normalizedTitle;
420
+ if (isNegativeSection(section) || title.includes('testes obrigatorios') || title.includes('docs') || title.includes('validacoes') || title.includes('commit') || title.includes('entrega final')) {
421
+ continue;
422
+ }
423
+ const evidence = explicitDeliverableEvidence(section);
424
+ if (isInformationalSection(section)) {
425
+ const bodyText = normalizeHeading(positiveLines(section.body).join('\n'));
426
+ const explicitUtility = title.includes('conflito') && /\b(criar funcao|adicionar funcao)\b/.test(bodyText);
427
+ if (!evidence.hasModuleTarget && !explicitUtility) {
428
+ continue;
429
+ }
430
+ }
431
+ if (!evidence.hasAction && !evidence.hasModuleTarget) {
432
+ continue;
433
+ }
434
+ const positiveBody = positiveLines(section.body).join('\n');
435
+ const targetFile = extractTargetFile(section.title, positiveBody) || evidence.targetFiles.find(file => targetFileKind(file) === 'module') || null;
436
+ const kind = targetFile ? targetFileKind(targetFile) : deliverableKind(section.title);
437
+ if (kind === 'file' || kind === 'tests' || kind === 'check-script') {
438
+ continue;
439
+ }
440
+ deliverables.push({
441
+ title: section.title.replace(/^\d+(?:\.\d+)*\.?\s*/, '').trim(),
442
+ kind,
443
+ targetFile,
444
+ exports: extractExports(positiveBody),
445
+ rules: sectionRules(positiveBody)
446
+ });
447
+ }
448
+ return dedupeDeliverables(deliverables);
449
+ }
450
+
451
+ function parseTestTargetFile(sections, allowedFiles) {
452
+ for (const section of sections) {
453
+ if (!section.normalizedTitle.includes('testes obrigatorios')) {
454
+ continue;
455
+ }
456
+ const explicit = extractFileCandidates(section.title + '\n' + section.body).find(file => targetFileKind(file) === 'tests');
457
+ if (explicit) {
458
+ return explicit;
459
+ }
460
+ }
461
+ return (allowedFiles || []).find(file => targetFileKind(file) === 'tests') || null;
462
+ }
463
+
464
+ function parsePackageCheckTarget(sections, allowedFiles, prohibitedFiles) {
465
+ const packageAllowed = (allowedFiles || []).includes('package.json');
466
+ const packageProhibited = (prohibitedFiles || []).includes('package.json');
467
+ if (packageProhibited) {
468
+ return null;
469
+ }
470
+ for (const section of sections) {
471
+ const text = normalizeHeading(section.title + '\n' + section.body);
472
+ if (text.includes('package.json') && text.includes('check') && (packageAllowed || text.includes('script'))) {
473
+ return 'package.json';
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+
479
+ function deliverableActionKey(deliverable) {
480
+ const title = normalizeHeading(deliverable.title);
481
+ if (deliverable.kind === 'module') return 'implement';
482
+ if (deliverable.kind === 'tests') return 'test';
483
+ if (deliverable.kind === 'check-script') return 'check';
484
+ if (title.includes('conflito') || (deliverable.exports || []).includes('detectFileConflicts')) return 'file-conflicts';
485
+ if (title.includes('delegation')) return 'delegation';
486
+ if (title.includes('subtask')) return 'subtasks';
487
+ return title.replace(/\b(criar|implementar|adicionar|atualizar|modificar|evoluir|alterar)\b/g, '').trim();
488
+ }
489
+
490
+ function deliverableKey(deliverable) {
491
+ const file = deliverable.targetFile || (deliverable.exports || []).join('+') || normalizeHeading(deliverable.title);
492
+ return [deliverable.kind || 'other', file, deliverableActionKey(deliverable)].join('|');
493
+ }
494
+
495
+ function dedupeDeliverables(deliverables) {
496
+ const byKey = new Map();
497
+ for (const deliverable of deliverables || []) {
498
+ const key = deliverableKey(deliverable);
499
+ if (!byKey.has(key)) {
500
+ byKey.set(key, {
501
+ ...deliverable,
502
+ exports: uniqueStrings(deliverable.exports),
503
+ rules: uniqueStrings(deliverable.rules)
504
+ });
505
+ continue;
506
+ }
507
+ const existing = byKey.get(key);
508
+ existing.exports = uniqueStrings([...(existing.exports || []), ...(deliverable.exports || [])]);
509
+ existing.rules = uniqueStrings([...(existing.rules || []), ...(deliverable.rules || [])]);
510
+ }
511
+ return [...byKey.values()];
512
+ }
513
+
514
+ function objectiveText(section) {
515
+ if (!section) return null;
516
+ const text = section.body
517
+ .replace(/\x60\x60\x60[\s\S]*?\x60\x60\x60/g, '')
518
+ .split(/\r?\n/)
519
+ .map(line => line.trim())
520
+ .filter(line => line && !line.startsWith('-'))
521
+ .join(' ')
522
+ .trim();
523
+ return text || null;
524
+ }
525
+
526
+ export function parseSpecificationMarkdown(specContent, specPath) {
527
+ try {
528
+ const content = String(specContent || '').slice(0, MAX_SPEC_BYTES);
529
+ const sections = markdownSections(content);
530
+ const objectiveSection = sectionByTitle(sections, 'Objetivo');
531
+ const doNotSection = sectionByTitle(sections, 'Nao implementar ainda');
532
+ const rulesSection = sectionByTitle(sections, 'Regras');
533
+ const testsSection = sectionByTitle(sections, 'Testes obrigatorios');
534
+ const docsSection = sectionByTitle(sections, 'Docs');
535
+ const validationSection = sectionByTitle(sections, 'Validacoes');
536
+ const commitSection = sectionByTitle(sections, 'Commit');
537
+ const deliverySection = sectionByTitle(sections, 'Entrega final');
538
+ const commitBlocks = fencedBlocks(commitSection ? commitSection.body : '');
539
+ const constraints = extractConstraintFiles(sections);
540
+ const deliverables = parseDeliverables(sections);
541
+ const mandatoryTests = numberedItems(testsSection ? testsSection.body : '');
542
+ const validationCommands = validationSection
543
+ ? fencedBlocks(validationSection.body).flatMap(block => block.split(/\r?\n/).map(line => line.trim()).filter(Boolean))
544
+ : extractValidationCommandsFromSections(sections, mandatoryTests.length > 0);
545
+
546
+ return {
547
+ specPath,
548
+ objective: objectiveText(objectiveSection),
549
+ doNotImplementYet: bulletItems(doNotSection ? doNotSection.body : ''),
550
+ rules: bulletItems(rulesSection ? rulesSection.body : ''),
551
+ deliverables,
552
+ mandatoryTests,
553
+ testTargetFile: parseTestTargetFile(sections, constraints.allowedFiles),
554
+ packageCheckTargetFile: parsePackageCheckTarget(sections, constraints.allowedFiles, constraints.prohibitedFiles),
555
+ docsToUpdate: docsToUpdateItems(docsSection ? docsSection.body : ''),
556
+ validationCommands,
557
+ expectedCommit: commitBlocks.length > 0 ? firstNonEmptyLine(commitBlocks[0]) : null,
558
+ deliveryChecklist: bulletItems(deliverySection ? deliverySection.body : ''),
559
+ acceptanceCriteria: extractAcceptanceCriteria(sections),
560
+ allowedFiles: constraints.allowedFiles,
561
+ prohibitedFiles: constraints.prohibitedFiles,
562
+ warnings: []
563
+ };
564
+ } catch (error) {
565
+ return {
566
+ specPath,
567
+ objective: null,
568
+ doNotImplementYet: [],
569
+ rules: [],
570
+ deliverables: [],
571
+ mandatoryTests: [],
572
+ testTargetFile: null,
573
+ packageCheckTargetFile: null,
574
+ docsToUpdate: [],
575
+ validationCommands: [],
576
+ expectedCommit: null,
577
+ deliveryChecklist: [],
578
+ acceptanceCriteria: [],
579
+ allowedFiles: [],
580
+ prohibitedFiles: [],
581
+ warnings: []
582
+ };
583
+ }
584
+ }
585
+
586
+ function taskTitleForDeliverable(deliverable) {
587
+ const target = String(deliverable.targetFile || '').toLowerCase();
588
+ const exports = deliverable.exports.join(' ');
589
+ if (target.includes('subtask-planner')) return 'Implementar subtask-planner';
590
+ if (target.includes('delegation-executor')) return 'Implementar delegation-executor';
591
+ if (target.includes('sector-managers')) return 'Evoluir sector-managers';
592
+ if (target.includes('orchestration-loop')) return 'Implementar orchestration loop deterministico em dry-run';
593
+ if (/detectFileConflicts/.test(exports) || /conflito/i.test(deliverable.title)) return 'Implementar detecção de conflitos de arquivos';
594
+ return 'Implementar ' + deliverable.title.replace(/\x60/g, '');
595
+ }
596
+
597
+ function specTaskBase(spec, request) {
598
+ return {
599
+ priority: 'high',
600
+ sourceRequest: request,
601
+ sourceSpec: spec.specPath,
602
+ requestKind: 'specification-file'
603
+ };
604
+ }
605
+
606
+ function restrictionAcceptance(spec) {
607
+ const acceptance = [];
608
+ if ((spec.allowedFiles || []).length > 0) {
609
+ acceptance.push('Respeitar arquivos permitidos extraidos do sourceSpec');
610
+ }
611
+ if ((spec.prohibitedFiles || []).length > 0) {
612
+ acceptance.push('Respeitar arquivos proibidos extraidos do sourceSpec');
613
+ }
614
+ return acceptance;
615
+ }
616
+
617
+ function safeExpectedFiles(files, spec) {
618
+ const prohibited = new Set((spec.prohibitedFiles || []).map(normalizeTaskFile));
619
+ return uniqueStrings(files)
620
+ .map(normalizeTaskFile)
621
+ .filter(file => isSafeTaskFile(file) && !prohibited.has(file));
622
+ }
623
+
624
+ function isSingleModuleSpecification(spec) {
625
+ const moduleFiles = uniqueStrings((spec.deliverables || [])
626
+ .map(deliverable => deliverable.targetFile)
627
+ .filter(file => targetFileKind(file) === 'module'));
628
+ return moduleFiles.length === 1;
629
+ }
630
+
631
+ function ensureConservativeSingleModulePlan(tasks, spec) {
632
+ if (!isSingleModuleSpecification(spec) || tasks.length <= 6) {
633
+ return tasks;
634
+ }
635
+ const requiredRoles = new Set(['implementation', 'tests', 'validation', 'commit']);
636
+ const implementation = tasks.find(task => task.planRole === 'implementation');
637
+ const checkTask = tasks.find(task => task.planRole === 'check-script');
638
+ if (implementation && checkTask && tasks.length > 6) {
639
+ implementation.files = safeExpectedFiles([...(implementation.files || []), ...(checkTask.files || [])], spec);
640
+ implementation.acceptance = uniqueStrings([...(implementation.acceptance || []), ...(checkTask.acceptance || [])]);
641
+ tasks = tasks.filter(task => task !== checkTask);
642
+ }
643
+ const extras = tasks.filter(task => !requiredRoles.has(task.planRole) && task.planRole !== 'check-script' && task.planRole !== 'docs');
644
+ if (implementation && extras.length > 0) {
645
+ implementation.files = safeExpectedFiles([...(implementation.files || []), ...extras.flatMap(task => task.files || [])], spec);
646
+ implementation.acceptance = uniqueStrings([
647
+ ...(implementation.acceptance || []),
648
+ ...extras.flatMap(task => task.acceptance || []),
649
+ 'Conceitos internos agrupados na implementacao principal; sem criar microtasks artificiais.'
650
+ ]);
651
+ tasks = tasks.filter(task => !extras.includes(task));
652
+ }
653
+ return tasks;
654
+ }
655
+
656
+ function taskDedupeKey(task) {
657
+ const files = Array.isArray(task.files) && task.files.length > 0 ? task.files.join('+') : normalizeHeading(task.title);
658
+ return [task.planRole || 'task', files, normalizeHeading(task.title)].join('|');
659
+ }
660
+
661
+ function dedupeTasks(tasks) {
662
+ const byKey = new Map();
663
+ for (const task of tasks || []) {
664
+ const key = taskDedupeKey(task);
665
+ if (!byKey.has(key)) {
666
+ byKey.set(key, {
667
+ ...task,
668
+ files: uniqueStrings(task.files || []),
669
+ acceptance: uniqueStrings(task.acceptance || [])
670
+ });
671
+ continue;
672
+ }
673
+ const existing = byKey.get(key);
674
+ existing.files = uniqueStrings([...(existing.files || []), ...(task.files || [])]);
675
+ existing.acceptance = uniqueStrings([...(existing.acceptance || []), ...(task.acceptance || [])]);
676
+ }
677
+ return [...byKey.values()];
678
+ }
679
+
680
+ function buildTasksFromSpecificationLegacy(spec, request) {
681
+ const tasks = [];
682
+ for (const deliverable of spec.deliverables || []) {
683
+ const title = taskTitleForDeliverable(deliverable);
684
+ const files = deliverable.targetFile ? [deliverable.targetFile] : [];
685
+ const acceptance = [
686
+ 'Implementação limitada ao deliverable do spec ' + spec.specPath,
687
+ 'Regras do spec respeitadas',
688
+ 'Sem executar comandos descritos no Markdown durante o planejamento'
689
+ ];
690
+ for (const exported of deliverable.exports || []) {
691
+ acceptance.push('Exportar ' + exported);
692
+ }
693
+ tasks.push({
694
+ ...specTaskBase(spec, request),
695
+ title,
696
+ description: 'Implementar o deliverable definido em ' + spec.specPath + ': ' + deliverable.title + '.',
697
+ files,
698
+ acceptance,
699
+ engine: inferEngine(title)
700
+ });
701
+ }
702
+
703
+ if ((spec.mandatoryTests || []).length > 0) {
704
+ tasks.push({
705
+ ...specTaskBase(spec, request),
706
+ title: 'Adicionar testes obrigatórios',
707
+ description: 'Adicionar a cobertura obrigatória definida em ' + spec.specPath + '.',
708
+ priority: 'high',
709
+ files: ['tests/'],
710
+ acceptance: ['Cobrir os ' + spec.mandatoryTests.length + ' testes obrigatórios do spec', 'Não chamar worker real, engine paga, Claude ou rede nos testes'],
711
+ engine: 'codex9'
712
+ });
713
+ }
714
+
715
+ if ((spec.docsToUpdate || []).length > 0) {
716
+ tasks.push({
717
+ ...specTaskBase(spec, request),
718
+ title: 'Atualizar documentação',
719
+ description: 'Atualizar somente a documentação solicitada em ' + spec.specPath + '.',
720
+ priority: 'medium',
721
+ files: spec.docsToUpdate,
722
+ acceptance: spec.docsToUpdate.map(item => 'Atualizar ' + item),
723
+ engine: 'memory'
724
+ });
725
+ }
726
+
727
+ if ((spec.validationCommands || []).length > 0) {
728
+ tasks.push({
729
+ ...specTaskBase(spec, request),
730
+ title: 'Validar implementação',
731
+ description: 'Rodar as validações listadas no spec depois da implementação, sem executá-las durante o planejamento.',
732
+ priority: 'medium',
733
+ files: [],
734
+ acceptance: spec.validationCommands.map(command => 'Validar com: ' + command),
735
+ engine: 'codex9'
736
+ });
737
+ }
738
+
739
+ tasks.push({
740
+ ...specTaskBase(spec, request),
741
+ title: 'Criar commit',
742
+ description: 'Criar o commit final solicitado pelo spec após validações e revisão do escopo.',
743
+ priority: 'medium',
744
+ files: [],
745
+ acceptance: [spec.expectedCommit ? 'Commit esperado: ' + spec.expectedCommit : 'Commit criado com mensagem coerente', 'Parte seguinte não implementada fora do spec'],
746
+ engine: 'codex9'
747
+ });
748
+
749
+ return tasks;
750
+ }
751
+
752
+ export function buildTasksFromSpecification(spec, request) {
753
+ const tasks = [];
754
+ for (const deliverable of spec.deliverables || []) {
755
+ const title = taskTitleForDeliverable(deliverable);
756
+ const acceptance = [
757
+ 'Implementar codigo backend somente nos arquivos esperados',
758
+ 'Regras tecnicas do spec respeitadas',
759
+ 'Sem executar comandos descritos no Markdown durante o planejamento'
760
+ ];
761
+ for (const exported of deliverable.exports || []) {
762
+ acceptance.push('Exportar ' + exported);
763
+ }
764
+ acceptance.push(...restrictionAcceptance(spec));
765
+ tasks.push({
766
+ ...specTaskBase(spec, request),
767
+ title,
768
+ description: 'Implementar codigo backend definido em ' + spec.specPath + ': ' + deliverable.title.replace(/\x60/g, '') + '.',
769
+ files: safeExpectedFiles(deliverable.targetFile ? [deliverable.targetFile] : [], spec),
770
+ acceptance,
771
+ engine: inferEngine(title),
772
+ planRole: 'implementation'
773
+ });
774
+ }
775
+
776
+ if ((spec.mandatoryTests || []).length > 0) {
777
+ tasks.push({
778
+ ...specTaskBase(spec, request),
779
+ title: 'Adicionar testes obrigat\u00f3rios',
780
+ description: 'Adicionar a cobertura obrigatoria definida em ' + spec.specPath + '.',
781
+ priority: 'high',
782
+ files: safeExpectedFiles([spec.testTargetFile || 'tests/'], spec),
783
+ acceptance: ['Cobrir os ' + spec.mandatoryTests.length + ' testes obrigatorios do spec', 'Nao chamar worker real, engine paga, Claude ou rede nos testes', ...restrictionAcceptance(spec)],
784
+ engine: 'codex9',
785
+ planRole: 'tests'
786
+ });
787
+ }
788
+
789
+ if (spec.packageCheckTargetFile) {
790
+ tasks.push({
791
+ ...specTaskBase(spec, request),
792
+ title: 'Atualizar script check no package.json',
793
+ description: 'Atualizar somente o script check quando o spec exigir inclusao de novo modulo.',
794
+ priority: 'medium',
795
+ files: safeExpectedFiles([spec.packageCheckTargetFile], spec),
796
+ acceptance: ['Script check cobre o novo modulo solicitado', 'Nenhum outro script e alterado sem necessidade', ...restrictionAcceptance(spec)],
797
+ engine: 'codex9',
798
+ planRole: 'check-script'
799
+ });
800
+ }
801
+
802
+ if ((spec.docsToUpdate || []).length > 0) {
803
+ tasks.push({
804
+ ...specTaskBase(spec, request),
805
+ title: 'Atualizar documenta\u00e7\u00e3o',
806
+ description: 'Atualizar somente a documentacao solicitada em ' + spec.specPath + '.',
807
+ priority: 'medium',
808
+ files: safeExpectedFiles(spec.docsToUpdate, spec),
809
+ acceptance: spec.docsToUpdate.map(item => 'Atualizar ' + item),
810
+ engine: 'memory',
811
+ planRole: 'docs'
812
+ });
813
+ }
814
+
815
+ if ((spec.validationCommands || []).length > 0) {
816
+ tasks.push({
817
+ ...specTaskBase(spec, request),
818
+ title: 'Validar implementa\u00e7\u00e3o',
819
+ description: 'Rodar as validacoes listadas no spec depois da implementacao, sem executa-las durante o planejamento.',
820
+ priority: 'medium',
821
+ files: [],
822
+ acceptance: [...spec.validationCommands.map(command => 'Validar com: ' + command), ...restrictionAcceptance(spec)],
823
+ engine: 'codex9',
824
+ planRole: 'validation'
825
+ });
826
+ }
827
+
828
+ tasks.push({
829
+ ...specTaskBase(spec, request),
830
+ title: 'Criar commit',
831
+ description: 'Criar o commit final solicitado pelo spec apos validacoes e revisao tecnica.',
832
+ priority: 'medium',
833
+ files: [],
834
+ acceptance: [spec.expectedCommit ? 'Commit esperado: ' + spec.expectedCommit : 'Commit criado com mensagem coerente', 'Parte seguinte nao implementada fora do spec'],
835
+ engine: 'codex9',
836
+ planRole: 'commit'
837
+ });
838
+
839
+ return ensureConservativeSingleModulePlan(dedupeTasks(tasks), spec).map(task => {
840
+ const { planRole, ...publicTask } = task;
841
+ return publicTask;
842
+ });
843
+ }
844
+
845
+ export async function buildSpecPlan(request) {
846
+ const reference = await detectSpecificationReference(request);
847
+ if (!reference.hasSpecFile) {
848
+ return { usedSpec: false };
849
+ }
850
+ const root = projectRoot();
851
+ const absoluteSpecPath = path.resolve(root, reference.specPath);
852
+ const stat = await fs.stat(absoluteSpecPath);
853
+ if (!stat.isFile() || stat.size > MAX_SPEC_BYTES) {
854
+ return { usedSpec: false };
855
+ }
856
+ const content = await fs.readFile(absoluteSpecPath, 'utf-8');
857
+ const spec = parseSpecificationMarkdown(content, reference.specPath);
858
+ if (!spec.deliverables || spec.deliverables.length === 0) {
859
+ return { usedSpec: false };
860
+ }
861
+ const tasks = buildTasksFromSpecification(spec, request);
862
+ return {
863
+ usedSpec: true,
864
+ specPath: reference.specPath,
865
+ plannerKind: SPEC_PLANNER_VERSION,
866
+ kind: 'specification',
867
+ tasks
868
+ };
869
+ }
870
+
871
+ export async function tryBuildSpecTasks(request) {
872
+ try {
873
+ return await buildSpecPlan(request);
874
+ } catch (error) {
875
+ return { usedSpec: false };
876
+ }
877
+ }