@planu/cli 4.3.9 → 4.3.11
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 +8 -0
- package/dist/cli/commands/status.js +79 -46
- package/dist/engine/pr-description-generator.js +5 -2
- package/dist/engine/spec-generator/api-key-resolver.d.ts +4 -0
- package/dist/engine/spec-generator/api-key-resolver.js +31 -0
- package/dist/engine/spec-generator/index.d.ts +3 -0
- package/dist/engine/spec-generator/index.js +3 -0
- package/dist/engine/spec-generator/opus-generator.d.ts +12 -0
- package/dist/engine/spec-generator/opus-generator.js +97 -0
- package/dist/engine/spec-generator/quality-validator.d.ts +5 -0
- package/dist/engine/spec-generator/quality-validator.js +22 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +9 -1
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +11 -2
- package/dist/engine/validator/checklist.js +1 -1
- package/dist/resources/process.js +1 -1
- package/dist/resources/templates.js +1 -1
- package/dist/tools/create-spec.js +5 -2
- package/dist/types/cli.d.ts +17 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/spec-format.d.ts +2 -2
- package/dist/types/spec-generator.d.ts +38 -0
- package/package.json +11 -10
- package/dist/engine/context-intelligence/index.d.ts +0 -7
- package/dist/engine/context-intelligence/index.js +0 -6
- package/dist/engine/legacy/ast-analyzer.d.ts +0 -21
- package/dist/engine/legacy/ast-analyzer.js +0 -113
- package/dist/engine/legacy/characterization-generator.d.ts +0 -6
- package/dist/engine/legacy/characterization-generator.js +0 -146
- package/dist/engine/legacy/hyrum-scanner.d.ts +0 -6
- package/dist/engine/legacy/hyrum-scanner.js +0 -137
- package/dist/engine/legacy/safety-net-orchestrator.d.ts +0 -7
- package/dist/engine/legacy/safety-net-orchestrator.js +0 -114
- package/dist/engine/legacy/seam-finder.d.ts +0 -6
- package/dist/engine/legacy/seam-finder.js +0 -139
- package/dist/tools/legacy/characterize-legacy-code.d.ts +0 -9
- package/dist/tools/legacy/characterize-legacy-code.js +0 -63
- package/dist/tools/legacy/detect-hyrum-risks.d.ts +0 -8
- package/dist/tools/legacy/detect-hyrum-risks.js +0 -47
- package/dist/tools/legacy/refactor-with-safety-net.d.ts +0 -10
- package/dist/tools/legacy/refactor-with-safety-net.js +0 -55
- package/dist/tools/legacy/seams-detector.d.ts +0 -8
- package/dist/tools/legacy/seams-detector.js +0 -47
- package/dist/types/legacy.d.ts +0 -151
- package/dist/types/legacy.js +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [4.3.11] - 2026-05-25
|
|
2
|
+
|
|
3
|
+
**Tarball SHA-256:** `8074e0be3d0a9613246e3330b30808472f99b1a887698a56bfe5aad109025f39`
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
- fix: close critical Planu delivery specs
|
|
7
|
+
|
|
8
|
+
|
|
1
9
|
## [4.3.9] - 2026-05-25
|
|
2
10
|
|
|
3
11
|
**Tarball SHA-256:** `a47146af1f2f8e695247fb6ee92cc8da8de00b2ab7967734a7faade7f3659aeb`
|
|
@@ -8,67 +8,100 @@ import { red, green } from '../colors.js';
|
|
|
8
8
|
export const statusCommand = {
|
|
9
9
|
name: 'status',
|
|
10
10
|
description: 'Update the status of a spec',
|
|
11
|
-
usage: 'planu status <specId> --set <status> [--project-id ID] | planu status batch --set <status> SPEC-001 SPEC-002',
|
|
11
|
+
usage: 'planu status <specId> --set <status> [--project-id ID] [--model-tier-used TIER --model-id ID --context-hash HASH --handoff-path PATH --reviewed-by ID --arbitrated-by ID] | planu status batch --set <status> SPEC-001 SPEC-002',
|
|
12
12
|
async run(args, flags) {
|
|
13
|
-
const { values, positionals } =
|
|
14
|
-
args,
|
|
15
|
-
options: {
|
|
16
|
-
set: { type: 'string', short: 's' },
|
|
17
|
-
'project-id': { type: 'string' },
|
|
18
|
-
notes: { type: 'string', short: 'n' },
|
|
19
|
-
},
|
|
20
|
-
strict: false,
|
|
21
|
-
allowPositionals: true,
|
|
22
|
-
});
|
|
13
|
+
const { values, positionals } = parseStatusArgs(args);
|
|
23
14
|
const isBatch = positionals[0] === 'batch';
|
|
24
15
|
const specId = positionals[0];
|
|
25
16
|
if (!specId) {
|
|
26
|
-
|
|
17
|
+
writeUsageError('Spec ID is required.');
|
|
27
18
|
process.exitCode = 1;
|
|
28
19
|
return;
|
|
29
20
|
}
|
|
30
21
|
const status = values.set;
|
|
31
22
|
if (!status) {
|
|
32
|
-
|
|
23
|
+
writeUsageError('--set <status> is required.');
|
|
33
24
|
process.exitCode = 1;
|
|
34
25
|
return;
|
|
35
26
|
}
|
|
36
|
-
const projectId = values['project-id'] ?? detectProjectId();
|
|
37
27
|
if (isBatch) {
|
|
38
|
-
|
|
39
|
-
specIds: positionals.slice(1),
|
|
40
|
-
projectId,
|
|
41
|
-
status: status,
|
|
42
|
-
reviewNotes: values.notes ?? undefined,
|
|
43
|
-
});
|
|
44
|
-
if (result.isError) {
|
|
45
|
-
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
46
|
-
process.exitCode = 1;
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
process.stdout.write(formatToolResult(result, flags) + '\n');
|
|
28
|
+
await runBatchStatus(positionals.slice(1), values, status, flags);
|
|
50
29
|
return;
|
|
51
30
|
}
|
|
52
|
-
|
|
53
|
-
specId,
|
|
54
|
-
projectId,
|
|
55
|
-
status: status,
|
|
56
|
-
reviewNotes: values.notes ?? undefined,
|
|
57
|
-
});
|
|
58
|
-
if (result.isError) {
|
|
59
|
-
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
60
|
-
process.exitCode = 1;
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const output = formatToolResult(result, flags);
|
|
64
|
-
if (flags?.quiet) {
|
|
65
|
-
if (output) {
|
|
66
|
-
process.stdout.write(output + '\n');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
process.stdout.write(`${green('Status updated!')} ${output}\n`);
|
|
71
|
-
}
|
|
31
|
+
await runSingleStatus(specId, values, status, flags);
|
|
72
32
|
},
|
|
73
33
|
};
|
|
34
|
+
function parseStatusArgs(args) {
|
|
35
|
+
const parsed = parseArgs({
|
|
36
|
+
args,
|
|
37
|
+
options: {
|
|
38
|
+
set: { type: 'string', short: 's' },
|
|
39
|
+
'project-id': { type: 'string' },
|
|
40
|
+
'project-path': { type: 'string' },
|
|
41
|
+
notes: { type: 'string', short: 'n' },
|
|
42
|
+
'model-id': { type: 'string' },
|
|
43
|
+
'model-tier-used': { type: 'string' },
|
|
44
|
+
'context-hash': { type: 'string' },
|
|
45
|
+
'handoff-path': { type: 'string' },
|
|
46
|
+
'handoff-artifact-id': { type: 'string' },
|
|
47
|
+
'reviewed-by': { type: 'string' },
|
|
48
|
+
'arbitrated-by': { type: 'string' },
|
|
49
|
+
reason: { type: 'string' },
|
|
50
|
+
force: { type: 'boolean' },
|
|
51
|
+
'force-status': { type: 'boolean' },
|
|
52
|
+
'force-status-reason': { type: 'string' },
|
|
53
|
+
},
|
|
54
|
+
strict: false,
|
|
55
|
+
allowPositionals: true,
|
|
56
|
+
});
|
|
57
|
+
return { values: parsed.values, positionals: parsed.positionals };
|
|
58
|
+
}
|
|
59
|
+
function writeUsageError(message) {
|
|
60
|
+
process.stderr.write(`${red('Error:')} ${message}\nUsage: ${statusCommand.usage}\n`);
|
|
61
|
+
}
|
|
62
|
+
async function runBatchStatus(specIds, values, status, flags) {
|
|
63
|
+
const result = await handleUpdateStatusBatch({
|
|
64
|
+
specIds,
|
|
65
|
+
projectId: values['project-id'] ?? detectProjectId(),
|
|
66
|
+
status: status,
|
|
67
|
+
reviewNotes: values.notes,
|
|
68
|
+
});
|
|
69
|
+
writeToolResult(result, flags, false);
|
|
70
|
+
}
|
|
71
|
+
async function runSingleStatus(specId, values, status, flags) {
|
|
72
|
+
const result = await handleUpdateStatus({
|
|
73
|
+
specId,
|
|
74
|
+
projectId: values['project-id'] ?? detectProjectId(),
|
|
75
|
+
projectPath: values['project-path'],
|
|
76
|
+
status: status,
|
|
77
|
+
reviewNotes: values.notes,
|
|
78
|
+
modelId: values['model-id'],
|
|
79
|
+
modelTierUsed: values['model-tier-used'],
|
|
80
|
+
contextHash: values['context-hash'],
|
|
81
|
+
handoffPath: values['handoff-path'],
|
|
82
|
+
handoffArtifactId: values['handoff-artifact-id'],
|
|
83
|
+
reviewedBy: values['reviewed-by'],
|
|
84
|
+
arbitratedBy: values['arbitrated-by'],
|
|
85
|
+
reason: values.reason,
|
|
86
|
+
force: values.force === true ? true : undefined,
|
|
87
|
+
forceStatus: values['force-status'] === true ? true : undefined,
|
|
88
|
+
forceStatusReason: values['force-status-reason'],
|
|
89
|
+
});
|
|
90
|
+
writeToolResult(result, flags, true);
|
|
91
|
+
}
|
|
92
|
+
function writeToolResult(result, flags, includeSuccessPrefix) {
|
|
93
|
+
if (result.isError) {
|
|
94
|
+
process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const output = formatToolResult(result, flags);
|
|
99
|
+
if (flags?.quiet) {
|
|
100
|
+
if (output) {
|
|
101
|
+
process.stdout.write(`${output}\n`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(includeSuccessPrefix ? `${green('Status updated!')} ${output}\n` : `${output}\n`);
|
|
106
|
+
}
|
|
74
107
|
//# sourceMappingURL=status.js.map
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
function technicalSectionRef(spec) {
|
|
2
|
+
return `${spec.specPath}#technical`;
|
|
3
|
+
}
|
|
1
4
|
// ---------------------------------------------------------------------------
|
|
2
5
|
// Title generation
|
|
3
6
|
// ---------------------------------------------------------------------------
|
|
@@ -46,7 +49,7 @@ function buildGithubMarkdown(spec, audience, includeChecklist) {
|
|
|
46
49
|
sections.push('');
|
|
47
50
|
sections.push(`## Changes`);
|
|
48
51
|
sections.push(`- Spec: \`${spec.specPath}\``);
|
|
49
|
-
sections.push(`- Technical: \`${spec
|
|
52
|
+
sections.push(`- Technical design: \`${technicalSectionRef(spec)}\``);
|
|
50
53
|
if (spec.tags.length > 0) {
|
|
51
54
|
sections.push(`- Tags: ${spec.tags.join(', ')}`);
|
|
52
55
|
}
|
|
@@ -86,7 +89,7 @@ function buildGitlabMarkdown(spec, audience, includeChecklist) {
|
|
|
86
89
|
sections.push(`Required by Planu spec ${spec.id}. Risk level: **${spec.risk}**. Difficulty: **${spec.difficulty}/5**.`);
|
|
87
90
|
sections.push('');
|
|
88
91
|
sections.push(`## How`);
|
|
89
|
-
sections.push(`See
|
|
92
|
+
sections.push(`See \`${technicalSectionRef(spec)}\` for implementation details.`);
|
|
90
93
|
if (spec.tags.length > 0) {
|
|
91
94
|
sections.push(`Tags: ${spec.tags.join(', ')}`);
|
|
92
95
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export class ApiKeyResolver {
|
|
4
|
+
async resolveAnthropicKey(projectPath) {
|
|
5
|
+
const envKey = process.env.PLANU_ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY;
|
|
6
|
+
if (envKey?.trim()) {
|
|
7
|
+
return envKey.trim();
|
|
8
|
+
}
|
|
9
|
+
for (const file of [
|
|
10
|
+
join(projectPath, 'src/config/api-keys.json'),
|
|
11
|
+
join(projectPath, '.planu/api-keys.json'),
|
|
12
|
+
]) {
|
|
13
|
+
const key = await readKeyFile(file);
|
|
14
|
+
if (key) {
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function readKeyFile(path) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(await readFile(path, 'utf-8'));
|
|
24
|
+
const key = parsed.anthropic?.apiKey;
|
|
25
|
+
return key?.trim() ? key.trim() : undefined;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=api-key-resolver.js.map
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { FallbackGenerator } from './fallback-generator.js';
|
|
2
|
+
export { ApiKeyResolver } from './api-key-resolver.js';
|
|
3
|
+
export { OpusGenerator } from './opus-generator.js';
|
|
4
|
+
export { QualityValidator } from './quality-validator.js';
|
|
2
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { OpusGeneratorDeps, SpecContentGenerator, SpecGenerationRequest, SpecGenerationResult } from '../../types/index.js';
|
|
2
|
+
export declare class OpusGenerator implements SpecContentGenerator {
|
|
3
|
+
private readonly model;
|
|
4
|
+
private readonly timeoutMs;
|
|
5
|
+
private readonly client;
|
|
6
|
+
private readonly fallback;
|
|
7
|
+
private readonly validator;
|
|
8
|
+
constructor(deps: OpusGeneratorDeps);
|
|
9
|
+
generate(request: SpecGenerationRequest): Promise<SpecGenerationResult>;
|
|
10
|
+
private fallbackWithReason;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=opus-generator.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { FallbackGenerator } from './fallback-generator.js';
|
|
3
|
+
import { QualityValidator } from './quality-validator.js';
|
|
4
|
+
export class OpusGenerator {
|
|
5
|
+
model;
|
|
6
|
+
timeoutMs;
|
|
7
|
+
client;
|
|
8
|
+
fallback;
|
|
9
|
+
validator;
|
|
10
|
+
constructor(deps) {
|
|
11
|
+
this.model = deps.model ?? 'claude-opus-4-5';
|
|
12
|
+
this.timeoutMs = deps.timeoutMs ?? 60_000;
|
|
13
|
+
this.client = deps.client ?? new Anthropic({ apiKey: deps.apiKey }).messages;
|
|
14
|
+
this.fallback = deps.fallback ?? new FallbackGenerator();
|
|
15
|
+
this.validator = deps.validator ?? new QualityValidator();
|
|
16
|
+
}
|
|
17
|
+
async generate(request) {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timeout = setTimeout(() => {
|
|
20
|
+
controller.abort();
|
|
21
|
+
}, this.timeoutMs);
|
|
22
|
+
try {
|
|
23
|
+
const response = await this.client.create({
|
|
24
|
+
model: this.model,
|
|
25
|
+
max_tokens: 6000,
|
|
26
|
+
temperature: 0.2,
|
|
27
|
+
system: buildSystemPrompt(),
|
|
28
|
+
messages: [{ role: 'user', content: buildUserPrompt(request) }],
|
|
29
|
+
}, { signal: controller.signal });
|
|
30
|
+
const text = response.content
|
|
31
|
+
.filter((block) => block.type === 'text' && typeof block.text === 'string')
|
|
32
|
+
.map((block) => block.text)
|
|
33
|
+
.join('\n')
|
|
34
|
+
.trim();
|
|
35
|
+
if (response.content.some((block) => block.type === 'tool_use') || text.length === 0) {
|
|
36
|
+
return await this.fallbackWithReason(request, 'tool-use-leak');
|
|
37
|
+
}
|
|
38
|
+
const result = parseGeneratedText(text, this.model);
|
|
39
|
+
const validation = this.validator.validate(result);
|
|
40
|
+
if (validation.passed) {
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
return { ...result, qualityWarnings: validation.warnings };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return await this.fallbackWithReason(request, 'timeout-or-http-error');
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async fallbackWithReason(request, reason) {
|
|
53
|
+
const fallback = await this.fallback.generate(request);
|
|
54
|
+
return {
|
|
55
|
+
...fallback,
|
|
56
|
+
qualityWarnings: [
|
|
57
|
+
...fallback.qualityWarnings,
|
|
58
|
+
`Opus generator fallback used: ${reason}. Configure PLANU_ANTHROPIC_API_KEY for Opus-quality create_spec generation.`,
|
|
59
|
+
],
|
|
60
|
+
fallbackReason: reason,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function buildSystemPrompt() {
|
|
65
|
+
return [
|
|
66
|
+
'You generate Planu unified spec.md content.',
|
|
67
|
+
'Do not invoke any tools. Do not request tool use. Return markdown only.',
|
|
68
|
+
'Include ## Problem, ## Acceptance Criteria, ## Technical, ## Files, and ## Verification.',
|
|
69
|
+
'Acceptance criteria must include at least 3 GIVEN/WHEN/THEN items.',
|
|
70
|
+
'Technical must include at least 2 TypeScript signatures relevant to the requested change.',
|
|
71
|
+
].join('\n');
|
|
72
|
+
}
|
|
73
|
+
function buildUserPrompt(request) {
|
|
74
|
+
return [
|
|
75
|
+
`Title: ${request.title}`,
|
|
76
|
+
`Type: ${request.type}`,
|
|
77
|
+
`Scope: ${request.scope}`,
|
|
78
|
+
`Target: ${request.target}`,
|
|
79
|
+
request.projectContext?.language ? `Language: ${request.projectContext.language}` : '',
|
|
80
|
+
request.projectContext?.framework ? `Framework: ${request.projectContext.framework}` : '',
|
|
81
|
+
'',
|
|
82
|
+
request.description,
|
|
83
|
+
]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join('\n');
|
|
86
|
+
}
|
|
87
|
+
function parseGeneratedText(text, model) {
|
|
88
|
+
const technicalMatch = /\n## Technical\n([\s\S]*?)(?=\n## |\s*$)/.exec(`\n${text}`);
|
|
89
|
+
return {
|
|
90
|
+
specBody: text,
|
|
91
|
+
technicalSection: technicalMatch?.[1]?.trim() ?? '',
|
|
92
|
+
generatedWithModel: model,
|
|
93
|
+
generatedAt: new Date().toISOString(),
|
|
94
|
+
qualityWarnings: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=opus-generator.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class QualityValidator {
|
|
2
|
+
validate(result) {
|
|
3
|
+
const text = `${result.specBody}\n${result.technicalSection}`;
|
|
4
|
+
const warnings = [];
|
|
5
|
+
for (const heading of ['## Problem', '## Acceptance Criteria', '## Technical']) {
|
|
6
|
+
if (!text.includes(heading)) {
|
|
7
|
+
warnings.push(`Missing required generated section: ${heading}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const bddCount = (text.match(/\bGIVEN\b[\s\S]*?\bWHEN\b[\s\S]*?\bTHEN\b/gi) ?? []).length;
|
|
11
|
+
if (bddCount < 3) {
|
|
12
|
+
warnings.push('Generated spec should include at least 3 GIVEN/WHEN/THEN criteria.');
|
|
13
|
+
}
|
|
14
|
+
const signatureCount = (result.technicalSection.match(/\b(?:export\s+)?(?:interface|type|class|function)\s+\w+/g) ??
|
|
15
|
+
[]).length;
|
|
16
|
+
if (signatureCount < 2) {
|
|
17
|
+
warnings.push('Generated technical section should include at least 2 TypeScript signatures.');
|
|
18
|
+
}
|
|
19
|
+
return { passed: warnings.length === 0, warnings };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=quality-validator.js.map
|
|
@@ -25,7 +25,15 @@ export const PLANU_CANONICAL_POLICY = {
|
|
|
25
25
|
'planu/specs/*/implementation-brief.md',
|
|
26
26
|
'planu/specs/*/risk-register.md',
|
|
27
27
|
],
|
|
28
|
-
legacyMergeBeforeDeleteFiles: [
|
|
28
|
+
legacyMergeBeforeDeleteFiles: [
|
|
29
|
+
'technical.md',
|
|
30
|
+
'plan.md',
|
|
31
|
+
'PLAN.md',
|
|
32
|
+
'progress.md',
|
|
33
|
+
'HU.md',
|
|
34
|
+
'FICHA-TECNICA.md',
|
|
35
|
+
'PROGRESS.md',
|
|
36
|
+
],
|
|
29
37
|
};
|
|
30
38
|
const ROOT_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootFiles);
|
|
31
39
|
const ROOT_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootDirs);
|
|
@@ -22,7 +22,12 @@ async function gitRmCached(projectPath, relPath) {
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
25
|
-
|
|
25
|
+
const args = ['rm', '--cached', '--quiet', '--ignore-unmatch'];
|
|
26
|
+
if (await pathIsDirectory(join(projectPath, relPath))) {
|
|
27
|
+
args.push('-r');
|
|
28
|
+
}
|
|
29
|
+
args.push(relPath);
|
|
30
|
+
await execFileAsync('git', args, {
|
|
26
31
|
cwd: projectPath,
|
|
27
32
|
timeout: 5_000,
|
|
28
33
|
});
|
|
@@ -51,7 +56,11 @@ async function mergeLegacySpecFile(projectPath, specDir, fileName) {
|
|
|
51
56
|
readFile(legacyPath, 'utf-8'),
|
|
52
57
|
]);
|
|
53
58
|
const body = stripFrontmatter(legacyContent);
|
|
54
|
-
const section = fileName === 'progress.md'
|
|
59
|
+
const section = fileName === 'progress.md' || fileName === 'PROGRESS.md'
|
|
60
|
+
? 'Progress'
|
|
61
|
+
: fileName === 'technical.md' || fileName === 'FICHA-TECNICA.md'
|
|
62
|
+
? 'Technical'
|
|
63
|
+
: 'Files';
|
|
55
64
|
const merged = appendSectionIfMissing(specContent, section, body);
|
|
56
65
|
if (merged !== specContent) {
|
|
57
66
|
await atomicWriteFile(specPath, merged, {
|
|
@@ -29,7 +29,7 @@ function buildCompletenessItems(spec) {
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
id: 'cl-comp-2',
|
|
32
|
-
question: 'Is the technical design documented in
|
|
32
|
+
question: 'Is the technical design documented in spec.md under ## Technical?',
|
|
33
33
|
category: 'completeness',
|
|
34
34
|
answer: spec.technicalPath ? 'pending' : 'no',
|
|
35
35
|
autoChecked: false,
|
|
@@ -39,7 +39,7 @@ function getSddProcess() {
|
|
|
39
39
|
id: 'creating-spec',
|
|
40
40
|
name: '3. Create Spec',
|
|
41
41
|
tools: ['create_spec', 'reverse_engineer', 'estimate'],
|
|
42
|
-
description: 'Create a full SDD spec
|
|
42
|
+
description: 'Create a full SDD spec.md from requirements or ' +
|
|
43
43
|
'reverse-engineer from existing code. Estimate effort, cost, and tokens.',
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -11,7 +11,7 @@ function getBuiltInTemplates() {
|
|
|
11
11
|
{
|
|
12
12
|
id: 'feature',
|
|
13
13
|
name: 'Feature Spec',
|
|
14
|
-
description: 'Full feature specification
|
|
14
|
+
description: 'Full feature specification using unified spec.md sections',
|
|
15
15
|
type: 'feature',
|
|
16
16
|
sections: [
|
|
17
17
|
'metadata',
|
|
@@ -19,7 +19,7 @@ import { generateLeanSpecContent } from '../engine/spec-format/lean-spec-generat
|
|
|
19
19
|
import { generateLeanTechnicalContent } from '../engine/spec-format/lean-technical-generator.js';
|
|
20
20
|
import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
|
|
21
21
|
import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
|
|
22
|
-
import { FallbackGenerator } from '../engine/spec-generator/index.js';
|
|
22
|
+
import { ApiKeyResolver, FallbackGenerator, OpusGenerator, } from '../engine/spec-generator/index.js';
|
|
23
23
|
import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
|
|
24
24
|
import { AutopilotSummaryCollector } from '../engine/autopilot/summary-collector.js';
|
|
25
25
|
import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
|
|
@@ -535,7 +535,10 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
535
535
|
.filter(Boolean)
|
|
536
536
|
.join('\n')
|
|
537
537
|
: '';
|
|
538
|
-
const
|
|
538
|
+
const anthropicKey = await new ApiKeyResolver().resolveAnthropicKey(params.projectPath ?? '');
|
|
539
|
+
const specGenerator = anthropicKey !== undefined
|
|
540
|
+
? new OpusGenerator({ apiKey: anthropicKey })
|
|
541
|
+
: new FallbackGenerator();
|
|
539
542
|
const generatedSpec = await measureStep('generateSpecBody', () => specGenerator.generate({
|
|
540
543
|
title: spec.title,
|
|
541
544
|
description: `${description}${contractNote}`,
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -17,4 +17,21 @@ export interface CliCommand {
|
|
|
17
17
|
/** Execute the command with parsed args */
|
|
18
18
|
run(args: string[], flags?: GlobalFlags): Promise<void>;
|
|
19
19
|
}
|
|
20
|
+
export interface StatusCommandValues {
|
|
21
|
+
set?: string;
|
|
22
|
+
'project-id'?: string;
|
|
23
|
+
'project-path'?: string;
|
|
24
|
+
notes?: string;
|
|
25
|
+
'model-id'?: string;
|
|
26
|
+
'model-tier-used'?: string;
|
|
27
|
+
'context-hash'?: string;
|
|
28
|
+
'handoff-path'?: string;
|
|
29
|
+
'handoff-artifact-id'?: string;
|
|
30
|
+
'reviewed-by'?: string;
|
|
31
|
+
'arbitrated-by'?: string;
|
|
32
|
+
reason?: string;
|
|
33
|
+
force?: boolean;
|
|
34
|
+
'force-status'?: boolean;
|
|
35
|
+
'force-status-reason'?: string;
|
|
36
|
+
}
|
|
20
37
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -228,7 +228,6 @@ export * from './spec-from-issue.js';
|
|
|
228
228
|
export * from './plan-mode.js';
|
|
229
229
|
export * from './self-healing.js';
|
|
230
230
|
export * from './deploy.js';
|
|
231
|
-
export * from './legacy.js';
|
|
232
231
|
export * from './multi-agent-review.js';
|
|
233
232
|
export * from './delete-first.js';
|
|
234
233
|
export * from './auto-checkpoint.js';
|
package/dist/types/index.js
CHANGED
|
@@ -225,7 +225,6 @@ export * from './spec-from-issue.js';
|
|
|
225
225
|
export * from './plan-mode.js';
|
|
226
226
|
export * from './self-healing.js';
|
|
227
227
|
export * from './deploy.js';
|
|
228
|
-
export * from './legacy.js';
|
|
229
228
|
export * from './multi-agent-review.js';
|
|
230
229
|
export * from './delete-first.js';
|
|
231
230
|
export * from './auto-checkpoint.js';
|
|
@@ -32,7 +32,7 @@ export interface LeanSpecInput {
|
|
|
32
32
|
/** SPEC-481: Acceptance criteria format. Defaults to 'checkbox'. */
|
|
33
33
|
acFormat?: 'checkbox' | 'bdd';
|
|
34
34
|
}
|
|
35
|
-
/** A file entry in the
|
|
35
|
+
/** A file entry in the unified spec.md Technical section. */
|
|
36
36
|
export interface LeanFileEntry {
|
|
37
37
|
path: string;
|
|
38
38
|
status: 'pending' | 'done';
|
|
@@ -102,7 +102,7 @@ export interface ReplaceSectionResult {
|
|
|
102
102
|
replaced: boolean;
|
|
103
103
|
appended: boolean;
|
|
104
104
|
}
|
|
105
|
-
/** Input for lean
|
|
105
|
+
/** Input for lean spec.md Technical section generation. */
|
|
106
106
|
export interface LeanTechnicalInput {
|
|
107
107
|
specId: string;
|
|
108
108
|
filesToCreate?: LeanFileEntry[];
|
|
@@ -23,4 +23,42 @@ export interface SpecGenerationResult {
|
|
|
23
23
|
export interface SpecContentGenerator {
|
|
24
24
|
generate(request: SpecGenerationRequest): Promise<SpecGenerationResult>;
|
|
25
25
|
}
|
|
26
|
+
export interface SpecQualityValidation {
|
|
27
|
+
passed: boolean;
|
|
28
|
+
warnings: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface SpecGeneratorMessagesClient {
|
|
31
|
+
create(body: {
|
|
32
|
+
model: string;
|
|
33
|
+
max_tokens: number;
|
|
34
|
+
temperature: number;
|
|
35
|
+
system: string;
|
|
36
|
+
messages: {
|
|
37
|
+
role: 'user';
|
|
38
|
+
content: string;
|
|
39
|
+
}[];
|
|
40
|
+
}, options?: {
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
content: {
|
|
44
|
+
type: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
}[];
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export interface OpusGeneratorDeps {
|
|
50
|
+
apiKey: string;
|
|
51
|
+
model?: string;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
client?: SpecGeneratorMessagesClient;
|
|
54
|
+
fallback?: SpecContentGenerator;
|
|
55
|
+
validator?: {
|
|
56
|
+
validate(result: SpecGenerationResult): SpecQualityValidation;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export interface ApiKeyConfig {
|
|
60
|
+
anthropic?: {
|
|
61
|
+
apiKey?: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
26
64
|
//# sourceMappingURL=spec-generator.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.11",
|
|
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",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"packageName": "@planu/core"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "4.3.
|
|
36
|
-
"@planu/core-darwin-x64": "4.3.
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "4.3.
|
|
38
|
-
"@planu/core-linux-arm64-musl": "4.3.
|
|
39
|
-
"@planu/core-linux-x64-gnu": "4.3.
|
|
40
|
-
"@planu/core-linux-x64-musl": "4.3.
|
|
41
|
-
"@planu/core-win32-arm64-msvc": "4.3.
|
|
42
|
-
"@planu/core-win32-x64-msvc": "4.3.
|
|
35
|
+
"@planu/core-darwin-arm64": "4.3.11",
|
|
36
|
+
"@planu/core-darwin-x64": "4.3.11",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.3.11",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.3.11",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.3.11",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.3.11",
|
|
41
|
+
"@planu/core-win32-arm64-msvc": "4.3.11",
|
|
42
|
+
"@planu/core-win32-x64-msvc": "4.3.11"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=24.0.0"
|
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
],
|
|
128
128
|
"license": "SEE LICENSE IN LICENSE",
|
|
129
129
|
"dependencies": {
|
|
130
|
+
"@anthropic-ai/sdk": "^0.98.0",
|
|
130
131
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
131
132
|
"glob": "^13.0.6",
|
|
132
133
|
"yaml": "^2.9.0",
|
|
@@ -178,7 +179,7 @@
|
|
|
178
179
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
179
180
|
"@stryker-mutator/core": "^9.6.1",
|
|
180
181
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
181
|
-
"@supabase/supabase-js": "^2.106.
|
|
182
|
+
"@supabase/supabase-js": "^2.106.2",
|
|
182
183
|
"@types/node": "^25.9.1",
|
|
183
184
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
184
185
|
"@vitest/coverage-v8": "^4.1.7",
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { PlanuContextGraphProvider, createContextGraphProvider } from './context-graph-provider.js';
|
|
2
|
-
export { getSensitivePathRefusal, collectPreservedFragments, verifyPreservedFragments, shouldBypassLeanMode, } from './compression-guards.js';
|
|
3
|
-
export { compressSafeContext } from './safe-context-compressor.js';
|
|
4
|
-
export { evaluateContextModes, summarizeContextEval } from './eval-harness.js';
|
|
5
|
-
export { emptyImpactMap, estimateImpactBudget, mergeImpactMaps } from './impact-map.js';
|
|
6
|
-
export type { ContextEvalCase, ContextEvalMode, ContextEvalResult, ContextGraphInput, ContextGraphProvider, ContextImpactEdge, ContextImpactFile, ContextImpactMap, ContextImpactSymbol, SafeCompressionResult, SafeContextCompressionInput, } from '../../types/context-intelligence.js';
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { PlanuContextGraphProvider, createContextGraphProvider } from './context-graph-provider.js';
|
|
2
|
-
export { getSensitivePathRefusal, collectPreservedFragments, verifyPreservedFragments, shouldBypassLeanMode, } from './compression-guards.js';
|
|
3
|
-
export { compressSafeContext } from './safe-context-compressor.js';
|
|
4
|
-
export { evaluateContextModes, summarizeContextEval } from './eval-harness.js';
|
|
5
|
-
export { emptyImpactMap, estimateImpactBudget, mergeImpactMaps } from './impact-map.js';
|
|
6
|
-
//# sourceMappingURL=index.js.map
|