up-cc 0.1.1 → 0.1.3

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/bin/up-tools.cjs CHANGED
@@ -3,13 +3,13 @@
3
3
  /**
4
4
  * UP Tools — CLI utility for UP workflow operations
5
5
  *
6
- * Simplified version of GSD Tools. Single file (+ core.cjs).
6
+ * UP Tools Single file CLI (+ core.cjs).
7
7
  *
8
8
  * Usage: node up-tools.cjs <command> [args] [--raw] [--cwd <path>]
9
9
  *
10
10
  * Commands:
11
- * init planejar-fase|executar-fase|novo-projeto|rapido|retomar
12
- * state load|get|update|advance-plan|update-progress|add-decision|record-session
11
+ * init planejar-fase|executar-fase|novo-projeto|rapido|retomar|operacao-fase|progresso|verificar-trabalho
12
+ * state load|get|update|advance-plan|update-progress|add-decision|record-session|record-metric|snapshot
13
13
  * roadmap get-phase|analyze|update-plan-progress
14
14
  * phase add|remove|find|complete
15
15
  * config get|set
@@ -18,6 +18,9 @@
18
18
  * progress [json|table|bar]
19
19
  * timestamp [full|date|filename]
20
20
  * slug <text>
21
+ * phase-plan-index <phase>
22
+ * state-snapshot
23
+ * summary-extract <path> [--fields field1,field2]
21
24
  */
22
25
 
23
26
  const fs = require('fs');
@@ -29,6 +32,85 @@ const {
29
32
  pathExistsInternal, generateSlugInternal, toPosixPath,
30
33
  } = require('./lib/core.cjs');
31
34
 
35
+ // --- Frontmatter helpers ---
36
+
37
+ function extractFrontmatter(content) {
38
+ const frontmatter = {};
39
+ const match = content.match(/^---\n([\s\S]+?)\n---/);
40
+ if (!match) return frontmatter;
41
+
42
+ const yaml = match[1];
43
+ const lines = yaml.split('\n');
44
+ let stack = [{ obj: frontmatter, key: null, indent: -1 }];
45
+
46
+ for (const line of lines) {
47
+ if (line.trim() === '') continue;
48
+ const indentMatch = line.match(/^(\s*)/);
49
+ const indent = indentMatch ? indentMatch[1].length : 0;
50
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
51
+ stack.pop();
52
+ }
53
+ const current = stack[stack.length - 1];
54
+ const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
55
+ if (keyMatch) {
56
+ const key = keyMatch[2];
57
+ const value = keyMatch[3].trim();
58
+ if (value === '' || value === '[') {
59
+ current.obj[key] = value === '[' ? [] : {};
60
+ current.key = null;
61
+ stack.push({ obj: current.obj[key], key: null, indent });
62
+ } else if (value.startsWith('[') && value.endsWith(']')) {
63
+ current.obj[key] = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
64
+ current.key = null;
65
+ } else {
66
+ current.obj[key] = value.replace(/^["']|["']$/g, '');
67
+ current.key = null;
68
+ }
69
+ } else if (line.trim().startsWith('- ')) {
70
+ const itemValue = line.trim().slice(2).replace(/^["']|["']$/g, '');
71
+ if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
72
+ const parent = stack.length > 1 ? stack[stack.length - 2] : null;
73
+ if (parent) {
74
+ for (const k of Object.keys(parent.obj)) {
75
+ if (parent.obj[k] === current.obj) {
76
+ parent.obj[k] = [itemValue];
77
+ current.obj = parent.obj[k];
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ } else if (Array.isArray(current.obj)) {
83
+ current.obj.push(itemValue);
84
+ }
85
+ }
86
+ }
87
+ return frontmatter;
88
+ }
89
+
90
+ function extractObjective(content) {
91
+ const m = content.match(/<objective>\s*\n?\s*(.+)/);
92
+ return m ? m[1].trim() : null;
93
+ }
94
+
95
+ function getMilestoneInfo(cwd) {
96
+ try {
97
+ const roadmap = fs.readFileSync(path.join(cwd, '.plano', 'ROADMAP.md'), 'utf-8');
98
+ const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/);
99
+ if (inProgressMatch) {
100
+ return { version: 'v' + inProgressMatch[1], name: inProgressMatch[2].trim() };
101
+ }
102
+ const cleaned = roadmap.replace(/<details>[\s\S]*?<\/details>/gi, '');
103
+ const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/);
104
+ if (headingMatch) {
105
+ return { version: 'v' + headingMatch[1], name: headingMatch[2].trim() };
106
+ }
107
+ const versionMatch = cleaned.match(/v(\d+\.\d+)/);
108
+ return { version: versionMatch ? 'v' + versionMatch[1] : 'v0.1', name: 'MVP' };
109
+ } catch {
110
+ return { version: 'v0.1', name: 'MVP' };
111
+ }
112
+ }
113
+
32
114
  // --- State helpers ---
33
115
 
34
116
  function stateExtractField(content, fieldName) {
@@ -109,8 +191,17 @@ function main() {
109
191
  case 'retomar':
110
192
  cmdInitRetomar(cwd, raw);
111
193
  break;
194
+ case 'operacao-fase':
195
+ cmdInitOperacaoFase(cwd, args[2], raw);
196
+ break;
197
+ case 'progresso':
198
+ cmdInitProgresso(cwd, raw);
199
+ break;
200
+ case 'verificar-trabalho':
201
+ cmdInitVerificarTrabalho(cwd, args[2], raw);
202
+ break;
112
203
  default:
113
- error(`Unknown init workflow: ${workflow}\nAvailable: planejar-fase, executar-fase, novo-projeto, rapido, retomar`);
204
+ error(`Unknown init workflow: ${workflow}\nAvailable: planejar-fase, executar-fase, novo-projeto, rapido, retomar, operacao-fase, progresso, verificar-trabalho`);
114
205
  }
115
206
  break;
116
207
  }
@@ -140,8 +231,23 @@ function main() {
140
231
  cmdStateRecordSession(cwd, {
141
232
  stopped_at: stoppedIdx !== -1 ? args[stoppedIdx + 1] : null,
142
233
  }, raw);
234
+ } else if (sub === 'record-metric') {
235
+ const phaseIdx = args.indexOf('--phase');
236
+ const planIdx = args.indexOf('--plan');
237
+ const durationIdx = args.indexOf('--duration');
238
+ const tasksIdx = args.indexOf('--tasks');
239
+ const filesIdx = args.indexOf('--files');
240
+ cmdStateRecordMetric(cwd, {
241
+ phase: phaseIdx !== -1 ? args[phaseIdx + 1] : null,
242
+ plan: planIdx !== -1 ? args[planIdx + 1] : null,
243
+ duration: durationIdx !== -1 ? args[durationIdx + 1] : null,
244
+ tasks: tasksIdx !== -1 ? args[tasksIdx + 1] : null,
245
+ files: filesIdx !== -1 ? args[filesIdx + 1] : null,
246
+ }, raw);
247
+ } else if (sub === 'snapshot') {
248
+ cmdStateSnapshot(cwd, raw);
143
249
  } else {
144
- error('Unknown state subcommand. Available: load, get, update, advance-plan, update-progress, add-decision, record-session');
250
+ error('Unknown state subcommand. Available: load, get, update, advance-plan, update-progress, add-decision, record-session, record-metric, snapshot');
145
251
  }
146
252
  break;
147
253
  }
@@ -232,6 +338,26 @@ function main() {
232
338
  break;
233
339
  }
234
340
 
341
+ // ==================== PHASE-PLAN-INDEX ====================
342
+ case 'phase-plan-index': {
343
+ cmdPhasePlanIndex(cwd, args[1], raw);
344
+ break;
345
+ }
346
+
347
+ // ==================== STATE-SNAPSHOT ====================
348
+ case 'state-snapshot': {
349
+ cmdStateSnapshot(cwd, raw);
350
+ break;
351
+ }
352
+
353
+ // ==================== SUMMARY-EXTRACT ====================
354
+ case 'summary-extract': {
355
+ const fieldsIdx = args.indexOf('--fields');
356
+ const fields = fieldsIdx !== -1 ? args[fieldsIdx + 1].split(',') : [];
357
+ cmdSummaryExtract(cwd, args[1], fields, raw);
358
+ break;
359
+ }
360
+
235
361
  default:
236
362
  error(`Unknown command: ${command}`);
237
363
  }
@@ -385,6 +511,174 @@ function cmdInitRetomar(cwd, raw) {
385
511
  output(result, raw);
386
512
  }
387
513
 
514
+ function cmdInitOperacaoFase(cwd, phase, raw) {
515
+ const config = loadConfig(cwd);
516
+ let phaseInfo = findPhaseInternal(cwd, phase);
517
+
518
+ if (!phaseInfo) {
519
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
520
+ if (roadmapPhase?.found) {
521
+ const phaseName = roadmapPhase.phase_name;
522
+ phaseInfo = {
523
+ found: true,
524
+ directory: null,
525
+ phase_number: roadmapPhase.phase_number,
526
+ phase_name: phaseName,
527
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
528
+ plans: [],
529
+ summaries: [],
530
+ incomplete_plans: [],
531
+ has_research: false,
532
+ has_context: false,
533
+ has_verification: false,
534
+ };
535
+ }
536
+ }
537
+
538
+ const result = {
539
+ commit_docs: config.commit_docs,
540
+ phase_found: !!phaseInfo,
541
+ phase_dir: phaseInfo?.directory || null,
542
+ phase_number: phaseInfo?.phase_number || null,
543
+ phase_name: phaseInfo?.phase_name || null,
544
+ phase_slug: phaseInfo?.phase_slug || null,
545
+ padded_phase: phaseInfo?.phase_number?.padStart(2, '0') || null,
546
+ has_research: phaseInfo?.has_research || false,
547
+ has_context: phaseInfo?.has_context || false,
548
+ has_plans: (phaseInfo?.plans?.length || 0) > 0,
549
+ has_verification: false,
550
+ plan_count: phaseInfo?.plans?.length || 0,
551
+ roadmap_exists: pathExistsInternal(cwd, '.plano/ROADMAP.md'),
552
+ plano_exists: pathExistsInternal(cwd, '.plano'),
553
+ state_path: '.plano/STATE.md',
554
+ roadmap_path: '.plano/ROADMAP.md',
555
+ };
556
+
557
+ if (phaseInfo?.directory) {
558
+ const phaseDirFull = path.join(cwd, phaseInfo.directory);
559
+ try {
560
+ const files = fs.readdirSync(phaseDirFull);
561
+ const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
562
+ if (contextFile) result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
563
+ const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
564
+ if (researchFile) result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
565
+ const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
566
+ if (verificationFile) {
567
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
568
+ result.has_verification = true;
569
+ }
570
+ } catch {}
571
+ }
572
+
573
+ output(result, raw);
574
+ }
575
+
576
+ function cmdInitProgresso(cwd, raw) {
577
+ const config = loadConfig(cwd);
578
+ const milestone = getMilestoneInfo(cwd);
579
+
580
+ const fasesDir = path.join(cwd, '.plano', 'fases');
581
+ const phases = [];
582
+ let currentPhase = null;
583
+ let nextPhase = null;
584
+
585
+ try {
586
+ const entries = fs.readdirSync(fasesDir, { withFileTypes: true });
587
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
588
+
589
+ for (const dir of dirs) {
590
+ const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
591
+ const phaseNumber = match ? match[1] : dir;
592
+ const phaseName = match && match[2] ? match[2] : null;
593
+
594
+ const phasePath = path.join(fasesDir, dir);
595
+ const phaseFiles = fs.readdirSync(phasePath);
596
+
597
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
598
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
599
+ const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
600
+
601
+ const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :
602
+ plans.length > 0 ? 'in_progress' :
603
+ hasResearch ? 'researched' : 'pending';
604
+
605
+ const phaseInfo = {
606
+ number: phaseNumber,
607
+ name: phaseName,
608
+ directory: '.plano/fases/' + dir,
609
+ status,
610
+ plan_count: plans.length,
611
+ summary_count: summaries.length,
612
+ has_research: hasResearch,
613
+ };
614
+
615
+ phases.push(phaseInfo);
616
+
617
+ if (!currentPhase && (status === 'in_progress' || status === 'researched')) {
618
+ currentPhase = phaseInfo;
619
+ }
620
+ if (!nextPhase && status === 'pending') {
621
+ nextPhase = phaseInfo;
622
+ }
623
+ }
624
+ } catch {}
625
+
626
+ let pausedAt = null;
627
+ try {
628
+ const state = fs.readFileSync(path.join(cwd, '.plano', 'STATE.md'), 'utf-8');
629
+ const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
630
+ if (pauseMatch) pausedAt = pauseMatch[1].trim();
631
+ } catch {}
632
+
633
+ const result = {
634
+ commit_docs: config.commit_docs,
635
+ milestone_version: milestone.version,
636
+ milestone_name: milestone.name,
637
+ phases,
638
+ phase_count: phases.length,
639
+ completed_count: phases.filter(p => p.status === 'complete').length,
640
+ in_progress_count: phases.filter(p => p.status === 'in_progress').length,
641
+ current_phase: currentPhase,
642
+ next_phase: nextPhase,
643
+ paused_at: pausedAt,
644
+ has_work_in_progress: !!currentPhase,
645
+ state_exists: pathExistsInternal(cwd, '.plano/STATE.md'),
646
+ roadmap_exists: pathExistsInternal(cwd, '.plano/ROADMAP.md'),
647
+ project_exists: pathExistsInternal(cwd, '.plano/PROJECT.md'),
648
+ state_path: '.plano/STATE.md',
649
+ roadmap_path: '.plano/ROADMAP.md',
650
+ project_path: '.plano/PROJECT.md',
651
+ };
652
+
653
+ output(result, raw);
654
+ }
655
+
656
+ function cmdInitVerificarTrabalho(cwd, phase, raw) {
657
+ if (!phase) error('phase required for init verificar-trabalho');
658
+
659
+ const config = loadConfig(cwd);
660
+ const phaseInfo = findPhaseInternal(cwd, phase);
661
+
662
+ let hasVerification = false;
663
+ if (phaseInfo?.directory) {
664
+ try {
665
+ const files = fs.readdirSync(path.join(cwd, phaseInfo.directory));
666
+ hasVerification = files.some(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
667
+ } catch {}
668
+ }
669
+
670
+ const result = {
671
+ commit_docs: config.commit_docs,
672
+ phase_found: !!phaseInfo,
673
+ phase_dir: phaseInfo?.directory || null,
674
+ phase_number: phaseInfo?.phase_number || null,
675
+ phase_name: phaseInfo?.phase_name || null,
676
+ has_verification: hasVerification,
677
+ };
678
+
679
+ output(result, raw);
680
+ }
681
+
388
682
  // =====================================================================
389
683
  // STATE COMMANDS
390
684
  // =====================================================================
@@ -593,6 +887,108 @@ function cmdStateRecordSession(cwd, options, raw) {
593
887
  }
594
888
  }
595
889
 
890
+ function cmdStateRecordMetric(cwd, options, raw) {
891
+ const statePath = path.join(cwd, '.plano', 'STATE.md');
892
+ if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
893
+
894
+ let content = fs.readFileSync(statePath, 'utf-8');
895
+ const { phase, plan, duration, tasks, files } = options;
896
+
897
+ if (!phase || !plan || !duration) {
898
+ output({ error: 'phase, plan, and duration required' }, raw);
899
+ return;
900
+ }
901
+
902
+ const metricsPattern = /(##\s*Performance Metrics[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n)([\s\S]*?)(?=\n##|\n$|$)/i;
903
+ const metricsMatch = content.match(metricsPattern);
904
+
905
+ if (metricsMatch) {
906
+ let tableBody = metricsMatch[2].trimEnd();
907
+ const newRow = `| Phase ${phase} P${plan} | ${duration} | ${tasks || '-'} tasks | ${files || '-'} files |`;
908
+
909
+ if (tableBody.trim() === '' || tableBody.includes('None yet')) {
910
+ tableBody = newRow;
911
+ } else {
912
+ tableBody = tableBody + '\n' + newRow;
913
+ }
914
+
915
+ content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
916
+ fs.writeFileSync(statePath, content, 'utf-8');
917
+ output({ recorded: true, phase, plan, duration }, raw, 'true');
918
+ } else {
919
+ output({ recorded: false, reason: 'Performance Metrics section not found in STATE.md' }, raw, 'false');
920
+ }
921
+ }
922
+
923
+ function cmdStateSnapshot(cwd, raw) {
924
+ const statePath = path.join(cwd, '.plano', 'STATE.md');
925
+ if (!fs.existsSync(statePath)) { output({ error: 'STATE.md not found' }, raw); return; }
926
+
927
+ const content = fs.readFileSync(statePath, 'utf-8');
928
+
929
+ const currentPhase = stateExtractField(content, 'Current Phase');
930
+ const currentPhaseName = stateExtractField(content, 'Current Phase Name');
931
+ const totalPhasesRaw = stateExtractField(content, 'Total Phases');
932
+ const currentPlan = stateExtractField(content, 'Current Plan');
933
+ const totalPlansRaw = stateExtractField(content, 'Total Plans in Phase');
934
+ const status = stateExtractField(content, 'Status');
935
+ const progressRaw = stateExtractField(content, 'Progress');
936
+ const lastActivity = stateExtractField(content, 'Last Activity');
937
+ const pausedAt = stateExtractField(content, 'Paused At');
938
+
939
+ const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
940
+ const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
941
+ const progressPercent = progressRaw ? parseInt(progressRaw.replace('%', ''), 10) : null;
942
+
943
+ const decisions = [];
944
+ const decisionsMatch = content.match(/##\s*Decisions Made[\s\S]*?\n\|[^\n]+\n\|[-|\s]+\n([\s\S]*?)(?=\n##|\n$|$)/i);
945
+ if (decisionsMatch) {
946
+ const rows = decisionsMatch[1].trim().split('\n').filter(r => r.includes('|'));
947
+ for (const row of rows) {
948
+ const cells = row.split('|').map(c => c.trim()).filter(Boolean);
949
+ if (cells.length >= 3) {
950
+ decisions.push({ phase: cells[0], summary: cells[1], rationale: cells[2] });
951
+ }
952
+ }
953
+ }
954
+
955
+ const blockers = [];
956
+ const blockersMatch = content.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i);
957
+ if (blockersMatch) {
958
+ const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
959
+ for (const item of items) {
960
+ blockers.push(item.replace(/^-\s+/, '').trim());
961
+ }
962
+ }
963
+
964
+ const session = { last_date: null, stopped_at: null, resume_file: null };
965
+ const sessionMatch = content.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i);
966
+ if (sessionMatch) {
967
+ const s = sessionMatch[1];
968
+ const ld = s.match(/\*\*Last Date:\*\*\s*(.+)/i) || s.match(/^Last Date:\s*(.+)/im);
969
+ const sa = s.match(/\*\*Stopped At:\*\*\s*(.+)/i) || s.match(/^Stopped At:\s*(.+)/im);
970
+ const rf = s.match(/\*\*Resume File:\*\*\s*(.+)/i) || s.match(/^Resume File:\s*(.+)/im);
971
+ if (ld) session.last_date = ld[1].trim();
972
+ if (sa) session.stopped_at = sa[1].trim();
973
+ if (rf) session.resume_file = rf[1].trim();
974
+ }
975
+
976
+ output({
977
+ current_phase: currentPhase,
978
+ current_phase_name: currentPhaseName,
979
+ total_phases: totalPhases,
980
+ current_plan: currentPlan,
981
+ total_plans_in_phase: totalPlansInPhase,
982
+ status,
983
+ progress_percent: progressPercent,
984
+ last_activity: lastActivity,
985
+ decisions,
986
+ blockers,
987
+ paused_at: pausedAt,
988
+ session,
989
+ }, raw);
990
+ }
991
+
596
992
  // =====================================================================
597
993
  // ROADMAP COMMANDS
598
994
  // =====================================================================
@@ -1116,6 +1512,128 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
1116
1512
  }, raw);
1117
1513
  }
1118
1514
 
1515
+ function cmdPhasePlanIndex(cwd, phase, raw) {
1516
+ if (!phase) error('phase required for phase-plan-index');
1517
+
1518
+ const fasesDir = path.join(cwd, '.plano', 'fases');
1519
+ const normalized = normalizePhaseName(phase);
1520
+
1521
+ let phaseDir = null;
1522
+ try {
1523
+ const entries = fs.readdirSync(fasesDir, { withFileTypes: true });
1524
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
1525
+ const match = dirs.find(d => d.startsWith(normalized));
1526
+ if (match) phaseDir = path.join(fasesDir, match);
1527
+ } catch {}
1528
+
1529
+ if (!phaseDir) {
1530
+ output({ phase: normalized, error: 'Phase not found', plans: [], waves: {}, incomplete: [], has_checkpoints: false }, raw);
1531
+ return;
1532
+ }
1533
+
1534
+ const phaseFiles = fs.readdirSync(phaseDir);
1535
+ const planFiles = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
1536
+ const summaryFiles = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
1537
+
1538
+ const completedPlanIds = new Set(
1539
+ summaryFiles.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
1540
+ );
1541
+
1542
+ const plans = [];
1543
+ const waves = {};
1544
+ const incomplete = [];
1545
+ let hasCheckpoints = false;
1546
+
1547
+ for (const planFile of planFiles) {
1548
+ const planId = planFile.replace('-PLAN.md', '').replace('PLAN.md', '');
1549
+ const planPath = path.join(phaseDir, planFile);
1550
+ const content = fs.readFileSync(planPath, 'utf-8');
1551
+ const fm = extractFrontmatter(content);
1552
+
1553
+ const xmlTasks = content.match(/<task[\s>]/gi) || [];
1554
+ const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
1555
+ const taskCount = xmlTasks.length || mdTasks.length;
1556
+
1557
+ const wave = parseInt(fm.wave, 10) || 1;
1558
+
1559
+ let autonomous = true;
1560
+ if (fm.autonomous !== undefined) {
1561
+ autonomous = fm.autonomous === 'true' || fm.autonomous === true;
1562
+ }
1563
+ if (!autonomous) hasCheckpoints = true;
1564
+
1565
+ let filesModified = [];
1566
+ const fmFiles = fm['files_modified'] || fm['files-modified'];
1567
+ if (fmFiles) {
1568
+ filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
1569
+ }
1570
+
1571
+ const hasSummary = completedPlanIds.has(planId);
1572
+ if (!hasSummary) incomplete.push(planId);
1573
+
1574
+ plans.push({
1575
+ id: planId,
1576
+ wave,
1577
+ autonomous,
1578
+ objective: extractObjective(content) || fm.objective || null,
1579
+ files_modified: filesModified,
1580
+ task_count: taskCount,
1581
+ has_summary: hasSummary,
1582
+ });
1583
+
1584
+ const waveKey = String(wave);
1585
+ if (!waves[waveKey]) waves[waveKey] = [];
1586
+ waves[waveKey].push(planId);
1587
+ }
1588
+
1589
+ output({ phase: normalized, plans, waves, incomplete, has_checkpoints: hasCheckpoints }, raw);
1590
+ }
1591
+
1592
+ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
1593
+ if (!summaryPath) error('summary-path required for summary-extract');
1594
+
1595
+ const fullPath = path.join(cwd, summaryPath);
1596
+ if (!fs.existsSync(fullPath)) {
1597
+ output({ error: 'File not found', path: summaryPath }, raw);
1598
+ return;
1599
+ }
1600
+
1601
+ const content = fs.readFileSync(fullPath, 'utf-8');
1602
+ const fm = extractFrontmatter(content);
1603
+
1604
+ const parseDecisions = (decisionsList) => {
1605
+ if (!decisionsList || !Array.isArray(decisionsList)) return [];
1606
+ return decisionsList.map(d => {
1607
+ const colonIdx = d.indexOf(':');
1608
+ if (colonIdx > 0) {
1609
+ return { summary: d.substring(0, colonIdx).trim(), rationale: d.substring(colonIdx + 1).trim() };
1610
+ }
1611
+ return { summary: d, rationale: null };
1612
+ });
1613
+ };
1614
+
1615
+ const fullResult = {
1616
+ path: summaryPath,
1617
+ one_liner: fm['one-liner'] || null,
1618
+ key_files: fm['key-files'] || [],
1619
+ tech_added: (fm['tech-stack'] && fm['tech-stack'].added) || [],
1620
+ patterns: fm['patterns-established'] || [],
1621
+ decisions: parseDecisions(fm['key-decisions']),
1622
+ requirements_completed: fm['requirements-completed'] || [],
1623
+ };
1624
+
1625
+ if (fields && fields.length > 0) {
1626
+ const filtered = { path: summaryPath };
1627
+ for (const field of fields) {
1628
+ if (fullResult[field] !== undefined) filtered[field] = fullResult[field];
1629
+ }
1630
+ output(filtered, raw);
1631
+ return;
1632
+ }
1633
+
1634
+ output(fullResult, raw);
1635
+ }
1636
+
1119
1637
  // =====================================================================
1120
1638
  // CONFIG COMMANDS
1121
1639
  // =====================================================================
package/commands/ajuda.md CHANGED
@@ -28,6 +28,7 @@ Sistema de desenvolvimento orientado a fases para projetos de software.
28
28
  | Comando | Descricao | Uso |
29
29
  |---------|-----------|-----|
30
30
  | `/up:novo-projeto` | Inicializar novo projeto com coleta de contexto | `/up:novo-projeto` |
31
+ | `/up:mapear-codigo` | Analisar codebase existente com agentes paralelos | `/up:mapear-codigo` |
31
32
  | `/up:retomar` | Restaurar contexto da sessao anterior | `/up:retomar` |
32
33
 
33
34
  ### Ciclo de Fase
@@ -87,6 +88,13 @@ Sistema de desenvolvimento orientado a fases para projetos de software.
87
88
 
88
89
  ## Fluxos de Trabalho Comuns
89
90
 
91
+ ### Projeto com Codigo Existente (brownfield)
92
+ ```
93
+ /up:mapear-codigo
94
+ /up:novo-projeto
95
+ /up:discutir-fase 1
96
+ ```
97
+
90
98
  ### Novo Projeto (do zero)
91
99
  ```
92
100
  /up:novo-projeto
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: up:mapear-codigo
3
+ description: Analisar codebase existente com agentes mapeadores paralelos
4
+ argument-hint: "[opcional: area especifica, ex: 'api' ou 'auth']"
5
+ allowed-tools:
6
+ - Read
7
+ - Bash
8
+ - Glob
9
+ - Grep
10
+ - Write
11
+ - Task
12
+ ---
13
+
14
+ <objective>
15
+ Analyze existing codebase using parallel up-mapeador-codigo agents to produce structured codebase documents.
16
+
17
+ Each mapper agent explores a focus area and **writes documents directly** to `.plano/codebase/`. The orchestrator only receives confirmations, keeping context usage minimal.
18
+
19
+ Output: .plano/codebase/ folder with 7 structured documents about the codebase state.
20
+ </objective>
21
+
22
+ <execution_context>
23
+ @~/.claude/up/workflows/mapear-codigo.md
24
+ @~/.claude/up/references/ui-brand.md
25
+ </execution_context>
26
+
27
+ <context>
28
+ Focus area: $ARGUMENTS (optional - if provided, tells agents to focus on specific subsystem)
29
+
30
+ **Load project state if exists:**
31
+ Check for .plano/STATE.md - loads context if project already initialized
32
+
33
+ **This command can run:**
34
+ - Before /up:novo-projeto (brownfield codebases) - creates codebase map first
35
+ - After /up:novo-projeto (greenfield codebases) - updates codebase map as code evolves
36
+ - Anytime to refresh codebase understanding
37
+ </context>
38
+
39
+ <when_to_use>
40
+ **Use mapear-codigo for:**
41
+ - Projetos brownfield antes da inicializacao (entender codigo existente primeiro)
42
+ - Atualizar mapa do codebase apos mudancas significativas
43
+ - Onboarding em codebase desconhecido
44
+ - Antes de refatoracao grande (entender estado atual)
45
+
46
+ **Skip mapear-codigo for:**
47
+ - Projetos greenfield sem codigo ainda (nada para mapear)
48
+ - Codebases triviais (<5 arquivos)
49
+ </when_to_use>
50
+
51
+ <process>
52
+ 1. Check if .plano/codebase/ already exists (offer to refresh or skip)
53
+ 2. Create .plano/codebase/ directory structure
54
+ 3. Spawn 4 parallel up-mapeador-codigo agents:
55
+ - Agent 1: tech focus -> writes STACK.md, INTEGRATIONS.md
56
+ - Agent 2: arch focus -> writes ARCHITECTURE.md, STRUCTURE.md
57
+ - Agent 3: quality focus -> writes CONVENTIONS.md, TESTING.md
58
+ - Agent 4: concerns focus -> writes CONCERNS.md
59
+ 4. Wait for agents to complete, collect confirmations (NOT document contents)
60
+ 5. Verify all 7 documents exist with line counts
61
+ 6. Commit codebase map
62
+ 7. Offer next steps (typically: /up:novo-projeto or /up:planejar-fase)
63
+ </process>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "up-cc",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Simplified spec-driven development for Claude Code, Gemini and OpenCode.",
5
5
  "bin": {
6
6
  "up-cc": "bin/install.js"