@planu/cli 4.10.7 → 4.10.8

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,13 @@
1
+ ## [4.10.8] - 2026-07-07
2
+
3
+ ### Refactoring
4
+ - refactor: merge SPEC-1113 native performance benchmark
5
+ - refactor: benchmark native mcp performance
6
+
7
+ ### Chores
8
+ - chore: exclude generated performance report
9
+
10
+
1
11
  ## [4.10.7] - 2026-07-07
2
12
 
3
13
  ### Bug Fixes
@@ -1,3 +1,8 @@
1
1
  import type { LintCheckResult } from '../types/index.js';
2
- export declare function runLintCheck(projectPath: string, lintCommand: string | null): LintCheckResult;
2
+ interface LintCheckContext {
3
+ projectId?: string;
4
+ specId?: string;
5
+ }
6
+ export declare function runLintCheck(projectPath: string, lintCommand: string | null, _context?: LintCheckContext): LintCheckResult;
7
+ export {};
3
8
  //# sourceMappingURL=validate-lint.d.ts.map
@@ -1,7 +1,13 @@
1
+ // tools/validate-lint.ts — Lint gate for validate.
2
+ import { execFileSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
1
6
  import { formatLintFailureDiagnostics, resolveProjectCommandPlan, runProjectCommandPlan, } from './validate-runtime.js';
2
7
  /** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
3
8
  const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
4
- const LINT_CHECK_TIMEOUT_MS = 50_000;
9
+ const DEFAULT_LINT_CHECK_TIMEOUT_MS = 10_000;
10
+ const LINT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
5
11
  function isSafeCommand(cmd) {
6
12
  return SAFE_COMMAND_RE.test(cmd);
7
13
  }
@@ -33,7 +39,7 @@ function parseLintIssueCount(output) {
33
39
  }
34
40
  return output.split('\n').filter((l) => l.trim().length > 0).length;
35
41
  }
36
- export function runLintCheck(projectPath, lintCommand) {
42
+ export function runLintCheck(projectPath, lintCommand, _context = {}) {
37
43
  if (lintCommand !== null && !isSafeCommand(lintCommand)) {
38
44
  console.warn(`[Planu] validate: lintCommand contains unsafe characters — skipping execution`);
39
45
  return {
@@ -45,16 +51,23 @@ export function runLintCheck(projectPath, lintCommand) {
45
51
  }
46
52
  const command = lintCommand ?? 'pnpm lint';
47
53
  const plan = resolveProjectCommandPlan(projectPath, command);
54
+ const fingerprint = calculateLintFingerprint(projectPath);
55
+ const cached = readPassedLintCache(projectPath, command, fingerprint);
56
+ if (cached !== null) {
57
+ return cached;
58
+ }
59
+ const timeoutMs = lintCheckTimeoutMs();
48
60
  try {
49
- runProjectCommandPlan(plan, projectPath, LINT_CHECK_TIMEOUT_MS);
61
+ runProjectCommandPlan(plan, projectPath, timeoutMs);
62
+ writePassedLintCache(projectPath, command, fingerprint);
50
63
  return { passed: true, command, issueCount: 0, output: '' };
51
64
  }
52
65
  catch (err) {
53
66
  if (isCommandTimeout(err)) {
54
- const seconds = Math.round(LINT_CHECK_TIMEOUT_MS / 1000);
67
+ const seconds = Math.round(timeoutMs / 1000);
55
68
  const output = `Lint command timed out after ${String(seconds)}s and was skipped so validate can complete within MCP request limits. ` +
56
- `Run \`${plan.executedCommand}\` manually before marking the spec done.`;
57
- console.warn(`[Planu] lintCheck timed out: command="${plan.executedCommand}" cwd="${projectPath}" timeoutMs=${String(LINT_CHECK_TIMEOUT_MS)}`);
69
+ `Run \`${plan.executedCommand}\` manually before marking the spec done, or set PLANU_VALIDATE_LINT_TIMEOUT_MS for an explicit full lint pass.`;
70
+ console.warn(`[Planu] lintCheck timed out: command="${plan.executedCommand}" cwd="${projectPath}" timeoutMs=${String(timeoutMs)}`);
58
71
  return { passed: false, command, issueCount: 1, output };
59
72
  }
60
73
  const raw = commandOutput(err);
@@ -64,4 +77,124 @@ export function runLintCheck(projectPath, lintCommand) {
64
77
  return { passed: false, command, issueCount, output };
65
78
  }
66
79
  }
80
+ function lintCheckTimeoutMs() {
81
+ const raw = process.env.PLANU_VALIDATE_LINT_TIMEOUT_MS;
82
+ if (raw === undefined || raw.trim() === '') {
83
+ return DEFAULT_LINT_CHECK_TIMEOUT_MS;
84
+ }
85
+ const parsed = Number.parseInt(raw, 10);
86
+ if (!Number.isFinite(parsed)) {
87
+ return DEFAULT_LINT_CHECK_TIMEOUT_MS;
88
+ }
89
+ return Math.min(300_000, Math.max(1_000, parsed));
90
+ }
91
+ function lintCachePath(projectPath) {
92
+ return join(projectPath, 'planu', 'state', 'lint-evidence.json');
93
+ }
94
+ function readPassedLintCache(projectPath, command, fingerprint) {
95
+ try {
96
+ const path = lintCachePath(projectPath);
97
+ if (!existsSync(path)) {
98
+ return null;
99
+ }
100
+ const cache = JSON.parse(readFileSync(path, 'utf-8'));
101
+ if (cache.schemaVersion !== '1.0.0' ||
102
+ cache.command !== command ||
103
+ cache.fingerprint !== fingerprint ||
104
+ typeof cache.checkedAt !== 'string') {
105
+ return null;
106
+ }
107
+ const ageMs = Date.now() - Date.parse(cache.checkedAt);
108
+ if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > LINT_CACHE_MAX_AGE_MS) {
109
+ return null;
110
+ }
111
+ return {
112
+ passed: true,
113
+ command,
114
+ issueCount: 0,
115
+ output: `(reused local lint evidence from ${cache.checkedAt})`,
116
+ };
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ function writePassedLintCache(projectPath, command, fingerprint) {
123
+ try {
124
+ const path = lintCachePath(projectPath);
125
+ mkdirSync(dirname(path), { recursive: true });
126
+ const cache = {
127
+ schemaVersion: '1.0.0',
128
+ command,
129
+ fingerprint,
130
+ checkedAt: new Date().toISOString(),
131
+ };
132
+ writeFileSync(path, `${JSON.stringify(cache, null, 2)}\n`, 'utf-8');
133
+ }
134
+ catch {
135
+ // Cache is a performance optimization only; lint result remains authoritative.
136
+ }
137
+ }
138
+ function calculateLintFingerprint(projectPath) {
139
+ const hash = createHash('sha256');
140
+ hash.update(gitOutput(projectPath, ['rev-parse', 'HEAD']));
141
+ hash.update('\0');
142
+ hash.update(gitOutput(projectPath, [
143
+ 'diff',
144
+ '--binary',
145
+ 'HEAD',
146
+ '--',
147
+ 'src',
148
+ 'tests',
149
+ 'package.json',
150
+ 'pnpm-lock.yaml',
151
+ 'eslint.config.js',
152
+ 'eslint.config.mjs',
153
+ 'eslint.config.cjs',
154
+ 'tsconfig.json',
155
+ 'tsconfig.build.json',
156
+ ]));
157
+ hash.update('\0');
158
+ const untracked = gitOutput(projectPath, [
159
+ 'ls-files',
160
+ '--others',
161
+ '--exclude-standard',
162
+ '--',
163
+ 'src',
164
+ 'tests',
165
+ ])
166
+ .split('\n')
167
+ .map((line) => line.trim())
168
+ .filter(Boolean)
169
+ .sort();
170
+ for (const file of untracked) {
171
+ hash.update(file);
172
+ hash.update('\0');
173
+ try {
174
+ hash.update(readFileSync(join(projectPath, file)));
175
+ }
176
+ catch {
177
+ hash.update('unreadable');
178
+ }
179
+ hash.update('\0');
180
+ }
181
+ return hash.digest('hex');
182
+ }
183
+ function gitOutput(projectPath, args) {
184
+ try {
185
+ const output = execFileSync('git', args, {
186
+ cwd: projectPath,
187
+ encoding: 'utf-8',
188
+ timeout: 5_000,
189
+ stdio: ['ignore', 'pipe', 'ignore'],
190
+ });
191
+ if (Buffer.isBuffer(output)) {
192
+ return output.toString('utf-8');
193
+ }
194
+ return typeof output === 'string' ? output : String(output);
195
+ }
196
+ catch {
197
+ return '';
198
+ }
199
+ }
67
200
  //# sourceMappingURL=validate-lint.js.map
@@ -72,7 +72,10 @@ export async function handleValidate(args, server) {
72
72
  const implementationQualityScore = calcQualityScore(result.qualityIssues);
73
73
  const auditedFiles = [...new Set(result.qualityIssues.map((i) => i.file))];
74
74
  const { conventionViolations, regressionDetected } = await scanProjectConventions(projectId, projectPath);
75
- const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
75
+ const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null, {
76
+ projectId,
77
+ specId,
78
+ });
76
79
  const assuranceGates = runAssuranceGates(projectPath);
77
80
  const minimalityReport = await buildMinimalityReport({
78
81
  projectPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.10.7",
3
+ "version": "4.10.8",
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.10.7",
38
- "@planu/core-darwin-x64": "4.10.7",
39
- "@planu/core-linux-arm64-gnu": "4.10.7",
40
- "@planu/core-linux-arm64-musl": "4.10.7",
41
- "@planu/core-linux-x64-gnu": "4.10.7",
42
- "@planu/core-linux-x64-musl": "4.10.7",
43
- "@planu/core-win32-arm64-msvc": "4.10.7",
44
- "@planu/core-win32-x64-msvc": "4.10.7"
37
+ "@planu/core-darwin-arm64": "4.10.8",
38
+ "@planu/core-darwin-x64": "4.10.8",
39
+ "@planu/core-linux-arm64-gnu": "4.10.8",
40
+ "@planu/core-linux-arm64-musl": "4.10.8",
41
+ "@planu/core-linux-x64-gnu": "4.10.8",
42
+ "@planu/core-linux-x64-musl": "4.10.8",
43
+ "@planu/core-win32-arm64-msvc": "4.10.8",
44
+ "@planu/core-win32-x64-msvc": "4.10.8"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
package/planu-native.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.10.7",
4
+ "version": "4.10.8",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
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.10.7",
5
+ "version": "4.10.8",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",