@planu/cli 4.10.4 → 4.10.5

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,9 @@
1
+ ## [4.10.5] - 2026-07-07
2
+
3
+ ### Bug Fixes
4
+ - fix(SPEC-1109): resolve audit backlog
5
+
6
+
1
7
  ## [4.10.4] - 2026-07-06
2
8
 
3
9
  ### Bug Fixes
@@ -1,4 +1,4 @@
1
- // version.ts — reads from package.json at runtime; updated automatically by semantic-release
1
+ // version.ts — reads from package.json at runtime; updated by the local release flow
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
@@ -1,4 +1,4 @@
1
- // version.ts — reads from package.json at runtime; updated automatically by semantic-release
1
+ // version.ts — reads from package.json at runtime; updated by the local release flow
2
2
 
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
@@ -1,6 +1,6 @@
1
1
  import { safeJsonParse } from '../../../storage/base-store.js';
2
2
  import { hashText } from '../cache.js';
3
- import { redactGraphText } from '../query.js';
3
+ import { redactGraphText } from '../redaction.js';
4
4
  function isDecision(value) {
5
5
  return (value !== null &&
6
6
  typeof value === 'object' &&
@@ -1,6 +1,6 @@
1
1
  import { promisify } from 'node:util';
2
2
  import { hashText } from '../cache.js';
3
- import { redactGraphText } from '../query.js';
3
+ import { redactGraphText } from '../redaction.js';
4
4
  export async function extractGitGraph(args) {
5
5
  try {
6
6
  const childProcess = await import('node:child_process');
@@ -1,5 +1,5 @@
1
1
  import { hashText } from '../cache.js';
2
- import { redactGraphText } from '../query.js';
2
+ import { redactGraphText } from '../redaction.js';
3
3
  function specIdFromPath(path) {
4
4
  return path.split('/').find((part) => /^SPEC-\d+$/i.test(part));
5
5
  }
@@ -1,6 +1,6 @@
1
1
  import YAML from 'yaml';
2
2
  import { hashText } from '../cache.js';
3
- import { redactGraphText } from '../query.js';
3
+ import { redactGraphText } from '../redaction.js';
4
4
  function frontmatter(content) {
5
5
  if (!content.startsWith('---')) {
6
6
  return {};
@@ -1,5 +1,5 @@
1
1
  import { hashText } from '../cache.js';
2
- import { redactGraphText } from '../query.js';
2
+ import { redactGraphText } from '../redaction.js';
3
3
  function specIdFromPath(path) {
4
4
  return path.split('/').find((part) => /^SPEC-\d+$/i.test(part));
5
5
  }
@@ -1,4 +1,5 @@
1
1
  export { buildProjectKnowledgeGraph } from './builder.js';
2
- export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, redactGraphText, } from './query.js';
2
+ export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, } from './query.js';
3
+ export { redactGraphText } from './redaction.js';
3
4
  export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  export { buildProjectKnowledgeGraph } from './builder.js';
2
- export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, redactGraphText, } from './query.js';
2
+ export { queryProjectGraphSlice, formatProjectGraphContext, getProjectGraphFreshnessHint, } from './query.js';
3
+ export { redactGraphText } from './redaction.js';
3
4
  export { getProjectGraphFreshness, loadProjectGraphPolicy, projectGraphPath, projectGraphCachePath, } from './cache.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,4 @@
1
- import type { ProjectGraphPolicy, ProjectGraphQueryInput, ProjectGraphSlice } from '../../types/project-knowledge-graph.js';
2
- export declare function redactGraphText(text: string, policy: ProjectGraphPolicy, maxChars?: number): string;
1
+ import type { ProjectGraphQueryInput, ProjectGraphSlice } from '../../types/project-knowledge-graph.js';
3
2
  export declare function queryProjectGraphSlice(input: ProjectGraphQueryInput): Promise<ProjectGraphSlice | null>;
4
3
  export declare function formatProjectGraphContext(slice: ProjectGraphSlice): Promise<string>;
5
4
  export declare function getProjectGraphFreshnessHint(args: {
@@ -1,16 +1,7 @@
1
1
  import { hashProjectPath } from '../../storage/base-store.js';
2
2
  import { buildProjectKnowledgeGraph } from './builder.js';
3
3
  import { getProjectGraphFreshness, loadProjectGraphPolicy, readProjectGraph } from './cache.js';
4
- function escapeInert(text) {
5
- return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
6
- }
7
- export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
8
- const truncated = text.slice(0, maxChars);
9
- return policy.redaction.redactPatterns.reduce((current, pattern) => {
10
- const re = new RegExp(pattern, 'gi');
11
- return current.replace(re, '[redacted]');
12
- }, truncated);
13
- }
4
+ import { escapeInert, redactGraphText } from './redaction.js';
14
5
  function sanitizeLabel(label, policy) {
15
6
  return escapeInert(redactGraphText(label, policy));
16
7
  }
@@ -0,0 +1,4 @@
1
+ import type { ProjectGraphPolicy } from '../../types/project-knowledge-graph.js';
2
+ export declare function escapeInert(text: string): string;
3
+ export declare function redactGraphText(text: string, policy: ProjectGraphPolicy, maxChars?: number): string;
4
+ //# sourceMappingURL=redaction.d.ts.map
@@ -0,0 +1,11 @@
1
+ export function escapeInert(text) {
2
+ return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
3
+ }
4
+ export function redactGraphText(text, policy, maxChars = policy.redaction.maxSnippetChars) {
5
+ const truncated = text.slice(0, maxChars);
6
+ return policy.redaction.redactPatterns.reduce((current, pattern) => {
7
+ const re = new RegExp(pattern, 'gi');
8
+ return current.replace(re, '[redacted]');
9
+ }, truncated);
10
+ }
11
+ //# sourceMappingURL=redaction.js.map
@@ -1,15 +1,16 @@
1
- import { appendArtifact } from '../handoff-artifacts/io.js';
1
+ import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
2
2
  import { runSpecCompliance, } from './spec-compliance-runner.js';
3
3
  export async function writeImplementationReviewReport(input) {
4
4
  const specCompliance = await runSpecCompliance(input.spec, input.projectPath);
5
5
  const gates = buildGates({
6
6
  score: input.score,
7
- lintPassed: input.lintPassed ?? true,
7
+ lintPassed: input.lintPassed,
8
8
  conventionRegression: input.conventionRegression ?? false,
9
9
  specCompliance,
10
10
  minimalityReport: input.minimalityReport,
11
11
  });
12
- const passed = gates.every((gate) => gate.passed);
12
+ const mergedGates = await preserveUnobservedBlockingGates(input, gates);
13
+ const passed = mergedGates.every((gate) => gate.passed);
13
14
  const reviewer = {
14
15
  kind: 'implementation-review-agent',
15
16
  agent: 'planu-implementation-reviewer',
@@ -20,7 +21,7 @@ export async function writeImplementationReviewReport(input) {
20
21
  schema_version: '1.0.0',
21
22
  specId: input.specId,
22
23
  passed,
23
- gates,
24
+ gates: mergedGates,
24
25
  reviewer,
25
26
  specCompliance: reportCompliance,
26
27
  minimalityReport: input.minimalityReport,
@@ -40,7 +41,7 @@ export async function writeImplementationReviewReport(input) {
40
41
  sha: artifact.sha,
41
42
  reviewer,
42
43
  passed,
43
- gates,
44
+ gates: mergedGates,
44
45
  specCompliance: reportCompliance,
45
46
  };
46
47
  }
@@ -50,11 +51,38 @@ export async function writeImplementationReviewReport(input) {
50
51
  error: err instanceof Error ? err.message : String(err),
51
52
  reviewer,
52
53
  passed,
53
- gates,
54
+ gates: mergedGates,
54
55
  specCompliance: reportCompliance,
55
56
  };
56
57
  }
57
58
  }
59
+ async function preserveUnobservedBlockingGates(input, nextGates) {
60
+ const currentGateNames = new Set(nextGates.map((gate) => gate.name));
61
+ const explicitlyObserved = new Set([
62
+ ...(input.lintPassed !== undefined ? ['lint'] : []),
63
+ ...(input.conventionRegression !== undefined ? ['convention-regression'] : []),
64
+ ...(input.minimalityReport !== undefined ? ['minimal-implementation'] : []),
65
+ ]);
66
+ const existing = await readArtifact({
67
+ projectId: input.projectId,
68
+ specId: input.specId,
69
+ kind: 'validation-report',
70
+ }).catch(() => null);
71
+ if (!existing?.ok) {
72
+ return nextGates;
73
+ }
74
+ const preserved = existing.payload.gates.filter((gate) => !gate.passed &&
75
+ currentGateNames.has(gate.name) &&
76
+ !explicitlyObserved.has(gate.name) &&
77
+ (gate.name === 'lint' ||
78
+ gate.name === 'convention-regression' ||
79
+ gate.name === 'minimal-implementation'));
80
+ if (preserved.length === 0) {
81
+ return nextGates;
82
+ }
83
+ const preservedByName = new Map(preserved.map((gate) => [gate.name, gate]));
84
+ return nextGates.map((gate) => preservedByName.get(gate.name) ?? gate);
85
+ }
58
86
  function buildGates(args) {
59
87
  return [
60
88
  {
@@ -66,8 +94,12 @@ function buildGates(args) {
66
94
  },
67
95
  {
68
96
  name: 'lint',
69
- passed: args.lintPassed,
70
- reason: args.lintPassed ? undefined : 'Configured lint command failed.',
97
+ passed: args.lintPassed === true,
98
+ reason: args.lintPassed === true
99
+ ? undefined
100
+ : args.lintPassed === false
101
+ ? 'Configured lint command failed.'
102
+ : 'Lint result was not provided; report cannot claim lint passed.',
71
103
  },
72
104
  {
73
105
  name: 'convention-regression',
@@ -63,11 +63,16 @@ export function generateMigrations(tables, indexes, rlsPolicies, engine) {
63
63
  }
64
64
  for (const policy of rlsPolicies) {
65
65
  const rolesClause = policy.roles?.length ? ` TO ${policy.roles.join(', ')}` : '';
66
- const usingClause = `USING (${policy.using})`;
67
- const withCheckClause = policy.withCheck ? ` WITH CHECK (${policy.withCheck})` : '';
66
+ const usingClause = policy.operation === 'INSERT' ? '' : ` USING (${policy.using})`;
67
+ const withCheckClause = policy.withCheck || policy.operation === 'INSERT'
68
+ ? ` WITH CHECK (${policy.withCheck ?? policy.using})`
69
+ : '';
70
+ const policyClauses = [rolesClause, usingClause, withCheckClause]
71
+ .filter((clause) => clause.length > 0)
72
+ .join('');
68
73
  migrations.push({
69
74
  order: order++,
70
- sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${rolesClause} ${usingClause}${withCheckClause};`,
75
+ sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${policyClauses};`,
71
76
  reversible: true,
72
77
  reverseSql: `DROP POLICY IF EXISTS ${policy.name} ON ${policy.table};`,
73
78
  dataLoss: false,
@@ -210,12 +210,28 @@ export function generateRLSPolicies(tables) {
210
210
  }
211
211
  else {
212
212
  policies.push({
213
- name: `${table.name}_all_authenticated`,
213
+ name: `${table.name}_insert_authenticated`,
214
214
  table: table.name,
215
- operation: 'ALL',
215
+ operation: 'INSERT',
216
+ roles: ['authenticated'],
217
+ using: 'true',
218
+ withCheck: 'true',
219
+ justification: 'Authenticated users can insert records',
220
+ }, {
221
+ name: `${table.name}_update_authenticated`,
222
+ table: table.name,
223
+ operation: 'UPDATE',
224
+ roles: ['authenticated'],
225
+ using: 'true',
226
+ withCheck: 'true',
227
+ justification: 'Authenticated users can update records',
228
+ }, {
229
+ name: `${table.name}_delete_authenticated`,
230
+ table: table.name,
231
+ operation: 'DELETE',
216
232
  roles: ['authenticated'],
217
233
  using: 'true',
218
- justification: 'All CRUD operations require authentication',
234
+ justification: 'Authenticated users can delete records',
219
235
  });
220
236
  }
221
237
  }
@@ -55,7 +55,7 @@ export function runLintCheck(projectPath, lintCommand) {
55
55
  const output = `Lint command timed out after ${String(seconds)}s and was skipped so validate can complete within MCP request limits. ` +
56
56
  `Run \`${plan.executedCommand}\` manually before marking the spec done.`;
57
57
  console.warn(`[Planu] lintCheck timed out: command="${plan.executedCommand}" cwd="${projectPath}" timeoutMs=${String(LINT_CHECK_TIMEOUT_MS)}`);
58
- return { passed: true, command, issueCount: 0, output };
58
+ return { passed: false, command, issueCount: 1, output };
59
59
  }
60
60
  const raw = commandOutput(err);
61
61
  const output = formatLintFailureDiagnostics(plan, raw);
@@ -296,6 +296,7 @@ export async function handleValidate(args, server) {
296
296
  passedCount,
297
297
  failedCount,
298
298
  totalCriteria,
299
+ qualityFailures: qualityGateSummary.failures,
299
300
  missing: result.missing,
300
301
  indeterminate,
301
302
  commitReminder,
@@ -400,13 +401,18 @@ function buildCompactSummary(specId, score, passedCount, failedCount, totalCrite
400
401
  };
401
402
  }
402
403
  function buildCompactValidateText(args) {
403
- const { score, passedCount, failedCount, totalCriteria, missing, indeterminate, commitReminder, planuReminder, } = args;
404
- if (failedCount === 0) {
404
+ const { score, passedCount, failedCount, totalCriteria, qualityFailures, missing, indeterminate, commitReminder, planuReminder, } = args;
405
+ if (failedCount === 0 && qualityFailures.length === 0) {
405
406
  // All passing — single line
406
407
  return (`✅ Validate: ${String(score)}/100 — ${String(passedCount)}/${String(totalCriteria)} criteria passing` +
407
408
  commitReminder +
408
409
  planuReminder);
409
410
  }
411
+ if (failedCount === 0) {
412
+ return (`❌ Validate: ${String(score)}/100 — failing; ${String(passedCount)}/${String(totalCriteria)} criteria passing; failing gates: ${qualityFailures.join(', ')}` +
413
+ commitReminder +
414
+ planuReminder);
415
+ }
410
416
  // Failures exist — list only the failing ones (max 5)
411
417
  const MAX_FAILURES = 5;
412
418
  const labelledFailures = [
@@ -423,7 +429,8 @@ function buildCompactValidateText(args) {
423
429
  .slice(0, 3)
424
430
  .map((failure) => `${failure.label}: ${failure.criterion}`)
425
431
  .join(', ');
426
- const summaryLine = `❌ Validate: ${String(score)}/100 ${String(failedCount)} failing: [${failNames}${remainingCount + shownFailures.length > 3 ? '...' : ''}]`;
432
+ const gateSuffix = qualityFailures.length > 0 ? `; failing gates: ${qualityFailures.join(', ')}` : '';
433
+ const summaryLine = `❌ Validate: ${String(score)}/100 — failing; ${String(failedCount)} failing criteria: [${failNames}${remainingCount + shownFailures.length > 3 ? '...' : ''}]${gateSuffix}`;
427
434
  return summaryLine + '\n' + failureList + moreLine + commitReminder + planuReminder;
428
435
  }
429
436
  function scoreToGrade(score) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.10.4",
3
+ "version": "4.10.5",
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.4",
38
- "@planu/core-darwin-x64": "4.10.4",
39
- "@planu/core-linux-arm64-gnu": "4.10.4",
40
- "@planu/core-linux-arm64-musl": "4.10.4",
41
- "@planu/core-linux-x64-gnu": "4.10.4",
42
- "@planu/core-linux-x64-musl": "4.10.4",
43
- "@planu/core-win32-arm64-msvc": "4.10.4",
44
- "@planu/core-win32-x64-msvc": "4.10.4"
37
+ "@planu/core-darwin-arm64": "4.10.5",
38
+ "@planu/core-darwin-x64": "4.10.5",
39
+ "@planu/core-linux-arm64-gnu": "4.10.5",
40
+ "@planu/core-linux-arm64-musl": "4.10.5",
41
+ "@planu/core-linux-x64-gnu": "4.10.5",
42
+ "@planu/core-linux-x64-musl": "4.10.5",
43
+ "@planu/core-win32-arm64-msvc": "4.10.5",
44
+ "@planu/core-win32-x64-msvc": "4.10.5"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
@@ -142,18 +142,12 @@
142
142
  "@napi-rs/cli": "^3.7.2",
143
143
  "@secretlint/secretlint-rule-no-homedir": "^13.0.2",
144
144
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
145
- "@semantic-release/changelog": "^6.0.3",
146
- "@semantic-release/commit-analyzer": "^13.0.1",
147
- "@semantic-release/git": "^10.0.1",
148
- "@semantic-release/github": "^12.0.9",
149
- "@semantic-release/npm": "^13.1.5",
150
- "@semantic-release/release-notes-generator": "^14.1.1",
151
145
  "@stryker-mutator/core": "^9.6.1",
152
146
  "@stryker-mutator/vitest-runner": "^9.6.1",
153
147
  "@supabase/supabase-js": "^2.110.0",
154
148
  "@types/node": "^26.1.0",
155
149
  "@vitejs/plugin-vue": "^6.0.7",
156
- "@vitest/coverage-v8": "^4.1.9",
150
+ "@vitest/coverage-v8": "^4.1.10",
157
151
  "@vue/test-utils": "^2.4.11",
158
152
  "eslint": "^10.6.0",
159
153
  "eslint-config-prettier": "^10.1.8",
@@ -167,13 +161,12 @@
167
161
  "madge": "^8.0.0",
168
162
  "prettier": "^3.9.4",
169
163
  "secretlint": "^13.0.2",
170
- "semantic-release": "^25.0.5",
171
164
  "tsc-alias": "^1.9.0",
172
165
  "type-coverage": "^2.29.7",
173
166
  "typescript": "^6.0.3",
174
167
  "typescript-eslint": "^8.62.1",
175
168
  "vite": "^8.1.3",
176
- "vitest": "^4.1.9",
169
+ "vitest": "^4.1.10",
177
170
  "vue": "^3.5.39"
178
171
  }
179
172
  }
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.4",
4
+ "version": "4.10.5",
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.4",
5
+ "version": "4.10.5",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",
@@ -1,2 +0,0 @@
1
- export { getContextArtifactStats, retrieveContextArtifact, storeContextArtifact } from './store.js';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1,2 +0,0 @@
1
- export { getContextArtifactStats, retrieveContextArtifact, storeContextArtifact } from './store.js';
2
- //# sourceMappingURL=index.js.map