@planu/cli 4.4.1 → 4.4.3

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 (32) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/dist/engine/evidence-gates/artifact-reader.js +5 -31
  3. package/dist/engine/implementation-contract/common.d.ts +5 -0
  4. package/dist/engine/implementation-contract/common.js +30 -0
  5. package/dist/engine/implementation-contract/evaluator.d.ts +3 -0
  6. package/dist/engine/implementation-contract/evaluator.js +105 -0
  7. package/dist/engine/implementation-contract/index.d.ts +4 -0
  8. package/dist/engine/implementation-contract/index.js +4 -0
  9. package/dist/engine/implementation-contract/renderer.d.ts +4 -0
  10. package/dist/engine/implementation-contract/renderer.js +98 -0
  11. package/dist/engine/readiness-checker.js +18 -6
  12. package/dist/engine/skill-registry/index.d.ts +1 -0
  13. package/dist/engine/skill-registry/index.js +1 -0
  14. package/dist/engine/skill-registry/installer.d.ts +2 -2
  15. package/dist/engine/skill-registry/installer.js +69 -37
  16. package/dist/engine/skill-registry/skill-security-scanner.d.ts +12 -0
  17. package/dist/engine/skill-registry/skill-security-scanner.js +87 -0
  18. package/dist/engine/spec-format/bdd-parser.js +9 -0
  19. package/dist/engine/spec-format/lean-spec-generator.js +10 -2
  20. package/dist/engine/spec-migrator/planu-canonical-policy.js +3 -3
  21. package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
  22. package/dist/tools/challenge-spec/implementation-contract-challenge-scenarios.d.ts +3 -0
  23. package/dist/tools/challenge-spec/implementation-contract-challenge-scenarios.js +51 -0
  24. package/dist/tools/challenge-spec.js +4 -0
  25. package/dist/tools/create-spec.js +19 -2
  26. package/dist/tools/skill-registry/install.js +9 -0
  27. package/dist/tools/update-status/evidence-gate.js +2 -2
  28. package/dist/types/skill-registry.d.ts +59 -0
  29. package/dist/types/spec-format.d.ts +21 -0
  30. package/package.json +16 -16
  31. package/planu-native.json +8 -29
  32. package/planu-plugin.json +7 -35
@@ -0,0 +1,87 @@
1
+ // engine/skill-registry/skill-security-scanner.ts — SPEC-1081
2
+ // Optional SkillSpector adapter for scanning external AI agent skills.
3
+ import { execFile } from 'node:child_process';
4
+ const SCANNER = 'skillspector';
5
+ const COMMAND = 'skillspector scan <skillDir> --no-llm --format json';
6
+ export class SkillSecurityScannerUnavailableError extends Error {
7
+ constructor(skillName, cause) {
8
+ const detail = cause instanceof Error ? cause.message : String(cause);
9
+ super(`Skill security scanner unavailable for "${skillName}". Command: ${COMMAND}. ${detail}`);
10
+ this.name = 'SkillSecurityScannerUnavailableError';
11
+ }
12
+ }
13
+ function normalizeSeverity(value) {
14
+ if (value === 'LOW' || value === 'MEDIUM' || value === 'HIGH' || value === 'CRITICAL') {
15
+ return value;
16
+ }
17
+ return 'UNKNOWN';
18
+ }
19
+ function normalizeRecommendation(value) {
20
+ if (value === 'SAFE' || value === 'CAUTION' || value === 'DO_NOT_INSTALL') {
21
+ return value;
22
+ }
23
+ return 'UNKNOWN';
24
+ }
25
+ function normalizeScore(value) {
26
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
27
+ return 100;
28
+ }
29
+ return Math.max(0, Math.min(100, value));
30
+ }
31
+ function parseReport(stdout, skillName) {
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(stdout);
35
+ }
36
+ catch (err) {
37
+ throw new SkillSecurityScannerUnavailableError(skillName, err);
38
+ }
39
+ const risk = parsed.risk_assessment ?? {};
40
+ const version = parsed.metadata?.skillspector_version;
41
+ return {
42
+ scanner: SCANNER,
43
+ scannerVersion: typeof version === 'string' ? version : undefined,
44
+ scannedAt: new Date().toISOString(),
45
+ score: normalizeScore(risk.score),
46
+ severity: normalizeSeverity(risk.severity),
47
+ recommendation: normalizeRecommendation(risk.recommendation),
48
+ issuesCount: Array.isArray(parsed.issues) ? parsed.issues.length : 0,
49
+ command: COMMAND,
50
+ available: true,
51
+ };
52
+ }
53
+ function runSkillSpector(skillDir) {
54
+ return new Promise((resolve, reject) => {
55
+ execFile(SCANNER, ['scan', skillDir, '--no-llm', '--format', 'json'], (err, stdout) => {
56
+ if (err) {
57
+ const error = err instanceof Error ? err : new Error('Unknown execFile error');
58
+ reject(Object.assign(error, { stdout }));
59
+ return;
60
+ }
61
+ resolve(stdout);
62
+ });
63
+ });
64
+ }
65
+ /**
66
+ * Scan a prepared skill directory using SkillSpector static analysis only.
67
+ *
68
+ * SkillSpector returns exit code 1 for high-risk reports. Planu still parses stdout
69
+ * in that case because the report is valid and should drive the gate decision.
70
+ */
71
+ export async function scanSkillSecurity(skillDir, skillName) {
72
+ try {
73
+ const stdout = await runSkillSpector(skillDir);
74
+ return parseReport(stdout, skillName);
75
+ }
76
+ catch (err) {
77
+ if (typeof err === 'object' &&
78
+ err !== null &&
79
+ 'stdout' in err &&
80
+ typeof err.stdout === 'string' &&
81
+ err.stdout.trim().length > 0) {
82
+ return parseReport(err.stdout, skillName);
83
+ }
84
+ throw new SkillSecurityScannerUnavailableError(skillName, err);
85
+ }
86
+ }
87
+ //# sourceMappingURL=skill-security-scanner.js.map
@@ -83,6 +83,15 @@ export function renderBddScenariosYaml(scenarios) {
83
83
  for (const scenario of scenarios) {
84
84
  lines.push(` - title: "${escapeYaml(scenario.title)}"`);
85
85
  lines.push(` done: ${String(scenario.done)}`);
86
+ if (scenario.tests && scenario.tests.length > 0) {
87
+ lines.push(` tests:`);
88
+ for (const test of scenario.tests) {
89
+ lines.push(` - path: ${test.path}`);
90
+ if (test.line !== undefined) {
91
+ lines.push(` line: ${String(test.line)}`);
92
+ }
93
+ }
94
+ }
86
95
  lines.push(` steps:`);
87
96
  for (const step of scenario.steps) {
88
97
  lines.push(` - keyword: ${step.keyword}`);
@@ -55,7 +55,7 @@ export function generateLeanSpecContent(input) {
55
55
  ];
56
56
  let acLines;
57
57
  if (acFormat === 'bdd') {
58
- acLines = buildBddLines(description, extraCriteria, input.criteriaOverride);
58
+ acLines = buildBddLines(description, extraCriteria, input.criteriaOverride, input.scenarioTestPaths ?? []);
59
59
  }
60
60
  else {
61
61
  acLines = buildCheckboxLines(description, extraCriteria, input.criteriaOverride);
@@ -88,7 +88,7 @@ function buildCheckboxLines(description, extraCriteria, criteriaOverride) {
88
88
  ];
89
89
  }
90
90
  /** Build BDD scenario lines. */
91
- function buildBddLines(description, extraCriteria, criteriaOverride) {
91
+ function buildBddLines(description, extraCriteria, criteriaOverride, scenarioTestPaths) {
92
92
  let scenarios = parseBddScenarios(description);
93
93
  // Fallback: convert checkbox criteria to BDD scenarios
94
94
  if (scenarios.length === 0) {
@@ -108,6 +108,14 @@ function buildBddLines(description, extraCriteria, criteriaOverride) {
108
108
  });
109
109
  }
110
110
  }
111
+ if (scenarioTestPaths.length > 0) {
112
+ scenarios = scenarios.map((scenario) => ({
113
+ ...scenario,
114
+ tests: scenario.tests && scenario.tests.length > 0
115
+ ? scenario.tests
116
+ : scenarioTestPaths.map((path) => ({ path })),
117
+ }));
118
+ }
111
119
  return renderBddScenariosYaml(scenarios);
112
120
  }
113
121
  /** Extract acceptance criteria from description. Looks for checkbox patterns or generates a default.
@@ -4,7 +4,7 @@ export const PLANU_CANONICAL_POLICY = {
4
4
  canonicalRootFiles: ['conventions.json', 'context.md', 'session-context.md', 'session.json'],
5
5
  canonicalRootDirs: ['releases', 'specs'],
6
6
  canonicalSpecFiles: ['spec.md'],
7
- canonicalSpecDirs: ['evidence'],
7
+ canonicalSpecDirs: [],
8
8
  forbiddenHostAssetRootDirs: ['agents', 'skills', 'rules', 'hooks'],
9
9
  generatedRuntimePatterns: [
10
10
  'planu/index.html',
@@ -71,8 +71,8 @@ export function canonicalContractText() {
71
71
  ' specs/',
72
72
  ' SPEC-XXX-slug/',
73
73
  ' spec.md',
74
- ' evidence/',
75
- ' *.json',
74
+ '',
75
+ 'Evidence artifacts are Planu runtime data and are stored outside planu/specs/.',
76
76
  '',
77
77
  'Host adapters are written outside planu/:',
78
78
  ' Claude Code: .claude/agents, .claude/skills, .claude/rules',
@@ -28,7 +28,7 @@ For non-trivial specs, Planu blocks lifecycle transitions unless evidence is pre
28
28
  | done | Traceability matrix covering every acceptance criterion |
29
29
  | done for API/UI/event/MCP work | Passing contract validation evidence |
30
30
 
31
- Evidence can live under \`planu/specs/<spec>/evidence/\` or the external Planu handoff store. Trivial specs may use lightweight evidence, but \`done\` still needs at least one traceability row.
31
+ Evidence lives in the external Planu handoff store, not under \`planu/specs/<spec>/\`. Spec folders remain \`spec.md\`-only. Trivial specs may use lightweight evidence, but \`done\` still needs at least one traceability row.
32
32
 
33
33
  ## When to use \`facilitate\`
34
34
 
@@ -0,0 +1,3 @@
1
+ import type { FailureScenario, Spec } from '../../types/index.js';
2
+ export declare function generateImplementationContractChallengeScenarios(spec: Spec, specContent: string): FailureScenario[];
3
+ //# sourceMappingURL=implementation-contract-challenge-scenarios.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { evaluateImplementationContract } from '../../engine/implementation-contract/index.js';
2
+ import { parseFrontmatterScenarios } from '../../engine/validator/spec-compliance-runner.js';
3
+ export function generateImplementationContractChallengeScenarios(spec, specContent) {
4
+ const criteria = parseFrontmatterScenarios(specContent).map((scenario) => scenario.title);
5
+ const issues = evaluateImplementationContract(stripFrontmatter(specContent), criteria);
6
+ if (issues.length === 0) {
7
+ return [];
8
+ }
9
+ const missingEdges = !/\b(edge cases?|failure modes?)\b/i.test(specContent);
10
+ const missingBoundaries = !/\b(no-touch|forbidden|non-goals?|out of scope)\b/i.test(specContent);
11
+ const scenarios = [];
12
+ scenarios.push({
13
+ scenario: `[${spec.id}] Implementation Contract Gap: spec leaves the implementer to infer required behavior, file scope, verification, or missing decisions.`,
14
+ probability: 'high',
15
+ impact: 'high',
16
+ currentHandling: issues
17
+ .map((issue) => issue.message)
18
+ .slice(0, 3)
19
+ .join('; '),
20
+ requiredHandling: 'Add or repair ## Implementation Contract before coding: user outcome, behavior contract, file-level work plan, acceptance-to-verification map, edge cases, non-goals, and verification commands.',
21
+ dataConsistency: 'Do not start implementation until the spec states what persistent files or state may change.',
22
+ userExperience: 'The planner should ask for concrete missing decisions instead of sending an ambiguous spec to the implementer.',
23
+ });
24
+ if (missingEdges) {
25
+ scenarios.push({
26
+ scenario: `[${spec.id}] Missing Edge Cases: implementation may pass happy-path criteria while failing required failure modes.`,
27
+ probability: 'medium',
28
+ impact: 'high',
29
+ currentHandling: 'Edge cases and failure modes are not explicit in the spec.',
30
+ requiredHandling: 'Add concrete edge cases and failure modes to ## Implementation Contract before approval.',
31
+ dataConsistency: 'State-changing behavior must define rollback or no-write behavior for failures.',
32
+ userExperience: 'Users should not discover undefined behavior during implementation review.',
33
+ });
34
+ }
35
+ if (missingBoundaries) {
36
+ scenarios.push({
37
+ scenario: `[${spec.id}] Missing No-Touch Boundaries: implementer may refactor unrelated code or satisfy the wording with an architecture-violating shortcut.`,
38
+ probability: 'medium',
39
+ impact: 'high',
40
+ currentHandling: 'No non-goals, forbidden approaches, or no-touch areas are explicit.',
41
+ requiredHandling: 'Add non-goals, forbidden approaches, and no-touch areas to the implementation contract.',
42
+ dataConsistency: 'Unrelated persistent formats and stores must not change without explicit scope.',
43
+ userExperience: 'The implementer receives bounded work instead of open-ended refactoring permission.',
44
+ });
45
+ }
46
+ return scenarios;
47
+ }
48
+ function stripFrontmatter(raw) {
49
+ return raw.replace(/^---\n[\s\S]*?\n---\n?/, '');
50
+ }
51
+ //# sourceMappingURL=implementation-contract-challenge-scenarios.js.map
@@ -14,6 +14,7 @@ import { prioritizeScenarios, buildPrioritizedSummary } from '../engine/challeng
14
14
  import { checkContradictions as checkScopeContradictions } from '../engine/scope-boundaries/index.js';
15
15
  import { buildChallengeSpecSummary } from '../engine/human-summary.js';
16
16
  import { generateAgentChallengeScenarios, isAgentSpec, } from './challenge-spec/agent-challenge-scenarios.js';
17
+ import { generateImplementationContractChallengeScenarios } from './challenge-spec/implementation-contract-challenge-scenarios.js';
17
18
  import { getPlatformChallenges } from './challenge-spec/platform-challenge-scenarios.js';
18
19
  import { generateSecurityChallengeScenarios } from './challenge-spec/security-challenge-scenarios.js';
19
20
  import { generatePrivacyChallengeScenarios } from './challenge-spec/privacy-challenge-scenarios.js';
@@ -150,6 +151,9 @@ export async function handleChallengeSpec(args, server) {
150
151
  if (isAgentSpec(spec, specContent)) {
151
152
  failureScenarios.push(...generateAgentChallengeScenarios(spec, specContent, knowledge));
152
153
  }
154
+ if (focusAreas.includes('failures')) {
155
+ failureScenarios.push(...generateImplementationContractChallengeScenarios(spec, specContent));
156
+ }
153
157
  // 5h. SPEC-015a: Smart contract, bot, and IoT challenges
154
158
  failureScenarios.push(...getPlatformChallenges(spec, specContent, knowledge));
155
159
  // 5i. SPEC-029: Security authorization and STRIDE challenges
@@ -20,6 +20,7 @@ import { extractCriteria, generateLeanSpecContent, } from '../engine/spec-format
20
20
  import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-technical-generator.js';
21
21
  import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-populator.js';
22
22
  import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
23
+ import { appendImplementationContractIfMissing } from '../engine/implementation-contract/index.js';
23
24
  import { validateEnglishOnlySpecText } from '../engine/spec-language/english-only.js';
24
25
  import { ApiKeyResolver, FallbackGenerator, OpusGenerator, } from '../engine/spec-generator/index.js';
25
26
  import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
@@ -666,6 +667,8 @@ export async function handleCreateSpec(inputParams, server) {
666
667
  userInput: description,
667
668
  autopilot,
668
669
  }));
670
+ const outOfScopeResolved = resolveOutOfScope(description, params.outOfScope);
671
+ const scenarioTestPaths = groundedTechnical.files.test.map((file) => file.path);
669
672
  const leanSpec = generateLeanSpecContent({
670
673
  spec,
671
674
  description: generatedSpec.specBody,
@@ -677,6 +680,7 @@ export async function handleCreateSpec(inputParams, server) {
677
680
  groundingCriteria,
678
681
  groundingTechnicalReferences: groundedTechnical.records,
679
682
  acFormat: params.acFormat,
683
+ scenarioTestPaths,
680
684
  });
681
685
  const leanTechnical = generateLeanTechnicalContent({
682
686
  specId: spec.id,
@@ -687,7 +691,21 @@ export async function handleCreateSpec(inputParams, server) {
687
691
  // SPEC-709: write unified spec.md from origin — no separate technical.md.
688
692
  // The legacy two-file output is preserved by appending the technical body
689
693
  // as a `## Technical` section inside spec.md.
690
- const unifiedSpec = buildUnifiedSpecContent(leanSpec, leanTechnical);
694
+ const unifiedWithoutContract = buildUnifiedSpecContent(leanSpec, leanTechnical);
695
+ const unifiedSpec = appendImplementationContractIfMissing(unifiedWithoutContract, {
696
+ description: generatedSpec.specBody,
697
+ criteria: contractCriteria.map((record) => ({ text: record.text, done: false })),
698
+ files: groundedTechnical.files,
699
+ outOfScope: outOfScopeResolved.items,
700
+ verificationCommands: [
701
+ ...(scenarioTestPaths.length > 0
702
+ ? [`pnpm vitest run ${scenarioTestPaths.join(' ')}`]
703
+ : []),
704
+ 'pnpm typecheck',
705
+ 'pnpm lint',
706
+ 'pnpm test',
707
+ ],
708
+ });
691
709
  const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
692
710
  if (!genericOutputGate.passed) {
693
711
  return {
@@ -725,7 +743,6 @@ export async function handleCreateSpec(inputParams, server) {
725
743
  throw writeErr;
726
744
  }
727
745
  // SPEC-612: Auto-suggest outOfScope when user did not provide any
728
- const outOfScopeResolved = resolveOutOfScope(description, params.outOfScope);
729
746
  if (outOfScopeResolved.items.length > 0) {
730
747
  spec.outOfScope = outOfScopeResolved.items;
731
748
  }
@@ -36,6 +36,15 @@ export async function handleSkillInstall(params) {
36
36
  if (result.version) {
37
37
  lines.push(`Version: ${result.version}`);
38
38
  }
39
+ if (result.securityScan) {
40
+ lines.push('');
41
+ if (result.securityScan.available) {
42
+ lines.push(`Security scan: ${result.securityScan.severity} (${result.securityScan.recommendation}) — score ${String(result.securityScan.score)}/100`);
43
+ }
44
+ else {
45
+ lines.push(`Security scan: unavailable (${result.securityScan.recommendation})`);
46
+ }
47
+ }
39
48
  if (splitInfo.splitApplied) {
40
49
  lines.push('');
41
50
  lines.push(`Note: Skill was split into ${String(splitInfo.referenceFiles.length + 1)} files for context efficiency (progressive disclosure).`);
@@ -80,7 +80,7 @@ function evidenceGateError(args) {
80
80
  transition: args.transition,
81
81
  issues: args.issues,
82
82
  requiredContractKinds: args.requiredContractKinds,
83
- fixHint: 'Add the missing evidence artifact under planu/specs/<spec>/evidence/ or external Planu project data handoffs/<specId>/, then retry the transition.',
83
+ fixHint: 'Add the missing evidence artifact to the external Planu project data handoff store, then retry the transition. Do not create JSON files under planu/specs/<spec>/.',
84
84
  }, null, 2),
85
85
  },
86
86
  ],
@@ -95,7 +95,7 @@ function evidenceGateError(args) {
95
95
  issues: args.issues,
96
96
  requiredContractKinds: args.requiredContractKinds,
97
97
  },
98
- fixHint: 'Add discovery.json, task-plan.json, traceability-matrix.json, and contract-validation-*.json as required for this transition.',
98
+ fixHint: 'Add discovery.json, task-plan.json, traceability-matrix.json, and contract-validation-*.json to the external Planu handoff store as required for this transition. Spec folders remain spec.md-only.',
99
99
  },
100
100
  };
101
101
  }
@@ -69,6 +69,63 @@ export interface SkillInstallResult {
69
69
  hasScripts: boolean;
70
70
  /** Installed version, if available. */
71
71
  version?: string;
72
+ /** Security scan summary for this install, when the skill source was scanned. */
73
+ securityScan?: SkillSecurityScanResult;
74
+ }
75
+ /** Severity labels returned by the skill security scanner. */
76
+ export type SkillSecuritySeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | 'UNKNOWN';
77
+ /** Recommendation labels returned by the skill security scanner. */
78
+ export type SkillSecurityRecommendation = 'SAFE' | 'CAUTION' | 'DO_NOT_INSTALL' | 'UNKNOWN';
79
+ /** Policy to use when the external skill security scanner cannot run. */
80
+ export type SkillSecurityScannerUnavailablePolicy = 'fail-closed' | 'allow';
81
+ /** Options controlling skill security scanning during installation. */
82
+ export interface SkillInstallOptions {
83
+ /** Policy for scanner execution failures. Defaults to fail-closed for external skills. */
84
+ scannerUnavailablePolicy?: SkillSecurityScannerUnavailablePolicy;
85
+ }
86
+ /** In-memory skill content prepared before writing to host skill directories. */
87
+ export interface PreparedSkillInstallContent {
88
+ /** SKILL.md content to write or scan. */
89
+ content: string;
90
+ /** Whether the source skill advertises executable script files. */
91
+ hasScripts: boolean;
92
+ /** Installed version, if available. */
93
+ version?: string;
94
+ }
95
+ /** Normalized security scan summary persisted by Planu. */
96
+ export interface SkillSecurityScanResult {
97
+ /** Scanner name. */
98
+ scanner: 'skillspector';
99
+ /** Scanner version, if reported by the scanner. */
100
+ scannerVersion?: string;
101
+ /** ISO 8601 timestamp when Planu ran or recorded the scan. */
102
+ scannedAt: string;
103
+ /** Numeric risk score, 0-100. */
104
+ score: number;
105
+ /** Risk severity label. */
106
+ severity: SkillSecuritySeverity;
107
+ /** Scanner recommendation. */
108
+ recommendation: SkillSecurityRecommendation;
109
+ /** Number of findings/issues reported by the scanner. */
110
+ issuesCount: number;
111
+ /** Exact scanner command shape used, without secrets or environment values. */
112
+ command: string;
113
+ /** Whether the scanner completed successfully. */
114
+ available: boolean;
115
+ /** Optional failure reason when available=false. */
116
+ error?: string;
117
+ }
118
+ /** Minimal JSON shape emitted by SkillSpector in --format json mode. */
119
+ export interface SkillSpectorJsonReport {
120
+ risk_assessment?: {
121
+ score?: unknown;
122
+ severity?: unknown;
123
+ recommendation?: unknown;
124
+ };
125
+ issues?: unknown[];
126
+ metadata?: {
127
+ skillspector_version?: unknown;
128
+ };
72
129
  }
73
130
  /** Manifest tracking all skills installed in a project. */
74
131
  export interface SkillManifest {
@@ -95,6 +152,8 @@ export interface InstalledSkillEntry {
95
152
  sourceStatus?: 'active' | 'source-unavailable';
96
153
  /** Absolute path to the installed skill directory. */
97
154
  path: string;
155
+ /** Security scan summary captured during installation, when available. */
156
+ securityScan?: SkillSecurityScanResult;
98
157
  }
99
158
  /** SPEC-668: Result of a skills TTL refresh operation. */
100
159
  export interface SkillTTLRefreshResult {
@@ -22,6 +22,10 @@ export interface BddScenario {
22
22
  title: string;
23
23
  steps: BddStep[];
24
24
  done: boolean;
25
+ tests?: {
26
+ path: string;
27
+ line?: number;
28
+ }[];
25
29
  }
26
30
  /** Input for lean spec generation. */
27
31
  export interface LeanSpecInput {
@@ -38,6 +42,8 @@ export interface LeanSpecInput {
38
42
  groundingTechnicalReferences?: TechnicalReferenceGroundingRecord[];
39
43
  /** SPEC-481: Acceptance criteria format. Defaults to 'checkbox'. */
40
44
  acFormat?: 'checkbox' | 'bdd';
45
+ /** SPEC-1082: Test links to attach to generated BDD scenarios when available. */
46
+ scenarioTestPaths?: string[];
41
47
  }
42
48
  /** A file entry in the unified spec.md Technical section. */
43
49
  export interface LeanFileEntry {
@@ -121,4 +127,19 @@ export interface LeanTechnicalInput {
121
127
  filesToModify?: LeanFileEntry[];
122
128
  filesToTest?: LeanFileEntry[];
123
129
  }
130
+ export interface ImplementationContractInput {
131
+ description: string;
132
+ criteria: LeanCriterion[];
133
+ files: {
134
+ create: LeanFileEntry[];
135
+ modify: LeanFileEntry[];
136
+ test: LeanFileEntry[];
137
+ };
138
+ outOfScope: string[];
139
+ verificationCommands: string[];
140
+ }
141
+ export interface ImplementationContractIssue {
142
+ code: 'contract_missing' | 'contract_subsection_missing' | 'contract_subsection_empty' | 'contract_filler' | 'contract_needs_decision' | 'contract_unmapped_criterion' | 'contract_manual_verification_vague';
143
+ message: string;
144
+ }
124
145
  //# sourceMappingURL=spec-format.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.4.1",
3
+ "version": "4.4.3",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -34,14 +34,14 @@
34
34
  "packageName": "@planu/core"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@planu/core-darwin-arm64": "4.4.1",
38
- "@planu/core-darwin-x64": "4.4.1",
39
- "@planu/core-linux-arm64-gnu": "4.4.1",
40
- "@planu/core-linux-arm64-musl": "4.4.1",
41
- "@planu/core-linux-x64-gnu": "4.4.1",
42
- "@planu/core-linux-x64-musl": "4.4.1",
43
- "@planu/core-win32-arm64-msvc": "4.4.1",
44
- "@planu/core-win32-x64-msvc": "4.4.1"
37
+ "@planu/core-darwin-arm64": "4.4.3",
38
+ "@planu/core-darwin-x64": "4.4.3",
39
+ "@planu/core-linux-arm64-gnu": "4.4.3",
40
+ "@planu/core-linux-arm64-musl": "4.4.3",
41
+ "@planu/core-linux-x64-gnu": "4.4.3",
42
+ "@planu/core-linux-x64-musl": "4.4.3",
43
+ "@planu/core-win32-arm64-msvc": "4.4.3",
44
+ "@planu/core-win32-x64-msvc": "4.4.3"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
@@ -129,7 +129,7 @@
129
129
  ],
130
130
  "license": "SEE LICENSE IN LICENSE",
131
131
  "dependencies": {
132
- "@anthropic-ai/sdk": "^0.100.1",
132
+ "@anthropic-ai/sdk": "^0.104.1",
133
133
  "@modelcontextprotocol/sdk": "^1.29.0",
134
134
  "glob": "^13.0.6",
135
135
  "yaml": "^2.9.0",
@@ -181,8 +181,8 @@
181
181
  "@semantic-release/release-notes-generator": "^14.1.1",
182
182
  "@stryker-mutator/core": "^9.6.1",
183
183
  "@stryker-mutator/vitest-runner": "^9.6.1",
184
- "@supabase/supabase-js": "^2.107.0",
185
- "@types/node": "^25.9.1",
184
+ "@supabase/supabase-js": "^2.108.1",
185
+ "@types/node": "^25.9.2",
186
186
  "@vitejs/plugin-vue": "^6.0.7",
187
187
  "@vitest/coverage-v8": "^4.1.8",
188
188
  "@vue/test-utils": "^2.4.11",
@@ -190,19 +190,19 @@
190
190
  "eslint-config-prettier": "^10.1.8",
191
191
  "eslint-import-resolver-typescript": "^4.4.5",
192
192
  "eslint-plugin-import": "^2.32.0",
193
- "happy-dom": "^20.10.1",
193
+ "happy-dom": "^20.10.2",
194
194
  "husky": "^9.1.7",
195
195
  "javascript-obfuscator": "^5.4.3",
196
- "knip": "^6.15.0",
196
+ "knip": "^6.16.1",
197
197
  "lint-staged": "^17.0.7",
198
198
  "madge": "^8.0.0",
199
- "prettier": "^3.8.3",
199
+ "prettier": "^3.8.4",
200
200
  "secretlint": "^13.0.2",
201
201
  "semantic-release": "^25.0.3",
202
202
  "tsc-alias": "^1.8.17",
203
203
  "type-coverage": "^2.29.7",
204
204
  "typescript": "^6.0.3",
205
- "typescript-eslint": "^8.60.1",
205
+ "typescript-eslint": "^8.61.0",
206
206
  "vite": "^8.0.16",
207
207
  "vitest": "^4.1.8",
208
208
  "vue": "^3.5.35"
package/planu-native.json CHANGED
@@ -1,26 +1,20 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.4.1",
4
+ "version": "4.4.3",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
8
8
  "requiresMcp": false,
9
9
  "requiresDaemon": false,
10
- "hosts": [
11
- "codex",
12
- "claude-code"
13
- ],
10
+ "hosts": ["codex", "claude-code"],
14
11
  "commands": [
15
12
  {
16
13
  "id": "planu.status",
17
14
  "title": "Project status",
18
15
  "description": "Show the compact Planu project snapshot without loading the MCP tool graph.",
19
16
  "invocation": "planu status",
20
- "hosts": [
21
- "codex",
22
- "claude-code"
23
- ],
17
+ "hosts": ["codex", "claude-code"],
24
18
  "requiresMcp": false,
25
19
  "requiresDaemon": false,
26
20
  "mapsTo": "handlePlanStatus"
@@ -30,10 +24,7 @@
30
24
  "title": "Create spec",
31
25
  "description": "Create a new spec through the CLI-backed SDD contract.",
32
26
  "invocation": "planu spec create \"<title>\"",
33
- "hosts": [
34
- "codex",
35
- "claude-code"
36
- ],
27
+ "hosts": ["codex", "claude-code"],
37
28
  "requiresMcp": false,
38
29
  "requiresDaemon": false,
39
30
  "mapsTo": "handleCreateSpec"
@@ -43,10 +34,7 @@
43
34
  "title": "List specs",
44
35
  "description": "List specs in the current project with optional status/type filters.",
45
36
  "invocation": "planu spec list",
46
- "hosts": [
47
- "codex",
48
- "claude-code"
49
- ],
37
+ "hosts": ["codex", "claude-code"],
50
38
  "requiresMcp": false,
51
39
  "requiresDaemon": false,
52
40
  "mapsTo": "handleListSpecs"
@@ -56,10 +44,7 @@
56
44
  "title": "Validate spec",
57
45
  "description": "Validate a spec against the current codebase from the native CLI surface.",
58
46
  "invocation": "planu spec validate SPEC-001",
59
- "hosts": [
60
- "codex",
61
- "claude-code"
62
- ],
47
+ "hosts": ["codex", "claude-code"],
63
48
  "requiresMcp": false,
64
49
  "requiresDaemon": false,
65
50
  "mapsTo": "handleValidate"
@@ -69,10 +54,7 @@
69
54
  "title": "Audit technical debt",
70
55
  "description": "Run the read-only project audit path for lightweight debt checks.",
71
56
  "invocation": "planu audit debt",
72
- "hosts": [
73
- "codex",
74
- "claude-code"
75
- ],
57
+ "hosts": ["codex", "claude-code"],
76
58
  "requiresMcp": false,
77
59
  "requiresDaemon": false,
78
60
  "mapsTo": "handleAudit"
@@ -82,10 +64,7 @@
82
64
  "title": "Check release readiness",
83
65
  "description": "Check local branch cleanliness and main/develop/release sync readiness.",
84
66
  "invocation": "planu release check",
85
- "hosts": [
86
- "codex",
87
- "claude-code"
88
- ],
67
+ "hosts": ["codex", "claude-code"],
89
68
  "requiresMcp": false,
90
69
  "requiresDaemon": false,
91
70
  "mapsTo": "releaseCommand"