@planu/cli 4.3.20 → 4.3.22

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 (33) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/engine/spec-format/lean-technical-generator.js +5 -7
  3. package/dist/engine/spec-format/unified-spec-builder.js +1 -1
  4. package/dist/engine/spec-generator/fallback-generator.js +4 -30
  5. package/dist/engine/test-generators/contract-test-generator.d.ts +3 -2
  6. package/dist/engine/test-generators/contract-test-generator.js +8 -43
  7. package/dist/engine/test-generators/mock-generator.d.ts +1 -1
  8. package/dist/engine/test-generators/mock-generator.js +13 -31
  9. package/dist/tools/bump-spec-version.js +2 -1
  10. package/dist/tools/create-spec.js +30 -10
  11. package/dist/tools/generate-batch-script.js +2 -1
  12. package/dist/tools/generate-proposal.js +2 -1
  13. package/dist/tools/generate-tests/adapters/contract-testing-adapter.js +9 -3
  14. package/dist/tools/heal-spec-docs.js +96 -40
  15. package/dist/tools/migrate-tech/core-handlers.js +24 -72
  16. package/dist/tools/register-agent-squad-tools.js +2 -1
  17. package/dist/tools/register-platform-tools/design-stack-tools.js +10 -10
  18. package/dist/tools/register-platform-tools/lifecycle-infra-tools.js +8 -8
  19. package/dist/tools/register-spec-tools/analysis-tools.js +7 -7
  20. package/dist/tools/register-spec-tools/core-spec-tools.js +6 -6
  21. package/dist/tools/render-spec-for-provider.js +2 -1
  22. package/dist/tools/schemas/github.js +4 -3
  23. package/dist/tools/schemas/index.d.ts +1 -0
  24. package/dist/tools/schemas/index.js +1 -0
  25. package/dist/tools/schemas/spec-id.d.ts +7 -0
  26. package/dist/tools/schemas/spec-id.js +17 -0
  27. package/dist/tools/spec-prompt-handler.js +2 -1
  28. package/dist/tools/tool-registry/core-tools.js +3 -2
  29. package/dist/tools/tool-registry/group-infra.js +3 -3
  30. package/dist/tools/tool-registry/group-quality-compliance.js +11 -14
  31. package/package.json +9 -9
  32. package/planu-native.json +1 -1
  33. package/planu-plugin.json +1 -1
@@ -1,41 +1,20 @@
1
1
  // tools/migrate-tech/core-handlers.ts — Handlers: analyze | map | plan | strategy | validate
2
2
  import { t } from '../../i18n/index.js';
3
- import { buildStackAnalysis, scoreMigratableComponents, buildEquivalenceMap, generateMigrationPlan, recommendMigrationStrategy, generateParityValidation, } from '../../engine/migration/index.js';
3
+ import { buildStackAnalysis, buildEquivalenceMap, generateMigrationPlan, recommendMigrationStrategy, generateParityValidation, } from '../../engine/migration/index.js';
4
4
  export function handleAnalyze(args) {
5
- const components = scoreMigratableComponents([
6
- {
7
- id: 'app-core',
8
- name: 'Application Core',
9
- path: args.projectPath ?? './src',
10
- type: 'module',
11
- cyclomaticComplexity: 8,
12
- couplingScore: 5,
13
- dependsOn: [],
14
- dependents: ['api-handler'],
15
- linesOfCode: 400,
16
- },
17
- {
18
- id: 'api-handler',
19
- name: 'API Handler',
20
- path: args.projectPath ? `${args.projectPath}/api` : './src/api',
21
- type: 'handler',
22
- cyclomaticComplexity: 12,
23
- couplingScore: 6,
24
- dependsOn: ['app-core'],
25
- dependents: [],
26
- linesOfCode: 250,
27
- },
28
- ]);
29
5
  const data = buildStackAnalysis({
30
6
  language: args.sourceStack ?? 'unknown',
31
7
  framework: args.sourceStack ?? 'unknown',
32
8
  components: [],
33
- entryPoints: [{ type: 'http', name: 'REST API', description: 'Main HTTP entry point' }],
9
+ entryPoints: [],
34
10
  externalIntegrations: [],
35
- migratableComponents: components,
11
+ migratableComponents: [],
36
12
  hasTests: false,
37
13
  });
38
- const warnings = data.riskFlags;
14
+ const warnings = [
15
+ ...data.riskFlags,
16
+ 'No project-derived migration inventory was provided; analysis does not fabricate components or entry points.',
17
+ ];
39
18
  return {
40
19
  action: 'analyze',
41
20
  data,
@@ -60,48 +39,15 @@ export function handleMap(args) {
60
39
  };
61
40
  }
62
41
  export function handlePlan(args) {
63
- const components = scoreMigratableComponents([
64
- {
65
- id: 'utils',
66
- name: 'Utils',
67
- path: './src/utils',
68
- type: 'util',
69
- cyclomaticComplexity: 3,
70
- couplingScore: 1,
71
- dependsOn: [],
72
- dependents: ['service'],
73
- linesOfCode: 100,
74
- },
75
- {
76
- id: 'service',
77
- name: 'Service',
78
- path: './src/service',
79
- type: 'service',
80
- cyclomaticComplexity: 10,
81
- couplingScore: 4,
82
- dependsOn: ['utils'],
83
- dependents: ['handler'],
84
- linesOfCode: 300,
85
- },
86
- {
87
- id: 'handler',
88
- name: 'Handler',
89
- path: './src/handler',
90
- type: 'handler',
91
- cyclomaticComplexity: 6,
92
- couplingScore: 3,
93
- dependsOn: ['service'],
94
- dependents: [],
95
- linesOfCode: 150,
96
- },
97
- ]);
98
42
  const data = generateMigrationPlan({
99
43
  sourceStack: args.sourceStack ?? 'unknown',
100
44
  targetStack: args.targetStack ?? 'unknown',
101
- components,
45
+ components: [],
102
46
  hasTests: false,
103
47
  });
104
- const warnings = [];
48
+ const warnings = [
49
+ 'No project-derived migration components were provided; plan does not fabricate modules.',
50
+ ];
105
51
  if (data.circularDependencies.length > 0) {
106
52
  warnings.push(`${data.circularDependencies.length} circular dependency cycles detected — resolve before migrating`);
107
53
  }
@@ -114,32 +60,38 @@ export function handlePlan(args) {
114
60
  }
115
61
  export function handleStrategy(_args) {
116
62
  const data = recommendMigrationStrategy({
117
- componentCount: 15,
118
- hasActiveUsers: true,
63
+ componentCount: 0,
64
+ hasActiveUsers: false,
119
65
  hasHighSla: false,
120
66
  hasCriticalData: false,
121
67
  migratableComponents: [],
122
- entryPoints: ['/api/users', '/api/orders', '/api/products'],
68
+ entryPoints: [],
123
69
  });
124
70
  return {
125
71
  action: 'strategy',
126
72
  data,
127
73
  message: t('tools.migrate_tech.strategySuccess'),
128
- warnings: [],
74
+ warnings: [
75
+ 'No project-derived strategy inputs were provided; recommendation uses neutral empty evidence.',
76
+ ],
129
77
  };
130
78
  }
131
79
  export function handleValidate(args) {
132
- const component = args.component ?? 'Unknown Component';
80
+ const component = args.component?.trim() ?? '';
133
81
  const data = generateParityValidation({
134
82
  component,
135
- endpoints: ['GET /api/resource', 'POST /api/resource', 'DELETE /api/resource/:id'],
83
+ endpoints: [],
136
84
  observedDifferences: [],
137
85
  });
138
86
  return {
139
87
  action: 'validate',
140
88
  data,
141
89
  message: t('tools.migrate_tech.validateSuccess'),
142
- warnings: [],
90
+ warnings: args.component === undefined
91
+ ? [
92
+ 'No component was provided; validation checklist is unscoped and no contract tests were generated.',
93
+ ]
94
+ : ['No endpoints were provided; contract tests were not generated.'],
143
95
  };
144
96
  }
145
97
  //# sourceMappingURL=core-handlers.js.map
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { safeLicensed, safeTracked } from './safe-handler.js';
3
+ import { SpecIdSchema } from './schemas/index.js';
3
4
  import { onAutopilotEvent } from '../engine/autopilot/event-bus.js';
4
5
  import { dispatchSquadForPhase } from '../engine/agent-squad/dispatcher.js';
5
6
  import { getSquadConfig, setAgentEnabled, getSpecRunHistory, getSquadRuns, } from '../storage/agent-squad-store.js';
@@ -143,7 +144,7 @@ export function registerAgentSquadTools(server) {
143
144
  'which agents ran, their verdict, and when.',
144
145
  inputSchema: {
145
146
  projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
146
- specId: z.string().min(1).max(100).describe('Spec ID to query (e.g. SPEC-550).'),
147
+ specId: SpecIdSchema.describe('Spec ID to query (e.g. SPEC-550).'),
147
148
  },
148
149
  }, safeTracked('agent_run_history', async (args) => {
149
150
  const { projectPath, specId } = args;
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { t } from '../../i18n/index.js';
3
3
  import { safeTracked, safeLicensed } from '../safe-handler.js';
4
- import { AgentPlatformEnum, SubAgentFrameworkEnum, ChallengeSpecFocusEnum, } from '../schemas/index.js';
4
+ import { AgentPlatformEnum, SubAgentFrameworkEnum, ChallengeSpecFocusEnum, SpecIdSchema, } from '../schemas/index.js';
5
5
  import { handleGenerateADR } from '../generate-adr.js';
6
6
  import { handleDesignSchema } from '../design-schema.js';
7
7
  import { handleDefineUIContract } from '../define-ui-contract.js';
@@ -25,7 +25,7 @@ export function registerDesignStackTools(server) {
25
25
  description: t('tools.generate_adr.description'),
26
26
  annotations: { readOnlyHint: true },
27
27
  inputSchema: {
28
- specId: z.string().max(500).describe('Spec ID'),
28
+ specId: SpecIdSchema.describe('Spec ID'),
29
29
  projectId: z.string().max(500).describe('Project ID'),
30
30
  decisions: z
31
31
  .array(z.string().max(10_000))
@@ -39,7 +39,7 @@ export function registerDesignStackTools(server) {
39
39
  description: t('tools.design_schema.description'),
40
40
  annotations: { readOnlyHint: true },
41
41
  inputSchema: {
42
- specId: z.string().max(500).describe('Spec ID'),
42
+ specId: SpecIdSchema.describe('Spec ID'),
43
43
  projectId: z.string().max(500).describe('Project ID'),
44
44
  description: z
45
45
  .string()
@@ -53,7 +53,7 @@ export function registerDesignStackTools(server) {
53
53
  description: t('tools.define_ui_contract.description'),
54
54
  annotations: { readOnlyHint: true },
55
55
  inputSchema: {
56
- specId: z.string().max(500).describe('Spec ID'),
56
+ specId: SpecIdSchema.describe('Spec ID'),
57
57
  projectId: z.string().max(500).describe('Project ID'),
58
58
  screens: z
59
59
  .array(z.string().max(500))
@@ -67,7 +67,7 @@ export function registerDesignStackTools(server) {
67
67
  description: t('tools.challenge_spec.description'),
68
68
  annotations: { readOnlyHint: true },
69
69
  inputSchema: {
70
- specId: z.string().max(500).describe('Spec ID to challenge'),
70
+ specId: SpecIdSchema.describe('Spec ID to challenge'),
71
71
  projectId: z
72
72
  .string()
73
73
  .max(500)
@@ -90,7 +90,7 @@ export function registerDesignStackTools(server) {
90
90
  description: t('tools.generate_execution_plan.description'),
91
91
  annotations: { readOnlyHint: true },
92
92
  inputSchema: {
93
- specId: z.string().max(500).describe('Spec ID'),
93
+ specId: SpecIdSchema.describe('Spec ID'),
94
94
  projectId: z.string().max(500).describe('Project ID'),
95
95
  },
96
96
  }, safeTracked('generate_execution_plan', async (args) => handleGenerateExecutionPlan(args)));
@@ -145,7 +145,7 @@ export function registerDesignStackTools(server) {
145
145
  .describe('Skill type: project-rules | domain-skill | workflow | convention'),
146
146
  name: z.string().max(500).describe('Skill name'),
147
147
  description: z.string().max(10_000).describe('Skill description'),
148
- specId: z.string().max(500).optional().describe('Spec ID for context'),
148
+ specId: SpecIdSchema.optional().describe('Spec ID for context'),
149
149
  },
150
150
  }, safeLicensed('generate_skill', async (args) => handleGenerateSkill(args)));
151
151
  // 28. detect_agent
@@ -208,10 +208,10 @@ export function registerDesignStackTools(server) {
208
208
  annotations: { readOnlyHint: true },
209
209
  inputSchema: {
210
210
  specIds: z
211
- .array(z.string().max(500))
211
+ .array(SpecIdSchema)
212
212
  .min(1)
213
213
  .max(100)
214
- .describe('List of spec IDs to run in parallel (e.g. ["SPEC-013", "SPEC-014a"])'),
214
+ .describe('List of canonical spec IDs to run in parallel (e.g. ["SPEC-013", "SPEC-014"])'),
215
215
  projectPath: z.string().max(4096).describe('Absolute path to the project root'),
216
216
  mainBranch: z.string().max(500).optional().describe('Main branch name (default: main)'),
217
217
  baseBranch: z
@@ -226,7 +226,7 @@ export function registerDesignStackTools(server) {
226
226
  description: 'Adversarial review — argues against the spec from first principles before approval. Runs pre-mortem, surfaces hidden assumptions, proposes alternatives.',
227
227
  annotations: { readOnlyHint: true },
228
228
  inputSchema: {
229
- specId: z.string().max(500).describe('Spec ID to red-team'),
229
+ specId: SpecIdSchema.describe('Spec ID to red-team'),
230
230
  projectId: z.string().max(500).describe('Project ID'),
231
231
  intensity: z
232
232
  .enum(['light', 'full'])
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { t } from '../../i18n/index.js';
3
3
  import { safeTracked, safeLicensed } from '../safe-handler.js';
4
- import { DocumentationTypeEnum, GitActionEnum, DevLifecycleCategoryEnum, PMPlatformEnum, PMActionEnum, OrchestrateActionEnum, ManageContextActionEnum, ContextBudgetActionEnum, } from '../schemas/index.js';
4
+ import { DocumentationTypeEnum, GitActionEnum, DevLifecycleCategoryEnum, PMPlatformEnum, PMActionEnum, OrchestrateActionEnum, ManageContextActionEnum, ContextBudgetActionEnum, SpecIdSchema, } from '../schemas/index.js';
5
5
  import { handleSuggestTooling } from '../suggest-tooling.js';
6
6
  import { handleGenerateTests } from '../generate-tests.js';
7
7
  import { handleGenerateDocs } from '../generate-docs.js';
@@ -21,7 +21,7 @@ export function registerLifecycleInfraTools(server) {
21
21
  annotations: { readOnlyHint: true },
22
22
  inputSchema: {
23
23
  projectId: z.string().max(500).describe('Project ID'),
24
- specId: z.string().max(500).optional().describe('Spec ID for context'),
24
+ specId: SpecIdSchema.optional().describe('Spec ID for context'),
25
25
  categories: z
26
26
  .array(DevLifecycleCategoryEnum)
27
27
  .optional()
@@ -33,7 +33,7 @@ export function registerLifecycleInfraTools(server) {
33
33
  description: t('tools.generate_tests.description'),
34
34
  annotations: { readOnlyHint: true },
35
35
  inputSchema: {
36
- specId: z.string().max(500).describe('Spec ID'),
36
+ specId: SpecIdSchema.describe('Spec ID'),
37
37
  projectId: z.string().max(500).describe('Project ID'),
38
38
  autoGenerate: z.boolean().optional().describe('Auto-generate test files (default: false)'),
39
39
  },
@@ -49,7 +49,7 @@ export function registerLifecycleInfraTools(server) {
49
49
  .max(100)
50
50
  .describe('Types of documentation to generate'),
51
51
  docType: DocumentationTypeEnum.optional().describe('Single documentation type for routing — shorthand for types: [docType]'),
52
- specId: z.string().max(500).optional().describe('Spec ID for context'),
52
+ specId: SpecIdSchema.optional().describe('Spec ID for context'),
53
53
  audience: z
54
54
  .enum(['end-user', 'developer', 'stakeholder', 'ops'])
55
55
  .optional()
@@ -88,7 +88,7 @@ export function registerLifecycleInfraTools(server) {
88
88
  inputSchema: {
89
89
  projectId: z.string().max(500).describe('Project ID'),
90
90
  action: GitActionEnum.describe('Git action to perform'),
91
- specId: z.string().max(500).optional().describe('Spec ID for context'),
91
+ specId: SpecIdSchema.optional().describe('Spec ID for context'),
92
92
  config: z
93
93
  .object({
94
94
  branchPrefix: z.record(z.string().max(500), z.string().max(500)).optional(),
@@ -113,10 +113,10 @@ export function registerLifecycleInfraTools(server) {
113
113
  projectId: z.string().max(500).describe('Project ID'),
114
114
  action: OrchestrateActionEnum.describe('Orchestration action'),
115
115
  sessionId: z.string().max(500).optional().describe('Agent session ID'),
116
- specId: z.string().max(500).optional().describe('Spec ID for context'),
116
+ specId: SpecIdSchema.optional().describe('Spec ID for context'),
117
117
  resourcePath: z.string().max(4096).optional().describe('Resource path to lock/unlock'),
118
118
  specIds: z
119
- .array(z.string().max(500))
119
+ .array(SpecIdSchema)
120
120
  .max(100)
121
121
  .optional()
122
122
  .describe('List of spec IDs for pre-check'),
@@ -163,7 +163,7 @@ export function registerLifecycleInfraTools(server) {
163
163
  projectId: z.string().max(500).describe('Project ID'),
164
164
  platform: PMPlatformEnum.describe('PM platform'),
165
165
  action: PMActionEnum.describe('Integration action'),
166
- specId: z.string().max(500).optional().describe('Spec ID'),
166
+ specId: SpecIdSchema.optional().describe('Spec ID'),
167
167
  config: z
168
168
  .record(z.string().max(500), z.string().max(10_000))
169
169
  .optional()
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { t } from '../../i18n/index.js';
3
3
  import { safeTracked, safeLicensed } from '../safe-handler.js';
4
- import { ChecklistFocusEnum, AuditCategoryEnum } from '../schemas/index.js';
4
+ import { ChecklistFocusEnum, AuditCategoryEnum, SpecIdSchema } from '../schemas/index.js';
5
5
  import { resolveProjectId, missingProjectIdError } from '../resolve-project-id.js';
6
6
  import { handleDetectDrift } from '../detect-drift.js';
7
7
  import { handleSummarizeSpec } from '../summarize-spec.js';
@@ -27,7 +27,7 @@ export function registerAnalysisTools(server) {
27
27
  server.registerTool('detect_drift', {
28
28
  description: t('tools.detect_drift.description'),
29
29
  inputSchema: {
30
- specId: z.string().min(1).max(500).describe('Spec ID to check'),
30
+ specId: SpecIdSchema.describe('Spec ID to check'),
31
31
  projectId: z.string().max(500).optional().describe('Project ID hash. Prefer projectPath.'),
32
32
  projectPath: z
33
33
  .string()
@@ -56,7 +56,7 @@ export function registerAnalysisTools(server) {
56
56
  description: t('tools.summarize_spec.description'),
57
57
  annotations: { readOnlyHint: true },
58
58
  inputSchema: {
59
- specId: z.string().min(1).max(500).describe('Spec ID to summarize'),
59
+ specId: SpecIdSchema.describe('Spec ID to summarize'),
60
60
  projectId: z.string().max(500).optional().describe('Project ID hash. Prefer projectPath.'),
61
61
  projectPath: z
62
62
  .string()
@@ -79,7 +79,7 @@ export function registerAnalysisTools(server) {
79
79
  server.registerTool('generate_checklist', {
80
80
  description: t('tools.generate_checklist.description'),
81
81
  inputSchema: {
82
- specId: z.string().min(1).max(500).describe('Spec ID'),
82
+ specId: SpecIdSchema.describe('Spec ID'),
83
83
  projectId: z.string().max(500).optional().describe('Project ID hash. Prefer projectPath.'),
84
84
  projectPath: z
85
85
  .string()
@@ -103,7 +103,7 @@ export function registerAnalysisTools(server) {
103
103
  server.registerTool('reconcile_spec', {
104
104
  description: t('tools.reconcile_spec.description'),
105
105
  inputSchema: {
106
- specId: z.string().min(1).max(500).describe('Spec ID to reconcile'),
106
+ specId: SpecIdSchema.describe('Spec ID to reconcile'),
107
107
  projectId: z.string().max(500).optional().describe('Project ID hash. Prefer projectPath.'),
108
108
  projectPath: z
109
109
  .string()
@@ -176,7 +176,7 @@ export function registerAnalysisTools(server) {
176
176
  .max(100)
177
177
  .optional()
178
178
  .describe('Audit categories to check'),
179
- specId: z.string().max(500).optional().describe('Spec ID to audit against'),
179
+ specId: SpecIdSchema.optional().describe('Spec ID to audit against'),
180
180
  },
181
181
  }, safeLicensed('audit', async (args) => {
182
182
  const pid = resolveProjectId(args);
@@ -231,7 +231,7 @@ export function registerAnalysisTools(server) {
231
231
  .max(4096)
232
232
  .optional()
233
233
  .describe('Absolute path to project root. Derives projectId automatically.'),
234
- specId: z.string().max(500).optional().describe('Spec ID for context-specific suggestions'),
234
+ specId: SpecIdSchema.optional().describe('Spec ID for context-specific suggestions'),
235
235
  },
236
236
  }, safeLicensed('suggest_mcps', async (args) => {
237
237
  const pid = resolveProjectId(args);
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { t } from '../../i18n/index.js';
3
3
  import { safeTracked, safeLicensed } from '../safe-handler.js';
4
- import { SupportedLocaleEnum, SpecStatusEnum, SpecTypeEnum, SpecScopeEnum, SpecTargetEnum, ExperienceLevelEnum, WorkModeEnum, ListSpecsOutputSchema, EstimateOutputSchema, ValidateOutputSchema, } from '../schemas/index.js';
4
+ import { SupportedLocaleEnum, SpecStatusEnum, SpecTypeEnum, SpecScopeEnum, SpecTargetEnum, ExperienceLevelEnum, WorkModeEnum, ListSpecsOutputSchema, EstimateOutputSchema, ValidateOutputSchema, SpecIdSchema, } from '../schemas/index.js';
5
5
  import { handleSetLocale } from '../set-locale.js';
6
6
  import { handleInitProject } from '../init-project.js';
7
7
  import { handleSetWorkMode } from '../set-work-mode-handler.js';
@@ -274,7 +274,7 @@ export function registerCoreSpecTools(server) {
274
274
  server.registerTool('update_status', {
275
275
  description: t('tools.update_status.description'),
276
276
  inputSchema: {
277
- specId: z.string().min(1).max(500).describe('Spec ID to update'),
277
+ specId: SpecIdSchema.describe('Spec ID to update'),
278
278
  projectId: z
279
279
  .string()
280
280
  .max(500)
@@ -379,7 +379,7 @@ export function registerCoreSpecTools(server) {
379
379
  server.registerTool('update_status_batch', {
380
380
  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.',
381
381
  inputSchema: {
382
- specIds: z.array(z.string().min(1).max(500)).min(1).describe('Spec IDs to update'),
382
+ specIds: z.array(SpecIdSchema).min(1).describe('Spec IDs to update'),
383
383
  projectId: z.string().max(500).optional().describe('Project ID, if known'),
384
384
  projectPath: z
385
385
  .string()
@@ -399,7 +399,7 @@ export function registerCoreSpecTools(server) {
399
399
  server.registerTool('estimate', {
400
400
  description: t('tools.estimate.description'),
401
401
  inputSchema: {
402
- specId: z.string().max(500).describe('Spec ID to estimate'),
402
+ specId: SpecIdSchema.describe('Spec ID to estimate'),
403
403
  projectId: z.string().max(500).describe('Project ID'),
404
404
  },
405
405
  outputSchema: EstimateOutputSchema,
@@ -420,7 +420,7 @@ export function registerCoreSpecTools(server) {
420
420
  server.registerTool('validate', {
421
421
  description: t('tools.validate.description'),
422
422
  inputSchema: {
423
- specId: z.string().max(500).describe('Spec ID to validate'),
423
+ specId: SpecIdSchema.describe('Spec ID to validate'),
424
424
  projectId: z
425
425
  .string()
426
426
  .max(500)
@@ -441,7 +441,7 @@ export function registerCoreSpecTools(server) {
441
441
  'and missing parameter type annotations. Returns a health score (0-100) and actionable suggestions.',
442
442
  inputSchema: {
443
443
  projectPath: z.string().max(4096).describe('Absolute path to the project root'),
444
- specId: z.string().min(1).max(500).describe('Spec ID being validated'),
444
+ specId: SpecIdSchema.describe('Spec ID being validated'),
445
445
  waiver: z
446
446
  .array(z.string().max(4096))
447
447
  .max(100)
@@ -8,9 +8,10 @@ import { detectLLMClient } from '../engine/client-detection.js';
8
8
  import { getRegisteredProviders, getRenderer, llmClientToProvider, } from '../engine/provider-adapters/registry.js';
9
9
  import { loadSpecForRendering } from '../engine/provider-adapters/shared/spec-loader.js';
10
10
  import { compactError, compactResult, formatKeyValue } from './output-formatter.js';
11
+ import { SpecIdSchema } from './schemas/index.js';
11
12
  const PROVIDER_VALUES = ['claude', 'gpt4', 'gemini', 'markdown'];
12
13
  export const RenderSpecForProviderInputSchema = z.object({
13
- specId: z.string().describe('Spec identifier, e.g. "SPEC-670"'),
14
+ specId: SpecIdSchema.describe('Spec identifier, e.g. "SPEC-670"'),
14
15
  projectPath: z.string().describe('Absolute path to the project root'),
15
16
  provider: z
16
17
  .enum(PROVIDER_VALUES)
@@ -1,7 +1,8 @@
1
1
  // schemas/github.ts — Zod schemas for GitHub integration tools (SPEC-086).
2
2
  import { z } from 'zod';
3
+ import { SpecIdSchema } from './spec-id.js';
3
4
  export const CreatePRFromSpecSchema = z.object({
4
- specId: z.string().min(1).max(500).describe('ID of the spec (e.g., SPEC-086)'),
5
+ specId: SpecIdSchema.describe('ID of the spec (e.g., SPEC-086)'),
5
6
  baseBranch: z
6
7
  .string()
7
8
  .max(500)
@@ -11,7 +12,7 @@ export const CreatePRFromSpecSchema = z.object({
11
12
  });
12
13
  export const PRStatusSchema = z.object({
13
14
  prNumber: z.number().optional().describe('Specific PR number to query'),
14
- specId: z.string().max(500).optional().describe('Spec ID to find related PRs (e.g., SPEC-086)'),
15
+ specId: SpecIdSchema.optional().describe('Spec ID to find related PRs (e.g., SPEC-086)'),
15
16
  view: z.enum(['prs', 'milestones']).default('prs').describe('View mode: prs | milestones'),
16
17
  });
17
18
  export const ReviewPRSchema = z.object({
@@ -55,7 +56,7 @@ export const GenerateChangelogSchema = z.object({
55
56
  .describe('Write the generated changelog to CHANGELOG.md (prepends to existing file). Default: false.'),
56
57
  });
57
58
  export const CreateIssueFromSpecSchema = z.object({
58
- specId: z.string().min(1).max(500).describe('ID of the spec (e.g., SPEC-086)'),
59
+ specId: SpecIdSchema.describe('ID of the spec (e.g., SPEC-086)'),
59
60
  criteriaIds: z
60
61
  .array(z.string().max(500))
61
62
  .max(1000)
@@ -1,4 +1,5 @@
1
1
  export { SupportedLocaleEnum, SpecStatusEnum, SpecTypeEnum, SpecScopeEnum, SpecTargetEnum, ExperienceLevelEnum, WorkModeEnum, } from './spec.js';
2
+ export { SPEC_ID_PATTERN, SPEC_DIR_NAME_PATTERN, SpecIdSchema, SpecDirNameSchema, isCanonicalSpecId, } from './spec-id.js';
2
3
  export { AuditCategoryEnum, ChecklistFocusEnum, ChallengeSpecFocusEnum } from './analysis.js';
3
4
  export { AgentPlatformEnum, SubAgentFrameworkEnum } from './agents.js';
4
5
  export { DocumentationTypeEnum, GitActionEnum, DevLifecycleCategoryEnum, PMPlatformEnum, PMActionEnum, } from './lifecycle.js';
@@ -1,5 +1,6 @@
1
1
  // schemas/index.ts — Barrel re-export for all Zod enum schemas.
2
2
  export { SupportedLocaleEnum, SpecStatusEnum, SpecTypeEnum, SpecScopeEnum, SpecTargetEnum, ExperienceLevelEnum, WorkModeEnum, } from './spec.js';
3
+ export { SPEC_ID_PATTERN, SPEC_DIR_NAME_PATTERN, SpecIdSchema, SpecDirNameSchema, isCanonicalSpecId, } from './spec-id.js';
3
4
  export { AuditCategoryEnum, ChecklistFocusEnum, ChallengeSpecFocusEnum } from './analysis.js';
4
5
  export { AgentPlatformEnum, SubAgentFrameworkEnum } from './agents.js';
5
6
  export { DocumentationTypeEnum, GitActionEnum, DevLifecycleCategoryEnum, PMPlatformEnum, PMActionEnum, } from './lifecycle.js';
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ export declare const SPEC_ID_PATTERN: RegExp;
3
+ export declare const SPEC_DIR_NAME_PATTERN: RegExp;
4
+ export declare const SpecIdSchema: z.ZodString;
5
+ export declare const SpecDirNameSchema: z.ZodString;
6
+ export declare function isCanonicalSpecId(value: string): boolean;
7
+ //# sourceMappingURL=spec-id.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export const SPEC_ID_PATTERN = /^SPEC-\d+$/;
3
+ export const SPEC_DIR_NAME_PATTERN = /^SPEC-\d+(?:-[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
4
+ export const SpecIdSchema = z
5
+ .string()
6
+ .min(1)
7
+ .max(50)
8
+ .regex(SPEC_ID_PATTERN, 'Spec ID must use canonical numeric format like SPEC-042.');
9
+ export const SpecDirNameSchema = z
10
+ .string()
11
+ .min(1)
12
+ .max(200)
13
+ .regex(SPEC_DIR_NAME_PATTERN, 'Spec directory must start with a canonical numeric ID like SPEC-042 or SPEC-042-my-feature.');
14
+ export function isCanonicalSpecId(value) {
15
+ return SPEC_ID_PATTERN.test(value);
16
+ }
17
+ //# sourceMappingURL=spec-id.js.map
@@ -4,6 +4,7 @@ import { getSpec } from '../storage/spec-store.js';
4
4
  import { hashProjectPath } from '../storage/base-store.js';
5
5
  import { generatePromptFromSpec } from '../engine/spec-prompt-generator.js';
6
6
  import { compactResult } from './output-formatter.js';
7
+ import { SpecIdSchema } from './schemas/index.js';
7
8
  // ---------------------------------------------------------------------------
8
9
  // In-memory registry of registered prompts for this session
9
10
  // ---------------------------------------------------------------------------
@@ -12,7 +13,7 @@ export const registeredPrompts = new Map();
12
13
  // Zod schemas
13
14
  // ---------------------------------------------------------------------------
14
15
  const ExposeSpecAsPromptInputSchema = {
15
- specId: z.string().describe('The spec ID to expose as an MCP prompt (e.g. "SPEC-042")'),
16
+ specId: SpecIdSchema.describe('The spec ID to expose as an MCP prompt (e.g. "SPEC-042")'),
16
17
  projectPath: z.string().describe('Absolute path to the project root where the spec is stored'),
17
18
  promptName: z
18
19
  .string()
@@ -10,6 +10,7 @@
10
10
  // To add a tool: import its handler + schema, add an entry to the array.
11
11
  // To read them: import coreToolsRegistry from this file.
12
12
  import { z } from 'zod';
13
+ import { SpecIdSchema } from '../schemas/index.js';
13
14
  // ── Handlers ────────────────────────────────────────────────────────────────
14
15
  import { GenerateBatchScriptInputSchema, handleGenerateBatchScript, } from '../generate-batch-script.js';
15
16
  import { GenerateAutomationGuideInputSchema, handleGenerateAutomationGuide, } from '../generate-automation-guide.js';
@@ -373,7 +374,7 @@ const coreToolsRegistry = [
373
374
  '(structured JSON optimized for autonomous coding agents: Devin, Kiro, SWE-agent, generic). ' +
374
375
  'Writes files to .specify/specs/{slug}/ by default.',
375
376
  schema: {
376
- specId: z.string().min(1).max(500).describe('Spec ID to export (e.g. "SPEC-001")'),
377
+ specId: SpecIdSchema.describe('Spec ID to export (e.g. "SPEC-001")'),
377
378
  projectPath: z.string().min(1).max(2000).describe('Absolute path to the project root'),
378
379
  format: z
379
380
  .enum(['spec-kit', 'agent-ready'])
@@ -687,7 +688,7 @@ const coreToolsRegistry = [
687
688
  'detects drift, and updates the ## Progress section of spec.md with real coverage data. ' +
688
689
  'Works entirely offline — pure local file analysis.',
689
690
  schema: {
690
- specId: z.string().min(1).max(500).describe('Spec ID, e.g. SPEC-042'),
691
+ specId: SpecIdSchema.describe('Spec ID, e.g. SPEC-042'),
691
692
  projectPath: z.string().min(1).max(4096).describe('Absolute path to project root'),
692
693
  },
693
694
  handler: async (args) => handleReconcileSpecLiving({
@@ -13,7 +13,7 @@ import { safe, safeLicensed, safeTracked } from '../safe-handler.js';
13
13
  // ── License tools (register-license-tools.ts) ───────────────────────────────
14
14
  import { handleActivateLicense, handleDeactivateLicense } from '../activate-license.js';
15
15
  import { handleLicenseStatus } from '../license-status.js';
16
- import { LicenseStatusOutputSchema } from '../schemas/index.js';
16
+ import { LicenseStatusOutputSchema, SpecIdSchema } from '../schemas/index.js';
17
17
  // ── OAuth tools (register-oauth-tools.ts / register-configure-oauth-tool.ts) ─
18
18
  import { handleStartOAuthFlow, handleOAuthStatus, StartOAuthFlowInputSchema, OAuthStatusInputSchema, } from '../oauth-handler.js';
19
19
  import { handleConfigureOAuth } from '../configure-oauth-handler.js';
@@ -386,7 +386,7 @@ export function registerInfraGroupTools(server) {
386
386
  inputSchema: {
387
387
  projectPath: z.string().min(1).describe('Absolute path to the project root'),
388
388
  provider: deployProviderEnum,
389
- specId: z.string().optional().describe('Optional spec ID for traceability'),
389
+ specId: SpecIdSchema.optional().describe('Optional spec ID for traceability'),
390
390
  environment: z
391
391
  .enum(['production', 'preview'])
392
392
  .optional()
@@ -484,7 +484,7 @@ export function registerInfraGroupTools(server) {
484
484
  .string()
485
485
  .min(1)
486
486
  .describe('Absolute path to the project root (e.g. "/home/user/my-app").'),
487
- specId: z.string().min(1).describe('Spec ID to validate (e.g. "SPEC-042").'),
487
+ specId: SpecIdSchema.describe('Spec ID to validate (e.g. "SPEC-042").'),
488
488
  baseUrl: z
489
489
  .string()
490
490
  .min(1)