@planu/cli 3.9.1 → 3.9.2
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 +13 -1
- package/dist/engine/ai-cost-estimator/token-estimator.js +17 -6
- package/dist/engine/autopilot/crash-shield-rate-limiter.js +2 -2
- package/dist/engine/session-safeguard/learnings-buffer.js +13 -15
- package/dist/engine/spec-format/lean-spec-generator.js +5 -0
- package/dist/engine/spec-generator/fallback-generator.d.ts +5 -0
- package/dist/engine/spec-generator/fallback-generator.js +120 -0
- package/dist/engine/spec-generator/index.d.ts +2 -0
- package/dist/engine/spec-generator/index.js +2 -0
- package/dist/engine/spec-language/english-only.d.ts +15 -0
- package/dist/engine/spec-language/english-only.js +142 -0
- package/dist/engine/spec-migrator/unified-migration.js +14 -0
- package/dist/engine/status-reconciler/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/tools/create-spec.js +46 -1
- package/dist/tools/init-project/handler.js +6 -4
- package/dist/tools/list-specs.js +1 -24
- package/dist/tools/update-status/transition-guard.d.ts +6 -3
- package/dist/tools/update-status/transition-guard.js +75 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec/core.d.ts +6 -0
- package/dist/types/spec-generator.d.ts +26 -0
- package/dist/types/spec-generator.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [3.9.2] - 2026-05-15
|
|
2
|
+
|
|
3
|
+
**Tarball SHA-256:** `3768f401213d28afacbca964d7318d079e92d5f4d454b78705f9995c272034ac`
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
- fix(release): allow own native core packages in license audit
|
|
7
|
+
- fix(tests): stabilize release preflight checks
|
|
8
|
+
- fix(release): support lock fallback without flock
|
|
9
|
+
- fix(specs): enforce English structured spec generation
|
|
10
|
+
- fix: stabilize mcp slim test suite
|
|
11
|
+
|
|
12
|
+
|
|
1
13
|
## [3.9.0] — 2026-05-12 — Single official SDD MCP surface
|
|
2
14
|
|
|
3
15
|
### Changed — Planu now exposes one focused MCP surface
|
|
@@ -3721,4 +3733,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
3721
3733
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
3722
3734
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
3723
3735
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
3724
|
-
- 10,857 tests with ≥95% coverage
|
|
3736
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -62,19 +62,24 @@ export async function detectProjectLanguage(projectPath) {
|
|
|
62
62
|
// Codebase scanning
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
const MAX_SCAN_DEPTH = 5;
|
|
65
|
+
const MAX_SCANNED_SOURCE_FILES = 200;
|
|
65
66
|
const EXCLUDED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.next']);
|
|
66
67
|
/** Recursively counts total characters in source files up to a given depth. */
|
|
67
|
-
async function countCodebaseChars(dirPath, depth) {
|
|
68
|
+
async function countCodebaseChars(dirPath, depth, filesScanned = 0) {
|
|
68
69
|
if (depth > MAX_SCAN_DEPTH) {
|
|
69
|
-
return 0;
|
|
70
|
+
return { chars: 0, files: filesScanned };
|
|
71
|
+
}
|
|
72
|
+
if (filesScanned >= MAX_SCANNED_SOURCE_FILES) {
|
|
73
|
+
return { chars: 0, files: filesScanned };
|
|
70
74
|
}
|
|
71
75
|
let totalChars = 0;
|
|
76
|
+
let totalFiles = filesScanned;
|
|
72
77
|
let entries;
|
|
73
78
|
try {
|
|
74
79
|
entries = await readdir(dirPath, { withFileTypes: true, encoding: 'utf-8' });
|
|
75
80
|
}
|
|
76
81
|
catch {
|
|
77
|
-
return 0;
|
|
82
|
+
return { chars: 0, files: totalFiles };
|
|
78
83
|
}
|
|
79
84
|
for (const entry of entries) {
|
|
80
85
|
const entryName = entry.name;
|
|
@@ -83,19 +88,25 @@ async function countCodebaseChars(dirPath, depth) {
|
|
|
83
88
|
}
|
|
84
89
|
const fullPath = join(dirPath, entryName);
|
|
85
90
|
if (entry.isDirectory()) {
|
|
86
|
-
|
|
91
|
+
const nested = await countCodebaseChars(fullPath, depth + 1, totalFiles);
|
|
92
|
+
totalChars += nested.chars;
|
|
93
|
+
totalFiles = nested.files;
|
|
87
94
|
}
|
|
88
95
|
else if (entry.isFile() && isSourceFile(entryName)) {
|
|
96
|
+
if (totalFiles >= MAX_SCANNED_SOURCE_FILES) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
89
99
|
try {
|
|
90
100
|
const content = await readFile(fullPath, 'utf-8');
|
|
91
101
|
totalChars += content.length;
|
|
102
|
+
totalFiles++;
|
|
92
103
|
}
|
|
93
104
|
catch {
|
|
94
105
|
// Skip unreadable files
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
|
-
return totalChars;
|
|
109
|
+
return { chars: totalChars, files: totalFiles };
|
|
99
110
|
}
|
|
100
111
|
/** Returns true if the file extension is a known source file type. */
|
|
101
112
|
function isSourceFile(filename) {
|
|
@@ -120,7 +131,7 @@ export async function estimateInputTokens(specContent, projectPath, config) {
|
|
|
120
131
|
// 2. Codebase context tokens (optional)
|
|
121
132
|
let codebaseContextTokens = 0;
|
|
122
133
|
if (projectPath) {
|
|
123
|
-
const codebaseChars = await countCodebaseChars(projectPath, 0);
|
|
134
|
+
const { chars: codebaseChars } = await countCodebaseChars(projectPath, 0);
|
|
124
135
|
codebaseContextTokens = Math.ceil(codebaseChars / charsPerToken);
|
|
125
136
|
}
|
|
126
137
|
const inputTokens = specContentTokens + codebaseContextTokens + toolDefinitionsTokens;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// engine/autopilot/crash-shield-rate-limiter.ts — SPEC-628: Rate-limit crash scans to max once per 6h per project
|
|
2
2
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
|
-
import { hashProjectPath } from '../../storage/base-store.js';
|
|
4
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
5
5
|
const SIX_HOURS_MS = 6 * 60 * 60 * 1000;
|
|
6
6
|
function stateFilePath(projectPath) {
|
|
7
7
|
const projectId = hashProjectPath(projectPath);
|
|
8
|
-
return join(
|
|
8
|
+
return join(projectDataDir(projectId), 'crash-shield-state.json');
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* Returns { skip: true, reason } if a crash scan was recorded within maxAgeMs.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// engine/session-safeguard/learnings-buffer.ts — SPEC-585: Append/flush learnings buffer
|
|
2
|
-
import { open, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { open, readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { hashProjectPath } from '../../storage/base-store.js';
|
|
4
|
+
import { hashProjectPath, projectDataDir } from '../../storage/base-store.js';
|
|
5
5
|
const BUFFER_VERSION = 1;
|
|
6
6
|
const MEMORY_MD_PATH = '/Users/kevin/.claude/projects/-Users-kevin-Documents-desarrollo-sdd-mcp-server/memory/MEMORY.md';
|
|
7
|
-
const MAX_RETRIES =
|
|
7
|
+
const MAX_RETRIES = 50;
|
|
8
8
|
const JITTER_MS = 100;
|
|
9
9
|
function bufferPath(projectPath) {
|
|
10
10
|
const hash = hashProjectPath(projectPath);
|
|
11
|
-
return join(
|
|
11
|
+
return join(projectDataDir(hash), 'learnings-buffer.json');
|
|
12
12
|
}
|
|
13
13
|
function emptyBuffer() {
|
|
14
14
|
return { version: BUFFER_VERSION, entries: [] };
|
|
@@ -40,7 +40,6 @@ export async function appendToBuffer(projectPath, entry) {
|
|
|
40
40
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
41
41
|
try {
|
|
42
42
|
const fh = await open(lockFile, 'wx');
|
|
43
|
-
await fh.close();
|
|
44
43
|
try {
|
|
45
44
|
const buffer = await readBuffer(projectPath);
|
|
46
45
|
buffer.entries.push(entry);
|
|
@@ -48,7 +47,12 @@ export async function appendToBuffer(projectPath, entry) {
|
|
|
48
47
|
}
|
|
49
48
|
finally {
|
|
50
49
|
try {
|
|
51
|
-
|
|
50
|
+
await fh.close();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* best-effort */
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
52
56
|
await unlink(lockFile);
|
|
53
57
|
}
|
|
54
58
|
catch {
|
|
@@ -68,15 +72,9 @@ export async function appendToBuffer(projectPath, entry) {
|
|
|
68
72
|
await new Promise((resolve) => setTimeout(resolve, JITTER_MS + Math.random() * JITTER_MS));
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
buffer.entries.push(entry);
|
|
75
|
-
await writeBuffer(projectPath, buffer);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
throw lastError;
|
|
79
|
-
}
|
|
75
|
+
throw lastError instanceof Error
|
|
76
|
+
? lastError
|
|
77
|
+
: new Error('Unable to acquire learnings buffer lock');
|
|
80
78
|
}
|
|
81
79
|
/** Build the markdown bullet for an entry to append to MEMORY.md. */
|
|
82
80
|
function entryToMarkdown(entry) {
|
|
@@ -42,6 +42,11 @@ export function generateLeanSpecContent(input) {
|
|
|
42
42
|
` model: ${estimation.recommendedModel}`,
|
|
43
43
|
`model: ${model}`,
|
|
44
44
|
`budget: ${String(budget)}`,
|
|
45
|
+
...(spec.generatedWithModel ? [`generatedWithModel: ${spec.generatedWithModel}`] : []),
|
|
46
|
+
...(spec.generatedAt ? [`generatedAt: ${spec.generatedAt}`] : []),
|
|
47
|
+
...(spec.qualityWarnings && spec.qualityWarnings.length > 0
|
|
48
|
+
? [`qualityWarnings: ${JSON.stringify(spec.qualityWarnings)}`]
|
|
49
|
+
: []),
|
|
45
50
|
];
|
|
46
51
|
let acLines;
|
|
47
52
|
if (acFormat === 'bdd') {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SpecContentGenerator, SpecGenerationRequest, SpecGenerationResult } from '../../types/index.js';
|
|
2
|
+
export declare class FallbackGenerator implements SpecContentGenerator {
|
|
3
|
+
generate(request: SpecGenerationRequest): Promise<SpecGenerationResult>;
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=fallback-generator.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const MAX_SUMMARY_LENGTH = 700;
|
|
2
|
+
export class FallbackGenerator {
|
|
3
|
+
generate(request) {
|
|
4
|
+
const slug = slugify(request.title);
|
|
5
|
+
const pascalName = toPascalCase(request.title);
|
|
6
|
+
const sourceSummary = normalizeSummary(request.description, request.title);
|
|
7
|
+
const contextLine = buildContextLine(request);
|
|
8
|
+
const technicalSection = buildTechnicalSection(slug, pascalName, contextLine);
|
|
9
|
+
const specBody = [
|
|
10
|
+
'## Problem',
|
|
11
|
+
sourceSummary,
|
|
12
|
+
'',
|
|
13
|
+
'## Solution',
|
|
14
|
+
`Implement ${request.title} with explicit behavior, project-scoped file ownership, and verification gates. ${contextLine}`,
|
|
15
|
+
'',
|
|
16
|
+
'## Acceptance Criteria',
|
|
17
|
+
'',
|
|
18
|
+
`Scenario: ${request.title} happy path`,
|
|
19
|
+
'GIVEN the relevant user or system context exists',
|
|
20
|
+
`WHEN the requested ${request.title} behavior is executed`,
|
|
21
|
+
'THEN the expected outcome from the request is observable',
|
|
22
|
+
'',
|
|
23
|
+
`Scenario: ${request.title} failure handling`,
|
|
24
|
+
'GIVEN invalid input or an unavailable dependency',
|
|
25
|
+
`WHEN the requested ${request.title} behavior runs`,
|
|
26
|
+
'THEN the system returns a clear failure state without corrupting data',
|
|
27
|
+
'',
|
|
28
|
+
`Scenario: ${request.title} verification`,
|
|
29
|
+
'GIVEN the implementation is complete',
|
|
30
|
+
'WHEN the configured verification commands run',
|
|
31
|
+
'THEN tests and lint pass without regressions',
|
|
32
|
+
'',
|
|
33
|
+
technicalSection,
|
|
34
|
+
].join('\n');
|
|
35
|
+
return Promise.resolve({
|
|
36
|
+
specBody,
|
|
37
|
+
technicalSection,
|
|
38
|
+
generatedWithModel: 'deterministic-fallback',
|
|
39
|
+
generatedAt: new Date().toISOString(),
|
|
40
|
+
qualityWarnings: [
|
|
41
|
+
'Fallback generator used. Review candidate file paths and replace placeholders with exact code owners before approval.',
|
|
42
|
+
],
|
|
43
|
+
fallbackReason: 'No external model generator is configured for create_spec.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function normalizeSummary(description, title) {
|
|
48
|
+
const compact = description
|
|
49
|
+
.replace(/\r\n/g, '\n')
|
|
50
|
+
.split('\n')
|
|
51
|
+
.map((line) => line.trim())
|
|
52
|
+
.filter((line) => line.length > 0 && !line.startsWith('- [ ]'))
|
|
53
|
+
.join(' ')
|
|
54
|
+
.replace(/\s+/g, ' ')
|
|
55
|
+
.trim();
|
|
56
|
+
const summary = compact.length > 0
|
|
57
|
+
? compact.slice(0, MAX_SUMMARY_LENGTH)
|
|
58
|
+
: `The project needs ${title} defined with enough implementation detail to proceed safely.`;
|
|
59
|
+
return summary.endsWith('.') ? summary : `${summary}.`;
|
|
60
|
+
}
|
|
61
|
+
function buildContextLine(request) {
|
|
62
|
+
const parts = [
|
|
63
|
+
request.projectContext?.language,
|
|
64
|
+
request.projectContext?.framework,
|
|
65
|
+
request.projectContext?.architecture,
|
|
66
|
+
].filter((part) => part !== undefined && part.trim().length > 0);
|
|
67
|
+
if (parts.length === 0) {
|
|
68
|
+
return 'Use the existing project architecture and conventions.';
|
|
69
|
+
}
|
|
70
|
+
return `Use the existing ${parts.join(' / ')} project conventions.`;
|
|
71
|
+
}
|
|
72
|
+
function buildTechnicalSection(slug, pascalName, contextLine) {
|
|
73
|
+
return [
|
|
74
|
+
'## Technical',
|
|
75
|
+
'',
|
|
76
|
+
'### Implementation Notes',
|
|
77
|
+
`- ${contextLine}`,
|
|
78
|
+
'- Replace candidate paths with exact files before moving the spec to approved.',
|
|
79
|
+
'',
|
|
80
|
+
'## Files',
|
|
81
|
+
'',
|
|
82
|
+
'### Create',
|
|
83
|
+
`- src/${slug}.ts (candidate path; replace with exact owner during planning)`,
|
|
84
|
+
'### Modify',
|
|
85
|
+
'- src/index.ts (candidate integration point; replace with exact owner during planning)',
|
|
86
|
+
'### Test',
|
|
87
|
+
`- tests/${slug}.test.ts (focused coverage for the behavior and failure path)`,
|
|
88
|
+
'',
|
|
89
|
+
'## Type Signatures',
|
|
90
|
+
'```typescript',
|
|
91
|
+
`interface ${pascalName}Input {`,
|
|
92
|
+
' readonly requestId?: string;',
|
|
93
|
+
'}',
|
|
94
|
+
'',
|
|
95
|
+
`interface ${pascalName}Result {`,
|
|
96
|
+
' readonly ok: boolean;',
|
|
97
|
+
' readonly reason?: string;',
|
|
98
|
+
'}',
|
|
99
|
+
'```',
|
|
100
|
+
'',
|
|
101
|
+
'## Verification',
|
|
102
|
+
'- pnpm typecheck',
|
|
103
|
+
'- pnpm lint',
|
|
104
|
+
'- pnpm test',
|
|
105
|
+
].join('\n');
|
|
106
|
+
}
|
|
107
|
+
function slugify(input) {
|
|
108
|
+
const slug = input
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
111
|
+
.replace(/^-+|-+$/g, '');
|
|
112
|
+
return slug.length > 0 ? slug : 'generated-spec';
|
|
113
|
+
}
|
|
114
|
+
function toPascalCase(input) {
|
|
115
|
+
const words = input.match(/[a-zA-Z0-9]+/g) ?? ['Generated', 'Spec'];
|
|
116
|
+
return words
|
|
117
|
+
.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`)
|
|
118
|
+
.join('');
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=fallback-generator.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate that persisted spec documentation is authored in English.
|
|
3
|
+
*
|
|
4
|
+
* The detector is intentionally conservative: it ignores code fences, inline code,
|
|
5
|
+
* file paths, markdown headings, and BDD keywords before looking for common
|
|
6
|
+
* Spanish/Portuguese function words. It should block obvious non-English prose
|
|
7
|
+
* without punishing short technical descriptions.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateEnglishOnlySpecText(text: string): {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
detectedLanguage: 'en' | 'es' | 'pt' | 'unknown';
|
|
12
|
+
reason?: string;
|
|
13
|
+
signals: string[];
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=english-only.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// engine/spec-language/english-only.ts — deterministic guard for English-only spec docs.
|
|
2
|
+
const MIN_PROSE_TOKENS = 8;
|
|
3
|
+
const MIN_LANGUAGE_HITS = 2;
|
|
4
|
+
const MIN_LANGUAGE_RATIO = 0.08;
|
|
5
|
+
const NON_ENGLISH_SIGNATURES = {
|
|
6
|
+
es: new Set([
|
|
7
|
+
'abajo',
|
|
8
|
+
'acepta',
|
|
9
|
+
'actual',
|
|
10
|
+
'actualiza',
|
|
11
|
+
'cada',
|
|
12
|
+
'como',
|
|
13
|
+
'con',
|
|
14
|
+
'cuando',
|
|
15
|
+
'debajo',
|
|
16
|
+
'de',
|
|
17
|
+
'del',
|
|
18
|
+
'dentro',
|
|
19
|
+
'devuelve',
|
|
20
|
+
'diseñan',
|
|
21
|
+
'el',
|
|
22
|
+
'en',
|
|
23
|
+
'entre',
|
|
24
|
+
'fija',
|
|
25
|
+
'hacia',
|
|
26
|
+
'hay',
|
|
27
|
+
'la',
|
|
28
|
+
'las',
|
|
29
|
+
'lista',
|
|
30
|
+
'los',
|
|
31
|
+
'mismo',
|
|
32
|
+
'muestra',
|
|
33
|
+
'no',
|
|
34
|
+
'oculta',
|
|
35
|
+
'para',
|
|
36
|
+
'por',
|
|
37
|
+
'queda',
|
|
38
|
+
'que',
|
|
39
|
+
'renderiza',
|
|
40
|
+
'reporta',
|
|
41
|
+
'se',
|
|
42
|
+
'sin',
|
|
43
|
+
'usuario',
|
|
44
|
+
'usa',
|
|
45
|
+
]),
|
|
46
|
+
pt: new Set([
|
|
47
|
+
'abaixo',
|
|
48
|
+
'aceita',
|
|
49
|
+
'atualiza',
|
|
50
|
+
'cada',
|
|
51
|
+
'com',
|
|
52
|
+
'como',
|
|
53
|
+
'quando',
|
|
54
|
+
'da',
|
|
55
|
+
'das',
|
|
56
|
+
'de',
|
|
57
|
+
'dentro',
|
|
58
|
+
'devolve',
|
|
59
|
+
'do',
|
|
60
|
+
'dos',
|
|
61
|
+
'em',
|
|
62
|
+
'entre',
|
|
63
|
+
'fica',
|
|
64
|
+
'há',
|
|
65
|
+
'lista',
|
|
66
|
+
'mesmo',
|
|
67
|
+
'mostra',
|
|
68
|
+
'não',
|
|
69
|
+
'oculta',
|
|
70
|
+
'o',
|
|
71
|
+
'os',
|
|
72
|
+
'para',
|
|
73
|
+
'por',
|
|
74
|
+
'que',
|
|
75
|
+
'renderiza',
|
|
76
|
+
'sem',
|
|
77
|
+
'usuário',
|
|
78
|
+
'usa',
|
|
79
|
+
]),
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Validate that persisted spec documentation is authored in English.
|
|
83
|
+
*
|
|
84
|
+
* The detector is intentionally conservative: it ignores code fences, inline code,
|
|
85
|
+
* file paths, markdown headings, and BDD keywords before looking for common
|
|
86
|
+
* Spanish/Portuguese function words. It should block obvious non-English prose
|
|
87
|
+
* without punishing short technical descriptions.
|
|
88
|
+
*/
|
|
89
|
+
export function validateEnglishOnlySpecText(text) {
|
|
90
|
+
const tokens = tokenizeProse(text);
|
|
91
|
+
if (tokens.length < MIN_PROSE_TOKENS) {
|
|
92
|
+
return { ok: true, detectedLanguage: 'unknown', signals: [] };
|
|
93
|
+
}
|
|
94
|
+
const scored = Object.keys(NON_ENGLISH_SIGNATURES).map((lang) => {
|
|
95
|
+
const signature = NON_ENGLISH_SIGNATURES[lang];
|
|
96
|
+
const hits = tokens.filter((token) => signature.has(token));
|
|
97
|
+
return {
|
|
98
|
+
lang,
|
|
99
|
+
hits,
|
|
100
|
+
ratio: hits.length / tokens.length,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const best = scored.sort((a, b) => b.hits.length - a.hits.length || b.ratio - a.ratio)[0];
|
|
104
|
+
if (!best || best.hits.length < MIN_LANGUAGE_HITS || best.ratio < MIN_LANGUAGE_RATIO) {
|
|
105
|
+
return { ok: true, detectedLanguage: 'en', signals: [] };
|
|
106
|
+
}
|
|
107
|
+
const signals = [...new Set(best.hits)].slice(0, 8);
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
detectedLanguage: best.lang,
|
|
111
|
+
signals,
|
|
112
|
+
reason: `Spec documents must be written in English. Detected ${languageName(best.lang)} prose ` +
|
|
113
|
+
`signals: ${signals.join(', ')}.`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function tokenizeProse(text) {
|
|
117
|
+
const withoutCode = text
|
|
118
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
119
|
+
.replace(/~~~[\s\S]*?~~~/g, ' ')
|
|
120
|
+
.replace(/`[^`]*`/g, ' ')
|
|
121
|
+
.replace(/\b[a-zA-Z][\w.-]*(?:\/[\w.-]+)+\b/g, ' ')
|
|
122
|
+
.replace(/^---[\s\S]*?---/m, ' ');
|
|
123
|
+
return withoutCode
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.normalize('NFC')
|
|
126
|
+
.split(/[^\p{L}]+/u)
|
|
127
|
+
.filter((token) => token.length > 1)
|
|
128
|
+
.filter((token) => !isIgnoredToken(token));
|
|
129
|
+
}
|
|
130
|
+
function isIgnoredToken(token) {
|
|
131
|
+
return (token === 'given' ||
|
|
132
|
+
token === 'when' ||
|
|
133
|
+
token === 'then' ||
|
|
134
|
+
token === 'and' ||
|
|
135
|
+
token === 'or' ||
|
|
136
|
+
token === 'ac' ||
|
|
137
|
+
token === 'bdd');
|
|
138
|
+
}
|
|
139
|
+
function languageName(lang) {
|
|
140
|
+
return lang === 'es' ? 'Spanish' : 'Portuguese';
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=english-only.js.map
|
|
@@ -28,6 +28,20 @@ export async function migrateToUnified(specDir, projectPath) {
|
|
|
28
28
|
try {
|
|
29
29
|
const specContent = await readFile(specPath, 'utf-8');
|
|
30
30
|
if (/\n## Technical(\n|$)/.test(specContent)) {
|
|
31
|
+
try {
|
|
32
|
+
await readFile(techPath, 'utf-8');
|
|
33
|
+
if (projectPath) {
|
|
34
|
+
await safeUnlink(projectPath, techPath);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const { unlink } = await import('node:fs/promises');
|
|
38
|
+
await unlink(techPath);
|
|
39
|
+
}
|
|
40
|
+
return { specDir, merged: true, reason: 'already-unified' };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* no residual technical.md */
|
|
44
|
+
}
|
|
31
45
|
return { specDir, merged: false, reason: 'already-unified' };
|
|
32
46
|
}
|
|
33
47
|
let techContent;
|
|
@@ -125,7 +125,7 @@ export async function reconcileStatusFromDisk(projectPath, projectId) {
|
|
|
125
125
|
reconcileReason: `count mismatch: status.json had ${String(current.totalSpecs)}, disk has ${String(diskTotal)}`,
|
|
126
126
|
};
|
|
127
127
|
try {
|
|
128
|
-
await writeJsonSafe(statusPath, updated);
|
|
128
|
+
await writeJsonSafe(statusPath, updated, { verify: false });
|
|
129
129
|
}
|
|
130
130
|
catch {
|
|
131
131
|
// Best-effort — if write fails, return result without throwing
|
package/dist/index.js
CHANGED
|
@@ -63,6 +63,7 @@ const server = createMcpServer();
|
|
|
63
63
|
async function setupPostHandshake(handshakeGate) {
|
|
64
64
|
// Wait for initialize + notifications/initialized before firing any notifications.
|
|
65
65
|
await handshakeGate;
|
|
66
|
+
const envKey = process.env.SDD_LICENSE_KEY;
|
|
66
67
|
const [{ bootstrapAutopilotHandlers }, { registerAgentSquadHandlers }, { startupSync }, { checkForUpdates }, { setConnectedClient }, { handleActivateLicense }, { GroupManager }, { setGroupManager }, { registerSddTools, OFFICIAL_SDD_TOOL_NAMES },] = await Promise.all([
|
|
67
68
|
import('./engine/autopilot/bootstrap.js'),
|
|
68
69
|
import('./tools/register-agent-squad-tools.js'),
|
|
@@ -109,7 +110,6 @@ async function setupPostHandshake(handshakeGate) {
|
|
|
109
110
|
console.error('[Planu] Startup spec state sync failed:', err);
|
|
110
111
|
});
|
|
111
112
|
// Auto-activate license from environment variable if present.
|
|
112
|
-
const envKey = process.env.SDD_LICENSE_KEY;
|
|
113
113
|
if (envKey) {
|
|
114
114
|
const { licenseStore: ls } = await import('./storage/index.js');
|
|
115
115
|
const existingState = await ls.getLicenseState().catch(() => null);
|
|
@@ -17,6 +17,8 @@ import { runAutoPostCreatePipeline, formatPipelineLines } from './create-spec/au
|
|
|
17
17
|
import { generateLeanSpecContent } from '../engine/spec-format/lean-spec-generator.js';
|
|
18
18
|
import { generateLeanTechnicalContent } from '../engine/spec-format/lean-technical-generator.js';
|
|
19
19
|
import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
|
|
20
|
+
import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
|
|
21
|
+
import { FallbackGenerator } from '../engine/spec-generator/index.js';
|
|
20
22
|
import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
|
|
21
23
|
import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
|
|
22
24
|
import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
|
|
@@ -327,6 +329,28 @@ async function findRecentlyWrittenSpecs(projectPath, windowMs) {
|
|
|
327
329
|
}
|
|
328
330
|
const DESCRIPTION_MAX_CHARS = 10_000;
|
|
329
331
|
const DESCRIPTION_EXCEED_MSG = 'Description exceeds 10000 chars. Create with a summary (<3k) and use reconcile_spec to add the rest.';
|
|
332
|
+
function checkEnglishOnlyInput(params) {
|
|
333
|
+
const validation = validateEnglishOnlySpecText(`${params.title}\n\n${params.description}`);
|
|
334
|
+
if (validation.ok) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
content: [
|
|
339
|
+
{
|
|
340
|
+
type: 'text',
|
|
341
|
+
text: `English-only spec gate blocked create_spec.\n\n${validation.reason ?? 'Non-English prose detected.'}\n\n` +
|
|
342
|
+
'Rewrite the title and description in English, then call create_spec again. User-facing responses may be localized, but spec.md must be English.',
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
isError: true,
|
|
346
|
+
structuredContent: {
|
|
347
|
+
error: 'SPEC_LANGUAGE_GATE_BLOCKED',
|
|
348
|
+
detectedLanguage: validation.detectedLanguage,
|
|
349
|
+
signals: validation.signals,
|
|
350
|
+
fixHint: 'Rewrite title and description in English before creating the spec.',
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
330
354
|
// eslint-disable-next-line max-lines-per-function
|
|
331
355
|
export async function handleCreateSpec(inputParams, server) {
|
|
332
356
|
// SPEC-781: Friendly error for description exceeding 10000 chars (replaces cryptic Zod error)
|
|
@@ -363,6 +387,10 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
363
387
|
inputParams.specId) {
|
|
364
388
|
return handleAgentTeamSynthesis(inputParams.specId, resolvedPath, inputParams.agentTeamFindings);
|
|
365
389
|
}
|
|
390
|
+
const languageGate = checkEnglishOnlyInput(resolvedInputParams);
|
|
391
|
+
if (languageGate) {
|
|
392
|
+
return languageGate;
|
|
393
|
+
}
|
|
366
394
|
// SPEC-665: Fire-and-forget TTL refresh for conventions.json (never blocks create_spec)
|
|
367
395
|
void import('../engine/conventions-ttl.js').then(({ triggerConventionsTtlRefresh }) => {
|
|
368
396
|
void triggerConventionsTtlRefresh(resolvedPath).catch(() => {
|
|
@@ -463,9 +491,26 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
463
491
|
await measureStep('mkdir-specDir', () => mkdir(specDir, { recursive: true }));
|
|
464
492
|
// SPEC-586: Filter suggested criteria by spec tags/target/scope before injection
|
|
465
493
|
const filteredCriteria = await filterCriteriaByTags(autopilot.suggestedCriteria, spec.tags, spec.target, spec.scope).catch(() => autopilot.suggestedCriteria);
|
|
494
|
+
const specGenerator = new FallbackGenerator();
|
|
495
|
+
const generatedSpec = await measureStep('generateSpecBody', () => specGenerator.generate({
|
|
496
|
+
title: spec.title,
|
|
497
|
+
description,
|
|
498
|
+
type: spec.type,
|
|
499
|
+
scope: spec.scope,
|
|
500
|
+
target: spec.target,
|
|
501
|
+
acFormat: params.acFormat,
|
|
502
|
+
projectContext: {
|
|
503
|
+
language: knowledge?.language ?? undefined,
|
|
504
|
+
framework: knowledge?.framework ?? undefined,
|
|
505
|
+
architecture: knowledge?.architecture.primary ?? undefined,
|
|
506
|
+
},
|
|
507
|
+
}));
|
|
508
|
+
spec.generatedWithModel = generatedSpec.generatedWithModel;
|
|
509
|
+
spec.generatedAt = generatedSpec.generatedAt;
|
|
510
|
+
spec.qualityWarnings = generatedSpec.qualityWarnings;
|
|
466
511
|
const leanSpec = generateLeanSpecContent({
|
|
467
512
|
spec,
|
|
468
|
-
description,
|
|
513
|
+
description: generatedSpec.specBody,
|
|
469
514
|
estimation,
|
|
470
515
|
extraCriteria: filteredCriteria,
|
|
471
516
|
acFormat: params.acFormat,
|
|
@@ -339,14 +339,16 @@ export async function handleInitProject(params, server) {
|
|
|
339
339
|
void withAudit(projectPath, 'init_project', 'configurePlanuHooks', () => configurePlanuHooks(projectPath)).catch(() => {
|
|
340
340
|
/* best-effort */
|
|
341
341
|
});
|
|
342
|
-
// SPEC-779: Install universal rules catalog
|
|
343
|
-
|
|
342
|
+
// SPEC-779: Install universal rules catalog before returning so init_project
|
|
343
|
+
// has no delayed project mutations after the tool result is delivered.
|
|
344
|
+
try {
|
|
344
345
|
const hostHint = detectHost();
|
|
345
346
|
const host = hostHint === 'codex' ? 'codex' : hostHint === 'gemini' ? 'gemini' : 'claude-code';
|
|
346
347
|
await installUniversalRules(projectPath, host);
|
|
347
|
-
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
348
350
|
/* best-effort */
|
|
349
|
-
}
|
|
351
|
+
}
|
|
350
352
|
// SPEC-566: Auto-configure Claude Code keybindings (shift+enter → submit) — fire-and-forget
|
|
351
353
|
void configureClaudeKeybindings()
|
|
352
354
|
.then((configured) => configured ? globalStore.updateGlobalConfig({ keybindingsConfigured: true }) : undefined)
|
package/dist/tools/list-specs.js
CHANGED
|
@@ -3,7 +3,6 @@ import { specStore, knowledgeStore } from '../storage/index.js';
|
|
|
3
3
|
import { buildListSpecsSummary } from '../engine/human-summary.js';
|
|
4
4
|
import { dirname } from 'node:path';
|
|
5
5
|
import { checkBundledVersionGap } from '../engine/version-detector/bundled-version-checker.js';
|
|
6
|
-
import { checkAndFixBundledVersion } from '../engine/mcp-config/mcp-config-writer.js';
|
|
7
6
|
import { readLastModifiedTimestamp, formatRelativeDate } from '../engine/spec-changelog/index.js';
|
|
8
7
|
import { discoverAndFlattenSpecs, filterUnprefixedSpecs, importFilesystemSpecs, migrateSpecFolderNames, reconcileSpecPaths, scanForAmbiguousCriteria, } from '../engine/spec-migrator.js';
|
|
9
8
|
import { detectDrift, formatDriftMessage } from '../engine/spec-migrator/drift-detector.js';
|
|
@@ -109,15 +108,8 @@ export async function handleListSpecs(params) {
|
|
|
109
108
|
const collector = new AutopilotSummaryCollector();
|
|
110
109
|
// SPEC-756: lazy version check — fire-and-forget, non-blocking
|
|
111
110
|
if (params.projectPath) {
|
|
112
|
-
const lazyPath = params.projectPath;
|
|
113
111
|
checkBundledVersionGap(2000)
|
|
114
|
-
.then((
|
|
115
|
-
if (gap.gap) {
|
|
116
|
-
checkAndFixBundledVersion(lazyPath, gap.current, gap.latest).catch(() => {
|
|
117
|
-
/* best-effort — never block list_specs */
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
})
|
|
112
|
+
.then(() => undefined)
|
|
121
113
|
.catch(() => {
|
|
122
114
|
/* best-effort */
|
|
123
115
|
});
|
|
@@ -151,21 +143,6 @@ export async function handleListSpecs(params) {
|
|
|
151
143
|
if (knowledge.projectPath) {
|
|
152
144
|
await autoDiscoverProject(projectId, knowledge.projectPath, collector);
|
|
153
145
|
}
|
|
154
|
-
// SPEC-768: Auto-migrate legacy technical.md into unified spec.md (non-blocking)
|
|
155
|
-
if (knowledge.projectPath) {
|
|
156
|
-
try {
|
|
157
|
-
const { runSsrBackMigration } = await import('../engine/spec-migrator/ssr-back-migration.js');
|
|
158
|
-
await runSsrBackMigration({
|
|
159
|
-
projectPath: knowledge.projectPath,
|
|
160
|
-
dryRun: false,
|
|
161
|
-
foldProgressMd: true,
|
|
162
|
-
allowDoneSpecs: false,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
/* best-effort — never block list_specs */
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
146
|
// Get all specs
|
|
170
147
|
let specs = await specStore.listSpecs(projectId);
|
|
171
148
|
// Apply filters
|
|
@@ -59,12 +59,15 @@ export declare function checkDorGate(spec: Spec, specId: string, projectId: stri
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function checkAmbiguityGate(spec: Spec, newStatus: SpecStatus): Promise<ToolResult | null>;
|
|
61
61
|
/**
|
|
62
|
-
* SPEC-769: Readiness gate — block 'approved' when spec
|
|
62
|
+
* SPEC-769/SPEC-492: Readiness gate — block 'review'/'approved' when spec
|
|
63
|
+
* quality is too low or the persisted spec document is not English.
|
|
63
64
|
* Best-effort: if spec.md cannot be read, the gate is skipped (returns no block).
|
|
64
65
|
*
|
|
65
66
|
* - 0 criteria → InteractiveQuestion (hard block, no force path)
|
|
66
|
-
* - score < 70 && !forceApprove → InteractiveQuestion with 3 options
|
|
67
|
-
* - score < 70 && forceApprove → proceed, qualityWarnings populated
|
|
67
|
+
* - score < 70 on approved && !forceApprove → InteractiveQuestion with 3 options
|
|
68
|
+
* - score < 70 on approved && forceApprove → proceed, qualityWarnings populated
|
|
69
|
+
* - score < 70 on review → block and keep draft
|
|
70
|
+
* - non-English prose → hard block
|
|
68
71
|
* - score >= 70 → proceed, no warnings
|
|
69
72
|
*/
|
|
70
73
|
export declare function checkReadinessGate(spec: Spec, newStatus: SpecStatus, forceApprove: boolean | undefined): Promise<ReadinessGateOutput>;
|
|
@@ -5,6 +5,7 @@ import { validateDoR } from '../../engine/dor-dod.js';
|
|
|
5
5
|
import { dispatchFeedbackEvent } from '../learn.js';
|
|
6
6
|
import { scoreAmbiguityFromPath } from '../../engine/ambiguity-scorer.js';
|
|
7
7
|
import { checkReadinessInternal } from '../../engine/readiness-checker.js';
|
|
8
|
+
import { validateEnglishOnlySpecText } from '../../engine/spec-language/english-only.js';
|
|
8
9
|
/**
|
|
9
10
|
* Valid state transitions for spec lifecycle.
|
|
10
11
|
* draft -> review -> approved -> implementing -> done
|
|
@@ -173,16 +174,19 @@ export async function checkAmbiguityGate(spec, newStatus) {
|
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
/**
|
|
176
|
-
* SPEC-769: Readiness gate — block 'approved' when spec
|
|
177
|
+
* SPEC-769/SPEC-492: Readiness gate — block 'review'/'approved' when spec
|
|
178
|
+
* quality is too low or the persisted spec document is not English.
|
|
177
179
|
* Best-effort: if spec.md cannot be read, the gate is skipped (returns no block).
|
|
178
180
|
*
|
|
179
181
|
* - 0 criteria → InteractiveQuestion (hard block, no force path)
|
|
180
|
-
* - score < 70 && !forceApprove → InteractiveQuestion with 3 options
|
|
181
|
-
* - score < 70 && forceApprove → proceed, qualityWarnings populated
|
|
182
|
+
* - score < 70 on approved && !forceApprove → InteractiveQuestion with 3 options
|
|
183
|
+
* - score < 70 on approved && forceApprove → proceed, qualityWarnings populated
|
|
184
|
+
* - score < 70 on review → block and keep draft
|
|
185
|
+
* - non-English prose → hard block
|
|
182
186
|
* - score >= 70 → proceed, no warnings
|
|
183
187
|
*/
|
|
184
188
|
export async function checkReadinessGate(spec, newStatus, forceApprove) {
|
|
185
|
-
if (newStatus !== 'approved' || !spec.specPath) {
|
|
189
|
+
if ((newStatus !== 'review' && newStatus !== 'approved') || !spec.specPath) {
|
|
186
190
|
return { blockResult: null, qualityWarnings: [] };
|
|
187
191
|
}
|
|
188
192
|
let body;
|
|
@@ -197,6 +201,29 @@ export async function checkReadinessGate(spec, newStatus, forceApprove) {
|
|
|
197
201
|
if (typeof body !== 'string') {
|
|
198
202
|
return { blockResult: null, qualityWarnings: [] };
|
|
199
203
|
}
|
|
204
|
+
const languageValidation = validateEnglishOnlySpecText(body);
|
|
205
|
+
if (!languageValidation.ok) {
|
|
206
|
+
return {
|
|
207
|
+
blockResult: {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: `English-only spec gate blocked transition to ${newStatus}.\n\n` +
|
|
212
|
+
`${languageValidation.reason ?? 'Non-English prose detected.'}\n\n` +
|
|
213
|
+
'Rewrite spec.md in English before moving it forward.',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
isError: true,
|
|
217
|
+
structuredContent: {
|
|
218
|
+
error: 'SPEC_LANGUAGE_GATE_BLOCKED',
|
|
219
|
+
detectedLanguage: languageValidation.detectedLanguage,
|
|
220
|
+
signals: languageValidation.signals,
|
|
221
|
+
fixHint: 'Rewrite spec.md in English before review or approval.',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
qualityWarnings: [],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
200
227
|
let score;
|
|
201
228
|
let warnings;
|
|
202
229
|
let criteriaCount;
|
|
@@ -207,6 +234,27 @@ export async function checkReadinessGate(spec, newStatus, forceApprove) {
|
|
|
207
234
|
return { blockResult: null, qualityWarnings: [] };
|
|
208
235
|
}
|
|
209
236
|
if (criteriaCount === 0) {
|
|
237
|
+
if (newStatus === 'review') {
|
|
238
|
+
return {
|
|
239
|
+
blockResult: {
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: 'text',
|
|
243
|
+
text: 'Readiness gate blocked transition to review: spec has 0 acceptance criteria.',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
isError: true,
|
|
247
|
+
structuredContent: {
|
|
248
|
+
error: 'READINESS_GATE_BLOCKED',
|
|
249
|
+
score,
|
|
250
|
+
threshold: 70,
|
|
251
|
+
warnings,
|
|
252
|
+
fixHint: 'Add at least 3 testable acceptance criteria before moving to review.',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
qualityWarnings: [],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
210
258
|
const question = {
|
|
211
259
|
header: 'Spec incomplete',
|
|
212
260
|
question: 'This spec has 0 acceptance criteria. Add criteria before approving — transition blocked.',
|
|
@@ -244,6 +292,29 @@ export async function checkReadinessGate(spec, newStatus, forceApprove) {
|
|
|
244
292
|
if (score >= 70) {
|
|
245
293
|
return { blockResult: null, qualityWarnings: [] };
|
|
246
294
|
}
|
|
295
|
+
if (newStatus === 'review') {
|
|
296
|
+
const warningsSummary = warnings.slice(0, 3).join('; ');
|
|
297
|
+
return {
|
|
298
|
+
blockResult: {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: 'text',
|
|
302
|
+
text: `Readiness gate blocked transition to review: score ${String(score)}/100 — below 70 threshold. ` +
|
|
303
|
+
`Warnings: ${warningsSummary}`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
isError: true,
|
|
307
|
+
structuredContent: {
|
|
308
|
+
error: 'READINESS_GATE_BLOCKED',
|
|
309
|
+
score,
|
|
310
|
+
threshold: 70,
|
|
311
|
+
warnings,
|
|
312
|
+
fixHint: 'Complete the spec before moving from draft to review.',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
qualityWarnings: [],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
247
318
|
// Score < 70
|
|
248
319
|
if (forceApprove) {
|
|
249
320
|
// Caller wants to force through — proceed with warnings attached
|
package/dist/types/index.d.ts
CHANGED
|
@@ -59,6 +59,7 @@ export * from './hooks.js';
|
|
|
59
59
|
export * from './webhook.js';
|
|
60
60
|
export * from './ci.js';
|
|
61
61
|
export * from './spec-templates.js';
|
|
62
|
+
export * from './spec-generator.js';
|
|
62
63
|
export * from './registry.js';
|
|
63
64
|
export * from './tool-groups.js';
|
|
64
65
|
export * from './code-transforms.js';
|
package/dist/types/index.js
CHANGED
|
@@ -60,6 +60,7 @@ export * from './hooks.js';
|
|
|
60
60
|
export * from './webhook.js';
|
|
61
61
|
export * from './ci.js';
|
|
62
62
|
export * from './spec-templates.js';
|
|
63
|
+
export * from './spec-generator.js';
|
|
63
64
|
export * from './registry.js';
|
|
64
65
|
export * from './tool-groups.js';
|
|
65
66
|
export * from './code-transforms.js';
|
|
@@ -57,6 +57,12 @@ export interface Spec {
|
|
|
57
57
|
model?: 'haiku' | 'sonnet' | 'opus';
|
|
58
58
|
/** SPEC-630: token budget derived from devHours. */
|
|
59
59
|
budget?: 800 | 2000 | 4000;
|
|
60
|
+
/** SPEC-766: Generator that produced the initial spec body. */
|
|
61
|
+
generatedWithModel?: string;
|
|
62
|
+
/** SPEC-766: ISO timestamp for initial spec body generation. */
|
|
63
|
+
generatedAt?: string;
|
|
64
|
+
/** SPEC-766: Non-blocking quality warnings emitted by the generator. */
|
|
65
|
+
qualityWarnings?: string[];
|
|
60
66
|
/** SPEC-686: Implementation priority for ROI scoring. Defaults to 'medium'. */
|
|
61
67
|
priority?: 'high' | 'medium' | 'low';
|
|
62
68
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SpecScope, SpecTarget, SpecType } from './common/index.js';
|
|
2
|
+
export interface SpecGenerationRequest {
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
type: SpecType;
|
|
6
|
+
scope: SpecScope;
|
|
7
|
+
target: SpecTarget;
|
|
8
|
+
acFormat?: 'checkbox' | 'bdd';
|
|
9
|
+
projectContext?: {
|
|
10
|
+
language?: string;
|
|
11
|
+
framework?: string;
|
|
12
|
+
architecture?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface SpecGenerationResult {
|
|
16
|
+
specBody: string;
|
|
17
|
+
technicalSection: string;
|
|
18
|
+
generatedWithModel: string;
|
|
19
|
+
generatedAt: string;
|
|
20
|
+
qualityWarnings: string[];
|
|
21
|
+
fallbackReason?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SpecContentGenerator {
|
|
24
|
+
generate(request: SpecGenerationRequest): Promise<SpecGenerationResult>;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=spec-generator.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.2",
|
|
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",
|