@planu/cli 5.3.61 → 5.3.63
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 +32 -0
- package/dist/.planu-build.json +1 -1
- package/dist/engine/challenge-prioritizer.js +2 -1
- package/dist/engine/rules-generator/index.js +4 -47
- package/dist/engine/skill-registry/installer.js +17 -28
- package/dist/engine/skills/skills-fetcher.js +0 -9
- package/dist/engine/skills-reconciler.js +1 -13
- package/dist/tools/challenge-spec/scenarios-utils.js +1 -1
- package/dist/tools/challenge-spec.js +11 -2
- package/dist/tools/suggest-tooling/skills-catalog.js +0 -9
- package/dist/tools/update-status/dod-gates.js +20 -10
- package/dist/tools/update-status/file-sync.js +37 -5
- package/dist/types/ai-tool-rules.d.ts +0 -1
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## [5.3.63] - 2026-08-27
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(SPEC-1644): stop emitting the default architecture rule and every line-cap instruction
|
|
5
|
+
- fix(SPEC-1643): remove the empty typescript-patterns builtin skill
|
|
6
|
+
- fix(SPEC-1371): compute branch cleanup against main with literal protected matching
|
|
7
|
+
- fix(SPEC-1646): discriminate contradiction findings by offending criterion
|
|
8
|
+
- fix(SPEC-1646): discriminate contradiction findings by offending criterion
|
|
9
|
+
- fix(SPEC-1645): widen the load-bearing separator class per dual review
|
|
10
|
+
- fix(SPEC-1645): stop treating load-bearing as a scale signal
|
|
11
|
+
|
|
12
|
+
### Chores
|
|
13
|
+
- chore(planu): file goal stop-hook loop dogfood bug
|
|
14
|
+
- chore(SPEC-1644): record done transition
|
|
15
|
+
- chore(planu): file SPEC-1651 and SPEC-1652 dogfood bugs
|
|
16
|
+
- chore(SPEC-1644): record approved status
|
|
17
|
+
- chore(SPEC-1643): mark acceptance criteria done
|
|
18
|
+
- chore(SPEC-1643): lifecycle transition to approved
|
|
19
|
+
- chore(planu): record SPEC-1371 approval and SPEC-1649 filing
|
|
20
|
+
- chore(SPEC-1646): record approval transition
|
|
21
|
+
- chore(SPEC-1645): record done transition
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [5.3.62] - 2026-08-26
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
- fix(SPEC-1639,1640,1641,1642): four dogfood defects in challenge, gates, coverage and sync
|
|
28
|
+
|
|
29
|
+
### Chores
|
|
30
|
+
- chore(planu): close SPEC-1639, SPEC-1640, SPEC-1641 and SPEC-1642
|
|
31
|
+
|
|
32
|
+
|
|
1
33
|
## [5.3.61] - 2026-08-26
|
|
2
34
|
|
|
3
35
|
### Refactoring
|
package/dist/.planu-build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"commit":"
|
|
1
|
+
{"schemaVersion":1,"commit":"ae29e281086a9333b51c28dc6fd1c297538efe4d"}
|
|
@@ -72,6 +72,7 @@ export function buildPrioritizedSummary(top3) {
|
|
|
72
72
|
return '';
|
|
73
73
|
}
|
|
74
74
|
const lines = top3.map((s) => `${s.rank}. ${s.scenario}`);
|
|
75
|
-
|
|
75
|
+
const noun = top3.length === 1 ? 'thing' : 'things';
|
|
76
|
+
return `Here are the ${String(top3.length)} ${noun} you MUST address before coding:\n${lines.join('\n')}`;
|
|
76
77
|
}
|
|
77
78
|
//# sourceMappingURL=challenge-prioritizer.js.map
|
|
@@ -27,11 +27,9 @@ export const ARCHITECTURE_RULES = {
|
|
|
27
27
|
],
|
|
28
28
|
conventions: [
|
|
29
29
|
'Never put business logic in route handlers',
|
|
30
|
-
'Route handlers should be under 30 lines',
|
|
31
30
|
'Services are framework-agnostic (no NextRequest/NextResponse)',
|
|
32
31
|
'Repositories return plain objects, never ORM entities',
|
|
33
32
|
],
|
|
34
|
-
maxLinesPerHandler: 30,
|
|
35
33
|
},
|
|
36
34
|
express: {
|
|
37
35
|
title: 'Express Architecture',
|
|
@@ -58,7 +56,6 @@ export const ARCHITECTURE_RULES = {
|
|
|
58
56
|
'Services are stateless',
|
|
59
57
|
'One file per resource',
|
|
60
58
|
],
|
|
61
|
-
maxLinesPerHandler: 40,
|
|
62
59
|
},
|
|
63
60
|
fastapi: {
|
|
64
61
|
title: 'FastAPI Architecture',
|
|
@@ -85,34 +82,6 @@ export const ARCHITECTURE_RULES = {
|
|
|
85
82
|
'Pydantic models for all I/O',
|
|
86
83
|
'Async by default',
|
|
87
84
|
],
|
|
88
|
-
maxLinesPerHandler: 30,
|
|
89
|
-
},
|
|
90
|
-
default: {
|
|
91
|
-
title: 'Clean Architecture',
|
|
92
|
-
description: 'Generic clean architecture rules',
|
|
93
|
-
layers: [
|
|
94
|
-
{
|
|
95
|
-
name: 'Handler',
|
|
96
|
-
path: 'src/handlers/',
|
|
97
|
-
responsibility: 'Input/output boundary',
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'Service',
|
|
101
|
-
path: 'src/services/',
|
|
102
|
-
responsibility: 'Business logic',
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: 'Repository',
|
|
106
|
-
path: 'src/repositories/',
|
|
107
|
-
responsibility: 'Data access',
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
conventions: [
|
|
111
|
-
'Separate concerns by layer',
|
|
112
|
-
'Dependencies point inward',
|
|
113
|
-
'No framework code in business logic',
|
|
114
|
-
],
|
|
115
|
-
maxLinesPerHandler: 50,
|
|
116
85
|
},
|
|
117
86
|
};
|
|
118
87
|
/** Detect stack from project files. Returns a key into ARCHITECTURE_RULES. */
|
|
@@ -187,9 +156,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
187
156
|
'## Conventions',
|
|
188
157
|
conventions,
|
|
189
158
|
'',
|
|
190
|
-
`## Handler Size Limit`,
|
|
191
|
-
`Max ${rule.maxLinesPerHandler} lines per handler/controller.`,
|
|
192
|
-
'',
|
|
193
159
|
].join('\n');
|
|
194
160
|
}
|
|
195
161
|
case 'copilot': {
|
|
@@ -205,9 +171,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
205
171
|
'## Coding Conventions',
|
|
206
172
|
conventions,
|
|
207
173
|
'',
|
|
208
|
-
`## Size Constraints`,
|
|
209
|
-
`- Max ${rule.maxLinesPerHandler} lines per handler`,
|
|
210
|
-
'',
|
|
211
174
|
].join('\n');
|
|
212
175
|
}
|
|
213
176
|
case 'aider': {
|
|
@@ -223,9 +186,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
223
186
|
'## Conventions',
|
|
224
187
|
conventions,
|
|
225
188
|
'',
|
|
226
|
-
`## Constraints`,
|
|
227
|
-
`- Handlers/controllers: max ${rule.maxLinesPerHandler} lines`,
|
|
228
|
-
'',
|
|
229
189
|
].join('\n');
|
|
230
190
|
}
|
|
231
191
|
case 'claude':
|
|
@@ -244,11 +204,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
244
204
|
'',
|
|
245
205
|
conventions,
|
|
246
206
|
'',
|
|
247
|
-
'## Size Budget',
|
|
248
|
-
'',
|
|
249
|
-
`- Max **${rule.maxLinesPerHandler} lines** per handler/controller`,
|
|
250
|
-
'- Extract helpers when exceeding this limit',
|
|
251
|
-
'',
|
|
252
207
|
].join('\n');
|
|
253
208
|
}
|
|
254
209
|
}
|
|
@@ -273,8 +228,10 @@ function resolveRulesFilePath(projectPath, tool, stackKey) {
|
|
|
273
228
|
/** Main entry: detect stack + tools, generate all rules objects. */
|
|
274
229
|
export function generateProjectRules(projectPath) {
|
|
275
230
|
const stackKey = detectArchitectureStack(projectPath);
|
|
276
|
-
|
|
277
|
-
|
|
231
|
+
if (stackKey === 'default') {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const rule = ARCHITECTURE_RULES[stackKey];
|
|
278
235
|
if (!rule) {
|
|
279
236
|
return [];
|
|
280
237
|
}
|
|
@@ -79,35 +79,24 @@ function buildFrontmatter(entry) {
|
|
|
79
79
|
/** Prepare a built-in skill from the local catalog, generating a SKILL.md. */
|
|
80
80
|
function prepareFromBuiltIn(skillName) {
|
|
81
81
|
const entry = getBuiltInSkillEntry(skillName);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const frontmatter = buildFrontmatter(entry);
|
|
85
|
-
content = [
|
|
86
|
-
frontmatter,
|
|
87
|
-
`# ${entry.name}`,
|
|
88
|
-
``,
|
|
89
|
-
entry.description,
|
|
90
|
-
``,
|
|
91
|
-
`## When to use`,
|
|
92
|
-
``,
|
|
93
|
-
entry.trigger,
|
|
94
|
-
``,
|
|
95
|
-
`## Tags`,
|
|
96
|
-
``,
|
|
97
|
-
entry.tags.join(', '),
|
|
98
|
-
].join('\n');
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
content = [
|
|
102
|
-
`# ${skillName}`,
|
|
103
|
-
``,
|
|
104
|
-
`Built-in Planu skill.`,
|
|
105
|
-
``,
|
|
106
|
-
`## When to use`,
|
|
107
|
-
``,
|
|
108
|
-
`Consult the Planu documentation.`,
|
|
109
|
-
].join('\n');
|
|
82
|
+
if (!entry) {
|
|
83
|
+
throw new Error(`Built-in skill '${skillName}' has no catalog entry; refusing to install a placeholder`);
|
|
110
84
|
}
|
|
85
|
+
const frontmatter = buildFrontmatter(entry);
|
|
86
|
+
const content = [
|
|
87
|
+
frontmatter,
|
|
88
|
+
`# ${entry.name}`,
|
|
89
|
+
``,
|
|
90
|
+
entry.description,
|
|
91
|
+
``,
|
|
92
|
+
`## When to use`,
|
|
93
|
+
``,
|
|
94
|
+
entry.trigger,
|
|
95
|
+
``,
|
|
96
|
+
`## Tags`,
|
|
97
|
+
``,
|
|
98
|
+
entry.tags.join(', '),
|
|
99
|
+
].join('\n');
|
|
111
100
|
return { content, hasScripts: false };
|
|
112
101
|
}
|
|
113
102
|
/** Prepare a skill placeholder for skillssh/agentskill sources (no direct file API yet). */
|
|
@@ -89,15 +89,6 @@ async function fetchSkillsShCatalog(stack, language) {
|
|
|
89
89
|
function buildFallbackSkills(knowledge) {
|
|
90
90
|
const skills = [];
|
|
91
91
|
const stackLower = knowledge.stack.map((s) => s.toLowerCase());
|
|
92
|
-
const lang = knowledge.language.toLowerCase();
|
|
93
|
-
if (lang === technologyValue('technology-typescript-969545') || lang === 'javascript') {
|
|
94
|
-
skills.push({
|
|
95
|
-
name: 'typescript-patterns',
|
|
96
|
-
description: 'TypeScript best practices, strict mode patterns, and type safety conventions',
|
|
97
|
-
installCommand: 'npx skills add typescript-patterns',
|
|
98
|
-
justification: `Project uses ${knowledge.language}`,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
92
|
if (stackLower.some((s) => s.includes(technologyValue('technology-react-275976')))) {
|
|
102
93
|
skills.push({
|
|
103
94
|
name: 'react-patterns',
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { technologyValue } from './technology-registry.js';
|
|
2
1
|
// engine/skills-reconciler.ts — SPEC-189: Detect stack changes and reconcile skills
|
|
3
2
|
// SPEC-395: autoFixSkills — install missing, remove stale, repair corrupt paths.
|
|
4
3
|
// SPEC-607: Detect old-format skills and new agents, trigger regeneration.
|
|
@@ -32,7 +31,7 @@ export async function reconcileSkills(projectPath, knowledge) {
|
|
|
32
31
|
const newSkillsDetected = recommendedSkills
|
|
33
32
|
.filter((s) => !installedSet.has(s.name))
|
|
34
33
|
.map((s) => s.name);
|
|
35
|
-
const staleSkills = installedNames.filter((name) => !recommendedNames.has(name)
|
|
34
|
+
const staleSkills = installedNames.filter((name) => !recommendedNames.has(name));
|
|
36
35
|
const totalRelevant = recommendedNames.size;
|
|
37
36
|
const alreadyInSync = installedNames.filter((name) => recommendedNames.has(name)).length;
|
|
38
37
|
const healthScore = computeHealthScore(totalRelevant, alreadyInSync, staleSkills.length);
|
|
@@ -167,17 +166,6 @@ function computeHealthScore(totalRelevant, alreadyInSync, staleCount) {
|
|
|
167
166
|
const penalty = staleCount * 5;
|
|
168
167
|
return Math.max(0, base - penalty);
|
|
169
168
|
}
|
|
170
|
-
function isProtectedBuiltInSkill(name, knowledge) {
|
|
171
|
-
if (name !== 'typescript-patterns') {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
const language = knowledge.language.toLowerCase();
|
|
175
|
-
const stack = knowledge.stack.map((item) => item.toLowerCase());
|
|
176
|
-
return (language === technologyValue('technology-typescript-969545') ||
|
|
177
|
-
language === 'javascript' ||
|
|
178
|
-
stack.some((item) => item.includes(technologyValue('technology-typescript-969545')) ||
|
|
179
|
-
item.includes('javascript')));
|
|
180
|
-
}
|
|
181
169
|
/** Return a neutral reconciliation when prerequisites are missing. */
|
|
182
170
|
function buildEmptyReconciliation(_reason) {
|
|
183
171
|
return {
|
|
@@ -33,7 +33,7 @@ const CAPABILITY_SIGNALS = {
|
|
|
33
33
|
/\bbatch\s+upload\b/i,
|
|
34
34
|
],
|
|
35
35
|
scale: [
|
|
36
|
-
/\b(?:traffic|load|throughput|requests?\s+per\s+second|concurrent\s+users?)\b/i,
|
|
36
|
+
/\b(?:traffic|load\b(?![\s‐-―-]+bearing)|throughput|requests?\s+per\s+second|concurrent\s+users?)\b/i,
|
|
37
37
|
/\b(?:auto[ -]?scal|load\s+test|capacity\s+plan)/i,
|
|
38
38
|
/\b(?:data\s+growth|growing\s+dataset|query\s+performance\s+at\s+scale)\b/i,
|
|
39
39
|
],
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Analyzes a spec from adversarial perspectives: failure scenarios,
|
|
3
3
|
// concurrency issues, scale limits, security holes, and data consistency.
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { specStore, knowledgeStore } from '../storage/index.js';
|
|
6
7
|
// SPEC-1011 Bug F: fallback resolver using disk fingerprints
|
|
7
8
|
import { resolveProjectFromPath } from '../storage/project-resolver.js';
|
|
@@ -29,6 +30,14 @@ const ALL_FOCUS_AREAS = [
|
|
|
29
30
|
'security',
|
|
30
31
|
'data-consistency',
|
|
31
32
|
];
|
|
33
|
+
function truncateCriterionDiscriminator(criterion) {
|
|
34
|
+
const codePoints = Array.from(criterion);
|
|
35
|
+
if (codePoints.length <= 80) {
|
|
36
|
+
return criterion;
|
|
37
|
+
}
|
|
38
|
+
const digest = createHash('sha256').update(criterion).digest('hex').slice(0, 8);
|
|
39
|
+
return `${codePoints.slice(0, 79).join('')}… [${digest}]`;
|
|
40
|
+
}
|
|
32
41
|
function extractOutOfScopeSection(content) {
|
|
33
42
|
const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
34
43
|
const fencePattern = /^[ \t]{0,3}(`{3,}|~{3,})/;
|
|
@@ -224,7 +233,7 @@ export async function handleChallengeSpec(args, server) {
|
|
|
224
233
|
const contradictionResult = checkScopeContradictions(resolvedOutOfScope, criteriaTexts);
|
|
225
234
|
for (const c of contradictionResult.contradictions) {
|
|
226
235
|
failureScenarios.push({
|
|
227
|
-
scenario: `Criterion contradicts outOfScope declaration: "${c.contradicts}"`,
|
|
236
|
+
scenario: `Criterion contradicts outOfScope declaration: "${c.contradicts}" (criterion: "${truncateCriterionDiscriminator(c.criterion)}")`,
|
|
228
237
|
probability: 'medium',
|
|
229
238
|
impact: 'low',
|
|
230
239
|
currentHandling: `Criterion: "${c.criterion}"`,
|
|
@@ -314,7 +323,7 @@ export async function handleChallengeSpec(args, server) {
|
|
|
314
323
|
totalScenarios: failureScenarios.length,
|
|
315
324
|
actionableScenarios: failureScenariosScored.length,
|
|
316
325
|
suppressedLowRelevanceScenarios: suppressedScenarioCount + concurrencySuppressedCount,
|
|
317
|
-
shownByDefault:
|
|
326
|
+
shownByDefault: prioritizedBlocking.length,
|
|
318
327
|
mustAddressBeforeCoding: prioritizedSummary,
|
|
319
328
|
remainingAdvisories: advisoryIdentities,
|
|
320
329
|
gateStatus: gateState.gateStatus,
|
|
@@ -209,15 +209,6 @@ async function fetchSkillsShCatalog(stack, language) {
|
|
|
209
209
|
function buildFallbackSkills(knowledge) {
|
|
210
210
|
const skills = [];
|
|
211
211
|
const stackLower = knowledge.stack.map((s) => s.toLowerCase());
|
|
212
|
-
const lang = knowledge.language.toLowerCase();
|
|
213
|
-
if (lang === technologyValue('technology-typescript-969545') || lang === 'javascript') {
|
|
214
|
-
skills.push({
|
|
215
|
-
name: 'typescript-patterns',
|
|
216
|
-
description: 'TypeScript best practices, strict mode patterns, and type safety conventions',
|
|
217
|
-
installCommand: 'npx skills add typescript-patterns',
|
|
218
|
-
justification: `Project uses ${knowledge.language}`,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
212
|
if (stackLower.some((s) => s.includes(technologyValue('technology-react-275976')))) {
|
|
222
213
|
skills.push({
|
|
223
214
|
name: 'react-patterns',
|
|
@@ -634,14 +634,20 @@ export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
|
|
|
634
634
|
fixHint: 'Resolve the spec review blockers and have planu-spec-reviewer write updated evidence, then retry approval.',
|
|
635
635
|
});
|
|
636
636
|
}
|
|
637
|
-
|
|
638
|
-
|
|
637
|
+
const specReviewerBlockers = [];
|
|
638
|
+
if (review.reviewer.kind !== 'spec-review-agent') {
|
|
639
|
+
specReviewerBlockers.push(`reviewer.kind was ${review.reviewer.kind}. Expected spec-review-agent.`);
|
|
640
|
+
}
|
|
641
|
+
if (review.reviewer.agent !== 'planu-spec-reviewer') {
|
|
642
|
+
specReviewerBlockers.push(`reviewer.agent was ${review.reviewer.agent}. Expected planu-spec-reviewer.`);
|
|
643
|
+
}
|
|
644
|
+
if (specReviewerBlockers.length > 0) {
|
|
639
645
|
return specReviewGateError({
|
|
640
646
|
specId,
|
|
641
647
|
error: 'spec_review_wrong_reviewer',
|
|
642
648
|
message: 'Approval requires a dedicated spec reviewer. The implementation reviewer cannot approve the spec plan.',
|
|
643
|
-
blockers:
|
|
644
|
-
fixHint: 'Have planu-spec-reviewer
|
|
649
|
+
blockers: specReviewerBlockers,
|
|
650
|
+
fixHint: 'Have planu-spec-reviewer write the review artifact with reviewer.kind spec-review-agent and reviewer.agent planu-spec-reviewer, then retry approval.',
|
|
645
651
|
});
|
|
646
652
|
}
|
|
647
653
|
return null;
|
|
@@ -698,16 +704,20 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
|
|
|
698
704
|
fixHint: 'Resolve the implementation review blockers and have planu-implementation-reviewer write updated evidence, then retry done.',
|
|
699
705
|
});
|
|
700
706
|
}
|
|
701
|
-
|
|
702
|
-
|
|
707
|
+
const implementationReviewerBlockers = [];
|
|
708
|
+
if (review.reviewer.kind !== 'implementation-review-agent') {
|
|
709
|
+
implementationReviewerBlockers.push(`reviewer.kind was ${review.reviewer.kind}. Expected implementation-review-agent.`);
|
|
710
|
+
}
|
|
711
|
+
if (review.reviewer.agent !== 'planu-implementation-reviewer') {
|
|
712
|
+
implementationReviewerBlockers.push(`reviewer.agent was ${review.reviewer.agent}. Expected planu-implementation-reviewer.`);
|
|
713
|
+
}
|
|
714
|
+
if (implementationReviewerBlockers.length > 0) {
|
|
703
715
|
return implementationReviewGateError({
|
|
704
716
|
specId,
|
|
705
717
|
error: 'implementation_review_wrong_reviewer',
|
|
706
718
|
message: 'Done requires the recognized independent implementation reviewer identity.',
|
|
707
|
-
blockers:
|
|
708
|
-
|
|
709
|
-
],
|
|
710
|
-
fixHint: 'Have planu-implementation-reviewer (with the recognized identity) write the review artifact, then retry done.',
|
|
719
|
+
blockers: implementationReviewerBlockers,
|
|
720
|
+
fixHint: 'Have planu-implementation-reviewer write the review artifact with reviewer.kind implementation-review-agent and reviewer.agent planu-implementation-reviewer, then retry done.',
|
|
711
721
|
});
|
|
712
722
|
}
|
|
713
723
|
let implementerActor;
|
|
@@ -250,13 +250,45 @@ function isLeanSpecContent(content) {
|
|
|
250
250
|
* SPEC-630: Remove done criteria entries from lean spec and annotate the count on the criteria key.
|
|
251
251
|
* Only call this after markAllCriteriaDone — assumes all completed criteria have `done: true`.
|
|
252
252
|
*/
|
|
253
|
+
function indentWidth(line) {
|
|
254
|
+
return /^[ \t]*/.exec(line)?.[0].length ?? 0;
|
|
255
|
+
}
|
|
256
|
+
function isNestedUnder(line, itemIndent) {
|
|
257
|
+
return line.trim().length === 0 || indentWidth(line) > itemIndent.length;
|
|
258
|
+
}
|
|
259
|
+
function isDoneTrueField(line, fieldIndent) {
|
|
260
|
+
return new RegExp(`^${fieldIndent}done:[ \\t]*true[ \\t]*(?:#.*)?$`).test(line);
|
|
261
|
+
}
|
|
253
262
|
function stripDoneCriteria(content) {
|
|
263
|
+
const lines = content.split('\n');
|
|
264
|
+
const kept = [];
|
|
254
265
|
let doneCount = 0;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
266
|
+
let index = 0;
|
|
267
|
+
while (index < lines.length) {
|
|
268
|
+
const line = lines[index] ?? '';
|
|
269
|
+
const itemIndent = /^([ \t]+)- text:/.exec(line)?.[1];
|
|
270
|
+
if (itemIndent === undefined) {
|
|
271
|
+
kept.push(line);
|
|
272
|
+
index += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
let end = index + 1;
|
|
276
|
+
while (end < lines.length && isNestedUnder(lines[end] ?? '', itemIndent)) {
|
|
277
|
+
end += 1;
|
|
278
|
+
}
|
|
279
|
+
const item = lines.slice(index + 1, end);
|
|
280
|
+
const firstField = item.find((entry) => entry.trim().length > 0);
|
|
281
|
+
const fieldIndent = firstField === undefined ? '' : (/^[ \t]*/.exec(firstField)?.[0] ?? '');
|
|
282
|
+
const isDone = firstField !== undefined && item.some((entry) => isDoneTrueField(entry, fieldIndent));
|
|
283
|
+
if (isDone) {
|
|
284
|
+
doneCount += 1;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
kept.push(line, ...item);
|
|
288
|
+
}
|
|
289
|
+
index = end;
|
|
290
|
+
}
|
|
291
|
+
return kept.join('\n').replace(/^(criteria:)(\s*#.*)?$/m, `$1 # ${String(doneCount)} done`);
|
|
260
292
|
}
|
|
261
293
|
export async function tryReconcile(newStatus, specId, projectId) {
|
|
262
294
|
if (newStatus !== 'done') {
|
package/package.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.3.
|
|
5
|
+
"version": "5.3.63",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|