@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.
- package/CHANGELOG.md +12 -0
- package/dist/config/criteria-injection-rules.json +1 -1
- package/dist/config/dor-dod-items.json +3 -3
- package/dist/config/hook-templates/planu-spec-sanctity.sh +13 -3
- package/dist/config/server-instructions.js +1 -1
- package/dist/config/server-instructions.ts +1 -1
- package/dist/config/skill-templates/planu-new-spec.md +1 -1
- package/dist/config/skill-templates/planu-resume-work.md +1 -1
- package/dist/config/subagent-templates/planu-readiness-auditor.md +3 -3
- package/dist/config/subagent-templates/planu-spec-implementer.md +1 -1
- package/dist/config/workflow-conventions-catalog.json +3 -3
- package/dist/core/spec-api.js +1 -1
- package/dist/engine/agent-generator/builders.js +1 -1
- package/dist/engine/autopilot/handlers-b2.d.ts +2 -2
- package/dist/engine/autopilot/handlers-b2.js +15 -19
- package/dist/engine/ci-generator/local-script.js +4 -4
- package/dist/engine/ci-generator/planu-steps.js +4 -5
- package/dist/engine/compliance/auto-remediator.js +0 -1
- package/dist/engine/drift/violation-resolver.js +0 -1
- package/dist/engine/health/auto-fixer.js +1 -33
- package/dist/engine/living-spec-analyzer.js +3 -34
- package/dist/engine/planu-config-writer.js +1 -1
- package/dist/engine/scan-project/index.js +2 -2
- package/dist/engine/skill-generator/skills-content.js +1 -1
- package/dist/engine/spec-migrator/drift-detector.js +1 -1
- package/dist/engine/spec-migrator/lean-migration.js +5 -4
- package/dist/engine/spec-migrator/planu-root-cleaner.js +1 -1
- package/dist/engine/spec-registry/packager.d.ts +1 -1
- package/dist/engine/spec-registry/packager.js +2 -2
- package/dist/engine/spec-registry/validator.js +1 -2
- package/dist/engine/spec-splitter.js +2 -2
- package/dist/engine/spec-summary-html/report-renderer.d.ts +3 -4
- package/dist/engine/spec-summary-html/report-renderer.js +6 -135
- package/dist/engine/spec-summary-html.js +1 -1
- package/dist/engine/universal-rules/rules/planu-english-specs.js +4 -6
- package/dist/server/routes/specs.js +1 -1
- package/dist/tools/create-spec/autopilot-analyzer.js +1 -1
- package/dist/tools/create-spec/spec-builder.js +1 -1
- package/dist/tools/decompose-spec.js +1 -1
- package/dist/tools/git/pr-ops.js +2 -2
- package/dist/tools/git/sync-ops.js +1 -1
- package/dist/tools/reconcile-spec-living-handler.js +1 -1
- package/dist/tools/reconcile-spec.js +1 -1
- package/dist/tools/registry/install.js +27 -29
- package/dist/tools/reverse-engineer/handler.js +1 -1
- package/dist/tools/schemas/registry.js +1 -1
- package/dist/tools/spec-coverage.js +2 -2
- package/dist/tools/spec-templates.d.ts +1 -1
- package/dist/tools/spec-templates.js +17 -9
- package/dist/tools/tool-registry/group-infra.js +1 -1
- package/dist/tools/tool-registry/group-platform.js +1 -1
- package/dist/types/spec-templates.d.ts +3 -5
- package/package.json +1 -1
- package/src/i18n/messages/en.json +3 -3
- package/src/i18n/messages/es.json +3 -3
- 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.
|
|
293
|
-
progressPath:
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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, '&')
|
|
54
|
-
.replace(/</g, '<')
|
|
55
|
-
.replace(/>/g, '>')
|
|
56
|
-
.replace(/"/g, '"');
|
|
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">← 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
|
-
· 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
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
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, '
|
|
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
|
|
9
|
+
All generated spec content is written in English, regardless of the user's conversation language:
|
|
10
10
|
|
|
11
11
|
- \`spec.md\`
|
|
12
|
-
- \`
|
|
13
|
-
-
|
|
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}/
|
|
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
|
|
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 =
|
|
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
|
|
60
|
+
'Add file paths to the inline ## Files section in spec.md, then re-run decompose_spec.',
|
|
61
61
|
];
|
|
62
62
|
}
|
|
63
63
|
return [
|
package/dist/tools/git/pr-ops.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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('', '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, '
|
|
97
|
-
const
|
|
98
|
-
|
|
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 (
|
|
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
|
|
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
|
-
/**
|
|
65
|
+
/** Legacy template content for the main spec body. */
|
|
66
66
|
huTemplate: string;
|
|
67
|
-
/**
|
|
67
|
+
/** Legacy template content for technical details, now embedded into spec.md. */
|
|
68
68
|
fichaTecnicaTemplate: string;
|
|
69
|
-
/**
|
|
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.
|
|
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": "
|
|
410
|
+
"filesCreated": "spec.md created in: {dir}",
|
|
411
411
|
"nextSteps": "Next steps:",
|
|
412
|
-
"step1": "1. Review and customize
|
|
413
|
-
"step2": "2. Update
|
|
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": "
|
|
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
|
|
413
|
-
"step2": "2. Actualizar
|
|
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": "
|
|
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
|
|
413
|
-
"step2": "2. Atualizar
|
|
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
|
},
|