@planu/cli 4.7.1 → 4.7.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 CHANGED
@@ -1,3 +1,12 @@
1
+ ## [4.7.2] - 2026-06-16
2
+
3
+ ### Features
4
+ - feat(SPEC-1088): add policy-driven minimal implementation gate
5
+
6
+ ### Chores
7
+ - chore(deps): update patch/minor dependencies
8
+
9
+
1
10
  ## [4.7.1] - 2026-06-12
2
11
 
3
12
  ### Chores
@@ -4085,4 +4094,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4085
4094
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
4086
4095
  - Multi-language i18n (EN/ES/PT) for generated specs
4087
4096
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
4088
- - 10,857 tests with ≥95% coverage
4097
+ - 10,857 tests with ≥95% coverage
@@ -0,0 +1,110 @@
1
+ {
2
+ "version": 1,
3
+ "enabled": true,
4
+ "report": {
5
+ "maxFindings": 12,
6
+ "maxMarkdownLines": 12,
7
+ "maxEvidenceChars": 180
8
+ },
9
+ "tags": {
10
+ "delete": { "description": "Remove avoidable code or files." },
11
+ "stdlib": { "description": "Use a standard library capability before adding custom code." },
12
+ "native": { "description": "Use an existing platform or Planu-native capability." },
13
+ "installed-dependency": { "description": "Use an already-installed dependency instead of adding another." },
14
+ "yagni": { "description": "Avoid speculative code that is not required by the approved spec." },
15
+ "shrink": { "description": "Reduce an oversized abstraction or workflow." },
16
+ "dependency": { "description": "Avoid an unnecessary new package or dependency path." },
17
+ "file-scope": { "description": "Keep implementation inside the approved file scope." },
18
+ "debt-marker": { "description": "Record accepted shortcuts as structured debt evidence." }
19
+ },
20
+ "rules": [
21
+ {
22
+ "id": "minimality.delete.dead-compat",
23
+ "tag": "delete",
24
+ "severity": "warning",
25
+ "confidence": "medium",
26
+ "patterns": ["legacy compatibility", "deprecated fallback", "unused fallback"],
27
+ "replacementGuidance": "Remove the compatibility path unless the approved spec explicitly requires backward compatibility."
28
+ },
29
+ {
30
+ "id": "minimality.stdlib.custom-parser",
31
+ "tag": "stdlib",
32
+ "severity": "warning",
33
+ "confidence": "medium",
34
+ "patterns": ["custom parser", "manual parser", "hand rolled parser"],
35
+ "replacementGuidance": "Use an existing parser from the runtime or current dependency set before writing parsing code."
36
+ },
37
+ {
38
+ "id": "minimality.native.existing-planu-surface",
39
+ "tag": "native",
40
+ "severity": "warning",
41
+ "confidence": "high",
42
+ "patterns": ["new public mcp tool", "new command", "new cli command"],
43
+ "replacementGuidance": "Prefer existing Planu SDD surfaces unless the spec proves a new public surface is required."
44
+ },
45
+ {
46
+ "id": "minimality.installed-dependency.add-package",
47
+ "tag": "installed-dependency",
48
+ "severity": "warning",
49
+ "confidence": "medium",
50
+ "patterns": ["add dependency", "new dependency", "install package"],
51
+ "replacementGuidance": "Check package.json first and reuse an already-installed dependency when possible."
52
+ },
53
+ {
54
+ "id": "minimality.yagni.future-proofing",
55
+ "tag": "yagni",
56
+ "severity": "blocker",
57
+ "confidence": "high",
58
+ "patterns": ["future-proof", "just in case", "might need later", "eventually support"],
59
+ "replacementGuidance": "Remove speculative scope unless it is directly required by an acceptance criterion.",
60
+ "blockWhenRiskAtLeast": "medium"
61
+ },
62
+ {
63
+ "id": "minimality.shrink.over-architecture",
64
+ "tag": "shrink",
65
+ "severity": "warning",
66
+ "confidence": "medium",
67
+ "patterns": ["framework", "orchestration layer", "plugin architecture", "abstract factory"],
68
+ "replacementGuidance": "Shrink the design to the smallest correct module/function that satisfies the spec."
69
+ },
70
+ {
71
+ "id": "minimality.dependency.new-runtime",
72
+ "tag": "dependency",
73
+ "severity": "blocker",
74
+ "confidence": "high",
75
+ "patterns": ["npm install", "pnpm add", "yarn add"],
76
+ "replacementGuidance": "Avoid adding runtime dependencies unless the approved spec explicitly requires one.",
77
+ "blockWhenRiskAtLeast": "low"
78
+ },
79
+ {
80
+ "id": "minimality.file-scope.unapproved-area",
81
+ "tag": "file-scope",
82
+ "severity": "warning",
83
+ "confidence": "medium",
84
+ "patterns": ["touch unrelated", "broad refactor", "repo-wide refactor"],
85
+ "replacementGuidance": "Keep edits inside the approved technical references and tests."
86
+ },
87
+ {
88
+ "id": "minimality.debt-marker.comment-only",
89
+ "tag": "debt-marker",
90
+ "severity": "warning",
91
+ "confidence": "medium",
92
+ "patterns": ["todo debt", "temporary hack", "shortcut accepted"],
93
+ "replacementGuidance": "Record accepted shortcuts as structured debt evidence with ceiling and upgrade trigger."
94
+ }
95
+ ],
96
+ "safetyExclusions": [
97
+ "security",
98
+ "accessibility",
99
+ "data-loss",
100
+ "compliance",
101
+ "money",
102
+ "access-control",
103
+ "explicitly requested",
104
+ "trust boundary"
105
+ ],
106
+ "excludedPathPatterns": ["dist/**", "coverage/**", "node_modules/**", "pnpm-lock.yaml", "*.snap"],
107
+ "debtEvidence": {
108
+ "requiredFields": ["specId", "files", "ceiling", "upgradeTrigger", "reviewRationale"]
109
+ }
110
+ }
@@ -96,6 +96,62 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
96
96
  evidence: z.ZodArray<z.ZodString>;
97
97
  }, z.core.$strip>>;
98
98
  }, z.core.$strip>>;
99
+ minimalityReport: z.ZodOptional<z.ZodObject<{
100
+ enabled: z.ZodBoolean;
101
+ blocked: z.ZodBoolean;
102
+ findings: z.ZodArray<z.ZodObject<{
103
+ ruleId: z.ZodString;
104
+ tag: z.ZodString;
105
+ severity: z.ZodEnum<{
106
+ blocker: "blocker";
107
+ warning: "warning";
108
+ info: "info";
109
+ }>;
110
+ confidence: z.ZodEnum<{
111
+ low: "low";
112
+ medium: "medium";
113
+ high: "high";
114
+ }>;
115
+ target: z.ZodString;
116
+ line: z.ZodOptional<z.ZodNumber>;
117
+ evidence: z.ZodString;
118
+ replacementGuidance: z.ZodString;
119
+ blocksDone: z.ZodBoolean;
120
+ evidenceSource: z.ZodArray<z.ZodObject<{
121
+ source: z.ZodEnum<{
122
+ spec: "spec";
123
+ runtime: "runtime";
124
+ policy: "policy";
125
+ "project-config": "project-config";
126
+ }>;
127
+ key: z.ZodString;
128
+ value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
129
+ }, z.core.$strip>>;
130
+ }, z.core.$strip>>;
131
+ safetyExceptions: z.ZodArray<z.ZodObject<{
132
+ target: z.ZodString;
133
+ reason: z.ZodString;
134
+ evidence: z.ZodString;
135
+ }, z.core.$strip>>;
136
+ debtEvidence: z.ZodArray<z.ZodObject<{
137
+ specId: z.ZodString;
138
+ files: z.ZodArray<z.ZodString>;
139
+ ceiling: z.ZodString;
140
+ upgradeTrigger: z.ZodString;
141
+ reviewRationale: z.ZodString;
142
+ }, z.core.$strip>>;
143
+ evidence: z.ZodArray<z.ZodObject<{
144
+ source: z.ZodEnum<{
145
+ spec: "spec";
146
+ runtime: "runtime";
147
+ policy: "policy";
148
+ "project-config": "project-config";
149
+ }>;
150
+ key: z.ZodString;
151
+ value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
152
+ }, z.core.$strip>>;
153
+ markdown: z.ZodString;
154
+ }, z.core.$strip>>;
99
155
  score: z.ZodOptional<z.ZodNumber>;
100
156
  completedAt: z.ZodISODateTime;
101
157
  }, z.core.$strip>;
@@ -194,6 +250,62 @@ export declare const ARTIFACT_SCHEMAS: {
194
250
  evidence: z.ZodArray<z.ZodString>;
195
251
  }, z.core.$strip>>;
196
252
  }, z.core.$strip>>;
253
+ minimalityReport: z.ZodOptional<z.ZodObject<{
254
+ enabled: z.ZodBoolean;
255
+ blocked: z.ZodBoolean;
256
+ findings: z.ZodArray<z.ZodObject<{
257
+ ruleId: z.ZodString;
258
+ tag: z.ZodString;
259
+ severity: z.ZodEnum<{
260
+ blocker: "blocker";
261
+ warning: "warning";
262
+ info: "info";
263
+ }>;
264
+ confidence: z.ZodEnum<{
265
+ low: "low";
266
+ medium: "medium";
267
+ high: "high";
268
+ }>;
269
+ target: z.ZodString;
270
+ line: z.ZodOptional<z.ZodNumber>;
271
+ evidence: z.ZodString;
272
+ replacementGuidance: z.ZodString;
273
+ blocksDone: z.ZodBoolean;
274
+ evidenceSource: z.ZodArray<z.ZodObject<{
275
+ source: z.ZodEnum<{
276
+ spec: "spec";
277
+ runtime: "runtime";
278
+ policy: "policy";
279
+ "project-config": "project-config";
280
+ }>;
281
+ key: z.ZodString;
282
+ value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
283
+ }, z.core.$strip>>;
284
+ }, z.core.$strip>>;
285
+ safetyExceptions: z.ZodArray<z.ZodObject<{
286
+ target: z.ZodString;
287
+ reason: z.ZodString;
288
+ evidence: z.ZodString;
289
+ }, z.core.$strip>>;
290
+ debtEvidence: z.ZodArray<z.ZodObject<{
291
+ specId: z.ZodString;
292
+ files: z.ZodArray<z.ZodString>;
293
+ ceiling: z.ZodString;
294
+ upgradeTrigger: z.ZodString;
295
+ reviewRationale: z.ZodString;
296
+ }, z.core.$strip>>;
297
+ evidence: z.ZodArray<z.ZodObject<{
298
+ source: z.ZodEnum<{
299
+ spec: "spec";
300
+ runtime: "runtime";
301
+ policy: "policy";
302
+ "project-config": "project-config";
303
+ }>;
304
+ key: z.ZodString;
305
+ value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
306
+ }, z.core.$strip>>;
307
+ markdown: z.ZodString;
308
+ }, z.core.$strip>>;
197
309
  score: z.ZodOptional<z.ZodNumber>;
198
310
  completedAt: z.ZodISODateTime;
199
311
  }, z.core.$strip>;
@@ -73,6 +73,46 @@ export const ValidationReportV1Schema = z.object({
73
73
  })),
74
74
  })
75
75
  .optional(),
76
+ minimalityReport: z
77
+ .object({
78
+ enabled: z.boolean(),
79
+ blocked: z.boolean(),
80
+ findings: z.array(z.object({
81
+ ruleId: z.string(),
82
+ tag: z.string(),
83
+ severity: z.enum(['info', 'warning', 'blocker']),
84
+ confidence: z.enum(['low', 'medium', 'high']),
85
+ target: z.string(),
86
+ line: z.number().optional(),
87
+ evidence: z.string(),
88
+ replacementGuidance: z.string(),
89
+ blocksDone: z.boolean(),
90
+ evidenceSource: z.array(z.object({
91
+ source: z.enum(['policy', 'project-config', 'runtime', 'spec']),
92
+ key: z.string(),
93
+ value: z.union([z.string(), z.number(), z.boolean()]).optional(),
94
+ })),
95
+ })),
96
+ safetyExceptions: z.array(z.object({
97
+ target: z.string(),
98
+ reason: z.string(),
99
+ evidence: z.string(),
100
+ })),
101
+ debtEvidence: z.array(z.object({
102
+ specId: z.string(),
103
+ files: z.array(z.string()),
104
+ ceiling: z.string(),
105
+ upgradeTrigger: z.string(),
106
+ reviewRationale: z.string(),
107
+ })),
108
+ evidence: z.array(z.object({
109
+ source: z.enum(['policy', 'project-config', 'runtime', 'spec']),
110
+ key: z.string(),
111
+ value: z.union([z.string(), z.number(), z.boolean()]).optional(),
112
+ })),
113
+ markdown: z.string(),
114
+ })
115
+ .optional(),
76
116
  score: z.number().optional(),
77
117
  completedAt: z.iso.datetime(),
78
118
  });
@@ -0,0 +1,3 @@
1
+ import type { MinimalImplementationInput, MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
2
+ export declare function analyzeMinimalImplementation(input: MinimalImplementationInput): MinimalImplementationReport;
3
+ //# sourceMappingURL=analyzer.d.ts.map
@@ -0,0 +1,140 @@
1
+ import { formatMinimalImplementationReport } from './formatter.js';
2
+ const RISK_ORDER = ['low', 'medium', 'high', 'max'];
3
+ function riskRank(risk) {
4
+ const index = RISK_ORDER.indexOf(risk);
5
+ return index >= 0 ? index : 1;
6
+ }
7
+ function isExcludedPath(path, policy) {
8
+ return policy.excludedPathPatterns.some((pattern) => {
9
+ const escaped = pattern
10
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
11
+ .replace(/\*\*/g, '.*')
12
+ .replace(/\*/g, '[^/]*');
13
+ return new RegExp(`^${escaped}$`).test(path);
14
+ });
15
+ }
16
+ function firstMatchingSafetyExclusion(text, policy) {
17
+ const lower = text.toLowerCase();
18
+ return policy.safetyExclusions.find((term) => lower.includes(term.toLowerCase())) ?? null;
19
+ }
20
+ function trimEvidence(value, maxChars) {
21
+ const compact = value.replace(/\s+/g, ' ').trim();
22
+ return compact.length > maxChars ? `${compact.slice(0, maxChars - 1)}...` : compact;
23
+ }
24
+ function lineForMatch(content, index) {
25
+ return content.slice(0, index).split('\n').length;
26
+ }
27
+ function ruleBlocksDone(rule, specRisk) {
28
+ if (rule.severity !== 'blocker') {
29
+ return false;
30
+ }
31
+ if (rule.blockWhenRiskAtLeast === undefined) {
32
+ return true;
33
+ }
34
+ return riskRank(specRisk) >= riskRank(rule.blockWhenRiskAtLeast);
35
+ }
36
+ function makeEvidence(input, rule) {
37
+ return [...(input.evidence ?? []), { source: 'policy', key: rule.id }];
38
+ }
39
+ function scanText(args) {
40
+ for (const pattern of args.rule.patterns) {
41
+ const index = args.content.toLowerCase().indexOf(pattern.toLowerCase());
42
+ if (index < 0) {
43
+ continue;
44
+ }
45
+ const evidence = trimEvidence(args.content.slice(index, index + 220), args.policy.report.maxEvidenceChars);
46
+ const safetyTerm = firstMatchingSafetyExclusion(args.content, args.policy);
47
+ if (safetyTerm !== null) {
48
+ return {
49
+ safety: {
50
+ target: args.target,
51
+ reason: `Matched safety exclusion: ${safetyTerm}`,
52
+ evidence,
53
+ },
54
+ };
55
+ }
56
+ return {
57
+ finding: {
58
+ ruleId: args.rule.id,
59
+ tag: args.rule.tag,
60
+ severity: args.rule.severity,
61
+ confidence: args.rule.confidence,
62
+ target: args.target,
63
+ line: args.target.includes(':') ? undefined : lineForMatch(args.content, index),
64
+ evidence,
65
+ replacementGuidance: args.rule.replacementGuidance,
66
+ blocksDone: ruleBlocksDone(args.rule, args.input.specRisk),
67
+ evidenceSource: makeEvidence(args.input, args.rule),
68
+ },
69
+ };
70
+ }
71
+ return {};
72
+ }
73
+ function collectSources(input) {
74
+ const virtual = [];
75
+ if (input.specText) {
76
+ virtual.push({ path: 'spec.md', content: input.specText });
77
+ }
78
+ if (input.handoffText) {
79
+ virtual.push({ path: 'handoff', content: input.handoffText });
80
+ }
81
+ if (input.packageManifestText) {
82
+ virtual.push({ path: 'package.json', content: input.packageManifestText });
83
+ }
84
+ return [...virtual, ...(input.files ?? [])];
85
+ }
86
+ export function analyzeMinimalImplementation(input) {
87
+ const { policy } = input;
88
+ if (!policy.enabled) {
89
+ return {
90
+ enabled: false,
91
+ blocked: false,
92
+ findings: [],
93
+ safetyExceptions: [],
94
+ debtEvidence: input.acceptedDebt ?? [],
95
+ evidence: input.evidence ?? [],
96
+ markdown: '',
97
+ };
98
+ }
99
+ const findings = [];
100
+ const safetyExceptions = [];
101
+ for (const source of collectSources(input)) {
102
+ if (isExcludedPath(source.path, policy) || source.content === undefined) {
103
+ continue;
104
+ }
105
+ for (const rule of policy.rules) {
106
+ const result = scanText({
107
+ target: source.path,
108
+ content: source.content,
109
+ policy,
110
+ input,
111
+ rule,
112
+ });
113
+ if (result.safety) {
114
+ safetyExceptions.push(result.safety);
115
+ }
116
+ if (result.finding) {
117
+ findings.push(result.finding);
118
+ }
119
+ if (findings.length >= policy.report.maxFindings) {
120
+ break;
121
+ }
122
+ }
123
+ if (findings.length >= policy.report.maxFindings) {
124
+ break;
125
+ }
126
+ }
127
+ const report = {
128
+ enabled: true,
129
+ blocked: findings.some((finding) => finding.blocksDone),
130
+ findings,
131
+ safetyExceptions,
132
+ debtEvidence: input.acceptedDebt ?? [],
133
+ evidence: input.evidence ?? [],
134
+ };
135
+ return {
136
+ ...report,
137
+ markdown: formatMinimalImplementationReport({ ...report, markdown: '' }, policy),
138
+ };
139
+ }
140
+ //# sourceMappingURL=analyzer.js.map
@@ -0,0 +1,3 @@
1
+ import type { MinimalImplementationPolicy, MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
2
+ export declare function formatMinimalImplementationReport(report: MinimalImplementationReport, policy?: MinimalImplementationPolicy): string;
3
+ //# sourceMappingURL=formatter.d.ts.map
@@ -0,0 +1,25 @@
1
+ export function formatMinimalImplementationReport(report, policy) {
2
+ if (!report.enabled) {
3
+ return '';
4
+ }
5
+ const maxLines = policy?.report.maxMarkdownLines ?? 12;
6
+ const lines = ['## Minimal Implementation', ''];
7
+ if (report.findings.length === 0) {
8
+ lines.push('- No avoidable complexity findings from the current policy.');
9
+ }
10
+ else {
11
+ for (const finding of report.findings.slice(0, Math.max(1, maxLines - 4))) {
12
+ const location = finding.line !== undefined ? `${finding.target}:${String(finding.line)}` : finding.target;
13
+ const marker = finding.blocksDone ? 'BLOCK' : finding.severity.toUpperCase();
14
+ lines.push(`- [${marker}] ${finding.tag} ${location}: ${finding.replacementGuidance}`);
15
+ }
16
+ }
17
+ if (report.safetyExceptions.length > 0) {
18
+ lines.push(`- Safety exceptions preserved: ${String(report.safetyExceptions.length)} required safeguard(s).`);
19
+ }
20
+ if (report.debtEvidence.length > 0) {
21
+ lines.push(`- Structured debt evidence recorded: ${String(report.debtEvidence.length)} item(s).`);
22
+ }
23
+ return lines.slice(0, maxLines).join('\n');
24
+ }
25
+ //# sourceMappingURL=formatter.js.map
@@ -0,0 +1,4 @@
1
+ export { loadMinimalImplementationPolicy } from './policy-loader.js';
2
+ export { analyzeMinimalImplementation } from './analyzer.js';
3
+ export { formatMinimalImplementationReport } from './formatter.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ export { loadMinimalImplementationPolicy } from './policy-loader.js';
2
+ export { analyzeMinimalImplementation } from './analyzer.js';
3
+ export { formatMinimalImplementationReport } from './formatter.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { LoadedMinimalImplementationPolicy, MinimalImplementationPolicyLoadOptions } from '../../types/minimal-implementation-gate.js';
2
+ export declare function loadMinimalImplementationPolicy(projectPath?: string, options?: MinimalImplementationPolicyLoadOptions): Promise<LoadedMinimalImplementationPolicy>;
3
+ //# sourceMappingURL=policy-loader.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ function configPath() {
5
+ return join(dirname(fileURLToPath(import.meta.url)), '../../config/minimal-implementation-gate.json');
6
+ }
7
+ function isRecord(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+ function mergeDeep(base, override) {
11
+ const result = { ...base };
12
+ for (const [key, value] of Object.entries(override)) {
13
+ const current = result[key];
14
+ result[key] = isRecord(current) && isRecord(value) ? mergeDeep(current, value) : value;
15
+ }
16
+ return result;
17
+ }
18
+ function assertString(value, path) {
19
+ if (value === undefined) {
20
+ throw new Error(`Invalid minimal implementation policy at ${path}: string is required`);
21
+ }
22
+ if (value === null) {
23
+ throw new Error(`Invalid minimal implementation policy at ${path}: null is not allowed`);
24
+ }
25
+ if (typeof value !== 'string') {
26
+ throw new Error(`Invalid minimal implementation policy at ${path}: expected string`);
27
+ }
28
+ if (value.length === 0) {
29
+ throw new Error(`Invalid minimal implementation policy at ${path}: empty string is not allowed`);
30
+ }
31
+ if (value.trim().length === 0) {
32
+ throw new Error(`Invalid minimal implementation policy at ${path}: whitespace-only string is not allowed`);
33
+ }
34
+ if (value.length > 500) {
35
+ throw new Error(`Invalid minimal implementation policy at ${path}: string is too long`);
36
+ }
37
+ }
38
+ function assertStringArray(value, path) {
39
+ if (!Array.isArray(value)) {
40
+ throw new Error(`Invalid minimal implementation policy at ${path}: expected string[]`);
41
+ }
42
+ value.forEach((item, index) => {
43
+ assertString(item, `${path}.${String(index)}`);
44
+ });
45
+ }
46
+ function validateRule(value, path) {
47
+ if (!isRecord(value)) {
48
+ throw new Error(`Invalid minimal implementation policy at ${path}: expected object`);
49
+ }
50
+ assertString(value.id, `${path}.id`);
51
+ assertString(value.tag, `${path}.tag`);
52
+ if (!['info', 'warning', 'blocker'].includes(String(value.severity))) {
53
+ throw new Error(`Invalid minimal implementation policy at ${path}.severity`);
54
+ }
55
+ if (!['low', 'medium', 'high'].includes(String(value.confidence))) {
56
+ throw new Error(`Invalid minimal implementation policy at ${path}.confidence`);
57
+ }
58
+ assertStringArray(value.patterns, `${path}.patterns`);
59
+ assertString(value.replacementGuidance, `${path}.replacementGuidance`);
60
+ if (value.blockWhenRiskAtLeast !== undefined &&
61
+ (typeof value.blockWhenRiskAtLeast !== 'string' ||
62
+ !['low', 'medium', 'high', 'max'].includes(value.blockWhenRiskAtLeast))) {
63
+ throw new Error(`Invalid minimal implementation policy at ${path}.blockWhenRiskAtLeast`);
64
+ }
65
+ return value;
66
+ }
67
+ function validatePolicy(value) {
68
+ if (!isRecord(value)) {
69
+ throw new Error('Invalid minimal implementation policy: expected object');
70
+ }
71
+ if (value.version !== 1) {
72
+ throw new Error('Invalid minimal implementation policy: version must be 1');
73
+ }
74
+ const policy = value;
75
+ if (typeof policy.enabled !== 'boolean') {
76
+ throw new Error('Invalid minimal implementation policy at enabled: expected boolean');
77
+ }
78
+ if (!isRecord(policy.report)) {
79
+ throw new Error('Invalid minimal implementation policy at report: expected object');
80
+ }
81
+ for (const key of ['maxFindings', 'maxMarkdownLines', 'maxEvidenceChars']) {
82
+ if (!Number.isFinite(policy.report[key]) || policy.report[key] < 1) {
83
+ throw new Error(`Invalid minimal implementation policy at report.${key}`);
84
+ }
85
+ }
86
+ if (!isRecord(policy.tags)) {
87
+ throw new Error('Invalid minimal implementation policy at tags: expected object');
88
+ }
89
+ for (const [tag, definition] of Object.entries(policy.tags)) {
90
+ assertString(tag, `tags.${tag}`);
91
+ if (!isRecord(definition)) {
92
+ throw new Error(`Invalid minimal implementation policy at tags.${tag}: expected object`);
93
+ }
94
+ assertString(definition.description, `tags.${tag}.description`);
95
+ }
96
+ if (!Array.isArray(policy.rules) || policy.rules.length === 0) {
97
+ throw new Error('Invalid minimal implementation policy at rules: expected non-empty array');
98
+ }
99
+ policy.rules.forEach((rule, index) => validateRule(rule, `rules.${String(index)}`));
100
+ assertStringArray(policy.safetyExclusions, 'safetyExclusions');
101
+ assertStringArray(policy.excludedPathPatterns, 'excludedPathPatterns');
102
+ if (!isRecord(policy.debtEvidence)) {
103
+ throw new Error('Invalid minimal implementation policy at debtEvidence: expected object');
104
+ }
105
+ assertStringArray(policy.debtEvidence.requiredFields, 'debtEvidence.requiredFields');
106
+ return policy;
107
+ }
108
+ async function readJson(path) {
109
+ return JSON.parse(await readFile(path, 'utf-8'));
110
+ }
111
+ export async function loadMinimalImplementationPolicy(projectPath, options = {}) {
112
+ const defaultPolicyPath = options.defaultPolicyPath ?? configPath();
113
+ const defaultPolicy = validatePolicy(await readJson(defaultPolicyPath));
114
+ const evidence = [
115
+ { source: 'policy', key: defaultPolicyPath },
116
+ ];
117
+ let merged = defaultPolicy;
118
+ if (projectPath !== undefined) {
119
+ const planuConfigPath = join(projectPath, 'planu.json');
120
+ try {
121
+ const config = await readJson(planuConfigPath);
122
+ if (isRecord(config) && isRecord(config.minimalImplementationGate)) {
123
+ merged = validatePolicy(mergeDeep(defaultPolicy, config.minimalImplementationGate));
124
+ evidence.push({ source: 'project-config', key: 'planu.json.minimalImplementationGate' });
125
+ }
126
+ }
127
+ catch {
128
+ // Missing or malformed project config does not block default policy loading.
129
+ }
130
+ }
131
+ return { policy: merged, evidence };
132
+ }
133
+ //# sourceMappingURL=policy-loader.js.map
@@ -1,5 +1,6 @@
1
1
  import type { Spec } from '../../types/index.js';
2
2
  import type { ValidationReportV1 } from '../../types/handoff-artifacts.js';
3
+ import type { MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
3
4
  export declare function writeImplementationReviewReport(input: {
4
5
  projectId: string;
5
6
  specId: string;
@@ -8,6 +9,7 @@ export declare function writeImplementationReviewReport(input: {
8
9
  score: number | null;
9
10
  lintPassed?: boolean;
10
11
  conventionRegression?: boolean;
12
+ minimalityReport?: MinimalImplementationReport;
11
13
  }): Promise<{
12
14
  written: boolean;
13
15
  path?: string;
@@ -7,6 +7,7 @@ export async function writeImplementationReviewReport(input) {
7
7
  lintPassed: input.lintPassed ?? true,
8
8
  conventionRegression: input.conventionRegression ?? false,
9
9
  specCompliance,
10
+ minimalityReport: input.minimalityReport,
10
11
  });
11
12
  const passed = gates.every((gate) => gate.passed);
12
13
  const reviewer = {
@@ -22,6 +23,7 @@ export async function writeImplementationReviewReport(input) {
22
23
  gates,
23
24
  reviewer,
24
25
  specCompliance: reportCompliance,
26
+ minimalityReport: input.minimalityReport,
25
27
  score: input.score ?? undefined,
26
28
  completedAt: new Date().toISOString(),
27
29
  };
@@ -73,8 +75,25 @@ function buildGates(args) {
73
75
  reason: args.conventionRegression ? 'Convention baseline regression detected.' : undefined,
74
76
  },
75
77
  buildSpecComplianceGate(args.specCompliance),
78
+ buildMinimalityGate(args.minimalityReport),
76
79
  ];
77
80
  }
81
+ function buildMinimalityGate(report) {
82
+ if (report?.enabled !== true) {
83
+ return {
84
+ name: 'minimal-implementation',
85
+ passed: true,
86
+ reason: 'Minimal implementation report unavailable or disabled.',
87
+ };
88
+ }
89
+ return {
90
+ name: 'minimal-implementation',
91
+ passed: !report.blocked,
92
+ reason: report.blocked
93
+ ? `Blocking minimality findings: ${String(report.findings.filter((finding) => finding.blocksDone).length)}.`
94
+ : undefined,
95
+ };
96
+ }
78
97
  function buildSpecComplianceGate(result) {
79
98
  if (result.perScenario.length === 0) {
80
99
  return {
@@ -24,6 +24,7 @@ import { generateResilienceChallengeScenarios } from './challenge-spec/resilienc
24
24
  import { detectContradictions as detectDecisionContradictions, searchPriorDecisions, } from '../engine/prior-decisions/index.js';
25
25
  import { getDecisions } from '../storage/decision-store.js';
26
26
  import { calculateTokenBudget, injectBudgetIntoPrompt } from '../engine/token-budget/index.js';
27
+ import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
27
28
  const ALL_FOCUS_AREAS = [
28
29
  'failures',
29
30
  'concurrency',
@@ -127,6 +128,30 @@ export async function handleChallengeSpec(args, server) {
127
128
  const focusAreas = focus && focus.length > 0 ? focus : ALL_FOCUS_AREAS;
128
129
  // 5. Generate failure scenarios
129
130
  const failureScenarios = [];
131
+ try {
132
+ const { policy, evidence } = await loadMinimalImplementationPolicy(knowledge.projectPath);
133
+ const minimality = analyzeMinimalImplementation({
134
+ policy,
135
+ specId,
136
+ specRisk: spec.risk,
137
+ specText: specContent,
138
+ evidence,
139
+ });
140
+ for (const finding of minimality.findings.slice(0, 5)) {
141
+ failureScenarios.push({
142
+ scenario: `Minimal implementation concern: ${finding.tag}`,
143
+ probability: finding.confidence === 'high' ? 'high' : 'medium',
144
+ impact: finding.blocksDone ? 'high' : 'low',
145
+ currentHandling: `${finding.target}${finding.line ? `:${String(finding.line)}` : ''} — ${finding.evidence}`,
146
+ requiredHandling: finding.replacementGuidance,
147
+ dataConsistency: 'Scope control risk',
148
+ userExperience: 'Avoidable implementation complexity can increase token use and maintenance cost',
149
+ });
150
+ }
151
+ }
152
+ catch {
153
+ // Minimality challenge is best-effort and must not hide resilience findings.
154
+ }
130
155
  if (focusAreas.includes('failures')) {
131
156
  failureScenarios.push(...generateFailureScenarios(spec, specContent, knowledge));
132
157
  }
@@ -6,6 +6,7 @@ import { detectParadigms } from '../engine/paradigm-detector.js';
6
6
  import { analyzeContextPreflight, buildTokenWasteReport, formatTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from '../engine/token-optimizer/index.js';
7
7
  import { appendTransitionEvent } from '../storage/transition-log.js';
8
8
  import { formatProjectGraphContext, queryProjectGraphSlice, } from '../engine/project-graph/index.js';
9
+ import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
9
10
  // ── Formatting helpers ───────────────────────────────────────────────────────
10
11
  function formatHandoff(pkg) {
11
12
  const lines = [];
@@ -196,6 +197,22 @@ async function buildGraphContextSection(args) {
196
197
  return null;
197
198
  }
198
199
  }
200
+ async function buildMinimalImplementationReport(pkg, knowledge, spec, handoffText) {
201
+ try {
202
+ const { policy, evidence } = await loadMinimalImplementationPolicy(knowledge.projectPath);
203
+ return analyzeMinimalImplementation({
204
+ policy,
205
+ specId: spec.id,
206
+ specRisk: spec.risk,
207
+ handoffText,
208
+ files: [...pkg.filesToModify, ...pkg.filesToCreate].map((path) => ({ path, content: path })),
209
+ evidence,
210
+ });
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
199
216
  // ── Handler ──────────────────────────────────────────────────────────────────
200
217
  export async function handlePackageHandoff(args) {
201
218
  const { projectId, specId } = args;
@@ -256,9 +273,13 @@ export async function handlePackageHandoff(args) {
256
273
  projectPath: knowledge.projectPath,
257
274
  specId,
258
275
  });
259
- const handoffText = graphContext !== null
276
+ const baseHandoffText = graphContext !== null
260
277
  ? `${formatHandoff(pkgWithScore)}\n${graphContext.text}`
261
278
  : formatHandoff(pkgWithScore);
279
+ const minimalityReport = await buildMinimalImplementationReport(pkgWithScore, knowledge, spec, baseHandoffText);
280
+ const handoffText = minimalityReport !== null && minimalityReport.markdown.length > 0
281
+ ? `${baseHandoffText}\n${minimalityReport.markdown}`
282
+ : baseHandoffText;
262
283
  const formatted = tokenWasteReport !== null
263
284
  ? `${handoffText}\n${formatTokenWasteReport(tokenWasteReport)}`
264
285
  : handoffText;
@@ -282,6 +303,7 @@ export async function handlePackageHandoff(args) {
282
303
  blockers: pkgWithScore.blockers,
283
304
  handoffPath: pkgWithScore.handoffPath,
284
305
  contextHash: pkgWithScore.contextHash,
306
+ ...(minimalityReport !== null ? { minimality: minimalityReport } : {}),
285
307
  ...(tokenWasteReport !== null ? { tokenWaste: tokenWasteReport } : {}),
286
308
  ...(graphContext !== null ? { graphContext: graphContext.structured } : {}),
287
309
  },
@@ -299,6 +299,15 @@ export async function checkValidationReportGate(specId, projectId, force) {
299
299
  fixHint: 'Fix failing gates and re-run validate before marking done.',
300
300
  });
301
301
  }
302
+ if (result.payload.minimalityReport?.blocked) {
303
+ return validationReportGateError({
304
+ specId,
305
+ error: 'minimality_findings_block_done',
306
+ message: 'Validation report contains blocking minimal implementation findings. Remove avoidable complexity or provide an audited forceStatusReason.',
307
+ gates: result.payload.gates,
308
+ fixHint: 'Fix blocking minimality findings and re-run validate before marking done. Use forceStatus only with an audited reason for accepted complexity.',
309
+ });
310
+ }
302
311
  if (result.payload.reviewer.verdict !== 'approved' ||
303
312
  result.payload.reviewer.agent.trim().length === 0) {
304
313
  return validationReportGateError({
@@ -18,6 +18,7 @@ import { compareWithBaseline } from '../storage/convention-baseline.js';
18
18
  import { writeImplementationReviewReport } from '../engine/validator/validation-report-writer.js';
19
19
  import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
20
20
  import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
21
+ import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
21
22
  // Re-export for external use (SPEC-018)
22
23
  export { validateContractCompliance };
23
24
  function graphCoverageGaps(slice) {
@@ -47,6 +48,25 @@ function formatGraphCoverageText(report) {
47
48
  ? `\nGRAPH ${String(report.gaps.length)} graph-backed coverage gap(s)`
48
49
  : '';
49
50
  }
51
+ async function buildMinimalityReport(args) {
52
+ try {
53
+ const { policy, evidence } = await loadMinimalImplementationPolicy(args.projectPath);
54
+ return analyzeMinimalImplementation({
55
+ policy,
56
+ specId: args.specId,
57
+ specRisk: args.risk,
58
+ specText: [args.title ?? '', ...args.missing, ...args.extra].join('\n'),
59
+ files: args.qualityIssues.map((issue) => ({
60
+ path: issue.file,
61
+ content: [issue.rule, issue.message, issue.suggestion ?? ''].join('\n'),
62
+ })),
63
+ evidence,
64
+ });
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
50
70
  async function validateStrictLayoutOrError(args) {
51
71
  try {
52
72
  const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
@@ -176,6 +196,15 @@ export async function handleValidate(args, server) {
176
196
  // 7. Lint check (best-effort — non-blocking)
177
197
  const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
178
198
  const assuranceGates = runAssuranceGates(projectPath);
199
+ const minimalityReport = await buildMinimalityReport({
200
+ projectPath,
201
+ specId,
202
+ risk: spec.risk,
203
+ title: spec.title,
204
+ missing: result.missing,
205
+ extra: result.extra,
206
+ qualityIssues: result.qualityIssues,
207
+ });
179
208
  // SPEC-1050: Generate a mandatory implementation-review artifact.
180
209
  const validationReport = await writeImplementationReviewReport({
181
210
  projectId,
@@ -185,6 +214,7 @@ export async function handleValidate(args, server) {
185
214
  score: result.score,
186
215
  lintPassed: lintCheck.passed,
187
216
  conventionRegression: regressionDetected,
217
+ minimalityReport: minimalityReport ?? undefined,
188
218
  });
189
219
  // 8. Build output
190
220
  const output = {
@@ -262,6 +292,7 @@ export async function handleValidate(args, server) {
262
292
  lintCheck,
263
293
  assuranceGates,
264
294
  validationReport,
295
+ minimalityReport,
265
296
  graphCoverage,
266
297
  };
267
298
  // SPEC-612: Scope boundary validation — warn if impl files match outOfScope items
@@ -312,6 +343,9 @@ export async function handleValidate(args, server) {
312
343
  if (!assuranceGates.newCode.passed) {
313
344
  suggestions.push(`Assurance gate failed: ${String(assuranceGates.newCode.findings.length)} new-code domain inference issue(s) found.`);
314
345
  }
346
+ if (minimalityReport?.blocked) {
347
+ suggestions.push(`${minimalityReport.findings.filter((finding) => finding.blocksDone).length} blocking minimality finding(s) detected. Remove avoidable complexity or record accepted debt before marking done.`);
348
+ }
315
349
  const outputWithSuggestions = compactObj({
316
350
  ...output,
317
351
  suggestions: suggestions.length > 0 ? suggestions : undefined,
@@ -72,6 +72,7 @@ export interface ValidationReportV1 {
72
72
  evidence: string[];
73
73
  }[];
74
74
  };
75
+ minimalityReport?: MinimalImplementationReport;
75
76
  score?: number;
76
77
  completedAt: string;
77
78
  }
@@ -83,4 +84,5 @@ export interface ArtifactPayloadMap {
83
84
  'validation-report': ValidationReportV1;
84
85
  }
85
86
  export type ArtifactPayload<K extends ArtifactKind> = ArtifactPayloadMap[K];
87
+ import type { MinimalImplementationReport } from './minimal-implementation-gate.js';
86
88
  //# sourceMappingURL=handoff-artifacts.d.ts.map
@@ -80,6 +80,7 @@ export * from './workers.js';
80
80
  export * from './orchestration-runtime.js';
81
81
  export * from './token-optimization.js';
82
82
  export * from './token-waste-autopilot.js';
83
+ export * from './minimal-implementation-gate.js';
83
84
  export * from './llm-providers.js';
84
85
  export * from './plugins.js';
85
86
  export * from './github.js';
@@ -81,6 +81,7 @@ export * from './workers.js';
81
81
  export * from './orchestration-runtime.js';
82
82
  export * from './token-optimization.js';
83
83
  export * from './token-waste-autopilot.js';
84
+ export * from './minimal-implementation-gate.js';
84
85
  export * from './llm-providers.js';
85
86
  export * from './plugins.js';
86
87
  export * from './github.js';
@@ -0,0 +1,92 @@
1
+ export type MinimalImplementationTag = string;
2
+ export type MinimalImplementationSeverity = 'info' | 'warning' | 'blocker';
3
+ export type MinimalImplementationConfidence = 'low' | 'medium' | 'high';
4
+ export type MinimalImplementationSource = 'policy' | 'project-config' | 'runtime' | 'spec';
5
+ export interface MinimalImplementationPolicyLoadOptions {
6
+ defaultPolicyPath?: string;
7
+ }
8
+ export interface MinimalImplementationEvidence {
9
+ source: MinimalImplementationSource;
10
+ key: string;
11
+ value?: string | number | boolean;
12
+ }
13
+ export interface MinimalImplementationRule {
14
+ id: string;
15
+ tag: MinimalImplementationTag;
16
+ severity: MinimalImplementationSeverity;
17
+ confidence: MinimalImplementationConfidence;
18
+ patterns: string[];
19
+ replacementGuidance: string;
20
+ blockWhenRiskAtLeast?: 'low' | 'medium' | 'high' | 'max';
21
+ }
22
+ export interface MinimalImplementationPolicy {
23
+ version: 1;
24
+ enabled: boolean;
25
+ report: {
26
+ maxFindings: number;
27
+ maxMarkdownLines: number;
28
+ maxEvidenceChars: number;
29
+ };
30
+ tags: Record<string, {
31
+ description: string;
32
+ }>;
33
+ rules: MinimalImplementationRule[];
34
+ safetyExclusions: string[];
35
+ excludedPathPatterns: string[];
36
+ debtEvidence: {
37
+ requiredFields: string[];
38
+ };
39
+ }
40
+ export interface LoadedMinimalImplementationPolicy {
41
+ policy: MinimalImplementationPolicy;
42
+ evidence: MinimalImplementationEvidence[];
43
+ }
44
+ export interface MinimalImplementationFileSignal {
45
+ path: string;
46
+ content?: string;
47
+ }
48
+ export interface MinimalImplementationInput {
49
+ policy: MinimalImplementationPolicy;
50
+ specId?: string;
51
+ specRisk?: string;
52
+ specText?: string;
53
+ handoffText?: string;
54
+ files?: MinimalImplementationFileSignal[];
55
+ packageManifestText?: string;
56
+ acceptedDebt?: MinimalImplementationDebtEvidence[];
57
+ evidence?: MinimalImplementationEvidence[];
58
+ }
59
+ export interface MinimalImplementationFinding {
60
+ ruleId: string;
61
+ tag: MinimalImplementationTag;
62
+ severity: MinimalImplementationSeverity;
63
+ confidence: MinimalImplementationConfidence;
64
+ target: string;
65
+ line?: number;
66
+ evidence: string;
67
+ replacementGuidance: string;
68
+ blocksDone: boolean;
69
+ evidenceSource: MinimalImplementationEvidence[];
70
+ }
71
+ export interface MinimalImplementationSafetyException {
72
+ target: string;
73
+ reason: string;
74
+ evidence: string;
75
+ }
76
+ export interface MinimalImplementationDebtEvidence {
77
+ specId: string;
78
+ files: string[];
79
+ ceiling: string;
80
+ upgradeTrigger: string;
81
+ reviewRationale: string;
82
+ }
83
+ export interface MinimalImplementationReport {
84
+ enabled: boolean;
85
+ blocked: boolean;
86
+ findings: MinimalImplementationFinding[];
87
+ safetyExceptions: MinimalImplementationSafetyException[];
88
+ debtEvidence: MinimalImplementationDebtEvidence[];
89
+ evidence: MinimalImplementationEvidence[];
90
+ markdown: string;
91
+ }
92
+ //# sourceMappingURL=minimal-implementation-gate.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=minimal-implementation-gate.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.7.1",
3
+ "version": "4.7.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",
@@ -34,14 +34,14 @@
34
34
  "packageName": "@planu/core"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@planu/core-darwin-arm64": "4.7.1",
38
- "@planu/core-darwin-x64": "4.7.1",
39
- "@planu/core-linux-arm64-gnu": "4.7.1",
40
- "@planu/core-linux-arm64-musl": "4.7.1",
41
- "@planu/core-linux-x64-gnu": "4.7.1",
42
- "@planu/core-linux-x64-musl": "4.7.1",
43
- "@planu/core-win32-arm64-msvc": "4.7.1",
44
- "@planu/core-win32-x64-msvc": "4.7.1"
37
+ "@planu/core-darwin-arm64": "4.7.2",
38
+ "@planu/core-darwin-x64": "4.7.2",
39
+ "@planu/core-linux-arm64-gnu": "4.7.2",
40
+ "@planu/core-linux-arm64-musl": "4.7.2",
41
+ "@planu/core-linux-x64-gnu": "4.7.2",
42
+ "@planu/core-linux-x64-musl": "4.7.2",
43
+ "@planu/core-win32-arm64-msvc": "4.7.2",
44
+ "@planu/core-win32-x64-msvc": "4.7.2"
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.104.1",
132
+ "@anthropic-ai/sdk": "^0.104.2",
133
133
  "@modelcontextprotocol/sdk": "^1.29.0",
134
134
  "glob": "^13.0.6",
135
135
  "yaml": "^2.9.0",
@@ -171,7 +171,7 @@
171
171
  "@commitlint/cli": "^21.0.2",
172
172
  "@commitlint/config-conventional": "^21.0.2",
173
173
  "@eslint/js": "^10.0.1",
174
- "@napi-rs/cli": "^3.7.1",
174
+ "@napi-rs/cli": "^3.7.2",
175
175
  "@secretlint/secretlint-rule-no-homedir": "^13.0.2",
176
176
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
177
177
  "@semantic-release/changelog": "^6.0.3",
@@ -182,19 +182,19 @@
182
182
  "@semantic-release/release-notes-generator": "^14.1.1",
183
183
  "@stryker-mutator/core": "^9.6.1",
184
184
  "@stryker-mutator/vitest-runner": "^9.6.1",
185
- "@supabase/supabase-js": "^2.108.1",
185
+ "@supabase/supabase-js": "^2.108.2",
186
186
  "@types/node": "^25.9.3",
187
187
  "@vitejs/plugin-vue": "^6.0.7",
188
- "@vitest/coverage-v8": "^4.1.8",
188
+ "@vitest/coverage-v8": "^4.1.9",
189
189
  "@vue/test-utils": "^2.4.11",
190
190
  "eslint": "^10.5.0",
191
191
  "eslint-config-prettier": "^10.1.8",
192
192
  "eslint-import-resolver-typescript": "^4.4.5",
193
193
  "eslint-plugin-import": "^2.32.0",
194
- "happy-dom": "^20.10.3",
194
+ "happy-dom": "^20.10.5",
195
195
  "husky": "^9.1.7",
196
196
  "javascript-obfuscator": "^5.4.3",
197
- "knip": "^6.16.1",
197
+ "knip": "^6.17.1",
198
198
  "lint-staged": "^17.0.7",
199
199
  "madge": "^8.0.0",
200
200
  "prettier": "^3.8.4",
@@ -203,9 +203,9 @@
203
203
  "tsc-alias": "^1.8.17",
204
204
  "type-coverage": "^2.29.7",
205
205
  "typescript": "^6.0.3",
206
- "typescript-eslint": "^8.61.0",
206
+ "typescript-eslint": "^8.61.1",
207
207
  "vite": "^8.0.16",
208
- "vitest": "^4.1.8",
208
+ "vitest": "^4.1.9",
209
209
  "vue": "^3.5.38"
210
210
  }
211
211
  }
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.7.1",
4
+ "version": "4.7.2",
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"
package/planu-plugin.json CHANGED
@@ -2,12 +2,9 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "4.7.1",
5
+ "version": "4.7.2",
6
6
  "icon": "assets/plugin/icon.svg",
7
- "command": [
8
- "npx",
9
- "@planu/cli@latest"
10
- ],
7
+ "command": ["npx", "@planu/cli@latest"],
11
8
  "packageName": "@planu/cli",
12
9
  "capabilities": {
13
10
  "tools": [
@@ -26,42 +23,17 @@
26
23
  "create_skill",
27
24
  "skill_search"
28
25
  ],
29
- "resources": [
30
- "planu://specs/list",
31
- "planu://specs/{id}",
32
- "planu://project/status",
33
- "planu://roadmap"
34
- ],
35
- "prompts": [
36
- "create-spec-from-idea",
37
- "review-spec-readiness",
38
- "generate-implementation-plan"
39
- ],
40
- "subagents": [
41
- "sdd-orchestrator",
42
- "spec-challenger",
43
- "test-generator"
44
- ]
26
+ "resources": ["planu://specs/list", "planu://specs/{id}", "planu://project/status", "planu://roadmap"],
27
+ "prompts": ["create-spec-from-idea", "review-spec-readiness", "generate-implementation-plan"],
28
+ "subagents": ["sdd-orchestrator", "spec-challenger", "test-generator"]
45
29
  },
46
30
  "compatibility": {
47
31
  "minimumHostVersion": "1.0.0",
48
- "requiredFeatures": [
49
- "mcp-tools",
50
- "file-editing"
51
- ]
32
+ "requiredFeatures": ["mcp-tools", "file-editing"]
52
33
  },
53
34
  "repository": "https://github.com/planu-dev/planu",
54
35
  "author": "Planu",
55
36
  "license": "MIT",
56
37
  "homepage": "https://planu.dev",
57
- "keywords": [
58
- "sdd",
59
- "spec-driven-development",
60
- "mcp",
61
- "specs",
62
- "planning",
63
- "ai",
64
- "bdd",
65
- "tdd"
66
- ]
38
+ "keywords": ["sdd", "spec-driven-development", "mcp", "specs", "planning", "ai", "bdd", "tdd"]
67
39
  }