@planu/cli 5.7.6 → 5.7.7

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 (27) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/.planu-build.json +1 -1
  3. package/dist/config/hook-templates/planu-review-panel-trigger.sh +2 -3
  4. package/dist/config/skill-templates/planu-multi-teammate-review.md +7 -11
  5. package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
  6. package/dist/engine/readiness-checker.js +17 -6
  7. package/dist/engine/reconcile/propagate-mirrors.js +6 -6
  8. package/dist/engine/spec-format/lean-spec-generator.js +2 -2
  9. package/dist/engine/spec-grounding/contract.d.ts +3 -3
  10. package/dist/engine/spec-grounding/contract.js +23 -76
  11. package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
  12. package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
  13. package/dist/storage/spec-store.js +8 -1
  14. package/dist/tools/bump-spec-version.js +5 -5
  15. package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
  16. package/dist/tools/multi-teammate-review.js +1 -14
  17. package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
  18. package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
  19. package/dist/tools/register-spec-tools/core-spec-tools.js +97 -43
  20. package/dist/tools/tool-registry/core-tools.js +71 -24
  21. package/dist/tools/tool-registry/group-quality-compliance.js +11 -14
  22. package/dist/tools/tool-registry-helpers.js +6 -2
  23. package/dist/types/spec-grounding.d.ts +14 -0
  24. package/package.json +1 -1
  25. package/planu-plugin.json +1 -1
  26. package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
  27. package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
@@ -67,17 +67,17 @@ export function registerDesignStackTools(server) {
67
67
  description: t('tools.challenge_spec.description'),
68
68
  annotations: { readOnlyHint: true },
69
69
  inputSchema: {
70
- specId: SpecIdSchema.describe('Spec ID to challenge'),
70
+ specId: SpecIdSchema.describe('Spec ID to challenge. Must match pattern ^SPEC-\\d+$, max 50 characters.'),
71
71
  projectId: z
72
72
  .string()
73
73
  .max(500)
74
74
  .optional()
75
- .describe('Project ID hash. Prefer projectPath — stays correct after context compaction.'),
75
+ .describe('Project ID hash. Prefer projectPath — stays correct after context compaction. Max 500 characters.'),
76
76
  projectPath: z
77
77
  .string()
78
78
  .max(4096)
79
79
  .optional()
80
- .describe('Absolute path to project root. Derives projectId automatically — always correct even after context compaction.'),
80
+ .describe('Max 4096 characters. Absolute path to project root. Derives projectId automatically — always correct even after context compaction.'),
81
81
  focus: z
82
82
  .array(ChallengeSpecFocusEnum)
83
83
  .max(100)
@@ -26,7 +26,7 @@ export const DeclaredDriftInputSchema = z.object({
26
26
  .string()
27
27
  .min(100)
28
28
  .max(10_000)
29
- .describe('Why the implementation drifted from the approved architectural premise. Minimum 100 characters — becomes the reconciliation transition reason.'),
29
+ .describe('Min 100, max 10000 characters. Why the implementation drifted from the approved architectural premise — becomes the reconciliation transition reason.'),
30
30
  });
31
31
  import { handleSsrBackMigration } from '../ssr-back-migration.js';
32
32
  import { registerMigrateLegacySpecTool } from '../migrate-legacy-spec.js';
@@ -115,13 +115,17 @@ export function registerAnalysisTools(server) {
115
115
  server.registerTool('reconcile_spec', {
116
116
  description: t('tools.reconcile_spec.description'),
117
117
  inputSchema: {
118
- specId: SpecIdSchema.describe('Spec ID to reconcile'),
119
- projectId: z.string().max(500).optional().describe('Project ID hash. Prefer projectPath.'),
118
+ specId: SpecIdSchema.describe('Spec ID to reconcile. Must match pattern ^SPEC-\\d+$, max 50 characters.'),
119
+ projectId: z
120
+ .string()
121
+ .max(500)
122
+ .optional()
123
+ .describe('Project ID hash. Prefer projectPath. Max 500 characters.'),
120
124
  projectPath: z
121
125
  .string()
122
126
  .max(4096)
123
127
  .optional()
124
- .describe('Absolute path to project root. Derives projectId automatically.'),
128
+ .describe('Absolute path to project root. Derives projectId automatically. Max 4096 characters.'),
125
129
  autoDetect: z
126
130
  .boolean()
127
131
  .optional()
@@ -132,11 +136,20 @@ export function registerAnalysisTools(server) {
132
136
  .describe('Enable living spec mode: compare acceptance criteria against codebase, auto-mark criteria as met/pending/drift, update the ## Progress section of spec.md (default: false)'),
133
137
  changes: z
134
138
  .array(z.object({
135
- section: z.string().max(500),
136
- originalValue: z.string().max(10_000),
137
- newValue: z.string().max(10_000),
138
- reason: z.string().max(10_000),
139
- approved: z.boolean(),
139
+ section: z
140
+ .string()
141
+ .max(500)
142
+ .describe('Max 500 characters. Spec section the change applies to.'),
143
+ originalValue: z
144
+ .string()
145
+ .max(10_000)
146
+ .describe('Max 10000 characters. Current text being replaced.'),
147
+ newValue: z.string().max(10_000).describe('Max 10000 characters. Replacement text.'),
148
+ reason: z
149
+ .string()
150
+ .max(10_000)
151
+ .describe('Max 10000 characters. Why the change is needed.'),
152
+ approved: z.boolean().describe('Whether the user approved this change.'),
140
153
  }))
141
154
  .max(1000)
142
155
  .optional()
@@ -18,7 +18,10 @@ import { handleTypeSafetyGate } from '../type-safety-gate.js';
18
18
  import { runWithTrustedLocalMcpContext } from '../../engine/lifecycle-reconciliation.js';
19
19
  /** init_project inputSchema — extracted to keep registerCoreSpecTools within line budget. */
20
20
  const INIT_PROJECT_INPUT_SCHEMA = {
21
- projectPath: z.string().max(4096).describe('Absolute path to the project root'),
21
+ projectPath: z
22
+ .string()
23
+ .max(4096)
24
+ .describe('Absolute path to the project root. Max 4096 characters.'),
22
25
  mode: z
23
26
  .enum(['existing_project', 'new_project'])
24
27
  .optional()
@@ -27,18 +30,40 @@ const INIT_PROJECT_INPUT_SCHEMA = {
27
30
  .string()
28
31
  .max(4096)
29
32
  .optional()
30
- .describe('Parent workspace path for explicit new_project onboarding'),
31
- appName: z.string().max(500).optional().describe('Human-readable app name for new_project'),
33
+ .describe('Parent workspace path for explicit new_project onboarding. Max 4096 characters.'),
34
+ appName: z
35
+ .string()
36
+ .max(500)
37
+ .optional()
38
+ .describe('Human-readable app name for new_project. Max 500 characters.'),
32
39
  appSlug: z
33
40
  .string()
34
41
  .max(200)
35
42
  .optional()
36
- .describe('Safe folder slug for new_project; must not contain path separators'),
37
- projectType: z.string().max(500).optional().describe('Type of project being initialized'),
38
- platform: z.string().max(500).optional().describe('Runtime/deployment platform'),
39
- language: z.string().max(500).optional().describe('User-confirmed language'),
40
- framework: z.string().max(500).nullable().optional().describe('User-confirmed framework'),
41
- database: z.string().max(500).nullable().optional().describe('User-confirmed database'),
43
+ .describe('Safe folder slug for new_project; must not contain path separators. Max 200 characters.'),
44
+ projectType: z
45
+ .string()
46
+ .max(500)
47
+ .optional()
48
+ .describe('Type of project being initialized. Max 500 characters.'),
49
+ platform: z
50
+ .string()
51
+ .max(500)
52
+ .optional()
53
+ .describe('Runtime/deployment platform. Max 500 characters.'),
54
+ language: z.string().max(500).optional().describe('User-confirmed language. Max 500 characters.'),
55
+ framework: z
56
+ .string()
57
+ .max(500)
58
+ .nullable()
59
+ .optional()
60
+ .describe('Max 500 characters. User-confirmed framework.'),
61
+ database: z
62
+ .string()
63
+ .max(500)
64
+ .nullable()
65
+ .optional()
66
+ .describe('Max 500 characters. User-confirmed database.'),
42
67
  createDirectory: z
43
68
  .boolean()
44
69
  .optional()
@@ -162,7 +187,11 @@ export function registerCoreSpecTools(server) {
162
187
  server.registerTool('create_spec', {
163
188
  description: t('tools.create_spec.description'),
164
189
  inputSchema: {
165
- title: z.string().min(1).max(500).describe('Title of the spec / user story'),
190
+ title: z
191
+ .string()
192
+ .min(1)
193
+ .max(500)
194
+ .describe('Title of the spec / user story. Max 500 characters.'),
166
195
  description: z
167
196
  .string()
168
197
  .max(10_000)
@@ -176,21 +205,25 @@ export function registerCoreSpecTools(server) {
176
205
  .max(128)
177
206
  .regex(/^[A-Za-z0-9._:-]+$/)
178
207
  .optional()
179
- .describe('Stable caller key. Retries return the exact committed create_spec result.'),
208
+ .describe('Must match pattern ^[A-Za-z0-9._:-]+$, max 128 characters. Stable caller key retries return the exact committed create_spec result.'),
180
209
  projectPath: z
181
210
  .string()
182
211
  .max(4096)
183
212
  .optional()
184
- .describe('Absolute path to the project root. Auto-detected from git root when omitted.'),
213
+ .describe('Max 4096 characters. Absolute path to the project root. Auto-detected from git root when omitted.'),
185
214
  type: SpecTypeEnum.optional().describe('Spec type (default: feature)'),
186
215
  scope: SpecScopeEnum.optional().describe('Spec scope (default: feature)'),
187
216
  target: SpecTargetEnum.optional().describe('Target area: frontend, backend, shared, fullstack, infrastructure, database'),
188
- tags: z.array(z.string().max(500)).max(100).optional().describe('Tags for categorization'),
217
+ tags: z
218
+ .array(z.string().max(500))
219
+ .max(100)
220
+ .optional()
221
+ .describe('Tags for categorization. Each tag max 500 characters.'),
189
222
  feature: z
190
223
  .string()
191
224
  .max(500)
192
225
  .optional()
193
- .describe('Feature group tag (e.g. "Authentication", "Billing"). Added to tags for categorization. Flat structure — no subdirectories created.'),
226
+ .describe('Max 500 characters. Feature group tag (e.g. "Authentication", "Billing"). Added to tags for categorization. Flat structure — no subdirectories created.'),
194
227
  acFormat: SpecAcFormatEnum.optional().describe('SPEC-224: Format for acceptance criteria in spec.md. ' +
195
228
  'checkbox (default) — markdown checkboxes (- [ ] criterion). ' +
196
229
  'bdd — Gherkin Given-When-Then scenarios for BDD workflows.'),
@@ -242,12 +275,12 @@ export function registerCoreSpecTools(server) {
242
275
  .string()
243
276
  .max(500)
244
277
  .optional()
245
- .describe('Project ID (hash). Prefer projectPath if unknown.'),
278
+ .describe('Max 500 characters. Project ID (hash). Prefer projectPath if unknown.'),
246
279
  projectPath: z
247
280
  .string()
248
281
  .max(4096)
249
282
  .optional()
250
- .describe('Absolute path to the project root. Used to derive projectId automatically.'),
283
+ .describe('Max 4096 characters. Absolute path to the project root. Used to derive projectId automatically.'),
251
284
  status: SpecStatusEnum.optional().describe('Filter by spec status'),
252
285
  type: SpecTypeEnum.optional().describe('Filter by spec type'),
253
286
  detail: z
@@ -275,28 +308,28 @@ export function registerCoreSpecTools(server) {
275
308
  server.registerTool('update_status', {
276
309
  description: t('tools.update_status.description'),
277
310
  inputSchema: {
278
- specId: SpecIdSchema.describe('Spec ID to update'),
311
+ specId: SpecIdSchema.describe('Spec ID to update. Must match pattern ^SPEC-\\d+$ (e.g. SPEC-042), max 50 characters.'),
279
312
  projectId: z
280
313
  .string()
281
314
  .max(500)
282
315
  .optional()
283
- .describe('Project ID (hash). Prefer projectPath if unknown.'),
316
+ .describe('Project ID (hash). Prefer projectPath if unknown. Max 500 characters.'),
284
317
  projectPath: z
285
318
  .string()
286
319
  .max(4096)
287
320
  .optional()
288
- .describe('Absolute path to the project root. Used to derive projectId automatically.'),
321
+ .describe('Absolute path to the project root. Used to derive projectId automatically. Max 4096 characters.'),
289
322
  status: SpecStatusEnum.describe('New status'),
290
323
  sessionId: z
291
324
  .string()
292
325
  .max(500)
293
326
  .optional()
294
- .describe('Agent/session ID performing this lifecycle transition.'),
327
+ .describe('Agent/session ID performing this lifecycle transition. Max 500 characters.'),
295
328
  modelId: z
296
329
  .string()
297
330
  .max(500)
298
331
  .optional()
299
- .describe('Concrete model ID used for this transition. Can satisfy SDD model-routing evidence when it maps to the required tier.'),
332
+ .describe('Max 500 characters. Concrete model ID used for this transition. Can satisfy SDD model-routing evidence when it maps to the required tier.'),
300
333
  modelTierUsed: z
301
334
  .enum(['max', 'implementation', 'review'])
302
335
  .optional()
@@ -305,27 +338,27 @@ export function registerCoreSpecTools(server) {
305
338
  .string()
306
339
  .max(500)
307
340
  .optional()
308
- .describe('SHA-256 hash of the persisted context package used to prove context continuity across agents.'),
341
+ .describe('Max 500 characters. SHA-256 hash of the persisted context package used to prove context continuity across agents.'),
309
342
  handoffPath: z
310
343
  .string()
311
344
  .max(4096)
312
345
  .optional()
313
- .describe('Path to the persisted handoff package generated by package_handoff. Required for implementing/done unless handoffArtifactId is provided.'),
346
+ .describe('Max 4096 characters. Path to the persisted handoff package generated by package_handoff. Required for implementing/done unless handoffArtifactId is provided.'),
314
347
  handoffArtifactId: z
315
348
  .string()
316
349
  .max(500)
317
350
  .optional()
318
- .describe('External handoff artifact ID when the host stores handoff evidence outside the filesystem.'),
351
+ .describe('Max 500 characters. External handoff artifact ID when the host stores handoff evidence outside the filesystem.'),
319
352
  reviewedBy: z
320
353
  .string()
321
354
  .max(500)
322
355
  .optional()
323
- .describe('Reviewer identity/evidence required before status=done.'),
356
+ .describe('Reviewer identity/evidence required before status=done. Max 500 characters.'),
324
357
  arbitratedBy: z
325
358
  .string()
326
359
  .max(500)
327
360
  .optional()
328
- .describe('Arbiter identity/evidence required before status=done.'),
361
+ .describe('Arbiter identity/evidence required before status=done. Max 500 characters.'),
329
362
  reconcileRequired: z
330
363
  .boolean()
331
364
  .optional()
@@ -339,8 +372,16 @@ export function registerCoreSpecTools(server) {
339
372
  apiCostUsd: z.number().min(0).optional(),
340
373
  humanCostUsd: z.number().min(0).optional(),
341
374
  totalCostUsd: z.number().min(0).optional(),
342
- completedAt: z.string().max(500).optional(),
343
- notes: z.string().max(10_000).optional(),
375
+ completedAt: z
376
+ .string()
377
+ .max(500)
378
+ .optional()
379
+ .describe('ISO timestamp. Max 500 characters.'),
380
+ notes: z
381
+ .string()
382
+ .max(10_000)
383
+ .optional()
384
+ .describe('Free-text notes. Max 10000 characters.'),
344
385
  })
345
386
  .optional()
346
387
  .describe('Actual metrics for status = done. All fields optional (SPEC-1356): missing numeric fields default to 0, completedAt/notes are auto-filled when omitted.'),
@@ -348,7 +389,7 @@ export function registerCoreSpecTools(server) {
348
389
  .string()
349
390
  .max(10_000)
350
391
  .optional()
351
- .describe('Feedback or observations when sending a spec back (e.g., review→draft). Stored in the spec for the implementer to address.'),
392
+ .describe('Max 10000 characters. Feedback or observations when sending a spec back (e.g., review→draft). Stored in the spec for the implementer to address.'),
352
393
  force: z
353
394
  .boolean()
354
395
  .optional()
@@ -361,7 +402,7 @@ export function registerCoreSpecTools(server) {
361
402
  .string()
362
403
  .max(2000)
363
404
  .optional()
364
- .describe('SPEC-721: Required when forceStatus=true. ≥100 characters explaining why validate gate is bypassed.'),
405
+ .describe('SPEC-721: Required when forceStatus=true. ≥100 and <=2000 characters explaining why validate gate is bypassed.'),
365
406
  dry_run: z
366
407
  .boolean()
367
408
  .optional()
@@ -370,18 +411,24 @@ export function registerCoreSpecTools(server) {
370
411
  .string()
371
412
  .max(2000)
372
413
  .optional()
373
- .describe('SPEC-733: Required for reverse transitions (done→implementing, discarded→draft). Must be ≥30 chars explaining why the spec is being reopened.'),
414
+ .describe('Must be ≥30 and <=2000 chars. SPEC-733: Required for reverse transitions (done→implementing, discarded→draft), explaining why the spec is being reopened.'),
374
415
  reconciliationRequestId: z
375
416
  .string()
376
417
  .regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u)
377
418
  .optional()
378
- .describe('Trusted local MCP only: lowercase UUID idempotency key for implementing to review reconciliation.'),
379
- expectedImplementingTransitionId: z.string().max(500).optional(),
419
+ .describe('Must match pattern ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ (lowercase UUID). ' +
420
+ 'Trusted local MCP only: idempotency key for implementing to review reconciliation.'),
421
+ expectedImplementingTransitionId: z
422
+ .string()
423
+ .max(500)
424
+ .optional()
425
+ .describe('Max 500 characters. Idempotency guard: the transitionId returned by the prior implementing transition.'),
380
426
  implementationReviewDigest: z
381
427
  .string()
382
428
  .regex(/^sha256:[a-f0-9]{64}$/u)
383
429
  .optional()
384
- .describe('SHA-256 of the exact validation-report.json bytes. Required implementation evidence for status=done; for status=review it is valid only inside the complete trusted local-MCP reconciliation tuple.'),
430
+ .describe('Must match pattern ^sha256:[a-f0-9]{64}$ the literal prefix "sha256:" followed by 64 lowercase hex characters. ' +
431
+ 'SHA-256 of the exact validation-report.json bytes. Required implementation evidence for status=done; for status=review it is valid only inside the complete trusted local-MCP reconciliation tuple.'),
385
432
  forceApprove: z
386
433
  .boolean()
387
434
  .optional()
@@ -391,20 +438,27 @@ export function registerCoreSpecTools(server) {
391
438
  server.registerTool('update_status_batch', {
392
439
  description: 'Batch update many specs to the same status in one MCP execution. Uses the same transition rules as update_status and reports updated/skipped/failed per spec.',
393
440
  inputSchema: {
394
- specIds: z.array(SpecIdSchema).min(1).describe('Spec IDs to update'),
395
- projectId: z.string().max(500).optional().describe('Project ID, if known'),
441
+ specIds: z
442
+ .array(SpecIdSchema)
443
+ .min(1)
444
+ .describe('Spec IDs to update. Each must match pattern ^SPEC-\\d+$, max 50 characters.'),
445
+ projectId: z
446
+ .string()
447
+ .max(500)
448
+ .optional()
449
+ .describe('Project ID, if known. Max 500 characters.'),
396
450
  projectPath: z
397
451
  .string()
398
452
  .max(4096)
399
453
  .optional()
400
- .describe('Absolute project root. Preferred when projectId is unknown.'),
454
+ .describe('Absolute project root. Preferred when projectId is unknown. Max 4096 characters.'),
401
455
  status: SpecStatusEnum.exclude(['done']).describe('New status for all specs. Batch supports draft, review, approved, implementing, and discarded; close done specs individually with full evidence.'),
402
456
  dryRun: z.boolean().optional().describe('Preview the batch without mutating any spec.'),
403
457
  reviewNotes: z
404
458
  .string()
405
459
  .max(10_000)
406
460
  .optional()
407
- .describe('Optional notes for each transition.'),
461
+ .describe('Optional notes for each transition. Max 10000 characters.'),
408
462
  evidence: z
409
463
  .record(z.string(), z.object({
410
464
  modelTierUsed: z
@@ -429,8 +483,8 @@ export function registerCoreSpecTools(server) {
429
483
  server.registerTool('estimate', {
430
484
  description: t('tools.estimate.description'),
431
485
  inputSchema: {
432
- specId: SpecIdSchema.describe('Spec ID to estimate'),
433
- projectId: z.string().max(500).describe('Project ID'),
486
+ specId: SpecIdSchema.describe('Spec ID to estimate. Must match pattern ^SPEC-\\d+$ (e.g. SPEC-042), max 50 characters.'),
487
+ projectId: z.string().max(500).describe('Project ID. Max 500 characters.'),
434
488
  },
435
489
  outputSchema: EstimateOutputSchema,
436
490
  }, safeTracked('estimate', async (args) => handleEstimate(args)));
@@ -450,7 +504,7 @@ export function registerCoreSpecTools(server) {
450
504
  server.registerTool('validate', {
451
505
  description: t('tools.validate.description'),
452
506
  inputSchema: {
453
- specId: SpecIdSchema.describe('Spec ID to validate'),
507
+ specId: SpecIdSchema.describe('Spec ID to validate. Must match pattern ^SPEC-\\d+$ (e.g. SPEC-042), max 50 characters.'),
454
508
  mode: z
455
509
  .literal('submit')
456
510
  .default('submit')
@@ -459,12 +513,12 @@ export function registerCoreSpecTools(server) {
459
513
  .string()
460
514
  .max(500)
461
515
  .optional()
462
- .describe('Project ID hash. Prefer projectPath — stays correct after context compaction.'),
516
+ .describe('Project ID hash. Prefer projectPath — stays correct after context compaction. Max 500 characters.'),
463
517
  projectPath: z
464
518
  .string()
465
519
  .max(4096)
466
520
  .optional()
467
- .describe('Absolute path to project root. Derives projectId automatically — always correct even after context compaction.'),
521
+ .describe('Max 4096 characters. Absolute path to project root. Derives projectId automatically — always correct even after context compaction.'),
468
522
  },
469
523
  outputSchema: ValidateOutputSchema,
470
524
  annotations: {
@@ -51,7 +51,12 @@ const coreToolsRegistry = [
51
51
  name: 'session_checkpoint',
52
52
  description: 'Persist a compact, bounded session checkpoint to planu/session-context.md for the next session.',
53
53
  schema: {
54
- projectPath: z.string().trim().min(1).max(4096),
54
+ projectPath: z
55
+ .string()
56
+ .trim()
57
+ .min(1)
58
+ .max(4096)
59
+ .describe('Absolute path to the project root. Max 4096 characters.'),
55
60
  },
56
61
  handler: handleSessionCheckpoint,
57
62
  annotations: {
@@ -66,9 +71,20 @@ const coreToolsRegistry = [
66
71
  name: 'get_job',
67
72
  description: 'Read the durable state and latest checkpoint or result for a local Planu job.',
68
73
  schema: {
69
- operationId: z.string().trim().min(1).max(128),
70
- projectId: z.string().trim().min(1).max(128),
71
- workspaceId: z.string().trim().min(1).max(128).optional(),
74
+ operationId: z
75
+ .string()
76
+ .trim()
77
+ .min(1)
78
+ .max(128)
79
+ .describe('Job operation ID. Max 128 characters.'),
80
+ projectId: z.string().trim().min(1).max(128).describe('Project ID hash. Max 128 characters.'),
81
+ workspaceId: z
82
+ .string()
83
+ .trim()
84
+ .min(1)
85
+ .max(128)
86
+ .optional()
87
+ .describe('Workspace ID. Max 128 characters.'),
72
88
  },
73
89
  handler: handleGetJob,
74
90
  annotations: {
@@ -84,9 +100,20 @@ const coreToolsRegistry = [
84
100
  name: 'restart_job',
85
101
  description: 'Restart a cancelled or failed durable job and schedule its typed executor.',
86
102
  schema: {
87
- operationId: z.string().trim().min(1).max(128),
88
- projectId: z.string().trim().min(1).max(128),
89
- workspaceId: z.string().trim().min(1).max(128).optional(),
103
+ operationId: z
104
+ .string()
105
+ .trim()
106
+ .min(1)
107
+ .max(128)
108
+ .describe('Job operation ID. Max 128 characters.'),
109
+ projectId: z.string().trim().min(1).max(128).describe('Project ID hash. Max 128 characters.'),
110
+ workspaceId: z
111
+ .string()
112
+ .trim()
113
+ .min(1)
114
+ .max(128)
115
+ .optional()
116
+ .describe('Workspace ID. Max 128 characters.'),
90
117
  },
91
118
  handler: handleRestartJob,
92
119
  annotations: {
@@ -257,12 +284,16 @@ const coreToolsRegistry = [
257
284
  name: 'planu_status',
258
285
  description: 'Get a compact world-snapshot of the current Planu project state in one call: active spec (in-progress), next recommended spec (highest-priority approved), queue count, git state (staged/modified files), and an actionable suggestion. Designed to return the full picture in ≤150 tokens.',
259
286
  schema: {
260
- projectPath: z.string().min(1).max(4096).describe('Absolute path to project root'),
287
+ projectPath: z
288
+ .string()
289
+ .min(1)
290
+ .max(4096)
291
+ .describe('Max 4096 characters. Absolute path to project root.'),
261
292
  projectId: z
262
293
  .string()
263
294
  .max(500)
264
295
  .optional()
265
- .describe('Project ID (optional — derived from projectPath if omitted)'),
296
+ .describe('Max 500 characters. Project ID (optional — derived from projectPath if omitted).'),
266
297
  },
267
298
  handler: async (args) => handlePlanStatus(args),
268
299
  annotations: { readOnlyHint: true },
@@ -544,17 +575,17 @@ const coreToolsRegistry = [
544
575
  description: z
545
576
  .string()
546
577
  .max(10_000)
547
- .describe('Free-form description: what you want to build, your problem, or your request'),
578
+ .describe('Max 10000 characters. Free-form description: what you want to build, your problem, or your request.'),
548
579
  projectId: z
549
580
  .string()
550
581
  .max(500)
551
582
  .optional()
552
- .describe('Project ID or project path (used for context loading and focus persistence)'),
583
+ .describe('Max 500 characters. Project ID or project path (used for context loading and focus persistence).'),
553
584
  sessionId: z
554
585
  .string()
555
586
  .max(500)
556
587
  .optional()
557
- .describe('Session ID (overrides projectId for focus tracking)'),
588
+ .describe('Max 500 characters. Session ID (overrides projectId for focus tracking).'),
558
589
  scenario: z
559
590
  .enum(['new_project', 'existing_project', 'concrete_problem'])
560
591
  .optional()
@@ -567,28 +598,44 @@ const coreToolsRegistry = [
567
598
  .string()
568
599
  .max(4096)
569
600
  .optional()
570
- .describe('Parent workspace path that should contain a new project folder'),
571
- appName: z.string().max(500).optional().describe('Human-readable new app name'),
601
+ .describe('Max 4096 characters. Parent workspace path that should contain a new project folder.'),
602
+ appName: z
603
+ .string()
604
+ .max(500)
605
+ .optional()
606
+ .describe('Max 500 characters. Human-readable new app name.'),
572
607
  appSlug: z
573
608
  .string()
574
609
  .max(200)
575
610
  .optional()
576
- .describe('Safe folder name for the new app; must not contain path separators'),
577
- projectType: z.string().max(500).optional().describe('Kind of project to create'),
578
- platform: z.string().max(500).optional().describe('Runtime or deployment platform'),
579
- language: z.string().max(500).optional().describe('User-confirmed programming language'),
611
+ .describe('Max 200 characters. Safe folder name for the new app; must not contain path separators.'),
612
+ projectType: z
613
+ .string()
614
+ .max(500)
615
+ .optional()
616
+ .describe('Max 500 characters. Kind of project to create.'),
617
+ platform: z
618
+ .string()
619
+ .max(500)
620
+ .optional()
621
+ .describe('Max 500 characters. Runtime or deployment platform.'),
622
+ language: z
623
+ .string()
624
+ .max(500)
625
+ .optional()
626
+ .describe('Max 500 characters. User-confirmed programming language.'),
580
627
  framework: z
581
628
  .string()
582
629
  .max(500)
583
630
  .nullable()
584
631
  .optional()
585
- .describe('User-confirmed framework, null for no framework'),
632
+ .describe('Max 500 characters. User-confirmed framework, null for no framework.'),
586
633
  database: z
587
634
  .string()
588
635
  .max(500)
589
636
  .nullable()
590
637
  .optional()
591
- .describe('User-confirmed database, null for no database'),
638
+ .describe('Max 500 characters. User-confirmed database, null for no database.'),
592
639
  createDirectory: z
593
640
  .boolean()
594
641
  .optional()
@@ -603,12 +650,12 @@ const coreToolsRegistry = [
603
650
  .min(0)
604
651
  .max(10)
605
652
  .optional()
606
- .describe('Current step index for new_project flow (0–2)'),
653
+ .describe('Maximum 10. Current step index for new_project flow (0–2).'),
607
654
  targetTool: z
608
655
  .string()
609
656
  .max(500)
610
657
  .optional()
611
- .describe('Planu tool name to check ambiguity for before calling it'),
658
+ .describe('Max 500 characters. Planu tool name to check ambiguity for before calling it.'),
612
659
  checkFocus: z
613
660
  .boolean()
614
661
  .optional()
@@ -617,12 +664,12 @@ const coreToolsRegistry = [
617
664
  .string()
618
665
  .max(10_000)
619
666
  .optional()
620
- .describe('Explicitly declare a new session objective'),
667
+ .describe('Max 10000 characters. Explicitly declare a new session objective.'),
621
668
  planContent: z
622
669
  .string()
623
670
  .max(10_000)
624
671
  .optional()
625
- .describe('Plan Mode output to convert into a spec. Paste the plan markdown here to extract ACs and route to create_spec.'),
672
+ .describe('Max 10000 characters. Plan Mode output to convert into a spec. Paste the plan markdown here to extract ACs and route to create_spec.'),
626
673
  },
627
674
  handler: (args) => handleFacilitate(args),
628
675
  annotations: { readOnlyHint: false },