@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
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 technical.md documents: model name, provider, and access date — EU AI Act Article 53(1)(a)",
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": "HU.md file exists and is complete",
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": "FICHA-TECNICA.md file exists and is complete",
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 (HU.md, FICHA-TECNICA.md, PLAN.md)",
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 and technical.md files must ALWAYS be written in English regardless of locale.',
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 and technical.md files must ALWAYS be written in English regardless of locale.',
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
- - A `technical.md` with file-level implementation plan
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`, `technical.md`, and `progress.json`
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 both `spec.md` and `technical.md`.
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 `technical.md` has placeholder content.
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 `technical.md` file must list concrete file paths, not placeholders
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` and `technical.md` for full context.
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's HU.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 its HU.md. Must run BEFORE starting implementation — never implement XL specs directly.",
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 HU.md", "HU.md has acceptance criteria section"],
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
  },
@@ -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 = join(specDir, 'technical.md');
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 or spec tracking files — delegate to parent skill');
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
- * technical.md with file references, and that those files exist on disk.
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 technical.md.
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 technical.md content (backtick-quoted paths with slashes). */
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
- * technical.md with file references, and that those files exist on disk.
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
- const technicalMdPath = join(specDirPath, 'technical.md');
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
- const specContent = await readFile(specMdPath, 'utf-8');
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: technical.md with file references
183
+ // Check 2: spec.md inline file references
184
184
  const implementedFiles = [];
185
- if (!existsSync(technicalMdPath)) {
186
- findings.push('technical.md is missing');
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('technical.md has no file references');
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 technical.md not found: ${listed}`);
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 technical.md.
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 technicalMdPath = join(specDirPath, 'technical.md');
237
- if (!existsSync(technicalMdPath)) {
232
+ const specMdPath = join(specDirPath, 'spec.md');
233
+ if (!existsSync(specMdPath)) {
238
234
  return {
239
235
  success: false,
240
- summary: `analyze_code_impact: technical.md not found for ${ctx.specId}`,
236
+ summary: `analyze_code_impact: spec.md not found for ${ctx.specId}`,
241
237
  durationMs: 0,
242
238
  };
243
239
  }
244
- const techContent = await readFile(technicalMdPath, 'utf-8');
245
- const allFiles = extractFileRefs(techContent);
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=$(ls "$spec_dir"spec.md "$spec_dir"HU.md 2>/dev/null | head -1 || echo "")',
64
- ' if [ -z "$SPEC_FILE" ]; then',
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/technical.md" ] || continue',
94
- ' SPEC_FILES=$(grep -oE \'src/[a-zA-Z0-9_./-]+\' "$spec_dir/technical.md" 2>/dev/null || echo "")',
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/technical.md" ] || [ -f "$spec_dir/FICHA-TECNICA.md" ]; then`,
117
- `${indent} TECH_FILE=$(ls "$spec_dir"technical.md "$spec_dir"FICHA-TECNICA.md 2>/dev/null | head -1)`,
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=$(ls "$spec_dir"spec.md "$spec_dir"HU.md 2>/dev/null | head -1)`,
149
- `${indent} if [ -z "$SPEC_FILE" ]; then`,
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, writeFile, stat } from 'node:fs/promises';
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 fallback: spec.md path unknown but progress.md path set
38
- progressUpdated = await updateProgressFile(spec.progressPath, assessments, completionPercent);
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: 'technical.md', progress: 'progress.md' };
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 = join(specDir, 'technical.md');
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 = join(overviewDir, 'technical.md');
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 PROGRESS.md or spec status with session results',
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', 'technical.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 lean technical.md (estimation only in spec.md — SPEC-462)
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 lean files
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', 'technical.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, technical.md, progress.md, planu-registry.json).
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', 'technical.md', 'progress.md', 'planu-registry.json'];
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, technical.md, progress.md, planu-registry.json).
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
- // SPEC-461: Lean format — only 2 required files (no progress.md)
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',