@planu/cli 4.9.0 → 4.10.1

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 (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/config/license-plans.json +5 -0
  3. package/dist/config/registries/hosts/codex.json +3 -4
  4. package/dist/engine/evidence-index/index-builder.js +38 -12
  5. package/dist/engine/project-graph/query.js +16 -1
  6. package/dist/engine/tdd-enforcer/stub-artifact-path.d.ts +2 -0
  7. package/dist/engine/tdd-enforcer/stub-artifact-path.js +7 -0
  8. package/dist/engine/tdd-enforcer/stub-generator.js +3 -2
  9. package/dist/engine/tdd-enforcer/tdd-gate.js +17 -14
  10. package/dist/storage/feedback-remote.d.ts +5 -1
  11. package/dist/storage/feedback-remote.js +75 -16
  12. package/dist/storage/feedback-store.d.ts +3 -1
  13. package/dist/storage/feedback-store.js +107 -10
  14. package/dist/tools/feedback-handler.d.ts +3 -1
  15. package/dist/tools/feedback-handler.js +47 -1
  16. package/dist/tools/generate-execution-plan.js +4 -3
  17. package/dist/tools/register-sdd-tools.d.ts +1 -1
  18. package/dist/tools/register-sdd-tools.js +5 -0
  19. package/dist/tools/status-handler.js +11 -11
  20. package/dist/tools/tool-registry/core-tools.js +97 -0
  21. package/dist/tools/validate-assurance.d.ts +6 -0
  22. package/dist/tools/validate-assurance.js +55 -0
  23. package/dist/tools/validate-graph-coverage.d.ts +8 -0
  24. package/dist/tools/validate-graph-coverage.js +30 -0
  25. package/dist/tools/validate-helpers.d.ts +9 -0
  26. package/dist/tools/validate-helpers.js +47 -0
  27. package/dist/tools/validate-lint.d.ts +3 -0
  28. package/dist/tools/validate-lint.js +67 -0
  29. package/dist/tools/validate-minimality.d.ts +16 -0
  30. package/dist/tools/validate-minimality.js +22 -0
  31. package/dist/tools/validate-runtime.d.ts +14 -0
  32. package/dist/tools/validate-runtime.js +85 -0
  33. package/dist/tools/validate.js +5 -222
  34. package/dist/types/evidence-index.d.ts +1 -0
  35. package/dist/types/feedback.d.ts +37 -0
  36. package/package.json +12 -12
  37. package/planu-native.json +1 -1
  38. package/planu-plugin.json +1 -1
@@ -1,7 +1,5 @@
1
1
  // tools/validate.ts — Compare spec vs implementation (SPEC-595: elicitation on failures)
2
2
  // Uses the validator engine to check coverage, missing items, quality issues.
3
- import { execSync, execFileSync } from 'node:child_process';
4
- import { readFileSync } from 'node:fs';
5
3
  import { elicitOrFallback, buildEnumSchema } from '../engine/elicitation/elicit-helper.js';
6
4
  import { compactObj } from '../engine/compact-obj.js';
7
5
  import { resolveProjectId, missingProjectIdError } from './resolve-project-id.js';
@@ -16,104 +14,13 @@ import { parseConventions, scanConventions } from '../engine/convention-scanner/
16
14
  import { validateScopeCompliance } from '../engine/scope-boundaries/index.js';
17
15
  import { compareWithBaseline } from '../storage/convention-baseline.js';
18
16
  import { writeImplementationReviewReport } from '../engine/validator/validation-report-writer.js';
19
- import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
20
- import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
21
- import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
17
+ import { buildGraphCoverageReport, formatGraphCoverageText } from './validate-graph-coverage.js';
18
+ import { buildMinimalityReport } from './validate-minimality.js';
19
+ import { buildValidateFailureFallback, validateStrictLayoutOrError } from './validate-helpers.js';
20
+ import { runLintCheck } from './validate-lint.js';
21
+ import { checkPlanuUncommittedChanges, runAssuranceGates } from './validate-assurance.js';
22
22
  // Re-export for external use (SPEC-018)
23
23
  export { validateContractCompliance };
24
- function graphCoverageGaps(slice) {
25
- if (slice === null) {
26
- return [];
27
- }
28
- const criteria = slice.nodes.filter((node) => node.type === 'criterion');
29
- return criteria
30
- .filter((criterion) => {
31
- const outgoing = slice.edges.filter((edge) => edge.from === criterion.id);
32
- return !outgoing.some((edge) => edge.type === 'implements' || edge.type === 'tests');
33
- })
34
- .map((criterion) => criterion.label)
35
- .slice(0, 10);
36
- }
37
- async function buildGraphCoverageReport(args) {
38
- const graphSlice = await queryProjectGraphSlice(args).catch(() => null);
39
- return {
40
- freshness: graphSlice?.freshness,
41
- gaps: graphCoverageGaps(graphSlice),
42
- compactNodes: graphSlice?.nodes.length ?? 0,
43
- compactEdges: graphSlice?.edges.length ?? 0,
44
- };
45
- }
46
- function formatGraphCoverageText(report) {
47
- return report.gaps.length > 0
48
- ? `\nGRAPH ${String(report.gaps.length)} graph-backed coverage gap(s)`
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
- }
70
- async function validateStrictLayoutOrError(args) {
71
- try {
72
- const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
73
- const layout = await validateStrictPlanuLayout(args.projectPath, {
74
- specPath: args.specPath,
75
- includeRoot: false,
76
- });
77
- if (layout.ok) {
78
- return null;
79
- }
80
- return {
81
- content: [
82
- {
83
- type: 'text',
84
- text: `Strict Planu layout validation failed for ${args.specId}.\n\n` +
85
- `Non-canonical paths:\n${layout.offenders.map((p) => `- ${p}`).join('\n')}\n\n` +
86
- `Canonical contract:\n${layout.contract}`,
87
- },
88
- ],
89
- isError: true,
90
- structuredContent: {
91
- error: 'strict_planu_layout_violation',
92
- offenders: layout.offenders,
93
- contract: layout.contract,
94
- },
95
- };
96
- }
97
- catch {
98
- return null;
99
- }
100
- }
101
- /** Build the fallback interactiveQuestion for validation failures. */
102
- function buildValidateFailureFallback(failedCount) {
103
- return {
104
- header: 'Failures',
105
- question: `${failedCount} criteria failed. How do you want to proceed?`,
106
- multiSelect: false,
107
- options: [
108
- {
109
- label: 're-implement',
110
- description: 'Fix the failing criteria and run validate again (Recommended)',
111
- },
112
- { label: 'ignore', description: 'Ignore failures and proceed (not recommended)' },
113
- { label: 'mark-manual', description: 'Mark as manually reviewed — will bypass auto-check' },
114
- ],
115
- };
116
- }
117
24
  // SPEC-595: server? param enables elicitation on failures
118
25
  export async function handleValidate(args, server) {
119
26
  const { specId } = args;
@@ -478,130 +385,6 @@ function buildCompactValidateText(args) {
478
385
  const summaryLine = `❌ Validate: ${String(score)}/100 — ${String(failedCount)} failing: [${failNames}${remainingCount + shownFailures.length > 3 ? '...' : ''}]`;
479
386
  return summaryLine + '\n' + failureList + moreLine + commitReminder + planuReminder;
480
387
  }
481
- /** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
482
- const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
483
- const LINT_CHECK_TIMEOUT_MS = 50_000;
484
- function isSafeCommand(cmd) {
485
- return SAFE_COMMAND_RE.test(cmd);
486
- }
487
- function isCommandTimeout(err) {
488
- if (!(err instanceof Error)) {
489
- return false;
490
- }
491
- const errorWithProcessFields = err;
492
- return (errorWithProcessFields.signal === 'SIGTERM' ||
493
- errorWithProcessFields.killed === true ||
494
- errorWithProcessFields.code === 'ETIMEDOUT' ||
495
- /timed out|timeout/i.test(err.message));
496
- }
497
- function commandOutput(err) {
498
- if (!(err instanceof Error)) {
499
- return String(err);
500
- }
501
- const errorWithOutput = err;
502
- return [errorWithOutput.stdout, errorWithOutput.stderr]
503
- .filter((chunk) => chunk !== undefined)
504
- .map((chunk) => String(chunk))
505
- .join('\n')
506
- .trim();
507
- }
508
- function runAssuranceGates(projectPath) {
509
- return {
510
- newCode: evaluateNewCodeGate(readChangedSpecLogicFiles(projectPath)),
511
- };
512
- }
513
- function readChangedSpecLogicFiles(projectPath) {
514
- let changedFiles;
515
- try {
516
- const output = execFileSync('git', [
517
- '-C',
518
- projectPath,
519
- 'diff',
520
- '--name-only',
521
- 'HEAD',
522
- '--',
523
- 'src/tools/create-spec',
524
- 'src/tools/validate.ts',
525
- 'src/engine',
526
- ], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
527
- changedFiles = output
528
- .split('\n')
529
- .map((line) => line.trim())
530
- .filter((line) => line.endsWith('.ts'));
531
- }
532
- catch {
533
- return [];
534
- }
535
- return changedFiles.flatMap((path) => {
536
- try {
537
- return [{ path, content: readFileSync(`${projectPath}/${path}`, 'utf-8') }];
538
- }
539
- catch {
540
- return [];
541
- }
542
- });
543
- }
544
- function runLintCheck(projectPath, lintCommand) {
545
- if (lintCommand !== null && !isSafeCommand(lintCommand)) {
546
- console.warn(`[Planu] validate: lintCommand contains unsafe characters — skipping execution`);
547
- return {
548
- passed: true,
549
- command: lintCommand,
550
- issueCount: 0,
551
- output: '(skipped — unsafe command)',
552
- };
553
- }
554
- // SPEC-787: use pnpm lint (same as `pnpm lint` in package.json) to match what
555
- // the developer runs manually. The old `npx eslint src ... || true` never raised
556
- // an error (|| true suppressed exit code) and ran against a different scope.
557
- const command = lintCommand ?? 'pnpm lint';
558
- try {
559
- execSync(command, {
560
- cwd: projectPath,
561
- stdio: ['pipe', 'pipe', 'pipe'],
562
- timeout: LINT_CHECK_TIMEOUT_MS,
563
- });
564
- return { passed: true, command, issueCount: 0, output: '' };
565
- }
566
- catch (err) {
567
- if (isCommandTimeout(err)) {
568
- const seconds = Math.round(LINT_CHECK_TIMEOUT_MS / 1000);
569
- const output = `Lint command timed out after ${String(seconds)}s and was skipped so validate can complete within MCP request limits. ` +
570
- `Run \`${command}\` manually before marking the spec done.`;
571
- console.warn(`[Planu] lintCheck timed out: command="${command}" cwd="${projectPath}" timeoutMs=${String(LINT_CHECK_TIMEOUT_MS)}`);
572
- return { passed: true, command, issueCount: 0, output };
573
- }
574
- const raw = commandOutput(err);
575
- // SPEC-787: never truncate — preserve full output so caller sees real issues
576
- const output = raw.trim();
577
- const issueCount = parseLintIssueCount(output);
578
- // SPEC-787: log exact command + cwd for debuggability
579
- console.warn(`[Planu] lintCheck failed: command="${command}" cwd="${projectPath}" issues=${String(issueCount)}`);
580
- return { passed: false, command, issueCount, output };
581
- }
582
- }
583
- function parseLintIssueCount(output) {
584
- const m = /(\d+)\s+problem/i.exec(output);
585
- if (m) {
586
- return parseInt(m[1] ?? '0', 10);
587
- }
588
- return output.split('\n').filter((l) => l.trim().length > 0).length;
589
- }
590
- /** Check if planu/ directory has uncommitted changes via git status --porcelain */
591
- function checkPlanuUncommittedChanges(projectPath) {
592
- try {
593
- const output = execFileSync('git', ['status', '--porcelain', 'planu/'], {
594
- cwd: projectPath,
595
- encoding: 'utf-8',
596
- timeout: 5_000,
597
- stdio: ['pipe', 'pipe', 'pipe'],
598
- });
599
- return output.trim().length > 0;
600
- }
601
- catch {
602
- return false;
603
- }
604
- }
605
388
  function scoreToGrade(score) {
606
389
  if (score >= 90) {
607
390
  return 'A';
@@ -1,5 +1,6 @@
1
1
  export type EvidenceRecordKind = 'scenario' | 'test' | 'contract' | 'manual' | 'changed-file' | 'validation' | 'reviewer';
2
2
  export type EvidenceRecordStatus = 'valid' | 'stale' | 'invalid' | 'missing';
3
+ export type EvidencePathField = 'testEvidence' | 'contractEvidence' | 'changedFiles' | 'contractValidation.reportPath';
3
4
  export interface EvidenceRecord {
4
5
  kind: EvidenceRecordKind;
5
6
  value: string;
@@ -1,5 +1,20 @@
1
1
  export type FeedbackType = 'bug' | 'feature' | 'suggestion' | 'dx';
2
2
  export type FeedbackStatus = 'pending' | 'triaged' | 'spec_created' | 'implementing' | 'resolved' | 'dismissed';
3
+ export type FeedbackRemoteStatus = 'pending' | 'synced' | 'failed' | 'skipped';
4
+ export interface FeedbackRemoteDelivery {
5
+ status: FeedbackRemoteStatus;
6
+ attemptCount: number;
7
+ lastAttemptAt?: string;
8
+ lastSyncedAt?: string;
9
+ lastError?: string;
10
+ endpoint?: string;
11
+ }
12
+ export interface FeedbackRemoteResult {
13
+ ok: boolean;
14
+ status: 'synced' | 'failed' | 'skipped';
15
+ endpoint?: string;
16
+ error?: string;
17
+ }
3
18
  export interface FeedbackEntry {
4
19
  id: string;
5
20
  type: FeedbackType;
@@ -20,6 +35,7 @@ export interface FeedbackEntry {
20
35
  planuVersion: string;
21
36
  tags?: string[];
22
37
  votes?: number;
38
+ remoteDelivery?: FeedbackRemoteDelivery;
23
39
  }
24
40
  export interface FeedbackFilter {
25
41
  type?: FeedbackType;
@@ -39,6 +55,10 @@ export interface FeedbackSummary {
39
55
  id: string;
40
56
  }[];
41
57
  }
58
+ export interface FeedbackRemoteHealth {
59
+ byRemoteStatus: Record<FeedbackRemoteStatus, number>;
60
+ retryable: number;
61
+ }
42
62
  export interface TriageGroup {
43
63
  representative: FeedbackEntry;
44
64
  similar: FeedbackEntry[];
@@ -65,7 +85,24 @@ export interface ResolveFeedbackInput {
65
85
  prUrl?: string;
66
86
  notes?: string;
67
87
  }
88
+ export interface SyncFeedbackInput {
89
+ projectPath?: string;
90
+ includeSynced?: boolean;
91
+ limit?: number;
92
+ }
93
+ export interface FeedbackStatusInput {
94
+ projectPath?: string;
95
+ }
96
+ export interface SyncFeedbackSummary {
97
+ total: number;
98
+ synced: number;
99
+ failed: number;
100
+ skipped: number;
101
+ alreadySynced: number;
102
+ processedIds: string[];
103
+ }
68
104
  export interface RemoteFeedbackPayload {
105
+ id?: string;
69
106
  type: string;
70
107
  title: string;
71
108
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.9.0",
3
+ "version": "4.10.1",
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.9.0",
38
- "@planu/core-darwin-x64": "4.9.0",
39
- "@planu/core-linux-arm64-gnu": "4.9.0",
40
- "@planu/core-linux-arm64-musl": "4.9.0",
41
- "@planu/core-linux-x64-gnu": "4.9.0",
42
- "@planu/core-linux-x64-musl": "4.9.0",
43
- "@planu/core-win32-arm64-msvc": "4.9.0",
44
- "@planu/core-win32-x64-msvc": "4.9.0"
37
+ "@planu/core-darwin-arm64": "4.10.1",
38
+ "@planu/core-darwin-x64": "4.10.1",
39
+ "@planu/core-linux-arm64-gnu": "4.10.1",
40
+ "@planu/core-linux-arm64-musl": "4.10.1",
41
+ "@planu/core-linux-x64-gnu": "4.10.1",
42
+ "@planu/core-linux-x64-musl": "4.10.1",
43
+ "@planu/core-win32-arm64-msvc": "4.10.1",
44
+ "@planu/core-win32-x64-msvc": "4.10.1"
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.107.0",
132
+ "@anthropic-ai/sdk": "^0.109.0",
133
133
  "@modelcontextprotocol/sdk": "^1.29.0",
134
134
  "glob": "^13.0.6",
135
135
  "yaml": "^2.9.0",
@@ -183,7 +183,7 @@
183
183
  "@stryker-mutator/core": "^9.6.1",
184
184
  "@stryker-mutator/vitest-runner": "^9.6.1",
185
185
  "@supabase/supabase-js": "^2.110.0",
186
- "@types/node": "^26.0.1",
186
+ "@types/node": "^26.1.0",
187
187
  "@vitejs/plugin-vue": "^6.0.7",
188
188
  "@vitest/coverage-v8": "^4.1.9",
189
189
  "@vue/test-utils": "^2.4.11",
@@ -204,7 +204,7 @@
204
204
  "type-coverage": "^2.29.7",
205
205
  "typescript": "^6.0.3",
206
206
  "typescript-eslint": "^8.62.1",
207
- "vite": "^8.1.1",
207
+ "vite": "^8.1.2",
208
208
  "vitest": "^4.1.9",
209
209
  "vue": "^3.5.39"
210
210
  }
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.9.0",
4
+ "version": "4.10.1",
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.9.0",
5
+ "version": "4.10.1",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",