@planu/cli 4.1.1 → 4.1.3

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 (62) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/config/license-plans.json +65 -361
  3. package/dist/engine/core-bridge.js +35 -4
  4. package/dist/engine/hooks/file-watcher.d.ts +6 -0
  5. package/dist/engine/hooks/file-watcher.js +69 -16
  6. package/dist/tools/git/hook-ops.js +23 -9
  7. package/dist/tools/tool-registry/group-infra.js +22 -0
  8. package/package.json +7 -7
  9. package/dist/engine/escalator/index.d.ts +0 -5
  10. package/dist/engine/escalator/index.js +0 -5
  11. package/dist/engine/freeze/retro-audit.d.ts +0 -6
  12. package/dist/engine/freeze/retro-audit.js +0 -24
  13. package/dist/engine/heal/backup.d.ts +0 -9
  14. package/dist/engine/heal/backup.js +0 -21
  15. package/dist/engine/idioma-validator/index.d.ts +0 -17
  16. package/dist/engine/idioma-validator/index.js +0 -89
  17. package/dist/engine/saga/index.d.ts +0 -4
  18. package/dist/engine/saga/index.js +0 -4
  19. package/dist/engine/spec-state-machine/index.d.ts +0 -3
  20. package/dist/engine/spec-state-machine/index.js +0 -2
  21. package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +0 -6
  22. package/dist/engine/spec-summary-html/dashboard-renderer.js +0 -333
  23. package/dist/engine/triagier/index.d.ts +0 -5
  24. package/dist/engine/triagier/index.js +0 -5
  25. package/dist/engine/universal-rules/index.d.ts +0 -5
  26. package/dist/engine/universal-rules/index.js +0 -6
  27. package/dist/testing/cassette/index.d.ts +0 -23
  28. package/dist/testing/cassette/index.js +0 -26
  29. package/dist/tools/domain-bundle-handler.d.ts +0 -37
  30. package/dist/tools/domain-bundle-handler.js +0 -71
  31. package/dist/tools/figma/rules-file.d.ts +0 -5
  32. package/dist/tools/figma/rules-file.js +0 -45
  33. package/dist/tools/heal-planu-root.d.ts +0 -8
  34. package/dist/tools/heal-planu-root.js +0 -144
  35. package/dist/tools/opencode-host-adapter.d.ts +0 -3
  36. package/dist/tools/opencode-host-adapter.js +0 -33
  37. package/dist/tools/plan-team-distribution.d.ts +0 -3
  38. package/dist/tools/plan-team-distribution.js +0 -71
  39. package/dist/tools/reconcile-status-json.d.ts +0 -4
  40. package/dist/tools/reconcile-status-json.js +0 -209
  41. package/dist/tools/register-all-tools.d.ts +0 -8
  42. package/dist/tools/register-all-tools.js +0 -239
  43. package/dist/tools/tool-registry/group-analysis-monitoring.d.ts +0 -3
  44. package/dist/tools/tool-registry/group-analysis-monitoring.js +0 -942
  45. package/dist/tools/tool-registry/group-integrations.d.ts +0 -3
  46. package/dist/tools/tool-registry/group-integrations.js +0 -1046
  47. package/dist/tools/tool-registry/group-misc.d.ts +0 -3
  48. package/dist/tools/tool-registry/group-misc.js +0 -1367
  49. package/dist/tools/tool-registry/group-platform.d.ts +0 -3
  50. package/dist/tools/tool-registry/group-platform.js +0 -1681
  51. package/dist/tools/tool-registry/group-session-knowledge.d.ts +0 -3
  52. package/dist/tools/tool-registry/group-session-knowledge.js +0 -1416
  53. package/dist/tools/tool-registry/group-spec-ops.d.ts +0 -3
  54. package/dist/tools/tool-registry/group-spec-ops.js +0 -917
  55. package/dist/tools/workspace-overview.d.ts +0 -4
  56. package/dist/tools/workspace-overview.js +0 -316
  57. package/dist/transports/middleware/index.d.ts +0 -9
  58. package/dist/transports/middleware/index.js +0 -7
  59. package/dist/transports/middleware/with-sandbox.d.ts +0 -21
  60. package/dist/transports/middleware/with-sandbox.js +0 -68
  61. package/dist/types/heal.d.ts +0 -18
  62. package/dist/types/heal.js +0 -3
@@ -1,917 +0,0 @@
1
- /* eslint-disable max-lines -- registration catalog: one block per tool group, cannot split further without losing colocation */
2
- // tools/tool-registry/group-spec-ops.ts — Declarative registry: spec operations tools.
3
- //
4
- // Phase 3 (refactor/registry-phase3): consolidates register-*.ts files for:
5
- // spec-export, spec-split, spec-quality-score, spec-search, spec-lock,
6
- // spec-hooks, spec-source, versioning, diff-spec, spec-diff-visual,
7
- // merge-risk (via re-export), domain-criteria, domain-bundle
8
- //
9
- // NOTE: register-spec-tools.ts and register-spec-prompt-tools.ts are barrel files
10
- // that delegate to sub-registrars — they are kept as-is.
11
- import { z } from 'zod';
12
- import { safeLicensed, safeTracked } from '../safe-handler.js';
13
- import { projectIdSchema, withProject } from '../tool-registry-helpers.js';
14
- // ── Spec export ───────────────────────────────────────────────────────────────
15
- import { handleExportSpecsCsv, handleExportSpecsMarkdown } from '../spec-export-handler.js';
16
- // ── SPEC-601: Spec portability (bundle import/export) ────────────────────────
17
- import { handleExportSpec as handleExportBundle, handleImportSpec as handleImportBundle, } from '../spec-portability-handler.js';
18
- // ── SPEC-613: Gherkin import/export ──────────────────────────────────────────
19
- import { handleImportGherkin } from '../import-gherkin.js';
20
- import { handleExportGherkin } from '../export-gherkin.js';
21
- // ── Spec split ────────────────────────────────────────────────────────────────
22
- import { handleAnalyzeSpecSize, handleSplitSpec } from '../spec-split-handler.js';
23
- // ── Spec quality score ────────────────────────────────────────────────────────
24
- import { handleSpecQualityScore } from '../spec-quality-score-handler.js';
25
- // ── Spec search ───────────────────────────────────────────────────────────────
26
- import { handleSearchSpecs, handleSearchSuggestions } from '../search-handler.js';
27
- // ── Spec lock ─────────────────────────────────────────────────────────────────
28
- import { handleLockSpec, handleUnlockSpec, handleListLocks, handleCheckSpecLock, } from '../spec-lock-handler.js';
29
- // ── Spec hooks ────────────────────────────────────────────────────────────────
30
- import { handleConfigureSpecHooks, handleListSpecHooks, handleTestSpecHook, handleDisableSpecHook, handleEnableSpecHook, } from '../spec-hooks-handler.js';
31
- // ── Spec source ───────────────────────────────────────────────────────────────
32
- import { handleSyncSpecFromCode, handleMarkAsSpecSource, handleListSpecBindings, } from '../spec-source-handler.js';
33
- // ── Versioning ────────────────────────────────────────────────────────────────
34
- import { handleVersionSpec } from '../version-spec.js';
35
- import { handleBranchSpec } from '../branch-spec.js';
36
- import { handleMergeSpecBranch } from '../merge-spec-branch.js';
37
- import { handleDiffSpecVersions, handleSpecVersionHistory } from '../spec-diff-handler.js';
38
- // ── SPEC-717: bump_spec_version ───────────────────────────────────────────────
39
- import { handleBumpSpecVersion } from '../bump-spec-version.js';
40
- // ── Diff spec (generate from git diff) ───────────────────────────────────────
41
- import { handleGenerateSpecFromDiff, handleGenerateSpecFromPr, handleGenerateSpecFromCommitRange, } from '../diff-spec-generator-handler.js';
42
- // ── Visual spec diff ──────────────────────────────────────────────────────────
43
- import { handleVisualSpecDiff, handleThreeWayMerge, handleSmartMergeConflictResolver, handleMergePreview, handleApplyMerge, } from '../spec-visual-diff-handler.js';
44
- // ── Merge risk ────────────────────────────────────────────────────────────────
45
- import { registerMergeRiskTools } from '../merge-risk-handler.js';
46
- // ── Domain criteria ───────────────────────────────────────────────────────────
47
- import { handleSuggestCriteria, handleInjectCriteria } from '../domain-criteria-handler.js';
48
- // ── Domain bundle ─────────────────────────────────────────────────────────────
49
- import { handleApplyDomainBundle, handleListDomainBundles, ApplyDomainBundleInputSchema, ListDomainBundlesInputSchema, } from '../domain-bundle-handler.js';
50
- // ---------------------------------------------------------------------------
51
- // Shared schema constants
52
- // ---------------------------------------------------------------------------
53
- const COLUMN_VALUES = [
54
- 'id',
55
- 'title',
56
- 'status',
57
- 'type',
58
- 'scope',
59
- 'difficulty',
60
- 'risk',
61
- 'target',
62
- 'tags',
63
- 'createdAt',
64
- 'updatedAt',
65
- 'devHours',
66
- 'totalCostUsd',
67
- 'estimatedModel',
68
- ];
69
- const COLUMN_DESCRIBE = 'Columns to include. Valid values: id, title, status, type, scope, difficulty, risk, target, tags, createdAt, updatedAt, devHours, totalCostUsd, estimatedModel. Defaults to all columns.';
70
- const columnsSchema = z.array(z.enum(COLUMN_VALUES)).optional().describe(COLUMN_DESCRIBE);
71
- const exportStatusSchema = z
72
- .array(z.string().max(50))
73
- .max(20)
74
- .optional()
75
- .describe('Filter by status. Valid values: draft, review, approved, implementing, done, cancelled.');
76
- const exportTypeSchema = z
77
- .array(z.string().max(50))
78
- .max(20)
79
- .optional()
80
- .describe('Filter by spec type. Valid values: feature, refactor, bugfix, infra, docs, project, agent, tech-debt.');
81
- const exportTagsSchema = z
82
- .array(z.string().max(500))
83
- .max(100)
84
- .optional()
85
- .describe('Filter specs that have at least one of these tags.');
86
- const exportOutputPathSchema = z
87
- .string()
88
- .optional()
89
- .describe('Absolute path to write the output file. If omitted, the content is returned but not written.');
90
- const STATUS_VALUES = ['draft', 'review', 'approved', 'implementing', 'done', 'discarded'];
91
- const TYPE_VALUES = [
92
- 'feature',
93
- 'refactor',
94
- 'bugfix',
95
- 'infra',
96
- 'docs',
97
- 'project',
98
- 'agent',
99
- 'tech-debt',
100
- ];
101
- const TARGET_VALUES = [
102
- 'frontend',
103
- 'backend',
104
- 'shared',
105
- 'fullstack',
106
- 'infrastructure',
107
- 'database',
108
- 'ios',
109
- 'android',
110
- ];
111
- const RISK_VALUES = ['low', 'medium', 'high', 'critical'];
112
- const SORT_BY_VALUES = ['relevance', 'createdAt', 'updatedAt', 'difficulty'];
113
- const SORT_ORDER_VALUES = ['asc', 'desc'];
114
- const SUGGESTION_FIELD_VALUES = ['tags', 'type', 'status', 'id'];
115
- const EVENT_DESCRIBE = 'Event that triggers the hook. Valid values: on-status-change | on-create | on-approve | on-done | on-drift-alert | on-validation-fail';
116
- const ACTION_DESCRIBE = 'Action to execute. Valid values: challenge-spec | check-readiness | notify-slack | create-git-branch | generate-changelog | send-email-digest | run-validate';
117
- // ---------------------------------------------------------------------------
118
- // Registration
119
- // ---------------------------------------------------------------------------
120
- // eslint-disable-next-line max-lines-per-function -- registration catalog: one block per tool, each ~20 lines
121
- export function registerSpecOpsGroupTools(s) {
122
- // ── Spec export ──────────────────────────────────────────────────────────────
123
- s.registerTool('export_specs_csv', {
124
- description: 'Export specs to CSV format for spreadsheet analysis. Supports filtering by status, type, or tags and custom column selection. Pro tier.',
125
- annotations: { readOnlyHint: true },
126
- inputSchema: {
127
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
128
- columns: columnsSchema,
129
- status: exportStatusSchema,
130
- type: exportTypeSchema,
131
- tags: exportTagsSchema,
132
- outputPath: exportOutputPathSchema,
133
- },
134
- }, safeTracked('export_specs_csv', async (args) => handleExportSpecsCsv(args)));
135
- s.registerTool('export_specs_markdown', {
136
- description: 'Export specs to a Markdown table for docs, reports, or PR descriptions. Supports filtering by status, type, or tags and custom column selection.',
137
- annotations: { readOnlyHint: true },
138
- inputSchema: {
139
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
140
- columns: columnsSchema,
141
- status: exportStatusSchema,
142
- type: exportTypeSchema,
143
- tags: exportTagsSchema,
144
- outputPath: exportOutputPathSchema,
145
- },
146
- }, safeTracked('export_specs_markdown', async (args) => handleExportSpecsMarkdown(args)));
147
- // ── Spec split ───────────────────────────────────────────────────────────────
148
- s.registerTool('analyze_spec_size', {
149
- description: 'Analyze a spec to determine if it is too large or complex and should be split into ' +
150
- 'smaller, more atomic specs. ' +
151
- 'Checks three criteria: >15 acceptance criteria, estimation >40h, or >3 subsystem signals. ' +
152
- 'Also detects ambiguous criteria with vague language (e.g. "should handle", "properly"). ' +
153
- 'Returns a SpecSizeAnalysis with shouldSplit flag, reasons, and suggestedSplits.',
154
- annotations: { readOnlyHint: true },
155
- inputSchema: {
156
- ...projectIdSchema,
157
- specId: z.string().min(1).max(500).describe('Spec ID to analyze (e.g. SPEC-042)'),
158
- },
159
- }, safeLicensed('analyze_spec_size', withProject((args) => handleAnalyzeSpecSize(args))));
160
- s.registerTool('split_spec', {
161
- description: 'Execute the division of a large spec into smaller child specs. ' +
162
- 'Creates N new child specs with fresh IDs, marks the original as superseded, ' +
163
- 'and distributes acceptance criteria according to the proposals. ' +
164
- 'Child specs inherit tags, target, type and risk from the original. ' +
165
- 'Use analyze_spec_size first to get the suggested splits, then optionally customize ' +
166
- 'proposedSplits before calling this tool. ' +
167
- 'DESTRUCTIVE: modifies the original spec status to "superseded".',
168
- annotations: { destructiveHint: true },
169
- inputSchema: {
170
- ...projectIdSchema,
171
- specId: z.string().min(1).max(500).describe('Spec ID to split (e.g. SPEC-042)'),
172
- proposedSplits: z
173
- .array(z.object({
174
- title: z.string().max(200).describe('Title for the child spec'),
175
- description: z.string().max(1000).describe('Description of this child spec scope'),
176
- criteriaIndices: z
177
- .array(z.number().int().min(0))
178
- .describe('Zero-based indices of acceptance criteria from the original spec to assign to this child'),
179
- dependsOn: z
180
- .array(z.number().int().min(0))
181
- .describe('Zero-based indices of other proposedSplits that this child depends on'),
182
- }))
183
- .optional()
184
- .describe('Custom split proposals. If omitted, uses auto-generated proposals from analyze_spec_size.'),
185
- },
186
- }, safeLicensed('split_spec', withProject((args) => handleSplitSpec(args))));
187
- // ── Spec quality score ───────────────────────────────────────────────────────
188
- s.registerTool('spec_quality_score', {
189
- description: 'Compute a 0-100 quality score for a spec across 4 dimensions: ' +
190
- 'completeness (title, description, AC, scope, estimation), ' +
191
- 'testability (verifiable AC, no vague words), ' +
192
- 'ambiguity (no contradictions, terms defined, edge cases covered), ' +
193
- 'and risk (cross-module deps, architectural scope, NFRs present). ' +
194
- 'Returns a letter grade (A-F) and actionable recommendations.',
195
- inputSchema: {
196
- specId: z.string().min(1).max(500).describe('Spec ID to evaluate (e.g. SPEC-042).'),
197
- projectPath: z
198
- .string()
199
- .min(1)
200
- .max(4096)
201
- .optional()
202
- .describe('Absolute path to the project root (preferred over projectId).'),
203
- projectId: z
204
- .string()
205
- .min(1)
206
- .max(500)
207
- .optional()
208
- .describe('Project hash ID (use projectPath instead when possible).'),
209
- },
210
- annotations: { title: 'Spec Quality Score', readOnlyHint: true },
211
- }, safeTracked('spec_quality_score', (args) => handleSpecQualityScore(args)));
212
- // ── Spec search ──────────────────────────────────────────────────────────────
213
- s.registerTool('search_specs', {
214
- description: 'Search specs with fuzzy text matching, multi-field filters, sorting, and pagination. Supports filtering by status, type, target, tags, difficulty range, risk level, and date ranges.',
215
- annotations: { readOnlyHint: true },
216
- inputSchema: {
217
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
218
- query: z
219
- .string()
220
- .max(200)
221
- .optional()
222
- .describe('Free-text fuzzy search query matched against spec id, title, and tags.'),
223
- status: z
224
- .array(z.enum(STATUS_VALUES))
225
- .optional()
226
- .describe('Filter by status (OR). Valid values: draft, review, approved, implementing, done, discarded.'),
227
- tags: z
228
- .array(z.string().max(100))
229
- .optional()
230
- .describe('Filter by tags (OR — spec must have at least one matching tag).'),
231
- type: z
232
- .array(z.enum(TYPE_VALUES))
233
- .optional()
234
- .describe('Filter by type (OR). Valid values: feature, refactor, bugfix, infra, docs, project, agent, tech-debt.'),
235
- target: z
236
- .array(z.enum(TARGET_VALUES))
237
- .optional()
238
- .describe('Filter by target (OR). Valid values: frontend, backend, shared, fullstack, infrastructure, database, ios, android.'),
239
- difficulty: z
240
- .object({
241
- min: z.number().int().min(1).max(5).optional(),
242
- max: z.number().int().min(1).max(5).optional(),
243
- })
244
- .optional()
245
- .describe('Difficulty range filter (inclusive, 1-5).'),
246
- risk: z
247
- .array(z.enum(RISK_VALUES))
248
- .optional()
249
- .describe('Filter by risk level (OR). Valid values: low, medium, high, critical.'),
250
- createdAfter: z
251
- .string()
252
- .optional()
253
- .describe('ISO date string — only include specs created after this date.'),
254
- createdBefore: z
255
- .string()
256
- .optional()
257
- .describe('ISO date string — only include specs created before this date.'),
258
- updatedAfter: z
259
- .string()
260
- .optional()
261
- .describe('ISO date string — only include specs updated after this date.'),
262
- updatedBefore: z
263
- .string()
264
- .optional()
265
- .describe('ISO date string — only include specs updated before this date.'),
266
- sortBy: z
267
- .enum(SORT_BY_VALUES)
268
- .optional()
269
- .describe('Sort field. Valid values: relevance, createdAt, updatedAt, difficulty.'),
270
- sortOrder: z
271
- .enum(SORT_ORDER_VALUES)
272
- .optional()
273
- .describe('Sort direction. Valid values: asc, desc.'),
274
- limit: z
275
- .number()
276
- .int()
277
- .min(1)
278
- .max(100)
279
- .optional()
280
- .describe('Maximum number of results to return (default: 20, max: 100).'),
281
- offset: z
282
- .number()
283
- .int()
284
- .min(0)
285
- .optional()
286
- .describe('Number of results to skip for pagination (default: 0).'),
287
- },
288
- }, safeTracked('search_specs', async (args) => handleSearchSpecs(args)));
289
- s.registerTool('search_suggestions', {
290
- description: 'Get autocomplete suggestions for a partial query against a specific field (tags, type, status, or spec id). Useful for building dynamic filters.',
291
- annotations: { readOnlyHint: true },
292
- inputSchema: {
293
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
294
- query: z.string().min(1).max(200).describe('Partial string to match against field values.'),
295
- field: z
296
- .enum(SUGGESTION_FIELD_VALUES)
297
- .optional()
298
- .describe('Field to search for suggestions. Valid values: tags, type, status, id. Defaults to tags.'),
299
- },
300
- }, safeTracked('search_suggestions', async (args) => handleSearchSuggestions(args)));
301
- // ── Spec lock ────────────────────────────────────────────────────────────────
302
- s.registerTool('lock_spec', {
303
- description: 'Acquire an exclusive lock on a spec to prevent concurrent edits by other agents or sessions. ' +
304
- 'If the spec is already locked by a different holder, returns an error. ' +
305
- 'Re-locking by the same holder refreshes the TTL.',
306
- inputSchema: {
307
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
308
- specId: z.string().min(1).max(500).describe('Spec ID to lock (e.g. SPEC-042).'),
309
- lockHolder: z
310
- .string()
311
- .min(1)
312
- .max(500)
313
- .describe('Identifier of the agent or session acquiring the lock (e.g. "agent-123").'),
314
- ttlMinutes: z
315
- .number()
316
- .int()
317
- .optional()
318
- .describe('Lock TTL in minutes. Clamped to [1, 480]. Default: 30.'),
319
- reason: z
320
- .string()
321
- .optional()
322
- .describe('Optional human-readable reason for acquiring the lock.'),
323
- },
324
- annotations: { title: 'Lock Spec', destructiveHint: false },
325
- }, safeTracked('lock_spec', (args) => handleLockSpec(args)));
326
- s.registerTool('unlock_spec', {
327
- description: 'Release an existing lock on a spec. ' +
328
- 'Fails if the lock is held by a different holder unless force=true is passed.',
329
- inputSchema: {
330
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
331
- specId: z.string().min(1).max(500).describe('Spec ID to unlock (e.g. SPEC-042).'),
332
- lockHolder: z
333
- .string()
334
- .min(1)
335
- .max(500)
336
- .describe('Identifier of the agent or session releasing the lock.'),
337
- force: z
338
- .boolean()
339
- .optional()
340
- .describe('If true, removes the lock even when held by a different lockHolder. Default: false.'),
341
- },
342
- annotations: { title: 'Unlock Spec', destructiveHint: true },
343
- }, safeTracked('unlock_spec', (args) => handleUnlockSpec(args)));
344
- s.registerTool('list_locks', {
345
- description: 'List all active (non-expired) locks for a project. ' +
346
- 'Returns the spec ID, lock holder, expiration time, and optional reason for each lock.',
347
- inputSchema: {
348
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
349
- },
350
- annotations: { title: 'List Active Locks', readOnlyHint: true },
351
- }, safeTracked('list_locks', (args) => handleListLocks(args)));
352
- s.registerTool('check_spec_lock', {
353
- description: 'Check whether a specific spec is currently locked. ' +
354
- 'Returns locked status, lock holder, expiration time, and reason if locked.',
355
- inputSchema: {
356
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
357
- specId: z.string().min(1).max(500).describe('Spec ID to check (e.g. SPEC-042).'),
358
- },
359
- annotations: { title: 'Check Spec Lock', readOnlyHint: true },
360
- }, safeTracked('check_spec_lock', (args) => handleCheckSpecLock(args)));
361
- // ── Spec hooks ───────────────────────────────────────────────────────────────
362
- s.registerTool('configure_spec_hooks', {
363
- description: 'Configure an event-driven hook for the spec lifecycle. ' +
364
- 'Hooks automatically trigger actions when spec events fire (status change, creation, approval, etc.). ' +
365
- 'Examples: auto-challenge high-risk specs on approve, auto-create git branch on implementing.',
366
- inputSchema: {
367
- projectPath: z
368
- .string()
369
- .min(1)
370
- .max(4096)
371
- .describe('Absolute path to the project root directory'),
372
- hookName: z
373
- .string()
374
- .min(1)
375
- .max(200)
376
- .describe('Unique name for this hook (e.g. "auto-challenge-on-approve")'),
377
- event: z
378
- .enum([
379
- 'on-status-change',
380
- 'on-create',
381
- 'on-approve',
382
- 'on-done',
383
- 'on-drift-alert',
384
- 'on-validation-fail',
385
- ])
386
- .describe(EVENT_DESCRIBE),
387
- action: z
388
- .enum([
389
- 'challenge-spec',
390
- 'check-readiness',
391
- 'notify-slack',
392
- 'create-git-branch',
393
- 'generate-changelog',
394
- 'send-email-digest',
395
- 'run-validate',
396
- ])
397
- .describe(ACTION_DESCRIBE),
398
- filter: z
399
- .object({
400
- tags: z
401
- .array(z.string().max(100))
402
- .optional()
403
- .describe('Only fire for specs with these tags'),
404
- priority: z
405
- .string()
406
- .max(50)
407
- .optional()
408
- .describe('Only fire for specs with this priority'),
409
- fromStatus: z
410
- .string()
411
- .max(50)
412
- .optional()
413
- .describe('Only fire when status changes FROM this value'),
414
- toStatus: z
415
- .string()
416
- .max(50)
417
- .optional()
418
- .describe('Only fire when status changes TO this value'),
419
- })
420
- .optional()
421
- .describe('Optional filter conditions — hook only fires when all conditions match'),
422
- enabled: z.boolean().optional().describe('Whether the hook is active (default: true)'),
423
- },
424
- annotations: { title: 'Configure Spec Hook', readOnlyHint: false },
425
- }, safeLicensed('configure_spec_hooks', async (args) => handleConfigureSpecHooks(args)));
426
- s.registerTool('list_spec_hooks', {
427
- description: 'List all configured spec lifecycle hooks with their run statistics ' +
428
- '(runCount, failCount, lastRunAt) and current enabled/disabled status.',
429
- inputSchema: {
430
- projectPath: z
431
- .string()
432
- .min(1)
433
- .max(4096)
434
- .describe('Absolute path to the project root directory'),
435
- },
436
- annotations: { title: 'List Spec Hooks', readOnlyHint: true },
437
- }, safeLicensed('list_spec_hooks', async (args) => handleListSpecHooks(args)));
438
- s.registerTool('test_spec_hook', {
439
- description: 'Simulate a spec lifecycle event and describe what hooks would fire and what actions ' +
440
- 'would be triggered — without executing any side effects. ' +
441
- 'Use this to verify hook configuration before it fires in production.',
442
- inputSchema: {
443
- projectPath: z
444
- .string()
445
- .min(1)
446
- .max(4096)
447
- .describe('Absolute path to the project root directory'),
448
- hookName: z.string().min(1).max(200).describe('Name of the hook to test'),
449
- specId: z
450
- .string()
451
- .min(1)
452
- .max(200)
453
- .describe('Spec ID to use in the simulation (e.g. SPEC-001)'),
454
- },
455
- annotations: { title: 'Test Spec Hook (Simulation)', readOnlyHint: true },
456
- }, safeLicensed('test_spec_hook', async (args) => handleTestSpecHook(args)));
457
- s.registerTool('disable_spec_hook', {
458
- description: 'Disable a spec lifecycle hook so it no longer fires. The hook config is preserved and can be re-enabled.',
459
- inputSchema: {
460
- projectPath: z
461
- .string()
462
- .min(1)
463
- .max(4096)
464
- .describe('Absolute path to the project root directory'),
465
- hookName: z.string().min(1).max(200).describe('Name of the hook to disable'),
466
- },
467
- annotations: { title: 'Disable Spec Hook', readOnlyHint: false },
468
- }, safeLicensed('disable_spec_hook', async (args) => handleDisableSpecHook(args)));
469
- s.registerTool('enable_spec_hook', {
470
- description: 'Enable a previously disabled spec lifecycle hook.',
471
- inputSchema: {
472
- projectPath: z
473
- .string()
474
- .min(1)
475
- .max(4096)
476
- .describe('Absolute path to the project root directory'),
477
- hookName: z.string().min(1).max(200).describe('Name of the hook to enable'),
478
- },
479
- annotations: { title: 'Enable Spec Hook', readOnlyHint: false },
480
- }, safeLicensed('enable_spec_hook', async (args) => handleEnableSpecHook(args)));
481
- // ── Spec source ──────────────────────────────────────────────────────────────
482
- s.registerTool('sync_spec_from_code', {
483
- description: 'Scan source files for @spec markers and sync them with the spec bindings store. Detects // @spec SPEC-XXX comments in code.',
484
- inputSchema: {
485
- projectPath: z.string().describe('Absolute path to the project root.'),
486
- filePaths: z
487
- .array(z.string())
488
- .describe('List of absolute file paths to scan for @spec markers.'),
489
- },
490
- annotations: { title: 'Sync Spec from Code', destructiveHint: false },
491
- }, safeTracked('sync_spec_from_code', async (args) => handleSyncSpecFromCode(args)));
492
- s.registerTool('mark_as_spec_source', {
493
- description: 'Mark a source file as implementing a specific spec by creating a binding. Also shows the marker comment to add to the file.',
494
- inputSchema: {
495
- projectPath: z.string().describe('Absolute path to the project root.'),
496
- specId: z.string().describe('Spec ID to bind (e.g. SPEC-001).'),
497
- filePath: z.string().describe('Absolute path to the source file implementing the spec.'),
498
- lineNumber: z
499
- .number()
500
- .optional()
501
- .describe('Optional line number where the implementation starts.'),
502
- },
503
- annotations: { title: 'Mark as Spec Source', destructiveHint: false },
504
- }, safeTracked('mark_as_spec_source', async (args) => handleMarkAsSpecSource(args)));
505
- s.registerTool('list_spec_bindings', {
506
- description: 'List all spec-to-code bindings for a project. Optionally filter by spec ID or unmark a binding.',
507
- inputSchema: {
508
- projectPath: z.string().describe('Absolute path to the project root.'),
509
- specId: z
510
- .string()
511
- .optional()
512
- .describe('Filter bindings to a specific spec ID (e.g. SPEC-001).'),
513
- action: z
514
- .enum(['list', 'unmark'])
515
- .optional()
516
- .describe('Action to perform. Values: list (default), unmark.'),
517
- bindingId: z
518
- .string()
519
- .optional()
520
- .describe('Binding ID to remove. Required for unmark action.'),
521
- },
522
- annotations: { title: 'List Spec Bindings', readOnlyHint: true },
523
- }, safeTracked('list_spec_bindings', async (args) => handleListSpecBindings(args)));
524
- // ── SPEC-717: bump_spec_version ──────────────────────────────────────────────
525
- s.registerTool('bump_spec_version', {
526
- description: "Bump the SemVer spec_version field of a spec's frontmatter and record a history entry. " +
527
- 'Use "patch" for typos/formatting, "minor" for new criteria or scope expansion, ' +
528
- '"major" for breaking interpretation changes. ' +
529
- 'Reads and writes spec.md atomically. Does not modify the data store.',
530
- inputSchema: {
531
- projectPath: z
532
- .string()
533
- .min(1)
534
- .max(4096)
535
- .optional()
536
- .describe('Absolute path to the project root. Takes precedence over projectId.'),
537
- projectId: z
538
- .string()
539
- .min(1)
540
- .max(500)
541
- .optional()
542
- .describe('Project ID (hash). Required if projectPath is not provided.'),
543
- specId: z.string().min(1).max(500).describe('Spec ID to bump, e.g. SPEC-042.'),
544
- kind: z
545
- .enum(['patch', 'minor', 'major'])
546
- .describe('Bump kind: patch (typo/format), minor (new criterion), major (breaking change).'),
547
- reason: z
548
- .string()
549
- .min(5)
550
- .max(500)
551
- .describe('Free-text reason for the bump. Recorded in history.'),
552
- by: z
553
- .string()
554
- .min(1)
555
- .max(200)
556
- .optional()
557
- .describe('Actor identifier. Defaults to a session token.'),
558
- },
559
- }, safeTracked('bump_spec_version', async (args) => handleBumpSpecVersion(args)));
560
- // ── Versioning ───────────────────────────────────────────────────────────────
561
- s.registerTool('version_spec', {
562
- description: 'Create a tagged version snapshot of a spec (e.g., v1.0.0, v1.1.0, v2.0.0). ' +
563
- 'Captures the full spec content at a point in time so you can compare or roll back. ' +
564
- 'Uses semantic versioning (vMAJOR.MINOR.PATCH). Tag is auto-incremented if not specified.',
565
- annotations: { title: 'Version Spec' },
566
- inputSchema: {
567
- ...projectIdSchema,
568
- specId: z.string().min(1).max(500).describe('Spec ID to snapshot (e.g., "SPEC-042")'),
569
- tag: z
570
- .string()
571
- .max(50)
572
- .regex(/^v\d+\.\d+\.\d+$/)
573
- .optional()
574
- .describe('Semantic version tag (e.g., "v1.0.0"). Auto-incremented from latest if not provided.'),
575
- message: z
576
- .string()
577
- .max(500)
578
- .optional()
579
- .describe('Human-readable description of what changed in this version'),
580
- author: z.string().max(200).optional().describe('Author identifier for audit trail'),
581
- specPath: z
582
- .string()
583
- .max(4096)
584
- .optional()
585
- .describe('Absolute path to the spec.md file to snapshot its content'),
586
- },
587
- }, safeLicensed('version_spec', withProject((args) => handleVersionSpec(args))));
588
- s.registerTool('branch_spec', {
589
- description: 'Create a spec branch for experimentation — try alternative approaches without affecting the main spec. ' +
590
- 'Useful for "what if we change the auth approach?" scenarios. ' +
591
- 'Branch name must be lowercase kebab-case (e.g., "auth-refactor", "alt-design-v2").',
592
- annotations: { title: 'Branch Spec' },
593
- inputSchema: {
594
- ...projectIdSchema,
595
- specId: z.string().min(1).max(500).describe('Spec ID to branch (e.g., "SPEC-042")'),
596
- branchName: z
597
- .string()
598
- .max(100)
599
- .regex(/^[a-z][a-z0-9-]*$/)
600
- .describe('Branch name in lowercase kebab-case (e.g., "auth-refactor", "v2-approach")'),
601
- fromVersion: z
602
- .string()
603
- .max(50)
604
- .optional()
605
- .describe('Version tag to branch from (e.g., "v1.0.0"). Defaults to the latest version.'),
606
- specPath: z
607
- .string()
608
- .max(4096)
609
- .optional()
610
- .describe('Absolute path to the spec.md file to use as branch starting content'),
611
- },
612
- }, safeLicensed('branch_spec', withProject((args) => handleBranchSpec(args))));
613
- s.registerTool('merge_spec_branch', {
614
- description: 'Merge an experimental spec branch back into the main spec. ' +
615
- 'Detects conflicts and creates a new version snapshot. ' +
616
- 'The branch is deleted after a successful merge.',
617
- annotations: { title: 'Merge Spec Branch' },
618
- inputSchema: {
619
- ...projectIdSchema,
620
- specId: z.string().min(1).max(500).describe('Spec ID to merge into (e.g., "SPEC-042")'),
621
- branchName: z.string().max(100).describe('Name of the branch to merge (must exist)'),
622
- resolvedContent: z
623
- .string()
624
- .max(100_000)
625
- .optional()
626
- .describe('Pre-resolved spec content to use as the merge result.'),
627
- newVersionTag: z
628
- .string()
629
- .max(50)
630
- .regex(/^v\d+\.\d+\.\d+$/)
631
- .optional()
632
- .describe('Version tag for the merged snapshot. Auto-incremented if not provided.'),
633
- message: z
634
- .string()
635
- .max(500)
636
- .optional()
637
- .describe('Merge commit message describing what was merged'),
638
- },
639
- }, safeLicensed('merge_spec_branch', withProject((args) => handleMergeSpecBranch(args))));
640
- s.registerTool('diff_spec_versions', {
641
- description: 'Compare two tagged version snapshots of a spec to see exactly what changed. ' +
642
- 'Returns a +/- line diff plus AC-level summary (N criteria added, M removed, K modified).',
643
- annotations: { title: 'Diff Spec Versions', readOnlyHint: true },
644
- inputSchema: {
645
- ...projectIdSchema,
646
- specId: z.string().min(1).max(500).describe('Spec ID to diff (e.g., "SPEC-042")'),
647
- fromVersion: z
648
- .string()
649
- .max(50)
650
- .optional()
651
- .describe('Starting version tag (e.g., "v1.0.0"). Omit to auto-select the penultimate version.'),
652
- toVersion: z
653
- .string()
654
- .max(50)
655
- .optional()
656
- .describe('Ending version tag (e.g., "v2.0.0"). Defaults to the latest version if not specified.'),
657
- format: z
658
- .enum(['text', 'json'])
659
- .optional()
660
- .describe('Output format. Valid values: "text" (default, +/- line diff) | "json" (structured object).'),
661
- },
662
- }, safeLicensed('diff_spec_versions', withProject((args) => handleDiffSpecVersions(args))));
663
- s.registerTool('spec_version_history', {
664
- description: 'List all tagged version snapshots of a spec with their timestamps, authors, and auto-generated change summaries. Returns versions in reverse chronological order (newest first).',
665
- annotations: { title: 'Spec Version History', readOnlyHint: true },
666
- inputSchema: {
667
- ...projectIdSchema,
668
- specId: z
669
- .string()
670
- .min(1)
671
- .max(500)
672
- .describe('Spec ID to retrieve version history for (e.g., "SPEC-042")'),
673
- limit: z
674
- .number()
675
- .int()
676
- .min(1)
677
- .max(100)
678
- .optional()
679
- .describe('Maximum number of versions to return (default: 20, max: 100)'),
680
- },
681
- }, safeLicensed('spec_version_history', withProject((args) => handleSpecVersionHistory(args))));
682
- // ── Diff spec (generate from git diff) ──────────────────────────────────────
683
- s.registerTool('generate_spec_from_diff', {
684
- description: 'Generate a spec draft from a git diff string. Analyzes modified files, added functions, and new endpoints to infer acceptance criteria.',
685
- inputSchema: {
686
- projectPath: z.string().describe('Absolute path to the project root.'),
687
- diff: z
688
- .string()
689
- .min(1)
690
- .describe('Raw git diff output (e.g. output of "git diff HEAD~1..HEAD").'),
691
- outputMode: z
692
- .enum(['draft', 'create'])
693
- .optional()
694
- .describe('Output mode. Values: draft (return content only), create (suggest create_spec call). Default: draft.'),
695
- title: z
696
- .string()
697
- .optional()
698
- .describe('Optional title override. If omitted, title is inferred from the diff.'),
699
- },
700
- annotations: { title: 'Generate Spec from Diff', readOnlyHint: true },
701
- }, safeTracked('generate_spec_from_diff', async (args) => handleGenerateSpecFromDiff(args)));
702
- s.registerTool('generate_spec_from_pr', {
703
- description: 'Generate a spec draft from a GitHub Pull Request by fetching its diff via the GitHub CLI (gh). Requires gh to be installed and authenticated.',
704
- inputSchema: {
705
- projectPath: z.string().describe('Absolute path to the project root.'),
706
- prNumber: z.number().int().positive().describe('Pull Request number (e.g. 42).'),
707
- repoPath: z
708
- .string()
709
- .optional()
710
- .describe('Optional path to the git repository. Defaults to projectPath.'),
711
- outputMode: z
712
- .enum(['draft', 'create'])
713
- .optional()
714
- .describe('Output mode. Values: draft, create. Default: draft.'),
715
- },
716
- annotations: { title: 'Generate Spec from PR', readOnlyHint: true },
717
- }, safeTracked('generate_spec_from_pr', async (args) => handleGenerateSpecFromPr(args)));
718
- s.registerTool('generate_spec_from_commit_range', {
719
- description: 'Generate a spec draft from a git commit range (e.g. HEAD~5..HEAD or abc123..def456).',
720
- inputSchema: {
721
- projectPath: z.string().describe('Absolute path to the project root.'),
722
- commitRange: z
723
- .string()
724
- .describe('Git commit range in the format from..to (e.g. HEAD~3..HEAD or sha1..sha2).'),
725
- outputMode: z
726
- .enum(['draft', 'create'])
727
- .optional()
728
- .describe('Output mode. Values: draft, create. Default: draft.'),
729
- },
730
- annotations: { title: 'Generate Spec from Commit Range', readOnlyHint: true },
731
- }, safeTracked('generate_spec_from_commit_range', async (args) => handleGenerateSpecFromCommitRange(args)));
732
- // ── Visual spec diff ─────────────────────────────────────────────────────────
733
- s.registerTool('visual_spec_diff', {
734
- description: 'Compute a visual line-by-line diff between two spec texts using LCS algorithm. Returns HTML output (default) or text format with +/- markers.',
735
- inputSchema: {
736
- textA: z.string().max(500000).describe('First spec text (base/original)'),
737
- textB: z.string().max(500000).describe('Second spec text (new/modified)'),
738
- labelA: z.string().max(100).optional().describe('Label for textA (default: A)'),
739
- labelB: z.string().max(100).optional().describe('Label for textB (default: B)'),
740
- outputFormat: z
741
- .enum(['html', 'text'])
742
- .optional()
743
- .describe('Output format: html (default) or text'),
744
- },
745
- annotations: { readOnlyHint: true },
746
- }, safeLicensed('visual_spec_diff', (args) => handleVisualSpecDiff(args)));
747
- s.registerTool('three_way_merge', {
748
- description: 'Perform a 3-way merge of two spec texts against a common base. Returns merged text with conflict markers where conflicts exist.',
749
- inputSchema: {
750
- base: z.string().max(500000).describe('Common base text'),
751
- textA: z.string().max(500000).describe('First branch text'),
752
- textB: z.string().max(500000).describe('Second branch text'),
753
- labelA: z.string().max(100).optional().describe('Label for textA (default: A)'),
754
- labelB: z.string().max(100).optional().describe('Label for textB (default: B)'),
755
- },
756
- annotations: { readOnlyHint: true },
757
- }, safeLicensed('three_way_merge', (args) => handleThreeWayMerge(args)));
758
- s.registerTool('smart_merge_conflict_resolver', {
759
- description: 'Automatically resolve simple merge conflicts: identical content on both sides, or whitespace-only differences. Remaining conflicts kept as-is for manual resolution.',
760
- inputSchema: {
761
- mergedText: z
762
- .string()
763
- .max(500000)
764
- .describe('Text containing conflict markers from three_way_merge'),
765
- },
766
- annotations: { readOnlyHint: true },
767
- }, safeLicensed('smart_merge_conflict_resolver', (args) => handleSmartMergeConflictResolver(args)));
768
- s.registerTool('merge_preview', {
769
- description: 'Preview the result of merging two spec texts against a base. Shows whether auto-merge is possible and the merged content.',
770
- inputSchema: {
771
- base: z.string().max(500000).describe('Common base text'),
772
- textA: z.string().max(500000).describe('First branch text'),
773
- textB: z.string().max(500000).describe('Second branch text'),
774
- },
775
- annotations: { readOnlyHint: true },
776
- }, safeLicensed('merge_preview', (args) => handleMergePreview(args)));
777
- s.registerTool('apply_merge', {
778
- description: 'Apply merged spec content to a spec file on disk. Fails if unresolved conflict markers are present.',
779
- inputSchema: {
780
- mergedText: z
781
- .string()
782
- .max(500000)
783
- .describe('Fully resolved merged text (no conflict markers)'),
784
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root'),
785
- specId: z.string().min(1).max(100).describe('Spec ID to update (e.g. SPEC-001)'),
786
- },
787
- annotations: { readOnlyHint: false, destructiveHint: true },
788
- }, safeLicensed('apply_merge', async (args) => handleApplyMerge(args)));
789
- // ── Merge risk ───────────────────────────────────────────────────────────────
790
- registerMergeRiskTools(s);
791
- // ── Domain criteria ──────────────────────────────────────────────────────────
792
- s.registerTool('suggest_criteria', {
793
- description: 'Analyze a spec and suggest missing best-practice acceptance criteria based on the detected domain ' +
794
- '(payments, authentication, file-upload, gdpr, etc.). ' +
795
- 'Returns suggested criteria grouped by domain and flags critical gaps that penalize readiness score.',
796
- annotations: { readOnlyHint: true },
797
- inputSchema: {
798
- ...projectIdSchema,
799
- specId: z.string().min(1).max(500).describe('Spec ID to analyze (e.g. SPEC-042)'),
800
- projectHash: z
801
- .string()
802
- .max(500)
803
- .optional()
804
- .describe('Optional project hash for loading project-level best-practices overrides.'),
805
- },
806
- }, safeLicensed('suggest_criteria', withProject((args) => handleSuggestCriteria(args))));
807
- s.registerTool('inject_criteria', {
808
- description: 'Inject missing domain best-practice criteria directly into a spec\'s "## Acceptance Criteria" section. ' +
809
- 'Criteria are automatically detected from the spec domain. ' +
810
- 'Use criteriaIds to inject only specific criteria; omit to inject all missing critical criteria.',
811
- annotations: { destructiveHint: true },
812
- inputSchema: {
813
- ...projectIdSchema,
814
- specId: z
815
- .string()
816
- .min(1)
817
- .max(500)
818
- .describe('Spec ID to inject criteria into (e.g. SPEC-042)'),
819
- criteriaIds: z
820
- .array(z.string().max(200))
821
- .optional()
822
- .describe('Optional list of specific criteria IDs to inject.'),
823
- projectHash: z
824
- .string()
825
- .max(500)
826
- .optional()
827
- .describe('Optional project hash for loading project-level best-practices overrides'),
828
- },
829
- }, safeLicensed('inject_criteria', withProject((args) => handleInjectCriteria(args))));
830
- // ── Domain bundle ────────────────────────────────────────────────────────────
831
- s.registerTool('apply_domain_bundle', {
832
- description: 'Install a pre-configured domain bundle (skills + rules + spec templates) into a project ' +
833
- 'with a single command. Available bundles: stripe-payments, auth-supabase, rest-api, ' +
834
- 'nextjs-fullstack, react-native.',
835
- inputSchema: ApplyDomainBundleInputSchema,
836
- annotations: {
837
- title: 'Apply Domain Bundle',
838
- readOnlyHint: false,
839
- destructiveHint: false,
840
- openWorldHint: false,
841
- },
842
- }, safeLicensed('apply_domain_bundle', async (args) => handleApplyDomainBundle(args)));
843
- s.registerTool('list_domain_bundles', {
844
- description: 'List all available pre-configured domain bundles with their id, name, description, and tags.',
845
- inputSchema: ListDomainBundlesInputSchema,
846
- annotations: {
847
- title: 'List Domain Bundles',
848
- readOnlyHint: true,
849
- destructiveHint: false,
850
- openWorldHint: false,
851
- },
852
- }, safeLicensed('list_domain_bundles', async (args) => handleListDomainBundles(args)));
853
- // ── SPEC-601: Portable spec bundles (UUID-based) ──────────────────────────
854
- s.registerTool('export_spec_bundle', {
855
- description: 'Export a spec as a portable JSON bundle (with UUID) for importing into another project. Use for cross-project spec sharing.',
856
- annotations: { readOnlyHint: true },
857
- inputSchema: {
858
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
859
- specId: z.string().min(1).max(500).describe('Spec ID to export (e.g. SPEC-042).'),
860
- },
861
- }, safeLicensed('export_spec_bundle', async (args) => handleExportBundle(args)));
862
- s.registerTool('import_spec_bundle', {
863
- description: 'Import a spec bundle from another project. Allocates a new SPEC-NNN locally while preserving the original UUID for collision detection.',
864
- inputSchema: {
865
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
866
- bundlePath: z
867
- .string()
868
- .min(1)
869
- .max(4096)
870
- .describe('Path to the .bundle.json file exported by export_spec_bundle.'),
871
- },
872
- }, safeLicensed('import_spec_bundle', async (args) => handleImportBundle(args)));
873
- // ── SPEC-613: Gherkin BDD import/export ───────────────────────────────────
874
- s.registerTool('import_gherkin', {
875
- description: 'Import Gherkin .feature file(s) and append scenarios as BDD acceptance criteria to an existing spec. Supports Scenario Outline + Examples (each row becomes one criterion). Provide exactly one of: featureFilePath, gherkinText, or featureDir.',
876
- inputSchema: {
877
- ...projectIdSchema,
878
- specId: z
879
- .string()
880
- .min(1)
881
- .max(500)
882
- .describe('Spec ID to append criteria to (e.g. SPEC-042).'),
883
- featureFilePath: z
884
- .string()
885
- .min(1)
886
- .max(4096)
887
- .optional()
888
- .describe('Absolute path to a single .feature file to import.'),
889
- gherkinText: z
890
- .string()
891
- .min(1)
892
- .optional()
893
- .describe('Raw Gherkin text content to parse (alternative to featureFilePath).'),
894
- featureDir: z
895
- .string()
896
- .min(1)
897
- .max(4096)
898
- .optional()
899
- .describe('Absolute path to a directory containing .feature files (all will be imported).'),
900
- },
901
- annotations: { title: 'Import Gherkin Scenarios' },
902
- }, safeLicensed('import_gherkin', withProject(async (args) => handleImportGherkin(args))));
903
- s.registerTool('export_gherkin', {
904
- description: 'Export the BDD acceptance criteria of a spec as a Cucumber-compatible .feature file. Only criteria following Given/When/Then patterns are emitted.',
905
- annotations: { title: 'Export Spec as Gherkin', readOnlyHint: true },
906
- inputSchema: {
907
- ...projectIdSchema,
908
- specId: z.string().min(1).max(500).describe('Spec ID to export (e.g. SPEC-042).'),
909
- outputPath: z
910
- .string()
911
- .min(1)
912
- .max(4096)
913
- .describe('Absolute path where the .feature file will be written.'),
914
- },
915
- }, safeLicensed('export_gherkin', withProject(async (args) => handleExportGherkin(args))));
916
- }
917
- //# sourceMappingURL=group-spec-ops.js.map