@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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [3.9.12] - 2026-05-19
|
|
2
|
+
|
|
3
|
+
**Tarball SHA-256:** `cd07a22fdfc0c982726a918c1e47f147ca300ecad710e8377f1751ef993fea60`
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
- fix(specs): purge legacy spec artifact writers
|
|
7
|
+
|
|
8
|
+
### Chores
|
|
9
|
+
- chore(claude): reconcile typescript skill asset
|
|
10
|
+
- chore(claude): remove unavailable skill artifact
|
|
11
|
+
|
|
12
|
+
|
|
1
13
|
## [3.9.11] - 2026-05-17
|
|
2
14
|
|
|
3
15
|
**Tarball SHA-256:** `a201430a93ae5f87af8322409c328266c29b340e890eb2fa49c7181da32886d3`
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"rules": [
|
|
4
4
|
{
|
|
5
5
|
"id": "eu-ai-act-53-1a",
|
|
6
|
-
"criterion": "GIVEN the feature uses a foundation model WHEN the spec is implemented THEN
|
|
6
|
+
"criterion": "GIVEN the feature uses a foundation model WHEN the spec is implemented THEN spec.md documents in its inline Technical section: model name, provider, and access date — EU AI Act Article 53(1)(a)",
|
|
7
7
|
"requiresTags": ["llm", "foundation-model", "ai-feature"],
|
|
8
8
|
"requiresTargets": [],
|
|
9
9
|
"requiresScopes": []
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{
|
|
43
43
|
"id": "dor-5",
|
|
44
44
|
"kind": "dor",
|
|
45
|
-
"description": "
|
|
45
|
+
"description": "spec.md file exists and is complete",
|
|
46
46
|
"category": "spec",
|
|
47
47
|
"required": true,
|
|
48
48
|
"autoCheck": true,
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
{
|
|
53
53
|
"id": "dor-6",
|
|
54
54
|
"kind": "dor",
|
|
55
|
-
"description": "
|
|
55
|
+
"description": "spec.md inline Technical section is complete",
|
|
56
56
|
"category": "design",
|
|
57
57
|
"required": true,
|
|
58
58
|
"autoCheck": true,
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
{
|
|
195
195
|
"id": "dod-7",
|
|
196
196
|
"kind": "dod",
|
|
197
|
-
"description": "Documentation updated
|
|
197
|
+
"description": "Documentation updated in spec.md",
|
|
198
198
|
"category": "docs",
|
|
199
199
|
"required": false,
|
|
200
200
|
"autoCheck": false,
|
|
@@ -20,7 +20,6 @@ set -euo pipefail
|
|
|
20
20
|
# ---------------------------------------------------------------------------
|
|
21
21
|
ALLOWED_FILES=(
|
|
22
22
|
"spec.md"
|
|
23
|
-
"technical.md"
|
|
24
23
|
"progress.json"
|
|
25
24
|
"implementation-brief.md"
|
|
26
25
|
)
|
|
@@ -79,6 +78,18 @@ NORMALISED="${NORMALISED#/*/}" # strip any leading absolute segment
|
|
|
79
78
|
if [[ "$NORMALISED" =~ ^(.*/)?planu/specs/SPEC-[^/]+/([^/]+)$ ]]; then
|
|
80
79
|
BASENAME="${BASH_REMATCH[2]}"
|
|
81
80
|
|
|
81
|
+
case "$BASENAME" in
|
|
82
|
+
technical.md|progress.md|HU.md|FICHA-TECNICA.md|PROGRESS.md)
|
|
83
|
+
cat >&2 <<EOF
|
|
84
|
+
[planu-spec-sanctity] BLOCKED: "$FILE_PATH" is a legacy spec artifact.
|
|
85
|
+
|
|
86
|
+
Planu specs are unified in spec.md. Put technical planning, file ownership,
|
|
87
|
+
and progress into inline sections: ## Technical, ## Files, and ## Progress.
|
|
88
|
+
EOF
|
|
89
|
+
exit 2
|
|
90
|
+
;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
82
93
|
# Check against whitelist
|
|
83
94
|
for allowed in "${ALLOWED_FILES[@]}"; do
|
|
84
95
|
if [[ "$BASENAME" == "$allowed" ]]; then
|
|
@@ -92,11 +103,10 @@ if [[ "$NORMALISED" =~ ^(.*/)?planu/specs/SPEC-[^/]+/([^/]+)$ ]]; then
|
|
|
92
103
|
|
|
93
104
|
Allowed files inside planu/specs/SPEC-*/ are:
|
|
94
105
|
- spec.md
|
|
95
|
-
- technical.md
|
|
96
106
|
- progress.json
|
|
97
107
|
- implementation-brief.md
|
|
98
108
|
|
|
99
|
-
Do NOT create PLAN.md, NOTES.md, ADRs, or other files here.
|
|
109
|
+
Do NOT create technical.md, progress.md, PLAN.md, NOTES.md, ADRs, or other files here.
|
|
100
110
|
Keep implementation notes in conversation context or in src/.
|
|
101
111
|
EOF
|
|
102
112
|
exit 2
|
|
@@ -53,7 +53,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
53
53
|
'4. When presenting lists or results, use plain language headers — not jargon.',
|
|
54
54
|
'5. If the user seems confused, offer to explain further. Never assume knowledge.',
|
|
55
55
|
"6. Adapt your language to the user's locale (English, Spanish, or Portuguese).",
|
|
56
|
-
' EXCEPTION: spec.md
|
|
56
|
+
' EXCEPTION: spec.md files must ALWAYS be written in English regardless of locale.',
|
|
57
57
|
' The user-facing narrative (tool response text) is translated; spec docs are always English.',
|
|
58
58
|
'',
|
|
59
59
|
'INTERACTIVE QUESTIONS — HARD GATE enforcement (SPEC-584):',
|
|
@@ -54,7 +54,7 @@ export const SERVER_INSTRUCTIONS = [
|
|
|
54
54
|
'4. When presenting lists or results, use plain language headers — not jargon.',
|
|
55
55
|
'5. If the user seems confused, offer to explain further. Never assume knowledge.',
|
|
56
56
|
"6. Adapt your language to the user's locale (English, Spanish, or Portuguese).",
|
|
57
|
-
' EXCEPTION: spec.md
|
|
57
|
+
' EXCEPTION: spec.md files must ALWAYS be written in English regardless of locale.',
|
|
58
58
|
' The user-facing narrative (tool response text) is translated; spec docs are always English.',
|
|
59
59
|
'',
|
|
60
60
|
'INTERACTIVE QUESTIONS — HARD GATE enforcement (SPEC-584):',
|
|
@@ -45,7 +45,7 @@ User: "I want to add Stripe payment support"
|
|
|
45
45
|
## What you get
|
|
46
46
|
|
|
47
47
|
- A complete `spec.md` with GIVEN/WHEN/THEN acceptance criteria
|
|
48
|
-
-
|
|
48
|
+
- Inline `## Technical` and `## Files` sections with file-level implementation plan
|
|
49
49
|
- Challenge report identifying any gaps found (auto-addressed where possible)
|
|
50
50
|
- Readiness score — must be ≥ 80 before implementation begins
|
|
51
51
|
|
|
@@ -25,7 +25,7 @@ Use this skill at the start of a session, or when returning to a project after a
|
|
|
25
25
|
|
|
26
26
|
1. **Check status** — calls `planu_status` to see all active specs
|
|
27
27
|
2. **Find in-progress** — identifies specs with status `implementing` or `review`
|
|
28
|
-
3. **Load context** — reads the spec's `spec.md`,
|
|
28
|
+
3. **Load context** — reads the spec's `spec.md` and any inline `## Technical`, `## Files`, and `## Progress` sections
|
|
29
29
|
4. **Restore session** — calls `restore_session` or `session_checkpoint` to recover the previous session state
|
|
30
30
|
5. **Report** — summarizes exactly where to resume: which files were being modified, which criteria are still open
|
|
31
31
|
|
|
@@ -23,12 +23,12 @@ You are a readiness gatekeeper. Your job is to verify that a spec is actionable
|
|
|
23
23
|
|
|
24
24
|
## Workflow
|
|
25
25
|
|
|
26
|
-
1. **Load spec** — call `list_specs` to find the spec. Read
|
|
26
|
+
1. **Load spec** — call `list_specs` to find the spec. Read `spec.md`, including inline `## Technical` and `## Files` sections.
|
|
27
27
|
2. **Check readiness** — call `check_readiness({ specId })`. Record the score and blockers.
|
|
28
28
|
3. **Quality score** — call `spec_quality_score({ specId })` for a structured breakdown.
|
|
29
29
|
4. **Size analysis** — call `analyze_spec_size({ specId })` to detect over/under-specified sections.
|
|
30
30
|
5. **Suggest missing criteria** — call `suggest_criteria({ specId })` for any dimension below threshold.
|
|
31
|
-
6. **Heal docs if needed** — call `heal_spec_docs({ specId })` if `
|
|
31
|
+
6. **Heal docs if needed** — call `heal_spec_docs({ specId })` if the inline `## Technical` section has placeholder content.
|
|
32
32
|
7. **Verdict**:
|
|
33
33
|
- Score ≥ 80 and no blockers → ✅ Ready for approval
|
|
34
34
|
- Score 60–79 → 🟡 Conditional — list specific improvements required
|
|
@@ -54,5 +54,5 @@ Score: XX/100
|
|
|
54
54
|
|
|
55
55
|
- Never approve a spec with undefined terms in criteria
|
|
56
56
|
- Every criterion must be independently verifiable by an automated test
|
|
57
|
-
- The `
|
|
57
|
+
- The inline `## Technical` / `## Files` sections must list concrete file paths, not placeholders
|
|
58
58
|
- Missing error-path criteria always block readiness
|
|
@@ -26,7 +26,7 @@ You are an autonomous implementation agent for Spec Driven Development. Your job
|
|
|
26
26
|
|
|
27
27
|
## Workflow
|
|
28
28
|
|
|
29
|
-
1. **Read the spec** — call `list_specs` and find the target spec. Read `spec.md
|
|
29
|
+
1. **Read the spec** — call `list_specs` and find the target spec. Read `spec.md`, including inline `## Technical`, `## Files`, and `## Progress` sections.
|
|
30
30
|
2. **Verify approval** — confirm spec status is `approved`. If not, call `check_readiness` and stop with a note.
|
|
31
31
|
3. **Update status to `implementing`** — call `update_status({ specId, status: "implementing" })`.
|
|
32
32
|
4. **Create a feature branch** — `git checkout -b feat/SPEC-NNN-slug`.
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
},
|
|
26
26
|
"propose-spec-split.sh": {
|
|
27
27
|
"name": "propose-spec-split.sh",
|
|
28
|
-
"purpose": "Analyze a spec
|
|
29
|
-
"triggerCondition": "When a spec has > 35 acceptance criteria in
|
|
28
|
+
"purpose": "Analyze a spec.md and propose a split when it exceeds the 35-criteria threshold for a single implementation context.",
|
|
29
|
+
"triggerCondition": "When a spec has > 35 acceptance criteria in spec.md. Must run BEFORE starting implementation — never implement XL specs directly.",
|
|
30
30
|
"expectedOutput": "Proposed sub-spec split with suggested groupings, estimated size per sub-spec, and recommended implementation order.",
|
|
31
|
-
"preConditions": ["SPEC-XXX directory exists with
|
|
31
|
+
"preConditions": ["SPEC-XXX directory exists with spec.md", "spec.md has acceptance criteria section"],
|
|
32
32
|
"whenToUse": "Proactively at spec planning time. The /implement-spec skill should call this automatically for specs > 35 criteria."
|
|
33
33
|
}
|
|
34
34
|
},
|
package/dist/core/spec-api.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function createSpec(input) {
|
|
|
45
45
|
const specLocation = 'planu/specs';
|
|
46
46
|
const specDir = resolve(join(projectPath, specLocation, `${specId}-${slug}`));
|
|
47
47
|
const specPath = join(specDir, 'spec.md');
|
|
48
|
-
const technicalPath =
|
|
48
|
+
const technicalPath = specPath;
|
|
49
49
|
const now = new Date().toISOString();
|
|
50
50
|
const spec = {
|
|
51
51
|
id: specId,
|
|
@@ -33,7 +33,7 @@ export function buildSystemPrompt(agentName, role, capabilities, knowledge) {
|
|
|
33
33
|
lines.push('');
|
|
34
34
|
lines.push('## Scope Restrictions');
|
|
35
35
|
lines.push('- NEVER git commit or git push changes — that is handled by the parent skill');
|
|
36
|
-
lines.push('- NEVER update PROGRESS.md
|
|
36
|
+
lines.push('- NEVER create or update standalone progress.md/PROGRESS.md tracking files — delegate to parent skill');
|
|
37
37
|
lines.push('- NEVER run the full CI/CD pipeline — parent skill manages pipeline execution');
|
|
38
38
|
lines.push('- Focus exclusively on your assigned subtask and report results clearly');
|
|
39
39
|
return lines.join('\n');
|
|
@@ -11,11 +11,11 @@ export declare function injectCriteria(ctx: ActionContext): Promise<ActionResult
|
|
|
11
11
|
export declare function rewriteCriteriaEars(ctx: ActionContext): Promise<ActionResult>;
|
|
12
12
|
/**
|
|
13
13
|
* verify_spec_compliance: Checks that a spec has spec.md with criteria,
|
|
14
|
-
*
|
|
14
|
+
* inline file references in spec.md, and that those files exist on disk.
|
|
15
15
|
*/
|
|
16
16
|
export declare function verifySpecCompliance(ctx: ActionContext): Promise<ActionResult>;
|
|
17
17
|
/**
|
|
18
|
-
* analyze_code_impact: Counts and categorizes files listed in
|
|
18
|
+
* analyze_code_impact: Counts and categorizes files listed in spec.md.
|
|
19
19
|
* Flags specs with >10 affected files as HIGH IMPACT.
|
|
20
20
|
*/
|
|
21
21
|
export declare function analyzeCodeImpact(ctx: ActionContext): Promise<ActionResult>;
|
|
@@ -21,7 +21,7 @@ async function findSpecDir(projectPath, specId) {
|
|
|
21
21
|
const dir = matches[0];
|
|
22
22
|
return dir !== undefined ? join(specDir, dir) : null;
|
|
23
23
|
}
|
|
24
|
-
/** Extract file path references from
|
|
24
|
+
/** Extract file path references from spec content (backtick-quoted paths with slashes). */
|
|
25
25
|
function extractFileRefs(content) {
|
|
26
26
|
const regex = /`([^`]*\/[^`]+\.[a-zA-Z]+)`/g;
|
|
27
27
|
const seen = new Set();
|
|
@@ -149,7 +149,7 @@ export async function rewriteCriteriaEars(ctx) {
|
|
|
149
149
|
// ---------------------------------------------------------------------------
|
|
150
150
|
/**
|
|
151
151
|
* verify_spec_compliance: Checks that a spec has spec.md with criteria,
|
|
152
|
-
*
|
|
152
|
+
* inline file references in spec.md, and that those files exist on disk.
|
|
153
153
|
*/
|
|
154
154
|
export async function verifySpecCompliance(ctx) {
|
|
155
155
|
if (!ctx.specId) {
|
|
@@ -168,29 +168,25 @@ export async function verifySpecCompliance(ctx) {
|
|
|
168
168
|
}
|
|
169
169
|
const findings = [];
|
|
170
170
|
const specMdPath = join(specDirPath, 'spec.md');
|
|
171
|
-
|
|
171
|
+
let specContent = '';
|
|
172
172
|
// Check 1: spec.md has acceptance criteria
|
|
173
173
|
if (!existsSync(specMdPath)) {
|
|
174
174
|
findings.push('spec.md is missing');
|
|
175
175
|
}
|
|
176
176
|
else {
|
|
177
|
-
|
|
177
|
+
specContent = await readFile(specMdPath, 'utf-8');
|
|
178
178
|
const criteriaCount = (specContent.match(/^\s*-\s+\[[ xX]\]\s+.+$/gm) ?? []).length;
|
|
179
179
|
if (criteriaCount === 0) {
|
|
180
180
|
findings.push('spec.md has no acceptance criteria');
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
// Check 2:
|
|
183
|
+
// Check 2: spec.md inline file references
|
|
184
184
|
const implementedFiles = [];
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
const techContent = await readFile(technicalMdPath, 'utf-8');
|
|
190
|
-
const refs = extractFileRefs(techContent);
|
|
185
|
+
if (specContent.length > 0) {
|
|
186
|
+
const refs = extractFileRefs(specContent);
|
|
191
187
|
implementedFiles.push(...refs);
|
|
192
188
|
if (refs.length === 0) {
|
|
193
|
-
findings.push('
|
|
189
|
+
findings.push('spec.md inline Technical/Files sections have no file references');
|
|
194
190
|
}
|
|
195
191
|
}
|
|
196
192
|
// Check 3: referenced files exist on disk
|
|
@@ -203,7 +199,7 @@ export async function verifySpecCompliance(ctx) {
|
|
|
203
199
|
}
|
|
204
200
|
if (missingFiles.length > 0) {
|
|
205
201
|
const listed = missingFiles.slice(0, 3).join(', ');
|
|
206
|
-
findings.push(`${String(missingFiles.length)} file(s) listed in
|
|
202
|
+
findings.push(`${String(missingFiles.length)} file(s) listed in spec.md not found: ${listed}`);
|
|
207
203
|
}
|
|
208
204
|
const passed = findings.length === 0;
|
|
209
205
|
const summary = passed
|
|
@@ -215,7 +211,7 @@ export async function verifySpecCompliance(ctx) {
|
|
|
215
211
|
// analyze_code_impact
|
|
216
212
|
// ---------------------------------------------------------------------------
|
|
217
213
|
/**
|
|
218
|
-
* analyze_code_impact: Counts and categorizes files listed in
|
|
214
|
+
* analyze_code_impact: Counts and categorizes files listed in spec.md.
|
|
219
215
|
* Flags specs with >10 affected files as HIGH IMPACT.
|
|
220
216
|
*/
|
|
221
217
|
export async function analyzeCodeImpact(ctx) {
|
|
@@ -233,16 +229,16 @@ export async function analyzeCodeImpact(ctx) {
|
|
|
233
229
|
durationMs: 0,
|
|
234
230
|
};
|
|
235
231
|
}
|
|
236
|
-
const
|
|
237
|
-
if (!existsSync(
|
|
232
|
+
const specMdPath = join(specDirPath, 'spec.md');
|
|
233
|
+
if (!existsSync(specMdPath)) {
|
|
238
234
|
return {
|
|
239
235
|
success: false,
|
|
240
|
-
summary: `analyze_code_impact:
|
|
236
|
+
summary: `analyze_code_impact: spec.md not found for ${ctx.specId}`,
|
|
241
237
|
durationMs: 0,
|
|
242
238
|
};
|
|
243
239
|
}
|
|
244
|
-
const
|
|
245
|
-
const allFiles = extractFileRefs(
|
|
240
|
+
const specContent = await readFile(specMdPath, 'utf-8');
|
|
241
|
+
const allFiles = extractFileRefs(specContent);
|
|
246
242
|
// Breakdown by extension — normalize .test.ts/.test.js as separate categories
|
|
247
243
|
const breakdown = {};
|
|
248
244
|
for (const filePath of allFiles) {
|
|
@@ -60,8 +60,8 @@ function buildValidateSection() {
|
|
|
60
60
|
' SPEC_NAME=$(basename "$spec_dir")',
|
|
61
61
|
' TOTAL=$((TOTAL + 1))',
|
|
62
62
|
'',
|
|
63
|
-
' SPEC_FILE
|
|
64
|
-
' if [ -
|
|
63
|
+
' SPEC_FILE="$spec_dir/spec.md"',
|
|
64
|
+
' if [ ! -f "$SPEC_FILE" ]; then',
|
|
65
65
|
' echo -e " ${RED}x${RESET} $SPEC_NAME - missing spec.md"',
|
|
66
66
|
' ISSUES=$((ISSUES + 1))',
|
|
67
67
|
' continue',
|
|
@@ -90,8 +90,8 @@ function buildDriftSection() {
|
|
|
90
90
|
' echo -e "${BOLD}Drift Detection${RESET}"',
|
|
91
91
|
' CHANGED_FILES=$(git diff HEAD --name-only 2>/dev/null || echo "")',
|
|
92
92
|
' for spec_dir in "$SPECS_DIR"/*/; do',
|
|
93
|
-
' [ -f "$spec_dir/
|
|
94
|
-
' SPEC_FILES=$(grep -oE \'src/[a-zA-Z0-9_./-]+\' "$spec_dir/
|
|
93
|
+
' [ -f "$spec_dir/spec.md" ] || continue',
|
|
94
|
+
' SPEC_FILES=$(grep -oE \'src/[a-zA-Z0-9_./-]+\' "$spec_dir/spec.md" 2>/dev/null || echo "")',
|
|
95
95
|
' for sf in $SPEC_FILES; do',
|
|
96
96
|
' if echo "$CHANGED_FILES" | grep -q "$sf"; then',
|
|
97
97
|
' SPEC_NAME=$(basename "$spec_dir")',
|
|
@@ -113,9 +113,8 @@ function buildDriftStepLines(indent, specsDir) {
|
|
|
113
113
|
`${indent} echo "$CHANGED_FILES"`,
|
|
114
114
|
`${indent} DRIFT_FOUND="false"`,
|
|
115
115
|
`${indent} for spec_dir in ${specsDir}/*/; do`,
|
|
116
|
-
`${indent} if [ -f "$spec_dir/
|
|
117
|
-
`${indent}
|
|
118
|
-
`${indent} SPEC_FILES=$(grep -oE 'src/[a-zA-Z0-9_./-]+' "$TECH_FILE" 2>/dev/null || echo "")`,
|
|
116
|
+
`${indent} if [ -f "$spec_dir/spec.md" ]; then`,
|
|
117
|
+
`${indent} SPEC_FILES=$(grep -oE 'src/[a-zA-Z0-9_./-]+' "$spec_dir/spec.md" 2>/dev/null || echo "")`,
|
|
119
118
|
`${indent} for sf in $SPEC_FILES; do`,
|
|
120
119
|
`${indent} if echo "$CHANGED_FILES" | grep -q "$sf"; then`,
|
|
121
120
|
`${indent} SPEC_NAME=$(basename "$spec_dir")`,
|
|
@@ -145,8 +144,8 @@ function buildValidateStepLines(indent, specsDir) {
|
|
|
145
144
|
`${indent} [ -d "$spec_dir" ] || continue`,
|
|
146
145
|
`${indent} SPEC_NAME=$(basename "$spec_dir")`,
|
|
147
146
|
`${indent} TOTAL=$((TOTAL + 1))`,
|
|
148
|
-
`${indent} SPEC_FILE
|
|
149
|
-
`${indent} if [ -
|
|
147
|
+
`${indent} SPEC_FILE="$spec_dir/spec.md"`,
|
|
148
|
+
`${indent} if [ ! -f "$SPEC_FILE" ]; then`,
|
|
150
149
|
`${indent} RESULTS+="$SPEC_NAME FAIL missing-spec\\n"`,
|
|
151
150
|
`${indent} COMMENT+="| $SPEC_NAME | FAIL | missing spec.md |\\n"`,
|
|
152
151
|
`${indent} ISSUES=$((ISSUES + 1))`,
|
|
@@ -48,7 +48,6 @@ async function writeStubSpec(projectPath, specId, title, framework, criteria, dr
|
|
|
48
48
|
'',
|
|
49
49
|
].join('\n');
|
|
50
50
|
await writeFile(join(specDir, 'spec.md'), specContent, 'utf-8');
|
|
51
|
-
await writeFile(join(specDir, 'progress.md'), `# ${specId} — Progress\n\nstatus: draft\n`, 'utf-8');
|
|
52
51
|
}
|
|
53
52
|
// ---------------------------------------------------------------------------
|
|
54
53
|
// Manual action builders
|
|
@@ -43,7 +43,6 @@ async function createFollowUpSpec(projectPath, parentSpecId, reason, dryRun) {
|
|
|
43
43
|
'',
|
|
44
44
|
].join('\n');
|
|
45
45
|
await writeFile(join(specDir, 'spec.md'), specContent, 'utf-8');
|
|
46
|
-
await writeFile(join(specDir, 'progress.md'), `# ${specId} — Progress\n\nstatus: draft\n`, 'utf-8');
|
|
47
46
|
}
|
|
48
47
|
return specId;
|
|
49
48
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// engine/health/auto-fixer.ts — Auto-fix for health check issues (SPEC-408)
|
|
2
|
-
import { mkdir, writeFile, access } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
2
|
import { specStore } from '../../storage/index.js';
|
|
5
3
|
import { computeSpecHealth } from '../validation-loop.js';
|
|
6
4
|
async function collectHealthIssues(projectPath, projectId) {
|
|
@@ -31,30 +29,6 @@ async function collectHealthIssues(projectPath, projectId) {
|
|
|
31
29
|
// ---------------------------------------------------------------------------
|
|
32
30
|
// Fix handlers
|
|
33
31
|
// ---------------------------------------------------------------------------
|
|
34
|
-
async function fixMissingProgress(issue, dryRun) {
|
|
35
|
-
const specId = issue.specId;
|
|
36
|
-
const progressPath = join(issue.projectPath, 'planu', 'specs', specId, 'progress.md');
|
|
37
|
-
try {
|
|
38
|
-
await access(progressPath);
|
|
39
|
-
return null; // Already exists
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// Does not exist — create it
|
|
43
|
-
if (!dryRun) {
|
|
44
|
-
await mkdir(join(issue.projectPath, 'planu', 'specs', specId), { recursive: true });
|
|
45
|
-
const content = `# ${specId} — Progress\n\nstatus: draft\n\n## Tasks\n\n- [ ] Define acceptance criteria\n`;
|
|
46
|
-
await writeFile(progressPath, content, 'utf-8');
|
|
47
|
-
}
|
|
48
|
-
return {
|
|
49
|
-
issue: 'missing-progress.md',
|
|
50
|
-
action: dryRun
|
|
51
|
-
? 'would create progress.md from template'
|
|
52
|
-
: 'created progress.md from template',
|
|
53
|
-
affectedItem: specId,
|
|
54
|
-
success: true,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
32
|
function buildSkipForIncompleteSpec(specId) {
|
|
59
33
|
return {
|
|
60
34
|
issue: `incomplete-spec: ${specId}`,
|
|
@@ -96,18 +70,12 @@ function buildSkipForDrift(specId) {
|
|
|
96
70
|
* When dryRun=true, reports what would be fixed without writing any files.
|
|
97
71
|
*/
|
|
98
72
|
export async function autoFixHealth(projectPath, projectId, dryRun = false) {
|
|
73
|
+
void dryRun;
|
|
99
74
|
const issues = await collectHealthIssues(projectPath, projectId);
|
|
100
75
|
const fixes = [];
|
|
101
76
|
const skipped = [];
|
|
102
77
|
for (const issue of issues) {
|
|
103
78
|
switch (issue.type) {
|
|
104
|
-
case 'missing-progress': {
|
|
105
|
-
const fix = await fixMissingProgress(issue, dryRun);
|
|
106
|
-
if (fix) {
|
|
107
|
-
fixes.push(fix);
|
|
108
|
-
}
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
79
|
case 'incomplete-spec':
|
|
112
80
|
skipped.push(buildSkipForIncompleteSpec(issue.specId));
|
|
113
81
|
break;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// engine/living-spec-analyzer.ts — Heuristic analysis of spec criteria vs codebase
|
|
2
2
|
// Compares acceptance criteria against actual code to produce a living spec report.
|
|
3
3
|
// No LLM calls — purely file-existence and pattern-matching heuristics.
|
|
4
|
-
import { readFile,
|
|
4
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
5
5
|
import { replaceSectionInSpec } from './spec-format/replace-section.js';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { glob } from 'glob';
|
|
@@ -34,8 +34,8 @@ export async function analyzeLivingSpec(spec, projectPath) {
|
|
|
34
34
|
progressUpdated = await updateProgressInSpec(spec.specPath, assessments, completionPercent);
|
|
35
35
|
}
|
|
36
36
|
else if (spec.progressPath) {
|
|
37
|
-
// Legacy
|
|
38
|
-
progressUpdated =
|
|
37
|
+
// Legacy progress.md paths are read-only compatibility metadata in unified spec mode.
|
|
38
|
+
progressUpdated = false;
|
|
39
39
|
}
|
|
40
40
|
return {
|
|
41
41
|
specId: spec.id,
|
|
@@ -362,37 +362,6 @@ function extractSectionBodyLocal(content, sectionName) {
|
|
|
362
362
|
const end = next ? next.index : normalized.length;
|
|
363
363
|
return normalized.slice(afterHeading, end).trim();
|
|
364
364
|
}
|
|
365
|
-
/**
|
|
366
|
-
* Update the spec's progress.md with living spec analysis results.
|
|
367
|
-
* Legacy fallback: only called when specPath is unknown but progressPath exists.
|
|
368
|
-
*/
|
|
369
|
-
async function updateProgressFile(progressPath, assessments, completionPercent) {
|
|
370
|
-
try {
|
|
371
|
-
let content;
|
|
372
|
-
try {
|
|
373
|
-
content = await readFile(progressPath, 'utf-8');
|
|
374
|
-
}
|
|
375
|
-
catch {
|
|
376
|
-
content = '';
|
|
377
|
-
}
|
|
378
|
-
const timestamp = new Date().toISOString();
|
|
379
|
-
const sectionHeader = '### Living Spec Analysis';
|
|
380
|
-
const newSection = buildProgressSection(assessments, completionPercent, timestamp);
|
|
381
|
-
if (content.includes(sectionHeader)) {
|
|
382
|
-
// Replace existing section (up to next ## or end of file)
|
|
383
|
-
content = content.replace(/### Living Spec Analysis[\s\S]*?(?=\n### |\n## |\n---\n|$)/, newSection);
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
// Append new section
|
|
387
|
-
content = content.trimEnd() + '\n\n---\n\n' + newSection + '\n';
|
|
388
|
-
}
|
|
389
|
-
await writeFile(progressPath, content, 'utf-8');
|
|
390
|
-
return true;
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
365
|
/**
|
|
397
366
|
* Build the markdown section for living spec progress.
|
|
398
367
|
*/
|
|
@@ -57,7 +57,7 @@ export function getSpecFileNames(naming) {
|
|
|
57
57
|
if (naming === 'legacy') {
|
|
58
58
|
return { spec: 'HU.md', technical: 'FICHA-TECNICA.md', progress: 'PROGRESS.md' };
|
|
59
59
|
}
|
|
60
|
-
return { spec: 'spec.md', technical: '
|
|
60
|
+
return { spec: 'spec.md', technical: 'spec.md', progress: 'spec.md' };
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Check if a specLocation uses legacy naming.
|
|
@@ -101,7 +101,7 @@ async function generateSpecs(input, modules, moduleResults, crossAnalysis, healt
|
|
|
101
101
|
});
|
|
102
102
|
const specDir = join(input.path, SPEC_LOCATION, `${specId}-${slug}`);
|
|
103
103
|
spec.specPath = join(specDir, 'spec.md');
|
|
104
|
-
spec.technicalPath =
|
|
104
|
+
spec.technicalPath = spec.specPath;
|
|
105
105
|
const body = `# ${spec.title}\n\nAuto-generated by scan_project.\n`;
|
|
106
106
|
await writeSpecFiles(specDir, spec, body);
|
|
107
107
|
await createSpec(input.projectId, spec);
|
|
@@ -127,7 +127,7 @@ async function generateSpecs(input, modules, moduleResults, crossAnalysis, healt
|
|
|
127
127
|
});
|
|
128
128
|
const overviewDir = join(input.path, SPEC_LOCATION, `${overviewSpecId}-${overviewSlug}`);
|
|
129
129
|
overviewSpec.specPath = join(overviewDir, 'spec.md');
|
|
130
|
-
overviewSpec.technicalPath =
|
|
130
|
+
overviewSpec.technicalPath = overviewSpec.specPath;
|
|
131
131
|
const overviewBody = overviewContent
|
|
132
132
|
? `# Project Overview — Automated Scan\n\n${overviewContent}\n`
|
|
133
133
|
: `# Project Overview — Automated Scan\n\nAuto-generated by scan_project.\n`;
|
|
@@ -43,7 +43,7 @@ export function generateDomainSkill(config, name, description, knowledge, specCo
|
|
|
43
43
|
'1. **Understand** — Read spec criteria, review existing code, identify affected modules',
|
|
44
44
|
'2. **Implement** — Write code following architecture patterns and project conventions',
|
|
45
45
|
'3. **Validate** — Run tests, typecheck, lint; verify all spec criteria pass',
|
|
46
|
-
'4. **Track** — Update
|
|
46
|
+
'4. **Track** — Update spec status or the inline ## Progress section with session results',
|
|
47
47
|
'5. **Commit** — Commit with conventional commit message (feat/fix/refactor)',
|
|
48
48
|
'',
|
|
49
49
|
'## Guidelines',
|
|
@@ -11,7 +11,7 @@ const CANONICAL_ROOT_FILES = new Set([
|
|
|
11
11
|
'roadmap.html',
|
|
12
12
|
]);
|
|
13
13
|
const CANONICAL_ROOT_DIRS = new Set(['specs']);
|
|
14
|
-
const CANONICAL_SPEC_FILES = new Set(['spec.md'
|
|
14
|
+
const CANONICAL_SPEC_FILES = new Set(['spec.md']);
|
|
15
15
|
async function scanPlanuRoot(projectPath) {
|
|
16
16
|
const rootFiles = [];
|
|
17
17
|
const planuDir = join(projectPath, 'planu');
|
|
@@ -7,8 +7,10 @@ import { safeUnlink } from './git-aware-fs.js';
|
|
|
7
7
|
import { parseFrontmatterLocal } from './frontmatter-parser.js';
|
|
8
8
|
import { generateLeanSpecContent } from '../spec-format/lean-spec-generator.js';
|
|
9
9
|
import { generateLeanTechnicalContent } from '../spec-format/lean-technical-generator.js';
|
|
10
|
+
import { buildUnifiedSpecContent } from '../spec-format/unified-spec-builder.js';
|
|
10
11
|
/** Files to delete from old-format specs. */
|
|
11
12
|
const OBSOLETE_FILES = [
|
|
13
|
+
'technical.md',
|
|
12
14
|
'progress.md',
|
|
13
15
|
'executive-report.html',
|
|
14
16
|
'technical-report.html',
|
|
@@ -380,7 +382,7 @@ export async function migrateSpecToLean(specDir, projectPath) {
|
|
|
380
382
|
const leanSpecLines = generateLeanSpecContent({ spec, description, estimation });
|
|
381
383
|
// Inject real criteria (replace the default one)
|
|
382
384
|
const leanSpecWithCriteria = replaceCriteria(leanSpecLines, criteria);
|
|
383
|
-
// Generate
|
|
385
|
+
// Generate the legacy technical body, then fold it into unified spec.md.
|
|
384
386
|
const leanTech = generateLeanTechnicalContent({
|
|
385
387
|
specId: id,
|
|
386
388
|
filesToCreate: files.create.map((p) => ({ path: p, status: 'pending' })),
|
|
@@ -394,9 +396,8 @@ export async function migrateSpecToLean(specDir, projectPath) {
|
|
|
394
396
|
catch (backupErr) {
|
|
395
397
|
return `backup failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`;
|
|
396
398
|
}
|
|
397
|
-
// Write
|
|
398
|
-
await atomicWriteFile(specPath, leanSpecWithCriteria);
|
|
399
|
-
await atomicWriteFile(techPath, leanTech);
|
|
399
|
+
// Write one unified spec.md.
|
|
400
|
+
await atomicWriteFile(specPath, buildUnifiedSpecContent(leanSpecWithCriteria, leanTech));
|
|
400
401
|
// Delete obsolete files
|
|
401
402
|
for (const file of OBSOLETE_FILES) {
|
|
402
403
|
const filePath = join(specDir, file);
|
|
@@ -14,7 +14,7 @@ const CANONICAL_ROOT_FILES = new Set([
|
|
|
14
14
|
/** Directories allowed in planu/ root. */
|
|
15
15
|
const CANONICAL_ROOT_DIRS = new Set(['specs']);
|
|
16
16
|
/** Files allowed inside each planu/specs/SPEC-XXX/ directory. */
|
|
17
|
-
const CANONICAL_SPEC_FILES = new Set(['spec.md'
|
|
17
|
+
const CANONICAL_SPEC_FILES = new Set(['spec.md']);
|
|
18
18
|
/** Remove a file from git tracking (best-effort, silent if not a git repo or file not tracked). */
|
|
19
19
|
function gitRmCached(absolutePath, cwd) {
|
|
20
20
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pack a spec directory into a gzipped archive buffer.
|
|
3
|
-
* Only includes recognized spec files (spec.md,
|
|
3
|
+
* Only includes recognized spec files (spec.md, planu-registry.json).
|
|
4
4
|
* @param specDir - Absolute path to the spec directory.
|
|
5
5
|
* @returns Gzipped buffer containing the archived spec files.
|
|
6
6
|
*/
|
|
@@ -10,10 +10,10 @@ const gunzipAsync = promisify(gunzip);
|
|
|
10
10
|
/** Maximum package size in bytes (500 KB). */
|
|
11
11
|
const MAX_PACKAGE_SIZE = 500 * 1024;
|
|
12
12
|
/** Files that may be included in a spec package. */
|
|
13
|
-
const ALLOWED_FILES = ['spec.md', '
|
|
13
|
+
const ALLOWED_FILES = ['spec.md', 'planu-registry.json'];
|
|
14
14
|
/**
|
|
15
15
|
* Pack a spec directory into a gzipped archive buffer.
|
|
16
|
-
* Only includes recognized spec files (spec.md,
|
|
16
|
+
* Only includes recognized spec files (spec.md, planu-registry.json).
|
|
17
17
|
* @param specDir - Absolute path to the spec directory.
|
|
18
18
|
* @returns Gzipped buffer containing the archived spec files.
|
|
19
19
|
*/
|
|
@@ -7,8 +7,7 @@ const MAX_TOTAL_SIZE = 500 * 1024;
|
|
|
7
7
|
/** Semver regex (simplified: major.minor.patch with optional pre-release). */
|
|
8
8
|
const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
|
|
9
9
|
/** Required files in a publishable spec directory. */
|
|
10
|
-
|
|
11
|
-
const REQUIRED_FILES = ['spec.md', 'technical.md'];
|
|
10
|
+
const REQUIRED_FILES = ['spec.md'];
|
|
12
11
|
/** Required fields in the planu-registry.json manifest. */
|
|
13
12
|
const REQUIRED_MANIFEST_FIELDS = [
|
|
14
13
|
'name',
|