@planu/cli 4.10.5 → 4.10.6

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 (31) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/engine/compact/tool-list-compactor.d.ts +1 -1
  3. package/dist/engine/evidence-gates/lifecycle-gate.js +17 -1
  4. package/dist/engine/evidence-index/done-drift.js +4 -1
  5. package/dist/engine/local-first/tool-classification.d.ts +6 -0
  6. package/dist/engine/local-first/tool-classification.js +119 -0
  7. package/dist/engine/qa-gate.js +13 -1
  8. package/dist/engine/scope-boundaries/scope-validator.js +14 -2
  9. package/dist/engine/validator/validation-report-writer.js +3 -4
  10. package/dist/tools/list-specs.js +15 -59
  11. package/dist/tools/output-compressor.d.ts +14 -1
  12. package/dist/tools/output-compressor.js +121 -2
  13. package/dist/tools/package-handoff.js +136 -12
  14. package/dist/tools/status-handler.js +6 -1
  15. package/dist/tools/token-recording.d.ts +2 -1
  16. package/dist/tools/token-recording.js +21 -2
  17. package/dist/tools/update-status/dod-gates.d.ts +2 -2
  18. package/dist/tools/update-status/dod-gates.js +8 -28
  19. package/dist/tools/update-status/index.js +7 -1
  20. package/dist/tools/update-status/qa-gate.d.ts +5 -0
  21. package/dist/tools/update-status/qa-gate.js +50 -0
  22. package/dist/tools/update-status/response-builder.js +96 -2
  23. package/dist/tools/update-status-convention-gate.js +22 -6
  24. package/dist/tools/validate.js +137 -16
  25. package/dist/transports/transport-factory.js +13 -0
  26. package/dist/types/qa-gate.d.ts +3 -0
  27. package/dist/types/tool-classification.d.ts +8 -0
  28. package/dist/types/tool-classification.js +2 -0
  29. package/package.json +1 -1
  30. package/planu-native.json +8 -29
  31. package/planu-plugin.json +7 -35
@@ -22,13 +22,13 @@ import { checkPlanuUncommittedChanges, runAssuranceGates } from './validate-assu
22
22
  // Re-export for external use (SPEC-018)
23
23
  export { validateContractCompliance };
24
24
  // SPEC-595: server? param enables elicitation on failures
25
+ // eslint-disable-next-line max-lines-per-function -- validate orchestrates many existing gates in one handler.
25
26
  export async function handleValidate(args, server) {
26
27
  const { specId } = args;
27
28
  const projectId = resolveProjectId(args);
28
29
  if (!projectId) {
29
30
  return missingProjectIdError;
30
31
  }
31
- // 1. Load spec
32
32
  const spec = await specStore.getSpec(projectId, specId);
33
33
  if (!spec) {
34
34
  return {
@@ -42,7 +42,6 @@ export async function handleValidate(args, server) {
42
42
  },
43
43
  };
44
44
  }
45
- // 2. Load project knowledge for project path
46
45
  const knowledge = await knowledgeStore.getKnowledge(projectId);
47
46
  if (!knowledge) {
48
47
  return {
@@ -66,18 +65,13 @@ export async function handleValidate(args, server) {
66
65
  if (strictLayoutError !== null) {
67
66
  return strictLayoutError;
68
67
  }
69
- // 3. Run validation engine
70
68
  const result = await validateSpec(spec, projectPath);
71
69
  const graphCoverage = await buildGraphCoverageReport({ projectId, projectPath, specId });
72
- // 4. Generate DoR and DoD checklists
73
70
  const dor = generateDoR(spec);
74
71
  const dod = await generateDoD(spec, result, undefined, projectPath);
75
- // 5. Compute implementation quality score from validated files (SPEC-010 S47)
76
72
  const implementationQualityScore = calcQualityScore(result.qualityIssues);
77
73
  const auditedFiles = [...new Set(result.qualityIssues.map((i) => i.file))];
78
- // 6. SPEC-190: Convention compliance check
79
74
  const { conventionViolations, regressionDetected } = await scanProjectConventions(projectId, projectPath);
80
- // 7. Lint check (best-effort — non-blocking)
81
75
  const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
82
76
  const assuranceGates = runAssuranceGates(projectPath);
83
77
  const minimalityReport = await buildMinimalityReport({
@@ -108,7 +102,6 @@ export async function handleValidate(args, server) {
108
102
  minimalityBlocked: minimalityReport?.blocked === true,
109
103
  score: result.score,
110
104
  });
111
- // 8. Build output
112
105
  const output = {
113
106
  specId,
114
107
  title: spec.title,
@@ -239,13 +232,6 @@ export async function handleValidate(args, server) {
239
232
  if (minimalityReport?.blocked) {
240
233
  suggestions.push(`${minimalityReport.findings.filter((finding) => finding.blocksDone).length} blocking minimality finding(s) detected. Remove avoidable complexity or record accepted debt before marking done.`);
241
234
  }
242
- const outputWithSuggestions = compactObj({
243
- ...output,
244
- suggestions: suggestions.length > 0 ? suggestions : undefined,
245
- });
246
- // `lintCheck.output` is part of the MCP output contract. Keep it even when
247
- // empty because compactObj removes empty strings recursively.
248
- outputWithSuggestions.lintCheck = lintCheck;
249
235
  // SPEC-012: Dispatch feedback event for auto-improvement loop (best-effort)
250
236
  void dispatchFeedbackEvent(projectId, {
251
237
  type: 'validate',
@@ -305,7 +291,26 @@ export async function handleValidate(args, server) {
305
291
  const graphText = formatGraphCoverageText(graphCoverage);
306
292
  // SPEC-512: Compact structuredContent — essential fields only at top level
307
293
  const compactSummary = buildCompactSummary(specId, result.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
308
- const structuredBase = { ...outputWithSuggestions, summary: compactSummary, indeterminate };
294
+ const structuredBase = buildValidateStructuredContent({
295
+ specId,
296
+ title: spec.title,
297
+ score: result.score === null ? null : effectiveScore,
298
+ compactSummary,
299
+ result,
300
+ indeterminate,
301
+ dor,
302
+ dod,
303
+ qualityIssues: output.qualityIssues,
304
+ implementationQualityScore: output.implementationQualityScore,
305
+ conventionCompliance: output.conventionCompliance,
306
+ lintCheck,
307
+ assuranceGates,
308
+ qualityGateSummary,
309
+ validationReport,
310
+ minimalityReport,
311
+ graphCoverage,
312
+ suggestions,
313
+ });
309
314
  // SPEC-595: Elicit next action when validation fails and a server is available
310
315
  if (server !== undefined && failedCount > 0) {
311
316
  const { field, property } = buildEnumSchema('action', ['re-implement', 'ignore', 'mark-manual'], ['Fix failing criteria (Recommended)', 'Ignore failures', 'Mark as manually reviewed'], 'Next action', 're-implement');
@@ -400,6 +405,122 @@ function buildCompactSummary(specId, score, passedCount, failedCount, totalCrite
400
405
  status: failedCount === 0 ? 'passing' : 'failing',
401
406
  };
402
407
  }
408
+ function buildValidateStructuredContent(args) {
409
+ const failedDorItems = args.dor.items.filter((item) => item.status === 'failed').slice(0, 5);
410
+ const failedDodItems = args.dod.items
411
+ .filter((item) => item.required === true && item.status !== 'passed')
412
+ .slice(0, 5);
413
+ const failedGates = args.validationReport.gates.filter((gate) => !gate.passed);
414
+ const minimalityBlockingFindings = args.minimalityReport?.findings.filter((finding) => finding.blocksDone) ?? [];
415
+ const blockers = [
416
+ ...args.result.missing.map((missing) => `missing: ${missing}`),
417
+ ...args.indeterminate.map((item) => `indeterminate: ${item}`),
418
+ ...args.qualityGateSummary.failures.map((failure) => `gate: ${failure}`),
419
+ ...args.qualityIssues.map((issue) => `${issue.rule}: ${issue.message}`),
420
+ ];
421
+ const risks = [
422
+ ...args.conventionCompliance.violations.map((violation) => violation.message),
423
+ ...minimalityBlockingFindings.map((finding) => `${finding.ruleId}: ${finding.target}`),
424
+ ...args.suggestions,
425
+ ];
426
+ const structured = compactObj({
427
+ specId: args.specId,
428
+ title: args.title,
429
+ ready: args.compactSummary.status === 'passing' && args.qualityGateSummary.passed,
430
+ status: args.compactSummary.status,
431
+ score: args.score,
432
+ summary: args.compactSummary,
433
+ counts: {
434
+ matches: args.result.matches.length,
435
+ missing: args.result.missing.length,
436
+ extra: args.result.extra.length,
437
+ indeterminate: args.indeterminate.length,
438
+ qualityIssues: args.qualityIssues.length,
439
+ conventionViolations: args.conventionCompliance.totalViolations,
440
+ graphGaps: args.graphCoverage.gaps.length,
441
+ failedGates: failedGates.length,
442
+ },
443
+ missingTop5: args.result.missing.slice(0, 5),
444
+ extraTop5: args.result.extra.slice(0, 5),
445
+ indeterminateTop5: args.indeterminate.slice(0, 5),
446
+ qualityIssuesTop5: args.qualityIssues.slice(0, 5),
447
+ qualityGateSummary: args.qualityGateSummary,
448
+ blockersCount: blockers.length,
449
+ blockersTop3: blockers.slice(0, 3),
450
+ risksTop5: risks.slice(0, 5),
451
+ autofixable: args.suggestions.length > 0 || args.qualityIssues.length > 0,
452
+ nextAction: args.suggestions[0] ??
453
+ (args.compactSummary.status === 'passing'
454
+ ? 'Proceed to update_status(done) after required release checks pass.'
455
+ : 'Fix the top blockers and rerun validate.'),
456
+ artifactRefs: {
457
+ validationReport: args.validationReport.path,
458
+ validationReportSha: args.validationReport.sha,
459
+ },
460
+ validationReport: {
461
+ written: args.validationReport.written,
462
+ path: args.validationReport.path,
463
+ sha: args.validationReport.sha,
464
+ passed: args.validationReport.passed,
465
+ reviewer: args.validationReport.reviewer,
466
+ gatesCount: args.validationReport.gates.length,
467
+ failedGatesTop5: failedGates.slice(0, 5),
468
+ },
469
+ implementationQualityScore: args.implementationQualityScore,
470
+ definitionOfReady: {
471
+ isReady: args.dor.isReady,
472
+ passed: args.dor.passedCount,
473
+ required: args.dor.requiredCount,
474
+ items: failedDorItems,
475
+ failedItemsTop5: failedDorItems,
476
+ },
477
+ definitionOfDone: {
478
+ isDone: args.dod.isDone,
479
+ passed: args.dod.passedCount,
480
+ required: args.dod.requiredCount,
481
+ failedItems: failedDodItems,
482
+ failedItemsTop5: failedDodItems,
483
+ },
484
+ conventionCompliance: {
485
+ totalViolations: args.conventionCompliance.totalViolations,
486
+ regressionDetected: args.conventionCompliance.regressionDetected,
487
+ violationsTop5: args.conventionCompliance.violations.slice(0, 5),
488
+ },
489
+ lintCheck: {
490
+ passed: args.lintCheck.passed,
491
+ issueCount: args.lintCheck.issueCount,
492
+ command: args.lintCheck.command,
493
+ output: args.lintCheck.output ?? '',
494
+ outputPreview: typeof args.lintCheck.output === 'string' ? args.lintCheck.output.slice(0, 500) : undefined,
495
+ },
496
+ assuranceGates: {
497
+ newCode: {
498
+ passed: args.assuranceGates.newCode.passed,
499
+ findingsCount: args.assuranceGates.newCode.findings.length,
500
+ findingsTop5: args.assuranceGates.newCode.findings.slice(0, 5),
501
+ },
502
+ },
503
+ minimalityReport: args.minimalityReport !== null
504
+ ? {
505
+ enabled: args.minimalityReport.enabled,
506
+ blocked: args.minimalityReport.blocked,
507
+ findingCount: args.minimalityReport.findings.length,
508
+ blockingFindingCount: minimalityBlockingFindings.length,
509
+ blockingFindingsTop5: minimalityBlockingFindings.slice(0, 5),
510
+ }
511
+ : undefined,
512
+ graphCoverage: {
513
+ gapsCount: args.graphCoverage.gaps.length,
514
+ gapsTop5: args.graphCoverage.gaps.slice(0, 5),
515
+ freshness: args.graphCoverage.freshness,
516
+ summary: args.graphCoverage.summary,
517
+ },
518
+ suggestionsTop5: args.suggestions.slice(0, 5),
519
+ });
520
+ const lintCheck = structured.lintCheck;
521
+ lintCheck.output = args.lintCheck.output ?? '';
522
+ return structured;
523
+ }
403
524
  function buildCompactValidateText(args) {
404
525
  const { score, passedCount, failedCount, totalCriteria, qualityFailures, missing, indeterminate, commitReminder, planuReminder, } = args;
405
526
  if (failedCount === 0 && qualityFailures.length === 0) {
@@ -1,6 +1,7 @@
1
1
  // transport-factory.ts — Parses CLI flags and selects the appropriate MCP transport.
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { createHttpTransport } from './http-transport.js';
4
+ import { compactToolsListMessage, isToolsListResponse, } from '../engine/compact/tool-list-compactor.js';
4
5
  const VALID_TRANSPORTS = ['stdio', 'http'];
5
6
  const DEFAULT_PORT = 3100;
6
7
  const DEFAULT_HOST = '127.0.0.1';
@@ -40,6 +41,7 @@ export async function selectTransport(server, args, serverFactory) {
40
41
  const config = parseTransportConfig(args);
41
42
  if (config.transport === 'stdio') {
42
43
  const transport = new StdioServerTransport();
44
+ installToolsListCompaction(transport);
43
45
  await server.connect(transport);
44
46
  installStdioShutdownHandlers(server);
45
47
  return;
@@ -48,6 +50,17 @@ export async function selectTransport(server, args, serverFactory) {
48
50
  const factory = serverFactory ?? (() => server);
49
51
  await createHttpTransport(factory, config);
50
52
  }
53
+ function installToolsListCompaction(transport) {
54
+ const originalSend = transport.send.bind(transport);
55
+ const compactingSend = (message) => {
56
+ const outgoing = isToolsListResponse(message)
57
+ ? compactToolsListMessage(message)
58
+ : message;
59
+ return originalSend(outgoing);
60
+ };
61
+ const wrappedTransport = transport;
62
+ wrappedTransport.send = compactingSend;
63
+ }
51
64
  function installStdioShutdownHandlers(server) {
52
65
  let shuttingDown = false;
53
66
  const shutdown = (reason) => {
@@ -4,6 +4,9 @@ export interface QaCheckResult {
4
4
  passed: boolean;
5
5
  output: string;
6
6
  durationMs: number;
7
+ command?: string;
8
+ timedOut?: boolean;
9
+ errorMessage?: string;
7
10
  }
8
11
  export interface QaGateResult {
9
12
  specId: string;
@@ -0,0 +1,8 @@
1
+ export type ToolExecutionClass = 'local' | 'hybrid' | 'llm-required';
2
+ export interface ToolClassification {
3
+ toolName: string;
4
+ executionClass: ToolExecutionClass;
5
+ tokenPolicy: 'no-llm' | 'summarize-first' | 'llm-product';
6
+ reason: string;
7
+ }
8
+ //# sourceMappingURL=tool-classification.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool-classification.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.10.5",
3
+ "version": "4.10.6",
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",
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.10.5",
4
+ "version": "4.10.6",
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-first release readiness, branch cleanliness, and optional gitflow drift.",
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.10.5",
5
+ "version": "4.10.6",
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
  }