@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/cli/commands/status.js +79 -46
  3. package/dist/engine/pr-description-generator.js +5 -2
  4. package/dist/engine/spec-generator/api-key-resolver.d.ts +4 -0
  5. package/dist/engine/spec-generator/api-key-resolver.js +31 -0
  6. package/dist/engine/spec-generator/index.d.ts +3 -0
  7. package/dist/engine/spec-generator/index.js +3 -0
  8. package/dist/engine/spec-generator/opus-generator.d.ts +12 -0
  9. package/dist/engine/spec-generator/opus-generator.js +97 -0
  10. package/dist/engine/spec-generator/quality-validator.d.ts +5 -0
  11. package/dist/engine/spec-generator/quality-validator.js +22 -0
  12. package/dist/engine/spec-migrator/planu-canonical-policy.js +9 -1
  13. package/dist/engine/spec-migrator/strict-planu-cleanup.js +11 -2
  14. package/dist/engine/validator/checklist.js +1 -1
  15. package/dist/resources/process.js +1 -1
  16. package/dist/resources/templates.js +1 -1
  17. package/dist/tools/create-spec.js +5 -2
  18. package/dist/types/cli.d.ts +17 -0
  19. package/dist/types/index.d.ts +0 -1
  20. package/dist/types/index.js +0 -1
  21. package/dist/types/spec-format.d.ts +2 -2
  22. package/dist/types/spec-generator.d.ts +38 -0
  23. package/package.json +11 -10
  24. package/dist/engine/context-intelligence/index.d.ts +0 -7
  25. package/dist/engine/context-intelligence/index.js +0 -6
  26. package/dist/engine/legacy/ast-analyzer.d.ts +0 -21
  27. package/dist/engine/legacy/ast-analyzer.js +0 -113
  28. package/dist/engine/legacy/characterization-generator.d.ts +0 -6
  29. package/dist/engine/legacy/characterization-generator.js +0 -146
  30. package/dist/engine/legacy/hyrum-scanner.d.ts +0 -6
  31. package/dist/engine/legacy/hyrum-scanner.js +0 -137
  32. package/dist/engine/legacy/safety-net-orchestrator.d.ts +0 -7
  33. package/dist/engine/legacy/safety-net-orchestrator.js +0 -114
  34. package/dist/engine/legacy/seam-finder.d.ts +0 -6
  35. package/dist/engine/legacy/seam-finder.js +0 -139
  36. package/dist/tools/legacy/characterize-legacy-code.d.ts +0 -9
  37. package/dist/tools/legacy/characterize-legacy-code.js +0 -63
  38. package/dist/tools/legacy/detect-hyrum-risks.d.ts +0 -8
  39. package/dist/tools/legacy/detect-hyrum-risks.js +0 -47
  40. package/dist/tools/legacy/refactor-with-safety-net.d.ts +0 -10
  41. package/dist/tools/legacy/refactor-with-safety-net.js +0 -55
  42. package/dist/tools/legacy/seams-detector.d.ts +0 -8
  43. package/dist/tools/legacy/seams-detector.js +0 -47
  44. package/dist/types/legacy.d.ts +0 -151
  45. 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 } = parseArgs({
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
- process.stderr.write(`${red('Error:')} Spec ID is required.\nUsage: ${statusCommand.usage}\n`);
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
- process.stderr.write(`${red('Error:')} --set <status> is required.\nUsage: ${statusCommand.usage}\n`);
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
- const result = await handleUpdateStatusBatch({
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
- const result = await handleUpdateStatus({
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.technicalPath}\``);
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 technical spec at \`${spec.technicalPath}\` for implementation details.`);
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,4 @@
1
+ export declare class ApiKeyResolver {
2
+ resolveAnthropicKey(projectPath: string): Promise<string | undefined>;
3
+ }
4
+ //# sourceMappingURL=api-key-resolver.d.ts.map
@@ -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
@@ -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.js.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,5 @@
1
+ import type { SpecGenerationResult, SpecQualityValidation } from '../../types/index.js';
2
+ export declare class QualityValidator {
3
+ validate(result: SpecGenerationResult): SpecQualityValidation;
4
+ }
5
+ //# sourceMappingURL=quality-validator.d.ts.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: ['technical.md', 'plan.md', 'PLAN.md', 'progress.md'],
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
- await execFileAsync('git', ['rm', '--cached', '--quiet', '--ignore-unmatch', relPath], {
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' ? 'Progress' : fileName === 'technical.md' ? 'Technical' : 'Files';
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 FICHA-TECNICA?',
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 (HU.md + FICHA-TECNICA.md) from requirements or ' +
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 with HU.md and FICHA-TECNICA.md',
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 specGenerator = new FallbackGenerator();
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}`,
@@ -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
@@ -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';
@@ -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 lean technical.md. */
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 technical.md generation. */
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.9",
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.9",
36
- "@planu/core-darwin-x64": "4.3.9",
37
- "@planu/core-linux-arm64-gnu": "4.3.9",
38
- "@planu/core-linux-arm64-musl": "4.3.9",
39
- "@planu/core-linux-x64-gnu": "4.3.9",
40
- "@planu/core-linux-x64-musl": "4.3.9",
41
- "@planu/core-win32-arm64-msvc": "4.3.9",
42
- "@planu/core-win32-x64-msvc": "4.3.9"
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.1",
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