@planu/cli 4.10.4 → 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 (49) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/dist/config/version.js +1 -1
  3. package/dist/config/version.ts +1 -1
  4. package/dist/engine/compact/tool-list-compactor.d.ts +1 -1
  5. package/dist/engine/evidence-gates/lifecycle-gate.js +17 -1
  6. package/dist/engine/evidence-index/done-drift.js +4 -1
  7. package/dist/engine/local-first/tool-classification.d.ts +6 -0
  8. package/dist/engine/local-first/tool-classification.js +119 -0
  9. package/dist/engine/project-graph/extractors/decision-store-extractor.js +1 -1
  10. package/dist/engine/project-graph/extractors/git-extractor.js +1 -1
  11. package/dist/engine/project-graph/extractors/handoff-extractor.js +1 -1
  12. package/dist/engine/project-graph/extractors/spec-extractor.js +1 -1
  13. package/dist/engine/project-graph/extractors/validation-extractor.js +1 -1
  14. package/dist/engine/project-graph/index.d.ts +2 -1
  15. package/dist/engine/project-graph/index.js +2 -1
  16. package/dist/engine/project-graph/query.d.ts +1 -2
  17. package/dist/engine/project-graph/query.js +1 -10
  18. package/dist/engine/project-graph/redaction.d.ts +4 -0
  19. package/dist/engine/project-graph/redaction.js +11 -0
  20. package/dist/engine/qa-gate.js +13 -1
  21. package/dist/engine/scope-boundaries/scope-validator.js +14 -2
  22. package/dist/engine/validator/validation-report-writer.js +39 -8
  23. package/dist/tools/design-schema-sql/migrations.js +8 -3
  24. package/dist/tools/design-schema-sql/tables.js +19 -3
  25. package/dist/tools/list-specs.js +15 -59
  26. package/dist/tools/output-compressor.d.ts +14 -1
  27. package/dist/tools/output-compressor.js +121 -2
  28. package/dist/tools/package-handoff.js +136 -12
  29. package/dist/tools/status-handler.js +6 -1
  30. package/dist/tools/token-recording.d.ts +2 -1
  31. package/dist/tools/token-recording.js +21 -2
  32. package/dist/tools/update-status/dod-gates.d.ts +2 -2
  33. package/dist/tools/update-status/dod-gates.js +8 -28
  34. package/dist/tools/update-status/index.js +7 -1
  35. package/dist/tools/update-status/qa-gate.d.ts +5 -0
  36. package/dist/tools/update-status/qa-gate.js +50 -0
  37. package/dist/tools/update-status/response-builder.js +96 -2
  38. package/dist/tools/update-status-convention-gate.js +22 -6
  39. package/dist/tools/validate-lint.js +1 -1
  40. package/dist/tools/validate.js +147 -19
  41. package/dist/transports/transport-factory.js +13 -0
  42. package/dist/types/qa-gate.d.ts +3 -0
  43. package/dist/types/tool-classification.d.ts +8 -0
  44. package/dist/types/tool-classification.js +2 -0
  45. package/package.json +11 -18
  46. package/planu-native.json +8 -29
  47. package/planu-plugin.json +7 -35
  48. package/dist/engine/context-artifacts/index.d.ts +0 -2
  49. package/dist/engine/context-artifacts/index.js +0 -2
@@ -116,7 +116,10 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
116
116
  lines.push(`\nšŸ¤– Tokens: ${tokenStr}${costStr}${r.actuals.estimated ? ' *(auto-estimated)*' : ''}`);
117
117
  }
118
118
  if (r.validateScore !== null && r.validateScore !== undefined) {
119
- lines.push(`\nāœ… Validate score: ${String(r.validateScore)}/100`);
119
+ const sourceSuffix = r.validateScoreSource === 'forced-best-effort-validateSpec'
120
+ ? ' (forced best-effort observation from validateSpec)'
121
+ : '';
122
+ lines.push(`\nāœ… Validate score: ${String(r.validateScore)}/100${sourceSuffix}`);
120
123
  }
121
124
  if (r.cascade.affectedSpecs.length > 0) {
122
125
  lines.push(`\nšŸ”— Downstream specs affected: ${r.cascade.affectedSpecs.map((s) => `SPEC-${s}`).join(', ')}`);
@@ -201,12 +204,103 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
201
204
  : withNextSteps;
202
205
  const displayTitle = specTitle ?? `SPEC-${specId}`;
203
206
  const humanSummary = buildUpdateStatusSummary(displayTitle, newStatus);
207
+ const warningItems = [
208
+ r.protectedBranchWarning,
209
+ r.mergeWarning,
210
+ r.validationWarning,
211
+ r.codeRealityWarning,
212
+ r.crashShieldWarning,
213
+ r.forcedBypassWarning,
214
+ ...(r.constitutionWarnings ?? []).map((w) => w.description),
215
+ ...(r.conventionWarnings ?? []),
216
+ ...(r.lintWarnings ?? []),
217
+ ...(r.testWarnings ?? []),
218
+ ...(r.compileWarnings ?? []),
219
+ ...(r.frontmatterSyncWarnings ?? []).map((w) => `frontmatter sync failed for ${w.specId}: ${w.reason}`),
220
+ ].filter((item) => typeof item === 'string' && item.length > 0);
221
+ const validationIssues = [
222
+ ...(r.validationWarning ? [r.validationWarning] : []),
223
+ ...(r.complianceGateResult?.issues ?? []),
224
+ ...(r.lintWarnings ?? []),
225
+ ...(r.testWarnings ?? []),
226
+ ...(r.compileWarnings ?? []),
227
+ ];
228
+ const structuredContent = {
229
+ specId,
230
+ previousStatus: currentStatus,
231
+ newStatus,
232
+ autoBranch: r.autoBranch ?? undefined,
233
+ protectedBranchWarning: r.protectedBranchWarning ?? undefined,
234
+ mergeWarning: r.mergeWarning ?? undefined,
235
+ validationWarning: r.validationWarning ?? undefined,
236
+ prSuggestion: r.prSuggestion ?? null,
237
+ metrics: r.metrics ?? undefined,
238
+ actuals: r.actuals ?? undefined,
239
+ cascade: {
240
+ ...(result.cascade ?? {}),
241
+ affectedSpecs: r.cascade.affectedSpecs,
242
+ },
243
+ constitutionWarnings: r.constitutionWarnings?.slice(0, 5),
244
+ conventionWarnings: r.conventionWarnings?.slice(0, 5),
245
+ lintWarnings: r.lintWarnings?.slice(0, 5),
246
+ testWarnings: r.testWarnings?.slice(0, 5),
247
+ compileWarnings: r.compileWarnings?.slice(0, 5),
248
+ validateScore: r.validateScore ?? undefined,
249
+ validateScoreSource: r.validateScoreSource ?? undefined,
250
+ complianceGateResult: r.complianceGateResult
251
+ ? {
252
+ skipped: r.complianceGateResult.skipped,
253
+ score: r.complianceGateResult.score,
254
+ blocked: r.complianceGateResult.blocked,
255
+ issues: r.complianceGateResult.issues.slice(0, 5),
256
+ }
257
+ : undefined,
258
+ forceApproveAuditId: typeof result.forceApproveAuditId === 'string' ? result.forceApproveAuditId : undefined,
259
+ forceStatusAuditId: typeof result.forceStatusAuditId === 'string' ? result.forceStatusAuditId : undefined,
260
+ qualityWarnings: Array.isArray(result.qualityWarnings)
261
+ ? result.qualityWarnings.slice(0, 5)
262
+ : undefined,
263
+ interactiveQuestions: Array.isArray(result.interactiveQuestions)
264
+ ? result.interactiveQuestions
265
+ : undefined,
266
+ transitionSummary: {
267
+ title: displayTitle,
268
+ from: currentStatus,
269
+ to: newStatus,
270
+ autoBranch: r.autoBranch ?? undefined,
271
+ versionSnapshotTag: r.versionSnapshotTag ?? undefined,
272
+ },
273
+ validationSummary: {
274
+ validateScore: r.validateScore ?? null,
275
+ validateScoreSource: r.validateScoreSource ?? null,
276
+ complianceScore: r.complianceGateResult?.score ?? null,
277
+ complianceBlocked: r.complianceGateResult?.blocked ?? false,
278
+ validationIssuesCount: validationIssues.length,
279
+ validationIssuesTop5: validationIssues.slice(0, 5),
280
+ },
281
+ blockersCount: validationIssues.length,
282
+ blockersTop3: validationIssues.slice(0, 3),
283
+ warningsCount: warningItems.length,
284
+ warningsTop5: warningItems.slice(0, 5),
285
+ affectedSpecsCount: r.cascade.affectedSpecs.length,
286
+ affectedSpecsTop5: r.cascade.affectedSpecs.slice(0, 5),
287
+ nextAction,
288
+ lifecycleMap,
289
+ humanSummary,
290
+ orchestrationPlan: r.orchestrationPlan
291
+ ? {
292
+ waves: r.orchestrationPlan.executionOrder.length,
293
+ parallelSafetyVerdict: r.orchestrationPlan.parallelSafetyVerdict,
294
+ executionOrder: r.orchestrationPlan.executionOrder,
295
+ }
296
+ : undefined,
297
+ };
204
298
  return {
205
299
  content: [
206
300
  { type: 'text', text: markdownText },
207
301
  { type: 'text', text: humanSummary },
208
302
  ],
209
- structuredContent: { ...result, nextAction, lifecycleMap, humanSummary },
303
+ structuredContent,
210
304
  };
211
305
  }
212
306
  /** Render a compact lifecycle map line showing current, next, and remaining steps */
@@ -22,7 +22,7 @@ async function execGate(cmd, cwd, timeoutMs) {
22
22
  timeout: timeoutMs,
23
23
  maxBuffer: 4 * 1024 * 1024,
24
24
  });
25
- return { output: stdout.trim(), ok: true };
25
+ return { output: stdout.trim(), ok: true, timedOut: false };
26
26
  }
27
27
  catch (err) {
28
28
  const e = err;
@@ -31,7 +31,8 @@ async function execGate(cmd, cwd, timeoutMs) {
31
31
  : typeof e.stdout === 'string'
32
32
  ? e.stdout
33
33
  : '';
34
- return { output: raw.trim(), ok: false };
34
+ const timedOut = e.killed === true || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT';
35
+ return { output: raw.trim(), ok: false, timedOut, errorMessage: e.message };
35
36
  }
36
37
  }
37
38
  function countLintIssues(output) {
@@ -87,23 +88,38 @@ async function checkTestGate(projectPath, testCmd) {
87
88
  }
88
89
  // Run tests with a reasonable timeout — only a warning if they fail
89
90
  const cmd = testCmd ?? 'npx vitest run --reporter=verbose';
90
- const { output, ok } = await execGate(cmd, projectPath, 20_000);
91
+ const timeoutMs = 120_000;
92
+ const { output, ok, timedOut, errorMessage } = await execGate(cmd, projectPath, timeoutMs);
91
93
  if (ok) {
92
94
  return [];
93
95
  }
96
+ if (timedOut) {
97
+ return [`Tests: timed out after ${String(timeoutMs)}ms. Command: ${cmd}`];
98
+ }
94
99
  // Extract summary line (e.g. "5 failed" or "FAIL src/foo.test.ts")
95
100
  const failLine = output
96
101
  .split('\n')
97
102
  .reverse()
98
- .find((l) => /fail|error/i.test(l) && l.trim().length > 0);
103
+ .find((l) => isFailureSummaryLine(l));
99
104
  const summary = failLine ??
100
105
  output
101
106
  .split('\n')
102
- .filter((l) => l.trim())
107
+ .filter((l) => l.trim() && !isPassingSummaryLine(l))
103
108
  .slice(-1)[0] ??
104
- '';
109
+ errorMessage ??
110
+ 'No failure summary available.';
105
111
  return [`Tests: failed. ${summary.trim()}`];
106
112
  }
113
+ function isPassingSummaryLine(line) {
114
+ return /^\s*(?:āœ“|PASS\b|Test Files\s+\d+\s+passed|Tests\s+\d+\s+passed)/i.test(line.trim());
115
+ }
116
+ function isFailureSummaryLine(line) {
117
+ const trimmed = line.trim();
118
+ if (trimmed.length === 0 || isPassingSummaryLine(trimmed)) {
119
+ return false;
120
+ }
121
+ return /\b(?:FAIL|failed|failing|error|errored)\b/i.test(trimmed);
122
+ }
107
123
  // ---------------------------------------------------------------------------
108
124
  // Convention gate (existing logic, unchanged)
109
125
  // ---------------------------------------------------------------------------
@@ -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);
@@ -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',
@@ -296,6 +282,7 @@ export async function handleValidate(args, server) {
296
282
  passedCount,
297
283
  failedCount,
298
284
  totalCriteria,
285
+ qualityFailures: qualityGateSummary.failures,
299
286
  missing: result.missing,
300
287
  indeterminate,
301
288
  commitReminder,
@@ -304,7 +291,26 @@ export async function handleValidate(args, server) {
304
291
  const graphText = formatGraphCoverageText(graphCoverage);
305
292
  // SPEC-512: Compact structuredContent — essential fields only at top level
306
293
  const compactSummary = buildCompactSummary(specId, result.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
307
- 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
+ });
308
314
  // SPEC-595: Elicit next action when validation fails and a server is available
309
315
  if (server !== undefined && failedCount > 0) {
310
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');
@@ -399,14 +405,135 @@ function buildCompactSummary(specId, score, passedCount, failedCount, totalCrite
399
405
  status: failedCount === 0 ? 'passing' : 'failing',
400
406
  };
401
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
+ }
402
524
  function buildCompactValidateText(args) {
403
- const { score, passedCount, failedCount, totalCriteria, missing, indeterminate, commitReminder, planuReminder, } = args;
404
- if (failedCount === 0) {
525
+ const { score, passedCount, failedCount, totalCriteria, qualityFailures, missing, indeterminate, commitReminder, planuReminder, } = args;
526
+ if (failedCount === 0 && qualityFailures.length === 0) {
405
527
  // All passing — single line
406
528
  return (`āœ… Validate: ${String(score)}/100 — ${String(passedCount)}/${String(totalCriteria)} criteria passing` +
407
529
  commitReminder +
408
530
  planuReminder);
409
531
  }
532
+ if (failedCount === 0) {
533
+ return (`āŒ Validate: ${String(score)}/100 — failing; ${String(passedCount)}/${String(totalCriteria)} criteria passing; failing gates: ${qualityFailures.join(', ')}` +
534
+ commitReminder +
535
+ planuReminder);
536
+ }
410
537
  // Failures exist — list only the failing ones (max 5)
411
538
  const MAX_FAILURES = 5;
412
539
  const labelledFailures = [
@@ -423,7 +550,8 @@ function buildCompactValidateText(args) {
423
550
  .slice(0, 3)
424
551
  .map((failure) => `${failure.label}: ${failure.criterion}`)
425
552
  .join(', ');
426
- const summaryLine = `āŒ Validate: ${String(score)}/100 — ${String(failedCount)} failing: [${failNames}${remainingCount + shownFailures.length > 3 ? '...' : ''}]`;
553
+ const gateSuffix = qualityFailures.length > 0 ? `; failing gates: ${qualityFailures.join(', ')}` : '';
554
+ const summaryLine = `āŒ Validate: ${String(score)}/100 — failing; ${String(failedCount)} failing criteria: [${failNames}${remainingCount + shownFailures.length > 3 ? '...' : ''}]${gateSuffix}`;
427
555
  return summaryLine + '\n' + failureList + moreLine + commitReminder + planuReminder;
428
556
  }
429
557
  function scoreToGrade(score) {
@@ -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.4",
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",
@@ -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,26 +1,20 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.10.4",
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"