@planu/cli 4.7.3 → 4.7.4
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 +9 -0
- package/README.md +6 -3
- package/dist/engine/cascade-hooks/core/append-releases.d.ts +2 -1
- package/dist/engine/cascade-hooks/core/append-releases.js +30 -3
- package/dist/engine/evidence-gates/artifact-reader.js +15 -2
- package/dist/engine/evidence-gates/lifecycle-gate.js +14 -2
- package/dist/engine/handoff-packager.js +6 -49
- package/dist/engine/readiness-checker.js +13 -7
- package/dist/engine/spec-format/acceptance-criteria.d.ts +5 -0
- package/dist/engine/spec-format/acceptance-criteria.js +106 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +16 -0
- package/dist/engine/universal-rules/rules/planu-release-policy.js +8 -5
- package/dist/native/lightweight-command-catalog.d.ts +1 -1
- package/dist/native/lightweight-command-catalog.js +1 -1
- package/dist/resources/process.js +3 -2
- package/dist/tools/challenge-spec/event-challenge-scenarios.js +10 -6
- package/dist/tools/git/hook-ops.js +1 -1
- package/dist/tools/init-project/per-client-files-writer.js +2 -1
- package/dist/tools/init-project/rules-generator.js +14 -0
- package/dist/tools/update-status/evidence-gate.js +5 -9
- package/dist/types/cascade-hooks.d.ts +5 -0
- package/dist/types/spec-format.d.ts +6 -0
- package/package.json +16 -16
- package/planu-native.json +2 -2
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**The high-performance SDD stack for AI coding agents.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@planu/cli)
|
|
6
|
-

|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
|
|
@@ -41,7 +41,7 @@ Add to your MCP client config (Claude Desktop: `~/Library/Application Support/Cl
|
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
**Requirements:** Node.js >=
|
|
44
|
+
**Requirements:** Node.js >= 24. The native Rust core is bundled and auto-detected for macOS (Intel/M1), Linux, and Windows.
|
|
45
45
|
|
|
46
46
|
---
|
|
47
47
|
|
|
@@ -112,7 +112,10 @@ data/ # Local storage (gitignored)
|
|
|
112
112
|
pnpm install # Install dependencies
|
|
113
113
|
pnpm build # Compile TS + Build Rust bridge
|
|
114
114
|
pnpm dev # Watch mode
|
|
115
|
-
pnpm
|
|
115
|
+
pnpm validate # Canonical local validation contract
|
|
116
|
+
pnpm check:strict # Extended local quality gates
|
|
117
|
+
pnpm test # Run the full test suite
|
|
118
|
+
pnpm release:local # Local-first release flow when shipping
|
|
116
119
|
```
|
|
117
120
|
|
|
118
121
|
---
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import type { CascadeContext, CoreActionResult } from '../../../types/cascade-hooks.js';
|
|
1
|
+
import type { CascadeContext, CoreActionResult, PendingReleaseEntry } from '../../../types/cascade-hooks.js';
|
|
2
|
+
export declare function normalizePendingReleaseEntries(value: unknown, now?: Date): PendingReleaseEntry[];
|
|
2
3
|
export declare function appendReleasesPending(ctx: CascadeContext): Promise<CoreActionResult>;
|
|
3
4
|
//# sourceMappingURL=append-releases.d.ts.map
|
|
@@ -5,6 +5,35 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
6
6
|
import { specStore } from '../../../storage/index.js';
|
|
7
7
|
const CORE_ACTION_BUDGET_MS = 2000;
|
|
8
|
+
const PENDING_ENTRY_MAX_AGE_DAYS = 30;
|
|
9
|
+
function isValidPendingReleaseEntry(value) {
|
|
10
|
+
return (typeof value === 'object' &&
|
|
11
|
+
value !== null &&
|
|
12
|
+
typeof value.specId === 'string' &&
|
|
13
|
+
typeof value.title === 'string' &&
|
|
14
|
+
typeof value.completedAt === 'string');
|
|
15
|
+
}
|
|
16
|
+
function isRecentPendingEntry(entry, now = new Date()) {
|
|
17
|
+
const completedAt = new Date(entry.completedAt);
|
|
18
|
+
if (Number.isNaN(completedAt.getTime())) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const ageMs = now.getTime() - completedAt.getTime();
|
|
22
|
+
return ageMs <= PENDING_ENTRY_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
|
|
23
|
+
}
|
|
24
|
+
export function normalizePendingReleaseEntries(value, now = new Date()) {
|
|
25
|
+
if (!Array.isArray(value)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const deduped = new Map();
|
|
29
|
+
for (const entry of value) {
|
|
30
|
+
if (!isValidPendingReleaseEntry(entry) || !isRecentPendingEntry(entry, now)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
deduped.set(entry.specId, entry);
|
|
34
|
+
}
|
|
35
|
+
return [...deduped.values()];
|
|
36
|
+
}
|
|
8
37
|
async function actuallyAppendReleasesPending(ctx, opts) {
|
|
9
38
|
const { projectPath, projectId, specId } = ctx;
|
|
10
39
|
if (!projectPath || ctx.newStatus !== 'done') {
|
|
@@ -26,9 +55,7 @@ async function actuallyAppendReleasesPending(ctx, opts) {
|
|
|
26
55
|
try {
|
|
27
56
|
const raw = await readFile(pendingPath, 'utf-8');
|
|
28
57
|
const parsed = JSON.parse(raw);
|
|
29
|
-
|
|
30
|
-
pendingList = parsed;
|
|
31
|
-
}
|
|
58
|
+
pendingList = normalizePendingReleaseEntries(parsed);
|
|
32
59
|
}
|
|
33
60
|
catch {
|
|
34
61
|
/* file doesn't exist yet — start fresh */
|
|
@@ -49,6 +49,19 @@ const ContractValidationSchema = z.object({
|
|
|
49
49
|
reportPath: z.string().optional(),
|
|
50
50
|
summary: z.string().optional(),
|
|
51
51
|
});
|
|
52
|
+
const ARTIFACT_HINTS = {
|
|
53
|
+
'Discovery evidence': 'Expected discovery.json with { version: 1, rules: string[], examples: [{ rule, example }], openQuestions: [{ question, status, resolution? }], outOfScope: string[], glossary: [{ term, meaning }] }.',
|
|
54
|
+
'Task plan evidence': 'Expected task-plan.json with { version: 1, tasks: [{ id, title, acceptanceCriteria: string[], status: "pending"|"doing"|"done" }] }. Acceptance criteria may be full text or stable IDs such as AC1 when they map to the canonical criteria order.',
|
|
55
|
+
'Traceability matrix evidence': 'Expected traceability-matrix.json with { version: 1, rows: [{ acceptanceCriterion, scenario?, testEvidence?, contractEvidence?, manualEvidence?, changedFiles: string[], validationEvidence?, reviewerEvidence? }] }.',
|
|
56
|
+
'contract-validation-api.json': 'Expected filename contract-validation-api.json with { version: 1, kind: "api", passed: boolean, reportPath?, summary? }.',
|
|
57
|
+
'contract-validation-graphql.json': 'Expected filename contract-validation-graphql.json with { version: 1, kind: "graphql", passed: boolean, reportPath?, summary? }.',
|
|
58
|
+
'contract-validation-event.json': 'Expected filename contract-validation-event.json with { version: 1, kind: "event", passed: boolean, reportPath?, summary? }.',
|
|
59
|
+
'contract-validation-ui.json': 'Expected filename contract-validation-ui.json with { version: 1, kind: "ui", passed: boolean, reportPath?, summary? }.',
|
|
60
|
+
'contract-validation-mcp.json': 'Expected filename contract-validation-mcp.json with { version: 1, kind: "mcp", passed: boolean, reportPath?, summary? }.',
|
|
61
|
+
};
|
|
62
|
+
function artifactHint(label) {
|
|
63
|
+
return ARTIFACT_HINTS[label] ?? '';
|
|
64
|
+
}
|
|
52
65
|
function handoffEvidencePath(projectId, specId, filename) {
|
|
53
66
|
return join(projectDataDir(projectId), 'handoffs', specId, filename);
|
|
54
67
|
}
|
|
@@ -78,13 +91,13 @@ async function readOptional(args) {
|
|
|
78
91
|
}
|
|
79
92
|
const parsed = args.schema.safeParse(found.value);
|
|
80
93
|
if (!parsed.success) {
|
|
81
|
-
args.invalidArtifacts.push(`${args.label} is invalid at ${found.path}: ${parsed.error.issues.map((issue) => issue.message).join('; ')}
|
|
94
|
+
args.invalidArtifacts.push(`${args.label} is invalid at ${found.path}: ${parsed.error.issues.map((issue) => issue.message).join('; ')} ${artifactHint(args.label)}`.trim());
|
|
82
95
|
return undefined;
|
|
83
96
|
}
|
|
84
97
|
return parsed.data;
|
|
85
98
|
}
|
|
86
99
|
catch (err) {
|
|
87
|
-
args.invalidArtifacts.push(`${args.label} is unreadable: ${err instanceof Error ? err.message : String(err)}
|
|
100
|
+
args.invalidArtifacts.push(`${args.label} is unreadable: ${err instanceof Error ? err.message : String(err)} ${artifactHint(args.label)}`.trim());
|
|
88
101
|
return undefined;
|
|
89
102
|
}
|
|
90
103
|
}
|
|
@@ -11,10 +11,22 @@ function isNonTrivial(spec) {
|
|
|
11
11
|
function normalizeCriterion(value) {
|
|
12
12
|
return value
|
|
13
13
|
.replace(/^-\s*\[[ x]\]\s*/i, '')
|
|
14
|
+
.replace(/^(AC\d+)\s*[:.)-]?\s*/i, '')
|
|
14
15
|
.replace(/\s+/g, ' ')
|
|
15
16
|
.trim()
|
|
16
17
|
.toLowerCase();
|
|
17
18
|
}
|
|
19
|
+
function criterionAliases(value, index) {
|
|
20
|
+
const aliases = new Set([
|
|
21
|
+
normalizeCriterion(value),
|
|
22
|
+
normalizeCriterion(`AC${String(index + 1)}`),
|
|
23
|
+
]);
|
|
24
|
+
const explicit = /^(AC\d+)\b/i.exec(value)?.[1];
|
|
25
|
+
if (explicit) {
|
|
26
|
+
aliases.add(normalizeCriterion(explicit));
|
|
27
|
+
}
|
|
28
|
+
return [...aliases].filter((alias) => alias.length > 0);
|
|
29
|
+
}
|
|
18
30
|
function hasText(value) {
|
|
19
31
|
return value !== undefined && value.trim().length > 0;
|
|
20
32
|
}
|
|
@@ -62,7 +74,7 @@ export function checkTaskPlanGate(spec, criteria, artifacts) {
|
|
|
62
74
|
];
|
|
63
75
|
}
|
|
64
76
|
const covered = new Set(artifacts.taskPlan.tasks.flatMap((task) => task.acceptanceCriteria.map(normalizeCriterion)));
|
|
65
|
-
const uncovered = criteria.filter((criterion) => !covered.has(
|
|
77
|
+
const uncovered = criteria.filter((criterion, index) => !criterionAliases(criterion, index).some((alias) => covered.has(alias)));
|
|
66
78
|
if (uncovered.length === 0) {
|
|
67
79
|
return [];
|
|
68
80
|
}
|
|
@@ -90,7 +102,7 @@ export function checkDoneEvidenceGate(spec, criteria, artifacts) {
|
|
|
90
102
|
}
|
|
91
103
|
else {
|
|
92
104
|
const covered = new Set(matrix.rows.map((row) => normalizeCriterion(row.acceptanceCriterion)));
|
|
93
|
-
const uncovered = criteria.filter((criterion) => !covered.has(
|
|
105
|
+
const uncovered = criteria.filter((criterion, index) => !criterionAliases(criterion, index).some((alias) => covered.has(alias)));
|
|
94
106
|
if (uncovered.length > 0) {
|
|
95
107
|
issues.push({
|
|
96
108
|
code: 'traceability_uncovered_criteria',
|
|
@@ -4,19 +4,8 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { stripFrontmatter } from './frontmatter-parser.js';
|
|
6
6
|
import { hashProjectPath, projectDataDir } from '../storage/base-store.js';
|
|
7
|
+
import { extractAcceptanceCriteriaTexts } from './spec-format/acceptance-criteria.js';
|
|
7
8
|
// ── Parsing helpers ──────────────────────────────────────────────────────────
|
|
8
|
-
async function safeReadFile(path) {
|
|
9
|
-
if (!path) {
|
|
10
|
-
return '';
|
|
11
|
-
}
|
|
12
|
-
try {
|
|
13
|
-
const raw = await readFile(path, 'utf-8');
|
|
14
|
-
return stripFrontmatter(raw);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return '';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
9
|
function extractObjective(huContent, spec) {
|
|
21
10
|
if (!huContent) {
|
|
22
11
|
return spec.title;
|
|
@@ -40,41 +29,6 @@ function extractObjective(huContent, spec) {
|
|
|
40
29
|
}
|
|
41
30
|
return spec.title;
|
|
42
31
|
}
|
|
43
|
-
function extractCriteria(huContent) {
|
|
44
|
-
if (!huContent) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
const bddBlocks = extractBddCriteria(huContent);
|
|
48
|
-
if (bddBlocks.length > 0) {
|
|
49
|
-
return bddBlocks;
|
|
50
|
-
}
|
|
51
|
-
return huContent
|
|
52
|
-
.split('\n')
|
|
53
|
-
.filter((line) => /^- \[[ x]\]/.test(line.trim()))
|
|
54
|
-
.map((line) => line.trim().replace(/^- \[[ x]\]\s*/, ''));
|
|
55
|
-
}
|
|
56
|
-
function extractBddCriteria(content) {
|
|
57
|
-
const criteria = [];
|
|
58
|
-
let current = [];
|
|
59
|
-
for (const rawLine of content.split('\n')) {
|
|
60
|
-
const trimmed = rawLine.trim().replace(/^-+\s*/, '');
|
|
61
|
-
if (/^(GIVEN|WHEN|THEN|AND)\b/i.test(trimmed)) {
|
|
62
|
-
if (/^GIVEN\b/i.test(trimmed) && current.length > 0) {
|
|
63
|
-
criteria.push(current.join(' / '));
|
|
64
|
-
current = [];
|
|
65
|
-
}
|
|
66
|
-
current.push(trimmed);
|
|
67
|
-
}
|
|
68
|
-
else if (current.length > 0 && trimmed === '') {
|
|
69
|
-
criteria.push(current.join(' / '));
|
|
70
|
-
current = [];
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (current.length > 0) {
|
|
74
|
-
criteria.push(current.join(' / '));
|
|
75
|
-
}
|
|
76
|
-
return criteria.filter((criterion) => /GIVEN\b/i.test(criterion) && /WHEN\b/i.test(criterion) && /THEN\b/i.test(criterion));
|
|
77
|
-
}
|
|
78
32
|
function extractFilesFromFicha(fichaContent) {
|
|
79
33
|
if (!fichaContent) {
|
|
80
34
|
return { toModify: [], toCreate: [] };
|
|
@@ -203,13 +157,16 @@ function buildConstraints(knowledge) {
|
|
|
203
157
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
204
158
|
export async function packageHandoff(spec, knowledge) {
|
|
205
159
|
const warnings = [];
|
|
206
|
-
const
|
|
160
|
+
const rawSpecContent = spec.specPath
|
|
161
|
+
? await readFile(spec.specPath, 'utf-8').catch(() => '')
|
|
162
|
+
: '';
|
|
163
|
+
const huContent = rawSpecContent ? stripFrontmatter(rawSpecContent) : '';
|
|
207
164
|
const fichaContent = huContent;
|
|
208
165
|
if (!huContent) {
|
|
209
166
|
warnings.push('spec.md not found — using minimal package. Run create_spec to generate it.');
|
|
210
167
|
}
|
|
211
168
|
const objective = extractObjective(huContent, spec);
|
|
212
|
-
const criteria =
|
|
169
|
+
const criteria = extractAcceptanceCriteriaTexts(rawSpecContent);
|
|
213
170
|
const parsedFiles = extractFilesFromFicha(fichaContent);
|
|
214
171
|
const discoveredFiles = extractBacktickedFiles(`${huContent}\n${fichaContent}`);
|
|
215
172
|
const toModify = parsedFiles.toModify.length > 0 ? parsedFiles.toModify : discoveredFiles;
|
|
@@ -6,6 +6,7 @@ import { loadReadinessConfig } from './readiness-config-loader.js';
|
|
|
6
6
|
import { runEarsGate } from './ears-gate.js';
|
|
7
7
|
import { findScenariosWithoutTests, parseFrontmatterScenarios, } from './validator/spec-compliance-runner.js';
|
|
8
8
|
import { evaluateImplementationContract } from './implementation-contract/index.js';
|
|
9
|
+
import { extractNormalizedAcceptanceCriteria } from './spec-format/acceptance-criteria.js';
|
|
9
10
|
// ── SPEC-784: Technical section quality constants ─────────────────────────────
|
|
10
11
|
const TECHNICAL_MIN_CHARS = 500;
|
|
11
12
|
// Detects "See technical.md", "See spec.md", or "See `<file>` technical.md" patterns
|
|
@@ -344,7 +345,8 @@ function checkSpecificityGate(spec, criteriaLines, fichaContent) {
|
|
|
344
345
|
*/
|
|
345
346
|
export function checkReadinessInternal(body) {
|
|
346
347
|
const bodyContent = stripFrontmatter(body);
|
|
347
|
-
const
|
|
348
|
+
const normalizedCriteria = extractNormalizedAcceptanceCriteria(body);
|
|
349
|
+
const criteriaLines = normalizedCriteria.map((criterion) => criterion.text);
|
|
348
350
|
const scenarioCount = criteriaLines.length === 0 ? countFrontmatterScenarios(body) : 0;
|
|
349
351
|
const criteriaCount = criteriaLines.length > 0 ? criteriaLines.length : scenarioCount;
|
|
350
352
|
const config = loadReadinessConfig();
|
|
@@ -402,19 +404,23 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
|
|
|
402
404
|
const huRaw = await readHuRaw(spec);
|
|
403
405
|
const huContent = stripFrontmatter(huRaw);
|
|
404
406
|
const fichaContent = await readFichaContent(spec);
|
|
405
|
-
const
|
|
407
|
+
const normalizedCriteria = extractNormalizedAcceptanceCriteria(huRaw);
|
|
408
|
+
const canonicalCriteriaLines = normalizedCriteria.map((criterion) => criterion.text);
|
|
409
|
+
const usesFrontmatterAsCanonical = normalizedCriteria.length > 0 &&
|
|
410
|
+
normalizedCriteria.every((criterion) => criterion.source === 'frontmatter');
|
|
406
411
|
// When a spec uses BDD format, acceptance criteria are stored as `scenarios:` in
|
|
407
412
|
// the YAML frontmatter (multi-line YAML that the simple KV parser cannot handle).
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
const
|
|
413
|
+
// When those scenarios are canonical, keep their linked tests in the readiness
|
|
414
|
+
// evidence model so specificity gates still see exact test paths and line refs.
|
|
415
|
+
const scenarioCriteria = usesFrontmatterAsCanonical ? frontmatterScenarioCriteria(huRaw) : [];
|
|
416
|
+
const effectiveCriteriaLines = scenarioCriteria.length > 0 ? scenarioCriteria : canonicalCriteriaLines;
|
|
411
417
|
const hu = scoreHuCompleteness(spec);
|
|
412
418
|
const criteria = scoreCriteria(effectiveCriteriaLines, vagueWords);
|
|
413
419
|
const files = scoreFilesIdentified(spec, fichaContent);
|
|
414
420
|
const deps = scoreDependencies(spec);
|
|
415
421
|
const totalScore = hu.points + criteria.points + files.points + deps.points;
|
|
416
422
|
// SPEC-631: EARS gate — hard-block for vague modal verbs ("should/may/could/might") in criteria
|
|
417
|
-
const earsBlockers = runEarsGate(
|
|
423
|
+
const earsBlockers = runEarsGate(effectiveCriteriaLines);
|
|
418
424
|
const allBlockers = [
|
|
419
425
|
...hu.blockers,
|
|
420
426
|
...criteria.blockers,
|
|
@@ -424,7 +430,7 @@ export async function checkSpecReadiness(spec, mode, projectHash) {
|
|
|
424
430
|
];
|
|
425
431
|
const allWarnings = [...hu.warnings, ...criteria.warnings, ...files.warnings, ...deps.warnings];
|
|
426
432
|
if (mode === 'strict' && spec.scope !== 'trivial' && spec.difficulty >= 3) {
|
|
427
|
-
for (const issue of evaluateImplementationContract(huContent,
|
|
433
|
+
for (const issue of evaluateImplementationContract(huContent, canonicalCriteriaLines)) {
|
|
428
434
|
allBlockers.push(`${issue.code}: ${issue.message}`);
|
|
429
435
|
}
|
|
430
436
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { NormalizedAcceptanceCriterion } from '../../types/spec-format.js';
|
|
2
|
+
export declare function extractNormalizedAcceptanceCriteria(raw: string): NormalizedAcceptanceCriterion[];
|
|
3
|
+
export declare function extractAcceptanceCriteriaTexts(raw: string): string[];
|
|
4
|
+
export declare function normalizeAcceptanceCriterionToken(value: string): string;
|
|
5
|
+
//# sourceMappingURL=acceptance-criteria.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { parseFrontmatterScenarios } from '../validator/spec-compliance-runner.js';
|
|
2
|
+
import { stripFrontmatter } from '../frontmatter-parser.js';
|
|
3
|
+
import { extractListItems, extractSection } from '../validator/extractors.js';
|
|
4
|
+
function normalizeCriterionValue(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/^-\s*\[[ x]\]\s*/i, '')
|
|
7
|
+
.replace(/[`*_()[\]:]/g, ' ')
|
|
8
|
+
.replace(/\s+/g, ' ')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function stripCriterionIdPrefix(value) {
|
|
13
|
+
return value.replace(/^(AC\d+)\s*[:.)-]?\s*/i, '').trim();
|
|
14
|
+
}
|
|
15
|
+
function detectCriterionId(value, index) {
|
|
16
|
+
const explicit = /^(AC\d+)\b/i.exec(value)?.[1];
|
|
17
|
+
return (explicit ?? `AC${String(index + 1)}`).toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
function extractSectionCriteria(section) {
|
|
20
|
+
const listItems = extractListItems(section);
|
|
21
|
+
if (listItems.length > 0) {
|
|
22
|
+
return listItems;
|
|
23
|
+
}
|
|
24
|
+
const lines = section.split('\n').map((line) => line.trim());
|
|
25
|
+
if (lines.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const bddBlocks = [];
|
|
29
|
+
let current = [];
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (line.length === 0) {
|
|
32
|
+
if (current.length > 0) {
|
|
33
|
+
bddBlocks.push(current.join(' '));
|
|
34
|
+
current = [];
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (/^(GIVEN|WHEN|THEN|AND|DADO|CUANDO|ENTONCES|Y)\b/i.test(line)) {
|
|
39
|
+
if (/^(GIVEN|DADO)\b/i.test(line) && current.length > 0) {
|
|
40
|
+
bddBlocks.push(current.join(' '));
|
|
41
|
+
current = [];
|
|
42
|
+
}
|
|
43
|
+
current.push(line.replace(/^[-*+]\s*/, ''));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (current.length > 0) {
|
|
47
|
+
current.push(line);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (current.length > 0) {
|
|
51
|
+
bddBlocks.push(current.join(' '));
|
|
52
|
+
}
|
|
53
|
+
return bddBlocks;
|
|
54
|
+
}
|
|
55
|
+
function extractBodyCriteria(raw) {
|
|
56
|
+
const body = stripFrontmatter(raw);
|
|
57
|
+
const acceptanceCriteria = extractSection(body, 'acceptance criteria', 'criterios de aceptaci');
|
|
58
|
+
if (acceptanceCriteria) {
|
|
59
|
+
const sectionCriteria = extractSectionCriteria(acceptanceCriteria);
|
|
60
|
+
if (sectionCriteria.length > 0) {
|
|
61
|
+
return sectionCriteria;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return body
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter((line) => /^-\s*\[[ x]\]/i.test(line) || /^- .*\bgiven\b.*\bwhen\b.*\bthen\b/i.test(line))
|
|
68
|
+
.map((line) => line.replace(/^-\s*\[[ x]\]\s*/i, '').trim());
|
|
69
|
+
}
|
|
70
|
+
function buildCriterion(text, source, index) {
|
|
71
|
+
const trimmed = text.trim();
|
|
72
|
+
if (trimmed.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const id = detectCriterionId(trimmed, index);
|
|
76
|
+
const withoutId = stripCriterionIdPrefix(trimmed);
|
|
77
|
+
const aliases = new Set([
|
|
78
|
+
normalizeCriterionValue(trimmed),
|
|
79
|
+
normalizeCriterionValue(withoutId),
|
|
80
|
+
normalizeCriterionValue(id),
|
|
81
|
+
]);
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
text: withoutId,
|
|
85
|
+
source,
|
|
86
|
+
aliases: [...aliases].filter((alias) => alias.length > 0),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function extractNormalizedAcceptanceCriteria(raw) {
|
|
90
|
+
const bodyCriteria = extractBodyCriteria(raw)
|
|
91
|
+
.map((criterion, index) => buildCriterion(criterion, 'body', index))
|
|
92
|
+
.filter((criterion) => criterion !== null);
|
|
93
|
+
if (bodyCriteria.length > 0) {
|
|
94
|
+
return bodyCriteria;
|
|
95
|
+
}
|
|
96
|
+
return parseFrontmatterScenarios(raw)
|
|
97
|
+
.map((scenario, index) => buildCriterion(scenario.title, 'frontmatter', index))
|
|
98
|
+
.filter((criterion) => criterion !== null);
|
|
99
|
+
}
|
|
100
|
+
export function extractAcceptanceCriteriaTexts(raw) {
|
|
101
|
+
return extractNormalizedAcceptanceCriteria(raw).map((criterion) => criterion.text);
|
|
102
|
+
}
|
|
103
|
+
export function normalizeAcceptanceCriterionToken(value) {
|
|
104
|
+
return normalizeCriterionValue(stripCriterionIdPrefix(value));
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=acceptance-criteria.js.map
|
|
@@ -133,6 +133,13 @@ async function walkSpecDirectory(projectPath, specDir, result) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
async function shouldDeleteOrphanSpecDir(specDir) {
|
|
137
|
+
const entries = await readdir(specDir).catch(() => []);
|
|
138
|
+
if (entries.includes('spec.md')) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return !entries.some((entry) => mustMergeBeforeDeleteSpecFile(entry));
|
|
142
|
+
}
|
|
136
143
|
export async function runStrictPlanuCleanup(projectPath) {
|
|
137
144
|
const result = { deleted: [], merged: [], gitignoreUpdated: false };
|
|
138
145
|
const planuDir = join(projectPath, 'planu');
|
|
@@ -170,6 +177,11 @@ export async function runStrictPlanuCleanup(projectPath) {
|
|
|
170
177
|
result.deleted.push(relative(projectPath, full));
|
|
171
178
|
continue;
|
|
172
179
|
}
|
|
180
|
+
if (await shouldDeleteOrphanSpecDir(full)) {
|
|
181
|
+
await removePath(projectPath, full);
|
|
182
|
+
result.deleted.push(relative(projectPath, full));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
173
185
|
await walkSpecDirectory(projectPath, full, result);
|
|
174
186
|
}
|
|
175
187
|
result.gitignoreUpdated = await updateGitignore(projectPath);
|
|
@@ -210,6 +222,10 @@ export async function validateStrictPlanuLayout(projectPath, options = {}) {
|
|
|
210
222
|
offenders.push(relative(projectPath, full));
|
|
211
223
|
continue;
|
|
212
224
|
}
|
|
225
|
+
if (await shouldDeleteOrphanSpecDir(full)) {
|
|
226
|
+
offenders.push(relative(projectPath, full));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
213
229
|
for (const entry of await readdir(full).catch(() => [])) {
|
|
214
230
|
const entryPath = join(full, entry);
|
|
215
231
|
const isDir = await pathIsDirectory(entryPath);
|
|
@@ -6,23 +6,26 @@ Auto-generated by \`init_project\`. Do not edit manually.
|
|
|
6
6
|
|
|
7
7
|
## Rule
|
|
8
8
|
|
|
9
|
-
When implementation changes are complete and validated, always address release explicitly:
|
|
9
|
+
When implementation changes are complete and validated, always address release explicitly with a local-first flow:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
11
|
+
- canonical local gates: \`pnpm validate\` then \`pnpm check:strict\`
|
|
12
|
+
- canonical local release command: \`pnpm release:local\`
|
|
13
|
+
- publish externally only when the user has explicitly requested shipping/publishing
|
|
14
|
+
- use \`main\` as the default release branch; treat \`develop\` as optional gitflow compatibility only when the repository actively uses it
|
|
13
15
|
|
|
14
16
|
## Required Release Checklist
|
|
15
17
|
|
|
16
|
-
-
|
|
18
|
+
- local validation commands passed
|
|
17
19
|
- changelog or release notes updated when applicable
|
|
18
20
|
- package version and git tag stay aligned
|
|
19
|
-
-
|
|
21
|
+
- release metadata under \`planu/releases/pending.json\` contains only genuinely pending entries
|
|
20
22
|
- published package smoke check is run after release when applicable
|
|
21
23
|
|
|
22
24
|
## Hard Blocks
|
|
23
25
|
|
|
24
26
|
- Do not silently skip release discussion after completed product/runtime changes.
|
|
25
27
|
- Do not publish without validation evidence.
|
|
28
|
+
- Do not let hosted CI, branch protection, or release bots redefine the canonical local release contract.
|
|
26
29
|
- Do not leave release notes promising capabilities that are not in the public runtime.
|
|
27
30
|
`;
|
|
28
31
|
}
|
|
@@ -47,7 +47,7 @@ export declare const LIGHTWEIGHT_COMMANDS: readonly [{
|
|
|
47
47
|
}, {
|
|
48
48
|
readonly id: "planu.release.check";
|
|
49
49
|
readonly title: "Check release readiness";
|
|
50
|
-
readonly description: "Check local branch cleanliness and
|
|
50
|
+
readonly description: "Check local-first release readiness, branch cleanliness, and optional gitflow drift.";
|
|
51
51
|
readonly invocation: "planu release check";
|
|
52
52
|
readonly hosts: readonly ["codex", "claude-code"];
|
|
53
53
|
readonly requiresMcp: false;
|
|
@@ -54,7 +54,7 @@ export const LIGHTWEIGHT_COMMANDS = [
|
|
|
54
54
|
{
|
|
55
55
|
id: 'planu.release.check',
|
|
56
56
|
title: 'Check release readiness',
|
|
57
|
-
description: 'Check local branch cleanliness and
|
|
57
|
+
description: 'Check local-first release readiness, branch cleanliness, and optional gitflow drift.',
|
|
58
58
|
invocation: 'planu release check',
|
|
59
59
|
hosts: ['codex', 'claude-code'],
|
|
60
60
|
requiresMcp: false,
|
|
@@ -93,8 +93,9 @@ function getSddProcess() {
|
|
|
93
93
|
id: 'documenting',
|
|
94
94
|
name: '8. Document & Ship',
|
|
95
95
|
tools: ['generate_docs', 'generate_docs_site', 'integrate_pm', 'learn_pattern'],
|
|
96
|
-
description: 'Generate documentation
|
|
97
|
-
'
|
|
96
|
+
description: 'Generate documentation, run the local-first release contract ' +
|
|
97
|
+
'(`pnpm validate`, `pnpm check:strict`, `pnpm release:local` when shipping), ' +
|
|
98
|
+
'sync with project management tools, and learn patterns for future estimations.',
|
|
98
99
|
},
|
|
99
100
|
],
|
|
100
101
|
specLifecycle: ['draft', 'review', 'approved', 'implementing', 'done'],
|
|
@@ -6,12 +6,8 @@ import { detectBrokers, getBrokerConfig, requiresIdempotence, } from '../../engi
|
|
|
6
6
|
* Returns true when the spec or project has event-driven characteristics.
|
|
7
7
|
*/
|
|
8
8
|
export function isEventDrivenSpec(spec, specContent, knowledge) {
|
|
9
|
-
const brokers = detectBrokers(knowledge.stack);
|
|
10
|
-
if (brokers.length > 0 && brokers[0] !== 'unknown') {
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
9
|
const lower = `${spec.title} ${spec.tags.join(' ')} ${specContent}`.toLowerCase();
|
|
14
|
-
|
|
10
|
+
const specSignals = lower.includes('event') ||
|
|
15
11
|
lower.includes('kafka') ||
|
|
16
12
|
lower.includes('rabbitmq') ||
|
|
17
13
|
lower.includes('pubsub') ||
|
|
@@ -21,7 +17,15 @@ export function isEventDrivenSpec(spec, specContent, knowledge) {
|
|
|
21
17
|
lower.includes('sns') ||
|
|
22
18
|
lower.includes('message') ||
|
|
23
19
|
lower.includes('consumer') ||
|
|
24
|
-
lower.includes('producer')
|
|
20
|
+
lower.includes('producer');
|
|
21
|
+
if (spec.target === 'frontend' && !specSignals) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const brokers = detectBrokers(knowledge.stack);
|
|
25
|
+
if (specSignals) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return brokers.length > 0 && brokers[0] !== 'unknown' && spec.target !== 'frontend';
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Generate event-driven failure scenarios for challenge_spec.
|
|
@@ -265,7 +265,7 @@ export async function handleSetupHooks(projectId, config) {
|
|
|
265
265
|
/* v8 ignore start */
|
|
266
266
|
const protectedBranches = mergedConfig.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
|
|
267
267
|
const stalenessThreshold = mergedConfig.stalenessThreshold ?? 50;
|
|
268
|
-
const baseBranch = mergedConfig.baseBranch ?? '
|
|
268
|
+
const baseBranch = mergedConfig.baseBranch ?? 'main';
|
|
269
269
|
/* v8 ignore end */
|
|
270
270
|
const preCommitScript = buildPreCommitScript(
|
|
271
271
|
/* v8 ignore next */ mergedConfig.requireSpecInCommit ?? true, stalenessThreshold, baseBranch);
|
|
@@ -63,6 +63,7 @@ Planu enforces Spec Driven Development — spec first, then implement, then vali
|
|
|
63
63
|
|
|
64
64
|
- NEVER write production code before spec is \`approved\`
|
|
65
65
|
- NEVER skip \`validate\` after implementation
|
|
66
|
+
- Specs stay \`spec.md\`-only. Do not recreate standalone \`technical.md\` or \`progress.md\`.
|
|
66
67
|
`;
|
|
67
68
|
const CODEX_NATIVE_SKILL_CONTENT = `# planu-native — Planu Lightweight Native Surface
|
|
68
69
|
|
|
@@ -79,7 +80,7 @@ Use Planu's lightweight CLI surface for common SDD checks without loading the fu
|
|
|
79
80
|
- \`planu spec list\` — list specs
|
|
80
81
|
- \`planu spec validate SPEC-001\` — validate one spec
|
|
81
82
|
- \`planu audit debt\` — read-only technical debt audit
|
|
82
|
-
- \`planu release check\` — local release readiness check
|
|
83
|
+
- \`planu release check\` — local-first release readiness check
|
|
83
84
|
|
|
84
85
|
## Escalate to Full MCP
|
|
85
86
|
|
|
@@ -20,6 +20,19 @@ Auto-generated by \`init_project\`. Do not edit manually.
|
|
|
20
20
|
|
|
21
21
|
Flow: \`facilitate → create_spec → challenge_spec → check_readiness → approve → implement → validate → done\`
|
|
22
22
|
|
|
23
|
+
## Evidence Gates
|
|
24
|
+
|
|
25
|
+
For non-trivial specs, Planu blocks lifecycle transitions unless evidence is present:
|
|
26
|
+
|
|
27
|
+
| Transition | Required evidence |
|
|
28
|
+
|------------|-------------------|
|
|
29
|
+
| \`approved\` | Discovery evidence: rules, examples, open questions, out-of-scope, glossary |
|
|
30
|
+
| \`implementing\` | Task plan mapped to acceptance criteria |
|
|
31
|
+
| \`done\` | Traceability matrix covering every acceptance criterion |
|
|
32
|
+
| \`done\` for API/UI/event/MCP work | Passing contract validation evidence |
|
|
33
|
+
|
|
34
|
+
Evidence lives in the external Planu handoff store, not under \`planu/specs/<spec>/\`. Spec folders remain \`spec.md\`-only.
|
|
35
|
+
|
|
23
36
|
## When to use \`facilitate\`
|
|
24
37
|
|
|
25
38
|
Any new feature, bug fix, refactor, integration, or task > 5 min.
|
|
@@ -51,6 +64,7 @@ Any new feature, bug fix, refactor, integration, or task > 5 min.
|
|
|
51
64
|
- NEVER write code before spec is \`approved\`
|
|
52
65
|
- NEVER skip \`validate\` after implementation
|
|
53
66
|
- NEVER create design docs outside \`planu/specs/\`
|
|
67
|
+
- NEVER recreate legacy \`technical.md\`, \`progress.md\`, or \`HU.md\` files for new specs
|
|
54
68
|
|
|
55
69
|
## Model Selection (MANDATORY)
|
|
56
70
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { isAbsolute, join } from 'node:path';
|
|
3
|
-
import { extractCriteria
|
|
4
|
-
import { stripFrontmatter } from '../../engine/frontmatter-parser.js';
|
|
3
|
+
import { extractCriteria } from '../../engine/validator/extractors.js';
|
|
5
4
|
import { readEvidenceArtifacts } from '../../engine/evidence-gates/artifact-reader.js';
|
|
6
5
|
import { checkLifecycleEvidenceGate } from '../../engine/evidence-gates/lifecycle-gate.js';
|
|
7
6
|
import { buildSpecEvidenceIndex } from '../../engine/evidence-index/index-builder.js';
|
|
8
7
|
import { checkDoneDriftContract } from '../../engine/evidence-index/done-drift.js';
|
|
9
8
|
import { parseTechnicalReferenceGroundingRecords } from '../../engine/spec-grounding/contract.js';
|
|
9
|
+
import { extractAcceptanceCriteriaTexts } from '../../engine/spec-format/acceptance-criteria.js';
|
|
10
10
|
/** SPEC-1054: BDD/SDD lifecycle evidence gate. */
|
|
11
11
|
export async function checkLifecycleEvidenceTransitionGate(args) {
|
|
12
12
|
const [criteriaResult, artifacts] = await Promise.all([
|
|
@@ -54,13 +54,9 @@ async function extractLifecycleGateCriteriaAndBody(spec, projectPath) {
|
|
|
54
54
|
const specPath = isAbsolute(spec.specPath) || !projectPath ? spec.specPath : join(projectPath, spec.specPath);
|
|
55
55
|
try {
|
|
56
56
|
const raw = await readFile(specPath, 'utf-8');
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const items = extractListItems(acceptanceCriteria);
|
|
61
|
-
if (items.length > 0) {
|
|
62
|
-
return { criteria: [...new Set(items)], body: raw };
|
|
63
|
-
}
|
|
57
|
+
const criteria = extractAcceptanceCriteriaTexts(raw);
|
|
58
|
+
if (criteria.length > 0) {
|
|
59
|
+
return { criteria: [...new Set(criteria)], body: raw };
|
|
64
60
|
}
|
|
65
61
|
}
|
|
66
62
|
catch {
|
|
@@ -35,6 +35,11 @@ export type CoreActionResult = {
|
|
|
35
35
|
ok: false;
|
|
36
36
|
error: string;
|
|
37
37
|
};
|
|
38
|
+
export interface PendingReleaseEntry {
|
|
39
|
+
specId: string;
|
|
40
|
+
title: string;
|
|
41
|
+
completedAt: string;
|
|
42
|
+
}
|
|
38
43
|
/** SPEC-772: Result of a single fast hook execution. */
|
|
39
44
|
export interface FastHookResult {
|
|
40
45
|
hookId: string;
|
|
@@ -27,6 +27,12 @@ export interface BddScenario {
|
|
|
27
27
|
line?: number;
|
|
28
28
|
}[];
|
|
29
29
|
}
|
|
30
|
+
export interface NormalizedAcceptanceCriterion {
|
|
31
|
+
id: string;
|
|
32
|
+
text: string;
|
|
33
|
+
source: 'body' | 'frontmatter';
|
|
34
|
+
aliases: string[];
|
|
35
|
+
}
|
|
30
36
|
/** Input for lean spec generation. */
|
|
31
37
|
export interface LeanSpecInput {
|
|
32
38
|
spec: Spec;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.4",
|
|
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",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"packageName": "@planu/core"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@planu/core-darwin-arm64": "4.7.
|
|
38
|
-
"@planu/core-darwin-x64": "4.7.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.7.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.7.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.7.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.7.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.7.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.7.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.7.4",
|
|
38
|
+
"@planu/core-darwin-x64": "4.7.4",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.7.4",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.7.4",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.7.4",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.7.4",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.7.4",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.7.4"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -168,8 +168,8 @@
|
|
|
168
168
|
}
|
|
169
169
|
},
|
|
170
170
|
"devDependencies": {
|
|
171
|
-
"@commitlint/cli": "^21.0
|
|
172
|
-
"@commitlint/config-conventional": "^21.0
|
|
171
|
+
"@commitlint/cli": "^21.1.0",
|
|
172
|
+
"@commitlint/config-conventional": "^21.1.0",
|
|
173
173
|
"@eslint/js": "^10.0.1",
|
|
174
174
|
"@napi-rs/cli": "^3.7.2",
|
|
175
175
|
"@secretlint/secretlint-rule-no-homedir": "^13.0.2",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
"@stryker-mutator/core": "^9.6.1",
|
|
184
184
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
185
185
|
"@supabase/supabase-js": "^2.108.2",
|
|
186
|
-
"@types/node": "^
|
|
186
|
+
"@types/node": "^26.0.0",
|
|
187
187
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
188
188
|
"@vitest/coverage-v8": "^4.1.9",
|
|
189
189
|
"@vue/test-utils": "^2.4.11",
|
|
@@ -194,8 +194,8 @@
|
|
|
194
194
|
"happy-dom": "^20.10.6",
|
|
195
195
|
"husky": "^9.1.7",
|
|
196
196
|
"javascript-obfuscator": "^5.4.3",
|
|
197
|
-
"knip": "^6.
|
|
198
|
-
"lint-staged": "^17.0.
|
|
197
|
+
"knip": "^6.18.0",
|
|
198
|
+
"lint-staged": "^17.0.8",
|
|
199
199
|
"madge": "^8.0.0",
|
|
200
200
|
"prettier": "^3.8.4",
|
|
201
201
|
"secretlint": "^13.0.2",
|
|
@@ -203,8 +203,8 @@
|
|
|
203
203
|
"tsc-alias": "^1.8.17",
|
|
204
204
|
"type-coverage": "^2.29.7",
|
|
205
205
|
"typescript": "^6.0.3",
|
|
206
|
-
"typescript-eslint": "^8.
|
|
207
|
-
"vite": "^8.0
|
|
206
|
+
"typescript-eslint": "^8.62.0",
|
|
207
|
+
"vite": "^8.1.0",
|
|
208
208
|
"vitest": "^4.1.9",
|
|
209
209
|
"vue": "^3.5.38"
|
|
210
210
|
}
|
package/planu-native.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev.planu.native",
|
|
3
3
|
"displayName": "Planu Native Lightweight Surface",
|
|
4
|
-
"version": "4.7.
|
|
4
|
+
"version": "4.7.4",
|
|
5
5
|
"packageName": "@planu/cli",
|
|
6
6
|
"modes": {
|
|
7
7
|
"lightweight": {
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
{
|
|
81
81
|
"id": "planu.release.check",
|
|
82
82
|
"title": "Check release readiness",
|
|
83
|
-
"description": "Check local branch cleanliness and
|
|
83
|
+
"description": "Check local-first release readiness, branch cleanliness, and optional gitflow drift.",
|
|
84
84
|
"invocation": "planu release check",
|
|
85
85
|
"hosts": [
|
|
86
86
|
"codex",
|
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": "4.7.
|
|
5
|
+
"version": "4.7.4",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|