@planu/cli 4.1.0 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/config/license-plans.json +65 -361
- package/dist/engine/hooks/git-hook-generator.js +31 -0
- package/dist/tools/git/hook-ops.js +53 -9
- package/dist/tools/tool-registry/group-infra.js +22 -0
- package/package.json +8 -7
- package/dist/engine/escalator/index.d.ts +0 -5
- package/dist/engine/escalator/index.js +0 -5
- package/dist/engine/freeze/retro-audit.d.ts +0 -6
- package/dist/engine/freeze/retro-audit.js +0 -24
- package/dist/engine/heal/backup.d.ts +0 -9
- package/dist/engine/heal/backup.js +0 -21
- package/dist/engine/idioma-validator/index.d.ts +0 -17
- package/dist/engine/idioma-validator/index.js +0 -89
- package/dist/engine/saga/index.d.ts +0 -4
- package/dist/engine/saga/index.js +0 -4
- package/dist/engine/spec-state-machine/index.d.ts +0 -3
- package/dist/engine/spec-state-machine/index.js +0 -2
- package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +0 -6
- package/dist/engine/spec-summary-html/dashboard-renderer.js +0 -333
- package/dist/engine/triagier/index.d.ts +0 -5
- package/dist/engine/triagier/index.js +0 -5
- package/dist/engine/universal-rules/index.d.ts +0 -5
- package/dist/engine/universal-rules/index.js +0 -6
- package/dist/testing/cassette/index.d.ts +0 -23
- package/dist/testing/cassette/index.js +0 -26
- package/dist/tools/domain-bundle-handler.d.ts +0 -37
- package/dist/tools/domain-bundle-handler.js +0 -71
- package/dist/tools/figma/rules-file.d.ts +0 -5
- package/dist/tools/figma/rules-file.js +0 -45
- package/dist/tools/heal-planu-root.d.ts +0 -8
- package/dist/tools/heal-planu-root.js +0 -144
- package/dist/tools/opencode-host-adapter.d.ts +0 -3
- package/dist/tools/opencode-host-adapter.js +0 -33
- package/dist/tools/plan-team-distribution.d.ts +0 -3
- package/dist/tools/plan-team-distribution.js +0 -71
- package/dist/tools/reconcile-status-json.d.ts +0 -4
- package/dist/tools/reconcile-status-json.js +0 -209
- package/dist/tools/register-all-tools.d.ts +0 -8
- package/dist/tools/register-all-tools.js +0 -239
- package/dist/tools/tool-registry/group-analysis-monitoring.d.ts +0 -3
- package/dist/tools/tool-registry/group-analysis-monitoring.js +0 -942
- package/dist/tools/tool-registry/group-integrations.d.ts +0 -3
- package/dist/tools/tool-registry/group-integrations.js +0 -1046
- package/dist/tools/tool-registry/group-misc.d.ts +0 -3
- package/dist/tools/tool-registry/group-misc.js +0 -1367
- package/dist/tools/tool-registry/group-platform.d.ts +0 -3
- package/dist/tools/tool-registry/group-platform.js +0 -1681
- package/dist/tools/tool-registry/group-session-knowledge.d.ts +0 -3
- package/dist/tools/tool-registry/group-session-knowledge.js +0 -1416
- package/dist/tools/tool-registry/group-spec-ops.d.ts +0 -3
- package/dist/tools/tool-registry/group-spec-ops.js +0 -917
- package/dist/tools/workspace-overview.d.ts +0 -4
- package/dist/tools/workspace-overview.js +0 -316
- package/dist/transports/middleware/index.d.ts +0 -9
- package/dist/transports/middleware/index.js +0 -7
- package/dist/transports/middleware/with-sandbox.d.ts +0 -21
- package/dist/transports/middleware/with-sandbox.js +0 -68
- package/dist/types/heal.d.ts +0 -18
- package/dist/types/heal.js +0 -3
|
@@ -1,1681 +0,0 @@
|
|
|
1
|
-
// SPEC-597-EXEMPT: canonical mapping (npm-package↔framework, keyword catalogue)
|
|
2
|
-
// — must enumerate framework names by design; no detection logic to migrate.
|
|
3
|
-
/* eslint-disable max-lines -- thematic registry group consolidated by SPEC refactor-phase3 (commit aafeea60); each block is a declarative tool registration ~20 lines, split would re-create the ~120 register-*.ts files this phase intentionally collapsed */
|
|
4
|
-
// tools/tool-registry/group-platform.ts — Group F: Platform tools (inline registry)
|
|
5
|
-
//
|
|
6
|
-
// Phase 3 (refactor/registry-phase3): inlines register-*.ts files for:
|
|
7
|
-
// migration, apply-migration-cleanup, stack, template, search, import,
|
|
8
|
-
// hooks, filesystem-hooks, dep-audit, changelog, event, feedback, comments, memory,
|
|
9
|
-
// infrastructure, global-rules, plan-mode, quality-gates, verifier (empty shim), context-profile
|
|
10
|
-
import { z } from 'zod';
|
|
11
|
-
import { safeLicensed, safeTracked } from '../safe-handler.js';
|
|
12
|
-
import { projectIdSchema, withProject } from '../tool-registry-helpers.js';
|
|
13
|
-
import { makeDeprecationStub } from './deprecated-stubs.js';
|
|
14
|
-
import { registerFromEntries } from '../tool-entry.js';
|
|
15
|
-
// ── SPEC-966: OpenCode Host Adapter ────────────────────────────────────────────
|
|
16
|
-
import { handleOpenCodeHostAdapter } from '../opencode-host-adapter.js';
|
|
17
|
-
// ── Migration (register-migration-tools.ts) ───────────────────────────────────
|
|
18
|
-
import { t } from '../../i18n/index.js';
|
|
19
|
-
import { handleMigrateTech } from '../migrate-tech.js';
|
|
20
|
-
// ── Apply Migration Cleanup (register-apply-migration-cleanup.ts) ─────────────
|
|
21
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
import { checkCriteriaQuality } from '../../engine/criteria-quality-checker.js';
|
|
24
|
-
// ── Stack (register-stack-tools.ts) ──────────────────────────────────────────
|
|
25
|
-
import { handleAuditStack } from '../audit-stack.js';
|
|
26
|
-
import { handlePlanUpgrade } from '../plan-upgrade.js';
|
|
27
|
-
import { handleDetectDeprecations } from '../detect-deprecations.js';
|
|
28
|
-
// ── Template (register-template-tools.ts) ────────────────────────────────────
|
|
29
|
-
import { handleListTemplates, handleApplyTemplate } from '../spec-templates.js';
|
|
30
|
-
// ── Search (register-search-tools.ts) ────────────────────────────────────────
|
|
31
|
-
import { handleSemanticSearch } from '../semantic-search-handler.js';
|
|
32
|
-
// ── Import (register-import-tools.ts) ────────────────────────────────────────
|
|
33
|
-
import { handleImportSpec, handleBulkImport } from '../import-spec-handler.js';
|
|
34
|
-
// ── Hooks (register-hooks-tools.ts) ──────────────────────────────────────────
|
|
35
|
-
import { handleManageHooks } from '../manage-hooks.js';
|
|
36
|
-
import { handleStartHooks, handleStopHooks, handleHookStatus, handleConfigureHooks, } from '../start-hooks.js';
|
|
37
|
-
// ── Filesystem Hooks (register-filesystem-hooks-tools.ts) ────────────────────
|
|
38
|
-
import { handleConfigureFilesystemHooks } from '../filesystem-hooks-handler.js';
|
|
39
|
-
// ── Dep Audit (register-dep-audit-tools.ts) ───────────────────────────────────
|
|
40
|
-
import { auditDeps } from '../../engine/dep-auditor/index.js';
|
|
41
|
-
import { handleDependencyHealth } from '../dependency-health.js';
|
|
42
|
-
// ── CVE Refresher (SPEC-773) ─────────────────────────────────────────────────
|
|
43
|
-
import { isCveDatabaseStale, refreshCveDatabase } from '../../engine/security/cve-refresher.js';
|
|
44
|
-
// ── Changelog (register-changelog-tools.ts) ───────────────────────────────────
|
|
45
|
-
import { handleSpecHistory } from '../spec-history.js';
|
|
46
|
-
// ── Event (register-event-tools.ts) ───────────────────────────────────────────
|
|
47
|
-
import { handleEventContracts } from '../event-contracts.js';
|
|
48
|
-
// ── Feedback (register-feedback-tools.ts) ────────────────────────────────────
|
|
49
|
-
import { handleSubmitFeedback, handleTriageFeedback, handleResolveFeedback, } from '../feedback-handler.js';
|
|
50
|
-
// ── Comments (register-comments-tools.ts) ────────────────────────────────────
|
|
51
|
-
import { handleAddComment, handleListComments, handleResolveComment, handleReplyComment, } from '../comments-handler.js';
|
|
52
|
-
// ── Memory (register-memory-tools.ts) ────────────────────────────────────────
|
|
53
|
-
import { handleLogDecision } from '../log-decision.js';
|
|
54
|
-
import { handleRealityCheck } from '../reality-check.js';
|
|
55
|
-
// ── Infrastructure (register-infrastructure-tools.ts) ────────────────────────
|
|
56
|
-
import { handleGenerateInfrastructure } from '../generate-infrastructure.js';
|
|
57
|
-
// ── Global Rules (register-global-rules-tools.ts) ────────────────────────────
|
|
58
|
-
import { handleManageGlobalRules, handleGlobalRulesStatus, } from '../global-rules-handler.js';
|
|
59
|
-
// ── Plan Mode (register-plan-mode-tools.ts) ───────────────────────────────────
|
|
60
|
-
import { handlePlanMode } from '../plan-mode-handler.js';
|
|
61
|
-
// ── Quality Gates (register-quality-gates.ts) ────────────────────────────────
|
|
62
|
-
import { handleInjectQualityGates } from '../inject-quality-gates-handler.js';
|
|
63
|
-
// ── Context Profile (register-context-profile.ts) ────────────────────────────
|
|
64
|
-
import { setActiveProfile, getActiveProfile } from '../../storage/context-profile-store.js';
|
|
65
|
-
import { getProfile, getAllProfiles } from '../../engine/context-profile/profile-catalog.js';
|
|
66
|
-
// ── Dep Audit — report formatting helpers ─────────────────────────────────────
|
|
67
|
-
function vulnLine(vuln) {
|
|
68
|
-
const fix = vuln.fixedIn ? ` — fixed in ${vuln.fixedIn}` : ' — no fix available';
|
|
69
|
-
return ` - [${vuln.severity.toUpperCase()}] ${vuln.cveId}: ${vuln.description}${fix}`;
|
|
70
|
-
}
|
|
71
|
-
function entryLines(entry) {
|
|
72
|
-
const lines = [`- **${entry.name}** @ ${entry.currentVersion}`];
|
|
73
|
-
for (const v of entry.vulns) {
|
|
74
|
-
lines.push(vulnLine(v));
|
|
75
|
-
}
|
|
76
|
-
if (entry.license.compatibility === 'critical') {
|
|
77
|
-
lines.push(` - [LICENSE] ${entry.license.reason}`);
|
|
78
|
-
}
|
|
79
|
-
if (entry.abandoned.isAbandoned) {
|
|
80
|
-
lines.push(` - [ABANDONED] ${entry.abandoned.reason}`);
|
|
81
|
-
if (entry.abandoned.suggestedAlternative) {
|
|
82
|
-
lines.push(` Alternative: ${entry.abandoned.suggestedAlternative}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return lines;
|
|
86
|
-
}
|
|
87
|
-
function criticalVulnCves(report) {
|
|
88
|
-
const cves = [];
|
|
89
|
-
for (const entry of report.critical) {
|
|
90
|
-
for (const v of entry.vulns) {
|
|
91
|
-
if (v.severity === 'critical') {
|
|
92
|
-
cves.push(`${v.cveId} (${entry.name})`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return cves;
|
|
97
|
-
}
|
|
98
|
-
function buildFullReport(report) {
|
|
99
|
-
const lines = [
|
|
100
|
-
'## Security Scan Report',
|
|
101
|
-
'',
|
|
102
|
-
`**Ecosystem**: ${report.ecosystem}`,
|
|
103
|
-
`**Dependencies scanned**: ${report.totalDeps}`,
|
|
104
|
-
`**Summary**: ${report.summary}`,
|
|
105
|
-
'',
|
|
106
|
-
];
|
|
107
|
-
if (report.critical.length > 0) {
|
|
108
|
-
lines.push('### Critical Issues', '');
|
|
109
|
-
for (const entry of report.critical) {
|
|
110
|
-
lines.push(...entryLines(entry));
|
|
111
|
-
}
|
|
112
|
-
lines.push('');
|
|
113
|
-
}
|
|
114
|
-
if (report.warnings.length > 0) {
|
|
115
|
-
lines.push('### Warnings (high/medium severity)', '');
|
|
116
|
-
for (const entry of report.warnings) {
|
|
117
|
-
lines.push(...entryLines(entry));
|
|
118
|
-
}
|
|
119
|
-
lines.push('');
|
|
120
|
-
}
|
|
121
|
-
if (report.duplicates.length > 0) {
|
|
122
|
-
lines.push('### Duplicate Dependency Groups', '');
|
|
123
|
-
for (const dup of report.duplicates) {
|
|
124
|
-
lines.push(`- [${dup.category}] ${dup.packages.join(', ')} — ${dup.recommendation}`);
|
|
125
|
-
}
|
|
126
|
-
lines.push('');
|
|
127
|
-
}
|
|
128
|
-
if (report.critical.length === 0 && report.warnings.length === 0) {
|
|
129
|
-
lines.push('No vulnerabilities or license conflicts detected.', '');
|
|
130
|
-
}
|
|
131
|
-
return lines.join('\n');
|
|
132
|
-
}
|
|
133
|
-
function buildFreeReport(report) {
|
|
134
|
-
const criticalEntries = report.critical.filter((e) => e.vulns.some((v) => v.severity === 'critical'));
|
|
135
|
-
const lines = [
|
|
136
|
-
'## Security Scan Report (free tier — npm critical CVEs only)',
|
|
137
|
-
'',
|
|
138
|
-
`**Ecosystem**: ${report.ecosystem}`,
|
|
139
|
-
`**Dependencies scanned**: ${report.totalDeps}`,
|
|
140
|
-
'',
|
|
141
|
-
];
|
|
142
|
-
if (criticalEntries.length === 0) {
|
|
143
|
-
lines.push('No critical CVEs detected in npm dependencies.');
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
lines.push('### Critical CVEs Detected', '');
|
|
147
|
-
for (const entry of criticalEntries) {
|
|
148
|
-
for (const v of entry.vulns) {
|
|
149
|
-
if (v.severity === 'critical') {
|
|
150
|
-
lines.push(vulnLine(v).replace(/^ {2}/, `- ${entry.name}@${entry.currentVersion} `));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
lines.push('');
|
|
155
|
-
lines.push('> Upgrade to Pro to see high/medium vulnerabilities, license conflicts, and abandoned packages.');
|
|
156
|
-
}
|
|
157
|
-
return lines.join('\n');
|
|
158
|
-
}
|
|
159
|
-
// ── Apply Migration Cleanup — helpers ─────────────────────────────────────────
|
|
160
|
-
async function findSpecPath(projectPath, specId) {
|
|
161
|
-
const { readdir, stat } = await import('node:fs/promises');
|
|
162
|
-
const specsDir = join(projectPath, 'planu/specs');
|
|
163
|
-
let entries;
|
|
164
|
-
try {
|
|
165
|
-
entries = await readdir(specsDir);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
for (const entry of entries) {
|
|
171
|
-
if (!entry.startsWith(specId)) {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const specDir = join(specsDir, entry);
|
|
175
|
-
try {
|
|
176
|
-
const info = await stat(specDir);
|
|
177
|
-
if (info.isDirectory()) {
|
|
178
|
-
return join(specDir, 'spec.md');
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// skip
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
function replaceCriterionInFrontmatter(content, escapedOld, escapedNew) {
|
|
188
|
-
if (!content.startsWith('---\n')) {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
const closingIdx = content.indexOf('\n---', 4);
|
|
192
|
-
if (closingIdx < 0) {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
const frontmatter = content.slice(4, closingIdx);
|
|
196
|
-
const afterFm = content.slice(closingIdx);
|
|
197
|
-
const target = ` - text: "${escapedOld}"`;
|
|
198
|
-
if (!frontmatter.includes(target)) {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
const updatedFm = frontmatter.replace(target, ` - text: "${escapedNew}"`);
|
|
202
|
-
return `---\n${updatedFm}${afterFm}`;
|
|
203
|
-
}
|
|
204
|
-
// ── Context Profile ───────────────────────────────────────────────────────────
|
|
205
|
-
const AVAILABLE_PHASES = ['brainstorm', 'plan', 'implement', 'review', 'release'];
|
|
206
|
-
const projectPathField = z
|
|
207
|
-
.string()
|
|
208
|
-
.min(1)
|
|
209
|
-
.max(4096)
|
|
210
|
-
.describe('Absolute path to the project root directory.');
|
|
211
|
-
const phaseField = z
|
|
212
|
-
.enum(['brainstorm', 'plan', 'implement', 'review', 'release'])
|
|
213
|
-
.describe('Work phase to activate: ' +
|
|
214
|
-
'brainstorm=explore ideas, ' +
|
|
215
|
-
'plan=design specs, ' +
|
|
216
|
-
'implement=build features, ' +
|
|
217
|
-
'review=validate quality, ' +
|
|
218
|
-
'release=publish');
|
|
219
|
-
// ── Migration enum ────────────────────────────────────────────────────────────
|
|
220
|
-
const MigrateTechActionEnum = z.enum([
|
|
221
|
-
'analyze',
|
|
222
|
-
'map',
|
|
223
|
-
'plan',
|
|
224
|
-
'strategy',
|
|
225
|
-
'validate',
|
|
226
|
-
'analyze-db',
|
|
227
|
-
'map-db',
|
|
228
|
-
'plan-db',
|
|
229
|
-
'analyze-services',
|
|
230
|
-
'map-services',
|
|
231
|
-
'plan-services',
|
|
232
|
-
'validate-service',
|
|
233
|
-
'rollback-plan',
|
|
234
|
-
'checkpoint',
|
|
235
|
-
'rollback',
|
|
236
|
-
'feature-flags',
|
|
237
|
-
'status',
|
|
238
|
-
'webpack-to-vite',
|
|
239
|
-
'vite6-to-vite7',
|
|
240
|
-
'detect-pending',
|
|
241
|
-
]);
|
|
242
|
-
// ── Template constants ────────────────────────────────────────────────────────
|
|
243
|
-
const TEMPLATE_CATEGORIES = [
|
|
244
|
-
'auth',
|
|
245
|
-
'crud',
|
|
246
|
-
'api',
|
|
247
|
-
'ui',
|
|
248
|
-
'infra',
|
|
249
|
-
'testing',
|
|
250
|
-
'integration',
|
|
251
|
-
'data',
|
|
252
|
-
'security',
|
|
253
|
-
'performance',
|
|
254
|
-
'industry',
|
|
255
|
-
];
|
|
256
|
-
const COMPLEXITY_SCORES = ['S', 'M', 'L', 'XL'];
|
|
257
|
-
// eslint-disable-next-line max-lines-per-function -- registration catalog: one block per tool. ~40 tools from 20 register-*.ts files.
|
|
258
|
-
export function registerPlatformGroupTools(s) {
|
|
259
|
-
// ── Migration ───────────────────────────────────────────────────────────────
|
|
260
|
-
s.registerTool('migrate_tech', {
|
|
261
|
-
description: t('tools.migrate_tech.description'),
|
|
262
|
-
annotations: { readOnlyHint: false },
|
|
263
|
-
inputSchema: {
|
|
264
|
-
action: MigrateTechActionEnum.describe('Migration action: analyze | map | plan | strategy | validate | analyze-db | map-db | plan-db | analyze-services | map-services | plan-services | validate-service | rollback-plan | checkpoint | rollback | feature-flags | status | webpack-to-vite | vite6-to-vite7 | detect-pending'),
|
|
265
|
-
...projectIdSchema,
|
|
266
|
-
sourceStack: z
|
|
267
|
-
.string()
|
|
268
|
-
.max(500)
|
|
269
|
-
.optional()
|
|
270
|
-
.describe('Source technology stack (e.g. "java-spring", "php-laravel", "express") — required for map/plan/strategy'),
|
|
271
|
-
targetStack: z
|
|
272
|
-
.string()
|
|
273
|
-
.max(500)
|
|
274
|
-
.optional()
|
|
275
|
-
.describe('Target technology stack (e.g. "go", "node-typescript", "fastify") — required for map/plan/strategy'),
|
|
276
|
-
component: z
|
|
277
|
-
.string()
|
|
278
|
-
.max(500)
|
|
279
|
-
.optional()
|
|
280
|
-
.describe('Component name to validate (required for validate action)'),
|
|
281
|
-
sourceDbEngine: z
|
|
282
|
-
.string()
|
|
283
|
-
.max(500)
|
|
284
|
-
.optional()
|
|
285
|
-
.describe('Source database engine (e.g. "postgresql", "mysql", "mongodb") — required for analyze-db/map-db/plan-db'),
|
|
286
|
-
targetDbEngine: z
|
|
287
|
-
.string()
|
|
288
|
-
.max(500)
|
|
289
|
-
.optional()
|
|
290
|
-
.describe('Target database engine — required for map-db/plan-db'),
|
|
291
|
-
existingAnalysisId: z
|
|
292
|
-
.string()
|
|
293
|
-
.max(500)
|
|
294
|
-
.optional()
|
|
295
|
-
.describe('ID of a previous analyze result to reuse for plan/strategy actions'),
|
|
296
|
-
},
|
|
297
|
-
}, safeLicensed('migrate_tech', withProject((args) => handleMigrateTech(args))));
|
|
298
|
-
// ── Apply Migration Cleanup ─────────────────────────────────────────────────
|
|
299
|
-
s.registerTool('apply_migration_cleanup', {
|
|
300
|
-
description: 'Apply rewritten acceptance criteria to spec files. ' +
|
|
301
|
-
'Each rewritten criterion must score >= 80 via checkCriteriaQuality (GIVEN/WHEN/THEN + concrete value). ' +
|
|
302
|
-
'Criteria scoring < 80 are skipped and returned in the skipped array. ' +
|
|
303
|
-
'Call this after list_specs returns migrationIssues and the user approves rewriting.',
|
|
304
|
-
annotations: { title: 'Apply Migration Cleanup', destructiveHint: true },
|
|
305
|
-
inputSchema: {
|
|
306
|
-
projectPath: z
|
|
307
|
-
.string()
|
|
308
|
-
.min(1)
|
|
309
|
-
.max(4096)
|
|
310
|
-
.describe('Absolute path to the project root where planu/ directory lives.'),
|
|
311
|
-
rewrittenCriteria: z
|
|
312
|
-
.array(z.object({
|
|
313
|
-
specId: z.string().min(1).max(500).describe('Spec ID (e.g. SPEC-042).'),
|
|
314
|
-
oldText: z.string().min(1).max(2000).describe('Exact criterion text to replace.'),
|
|
315
|
-
newText: z
|
|
316
|
-
.string()
|
|
317
|
-
.min(1)
|
|
318
|
-
.max(2000)
|
|
319
|
-
.describe('Replacement criterion in GIVEN/WHEN/THEN format with concrete values.'),
|
|
320
|
-
}))
|
|
321
|
-
.min(1)
|
|
322
|
-
.max(200)
|
|
323
|
-
.describe('Array of criteria rewrites. Each entry: { specId, oldText, newText }. ' +
|
|
324
|
-
'newText must use GIVEN/WHEN/THEN with a concrete value to score >= 80.'),
|
|
325
|
-
},
|
|
326
|
-
}, safeTracked('apply_migration_cleanup', async (args) => {
|
|
327
|
-
const { projectPath, rewrittenCriteria } = args;
|
|
328
|
-
let criteriaUpdated = 0;
|
|
329
|
-
const skipped = [];
|
|
330
|
-
for (const { specId, oldText, newText } of rewrittenCriteria) {
|
|
331
|
-
const quality = checkCriteriaQuality(newText);
|
|
332
|
-
if (quality.score < 80) {
|
|
333
|
-
skipped.push({
|
|
334
|
-
specId,
|
|
335
|
-
criterionText: newText,
|
|
336
|
-
reason: `Score ${String(quality.score)}/100 — ${quality.reason}`,
|
|
337
|
-
});
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
const specPath = await findSpecPath(projectPath, specId);
|
|
341
|
-
if (!specPath) {
|
|
342
|
-
skipped.push({
|
|
343
|
-
specId,
|
|
344
|
-
criterionText: oldText,
|
|
345
|
-
reason: 'spec.md not found for this specId',
|
|
346
|
-
});
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
let content;
|
|
350
|
-
try {
|
|
351
|
-
content = await readFile(specPath, 'utf-8');
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
skipped.push({ specId, criterionText: oldText, reason: 'Could not read spec.md' });
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
const escapedOld = oldText.replace(/"/g, '\\"');
|
|
358
|
-
const escapedNew = newText.replace(/"/g, '\\"');
|
|
359
|
-
if (!content.includes(`"${escapedOld}"`)) {
|
|
360
|
-
skipped.push({
|
|
361
|
-
specId,
|
|
362
|
-
criterionText: oldText,
|
|
363
|
-
reason: 'Criterion text not found in spec.md (already updated or text mismatch)',
|
|
364
|
-
});
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
const updatedContent = replaceCriterionInFrontmatter(content, escapedOld, escapedNew);
|
|
368
|
-
if (updatedContent === null) {
|
|
369
|
-
skipped.push({
|
|
370
|
-
specId,
|
|
371
|
-
criterionText: oldText,
|
|
372
|
-
reason: 'Could not locate criterion in YAML frontmatter',
|
|
373
|
-
});
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
try {
|
|
377
|
-
await writeFile(specPath, updatedContent, 'utf-8');
|
|
378
|
-
criteriaUpdated++;
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
skipped.push({ specId, criterionText: oldText, reason: 'Could not write spec.md' });
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
const summary = [
|
|
385
|
-
`✓ ${String(criteriaUpdated)} criteria updated`,
|
|
386
|
-
skipped.length > 0 ? `✗ ${String(skipped.length)} skipped` : '',
|
|
387
|
-
]
|
|
388
|
-
.filter(Boolean)
|
|
389
|
-
.join(' · ');
|
|
390
|
-
return {
|
|
391
|
-
content: [{ type: 'text', text: summary }],
|
|
392
|
-
structuredContent: { criteriaUpdated, skipped },
|
|
393
|
-
};
|
|
394
|
-
}));
|
|
395
|
-
// ── Stack ───────────────────────────────────────────────────────────────────
|
|
396
|
-
s.registerTool('audit_stack', {
|
|
397
|
-
description: 'Audit all project dependencies, classify as up_to_date/outdated/vulnerable/unmaintained, and generate a StackHealthScore (0-100).',
|
|
398
|
-
annotations: { readOnlyHint: true },
|
|
399
|
-
inputSchema: {
|
|
400
|
-
...projectIdSchema,
|
|
401
|
-
format: z
|
|
402
|
-
.enum(['summary', 'json'])
|
|
403
|
-
.optional()
|
|
404
|
-
.describe('Output format: summary (default) or json'),
|
|
405
|
-
since: z
|
|
406
|
-
.string()
|
|
407
|
-
.max(500)
|
|
408
|
-
.optional()
|
|
409
|
-
.describe('ISO timestamp — show only changes since this date'),
|
|
410
|
-
failOn: z
|
|
411
|
-
.enum(['none', 'warning', 'critical'])
|
|
412
|
-
.optional()
|
|
413
|
-
.describe('Set isError=true if score drops below threshold (for CI usage)'),
|
|
414
|
-
ci: z.boolean().optional().describe('CI mode — exit with error on critical findings'),
|
|
415
|
-
},
|
|
416
|
-
}, safeLicensed('audit_stack', withProject((args) => handleAuditStack(args))));
|
|
417
|
-
s.registerTool('plan_upgrade', {
|
|
418
|
-
description: 'Generate a migration plan for upgrading a specific package, including breaking changes, estimated effort, and rollback procedure.',
|
|
419
|
-
annotations: { readOnlyHint: true },
|
|
420
|
-
inputSchema: {
|
|
421
|
-
...projectIdSchema,
|
|
422
|
-
packageName: z
|
|
423
|
-
.string()
|
|
424
|
-
.max(500)
|
|
425
|
-
.describe('Name of the package to upgrade (e.g. "express")'),
|
|
426
|
-
fromVersion: z.string().max(500).describe('Current installed version'),
|
|
427
|
-
targetVersion: z.string().max(500).describe('Target version to upgrade to'),
|
|
428
|
-
changelogText: z
|
|
429
|
-
.string()
|
|
430
|
-
.max(1_000_000)
|
|
431
|
-
.optional()
|
|
432
|
-
.describe('Optional CHANGELOG.md content for breaking-change analysis'),
|
|
433
|
-
format: z
|
|
434
|
-
.enum(['summary', 'json'])
|
|
435
|
-
.optional()
|
|
436
|
-
.describe('Output format: summary (default) or json'),
|
|
437
|
-
},
|
|
438
|
-
}, safeLicensed('plan_upgrade', withProject((args) => handlePlanUpgrade(args))));
|
|
439
|
-
s.registerTool('detect_deprecations', {
|
|
440
|
-
description: 'Scan project source files for deprecated API patterns (pattern-based, no network). Returns a deprecation report with tech-debt estimate.',
|
|
441
|
-
annotations: { readOnlyHint: true },
|
|
442
|
-
inputSchema: {
|
|
443
|
-
...projectIdSchema,
|
|
444
|
-
ecosystem: z
|
|
445
|
-
.enum(['nodejs', 'python', 'browser', 'go', 'java', 'rust', 'dotnet'])
|
|
446
|
-
.optional()
|
|
447
|
-
.describe('Override ecosystem detection. If omitted, auto-detected from go.mod, Cargo.toml, pom.xml, build.gradle, .csproj, package.json, etc.'),
|
|
448
|
-
format: z
|
|
449
|
-
.enum(['summary', 'json'])
|
|
450
|
-
.optional()
|
|
451
|
-
.describe('Output format: summary (default) or json'),
|
|
452
|
-
},
|
|
453
|
-
}, safeLicensed('detect_deprecations', withProject((args) => handleDetectDeprecations(args))));
|
|
454
|
-
// ── Templates ───────────────────────────────────────────────────────────────
|
|
455
|
-
s.registerTool('list_templates', {
|
|
456
|
-
description: 'Browse the built-in and custom spec template catalog. Returns available templates with descriptions, ' +
|
|
457
|
-
'categories, tags, complexity scores, and required variables. ' +
|
|
458
|
-
'Custom templates in data/global/spec-templates/custom/ are included automatically. ' +
|
|
459
|
-
'Use apply_template to create a spec from a template.',
|
|
460
|
-
inputSchema: {
|
|
461
|
-
category: z
|
|
462
|
-
.enum(TEMPLATE_CATEGORIES)
|
|
463
|
-
.optional()
|
|
464
|
-
.describe('Filter templates by category. Type: auth | crud | api | ui | infra | testing | integration | data | security | performance | industry'),
|
|
465
|
-
search: z
|
|
466
|
-
.string()
|
|
467
|
-
.max(500)
|
|
468
|
-
.optional()
|
|
469
|
-
.describe('Full-text search across template names, descriptions, and tags (case-insensitive)'),
|
|
470
|
-
complexityScore: z
|
|
471
|
-
.enum(COMPLEXITY_SCORES)
|
|
472
|
-
.optional()
|
|
473
|
-
.describe('Filter by complexity score. Type: S (<16h) | M (16-40h) | L (40-80h) | XL (>80h)'),
|
|
474
|
-
},
|
|
475
|
-
annotations: { title: 'List Spec Templates', readOnlyHint: true },
|
|
476
|
-
}, safeTracked('list_templates', async (args) => handleListTemplates(args)));
|
|
477
|
-
s.registerTool('apply_template', {
|
|
478
|
-
description: 'Create a unified spec.md from a spec template. ' +
|
|
479
|
-
'Templates are language-agnostic and cover common feature patterns (auth, CRUD, API, etc.) and industry verticals (fintech, healthtech, e-commerce, SaaS). ' +
|
|
480
|
-
'Use list_templates first to discover available templates and their required variables. ' +
|
|
481
|
-
'Supports partial criteria selection via includeCriteria parameter.',
|
|
482
|
-
inputSchema: {
|
|
483
|
-
templateId: z
|
|
484
|
-
.string()
|
|
485
|
-
.max(500)
|
|
486
|
-
.describe('ID of the template to apply (e.g. "auth-jwt", "fintech-kyc"). Use list_templates to get valid IDs.'),
|
|
487
|
-
projectPath: z
|
|
488
|
-
.string()
|
|
489
|
-
.max(4096)
|
|
490
|
-
.describe('Absolute path to the project root where specs will be created'),
|
|
491
|
-
variables: z
|
|
492
|
-
.array(z.object({
|
|
493
|
-
key: z
|
|
494
|
-
.string()
|
|
495
|
-
.max(500)
|
|
496
|
-
.describe('Variable key as defined in the template (e.g. "EntityName")'),
|
|
497
|
-
value: z
|
|
498
|
-
.string()
|
|
499
|
-
.max(10_000)
|
|
500
|
-
.describe('Value to substitute for this variable (e.g. "Product")'),
|
|
501
|
-
}))
|
|
502
|
-
.max(1000)
|
|
503
|
-
.describe('Template variable substitutions. Use list_templates to see required variables for each template.'),
|
|
504
|
-
feature: z
|
|
505
|
-
.string()
|
|
506
|
-
.max(500)
|
|
507
|
-
.optional()
|
|
508
|
-
.describe('Feature group tag (e.g. "Authentication", "Products"). Added to tags for categorization. Flat structure — no subdirectories created.'),
|
|
509
|
-
outputDir: z
|
|
510
|
-
.string()
|
|
511
|
-
.max(4096)
|
|
512
|
-
.optional()
|
|
513
|
-
.describe('Custom absolute path for the output directory. Overrides the default planu/specs/{feature}/{template-id}/ layout.'),
|
|
514
|
-
includeCriteria: z
|
|
515
|
-
.array(z.string().max(500))
|
|
516
|
-
.max(1000)
|
|
517
|
-
.optional()
|
|
518
|
-
.describe('List of criteria IDs to include (e.g. ["AC-1", "AC-3"]). Omit to include all criteria. Required criteria are always included even if omitted from this list.'),
|
|
519
|
-
},
|
|
520
|
-
annotations: { title: 'Apply Spec Template', destructiveHint: false },
|
|
521
|
-
}, safeLicensed('apply_template', async (args) => handleApplyTemplate(args)));
|
|
522
|
-
// ── Search ──────────────────────────────────────────────────────────────────
|
|
523
|
-
s.registerTool('semantic_search', {
|
|
524
|
-
description: 'Search your project knowledge (specs, patterns, decisions, learned knowledge) by meaning — ' +
|
|
525
|
-
'not just by exact text. Uses TF-IDF embeddings to find semantically similar documents. ' +
|
|
526
|
-
'Requires init_project first.',
|
|
527
|
-
inputSchema: {
|
|
528
|
-
...projectIdSchema,
|
|
529
|
-
query: z.string().max(10_000).describe('Natural language search query'),
|
|
530
|
-
scope: z
|
|
531
|
-
.enum(['spec', 'pattern', 'knowledge', 'decision', 'all'])
|
|
532
|
-
.default('all')
|
|
533
|
-
.describe('Scope: spec | pattern | knowledge | decision | all'),
|
|
534
|
-
topK: z.number().default(5).describe('Maximum results to return (default 5)'),
|
|
535
|
-
threshold: z
|
|
536
|
-
.number()
|
|
537
|
-
.default(0.3)
|
|
538
|
-
.describe('Minimum similarity threshold 0-1 (default 0.3)'),
|
|
539
|
-
},
|
|
540
|
-
annotations: { title: 'Semantic Search', readOnlyHint: true },
|
|
541
|
-
}, safeLicensed('semantic_search', withProject((args) => handleSemanticSearch(args))));
|
|
542
|
-
// ── Import ──────────────────────────────────────────────────────────────────
|
|
543
|
-
const ImportSourceEnum = z.enum(['markdown', 'csv']).describe('Source format: markdown | csv');
|
|
544
|
-
s.registerTool('import_spec', {
|
|
545
|
-
description: 'Import a spec from an external source (Jira, Linear, Markdown, or CSV) into Planu. ' +
|
|
546
|
-
'Creates a new spec with the imported content. ' +
|
|
547
|
-
'Use bulk_import to import multiple specs at once.',
|
|
548
|
-
inputSchema: {
|
|
549
|
-
source: ImportSourceEnum,
|
|
550
|
-
filePath: z
|
|
551
|
-
.string()
|
|
552
|
-
.max(4096)
|
|
553
|
-
.optional()
|
|
554
|
-
.describe('Absolute path to the source file. Required if content is not provided.'),
|
|
555
|
-
content: z
|
|
556
|
-
.string()
|
|
557
|
-
.max(100_000)
|
|
558
|
-
.optional()
|
|
559
|
-
.describe('Inline content string. For jira/linear: JSON string of the issue. For markdown: markdown text. For csv: CSV text.'),
|
|
560
|
-
projectPath: z.string().max(4096).describe('Absolute path to the target project root'),
|
|
561
|
-
overwrite: z
|
|
562
|
-
.boolean()
|
|
563
|
-
.optional()
|
|
564
|
-
.describe('Overwrite an existing spec with the same title (default: false). When false, a warning is returned instead of creating a duplicate.'),
|
|
565
|
-
},
|
|
566
|
-
}, safeTracked('import_spec', async (args) => handleImportSpec(args)));
|
|
567
|
-
s.registerTool('bulk_import', {
|
|
568
|
-
description: 'Bulk import multiple specs from a CSV file or a JSON array of Jira/Linear issues. ' +
|
|
569
|
-
'Each row in CSV or each item in the JSON array becomes one spec. ' +
|
|
570
|
-
'Returns a summary with total, succeeded, failed counts and per-spec results.',
|
|
571
|
-
inputSchema: {
|
|
572
|
-
source: ImportSourceEnum,
|
|
573
|
-
filePath: z
|
|
574
|
-
.string()
|
|
575
|
-
.max(4096)
|
|
576
|
-
.optional()
|
|
577
|
-
.describe('Absolute path to the source file (CSV or JSON array file).'),
|
|
578
|
-
content: z
|
|
579
|
-
.string()
|
|
580
|
-
.max(500_000)
|
|
581
|
-
.optional()
|
|
582
|
-
.describe('Inline content string. For csv: CSV with header row. For jira/linear: JSON array of issues.'),
|
|
583
|
-
projectPath: z.string().max(4096).describe('Absolute path to the target project root'),
|
|
584
|
-
overwrite: z
|
|
585
|
-
.boolean()
|
|
586
|
-
.optional()
|
|
587
|
-
.describe('Overwrite existing specs with same title (default: false).'),
|
|
588
|
-
},
|
|
589
|
-
}, safeTracked('bulk_import', async (args) => handleBulkImport(args)));
|
|
590
|
-
// ── Hooks ───────────────────────────────────────────────────────────────────
|
|
591
|
-
s.registerTool('start_hooks', {
|
|
592
|
-
description: 'Start the file watcher and hook engine for a project. ' +
|
|
593
|
-
'Idempotent — if already running, returns current status. ' +
|
|
594
|
-
'Watches for file changes and dispatches to registered hook handlers.',
|
|
595
|
-
inputSchema: {
|
|
596
|
-
projectPath: z
|
|
597
|
-
.string()
|
|
598
|
-
.max(1000)
|
|
599
|
-
.describe('Absolute path to the project root directory to watch'),
|
|
600
|
-
hooks: z
|
|
601
|
-
.array(z.string().max(100))
|
|
602
|
-
.optional()
|
|
603
|
-
.describe('Hook types to enable. Valid values: on-save | on-create | on-delete | on-test-pass | on-commit. ' +
|
|
604
|
-
'Defaults to [on-save, on-create, on-delete]'),
|
|
605
|
-
},
|
|
606
|
-
annotations: { title: 'Start Hook Engine', readOnlyHint: false },
|
|
607
|
-
}, safeLicensed('start_hooks', async (args) => handleStartHooks(args)));
|
|
608
|
-
s.registerTool('stop_hooks', {
|
|
609
|
-
description: 'Stop the file watcher and hook engine. Cleans up all resources including ' +
|
|
610
|
-
'watchers, timers, and event listeners.',
|
|
611
|
-
inputSchema: {
|
|
612
|
-
reason: z
|
|
613
|
-
.string()
|
|
614
|
-
.max(500)
|
|
615
|
-
.optional()
|
|
616
|
-
.describe('Optional reason for stopping the hook engine'),
|
|
617
|
-
},
|
|
618
|
-
annotations: { title: 'Stop Hook Engine', readOnlyHint: false },
|
|
619
|
-
}, safeLicensed('stop_hooks', async (args) => handleStopHooks(args)));
|
|
620
|
-
s.registerTool('hook_status', {
|
|
621
|
-
description: 'Get the current status of the hook engine including running state, ' +
|
|
622
|
-
'active hooks, pending events, telemetry (execution count, avg latency, error rate), ' +
|
|
623
|
-
'and event history count.',
|
|
624
|
-
inputSchema: {},
|
|
625
|
-
annotations: { title: 'Hook Engine Status', readOnlyHint: true },
|
|
626
|
-
}, safeLicensed('hook_status', async () => handleHookStatus()));
|
|
627
|
-
s.registerTool('configure_hooks', {
|
|
628
|
-
description: 'View or update hook engine configuration at runtime. ' +
|
|
629
|
-
'If the engine is running, reloads config from .planu/hooks.json. ' +
|
|
630
|
-
'If not running, shows current config from disk.',
|
|
631
|
-
inputSchema: {
|
|
632
|
-
projectPath: z
|
|
633
|
-
.string()
|
|
634
|
-
.min(1)
|
|
635
|
-
.max(1000)
|
|
636
|
-
.describe('Absolute path to the project root directory'),
|
|
637
|
-
enabled: z.boolean().optional().describe('Master switch for all hooks (true = enabled)'),
|
|
638
|
-
debounceMs: z
|
|
639
|
-
.number()
|
|
640
|
-
.min(50)
|
|
641
|
-
.max(10000)
|
|
642
|
-
.optional()
|
|
643
|
-
.describe('Debounce window in milliseconds (50-10000, default: 300)'),
|
|
644
|
-
rateLimitPerFileMs: z
|
|
645
|
-
.number()
|
|
646
|
-
.min(1000)
|
|
647
|
-
.max(60000)
|
|
648
|
-
.optional()
|
|
649
|
-
.describe('Rate limit per file in milliseconds (1000-60000, default: 5000)'),
|
|
650
|
-
hooks: z
|
|
651
|
-
.record(z.string().max(100), z.object({
|
|
652
|
-
enabled: z.boolean().optional().describe('Whether this hook type is enabled'),
|
|
653
|
-
include: z.array(z.string().max(500)).optional().describe('Glob patterns to include'),
|
|
654
|
-
exclude: z.array(z.string().max(500)).optional().describe('Glob patterns to exclude'),
|
|
655
|
-
}))
|
|
656
|
-
.optional()
|
|
657
|
-
.describe('Per-hook-type configuration overrides'),
|
|
658
|
-
template: z
|
|
659
|
-
.enum([
|
|
660
|
-
'notification-macos',
|
|
661
|
-
'notification-linux',
|
|
662
|
-
'notification-windows',
|
|
663
|
-
'notification-desktop',
|
|
664
|
-
'instructions-loaded-debug',
|
|
665
|
-
'permission-auto-approve-readonly',
|
|
666
|
-
])
|
|
667
|
-
.optional()
|
|
668
|
-
.describe('SPEC-258 — Apply a Claude Code hook template. ' +
|
|
669
|
-
'Valid values: notification-macos | notification-linux | notification-windows | ' +
|
|
670
|
-
'notification-desktop | instructions-loaded-debug | permission-auto-approve-readonly. ' +
|
|
671
|
-
'Generates the hook config and merges it into .claude/settings.json.'),
|
|
672
|
-
notificationMatcher: z
|
|
673
|
-
.enum(['permission_prompt', 'idle_prompt', 'auth_success', 'elicitation_dialog', ''])
|
|
674
|
-
.optional()
|
|
675
|
-
.describe('SPEC-258 — Matcher for notification templates. ' +
|
|
676
|
-
'Valid values: permission_prompt | idle_prompt | auth_success | elicitation_dialog | "" (all events). ' +
|
|
677
|
-
'Only used when template is a notification-* variant.'),
|
|
678
|
-
action: z
|
|
679
|
-
.enum(['enable-plan-mode-integration'])
|
|
680
|
-
.optional()
|
|
681
|
-
.describe('SPEC-262 — Special configure action. ' +
|
|
682
|
-
'Valid values: enable-plan-mode-integration. ' +
|
|
683
|
-
'Creates .claude/hooks/planu-pre-plan.sh and .claude/hooks/planu-post-plan.sh and ' +
|
|
684
|
-
'registers PreToolUse/PostToolUse entries in .claude/settings.json.'),
|
|
685
|
-
},
|
|
686
|
-
annotations: { title: 'Configure Hooks', readOnlyHint: false },
|
|
687
|
-
}, safeLicensed('configure_hooks', async (args) => handleConfigureHooks(args)));
|
|
688
|
-
s.registerTool('manage_hooks', {
|
|
689
|
-
description: 'Create, list, delete, enable/disable, and install templates for agent hooks. ' +
|
|
690
|
-
'Hooks automate actions when Planu events fire (spec created, status change, drift detected, commit). ' +
|
|
691
|
-
'Use dry_run to preview which hooks would fire without side effects.',
|
|
692
|
-
inputSchema: {
|
|
693
|
-
...projectIdSchema,
|
|
694
|
-
operation: z
|
|
695
|
-
.enum([
|
|
696
|
-
'create',
|
|
697
|
-
'list',
|
|
698
|
-
'delete',
|
|
699
|
-
'enable',
|
|
700
|
-
'disable',
|
|
701
|
-
'install_template',
|
|
702
|
-
'dry_run',
|
|
703
|
-
'list_logs',
|
|
704
|
-
])
|
|
705
|
-
.describe('Operation: create | list | delete | enable | disable | install_template | dry_run | list_logs'),
|
|
706
|
-
hook: z
|
|
707
|
-
.object({
|
|
708
|
-
name: z.string().max(500).describe('Human-readable hook name').optional(),
|
|
709
|
-
description: z.string().max(10_000).describe('What this hook does').optional(),
|
|
710
|
-
trigger: z
|
|
711
|
-
.enum(['on_spec_created', 'on_spec_status_change', 'on_drift_detected', 'on_commit'])
|
|
712
|
-
.describe('Trigger: on_spec_created | on_spec_status_change | on_drift_detected | on_commit')
|
|
713
|
-
.optional(),
|
|
714
|
-
filter: z
|
|
715
|
-
.object({
|
|
716
|
-
fromStatus: z
|
|
717
|
-
.string()
|
|
718
|
-
.max(500)
|
|
719
|
-
.optional()
|
|
720
|
-
.describe('Only fire when status changes FROM this'),
|
|
721
|
-
toStatus: z
|
|
722
|
-
.string()
|
|
723
|
-
.max(500)
|
|
724
|
-
.optional()
|
|
725
|
-
.describe('Only fire when status changes TO this'),
|
|
726
|
-
specIdPattern: z
|
|
727
|
-
.string()
|
|
728
|
-
.max(500)
|
|
729
|
-
.optional()
|
|
730
|
-
.describe('Only fire for specs matching this glob pattern (e.g. SPEC-*)'),
|
|
731
|
-
})
|
|
732
|
-
.optional()
|
|
733
|
-
.describe('Optional filter conditions for conditional firing'),
|
|
734
|
-
actions: z
|
|
735
|
-
.array(z.object({
|
|
736
|
-
type: z
|
|
737
|
-
.enum(['run_tool', 'log_message', 'set_variable', 'notify'])
|
|
738
|
-
.describe('Action type: run_tool | log_message | set_variable | notify'),
|
|
739
|
-
tool: z.string().max(500).optional().describe('Tool name (for run_tool)'),
|
|
740
|
-
args: z
|
|
741
|
-
.record(z.string().max(500), z.unknown())
|
|
742
|
-
.optional()
|
|
743
|
-
.describe('Tool args (for run_tool)'),
|
|
744
|
-
message: z
|
|
745
|
-
.string()
|
|
746
|
-
.max(10_000)
|
|
747
|
-
.optional()
|
|
748
|
-
.describe('Message template (for log_message/notify)'),
|
|
749
|
-
key: z.string().max(500).optional().describe('Variable key (for set_variable)'),
|
|
750
|
-
value: z
|
|
751
|
-
.string()
|
|
752
|
-
.max(10_000)
|
|
753
|
-
.optional()
|
|
754
|
-
.describe('Variable value (for set_variable)'),
|
|
755
|
-
}))
|
|
756
|
-
.max(100)
|
|
757
|
-
.optional()
|
|
758
|
-
.describe('Ordered list of actions to execute when hook fires'),
|
|
759
|
-
enabled: z.boolean().optional().describe('Whether hook is active (default: true)'),
|
|
760
|
-
})
|
|
761
|
-
.optional()
|
|
762
|
-
.describe('Hook definition — required for create operation'),
|
|
763
|
-
hookId: z
|
|
764
|
-
.string()
|
|
765
|
-
.max(500)
|
|
766
|
-
.optional()
|
|
767
|
-
.describe('Hook ID — required for delete, enable, disable, dry_run'),
|
|
768
|
-
templateName: z
|
|
769
|
-
.string()
|
|
770
|
-
.max(500)
|
|
771
|
-
.optional()
|
|
772
|
-
.describe('Template name — required for install_template. Options: sdd-full | ci-ready | quality-gate'),
|
|
773
|
-
dryRunPayload: z
|
|
774
|
-
.object({
|
|
775
|
-
trigger: z
|
|
776
|
-
.enum(['on_spec_created', 'on_spec_status_change', 'on_drift_detected', 'on_commit'])
|
|
777
|
-
.describe('Trigger type to simulate'),
|
|
778
|
-
projectId: z.string().max(500).describe('Project ID for the simulated event'),
|
|
779
|
-
timestamp: z.string().max(500).optional().describe('Event timestamp (defaults to now)'),
|
|
780
|
-
specId: z.string().max(500).optional().describe('Spec ID (for spec-related triggers)'),
|
|
781
|
-
specTitle: z.string().max(500).optional().describe('Spec title'),
|
|
782
|
-
specType: z.string().max(500).optional().describe('Spec type (for on_spec_created)'),
|
|
783
|
-
fromStatus: z
|
|
784
|
-
.string()
|
|
785
|
-
.max(500)
|
|
786
|
-
.optional()
|
|
787
|
-
.describe('Previous status (for on_spec_status_change)'),
|
|
788
|
-
toStatus: z
|
|
789
|
-
.string()
|
|
790
|
-
.max(500)
|
|
791
|
-
.optional()
|
|
792
|
-
.describe('New status (for on_spec_status_change)'),
|
|
793
|
-
driftScore: z.number().optional().describe('Drift score 0-100 (for on_drift_detected)'),
|
|
794
|
-
driftSummary: z
|
|
795
|
-
.string()
|
|
796
|
-
.max(10_000)
|
|
797
|
-
.optional()
|
|
798
|
-
.describe('Drift summary (for on_drift_detected)'),
|
|
799
|
-
commitHash: z.string().max(500).optional().describe('Commit hash (for on_commit)'),
|
|
800
|
-
commitMessage: z
|
|
801
|
-
.string()
|
|
802
|
-
.max(10_000)
|
|
803
|
-
.optional()
|
|
804
|
-
.describe('Commit message (for on_commit)'),
|
|
805
|
-
branch: z.string().max(500).optional().describe('Branch name (for on_commit)'),
|
|
806
|
-
})
|
|
807
|
-
.optional()
|
|
808
|
-
.describe('Simulated event payload for dry_run operation'),
|
|
809
|
-
},
|
|
810
|
-
annotations: { title: 'Manage Agent Hooks', readOnlyHint: false },
|
|
811
|
-
}, safeLicensed('manage_hooks', withProject((args) => handleManageHooks(args))));
|
|
812
|
-
// ── Filesystem Hooks ────────────────────────────────────────────────────────
|
|
813
|
-
s.registerTool('configure_filesystem_hooks', {
|
|
814
|
-
description: 'Configure reactive filesystem hooks for a project. ' +
|
|
815
|
-
'Hooks auto-trigger Planu tools when project files change (save, create, delete). ' +
|
|
816
|
-
'Use enable/disable to toggle the entire system, add-hook to register a new hook, ' +
|
|
817
|
-
'remove-hook to delete by index, list to view all hooks, or status to see current state.',
|
|
818
|
-
annotations: {
|
|
819
|
-
title: 'Configure Filesystem Hooks',
|
|
820
|
-
readOnlyHint: false,
|
|
821
|
-
destructiveHint: false,
|
|
822
|
-
},
|
|
823
|
-
inputSchema: {
|
|
824
|
-
action: z
|
|
825
|
-
.enum(['enable', 'disable', 'add-hook', 'remove-hook', 'list', 'status'])
|
|
826
|
-
.describe('Action to perform. Valid values: enable | disable | add-hook | remove-hook | list | status.'),
|
|
827
|
-
projectPath: z
|
|
828
|
-
.string()
|
|
829
|
-
.min(1)
|
|
830
|
-
.max(4096)
|
|
831
|
-
.describe('Absolute path to the project root directory'),
|
|
832
|
-
hook: z
|
|
833
|
-
.object({
|
|
834
|
-
event: z
|
|
835
|
-
.enum(['on_save', 'on_create', 'on_delete'])
|
|
836
|
-
.describe('Filesystem event to watch. Valid values: on_save | on_create | on_delete.'),
|
|
837
|
-
pattern: z
|
|
838
|
-
.string()
|
|
839
|
-
.min(1)
|
|
840
|
-
.max(500)
|
|
841
|
-
.describe('Glob pattern to match (e.g. "src/**/*.ts", "*.json")'),
|
|
842
|
-
tool: z
|
|
843
|
-
.string()
|
|
844
|
-
.min(1)
|
|
845
|
-
.max(200)
|
|
846
|
-
.describe('Planu tool name to trigger when pattern matches (e.g. "validate")'),
|
|
847
|
-
args: z
|
|
848
|
-
.record(z.string().max(200), z.unknown())
|
|
849
|
-
.optional()
|
|
850
|
-
.describe('Extra arguments to pass to the triggered tool'),
|
|
851
|
-
debounceMs: z
|
|
852
|
-
.number()
|
|
853
|
-
.min(0)
|
|
854
|
-
.max(60000)
|
|
855
|
-
.optional()
|
|
856
|
-
.describe('Debounce window in milliseconds (default: 500)'),
|
|
857
|
-
})
|
|
858
|
-
.optional()
|
|
859
|
-
.describe('Hook definition — required when action is add-hook'),
|
|
860
|
-
hookIndex: z
|
|
861
|
-
.number()
|
|
862
|
-
.int()
|
|
863
|
-
.min(0)
|
|
864
|
-
.optional()
|
|
865
|
-
.describe('Zero-based index of the hook to remove — required for remove-hook action'),
|
|
866
|
-
},
|
|
867
|
-
}, safeLicensed('configure_filesystem_hooks', async (args) => handleConfigureFilesystemHooks({
|
|
868
|
-
action: args.action,
|
|
869
|
-
projectPath: args.projectPath,
|
|
870
|
-
hook: args.hook,
|
|
871
|
-
hookIndex: args.hookIndex,
|
|
872
|
-
})));
|
|
873
|
-
s.registerTool('filesystem_hooks_status', {
|
|
874
|
-
description: 'Check the current status of filesystem hooks for a project. ' +
|
|
875
|
-
'Returns whether hooks are enabled and how many are configured.',
|
|
876
|
-
annotations: {
|
|
877
|
-
title: 'Filesystem Hooks Status',
|
|
878
|
-
readOnlyHint: true,
|
|
879
|
-
destructiveHint: false,
|
|
880
|
-
},
|
|
881
|
-
inputSchema: {
|
|
882
|
-
projectPath: z
|
|
883
|
-
.string()
|
|
884
|
-
.min(1)
|
|
885
|
-
.max(4096)
|
|
886
|
-
.describe('Absolute path to the project root directory'),
|
|
887
|
-
},
|
|
888
|
-
}, safeTracked('filesystem_hooks_status', async (args) => handleConfigureFilesystemHooks({ action: 'status', projectPath: args.projectPath })));
|
|
889
|
-
// ── Dep Audit ───────────────────────────────────────────────────────────────
|
|
890
|
-
s.registerTool('security_scan', {
|
|
891
|
-
description: 'Scan project dependencies for security vulnerabilities, license conflicts, and abandoned packages. ' +
|
|
892
|
-
'Free tier: npm critical CVEs only. Pro: all ecosystems, full severity breakdown.',
|
|
893
|
-
annotations: { readOnlyHint: true },
|
|
894
|
-
inputSchema: {
|
|
895
|
-
projectPath: z
|
|
896
|
-
.string()
|
|
897
|
-
.min(1)
|
|
898
|
-
.max(4096)
|
|
899
|
-
.describe('Absolute path to the project root to audit'),
|
|
900
|
-
ecosystem: z
|
|
901
|
-
.enum(['npm', 'python', 'all'])
|
|
902
|
-
.optional()
|
|
903
|
-
.describe('Ecosystem filter: npm (Node.js), python, or all. Pro only for python/all. Default: npm'),
|
|
904
|
-
force_refresh: z
|
|
905
|
-
.boolean()
|
|
906
|
-
.optional()
|
|
907
|
-
.describe('Force synchronous CVE database refresh from OSV.dev before scanning, ignoring the 7-day cache.'),
|
|
908
|
-
},
|
|
909
|
-
}, safeTracked('security_scan', async (args) => {
|
|
910
|
-
const ecosystems = args.ecosystem === 'all'
|
|
911
|
-
? ['npm', 'pip', 'go', 'cargo']
|
|
912
|
-
: args.ecosystem === 'python'
|
|
913
|
-
? ['pip']
|
|
914
|
-
: ['npm'];
|
|
915
|
-
// SPEC-773: CVE database freshness check
|
|
916
|
-
const staleness = await isCveDatabaseStale();
|
|
917
|
-
if (args.force_refresh) {
|
|
918
|
-
await refreshCveDatabase(ecosystems);
|
|
919
|
-
}
|
|
920
|
-
else if (staleness.stale) {
|
|
921
|
-
// Fire-and-forget — scan proceeds immediately with current data
|
|
922
|
-
refreshCveDatabase(ecosystems).catch(() => undefined);
|
|
923
|
-
}
|
|
924
|
-
const report = await auditDeps(args.projectPath);
|
|
925
|
-
let text = buildFreeReport(report);
|
|
926
|
-
if (staleness.stale) {
|
|
927
|
-
text += `\n\n> ⚠️ CVE database is ${String(staleness.ageDays)} days old — refresh triggered in background. Run with force_refresh: true to wait for fresh data.`;
|
|
928
|
-
}
|
|
929
|
-
return { content: [{ type: 'text', text }] };
|
|
930
|
-
}));
|
|
931
|
-
s.registerTool('security_scan_pro', {
|
|
932
|
-
description: 'Full security scan across all ecosystems (npm, Python, Go, Rust, Java). ' +
|
|
933
|
-
'Reports critical → high → medium → low vulnerabilities, license conflicts, and abandoned packages. Requires Pro plan.',
|
|
934
|
-
annotations: { readOnlyHint: true },
|
|
935
|
-
inputSchema: {
|
|
936
|
-
projectPath: z
|
|
937
|
-
.string()
|
|
938
|
-
.min(1)
|
|
939
|
-
.max(4096)
|
|
940
|
-
.describe('Absolute path to the project root to audit'),
|
|
941
|
-
ecosystem: z
|
|
942
|
-
.enum(['npm', 'python', 'all'])
|
|
943
|
-
.optional()
|
|
944
|
-
.describe('Ecosystem filter: npm, python, or all (default). Pro feature for python/all ecosystems.'),
|
|
945
|
-
},
|
|
946
|
-
}, safeLicensed('security_scan_pro', async (args) => {
|
|
947
|
-
const report = await auditDeps(args.projectPath);
|
|
948
|
-
const text = buildFullReport(report);
|
|
949
|
-
const cves = criticalVulnCves(report);
|
|
950
|
-
if (cves.length > 0) {
|
|
951
|
-
const warnText = `\n\n> CRITICAL CVEs detected: ${cves.join(', ')}. Address these before deploying to production.`;
|
|
952
|
-
return { content: [{ type: 'text', text: text + warnText }] };
|
|
953
|
-
}
|
|
954
|
-
return { content: [{ type: 'text', text }] };
|
|
955
|
-
}));
|
|
956
|
-
// ── Dependency Health (SPEC-967) ────────────────────────────────────────────
|
|
957
|
-
s.registerTool('dependency_health', {
|
|
958
|
-
description: 'Analyze project dependency health: detects unused, missing, outdated, ' +
|
|
959
|
-
'duplicated, and peer-conflicting dependencies. Returns a 0-100 score with actionable recommendations.',
|
|
960
|
-
annotations: { readOnlyHint: true, title: 'Dependency Health' },
|
|
961
|
-
inputSchema: {
|
|
962
|
-
projectPath: z
|
|
963
|
-
.string()
|
|
964
|
-
.min(1)
|
|
965
|
-
.max(4096)
|
|
966
|
-
.describe('Absolute path to the project root to audit'),
|
|
967
|
-
ecosystem: z
|
|
968
|
-
.enum(['npm', 'python', 'rust', 'go'])
|
|
969
|
-
.optional()
|
|
970
|
-
.describe('Package ecosystem to analyze. Valid values: npm, python, rust, go. Default: npm'),
|
|
971
|
-
},
|
|
972
|
-
}, safeTracked('dependency_health', async (args) => {
|
|
973
|
-
const result = await handleDependencyHealth(args);
|
|
974
|
-
return result;
|
|
975
|
-
}));
|
|
976
|
-
// ── Changelog ───────────────────────────────────────────────────────────────
|
|
977
|
-
s.registerTool('spec_history', {
|
|
978
|
-
description: 'View the changelog history of a spec — see what changed, when, and why. ' +
|
|
979
|
-
'Shows criteria added/removed, status transitions, and reconciliation events. ' +
|
|
980
|
-
'Returns an empty list if no changelog exists yet for this spec.',
|
|
981
|
-
annotations: { title: 'Spec History', readOnlyHint: true },
|
|
982
|
-
inputSchema: {
|
|
983
|
-
specId: z
|
|
984
|
-
.string()
|
|
985
|
-
.min(1)
|
|
986
|
-
.max(500)
|
|
987
|
-
.describe('Spec ID to retrieve history for (e.g. "SPEC-067")'),
|
|
988
|
-
specDir: z
|
|
989
|
-
.string()
|
|
990
|
-
.max(4096)
|
|
991
|
-
.describe('Absolute path to the spec directory containing CHANGELOG.md'),
|
|
992
|
-
filter: z
|
|
993
|
-
.string()
|
|
994
|
-
.max(500)
|
|
995
|
-
.optional()
|
|
996
|
-
.describe('Filter by change type(s), comma-separated. ' +
|
|
997
|
-
'Valid values: criteria_added | criteria_removed | status_changed | scope_changed | progress_updated | reconciled'),
|
|
998
|
-
},
|
|
999
|
-
}, safeLicensed('spec_history', async (args) => handleSpecHistory(args)));
|
|
1000
|
-
// ── Event ───────────────────────────────────────────────────────────────────
|
|
1001
|
-
s.registerTool('event_contracts', {
|
|
1002
|
-
description: 'Manage event-driven architecture contracts: generate JSON Schema contracts for events, ' +
|
|
1003
|
-
'validate payloads, evolve schemas with backward-compatibility classification, ' +
|
|
1004
|
-
'and define Saga contracts for distributed transactions.',
|
|
1005
|
-
annotations: { readOnlyHint: false },
|
|
1006
|
-
inputSchema: {
|
|
1007
|
-
subcommand: z
|
|
1008
|
-
.enum(['generate-contract', 'validate-contract', 'evolve-contract', 'saga-spec'])
|
|
1009
|
-
.describe('generate-contract: create a new event contract JSON Schema; ' +
|
|
1010
|
-
'validate-contract: verify a payload against the active contract; ' +
|
|
1011
|
-
'evolve-contract: classify a schema change and apply the new version; ' +
|
|
1012
|
-
'saga-spec: define a Saga with compensation events per step'),
|
|
1013
|
-
...projectIdSchema,
|
|
1014
|
-
generateContract: z
|
|
1015
|
-
.object({
|
|
1016
|
-
projectId: z.string().min(1).max(500).describe('Project ID'),
|
|
1017
|
-
eventName: z.string().max(500).describe('Unique event name, e.g. "order.created"'),
|
|
1018
|
-
producer: z.string().max(500).describe('Service or component that emits this event'),
|
|
1019
|
-
consumers: z
|
|
1020
|
-
.array(z.string().max(500))
|
|
1021
|
-
.max(100)
|
|
1022
|
-
.describe('Known consumer service names'),
|
|
1023
|
-
payloadFields: z
|
|
1024
|
-
.array(z.object({
|
|
1025
|
-
name: z.string().max(500).describe('Field name'),
|
|
1026
|
-
type: z
|
|
1027
|
-
.string()
|
|
1028
|
-
.max(500)
|
|
1029
|
-
.describe('JSON Schema type: string, number, boolean, object, array'),
|
|
1030
|
-
required: z.boolean().describe('Whether this field is required'),
|
|
1031
|
-
description: z.string().max(10_000).optional().describe('Field description'),
|
|
1032
|
-
}))
|
|
1033
|
-
.max(1000)
|
|
1034
|
-
.describe('Payload field definitions'),
|
|
1035
|
-
broker: z
|
|
1036
|
-
.enum(['kafka', 'rabbitmq', 'sqs-sns', 'nats', 'eventbridge', 'pubsub', 'unknown'])
|
|
1037
|
-
.optional()
|
|
1038
|
-
.describe('Message broker (auto-detected from project stack if omitted)'),
|
|
1039
|
-
specId: z
|
|
1040
|
-
.string()
|
|
1041
|
-
.max(500)
|
|
1042
|
-
.optional()
|
|
1043
|
-
.describe('SPEC-XXX that references this contract'),
|
|
1044
|
-
})
|
|
1045
|
-
.optional()
|
|
1046
|
-
.describe('Input for generate-contract subcommand'),
|
|
1047
|
-
validateContract: z
|
|
1048
|
-
.object({
|
|
1049
|
-
projectId: z.string().min(1).max(500).describe('Project ID'),
|
|
1050
|
-
eventName: z.string().max(500).describe('Event name to validate against'),
|
|
1051
|
-
payload: z
|
|
1052
|
-
.record(z.string().max(500), z.unknown())
|
|
1053
|
-
.describe('Payload object to validate'),
|
|
1054
|
-
})
|
|
1055
|
-
.optional()
|
|
1056
|
-
.describe('Input for validate-contract subcommand'),
|
|
1057
|
-
evolveContract: z
|
|
1058
|
-
.object({
|
|
1059
|
-
projectId: z.string().min(1).max(500).describe('Project ID'),
|
|
1060
|
-
eventName: z.string().max(500).describe('Event name to evolve'),
|
|
1061
|
-
changeDescription: z
|
|
1062
|
-
.string()
|
|
1063
|
-
.max(10_000)
|
|
1064
|
-
.describe('Human-readable description of the change'),
|
|
1065
|
-
addedFields: z
|
|
1066
|
-
.array(z.string().max(500))
|
|
1067
|
-
.max(100)
|
|
1068
|
-
.optional()
|
|
1069
|
-
.describe('New optional fields added'),
|
|
1070
|
-
removedFields: z
|
|
1071
|
-
.array(z.string().max(500))
|
|
1072
|
-
.max(100)
|
|
1073
|
-
.optional()
|
|
1074
|
-
.describe('Fields removed (breaking)'),
|
|
1075
|
-
changedFields: z
|
|
1076
|
-
.array(z.object({
|
|
1077
|
-
field: z.string().max(500),
|
|
1078
|
-
from: z.string().max(500).describe('Previous type'),
|
|
1079
|
-
to: z.string().max(500).describe('New type'),
|
|
1080
|
-
}))
|
|
1081
|
-
.max(100)
|
|
1082
|
-
.optional()
|
|
1083
|
-
.describe('Fields whose type changed (breaking)'),
|
|
1084
|
-
deprecatedFields: z
|
|
1085
|
-
.array(z.object({
|
|
1086
|
-
field: z.string().max(500),
|
|
1087
|
-
reason: z.string().max(10_000),
|
|
1088
|
-
sunsetVersion: z
|
|
1089
|
-
.string()
|
|
1090
|
-
.max(500)
|
|
1091
|
-
.describe('Version at which field will be removed'),
|
|
1092
|
-
}))
|
|
1093
|
-
.max(100)
|
|
1094
|
-
.optional()
|
|
1095
|
-
.describe('Fields marked deprecated'),
|
|
1096
|
-
})
|
|
1097
|
-
.optional()
|
|
1098
|
-
.describe('Input for evolve-contract subcommand'),
|
|
1099
|
-
sagaSpec: z
|
|
1100
|
-
.object({
|
|
1101
|
-
projectId: z.string().min(1).max(500).describe('Project ID'),
|
|
1102
|
-
sagaId: z.string().max(500).describe('Unique Saga ID, e.g. "checkout-saga"'),
|
|
1103
|
-
description: z.string().max(10_000).describe('What the Saga achieves'),
|
|
1104
|
-
coordinationStyle: z
|
|
1105
|
-
.enum(['choreography', 'orchestration'])
|
|
1106
|
-
.describe('Coordination style'),
|
|
1107
|
-
steps: z
|
|
1108
|
-
.array(z.object({
|
|
1109
|
-
id: z.string().max(500).describe('Unique step ID'),
|
|
1110
|
-
name: z.string().max(500).describe('Step name'),
|
|
1111
|
-
service: z.string().max(500).describe('Service executing this step'),
|
|
1112
|
-
successEvent: z.string().max(500).describe('Event emitted on step success'),
|
|
1113
|
-
failureEvent: z.string().max(500).describe('Event emitted on step failure'),
|
|
1114
|
-
compensationEvent: z
|
|
1115
|
-
.string()
|
|
1116
|
-
.max(500)
|
|
1117
|
-
.describe('Event that compensates / undoes this step'),
|
|
1118
|
-
failureConditions: z
|
|
1119
|
-
.array(z.string().max(500))
|
|
1120
|
-
.max(100)
|
|
1121
|
-
.describe('Conditions causing failure'),
|
|
1122
|
-
dependsOn: z
|
|
1123
|
-
.array(z.string().max(500))
|
|
1124
|
-
.max(100)
|
|
1125
|
-
.describe('Step IDs that must complete first'),
|
|
1126
|
-
}))
|
|
1127
|
-
.max(100)
|
|
1128
|
-
.describe('Ordered Saga steps — each must have a compensationEvent'),
|
|
1129
|
-
orchestratorService: z
|
|
1130
|
-
.string()
|
|
1131
|
-
.max(500)
|
|
1132
|
-
.optional()
|
|
1133
|
-
.describe('Orchestrator service name (required if orchestration style)'),
|
|
1134
|
-
specId: z.string().max(500).optional().describe('SPEC-XXX implementing this Saga'),
|
|
1135
|
-
})
|
|
1136
|
-
.optional()
|
|
1137
|
-
.describe('Input for saga-spec subcommand'),
|
|
1138
|
-
},
|
|
1139
|
-
}, safeLicensed('event_contracts', withProject((args) => handleEventContracts(args))));
|
|
1140
|
-
// ── Feedback ────────────────────────────────────────────────────────────────
|
|
1141
|
-
const FeedbackTypeEnum = z.enum(['bug', 'feature', 'suggestion', 'dx']);
|
|
1142
|
-
s.registerTool('submit_feedback', {
|
|
1143
|
-
description: 'Submit a bug report, feature request, or suggestion. Planu tracks it and links it to the spec that resolves it.',
|
|
1144
|
-
inputSchema: {
|
|
1145
|
-
type: FeedbackTypeEnum.describe('Feedback category. Values: bug | feature | suggestion | dx. ' +
|
|
1146
|
-
'bug = software defect, feature = new capability, suggestion = improvement idea, dx = developer experience.'),
|
|
1147
|
-
title: z
|
|
1148
|
-
.string()
|
|
1149
|
-
.min(1)
|
|
1150
|
-
.describe('Short, descriptive title for the feedback (1-100 chars).'),
|
|
1151
|
-
description: z
|
|
1152
|
-
.string()
|
|
1153
|
-
.min(1)
|
|
1154
|
-
.describe('Detailed description of the issue, request, or suggestion.'),
|
|
1155
|
-
context: z
|
|
1156
|
-
.object({
|
|
1157
|
-
projectPath: z
|
|
1158
|
-
.string()
|
|
1159
|
-
.optional()
|
|
1160
|
-
.describe('Absolute path to the project root where the issue occurred.'),
|
|
1161
|
-
currentFile: z
|
|
1162
|
-
.string()
|
|
1163
|
-
.optional()
|
|
1164
|
-
.describe('File path where the issue was observed (relative to project root).'),
|
|
1165
|
-
errorMessage: z.string().optional().describe('Exact error message if reporting a bug.'),
|
|
1166
|
-
stack: z.string().optional().describe('Stack trace if available.'),
|
|
1167
|
-
})
|
|
1168
|
-
.optional()
|
|
1169
|
-
.describe('Optional context to help reproduce or understand the feedback.'),
|
|
1170
|
-
},
|
|
1171
|
-
annotations: { title: 'Submit Feedback', destructiveHint: false },
|
|
1172
|
-
}, safeTracked('submit_feedback', async (args) => handleSubmitFeedback(args)));
|
|
1173
|
-
s.registerTool('triage_feedback', {
|
|
1174
|
-
description: 'View and prioritize pending user feedback grouped by similarity. Shows which feedback should become specs.',
|
|
1175
|
-
inputSchema: {
|
|
1176
|
-
filter: FeedbackTypeEnum.optional().describe('Filter by feedback type. Values: bug | feature | suggestion | dx. Omit to show all types.'),
|
|
1177
|
-
},
|
|
1178
|
-
annotations: { title: 'Triage Feedback', readOnlyHint: true },
|
|
1179
|
-
}, safeLicensed('triage_feedback', (args) => Promise.resolve(handleTriageFeedback(args))));
|
|
1180
|
-
s.registerTool('resolve_feedback', {
|
|
1181
|
-
description: 'Mark feedback as resolved, linking it to the spec and PR that fixed it.',
|
|
1182
|
-
inputSchema: {
|
|
1183
|
-
feedbackId: z.string().min(1).describe('Feedback entry ID to resolve (e.g. FB-042).'),
|
|
1184
|
-
specId: z
|
|
1185
|
-
.string()
|
|
1186
|
-
.optional()
|
|
1187
|
-
.describe('SPEC-XXX ID of the spec that addressed this feedback (e.g. SPEC-188).'),
|
|
1188
|
-
prUrl: z
|
|
1189
|
-
.string()
|
|
1190
|
-
.optional()
|
|
1191
|
-
.describe('GitHub PR URL that delivered the fix (e.g. https://github.com/org/repo/pull/123).'),
|
|
1192
|
-
notes: z
|
|
1193
|
-
.string()
|
|
1194
|
-
.optional()
|
|
1195
|
-
.describe('Optional resolution notes explaining how the feedback was addressed.'),
|
|
1196
|
-
},
|
|
1197
|
-
annotations: { title: 'Resolve Feedback', destructiveHint: false },
|
|
1198
|
-
}, safeLicensed('resolve_feedback', async (args) => handleResolveFeedback(args)));
|
|
1199
|
-
// ── Comments ────────────────────────────────────────────────────────────────
|
|
1200
|
-
s.registerTool('add_comment', {
|
|
1201
|
-
description: 'Add an inline comment to a spec. Comments can be anchored to a specific acceptance criterion (acIndex) and marked as blocking to prevent readiness approval.',
|
|
1202
|
-
inputSchema: {
|
|
1203
|
-
projectPath: z
|
|
1204
|
-
.string()
|
|
1205
|
-
.describe('Absolute path to the project root (e.g. /home/user/my-project).'),
|
|
1206
|
-
specId: z
|
|
1207
|
-
.string()
|
|
1208
|
-
.describe('SPEC-XXX identifier of the spec to comment on (e.g. SPEC-042).'),
|
|
1209
|
-
author: z.string().min(1).describe('Free-form author name or handle (e.g. "alice").'),
|
|
1210
|
-
text: z.string().min(1).describe('Comment body text.'),
|
|
1211
|
-
acIndex: z
|
|
1212
|
-
.number()
|
|
1213
|
-
.int()
|
|
1214
|
-
.min(0)
|
|
1215
|
-
.optional()
|
|
1216
|
-
.describe('Optional 0-based index of the acceptance criterion this comment is anchored to.'),
|
|
1217
|
-
blocking: z
|
|
1218
|
-
.boolean()
|
|
1219
|
-
.optional()
|
|
1220
|
-
.describe('When true, check_readiness will fail while this comment is unresolved. Defaults to false.'),
|
|
1221
|
-
},
|
|
1222
|
-
annotations: { title: 'Add Comment', destructiveHint: false },
|
|
1223
|
-
}, safeTracked('add_comment', async (args) => handleAddComment(args)));
|
|
1224
|
-
s.registerTool('list_comments', {
|
|
1225
|
-
description: 'List comments for a spec. Optionally filter by resolved status to see open discussions or archived resolutions.',
|
|
1226
|
-
inputSchema: {
|
|
1227
|
-
projectPath: z
|
|
1228
|
-
.string()
|
|
1229
|
-
.describe('Absolute path to the project root (e.g. /home/user/my-project).'),
|
|
1230
|
-
specId: z
|
|
1231
|
-
.string()
|
|
1232
|
-
.describe('SPEC-XXX identifier of the spec whose comments to list (e.g. SPEC-042).'),
|
|
1233
|
-
resolved: z
|
|
1234
|
-
.boolean()
|
|
1235
|
-
.optional()
|
|
1236
|
-
.describe('Filter by resolved status. true = show only resolved comments, false = show only open comments. Omit to show all comments.'),
|
|
1237
|
-
},
|
|
1238
|
-
annotations: { title: 'List Comments', readOnlyHint: true },
|
|
1239
|
-
}, safeTracked('list_comments', (args) => Promise.resolve(handleListComments(args))));
|
|
1240
|
-
s.registerTool('resolve_comment', {
|
|
1241
|
-
description: 'Mark a comment as resolved. The comment is archived (not deleted) so the discussion history is preserved.',
|
|
1242
|
-
inputSchema: {
|
|
1243
|
-
projectPath: z
|
|
1244
|
-
.string()
|
|
1245
|
-
.describe('Absolute path to the project root (e.g. /home/user/my-project).'),
|
|
1246
|
-
specId: z
|
|
1247
|
-
.string()
|
|
1248
|
-
.describe('SPEC-XXX identifier of the spec the comment belongs to (e.g. SPEC-042).'),
|
|
1249
|
-
commentId: z.string().describe('Comment ID to resolve (e.g. CMT-003).'),
|
|
1250
|
-
},
|
|
1251
|
-
annotations: { title: 'Resolve Comment', destructiveHint: false },
|
|
1252
|
-
}, safeTracked('resolve_comment', async (args) => handleResolveComment(args)));
|
|
1253
|
-
s.registerTool('reply_comment', {
|
|
1254
|
-
description: 'Reply to an existing comment, creating a threaded discussion. The reply is linked to the parent comment via parentId.',
|
|
1255
|
-
inputSchema: {
|
|
1256
|
-
projectPath: z
|
|
1257
|
-
.string()
|
|
1258
|
-
.describe('Absolute path to the project root (e.g. /home/user/my-project).'),
|
|
1259
|
-
specId: z
|
|
1260
|
-
.string()
|
|
1261
|
-
.describe('SPEC-XXX identifier of the spec the thread belongs to (e.g. SPEC-042).'),
|
|
1262
|
-
parentId: z.string().describe('ID of the parent comment to reply to (e.g. CMT-001).'),
|
|
1263
|
-
author: z.string().min(1).describe('Free-form author name or handle (e.g. "bob").'),
|
|
1264
|
-
text: z.string().min(1).describe('Reply body text.'),
|
|
1265
|
-
},
|
|
1266
|
-
annotations: { title: 'Reply to Comment', destructiveHint: false },
|
|
1267
|
-
}, safeTracked('reply_comment', async (args) => handleReplyComment(args)));
|
|
1268
|
-
// ── Memory ──────────────────────────────────────────────────────────────────
|
|
1269
|
-
s.registerTool('log_decision', {
|
|
1270
|
-
description: 'Register, list, search, or supersede architectural and process decisions for a project. ' +
|
|
1271
|
-
'Prevents re-debating settled choices across sessions. ' +
|
|
1272
|
-
'Warns when a proposed change conflicts with an existing active decision.',
|
|
1273
|
-
annotations: { readOnlyHint: false },
|
|
1274
|
-
inputSchema: {
|
|
1275
|
-
projectId: z
|
|
1276
|
-
.string()
|
|
1277
|
-
.max(500)
|
|
1278
|
-
.describe('Project ID (hash of project path from init_project)'),
|
|
1279
|
-
operation: z
|
|
1280
|
-
.enum(['register', 'list', 'supersede', 'search'])
|
|
1281
|
-
.describe('register: save a new decision | ' +
|
|
1282
|
-
'list: show all decisions (filter by specId via query) | ' +
|
|
1283
|
-
'supersede: mark a decision as replaced | ' +
|
|
1284
|
-
'search: full-text search in decision history'),
|
|
1285
|
-
decision: z
|
|
1286
|
-
.object({
|
|
1287
|
-
what: z.string().max(10_000).describe('What was decided (short declarative statement)'),
|
|
1288
|
-
why: z.string().max(10_000).describe('Rationale — why this option was chosen'),
|
|
1289
|
-
alternatives: z
|
|
1290
|
-
.array(z.string().max(10_000))
|
|
1291
|
-
.max(100)
|
|
1292
|
-
.optional()
|
|
1293
|
-
.describe('Alternatives that were considered but rejected'),
|
|
1294
|
-
decidedBy: z
|
|
1295
|
-
.enum(['human', 'ai', 'both'])
|
|
1296
|
-
.optional()
|
|
1297
|
-
.describe('Who made the decision (default: both)'),
|
|
1298
|
-
category: z
|
|
1299
|
-
.enum(['architecture', 'stack', 'ux', 'business', 'process'])
|
|
1300
|
-
.optional()
|
|
1301
|
-
.describe('Decision category — auto-inferred from content if omitted'),
|
|
1302
|
-
specId: z
|
|
1303
|
-
.string()
|
|
1304
|
-
.max(500)
|
|
1305
|
-
.optional()
|
|
1306
|
-
.describe('Associate decision with a specific spec (e.g. "SPEC-001")'),
|
|
1307
|
-
})
|
|
1308
|
-
.optional()
|
|
1309
|
-
.describe('Decision payload — required for operation "register"'),
|
|
1310
|
-
decisionId: z
|
|
1311
|
-
.string()
|
|
1312
|
-
.max(500)
|
|
1313
|
-
.optional()
|
|
1314
|
-
.describe('Decision ID to supersede — required for operation "supersede"'),
|
|
1315
|
-
query: z
|
|
1316
|
-
.string()
|
|
1317
|
-
.max(500)
|
|
1318
|
-
.optional()
|
|
1319
|
-
.describe('Search query for operation "search"; ' +
|
|
1320
|
-
'specId filter for operation "list" (optional)'),
|
|
1321
|
-
},
|
|
1322
|
-
}, safeLicensed('log_decision', async (args) => handleLogDecision(args)));
|
|
1323
|
-
s.registerTool('reality_check', {
|
|
1324
|
-
description: 'Evaluate whether a set of requirements is feasible given the project context. ' +
|
|
1325
|
-
'Detects impossible timelines, contradictory requirements (offline + real-time sync), ' +
|
|
1326
|
-
'performance paradoxes (fast + secure + cheap simultaneously), and scope creep. ' +
|
|
1327
|
-
'Returns a FeasibilityReport with a score 0-100, issues with alternatives, and viability flag. ' +
|
|
1328
|
-
'Score < 50 triggers a prominent warning.',
|
|
1329
|
-
annotations: { readOnlyHint: true },
|
|
1330
|
-
inputSchema: {
|
|
1331
|
-
projectId: z
|
|
1332
|
-
.string()
|
|
1333
|
-
.max(500)
|
|
1334
|
-
.describe('Project ID (hash of project path from init_project)'),
|
|
1335
|
-
requirements: z
|
|
1336
|
-
.array(z.string().max(10_000))
|
|
1337
|
-
.min(1)
|
|
1338
|
-
.max(1000)
|
|
1339
|
-
.describe('List of requirements or user stories to evaluate. ' +
|
|
1340
|
-
'Each item should describe one requirement or expectation.'),
|
|
1341
|
-
},
|
|
1342
|
-
}, safeLicensed('reality_check', async (args) => handleRealityCheck(args)));
|
|
1343
|
-
// ── Infrastructure ──────────────────────────────────────────────────────────
|
|
1344
|
-
s.registerTool('generate_infrastructure', {
|
|
1345
|
-
description: 'Generate Infrastructure-as-Code (IaC) from a spec. ' +
|
|
1346
|
-
'Analyzes spec text for infrastructure signals (database, cache, queue, storage, deployment) ' +
|
|
1347
|
-
'and generates Terraform, Docker Compose, Kubernetes manifests, or Railway config. ' +
|
|
1348
|
-
'Includes monthly cost estimates for detected services.',
|
|
1349
|
-
annotations: { readOnlyHint: true },
|
|
1350
|
-
inputSchema: {
|
|
1351
|
-
...projectIdSchema,
|
|
1352
|
-
specId: z
|
|
1353
|
-
.string()
|
|
1354
|
-
.min(1)
|
|
1355
|
-
.max(500)
|
|
1356
|
-
.describe('Spec ID to analyze for infrastructure signals (e.g. SPEC-042)'),
|
|
1357
|
-
formats: z
|
|
1358
|
-
.array(z
|
|
1359
|
-
.enum(['terraform', 'docker-compose', 'kubernetes', 'railway'])
|
|
1360
|
-
.describe('Output format: terraform | docker-compose | kubernetes | railway'))
|
|
1361
|
-
.max(4)
|
|
1362
|
-
.optional()
|
|
1363
|
-
.describe('IaC formats to generate: terraform | docker-compose | kubernetes | railway. ' +
|
|
1364
|
-
'Default: [terraform, docker-compose]'),
|
|
1365
|
-
environment: z
|
|
1366
|
-
.enum(['dev', 'staging', 'prod'])
|
|
1367
|
-
.optional()
|
|
1368
|
-
.describe('Target environment: dev | staging | prod. ' +
|
|
1369
|
-
'Affects instance sizing and cost estimates. Default: dev'),
|
|
1370
|
-
},
|
|
1371
|
-
}, safeTracked('generate_infrastructure', withProject((args) => handleGenerateInfrastructure(args))));
|
|
1372
|
-
// ── Global Rules ────────────────────────────────────────────────────────────
|
|
1373
|
-
s.registerTool('manage_global_rules', {
|
|
1374
|
-
description: 'Manage global steering rules stored in ~/.planu/global-rules/. Rules can be applied to any project CLAUDE.md to inject consistent conventions.',
|
|
1375
|
-
inputSchema: {
|
|
1376
|
-
action: z
|
|
1377
|
-
.enum(['create', 'list', 'get', 'update', 'delete', 'apply', 'export'])
|
|
1378
|
-
.describe('Action to perform. Values: create (new rule), list (all rules), get (one rule), update (modify), delete (remove), apply (inject to CLAUDE.md), export (raw content).'),
|
|
1379
|
-
slug: z
|
|
1380
|
-
.string()
|
|
1381
|
-
.optional()
|
|
1382
|
-
.describe('Rule slug — kebab-case identifier (e.g. "no-any-types"). Required for get, update, delete, apply, export.'),
|
|
1383
|
-
name: z.string().optional().describe('Human-readable rule name. Required for create.'),
|
|
1384
|
-
content: z.string().optional().describe('Rule body in Markdown. Required for create.'),
|
|
1385
|
-
tags: z
|
|
1386
|
-
.array(z.string())
|
|
1387
|
-
.optional()
|
|
1388
|
-
.describe('Optional tag list for filtering (e.g. ["typescript", "quality"]).'),
|
|
1389
|
-
author: z.string().optional().describe('Optional author identifier.'),
|
|
1390
|
-
version: z
|
|
1391
|
-
.string()
|
|
1392
|
-
.optional()
|
|
1393
|
-
.describe('Semver version string (e.g. "1.0.0"). Defaults to "1.0.0".'),
|
|
1394
|
-
projectPath: z
|
|
1395
|
-
.string()
|
|
1396
|
-
.optional()
|
|
1397
|
-
.describe('Absolute project path. Required for apply action.'),
|
|
1398
|
-
},
|
|
1399
|
-
annotations: { title: 'Manage Global Rules', destructiveHint: false },
|
|
1400
|
-
}, safeTracked('manage_global_rules', async (args) => handleManageGlobalRules(args)));
|
|
1401
|
-
s.registerTool('global_rules_status', {
|
|
1402
|
-
description: 'Show a summary of all global steering rules stored in ~/.planu/global-rules/.',
|
|
1403
|
-
inputSchema: {
|
|
1404
|
-
projectPath: z
|
|
1405
|
-
.string()
|
|
1406
|
-
.optional()
|
|
1407
|
-
.describe('Optional project path (unused, for context only).'),
|
|
1408
|
-
},
|
|
1409
|
-
annotations: { title: 'Global Rules Status', readOnlyHint: true },
|
|
1410
|
-
}, safeTracked('global_rules_status', async (args) => handleGlobalRulesStatus(args)));
|
|
1411
|
-
// ── Plan Mode ───────────────────────────────────────────────────────────────
|
|
1412
|
-
s.registerTool('plan_mode', {
|
|
1413
|
-
description: 'Generate a diff preview plan for a spec before implementing. Shows which files will be created or modified, estimated line changes, risk level, and warnings — without touching any code.',
|
|
1414
|
-
inputSchema: {
|
|
1415
|
-
specId: z.string().describe('Spec ID to plan (e.g. SPEC-473). Must be in the project.'),
|
|
1416
|
-
projectPath: z.string().describe('Absolute path to the project root.'),
|
|
1417
|
-
},
|
|
1418
|
-
annotations: { title: 'Plan Mode — Diff Preview', readOnlyHint: true },
|
|
1419
|
-
}, safeTracked('plan_mode', async (args) => handlePlanMode(args)));
|
|
1420
|
-
// ── implement_plan (migrated to skill, SPEC-658) ──────────────────────────────
|
|
1421
|
-
// Tool stays registered for backward compat; callers receive a deprecation notice.
|
|
1422
|
-
registerFromEntries(s, [
|
|
1423
|
-
makeDeprecationStub('implement_plan', '.claude/skills/implement-spec.md', 'implement-spec'),
|
|
1424
|
-
]);
|
|
1425
|
-
// ── Quality Gates ───────────────────────────────────────────────────────────
|
|
1426
|
-
s.registerTool('inject_quality_gates', {
|
|
1427
|
-
description: 'Inject security, testing, architecture, and performance quality gate criteria ' +
|
|
1428
|
-
'into a spec automatically based on the project stack and spec risk level. ' +
|
|
1429
|
-
'Gates are appended as a "## Quality Gates [AUTO]" section in the spec.md file. ' +
|
|
1430
|
-
'Use profile to control which categories are injected: ' +
|
|
1431
|
-
'strict (all 4 categories), standard (security+testing+architecture), ' +
|
|
1432
|
-
'minimal (security+testing only). ' +
|
|
1433
|
-
'Gates are filtered by spec type, risk level, and detected stack (typescript/python/java/go/generic).',
|
|
1434
|
-
annotations: { readOnlyHint: false },
|
|
1435
|
-
inputSchema: {
|
|
1436
|
-
...projectIdSchema,
|
|
1437
|
-
specId: z
|
|
1438
|
-
.string()
|
|
1439
|
-
.min(1)
|
|
1440
|
-
.max(500)
|
|
1441
|
-
.describe('Spec ID to inject quality gates into (e.g. SPEC-042)'),
|
|
1442
|
-
profile: z
|
|
1443
|
-
.enum(['strict', 'standard', 'minimal'])
|
|
1444
|
-
.optional()
|
|
1445
|
-
.describe('Gate profile: strict (all categories), standard (security+testing+architecture), ' +
|
|
1446
|
-
'minimal (security+testing only). Defaults to standard.'),
|
|
1447
|
-
categories: z
|
|
1448
|
-
.array(z.enum(['security', 'testing', 'architecture', 'performance']))
|
|
1449
|
-
.optional()
|
|
1450
|
-
.describe('Override gate categories to inject. Valid values: security, testing, architecture, performance. ' +
|
|
1451
|
-
'If omitted, profile is used.'),
|
|
1452
|
-
},
|
|
1453
|
-
}, safeLicensed('inject_quality_gates', withProject((args) => {
|
|
1454
|
-
const { projectId, specId, profile, categories } = args;
|
|
1455
|
-
return handleInjectQualityGates(projectId, specId, { profile, categories });
|
|
1456
|
-
})));
|
|
1457
|
-
// ── Verifier (empty shim — tools moved to register-compliance-tools.ts) ─────
|
|
1458
|
-
// verify_spec_compliance and compliance_score_report are registered in
|
|
1459
|
-
// group-quality-compliance.ts as shims for check_compliance(mode).
|
|
1460
|
-
// See SPEC-557 for the consolidation rationale.
|
|
1461
|
-
// ── Context Profile ─────────────────────────────────────────────────────────
|
|
1462
|
-
s.registerTool('set_context_profile', {
|
|
1463
|
-
description: 'Activate a work-phase context profile that guides the LLM to use only the ' +
|
|
1464
|
-
'relevant tools for the current phase (brainstorm/plan/implement/review/release). ' +
|
|
1465
|
-
'Reduces token waste by surfacing only the ~10 most relevant tools per phase.',
|
|
1466
|
-
inputSchema: {
|
|
1467
|
-
projectPath: projectPathField,
|
|
1468
|
-
phase: phaseField,
|
|
1469
|
-
},
|
|
1470
|
-
annotations: { readOnlyHint: false },
|
|
1471
|
-
}, safeTracked('set_context_profile', async (args) => {
|
|
1472
|
-
const { projectPath, phase } = args;
|
|
1473
|
-
await setActiveProfile(projectPath, phase);
|
|
1474
|
-
const profile = getProfile(phase);
|
|
1475
|
-
// SPEC-508: Apply real tool-group filtering via GroupManager (fire-and-forget)
|
|
1476
|
-
void import('../tool-group-handler.js')
|
|
1477
|
-
.then(({ getGroupManager }) => {
|
|
1478
|
-
const manager = getGroupManager();
|
|
1479
|
-
if (!manager) {
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
const phaseGroups = {
|
|
1483
|
-
brainstorm: {
|
|
1484
|
-
enable: ['spec-lifecycle'],
|
|
1485
|
-
disable: [
|
|
1486
|
-
'analysis',
|
|
1487
|
-
'generation',
|
|
1488
|
-
'orchestration',
|
|
1489
|
-
'devops',
|
|
1490
|
-
'governance',
|
|
1491
|
-
'learning',
|
|
1492
|
-
],
|
|
1493
|
-
},
|
|
1494
|
-
plan: {
|
|
1495
|
-
enable: ['spec-lifecycle', 'analysis'],
|
|
1496
|
-
disable: ['generation', 'orchestration', 'devops', 'governance', 'learning'],
|
|
1497
|
-
},
|
|
1498
|
-
implement: {
|
|
1499
|
-
enable: ['spec-lifecycle', 'generation'],
|
|
1500
|
-
disable: ['analysis', 'orchestration', 'devops', 'governance', 'learning'],
|
|
1501
|
-
},
|
|
1502
|
-
review: {
|
|
1503
|
-
enable: ['spec-lifecycle', 'analysis', 'governance'],
|
|
1504
|
-
disable: ['generation', 'orchestration', 'devops', 'learning'],
|
|
1505
|
-
},
|
|
1506
|
-
release: {
|
|
1507
|
-
enable: ['spec-lifecycle', 'devops'],
|
|
1508
|
-
disable: ['analysis', 'generation', 'orchestration', 'governance', 'learning'],
|
|
1509
|
-
},
|
|
1510
|
-
quickstart: {
|
|
1511
|
-
enable: ['spec-lifecycle'],
|
|
1512
|
-
disable: [
|
|
1513
|
-
'analysis',
|
|
1514
|
-
'generation',
|
|
1515
|
-
'orchestration',
|
|
1516
|
-
'devops',
|
|
1517
|
-
'governance',
|
|
1518
|
-
'learning',
|
|
1519
|
-
],
|
|
1520
|
-
},
|
|
1521
|
-
};
|
|
1522
|
-
const plan = phaseGroups[phase];
|
|
1523
|
-
for (const id of plan.enable) {
|
|
1524
|
-
manager.enableGroup(id);
|
|
1525
|
-
}
|
|
1526
|
-
for (const id of plan.disable) {
|
|
1527
|
-
manager.disableGroup(id);
|
|
1528
|
-
}
|
|
1529
|
-
})
|
|
1530
|
-
.catch(() => {
|
|
1531
|
-
/* best-effort — never blocks response */
|
|
1532
|
-
});
|
|
1533
|
-
const focusList = profile.focusTools.map((tmpl) => ` • ${tmpl}`).join('\n');
|
|
1534
|
-
const tipsList = profile.tips.map((tmpl) => ` - ${tmpl}`).join('\n');
|
|
1535
|
-
const text = `You are now in **${profile.name}**.\n\n` +
|
|
1536
|
-
`${profile.description}\n\n` +
|
|
1537
|
-
`**Focus tools (~${profile.estimatedTokensSaved}% token savings):**\n${focusList}\n\n` +
|
|
1538
|
-
`**Tips:**\n${tipsList}`;
|
|
1539
|
-
return { content: [{ type: 'text', text }] };
|
|
1540
|
-
}));
|
|
1541
|
-
s.registerTool('get_context_profile', {
|
|
1542
|
-
description: 'Get the currently active context profile for a project, ' +
|
|
1543
|
-
'including focus tools and phase-specific tips. ' +
|
|
1544
|
-
'Returns a prompt when no profile is active.',
|
|
1545
|
-
inputSchema: {
|
|
1546
|
-
projectPath: projectPathField,
|
|
1547
|
-
},
|
|
1548
|
-
annotations: { readOnlyHint: true },
|
|
1549
|
-
}, safeTracked('get_context_profile', async (args) => {
|
|
1550
|
-
const { projectPath } = args;
|
|
1551
|
-
const active = await getActiveProfile(projectPath);
|
|
1552
|
-
if (!active) {
|
|
1553
|
-
return {
|
|
1554
|
-
content: [
|
|
1555
|
-
{
|
|
1556
|
-
type: 'text',
|
|
1557
|
-
text: 'No active context profile. Use set_context_profile to activate one.\n\n' +
|
|
1558
|
-
`Available phases: ${AVAILABLE_PHASES.join(', ')}`,
|
|
1559
|
-
},
|
|
1560
|
-
],
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
const profile = getProfile(active.phase);
|
|
1564
|
-
const focusList = profile.focusTools.map((tmpl) => ` • ${tmpl}`).join('\n');
|
|
1565
|
-
const avoidList = profile.avoidTools.map((tmpl) => ` • ${tmpl}`).join('\n');
|
|
1566
|
-
const tipsList = profile.tips.map((tmpl) => ` - ${tmpl}`).join('\n');
|
|
1567
|
-
const text = `**Active profile: ${profile.name}**\n` +
|
|
1568
|
-
`Activated: ${active.activatedAt}\n\n` +
|
|
1569
|
-
`${profile.description}\n\n` +
|
|
1570
|
-
`**Focus tools:**\n${focusList}\n\n` +
|
|
1571
|
-
`**Avoid tools:**\n${avoidList}\n\n` +
|
|
1572
|
-
`**Tips:**\n${tipsList}\n\n` +
|
|
1573
|
-
`Estimated token savings: ~${profile.estimatedTokensSaved}%`;
|
|
1574
|
-
return { content: [{ type: 'text', text }] };
|
|
1575
|
-
}));
|
|
1576
|
-
s.registerTool('list_context_profiles', {
|
|
1577
|
-
description: 'List all 5 available context profiles (brainstorm/plan/implement/review/release) ' +
|
|
1578
|
-
'with their focus tools and estimated token savings. ' +
|
|
1579
|
-
'Use this to discover which profile is best for the current work.',
|
|
1580
|
-
inputSchema: {
|
|
1581
|
-
projectPath: projectPathField,
|
|
1582
|
-
},
|
|
1583
|
-
annotations: { readOnlyHint: true },
|
|
1584
|
-
}, safeTracked('list_context_profiles', async (args) => {
|
|
1585
|
-
const { projectPath } = args;
|
|
1586
|
-
const active = await getActiveProfile(projectPath);
|
|
1587
|
-
const profiles = getAllProfiles();
|
|
1588
|
-
const lines = ['**Available context profiles:**\n'];
|
|
1589
|
-
for (const p of profiles) {
|
|
1590
|
-
const isActive = active?.phase === p.phase ? ' (ACTIVE)' : '';
|
|
1591
|
-
lines.push(`### ${p.name}${isActive}`, `Phase: \`${p.phase}\``, p.description, `Focus: ${p.focusTools.slice(0, 5).join(', ')}${p.focusTools.length > 5 ? ` +${String(p.focusTools.length - 5)} more` : ''}`, `Token savings: ~${String(p.estimatedTokensSaved)}%`, '');
|
|
1592
|
-
}
|
|
1593
|
-
lines.push(`Use set_context_profile with phase=<phase> to activate one.`);
|
|
1594
|
-
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1595
|
-
}));
|
|
1596
|
-
// ── configure_permissions (SPEC-594) ─────────────────────────────────────────
|
|
1597
|
-
s.registerTool('configure_permissions', {
|
|
1598
|
-
description: 'Auto-configure Claude Code permissions in .claude/settings.json (project-level only — ' +
|
|
1599
|
-
'NEVER touches ~/.claude/settings.json). Writes an idempotent allow/deny list based on ' +
|
|
1600
|
-
'the chosen mode and the detected project stack (Vercel, Supabase, Prisma, Docker, etc.). ' +
|
|
1601
|
-
'Preserves all existing user entries; deny rules are always additive; defaultMode is never downgraded. ' +
|
|
1602
|
-
'Use dryRun:true to preview changes without writing.',
|
|
1603
|
-
inputSchema: {
|
|
1604
|
-
projectPath: z
|
|
1605
|
-
.string()
|
|
1606
|
-
.min(1)
|
|
1607
|
-
.max(4096)
|
|
1608
|
-
.describe('Absolute path to the project root. .claude/settings.json will be written here.'),
|
|
1609
|
-
mode: z
|
|
1610
|
-
.enum(['minimal', 'recommended', 'full'])
|
|
1611
|
-
.default('recommended')
|
|
1612
|
-
.describe('Permission mode: ' +
|
|
1613
|
-
'minimal=only mcp__planu__* + always-deny rules; ' +
|
|
1614
|
-
'recommended=above + dev tooling + git reads + FS reads + Claude Code native tools; ' +
|
|
1615
|
-
'full=above + Bash(*) blanket (deny still wins)'),
|
|
1616
|
-
dryRun: z
|
|
1617
|
-
.boolean()
|
|
1618
|
-
.optional()
|
|
1619
|
-
.default(false)
|
|
1620
|
-
.describe('When true, compute and return the merge plan without writing any files.'),
|
|
1621
|
-
},
|
|
1622
|
-
}, safeLicensed('configure_permissions', async (args) => {
|
|
1623
|
-
const { handleConfigurePermissions } = await import('../configure-permissions.js');
|
|
1624
|
-
const { projectPath, mode, dryRun } = args;
|
|
1625
|
-
return handleConfigurePermissions({ projectPath, mode, dryRun });
|
|
1626
|
-
}));
|
|
1627
|
-
// ── install_plugins (SPEC-596) ─────────────────────────────────────────────
|
|
1628
|
-
s.registerTool('install_plugins', {
|
|
1629
|
-
description: 'Auto-register the official Anthropic Claude Code plugin marketplace and install ' +
|
|
1630
|
-
'recommended plugins based on the detected project stack. ' +
|
|
1631
|
-
'Idempotent: already-registered marketplace and already-installed plugins are skipped. ' +
|
|
1632
|
-
'Safe when `claude` CLI is absent — skips silently with a warning. ' +
|
|
1633
|
-
'Use dryRun:true to preview the install plan without any side effects.',
|
|
1634
|
-
inputSchema: {
|
|
1635
|
-
projectPath: z
|
|
1636
|
-
.string()
|
|
1637
|
-
.min(1)
|
|
1638
|
-
.max(4096)
|
|
1639
|
-
.describe('Absolute path to the project root. Used to detect stack signals.'),
|
|
1640
|
-
mode: z
|
|
1641
|
-
.enum(['minimal', 'recommended', 'full'])
|
|
1642
|
-
.default('recommended')
|
|
1643
|
-
.describe('Installation mode: ' +
|
|
1644
|
-
'minimal=always-tier plugins only (commit-commands); ' +
|
|
1645
|
-
'recommended=above + stack-matched plugins per detected stack; ' +
|
|
1646
|
-
'full=above + all catalog plugins for detected stacks'),
|
|
1647
|
-
stackHint: z
|
|
1648
|
-
.array(z.string())
|
|
1649
|
-
.optional()
|
|
1650
|
-
.describe('Optional extra stack hints to force-include, e.g. ["vercel", "react"]. ' +
|
|
1651
|
-
'Merged with auto-detected stack signals at high confidence.'),
|
|
1652
|
-
dryRun: z
|
|
1653
|
-
.boolean()
|
|
1654
|
-
.optional()
|
|
1655
|
-
.default(false)
|
|
1656
|
-
.describe('When true, return the install plan without executing any CLI commands.'),
|
|
1657
|
-
},
|
|
1658
|
-
}, safeLicensed('install_plugins', async (args) => {
|
|
1659
|
-
const { handleInstallPlugins } = await import('../install-plugins.js');
|
|
1660
|
-
const { projectPath, mode, stackHint, dryRun } = args;
|
|
1661
|
-
return handleInstallPlugins({ projectPath, mode, stackHint, dryRun });
|
|
1662
|
-
}));
|
|
1663
|
-
// ── SPEC-966: OpenCode Host Adapter ──────────────────────────────────────────
|
|
1664
|
-
s.registerTool('opencode_host_adapter', {
|
|
1665
|
-
description: 'Detect, scaffold, or coach OpenCode workspace integration for Planu. ' +
|
|
1666
|
-
'Equivalent to Claude Code / Codex / Gemini adapters. ' +
|
|
1667
|
-
'Actions: detect (find workspace markers), scaffold (generate .opencode/rules/, .opencode/skills/, AGENTS.md), ' +
|
|
1668
|
-
'coach (list OpenCode-specific SDD rules).',
|
|
1669
|
-
inputSchema: {
|
|
1670
|
-
projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root'),
|
|
1671
|
-
action: z
|
|
1672
|
-
.enum(['detect', 'scaffold', 'coach'])
|
|
1673
|
-
.describe('Action: detect — find workspace; scaffold — generate config files; coach — list rules'),
|
|
1674
|
-
},
|
|
1675
|
-
annotations: { title: 'OpenCode Host Adapter', readOnlyHint: false },
|
|
1676
|
-
}, safeTracked('opencode_host_adapter', (args) => handleOpenCodeHostAdapter({
|
|
1677
|
-
projectPath: args.projectPath,
|
|
1678
|
-
action: args.action,
|
|
1679
|
-
})));
|
|
1680
|
-
}
|
|
1681
|
-
//# sourceMappingURL=group-platform.js.map
|