@planu/cli 3.9.11 → 3.9.12

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 (56) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/config/criteria-injection-rules.json +1 -1
  3. package/dist/config/dor-dod-items.json +3 -3
  4. package/dist/config/hook-templates/planu-spec-sanctity.sh +13 -3
  5. package/dist/config/server-instructions.js +1 -1
  6. package/dist/config/server-instructions.ts +1 -1
  7. package/dist/config/skill-templates/planu-new-spec.md +1 -1
  8. package/dist/config/skill-templates/planu-resume-work.md +1 -1
  9. package/dist/config/subagent-templates/planu-readiness-auditor.md +3 -3
  10. package/dist/config/subagent-templates/planu-spec-implementer.md +1 -1
  11. package/dist/config/workflow-conventions-catalog.json +3 -3
  12. package/dist/core/spec-api.js +1 -1
  13. package/dist/engine/agent-generator/builders.js +1 -1
  14. package/dist/engine/autopilot/handlers-b2.d.ts +2 -2
  15. package/dist/engine/autopilot/handlers-b2.js +15 -19
  16. package/dist/engine/ci-generator/local-script.js +4 -4
  17. package/dist/engine/ci-generator/planu-steps.js +4 -5
  18. package/dist/engine/compliance/auto-remediator.js +0 -1
  19. package/dist/engine/drift/violation-resolver.js +0 -1
  20. package/dist/engine/health/auto-fixer.js +1 -33
  21. package/dist/engine/living-spec-analyzer.js +3 -34
  22. package/dist/engine/planu-config-writer.js +1 -1
  23. package/dist/engine/scan-project/index.js +2 -2
  24. package/dist/engine/skill-generator/skills-content.js +1 -1
  25. package/dist/engine/spec-migrator/drift-detector.js +1 -1
  26. package/dist/engine/spec-migrator/lean-migration.js +5 -4
  27. package/dist/engine/spec-migrator/planu-root-cleaner.js +1 -1
  28. package/dist/engine/spec-registry/packager.d.ts +1 -1
  29. package/dist/engine/spec-registry/packager.js +2 -2
  30. package/dist/engine/spec-registry/validator.js +1 -2
  31. package/dist/engine/spec-splitter.js +2 -2
  32. package/dist/engine/spec-summary-html/report-renderer.d.ts +3 -4
  33. package/dist/engine/spec-summary-html/report-renderer.js +6 -135
  34. package/dist/engine/spec-summary-html.js +1 -1
  35. package/dist/engine/universal-rules/rules/planu-english-specs.js +4 -6
  36. package/dist/server/routes/specs.js +1 -1
  37. package/dist/tools/create-spec/autopilot-analyzer.js +1 -1
  38. package/dist/tools/create-spec/spec-builder.js +1 -1
  39. package/dist/tools/decompose-spec.js +1 -1
  40. package/dist/tools/git/pr-ops.js +2 -2
  41. package/dist/tools/git/sync-ops.js +1 -1
  42. package/dist/tools/reconcile-spec-living-handler.js +1 -1
  43. package/dist/tools/reconcile-spec.js +1 -1
  44. package/dist/tools/registry/install.js +27 -29
  45. package/dist/tools/reverse-engineer/handler.js +1 -1
  46. package/dist/tools/schemas/registry.js +1 -1
  47. package/dist/tools/spec-coverage.js +2 -2
  48. package/dist/tools/spec-templates.d.ts +1 -1
  49. package/dist/tools/spec-templates.js +17 -9
  50. package/dist/tools/tool-registry/group-infra.js +1 -1
  51. package/dist/tools/tool-registry/group-platform.js +1 -1
  52. package/dist/types/spec-templates.d.ts +3 -5
  53. package/package.json +1 -1
  54. package/src/i18n/messages/en.json +3 -3
  55. package/src/i18n/messages/es.json +3 -3
  56. package/src/i18n/messages/pt.json +3 -3
@@ -289,8 +289,8 @@ export function buildChildSpecs(original, proposals, nextIdFn) {
289
289
  createdAt: now,
290
290
  updatedAt: now,
291
291
  specPath: original.specPath.replace(/\/[^/]+\/spec\.md$/, `/${childId}-${childSlug}/spec.md`),
292
- technicalPath: original.technicalPath.replace(/\/[^/]+\/technical\.md$/, `/${childId}-${childSlug}/technical.md`),
293
- progressPath: original.progressPath?.replace(/\/[^/]+\/progress\.md$/, `/${childId}-${childSlug}/progress.md`),
292
+ technicalPath: original.specPath.replace(/\/[^/]+\/spec\.md$/, `/${childId}-${childSlug}/spec.md`),
293
+ progressPath: undefined,
294
294
  estimation,
295
295
  actuals: null,
296
296
  target: original.target,
@@ -1,9 +1,8 @@
1
1
  import type { Spec } from '../../types/index.js';
2
2
  /**
3
- * Regenerate executive-report.html and technical-report.html for each spec.
4
- * Reads actual spec.md and technical.md content to produce rich HTML reports.
5
- * Also generates structural reports as fallback data source.
6
- * Best-effort per spec — one failure doesn't block others.
3
+ * Per-spec HTML reports are deprecated. The unified docs site renders spec.md
4
+ * on demand, so this function intentionally does not write files inside
5
+ * planu/specs/SPEC-* directories.
7
6
  */
8
7
  export declare function regeneratePerSpecReports(specs: Spec[]): Promise<void>;
9
8
  //# sourceMappingURL=report-renderer.d.ts.map
@@ -1,139 +1,10 @@
1
- import { writeFile, readFile } from 'node:fs/promises';
2
- import { join, dirname } from 'node:path';
3
- import { generatePerSpecExecutiveReport, generatePerSpecTechnicalReport, } from '../doc-generator/per-spec-report/index.js';
4
- import { buildSpecMetricsSection, buildEffortDistributionSvg, buildSpecDependenciesSection, buildSpecTagsSection, } from '../doc-generator/per-spec-report/spec-section-builders.js';
5
- import { extractPerSpecData } from '../doc-generator/per-spec-report/spec-data-extractor.js';
6
- import { markdownToHtml } from '../markdown-renderer.js';
7
- const STATUS_COLORS = {
8
- draft: { bg: '#e5e7eb', fg: '#374151' },
9
- review: { bg: '#dbeafe', fg: '#1d4ed8' },
10
- approved: { bg: '#d1fae5', fg: '#065f46' },
11
- implementing: { bg: '#fef3c7', fg: '#92400e' },
12
- done: { bg: '#d1fae5', fg: '#065f46' },
13
- discarded: { bg: '#f3f4f6', fg: '#9ca3af' },
14
- };
15
- const REPORT_CSS = `
16
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 900px; margin: 0 auto; padding: 24px; color: #1f2937; background: #fff; line-height: 1.6; font-size: 14px; }
17
- .header { border-bottom: 3px solid #4F46E5; padding-bottom: 16px; margin-bottom: 24px; }
18
- .header h1 { font-size: 1.1em; color: #6b7280; font-weight: 400; margin: 0 0 4px; }
19
- .header h2 { font-size: 1.6em; color: #111827; margin: 0; }
20
- .header .meta { margin-top: 8px; font-size: 0.85em; color: #6b7280; }
21
- .badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.78em; font-weight: 600; }
22
- .nav { margin-bottom: 24px; }
23
- .nav a { color: #4F46E5; text-decoration: none; font-size: 0.85em; }
24
- .nav a:hover { text-decoration: underline; }
25
- .section { margin-bottom: 28px; }
26
- .section h2 { font-size: 1.15em; color: #4F46E5; border-left: 4px solid #4F46E5; padding-left: 10px; margin-bottom: 14px; }
27
- .metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px; }
28
- .metric-card { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; text-align: center; }
29
- .metric-value { font-size: 1.8em; font-weight: 700; color: #4F46E5; line-height: 1; }
30
- .metric-label { font-size: 0.78em; color: #6b7280; margin-top: 4px; }
31
- .tags-section .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #ede9fe; color: #5b21b6; font-size: 0.8em; margin: 2px 4px; }
32
- .md-content h1 { font-size: 1.4em; color: #111827; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; margin: 24px 0 12px; }
33
- .md-content h2 { font-size: 1.2em; color: #4F46E5; border-left: 4px solid #4F46E5; padding-left: 10px; margin: 20px 0 10px; }
34
- .md-content h3 { font-size: 1.05em; color: #374151; margin: 16px 0 8px; }
35
- .md-content p { margin: 8px 0; }
36
- .md-content ul, .md-content ol { padding-left: 24px; margin: 8px 0; }
37
- .md-content li { margin: 4px 0; }
38
- .md-content blockquote { border-left: 4px solid #d1d5db; padding: 8px 16px; margin: 12px 0; color: #6b7280; background: #f9fafb; border-radius: 0 6px 6px 0; }
39
- .md-content code { background: #f3f4f6; padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
40
- .md-content pre { background: #1f2937; color: #e5e7eb; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
41
- .md-content pre code { background: none; color: inherit; padding: 0; }
42
- .md-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 0.85em; }
43
- .md-table th { text-align: left; padding: 8px 10px; background: #f9fafb; border-bottom: 2px solid #e5e7eb; font-weight: 600; color: #374151; }
44
- .md-table td { padding: 8px 10px; border-bottom: 1px solid #f3f4f6; }
45
- .md-table tr:hover { background: #f9fafb; }
46
- .md-content hr { border: none; border-top: 1px solid #e5e7eb; margin: 20px 0; }
47
- .md-content input[type=checkbox] { margin-right: 6px; }
48
- .footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid #e5e7eb; font-size: 0.78em; color: #9ca3af; text-align: center; }
49
- .footer a { color: #4F46E5; text-decoration: none; }
50
- @media print { body { padding: 0; } .nav { display: none; } }`;
51
- function escapeHtml(text) {
52
- return text
53
- .replace(/&/g, '&amp;')
54
- .replace(/</g, '&lt;')
55
- .replace(/>/g, '&gt;')
56
- .replace(/"/g, '&quot;');
57
- }
58
- function buildMetricsDashboard(spec) {
59
- const data = extractPerSpecData(spec, [], []);
60
- return [
61
- buildSpecMetricsSection(data),
62
- buildEffortDistributionSvg(data.devHours, data.reviewHours),
63
- buildSpecDependenciesSection(data),
64
- buildSpecTagsSection(data),
65
- ]
66
- .filter(Boolean)
67
- .join('\n');
68
- }
69
- function buildReportHtml(spec, title, mdContent) {
70
- const colors = STATUS_COLORS[spec.status] ?? { bg: '#e5e7eb', fg: '#374151' };
71
- const date = new Date().toLocaleDateString('en-US', {
72
- year: 'numeric',
73
- month: 'long',
74
- day: 'numeric',
75
- });
76
- const bodyHtml = markdownToHtml(mdContent);
77
- const dashboard = buildMetricsDashboard(spec);
78
- return `<!DOCTYPE html>
79
- <html lang="en">
80
- <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
81
- <title>${escapeHtml(title)}</title>
82
- <style>${REPORT_CSS}</style></head>
83
- <body>
84
- <div class="nav"><a href="../../index.html">&larr; Back to Dashboard</a></div>
85
- <div class="header">
86
- <h1>Planu — Spec Report</h1>
87
- <h2>${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</h2>
88
- <div class="meta">
89
- <span class="badge" style="background:${colors.bg};color:${colors.fg}">${escapeHtml(spec.status)}</span>
90
- &middot; Generated ${escapeHtml(date)}
91
- </div>
92
- </div>
93
- ${dashboard}
94
- <div class="md-content">${bodyHtml}</div>
95
- <div class="footer">Generated by <strong>Planu</strong> — Spec Driven Development</div>
96
- </body></html>`;
97
- }
98
- async function readMdSafe(path) {
99
- try {
100
- return await readFile(path, 'utf-8');
101
- }
102
- catch {
103
- return '';
104
- }
105
- }
106
1
  /**
107
- * Regenerate executive-report.html and technical-report.html for each spec.
108
- * Reads actual spec.md and technical.md content to produce rich HTML reports.
109
- * Also generates structural reports as fallback data source.
110
- * Best-effort per spec — one failure doesn't block others.
2
+ * Per-spec HTML reports are deprecated. The unified docs site renders spec.md
3
+ * on demand, so this function intentionally does not write files inside
4
+ * planu/specs/SPEC-* directories.
111
5
  */
112
- export async function regeneratePerSpecReports(specs) {
113
- await Promise.all(specs.map(async (spec) => {
114
- if (!spec.specPath) {
115
- return;
116
- }
117
- const specDir = dirname(spec.specPath);
118
- try {
119
- const [specMd, techMd] = await Promise.all([
120
- readMdSafe(spec.specPath),
121
- readMdSafe(spec.technicalPath),
122
- ]);
123
- const execHtml = specMd
124
- ? buildReportHtml(spec, `Executive Report — ${spec.id}: ${spec.title}`, specMd)
125
- : generatePerSpecExecutiveReport(spec, [], []).content;
126
- const techHtml = techMd
127
- ? buildReportHtml(spec, `Technical Report — ${spec.id}: ${spec.title}`, techMd)
128
- : generatePerSpecTechnicalReport(spec, [], [], []).content;
129
- await Promise.all([
130
- writeFile(join(specDir, 'executive-report.html'), execHtml, 'utf-8'),
131
- writeFile(join(specDir, 'technical-report.html'), techHtml, 'utf-8'),
132
- ]);
133
- }
134
- catch {
135
- /* best-effort per spec */
136
- }
137
- }));
6
+ export function regeneratePerSpecReports(specs) {
7
+ void specs;
8
+ return Promise.resolve();
138
9
  }
139
10
  //# sourceMappingURL=report-renderer.js.map
@@ -43,7 +43,7 @@ async function scanFilesystemSpecs(projectPath) {
43
43
  createdAt: str(m.created ?? m.createdAt),
44
44
  updatedAt: str(m.updated ?? m.updatedAt),
45
45
  specPath: join(specsDir, folder, 'spec.md'),
46
- technicalPath: join(specsDir, folder, 'technical.md'),
46
+ technicalPath: join(specsDir, folder, 'spec.md'),
47
47
  estimation: {
48
48
  devHours: num(m.devHours, 8),
49
49
  reviewHours: num(m.reviewHours, 2),
@@ -6,14 +6,11 @@ Auto-generated by \`init_project\`. Do not edit manually.
6
6
 
7
7
  ## Rule
8
8
 
9
- All generated spec artifacts are written in English, regardless of the user's conversation language:
9
+ All generated spec content is written in English, regardless of the user's conversation language:
10
10
 
11
11
  - \`spec.md\`
12
- - \`technical.md\`
13
- - architecture notes inside \`planu/specs/**\`
14
- - acceptance criteria
15
- - implementation notes
16
- - validation and reconciliation notes
12
+ - inline \`## Technical\`, \`## Files\`, and \`## Progress\` sections
13
+ - acceptance criteria, implementation notes, validation notes, and reconciliation notes
17
14
 
18
15
  User-facing chat may use the user's preferred language. The spec contract itself stays in English so every agent, tool, and CI gate can parse it consistently.
19
16
 
@@ -21,6 +18,7 @@ User-facing chat may use the user's preferred language. The spec contract itself
21
18
 
22
19
  - Do not create mixed-language acceptance criteria.
23
20
  - Do not translate BDD keywords.
21
+ - Do not create standalone \`technical.md\`, \`progress.md\`, \`HU.md\`, \`FICHA-TECNICA.md\`, or \`PROGRESS.md\`.
24
22
  - Do not approve a spec that contains unresolved placeholders such as \`to be determined\`, \`TBD\`, \`TODO\`, or equivalent filler.
25
23
  `;
26
24
  }
@@ -83,7 +83,7 @@ export async function handleCreateSpec(ctx) {
83
83
  createdAt: now,
84
84
  updatedAt: now,
85
85
  specPath: `planu/specs/${specId}-${slug}/spec.md`,
86
- technicalPath: `planu/specs/${specId}-${slug}/technical.md`,
86
+ technicalPath: `planu/specs/${specId}-${slug}/spec.md`,
87
87
  estimation: {
88
88
  devHours: 4,
89
89
  reviewHours: 1,
@@ -210,7 +210,7 @@ function generatePatternCriteria(patterns, _knowledge) {
210
210
  break;
211
211
  // SPEC-535: EU AI Act Article 53-55 compliance criteria for foundation model features
212
212
  case 'llm-feature':
213
- criteria.push('GIVEN the feature uses a foundation model WHEN the spec is implemented THEN technical.md documents: model name, provider (Anthropic/OpenAI/Google), and access date — required by EU AI Act Article 53(1)(a)');
213
+ criteria.push('GIVEN the feature uses a foundation model WHEN the spec is implemented THEN spec.md documents in its inline Technical section: model name, provider (Anthropic/OpenAI/Google), and access date — required by EU AI Act Article 53(1)(a)');
214
214
  criteria.push('GIVEN EU users interact with this feature WHEN the feature is deployed THEN an acceptable use policy is visible before the first AI interaction — required by EU AI Act Article 53(1)(c)');
215
215
  criteria.push("GIVEN the feature processes user-generated content via LLM WHEN any EU user's data is involved THEN the privacy notice explicitly states LLM processing and links to the model provider's data processing terms");
216
216
  break;
@@ -81,7 +81,7 @@ export async function buildSpecContext(params) {
81
81
  // Expected: directory doesn't exist yet — proceed
82
82
  }
83
83
  const specPath = join(specDir, fileNames.spec);
84
- const technicalPath = join(specDir, fileNames.technical);
84
+ const technicalPath = specPath;
85
85
  // Generate git branch name
86
86
  const gitBranch = generateBranchName(specId, slug, type);
87
87
  // SPEC-770: derive idempotency key from sha256(title + projectPath) to prevent duplicate specs
@@ -57,7 +57,7 @@ function buildNextSteps(orchestrationReady, taskCount) {
57
57
  if (!orchestrationReady) {
58
58
  return [
59
59
  'Review the warnings above — some tasks have no owned files.',
60
- 'Add file paths to the technical.md then re-run decompose_spec.',
60
+ 'Add file paths to the inline ## Files section in spec.md, then re-run decompose_spec.',
61
61
  ];
62
62
  }
63
63
  return [
@@ -48,7 +48,7 @@ export async function handleGeneratePr(projectId, specId) {
48
48
  }
49
49
  const typeLabel = spec.type.charAt(0).toUpperCase() + spec.type.slice(1);
50
50
  const prTitle = `[${specId}] ${spec.title}`;
51
- // Build acceptance criteria checklist from spec (criteria live in HU.md, not on spec object)
51
+ // Build acceptance criteria checklist from spec.md.
52
52
  const criteriaChecklist = buildCriteriaChecklist();
53
53
  const prBody = [
54
54
  `## ${typeLabel}: ${spec.title}`,
@@ -135,7 +135,7 @@ export async function handleGeneratePr(projectId, specId) {
135
135
  return compactResult(text);
136
136
  }
137
137
  function buildCriteriaChecklist() {
138
- return '- [ ] All acceptance criteria met (see HU.md)';
138
+ return '- [ ] All acceptance criteria met (see spec.md)';
139
139
  }
140
140
  function buildLabels(type, risk, scope) {
141
141
  const labels = [];
@@ -158,7 +158,7 @@ export async function handleResolveConflict(projectId, files) {
158
158
  specId: s.id,
159
159
  title: s.title,
160
160
  status: s.status,
161
- context: `Resolve keeping spec ${s.id} (${s.title}) intent. Check HU.md acceptance criteria for guidance.`,
161
+ context: `Resolve keeping spec ${s.id} (${s.title}) intent. Check spec.md acceptance criteria for guidance.`,
162
162
  })),
163
163
  };
164
164
  });
@@ -36,7 +36,7 @@ export async function handleReconcileSpecLiving(input) {
36
36
  lines.push(`- ${icon} ${cr.criterion} — _${cr.evidence}_`);
37
37
  }
38
38
  }
39
- lines.push('', '_progress.md updated with auto-reconcile data._');
39
+ lines.push('', '_spec.md Progress section updated with auto-reconcile data._');
40
40
  return {
41
41
  content: [{ type: 'text', text: lines.join('\n') }],
42
42
  };
@@ -91,7 +91,7 @@ async function autoDetectChanges(spec, _knowledge) {
91
91
  section: 'ficha-file',
92
92
  originalValue: spec.technicalPath,
93
93
  newValue: 'File not found',
94
- reason: 'technical.md file is missing or was moved',
94
+ reason: 'spec.md inline Technical/Files section is missing or incomplete',
95
95
  approved: false,
96
96
  });
97
97
  }
@@ -22,25 +22,6 @@ async function collectExistingSpecIds(specsDir) {
22
22
  }
23
23
  /* v8 ignore stop */
24
24
  }
25
- /** Generate a clean progress.md for the installed spec. */
26
- function generateProgressMd(specId, source, version) {
27
- const now = new Date().toISOString();
28
- return [
29
- `# ${specId} — Progress`,
30
- '',
31
- `> Installed from registry: ${source}@${version}`,
32
- `> Installed at: ${now}`,
33
- '',
34
- '## Status: pendiente',
35
- '',
36
- '## Acceptance Criteria',
37
- '',
38
- '- [ ] Review and adapt spec to local project',
39
- '- [ ] Implement according to spec',
40
- '- [ ] All tests passing',
41
- '',
42
- ].join('\n');
43
- }
44
25
  /** Inject registry source metadata into spec.md frontmatter. */
45
26
  function injectRegistrySource(content, org, name, version) {
46
27
  const frontmatterEnd = content.indexOf('\n---', 3);
@@ -50,9 +31,28 @@ function injectRegistrySource(content, org, name, version) {
50
31
  const insertion = `\nregistry:\n source: "${org}/${name}"\n version: "${version}"`;
51
32
  return content.slice(0, frontmatterEnd) + insertion + content.slice(frontmatterEnd);
52
33
  }
34
+ function mergeLegacyRegistryFiles(files) {
35
+ const specContent = files['spec.md'] ?? '';
36
+ const sections = [specContent.trimEnd()];
37
+ const technical = files['technical.md'] ?? files['FICHA-TECNICA.md'];
38
+ if (technical !== undefined && technical.trim().length > 0) {
39
+ sections.push('## Technical\n\n' + stripLeadingTitleAndFrontmatter(technical).trim());
40
+ }
41
+ const progress = files['progress.md'] ?? files['PROGRESS.md'];
42
+ if (progress !== undefined && progress.trim().length > 0) {
43
+ sections.push('## Progress\n\n' + stripLeadingTitleAndFrontmatter(progress).trim());
44
+ }
45
+ return sections.join('\n\n') + '\n';
46
+ }
47
+ function stripLeadingTitleAndFrontmatter(content) {
48
+ return content.replace(/^---\n[\s\S]*?\n---\n/, '').replace(/^#\s+.*(?:\r?\n)+/, '');
49
+ }
50
+ function isInstallableRegistrySidecar(filePath) {
51
+ return filePath === 'planu-registry.json';
52
+ }
53
53
  /**
54
54
  * Install a spec from raw YAML content (HTTP backend path).
55
- * Writes spec.md with registry source injected + progress.md, then attempts stack adaptation.
55
+ * Writes one unified spec.md with registry source injected, then attempts stack adaptation.
56
56
  */
57
57
  async function installFromYaml(yamlContent, org, name, version, projectPath) {
58
58
  const specsDir = join(projectPath, 'planu', 'specs');
@@ -64,7 +64,6 @@ async function installFromYaml(yamlContent, org, name, version, projectPath) {
64
64
  const specContent = injectRegistrySource(yamlContent, org, name, version);
65
65
  await writeFile(join(specDir, 'spec.md'), specContent, 'utf-8');
66
66
  const source = `${org}/${name}`;
67
- await writeFile(join(specDir, 'progress.md'), generateProgressMd(specId, source, version), 'utf-8');
68
67
  let adapted = false;
69
68
  let adaptationsCount = 0;
70
69
  try {
@@ -140,17 +139,16 @@ export async function handleRegistryInstall(args) {
140
139
  const specDir = join(specsDir, `${specId}-${slug}`);
141
140
  // Create spec directory
142
141
  await mkdir(specDir, { recursive: true });
143
- // Write downloaded files, injecting registry source into spec.md frontmatter
142
+ // Write a unified spec.md. Legacy registry sidecars are folded into sections
143
+ // instead of being materialized as standalone files in the spec directory.
144
+ const source = `${org}/${name}`;
145
+ const specContent = injectRegistrySource(mergeLegacyRegistryFiles(pkg.files), org, name, version);
146
+ await writeFile(join(specDir, 'spec.md'), specContent, 'utf-8');
144
147
  for (const [filePath, content] of Object.entries(pkg.files)) {
145
- let finalContent = content;
146
- if (filePath === 'spec.md') {
147
- finalContent = injectRegistrySource(content, org, name, version);
148
+ if (isInstallableRegistrySidecar(filePath)) {
149
+ await writeFile(join(specDir, filePath), content, 'utf-8');
148
150
  }
149
- await writeFile(join(specDir, filePath), finalContent, 'utf-8');
150
151
  }
151
- // Generate local progress.md
152
- const source = `${org}/${name}`;
153
- await writeFile(join(specDir, 'progress.md'), generateProgressMd(specId, source, version), 'utf-8');
154
152
  // Attempt stack detection and spec adaptation (non-blocking on failure)
155
153
  let adapted = false;
156
154
  let adaptationsCount = 0;
@@ -204,7 +204,7 @@ export async function handleReverseEngineer(args) {
204
204
  const specLocation = projectKnowledge?.specLocation ?? 'planu/specs';
205
205
  const specDir = resolve(join(projectPath, specLocation, `${specId}-${slug}`));
206
206
  const specFilePath = join(specDir, 'spec.md');
207
- const technicalFilePath = join(specDir, 'technical.md');
207
+ const technicalFilePath = specFilePath;
208
208
  spec.specPath = specFilePath;
209
209
  spec.technicalPath = technicalFilePath;
210
210
  try {
@@ -24,7 +24,7 @@ export const CheckSpecAccuracySchema = z.object({
24
24
  .string()
25
25
  .min(1)
26
26
  .max(1_000_000)
27
- .describe('Contenido de la spec o fragmento a validar (texto de HU.md o criterios de aceptacion)'),
27
+ .describe('Contenido de la spec o fragmento a validar (texto de spec.md o criterios de aceptacion)'),
28
28
  frameworks: z
29
29
  .array(z.string().max(500))
30
30
  .max(100)
@@ -29,7 +29,7 @@ async function analyzeSingleSpec(projectId, specId, projectPath) {
29
29
  return errorResult(`Spec "${specId}" not found in project "${projectId}".`);
30
30
  }
31
31
  if (!spec.specPath) {
32
- return errorResult(`Spec "${specId}" has no HU.md path recorded. ` +
32
+ return errorResult(`Spec "${specId}" has no spec.md path recorded. ` +
33
33
  `The spec may have been created before this feature was available.`);
34
34
  }
35
35
  const huPath = join(projectPath, spec.specPath);
@@ -57,7 +57,7 @@ async function analyzeAllSpecs(projectId, projectPath) {
57
57
  const errors = [];
58
58
  for (const spec of activeSpecs) {
59
59
  if (!spec.specPath) {
60
- errors.push(`${spec.id}: no HU.md path`);
60
+ errors.push(`${spec.id}: no spec.md path`);
61
61
  continue;
62
62
  }
63
63
  const huPath = join(projectPath, spec.specPath);
@@ -4,7 +4,7 @@ import type { ListTemplatesInput, ApplyTemplateInput, ToolResult } from '../type
4
4
  */
5
5
  export declare function handleListTemplates(args: ListTemplatesInput): Promise<ToolResult>;
6
6
  /**
7
- * Applies a template to create HU.md + FICHA-TECNICA.md + PROGRESS.md.
7
+ * Applies a template to create one unified spec.md file.
8
8
  */
9
9
  export declare function handleApplyTemplate(args: ApplyTemplateInput): Promise<ToolResult>;
10
10
  //# sourceMappingURL=spec-templates.d.ts.map
@@ -62,7 +62,7 @@ export async function handleListTemplates(args) {
62
62
  }
63
63
  }
64
64
  /**
65
- * Applies a template to create HU.md + FICHA-TECNICA.md + PROGRESS.md.
65
+ * Applies a template to create one unified spec.md file.
66
66
  */
67
67
  export async function handleApplyTemplate(args) {
68
68
  try {
@@ -93,19 +93,14 @@ export async function handleApplyTemplate(args) {
93
93
  const rendered = renderTemplate(template, args.variables, args.includeCriteria);
94
94
  const outputDir = resolveOutputDir(args);
95
95
  await mkdir(outputDir, { recursive: true });
96
- const specPath = join(outputDir, 'HU.md');
97
- const fichaTecnicaPath = join(outputDir, 'FICHA-TECNICA.md');
98
- const progressPath = join(outputDir, 'PROGRESS.md');
99
- await atomicWriteFile(specPath, rendered.hu);
100
- await atomicWriteFile(fichaTecnicaPath, rendered.fichaTecnica);
101
- await atomicWriteFile(progressPath, rendered.progress);
96
+ const specPath = join(outputDir, 'spec.md');
97
+ const specContent = buildUnifiedTemplateSpec(rendered.hu, rendered.fichaTecnica, rendered.progress);
98
+ await atomicWriteFile(specPath, specContent);
102
99
  const result = {
103
100
  templateId: template.id,
104
101
  templateName: template.name,
105
102
  complexityScore: template.complexityScore,
106
103
  specPath,
107
- fichaTecnicaPath,
108
- progressPath,
109
104
  variablesApplied: args.variables.length,
110
105
  criteriaIncluded: rendered.criteriaIncluded,
111
106
  criteriaTotal: rendered.criteriaTotal,
@@ -125,6 +120,19 @@ export async function handleApplyTemplate(args) {
125
120
  };
126
121
  }
127
122
  }
123
+ function buildUnifiedTemplateSpec(hu, technical, progress) {
124
+ const sections = [hu.trimEnd()];
125
+ if (technical.trim().length > 0) {
126
+ sections.push('## Technical\n\n' + stripLeadingTitle(technical).trim());
127
+ }
128
+ if (progress.trim().length > 0) {
129
+ sections.push('## Progress\n\n' + stripLeadingTitle(progress).trim());
130
+ }
131
+ return sections.join('\n\n') + '\n';
132
+ }
133
+ function stripLeadingTitle(content) {
134
+ return content.replace(/^#\s+.*(?:\r?\n)+/, '');
135
+ }
128
136
  /**
129
137
  * Resolves the output directory for the spec files.
130
138
  */
@@ -562,7 +562,7 @@ export function registerInfraGroupTools(server) {
562
562
  }, safeLicensed('assign_role', (args) => handleAssignRole(args)));
563
563
  // ── Framework registry tools ───────────────────────────────────────────────
564
564
  server.registerTool('check_spec_accuracy', {
565
- description: 'Validate spec content (HU.md text or acceptance criteria) against the framework registry ' +
565
+ description: 'Validate spec content (spec.md text or acceptance criteria) against the framework registry ' +
566
566
  'to detect hallucinated APIs, deprecated patterns, and common mistakes. ' +
567
567
  'Returns a confidence score (0-100) and actionable suggestions.',
568
568
  inputSchema: {
@@ -475,7 +475,7 @@ export function registerPlatformGroupTools(s) {
475
475
  annotations: { title: 'List Spec Templates', readOnlyHint: true },
476
476
  }, safeTracked('list_templates', async (args) => handleListTemplates(args)));
477
477
  s.registerTool('apply_template', {
478
- description: 'Create HU.md, FICHA-TECNICA.md, and PROGRESS.md from a spec template. ' +
478
+ description: 'Create a unified spec.md from a spec template. ' +
479
479
  'Templates are language-agnostic and cover common feature patterns (auth, CRUD, API, etc.) and industry verticals (fintech, healthtech, e-commerce, SaaS). ' +
480
480
  'Use list_templates first to discover available templates and their required variables. ' +
481
481
  'Supports partial criteria selection via includeCriteria parameter.',
@@ -62,11 +62,11 @@ export interface SpecTemplateEntry {
62
62
  criteria: TemplateCriterion[];
63
63
  /** Variables that users need to provide when applying the template. */
64
64
  variables: TemplateVariable[];
65
- /** Template content for HU.md (Handlebars-style {{VariableKey}} placeholders). */
65
+ /** Legacy template content for the main spec body. */
66
66
  huTemplate: string;
67
- /** Template content for FICHA-TECNICA.md. */
67
+ /** Legacy template content for technical details, now embedded into spec.md. */
68
68
  fichaTecnicaTemplate: string;
69
- /** Template content for PROGRESS.md. */
69
+ /** Legacy template content for progress details, now embedded into spec.md. */
70
70
  progressTemplate: string;
71
71
  }
72
72
  /** Result returned by the list_templates tool. */
@@ -115,8 +115,6 @@ export interface ApplyTemplateResult {
115
115
  templateName: string;
116
116
  complexityScore: TemplateComplexity;
117
117
  specPath: string;
118
- fichaTecnicaPath: string;
119
- progressPath: string;
120
118
  variablesApplied: number;
121
119
  criteriaIncluded: number;
122
120
  criteriaTotal: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "3.9.11",
3
+ "version": "3.9.12",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -407,10 +407,10 @@
407
407
  "missingVarExample": "Example: \"{example}\"",
408
408
  "missingVarsHint": "Provide all required variables in the `variables` array and try again.",
409
409
  "applySuccess": "Template \"{name}\" applied successfully.",
410
- "filesCreated": "Files created in: {dir}",
410
+ "filesCreated": "spec.md created in: {dir}",
411
411
  "nextSteps": "Next steps:",
412
- "step1": "1. Review and customize HU.md acceptance criteria",
413
- "step2": "2. Update FICHA-TECNICA.md with project-specific details",
412
+ "step1": "1. Review and customize acceptance criteria in spec.md",
413
+ "step2": "2. Update the inline Technical section with project-specific details",
414
414
  "step3": "3. Run create_spec or update_status once the spec is approved",
415
415
  "applyError": "Error applying template: {message}"
416
416
  },
@@ -407,10 +407,10 @@
407
407
  "missingVarExample": "Ejemplo: \"{example}\"",
408
408
  "missingVarsHint": "Proporciona todas las variables requeridas en el array `variables` e intenta de nuevo.",
409
409
  "applySuccess": "Plantilla \"{name}\" aplicada exitosamente.",
410
- "filesCreated": "Archivos creados en: {dir}",
410
+ "filesCreated": "spec.md creado en: {dir}",
411
411
  "nextSteps": "Próximos pasos:",
412
- "step1": "1. Revisar y personalizar los criterios de aceptación en HU.md",
413
- "step2": "2. Actualizar FICHA-TECNICA.md con detalles específicos del proyecto",
412
+ "step1": "1. Revisar y personalizar los criterios de aceptación en spec.md",
413
+ "step2": "2. Actualizar la sección Technical embebida con detalles específicos del proyecto",
414
414
  "step3": "3. Ejecutar create_spec o update_status cuando la spec esté aprobada",
415
415
  "applyError": "Error al aplicar plantilla: {message}"
416
416
  },
@@ -407,10 +407,10 @@
407
407
  "missingVarExample": "Exemplo: \"{example}\"",
408
408
  "missingVarsHint": "Forneça todas as variáveis obrigatórias no array `variables` e tente novamente.",
409
409
  "applySuccess": "Modelo \"{name}\" aplicado com sucesso.",
410
- "filesCreated": "Arquivos criados em: {dir}",
410
+ "filesCreated": "spec.md criado em: {dir}",
411
411
  "nextSteps": "Próximos passos:",
412
- "step1": "1. Revisar e personalizar os critérios de aceitação em HU.md",
413
- "step2": "2. Atualizar FICHA-TECNICA.md com detalhes específicos do projeto",
412
+ "step1": "1. Revisar e personalizar os critérios de aceitação em spec.md",
413
+ "step2": "2. Atualizar a seção Technical embutida com detalhes específicos do projeto",
414
414
  "step3": "3. Executar create_spec ou update_status quando a spec estiver aprovada",
415
415
  "applyError": "Erro ao aplicar modelo: {message}"
416
416
  },