legacy-squad 1.0.1 → 1.3.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/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // apps/cli/src/index.ts
4
4
  import { Command } from "commander";
5
- import path8 from "node:path";
5
+ import path10 from "node:path";
6
6
  import { existsSync } from "node:fs";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
@@ -224,13 +224,89 @@ var ALL_AGENTS = [
224
224
  MODERNIZATION_AGENT
225
225
  ];
226
226
 
227
+ // packages/agents/src/findings-writer.ts
228
+ import path from "node:path";
229
+
230
+ // packages/core/src/paths.ts
231
+ function toPosix(p) {
232
+ return p.replace(/\\/g, "/");
233
+ }
234
+
235
+ // packages/agents/src/findings-writer.ts
236
+ var FINDINGS_DIR = "findings";
237
+ var INDEX_FILE = "index.json";
238
+ var FindingsWriter = class {
239
+ /**
240
+ * @param fs Porta de escrita injetada; implementação concreta na camada externa.
241
+ */
242
+ constructor(fs) {
243
+ this.fs = fs;
244
+ }
245
+ fs;
246
+ /**
247
+ * Particiona e grava os findings:
248
+ * - `findings/index.json`: lista slim (id, pillar, severity, title, priority);
249
+ * - `findings/<pilar>.json`: findings completos do pilar. Pilar sem achados
250
+ * não gera arquivo.
251
+ *
252
+ * O `index.json` é sempre gravado (mesmo vazio): é o ponto de entrada que
253
+ * sinaliza a nova estrutura para o `doctor.ts` e para os geradores.
254
+ *
255
+ * @param findings Findings consolidados pelo Compliance Engine.
256
+ * @param memoryDir Caminho de `.legacy-squad/memory` (nativo do SO; normalizado
257
+ * para POSIX na escrita — ver DA-006).
258
+ * @returns Promise resolvida quando todos os arquivos foram gravados.
259
+ */
260
+ async write(findings, memoryDir) {
261
+ const findingsDir = toPosix(path.join(memoryDir, FINDINGS_DIR));
262
+ await this.fs.mkdir(findingsDir);
263
+ const index = findings.map((finding) => ({
264
+ id: finding.id,
265
+ pillar: finding.pillar,
266
+ severity: finding.severity,
267
+ title: finding.title,
268
+ priority: finding.priority
269
+ }));
270
+ await this.fs.writeFile(
271
+ toPosix(path.join(findingsDir, INDEX_FILE)),
272
+ JSON.stringify(index, null, 2)
273
+ );
274
+ for (const [pillar, pillarFindings] of this.groupByPillar(findings)) {
275
+ await this.fs.writeFile(
276
+ toPosix(path.join(findingsDir, this.pillarToFileName(pillar))),
277
+ JSON.stringify(pillarFindings, null, 2)
278
+ );
279
+ }
280
+ }
281
+ /** Agrupa os findings por pilar, preservando a ordem de inserção. */
282
+ groupByPillar(findings) {
283
+ const byPillar = /* @__PURE__ */ new Map();
284
+ for (const finding of findings) {
285
+ const bucket = byPillar.get(finding.pillar);
286
+ if (bucket) {
287
+ bucket.push(finding);
288
+ } else {
289
+ byPillar.set(finding.pillar, [finding]);
290
+ }
291
+ }
292
+ return byPillar;
293
+ }
294
+ /**
295
+ * Converte o pilar (enum com `_`) no nome do arquivo (com `-`).
296
+ * Ex.: `legacy_code` → `legacy-code.json`.
297
+ */
298
+ pillarToFileName(pillar) {
299
+ return `${pillar.replace(/_/g, "-")}.json`;
300
+ }
301
+ };
302
+
227
303
  // packages/agents/src/installer.ts
228
- import { writeFile, mkdir, readFile as readFile2 } from "node:fs/promises";
229
- import path6 from "node:path";
304
+ import { writeFile as writeFile2, mkdir as mkdir2, readFile as readFile2 } from "node:fs/promises";
305
+ import path7 from "node:path";
230
306
 
231
307
  // packages/scanner/src/node-filesystem.ts
232
- import { readdir, readFile, stat } from "node:fs/promises";
233
- import path from "node:path";
308
+ import { readdir, readFile, stat, mkdir, writeFile } from "node:fs/promises";
309
+ import path2 from "node:path";
234
310
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
235
311
  "node_modules",
236
312
  ".git",
@@ -248,7 +324,7 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
248
324
  var NodeFileSystem = class {
249
325
  async readDir(dirPath) {
250
326
  const entries = await readdir(dirPath, { withFileTypes: true });
251
- return entries.filter((e) => !IGNORED_DIRS.has(e.name)).map((e) => path.join(dirPath, e.name));
327
+ return entries.filter((e) => !IGNORED_DIRS.has(e.name)).map((e) => path2.join(dirPath, e.name));
252
328
  }
253
329
  async readFile(filePath) {
254
330
  return readFile(filePath, "utf-8");
@@ -270,11 +346,25 @@ var NodeFileSystem = class {
270
346
  await this.walkForGlob(rootPath, pattern, results);
271
347
  return results;
272
348
  }
349
+ /**
350
+ * Garante a existência do diretório criando todos os intermediários.
351
+ * Idempotente — não lança se o diretório já existir.
352
+ */
353
+ async mkdir(dirPath) {
354
+ await mkdir(dirPath, { recursive: true });
355
+ }
356
+ /**
357
+ * Escreve (sobrescrevendo) o arquivo com `content` em UTF-8.
358
+ * Pré-condição: o diretório-pai deve existir — chame `mkdir` antes.
359
+ */
360
+ async writeFile(filePath, content) {
361
+ await writeFile(filePath, content, "utf-8");
362
+ }
273
363
  async walkForGlob(dir, pattern, results) {
274
364
  const entries = await readdir(dir, { withFileTypes: true });
275
365
  for (const entry of entries) {
276
366
  if (IGNORED_DIRS.has(entry.name)) continue;
277
- const fullPath = path.join(dir, entry.name);
367
+ const fullPath = path2.join(dir, entry.name);
278
368
  if (entry.isDirectory()) {
279
369
  await this.walkForGlob(fullPath, pattern, results);
280
370
  } else if (entry.name.match(pattern)) {
@@ -285,10 +375,10 @@ var NodeFileSystem = class {
285
375
  };
286
376
 
287
377
  // packages/scanner/src/repo-scanner.ts
288
- import path3 from "node:path";
378
+ import path4 from "node:path";
289
379
 
290
380
  // packages/scanner/src/stack-detector.ts
291
- import path2 from "node:path";
381
+ import path3 from "node:path";
292
382
  var packageJsonDetector = {
293
383
  filename: "package.json",
294
384
  detect(content, filePath) {
@@ -351,7 +441,7 @@ var packageJsonDetector = {
351
441
  stack,
352
442
  dependencies,
353
443
  projectType,
354
- projectName: pkg.name ?? path2.basename(path2.dirname(filePath))
444
+ projectName: pkg.name ?? path3.basename(path3.dirname(filePath))
355
445
  };
356
446
  }
357
447
  };
@@ -409,7 +499,7 @@ var csprojDetector = {
409
499
  stack.push({ name: "asp.net", type: "framework", version: match[2], source: filePath });
410
500
  }
411
501
  }
412
- return { stack, dependencies, projectType: "backend", projectName: path2.basename(filePath, ".csproj") };
502
+ return { stack, dependencies, projectType: "backend", projectName: path3.basename(filePath, ".csproj") };
413
503
  }
414
504
  };
415
505
  var pomXmlDetector = {
@@ -459,7 +549,7 @@ async function detectFromManifests(rootPath, fs) {
459
549
  }
460
550
  continue;
461
551
  }
462
- const manifestPath = path2.join(rootPath, detector.filename);
552
+ const manifestPath = path3.join(rootPath, detector.filename);
463
553
  if (await fs.exists(manifestPath)) {
464
554
  const content = await fs.readFile(manifestPath);
465
555
  return detector.detect(content, manifestPath);
@@ -488,7 +578,7 @@ async function detectFromExtensions(rootPath, fs) {
488
578
  }
489
579
 
490
580
  // packages/scanner/src/repo-scanner.ts
491
- function toPosix(p) {
581
+ function toPosix2(p) {
492
582
  return p.replace(/\\/g, "/");
493
583
  }
494
584
  var SOURCE_EXTENSIONS = /\.(tsx?|jsx?|php|cs|java|py|dart|vue|svelte)$/;
@@ -504,7 +594,7 @@ var RepoScanner = class {
504
594
  if (stack.length === 0) {
505
595
  stack = await detectFromExtensions(effectiveRoot, this.fs);
506
596
  }
507
- const projectName = manifestResult?.projectName ?? path3.basename(effectiveRoot);
597
+ const projectName = manifestResult?.projectName ?? path4.basename(effectiveRoot);
508
598
  const projectType = manifestResult?.projectType ?? "backend";
509
599
  const dependencies = manifestResult?.dependencies ?? [];
510
600
  const sourceFiles = await this.collectSourceFiles(effectiveRoot);
@@ -568,7 +658,7 @@ var RepoScanner = class {
568
658
  detectModules(rootPath, files) {
569
659
  const moduleMap = /* @__PURE__ */ new Map();
570
660
  for (const file of files) {
571
- const relative = toPosix(path3.relative(rootPath, file));
661
+ const relative = toPosix2(path4.relative(rootPath, file));
572
662
  const parts = relative.split("/");
573
663
  if (parts.length >= 2) {
574
664
  const moduleKey = parts.slice(0, 2).join("/");
@@ -578,7 +668,7 @@ var RepoScanner = class {
578
668
  }
579
669
  }
580
670
  return Array.from(moduleMap.entries()).map(([modulePath, moduleFiles]) => ({
581
- name: path3.basename(modulePath),
671
+ name: path4.basename(modulePath),
582
672
  path: modulePath,
583
673
  type: "module",
584
674
  filesCount: moduleFiles.length,
@@ -589,7 +679,7 @@ var RepoScanner = class {
589
679
  const entrypoints = [];
590
680
  if (projectType === "mobile") {
591
681
  for (const file of files) {
592
- const relative = toPosix(path3.relative(rootPath, file));
682
+ const relative = toPosix2(path4.relative(rootPath, file));
593
683
  if (relative.match(/src\/screens\/[^/]+\/index\.(tsx?|jsx?)$/)) {
594
684
  const screenName = relative.split("/").slice(-2, -1)[0];
595
685
  entrypoints.push({
@@ -602,11 +692,11 @@ var RepoScanner = class {
602
692
  }
603
693
  }
604
694
  for (const file of files) {
605
- const relative = toPosix(path3.relative(rootPath, file));
695
+ const relative = toPosix2(path4.relative(rootPath, file));
606
696
  if (relative.match(/^(index|main|App)\.(tsx?|jsx?|js)$/)) {
607
697
  entrypoints.push({
608
698
  type: "component",
609
- name: path3.basename(relative),
699
+ name: path4.basename(relative),
610
700
  path: relative,
611
701
  method: "main"
612
702
  });
@@ -620,7 +710,7 @@ var RepoScanner = class {
620
710
  for (const file of files) {
621
711
  try {
622
712
  const content = await this.fs.readFile(file);
623
- const relative = toPosix(path3.relative(rootPath, file));
713
+ const relative = toPosix2(path4.relative(rootPath, file));
624
714
  const urlMatches = content.matchAll(/https?:\/\/[^\s'"`,)]+/g);
625
715
  for (const match of urlMatches) {
626
716
  const url = match[0];
@@ -659,7 +749,7 @@ var RepoScanner = class {
659
749
  for (const file of files) {
660
750
  try {
661
751
  const s = await this.fs.stat(file);
662
- const relative = toPosix(path3.relative(rootPath, file));
752
+ const relative = toPosix2(path4.relative(rootPath, file));
663
753
  if (s.size > LARGE_FILE_THRESHOLD) {
664
754
  hotspots.push({
665
755
  path: relative,
@@ -675,8 +765,8 @@ var RepoScanner = class {
675
765
  };
676
766
 
677
767
  // packages/context/src/context-builder.ts
678
- import path4 from "node:path";
679
- function toPosix2(p) {
768
+ import path5 from "node:path";
769
+ function toPosix3(p) {
680
770
  return p.replace(/\\/g, "/");
681
771
  }
682
772
  var TOKENS_PER_CHAR = 0.25;
@@ -694,12 +784,12 @@ var ContextBuilder = class {
694
784
  return packs;
695
785
  }
696
786
  async buildModulePack(rootPath, moduleName, modulePath, repoIndex) {
697
- const fullModulePath = path4.join(rootPath, modulePath);
787
+ const fullModulePath = path5.join(rootPath, modulePath);
698
788
  let keyFiles = [];
699
789
  let totalSize = 0;
700
790
  try {
701
791
  const files = await this.fs.glob(fullModulePath, "\\.(tsx?|jsx?|php|cs|java)$");
702
- keyFiles = files.map((f) => toPosix2(path4.relative(rootPath, f))).slice(0, 10);
792
+ keyFiles = files.map((f) => toPosix3(path5.relative(rootPath, f))).slice(0, 10);
703
793
  for (const file of files.slice(0, 10)) {
704
794
  try {
705
795
  const stat3 = await this.fs.stat(file);
@@ -725,7 +815,7 @@ var ContextBuilder = class {
725
815
  };
726
816
 
727
817
  // packages/rules/src/compliance-engine.ts
728
- import path5 from "node:path";
818
+ import path6 from "node:path";
729
819
 
730
820
  // packages/rules/src/rule-catalog.ts
731
821
  var BACKEND_LANGUAGES = [
@@ -1029,9 +1119,6 @@ var CODE_QUALITY_RULES = [
1029
1119
  var ALL_RULES = [...SECURITY_RULES, ...CODE_QUALITY_RULES];
1030
1120
 
1031
1121
  // packages/rules/src/compliance-engine.ts
1032
- function toPosix3(p) {
1033
- return p.replace(/\\/g, "/");
1034
- }
1035
1122
  var SEVERITY_TO_PRIORITY = {
1036
1123
  critical: "P0",
1037
1124
  high: "P1",
@@ -1089,7 +1176,7 @@ var ComplianceEngine = class {
1089
1176
  try {
1090
1177
  const content = await this.fs.readFile(file);
1091
1178
  const lines = content.split("\n");
1092
- const relative = toPosix3(path5.relative(rootPath, file));
1179
+ const relative = toPosix(path6.relative(rootPath, file));
1093
1180
  for (const pattern of rule.detection.patterns) {
1094
1181
  const regex = new RegExp(pattern, "gi");
1095
1182
  for (let i = 0; i < lines.length; i++) {
@@ -1124,7 +1211,7 @@ var ComplianceEngine = class {
1124
1211
  for (const pattern of rule.detection.patterns) {
1125
1212
  const matchingFiles = await this.fs.glob(rootPath, pattern);
1126
1213
  for (const file of matchingFiles) {
1127
- const relative = toPosix3(path5.relative(rootPath, file));
1214
+ const relative = toPosix(path6.relative(rootPath, file));
1128
1215
  allEvidence.push({
1129
1216
  file: relative,
1130
1217
  line: 0,
@@ -1162,7 +1249,7 @@ var ComplianceEngine = class {
1162
1249
  pillar: rule.category,
1163
1250
  severity: rule.severity,
1164
1251
  evidence: jsFiles.slice(0, 5).map((f) => ({
1165
- file: toPosix3(path5.relative(rootPath, f)),
1252
+ file: toPosix(path6.relative(rootPath, f)),
1166
1253
  line: 0,
1167
1254
  snippet: "JavaScript file in a TypeScript project"
1168
1255
  })),
@@ -1201,16 +1288,16 @@ var Installer = class {
1201
1288
  const findings = await compliance.evaluate(effectiveRoot, repoIndex);
1202
1289
  const contextBuilder = new ContextBuilder(fs);
1203
1290
  const contextPacks = await contextBuilder.buildPacks(effectiveRoot, repoIndex);
1204
- const memoryDir = path6.join(effectiveRoot, ".legacy-squad", "memory");
1205
- await mkdir(memoryDir, { recursive: true });
1206
- const repoIndexPath = path6.join(memoryDir, "repo-index.json");
1207
- const findingsPath = path6.join(memoryDir, "findings.json");
1208
- const contextPacksPath = path6.join(memoryDir, "context-packs.json");
1209
- await writeFile(repoIndexPath, JSON.stringify(repoIndex, null, 2), "utf-8");
1210
- await writeFile(findingsPath, JSON.stringify(findings, null, 2), "utf-8");
1211
- await writeFile(contextPacksPath, JSON.stringify(contextPacks, null, 2), "utf-8");
1212
- const configDir = path6.join(effectiveRoot, ".legacy-squad", "config");
1213
- await mkdir(configDir, { recursive: true });
1291
+ const memoryDir = path7.join(effectiveRoot, ".legacy-squad", "memory");
1292
+ await mkdir2(memoryDir, { recursive: true });
1293
+ const repoIndexPath = path7.join(memoryDir, "repo-index.json");
1294
+ const findingsPath = path7.join(memoryDir, "findings", "index.json");
1295
+ const contextPacksPath = path7.join(memoryDir, "context-packs.json");
1296
+ await writeFile2(repoIndexPath, JSON.stringify(repoIndex, null, 2), "utf-8");
1297
+ await new FindingsWriter(fs).write(findings, memoryDir);
1298
+ await writeFile2(contextPacksPath, JSON.stringify(contextPacks, null, 2), "utf-8");
1299
+ const configDir = path7.join(effectiveRoot, ".legacy-squad", "config");
1300
+ await mkdir2(configDir, { recursive: true });
1214
1301
  const projectConfig = {
1215
1302
  project: {
1216
1303
  name: repoIndex.project.name,
@@ -1231,19 +1318,21 @@ var Installer = class {
1231
1318
  mode: { execution: "read_only" },
1232
1319
  ide: { primary: "claude-code" },
1233
1320
  installed_at: (/* @__PURE__ */ new Date()).toISOString(),
1234
- framework_version: "1.0.0"
1321
+ framework_version: "1.3.0"
1235
1322
  };
1236
- await writeFile(
1237
- path6.join(configDir, "project.yaml"),
1323
+ await writeFile2(
1324
+ path7.join(configDir, "project.yaml"),
1238
1325
  this.toYaml(projectConfig),
1239
1326
  "utf-8"
1240
1327
  );
1241
- await mkdir(path6.join(effectiveRoot, ".legacy-squad", "outputs", "reports"), { recursive: true });
1242
- await mkdir(path6.join(effectiveRoot, ".legacy-squad", "outputs", "assessments"), { recursive: true });
1243
- await mkdir(path6.join(effectiveRoot, ".legacy-squad", "logs"), { recursive: true });
1244
- const claudeCommandsDir = path6.join(effectiveRoot, ".claude", "commands", "legacy-squad");
1245
- await mkdir(claudeCommandsDir, { recursive: true });
1328
+ await mkdir2(path7.join(effectiveRoot, ".legacy-squad", "outputs", "reports"), { recursive: true });
1329
+ await mkdir2(path7.join(effectiveRoot, ".legacy-squad", "outputs", "assessments"), { recursive: true });
1330
+ await mkdir2(path7.join(effectiveRoot, ".legacy-squad", "logs"), { recursive: true });
1331
+ const claudeCommandsRoot = path7.join(effectiveRoot, ".claude", "commands");
1332
+ const claudeCommandsDir = path7.join(claudeCommandsRoot, "legacy-squad");
1333
+ await mkdir2(claudeCommandsDir, { recursive: true });
1246
1334
  await this.copySlashCommands(templateDir, claudeCommandsDir);
1335
+ await this.copyOrchestrator(templateDir, claudeCommandsRoot);
1247
1336
  await this.generateAgentsMd(effectiveRoot, repoIndex.project.name);
1248
1337
  const logContent = [
1249
1338
  `Legacy Squad Framework \u2014 Install Log`,
@@ -1259,8 +1348,8 @@ var Installer = class {
1259
1348
  `Requested root: ${projectRoot}`,
1260
1349
  `Effective root: ${effectiveRoot}`
1261
1350
  ].join("\n");
1262
- await writeFile(
1263
- path6.join(effectiveRoot, ".legacy-squad", "logs", "install.log"),
1351
+ await writeFile2(
1352
+ path7.join(effectiveRoot, ".legacy-squad", "logs", "install.log"),
1264
1353
  logContent,
1265
1354
  "utf-8"
1266
1355
  );
@@ -1294,15 +1383,30 @@ var Installer = class {
1294
1383
  "scan.md"
1295
1384
  ];
1296
1385
  for (const file of commandFiles) {
1297
- const sourcePath = path6.join(templateDir, file);
1298
- const targetPath = path6.join(targetDir, file);
1386
+ const sourcePath = path7.join(templateDir, file);
1387
+ const targetPath = path7.join(targetDir, file);
1299
1388
  try {
1300
1389
  const content = await readFile2(sourcePath, "utf-8");
1301
- await writeFile(targetPath, content, "utf-8");
1390
+ await writeFile2(targetPath, content, "utf-8");
1302
1391
  } catch {
1303
1392
  }
1304
1393
  }
1305
1394
  }
1395
+ /**
1396
+ * DA-012: copia o template do orchestrator para `.claude/commands/legacy-squad.md`
1397
+ * (raiz de commands, não subdir). No Claude Code, arquivos na raiz de commands viram
1398
+ * o comando puro `/<filename>` — então este caminho expressa `/legacy-squad` (validado
1399
+ * emp.). Falha silenciosamente se o template não existir, no mesmo estilo de copySlashCommands.
1400
+ */
1401
+ async copyOrchestrator(templateDir, commandsRoot) {
1402
+ const sourcePath = path7.join(templateDir, "legacy-squad.md");
1403
+ const targetPath = path7.join(commandsRoot, "legacy-squad.md");
1404
+ try {
1405
+ const content = await readFile2(sourcePath, "utf-8");
1406
+ await writeFile2(targetPath, content, "utf-8");
1407
+ } catch {
1408
+ }
1409
+ }
1306
1410
  async generateAgentsMd(projectRoot, projectName) {
1307
1411
  const lines = [
1308
1412
  `# Legacy Squad Agents \u2014 ${projectName}`,
@@ -1321,7 +1425,7 @@ var Installer = class {
1321
1425
  lines.push("Leia `.legacy-squad/memory/` para contexto e produza o assessment em `.legacy-squad/outputs/assessments/`.");
1322
1426
  lines.push("");
1323
1427
  }
1324
- await writeFile(path6.join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8");
1428
+ await writeFile2(path7.join(projectRoot, "AGENTS.md"), lines.join("\n"), "utf-8");
1325
1429
  }
1326
1430
  toYaml(obj, indent = 0) {
1327
1431
  const lines = [];
@@ -1346,23 +1450,58 @@ var Installer = class {
1346
1450
 
1347
1451
  // packages/agents/src/doctor.ts
1348
1452
  import { stat as stat2 } from "node:fs/promises";
1349
- import path7 from "node:path";
1453
+ import path8 from "node:path";
1350
1454
  var Doctor = class {
1351
1455
  async check(projectRoot) {
1352
1456
  const checks = [];
1353
1457
  checks.push(await this.checkFile(projectRoot, ".legacy-squad/memory/repo-index.json", "Repo Index"));
1354
- checks.push(await this.checkFile(projectRoot, ".legacy-squad/memory/findings.json", "Findings"));
1458
+ checks.push(await this.checkFindingsStructure(projectRoot));
1355
1459
  checks.push(await this.checkFile(projectRoot, ".legacy-squad/memory/context-packs.json", "Context Packs"));
1356
1460
  checks.push(await this.checkFile(projectRoot, ".legacy-squad/config/project.yaml", "Project Config"));
1357
1461
  checks.push(await this.checkDir(projectRoot, ".claude/commands/legacy-squad", "Claude Code Agents"));
1462
+ checks.push(await this.checkFile(projectRoot, ".claude/commands/legacy-squad.md", "Orchestrator /legacy-squad"));
1358
1463
  checks.push(await this.checkFile(projectRoot, "AGENTS.md", "Codex AGENTS.md"));
1359
1464
  checks.push(await this.checkDir(projectRoot, ".legacy-squad/outputs/assessments", "Assessments Dir"));
1360
1465
  checks.push(await this.checkDir(projectRoot, ".legacy-squad/outputs/reports", "Reports Dir"));
1361
1466
  return checks;
1362
1467
  }
1468
+ /**
1469
+ * Verifica a estrutura de findings conforme DA-011:
1470
+ * - `findings/index.json` presente → ok (estrutura particionada)
1471
+ * - `findings.json` presente sem `findings/` → error com orientação de re-install
1472
+ * - Nenhum dos dois → error (não instalado)
1473
+ */
1474
+ async checkFindingsStructure(root) {
1475
+ const indexPath = path8.join(root, ".legacy-squad", "memory", "findings", "index.json");
1476
+ const legacyPath = path8.join(root, ".legacy-squad", "memory", "findings.json");
1477
+ if (await this.pathExists(indexPath)) {
1478
+ return { name: "Findings", status: "ok", message: ".legacy-squad/memory/findings/index.json \u2713" };
1479
+ }
1480
+ if (await this.pathExists(legacyPath)) {
1481
+ return {
1482
+ name: "Findings",
1483
+ status: "error",
1484
+ message: "Estrutura legada detectada (.legacy-squad/memory/findings.json). Execute npx legacy-squad install para regenerar na estrutura particionada (DA-011). A migra\xE7\xE3o n\xE3o \xE9 autom\xE1tica \u2014 nenhum dado foi alterado."
1485
+ };
1486
+ }
1487
+ return {
1488
+ name: "Findings",
1489
+ status: "error",
1490
+ message: ".legacy-squad/memory/findings/index.json not found"
1491
+ };
1492
+ }
1493
+ /** Retorna true se o path existir (arquivo ou diretório), false caso contrário. */
1494
+ async pathExists(fullPath) {
1495
+ try {
1496
+ await stat2(fullPath);
1497
+ return true;
1498
+ } catch {
1499
+ return false;
1500
+ }
1501
+ }
1363
1502
  async checkFile(root, relativePath, label) {
1364
1503
  try {
1365
- const fullPath = path7.join(root, relativePath);
1504
+ const fullPath = path8.join(root, relativePath);
1366
1505
  const s = await stat2(fullPath);
1367
1506
  if (s.size === 0) {
1368
1507
  return { name: label, status: "warning", message: `${relativePath} exists but is empty` };
@@ -1374,7 +1513,7 @@ var Doctor = class {
1374
1513
  }
1375
1514
  async checkDir(root, relativePath, label) {
1376
1515
  try {
1377
- const fullPath = path7.join(root, relativePath);
1516
+ const fullPath = path8.join(root, relativePath);
1378
1517
  const s = await stat2(fullPath);
1379
1518
  if (!s.isDirectory()) {
1380
1519
  return { name: label, status: "error", message: `${relativePath} is not a directory` };
@@ -1386,19 +1525,281 @@ var Doctor = class {
1386
1525
  }
1387
1526
  };
1388
1527
 
1528
+ // packages/agents/src/lifecycle-detector.ts
1529
+ import path9 from "node:path";
1530
+ var REPO_INDEX_PATH = ".legacy-squad/memory/repo-index.json";
1531
+ var FINDINGS_INDEX_PATH = ".legacy-squad/memory/findings/index.json";
1532
+ var assessmentPath = (slug) => `.legacy-squad/outputs/assessments/${slug}-assessment.md`;
1533
+ var PRS_PATH = ".legacy-squad/outputs/reports/PRS.md";
1534
+ var SDD_PATH = ".legacy-squad/outputs/sdd/SDD.md";
1535
+ var MMP_PATH = ".legacy-squad/outputs/mmp/MMP.md";
1536
+ var SPECS_PATH = ".legacy-squad/outputs/specs/INDEX.md";
1537
+ var MATURITY_LABELS = {
1538
+ 1: "Unknown",
1539
+ 2: "Understood",
1540
+ 3: "Assessed",
1541
+ 4: "Planned",
1542
+ 5: "Modernization Ready",
1543
+ 6: "Continuously Modernized"
1544
+ };
1545
+ var STEPS = [
1546
+ {
1547
+ id: "scan",
1548
+ label: "Scan",
1549
+ command: null,
1550
+ phase: "discovery",
1551
+ target: REPO_INDEX_PATH,
1552
+ reason: "Rode `npx legacy-squad install` para escanear o projeto e gerar o repo-index."
1553
+ },
1554
+ {
1555
+ id: "security",
1556
+ label: "Security assessment",
1557
+ command: "/legacy-squad:security",
1558
+ phase: "assessment",
1559
+ target: assessmentPath("security"),
1560
+ reason: "Inicie os assessments rodando `/legacy-squad:security`."
1561
+ },
1562
+ {
1563
+ id: "architecture",
1564
+ label: "Architecture assessment",
1565
+ command: "/legacy-squad:architecture",
1566
+ phase: "assessment",
1567
+ target: assessmentPath("architecture"),
1568
+ reason: "Rode `/legacy-squad:architecture`."
1569
+ },
1570
+ {
1571
+ id: "legacy-code",
1572
+ label: "Legacy code assessment",
1573
+ command: "/legacy-squad:legacy-code",
1574
+ phase: "assessment",
1575
+ target: assessmentPath("legacy-code"),
1576
+ reason: "Rode `/legacy-squad:legacy-code`."
1577
+ },
1578
+ {
1579
+ id: "business-rules",
1580
+ label: "Business rules assessment",
1581
+ command: "/legacy-squad:business-rules",
1582
+ phase: "assessment",
1583
+ target: assessmentPath("business-rules"),
1584
+ reason: "Rode `/legacy-squad:business-rules`."
1585
+ },
1586
+ {
1587
+ id: "modernization",
1588
+ label: "Modernization assessment",
1589
+ command: "/legacy-squad:modernization",
1590
+ phase: "assessment",
1591
+ target: assessmentPath("modernization"),
1592
+ reason: "Rode `/legacy-squad:modernization`."
1593
+ },
1594
+ {
1595
+ id: "generate-prs",
1596
+ label: "PRS",
1597
+ command: "/legacy-squad:generate-prs",
1598
+ phase: "assessment",
1599
+ target: PRS_PATH,
1600
+ reason: "Consolide o diagn\xF3stico com `/legacy-squad:generate-prs`."
1601
+ },
1602
+ {
1603
+ id: "generate-sdd",
1604
+ label: "SDD",
1605
+ command: "/legacy-squad:generate-sdd",
1606
+ phase: "design",
1607
+ target: SDD_PATH,
1608
+ reason: "Gere o desenho t\xE9cnico com `/legacy-squad:generate-sdd`."
1609
+ },
1610
+ {
1611
+ id: "generate-mmp",
1612
+ label: "MMP",
1613
+ command: "/legacy-squad:generate-mmp",
1614
+ phase: "planning",
1615
+ target: MMP_PATH,
1616
+ reason: "Gere o plano mestre com `/legacy-squad:generate-mmp`."
1617
+ },
1618
+ {
1619
+ id: "generate-specs",
1620
+ label: "Execution Specs",
1621
+ command: "/legacy-squad:generate-specs",
1622
+ phase: "execution",
1623
+ target: SPECS_PATH,
1624
+ reason: "Decomponha em specs com `/legacy-squad:generate-specs`."
1625
+ }
1626
+ ];
1627
+ var PHASE_ORDER = [
1628
+ { id: "discovery", label: "Discovery" },
1629
+ { id: "assessment", label: "Assessment" },
1630
+ { id: "design", label: "Design" },
1631
+ { id: "planning", label: "Planning" },
1632
+ { id: "execution", label: "Execution" }
1633
+ ];
1634
+ var ASSESSMENT_STEP_IDS = [
1635
+ "security",
1636
+ "architecture",
1637
+ "legacy-code",
1638
+ "business-rules",
1639
+ "modernization"
1640
+ ];
1641
+ var LifecycleDetector = class {
1642
+ constructor(fs) {
1643
+ this.fs = fs;
1644
+ }
1645
+ fs;
1646
+ /** Detecta e retorna o snapshot determinístico do lifecycle em `projectRoot`. */
1647
+ async detect(projectRoot) {
1648
+ const done = /* @__PURE__ */ new Map();
1649
+ for (const step of STEPS) {
1650
+ done.set(step.id, await this.fs.exists(path9.join(projectRoot, step.target)));
1651
+ }
1652
+ const installed = done.get("scan") === true;
1653
+ const project = installed ? await this.readProject(projectRoot) : null;
1654
+ const findingCount = installed ? await this.readFindingCount(projectRoot) : 0;
1655
+ const maturityLevel = this.computeMaturity(done);
1656
+ return {
1657
+ project,
1658
+ installed,
1659
+ findingCount,
1660
+ maturityLevel,
1661
+ maturityLabel: MATURITY_LABELS[maturityLevel],
1662
+ phases: this.buildPhases(done),
1663
+ nextStep: this.computeNextStep(done),
1664
+ complete: done.get("generate-specs") === true
1665
+ };
1666
+ }
1667
+ /** Agrupa os passos por fase, calculando a contagem de concluídos. */
1668
+ buildPhases(done) {
1669
+ return PHASE_ORDER.map(({ id, label }) => {
1670
+ const steps = STEPS.filter((s) => s.phase === id).map((s) => ({
1671
+ id: s.id,
1672
+ label: s.label,
1673
+ command: s.command,
1674
+ done: done.get(s.id) === true
1675
+ }));
1676
+ return {
1677
+ id,
1678
+ label,
1679
+ steps,
1680
+ doneCount: steps.filter((s) => s.done).length,
1681
+ totalCount: steps.length
1682
+ };
1683
+ });
1684
+ }
1685
+ /** Deriva o Maturity Level (§10) a partir dos artefatos presentes. */
1686
+ computeMaturity(done) {
1687
+ if (done.get("generate-specs")) return 5;
1688
+ if (done.get("generate-mmp")) return 4;
1689
+ if (ASSESSMENT_STEP_IDS.every((id) => done.get(id))) return 3;
1690
+ if (done.get("scan")) return 2;
1691
+ return 1;
1692
+ }
1693
+ /** Próximo passo = primeiro `target` ausente na ordem canônica; null se tudo concluído. */
1694
+ computeNextStep(done) {
1695
+ const [scanStep] = STEPS;
1696
+ if (!done.get("scan")) {
1697
+ return { id: "install", command: null, reason: scanStep.reason };
1698
+ }
1699
+ for (const step of STEPS) {
1700
+ if (!done.get(step.id)) {
1701
+ return { id: step.id, command: step.command, reason: step.reason };
1702
+ }
1703
+ }
1704
+ return null;
1705
+ }
1706
+ /** Lê nome, tipo e stack do repo-index; null se ausente ou ilegível. */
1707
+ async readProject(root) {
1708
+ try {
1709
+ const raw = await this.fs.readFile(path9.join(root, REPO_INDEX_PATH));
1710
+ const idx = JSON.parse(raw);
1711
+ return {
1712
+ name: idx.project?.name ?? "unknown",
1713
+ type: idx.project?.type ?? "unknown",
1714
+ stack: (idx.stack ?? []).map((s) => s.name ?? "").filter((n) => n.length > 0)
1715
+ };
1716
+ } catch {
1717
+ return null;
1718
+ }
1719
+ }
1720
+ /** Conta os achados no index slim (DA-011); 0 se ausente ou ilegível. */
1721
+ async readFindingCount(root) {
1722
+ try {
1723
+ const raw = await this.fs.readFile(path9.join(root, FINDINGS_INDEX_PATH));
1724
+ const parsed = JSON.parse(raw);
1725
+ return Array.isArray(parsed) ? parsed.length : 0;
1726
+ } catch {
1727
+ return 0;
1728
+ }
1729
+ }
1730
+ };
1731
+
1732
+ // packages/output/src/dashboard-renderer.ts
1733
+ var RULE = "\u2501".repeat(40);
1734
+ var STEP_DISPLAY = {
1735
+ scan: "scan",
1736
+ security: "security",
1737
+ architecture: "architecture",
1738
+ "legacy-code": "legacy-code",
1739
+ "business-rules": "business-rules",
1740
+ modernization: "modernization",
1741
+ "generate-prs": "PRS",
1742
+ "generate-sdd": "SDD",
1743
+ "generate-mmp": "MMP",
1744
+ "generate-specs": "Specs"
1745
+ };
1746
+ var display = (id) => STEP_DISPLAY[id] ?? id;
1747
+ function phaseIcon(phase) {
1748
+ if (phase.totalCount > 0 && phase.doneCount === phase.totalCount) return "\u2713";
1749
+ if (phase.doneCount > 0) return "\u25D0";
1750
+ return "\xB7";
1751
+ }
1752
+ function phaseDetail(phase) {
1753
+ if (phase.steps.length === 1) {
1754
+ return display(phase.steps[0].id);
1755
+ }
1756
+ const names = phase.steps.map((s) => display(s.id)).join(" \xB7 ");
1757
+ return `${phase.doneCount}/${phase.totalCount} ${names}`;
1758
+ }
1759
+ var DashboardRenderer = class {
1760
+ render(snapshot) {
1761
+ const lines = [RULE];
1762
+ if (snapshot.project) {
1763
+ const stack = snapshot.project.stack.join(", ");
1764
+ lines.push(` Legacy Squad \u2014 ${snapshot.project.name}`);
1765
+ lines.push(` ${snapshot.project.type}${stack ? ` \xB7 ${stack}` : ""}`);
1766
+ } else {
1767
+ lines.push(" Legacy Squad");
1768
+ lines.push(" (framework n\xE3o instalado neste diret\xF3rio)");
1769
+ }
1770
+ lines.push(RULE, "");
1771
+ lines.push(` Maturity Level ${snapshot.maturityLevel} \u2014 ${snapshot.maturityLabel}`);
1772
+ lines.push(` Findings ${snapshot.findingCount}`, "");
1773
+ const labelWidth = Math.max(...snapshot.phases.map((p) => p.label.length));
1774
+ for (const phase of snapshot.phases) {
1775
+ lines.push(` ${phaseIcon(phase)} ${phase.label.padEnd(labelWidth)} ${phaseDetail(phase)}`);
1776
+ }
1777
+ lines.push("");
1778
+ if (snapshot.nextStep) {
1779
+ const target = snapshot.nextStep.command ?? "npx legacy-squad install";
1780
+ lines.push(` \u2192 Pr\xF3ximo passo: ${target}`);
1781
+ lines.push(` ${snapshot.nextStep.reason}`);
1782
+ } else {
1783
+ lines.push(" \u2713 Lifecycle V1 completo \u2014 specs prontas para execu\xE7\xE3o.");
1784
+ }
1785
+ lines.push(RULE);
1786
+ return lines.join("\n");
1787
+ }
1788
+ };
1789
+
1389
1790
  // apps/cli/src/index.ts
1390
1791
  var program = new Command();
1391
1792
  function getTemplateDir() {
1392
- const cliDir = path8.dirname(fileURLToPath(import.meta.url));
1393
- const bundledPath = path8.resolve(cliDir, "templates", "claude-commands");
1793
+ const cliDir = path10.dirname(fileURLToPath(import.meta.url));
1794
+ const bundledPath = path10.resolve(cliDir, "templates", "claude-commands");
1394
1795
  if (existsSync(bundledPath)) return bundledPath;
1395
- const devPath = path8.resolve(cliDir, "..", "..", "..", "templates", "claude-commands");
1796
+ const devPath = path10.resolve(cliDir, "..", "..", "..", "templates", "claude-commands");
1396
1797
  if (existsSync(devPath)) return devPath;
1397
1798
  throw new Error("Templates not found. Run from the framework root or use the published package.");
1398
1799
  }
1399
- program.name("legacy-squad").description("AI-Powered Legacy Modernization Platform \u2014 Understand. Plan. Modernize.").version("1.0.0");
1800
+ program.name("legacy-squad").description("AI-Powered Legacy Modernization Platform \u2014 Understand. Plan. Modernize.").version("1.3.0");
1400
1801
  program.command("install").description("Install Legacy Squad Framework inside the current project").option("-p, --path <dir>", "Project root directory", ".").action(async (opts) => {
1401
- const projectRoot = path8.resolve(opts.path);
1802
+ const projectRoot = path10.resolve(opts.path);
1402
1803
  const templateDir = getTemplateDir();
1403
1804
  console.log("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
1404
1805
  console.log(" Legacy Squad Framework V1 \u2014 Install");
@@ -1440,21 +1841,21 @@ program.command("install").description("Install Legacy Squad Framework inside th
1440
1841
  console.log(" 1. Open Claude Code: claude");
1441
1842
  console.log("");
1442
1843
  console.log(" Analysis (run in order):");
1443
- console.log(" /legacy-squad-security");
1444
- console.log(" /legacy-squad-architecture");
1445
- console.log(" /legacy-squad-legacy-code");
1446
- console.log(" /legacy-squad-business-rules");
1447
- console.log(" /legacy-squad-modernization");
1844
+ console.log(" /legacy-squad:security");
1845
+ console.log(" /legacy-squad:architecture");
1846
+ console.log(" /legacy-squad:legacy-code");
1847
+ console.log(" /legacy-squad:business-rules");
1848
+ console.log(" /legacy-squad:modernization");
1448
1849
  console.log("");
1449
1850
  console.log(" Consolidated artifacts (run after analysis):");
1450
- console.log(" /legacy-squad-generate-prs (Product Refactor Specification)");
1451
- console.log(" /legacy-squad-generate-sdd (Software Design Document)");
1452
- console.log(" /legacy-squad-generate-mmp (Modernization Master Plan)");
1453
- console.log(" /legacy-squad-generate-specs (Execution Specs for V2)");
1851
+ console.log(" /legacy-squad:generate-prs (Product Refactor Specification)");
1852
+ console.log(" /legacy-squad:generate-sdd (Software Design Document)");
1853
+ console.log(" /legacy-squad:generate-mmp (Modernization Master Plan)");
1854
+ console.log(" /legacy-squad:generate-specs (Execution Specs for V2)");
1454
1855
  console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
1455
1856
  });
1456
1857
  program.command("scan").description("Re-scan the project and update .legacy-squad/memory/").option("-p, --path <dir>", "Project root directory", ".").action(async (opts) => {
1457
- const projectRoot = path8.resolve(opts.path);
1858
+ const projectRoot = path10.resolve(opts.path);
1458
1859
  console.log(`
1459
1860
  \u{1F50D} Re-scanning: ${projectRoot}
1460
1861
  `);
@@ -1468,19 +1869,19 @@ program.command("scan").description("Re-scan the project and update .legacy-squa
1468
1869
  }
1469
1870
  const compliance = new ComplianceEngine(fs);
1470
1871
  const findings = await compliance.evaluate(effectiveRoot, repoIndex);
1471
- const { mkdir: mkdir2, writeFile: writeFile2 } = await import("node:fs/promises");
1472
- const memoryDir = path8.join(effectiveRoot, ".legacy-squad", "memory");
1473
- await mkdir2(memoryDir, { recursive: true });
1474
- await writeFile2(path8.join(memoryDir, "repo-index.json"), JSON.stringify(repoIndex, null, 2), "utf-8");
1475
- await writeFile2(path8.join(memoryDir, "findings.json"), JSON.stringify(findings, null, 2), "utf-8");
1872
+ const { mkdir: mkdir3, writeFile: writeFile3 } = await import("node:fs/promises");
1873
+ const memoryDir = path10.join(effectiveRoot, ".legacy-squad", "memory");
1874
+ await mkdir3(memoryDir, { recursive: true });
1875
+ await writeFile3(path10.join(memoryDir, "repo-index.json"), JSON.stringify(repoIndex, null, 2), "utf-8");
1876
+ await new FindingsWriter(fs).write(findings, memoryDir);
1476
1877
  console.log(`\u2705 Stack: ${repoIndex.stack.map((s) => s.name).join(", ")}`);
1477
1878
  console.log(`\u{1F4E6} Modules: ${repoIndex.modules.length}`);
1478
1879
  console.log(`\u{1F512} Findings: ${findings.length}`);
1479
- console.log(`\u{1F4C4} Updated: ${path8.join(effectiveRoot, ".legacy-squad", "memory")}
1880
+ console.log(`\u{1F4C4} Updated: ${path10.join(effectiveRoot, ".legacy-squad", "memory")}
1480
1881
  `);
1481
1882
  });
1482
1883
  program.command("doctor").description("Verify Legacy Squad installation health").option("-p, --path <dir>", "Project root directory", ".").action(async (opts) => {
1483
- const projectRoot = path8.resolve(opts.path);
1884
+ const projectRoot = path10.resolve(opts.path);
1484
1885
  console.log("\n\u{1FA7A} Legacy Squad Doctor\n");
1485
1886
  const doctor = new Doctor();
1486
1887
  const checks = await doctor.check(projectRoot);
@@ -1497,4 +1898,16 @@ program.command("doctor").description("Verify Legacy Squad installation health")
1497
1898
  console.log("\n\u2705 All checks passed.\n");
1498
1899
  }
1499
1900
  });
1901
+ program.command("status").description("Show the project lifecycle dashboard and the recommended next step").option("-p, --path <dir>", "Project root directory", ".").option("--json", "Output the raw lifecycle snapshot as JSON").action(async (opts) => {
1902
+ const projectRoot = path10.resolve(opts.path);
1903
+ const fs = new NodeFileSystem();
1904
+ const snapshot = await new LifecycleDetector(fs).detect(projectRoot);
1905
+ if (opts.json) {
1906
+ console.log(JSON.stringify(snapshot, null, 2));
1907
+ return;
1908
+ }
1909
+ console.log("");
1910
+ console.log(new DashboardRenderer().render(snapshot));
1911
+ console.log("");
1912
+ });
1500
1913
  program.parse();