@planu/cli 5.3.50 → 5.3.51

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,12 @@
1
+ ## [5.3.51] - 2026-08-25
2
+
3
+ ### Bug Fixes
4
+ - fix(evidence-gates): derive rejection templates from the rejecting schema
5
+
6
+ ### Chores
7
+ - chore(planu): close SPEC-1578 and file SPEC-1601
8
+
9
+
1
10
  ## [5.3.50] - 2026-08-24
2
11
 
3
12
  ### Bug Fixes
@@ -1,5 +1,35 @@
1
+ import { z } from 'zod';
1
2
  import type { Spec } from '../../types/spec/core.js';
2
3
  import type { EvidenceArtifacts, TraceabilityMatrixEvidence } from '../../types/evidence-gates.js';
4
+ export declare const DiscoverySchema: z.ZodObject<{
5
+ version: z.ZodLiteral<1>;
6
+ provenance: z.ZodOptional<z.ZodObject<{
7
+ receiptId: z.ZodString;
8
+ artifactDigest: z.ZodString;
9
+ specId: z.ZodString;
10
+ issuerId: z.ZodString;
11
+ }, z.core.$strict>>;
12
+ rules: z.ZodArray<z.ZodString>;
13
+ examples: z.ZodArray<z.ZodObject<{
14
+ rule: z.ZodString;
15
+ example: z.ZodString;
16
+ }, z.core.$strict>>;
17
+ openQuestions: z.ZodArray<z.ZodObject<{
18
+ question: z.ZodString;
19
+ status: z.ZodEnum<{
20
+ open: "open";
21
+ out_of_scope: "out_of_scope";
22
+ resolved: "resolved";
23
+ }>;
24
+ resolution: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strict>>;
26
+ outOfScope: z.ZodArray<z.ZodString>;
27
+ glossary: z.ZodArray<z.ZodObject<{
28
+ term: z.ZodString;
29
+ meaning: z.ZodString;
30
+ }, z.core.$strict>>;
31
+ }, z.core.$strict>;
32
+ export declare const DISCOVERY_TEMPLATE_OVERRIDES: Record<string, unknown>;
3
33
  /** Digest of canonical artifact content; the provenance envelope is excluded. */
4
34
  export declare function evidenceArtifactDigest(artifact: unknown): string;
5
35
  export declare function evidenceArtifactCollectionDigest(artifacts: readonly {
@@ -7,6 +7,7 @@ import { z } from 'zod';
7
7
  import { projectDataDir } from '../../storage/base-store.js';
8
8
  import { computeValidationEvidenceIdentity, computeValidationEvidenceKey, normalizeValidationCommand, readMatchingValidationEvidence, } from '../validation-evidence-ledger.js';
9
9
  import { VALIDATION_COMMANDS } from '../validation-impact-planner.js';
10
+ import { renderSchemaSkeleton } from '../handoff-artifacts/schema-skeleton.js';
10
11
  const DIGEST = /^sha256:[a-f0-9]{64}$/u;
11
12
  const SPEC_ID = /^SPEC-[1-9]\d*$/u;
12
13
  const ISSUER_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
@@ -33,7 +34,7 @@ const ProvenanceSchema = z
33
34
  issuerId: z.string().regex(ISSUER_ID),
34
35
  })
35
36
  .strict();
36
- const DiscoverySchema = z
37
+ export const DiscoverySchema = z
37
38
  .object({
38
39
  version: z.literal(1),
39
40
  provenance: ProvenanceSchema.optional(),
@@ -56,6 +57,9 @@ const DiscoverySchema = z
56
57
  .max(200),
57
58
  })
58
59
  .strict();
60
+ export const DISCOVERY_TEMPLATE_OVERRIDES = {
61
+ 'openQuestions[].status': 'resolved',
62
+ };
59
63
  const TaskPlanSchema = z
60
64
  .object({
61
65
  version: z.literal(1),
@@ -244,56 +248,6 @@ const ARTIFACT_HINTS = {
244
248
  function artifactHint(label) {
245
249
  return ARTIFACT_HINTS[label] ?? '';
246
250
  }
247
- /**
248
- * SPEC-1356 / AC5: compact JSON literal per artifact kind, appended verbatim to
249
- * schema-validation errors so a retrying caller sees the exact expected shape
250
- * instead of re-deriving it from prose.
251
- */
252
- const ARTIFACT_EXAMPLES = {
253
- 'Discovery evidence': JSON.stringify({
254
- version: 1,
255
- rules: ['Rule text'],
256
- examples: [{ rule: 'Rule text', example: 'Example text' }],
257
- openQuestions: [{ question: 'Open question?', status: 'open' }],
258
- outOfScope: ['Not in scope item'],
259
- glossary: [{ term: 'Term', meaning: 'Meaning' }],
260
- }),
261
- 'Task plan evidence': JSON.stringify({
262
- version: 1,
263
- tasks: [{ id: 'T1', title: 'Task title', acceptanceCriteria: ['AC1'], status: 'pending' }],
264
- }),
265
- 'Traceability matrix evidence': JSON.stringify({
266
- version: 1,
267
- rows: [
268
- {
269
- acceptanceCriterion: 'AC text',
270
- testEvidence: ['tests/example.test.ts'],
271
- changedFiles: ['src/example.ts'],
272
- validationEvidence: 'validate passed with score 100',
273
- reviewerEvidence: 'planu-implementation-reviewer approved',
274
- },
275
- ],
276
- }),
277
- // Contract kinds are derived from the artifact filenames so no
278
- // configurable technology literal is hardcoded here (audit:hardcodes).
279
- ...Object.fromEntries([
280
- 'contract-validation-api.json',
281
- 'contract-validation-graphql.json',
282
- 'contract-validation-event.json',
283
- 'contract-validation-ui.json',
284
- 'contract-validation-mcp.json',
285
- ].map((label) => [
286
- label,
287
- JSON.stringify({
288
- version: 1,
289
- kind: label.slice('contract-validation-'.length, -'.json'.length),
290
- passed: true,
291
- }),
292
- ])),
293
- };
294
- function artifactExample(label) {
295
- return ARTIFACT_EXAMPLES[label] ?? '';
296
- }
297
251
  /** SPEC-1356: exported so evidence-autofill (and other producers) target the same path convention. */
298
252
  export function handoffEvidencePath(projectId, specId, filename) {
299
253
  return join(projectDataDir(projectId), 'handoffs', specId, filename);
@@ -329,7 +283,9 @@ async function readOptional(args) {
329
283
  const allIssues = parsed.error.issues
330
284
  .map((issue) => `${issue.path.length > 0 ? issue.path.join('.') : '(root)'}: ${issue.message}`)
331
285
  .join('; ');
332
- const example = artifactExample(args.label);
286
+ const example = args.template === false
287
+ ? ''
288
+ : renderSchemaSkeleton(args.schema, { overrides: args.templateOverrides });
333
289
  args.invalidArtifacts.push(`${args.label} is invalid at ${found.path}: ${allIssues} ${artifactHint(args.label)}${example ? ` Expected shape example: ${example}` : ''}`.trim());
334
290
  return undefined;
335
291
  }
@@ -340,6 +296,29 @@ async function readOptional(args) {
340
296
  return undefined;
341
297
  }
342
298
  }
299
+ const CONTRACT_VALIDATION_KIND_BY_FILENAME = {
300
+ 'contract-validation-api.json': 'api',
301
+ 'contract-validation-graphql.json': technologyValue('technology-graphql-aed31c'),
302
+ 'contract-validation-event.json': 'event',
303
+ 'contract-validation-ui.json': 'ui',
304
+ 'contract-validation-mcp.json': 'mcp',
305
+ };
306
+ async function readContractValidations(args, invalidArtifacts, contractProvenance) {
307
+ const contractValidations = [];
308
+ for (const filename of Object.keys(CONTRACT_VALIDATION_KIND_BY_FILENAME)) {
309
+ const evidence = await readOptional({
310
+ label: filename,
311
+ invalidArtifacts,
312
+ schema: ContractValidationSchema,
313
+ paths: [handoffEvidencePath(args.projectId, args.specId, filename)],
314
+ templateOverrides: { kind: CONTRACT_VALIDATION_KIND_BY_FILENAME[filename] },
315
+ });
316
+ if (evidence) {
317
+ contractValidations.push(contractProvenance ? { ...evidence, provenance: contractProvenance } : evidence);
318
+ }
319
+ }
320
+ return contractValidations;
321
+ }
343
322
  export async function readEvidenceArtifacts(args) {
344
323
  const invalidArtifacts = [];
345
324
  const discovery = await readOptional({
@@ -347,6 +326,7 @@ export async function readEvidenceArtifacts(args) {
347
326
  invalidArtifacts,
348
327
  schema: DiscoverySchema,
349
328
  paths: [handoffEvidencePath(args.projectId, args.specId, 'discovery.json')],
329
+ templateOverrides: DISCOVERY_TEMPLATE_OVERRIDES,
350
330
  });
351
331
  const taskPlan = await readOptional({
352
332
  label: 'Task plan evidence',
@@ -359,6 +339,11 @@ export async function readEvidenceArtifacts(args) {
359
339
  invalidArtifacts,
360
340
  schema: TraceabilityMatrixSchema,
361
341
  paths: [handoffEvidencePath(args.projectId, args.specId, 'traceability-matrix.json')],
342
+ templateOverrides: {
343
+ 'rows[].testEvidence': ['example'],
344
+ 'rows[].validationEvidence': 'example',
345
+ 'rows[].reviewerEvidence': 'example',
346
+ },
362
347
  });
363
348
  const [discoveryProvenance, taskPlanProvenance, traceabilityProvenance, contractProvenance] = await Promise.all([
364
349
  'discovery.provenance.json',
@@ -370,25 +355,9 @@ export async function readEvidenceArtifacts(args) {
370
355
  invalidArtifacts,
371
356
  schema: ProvenanceSchema,
372
357
  paths: [handoffEvidencePath(args.projectId, args.specId, filename)],
358
+ template: false,
373
359
  })));
374
- const contractValidations = [];
375
- for (const filename of [
376
- 'contract-validation-api.json',
377
- 'contract-validation-graphql.json',
378
- 'contract-validation-event.json',
379
- 'contract-validation-ui.json',
380
- 'contract-validation-mcp.json',
381
- ]) {
382
- const evidence = await readOptional({
383
- label: filename,
384
- invalidArtifacts,
385
- schema: ContractValidationSchema,
386
- paths: [handoffEvidencePath(args.projectId, args.specId, filename)],
387
- });
388
- if (evidence) {
389
- contractValidations.push(contractProvenance ? { ...evidence, provenance: contractProvenance } : evidence);
390
- }
391
- }
360
+ const contractValidations = await readContractValidations(args, invalidArtifacts, contractProvenance);
392
361
  return {
393
362
  discovery: discovery && discoveryProvenance
394
363
  ? { ...discovery, provenance: discoveryProvenance }
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, join } from 'node:path';
2
- import { evidenceArtifactCollectionDigest, evidenceArtifactDigest } from './artifact-reader.js';
2
+ import { DISCOVERY_TEMPLATE_OVERRIDES, DiscoverySchema, evidenceArtifactCollectionDigest, evidenceArtifactDigest, } from './artifact-reader.js';
3
+ import { renderSchemaSkeleton } from '../handoff-artifacts/schema-skeleton.js';
3
4
  import { hasAnyAffirmedMatch, stripMetaAnalysisText, stripNonContractText, } from '../text-signal-boundaries.js';
4
5
  import { createCriterionIdentity } from '../criterion-identity.js';
5
6
  import { computeDurableValidationBindings, doneReceiptBindingsDifferOnlyInEvidence, toValidationReceiptBindings, writeValidationArtifactProvenance, } from '../validation/durable-validation.js';
@@ -140,6 +141,9 @@ export function inferRequiredContractEvidence(spec, criteria) {
140
141
  }
141
142
  return required;
142
143
  }
144
+ const DISCOVERY_TEMPLATE = renderSchemaSkeleton(DiscoverySchema, {
145
+ overrides: DISCOVERY_TEMPLATE_OVERRIDES,
146
+ });
143
147
  export function checkDiscoveryGate(spec, artifacts) {
144
148
  if (!isNonTrivial(spec)) {
145
149
  return [];
@@ -148,7 +152,8 @@ export function checkDiscoveryGate(spec, artifacts) {
148
152
  if (!artifacts.discovery) {
149
153
  issues.push({
150
154
  code: 'discovery_missing',
151
- message: 'Discovery evidence is required before approval. Add discovery.json with rules, examples, open questions, out-of-scope boundaries, and glossary.',
155
+ message: 'Discovery evidence is required before approval. Add discovery.json with rules, examples, open questions, out-of-scope boundaries, and glossary.' +
156
+ (DISCOVERY_TEMPLATE ? ` Expected shape example: ${DISCOVERY_TEMPLATE}` : ''),
152
157
  });
153
158
  return issues;
154
159
  }
@@ -0,0 +1,5 @@
1
+ import type { z } from 'zod';
2
+ export declare function renderSchemaSkeleton(schema: z.ZodType, options?: {
3
+ overrides?: Record<string, unknown>;
4
+ }): string;
5
+ //# sourceMappingURL=schema-skeleton.d.ts.map
@@ -0,0 +1,108 @@
1
+ const MAX_TEMPLATE_LENGTH = 600;
2
+ const OMIT_FIELD = Symbol('omit-field');
3
+ class UnsupportedSchemaNodeError extends Error {
4
+ }
5
+ function runtimeDef(schema) {
6
+ const zodInternals = schema?._zod;
7
+ const def = zodInternals?.def;
8
+ if (!def || typeof def.type !== 'string') {
9
+ throw new UnsupportedSchemaNodeError();
10
+ }
11
+ return def;
12
+ }
13
+ function buildObjectValue(def) {
14
+ const shape = (def.shape ?? {});
15
+ const result = {};
16
+ for (const [key, propSchema] of Object.entries(shape)) {
17
+ const value = buildNodeValue(propSchema);
18
+ if (value !== OMIT_FIELD) {
19
+ result[key] = value;
20
+ }
21
+ }
22
+ return result;
23
+ }
24
+ function buildNodeValue(schema) {
25
+ const def = runtimeDef(schema);
26
+ switch (def.type) {
27
+ case 'object':
28
+ return buildObjectValue(def);
29
+ case 'array': {
30
+ const element = buildNodeValue(def.element);
31
+ if (element === OMIT_FIELD) {
32
+ throw new UnsupportedSchemaNodeError();
33
+ }
34
+ return [element];
35
+ }
36
+ case 'enum': {
37
+ const entries = def.entries;
38
+ const firstOption = entries ? Object.values(entries)[0] : undefined;
39
+ if (firstOption === undefined) {
40
+ throw new UnsupportedSchemaNodeError();
41
+ }
42
+ return firstOption;
43
+ }
44
+ case 'literal': {
45
+ const values = def.values;
46
+ const firstValue = values?.[0];
47
+ if (firstValue === undefined) {
48
+ throw new UnsupportedSchemaNodeError();
49
+ }
50
+ return firstValue;
51
+ }
52
+ case 'string':
53
+ return 'example';
54
+ case 'number':
55
+ return 1;
56
+ case 'boolean':
57
+ return true;
58
+ case 'optional':
59
+ return OMIT_FIELD;
60
+ default:
61
+ throw new UnsupportedSchemaNodeError();
62
+ }
63
+ }
64
+ function isPlainObject(value) {
65
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
66
+ }
67
+ function applyOverrideAtPath(node, segments, value) {
68
+ const [head, ...rest] = segments;
69
+ if (head === undefined) {
70
+ return value;
71
+ }
72
+ const arrayMatch = /^(.+)\[\]$/u.exec(head);
73
+ if (arrayMatch) {
74
+ const [, key] = arrayMatch;
75
+ if (key === undefined || !isPlainObject(node) || !Array.isArray(node[key])) {
76
+ return node;
77
+ }
78
+ return {
79
+ ...node,
80
+ [key]: node[key].map((element) => applyOverrideAtPath(element, rest, value)),
81
+ };
82
+ }
83
+ if (!isPlainObject(node)) {
84
+ return node;
85
+ }
86
+ return { ...node, [head]: applyOverrideAtPath(node[head], rest, value) };
87
+ }
88
+ function applyOverrides(skeleton, overrides) {
89
+ return Object.entries(overrides).reduce((node, [path, value]) => applyOverrideAtPath(node, path.split('.'), value), skeleton);
90
+ }
91
+ export function renderSchemaSkeleton(schema, options = {}) {
92
+ try {
93
+ const skeleton = buildNodeValue(schema);
94
+ if (skeleton === OMIT_FIELD) {
95
+ return '';
96
+ }
97
+ const withOverrides = applyOverrides(skeleton, options.overrides ?? {});
98
+ const json = JSON.stringify(withOverrides);
99
+ return json.length <= MAX_TEMPLATE_LENGTH ? json : '';
100
+ }
101
+ catch (err) {
102
+ if (err instanceof UnsupportedSchemaNodeError) {
103
+ return '';
104
+ }
105
+ throw err;
106
+ }
107
+ }
108
+ //# sourceMappingURL=schema-skeleton.js.map
@@ -4,6 +4,9 @@ import type { EscalateResult } from '../../types/escalator.js';
4
4
  import type { ValidationFreshnessLease } from '../../engine/validation/validation-freshness.js';
5
5
  import { type DoneReceiptVerifier } from './done-receipt-verifier.js';
6
6
  export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
7
+ export declare const reviewFeedbackTemplate: () => string;
8
+ export declare const implementationReviewTemplate: () => string;
9
+ export declare const validationReportTemplate: () => string;
7
10
  /**
8
11
  * The reason why the validate gate blocked a `done` transition.
9
12
  * Each reason maps to a specific fix-hint shown to the LLM.
@@ -18,8 +18,40 @@ import { reportClassifiedDegradation } from '../../errors/classified-degradation
18
18
  import { verifyCurrentDoneReceipt } from './done-receipt-verifier.js';
19
19
  import { projectDataDir } from '../../storage/base-store.js';
20
20
  import { readTransitionLog } from '../../storage/transition-log.js';
21
- import { ValidationReportV1Schema } from '../../engine/handoff-artifacts/schemas.js';
21
+ import { ImplementationReviewV1Schema, ReviewFeedbackV1Schema, ValidationReportV1Schema, } from '../../engine/handoff-artifacts/schemas.js';
22
+ import { renderSchemaSkeleton } from '../../engine/handoff-artifacts/schema-skeleton.js';
22
23
  export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
24
+ const HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP = '2026-01-01T00:00:00.000Z';
25
+ const HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION = '1.0.0';
26
+ function lazyTemplate(render) {
27
+ let cached;
28
+ return () => {
29
+ cached ??= render();
30
+ return cached;
31
+ };
32
+ }
33
+ export const reviewFeedbackTemplate = lazyTemplate(() => renderSchemaSkeleton(ReviewFeedbackV1Schema, {
34
+ overrides: {
35
+ schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
36
+ reviewedAt: HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP,
37
+ 'reviewer.agent': 'planu-spec-reviewer',
38
+ },
39
+ }));
40
+ export const implementationReviewTemplate = lazyTemplate(() => renderSchemaSkeleton(ImplementationReviewV1Schema, {
41
+ overrides: {
42
+ schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
43
+ reviewedAt: HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP,
44
+ 'reviewer.agent': 'planu-implementation-reviewer',
45
+ },
46
+ }));
47
+ export const validationReportTemplate = lazyTemplate(() => renderSchemaSkeleton(ValidationReportV1Schema, {
48
+ overrides: {
49
+ schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
50
+ completedAt: HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP,
51
+ 'reviewer.agent': AUTOMATED_VALIDATOR_AGENT,
52
+ 'reviewer.kind': 'automation',
53
+ },
54
+ }));
23
55
  /**
24
56
  * SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
25
57
  *
@@ -482,6 +514,7 @@ export async function readApprovedValidationReportGate(specId, projectId, force)
482
514
  message: 'No validation-report artifact exists for this spec. Run validate to generate implementation-review evidence before marking done.',
483
515
  gates: [],
484
516
  fixHint: 'Run validate for this spec, fix any failing gates, then retry update_status(done). Use force:true only with an audited reason.',
517
+ template: validationReportTemplate(),
485
518
  }),
486
519
  };
487
520
  }
@@ -493,6 +526,7 @@ export async function readApprovedValidationReportGate(specId, projectId, force)
493
526
  message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
494
527
  gates: [],
495
528
  fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
529
+ template: validationReportTemplate(),
496
530
  }),
497
531
  };
498
532
  }
@@ -572,6 +606,7 @@ export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
572
606
  fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
573
607
  ? 'Have planu-spec-reviewer write review_feedback.md (ReviewFeedbackV1) into the handoff store, then retry update_status(approved).'
574
608
  : 'Have planu-spec-reviewer rewrite review_feedback.md as valid ReviewFeedbackV1 evidence, then retry approval.',
609
+ template: reviewFeedbackTemplate(),
575
610
  });
576
611
  }
577
612
  const review = result.payload;
@@ -635,6 +670,7 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
635
670
  fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
636
671
  ? 'Have planu-implementation-reviewer write implementation_review.json (ImplementationReviewV1) into the handoff store, then retry update_status(done).'
637
672
  : 'Have planu-implementation-reviewer rewrite implementation_review.json as valid ImplementationReviewV1 evidence, then retry done.',
673
+ template: implementationReviewTemplate(),
638
674
  });
639
675
  }
640
676
  const review = result.payload;
@@ -724,6 +760,7 @@ function implementationReviewGateError(args) {
724
760
  code: 422,
725
761
  context: { specId: args.specId, artifactPath, blockers: args.blockers },
726
762
  fixHint: args.fixHint,
763
+ ...(args.template ? { template: args.template } : {}),
727
764
  },
728
765
  };
729
766
  }
@@ -749,6 +786,7 @@ function validationReportGateError(args) {
749
786
  code: 422,
750
787
  context: { specId: args.specId, artifactPath, gates: args.gates },
751
788
  fixHint: args.fixHint,
789
+ ...(args.template ? { template: args.template } : {}),
752
790
  },
753
791
  };
754
792
  }
@@ -774,6 +812,7 @@ function specReviewGateError(args) {
774
812
  code: 422,
775
813
  context: { specId: args.specId, artifactPath, blockers: args.blockers },
776
814
  fixHint: args.fixHint,
815
+ ...(args.template ? { template: args.template } : {}),
777
816
  },
778
817
  };
779
818
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.50",
3
+ "version": "5.3.51",
4
4
  "description": "Planu — MCP Server for Spec Driven Development. Cross-platform (Linux/macOS/Windows, x64/arm64).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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": "5.3.50",
5
+ "version": "5.3.51",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",